Compare commits

...

197 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
Jeremy Stretch
5c63a499d5 Merge pull request #1259 from digitalocean/develop
Release v2.0.6
2017-06-12 09:51:15 -04:00
Jeremy Stretch
3a2c5b318a Release v2.0.6 2017-06-12 09:44:09 -04:00
Jeremy Stretch
cfff69a715 Closes #1180: Simplified the process of finding related devices when viewing a device 2017-06-09 17:04:09 -04:00
Jeremy Stretch
08883d86ef Closes #913: Added headers to object CSV exports 2017-06-09 16:24:59 -04:00
Jeremy Stretch
8a849ebeff Closes #990: Enable logging configuration in configuration.py 2017-06-09 15:03:10 -04:00
Jeremy Stretch
05a796faf1 Closes #704: Allow filtering VLANs by group when editing prefixes 2017-06-09 14:15:12 -04:00
Jeremy Stretch
9e1d03b383 Formatting cleanup 2017-06-09 12:19:32 -04:00
Jeremy Stretch
0a929f2971 Fixes #1253: Improved upgrade.sh to allow forcing Python2 2017-06-09 12:13:47 -04:00
Jeremy Stretch
7878992570 First stab at an interactive shell which pre-imports all models 2017-06-08 16:38:25 -04:00
Jeremy Stretch
4f95926cbd Added utilization percetange to aggregate and prefix views 2017-06-08 12:48:49 -04:00
Jeremy Stretch
f3e997ea39 Closes #40: Added IP utilization graph to prefix list 2017-06-08 12:37:25 -04:00
Jeremy Stretch
2b921c21ff Post-release version bump 2017-06-08 10:12:39 -04:00
Jeremy Stretch
50496b1a59 Merge pull request #1251 from digitalocean/develop
Release v2.0.5
2017-06-08 10:10:41 -04:00
Jeremy Stretch
9736d63577 Release v2.0.5 2017-06-08 10:05:26 -04:00
Jeremy Stretch
13add414c4 Fixed formatting and typos 2017-06-08 10:02:16 -04:00
Jeremy Stretch
b032bc13db Merge pull request #1080 from bellwood/patch-2
Enhance LDAP documentation
2017-06-08 09:55:17 -04:00
Jeremy Stretch
aaad428438 Merge pull request #1249 from feuerrot/patch-1
upgrading.md: fix typo
2017-06-08 09:17:52 -04:00
Markus Witt
203895fc7e upgrading.md: fix typo 2017-06-08 10:58:30 +02:00
Jeremy Stretch
aab1fab445 PEP8 fix 2017-06-07 15:56:59 -04:00
Jeremy Stretch
e06221bc89 Merge branch 'import_headers' into develop 2017-06-07 15:54:59 -04:00
Jeremy Stretch
26a13edcf3 Layout tweaks 2017-06-07 15:54:48 -04:00
Jeremy Stretch
65b6fe576f Converted device fields to use FlexibleModelChoiceField; misc cleanup 2017-06-07 15:51:11 -04:00
Jeremy Stretch
4671829ad8 Removed obsolete validation from InterfaceConnectionCSVForm 2017-06-07 15:33:10 -04:00
Jeremy Stretch
293be752ca Form cleanup and fixed child device import 2017-06-07 15:30:28 -04:00
Jeremy Stretch
0a6e4f31d5 Updated CSVForm validation 2017-06-07 14:19:08 -04:00
Jeremy Stretch
e6c4ce51f7 Replaced all CSVForm ChoiceFields with CSVChoiceField 2017-06-07 13:22:06 -04:00
Jeremy Stretch
3924063060 Converted ConnectionStatusCSVField to a ChoiceField 2017-06-06 22:35:41 -04:00
Jeremy Stretch
d122f9f700 Added dynamic examples for CSV form fields 2017-06-06 17:27:26 -04:00
Brian Ellwood
d0649ba815 Update ldap.md
Wrap code in code block
2017-06-05 20:37:09 -04:00
Brian Ellwood
1ec09270a7 Update ldap.md
Capitalization
2017-06-05 20:35:05 -04:00
Jeremy Stretch
1ddd7415cb Replaced old CSVDataField 2017-06-05 16:16:23 -04:00
Jeremy Stretch
ec9d0d4008 Implemented ConnectionStatusCSVField 2017-06-05 16:12:51 -04:00
Jeremy Stretch
08c8bd3049 Renamed new import view to BulkImportView 2017-06-05 15:53:41 -04:00
Jeremy Stretch
2520d9f400 Converted console/power import views to new scheme 2017-06-05 15:53:03 -04:00
Jeremy Stretch
0e863ff9ca Converted interface connections import view to new scheme 2017-06-05 15:04:23 -04:00
Jeremy Stretch
1b78f54c6b Merge pull request #1245 from digitalocean/fix-secret-device-filter
Fixes #1244
2017-06-03 00:03:27 -04:00
Zach Moody
b732c24ec4 Fixes #1244 2017-06-02 16:33:49 -05:00
Jeremy Stretch
af604aba31 Converted secrets import view to new scheme 2017-06-02 17:23:41 -04:00
Jeremy Stretch
c82658440f Converted IPAM import views to new scheme 2017-06-02 16:07:11 -04:00
Jeremy Stretch
7e660d4d8e Converted site/rack/device import views to new scheme 2017-06-02 14:49:25 -04:00
Jeremy Stretch
4a8147f8a5 Converted circuits import views to new scheme 2017-06-02 13:40:52 -04:00
Jeremy Stretch
583830c652 #1190: Allow partial string matching when searching on custom fields 2017-06-01 16:57:32 -04:00
Jeremy Stretch
95fdb549d7 Fixes #1243: Catch ValueError in IP-based object filters 2017-06-01 16:13:07 -04:00
Jeremy Stretch
a598f0e632 Initial work on #655: CSV import headers 2017-05-31 17:40:11 -04:00
Jeremy Stretch
293dbd8a8b Fixes #1226: Improve validation for custom field values submitted via the API 2017-05-31 14:09:57 -04:00
Jeremy Stretch
f03a378ce0 Fixes #1239: Fix server error when creating VLANGroup via API 2017-05-31 11:50:03 -04:00
Jeremy Stretch
6aae8aee5b Closes #1237: Enabled setting limit=0 to disable pagination in API requests; added MAX_PAGE_SIZE configuration setting 2017-05-30 23:24:21 -04:00
Jeremy Stretch
6d908d3e79 Fixes #1236: Truncate rack names in elevations list; add facility ID 2017-05-30 13:13:01 -04:00
Jeremy Stretch
d5016c7133 Fixes #1235: Fix permission name for adding/editing inventory items 2017-05-30 13:03:25 -04:00
Jeremy Stretch
b5a1b692bd Fixes #1225: Fixed border on empty circuits table on provider view 2017-05-26 10:08:03 -04:00
Jeremy Stretch
834c396a22 Tweaked upgrade script to prefer pip3/python3 if present 2017-05-26 09:55:22 -04:00
Jeremy Stretch
bc18d241e8 Post-release version bump 2017-05-25 14:46:34 -04:00
Jeremy Stretch
f7b0d22f86 Merge pull request #1230 from digitalocean/develop
Release v2.0.4
2017-05-25 14:45:13 -04:00
Jeremy Stretch
5a1877087f Release v2.0.4 2017-05-25 14:42:58 -04:00
Jeremy Stretch
50462ec15d Added notes to discourage the prepending of arbitrary tags to issue titles 2017-05-25 14:38:33 -04:00
Jeremy Stretch
1dd5e2c926 Fixes #1229: Fix validation error on forms where API search is used 2017-05-25 14:33:50 -04:00
Jeremy Stretch
ebddc46bc0 PEP8 fix 2017-05-24 14:22:37 -04:00
Jeremy Stretch
138cbf9761 Created migrations for transition to Unicode literals 2017-05-24 14:18:52 -04:00
Jeremy Stretch
f21c6bca00 Import unicode_literals 2017-05-24 11:33:11 -04:00
Jeremy Stretch
9aad8a7774 Fixes #1219: Fix image attachment URLs when BASE_PATH is set 2017-05-24 10:34:01 -04:00
Jeremy Stretch
68b6c7d886 Fixes #1210: Fix TemplateDoesNotExist errors on browsable API views 2017-05-24 09:40:24 -04:00
Jeremy Stretch
1c489e57cc Added a warning to note "untracked migrations" warnings during an upgrade 2017-05-23 22:36:40 -04:00
Jeremy Stretch
6719578f14 Fixes #1212: Allow assigning new VLANs to global VLAN groups 2017-05-23 22:23:50 -04:00
Jeremy Stretch
d5587de316 Fixes #1213: Corrected table header ordering links 2017-05-23 22:15:13 -04:00
Jeremy Stretch
77f28e3441 Fixes #1214: Add status to list of required fields on child device import form 2017-05-23 22:12:17 -04:00
Jeremy Stretch
3fa63b774e Converted home view to a CBV 2017-05-19 16:03:51 -04:00
Jeremy Stretch
713c7cd8e3 Cleaned up 500 error template 2017-05-19 16:03:04 -04:00
Jeremy Stretch
e6b4d87939 Converted all user views to CBVs 2017-05-19 15:47:19 -04:00
Jeremy Stretch
27c94d9874 Fixes #1206: Fix redirection in admin UI after activating secret keys when BASE_PATH is set 2017-05-19 13:23:17 -04:00
Jeremy Stretch
eece8a0e26 Fixes #1207: Include nested LAG serializer when showing interface connections (API) 2017-05-19 12:59:27 -04:00
Jeremy Stretch
fb85867d72 Converted all object views to class-based views 2017-05-18 17:00:57 -04:00
Jeremy Stretch
c454bfcd84 Fixed incorrect message 2017-05-18 14:53:35 -04:00
Jeremy Stretch
769232f368 Post-release version bump 2017-05-18 14:32:11 -04:00
bellwood
5ff4e3b194 Enhance LDAP documentation
Incorporating @marvnrawley's enhancements from #518
2017-04-13 17:03:58 -04:00
261 changed files with 5886 additions and 4196 deletions

View File

@@ -45,6 +45,10 @@ sure to include:
* Any error messages generated
* Screenshots (if applicable)
* Please avoid prepending any sort of tag (e.g. "[Bug]") to the issue title.
The issue will be reviewed by a moderator after submission and the appropriate
labels will be applied.
* Keep in mind that we prioritize bugs based on their severity and how
much work is required to resolve them. It may take some time for someone
to address your issue.
@@ -91,6 +95,10 @@ following:
* Any third-party libraries or other resources which would be
involved
* Please avoid prepending any sort of tag (e.g. "[Feature]") to the issue title.
The issue will be reviewed by a moderator after submission and the appropriate
labels will be applied.
## Submitting Pull Requests
* Be sure to open an issue before starting work on a pull request, and

View File

@@ -136,3 +136,8 @@ The response will return devices 1 through 100. The URL provided in the `next` a
"results": [...]
}
```
The maximum number of objects that can be returned is limited by the [`MAX_PAGE_SIZE`](../configuration/optional-settings/#max_page_size) setting, which is 1000 by default. Setting this to `0` or `None` will remove the maximum limit. An API consumer can then pass `?limit=0` to retrieve _all_ matching objects with a single request.
!!! warning
Disabling the page size limit introduces a potential for very resource-intensive requests, since one API request can effectively retrieve an entire table from the database.

View File

@@ -83,6 +83,34 @@ Enforcement of unique IP space can be toggled on a per-VRF basis. To enforce uni
---
## LOGGING
By default, all messages of INFO severity or higher will be logged to the console. Additionally, if `DEBUG` is False and email access has been configured, ERROR and CRITICAL messages will be emailed to the users defined in `ADMINS`.
The Django framework on which NetBox runs allows for the customization of logging, e.g. to write logs to file. Please consult the [Django logging documentation](https://docs.djangoproject.com/en/1.11/topics/logging/) for more information on configuring this setting. Below is an example which will write all INFO and higher messages to a file:
```
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'file': {
'level': 'INFO',
'class': 'logging.FileHandler',
'filename': '/var/log/netbox.log',
},
},
'loggers': {
'django': {
'handlers': ['file'],
'level': 'INFO',
},
},
}
```
---
## LOGIN_REQUIRED
Default: False
@@ -99,11 +127,61 @@ Setting this to True will display a "maintenance mode" banner at the top of ever
---
## NETBOX_USERNAME
## MAX_PAGE_SIZE
## NETBOX_PASSWORD
Default: 1000
If provided, NetBox will use these credentials to authenticate against devices when collecting data.
An API consumer can request an arbitrary number of objects by appending the "limit" parameter to the URL (e.g. `?limit=1000`). This setting defines the maximum limit. Setting it to `0` or `None` will allow an API consumer to request all objects by specifying `?limit=0`.
---
## NAPALM_USERNAME
## NAPALM_PASSWORD
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]
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.

View File

@@ -1,5 +1,4 @@
This guide explains how to implement LDAP authentication using an external server. User authentication will fall back to
built-in Django users in the event of a failure.
This guide explains how to implement LDAP authentication using an external server. User authentication will fall back to built-in Django users in the event of a failure.
# Requirements
@@ -29,6 +28,9 @@ Create a file in the same directory as `configuration.py` (typically `netbox/net
## General Server Configuration
!!! info
When using Windows Server 2012 you may need to specify a port on `AUTH_LDAP_SERVER_URI`. Use `3269` for secure, or `3268` for non-secure.
```python
import ldap
@@ -52,6 +54,9 @@ LDAP_IGNORE_CERT_ERRORS = True
## User Authentication
!!! info
When using Windows Server, `2012 AUTH_LDAP_USER_DN_TEMPLATE` should be set to None.
```python
from django_auth_ldap.config import LDAPSearch
@@ -67,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.
AUTH_LDAP_USER_ATTR_MAP = {
"first_name": "givenName",
"last_name": "sn"
"last_name": "sn",
"email": "mail"
}
```
@@ -99,3 +105,7 @@ AUTH_LDAP_FIND_GROUP_PERMS = True
AUTH_LDAP_CACHE_GROUPS = True
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_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.

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
**Debian/Ubuntu**
**Ubuntu**
Python 3:
```no-highlight
# apt-get install -y python3 python3-dev python3-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev
# update-alternatives --install /usr/bin/python python /usr/bin/python3 1
```
Python 2:
@@ -15,7 +14,7 @@ Python 2:
# apt-get install -y python2.7 python-dev python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev
```
**CentOS/RHEL**
**CentOS**
Python 3:
@@ -57,13 +56,13 @@ Create the base directory for the NetBox installation. For this guide, we'll use
If `git` is not already installed, install it:
**Debian/Ubuntu**
**Ubuntu**
```no-highlight
# apt-get install -y git
```
**CentOS/RHEL**
**CentOS**
```no-highlight
# yum install -y git
@@ -98,6 +97,14 @@ Python 2:
# 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
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
Before NetBox can run, we need to install the database schema. This is done by running `./manage.py migrate` from the `netbox` directory (`/opt/netbox/netbox/` in our example):
!!! warning
The examples on the rest of this page call the `python` executable, which will be Python2 on most systems. Replace this with `python3` if you're running NetBox on Python3.
Before NetBox can run, we need to install the database schema. This is done by running `python manage.py migrate` from the `netbox` directory (`/opt/netbox/netbox/` in our example):
```no-highlight
# cd /opt/netbox/netbox/
# ./manage.py migrate
# python manage.py migrate
Operations to perform:
Apply all migrations: dcim, sessions, admin, ipam, utilities, auth, circuits, contenttypes, extras, secrets, users
Running migrations:
@@ -172,7 +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:
```no-highlight
# ./manage.py createsuperuser
# python manage.py createsuperuser
Username: admin
Email address: admin@example.com
Password:
@@ -183,7 +193,7 @@ Superuser created successfully.
# Collect Static Files
```no-highlight
# ./manage.py collectstatic --no-input
# python manage.py collectstatic --no-input
You have requested to collect static files at the destination
location as specified in your settings:
@@ -204,7 +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.
```no-highlight
# ./manage.py loaddata initial_data
# python manage.py loaddata initial_data
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:
```no-highlight
# ./manage.py runserver 0.0.0.0:8000 --insecure
# python manage.py runserver 0.0.0.0:8000 --insecure
Performing system checks...
System check identified no issues (0 silenced).

View File

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

View File

@@ -52,12 +52,27 @@ Once the new code is in place, run the upgrade script (which may need to be run
# ./upgrade.sh
```
!!! warning
The upgrade script will prefer Python3 and pip3 if both executables are available. To force it to use Python2 and pip, use the `-2` argument as below.
```no-highlight
# ./upgrade.sh -2
```
This script:
* Installs or upgrades any new required Python packages
* Applies any database migrations that were included in the release
* Collects all static files to be served by the HTTP service
!!! note
It's possible that the upgrade script will display a notice warning of unreflected database migrations:
Your models have changes that are not yet reflected in a migration, and so won't be applied.
Run 'manage.py makemigrations' to make new migrations, and then re-run 'manage.py migrate' to apply them.
This may occur due to semantic differences in environment, and can be safely ignored. Never attempt to create new migrations unless you are intentionally modifying the database schema.
# Restart the WSGI Service
Finally, restart the WSGI service to run the new code. If you followed this guide for the initial installation, this is done using `supervisorctl`:

View File

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

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

View File

@@ -1,9 +1,12 @@
from __future__ import unicode_literals
from rest_framework import serializers
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer
from extras.api.customfields import CustomFieldModelSerializer
from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ModelValidationMixin
#
@@ -42,7 +45,7 @@ class WritableProviderSerializer(CustomFieldModelSerializer):
# Circuit types
#
class CircuitTypeSerializer(serializers.ModelSerializer):
class CircuitTypeSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta:
model = CircuitType
@@ -108,7 +111,7 @@ class CircuitTerminationSerializer(serializers.ModelSerializer):
]
class WritableCircuitTerminationSerializer(serializers.ModelSerializer):
class WritableCircuitTerminationSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta:
model = CircuitTermination

View File

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
from rest_framework import routers
from . import views

View File

@@ -1,9 +1,11 @@
from django.shortcuts import get_object_or_404
from __future__ import unicode_literals
from rest_framework.decorators import detail_route
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from django.shortcuts import get_object_or_404
from circuits import filters
from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
from extras.models import Graph, GRAPH_TYPE_PROVIDER
@@ -41,6 +43,7 @@ class ProviderViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
class CircuitTypeViewSet(ModelViewSet):
queryset = CircuitType.objects.all()
serializer_class = serializers.CircuitTypeSerializer
filter_class = filters.CircuitTypeFilter
#

View File

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django.apps import AppConfig

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

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
import django_filters
from django.db.models import Q
@@ -29,7 +31,7 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta:
model = Provider
fields = ['name', 'account', 'asn']
fields = ['name', 'slug', 'asn', 'account']
def search(self, queryset, name, value):
if not value.strip():
@@ -37,10 +39,19 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
return queryset.filter(
Q(name__icontains=value) |
Q(account__icontains=value) |
Q(noc_contact__icontains=value) |
Q(admin_contact__icontains=value) |
Q(comments__icontains=value)
)
class CircuitTypeFilter(django_filters.FilterSet):
class Meta:
model = CircuitType
fields = ['name', 'slug']
class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
@@ -48,7 +59,6 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Search',
)
provider_id = django_filters.ModelMultipleChoiceFilter(
name='provider',
queryset=Provider.objects.all(),
label='Provider (ID)',
)
@@ -59,7 +69,6 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Provider (slug)',
)
type_id = django_filters.ModelMultipleChoiceFilter(
name='type',
queryset=CircuitType.objects.all(),
label='Circuit type (ID)',
)
@@ -70,7 +79,6 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Circuit type (slug)',
)
tenant_id = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
@@ -94,7 +102,7 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta:
model = Circuit
fields = ['install_date']
fields = ['cid', 'install_date', 'commit_rate']
def search(self, queryset, name, value):
if not value.strip():
@@ -109,12 +117,34 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
class CircuitTerminationFilter(django_filters.FilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
circuit_id = django_filters.ModelMultipleChoiceFilter(
name='circuit',
queryset=Circuit.objects.all(),
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:
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

@@ -1,13 +1,15 @@
from __future__ import unicode_literals
from django import forms
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 tenancy.forms import TenancyForm
from tenancy.models import Tenant
from utilities.forms import (
APISelect, BootstrapMixin, BulkImportForm, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, CSVDataField,
FilterChoiceField, Livesearch, SmallTextarea, SlugField,
APISelect, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, FilterChoiceField,
SmallTextarea, SlugField,
)
from .models import Circuit, CircuitTermination, CircuitType, Provider
@@ -37,15 +39,18 @@ class ProviderForm(BootstrapMixin, CustomFieldForm):
}
class ProviderFromCSVForm(forms.ModelForm):
class ProviderCSVForm(forms.ModelForm):
slug = SlugField()
class Meta:
model = Provider
fields = ['name', 'slug', 'asn', 'account', 'portal_url']
class ProviderImportForm(BootstrapMixin, BulkImportForm):
csv = CSVDataField(csv_form=ProviderFromCSVForm)
fields = ['name', 'slug', 'asn', 'account', 'portal_url', 'comments']
help_texts = {
'name': 'Provider name',
'asn': '32-bit autonomous system number',
'portal_url': 'Portal URL',
'comments': 'Free-form comments',
}
class ProviderBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
@@ -100,21 +105,36 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm):
}
class CircuitFromCSVForm(forms.ModelForm):
provider = forms.ModelChoiceField(Provider.objects.all(), to_field_name='name',
error_messages={'invalid_choice': 'Provider not found.'})
type = forms.ModelChoiceField(CircuitType.objects.all(), to_field_name='name',
error_messages={'invalid_choice': 'Invalid circuit type.'})
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'})
class CircuitCSVForm(forms.ModelForm):
provider = forms.ModelChoiceField(
queryset=Provider.objects.all(),
to_field_name='name',
help_text='Name of parent provider',
error_messages={
'invalid_choice': 'Provider not found.'
}
)
type = forms.ModelChoiceField(
queryset=CircuitType.objects.all(),
to_field_name='name',
help_text='Type of circuit',
error_messages={
'invalid_choice': 'Invalid circuit type.'
}
)
tenant = forms.ModelChoiceField(
queryset=Tenant.objects.all(),
required=False,
to_field_name='name',
help_text='Name of assigned tenant',
error_messages={
'invalid_choice': 'Tenant not found.'
}
)
class Meta:
model = Circuit
fields = ['cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description']
class CircuitImportForm(BootstrapMixin, BulkImportForm):
csv = CSVDataField(csv_form=CircuitFromCSVForm)
fields = ['cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description', 'comments']
class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
@@ -165,7 +185,9 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm
)
rack = ChainedModelChoiceField(
queryset=Rack.objects.all(),
chains={'site': 'site'},
chains=(
('site', 'site'),
),
required=False,
label='Rack',
widget=APISelect(
@@ -175,7 +197,10 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm
)
device = ChainedModelChoiceField(
queryset=Device.objects.all(),
chains={'site': 'site', 'rack': 'rack'},
chains=(
('site', 'site'),
('rack', 'rack'),
),
required=False,
label='Device',
widget=APISelect(
@@ -184,20 +209,13 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm
attrs={'filter-for': 'interface'}
)
)
livesearch = forms.CharField(
required=False,
label='Device',
widget=Livesearch(
query_key='q',
query_url='dcim-api:device-list',
field_to_update='device'
)
)
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'
),
chains={'device': 'device'},
chains=(
('device', 'device'),
),
required=False,
label='Interface',
widget=APISelect(
@@ -208,8 +226,10 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm
class Meta:
model = CircuitTermination
fields = ['term_side', 'site', 'rack', 'device', 'livesearch', 'interface', 'port_speed', 'upstream_speed',
'xconnect_id', 'pp_info']
fields = [
'term_side', 'site', 'rack', 'device', 'interface', 'port_speed', 'upstream_speed', 'xconnect_id',
'pp_info',
]
help_texts = {
'port_speed': "Physical circuit speed",
'xconnect_id': "ID of the local cross-connect",
@@ -224,7 +244,7 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm
# Initialize helper selectors
instance = kwargs.get('instance')
if instance and instance.interface is not None:
initial = kwargs.get('initial', {})
initial = kwargs.get('initial', {}).copy()
initial['rack'] = instance.interface.device.rack
initial['device'] = instance.interface.device
kwargs['initial'] = initial
@@ -232,6 +252,11 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm
super(CircuitTerminationForm, self).__init__(*args, **kwargs)
# Mark connected interfaces as disabled
self.fields['interface'].choices = [
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in self.fields['interface'].queryset
]
self.fields['interface'].choices = []
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

@@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2017-05-24 15:34
from __future__ import unicode_literals
import dcim.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0008_circuittermination_interface_protect_on_delete'),
]
operations = [
migrations.AlterField(
model_name='circuit',
name='cid',
field=models.CharField(max_length=50, verbose_name='Circuit ID'),
),
migrations.AlterField(
model_name='circuit',
name='commit_rate',
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)'),
),
migrations.AlterField(
model_name='circuit',
name='install_date',
field=models.DateField(blank=True, null=True, verbose_name='Date installed'),
),
migrations.AlterField(
model_name='circuittermination',
name='port_speed',
field=models.PositiveIntegerField(verbose_name='Port speed (Kbps)'),
),
migrations.AlterField(
model_name='circuittermination',
name='pp_info',
field=models.CharField(blank=True, max_length=100, verbose_name='Patch panel/port(s)'),
),
migrations.AlterField(
model_name='circuittermination',
name='term_side',
field=models.CharField(choices=[('A', 'A'), ('Z', 'Z')], max_length=1, verbose_name='Termination'),
),
migrations.AlterField(
model_name='circuittermination',
name='upstream_speed',
field=models.PositiveIntegerField(blank=True, help_text='Upstream speed, if different from port speed', null=True, verbose_name='Upstream speed (Kbps)'),
),
migrations.AlterField(
model_name='circuittermination',
name='xconnect_id',
field=models.CharField(blank=True, max_length=50, verbose_name='Cross-connect ID'),
),
migrations.AlterField(
model_name='provider',
name='account',
field=models.CharField(blank=True, max_length=30, verbose_name='Account number'),
),
migrations.AlterField(
model_name='provider',
name='admin_contact',
field=models.TextField(blank=True, verbose_name='Admin contact'),
),
migrations.AlterField(
model_name='provider',
name='asn',
field=dcim.fields.ASNField(blank=True, null=True, verbose_name='ASN'),
),
migrations.AlterField(
model_name='provider',
name='noc_contact',
field=models.TextField(blank=True, verbose_name='NOC contact'),
),
migrations.AlterField(
model_name='provider',
name='portal_url',
field=models.URLField(blank=True, verbose_name='Portal'),
),
]

View File

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django.contrib.contenttypes.fields import GenericRelation
from django.db import models
from django.urls import reverse
@@ -8,14 +10,7 @@ from extras.models import CustomFieldModel, CustomFieldValue
from tenancy.models import Tenant
from utilities.utils import csv_format
from utilities.models import CreatedUpdatedModel
TERM_SIDE_A = 'A'
TERM_SIDE_Z = 'Z'
TERM_SIDE_CHOICES = (
(TERM_SIDE_A, 'A'),
(TERM_SIDE_Z, 'Z'),
)
from .constants import *
def humanize_speed(speed):
@@ -50,6 +45,8 @@ class Provider(CreatedUpdatedModel, CustomFieldModel):
comments = models.TextField(blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url']
class Meta:
ordering = ['name']
@@ -105,12 +102,14 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
comments = models.TextField(blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
csv_headers = ['cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description']
class Meta:
ordering = ['provider', 'cid']
unique_together = ['provider', 'cid']
def __str__(self):
return u'{} {}'.format(self.provider, self.cid)
return '{} {}'.format(self.provider, self.cid)
def get_absolute_url(self):
return reverse('circuits:circuit', args=[self.pk])
@@ -166,7 +165,7 @@ class CircuitTermination(models.Model):
unique_together = ['circuit', 'term_side']
def __str__(self):
return u'{} (Side {})'.format(self.circuit, self.get_term_side_display())
return '{} (Side {})'.format(self.circuit, self.get_term_side_display())
def get_peer_termination(self):
peer_side = 'Z' if self.term_side == 'A' else 'A'

View File

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
from django.utils import timezone

View File

@@ -1,8 +1,9 @@
from __future__ import unicode_literals
import django_tables2 as tables
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
@@ -20,19 +21,18 @@ CIRCUITTYPE_ACTIONS = """
class ProviderTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn()
circuit_count = tables.Column(accessor=Accessor('count_circuits'), verbose_name='Circuits')
class Meta(BaseTable.Meta):
model = Provider
fields = ('pk', 'name', 'asn', 'account', 'circuit_count')
fields = ('pk', 'name', 'asn', 'account',)
class ProviderSearchTable(SearchTable):
name = tables.LinkColumn()
class ProviderDetailTable(ProviderTable):
circuit_count = tables.Column(accessor=Accessor('count_circuits'), verbose_name='Circuits')
class Meta(SearchTable.Meta):
class Meta(ProviderTable.Meta):
model = Provider
fields = ('name', 'asn', 'account')
fields = ('pk', 'name', 'asn', 'account', 'circuit_count')
#
@@ -73,19 +73,3 @@ class CircuitTable(BaseTable):
class Meta(BaseTable.Meta):
model = Circuit
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

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
from rest_framework import status
from rest_framework.test import APITestCase

View File

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django.conf.urls import url
from . import views
@@ -8,33 +10,33 @@ urlpatterns = [
# Providers
url(r'^providers/$', views.ProviderListView.as_view(), name='provider_list'),
url(r'^providers/add/$', views.ProviderEditView.as_view(), name='provider_add'),
url(r'^providers/add/$', views.ProviderCreateView.as_view(), name='provider_add'),
url(r'^providers/import/$', views.ProviderBulkImportView.as_view(), name='provider_import'),
url(r'^providers/edit/$', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'),
url(r'^providers/delete/$', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'),
url(r'^providers/(?P<slug>[\w-]+)/$', views.provider, name='provider'),
url(r'^providers/(?P<slug>[\w-]+)/$', views.ProviderView.as_view(), name='provider'),
url(r'^providers/(?P<slug>[\w-]+)/edit/$', views.ProviderEditView.as_view(), name='provider_edit'),
url(r'^providers/(?P<slug>[\w-]+)/delete/$', views.ProviderDeleteView.as_view(), name='provider_delete'),
# Circuit types
url(r'^circuit-types/$', views.CircuitTypeListView.as_view(), name='circuittype_list'),
url(r'^circuit-types/add/$', views.CircuitTypeEditView.as_view(), name='circuittype_add'),
url(r'^circuit-types/add/$', views.CircuitTypeCreateView.as_view(), name='circuittype_add'),
url(r'^circuit-types/delete/$', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'),
url(r'^circuit-types/(?P<slug>[\w-]+)/edit/$', views.CircuitTypeEditView.as_view(), name='circuittype_edit'),
# Circuits
url(r'^circuits/$', views.CircuitListView.as_view(), name='circuit_list'),
url(r'^circuits/add/$', views.CircuitEditView.as_view(), name='circuit_add'),
url(r'^circuits/add/$', views.CircuitCreateView.as_view(), name='circuit_add'),
url(r'^circuits/import/$', views.CircuitBulkImportView.as_view(), name='circuit_import'),
url(r'^circuits/edit/$', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'),
url(r'^circuits/delete/$', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'),
url(r'^circuits/(?P<pk>\d+)/$', views.circuit, name='circuit'),
url(r'^circuits/(?P<pk>\d+)/$', views.CircuitView.as_view(), name='circuit'),
url(r'^circuits/(?P<pk>\d+)/edit/$', views.CircuitEditView.as_view(), name='circuit_edit'),
url(r'^circuits/(?P<pk>\d+)/delete/$', views.CircuitDeleteView.as_view(), name='circuit_delete'),
url(r'^circuits/(?P<pk>\d+)/terminations/swap/$', views.circuit_terminations_swap, name='circuit_terminations_swap'),
# Circuit terminations
url(r'^circuits/(?P<circuit>\d+)/terminations/add/$', views.CircuitTerminationEditView.as_view(), name='circuittermination_add'),
url(r'^circuits/(?P<circuit>\d+)/terminations/add/$', views.CircuitTerminationCreateView.as_view(), name='circuittermination_add'),
url(r'^circuit-terminations/(?P<pk>\d+)/edit/$', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'),
url(r'^circuit-terminations/(?P<pk>\d+)/delete/$', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'),

View File

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django.contrib import messages
from django.contrib.auth.decorators import permission_required
from django.contrib.auth.mixins import PermissionRequiredMixin
@@ -5,13 +7,13 @@ from django.db import transaction
from django.db.models import Count
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.views.generic import View
from extras.models import Graph, GRAPH_TYPE_PROVIDER
from utilities.forms import ConfirmationForm
from utilities.views import (
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
)
from . import filters, forms, tables
from .models import Circuit, CircuitTermination, CircuitType, Provider, TERM_SIDE_A, TERM_SIDE_Z
@@ -24,32 +26,41 @@ class ProviderListView(ObjectListView):
queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
filter = filters.ProviderFilter
filter_form = forms.ProviderFilterForm
table = tables.ProviderTable
table = tables.ProviderDetailTable
template_name = 'circuits/provider_list.html'
def provider(request, slug):
class ProviderView(View):
provider = get_object_or_404(Provider, slug=slug)
circuits = Circuit.objects.filter(provider=provider).select_related('type', 'tenant')\
.prefetch_related('terminations__site')
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists()
def get(self, request, slug):
return render(request, 'circuits/provider.html', {
'provider': provider,
'circuits': circuits,
'show_graphs': show_graphs,
})
provider = get_object_or_404(Provider, slug=slug)
circuits = Circuit.objects.filter(provider=provider).select_related(
'type', 'tenant'
).prefetch_related(
'terminations__site'
)
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists()
return render(request, 'circuits/provider.html', {
'provider': provider,
'circuits': circuits,
'show_graphs': show_graphs,
})
class ProviderEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.change_provider'
class ProviderCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.add_provider'
model = Provider
form_class = forms.ProviderForm
template_name = 'circuits/provider_edit.html'
default_return_url = 'circuits:provider_list'
class ProviderEditView(ProviderCreateView):
permission_required = 'circuits.change_provider'
class ProviderDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'circuits.delete_provider'
model = Provider
@@ -58,9 +69,8 @@ class ProviderDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class ProviderBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'circuits.add_provider'
form = forms.ProviderImportForm
model_form = forms.ProviderCSVForm
table = tables.ProviderTable
template_name = 'circuits/provider_import.html'
default_return_url = 'circuits:provider_list'
@@ -68,8 +78,8 @@ class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'circuits.change_provider'
cls = Provider
filter = filters.ProviderFilter
table = tables.ProviderTable
form = forms.ProviderBulkEditForm
template_name = 'circuits/provider_bulk_edit.html'
default_return_url = 'circuits:provider_list'
@@ -77,6 +87,7 @@ class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_provider'
cls = Provider
filter = filters.ProviderFilter
table = tables.ProviderTable
default_return_url = 'circuits:provider_list'
@@ -90,8 +101,8 @@ class CircuitTypeListView(ObjectListView):
template_name = 'circuits/circuittype_list.html'
class CircuitTypeEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.change_circuittype'
class CircuitTypeCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.add_circuittype'
model = CircuitType
form_class = forms.CircuitTypeForm
@@ -99,9 +110,15 @@ class CircuitTypeEditView(PermissionRequiredMixin, ObjectEditView):
return reverse('circuits:circuittype_list')
class CircuitTypeEditView(CircuitTypeCreateView):
permission_required = 'circuits.change_circuittype'
class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_circuittype'
cls = CircuitType
queryset = CircuitType.objects.annotate(circuit_count=Count('circuits'))
table = tables.CircuitTypeTable
default_return_url = 'circuits:circuittype_list'
@@ -117,35 +134,41 @@ class CircuitListView(ObjectListView):
template_name = 'circuits/circuit_list.html'
def circuit(request, pk):
class CircuitView(View):
circuit = get_object_or_404(Circuit.objects.select_related('provider', 'type', 'tenant__group'), pk=pk)
termination_a = CircuitTermination.objects.select_related(
'site__region', 'interface__device'
).filter(
circuit=circuit, term_side=TERM_SIDE_A
).first()
termination_z = CircuitTermination.objects.select_related(
'site__region', 'interface__device'
).filter(
circuit=circuit, term_side=TERM_SIDE_Z
).first()
def get(self, request, pk):
return render(request, 'circuits/circuit.html', {
'circuit': circuit,
'termination_a': termination_a,
'termination_z': termination_z,
})
circuit = get_object_or_404(Circuit.objects.select_related('provider', 'type', 'tenant__group'), pk=pk)
termination_a = CircuitTermination.objects.select_related(
'site__region', 'interface__device'
).filter(
circuit=circuit, term_side=TERM_SIDE_A
).first()
termination_z = CircuitTermination.objects.select_related(
'site__region', 'interface__device'
).filter(
circuit=circuit, term_side=TERM_SIDE_Z
).first()
return render(request, 'circuits/circuit.html', {
'circuit': circuit,
'termination_a': termination_a,
'termination_z': termination_z,
})
class CircuitEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.change_circuit'
class CircuitCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.add_circuit'
model = Circuit
form_class = forms.CircuitForm
template_name = 'circuits/circuit_edit.html'
default_return_url = 'circuits:circuit_list'
class CircuitEditView(CircuitCreateView):
permission_required = 'circuits.change_circuit'
class CircuitDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'circuits.delete_circuit'
model = Circuit
@@ -154,25 +177,27 @@ class CircuitDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class CircuitBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'circuits.add_circuit'
form = forms.CircuitImportForm
model_form = forms.CircuitCSVForm
table = tables.CircuitTable
template_name = 'circuits/circuit_import.html'
default_return_url = 'circuits:circuit_list'
class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'circuits.change_circuit'
cls = Circuit
queryset = Circuit.objects.select_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
filter = filters.CircuitFilter
table = tables.CircuitTable
form = forms.CircuitBulkEditForm
template_name = 'circuits/circuit_bulk_edit.html'
default_return_url = 'circuits:circuit_list'
class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_circuit'
cls = Circuit
queryset = Circuit.objects.select_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
filter = filters.CircuitFilter
table = tables.CircuitTable
default_return_url = 'circuits:circuit_list'
@@ -225,8 +250,8 @@ def circuit_terminations_swap(request, pk):
# Circuit terminations
#
class CircuitTerminationEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.change_circuittermination'
class CircuitTerminationCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.add_circuittermination'
model = CircuitTermination
form_class = forms.CircuitTerminationForm
template_name = 'circuits/circuittermination_edit.html'
@@ -240,6 +265,10 @@ class CircuitTerminationEditView(PermissionRequiredMixin, ObjectEditView):
return obj.circuit.get_absolute_url()
class CircuitTerminationEditView(CircuitTerminationCreateView):
permission_required = 'circuits.change_circuittermination'
class CircuitTerminationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'circuits.delete_circuittermination'
model = CircuitTermination

View File

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
from rest_framework.exceptions import APIException

View File

@@ -1,17 +1,21 @@
from __future__ import unicode_literals
from collections import OrderedDict
from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator
from ipam.models import IPAddress
from circuits.models import Circuit, CircuitTermination
from dcim.models import (
CONNECTION_STATUS_CHOICES, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
DeviceBay, DeviceBayTemplate, DeviceType, DeviceRole, IFACE_FF_CHOICES, IFACE_ORDERING_CHOICES, Interface,
InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort,
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RACK_FACE_CHOICES, RACK_TYPE_CHOICES,
InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate,
PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RACK_FACE_CHOICES, RACK_TYPE_CHOICES,
RACK_WIDTH_CHOICES, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES,
)
from extras.api.customfields import CustomFieldModelSerializer
from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ChoiceFieldSerializer
from utilities.api import ChoiceFieldSerializer, ModelValidationMixin
#
@@ -34,7 +38,7 @@ class RegionSerializer(serializers.ModelSerializer):
fields = ['id', 'name', 'slug', 'parent']
class WritableRegionSerializer(serializers.ModelSerializer):
class WritableRegionSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta:
model = Region
@@ -96,7 +100,7 @@ class NestedRackGroupSerializer(serializers.ModelSerializer):
fields = ['id', 'url', 'name', 'slug']
class WritableRackGroupSerializer(serializers.ModelSerializer):
class WritableRackGroupSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta:
model = RackGroup
@@ -107,7 +111,7 @@ class WritableRackGroupSerializer(serializers.ModelSerializer):
# Rack roles
#
class RackRoleSerializer(serializers.ModelSerializer):
class RackRoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta:
model = RackRole
@@ -172,6 +176,9 @@ class WritableRackSerializer(CustomFieldModelSerializer):
validator.set_context(self)
validator(data)
# Enforce model validation
super(WritableRackSerializer, self).validate(data)
return data
@@ -209,7 +216,7 @@ class RackReservationSerializer(serializers.ModelSerializer):
fields = ['id', 'rack', 'units', 'created', 'user', 'description']
class WritableRackReservationSerializer(serializers.ModelSerializer):
class WritableRackReservationSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta:
model = RackReservation
@@ -220,7 +227,7 @@ class WritableRackReservationSerializer(serializers.ModelSerializer):
# Manufacturers
#
class ManufacturerSerializer(serializers.ModelSerializer):
class ManufacturerSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta:
model = Manufacturer
@@ -285,7 +292,7 @@ class ConsolePortTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name']
class WritableConsolePortTemplateSerializer(serializers.ModelSerializer):
class WritableConsolePortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta:
model = ConsolePortTemplate
@@ -304,7 +311,7 @@ class ConsoleServerPortTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name']
class WritableConsoleServerPortTemplateSerializer(serializers.ModelSerializer):
class WritableConsoleServerPortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta:
model = ConsoleServerPortTemplate
@@ -323,7 +330,7 @@ class PowerPortTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name']
class WritablePowerPortTemplateSerializer(serializers.ModelSerializer):
class WritablePowerPortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta:
model = PowerPortTemplate
@@ -342,7 +349,7 @@ class PowerOutletTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name']
class WritablePowerOutletTemplateSerializer(serializers.ModelSerializer):
class WritablePowerOutletTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta:
model = PowerOutletTemplate
@@ -362,7 +369,7 @@ class InterfaceTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name', 'form_factor', 'mgmt_only']
class WritableInterfaceTemplateSerializer(serializers.ModelSerializer):
class WritableInterfaceTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta:
model = InterfaceTemplate
@@ -381,7 +388,7 @@ class DeviceBayTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name']
class WritableDeviceBayTemplateSerializer(serializers.ModelSerializer):
class WritableDeviceBayTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta:
model = DeviceBayTemplate
@@ -392,7 +399,7 @@ class WritableDeviceBayTemplateSerializer(serializers.ModelSerializer):
# Device roles
#
class DeviceRoleSerializer(serializers.ModelSerializer):
class DeviceRoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta:
model = DeviceRole
@@ -411,11 +418,11 @@ class NestedDeviceRoleSerializer(serializers.ModelSerializer):
# Platforms
#
class PlatformSerializer(serializers.ModelSerializer):
class PlatformSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta:
model = Platform
fields = ['id', 'name', 'slug', 'rpc_client']
fields = ['id', 'name', 'slug', 'napalm_driver', 'rpc_client']
class NestedPlatformSerializer(serializers.ModelSerializer):
@@ -466,14 +473,10 @@ class DeviceSerializer(CustomFieldModelSerializer):
device_bay = obj.parent_bay
except DeviceBay.DoesNotExist:
return None
return {
'id': device_bay.device.pk,
'name': device_bay.device.name,
'device_bay': {
'id': device_bay.pk,
'name': device_bay.name,
}
}
context = {'request': self.context['request']}
data = NestedDeviceSerializer(instance=device_bay.device, context=context).data
data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).data
return data
class WritableDeviceSerializer(CustomFieldModelSerializer):
@@ -494,6 +497,9 @@ class WritableDeviceSerializer(CustomFieldModelSerializer):
validator.set_context(self)
validator(data)
# Enforce model validation
super(WritableDeviceSerializer, self).validate(data)
return data
@@ -510,7 +516,7 @@ class ConsoleServerPortSerializer(serializers.ModelSerializer):
read_only_fields = ['connected_console']
class WritableConsoleServerPortSerializer(serializers.ModelSerializer):
class WritableConsoleServerPortSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta:
model = ConsoleServerPort
@@ -530,7 +536,7 @@ class ConsolePortSerializer(serializers.ModelSerializer):
fields = ['id', 'device', 'name', 'cs_port', 'connection_status']
class WritableConsolePortSerializer(serializers.ModelSerializer):
class WritableConsolePortSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta:
model = ConsolePort
@@ -550,7 +556,7 @@ class PowerOutletSerializer(serializers.ModelSerializer):
read_only_fields = ['connected_port']
class WritablePowerOutletSerializer(serializers.ModelSerializer):
class WritablePowerOutletSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta:
model = PowerOutlet
@@ -570,7 +576,7 @@ class PowerPortSerializer(serializers.ModelSerializer):
fields = ['id', 'device', 'name', 'power_outlet', 'connection_status']
class WritablePowerPortSerializer(serializers.ModelSerializer):
class WritablePowerPortSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta:
model = PowerPort
@@ -589,28 +595,58 @@ class NestedInterfaceSerializer(serializers.ModelSerializer):
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):
device = NestedDeviceSerializer()
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
lag = NestedInterfaceSerializer()
connection = serializers.SerializerMethodField(read_only=True)
connected_interface = serializers.SerializerMethodField(read_only=True)
is_connected = serializers.SerializerMethodField(read_only=True)
interface_connection = serializers.SerializerMethodField(read_only=True)
circuit_termination = InterfaceCircuitTerminationSerializer()
class Meta:
model = Interface
fields = [
'id', 'device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description', 'connection',
'connected_interface',
'id', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
'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:
return NestedInterfaceConnectionSerializer(obj.connection, context=self.context).data
return None
return True
try:
circuit_termination = obj.circuit_termination
return True
except CircuitTermination.DoesNotExist:
pass
return False
def get_connected_interface(self, obj):
if obj.connected_interface:
return PeerInterfaceSerializer(obj.connected_interface, context=self.context).data
def get_interface_connection(self, obj):
if obj.connection:
return OrderedDict((
('interface', PeerInterfaceSerializer(obj.connected_interface, context=self.context).data),
('status', obj.connection.connection_status),
))
return None
@@ -618,17 +654,23 @@ class PeerInterfaceSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
device = NestedDeviceSerializer()
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
lag = NestedInterfaceSerializer()
class Meta:
model = Interface
fields = ['id', 'url', 'device', 'name', 'form_factor', '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:
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',
]
#
@@ -644,7 +686,15 @@ class DeviceBaySerializer(serializers.ModelSerializer):
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:
model = DeviceBay
@@ -661,14 +711,20 @@ class InventoryItemSerializer(serializers.ModelSerializer):
class Meta:
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:
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',
]
#
@@ -693,7 +749,7 @@ class NestedInterfaceConnectionSerializer(serializers.ModelSerializer):
fields = ['id', 'url', 'connection_status']
class WritableInterfaceConnectionSerializer(serializers.ModelSerializer):
class WritableInterfaceConnectionSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta:
model = InterfaceConnection

View File

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
from rest_framework import routers
from . import views

View File

@@ -1,10 +1,13 @@
from __future__ import unicode_literals
from collections import OrderedDict
from rest_framework.decorators import detail_route
from rest_framework.mixins import ListModelMixin
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ViewSet
from django.conf import settings
from django.http import HttpResponseBadRequest, HttpResponseForbidden
from django.shortcuts import get_object_or_404
from dcim.models import (
@@ -17,7 +20,7 @@ from dcim import filters
from extras.api.serializers import RenderedGraphSerializer
from extras.api.views import CustomFieldModelViewSet
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 . import serializers
@@ -30,6 +33,7 @@ class RegionViewSet(WritableSerializerMixin, ModelViewSet):
queryset = Region.objects.all()
serializer_class = serializers.RegionSerializer
write_serializer_class = serializers.WritableRegionSerializer
filter_class = filters.RegionFilter
#
@@ -71,6 +75,7 @@ class RackGroupViewSet(WritableSerializerMixin, ModelViewSet):
class RackRoleViewSet(ModelViewSet):
queryset = RackRole.objects.all()
serializer_class = serializers.RackRoleSerializer
filter_class = filters.RackRoleFilter
#
@@ -126,6 +131,7 @@ class RackReservationViewSet(WritableSerializerMixin, ModelViewSet):
class ManufacturerViewSet(ModelViewSet):
queryset = Manufacturer.objects.all()
serializer_class = serializers.ManufacturerSerializer
filter_class = filters.ManufacturerFilter
#
@@ -192,6 +198,7 @@ class DeviceBayTemplateViewSet(WritableSerializerMixin, ModelViewSet):
class DeviceRoleViewSet(ModelViewSet):
queryset = DeviceRole.objects.all()
serializer_class = serializers.DeviceRoleSerializer
filter_class = filters.DeviceRoleFilter
#
@@ -201,6 +208,7 @@ class DeviceRoleViewSet(ModelViewSet):
class PlatformViewSet(ModelViewSet):
queryset = Platform.objects.all()
serializer_class = serializers.PlatformSerializer
filter_class = filters.PlatformFilter
#
@@ -217,27 +225,66 @@ class DeviceViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
write_serializer_class = serializers.WritableDeviceSerializer
filter_class = filters.DeviceFilter
@detail_route(url_path='lldp-neighbors')
def lldp_neighbors(self, request, pk):
@detail_route(url_path='napalm')
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)
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()
if not RPC:
raise ServiceUnavailable("No RPC client available for this platform ({}).".format(device.platform))
# Connect to device and retrieve inventory info
# Check that NAPALM is installed and verify the configured driver
try:
with RPC(device, username=settings.NETBOX_USERNAME, password=settings.NETBOX_PASSWORD) as rpc_client:
lldp_neighbors = rpc_client.get_lldp_neighbors()
except:
raise ServiceUnavailable("Error connecting to the remote device.")
import napalm
from napalm_base.exceptions import ConnectAuthError, ModuleImportError
except ImportError:
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)
#
@@ -339,7 +386,7 @@ class ConnectedDeviceViewSet(ViewSet):
* `peer-device`: The name of the peer device
* `peer-interface`: The name of the peer interface
"""
permission_classes = [IsAuthenticated]
permission_classes = [IsAuthenticatedOrLoginNotRequired]
def get_view_name(self):
return "Connected Device Locator"

View File

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django.apps import AppConfig

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,3 +1,5 @@
from __future__ import unicode_literals
from netaddr import EUI, mac_unix_expanded
from django.core.exceptions import ValidationError

View File

@@ -1,19 +1,40 @@
from __future__ import unicode_literals
import django_filters
from netaddr import EUI
from netaddr.core import AddrFormatError
from django.contrib.auth.models import User
from django.db.models import Q
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
from utilities.filters import NullableCharFieldFilter, NullableModelMultipleChoiceFilter, NumericInFilter
from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, STATUS_CHOICES, IFACE_FF_LAG, Interface, InterfaceConnection,
InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort,
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, Region, Site, VIRTUAL_IFACE_TYPES,
InterfaceTemplate, Manufacturer, InventoryItem, NONCONNECTABLE_IFACE_TYPES, Platform, PowerOutlet,
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):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
@@ -21,23 +42,19 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Search',
)
region_id = NullableModelMultipleChoiceFilter(
name='region',
queryset=Region.objects.all(),
label='Region (ID)',
)
region = NullableModelMultipleChoiceFilter(
name='region',
queryset=Region.objects.all(),
to_field_name='slug',
label='Region (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)',
@@ -45,7 +62,7 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta:
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):
if not value.strip():
@@ -55,6 +72,9 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
Q(facility__icontains=value) |
Q(physical_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)
)
try:
@@ -66,7 +86,6 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
class RackGroupFilter(django_filters.FilterSet):
site_id = django_filters.ModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(),
label='Site (ID)',
)
@@ -79,7 +98,14 @@ class RackGroupFilter(django_filters.FilterSet):
class Meta:
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):
@@ -88,8 +114,8 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
method='search',
label='Search',
)
facility_id = NullableCharFieldFilter()
site_id = django_filters.ModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(),
label='Site (ID)',
)
@@ -100,7 +126,6 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Site (slug)',
)
group_id = NullableModelMultipleChoiceFilter(
name='group',
queryset=RackGroup.objects.all(),
label='Group (ID)',
)
@@ -111,7 +136,6 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Group',
)
tenant_id = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
@@ -122,7 +146,6 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Tenant (slug)',
)
role_id = NullableModelMultipleChoiceFilter(
name='role',
queryset=RackRole.objects.all(),
label='Role (ID)',
)
@@ -135,7 +158,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta:
model = Rack
fields = ['u_height']
fields = ['type', 'width', 'u_height', 'desc_units']
def search(self, queryset, name, value):
if not value.strip():
@@ -153,6 +176,10 @@ class RackReservationFilter(django_filters.FilterSet):
method='search',
label='Search',
)
rack_id = django_filters.ModelMultipleChoiceFilter(
queryset=Rack.objects.all(),
label='Rack (ID)',
)
site_id = django_filters.ModelMultipleChoiceFilter(
name='rack__site',
queryset=Site.objects.all(),
@@ -175,15 +202,20 @@ class RackReservationFilter(django_filters.FilterSet):
to_field_name='slug',
label='Group',
)
rack_id = django_filters.ModelMultipleChoiceFilter(
name='rack',
queryset=Rack.objects.all(),
label='Rack (ID)',
user_id = django_filters.ModelMultipleChoiceFilter(
queryset=User.objects.all(),
label='User (ID)',
)
user = django_filters.ModelMultipleChoiceFilter(
name='user',
queryset=User.objects.all(),
to_field_name='username',
label='User (name)',
)
class Meta:
model = RackReservation
fields = ['rack', 'user']
fields = ['created']
def search(self, queryset, name, value):
if not value.strip():
@@ -196,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):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
@@ -203,7 +242,6 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Search',
)
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
name='manufacturer',
queryset=Manufacturer.objects.all(),
label='Manufacturer (ID)',
)
@@ -217,7 +255,8 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta:
model = DeviceType
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):
@@ -233,16 +272,9 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
class DeviceTypeComponentFilterSet(django_filters.FilterSet):
devicetype_id = django_filters.ModelMultipleChoiceFilter(
name='device_type',
queryset=DeviceType.objects.all(),
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):
@@ -277,7 +309,7 @@ class InterfaceTemplateFilter(DeviceTypeComponentFilterSet):
class Meta:
model = InterfaceTemplate
fields = ['name', 'form_factor']
fields = ['name', 'form_factor', 'mgmt_only']
class DeviceBayTemplateFilter(DeviceTypeComponentFilterSet):
@@ -287,18 +319,75 @@ class DeviceBayTemplateFilter(DeviceTypeComponentFilterSet):
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):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
method='search',
label='Search',
)
mac_address = django_filters.CharFilter(
method='_mac_address',
label='MAC address',
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)',
)
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(
name='site',
queryset=Site.objects.all(),
label='Site (ID)',
)
@@ -318,60 +407,18 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
queryset=Rack.objects.all(),
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(
name='device_type__slug',
queryset=DeviceType.objects.all(),
to_field_name='slug',
label='Device model (slug)',
)
platform_id = NullableModelMultipleChoiceFilter(
name='platform',
queryset=Platform.objects.all(),
label='Platform (ID)',
status = django_filters.MultipleChoiceFilter(
choices=STATUS_CHOICES
)
platform = NullableModelMultipleChoiceFilter(
name='platform',
queryset=Platform.objects.all(),
to_field_name='slug',
label='Platform (slug)',
is_full_depth = django_filters.BooleanFilter(
name='device_type__is_full_depth',
label='Is full depth',
)
is_console_server = django_filters.BooleanFilter(
name='device_type__is_console_server',
@@ -385,17 +432,18 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
name='device_type__is_network_device',
label='Is a network device',
)
mac_address = django_filters.CharFilter(
method='_mac_address',
label='MAC address',
)
has_primary_ip = django_filters.BooleanFilter(
method='_has_primary_ip',
label='Has a primary IP',
)
status = django_filters.MultipleChoiceFilter(
choices=STATUS_CHOICES
)
class Meta:
model = Device
fields = ['name', 'serial', 'asset_tag']
fields = ['serial']
def search(self, queryset, name, value):
if not value.strip():
@@ -413,7 +461,8 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
if not value:
return queryset
try:
return queryset.filter(interfaces__mac_address=value).distinct()
mac = EUI(value.strip())
return queryset.filter(interfaces__mac_address=mac).distinct()
except AddrFormatError:
return queryset.none()
@@ -431,13 +480,11 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
class DeviceComponentFilterSet(django_filters.FilterSet):
device_id = django_filters.ModelMultipleChoiceFilter(
name='device',
device_id = django_filters.ModelChoiceFilter(
queryset=Device.objects.all(),
label='Device (ID)',
)
device = django_filters.ModelMultipleChoiceFilter(
name='device__name',
device = django_filters.ModelChoiceFilter(
queryset=Device.objects.all(),
to_field_name='name',
label='Device (name)',
@@ -472,7 +519,21 @@ class PowerOutletFilter(DeviceComponentFilterSet):
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(
method='filter_type',
label='Interface type',
@@ -489,24 +550,32 @@ class InterfaceFilter(DeviceComponentFilterSet):
class Meta:
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):
value = value.strip().lower()
if value == 'physical':
return queryset.exclude(form_factor__in=VIRTUAL_IFACE_TYPES)
elif value == 'virtual':
return queryset.filter(form_factor__in=VIRTUAL_IFACE_TYPES)
elif value == 'lag':
return queryset.filter(form_factor=IFACE_FF_LAG)
return queryset
return {
'physical': queryset.exclude(form_factor__in=NONCONNECTABLE_IFACE_TYPES),
'virtual': queryset.filter(form_factor__in=VIRTUAL_IFACE_TYPES),
'wireless': queryset.filter(form_factor__in=WIRELESS_IFACE_TYPES),
'lag': queryset.filter(form_factor=IFACE_FF_LAG),
}.get(value, queryset.none())
def _mac_address(self, queryset, name, value):
value = value.strip()
if not value:
return queryset
try:
return queryset.filter(mac_address=value)
mac = EUI(value.strip())
return queryset.filter(mac_address=mac)
except AddrFormatError:
return queryset.none()
@@ -519,10 +588,25 @@ class DeviceBayFilter(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:
model = InventoryItem
fields = ['name']
fields = ['name', 'part_id', 'serial', 'discovered']
class ConsoleConnectionFilter(django_filters.FilterSet):

View File

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
from netaddr import EUI, AddrFormatError
from django import forms

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,209 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2017-05-24 15:34
from __future__ import unicode_literals
import dcim.fields
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import utilities.fields
class Migration(migrations.Migration):
dependencies = [
('dcim', '0036_add_ff_juniper_vcp'),
]
operations = [
migrations.AlterField(
model_name='consoleport',
name='connection_status',
field=models.NullBooleanField(choices=[[False, 'Planned'], [True, 'Connected']], default=True),
),
migrations.AlterField(
model_name='consoleport',
name='cs_port',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='connected_console', to='dcim.ConsoleServerPort', verbose_name='Console server port'),
),
migrations.AlterField(
model_name='device',
name='asset_tag',
field=utilities.fields.NullableCharField(blank=True, help_text='A unique tag used to identify this device', max_length=50, null=True, unique=True, verbose_name='Asset tag'),
),
migrations.AlterField(
model_name='device',
name='face',
field=models.PositiveSmallIntegerField(blank=True, choices=[[0, 'Front'], [1, 'Rear']], null=True, verbose_name='Rack face'),
),
migrations.AlterField(
model_name='device',
name='position',
field=models.PositiveSmallIntegerField(blank=True, help_text='The lowest-numbered unit occupied by the device', null=True, validators=[django.core.validators.MinValueValidator(1)], verbose_name='Position (U)'),
),
migrations.AlterField(
model_name='device',
name='primary_ip4',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip4_for', to='ipam.IPAddress', verbose_name='Primary IPv4'),
),
migrations.AlterField(
model_name='device',
name='primary_ip6',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip6_for', to='ipam.IPAddress', verbose_name='Primary IPv6'),
),
migrations.AlterField(
model_name='device',
name='serial',
field=models.CharField(blank=True, max_length=50, verbose_name='Serial number'),
),
migrations.AlterField(
model_name='device',
name='status',
field=models.PositiveSmallIntegerField(choices=[[1, 'Active'], [0, 'Offline'], [2, 'Planned'], [3, 'Staged'], [4, 'Failed'], [5, 'Inventory']], default=1, verbose_name='Status'),
),
migrations.AlterField(
model_name='devicebay',
name='name',
field=models.CharField(max_length=50, verbose_name='Name'),
),
migrations.AlterField(
model_name='devicetype',
name='interface_ordering',
field=models.PositiveSmallIntegerField(choices=[[1, 'Slot/position'], [2, 'Name (alphabetically)']], default=1),
),
migrations.AlterField(
model_name='devicetype',
name='is_console_server',
field=models.BooleanField(default=False, help_text='This type of device has console server ports', verbose_name='Is a console server'),
),
migrations.AlterField(
model_name='devicetype',
name='is_full_depth',
field=models.BooleanField(default=True, help_text='Device consumes both front and rear rack faces', verbose_name='Is full depth'),
),
migrations.AlterField(
model_name='devicetype',
name='is_network_device',
field=models.BooleanField(default=True, help_text='This type of device has network interfaces', verbose_name='Is a network device'),
),
migrations.AlterField(
model_name='devicetype',
name='is_pdu',
field=models.BooleanField(default=False, help_text='This type of device has power outlets', verbose_name='Is a PDU'),
),
migrations.AlterField(
model_name='devicetype',
name='part_number',
field=models.CharField(blank=True, help_text='Discrete part number (optional)', max_length=50),
),
migrations.AlterField(
model_name='devicetype',
name='subdevice_role',
field=models.NullBooleanField(choices=[(None, 'None'), (True, 'Parent'), (False, 'Child')], default=None, help_text='Parent devices house child devices in device bays. Select "None" if this device type is neither a parent nor a child.', verbose_name='Parent/child status'),
),
migrations.AlterField(
model_name='devicetype',
name='u_height',
field=models.PositiveSmallIntegerField(default=1, verbose_name='Height (U)'),
),
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)']]], ['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)'], [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='interface',
name='lag',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='member_interfaces', to='dcim.Interface', verbose_name='Parent LAG'),
),
migrations.AlterField(
model_name='interface',
name='mac_address',
field=dcim.fields.MACAddressField(blank=True, null=True, verbose_name='MAC Address'),
),
migrations.AlterField(
model_name='interface',
name='mgmt_only',
field=models.BooleanField(default=False, help_text='This interface is used only for out-of-band management', verbose_name='OOB Management'),
),
migrations.AlterField(
model_name='interfaceconnection',
name='connection_status',
field=models.BooleanField(choices=[[False, 'Planned'], [True, 'Connected']], default=True, verbose_name='Status'),
),
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)']]], ['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)'], [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='mgmt_only',
field=models.BooleanField(default=False, verbose_name='Management only'),
),
migrations.AlterField(
model_name='inventoryitem',
name='discovered',
field=models.BooleanField(default=False, verbose_name='Discovered'),
),
migrations.AlterField(
model_name='inventoryitem',
name='name',
field=models.CharField(max_length=50, verbose_name='Name'),
),
migrations.AlterField(
model_name='inventoryitem',
name='part_id',
field=models.CharField(blank=True, max_length=50, verbose_name='Part ID'),
),
migrations.AlterField(
model_name='inventoryitem',
name='serial',
field=models.CharField(blank=True, max_length=50, verbose_name='Serial number'),
),
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='RPC client'),
),
migrations.AlterField(
model_name='powerport',
name='connection_status',
field=models.NullBooleanField(choices=[[False, 'Planned'], [True, 'Connected']], default=True),
),
migrations.AlterField(
model_name='rack',
name='desc_units',
field=models.BooleanField(default=False, help_text='Units are numbered top-to-bottom', verbose_name='Descending units'),
),
migrations.AlterField(
model_name='rack',
name='facility_id',
field=utilities.fields.NullableCharField(blank=True, max_length=30, null=True, verbose_name='Facility ID'),
),
migrations.AlterField(
model_name='rack',
name='type',
field=models.PositiveSmallIntegerField(blank=True, choices=[(100, '2-post frame'), (200, '4-post frame'), (300, '4-post cabinet'), (1000, 'Wall-mounted frame'), (1100, 'Wall-mounted cabinet')], null=True, verbose_name='Type'),
),
migrations.AlterField(
model_name='rack',
name='u_height',
field=models.PositiveSmallIntegerField(default=42, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Height (U)'),
),
migrations.AlterField(
model_name='rack',
name='width',
field=models.PositiveSmallIntegerField(choices=[(19, '19 inches'), (23, '23 inches')], default=19, help_text='Rail-to-rail width', verbose_name='Width'),
),
migrations.AlterField(
model_name='site',
name='asn',
field=dcim.fields.ASNField(blank=True, null=True, verbose_name='ASN'),
),
migrations.AlterField(
model_name='site',
name='contact_email',
field=models.EmailField(blank=True, max_length=254, verbose_name='Contact E-mail'),
),
]

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

@@ -1,3 +1,4 @@
from __future__ import unicode_literals
from collections import OrderedDict
from itertools import count, groupby
@@ -23,205 +24,10 @@ from utilities.fields import ColorField, NullableCharField
from utilities.managers import NaturalOrderByManager
from utilities.models import CreatedUpdatedModel
from utilities.utils import csv_format
from .constants import *
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
#
@@ -280,6 +86,10 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
objects = SiteManager()
csv_headers = [
'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email',
]
class Meta:
ordering = ['name']
@@ -346,7 +156,7 @@ class RackGroup(models.Model):
]
def __str__(self):
return u'{} - {}'.format(self.site.name, self.name)
return self.name
def get_absolute_url(self):
return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk)
@@ -402,6 +212,10 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
objects = RackManager()
csv_headers = [
'site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height', 'desc_units',
]
class Meta:
ordering = ['site', 'name']
unique_together = [
@@ -466,10 +280,10 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
@property
def display_name(self):
if self.facility_id:
return u"{} ({})".format(self.name, self.facility_id)
return "{} ({})".format(self.name, self.facility_id)
elif self.name:
return self.name
return u""
return ""
def get_rack_units(self, face=RACK_FACE_FRONT, exclude=None, remove_redundant=False):
"""
@@ -543,6 +357,16 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
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):
return self.devices.filter(position=0)
@@ -569,7 +393,7 @@ class RackReservation(models.Model):
ordering = ['created']
def __str__(self):
return u"Reservation for rack {}".format(self.rack)
return "Reservation for rack {}".format(self.rack)
def clean(self):
@@ -579,7 +403,7 @@ class RackReservation(models.Model):
invalid_units = [u for u in self.units if u not in self.rack.units]
if invalid_units:
raise ValidationError({
'units': u"Invalid unit(s) for {}U rack: {}".format(
'units': "Invalid unit(s) for {}U rack: {}".format(
self.rack.u_height,
', '.join([str(u) for u in invalid_units]),
),
@@ -733,7 +557,7 @@ class DeviceType(models.Model, CustomFieldModel):
@property
def full_name(self):
return u'{} {}'.format(self.manufacturer.name, self.model)
return '{} {}'.format(self.manufacturer.name, self.model)
@property
def is_parent_device(self):
@@ -808,7 +632,7 @@ class PowerOutletTemplate(models.Model):
return self.name
class InterfaceManager(models.Manager):
class InterfaceQuerySet(models.QuerySet):
def order_naturally(self, method=IFACE_ORDERING_POSITION):
"""
@@ -833,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 prescribed fields.
"""
queryset = self.get_queryset()
sql_col = '{}.name'.format(queryset.model._meta.db_table)
sql_col = '{}.name'.format(self.model._meta.db_table)
ordering = {
IFACE_ORDERING_POSITION: ('_slot', '_subslot', '_position', '_channel', '_vc', '_type', 'name'),
IFACE_ORDERING_NAME: ('_type', '_slot', '_subslot', '_position', '_channel', '_vc', 'name'),
}[method]
return queryset.extra(select={
return self.extra(select={
'_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),
'_subslot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
@@ -848,6 +671,13 @@ class InterfaceManager(models.Manager):
'_vc': "COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)".format(sql_col),
}).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
class InterfaceTemplate(models.Model):
@@ -859,7 +689,7 @@ class InterfaceTemplate(models.Model):
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS)
mgmt_only = models.BooleanField(default=False, verbose_name='Management only')
objects = InterfaceManager()
objects = InterfaceQuerySet.as_manager()
class Meta:
ordering = ['device_type', 'name']
@@ -918,7 +748,10 @@ class Platform(models.Model):
"""
name = models.CharField(max_length=50, 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:
ordering = ['name']
@@ -981,9 +814,18 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
objects = DeviceManager()
csv_headers = [
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
'site', 'rack_group', 'rack_name', 'position', 'face',
]
class Meta:
ordering = ['name']
unique_together = ['rack', 'position', 'face']
permissions = (
('napalm_read', 'Read-only access to devices via NAPALM'),
('napalm_write', 'Read/write access to devices via NAPALM'),
)
def __str__(self):
return self.display_name or super(Device, self).__str__()
@@ -1096,6 +938,7 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
self.asset_tag,
self.get_status_display(),
self.site.name,
self.rack.group.name if self.rack and self.rack.group else None,
self.rack.name if self.rack else None,
self.position,
self.get_face_display(),
@@ -1106,8 +949,8 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
if self.name:
return self.name
elif hasattr(self, 'device_type'):
return u"{}".format(self.device_type)
return u""
return "{}".format(self.device_type)
return ""
@property
def identifier(self):
@@ -1162,6 +1005,8 @@ class ConsolePort(models.Model):
verbose_name='Console server port', blank=True, null=True)
connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED)
csv_headers = ['console_server', 'cs_port', 'device', 'console_port', 'connection_status']
class Meta:
ordering = ['device', 'name']
unique_together = ['device', 'name']
@@ -1231,6 +1076,8 @@ class PowerPort(models.Model):
blank=True, null=True)
connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED)
csv_headers = ['pdu', 'power_outlet', 'device', 'power_port', 'connection_status']
class Meta:
ordering = ['device', 'name']
unique_together = ['device', 'name']
@@ -1290,16 +1137,27 @@ class Interface(models.Model):
of an InterfaceConnection.
"""
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,
verbose_name='Parent LAG')
lag = models.ForeignKey(
'self',
models.SET_NULL,
related_name='member_interfaces',
null=True,
blank=True,
verbose_name='Parent LAG'
)
name = models.CharField(max_length=30)
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')
mgmt_only = models.BooleanField(default=False, verbose_name='OOB Management',
help_text="This interface is used only for out-of-band management")
mtu = models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='MTU')
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)
objects = InterfaceManager()
objects = InterfaceQuerySet.as_manager()
class Meta:
ordering = ['device', 'name']
@@ -1311,31 +1169,31 @@ class Interface(models.Model):
def clean(self):
# 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({
'form_factor': "Virtual interfaces cannot be connected to another interface or circuit. Disconnect the "
"interface or choose a physical form factor."
'form_factor': "Virtual and wireless interfaces cannot be connected to another interface or circuit. "
"Disconnect the interface or choose a suitable form factor."
})
# An interface's LAG must belong to the same device
if self.lag and self.lag.device != self.device:
raise ValidationError({
'lag': u"The selected LAG interface ({}) belongs to a different device ({}).".format(
'lag': "The selected LAG interface ({}) belongs to a different device ({}).".format(
self.lag.name, self.lag.device.name
)
})
# 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({
'lag': u"{} 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())
})
# Only a LAG can have LAG members
if self.form_factor != IFACE_FF_LAG and self.member_interfaces.exists():
raise ValidationError({
'form_factor': "Cannot change interface form factor; it has LAG members ({}).".format(
u", ".join([iface.name for iface in self.member_interfaces.all()])
", ".join([iface.name for iface in self.member_interfaces.all()])
)
})
@@ -1343,6 +1201,10 @@ class Interface(models.Model):
def is_virtual(self):
return self.form_factor in VIRTUAL_IFACE_TYPES
@property
def is_wireless(self):
return self.form_factor in WIRELESS_IFACE_TYPES
@property
def is_lag(self):
return self.form_factor == IFACE_FF_LAG
@@ -1392,11 +1254,16 @@ class InterfaceConnection(models.Model):
connection_status = models.BooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED,
verbose_name='Status')
csv_headers = ['device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status']
def clean(self):
if self.interface_a == self.interface_b:
raise ValidationError({
'interface_b': "Cannot connect an interface to itself."
})
try:
if self.interface_a == self.interface_b:
raise ValidationError({
'interface_b': "Cannot connect an interface to itself."
})
except ObjectDoesNotExist:
pass
# Used for connections export
def to_csv(self):
@@ -1428,7 +1295,7 @@ class DeviceBay(models.Model):
unique_together = ['device', 'name']
def __str__(self):
return u'{} - {}'.format(self.device.name, self.name)
return '{} - {}'.format(self.device.name, self.name)
def clean(self):
@@ -1456,11 +1323,17 @@ class InventoryItem(models.Model):
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)
name = models.CharField(max_length=50, verbose_name='Name')
manufacturer = models.ForeignKey('Manufacturer', related_name='inventory_items', blank=True, null=True,
on_delete=models.PROTECT)
manufacturer = models.ForeignKey(
'Manufacturer', models.PROTECT, related_name='inventory_items', blank=True, null=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)
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')
description = models.CharField(max_length=100, blank=True)
class Meta:
ordering = ['device__id', 'parent__id', 'name']

View File

@@ -1,12 +1,13 @@
from __future__ import unicode_literals
import django_tables2 as tables
from django_tables2.utils import Accessor
from utilities.tables import BaseTable, SearchTable, ToggleColumn
from utilities.tables import BaseTable, ToggleColumn
from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType,
Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
RackGroup, RackReservation, Region, Site,
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutlet,
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, Region, Site,
)
@@ -141,30 +142,26 @@ class SiteTable(BaseTable):
name = tables.LinkColumn()
region = tables.TemplateColumn(template_code=SITE_REGION_LINK)
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')
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')
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')
class Meta(BaseTable.Meta):
model = Site
class Meta(SiteTable.Meta):
fields = (
'pk', 'name', 'facility', 'region', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_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
#
@@ -213,29 +210,22 @@ class RackTable(BaseTable):
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')
devices = tables.Column(accessor=Accessor('device_count'))
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
class Meta(BaseTable.Meta):
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 = (
'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):
name = tables.LinkColumn('dcim:rack', args=[Accessor('pk')], verbose_name='Name')
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
@@ -246,7 +236,7 @@ class RackImportTable(BaseTable):
class Meta(BaseTable.Meta):
model = Rack
fields = ('site', 'group', 'name', 'facility_id', 'tenant', 'u_height')
fields = ('name', 'site', 'group', 'facility_id', 'tenant', 'u_height')
#
@@ -301,23 +291,7 @@ class DeviceTypeTable(BaseTable):
model = DeviceType
fields = (
'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
'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',
'is_network_device', 'subdevice_role', 'instance_count',
)
@@ -332,7 +306,6 @@ class ConsolePortTemplateTable(BaseTable):
model = ConsolePortTemplate
fields = ('pk', 'name')
empty_text = "None"
show_header = False
class ConsoleServerPortTemplateTable(BaseTable):
@@ -342,7 +315,6 @@ class ConsoleServerPortTemplateTable(BaseTable):
model = ConsoleServerPortTemplate
fields = ('pk', 'name')
empty_text = "None"
show_header = False
class PowerPortTemplateTable(BaseTable):
@@ -352,7 +324,6 @@ class PowerPortTemplateTable(BaseTable):
model = PowerPortTemplate
fields = ('pk', 'name')
empty_text = "None"
show_header = False
class PowerOutletTemplateTable(BaseTable):
@@ -362,17 +333,16 @@ class PowerOutletTemplateTable(BaseTable):
model = PowerOutletTemplate
fields = ('pk', 'name')
empty_text = "None"
show_header = False
class InterfaceTemplateTable(BaseTable):
pk = ToggleColumn()
mgmt_only = tables.TemplateColumn("{% if value %}OOB Management{% endif %}")
class Meta(BaseTable.Meta):
model = InterfaceTemplate
fields = ('pk', 'name', 'form_factor')
fields = ('pk', 'name', 'mgmt_only', 'form_factor')
empty_text = "None"
show_header = False
class DeviceBayTemplateTable(BaseTable):
@@ -382,7 +352,6 @@ class DeviceBayTemplateTable(BaseTable):
model = DeviceBayTemplate
fields = ('pk', 'name')
empty_text = "None"
show_header = False
#
@@ -437,32 +406,22 @@ class DeviceTable(BaseTable):
'dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type',
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(
orderable=False, verbose_name='IP Address', template_code=DEVICE_PRIMARY_IP
)
class Meta(BaseTable.Meta):
class Meta(DeviceTable.Meta):
model = Device
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):
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status')
@@ -479,6 +438,52 @@ class DeviceImportTable(BaseTable):
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
#

View File

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
from rest_framework import status
from rest_framework.test import APITestCase

View File

@@ -1,4 +1,7 @@
from __future__ import unicode_literals
from django.test import TestCase
from dcim.forms import *
from dcim.models import *

View File

@@ -1,4 +1,7 @@
from __future__ import unicode_literals
from django.test import TestCase
from dcim.models import *

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django import forms
from django.contrib import admin
from django.utils.safestring import mark_safe

View File

@@ -1,10 +1,15 @@
from django.contrib.contenttypes.models import ContentType
from django.db import transaction
from __future__ import unicode_literals
from datetime import datetime
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from extras.models import CF_TYPE_SELECT, CustomField, CustomFieldChoice, CustomFieldValue
from django.contrib.contenttypes.models import ContentType
from django.db import transaction
from extras.models import (
CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_SELECT, CustomField, CustomFieldChoice, CustomFieldValue,
)
#
@@ -23,16 +28,34 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
for field_name, value in data.items():
cf = custom_fields[field_name]
# Validate custom field name
if field_name not in custom_fields:
raise ValidationError(u"Invalid custom field for {} objects: {}".format(content_type, field_name))
raise ValidationError("Invalid custom field for {} objects: {}".format(content_type, field_name))
# Validate boolean
if cf.type == CF_TYPE_BOOLEAN and value not in [True, False, 1, 0]:
raise ValidationError("Invalid value for boolean field {}: {}".format(field_name, value))
# Validate date
if cf.type == CF_TYPE_DATE:
try:
datetime.strptime(value, '%Y-%m-%d')
except ValueError:
raise ValidationError("Invalid date for field {}: {}. (Required format is YYYY-MM-DD.)".format(
field_name, value
))
# Validate selected choice
cf = custom_fields[field_name]
if cf.type == CF_TYPE_SELECT:
try:
value = int(value)
except ValueError:
raise ValidationError("{}: Choice selections must be passed as integers.".format(field_name))
valid_choices = [c.pk for c in cf.choices.all()]
if value not in valid_choices:
raise ValidationError(u"Invalid choice ({}) for field {}".format(value, field_name))
raise ValidationError("Invalid choice for field {}: {}".format(field_name, value))
# Check for missing required fields
missing_fields = []
@@ -40,7 +63,7 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
if field.required and field_name not in data:
missing_fields.append(field_name)
if missing_fields:
raise ValidationError(u"Missing required fields: {}".format(u", ".join(missing_fields)))
raise ValidationError("Missing required fields: {}".format(u", ".join(missing_fields)))
return data
@@ -85,9 +108,19 @@ class CustomFieldModelSerializer(serializers.ModelSerializer):
field=custom_field,
obj_type=content_type,
obj_id=instance.pk,
defaults={'serialized_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):
custom_fields = validated_data.pop('custom_fields', None)

View File

@@ -1,14 +1,16 @@
from rest_framework import serializers
from __future__ import unicode_literals
from django.core.exceptions import ObjectDoesNotExist
from rest_framework import serializers
from dcim.api.serializers import NestedDeviceSerializer, NestedRackSerializer, NestedSiteSerializer
from dcim.models import Device, Rack, Site
from extras.models import (
ACTION_CHOICES, ExportTemplate, Graph, GRAPH_TYPE_CHOICES, ImageAttachment, TopologyMap, UserAction,
)
from users.api.serializers import NestedUserSerializer
from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer
from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer, ModelValidationMixin
#
@@ -102,7 +104,7 @@ class ImageAttachmentSerializer(serializers.ModelSerializer):
return serializer(obj.parent, context={'request': self.context['request']}).data
class WritableImageAttachmentSerializer(serializers.ModelSerializer):
class WritableImageAttachmentSerializer(ModelValidationMixin, serializers.ModelSerializer):
content_type = ContentTypeFieldSerializer()
class Meta:
@@ -119,6 +121,9 @@ class WritableImageAttachmentSerializer(serializers.ModelSerializer):
"Invalid parent object: {} ID {}".format(data['content_type'], data['object_id'])
)
# Enforce model validation
super(WritableImageAttachmentSerializer, self).validate(data)
return data

View File

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
from rest_framework import routers
from . import views

View File

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
from rest_framework.decorators import detail_route
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet

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

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
import django_filters
from django.contrib.auth.models import User
@@ -30,7 +32,7 @@ class CustomFieldFilter(django_filters.Filter):
pass
return queryset.filter(
custom_field_values__field__name=self.name,
custom_field_values__serialized_value=value,
custom_field_values__serialized_value__icontains=value,
)

View File

@@ -1,3 +1,4 @@
from __future__ import unicode_literals
from collections import OrderedDict
from django import forms
@@ -104,7 +105,7 @@ class CustomFieldForm(forms.ModelForm):
obj_id=self.instance.pk)
except CustomFieldValue.DoesNotExist:
# Skip this field if none exists already and its value is empty
if self.cleaned_data[field_name] in [None, u'']:
if self.cleaned_data[field_name] in [None, '']:
continue
cfv = CustomFieldValue(
field=self.fields[field_name].model,

View File

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

View File

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
from getpass import getpass
from ncclient.transport.errors import AuthenticationError
from paramiko import AuthenticationException
@@ -11,8 +13,8 @@ from dcim.models import Device, InventoryItem, Site, STATUS_ACTIVE
class Command(BaseCommand):
help = "Update inventory information for specified devices"
username = settings.NETBOX_USERNAME
password = settings.NETBOX_PASSWORD
username = settings.NAPALM_USERNAME
password = settings.NAPALM_PASSWORD
def add_arguments(self, parser):
parser.add_argument('-u', '--username', dest='username', help="Specify the username to use")

View File

@@ -0,0 +1,91 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2017-05-24 15:34
from __future__ import unicode_literals
from django.db import migrations, models
import extras.models
class Migration(migrations.Migration):
dependencies = [
('extras', '0006_add_imageattachments'),
]
operations = [
migrations.AlterField(
model_name='customfield',
name='default',
field=models.CharField(blank=True, help_text='Default value for the field. Use "true" or "false" for booleans. N/A for selection fields.', max_length=100),
),
migrations.AlterField(
model_name='customfield',
name='is_filterable',
field=models.BooleanField(default=True, help_text='This field can be used to filter objects.'),
),
migrations.AlterField(
model_name='customfield',
name='label',
field=models.CharField(blank=True, help_text="Name of the field as displayed to users (if not provided, the field's name will be used)", max_length=50),
),
migrations.AlterField(
model_name='customfield',
name='obj_type',
field=models.ManyToManyField(help_text='The object(s) to which this field applies.', related_name='custom_fields', to='contenttypes.ContentType', verbose_name='Object(s)'),
),
migrations.AlterField(
model_name='customfield',
name='required',
field=models.BooleanField(default=False, help_text='Determines whether this field is required when creating new objects or editing an existing object.'),
),
migrations.AlterField(
model_name='customfield',
name='type',
field=models.PositiveSmallIntegerField(choices=[(100, 'Text'), (200, 'Integer'), (300, 'Boolean (true/false)'), (400, 'Date'), (500, 'URL'), (600, 'Selection')], default=100),
),
migrations.AlterField(
model_name='customfield',
name='weight',
field=models.PositiveSmallIntegerField(default=100, help_text='Fields with higher weights appear lower in a form'),
),
migrations.AlterField(
model_name='customfieldchoice',
name='weight',
field=models.PositiveSmallIntegerField(default=100, help_text='Higher weights appear lower in the list'),
),
migrations.AlterField(
model_name='graph',
name='link',
field=models.URLField(blank=True, verbose_name='Link URL'),
),
migrations.AlterField(
model_name='graph',
name='name',
field=models.CharField(max_length=100, verbose_name='Name'),
),
migrations.AlterField(
model_name='graph',
name='source',
field=models.CharField(max_length=500, verbose_name='Source URL'),
),
migrations.AlterField(
model_name='graph',
name='type',
field=models.PositiveSmallIntegerField(choices=[(100, 'Interface'), (200, 'Provider'), (300, 'Site')]),
),
migrations.AlterField(
model_name='imageattachment',
name='image',
field=models.ImageField(height_field='image_height', upload_to=extras.models.image_upload, width_field='image_width'),
),
migrations.AlterField(
model_name='topologymap',
name='device_patterns',
field=models.TextField(help_text='Identify devices to include in the diagram using regular expressions, one per line. Each line will result in a new tier of the drawing. Separate multiple regexes within a line using semicolons. Devices will be rendered in the order they are defined.'),
),
migrations.AlterField(
model_name='useraction',
name='action',
field=models.PositiveSmallIntegerField(choices=[(1, 'created'), (7, 'bulk created'), (2, 'imported'), (3, 'modified'), (4, 'bulk edited'), (5, 'deleted'), (6, 'bulk deleted')]),
),
]

View File

@@ -1,3 +1,4 @@
from __future__ import unicode_literals
from collections import OrderedDict
from datetime import date
import graphviz
@@ -14,62 +15,7 @@ from django.utils.encoding import python_2_unicode_compatible
from django.utils.safestring import mark_safe
from utilities.utils import foreground_color
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'),
)
from .constants import *
#
@@ -138,7 +84,11 @@ class CustomField(models.Model):
if self.type == CF_TYPE_BOOLEAN:
return str(int(bool(value)))
if self.type == CF_TYPE_DATE:
return value.strftime('%Y-%m-%d')
# Could be date/datetime object or string
try:
return value.strftime('%Y-%m-%d')
except AttributeError:
return value
if self.type == CF_TYPE_SELECT:
# Could be ModelChoiceField or TypedChoiceField
return str(value.id) if hasattr(value, 'id') else str(value)
@@ -175,7 +125,7 @@ class CustomFieldValue(models.Model):
unique_together = ['field', 'obj_type', 'obj_id']
def __str__(self):
return u'{} {}'.format(self.obj, self.field)
return '{} {}'.format(self.obj, self.field)
@property
def value(self):
@@ -269,7 +219,7 @@ class ExportTemplate(models.Model):
]
def __str__(self):
return u'{}: {}'.format(self.content_type, self.name)
return '{}: {}'.format(self.content_type, self.name)
def to_response(self, context_dict, filename):
"""
@@ -366,7 +316,8 @@ class TopologyMap(models.Model):
# Add all circuits to the graph
for termination in CircuitTermination.objects.filter(term_side='A', interface__device__in=devices):
peer_termination = termination.get_peer_termination()
if peer_termination is not None and peer_termination.interface.device in devices:
if (peer_termination is not None and peer_termination.interface is not None and
peer_termination.interface.device in devices):
graph.edge(termination.interface.device.name, peer_termination.interface.device.name, color='blue')
return graph.pipe(format=img_format)
@@ -381,13 +332,13 @@ def image_upload(instance, filename):
path = 'image-attachments/'
# Rename the file to the provided name, if any. Attempt to preserve the file extension.
extension = filename.rsplit('.')[-1]
extension = filename.rsplit('.')[-1].lower()
if instance.name and extension in ['bmp', 'gif', 'jpeg', 'jpg', 'png']:
filename = '.'.join([instance.name, extension])
elif instance.name:
filename = instance.name
return u'{}{}_{}_{}'.format(path, instance.content_type.name, instance.object_id, filename)
return '{}{}_{}_{}'.format(path, instance.content_type.name, instance.object_id, filename)
@python_2_unicode_compatible
@@ -503,8 +454,8 @@ class UserAction(models.Model):
def __str__(self):
if self.message:
return u'{} {}'.format(self.user, self.message)
return u'{} {} {}'.format(self.user, self.get_action_display(), self.content_type)
return '{} {}'.format(self.user, self.message)
return '{} {} {}'.format(self.user, self.get_action_display(), self.content_type)
def icon(self):
if self.action in [ACTION_CREATE, ACTION_BULK_CREATE, ACTION_IMPORT]:

View File

@@ -1,8 +1,10 @@
from __future__ import unicode_literals
import re
import time
from ncclient import manager
import paramiko
import re
import xmltodict
import time
CONNECT_TIMEOUT = 5 # seconds

View File

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
from rest_framework import status
from rest_framework.test import APITestCase

View File

@@ -1,3 +1,4 @@
from __future__ import unicode_literals
from datetime import date
from rest_framework import status
@@ -9,7 +10,6 @@ from django.test import TestCase
from django.urls import reverse
from dcim.models import Site
from extras.models import (
CustomField, CustomFieldValue, CustomFieldChoice, CF_TYPE_TEXT, CF_TYPE_INTEGER, CF_TYPE_BOOLEAN, CF_TYPE_DATE,
CF_TYPE_SELECT, CF_TYPE_URL,

View File

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django.conf.urls import url
from extras import views

View File

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.shortcuts import get_object_or_404
@@ -23,7 +25,7 @@ class ImageAttachmentEditView(PermissionRequiredMixin, ObjectEditView):
class ImageAttachmentDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_imageattachment'
permission_required = 'extras.delete_imageattachment'
model = ImageAttachment
def get_return_url(self, request, imageattachment):

View File

@@ -1,14 +1,17 @@
from __future__ import unicode_literals
from collections import OrderedDict
from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator
from dcim.api.serializers import NestedDeviceSerializer, InterfaceSerializer, NestedSiteSerializer
from extras.api.customfields import CustomFieldModelSerializer
from ipam.models import (
Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, IP_PROTOCOL_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role,
Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF,
Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, IP_PROTOCOL_CHOICES, Prefix,
PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF,
)
from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ChoiceFieldSerializer
from utilities.api import ChoiceFieldSerializer, ModelValidationMixin
#
@@ -20,7 +23,7 @@ class VRFSerializer(CustomFieldModelSerializer):
class Meta:
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):
@@ -42,7 +45,7 @@ class WritableVRFSerializer(CustomFieldModelSerializer):
# Roles
#
class RoleSerializer(serializers.ModelSerializer):
class RoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta:
model = Role
@@ -61,7 +64,7 @@ class NestedRoleSerializer(serializers.ModelSerializer):
# RIRs
#
class RIRSerializer(serializers.ModelSerializer):
class RIRSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta:
model = RIR
@@ -135,10 +138,13 @@ class WritableVLANGroupSerializer(serializers.ModelSerializer):
# Validate uniqueness of name and slug if a site has been assigned.
if data.get('site', None):
for field in ['name', 'slug']:
validator = UniqueTogetherValidator(queryset=VLAN.objects.all(), fields=('site', field))
validator = UniqueTogetherValidator(queryset=VLANGroup.objects.all(), fields=('site', field))
validator.set_context(self)
validator(data)
# Enforce model validation
super(WritableVLANGroupSerializer, self).validate(data)
return data
@@ -185,6 +191,9 @@ class WritableVLANSerializer(CustomFieldModelSerializer):
validator.set_context(self)
validator(data)
# Enforce model validation
super(WritableVLANSerializer, self).validate(data)
return data
@@ -234,12 +243,13 @@ class IPAddressSerializer(CustomFieldModelSerializer):
vrf = NestedVRFSerializer()
tenant = NestedTenantSerializer()
status = ChoiceFieldSerializer(choices=IPADDRESS_STATUS_CHOICES)
role = ChoiceFieldSerializer(choices=IPADDRESS_ROLE_CHOICES)
interface = InterfaceSerializer()
class Meta:
model = IPAddress
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',
]
@@ -259,7 +269,24 @@ class WritableIPAddressSerializer(CustomFieldModelSerializer):
class Meta:
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),
])
#
@@ -276,6 +303,7 @@ class ServiceSerializer(serializers.ModelSerializer):
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 Meta:

View File

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
from rest_framework import routers
from . import views

View File

@@ -1,5 +1,14 @@
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 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 import filters
from extras.api.views import CustomFieldModelViewSet
@@ -18,15 +27,6 @@ class VRFViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
filter_class = filters.VRFFilter
#
# Roles
#
class RoleViewSet(ModelViewSet):
queryset = Role.objects.all()
serializer_class = serializers.RoleSerializer
#
# RIRs
#
@@ -48,6 +48,16 @@ class AggregateViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
filter_class = filters.AggregateFilter
#
# Roles
#
class RoleViewSet(ModelViewSet):
queryset = Role.objects.all()
serializer_class = serializers.RoleSerializer
filter_class = filters.RoleFilter
#
# Prefixes
#
@@ -58,6 +68,66 @@ class PrefixViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
write_serializer_class = serializers.WritablePrefixSerializer
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

View File

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django.apps import AppConfig

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

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
from netaddr import IPNetwork
from django.core.exceptions import ValidationError

View File

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
import django_filters
from netaddr import IPNetwork
from netaddr.core import AddrFormatError
@@ -8,10 +10,9 @@ from dcim.models import Site, Device, Interface
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
from .models import (
Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN,
VLAN_STATUS_CHOICES, VLANGroup, VRF,
Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role,
Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF,
)
@@ -22,7 +23,6 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Search',
)
tenant_id = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
@@ -44,7 +44,7 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta:
model = VRF
fields = ['name', 'rd']
fields = ['name', 'rd', 'enforce_unique']
class RIRFilter(django_filters.FilterSet):
@@ -52,7 +52,7 @@ class RIRFilter(django_filters.FilterSet):
class Meta:
model = RIR
fields = ['is_private']
fields = ['name', 'slug', 'is_private']
class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
@@ -62,7 +62,6 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Search',
)
rir_id = django_filters.ModelMultipleChoiceFilter(
name='rir',
queryset=RIR.objects.all(),
label='RIR (ID)',
)
@@ -84,11 +83,18 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
try:
prefix = str(IPNetwork(value.strip()).cidr)
qs_filter |= Q(prefix__net_contains_or_equals=prefix)
except AddrFormatError:
except (AddrFormatError, ValueError):
pass
return queryset.filter(qs_filter)
class RoleFilter(django_filters.FilterSet):
class Meta:
model = Role
fields = ['name', 'slug']
class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
@@ -104,7 +110,6 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Mask length',
)
vrf_id = NullableModelMultipleChoiceFilter(
name='vrf_id',
queryset=VRF.objects.all(),
label='VRF',
)
@@ -115,7 +120,6 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='VRF (RD)',
)
tenant_id = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
@@ -126,7 +130,6 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Tenant (slug)',
)
site_id = NullableModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(),
label='Site (ID)',
)
@@ -137,7 +140,6 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Site (slug)',
)
vlan_id = NullableModelMultipleChoiceFilter(
name='vlan',
queryset=VLAN.objects.all(),
label='VLAN (ID)',
)
@@ -146,7 +148,6 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='VLAN number (1-4095)',
)
role_id = NullableModelMultipleChoiceFilter(
name='role',
queryset=Role.objects.all(),
label='Role (ID)',
)
@@ -162,7 +163,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta:
model = Prefix
fields = ['family']
fields = ['family', 'is_pool']
def search(self, queryset, name, value):
if not value.strip():
@@ -171,7 +172,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
try:
prefix = str(IPNetwork(value.strip()).cidr)
qs_filter |= Q(prefix__net_contains_or_equals=prefix)
except AddrFormatError:
except (AddrFormatError, ValueError):
pass
return queryset.filter(qs_filter)
@@ -182,7 +183,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
try:
query = str(IPNetwork(value).cidr)
return queryset.filter(prefix__net_contained_or_equal=query)
except AddrFormatError:
except (AddrFormatError, ValueError):
return queryset.none()
def filter_mask_length(self, queryset, name, value):
@@ -206,7 +207,6 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Mask length',
)
vrf_id = NullableModelMultipleChoiceFilter(
name='vrf_id',
queryset=VRF.objects.all(),
label='VRF',
)
@@ -217,7 +217,6 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='VRF (RD)',
)
tenant_id = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
@@ -239,13 +238,15 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Device (name)',
)
interface_id = django_filters.ModelMultipleChoiceFilter(
name='interface',
queryset=Interface.objects.all(),
label='Interface (ID)',
)
status = django_filters.MultipleChoiceFilter(
choices=IPADDRESS_STATUS_CHOICES
)
role = django_filters.MultipleChoiceFilter(
choices=IPADDRESS_ROLE_CHOICES
)
class Meta:
model = IPAddress
@@ -258,7 +259,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
try:
ipaddress = str(IPNetwork(value.strip()))
qs_filter |= Q(address__net_host=ipaddress)
except AddrFormatError:
except (AddrFormatError, ValueError):
pass
return queryset.filter(qs_filter)
@@ -269,7 +270,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
try:
query = str(IPNetwork(value.strip()).cidr)
return queryset.filter(address__net_host_contained=query)
except AddrFormatError:
except (AddrFormatError, ValueError):
return queryset.none()
def filter_mask_length(self, queryset, name, value):
@@ -280,7 +281,6 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
class VLANGroupFilter(django_filters.FilterSet):
site_id = NullableModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(),
label='Site (ID)',
)
@@ -293,7 +293,7 @@ class VLANGroupFilter(django_filters.FilterSet):
class Meta:
model = VLANGroup
fields = ['name']
fields = ['name', 'slug']
class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
@@ -303,7 +303,6 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Search',
)
site_id = NullableModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(),
label='Site (ID)',
)
@@ -314,7 +313,6 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Site (slug)',
)
group_id = NullableModelMultipleChoiceFilter(
name='group',
queryset=VLANGroup.objects.all(),
label='Group (ID)',
)
@@ -325,7 +323,6 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Group',
)
tenant_id = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
@@ -336,7 +333,6 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Tenant (slug)',
)
role_id = NullableModelMultipleChoiceFilter(
name='role',
queryset=Role.objects.all(),
label='Role (ID)',
)
@@ -352,7 +348,7 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta:
model = VLAN
fields = ['name', 'vid']
fields = ['vid', 'name']
def search(self, queryset, name, value):
if not value.strip():
@@ -367,7 +363,6 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
class ServiceFilter(django_filters.FilterSet):
device_id = django_filters.ModelMultipleChoiceFilter(
name='device',
queryset=Device.objects.all(),
label='Device (ID)',
)

View File

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
from netaddr import IPNetwork, AddrFormatError
from django import forms

View File

@@ -1,5 +1,7 @@
from __future__ import unicode_literals
from django import forms
from django.core.exceptions import ValidationError
from django.core.exceptions import MultipleObjectsReturned
from django.db.models import Count
from dcim.models import Site, Rack, Device, Interface
@@ -7,13 +9,13 @@ from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFi
from tenancy.forms import TenancyForm
from tenancy.models import Tenant
from utilities.forms import (
APISelect, BootstrapMixin, BulkEditNullBooleanSelect, BulkImportForm, ChainedModelChoiceField, CSVDataField,
ExpandableIPAddressField, FilterChoiceField, Livesearch, ReturnURLForm, SlugField, add_blank_choice,
APISelect, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField, CSVChoiceField,
ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, ReturnURLForm, SlugField,
add_blank_choice,
)
from .models import (
Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN,
VLANGroup, VLAN_STATUS_CHOICES, VRF,
Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role,
Service, VLAN, VLANGroup, VLAN_STATUS_CHOICES, VRF,
)
@@ -47,17 +49,23 @@ class VRFForm(BootstrapMixin, TenancyForm, CustomFieldForm):
}
class VRFFromCSVForm(forms.ModelForm):
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'})
class VRFCSVForm(forms.ModelForm):
tenant = forms.ModelChoiceField(
queryset=Tenant.objects.all(),
required=False,
to_field_name='name',
help_text='Name of assigned tenant',
error_messages={
'invalid_choice': 'Tenant not found.',
}
)
class Meta:
model = VRF
fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
class VRFImportForm(BootstrapMixin, BulkImportForm):
csv = CSVDataField(csv_form=VRFFromCSVForm)
help_texts = {
'name': 'VRF name',
}
class VRFBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
@@ -115,19 +123,21 @@ class AggregateForm(BootstrapMixin, CustomFieldForm):
}
class AggregateFromCSVForm(forms.ModelForm):
rir = forms.ModelChoiceField(queryset=RIR.objects.all(), to_field_name='name',
error_messages={'invalid_choice': 'RIR not found.'})
class AggregateCSVForm(forms.ModelForm):
rir = forms.ModelChoiceField(
queryset=RIR.objects.all(),
to_field_name='name',
help_text='Name of parent RIR',
error_messages={
'invalid_choice': 'RIR not found.',
}
)
class Meta:
model = Aggregate
fields = ['prefix', 'rir', 'date_added', 'description']
class AggregateImportForm(BootstrapMixin, BulkImportForm):
csv = CSVDataField(csv_form=AggregateFromCSVForm)
class AggregateBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
pk = forms.ModelMultipleChoiceField(queryset=Aggregate.objects.all(), widget=forms.MultipleHiddenInput)
rir = forms.ModelChoiceField(queryset=RIR.objects.all(), required=False, label='RIR')
@@ -167,13 +177,35 @@ class RoleForm(BootstrapMixin, forms.ModelForm):
class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
site = forms.ModelChoiceField(
queryset=Site.objects.all(), required=False, label='Site', widget=forms.Select(
queryset=Site.objects.all(),
required=False,
label='Site',
widget=forms.Select(
attrs={'filter-for': 'vlan_group', 'nullable': 'true'}
)
)
vlan_group = ChainedModelChoiceField(
queryset=VLANGroup.objects.all(),
chains=(
('site', 'site'),
),
required=False,
label='VLAN group',
widget=APISelect(
api_url='/api/ipam/vlan-groups/?site_id={{site}}',
attrs={'filter-for': 'vlan', 'nullable': 'true'}
)
)
vlan = ChainedModelChoiceField(
queryset=VLAN.objects.all(), chains={'site': 'site'}, required=False, label='VLAN', widget=APISelect(
api_url='/api/ipam/vlans/?site_id={{site}}', display_field='display_name'
queryset=VLAN.objects.all(),
chains=(
('site', 'site'),
('group', 'vlan_group'),
),
required=False,
label='VLAN',
widget=APISelect(
api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}', display_field='display_name'
)
)
@@ -182,74 +214,108 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
fields = ['prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'description', 'tenant_group', 'tenant']
def __init__(self, *args, **kwargs):
# Initialize helper selectors
instance = kwargs.get('instance')
initial = kwargs.get('initial', {}).copy()
if instance and instance.vlan is not None:
initial['vlan_group'] = instance.vlan.group
kwargs['initial'] = initial
super(PrefixForm, self).__init__(*args, **kwargs)
self.fields['vrf'].empty_label = 'Global'
class PrefixFromCSVForm(forms.ModelForm):
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
error_messages={'invalid_choice': 'VRF not found.'})
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'})
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Site not found.'})
vlan_group_name = forms.CharField(required=False)
vlan_vid = forms.IntegerField(required=False)
status = forms.CharField()
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Invalid role.'})
class PrefixCSVForm(forms.ModelForm):
vrf = forms.ModelChoiceField(
queryset=VRF.objects.all(),
required=False,
to_field_name='rd',
help_text='Route distinguisher of parent VRF',
error_messages={
'invalid_choice': 'VRF not found.',
}
)
tenant = forms.ModelChoiceField(
queryset=Tenant.objects.all(),
required=False,
to_field_name='name',
help_text='Name of assigned tenant',
error_messages={
'invalid_choice': 'Tenant not found.',
}
)
site = forms.ModelChoiceField(
queryset=Site.objects.all(),
required=False,
to_field_name='name',
help_text='Name of parent site',
error_messages={
'invalid_choice': 'Site not found.',
}
)
vlan_group = forms.CharField(
help_text='Group name of assigned VLAN',
required=False
)
vlan_vid = forms.IntegerField(
help_text='Numeric ID of assigned VLAN',
required=False
)
status = CSVChoiceField(
choices=PREFIX_STATUS_CHOICES,
help_text='Operational status'
)
role = forms.ModelChoiceField(
queryset=Role.objects.all(),
required=False,
to_field_name='name',
help_text='Functional role',
error_messages={
'invalid_choice': 'Invalid role.',
}
)
class Meta:
model = Prefix
fields = [
'prefix', 'vrf', 'tenant', 'site', 'vlan_group_name', 'vlan_vid', 'status', 'role', 'is_pool',
'description',
'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description',
]
def clean(self):
super(PrefixFromCSVForm, self).clean()
super(PrefixCSVForm, self).clean()
site = self.cleaned_data.get('site')
vlan_group_name = self.cleaned_data.get('vlan_group_name')
vlan_group = self.cleaned_data.get('vlan_group')
vlan_vid = self.cleaned_data.get('vlan_vid')
vlan_group = None
# Validate VLAN group
if vlan_group_name:
try:
vlan_group = VLANGroup.objects.get(site=site, name=vlan_group_name)
except VLANGroup.DoesNotExist:
if site:
self.add_error('vlan_group_name', "Invalid VLAN group ({} - {}).".format(site, vlan_group_name))
else:
self.add_error('vlan_group_name', "Invalid global VLAN group ({}).".format(vlan_group_name))
# Validate VLAN
if vlan_vid:
if vlan_group and vlan_vid:
try:
self.instance.vlan = VLAN.objects.get(site=site, group=vlan_group, vid=vlan_vid)
self.instance.vlan = VLAN.objects.get(site=site, group__name=vlan_group, vid=vlan_vid)
except VLAN.DoesNotExist:
if site:
self.add_error('vlan_vid', "Invalid VLAN ID ({}) for site {}.".format(vlan_vid, site))
elif vlan_group:
self.add_error('vlan_vid', "Invalid VLAN ID ({}) for group {}.".format(vlan_vid, vlan_group_name))
elif not vlan_group_name:
self.add_error('vlan_vid', "Invalid global VLAN ID ({}).".format(vlan_vid))
except VLAN.MultipleObjectsReturned:
self.add_error('vlan_vid', "Multiple VLANs found ({} - VID {})".format(site, vlan_vid))
def clean_status(self):
status_choices = {s[1].lower(): s[0] for s in PREFIX_STATUS_CHOICES}
try:
return status_choices[self.cleaned_data['status'].lower()]
except KeyError:
raise ValidationError("Invalid status: {}".format(self.cleaned_data['status']))
class PrefixImportForm(BootstrapMixin, BulkImportForm):
csv = CSVDataField(csv_form=PrefixFromCSVForm)
raise forms.ValidationError("VLAN {} not found in site {} group {}".format(
vlan_vid, site, vlan_group
))
else:
raise forms.ValidationError("Global VLAN {} not found in group {}".format(vlan_vid, vlan_group))
except MultipleObjectsReturned:
raise forms.ValidationError(
"Multiple VLANs with VID {} found in group {}".format(vlan_vid, vlan_group)
)
elif vlan_vid:
try:
self.instance.vlan = VLAN.objects.get(site=site, group__isnull=True, vid=vlan_vid)
except VLAN.DoesNotExist:
if site:
raise forms.ValidationError("VLAN {} not found in site {}".format(vlan_vid, site))
else:
raise forms.ValidationError("Global VLAN {} not found".format(vlan_vid))
except MultipleObjectsReturned:
raise forms.ValidationError("Multiple VLANs with VID {} found".format(vlan_vid))
class PrefixBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
@@ -270,7 +336,7 @@ def prefix_status_choices():
status_counts = {}
for status in Prefix.objects.values('status').annotate(count=Count('status')).order_by('status'):
status_counts[status['status']] = status['count']
return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in PREFIX_STATUS_CHOICES]
return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in PREFIX_STATUS_CHOICES]
class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
@@ -321,7 +387,9 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
)
interface_rack = ChainedModelChoiceField(
queryset=Rack.objects.all(),
chains={'site': 'interface_site'},
chains=(
('site', 'interface_site'),
),
required=False,
label='Rack',
widget=APISelect(
@@ -332,7 +400,10 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
)
interface_device = ChainedModelChoiceField(
queryset=Device.objects.all(),
chains={'site': 'interface_site', 'rack': 'interface_rack'},
chains=(
('site', 'interface_site'),
('rack', 'interface_rack'),
),
required=False,
label='Device',
widget=APISelect(
@@ -343,7 +414,9 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
)
interface = ChainedModelChoiceField(
queryset=Interface.objects.all(),
chains={'device': 'interface_device'},
chains=(
('device', 'interface_device'),
),
required=False,
widget=APISelect(
api_url='/api/dcim/interfaces/?device_id={{interface_device}}'
@@ -354,34 +427,41 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
required=False,
label='Site',
widget=forms.Select(
attrs={'filter-for': 'nat_device'}
attrs={'filter-for': 'nat_rack'}
)
)
nat_rack = ChainedModelChoiceField(
queryset=Rack.objects.all(),
chains={'site': 'nat_site'},
chains=(
('site', 'nat_site'),
),
required=False,
label='Rack',
widget=APISelect(
api_url='/api/dcim/racks/?site_id={{interface_site}}',
api_url='/api/dcim/racks/?site_id={{nat_site}}',
display_field='display_name',
attrs={'filter-for': 'nat_device', 'nullable': 'true'}
)
)
nat_device = ChainedModelChoiceField(
queryset=Device.objects.all(),
chains={'site': 'nat_site'},
chains=(
('site', 'nat_site'),
('rack', 'nat_rack'),
),
required=False,
label='Device',
widget=APISelect(
api_url='/api/dcim/devices/?site_id={{nat_site}}',
api_url='/api/dcim/devices/?site_id={{nat_site}}&rack_id={{nat_rack}}',
display_field='display_name',
attrs={'filter-for': 'nat_inside'}
)
)
nat_inside = ChainedModelChoiceField(
queryset=IPAddress.objects.all(),
chains={'interface__device': 'nat_device'},
chains=(
('interface__device', 'nat_device'),
),
required=False,
label='IP Address',
widget=APISelect(
@@ -391,7 +471,7 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
)
livesearch = forms.CharField(
required=False,
label='IP Address',
label='Search',
widget=Livesearch(
query_key='q',
query_url='ipam-api:ipaddress-list',
@@ -404,20 +484,20 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
class Meta:
model = IPAddress
fields = [
'address', 'vrf', 'status', 'description', 'interface', 'primary_for_device', 'nat_inside', 'tenant_group',
'tenant',
'address', 'vrf', 'status', 'role', 'description', 'interface', 'primary_for_device', 'nat_site', 'nat_rack',
'nat_inside', 'tenant_group', 'tenant',
]
def __init__(self, *args, **kwargs):
# Initialize helper selectors
instance = kwargs.get('instance')
initial = kwargs.get('initial', {})
initial = kwargs.get('initial', {}).copy()
if instance and instance.interface is not None:
initial['interface_site'] = instance.interface.device.site
initial['interface_rack'] = instance.interface.device.rack
initial['interface_device'] = instance.interface.device
if instance and instance.nat_inside is not None:
if instance and instance.nat_inside and instance.nat_inside.device is not None:
initial['nat_site'] = instance.nat_inside.device.site
initial['nat_rack'] = instance.nat_inside.device.rack
initial['nat_device'] = instance.nat_inside.device
@@ -482,30 +562,67 @@ class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldForm):
class Meta:
model = IPAddress
fields = ['address', 'status', 'vrf', 'description', 'tenant_group', 'tenant']
fields = ['address', 'vrf', 'status', 'role', 'description', 'tenant_group', 'tenant']
def __init__(self, *args, **kwargs):
super(IPAddressBulkAddForm, self).__init__(*args, **kwargs)
self.fields['vrf'].empty_label = 'Global'
class IPAddressFromCSVForm(forms.ModelForm):
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
error_messages={'invalid_choice': 'VRF not found.'})
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'})
status = forms.CharField()
device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Device not found.'})
interface_name = forms.CharField(required=False)
is_primary = forms.BooleanField(required=False)
class IPAddressCSVForm(forms.ModelForm):
vrf = forms.ModelChoiceField(
queryset=VRF.objects.all(),
required=False,
to_field_name='rd',
help_text='Route distinguisher of the assigned VRF',
error_messages={
'invalid_choice': 'VRF not found.',
}
)
tenant = forms.ModelChoiceField(
queryset=Tenant.objects.all(),
to_field_name='name',
required=False,
help_text='Name of the assigned tenant',
error_messages={
'invalid_choice': 'Tenant not found.',
}
)
status = CSVChoiceField(
choices=IPADDRESS_STATUS_CHOICES,
help_text='Operational status'
)
role = CSVChoiceField(
choices=IPADDRESS_ROLE_CHOICES,
required=False,
help_text='Functional role'
)
device = FlexibleModelChoiceField(
queryset=Device.objects.all(),
required=False,
to_field_name='name',
help_text='Name or ID of assigned device',
error_messages={
'invalid_choice': 'Device not found.',
}
)
interface_name = forms.CharField(
help_text='Name of assigned interface',
required=False
)
is_primary = forms.BooleanField(
help_text='Make this the primary IP for the assigned device',
required=False
)
class Meta:
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):
super(IPAddressCSVForm, self).clean()
device = self.cleaned_data.get('device')
interface_name = self.cleaned_data.get('interface_name')
is_primary = self.cleaned_data.get('is_primary')
@@ -513,43 +630,39 @@ class IPAddressFromCSVForm(forms.ModelForm):
# Validate interface
if device and interface_name:
try:
Interface.objects.get(device=device, name=interface_name)
self.instance.interface = Interface.objects.get(device=device, name=interface_name)
except Interface.DoesNotExist:
self.add_error('interface_name', "Invalid interface ({}) for {}".format(interface_name, device))
raise forms.ValidationError("Invalid interface {} for device {}".format(interface_name, device))
elif device and not interface_name:
self.add_error('interface_name', "Device set ({}) but interface missing".format(device))
raise forms.ValidationError("Device set ({}) but interface missing".format(device))
elif interface_name and not device:
self.add_error('device', "Interface set ({}) but device missing or invalid".format(interface_name))
raise forms.ValidationError("Interface set ({}) but device missing or invalid".format(interface_name))
# Validate is_primary
if is_primary and not device:
self.add_error('is_primary', "No device specified; cannot set as primary IP")
def clean_status(self):
status_choices = {s[1].lower(): s[0] for s in IPADDRESS_STATUS_CHOICES}
try:
return status_choices[self.cleaned_data['status'].lower()]
except KeyError:
raise ValidationError("Invalid status: {}".format(self.cleaned_data['status']))
raise forms.ValidationError("No device specified; cannot set as primary IP")
def save(self, *args, **kwargs):
# Set interface
if self.cleaned_data['device'] and self.cleaned_data['interface_name']:
self.instance.interface = Interface.objects.get(device=self.cleaned_data['device'],
name=self.cleaned_data['interface_name'])
self.instance.interface = Interface.objects.get(
device=self.cleaned_data['device'],
name=self.cleaned_data['interface_name']
)
ipaddress = super(IPAddressCSVForm, self).save(*args, **kwargs)
# Set as primary for device
if self.cleaned_data['is_primary']:
device = self.cleaned_data['device']
if self.instance.address.version == 4:
self.instance.primary_ip4_for = self.cleaned_data['device']
device.primary_ip4 = ipaddress
elif self.instance.address.version == 6:
self.instance.primary_ip6_for = self.cleaned_data['device']
device.primary_ip6 = ipaddress
device.save()
return super(IPAddressFromCSVForm, self).save(*args, **kwargs)
class IPAddressImportForm(BootstrapMixin, BulkImportForm):
csv = CSVDataField(csv_form=IPAddressFromCSVForm)
return ipaddress
class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
@@ -557,17 +670,25 @@ class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF')
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), 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)
class Meta:
nullable_fields = ['vrf', 'tenant', 'description']
nullable_fields = ['vrf', 'role', 'tenant', 'description']
def ipaddress_status_choices():
status_counts = {}
for status in IPAddress.objects.values('status').annotate(count=Count('status')).order_by('status'):
status_counts[status['status']] = status['count']
return [(s[0], u'{} ({})'.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):
@@ -590,6 +711,7 @@ class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
null_option=(0, 'None')
)
status = forms.MultipleChoiceField(choices=ipaddress_status_choices, required=False)
role = forms.MultipleChoiceField(choices=ipaddress_role_choices, required=False)
#
@@ -626,7 +748,9 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm):
)
group = ChainedModelChoiceField(
queryset=VLANGroup.objects.all(),
chains={'site': 'site'},
chains=(
('site', 'site'),
),
required=False,
label='Group',
widget=APISelect(
@@ -647,60 +771,67 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm):
}
class VLANFromCSVForm(forms.ModelForm):
class VLANCSVForm(forms.ModelForm):
site = forms.ModelChoiceField(
queryset=Site.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Site not found.'}
queryset=Site.objects.all(),
required=False,
to_field_name='name',
help_text='Name of parent site',
error_messages={
'invalid_choice': 'Site not found.',
}
)
group_name = forms.CharField(
help_text='Name of VLAN group',
required=False
)
group_name = forms.CharField(required=False)
tenant = forms.ModelChoiceField(
Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'}
queryset=Tenant.objects.all(),
to_field_name='name',
required=False,
help_text='Name of assigned tenant',
error_messages={
'invalid_choice': 'Tenant not found.',
}
)
status = CSVChoiceField(
choices=VLAN_STATUS_CHOICES,
help_text='Operational status'
)
status = forms.CharField()
role = forms.ModelChoiceField(
queryset=Role.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Invalid role.'}
queryset=Role.objects.all(),
required=False,
to_field_name='name',
help_text='Functional role',
error_messages={
'invalid_choice': 'Invalid role.',
}
)
class Meta:
model = VLAN
fields = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
help_texts = {
'vid': 'Numeric VLAN ID (1-4095)',
'name': 'VLAN name',
}
def clean(self):
super(VLANFromCSVForm, self).clean()
super(VLANCSVForm, self).clean()
# Validate VLANGroup
site = self.cleaned_data.get('site')
group_name = self.cleaned_data.get('group_name')
# Validate VLAN group
if group_name:
try:
VLANGroup.objects.get(site=self.cleaned_data.get('site'), name=group_name)
self.instance.group = VLANGroup.objects.get(site=site, name=group_name)
except VLANGroup.DoesNotExist:
self.add_error('group_name', "Invalid VLAN group {}.".format(group_name))
def clean_status(self):
status_choices = {s[1].lower(): s[0] for s in VLAN_STATUS_CHOICES}
try:
return status_choices[self.cleaned_data['status'].lower()]
except KeyError:
raise ValidationError("Invalid status: {}".format(self.cleaned_data['status']))
def save(self, *args, **kwargs):
vlan = super(VLANFromCSVForm, self).save(commit=False)
# Assign VLANGroup by site and name
if self.cleaned_data['group_name']:
vlan.group = VLANGroup.objects.get(site=self.cleaned_data['site'], name=self.cleaned_data['group_name'])
if kwargs.get('commit'):
vlan.save()
return vlan
class VLANImportForm(BootstrapMixin, BulkImportForm):
csv = CSVDataField(csv_form=VLANFromCSVForm)
if site:
raise forms.ValidationError("VLAN group {} not found for site {}".format(group_name, site))
else:
raise forms.ValidationError("Global VLAN group {} not found".format(group_name))
class VLANBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
@@ -720,7 +851,7 @@ def vlan_status_choices():
status_counts = {}
for status in VLAN.objects.values('status').annotate(count=Count('status')).order_by('status'):
status_counts[status['status']] = status['count']
return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in VLAN_STATUS_CHOICES]
return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in VLAN_STATUS_CHOICES]
class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):

View File

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django.db.models import Lookup, Transform, IntegerField
from django.db.models.lookups import BuiltinLookup

View File

@@ -0,0 +1,133 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2017-05-24 15:34
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import ipam.fields
class Migration(migrations.Migration):
dependencies = [
('ipam', '0015_global_vlans'),
]
operations = [
migrations.AlterField(
model_name='aggregate',
name='family',
field=models.PositiveSmallIntegerField(choices=[(4, 'IPv4'), (6, 'IPv6')]),
),
migrations.AlterField(
model_name='aggregate',
name='rir',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='aggregates', to='ipam.RIR', verbose_name='RIR'),
),
migrations.AlterField(
model_name='ipaddress',
name='address',
field=ipam.fields.IPAddressField(help_text='IPv4 or IPv6 address (with mask)'),
),
migrations.AlterField(
model_name='ipaddress',
name='family',
field=models.PositiveSmallIntegerField(choices=[(4, 'IPv4'), (6, 'IPv6')], editable=False),
),
migrations.AlterField(
model_name='ipaddress',
name='nat_inside',
field=models.OneToOneField(blank=True, help_text='The IP for which this address is the "outside" IP', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='nat_outside', to='ipam.IPAddress', verbose_name='NAT (Inside)'),
),
migrations.AlterField(
model_name='ipaddress',
name='status',
field=models.PositiveSmallIntegerField(choices=[(1, 'Active'), (2, 'Reserved'), (3, 'Deprecated'), (5, 'DHCP')], default=1, verbose_name='Status'),
),
migrations.AlterField(
model_name='ipaddress',
name='vrf',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='ip_addresses', to='ipam.VRF', verbose_name='VRF'),
),
migrations.AlterField(
model_name='prefix',
name='family',
field=models.PositiveSmallIntegerField(choices=[(4, 'IPv4'), (6, 'IPv6')], editable=False),
),
migrations.AlterField(
model_name='prefix',
name='is_pool',
field=models.BooleanField(default=False, help_text='All IP addresses within this prefix are considered usable', verbose_name='Is a pool'),
),
migrations.AlterField(
model_name='prefix',
name='prefix',
field=ipam.fields.IPNetworkField(help_text='IPv4 or IPv6 network with mask'),
),
migrations.AlterField(
model_name='prefix',
name='role',
field=models.ForeignKey(blank=True, help_text='The primary function of this prefix', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='prefixes', to='ipam.Role'),
),
migrations.AlterField(
model_name='prefix',
name='status',
field=models.PositiveSmallIntegerField(choices=[(0, 'Container'), (1, 'Active'), (2, 'Reserved'), (3, 'Deprecated')], default=1, help_text='Operational status of this prefix', verbose_name='Status'),
),
migrations.AlterField(
model_name='prefix',
name='vlan',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='prefixes', to='ipam.VLAN', verbose_name='VLAN'),
),
migrations.AlterField(
model_name='prefix',
name='vrf',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='prefixes', to='ipam.VRF', verbose_name='VRF'),
),
migrations.AlterField(
model_name='rir',
name='is_private',
field=models.BooleanField(default=False, help_text='IP space managed by this RIR is considered private', verbose_name='Private'),
),
migrations.AlterField(
model_name='service',
name='device',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='services', to='dcim.Device', verbose_name='device'),
),
migrations.AlterField(
model_name='service',
name='ipaddresses',
field=models.ManyToManyField(blank=True, related_name='services', to='ipam.IPAddress', verbose_name='IP addresses'),
),
migrations.AlterField(
model_name='service',
name='port',
field=models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(65535)], verbose_name='Port number'),
),
migrations.AlterField(
model_name='service',
name='protocol',
field=models.PositiveSmallIntegerField(choices=[(6, 'TCP'), (17, 'UDP')]),
),
migrations.AlterField(
model_name='vlan',
name='status',
field=models.PositiveSmallIntegerField(choices=[(1, 'Active'), (2, 'Reserved'), (3, 'Deprecated')], default=1, verbose_name='Status'),
),
migrations.AlterField(
model_name='vlan',
name='vid',
field=models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(4094)], verbose_name='ID'),
),
migrations.AlterField(
model_name='vrf',
name='enforce_unique',
field=models.BooleanField(default=True, help_text='Prevent duplicate prefixes/IP addresses within this VRF', verbose_name='Enforce unique space'),
),
migrations.AlterField(
model_name='vrf',
name='rd',
field=models.CharField(max_length=21, unique=True, verbose_name='Route distinguisher'),
),
]

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,4 +1,5 @@
from netaddr import IPNetwork, cidr_merge
from __future__ import unicode_literals
import netaddr
from django.conf import settings
from django.contrib.contenttypes.fields import GenericRelation
@@ -15,64 +16,10 @@ from tenancy.models import Tenant
from utilities.models import CreatedUpdatedModel
from utilities.sql import NullsFirstQuerySet
from utilities.utils import csv_format
from .constants import *
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
class VRF(CreatedUpdatedModel, CustomFieldModel):
"""
@@ -88,13 +35,15 @@ class VRF(CreatedUpdatedModel, CustomFieldModel):
description = models.CharField(max_length=100, blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
csv_headers = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
class Meta:
ordering = ['name']
verbose_name = 'VRF'
verbose_name_plural = 'VRFs'
def __str__(self):
return self.name
return self.display_name or super(VRF, self).__str__()
def get_absolute_url(self):
return reverse('ipam:vrf', args=[self.pk])
@@ -108,6 +57,12 @@ class VRF(CreatedUpdatedModel, CustomFieldModel):
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
class RIR(models.Model):
@@ -145,6 +100,8 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
description = models.CharField(max_length=100, blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
csv_headers = ['prefix', 'rir', 'date_added', 'description']
class Meta:
ordering = ['family', 'prefix']
@@ -199,15 +156,11 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
def get_utilization(self):
"""
Determine the utilization rate of the aggregate prefix and return it as a percentage.
Determine the prefix utilization of the aggregate and return it as a percentage.
"""
child_prefixes = Prefix.objects.filter(prefix__net_contained_or_equal=str(self.prefix))
# Remove overlapping prefixes from list of children
networks = cidr_merge([c.prefix for c in child_prefixes])
children_size = float(0)
for p in networks:
children_size += p.size
return int(children_size / self.prefix.size * 100)
queryset = Prefix.objects.filter(prefix__net_contained_or_equal=str(self.prefix))
child_prefixes = netaddr.IPSet([p.prefix for p in queryset])
return int(float(child_prefixes.size) / self.prefix.size * 100)
@python_2_unicode_compatible
@@ -296,6 +249,10 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
objects = PrefixQuerySet.as_manager()
csv_headers = [
'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description',
]
class Meta:
ordering = ['vrf', 'family', 'prefix']
verbose_name_plural = 'prefixes'
@@ -306,9 +263,6 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
def get_absolute_url(self):
return reverse('ipam:prefix', args=[self.pk])
def get_duplicates(self):
return Prefix.objects.filter(vrf=self.vrf, prefix=str(self.prefix)).exclude(pk=self.pk)
def clean(self):
if self.prefix:
@@ -356,20 +310,64 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
self.description,
])
def get_status_class(self):
return STATUS_CHOICE_CLASSES[self.status]
def get_duplicates(self):
return Prefix.objects.filter(vrf=self.vrf, prefix=str(self.prefix)).exclude(pk=self.pk)
def get_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):
"""
Determine the utilization of the prefix and return it as a percentage. For Prefixes with a status of
"container", calculate utilization based on child prefixes. For all others, count child IP addresses.
"""
if self.status == PREFIX_STATUS_CONTAINER:
queryset = Prefix.objects.filter(prefix__net_contained=str(self.prefix), vrf=self.vrf)
child_prefixes = 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
if self.family == 4 and self.prefix.prefixlen < 31 and not self.is_pool:
prefix_size -= 2
return int(float(child_count) / prefix_size * 100)
@property
def new_subnet(self):
if self.family == 4:
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
if self.family == 6:
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
def get_status_class(self):
return STATUS_CHOICE_CLASSES[self.status]
class IPAddressManager(models.Manager):
@@ -402,7 +400,13 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
vrf = models.ForeignKey('VRF', related_name='ip_addresses', on_delete=models.PROTECT, blank=True, null=True,
verbose_name='VRF')
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,
null=True)
nat_inside = models.OneToOneField('self', related_name='nat_outside', on_delete=models.SET_NULL, blank=True,
@@ -413,6 +417,10 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
objects = IPAddressManager()
csv_headers = [
'address', 'vrf', 'tenant', 'status', 'role', 'device', 'interface_name', 'is_primary', 'description',
]
class Meta:
ordering = ['family', 'address']
verbose_name = 'IP address'
@@ -451,17 +459,19 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
def to_csv(self):
# Determine if this IP is primary for a Device
is_primary = False
if self.family == 4 and getattr(self, 'primary_ip4_for', False):
is_primary = True
elif self.family == 6 and getattr(self, 'primary_ip6_for', False):
is_primary = True
else:
is_primary = False
return csv_format([
self.address,
self.vrf.rd if self.vrf else None,
self.tenant.name if self.tenant else None,
self.get_status_display(),
self.get_role_display(),
self.device.identifier if self.device else None,
self.interface.name if self.interface else None,
is_primary,
@@ -497,9 +507,7 @@ class VLANGroup(models.Model):
verbose_name_plural = 'VLAN groups'
def __str__(self):
if self.site is None:
return self.name
return u'{} - {}'.format(self.site.name, self.name)
return self.name
def get_absolute_url(self):
return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk)
@@ -528,6 +536,8 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
description = models.CharField(max_length=100, blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
csv_headers = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
class Meta:
ordering = ['site', 'group', 'vid']
unique_together = [
@@ -566,7 +576,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
@property
def display_name(self):
if self.vid and self.name:
return u"{} ({})".format(self.vid, self.name)
return "{} ({})".format(self.vid, self.name)
return None
def get_status_class(self):
@@ -590,7 +600,6 @@ class Service(CreatedUpdatedModel):
class Meta:
ordering = ['device', 'protocol', 'port']
unique_together = ['device', 'protocol', 'port']
def __str__(self):
return u'{} ({}/{})'.format(self.name, self.port, self.get_protocol_display())
return '{} ({}/{})'.format(self.name, self.port, self.get_protocol_display())

View File

@@ -1,8 +1,9 @@
from __future__ import unicode_literals
import django_tables2 as tables
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
@@ -33,7 +34,7 @@ RIR_ACTIONS = """
UTILIZATION_GRAPH = """
{% load helpers %}
{% utilization_graph value %}
{% if record.pk %}{% utilization_graph value %}{% else %}&mdash;{% endif %}
"""
ROLE_ACTIONS = """
@@ -151,16 +152,6 @@ class VRFTable(BaseTable):
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
#
@@ -170,6 +161,14 @@ class RIRTable(BaseTable):
name = tables.LinkColumn(verbose_name='Name')
is_private = tables.BooleanColumn(verbose_name='Private')
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',
footer=lambda table: sum(r.stats['total'] for r in table.data))
stats_active = tables.Column(accessor='stats.active', verbose_name='Active',
@@ -181,12 +180,12 @@ class RIRTable(BaseTable):
stats_available = tables.Column(accessor='stats.available', verbose_name='Available',
footer=lambda table: sum(r.stats['available'] for r in table.data))
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):
model = RIR
fields = ('pk', 'name', 'is_private', 'aggregate_count', 'stats_total', 'stats_active', 'stats_reserved',
'stats_deprecated', 'stats_available', 'utilization', 'actions')
class Meta(RIRTable.Meta):
fields = (
'pk', 'name', 'is_private', 'aggregate_count', 'stats_total', 'stats_active', 'stats_reserved',
'stats_deprecated', 'stats_available', 'utilization', 'actions',
)
#
@@ -196,24 +195,21 @@ class RIRTable(BaseTable):
class AggregateTable(BaseTable):
pk = ToggleColumn()
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')
class Meta(BaseTable.Meta):
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')
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
#
@@ -253,31 +249,11 @@ class PrefixTable(BaseTable):
}
class PrefixBriefTable(BaseTable):
prefix = tables.TemplateColumn(PREFIX_LINK_BRIEF)
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 PrefixDetailTable(PrefixTable):
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
class Meta(BaseTable.Meta):
model = Prefix
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')
class Meta(PrefixTable.Meta):
fields = ('pk', 'prefix', 'status', 'vrf', 'get_utilization', 'tenant', 'site', 'vlan', 'role', 'description')
#
@@ -290,43 +266,26 @@ class IPAddressTable(BaseTable):
status = tables.TemplateColumn(STATUS_LABEL)
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
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)
interface = tables.Column(orderable=False)
class Meta(BaseTable.Meta):
model = IPAddress
fields = ('pk', 'address', 'status', 'vrf', 'tenant', 'nat_inside', 'device', 'description')
fields = ('pk', 'address', 'vrf', 'status', 'role', 'tenant', 'device', 'interface', 'description')
row_attrs = {
'class': lambda record: 'success' if not isinstance(record, IPAddress) else '',
}
class IPAddressBriefTable(BaseTable):
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)
class IPAddressDetailTable(IPAddressTable):
nat_inside = tables.LinkColumn(
'ipam:ipaddress', args=[Accessor('nat_inside.pk')], orderable=False, verbose_name='NAT (Inside)'
)
class Meta(BaseTable.Meta):
model = IPAddress
fields = ('address', 'device', 'interface', 'nat_inside')
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')
class Meta(IPAddressTable.Meta):
fields = (
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'device', 'interface', 'description',
)
#
@@ -356,24 +315,17 @@ class VLANTable(BaseTable):
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')
prefixes = tables.TemplateColumn(VLAN_PREFIXES, orderable=False, verbose_name='Prefixes')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
status = tables.TemplateColumn(STATUS_LABEL)
role = tables.TemplateColumn(VLAN_ROLE_LINK)
class Meta(BaseTable.Meta):
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')
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

@@ -1,5 +1,6 @@
from netaddr import IPNetwork
from __future__ import unicode_literals
from netaddr import IPNetwork
from rest_framework import status
from rest_framework.test import APITestCase
@@ -366,6 +367,35 @@ class PrefixTest(HttpStatusMixin, APITestCase):
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
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):

View File

@@ -1,9 +1,11 @@
from __future__ import unicode_literals
import netaddr
from django.core.exceptions import ValidationError
from django.test import TestCase, override_settings
from ipam.models import IPAddress, Prefix, VRF
from django.core.exceptions import ValidationError
class TestPrefix(TestCase):

View File

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django.conf.urls import url
from . import views
@@ -8,71 +10,71 @@ urlpatterns = [
# VRFs
url(r'^vrfs/$', views.VRFListView.as_view(), name='vrf_list'),
url(r'^vrfs/add/$', views.VRFEditView.as_view(), name='vrf_add'),
url(r'^vrfs/add/$', views.VRFCreateView.as_view(), name='vrf_add'),
url(r'^vrfs/import/$', views.VRFBulkImportView.as_view(), name='vrf_import'),
url(r'^vrfs/edit/$', views.VRFBulkEditView.as_view(), name='vrf_bulk_edit'),
url(r'^vrfs/delete/$', views.VRFBulkDeleteView.as_view(), name='vrf_bulk_delete'),
url(r'^vrfs/(?P<pk>\d+)/$', views.vrf, name='vrf'),
url(r'^vrfs/(?P<pk>\d+)/$', views.VRFView.as_view(), name='vrf'),
url(r'^vrfs/(?P<pk>\d+)/edit/$', views.VRFEditView.as_view(), name='vrf_edit'),
url(r'^vrfs/(?P<pk>\d+)/delete/$', views.VRFDeleteView.as_view(), name='vrf_delete'),
# RIRs
url(r'^rirs/$', views.RIRListView.as_view(), name='rir_list'),
url(r'^rirs/add/$', views.RIREditView.as_view(), name='rir_add'),
url(r'^rirs/add/$', views.RIRCreateView.as_view(), name='rir_add'),
url(r'^rirs/delete/$', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'),
url(r'^rirs/(?P<slug>[\w-]+)/edit/$', views.RIREditView.as_view(), name='rir_edit'),
# Aggregates
url(r'^aggregates/$', views.AggregateListView.as_view(), name='aggregate_list'),
url(r'^aggregates/add/$', views.AggregateEditView.as_view(), name='aggregate_add'),
url(r'^aggregates/add/$', views.AggregateCreateView.as_view(), name='aggregate_add'),
url(r'^aggregates/import/$', views.AggregateBulkImportView.as_view(), name='aggregate_import'),
url(r'^aggregates/edit/$', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'),
url(r'^aggregates/delete/$', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'),
url(r'^aggregates/(?P<pk>\d+)/$', views.aggregate, name='aggregate'),
url(r'^aggregates/(?P<pk>\d+)/$', views.AggregateView.as_view(), name='aggregate'),
url(r'^aggregates/(?P<pk>\d+)/edit/$', views.AggregateEditView.as_view(), name='aggregate_edit'),
url(r'^aggregates/(?P<pk>\d+)/delete/$', views.AggregateDeleteView.as_view(), name='aggregate_delete'),
# Roles
url(r'^roles/$', views.RoleListView.as_view(), name='role_list'),
url(r'^roles/add/$', views.RoleEditView.as_view(), name='role_add'),
url(r'^roles/add/$', views.RoleCreateView.as_view(), name='role_add'),
url(r'^roles/delete/$', views.RoleBulkDeleteView.as_view(), name='role_bulk_delete'),
url(r'^roles/(?P<slug>[\w-]+)/edit/$', views.RoleEditView.as_view(), name='role_edit'),
# Prefixes
url(r'^prefixes/$', views.PrefixListView.as_view(), name='prefix_list'),
url(r'^prefixes/add/$', views.PrefixEditView.as_view(), name='prefix_add'),
url(r'^prefixes/add/$', views.PrefixCreateView.as_view(), name='prefix_add'),
url(r'^prefixes/import/$', views.PrefixBulkImportView.as_view(), name='prefix_import'),
url(r'^prefixes/edit/$', views.PrefixBulkEditView.as_view(), name='prefix_bulk_edit'),
url(r'^prefixes/delete/$', views.PrefixBulkDeleteView.as_view(), name='prefix_bulk_delete'),
url(r'^prefixes/(?P<pk>\d+)/$', views.prefix, name='prefix'),
url(r'^prefixes/(?P<pk>\d+)/$', views.PrefixView.as_view(), name='prefix'),
url(r'^prefixes/(?P<pk>\d+)/edit/$', views.PrefixEditView.as_view(), name='prefix_edit'),
url(r'^prefixes/(?P<pk>\d+)/delete/$', views.PrefixDeleteView.as_view(), name='prefix_delete'),
url(r'^prefixes/(?P<pk>\d+)/ip-addresses/$', views.prefix_ipaddresses, name='prefix_ipaddresses'),
url(r'^prefixes/(?P<pk>\d+)/ip-addresses/$', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'),
# IP addresses
url(r'^ip-addresses/$', views.IPAddressListView.as_view(), name='ipaddress_list'),
url(r'^ip-addresses/add/$', views.IPAddressEditView.as_view(), name='ipaddress_add'),
url(r'^ip-addresses/bulk-add/$', views.IPAddressBulkAddView.as_view(), name='ipaddress_bulk_add'),
url(r'^ip-addresses/add/$', views.IPAddressCreateView.as_view(), name='ipaddress_add'),
url(r'^ip-addresses/bulk-add/$', views.IPAddressBulkCreateView.as_view(), name='ipaddress_bulk_add'),
url(r'^ip-addresses/import/$', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'),
url(r'^ip-addresses/edit/$', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'),
url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'),
url(r'^ip-addresses/(?P<pk>\d+)/$', views.ipaddress, name='ipaddress'),
url(r'^ip-addresses/(?P<pk>\d+)/$', views.IPAddressView.as_view(), name='ipaddress'),
url(r'^ip-addresses/(?P<pk>\d+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'),
url(r'^ip-addresses/(?P<pk>\d+)/delete/$', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'),
# VLAN groups
url(r'^vlan-groups/$', views.VLANGroupListView.as_view(), name='vlangroup_list'),
url(r'^vlan-groups/add/$', views.VLANGroupEditView.as_view(), name='vlangroup_add'),
url(r'^vlan-groups/add/$', views.VLANGroupCreateView.as_view(), name='vlangroup_add'),
url(r'^vlan-groups/delete/$', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'),
url(r'^vlan-groups/(?P<pk>\d+)/edit/$', views.VLANGroupEditView.as_view(), name='vlangroup_edit'),
# VLANs
url(r'^vlans/$', views.VLANListView.as_view(), name='vlan_list'),
url(r'^vlans/add/$', views.VLANEditView.as_view(), name='vlan_add'),
url(r'^vlans/add/$', views.VLANCreateView.as_view(), name='vlan_add'),
url(r'^vlans/import/$', views.VLANBulkImportView.as_view(), name='vlan_import'),
url(r'^vlans/edit/$', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'),
url(r'^vlans/delete/$', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'),
url(r'^vlans/(?P<pk>\d+)/$', views.vlan, name='vlan'),
url(r'^vlans/(?P<pk>\d+)/$', views.VLANView.as_view(), name='vlan'),
url(r'^vlans/(?P<pk>\d+)/edit/$', views.VLANEditView.as_view(), name='vlan_edit'),
url(r'^vlans/(?P<pk>\d+)/delete/$', views.VLANDeleteView.as_view(), name='vlan_delete'),

View File

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django_tables2 import RequestConfig
import netaddr
@@ -6,13 +8,13 @@ from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db.models import Count, Q
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.views.generic import View
from dcim.models import Device
from utilities.paginator import EnhancedPaginator
from utilities.views import (
BulkAddView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
BulkCreateView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
)
from . import filters, forms, tables
from .models import (
Aggregate, IPAddress, PREFIX_STATUS_ACTIVE, PREFIX_STATUS_DEPRECATED, PREFIX_STATUS_RESERVED, Prefix, RIR, Role,
@@ -96,28 +98,34 @@ class VRFListView(ObjectListView):
template_name = 'ipam/vrf_list.html'
def vrf(request, pk):
class VRFView(View):
vrf = get_object_or_404(VRF.objects.all(), pk=pk)
prefix_table = tables.PrefixBriefTable(
list(Prefix.objects.filter(vrf=vrf).select_related('site', 'role'))
)
prefix_table.exclude = ('vrf',)
def get(self, request, pk):
return render(request, 'ipam/vrf.html', {
'vrf': vrf,
'prefix_table': prefix_table,
})
vrf = get_object_or_404(VRF.objects.all(), pk=pk)
prefix_table = tables.PrefixTable(
list(Prefix.objects.filter(vrf=vrf).select_related('site', 'role')), orderable=False
)
prefix_table.exclude = ('vrf',)
return render(request, 'ipam/vrf.html', {
'vrf': vrf,
'prefix_table': prefix_table,
})
class VRFEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_vrf'
class VRFCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_vrf'
model = VRF
form_class = forms.VRFForm
template_name = 'ipam/vrf_edit.html'
default_return_url = 'ipam:vrf_list'
class VRFEditView(VRFCreateView):
permission_required = 'ipam.change_vrf'
class VRFDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'ipam.delete_vrf'
model = VRF
@@ -126,25 +134,27 @@ class VRFDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class VRFBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'ipam.add_vrf'
form = forms.VRFImportForm
model_form = forms.VRFCSVForm
table = tables.VRFTable
template_name = 'ipam/vrf_import.html'
default_return_url = 'ipam:vrf_list'
class VRFBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'ipam.change_vrf'
cls = VRF
queryset = VRF.objects.select_related('tenant')
filter = filters.VRFFilter
table = tables.VRFTable
form = forms.VRFBulkEditForm
template_name = 'ipam/vrf_bulk_edit.html'
default_return_url = 'ipam:vrf_list'
class VRFBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_vrf'
cls = VRF
queryset = VRF.objects.select_related('tenant')
filter = filters.VRFFilter
table = tables.VRFTable
default_return_url = 'ipam:vrf_list'
@@ -156,7 +166,7 @@ class RIRListView(ObjectListView):
queryset = RIR.objects.annotate(aggregate_count=Count('aggregates'))
filter = filters.RIRFilter
filter_form = forms.RIRFilterForm
table = tables.RIRTable
table = tables.RIRDetailTable
template_name = 'ipam/rir_list.html'
def alter_queryset(self, request):
@@ -236,8 +246,8 @@ class RIRListView(ObjectListView):
}
class RIREditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_rir'
class RIRCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_rir'
model = RIR
form_class = forms.RIRForm
@@ -245,10 +255,16 @@ class RIREditView(PermissionRequiredMixin, ObjectEditView):
return reverse('ipam:rir_list')
class RIREditView(RIRCreateView):
permission_required = 'ipam.change_rir'
class RIRBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_rir'
cls = RIR
queryset = RIR.objects.annotate(aggregate_count=Count('aggregates'))
filter = filters.RIRFilter
table = tables.RIRTable
default_return_url = 'ipam:rir_list'
@@ -262,7 +278,7 @@ class AggregateListView(ObjectListView):
})
filter = filters.AggregateFilter
filter_form = forms.AggregateFilterForm
table = tables.AggregateTable
table = tables.AggregateDetailTable
template_name = 'ipam/aggregate_list.html'
def extra_context(self):
@@ -281,47 +297,58 @@ class AggregateListView(ObjectListView):
}
def aggregate(request, pk):
class AggregateView(View):
aggregate = get_object_or_404(Aggregate, pk=pk)
def get(self, request, pk):
# Find all child prefixes contained by this aggregate
child_prefixes = Prefix.objects.filter(prefix__net_contained_or_equal=str(aggregate.prefix))\
.select_related('site', 'role').annotate_depth(limit=0)
child_prefixes = add_available_prefixes(aggregate.prefix, child_prefixes)
aggregate = get_object_or_404(Aggregate, pk=pk)
prefix_table = tables.PrefixTable(child_prefixes)
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
prefix_table.base_columns['pk'].visible = True
# Find all child prefixes contained by this aggregate
child_prefixes = Prefix.objects.filter(
prefix__net_contained_or_equal=str(aggregate.prefix)
).select_related(
'site', 'role'
).annotate_depth(
limit=0
)
child_prefixes = add_available_prefixes(aggregate.prefix, child_prefixes)
paginate = {
'klass': EnhancedPaginator,
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
}
RequestConfig(request, paginate).configure(prefix_table)
prefix_table = tables.PrefixTable(child_prefixes)
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
prefix_table.base_columns['pk'].visible = True
# Compile permissions list for rendering the object table
permissions = {
'add': request.user.has_perm('ipam.add_prefix'),
'change': request.user.has_perm('ipam.change_prefix'),
'delete': request.user.has_perm('ipam.delete_prefix'),
}
paginate = {
'klass': EnhancedPaginator,
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
}
RequestConfig(request, paginate).configure(prefix_table)
return render(request, 'ipam/aggregate.html', {
'aggregate': aggregate,
'prefix_table': prefix_table,
'permissions': permissions,
})
# Compile permissions list for rendering the object table
permissions = {
'add': request.user.has_perm('ipam.add_prefix'),
'change': request.user.has_perm('ipam.change_prefix'),
'delete': request.user.has_perm('ipam.delete_prefix'),
}
return render(request, 'ipam/aggregate.html', {
'aggregate': aggregate,
'prefix_table': prefix_table,
'permissions': permissions,
})
class AggregateEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_aggregate'
class AggregateCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_aggregate'
model = Aggregate
form_class = forms.AggregateForm
template_name = 'ipam/aggregate_edit.html'
default_return_url = 'ipam:aggregate_list'
class AggregateEditView(AggregateCreateView):
permission_required = 'ipam.change_aggregate'
class AggregateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'ipam.delete_aggregate'
model = Aggregate
@@ -330,25 +357,27 @@ class AggregateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class AggregateBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'ipam.add_aggregate'
form = forms.AggregateImportForm
model_form = forms.AggregateCSVForm
table = tables.AggregateTable
template_name = 'ipam/aggregate_import.html'
default_return_url = 'ipam:aggregate_list'
class AggregateBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'ipam.change_aggregate'
cls = Aggregate
queryset = Aggregate.objects.select_related('rir')
filter = filters.AggregateFilter
table = tables.AggregateTable
form = forms.AggregateBulkEditForm
template_name = 'ipam/aggregate_bulk_edit.html'
default_return_url = 'ipam:aggregate_list'
class AggregateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_aggregate'
cls = Aggregate
queryset = Aggregate.objects.select_related('rir')
filter = filters.AggregateFilter
table = tables.AggregateTable
default_return_url = 'ipam:aggregate_list'
@@ -362,8 +391,8 @@ class RoleListView(ObjectListView):
template_name = 'ipam/role_list.html'
class RoleEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_role'
class RoleCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_role'
model = Role
form_class = forms.RoleForm
@@ -371,9 +400,14 @@ class RoleEditView(PermissionRequiredMixin, ObjectEditView):
return reverse('ipam:role_list')
class RoleEditView(RoleCreateView):
permission_required = 'ipam.change_role'
class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_role'
cls = Role
table = tables.RoleTable
default_return_url = 'ipam:role_list'
@@ -385,7 +419,7 @@ class PrefixListView(ObjectListView):
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
filter = filters.PrefixFilter
filter_form = forms.PrefixFilterForm
table = tables.PrefixTable
table = tables.PrefixDetailTable
template_name = 'ipam/prefix_list.html'
def alter_queryset(self, request):
@@ -394,76 +428,134 @@ class PrefixListView(ObjectListView):
return self.queryset.annotate_depth(limit=limit)
def prefix(request, pk):
class PrefixView(View):
prefix = get_object_or_404(Prefix.objects.select_related(
'vrf', 'site__region', 'tenant__group', 'vlan__group', 'role'
), pk=pk)
def get(self, request, pk):
try:
aggregate = Aggregate.objects.get(prefix__net_contains_or_equals=str(prefix.prefix))
except Aggregate.DoesNotExist:
aggregate = None
prefix = get_object_or_404(Prefix.objects.select_related(
'vrf', 'site__region', 'tenant__group', 'vlan__group', 'role'
), pk=pk)
# Count child IP addresses
ipaddress_count = IPAddress.objects.filter(vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix))\
.count()
try:
aggregate = Aggregate.objects.get(prefix__net_contains_or_equals=str(prefix.prefix))
except Aggregate.DoesNotExist:
aggregate = None
# Parent prefixes table
parent_prefixes = Prefix.objects.filter(Q(vrf=prefix.vrf) | Q(vrf__isnull=True))\
.filter(prefix__net_contains=str(prefix.prefix))\
.select_related('site', 'role').annotate_depth()
parent_prefix_table = tables.PrefixBriefTable(parent_prefixes)
parent_prefix_table.exclude = ('vrf',)
# Count child IP addresses
ipaddress_count = IPAddress.objects.filter(
vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix)
).count()
# Duplicate prefixes table
duplicate_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix=str(prefix.prefix)).exclude(pk=prefix.pk)\
.select_related('site', 'role')
duplicate_prefix_table = tables.PrefixBriefTable(list(duplicate_prefixes))
duplicate_prefix_table.exclude = ('vrf',)
# Parent prefixes table
parent_prefixes = Prefix.objects.filter(
Q(vrf=prefix.vrf) | Q(vrf__isnull=True)
).filter(
prefix__net_contains=str(prefix.prefix)
).select_related(
'site', 'role'
).annotate_depth()
parent_prefix_table = tables.PrefixTable(list(parent_prefixes), orderable=False)
parent_prefix_table.exclude = ('vrf',)
# Child prefixes table
child_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix))\
.select_related('site', 'role').annotate_depth(limit=0)
if child_prefixes:
child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes)
child_prefix_table = tables.PrefixTable(child_prefixes)
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
child_prefix_table.base_columns['pk'].visible = True
# Duplicate prefixes table
duplicate_prefixes = Prefix.objects.filter(
vrf=prefix.vrf, prefix=str(prefix.prefix)
).exclude(
pk=prefix.pk
).select_related(
'site', 'role'
)
duplicate_prefix_table = tables.PrefixTable(list(duplicate_prefixes), orderable=False)
duplicate_prefix_table.exclude = ('vrf',)
paginate = {
'klass': EnhancedPaginator,
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
}
RequestConfig(request, paginate).configure(child_prefix_table)
# Child prefixes table
child_prefixes = Prefix.objects.filter(
vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix)
).select_related(
'site', 'role'
).annotate_depth(limit=0)
if child_prefixes:
child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes)
child_prefix_table = tables.PrefixTable(child_prefixes)
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
child_prefix_table.base_columns['pk'].visible = True
# Compile permissions list for rendering the object table
permissions = {
'add': request.user.has_perm('ipam.add_prefix'),
'change': request.user.has_perm('ipam.change_prefix'),
'delete': request.user.has_perm('ipam.delete_prefix'),
}
paginate = {
'klass': EnhancedPaginator,
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
}
RequestConfig(request, paginate).configure(child_prefix_table)
return render(request, 'ipam/prefix.html', {
'prefix': prefix,
'aggregate': aggregate,
'ipaddress_count': ipaddress_count,
'parent_prefix_table': parent_prefix_table,
'child_prefix_table': child_prefix_table,
'duplicate_prefix_table': duplicate_prefix_table,
'permissions': permissions,
'return_url': prefix.get_absolute_url(),
})
# Compile permissions list for rendering the object table
permissions = {
'add': request.user.has_perm('ipam.add_prefix'),
'change': request.user.has_perm('ipam.change_prefix'),
'delete': request.user.has_perm('ipam.delete_prefix'),
}
return render(request, 'ipam/prefix.html', {
'prefix': prefix,
'aggregate': aggregate,
'ipaddress_count': ipaddress_count,
'parent_prefix_table': parent_prefix_table,
'child_prefix_table': child_prefix_table,
'duplicate_prefix_table': duplicate_prefix_table,
'permissions': permissions,
'return_url': prefix.get_absolute_url(),
})
class PrefixEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_prefix'
class PrefixIPAddressesView(View):
def get(self, request, pk):
prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
# Find all IPAddresses belonging to this Prefix
ipaddresses = IPAddress.objects.filter(
vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix)
).select_related(
'vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for'
)
ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses, prefix.is_pool)
ip_table = tables.IPAddressTable(ipaddresses)
if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'):
ip_table.base_columns['pk'].visible = True
paginate = {
'klass': EnhancedPaginator,
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
}
RequestConfig(request, paginate).configure(ip_table)
# Compile permissions list for rendering the object table
permissions = {
'add': request.user.has_perm('ipam.add_ipaddress'),
'change': request.user.has_perm('ipam.change_ipaddress'),
'delete': request.user.has_perm('ipam.delete_ipaddress'),
}
return render(request, 'ipam/prefix_ipaddresses.html', {
'prefix': prefix,
'ip_table': ip_table,
'permissions': permissions,
'bulk_querystring': 'vrf_id={}&parent={}'.format(prefix.vrf or '0', prefix.prefix),
})
class PrefixCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_prefix'
model = Prefix
form_class = forms.PrefixForm
template_name = 'ipam/prefix_edit.html'
default_return_url = 'ipam:prefix_list'
class PrefixEditView(PrefixCreateView):
permission_required = 'ipam.change_prefix'
class PrefixDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'ipam.delete_prefix'
model = Prefix
@@ -473,62 +565,30 @@ class PrefixDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class PrefixBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'ipam.add_prefix'
form = forms.PrefixImportForm
model_form = forms.PrefixCSVForm
table = tables.PrefixTable
template_name = 'ipam/prefix_import.html'
default_return_url = 'ipam:prefix_list'
class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'ipam.change_prefix'
cls = Prefix
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
filter = filters.PrefixFilter
table = tables.PrefixTable
form = forms.PrefixBulkEditForm
template_name = 'ipam/prefix_bulk_edit.html'
default_return_url = 'ipam:prefix_list'
class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_prefix'
cls = Prefix
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
filter = filters.PrefixFilter
table = tables.PrefixTable
default_return_url = 'ipam:prefix_list'
def prefix_ipaddresses(request, pk):
prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
# Find all IPAddresses belonging to this Prefix
ipaddresses = IPAddress.objects.filter(vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix))\
.select_related('vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for')
ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses, prefix.is_pool)
ip_table = tables.IPAddressTable(ipaddresses)
if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'):
ip_table.base_columns['pk'].visible = True
paginate = {
'klass': EnhancedPaginator,
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
}
RequestConfig(request, paginate).configure(ip_table)
# Compile permissions list for rendering the object table
permissions = {
'add': request.user.has_perm('ipam.add_ipaddress'),
'change': request.user.has_perm('ipam.change_ipaddress'),
'delete': request.user.has_perm('ipam.delete_ipaddress'),
}
return render(request, 'ipam/prefix_ipaddresses.html', {
'prefix': prefix,
'ip_table': ip_table,
'permissions': permissions,
'bulk_querystring': 'vrf_id={}&parent={}'.format(prefix.vrf or '0', prefix.prefix),
})
#
# IP addresses
#
@@ -537,53 +597,72 @@ class IPAddressListView(ObjectListView):
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside')
filter = filters.IPAddressFilter
filter_form = forms.IPAddressFilterForm
table = tables.IPAddressTable
table = tables.IPAddressDetailTable
template_name = 'ipam/ipaddress_list.html'
def ipaddress(request, pk):
class IPAddressView(View):
ipaddress = get_object_or_404(IPAddress.objects.select_related('interface__device'), pk=pk)
def get(self, request, pk):
# Parent prefixes table
parent_prefixes = Prefix.objects.filter(vrf=ipaddress.vrf, prefix__net_contains=str(ipaddress.address.ip))\
.select_related('site', 'role')
parent_prefixes_table = tables.PrefixBriefTable(list(parent_prefixes))
parent_prefixes_table.exclude = ('vrf',)
ipaddress = get_object_or_404(IPAddress.objects.select_related('interface__device'), pk=pk)
# Duplicate IPs table
duplicate_ips = IPAddress.objects.filter(vrf=ipaddress.vrf, address=str(ipaddress.address))\
.exclude(pk=ipaddress.pk).select_related('interface__device', 'nat_inside')
duplicate_ips_table = tables.IPAddressBriefTable(list(duplicate_ips))
# Parent prefixes table
parent_prefixes = Prefix.objects.filter(
vrf=ipaddress.vrf, prefix__net_contains=str(ipaddress.address.ip)
).select_related(
'site', 'role'
)
parent_prefixes_table = tables.PrefixTable(list(parent_prefixes), orderable=False)
parent_prefixes_table.exclude = ('vrf',)
# Related IP table
related_ips = IPAddress.objects.select_related('interface__device').exclude(address=str(ipaddress.address))\
.filter(vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address))
related_ips_table = tables.IPAddressBriefTable(list(related_ips))
# Duplicate IPs table
duplicate_ips = IPAddress.objects.filter(
vrf=ipaddress.vrf, address=str(ipaddress.address)
).exclude(
pk=ipaddress.pk
).select_related(
'interface__device', 'nat_inside'
)
duplicate_ips_table = tables.IPAddressTable(list(duplicate_ips), orderable=False)
return render(request, 'ipam/ipaddress.html', {
'ipaddress': ipaddress,
'parent_prefixes_table': parent_prefixes_table,
'duplicate_ips_table': duplicate_ips_table,
'related_ips_table': related_ips_table,
})
# Related IP table
related_ips = IPAddress.objects.select_related(
'interface__device'
).exclude(
address=str(ipaddress.address)
).filter(
vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address)
)
related_ips_table = tables.IPAddressTable(list(related_ips), orderable=False)
return render(request, 'ipam/ipaddress.html', {
'ipaddress': ipaddress,
'parent_prefixes_table': parent_prefixes_table,
'duplicate_ips_table': duplicate_ips_table,
'related_ips_table': related_ips_table,
})
class IPAddressEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_ipaddress'
class IPAddressCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_ipaddress'
model = IPAddress
form_class = forms.IPAddressForm
template_name = 'ipam/ipaddress_edit.html'
default_return_url = 'ipam:ipaddress_list'
class IPAddressEditView(IPAddressCreateView):
permission_required = 'ipam.change_ipaddress'
class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'ipam.delete_ipaddress'
model = IPAddress
default_return_url = 'ipam:ipaddress_list'
class IPAddressBulkAddView(PermissionRequiredMixin, BulkAddView):
class IPAddressBulkCreateView(PermissionRequiredMixin, BulkCreateView):
permission_required = 'ipam.add_ipaddress'
pattern_form = forms.IPAddressPatternForm
model_form = forms.IPAddressBulkAddForm
@@ -594,38 +673,27 @@ class IPAddressBulkAddView(PermissionRequiredMixin, BulkAddView):
class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'ipam.add_ipaddress'
form = forms.IPAddressImportForm
model_form = forms.IPAddressCSVForm
table = tables.IPAddressTable
template_name = 'ipam/ipaddress_import.html'
default_return_url = 'ipam:ipaddress_list'
def save_obj(self, obj):
obj.save()
# Update primary IP for device if needed. The Device must be updated directly in the database; otherwise we risk
# overwriting a previous IP assignment from the same import (see #861).
try:
if obj.family == 4 and obj.primary_ip4_for:
Device.objects.filter(pk=obj.primary_ip4_for.pk).update(primary_ip4=obj)
elif obj.family == 6 and obj.primary_ip6_for:
Device.objects.filter(pk=obj.primary_ip6_for.pk).update(primary_ip6=obj)
except Device.DoesNotExist:
pass
class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'ipam.change_ipaddress'
cls = IPAddress
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device')
filter = filters.IPAddressFilter
table = tables.IPAddressTable
form = forms.IPAddressBulkEditForm
template_name = 'ipam/ipaddress_bulk_edit.html'
default_return_url = 'ipam:ipaddress_list'
class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_ipaddress'
cls = IPAddress
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device')
filter = filters.IPAddressFilter
table = tables.IPAddressTable
default_return_url = 'ipam:ipaddress_list'
@@ -641,8 +709,8 @@ class VLANGroupListView(ObjectListView):
template_name = 'ipam/vlangroup_list.html'
class VLANGroupEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_vlangroup'
class VLANGroupCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_vlangroup'
model = VLANGroup
form_class = forms.VLANGroupForm
@@ -650,10 +718,16 @@ class VLANGroupEditView(PermissionRequiredMixin, ObjectEditView):
return reverse('ipam:vlangroup_list')
class VLANGroupEditView(VLANGroupCreateView):
permission_required = 'ipam.change_vlangroup'
class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_vlangroup'
cls = VLANGroup
queryset = VLANGroup.objects.select_related('site').annotate(vlan_count=Count('vlans'))
filter = filters.VLANGroupFilter
table = tables.VLANGroupTable
default_return_url = 'ipam:vlangroup_list'
@@ -665,31 +739,39 @@ class VLANListView(ObjectListView):
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('prefixes')
filter = filters.VLANFilter
filter_form = forms.VLANFilterForm
table = tables.VLANTable
table = tables.VLANDetailTable
template_name = 'ipam/vlan_list.html'
def vlan(request, pk):
class VLANView(View):
vlan = get_object_or_404(VLAN.objects.select_related('site__region', 'tenant__group', 'role'), pk=pk)
prefixes = Prefix.objects.filter(vlan=vlan).select_related('vrf', 'site', 'role')
prefix_table = tables.PrefixBriefTable(list(prefixes))
prefix_table.exclude = ('vlan',)
def get(self, request, pk):
return render(request, 'ipam/vlan.html', {
'vlan': vlan,
'prefix_table': prefix_table,
})
vlan = get_object_or_404(VLAN.objects.select_related(
'site__region', 'tenant__group', 'role'
), pk=pk)
prefixes = Prefix.objects.filter(vlan=vlan).select_related('vrf', 'site', 'role')
prefix_table = tables.PrefixTable(list(prefixes), orderable=False)
prefix_table.exclude = ('vlan',)
return render(request, 'ipam/vlan.html', {
'vlan': vlan,
'prefix_table': prefix_table,
})
class VLANEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_vlan'
class VLANCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_vlan'
model = VLAN
form_class = forms.VLANForm
template_name = 'ipam/vlan_edit.html'
default_return_url = 'ipam:vlan_list'
class VLANEditView(VLANCreateView):
permission_required = 'ipam.change_vlan'
class VLANDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'ipam.delete_vlan'
model = VLAN
@@ -698,25 +780,27 @@ class VLANDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class VLANBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'ipam.add_vlan'
form = forms.VLANImportForm
model_form = forms.VLANCSVForm
table = tables.VLANTable
template_name = 'ipam/vlan_import.html'
default_return_url = 'ipam:vlan_list'
class VLANBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'ipam.change_vlan'
cls = VLAN
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
filter = filters.VLANFilter
table = tables.VLANTable
form = forms.VLANBulkEditForm
template_name = 'ipam/vlan_bulk_edit.html'
default_return_url = 'ipam:vlan_list'
class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_vlan'
cls = VLAN
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
filter = filters.VLANFilter
table = tables.VLANTable
default_return_url = 'ipam:vlan_list'
@@ -724,8 +808,8 @@ class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Services
#
class ServiceEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_service'
class ServiceCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_service'
model = Service
form_class = forms.ServiceForm
template_name = 'ipam/service_edit.html'
@@ -739,6 +823,10 @@ class ServiceEditView(PermissionRequiredMixin, ObjectEditView):
return obj.device.get_absolute_url()
class ServiceEditView(ServiceCreateView):
permission_required = 'ipam.change_service'
class ServiceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'ipam.delete_service'
model = Service

View File

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

View File

@@ -58,6 +58,11 @@ CORS_ORIGIN_REGEX_WHITELIST = [
# r'^(https?://)?(\w+\.)?example\.com$',
]
# Set to True to enable server debugging. WARNING: Debugging introduces a substantial performance penalty and may reveal
# sensitive information about your installation. Only enable debugging while performing testing. Never enable debugging
# on a production system.
DEBUG = False
# Email settings
EMAIL = {
'SERVER': 'localhost',
@@ -72,6 +77,10 @@ EMAIL = {
# (all prefixes and IP addresses not assigned to a VRF), set ENFORCE_GLOBAL_UNIQUE to True.
ENFORCE_GLOBAL_UNIQUE = False
# Enable custom logging. Please see the Django documentation for detailed guidance on configuring custom logs:
# https://docs.djangoproject.com/en/1.11/topics/logging/
LOGGING = {}
# Setting this to True will permit only authenticated users to access any part of NetBox. By default, anonymous users
# are permitted to access most data in NetBox (excluding secrets) but not make any changes.
LOGIN_REQUIRED = False
@@ -79,9 +88,21 @@ LOGIN_REQUIRED = False
# Setting this to True will display a "maintenance mode" banner at the top of every page.
MAINTENANCE_MODE = False
# Credentials that NetBox will use to access live devices (future use).
NETBOX_USERNAME = ''
NETBOX_PASSWORD = ''
# An API consumer can request an arbitrary number of objects =by appending the "limit" parameter to the URL (e.g.
# "?limit=1000"). This setting defines the maximum limit. Setting it to 0 or None will allow an API consumer to request
# all objects by specifying "?limit=0".
MAX_PAGE_SIZE = 1000
# Credentials that NetBox will uses to authenticate to devices when connecting via NAPALM.
NAPALM_USERNAME = ''
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)
PAGINATE_COUNT = 50

View File

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django import forms
from utilities.forms import BootstrapMixin

View File

@@ -13,9 +13,9 @@ except ImportError:
)
VERSION = '2.0.3'
VERSION = '2.1.2'
# Import local configuration
# Import required configuration parameters
ALLOWED_HOSTS = DATABASE = SECRET_KEY = None
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
try:
@@ -25,34 +25,54 @@ for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
"Mandatory setting {} is missing from configuration.py.".format(setting)
)
# Default configurations
# Import optional configuration parameters
ADMINS = getattr(configuration, 'ADMINS', [])
DEBUG = getattr(configuration, 'DEBUG', False)
EMAIL = getattr(configuration, 'EMAIL', {})
LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False)
BANNER_BOTTOM = getattr(configuration, 'BANNER_BOTTOM', False)
BANNER_TOP = getattr(configuration, 'BANNER_TOP', False)
BASE_PATH = getattr(configuration, 'BASE_PATH', '')
if BASE_PATH:
BASE_PATH = BASE_PATH.strip('/') + '/' # Enforce trailing slash only
MAINTENANCE_MODE = getattr(configuration, 'MAINTENANCE_MODE', False)
PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50)
NETBOX_USERNAME = getattr(configuration, 'NETBOX_USERNAME', '')
NETBOX_PASSWORD = getattr(configuration, 'NETBOX_PASSWORD', '')
TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC')
DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y')
SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d')
TIME_FORMAT = getattr(configuration, 'TIME_FORMAT', 'g:i a')
SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s')
DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
BANNER_TOP = getattr(configuration, 'BANNER_TOP', False)
BANNER_BOTTOM = getattr(configuration, 'BANNER_BOTTOM', False)
PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False)
ENFORCE_GLOBAL_UNIQUE = getattr(configuration, 'ENFORCE_GLOBAL_UNIQUE', False)
CORS_ORIGIN_ALLOW_ALL = getattr(configuration, 'CORS_ORIGIN_ALLOW_ALL', False)
CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', [])
CORS_ORIGIN_REGEX_WHITELIST = getattr(configuration, 'CORS_ORIGIN_REGEX_WHITELIST', [])
CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', [])
DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y')
DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
DEBUG = getattr(configuration, 'DEBUG', False)
ENFORCE_GLOBAL_UNIQUE = getattr(configuration, 'ENFORCE_GLOBAL_UNIQUE', False)
EMAIL = getattr(configuration, 'EMAIL', {})
LOGGING = getattr(configuration, 'LOGGING', {})
LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False)
MAINTENANCE_MODE = getattr(configuration, 'MAINTENANCE_MODE', False)
MAX_PAGE_SIZE = getattr(configuration, 'MAX_PAGE_SIZE', 1000)
PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50)
PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False)
NAPALM_USERNAME = getattr(configuration, 'NAPALM_USERNAME', '')
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_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s')
TIME_FORMAT = getattr(configuration, 'TIME_FORMAT', 'g:i a')
TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC')
CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
# 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
LDAP_IGNORE_CERT_ERRORS = False
try:
@@ -75,9 +95,9 @@ if LDAP_CONFIGURED:
if LDAP_IGNORE_CERT_ERRORS:
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
# Enable logging for django_auth_ldap
logger = logging.getLogger('django_auth_ldap')
logger.addHandler(logging.StreamHandler())
logger.setLevel(logging.DEBUG)
ldap_logger = logging.getLogger('django_auth_ldap')
ldap_logger.addHandler(logging.StreamHandler())
ldap_logger.setLevel(logging.DEBUG)
except ImportError:
raise ImproperlyConfigured(
"LDAP authentication has been configured, but django-auth-ldap is not installed. You can remove "
@@ -112,6 +132,7 @@ INSTALLED_APPS = (
'django.contrib.humanize',
'corsheaders',
'debug_toolbar',
'django_filters',
'django_tables2',
'mptt',
'rest_framework',
@@ -180,8 +201,8 @@ STATICFILES_DIRS = (
)
# Media
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/{}media/'.format(BASE_PATH)
# Disable default limit of 1000 fields per request. Needed for bulk deletion of objects. (Added in Django 1.10.)
DATA_UPLOAD_MAX_NUMBER_FIELDS = None
@@ -207,7 +228,7 @@ REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': (
'rest_framework.filters.DjangoFilterBackend',
),
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'DEFAULT_PAGINATION_CLASS': 'utilities.api.OptionalLimitOffsetPagination',
'DEFAULT_PERMISSION_CLASSES': (
'utilities.api.TokenPermissions',
),

View File

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
from rest_framework_swagger.views import get_swagger_view
from django.conf import settings
@@ -5,8 +7,8 @@ from django.conf.urls import include, url
from django.contrib import admin
from django.views.static import serve
from netbox.views import APIRootView, home, handle_500, SearchView, trigger_500
from users.views import login, logout
from netbox.views import APIRootView, handle_500, HomeView, SearchView, trigger_500
from users.views import LoginView, LogoutView
handler500 = handle_500
@@ -15,12 +17,12 @@ swagger_view = get_swagger_view(title='NetBox API')
_patterns = [
# Base views
url(r'^$', home, name='home'),
url(r'^$', HomeView.as_view(), name='home'),
url(r'^search/$', SearchView.as_view(), name='search'),
# Login/logout
url(r'^login/$', login, name='login'),
url(r'^logout/$', logout, name='logout'),
url(r'^login/$', LoginView.as_view(), name='login'),
url(r'^logout/$', LogoutView.as_view(), name='logout'),
# Apps
url(r'^circuits/', include('circuits.urls')),

View File

@@ -1,3 +1,4 @@
from __future__ import unicode_literals
from collections import OrderedDict
import sys
@@ -10,20 +11,20 @@ from django.views.generic import View
from circuits.filters import CircuitFilter, ProviderFilter
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.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 ipam.filters import AggregateFilter, IPAddressFilter, PrefixFilter, VLANFilter, VRFFilter
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.models import Secret
from secrets.tables import SecretSearchTable
from secrets.tables import SecretTable
from tenancy.filters import TenantFilter
from tenancy.models import Tenant
from tenancy.tables import TenantSearchTable
from tenancy.tables import TenantTable
from .forms import SearchForm
@@ -33,125 +34,130 @@ SEARCH_TYPES = OrderedDict((
('provider', {
'queryset': Provider.objects.all(),
'filter': ProviderFilter,
'table': ProviderSearchTable,
'table': ProviderTable,
'url': 'circuits:provider_list',
}),
('circuit', {
'queryset': Circuit.objects.select_related('type', 'provider', 'tenant').prefetch_related('terminations__site'),
'filter': CircuitFilter,
'table': CircuitSearchTable,
'table': CircuitTable,
'url': 'circuits:circuit_list',
}),
# DCIM
('site', {
'queryset': Site.objects.select_related('region', 'tenant'),
'filter': SiteFilter,
'table': SiteSearchTable,
'table': SiteTable,
'url': 'dcim:site_list',
}),
('rack', {
'queryset': Rack.objects.select_related('site', 'group', 'tenant', 'role'),
'filter': RackFilter,
'table': RackSearchTable,
'table': RackTable,
'url': 'dcim:rack_list',
}),
('devicetype', {
'queryset': DeviceType.objects.select_related('manufacturer'),
'filter': DeviceTypeFilter,
'table': DeviceTypeSearchTable,
'table': DeviceTypeTable,
'url': 'dcim:devicetype_list',
}),
('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,
'table': DeviceSearchTable,
'table': DeviceTable,
'url': 'dcim:device_list',
}),
# IPAM
('vrf', {
'queryset': VRF.objects.select_related('tenant'),
'filter': VRFFilter,
'table': VRFSearchTable,
'table': VRFTable,
'url': 'ipam:vrf_list',
}),
('aggregate', {
'queryset': Aggregate.objects.select_related('rir'),
'filter': AggregateFilter,
'table': AggregateSearchTable,
'table': AggregateTable,
'url': 'ipam:aggregate_list',
}),
('prefix', {
'queryset': Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role'),
'filter': PrefixFilter,
'table': PrefixSearchTable,
'table': PrefixTable,
'url': 'ipam:prefix_list',
}),
('ipaddress', {
'queryset': IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device'),
'filter': IPAddressFilter,
'table': IPAddressSearchTable,
'table': IPAddressTable,
'url': 'ipam:ipaddress_list',
}),
('vlan', {
'queryset': VLAN.objects.select_related('site', 'group', 'tenant', 'role'),
'filter': VLANFilter,
'table': VLANSearchTable,
'table': VLANTable,
'url': 'ipam:vlan_list',
}),
# Secrets
('secret', {
'queryset': Secret.objects.select_related('role', 'device'),
'filter': SecretFilter,
'table': SecretSearchTable,
'table': SecretTable,
'url': 'secrets:secret_list',
}),
# Tenancy
('tenant', {
'queryset': Tenant.objects.select_related('group'),
'filter': TenantFilter,
'table': TenantSearchTable,
'table': TenantTable,
'url': 'tenancy:tenant_list',
}),
))
def home(request):
class HomeView(View):
template_name = 'home.html'
stats = {
def get(self, request):
# Organization
'site_count': Site.objects.count(),
'tenant_count': Tenant.objects.count(),
stats = {
# DCIM
'rack_count': Rack.objects.count(),
'device_count': Device.objects.count(),
'interface_connections_count': InterfaceConnection.objects.count(),
'console_connections_count': ConsolePort.objects.filter(cs_port__isnull=False).count(),
'power_connections_count': PowerPort.objects.filter(power_outlet__isnull=False).count(),
# Organization
'site_count': Site.objects.count(),
'tenant_count': Tenant.objects.count(),
# IPAM
'vrf_count': VRF.objects.count(),
'aggregate_count': Aggregate.objects.count(),
'prefix_count': Prefix.objects.count(),
'ipaddress_count': IPAddress.objects.count(),
'vlan_count': VLAN.objects.count(),
# DCIM
'rack_count': Rack.objects.count(),
'device_count': Device.objects.count(),
'interface_connections_count': InterfaceConnection.objects.count(),
'console_connections_count': ConsolePort.objects.filter(cs_port__isnull=False).count(),
'power_connections_count': PowerPort.objects.filter(power_outlet__isnull=False).count(),
# Circuits
'provider_count': Provider.objects.count(),
'circuit_count': Circuit.objects.count(),
# IPAM
'vrf_count': VRF.objects.count(),
'aggregate_count': Aggregate.objects.count(),
'prefix_count': Prefix.objects.count(),
'ipaddress_count': IPAddress.objects.count(),
'vlan_count': VLAN.objects.count(),
# Secrets
'secret_count': Secret.objects.count(),
# Circuits
'provider_count': Provider.objects.count(),
'circuit_count': Circuit.objects.count(),
}
# Secrets
'secret_count': Secret.objects.count(),
return render(request, 'home.html', {
'search_form': SearchForm(),
'stats': stats,
'topology_maps': TopologyMap.objects.filter(site__isnull=True),
'recent_activity': UserAction.objects.select_related('user')[:50]
})
}
return render(request, self.template_name, {
'search_form': SearchForm(),
'stats': stats,
'topology_maps': TopologyMap.objects.filter(site__isnull=True),
'recent_activity': UserAction.objects.select_related('user')[:50]
})
class SearchView(View):
@@ -185,14 +191,14 @@ class SearchView(View):
# Construct the results table for this object type
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)
if table.page:
results.append({
'name': queryset.model._meta.verbose_name_plural,
'table': table,
'url': u'{}?q={}'.format(reverse(url), form.cleaned_data['q'])
'url': '{}?q={}'.format(reverse(url), form.cleaned_data['q'])
})
return render(request, 'search.html', {
@@ -206,7 +212,7 @@ class APIRootView(APIView):
exclude_from_schema = True
def get_view_name(self):
return u"API Root"
return "API Root"
def get(self, request, format=None):
@@ -235,5 +241,6 @@ def trigger_500(request):
"""
Hot-wired method of triggering a server error to test reporting
"""
raise Exception("Congratulations, you've triggered an exception! Go tell all your friends what an exceptional "
"person you are.")
raise Exception(
"Congratulations, you've triggered an exception! Go tell all your friends what an exceptional person you are."
)

View File

@@ -11,6 +11,7 @@ import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "netbox.settings")
application = get_wsgi_application()

View File

@@ -81,6 +81,11 @@ footer p {
}
}
/* Navigation menu */
li.subnav > a {
padding-left: 30px;
}
/* Forms */
label {
font-weight: normal;
@@ -333,6 +338,31 @@ table.component-list tr.ipaddress:hover td {
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 */
.banner-bottom {
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() {
var search_field = $('#id_livesearch');
var real_field = $('#id_' + search_field.attr('data-field'));
var select_fields = $('#select select');
var search_key = search_field.attr('data-key');
var label = search_field.attr('data-label');
if (!label) {
@@ -40,13 +41,22 @@ $(document).ready(function() {
select: function(event, ui) {
event.preventDefault();
search_field.val(ui.item.label);
select_fields.val('');
select_fields.attr('disabled', 'disabled');
real_field.empty();
real_field.append($("<option></option>").attr('value', ui.item.value).text(ui.item.label));
real_field.change();
// If the field has a parent helper, reset the parent to no selection
$('select[filter-for="' + real_field.attr('name') + '"]').val('');
// Disable parent selection fields
// $('select[filter-for="' + real_field.attr('name') + '"]').val('');
},
minLength: 4,
delay: 500
});
search_field.change(function() {
if (!search_field.val()) {
select_fields.removeAttr('disabled');
select_fields.val('');
}
});
});

View File

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django.contrib import admin, messages
from django.shortcuts import redirect, render
@@ -34,19 +36,21 @@ class UserKeyAdmin(admin.ModelAdmin):
try:
my_userkey = UserKey.objects.get(user=request.user)
except UserKey.DoesNotExist:
messages.error(request, u"You do not have an active User Key.")
return redirect('/admin/secrets/userkey/')
messages.error(request, "You do not have an active User Key.")
return redirect('admin:secrets_userkey_changelist')
if 'activate' in request.POST:
form = ActivateUserKeyForm(request.POST)
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']:
uk.activate(master_key)
return redirect('/admin/secrets/userkey/')
except ValueError:
messages.error(request, u"Invalid private key provided. Unable to retrieve master key.")
return redirect('admin:secrets_userkey_changelist')
else:
messages.error(
request, "Invalid private key provided. Unable to retrieve master key.", extra_tags='error'
)
else:
form = ActivateUserKeyForm(initial={'_selected_action': request.POST.getlist(admin.ACTION_CHECKBOX_NAME)})

View File

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

View File

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
from rest_framework import routers
from . import views

View File

@@ -1,13 +1,14 @@
from __future__ import unicode_literals
import base64
from Crypto.PublicKey import RSA
from django.http import HttpResponseBadRequest
from rest_framework.exceptions import ValidationError
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet, ViewSet
from django.http import HttpResponseBadRequest
from secrets import filters
from secrets.exceptions import InvalidKey
from secrets.models import Secret, SecretRole, SessionKey, UserKey
@@ -29,6 +30,7 @@ class SecretRoleViewSet(ModelViewSet):
queryset = SecretRole.objects.all()
serializer_class = serializers.SecretRoleSerializer
permission_classes = [IsAuthenticated]
filter_class = filters.SecretRoleFilter
#

View File

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django.contrib import messages
from django.shortcuts import redirect
@@ -14,10 +16,10 @@ def userkey_required():
try:
uk = UserKey.objects.get(user=request.user)
except UserKey.DoesNotExist:
messages.warning(request, u"This operation requires an active user key, but you don't have one.")
messages.warning(request, "This operation requires an active user key, but you don't have one.")
return redirect('user:userkey')
if not uk.is_active():
messages.warning(request, u"This operation is not available. Your user key has not been activated.")
messages.warning(request, "This operation is not available. Your user key has not been activated.")
return redirect('user:userkey')
return view(request, *args, **kwargs)
return wrapped_view

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