Compare commits

...

738 Commits

Author SHA1 Message Date
Jeremy Stretch
7a64404299 Merge pull request #1614 from digitalocean/develop
Release v2.2.2
2017-10-17 11:24:02 -04:00
Jeremy Stretch
2afa6ed2cb Release v2.2.2 2017-10-17 11:21:58 -04:00
Jeremy Stretch
34f1a9ebfb Fixes #1579: Devices already assigned to a cluster cannot be added to a different cluster 2017-10-17 09:59:35 -04:00
Jeremy Stretch
6f2f8697ae Fixes #1609: Added missing virtual_machine field to IP address interface serializer 2017-10-17 09:23:53 -04:00
Jeremy Stretch
6ec9d1d6ce Merge pull request #1598 from candlerb/candlerb/1498
Avoid creating repeated graph nodes where device matches multiple regexps
2017-10-16 17:16:46 -04:00
Jeremy Stretch
047f22e110 Fixes #1605: Added clusters and virtual machines to object list for global search 2017-10-16 16:44:15 -04:00
root
5fc3eac0f6 Avoid creating repeated graph nodes where device matches multiple regexps
Fixes #1498
2017-10-16 10:13:39 +00:00
Jeremy Stretch
34259d5d9d Removed deprecated xstr and expand_pattern functions 2017-10-13 14:29:55 -04:00
Jeremy Stretch
91b6ebb0c0 Closes #1580: Allow cluster assignment when bulk importing devices 2017-10-13 14:19:41 -04:00
Jeremy Stretch
60b4f1f89f Fixes #1585: Fixed slug-based filtering of virtual machines 2017-10-13 12:14:19 -04:00
Jeremy Stretch
023ff6834a Designated new Docker build repo; removed stale Heroku build repo 2017-10-13 11:50:06 -04:00
Jeremy Stretch
d00cab0b0a Merge branch 'develop' of github.com:digitalocean/netbox into develop 2017-10-13 10:54:24 -04:00
Jeremy Stretch
17493ff655 Closes #1587: Add primary IP column for virtual machines in global search results 2017-10-13 10:53:25 -04:00
Jeremy Stretch
6c27e6c4fe Fixes #1584: Colorized virtual machine role column 2017-10-13 10:45:34 -04:00
Jeremy Stretch
4cb0be4df3 Fixes #1582: Add virtual_machine attribute to IPAddress 2017-10-13 10:42:45 -04:00
Jeremy Stretch
40f555a3b6 Merge pull request #1545 from digitalocean/mdl-ldap-docs-formatting
Minor LDAP documentation formatting cleanup
2017-10-12 23:42:29 -04:00
Jeremy Stretch
881fdbe893 Post-release version bump 2017-10-12 16:39:51 -04:00
Jeremy Stretch
2bda399982 Merge pull request #1577 from digitalocean/develop
Release v2.2.1
2017-10-12 16:11:17 -04:00
Jeremy Stretch
75d840fa1a PEP8 fix 2017-10-12 16:07:13 -04:00
Jeremy Stretch
800bdd8fc5 Release v2.2.1 2017-10-12 16:04:01 -04:00
Jeremy Stretch
b04ade8060 Fixes #1576: Move PostgreSQL validation logic into the relevant migration 2017-10-12 16:02:15 -04:00
Jeremy Stretch
7f4d96f33e Post-release version bump 2017-10-12 14:01:52 -04:00
Jeremy Stretch
74731bc6ae Merge pull request #1575 from digitalocean/develop
Release v2.2.0
2017-10-12 14:01:28 -04:00
Jeremy Stretch
9d8daca54d Release v2.2.0 2017-10-12 13:54:04 -04:00
Jeremy Stretch
bb898b719f Added reports CLI documentation 2017-10-12 13:48:08 -04:00
Jeremy Stretch
3a0b57b50f Fixed typo 2017-10-12 13:47:44 -04:00
Ryan Breaker
38d2333165 Add meta tags for charset and convert some tabs to spaces for consistency (#1574)
* Add meta tag for charset.

* Add meta tag for charset in 500.html and convert some tabs to spaces for consistency.
2017-10-12 13:39:28 -04:00
Jeremy Stretch
a7f0b5adb3 Refreshed installation docs 2017-10-12 13:38:23 -04:00
Jeremy Stretch
109fff0fa6 Reorganized the docs 2017-10-12 10:59:39 -04:00
Jeremy Stretch
8c591e7248 Fixed up reports documentation 2017-10-11 17:05:01 -04:00
Jeremy Stretch
0dafa10e27 Merge branch 'develop-2.2' into develop
Conflicts:
	netbox/netbox/settings.py
2017-10-11 16:45:18 -04:00
Jeremy Stretch
60e3ff0bf0 Updated version number for v2.2 build 2017-10-11 16:42:44 -04:00
Jeremy Stretch
6052405eb7 Added info about API _choices endpoints 2017-10-11 16:41:16 -04:00
Jeremy Stretch
17a45109f4 Bypass PostgreSQL 9.4 check if database is inaccessible 2017-10-11 16:28:05 -04:00
Jeremy Stretch
392b45e6cb Added warning for PostgreSQL 9.4 requirement 2017-10-11 16:20:50 -04:00
Jeremy Stretch
f27e1ba885 Check that PostgreSQL is 9.4 or higher on initialization 2017-10-11 16:16:44 -04:00
Jeremy Stretch
990c9d6f7c Updated package requirements 2017-10-11 15:11:51 -04:00
Jeremy Stretch
b301b8f6f2 Branded the browsable API template 2017-10-11 14:47:43 -04:00
Jeremy Stretch
924cf9bfd4 Removed obsolete docker build file 2017-10-11 14:39:51 -04:00
Jeremy Stretch
7e6573d191 Added missing permission class to reports API viewset 2017-10-11 14:37:25 -04:00
Jeremy Stretch
fdae3a3f31 Introduced the REPORTS_ROOT config parameter; Python2 fixes 2017-10-11 14:03:35 -04:00
Jeremy Stretch
51194e20f2 Improved template for when no reports are found 2017-10-11 13:30:38 -04:00
Jeremy Stretch
043f2cb214 Tweaked PostgreSQL version number in example 2017-10-11 13:12:49 -04:00
Jeremy Stretch
651d462456 Post-release version bump 2017-10-11 13:04:02 -04:00
Jeremy Stretch
7cb287d6c6 Merge pull request #1572 from digitalocean/develop
Release v2.1.6
2017-10-11 13:02:32 -04:00
Jeremy Stretch
d16d89028f Release v2.1.6 2017-10-11 12:59:43 -04:00
Jeremy Stretch
baa277c592 Updated install docs to install pip using setuptools 2017-10-11 12:22:35 -04:00
Jeremy Stretch
007f930fed Merge branch 'develop' into develop-2.2
Conflicts:
	netbox/utilities/api.py
2017-10-11 10:47:21 -04:00
Jeremy Stretch
f253f164a3 Closes #1510: Added ability to search by name when adding devices to a cluster 2017-10-11 10:14:09 -04:00
Jeremy Stretch
abfe71bb04 #1556: Swapped label and value 2017-10-10 17:47:53 -04:00
Jeremy Stretch
0c645b12d1 Moved querysets to separate files; tweaked interface ordering logic (#1523) 2017-10-10 17:23:41 -04:00
Jeremy Stretch
f824d1eb3b Closes #1556: Added API endpoints listing static field choices for each app 2017-10-10 16:41:35 -04:00
Jeremy Stretch
afbbe1148f Moved core API classes out of utilities 2017-10-10 14:21:20 -04:00
Jeremy Stretch
8403e91fc9 Fixes #1567: Prompt user for session key when importing secrets 2017-10-10 13:22:40 -04:00
Jeremy Stretch
c300879995 Fixes #1527: Replace deprecated pycrypto library with pycryptodome 2017-10-09 17:40:15 -04:00
Jeremy Stretch
ea1d298eb3 Closes #494: Include asset tag in device info pop-up on rack elevation 2017-10-09 16:25:52 -04:00
Jeremy Stretch
d17b36519a Closes #1479: Added CARP IP address role 2017-10-09 16:03:45 -04:00
Jeremy Stretch
f4f0896470 Added bulk import view for secret roles 2017-10-09 15:56:17 -04:00
Jeremy Stretch
2c9b41db75 Added bulk import view for circuit types 2017-10-09 15:52:22 -04:00
Jeremy Stretch
a38cd449c5 Added bulk import views for cluster types and groups 2017-10-09 15:49:48 -04:00
Jeremy Stretch
977cad3830 Added bulk import views for RIRs, roles, and VLAN groups 2017-10-09 15:42:06 -04:00
Jeremy Stretch
987587b5f3 Added NAPALM driver to platform table; removed legacy RPC client 2017-10-09 15:30:48 -04:00
Jeremy Stretch
9351f686b1 Added bulk import views for rack roles, device roles, and platforms 2017-10-09 15:28:46 -04:00
Jeremy Stretch
42e557bd3f Added bulk import view for tenant groups 2017-10-09 15:09:40 -04:00
Jeremy Stretch
35c2c8e8de #1444: Added a serial number field to the rack model 2017-10-09 15:01:57 -04:00
Jeremy Stretch
960e1d49c2 Closes #1551: Added CPAK, CFP2, and CFP4 100GE interface form factors 2017-10-09 14:45:10 -04:00
Jeremy Stretch
100d979d3e Fixes #1554: Don't require form_factor when creating an interface assigned to a virtual machine 2017-10-09 14:27:31 -04:00
Jeremy Stretch
b734599f40 Closes #1557: Added filtering for virtual machine interfaces 2017-10-09 13:59:53 -04:00
Jeremy Stretch
b41b4fc584 Closes #1506: Extended rack facility ID field from 30 to 50 characters 2017-10-09 13:44:22 -04:00
Jeremy Stretch
b69bf58c63 Closes #1548: Automatically populate tenant assignment when adding an IP address from the prefix view 2017-10-09 13:40:28 -04:00
Jeremy Stretch
a73e598a6e #1561: Selected related primary IPs for devices under global search 2017-10-09 13:32:30 -04:00
huzichunjohn
af2de6776d Closes #1561: Add Primary IP for a device to Search Results (#1565) 2017-10-09 13:28:39 -04:00
Jeremy Stretch
98195c9e74 Fixes #1550: Corrected interface connections link in navigation menu 2017-10-06 15:39:39 -04:00
Jeremy Stretch
97188ad85b Fixes #1563: Made necessary updates for Django REST Framework v3.7.0 2017-10-06 15:26:56 -04:00
Jeremy Stretch
9c673d2a2e Updated the docs to call out PostgreSQL 9.4+ as a requirement 2017-09-29 14:36:55 -04:00
Jeremy Stretch
77182d8711 Redesigned the navigation menus 2017-09-29 14:27:42 -04:00
Matt Layher
a01c9ff379 Minor LDAP documentation formatting cleanup 2017-09-29 13:22:42 -04:00
Jeremy Stretch
32399b0cb5 Merge branch 'develop-2.2' of github.com:digitalocean/netbox into develop-2.2 2017-09-29 12:54:38 -04:00
Jeremy Stretch
fe3cdb1e20 Merge pull request #1359 from paravoid/develop-2.1
Add bulk (CSV) import/export for more object types
2017-09-29 12:35:36 -04:00
Jeremy Stretch
3d25cecc69 Merge branch 'develop-2.2' into develop-2.1 2017-09-29 12:30:36 -04:00
Jeremy Stretch
068c0ff36c Merge branch 'develop' into develop-2.1 2017-09-29 12:26:54 -04:00
Jeremy Stretch
86a298792a Corrected default return URL for BulkComponentCreateView 2017-09-29 12:15:14 -04:00
Jeremy Stretch
a6599874db #1493: Extended DeviceRole to include a toggle indicating applicability to virtual machines 2017-09-29 12:11:20 -04:00
Jeremy Stretch
0deae84ecb Renamed column 2017-09-29 11:50:26 -04:00
Jeremy Stretch
6243fbfd0d Closes #1493: Added functional roles for virtual machines 2017-09-29 11:13:41 -04:00
Jeremy Stretch
f49d7ce1da Removed deprecated NETBOX_USERNAME and NETBOX_PASSWORD configuration parameters 2017-09-29 10:18:18 -04:00
Jeremy Stretch
75fed52bb6 Fixed YAML 2017-09-28 17:55:05 -04:00
Jeremy Stretch
90ec177360 Troubleshooting Travis CI 2017-09-28 17:47:11 -04:00
Jeremy Stretch
667eadb430 Removed Docker bits from CI build 2017-09-28 17:36:05 -04:00
Jeremy Stretch
afbe0bc307 Merge pull request #1544 from digitalocean/reports
Closes #1511: Implemented reports
2017-09-28 17:16:10 -04:00
Jeremy Stretch
c65af6a74f Trying to get Travis to run PostgreSQL 9.4 2017-09-28 17:03:47 -04:00
Jeremy Stretch
669aee2d73 Bumped psycopg2 hoping to fix jsonb errors in CI 2017-09-28 16:43:35 -04:00
Jeremy Stretch
67f0dfa449 We need PostgreSQL 9.4 or higher for jsonb fields 2017-09-28 16:32:59 -04:00
Jeremy Stretch
e630a1ace1 Added docs for reports 2017-09-28 16:25:42 -04:00
Jeremy Stretch
21485ca6e2 Restrict the running of reports via API to POST requests 2017-09-28 16:04:37 -04:00
Jeremy Stretch
87fb4af5cb Cosmetic cleanup 2017-09-28 15:18:40 -04:00
Jeremy Stretch
e86ec75513 Fixed 'failure' field name 2017-09-28 14:57:54 -04:00
Jeremy Stretch
1ad099d9fd Added nav menu link to reports list 2017-09-28 14:54:11 -04:00
Jeremy Stretch
f9a677c1a3 Bugfixes 2017-09-28 13:36:50 -04:00
Jeremy Stretch
6c6b67330f Expanded report view 2017-09-28 13:35:18 -04:00
Jeremy Stretch
2b33e78fd3 Added a run view for reports 2017-09-28 12:51:10 -04:00
Jeremy Stretch
b65e9fe0f5 Fixed runreport management command 2017-09-28 12:50:52 -04:00
Jeremy Stretch
571b817f04 Moved ReportResult creation into Report.run() 2017-09-28 12:50:32 -04:00
Jeremy Stretch
e2b2815545 Fixes #1542: Added cluster field to device view 2017-09-28 10:21:41 -04:00
Jeremy Stretch
0ea58ab268 Fixes #1543: Added missing virtual machine field to IP address and service filters 2017-09-28 09:29:12 -04:00
Jeremy Stretch
2fbb39bf6f Started adding a view for individual reports 2017-09-27 17:39:22 -04:00
Jeremy Stretch
f4c87b3739 Removed custom permission 2017-09-26 17:31:16 -04:00
Jeremy Stretch
696d91daa3 Prettied up the reports list 2017-09-26 17:17:28 -04:00
Jeremy Stretch
9a1781e6e7 Added url field for nested report results 2017-09-26 16:55:25 -04:00
Jeremy Stretch
3395b51086 Cleaned up the API quite a bit 2017-09-26 16:36:43 -04:00
Jeremy Stretch
a3a77a0bb6 Fixes #1540: Added missing 'cluster' field to WritableDeviceSerializer 2017-09-26 15:45:14 -04:00
Jeremy Stretch
d35a2b0faa Extended reports API 2017-09-25 17:27:58 -04:00
Jeremy Stretch
88c57d002d Added navigation panel 2017-09-25 16:22:50 -04:00
Jeremy Stretch
0d9ff907a8 Merge branch 'develop' of github.com:digitalocean/netbox into develop-2.2 2017-09-25 15:12:47 -04:00
Jeremy Stretch
02d8897b82 Post-release version bump 2017-09-25 14:53:10 -04:00
Jeremy Stretch
aa8f734bd1 Merge pull request #1537 from digitalocean/develop
Release v2.1.5
2017-09-25 14:52:43 -04:00
Jeremy Stretch
0015484679 Release v2.1.5 2017-09-25 14:50:45 -04:00
Jeremy Stretch
aa5aa798e6 Fixes #1486: Ignore subinterface IDs when validating LLDP neighbor connections 2017-09-25 14:33:32 -04:00
Jeremy Stretch
63a4a70420 #1499: Added utilization graph to child prefixes for aggregates 2017-09-25 13:39:03 -04:00
Jeremy Stretch
2badb04a03 Fixes #1489: Corrected server error on validation of empty required custom field 2017-09-25 13:37:11 -04:00
Jeremy Stretch
a328e12642 Closes #1536: Improved formatting of aggregate prefix statistics 2017-09-25 12:14:55 -04:00
Jeremy Stretch
ce9f1eb201 Closes #1499: Added utilization graph to child prefixes table 2017-09-22 21:17:46 -04:00
Jeremy Stretch
b454ac9ce5 Merge pull request #1524 from larsuhartmann/develop
mentioned support of nested groups in LDAP installation documentation.
2017-09-22 16:19:32 -04:00
Jeremy Stretch
a010f7439d More work on #1523 (natural ordering for interfaces) 2017-09-22 16:18:43 -04:00
Jeremy Stretch
d7b0ba57e0 Merge pull request #1526 from tarkatronic/order_naturally_enhancements
Fixes #1523 - Interface.objects.order_naturally() enhancements
2017-09-22 14:12:43 -04:00
Jeremy Stretch
2ca161f3d8 Closes #1509: Extended cluster model to allow site assignment 2017-09-22 12:53:09 -04:00
Jeremy Stretch
79fdf641c0 Implemented rough UI for accessing report results 2017-09-22 12:11:10 -04:00
Joey Wilhelm
89194c067b Another test case to ensure subinterface ordering 2017-09-21 17:21:21 -07:00
Jeremy Stretch
b5ab498e75 Initial work on reports API 2017-09-21 16:32:05 -04:00
Joey Wilhelm
126a5e5e4e Fix order_naturally with unbalanced names and use RawSQL instead of extra 2017-09-21 11:00:55 -07:00
Jeremy Stretch
16d1f9aca8 Tweaked report run logic 2017-09-21 13:49:04 -04:00
Lars Hartmann
466ab0b404 mentioned support of nested groups in LDAP installation documentation. 2017-09-21 16:36:25 +02:00
Jeremy Stretch
a52a871a94 Fixes #1522: Removed object create/edit forms from the browsable API 2017-09-20 16:54:27 -04:00
Jeremy Stretch
4f9379a5e2 Cleaned up device interfaces/IPs table 2017-09-20 14:44:51 -04:00
Jeremy Stretch
4cfad2ef3b Added virtualization models to nbshell and fixed up status constants 2017-09-20 14:03:56 -04:00
Jeremy Stretch
6e81b0ac32 Merge pull request #1517 from huzichunjohn/issue_1514
Fixes #1514: No field to edit Cluster comments
2017-09-20 11:53:43 -04:00
Jeremy Stretch
d87023e1ce Limited virtual machine interfaces to type "virtual" (removed LAG type) 2017-09-20 11:47:37 -04:00
Jeremy Stretch
e31d2c9857 Fixes #1520: Redirect on GET request to bulk edit/delete views 2017-09-20 09:56:20 -04:00
johnhu
2174ccf016 Fixes #1514: No field to edit Cluster comments 2017-09-20 11:48:49 +00:00
Jeremy Stretch
8f1607e010 Initial work on reports 2017-09-19 17:47:42 -04:00
Jeremy Stretch
e93129f1ae Enabled export templates for clusters and virtual machines 2017-09-19 11:31:29 -04:00
Jeremy Stretch
35b4e190d9 Fixes #1508: Enabled creating custom fields for clusters and virtual machines via the admin UI 2017-09-19 10:40:38 -04:00
Ryan Breaker
b252f52f8e Added missing package for CentOS and Fedora (#1470)
Without the added package, installation fails during `pip install -r requirements.txt` on the pycrypto package for me in CentOS 6 and 7 and Fedora.
2017-09-18 16:37:51 -04:00
Chris Howells
eba30c4d79 Document the PATCH method. Closes #1481. (#1482) 2017-09-18 16:37:09 -04:00
Jeremy Stretch
ae1662e433 Fixes #1507: Fixed error when creating the next available IP from a prefix within a VRF 2017-09-18 16:34:28 -04:00
Jeremy Stretch
512a3bf09a Fixed #1500: Allow assigning IP addresses to virtual machines during CSV bulk import 2017-09-18 13:30:20 -04:00
Jeremy Stretch
9927ce14d3 Fixes #1502: Fixed CSV export for clusters and virtual machines 2017-09-18 13:12:58 -04:00
Jeremy Stretch
e5e169f476 Fixes #1504: Added missing vcpus/memory/disk fields to virtual machine API serializer 2017-09-18 12:54:50 -04:00
Jeremy Stretch
81df837a33 Closes #1485: Added LOGIN_BANNER configuration setting to display a banner on the login page 2017-09-15 15:28:09 -04:00
Jeremy Stretch
b84b526a43 Closes #1484: Added individual "add VLAN" buttons on the VLAN groups list 2017-09-15 15:14:35 -04:00
Jeremy Stretch
5fc92ab0e6 Removed old RPC-based LLDP neighbors code 2017-09-15 14:07:09 -04:00
Jeremy Stretch
039c2a6d57 Closes #1496: Added primary IP column to virtual machine list 2017-09-15 11:47:29 -04:00
Jeremy Stretch
d053422657 Fixes #1497: Added cluster_id filter 2017-09-15 11:36:06 -04:00
Jeremy Stretch
90d9eb3520 Closes #1490: Rearrange order of items in the virtualization nav menu 2017-09-15 11:08:23 -04:00
Jeremy Stretch
3466da4338 Merge branch 'develop' into develop-2.2
Conflicts:
	netbox/dcim/forms.py
2017-09-14 14:58:45 -04:00
Jeremy Stretch
17c1a1e465 Use add_blank_choice() to prepend a null choice to field options 2017-09-14 14:45:45 -04:00
Jeremy Stretch
6872ab0e14 Added a status field for virtual machines 2017-09-14 14:35:34 -04:00
Jeremy Stretch
136d16b7fd Don't add a placeholder option to multiselect widgets 2017-09-12 14:14:08 -04:00
Jeremy Stretch
d651deb31c Renamed form_class attribute to model_form for consistency 2017-09-12 13:55:11 -04:00
Jeremy Stretch
8fb37233f4 Started documenting utility views 2017-09-12 13:53:15 -04:00
Jeremy Stretch
700194b80d Implemented bulk interface creation for virtual machines 2017-09-12 12:49:01 -04:00
Jeremy Stretch
ef2dd673ec Finished bulk edit/delete views 2017-09-11 16:14:05 -04:00
Jeremy Stretch
7a3adca771 Added device count to clusters table 2017-09-11 15:51:03 -04:00
Jeremy Stretch
f66b0b7fea Finished implementing CSV imports 2017-09-11 15:42:18 -04:00
huzichunjohn
b6df0209ba Fixes #1472: Secret truncated when using '<' character (#1477) 2017-09-11 10:55:04 -04:00
Jeremy Stretch
6b53d263fe Fixes #1469: Allow a NAT IP to be assigned as the primary IP for a device 2017-09-01 13:00:44 -04:00
Jeremy Stretch
00024240bb Merge branch 'virtualization' into develop-2.2 2017-09-01 12:29:12 -04:00
Jeremy Stretch
237a889f54 Preserve query parameters on redirect after clicking "add another" 2017-09-01 12:28:34 -04:00
Jeremy Stretch
f5c265b7b5 Modified form action to preserve query parameters 2017-09-01 12:25:57 -04:00
Jeremy Stretch
1341ab5703 Fixed selection of sites not assigned to a region 2017-09-01 10:10:10 -04:00
Jeremy Stretch
3ded8196c4 Fixed IP toggle button 2017-09-01 10:09:52 -04:00
Jeremy Stretch
cbc239ceaa Added virtualization API tests 2017-08-31 14:03:12 -04:00
Jeremy Stretch
a6b43baafe Added documentation for virtualization data model 2017-08-31 13:22:12 -04:00
Jeremy Stretch
ba0a261840 Removed extraneous import 2017-08-31 13:04:56 -04:00
Jeremy Stretch
3bb0d523d3 Enabled services on virtual machines 2017-08-31 12:50:35 -04:00
Jeremy Stretch
85cb333a5d Added virtualization to API root 2017-08-31 11:36:29 -04:00
Jeremy Stretch
2abb073b3a Regenerated migrations 2017-08-31 10:17:59 -04:00
Jeremy Stretch
0634386b2e Merge branch 'develop' into virtualization 2017-08-31 10:14:50 -04:00
Jeremy Stretch
372e9335b1 Post-release version bump 2017-08-30 14:44:55 -04:00
Jeremy Stretch
f6d1163ddd Merge pull request #1461 from digitalocean/develop
Release v2.1.4
2017-08-30 14:43:01 -04:00
Jeremy Stretch
e6b6082a2b Release v2.1.4 2017-08-30 14:40:52 -04:00
Jeremy Stretch
e0ee0b9254 Closes #1460: Hostnames with no domain are now acceptable in custom URL fields 2017-08-30 14:08:39 -04:00
Jeremy Stretch
fde47133da Fix global search placeholder text 2017-08-30 12:07:11 -04:00
Jeremy Stretch
643f64df3f Closes #1341: Added a MEDIA_ROOT configuration setting to specify where uploaded files are stored on disk 2017-08-30 11:11:59 -04:00
Jeremy Stretch
a6ec1ba23b Closes #1376: Ignore anycast when detecting duplicate IPs 2017-08-30 10:46:30 -04:00
Jeremy Stretch
ae231b1d1b Moved circuit speed humanization to a template tag 2017-08-29 22:42:06 -04:00
Jeremy Stretch
ecdf66c454 Closes #1326: Added dropdown widget with common values for circuit speed fields 2017-08-29 22:29:30 -04:00
Jeremy Stretch
aaca28c1a6 Closes #1402: Increased max length of name field for device components 2017-08-29 17:27:30 -04:00
Jeremy Stretch
f0d8fdf2d8 Closes #1431: Added interface form factor for 10GBASE-CX4 2017-08-29 17:02:09 -04:00
Jeremy Stretch
c9288a052a Closes #1432: Added a commit_rate field to the circuits list search form 2017-08-29 16:48:11 -04:00
Jeremy Stretch
4b3a425888 Fixed role link on IP address view 2017-08-29 16:14:32 -04:00
Jeremy Stretch
71069ced7f Added platform info to VM view 2017-08-29 16:12:52 -04:00
Jeremy Stretch
1308839a41 Fixes #1458: Corrected permission name on prefix/VLAN roles list 2017-08-29 15:36:51 -04:00
Jeremy Stretch
6d849ad5f6 Deleted obsolete templates 2017-08-29 15:31:11 -04:00
Jeremy Stretch
fa95191792 Allowed assigning an IP address to either a device or a VM 2017-08-29 15:26:35 -04:00
Jeremy Stretch
e945aafd7b Added fitlers to return the verbose model name of an object 2017-08-29 15:10:52 -04:00
Jeremy Stretch
e9f75adddb Ditched VMInterface in favor of reusing dcim.Interface 2017-08-29 14:24:58 -04:00
Jeremy Stretch
3e4b66675b Rearranged circuits navigation dropdown 2017-08-24 12:51:50 -04:00
Jeremy Stretch
6f8d3f7a57 Fixes #1443: Fixed API validation error involving custom field data 2017-08-22 11:31:33 -04:00
Jeremy Stretch
eb994a691a Added style to BaseTable's empty_text 2017-08-21 17:38:27 -04:00
Jeremy Stretch
6ebd8e78c8 Added VMs to tenant view 2017-08-21 17:21:02 -04:00
Jeremy Stretch
daa947e7eb Added virtualization models to home/search 2017-08-21 17:17:01 -04:00
Jeremy Stretch
b117130aac Added icons 2017-08-21 17:02:35 -04:00
Jeremy Stretch
4587aba1d4 Added views to add/remove hosts to/from clusters 2017-08-21 16:53:36 -04:00
Jeremy Stretch
ae73e6f04d Removed task list formatting 2017-08-21 11:10:07 -04:00
Jeremy Stretch
5930a64203 Converted IPAddress.interface to a GenericForeignKey 2017-08-18 16:57:20 -04:00
Jeremy Stretch
97536c4e9b Genericized component edit/delete views 2017-08-18 14:43:11 -04:00
Jeremy Stretch
e81e33af38 Added views for VM interfaces 2017-08-18 14:37:19 -04:00
Jeremy Stretch
a7c56eab86 Moved DeviceComponentForm to utilities.forms 2017-08-18 13:10:19 -04:00
Jeremy Stretch
6e580a731e Added template names to device component create views 2017-08-18 12:29:39 -04:00
Jeremy Stretch
7843a6bf77 Fixed devicetype_id filter for DeviceType components 2017-08-18 10:27:43 -04:00
Jeremy Stretch
fa6d933544 Converted list of issue types to a task list 2017-08-17 15:14:20 -04:00
Jeremy Stretch
83085d08ca Revised the GitHub issue template 2017-08-17 15:11:55 -04:00
Jeremy Stretch
a02055e9b5 Reworked relationship between devices and clusters 2017-08-16 17:21:34 -04:00
Jeremy Stretch
9acd792abe Added virtualization filters 2017-08-16 17:00:17 -04:00
Jeremy Stretch
4ef55502b4 Updates for v2.1.3 and expanded VirtualMachine model 2017-08-16 15:25:33 -04:00
Jeremy Stretch
3009863877 Merge branch 'develop' into virtualization 2017-08-16 15:08:37 -04:00
Jeremy Stretch
70a6b171d0 Fixes #1429: Fixed uptime formatting on device status page 2017-08-16 11:20:52 -04:00
Jeremy Stretch
9a25b5458d Merge pull request #1430 from xmudrii/fix-docs
docs: web-server link to new ssl tutorials for 16.04
2017-08-15 21:39:59 -04:00
Marko Mudrinić
416aeb4072 docs: web-server link to new ssl tutorials for 16.04 2017-08-15 23:39:10 +02:00
Jeremy Stretch
0c1594b8f8 Post-release version bump 2017-08-15 15:53:10 -04:00
Jeremy Stretch
5be30bd278 Merge pull request #1428 from digitalocean/develop
Release v2.1.3
2017-08-15 15:52:34 -04:00
Jeremy Stretch
669ae104a4 Release v2.1.3 2017-08-15 15:50:51 -04:00
Jeremy Stretch
c37cfeb74f Fixed page titles in the browsable API 2017-08-15 15:30:45 -04:00
Jeremy Stretch
51b1da660a Fixes #1330: Raise validation error when assigning an unrelated IP as the primary IP for a device 2017-08-15 14:05:02 -04:00
Jeremy Stretch
c394985b1b Fixes #1421: Improved model validation logic for API serializers 2017-08-15 13:54:04 -04:00
Jeremy Stretch
04c300b8e2 Fixes #1420: Exclude virtual interfaces from device LLDP neighbors view 2017-08-15 11:07:26 -04:00
Jeremy Stretch
117f33afc5 Fixes #1419: Allow editing image attachments without re-uploading an image 2017-08-11 10:47:06 -04:00
Jeremy Stretch
3d92669df5 Corrected HTTP verb in API docs 2017-08-10 11:41:25 -04:00
Jeremy Stretch
63757af1a0 Expanded API overview documentation 2017-08-09 16:55:49 -04:00
Jeremy Stretch
e6a58b6700 Fixes #1415: Ignore leading/trailing semicolons in topology map device lists 2017-08-09 15:46:40 -04:00
Jeremy Stretch
8fb504c963 Tweaked installation docs to better accommodate Python 3 2017-08-09 14:19:33 -04:00
Jeremy Stretch
babe42ef35 Closes #1414: Selecting a site from the rack filters automatically updates the available rack groups 2017-08-09 13:59:25 -04:00
Jeremy Stretch
b67e3ff957 Implemented initial virtualization API 2017-08-08 16:33:34 -04:00
Jeremy Stretch
7557220d5d Fixes #1389: Avoid splitting carat/prefix on prefix list 2017-08-08 11:48:51 -04:00
Jeremy Stretch
50ccccc3f4 Merge pull request #1403 from huzichunjohn/issue_1400
Fixes #1400: Device interface shows twice on IP Addresses page
2017-08-08 08:23:14 -04:00
johnhu
b6afc68847 Fixes #1400: Device interface shows twice on IP Addresses page 2017-08-08 11:44:50 +00:00
Jeremy Stretch
d06813f528 Initial work on virtualization support (#142) 2017-08-04 17:02:52 -04:00
Jeremy Stretch
fa7b7288c9 Merge pull request #1398 from digitalocean/develop
Release v2.1.2
2017-08-04 10:54:29 -04:00
Jeremy Stretch
36d5debe74 Post-release version bump 2017-08-04 10:52: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
Faidon Liambotis
4544893b4c Allow import/export of device types (#1347) 2017-07-25 17:21:34 +03:00
Faidon Liambotis
f30fa925ea Allow import/export of manufacturers (#1347) 2017-07-25 17:21:34 +03:00
Faidon Liambotis
beb91559e5 Allow import/export of rack groups (#1347) 2017-07-25 17:21:34 +03:00
Faidon Liambotis
fbc5e74318 Allow import/export of regions (#1347) 2017-07-25 17:21:34 +03: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
ad95b86fdd Merge pull request #1201 from digitalocean/develop
Release v2.0.3
2017-05-18 14:37:19 -04:00
Jeremy Stretch
769232f368 Post-release version bump 2017-05-18 14:32:11 -04:00
Jeremy Stretch
9cf10eecd1 Release v2.0.3 2017-05-18 14:31:48 -04:00
Jeremy Stretch
f927d5b8f5 Closes #1198: Allow filtering unracked devices on device list 2017-05-18 14:27:07 -04:00
Jeremy Stretch
7fa696dace Fixes #1195: Unable to create an interface connection when searching for peer device 2017-05-18 13:33:26 -04:00
Jeremy Stretch
feac93389c Fixes #1200: Form validation error when connecting power ports to power outlets 2017-05-18 12:11:14 -04:00
Jeremy Stretch
f7969d91b3 Fixes #1199: Bulk import of secrets does not prompt user to generate a session key 2017-05-18 09:17:41 -04:00
Jeremy Stretch
92aafb9043 Added WSGIPassAuthorization to example Apache config 2017-05-17 17:23:08 -04:00
Jeremy Stretch
f9328d53b4 Fixes #1197: Fixed status assignment during bulk import of devices, prefixes, IPs, and VLANs 2017-05-17 17:16:02 -04:00
Jeremy Stretch
f1cbc7da33 Fixes #1157: Hide nav menu search bar on small displays 2017-05-17 16:00:46 -04:00
Jeremy Stretch
01becd21de Closes #1196: Added a lag_id filter to the API interfaces view 2017-05-17 14:43:44 -04:00
Jeremy Stretch
7768b94279 Fixes #1188: Serialize interface LAG as nested objected (API) 2017-05-17 14:32:39 -04:00
Jeremy Stretch
3bc51c8e69 Fixes #1191: Bulk selection of IPs under a prefix incorrect when "select all" is used 2017-05-17 14:23:08 -04:00
Jeremy Stretch
d206be91d5 Fixes #1130: Added zlib1g-dev to Ubuntu/Debian packages list 2017-05-17 12:48:31 -04:00
Jeremy Stretch
6e69c9e375 Restored the option to hide the paginator on panel tables 2017-05-17 12:18:32 -04:00
Jeremy Stretch
f2846af4ec Fixes #1189: Enforce consistent ordering of objects returned by a global search 2017-05-17 12:16:57 -04:00
Jeremy Stretch
657eed1dc9 Merge pull request #1185 from ryanmerolle/patch-1
Added vagrant alternative installation link
2017-05-16 16:53:01 -04:00
Jeremy Stretch
e351ab0171 Fixes #1186: Corrected VLAN edit form so that site assignment is not required 2017-05-16 16:30:28 -04:00
Jeremy Stretch
779446da64 Fixes #1187: Fixed table pagination by introducing a custom table template 2017-05-16 16:19:55 -04:00
ryanmerolle
2ff0d7aa83 Added vagrant alternative installation link 2017-05-16 07:13:05 -04:00
Jeremy Stretch
7ceb64b57b Post-release version bump 2017-05-15 13:24:37 -04:00
Jeremy Stretch
43e1e0dbc8 Merge pull request #1181 from digitalocean/develop
Release v2.0.2
2017-05-15 13:23:33 -04:00
Jeremy Stretch
a1c12cfd77 Release v2.0.2 2017-05-15 13:19:18 -04:00
Jeremy Stretch
aa6ca21a34 PEP8 fix 2017-05-15 13:18:49 -04:00
Jeremy Stretch
a49521d683 #1177: Render planned connections as dashed lines on topology maps 2017-05-15 13:11:20 -04:00
Jeremy Stretch
3be6e5b015 Closes #1179: Adjust topology map text color based on node background 2017-05-15 12:56:16 -04:00
Jeremy Stretch
ca1725b98c Fixes #1178: Fix API representation of connected interface's form factor 2017-05-15 11:03:11 -04:00
Jeremy Stretch
d11dfe2ced Closes #1137: Allow filtering devices list by rack 2017-05-12 22:41:27 -04:00
Jeremy Stretch
ab30ba1e1b Fixed dynamic selection of device type filter on devices list 2017-05-12 22:20:21 -04:00
Jeremy Stretch
7f23cb9bf5 Closes #1122: Include NAT inside IPs in IP address list 2017-05-12 22:11:20 -04:00
Jeremy Stretch
c9d3cf301e Fixes #1173: Tweak interface manager to fall back to naive ordering 2017-05-12 16:10:18 -04:00
Jeremy Stretch
67282882fa Fixed RelatedObjectDoesNotExist error when trying to create a new device 2017-05-12 15:55:18 -04:00
Jeremy Stretch
73bf4f45c3 Adapted model get_display_name() to better handle unsaved instances 2017-05-12 15:31:34 -04:00
Jeremy Stretch
66ae62fb91 Closes #1172: Linkify racks in side-by-side elevations view 2017-05-12 14:19:37 -04:00
Jeremy Stretch
8bae804508 Closes #1170: Include A and Z sites for circuits in global search results 2017-05-12 12:12:47 -04:00
Jeremy Stretch
d87acc97c3 Fixes #1171: Allow removing site assignment when bulk editing VLANs 2017-05-12 12:06:37 -04:00
Jeremy Stretch
f9b2c59974 Moved tenancy to separate panel on bulk IP creation form 2017-05-12 12:04:06 -04:00
Jeremy Stretch
a870a3b918 Fixes #1166: Re-implemented bulk IP address creation 2017-05-12 12:00:26 -04:00
Jeremy Stretch
008ed34553 Fixes #1168: Total count of obejcts missing from list view paginator 2017-05-11 23:30:23 -04:00
Jeremy Stretch
e239045688 PEP8 fixes 2017-05-11 17:54:43 -04:00
Jeremy Stretch
ed80bfaf02 Fixed selector initializations for TenancyForms 2017-05-11 17:52:23 -04:00
Jeremy Stretch
473b35f9a3 Added tenant_group/tenant form section to all objects with tenancy 2017-05-11 17:35:20 -04:00
Jeremy Stretch
45bb7eec0b Corrected queryset filter when parent_field is None 2017-05-11 17:20:50 -04:00
Jeremy Stretch
58bb029666 Closes #1167: Introduced ChainedModelChoiceFields 2017-05-11 16:30:16 -04:00
Jeremy Stretch
0f97478b55 Fixes #1161: Fix "add another" behavior when creating an API token 2017-05-10 22:22:49 -04:00
Jeremy Stretch
9efa70a551 Fixes #1159: Only superusers can see "edit IP" buttons on the device interfaces list 2017-05-10 16:02:50 -04:00
Jeremy Stretch
ed65721085 Fixes #1160: Linkify secrets and tenants in global search results 2017-05-10 13:16:33 -04:00
Jeremy Stretch
83688fceb7 Fixes #1158: Exception thrown when creating a device component with an invalid name 2017-05-10 11:23:54 -04:00
Jeremy Stretch
088f75ba0c Added client_max_body_size to nginx config; removed statement disabling access logging 2017-05-10 11:11:03 -04:00
Jeremy Stretch
188cfa08a9 Post-release version bump 2017-05-09 22:48:14 -04:00
Jeremy Stretch
f731900e2f Merge pull request #1154 from digitalocean/develop
Release v2.0.1
2017-05-09 22:47:52 -04:00
Jeremy Stretch
b89bd24bed Release v2.0.1 2017-05-09 22:41:37 -04:00
Jeremy Stretch
effda88b51 Fixes #1153: UnicodeEncodeError when searching for non-ASCII characters on Python 2 2017-05-09 22:40:46 -04:00
Jeremy Stretch
3844f70a4d Fixes #1152: Unable to edit user keys 2017-05-09 17:53:37 -04:00
Jeremy Stretch
8e333757f9 Fixes #1150: Error when uploading image attachments with Unicode names under Python 2 2017-05-09 17:12:45 -04:00
Jeremy Stretch
0fb12bcc9c Fixes #1151: name 'escape' is not defined 2017-05-09 17:06:17 -04:00
Jeremy Stretch
44d78ef92a Fixes #1149: Port list does not populate when creating a console or power connection 2017-05-09 17:01:43 -04:00
Jeremy Stretch
ebb6729a26 Post-release version bump 2017-05-09 15:13:40 -04:00
Jeremy Stretch
b1bcaa33e7 Merge pull request #1148 from digitalocean/develop
Release v2.0.0
2017-05-09 15:09:28 -04:00
Jeremy Stretch
a35f8bddde PEP8 fix 2017-05-09 14:44:32 -04:00
Jeremy Stretch
8fbe7ba742 Release v2.0.0! 2017-05-09 14:29:11 -04:00
Jeremy Stretch
f039b0b6e9 Closes #960: Added form factor for Juniper VCP interfaces 2017-05-09 12:00:49 -04:00
Jeremy Stretch
9ad9ef7957 Fixed incorrect API URL in IPAddressForm 2017-05-09 11:11:30 -04:00
Jeremy Stretch
5c7db04465 Closes #853: Add 'status' field to device bulk import form 2017-05-09 10:25:30 -04:00
Jeremy Stretch
838105fb65 Merging v2.0 development into mainline (#1145)
Merging v2.0 development into mainline
2017-05-08 15:06:57 -04:00
Jeremy Stretch
5ca87c0f20 Merge branch 'develop' into v2-develop 2017-05-08 15:02:06 -04:00
Jeremy Stretch
af4edff370 Related to #1144: Allow multiple status selections when filtering device list 2017-05-08 14:56:25 -04:00
Jeremy Stretch
f40c048475 Fixes #1144: Allow multiple status selections for Prefix, IP address, and VLAN filters 2017-05-08 14:32:29 -04:00
Jeremy Stretch
77247cccbe Closes #154: Expand device status field options 2017-05-08 13:55:19 -04:00
Jeremy Stretch
fcfcd77bfd Moved LAG members list to the description column 2017-05-05 15:37:42 -04:00
Jeremy Stretch
b3667befb4 Removed reduntant title block 2017-05-05 15:24:58 -04:00
Jeremy Stretch
a6cb0e0a96 Updated console/power connection icons 2017-05-03 17:24:57 -04:00
Jeremy Stretch
c047f943de Fixes #403: Record console/power/interface connects and disconnects as user actions 2017-05-03 17:12:34 -04:00
Jeremy Stretch
79089cc47e Introduced an object import template 2017-05-03 15:41:36 -04:00
Jeremy Stretch
3c631902e1 Closes #1100: Add a "view all" link to completed bulk import views is_pool for prefixes 2017-05-03 15:27:26 -04:00
Jeremy Stretch
379c24a012 Fixed typo in template 2017-05-03 14:32:27 -04:00
Brian Ellwood
4035b87693 Allow responsive tables (#1124)
* Make tables responsive #1115

Resolves #1115
2017-05-03 14:30:05 -04:00
Jeremy Stretch
11d1a8c3cf Merge pull request #1128 from digitalocean/readme-branches
Fix misleading build matrix
2017-05-03 14:21:07 -04:00
Jeremy Stretch
7eb9c8265c Fixes #1132: Prompt user to unlock session key when importing secrets 2017-05-03 11:47:28 -04:00
Matt Layher
572beb2311 Fix misleading build matrix
At one point, I had intended to have a matrix of build badges for each different branch and Python version combination.  It seems this is not possible with Travis.

This change replaces "python 2.7" with "status" and clarifies that both Python 2.7 and 3.5 are tested, but Python 3.5 is recommended.
2017-05-02 20:39:43 -04:00
Jeremy Stretch
d861d8bfb8 Fixes #1118: Allow designating an IP as primary for a device while editing the IP 2017-05-02 16:46:23 -04:00
Jeremy Stretch
6791ff6192 Fixes #1125: Include MAC addresses on a device's interface list 2017-05-02 15:01:27 -04:00
Jeremy Stretch
9d9de6b2a3 Fixes #1126: Fix error when editing a user key via admin UI 2017-05-02 14:50:36 -04:00
Jeremy Stretch
1f7ef15ad1 Fixes #1116: Correct object links on recursive deletion error 2017-05-02 11:43:11 -04:00
Jeremy Stretch
16c582ec7a Enable stale .pyc cleanup in upgrade.sh 2017-05-01 16:53:51 -04:00
Jeremy Stretch
de58d0ecca Fixes #1114: Suppress OSError when attempting to access a delete image attachment 2017-04-28 14:26:17 -04:00
Jeremy Stretch
010f6c7f1a Fixes #1113: Fixes server error when attempting to delete an image attachment 2017-04-28 14:05:02 -04:00
Jeremy Stretch
aea5612c39 Closes #1110: Expand bulk edit forms to include boolean fields (e.g. toggle is_pool for prefixes) 2017-04-28 12:32:27 -04:00
Jeremy Stretch
b8b912bdd5 Post-release version adjustment 2017-04-27 15:42:24 -04:00
Jeremy Stretch
e4ca88726e Release v2.0-beta3 2017-04-27 15:37:15 -04:00
Jeremy Stretch
616f109671 Merge branch 'develop' into v2-develop
Conflicts:
	netbox/ipam/forms.py
2017-04-27 15:29:40 -04:00
Jeremy Stretch
8e0580ff96 Improved upgrade script 2017-04-27 14:42:52 -04:00
Jeremy Stretch
4b2e7620dd Switched user nav menu with search form 2017-04-27 13:27:16 -04:00
Jeremy Stretch
b82f25c503 Merge branch 'writable-custom-fields' into v2-develop 2017-04-27 13:05:44 -04:00
Jeremy Stretch
c174c0cc6d Converted all necessary serializers to CustomFieldModelSerializers 2017-04-27 12:50:43 -04:00
Jeremy Stretch
117da337c7 Corrected tests and improved validation 2017-04-27 12:46:04 -04:00
Jeremy Stretch
01da46f753 Fixes #1107: Corrected exception on creating/deleting image attachments 2017-04-27 11:32:08 -04:00
Jeremy Stretch
d17efce4f5 Fixes #1111: Correct database ordering of SessionKey model 2017-04-27 11:27:34 -04:00
Jeremy Stretch
e7a6d1f532 Fixes #1104: Fix VLAN assignment on prefix import 2017-04-26 13:28:09 -04:00
Jeremy Stretch
f643f2c601 Fixes #1103: Correct handling of validation errors when creating IP addresses in bulk 2017-04-26 13:21:38 -04:00
Jeremy Stretch
480faa6461 Removed deprecated IPAddressAssignForm 2017-04-26 13:03:18 -04:00
Jeremy Stretch
1fa084b6be Fixes #1101: Fix AJAX scripting for device component selection forms 2017-04-26 12:53:14 -04:00
Jeremy Stretch
1c86b00b5c Added custom field API tests 2017-04-25 14:53:18 -04:00
Jeremy Stretch
10823e1c37 Got rudimentary custom field creates/updates working 2017-04-25 13:00:28 -04:00
Jeremy Stretch
f73693206f Merge branch 'develop' into v2-develop
Conflicts:
	netbox/circuits/models.py
	netbox/netbox/settings.py
	upgrade.sh
2017-04-21 15:07:48 -04:00
Jeremy Stretch
861c8b29c0 Post-release version bump 2017-04-21 14:56:36 -04:00
Jeremy Stretch
13cc29cd8c Closes #951: Provide a side-by-side view of rack elevations 2017-04-20 13:07:22 -04:00
Jeremy Stretch
864fa17b75 Closes #1008: Moved Docker components into their own repository 2017-04-19 10:58:42 -04:00
Jeremy Stretch
a98c9ed0af Corrected invalid API URL name 2017-04-17 15:52:23 -04:00
Jeremy Stretch
8032aa1ad9 Fixes #1078: Increase default limit for number of objects returned by web form API call 2017-04-17 15:50:00 -04:00
bellwood
5ff4e3b194 Enhance LDAP documentation
Incorporating @marvnrawley's enhancements from #518
2017-04-13 17:03:58 -04:00
Jeremy Stretch
b01bf6089c Merge branch 'develop' into v2-develop
Conflicts:
	netbox/dcim/forms.py
	netbox/dcim/views.py
	netbox/ipam/forms.py
	netbox/templates/_base.html
	netbox/utilities/views.py
2017-04-13 15:42:50 -04:00
Jeremy Stretch
2d52b9fb39 Fixes #1059: Allow filtering of interface connections via API 2017-04-10 16:15:36 -04:00
Jeremy Stretch
863cbb785d Merge pull request #1064 from eliezerlp/v2-develop
Pointing Dockerfile to 'v2-beta' branch instead of a particular tag
2017-04-10 10:57:18 -04:00
Eliezer Paiewonsky
d21b67446f Pointing Dockerfile to 'v2-beta' branch
Was pointing to a particular tag instead.
2017-04-10 10:27:32 -04:00
Jeremy Stretch
57fc6a3f50 Merge branch 'develop' into v2-develop
Conflicts:
	netbox/netbox/settings.py
	netbox/netbox/urls.py
	requirements.txt
2017-04-06 17:01:13 -04:00
Jeremy Stretch
9f67da00d1 Colored nodes in topology maps 2017-04-06 14:12:30 -04:00
Jeremy Stretch
82d53a8c3d Fixes #1049: Prompt user if missing session key when adding/editing a secret 2017-04-06 13:55:40 -04:00
Jeremy Stretch
ee11775425 Fixes #1051: Upgraded django-rest-swagger 2017-04-06 09:40:09 -04:00
Jeremy Stretch
bcdf9ac5ca Merge pull request #1046 from digitalocean/component-filter-by-name
Fixes #1045
2017-04-06 09:14:41 -04:00
Jeremy Stretch
4accdf77f8 Closes #578: Show topology maps not assigned to a site on the home view 2017-04-05 17:33:39 -04:00
Jeremy Stretch
fc46f70153 Closes #430: Include circuits when rendering topology maps 2017-04-05 17:24:40 -04:00
Zach Moody
e7cf7d58b8 Fixes #1045 2017-04-05 15:29:53 -05:00
Jeremy Stretch
d98e9e1838 Resolved RemovedInDjango20Warning deprecation warnings 2017-04-05 14:40:25 -04:00
Jeremy Stretch
369d3aa62e Rearranged URL namespaces to satisfy deprecation warnings 2017-04-05 14:26:33 -04:00
Jeremy Stretch
d4ac6dbfe4 Fixes #1043: Corrected queryset in WritableDeviceSerializer validation 2017-04-05 13:38:23 -04:00
Jeremy Stretch
91d35905fd Reset version 2017-04-05 12:11:48 -04:00
Jeremy Stretch
1a34830f0e Merge pull request #1042 from digitalocean/v2-develop
Release v2.0 Beta 2
2017-04-05 11:57:04 -04:00
Jeremy Stretch
f000df1e15 Release v2.0-beta2 2017-04-05 11:52:14 -04:00
Jeremy Stretch
178f7b4643 Added API endpoints for console and power connections 2017-04-05 11:29:12 -04:00
Jeremy Stretch
f34a8fff6a Extend upgrade script to remove cached bytecode 2017-04-05 10:22:37 -04:00
Jeremy Stretch
bde1f6d199 Bump migration index due to a new migration in 1.9.4-r1 2017-04-04 15:59:33 -04:00
Jeremy Stretch
0d7ee6f208 Merge branch 'develop' into v2-develop 2017-04-04 15:56:33 -04:00
Jeremy Stretch
280d98bad9 #1033: Updated requirements.txt for Django 1.11 2017-04-04 14:42:00 -04:00
Jeremy Stretch
ae5bf747c9 #1033: Tweak SelectWithDisabled option template 2017-04-04 14:36:35 -04:00
Jeremy Stretch
1ae0820ecc #1033: Update ArrayFieldSelectMultiple for Django 1.11 2017-04-04 14:30:00 -04:00
Jeremy Stretch
c09473f41e #1033: Convert SelectWithDisabled to a templatized widget 2017-04-04 14:19:48 -04:00
Jeremy Stretch
99a3e0c399 Corrected CustomFieldModelSerializer behavior when serializing lists of objects 2017-04-04 14:09:14 -04:00
Jeremy Stretch
d2bd4a213b #1033: Convert ColorSelect to a templatized widget 2017-04-04 13:45:32 -04:00
Jeremy Stretch
1dcb0b52e2 #1033: Disable the debug toolbar templates panel by default due to a performance issue under Django 1.11 2017-04-04 13:44:55 -04:00
Jeremy Stretch
409c9c4e23 Merge branch 'develop' into v2-develop
Conflicts:
	netbox/netbox/settings.py
2017-04-04 12:06:49 -04:00
Jeremy Stretch
5187138547 Refactored custom field serializers 2017-04-04 11:25:23 -04:00
Jeremy Stretch
2bb6387dae Fixes #1028: Corrected API endpoint URL name in IPAddressForm 2017-04-03 17:11:22 -04:00
Jeremy Stretch
ca293dc0e7 Corrected topology map link 2017-04-03 16:34:37 -04:00
Jeremy Stretch
ea1d4e7f50 Updated static CSS/JS libraries 2017-04-03 16:15:06 -04:00
Jeremy Stretch
80267aa418 Documented image attachments 2017-04-03 15:56:04 -04:00
Jeremy Stretch
36c31a21b9 Fixed deprecated references to ImageAttachment.obj 2017-04-03 15:51:45 -04:00
Jeremy Stretch
51725d3d9c Added a search box to the navigation menu 2017-04-03 15:33:41 -04:00
Jeremy Stretch
8799a15e73 What would we do without you, PEP8? 2017-04-03 14:26:20 -04:00
Jeremy Stretch
2cde9a82a0 Merge pull request #1026 from digitalocean/image-attachments
#152: Image attachments
2017-04-03 14:21:15 -04:00
Jeremy Stretch
2c1fa628a2 Implemented API endpoints for ImageAttachments 2017-04-03 14:00:15 -04:00
Jeremy Stretch
a67fc64afb Fixes #1025: Applied missing API view filters 2017-04-03 11:04:17 -04:00
Jeremy Stretch
6bbdc2bae1 Enable serving static media through Django 2017-03-31 15:51:17 -04:00
Jeremy Stretch
50d7fd776f Added image attachments to sites and devices 2017-03-31 15:19:44 -04:00
Jeremy Stretch
1c38f705a7 Fixes #1021: Corrected evaluation of API token expiration time 2017-03-31 11:13:37 -04:00
Jeremy Stretch
b643939cc4 Initial work on #152: Image attachments 2017-03-30 21:55:57 -04:00
Jeremy Stretch
3ed3e93b25 Appended a version flag to all CSS/JS references to invalidate browser cache after an upgrade 2017-03-30 15:40:00 -04:00
Jeremy Stretch
f6ea09e581 Removed duplicate 'Regions' section 2017-03-30 10:27:20 -04:00
Jeremy Stretch
998f89216e Updated the docstring for Device 2017-03-30 09:56:47 -04:00
Jeremy Stretch
aefc6ff7b4 Merge branch 'global-search' into v2-develop 2017-03-29 16:45:57 -04:00
Jeremy Stretch
66615f1a96 Prettied things up a bit 2017-03-29 16:45:25 -04:00
Jeremy Stretch
a5dc91c175 Introduced SearchTable for improved performance 2017-03-29 16:05:23 -04:00
Jeremy Stretch
d04436aa0a Search form improvements 2017-03-29 14:22:27 -04:00
Jeremy Stretch
6542b8b198 Base64 decoding tweaks 2017-03-29 13:39:59 -04:00
Jeremy Stretch
afdb24610d Initial work on global search 2017-03-29 12:04:57 -04:00
Jeremy Stretch
58e4bf1cc3 Closes #973: Removed extraneous admin UI functions 2017-03-28 16:41:53 -04:00
Jeremy Stretch
69e54ab410 Token admin form improvements 2017-03-28 12:19:08 -04:00
Jeremy Stretch
116ceb6f93 Added tests for get-session-key API endpoint 2017-03-28 11:30:38 -04:00
Jeremy Stretch
5d022a575a Closes #985: Added preserve_key to get-session-key endpoint 2017-03-28 11:13:13 -04:00
Jeremy Stretch
cf3e7f90d6 Merge pull request #997 from digitalocean/livesearch-fix
Fixes #996
2017-03-27 09:38:48 -04:00
Zach Moody
22bfac746e fix remaining legacy api url paths. 2017-03-24 18:58:19 -05:00
Zach Moody
066a3b8b52 update api_url with new interfaces endpoint. 2017-03-24 18:42:23 -05:00
Zach Moody
48141c0693 Fixes #996 2017-03-24 17:38:06 -05:00
Jeremy Stretch
576e21eb65 Merge branch 'develop' into v2-develop
Conflicts:
	netbox/netbox/settings.py
2017-03-23 17:14:41 -04:00
Jeremy Stretch
93a4327921 Merge branch 'api2' into v2-develop 2017-03-23 13:36:09 -04:00
Jeremy Stretch
0f2bbd7bfd Merge pull request #986 from alejojo/patch-1
Update Dockerfile
2017-03-23 10:28:10 -04:00
Alejandro
516372e5db Update Dockerfile
change branch MASTER > v2.0-beta1
2017-03-23 18:39:14 +09:00
Jeremy Stretch
0899a1052e Only attempt to process session key if user is authenticated 2017-03-22 17:43:29 -04:00
Jeremy Stretch
007fe6a030 Markdown fixes 2017-03-22 10:49:20 -04:00
Jeremy Stretch
f5f9491811 v2.0 Beta 1 release 2017-03-22 10:07:37 -04:00
Jeremy Stretch
04e09c0078 Merge branch 'develop' into api2
Conflicts:
	netbox/circuits/filters.py
2017-03-22 09:48:41 -04:00
Jeremy Stretch
1791a5bb11 Added has_primary_ip filter for Devices 2017-03-21 21:29:03 -04:00
Jeremy Stretch
3e6a99fc22 Allow editing of platform RPC client 2017-03-21 17:33:40 -04:00
Jeremy Stretch
a5419ecc5c RPC API fixes 2017-03-21 17:24:16 -04:00
Jeremy Stretch
a36b138efe Added API doc for working with secrets 2017-03-21 16:00:02 -04:00
Jeremy Stretch
6d30fdb83d Finished work on secrets views; removed path from cookie assignment 2017-03-21 15:30:36 -04:00
Jeremy Stretch
5c4741c5d4 Added section on pagination 2017-03-21 14:34:52 -04:00
Jeremy Stretch
93c748bd3c Merge branch 'develop' into api2 2017-03-21 14:10:53 -04:00
Jeremy Stretch
7ba6e320e7 Fixes #843: Implemented CORS headers for API 2017-03-21 13:53:07 -04:00
Jeremy Stretch
54468ab1a8 Include the API version in responses 2017-03-21 13:23:56 -04:00
Jeremy Stretch
01f5435f63 Tweak how we set the API version 2017-03-21 13:17:50 -04:00
Jeremy Stretch
22768ff6c6 Renamed Module to InventoryItem (prep for #824) 2017-03-21 12:54:08 -04:00
Jeremy Stretch
122526a9d0 Custom name for ConnectedDeviceViewSet 2017-03-20 21:54:01 -04:00
Jeremy Stretch
6cb36a6cee Fixed browsable API breadcrumbs 2017-03-20 21:50:10 -04:00
Jeremy Stretch
925afe0999 Added test case for ConnectedDeviceViewSet 2017-03-20 21:39:40 -04:00
Jeremy Stretch
f743410b4e Renamed rack-units API and added a test 2017-03-20 21:18:37 -04:00
Jeremy Stretch
4a2206ecb1 Removed custom renderers 2017-03-20 17:47:18 -04:00
Jeremy Stretch
ffde2c96c7 Fixed custom renderers to work with paginated data 2017-03-20 17:15:42 -04:00
Jeremy Stretch
2bd46230be Converted ChoiceFieldSerializer to display an object 2017-03-20 16:32:59 -04:00
Jeremy Stretch
b04fe21d65 Wrote API endpoints, tests for ExportTemplates 2017-03-20 16:21:10 -04:00
Jeremy Stretch
266f9cc370 Added API endpoint, tests for Graphs 2017-03-20 15:14:33 -04:00
Jeremy Stretch
42fd14f5c0 Introduced HttpStatusMixin to provide more detail on HTTP response status test failures 2017-03-20 13:46:47 -04:00
Jeremy Stretch
1988c02b7f Enforce API versioning 2017-03-20 12:33:42 -04:00
Jeremy Stretch
517eaa8b80 Expanded API documentation 2017-03-20 12:18:18 -04:00
Jeremy Stretch
1f78462f58 Updated RackViewSet() to be compatible with paginated API 2017-03-20 10:38:09 -04:00
Jeremy Stretch
36bbcc8559 Fix API JS to read response.results for new API 2017-03-20 10:06:25 -04:00
Jeremy Stretch
671d53877a Python3 fixes 2017-03-17 21:39:29 -04:00
Jeremy Stretch
97710a4576 Make CI happy 2017-03-17 17:39:56 -04:00
Jeremy Stretch
c08fae8bce Restore not-so-extraneous 'id' field to all WritableSerializers 2017-03-17 17:32:43 -04:00
Jeremy Stretch
f02dd2f439 Merge branch 'develop' into api2 2017-03-17 17:06:01 -04:00
Jeremy Stretch
e544f1fa1e Removed extraneous 'id' field from all WritableSerializers 2017-03-17 16:20:34 -04:00
Jeremy Stretch
130ff27f26 Wrote tests for secrets API 2017-03-17 16:01:57 -04:00
Jeremy Stretch
79a9ac3bc8 Assign RackReservation user from request context 2017-03-17 14:45:14 -04:00
Jeremy Stretch
c5308d51f4 Make RackReservation.rack editble for API compatability 2017-03-17 14:40:11 -04:00
Jeremy Stretch
a6f4de5817 Wrote tests for IPAM API 2017-03-17 14:36:59 -04:00
Jeremy Stretch
8825a03033 Removed unneeded services endpoint from DCIM API 2017-03-17 12:23:23 -04:00
Jeremy Stretch
abdfc5c597 Finished DCIM API model tests 2017-03-17 12:16:24 -04:00
Jeremy Stretch
be2faaa110 Fixed bug interpreting facility_id as a required field 2017-03-16 17:25:34 -04:00
Jeremy Stretch
f33269e50b First batch of DCIM API tests 2017-03-16 16:50:18 -04:00
Jeremy Stretch
bbc355df07 Improved create/update validation 2017-03-16 14:17:14 -04:00
Jeremy Stretch
d58f9031d1 Wrote tests for tenancy API 2017-03-16 13:29:55 -04:00
Jeremy Stretch
0312016f89 Wrote tests for circuits API 2017-03-16 13:23:01 -04:00
Jeremy Stretch
e3ae013e42 Implemented full read/write support for secrets 2017-03-15 14:47:18 -04:00
Jeremy Stretch
07a2b136b8 Refactored SecretViewSet 2017-03-15 13:48:09 -04:00
Jeremy Stretch
3d76a982aa Removed old API doc 2017-03-15 13:15:09 -04:00
Jeremy Stretch
3dc15068b9 Allow user to delete session key 2017-03-14 14:01:06 -04:00
Jeremy Stretch
4cb30f1ce4 Relate SessionKey to UserKey rather than User 2017-03-14 13:32:07 -04:00
Jeremy Stretch
b868de8d67 Updated user URLs 2017-03-14 12:59:10 -04:00
Jeremy Stretch
04aedcc056 Merge branch 'develop' into api2
Conflicts:
	netbox/templates/users/_user.html
	netbox/users/urls.py
2017-03-14 12:40:28 -04:00
Jeremy Stretch
105d17748e Secrets UI work 2017-03-14 12:32:08 -04:00
Jeremy Stretch
dd27950fae Simplify SessionKey usage 2017-03-14 10:58:57 -04:00
Jeremy Stretch
9e4e3a8dfa Updated API docs 2017-03-13 10:00:13 -04:00
Jeremy Stretch
4d4441217f APIRootView tweaks 2017-03-09 15:18:50 -05:00
Jeremy Stretch
7e51ca9912 Provided a root API view 2017-03-09 15:05:01 -05:00
Jeremy Stretch
94a29be415 Removed deprecated GraphListView 2017-03-09 14:28:52 -05:00
Jeremy Stretch
9dfda83946 Closes #855: Added an API endpoint for recent activity 2017-03-09 14:26:39 -05:00
Jeremy Stretch
41826fc3cb Fixed serialization of CustomFieldChoices 2017-03-09 13:50:30 -05:00
Jeremy Stretch
0ed13f6943 Removed browsable API login/logout 2017-03-09 13:38:15 -05:00
Jeremy Stretch
6c2ed1be22 Standardized API URL definitions 2017-03-09 13:24:02 -05:00
Jeremy Stretch
ddec424429 Replaced RelatedConnectionsView with views.ConnectedDeviceViewSet 2017-03-09 12:18:53 -05:00
Jeremy Stretch
7e6d061646 Converted GetSessionKey and RSAKeyGeneratorView to ViewSets 2017-03-08 17:57:51 -05:00
Jeremy Stretch
c19725506d Cleanup 2017-03-08 16:30:32 -05:00
Jeremy Stretch
a6ceaf8d96 Moved custom field serializers to their own module to avoid circular dependency 2017-03-08 16:18:41 -05:00
Jeremy Stretch
f43fbffdf7 Moved TopologyMaps from DCIM to extras 2017-03-08 16:12:14 -05:00
Jeremy Stretch
68c099a2af Merge branch 'develop' into api2
Conflicts:
	netbox/netbox/settings.py
2017-03-08 15:18:32 -05:00
Jeremy Stretch
4f6d2a8b71 Finished user control panel for tokens 2017-03-08 11:34:47 -05:00
Jeremy Stretch
d58a8ebba0 Initial work on user control panel for tokens 2017-03-07 23:30:53 -05:00
Jeremy Stretch
6be465fe9b Addded is_expired property to Token 2017-03-07 23:30:31 -05:00
Jeremy Stretch
26225aff57 Shorten key length to 20 bytes 2017-03-07 22:56:29 -05:00
Jeremy Stretch
fd55360672 Suppress default permissions for Token model 2017-03-07 22:40:05 -05:00
Jeremy Stretch
0b10d98e0b Initial work on token authentication 2017-03-07 17:17:39 -05:00
Jeremy Stretch
be0a3fb1f2 Corrected merge conflict 2017-03-07 16:55:49 -05:00
Jeremy Stretch
02e89d77bb Merge branch 'develop' into api2
Conflicts:
	netbox/dcim/api/views.py
2017-03-07 14:08:06 -05:00
Jeremy Stretch
a7a7b956b1 Enable API versioning 2017-03-02 16:20:16 -05:00
Jeremy Stretch
9b39ba169c Merge branch 'develop' into api2
Conflicts:
	netbox/dcim/api/serializers.py
	netbox/dcim/api/urls.py
	netbox/dcim/api/views.py
	netbox/dcim/filters.py
	netbox/dcim/tables.py
	requirements.txt
2017-03-02 16:01:25 -05:00
Jeremy Stretch
90fe556e5f Corrected region serializers 2017-02-28 16:23:39 -05:00
Jeremy Stretch
c0152940f9 Merged develop 2017-02-28 16:10:53 -05:00
Jeremy Stretch
8f42f59a80 Merge branch 'develop' into api2
Conflicts:
	netbox/dcim/api/serializers.py
	netbox/dcim/api/views.py
	netbox/dcim/filters.py
2017-02-27 17:04:08 -05:00
Jeremy Stretch
f1518226bd Merge branch 'develop' into api2
Conflicts:
	netbox/dcim/api/serializers.py
2017-02-17 15:12:53 -05:00
Jeremy Stretch
21281789e0 Tweaked ChoiceFieldSerializer to display a field as (value, label) 2017-02-16 14:37:21 -05:00
Jeremy Stretch
b71566f206 Merge branch 'develop' into api2
Conflicts:
	netbox/dcim/api/serializers.py
	netbox/dcim/api/urls.py
	netbox/dcim/api/views.py
	netbox/dcim/filters.py
2017-02-16 14:28:06 -05:00
Jeremy Stretch
0e04d20762 Re-implemented CustomFieldSerializer (read-only for now) 2017-02-09 16:55:54 -05:00
Jeremy Stretch
7040086201 Introduced ChoiceFieldSerializer for choice fields 2017-02-09 15:50:25 -05:00
Jeremy Stretch
6f3c3b6d61 Added API endpoints for device type components 2017-02-03 17:18:47 -05:00
Jeremy Stretch
37f250ddc1 Corrected API URL names 2017-02-03 16:54:13 -05:00
Jeremy Stretch
35f310885e Standardize API URL inclusions 2017-02-03 16:20:14 -05:00
Jeremy Stretch
616ca4fe1f Adapted the web UI to work with the new secrets API 2017-02-03 16:14:42 -05:00
Jeremy Stretch
a9fe39459a Merge branch 'develop' into api2 2017-02-03 14:45:37 -05:00
Jeremy Stretch
a42eeb12d2 Implemented SessionKeys for secrets 2017-02-03 12:49:32 -05:00
Jeremy Stretch
cf66f67fb6 Initial work on using session-based master key ciphers 2017-02-02 21:26:51 -05:00
Jeremy Stretch
2408d78f47 Introduced ability to decrypt secrets by sending the user's private key in an HTTP header 2017-02-01 17:40:50 -05:00
Jeremy Stretch
4f8a5eb1a0 Moved secret views into a ViewSet (no write ability yet) 2017-02-01 16:21:33 -05:00
Jeremy Stretch
06e5966cb4 Include API routers directly where possible 2017-02-01 15:09:23 -05:00
Jeremy Stretch
ea51f1c896 Removed circuit-specific endpoint for CircuitTerminations 2017-02-01 15:01:56 -05:00
Jeremy Stretch
77e5450746 Removed all device-specific API endpoints 2017-02-01 14:34:19 -05:00
Jeremy Stretch
6e10fea119 Started API documentation 2017-02-01 14:04:45 -05:00
Jeremy Stretch
f52c247bd5 Re-implemented Swagger now that URL resolution has been fixed 2017-02-01 12:37:19 -05:00
Jeremy Stretch
0dd857f7a2 Merge branch 'develop' into api2 2017-02-01 12:33:37 -05:00
Jeremy Stretch
bb1f97abc2 Implemented static writable ModelSerializers for all models 2017-01-31 15:35:09 -05:00
Jeremy Stretch
e1cd846c9a Enabled creation of device components 2017-01-31 12:19:41 -05:00
Jeremy Stretch
1fcc2b0029 Namespaced all API URLs 2017-01-31 10:40:53 -05:00
Jeremy Stretch
173a6eee03 Moved rack units and device LLDP neighbors views into model viewsets 2017-01-30 17:24:04 -05:00
Jeremy Stretch
d9e4017677 Moved graph views into model viewsets 2017-01-30 17:00:58 -05:00
Jeremy Stretch
7beac0b105 Converted device component views to a router 2017-01-30 16:15:12 -05:00
Jeremy Stretch
f0fef94a4f Re-implemented interface/connection serializers 2017-01-30 15:35:01 -05:00
Jeremy Stretch
78cd4481e4 Merge branch 'develop' into api2 2017-01-30 13:38:49 -05:00
Jeremy Stretch
0cf029edd4 Added Service serializers 2017-01-27 16:19:38 -05:00
Jeremy Stretch
c0dac1383d Fix retrieval of model under viewsets without a statically defined queryset 2017-01-27 15:12:46 -05:00
Jeremy Stretch
a3d0d4a5bf Enabled pagination 2017-01-27 14:54:12 -05:00
Jeremy Stretch
12d263999b Introduced WritableSerializerMixin 2017-01-27 14:36:13 -05:00
Jeremy Stretch
fa900d5dbb Converted nested serializers to HyperlinkedModelSerializer 2017-01-27 12:22:29 -05:00
Jeremy Stretch
ddc2c8d110 Cleaned up device component nested serializers 2017-01-26 22:37:17 -05:00
Jeremy Stretch
acfba410dd Standardized implementation of nested ViewSets 2017-01-26 17:58:36 -05:00
Jeremy Stretch
b8ca530c55 Added an endpoint for CircuitTerminations 2017-01-26 17:18:41 -05:00
Jeremy Stretch
b31c097531 Removed Swagger 2017-01-26 15:36:19 -05:00
Jeremy Stretch
0f9fe8648e Converted static URL definitions to routers 2017-01-26 15:34:07 -05:00
Jeremy Stretch
791a641eef Created CircuitDetailSerializer 2017-01-26 15:33:41 -05:00
Jeremy Stretch
c5fba24cc5 Merge branch 'develop' into api2 2017-01-26 14:07:23 -05:00
Jeremy Stretch
0b228ed6d3 Merge branch 'develop' into api2 2017-01-25 16:26:45 -05:00
Jeremy Stretch
062a5bfe8d Initial work on API v2.0 2017-01-24 17:12:16 -05:00
495 changed files with 41099 additions and 22239 deletions

View File

@@ -1,28 +1,41 @@
<!--
Please note: GitHub issues are to be used only for feature requests
and bug reports. For installation assistance or general discussion,
please join us on the mailing list:
Before opening a new issue, please search through the existing issues to
see if your topic has already been addressed. Note that you may need to
remove the "is:open" filter from the search bar to include closed issues.
Check the appropriate type for your issue below by placing an x between the
brackets. If none of the below apply, please raise your issue for
discussion on our mailing list:
https://groups.google.com/forum/#!forum/netbox-discuss
Please indicate "bug report" or "feature request" below. Be sure to
search the existing set of issues (both open and closed) to see if
a similar issue has already been raised.
-->
### Issue type:
Please note that issues which do not fall under any of the below categories
will be closed.
--->
### Issue type
[ ] Feature request <!-- Requesting the implementation of a new feature -->
[ ] Bug report <!-- Reporting unexpected or erroneous behavior -->
[ ] Documentation <!-- Proposing a modification to the documentation -->
<!--
If filing a bug, please indicate the version of Python and NetBox
you are running. (This is not necessary for feature requests.)
Please describe the environment in which you are running NetBox. (Be sure
to verify that you are running the latest stable release of NetBox before
submitting a bug report.)
-->
**Python version:**
**NetBox version:**
### Environment
* Python version: <!-- Example: 3.5.4 -->
* NetBox version: <!-- Example: 2.1.3 -->
<!--
If filing a bug, please record the exact steps taken to reproduce
the bug and any errors messages that are generated.
BUG REPORTS must include:
* A list of the steps needed to reproduce the bug
* A description of the expected behavior
* Any relevant error messages (screenshots may also help)
If filing a feature request, please precisely describe the data
model or workflow you would like to see implemented, and provide a
use case.
FEATURE REQUESTS must include:
* A detailed description of the proposed functionality
* A use case for the new feature
* A rough description of any necessary changes to the database schema
* Any relevant third-party libraries which would be needed
-->
### Description

2
.gitignore vendored
View File

@@ -1,6 +1,8 @@
*.pyc
/netbox/netbox/configuration.py
/netbox/netbox/ldap_config.py
/netbox/reports/*
!/netbox/reports/__init__.py
/netbox/static
.idea
/*.sh

View File

@@ -1,11 +1,8 @@
sudo: required
services:
- docker
env:
- DOCKER_TAG=$TRAVIS_TAG
- postgresql
addons:
postgresql: "9.4"
language: python
python:
- "2.7"
@@ -13,9 +10,8 @@ python:
install:
- pip install -r requirements.txt
- pip install pep8
before_script:
- psql --version
- psql -U postgres -c 'SELECT version();'
script:
- ./scripts/cibuild.sh
after_success:
- if [ ! -z "$TRAVIS_TAG" ] && [ "$TRAVIS_PULL_REQUEST" == "false" ]; then
./scripts/docker-build.sh;
fi

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

@@ -1,20 +0,0 @@
FROM python:2.7-wheezy
WORKDIR /opt/netbox
ARG BRANCH=master
ARG URL=https://github.com/digitalocean/netbox.git
RUN git clone --depth 1 $URL -b $BRANCH . && \
apt-get update -qq && apt-get install -y libldap2-dev libsasl2-dev libssl-dev graphviz && \
pip install gunicorn==17.5 && \
pip install django-auth-ldap && \
pip install -r requirements.txt
ADD docker/docker-entrypoint.sh /docker-entrypoint.sh
ADD netbox/netbox/configuration.docker.py /opt/netbox/netbox/netbox/configuration.py
ENTRYPOINT [ "/docker-entrypoint.sh" ]
ADD docker/gunicorn_config.py /opt/netbox/
ADD docker/nginx.conf /etc/netbox-nginx/
VOLUME ["/etc/netbox-nginx/"]

View File

@@ -10,7 +10,9 @@ Questions? Comments? Please subscribe to [the netbox-discuss mailing list](https
### Build Status
| | python 2.7 |
NetBox is built against both Python 2.7 and 3.5. Python 3.5 is recommended.
| | status |
|-------------|------------|
| **master** | [![Build Status](https://travis-ci.org/digitalocean/netbox.svg?branch=master)](https://travis-ci.org/digitalocean/netbox) |
| **develop** | [![Build Status](https://travis-ci.org/digitalocean/netbox.svg?branch=develop)](https://travis-ci.org/digitalocean/netbox) |
@@ -29,5 +31,5 @@ Please see [the documentation](http://netbox.readthedocs.io/en/stable/) for inst
## Alternative Installations
* [Docker container](http://netbox.readthedocs.io/en/stable/installation/docker/)
* [Heroku deployment](https://heroku.com/deploy?template=https://github.com/BILDQUADRAT/netbox/tree/heroku) (via [@mraerino](https://github.com/BILDQUADRAT/netbox/tree/heroku))
* [Docker container](https://github.com/ninech/netbox-docker) (via [@cimnine](https://github.com/cimnine))
* [Vagrant deployment](https://github.com/ryanmerolle/netbox-vagrant) (via [@ryanmerolle](https://github.com/ryanmerolle))

View File

@@ -1,53 +0,0 @@
version: '2'
services:
postgres:
image: postgres:9.6
container_name: postgres
environment:
POSTGRES_USER: netbox
POSTGRES_PASSWORD: J5brHrAXFLQSif0K
POSTGRES_DB: netbox
netbox:
build: .
image: digitalocean/netbox
links:
- postgres
container_name: netbox
depends_on:
- postgres
environment:
SUPERUSER_NAME: admin
SUPERUSER_EMAIL: admin@example.com
SUPERUSER_PASSWORD: admin
ALLOWED_HOSTS: localhost
DB_NAME: netbox
DB_USER: netbox
DB_PASSWORD: J5brHrAXFLQSif0K
DB_HOST: postgres
SECRET_KEY: r8OwDznj!!dci#P9ghmRfdu1Ysxm0AiPeDCQhKE+N_rClfWNj
EMAIL_SERVER: localhost
EMAIL_PORT: 25
EMAIL_USERNAME: foo
EMAIL_PASSWORD: bar
EMAIL_TIMEOUT: 10
EMAIL_FROM: netbox@bar.com
NETBOX_USERNAME: guest
NETBOX_PASSWORD: guest
volumes:
- netbox-static-files:/opt/netbox/netbox/static
nginx:
image: nginx:1.11.1-alpine
links:
- netbox
container_name: nginx
command: nginx -g 'daemon off;' -c /etc/netbox-nginx/nginx.conf
depends_on:
- netbox
ports:
- 80:80
volumes_from:
- netbox
volumes:
netbox-static-files:
driver: local

View File

@@ -1,22 +0,0 @@
#!/bin/bash
set -e
# run db migrations (retry on error)
while ! /opt/netbox/netbox/manage.py migrate 2>&1; do
sleep 5
done
# create superuser silently
if [[ -z ${SUPERUSER_NAME} || -z ${SUPERUSER_EMAIL} || -z ${SUPERUSER_PASSWORD} ]]; then
SUPERUSER_NAME='admin'
SUPERUSER_EMAIL='admin@example.com'
SUPERUSER_PASSWORD='admin'
echo "Using defaults: Username: ${SUPERUSER_NAME}, E-Mail: ${SUPERUSER_EMAIL}, Password: ${SUPERUSER_PASSWORD}"
fi
echo "from django.contrib.auth.models import User; User.objects.create_superuser('${SUPERUSER_NAME}', '${SUPERUSER_EMAIL}', '${SUPERUSER_PASSWORD}')" | python /opt/netbox/netbox/manage.py shell
# copy static files
/opt/netbox/netbox/manage.py collectstatic --no-input
# start unicorn
gunicorn --log-level debug --debug --error-logfile /dev/stderr --log-file /dev/stdout -c /opt/netbox/gunicorn_config.py netbox.wsgi

View File

@@ -1,5 +0,0 @@
command = '/usr/bin/gunicorn'
pythonpath = '/opt/netbox/netbox'
bind = '0.0.0.0:8001'
workers = 3
user = 'root'

View File

@@ -1,35 +0,0 @@
worker_processes 1;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
tcp_nopush on;
keepalive_timeout 65;
gzip on;
server_tokens off;
server {
listen 80;
server_name localhost;
access_log off;
location /static/ {
alias /opt/netbox/netbox/static/;
}
location / {
proxy_pass http://netbox:8001;
proxy_set_header X-Forwarded-Host $server_name;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
add_header P3P 'CP="ALL DSP COR PSAa PSDa OUR NOR ONL UNI COM NAV"';
}
}
}

View File

@@ -1,19 +0,0 @@
# API Integration
NetBox features a read-only REST API which can be used to integrate it with
other applications.
In the future, both read and write actions will be available via the API.
## Clients
The easiest way to start integrating your applications with NetBox is to make
use of an API client. If you build or discover an API client that is not part
of this list, please send a pull request!
- **Go**: [github.com/digitalocean/go-netbox](https://github.com/digitalocean/go-netbox)
## Documentation
If you wish to build a new API client or simply explore the NetBox API,
Swagger documentation can be found at the URL `/api/docs/` on a NetBox server.

View File

@@ -0,0 +1,48 @@
The NetBox API employs token-based authentication. For convenience, cookie authentication can also be used when navigating the browsable API.
# Tokens
A token is a unique identifier that identifies a user to the API. Each user in NetBox may have one or more tokens which he or she can use to authenticate to the API. To create a token, navigate to the API tokens page at `/user/api-tokens/`.
Each token contains a 160-bit key represented as 40 hexadecimal characters. When creating a token, you'll typically leave the key field blank so that a random key will be automatically generated. However, NetBox allows you to specify a key in case you need to restore a previously deleted token to operation.
By default, a token can be used for all operations available via the API. Deselecting the "write enabled" option will restrict API requests made with the token to read operations (e.g. GET) only.
Additionally, a token can be set to expire at a specific time. This can be useful if an external client needs to be granted temporary access to NetBox.
# Authenticating to the API
By default, read operations will be available without authentication. In this case, a token may be included in the request, but is not necessary.
```
$ curl -H "Accept: application/json; indent=4" http://localhost/api/dcim/sites/
{
"count": 10,
"next": null,
"previous": null,
"results": [...]
}
```
However, if the [`LOGIN_REQUIRED`](../configuration/optional-settings/#login_required) configuration setting has been set to `True`, all requests must be authenticated.
```
$ curl -H "Accept: application/json; indent=4" http://localhost/api/dcim/sites/
{
"detail": "Authentication credentials were not provided."
}
```
To authenticate to the API, set the HTTP `Authorization` header to the string `Token ` (note the trailing space) followed by the token key.
```
$ curl -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0" -H "Accept: application/json; indent=4" http://localhost/api/dcim/sites/
{
"count": 10,
"next": null,
"previous": null,
"results": [...]
}
```
Additionally, the browsable interface to the API (which can be seen by navigating to the API root `/api/` in a web browser) will attempt to authenticate requests using the same cookie that the normal NetBox front end uses. Thus, if you have logged into NetBox, you will be logged into the browsable API as well.

146
docs/api/examples.md Normal file
View File

@@ -0,0 +1,146 @@
# API Examples
Supported HTTP methods:
* `GET`: Retrieve an object or list of objects
* `POST`: Create a new object
* `PUT`: Update an existing object, all mandatory fields must be specified
* `PATCH`: Updates an existing object, only specifiying the field to be changed
* `DELETE`: Delete an existing object
To authenticate a request, attach your token in an `Authorization` header:
```
curl -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0"
```
### Retrieving a list of sites
Send a `GET` request to the object list endpoint. The response contains a paginated list of JSON objects.
```
$ curl -H "Accept: application/json; indent=4" http://localhost/api/dcim/sites/
{
"count": 14,
"next": null,
"previous": null,
"results": [
{
"id": 6,
"name": "Corporate HQ",
"slug": "corporate-hq",
"region": null,
"tenant": null,
"facility": "",
"asn": null,
"physical_address": "742 Evergreen Terrace, Springfield, USA",
"shipping_address": "",
"contact_name": "",
"contact_phone": "",
"contact_email": "",
"comments": "",
"custom_fields": {},
"count_prefixes": 108,
"count_vlans": 46,
"count_racks": 8,
"count_devices": 254,
"count_circuits": 6
},
...
]
}
```
### Retrieving a single site by ID
Send a `GET` request to the object detail endpoint. The response contains a single JSON object.
```
$ curl -H "Accept: application/json; indent=4" http://localhost/api/dcim/sites/6/
{
"id": 6,
"name": "Corporate HQ",
"slug": "corporate-hq",
"region": null,
"tenant": null,
"facility": "",
"asn": null,
"physical_address": "742 Evergreen Terrace, Springfield, USA",
"shipping_address": "",
"contact_name": "",
"contact_phone": "",
"contact_email": "",
"comments": "",
"custom_fields": {},
"count_prefixes": 108,
"count_vlans": 46,
"count_racks": 8,
"count_devices": 254,
"count_circuits": 6
}
```
### Creating a new site
Send a `POST` request to the site list endpoint with token authentication and JSON-formatted data. Only mandatory fields are required.
```
$ curl -X POST -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0" -H "Content-Type: application/json" -H "Accept: application/json; indent=4" http://localhost:8000/api/dcim/sites/ --data '{"name": "My New Site", "slug": "my-new-site"}'
{
"id": 16,
"name": "My New Site",
"slug": "my-new-site",
"region": null,
"tenant": null,
"facility": "",
"asn": null,
"physical_address": "",
"shipping_address": "",
"contact_name": "",
"contact_phone": "",
"contact_email": "",
"comments": ""
}
```
### Modify an existing site
Make an authenticated `PUT` request to the site detail endpoint. As with a create (`POST`) request, all mandatory fields must be included.
```
$ curl -X PUT -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0" -H "Content-Type: application/json" -H "Accept: application/json; indent=4" http://localhost:8000/api/dcim/sites/16/ --data '{"name": "Renamed Site", "slug": "renamed-site"}'
```
### Modify an object by changing a field
Make an authenticated `PATCH` request to the device endpoint. With `PATCH`, unlike `POST` and `PUT`, we only specify the field that is being changed. In this example, we add a serial number to a device.
```
$ curl -X PATCH -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0" -H "Content-Type: application/json" -H "Accept: application/json; indent=4" http://localhost:8000/api/dcim/devices/2549/ --data '{"serial": "FTX1123A090"}'
```
### Delete an existing site
Send an authenticated `DELETE` request to the site detail endpoint.
```
$ curl -v X DELETE -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0" -H "Content-Type: application/json" -H "Accept: application/json; indent=4" http://localhost:8000/api/dcim/sites/16/
* Connected to localhost (127.0.0.1) port 8000 (#0)
> DELETE /api/dcim/sites/16/ HTTP/1.1
> User-Agent: curl/7.35.0
> Host: localhost:8000
> Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0
> Content-Type: application/json
> Accept: application/json; indent=4
>
* HTTP 1.0, assume close after body
< HTTP/1.0 204 No Content
< Date: Mon, 20 Mar 2017 16:13:08 GMT
< Server: WSGIServer/0.1 Python/2.7.6
< Vary: Accept, Cookie
< X-Frame-Options: SAMEORIGIN
< Allow: GET, PUT, PATCH, DELETE, OPTIONS
<
* Closing connection 0
```
The response to a successfull `DELETE` request will have code 204 (No Content); the body of the response will be empty.

208
docs/api/overview.md Normal file
View File

@@ -0,0 +1,208 @@
NetBox v2.0 and later includes a full-featured REST API that allows its data model to be read and manipulated externally.
# What is a REST API?
REST stands for [representational state transfer](https://en.wikipedia.org/wiki/Representational_state_transfer). It's a particular type of API which employs HTTP to create, retrieve, update, and delete objects from a database. (This set of operations is commonly referred to as CRUD.) Each type of operation is associated with a particular HTTP verb:
* `GET`: Retrieve an object or list of objects
* `POST`: Create an object
* `PUT` / `PATCH`: Modify an existing object. `PUT` requires all mandatory fields to be specified, while `PATCH` only expects the field that is being modified to be specified.
* `DELETE`: Delete an existing object
The NetBox API represents all objects in [JavaScript Object Notation (JSON)](http://www.json.org/). This makes it very easy to interact with NetBox data on the command line with common tools. For example, we can request an IP address from NetBox and output the JSON using `curl` and `jq`. (Piping the output through `jq` isn't strictly required but makes it much easier to read.)
```
$ curl -s http://localhost:8000/api/ipam/ip-addresses/2954/ | jq '.'
{
"custom_fields": {},
"nat_outside": null,
"nat_inside": null,
"description": "An example IP address",
"id": 2954,
"family": 4,
"address": "5.101.108.132/26",
"vrf": null,
"tenant": null,
"status": {
"label": "Active",
"value": 1
},
"role": null,
"interface": null
}
```
Each attribute of the NetBox object is expressed as a field in the dictionary. Fields may include their own nested objects, as in the case of the `status` field above. Every object includes a primary key named `id` which uniquely identifies it in the database.
# URL Hierarchy
NetBox's entire API is housed under the API root at `https://<hostname>/api/`. The URL structure is divided at the root level by application: circuits, DCIM, extras, IPAM, secrets, and tenancy. Within each application, each model has its own path. For example, the provider and circuit objects are located under the "circuits" application:
* /api/circuits/providers/
* /api/circuits/circuits/
Likewise, the site, rack, and device objects are located under the "DCIM" application:
* /api/dcim/sites/
* /api/dcim/racks/
* /api/dcim/devices/
The full hierarchy of available endpoints can be viewed by navigating to the API root in a web browser.
Each model generally has two views associated with it: a list view and a detail view. The list view is used to request a list of multiple objects or to create a new object. The detail view is used to retrieve, update, or delete an existing object. All objects are referenced by their numeric primary key (`id`).
* /api/dcim/devices/ - List devices or create a new device
* /api/dcim/devices/123/ - Retrieve, update, or delete the device with ID 123
Lists of objects can be filtered using a set of query parameters. For example, to find all interfaces belonging to the device with ID 123:
```
GET /api/dcim/interfaces/?device_id=123
```
# Serialization
The NetBox API employs three types of serializers to represent model data:
* Base serializer
* Nested serializer
* Writable serializer
The base serializer is used to represent the default view of a model. This includes all database table fields which comprise the model, and may include additional metadata. A base serializer includes relationships to parent objects, but **does not** include child objects. For example, the `VLANSerializer` includes a nested representation its parent VLANGroup (if any), but does not include any assigned Prefixes.
```
{
"id": 1048,
"site": {
"id": 7,
"url": "http://localhost:8000/api/dcim/sites/7/",
"name": "Corporate HQ",
"slug": "corporate-hq"
},
"group": {
"id": 4,
"url": "http://localhost:8000/api/ipam/vlan-groups/4/",
"name": "Production",
"slug": "production"
},
"vid": 101,
"name": "Users-Floor1",
"tenant": null,
"status": {
"value": 1,
"label": "Active"
},
"role": {
"id": 9,
"url": "http://localhost:8000/api/ipam/roles/9/",
"name": "User Access",
"slug": "user-access"
},
"description": "",
"display_name": "101 (Users-Floor1)",
"custom_fields": {}
}
```
Related objects (e.g. `ForeignKey` fields) are represented using a nested serializer. A nested serializer provides a minimal representation of an object, including only its URL and enough information to construct its name.
When a base serializer includes one or more nested serializers, the hierarchical structure precludes it from being used for write operations. Thus, a flat representation of an object may be provided using a writable serializer. This serializer includes only raw database values and is not typically used for retrieval, except as part of the response to the creation or updating of an object.
```
{
"id": 1201,
"site": 7,
"group": 4,
"vid": 102,
"name": "Users-Floor2",
"tenant": null,
"status": 1,
"role": 9,
"description": ""
}
```
## Static Choice Fields
Some model fields, such as the `status` field in the above example, utilize static integers corresponding to static choices. The available choices can be retrieved from the read-only `_choices` endpoint within each app. A specific `model:field` tuple may optionally be specified in the URL.
Each choice includes a human-friendly label and its corresponding numeric value. For example, `GET /api/ipam/_choices/prefix:status/` will return:
```
[
{
"value": 0,
"label": "Container"
},
{
"value": 1,
"label": "Active"
},
{
"value": 2,
"label": "Reserved"
},
{
"value": 3,
"label": "Deprecated"
}
]
```
Thus, to set a prefix's status to "Reserved," it would be assigned the integer `2`.
A request for `GET /api/ipam/_choices/` will return choices for _all_ fields belonging to models within the IPAM app.
# Pagination
API responses which contain a list of objects (for example, a request to `/api/dcim/devices/`) will be paginated to avoid unnecessary overhead. The root JSON object will contain the following attributes:
* `count`: The total count of all objects matching the query
* `next`: A hyperlink to the next page of results (if applicable)
* `previous`: A hyperlink to the previous page of results (if applicable)
* `results`: The list of returned objects
Here is an example of a paginated response:
```
HTTP 200 OK
Allow: GET, POST, OPTIONS
Content-Type: application/json
Vary: Accept
{
"count": 2861,
"next": "http://localhost:8000/api/dcim/devices/?limit=50&offset=50",
"previous": null,
"results": [
{
"id": 123,
"name": "DeviceName123",
...
},
...
]
}
```
The default page size derives from the [`PAGINATE_COUNT`](../configuration/optional-settings/#paginate_count) configuration setting, which defaults to 50. However, this can be overridden per request by specifying the desired `offset` and `limit` query parameters. For example, if you wish to retrieve a hundred devices at a time, you would make a request for:
```
http://localhost:8000/api/dcim/devices/?limit=100
```
The response will return devices 1 through 100. The URL provided in the `next` attribute of the response will return devices 101 through 200:
```
{
"count": 2861,
"next": "http://localhost:8000/api/dcim/devices/?limit=100&offset=100",
"previous": null,
"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

@@ -0,0 +1,136 @@
As with most other objects, the NetBox API can be used to create, modify, and delete secrets. However, additional steps are needed to encrypt or decrypt secret data.
# Generating a Session Key
In order to encrypt or decrypt secret data, a session key must be attached to the API request. To generate a session key, send an authenticated request to the `/api/secrets/get-session-key/` endpoint with the private RSA key which matches your [UserKey](../data-model/secrets/#user-keys). The private key must be POSTed with the name `private_key`.
```
$ curl -X POST http://localhost:8000/api/secrets/get-session-key/ \
-H "Authorization: Token c639d619ecbeb1f3055c4141ba6870e20572edd7" \
-H "Accept: application/json; indent=4" \
--data-urlencode "private_key@<filename>"
{
"session_key": "dyEnxlc9lnGzaOAV1dV/xqYPV63njIbdZYOgnAlGPHk="
}
```
!!! note
To read the private key from a file, use the convention above. Alternatively, the private key can be read from an environment variable using `--data-urlencode "private_key=$PRIVATE_KEY"`.
The request uses your private key to unlock your stored copy of the master key and generate a session key which can be attached in the `X-Session-Key` header of future API requests.
# Retrieving Secrets
A session key is not needed to retrieve unencrypted secrets: The secret is returned like any normal object with its `plaintext` field set to null.
```
$ curl http://localhost:8000/api/secrets/secrets/2587/ \
-H "Authorization: Token c639d619ecbeb1f3055c4141ba6870e20572edd7" \
-H "Accept: application/json; indent=4"
{
"id": 2587,
"device": {
"id": 1827,
"url": "http://localhost:8000/api/dcim/devices/1827/",
"name": "MyTestDevice",
"display_name": "MyTestDevice"
},
"role": {
"id": 1,
"url": "http://localhost:8000/api/secrets/secret-roles/1/",
"name": "Login Credentials",
"slug": "login-creds"
},
"name": "admin",
"plaintext": null,
"hash": "pbkdf2_sha256$1000$G6mMFe4FetZQ$f+0itZbAoUqW5pd8+NH8W5rdp/2QNLIBb+LGdt4OSKA=",
"created": "2017-03-21",
"last_updated": "2017-03-21T19:28:44.265582Z"
}
```
To decrypt a secret, we must include our session key in the `X-Session-Key` header:
```
$ curl http://localhost:8000/api/secrets/secrets/2587/ \
-H "Authorization: Token c639d619ecbeb1f3055c4141ba6870e20572edd7" \
-H "Accept: application/json; indent=4" \
-H "X-Session-Key: dyEnxlc9lnGzaOAV1dV/xqYPV63njIbdZYOgnAlGPHk="
{
"id": 2587,
"device": {
"id": 1827,
"url": "http://localhost:8000/api/dcim/devices/1827/",
"name": "MyTestDevice",
"display_name": "MyTestDevice"
},
"role": {
"id": 1,
"url": "http://localhost:8000/api/secrets/secret-roles/1/",
"name": "Login Credentials",
"slug": "login-creds"
},
"name": "admin",
"plaintext": "foobar",
"hash": "pbkdf2_sha256$1000$G6mMFe4FetZQ$f+0itZbAoUqW5pd8+NH8W5rdp/2QNLIBb+LGdt4OSKA=",
"created": "2017-03-21",
"last_updated": "2017-03-21T19:28:44.265582Z"
}
```
Lists of secrets can be decrypted in this manner as well:
```
$ curl http://localhost:8000/api/secrets/secrets/?limit=3 \
-H "Authorization: Token c639d619ecbeb1f3055c4141ba6870e20572edd7" \
-H "Accept: application/json; indent=4" \
-H "X-Session-Key: dyEnxlc9lnGzaOAV1dV/xqYPV63njIbdZYOgnAlGPHk="
{
"count": 3482,
"next": "http://localhost:8000/api/secrets/secrets/?limit=3&offset=3",
"previous": null,
"results": [
{
"id": 2587,
...
"plaintext": "foobar",
...
},
{
"id": 2588,
...
"plaintext": "MyP@ssw0rd!",
...
},
{
"id": 2589,
...
"plaintext": "AnotherSecret!",
...
},
]
}
```
# Creating Secrets
Session keys are also used to decrypt new or modified secrets. This is done by setting the `plaintext` field of the submitted object:
```
$ curl -X POST http://localhost:8000/api/secrets/secrets/ \
-H "Content-Type: application/json" \
-H "Authorization: Token c639d619ecbeb1f3055c4141ba6870e20572edd7" \
-H "Accept: application/json; indent=4" \
-H "X-Session-Key: dyEnxlc9lnGzaOAV1dV/xqYPV63njIbdZYOgnAlGPHk=" \
--data '{"device": 1827, "role": 1, "name": "backup", "plaintext": "Drowssap1"}'
{
"id": 2590,
"device": 1827,
"role": 1,
"name": "backup",
"plaintext": "Drowssap1"
}
```
!!! note
Don't forget to include the `Content-Type: application/json` header when making a POST request.

View File

@@ -17,7 +17,7 @@ ADMINS = [
## BANNER_BOTTOM
Setting these variables will display content in a banner at the top and/or bottom of the page, respectively. To replicate the content of the top banner in the bottom banner, set:
Setting these variables will display content in a banner at the top and/or bottom of the page, respectively. HTML is allowed. To replicate the content of the top banner in the bottom banner, set:
```
BANNER_TOP = 'Your banner text'
@@ -26,6 +26,12 @@ BANNER_BOTTOM = BANNER_TOP
---
## BANNER_LOGIN
The value of this variable will be displayed on the login page above the login form. HTML is allowed.
---
## BASE_PATH
Default: None
@@ -38,6 +44,22 @@ BASE_PATH = 'netbox/'
---
## CORS_ORIGIN_ALLOW_ALL
Default: False
If True, cross-origin resource sharing (CORS) requests will be accepted from all origins. If False, a whitelist will be used (see below).
---
## CORS_ORIGIN_WHITELIST
## CORS_ORIGIN_REGEX_WHITELIST
These settings specify a list of origins that are authorized to make cross-site API requests. Use `CORS_ORIGIN_WHITELIST` to define a list of exact hostnames, or `CORS_ORIGIN_REGEX_WHITELIST` to define a set of regular expressions. (These settings have no effect if `CORS_ORIGIN_ALLOW_ALL` is True.)
---
## DEBUG
Default: False
@@ -67,6 +89,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
@@ -83,11 +133,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`.
---
## MEDIA_ROOT
Default: $BASE_DIR/netbox/media/
The file path to the location where media files (such as image attachments) are stored. By default, this is the `netbox/media/` directory within the base NetBox installation path.
---
## 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.
---
@@ -107,6 +207,14 @@ When determining the primary IP address for a device, IPv6 is preferred over IPv
---
## REPORTS_ROOT
Default: $BASE_DIR/netbox/reports/
The file path to the location where custom reports will be kept. By default, this is the `netbox/reports/` directory within the base NetBox installation path.
---
## TIME_ZONE
Default: UTC

View File

@@ -89,9 +89,12 @@ A device's platform is used to denote the type of software running on it. This c
The assignment of platforms to devices is an optional feature, and may be disregarded if not desired.
### Modules
### Inventory Items
A device can be assigned modules which represent internal components. Currently, these are used merely for inventory tracking, although future development might see their functionality expand. Each module can optionally be assigned to a manufacturer.
Inventory items represent hardware components installed within a device, such as a power supply or CPU. Currently, these are used merely for inventory tracking, although future development might see their functionality expand. Each item can optionally be assigned a manufacturer.
!!! note
Prior to version 2.0, inventory items were called modules.
### Components
@@ -109,6 +112,3 @@ Console ports connect only to console server ports, and power ports connect only
Each interface is a assigned a form factor denoting its physical properties. Two special form factors exist: the "virtual" form factor can be used to designate logical interfaces (such as SVIs), and the "LAG" form factor can be used to desinate link aggregation groups to which physical interfaces can be assigned. Each interface can also be designated as management-only (for out-of-band management) and assigned a short description.
Device bays represent the ability of a device to house child devices. For example, you might install four blade servers into a 2U chassis. The chassis would appear in the rack elevation as a 2U device with four device bays. Each server within it would be defined as a 0U device installed in one of the device bays. Child devices do not appear on rack elevations, but they are included in the "Non-Racked Devices" list within the rack view.
!!! note
Child devices differ from modules in that they are still treated as independent devices, with their own console/power/data components, modules, and IP addresses. Modules, on the other hand, are parts within a device, such as a hard disk or power supply, which do not provide their own management plane.

View File

@@ -119,7 +119,14 @@ 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.
# Image Attachments
Certain objects within NetBox (namely sites, racks, and devices) can have photos or other images attached to them. (Note that _only_ image files are supported.) Each attachment may optionally be assigned a name; if omitted, the attachment will be represented by its file name.
!!! note
If you experience a server error while attempting to upload an image attachment, verify that the system user NetBox runs as has write permission to the media root directory (`netbox/media/`).

View File

@@ -96,4 +96,4 @@ VLAN groups can be employed for administrative organization within NetBox. Each
# Services
A service represents a TCP or UDP service available on a device. Each service must be defined with a name, protocol, and port number; for example, "SSH (TCP/22)." A service may optionally be bound to one or more specific IP addresses belonging to a device. (If no IP addresses are bound, the service is assumed to be reachable via any assigned IP address.)
A service represents a TCP or UDP service available on a device or virtual machine. Each service must be defined with a name, protocol, and port number; for example, "SSH (TCP/22)." A service may optionally be bound to one or more specific IP addresses belonging to its parent. (If no IP addresses are bound, the service is assumed to be reachable via any assigned IP address.)

View File

@@ -0,0 +1,29 @@
NetBox supports the definition of virtual machines arranged in clusters. A cluster can optionally have physical host devices associated with it.
# Clusters
A cluster is a logical grouping of physical resources within which virtual machines run. A cluster must be assigned a type, and may optionally be assigned an organizational group.
Physical devices (from NetBox's DCIM component) may be associated with clusters as hosts. This allows users to track on which host(s) a particular VM may reside. However, NetBox does not support pinning a specific VM within a cluster to a particular host device.
### Cluster Types
A cluster type represents a technology or mechanism by which a cluster is formed. For example, you might create a cluster type named "VMware vSphere" for a locally hosted cluster or "DigitalOcean NYC3" for one hosted by a cloud provider.
### Cluster Groups
Cluster groups may be created for the purpose of organizing clusters.
---
# Virtual Machines
A virtual machine represents a virtual compute instance hosted within a cluster. Each VM must be associated with exactly one cluster.
Like devices, each VM can have interfaces created on it. These behave similarly to device interfaces, and can be assigned IP addresses, however given their virtual nature they cannot be connected to other interfaces. VMs can also be assigned layer four services. Unlike physical devices, VMs cannot be assigned console or power ports, or device bays.
The following resources can be defined for each VM:
* vCPU count
* Memory (MB)
* Disk space (GB)

View File

@@ -0,0 +1,53 @@
# Utility Views
Utility views are reusable views that handle common CRUD tasks, such as listing and updating objects. Some views operate on individual objects, whereas others (referred to as "bulk" views) operate on multiple objects at once.
## Individual Views
### ObjectListView
Generates a paginated table of objects from a given queryset, which may optionally be filtered.
### ObjectEditView
Updates an object identified by a primary key (PK) or slug. If no existing object is specified, a new object will be created.
### ObjectDeleteView
Deletes an object. The user is redirected to a confirmation page before the deletion is executed.
## Bulk Views
### BulkCreateView
Creates multiple objects at once based on a given pattern. Currently used only for IP addresses.
### BulkImportView
Accepts CSV-formatted data and creates a new object for each line. Creation is all-or-none.
### BulkEditView
Applies changes to multiple objects at once in a two-step operation. First, the list of PKs for selected objects is POSTed and an edit form is presented to the user. On submission of that form, the specified changes are made to all selected objects.
### BulkDeleteView
Deletes multiple objects. The user selects the objects to be deleted and confirms the deletion.
## Component Views
### ComponentCreateView
Create one or more component objects beloning to a parent object (e.g. interfaces attached to a device).
### ComponentEditView
A subclass of `ObjectEditView`: Updates an individual component object.
### ComponentDeleteView
A subclass of `ObjectDeleteView`: Deletes an individual component object.
### BulkComponentCreateView
Create a set of components objects for each of a selected set of parent objects. This view can be used e.g. to create multiple interfaces on multiple devices at once.

View File

@@ -6,6 +6,7 @@ NetBox is an open source web application designed to help manage and document co
* **Equipment racks** - Organized by group and site
* **Devices** - Types of devices and where they are installed
* **Connections** - Network, console, and power connections among devices
* **Virtualization** - Virtual machines and clusters
* **Data circuits** - Long-haul communications circuits and providers
* **Secrets** - Encrypted storage of sensitive credentials
@@ -46,7 +47,7 @@ NetBox is built on the [Django](https://djangoproject.com/) Python framework and
| HTTP Service | nginx or Apache |
| WSGI Service | gunicorn or uWSGI |
| Application | Django/Python |
| Database | PostgreSQL |
| Database | PostgreSQL 9.4+ |
# Getting Started

View File

@@ -1,51 +0,0 @@
This guide demonstrates how to build and run NetBox as a Docker container. It assumes that the latest versions of [Docker](https://www.docker.com/) and [docker-compose](https://docs.docker.com/compose/) are already installed in your host.
# Quickstart
To get NetBox up and running:
```no-highlight
# git clone -b master https://github.com/digitalocean/netbox.git
# cd netbox
# docker-compose up -d
```
The application will be available on http://localhost/ after a few minutes.
Default credentials:
* Username: **admin**
* Password: **admin**
# Configuration
You can configure the app at runtime using variables (see `docker-compose.yml`). Possible environment variables include:
* SUPERUSER_NAME
* SUPERUSER_EMAIL
* SUPERUSER_PASSWORD
* ALLOWED_HOSTS
* DB_NAME
* DB_USER
* DB_PASSWORD
* DB_HOST
* DB_PORT
* SECRET_KEY
* EMAIL_SERVER
* EMAIL_PORT
* EMAIL_USERNAME
* EMAIL_PASSWORD
* EMAIL_TIMEOUT
* EMAIL_FROM
* LOGIN_REQUIRED
* MAINTENANCE_MODE
* NETBOX_USERNAME
* NETBOX_PASSWORD
* PAGINATE_COUNT
* TIME_ZONE
* DATE_FORMAT
* SHORT_DATE_FORMAT
* TIME_FORMAT
* SHORT_TIME_FORMAT
* DATETIME_FORMAT
* SHORT_DATETIME_FORMAT

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,11 +72,14 @@ 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"
}
```
# User Groups for Permissions
!!! Info
When using Microsoft Active Directory, Support for nested Groups can be activated by using `GroupOfNamesType()` instead of `NestedGroupOfNamesType()` for `AUTH_LDAP_GROUP_TYPE`.
```python
from django_auth_ldap.config import LDAPSearch, GroupOfNamesType
@@ -99,3 +107,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,36 +1,42 @@
# Installation
**Debian/Ubuntu**
This section of the documentation discusses installing and configuring the NetBox application.
!!! note
Python 3 is strongly encouraged for new installations. Support for Python 2 will be discontinued in the near future. This documentation includes a guide on [migrating from Python 2 to Python 3](migrating-to-python3).
**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
# update-alternatives --install /usr/bin/python python /usr/bin/python3 1
# apt-get install -y python3 python3-dev python3-setuptools build-essential libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev
# easy_install3 pip
```
Python 2:
```no-highlight
# apt-get install -y python2.7 python-dev python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev
# apt-get install -y python2.7 python-dev python-setuptools build-essential libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev
# easy_install pip
```
**CentOS/RHEL**
**CentOS**
Python 3:
```no-highlight
# yum install -y epel-release
# yum install -y gcc python34 python34-devel python34-setuptools libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel
# yum install -y gcc python34 python34-devel python34-setuptools libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel redhat-rpm-config
# easy_install-3.4 pip
# ln -s -f python3.4 /usr/bin/python
```
Python 2:
```no-highlight
# yum install -y epel-release
# yum install -y gcc python2 python-devel python-pip libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel
# yum install -y gcc python2 python-devel python-setuptools libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel redhat-rpm-config
# easy_install pip
```
You may opt to install NetBox either from a numbered release or by cloning the master branch of its repository on GitHub.
@@ -57,13 +63,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 +104,17 @@ Python 2:
# pip install -r requirements.txt
```
!!! note
If you encounter errors while installing the required packages, check that you're running a recent version of pip (v9.0.1 or higher) with the command `pip -V` or `pip3 -V`.
### 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
# pip3 install napalm
```
# Configuration
Move into the NetBox configuration directory and make a copy of `configuration.example.py` named `configuration.py`.
@@ -150,11 +167,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 `python3` executable. Replace this with `python2` or `python` if you're using Python 2.
Before NetBox can run, we need to install the database schema. This is done by running `python3 manage.py migrate` from the `netbox` directory (`/opt/netbox/netbox/` in our example):
```no-highlight
# cd /opt/netbox/netbox/
# ./manage.py migrate
# python3 manage.py migrate
Operations to perform:
Apply all migrations: dcim, sessions, admin, ipam, utilities, auth, circuits, contenttypes, extras, secrets, users
Running migrations:
@@ -172,7 +192,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
# python3 manage.py createsuperuser
Username: admin
Email address: admin@example.com
Password:
@@ -183,7 +203,7 @@ Superuser created successfully.
# Collect Static Files
```no-highlight
# ./manage.py collectstatic --no-input
# python3 manage.py collectstatic --no-input
You have requested to collect static files at the destination
location as specified in your settings:
@@ -204,7 +224,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
# python3 manage.py loaddata initial_data
Installed 43 object(s) from 4 fixture(s)
```
@@ -213,7 +233,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
# python3 manage.py runserver 0.0.0.0:8000 --insecure
Performing system checks...
System check identified no issues (0 silenced).

View File

@@ -1,32 +1,44 @@
NetBox requires a PostgreSQL database to store data. (Please note that MySQL is not supported, as NetBox leverages PostgreSQL's built-in [network address types](https://www.postgresql.org/docs/9.1/static/datatype-net-types.html).)
NetBox requires a PostgreSQL database to store data. This can be hosted locally or on a remote server. (Please note that MySQL is not supported, as NetBox leverages PostgreSQL's built-in [network address types](https://www.postgresql.org/docs/current/static/datatype-net-types.html).)
!!! note
The installation instructions provided here have been tested to work on Ubuntu 16.04 and CentOS 7.4. 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.
!!! warning
NetBox v2.2 and later requires PostgreSQL 9.4 or higher.
# Installation
**Debian/Ubuntu**
**Ubuntu**
If a recent enough version of PostgreSQL is not available through your distribution's package manager, you'll need to install it from an official [PostgreSQL repository](https://wiki.postgresql.org/wiki/Apt).
```no-highlight
# apt-get update
# apt-get install -y postgresql libpq-dev
```
**CentOS/RHEL**
**CentOS**
CentOS 7.4 does not ship with a recent enough version of PostgreSQL, so it will need to be installed from an external repository. The instructions below show the installation of PostgreSQL 9.6.
```no-highlight
# yum install -y postgresql postgresql-server postgresql-devel
# postgresql-setup initdb
# yum install https://download.postgresql.org/pub/repos/yum/9.6/redhat/rhel-7-x86_64/pgdg-centos96-9.6-3.noarch.rpm
# yum install postgresql96 postgresql96-server postgresql96-devel
# /usr/pgsql-9.6/bin/postgresql96-setup initdb
```
CentOS users should modify the PostgreSQL configuration to accept password-based authentication by replacing `ident` with `md5` for all host entries within `/var/lib/pgsql/data/pg_hba.conf`. For example:
CentOS users should modify the PostgreSQL configuration to accept password-based authentication by replacing `ident` with `md5` for all host entries within `/var/lib/pgsql/9.6/data/pg_hba.conf`. For example:
```no-highlight
host all all 127.0.0.1/32 md5
host all all ::1/128 md5
```
Then, start the service:
Then, start the service and enable it to run at boot:
```no-highlight
# systemctl start postgresql
# systemctl start postgresql-9.6
# systemctl enable postgresql-9.6
```
# Database Creation
@@ -38,7 +50,7 @@ At a minimum, we need to create a database for NetBox and assign it a username a
```no-highlight
# sudo -u postgres psql
psql (9.3.13)
psql (9.4.5)
Type "help" for help.
postgres=# CREATE DATABASE netbox;
@@ -50,10 +62,10 @@ GRANT
postgres=# \q
```
You can verify that authentication works issuing the following command and providing the configured password:
You can verify that authentication works issuing the following command and providing the configured password. (Replace `localhost` with your database server if using a remote database.)
```no-highlight
# psql -U netbox -h localhost -W
# psql -U netbox -W -h localhost netbox
```
If successful, you will enter a `postgres` prompt. Type `\q` to exit.
If successful, you will enter a `netbox` prompt. Type `\q` to exit.

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

@@ -1,13 +1,9 @@
# Web Server Installation
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
```
# Web Server Installation
## Option A: nginx
@@ -25,7 +21,7 @@ server {
server_name netbox.example.com;
access_log off;
client_max_body_size 25m;
location /static/ {
alias /opt/netbox/netbox/static/;
@@ -55,7 +51,7 @@ Restart the nginx service to use the new configuration.
# service nginx restart
```
To enable SSL, consider this guide on [securing nginx with Let's Encrypt](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-14-04).
To enable SSL, consider this guide on [securing nginx with Let's Encrypt](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-16-04).
## Option B: Apache
@@ -73,6 +69,9 @@ Once Apache is installed, proceed with the following configuration (Be sure to m
Alias /static /opt/netbox/netbox/static
# Needed to allow token-based API authentication
WSGIPassAuthorization on
<Directory /opt/netbox/netbox/static>
Options Indexes FollowSymLinks MultiViews
AllowOverride None
@@ -97,10 +96,16 @@ Save the contents of the above example in `/etc/apache2/sites-available/netbox.c
# service apache2 restart
```
To enable SSL, consider this guide on [securing Apache with Let's Encrypt](https://www.digitalocean.com/community/tutorials/how-to-secure-apache-with-let-s-encrypt-on-ubuntu-14-04).
To enable SSL, consider this guide on [securing Apache with Let's Encrypt](https://www.digitalocean.com/community/tutorials/how-to-secure-apache-with-let-s-encrypt-on-ubuntu-16-04).
# gunicorn Installation
Install gunicorn using `pip3` (Python 3) or `pip` (Python 2):
```no-highlight
# pip3 install gunicorn
```
Save the following configuration in the root netbox installation path as `gunicorn_config.py` (e.g. `/opt/netbox/gunicorn_config.py` per our example installation). Be sure to verify the location of the gunicorn executable on your server (e.g. `which gunicorn`) and to update the `pythonpath` variable if needed. If using CentOS/RHEL, change the username from `www-data` to `nginx` or `apache`.
```no-highlight
@@ -113,6 +118,12 @@ user = 'www-data'
# supervisord Installation
Install supervisor:
```no-highlight
# apt-get install -y supervisor
```
Save the following as `/etc/supervisor/conf.d/netbox.conf`. Update the `command` and `directory` paths as needed. If using CentOS/RHEL, change the username from `www-data` to `nginx` or `apache`.
```no-highlight

View File

@@ -0,0 +1,129 @@
# NetBox Reports
A NetBox report is a mechanism for validating the integrity of data within NetBox. Running a report allows the user to verify that the objects defined within NetBox meet certain arbitrary conditions. For example, you can write reports to check that:
* All top-of-rack switches have a console connection
* Every router has a loopback interface with an IP address assigned
* Each interface description conforms to a standard format
* Every site has a minimum set of VLANs defined
* All IP addresses have a parent prefix
...and so on. Reports are completely customizable, so there's practically no limit to what you can test for.
## Writing Reports
Reports must be saved as files in the [`REPORTS_ROOT`](../configuration/optional-settings/#reports_root) path (which defaults to `netbox/reports/`). Each file created within this path is considered a separate module. Each module holds one or more reports (Python classes), each of which performs a certain function. The logic of each report is broken into discrete test methods, each of which applies a small portion of the logic comprising the overall test.
!!! warning
The reports path includes a file named `__init__.py`, which registers the path as a Python module. Do not delete this file.
For example, we can create a module named `devices.py` to hold all of our reports which pertain to devices in NetBox. Within that module, we might define several reports. Each report is defined as a Python class inheriting from `extras.reports.Report`.
```
from extras.reports import Report
class DeviceConnectionsReport(Report):
description = "Validate the minimum physical connections for each device"
class DeviceIPsReport(Report):
description = "Check that every device has a primary IP address assigned"
```
Within each report class, we'll create a number of test methods to execute our report's logic. In DeviceConnectionsReport, for instance, we want to ensure that every live device has a console connection, an out-of-band management connection, and two power connections.
```
from dcim.constants import CONNECTION_STATUS_PLANNED, STATUS_ACTIVE
from dcim.models import ConsolePort, Device, PowerPort
from extras.reports import Report
class DeviceConnectionsReport(Report):
description = "Validate the minimum physical connections for each device"
def test_console_connection(self):
# Check that every console port for every active device has a connection defined.
for console_port in ConsolePort.objects.select_related('device').filter(device__status=STATUS_ACTIVE):
if console_port.cs_port is None:
self.log_failure(
console_port.device,
"No console connection defined for {}".format(console_port.name)
)
elif console_port.connection_status == CONNECTION_STATUS_PLANNED:
self.log_warning(
console_port.device,
"Console connection for {} marked as planned".format(console_port.name)
)
else:
self.log_success(console_port.device)
def test_power_connections(self):
# Check that every active device has at least two connected power supplies.
for device in Device.objects.filter(status=STATUS_ACTIVE):
connected_ports = 0
for power_port in PowerPort.objects.filter(device=device):
if power_port.power_outlet is not None:
connected_ports += 1
if power_port.connection_status == CONNECTION_STATUS_PLANNED:
self.log_warning(
device,
"Power connection for {} marked as planned".format(power_port.name)
)
if connected_ports < 2:
self.log_failure(
device,
"{} connected power supplies found (2 needed)".format(connected_ports)
)
else:
self.log_success(device)
```
As you can see, reports are completely customizable. Validation logic can be as simple or as complex as needed.
!!! warning
Reports should never alter data: If you find yourself using the `create()`, `save()`, `update()`, or `delete()` methods on objects within reports, stop and re-evaluate what you're trying to accomplish. Note that there are no safeguards against the accidental alteration or destruction of data.
The following methods are available to log results within a report:
* log(message)
* log_success(object, message=None)
* log_info(object, message)
* log_warning(object, message)
* log_failure(object, message)
The recording of one or more failure messages will automatically flag a report as failed. It is advised to log a success for each object that is evaluated so that the results will reflect how many objects are being reported on. (The inclusion of a log message is optional for successes.) Messages recorded with `log()` will appear in a report's results but are not associated with a particular object or status.
Once you have created a report, it will appear in the reports list. Initially, reports will have no results associated with them. To generate results, run the report.
## Running Reports
### Via the Web UI
Reports can be run via the web UI by navigating to the report and clicking the "run report" button at top right. Note that a user must have permission to create ReportResults in order to run reports. (Permissions can be assigned through the admin UI.)
Once a report has been run, its associated results will be included in the report view.
### Via the API
To run a report via the API, simply issue a POST request to its `run` endpoint. Reports are identified by their module and class name.
```
POST /api/extras/reports/<module>.<name>/run/
```
Our example report above would be called as:
```
POST /api/extras/reports/devices.DeviceConnectionsReport/run/
```
### Via the CLI
Reports can be run on the CLI by invoking the management command:
```
python3 manage.py runreport <module>
```
One or more report modules may be specified.

194
docs/miscellaneous/shell.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,7 +8,7 @@ pages:
- 'Web Server': 'installation/web-server.md'
- 'LDAP (Optional)': 'installation/ldap.md'
- 'Upgrading': 'installation/upgrading.md'
- 'Alternate Install: Docker': 'installation/docker.md'
- 'Migrating to Python3': 'installation/migrating-to-python3.md'
- 'Configuration':
- 'Mandatory Settings': 'configuration/mandatory-settings.md'
- 'Optional Settings': 'configuration/optional-settings.md'
@@ -18,8 +18,18 @@ pages:
- 'IPAM': 'data-model/ipam.md'
- 'Secrets': 'data-model/secrets.md'
- 'Tenancy': 'data-model/tenancy.md'
- 'Virtualization': 'data-model/virtualization.md'
- 'Extras': 'data-model/extras.md'
- 'API Integration': 'api-integration.md'
- 'API':
- 'Overview': 'api/overview.md'
- 'Authentication': 'api/authentication.md'
- 'Working with Secrets': 'api/working-with-secrets.md'
- 'Examples': 'api/examples.md'
- 'Miscellaneous':
- 'Reports': 'miscellaneous/reports.md'
- 'Shell': 'miscellaneous/shell.md'
- 'Development':
- 'Utility Views': 'development/utility-views.md'
markdown_extensions:
- admonition:

View File

@@ -1,29 +0,0 @@
from django.contrib import admin
from .models import Provider, CircuitType, Circuit
@admin.register(Provider)
class ProviderAdmin(admin.ModelAdmin):
prepopulated_fields = {
'slug': ['name'],
}
list_display = ['name', 'slug', 'asn']
@admin.register(CircuitType)
class CircuitTypeAdmin(admin.ModelAdmin):
prepopulated_fields = {
'slug': ['name'],
}
list_display = ['name', 'slug']
@admin.register(Circuit)
class CircuitAdmin(admin.ModelAdmin):
list_display = ['cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate_human']
list_filter = ['provider', 'type', 'tenant']
def get_queryset(self, request):
qs = super(CircuitAdmin, self).get_queryset(request)
return qs.select_related('provider', 'type', 'tenant')

View File

@@ -1,72 +1,120 @@
from __future__ import unicode_literals
from rest_framework import serializers
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer
from extras.api.serializers import CustomFieldSerializer
from tenancy.api.serializers import TenantNestedSerializer
from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer
from extras.api.customfields import CustomFieldModelSerializer
from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ValidatedModelSerializer
#
# Providers
#
class ProviderSerializer(CustomFieldSerializer, serializers.ModelSerializer):
class ProviderSerializer(CustomFieldModelSerializer):
class Meta:
model = Provider
fields = ['id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
'custom_fields']
fields = [
'id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
'custom_fields',
]
class ProviderNestedSerializer(ProviderSerializer):
class NestedProviderSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
class Meta(ProviderSerializer.Meta):
fields = ['id', 'name', 'slug']
class Meta:
model = Provider
fields = ['id', 'url', 'name', 'slug']
class WritableProviderSerializer(CustomFieldModelSerializer):
class Meta:
model = Provider
fields = [
'id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
'custom_fields',
]
#
# Circuit types
#
class CircuitTypeSerializer(serializers.ModelSerializer):
class CircuitTypeSerializer(ValidatedModelSerializer):
class Meta:
model = CircuitType
fields = ['id', 'name', 'slug']
class CircuitTypeNestedSerializer(CircuitTypeSerializer):
class NestedCircuitTypeSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
class Meta(CircuitTypeSerializer.Meta):
pass
class Meta:
model = CircuitType
fields = ['id', 'url', 'name', 'slug']
#
# Circuits
#
class CircuitTerminationSerializer(serializers.ModelSerializer):
site = SiteNestedSerializer()
interface = InterfaceNestedSerializer()
class Meta:
model = CircuitTermination
fields = ['id', 'term_side', 'site', 'interface', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info']
class CircuitSerializer(CustomFieldSerializer, serializers.ModelSerializer):
provider = ProviderNestedSerializer()
type = CircuitTypeNestedSerializer()
tenant = TenantNestedSerializer()
terminations = CircuitTerminationSerializer(many=True)
class CircuitSerializer(CustomFieldModelSerializer):
provider = NestedProviderSerializer()
type = NestedCircuitTypeSerializer()
tenant = NestedTenantSerializer()
class Meta:
model = Circuit
fields = ['id', 'cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
'terminations', 'custom_fields']
fields = [
'id', 'cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
'custom_fields',
]
class CircuitNestedSerializer(CircuitSerializer):
class NestedCircuitSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
class Meta(CircuitSerializer.Meta):
fields = ['id', 'cid']
class Meta:
model = Circuit
fields = ['id', 'url', 'cid']
class WritableCircuitSerializer(CustomFieldModelSerializer):
class Meta:
model = Circuit
fields = [
'id', 'cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
'custom_fields',
]
#
# Circuit Terminations
#
class CircuitTerminationSerializer(serializers.ModelSerializer):
circuit = NestedCircuitSerializer()
site = NestedSiteSerializer()
interface = InterfaceSerializer()
class Meta:
model = CircuitTermination
fields = [
'id', 'circuit', 'term_side', 'site', 'interface', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
]
class WritableCircuitTerminationSerializer(ValidatedModelSerializer):
class Meta:
model = CircuitTermination
fields = [
'id', 'circuit', 'term_side', 'site', 'interface', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
]

View File

@@ -1,25 +1,31 @@
from django.conf.urls import url
from __future__ import unicode_literals
from extras.models import GRAPH_TYPE_PROVIDER
from extras.api.views import GraphListView
from rest_framework import routers
from .views import *
from . import views
urlpatterns = [
class CircuitsRootView(routers.APIRootView):
"""
Circuits API root view
"""
def get_view_name(self):
return 'Circuits'
# Providers
url(r'^providers/$', ProviderListView.as_view(), name='provider_list'),
url(r'^providers/(?P<pk>\d+)/$', ProviderDetailView.as_view(), name='provider_detail'),
url(r'^providers/(?P<pk>\d+)/graphs/$', GraphListView.as_view(), {'type': GRAPH_TYPE_PROVIDER},
name='provider_graphs'),
# Circuit types
url(r'^circuit-types/$', CircuitTypeListView.as_view(), name='circuittype_list'),
url(r'^circuit-types/(?P<pk>\d+)/$', CircuitTypeDetailView.as_view(), name='circuittype_detail'),
router = routers.DefaultRouter()
router.APIRootView = CircuitsRootView
# Circuits
url(r'^circuits/$', CircuitListView.as_view(), name='circuit_list'),
url(r'^circuits/(?P<pk>\d+)/$', CircuitDetailView.as_view(), name='circuit_detail'),
# Field choices
router.register(r'_choices', views.CircuitsFieldChoicesViewSet, base_name='field-choice')
]
# Providers
router.register(r'providers', views.ProviderViewSet)
# Circuits
router.register(r'circuit-types', views.CircuitTypeViewSet)
router.register(r'circuits', views.CircuitViewSet)
router.register(r'circuit-terminations', views.CircuitTerminationViewSet)
app_name = 'circuits-api'
urlpatterns = router.urls

View File

@@ -1,58 +1,78 @@
from rest_framework import generics
from __future__ import unicode_literals
from circuits.models import Provider, CircuitType, Circuit
from circuits.filters import CircuitFilter
from rest_framework.decorators import detail_route
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from extras.api.views import CustomFieldModelAPIView
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
from extras.api.serializers import RenderedGraphSerializer
from extras.api.views import CustomFieldModelViewSet
from utilities.api import FieldChoicesViewSet, WritableSerializerMixin
from . import serializers
class ProviderListView(CustomFieldModelAPIView, generics.ListAPIView):
"""
List all providers
"""
queryset = Provider.objects.prefetch_related('custom_field_values__field')
#
# Field choices
#
class CircuitsFieldChoicesViewSet(FieldChoicesViewSet):
fields = (
(CircuitTermination, ['term_side']),
)
#
# Providers
#
class ProviderViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
queryset = Provider.objects.all()
serializer_class = serializers.ProviderSerializer
write_serializer_class = serializers.WritableProviderSerializer
filter_class = filters.ProviderFilter
@detail_route()
def graphs(self, request, pk=None):
"""
A convenience method for rendering graphs for a particular provider.
"""
provider = get_object_or_404(Provider, pk=pk)
queryset = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER)
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': provider})
return Response(serializer.data)
class ProviderDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
"""
Retrieve a single provider
"""
queryset = Provider.objects.prefetch_related('custom_field_values__field')
serializer_class = serializers.ProviderSerializer
#
# Circuit Types
#
class CircuitTypeListView(generics.ListAPIView):
"""
List all circuit types
"""
class CircuitTypeViewSet(ModelViewSet):
queryset = CircuitType.objects.all()
serializer_class = serializers.CircuitTypeSerializer
filter_class = filters.CircuitTypeFilter
class CircuitTypeDetailView(generics.RetrieveAPIView):
"""
Retrieve a single circuit type
"""
queryset = CircuitType.objects.all()
serializer_class = serializers.CircuitTypeSerializer
#
# Circuits
#
class CircuitListView(CustomFieldModelAPIView, generics.ListAPIView):
"""
List circuits (filterable)
"""
queryset = Circuit.objects.select_related('type', 'tenant', 'provider')\
.prefetch_related('custom_field_values__field')
class CircuitViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
queryset = Circuit.objects.select_related('type', 'tenant', 'provider')
serializer_class = serializers.CircuitSerializer
filter_class = CircuitFilter
write_serializer_class = serializers.WritableCircuitSerializer
filter_class = filters.CircuitFilter
class CircuitDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
"""
Retrieve a single circuit
"""
queryset = Circuit.objects.select_related('type', 'tenant', 'provider')\
.prefetch_related('custom_field_values__field')
serializer_class = serializers.CircuitSerializer
#
# Circuit Terminations
#
class CircuitTerminationViewSet(WritableSerializerMixin, ModelViewSet):
queryset = CircuitTermination.objects.select_related('circuit', 'site', 'interface__device')
serializer_class = serializers.CircuitTerminationSerializer
write_serializer_class = serializers.WritableCircuitTerminationSerializer
filter_class = filters.CircuitTerminationFilter

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
@@ -6,8 +8,7 @@ from dcim.models import Site
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
from .models import Provider, Circuit, CircuitType
from .models import Provider, Circuit, CircuitTermination, CircuitType
class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
@@ -30,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():
@@ -38,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(
@@ -49,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)',
)
@@ -60,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)',
)
@@ -71,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)',
)
@@ -95,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():
@@ -107,3 +114,37 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
Q(description__icontains=value) |
Q(comments__icontains=value)
).distinct()
class CircuitTerminationFilter(django_filters.FilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
circuit_id = django_filters.ModelMultipleChoiceFilter(
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', '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,12 +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, CommentField, CSVDataField, FilterChoiceField, Livesearch, SmallTextarea,
SlugField,
APISelect, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, FilterChoiceField,
SmallTextarea, SlugField,
)
from .models import Circuit, CircuitTermination, CircuitType, Provider
@@ -36,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):
@@ -79,16 +85,30 @@ class CircuitTypeForm(BootstrapMixin, forms.ModelForm):
fields = ['name', 'slug']
class CircuitTypeCSVForm(forms.ModelForm):
slug = SlugField()
class Meta:
model = CircuitType
fields = ['name', 'slug']
help_texts = {
'name': 'Name of circuit type',
}
#
# Circuits
#
class CircuitForm(BootstrapMixin, CustomFieldForm):
class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm):
comments = CommentField()
class Meta:
model = Circuit
fields = ['cid', 'type', 'provider', 'tenant', 'install_date', 'commit_rate', 'description', 'comments']
fields = [
'cid', 'type', 'provider', 'install_date', 'commit_rate', 'description', 'tenant_group', 'tenant',
'comments',
]
help_texts = {
'cid': "Unique circuit ID",
'install_date': "Format: YYYY-MM-DD",
@@ -96,21 +116,36 @@ class CircuitForm(BootstrapMixin, 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):
@@ -146,21 +181,25 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
queryset=Site.objects.annotate(filter_count=Count('circuit_terminations')),
to_field_name='slug'
)
commit_rate = forms.IntegerField(required=False, min_value=0, label='Commit rate (Kbps)')
#
# Circuit terminations
#
class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
site = forms.ModelChoiceField(
queryset=Site.objects.all(),
widget=forms.Select(
attrs={'filter-for': 'rack'}
)
)
rack = forms.ModelChoiceField(
rack = ChainedModelChoiceField(
queryset=Rack.objects.all(),
chains=(
('site', 'site'),
),
required=False,
label='Rack',
widget=APISelect(
@@ -168,8 +207,12 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
attrs={'filter-for': 'device', 'nullable': 'true'}
)
)
device = forms.ModelChoiceField(
device = ChainedModelChoiceField(
queryset=Device.objects.all(),
chains=(
('site', 'site'),
('rack', 'rack'),
),
required=False,
label='Device',
widget=APISelect(
@@ -178,29 +221,27 @@ class CircuitTerminationForm(BootstrapMixin, 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 = forms.ModelChoiceField(
queryset=Interface.objects.all(),
interface = ChainedModelChoiceField(
queryset=Interface.objects.connectable().select_related(
'circuit_termination', 'connected_as_a', 'connected_as_b'
),
chains=(
('device', 'device'),
),
required=False,
label='Interface',
widget=APISelect(
api_url='/api/dcim/devices/{{device}}/interfaces/?type=physical',
api_url='/api/dcim/interfaces/?device_id={{device}}&type=physical',
disabled_indicator='is_connected'
)
)
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",
@@ -212,49 +253,22 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
def __init__(self, *args, **kwargs):
# Initialize helper selectors
instance = kwargs.get('instance')
if instance and instance.interface is not None:
initial = kwargs.get('initial', {}).copy()
initial['rack'] = instance.interface.device.rack
initial['device'] = instance.interface.device
kwargs['initial'] = initial
super(CircuitTerminationForm, self).__init__(*args, **kwargs)
# If an interface has been assigned, initialize rack and device
if self.instance.interface:
self.initial['rack'] = self.instance.interface.device.rack
self.initial['device'] = self.instance.interface.device
# Limit rack choices
if self.is_bound:
self.fields['rack'].queryset = Rack.objects.filter(site__pk=self.data['site'])
elif self.initial.get('site'):
self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site'])
else:
self.fields['rack'].choices = []
# Limit device choices
if self.is_bound and self.data.get('rack'):
self.fields['device'].queryset = Device.objects.filter(rack=self.data['rack'])
elif self.initial.get('rack'):
self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack'])
else:
self.fields['device'].choices = []
# Limit interface choices
if self.is_bound and self.data.get('device'):
interfaces = Interface.objects.filter(device=self.data['device']).exclude(
form_factor__in=VIRTUAL_IFACE_TYPES
).select_related(
'circuit_termination', 'connected_as_a', 'connected_as_b'
# Mark connected interfaces as disabled
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'),
})
)
self.fields['interface'].widget.attrs['initial'] = self.data.get('interface')
elif self.initial.get('device'):
interfaces = Interface.objects.filter(device=self.initial['device']).exclude(
form_factor__in=VIRTUAL_IFACE_TYPES
).select_related(
'circuit_termination', 'connected_as_a', 'connected_as_b'
)
self.fields['interface'].widget.attrs['initial'] = self.initial.get('interface')
else:
interfaces = []
self.fields['interface'].choices = [
(iface.id, {
'label': iface.name,
'disabled': iface.is_connected and iface.id != self.fields['interface'].widget.attrs.get('initial'),
}) for iface in interfaces
]

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,6 +1,8 @@
from __future__ import unicode_literals
from django.contrib.contenttypes.fields import GenericRelation
from django.core.urlresolvers import reverse
from django.db import models
from django.urls import reverse
from django.utils.encoding import python_2_unicode_compatible
from dcim.fields import ASNField
@@ -8,30 +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'),
)
def humanize_speed(speed):
"""
Humanize speeds given in Kbps (e.g. 10000000 becomes '10 Gbps')
"""
if speed >= 1000000000 and speed % 1000000000 == 0:
return '{} Tbps'.format(speed / 1000000000)
elif speed >= 1000000 and speed % 1000000 == 0:
return '{} Gbps'.format(speed / 1000000)
elif speed >= 1000 and speed % 1000 == 0:
return '{} Mbps'.format(speed / 1000)
elif speed >= 1000:
return '{} Mbps'.format(float(speed) / 1000)
else:
return '{} Kbps'.format(speed)
from .constants import *
@python_2_unicode_compatible
@@ -50,6 +29,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 +86,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])
@@ -140,10 +123,6 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
def termination_z(self):
return self._get_termination('Z')
def commit_rate_human(self):
return '' if not self.commit_rate else humanize_speed(self.commit_rate)
commit_rate_human.admin_order_field = 'commit_rate'
@python_2_unicode_compatible
class CircuitTermination(models.Model):
@@ -166,7 +145,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'
@@ -174,11 +153,3 @@ class CircuitTermination(models.Model):
return CircuitTermination.objects.select_related('site').get(circuit=self.circuit, term_side=peer_side)
except CircuitTermination.DoesNotExist:
return None
def port_speed_human(self):
return humanize_speed(self.port_speed)
port_speed_human.admin_order_field = 'port_speed'
def upstream_speed_human(self):
return '' if not self.upstream_speed else humanize_speed(self.upstream_speed)
upstream_speed_human.admin_order_field = 'upstream_speed'

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, ToggleColumn
from .models import Circuit, CircuitType, Provider
@@ -19,12 +20,17 @@ CIRCUITTYPE_ACTIONS = """
class ProviderTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn('circuits:provider', args=[Accessor('slug')], verbose_name='Name')
asn = tables.Column(verbose_name='ASN')
account = tables.Column(verbose_name='Account')
circuit_count = tables.Column(accessor=Accessor('count_circuits'), verbose_name='Circuits')
name = tables.LinkColumn()
class Meta(BaseTable.Meta):
model = Provider
fields = ('pk', 'name', 'asn', 'account',)
class ProviderDetailTable(ProviderTable):
circuit_count = tables.Column(accessor=Accessor('count_circuits'), verbose_name='Circuits')
class Meta(ProviderTable.Meta):
model = Provider
fields = ('pk', 'name', 'asn', 'account', 'circuit_count')
@@ -35,11 +41,11 @@ class ProviderTable(BaseTable):
class CircuitTypeTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Name')
name = tables.LinkColumn()
circuit_count = tables.Column(verbose_name='Circuits')
slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn(template_code=CIRCUITTYPE_ACTIONS, attrs={'td': {'class': 'text-right'}},
verbose_name='')
actions = tables.TemplateColumn(
template_code=CIRCUITTYPE_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name=''
)
class Meta(BaseTable.Meta):
model = CircuitType
@@ -52,15 +58,17 @@ class CircuitTypeTable(BaseTable):
class CircuitTable(BaseTable):
pk = ToggleColumn()
cid = tables.LinkColumn('circuits:circuit', args=[Accessor('pk')], verbose_name='ID')
type = tables.Column(verbose_name='Type')
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')], verbose_name='Provider')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
a_side = tables.LinkColumn('dcim:site', accessor=Accessor('termination_a.site'), orderable=False,
args=[Accessor('termination_a.site.slug')])
z_side = tables.LinkColumn('dcim:site', accessor=Accessor('termination_z.site'), orderable=False,
args=[Accessor('termination_z.site.slug')])
description = tables.Column(verbose_name='Description')
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'), orderable=False,
args=[Accessor('termination_a.site.slug')]
)
z_side = tables.LinkColumn(
'dcim:site', accessor=Accessor('termination_z.site'), orderable=False,
args=[Accessor('termination_z.site.slug')]
)
class Meta(BaseTable.Meta):
model = Circuit

View File

View File

@@ -0,0 +1,331 @@
from __future__ import unicode_literals
from rest_framework import status
from rest_framework.test import APITestCase
from django.contrib.auth.models import User
from django.urls import reverse
from dcim.models import Site
from extras.models import Graph, GRAPH_TYPE_PROVIDER
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider, TERM_SIDE_A, TERM_SIDE_Z
from users.models import Token
from utilities.tests import HttpStatusMixin
class ProviderTest(HttpStatusMixin, APITestCase):
def setUp(self):
user = User.objects.create(username='testuser', is_superuser=True)
token = Token.objects.create(user=user)
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
self.provider1 = Provider.objects.create(name='Test Provider 1', slug='test-provider-1')
self.provider2 = Provider.objects.create(name='Test Provider 2', slug='test-provider-2')
self.provider3 = Provider.objects.create(name='Test Provider 3', slug='test-provider-3')
def test_get_provider(self):
url = reverse('circuits-api:provider-detail', kwargs={'pk': self.provider1.pk})
response = self.client.get(url, **self.header)
self.assertEqual(response.data['name'], self.provider1.name)
def test_get_provider_graphs(self):
self.graph1 = Graph.objects.create(
type=GRAPH_TYPE_PROVIDER, name='Test Graph 1',
source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=1'
)
self.graph2 = Graph.objects.create(
type=GRAPH_TYPE_PROVIDER, name='Test Graph 2',
source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=2'
)
self.graph3 = Graph.objects.create(
type=GRAPH_TYPE_PROVIDER, name='Test Graph 3',
source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=3'
)
url = reverse('circuits-api:provider-graphs', kwargs={'pk': self.provider1.pk})
response = self.client.get(url, **self.header)
self.assertEqual(len(response.data), 3)
self.assertEqual(response.data[0]['embed_url'], 'http://example.com/graphs.py?provider=test-provider-1&foo=1')
def test_list_providers(self):
url = reverse('circuits-api:provider-list')
response = self.client.get(url, **self.header)
self.assertEqual(response.data['count'], 3)
def test_create_provider(self):
data = {
'name': 'Test Provider 4',
'slug': 'test-provider-4',
}
url = reverse('circuits-api:provider-list')
response = self.client.post(url, data, **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(Provider.objects.count(), 4)
provider4 = Provider.objects.get(pk=response.data['id'])
self.assertEqual(provider4.name, data['name'])
self.assertEqual(provider4.slug, data['slug'])
def test_update_provider(self):
data = {
'name': 'Test Provider X',
'slug': 'test-provider-x',
}
url = reverse('circuits-api:provider-detail', kwargs={'pk': self.provider1.pk})
response = self.client.put(url, data, **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(Provider.objects.count(), 3)
provider1 = Provider.objects.get(pk=response.data['id'])
self.assertEqual(provider1.name, data['name'])
self.assertEqual(provider1.slug, data['slug'])
def test_delete_provider(self):
url = reverse('circuits-api:provider-detail', kwargs={'pk': self.provider1.pk})
response = self.client.delete(url, **self.header)
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
self.assertEqual(Provider.objects.count(), 2)
class CircuitTypeTest(HttpStatusMixin, APITestCase):
def setUp(self):
user = User.objects.create(username='testuser', is_superuser=True)
token = Token.objects.create(user=user)
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
self.circuittype1 = CircuitType.objects.create(name='Test Circuit Type 1', slug='test-circuit-type-1')
self.circuittype2 = CircuitType.objects.create(name='Test Circuit Type 2', slug='test-circuit-type-2')
self.circuittype3 = CircuitType.objects.create(name='Test Circuit Type 3', slug='test-circuit-type-3')
def test_get_circuittype(self):
url = reverse('circuits-api:circuittype-detail', kwargs={'pk': self.circuittype1.pk})
response = self.client.get(url, **self.header)
self.assertEqual(response.data['name'], self.circuittype1.name)
def test_list_circuittypes(self):
url = reverse('circuits-api:circuittype-list')
response = self.client.get(url, **self.header)
self.assertEqual(response.data['count'], 3)
def test_create_circuittype(self):
data = {
'name': 'Test Circuit Type 4',
'slug': 'test-circuit-type-4',
}
url = reverse('circuits-api:circuittype-list')
response = self.client.post(url, data, **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(CircuitType.objects.count(), 4)
circuittype4 = CircuitType.objects.get(pk=response.data['id'])
self.assertEqual(circuittype4.name, data['name'])
self.assertEqual(circuittype4.slug, data['slug'])
def test_update_circuittype(self):
data = {
'name': 'Test Circuit Type X',
'slug': 'test-circuit-type-x',
}
url = reverse('circuits-api:circuittype-detail', kwargs={'pk': self.circuittype1.pk})
response = self.client.put(url, data, **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(CircuitType.objects.count(), 3)
circuittype1 = CircuitType.objects.get(pk=response.data['id'])
self.assertEqual(circuittype1.name, data['name'])
self.assertEqual(circuittype1.slug, data['slug'])
def test_delete_circuittype(self):
url = reverse('circuits-api:circuittype-detail', kwargs={'pk': self.circuittype1.pk})
response = self.client.delete(url, **self.header)
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
self.assertEqual(CircuitType.objects.count(), 2)
class CircuitTest(HttpStatusMixin, APITestCase):
def setUp(self):
user = User.objects.create(username='testuser', is_superuser=True)
token = Token.objects.create(user=user)
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
self.provider1 = Provider.objects.create(name='Test Provider 1', slug='test-provider-1')
self.provider2 = Provider.objects.create(name='Test Provider 2', slug='test-provider-2')
self.circuittype1 = CircuitType.objects.create(name='Test Circuit Type 1', slug='test-circuit-type-1')
self.circuittype2 = CircuitType.objects.create(name='Test Circuit Type 2', slug='test-circuit-type-2')
self.circuit1 = Circuit.objects.create(cid='TEST0001', provider=self.provider1, type=self.circuittype1)
self.circuit2 = Circuit.objects.create(cid='TEST0002', provider=self.provider1, type=self.circuittype1)
self.circuit3 = Circuit.objects.create(cid='TEST0003', provider=self.provider1, type=self.circuittype1)
def test_get_circuit(self):
url = reverse('circuits-api:circuit-detail', kwargs={'pk': self.circuit1.pk})
response = self.client.get(url, **self.header)
self.assertEqual(response.data['cid'], self.circuit1.cid)
def test_list_circuits(self):
url = reverse('circuits-api:circuit-list')
response = self.client.get(url, **self.header)
self.assertEqual(response.data['count'], 3)
def test_create_circuit(self):
data = {
'cid': 'TEST0004',
'provider': self.provider1.pk,
'type': self.circuittype1.pk,
}
url = reverse('circuits-api:circuit-list')
response = self.client.post(url, data, **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(Circuit.objects.count(), 4)
circuit4 = Circuit.objects.get(pk=response.data['id'])
self.assertEqual(circuit4.cid, data['cid'])
self.assertEqual(circuit4.provider_id, data['provider'])
self.assertEqual(circuit4.type_id, data['type'])
def test_update_circuit(self):
data = {
'cid': 'TEST000X',
'provider': self.provider2.pk,
'type': self.circuittype2.pk,
}
url = reverse('circuits-api:circuit-detail', kwargs={'pk': self.circuit1.pk})
response = self.client.put(url, data, **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(Circuit.objects.count(), 3)
circuit1 = Circuit.objects.get(pk=response.data['id'])
self.assertEqual(circuit1.cid, data['cid'])
self.assertEqual(circuit1.provider_id, data['provider'])
self.assertEqual(circuit1.type_id, data['type'])
def test_delete_circuit(self):
url = reverse('circuits-api:circuit-detail', kwargs={'pk': self.circuit1.pk})
response = self.client.delete(url, **self.header)
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
self.assertEqual(Circuit.objects.count(), 2)
class CircuitTerminationTest(HttpStatusMixin, APITestCase):
def setUp(self):
user = User.objects.create(username='testuser', is_superuser=True)
token = Token.objects.create(user=user)
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
provider = Provider.objects.create(name='Test Provider', slug='test-provider')
circuittype = CircuitType.objects.create(name='Test Circuit Type', slug='test-circuit-type')
self.circuit1 = Circuit.objects.create(cid='TEST0001', provider=provider, type=circuittype)
self.circuit2 = Circuit.objects.create(cid='TEST0002', provider=provider, type=circuittype)
self.circuit3 = Circuit.objects.create(cid='TEST0003', provider=provider, type=circuittype)
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
self.site2 = Site.objects.create(name='Test Site 2', slug='test-site-2')
self.circuittermination1 = CircuitTermination.objects.create(
circuit=self.circuit1, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000
)
self.circuittermination2 = CircuitTermination.objects.create(
circuit=self.circuit2, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000
)
self.circuittermination3 = CircuitTermination.objects.create(
circuit=self.circuit3, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000
)
def test_get_circuittermination(self):
url = reverse('circuits-api:circuittermination-detail', kwargs={'pk': self.circuittermination1.pk})
response = self.client.get(url, **self.header)
self.assertEqual(response.data['id'], self.circuittermination1.pk)
def test_list_circuitterminations(self):
url = reverse('circuits-api:circuittermination-list')
response = self.client.get(url, **self.header)
self.assertEqual(response.data['count'], 3)
def test_create_circuittermination(self):
data = {
'circuit': self.circuit1.pk,
'term_side': TERM_SIDE_Z,
'site': self.site2.pk,
'port_speed': 1000000,
}
url = reverse('circuits-api:circuittermination-list')
response = self.client.post(url, data, **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(CircuitTermination.objects.count(), 4)
circuittermination4 = CircuitTermination.objects.get(pk=response.data['id'])
self.assertEqual(circuittermination4.circuit_id, data['circuit'])
self.assertEqual(circuittermination4.term_side, data['term_side'])
self.assertEqual(circuittermination4.site_id, data['site'])
self.assertEqual(circuittermination4.port_speed, data['port_speed'])
def test_update_circuittermination(self):
data = {
'circuit': self.circuit1.pk,
'term_side': TERM_SIDE_Z,
'site': self.site2.pk,
'port_speed': 1000000,
}
url = reverse('circuits-api:circuittermination-detail', kwargs={'pk': self.circuittermination1.pk})
response = self.client.put(url, data, **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(CircuitTermination.objects.count(), 3)
circuittermination1 = CircuitTermination.objects.get(pk=response.data['id'])
self.assertEqual(circuittermination1.circuit_id, data['circuit'])
self.assertEqual(circuittermination1.term_side, data['term_side'])
self.assertEqual(circuittermination1.site_id, data['site'])
self.assertEqual(circuittermination1.port_speed, data['port_speed'])
def test_delete_circuittermination(self):
url = reverse('circuits-api:circuittermination-detail', kwargs={'pk': self.circuittermination1.pk})
response = self.client.delete(url, **self.header)
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
self.assertEqual(CircuitTermination.objects.count(), 2)

View File

@@ -1,39 +1,43 @@
from __future__ import unicode_literals
from django.conf.urls import url
from . import views
app_name = 'circuits'
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/import/$', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'),
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,17 +1,19 @@
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
from django.core.urlresolvers import reverse
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
model_form = 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,18 +101,31 @@ 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
model_form = forms.CircuitTypeForm
def get_return_url(self, request, obj):
return reverse('circuits:circuittype_list')
class CircuitTypeEditView(CircuitTypeCreateView):
permission_required = 'circuits.change_circuittype'
class CircuitTypeBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'circuits.add_circuittype'
model_form = forms.CircuitTypeCSVForm
table = tables.CircuitTypeTable
default_return_url = 'circuits:circuittype_list'
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 +141,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
model_form = 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 +184,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,10 +257,10 @@ 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
model_form = forms.CircuitTerminationForm
template_name = 'circuits/circuittermination_edit.html'
def alter_obj(self, obj, request, url_args, url_kwargs):
@@ -240,6 +272,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,212 +0,0 @@
from django.contrib import admin
from django.db.models import Count
from mptt.admin import MPTTModelAdmin
from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Module, Platform,
PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, Region,
Site,
)
@admin.register(Region)
class RegionAdmin(MPTTModelAdmin):
list_display = ['name', 'parent', 'slug']
prepopulated_fields = {
'slug': ['name'],
}
@admin.register(Site)
class SiteAdmin(admin.ModelAdmin):
list_display = ['name', 'slug', 'facility', 'asn']
prepopulated_fields = {
'slug': ['name'],
}
@admin.register(RackGroup)
class RackGroupAdmin(admin.ModelAdmin):
list_display = ['name', 'slug', 'site']
prepopulated_fields = {
'slug': ['name'],
}
@admin.register(RackRole)
class RackRoleAdmin(admin.ModelAdmin):
list_display = ['name', 'slug', 'color']
prepopulated_fields = {
'slug': ['name'],
}
@admin.register(Rack)
class RackAdmin(admin.ModelAdmin):
list_display = ['name', 'facility_id', 'site', 'group', 'tenant', 'role', 'type', 'width', 'u_height']
@admin.register(RackReservation)
class RackRackReservationAdmin(admin.ModelAdmin):
list_display = ['rack', 'units', 'description', 'user', 'created']
#
# Device types
#
@admin.register(Manufacturer)
class ManufacturerAdmin(admin.ModelAdmin):
prepopulated_fields = {
'slug': ['name'],
}
list_display = ['name', 'slug']
class ConsolePortTemplateAdmin(admin.TabularInline):
model = ConsolePortTemplate
class ConsoleServerPortTemplateAdmin(admin.TabularInline):
model = ConsoleServerPortTemplate
class PowerPortTemplateAdmin(admin.TabularInline):
model = PowerPortTemplate
class PowerOutletTemplateAdmin(admin.TabularInline):
model = PowerOutletTemplate
class InterfaceTemplateAdmin(admin.TabularInline):
model = InterfaceTemplate
class DeviceBayTemplateAdmin(admin.TabularInline):
model = DeviceBayTemplate
@admin.register(DeviceType)
class DeviceTypeAdmin(admin.ModelAdmin):
prepopulated_fields = {
'slug': ['model'],
}
inlines = [
ConsolePortTemplateAdmin,
ConsoleServerPortTemplateAdmin,
PowerPortTemplateAdmin,
PowerOutletTemplateAdmin,
InterfaceTemplateAdmin,
DeviceBayTemplateAdmin,
]
list_display = ['model', 'manufacturer', 'slug', 'part_number', 'u_height', 'console_ports', 'console_server_ports',
'power_ports', 'power_outlets', 'interfaces', 'device_bays']
list_filter = ['manufacturer']
def get_queryset(self, request):
return DeviceType.objects.annotate(
console_port_count=Count('console_port_templates', distinct=True),
cs_port_count=Count('cs_port_templates', distinct=True),
power_port_count=Count('power_port_templates', distinct=True),
power_outlet_count=Count('power_outlet_templates', distinct=True),
interface_count=Count('interface_templates', distinct=True),
devicebay_count=Count('device_bay_templates', distinct=True),
)
def console_ports(self, instance):
return instance.console_port_count
def console_server_ports(self, instance):
return instance.cs_port_count
def power_ports(self, instance):
return instance.power_port_count
def power_outlets(self, instance):
return instance.power_outlet_count
def interfaces(self, instance):
return instance.interface_count
def device_bays(self, instance):
return instance.devicebay_count
#
# Devices
#
@admin.register(DeviceRole)
class DeviceRoleAdmin(admin.ModelAdmin):
prepopulated_fields = {
'slug': ['name'],
}
list_display = ['name', 'slug', 'color']
@admin.register(Platform)
class PlatformAdmin(admin.ModelAdmin):
prepopulated_fields = {
'slug': ['name'],
}
list_display = ['name', 'rpc_client']
class ConsolePortAdmin(admin.TabularInline):
model = ConsolePort
readonly_fields = ['cs_port']
class ConsoleServerPortAdmin(admin.TabularInline):
model = ConsoleServerPort
class PowerPortAdmin(admin.TabularInline):
model = PowerPort
readonly_fields = ['power_outlet']
class PowerOutletAdmin(admin.TabularInline):
model = PowerOutlet
class InterfaceAdmin(admin.TabularInline):
model = Interface
class DeviceBayAdmin(admin.TabularInline):
model = DeviceBay
fk_name = 'device'
readonly_fields = ['installed_device']
class ModuleAdmin(admin.TabularInline):
model = Module
readonly_fields = ['parent', 'discovered']
@admin.register(Device)
class DeviceAdmin(admin.ModelAdmin):
inlines = [
ConsolePortAdmin,
ConsoleServerPortAdmin,
PowerPortAdmin,
PowerOutletAdmin,
InterfaceAdmin,
DeviceBayAdmin,
ModuleAdmin,
]
list_display = ['display_name', 'device_type_full_name', 'device_role', 'primary_ip', 'rack', 'position', 'asset_tag',
'serial']
list_filter = ['device_role']
def get_queryset(self, request):
qs = super(DeviceAdmin, self).get_queryset(request)
return qs.select_related('device_type__manufacturer', 'device_role', 'primary_ip4', 'primary_ip6', 'rack')
def device_type_full_name(self, obj):
return obj.device_type.full_name
device_type_full_name.short_description = 'Device type'

View File

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

View File

@@ -1,28 +1,45 @@
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 (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceType,
DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet,
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RACK_FACE_FRONT,
RACK_FACE_REAR, Region, Site, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT,
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,
RACK_WIDTH_CHOICES, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES,
)
from extras.api.serializers import CustomFieldSerializer
from tenancy.api.serializers import TenantNestedSerializer
from extras.api.customfields import CustomFieldModelSerializer
from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
from virtualization.models import Cluster
#
# Regions
#
class RegionNestedSerializer(serializers.ModelSerializer):
class NestedRegionSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
class Meta:
model = Region
fields = ['id', 'name', 'slug']
fields = ['id', 'url', 'name', 'slug']
class RegionSerializer(serializers.ModelSerializer):
parent = NestedRegionSerializer()
class Meta:
model = Region
fields = ['id', 'name', 'slug', 'parent']
class WritableRegionSerializer(ValidatedModelSerializer):
class Meta:
model = Region
@@ -33,21 +50,35 @@ class RegionSerializer(serializers.ModelSerializer):
# Sites
#
class SiteSerializer(CustomFieldSerializer, serializers.ModelSerializer):
region = RegionNestedSerializer()
tenant = TenantNestedSerializer()
class SiteSerializer(CustomFieldModelSerializer):
region = NestedRegionSerializer()
tenant = NestedTenantSerializer()
class Meta:
model = Site
fields = ['id', 'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address',
'contact_name', 'contact_phone', 'contact_email', 'comments', 'custom_fields', 'count_prefixes',
'count_vlans', 'count_racks', 'count_devices', 'count_circuits']
fields = [
'id', 'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address',
'contact_name', 'contact_phone', 'contact_email', 'comments', 'custom_fields', 'count_prefixes',
'count_vlans', 'count_racks', 'count_devices', 'count_circuits',
]
class SiteNestedSerializer(SiteSerializer):
class NestedSiteSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
class Meta(SiteSerializer.Meta):
fields = ['id', 'name', 'slug']
class Meta:
model = Site
fields = ['id', 'url', 'name', 'slug']
class WritableSiteSerializer(CustomFieldModelSerializer):
class Meta:
model = Site
fields = [
'id', 'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address',
'contact_name', 'contact_phone', 'contact_email', 'comments', 'custom_fields',
]
#
@@ -55,85 +86,123 @@ class SiteNestedSerializer(SiteSerializer):
#
class RackGroupSerializer(serializers.ModelSerializer):
site = SiteNestedSerializer()
site = NestedSiteSerializer()
class Meta:
model = RackGroup
fields = ['id', 'name', 'slug', 'site']
class RackGroupNestedSerializer(RackGroupSerializer):
class NestedRackGroupSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackgroup-detail')
class Meta(SiteSerializer.Meta):
fields = ['id', 'name', 'slug']
class Meta:
model = RackGroup
fields = ['id', 'url', 'name', 'slug']
class WritableRackGroupSerializer(ValidatedModelSerializer):
class Meta:
model = RackGroup
fields = ['id', 'name', 'slug', 'site']
#
# Rack roles
#
class RackRoleSerializer(serializers.ModelSerializer):
class RackRoleSerializer(ValidatedModelSerializer):
class Meta:
model = RackRole
fields = ['id', 'name', 'slug', 'color']
class RackRoleNestedSerializer(RackRoleSerializer):
class NestedRackRoleSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
class Meta(RackRoleSerializer.Meta):
fields = ['id', 'name', 'slug']
class Meta:
model = RackRole
fields = ['id', 'url', 'name', 'slug']
#
# Racks
#
class RackReservationNestedSerializer(serializers.ModelSerializer):
class Meta:
model = RackReservation
fields = ['id', 'units', 'created', 'user', 'description']
class RackSerializer(CustomFieldSerializer, serializers.ModelSerializer):
site = SiteNestedSerializer()
group = RackGroupNestedSerializer()
tenant = TenantNestedSerializer()
role = RackRoleNestedSerializer()
class RackSerializer(CustomFieldModelSerializer):
site = NestedSiteSerializer()
group = NestedRackGroupSerializer()
tenant = NestedTenantSerializer()
role = NestedRackRoleSerializer()
type = ChoiceFieldSerializer(choices=RACK_TYPE_CHOICES)
width = ChoiceFieldSerializer(choices=RACK_WIDTH_CHOICES)
class Meta:
model = Rack
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'type', 'width',
'u_height', 'desc_units', 'comments', 'custom_fields']
fields = [
'id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'serial', 'type', 'width',
'u_height', 'desc_units', 'comments', 'custom_fields',
]
class RackNestedSerializer(RackSerializer):
class NestedRackSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
class Meta(RackSerializer.Meta):
fields = ['id', 'name', 'facility_id', 'display_name']
class Meta:
model = Rack
fields = ['id', 'url', 'name', 'display_name']
class RackDetailSerializer(RackSerializer):
front_units = serializers.SerializerMethodField()
rear_units = serializers.SerializerMethodField()
reservations = RackReservationNestedSerializer(many=True)
class WritableRackSerializer(CustomFieldModelSerializer):
class Meta(RackSerializer.Meta):
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'type', 'width',
'u_height', 'desc_units', 'reservations', 'comments', 'custom_fields', 'front_units', 'rear_units']
class Meta:
model = Rack
fields = [
'id', 'name', 'facility_id', 'site', 'group', 'tenant', 'role', 'serial', 'type', 'width', 'u_height',
'desc_units', 'comments', 'custom_fields',
]
# Omit the UniqueTogetherValidator that would be automatically added to validate (site, facility_id). This
# prevents facility_id from being interpreted as a required field.
validators = [
UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('site', 'name'))
]
def get_front_units(self, obj):
units = obj.get_rack_units(face=RACK_FACE_FRONT)
for u in units:
u['device'] = DeviceNestedSerializer(u['device']).data if u['device'] else None
return units
def validate(self, data):
def get_rear_units(self, obj):
units = obj.get_rack_units(face=RACK_FACE_REAR)
for u in units:
u['device'] = DeviceNestedSerializer(u['device']).data if u['device'] else None
return units
# Validate uniqueness of (site, facility_id) since we omitted the automatically-created validator from Meta.
if data.get('facility_id', None):
validator = UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('site', 'facility_id'))
validator.set_context(self)
validator(data)
# Enforce model validation
super(WritableRackSerializer, self).validate(data)
return data
#
# Rack units
#
class NestedDeviceSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
class Meta:
model = Device
fields = ['id', 'url', 'name', 'display_name']
class RackUnitSerializer(serializers.Serializer):
"""
A rack unit is an abstraction formed by the set (rack, position, face); it does not exist as a row in the database.
"""
id = serializers.IntegerField(read_only=True)
name = serializers.CharField(read_only=True)
face = serializers.IntegerField(read_only=True)
device = NestedDeviceSerializer(read_only=True)
#
@@ -141,172 +210,273 @@ class RackDetailSerializer(RackSerializer):
#
class RackReservationSerializer(serializers.ModelSerializer):
rack = RackNestedSerializer()
rack = NestedRackSerializer()
class Meta:
model = RackReservation
fields = ['id', 'rack', 'units', 'created', 'user', 'description']
class WritableRackReservationSerializer(ValidatedModelSerializer):
class Meta:
model = RackReservation
fields = ['id', 'rack', 'units', 'description']
#
# Manufacturers
#
class ManufacturerSerializer(serializers.ModelSerializer):
class ManufacturerSerializer(ValidatedModelSerializer):
class Meta:
model = Manufacturer
fields = ['id', 'name', 'slug']
class ManufacturerNestedSerializer(ManufacturerSerializer):
class NestedManufacturerSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
class Meta(ManufacturerSerializer.Meta):
pass
class Meta:
model = Manufacturer
fields = ['id', 'url', 'name', 'slug']
#
# Device types
#
class DeviceTypeSerializer(CustomFieldSerializer, serializers.ModelSerializer):
manufacturer = ManufacturerNestedSerializer()
subdevice_role = serializers.SerializerMethodField()
class DeviceTypeSerializer(CustomFieldModelSerializer):
manufacturer = NestedManufacturerSerializer()
interface_ordering = ChoiceFieldSerializer(choices=IFACE_ORDERING_CHOICES)
subdevice_role = ChoiceFieldSerializer(choices=SUBDEVICE_ROLE_CHOICES)
instance_count = serializers.IntegerField(source='instances.count', read_only=True)
class Meta:
model = DeviceType
fields = ['id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
'interface_ordering', 'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role',
'comments', 'custom_fields', 'instance_count']
def get_subdevice_role(self, obj):
return {
SUBDEVICE_ROLE_PARENT: 'parent',
SUBDEVICE_ROLE_CHILD: 'child',
None: None,
}[obj.subdevice_role]
fields = [
'id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'interface_ordering',
'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments', 'custom_fields',
'instance_count',
]
class DeviceTypeNestedSerializer(DeviceTypeSerializer):
class NestedDeviceTypeSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
manufacturer = NestedManufacturerSerializer()
class Meta(DeviceTypeSerializer.Meta):
fields = ['id', 'manufacturer', 'model', 'slug']
class Meta:
model = DeviceType
fields = ['id', 'url', 'manufacturer', 'model', 'slug']
class ConsolePortTemplateNestedSerializer(serializers.ModelSerializer):
class WritableDeviceTypeSerializer(CustomFieldModelSerializer):
class Meta:
model = DeviceType
fields = [
'id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'interface_ordering',
'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments', 'custom_fields',
]
#
# Console port templates
#
class ConsolePortTemplateSerializer(serializers.ModelSerializer):
device_type = NestedDeviceTypeSerializer()
class Meta:
model = ConsolePortTemplate
fields = ['id', 'name']
fields = ['id', 'device_type', 'name']
class ConsoleServerPortTemplateNestedSerializer(serializers.ModelSerializer):
class WritableConsolePortTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = ConsolePortTemplate
fields = ['id', 'device_type', 'name']
#
# Console server port templates
#
class ConsoleServerPortTemplateSerializer(serializers.ModelSerializer):
device_type = NestedDeviceTypeSerializer()
class Meta:
model = ConsoleServerPortTemplate
fields = ['id', 'name']
fields = ['id', 'device_type', 'name']
class PowerPortTemplateNestedSerializer(serializers.ModelSerializer):
class WritableConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = ConsoleServerPortTemplate
fields = ['id', 'device_type', 'name']
#
# Power port templates
#
class PowerPortTemplateSerializer(serializers.ModelSerializer):
device_type = NestedDeviceTypeSerializer()
class Meta:
model = PowerPortTemplate
fields = ['id', 'name']
fields = ['id', 'device_type', 'name']
class PowerOutletTemplateNestedSerializer(serializers.ModelSerializer):
class WritablePowerPortTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = PowerPortTemplate
fields = ['id', 'device_type', 'name']
#
# Power outlet templates
#
class PowerOutletTemplateSerializer(serializers.ModelSerializer):
device_type = NestedDeviceTypeSerializer()
class Meta:
model = PowerOutletTemplate
fields = ['id', 'name']
fields = ['id', 'device_type', 'name']
class InterfaceTemplateNestedSerializer(serializers.ModelSerializer):
class WritablePowerOutletTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = PowerOutletTemplate
fields = ['id', 'device_type', 'name']
#
# Interface templates
#
class InterfaceTemplateSerializer(serializers.ModelSerializer):
device_type = NestedDeviceTypeSerializer()
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
class Meta:
model = InterfaceTemplate
fields = ['id', 'name', 'form_factor', 'mgmt_only']
fields = ['id', 'device_type', 'name', 'form_factor', 'mgmt_only']
class DeviceTypeDetailSerializer(DeviceTypeSerializer):
console_port_templates = ConsolePortTemplateNestedSerializer(many=True, read_only=True)
cs_port_templates = ConsoleServerPortTemplateNestedSerializer(many=True, read_only=True)
power_port_templates = PowerPortTemplateNestedSerializer(many=True, read_only=True)
power_outlet_templates = PowerPortTemplateNestedSerializer(many=True, read_only=True)
interface_templates = InterfaceTemplateNestedSerializer(many=True, read_only=True)
class WritableInterfaceTemplateSerializer(ValidatedModelSerializer):
class Meta(DeviceTypeSerializer.Meta):
fields = ['id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
'interface_ordering', 'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role',
'comments', 'custom_fields', 'console_port_templates', 'cs_port_templates', 'power_port_templates',
'power_outlet_templates', 'interface_templates']
class Meta:
model = InterfaceTemplate
fields = ['id', 'device_type', 'name', 'form_factor', 'mgmt_only']
#
# Device bay templates
#
class DeviceBayTemplateSerializer(serializers.ModelSerializer):
device_type = NestedDeviceTypeSerializer()
class Meta:
model = DeviceBayTemplate
fields = ['id', 'device_type', 'name']
class WritableDeviceBayTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = DeviceBayTemplate
fields = ['id', 'device_type', 'name']
#
# Device roles
#
class DeviceRoleSerializer(serializers.ModelSerializer):
class DeviceRoleSerializer(ValidatedModelSerializer):
class Meta:
model = DeviceRole
fields = ['id', 'name', 'slug', 'color']
fields = ['id', 'name', 'slug', 'color', 'vm_role']
class DeviceRoleNestedSerializer(DeviceRoleSerializer):
class NestedDeviceRoleSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
class Meta(DeviceRoleSerializer.Meta):
fields = ['id', 'name', 'slug']
class Meta:
model = DeviceRole
fields = ['id', 'url', 'name', 'slug']
#
# Platforms
#
class PlatformSerializer(serializers.ModelSerializer):
class PlatformSerializer(ValidatedModelSerializer):
class Meta:
model = Platform
fields = ['id', 'name', 'slug', 'rpc_client']
fields = ['id', 'name', 'slug', 'napalm_driver', 'rpc_client']
class PlatformNestedSerializer(PlatformSerializer):
class NestedPlatformSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
class Meta(PlatformSerializer.Meta):
fields = ['id', 'name', 'slug']
class Meta:
model = Platform
fields = ['id', 'url', 'name', 'slug']
#
# Devices
#
# Cannot import ipam.api.IPAddressNestedSerializer due to circular dependency
class DeviceIPAddressNestedSerializer(serializers.ModelSerializer):
# Cannot import ipam.api.NestedIPAddressSerializer due to circular dependency
class DeviceIPAddressSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
class Meta:
model = IPAddress
fields = ['id', 'family', 'address']
fields = ['id', 'url', 'family', 'address']
class DeviceSerializer(CustomFieldSerializer, serializers.ModelSerializer):
device_type = DeviceTypeNestedSerializer()
device_role = DeviceRoleNestedSerializer()
tenant = TenantNestedSerializer()
platform = PlatformNestedSerializer()
site = SiteNestedSerializer()
rack = RackNestedSerializer()
primary_ip = DeviceIPAddressNestedSerializer()
primary_ip4 = DeviceIPAddressNestedSerializer()
primary_ip6 = DeviceIPAddressNestedSerializer()
# Cannot import virtualization.api.NestedClusterSerializer due to circular dependency
class NestedClusterSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
class Meta:
model = Cluster
fields = ['id', 'url', 'name']
class DeviceSerializer(CustomFieldModelSerializer):
device_type = NestedDeviceTypeSerializer()
device_role = NestedDeviceRoleSerializer()
tenant = NestedTenantSerializer()
platform = NestedPlatformSerializer()
site = NestedSiteSerializer()
rack = NestedRackSerializer()
face = ChoiceFieldSerializer(choices=RACK_FACE_CHOICES)
status = ChoiceFieldSerializer(choices=STATUS_CHOICES)
primary_ip = DeviceIPAddressSerializer()
primary_ip4 = DeviceIPAddressSerializer()
primary_ip6 = DeviceIPAddressSerializer()
parent_device = serializers.SerializerMethodField()
cluster = NestedClusterSerializer()
class Meta:
model = Device
fields = [
'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6',
'comments', 'custom_fields',
'cluster', 'comments', 'custom_fields',
]
def get_parent_device(self, obj):
@@ -314,21 +484,34 @@ class DeviceSerializer(CustomFieldSerializer, serializers.ModelSerializer):
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 DeviceNestedSerializer(serializers.ModelSerializer):
class WritableDeviceSerializer(CustomFieldModelSerializer):
class Meta:
model = Device
fields = ['id', 'name', 'display_name']
fields = [
'id', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag', 'site', 'rack',
'position', 'face', 'status', 'primary_ip4', 'primary_ip6', 'cluster', 'comments', 'custom_fields',
]
validators = []
def validate(self, data):
# Validate uniqueness of (rack, position, face) since we omitted the automatically-created validator from Meta.
if data.get('rack') and data.get('position') and data.get('face'):
validator = UniqueTogetherValidator(queryset=Device.objects.all(), fields=('rack', 'position', 'face'))
validator.set_context(self)
validator(data)
# Enforce model validation
super(WritableDeviceSerializer, self).validate(data)
return data
#
@@ -336,16 +519,18 @@ class DeviceNestedSerializer(serializers.ModelSerializer):
#
class ConsoleServerPortSerializer(serializers.ModelSerializer):
device = DeviceNestedSerializer()
device = NestedDeviceSerializer()
class Meta:
model = ConsoleServerPort
fields = ['id', 'device', 'name', 'connected_console']
read_only_fields = ['connected_console']
class ConsoleServerPortNestedSerializer(ConsoleServerPortSerializer):
class WritableConsoleServerPortSerializer(ValidatedModelSerializer):
class Meta(ConsoleServerPortSerializer.Meta):
class Meta:
model = ConsoleServerPort
fields = ['id', 'device', 'name']
@@ -354,18 +539,19 @@ class ConsoleServerPortNestedSerializer(ConsoleServerPortSerializer):
#
class ConsolePortSerializer(serializers.ModelSerializer):
device = DeviceNestedSerializer()
cs_port = ConsoleServerPortNestedSerializer()
device = NestedDeviceSerializer()
cs_port = ConsoleServerPortSerializer()
class Meta:
model = ConsolePort
fields = ['id', 'device', 'name', 'cs_port', 'connection_status']
class ConsolePortNestedSerializer(ConsolePortSerializer):
class WritableConsolePortSerializer(ValidatedModelSerializer):
class Meta(ConsolePortSerializer.Meta):
fields = ['id', 'device', 'name']
class Meta:
model = ConsolePort
fields = ['id', 'device', 'name', 'cs_port', 'connection_status']
#
@@ -373,16 +559,18 @@ class ConsolePortNestedSerializer(ConsolePortSerializer):
#
class PowerOutletSerializer(serializers.ModelSerializer):
device = DeviceNestedSerializer()
device = NestedDeviceSerializer()
class Meta:
model = PowerOutlet
fields = ['id', 'device', 'name', 'connected_port']
read_only_fields = ['connected_port']
class PowerOutletNestedSerializer(PowerOutletSerializer):
class WritablePowerOutletSerializer(ValidatedModelSerializer):
class Meta(PowerOutletSerializer.Meta):
class Meta:
model = PowerOutlet
fields = ['id', 'device', 'name']
@@ -391,58 +579,108 @@ class PowerOutletNestedSerializer(PowerOutletSerializer):
#
class PowerPortSerializer(serializers.ModelSerializer):
device = DeviceNestedSerializer()
power_outlet = PowerOutletNestedSerializer()
device = NestedDeviceSerializer()
power_outlet = PowerOutletSerializer()
class Meta:
model = PowerPort
fields = ['id', 'device', 'name', 'power_outlet', 'connection_status']
class PowerPortNestedSerializer(PowerPortSerializer):
class WritablePowerPortSerializer(ValidatedModelSerializer):
class Meta(PowerPortSerializer.Meta):
fields = ['id', 'device', 'name']
class Meta:
model = PowerPort
fields = ['id', 'device', 'name', 'power_outlet', 'connection_status']
#
# Interfaces
#
class LAGInterfaceNestedSerializer(serializers.ModelSerializer):
form_factor = serializers.ReadOnlyField(source='get_form_factor_display')
class NestedInterfaceSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
class Meta:
model = Interface
fields = ['id', 'name', 'form_factor']
fields = ['id', 'url', 'name']
class InterfaceSerializer(serializers.ModelSerializer):
device = DeviceNestedSerializer()
form_factor = serializers.ReadOnlyField(source='get_form_factor_display')
lag = LAGInterfaceNestedSerializer()
class InterfaceNestedCircuitSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
class Meta:
model = Interface
model = Circuit
fields = ['id', 'url', 'cid']
class InterfaceCircuitTerminationSerializer(serializers.ModelSerializer):
circuit = InterfaceNestedCircuitSerializer()
class Meta:
model = CircuitTermination
fields = [
'id', 'device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description', 'is_connected',
'id', 'circuit', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
]
class InterfaceNestedSerializer(InterfaceSerializer):
form_factor = serializers.ReadOnlyField(source='get_form_factor_display')
class InterfaceSerializer(serializers.ModelSerializer):
device = NestedDeviceSerializer()
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
lag = NestedInterfaceSerializer()
is_connected = serializers.SerializerMethodField(read_only=True)
interface_connection = serializers.SerializerMethodField(read_only=True)
circuit_termination = InterfaceCircuitTerminationSerializer()
class Meta(InterfaceSerializer.Meta):
fields = ['id', 'device', 'name']
class InterfaceDetailSerializer(InterfaceSerializer):
connected_interface = InterfaceSerializer()
class Meta(InterfaceSerializer.Meta):
class Meta:
model = Interface
fields = [
'id', 'device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description', 'is_connected',
'connected_interface',
'id', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
'is_connected', 'interface_connection', 'circuit_termination',
]
def get_is_connected(self, obj):
"""
Return True if the interface has a connected interface or circuit termination.
"""
if obj.connection:
return True
try:
circuit_termination = obj.circuit_termination
return True
except CircuitTermination.DoesNotExist:
pass
return False
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
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', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only',
'description',
]
class WritableInterfaceSerializer(ValidatedModelSerializer):
class Meta:
model = Interface
fields = [
'id', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
]
@@ -451,44 +689,53 @@ class InterfaceDetailSerializer(InterfaceSerializer):
#
class DeviceBaySerializer(serializers.ModelSerializer):
device = DeviceNestedSerializer()
device = NestedDeviceSerializer()
installed_device = NestedDeviceSerializer()
class Meta:
model = DeviceBay
fields = ['id', 'device', 'name']
fields = ['id', 'device', 'name', 'installed_device']
class DeviceBayNestedSerializer(DeviceBaySerializer):
installed_device = DeviceNestedSerializer()
class NestedDeviceBaySerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
class Meta(DeviceBaySerializer.Meta):
fields = ['id', 'name', 'installed_device']
class Meta:
model = DeviceBay
fields = ['id', 'url', 'name']
class DeviceBayDetailSerializer(DeviceBaySerializer):
installed_device = DeviceNestedSerializer()
class WritableDeviceBaySerializer(ValidatedModelSerializer):
class Meta(DeviceBaySerializer.Meta):
class Meta:
model = DeviceBay
fields = ['id', 'device', 'name', 'installed_device']
#
# Modules
# Inventory items
#
class ModuleSerializer(serializers.ModelSerializer):
device = DeviceNestedSerializer()
manufacturer = ManufacturerNestedSerializer()
class InventoryItemSerializer(serializers.ModelSerializer):
device = NestedDeviceSerializer()
manufacturer = NestedManufacturerSerializer()
class Meta:
model = Module
fields = ['id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'discovered']
model = InventoryItem
fields = [
'id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
'description',
]
class ModuleNestedSerializer(ModuleSerializer):
class WritableInventoryItemSerializer(ValidatedModelSerializer):
class Meta(ModuleSerializer.Meta):
fields = ['id', 'device', 'parent', 'name']
class Meta:
model = InventoryItem
fields = [
'id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
'description',
]
#
@@ -496,6 +743,24 @@ class ModuleNestedSerializer(ModuleSerializer):
#
class InterfaceConnectionSerializer(serializers.ModelSerializer):
interface_a = PeerInterfaceSerializer()
interface_b = PeerInterfaceSerializer()
connection_status = ChoiceFieldSerializer(choices=CONNECTION_STATUS_CHOICES)
class Meta:
model = InterfaceConnection
fields = ['id', 'interface_a', 'interface_b', 'connection_status']
class NestedInterfaceConnectionSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfaceconnection-detail')
class Meta:
model = InterfaceConnection
fields = ['id', 'url', 'connection_status']
class WritableInterfaceConnectionSerializer(ValidatedModelSerializer):
class Meta:
model = InterfaceConnection

View File

@@ -1,84 +1,67 @@
from django.conf.urls import url
from __future__ import unicode_literals
from extras.models import GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
from extras.api.views import GraphListView, TopologyMapView
from rest_framework import routers
from .views import *
from . import views
urlpatterns = [
class DCIMRootView(routers.APIRootView):
"""
DCIM API root view
"""
def get_view_name(self):
return 'DCIM'
# Regions
url(r'^regions/$', RegionListView.as_view(), name='region_list'),
url(r'^regions/(?P<pk>\d+)/$', RegionDetailView.as_view(), name='region_detail'),
# Sites
url(r'^sites/$', SiteListView.as_view(), name='site_list'),
url(r'^sites/(?P<pk>\d+)/$', SiteDetailView.as_view(), name='site_detail'),
url(r'^sites/(?P<pk>\d+)/graphs/$', GraphListView.as_view(), {'type': GRAPH_TYPE_SITE}, name='site_graphs'),
url(r'^sites/(?P<site>\d+)/racks/$', RackListView.as_view(), name='site_racks'),
router = routers.DefaultRouter()
router.APIRootView = DCIMRootView
# Rack groups
url(r'^rack-groups/$', RackGroupListView.as_view(), name='rackgroup_list'),
url(r'^rack-groups/(?P<pk>\d+)/$', RackGroupDetailView.as_view(), name='rackgroup_detail'),
# Field choices
router.register(r'_choices', views.DCIMFieldChoicesViewSet, base_name='field-choice')
# Rack roles
url(r'^rack-roles/$', RackRoleListView.as_view(), name='rackrole_list'),
url(r'^rack-roles/(?P<pk>\d+)/$', RackRoleDetailView.as_view(), name='rackrole_detail'),
# Sites
router.register(r'regions', views.RegionViewSet)
router.register(r'sites', views.SiteViewSet)
# Racks
url(r'^racks/$', RackListView.as_view(), name='rack_list'),
url(r'^racks/(?P<pk>\d+)/$', RackDetailView.as_view(), name='rack_detail'),
url(r'^racks/(?P<pk>\d+)/rack-units/$', RackUnitListView.as_view(), name='rack_units'),
# Racks
router.register(r'rack-groups', views.RackGroupViewSet)
router.register(r'rack-roles', views.RackRoleViewSet)
router.register(r'racks', views.RackViewSet)
router.register(r'rack-reservations', views.RackReservationViewSet)
# Rack reservations
url(r'^rack-reservations/$', RackReservationListView.as_view(), name='rackreservation_list'),
url(r'^rack-reservations/(?P<pk>\d+)/$', RackReservationDetailView.as_view(), name='rackreservation_detail'),
# Device types
router.register(r'manufacturers', views.ManufacturerViewSet)
router.register(r'device-types', views.DeviceTypeViewSet)
# Manufacturers
url(r'^manufacturers/$', ManufacturerListView.as_view(), name='manufacturer_list'),
url(r'^manufacturers/(?P<pk>\d+)/$', ManufacturerDetailView.as_view(), name='manufacturer_detail'),
# Device type components
router.register(r'console-port-templates', views.ConsolePortTemplateViewSet)
router.register(r'console-server-port-templates', views.ConsoleServerPortTemplateViewSet)
router.register(r'power-port-templates', views.PowerPortTemplateViewSet)
router.register(r'power-outlet-templates', views.PowerOutletTemplateViewSet)
router.register(r'interface-templates', views.InterfaceTemplateViewSet)
router.register(r'device-bay-templates', views.DeviceBayTemplateViewSet)
# Device types
url(r'^device-types/$', DeviceTypeListView.as_view(), name='devicetype_list'),
url(r'^device-types/(?P<pk>\d+)/$', DeviceTypeDetailView.as_view(), name='devicetype_detail'),
# Devices
router.register(r'device-roles', views.DeviceRoleViewSet)
router.register(r'platforms', views.PlatformViewSet)
router.register(r'devices', views.DeviceViewSet)
# Device roles
url(r'^device-roles/$', DeviceRoleListView.as_view(), name='devicerole_list'),
url(r'^device-roles/(?P<pk>\d+)/$', DeviceRoleDetailView.as_view(), name='devicerole_detail'),
# Device components
router.register(r'console-ports', views.ConsolePortViewSet)
router.register(r'console-server-ports', views.ConsoleServerPortViewSet)
router.register(r'power-ports', views.PowerPortViewSet)
router.register(r'power-outlets', views.PowerOutletViewSet)
router.register(r'interfaces', views.InterfaceViewSet)
router.register(r'device-bays', views.DeviceBayViewSet)
router.register(r'inventory-items', views.InventoryItemViewSet)
# Platforms
url(r'^platforms/$', PlatformListView.as_view(), name='platform_list'),
url(r'^platforms/(?P<pk>\d+)/$', PlatformDetailView.as_view(), name='platform_detail'),
# Connections
router.register(r'console-connections', views.ConsoleConnectionViewSet, base_name='consoleconnections')
router.register(r'power-connections', views.PowerConnectionViewSet, base_name='powerconnections')
router.register(r'interface-connections', views.InterfaceConnectionViewSet)
# Devices
url(r'^devices/$', DeviceListView.as_view(), name='device_list'),
url(r'^devices/(?P<pk>\d+)/$', DeviceDetailView.as_view(), name='device_detail'),
url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', LLDPNeighborsView.as_view(), name='device_lldp-neighbors'),
url(r'^devices/(?P<pk>\d+)/console-ports/$', ConsolePortListView.as_view(), name='device_consoleports'),
url(r'^devices/(?P<pk>\d+)/console-server-ports/$', ConsoleServerPortListView.as_view(),
name='device_consoleserverports'),
url(r'^devices/(?P<pk>\d+)/power-ports/$', PowerPortListView.as_view(), name='device_powerports'),
url(r'^devices/(?P<pk>\d+)/power-outlets/$', PowerOutletListView.as_view(), name='device_poweroutlets'),
url(r'^devices/(?P<pk>\d+)/interfaces/$', InterfaceListView.as_view(), name='device_interfaces'),
url(r'^devices/(?P<pk>\d+)/device-bays/$', DeviceBayListView.as_view(), name='device_devicebays'),
url(r'^devices/(?P<pk>\d+)/modules/$', ModuleListView.as_view(), name='device_modules'),
# Miscellaneous
router.register(r'connected-device', views.ConnectedDeviceViewSet, base_name='connected-device')
# Console ports
url(r'^console-ports/(?P<pk>\d+)/$', ConsolePortView.as_view(), name='consoleport'),
# Power ports
url(r'^power-ports/(?P<pk>\d+)/$', PowerPortView.as_view(), name='powerport'),
# Interfaces
url(r'^interfaces/(?P<pk>\d+)/$', InterfaceDetailView.as_view(), name='interface_detail'),
url(r'^interfaces/(?P<pk>\d+)/graphs/$', GraphListView.as_view(), {'type': GRAPH_TYPE_INTERFACE},
name='interface_graphs'),
url(r'^interface-connections/$', InterfaceConnectionListView.as_view(), name='interfaceconnection_list'),
url(r'^interface-connections/(?P<pk>\d+)/$', InterfaceConnectionView.as_view(), name='interfaceconnection_detail'),
# Miscellaneous
url(r'^related-connections/$', RelatedConnectionsView.as_view(), name='related_connections'),
url(r'^topology-maps/(?P<slug>[\w-]+)/$', TopologyMapView.as_view(), name='topology_map'),
]
app_name = 'dcim-api'
urlpatterns = router.urls

View File

@@ -1,142 +1,114 @@
from rest_framework import generics
from rest_framework.permissions import DjangoModelPermissionsOrAnonReadOnly
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.response import Response
from rest_framework.settings import api_settings
from rest_framework.views import APIView
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ViewSet
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.http import Http404
from django.http import HttpResponseBadRequest, HttpResponseForbidden
from django.shortcuts import get_object_or_404
from dcim.models import (
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, DeviceType, Interface, InterfaceConnection,
Manufacturer, Module, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackReservation, RackRole, Region, Site,
VIRTUAL_IFACE_TYPES,
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
RackReservation, RackRole, Region, Site,
)
from dcim import filters
from extras.api.views import CustomFieldModelAPIView
from extras.api.renderers import BINDZoneRenderer, FlatJSONRenderer
from utilities.api import ServiceUnavailable
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 IsAuthenticatedOrLoginNotRequired, FieldChoicesViewSet, ServiceUnavailable, WritableSerializerMixin
from .exceptions import MissingFilterException
from . import serializers
#
# Field choices
#
class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
fields = (
(Device, ['face', 'status']),
(ConsolePort, ['connection_status']),
(Interface, ['form_factor']),
(InterfaceConnection, ['connection_status']),
(InterfaceTemplate, ['form_factor']),
(PowerPort, ['connection_status']),
(Rack, ['type', 'width']),
)
#
# Regions
#
class RegionListView(generics.ListAPIView):
"""
List all regions
"""
queryset = Region.objects.all()
serializer_class = serializers.RegionSerializer
class RegionDetailView(generics.RetrieveAPIView):
"""
Retrieve a single region
"""
class RegionViewSet(WritableSerializerMixin, ModelViewSet):
queryset = Region.objects.all()
serializer_class = serializers.RegionSerializer
write_serializer_class = serializers.WritableRegionSerializer
filter_class = filters.RegionFilter
#
# Sites
#
class SiteListView(CustomFieldModelAPIView, generics.ListAPIView):
"""
List all sites
"""
queryset = Site.objects.select_related('tenant').prefetch_related('custom_field_values__field')
class SiteViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
queryset = Site.objects.select_related('region', 'tenant')
serializer_class = serializers.SiteSerializer
write_serializer_class = serializers.WritableSiteSerializer
filter_class = filters.SiteFilter
class SiteDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
"""
Retrieve a single site
"""
queryset = Site.objects.select_related('tenant').prefetch_related('custom_field_values__field')
serializer_class = serializers.SiteSerializer
@detail_route()
def graphs(self, request, pk=None):
"""
A convenience method for rendering graphs for a particular site.
"""
site = get_object_or_404(Site, pk=pk)
queryset = Graph.objects.filter(type=GRAPH_TYPE_SITE)
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': site})
return Response(serializer.data)
#
# Rack groups
#
class RackGroupListView(generics.ListAPIView):
"""
List all rack groups
"""
class RackGroupViewSet(WritableSerializerMixin, ModelViewSet):
queryset = RackGroup.objects.select_related('site')
serializer_class = serializers.RackGroupSerializer
write_serializer_class = serializers.WritableRackGroupSerializer
filter_class = filters.RackGroupFilter
class RackGroupDetailView(generics.RetrieveAPIView):
"""
Retrieve a single rack group
"""
queryset = RackGroup.objects.select_related('site')
serializer_class = serializers.RackGroupSerializer
#
# Rack roles
#
class RackRoleListView(generics.ListAPIView):
"""
List all rack roles
"""
queryset = RackRole.objects.all()
serializer_class = serializers.RackRoleSerializer
class RackRoleDetailView(generics.RetrieveAPIView):
"""
Retrieve a single rack role
"""
class RackRoleViewSet(ModelViewSet):
queryset = RackRole.objects.all()
serializer_class = serializers.RackRoleSerializer
filter_class = filters.RackRoleFilter
#
# Racks
#
class RackListView(CustomFieldModelAPIView, generics.ListAPIView):
"""
List racks (filterable)
"""
queryset = Rack.objects.select_related('site', 'group__site', 'tenant')\
.prefetch_related('custom_field_values__field')
class RackViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
queryset = Rack.objects.select_related('site', 'group__site', 'tenant')
serializer_class = serializers.RackSerializer
write_serializer_class = serializers.WritableRackSerializer
filter_class = filters.RackFilter
class RackDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
"""
Retrieve a single rack
"""
queryset = Rack.objects.select_related('site', 'group__site', 'tenant')\
.prefetch_related('custom_field_values__field')
serializer_class = serializers.RackDetailSerializer
#
# Rack units
#
class RackUnitListView(APIView):
"""
List rack units (by rack)
"""
def get(self, request, pk):
@detail_route()
def units(self, request, pk=None):
"""
List rack units (by rack)
"""
rack = get_object_or_404(Rack, pk=pk)
face = request.GET.get('face', 0)
exclude_pk = request.GET.get('exclude', None)
@@ -147,398 +119,306 @@ class RackUnitListView(APIView):
exclude_pk = None
elevation = rack.get_rack_units(face, exclude_pk)
# Serialize Devices within the rack elevation
for u in elevation:
if u['device']:
u['device'] = serializers.DeviceNestedSerializer(instance=u['device']).data
return Response(elevation)
page = self.paginate_queryset(elevation)
if page is not None:
rack_units = serializers.RackUnitSerializer(page, many=True, context={'request': request})
return self.get_paginated_response(rack_units.data)
#
# Rack reservations
#
class RackReservationListView(generics.ListAPIView):
"""
List all rack reservation
"""
queryset = RackReservation.objects.all()
class RackReservationViewSet(WritableSerializerMixin, ModelViewSet):
queryset = RackReservation.objects.select_related('rack')
serializer_class = serializers.RackReservationSerializer
write_serializer_class = serializers.WritableRackReservationSerializer
filter_class = filters.RackReservationFilter
class RackReservationDetailView(generics.RetrieveAPIView):
"""
Retrieve a single rack reservation
"""
queryset = RackReservation.objects.all()
serializer_class = serializers.RackReservationSerializer
# Assign user from request
def perform_create(self, serializer):
serializer.save(user=self.request.user)
#
# Manufacturers
#
class ManufacturerListView(generics.ListAPIView):
"""
List all hardware manufacturers
"""
queryset = Manufacturer.objects.all()
serializer_class = serializers.ManufacturerSerializer
class ManufacturerDetailView(generics.RetrieveAPIView):
"""
Retrieve a single hardware manufacturers
"""
class ManufacturerViewSet(ModelViewSet):
queryset = Manufacturer.objects.all()
serializer_class = serializers.ManufacturerSerializer
filter_class = filters.ManufacturerFilter
#
# Device Types
# Device types
#
class DeviceTypeListView(CustomFieldModelAPIView, generics.ListAPIView):
"""
List device types (filterable)
"""
queryset = DeviceType.objects.select_related('manufacturer').prefetch_related('custom_field_values__field')
class DeviceTypeViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
queryset = DeviceType.objects.select_related('manufacturer')
serializer_class = serializers.DeviceTypeSerializer
write_serializer_class = serializers.WritableDeviceTypeSerializer
filter_class = filters.DeviceTypeFilter
class DeviceTypeDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
"""
Retrieve a single device type
"""
queryset = DeviceType.objects.select_related('manufacturer').prefetch_related('custom_field_values__field')
serializer_class = serializers.DeviceTypeDetailSerializer
#
# Device type components
#
class ConsolePortTemplateViewSet(WritableSerializerMixin, ModelViewSet):
queryset = ConsolePortTemplate.objects.select_related('device_type__manufacturer')
serializer_class = serializers.ConsolePortTemplateSerializer
write_serializer_class = serializers.WritableConsolePortTemplateSerializer
filter_class = filters.ConsolePortTemplateFilter
class ConsoleServerPortTemplateViewSet(WritableSerializerMixin, ModelViewSet):
queryset = ConsoleServerPortTemplate.objects.select_related('device_type__manufacturer')
serializer_class = serializers.ConsoleServerPortTemplateSerializer
write_serializer_class = serializers.WritableConsoleServerPortTemplateSerializer
filter_class = filters.ConsoleServerPortTemplateFilter
class PowerPortTemplateViewSet(WritableSerializerMixin, ModelViewSet):
queryset = PowerPortTemplate.objects.select_related('device_type__manufacturer')
serializer_class = serializers.PowerPortTemplateSerializer
write_serializer_class = serializers.WritablePowerPortTemplateSerializer
filter_class = filters.PowerPortTemplateFilter
class PowerOutletTemplateViewSet(WritableSerializerMixin, ModelViewSet):
queryset = PowerOutletTemplate.objects.select_related('device_type__manufacturer')
serializer_class = serializers.PowerOutletTemplateSerializer
write_serializer_class = serializers.WritablePowerOutletTemplateSerializer
filter_class = filters.PowerOutletTemplateFilter
class InterfaceTemplateViewSet(WritableSerializerMixin, ModelViewSet):
queryset = InterfaceTemplate.objects.select_related('device_type__manufacturer')
serializer_class = serializers.InterfaceTemplateSerializer
write_serializer_class = serializers.WritableInterfaceTemplateSerializer
filter_class = filters.InterfaceTemplateFilter
class DeviceBayTemplateViewSet(WritableSerializerMixin, ModelViewSet):
queryset = DeviceBayTemplate.objects.select_related('device_type__manufacturer')
serializer_class = serializers.DeviceBayTemplateSerializer
write_serializer_class = serializers.WritableDeviceBayTemplateSerializer
filter_class = filters.DeviceBayTemplateFilter
#
# Device roles
#
class DeviceRoleListView(generics.ListAPIView):
"""
List all device roles
"""
queryset = DeviceRole.objects.all()
serializer_class = serializers.DeviceRoleSerializer
class DeviceRoleDetailView(generics.RetrieveAPIView):
"""
Retrieve a single device role
"""
class DeviceRoleViewSet(ModelViewSet):
queryset = DeviceRole.objects.all()
serializer_class = serializers.DeviceRoleSerializer
filter_class = filters.DeviceRoleFilter
#
# Platforms
#
class PlatformListView(generics.ListAPIView):
"""
List all platforms
"""
queryset = Platform.objects.all()
serializer_class = serializers.PlatformSerializer
class PlatformDetailView(generics.RetrieveAPIView):
"""
Retrieve a single platform
"""
class PlatformViewSet(ModelViewSet):
queryset = Platform.objects.all()
serializer_class = serializers.PlatformSerializer
filter_class = filters.PlatformFilter
#
# Devices
#
class DeviceListView(CustomFieldModelAPIView, generics.ListAPIView):
"""
List devices (filterable)
"""
class DeviceViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
queryset = Device.objects.select_related(
'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'rack', 'parent_bay'
'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'rack', 'parent_bay',
).prefetch_related(
'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'custom_field_values__field'
'primary_ip4__nat_outside', 'primary_ip6__nat_outside',
)
serializer_class = serializers.DeviceSerializer
write_serializer_class = serializers.WritableDeviceSerializer
filter_class = filters.DeviceFilter
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [BINDZoneRenderer, FlatJSONRenderer]
class DeviceDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
"""
Retrieve a single device
"""
queryset = Device.objects.select_related(
'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'rack', 'parent_bay'
).prefetch_related('custom_field_values__field')
serializer_class = serializers.DeviceSerializer
#
# Console ports
#
class ConsolePortListView(generics.ListAPIView):
"""
List console ports (by device)
"""
serializer_class = serializers.ConsolePortSerializer
def get_queryset(self):
device = get_object_or_404(Device, pk=self.kwargs['pk'])
return ConsolePort.objects.filter(device=device).select_related('cs_port')
class ConsolePortView(generics.RetrieveUpdateDestroyAPIView):
permission_classes = [DjangoModelPermissionsOrAnonReadOnly]
serializer_class = serializers.ConsolePortSerializer
queryset = ConsolePort.objects.all()
#
# Console server ports
#
class ConsoleServerPortListView(generics.ListAPIView):
"""
List console server ports (by device)
"""
serializer_class = serializers.ConsoleServerPortSerializer
def get_queryset(self):
device = get_object_or_404(Device, pk=self.kwargs['pk'])
return ConsoleServerPort.objects.filter(device=device).select_related('connected_console')
#
# Power ports
#
class PowerPortListView(generics.ListAPIView):
"""
List power ports (by device)
"""
serializer_class = serializers.PowerPortSerializer
def get_queryset(self):
device = get_object_or_404(Device, pk=self.kwargs['pk'])
return PowerPort.objects.filter(device=device).select_related('power_outlet')
class PowerPortView(generics.RetrieveUpdateDestroyAPIView):
permission_classes = [DjangoModelPermissionsOrAnonReadOnly]
serializer_class = serializers.PowerPortSerializer
queryset = PowerPort.objects.all()
#
# Power outlets
#
class PowerOutletListView(generics.ListAPIView):
"""
List power outlets (by device)
"""
serializer_class = serializers.PowerOutletSerializer
def get_queryset(self):
device = get_object_or_404(Device, pk=self.kwargs['pk'])
return PowerOutlet.objects.filter(device=device).select_related('connected_port')
#
# Interfaces
#
class InterfaceListView(generics.ListAPIView):
"""
List interfaces (by device)
"""
serializer_class = serializers.InterfaceSerializer
filter_class = filters.InterfaceFilter
def get_queryset(self):
device = get_object_or_404(Device, pk=self.kwargs['pk'])
queryset = Interface.objects.order_naturally(device.device_type.interface_ordering).filter(device=device)\
.select_related('connected_as_a', 'connected_as_b', 'circuit_termination')
# Filter by type (physical or virtual)
iface_type = self.request.query_params.get('type')
if iface_type == 'physical':
queryset = queryset.exclude(form_factor__in=VIRTUAL_IFACE_TYPES)
elif iface_type == 'virtual':
queryset = queryset.filter(form_factor__in=VIRTUAL_IFACE_TYPES)
elif iface_type is not None:
queryset = queryset.empty()
return queryset
class InterfaceDetailView(generics.RetrieveAPIView):
"""
Retrieve a single interface
"""
queryset = Interface.objects.select_related('device')
serializer_class = serializers.InterfaceDetailSerializer
class InterfaceConnectionView(generics.RetrieveUpdateDestroyAPIView):
permission_classes = [DjangoModelPermissionsOrAnonReadOnly]
serializer_class = serializers.InterfaceConnectionSerializer
queryset = InterfaceConnection.objects.all()
class InterfaceConnectionListView(generics.ListAPIView):
"""
Retrieve a list of all interface connections
"""
serializer_class = serializers.InterfaceConnectionSerializer
queryset = InterfaceConnection.objects.all()
#
# Device bays
#
class DeviceBayListView(generics.ListAPIView):
"""
List device bays (by device)
"""
serializer_class = serializers.DeviceBayNestedSerializer
def get_queryset(self):
device = get_object_or_404(Device, pk=self.kwargs['pk'])
return DeviceBay.objects.filter(device=device).select_related('installed_device')
#
# Modules
#
class ModuleListView(generics.ListAPIView):
"""
List device modules (by device)
"""
serializer_class = serializers.ModuleSerializer
def get_queryset(self):
device = get_object_or_404(Device, pk=self.kwargs['pk'])
return Module.objects.filter(device=device).select_related('device', 'manufacturer')
#
# Live queries
#
class LLDPNeighborsView(APIView):
"""
Retrieve live LLDP neighbors of a device
"""
def get(self, request, pk):
@detail_route(url_path='napalm')
def napalm(self, request, pk):
"""
Execute a NAPALM method on a Device
"""
device = get_object_or_404(Device, pk=pk)
if not device.primary_ip:
raise ServiceUnavailable(detail="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(detail="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(detail="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)
#
# Device components
#
class ConsolePortViewSet(WritableSerializerMixin, ModelViewSet):
queryset = ConsolePort.objects.select_related('device', 'cs_port__device')
serializer_class = serializers.ConsolePortSerializer
write_serializer_class = serializers.WritableConsolePortSerializer
filter_class = filters.ConsolePortFilter
class ConsoleServerPortViewSet(WritableSerializerMixin, ModelViewSet):
queryset = ConsoleServerPort.objects.select_related('device', 'connected_console__device')
serializer_class = serializers.ConsoleServerPortSerializer
write_serializer_class = serializers.WritableConsoleServerPortSerializer
filter_class = filters.ConsoleServerPortFilter
class PowerPortViewSet(WritableSerializerMixin, ModelViewSet):
queryset = PowerPort.objects.select_related('device', 'power_outlet__device')
serializer_class = serializers.PowerPortSerializer
write_serializer_class = serializers.WritablePowerPortSerializer
filter_class = filters.PowerPortFilter
class PowerOutletViewSet(WritableSerializerMixin, ModelViewSet):
queryset = PowerOutlet.objects.select_related('device', 'connected_port__device')
serializer_class = serializers.PowerOutletSerializer
write_serializer_class = serializers.WritablePowerOutletSerializer
filter_class = filters.PowerOutletFilter
class InterfaceViewSet(WritableSerializerMixin, ModelViewSet):
queryset = Interface.objects.select_related('device')
serializer_class = serializers.InterfaceSerializer
write_serializer_class = serializers.WritableInterfaceSerializer
filter_class = filters.InterfaceFilter
@detail_route()
def graphs(self, request, pk=None):
"""
A convenience method for rendering graphs for a particular interface.
"""
interface = get_object_or_404(Interface, pk=pk)
queryset = Graph.objects.filter(type=GRAPH_TYPE_INTERFACE)
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': interface})
return Response(serializer.data)
class DeviceBayViewSet(WritableSerializerMixin, ModelViewSet):
queryset = DeviceBay.objects.select_related('installed_device')
serializer_class = serializers.DeviceBaySerializer
write_serializer_class = serializers.WritableDeviceBaySerializer
filter_class = filters.DeviceBayFilter
class InventoryItemViewSet(WritableSerializerMixin, ModelViewSet):
queryset = InventoryItem.objects.select_related('device', 'manufacturer')
serializer_class = serializers.InventoryItemSerializer
write_serializer_class = serializers.WritableInventoryItemSerializer
filter_class = filters.InventoryItemFilter
#
# Connections
#
class ConsoleConnectionViewSet(ListModelMixin, GenericViewSet):
queryset = ConsolePort.objects.select_related('device', 'cs_port__device').filter(cs_port__isnull=False)
serializer_class = serializers.ConsolePortSerializer
filter_class = filters.ConsoleConnectionFilter
class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
queryset = PowerPort.objects.select_related('device', 'power_outlet__device').filter(power_outlet__isnull=False)
serializer_class = serializers.PowerPortSerializer
filter_class = filters.PowerConnectionFilter
class InterfaceConnectionViewSet(WritableSerializerMixin, ModelViewSet):
queryset = InterfaceConnection.objects.select_related('interface_a__device', 'interface_b__device')
serializer_class = serializers.InterfaceConnectionSerializer
write_serializer_class = serializers.WritableInterfaceConnectionSerializer
filter_class = filters.InterfaceConnectionFilter
#
# Miscellaneous
#
class RelatedConnectionsView(APIView):
class ConnectedDeviceViewSet(ViewSet):
"""
Retrieve all connections related to a given console/power/interface connection
This endpoint allows a user to determine what device (if any) is connected to a given peer device and peer
interface. This is useful in a situation where a device boots with no configuration, but can detect its neighbors
via a protocol such as LLDP. Two query parameters must be included in the request:
* `peer-device`: The name of the peer device
* `peer-interface`: The name of the peer interface
"""
permission_classes = [IsAuthenticatedOrLoginNotRequired]
def __init__(self):
super(RelatedConnectionsView, self).__init__()
def get_view_name(self):
return "Connected Device Locator"
# Custom fields
self.content_type = ContentType.objects.get_for_model(Device)
self.custom_fields = self.content_type.custom_fields.prefetch_related('choices')
def list(self, request):
def get(self, request):
peer_device_name = request.query_params.get('peer-device')
peer_interface_name = request.query_params.get('peer-interface')
if not peer_device_name or not peer_interface_name:
raise MissingFilterException(detail='Request must include "peer-device" and "peer-interface" filters.')
peer_device = request.GET.get('peer-device')
peer_interface = request.GET.get('peer-interface')
# Determine local interface from peer interface's connection
peer_interface = get_object_or_404(Interface, device__name=peer_device_name, name=peer_interface_name)
local_interface = peer_interface.connected_interface
# Search by interface
if peer_device and peer_interface:
if local_interface is None:
return Response()
# Determine local interface from peer interface's connection
try:
peer_iface = Interface.objects.get(device__name=peer_device, name=peer_interface)
except Interface.DoesNotExist:
raise Http404()
local_iface = peer_iface.connected_interface
if local_iface:
device = local_iface.device
else:
return Response()
else:
raise MissingFilterException(detail='Must specify search parameters "peer-device" and "peer-interface".')
# Initialize response skeleton
response = {
'device': serializers.DeviceSerializer(device, context={'view': self}).data,
'console-ports': [],
'power-ports': [],
'interfaces': [],
}
# Console connections
console_ports = ConsolePort.objects.filter(device=device).select_related('cs_port__device')
for cp in console_ports:
data = serializers.ConsolePortSerializer(instance=cp).data
del(data['device'])
response['console-ports'].append(data)
# Power connections
power_ports = PowerPort.objects.filter(device=device).select_related('power_outlet__device')
for pp in power_ports:
data = serializers.PowerPortSerializer(instance=pp).data
del(data['device'])
response['power-ports'].append(data)
# Interface connections
interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering).filter(device=device)\
.select_related('connected_as_a', 'connected_as_b', 'circuit_termination')
for iface in interfaces:
data = serializers.InterfaceDetailSerializer(instance=iface).data
del(data['device'])
response['interfaces'].append(data)
return Response(response)
return Response(serializers.DeviceSerializer(local_interface.device, context={'request': request}).data)

View File

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

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

@@ -0,0 +1,238 @@
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_CX4 = 1170
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_CFP2 = 1510
IFACE_FF_100GE_CFP4 = 1520
IFACE_FF_100GE_CPAK = 1550
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)'],
[IFACE_FF_10GE_CX4, '10GBASE-CX4 (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_CFP2, 'CFP2 (100GE)'],
[IFACE_FF_100GE_CFP4, 'CFP4 (100GE)'],
[IFACE_FF_100GE_CPAK, 'Cisco CPAK (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,18 +1,41 @@
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 virtualization.models import Cluster
from .models import (
ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, IFACE_FF_LAG, Interface, InterfaceConnection,
Manufacturer, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackReservation, RackRole, Region, Site,
VIRTUAL_IFACE_TYPES,
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, STATUS_CHOICES, IFACE_FF_LAG, Interface, InterfaceConnection,
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(
@@ -20,23 +43,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)',
@@ -44,7 +63,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():
@@ -54,6 +73,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:
@@ -65,7 +87,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)',
)
@@ -78,7 +99,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):
@@ -87,8 +115,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)',
)
@@ -99,7 +127,6 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Site (slug)',
)
group_id = NullableModelMultipleChoiceFilter(
name='group',
queryset=RackGroup.objects.all(),
label='Group (ID)',
)
@@ -110,7 +137,6 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Group',
)
tenant_id = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
@@ -121,7 +147,6 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Tenant (slug)',
)
role_id = NullableModelMultipleChoiceFilter(
name='role',
queryset=RackRole.objects.all(),
label='Role (ID)',
)
@@ -134,7 +159,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta:
model = Rack
fields = ['u_height']
fields = ['serial', 'type', 'width', 'u_height', 'desc_units']
def search(self, queryset, name, value):
if not value.strip():
@@ -142,6 +167,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
return queryset.filter(
Q(name__icontains=value) |
Q(facility_id__icontains=value) |
Q(serial__icontains=value.strip()) |
Q(comments__icontains=value)
)
@@ -152,6 +178,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(),
@@ -174,15 +204,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():
@@ -195,6 +230,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(
@@ -202,7 +244,6 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Search',
)
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
name='manufacturer',
queryset=Manufacturer.objects.all(),
label='Manufacturer (ID)',
)
@@ -216,7 +257,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):
@@ -230,18 +272,125 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
)
class DeviceTypeComponentFilterSet(django_filters.FilterSet):
devicetype_id = django_filters.ModelMultipleChoiceFilter(
queryset=DeviceType.objects.all(),
name='device_type_id',
label='Device type (ID)',
)
class ConsolePortTemplateFilter(DeviceTypeComponentFilterSet):
class Meta:
model = ConsolePortTemplate
fields = ['name']
class ConsoleServerPortTemplateFilter(DeviceTypeComponentFilterSet):
class Meta:
model = ConsoleServerPortTemplate
fields = ['name']
class PowerPortTemplateFilter(DeviceTypeComponentFilterSet):
class Meta:
model = PowerPortTemplate
fields = ['name']
class PowerOutletTemplateFilter(DeviceTypeComponentFilterSet):
class Meta:
model = PowerOutletTemplate
fields = ['name']
class InterfaceTemplateFilter(DeviceTypeComponentFilterSet):
class Meta:
model = InterfaceTemplate
fields = ['name', 'form_factor', 'mgmt_only']
class DeviceBayTemplateFilter(DeviceTypeComponentFilterSet):
class Meta:
model = DeviceBayTemplate
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)',
)
@@ -261,43 +410,9 @@ 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)',
cluster_id = NullableModelMultipleChoiceFilter(
queryset=Cluster.objects.all(),
label='VM cluster (ID)',
)
model = django_filters.ModelMultipleChoiceFilter(
name='device_type__slug',
@@ -305,20 +420,12 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
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)',
)
status = django_filters.BooleanFilter(
name='status',
label='Status',
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',
@@ -332,10 +439,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',
)
class Meta:
model = Device
fields = ['name', 'serial', 'asset_tag']
fields = ['serial']
def search(self, queryset, name, value):
if not value.strip():
@@ -343,7 +458,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
return queryset.filter(
Q(name__icontains=value) |
Q(serial__icontains=value.strip()) |
Q(modules__serial__icontains=value.strip()) |
Q(inventory_items__serial__icontains=value.strip()) |
Q(asset_tag=value.strip()) |
Q(comments__icontains=value)
).distinct()
@@ -353,77 +468,58 @@ 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()
def _has_primary_ip(self, queryset, name, value):
if value:
return queryset.filter(
Q(primary_ip4__isnull=False) |
Q(primary_ip6__isnull=False)
)
else:
return queryset.exclude(
Q(primary_ip4__isnull=False) |
Q(primary_ip6__isnull=False)
)
class ConsolePortFilter(django_filters.FilterSet):
device_id = django_filters.ModelMultipleChoiceFilter(
name='device',
class DeviceComponentFilterSet(django_filters.FilterSet):
device_id = django_filters.ModelChoiceFilter(
queryset=Device.objects.all(),
label='Device (ID)',
)
device = django_filters.ModelMultipleChoiceFilter(
name='device',
device = django_filters.ModelChoiceFilter(
queryset=Device.objects.all(),
to_field_name='name',
label='Device (name)',
)
class ConsolePortFilter(DeviceComponentFilterSet):
class Meta:
model = ConsolePort
fields = ['name']
class ConsoleServerPortFilter(django_filters.FilterSet):
device_id = django_filters.ModelMultipleChoiceFilter(
name='device',
queryset=Device.objects.all(),
label='Device (ID)',
)
device = django_filters.ModelMultipleChoiceFilter(
name='device',
queryset=Device.objects.all(),
to_field_name='name',
label='Device (name)',
)
class ConsoleServerPortFilter(DeviceComponentFilterSet):
class Meta:
model = ConsoleServerPort
fields = ['name']
class PowerPortFilter(django_filters.FilterSet):
device_id = django_filters.ModelMultipleChoiceFilter(
name='device',
queryset=Device.objects.all(),
label='Device (ID)',
)
device = django_filters.ModelMultipleChoiceFilter(
name='device',
queryset=Device.objects.all(),
to_field_name='name',
label='Device (name)',
)
class PowerPortFilter(DeviceComponentFilterSet):
class Meta:
model = PowerPort
fields = ['name']
class PowerOutletFilter(django_filters.FilterSet):
device_id = django_filters.ModelMultipleChoiceFilter(
name='device',
queryset=Device.objects.all(),
label='Device (ID)',
)
device = django_filters.ModelMultipleChoiceFilter(
name='device',
queryset=Device.objects.all(),
to_field_name='name',
label='Device (name)',
)
class PowerOutletFilter(DeviceComponentFilterSet):
class Meta:
model = PowerOutlet
@@ -431,21 +527,29 @@ class PowerOutletFilter(django_filters.FilterSet):
class InterfaceFilter(django_filters.FilterSet):
device_id = django_filters.ModelMultipleChoiceFilter(
name='device',
queryset=Device.objects.all(),
label='Device (ID)',
"""
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 = django_filters.ModelMultipleChoiceFilter(
name='device',
queryset=Device.objects.all(),
to_field_name='name',
label='Device (name)',
device_id = django_filters.NumberFilter(
method='filter_device',
name='pk',
label='Device (ID)',
)
type = django_filters.CharFilter(
method='filter_type',
label='Interface type',
)
lag_id = django_filters.ModelMultipleChoiceFilter(
name='lag',
queryset=Interface.objects.all(),
label='LAG interface (ID)',
)
mac_address = django_filters.CharFilter(
method='_mac_address',
label='MAC address',
@@ -453,28 +557,65 @@ class InterfaceFilter(django_filters.FilterSet):
class Meta:
model = Interface
fields = ['name']
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()
class DeviceBayFilter(DeviceComponentFilterSet):
class Meta:
model = DeviceBay
fields = ['name']
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', 'part_id', 'serial', 'discovered']
class ConsoleConnectionFilter(django_filters.FilterSet):
site = django_filters.CharFilter(
method='filter_site',
@@ -485,6 +626,10 @@ class ConsoleConnectionFilter(django_filters.FilterSet):
label='Device',
)
class Meta:
model = ConsolePort
fields = ['name', 'connection_status']
def filter_site(self, queryset, name, value):
if not value.strip():
return queryset
@@ -509,6 +654,10 @@ class PowerConnectionFilter(django_filters.FilterSet):
label='Device',
)
class Meta:
model = PowerPort
fields = ['name', 'connection_status']
def filter_site(self, queryset, name, value):
if not value.strip():
return queryset
@@ -533,6 +682,10 @@ class InterfaceConnectionFilter(django_filters.FilterSet):
label='Device',
)
class Meta:
model = InterfaceConnection
fields = ['connection_status']
def filter_site(self, queryset, name, value):
if not value.strip():
return queryset

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,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.6 on 2017-03-17 18:39
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dcim', '0032_device_increase_name_length'),
]
operations = [
migrations.AlterField(
model_name='rackreservation',
name='rack',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='dcim.Rack'),
),
]

View File

@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.6 on 2017-03-21 14:55
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dcim', '0033_rackreservation_rack_editable'),
]
operations = [
migrations.RenameModel(
old_name='Module',
new_name='InventoryItem',
),
migrations.AlterField(
model_name='inventoryitem',
name='device',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='inventory_items', to='dcim.Device'),
),
migrations.AlterField(
model_name='inventoryitem',
name='parent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='child_items', to='dcim.InventoryItem'),
),
migrations.AlterField(
model_name='inventoryitem',
name='manufacturer',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='inventory_items', to='dcim.Manufacturer'),
),
]

View File

@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-05-08 15:57
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0034_rename_module_to_inventoryitem'),
]
# We convert the BooleanField to an IntegerField first as PostgreSQL does not provide a direct cast for boolean to
# smallint (attempting to convert directly yields the error "cannot cast type boolean to smallint").
operations = [
migrations.AlterField(
model_name='device',
name='status',
field=models.PositiveIntegerField(choices=[[1, b'Active'], [0, b'Offline'], [2, b'Planned'], [3, b'Staged'], [4, b'Failed'], [5, b'Inventory']], default=1, verbose_name=b'Status'),
),
migrations.AlterField(
model_name='device',
name='status',
field=models.PositiveSmallIntegerField(choices=[[1, b'Active'], [0, b'Offline'], [2, b'Planned'], [3, b'Staged'], [4, b'Failed'], [5, b'Inventory']], default=1, verbose_name=b'Status'),
),
]

View File

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

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

@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-08-29 21:00
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0041_napalm_integration'),
]
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)'], [1170, '10GBASE-CX4 (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)'], [1170, '10GBASE-CX4 (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,70 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-08-29 21:26
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0042_interface_ff_10ge_cx4'),
]
operations = [
migrations.AlterField(
model_name='consoleport',
name='name',
field=models.CharField(max_length=50),
),
migrations.AlterField(
model_name='consoleporttemplate',
name='name',
field=models.CharField(max_length=50),
),
migrations.AlterField(
model_name='consoleserverport',
name='name',
field=models.CharField(max_length=50),
),
migrations.AlterField(
model_name='consoleserverporttemplate',
name='name',
field=models.CharField(max_length=50),
),
migrations.AlterField(
model_name='devicebaytemplate',
name='name',
field=models.CharField(max_length=50),
),
migrations.AlterField(
model_name='interface',
name='name',
field=models.CharField(max_length=64),
),
migrations.AlterField(
model_name='interfacetemplate',
name='name',
field=models.CharField(max_length=64),
),
migrations.AlterField(
model_name='poweroutlet',
name='name',
field=models.CharField(max_length=50),
),
migrations.AlterField(
model_name='poweroutlettemplate',
name='name',
field=models.CharField(max_length=50),
),
migrations.AlterField(
model_name='powerport',
name='name',
field=models.CharField(max_length=50),
),
migrations.AlterField(
model_name='powerporttemplate',
name='name',
field=models.CharField(max_length=50),
),
]

View File

@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-08-31 14:15
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('virtualization', '0001_virtualization'),
('dcim', '0043_device_component_name_lengths'),
]
operations = [
migrations.AddField(
model_name='device',
name='cluster',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='devices', to='virtualization.Cluster'),
),
migrations.AddField(
model_name='interface',
name='virtual_machine',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfaces', to='virtualization.VirtualMachine'),
),
migrations.AlterField(
model_name='interface',
name='device',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfaces', to='dcim.Device'),
),
]

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-09-29 16:09
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0044_virtualization'),
]
operations = [
migrations.AddField(
model_name='devicerole',
name='vm_role',
field=models.BooleanField(default=True, help_text='Virtual machines may be assigned to this role', verbose_name='VM Role'),
),
]

View File

@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-10-09 17:43
from __future__ import unicode_literals
from django.db import migrations
import utilities.fields
class Migration(migrations.Migration):
dependencies = [
('dcim', '0045_devicerole_vm_role'),
]
operations = [
migrations.AlterField(
model_name='rack',
name='facility_id',
field=utilities.fields.NullableCharField(blank=True, max_length=50, null=True, verbose_name='Facility ID'),
),
]

View File

@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-10-09 18:43
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0046_rack_lengthen_facility_id'),
]
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)'], [1170, '10GBASE-CX4 (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)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (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)'], [1170, '10GBASE-CX4 (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)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (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,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-10-09 18:50
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0047_more_100ge_form_factors'),
]
operations = [
migrations.AddField(
model_name='rack',
name='serial',
field=models.CharField(blank=True, max_length=50, verbose_name='Serial number'),
),
]

View File

@@ -1,3 +1,4 @@
from __future__ import unicode_literals
from collections import OrderedDict
from itertools import count, groupby
@@ -9,198 +10,23 @@ from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericRelation
from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import Count, Q, ObjectDoesNotExist
from django.urls import reverse
from django.utils.encoding import python_2_unicode_compatible
from circuits.models import Circuit
from extras.models import CustomFieldModel, CustomField, CustomFieldValue
from extras.models import CustomFieldModel, CustomField, CustomFieldValue, ImageAttachment
from extras.rpc import RPC_CLIENTS
from tenancy.models import Tenant
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
# 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'],
]
],
[
'Other',
[
[IFACE_FF_OTHER, 'Other'],
]
],
]
VIRTUAL_IFACE_TYPES = [
IFACE_FF_VIRTUAL,
IFACE_FF_LAG,
]
STATUS_ACTIVE = True
STATUS_OFFLINE = False
STATUS_CHOICES = [
[STATUS_ACTIVE, 'Active'],
[STATUS_OFFLINE, 'Offline'],
]
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)'],
]
from .querysets import InterfaceQuerySet
#
@@ -212,10 +38,16 @@ class Region(MPTTModel):
"""
Sites can be grouped within geographic Regions.
"""
parent = TreeForeignKey('self', null=True, blank=True, related_name='children', db_index=True)
parent = TreeForeignKey(
'self', null=True, blank=True, related_name='children', db_index=True, on_delete=models.CASCADE
)
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
csv_headers = [
'name', 'slug', 'parent',
]
class MPTTMeta:
order_insertion_by = ['name']
@@ -225,6 +57,13 @@ class Region(MPTTModel):
def get_absolute_url(self):
return "{}?region={}".format(reverse('dcim:site_list'), self.slug)
def to_csv(self):
return csv_format([
self.name,
self.slug,
self.parent.name if self.parent else None,
])
#
# Sites
@@ -255,9 +94,14 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
contact_email = models.EmailField(blank=True, verbose_name="Contact E-mail")
comments = models.TextField(blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
images = GenericRelation(ImageAttachment)
objects = SiteManager()
csv_headers = [
'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email',
]
class Meta:
ordering = ['name']
@@ -314,7 +158,11 @@ class RackGroup(models.Model):
"""
name = models.CharField(max_length=50)
slug = models.SlugField()
site = models.ForeignKey('Site', related_name='rack_groups')
site = models.ForeignKey('Site', related_name='rack_groups', on_delete=models.CASCADE)
csv_headers = [
'site', 'name', 'slug',
]
class Meta:
ordering = ['site', 'name']
@@ -324,11 +172,18 @@ 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)
def to_csv(self):
return csv_format([
self.site,
self.name,
self.slug,
])
@python_2_unicode_compatible
class RackRole(models.Model):
@@ -362,11 +217,12 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
Each Rack is assigned to a Site and (optionally) a RackGroup.
"""
name = models.CharField(max_length=50)
facility_id = NullableCharField(max_length=30, blank=True, null=True, verbose_name='Facility ID')
facility_id = NullableCharField(max_length=50, blank=True, null=True, verbose_name='Facility ID')
site = models.ForeignKey('Site', related_name='racks', on_delete=models.PROTECT)
group = models.ForeignKey('RackGroup', related_name='racks', blank=True, null=True, on_delete=models.SET_NULL)
tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='racks', on_delete=models.PROTECT)
role = models.ForeignKey('RackRole', related_name='racks', blank=True, null=True, on_delete=models.PROTECT)
serial = models.CharField(max_length=50, blank=True, verbose_name='Serial number')
type = models.PositiveSmallIntegerField(choices=RACK_TYPE_CHOICES, blank=True, null=True, verbose_name='Type')
width = models.PositiveSmallIntegerField(choices=RACK_WIDTH_CHOICES, default=RACK_WIDTH_19IN, verbose_name='Width',
help_text='Rail-to-rail width')
@@ -376,9 +232,15 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
help_text='Units are numbered top-to-bottom')
comments = models.TextField(blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
images = GenericRelation(ImageAttachment)
objects = RackManager()
csv_headers = [
'site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'serial', 'width', 'u_height',
'desc_units',
]
class Meta:
ordering = ['site', 'name']
unique_together = [
@@ -387,7 +249,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
]
def __str__(self):
return self.display_name
return self.display_name or super(Rack, self).__str__()
def get_absolute_url(self):
return reverse('dcim:rack', args=[self.pk])
@@ -443,8 +305,10 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
@property
def display_name(self):
if self.facility_id:
return u"{} ({})".format(self.name, self.facility_id)
return self.name
return "{} ({})".format(self.name, self.facility_id)
elif self.name:
return self.name
return ""
def get_rack_units(self, face=RACK_FACE_FRONT, exclude=None, remove_redundant=False):
"""
@@ -518,6 +382,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)
@@ -534,7 +408,7 @@ class RackReservation(models.Model):
"""
One or more reserved units within a Rack.
"""
rack = models.ForeignKey('Rack', related_name='reservations', editable=False, on_delete=models.CASCADE)
rack = models.ForeignKey('Rack', related_name='reservations', on_delete=models.CASCADE)
units = ArrayField(models.PositiveSmallIntegerField())
created = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey(User, editable=False, on_delete=models.PROTECT)
@@ -544,7 +418,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):
@@ -554,7 +428,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]),
),
@@ -594,6 +468,10 @@ class Manufacturer(models.Model):
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
csv_headers = [
'name', 'slug',
]
class Meta:
ordering = ['name']
@@ -603,6 +481,12 @@ class Manufacturer(models.Model):
def get_absolute_url(self):
return "{}?manufacturer={}".format(reverse('dcim:devicetype_list'), self.slug)
def to_csv(self):
return csv_format([
self.name,
self.slug,
])
@python_2_unicode_compatible
class DeviceType(models.Model, CustomFieldModel):
@@ -642,6 +526,11 @@ class DeviceType(models.Model, CustomFieldModel):
comments = models.TextField(blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
csv_headers = [
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server',
'is_pdu', 'is_network_device', 'subdevice_role', 'interface_ordering',
]
class Meta:
ordering = ['manufacturer', 'model']
unique_together = [
@@ -661,6 +550,21 @@ class DeviceType(models.Model, CustomFieldModel):
def get_absolute_url(self):
return reverse('dcim:devicetype', args=[self.pk])
def to_csv(self):
return csv_format([
self.manufacturer.name,
self.model,
self.slug,
self.part_number,
self.u_height,
self.is_full_depth,
self.is_console_server,
self.is_pdu,
self.is_network_device,
self.get_subdevice_role_display() if self.subdevice_role else None,
self.get_interface_ordering_display(),
])
def clean(self):
# If editing an existing DeviceType to have a larger u_height, first validate that *all* instances of it have
@@ -708,7 +612,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):
@@ -725,7 +629,7 @@ class ConsolePortTemplate(models.Model):
A template for a ConsolePort to be created for a new Device.
"""
device_type = models.ForeignKey('DeviceType', related_name='console_port_templates', on_delete=models.CASCADE)
name = models.CharField(max_length=30)
name = models.CharField(max_length=50)
class Meta:
ordering = ['device_type', 'name']
@@ -741,7 +645,7 @@ class ConsoleServerPortTemplate(models.Model):
A template for a ConsoleServerPort to be created for a new Device.
"""
device_type = models.ForeignKey('DeviceType', related_name='cs_port_templates', on_delete=models.CASCADE)
name = models.CharField(max_length=30)
name = models.CharField(max_length=50)
class Meta:
ordering = ['device_type', 'name']
@@ -757,7 +661,7 @@ class PowerPortTemplate(models.Model):
A template for a PowerPort to be created for a new Device.
"""
device_type = models.ForeignKey('DeviceType', related_name='power_port_templates', on_delete=models.CASCADE)
name = models.CharField(max_length=30)
name = models.CharField(max_length=50)
class Meta:
ordering = ['device_type', 'name']
@@ -773,7 +677,7 @@ class PowerOutletTemplate(models.Model):
A template for a PowerOutlet to be created for a new Device.
"""
device_type = models.ForeignKey('DeviceType', related_name='power_outlet_templates', on_delete=models.CASCADE)
name = models.CharField(max_length=30)
name = models.CharField(max_length=50)
class Meta:
ordering = ['device_type', 'name']
@@ -783,57 +687,17 @@ class PowerOutletTemplate(models.Model):
return self.name
class InterfaceManager(models.Manager):
def order_naturally(self, method=IFACE_ORDERING_POSITION):
"""
Naturally order interfaces by their name and numeric position. The sort method must be one of the defined
IFACE_ORDERING_CHOICES (typically indicated by a parent Device's DeviceType).
To order interfaces naturally, the `name` field is split into five distinct components: leading text (name),
slot, subslot, position, channel, and virtual circuit:
{name}{slot}/{subslot}/{position}:{channel}.{vc}
Components absent from the interface name are ignored. For example, an interface named GigabitEthernet0/1 would
be parsed as follows:
name = 'GigabitEthernet'
slot = None
subslot = 0
position = 1
channel = None
vc = 0
The chosen sorting method will determine which fields are ordered first in the query.
"""
queryset = self.get_queryset()
sql_col = '{}.name'.format(queryset.model._meta.db_table)
ordering = {
IFACE_ORDERING_POSITION: ('_slot', '_subslot', '_position', '_channel', '_vc', '_name'),
IFACE_ORDERING_NAME: ('_name', '_slot', '_subslot', '_position', '_channel', '_vc'),
}[method]
return queryset.extra(select={
'_name': "SUBSTRING({} FROM '^([^0-9]+)')".format(sql_col),
'_slot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
'_subslot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
'_position': "CAST(SUBSTRING({} FROM '([0-9]+)(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
'_channel': "COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)".format(sql_col),
'_vc': "COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)".format(sql_col),
}).order_by(*ordering)
@python_2_unicode_compatible
class InterfaceTemplate(models.Model):
"""
A template for a physical data interface on a new Device.
"""
device_type = models.ForeignKey('DeviceType', related_name='interface_templates', on_delete=models.CASCADE)
name = models.CharField(max_length=30)
name = models.CharField(max_length=64)
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']
@@ -849,7 +713,7 @@ class DeviceBayTemplate(models.Model):
A template for a DeviceBay to be created for a new parent Device.
"""
device_type = models.ForeignKey('DeviceType', related_name='device_bay_templates', on_delete=models.CASCADE)
name = models.CharField(max_length=30)
name = models.CharField(max_length=50)
class Meta:
ordering = ['device_type', 'name']
@@ -867,11 +731,17 @@ class DeviceBayTemplate(models.Model):
class DeviceRole(models.Model):
"""
Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a
color to be used when displaying rack elevations.
color to be used when displaying rack elevations. The vm_role field determines whether the role is applicable to
virtual machines as well.
"""
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
color = ColorField()
vm_role = models.BooleanField(
default=True,
verbose_name="VM Role",
help_text="Virtual machines may be assigned to this role"
)
class Meta:
ordering = ['name']
@@ -892,7 +762,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']
@@ -916,11 +789,11 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
DeviceRole, and (optionally) a Platform. Device names are not required, however if one is set it must be unique.
Each Device must be assigned to a Rack, although associating it with a particular rack face or unit is optional (for
example, vertically mounted PDUs do not consume rack units).
Each Device must be assigned to a site, and optionally to a rack within that site. Associating a device with a
particular rack face or unit is optional (for example, vertically mounted PDUs do not consume rack units).
When a new Device is created, console/power/interface components are created along with it as dictated by the
component templates assigned to its DeviceType. Components can also be added, modified, or deleted after the
When a new Device is created, console/power/interface/device bay components are created along with it as dictated
by the component templates assigned to its DeviceType. Components can also be added, modified, or deleted after the
creation of a Device.
"""
device_type = models.ForeignKey('DeviceType', related_name='instances', on_delete=models.PROTECT)
@@ -929,30 +802,54 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
platform = models.ForeignKey('Platform', related_name='devices', blank=True, null=True, on_delete=models.SET_NULL)
name = NullableCharField(max_length=64, blank=True, null=True, unique=True)
serial = models.CharField(max_length=50, blank=True, verbose_name='Serial number')
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 device')
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 device'
)
site = models.ForeignKey('Site', related_name='devices', on_delete=models.PROTECT)
rack = models.ForeignKey('Rack', related_name='devices', blank=True, null=True, on_delete=models.PROTECT)
position = models.PositiveSmallIntegerField(blank=True, null=True, validators=[MinValueValidator(1)],
verbose_name='Position (U)',
help_text='The lowest-numbered unit occupied by the device')
position = models.PositiveSmallIntegerField(
blank=True, null=True, validators=[MinValueValidator(1)], verbose_name='Position (U)',
help_text='The lowest-numbered unit occupied by the device'
)
face = models.PositiveSmallIntegerField(blank=True, null=True, choices=RACK_FACE_CHOICES, verbose_name='Rack face')
status = models.BooleanField(choices=STATUS_CHOICES, default=STATUS_ACTIVE, verbose_name='Status')
primary_ip4 = models.OneToOneField('ipam.IPAddress', related_name='primary_ip4_for', on_delete=models.SET_NULL,
blank=True, null=True, verbose_name='Primary IPv4')
primary_ip6 = models.OneToOneField('ipam.IPAddress', related_name='primary_ip6_for', on_delete=models.SET_NULL,
blank=True, null=True, verbose_name='Primary IPv6')
status = models.PositiveSmallIntegerField(choices=STATUS_CHOICES, default=STATUS_ACTIVE, verbose_name='Status')
primary_ip4 = models.OneToOneField(
'ipam.IPAddress', related_name='primary_ip4_for', on_delete=models.SET_NULL, blank=True, null=True,
verbose_name='Primary IPv4'
)
primary_ip6 = models.OneToOneField(
'ipam.IPAddress', related_name='primary_ip6_for', on_delete=models.SET_NULL, blank=True, null=True,
verbose_name='Primary IPv6'
)
cluster = models.ForeignKey(
to='virtualization.Cluster',
on_delete=models.SET_NULL,
related_name='devices',
blank=True,
null=True
)
comments = models.TextField(blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
images = GenericRelation(ImageAttachment)
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
return self.display_name or super(Device, self).__str__()
def get_absolute_url(self):
return reverse('dcim:device', args=[self.pk])
@@ -1014,6 +911,36 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
except DeviceType.DoesNotExist:
pass
# Validate primary IPv4 address
if self.primary_ip4 and (
self.primary_ip4.interface is None or
self.primary_ip4.interface.device != self
) and (
self.primary_ip4.nat_inside.interface is None or
self.primary_ip4.nat_inside.interface.device != self
):
raise ValidationError({
'primary_ip4': "The specified IP address ({}) is not assigned to this device.".format(self.primary_ip4),
})
# Validate primary IPv6 address
if self.primary_ip6 and (
self.primary_ip6.interface is None or
self.primary_ip6.interface.device != self
) and (
self.primary_ip6.nat_inside.interface is None or
self.primary_ip6.nat_inside.interface.device != self
):
raise ValidationError({
'primary_ip6': "The specified IP address ({}) is not assigned to this device.".format(self.primary_ip6),
})
# A Device can only be assigned to a Cluster in the same Site (or no Site)
if self.cluster and self.cluster.site is not None and self.cluster.site != self.site:
raise ValidationError({
'cluster': "The assigned cluster belongs to a different site ({})".format(self.cluster.site)
})
def save(self, *args, **kwargs):
is_new = not bool(self.pk)
@@ -1060,7 +987,9 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
self.platform.name if self.platform else None,
self.serial,
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(),
@@ -1070,12 +999,9 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
def display_name(self):
if self.name:
return self.name
elif self.position:
return u"{} ({} U{})".format(self.device_type, self.rack.name, self.position)
elif self.rack:
return u"{} ({})".format(self.device_type, self.rack.name)
else:
return u"{} ({})".format(self.device_type, self.site.name)
elif hasattr(self, 'device_type'):
return "{}".format(self.device_type)
return ""
@property
def identifier(self):
@@ -1103,6 +1029,9 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
"""
return Device.objects.filter(parent_bay__device=self.pk)
def get_status_class(self):
return DEVICE_STATUS_CLASSES[self.status]
def get_rpc_client(self):
"""
Return the appropriate RPC (e.g. NETCONF, ssh, etc.) client for this device's platform, if one is defined.
@@ -1122,11 +1051,13 @@ class ConsolePort(models.Model):
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
"""
device = models.ForeignKey('Device', related_name='console_ports', on_delete=models.CASCADE)
name = models.CharField(max_length=30)
name = models.CharField(max_length=50)
cs_port = models.OneToOneField('ConsoleServerPort', related_name='connected_console', on_delete=models.SET_NULL,
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']
@@ -1170,7 +1101,7 @@ class ConsoleServerPort(models.Model):
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
"""
device = models.ForeignKey('Device', related_name='cs_ports', on_delete=models.CASCADE)
name = models.CharField(max_length=30)
name = models.CharField(max_length=50)
objects = ConsoleServerPortManager()
@@ -1191,11 +1122,13 @@ class PowerPort(models.Model):
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
"""
device = models.ForeignKey('Device', related_name='power_ports', on_delete=models.CASCADE)
name = models.CharField(max_length=30)
name = models.CharField(max_length=50)
power_outlet = models.OneToOneField('PowerOutlet', related_name='connected_port', on_delete=models.SET_NULL,
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']
@@ -1233,7 +1166,7 @@ class PowerOutlet(models.Model):
A physical power outlet (output) within a Device which provides power to a PowerPort.
"""
device = models.ForeignKey('Device', related_name='power_outlets', on_delete=models.CASCADE)
name = models.CharField(max_length=30)
name = models.CharField(max_length=50)
objects = PowerOutletManager()
@@ -1251,20 +1184,44 @@ class PowerOutlet(models.Model):
@python_2_unicode_compatible
class Interface(models.Model):
"""
A physical data interface within a Device. An Interface can connect to exactly one other Interface via the creation
of an InterfaceConnection.
A network interface within a Device or VirtualMachine. A physical Interface can connect to exactly one other
Interface via the creation 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')
name = models.CharField(max_length=30)
device = models.ForeignKey(
to='Device',
on_delete=models.CASCADE,
related_name='interfaces',
null=True,
blank=True
)
virtual_machine = models.ForeignKey(
to='virtualization.VirtualMachine',
on_delete=models.CASCADE,
related_name='interfaces',
null=True,
blank=True
)
lag = models.ForeignKey(
to='self',
on_delete=models.SET_NULL,
related_name='member_interfaces',
null=True,
blank=True,
verbose_name='Parent LAG'
)
name = models.CharField(max_length=64)
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']
@@ -1275,39 +1232,59 @@ class Interface(models.Model):
def clean(self):
# Virtual interfaces cannot be connected
if self.form_factor in VIRTUAL_IFACE_TYPES and self.is_connected:
# An Interface must belong to a Device *or* to a VirtualMachine
if self.device and self.virtual_machine:
raise ValidationError("An interface cannot belong to both a device and a virtual machine.")
if not self.device and not self.virtual_machine:
raise ValidationError("An interface must belong to either a device or a virtual machine.")
# VM interfaces must be virtual
if self.virtual_machine and self.form_factor is not IFACE_FF_VIRTUAL:
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 machines can only have virtual interfaces."
})
# Virtual interfaces cannot be connected
if self.form_factor in NONCONNECTABLE_IFACE_TYPES and self.is_connected:
raise ValidationError({
'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()])
)
})
@property
def parent(self):
return self.device or self.virtual_machine
@property
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
@@ -1357,11 +1334,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):
@@ -1393,7 +1375,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):
@@ -1409,23 +1391,29 @@ class DeviceBay(models.Model):
#
# Modules
# Inventory items
#
@python_2_unicode_compatible
class Module(models.Model):
class InventoryItem(models.Model):
"""
A Module represents a piece of hardware within a Device, such as a line card or power supply. Modules are used only
for inventory purposes.
An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
InventoryItems are used only for inventory purposes.
"""
device = models.ForeignKey('Device', related_name='modules', on_delete=models.CASCADE)
parent = models.ForeignKey('self', related_name='submodules', blank=True, null=True, on_delete=models.CASCADE)
device = models.ForeignKey('Device', related_name='inventory_items', on_delete=models.CASCADE)
parent = models.ForeignKey('self', related_name='child_items', blank=True, null=True, on_delete=models.CASCADE)
name = models.CharField(max_length=50, verbose_name='Name')
manufacturer = models.ForeignKey('Manufacturer', related_name='modules', 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']

72
netbox/dcim/querysets.py Normal file
View File

@@ -0,0 +1,72 @@
from __future__ import unicode_literals
from django.db.models import QuerySet
from django.db.models.expressions import RawSQL
from .constants import IFACE_ORDERING_NAME, IFACE_ORDERING_POSITION, NONCONNECTABLE_IFACE_TYPES
class InterfaceQuerySet(QuerySet):
def order_naturally(self, method=IFACE_ORDERING_POSITION):
"""
Naturally order interfaces by their type and numeric position. The sort method must be one of the defined
IFACE_ORDERING_CHOICES (typically indicated by a parent Device's DeviceType).
To order interfaces naturally, the `name` field is split into six distinct components: leading text (type),
slot, subslot, position, channel, and virtual circuit:
{type}{slot}/{subslot}/{position}/{subposition}:{channel}.{vc}
Components absent from the interface name are ignored. For example, an interface named GigabitEthernet1/2/3
would be parsed as follows:
name = 'GigabitEthernet'
slot = 1
subslot = 2
position = 3
subposition = 0
channel = None
vc = 0
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.
"""
sql_col = '{}.name'.format(self.model._meta.db_table)
ordering = {
IFACE_ORDERING_POSITION: (
'_slot', '_subslot', '_position', '_subposition', '_channel', '_type', '_vc', '_id', 'name',
),
IFACE_ORDERING_NAME: (
'_type', '_slot', '_subslot', '_position', '_subposition', '_channel', '_vc', '_id', 'name',
),
}[method]
TYPE_RE = r"SUBSTRING({} FROM '^([^0-9]+)')"
ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)$') AS integer)"
SLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)\/') AS integer)"
SUBSLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/)([0-9]+)') AS integer), 0)"
POSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/){{2}}([0-9]+)') AS integer), 0)"
SUBPOSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/){{3}}([0-9]+)') AS integer), 0)"
CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)"
VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)"
fields = {
'_type': RawSQL(TYPE_RE.format(sql_col), []),
'_id': RawSQL(ID_RE.format(sql_col), []),
'_slot': RawSQL(SLOT_RE.format(sql_col), []),
'_subslot': RawSQL(SUBSLOT_RE.format(sql_col), []),
'_position': RawSQL(POSITION_RE.format(sql_col), []),
'_subposition': RawSQL(SUBPOSITION_RE.format(sql_col), []),
'_channel': RawSQL(CHANNEL_RE.format(sql_col), []),
'_vc': RawSQL(VC_RE.format(sql_col), []),
}
return self.annotate(**fields).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)

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, 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,
)
@@ -92,12 +93,8 @@ DEVICE_ROLE = """
<label class="label" style="background-color: #{{ record.device_role.color }}">{{ value }}</label>
"""
STATUS_ICON = """
{% if record.status %}
<span class="glyphicon glyphicon-ok-sign text-success" title="Active" aria-hidden="true"></span>
{% else %}
<span class="glyphicon glyphicon-minus-sign text-danger" title="Offline" aria-hidden="true"></span>
{% endif %}
DEVICE_STATUS = """
<span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
"""
DEVICE_PRIMARY_IP = """
@@ -142,19 +139,23 @@ class RegionTable(BaseTable):
class SiteTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn('dcim:site', args=[Accessor('slug')], verbose_name='Name')
facility = tables.Column(verbose_name='Facility')
region = tables.TemplateColumn(template_code=SITE_REGION_LINK, verbose_name='Region')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
asn = tables.Column(verbose_name='ASN')
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',
@@ -203,20 +204,26 @@ class RackRoleTable(BaseTable):
class RackTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn('dcim:rack', args=[Accessor('pk')], verbose_name='Name')
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
name = tables.LinkColumn()
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
facility_id = tables.Column(verbose_name='Facility ID')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
role = tables.TemplateColumn(RACK_ROLE, verbose_name='Role')
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'), verbose_name='Devices')
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', 'devices',
'get_utilization')
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 RackImportTable(BaseTable):
@@ -229,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')
#
@@ -272,9 +279,7 @@ class ManufacturerTable(BaseTable):
class DeviceTypeTable(BaseTable):
pk = ToggleColumn()
manufacturer = tables.Column(verbose_name='Manufacturer')
model = tables.LinkColumn('dcim:devicetype', args=[Accessor('pk')], verbose_name='Device Type')
part_number = tables.Column(verbose_name='Part Number')
is_full_depth = tables.BooleanColumn(verbose_name='Full Depth')
is_console_server = tables.BooleanColumn(verbose_name='CS')
is_pdu = tables.BooleanColumn(verbose_name='PDU')
@@ -286,7 +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'
'is_network_device', 'subdevice_role', 'instance_count',
)
@@ -301,7 +306,6 @@ class ConsolePortTemplateTable(BaseTable):
model = ConsolePortTemplate
fields = ('pk', 'name')
empty_text = "None"
show_header = False
class ConsoleServerPortTemplateTable(BaseTable):
@@ -311,7 +315,6 @@ class ConsoleServerPortTemplateTable(BaseTable):
model = ConsoleServerPortTemplate
fields = ('pk', 'name')
empty_text = "None"
show_header = False
class PowerPortTemplateTable(BaseTable):
@@ -321,7 +324,6 @@ class PowerPortTemplateTable(BaseTable):
model = PowerPortTemplate
fields = ('pk', 'name')
empty_text = "None"
show_header = False
class PowerOutletTemplateTable(BaseTable):
@@ -331,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):
@@ -351,7 +352,6 @@ class DeviceBayTemplateTable(BaseTable):
model = DeviceBayTemplate
fields = ('pk', 'name')
empty_text = "None"
show_header = False
#
@@ -362,14 +362,14 @@ class DeviceRoleTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Name')
device_count = tables.Column(verbose_name='Devices')
color = tables.TemplateColumn(COLOR_LABEL, verbose_name='Color')
color = tables.TemplateColumn(COLOR_LABEL, verbose_name='Label')
slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn(template_code=DEVICEROLE_ACTIONS, attrs={'td': {'class': 'text-right'}},
verbose_name='')
class Meta(BaseTable.Meta):
model = DeviceRole
fields = ('pk', 'name', 'device_count', 'color', 'slug', 'actions')
fields = ('pk', 'name', 'device_count', 'color', 'vm_role', 'slug', 'actions')
#
@@ -386,7 +386,7 @@ class PlatformTable(BaseTable):
class Meta(BaseTable.Meta):
model = Platform
fields = ('pk', 'name', 'device_count', 'slug', 'actions')
fields = ('pk', 'name', 'device_count', 'slug', 'napalm_driver', 'actions')
#
@@ -395,24 +395,35 @@ class PlatformTable(BaseTable):
class DeviceTable(BaseTable):
pk = ToggleColumn()
status = tables.TemplateColumn(template_code=STATUS_ICON, verbose_name='')
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack')
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)
primary_ip = tables.TemplateColumn(orderable=False, verbose_name='IP Address',
template_code=DEVICE_PRIMARY_IP)
device_type = tables.LinkColumn(
'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(DeviceTable.Meta):
model = Device
fields = ('pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type', 'primary_ip')
class DeviceImportTable(BaseTable):
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack')
@@ -422,10 +433,56 @@ class DeviceImportTable(BaseTable):
class Meta(BaseTable.Meta):
model = Device
fields = ('name', 'tenant', 'site', 'rack', 'position', 'device_role', 'device_type')
fields = ('name', 'status', 'tenant', 'site', 'rack', 'position', 'device_role', 'device_type')
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
#

File diff suppressed because it is too large Load Diff

View File

@@ -1,676 +0,0 @@
import json
from rest_framework import status
from rest_framework.test import APITestCase
from django.conf import settings
class SiteTest(APITestCase):
fixtures = [
'dcim',
'ipam',
'extras',
]
standard_fields = [
'id',
'name',
'slug',
'region',
'tenant',
'facility',
'asn',
'physical_address',
'shipping_address',
'contact_name',
'contact_phone',
'contact_email',
'comments',
'custom_fields',
'count_prefixes',
'count_vlans',
'count_racks',
'count_devices',
'count_circuits'
]
nested_fields = [
'id',
'name',
'slug'
]
rack_fields = [
'id',
'name',
'facility_id',
'display_name',
'site',
'group',
'tenant',
'role',
'type',
'width',
'u_height',
'desc_units',
'comments',
'custom_fields',
]
graph_fields = [
'name',
'embed_url',
'embed_link',
]
def test_get_list(self, endpoint='/{}api/dcim/sites/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
for i in content:
self.assertEqual(
sorted(i.keys()),
sorted(self.standard_fields),
)
def test_get_detail(self, endpoint='/{}api/dcim/sites/1/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
sorted(content.keys()),
sorted(self.standard_fields),
)
def test_get_site_list_rack(self, endpoint='/{}api/dcim/sites/1/racks/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
for i in json.loads(response.content.decode('utf-8')):
self.assertEqual(
sorted(i.keys()),
sorted(self.rack_fields),
)
# Check Nested Serializer.
self.assertEqual(
sorted(i.get('site').keys()),
sorted(self.nested_fields),
)
def test_get_site_list_graphs(self, endpoint='/{}api/dcim/sites/1/graphs/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
for i in json.loads(response.content.decode('utf-8')):
self.assertEqual(
sorted(i.keys()),
sorted(self.graph_fields),
)
class RackTest(APITestCase):
fixtures = [
'dcim',
'ipam'
]
nested_fields = [
'id',
'name',
'facility_id',
'display_name'
]
standard_fields = [
'id',
'name',
'facility_id',
'display_name',
'site',
'group',
'tenant',
'role',
'type',
'width',
'u_height',
'desc_units',
'comments',
'custom_fields',
]
detail_fields = [
'id',
'name',
'facility_id',
'display_name',
'site',
'group',
'tenant',
'role',
'type',
'width',
'u_height',
'desc_units',
'reservations',
'comments',
'custom_fields',
'front_units',
'rear_units'
]
def test_get_list(self, endpoint='/{}api/dcim/racks/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
for i in content:
self.assertEqual(
sorted(i.keys()),
sorted(self.standard_fields),
)
self.assertEqual(
sorted(i.get('site').keys()),
sorted(SiteTest.nested_fields),
)
def test_get_detail(self, endpoint='/{}api/dcim/racks/1/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
sorted(content.keys()),
sorted(self.detail_fields),
)
self.assertEqual(
sorted(content.get('site').keys()),
sorted(SiteTest.nested_fields),
)
class ManufacturersTest(APITestCase):
fixtures = [
'dcim',
'ipam'
]
standard_fields = [
'id',
'name',
'slug',
]
nested_fields = standard_fields
def test_get_list(self, endpoint='/{}api/dcim/manufacturers/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
for i in content:
self.assertEqual(
sorted(i.keys()),
sorted(self.standard_fields),
)
def test_get_detail(self, endpoint='/{}api/dcim/manufacturers/1/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
sorted(content.keys()),
sorted(self.standard_fields),
)
class DeviceTypeTest(APITestCase):
fixtures = ['dcim', 'ipam']
standard_fields = [
'id',
'manufacturer',
'model',
'slug',
'part_number',
'u_height',
'is_full_depth',
'interface_ordering',
'is_console_server',
'is_pdu',
'is_network_device',
'subdevice_role',
'comments',
'custom_fields',
'instance_count',
]
nested_fields = [
'id',
'manufacturer',
'model',
'slug'
]
def test_get_list(self, endpoint='/{}api/dcim/device-types/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
for i in content:
self.assertEqual(
sorted(i.keys()),
sorted(self.standard_fields),
)
def test_detail_list(self, endpoint='/{}api/dcim/device-types/1/'.format(settings.BASE_PATH)):
# TODO: details returns list view.
# response = self.client.get(endpoint)
# content = json.loads(response.content.decode('utf-8'))
# self.assertEqual(response.status_code, status.HTTP_200_OK)
# self.assertEqual(
# sorted(content.keys()),
# sorted(self.standard_fields),
# )
# self.assertEqual(
# sorted(content.get('manufacturer').keys()),
# sorted(ManufacturersTest.nested_fields),
# )
pass
class DeviceRolesTest(APITestCase):
fixtures = ['dcim', 'ipam']
standard_fields = ['id', 'name', 'slug', 'color']
nested_fields = ['id', 'name', 'slug']
def test_get_list(self, endpoint='/{}api/dcim/device-roles/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
for i in content:
self.assertEqual(
sorted(i.keys()),
sorted(self.standard_fields),
)
def test_get_detail(self, endpoint='/{}api/dcim/device-roles/1/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
sorted(content.keys()),
sorted(self.standard_fields),
)
class PlatformsTest(APITestCase):
fixtures = ['dcim', 'ipam']
standard_fields = ['id', 'name', 'slug', 'rpc_client']
nested_fields = ['id', 'name', 'slug']
def test_get_list(self, endpoint='/{}api/dcim/platforms/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
for i in content:
self.assertEqual(
sorted(i.keys()),
sorted(self.standard_fields),
)
def test_get_detail(self, endpoint='/{}api/dcim/platforms/1/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
sorted(content.keys()),
sorted(self.standard_fields),
)
class DeviceTest(APITestCase):
fixtures = ['dcim', 'ipam']
standard_fields = [
'id',
'name',
'display_name',
'device_type',
'device_role',
'tenant',
'platform',
'serial',
'asset_tag',
'site',
'rack',
'position',
'face',
'parent_device',
'status',
'primary_ip',
'primary_ip4',
'primary_ip6',
'comments',
'custom_fields',
]
nested_fields = ['id', 'name', 'display_name']
def test_get_list(self, endpoint='/{}api/dcim/devices/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
for device in content:
self.assertEqual(
sorted(device.keys()),
sorted(self.standard_fields),
)
self.assertEqual(
sorted(device.get('device_type')),
sorted(DeviceTypeTest.nested_fields),
)
self.assertEqual(
sorted(device.get('device_role')),
sorted(DeviceRolesTest.nested_fields),
)
if device.get('platform'):
self.assertEqual(
sorted(device.get('platform')),
sorted(PlatformsTest.nested_fields),
)
self.assertEqual(
sorted(device.get('rack')),
sorted(RackTest.nested_fields),
)
def test_get_list_flat(self, endpoint='/{}api/dcim/devices/?format=json_flat'.format(settings.BASE_PATH)):
flat_fields = [
'asset_tag',
'comments',
'device_role_id',
'device_role_name',
'device_role_slug',
'device_type_id',
'device_type_manufacturer_id',
'device_type_manufacturer_name',
'device_type_manufacturer_slug',
'device_type_model',
'device_type_slug',
'display_name',
'face',
'id',
'name',
'parent_device',
'platform_id',
'platform_name',
'platform_slug',
'position',
'primary_ip_address',
'primary_ip_family',
'primary_ip_id',
'primary_ip4_address',
'primary_ip4_family',
'primary_ip4_id',
'primary_ip6',
'site_id',
'site_name',
'site_slug',
'rack_display_name',
'rack_facility_id',
'rack_id',
'rack_name',
'serial',
'status',
'tenant',
]
response = self.client.get(endpoint)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
device = content[0]
self.assertEqual(
sorted(device.keys()),
sorted(flat_fields),
)
def test_get_detail(self, endpoint='/{}api/dcim/devices/1/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
sorted(content.keys()),
sorted(self.standard_fields),
)
class ConsoleServerPortsTest(APITestCase):
fixtures = ['dcim', 'ipam']
standard_fields = ['id', 'device', 'name', 'connected_console']
nested_fields = ['id', 'device', 'name']
def test_get_list(self, endpoint='/{}api/dcim/devices/9/console-server-ports/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
for console_port in content:
self.assertEqual(
sorted(console_port.keys()),
sorted(self.standard_fields),
)
self.assertEqual(
sorted(console_port.get('device')),
sorted(DeviceTest.nested_fields),
)
class ConsolePortsTest(APITestCase):
fixtures = ['dcim', 'ipam']
standard_fields = ['id', 'device', 'name', 'cs_port', 'connection_status']
nested_fields = ['id', 'device', 'name']
def test_get_list(self, endpoint='/{}api/dcim/devices/1/console-ports/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
for console_port in content:
self.assertEqual(
sorted(console_port.keys()),
sorted(self.standard_fields),
)
self.assertEqual(
sorted(console_port.get('device')),
sorted(DeviceTest.nested_fields),
)
self.assertEqual(
sorted(console_port.get('cs_port')),
sorted(ConsoleServerPortsTest.nested_fields),
)
def test_get_detail(self, endpoint='/{}api/dcim/console-ports/1/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
sorted(content.keys()),
sorted(self.standard_fields),
)
self.assertEqual(
sorted(content.get('device')),
sorted(DeviceTest.nested_fields),
)
class PowerPortsTest(APITestCase):
fixtures = ['dcim', 'ipam']
standard_fields = ['id', 'device', 'name', 'power_outlet', 'connection_status']
nested_fields = ['id', 'device', 'name']
def test_get_list(self, endpoint='/{}api/dcim/devices/1/power-ports/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
for i in content:
self.assertEqual(
sorted(i.keys()),
sorted(self.standard_fields),
)
self.assertEqual(
sorted(i.get('device')),
sorted(DeviceTest.nested_fields),
)
def test_get_detail(self, endpoint='/{}api/dcim/power-ports/1/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
sorted(content.keys()),
sorted(self.standard_fields),
)
self.assertEqual(
sorted(content.get('device')),
sorted(DeviceTest.nested_fields),
)
class PowerOutletsTest(APITestCase):
fixtures = ['dcim', 'ipam']
standard_fields = ['id', 'device', 'name', 'connected_port']
nested_fields = ['id', 'device', 'name']
def test_get_list(self, endpoint='/{}api/dcim/devices/11/power-outlets/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
for i in content:
self.assertEqual(
sorted(i.keys()),
sorted(self.standard_fields),
)
self.assertEqual(
sorted(i.get('device')),
sorted(DeviceTest.nested_fields),
)
class InterfaceTest(APITestCase):
fixtures = ['dcim', 'ipam', 'extras']
standard_fields = [
'id',
'device',
'name',
'form_factor',
'lag',
'mac_address',
'mgmt_only',
'description',
'is_connected'
]
nested_fields = ['id', 'device', 'name']
detail_fields = [
'id',
'device',
'name',
'form_factor',
'lag',
'mac_address',
'mgmt_only',
'description',
'is_connected',
'connected_interface'
]
connection_fields = [
'id',
'interface_a',
'interface_b',
'connection_status',
]
def test_get_list(self, endpoint='/{}api/dcim/devices/1/interfaces/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
for i in content:
self.assertEqual(
sorted(i.keys()),
sorted(self.standard_fields),
)
self.assertEqual(
sorted(i.get('device')),
sorted(DeviceTest.nested_fields),
)
def test_get_detail(self, endpoint='/{}api/dcim/interfaces/1/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
sorted(content.keys()),
sorted(self.detail_fields),
)
self.assertEqual(
sorted(content.get('device')),
sorted(DeviceTest.nested_fields),
)
def test_get_graph_list(self, endpoint='/{}api/dcim/interfaces/1/graphs/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
for i in content:
self.assertEqual(
sorted(i.keys()),
sorted(SiteTest.graph_fields),
)
def test_get_interface_connections(self, endpoint='/{}api/dcim/interface-connections/4/'
.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
sorted(content.keys()),
sorted(self.connection_fields),
)
class RelatedConnectionsTest(APITestCase):
fixtures = ['dcim', 'ipam']
standard_fields = [
'device',
'console-ports',
'power-ports',
'interfaces',
]
def test_get_list(self, endpoint=('/{}api/dcim/related-connections/?peer-device=test1-edge1&peer-interface=xe-0/0/3'
.format(settings.BASE_PATH))):
response = self.client.get(endpoint)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
sorted(content.keys()),
sorted(self.standard_fields),
)

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 *
@@ -95,3 +98,112 @@ class RackTestCase(TestCase):
face=None,
)
self.assertTrue(pdu)
class InterfaceTestCase(TestCase):
def setUp(self):
self.site = Site.objects.create(
name='TestSite1',
slug='my-test-site'
)
self.rack = Rack.objects.create(
name='TestRack1',
facility_id='A101',
site=self.site,
u_height=42
)
self.manufacturer = Manufacturer.objects.create(
name='Acme',
slug='acme'
)
self.device_type = DeviceType.objects.create(
manufacturer=self.manufacturer,
model='FrameForwarder 2048',
slug='ff2048'
)
self.role = DeviceRole.objects.create(
name='Switch',
slug='switch',
)
def test_interface_order_natural(self):
device1 = Device.objects.create(
name='TestSwitch1',
device_type=self.device_type,
device_role=self.role,
site=self.site,
rack=self.rack,
position=10,
face=RACK_FACE_REAR,
)
interface1 = Interface.objects.create(
device=device1,
name='Ethernet1/3/1'
)
interface2 = Interface.objects.create(
device=device1,
name='Ethernet1/5/1'
)
interface3 = Interface.objects.create(
device=device1,
name='Ethernet1/4'
)
interface4 = Interface.objects.create(
device=device1,
name='Ethernet1/3/2/4'
)
interface5 = Interface.objects.create(
device=device1,
name='Ethernet1/3/2/1'
)
interface6 = Interface.objects.create(
device=device1,
name='Loopback1'
)
self.assertEqual(
list(Interface.objects.all().order_naturally()),
[interface1, interface5, interface4, interface3, interface2, interface6]
)
def test_interface_order_natural_subinterfaces(self):
device1 = Device.objects.create(
name='TestSwitch1',
device_type=self.device_type,
device_role=self.role,
site=self.site,
rack=self.rack,
position=10,
face=RACK_FACE_REAR,
)
interface1 = Interface.objects.create(
device=device1,
name='GigabitEthernet0/0/3'
)
interface2 = Interface.objects.create(
device=device1,
name='GigabitEthernet0/0/2.2'
)
interface3 = Interface.objects.create(
device=device1,
name='GigabitEthernet0/0/0.120'
)
interface4 = Interface.objects.create(
device=device1,
name='GigabitEthernet0/0/0'
)
interface5 = Interface.objects.create(
device=device1,
name='GigabitEthernet0/0/1.117'
)
interface6 = Interface.objects.create(
device=device1,
name='GigabitEthernet0'
)
self.assertEqual(
list(Interface.objects.all().order_naturally()),
[interface4, interface3, interface5, interface2, interface1, interface6]
)

View File

@@ -1,37 +1,45 @@
from __future__ import unicode_literals
from django.conf.urls import url
from ipam.views import ServiceEditView
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
app_name = 'dcim'
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/import/$', views.RegionBulkImportView.as_view(), name='region_import'),
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/import/$', views.RackGroupBulkImportView.as_view(), name='rackgroup_import'),
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/import/$', views.RackRoleBulkImportView.as_view(), name='rackrole_import'),
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'),
@@ -43,85 +51,94 @@ urlpatterns = [
# Racks
url(r'^racks/$', views.RackListView.as_view(), name='rack_list'),
url(r'^rack-elevations/$', views.RackElevationListView.as_view(), name='rack_elevation_list'),
url(r'^racks/add/$', views.RackEditView.as_view(), name='rack_add'),
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/import/$', views.ManufacturerBulkImportView.as_view(), name='manufacturer_import'),
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/import/$', views.DeviceTypeBulkImportView.as_view(), name='devicetype_import'),
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/import/$', views.DeviceRoleBulkImportView.as_view(), name='devicerole_import'),
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/import/$', views.PlatformBulkImportView.as_view(), name='platform_import'),
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='device_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'),
@@ -130,7 +147,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'),
@@ -139,7 +157,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'),
@@ -148,7 +166,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'),
@@ -157,8 +176,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'),
@@ -167,13 +187,18 @@ 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'),
url(r'^device-bays/(?P<pk>\d+)/populate/$', views.devicebay_populate, name='devicebay_populate'),
url(r'^device-bays/(?P<pk>\d+)/depopulate/$', views.devicebay_depopulate, name='devicebay_depopulate'),
# Inventory items
url(r'^devices/(?P<device>\d+)/inventory-items/add/$', views.InventoryItemEditView.as_view(), name='inventoryitem_add'),
url(r'^inventory-items/(?P<pk>\d+)/edit/$', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'),
url(r'^inventory-items/(?P<pk>\d+)/delete/$', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'),
# Console/power/interface connections
url(r'^console-connections/$', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'),
url(r'^console-connections/import/$', views.ConsoleConnectionsBulkImportView.as_view(), name='console_connections_import'),
@@ -182,9 +207,4 @@ urlpatterns = [
url(r'^interface-connections/$', views.InterfaceConnectionsListView.as_view(), name='interface_connections_list'),
url(r'^interface-connections/import/$', views.InterfaceConnectionsBulkImportView.as_view(), name='interface_connections_import'),
# Modules
url(r'^devices/(?P<device>\d+)/modules/add/$', views.ModuleEditView.as_view(), name='module_add'),
url(r'^modules/(?P<pk>\d+)/edit/$', views.ModuleEditView.as_view(), name='module_edit'),
url(r'^modules/(?P<pk>\d+)/delete/$', views.ModuleDeleteView.as_view(), name='module_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
@@ -5,6 +7,18 @@ from django.utils.safestring import mark_safe
from .models import CustomField, CustomFieldChoice, Graph, ExportTemplate, TopologyMap, UserAction
def order_content_types(field):
"""
Order the list of available ContentTypes by application
"""
queryset = field.queryset.order_by('app_label', 'model')
field.choices = [(ct.pk, '{} > {}'.format(ct.app_label, ct.name)) for ct in queryset]
#
# Custom fields
#
class CustomFieldForm(forms.ModelForm):
class Meta:
@@ -14,9 +28,7 @@ class CustomFieldForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(CustomFieldForm, self).__init__(*args, **kwargs)
# Organize the available ContentTypes
queryset = self.fields['obj_type'].queryset.order_by('app_label', 'model')
self.fields['obj_type'].choices = [(ct.pk, '{} > {}'.format(ct.app_label, ct.name)) for ct in queryset]
order_content_types(self.fields['obj_type'])
class CustomFieldChoiceAdmin(admin.TabularInline):
@@ -34,16 +46,43 @@ class CustomFieldAdmin(admin.ModelAdmin):
return ', '.join([ct.name for ct in obj.obj_type.all()])
#
# Graphs
#
@admin.register(Graph)
class GraphAdmin(admin.ModelAdmin):
list_display = ['name', 'type', 'weight', 'source']
#
# Export templates
#
class ExportTemplateForm(forms.ModelForm):
class Meta:
model = ExportTemplate
exclude = []
def __init__(self, *args, **kwargs):
super(ExportTemplateForm, self).__init__(*args, **kwargs)
# Format ContentType choices
order_content_types(self.fields['content_type'])
self.fields['content_type'].choices.insert(0, ('', '---------'))
@admin.register(ExportTemplate)
class ExportTemplateAdmin(admin.ModelAdmin):
list_display = ['name', 'content_type', 'description', 'mime_type', 'file_extension']
form = ExportTemplateForm
#
# Topology maps
#
@admin.register(TopologyMap)
class TopologyMapAdmin(admin.ModelAdmin):
list_display = ['name', 'slug', 'site']
@@ -52,6 +91,10 @@ class TopologyMapAdmin(admin.ModelAdmin):
}
#
# User actions
#
@admin.register(UserAction)
class UserActionAdmin(admin.ModelAdmin):
actions = None

View File

@@ -0,0 +1,168 @@
from __future__ import unicode_literals
from datetime import datetime
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
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,
)
from utilities.api import ValidatedModelSerializer
#
# Custom fields
#
class CustomFieldsSerializer(serializers.BaseSerializer):
def to_representation(self, obj):
return obj
def to_internal_value(self, data):
content_type = ContentType.objects.get_for_model(self.parent.Meta.model)
custom_fields = {field.name: field for field in CustomField.objects.filter(obj_type=content_type)}
for field_name, value in data.items():
try:
cf = custom_fields[field_name]
except KeyError:
raise ValidationError(
"Invalid custom field for {} objects: {}".format(content_type, field_name)
)
# Data validation
if value not in [None, '']:
# 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
if cf.type == CF_TYPE_SELECT:
try:
value = int(value)
except ValueError:
raise ValidationError(
"{}: Choice selections must be passed as integers.".format(field_name)
)
valid_choices = [c.pk for c in cf.choices.all()]
if value not in valid_choices:
raise ValidationError(
"Invalid choice for field {}: {}".format(field_name, value)
)
elif cf.required:
raise ValidationError("Required field {} cannot be empty.".format(field_name))
# Check for missing required fields
missing_fields = []
for field_name, field in custom_fields.items():
if field.required and field_name not in data:
missing_fields.append(field_name)
if missing_fields:
raise ValidationError("Missing required fields: {}".format(u", ".join(missing_fields)))
return data
class CustomFieldModelSerializer(ValidatedModelSerializer):
"""
Extends ModelSerializer to render any CustomFields and their values associated with an object.
"""
custom_fields = CustomFieldsSerializer(required=False)
def __init__(self, *args, **kwargs):
def _populate_custom_fields(instance, fields):
custom_fields = {f.name: None for f in fields}
for cfv in instance.custom_field_values.all():
if cfv.field.type == CF_TYPE_SELECT:
custom_fields[cfv.field.name] = CustomFieldChoiceSerializer(cfv.value).data
else:
custom_fields[cfv.field.name] = cfv.value
instance.custom_fields = custom_fields
super(CustomFieldModelSerializer, self).__init__(*args, **kwargs)
if self.instance is not None:
# Retrieve the set of CustomFields which apply to this type of object
content_type = ContentType.objects.get_for_model(self.Meta.model)
fields = CustomField.objects.filter(obj_type=content_type)
# Populate CustomFieldValues for each instance from database
try:
for obj in self.instance:
_populate_custom_fields(obj, fields)
except TypeError:
_populate_custom_fields(self.instance, fields)
def _save_custom_fields(self, instance, custom_fields):
content_type = ContentType.objects.get_for_model(self.Meta.model)
for field_name, value in custom_fields.items():
custom_field = CustomField.objects.get(name=field_name)
CustomFieldValue.objects.update_or_create(
field=custom_field,
obj_type=content_type,
obj_id=instance.pk,
defaults={'serialized_value': custom_field.serialize_value(value)},
)
def create(self, validated_data):
custom_fields = validated_data.pop('custom_fields', None)
with transaction.atomic():
instance = super(CustomFieldModelSerializer, self).create(validated_data)
# Save custom fields
if custom_fields is not None:
self._save_custom_fields(instance, custom_fields)
instance.custom_fields = custom_fields
return instance
def update(self, instance, validated_data):
custom_fields = validated_data.pop('custom_fields', None)
with transaction.atomic():
instance = super(CustomFieldModelSerializer, self).update(instance, validated_data)
# Save custom fields
if custom_fields is not None:
self._save_custom_fields(instance, custom_fields)
instance.custom_fields = custom_fields
return instance
class CustomFieldChoiceSerializer(serializers.ModelSerializer):
"""
Imitate utilities.api.ChoiceFieldSerializer
"""
value = serializers.IntegerField(source='pk')
label = serializers.CharField(source='value')
class Meta:
model = CustomFieldChoice
fields = ['value', 'label']

View File

@@ -1,88 +0,0 @@
import json
from rest_framework import renderers
# IP address family designations
AF = {
4: 'A',
6: 'AAAA',
}
class FormlessBrowsableAPIRenderer(renderers.BrowsableAPIRenderer):
"""
An instance of the browseable API with forms suppressed. Useful for POST endpoints that don't create objects.
"""
def show_form_for_method(self, *args, **kwargs):
return False
class BINDZoneRenderer(renderers.BaseRenderer):
"""
Generate a BIND zone file from a list of DNS records.
Required fields: `name`, `primary_ip`
"""
media_type = 'text/plain'
format = 'bind-zone'
def render(self, data, media_type=None, renderer_context=None):
records = []
for record in data:
if record.get('name') and record.get('primary_ip'):
try:
records.append("{} IN {} {}".format(
record['name'],
AF[record['primary_ip']['family']],
record['primary_ip']['address'].split('/')[0],
))
except KeyError:
pass
return '\n'.join(records)
class FlatJSONRenderer(renderers.BaseRenderer):
"""
Flattens a nested JSON response.
"""
format = 'json_flat'
media_type = 'application/json'
def render(self, data, media_type=None, renderer_context=None):
def flatten(entry):
for key, val in entry.items():
if isinstance(val, dict):
for child_key, child_val in flatten(val):
yield "{}_{}".format(key, child_key), child_val
else:
yield key, val
return json.dumps([dict(flatten(i)) for i in data])
class FreeRADIUSClientsRenderer(renderers.BaseRenderer):
"""
Generate a FreeRADIUS clients.conf file from a list of Secrets.
"""
media_type = 'text/plain'
format = 'freeradius'
CLIENT_TEMPLATE = """client {name} {{
ipaddr = {ip}
secret = {secret}
}}"""
def render(self, data, media_type=None, renderer_context=None):
clients = []
try:
for secret in data:
if secret['device']['primary_ip'] and secret['plaintext']:
client = self.CLIENT_TEMPLATE.format(
name=secret['device']['name'],
ip=secret['device']['primary_ip']['address'].split('/')[0],
secret=secret['plaintext']
)
clients.append(client)
except:
pass
return '\n'.join(clients)

View File

@@ -1,56 +1,175 @@
from __future__ import unicode_literals
from django.core.exceptions import ObjectDoesNotExist
from rest_framework import serializers
from extras.models import CF_TYPE_SELECT, CustomFieldChoice, Graph
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, ReportResult, TopologyMap, UserAction,
)
from users.api.serializers import NestedUserSerializer
from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer, ValidatedModelSerializer
class CustomFieldSerializer(serializers.Serializer):
"""
Extends a ModelSerializer to render any CustomFields and their values associated with an object.
"""
custom_fields = serializers.SerializerMethodField()
def get_custom_fields(self, obj):
# Gather all CustomFields applicable to this object
fields = {cf.name: None for cf in self.context['view'].custom_fields}
# Attach any defined CustomFieldValues to their respective CustomFields
for cfv in obj.custom_field_values.all():
# Attempt to suppress database lookups for CustomFieldChoices by using the cached choice set from the view
# context.
if cfv.field.type == CF_TYPE_SELECT and hasattr(self, 'custom_field_choices'):
cfc = {
'id': int(cfv.serialized_value),
'value': self.context['view'].custom_field_choices[int(cfv.serialized_value)]
}
fields[cfv.field.name] = CustomFieldChoiceSerializer(instance=cfc).data
# Fall back to hitting the database in case we're in a view that doesn't inherit CustomFieldModelAPIView.
elif cfv.field.type == CF_TYPE_SELECT:
fields[cfv.field.name] = CustomFieldChoiceSerializer(instance=cfv.value).data
else:
fields[cfv.field.name] = cfv.value
return fields
class CustomFieldChoiceSerializer(serializers.ModelSerializer):
class Meta:
model = CustomFieldChoice
fields = ['id', 'value']
#
# Graphs
#
class GraphSerializer(serializers.ModelSerializer):
embed_url = serializers.SerializerMethodField()
embed_link = serializers.SerializerMethodField()
type = ChoiceFieldSerializer(choices=GRAPH_TYPE_CHOICES)
class Meta:
model = Graph
fields = ['name', 'embed_url', 'embed_link']
fields = ['id', 'type', 'weight', 'name', 'source', 'link']
class WritableGraphSerializer(serializers.ModelSerializer):
class Meta:
model = Graph
fields = ['id', 'type', 'weight', 'name', 'source', 'link']
class RenderedGraphSerializer(serializers.ModelSerializer):
embed_url = serializers.SerializerMethodField()
embed_link = serializers.SerializerMethodField()
type = ChoiceFieldSerializer(choices=GRAPH_TYPE_CHOICES)
class Meta:
model = Graph
fields = ['id', 'type', 'weight', 'name', 'embed_url', 'embed_link']
def get_embed_url(self, obj):
return obj.embed_url(self.context['graphed_object'])
def get_embed_link(self, obj):
return obj.embed_link(self.context['graphed_object'])
#
# Export templates
#
class ExportTemplateSerializer(serializers.ModelSerializer):
class Meta:
model = ExportTemplate
fields = ['id', 'content_type', 'name', 'description', 'template_code', 'mime_type', 'file_extension']
#
# Topology maps
#
class TopologyMapSerializer(serializers.ModelSerializer):
site = NestedSiteSerializer()
class Meta:
model = TopologyMap
fields = ['id', 'name', 'slug', 'site', 'device_patterns', 'description']
class WritableTopologyMapSerializer(serializers.ModelSerializer):
class Meta:
model = TopologyMap
fields = ['id', 'name', 'slug', 'site', 'device_patterns', 'description']
#
# Image attachments
#
class ImageAttachmentSerializer(serializers.ModelSerializer):
parent = serializers.SerializerMethodField()
class Meta:
model = ImageAttachment
fields = ['id', 'parent', 'name', 'image', 'image_height', 'image_width', 'created']
def get_parent(self, obj):
# Static mapping of models to their nested serializers
if isinstance(obj.parent, Device):
serializer = NestedDeviceSerializer
elif isinstance(obj.parent, Rack):
serializer = NestedRackSerializer
elif isinstance(obj.parent, Site):
serializer = NestedSiteSerializer
else:
raise Exception("Unexpected type of parent object for ImageAttachment")
return serializer(obj.parent, context={'request': self.context['request']}).data
class WritableImageAttachmentSerializer(ValidatedModelSerializer):
content_type = ContentTypeFieldSerializer()
class Meta:
model = ImageAttachment
fields = ['id', 'content_type', 'object_id', 'name', 'image']
def validate(self, data):
# Validate that the parent object exists
try:
data['content_type'].get_object_for_this_type(id=data['object_id'])
except ObjectDoesNotExist:
raise serializers.ValidationError(
"Invalid parent object: {} ID {}".format(data['content_type'], data['object_id'])
)
# Enforce model validation
super(WritableImageAttachmentSerializer, self).validate(data)
return data
#
# Reports
#
class ReportResultSerializer(serializers.ModelSerializer):
class Meta:
model = ReportResult
fields = ['created', 'user', 'failed', 'data']
class NestedReportResultSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='extras-api:report-detail',
lookup_field='report',
lookup_url_kwarg='pk'
)
class Meta:
model = ReportResult
fields = ['url', 'created', 'user', 'failed']
class ReportSerializer(serializers.Serializer):
module = serializers.CharField(max_length=255)
name = serializers.CharField(max_length=255)
description = serializers.CharField(max_length=255, required=False)
test_methods = serializers.ListField(child=serializers.CharField(max_length=255))
result = NestedReportResultSerializer()
class ReportDetailSerializer(ReportSerializer):
result = ReportResultSerializer()
#
# User actions
#
class UserActionSerializer(serializers.ModelSerializer):
user = NestedUserSerializer()
action = ChoiceFieldSerializer(choices=ACTION_CHOICES)
class Meta:
model = UserAction
fields = ['id', 'time', 'user', 'action', 'message']

41
netbox/extras/api/urls.py Normal file
View File

@@ -0,0 +1,41 @@
from __future__ import unicode_literals
from rest_framework import routers
from . import views
class ExtrasRootView(routers.APIRootView):
"""
Extras API root view
"""
def get_view_name(self):
return 'Extras'
router = routers.DefaultRouter()
router.APIRootView = ExtrasRootView
# Field choices
router.register(r'_choices', views.ExtrasFieldChoicesViewSet, base_name='field-choice')
# Graphs
router.register(r'graphs', views.GraphViewSet)
# Export templates
router.register(r'export-templates', views.ExportTemplateViewSet)
# Topology maps
router.register(r'topology-maps', views.TopologyMapViewSet)
# Image attachments
router.register(r'image-attachments', views.ImageAttachmentViewSet)
# Reports
router.register(r'reports', views.ReportViewSet, base_name='report')
# Recent activity
router.register(r'recent-activity', views.RecentActivityViewSet)
app_name = 'extras-api'
urlpatterns = router.urls

View File

@@ -1,115 +1,211 @@
import graphviz
from rest_framework import generics
from rest_framework.views import APIView
from __future__ import unicode_literals
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, ReadOnlyModelViewSet, ViewSet
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404
from circuits.models import Provider
from dcim.models import Site, Device, Interface, InterfaceConnection
from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_PROVIDER, GRAPH_TYPE_SITE
from .serializers import GraphSerializer
from extras import filters
from extras.models import CustomField, ExportTemplate, Graph, ImageAttachment, ReportResult, TopologyMap, UserAction
from extras.reports import get_report, get_reports
from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, WritableSerializerMixin
from . import serializers
class CustomFieldModelAPIView(object):
#
# Field choices
#
class ExtrasFieldChoicesViewSet(FieldChoicesViewSet):
fields = (
(CustomField, ['type']),
(Graph, ['type']),
)
#
# Custom fields
#
class CustomFieldModelViewSet(ModelViewSet):
"""
Include the applicable set of CustomField in the view context.
Include the applicable set of CustomFields in the ModelViewSet context.
"""
def __init__(self):
super(CustomFieldModelAPIView, self).__init__()
self.content_type = ContentType.objects.get_for_model(self.queryset.model)
self.custom_fields = self.content_type.custom_fields.prefetch_related('choices')
def get_serializer_context(self):
# Gather all custom fields for the model
content_type = ContentType.objects.get_for_model(self.queryset.model)
custom_fields = content_type.custom_fields.prefetch_related('choices')
# Cache all relevant CustomFieldChoices. This saves us from having to do a lookup per select field per object.
custom_field_choices = {}
for field in self.custom_fields:
for field in custom_fields:
for cfc in field.choices.all():
custom_field_choices[cfc.id] = cfc.value
self.custom_field_choices = custom_field_choices
custom_field_choices = custom_field_choices
class GraphListView(generics.ListAPIView):
"""
Returns a list of relevant graphs
"""
serializer_class = GraphSerializer
def get_serializer_context(self):
cls = {
GRAPH_TYPE_INTERFACE: Interface,
GRAPH_TYPE_PROVIDER: Provider,
GRAPH_TYPE_SITE: Site,
}
context = super(GraphListView, self).get_serializer_context()
context.update({'graphed_object': get_object_or_404(cls[self.kwargs.get('type')], pk=self.kwargs['pk'])})
context = super(CustomFieldModelViewSet, self).get_serializer_context()
context.update({
'custom_fields': custom_fields,
'custom_field_choices': custom_field_choices,
})
return context
def get_queryset(self):
graph_type = self.kwargs.get('type', None)
if not graph_type:
raise Http404()
queryset = Graph.objects.filter(type=graph_type)
return queryset
# Prefetch custom field values
return super(CustomFieldModelViewSet, self).get_queryset().prefetch_related('custom_field_values__field')
class TopologyMapView(APIView):
"""
Generate a topology diagram
"""
#
# Graphs
#
def get(self, request, slug):
class GraphViewSet(WritableSerializerMixin, ModelViewSet):
queryset = Graph.objects.all()
serializer_class = serializers.GraphSerializer
write_serializer_class = serializers.WritableGraphSerializer
filter_class = filters.GraphFilter
tmap = get_object_or_404(TopologyMap, slug=slug)
# Construct the graph
graph = graphviz.Graph()
graph.graph_attr['ranksep'] = '1'
for i, device_set in enumerate(tmap.device_sets):
#
# Export templates
#
subgraph = graphviz.Graph(name='sg{}'.format(i))
subgraph.graph_attr['rank'] = 'same'
class ExportTemplateViewSet(WritableSerializerMixin, ModelViewSet):
queryset = ExportTemplate.objects.all()
serializer_class = serializers.ExportTemplateSerializer
filter_class = filters.ExportTemplateFilter
# Add a pseudonode for each device_set to enforce hierarchical layout
subgraph.node('set{}'.format(i), label='', shape='none', width='0')
if i:
graph.edge('set{}'.format(i - 1), 'set{}'.format(i), style='invis')
# Add each device to the graph
devices = []
for query in device_set.split(';'): # Split regexes on semicolons
devices += Device.objects.filter(name__regex=query)
for d in devices:
subgraph.node(d.name)
#
# Topology maps
#
# Add an invisible connection to each successive device in a set to enforce horizontal order
for j in range(0, len(devices) - 1):
subgraph.edge(devices[j].name, devices[j + 1].name, style='invis')
class TopologyMapViewSet(WritableSerializerMixin, ModelViewSet):
queryset = TopologyMap.objects.select_related('site')
serializer_class = serializers.TopologyMapSerializer
write_serializer_class = serializers.WritableTopologyMapSerializer
filter_class = filters.TopologyMapFilter
graph.subgraph(subgraph)
@detail_route()
def render(self, request, pk):
# Compile list of all devices
device_superset = Q()
for device_set in tmap.device_sets:
for query in device_set.split(';'): # Split regexes on semicolons
device_superset = device_superset | Q(name__regex=query)
tmap = get_object_or_404(TopologyMap, pk=pk)
img_format = 'png'
# Add all connections to the graph
devices = Device.objects.filter(*(device_superset,))
connections = InterfaceConnection.objects.filter(interface_a__device__in=devices,
interface_b__device__in=devices)
for c in connections:
graph.edge(c.interface_a.device.name, c.interface_b.device.name)
# Get the image data and return
try:
topo_data = graph.pipe(format='png')
data = tmap.render(img_format=img_format)
except:
return HttpResponse("There was an error generating the requested graph. Ensure that the GraphViz "
"executables have been installed correctly.")
response = HttpResponse(topo_data, content_type='image/png')
return HttpResponse(
"There was an error generating the requested graph. Ensure that the GraphViz executables have been "
"installed correctly."
)
response = HttpResponse(data, content_type='image/{}'.format(img_format))
response['Content-Disposition'] = 'inline; filename="{}.{}"'.format(tmap.slug, img_format)
return response
#
# Image attachments
#
class ImageAttachmentViewSet(WritableSerializerMixin, ModelViewSet):
queryset = ImageAttachment.objects.all()
serializer_class = serializers.ImageAttachmentSerializer
write_serializer_class = serializers.WritableImageAttachmentSerializer
#
# Reports
#
class ReportViewSet(ViewSet):
permission_classes = [IsAuthenticatedOrLoginNotRequired]
_ignore_model_permissions = True
exclude_from_schema = True
lookup_value_regex = '[^/]+' # Allow dots
def _retrieve_report(self, pk):
# Read the PK as "<module>.<report>"
if '.' not in pk:
raise Http404
module_name, report_name = pk.split('.', 1)
# Raise a 404 on an invalid Report module/name
report = get_report(module_name, report_name)
if report is None:
raise Http404
return report
def list(self, request):
"""
Compile all reports and their related results (if any). Result data is deferred in the list view.
"""
report_list = []
# Iterate through all available Reports.
for module_name, reports in get_reports():
for report in reports:
# Attach the relevant ReportResult (if any) to each Report.
report.result = ReportResult.objects.filter(report=report.full_name).defer('data').first()
report_list.append(report)
serializer = serializers.ReportSerializer(report_list, many=True, context={
'request': request,
})
return Response(serializer.data)
def retrieve(self, request, pk):
"""
Retrieve a single Report identified as "<module>.<report>".
"""
# Retrieve the Report and ReportResult, if any.
report = self._retrieve_report(pk)
report.result = ReportResult.objects.filter(report=report.full_name).first()
serializer = serializers.ReportDetailSerializer(report)
return Response(serializer.data)
@detail_route(methods=['post'])
def run(self, request, pk):
"""
Run a Report and create a new ReportResult, overwriting any previous result for the Report.
"""
# Check that the user has permission to run reports.
if not request.user.has_perm('extras.add_reportresult'):
raise PermissionDenied("This user does not have permission to run reports.")
# Retrieve and run the Report. This will create a new ReportResult.
report = self._retrieve_report(pk)
report.run()
serializer = serializers.ReportDetailSerializer(report)
return Response(serializer.data)
#
# User activity
#
class RecentActivityViewSet(ReadOnlyModelViewSet):
"""
List all UserActions to provide a log of recent activity.
"""
queryset = UserAction.objects.all()
serializer_class = serializers.UserActionSerializer
filter_class = filters.UserActionFilter

View File

@@ -0,0 +1,79 @@
from __future__ import unicode_literals
# Models which support custom fields
CUSTOMFIELD_MODELS = (
'provider', 'circuit', # Circuits
'site', 'rack', 'devicetype', 'device', # DCIM
'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', # IPAM
'tenant', # Tenancy
'cluster', 'virtualmachine', # Virtualization
)
# 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 = [
'provider', 'circuit', # Circuits
'site', 'region', 'rack', 'rackgroup', 'manufacturer', 'devicetype', 'device', # DCIM
'consoleport', 'powerport', 'interfaceconnection', # DCIM
'aggregate', 'prefix', 'ipaddress', 'vlan', # IPAM
'tenant', # Tenancy
'cluster', 'virtualmachine', # Virtualization
]
# 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'),
)
# Report logging levels
LOG_DEFAULT = 0
LOG_SUCCESS = 10
LOG_INFO = 20
LOG_WARNING = 30
LOG_FAILURE = 40
LOG_LEVEL_CODES = {
LOG_DEFAULT: 'default',
LOG_SUCCESS: 'success',
LOG_INFO: 'info',
LOG_WARNING: 'warning',
LOG_FAILURE: 'failure',
}

View File

@@ -1,8 +1,12 @@
from __future__ import unicode_literals
import django_filters
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from .models import CF_TYPE_SELECT, CustomField
from dcim.models import Site
from .models import CF_TYPE_SELECT, CustomField, Graph, ExportTemplate, TopologyMap, UserAction
class CustomFieldFilter(django_filters.Filter):
@@ -28,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,
)
@@ -44,3 +48,47 @@ class CustomFieldFilterSet(django_filters.FilterSet):
custom_fields = CustomField.objects.filter(obj_type=obj_type, is_filterable=True)
for cf in custom_fields:
self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(name=cf.name, cf_type=cf.type)
class GraphFilter(django_filters.FilterSet):
class Meta:
model = Graph
fields = ['type', 'name']
class ExportTemplateFilter(django_filters.FilterSet):
class Meta:
model = ExportTemplate
fields = ['content_type', 'name']
class TopologyMapFilter(django_filters.FilterSet):
site_id = django_filters.ModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(),
label='Site',
)
site = django_filters.ModelMultipleChoiceFilter(
name='site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
)
class Meta:
model = TopologyMap
fields = ['name', 'slug']
class UserActionFilter(django_filters.FilterSet):
username = django_filters.ModelMultipleChoiceFilter(
name='user__username',
queryset=User.objects.all(),
to_field_name='username',
)
class Meta:
model = UserAction
fields = ['user']

View File

@@ -1,11 +1,13 @@
from __future__ import unicode_literals
from collections import OrderedDict
from django import forms
from django.contrib.contenttypes.models import ContentType
from utilities.forms import BulkEditForm, LaxURLField
from utilities.forms import BootstrapMixin, BulkEditForm, LaxURLField
from .models import (
CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL, CustomField, CustomFieldValue
CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL, CustomField, CustomFieldValue,
ImageAttachment,
)
@@ -103,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,
@@ -158,3 +160,10 @@ class CustomFieldFilterForm(forms.Form):
for name, field in custom_fields:
field.required = False
self.fields[name] = field
class ImageAttachmentForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = ImageAttachment
fields = ['name', 'image']

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', 'virtualization']
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
@@ -6,13 +8,13 @@ from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from dcim.models import Device, Module, Site
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")
@@ -25,12 +27,12 @@ class Command(BaseCommand):
def handle(self, *args, **options):
def create_modules(modules, parent=None):
for module in modules:
m = Module(device=device, parent=parent, name=module['name'], part_id=module['part_id'],
serial=module['serial'], discovered=True)
m.save()
create_modules(module.get('modules', []), parent=m)
def create_inventory_items(inventory_items, parent=None):
for item in inventory_items:
i = InventoryItem(device=device, parent=parent, name=item['name'], part_id=item['part_id'],
serial=item['serial'], discovered=True)
i.save()
create_inventory_items(item.get('items', []), parent=i)
# Credentials
if options['username']:
@@ -39,7 +41,7 @@ class Command(BaseCommand):
self.password = getpass("Password: ")
# Attempt to inventory only active devices
device_list = Device.objects.filter(status=True)
device_list = Device.objects.filter(status=STATUS_ACTIVE)
# --site: Include only devices belonging to specified site(s)
if options['site']:
@@ -72,7 +74,7 @@ class Command(BaseCommand):
# Skip inactive devices
if not device.status:
self.stdout.write("Skipped (inactive)")
self.stdout.write("Skipped (not active)")
continue
# Skip devices without primary_ip set
@@ -107,9 +109,9 @@ class Command(BaseCommand):
self.stdout.write("")
self.stdout.write("\tSerial: {}".format(inventory['chassis']['serial']))
self.stdout.write("\tDescription: {}".format(inventory['chassis']['description']))
for module in inventory['modules']:
self.stdout.write("\tModule: {} / {} ({})".format(module['name'], module['part_id'],
module['serial']))
for item in inventory['items']:
self.stdout.write("\tItem: {} / {} ({})".format(item['name'], item['part_id'],
item['serial']))
else:
self.stdout.write("{} ({})".format(inventory['chassis']['description'], inventory['chassis']['serial']))
@@ -119,7 +121,7 @@ class Command(BaseCommand):
if device.serial != inventory['chassis']['serial']:
device.serial = inventory['chassis']['serial']
device.save()
Module.objects.filter(device=device, discovered=True).delete()
create_modules(inventory.get('modules', []))
InventoryItem.objects.filter(device=device, discovered=True).delete()
create_inventory_items(inventory.get('items', []))
self.stdout.write("Finished!")

View File

@@ -0,0 +1,47 @@
from __future__ import unicode_literals
from django.core.management.base import BaseCommand
from django.utils import timezone
from extras.models import ReportResult
from extras.reports import get_reports
class Command(BaseCommand):
help = "Run a report to validate data in NetBox"
def add_arguments(self, parser):
parser.add_argument('reports', nargs='+', help="Report(s) to run")
def handle(self, *args, **options):
# Gather all available reports
reports = get_reports()
# Run reports
for module_name, report_list in reports:
for report in report_list:
if module_name in options['reports'] or report.full_name in options['reports']:
# Run the report and create a new ReportResult
self.stdout.write(
"[{:%H:%M:%S}] Running {}...".format(timezone.now(), report.full_name)
)
report.run()
# Report on success/failure
status = self.style.ERROR('FAILED') if report.failed else self.style.SUCCESS('SUCCESS')
for test_name, attrs in report.result.data.items():
self.stdout.write(
"\t{}: {} success, {} info, {} warning, {} failure".format(
test_name, attrs['success'], attrs['info'], attrs['warning'], attrs['failure']
)
)
self.stdout.write(
"[{:%H:%M:%S}] {}: {}".format(timezone.now(), report.full_name, status)
)
# Wrap things up
self.stdout.write(
"[{:%H:%M:%S}] Finished".format(timezone.now())
)

View File

@@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2017-04-04 19:58
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import extras.models
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('extras', '0005_useraction_add_bulk_create'),
]
operations = [
migrations.CreateModel(
name='ImageAttachment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_id', models.PositiveIntegerField()),
('image', models.ImageField(height_field=b'image_height', upload_to=extras.models.image_upload, width_field=b'image_width')),
('image_height', models.PositiveSmallIntegerField()),
('image_width', models.PositiveSmallIntegerField()),
('name', models.CharField(blank=True, max_length=50)),
('created', models.DateTimeField(auto_now_add=True)),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
],
options={
'ordering': ['name'],
},
),
]

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

@@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-09-26 21:25
from __future__ import unicode_literals
from distutils.version import StrictVersion
from django.conf import settings
import django.contrib.postgres.fields.jsonb
from django.db import connection, migrations, models
import django.db.models.deletion
from django.db.utils import OperationalError
def verify_postgresql_version(apps, schema_editor):
"""
Verify that PostgreSQL is version 9.4 or higher.
"""
try:
with connection.cursor() as cursor:
cursor.execute("SELECT VERSION()")
row = cursor.fetchone()
pg_version = row[0].split()[1]
if StrictVersion(pg_version) < StrictVersion('9.4.0'):
raise Exception("PostgreSQL 9.4.0 or higher is required ({} found). Upgrade PostgreSQL and then run migrations again.".format(pg_version))
# Skip if the database is missing (e.g. for CI testing) or misconfigured.
except OperationalError:
pass
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('extras', '0007_unicode_literals'),
]
operations = [
migrations.RunPython(verify_postgresql_version),
migrations.CreateModel(
name='ReportResult',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('report', models.CharField(max_length=255, unique=True)),
('created', models.DateTimeField(auto_now_add=True)),
('failed', models.BooleanField()),
('data', django.contrib.postgres.fields.jsonb.JSONField()),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['report'],
},
),
]

View File

@@ -1,72 +1,27 @@
from __future__ import unicode_literals
from collections import OrderedDict
from datetime import date
import graphviz
from django.contrib.auth.models import User
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields import JSONField
from django.core.validators import ValidationError
from django.db import models
from django.db.models import Q
from django.http import HttpResponse
from django.template import Template, Context
from django.utils.encoding import python_2_unicode_compatible
from django.utils.safestring import mark_safe
from utilities.utils import foreground_color
from .constants import *
CUSTOMFIELD_MODELS = (
'site', 'rack', 'devicetype', 'device', # DCIM
'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', # IPAM
'provider', 'circuit', # Circuits
'tenant', # Tenants
)
CF_TYPE_TEXT = 100
CF_TYPE_INTEGER = 200
CF_TYPE_BOOLEAN = 300
CF_TYPE_DATE = 400
CF_TYPE_URL = 500
CF_TYPE_SELECT = 600
CUSTOMFIELD_TYPE_CHOICES = (
(CF_TYPE_TEXT, 'Text'),
(CF_TYPE_INTEGER, 'Integer'),
(CF_TYPE_BOOLEAN, 'Boolean (true/false)'),
(CF_TYPE_DATE, 'Date'),
(CF_TYPE_URL, 'URL'),
(CF_TYPE_SELECT, 'Selection'),
)
GRAPH_TYPE_INTERFACE = 100
GRAPH_TYPE_PROVIDER = 200
GRAPH_TYPE_SITE = 300
GRAPH_TYPE_CHOICES = (
(GRAPH_TYPE_INTERFACE, 'Interface'),
(GRAPH_TYPE_PROVIDER, 'Provider'),
(GRAPH_TYPE_SITE, 'Site'),
)
EXPORTTEMPLATE_MODELS = [
'site', 'rack', 'device', 'consoleport', 'powerport', 'interfaceconnection', # DCIM
'aggregate', 'prefix', 'ipaddress', 'vlan', # IPAM
'provider', 'circuit', # Circuits
'tenant', # Tenants
]
ACTION_CREATE = 1
ACTION_IMPORT = 2
ACTION_EDIT = 3
ACTION_BULK_EDIT = 4
ACTION_DELETE = 5
ACTION_BULK_DELETE = 6
ACTION_BULK_CREATE = 7
ACTION_CHOICES = (
(ACTION_CREATE, 'created'),
(ACTION_BULK_CREATE, 'bulk created'),
(ACTION_IMPORT, 'imported'),
(ACTION_EDIT, 'modified'),
(ACTION_BULK_EDIT, 'bulk edited'),
(ACTION_DELETE, 'deleted'),
(ACTION_BULK_DELETE, 'bulk deleted'),
)
#
# Custom fields
#
class CustomFieldModel(object):
@@ -130,7 +85,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)
@@ -150,16 +109,13 @@ class CustomField(models.Model):
# Read date as YYYY-MM-DD
return date(*[int(n) for n in serialized_value.split('-')])
if self.type == CF_TYPE_SELECT:
try:
return self.choices.get(pk=int(serialized_value))
except CustomFieldChoice.DoesNotExist:
return None
return self.choices.get(pk=int(serialized_value))
return serialized_value
@python_2_unicode_compatible
class CustomFieldValue(models.Model):
field = models.ForeignKey('CustomField', related_name='values')
field = models.ForeignKey('CustomField', related_name='values', on_delete=models.CASCADE)
obj_type = models.ForeignKey(ContentType, related_name='+', on_delete=models.PROTECT)
obj_id = models.PositiveIntegerField()
obj = GenericForeignKey('obj_type', 'obj_id')
@@ -170,7 +126,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):
@@ -213,6 +169,10 @@ class CustomFieldChoice(models.Model):
CustomFieldValue.objects.filter(field__type=CF_TYPE_SELECT, serialized_value=str(pk)).delete()
#
# Graphs
#
@python_2_unicode_compatible
class Graph(models.Model):
type = models.PositiveSmallIntegerField(choices=GRAPH_TYPE_CHOICES)
@@ -238,9 +198,15 @@ class Graph(models.Model):
return template.render(Context({'obj': obj}))
#
# Export templates
#
@python_2_unicode_compatible
class ExportTemplate(models.Model):
content_type = models.ForeignKey(ContentType, limit_choices_to={'model__in': EXPORTTEMPLATE_MODELS})
content_type = models.ForeignKey(
ContentType, limit_choices_to={'model__in': EXPORTTEMPLATE_MODELS}, on_delete=models.CASCADE
)
name = models.CharField(max_length=100)
description = models.CharField(max_length=200, blank=True)
template_code = models.TextField()
@@ -254,7 +220,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):
"""
@@ -272,11 +238,15 @@ class ExportTemplate(models.Model):
return response
#
# Topology maps
#
@python_2_unicode_compatible
class TopologyMap(models.Model):
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
site = models.ForeignKey('dcim.Site', related_name='topology_maps', blank=True, null=True)
site = models.ForeignKey('dcim.Site', related_name='topology_maps', blank=True, null=True, on_delete=models.CASCADE)
device_patterns = 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. "
@@ -296,6 +266,154 @@ class TopologyMap(models.Model):
return None
return [line.strip() for line in self.device_patterns.split('\n')]
def render(self, img_format='png'):
from circuits.models import CircuitTermination
from dcim.models import CONNECTION_STATUS_CONNECTED, Device, InterfaceConnection
# Construct the graph
graph = graphviz.Graph()
graph.graph_attr['ranksep'] = '1'
seen = set()
for i, device_set in enumerate(self.device_sets):
subgraph = graphviz.Graph(name='sg{}'.format(i))
subgraph.graph_attr['rank'] = 'same'
# Add a pseudonode for each device_set to enforce hierarchical layout
subgraph.node('set{}'.format(i), label='', shape='none', width='0')
if i:
graph.edge('set{}'.format(i - 1), 'set{}'.format(i), style='invis')
# Add each device to the graph
devices = []
for query in device_set.strip(';').split(';'): # Split regexes on semicolons
devices += Device.objects.filter(name__regex=query).select_related('device_role')
# Remove duplicate devices
devices = [d for d in devices if d.id not in seen]
seen.update([d.id for d in devices])
for d in devices:
bg_color = '#{}'.format(d.device_role.color)
fg_color = '#{}'.format(foreground_color(d.device_role.color))
subgraph.node(d.name, style='filled', fillcolor=bg_color, fontcolor=fg_color, fontname='sans')
# Add an invisible connection to each successive device in a set to enforce horizontal order
for j in range(0, len(devices) - 1):
subgraph.edge(devices[j].name, devices[j + 1].name, style='invis')
graph.subgraph(subgraph)
# Compile list of all devices
device_superset = Q()
for device_set in self.device_sets:
for query in device_set.split(';'): # Split regexes on semicolons
device_superset = device_superset | Q(name__regex=query)
# Add all interface connections to the graph
devices = Device.objects.filter(*(device_superset,))
connections = InterfaceConnection.objects.filter(
interface_a__device__in=devices, interface_b__device__in=devices
)
for c in connections:
style = 'solid' if c.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
graph.edge(c.interface_a.device.name, c.interface_b.device.name, style=style)
# 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 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)
#
# Image attachments
#
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].lower()
if instance.name and extension in ['bmp', 'gif', 'jpeg', 'jpg', 'png']:
filename = '.'.join([instance.name, extension])
elif instance.name:
filename = instance.name
return '{}{}_{}_{}'.format(path, instance.content_type.name, instance.object_id, filename)
@python_2_unicode_compatible
class ImageAttachment(models.Model):
"""
An uploaded image which is associated with an object.
"""
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
parent = GenericForeignKey('content_type', 'object_id')
image = models.ImageField(upload_to=image_upload, height_field='image_height', width_field='image_width')
image_height = models.PositiveSmallIntegerField()
image_width = models.PositiveSmallIntegerField()
name = models.CharField(max_length=50, blank=True)
created = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['name']
def __str__(self):
if self.name:
return self.name
filename = self.image.name.rsplit('/', 1)[-1]
return filename.split('_', 2)[2]
def delete(self, *args, **kwargs):
_name = self.image.name
super(ImageAttachment, self).delete(*args, **kwargs)
# Delete file from disk
self.image.delete(save=False)
# Deleting the file erases its name. We restore the image's filename here in case we still need to reference it
# before the request finishes. (For example, to display a message indicating the ImageAttachment was deleted.)
self.image.name = _name
@property
def size(self):
"""
Wrapper around `image.size` to suppress an OSError in case the file is inaccessible.
"""
try:
return self.image.size
except OSError:
return None
#
# Report results
#
class ReportResult(models.Model):
"""
This model stores the results from running a user-defined report.
"""
report = models.CharField(max_length=255, unique=True)
created = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey(User, on_delete=models.SET_NULL, related_name='+', blank=True, null=True)
failed = models.BooleanField()
data = JSONField()
class Meta:
ordering = ['report']
#
# User actions
#
class UserActionManager(models.Manager):
@@ -359,8 +477,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]:

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