Compare commits

..

442 Commits

Author SHA1 Message Date
Jeremy Stretch
fe4de7f929 Merge pull request #6542 from netbox-community/develop
Release v2.11.5
2021-06-04 09:29:32 -04:00
jeremystretch
0783d57459 Release v2.11.5 2021-06-04 09:09:56 -04:00
jeremystretch
4e1e5bd8c4 Fix "select all" box (again) 2021-06-04 09:01:58 -04:00
jeremystretch
b3a14e9a7b Improve performance when fetching objects for bulk edit 2021-06-03 21:11:45 -04:00
jeremystretch
b725a9bcea Closes #6495: Replace 'help' link in footer with 'community' 2021-06-03 20:35:53 -04:00
jeremystretch
5c263fac8d Closes #6540: Add a 'flat' column to the prefix table 2021-06-03 20:31:09 -04:00
jeremystretch
04c1619eb4 Remove unused function 2021-06-03 20:27:24 -04:00
jeremystretch
d74dbb722a Changelog for #6527 2021-06-03 17:20:24 -04:00
Jeremy Stretch
95969c4979 Merge pull request #6537 from maximumG/6527-report-description-makdown
feat: markdown support in report's description
2021-06-03 17:18:22 -04:00
maximumG
10c9954ebc fix: remove call-outs regarding markdown support 2021-06-03 20:36:52 +02:00
maxime-gerges-external
e61b2b1fc5 feat: markdown support in report's description
* markdown support in report list and report result pages
* Add notes in the documentation regarding markdown
2021-06-03 14:48:18 +02:00
Daniel Sheppard
46ecb0ac03 Fixes: #6432 - Properly mark nat_outside as read-only and not-required. 2021-06-02 22:45:17 -05:00
jeremystretch
0a0b852f2c Fixes #6492: Correct tag population in post-change data resulting from REST API changes 2021-06-02 17:02:44 -04:00
jeremystretch
1658d7ae86 Fixes #6217: Disallow passing of string values for integer custom fields 2021-06-02 16:12:11 -04:00
jeremystretch
ca44cda112 Suppress migration output during testing 2021-06-02 16:02:38 -04:00
jeremystretch
1935f8b27f Fixes #6517: Fix assignment of user when creating rack reservations via REST API 2021-06-02 16:02:22 -04:00
jeremystretch
d32dba43b4 Fixes #6525: Paginate related IPs table under IP address view 2021-06-02 15:48:15 -04:00
jeremystretch
8d0a3c8e69 Closes #6519: Avoid querying applicable webhooks for every object 2021-06-01 13:55:17 -04:00
Jeremy Stretch
f561b2d955 Merge pull request #6516 from netbox-community/6284-m2m-webhooks
Closes #6284: Fix redundant webhooks
2021-06-01 13:09:21 -04:00
jeremystretch
8afb7d654d Changelog for #6284 2021-06-01 12:57:31 -04:00
jeremystretch
32cbc20108 Restore webhooks worker test 2021-06-01 12:52:25 -04:00
jeremystretch
be3cd2a434 Add bulk operation tests for webhooks 2021-06-01 09:50:38 -04:00
jeremystretch
ba3ca6b00d Update post-change snapshot for M2M changes 2021-06-01 09:30:54 -04:00
jeremystretch
c88dcef900 Extend webhook create/update/delete tests 2021-06-01 09:04:01 -04:00
jeremystretch
3d1e4fde81 Initial work on #6284 2021-05-28 16:07:27 -04:00
jeremystretch
1e02bb5999 Fixes #6064: Fix object permission assignments for user and group models 2021-05-28 13:27:05 -04:00
jeremystretch
bd7bcf8a0b Fixes #6496: Fix upgrade script when Python installed in nonstandard path 2021-05-28 13:18:50 -04:00
jeremystretch
1c0f3e1b81 Fixes #6502: Correct permissions evaluation for running a report via the REST API 2021-05-28 13:16:25 -04:00
jeremystretch
b2b3f388b1 Correct Prefix REST API test case 2021-05-28 11:15:45 -04:00
jeremystretch
110a6d11a5 Closes #6487: Add location filter to cable connection form 2021-05-28 09:09:59 -04:00
jeremystretch
75faf7d30e Closes #6501: Expose prefix depth and children on REST API serializer 2021-05-28 08:56:55 -04:00
Jeremy Stretch
e95a9731be Merge pull request #6488 from netbox-community/6087-prefix-depth-children
Closes #6087: Cache prefix depth & children count
2021-05-28 08:37:45 -04:00
jeremystretch
5cb5f9a963 Linkify prefix children count 2021-05-27 15:40:55 -04:00
jeremystretch
88aa3a4e19 Specify batch size for bulk_update() 2021-05-27 15:25:40 -04:00
jeremystretch
d34b9ee00e Add max depth selector 2021-05-27 13:24:31 -04:00
jeremystretch
103730a642 Extend depth & children filters 2021-05-27 12:54:41 -04:00
jeremystretch
84017776ec Fix handling of duplicate prefixes 2021-05-27 10:03:00 -04:00
jeremystretch
34e673f7d6 Introduce rebuild_prefixes management command 2021-05-27 09:24:29 -04:00
jeremystretch
5ac6a307bf Rearrange contact links 2021-05-26 21:45:18 -04:00
jeremystretch
8c1b681391 Add GitHub discussions link; replace Google Group with netdev.chat 2021-05-26 21:43:32 -04:00
jeremystretch
da558de769 Initial work on #6087 2021-05-26 16:06:03 -04:00
jeremystretch
da1fb4f969 Replace references to v2.12 with v3.0 2021-05-25 15:05:02 -04:00
jeremystretch
9046f59b9f PRVB 2021-05-25 12:12:08 -04:00
Jeremy Stretch
6c1f9dba52 Merge pull request #6480 from netbox-community/develop
Release v2.11.4
2021-05-25 12:08:16 -04:00
jeremystretch
ea1df2b5c3 Merge branch 'master' into develop 2021-05-25 11:49:03 -04:00
jeremystretch
b3423e1722 Release v2.11.4 2021-05-25 11:38:43 -04:00
jeremystretch
bfb91fcf10 Closes #6422: Enable filtering users by group under admin UI 2021-05-25 11:26:18 -04:00
jeremystretch
44c62f8f44 Release notes for #6358 2021-05-25 11:16:06 -04:00
Jeremy Stretch
c8c47961db Merge pull request #6473 from rodvand/develop
Closes #6358: Add search to VLAN group overview.
2021-05-25 10:33:37 -04:00
Martin Rødvand
78b0e50742 Closes #6358: Add search to VLAN group overview. 2021-05-22 14:27:18 +02:00
jeremystretch
a7371c048b Changelog for #5121 2021-05-21 17:25:37 -04:00
Jeremy Stretch
f3dfa81811 Merge pull request #6470 from netbox-community/5121-filter-tags-content-type
Closes #5121: Add object type filters for Tags
2021-05-21 17:22:30 -04:00
jeremystretch
5b4793a2d5 Closes #5121: Add content_type filters for tags 2021-05-21 17:05:32 -04:00
jeremystretch
b6660c72e1 Add tags as a feature query 2021-05-21 16:54:33 -04:00
jeremystretch
a6eeed4061 Fixes #6467: Fix access to metrics on custom BASE_PATH when login is required 2021-05-21 15:56:22 -04:00
jeremystretch
239fddcac2 Fixes #6468: Disable ordering VLAN groups list by scope object 2021-05-21 15:43:18 -04:00
jeremystretch
b27f9bf74c Changelog for #6465 2021-05-21 11:17:58 -04:00
Jeremy Stretch
09b856bf0b Merge pull request #6449 from 991jo/device_type_import_fix
Fixed #6438 Device Type Import does not import/export description/label fields for many components
2021-05-21 11:16:32 -04:00
Jeremy Stretch
9954c6a571 Merge pull request #6419 from tehtbl-oss/develop
Update netbox/extras/plugins/views.py#L45 by fixing a typo in method _get_plugin_data
2021-05-21 11:08:11 -04:00
jeremystretch
44b24de5d0 Add DigitalOcean as sponsor 2021-05-20 12:41:23 -04:00
jeremystretch
22927bfc76 Closes #6441: Improve UI paginator to optimize page object count 2021-05-20 12:00:31 -04:00
jeremystretch
a39522a25e Closes #6434: Add deprecation warning for stock secrets functionality 2021-05-20 10:51:41 -04:00
Johannes Erwerle
ea6c8a1a65 Fixed #6438 Device Type Import does not import/export description/label fields for many components 2021-05-20 06:30:44 +00:00
jeremystretch
546bbe5418 Fixes #6426: Allow assigning virtual chassis member interfaces to LAG on VC master 2021-05-18 16:42:21 -04:00
jeremystretch
5ca7f375d3 Clean up stray quote 2021-05-17 13:18:02 -04:00
jeremystretch
568148a349 Warn against relying on demo instance for bug reports 2021-05-17 13:10:49 -04:00
Jeremy Stretch
fedf745d25 Merge pull request #6428 from shinsterneck/patch-1
Typo fix in Documentation section "Invalidating Cached Data"
2021-05-17 08:41:16 -04:00
Shin Sterneck
8823aeb9d7 Typo fix
Fix a small typo
2021-05-17 09:27:44 +02:00
Thomas
dc57332988 Update views.py
Fixing typo in 'version'
2021-05-14 18:22:01 +02:00
jeremystretch
138231059b Closes #6400: Add cyan color choice for plugin buttons 2021-05-14 09:13:36 -04:00
jeremystretch
834b233c30 Fixes #6398: Avoid exception when deleting device connected to self via circuit 2021-05-14 09:06:00 -04:00
jeremystretch
72d41eac85 Fixes #6376: Fix assignment of VLAN groups to clusters, cluster groups via REST API 2021-05-12 13:47:42 -04:00
jeremystretch
0fec03ad3f Closes #6393: Add description filter for IP addresses 2021-05-12 13:38:52 -04:00
Jeremy Stretch
7dc71f92d0 Merge pull request #6378 from bluikko/patch-1
Typo in powerfeed.md
2021-05-10 09:16:46 -04:00
bluikko
f74b47ca16 Typo in powerfeed.md
pot -> port
2021-05-09 12:33:09 +07:00
jeremystretch
4dff20cc8c PRVB 2021-05-07 10:22:30 -04:00
Jeremy Stretch
c855570b55 Merge pull request #6371 from netbox-community/develop
Release v2.11.3
2021-05-07 10:19:48 -04:00
jeremystretch
395add8114 Merge branch 'master' of https://github.com/netbox-community/netbox into develop 2021-05-07 10:06:37 -04:00
jeremystretch
019a5563c4 Release v2.11.3 2021-05-07 10:01:17 -04:00
jeremystretch
e9b21aaf86 Fixes #6312: Interface device filter should return all virtual chassis interfaces only if device is master 2021-05-07 09:47:32 -04:00
jeremystretch
d6a0cbb1a0 Add sponsors 2021-05-06 16:37:47 -04:00
jeremystretch
3900b97136 Extend release checklist to include bumping version in GitHub issue templates 2021-05-06 14:41:16 -04:00
jeremystretch
2d4ae38a09 Fixes #6369: Fix interface assignment for VLANs in non-scoped groups 2021-05-06 14:36:23 -04:00
jeremystretch
7f2f98885b Fixes #6350: Include first & last IP addresses when allocating available IPv6 addresses via the REST API 2021-05-06 13:59:10 -04:00
jeremystretch
a4955b420a Fixes #6355: Fix caching error when swapping A/Z circuit terminations 2021-05-06 13:29:52 -04:00
jeremystretch
fe78f60b1f Fixes #6357: Fix ProviderNetwork nested API serializer 2021-05-06 13:06:10 -04:00
jeremystretch
21d14a782e Closes #6359: Enable custom links for organizational and nested group models 2021-05-06 13:01:20 -04:00
jeremystretch
c0f1243879 Fixes #6363: Correct pre-population of cluster group when creating a cluster 2021-05-06 12:47:58 -04:00
jeremystretch
67945f2f33 Closes #6351: Add aggregates count to tenant view 2021-05-05 09:53:06 -04:00
jeremystretch
30ffa4c3f2 Relax stale issue timers to 60/30 days 2021-05-05 09:46:54 -04:00
jeremystretch
97d5873e3d Fixes #6240: Fix display of available VLAN ranges under VLAN group view 2021-05-04 09:36:01 -04:00
jeremystretch
fb1173bc30 Fixes #6339: Improve ordering of interfaces when viewing virtual chassis master 2021-05-04 09:12:26 -04:00
Jeremy Stretch
f0acaa16c4 Merge pull request #6334 from netbox-community/6320-filterset-testing
Closes #6320: Add tests for created & last_updated fields
2021-05-03 15:45:02 -04:00
jeremystretch
3bd99e1910 Closes #6320: Introduce ChangeLoggedFilterSetTests 2021-05-03 15:25:16 -04:00
jeremystretch
ad19b09ae3 Fixes #6333: Fix filtering of circuit terminations by primary key 2021-05-03 14:38:26 -04:00
jeremystretch
ffa4cd134b Introduce BaseFilterSetTests to standardize testing of PK filters 2021-05-03 14:36:44 -04:00
jeremystretch
fbffef1cc4 Rename FilterSet test modules 2021-05-03 13:07:19 -04:00
jeremystretch
2a402b632d Raise operations limit for stalebot 2021-05-03 11:18:04 -04:00
jeremystretch
3bba1089ed Clean up YAML string formatting 2021-04-30 16:08:50 -04:00
jeremystretch
c9c8108a53 Reference demo instance for testing bug reports 2021-04-30 15:54:43 -04:00
jeremystretch
067fdaeb8f Changelog for #6321 2021-04-30 15:47:06 -04:00
Jeremy Stretch
b6e862bd10 Merge pull request #6322 from checktheroads/develop-6321
Closes #6321: Re-add missing 'Add an IP Address' button in prefix view
2021-04-30 15:36:22 -04:00
checktheroads
f1cdd72575 move 'Add an IP Address' button to ip_addresses template 2021-04-30 12:07:56 -07:00
checktheroads
c59c4290f9 Closes #6321: Re-add missing 'Add an IP Address' button in prefix view 2021-04-30 09:50:14 -07:00
jeremystretch
fd9d9d9d35 Closes #6318: Add OM5 MMF cable type 2021-04-30 10:10:03 -04:00
jeremystretch
2a5b497d8a Fixes #6313: Fix device type instance count under manufacturer view 2021-04-30 10:08:15 -04:00
Jeremy Stretch
b93570eeb0 Merge pull request #6315 from netbox-community/6314-filterset-cleanup
Closes #6314: FilterSet cleanup
2021-04-29 19:42:52 -04:00
jeremystretch
3ef6284a0d Move base FilterSet classes under netbox core 2021-04-29 16:53:48 -04:00
jeremystretch
1024782b9e Rename FilterSet modules 2021-04-29 16:48:24 -04:00
jeremystretch
d35ac1347c Move TagFilter to extras 2021-04-29 16:23:55 -04:00
jeremystretch
c4e88fd11a Consolidate FilterSet classes 2021-04-29 15:59:11 -04:00
jeremystretch
0de50e0afe Split Filter and FilterSet classes 2021-04-29 15:13:44 -04:00
jeremystretch
763b02975c Reference the demo instance in the README 2021-04-29 13:45:44 -04:00
jeremystretch
cc57d1edf7 Fixes #6309: Restrict parent VM interface assignment to the parent VM 2021-04-29 08:50:19 -04:00
jeremystretch
bb988701fe Fixes #6308: Fix linking of available VLANs in VLAN group view 2021-04-29 08:43:46 -04:00
jeremystretch
75fdff4d41 Changelog & docs for #6197 2021-04-29 08:10:11 -04:00
Jeremy Stretch
8fc49f37a7 Merge pull request #6307 from mpalmer/patch-1
Expose Django SESSION_COOKIE_NAME setting
2021-04-29 08:04:59 -04:00
jeremystretch
51f6d2f45e PRVB 2021-04-27 10:47:48 -04:00
Jeremy Stretch
1abaa5e60c Merge pull request #6292 from netbox-community/develop
Release v2.11.2
2021-04-27 10:45:12 -04:00
Jeremy Stretch
f4792eea04 Merge branch 'master' into develop 2021-04-27 10:24:13 -04:00
jeremystretch
1fe1e1ce7e Release v2.11.2 2021-04-27 10:20:36 -04:00
jeremystretch
58659cf3b6 Fixes #6262: Support filtering by created/updated time for all relevant objects 2021-04-27 10:04:28 -04:00
jeremystretch
9a588231c5 Fixes #6289: Fix assignment of VC member interfaces to LAG interfaces 2021-04-27 09:36:48 -04:00
jeremystretch
f408ad16e4 Closes #6287: Add option to clear assigned max length filter on prefixes list 2021-04-27 08:34:21 -04:00
jeremystretch
fecca5ad83 Fixes #6267: Fix cable tracing API endpoint for circuit terminations 2021-04-26 16:49:52 -04:00
jeremystretch
9e5d41c48a Remove myself from funding 2021-04-26 15:55:53 -04:00
jeremystretch
87b4cfff1a Closes #6278: Note device locations on cable traces 2021-04-26 15:31:53 -04:00
jeremystretch
19a2b9042f Closes #6275: Linkify rack, device counts on locations list 2021-04-26 15:28:35 -04:00
jeremystretch
2618dde1e2 Fixes #6236: Journal entry title should account for configured timezone 2021-04-23 15:27:58 -04:00
jeremystretch
1dd9f8c1d4 Fixes #6248: Fix table column reconfiguration under Chrome 2021-04-23 10:27:58 -04:00
jeremystretch
4939b6b641 Fixes #6252: Fix assignment of console port speed values above 19.2kbps 2021-04-23 10:07:11 -04:00
jeremystretch
52747e364a Fixes #6254: Disable ordering of space column in racks table 2021-04-23 09:44:29 -04:00
jeremystretch
1901e93b1e Fixes #6258: Fix parent assignment for SiteGroup API serializer 2021-04-23 09:37:22 -04:00
jeremystretch
648b9dd7d8 Closes #6239: Fix sudo invokations of echo 2021-04-22 14:11:58 -04:00
jeremystretch
396c91f8f7 Fixes #6246: Permit full-length descriptions when creating device components and VM interfaces 2021-04-22 14:05:07 -04:00
jeremystretch
83f520f7a8 PRVB 2021-04-21 10:37:51 -04:00
Jeremy Stretch
d8ae65a762 Merge pull request #6230 from netbox-community/develop
Release v2.11.1
2021-04-21 10:05:35 -04:00
Jeremy Stretch
efea511211 Merge branch 'master' into develop 2021-04-21 09:53:23 -04:00
jeremystretch
2564818c3e Release v2.11.1 2021-04-21 09:47:30 -04:00
jeremystretch
e6930d9601 Closes #6161: Enable ordering of device component tables 2021-04-20 20:21:52 -04:00
jeremystretch
4e405ce530 Closes #6210: Include child locations on location view 2021-04-20 14:15:12 -04:00
jeremystretch
88ffc9b145 Update GitHub issue templates 2021-04-20 11:37:03 -04:00
jeremystretch
9ed76400de Closes #6179: Enable natural ordering for virtual machines 2021-04-20 09:37:43 -04:00
jeremystretch
497e50c559 Closes #6190: Allow filtering devices with no location assigned 2021-04-19 16:55:57 -04:00
jeremystretch
7cf9e202a3 Fixes #6215: Restore tenancy section in virtual machine form 2021-04-19 16:50:56 -04:00
jeremystretch
620d222f98 Closes #6189: Add ability to search for locations by name or description 2021-04-19 15:56:39 -04:00
jeremystretch
a2d16143e3 Fixes #6188: Support custom field filtering for regions, site groups, and locations 2021-04-19 11:05:40 -04:00
jeremystretch
97c087ef5f Fixes #6196: Fix object list display for users with read-only permissions 2021-04-19 10:46:23 -04:00
Matt Palmer
07f39b31da Expose Django SESSION_COOKIE_NAME setting
There are situations in which it is convenient to be able to modify the name of the cookie that the application uses for storing the session token (conflicts with other cookies on the same domain, for example).
2021-04-19 12:00:27 +10:00
jeremystretch
6bd4b3c167 Fixes #6184: Fix parent object table column in prefix IP addresses list 2021-04-16 13:07:41 -04:00
jeremystretch
89350a80ad PRVB 2021-04-16 10:32:21 -04:00
Jeremy Stretch
f26227ecec Merge pull request #6180 from netbox-community/develop
Release v2.11.0
2021-04-16 09:52:45 -04:00
Jeremy Stretch
c002501c2e Merge branch 'master' into develop 2021-04-16 09:40:29 -04:00
jeremystretch
8af78c6d84 Release v2.11.0 2021-04-16 09:36:05 -04:00
jeremystretch
7e8ffd8390 Fix markdown list indentation 2021-04-16 09:32:37 -04:00
jeremystretch
5bd30060e7 Fixes #6177: Prevent VM interface from being assigned as its own parent 2021-04-16 09:18:58 -04:00
jeremystretch
c24cac9a44 Fixes #6176: Correct position of MAC address field when creating VM interfaces 2021-04-16 09:16:43 -04:00
jeremystretch
2bc9730b1d Merge release notes from v2.11-beta1 2021-04-16 09:16:01 -04:00
Jeremy Stretch
428858dc75 Merge pull request #6175 from netbox-community/feature
Prep for v2.11 release
2021-04-16 08:45:23 -04:00
jeremystretch
8a425a93ae Fixes #6173: Fix assigned device/VM count when bulk editing/deleting device roles 2021-04-15 19:53:46 -04:00
jeremystretch
0bb2a051b6 Fixes #6171: Fix display of horizontally-scrolling object lists 2021-04-15 15:52:11 -04:00
jeremystretch
ef78a7d41b Merge branch 'develop' into feature 2021-04-15 15:46:48 -04:00
Jeremy Stretch
6c1c695616 Merge pull request #6172 from netbox-community/develop
Release v2.10.10
2021-04-15 15:34:24 -04:00
Jeremy Stretch
8bdab347b6 Merge branch 'master' into develop 2021-04-15 15:24:36 -04:00
jeremystretch
ae349d4424 Release v2.10.10 2021-04-15 15:20:30 -04:00
jeremystretch
3e49745257 Changelog for #6168 2021-04-15 13:30:42 -04:00
Jeremy Stretch
c2b7226205 Merge pull request #6169 from promasu/develop
Fixes #6168: Add SFP56 interface type
2021-04-15 13:29:44 -04:00
jeremystretch
f67369a9a9 Fixes #5643: Fix VLAN assignment when editing VM interfaces in bulk 2021-04-15 13:26:19 -04:00
Adrian Nöthlich
14d2a49942 Fixes #6168: Add SFP56 interface type
Signed-off-by: Adrian Nöthlich <git@promasu.tech>
2021-04-15 18:37:45 +02:00
jeremystretch
92df40a6a0 Fixes #5652: Update object data when renaming a custom field 2021-04-15 12:04:34 -04:00
jeremystretch
a2203da1c6 Fixes #6162: Fix choice field filters (multiple models) 2021-04-14 16:17:19 -04:00
jeremystretch
0fb8291b8b Documentation cleanup 2021-04-14 15:21:30 -04:00
jeremystretch
b393ec2856 Enable webhooks for JournalEntry 2021-04-14 14:55:01 -04:00
jeremystretch
664ba55460 Reorganize base TestCase classes 2021-04-14 14:22:58 -04:00
jeremystretch
3d0882856f Closes #5796: Add DC terminal power port, outlet types 2021-04-14 10:44:15 -04:00
jeremystretch
e849d28276 Closes #5980: Add Saf-D-Grid power port, outlet types 2021-04-14 10:40:31 -04:00
jeremystretch
18a691482d Closes #6160: Add F connector port type 2021-04-14 10:38:54 -04:00
jeremystretch
7c48e3632d Closes #6157: Support Markdown rendering for report logs 2021-04-14 10:21:07 -04:00
jeremystretch
8701b20899 Fixes #6155: Fix admin links for plugins, background tasks 2021-04-14 09:46:05 -04:00
jeremystretch
e3f5062583 Merge branch 'develop' into feature 2021-04-13 16:41:46 -04:00
jeremystretch
d427e0522e Closes #5757: Add unique identifier to every object view 2021-04-13 16:14:12 -04:00
jeremystretch
46e144f647 Clean up object header 2021-04-13 16:03:07 -04:00
jeremystretch
6ad20c53d9 Delete unused template 2021-04-13 15:58:21 -04:00
jeremystretch
cc433388f5 Fixes #6056: Optimize change log cleanup 2021-04-13 13:48:22 -04:00
jeremystretch
1fba4b7e32 Fixes #5419: Update parent device/VM when deleting a primary IP 2021-04-13 13:23:25 -04:00
jeremystretch
c249cd4ffd Fixes #6152: Fix custom field filtering for cables, virtual chassis 2021-04-13 12:05:44 -04:00
jeremystretch
d54bf5f75e Fixes #6144: Fix MAC address field display in VM interfaces search form 2021-04-13 11:52:32 -04:00
jeremystretch
9cbe3ff551 Enable close-stale-issue action 2021-04-13 11:46:14 -04:00
jeremystretch
b1d20d3228 Closes #6149: Support image attachments for locations 2021-04-13 11:39:04 -04:00
jeremystretch
e5602abee0 Closes #5848: Filter custom fields by content type in format <app_label>.<model> 2021-04-13 11:30:45 -04:00
jeremystretch
a296a9e109 Closes #6150: Enable change logging for journal entries 2021-04-13 10:53:55 -04:00
jeremystretch
e5bbf47ab9 Fixes #5583: Eliminate redundant change records when adding/removing tags 2021-04-13 10:14:25 -04:00
jeremystretch
9bda2a44ae Fix permissions for cable trace view tests 2021-04-12 16:25:52 -04:00
jeremystretch
a1d32c3a21 Add view tests for CircuitTermination 2021-04-12 16:07:52 -04:00
jeremystretch
b4b68c0b00 Move create_test_device() to testing utils 2021-04-12 16:07:03 -04:00
jeremystretch
608bf30bda Add cable trace view tests 2021-04-12 15:52:24 -04:00
jeremystretch
d0be5f09a5 Merge branch 'develop' into feature 2021-04-12 15:35:12 -04:00
jeremystretch
3d8a3a2204 Fix link 2021-04-12 15:17:50 -04:00
jeremystretch
b19734004a Removed the "Additional information" blocks from issue templates (no longer needed) 2021-04-12 15:06:40 -04:00
jeremystretch
f503c72782 Closes #6146: Add bulk disconnect support for power feeds 2021-04-12 15:02:29 -04:00
jeremystretch
f8903a8183 Merge branch 'develop' into feature 2021-04-12 14:19:30 -04:00
jeremystretch
d43d5a6cb6 Change stale issues management to GitHub Actions 2021-04-12 13:53:38 -04:00
jeremystretch
99f4b2cf95 PRVB 2021-04-12 13:29:37 -04:00
Jeremy Stretch
b493d739bd Merge pull request #6143 from netbox-community/develop
Release v2.10.9
2021-04-12 13:28:28 -04:00
jeremystretch
92fb43a455 Merge branch 'master' into develop 2021-04-12 13:18:24 -04:00
jeremystretch
7c29fb449e Release v2.10.9 2021-04-12 13:10:33 -04:00
jeremystretch
7ddcec3a0d Fixes #6082: Support colons in webhook header values 2021-04-12 12:09:37 -04:00
jeremystretch
18f206747c Closes #6088: Improved table configuration form 2021-04-12 10:46:32 -04:00
jeremystretch
0bce1da4e3 Clean up stray references to old RackGroup model 2021-04-11 13:43:06 -04:00
jeremystretch
65ed047084 Fixes #6124: Location parent filter should return all child locations (not just those directly assigned) 2021-04-11 13:42:24 -04:00
jeremystretch
b0573f88e6 Merge branch 'develop' into feature 2021-04-11 13:16:00 -04:00
jeremystretch
348fca7e28 Fixes #6117: Handle exception when attempting to assign an MPTT-enabled model as its own parent 2021-04-11 12:57:53 -04:00
jeremystretch
cc9b750eff Changelog & docs for #6083 2021-04-09 14:58:40 -04:00
Marcus Weiner
701ad8a4a9 Allow skipping TLS cert verification on Redis connection (#6084)
* Allow skipping redis tls cert verification

* Add config example
2021-04-09 14:51:58 -04:00
jeremystretch
2cc088c633 Fixes #6131: Correct handling of boolean fields when cloning objects 2021-04-09 14:42:07 -04:00
tcaiazza
4dfba3a2ad Update export-templates.md (#6091)
* Update export-templates.md

* Update export-templates.md

Co-authored-by: Jeremy Stretch <jeremy@netverity.dev>
2021-04-09 14:14:08 -04:00
jeremystretch
a3721a94ce Closes #6121: Extend parent interface assignment to VM interfaces 2021-04-09 10:53:05 -04:00
jeremystretch
7439faad34 Fixes #6123: Prevent device from being assigned to mismatched site and location 2021-04-09 09:56:36 -04:00
jeremystretch
6efe54aa88 Closes #6125: Add locations count to home page 2021-04-09 09:47:34 -04:00
jeremystretch
9e62d1ad8f Fixes #6130: Improve display of assigned models in custom fields list 2021-04-09 09:43:35 -04:00
jeremystretch
e69251b21a Fixes #6070: Add missing 'count_ipaddresses' attribute to VMInterface serializer 2021-04-08 14:22:45 -04:00
jeremystretch
f096c4a5d0 #6081: Tweak queryset filtering 2021-04-08 14:18:07 -04:00
jeremystretch
03b3f5937f Fixes #6108: Do not infer tenant assignment from parent objects for prefixes, IP addresses 2021-04-08 13:50:06 -04:00
jeremystretch
696b5c80a7 Closes #6097: Redirect old slug-based object views 2021-04-08 13:25:29 -04:00
jeremystretch
d6fcd22752 Fixes #6110: Fix handling of TemplateColumn values for table export 2021-04-08 10:30:13 -04:00
jeremystretch
54d9ca8ed8 Add racks count to location view 2021-04-08 10:10:46 -04:00
jeremystretch
05d8a06cd5 Closes #6109: Add device counts to locations table 2021-04-08 10:08:50 -04:00
jeremystretch
4f7626828a Fixes #6107: Fix rack selection field on device form 2021-04-07 16:58:40 -04:00
jeremystretch
81193eb550 Fixes #6106: Allow assigning a virtual interface as the parent of an existing interface 2021-04-07 16:36:09 -04:00
jeremystretch
38b09dc610 Fixes #6105: Hide checkboxes for VMs under cluster VMs view 2021-04-07 16:26:16 -04:00
jeremystretch
ae3527df16 Fixes #6081: Fix interface connections REST API endpoint 2021-04-07 16:04:32 -04:00
jeremystretch
85d0270af0 Fixes #6099: Correct example permission description 2021-04-07 15:50:24 -04:00
jeremystretch
59e185b781 Fixes #6104: Fix location column on racks table 2021-04-07 15:40:03 -04:00
jeremystretch
b5ad29e3f2 Fixes #6100: Fix VM interfaces table "add interfaces" link 2021-04-07 15:17:02 -04:00
jeremystretch
f1e2b99456 Release v2.11-beta1 2021-04-06 11:45:32 -04:00
jeremystretch
0635e7ae1e Update dependencies for v2.11-beta1 2021-04-06 11:36:30 -04:00
jeremystretch
8f674aede9 Bump Django to 3.2 2021-04-06 09:45:59 -04:00
jeremystretch
838200219f Include child regions, site groups 2021-04-06 09:42:36 -04:00
jeremystretch
e543b305c3 Docs cleanup 2021-04-05 21:46:48 -04:00
jeremystretch
d42b0691b2 Fix 'select all' widget 2021-04-05 17:13:32 -04:00
jeremystretch
71022d58d3 Site is required when creating devices 2021-04-05 16:35:01 -04:00
jeremystretch
a313b675a6 Simplify CircuitTermination display in circuits table 2021-04-05 15:24:57 -04:00
jeremystretch
ae18693715 Add 'add export template' link to dropdown 2021-04-05 15:13:35 -04:00
jeremystretch
3ad7622bf0 Catch AttributeError when generating ContentType labels 2021-04-05 15:11:29 -04:00
jeremystretch
19cb575b90 Permit the assignment of virtual interfaces as parents 2021-04-05 14:48:11 -04:00
jeremystretch
3cfab25f8a Tweak JournalEntry form layout 2021-04-05 14:35:09 -04:00
jeremystretch
85359bb10f Fix ContentType assignment 2021-04-05 14:05:28 -04:00
jeremystretch
aa2beb1d78 Add tagged items count to tag view 2021-04-05 13:53:25 -04:00
jeremystretch
2e07ac5a47 Rearrange provider, provider network templates 2021-04-05 13:18:19 -04:00
jeremystretch
d844ee2c79 Empty time_zone should be null 2021-04-05 13:05:07 -04:00
jeremystretch
266482649f Update VLAN documentation 2021-04-05 12:55:57 -04:00
jeremystretch
83496c66d1 Move breadcrumb generation to template 2021-04-05 12:06:59 -04:00
jeremystretch
374cb74978 Filter parent location by ID 2021-04-05 11:53:40 -04:00
jeremystretch
f0018dcba9 LinkedCountColumn should always invoke filter by ID 2021-04-05 11:47:25 -04:00
jeremystretch
7949a5e1fd Migrate VLANGroup site assignments 2021-04-05 11:40:46 -04:00
jeremystretch
0a1531ce8a TenantColumn should export null if no tenant is assigned 2021-04-05 11:18:30 -04:00
jeremystretch
7b8bd2d4ce Location model cleanup 2021-04-05 11:09:33 -04:00
jeremystretch
4883bc3dd4 Empty MAC address should be null 2021-04-02 17:53:00 -04:00
jeremystretch
72a115b434 Add child interfaces table to interface view 2021-04-02 17:33:34 -04:00
jeremystretch
f28edd0864 Use CommentField for JournalEntry comments 2021-04-02 17:14:15 -04:00
jeremystretch
d69ec7f8e4 Add manufacturer column to DeviceTable 2021-04-02 17:02:12 -04:00
jeremystretch
779837389b Convert all LinkColumns to Column(linkify=True) 2021-04-02 16:59:53 -04:00
jeremystretch
ea9e9d7273 Add the models index 2021-04-02 16:29:01 -04:00
jeremystretch
237dfce8a0 Fix tab headers 2021-04-02 15:44:15 -04:00
jeremystretch
2b0ccf3acd Provide absolute URL for object search form 2021-04-02 15:27:00 -04:00
jeremystretch
e4f3b3447e Remove redundant prechange_data assignments 2021-04-02 15:21:11 -04:00
Jeremy Stretch
eb8e4f64fc Restore 'brief' parameter (regression from a292ff5cc0) 2021-04-02 11:37:09 -04:00
Jeremy Stretch
6287f75e67 Toggle VLANGroup scope selector fields 2021-04-02 11:31:46 -04:00
Jeremy Stretch
d82f2e289a Use ContentTypeChoiceField for all ContentType fields 2021-04-02 10:55:16 -04:00
Jeremy Stretch
73e9842877 Introduce ContentTypeChoiceField 2021-04-02 10:54:13 -04:00
Jeremy Stretch
a86178f19b Simplify VLANGroup scope assignment (WIP) 2021-04-02 09:36:14 -04:00
Jeremy Stretch
b77c228853 Rebuild CablePaths when a CircuitTermination is modified 2021-04-02 09:17:11 -04:00
Jeremy Stretch
5e4432b9ad Show the last node in a CablePath with no destination 2021-04-01 21:29:46 -04:00
Jeremy Stretch
e7f10fdaea Include termination side in CircuitTermination string repr 2021-04-01 21:03:00 -04:00
Jeremy Stretch
8713ed5d73 Add test for trace through multiple circuits 2021-04-01 20:38:36 -04:00
Jeremy Stretch
cd64fcac8d Linkify connected object 2021-04-01 17:21:04 -04:00
Jeremy Stretch
96759af86f Closes #6071: Cable traces now traverse circuits 2021-04-01 14:31:10 -04:00
Jeremy Stretch
d57222328b Rename Cloud to ProviderNetwork 2021-04-01 10:21:41 -04:00
Jeremy Stretch
83c4577f6d Merge branch 'develop' into feature 2021-03-31 17:11:54 -04:00
Jeremy Stretch
7bd853e87b Fixes #5805: Fix missing custom field filters for cables, rack reservations 2021-03-31 17:02:21 -04:00
Jeremy Stretch
861a52d27c Closes #5965: Mention cf property on CustomFieldModel in docs 2021-03-31 16:35:28 -04:00
Jeremy Stretch
9df2130e11 Closes #5840: Add column to cable termination objects to display cable color 2021-03-31 15:49:29 -04:00
Jeremy Stretch
6242e195be Closes #5756: Omit child devices from non-racked devices list under rack view 2021-03-31 15:33:06 -04:00
Jeremy Stretch
f2f0ea8d04 Closes #5526: Add MAC address search field to VM interfaces list 2021-03-31 15:27:38 -04:00
Jeremy Stretch
b7309d5c69 Closes #6054: Display NAPALM-enabled device tabs only when relevant 2021-03-31 15:21:07 -04:00
Jeremy Stretch
613e0d10b3 Add link to v2.11 release notes 2021-03-31 14:24:43 -04:00
Jeremy Stretch
04fc3a5a9e Closes #6001: Paginate component tables under device views 2021-03-31 14:24:05 -04:00
Jeremy Stretch
6ec8ac7597 Fixes #6073: Permit users to manage their own REST API tokens without needing explicit permission 2021-03-31 13:25:06 -04:00
Jeremy Stretch
9a5f54bdaf Add tests for filtering VLANs by device/VM 2021-03-31 10:59:15 -04:00
Jeremy Stretch
ad5e167ad7 Remove obsolete grouping logic 2021-03-31 10:04:44 -04:00
Jeremy Stretch
d33d9522cc Fix VM VLAN filtering 2021-03-30 21:46:56 -04:00
Jeremy Stretch
a292ff5cc0 Remove brief_mode parameter from DynamicModelChoiceMixin 2021-03-30 21:34:50 -04:00
Jeremy Stretch
b91e5763e2 Add 'available_on' VLAN filters for devices & VMs 2021-03-30 21:32:48 -04:00
Jeremy Stretch
e1e840eb69 Fix VLANGroup 'add VLAN' button 2021-03-30 21:23:57 -04:00
Jeremy Stretch
0eb9f41470 Changelog & docs for #5380 2021-03-30 18:59:47 -04:00
Jeremy Stretch
34457ea1b4 Merge pull request #6072 from rodvand/origin/feature
#5830: Expand ExportTemplate model with attachment choice
2021-03-30 18:53:38 -04:00
Martin Rødvand
878154c305 Closes #5830: Add as_attachment field to API serializers and admin view. 2021-03-30 23:26:55 +02:00
Martin Rødvand
9ede726eea Add boolean as_attachment to Export template 2021-03-30 22:27:26 +02:00
Jeremy Stretch
bfa95c16e3 Add missing tests for SiteGroup 2021-03-30 11:06:59 -04:00
Jeremy Stretch
7885ec5511 Clean up custom field column implementation 2021-03-29 17:51:45 -04:00
Tom Grozev
be3d33eebd Add support for custom fields in tables (#5460)
* Add support for custom fields in tables

* Fix empty list displays as none

Co-authored-by: TomGrozev <TomGrozev@users.noreply.github.com>
Co-authored-by: Jeremy Stretch <jeremy@netverity.dev>
2021-03-29 17:15:21 -04:00
Jeremy Stretch
c7040fd418 Closes #6038: Include tagged objects list on tag view 2021-03-29 16:53:41 -04:00
Jeremy Stretch
eac53a779b Migrate prefix, VLAN templates to new structure 2021-03-29 16:43:29 -04:00
Jeremy Stretch
0986fd1081 Rearrange locations link in navigation menu 2021-03-29 16:27:37 -04:00
Jeremy Stretch
1e3c7e1a87 Fix form errors 2021-03-29 16:23:26 -04:00
Jeremy Stretch
3869028d53 Clean up organizational object URLs 2021-03-29 16:17:25 -04:00
Jeremy Stretch
12945704e9 Fix ConfigContext application for SiteGroups 2021-03-29 15:57:44 -04:00
Jeremy Stretch
cd629fc737 Closes #4833: Allow assigning config contexts by device type 2021-03-29 15:40:09 -04:00
Jeremy Stretch
b070be1c41 Closes #5425: Create separate tabs for VMs and devices under the cluster view 2021-03-29 14:55:17 -04:00
Jeremy Stretch
8fa37d3ec8 Device component lists should link to component-specific device view 2021-03-29 14:49:22 -04:00
Jeremy Stretch
3135dc42c3 Update changelog 2021-03-29 11:56:07 -04:00
Jeremy Stretch
08ef5c9a02 Update docs to indicate location assignment ability 2021-03-29 11:55:34 -04:00
Jeremy Stretch
b1935ddcab Fix initial tab display 2021-03-29 11:41:30 -04:00
Jeremy Stretch
93353e94a2 Note that 'table' is a reserved name for ExportTemplates 2021-03-29 11:25:57 -04:00
Jeremy Stretch
30e4504ee5 Enable bulk editing of device location assignment 2021-03-29 11:01:17 -04:00
Jeremy Stretch
a9716af0fa Tweak table display for components marked as connected 2021-03-29 10:53:02 -04:00
Jeremy Stretch
042f3590c8 Update interfaces documentation 2021-03-29 10:35:07 -04:00
Jeremy Stretch
2c9b791b85 Improve Interface validation 2021-03-29 10:34:31 -04:00
Jeremy Stretch
7396975500 Update documentation for journal entries 2021-03-29 09:59:44 -04:00
Jeremy Stretch
fe4bf62793 Truncate comments in JournalEntryTable 2021-03-29 09:54:06 -04:00
Jeremy Stretch
8a8342b106 Strip microseconds from JournalEntry creation time 2021-03-29 09:48:45 -04:00
Jeremy Stretch
e52702f6c2 Fix journal entry table ordering 2021-03-29 09:44:04 -04:00
Jeremy Stretch
0fae7504b3 Add Markdown support for JournalEntry comments 2021-03-29 09:43:21 -04:00
Jeremy Stretch
ab612c1ca6 Update changelog 2021-03-26 20:25:55 -04:00
Jeremy Stretch
0364d8cd43 Closes #6014: Move virtual machine interfaces list to a separate view 2021-03-26 20:19:19 -04:00
Jeremy Stretch
1544823d73 Closes #5926: Strip leading/trailing whitespace from TemplateColumns rendered for export 2021-03-26 16:20:01 -04:00
Jeremy Stretch
b793ee3aff Merge pull request #6062 from netbox-community/5971-org-object-views
Closes #5971: Dedicated views for organizational models
2021-03-26 16:06:45 -04:00
Jeremy Stretch
36c903da04 Add dedicated view for tags 2021-03-26 15:25:18 -04:00
Jeremy Stretch
981e7017bb Enable get view tests for organizational objects 2021-03-26 15:15:59 -04:00
Jeremy Stretch
2820d26a0f Add dedicated views for nested group models 2021-03-26 15:07:29 -04:00
Jeremy Stretch
b7e44a744d Add dedicated views for organizational models 2021-03-26 14:44:43 -04:00
Jeremy Stretch
bb00f2ff46 Introduce paginate_table() utility to simplify table pagination 2021-03-26 13:02:55 -04:00
Jeremy Stretch
65d90aa8a3 Merge branch 'develop' into feature 2021-03-26 10:53:21 -04:00
Jeremy Stretch
c8eae3a5c3 PRVB 2021-03-26 10:11:57 -04:00
Jeremy Stretch
c828806b67 Merge branch 'develop' into feature 2021-03-25 16:09:28 -04:00
Jeremy Stretch
b9176adca4 Merge pull request #6011 from netbox-community/5986-cloud-model
Closes #5896: Introduce the cloud model
2021-03-18 15:22:48 -04:00
Jeremy Stretch
b6f6293b76 Prevent the attachment of a Cable to a CircuitTermination on a Cloud 2021-03-18 15:07:22 -04:00
Jeremy Stretch
d45edcd216 Linkify circuit terminations in table 2021-03-18 14:49:06 -04:00
Jeremy Stretch
89c487de65 Documentation and changelog for #5986 2021-03-18 14:43:07 -04:00
Jeremy Stretch
d45a17247d Add circuit cloud filters & tests 2021-03-18 14:32:28 -04:00
Jeremy Stretch
2e97bf48c5 Include circuits list on cloud view 2021-03-18 14:05:32 -04:00
Jeremy Stretch
872e936924 Add termination FKs on Circuit model 2021-03-18 13:54:05 -04:00
Jeremy Stretch
574a43fff7 Enable attaching circuit terminations to clouds 2021-03-18 13:53:48 -04:00
Jeremy Stretch
6ff8a267e9 Introduce the Cloud model 2021-03-18 11:20:09 -04:00
Jeremy Stretch
433c48a1a3 Fix IP address interface validation 2021-03-17 16:44:34 -04:00
Jeremy Stretch
889316085c Closes #5998: Introduce ContentTypeColumn 2021-03-17 16:29:43 -04:00
Jeremy Stretch
9a68a61ad3 Merge pull request #5999 from netbox-community/151-journaling
Closes #151: Add object journaling
2021-03-17 13:14:50 -04:00
Jeremy Stretch
7e65a3d3b4 Add JournalEntry filter for kind 2021-03-17 13:02:40 -04:00
Jeremy Stretch
82fbd975f1 Add kind field to JournalEntry 2021-03-17 12:51:39 -04:00
Jeremy Stretch
bd95d2b852 Changelog and documentation for #151 2021-03-17 10:58:40 -04:00
Jeremy Stretch
956e2728c2 Add bulk edit, delete views for journal entries 2021-03-17 10:41:06 -04:00
Jeremy Stretch
7f1d9aeaf8 Skip secondary check if get_absolute_url() not defined for model 2021-03-16 16:48:08 -04:00
Jeremy Stretch
f2c079de87 Add JournalEntry tests 2021-03-16 16:47:35 -04:00
Jeremy Stretch
8be4fbbce3 Add JournalEntry list view w/filtering 2021-03-16 15:57:23 -04:00
Jeremy Stretch
64d11d32c8 Fix conditional display journal & change log tabs 2021-03-16 15:33:41 -04:00
Jeremy Stretch
1f1a62da67 Initial work on #151: Object journaling 2021-03-16 15:00:08 -04:00
Jeremy Stretch
e97adcb614 Move ConfigContext classes out of models.py 2021-03-16 13:08:07 -04:00
Jeremy Stretch
2bb4a81e23 Closes #5975: Allow partial vCPU allocations for virtual machines 2021-03-16 11:52:59 -04:00
Jeremy Stretch
46a024441c Closes #5995: Dropped backward compatibility for queryset parameter on ObjectVar and MultiObjectVar 2021-03-16 11:28:50 -04:00
Jeremy Stretch
a694dbb020 Closes #5990: Deprecated display_field parameter for custom script ObjectVar and MultiObjectVar fields 2021-03-16 11:08:34 -04:00
Jeremy Stretch
f74c88027e Merge pull request #5992 from netbox-community/5891-api-serializer-display
Closes #5981: Add display field to all REST API serializers
2021-03-16 10:53:59 -04:00
Jeremy Stretch
c64f96d65b Drop usage of display_field on DynamicModelChoiceFields 2021-03-16 10:29:15 -04:00
Jeremy Stretch
fb48c1f6dd Add 'display' field to all REST API serializers 2021-03-16 10:06:25 -04:00
Jeremy Stretch
ee7f7c877a Merge pull request #5985 from netbox-community/5284-vlangroup-scope
Closes #5284: Allow VLANGroup assignment beyond sites
2021-03-15 21:11:49 -04:00
Jeremy Stretch
f64f205e81 Omit prefetch_related() for VLANGroup 2021-03-15 20:58:23 -04:00
Jeremy Stretch
10778f8479 Merge branch 'feature' into 5284-vlangroup-scope 2021-03-15 20:48:55 -04:00
Jeremy Stretch
bb6360cad4 Fix up VLANGroup tests 2021-03-15 20:35:18 -04:00
Jeremy Stretch
c0c4eed3a8 Extend VLANGroup to support cluster/cluster group assignment 2021-03-15 16:32:07 -04:00
Jeremy Stretch
1aa22d1ba8 Add missing SiteGroup filter test case 2021-03-12 16:15:19 -05:00
Jeremy Stretch
61d23df83a Closes #5972: Bulk edit support for organizational models (#5974)
* Enable bulk editing of organizational models

* Enable bulk editing of nested group models

* Changelog for #5972
2021-03-12 16:14:42 -05:00
Jeremy Stretch
fca5accba8 Clean up base serializers 2021-03-12 13:31:28 -05:00
Jeremy Stretch
947da63d56 Move Location from racks.py to sites.py 2021-03-12 11:20:16 -05:00
Jeremy Stretch
0115a61ab7 Add changelog for #5284 2021-03-11 11:13:41 -05:00
Jeremy Stretch
fadf15dbc0 Fix VLAN group assignment for VLANs 2021-03-11 11:07:27 -05:00
Jeremy Stretch
6ab1c06036 Extend VLAN group assignment form 2021-03-10 16:50:04 -05:00
Jeremy Stretch
d3fee54644 Fix VLAN.clean() 2021-03-10 16:26:27 -05:00
Jeremy Stretch
f4e49495e2 Merge branch 'feature' into 5284-vlangroup-scope 2021-03-10 14:51:11 -05:00
Jeremy Stretch
c072ba088f Add webhook support for additional models 2021-03-10 14:49:02 -05:00
Jeremy Stretch
4397d3da02 Remove redundant TreeManager from TenantGroup 2021-03-10 14:35:44 -05:00
Jeremy Stretch
fc5bb966f0 Declare TaggableManager on PrimaryModel base class 2021-03-10 14:32:50 -05:00
Jeremy Stretch
20f60cd99d Fix feature assignments for device component templates 2021-03-10 14:03:50 -05:00
Jeremy Stretch
ac0c54cce4 #5401: Add custom field support for VMInterface 2021-03-10 13:49:10 -05:00
Jeremy Stretch
19b78e63ce Introduce ChangeLoggedModel as a standard model 2021-03-10 13:37:13 -05:00
Jeremy Stretch
653af17755 Merge branch 'feature' into 5284-vlangroup-scope 2021-03-09 20:17:47 -05:00
Jeremy Stretch
bac2387f50 Merge branch 'develop' into feature 2021-03-09 20:04:20 -05:00
Jeremy Stretch
4dae781be0 Change VLANGroup site to scope (GFK) 2021-03-09 16:18:59 -05:00
Jeremy Stretch
c6641ec1de Closes #3451: Add pre-/post-change snapshots to webhooks 2021-03-09 13:03:44 -05:00
Jeremy Stretch
c083b862a7 Improve JSON output formatting of webhook receiver 2021-03-09 12:03:56 -05:00
Jeremy Stretch
6ffadb501b Closes #5610: Add REST API endpoint for webhooks 2021-03-09 09:22:58 -05:00
Jeremy Stretch
38ded66c4e Closes #5608: Add REST API endpoint for custom links 2021-03-08 20:57:44 -05:00
Jeremy Stretch
14bc3a3cf8 Closes #5938: Deprecate support for Python 3.6 2021-03-08 13:36:56 -05:00
Jeremy Stretch
79b19821f6 Closes #5892: Introduce SiteGroup model (#5937)
* Initial work on #5892

* Add site group selection to object edit forms

* Add documentation for site groups

* Changelog for #5892

* Finish application of site groups to config context
2021-03-08 13:28:53 -05:00
Jeremy Stretch
1c66733b8a Merge pull request #5930 from netbox-community/1519-interface-parent
Closes #1519: Enable parent assignment for interfaces
2021-03-05 16:51:39 -05:00
Jeremy Stretch
2ef85ea195 Changelog for #1519 2021-03-05 16:25:39 -05:00
Jeremy Stretch
69a3d14b53 Add filter tests for interface parent and LAG 2021-03-05 16:20:46 -05:00
Jeremy Stretch
d74c07e1a2 Clean up parent/LAG interface form validation 2021-03-05 14:13:03 -05:00
Jeremy Stretch
e1a86139dc Add parent field to Interface 2021-03-05 13:49:41 -05:00
Jeremy Stretch
8e1fe6339e Rename parent attribute on CableTerminations to parent_object 2021-03-05 13:06:21 -05:00
Jeremy Stretch
7a5cf80412 Clean up extraneous imports 2021-03-05 12:48:53 -05:00
Jeremy Stretch
5406e8e2e7 Optimize background site/location updates 2021-03-05 09:56:47 -05:00
Jeremy Stretch
d3974c9f44 Merge pull request #5929 from netbox-community/4999-table-export
Closes #4999: Support for dynamic table-based exports
2021-03-05 09:30:06 -05:00
Jeremy Stretch
0015c8dfa2 Changelog for #4999 2021-03-05 09:15:27 -05:00
Jeremy Stretch
e703d9ff78 Introduce UtilizationColumn to render utilization graphs consistently 2021-03-04 20:58:43 -05:00
Jeremy Stretch
32501c96e5 Introduce MPTTColumn to represent MPTT models 2021-03-04 20:47:24 -05:00
Jeremy Stretch
6149ba6e36 Export only visible columns 2021-03-04 17:31:37 -05:00
Jeremy Stretch
0d44a320a0 Add tablib as a required package 2021-03-04 17:15:07 -05:00
Jeremy Stretch
a8a272b068 Enable table-based export 2021-03-04 16:58:56 -05:00
Jeremy Stretch
20a85c1ef2 Introduce TenantColumn to simplify representation 2021-03-04 16:07:55 -05:00
Jeremy Stretch
23b58ccbe8 Override value() on custom table columns 2021-03-04 15:56:12 -05:00
Jeremy Stretch
9c967ee3ea Fixes #5913: Improve change logging (#5924)
* Initial work on #5913
* Provide per-line diff highlighting
* BulkDeteView should delete objects individually to secure a pre-change snapshot
* Add changelog tests for bulk operations
2021-03-04 13:06:04 -05:00
Jeremy Stretch
f495addb1e Update documentation 2021-03-03 14:50:05 -05:00
Jeremy Stretch
d750b690e7 Closes #4971: Allow assigning devices to locations without a rack 2021-03-03 14:28:07 -05:00
Jeremy Stretch
fdb3e3f9a4 Closes #5895: Rename RackGroup to Location 2021-03-03 13:30:33 -05:00
Jeremy Stretch
a17018a875 #3648: Add missing mark_connected form fields 2021-03-03 10:20:35 -05:00
Jeremy Stretch
8de20fcd1f Closes #5375: Add 'speed' attribute to console port models 2021-03-03 10:20:08 -05:00
Jeremy Stretch
51120ccf31 Introduce generic object template 2021-03-02 15:58:33 -05:00
Jeremy Stretch
8dd7123923 Closes #3648: Mark cable termination models as connected without attaching a cable 2021-03-01 21:34:42 -05:00
Jeremy Stretch
6ed2e7b636 Closes #5894: Use primary keys when filtering object lists by related objects in the UI 2021-03-01 17:24:30 -05:00
Jeremy Stretch
07e6abdac4 Closes #5901: Add 'created' and 'last_updated' fields to device component models 2021-03-01 15:42:39 -05:00
Jeremy Stretch
1ddc1a6781 Closes #5451: Add support for multiple-selection custom fields 2021-03-01 14:52:57 -05:00
Jeremy Stretch
3f216fa4a3 Remove unused CustomFieldModel class 2021-03-01 14:33:52 -05:00
Jeremy Stretch
4fb79e6e2a Changelog for #5401 2021-03-01 14:17:25 -05:00
Jeremy Stretch
fdda30704b Add custom fields section to interface edit template 2021-03-01 14:12:09 -05:00
Jeremy Stretch
5418520252 Add custom field support to component bulk edit forms 2021-03-01 13:57:57 -05:00
Jeremy Stretch
9d526b0907 Add custom field support to component creation forms 2021-03-01 13:42:31 -05:00
Jeremy Stretch
9db492eb07 Add custom field support to ComponentCreateForm 2021-03-01 13:37:53 -05:00
Jeremy Stretch
d6ee4d58ba Add custom field support for device component models 2021-03-01 13:07:25 -05:00
Jeremy Stretch
6a9b50f95d Closes #5873: Use numeric IDs in all object URLs 2021-02-26 17:23:23 -05:00
Jeremy Stretch
1dcd857ca6 Closes #5370: Extend custom field support to organizational models 2021-02-26 16:25:37 -05:00
Jeremy Stretch
7e6cb9d186 Closes #1638: Migrate all primary keys to 64-bit integers 2021-02-26 16:12:52 -05:00
Jeremy Stretch
cf78307577 Update organizational models to use custom field forms 2021-02-26 11:25:23 -05:00
Jeremy Stretch
12fbd34962 Introduce NestedGroupModelSerializer 2021-02-25 17:15:42 -05:00
Jeremy Stretch
ed059d80d6 Introduce OrganizationalModelSerializer 2021-02-25 16:44:16 -05:00
Jeremy Stretch
664a39911c Add custom field support for organizational models to API serializers 2021-02-25 15:58:13 -05:00
Jeremy Stretch
d6cf385a3c Update CircuitTermination fields to use SelectSpeedWidget 2021-02-25 14:12:34 -05:00
Jeremy Stretch
cef8204f40 Merge tags fields with primary object fields in form display 2021-02-25 14:03:14 -05:00
Jeremy Stretch
42e82f0ead Update object edit template to use fieldsets where possible 2021-02-25 13:51:48 -05:00
Jeremy Stretch
2a517cde9f Add support for form fieldsets 2021-02-25 13:08:02 -05:00
Jeremy Stretch
992657cbe0 Introduce SelectSpeedWidget 2021-02-25 11:47:26 -05:00
Jeremy Stretch
3208c8317d Switch docs to mkdocs-material 2021-02-24 21:21:17 -05:00
Jeremy Stretch
bec7ea7072 Standardize model types based on function 2021-02-24 21:01:16 -05:00
Jeremy Stretch
0a6ebdee48 Upgrade Django to 3.2b1 2021-02-24 19:00:14 -05:00
412 changed files with 17453 additions and 8735 deletions

1
.github/FUNDING.yml vendored
View File

@@ -1 +0,0 @@
github: [jeremystretch]

View File

@@ -1,25 +1,29 @@
--- ---
name: 🐛 Bug Report name: 🐛 Bug Report
about: Report a reproducible bug in the current release of NetBox description: Report a reproducible bug in the current release of NetBox
labels: ["type: bug"] labels: ["type: bug"]
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: "**NOTE:** This form is only for reporting _reproducible bugs_ in a value: >
current NetBox installation. If you're having trouble with installation or just **NOTE:** This form is only for reporting _reproducible bugs_ in a current NetBox
looking for assistance with using NetBox, please visit our installation. If you're having trouble with installation or just looking for
[discussion forum](https://github.com/netbox-community/netbox/discussions) instead." assistance with using NetBox, please visit our
[discussion forum](https://github.com/netbox-community/netbox/discussions) instead.
- type: input - type: input
attributes: attributes:
label: NetBox version label: NetBox version
description: "What version of NetBox are you currently running?" description: >
placeholder: v2.10.4 What version of NetBox are you currently running? (If you don't have access to the most
recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/)
before opening a bug report to see if your issue has already been addressed.)
placeholder: v2.11.5
validations: validations:
required: true required: true
- type: dropdown - type: dropdown
attributes: attributes:
label: Python version label: Python version
description: "What version of Python are you currently running?" description: What version of Python are you currently running?
options: options:
- 3.6 - 3.6
- 3.7 - 3.7
@@ -30,12 +34,14 @@ body:
- type: textarea - type: textarea
attributes: attributes:
label: Steps to Reproduce label: Steps to Reproduce
description: "Describe in detail the exact steps that someone else can take to description: >
reproduce this bug using the current stable release of NetBox. Begin with the Describe in detail the exact steps that someone else can take to
creation of any necessary database objects and call out every operation being reproduce this bug using the current stable release of NetBox. Begin with the
performed explicitly. If reporting a bug in the REST API, be sure to reconstruct creation of any necessary database objects and call out every operation being
the raw HTTP request(s) being made: Don't rely on a client library such as performed explicitly. If reporting a bug in the REST API, be sure to reconstruct
pynetbox." the raw HTTP request(s) being made: Don't rely on a client library such as
pynetbox. Additionally, **do not rely on the demo instance** for reproducing
suspected bugs, as its data is prone to modification or deletion at any time.
placeholder: | placeholder: |
1. Click on "create widget" 1. Click on "create widget"
2. Set foo to 12 and bar to G 2. Set foo to 12 and bar to G
@@ -45,19 +51,14 @@ body:
- type: textarea - type: textarea
attributes: attributes:
label: Expected Behavior label: Expected Behavior
description: "What did you expect to happen?" description: What did you expect to happen?
placeholder: "A new widget should have been created with the specified attributes" placeholder: A new widget should have been created with the specified attributes
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: Observed Behavior label: Observed Behavior
description: "What happened instead?" description: What happened instead?
placeholder: "A TypeError exception was raised" placeholder: A TypeError exception was raised
validations: validations:
required: true required: true
- type: markdown
attributes:
value: |
### Additional information
You can use the space below to provide any additional information or to attach files.

View File

@@ -3,7 +3,10 @@ blank_issues_enabled: false
contact_links: contact_links:
- name: 📖 Contributing Policy - name: 📖 Contributing Policy
url: https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md url: https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md
about: Please read through our contributing policy before opening an issue or pull request about: "Please read through our contributing policy before opening an issue or pull request"
- name: 💬 Discussion Group - name: Discussion
url: https://groups.google.com/g/netbox-discuss url: https://github.com/netbox-community/netbox/discussions
about: Join our discussion group for assistance with installation issues and other problems about: "If you're just looking for help, try starting a discussion instead"
- name: 💬 Community Slack
url: https://netdev.chat/
about: "Join #netbox on the NetDev Community Slack for assistance with installation issues and other problems"

View File

@@ -1,6 +1,6 @@
--- ---
name: 📖 Documentation Change name: 📖 Documentation Change
about: Suggest an addition or modification to the NetBox documentation description: Suggest an addition or modification to the NetBox documentation
labels: ["type: documentation"] labels: ["type: documentation"]
body: body:
- type: dropdown - type: dropdown
@@ -30,11 +30,6 @@ body:
- type: textarea - type: textarea
attributes: attributes:
label: Proposed Changes label: Proposed Changes
description: "Describe the proposed changes and why they are necessary" description: Describe the proposed changes and why they are necessary.
validations: validations:
required: true required: true
- type: markdown
attributes:
value: |
### Additional information
You can use the space below to provide any additional information or to attach files.

View File

@@ -1,19 +1,20 @@
--- ---
name: ✨ Feature Request name: ✨ Feature Request
about: Propose a new NetBox feature or enhancement description: Propose a new NetBox feature or enhancement
labels: ["type: feature"] labels: ["type: feature"]
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: "**NOTE:** This form is only for submitting well-formed proposals to extend or value: >
modify NetBox in some way. If you're trying to solve a problem but can't figure out how, **NOTE:** This form is only for submitting well-formed proposals to extend or modify
or if you still need time to work on the details of a proposed new feature, please start NetBox in some way. If you're trying to solve a problem but can't figure out how, or if
a [discussion](https://github.com/netbox-community/netbox/discussions) instead." you still need time to work on the details of a proposed new feature, please start a
[discussion](https://github.com/netbox-community/netbox/discussions) instead.
- type: input - type: input
attributes: attributes:
label: NetBox version label: NetBox version
description: "What version of NetBox are you currently running?" description: What version of NetBox are you currently running?
placeholder: v2.10.4 placeholder: v2.11.5
validations: validations:
required: true required: true
- type: dropdown - type: dropdown
@@ -28,31 +29,29 @@ body:
- type: textarea - type: textarea
attributes: attributes:
label: Proposed functionality label: Proposed functionality
description: "Describe in detail the new feature or behavior you'd like to propose. description: >
Include any specific changes to work flows, data models, or the user interface." Describe in detail the new feature or behavior you'd like to propose. Include any specific
changes to work flows, data models, or the user interface.
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: Use case label: Use case
description: "Explain how adding this functionality would benefit NetBox users. What description: >
need does it address?" Explain how adding this functionality would benefit NetBox users. What need does it address?
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: Database changes label: Database changes
description: "Note any changes to the database schema necessary to support the new description: >
feature. For example, does the proposal require adding a new model or field? (Not Note any changes to the database schema necessary to support the new feature. For example,
all new features require database changes.)" does the proposal require adding a new model or field? (Not all new features require database
changes.)
- type: textarea - type: textarea
attributes: attributes:
label: External dependencies label: External dependencies
description: "List any new dependencies on external libraries or services that this description: >
new feature would introduce. For example, does the proposal require the installation List any new dependencies on external libraries or services that this new feature would
of a new Python package? (Not all new features introduce new dependencies.)" introduce. For example, does the proposal require the installation of a new Python package?
- type: markdown (Not all new features introduce new dependencies.)
attributes:
value: |
### Additional information
You can use the space below to provide any additional information or to attach files.

View File

@@ -1,27 +1,24 @@
--- ---
name: 🏡 Housekeeping name: 🏡 Housekeeping
about: A change pertaining to the codebase itself (developers only) description: A change pertaining to the codebase itself (developers only)
labels: ["type: housekeeping"] labels: ["type: housekeeping"]
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: "**NOTE:** This template is for use by maintainers only. Please do not submit value: >
an issue using this template unless you have been specifically asked to do so." **NOTE:** This template is for use by maintainers only. Please do not submit
an issue using this template unless you have been specifically asked to do so.
- type: textarea - type: textarea
attributes: attributes:
label: Proposed Changes label: Proposed Changes
description: "Describe in detail the new feature or behavior you'd like to propose. description: >
Include any specific changes to work flows, data models, or the user interface." Describe in detail the new feature or behavior you'd like to propose.
Include any specific changes to work flows, data models, or the user interface.
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: Justification label: Justification
description: "Please provide justification for the proposed change(s)." description: Please provide justification for the proposed change(s).
validations: validations:
required: true required: true
- type: markdown
attributes:
value: |
### Additional information
You can use the space below to provide any additional information or to attach files.

30
.github/stale.yml vendored
View File

@@ -1,30 +0,0 @@
# Configuration for Stale (https://github.com/apps/stale)
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 45
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 15
# Issues with these labels will never be considered stale
exemptLabels:
- "status: accepted"
- "status: blocked"
- "status: needs milestone"
# Label to use when marking an issue as stale
staleLabel: "pending closure"
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. NetBox
is governed by a small group of core maintainers which means not all opened
issues may receive direct feedback. Please see our [contributing guide](https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md).
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: >
This issue has been automatically closed due to lack of activity. In an
effort to reduce noise, please do not comment any further. Note that the
core maintainers may elect to reopen this issue at a later date if deemed
necessary.

35
.github/workflows/stale.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
# close-stale-issues (https://github.com/marketplace/actions/close-stale-issues)
name: 'Close stale issues/PRs'
on:
schedule:
- cron: '0 4 * * *'
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v3
with:
close-issue-message: >
This issue has been automatically closed due to lack of activity. In an
effort to reduce noise, please do not comment any further. Note that the
core maintainers may elect to reopen this issue at a later date if deemed
necessary.
close-pr-message: >
This PR has been automatically closed due to lack of activity.
days-before-stale: 60
days-before-close: 30
exempt-issue-labels: 'status: accepted,status: blocked,status: needs milestone'
operations-per-run: 100
remove-stale-when-updated: false
stale-issue-label: 'pending closure'
stale-issue-message: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. NetBox
is governed by a small group of core maintainers which means not all opened
issues may receive direct feedback. Please see our [contributing guide](https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md).
stale-pr-label: 'pending closure'
stale-pr-message: >
This PR has been automatically marked as stale because it has not had
recent activity. It will be closed automatically if no further action is
taken.

View File

@@ -1,4 +1,6 @@
![NetBox](docs/netbox_logo.svg "NetBox logo") <div align="center">
<img src="https://raw.githubusercontent.com/netbox-community/netbox/develop/docs/netbox_logo.svg" width="400" alt="NetBox logo" />
</div>
NetBox is an IP address management (IPAM) and data center infrastructure NetBox is an IP address management (IPAM) and data center infrastructure
management (DCIM) tool. Initially conceived by the network engineering team at management (DCIM) tool. Initially conceived by the network engineering team at
@@ -10,7 +12,23 @@ NetBox runs as a web application atop the [Django](https://www.djangoproject.com
Python framework with a [PostgreSQL](https://www.postgresql.org/) database. For a Python framework with a [PostgreSQL](https://www.postgresql.org/) database. For a
complete list of requirements, see `requirements.txt`. The code is available [on GitHub](https://github.com/netbox-community/netbox). complete list of requirements, see `requirements.txt`. The code is available [on GitHub](https://github.com/netbox-community/netbox).
The complete documentation for NetBox can be found at [Read the Docs](https://netbox.readthedocs.io/en/stable/). The complete documentation for NetBox can be found at [Read the Docs](https://netbox.readthedocs.io/en/stable/). A public demo instance is available at https://demo.netbox.dev.
| | status |
|-------------|------------|
| **master** | ![Build status](https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=master) |
| **develop** | ![Build status](https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=develop) |
<div align="center">
<h4>Thank you to our sponsors!</h4>
[![DigitalOcean](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/digitalocean.png)](https://try.digitalocean.com/developer-cloud)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
[![NS1](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/ns1.png)](https://ns1.com/)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
[![Stellar Technologies](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/stellar.png)](https://stellar.tech/)
</div>
### Discussion ### Discussion
@@ -18,33 +36,14 @@ The complete documentation for NetBox can be found at [Read the Docs](https://ne
* [Slack](https://slack.netbox.dev/) - Real-time chat hosted by the NetDev Community; best for unstructured discussion or just hanging out * [Slack](https://slack.netbox.dev/) - Real-time chat hosted by the NetDev Community; best for unstructured discussion or just hanging out
* [Google Group](https://groups.google.com/g/netbox-discuss) - Legacy mailing list; slowly being replaced by GitHub discussions * [Google Group](https://groups.google.com/g/netbox-discuss) - Legacy mailing list; slowly being replaced by GitHub discussions
### Build Status ### Installation
| | status |
|-------------|------------|
| **master** | ![Build status](https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=master) |
| **develop** | ![Build status](https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=develop) |
### Screenshots
![Screenshot of main page](docs/media/screenshot1.png "Main page")
---
![Screenshot of rack elevation](docs/media/screenshot2.png "Rack elevation")
---
![Screenshot of prefix hierarchy](docs/media/screenshot3.png "Prefix hierarchy")
## Installation
Please see [the documentation](https://netbox.readthedocs.io/en/stable/) for Please see [the documentation](https://netbox.readthedocs.io/en/stable/) for
instructions on installing NetBox. To upgrade NetBox, please download the instructions on installing NetBox. To upgrade NetBox, please download the
[latest release](https://github.com/netbox-community/netbox/releases) and [latest release](https://github.com/netbox-community/netbox/releases) and
run `upgrade.sh`. run `upgrade.sh`.
## Providing Feedback ### Providing Feedback
The best platform for general feedback, assistance, and other discussion is our The best platform for general feedback, assistance, and other discussion is our
[GitHub discussions](https://github.com/netbox-community/netbox/discussions). [GitHub discussions](https://github.com/netbox-community/netbox/discussions).
@@ -54,7 +53,15 @@ the [appropriate template](https://github.com/netbox-community/netbox/issues/new
If you are interested in contributing to the development of NetBox, please read If you are interested in contributing to the development of NetBox, please read
our [contributing guide](CONTRIBUTING.md) prior to beginning any work. our [contributing guide](CONTRIBUTING.md) prior to beginning any work.
## Related projects ### Screenshots
![Screenshot of main page](docs/media/screenshot1.png "Main page")
![Screenshot of rack elevation](docs/media/screenshot2.png "Rack elevation")
![Screenshot of prefix hierarchy](docs/media/screenshot3.png "Prefix hierarchy")
### Related projects
Please see [our wiki](https://github.com/netbox-community/netbox/wiki/Community-Contributions) Please see [our wiki](https://github.com/netbox-community/netbox/wiki/Community-Contributions)
for a list of relevant community projects. for a list of relevant community projects.

View File

@@ -93,3 +93,7 @@ redis
# SVG image rendering (used for rack elevations) # SVG image rendering (used for rack elevations)
# https://github.com/mozman/svgwrite # https://github.com/mozman/svgwrite
svgwrite svgwrite
# Tabular dataset library (for table-based exports)
# https://github.com/jazzband/tablib
tablib

View File

@@ -6,7 +6,7 @@ If a change is made to any of the objects returned by the query within that time
## Invalidating Cached Data ## Invalidating Cached Data
Although caching is performed automatically and rarely requires administrative intervention, NetBox provides the `invalidate` management command to force invalidation of cached results. This command can reference a specific object my its type and numeric ID: Although caching is performed automatically and rarely requires administrative intervention, NetBox provides the `invalidate` management command to force invalidation of cached results. This command can reference a specific object by its type and numeric ID:
```no-highlight ```no-highlight
$ python netbox/manage.py invalidate dcim.Device.34 $ python netbox/manage.py invalidate dcim.Device.34

View File

@@ -1,6 +1,6 @@
# Change Logging # Change Logging
Every time an object in NetBox is created, updated, or deleted, a serialized copy of that object is saved to the database, along with meta data including the current time and the user associated with the change. These records form a persistent record of changes both for each individual object as well as NetBox as a whole. The global change log can be viewed by navigating to Other > Change Log. Every time an object in NetBox is created, updated, or deleted, a serialized copy of that object taken both before and after the change is saved to the database, along with meta data including the current time and the user associated with the change. These records form a persistent record of changes both for each individual object as well as NetBox as a whole. The global change log can be viewed by navigating to Other > Change Log.
A serialized representation of the instance being modified is included in JSON format. This is similar to how objects are conveyed within the REST API, but does not include any nested representations. For instance, the `tenant` field of a site will record only the tenant's ID, not a representation of the tenant. A serialized representation of the instance being modified is included in JSON format. This is similar to how objects are conveyed within the REST API, but does not include any nested representations. For instance, the `tenant` field of a site will record only the tenant's ID, not a representation of the tenant.

View File

@@ -16,6 +16,7 @@ Custom fields must be created through the admin UI under Extras > Custom Fields.
* Date: A date in ISO 8601 format (YYYY-MM-DD) * Date: A date in ISO 8601 format (YYYY-MM-DD)
* URL: This will be presented as a link in the web UI * URL: This will be presented as a link in the web UI
* Selection: A selection of one of several pre-defined custom choices * Selection: A selection of one of several pre-defined custom choices
* Multiple selection: A selection field which supports the assignment of multiple values
Each custom field must have a name; this should be a simple database-friendly string, e.g. `tps_report`. You may also assign a corresponding human-friendly label (e.g. "TPS report"); the label will be displayed on web forms. A weight is also required: Higher-weight fields will be ordered lower within a form. (The default weight is 100.) If a description is provided, it will appear beneath the field in a form. Each custom field must have a name; this should be a simple database-friendly string, e.g. `tps_report`. You may also assign a corresponding human-friendly label (e.g. "TPS report"); the label will be displayed on web forms. A weight is also required: Higher-weight fields will be ordered lower within a form. (The default weight is 100.) If a description is provided, it will appear beneath the field in a form.
@@ -37,7 +38,13 @@ NetBox supports limited custom validation for custom field values. Following are
Each custom selection field must have at least two choices. These are specified as a comma-separated list. Choices appear in forms in the order they are listed. Note that choice values are saved exactly as they appear, so it's best to avoid superfluous punctuation or symbols where possible. Each custom selection field must have at least two choices. These are specified as a comma-separated list. Choices appear in forms in the order they are listed. Note that choice values are saved exactly as they appear, so it's best to avoid superfluous punctuation or symbols where possible.
If a default value is specified for a selection field, it must exactly match one of the provided choices. If a default value is specified for a selection field, it must exactly match one of the provided choices. The value of a multiple selection field will always return a list, even if only one value is selected.
## Custom Fields in Templates
Several features within NetBox, such as export templates and webhooks, utilize Jinja2 templating. For convenience, objects which support custom field assignment expose custom field data through the `cf` property. This is a bit cleaner than accessing custom field data through the actual field (`custom_field_data`).
For example, a custom field named `foo123` on the Site model is accessible on an instance as `{{ site.cf.foo123 }}`.
## Custom Fields and the REST API ## Custom Fields and the REST API

View File

@@ -170,18 +170,13 @@ Similar to `ChoiceVar`, but allows for the selection of multiple choices.
A particular object within NetBox. Each ObjectVar must specify a particular model, and allows the user to select one of the available instances. ObjectVar accepts several arguments, listed below. A particular object within NetBox. Each ObjectVar must specify a particular model, and allows the user to select one of the available instances. ObjectVar accepts several arguments, listed below.
* `model` - The model class * `model` - The model class
* `display_field` - The name of the REST API object field to display in the selection list (default: `'name'`) * `display_field` - The name of the REST API object field to display in the selection list (default: `'display'`)
* `query_params` - A dictionary of query parameters to use when retrieving available options (optional) * `query_params` - A dictionary of query parameters to use when retrieving available options (optional)
* `null_option` - A label representing a "null" or empty choice (optional) * `null_option` - A label representing a "null" or empty choice (optional)
The `display_field` argument is useful when referencing a model which does not have a `name` field. For example, when displaying a list of device types, you would likely use the `model` field: !!! warning
The `display_field` parameter is now deprecated, and will be removed in NetBox v3.0. All ObjectVar instances will
```python instead use the new standard `display` field for all serializers (introduced in NetBox v2.11).
device_type = ObjectVar(
model=DeviceType,
display_field='model'
)
```
To limit the selections available within the list, additional query parameters can be passed as the `query_params` dictionary. For example, to show only devices with an "active" status: To limit the selections available within the list, additional query parameters can be passed as the `query_params` dictionary. For example, to show only devices with an "active" status:

View File

@@ -2,7 +2,10 @@
NetBox allows users to define custom templates that can be used when exporting objects. To create an export template, navigate to Extras > Export Templates under the admin interface. NetBox allows users to define custom templates that can be used when exporting objects. To create an export template, navigate to Extras > Export Templates under the admin interface.
Each export template is associated with a certain type of object. For instance, if you create an export template for VLANs, your custom template will appear under the "Export" button on the VLANs list. Each export template is associated with a certain type of object. For instance, if you create an export template for VLANs, your custom template will appear under the "Export" button on the VLANs list. Each export template must have a name, and may optionally designate a specific export [MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) and/or file extension.
!!! note
The name `table` is reserved for internal use.
Export templates must be written in [Jinja2](https://jinja.palletsprojects.com/). Export templates must be written in [Jinja2](https://jinja.palletsprojects.com/).
@@ -18,6 +21,16 @@ Height: {{ rack.u_height }}U
To access custom fields of an object within a template, use the `cf` attribute. For example, `{{ obj.cf.color }}` will return the value (if any) for a custom field named `color` on `obj`. To access custom fields of an object within a template, use the `cf` attribute. For example, `{{ obj.cf.color }}` will return the value (if any) for a custom field named `color` on `obj`.
If you need to use the config context data in an export template, you'll should use the function `get_config_context` to get all the config context data. For example:
```
{% for server in queryset %}
{% set data = server.get_config_context() %}
{{ data.syslog }}
{% endfor %}
```
The `as_attachment` attribute of an export template controls its behavior when rendered. If true, the rendered content will be returned to the user as a downloadable file. If false, it will be displayed within the browser. (This may be handy e.g. for generating HTML content.)
A MIME type and file extension can optionally be defined for each export template. The default MIME type is `text/plain`. A MIME type and file extension can optionally be defined for each export template. The default MIME type is `text/plain`.
## Example ## Example

View File

@@ -0,0 +1,5 @@
# Journaling
All primary objects in NetBox support journaling. A journal is a collection of human-generated notes and comments about an object maintained for historical context. It supplements NetBox's change log to provide additional information about why changes have been made or to convey events which occur outside NetBox. Unlike the change log, in which records typically expire after a configurable period of time, journal entries persist for the life of their associated object.
Each journal entry has a selectable kind (info, success, warning, or danger) and a user-populated `comments` field. Each entry automatically records the date, time, and associated user upon being created.

View File

@@ -2,6 +2,13 @@
NetBox supports integration with the [NAPALM automation](https://napalm-automation.net/) library. NAPALM allows NetBox to serve a proxy for operational data, fetching live data from network devices and returning it to a requester via its REST API. Note that NetBox does not store any NAPALM data locally. NetBox supports integration with the [NAPALM automation](https://napalm-automation.net/) library. NAPALM allows NetBox to serve a proxy for operational data, fetching live data from network devices and returning it to a requester via its REST API. Note that NetBox does not store any NAPALM data locally.
The NetBox UI will display tabs for status, LLDP neighbors, and configuration under the device view if the following conditions are met:
* Device status is "Active"
* A primary IP has been assigned to the device
* A platform with a NAPALM driver has been assigned
* The authenticated user has the `dcim.napalm_read_device` permission
!!! note !!! note
To enable this integration, the NAPALM library must be installed. See [installation steps](../../installation/3-netbox/#napalm) for more information. To enable this integration, the NAPALM library must be installed. See [installation steps](../../installation/3-netbox/#napalm) for more information.

View File

@@ -80,7 +80,7 @@ class DeviceConnectionsReport(Report):
self.log_success(device) self.log_success(device)
``` ```
As you can see, reports are completely customizable. Validation logic can be as simple or as complex as needed. As you can see, reports are completely customizable. Validation logic can be as simple or as complex as needed. Also note that the `description` attribute support markdown syntax. It will be rendered in the report list page.
!!! warning !!! 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. 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.
@@ -93,7 +93,7 @@ The following methods are available to log results within a report:
* log_warning(object, message) * log_warning(object, message)
* log_failure(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. 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. Log messages also support using markdown syntax and will be rendered on the report result page.
To perform additional tasks, such as sending an email or calling a webhook, after a report has been run, extend the `post_run()` method. The status of the report is available as `self.failed` and the results object is `self.result`. To perform additional tasks, such as sending an email or calling a webhook, after a report has been run, extend the `post_run()` method. The status of the report is available as `self.failed` and the results object is `self.result`.

View File

@@ -38,7 +38,8 @@ The following data is available as context for Jinja2 templates:
* `timestamp` - The time at which the event occurred (in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format). * `timestamp` - The time at which the event occurred (in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format).
* `username` - The name of the user account associated with the change. * `username` - The name of the user account associated with the change.
* `request_id` - The unique request ID. This may be used to correlate multiple changes associated with a single request. * `request_id` - The unique request ID. This may be used to correlate multiple changes associated with a single request.
* `data` - A serialized representation of the object _after_ the change was made. This is typically equivalent to the model's representation in NetBox's REST API. * `data` - A detailed representation of the object in its current state. This is typically equivalent to the model's representation in NetBox's REST API.
* `snapshots` - Minimal "snapshots" of the object state both before and after the change was made; provided ass a dictionary with keys named `prechange` and `postchange`. These are not as extensive as the fully serialized representation, but contain enough information to convey what has changed.
### Default Request Body ### Default Request Body
@@ -47,7 +48,7 @@ If no body template is specified, the request body will be populated with a JSON
```no-highlight ```no-highlight
{ {
"event": "created", "event": "created",
"timestamp": "2020-02-25 15:10:26.010582+00:00", "timestamp": "2021-03-09 17:55:33.968016+00:00",
"model": "site", "model": "site",
"username": "jstretch", "username": "jstretch",
"request_id": "fdbca812-3142-4783-b364-2e2bd5c16c6a", "request_id": "fdbca812-3142-4783-b364-2e2bd5c16c6a",
@@ -62,6 +63,17 @@ If no body template is specified, the request body will be populated with a JSON
}, },
"region": null, "region": null,
... ...
},
"snapshots": {
"prechange": null,
"postchange": {
"created": "2021-03-09",
"last_updated": "2021-03-09T17:55:33.851Z",
"name": "Site 1",
"slug": "site-1",
"status": "active",
...
}
} }
} }
``` ```

View File

@@ -10,7 +10,7 @@ NetBox v2.9 introduced a new object-based permissions framework, which replace's
| ----------- | ----------- | | ----------- | ----------- |
| `{"status": "active"}` | Status is active | | `{"status": "active"}` | Status is active |
| `{"status__in": ["planned", "reserved"]}` | Status is active **OR** reserved | | `{"status__in": ["planned", "reserved"]}` | Status is active **OR** reserved |
| `{"status": "active", "role": "testing"}` | Status is active **OR** role is testing | | `{"status": "active", "role": "testing"}` | Status is active **AND** role is testing |
| `{"name__startswith": "Foo"}` | Name starts with "Foo" (case-sensitive) | | `{"name__startswith": "Foo"}` | Name starts with "Foo" (case-sensitive) |
| `{"name__iendswith": "bar"}` | Name ends with "bar" (case-insensitive) | | `{"name__iendswith": "bar"}` | Name ends with "bar" (case-insensitive) |
| `{"vid__gte": 100, "vid__lt": 200}` | VLAN ID is greater than or equal to 100 **AND** less than 200 | | `{"vid__gte": 100, "vid__lt": 200}` | VLAN ID is greater than or equal to 100 **AND** less than 200 |

View File

@@ -515,6 +515,14 @@ The file path to the location where custom scripts will be kept. By default, thi
--- ---
## SESSION_COOKIE_NAME
Default: `sessionid`
The name used for the session cookie. See the [Django documentation](https://docs.djangoproject.com/en/stable/ref/settings/#session-cookie-name) for more detail.
---
## SESSION_FILE_PATH ## SESSION_FILE_PATH
Default: None Default: None

View File

@@ -66,6 +66,7 @@ Redis is configured using a configuration setting similar to `DATABASE` and thes
* `PASSWORD` - Redis password (if set) * `PASSWORD` - Redis password (if set)
* `DATABASE` - Numeric database ID * `DATABASE` - Numeric database ID
* `SSL` - Use SSL connection to Redis * `SSL` - Use SSL connection to Redis
* `INSECURE_SKIP_TLS_VERIFY` - Set to `True` to **disable** TLS certificate verification (not recommended)
An example configuration is provided below: An example configuration is provided below:

View File

@@ -1,6 +1,7 @@
# Circuits # Circuits
{!docs/models/circuits/provider.md!} {!docs/models/circuits/provider.md!}
{!docs/models/circuits/providernetwork.md!}
--- ---

View File

@@ -8,6 +8,8 @@
## Device Components ## Device Components
Device components represent discrete objects within a device which are used to terminate cables, house child devices, or track resources.
{!docs/models/dcim/consoleport.md!} {!docs/models/dcim/consoleport.md!}
{!docs/models/dcim/consoleserverport.md!} {!docs/models/dcim/consoleserverport.md!}
{!docs/models/dcim/powerport.md!} {!docs/models/dcim/powerport.md!}

View File

@@ -1,11 +1,12 @@
# Sites and Racks # Sites and Racks
{!docs/models/dcim/site.md!}
{!docs/models/dcim/region.md!} {!docs/models/dcim/region.md!}
{!docs/models/dcim/sitegroup.md!}
{!docs/models/dcim/site.md!}
{!docs/models/dcim/location.md!}
--- ---
{!docs/models/dcim/rack.md!} {!docs/models/dcim/rack.md!}
{!docs/models/dcim/rackgroup.md!}
{!docs/models/dcim/rackrole.md!} {!docs/models/dcim/rackrole.md!}
{!docs/models/dcim/rackreservation.md!} {!docs/models/dcim/rackreservation.md!}

View File

@@ -0,0 +1,98 @@
# NetBox Models
## Model Types
A NetBox model represents a discrete object type such as a device or IP address. Each model is defined as a Python class and has its own SQL table. All NetBox data models can be categorized by type.
The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/) framework can be used to reference models within the database. A ContentType instance references a model by its `app_label` and `name`: For example, the Site model is referred to as `dcim.site`. The content type combined with an object's primary key form a globally unique identifier for the object (e.g. `dcim.site:123`).
### Features Matrix
* [Change logging](../additional-features/change-logging.md) - Changes to these objects are automatically recorded in the change log
* [Webhooks](../additional-features/webhooks.md) - NetBox is capable of generating outgoing webhooks for these objects
* [Custom fields](../additional-features/custom-fields.md) - These models support the addition of user-defined fields
* [Export templates](../additional-features/export-templates.md) - Users can create custom export templates for these models
* [Tagging](../models/extras/tag.md) - The models can be tagged with user-defined tags
* [Journaling](../additional-features/journaling.md) - These models support persistent historical commentary
* Nesting - These models can be nested recursively to create a hierarchy
| Type | Change Logging | Webhooks | Custom Fields | Export Templates | Tags | Journaling | Nesting |
| ------------------ | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- |
| Primary | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | |
| Organizational | :material-check: | :material-check: | :material-check: | :material-check: | | | |
| Nested Group | :material-check: | :material-check: | :material-check: | :material-check: | | | :material-check: |
| Component | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | | |
| Component Template | :material-check: | :material-check: | :material-check: | | | | |
## Models Index
### Primary Models
* [circuits.Circuit](../models/circuits/circuit.md)
* [circuits.Provider](../models/circuits/provider.md)
* [circuits.ProviderNetwork](../models/circuits/providernetwork.md)
* [dcim.Cable](../models/dcim/cable.md)
* [dcim.Device](../models/dcim/device.md)
* [dcim.DeviceType](../models/dcim/devicetype.md)
* [dcim.PowerFeed](../models/dcim/powerfeed.md)
* [dcim.PowerPanel](../models/dcim/powerpanel.md)
* [dcim.Rack](../models/dcim/rack.md)
* [dcim.RackReservation](../models/dcim/rackreservation.md)
* [dcim.Site](../models/dcim/site.md)
* [dcim.VirtualChassis](../models/dcim/virtualchassis.md)
* [ipam.Aggregate](../models/ipam/aggregate.md)
* [ipam.IPAddress](../models/ipam/ipaddress.md)
* [ipam.Prefix](../models/ipam/prefix.md)
* [ipam.RouteTarget](../models/ipam/routetarget.md)
* [ipam.Service](../models/ipam/service.md)
* [ipam.VLAN](../models/ipam/vlan.md)
* [ipam.VRF](../models/ipam/vrf.md)
* [secrets.Secret](../models/secrets/secret.md)
* [tenancy.Tenant](../models/tenancy/tenant.md)
* [virtualization.Cluster](../models/virtualization/cluster.md)
* [virtualization.VirtualMachine](../models/virtualization/virtualmachine.md)
### Organizational Models
* [circuits.CircuitType](../models/circuits/circuittype.md)
* [dcim.DeviceRole](../models/dcim/devicerole.md)
* [dcim.Manufacturer](../models/dcim/manufacturer.md)
* [dcim.Platform](../models/dcim/platform.md)
* [dcim.RackRole](../models/dcim/rackrole.md)
* [ipam.RIR](../models/ipam/rir.md)
* [ipam.Role](../models/ipam/role.md)
* [ipam.VLANGroup](../models/ipam/vlangroup.md)
* [secrets.SecretRole](../models/secrets/secretrole.md)
* [virtualization.ClusterGroup](../models/virtualization/clustergroup.md)
* [virtualization.ClusterType](../models/virtualization/clustertype.md)
### Nested Group Models
* [dcim.Location](../models/dcim/location.md) (formerly RackGroup)
* [dcim.Region](../models/dcim/region.md)
* [dcim.SiteGroup](../models/dcim/sitegroup.md)
* [tenancy.TenantGroup](../models/tenancy/tenantgroup.md)
### Component Models
* [dcim.ConsolePort](../models/dcim/consoleport.md)
* [dcim.ConsoleServerPort](../models/dcim/consoleserverport.md)
* [dcim.DeviceBay](../models/dcim/devicebay.md)
* [dcim.FrontPort](../models/dcim/frontport.md)
* [dcim.Interface](../models/dcim/interface.md)
* [dcim.InventoryItem](../models/dcim/inventoryitem.md)
* [dcim.PowerOutlet](../models/dcim/poweroutlet.md)
* [dcim.PowerPort](../models/dcim/powerport.md)
* [dcim.RearPort](../models/dcim/rearport.md)
* [virtualization.VMInterface](../models/virtualization/vminterface.md)
### Component Template Models
* [dcim.ConsolePortTemplate](../models/dcim/consoleporttemplate.md)
* [dcim.ConsoleServerPortTemplate](../models/dcim/consoleserverporttemplate.md)
* [dcim.DeviceBayTemplate](../models/dcim/devicebaytemplate.md)
* [dcim.FrontPortTemplate](../models/dcim/frontporttemplate.md)
* [dcim.InterfaceTemplate](../models/dcim/interfacetemplate.md)
* [dcim.PowerOutletTemplate](../models/dcim/poweroutlettemplate.md)
* [dcim.PowerPortTemplate](../models/dcim/powerporttemplate.md)
* [dcim.RearPortTemplate](../models/dcim/rearporttemplate.md)

View File

@@ -70,7 +70,11 @@ Ensure that continuous integration testing on the `develop` branch is completing
### Update Version and Changelog ### Update Version and Changelog
Update the `VERSION` constant in `settings.py` to the new release version and annotate the current data in the release notes for the new version. Commit these changes to the `develop` branch. * Update the `VERSION` constant in `settings.py` to the new release version.
* Update the example version numbers in the feature request and bug report templates under `.github/ISSUE_TEMPLATES/`.
* Replace the "FUTURE" placeholder in the release notes with the current date.
Commit these changes to the `develop` branch.
### Submit a Pull Request ### Submit a Pull Request

View File

@@ -7,32 +7,31 @@ This section entails the installation and configuration of a local PostgreSQL da
## Installation ## Installation
#### Ubuntu === "Ubuntu"
Install the PostgreSQL server and client development libraries using `apt`. ```no-highlight
sudo apt update
sudo apt install -y postgresql libpq-dev
```
```no-highlight === "CentOS"
sudo apt update
sudo apt install -y postgresql libpq-dev
```
#### CentOS ```no-highlight
sudo yum install -y postgresql-server libpq-devel
sudo postgresql-setup --initdb
```
PostgreSQL 9.6 and later are available natively on CentOS 8.2. If using an earlier CentOS release, you may need to [install it from an RPM](https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/). !!! info
PostgreSQL 9.6 and later are available natively on CentOS 8.2. If using an earlier CentOS release, you may need to [install it from an RPM](https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/).
```no-highlight CentOS configures ident host-based authentication for PostgreSQL by default. Because NetBox will need to authenticate using a username and password, modify `/var/lib/pgsql/data/pg_hba.conf` to support MD5 authentication by changing `ident` to `md5` for the lines below:
sudo yum install -y postgresql-server libpq-devel
sudo postgresql-setup --initdb
```
CentOS configures ident host-based authentication for PostgreSQL by default. Because NetBox will need to authenticate using a username and password, modify `/var/lib/pgsql/data/pg_hba.conf` to support MD5 authentication by changing `ident` to `md5` for the lines below: ```no-highlight
host all all 127.0.0.1/32 md5
host all all ::1/128 md5
```
```no-highlight Once PostgreSQL has been installed, start the service and enable it to run at boot:
host all all 127.0.0.1/32 md5
host all all ::1/128 md5
```
Then, start the service and enable it to run at boot:
```no-highlight ```no-highlight
sudo systemctl start postgresql sudo systemctl start postgresql

View File

@@ -7,19 +7,19 @@
!!! note !!! note
NetBox v2.9.0 and later require Redis v4.0 or higher. If your distribution does not offer a recent enough release, you will need to build Redis from source. Please see [the Redis installation documentation](https://github.com/redis/redis) for further details. NetBox v2.9.0 and later require Redis v4.0 or higher. If your distribution does not offer a recent enough release, you will need to build Redis from source. Please see [the Redis installation documentation](https://github.com/redis/redis) for further details.
### Ubuntu === "Ubuntu"
```no-highlight ```no-highlight
sudo apt install -y redis-server sudo apt install -y redis-server
``` ```
### CentOS === "CentOS"
```no-highlight ```no-highlight
sudo yum install -y redis sudo yum install -y redis
sudo systemctl start redis sudo systemctl start redis
sudo systemctl enable redis sudo systemctl enable redis
``` ```
You may wish to modify the Redis configuration at `/etc/redis.conf` or `/etc/redis/redis.conf`, however in most cases the default configuration is sufficient. You may wish to modify the Redis configuration at `/etc/redis.conf` or `/etc/redis/redis.conf`, however in most cases the default configuration is sufficient.

View File

@@ -9,17 +9,17 @@ Begin by installing all system packages required by NetBox and its dependencies.
!!! note !!! note
NetBox v2.8.0 and later require Python 3.6, 3.7, or 3.8. NetBox v2.8.0 and later require Python 3.6, 3.7, or 3.8.
### Ubuntu === "Ubuntu"
```no-highlight ```no-highlight
sudo apt install -y python3 python3-pip python3-venv python3-dev build-essential libxml2-dev libxslt1-dev libffi-dev libpq-dev libssl-dev zlib1g-dev sudo apt install -y python3 python3-pip python3-venv python3-dev build-essential libxml2-dev libxslt1-dev libffi-dev libpq-dev libssl-dev zlib1g-dev
``` ```
### CentOS === "CentOS"
```no-highlight ```no-highlight
sudo yum install -y gcc python36 python36-devel python3-pip libxml2-devel libxslt-devel libffi-devel openssl-devel redhat-rpm-config sudo yum install -y gcc python36 python36-devel python3-pip libxml2-devel libxslt-devel libffi-devel openssl-devel redhat-rpm-config
``` ```
Before continuing with either platform, update pip (Python's package management tool) to its latest release: Before continuing with either platform, update pip (Python's package management tool) to its latest release:
@@ -57,17 +57,17 @@ sudo mkdir -p /opt/netbox/ && cd /opt/netbox/
If `git` is not already installed, install it: If `git` is not already installed, install it:
#### Ubuntu === "Ubuntu"
```no-highlight ```no-highlight
sudo apt install -y git sudo apt install -y git
``` ```
#### CentOS === "CentOS"
```no-highlight ```no-highlight
sudo yum install -y git sudo yum install -y git
``` ```
Next, clone the **master** branch of the NetBox GitHub repository into the current directory. (This branch always holds the current stable release.) Next, clone the **master** branch of the NetBox GitHub repository into the current directory. (This branch always holds the current stable release.)
@@ -89,20 +89,20 @@ Checking connectivity... done.
Create a system user account named `netbox`. We'll configure the WSGI and HTTP services to run under this account. We'll also assign this user ownership of the media directory. This ensures that NetBox will be able to save uploaded files. Create a system user account named `netbox`. We'll configure the WSGI and HTTP services to run under this account. We'll also assign this user ownership of the media directory. This ensures that NetBox will be able to save uploaded files.
#### Ubuntu === "Ubuntu"
``` ```
sudo adduser --system --group netbox sudo adduser --system --group netbox
sudo chown --recursive netbox /opt/netbox/netbox/media/ sudo chown --recursive netbox /opt/netbox/netbox/media/
``` ```
#### CentOS === "CentOS"
``` ```
sudo groupadd --system netbox sudo groupadd --system netbox
sudo adduser --system -g netbox netbox sudo adduser --system -g netbox netbox
sudo chown --recursive netbox /opt/netbox/netbox/media/ sudo chown --recursive netbox /opt/netbox/netbox/media/
``` ```
## Configuration ## Configuration
@@ -198,7 +198,7 @@ All Python packages required by NetBox are listed in `requirements.txt` and will
The [NAPALM automation](https://napalm-automation.net/) library allows NetBox to fetch live data from devices and return it to a requester via its REST API. The `NAPALM_USERNAME` and `NAPALM_PASSWORD` configuration parameters define the credentials to be used when connecting to a device. The [NAPALM automation](https://napalm-automation.net/) library allows NetBox to fetch live data from devices and return it to a requester via its REST API. The `NAPALM_USERNAME` and `NAPALM_PASSWORD` configuration parameters define the credentials to be used when connecting to a device.
```no-highlight ```no-highlight
sudo echo napalm >> /opt/netbox/local_requirements.txt sudo sh -c "echo 'napalm' >> /opt/netbox/local_requirements.txt"
``` ```
### Remote File Storage ### Remote File Storage
@@ -206,7 +206,7 @@ sudo echo napalm >> /opt/netbox/local_requirements.txt
By default, NetBox will use the local filesystem to store uploaded files. To use a remote filesystem, install the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) library and configure your [desired storage backend](../configuration/optional-settings.md#storage_backend) in `configuration.py`. By default, NetBox will use the local filesystem to store uploaded files. To use a remote filesystem, install the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) library and configure your [desired storage backend](../configuration/optional-settings.md#storage_backend) in `configuration.py`.
```no-highlight ```no-highlight
sudo echo django-storages >> /opt/netbox/local_requirements.txt sudo sh -c "echo 'django-storages' >> /opt/netbox/local_requirements.txt"
``` ```
## Run the Upgrade Script ## Run the Upgrade Script
@@ -264,7 +264,7 @@ Quit the server with CONTROL-C.
Next, connect to the name or IP of the server (as defined in `ALLOWED_HOSTS`) on port 8000; for example, <http://127.0.0.1:8000/>. You should be greeted with the NetBox home page. Next, connect to the name or IP of the server (as defined in `ALLOWED_HOSTS`) on port 8000; for example, <http://127.0.0.1:8000/>. You should be greeted with the NetBox home page.
!!! warning !!! danger
The development server is for development and testing purposes only. It is neither performant nor secure enough for production use. **Do not use it in production.** The development server is for development and testing purposes only. It is neither performant nor secure enough for production use. **Do not use it in production.**
!!! warning !!! warning

View File

@@ -30,7 +30,7 @@ pip3 install django-auth-ldap
Once installed, add the package to `local_requirements.txt` to ensure it is re-installed during future rebuilds of the virtual environment: Once installed, add the package to `local_requirements.txt` to ensure it is re-installed during future rebuilds of the virtual environment:
```no-highlight ```no-highlight
sudo echo django-auth-ldap >> /opt/netbox/local_requirements.txt sudo sh -c "echo 'django-auth-ldap' >> /opt/netbox/local_requirements.txt"
``` ```
## Configuration ## Configuration

View File

@@ -23,6 +23,9 @@ The video below demonstrates the installation of NetBox v2.10.3 on Ubuntu 20.04
| PostgreSQL | 9.6 | | PostgreSQL | 9.6 |
| Redis | 4.0 | | Redis | 4.0 |
!!! note
Python 3.7 or later will be required in NetBox v3.0. Users are strongly encouraged to install NetBox using Python 3.7 or later for new deployments.
Below is a simplified overview of the NetBox application stack for reference: Below is a simplified overview of the NetBox application stack for reference:
![NetBox UI as seen by a non-authenticated user](../media/installation/netbox_application_stack.png) ![NetBox UI as seen by a non-authenticated user](../media/installation/netbox_application_stack.png)
@@ -30,6 +33,3 @@ Below is a simplified overview of the NetBox application stack for reference:
## Upgrading ## Upgrading
If you are upgrading from an existing installation, please consult the [upgrading guide](upgrading.md). If you are upgrading from an existing installation, please consult the [upgrading guide](upgrading.md).
!!! note
Beginning with v2.5.9, the official documentation calls for systemd to be used for managing the WSGI workers in place of supervisord. Please see the instructions for [migrating to systemd](migrating-to-systemd.md) if you are still using supervisord.

View File

@@ -2,9 +2,9 @@
The association of a circuit with a particular site and/or device is modeled separately as a circuit termination. A circuit may have up to two terminations, labeled A and Z. A single-termination circuit can be used when you don't know (or care) about the far end of a circuit (for example, an Internet access circuit which connects to a transit provider). A dual-termination circuit is useful for tracking circuits which connect two sites. The association of a circuit with a particular site and/or device is modeled separately as a circuit termination. A circuit may have up to two terminations, labeled A and Z. A single-termination circuit can be used when you don't know (or care) about the far end of a circuit (for example, an Internet access circuit which connects to a transit provider). A dual-termination circuit is useful for tracking circuits which connect two sites.
Each circuit termination is tied to a site, and may optionally be connected via a cable to a specific device interface or port within that site. Each termination must be assigned a port speed, and can optionally be assigned an upstream speed if it differs from the downstream speed (a common scenario with e.g. DOCSIS cable modems). Fields are also available to track cross-connect and patch panel details. Each circuit termination is attached to either a site or to a provider network. Site terminations may optionally be connected via a cable to a specific device interface or port within that site. Each termination must be assigned a port speed, and can optionally be assigned an upstream speed if it differs from the downstream speed (a common scenario with e.g. DOCSIS cable modems). Fields are also available to track cross-connect and patch panel details.
In adherence with NetBox's philosophy of closely modeling the real world, a circuit may terminate only to a physical interface. For example, circuits may not terminate to LAG interfaces, which are virtual in nature. In such cases, a separate physical circuit is associated with each LAG member interface and each needs to be modeled discretely. In adherence with NetBox's philosophy of closely modeling the real world, a circuit may be connected only to a physical interface. For example, circuits may not terminate to LAG interfaces, which are virtual in nature. In such cases, a separate physical circuit is associated with each LAG member interface and each needs to be modeled discretely.
!!! note !!! note
A circuit in NetBox represents a physical link, and cannot have more than two endpoints. When modeling a multi-point topology, each leg of the topology must be defined as a discrete circuit, with one end terminating within the provider's infrastructure. A circuit in NetBox represents a physical link, and cannot have more than two endpoints. When modeling a multi-point topology, each leg of the topology must be defined as a discrete circuit, with one end terminating within the provider's infrastructure. The provider network model is ideal for representing these networks.

View File

@@ -0,0 +1,5 @@
# Provider Networks
This model can be used to represent the boundary of a provider network, the details of which are unknown or unimportant to the NetBox user. For example, it might represent a provider's regional MPLS network to which multiple circuits provide connectivity.
Each provider network must be assigned to a provider. A circuit may terminate to either a provider network or to a site.

View File

@@ -8,7 +8,7 @@ A device is said to be full-depth if its installation on one rack face prevents
Each device must be instantiated from a pre-created device type, and its default components (console ports, power ports, interfaces, etc.) will be created automatically. (The device type associated with a device may be changed after its creation, however its components will not be updated retroactively.) Each device must be instantiated from a pre-created device type, and its default components (console ports, power ports, interfaces, etc.) will be created automatically. (The device type associated with a device may be changed after its creation, however its components will not be updated retroactively.)
Each device must be assigned a site, device role, and operational status, and may optionally be assigned to a specific rack within a site. A platform, serial number, and asset tag may optionally be assigned to each device. Each device must be assigned a site, device role, and operational status, and may optionally be assigned to a specific location and/or rack within a site. A platform, serial number, and asset tag may optionally be assigned to each device.
Device names must be unique within a site, unless the device has been assigned to a tenant. Devices may also be unnamed. Device names must be unique within a site, unless the device has been assigned to a tenant. Devices may also be unnamed.

View File

@@ -2,11 +2,15 @@
Interfaces in NetBox represent network interfaces used to exchange data with connected devices. On modern networks, these are most commonly Ethernet, but other types are supported as well. Each interface must be assigned a type, and may optionally be assigned a MAC address, MTU, and IEEE 802.1Q mode (tagged or access). Each interface can also be enabled or disabled, and optionally designated as management-only (for out-of-band management). Interfaces in NetBox represent network interfaces used to exchange data with connected devices. On modern networks, these are most commonly Ethernet, but other types are supported as well. Each interface must be assigned a type, and may optionally be assigned a MAC address, MTU, and IEEE 802.1Q mode (tagged or access). Each interface can also be enabled or disabled, and optionally designated as management-only (for out-of-band management).
Interfaces may be physical or virtual in nature, but only physical interfaces may be connected via cables. Cables can connect interfaces to pass-through ports, circuit terminations, or other interfaces. !!! note
Although devices and virtual machines both can have interfaces, a separate model is used for each. Thus, device interfaces have some properties that are not present on virtual machine interfaces and vice versa.
### Interface Types
Interfaces may be physical or virtual in nature, but only physical interfaces may be connected via cables. Cables can connect interfaces to pass-through ports, circuit terminations, or other interfaces. Virtual interfaces, such as 802.1Q-tagged subinterfaces, may be assigned to physical parent interfaces.
Physical interfaces may be arranged into a link aggregation group (LAG) and associated with a parent LAG (virtual) interface. LAG interfaces can be recursively nested to model bonding of trunk groups. Like all virtual interfaces, LAG interfaces cannot be connected physically. Physical interfaces may be arranged into a link aggregation group (LAG) and associated with a parent LAG (virtual) interface. LAG interfaces can be recursively nested to model bonding of trunk groups. Like all virtual interfaces, LAG interfaces cannot be connected physically.
IP addresses can be assigned to interfaces. VLANs can also be assigned to each interface as either tagged or untagged. (An interface may have only one untagged VLAN.) ### IP Address Assignment
!!! note IP addresses can be assigned to interfaces. VLANs can also be assigned to each interface as either tagged or untagged. (An interface may have only one untagged VLAN.)
Although devices and virtual machines both can have interfaces, a separate model is used for each. Thus, device interfaces have some properties that are not present on virtual machine interfaces and vice versa.

View File

@@ -0,0 +1,5 @@
# Locations
Racks and devices can be grouped by location within a site. A location may represent a floor, room, cage, or similar organizational unit. Locations can be nested to form a hierarchy. For example, you may have floors within a site, and rooms within a floor.
The name and facility ID of each rack within a location must be unique. (Racks not assigned to the same location may have identical names and/or facility IDs.)

View File

@@ -1,6 +1,6 @@
# Power Feed # Power Feed
A power feed represents the distribution of power from a power panel to a particular device, typically a power distribution unit (PDU). The power pot (inlet) on a device can be connected via a cable to a power feed. A power feed may optionally be assigned to a rack to allow more easily tracking the distribution of power among racks. A power feed represents the distribution of power from a power panel to a particular device, typically a power distribution unit (PDU). The power port (inlet) on a device can be connected via a cable to a power feed. A power feed may optionally be assigned to a rack to allow more easily tracking the distribution of power among racks.
Each power feed is assigned an operational type (primary or redundant) and one of the following statuses: Each power feed is assigned an operational type (primary or redundant) and one of the following statuses:

View File

@@ -2,7 +2,7 @@
A power panel represents the origin point in NetBox for electrical power being disseminated by one or more power feeds. In a data center environment, one power panel often serves a group of racks, with an individual power feed extending to each rack, though this is not always the case. It is common to have two sets of panels and feeds arranged in parallel to provide redundant power to each rack. A power panel represents the origin point in NetBox for electrical power being disseminated by one or more power feeds. In a data center environment, one power panel often serves a group of racks, with an individual power feed extending to each rack, though this is not always the case. It is common to have two sets of panels and feeds arranged in parallel to provide redundant power to each rack.
Each power panel must be assigned to a site, and may optionally be assigned to a particular rack group. Each power panel must be assigned to a site, and may optionally be assigned to a particular location within that site.
!!! note !!! note
NetBox does not model the mechanism by which power is delivered to a power panel. Power panels define the root level of the power distribution hierarchy in NetBox. NetBox does not model the mechanism by which power is delivered to a power panel. Power panels define the root level of the power distribution hierarchy in NetBox.

View File

@@ -1,6 +1,6 @@
# Racks # Racks
The rack model represents a physical two- or four-post equipment rack in which devices can be installed. Each rack must be assigned to a site, and may optionally be assigned to a rack group and/or tenant. Racks can also be organized by user-defined functional roles. The rack model represents a physical two- or four-post equipment rack in which devices can be installed. Each rack must be assigned to a site, and may optionally be assigned to a location and/or tenant. Racks can also be organized by user-defined functional roles.
Rack height is measured in *rack units* (U); racks are commonly between 42U and 48U tall, but NetBox allows you to define racks of arbitrary height. A toggle is provided to indicate whether rack units are in ascending (from the ground up) or descending order. Rack height is measured in *rack units* (U); racks are commonly between 42U and 48U tall, but NetBox allows you to define racks of arbitrary height. A toggle is provided to indicate whether rack units are in ascending (from the ground up) or descending order.

View File

@@ -1,7 +0,0 @@
# Rack Groups
Racks can be organized into groups, which can be nested into themselves similar to regions. As with sites, how you choose to designate rack groups will depend on the nature of your organization. For example, if each site represents a campus, each group might represent a building within a campus. If each site represents a building, each rack group might equate to a floor or room.
Each rack group must be assigned to a parent site, and rack groups may optionally be nested within a site to model a multi-level hierarchy. For example, you might have a tier of rooms beneath a tier of floors, all belonging to the same parent building (site).
The name and facility ID of each rack within a group must be unique. (Racks not assigned to the same rack group may have identical names and/or facility IDs.)

View File

@@ -0,0 +1,3 @@
# Site Groups
Like regions, site groups can be used to organize sites. Whereas regions are intended to provide geographic organization, site groups can be used to classify sites by role or function. Also like regions, site groups can be nested to form a hierarchy. Sites which belong to a child group are also considered to be members of any of its parent groups.

View File

@@ -3,11 +3,13 @@
Sometimes it is desirable to associate additional data with a group of devices or virtual machines to aid in automated configuration. For example, you might want to associate a set of syslog servers for all devices within a particular region. Context data enables the association of extra user-defined data with devices and virtual machines grouped by one or more of the following assignments: Sometimes it is desirable to associate additional data with a group of devices or virtual machines to aid in automated configuration. For example, you might want to associate a set of syslog servers for all devices within a particular region. Context data enables the association of extra user-defined data with devices and virtual machines grouped by one or more of the following assignments:
* Region * Region
* Site group
* Site * Site
* Device type (devices only)
* Role * Role
* Platform * Platform
* Cluster group * Cluster group (VMs only)
* Cluster * Cluster (VMs only)
* Tenant group * Tenant group
* Tenant * Tenant
* Tag * Tag

View File

@@ -1,6 +1,6 @@
# VLANs # VLANs
A VLAN represents an isolated layer two domain, identified by a name and a numeric ID (1-4094) as defined in [IEEE 802.1Q](https://en.wikipedia.org/wiki/IEEE_802.1Q). Each VLAN may be assigned to a site, tenant, and/or VLAN group. A VLAN represents an isolated layer two domain, identified by a name and a numeric ID (1-4094) as defined in [IEEE 802.1Q](https://en.wikipedia.org/wiki/IEEE_802.1Q). VLANs are arranged into VLAN groups to define scope and to enforce uniqueness.
Each VLAN must be assigned one of the following operational statuses: Each VLAN must be assigned one of the following operational statuses:

View File

@@ -1,5 +1,5 @@
# VLAN Groups # VLAN Groups
VLAN groups can be used to organize VLANs within NetBox. Each group may optionally be assigned to a specific site, but a group cannot belong to multiple sites. VLAN groups can be used to organize VLANs within NetBox. Each VLAN group can be scoped to a particular region, site group, site, location, rack, cluster group, or cluster. Member VLANs will be available for assignment to devices and/or virtual machines within the specified scope.
Groups can also be used to enforce uniqueness: Each VLAN within a group must have a unique ID and name. VLANs which are not assigned to a group may have overlapping names and IDs (including VLANs which belong to a common site). For example, you can create two VLANs with ID 123, but they cannot both be assigned to the same group. Groups can also be used to enforce uniqueness: Each VLAN within a group must have a unique ID and name. VLANs which are not assigned to a group may have overlapping names and IDs (including VLANs which belong to a common site). For example, you can create two VLANs with ID 123, but they cannot both be assigned to the same group.

View File

@@ -11,4 +11,6 @@ Like devices, each VM can be assigned a platform and/or functional role, and mus
* Failed * Failed
* Decommissioning * Decommissioning
Additional fields are available for annotating the vCPU count, memory (GB), and disk (GB) allocated to each VM. Each VM may optionally be assigned to a tenant. Virtual machines may have virtual interfaces assigned to them, but do not support any physical component. Additional fields are available for annotating the vCPU count, memory (GB), and disk (GB) allocated to each VM. A VM may be allocated a partial vCPU count (e.g. 1.5 vCPU).
Each VM may optionally be assigned to a tenant. Virtual machines may have virtual interfaces assigned to them, but do not support any physical component.

View File

@@ -1 +1 @@
version-2.10.md version-2.11.md

View File

@@ -1,5 +1,50 @@
# NetBox v2.10 # NetBox v2.10
## v2.10.10 (2021-04-15)
### Enhancements
* [#5796](https://github.com/netbox-community/netbox/issues/5796) - Add DC terminal power port, outlet types
* [#5980](https://github.com/netbox-community/netbox/issues/5980) - Add Saf-D-Grid power port, outlet types
* [#6157](https://github.com/netbox-community/netbox/issues/6157) - Support Markdown rendering for report logs
* [#6160](https://github.com/netbox-community/netbox/issues/6160) - Add F connector port type
* [#6168](https://github.com/netbox-community/netbox/issues/6168) - Add SFP56 50GE interface type
### Bug Fixes
* [#5419](https://github.com/netbox-community/netbox/issues/5419) - Update parent device/VM when deleting a primary IP
* [#5643](https://github.com/netbox-community/netbox/issues/5643) - Fix VLAN assignment when editing VM interfaces in bulk
* [#5652](https://github.com/netbox-community/netbox/issues/5652) - Update object data when renaming a custom field
* [#6056](https://github.com/netbox-community/netbox/issues/6056) - Optimize change log cleanup
* [#6144](https://github.com/netbox-community/netbox/issues/6144) - Fix MAC address field display in VM interfaces search form
* [#6152](https://github.com/netbox-community/netbox/issues/6152) - Fix custom field filtering for cables, virtual chassis
* [#6162](https://github.com/netbox-community/netbox/issues/6162) - Fix choice field filters (multiple models)
---
## v2.10.9 (2021-04-12)
### Enhancements
* [#5526](https://github.com/netbox-community/netbox/issues/5526) - Add MAC address search field to VM interfaces list
* [#5756](https://github.com/netbox-community/netbox/issues/5756) - Omit child devices from non-racked devices list under rack view
* [#5840](https://github.com/netbox-community/netbox/issues/5840) - Add column to cable termination objects to display cable color
* [#6054](https://github.com/netbox-community/netbox/issues/6054) - Display NAPALM-enabled device tabs only when relevant
* [#6083](https://github.com/netbox-community/netbox/issues/6083) - Support disabling TLS certificate validation for Redis
### Bug Fixes
* [#5805](https://github.com/netbox-community/netbox/issues/5805) - Fix missing custom field filters for cables, rack reservations
* [#6070](https://github.com/netbox-community/netbox/issues/6070) - Add missing `count_ipaddresses` attribute to VMInterface serializer
* [#6073](https://github.com/netbox-community/netbox/issues/6073) - Permit users to manage their own REST API tokens without needing explicit permission
* [#6081](https://github.com/netbox-community/netbox/issues/6081) - Fix interface connections REST API endpoint
* [#6082](https://github.com/netbox-community/netbox/issues/6082) - Support colons in webhook header values
* [#6108](https://github.com/netbox-community/netbox/issues/6108) - Do not infer tenant assignment from parent objects for prefixes, IP addresses
* [#6117](https://github.com/netbox-community/netbox/issues/6117) - Handle exception when attempting to assign an MPTT-enabled model as its own parent
* [#6131](https://github.com/netbox-community/netbox/issues/6131) - Correct handling of boolean fields when cloning objects
---
## v2.10.8 (2021-03-26) ## v2.10.8 (2021-03-26)
### Bug Fixes ### Bug Fixes
@@ -255,43 +300,43 @@ All end-to-end cable paths are now cached using the new CablePath backend model.
* Removed the `/extras/_custom_field_choices/` endpoint (replaced by new custom fields endpoint) * Removed the `/extras/_custom_field_choices/` endpoint (replaced by new custom fields endpoint)
* Added the `/status/` endpoint to convey NetBox's current status * Added the `/status/` endpoint to convey NetBox's current status
* circuits.CircuitTermination: * circuits.CircuitTermination:
* Added the `/trace/` endpoint * Added the `/trace/` endpoint
* Replaced `connection_status` with `connected_endpoint_reachable` (boolean) * Replaced `connection_status` with `connected_endpoint_reachable` (boolean)
* Added `cable_peer` and `cable_peer_type` * Added `cable_peer` and `cable_peer_type`
* `port_speed` may now be null * `port_speed` may now be null
* dcim.Cable: Added `custom_fields` * dcim.Cable: Added `custom_fields`
* dcim.ConsolePort: * dcim.ConsolePort:
* Replaced `connection_status` with `connected_endpoint_reachable` (boolean) * Replaced `connection_status` with `connected_endpoint_reachable` (boolean)
* Added `cable_peer` and `cable_peer_type` * Added `cable_peer` and `cable_peer_type`
* Removed `connection_status` from nested serializer * Removed `connection_status` from nested serializer
* dcim.ConsoleServerPort: * dcim.ConsoleServerPort:
* Replaced `connection_status` with `connected_endpoint_reachable` (boolean) * Replaced `connection_status` with `connected_endpoint_reachable` (boolean)
* Added `cable_peer` and `cable_peer_type` * Added `cable_peer` and `cable_peer_type`
* Removed `connection_status` from nested serializer * Removed `connection_status` from nested serializer
* dcim.FrontPort: * dcim.FrontPort:
* Replaced the `/trace/` endpoint with `/paths/`, which returns a list of cable paths * Replaced the `/trace/` endpoint with `/paths/`, which returns a list of cable paths
* Added `cable_peer` and `cable_peer_type` * Added `cable_peer` and `cable_peer_type`
* dcim.Interface: * dcim.Interface:
* Replaced `connection_status` with `connected_endpoint_reachable` (boolean) * Replaced `connection_status` with `connected_endpoint_reachable` (boolean)
* Added `cable_peer` and `cable_peer_type` * Added `cable_peer` and `cable_peer_type`
* Removed `connection_status` from nested serializer * Removed `connection_status` from nested serializer
* dcim.InventoryItem: The `_depth` field has been added to reflect MPTT positioning * dcim.InventoryItem: The `_depth` field has been added to reflect MPTT positioning
* dcim.PowerFeed: * dcim.PowerFeed:
* Added the `/trace/` endpoint * Added the `/trace/` endpoint
* Added fields `connected_endpoint`, `connected_endpoint_type`, `connected_endpoint_reachable`, `cable_peer`, and `cable_peer_type` * Added fields `connected_endpoint`, `connected_endpoint_type`, `connected_endpoint_reachable`, `cable_peer`, and `cable_peer_type`
* dcim.PowerOutlet: * dcim.PowerOutlet:
* Replaced `connection_status` with `connected_endpoint_reachable` (boolean) * Replaced `connection_status` with `connected_endpoint_reachable` (boolean)
* Added `cable_peer` and `cable_peer_type` * Added `cable_peer` and `cable_peer_type`
* Removed `connection_status` from nested serializer * Removed `connection_status` from nested serializer
* dcim.PowerPanel: Added `custom_fields` * dcim.PowerPanel: Added `custom_fields`
* dcim.PowerPort * dcim.PowerPort
* Replaced `connection_status` with `connected_endpoint_reachable` (boolean) * Replaced `connection_status` with `connected_endpoint_reachable` (boolean)
* Added `cable_peer` and `cable_peer_type` * Added `cable_peer` and `cable_peer_type`
* Removed `connection_status` from nested serializer * Removed `connection_status` from nested serializer
* dcim.RackReservation: Added `custom_fields` * dcim.RackReservation: Added `custom_fields`
* dcim.RearPort: * dcim.RearPort:
* Replaced the `/trace/` endpoint with `/paths/`, which returns a list of cable paths * Replaced the `/trace/` endpoint with `/paths/`, which returns a list of cable paths
* Added `cable_peer` and `cable_peer_type` * Added `cable_peer` and `cable_peer_type`
* dcim.VirtualChassis: Added `custom_fields` * dcim.VirtualChassis: Added `custom_fields`
* extras.ExportTemplate: The `template_language` field has been removed * extras.ExportTemplate: The `template_language` field has been removed
* extras.Graph: This API endpoint has been removed (see #4349) * extras.Graph: This API endpoint has been removed (see #4349)

View File

@@ -0,0 +1,309 @@
# NetBox v2.11
## v2.11.5 (2021-06-04)
**NOTE:** This release includes a database migration that calculates and annotates prefix depth. It may impose a noticeable delay on the upgrade process: Users should anticipate roughly one minute of delay per 100 thousand prefixes being updated.
### Enhancements
* [#6087](https://github.com/netbox-community/netbox/issues/6087) - Improved prefix hierarchy rendering
* [#6487](https://github.com/netbox-community/netbox/issues/6487) - Add location filter to cable connection form
* [#6501](https://github.com/netbox-community/netbox/issues/6501) - Expose prefix depth and children on REST API serializer
* [#6527](https://github.com/netbox-community/netbox/issues/6527) - Support Markdown for report descriptions
* [#6540](https://github.com/netbox-community/netbox/issues/6540) - Add a "flat" column to the prefix table
### Bug Fixes
* [#6064](https://github.com/netbox-community/netbox/issues/6064) - Fix object permission assignments for user and group models
* [#6217](https://github.com/netbox-community/netbox/issues/6217) - Disallow passing of string values for integer custom fields
* [#6284](https://github.com/netbox-community/netbox/issues/6284) - Avoid sending redundant webhooks when adding/removing tags
* [#6492](https://github.com/netbox-community/netbox/issues/6492) - Correct tag population in post-change data resulting from REST API changes
* [#6496](https://github.com/netbox-community/netbox/issues/6496) - Fix upgrade script when Python installed in nonstandard path
* [#6502](https://github.com/netbox-community/netbox/issues/6502) - Correct permissions evaluation for running a report via the REST API
* [#6517](https://github.com/netbox-community/netbox/issues/6517) - Fix assignment of user when creating rack reservations via REST API
* [#6525](https://github.com/netbox-community/netbox/issues/6525) - Paginate related IPs table under IP address view
---
## v2.11.4 (2021-05-25)
### Enhancements
* [#5121](https://github.com/netbox-community/netbox/issues/5121) - Add content type filters for tags
* [#6358](https://github.com/netbox-community/netbox/issues/6358) - Add search field for VLAN groups
* [#6393](https://github.com/netbox-community/netbox/issues/6393) - Add `description` filter for IP addresses
* [#6400](https://github.com/netbox-community/netbox/issues/6400) - Add cyan color choice for plugin buttons
* [#6422](https://github.com/netbox-community/netbox/issues/6422) - Enable filtering users by group under admin UI
* [#6441](https://github.com/netbox-community/netbox/issues/6441) - Improve UI paginator to optimize page object count
### Bug Fixes
* [#6376](https://github.com/netbox-community/netbox/issues/6376) - Fix assignment of VLAN groups to clusters, cluster groups via REST API
* [#6398](https://github.com/netbox-community/netbox/issues/6398) - Avoid exception when deleting device connected to self via circuit
* [#6426](https://github.com/netbox-community/netbox/issues/6426) - Allow assigning virtual chassis member interfaces to LAG on VC master
* [#6438](https://github.com/netbox-community/netbox/issues/6438) - Fix missing descriptions and label for device type imports and exports
* [#6465](https://github.com/netbox-community/netbox/issues/6465) - Fix typo in installed plugins REST API endpoint
* [#6467](https://github.com/netbox-community/netbox/issues/6467) - Fix access to metrics on custom `BASE_PATH` when login is required
* [#6468](https://github.com/netbox-community/netbox/issues/6468) - Disable ordering VLAN groups list by scope object
---
## v2.11.3 (2021-05-07)
### Enhancements
* [#6197](https://github.com/netbox-community/netbox/issues/6197) - Introduced `SESSION_COOKIE_NAME` config parameter
* [#6318](https://github.com/netbox-community/netbox/issues/6318) - Add OM5 MMF cable type
* [#6351](https://github.com/netbox-community/netbox/issues/6351) - Add aggregates count to tenant view
* [#6359](https://github.com/netbox-community/netbox/issues/6359) - Enable custom links for organizational and nested group models
### Bug Fixes
* [#6240](https://github.com/netbox-community/netbox/issues/6240) - Fix display of available VLAN ranges under VLAN group view
* [#6308](https://github.com/netbox-community/netbox/issues/6308) - Fix linking of available VLANs in VLAN group view
* [#6309](https://github.com/netbox-community/netbox/issues/6309) - Restrict parent VM interface assignment to the parent VM
* [#6312](https://github.com/netbox-community/netbox/issues/6312) - Interface device filter should return all virtual chassis interfaces only if device is master
* [#6313](https://github.com/netbox-community/netbox/issues/6313) - Fix device type instance count under manufacturer view
* [#6321](https://github.com/netbox-community/netbox/issues/6321) - Restore "add an IP" button under prefix IPs view
* [#6333](https://github.com/netbox-community/netbox/issues/6333) - Fix filtering of circuit terminations by primary key
* [#6339](https://github.com/netbox-community/netbox/issues/6339) - Improve ordering of interfaces when viewing virtual chassis master
* [#6350](https://github.com/netbox-community/netbox/issues/6350) - Include first & last IP addresses when allocating available IPv6 addresses via the REST API
* [#6355](https://github.com/netbox-community/netbox/issues/6355) - Fix caching error when swapping A/Z circuit terminations
* [#6357](https://github.com/netbox-community/netbox/issues/6357) - Fix ProviderNetwork nested API serializer
* [#6363](https://github.com/netbox-community/netbox/issues/6363) - Correct pre-population of cluster group when creating a cluster
* [#6369](https://github.com/netbox-community/netbox/issues/6369) - Fix interface assignment for VLANs in non-scoped groups
---
## v2.11.2 (2021-04-27)
### Enhancements
* [#6275](https://github.com/netbox-community/netbox/issues/6275) - Linkify rack, device counts on locations list
* [#6278](https://github.com/netbox-community/netbox/issues/6278) - Note device locations on cable traces
* [#6287](https://github.com/netbox-community/netbox/issues/6287) - Add option to clear assigned max length filter on prefixes list
### Bug Fixes
* [#6236](https://github.com/netbox-community/netbox/issues/6236) - Journal entry title should account for configured timezone
* [#6246](https://github.com/netbox-community/netbox/issues/6246) - Permit full-length descriptions when creating device components and VM interfaces
* [#6248](https://github.com/netbox-community/netbox/issues/6248) - Fix table column reconfiguration under Chrome
* [#6252](https://github.com/netbox-community/netbox/issues/6252) - Fix assignment of console port speed values above 19.2kbps
* [#6254](https://github.com/netbox-community/netbox/issues/6254) - Disable ordering of space column in racks table
* [#6258](https://github.com/netbox-community/netbox/issues/6258) - Fix parent assignment for SiteGroup API serializer
* [#6262](https://github.com/netbox-community/netbox/issues/6262) - Support filtering by created/updated time for all relevant objects
* [#6267](https://github.com/netbox-community/netbox/issues/6267) - Fix cable tracing API endpoint for circuit terminations
* [#6289](https://github.com/netbox-community/netbox/issues/6289) - Fix assignment of VC member interfaces to LAG interfaces
---
## v2.11.1 (2021-04-21)
### Enhancements
* [#6161](https://github.com/netbox-community/netbox/issues/6161) - Enable ordering of device component tables
* [#6179](https://github.com/netbox-community/netbox/issues/6179) - Enable natural ordering for virtual machines
* [#6189](https://github.com/netbox-community/netbox/issues/6189) - Add ability to search for locations by name or description
* [#6190](https://github.com/netbox-community/netbox/issues/6190) - Allow filtering devices with no location assigned
* [#6210](https://github.com/netbox-community/netbox/issues/6210) - Include child locations on location view
### Bug Fixes
* [#6184](https://github.com/netbox-community/netbox/issues/6184) - Fix parent object table column in prefix IP addresses list
* [#6188](https://github.com/netbox-community/netbox/issues/6188) - Support custom field filtering for regions, site groups, and locations
* [#6196](https://github.com/netbox-community/netbox/issues/6196) - Fix object list display for users with read-only permissions
* [#6215](https://github.com/netbox-community/netbox/issues/6215) - Restore tenancy section in virtual machine form
---
## v2.11.0 (2021-04-16)
**Note:** NetBox v2.11 is the last major release that will support Python 3.6. Beginning with NetBox v3.0, Python 3.7 or later will be required.
### Breaking Changes
* All objects now use numeric IDs in their UI view URLs instead of slugs. You may need to update external references to NetBox objects. (Note that this does _not_ affect the REST API.)
* The UI now uses numeric IDs when filtering object lists. You may need to update external links to filtered object lists. (Note that the slug- and name-based filters will continue to work, however the filter selection fields within the UI will not be automatically populated.)
* The RackGroup model has been renamed to Location (see [#4971](https://github.com/netbox-community/netbox/issues/4971)). Its REST API endpoint has changed from `/api/dcim/rack-groups/` to `/api/dcim/locations/`.
* The foreign key field `group` on dcim.Rack has been renamed to `location`.
* The foreign key field `site` on ipam.VLANGroup has been replaced with the `scope` generic foreign key (see [#5284](https://github.com/netbox-community/netbox/issues/5284)).
* Custom script ObjectVars no longer support the `queryset` parameter: Use `model` instead (see [#5995](https://github.com/netbox-community/netbox/issues/5995)).
### New Features
#### Journaling Support ([#151](https://github.com/netbox-community/netbox/issues/151))
NetBox now supports journaling for all primary objects. The journal is a collection of human-generated notes and comments about an object maintained for historical context. It supplements NetBox's change log to provide additional information about why changes have been made or to convey events which occur outside NetBox. Unlike the change log, in which records typically expire after some time, journal entries persist for the life of the associated object.
#### Parent Interface Assignments ([#1519](https://github.com/netbox-community/netbox/issues/1519))
Virtual device and VM interfaces can now be assigned to a "parent" interface by setting the `parent` field on the interface object. This is helpful for associating subinterfaces with their physical counterpart. For example, you might assign virtual interfaces Gi0/0.100 and Gi0/0.200 as children of the physical interface Gi0/0.
#### Pre- and Post-Change Snapshots in Webhooks ([#3451](https://github.com/netbox-community/netbox/issues/3451))
In conjunction with the newly improved change logging functionality ([#5913](https://github.com/netbox-community/netbox/issues/5913)), outgoing webhooks now include both pre- and post-change representations of the modified object. These are available in the rendering context as a dictionary named `snapshots` with keys `prechange` and `postchange`. For example, here are the abridged snapshots resulting from renaming a site and changing its status:
```json
"snapshots": {
"prechange": {
"name": "Site 1",
"slug": "site-1",
"status": "active",
...
},
"postchange": {
"name": "Site 2",
"slug": "site-2",
"status": "planned",
...
}
}
```
Note: The pre-change snapshot for a newly created will always be null, as will the post-change snapshot for a deleted object.
#### Mark as Connected Without a Cable ([#3648](https://github.com/netbox-community/netbox/issues/3648))
Cable termination objects (circuit terminations, power feeds, and most device components) can now be marked as "connected" without actually attaching a cable. This helps simplify the process of modeling an infrastructure boundary where we don't necessarily know or care what is connected to an attachment point, but still need to reflect the termination as being occupied.
In addition to the new `mark_connected` boolean field, the REST API representation of these objects now also includes a read-only boolean field named `_occupied`. This conveniently returns true if either a cable is attached or `mark_connected` is true.
#### Allow Assigning Devices to Locations ([#4971](https://github.com/netbox-community/netbox/issues/4971))
Devices can now be assigned to locations (formerly known as rack groups) within a site without needing to be assigned to a particular rack. This is handy for assigning devices to rooms or floors within a building where racks are not used. The `location` foreign key field has been added to the Device model to support this.
#### Dynamic Object Exports ([#4999](https://github.com/netbox-community/netbox/issues/4999))
When exporting a list of objects in NetBox, users now have the option of selecting the "current view". This will render CSV output matching the current configuration of the table being viewed. For example, if you modify the sites list to display only the site name, tenant, and status, the rendered CSV will include only these columns, and they will appear in the order chosen.
The legacy static export behavior has been retained to ensure backward compatibility for dependent integrations. However, users are strongly encouraged to adapt custom export templates where needed as this functionality will be removed in v3.0.
#### Variable Scope Support for VLAN Groups ([#5284](https://github.com/netbox-community/netbox/issues/5284))
In previous releases, VLAN groups could be assigned only to a site. To afford more flexibility in conveying the true scope of an L2 domain, a VLAN group can now be assigned to a region, site group (new in v2.11), site, location, or rack. VLANs assigned to a group will be available only to devices and virtual machines which exist within its scope.
For example, a VLAN within a group assigned to a location will be available only to devices assigned to that location (or one of its child locations), or to a rack within that location.
#### New Site Group Model ([#5892](https://github.com/netbox-community/netbox/issues/5892))
This release introduces the new SiteGroup model, which can be used to organize sites similar to the existing Region model. Whereas regions are intended for geographically arranging sites into countries, states, and so on, the new site group model can be used to organize sites by functional role or other arbitrary classification. Using regions and site groups in conjunction provides two dimensions along which sites can be organized, offering greater flexibility to the user.
#### Improved Change Logging ([#5913](https://github.com/netbox-community/netbox/issues/5913))
The ObjectChange model (which is used to record the creation, modification, and deletion of NetBox objects) now explicitly records the pre-change and post-change state of each object, rather than only the post-change state. This was done to present a more clear depiction of each change being made, and to prevent the erroneous association of a previous unlogged change with its successor.
#### Provider Network Modeling ([#5986](https://github.com/netbox-community/netbox/issues/5986))
A new provider network model has been introduced to represent the boundary of a network that exists outside the scope of NetBox. Each instance of this model must be assigned to a provider, and circuits can now terminate to either provider networks or to sites. The use of this model will likely be extended by future releases to support overlay and virtual circuit modeling.
### Enhancements
* [#4833](https://github.com/netbox-community/netbox/issues/4833) - Allow assigning config contexts by device type
* [#5344](https://github.com/netbox-community/netbox/issues/5344) - Add support for custom fields in tables
* [#5370](https://github.com/netbox-community/netbox/issues/5370) - Extend custom field support to organizational models
* [#5375](https://github.com/netbox-community/netbox/issues/5375) - Add `speed` attribute to console port models
* [#5401](https://github.com/netbox-community/netbox/issues/5401) - Extend custom field support to device component models
* [#5425](https://github.com/netbox-community/netbox/issues/5425) - Create separate tabs for VMs and devices under the cluster view
* [#5451](https://github.com/netbox-community/netbox/issues/5451) - Add support for multiple-selection custom fields
* [#5608](https://github.com/netbox-community/netbox/issues/5608) - Add REST API endpoint for custom links
* [#5610](https://github.com/netbox-community/netbox/issues/5610) - Add REST API endpoint for webhooks
* [#5757](https://github.com/netbox-community/netbox/issues/5757) - Add unique identifier to every object view
* [#5830](https://github.com/netbox-community/netbox/issues/5830) - Add `as_attachment` to ExportTemplate to control download behavior
* [#5848](https://github.com/netbox-community/netbox/issues/5848) - Filter custom fields by content type in format `<app_label>.<model>`
* [#5891](https://github.com/netbox-community/netbox/issues/5891) - Add `display` field to all REST API serializers
* [#5894](https://github.com/netbox-community/netbox/issues/5894) - Use primary keys when filtering object lists by related objects in the UI
* [#5895](https://github.com/netbox-community/netbox/issues/5895) - Rename RackGroup to Location
* [#5901](https://github.com/netbox-community/netbox/issues/5901) - Add `created` and `last_updated` fields to device component models
* [#5971](https://github.com/netbox-community/netbox/issues/5971) - Add dedicated views for organizational models
* [#5972](https://github.com/netbox-community/netbox/issues/5972) - Enable bulk editing for organizational models
* [#5975](https://github.com/netbox-community/netbox/issues/5975) - Allow partial (decimal) vCPU allocations for virtual machines
* [#6001](https://github.com/netbox-community/netbox/issues/6001) - Paginate component tables under device views
* [#6038](https://github.com/netbox-community/netbox/issues/6038) - Include tagged objects list on tag view
* [#6088](https://github.com/netbox-community/netbox/issues/6088) - Improved table configuration form
* [#6097](https://github.com/netbox-community/netbox/issues/6097) - Redirect old slug-based object views
* [#6125](https://github.com/netbox-community/netbox/issues/6125) - Add locations count to home page
* [#6146](https://github.com/netbox-community/netbox/issues/6146) - Add bulk disconnect support for power feeds
* [#6149](https://github.com/netbox-community/netbox/issues/6149) - Support image attachments for locations
### Bug Fixes (from v2.11-beta1)
* [#5583](https://github.com/netbox-community/netbox/issues/5583) - Eliminate redundant change records when adding/removing tags
* [#6100](https://github.com/netbox-community/netbox/issues/6100) - Fix VM interfaces table "add interfaces" link
* [#6104](https://github.com/netbox-community/netbox/issues/6104) - Fix location column on racks table
* [#6105](https://github.com/netbox-community/netbox/issues/6105) - Hide checkboxes for VMs under cluster VMs view
* [#6106](https://github.com/netbox-community/netbox/issues/6106) - Allow assigning a virtual interface as the parent of an existing interface
* [#6107](https://github.com/netbox-community/netbox/issues/6107) - Fix rack selection field on device form
* [#6110](https://github.com/netbox-community/netbox/issues/6110) - Fix handling of TemplateColumn values for table export
* [#6123](https://github.com/netbox-community/netbox/issues/6123) - Prevent device from being assigned to mismatched site and location
* [#6124](https://github.com/netbox-community/netbox/issues/6124) - Location `parent` filter should return all child locations (not just those directly assigned)
* [#6130](https://github.com/netbox-community/netbox/issues/6130) - Improve display of assigned models in custom fields list
* [#6155](https://github.com/netbox-community/netbox/issues/6155) - Fix admin links for plugins, background tasks
* [#6171](https://github.com/netbox-community/netbox/issues/6171) - Fix display of horizontally-scrolling object lists
* [#6173](https://github.com/netbox-community/netbox/issues/6173) - Fix assigned device/VM count when bulk editing/deleting device roles
* [#6176](https://github.com/netbox-community/netbox/issues/6176) - Correct position of MAC address field when creating VM interfaces
* [#6177](https://github.com/netbox-community/netbox/issues/6177) - Prevent VM interface from being assigned as its own parent
### Other Changes
* [#1638](https://github.com/netbox-community/netbox/issues/1638) - Migrate all primary keys to 64-bit integers
* [#5873](https://github.com/netbox-community/netbox/issues/5873) - Use numeric IDs in all object URLs
* [#5938](https://github.com/netbox-community/netbox/issues/5938) - Deprecated support for Python 3.6
* [#5990](https://github.com/netbox-community/netbox/issues/5990) - Deprecated `display_field` parameter for custom script ObjectVar and MultiObjectVar fields
* [#5995](https://github.com/netbox-community/netbox/issues/5995) - Dropped backward compatibility for `queryset` parameter on ObjectVar and MultiObjectVar (use `model` instead)
* [#6014](https://github.com/netbox-community/netbox/issues/6014) - Moved the virtual machine interfaces list to a separate view
* [#6071](https://github.com/netbox-community/netbox/issues/6071) - Cable traces now traverse circuits
### REST API Changes
* All primary keys are now 64-bit integers
* All model serializers now include a `display` field to be used for the presentation of an object to a human user
* All device components
* Added support for custom fields
* Added `created` and `last_updated` fields to track object creation and modification
* All device component templates
* Added `created` and `last_updated` fields to track object creation and modification
* All organizational models
* Added support for custom fields
* All cable termination models (cabled device components, power feeds, and circuit terminations)
* Added `mark_connected` boolean field to force connection status
* Added `_occupied` read-only boolean field as common attribute for determining whether an object is occupied
* Renamed RackGroup to Location
* The `/dcim/rack-groups/` endpoint is now `/dcim/locations/`
* circuits.CircuitTermination
* Added the `provider_network` field
* Removed the `connected_endpoint`, `connected_endpoint_type`, and `connected_endpoint_reachable` fields
* The `trace/` endpoint has been replaced with `paths/`
* circuits.ProviderNetwork
* Added the `/api/circuits/provider-networks/` endpoint
* dcim.Device
* Added the `location` field
* dcim.Interface
* Added the `parent` field
* dcim.PowerPanel
* Renamed `rack_group` field to `location`
* dcim.Rack
* Renamed `group` field to `location`
* dcim.Site
* Added the `group` foreign key field to SiteGroup
* dcim.SiteGroup
* Added the `/api/dcim/site-groups/` endpoint
* extras.ConfigContext
* Added the `site_groups` many-to-many field to track the assignment of ConfigContexts to SiteGroups
* extras.CustomField
* Added new custom field type: `multi-select`
* extras.CustomLink
* Added the `/api/extras/custom-links/` endpoint
* extras.ExportTemplate
* Added the `as_attachment` boolean field
* extras.ObjectChange
* Added the `prechange_data` field
* Renamed `object_data` to `postchange_data`
* extras.Webhook
* Added the `/api/extras/webhooks/` endpoint
* ipam.VLANGroup
* Added the `scope_type`, `scope_id`, and `scope` fields (`scope` is a generic foreign key)
* Dropped the `site` foreign key field
* virtualization.VirtualMachine
* `vcpus` has been changed from an integer to a decimal value
* virtualization.VMInterface
* Added the `parent` field

View File

@@ -1,2 +1,2 @@
mkdocs==1.1 mkdocs-material
git+https://github.com/cmacmackin/markdown-include.git git+https://github.com/cmacmackin/markdown-include.git

View File

@@ -387,7 +387,7 @@ curl -s -X GET http://netbox/api/ipam/ip-addresses/5618/ | jq '.'
### Creating a New Object ### Creating a New Object
To create a new object, make a `POST` request to the model's _list_ endpoint with JSON data pertaining to the object being created. Note that a REST API token is required for all write operations; see the [authentication documentation](../authentication/index.md) for more information. Also be sure to set the `Content-Type` HTTP header to `application/json`. To create a new object, make a `POST` request to the model's _list_ endpoint with JSON data pertaining to the object being created. Note that a REST API token is required for all write operations; see the [authentication documentation](authentication.md) for more information. Also be sure to set the `Content-Type` HTTP header to `application/json`.
```no-highlight ```no-highlight
curl -s -X POST \ curl -s -X POST \

View File

@@ -5,14 +5,18 @@ python:
install: install:
- requirements: docs/requirements.txt - requirements: docs/requirements.txt
theme: theme:
name: readthedocs name: material
navigation_depth: 3
extra_css: extra_css:
- extra.css - extra.css
markdown_extensions: markdown_extensions:
- admonition: - admonition
- markdown_include.include: - markdown_include.include:
headingOffset: 1 headingOffset: 1
- pymdownx.emoji:
emoji_index: !!python/name:materialx.emoji.twemoji
emoji_generator: !!python/name:materialx.emoji.to_svg
- pymdownx.superfences
- pymdownx.tabbed
nav: nav:
- Introduction: 'index.md' - Introduction: 'index.md'
- Installation: - Installation:
@@ -49,6 +53,7 @@ nav:
- Custom Links: 'additional-features/custom-links.md' - Custom Links: 'additional-features/custom-links.md'
- Custom Scripts: 'additional-features/custom-scripts.md' - Custom Scripts: 'additional-features/custom-scripts.md'
- Export Templates: 'additional-features/export-templates.md' - Export Templates: 'additional-features/export-templates.md'
- Journaling: 'additional-features/journaling.md'
- NAPALM: 'additional-features/napalm.md' - NAPALM: 'additional-features/napalm.md'
- Prometheus Metrics: 'additional-features/prometheus-metrics.md' - Prometheus Metrics: 'additional-features/prometheus-metrics.md'
- Reports: 'additional-features/reports.md' - Reports: 'additional-features/reports.md'
@@ -70,11 +75,13 @@ nav:
- Introduction: 'development/index.md' - Introduction: 'development/index.md'
- Getting Started: 'development/getting-started.md' - Getting Started: 'development/getting-started.md'
- Style Guide: 'development/style-guide.md' - Style Guide: 'development/style-guide.md'
- Models: 'development/models.md'
- Extending Models: 'development/extending-models.md' - Extending Models: 'development/extending-models.md'
- Application Registry: 'development/application-registry.md' - Application Registry: 'development/application-registry.md'
- User Preferences: 'development/user-preferences.md' - User Preferences: 'development/user-preferences.md'
- Release Checklist: 'development/release-checklist.md' - Release Checklist: 'development/release-checklist.md'
- Release Notes: - Release Notes:
- Version 2.11: 'release-notes/version-2.11.md'
- Version 2.10: 'release-notes/version-2.10.md' - Version 2.10: 'release-notes/version-2.10.md'
- Version 2.9: 'release-notes/version-2.9.md' - Version 2.9: 'release-notes/version-2.9.md'
- Version 2.8: 'release-notes/version-2.8.md' - Version 2.8: 'release-notes/version-2.8.md'

View File

@@ -1,16 +1,29 @@
from rest_framework import serializers from rest_framework import serializers
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider from circuits.models import *
from netbox.api import WritableNestedSerializer from netbox.api import WritableNestedSerializer
__all__ = [ __all__ = [
'NestedCircuitSerializer', 'NestedCircuitSerializer',
'NestedCircuitTerminationSerializer', 'NestedCircuitTerminationSerializer',
'NestedCircuitTypeSerializer', 'NestedCircuitTypeSerializer',
'NestedProviderNetworkSerializer',
'NestedProviderSerializer', 'NestedProviderSerializer',
] ]
#
# Provider networks
#
class NestedProviderNetworkSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail')
class Meta:
model = ProviderNetwork
fields = ['id', 'url', 'display', 'name']
# #
# Providers # Providers
# #
@@ -21,7 +34,7 @@ class NestedProviderSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = Provider model = Provider
fields = ['id', 'url', 'name', 'slug', 'circuit_count'] fields = ['id', 'url', 'display', 'name', 'slug', 'circuit_count']
# #
@@ -34,7 +47,7 @@ class NestedCircuitTypeSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = CircuitType model = CircuitType
fields = ['id', 'url', 'name', 'slug', 'circuit_count'] fields = ['id', 'url', 'display', 'name', 'slug', 'circuit_count']
class NestedCircuitSerializer(WritableNestedSerializer): class NestedCircuitSerializer(WritableNestedSerializer):
@@ -42,7 +55,7 @@ class NestedCircuitSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = Circuit model = Circuit
fields = ['id', 'url', 'cid'] fields = ['id', 'url', 'display', 'cid']
class NestedCircuitTerminationSerializer(WritableNestedSerializer): class NestedCircuitTerminationSerializer(WritableNestedSerializer):
@@ -51,4 +64,4 @@ class NestedCircuitTerminationSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = CircuitTermination model = CircuitTermination
fields = ['id', 'url', 'circuit', 'term_side', 'cable'] fields = ['id', 'url', 'display', 'circuit', 'term_side', 'cable', '_occupied']

View File

@@ -1,12 +1,13 @@
from rest_framework import serializers from rest_framework import serializers
from circuits.choices import CircuitStatusChoices from circuits.choices import CircuitStatusChoices
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType from circuits.models import *
from dcim.api.nested_serializers import NestedCableSerializer, NestedInterfaceSerializer, NestedSiteSerializer from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
from dcim.api.serializers import CableTerminationSerializer, ConnectedEndpointSerializer from dcim.api.serializers import CableTerminationSerializer, ConnectedEndpointSerializer
from extras.api.customfields import CustomFieldModelSerializer from netbox.api import ChoiceField
from extras.api.serializers import TaggedObjectSerializer from netbox.api.serializers import (
from netbox.api import ChoiceField, ValidatedModelSerializer, WritableNestedSerializer BaseModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer, WritableNestedSerializer
)
from tenancy.api.nested_serializers import NestedTenantSerializer from tenancy.api.nested_serializers import NestedTenantSerializer
from .nested_serializers import * from .nested_serializers import *
@@ -15,15 +16,31 @@ from .nested_serializers import *
# Providers # Providers
# #
class ProviderSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): class ProviderSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail') url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
circuit_count = serializers.IntegerField(read_only=True) circuit_count = serializers.IntegerField(read_only=True)
class Meta: class Meta:
model = Provider model = Provider
fields = [ fields = [
'id', 'url', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags', 'id', 'url', 'display', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact',
'custom_fields', 'created', 'last_updated', 'circuit_count', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count',
]
#
# Provider networks
#
class ProviderNetworkSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail')
provider = NestedProviderSerializer()
class Meta:
model = ProviderNetwork
fields = [
'id', 'url', 'display', 'provider', 'name', 'description', 'comments', 'tags', 'custom_fields', 'created',
'last_updated',
] ]
@@ -31,28 +48,31 @@ class ProviderSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
# Circuits # Circuits
# #
class CircuitTypeSerializer(ValidatedModelSerializer): class CircuitTypeSerializer(OrganizationalModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail') url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
circuit_count = serializers.IntegerField(read_only=True) circuit_count = serializers.IntegerField(read_only=True)
class Meta: class Meta:
model = CircuitType model = CircuitType
fields = ['id', 'url', 'name', 'slug', 'description', 'circuit_count'] fields = [
'id', 'url', 'display', 'name', 'slug', 'description', 'custom_fields', 'created', 'last_updated',
'circuit_count',
]
class CircuitCircuitTerminationSerializer(WritableNestedSerializer, ConnectedEndpointSerializer): class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail') url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
site = NestedSiteSerializer() site = NestedSiteSerializer()
provider_network = NestedProviderNetworkSerializer()
class Meta: class Meta:
model = CircuitTermination model = CircuitTermination
fields = [ fields = [
'id', 'url', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'connected_endpoint', 'id', 'url', 'display', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
'connected_endpoint_type', 'connected_endpoint_reachable',
] ]
class CircuitSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): class CircuitSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail') url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
provider = NestedProviderSerializer() provider = NestedProviderSerializer()
status = ChoiceField(choices=CircuitStatusChoices, required=False) status = ChoiceField(choices=CircuitStatusChoices, required=False)
@@ -64,21 +84,23 @@ class CircuitSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
class Meta: class Meta:
model = Circuit model = Circuit
fields = [ fields = [
'id', 'url', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'id', 'url', 'display', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate',
'termination_a', 'termination_z', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'description', 'termination_a', 'termination_z', 'comments', 'tags', 'custom_fields', 'created',
'last_updated',
] ]
class CircuitTerminationSerializer(CableTerminationSerializer, ConnectedEndpointSerializer): class CircuitTerminationSerializer(BaseModelSerializer, CableTerminationSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail') url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
circuit = NestedCircuitSerializer() circuit = NestedCircuitSerializer()
site = NestedSiteSerializer() site = NestedSiteSerializer(required=False)
provider_network = NestedProviderNetworkSerializer(required=False)
cable = NestedCableSerializer(read_only=True) cable = NestedCableSerializer(read_only=True)
class Meta: class Meta:
model = CircuitTermination model = CircuitTermination
fields = [ fields = [
'id', 'url', 'circuit', 'term_side', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', 'id', 'url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed',
'description', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type', 'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type',
'connected_endpoint_reachable' '_occupied',
] ]

View File

@@ -13,5 +13,8 @@ router.register('circuit-types', views.CircuitTypeViewSet)
router.register('circuits', views.CircuitViewSet) router.register('circuits', views.CircuitViewSet)
router.register('circuit-terminations', views.CircuitTerminationViewSet) router.register('circuit-terminations', views.CircuitTerminationViewSet)
# Provider networks
router.register('provider-networks', views.ProviderNetworkViewSet)
app_name = 'circuits-api' app_name = 'circuits-api'
urlpatterns = router.urls urlpatterns = router.urls

View File

@@ -1,9 +1,8 @@
from django.db.models import Prefetch
from rest_framework.routers import APIRootView from rest_framework.routers import APIRootView
from circuits import filters from circuits import filtersets
from circuits.models import Provider, CircuitTermination, CircuitType, Circuit from circuits.models import *
from dcim.api.views import PathEndpointMixin from dcim.api.views import PassThroughPortMixin
from extras.api.views import CustomFieldModelViewSet from extras.api.views import CustomFieldModelViewSet
from netbox.api.views import ModelViewSet from netbox.api.views import ModelViewSet
from utilities.utils import count_related from utilities.utils import count_related
@@ -27,19 +26,19 @@ class ProviderViewSet(CustomFieldModelViewSet):
circuit_count=count_related(Circuit, 'provider') circuit_count=count_related(Circuit, 'provider')
) )
serializer_class = serializers.ProviderSerializer serializer_class = serializers.ProviderSerializer
filterset_class = filters.ProviderFilterSet filterset_class = filtersets.ProviderFilterSet
# #
# Circuit Types # Circuit Types
# #
class CircuitTypeViewSet(ModelViewSet): class CircuitTypeViewSet(CustomFieldModelViewSet):
queryset = CircuitType.objects.annotate( queryset = CircuitType.objects.annotate(
circuit_count=count_related(Circuit, 'type') circuit_count=count_related(Circuit, 'type')
) )
serializer_class = serializers.CircuitTypeSerializer serializer_class = serializers.CircuitTypeSerializer
filterset_class = filters.CircuitTypeFilterSet filterset_class = filtersets.CircuitTypeFilterSet
# #
@@ -48,21 +47,30 @@ class CircuitTypeViewSet(ModelViewSet):
class CircuitViewSet(CustomFieldModelViewSet): class CircuitViewSet(CustomFieldModelViewSet):
queryset = Circuit.objects.prefetch_related( queryset = Circuit.objects.prefetch_related(
Prefetch('terminations', queryset=CircuitTermination.objects.prefetch_related('site')), 'type', 'tenant', 'provider', 'termination_a', 'termination_z'
'type', 'tenant', 'provider',
).prefetch_related('tags') ).prefetch_related('tags')
serializer_class = serializers.CircuitSerializer serializer_class = serializers.CircuitSerializer
filterset_class = filters.CircuitFilterSet filterset_class = filtersets.CircuitFilterSet
# #
# Circuit Terminations # Circuit Terminations
# #
class CircuitTerminationViewSet(PathEndpointMixin, ModelViewSet): class CircuitTerminationViewSet(PassThroughPortMixin, ModelViewSet):
queryset = CircuitTermination.objects.prefetch_related( queryset = CircuitTermination.objects.prefetch_related(
'circuit', 'site', '_path__destination', 'cable' 'circuit', 'site', 'provider_network', 'cable'
) )
serializer_class = serializers.CircuitTerminationSerializer serializer_class = serializers.CircuitTerminationSerializer
filterset_class = filters.CircuitTerminationFilterSet filterset_class = filtersets.CircuitTerminationFilterSet
brief_prefetch_fields = ['circuit'] brief_prefetch_fields = ['circuit']
#
# Provider networks
#
class ProviderNetworkViewSet(CustomFieldModelViewSet):
queryset = ProviderNetwork.objects.prefetch_related('tags')
serializer_class = serializers.ProviderNetworkSerializer
filterset_class = filtersets.ProviderNetworkFilterSet

View File

@@ -1,25 +1,25 @@
import django_filters import django_filters
from django.db.models import Q from django.db.models import Q
from dcim.filters import CableTerminationFilterSet, PathEndpointFilterSet from dcim.filtersets import CableTerminationFilterSet
from dcim.models import Region, Site from dcim.models import Region, Site, SiteGroup
from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet from extras.filters import TagFilter
from tenancy.filters import TenancyFilterSet from netbox.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet
from utilities.filters import ( from tenancy.filtersets import TenancyFilterSet
BaseFilterSet, NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter from utilities.filters import TreeNodeMultipleChoiceFilter
)
from .choices import * from .choices import *
from .models import Circuit, CircuitTermination, CircuitType, Provider from .models import *
__all__ = ( __all__ = (
'CircuitFilterSet', 'CircuitFilterSet',
'CircuitTerminationFilterSet', 'CircuitTerminationFilterSet',
'CircuitTypeFilterSet', 'CircuitTypeFilterSet',
'ProviderNetworkFilterSet',
'ProviderFilterSet', 'ProviderFilterSet',
) )
class ProviderFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet): class ProviderFilterSet(PrimaryModelFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@@ -37,6 +37,19 @@ class ProviderFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdated
to_field_name='slug', to_field_name='slug',
label='Region (slug)', label='Region (slug)',
) )
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='circuits__terminations__site__group',
lookup_expr='in',
label='Site group (ID)',
)
site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='circuits__terminations__site__group',
lookup_expr='in',
to_field_name='slug',
label='Site group (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
field_name='circuits__terminations__site', field_name='circuits__terminations__site',
queryset=Site.objects.all(), queryset=Site.objects.all(),
@@ -66,14 +79,7 @@ class ProviderFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdated
) )
class CircuitTypeFilterSet(BaseFilterSet, NameSlugSearchFilterSet): class ProviderNetworkFilterSet(PrimaryModelFilterSet):
class Meta:
model = CircuitType
fields = ['id', 'name', 'slug']
class CircuitFilterSet(BaseFilterSet, CustomFieldModelFilterSet, TenancyFilterSet, CreatedUpdatedFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@@ -88,6 +94,48 @@ class CircuitFilterSet(BaseFilterSet, CustomFieldModelFilterSet, TenancyFilterSe
to_field_name='slug', to_field_name='slug',
label='Provider (slug)', label='Provider (slug)',
) )
tag = TagFilter()
class Meta:
model = ProviderNetwork
fields = ['id', 'name']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(description__icontains=value) |
Q(comments__icontains=value)
).distinct()
class CircuitTypeFilterSet(OrganizationalModelFilterSet):
class Meta:
model = CircuitType
fields = ['id', 'name', 'slug']
class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
provider_id = django_filters.ModelMultipleChoiceFilter(
queryset=Provider.objects.all(),
label='Provider (ID)',
)
provider = django_filters.ModelMultipleChoiceFilter(
field_name='provider__slug',
queryset=Provider.objects.all(),
to_field_name='slug',
label='Provider (slug)',
)
provider_network_id = django_filters.ModelMultipleChoiceFilter(
field_name='terminations__provider_network',
queryset=ProviderNetwork.objects.all(),
label='ProviderNetwork (ID)',
)
type_id = django_filters.ModelMultipleChoiceFilter( type_id = django_filters.ModelMultipleChoiceFilter(
queryset=CircuitType.objects.all(), queryset=CircuitType.objects.all(),
label='Circuit type (ID)', label='Circuit type (ID)',
@@ -102,17 +150,6 @@ class CircuitFilterSet(BaseFilterSet, CustomFieldModelFilterSet, TenancyFilterSe
choices=CircuitStatusChoices, choices=CircuitStatusChoices,
null_value=None null_value=None
) )
site_id = django_filters.ModelMultipleChoiceFilter(
field_name='terminations__site',
queryset=Site.objects.all(),
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
field_name='terminations__site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
)
region_id = TreeNodeMultipleChoiceFilter( region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
field_name='terminations__site__region', field_name='terminations__site__region',
@@ -126,6 +163,30 @@ class CircuitFilterSet(BaseFilterSet, CustomFieldModelFilterSet, TenancyFilterSe
to_field_name='slug', to_field_name='slug',
label='Region (slug)', label='Region (slug)',
) )
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='terminations__site__group',
lookup_expr='in',
label='Site group (ID)',
)
site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='terminations__site__group',
lookup_expr='in',
to_field_name='slug',
label='Site group (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter(
field_name='terminations__site',
queryset=Site.objects.all(),
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
field_name='terminations__site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
)
tag = TagFilter() tag = TagFilter()
class Meta: class Meta:
@@ -145,7 +206,7 @@ class CircuitFilterSet(BaseFilterSet, CustomFieldModelFilterSet, TenancyFilterSe
).distinct() ).distinct()
class CircuitTerminationFilterSet(BaseFilterSet, CableTerminationFilterSet, PathEndpointFilterSet): class CircuitTerminationFilterSet(ChangeLoggedModelFilterSet, CableTerminationFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@@ -164,10 +225,14 @@ class CircuitTerminationFilterSet(BaseFilterSet, CableTerminationFilterSet, Path
to_field_name='slug', to_field_name='slug',
label='Site (slug)', label='Site (slug)',
) )
provider_network_id = django_filters.ModelMultipleChoiceFilter(
queryset=ProviderNetwork.objects.all(),
label='ProviderNetwork (ID)',
)
class Meta: class Meta:
model = CircuitTermination model = CircuitTermination
fields = ['term_side', 'port_speed', 'upstream_speed', 'xconnect_id'] fields = ['id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():

View File

@@ -1,6 +1,7 @@
from django import forms from django import forms
from django.utils.translation import gettext as _
from dcim.models import Region, Site from dcim.models import Region, Site, SiteGroup
from extras.forms import ( from extras.forms import (
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm, AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm,
) )
@@ -8,12 +9,12 @@ from extras.models import Tag
from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.forms import TenancyFilterForm, TenancyForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import ( from utilities.forms import (
add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, CSVModelChoiceField, CSVModelForm, DatePicker, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, CSVModelChoiceField, DatePicker,
DynamicModelChoiceField, DynamicModelMultipleChoiceField, SmallTextarea, SlugField, StaticSelect2, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SelectSpeedWidget, SmallTextarea, SlugField,
StaticSelect2Multiple, TagFilterField, StaticSelect2, StaticSelect2Multiple, TagFilterField,
) )
from .choices import CircuitStatusChoices from .choices import CircuitStatusChoices
from .models import Circuit, CircuitTermination, CircuitType, Provider from .models import *
# #
@@ -33,6 +34,10 @@ class ProviderForm(BootstrapMixin, CustomFieldModelForm):
fields = [ fields = [
'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags',
] ]
fieldsets = (
('Provider', ('name', 'slug', 'asn', 'tags')),
('Support Info', ('account', 'portal_url', 'noc_contact', 'admin_contact')),
)
widgets = { widgets = {
'noc_contact': SmallTextarea( 'noc_contact': SmallTextarea(
attrs={'rows': 5} attrs={'rows': 5}
@@ -101,24 +106,101 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Provider model = Provider
q = forms.CharField( q = forms.CharField(
required=False, required=False,
label='Search' label=_('Search')
) )
region = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
to_field_name='slug', required=False,
required=False label=_('Region')
) )
site = DynamicModelMultipleChoiceField( site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='slug',
required=False, required=False,
query_params={ query_params={
'region': '$region' 'region_id': '$region_id'
} },
label=_('Site')
) )
asn = forms.IntegerField( asn = forms.IntegerField(
required=False, required=False,
label='ASN' label=_('ASN')
)
tag = TagFilterField(model)
#
# Provider networks
#
class ProviderNetworkForm(BootstrapMixin, CustomFieldModelForm):
provider = DynamicModelChoiceField(
queryset=Provider.objects.all()
)
comments = CommentField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta:
model = ProviderNetwork
fields = [
'provider', 'name', 'description', 'comments', 'tags',
]
fieldsets = (
('Provider Network', ('provider', 'name', 'description', 'tags')),
)
class ProviderNetworkCSVForm(CustomFieldModelCSVForm):
provider = CSVModelChoiceField(
queryset=Provider.objects.all(),
to_field_name='name',
help_text='Assigned provider'
)
class Meta:
model = ProviderNetwork
fields = [
'provider', 'name', 'description', 'comments',
]
class ProviderNetworkBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=ProviderNetwork.objects.all(),
widget=forms.MultipleHiddenInput
)
provider = DynamicModelChoiceField(
queryset=Provider.objects.all(),
required=False
)
description = forms.CharField(
max_length=100,
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
class Meta:
nullable_fields = [
'description', 'comments',
]
class ProviderNetworkFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = ProviderNetwork
field_order = ['q', 'provider_id']
q = forms.CharField(
required=False,
label=_('Search')
)
provider_id = DynamicModelMultipleChoiceField(
queryset=Provider.objects.all(),
required=False,
label=_('Provider')
) )
tag = TagFilterField(model) tag = TagFilterField(model)
@@ -127,7 +209,7 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
# Circuit types # Circuit types
# #
class CircuitTypeForm(BootstrapMixin, forms.ModelForm): class CircuitTypeForm(BootstrapMixin, CustomFieldModelForm):
slug = SlugField() slug = SlugField()
class Meta: class Meta:
@@ -137,7 +219,21 @@ class CircuitTypeForm(BootstrapMixin, forms.ModelForm):
] ]
class CircuitTypeCSVForm(CSVModelForm): class CircuitTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=CircuitType.objects.all(),
widget=forms.MultipleHiddenInput
)
description = forms.CharField(
max_length=200,
required=False
)
class Meta:
nullable_fields = ['description']
class CircuitTypeCSVForm(CustomFieldModelCSVForm):
slug = SlugField() slug = SlugField()
class Meta: class Meta:
@@ -171,6 +267,10 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
'cid', 'type', 'provider', 'status', 'install_date', 'commit_rate', 'description', 'tenant_group', 'tenant', 'cid', 'type', 'provider', 'status', 'install_date', 'commit_rate', 'description', 'tenant_group', 'tenant',
'comments', 'tags', 'comments', 'tags',
] ]
fieldsets = (
('Circuit', ('provider', 'cid', 'type', 'status', 'install_date', 'commit_rate', 'description', 'tags')),
('Tenancy', ('tenant_group', 'tenant')),
)
help_texts = { help_texts = {
'cid': "Unique circuit ID", 'cid': "Unique circuit ID",
'commit_rate': "Committed rate", 'commit_rate': "Committed rate",
@@ -178,6 +278,7 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
widgets = { widgets = {
'status': StaticSelect2(), 'status': StaticSelect2(),
'install_date': DatePicker(), 'install_date': DatePicker(),
'commit_rate': SelectSpeedWidget(),
} }
@@ -256,44 +357,53 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
model = Circuit model = Circuit
field_order = [ field_order = [
'q', 'type', 'provider', 'status', 'region', 'site', 'tenant_group', 'tenant', 'commit_rate', 'q', 'type_id', 'provider_id', 'provider_network_id', 'status', 'region_id', 'site_id', 'tenant_group_id', 'tenant_id',
'commit_rate',
] ]
q = forms.CharField( q = forms.CharField(
required=False, required=False,
label='Search' label=_('Search')
) )
type = DynamicModelMultipleChoiceField( type_id = DynamicModelMultipleChoiceField(
queryset=CircuitType.objects.all(), queryset=CircuitType.objects.all(),
to_field_name='slug', required=False,
required=False label=_('Type')
) )
provider = DynamicModelMultipleChoiceField( provider_id = DynamicModelMultipleChoiceField(
queryset=Provider.objects.all(), queryset=Provider.objects.all(),
to_field_name='slug', required=False,
required=False label=_('Provider')
)
provider_network_id = DynamicModelMultipleChoiceField(
queryset=ProviderNetwork.objects.all(),
required=False,
query_params={
'provider_id': '$provider_id'
},
label=_('Provider network')
) )
status = forms.MultipleChoiceField( status = forms.MultipleChoiceField(
choices=CircuitStatusChoices, choices=CircuitStatusChoices,
required=False, required=False,
widget=StaticSelect2Multiple() widget=StaticSelect2Multiple()
) )
region = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
to_field_name='slug', required=False,
required=False label=_('Region')
) )
site = DynamicModelMultipleChoiceField( site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='slug',
required=False, required=False,
query_params={ query_params={
'region': '$region' 'region_id': '$region_id'
} },
label=_('Site')
) )
commit_rate = forms.IntegerField( commit_rate = forms.IntegerField(
required=False, required=False,
min_value=0, min_value=0,
label='Commit rate (Kbps)' label=_('Commit rate (Kbps)')
) )
tag = TagFilterField(model) tag = TagFilterField(model)
@@ -310,17 +420,31 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
'sites': '$site' 'sites': '$site'
} }
) )
site_group = DynamicModelChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
initial_params={
'sites': '$site'
}
)
site = DynamicModelChoiceField( site = DynamicModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
query_params={ query_params={
'region_id': '$region' 'region_id': '$region',
} 'group_id': '$site_group',
},
required=False
)
provider_network = DynamicModelChoiceField(
queryset=ProviderNetwork.objects.all(),
required=False
) )
class Meta: class Meta:
model = CircuitTermination model = CircuitTermination
fields = [ fields = [
'term_side', 'region', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', 'description', 'term_side', 'region', 'site_group', 'site', 'provider_network', 'mark_connected', 'port_speed',
'upstream_speed', 'xconnect_id', 'pp_info', 'description',
] ]
help_texts = { help_texts = {
'port_speed': "Physical circuit speed", 'port_speed': "Physical circuit speed",
@@ -329,4 +453,11 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
} }
widgets = { widgets = {
'term_side': forms.HiddenInput(), 'term_side': forms.HiddenInput(),
'port_speed': SelectSpeedWidget(),
'upstream_speed': SelectSpeedWidget(),
} }
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['provider_network'].widget.add_query_param('provider_id', self.instance.circuit.provider_id)

View File

@@ -0,0 +1,47 @@
import django.core.serializers.json
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0024_standardize_name_length'),
]
operations = [
migrations.AddField(
model_name='circuittype',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AlterField(
model_name='circuit',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='circuittermination',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='circuittype',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='provider',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AddField(
model_name='circuittermination',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='circuittermination',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
]

View File

@@ -0,0 +1,16 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0025_standardize_models'),
]
operations = [
migrations.AddField(
model_name='circuittermination',
name='mark_connected',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,65 @@
import django.core.serializers.json
from django.db import migrations, models
import django.db.models.deletion
import taggit.managers
class Migration(migrations.Migration):
dependencies = [
('extras', '0058_journalentry'),
('circuits', '0026_mark_connected'),
]
operations = [
# Create the new ProviderNetwork model
migrations.CreateModel(
name='ProviderNetwork',
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100)),
('description', models.CharField(blank=True, max_length=200)),
('comments', models.TextField(blank=True)),
('provider', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='networks', to='circuits.provider')),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
'ordering': ('provider', 'name'),
},
),
migrations.AddConstraint(
model_name='providernetwork',
constraint=models.UniqueConstraint(fields=('provider', 'name'), name='circuits_providernetwork_provider_name'),
),
migrations.AlterUniqueTogether(
name='providernetwork',
unique_together={('provider', 'name')},
),
# Add ProviderNetwork FK to CircuitTermination
migrations.AddField(
model_name='circuittermination',
name='provider_network',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuit_terminations', to='circuits.providernetwork'),
),
migrations.AlterField(
model_name='circuittermination',
name='site',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuit_terminations', to='dcim.site'),
),
# Add FKs to CircuitTermination on Circuit
migrations.AddField(
model_name='circuit',
name='termination_a',
field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='circuits.circuittermination'),
),
migrations.AddField(
model_name='circuit',
name='termination_z',
field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='circuits.circuittermination'),
),
]

View File

@@ -0,0 +1,37 @@
import sys
from django.db import migrations
def cache_circuit_terminations(apps, schema_editor):
Circuit = apps.get_model('circuits', 'Circuit')
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
if 'test' not in sys.argv:
print(f"\n Caching circuit terminations...", flush=True)
a_terminations = {
ct.circuit_id: ct.pk for ct in CircuitTermination.objects.filter(term_side='A')
}
z_terminations = {
ct.circuit_id: ct.pk for ct in CircuitTermination.objects.filter(term_side='Z')
}
for circuit in Circuit.objects.all():
Circuit.objects.filter(pk=circuit.pk).update(
termination_a_id=a_terminations.get(circuit.pk),
termination_z_id=z_terminations.get(circuit.pk),
)
class Migration(migrations.Migration):
dependencies = [
('circuits', '0027_providernetwork'),
]
operations = [
migrations.RunPython(
code=cache_circuit_terminations,
reverse_code=migrations.RunPython.noop
),
]

View File

@@ -0,0 +1,32 @@
from django.db import migrations
from django.db.models import Q
def delete_obsolete_cablepaths(apps, schema_editor):
"""
Delete all CablePath instances which originate or terminate at a CircuitTermination.
"""
ContentType = apps.get_model('contenttypes', 'ContentType')
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
CablePath = apps.get_model('dcim', 'CablePath')
ct = ContentType.objects.get_for_model(CircuitTermination)
CablePath.objects.filter(Q(origin_type=ct) | Q(destination_type=ct)).delete()
class Migration(migrations.Migration):
dependencies = [
('circuits', '0028_cache_circuit_terminations'),
]
operations = [
migrations.RemoveField(
model_name='circuittermination',
name='_path',
),
migrations.RunPython(
code=delete_obsolete_cablepaths,
reverse_code=migrations.RunPython.noop
),
]

View File

@@ -1,27 +1,27 @@
from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from taggit.managers import TaggableManager
from dcim.fields import ASNField from dcim.fields import ASNField
from dcim.models import CableTermination, PathEndpoint from dcim.models import CableTermination, PathEndpoint
from extras.models import ChangeLoggedModel, CustomFieldModel, ObjectChange, TaggedItem from extras.models import ObjectChange
from extras.utils import extras_features from extras.utils import extras_features
from netbox.models import BigIDModel, ChangeLoggedModel, OrganizationalModel, PrimaryModel
from utilities.querysets import RestrictedQuerySet from utilities.querysets import RestrictedQuerySet
from utilities.utils import serialize_object
from .choices import * from .choices import *
from .querysets import CircuitQuerySet
__all__ = ( __all__ = (
'Circuit', 'Circuit',
'CircuitTermination', 'CircuitTermination',
'CircuitType', 'CircuitType',
'ProviderNetwork',
'Provider', 'Provider',
) )
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Provider(ChangeLoggedModel, CustomFieldModel): class Provider(PrimaryModel):
""" """
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
stores information pertinent to the user's relationship with the Provider. stores information pertinent to the user's relationship with the Provider.
@@ -60,7 +60,6 @@ class Provider(ChangeLoggedModel, CustomFieldModel):
comments = models.TextField( comments = models.TextField(
blank=True blank=True
) )
tags = TaggableManager(through=TaggedItem)
objects = RestrictedQuerySet.as_manager() objects = RestrictedQuerySet.as_manager()
@@ -78,7 +77,7 @@ class Provider(ChangeLoggedModel, CustomFieldModel):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
return reverse('circuits:provider', args=[self.slug]) return reverse('circuits:provider', args=[self.pk])
def to_csv(self): def to_csv(self):
return ( return (
@@ -93,7 +92,65 @@ class Provider(ChangeLoggedModel, CustomFieldModel):
) )
class CircuitType(ChangeLoggedModel): #
# Provider networks
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class ProviderNetwork(PrimaryModel):
"""
This represents a provider network which exists outside of NetBox, the details of which are unknown or
unimportant to the user.
"""
name = models.CharField(
max_length=100
)
provider = models.ForeignKey(
to='circuits.Provider',
on_delete=models.PROTECT,
related_name='networks'
)
description = models.CharField(
max_length=200,
blank=True
)
comments = models.TextField(
blank=True
)
csv_headers = [
'provider', 'name', 'description', 'comments',
]
objects = RestrictedQuerySet.as_manager()
class Meta:
ordering = ('provider', 'name')
constraints = (
models.UniqueConstraint(
fields=('provider', 'name'),
name='circuits_providernetwork_provider_name'
),
)
unique_together = ('provider', 'name')
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('circuits:providernetwork', args=[self.pk])
def to_csv(self):
return (
self.provider.name,
self.name,
self.description,
self.comments,
)
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class CircuitType(OrganizationalModel):
""" """
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
"Long Haul," "Metro," or "Out-of-Band". "Long Haul," "Metro," or "Out-of-Band".
@@ -122,7 +179,7 @@ class CircuitType(ChangeLoggedModel):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
return "{}?type={}".format(reverse('circuits:circuit_list'), self.slug) return reverse('circuits:circuittype', args=[self.pk])
def to_csv(self): def to_csv(self):
return ( return (
@@ -132,8 +189,8 @@ class CircuitType(ChangeLoggedModel):
) )
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Circuit(ChangeLoggedModel, CustomFieldModel): class Circuit(PrimaryModel):
""" """
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
circuits. Each circuit is also assigned a CircuitType and a Site. Circuit port speed and commit rate are measured circuits. Each circuit is also assigned a CircuitType and a Site. Circuit port speed and commit rate are measured
@@ -182,8 +239,25 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
blank=True blank=True
) )
objects = CircuitQuerySet.as_manager() # Cache associated CircuitTerminations
tags = TaggableManager(through=TaggedItem) termination_a = models.ForeignKey(
to='circuits.CircuitTermination',
on_delete=models.SET_NULL,
related_name='+',
editable=False,
blank=True,
null=True
)
termination_z = models.ForeignKey(
to='circuits.CircuitTermination',
on_delete=models.SET_NULL,
related_name='+',
editable=False,
blank=True,
null=True
)
objects = RestrictedQuerySet.as_manager()
csv_headers = [ csv_headers = [
'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
@@ -218,22 +292,9 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
def get_status_class(self): def get_status_class(self):
return CircuitStatusChoices.CSS_CLASSES.get(self.status) return CircuitStatusChoices.CSS_CLASSES.get(self.status)
def _get_termination(self, side):
for ct in self.terminations.all():
if ct.term_side == side:
return ct
return None
@property @extras_features('webhooks')
def termination_a(self): class CircuitTermination(ChangeLoggedModel, CableTermination):
return self._get_termination('A')
@property
def termination_z(self):
return self._get_termination('Z')
class CircuitTermination(PathEndpoint, CableTermination):
circuit = models.ForeignKey( circuit = models.ForeignKey(
to='circuits.Circuit', to='circuits.Circuit',
on_delete=models.CASCADE, on_delete=models.CASCADE,
@@ -247,7 +308,16 @@ class CircuitTermination(PathEndpoint, CableTermination):
site = models.ForeignKey( site = models.ForeignKey(
to='dcim.Site', to='dcim.Site',
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='circuit_terminations' related_name='circuit_terminations',
blank=True,
null=True
)
provider_network = models.ForeignKey(
to=ProviderNetwork,
on_delete=models.PROTECT,
related_name='circuit_terminations',
blank=True,
null=True
) )
port_speed = models.PositiveIntegerField( port_speed = models.PositiveIntegerField(
verbose_name='Port speed (Kbps)', verbose_name='Port speed (Kbps)',
@@ -282,26 +352,33 @@ class CircuitTermination(PathEndpoint, CableTermination):
unique_together = ['circuit', 'term_side'] unique_together = ['circuit', 'term_side']
def __str__(self): def __str__(self):
return 'Side {}'.format(self.get_term_side_display()) return f'Termination {self.term_side}: {self.site or self.provider_network}'
def get_absolute_url(self):
if self.site:
return self.site.get_absolute_url()
return self.provider_network.get_absolute_url()
def clean(self):
super().clean()
# Must define either site *or* provider network
if self.site is None and self.provider_network is None:
raise ValidationError("A circuit termination must attach to either a site or a provider network.")
if self.site and self.provider_network:
raise ValidationError("A circuit termination cannot attach to both a site and a provider network.")
def to_objectchange(self, action): def to_objectchange(self, action):
# Annotate the parent Circuit # Annotate the parent Circuit
try: try:
related_object = self.circuit circuit = self.circuit
except Circuit.DoesNotExist: except Circuit.DoesNotExist:
# Parent circuit has been deleted # Parent circuit has been deleted
related_object = None circuit = None
return super().to_objectchange(action, related_object=circuit)
return ObjectChange(
changed_object=self,
object_repr=str(self),
action=action,
related_object=related_object,
object_data=serialize_object(self)
)
@property @property
def parent(self): def parent_object(self):
return self.circuit return self.circuit
def get_peer_termination(self): def get_peer_termination(self):

View File

@@ -1,17 +0,0 @@
from django.db.models import OuterRef, Subquery
from utilities.querysets import RestrictedQuerySet
class CircuitQuerySet(RestrictedQuerySet):
def annotate_sites(self):
"""
Annotate the A and Z termination site names for ordering.
"""
from circuits.models import CircuitTermination
_terminations = CircuitTermination.objects.filter(circuit=OuterRef('pk'))
return self.annotate(
a_side=Subquery(_terminations.filter(term_side='A').values('site__name')[:1]),
z_side=Subquery(_terminations.filter(term_side='Z').values('site__name')[:1]),
)

View File

@@ -1,17 +1,26 @@
from django.db.models.signals import post_delete, post_save from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.utils import timezone
from .models import Circuit, CircuitTermination from dcim.signals import rebuild_paths
from .models import CircuitTermination
@receiver(post_save, sender=CircuitTermination)
def update_circuit(instance, **kwargs):
"""
When a CircuitTermination has been modified, update its parent Circuit.
"""
termination_name = f'termination_{instance.term_side.lower()}'
setattr(instance.circuit, termination_name, instance)
instance.circuit.save()
@receiver((post_save, post_delete), sender=CircuitTermination) @receiver((post_save, post_delete), sender=CircuitTermination)
def update_circuit(instance, **kwargs): def rebuild_cablepaths(instance, raw=False, **kwargs):
""" """
When a CircuitTermination has been modified, update the last_updated time of its parent Circuit. Rebuild any CablePaths which traverse the peer CircuitTermination.
""" """
circuits = Circuit.objects.filter(pk=instance.circuit_id) if not raw:
time = timezone.now() peer_termination = instance.get_peer_termination()
for circuit in circuits: if peer_termination:
circuit.last_updated = time rebuild_paths(peer_termination)
circuit.save()

View File

@@ -1,9 +1,18 @@
import django_tables2 as tables import django_tables2 as tables
from django_tables2.utils import Accessor from django_tables2.utils import Accessor
from tenancy.tables import COL_TENANT from tenancy.tables import TenantColumn
from utilities.tables import BaseTable, ButtonsColumn, ChoiceFieldColumn, TagColumn, ToggleColumn from utilities.tables import BaseTable, ButtonsColumn, ChoiceFieldColumn, TagColumn, ToggleColumn
from .models import Circuit, CircuitType, Provider from .models import *
CIRCUITTERMINATION_LINK = """
{% if value.site %}
<a href="{{ value.site.get_absolute_url }}">{{ value.site }}</a>
{% elif value.provider_network %}
<a href="{{ value.provider_network.get_absolute_url }}">{{ value.provider_network }}</a>
{% endif %}
"""
# #
@@ -12,7 +21,9 @@ from .models import Circuit, CircuitType, Provider
class ProviderTable(BaseTable): class ProviderTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn() name = tables.Column(
linkify=True
)
circuit_count = tables.Column( circuit_count = tables.Column(
accessor=Accessor('count_circuits'), accessor=Accessor('count_circuits'),
verbose_name='Circuits' verbose_name='Circuits'
@@ -29,17 +40,41 @@ class ProviderTable(BaseTable):
default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count') default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count')
#
# Provider networks
#
class ProviderNetworkTable(BaseTable):
pk = ToggleColumn()
name = tables.Column(
linkify=True
)
provider = tables.Column(
linkify=True
)
tags = TagColumn(
url_name='circuits:providernetwork_list'
)
class Meta(BaseTable.Meta):
model = ProviderNetwork
fields = ('pk', 'name', 'provider', 'description', 'tags')
default_columns = ('pk', 'name', 'provider', 'description')
# #
# Circuit types # Circuit types
# #
class CircuitTypeTable(BaseTable): class CircuitTypeTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn() name = tables.Column(
linkify=True
)
circuit_count = tables.Column( circuit_count = tables.Column(
verbose_name='Circuits' verbose_name='Circuits'
) )
actions = ButtonsColumn(CircuitType, pk_field='slug') actions = ButtonsColumn(CircuitType)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = CircuitType model = CircuitType
@@ -53,22 +88,22 @@ class CircuitTypeTable(BaseTable):
class CircuitTable(BaseTable): class CircuitTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
cid = tables.LinkColumn( cid = tables.Column(
linkify=True,
verbose_name='ID' verbose_name='ID'
) )
provider = tables.LinkColumn( provider = tables.Column(
viewname='circuits:provider', linkify=True
args=[Accessor('provider__slug')]
) )
status = ChoiceFieldColumn() status = ChoiceFieldColumn()
tenant = tables.TemplateColumn( tenant = TenantColumn()
template_code=COL_TENANT termination_a = tables.TemplateColumn(
template_code=CIRCUITTERMINATION_LINK,
verbose_name='Side A'
) )
a_side = tables.Column( termination_z = tables.TemplateColumn(
verbose_name='A Side' template_code=CIRCUITTERMINATION_LINK,
) verbose_name='Side Z'
z_side = tables.Column(
verbose_name='Z Side'
) )
tags = TagColumn( tags = TagColumn(
url_name='circuits:circuit_list' url_name='circuits:circuit_list'
@@ -77,7 +112,9 @@ class CircuitTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Circuit model = Circuit
fields = ( fields = (
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'a_side', 'z_side', 'install_date', 'commit_rate', 'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'install_date',
'description', 'tags', 'commit_rate', 'description', 'tags',
)
default_columns = (
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'description',
) )
default_columns = ('pk', 'cid', 'provider', 'type', 'status', 'tenant', 'a_side', 'z_side', 'description')

View File

@@ -1,7 +1,7 @@
from django.urls import reverse from django.urls import reverse
from circuits.choices import * from circuits.choices import *
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider from circuits.models import *
from dcim.models import Site from dcim.models import Site
from utilities.testing import APITestCase, APIViewTestCases from utilities.testing import APITestCase, APIViewTestCases
@@ -17,7 +17,7 @@ class AppTest(APITestCase):
class ProviderTest(APIViewTestCases.APIViewTestCase): class ProviderTest(APIViewTestCases.APIViewTestCase):
model = Provider model = Provider
brief_fields = ['circuit_count', 'id', 'name', 'slug', 'url'] brief_fields = ['circuit_count', 'display', 'id', 'name', 'slug', 'url']
create_data = [ create_data = [
{ {
'name': 'Provider 4', 'name': 'Provider 4',
@@ -49,7 +49,7 @@ class ProviderTest(APIViewTestCases.APIViewTestCase):
class CircuitTypeTest(APIViewTestCases.APIViewTestCase): class CircuitTypeTest(APIViewTestCases.APIViewTestCase):
model = CircuitType model = CircuitType
brief_fields = ['circuit_count', 'id', 'name', 'slug', 'url'] brief_fields = ['circuit_count', 'display', 'id', 'name', 'slug', 'url']
create_data = ( create_data = (
{ {
'name': 'Circuit Type 4', 'name': 'Circuit Type 4',
@@ -81,7 +81,7 @@ class CircuitTypeTest(APIViewTestCases.APIViewTestCase):
class CircuitTest(APIViewTestCases.APIViewTestCase): class CircuitTest(APIViewTestCases.APIViewTestCase):
model = Circuit model = Circuit
brief_fields = ['cid', 'id', 'url'] brief_fields = ['cid', 'display', 'id', 'url']
bulk_update_data = { bulk_update_data = {
'status': 'planned', 'status': 'planned',
} }
@@ -129,7 +129,7 @@ class CircuitTest(APIViewTestCases.APIViewTestCase):
class CircuitTerminationTest(APIViewTestCases.APIViewTestCase): class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
model = CircuitTermination model = CircuitTermination
brief_fields = ['cable', 'circuit', 'id', 'term_side', 'url'] brief_fields = ['_occupied', 'cable', 'circuit', 'display', 'id', 'term_side', 'url']
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@@ -178,3 +178,43 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
cls.bulk_update_data = { cls.bulk_update_data = {
'port_speed': 123456 'port_speed': 123456
} }
class ProviderNetworkTest(APIViewTestCases.APIViewTestCase):
model = ProviderNetwork
brief_fields = ['display', 'id', 'name', 'url']
@classmethod
def setUpTestData(cls):
providers = (
Provider(name='Provider 1', slug='provider-1'),
Provider(name='Provider 2', slug='provider-2'),
)
Provider.objects.bulk_create(providers)
provider_networks = (
ProviderNetwork(name='Provider Network 1', provider=providers[0]),
ProviderNetwork(name='Provider Network 2', provider=providers[0]),
ProviderNetwork(name='Provider Network 3', provider=providers[0]),
)
ProviderNetwork.objects.bulk_create(provider_networks)
cls.create_data = [
{
'name': 'Provider Network 4',
'provider': providers[0].pk,
},
{
'name': 'Provider Network 5',
'provider': providers[0].pk,
},
{
'name': 'Provider Network 6',
'provider': providers[0].pk,
},
]
cls.bulk_update_data = {
'provider': providers[1].pk,
'description': 'New description',
}

View File

@@ -1,13 +1,14 @@
from django.test import TestCase from django.test import TestCase
from circuits.choices import * from circuits.choices import *
from circuits.filters import * from circuits.filtersets import *
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider from circuits.models import *
from dcim.models import Cable, Region, Site from dcim.models import Cable, Region, Site, SiteGroup
from tenancy.models import Tenant, TenantGroup from tenancy.models import Tenant, TenantGroup
from utilities.testing import ChangeLoggedFilterSetTests
class ProviderTestCase(TestCase): class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Provider.objects.all() queryset = Provider.objects.all()
filterset = ProviderFilterSet filterset = ProviderFilterSet
@@ -27,13 +28,20 @@ class ProviderTestCase(TestCase):
Region(name='Test Region 1', slug='test-region-1'), Region(name='Test Region 1', slug='test-region-1'),
Region(name='Test Region 2', slug='test-region-2'), Region(name='Test Region 2', slug='test-region-2'),
) )
# Can't use bulk_create for models with MPTT fields
for r in regions: for r in regions:
r.save() r.save()
site_groups = (
SiteGroup(name='Site Group 1', slug='site-group-1'),
SiteGroup(name='Site Group 2', slug='site-group-2'),
SiteGroup(name='Site Group 3', slug='site-group-3'),
)
for site_group in site_groups:
site_group.save()
sites = ( sites = (
Site(name='Test Site 1', slug='test-site-1', region=regions[0]), Site(name='Test Site 1', slug='test-site-1', region=regions[0], group=site_groups[0]),
Site(name='Test Site 2', slug='test-site-2', region=regions[1]), Site(name='Test Site 2', slug='test-site-2', region=regions[1], group=site_groups[1]),
) )
Site.objects.bulk_create(sites) Site.objects.bulk_create(sites)
@@ -54,10 +62,6 @@ class ProviderTestCase(TestCase):
CircuitTermination(circuit=circuits[1], site=sites[0], term_side='A'), CircuitTermination(circuit=circuits[1], site=sites[0], term_side='A'),
)) ))
def test_id(self):
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self): def test_name(self):
params = {'name': ['Provider 1', 'Provider 2']} params = {'name': ['Provider 1', 'Provider 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -74,13 +78,6 @@ class ProviderTestCase(TestCase):
params = {'account': ['1234', '2345']} params = {'account': ['1234', '2345']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_site(self):
sites = Site.objects.all()[:2]
params = {'site_id': [sites[0].pk, sites[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'site': [sites[0].slug, sites[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_region(self): def test_region(self):
regions = Region.objects.all()[:2] regions = Region.objects.all()[:2]
params = {'region_id': [regions[0].pk, regions[1].pk]} params = {'region_id': [regions[0].pk, regions[1].pk]}
@@ -88,8 +85,22 @@ class ProviderTestCase(TestCase):
params = {'region': [regions[0].slug, regions[1].slug]} params = {'region': [regions[0].slug, regions[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_site_group(self):
site_groups = SiteGroup.objects.all()[:2]
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class CircuitTypeTestCase(TestCase): def test_site(self):
sites = Site.objects.all()[:2]
params = {'site_id': [sites[0].pk, sites[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'site': [sites[0].slug, sites[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class CircuitTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = CircuitType.objects.all() queryset = CircuitType.objects.all()
filterset = CircuitTypeFilterSet filterset = CircuitTypeFilterSet
@@ -102,10 +113,6 @@ class CircuitTypeTestCase(TestCase):
CircuitType(name='Circuit Type 3', slug='circuit-type-3'), CircuitType(name='Circuit Type 3', slug='circuit-type-3'),
)) ))
def test_id(self):
params = {'id': [self.queryset.first().pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_name(self): def test_name(self):
params = {'name': ['Circuit Type 1']} params = {'name': ['Circuit Type 1']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
@@ -115,7 +122,7 @@ class CircuitTypeTestCase(TestCase):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
class CircuitTestCase(TestCase): class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Circuit.objects.all() queryset = Circuit.objects.all()
filterset = CircuitFilterSet filterset = CircuitFilterSet
@@ -127,14 +134,21 @@ class CircuitTestCase(TestCase):
Region(name='Test Region 2', slug='test-region-2'), Region(name='Test Region 2', slug='test-region-2'),
Region(name='Test Region 3', slug='test-region-3'), Region(name='Test Region 3', slug='test-region-3'),
) )
# Can't use bulk_create for models with MPTT fields
for r in regions: for r in regions:
r.save() r.save()
site_groups = (
SiteGroup(name='Site Group 1', slug='site-group-1'),
SiteGroup(name='Site Group 2', slug='site-group-2'),
SiteGroup(name='Site Group 3', slug='site-group-3'),
)
for site_group in site_groups:
site_group.save()
sites = ( sites = (
Site(name='Test Site 1', slug='test-site-1', region=regions[0]), Site(name='Test Site 1', slug='test-site-1', region=regions[0], group=site_groups[0]),
Site(name='Test Site 2', slug='test-site-2', region=regions[1]), Site(name='Test Site 2', slug='test-site-2', region=regions[1], group=site_groups[1]),
Site(name='Test Site 3', slug='test-site-3', region=regions[2]), Site(name='Test Site 3', slug='test-site-3', region=regions[2], group=site_groups[2]),
) )
Site.objects.bulk_create(sites) Site.objects.bulk_create(sites)
@@ -165,6 +179,13 @@ class CircuitTestCase(TestCase):
) )
Provider.objects.bulk_create(providers) Provider.objects.bulk_create(providers)
provider_networks = (
ProviderNetwork(name='Provider Network 1', provider=providers[1]),
ProviderNetwork(name='Provider Network 2', provider=providers[1]),
ProviderNetwork(name='Provider Network 3', provider=providers[1]),
)
ProviderNetwork.objects.bulk_create(provider_networks)
circuits = ( circuits = (
Circuit(provider=providers[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 1', install_date='2020-01-01', commit_rate=1000, status=CircuitStatusChoices.STATUS_ACTIVE), Circuit(provider=providers[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 1', install_date='2020-01-01', commit_rate=1000, status=CircuitStatusChoices.STATUS_ACTIVE),
Circuit(provider=providers[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 2', install_date='2020-01-02', commit_rate=2000, status=CircuitStatusChoices.STATUS_ACTIVE), Circuit(provider=providers[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 2', install_date='2020-01-02', commit_rate=2000, status=CircuitStatusChoices.STATUS_ACTIVE),
@@ -179,13 +200,12 @@ class CircuitTestCase(TestCase):
CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A'), CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A'),
CircuitTermination(circuit=circuits[1], site=sites[1], term_side='A'), CircuitTermination(circuit=circuits[1], site=sites[1], term_side='A'),
CircuitTermination(circuit=circuits[2], site=sites[2], term_side='A'), CircuitTermination(circuit=circuits[2], site=sites[2], term_side='A'),
CircuitTermination(circuit=circuits[3], provider_network=provider_networks[0], term_side='A'),
CircuitTermination(circuit=circuits[4], provider_network=provider_networks[1], term_side='A'),
CircuitTermination(circuit=circuits[5], provider_network=provider_networks[2], term_side='A'),
)) ))
CircuitTermination.objects.bulk_create(circuit_terminations) CircuitTermination.objects.bulk_create(circuit_terminations)
def test_id(self):
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_cid(self): def test_cid(self):
params = {'cid': ['Test Circuit 1', 'Test Circuit 2']} params = {'cid': ['Test Circuit 1', 'Test Circuit 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -205,6 +225,11 @@ class CircuitTestCase(TestCase):
params = {'provider': [provider.slug]} params = {'provider': [provider.slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
def test_provider_network(self):
provider_networks = ProviderNetwork.objects.all()[:2]
params = {'provider_network_id': [provider_networks[0].pk, provider_networks[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_type(self): def test_type(self):
circuit_type = CircuitType.objects.first() circuit_type = CircuitType.objects.first()
params = {'type_id': [circuit_type.pk]} params = {'type_id': [circuit_type.pk]}
@@ -223,6 +248,13 @@ class CircuitTestCase(TestCase):
params = {'region': [regions[0].slug, regions[1].slug]} params = {'region': [regions[0].slug, regions[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_site_group(self):
site_groups = SiteGroup.objects.all()[:2]
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_site(self): def test_site(self):
sites = Site.objects.all()[:2] sites = Site.objects.all()[:2]
params = {'site_id': [sites[0].pk, sites[1].pk]} params = {'site_id': [sites[0].pk, sites[1].pk]}
@@ -245,7 +277,7 @@ class CircuitTestCase(TestCase):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
class CircuitTerminationTestCase(TestCase): class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = CircuitTermination.objects.all() queryset = CircuitTermination.objects.all()
filterset = CircuitTerminationFilterSet filterset = CircuitTerminationFilterSet
@@ -253,14 +285,14 @@ class CircuitTerminationTestCase(TestCase):
def setUpTestData(cls): def setUpTestData(cls):
sites = ( sites = (
Site(name='Test Site 1', slug='test-site-1'), Site(name='Site 1', slug='site-1'),
Site(name='Test Site 2', slug='test-site-2'), Site(name='Site 2', slug='site-2'),
Site(name='Test Site 3', slug='test-site-3'), Site(name='Site 3', slug='site-3'),
) )
Site.objects.bulk_create(sites) Site.objects.bulk_create(sites)
circuit_types = ( circuit_types = (
CircuitType(name='Test Circuit Type 1', slug='test-circuit-type-1'), CircuitType(name='Circuit Type 1', slug='circuit-type-1'),
) )
CircuitType.objects.bulk_create(circuit_types) CircuitType.objects.bulk_create(circuit_types)
@@ -269,10 +301,20 @@ class CircuitTerminationTestCase(TestCase):
) )
Provider.objects.bulk_create(providers) Provider.objects.bulk_create(providers)
provider_networks = (
ProviderNetwork(name='Provider Network 1', provider=providers[0]),
ProviderNetwork(name='Provider Network 2', provider=providers[0]),
ProviderNetwork(name='Provider Network 3', provider=providers[0]),
)
ProviderNetwork.objects.bulk_create(provider_networks)
circuits = ( circuits = (
Circuit(provider=providers[0], type=circuit_types[0], cid='Test Circuit 1'), Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 1'),
Circuit(provider=providers[0], type=circuit_types[0], cid='Test Circuit 2'), Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 2'),
Circuit(provider=providers[0], type=circuit_types[0], cid='Test Circuit 3'), Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 3'),
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 4'),
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 5'),
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 6'),
) )
Circuit.objects.bulk_create(circuits) Circuit.objects.bulk_create(circuits)
@@ -283,6 +325,9 @@ class CircuitTerminationTestCase(TestCase):
CircuitTermination(circuit=circuits[1], site=sites[2], term_side='Z', port_speed=2000, upstream_speed=2000, xconnect_id='JKL'), CircuitTermination(circuit=circuits[1], site=sites[2], term_side='Z', port_speed=2000, upstream_speed=2000, xconnect_id='JKL'),
CircuitTermination(circuit=circuits[2], site=sites[2], term_side='A', port_speed=3000, upstream_speed=3000, xconnect_id='MNO'), CircuitTermination(circuit=circuits[2], site=sites[2], term_side='A', port_speed=3000, upstream_speed=3000, xconnect_id='MNO'),
CircuitTermination(circuit=circuits[2], site=sites[0], term_side='Z', port_speed=3000, upstream_speed=3000, xconnect_id='PQR'), CircuitTermination(circuit=circuits[2], site=sites[0], term_side='Z', port_speed=3000, upstream_speed=3000, xconnect_id='PQR'),
CircuitTermination(circuit=circuits[3], provider_network=provider_networks[0], term_side='A'),
CircuitTermination(circuit=circuits[4], provider_network=provider_networks[1], term_side='A'),
CircuitTermination(circuit=circuits[5], provider_network=provider_networks[2], term_side='A'),
)) ))
CircuitTermination.objects.bulk_create(circuit_terminations) CircuitTermination.objects.bulk_create(circuit_terminations)
@@ -290,7 +335,7 @@ class CircuitTerminationTestCase(TestCase):
def test_term_side(self): def test_term_side(self):
params = {'term_side': 'A'} params = {'term_side': 'A'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
def test_port_speed(self): def test_port_speed(self):
params = {'port_speed': ['1000', '2000']} params = {'port_speed': ['1000', '2000']}
@@ -316,12 +361,44 @@ class CircuitTerminationTestCase(TestCase):
params = {'site': [sites[0].slug, sites[1].slug]} params = {'site': [sites[0].slug, sites[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_provider_network(self):
provider_networks = ProviderNetwork.objects.all()[:2]
params = {'provider_network_id': [provider_networks[0].pk, provider_networks[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_cabled(self): def test_cabled(self):
params = {'cabled': True} params = {'cabled': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_connected(self):
params = {'connected': True} class ProviderNetworkTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = ProviderNetwork.objects.all()
filterset = ProviderNetworkFilterSet
@classmethod
def setUpTestData(cls):
providers = (
Provider(name='Provider 1', slug='provider-1'),
Provider(name='Provider 2', slug='provider-2'),
Provider(name='Provider 3', slug='provider-3'),
)
Provider.objects.bulk_create(providers)
provider_networks = (
ProviderNetwork(name='Provider Network 1', provider=providers[0]),
ProviderNetwork(name='Provider Network 2', provider=providers[1]),
ProviderNetwork(name='Provider Network 3', provider=providers[2]),
)
ProviderNetwork.objects.bulk_create(provider_networks)
def test_name(self):
params = {'name': ['Provider Network 1', 'Provider Network 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_provider(self):
providers = Provider.objects.all()[:2]
params = {'provider_id': [providers[0].pk, providers[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'provider': [providers[0].slug, providers[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'connected': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)

View File

@@ -1,8 +1,12 @@
import datetime import datetime
from django.test import override_settings
from django.urls import reverse
from circuits.choices import * from circuits.choices import *
from circuits.models import Circuit, CircuitType, Provider from circuits.models import *
from utilities.testing import ViewTestCases from dcim.models import Cable, Interface, Site
from utilities.testing import ViewTestCases, create_tags, create_test_device
class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase): class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase):
@@ -17,7 +21,7 @@ class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase):
Provider(name='Provider 3', slug='provider-3', asn=65003), Provider(name='Provider 3', slug='provider-3', asn=65003),
]) ])
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie') tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = { cls.form_data = {
'name': 'Provider X', 'name': 'Provider X',
@@ -73,6 +77,10 @@ class CircuitTypeTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
"Circuit Type 6,circuit-type-6", "Circuit Type 6,circuit-type-6",
) )
cls.bulk_edit_data = {
'description': 'Foo',
}
class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase): class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = Circuit model = Circuit
@@ -98,7 +106,7 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
Circuit(cid='Circuit 3', provider=providers[0], type=circuittypes[0]), Circuit(cid='Circuit 3', provider=providers[0], type=circuittypes[0]),
]) ])
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie') tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = { cls.form_data = {
'cid': 'Circuit X', 'cid': 'Circuit X',
@@ -129,3 +137,99 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'description': 'New description', 'description': 'New description',
'comments': 'New comments', 'comments': 'New comments',
} }
class ProviderNetworkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = ProviderNetwork
@classmethod
def setUpTestData(cls):
providers = (
Provider(name='Provider 1', slug='provider-1'),
Provider(name='Provider 2', slug='provider-2'),
)
Provider.objects.bulk_create(providers)
ProviderNetwork.objects.bulk_create([
ProviderNetwork(name='Provider Network 1', provider=providers[0]),
ProviderNetwork(name='Provider Network 2', provider=providers[0]),
ProviderNetwork(name='Provider Network 3', provider=providers[0]),
])
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'name': 'Provider Network X',
'provider': providers[1].pk,
'description': 'A new provider network',
'comments': 'Longer description goes here',
'tags': [t.pk for t in tags],
}
cls.csv_data = (
"name,provider,description",
"Provider Network 4,Provider 1,Foo",
"Provider Network 5,Provider 1,Bar",
"Provider Network 6,Provider 1,Baz",
)
cls.bulk_edit_data = {
'provider': providers[1].pk,
'description': 'New description',
'comments': 'New comments',
}
class CircuitTerminationTestCase(
ViewTestCases.EditObjectViewTestCase,
ViewTestCases.DeleteObjectViewTestCase,
):
model = CircuitTermination
@classmethod
def setUpTestData(cls):
sites = (
Site(name='Site 1', slug='site-1'),
Site(name='Site 2', slug='site-2'),
Site(name='Site 3', slug='site-3'),
)
Site.objects.bulk_create(sites)
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
circuits = (
Circuit(cid='Circuit 1', provider=provider, type=circuittype),
Circuit(cid='Circuit 2', provider=provider, type=circuittype),
Circuit(cid='Circuit 3', provider=provider, type=circuittype),
)
Circuit.objects.bulk_create(circuits)
circuit_terminations = (
CircuitTermination(circuit=circuits[0], term_side='A', site=sites[0]),
CircuitTermination(circuit=circuits[0], term_side='Z', site=sites[1]),
CircuitTermination(circuit=circuits[1], term_side='A', site=sites[0]),
CircuitTermination(circuit=circuits[1], term_side='Z', site=sites[1]),
)
CircuitTermination.objects.bulk_create(circuit_terminations)
cls.form_data = {
'term_side': 'A',
'site': sites[2].pk,
'description': 'New description',
}
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_trace(self):
device = create_test_device('Device 1')
circuittermination = CircuitTermination.objects.first()
interface = Interface.objects.create(
device=device,
name='Interface 1'
)
Cable(termination_a=circuittermination, termination_b=interface).save()
response = self.client.get(reverse('circuits:circuittermination_trace', kwargs={'pk': circuittermination.pk}))
self.assertHttpStatus(response, 200)

View File

@@ -1,9 +1,10 @@
from django.urls import path from django.urls import path
from dcim.views import CableCreateView, PathTraceView from dcim.views import CableCreateView, PathTraceView
from extras.views import ObjectChangeLogView from extras.views import ObjectChangeLogView, ObjectJournalView
from utilities.views import SlugRedirectView
from . import views from . import views
from .models import Circuit, CircuitTermination, CircuitType, Provider from .models import *
app_name = 'circuits' app_name = 'circuits'
urlpatterns = [ urlpatterns = [
@@ -14,19 +15,35 @@ urlpatterns = [
path('providers/import/', views.ProviderBulkImportView.as_view(), name='provider_import'), path('providers/import/', views.ProviderBulkImportView.as_view(), name='provider_import'),
path('providers/edit/', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'), path('providers/edit/', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'),
path('providers/delete/', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'), path('providers/delete/', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'),
path('providers/<slug:slug>/', views.ProviderView.as_view(), name='provider'), path('providers/<int:pk>/', views.ProviderView.as_view(), name='provider'),
path('providers/<slug:slug>/edit/', views.ProviderEditView.as_view(), name='provider_edit'), path('providers/<slug:slug>/', SlugRedirectView.as_view(), kwargs={'model': Provider}),
path('providers/<slug:slug>/delete/', views.ProviderDeleteView.as_view(), name='provider_delete'), path('providers/<int:pk>/edit/', views.ProviderEditView.as_view(), name='provider_edit'),
path('providers/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}), path('providers/<int:pk>/delete/', views.ProviderDeleteView.as_view(), name='provider_delete'),
path('providers/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}),
path('providers/<int:pk>/journal/', ObjectJournalView.as_view(), name='provider_journal', kwargs={'model': Provider}),
# Provider networks
path('provider-networks/', views.ProviderNetworkListView.as_view(), name='providernetwork_list'),
path('provider-networks/add/', views.ProviderNetworkEditView.as_view(), name='providernetwork_add'),
path('provider-networks/import/', views.ProviderNetworkBulkImportView.as_view(), name='providernetwork_import'),
path('provider-networks/edit/', views.ProviderNetworkBulkEditView.as_view(), name='providernetwork_bulk_edit'),
path('provider-networks/delete/', views.ProviderNetworkBulkDeleteView.as_view(), name='providernetwork_bulk_delete'),
path('provider-networks/<int:pk>/', views.ProviderNetworkView.as_view(), name='providernetwork'),
path('provider-networks/<int:pk>/edit/', views.ProviderNetworkEditView.as_view(), name='providernetwork_edit'),
path('provider-networks/<int:pk>/delete/', views.ProviderNetworkDeleteView.as_view(), name='providernetwork_delete'),
path('provider-networks/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='providernetwork_changelog', kwargs={'model': ProviderNetwork}),
path('provider-networks/<int:pk>/journal/', ObjectJournalView.as_view(), name='providernetwork_journal', kwargs={'model': ProviderNetwork}),
# Circuit types # Circuit types
path('circuit-types/', views.CircuitTypeListView.as_view(), name='circuittype_list'), path('circuit-types/', views.CircuitTypeListView.as_view(), name='circuittype_list'),
path('circuit-types/add/', views.CircuitTypeEditView.as_view(), name='circuittype_add'), path('circuit-types/add/', views.CircuitTypeEditView.as_view(), name='circuittype_add'),
path('circuit-types/import/', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'), path('circuit-types/import/', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'),
path('circuit-types/edit/', views.CircuitTypeBulkEditView.as_view(), name='circuittype_bulk_edit'),
path('circuit-types/delete/', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'), path('circuit-types/delete/', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'),
path('circuit-types/<slug:slug>/edit/', views.CircuitTypeEditView.as_view(), name='circuittype_edit'), path('circuit-types/<int:pk>/', views.CircuitTypeView.as_view(), name='circuittype'),
path('circuit-types/<slug:slug>/delete/', views.CircuitTypeDeleteView.as_view(), name='circuittype_delete'), path('circuit-types/<int:pk>/edit/', views.CircuitTypeEditView.as_view(), name='circuittype_edit'),
path('circuit-types/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}), path('circuit-types/<int:pk>/delete/', views.CircuitTypeDeleteView.as_view(), name='circuittype_delete'),
path('circuit-types/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}),
# Circuits # Circuits
path('circuits/', views.CircuitListView.as_view(), name='circuit_list'), path('circuits/', views.CircuitListView.as_view(), name='circuit_list'),
@@ -38,6 +55,7 @@ urlpatterns = [
path('circuits/<int:pk>/edit/', views.CircuitEditView.as_view(), name='circuit_edit'), path('circuits/<int:pk>/edit/', views.CircuitEditView.as_view(), name='circuit_edit'),
path('circuits/<int:pk>/delete/', views.CircuitDeleteView.as_view(), name='circuit_delete'), path('circuits/<int:pk>/delete/', views.CircuitDeleteView.as_view(), name='circuit_delete'),
path('circuits/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='circuit_changelog', kwargs={'model': Circuit}), path('circuits/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='circuit_changelog', kwargs={'model': Circuit}),
path('circuits/<int:pk>/journal/', ObjectJournalView.as_view(), name='circuit_journal', kwargs={'model': Circuit}),
path('circuits/<int:pk>/terminations/swap/', views.CircuitSwapTerminations.as_view(), name='circuit_terminations_swap'), path('circuits/<int:pk>/terminations/swap/', views.CircuitSwapTerminations.as_view(), name='circuit_terminations_swap'),
# Circuit terminations # Circuit terminations

View File

@@ -1,15 +1,15 @@
from django.contrib import messages from django.contrib import messages
from django.db import transaction from django.db import transaction
from django.db.models import Q
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django_tables2 import RequestConfig
from netbox.views import generic from netbox.views import generic
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from utilities.paginator import EnhancedPaginator, get_paginate_count from utilities.tables import paginate_table
from utilities.utils import count_related from utilities.utils import count_related
from . import filters, forms, tables from . import filtersets, forms, tables
from .choices import CircuitTerminationSideChoices from .choices import CircuitTerminationSideChoices
from .models import Circuit, CircuitTermination, CircuitType, Provider from .models import *
# #
@@ -20,7 +20,7 @@ class ProviderListView(generic.ObjectListView):
queryset = Provider.objects.annotate( queryset = Provider.objects.annotate(
count_circuits=count_related(Circuit, 'provider') count_circuits=count_related(Circuit, 'provider')
) )
filterset = filters.ProviderFilterSet filterset = filtersets.ProviderFilterSet
filterset_form = forms.ProviderFilterForm filterset_form = forms.ProviderFilterForm
table = tables.ProviderTable table = tables.ProviderTable
@@ -33,16 +33,11 @@ class ProviderView(generic.ObjectView):
provider=instance provider=instance
).prefetch_related( ).prefetch_related(
'type', 'tenant', 'terminations__site' 'type', 'tenant', 'terminations__site'
).annotate_sites() )
circuits_table = tables.CircuitTable(circuits) circuits_table = tables.CircuitTable(circuits)
circuits_table.columns.hide('provider') circuits_table.columns.hide('provider')
paginate_table(circuits_table, request)
paginate = {
'paginator_class': EnhancedPaginator,
'per_page': get_paginate_count(request)
}
RequestConfig(request, paginate).configure(circuits_table)
return { return {
'circuits_table': circuits_table, 'circuits_table': circuits_table,
@@ -52,7 +47,6 @@ class ProviderView(generic.ObjectView):
class ProviderEditView(generic.ObjectEditView): class ProviderEditView(generic.ObjectEditView):
queryset = Provider.objects.all() queryset = Provider.objects.all()
model_form = forms.ProviderForm model_form = forms.ProviderForm
template_name = 'circuits/provider_edit.html'
class ProviderDeleteView(generic.ObjectDeleteView): class ProviderDeleteView(generic.ObjectDeleteView):
@@ -69,7 +63,7 @@ class ProviderBulkEditView(generic.BulkEditView):
queryset = Provider.objects.annotate( queryset = Provider.objects.annotate(
count_circuits=count_related(Circuit, 'provider') count_circuits=count_related(Circuit, 'provider')
) )
filterset = filters.ProviderFilterSet filterset = filtersets.ProviderFilterSet
table = tables.ProviderTable table = tables.ProviderTable
form = forms.ProviderBulkEditForm form = forms.ProviderBulkEditForm
@@ -78,10 +72,70 @@ class ProviderBulkDeleteView(generic.BulkDeleteView):
queryset = Provider.objects.annotate( queryset = Provider.objects.annotate(
count_circuits=count_related(Circuit, 'provider') count_circuits=count_related(Circuit, 'provider')
) )
filterset = filters.ProviderFilterSet filterset = filtersets.ProviderFilterSet
table = tables.ProviderTable table = tables.ProviderTable
#
# Provider networks
#
class ProviderNetworkListView(generic.ObjectListView):
queryset = ProviderNetwork.objects.all()
filterset = filtersets.ProviderNetworkFilterSet
filterset_form = forms.ProviderNetworkFilterForm
table = tables.ProviderNetworkTable
class ProviderNetworkView(generic.ObjectView):
queryset = ProviderNetwork.objects.all()
def get_extra_context(self, request, instance):
circuits = Circuit.objects.restrict(request.user, 'view').filter(
Q(termination_a__provider_network=instance.pk) |
Q(termination_z__provider_network=instance.pk)
).prefetch_related(
'type', 'tenant', 'terminations__site'
)
circuits_table = tables.CircuitTable(circuits)
circuits_table.columns.hide('termination_a')
circuits_table.columns.hide('termination_z')
paginate_table(circuits_table, request)
return {
'circuits_table': circuits_table,
}
class ProviderNetworkEditView(generic.ObjectEditView):
queryset = ProviderNetwork.objects.all()
model_form = forms.ProviderNetworkForm
class ProviderNetworkDeleteView(generic.ObjectDeleteView):
queryset = ProviderNetwork.objects.all()
class ProviderNetworkBulkImportView(generic.BulkImportView):
queryset = ProviderNetwork.objects.all()
model_form = forms.ProviderNetworkCSVForm
table = tables.ProviderNetworkTable
class ProviderNetworkBulkEditView(generic.BulkEditView):
queryset = ProviderNetwork.objects.all()
filterset = filtersets.ProviderNetworkFilterSet
table = tables.ProviderNetworkTable
form = forms.ProviderNetworkBulkEditForm
class ProviderNetworkBulkDeleteView(generic.BulkDeleteView):
queryset = ProviderNetwork.objects.all()
filterset = filtersets.ProviderNetworkFilterSet
table = tables.ProviderNetworkTable
# #
# Circuit Types # Circuit Types
# #
@@ -93,6 +147,23 @@ class CircuitTypeListView(generic.ObjectListView):
table = tables.CircuitTypeTable table = tables.CircuitTypeTable
class CircuitTypeView(generic.ObjectView):
queryset = CircuitType.objects.all()
def get_extra_context(self, request, instance):
circuits = Circuit.objects.restrict(request.user, 'view').filter(
type=instance
)
circuits_table = tables.CircuitTable(circuits)
circuits_table.columns.hide('type')
paginate_table(circuits_table, request)
return {
'circuits_table': circuits_table,
}
class CircuitTypeEditView(generic.ObjectEditView): class CircuitTypeEditView(generic.ObjectEditView):
queryset = CircuitType.objects.all() queryset = CircuitType.objects.all()
model_form = forms.CircuitTypeForm model_form = forms.CircuitTypeForm
@@ -108,6 +179,15 @@ class CircuitTypeBulkImportView(generic.BulkImportView):
table = tables.CircuitTypeTable table = tables.CircuitTypeTable
class CircuitTypeBulkEditView(generic.BulkEditView):
queryset = CircuitType.objects.annotate(
circuit_count=count_related(Circuit, 'type')
)
filterset = filtersets.CircuitTypeFilterSet
table = tables.CircuitTypeTable
form = forms.CircuitTypeBulkEditForm
class CircuitTypeBulkDeleteView(generic.BulkDeleteView): class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
queryset = CircuitType.objects.annotate( queryset = CircuitType.objects.annotate(
circuit_count=count_related(Circuit, 'type') circuit_count=count_related(Circuit, 'type')
@@ -121,9 +201,9 @@ class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
class CircuitListView(generic.ObjectListView): class CircuitListView(generic.ObjectListView):
queryset = Circuit.objects.prefetch_related( queryset = Circuit.objects.prefetch_related(
'provider', 'type', 'tenant', 'terminations' 'provider', 'type', 'tenant', 'termination_a', 'termination_z'
).annotate_sites() )
filterset = filters.CircuitFilterSet filterset = filtersets.CircuitFilterSet
filterset_form = forms.CircuitFilterForm filterset_form = forms.CircuitFilterForm
table = tables.CircuitTable table = tables.CircuitTable
@@ -131,36 +211,10 @@ class CircuitListView(generic.ObjectListView):
class CircuitView(generic.ObjectView): class CircuitView(generic.ObjectView):
queryset = Circuit.objects.all() queryset = Circuit.objects.all()
def get_extra_context(self, request, instance):
# A-side termination
termination_a = CircuitTermination.objects.restrict(request.user, 'view').prefetch_related(
'site__region'
).filter(
circuit=instance, term_side=CircuitTerminationSideChoices.SIDE_A
).first()
if termination_a and termination_a.connected_endpoint and hasattr(termination_a.connected_endpoint, 'ip_addresses'):
termination_a.ip_addresses = termination_a.connected_endpoint.ip_addresses.restrict(request.user, 'view')
# Z-side termination
termination_z = CircuitTermination.objects.restrict(request.user, 'view').prefetch_related(
'site__region'
).filter(
circuit=instance, term_side=CircuitTerminationSideChoices.SIDE_Z
).first()
if termination_z and termination_z.connected_endpoint and hasattr(termination_z.connected_endpoint, 'ip_addresses'):
termination_z.ip_addresses = termination_z.connected_endpoint.ip_addresses.restrict(request.user, 'view')
return {
'termination_a': termination_a,
'termination_z': termination_z,
}
class CircuitEditView(generic.ObjectEditView): class CircuitEditView(generic.ObjectEditView):
queryset = Circuit.objects.all() queryset = Circuit.objects.all()
model_form = forms.CircuitForm model_form = forms.CircuitForm
template_name = 'circuits/circuit_edit.html'
class CircuitDeleteView(generic.ObjectDeleteView): class CircuitDeleteView(generic.ObjectDeleteView):
@@ -177,7 +231,7 @@ class CircuitBulkEditView(generic.BulkEditView):
queryset = Circuit.objects.prefetch_related( queryset = Circuit.objects.prefetch_related(
'provider', 'type', 'tenant', 'terminations' 'provider', 'type', 'tenant', 'terminations'
) )
filterset = filters.CircuitFilterSet filterset = filtersets.CircuitFilterSet
table = tables.CircuitTable table = tables.CircuitTable
form = forms.CircuitBulkEditForm form = forms.CircuitBulkEditForm
@@ -186,7 +240,7 @@ class CircuitBulkDeleteView(generic.BulkDeleteView):
queryset = Circuit.objects.prefetch_related( queryset = Circuit.objects.prefetch_related(
'provider', 'type', 'tenant', 'terminations' 'provider', 'type', 'tenant', 'terminations'
) )
filterset = filters.CircuitFilterSet filterset = filtersets.CircuitFilterSet
table = tables.CircuitTable table = tables.CircuitTable
@@ -221,16 +275,11 @@ class CircuitSwapTerminations(generic.ObjectEditView):
if form.is_valid(): if form.is_valid():
termination_a = CircuitTermination.objects.filter( termination_a = CircuitTermination.objects.filter(pk=circuit.termination_a_id).first()
circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_A termination_z = CircuitTermination.objects.filter(pk=circuit.termination_z_id).first()
).first()
termination_z = CircuitTermination.objects.filter(
circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_Z
).first()
if termination_a and termination_z: if termination_a and termination_z:
# Use a placeholder to avoid an IntegrityError on the (circuit, term_side) unique constraint # Use a placeholder to avoid an IntegrityError on the (circuit, term_side) unique constraint
print('swapping')
with transaction.atomic(): with transaction.atomic():
termination_a.term_side = '_' termination_a.term_side = '_'
termination_a.save() termination_a.save()
@@ -241,11 +290,20 @@ class CircuitSwapTerminations(generic.ObjectEditView):
elif termination_a: elif termination_a:
termination_a.term_side = 'Z' termination_a.term_side = 'Z'
termination_a.save() termination_a.save()
circuit.refresh_from_db()
circuit.termination_a = None
circuit.save()
else: else:
termination_z.term_side = 'A' termination_z.term_side = 'A'
termination_z.save() termination_z.save()
circuit.refresh_from_db()
circuit.termination_z = None
circuit.save()
messages.success(request, "Swapped terminations for circuit {}.".format(circuit)) print(f'term A: {circuit.termination_a}')
print(f'term Z: {circuit.termination_z}')
messages.success(request, f"Swapped terminations for circuit {circuit}.")
return redirect('circuits:circuit', pk=circuit.pk) return redirect('circuits:circuit', pk=circuit.pk)
return render(request, 'circuits/circuit_terminations_swap.html', { return render(request, 'circuits/circuit_terminations_swap.html', {

View File

@@ -1,7 +1,7 @@
from rest_framework import serializers from rest_framework import serializers
from dcim import models from dcim import models
from netbox.api import WritableNestedSerializer from netbox.api.serializers import BaseModelSerializer, WritableNestedSerializer
__all__ = [ __all__ = [
'NestedCableSerializer', 'NestedCableSerializer',
@@ -27,7 +27,7 @@ __all__ = [
'NestedPowerPanelSerializer', 'NestedPowerPanelSerializer',
'NestedPowerPortSerializer', 'NestedPowerPortSerializer',
'NestedPowerPortTemplateSerializer', 'NestedPowerPortTemplateSerializer',
'NestedRackGroupSerializer', 'NestedLocationSerializer',
'NestedRackReservationSerializer', 'NestedRackReservationSerializer',
'NestedRackRoleSerializer', 'NestedRackRoleSerializer',
'NestedRackSerializer', 'NestedRackSerializer',
@@ -35,6 +35,7 @@ __all__ = [
'NestedRearPortTemplateSerializer', 'NestedRearPortTemplateSerializer',
'NestedRegionSerializer', 'NestedRegionSerializer',
'NestedSiteSerializer', 'NestedSiteSerializer',
'NestedSiteGroupSerializer',
'NestedVirtualChassisSerializer', 'NestedVirtualChassisSerializer',
] ]
@@ -50,7 +51,17 @@ class NestedRegionSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.Region model = models.Region
fields = ['id', 'url', 'name', 'slug', 'site_count', '_depth'] fields = ['id', 'url', 'display', 'name', 'slug', 'site_count', '_depth']
class NestedSiteGroupSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail')
site_count = serializers.IntegerField(read_only=True)
_depth = serializers.IntegerField(source='level', read_only=True)
class Meta:
model = models.SiteGroup
fields = ['id', 'url', 'display', 'name', 'slug', 'site_count', '_depth']
class NestedSiteSerializer(WritableNestedSerializer): class NestedSiteSerializer(WritableNestedSerializer):
@@ -58,21 +69,21 @@ class NestedSiteSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.Site model = models.Site
fields = ['id', 'url', 'name', 'slug'] fields = ['id', 'url', 'display', 'name', 'slug']
# #
# Racks # Racks
# #
class NestedRackGroupSerializer(WritableNestedSerializer): class NestedLocationSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackgroup-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail')
rack_count = serializers.IntegerField(read_only=True) rack_count = serializers.IntegerField(read_only=True)
_depth = serializers.IntegerField(source='level', read_only=True) _depth = serializers.IntegerField(source='level', read_only=True)
class Meta: class Meta:
model = models.RackGroup model = models.Location
fields = ['id', 'url', 'name', 'slug', 'rack_count', '_depth'] fields = ['id', 'url', 'display', 'name', 'slug', 'rack_count', '_depth']
class NestedRackRoleSerializer(WritableNestedSerializer): class NestedRackRoleSerializer(WritableNestedSerializer):
@@ -81,7 +92,7 @@ class NestedRackRoleSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.RackRole model = models.RackRole
fields = ['id', 'url', 'name', 'slug', 'rack_count'] fields = ['id', 'url', 'display', 'name', 'slug', 'rack_count']
class NestedRackSerializer(WritableNestedSerializer): class NestedRackSerializer(WritableNestedSerializer):
@@ -90,7 +101,7 @@ class NestedRackSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.Rack model = models.Rack
fields = ['id', 'url', 'name', 'display_name', 'device_count'] fields = ['id', 'url', 'display', 'name', 'display_name', 'device_count']
class NestedRackReservationSerializer(WritableNestedSerializer): class NestedRackReservationSerializer(WritableNestedSerializer):
@@ -99,7 +110,7 @@ class NestedRackReservationSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.RackReservation model = models.RackReservation
fields = ['id', 'url', 'user', 'units'] fields = ['id', 'url', 'display', 'user', 'units']
def get_user(self, obj): def get_user(self, obj):
return obj.user.username return obj.user.username
@@ -115,7 +126,7 @@ class NestedManufacturerSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.Manufacturer model = models.Manufacturer
fields = ['id', 'url', 'name', 'slug', 'devicetype_count'] fields = ['id', 'url', 'display', 'name', 'slug', 'devicetype_count']
class NestedDeviceTypeSerializer(WritableNestedSerializer): class NestedDeviceTypeSerializer(WritableNestedSerializer):
@@ -125,7 +136,7 @@ class NestedDeviceTypeSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.DeviceType model = models.DeviceType
fields = ['id', 'url', 'manufacturer', 'model', 'slug', 'display_name', 'device_count'] fields = ['id', 'url', 'display', 'manufacturer', 'model', 'slug', 'display_name', 'device_count']
class NestedConsolePortTemplateSerializer(WritableNestedSerializer): class NestedConsolePortTemplateSerializer(WritableNestedSerializer):
@@ -133,7 +144,7 @@ class NestedConsolePortTemplateSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.ConsolePortTemplate model = models.ConsolePortTemplate
fields = ['id', 'url', 'name'] fields = ['id', 'url', 'display', 'name']
class NestedConsoleServerPortTemplateSerializer(WritableNestedSerializer): class NestedConsoleServerPortTemplateSerializer(WritableNestedSerializer):
@@ -141,7 +152,7 @@ class NestedConsoleServerPortTemplateSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.ConsoleServerPortTemplate model = models.ConsoleServerPortTemplate
fields = ['id', 'url', 'name'] fields = ['id', 'url', 'display', 'name']
class NestedPowerPortTemplateSerializer(WritableNestedSerializer): class NestedPowerPortTemplateSerializer(WritableNestedSerializer):
@@ -149,7 +160,7 @@ class NestedPowerPortTemplateSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.PowerPortTemplate model = models.PowerPortTemplate
fields = ['id', 'url', 'name'] fields = ['id', 'url', 'display', 'name']
class NestedPowerOutletTemplateSerializer(WritableNestedSerializer): class NestedPowerOutletTemplateSerializer(WritableNestedSerializer):
@@ -157,7 +168,7 @@ class NestedPowerOutletTemplateSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.PowerOutletTemplate model = models.PowerOutletTemplate
fields = ['id', 'url', 'name'] fields = ['id', 'url', 'display', 'name']
class NestedInterfaceTemplateSerializer(WritableNestedSerializer): class NestedInterfaceTemplateSerializer(WritableNestedSerializer):
@@ -165,7 +176,7 @@ class NestedInterfaceTemplateSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.InterfaceTemplate model = models.InterfaceTemplate
fields = ['id', 'url', 'name'] fields = ['id', 'url', 'display', 'name']
class NestedRearPortTemplateSerializer(WritableNestedSerializer): class NestedRearPortTemplateSerializer(WritableNestedSerializer):
@@ -173,7 +184,7 @@ class NestedRearPortTemplateSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.RearPortTemplate model = models.RearPortTemplate
fields = ['id', 'url', 'name'] fields = ['id', 'url', 'display', 'name']
class NestedFrontPortTemplateSerializer(WritableNestedSerializer): class NestedFrontPortTemplateSerializer(WritableNestedSerializer):
@@ -181,7 +192,7 @@ class NestedFrontPortTemplateSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.FrontPortTemplate model = models.FrontPortTemplate
fields = ['id', 'url', 'name'] fields = ['id', 'url', 'display', 'name']
class NestedDeviceBayTemplateSerializer(WritableNestedSerializer): class NestedDeviceBayTemplateSerializer(WritableNestedSerializer):
@@ -189,7 +200,7 @@ class NestedDeviceBayTemplateSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.DeviceBayTemplate model = models.DeviceBayTemplate
fields = ['id', 'url', 'name'] fields = ['id', 'url', 'display', 'name']
# #
@@ -203,7 +214,7 @@ class NestedDeviceRoleSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.DeviceRole model = models.DeviceRole
fields = ['id', 'url', 'name', 'slug', 'device_count', 'virtualmachine_count'] fields = ['id', 'url', 'display', 'name', 'slug', 'device_count', 'virtualmachine_count']
class NestedPlatformSerializer(WritableNestedSerializer): class NestedPlatformSerializer(WritableNestedSerializer):
@@ -213,7 +224,7 @@ class NestedPlatformSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.Platform model = models.Platform
fields = ['id', 'url', 'name', 'slug', 'device_count', 'virtualmachine_count'] fields = ['id', 'url', 'display', 'name', 'slug', 'device_count', 'virtualmachine_count']
class NestedDeviceSerializer(WritableNestedSerializer): class NestedDeviceSerializer(WritableNestedSerializer):
@@ -221,7 +232,7 @@ class NestedDeviceSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.Device model = models.Device
fields = ['id', 'url', 'name', 'display_name'] fields = ['id', 'url', 'display', 'name', 'display_name']
class NestedConsoleServerPortSerializer(WritableNestedSerializer): class NestedConsoleServerPortSerializer(WritableNestedSerializer):
@@ -230,7 +241,7 @@ class NestedConsoleServerPortSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.ConsoleServerPort model = models.ConsoleServerPort
fields = ['id', 'url', 'device', 'name', 'cable'] fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
class NestedConsolePortSerializer(WritableNestedSerializer): class NestedConsolePortSerializer(WritableNestedSerializer):
@@ -239,7 +250,7 @@ class NestedConsolePortSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.ConsolePort model = models.ConsolePort
fields = ['id', 'url', 'device', 'name', 'cable'] fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
class NestedPowerOutletSerializer(WritableNestedSerializer): class NestedPowerOutletSerializer(WritableNestedSerializer):
@@ -248,7 +259,7 @@ class NestedPowerOutletSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.PowerOutlet model = models.PowerOutlet
fields = ['id', 'url', 'device', 'name', 'cable'] fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
class NestedPowerPortSerializer(WritableNestedSerializer): class NestedPowerPortSerializer(WritableNestedSerializer):
@@ -257,7 +268,7 @@ class NestedPowerPortSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.PowerPort model = models.PowerPort
fields = ['id', 'url', 'device', 'name', 'cable'] fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
class NestedInterfaceSerializer(WritableNestedSerializer): class NestedInterfaceSerializer(WritableNestedSerializer):
@@ -266,7 +277,7 @@ class NestedInterfaceSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.Interface model = models.Interface
fields = ['id', 'url', 'device', 'name', 'cable'] fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
class NestedRearPortSerializer(WritableNestedSerializer): class NestedRearPortSerializer(WritableNestedSerializer):
@@ -275,7 +286,7 @@ class NestedRearPortSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.RearPort model = models.RearPort
fields = ['id', 'url', 'device', 'name', 'cable'] fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
class NestedFrontPortSerializer(WritableNestedSerializer): class NestedFrontPortSerializer(WritableNestedSerializer):
@@ -284,7 +295,7 @@ class NestedFrontPortSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.FrontPort model = models.FrontPort
fields = ['id', 'url', 'device', 'name', 'cable'] fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
class NestedDeviceBaySerializer(WritableNestedSerializer): class NestedDeviceBaySerializer(WritableNestedSerializer):
@@ -293,7 +304,7 @@ class NestedDeviceBaySerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.DeviceBay model = models.DeviceBay
fields = ['id', 'url', 'device', 'name'] fields = ['id', 'url', 'display', 'device', 'name']
class NestedInventoryItemSerializer(WritableNestedSerializer): class NestedInventoryItemSerializer(WritableNestedSerializer):
@@ -303,19 +314,19 @@ class NestedInventoryItemSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.InventoryItem model = models.InventoryItem
fields = ['id', 'url', 'device', 'name', '_depth'] fields = ['id', 'url', 'display', 'device', 'name', '_depth']
# #
# Cables # Cables
# #
class NestedCableSerializer(serializers.ModelSerializer): class NestedCableSerializer(BaseModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
class Meta: class Meta:
model = models.Cable model = models.Cable
fields = ['id', 'url', 'label'] fields = ['id', 'url', 'display', 'label']
# #
@@ -342,7 +353,7 @@ class NestedPowerPanelSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.PowerPanel model = models.PowerPanel
fields = ['id', 'url', 'name', 'powerfeed_count'] fields = ['id', 'url', 'display', 'name', 'powerfeed_count']
class NestedPowerFeedSerializer(WritableNestedSerializer): class NestedPowerFeedSerializer(WritableNestedSerializer):
@@ -350,4 +361,4 @@ class NestedPowerFeedSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.PowerFeed model = models.PowerFeed
fields = ['id', 'url', 'name', 'cable'] fields = ['id', 'url', 'display', 'name', 'cable', '_occupied']

View File

@@ -7,19 +7,12 @@ from timezone_field.rest_framework import TimeZoneSerializerField
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.models import ( from dcim.models import *
Cable, CablePath, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceType, DeviceRole, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
Manufacturer, InventoryItem, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
VirtualChassis,
)
from extras.api.customfields import CustomFieldModelSerializer
from extras.api.serializers import TaggedObjectSerializer
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
from ipam.models import VLAN from ipam.models import VLAN
from netbox.api import ( from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
ChoiceField, ContentTypeField, SerializedPKRelatedField, ValidatedModelSerializer, from netbox.api.serializers import (
NestedGroupModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer,
WritableNestedSerializer, WritableNestedSerializer,
) )
from tenancy.api.nested_serializers import NestedTenantSerializer from tenancy.api.nested_serializers import NestedTenantSerializer
@@ -50,7 +43,7 @@ class CableTerminationSerializer(serializers.ModelSerializer):
return None return None
class ConnectedEndpointSerializer(ValidatedModelSerializer): class ConnectedEndpointSerializer(serializers.ModelSerializer):
connected_endpoint_type = serializers.SerializerMethodField(read_only=True) connected_endpoint_type = serializers.SerializerMethodField(read_only=True)
connected_endpoint = serializers.SerializerMethodField(read_only=True) connected_endpoint = serializers.SerializerMethodField(read_only=True)
connected_endpoint_reachable = serializers.SerializerMethodField(read_only=True) connected_endpoint_reachable = serializers.SerializerMethodField(read_only=True)
@@ -82,21 +75,37 @@ class ConnectedEndpointSerializer(ValidatedModelSerializer):
# Regions/sites # Regions/sites
# #
class RegionSerializer(serializers.ModelSerializer): class RegionSerializer(NestedGroupModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
parent = NestedRegionSerializer(required=False, allow_null=True) parent = NestedRegionSerializer(required=False, allow_null=True)
site_count = serializers.IntegerField(read_only=True) site_count = serializers.IntegerField(read_only=True)
_depth = serializers.IntegerField(source='level', read_only=True)
class Meta: class Meta:
model = Region model = Region
fields = ['id', 'url', 'name', 'slug', 'parent', 'description', 'site_count', '_depth'] fields = [
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'custom_fields', 'created', 'last_updated',
'site_count', '_depth',
]
class SiteSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): class SiteGroupSerializer(NestedGroupModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail')
parent = NestedSiteGroupSerializer(required=False, allow_null=True)
site_count = serializers.IntegerField(read_only=True)
class Meta:
model = SiteGroup
fields = [
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'custom_fields', 'created', 'last_updated',
'site_count', '_depth',
]
class SiteSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
status = ChoiceField(choices=SiteStatusChoices, required=False) status = ChoiceField(choices=SiteStatusChoices, required=False)
region = NestedRegionSerializer(required=False, allow_null=True) region = NestedRegionSerializer(required=False, allow_null=True)
group = NestedSiteGroupSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True)
time_zone = TimeZoneSerializerField(required=False) time_zone = TimeZoneSerializerField(required=False)
circuit_count = serializers.IntegerField(read_only=True) circuit_count = serializers.IntegerField(read_only=True)
@@ -109,10 +118,10 @@ class SiteSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
class Meta: class Meta:
model = Site model = Site
fields = [ fields = [
'id', 'url', 'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'id', 'url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'asn',
'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', 'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name',
'contact_email', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count', 'contact_phone', 'contact_email', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
'device_count', 'prefix_count', 'rack_count', 'virtualmachine_count', 'vlan_count', 'circuit_count', 'device_count', 'prefix_count', 'rack_count', 'virtualmachine_count', 'vlan_count',
] ]
@@ -120,31 +129,37 @@ class SiteSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
# Racks # Racks
# #
class RackGroupSerializer(ValidatedModelSerializer): class LocationSerializer(NestedGroupModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackgroup-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail')
site = NestedSiteSerializer() site = NestedSiteSerializer()
parent = NestedRackGroupSerializer(required=False, allow_null=True) parent = NestedLocationSerializer(required=False, allow_null=True)
rack_count = serializers.IntegerField(read_only=True) rack_count = serializers.IntegerField(read_only=True)
_depth = serializers.IntegerField(source='level', read_only=True) device_count = serializers.IntegerField(read_only=True)
class Meta: class Meta:
model = RackGroup model = Location
fields = ['id', 'url', 'name', 'slug', 'site', 'parent', 'description', 'rack_count', '_depth'] fields = [
'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'description', 'custom_fields', 'created',
'last_updated', 'rack_count', 'device_count', '_depth',
]
class RackRoleSerializer(ValidatedModelSerializer): class RackRoleSerializer(OrganizationalModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
rack_count = serializers.IntegerField(read_only=True) rack_count = serializers.IntegerField(read_only=True)
class Meta: class Meta:
model = RackRole model = RackRole
fields = ['id', 'url', 'name', 'slug', 'color', 'description', 'rack_count'] fields = [
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'custom_fields', 'created', 'last_updated',
'rack_count',
]
class RackSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): class RackSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
site = NestedSiteSerializer() site = NestedSiteSerializer()
group = NestedRackGroupSerializer(required=False, allow_null=True, default=None) location = NestedLocationSerializer(required=False, allow_null=True, default=None)
tenant = NestedTenantSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True)
status = ChoiceField(choices=RackStatusChoices, required=False) status = ChoiceField(choices=RackStatusChoices, required=False)
role = NestedRackRoleSerializer(required=False, allow_null=True) role = NestedRackRoleSerializer(required=False, allow_null=True)
@@ -157,21 +172,22 @@ class RackSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
class Meta: class Meta:
model = Rack model = Rack
fields = [ fields = [
'id', 'url', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'status', 'role', 'serial', 'id', 'url', 'display', 'name', 'facility_id', 'display_name', 'site', 'location', 'tenant', 'status',
'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'role', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth',
'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count', 'outer_unit', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count',
'powerfeed_count',
] ]
# Omit the UniqueTogetherValidator that would be automatically added to validate (group, facility_id). This # Omit the UniqueTogetherValidator that would be automatically added to validate (location, facility_id). This
# prevents facility_id from being interpreted as a required field. # prevents facility_id from being interpreted as a required field.
validators = [ validators = [
UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('group', 'name')) UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('location', 'name'))
] ]
def validate(self, data): def validate(self, data):
# Validate uniqueness of (group, facility_id) since we omitted the automatically-created validator from Meta. # Validate uniqueness of (location, facility_id) since we omitted the automatically-created validator from Meta.
if data.get('facility_id', None): if data.get('facility_id', None):
validator = UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('group', 'facility_id')) validator = UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('location', 'facility_id'))
validator(data, self) validator(data, self)
# Enforce model validation # Enforce model validation
@@ -191,7 +207,7 @@ class RackUnitSerializer(serializers.Serializer):
occupied = serializers.BooleanField(read_only=True) occupied = serializers.BooleanField(read_only=True)
class RackReservationSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): class RackReservationSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail')
rack = NestedRackSerializer() rack = NestedRackSerializer()
user = NestedUserSerializer() user = NestedUserSerializer()
@@ -199,7 +215,10 @@ class RackReservationSerializer(TaggedObjectSerializer, CustomFieldModelSerializ
class Meta: class Meta:
model = RackReservation model = RackReservation
fields = ['id', 'url', 'rack', 'units', 'created', 'user', 'tenant', 'description', 'tags', 'custom_fields'] fields = [
'id', 'url', 'display', 'rack', 'units', 'created', 'user', 'tenant', 'description', 'tags',
'custom_fields',
]
class RackElevationDetailFilterSerializer(serializers.Serializer): class RackElevationDetailFilterSerializer(serializers.Serializer):
@@ -242,7 +261,7 @@ class RackElevationDetailFilterSerializer(serializers.Serializer):
# Device types # Device types
# #
class ManufacturerSerializer(ValidatedModelSerializer): class ManufacturerSerializer(OrganizationalModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
devicetype_count = serializers.IntegerField(read_only=True) devicetype_count = serializers.IntegerField(read_only=True)
inventoryitem_count = serializers.IntegerField(read_only=True) inventoryitem_count = serializers.IntegerField(read_only=True)
@@ -251,11 +270,12 @@ class ManufacturerSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = Manufacturer model = Manufacturer
fields = [ fields = [
'id', 'url', 'name', 'slug', 'description', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'id', 'url', 'display', 'name', 'slug', 'description', 'custom_fields', 'created', 'last_updated',
'devicetype_count', 'inventoryitem_count', 'platform_count',
] ]
class DeviceTypeSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): class DeviceTypeSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
manufacturer = NestedManufacturerSerializer() manufacturer = NestedManufacturerSerializer()
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False) subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False)
@@ -264,9 +284,9 @@ class DeviceTypeSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
class Meta: class Meta:
model = DeviceType model = DeviceType
fields = [ fields = [
'id', 'url', 'manufacturer', 'model', 'slug', 'display_name', 'part_number', 'u_height', 'is_full_depth', 'id', 'url', 'display', 'manufacturer', 'model', 'slug', 'display_name', 'part_number', 'u_height',
'subdevice_role', 'front_image', 'rear_image', 'comments', 'tags', 'custom_fields', 'created', 'is_full_depth', 'subdevice_role', 'front_image', 'rear_image', 'comments', 'tags', 'custom_fields',
'last_updated', 'device_count', 'created', 'last_updated', 'device_count',
] ]
@@ -281,7 +301,9 @@ class ConsolePortTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = ConsolePortTemplate model = ConsolePortTemplate
fields = ['id', 'url', 'device_type', 'name', 'label', 'type', 'description'] fields = [
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'description', 'created', 'last_updated',
]
class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer): class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
@@ -295,7 +317,9 @@ class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = ConsoleServerPortTemplate model = ConsoleServerPortTemplate
fields = ['id', 'url', 'device_type', 'name', 'label', 'type', 'description'] fields = [
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'description', 'created', 'last_updated',
]
class PowerPortTemplateSerializer(ValidatedModelSerializer): class PowerPortTemplateSerializer(ValidatedModelSerializer):
@@ -309,7 +333,10 @@ class PowerPortTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = PowerPortTemplate model = PowerPortTemplate
fields = ['id', 'url', 'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description'] fields = [
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw',
'description', 'created', 'last_updated',
]
class PowerOutletTemplateSerializer(ValidatedModelSerializer): class PowerOutletTemplateSerializer(ValidatedModelSerializer):
@@ -331,7 +358,10 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = PowerOutletTemplate model = PowerOutletTemplate
fields = ['id', 'url', 'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description'] fields = [
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
'created', 'last_updated',
]
class InterfaceTemplateSerializer(ValidatedModelSerializer): class InterfaceTemplateSerializer(ValidatedModelSerializer):
@@ -341,7 +371,10 @@ class InterfaceTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = InterfaceTemplate model = InterfaceTemplate
fields = ['id', 'url', 'device_type', 'name', 'label', 'type', 'mgmt_only', 'description'] fields = [
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'mgmt_only', 'description', 'created',
'last_updated',
]
class RearPortTemplateSerializer(ValidatedModelSerializer): class RearPortTemplateSerializer(ValidatedModelSerializer):
@@ -351,7 +384,10 @@ class RearPortTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = RearPortTemplate model = RearPortTemplate
fields = ['id', 'url', 'device_type', 'name', 'label', 'type', 'positions', 'description'] fields = [
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'positions', 'description', 'created',
'last_updated',
]
class FrontPortTemplateSerializer(ValidatedModelSerializer): class FrontPortTemplateSerializer(ValidatedModelSerializer):
@@ -362,7 +398,10 @@ class FrontPortTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = FrontPortTemplate model = FrontPortTemplate
fields = ['id', 'url', 'device_type', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description'] fields = [
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'rear_port', 'rear_port_position',
'description', 'created', 'last_updated',
]
class DeviceBayTemplateSerializer(ValidatedModelSerializer): class DeviceBayTemplateSerializer(ValidatedModelSerializer):
@@ -371,14 +410,14 @@ class DeviceBayTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = DeviceBayTemplate model = DeviceBayTemplate
fields = ['id', 'url', 'device_type', 'name', 'label', 'description'] fields = ['id', 'url', 'display', 'device_type', 'name', 'label', 'description', 'created', 'last_updated']
# #
# Devices # Devices
# #
class DeviceRoleSerializer(ValidatedModelSerializer): class DeviceRoleSerializer(OrganizationalModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
device_count = serializers.IntegerField(read_only=True) device_count = serializers.IntegerField(read_only=True)
virtualmachine_count = serializers.IntegerField(read_only=True) virtualmachine_count = serializers.IntegerField(read_only=True)
@@ -386,11 +425,12 @@ class DeviceRoleSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = DeviceRole model = DeviceRole
fields = [ fields = [
'id', 'url', 'name', 'slug', 'color', 'vm_role', 'description', 'device_count', 'virtualmachine_count', 'id', 'url', 'display', 'name', 'slug', 'color', 'vm_role', 'description', 'custom_fields', 'created',
'last_updated', 'device_count', 'virtualmachine_count',
] ]
class PlatformSerializer(ValidatedModelSerializer): class PlatformSerializer(OrganizationalModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True) manufacturer = NestedManufacturerSerializer(required=False, allow_null=True)
device_count = serializers.IntegerField(read_only=True) device_count = serializers.IntegerField(read_only=True)
@@ -399,18 +439,19 @@ class PlatformSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = Platform model = Platform
fields = [ fields = [
'id', 'url', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'device_count', 'id', 'url', 'display', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description',
'virtualmachine_count', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
] ]
class DeviceSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): class DeviceSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
device_type = NestedDeviceTypeSerializer() device_type = NestedDeviceTypeSerializer()
device_role = NestedDeviceRoleSerializer() device_role = NestedDeviceRoleSerializer()
tenant = NestedTenantSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True)
platform = NestedPlatformSerializer(required=False, allow_null=True) platform = NestedPlatformSerializer(required=False, allow_null=True)
site = NestedSiteSerializer() site = NestedSiteSerializer()
location = NestedLocationSerializer(required=False, allow_null=True, default=None)
rack = NestedRackSerializer(required=False, allow_null=True) rack = NestedRackSerializer(required=False, allow_null=True)
face = ChoiceField(choices=DeviceFaceChoices, allow_blank=True, required=False) face = ChoiceField(choices=DeviceFaceChoices, allow_blank=True, required=False)
status = ChoiceField(choices=DeviceStatusChoices, required=False) status = ChoiceField(choices=DeviceStatusChoices, required=False)
@@ -424,10 +465,10 @@ class DeviceSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
class Meta: class Meta:
model = Device model = Device
fields = [ fields = [
'id', 'url', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'id', 'url', 'display', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform',
'asset_tag', 'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'serial', 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status',
'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'local_context_data', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority',
'tags', 'custom_fields', 'created', 'last_updated', 'comments', 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated',
] ]
validators = [] validators = []
@@ -460,10 +501,10 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
class Meta(DeviceSerializer.Meta): class Meta(DeviceSerializer.Meta):
fields = [ fields = [
'id', 'url', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'id', 'url', 'display', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform',
'asset_tag', 'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'serial', 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status',
'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'local_context_data', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority',
'tags', 'custom_fields', 'config_context', 'created', 'last_updated', 'comments', 'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
] ]
@swagger_serializer_method(serializer_or_field=serializers.DictField) @swagger_serializer_method(serializer_or_field=serializers.DictField)
@@ -475,7 +516,11 @@ class DeviceNAPALMSerializer(serializers.Serializer):
method = serializers.DictField() method = serializers.DictField()
class ConsoleServerPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer): #
# Device components
#
class ConsoleServerPortSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField( type = ChoiceField(
@@ -483,17 +528,23 @@ class ConsoleServerPortSerializer(TaggedObjectSerializer, CableTerminationSerial
allow_blank=True, allow_blank=True,
required=False required=False
) )
speed = ChoiceField(
choices=ConsolePortSpeedChoices,
allow_blank=True,
required=False
)
cable = NestedCableSerializer(read_only=True) cable = NestedCableSerializer(read_only=True)
class Meta: class Meta:
model = ConsoleServerPort model = ConsoleServerPort
fields = [ fields = [
'id', 'url', 'device', 'name', 'label', 'type', 'description', 'cable', 'cable_peer', 'cable_peer_type', 'id', 'url', 'display', 'device', 'name', 'label', 'type', 'speed', 'description', 'mark_connected',
'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
] ]
class ConsolePortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer): class ConsolePortSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField( type = ChoiceField(
@@ -501,17 +552,23 @@ class ConsolePortSerializer(TaggedObjectSerializer, CableTerminationSerializer,
allow_blank=True, allow_blank=True,
required=False required=False
) )
speed = ChoiceField(
choices=ConsolePortSpeedChoices,
allow_blank=True,
required=False
)
cable = NestedCableSerializer(read_only=True) cable = NestedCableSerializer(read_only=True)
class Meta: class Meta:
model = ConsolePort model = ConsolePort
fields = [ fields = [
'id', 'url', 'device', 'name', 'label', 'type', 'description', 'cable', 'cable_peer', 'cable_peer_type', 'id', 'url', 'display', 'device', 'name', 'label', 'type', 'speed', 'description', 'mark_connected',
'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
] ]
class PowerOutletSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer): class PowerOutletSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField( type = ChoiceField(
@@ -534,13 +591,13 @@ class PowerOutletSerializer(TaggedObjectSerializer, CableTerminationSerializer,
class Meta: class Meta:
model = PowerOutlet model = PowerOutlet
fields = [ fields = [
'id', 'url', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'cable', 'id', 'url', 'display', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
'connected_endpoint_reachable', 'tags', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
] ]
class PowerPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer): class PowerPortSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField( type = ChoiceField(
@@ -553,16 +610,17 @@ class PowerPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, Co
class Meta: class Meta:
model = PowerPort model = PowerPort
fields = [ fields = [
'id', 'url', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'cable', 'id', 'url', 'display', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
'connected_endpoint_reachable', 'tags', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
] ]
class InterfaceSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer): class InterfaceSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField(choices=InterfaceTypeChoices) type = ChoiceField(choices=InterfaceTypeChoices)
parent = NestedInterfaceSerializer(required=False, allow_null=True)
lag = NestedInterfaceSerializer(required=False, allow_null=True) lag = NestedInterfaceSerializer(required=False, allow_null=True)
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False) mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True) untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
@@ -578,10 +636,11 @@ class InterfaceSerializer(TaggedObjectSerializer, CableTerminationSerializer, Co
class Meta: class Meta:
model = Interface model = Interface
fields = [ fields = [
'id', 'url', 'device', 'name', 'label', 'type', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'id', 'url', 'display', 'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address',
'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'cable', 'cable_peer', 'cable_peer_type', 'mgmt_only', 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable',
'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
'count_ipaddresses', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses',
'_occupied',
] ]
def validate(self, data): def validate(self, data):
@@ -598,7 +657,7 @@ class InterfaceSerializer(TaggedObjectSerializer, CableTerminationSerializer, Co
return super().validate(data) return super().validate(data)
class RearPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ValidatedModelSerializer): class RearPortSerializer(PrimaryModelSerializer, CableTerminationSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField(choices=PortTypeChoices) type = ChoiceField(choices=PortTypeChoices)
@@ -607,8 +666,8 @@ class RearPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, Val
class Meta: class Meta:
model = RearPort model = RearPort
fields = [ fields = [
'id', 'url', 'device', 'name', 'label', 'type', 'positions', 'description', 'cable', 'cable_peer', 'id', 'url', 'display', 'device', 'name', 'label', 'type', 'positions', 'description', 'mark_connected',
'cable_peer_type', 'tags', 'cable', 'cable_peer', 'cable_peer_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
] ]
@@ -620,10 +679,10 @@ class FrontPortRearPortSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = RearPort model = RearPort
fields = ['id', 'url', 'name', 'label'] fields = ['id', 'url', 'display', 'name', 'label']
class FrontPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ValidatedModelSerializer): class FrontPortSerializer(PrimaryModelSerializer, CableTerminationSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField(choices=PortTypeChoices) type = ChoiceField(choices=PortTypeChoices)
@@ -633,26 +692,30 @@ class FrontPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, Va
class Meta: class Meta:
model = FrontPort model = FrontPort
fields = [ fields = [
'id', 'url', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'id', 'url', 'display', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description',
'cable_peer', 'cable_peer_type', 'tags', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'tags', 'custom_fields', 'created',
'last_updated', '_occupied',
] ]
class DeviceBaySerializer(TaggedObjectSerializer, ValidatedModelSerializer): class DeviceBaySerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
installed_device = NestedDeviceSerializer(required=False, allow_null=True) installed_device = NestedDeviceSerializer(required=False, allow_null=True)
class Meta: class Meta:
model = DeviceBay model = DeviceBay
fields = ['id', 'url', 'device', 'name', 'label', 'description', 'installed_device', 'tags'] fields = [
'id', 'url', 'display', 'device', 'name', 'label', 'description', 'installed_device', 'tags',
'custom_fields', 'created', 'last_updated',
]
# #
# Inventory items # Inventory items
# #
class InventoryItemSerializer(TaggedObjectSerializer, ValidatedModelSerializer): class InventoryItemSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
# Provide a default value to satisfy UniqueTogetherValidator # Provide a default value to satisfy UniqueTogetherValidator
@@ -663,8 +726,8 @@ class InventoryItemSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
class Meta: class Meta:
model = InventoryItem model = InventoryItem
fields = [ fields = [
'id', 'url', 'device', 'parent', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'id', 'url', 'display', 'device', 'parent', 'name', 'label', 'manufacturer', 'part_id', 'serial',
'discovered', 'description', 'tags', '_depth', 'asset_tag', 'discovered', 'description', 'tags', 'custom_fields', 'created', 'last_updated', '_depth',
] ]
@@ -672,7 +735,7 @@ class InventoryItemSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
# Cables # Cables
# #
class CableSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): class CableSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
termination_a_type = ContentTypeField( termination_a_type = ContentTypeField(
queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS) queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS)
@@ -688,7 +751,7 @@ class CableSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
class Meta: class Meta:
model = Cable model = Cable
fields = [ fields = [
'id', 'url', 'termination_a_type', 'termination_a_id', 'termination_a', 'termination_b_type', 'id', 'url', 'display', 'termination_a_type', 'termination_a_id', 'termination_a', 'termination_b_type',
'termination_b_id', 'termination_b', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags', 'termination_b_id', 'termination_b', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags',
'custom_fields', 'custom_fields',
] ]
@@ -779,7 +842,7 @@ class CablePathSerializer(serializers.ModelSerializer):
class InterfaceConnectionSerializer(ValidatedModelSerializer): class InterfaceConnectionSerializer(ValidatedModelSerializer):
interface_a = serializers.SerializerMethodField() interface_a = serializers.SerializerMethodField()
interface_b = NestedInterfaceSerializer(source='connected_endpoint') interface_b = NestedInterfaceSerializer(source='_path.destination')
connected_endpoint_reachable = serializers.SerializerMethodField(read_only=True) connected_endpoint_reachable = serializers.SerializerMethodField(read_only=True)
class Meta: class Meta:
@@ -802,24 +865,24 @@ class InterfaceConnectionSerializer(ValidatedModelSerializer):
# Virtual chassis # Virtual chassis
# #
class VirtualChassisSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): class VirtualChassisSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
master = NestedDeviceSerializer(required=False) master = NestedDeviceSerializer(required=False)
member_count = serializers.IntegerField(read_only=True) member_count = serializers.IntegerField(read_only=True)
class Meta: class Meta:
model = VirtualChassis model = VirtualChassis
fields = ['id', 'url', 'name', 'domain', 'master', 'tags', 'custom_fields', 'member_count'] fields = ['id', 'url', 'display', 'name', 'domain', 'master', 'tags', 'custom_fields', 'member_count']
# #
# Power panels # Power panels
# #
class PowerPanelSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): class PowerPanelSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
site = NestedSiteSerializer() site = NestedSiteSerializer()
rack_group = NestedRackGroupSerializer( location = NestedLocationSerializer(
required=False, required=False,
allow_null=True, allow_null=True,
default=None default=None
@@ -828,15 +891,10 @@ class PowerPanelSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
class Meta: class Meta:
model = PowerPanel model = PowerPanel
fields = ['id', 'url', 'site', 'rack_group', 'name', 'tags', 'custom_fields', 'powerfeed_count'] fields = ['id', 'url', 'display', 'site', 'location', 'name', 'tags', 'custom_fields', 'powerfeed_count']
class PowerFeedSerializer( class PowerFeedSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
TaggedObjectSerializer,
CableTerminationSerializer,
ConnectedEndpointSerializer,
CustomFieldModelSerializer
):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
power_panel = NestedPowerPanelSerializer() power_panel = NestedPowerPanelSerializer()
rack = NestedRackSerializer( rack = NestedRackSerializer(
@@ -865,8 +923,8 @@ class PowerFeedSerializer(
class Meta: class Meta:
model = PowerFeed model = PowerFeed
fields = [ fields = [
'id', 'url', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'id', 'url', 'display', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage',
'max_utilization', 'comments', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'amperage', 'max_utilization', 'comments', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type',
'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields',
'last_updated', 'created', 'last_updated', '_occupied',
] ]

View File

@@ -7,10 +7,11 @@ router.APIRootView = views.DCIMRootView
# Sites # Sites
router.register('regions', views.RegionViewSet) router.register('regions', views.RegionViewSet)
router.register('site-groups', views.SiteGroupViewSet)
router.register('sites', views.SiteViewSet) router.register('sites', views.SiteViewSet)
# Racks # Racks
router.register('rack-groups', views.RackGroupViewSet) router.register('locations', views.LocationViewSet)
router.register('rack-roles', views.RackRoleViewSet) router.register('rack-roles', views.RackRoleViewSet)
router.register('racks', views.RackViewSet) router.register('racks', views.RackViewSet)
router.register('rack-reservations', views.RackReservationViewSet) router.register('rack-reservations', views.RackReservationViewSet)

View File

@@ -2,6 +2,7 @@ import socket
from collections import OrderedDict from collections import OrderedDict
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.db.models import F from django.db.models import F
from django.http import HttpResponseForbidden, HttpResponse from django.http import HttpResponseForbidden, HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
@@ -15,14 +16,8 @@ from rest_framework.routers import APIRootView
from rest_framework.viewsets import GenericViewSet, ViewSet from rest_framework.viewsets import GenericViewSet, ViewSet
from circuits.models import Circuit from circuits.models import Circuit
from dcim import filters from dcim import filtersets
from dcim.models import ( from dcim.models import *
Cable, CablePath, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
Manufacturer, InventoryItem, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
VirtualChassis,
)
from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet
from ipam.models import Prefix, VLAN from ipam.models import Prefix, VLAN
from netbox.api.views import ModelViewSet from netbox.api.views import ModelViewSet
@@ -99,7 +94,7 @@ class PassThroughPortMixin(object):
# Regions # Regions
# #
class RegionViewSet(ModelViewSet): class RegionViewSet(CustomFieldModelViewSet):
queryset = Region.objects.add_related_count( queryset = Region.objects.add_related_count(
Region.objects.all(), Region.objects.all(),
Site, Site,
@@ -108,7 +103,23 @@ class RegionViewSet(ModelViewSet):
cumulative=True cumulative=True
) )
serializer_class = serializers.RegionSerializer serializer_class = serializers.RegionSerializer
filterset_class = filters.RegionFilterSet filterset_class = filtersets.RegionFilterSet
#
# Site groups
#
class SiteGroupViewSet(CustomFieldModelViewSet):
queryset = SiteGroup.objects.add_related_count(
SiteGroup.objects.all(),
Site,
'group',
'site_count',
cumulative=True
)
serializer_class = serializers.SiteGroupSerializer
filterset_class = filtersets.SiteGroupFilterSet
# #
@@ -127,35 +138,41 @@ class SiteViewSet(CustomFieldModelViewSet):
virtualmachine_count=count_related(VirtualMachine, 'cluster__site') virtualmachine_count=count_related(VirtualMachine, 'cluster__site')
) )
serializer_class = serializers.SiteSerializer serializer_class = serializers.SiteSerializer
filterset_class = filters.SiteFilterSet filterset_class = filtersets.SiteFilterSet
# #
# Rack groups # Locations
# #
class RackGroupViewSet(ModelViewSet): class LocationViewSet(CustomFieldModelViewSet):
queryset = RackGroup.objects.add_related_count( queryset = Location.objects.add_related_count(
RackGroup.objects.all(), Location.objects.add_related_count(
Location.objects.all(),
Device,
'location',
'device_count',
cumulative=True
),
Rack, Rack,
'group', 'location',
'rack_count', 'rack_count',
cumulative=True cumulative=True
).prefetch_related('site') ).prefetch_related('site')
serializer_class = serializers.RackGroupSerializer serializer_class = serializers.LocationSerializer
filterset_class = filters.RackGroupFilterSet filterset_class = filtersets.LocationFilterSet
# #
# Rack roles # Rack roles
# #
class RackRoleViewSet(ModelViewSet): class RackRoleViewSet(CustomFieldModelViewSet):
queryset = RackRole.objects.annotate( queryset = RackRole.objects.annotate(
rack_count=count_related(Rack, 'role') rack_count=count_related(Rack, 'role')
) )
serializer_class = serializers.RackRoleSerializer serializer_class = serializers.RackRoleSerializer
filterset_class = filters.RackRoleFilterSet filterset_class = filtersets.RackRoleFilterSet
# #
@@ -164,13 +181,13 @@ class RackRoleViewSet(ModelViewSet):
class RackViewSet(CustomFieldModelViewSet): class RackViewSet(CustomFieldModelViewSet):
queryset = Rack.objects.prefetch_related( queryset = Rack.objects.prefetch_related(
'site', 'group__site', 'role', 'tenant', 'tags' 'site', 'location', 'role', 'tenant', 'tags'
).annotate( ).annotate(
device_count=count_related(Device, 'rack'), device_count=count_related(Device, 'rack'),
powerfeed_count=count_related(PowerFeed, 'rack') powerfeed_count=count_related(PowerFeed, 'rack')
) )
serializer_class = serializers.RackSerializer serializer_class = serializers.RackSerializer
filterset_class = filters.RackFilterSet filterset_class = filtersets.RackFilterSet
@swagger_auto_schema( @swagger_auto_schema(
responses={200: serializers.RackUnitSerializer(many=True)}, responses={200: serializers.RackUnitSerializer(many=True)},
@@ -227,25 +244,21 @@ class RackViewSet(CustomFieldModelViewSet):
class RackReservationViewSet(ModelViewSet): class RackReservationViewSet(ModelViewSet):
queryset = RackReservation.objects.prefetch_related('rack', 'user', 'tenant') queryset = RackReservation.objects.prefetch_related('rack', 'user', 'tenant')
serializer_class = serializers.RackReservationSerializer serializer_class = serializers.RackReservationSerializer
filterset_class = filters.RackReservationFilterSet filterset_class = filtersets.RackReservationFilterSet
# Assign user from request
def perform_create(self, serializer):
serializer.save(user=self.request.user)
# #
# Manufacturers # Manufacturers
# #
class ManufacturerViewSet(ModelViewSet): class ManufacturerViewSet(CustomFieldModelViewSet):
queryset = Manufacturer.objects.annotate( queryset = Manufacturer.objects.annotate(
devicetype_count=count_related(DeviceType, 'manufacturer'), devicetype_count=count_related(DeviceType, 'manufacturer'),
inventoryitem_count=count_related(InventoryItem, 'manufacturer'), inventoryitem_count=count_related(InventoryItem, 'manufacturer'),
platform_count=count_related(Platform, 'manufacturer') platform_count=count_related(Platform, 'manufacturer')
) )
serializer_class = serializers.ManufacturerSerializer serializer_class = serializers.ManufacturerSerializer
filterset_class = filters.ManufacturerFilterSet filterset_class = filtersets.ManufacturerFilterSet
# #
@@ -257,7 +270,7 @@ class DeviceTypeViewSet(CustomFieldModelViewSet):
device_count=count_related(Device, 'device_type') device_count=count_related(Device, 'device_type')
) )
serializer_class = serializers.DeviceTypeSerializer serializer_class = serializers.DeviceTypeSerializer
filterset_class = filters.DeviceTypeFilterSet filterset_class = filtersets.DeviceTypeFilterSet
brief_prefetch_fields = ['manufacturer'] brief_prefetch_fields = ['manufacturer']
@@ -268,75 +281,75 @@ class DeviceTypeViewSet(CustomFieldModelViewSet):
class ConsolePortTemplateViewSet(ModelViewSet): class ConsolePortTemplateViewSet(ModelViewSet):
queryset = ConsolePortTemplate.objects.prefetch_related('device_type__manufacturer') queryset = ConsolePortTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.ConsolePortTemplateSerializer serializer_class = serializers.ConsolePortTemplateSerializer
filterset_class = filters.ConsolePortTemplateFilterSet filterset_class = filtersets.ConsolePortTemplateFilterSet
class ConsoleServerPortTemplateViewSet(ModelViewSet): class ConsoleServerPortTemplateViewSet(ModelViewSet):
queryset = ConsoleServerPortTemplate.objects.prefetch_related('device_type__manufacturer') queryset = ConsoleServerPortTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.ConsoleServerPortTemplateSerializer serializer_class = serializers.ConsoleServerPortTemplateSerializer
filterset_class = filters.ConsoleServerPortTemplateFilterSet filterset_class = filtersets.ConsoleServerPortTemplateFilterSet
class PowerPortTemplateViewSet(ModelViewSet): class PowerPortTemplateViewSet(ModelViewSet):
queryset = PowerPortTemplate.objects.prefetch_related('device_type__manufacturer') queryset = PowerPortTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.PowerPortTemplateSerializer serializer_class = serializers.PowerPortTemplateSerializer
filterset_class = filters.PowerPortTemplateFilterSet filterset_class = filtersets.PowerPortTemplateFilterSet
class PowerOutletTemplateViewSet(ModelViewSet): class PowerOutletTemplateViewSet(ModelViewSet):
queryset = PowerOutletTemplate.objects.prefetch_related('device_type__manufacturer') queryset = PowerOutletTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.PowerOutletTemplateSerializer serializer_class = serializers.PowerOutletTemplateSerializer
filterset_class = filters.PowerOutletTemplateFilterSet filterset_class = filtersets.PowerOutletTemplateFilterSet
class InterfaceTemplateViewSet(ModelViewSet): class InterfaceTemplateViewSet(ModelViewSet):
queryset = InterfaceTemplate.objects.prefetch_related('device_type__manufacturer') queryset = InterfaceTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.InterfaceTemplateSerializer serializer_class = serializers.InterfaceTemplateSerializer
filterset_class = filters.InterfaceTemplateFilterSet filterset_class = filtersets.InterfaceTemplateFilterSet
class FrontPortTemplateViewSet(ModelViewSet): class FrontPortTemplateViewSet(ModelViewSet):
queryset = FrontPortTemplate.objects.prefetch_related('device_type__manufacturer') queryset = FrontPortTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.FrontPortTemplateSerializer serializer_class = serializers.FrontPortTemplateSerializer
filterset_class = filters.FrontPortTemplateFilterSet filterset_class = filtersets.FrontPortTemplateFilterSet
class RearPortTemplateViewSet(ModelViewSet): class RearPortTemplateViewSet(ModelViewSet):
queryset = RearPortTemplate.objects.prefetch_related('device_type__manufacturer') queryset = RearPortTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.RearPortTemplateSerializer serializer_class = serializers.RearPortTemplateSerializer
filterset_class = filters.RearPortTemplateFilterSet filterset_class = filtersets.RearPortTemplateFilterSet
class DeviceBayTemplateViewSet(ModelViewSet): class DeviceBayTemplateViewSet(ModelViewSet):
queryset = DeviceBayTemplate.objects.prefetch_related('device_type__manufacturer') queryset = DeviceBayTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.DeviceBayTemplateSerializer serializer_class = serializers.DeviceBayTemplateSerializer
filterset_class = filters.DeviceBayTemplateFilterSet filterset_class = filtersets.DeviceBayTemplateFilterSet
# #
# Device roles # Device roles
# #
class DeviceRoleViewSet(ModelViewSet): class DeviceRoleViewSet(CustomFieldModelViewSet):
queryset = DeviceRole.objects.annotate( queryset = DeviceRole.objects.annotate(
device_count=count_related(Device, 'device_role'), device_count=count_related(Device, 'device_role'),
virtualmachine_count=count_related(VirtualMachine, 'role') virtualmachine_count=count_related(VirtualMachine, 'role')
) )
serializer_class = serializers.DeviceRoleSerializer serializer_class = serializers.DeviceRoleSerializer
filterset_class = filters.DeviceRoleFilterSet filterset_class = filtersets.DeviceRoleFilterSet
# #
# Platforms # Platforms
# #
class PlatformViewSet(ModelViewSet): class PlatformViewSet(CustomFieldModelViewSet):
queryset = Platform.objects.annotate( queryset = Platform.objects.annotate(
device_count=count_related(Device, 'platform'), device_count=count_related(Device, 'platform'),
virtualmachine_count=count_related(VirtualMachine, 'platform') virtualmachine_count=count_related(VirtualMachine, 'platform')
) )
serializer_class = serializers.PlatformSerializer serializer_class = serializers.PlatformSerializer
filterset_class = filters.PlatformFilterSet filterset_class = filtersets.PlatformFilterSet
# #
@@ -345,10 +358,10 @@ class PlatformViewSet(ModelViewSet):
class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet): class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
queryset = Device.objects.prefetch_related( queryset = Device.objects.prefetch_related(
'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'rack', 'parent_bay', 'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'location', 'rack', 'parent_bay',
'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags', 'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags',
) )
filterset_class = filters.DeviceFilterSet filterset_class = filtersets.DeviceFilterSet
def get_serializer_class(self): def get_serializer_class(self):
""" """
@@ -493,7 +506,7 @@ class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
class ConsolePortViewSet(PathEndpointMixin, ModelViewSet): class ConsolePortViewSet(PathEndpointMixin, ModelViewSet):
queryset = ConsolePort.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags') queryset = ConsolePort.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags')
serializer_class = serializers.ConsolePortSerializer serializer_class = serializers.ConsolePortSerializer
filterset_class = filters.ConsolePortFilterSet filterset_class = filtersets.ConsolePortFilterSet
brief_prefetch_fields = ['device'] brief_prefetch_fields = ['device']
@@ -502,58 +515,58 @@ class ConsoleServerPortViewSet(PathEndpointMixin, ModelViewSet):
'device', '_path__destination', 'cable', '_cable_peer', 'tags' 'device', '_path__destination', 'cable', '_cable_peer', 'tags'
) )
serializer_class = serializers.ConsoleServerPortSerializer serializer_class = serializers.ConsoleServerPortSerializer
filterset_class = filters.ConsoleServerPortFilterSet filterset_class = filtersets.ConsoleServerPortFilterSet
brief_prefetch_fields = ['device'] brief_prefetch_fields = ['device']
class PowerPortViewSet(PathEndpointMixin, ModelViewSet): class PowerPortViewSet(PathEndpointMixin, ModelViewSet):
queryset = PowerPort.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags') queryset = PowerPort.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags')
serializer_class = serializers.PowerPortSerializer serializer_class = serializers.PowerPortSerializer
filterset_class = filters.PowerPortFilterSet filterset_class = filtersets.PowerPortFilterSet
brief_prefetch_fields = ['device'] brief_prefetch_fields = ['device']
class PowerOutletViewSet(PathEndpointMixin, ModelViewSet): class PowerOutletViewSet(PathEndpointMixin, ModelViewSet):
queryset = PowerOutlet.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags') queryset = PowerOutlet.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags')
serializer_class = serializers.PowerOutletSerializer serializer_class = serializers.PowerOutletSerializer
filterset_class = filters.PowerOutletFilterSet filterset_class = filtersets.PowerOutletFilterSet
brief_prefetch_fields = ['device'] brief_prefetch_fields = ['device']
class InterfaceViewSet(PathEndpointMixin, ModelViewSet): class InterfaceViewSet(PathEndpointMixin, ModelViewSet):
queryset = Interface.objects.prefetch_related( queryset = Interface.objects.prefetch_related(
'device', '_path__destination', 'cable', '_cable_peer', 'ip_addresses', 'tags' 'device', 'parent', 'lag', '_path__destination', 'cable', '_cable_peer', 'ip_addresses', 'tags'
) )
serializer_class = serializers.InterfaceSerializer serializer_class = serializers.InterfaceSerializer
filterset_class = filters.InterfaceFilterSet filterset_class = filtersets.InterfaceFilterSet
brief_prefetch_fields = ['device'] brief_prefetch_fields = ['device']
class FrontPortViewSet(PassThroughPortMixin, ModelViewSet): class FrontPortViewSet(PassThroughPortMixin, ModelViewSet):
queryset = FrontPort.objects.prefetch_related('device__device_type__manufacturer', 'rear_port', 'cable', 'tags') queryset = FrontPort.objects.prefetch_related('device__device_type__manufacturer', 'rear_port', 'cable', 'tags')
serializer_class = serializers.FrontPortSerializer serializer_class = serializers.FrontPortSerializer
filterset_class = filters.FrontPortFilterSet filterset_class = filtersets.FrontPortFilterSet
brief_prefetch_fields = ['device'] brief_prefetch_fields = ['device']
class RearPortViewSet(PassThroughPortMixin, ModelViewSet): class RearPortViewSet(PassThroughPortMixin, ModelViewSet):
queryset = RearPort.objects.prefetch_related('device__device_type__manufacturer', 'cable', 'tags') queryset = RearPort.objects.prefetch_related('device__device_type__manufacturer', 'cable', 'tags')
serializer_class = serializers.RearPortSerializer serializer_class = serializers.RearPortSerializer
filterset_class = filters.RearPortFilterSet filterset_class = filtersets.RearPortFilterSet
brief_prefetch_fields = ['device'] brief_prefetch_fields = ['device']
class DeviceBayViewSet(ModelViewSet): class DeviceBayViewSet(ModelViewSet):
queryset = DeviceBay.objects.prefetch_related('installed_device').prefetch_related('tags') queryset = DeviceBay.objects.prefetch_related('installed_device').prefetch_related('tags')
serializer_class = serializers.DeviceBaySerializer serializer_class = serializers.DeviceBaySerializer
filterset_class = filters.DeviceBayFilterSet filterset_class = filtersets.DeviceBayFilterSet
brief_prefetch_fields = ['device'] brief_prefetch_fields = ['device']
class InventoryItemViewSet(ModelViewSet): class InventoryItemViewSet(ModelViewSet):
queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer').prefetch_related('tags') queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer').prefetch_related('tags')
serializer_class = serializers.InventoryItemSerializer serializer_class = serializers.InventoryItemSerializer
filterset_class = filters.InventoryItemFilterSet filterset_class = filtersets.InventoryItemFilterSet
brief_prefetch_fields = ['device'] brief_prefetch_fields = ['device']
@@ -566,7 +579,7 @@ class ConsoleConnectionViewSet(ListModelMixin, GenericViewSet):
_path__destination_id__isnull=False _path__destination_id__isnull=False
) )
serializer_class = serializers.ConsolePortSerializer serializer_class = serializers.ConsolePortSerializer
filterset_class = filters.ConsoleConnectionFilterSet filterset_class = filtersets.ConsoleConnectionFilterSet
class PowerConnectionViewSet(ListModelMixin, GenericViewSet): class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
@@ -574,17 +587,19 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
_path__destination_id__isnull=False _path__destination_id__isnull=False
) )
serializer_class = serializers.PowerPortSerializer serializer_class = serializers.PowerPortSerializer
filterset_class = filters.PowerConnectionFilterSet filterset_class = filtersets.PowerConnectionFilterSet
class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet): class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet):
queryset = Interface.objects.prefetch_related('device', '_path').filter( queryset = Interface.objects.prefetch_related('device', '_path').filter(
# Avoid duplicate connections by only selecting the lower PK in a connected pair # Avoid duplicate connections by only selecting the lower PK in a connected pair
_path__destination_type__app_label='dcim',
_path__destination_type__model='interface',
_path__destination_id__isnull=False, _path__destination_id__isnull=False,
pk__lt=F('_path__destination_id') pk__lt=F('_path__destination_id')
) )
serializer_class = serializers.InterfaceConnectionSerializer serializer_class = serializers.InterfaceConnectionSerializer
filterset_class = filters.InterfaceConnectionFilterSet filterset_class = filtersets.InterfaceConnectionFilterSet
# #
@@ -597,7 +612,7 @@ class CableViewSet(ModelViewSet):
'termination_a', 'termination_b' 'termination_a', 'termination_b'
) )
serializer_class = serializers.CableSerializer serializer_class = serializers.CableSerializer
filterset_class = filters.CableFilterSet filterset_class = filtersets.CableFilterSet
# #
@@ -609,7 +624,7 @@ class VirtualChassisViewSet(ModelViewSet):
member_count=count_related(Device, 'virtual_chassis') member_count=count_related(Device, 'virtual_chassis')
) )
serializer_class = serializers.VirtualChassisSerializer serializer_class = serializers.VirtualChassisSerializer
filterset_class = filters.VirtualChassisFilterSet filterset_class = filtersets.VirtualChassisFilterSet
brief_prefetch_fields = ['master'] brief_prefetch_fields = ['master']
@@ -619,12 +634,12 @@ class VirtualChassisViewSet(ModelViewSet):
class PowerPanelViewSet(ModelViewSet): class PowerPanelViewSet(ModelViewSet):
queryset = PowerPanel.objects.prefetch_related( queryset = PowerPanel.objects.prefetch_related(
'site', 'rack_group' 'site', 'location'
).annotate( ).annotate(
powerfeed_count=count_related(PowerFeed, 'power_panel') powerfeed_count=count_related(PowerFeed, 'power_panel')
) )
serializer_class = serializers.PowerPanelSerializer serializer_class = serializers.PowerPanelSerializer
filterset_class = filters.PowerPanelFilterSet filterset_class = filtersets.PowerPanelFilterSet
# #
@@ -636,7 +651,7 @@ class PowerFeedViewSet(PathEndpointMixin, CustomFieldModelViewSet):
'power_panel', 'rack', '_path__destination', 'cable', '_cable_peer', 'tags' 'power_panel', 'rack', '_path__destination', 'cable', '_cable_peer', 'tags'
) )
serializer_class = serializers.PowerFeedSerializer serializer_class = serializers.PowerFeedSerializer
filterset_class = filters.PowerFeedFilterSet filterset_class = filtersets.PowerFeedFilterSet
# #

View File

@@ -217,6 +217,29 @@ class ConsolePortTypeChoices(ChoiceSet):
) )
class ConsolePortSpeedChoices(ChoiceSet):
SPEED_1200 = 1200
SPEED_2400 = 2400
SPEED_4800 = 4800
SPEED_9600 = 9600
SPEED_19200 = 19200
SPEED_38400 = 38400
SPEED_57600 = 57600
SPEED_115200 = 115200
CHOICES = (
(SPEED_1200, '1200 bps'),
(SPEED_2400, '2400 bps'),
(SPEED_4800, '4800 bps'),
(SPEED_9600, '9600 bps'),
(SPEED_19200, '19.2 kbps'),
(SPEED_38400, '38.4 kbps'),
(SPEED_57600, '57.6 kbps'),
(SPEED_115200, '115.2 kbps'),
)
# #
# PowerPorts # PowerPorts
# #
@@ -314,6 +337,10 @@ class PowerPortTypeChoices(ChoiceSet):
TYPE_USB_MICRO_B = 'usb-micro-b' TYPE_USB_MICRO_B = 'usb-micro-b'
TYPE_USB_3_B = 'usb-3-b' TYPE_USB_3_B = 'usb-3-b'
TYPE_USB_3_MICROB = 'usb-3-micro-b' TYPE_USB_3_MICROB = 'usb-3-micro-b'
# Direct current (DC)
TYPE_DC = 'dc-terminal'
# Proprietary
TYPE_SAF_D_GRID = 'saf-d-grid'
CHOICES = ( CHOICES = (
('IEC 60320', ( ('IEC 60320', (
@@ -414,6 +441,12 @@ class PowerPortTypeChoices(ChoiceSet):
(TYPE_USB_3_B, 'USB 3.0 Type B'), (TYPE_USB_3_B, 'USB 3.0 Type B'),
(TYPE_USB_3_MICROB, 'USB 3.0 Micro B'), (TYPE_USB_3_MICROB, 'USB 3.0 Micro B'),
)), )),
('DC', (
(TYPE_DC, 'DC Terminal'),
)),
('Proprietary', (
(TYPE_SAF_D_GRID, 'Saf-D-Grid'),
)),
) )
@@ -507,8 +540,11 @@ class PowerOutletTypeChoices(ChoiceSet):
TYPE_USB_A = 'usb-a' TYPE_USB_A = 'usb-a'
TYPE_USB_MICROB = 'usb-micro-b' TYPE_USB_MICROB = 'usb-micro-b'
TYPE_USB_C = 'usb-c' TYPE_USB_C = 'usb-c'
# Direct current (DC)
TYPE_DC = 'dc-terminal'
# Proprietary # Proprietary
TYPE_HDOT_CX = 'hdot-cx' TYPE_HDOT_CX = 'hdot-cx'
TYPE_SAF_D_GRID = 'saf-d-grid'
CHOICES = ( CHOICES = (
('IEC 60320', ( ('IEC 60320', (
@@ -602,8 +638,12 @@ class PowerOutletTypeChoices(ChoiceSet):
(TYPE_USB_MICROB, 'USB Micro B'), (TYPE_USB_MICROB, 'USB Micro B'),
(TYPE_USB_C, 'USB Type C'), (TYPE_USB_C, 'USB Type C'),
)), )),
('DC', (
(TYPE_DC, 'DC Terminal'),
)),
('Proprietary', ( ('Proprietary', (
(TYPE_HDOT_CX, 'HDOT Cx'), (TYPE_HDOT_CX, 'HDOT Cx'),
(TYPE_SAF_D_GRID, 'Saf-D-Grid'),
)), )),
) )
@@ -645,6 +685,7 @@ class InterfaceTypeChoices(ChoiceSet):
TYPE_10GE_XENPAK = '10gbase-x-xenpak' TYPE_10GE_XENPAK = '10gbase-x-xenpak'
TYPE_10GE_X2 = '10gbase-x-x2' TYPE_10GE_X2 = '10gbase-x-x2'
TYPE_25GE_SFP28 = '25gbase-x-sfp28' TYPE_25GE_SFP28 = '25gbase-x-sfp28'
TYPE_50GE_SFP56 = '50gbase-x-sfp56'
TYPE_40GE_QSFP_PLUS = '40gbase-x-qsfpp' TYPE_40GE_QSFP_PLUS = '40gbase-x-qsfpp'
TYPE_50GE_QSFP28 = '50gbase-x-sfp28' TYPE_50GE_QSFP28 = '50gbase-x-sfp28'
TYPE_100GE_CFP = '100gbase-x-cfp' TYPE_100GE_CFP = '100gbase-x-cfp'
@@ -749,6 +790,7 @@ class InterfaceTypeChoices(ChoiceSet):
(TYPE_10GE_XENPAK, 'XENPAK (10GE)'), (TYPE_10GE_XENPAK, 'XENPAK (10GE)'),
(TYPE_10GE_X2, 'X2 (10GE)'), (TYPE_10GE_X2, 'X2 (10GE)'),
(TYPE_25GE_SFP28, 'SFP28 (25GE)'), (TYPE_25GE_SFP28, 'SFP28 (25GE)'),
(TYPE_50GE_SFP56, 'SFP56 (50GE)'),
(TYPE_40GE_QSFP_PLUS, 'QSFP+ (40GE)'), (TYPE_40GE_QSFP_PLUS, 'QSFP+ (40GE)'),
(TYPE_50GE_QSFP28, 'QSFP28 (50GE)'), (TYPE_50GE_QSFP28, 'QSFP28 (50GE)'),
(TYPE_100GE_CFP, 'CFP (100GE)'), (TYPE_100GE_CFP, 'CFP (100GE)'),
@@ -881,6 +923,7 @@ class PortTypeChoices(ChoiceSet):
TYPE_TERA1P = 'tera-1p' TYPE_TERA1P = 'tera-1p'
TYPE_110_PUNCH = '110-punch' TYPE_110_PUNCH = '110-punch'
TYPE_BNC = 'bnc' TYPE_BNC = 'bnc'
TYPE_F = 'f'
TYPE_MRJ21 = 'mrj21' TYPE_MRJ21 = 'mrj21'
TYPE_ST = 'st' TYPE_ST = 'st'
TYPE_SC = 'sc' TYPE_SC = 'sc'
@@ -910,6 +953,7 @@ class PortTypeChoices(ChoiceSet):
(TYPE_TERA1P, 'TERA 1P'), (TYPE_TERA1P, 'TERA 1P'),
(TYPE_110_PUNCH, '110 Punch'), (TYPE_110_PUNCH, '110 Punch'),
(TYPE_BNC, 'BNC'), (TYPE_BNC, 'BNC'),
(TYPE_F, 'F Connector'),
(TYPE_MRJ21, 'MRJ21'), (TYPE_MRJ21, 'MRJ21'),
), ),
), ),
@@ -957,6 +1001,7 @@ class CableTypeChoices(ChoiceSet):
TYPE_MMF_OM2 = 'mmf-om2' TYPE_MMF_OM2 = 'mmf-om2'
TYPE_MMF_OM3 = 'mmf-om3' TYPE_MMF_OM3 = 'mmf-om3'
TYPE_MMF_OM4 = 'mmf-om4' TYPE_MMF_OM4 = 'mmf-om4'
TYPE_MMF_OM5 = 'mmf-om5'
TYPE_SMF = 'smf' TYPE_SMF = 'smf'
TYPE_SMF_OS1 = 'smf-os1' TYPE_SMF_OS1 = 'smf-os1'
TYPE_SMF_OS2 = 'smf-os2' TYPE_SMF_OS2 = 'smf-os2'
@@ -987,6 +1032,7 @@ class CableTypeChoices(ChoiceSet):
(TYPE_MMF_OM2, 'Multimode Fiber (OM2)'), (TYPE_MMF_OM2, 'Multimode Fiber (OM2)'),
(TYPE_MMF_OM3, 'Multimode Fiber (OM3)'), (TYPE_MMF_OM3, 'Multimode Fiber (OM3)'),
(TYPE_MMF_OM4, 'Multimode Fiber (OM4)'), (TYPE_MMF_OM4, 'Multimode Fiber (OM4)'),
(TYPE_MMF_OM5, 'Multimode Fiber (OM5)'),
(TYPE_SMF, 'Singlemode Fiber'), (TYPE_SMF, 'Singlemode Fiber'),
(TYPE_SMF_OS1, 'Singlemode Fiber (OS1)'), (TYPE_SMF_OS1, 'Singlemode Fiber (OS1)'),
(TYPE_SMF_OS2, 'Singlemode Fiber (OS2)'), (TYPE_SMF_OS2, 'Singlemode Fiber (OS2)'),

View File

@@ -1,25 +1,21 @@
import django_filters import django_filters
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models import Count
from extras.filters import CustomFieldModelFilterSet, LocalConfigContextFilterSet, CreatedUpdatedFilterSet from extras.filters import TagFilter
from tenancy.filters import TenancyFilterSet from extras.filtersets import LocalConfigContextFilterSet
from netbox.filtersets import (
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet,
)
from tenancy.filtersets import TenancyFilterSet
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.choices import ColorChoices from utilities.choices import ColorChoices
from utilities.filters import ( from utilities.filters import (
BaseFilterSet, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, TreeNodeMultipleChoiceFilter,
NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter,
) )
from virtualization.models import Cluster from virtualization.models import Cluster
from .choices import * from .choices import *
from .constants import * from .constants import *
from .models import ( from .models import *
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
InventoryItem, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
VirtualChassis,
)
__all__ = ( __all__ = (
@@ -41,6 +37,7 @@ __all__ = (
'InterfaceFilterSet', 'InterfaceFilterSet',
'InterfaceTemplateFilterSet', 'InterfaceTemplateFilterSet',
'InventoryItemFilterSet', 'InventoryItemFilterSet',
'LocationFilterSet',
'ManufacturerFilterSet', 'ManufacturerFilterSet',
'PathEndpointFilterSet', 'PathEndpointFilterSet',
'PlatformFilterSet', 'PlatformFilterSet',
@@ -52,18 +49,18 @@ __all__ = (
'PowerPortFilterSet', 'PowerPortFilterSet',
'PowerPortTemplateFilterSet', 'PowerPortTemplateFilterSet',
'RackFilterSet', 'RackFilterSet',
'RackGroupFilterSet',
'RackReservationFilterSet', 'RackReservationFilterSet',
'RackRoleFilterSet', 'RackRoleFilterSet',
'RearPortFilterSet', 'RearPortFilterSet',
'RearPortTemplateFilterSet', 'RearPortTemplateFilterSet',
'RegionFilterSet', 'RegionFilterSet',
'SiteFilterSet', 'SiteFilterSet',
'SiteGroupFilterSet',
'VirtualChassisFilterSet', 'VirtualChassisFilterSet',
) )
class RegionFilterSet(BaseFilterSet, NameSlugSearchFilterSet): class RegionFilterSet(OrganizationalModelFilterSet):
parent_id = django_filters.ModelMultipleChoiceFilter( parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
label='Parent region (ID)', label='Parent region (ID)',
@@ -80,7 +77,24 @@ class RegionFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug', 'description'] fields = ['id', 'name', 'slug', 'description']
class SiteFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet): class SiteGroupFilterSet(OrganizationalModelFilterSet):
parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
label='Parent site group (ID)',
)
parent = django_filters.ModelMultipleChoiceFilter(
field_name='parent__slug',
queryset=SiteGroup.objects.all(),
to_field_name='slug',
label='Parent site group (slug)',
)
class Meta:
model = SiteGroup
fields = ['id', 'name', 'slug', 'description']
class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@@ -97,11 +111,22 @@ class SiteFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
) )
region = TreeNodeMultipleChoiceFilter( region = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
field_name='region',
lookup_expr='in', lookup_expr='in',
to_field_name='slug', to_field_name='slug',
label='Region (slug)', label='Region (slug)',
) )
group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='group',
lookup_expr='in',
label='Group (ID)',
)
group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
lookup_expr='in',
to_field_name='slug',
label='Group (slug)',
)
tag = TagFilter() tag = TagFilter()
class Meta: class Meta:
@@ -132,7 +157,7 @@ class SiteFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
class RackGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet): class LocationFilterSet(OrganizationalModelFilterSet):
region_id = TreeNodeMultipleChoiceFilter( region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
field_name='site__region', field_name='site__region',
@@ -146,6 +171,19 @@ class RackGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
to_field_name='slug', to_field_name='slug',
label='Region (slug)', label='Region (slug)',
) )
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='site__group',
lookup_expr='in',
label='Site group (ID)',
)
site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='site__group',
lookup_expr='in',
to_field_name='slug',
label='Site group (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(), queryset=Site.objects.all(),
label='Site (ID)', label='Site (ID)',
@@ -156,30 +194,41 @@ class RackGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
to_field_name='slug', to_field_name='slug',
label='Site (slug)', label='Site (slug)',
) )
parent_id = django_filters.ModelMultipleChoiceFilter( parent_id = TreeNodeMultipleChoiceFilter(
queryset=RackGroup.objects.all(), queryset=Location.objects.all(),
label='Rack group (ID)', field_name='parent',
lookup_expr='in',
label='Location (ID)',
) )
parent = django_filters.ModelMultipleChoiceFilter( parent = TreeNodeMultipleChoiceFilter(
field_name='parent__slug', queryset=Location.objects.all(),
queryset=RackGroup.objects.all(), field_name='parent',
lookup_expr='in',
to_field_name='slug', to_field_name='slug',
label='Rack group (slug)', label='Location (slug)',
) )
class Meta: class Meta:
model = RackGroup model = Location
fields = ['id', 'name', 'slug', 'description'] fields = ['id', 'name', 'slug', 'description']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(description__icontains=value)
)
class RackRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
class RackRoleFilterSet(OrganizationalModelFilterSet):
class Meta: class Meta:
model = RackRole model = RackRole
fields = ['id', 'name', 'slug', 'color'] fields = ['id', 'name', 'slug', 'color']
class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet): class RackFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@@ -197,6 +246,19 @@ class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
to_field_name='slug', to_field_name='slug',
label='Region (slug)', label='Region (slug)',
) )
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='site__group',
lookup_expr='in',
label='Site group (ID)',
)
site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='site__group',
lookup_expr='in',
to_field_name='slug',
label='Site group (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(), queryset=Site.objects.all(),
label='Site (ID)', label='Site (ID)',
@@ -207,18 +269,18 @@ class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
to_field_name='slug', to_field_name='slug',
label='Site (slug)', label='Site (slug)',
) )
group_id = TreeNodeMultipleChoiceFilter( location_id = TreeNodeMultipleChoiceFilter(
queryset=RackGroup.objects.all(), queryset=Location.objects.all(),
field_name='group', field_name='location',
lookup_expr='in', lookup_expr='in',
label='Rack group (ID)', label='Location (ID)',
) )
group = TreeNodeMultipleChoiceFilter( location = TreeNodeMultipleChoiceFilter(
queryset=RackGroup.objects.all(), queryset=Location.objects.all(),
field_name='group', field_name='location',
lookup_expr='in', lookup_expr='in',
to_field_name='slug', to_field_name='slug',
label='Rack group (slug)', label='Location (slug)',
) )
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=RackStatusChoices, choices=RackStatusChoices,
@@ -264,7 +326,7 @@ class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
) )
class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet): class RackReservationFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@@ -284,18 +346,18 @@ class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModel
to_field_name='slug', to_field_name='slug',
label='Site (slug)', label='Site (slug)',
) )
group_id = TreeNodeMultipleChoiceFilter( location_id = TreeNodeMultipleChoiceFilter(
queryset=RackGroup.objects.all(), queryset=Location.objects.all(),
field_name='rack__group', field_name='rack__location',
lookup_expr='in', lookup_expr='in',
label='Rack group (ID)', label='Location (ID)',
) )
group = TreeNodeMultipleChoiceFilter( location = TreeNodeMultipleChoiceFilter(
queryset=RackGroup.objects.all(), queryset=Location.objects.all(),
field_name='rack__group', field_name='rack__location',
lookup_expr='in', lookup_expr='in',
to_field_name='slug', to_field_name='slug',
label='Rack group (slug)', label='Location (slug)',
) )
user_id = django_filters.ModelMultipleChoiceFilter( user_id = django_filters.ModelMultipleChoiceFilter(
queryset=User.objects.all(), queryset=User.objects.all(),
@@ -324,14 +386,14 @@ class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModel
) )
class ManufacturerFilterSet(BaseFilterSet, NameSlugSearchFilterSet): class ManufacturerFilterSet(OrganizationalModelFilterSet):
class Meta: class Meta:
model = Manufacturer model = Manufacturer
fields = ['id', 'name', 'slug', 'description'] fields = ['id', 'name', 'slug', 'description']
class DeviceTypeFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet): class DeviceTypeFilterSet(PrimaryModelFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@@ -417,7 +479,7 @@ class DeviceTypeFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdat
return queryset.exclude(devicebaytemplates__isnull=value) return queryset.exclude(devicebaytemplates__isnull=value)
class DeviceTypeComponentFilterSet(NameSlugSearchFilterSet): class DeviceTypeComponentFilterSet(django_filters.FilterSet):
devicetype_id = django_filters.ModelMultipleChoiceFilter( devicetype_id = django_filters.ModelMultipleChoiceFilter(
queryset=DeviceType.objects.all(), queryset=DeviceType.objects.all(),
field_name='device_type_id', field_name='device_type_id',
@@ -425,70 +487,86 @@ class DeviceTypeComponentFilterSet(NameSlugSearchFilterSet):
) )
class ConsolePortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): class ConsolePortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
class Meta: class Meta:
model = ConsolePortTemplate model = ConsolePortTemplate
fields = ['id', 'name', 'type'] fields = ['id', 'name', 'type']
class ConsoleServerPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): class ConsoleServerPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
class Meta: class Meta:
model = ConsoleServerPortTemplate model = ConsoleServerPortTemplate
fields = ['id', 'name', 'type'] fields = ['id', 'name', 'type']
class PowerPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): class PowerPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
class Meta: class Meta:
model = PowerPortTemplate model = PowerPortTemplate
fields = ['id', 'name', 'type', 'maximum_draw', 'allocated_draw'] fields = ['id', 'name', 'type', 'maximum_draw', 'allocated_draw']
class PowerOutletTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
feed_leg = django_filters.MultipleChoiceFilter(
choices=PowerOutletFeedLegChoices,
null_value=None
)
class Meta: class Meta:
model = PowerOutletTemplate model = PowerOutletTemplate
fields = ['id', 'name', 'type', 'feed_leg'] fields = ['id', 'name', 'type', 'feed_leg']
class InterfaceTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=InterfaceTypeChoices,
null_value=None
)
class Meta: class Meta:
model = InterfaceTemplate model = InterfaceTemplate
fields = ['id', 'name', 'type', 'mgmt_only'] fields = ['id', 'name', 'type', 'mgmt_only']
class FrontPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=PortTypeChoices,
null_value=None
)
class Meta: class Meta:
model = FrontPortTemplate model = FrontPortTemplate
fields = ['id', 'name', 'type'] fields = ['id', 'name', 'type']
class RearPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=PortTypeChoices,
null_value=None
)
class Meta: class Meta:
model = RearPortTemplate model = RearPortTemplate
fields = ['id', 'name', 'type', 'positions'] fields = ['id', 'name', 'type', 'positions']
class DeviceBayTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): class DeviceBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
class Meta: class Meta:
model = DeviceBayTemplate model = DeviceBayTemplate
fields = ['id', 'name'] fields = ['id', 'name']
class DeviceRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet): class DeviceRoleFilterSet(OrganizationalModelFilterSet):
class Meta: class Meta:
model = DeviceRole model = DeviceRole
fields = ['id', 'name', 'slug', 'color', 'vm_role'] fields = ['id', 'name', 'slug', 'color', 'vm_role']
class PlatformFilterSet(BaseFilterSet, NameSlugSearchFilterSet): class PlatformFilterSet(OrganizationalModelFilterSet):
manufacturer_id = django_filters.ModelMultipleChoiceFilter( manufacturer_id = django_filters.ModelMultipleChoiceFilter(
field_name='manufacturer', field_name='manufacturer',
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
@@ -506,13 +584,7 @@ class PlatformFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug', 'napalm_driver', 'description'] fields = ['id', 'name', 'slug', 'napalm_driver', 'description']
class DeviceFilterSet( class DeviceFilterSet(PrimaryModelFilterSet, TenancyFilterSet, LocalConfigContextFilterSet):
BaseFilterSet,
TenancyFilterSet,
LocalConfigContextFilterSet,
CustomFieldModelFilterSet,
CreatedUpdatedFilterSet
):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@@ -566,6 +638,19 @@ class DeviceFilterSet(
to_field_name='slug', to_field_name='slug',
label='Region (slug)', label='Region (slug)',
) )
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='site__group',
lookup_expr='in',
label='Site group (ID)',
)
site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='site__group',
lookup_expr='in',
to_field_name='slug',
label='Site group (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(), queryset=Site.objects.all(),
label='Site (ID)', label='Site (ID)',
@@ -576,11 +661,11 @@ class DeviceFilterSet(
to_field_name='slug', to_field_name='slug',
label='Site name (slug)', label='Site name (slug)',
) )
rack_group_id = TreeNodeMultipleChoiceFilter( location_id = TreeNodeMultipleChoiceFilter(
queryset=RackGroup.objects.all(), queryset=Location.objects.all(),
field_name='rack__group', field_name='location',
lookup_expr='in', lookup_expr='in',
label='Rack group (ID)', label='Location (ID)',
) )
rack_id = django_filters.ModelMultipleChoiceFilter( rack_id = django_filters.ModelMultipleChoiceFilter(
field_name='rack', field_name='rack',
@@ -722,6 +807,19 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
to_field_name='slug', to_field_name='slug',
label='Region (slug)', label='Region (slug)',
) )
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='device__site__group',
lookup_expr='in',
label='Site group (ID)',
)
site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='device__site__group',
lookup_expr='in',
to_field_name='slug',
label='Site group (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
field_name='device__site', field_name='device__site',
queryset=Site.objects.all(), queryset=Site.objects.all(),
@@ -775,7 +873,7 @@ class PathEndpointFilterSet(django_filters.FilterSet):
return queryset.filter(Q(_path__isnull=True) | Q(_path__is_active=False)) return queryset.filter(Q(_path__isnull=True) | Q(_path__is_active=False))
class ConsolePortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet): class ConsolePortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
type = django_filters.MultipleChoiceFilter( type = django_filters.MultipleChoiceFilter(
choices=ConsolePortTypeChoices, choices=ConsolePortTypeChoices,
null_value=None null_value=None
@@ -786,12 +884,7 @@ class ConsolePortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTermina
fields = ['id', 'name', 'label', 'description'] fields = ['id', 'name', 'label', 'description']
class ConsoleServerPortFilterSet( class ConsoleServerPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
BaseFilterSet,
DeviceComponentFilterSet,
CableTerminationFilterSet,
PathEndpointFilterSet
):
type = django_filters.MultipleChoiceFilter( type = django_filters.MultipleChoiceFilter(
choices=ConsolePortTypeChoices, choices=ConsolePortTypeChoices,
null_value=None null_value=None
@@ -802,7 +895,7 @@ class ConsoleServerPortFilterSet(
fields = ['id', 'name', 'label', 'description'] fields = ['id', 'name', 'label', 'description']
class PowerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet): class PowerPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
type = django_filters.MultipleChoiceFilter( type = django_filters.MultipleChoiceFilter(
choices=PowerPortTypeChoices, choices=PowerPortTypeChoices,
null_value=None null_value=None
@@ -813,18 +906,22 @@ class PowerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati
fields = ['id', 'name', 'label', 'maximum_draw', 'allocated_draw', 'description'] fields = ['id', 'name', 'label', 'maximum_draw', 'allocated_draw', 'description']
class PowerOutletFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet): class PowerOutletFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
type = django_filters.MultipleChoiceFilter( type = django_filters.MultipleChoiceFilter(
choices=PowerOutletTypeChoices, choices=PowerOutletTypeChoices,
null_value=None null_value=None
) )
feed_leg = django_filters.MultipleChoiceFilter(
choices=PowerOutletFeedLegChoices,
null_value=None
)
class Meta: class Meta:
model = PowerOutlet model = PowerOutlet
fields = ['id', 'name', 'label', 'feed_leg', 'description'] fields = ['id', 'name', 'label', 'feed_leg', 'description']
class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet): class InterfaceFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@@ -845,6 +942,11 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati
method='filter_kind', method='filter_kind',
label='Kind of interface', label='Kind of interface',
) )
parent_id = django_filters.ModelMultipleChoiceFilter(
field_name='parent',
queryset=Interface.objects.all(),
label='Parent interface (ID)',
)
lag_id = django_filters.ModelMultipleChoiceFilter( lag_id = django_filters.ModelMultipleChoiceFilter(
field_name='lag', field_name='lag',
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
@@ -874,7 +976,7 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati
devices = Device.objects.filter(**{'{}__in'.format(name): value}) devices = Device.objects.filter(**{'{}__in'.format(name): value})
vc_interface_ids = [] vc_interface_ids = []
for device in devices: for device in devices:
vc_interface_ids.extend(device.vc_interfaces.values_list('id', flat=True)) vc_interface_ids.extend(device.vc_interfaces().values_list('id', flat=True))
return queryset.filter(pk__in=vc_interface_ids) return queryset.filter(pk__in=vc_interface_ids)
except Device.DoesNotExist: except Device.DoesNotExist:
return queryset.none() return queryset.none()
@@ -885,7 +987,7 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati
try: try:
devices = Device.objects.filter(pk__in=id_list) devices = Device.objects.filter(pk__in=id_list)
for device in devices: for device in devices:
vc_interface_ids += device.vc_interfaces.values_list('id', flat=True) vc_interface_ids += device.vc_interfaces().values_list('id', flat=True)
return queryset.filter(pk__in=vc_interface_ids) return queryset.filter(pk__in=vc_interface_ids)
except Device.DoesNotExist: except Device.DoesNotExist:
return queryset.none() return queryset.none()
@@ -917,28 +1019,36 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati
}.get(value, queryset.none()) }.get(value, queryset.none())
class FrontPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet): class FrontPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=PortTypeChoices,
null_value=None
)
class Meta: class Meta:
model = FrontPort model = FrontPort
fields = ['id', 'name', 'label', 'type', 'description'] fields = ['id', 'name', 'label', 'type', 'description']
class RearPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet): class RearPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=PortTypeChoices,
null_value=None
)
class Meta: class Meta:
model = RearPort model = RearPort
fields = ['id', 'name', 'label', 'type', 'positions', 'description'] fields = ['id', 'name', 'label', 'type', 'positions', 'description']
class DeviceBayFilterSet(BaseFilterSet, DeviceComponentFilterSet): class DeviceBayFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):
class Meta: class Meta:
model = DeviceBay model = DeviceBay
fields = ['id', 'name', 'label', 'description'] fields = ['id', 'name', 'label', 'description']
class InventoryItemFilterSet(BaseFilterSet, DeviceComponentFilterSet): class InventoryItemFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@@ -1011,7 +1121,7 @@ class InventoryItemFilterSet(BaseFilterSet, DeviceComponentFilterSet):
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
class VirtualChassisFilterSet(BaseFilterSet): class VirtualChassisFilterSet(PrimaryModelFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@@ -1039,6 +1149,19 @@ class VirtualChassisFilterSet(BaseFilterSet):
to_field_name='slug', to_field_name='slug',
label='Region (slug)', label='Region (slug)',
) )
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='master__site__group',
lookup_expr='in',
label='Site group (ID)',
)
site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='master__site__group',
lookup_expr='in',
to_field_name='slug',
label='Site group (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
field_name='master__site', field_name='master__site',
queryset=Site.objects.all(), queryset=Site.objects.all(),
@@ -1078,7 +1201,7 @@ class VirtualChassisFilterSet(BaseFilterSet):
return queryset.filter(qs_filter).distinct() return queryset.filter(qs_filter).distinct()
class CableFilterSet(BaseFilterSet): class CableFilterSet(PrimaryModelFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@@ -1142,7 +1265,7 @@ class CableFilterSet(BaseFilterSet):
return queryset return queryset
class ConnectionFilterSet: class ConnectionFilterSet(BaseFilterSet):
def filter_site(self, queryset, name, value): def filter_site(self, queryset, name, value):
if not value.strip(): if not value.strip():
@@ -1155,7 +1278,7 @@ class ConnectionFilterSet:
return queryset.filter(**{f'{name}__in': value}) return queryset.filter(**{f'{name}__in': value})
class ConsoleConnectionFilterSet(ConnectionFilterSet, BaseFilterSet): class ConsoleConnectionFilterSet(ConnectionFilterSet):
site = django_filters.CharFilter( site = django_filters.CharFilter(
method='filter_site', method='filter_site',
label='Site (slug)', label='Site (slug)',
@@ -1173,7 +1296,7 @@ class ConsoleConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):
fields = ['name'] fields = ['name']
class PowerConnectionFilterSet(ConnectionFilterSet, BaseFilterSet): class PowerConnectionFilterSet(ConnectionFilterSet):
site = django_filters.CharFilter( site = django_filters.CharFilter(
method='filter_site', method='filter_site',
label='Site (slug)', label='Site (slug)',
@@ -1191,7 +1314,7 @@ class PowerConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):
fields = ['name'] fields = ['name']
class InterfaceConnectionFilterSet(ConnectionFilterSet, BaseFilterSet): class InterfaceConnectionFilterSet(ConnectionFilterSet):
site = django_filters.CharFilter( site = django_filters.CharFilter(
method='filter_site', method='filter_site',
label='Site (slug)', label='Site (slug)',
@@ -1209,7 +1332,7 @@ class InterfaceConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):
fields = [] fields = []
class PowerPanelFilterSet(BaseFilterSet): class PowerPanelFilterSet(PrimaryModelFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@@ -1227,6 +1350,19 @@ class PowerPanelFilterSet(BaseFilterSet):
to_field_name='slug', to_field_name='slug',
label='Region (slug)', label='Region (slug)',
) )
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='site__group',
lookup_expr='in',
label='Site group (ID)',
)
site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='site__group',
lookup_expr='in',
to_field_name='slug',
label='Site group (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(), queryset=Site.objects.all(),
label='Site (ID)', label='Site (ID)',
@@ -1237,11 +1373,11 @@ class PowerPanelFilterSet(BaseFilterSet):
to_field_name='slug', to_field_name='slug',
label='Site name (slug)', label='Site name (slug)',
) )
rack_group_id = TreeNodeMultipleChoiceFilter( location_id = TreeNodeMultipleChoiceFilter(
queryset=RackGroup.objects.all(), queryset=Location.objects.all(),
field_name='rack_group', field_name='location',
lookup_expr='in', lookup_expr='in',
label='Rack group (ID)', label='Location (ID)',
) )
tag = TagFilter() tag = TagFilter()
@@ -1258,13 +1394,7 @@ class PowerPanelFilterSet(BaseFilterSet):
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
class PowerFeedFilterSet( class PowerFeedFilterSet(PrimaryModelFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
BaseFilterSet,
CableTerminationFilterSet,
PathEndpointFilterSet,
CustomFieldModelFilterSet,
CreatedUpdatedFilterSet
):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@@ -1282,6 +1412,19 @@ class PowerFeedFilterSet(
to_field_name='slug', to_field_name='slug',
label='Region (slug)', label='Region (slug)',
) )
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='power_panel__site__group',
lookup_expr='in',
label='Site group (ID)',
)
site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='power_panel__site__group',
lookup_expr='in',
to_field_name='slug',
label='Site group (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
field_name='power_panel__site', field_name='power_panel__site',
queryset=Site.objects.all(), queryset=Site.objects.all(),
@@ -1302,6 +1445,10 @@ class PowerFeedFilterSet(
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
label='Rack (ID)', label='Rack (ID)',
) )
status = django_filters.MultipleChoiceFilter(
choices=PowerFeedStatusChoices,
null_value=None
)
tag = TagFilter() tag = TagFilter()
class Meta: class Meta:

File diff suppressed because it is too large Load Diff

View File

@@ -2,12 +2,10 @@ from django.core.management.base import BaseCommand
from django.core.management.color import no_style from django.core.management.color import no_style
from django.db import connection from django.db import connection
from circuits.models import CircuitTermination
from dcim.models import CablePath, ConsolePort, ConsoleServerPort, Interface, PowerFeed, PowerOutlet, PowerPort from dcim.models import CablePath, ConsolePort, ConsoleServerPort, Interface, PowerFeed, PowerOutlet, PowerPort
from dcim.signals import create_cablepath from dcim.signals import create_cablepath
ENDPOINT_MODELS = ( ENDPOINT_MODELS = (
CircuitTermination,
ConsolePort, ConsolePort,
ConsoleServerPort, ConsoleServerPort,
Interface, Interface,

View File

@@ -0,0 +1,422 @@
import django.core.serializers.json
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0122_standardize_name_length'),
]
operations = [
migrations.AddField(
model_name='consoleport',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='consoleport',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='consoleport',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='consoleporttemplate',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='consoleporttemplate',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='consoleserverport',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='consoleserverport',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='consoleserverport',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='consoleserverporttemplate',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='consoleserverporttemplate',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='devicebay',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='devicebay',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='devicebay',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='devicebaytemplate',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='devicebaytemplate',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='devicerole',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='frontport',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='frontport',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='frontport',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='frontporttemplate',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='frontporttemplate',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='interface',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='interface',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='interface',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='interfacetemplate',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='interfacetemplate',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='inventoryitem',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='inventoryitem',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='inventoryitem',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='manufacturer',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='platform',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='poweroutlet',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='poweroutlet',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='poweroutlet',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='poweroutlettemplate',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='poweroutlettemplate',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='powerport',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='powerport',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='powerport',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='powerporttemplate',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='powerporttemplate',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='rackgroup',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='rackrole',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='rearport',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='rearport',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='rearport',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='rearporttemplate',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='rearporttemplate',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='region',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AlterField(
model_name='cable',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='cablepath',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='consoleport',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='consoleporttemplate',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='consoleserverport',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='consoleserverporttemplate',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='device',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='devicebay',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='devicebaytemplate',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='devicerole',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='devicetype',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='frontport',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='frontporttemplate',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='interface',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='interfacetemplate',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='inventoryitem',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='manufacturer',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='platform',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='powerfeed',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='poweroutlet',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='poweroutlettemplate',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='powerpanel',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='powerport',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='powerporttemplate',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='rack',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='rackgroup',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='rackreservation',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='rackrole',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='rearport',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='rearporttemplate',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='region',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='site',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='virtualchassis',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
]

View File

@@ -0,0 +1,51 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0123_standardize_models'),
]
operations = [
migrations.AddField(
model_name='consoleport',
name='mark_connected',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='consoleserverport',
name='mark_connected',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='frontport',
name='mark_connected',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='interface',
name='mark_connected',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='powerfeed',
name='mark_connected',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='poweroutlet',
name='mark_connected',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='powerport',
name='mark_connected',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='rearport',
name='mark_connected',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,21 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0124_mark_connected'),
]
operations = [
migrations.AddField(
model_name='consoleport',
name='speed',
field=models.PositiveSmallIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='consoleserverport',
name='speed',
field=models.PositiveSmallIntegerField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,39 @@
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dcim', '0125_console_port_speed'),
]
operations = [
migrations.RenameModel(
old_name='RackGroup',
new_name='Location',
),
migrations.AlterModelOptions(
name='rack',
options={'ordering': ('site', 'location', '_name', 'pk')},
),
migrations.AlterField(
model_name='location',
name='site',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='locations', to='dcim.site'),
),
migrations.RenameField(
model_name='powerpanel',
old_name='rack_group',
new_name='location',
),
migrations.RenameField(
model_name='rack',
old_name='group',
new_name='location',
),
migrations.AlterUniqueTogether(
name='rack',
unique_together={('location', 'facility_id'), ('location', 'name')},
),
]

View File

@@ -0,0 +1,17 @@
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dcim', '0126_rename_rackgroup_location'),
]
operations = [
migrations.AddField(
model_name='device',
name='location',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.location'),
),
]

View File

@@ -0,0 +1,24 @@
from django.db import migrations
from django.db.models import Subquery, OuterRef
def populate_device_location(apps, schema_editor):
Device = apps.get_model('dcim', 'Device')
Device.objects.filter(rack__isnull=False).update(
location_id=Subquery(
Device.objects.filter(pk=OuterRef('pk')).values('rack__location_id')[:1]
)
)
class Migration(migrations.Migration):
dependencies = [
('dcim', '0127_device_location'),
]
operations = [
migrations.RunPython(
code=populate_device_location
),
]

View File

@@ -0,0 +1,17 @@
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dcim', '0128_device_location_populate'),
]
operations = [
migrations.AddField(
model_name='interface',
name='parent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='child_interfaces', to='dcim.interface'),
),
]

View File

@@ -0,0 +1,39 @@
import django.core.serializers.json
from django.db import migrations, models
import django.db.models.deletion
import mptt.fields
class Migration(migrations.Migration):
dependencies = [
('dcim', '0129_interface_parent'),
]
operations = [
migrations.CreateModel(
name='SiteGroup',
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, unique=True)),
('slug', models.SlugField(max_length=100, unique=True)),
('description', models.CharField(blank=True, max_length=200)),
('lft', models.PositiveIntegerField(editable=False)),
('rght', models.PositiveIntegerField(editable=False)),
('tree_id', models.PositiveIntegerField(db_index=True, editable=False)),
('level', models.PositiveIntegerField(editable=False)),
('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='dcim.sitegroup')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='site',
name='group',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sites', to='dcim.sitegroup'),
),
]

View File

@@ -0,0 +1,21 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0130_sitegroup'),
]
operations = [
migrations.AlterField(
model_name='consoleport',
name='speed',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AlterField(
model_name='consoleserverport',
name='speed',
field=models.PositiveIntegerField(blank=True, null=True),
),
]

View File

@@ -34,12 +34,13 @@ __all__ = (
'PowerPort', 'PowerPort',
'PowerPortTemplate', 'PowerPortTemplate',
'Rack', 'Rack',
'RackGroup', 'Location',
'RackReservation', 'RackReservation',
'RackRole', 'RackRole',
'RearPort', 'RearPort',
'RearPortTemplate', 'RearPortTemplate',
'Region', 'Region',
'Site', 'Site',
'SiteGroup',
'VirtualChassis', 'VirtualChassis',
) )

View File

@@ -6,14 +6,13 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db import models from django.db import models
from django.db.models import Sum from django.db.models import Sum
from django.urls import reverse from django.urls import reverse
from taggit.managers import TaggableManager
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.fields import PathField from dcim.fields import PathField
from dcim.utils import decompile_path_node, object_to_path_node, path_node_to_object from dcim.utils import decompile_path_node, object_to_path_node, path_node_to_object
from extras.models import ChangeLoggedModel, CustomFieldModel, TaggedItem
from extras.utils import extras_features from extras.utils import extras_features
from netbox.models import BigIDModel, PrimaryModel
from utilities.fields import ColorField from utilities.fields import ColorField
from utilities.querysets import RestrictedQuerySet from utilities.querysets import RestrictedQuerySet
from utilities.utils import to_meters from utilities.utils import to_meters
@@ -31,8 +30,8 @@ __all__ = (
# Cables # Cables
# #
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Cable(ChangeLoggedModel, CustomFieldModel): class Cable(PrimaryModel):
""" """
A physical connection between two endpoints. A physical connection between two endpoints.
""" """
@@ -107,7 +106,6 @@ class Cable(ChangeLoggedModel, CustomFieldModel):
blank=True, blank=True,
null=True null=True
) )
tags = TaggableManager(through=TaggedItem)
objects = RestrictedQuerySet.as_manager() objects = RestrictedQuerySet.as_manager()
@@ -244,6 +242,16 @@ class Cable(ChangeLoggedModel, CustomFieldModel):
): ):
raise ValidationError("A front port cannot be connected to it corresponding rear port") raise ValidationError("A front port cannot be connected to it corresponding rear port")
# A CircuitTermination attached to a ProviderNetwork cannot have a Cable
if isinstance(self.termination_a, CircuitTermination) and self.termination_a.provider_network is not None:
raise ValidationError({
'termination_a_id': "Circuit terminations attached to a provider network may not be cabled."
})
if isinstance(self.termination_b, CircuitTermination) and self.termination_b.provider_network is not None:
raise ValidationError({
'termination_b_id': "Circuit terminations attached to a provider network may not be cabled."
})
# Check for an existing Cable connected to either termination object # Check for an existing Cable connected to either termination object
if self.termination_a.cable not in (None, self): if self.termination_a.cable not in (None, self):
raise ValidationError("{} already has a cable attached (#{})".format( raise ValidationError("{} already has a cable attached (#{})".format(
@@ -305,7 +313,7 @@ class Cable(ChangeLoggedModel, CustomFieldModel):
return COMPATIBLE_TERMINATION_TYPES[self.termination_a._meta.model_name] return COMPATIBLE_TERMINATION_TYPES[self.termination_a._meta.model_name]
class CablePath(models.Model): class CablePath(BigIDModel):
""" """
A CablePath instance represents the physical path from an origin to a destination, including all intermediate A CablePath instance represents the physical path from an origin to a destination, including all intermediate
elements in the path. Every instance must specify an `origin`, whereas `destination` may be null (for paths which do elements in the path. Every instance must specify an `origin`, whereas `destination` may be null (for paths which do
@@ -386,6 +394,8 @@ class CablePath(models.Model):
""" """
Create a new CablePath instance as traced from the given path origin. Create a new CablePath instance as traced from the given path origin.
""" """
from circuits.models import CircuitTermination
if origin is None or origin.cable is None: if origin is None or origin.cable is None:
return None return None
@@ -433,6 +443,23 @@ class CablePath(models.Model):
# No corresponding FrontPort found for the RearPort # No corresponding FrontPort found for the RearPort
break break
# Follow a CircuitTermination to its corresponding CircuitTermination (A to Z or vice versa)
elif isinstance(peer_termination, CircuitTermination):
path.append(object_to_path_node(peer_termination))
# Get peer CircuitTermination
node = peer_termination.get_peer_termination()
if node:
path.append(object_to_path_node(node))
if node.provider_network:
destination = node.provider_network
break
elif node.site and not node.cable:
destination = node.site
break
else:
# No peer CircuitTermination exists; halt the trace
break
# Anything else marks the end of the path # Anything else marks the end of the path
else: else:
destination = peer_termination destination = peer_termination
@@ -478,15 +505,33 @@ class CablePath(models.Model):
return path return path
@property
def last_node(self):
"""
Return either the destination or the last node within the path.
"""
return self.destination or path_node_to_object(self.path[-1])
def get_cable_ids(self):
"""
Return all Cable IDs within the path.
"""
cable_ct = ContentType.objects.get_for_model(Cable).pk
cable_ids = []
for node in self.path:
ct, id = decompile_path_node(node)
if ct == cable_ct:
cable_ids.append(id)
return cable_ids
def get_total_length(self): def get_total_length(self):
""" """
Return a tuple containing the sum of the length of each cable in the path Return a tuple containing the sum of the length of each cable in the path
and a flag indicating whether the length is definitive. and a flag indicating whether the length is definitive.
""" """
cable_ids = [ cable_ids = self.get_cable_ids()
# Starting from the first element, every third element in the path should be a Cable
decompile_path_node(self.path[i])[1] for i in range(0, len(self.path), 3)
]
cables = Cable.objects.filter(id__in=cable_ids, _abs_length__isnull=False) cables = Cable.objects.filter(id__in=cable_ids, _abs_length__isnull=False)
total_length = cables.aggregate(total=Sum('_abs_length'))['total'] total_length = cables.aggregate(total=Sum('_abs_length'))['total']
is_definitive = len(cables) == len(cable_ids) is_definitive = len(cables) == len(cable_ids)

View File

@@ -4,11 +4,11 @@ from django.db import models
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from extras.models import ObjectChange from extras.utils import extras_features
from netbox.models import ChangeLoggedModel
from utilities.fields import NaturalOrderingField from utilities.fields import NaturalOrderingField
from utilities.querysets import RestrictedQuerySet from utilities.querysets import RestrictedQuerySet
from utilities.ordering import naturalize_interface from utilities.ordering import naturalize_interface
from utilities.utils import serialize_object
from .device_components import ( from .device_components import (
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, PowerOutlet, PowerPort, RearPort, ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, PowerOutlet, PowerPort, RearPort,
) )
@@ -26,7 +26,7 @@ __all__ = (
) )
class ComponentTemplateModel(models.Model): class ComponentTemplateModel(ChangeLoggedModel):
device_type = models.ForeignKey( device_type = models.ForeignKey(
to='dcim.DeviceType', to='dcim.DeviceType',
on_delete=models.CASCADE, on_delete=models.CASCADE,
@@ -73,15 +73,10 @@ class ComponentTemplateModel(models.Model):
except ObjectDoesNotExist: except ObjectDoesNotExist:
# The parent DeviceType has already been deleted # The parent DeviceType has already been deleted
device_type = None device_type = None
return ObjectChange( return super().to_objectchange(action, related_object=device_type)
changed_object=self,
object_repr=str(self),
action=action,
related_object=device_type,
object_data=serialize_object(self)
)
@extras_features('webhooks')
class ConsolePortTemplate(ComponentTemplateModel): class ConsolePortTemplate(ComponentTemplateModel):
""" """
A template for a ConsolePort to be created for a new Device. A template for a ConsolePort to be created for a new Device.
@@ -105,6 +100,7 @@ class ConsolePortTemplate(ComponentTemplateModel):
) )
@extras_features('webhooks')
class ConsoleServerPortTemplate(ComponentTemplateModel): class ConsoleServerPortTemplate(ComponentTemplateModel):
""" """
A template for a ConsoleServerPort to be created for a new Device. A template for a ConsoleServerPort to be created for a new Device.
@@ -128,6 +124,7 @@ class ConsoleServerPortTemplate(ComponentTemplateModel):
) )
@extras_features('webhooks')
class PowerPortTemplate(ComponentTemplateModel): class PowerPortTemplate(ComponentTemplateModel):
""" """
A template for a PowerPort to be created for a new Device. A template for a PowerPort to be created for a new Device.
@@ -174,6 +171,7 @@ class PowerPortTemplate(ComponentTemplateModel):
}) })
@extras_features('webhooks')
class PowerOutletTemplate(ComponentTemplateModel): class PowerOutletTemplate(ComponentTemplateModel):
""" """
A template for a PowerOutlet to be created for a new Device. A template for a PowerOutlet to be created for a new Device.
@@ -225,6 +223,7 @@ class PowerOutletTemplate(ComponentTemplateModel):
) )
@extras_features('webhooks')
class InterfaceTemplate(ComponentTemplateModel): class InterfaceTemplate(ComponentTemplateModel):
""" """
A template for a physical data interface on a new Device. A template for a physical data interface on a new Device.
@@ -259,6 +258,7 @@ class InterfaceTemplate(ComponentTemplateModel):
) )
@extras_features('webhooks')
class FrontPortTemplate(ComponentTemplateModel): class FrontPortTemplate(ComponentTemplateModel):
""" """
Template for a pass-through port on the front of a new Device. Template for a pass-through port on the front of a new Device.
@@ -319,6 +319,7 @@ class FrontPortTemplate(ComponentTemplateModel):
) )
@extras_features('webhooks')
class RearPortTemplate(ComponentTemplateModel): class RearPortTemplate(ComponentTemplateModel):
""" """
Template for a pass-through port on the rear of a new Device. Template for a pass-through port on the rear of a new Device.
@@ -349,6 +350,7 @@ class RearPortTemplate(ComponentTemplateModel):
) )
@extras_features('webhooks')
class DeviceBayTemplate(ComponentTemplateModel): class DeviceBayTemplate(ComponentTemplateModel):
""" """
A template for a DeviceBay to be created for a new parent Device. A template for a DeviceBay to be created for a new parent Device.

View File

@@ -6,19 +6,17 @@ from django.db import models
from django.db.models import Sum from django.db.models import Sum
from django.urls import reverse from django.urls import reverse
from mptt.models import MPTTModel, TreeForeignKey from mptt.models import MPTTModel, TreeForeignKey
from taggit.managers import TaggableManager
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.fields import MACAddressField from dcim.fields import MACAddressField
from extras.models import ObjectChange, TaggedItem
from extras.utils import extras_features from extras.utils import extras_features
from netbox.models import PrimaryModel
from utilities.fields import NaturalOrderingField from utilities.fields import NaturalOrderingField
from utilities.mptt import TreeManager from utilities.mptt import TreeManager
from utilities.ordering import naturalize_interface from utilities.ordering import naturalize_interface
from utilities.querysets import RestrictedQuerySet from utilities.querysets import RestrictedQuerySet
from utilities.query_functions import CollateAsChar from utilities.query_functions import CollateAsChar
from utilities.utils import serialize_object
__all__ = ( __all__ = (
@@ -37,7 +35,7 @@ __all__ = (
) )
class ComponentModel(models.Model): class ComponentModel(PrimaryModel):
""" """
An abstract model inherited by any model which has a parent Device. An abstract model inherited by any model which has a parent Device.
""" """
@@ -81,17 +79,11 @@ class ComponentModel(models.Model):
except ObjectDoesNotExist: except ObjectDoesNotExist:
# The parent Device has already been deleted # The parent Device has already been deleted
device = None device = None
return ObjectChange( return super().to_objectchange(action, related_object=device)
changed_object=self,
object_repr=str(self),
action=action,
related_object=device,
object_data=serialize_object(self)
)
@property @property
def parent(self): def parent_object(self):
return getattr(self, 'device', None) return self.device
class CableTermination(models.Model): class CableTermination(models.Model):
@@ -125,6 +117,10 @@ class CableTermination(models.Model):
ct_field='_cable_peer_type', ct_field='_cable_peer_type',
fk_field='_cable_peer_id' fk_field='_cable_peer_id'
) )
mark_connected = models.BooleanField(
default=False,
help_text="Treat as if a cable is connected"
)
# Generic relations to Cable. These ensure that an attached Cable is deleted if the terminated object is deleted. # Generic relations to Cable. These ensure that an attached Cable is deleted if the terminated object is deleted.
_cabled_as_a = GenericRelation( _cabled_as_a = GenericRelation(
@@ -141,14 +137,30 @@ class CableTermination(models.Model):
class Meta: class Meta:
abstract = True abstract = True
def clean(self):
super().clean()
if self.mark_connected and self.cable_id:
raise ValidationError({
"mark_connected": "Cannot mark as connected with a cable attached."
})
def get_cable_peer(self): def get_cable_peer(self):
return self._cable_peer return self._cable_peer
@property
def _occupied(self):
return bool(self.mark_connected or self.cable_id)
@property
def parent_object(self):
raise NotImplementedError("CableTermination models must implement parent_object()")
class PathEndpoint(models.Model): class PathEndpoint(models.Model):
""" """
An abstract model inherited by any CableTermination subclass which represents the end of a CablePath; specifically, An abstract model inherited by any CableTermination subclass which represents the end of a CablePath; specifically,
these include ConsolePort, ConsoleServerPort, PowerPort, PowerOutlet, Interface, PowerFeed, and CircuitTermination. these include ConsolePort, ConsoleServerPort, PowerPort, PowerOutlet, Interface, and PowerFeed.
`_path` references the CablePath originating from this instance, if any. It is set or cleared by the receivers in `_path` references the CablePath originating from this instance, if any. It is set or cleared by the receivers in
dcim.signals in response to changes in the cable path, and complements the `origin` GenericForeignKey field on the dcim.signals in response to changes in the cable path, and complements the `origin` GenericForeignKey field on the
@@ -172,10 +184,11 @@ class PathEndpoint(models.Model):
# Construct the complete path # Construct the complete path
path = [self, *self._path.get_path()] path = [self, *self._path.get_path()]
while (len(path) + 1) % 3: if self._path.destination:
path.append(self._path.destination)
while len(path) % 3:
# Pad to ensure we have complete three-tuples (e.g. for paths that end at a RearPort) # Pad to ensure we have complete three-tuples (e.g. for paths that end at a RearPort)
path.append(None) path.insert(-1, None)
path.append(self._path.destination)
# Return the path as a list of three-tuples (A termination, cable, B termination) # Return the path as a list of three-tuples (A termination, cable, B termination)
return list(zip(*[iter(path)] * 3)) return list(zip(*[iter(path)] * 3))
@@ -198,8 +211,8 @@ class PathEndpoint(models.Model):
# Console ports # Console ports
# #
@extras_features('export_templates', 'webhooks', 'custom_links') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class ConsolePort(CableTermination, PathEndpoint, ComponentModel): class ConsolePort(ComponentModel, CableTermination, PathEndpoint):
""" """
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts. A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
""" """
@@ -209,9 +222,14 @@ class ConsolePort(CableTermination, PathEndpoint, ComponentModel):
blank=True, blank=True,
help_text='Physical port type' help_text='Physical port type'
) )
tags = TaggableManager(through=TaggedItem) speed = models.PositiveIntegerField(
choices=ConsolePortSpeedChoices,
blank=True,
null=True,
help_text='Port speed in bits per second'
)
csv_headers = ['device', 'name', 'label', 'type', 'description'] csv_headers = ['device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description']
class Meta: class Meta:
ordering = ('device', '_name') ordering = ('device', '_name')
@@ -226,6 +244,8 @@ class ConsolePort(CableTermination, PathEndpoint, ComponentModel):
self.name, self.name,
self.label, self.label,
self.type, self.type,
self.speed,
self.mark_connected,
self.description, self.description,
) )
@@ -234,8 +254,8 @@ class ConsolePort(CableTermination, PathEndpoint, ComponentModel):
# Console server ports # Console server ports
# #
@extras_features('webhooks', 'custom_links') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class ConsoleServerPort(CableTermination, PathEndpoint, ComponentModel): class ConsoleServerPort(ComponentModel, CableTermination, PathEndpoint):
""" """
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts. A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
""" """
@@ -245,9 +265,14 @@ class ConsoleServerPort(CableTermination, PathEndpoint, ComponentModel):
blank=True, blank=True,
help_text='Physical port type' help_text='Physical port type'
) )
tags = TaggableManager(through=TaggedItem) speed = models.PositiveIntegerField(
choices=ConsolePortSpeedChoices,
blank=True,
null=True,
help_text='Port speed in bits per second'
)
csv_headers = ['device', 'name', 'label', 'type', 'description'] csv_headers = ['device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description']
class Meta: class Meta:
ordering = ('device', '_name') ordering = ('device', '_name')
@@ -262,6 +287,8 @@ class ConsoleServerPort(CableTermination, PathEndpoint, ComponentModel):
self.name, self.name,
self.label, self.label,
self.type, self.type,
self.speed,
self.mark_connected,
self.description, self.description,
) )
@@ -270,8 +297,8 @@ class ConsoleServerPort(CableTermination, PathEndpoint, ComponentModel):
# Power ports # Power ports
# #
@extras_features('export_templates', 'webhooks', 'custom_links') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class PowerPort(CableTermination, PathEndpoint, ComponentModel): class PowerPort(ComponentModel, CableTermination, PathEndpoint):
""" """
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets. A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
""" """
@@ -293,9 +320,10 @@ class PowerPort(CableTermination, PathEndpoint, ComponentModel):
validators=[MinValueValidator(1)], validators=[MinValueValidator(1)],
help_text="Allocated power draw (watts)" help_text="Allocated power draw (watts)"
) )
tags = TaggableManager(through=TaggedItem)
csv_headers = ['device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description'] csv_headers = [
'device', 'name', 'label', 'type', 'mark_connected', 'maximum_draw', 'allocated_draw', 'description',
]
class Meta: class Meta:
ordering = ('device', '_name') ordering = ('device', '_name')
@@ -310,6 +338,7 @@ class PowerPort(CableTermination, PathEndpoint, ComponentModel):
self.name, self.name,
self.label, self.label,
self.get_type_display(), self.get_type_display(),
self.mark_connected,
self.maximum_draw, self.maximum_draw,
self.allocated_draw, self.allocated_draw,
self.description, self.description,
@@ -379,8 +408,8 @@ class PowerPort(CableTermination, PathEndpoint, ComponentModel):
# Power outlets # Power outlets
# #
@extras_features('webhooks', 'custom_links') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class PowerOutlet(CableTermination, PathEndpoint, ComponentModel): class PowerOutlet(ComponentModel, CableTermination, PathEndpoint):
""" """
A physical power outlet (output) within a Device which provides power to a PowerPort. A physical power outlet (output) within a Device which provides power to a PowerPort.
""" """
@@ -403,9 +432,8 @@ class PowerOutlet(CableTermination, PathEndpoint, ComponentModel):
blank=True, blank=True,
help_text="Phase (for three-phase feeds)" help_text="Phase (for three-phase feeds)"
) )
tags = TaggableManager(through=TaggedItem)
csv_headers = ['device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description'] csv_headers = ['device', 'name', 'label', 'type', 'mark_connected', 'power_port', 'feed_leg', 'description']
class Meta: class Meta:
ordering = ('device', '_name') ordering = ('device', '_name')
@@ -420,6 +448,7 @@ class PowerOutlet(CableTermination, PathEndpoint, ComponentModel):
self.name, self.name,
self.label, self.label,
self.get_type_display(), self.get_type_display(),
self.mark_connected,
self.power_port.name if self.power_port else None, self.power_port.name if self.power_port else None,
self.get_feed_leg_display(), self.get_feed_leg_display(),
self.description, self.description,
@@ -478,9 +507,13 @@ class BaseInterface(models.Model):
return super().save(*args, **kwargs) return super().save(*args, **kwargs)
@property
def count_ipaddresses(self):
return self.ip_addresses.count()
@extras_features('export_templates', 'webhooks', 'custom_links')
class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface): @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
""" """
A network interface within a Device. A physical Interface can connect to exactly one other Interface. A network interface within a Device. A physical Interface can connect to exactly one other Interface.
""" """
@@ -491,6 +524,14 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
max_length=100, max_length=100,
blank=True blank=True
) )
parent = models.ForeignKey(
to='self',
on_delete=models.SET_NULL,
related_name='child_interfaces',
null=True,
blank=True,
verbose_name='Parent interface'
)
lag = models.ForeignKey( lag = models.ForeignKey(
to='self', to='self',
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
@@ -528,10 +569,10 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
object_id_field='assigned_object_id', object_id_field='assigned_object_id',
related_query_name='interface' related_query_name='interface'
) )
tags = TaggableManager(through=TaggedItem)
csv_headers = [ csv_headers = [
'device', 'name', 'label', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only', 'description', 'mode', 'device', 'name', 'label', 'parent', 'lag', 'type', 'enabled', 'mark_connected', 'mac_address', 'mtu',
'mgmt_only', 'description', 'mode',
] ]
class Meta: class Meta:
@@ -546,9 +587,11 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
self.device.identifier if self.device else None, self.device.identifier if self.device else None,
self.name, self.name,
self.label, self.label,
self.parent.name if self.parent else None,
self.lag.name if self.lag else None, self.lag.name if self.lag else None,
self.get_type_display(), self.get_type_display(),
self.enabled, self.enabled,
self.mark_connected,
self.mac_address, self.mac_address,
self.mtu, self.mtu,
self.mgmt_only, self.mgmt_only,
@@ -560,14 +603,38 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
super().clean() super().clean()
# Virtual interfaces cannot be connected # Virtual interfaces cannot be connected
if self.type in NONCONNECTABLE_IFACE_TYPES and ( if not self.is_connectable and self.cable:
self.cable or getattr(self, 'circuit_termination', False)
):
raise ValidationError({ raise ValidationError({
'type': "Virtual and wireless interfaces cannot be connected to another interface or circuit. " 'type': f"{self.get_type_display()} interfaces cannot have a cable attached."
"Disconnect the interface or choose a suitable type."
}) })
# Non-connectable interfaces cannot be marked as connected
if not self.is_connectable and self.mark_connected:
raise ValidationError({
'mark_connected': f"{self.get_type_display()} interfaces cannot be marked as connected."
})
# An interface's parent must belong to the same device or virtual chassis
if self.parent and self.parent.device != self.device:
if self.device.virtual_chassis is None:
raise ValidationError({
'parent': f"The selected parent interface ({self.parent}) belongs to a different device "
f"({self.parent.device})."
})
elif self.parent.device.virtual_chassis != self.parent.virtual_chassis:
raise ValidationError({
'parent': f"The selected parent interface ({self.parent}) belongs to {self.parent.device}, which "
f"is not part of virtual chassis {self.device.virtual_chassis}."
})
# An interface cannot be its own parent
if self.pk and self.parent_id == self.pk:
raise ValidationError({'parent': "An interface cannot be its own parent."})
# A physical interface cannot have a parent interface
if self.type != InterfaceTypeChoices.TYPE_VIRTUAL and self.parent is not None:
raise ValidationError({'parent': "Only virtual interfaces may be assigned to a parent interface."})
# An interface's LAG must belong to the same device or virtual chassis # An interface's LAG must belong to the same device or virtual chassis
if self.lag and self.lag.device != self.device: if self.lag and self.lag.device != self.device:
if self.device.virtual_chassis is None: if self.device.virtual_chassis is None:
@@ -589,16 +656,12 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
raise ValidationError({'lag': "A LAG interface cannot be its own parent."}) raise ValidationError({'lag': "A LAG interface cannot be its own parent."})
# Validate untagged VLAN # Validate untagged VLAN
if self.untagged_vlan and self.untagged_vlan.site not in [self.parent.site, None]: if self.untagged_vlan and self.untagged_vlan.site not in [self.device.site, None]:
raise ValidationError({ raise ValidationError({
'untagged_vlan': "The untagged VLAN ({}) must belong to the same site as the interface's parent " 'untagged_vlan': "The untagged VLAN ({}) must belong to the same site as the interface's parent "
"device, or it must be global".format(self.untagged_vlan) "device, or it must be global".format(self.untagged_vlan)
}) })
@property
def parent(self):
return self.device
@property @property
def is_connectable(self): def is_connectable(self):
return self.type not in NONCONNECTABLE_IFACE_TYPES return self.type not in NONCONNECTABLE_IFACE_TYPES
@@ -615,17 +678,13 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
def is_lag(self): def is_lag(self):
return self.type == InterfaceTypeChoices.TYPE_LAG return self.type == InterfaceTypeChoices.TYPE_LAG
@property
def count_ipaddresses(self):
return self.ip_addresses.count()
# #
# Pass-through ports # Pass-through ports
# #
@extras_features('webhooks', 'custom_links') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class FrontPort(CableTermination, ComponentModel): class FrontPort(ComponentModel, CableTermination):
""" """
A pass-through port on the front of a Device. A pass-through port on the front of a Device.
""" """
@@ -645,9 +704,10 @@ class FrontPort(CableTermination, ComponentModel):
MaxValueValidator(REARPORT_POSITIONS_MAX) MaxValueValidator(REARPORT_POSITIONS_MAX)
] ]
) )
tags = TaggableManager(through=TaggedItem)
csv_headers = ['device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description'] csv_headers = [
'device', 'name', 'label', 'type', 'mark_connected', 'rear_port', 'rear_port_position', 'description',
]
class Meta: class Meta:
ordering = ('device', '_name') ordering = ('device', '_name')
@@ -665,6 +725,7 @@ class FrontPort(CableTermination, ComponentModel):
self.name, self.name,
self.label, self.label,
self.get_type_display(), self.get_type_display(),
self.mark_connected,
self.rear_port.name, self.rear_port.name,
self.rear_port_position, self.rear_port_position,
self.description, self.description,
@@ -687,8 +748,8 @@ class FrontPort(CableTermination, ComponentModel):
}) })
@extras_features('webhooks', 'custom_links') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class RearPort(CableTermination, ComponentModel): class RearPort(ComponentModel, CableTermination):
""" """
A pass-through port on the rear of a Device. A pass-through port on the rear of a Device.
""" """
@@ -703,9 +764,8 @@ class RearPort(CableTermination, ComponentModel):
MaxValueValidator(REARPORT_POSITIONS_MAX) MaxValueValidator(REARPORT_POSITIONS_MAX)
] ]
) )
tags = TaggableManager(through=TaggedItem)
csv_headers = ['device', 'name', 'label', 'type', 'positions', 'description'] csv_headers = ['device', 'name', 'label', 'type', 'mark_connected', 'positions', 'description']
class Meta: class Meta:
ordering = ('device', '_name') ordering = ('device', '_name')
@@ -731,6 +791,7 @@ class RearPort(CableTermination, ComponentModel):
self.name, self.name,
self.label, self.label,
self.get_type_display(), self.get_type_display(),
self.mark_connected,
self.positions, self.positions,
self.description, self.description,
) )
@@ -740,7 +801,7 @@ class RearPort(CableTermination, ComponentModel):
# Device bays # Device bays
# #
@extras_features('webhooks', 'custom_links') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class DeviceBay(ComponentModel): class DeviceBay(ComponentModel):
""" """
An empty space within a Device which can house a child device An empty space within a Device which can house a child device
@@ -752,7 +813,6 @@ class DeviceBay(ComponentModel):
blank=True, blank=True,
null=True null=True
) )
tags = TaggableManager(through=TaggedItem)
csv_headers = ['device', 'name', 'label', 'installed_device', 'description'] csv_headers = ['device', 'name', 'label', 'installed_device', 'description']
@@ -800,7 +860,7 @@ class DeviceBay(ComponentModel):
# Inventory items # Inventory items
# #
@extras_features('export_templates', 'webhooks', 'custom_links') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class InventoryItem(MPTTModel, ComponentModel): class InventoryItem(MPTTModel, ComponentModel):
""" """
An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply. An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
@@ -845,8 +905,6 @@ class InventoryItem(MPTTModel, ComponentModel):
help_text='This item was automatically discovered' help_text='This item was automatically discovered'
) )
tags = TaggableManager(through=TaggedItem)
objects = TreeManager() objects = TreeManager()
csv_headers = [ csv_headers = [

View File

@@ -9,13 +9,13 @@ from django.db import models
from django.db.models import F, ProtectedError from django.db.models import F, ProtectedError
from django.urls import reverse from django.urls import reverse
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from taggit.managers import TaggableManager
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from extras.models import ChangeLoggedModel, ConfigContextModel, CustomFieldModel, TaggedItem from extras.models import ConfigContextModel
from extras.querysets import ConfigContextModelQuerySet from extras.querysets import ConfigContextModelQuerySet
from extras.utils import extras_features from extras.utils import extras_features
from netbox.models import OrganizationalModel, PrimaryModel
from utilities.choices import ColorChoices from utilities.choices import ColorChoices
from utilities.fields import ColorField, NaturalOrderingField from utilities.fields import ColorField, NaturalOrderingField
from utilities.querysets import RestrictedQuerySet from utilities.querysets import RestrictedQuerySet
@@ -36,8 +36,8 @@ __all__ = (
# Device Types # Device Types
# #
@extras_features('export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Manufacturer(ChangeLoggedModel): class Manufacturer(OrganizationalModel):
""" """
A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell. A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
""" """
@@ -65,7 +65,7 @@ class Manufacturer(ChangeLoggedModel):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
return "{}?manufacturer={}".format(reverse('dcim:devicetype_list'), self.slug) return reverse('dcim:manufacturer', args=[self.pk])
def to_csv(self): def to_csv(self):
return ( return (
@@ -75,8 +75,8 @@ class Manufacturer(ChangeLoggedModel):
) )
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class DeviceType(ChangeLoggedModel, CustomFieldModel): class DeviceType(PrimaryModel):
""" """
A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as
well as high-level functional role(s). well as high-level functional role(s).
@@ -135,7 +135,6 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
comments = models.TextField( comments = models.TextField(
blank=True blank=True
) )
tags = TaggableManager(through=TaggedItem)
objects = RestrictedQuerySet.as_manager() objects = RestrictedQuerySet.as_manager()
@@ -184,6 +183,8 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
{ {
'name': c.name, 'name': c.name,
'type': c.type, 'type': c.type,
'label': c.label,
'description': c.description,
} }
for c in self.consoleporttemplates.all() for c in self.consoleporttemplates.all()
] ]
@@ -192,6 +193,8 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
{ {
'name': c.name, 'name': c.name,
'type': c.type, 'type': c.type,
'label': c.label,
'description': c.description,
} }
for c in self.consoleserverporttemplates.all() for c in self.consoleserverporttemplates.all()
] ]
@@ -202,6 +205,8 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
'type': c.type, 'type': c.type,
'maximum_draw': c.maximum_draw, 'maximum_draw': c.maximum_draw,
'allocated_draw': c.allocated_draw, 'allocated_draw': c.allocated_draw,
'label': c.label,
'description': c.description,
} }
for c in self.powerporttemplates.all() for c in self.powerporttemplates.all()
] ]
@@ -212,6 +217,8 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
'type': c.type, 'type': c.type,
'power_port': c.power_port.name if c.power_port else None, 'power_port': c.power_port.name if c.power_port else None,
'feed_leg': c.feed_leg, 'feed_leg': c.feed_leg,
'label': c.label,
'description': c.description,
} }
for c in self.poweroutlettemplates.all() for c in self.poweroutlettemplates.all()
] ]
@@ -221,6 +228,8 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
'name': c.name, 'name': c.name,
'type': c.type, 'type': c.type,
'mgmt_only': c.mgmt_only, 'mgmt_only': c.mgmt_only,
'label': c.label,
'description': c.description,
} }
for c in self.interfacetemplates.all() for c in self.interfacetemplates.all()
] ]
@@ -231,6 +240,8 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
'type': c.type, 'type': c.type,
'rear_port': c.rear_port.name, 'rear_port': c.rear_port.name,
'rear_port_position': c.rear_port_position, 'rear_port_position': c.rear_port_position,
'label': c.label,
'description': c.description,
} }
for c in self.frontporttemplates.all() for c in self.frontporttemplates.all()
] ]
@@ -240,6 +251,8 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
'name': c.name, 'name': c.name,
'type': c.type, 'type': c.type,
'positions': c.positions, 'positions': c.positions,
'label': c.label,
'description': c.description,
} }
for c in self.rearporttemplates.all() for c in self.rearporttemplates.all()
] ]
@@ -247,6 +260,8 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
data['device-bays'] = [ data['device-bays'] = [
{ {
'name': c.name, 'name': c.name,
'label': c.label,
'description': c.description,
} }
for c in self.devicebaytemplates.all() for c in self.devicebaytemplates.all()
] ]
@@ -338,7 +353,8 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
# Devices # Devices
# #
class DeviceRole(ChangeLoggedModel): @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class DeviceRole(OrganizationalModel):
""" """
Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a 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. The vm_role field determines whether the role is applicable to color to be used when displaying rack elevations. The vm_role field determines whether the role is applicable to
@@ -375,6 +391,9 @@ class DeviceRole(ChangeLoggedModel):
def __str__(self): def __str__(self):
return self.name return self.name
def get_absolute_url(self):
return reverse('dcim:devicerole', args=[self.pk])
def to_csv(self): def to_csv(self):
return ( return (
self.name, self.name,
@@ -385,7 +404,8 @@ class DeviceRole(ChangeLoggedModel):
) )
class Platform(ChangeLoggedModel): @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Platform(OrganizationalModel):
""" """
Platform refers to the software or firmware running on a Device. For example, "Cisco IOS-XR" or "Juniper Junos". Platform refers to the software or firmware running on a Device. For example, "Cisco IOS-XR" or "Juniper Junos".
NetBox uses Platforms to determine how to interact with devices when pulling inventory data or other information by NetBox uses Platforms to determine how to interact with devices when pulling inventory data or other information by
@@ -435,7 +455,7 @@ class Platform(ChangeLoggedModel):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
return "{}?platform={}".format(reverse('dcim:device_list'), self.slug) return reverse('dcim:platform', args=[self.pk])
def to_csv(self): def to_csv(self):
return ( return (
@@ -448,8 +468,8 @@ class Platform(ChangeLoggedModel):
) )
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): class Device(PrimaryModel, ConfigContextModel):
""" """
A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType, 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. DeviceRole, and (optionally) a Platform. Device names are not required, however if one is set it must be unique.
@@ -514,6 +534,13 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='devices' related_name='devices'
) )
location = models.ForeignKey(
to='dcim.Location',
on_delete=models.PROTECT,
related_name='devices',
blank=True,
null=True
)
rack = models.ForeignKey( rack = models.ForeignKey(
to='dcim.Rack', to='dcim.Rack',
on_delete=models.PROTECT, on_delete=models.PROTECT,
@@ -591,16 +618,15 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
object_id_field='assigned_object_id', object_id_field='assigned_object_id',
related_query_name='device' related_query_name='device'
) )
tags = TaggableManager(through=TaggedItem)
objects = ConfigContextModelQuerySet.as_manager() objects = ConfigContextModelQuerySet.as_manager()
csv_headers = [ csv_headers = [
'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status', 'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
'site', 'rack_group', 'rack_name', 'position', 'face', 'comments', 'site', 'location', 'rack_name', 'position', 'face', 'comments',
] ]
clone_fields = [ clone_fields = [
'device_type', 'device_role', 'tenant', 'platform', 'site', 'rack', 'status', 'cluster', 'device_type', 'device_role', 'tenant', 'platform', 'site', 'location', 'rack', 'status', 'cluster',
] ]
class Meta: class Meta:
@@ -637,11 +663,21 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
def clean(self): def clean(self):
super().clean() super().clean()
# Validate site/rack combination # Validate site/location/rack combination
if self.rack and self.site != self.rack.site: if self.rack and self.site != self.rack.site:
raise ValidationError({ raise ValidationError({
'rack': f"Rack {self.rack} does not belong to site {self.site}.", 'rack': f"Rack {self.rack} does not belong to site {self.site}.",
}) })
if self.location and self.site != self.location.site:
raise ValidationError({
'location': f"Location {self.location} does not belong to site {self.site}.",
})
if self.rack and self.location and self.rack.location != self.location:
raise ValidationError({
'rack': f"Rack {self.rack} does not belong to location {self.location}.",
})
elif self.rack:
self.location = self.rack.location
if self.rack is None: if self.rack is None:
if self.face: if self.face:
@@ -696,7 +732,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
pass pass
# Validate primary IP addresses # Validate primary IP addresses
vc_interfaces = self.vc_interfaces.all() vc_interfaces = self.vc_interfaces(if_master=False)
if self.primary_ip4: if self.primary_ip4:
if self.primary_ip4.family != 4: if self.primary_ip4.family != 4:
raise ValidationError({ raise ValidationError({
@@ -796,7 +832,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
self.asset_tag, self.asset_tag,
self.get_status_display(), self.get_status_display(),
self.site.name, self.site.name,
self.rack.group.name if self.rack and self.rack.group else None, self.rack.location.name if self.rack and self.rack.location else None,
self.rack.name if self.rack else None, self.rack.name if self.rack else None,
self.position, self.position,
self.get_face_display(), self.get_face_display(),
@@ -834,20 +870,25 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
else: else:
return None return None
@property
def interfaces_count(self):
return self.vc_interfaces().count()
def get_vc_master(self): def get_vc_master(self):
""" """
If this Device is a VirtualChassis member, return the VC master. Otherwise, return None. If this Device is a VirtualChassis member, return the VC master. Otherwise, return None.
""" """
return self.virtual_chassis.master if self.virtual_chassis else None return self.virtual_chassis.master if self.virtual_chassis else None
@property def vc_interfaces(self, if_master=True):
def vc_interfaces(self):
""" """
Return a QuerySet matching all Interfaces assigned to this Device or, if this Device is a VC master, to another Return a QuerySet matching all Interfaces assigned to this Device or, if this Device is a VC master, to another
Device belonging to the same VirtualChassis. Device belonging to the same VirtualChassis.
:param if_master: If True, return VC member interfaces only if this Device is the VC master.
""" """
filter = Q(device=self) filter = Q(device=self)
if self.virtual_chassis and self.virtual_chassis.master == self: if self.virtual_chassis and (self.virtual_chassis.master == self or not if_master):
filter |= Q(device__virtual_chassis=self.virtual_chassis, mgmt_only=False) filter |= Q(device__virtual_chassis=self.virtual_chassis, mgmt_only=False)
return Interface.objects.filter(filter) return Interface.objects.filter(filter)
@@ -881,8 +922,8 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
# Virtual chassis # Virtual chassis
# #
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class VirtualChassis(ChangeLoggedModel, CustomFieldModel): class VirtualChassis(PrimaryModel):
""" """
A collection of Devices which operate with a shared control plane (e.g. a switch stack). A collection of Devices which operate with a shared control plane (e.g. a switch stack).
""" """
@@ -900,7 +941,6 @@ class VirtualChassis(ChangeLoggedModel, CustomFieldModel):
max_length=30, max_length=30,
blank=True blank=True
) )
tags = TaggableManager(through=TaggedItem)
objects = RestrictedQuerySet.as_manager() objects = RestrictedQuerySet.as_manager()

View File

@@ -2,12 +2,11 @@ from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from taggit.managers import TaggableManager
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from extras.models import ChangeLoggedModel, CustomFieldModel, TaggedItem
from extras.utils import extras_features from extras.utils import extras_features
from netbox.models import PrimaryModel
from utilities.querysets import RestrictedQuerySet from utilities.querysets import RestrictedQuerySet
from utilities.validators import ExclusionValidator from utilities.validators import ExclusionValidator
from .device_components import CableTermination, PathEndpoint from .device_components import CableTermination, PathEndpoint
@@ -22,8 +21,8 @@ __all__ = (
# Power # Power
# #
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class PowerPanel(ChangeLoggedModel, CustomFieldModel): class PowerPanel(PrimaryModel):
""" """
A distribution point for electrical power; e.g. a data center RPP. A distribution point for electrical power; e.g. a data center RPP.
""" """
@@ -31,8 +30,8 @@ class PowerPanel(ChangeLoggedModel, CustomFieldModel):
to='Site', to='Site',
on_delete=models.PROTECT on_delete=models.PROTECT
) )
rack_group = models.ForeignKey( location = models.ForeignKey(
to='RackGroup', to='dcim.Location',
on_delete=models.PROTECT, on_delete=models.PROTECT,
blank=True, blank=True,
null=True null=True
@@ -40,11 +39,10 @@ class PowerPanel(ChangeLoggedModel, CustomFieldModel):
name = models.CharField( name = models.CharField(
max_length=100 max_length=100
) )
tags = TaggableManager(through=TaggedItem)
objects = RestrictedQuerySet.as_manager() objects = RestrictedQuerySet.as_manager()
csv_headers = ['site', 'rack_group', 'name'] csv_headers = ['site', 'location', 'name']
class Meta: class Meta:
ordering = ['site', 'name'] ordering = ['site', 'name']
@@ -59,22 +57,22 @@ class PowerPanel(ChangeLoggedModel, CustomFieldModel):
def to_csv(self): def to_csv(self):
return ( return (
self.site.name, self.site.name,
self.rack_group.name if self.rack_group else None, self.location.name if self.location else None,
self.name, self.name,
) )
def clean(self): def clean(self):
super().clean() super().clean()
# RackGroup must belong to assigned Site # Location must belong to assigned Site
if self.rack_group and self.rack_group.site != self.site: if self.location and self.location.site != self.site:
raise ValidationError("Rack group {} ({}) is in a different site than {}".format( raise ValidationError(
self.rack_group, self.rack_group.site, self.site f"Location {self.location} ({self.location.site}) is in a different site than {self.site}"
)) )
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class PowerFeed(ChangeLoggedModel, PathEndpoint, CableTermination, CustomFieldModel): class PowerFeed(PrimaryModel, PathEndpoint, CableTermination):
""" """
An electrical circuit delivered from a PowerPanel. An electrical circuit delivered from a PowerPanel.
""" """
@@ -132,17 +130,16 @@ class PowerFeed(ChangeLoggedModel, PathEndpoint, CableTermination, CustomFieldMo
comments = models.TextField( comments = models.TextField(
blank=True blank=True
) )
tags = TaggableManager(through=TaggedItem)
objects = RestrictedQuerySet.as_manager() objects = RestrictedQuerySet.as_manager()
csv_headers = [ csv_headers = [
'site', 'power_panel', 'rack_group', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase',
'amperage', 'max_utilization', 'comments', 'voltage', 'amperage', 'max_utilization', 'comments',
] ]
clone_fields = [ clone_fields = [
'power_panel', 'rack', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', 'power_panel', 'rack', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage',
'available_power', 'max_utilization', 'available_power',
] ]
class Meta: class Meta:
@@ -159,11 +156,12 @@ class PowerFeed(ChangeLoggedModel, PathEndpoint, CableTermination, CustomFieldMo
return ( return (
self.power_panel.site.name, self.power_panel.site.name,
self.power_panel.name, self.power_panel.name,
self.rack.group.name if self.rack and self.rack.group else None, self.rack.location.name if self.rack and self.rack.location else None,
self.rack.name if self.rack else None, self.rack.name if self.rack else None,
self.name, self.name,
self.get_status_display(), self.get_status_display(),
self.get_type_display(), self.get_type_display(),
self.mark_connected,
self.get_supply_display(), self.get_supply_display(),
self.get_phase_display(), self.get_phase_display(),
self.voltage, self.voltage,
@@ -199,7 +197,7 @@ class PowerFeed(ChangeLoggedModel, PathEndpoint, CableTermination, CustomFieldMo
super().save(*args, **kwargs) super().save(*args, **kwargs)
@property @property
def parent(self): def parent_object(self):
return self.power_panel return self.power_panel
def get_type_class(self): def get_type_class(self):

View File

@@ -10,26 +10,22 @@ from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.db.models import Count, Sum from django.db.models import Count, Sum
from django.urls import reverse from django.urls import reverse
from mptt.models import MPTTModel, TreeForeignKey
from taggit.managers import TaggableManager
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.elevations import RackElevationSVG from dcim.elevations import RackElevationSVG
from extras.models import ChangeLoggedModel, CustomFieldModel, ObjectChange, TaggedItem
from extras.utils import extras_features from extras.utils import extras_features
from netbox.models import OrganizationalModel, PrimaryModel
from utilities.choices import ColorChoices from utilities.choices import ColorChoices
from utilities.fields import ColorField, NaturalOrderingField from utilities.fields import ColorField, NaturalOrderingField
from utilities.querysets import RestrictedQuerySet from utilities.querysets import RestrictedQuerySet
from utilities.mptt import TreeManager from utilities.utils import array_to_string
from utilities.utils import array_to_string, serialize_object
from .device_components import PowerOutlet, PowerPort from .device_components import PowerOutlet, PowerPort
from .devices import Device from .devices import Device
from .power import PowerFeed from .power import PowerFeed
__all__ = ( __all__ = (
'Rack', 'Rack',
'RackGroup',
'RackReservation', 'RackReservation',
'RackRole', 'RackRole',
) )
@@ -39,84 +35,8 @@ __all__ = (
# Racks # Racks
# #
@extras_features('export_templates') @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class RackGroup(MPTTModel, ChangeLoggedModel): class RackRole(OrganizationalModel):
"""
Racks can be grouped as subsets within a Site. The scope of a group will depend on how Sites are defined. For
example, if a Site spans a corporate campus, a RackGroup might be defined to represent each building within that
campus. If a Site instead represents a single building, a RackGroup might represent a single room or floor.
"""
name = models.CharField(
max_length=100
)
slug = models.SlugField(
max_length=100
)
site = models.ForeignKey(
to='dcim.Site',
on_delete=models.CASCADE,
related_name='rack_groups'
)
parent = TreeForeignKey(
to='self',
on_delete=models.CASCADE,
related_name='children',
blank=True,
null=True,
db_index=True
)
description = models.CharField(
max_length=200,
blank=True
)
objects = TreeManager()
csv_headers = ['site', 'parent', 'name', 'slug', 'description']
class Meta:
ordering = ['site', 'name']
unique_together = [
['site', 'name'],
['site', 'slug'],
]
class MPTTMeta:
order_insertion_by = ['name']
def __str__(self):
return self.name
def get_absolute_url(self):
return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk)
def to_csv(self):
return (
self.site,
self.parent.name if self.parent else '',
self.name,
self.slug,
self.description,
)
def to_objectchange(self, action):
# Remove MPTT-internal fields
return ObjectChange(
changed_object=self,
object_repr=str(self),
action=action,
object_data=serialize_object(self, exclude=['level', 'lft', 'rght', 'tree_id'])
)
def clean(self):
super().clean()
# Parent RackGroup (if any) must belong to the same Site
if self.parent and self.parent.site != self.site:
raise ValidationError(f"Parent rack group ({self.parent}) must belong to the same site ({self.site})")
class RackRole(ChangeLoggedModel):
""" """
Racks can be organized by functional role, similar to Devices. Racks can be organized by functional role, similar to Devices.
""" """
@@ -147,7 +67,7 @@ class RackRole(ChangeLoggedModel):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
return "{}?role={}".format(reverse('dcim:rack_list'), self.slug) return reverse('dcim:rackrole', args=[self.pk])
def to_csv(self): def to_csv(self):
return ( return (
@@ -158,11 +78,11 @@ class RackRole(ChangeLoggedModel):
) )
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Rack(ChangeLoggedModel, CustomFieldModel): class Rack(PrimaryModel):
""" """
Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face. Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
Each Rack is assigned to a Site and (optionally) a RackGroup. Each Rack is assigned to a Site and (optionally) a Location.
""" """
name = models.CharField( name = models.CharField(
max_length=100 max_length=100
@@ -184,13 +104,12 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='racks' related_name='racks'
) )
group = models.ForeignKey( location = models.ForeignKey(
to='dcim.RackGroup', to='dcim.Location',
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
related_name='racks', related_name='racks',
blank=True, blank=True,
null=True, null=True
help_text='Assigned group'
) )
tenant = models.ForeignKey( tenant = models.ForeignKey(
to='tenancy.Tenant', to='tenancy.Tenant',
@@ -269,25 +188,24 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
images = GenericRelation( images = GenericRelation(
to='extras.ImageAttachment' to='extras.ImageAttachment'
) )
tags = TaggableManager(through=TaggedItem)
objects = RestrictedQuerySet.as_manager() objects = RestrictedQuerySet.as_manager()
csv_headers = [ csv_headers = [
'site', 'group', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag', 'width', 'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag', 'width',
'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments',
] ]
clone_fields = [ clone_fields = [
'site', 'group', 'tenant', 'status', 'role', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'site', 'location', 'tenant', 'status', 'role', 'type', 'width', 'u_height', 'desc_units', 'outer_width',
'outer_depth', 'outer_unit', 'outer_depth', 'outer_unit',
] ]
class Meta: class Meta:
ordering = ('site', 'group', '_name', 'pk') # (site, group, name) may be non-unique ordering = ('site', 'location', '_name', 'pk') # (site, location, name) may be non-unique
unique_together = ( unique_together = (
# Name and facility_id must be unique *only* within a RackGroup # Name and facility_id must be unique *only* within a Location
('group', 'name'), ('location', 'name'),
('group', 'facility_id'), ('location', 'facility_id'),
) )
def __str__(self): def __str__(self):
@@ -299,9 +217,9 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
def clean(self): def clean(self):
super().clean() super().clean()
# Validate group/site assignment # Validate location/site assignment
if self.site and self.group and self.group.site != self.site: if self.site and self.location and self.location.site != self.site:
raise ValidationError(f"Assigned rack group must belong to parent site ({self.site}).") raise ValidationError(f"Assigned location must belong to parent site ({self.site}).")
# Validate outer dimensions and unit # Validate outer dimensions and unit
if (self.outer_width is not None or self.outer_depth is not None) and not self.outer_unit: if (self.outer_width is not None or self.outer_depth is not None) and not self.outer_unit:
@@ -324,17 +242,17 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
min_height min_height
) )
}) })
# Validate that Rack was assigned a group of its same site, if applicable # Validate that Rack was assigned a Location of its same site, if applicable
if self.group: if self.location:
if self.group.site != self.site: if self.location.site != self.site:
raise ValidationError({ raise ValidationError({
'group': "Rack group must be from the same site, {}.".format(self.site) 'location': f"Location must be from the same site, {self.site}."
}) })
def to_csv(self): def to_csv(self):
return ( return (
self.site.name, self.site.name,
self.group.name if self.group else None, self.location.name if self.location else None,
self.name, self.name,
self.facility_id, self.facility_id,
self.tenant.name if self.tenant else None, self.tenant.name if self.tenant else None,
@@ -549,8 +467,8 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
return int(allocated_draw_total / available_power_total * 100) return int(allocated_draw_total / available_power_total * 100)
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class RackReservation(ChangeLoggedModel, CustomFieldModel): class RackReservation(PrimaryModel):
""" """
One or more reserved units within a Rack. One or more reserved units within a Rack.
""" """
@@ -576,11 +494,10 @@ class RackReservation(ChangeLoggedModel, CustomFieldModel):
description = models.CharField( description = models.CharField(
max_length=200 max_length=200
) )
tags = TaggableManager(through=TaggedItem)
objects = RestrictedQuerySet.as_manager() objects = RestrictedQuerySet.as_manager()
csv_headers = ['site', 'rack_group', 'rack', 'units', 'tenant', 'user', 'description'] csv_headers = ['site', 'location', 'rack', 'units', 'tenant', 'user', 'description']
class Meta: class Meta:
ordering = ['created', 'pk'] ordering = ['created', 'pk']
@@ -621,7 +538,7 @@ class RackReservation(ChangeLoggedModel, CustomFieldModel):
def to_csv(self): def to_csv(self):
return ( return (
self.rack.site.name, self.rack.site.name,
self.rack.group if self.rack.group else None, self.rack.location if self.rack.location else None,
self.rack.name, self.rack.name,
','.join([str(u) for u in self.units]), ','.join([str(u) for u in self.units]),
self.tenant.name if self.tenant else None, self.tenant.name if self.tenant else None,

View File

@@ -1,23 +1,24 @@
from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from mptt.models import MPTTModel, TreeForeignKey from mptt.models import TreeForeignKey
from taggit.managers import TaggableManager
from timezone_field import TimeZoneField from timezone_field import TimeZoneField
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from django.core.exceptions import ValidationError
from dcim.fields import ASNField from dcim.fields import ASNField
from extras.models import ChangeLoggedModel, CustomFieldModel, ObjectChange, TaggedItem
from extras.utils import extras_features from extras.utils import extras_features
from netbox.models import NestedGroupModel, PrimaryModel
from utilities.fields import NaturalOrderingField from utilities.fields import NaturalOrderingField
from utilities.querysets import RestrictedQuerySet from utilities.querysets import RestrictedQuerySet
from utilities.mptt import TreeManager
from utilities.utils import serialize_object
__all__ = ( __all__ = (
'Location',
'Region', 'Region',
'Site', 'Site',
'SiteGroup',
) )
@@ -25,10 +26,12 @@ __all__ = (
# Regions # Regions
# #
@extras_features('export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Region(MPTTModel, ChangeLoggedModel): class Region(NestedGroupModel):
""" """
Sites can be grouped within geographic Regions. A region represents a geographic collection of sites. For example, you might create regions representing countries,
states, and/or cities. Regions are recursively nested into a hierarchy: all sites belonging to a child region are
also considered to be members of its parent and ancestor region(s).
""" """
parent = TreeForeignKey( parent = TreeForeignKey(
to='self', to='self',
@@ -51,18 +54,10 @@ class Region(MPTTModel, ChangeLoggedModel):
blank=True blank=True
) )
objects = TreeManager()
csv_headers = ['name', 'slug', 'parent', 'description'] csv_headers = ['name', 'slug', 'parent', 'description']
class MPTTMeta:
order_insertion_by = ['name']
def __str__(self):
return self.name
def get_absolute_url(self): def get_absolute_url(self):
return "{}?region={}".format(reverse('dcim:site_list'), self.slug) return reverse('dcim:region', args=[self.pk])
def to_csv(self): def to_csv(self):
return ( return (
@@ -78,22 +73,65 @@ class Region(MPTTModel, ChangeLoggedModel):
Q(region__in=self.get_descendants()) Q(region__in=self.get_descendants())
).count() ).count()
def to_objectchange(self, action):
# Remove MPTT-internal fields #
return ObjectChange( # Site groups
changed_object=self, #
object_repr=str(self),
action=action, @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
object_data=serialize_object(self, exclude=['level', 'lft', 'rght', 'tree_id']) class SiteGroup(NestedGroupModel):
"""
A site group is an arbitrary grouping of sites. For example, you might have corporate sites and customer sites; and
within corporate sites you might distinguish between offices and data centers. Like regions, site groups can be
nested recursively to form a hierarchy.
"""
parent = TreeForeignKey(
to='self',
on_delete=models.CASCADE,
related_name='children',
blank=True,
null=True,
db_index=True
)
name = models.CharField(
max_length=100,
unique=True
)
slug = models.SlugField(
max_length=100,
unique=True
)
description = models.CharField(
max_length=200,
blank=True
)
csv_headers = ['name', 'slug', 'parent', 'description']
def get_absolute_url(self):
return reverse('dcim:sitegroup', args=[self.pk])
def to_csv(self):
return (
self.name,
self.slug,
self.parent.name if self.parent else None,
self.description,
) )
def get_site_count(self):
return Site.objects.filter(
Q(group=self) |
Q(group__in=self.get_descendants())
).count()
# #
# Sites # Sites
# #
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Site(ChangeLoggedModel, CustomFieldModel): class Site(PrimaryModel):
""" """
A Site represents a geographic location within a network; typically a building or campus. The optional facility A Site represents a geographic location within a network; typically a building or campus. The optional facility
field can be used to include an external designation, such as a data center name (e.g. Equinix SV6). field can be used to include an external designation, such as a data center name (e.g. Equinix SV6).
@@ -123,6 +161,13 @@ class Site(ChangeLoggedModel, CustomFieldModel):
blank=True, blank=True,
null=True null=True
) )
group = models.ForeignKey(
to='dcim.SiteGroup',
on_delete=models.SET_NULL,
related_name='sites',
blank=True,
null=True
)
tenant = models.ForeignKey( tenant = models.ForeignKey(
to='tenancy.Tenant', to='tenancy.Tenant',
on_delete=models.PROTECT, on_delete=models.PROTECT,
@@ -188,16 +233,16 @@ class Site(ChangeLoggedModel, CustomFieldModel):
images = GenericRelation( images = GenericRelation(
to='extras.ImageAttachment' to='extras.ImageAttachment'
) )
tags = TaggableManager(through=TaggedItem)
objects = RestrictedQuerySet.as_manager() objects = RestrictedQuerySet.as_manager()
csv_headers = [ csv_headers = [
'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'asn', 'time_zone', 'description',
'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', 'contact_email', 'comments', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone',
'contact_email', 'comments',
] ]
clone_fields = [ clone_fields = [
'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address', 'status', 'region', 'group', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address',
'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', 'contact_email', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', 'contact_email',
] ]
@@ -208,7 +253,7 @@ class Site(ChangeLoggedModel, CustomFieldModel):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dcim:site', args=[self.slug]) return reverse('dcim:site', args=[self.pk])
def to_csv(self): def to_csv(self):
return ( return (
@@ -216,6 +261,7 @@ class Site(ChangeLoggedModel, CustomFieldModel):
self.slug, self.slug,
self.get_status_display(), self.get_status_display(),
self.region.name if self.region else None, self.region.name if self.region else None,
self.group.name if self.group else None,
self.tenant.name if self.tenant else None, self.tenant.name if self.tenant else None,
self.facility, self.facility,
self.asn, self.asn,
@@ -233,3 +279,70 @@ class Site(ChangeLoggedModel, CustomFieldModel):
def get_status_class(self): def get_status_class(self):
return SiteStatusChoices.CSS_CLASSES.get(self.status) return SiteStatusChoices.CSS_CLASSES.get(self.status)
#
# Locations
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Location(NestedGroupModel):
"""
A Location represents a subgroup of Racks and/or Devices within a Site. A Location may represent a building within a
site, or a room within a building, for example.
"""
name = models.CharField(
max_length=100
)
slug = models.SlugField(
max_length=100
)
site = models.ForeignKey(
to='dcim.Site',
on_delete=models.CASCADE,
related_name='locations'
)
parent = TreeForeignKey(
to='self',
on_delete=models.CASCADE,
related_name='children',
blank=True,
null=True,
db_index=True
)
description = models.CharField(
max_length=200,
blank=True
)
images = GenericRelation(
to='extras.ImageAttachment'
)
csv_headers = ['site', 'parent', 'name', 'slug', 'description']
clone_fields = ['site', 'parent', 'description']
class Meta:
ordering = ['site', 'name']
unique_together = [
['site', 'name'],
['site', 'slug'],
]
def get_absolute_url(self):
return reverse('dcim:location', args=[self.pk])
def to_csv(self):
return (
self.site,
self.parent.name if self.parent else '',
self.name,
self.slug,
self.description,
)
def clean(self):
super().clean()
# Parent Location (if any) must belong to the same Site
if self.parent and self.parent.site != self.site:
raise ValidationError(f"Parent location ({self.parent}) must belong to the same site ({self.site})")

View File

@@ -7,7 +7,7 @@ from django.db import transaction
from django.dispatch import receiver from django.dispatch import receiver
from .choices import CableStatusChoices from .choices import CableStatusChoices
from .models import Cable, CablePath, Device, PathEndpoint, PowerPanel, Rack, RackGroup, VirtualChassis from .models import Cable, CablePath, Device, PathEndpoint, PowerPanel, Rack, Location, VirtualChassis
def create_cablepath(node): def create_cablepath(node):
@@ -31,42 +31,37 @@ def rebuild_paths(obj):
with transaction.atomic(): with transaction.atomic():
for cp in cable_paths: for cp in cable_paths:
invalidate_obj(cp.origin)
cp.delete() cp.delete()
create_cablepath(cp.origin) if cp.origin:
invalidate_obj(cp.origin)
create_cablepath(cp.origin)
# #
# Site/rack/device assignment # Location/rack/device assignment
# #
@receiver(post_save, sender=RackGroup) @receiver(post_save, sender=Location)
def handle_rackgroup_site_change(instance, created, **kwargs): def handle_location_site_change(instance, created, **kwargs):
""" """
Update child RackGroups and Racks if Site assignment has changed. We intentionally recurse through each child Update child objects if Site assignment has changed. We intentionally recurse through each child
object instead of calling update() on the QuerySet to ensure the proper change records get created for each. object instead of calling update() on the QuerySet to ensure the proper change records get created for each.
""" """
if not created: if not created:
for rackgroup in instance.get_children(): instance.get_descendants().update(site=instance.site)
rackgroup.site = instance.site locations = instance.get_descendants(include_self=True).values_list('pk', flat=True)
rackgroup.save() Rack.objects.filter(location__in=locations).update(site=instance.site)
for rack in Rack.objects.filter(group=instance).exclude(site=instance.site): Device.objects.filter(location__in=locations).update(site=instance.site)
rack.site = instance.site PowerPanel.objects.filter(location__in=locations).update(site=instance.site)
rack.save()
for powerpanel in PowerPanel.objects.filter(rack_group=instance).exclude(site=instance.site):
powerpanel.site = instance.site
powerpanel.save()
@receiver(post_save, sender=Rack) @receiver(post_save, sender=Rack)
def handle_rack_site_change(instance, created, **kwargs): def handle_rack_site_change(instance, created, **kwargs):
""" """
Update child Devices if Site assignment has changed. Update child Devices if Site or Location assignment has changed.
""" """
if not created: if not created:
for device in Device.objects.filter(rack=instance).exclude(site=instance.site): Device.objects.filter(rack=instance).update(site=instance.site, location=instance.location)
device.site = instance.site
device.save()
# #

View File

@@ -26,9 +26,10 @@ class CableTable(BaseTable):
orderable=False, orderable=False,
verbose_name='Side A' verbose_name='Side A'
) )
termination_a = tables.LinkColumn( termination_a = tables.Column(
accessor=Accessor('termination_a'), accessor=Accessor('termination_a'),
orderable=False, orderable=False,
linkify=True,
verbose_name='Termination A' verbose_name='Termination A'
) )
termination_b_parent = tables.TemplateColumn( termination_b_parent = tables.TemplateColumn(
@@ -37,9 +38,10 @@ class CableTable(BaseTable):
orderable=False, orderable=False,
verbose_name='Side B' verbose_name='Side B'
) )
termination_b = tables.LinkColumn( termination_b = tables.Column(
accessor=Accessor('termination_b'), accessor=Accessor('termination_b'),
orderable=False, orderable=False,
linkify=True,
verbose_name='Termination B' verbose_name='Termination B'
) )
status = ChoiceFieldColumn() status = ChoiceFieldColumn()

View File

@@ -6,7 +6,7 @@ from dcim.models import (
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, FrontPort, Interface, InventoryItem, Platform, ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, FrontPort, Interface, InventoryItem, Platform,
PowerOutlet, PowerPort, RearPort, VirtualChassis, PowerOutlet, PowerPort, RearPort, VirtualChassis,
) )
from tenancy.tables import COL_TENANT from tenancy.tables import TenantColumn
from utilities.tables import ( from utilities.tables import (
BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn, BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn,
TagColumn, ToggleColumn, TagColumn, ToggleColumn,
@@ -44,25 +44,36 @@ __all__ = (
) )
def get_cabletermination_row_class(record):
if record.mark_connected:
return 'success'
elif record.cable:
return record.cable.get_status_class()
return ''
# #
# Device roles # Device roles
# #
class DeviceRoleTable(BaseTable): class DeviceRoleTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.Column(
linkify=True
)
device_count = LinkedCountColumn( device_count = LinkedCountColumn(
viewname='dcim:device_list', viewname='dcim:device_list',
url_params={'role': 'slug'}, url_params={'role_id': 'pk'},
verbose_name='Devices' verbose_name='Devices'
) )
vm_count = LinkedCountColumn( vm_count = LinkedCountColumn(
viewname='virtualization:virtualmachine_list', viewname='virtualization:virtualmachine_list',
url_params={'role': 'slug'}, url_params={'role_id': 'pk'},
verbose_name='VMs' verbose_name='VMs'
) )
color = ColorColumn() color = ColorColumn()
vm_role = BooleanColumn() vm_role = BooleanColumn()
actions = ButtonsColumn(DeviceRole, pk_field='slug') actions = ButtonsColumn(DeviceRole)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = DeviceRole model = DeviceRole
@@ -76,17 +87,20 @@ class DeviceRoleTable(BaseTable):
class PlatformTable(BaseTable): class PlatformTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.Column(
linkify=True
)
device_count = LinkedCountColumn( device_count = LinkedCountColumn(
viewname='dcim:device_list', viewname='dcim:device_list',
url_params={'platform': 'slug'}, url_params={'platform_id': 'pk'},
verbose_name='Devices' verbose_name='Devices'
) )
vm_count = LinkedCountColumn( vm_count = LinkedCountColumn(
viewname='virtualization:virtualmachine_list', viewname='virtualization:virtualmachine_list',
url_params={'platform': 'slug'}, url_params={'platform_id': 'pk'},
verbose_name='VMs' verbose_name='VMs'
) )
actions = ButtonsColumn(Platform, pk_field='slug') actions = ButtonsColumn(Platform)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Platform model = Platform
@@ -110,23 +124,26 @@ class DeviceTable(BaseTable):
template_code=DEVICE_LINK template_code=DEVICE_LINK
) )
status = ChoiceFieldColumn() status = ChoiceFieldColumn()
tenant = tables.TemplateColumn( tenant = TenantColumn()
template_code=COL_TENANT
)
site = tables.Column( site = tables.Column(
linkify=True linkify=True
) )
location = tables.Column(
linkify=True
)
rack = tables.Column( rack = tables.Column(
linkify=True linkify=True
) )
device_role = ColoredLabelColumn( device_role = ColoredLabelColumn(
verbose_name='Role' verbose_name='Role'
) )
device_type = tables.LinkColumn( manufacturer = tables.Column(
viewname='dcim:devicetype', accessor=Accessor('device_type__manufacturer'),
args=[Accessor('device_type__pk')], linkify=True
verbose_name='Type', )
text=lambda record: record.device_type.display_name device_type = tables.Column(
linkify=True,
verbose_name='Type'
) )
if settings.PREFER_IPV4: if settings.PREFER_IPV4:
primary_ip = tables.Column( primary_ip = tables.Column(
@@ -148,13 +165,11 @@ class DeviceTable(BaseTable):
linkify=True, linkify=True,
verbose_name='IPv6 Address' verbose_name='IPv6 Address'
) )
cluster = tables.LinkColumn( cluster = tables.Column(
viewname='virtualization:cluster', linkify=True
args=[Accessor('cluster__pk')]
) )
virtual_chassis = tables.LinkColumn( virtual_chassis = tables.Column(
viewname='dcim:virtualchassis', linkify=True
args=[Accessor('virtual_chassis__pk')]
) )
vc_position = tables.Column( vc_position = tables.Column(
verbose_name='VC Position' verbose_name='VC Position'
@@ -169,12 +184,13 @@ class DeviceTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Device model = Device
fields = ( fields = (
'pk', 'name', 'status', 'tenant', 'device_role', 'device_type', 'platform', 'serial', 'asset_tag', 'site', 'pk', 'name', 'status', 'tenant', 'device_role', 'manufacturer', 'device_type', 'platform', 'serial',
'rack', 'position', 'face', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'primary_ip', 'primary_ip4', 'primary_ip6',
'vc_position', 'vc_priority', 'tags', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'tags',
) )
default_columns = ( default_columns = (
'pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type', 'primary_ip', 'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type',
'primary_ip',
) )
@@ -183,9 +199,7 @@ class DeviceImportTable(BaseTable):
template_code=DEVICE_LINK template_code=DEVICE_LINK
) )
status = ChoiceFieldColumn() status = ChoiceFieldColumn()
tenant = tables.TemplateColumn( tenant = TenantColumn()
template_code=COL_TENANT
)
site = tables.Column( site = tables.Column(
linkify=True linkify=True
) )
@@ -221,6 +235,7 @@ class DeviceComponentTable(BaseTable):
cable = tables.Column( cable = tables.Column(
linkify=True linkify=True
) )
mark_connected = BooleanColumn()
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
order_by = ('device', 'name') order_by = ('device', 'name')
@@ -230,17 +245,23 @@ class CableTerminationTable(BaseTable):
cable = tables.Column( cable = tables.Column(
linkify=True linkify=True
) )
cable_color = ColorColumn(
accessor='cable.color',
orderable=False,
verbose_name='Cable Color'
)
cable_peer = tables.TemplateColumn( cable_peer = tables.TemplateColumn(
accessor='_cable_peer', accessor='_cable_peer',
template_code=CABLETERMINATION, template_code=CABLETERMINATION,
orderable=False, orderable=False,
verbose_name='Cable Peer' verbose_name='Cable Peer'
) )
mark_connected = BooleanColumn()
class PathEndpointTable(CableTerminationTable): class PathEndpointTable(CableTerminationTable):
connection = tables.TemplateColumn( connection = tables.TemplateColumn(
accessor='_path.destination', accessor='_path.last_node',
template_code=CABLETERMINATION, template_code=CABLETERMINATION,
verbose_name='Connection', verbose_name='Connection',
orderable=False orderable=False
@@ -248,6 +269,12 @@ class PathEndpointTable(CableTerminationTable):
class ConsolePortTable(DeviceComponentTable, PathEndpointTable): class ConsolePortTable(DeviceComponentTable, PathEndpointTable):
device = tables.Column(
linkify={
'viewname': 'dcim:device_consoleports',
'args': [Accessor('device_id')],
}
)
tags = TagColumn( tags = TagColumn(
url_name='dcim:consoleport_list' url_name='dcim:consoleport_list'
) )
@@ -255,14 +282,16 @@ class ConsolePortTable(DeviceComponentTable, PathEndpointTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = ConsolePort model = ConsolePort
fields = ( fields = (
'pk', 'device', 'name', 'label', 'type', 'description', 'cable', 'cable_peer', 'connection', 'tags', 'pk', 'device', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
'cable_peer', 'connection', 'tags',
) )
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description') default_columns = ('pk', 'device', 'name', 'label', 'type', 'speed', 'description')
class DeviceConsolePortTable(ConsolePortTable): class DeviceConsolePortTable(ConsolePortTable):
name = tables.TemplateColumn( name = tables.TemplateColumn(
template_code='<i class="mdi mdi-console"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>', template_code='<i class="mdi mdi-console"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}} attrs={'td': {'class': 'text-nowrap'}}
) )
actions = ButtonsColumn( actions = ButtonsColumn(
@@ -274,29 +303,40 @@ class DeviceConsolePortTable(ConsolePortTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = ConsolePort model = ConsolePort
fields = ( fields = (
'pk', 'name', 'label', 'type', 'description', 'cable', 'cable_peer', 'connection', 'tags', 'actions' 'pk', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
'cable_peer', 'connection', 'tags', 'actions'
) )
default_columns = ('pk', 'name', 'label', 'type', 'description', 'cable', 'connection', 'actions') default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection', 'actions')
row_attrs = { row_attrs = {
'class': lambda record: record.cable.get_status_class() if record.cable else '' 'class': get_cabletermination_row_class
} }
class ConsoleServerPortTable(DeviceComponentTable, PathEndpointTable): class ConsoleServerPortTable(DeviceComponentTable, PathEndpointTable):
device = tables.Column(
linkify={
'viewname': 'dcim:device_consoleserverports',
'args': [Accessor('device_id')],
}
)
tags = TagColumn( tags = TagColumn(
url_name='dcim:consoleserverport_list' url_name='dcim:consoleserverport_list'
) )
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = ConsoleServerPort model = ConsoleServerPort
fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'cable', 'cable_peer', 'connection', 'tags') fields = (
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description') 'pk', 'device', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
'cable_peer', 'connection', 'tags',
)
default_columns = ('pk', 'device', 'name', 'label', 'type', 'speed', 'description')
class DeviceConsoleServerPortTable(ConsoleServerPortTable): class DeviceConsoleServerPortTable(ConsoleServerPortTable):
name = tables.TemplateColumn( name = tables.TemplateColumn(
template_code='<i class="mdi mdi-console-network-outline"></i> ' template_code='<i class="mdi mdi-console-network-outline"></i> '
'<a href="{{ record.get_absolute_url }}">{{ value }}</a>', '<a href="{{ record.get_absolute_url }}">{{ value }}</a>',
order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}} attrs={'td': {'class': 'text-nowrap'}}
) )
actions = ButtonsColumn( actions = ButtonsColumn(
@@ -308,15 +348,22 @@ class DeviceConsoleServerPortTable(ConsoleServerPortTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = ConsoleServerPort model = ConsoleServerPort
fields = ( fields = (
'pk', 'name', 'label', 'type', 'description', 'cable', 'cable_peer', 'connection', 'tags', 'actions' 'pk', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
'cable_peer', 'connection', 'tags', 'actions',
) )
default_columns = ('pk', 'name', 'label', 'type', 'description', 'cable', 'connection', 'actions') default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection', 'actions')
row_attrs = { row_attrs = {
'class': lambda record: record.cable.get_status_class() if record.cable else '' 'class': get_cabletermination_row_class
} }
class PowerPortTable(DeviceComponentTable, PathEndpointTable): class PowerPortTable(DeviceComponentTable, PathEndpointTable):
device = tables.Column(
linkify={
'viewname': 'dcim:device_powerports',
'args': [Accessor('device_id')],
}
)
tags = TagColumn( tags = TagColumn(
url_name='dcim:powerport_list' url_name='dcim:powerport_list'
) )
@@ -324,8 +371,8 @@ class PowerPortTable(DeviceComponentTable, PathEndpointTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = PowerPort model = PowerPort
fields = ( fields = (
'pk', 'device', 'name', 'label', 'type', 'description', 'maximum_draw', 'allocated_draw', 'cable', 'pk', 'device', 'name', 'label', 'type', 'description', 'mark_connected', 'maximum_draw', 'allocated_draw',
'cable_peer', 'connection', 'tags', 'cable', 'cable_color', 'cable_peer', 'connection', 'tags',
) )
default_columns = ('pk', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description') default_columns = ('pk', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description')
@@ -334,6 +381,7 @@ class DevicePowerPortTable(PowerPortTable):
name = tables.TemplateColumn( name = tables.TemplateColumn(
template_code='<i class="mdi mdi-power-plug-outline"></i> <a href="{{ record.get_absolute_url }}">' template_code='<i class="mdi mdi-power-plug-outline"></i> <a href="{{ record.get_absolute_url }}">'
'{{ value }}</a>', '{{ value }}</a>',
order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}} attrs={'td': {'class': 'text-nowrap'}}
) )
actions = ButtonsColumn( actions = ButtonsColumn(
@@ -345,19 +393,25 @@ class DevicePowerPortTable(PowerPortTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = PowerPort model = PowerPort
fields = ( fields = (
'pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'cable', 'cable_peer', 'pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'mark_connected', 'cable',
'connection', 'tags', 'actions', 'cable_color', 'cable_peer', 'connection', 'tags', 'actions',
) )
default_columns = ( default_columns = (
'pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'cable', 'connection', 'pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'cable', 'connection',
'actions', 'actions',
) )
row_attrs = { row_attrs = {
'class': lambda record: record.cable.get_status_class() if record.cable else '' 'class': get_cabletermination_row_class
} }
class PowerOutletTable(DeviceComponentTable, PathEndpointTable): class PowerOutletTable(DeviceComponentTable, PathEndpointTable):
device = tables.Column(
linkify={
'viewname': 'dcim:device_poweroutlets',
'args': [Accessor('device_id')],
}
)
power_port = tables.Column( power_port = tables.Column(
linkify=True linkify=True
) )
@@ -368,8 +422,8 @@ class PowerOutletTable(DeviceComponentTable, PathEndpointTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = PowerOutlet model = PowerOutlet
fields = ( fields = (
'pk', 'device', 'name', 'label', 'type', 'description', 'power_port', 'feed_leg', 'cable', 'cable_peer', 'pk', 'device', 'name', 'label', 'type', 'description', 'power_port', 'feed_leg', 'mark_connected', 'cable',
'connection', 'tags', 'cable_color', 'cable_peer', 'connection', 'tags',
) )
default_columns = ('pk', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description') default_columns = ('pk', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description')
@@ -377,6 +431,7 @@ class PowerOutletTable(DeviceComponentTable, PathEndpointTable):
class DevicePowerOutletTable(PowerOutletTable): class DevicePowerOutletTable(PowerOutletTable):
name = tables.TemplateColumn( name = tables.TemplateColumn(
template_code='<i class="mdi mdi-power-socket"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>', template_code='<i class="mdi mdi-power-socket"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}} attrs={'td': {'class': 'text-nowrap'}}
) )
actions = ButtonsColumn( actions = ButtonsColumn(
@@ -388,14 +443,14 @@ class DevicePowerOutletTable(PowerOutletTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = PowerOutlet model = PowerOutlet
fields = ( fields = (
'pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'cable', 'cable_peer', 'connection', 'pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'mark_connected', 'cable',
'tags', 'actions', 'cable_color', 'cable_peer', 'connection', 'tags', 'actions',
) )
default_columns = ( default_columns = (
'pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'cable', 'connection', 'actions', 'pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'cable', 'connection', 'actions',
) )
row_attrs = { row_attrs = {
'class': lambda record: record.cable.get_status_class() if record.cable else '' 'class': get_cabletermination_row_class
} }
@@ -415,6 +470,12 @@ class BaseInterfaceTable(BaseTable):
class InterfaceTable(DeviceComponentTable, BaseInterfaceTable, PathEndpointTable): class InterfaceTable(DeviceComponentTable, BaseInterfaceTable, PathEndpointTable):
device = tables.Column(
linkify={
'viewname': 'dcim:device_interfaces',
'args': [Accessor('device_id')],
}
)
mgmt_only = BooleanColumn() mgmt_only = BooleanColumn()
tags = TagColumn( tags = TagColumn(
url_name='dcim:interface_list' url_name='dcim:interface_list'
@@ -424,7 +485,8 @@ class InterfaceTable(DeviceComponentTable, BaseInterfaceTable, PathEndpointTable
model = Interface model = Interface
fields = ( fields = (
'pk', 'device', 'name', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address', 'pk', 'device', 'name', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address',
'description', 'cable', 'cable_peer', 'connection', 'tags', 'ip_addresses', 'untagged_vlan', 'tagged_vlans', 'description', 'mark_connected', 'cable', 'cable_color', 'cable_peer', 'connection', 'tags', 'ip_addresses',
'untagged_vlan', 'tagged_vlans',
) )
default_columns = ('pk', 'device', 'name', 'label', 'enabled', 'type', 'description') default_columns = ('pk', 'device', 'name', 'label', 'enabled', 'type', 'description')
@@ -434,8 +496,13 @@ class DeviceInterfaceTable(InterfaceTable):
template_code='<i class="mdi mdi-{% if iface.mgmt_only %}wrench{% elif iface.is_lag %}drag-horizontal-variant' template_code='<i class="mdi mdi-{% if iface.mgmt_only %}wrench{% elif iface.is_lag %}drag-horizontal-variant'
'{% elif iface.is_virtual %}circle{% elif iface.is_wireless %}wifi{% else %}ethernet' '{% elif iface.is_virtual %}circle{% elif iface.is_wireless %}wifi{% else %}ethernet'
'{% endif %}"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>', '{% endif %}"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}} attrs={'td': {'class': 'text-nowrap'}}
) )
parent = tables.Column(
linkify=True,
verbose_name='Parent'
)
lag = tables.Column( lag = tables.Column(
linkify=True, linkify=True,
verbose_name='LAG' verbose_name='LAG'
@@ -449,20 +516,28 @@ class DeviceInterfaceTable(InterfaceTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = Interface model = Interface
fields = ( fields = (
'pk', 'name', 'label', 'enabled', 'type', 'lag', 'mgmt_only', 'mtu', 'mode', 'mac_address', 'description', 'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mgmt_only', 'mtu', 'mode', 'mac_address',
'cable', 'cable_peer', 'connection', 'tags', 'ip_addresses', 'untagged_vlan', 'tagged_vlans', 'actions', 'description', 'mark_connected', 'cable', 'cable_color', 'cable_peer', 'connection', 'tags', 'ip_addresses',
'untagged_vlan', 'tagged_vlans', 'actions',
) )
order_by = ('name',)
default_columns = ( default_columns = (
'pk', 'name', 'label', 'enabled', 'type', 'lag', 'mtu', 'mode', 'description', 'ip_addresses', 'cable', 'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mtu', 'mode', 'description', 'ip_addresses',
'connection', 'actions', 'cable', 'connection', 'actions',
) )
row_attrs = { row_attrs = {
'class': lambda record: record.cable.get_status_class() if record.cable else '', 'class': get_cabletermination_row_class,
'data-name': lambda record: record.name, 'data-name': lambda record: record.name,
} }
class FrontPortTable(DeviceComponentTable, CableTerminationTable): class FrontPortTable(DeviceComponentTable, CableTerminationTable):
device = tables.Column(
linkify={
'viewname': 'dcim:device_frontports',
'args': [Accessor('device_id')],
}
)
rear_port_position = tables.Column( rear_port_position = tables.Column(
verbose_name='Position' verbose_name='Position'
) )
@@ -476,8 +551,8 @@ class FrontPortTable(DeviceComponentTable, CableTerminationTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = FrontPort model = FrontPort
fields = ( fields = (
'pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'mark_connected',
'cable_peer', 'tags', 'cable', 'cable_color', 'cable_peer', 'tags',
) )
default_columns = ('pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description') default_columns = ('pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description')
@@ -486,6 +561,7 @@ class DeviceFrontPortTable(FrontPortTable):
name = tables.TemplateColumn( name = tables.TemplateColumn(
template_code='<i class="mdi mdi-square-rounded{% if not record.cable %}-outline{% endif %}"></i> ' template_code='<i class="mdi mdi-square-rounded{% if not record.cable %}-outline{% endif %}"></i> '
'<a href="{{ record.get_absolute_url }}">{{ value }}</a>', '<a href="{{ record.get_absolute_url }}">{{ value }}</a>',
order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}} attrs={'td': {'class': 'text-nowrap'}}
) )
actions = ButtonsColumn( actions = ButtonsColumn(
@@ -497,26 +573,35 @@ class DeviceFrontPortTable(FrontPortTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = FrontPort model = FrontPort
fields = ( fields = (
'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'cable_peer', 'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'mark_connected', 'cable',
'tags', 'actions', 'cable_color', 'cable_peer', 'tags', 'actions',
) )
default_columns = ( default_columns = (
'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'cable_peer', 'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'cable_peer',
'actions', 'actions',
) )
row_attrs = { row_attrs = {
'class': lambda record: record.cable.get_status_class() if record.cable else '' 'class': get_cabletermination_row_class
} }
class RearPortTable(DeviceComponentTable, CableTerminationTable): class RearPortTable(DeviceComponentTable, CableTerminationTable):
device = tables.Column(
linkify={
'viewname': 'dcim:device_rearports',
'args': [Accessor('device_id')],
}
)
tags = TagColumn( tags = TagColumn(
url_name='dcim:rearport_list' url_name='dcim:rearport_list'
) )
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = RearPort model = RearPort
fields = ('pk', 'device', 'name', 'label', 'type', 'positions', 'description', 'cable', 'cable_peer', 'tags') fields = (
'pk', 'device', 'name', 'label', 'type', 'positions', 'description', 'mark_connected', 'cable',
'cable_color', 'cable_peer', 'tags',
)
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description') default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
@@ -524,6 +609,7 @@ class DeviceRearPortTable(RearPortTable):
name = tables.TemplateColumn( name = tables.TemplateColumn(
template_code='<i class="mdi mdi-square-rounded{% if not record.cable %}-outline{% endif %}"></i> ' template_code='<i class="mdi mdi-square-rounded{% if not record.cable %}-outline{% endif %}"></i> '
'<a href="{{ record.get_absolute_url }}">{{ value }}</a>', '<a href="{{ record.get_absolute_url }}">{{ value }}</a>',
order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}} attrs={'td': {'class': 'text-nowrap'}}
) )
actions = ButtonsColumn( actions = ButtonsColumn(
@@ -535,17 +621,24 @@ class DeviceRearPortTable(RearPortTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = RearPort model = RearPort
fields = ( fields = (
'pk', 'name', 'label', 'type', 'positions', 'description', 'cable', 'cable_peer', 'tags', 'actions', 'pk', 'name', 'label', 'type', 'positions', 'description', 'mark_connected', 'cable', 'cable_color',
'cable_peer', 'tags', 'actions',
) )
default_columns = ( default_columns = (
'pk', 'name', 'label', 'type', 'positions', 'description', 'cable', 'cable_peer', 'actions', 'pk', 'name', 'label', 'type', 'positions', 'description', 'cable', 'cable_peer', 'actions',
) )
row_attrs = { row_attrs = {
'class': lambda record: record.cable.get_status_class() if record.cable else '' 'class': get_cabletermination_row_class
} }
class DeviceBayTable(DeviceComponentTable): class DeviceBayTable(DeviceComponentTable):
device = tables.Column(
linkify={
'viewname': 'dcim:device_devicebays',
'args': [Accessor('device_id')],
}
)
status = tables.TemplateColumn( status = tables.TemplateColumn(
template_code=DEVICEBAY_STATUS template_code=DEVICEBAY_STATUS
) )
@@ -566,6 +659,7 @@ class DeviceDeviceBayTable(DeviceBayTable):
name = tables.TemplateColumn( name = tables.TemplateColumn(
template_code='<i class="mdi mdi-circle{% if record.installed_device %}slice-8{% else %}outline{% endif %}' template_code='<i class="mdi mdi-circle{% if record.installed_device %}slice-8{% else %}outline{% endif %}'
'"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>', '"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}} attrs={'td': {'class': 'text-nowrap'}}
) )
actions = ButtonsColumn( actions = ButtonsColumn(
@@ -585,6 +679,12 @@ class DeviceDeviceBayTable(DeviceBayTable):
class InventoryItemTable(DeviceComponentTable): class InventoryItemTable(DeviceComponentTable):
device = tables.Column(
linkify={
'viewname': 'dcim:device_inventory',
'args': [Accessor('device_id')],
}
)
manufacturer = tables.Column( manufacturer = tables.Column(
linkify=True linkify=True
) )
@@ -607,6 +707,7 @@ class DeviceInventoryItemTable(InventoryItemTable):
name = tables.TemplateColumn( name = tables.TemplateColumn(
template_code='<a href="{{ record.get_absolute_url }}" style="padding-left: {{ record.level }}0px">' template_code='<a href="{{ record.get_absolute_url }}" style="padding-left: {{ record.level }}0px">'
'{{ value }}</a>', '{{ value }}</a>',
order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}} attrs={'td': {'class': 'text-nowrap'}}
) )
actions = ButtonsColumn( actions = ButtonsColumn(

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