Compare commits

...

353 Commits

Author SHA1 Message Date
Jeremy Stretch
bfda5d9011 Merge pull request #10937 from netbox-community/develop
Release v3.3.8
2022-11-16 11:32:44 -05:00
jeremystretch
62a80c46a8 Release v3.3.8 2022-11-16 10:45:29 -05:00
jeremystretch
ceec1055e0 Changelog for #10356, #10904, #10920 2022-11-16 10:40:18 -05:00
jeremystretch
540bba4544 Closes #10920: Include request cookies when queuing a custom script 2022-11-16 10:37:06 -05:00
jeremystretch
44c248e6c2 Closes #10934: Update release package URL 2022-11-16 10:36:30 -05:00
Patrick Hurrelmann
3a62fd49e6 Fixes: #10356 backplane connections (#10554)
* Fixes: #10356 Add interface type and cable for backplane connections

* Allow Backplone for front and readports , too.

* Correct tyo in port definition

* pep8 fix (blank lines)

* Remove port type and changed name/description of backplane cable

* Omit backplane cable type

Co-authored-by: Patrick Hurrelmann <patrick.hurrelmann@nfon.com>
Co-authored-by: jeremystretch <jstretch@ns1.com>
2022-11-16 10:26:46 -05:00
Derick Vigne
a2007a4728 Closes #10904: Added Colors to SVG for Front and Rear Ports (#10905)
* Added Colors to SVG for Front and Reaer Ports

Fix for feature request 10904 thanks to @TheZackCodec

* Simplify termination color resolution

Co-authored-by: jeremystretch <jstretch@ns1.com>
2022-11-16 09:57:49 -05:00
jeremystretch
316c3808f7 Changelog for #9439, #10902, #10914, #10915, #10919 2022-11-16 09:43:11 -05:00
Arthur Hanson
928d880f0e 10902 add location to power feed form (#10913)
* 10902 add location to power feed form

* Bind location field to selected site

Co-authored-by: jeremystretch <jstretch@ns1.com>
2022-11-16 09:27:49 -05:00
Arthur
c6930e3ea8 10919 add location to cable termination panels 2022-11-16 09:22:39 -05:00
Arthur
564884a774 10903 add module type on manufacturer page 2022-11-14 14:17:06 -05:00
Arthur
7401fd7050 10909 add l2vpn to tenant stats 2022-11-14 14:16:18 -05:00
jeremystretch
4a95cfd1c4 Permanently connect change logging & webhook receivers 2022-11-14 09:31:25 -05:00
jeremystretch
cd8943144b Use context vars instead of thread-local storage for change logging 2022-11-14 09:31:25 -05:00
jeremystretch
8400509358 Fixes #10891: Populate tag selection list for service filter form 2022-11-14 09:28:03 -05:00
jeremystretch
d971131198 Fixes #10897: Fix form widget styling on FHRP group form 2022-11-14 09:24:12 -05:00
jeremystretch
5729a06348 Fixes #10910: Fix cable creation links on power port view 2022-11-14 09:20:02 -05:00
jeremystretch
d59d23e308 Fixes #10881: Fix dark mode coloring for data on device status page 2022-11-10 16:47:17 -05:00
jeremystretch
3d1501e8fd Changelog for #10837, #10874 2022-11-10 16:33:34 -05:00
Brian Candler
c854c29016 Fix broken cookie paths when BASE_PATH is set (introduced in #10706) (#10856)
Fixes #10837
2022-11-10 16:23:05 -05:00
Arthur Hanson
33d8f8e5e7 10874 remove link to contact roles (#10879) 2022-11-10 16:19:43 -05:00
jeremystretch
93e241e8f3 Changelog for #10709, #10829 2022-11-04 16:56:52 -04:00
Arthur
43da786016 10829 fix top edit selected button 2022-11-04 16:54:24 -04:00
Arthur
271d524687 10709 add AzureAD Tenant Oauth2 2022-11-04 09:12:22 -04:00
Arthur Hanson
4ebcdd2b8f 8072 move js code from template to static file (#10824) 2022-11-03 09:29:45 -04:00
jeremystretch
2af8891f70 PRVB 2022-11-01 17:11:55 -04:00
jeremystretch
4e39021b6f Merge branch 'master' into develop 2022-11-01 17:10:18 -04:00
jeremystretch
2cd5fce62d Release v3.3.7 2022-11-01 17:09:55 -04:00
jeremystretch
ade307bc03 Fixes #10809: Permit nullifying site time_zone via REST API 2022-11-01 17:09:55 -04:00
jeremystretch
c8be4ef8e2 Fixes #10791: Permit nullifying VLAN group scope_type via REST API 2022-11-01 17:09:55 -04:00
jeremystretch
816214361d Fixes #10803: Fix exception when ordering contacts by number of assignments 2022-11-01 17:09:55 -04:00
jeremystretch
d1970ca85b Changelog for #10282, #10770 2022-11-01 17:09:55 -04:00
Arthur
8001694a4c 10282 fix race condition in API IP creation 2022-11-01 17:09:55 -04:00
Arthur
10e258739f 10770 fix social auth 2022-11-01 17:09:55 -04:00
jeremystretch
f3fdf03661 Changelog for #10666 (missed in v3.3.6) 2022-11-01 17:09:55 -04:00
jeremystretch
44814f759c PRVB 2022-11-01 17:09:55 -04:00
jeremystretch
4f5caa5ed2 Release v3.3.7 2022-11-01 16:48:40 -04:00
jeremystretch
aa7f04bf1b Fixes #10809: Permit nullifying site time_zone via REST API 2022-11-01 16:45:32 -04:00
jeremystretch
aaf1ea52b7 Fixes #10791: Permit nullifying VLAN group scope_type via REST API 2022-11-01 15:38:10 -04:00
jeremystretch
7990cfb078 Fixes #10803: Fix exception when ordering contacts by number of assignments 2022-11-01 15:27:35 -04:00
jeremystretch
a25ee66150 Changelog for #10282, #10770 2022-10-31 15:15:45 -04:00
Arthur
867af61875 10282 fix race condition in API IP creation 2022-10-31 15:04:43 -04:00
Arthur
8f4fa065f9 10770 fix social auth 2022-10-31 15:02:57 -04:00
jeremystretch
edb5220228 Changelog for #10666 (missed in v3.3.6) 2022-10-26 15:11:44 -04:00
jeremystretch
18332bdbf1 PRVB 2022-10-26 10:23:50 -04:00
Jeremy Stretch
f1a7bceef2 Merge pull request #10758 from netbox-community/develop
Release v3.3.6
2022-10-26 10:18:44 -04:00
jeremystretch
eac2ace80b Release v3.3.6 2022-10-26 09:58:31 -04:00
Kevin Petremann
174ba6cf0f Fix LDAP auth: user never updated if inactive 2022-10-26 09:40:05 -04:00
jeremystretch
658c9347f3 Fixes #10682: Correct home view links to connection lists 2022-10-26 09:32:29 -04:00
jeremystretch
7b3ef2ade5 Fixes #10719: Prevent user without sufficient permission from creating an IP address via FHRP group creation 2022-10-26 08:44:20 -04:00
jeremystretch
2a62b628cf Fixes #10723: Distinguish between inside/outside NAT assignments for device/VM primary IPs 2022-10-26 08:23:50 -04:00
Arthur
d8c07abd68 10610 interface_id query on lag return vc interfaces 2022-10-26 08:10:03 -04:00
Arthur Hanson
8d486c5838 10716 add left-right plugins to tags page (#10744)
* 10716 add left-right plugins to tags page

* 10716 add back plugin_full_width
2022-10-26 08:05:15 -04:00
jeremystretch
eb91934d70 Fixes #10745: Correct display of status field in clusters list 2022-10-25 16:41:07 -04:00
jeremystretch
01654765e8 Fixes #10746: Add missing status attribute to cluster view 2022-10-25 16:38:32 -04:00
jeremystretch
4c504870e0 Tweak PR template language 2022-10-21 12:47:19 -04:00
jeremystretch
3d687a6c2d Closes #10718: Optimize object-based permissions enforcement 2022-10-21 12:43:36 -04:00
jeremystretch
96c4696417 Changelog for #9584, #10580, #10639 2022-10-20 16:31:52 -04:00
Arthur Hanson
e7659a5f99 9584 add device type (slug) to filter list (#10630)
* 9584 add device type (slug) to filter list

* 9584 add test
2022-10-20 16:27:51 -04:00
Craig Pund
53c9c3cf8d Fixes #10580 (#10687)
* change IP address accessor to parent object

* set IP assigned check to link to interface

* Fix Assigned not being orderable

Co-authored-by: Jeremy Stretch <jstretch@ns1.com>

Co-authored-by: Craig Pund <cpund@iuhealth.org>
Co-authored-by: Jeremy Stretch <jstretch@ns1.com>
2022-10-20 16:26:26 -04:00
Patrick Rauscher
f60312febf Set *_COOKIE_PATH according to BASE_PATH
As discussed in #10639, all three `COOKIE_PATH`s should be set accordingly with the netbox-`BASE_PATH` to improve coexistance with other Django-projects probably hosted on the same Host
2022-10-20 16:24:38 -04:00
jeremystretch
7505baf3a1 Fixes #10712: Fix ModuleNotFoundError exception when generating API schema under Python 3.9+ 2022-10-20 15:49:13 -04:00
jeremystretch
33c6142365 Update documentation section options for new issues 2022-10-19 09:52:10 -04:00
jeremystretch
10e874039f Changelog for #10643, #10646 2022-10-19 09:02:09 -04:00
jeremystretch
060ee2dd96 Revert PR #10621 2022-10-19 08:55:30 -04:00
jeremystretch
43d1182b4b Fix styling for power input, rear port connection links 2022-10-19 08:47:14 -04:00
Arthur
d53da57f63 10646 fix cable power feed filter 2022-10-19 08:42:55 -04:00
Arthur Hanson
028b4b7ea7 10643 add fieldset to device role for improved add/edit form display (#10680)
* 10643 add fieldset to device role for improved add/edit form display

* 10643 update other forms

* 10643 update other forms

* Specify fieldsets for additional models

Co-authored-by: jeremystretch <jstretch@ns1.com>
2022-10-19 08:35:23 -04:00
jeremystretch
4cb0230878 Closes #10685: Position A/Z termination cards above the fold under circuit view 2022-10-18 16:51:37 -04:00
Arthur Hanson
2fe8df3cbb 10655 fix contacts display in list views (#10681)
* 10655 fix contacts display in list views

* 10655 review changes
2022-10-18 16:47:14 -04:00
Arthur
64d67e3b00 10584 add clone fields to ipam-service 2022-10-18 16:06:31 -04:00
jeremystretch
aaf829898b Changelog for #10575, #10596 2022-10-12 08:41:41 -04:00
Arthur
8481cf66e3 10575 add requirements for openid connect packages 2022-10-12 08:39:14 -04:00
Arthur Hanson
bb150379a2 10571 replace deprecated mkdoc settings (#10622)
* 10571 replace deprecated mkdoc settings

* Omit landing page from docs nav menu

Co-authored-by: jeremystretch <jstretch@ns1.com>
2022-10-12 08:36:02 -04:00
Arthur Hanson
cc811e5a56 10596 add overflow-auto to card-body (#10621)
* 10596 add overflow-auto to card-body

* 10596 add overflow-auto to card-body
2022-10-12 08:31:02 -04:00
jeremystretch
a9e583a693 Changelog for #9669, #9722 2022-10-05 12:04:47 -04:00
Jeremy Stretch
3a3ff474cb Merge pull request #10567 from scanplus/ldap-ca-cert
Added LDAP_CA_CERT_* to LDAP settings
2022-10-05 12:01:39 -04:00
Arthur Hanson
cc00789d35 9669 sanitize social auth usernames (#10549) 2022-10-05 11:50:47 -04:00
Jeremy Stretch
689f11a573 Merge pull request #10555 from kkthxbye-code/10527-update-js-dependencies
Fixes #10527 - Update JS dependencies
2022-10-05 11:04:37 -04:00
jeremystretch
ae90ad1fb7 PRVB 2022-10-05 10:13:02 -04:00
Jeremy Stretch
56d9725c39 Merge pull request #10570 from netbox-community/develop
Release v3.3.5
2022-10-05 10:10:44 -04:00
jeremystretch
1c69bfaf2c Release v3.3.5 2022-10-05 09:47:55 -04:00
Tobias Genannt
5e37f82b2f Added LDAP_CA_CERT_* to LDAP settings
These options can be used to specify a CA certificate to validate the LDAP
server certificate
2022-10-05 14:28:30 +02:00
jeremystretch
bdefd8ea8c Fixes #10562: Correct URL for contacts table tags column 2022-10-05 08:13:33 -04:00
kkthxbye-code
eabd405845 Fix graphiql by pinning esbuild 2022-10-04 22:00:32 +02:00
jeremystretch
03946f2ca8 Fixes #10559: Permit the pinning of a VM to a particular device within a cluster which has no site assignment 2022-10-04 15:46:55 -04:00
jeremystretch
fec8d1bc2f Fixes #10423: Enforce object type validation when creating journal entries 2022-10-04 15:26:52 -04:00
jeremystretch
53f5f46037 #10460: Fix PowerFeed details 2022-10-04 14:36:14 -04:00
kkthxbye-code
b227757b9a Update JS dependencies WIP 2022-10-04 15:02:37 +02:00
jeremystretch
eef5cefb5d Fixes #10460: Restore missing connection details for device components 2022-10-03 16:11:24 -04:00
jeremystretch
7712b81ab9 Fixes #10517: Automatically inherit site assignment from cluster when creating a virtual machine 2022-10-03 15:35:45 -04:00
jeremystretch
7feb86fe55 Changelog for #10352 2022-10-03 15:03:28 -04:00
PieterL75
d1efbf6620 Issue10352 removegetvariables (#10475)
* Add javascript to disable empty form fields

* add js cleanGetUrl

* use addEventListener submit

* use addEventListener

* update collectstatics

* Use FormData to remove empty fields

* optimeze ts-ignore

* update ts-ignore comment

* oneline of ts-ignore

* one line of ts-ingnore

* fix tsc errors by adding types (as per kkthxbye)

Co-authored-by: Pieter Lambrecht <pieter.lambrecht@sentia.com>
2022-10-03 14:32:01 -04:00
jeremystretch
aabee05a6a Changelog for #8424, #10491 2022-10-03 13:58:04 -04:00
jeremystretch
cf062b5b6a Closes #10346: Document how to access plugin config parameters 2022-10-03 13:56:46 -04:00
Arthur Hanson
0b6a3898fe 8424 device location (#10544)
* 8424 fix merge

* 8424 fix merge

* 8424 fix merge

* 8424 fix merge
2022-10-03 13:55:05 -04:00
Jeremy Stretch
517ebcfbcd Merge pull request #10525 from netbox-community/10491-delete-dependant
10491 improve error message for ProtectedError on contact assignment
2022-10-03 13:27:34 -04:00
jeremystretch
9ef24d3f43 Fixes #10513: Disable the reassignment of a module to a new device 2022-10-03 11:11:51 -04:00
Arthur
02ffc2ddee 10491 improve error message for ProtectedError on contact assignment 2022-09-30 09:09:21 -07:00
jeremystretch
62820ea2b8 Add workflow_dispatch event 2022-09-29 12:36:10 -04:00
jeremystretch
04738587e8 Move permissions block to root 2022-09-29 12:17:10 -04:00
jeremystretch
cbbfcd0e7b Bump stale to v6 2022-09-29 12:00:44 -04:00
jeremystretch
309a70df89 Tweak workflow permissions 2022-09-29 11:59:15 -04:00
Alex
4cb6984a65 GitHub Workflows security hardening (#10456)
* build: harden lock.yml permissions

Signed-off-by: Alex <aleksandrosansan@gmail.com>

* build: harden stale.yml permissions

Signed-off-by: Alex <aleksandrosansan@gmail.com>

* build: harden ci.yml permissions

Signed-off-by: Alex <aleksandrosansan@gmail.com>

Signed-off-by: Alex <aleksandrosansan@gmail.com>
2022-09-29 11:41:33 -04:00
jeremystretch
3c32c09a5a Fixes #10496: Use page.canonical_url to identify ReadTheDocs builds 2022-09-28 09:30:38 -04:00
jeremystretch
2d9852d6f1 Fixes #10408: Fix validation when attempting to add redundant contact assignments 2022-09-27 13:11:57 -04:00
jeremystretch
05542324fc Changelog for #10465, #10480 2022-09-27 11:53:11 -04:00
Patrick Hurrelmann
669e86f96e Fixes: #10465 Format all remaining displayed rackunits with floatformat (#10481)
* Fixes: #10465 Try to finish #10268 and format all remaining displayed rackunits with floatformat

* #10465: PEP8 fix

Co-authored-by: Patrick Hurrelmann <patrick.hurrelmann@nfon.com>
Co-authored-by: jeremystretch <jstretch@ns1.com>
2022-09-27 11:24:19 -04:00
Jeremy Stretch
cbf928f363 Merge pull request #10482 from phurrelmann/10480-fix-link-target-on-cable-svg
Fixes: #10480 Fix link-target on cable-trace svg
2022-09-27 11:07:14 -04:00
Patrick Hurrelmann
43b18c13e3 Fixes: #10480 Fix link-target on cable-trace svg to open link in the same window. 2022-09-27 13:23:51 +02:00
jeremystretch
dda193247a Fixes #10470: Omit read-only custom fields from CSV import forms 2022-09-26 16:47:34 -04:00
jeremystretch
2463e4efd3 Fixes #10461: Enable filtering by read-only custom fields in the UI 2022-09-26 16:42:11 -04:00
jeremystretch
a0b17887fd Fixes #10445: Avoid rounding virtual machine memory values 2022-09-26 15:45:58 -04:00
jeremystretch
96784640e3 Changelog for #10435, #10439 2022-09-26 10:27:35 -04:00
Jeremy Stretch
b75d12fe05 Merge pull request #10442 from netbox-community/10435-untagged-vlan
10435 check if vm.cluster in qs
2022-09-26 10:25:48 -04:00
Jeremy Stretch
5e389c32ed Merge pull request #10463 from netbox-community/revert-10410-10408-add-contact
Revert "10408 add error message if already exists"
2022-09-26 10:24:54 -04:00
Jeremy Stretch
fd89ef04b6 Revert "10408 add error message if already exists" 2022-09-26 10:24:40 -04:00
Jeremy Stretch
abcc10e938 Merge pull request #10410 from netbox-community/10408-add-contact
10408 add error message if already exists
2022-09-26 10:24:23 -04:00
jeremystretch
3ad337dd15 Filter VLANs and VLANGroups by site or cluster site for VM 2022-09-26 10:08:54 -04:00
Jeremy Stretch
a527767caa Merge pull request #10455 from miaow2/10439-airlow-widget
10439 Add widget for Airflow field in DeviceTypeForm
2022-09-26 09:20:48 -04:00
Arthur Hanson
39129ecedf 10407 fix documentation link to requests (#10409)
* 10407 fix documentation link to requests

* Append page heading to URL

Co-authored-by: jeremystretch <jstretch@ns1.com>
2022-09-26 09:17:02 -04:00
Artem I. Kotik
c97d2d4fe9 Add widget for Airflow field in DeviceTypeForm 2022-09-24 15:49:23 +04:00
Arthur
7735634649 10435 check if vm.cluster in qs 2022-09-22 10:34:37 -07:00
Jonathan Senecal
148c6a6c23 Merge pull request #10432 from netbox-community/10431-pylance-is-no-longer-working-by-default-in-vscode
Add [tool.pyright] to pyproject.toml and fix #10431
2022-09-21 15:33:25 -04:00
Jonathan Senecal
360172cad0 Add [tool.pyright] to pyproject.toml 2022-09-21 15:19:40 -04:00
Daniel Sheppard
75c91232b4 Update changelog for #9497 2022-09-20 09:49:46 -05:00
Daniel Sheppard
0190c0225e Merge pull request #10420 from netbox-community/9497-fix-site-location-nonracked-device-display
Fixes #9497 - Change non-racked filter for sites/locations
2022-09-20 09:48:09 -05:00
Daniel Sheppard
86d366be4d Fixes #9651 - Document Pre-Change process for scripts 2022-09-20 09:46:23 -05:00
Daniel Sheppard
71d71a6b1b Fixes #9497 - Change filter for sites/locations 2022-09-20 09:26:40 -05:00
Arthur
695ad47fe9 10408 add error message if already exists 2022-09-19 10:46:16 -07:00
jeremystretch
1b62c11db5 PRVB 2022-09-16 13:41:09 -04:00
Jeremy Stretch
83a66a672d Merge pull request #10391 from netbox-community/develop
Release v3.3.4
2022-09-16 13:39:06 -04:00
jeremystretch
30b9ddc251 Release v3.3.4 2022-09-16 13:14:14 -04:00
jeremystretch
4a9831bd23 Fixes #10383: Fix assignment of component templates to module types via web UI 2022-09-16 08:30:46 -04:00
jeremystretch
59388d89a0 Fixes #10387: Fix MultiValueDictKeyError exception when editing a device interface 2022-09-16 08:20:24 -04:00
jeremystretch
1d033bd286 Update lock threads action; lock closed PRs after 30 days 2022-09-15 16:08:43 -04:00
jeremystretch
935f008c16 PRVB 2022-09-15 14:29:43 -04:00
Arthur Hanson
b5d57f3418 Merge pull request #10381 from netbox-community/develop
Release v3.3.3
2022-09-15 11:18:08 -07:00
Arthur
05b17a0082 Release v3.3.3 2022-09-15 11:04:12 -07:00
jeremystretch
91fd25a548 Fixes #10353: Table action buttons should reserve return URL parameters 2022-09-15 13:40:39 -04:00
jeremystretch
e05696dfcc Fixes #10337: Display SSO links when local authentication fails 2022-09-15 13:17:04 -04:00
Jeremy Stretch
157a45b627 Merge pull request #10369 from netbox-community/10359-add-region-column
#10359 add region column to site table
2022-09-15 13:07:59 -04:00
jeremystretch
e9a91455e8 #10359: Add region and site group columns to the devices table 2022-09-15 12:55:21 -04:00
jeremystretch
f97eb99950 Changelog for #10247, #10258, #10305, #10362 2022-09-15 10:14:16 -04:00
Jeremy Stretch
c4b7ab067a Fixes #10247: Allow changing selected device/VM when creating a new component (#10312)
* Initial work on #10247

* Continued work on #10247

* Clean up component creation tests

* Move valdiation of replicated field to form

* Clean up ordering of fields in component creation forms

* Omit fieldset header if none

* Clean up ordering of fields in component template creation forms

* View tests should not move component templates to new device type

* Define replication_fields on VMInterfaceCreateForm

* Clean up expandable field help texts

* Update comments

* Update component bulk update forms & views to support new replication fields

* Fix ModularDeviceComponentForm parent class

* Fix bulk creation of VM interfaces (thanks @kkthxbye-code!)
2022-09-15 10:10:32 -04:00
Jeremy Stretch
7477f6584e Merge pull request #10370 from kvedder-amplex/develop
add custom fields to l2vpntermination edit template
2022-09-15 10:07:27 -04:00
Jeremy Stretch
7b4f5252f1 Merge pull request #10290 from kkthxbye-code/10258-nested-module-scripts
Fixes #10258 - Allow running scripts nested in modules/packages
2022-09-15 10:06:28 -04:00
kvedder
4e03419e85 add custom fields to l2vpntermination edit template 2022-09-14 22:15:12 -04:00
Arthur
4d97043e26 #10359 add region column to site table 2022-09-14 15:50:45 -07:00
kkthxbye-code
c335b76ec6 PEP8: Fix whitespace on blank line 2022-09-14 20:00:12 +02:00
kkthxbye-code
356ff457be Allow reports to be nested in submodules 2022-09-14 19:57:37 +02:00
Jeremy Stretch
833c3fbd39 Merge pull request #10349 from sasschary/10316-cableform-termination
Fixes #10305: Allows null master in VirtualChassis APIs
2022-09-14 12:13:16 -04:00
Zachary Clark
ace66eab61 Fixes #10305: Allows null master in VirtualChassis APIs 2022-09-13 19:54:39 -04:00
jeremystretch
ea9d2e3f88 Closes #9577: Add has_front_image and has_rear_image filters for device types 2022-09-13 14:14:18 -04:00
jeremystretch
211a1394d3 Changelog for #8580, #10333 2022-09-12 17:17:53 -04:00
Jeremy Stretch
44032ffc11 Merge pull request #10291 from netbox-community/8580-filter-connected
#8580 add interface filters for connected
2022-09-12 17:16:28 -04:00
jeremystretch
f10460d774 Add relevant tests for all device components 2022-09-12 17:03:33 -04:00
jeremystretch
57365ef7b9 Rename is_occupied to occupied 2022-09-12 16:42:04 -04:00
Arthur
d24f10ce6e #8580 add tests 2022-09-12 10:52:40 -07:00
Arthur
1daa2ff98d #8580 add tests 2022-09-12 10:22:05 -07:00
Arthur
0e6a6b784d Merge branch 'develop' into 8580-filter-connected 2022-09-12 10:11:20 -07:00
Jeremy Stretch
e3576e2614 Merge pull request #10338 from jsenecal/10298-pyproject-toml
Introduce pyproject.toml
2022-09-12 12:56:41 -04:00
Jeremy Stretch
0899d7aefd Merge pull request #10335 from netbox-community/10333-customfield-csv-ui_visibility
Fixes #10333: Show available values for ui_visibility field of CustomField for CSV import
2022-09-12 12:56:10 -04:00
Jonathan Senecal
d078befd33 Introduce pyproject.toml
- Tweaked to not break the existing codebase... too much
2022-09-12 11:48:26 -04:00
jeremystretch
f67cb71dbc Fixes #10333: Show available values for ui_visibility field of CustomField for CSV import 2022-09-12 10:36:19 -04:00
Arthur Hanson
721cd578bb 10310 pre commit yarn (#10315)
* #10310 run yarn pre-commit only if static files changed
2022-09-12 09:16:17 -04:00
jeremystretch
3aac62caa7 Changelog for #10304, #10307 2022-09-09 17:08:12 -04:00
Jeremy Stretch
6b9eb57de7 Merge pull request #10313 from netbox-community/10304-customfield-clone
Fixes #10304: Enable cloning for custom fields & custom links
2022-09-09 17:04:01 -04:00
jeremystretch
2b2a41edd2 Enable cloning for custom fields & custom links 2022-09-09 16:51:18 -04:00
jeremystretch
cd1ad452da Move clone() to CloningMixin 2022-09-09 16:44:58 -04:00
Jeremy Stretch
77868a9b17 Merge pull request #10309 from netbox-community/10307-fix-poe-type
#10307 fix choices for poe type
2022-09-09 16:20:21 -04:00
Arthur
59a2a43473 #10307 fix choices for poe type 2022-09-09 07:40:01 -07:00
jeremystretch
385a0f979e Changelog for #10250, #10294 2022-09-08 16:50:16 -04:00
Jeremy Stretch
770cc5a700 Merge pull request #10287 from netbox-community/10250-cable-import-virtual
Fixes #10250: Fix exception when CableTermination validation fails during bulk import of cables
2022-09-08 16:48:02 -04:00
Jeremy Stretch
a03155432e Merge pull request #10286 from netbox-community/10259-prefix-flat-link
Fixes #10259: Fix NoReverseMatch exception when listing available prefixes with "flat" column displayed
2022-09-08 16:47:27 -04:00
Arthur
d51e833bf3 #8580 changes from code review 2022-09-08 13:11:17 -07:00
jeremystretch
c6644ec1ae Annotate upgrade paths in upgrade documentation 2022-09-08 10:17:46 -04:00
Jeremy Stretch
a7562a6aa1 Merge pull request #10296 from kkthxbye-code/10294-fix-wwn-changelog
Fixes #10294 - Don't return empty string for empty wwn form field
2022-09-08 09:32:53 -04:00
jeremystretch
6b70436e2b Add Repography stats 2022-09-08 09:18:45 -04:00
kkthxbye-code
3dbc7bdd2c Add wwn CharField to InterfaceForm 2022-09-08 11:38:39 +02:00
Arthur
b4877e7fac #8580 add interface filters for connected 2022-09-07 15:45:01 -07:00
kkthxbye-code
f489ffa043 Allow running scripts nested in modules/packages 2022-09-07 22:33:24 +02:00
jeremystretch
211c7641c1 Fixes #10250: Fix exception when CableTermination validation fails during bulk import of cables 2022-09-07 14:41:48 -04:00
jeremystretch
51d066a1bc Fixes #10259: Fix NoReverseMatch exception when listing available prefixes with "flat" column displayed 2022-09-07 14:30:51 -04:00
jeremystretch
b702822857 Closes #10268: Omit trailing ".0" in device positions within UI 2022-09-07 14:09:17 -04:00
jeremystretch
1cbb2320c1 Changelog for #9231, #10270, #10278 2022-09-07 13:59:00 -04:00
Jeremy Stretch
1c4ad3f817 Merge pull request #10285 from netbox-community/10278-rack-photo-add-another
#10278 fix attaching an image to a rack "Create & Add Another"
2022-09-07 13:56:13 -04:00
Jeremy Stretch
ef5ec06141 Merge pull request #10283 from netbox-community/10270-save-custom-boolean
#10270 - fix custom field validation for IPAM services
2022-09-07 13:54:24 -04:00
Jeremy Stretch
0c37236d60 Merge pull request #10281 from netbox-community/9231-empty-search
9231 fix empty search
2022-09-07 13:53:33 -04:00
Arthur
def853e8c4 #10278 add get_extra_addanother_params 2022-09-07 10:40:24 -07:00
Arthur
7826cfb01f #10270 - fix custom field validation for ipam services 2022-09-07 08:45:56 -07:00
jeremystretch
ac8f0a7ef2 Add installation video to docs 2022-09-07 11:23:13 -04:00
Arthur
48a907ae45 #9231 add comment 2022-09-07 08:09:28 -07:00
Arthur
2fe620df70 #9231 call class method 2022-09-06 18:04:29 -07:00
Arthur
536bd37d05 #9231 make empty search work 2022-09-06 16:37:52 -07:00
jeremystretch
d29d265b0a PRVB 2022-09-02 15:44:41 -04:00
Jeremy Stretch
41d653738a Merge pull request #10254 from netbox-community/develop
Release v3.3.2
2022-09-02 15:33:54 -04:00
jeremystretch
1955497dbe Release v3.3.2 2022-09-02 15:20:14 -04:00
Jeremy Stretch
572803d7ac Merge pull request #10253 from netbox-community/9823-fix_numericarrayfield_validation_error
Fixes #9823 - Make validation error from NumericArrayField more verbose.
2022-09-02 15:18:42 -04:00
Daniel Sheppard
25a474f9f7 Fix PEP8 errors 2022-09-02 14:05:38 -05:00
Daniel Sheppard
c42f7ab6d3 Fixes #9823 - Make validation error from NumericArrayField more verbose. 2022-09-02 13:48:15 -05:00
Jeremy Stretch
a7717b432e Merge pull request #10252 from netbox-community/10034-l2vpn-column
Fixes #10034: Add L2VPN column to interface and VLAN tables
2022-09-02 14:39:59 -04:00
jeremystretch
2acfda3dc5 Fixes #10034: Add L2VPN column to interface and VLAN tables 2022-09-02 14:26:35 -04:00
jeremystretch
569525fb68 Changelog for #9481, #10231, #10233 2022-09-02 08:56:09 -04:00
Jeremy Stretch
15353b7513 Merge pull request #10233 from netbox-community/9665-rack-elevation-sorting
#9665 Rack Elevation Sorting Enhancements
2022-09-02 08:53:16 -04:00
Jeremy Stretch
4737336b85 Merge pull request #10239 from amhn/Add_missing_serializer_annotation
Closes #10231: Add missing serializer annotations
2022-09-02 08:27:26 -04:00
Jeremy Stretch
d85b1c775f Merge pull request #10246 from netbox-community/9481-child-parent-location
#9481 update child location whem move parent
2022-09-02 08:25:34 -04:00
Arthur
2ce1a96468 #9481 update child location whem move parent 2022-09-01 15:44:41 -07:00
Arthur
239b8d2e7c #9665 review changes 2022-09-01 11:15:23 -07:00
Arthur
43b3ce9ed1 #9665 review changes 2022-09-01 11:12:01 -07:00
Arthur
80d5a966db Merge branch 'develop' into 9665-rack-elevation-sorting 2022-09-01 09:38:38 -07:00
Andreas Nieß
b11fa53519 Closes #10231: Add missing serializer annotations 2022-09-01 17:18:09 +02:00
jeremystretch
ce4d00dc21 Fixes #10184: Fix vertical alignment when displaying object attributes with buttons 2022-09-01 09:53:48 -04:00
Jeremy Stretch
bb269affe2 Merge pull request #10235 from netbox-community/10220-vm-primary-ips
Fixes #10220: Validate IP version when assigning primary IPs to a VM
2022-09-01 09:47:50 -04:00
jeremystretch
d818c250b0 Fixes #10220: Validate IP version when assigning primary IPs to a virtual machine 2022-09-01 09:31:42 -04:00
Arthur
3d4d880110 #9665 Rack Elevation Sorting Enhancements 2022-08-31 14:42:59 -07:00
jeremystretch
899b61264f Fixes #9328: Hide available IPs when non-default ordering is applied 2022-08-31 16:55:03 -04:00
jeremystretch
4dc059fba3 Changelog for #9477, #10195 2022-08-31 16:44:43 -04:00
Jeremy Stretch
f94792fad8 Merge pull request #10230 from netbox-community/10195-filterdevice
10195 Filter device components by Rack
2022-08-31 16:41:31 -04:00
jeremystretch
6df2ff7ebf #10195: Add filterset tests 2022-08-31 16:22:37 -04:00
Jeremy Stretch
dc4ddedca3 Merge pull request #10232 from netbox-community/9477-reset-column-ordering
Closes #9477: Add a button to clear applied table column ordering
2022-08-31 15:46:00 -04:00
jeremystretch
73b85f9b29 Minor tweaks to git cheat sheet 2022-08-31 15:44:06 -04:00
jeremystretch
ce054dd37d Closes #9477: Add a button to clear applied table column ordering 2022-08-31 15:28:59 -04:00
Arthur
3a0e91a688 #10195 add rack to component filter forms 2022-08-31 11:40:03 -07:00
Arthur
60ca4f29d7 #10195 add rack to component filter forms 2022-08-31 08:41:47 -07:00
jeremystretch
301ebe0da3 Add content to the git cheat sheet 2022-08-31 10:52:58 -04:00
jeremystretch
ada745324f Changelog for #10176, #10217 2022-08-31 10:36:01 -04:00
jeremystretch
a69eec5fb9 Further tweaks to the PR template 2022-08-31 10:36:01 -04:00
Jeremy Stretch
8c35ebbb7e Merge pull request #10227 from netbox-community/10217-cable-trace-split
Fixes #10217: Handle exception when trace splits to multiple rear ports
2022-08-31 10:25:41 -04:00
jeremystretch
5ef2d1d7ad Fixes #10217: Handle exception when trace splits to multiple rear ports 2022-08-31 10:07:10 -04:00
Jeremy Stretch
815b2d8a2b Merge pull request #10225 from netbox-community/10176
#10176 fix space utilization for rack
2022-08-31 08:55:39 -04:00
Arthur
0cfe2d882d #10176 fix space utilization for rack 2022-08-30 16:16:15 -07:00
Jeremy Stretch
6019b738a4 Merge pull request #10218 from netbox-community/10043-available-vlans-limit
Closes #10043: Add support for 'limit' query parameter to available VLANs API endpoint
2022-08-30 15:31:09 -04:00
jeremystretch
de17a651e6 Closes #10043: Add support for 'limit' query parameter to available VLANs API endpoint 2022-08-30 15:16:34 -04:00
jeremystretch
91dc9f0c9d Tweak PR template 2022-08-30 14:27:27 -04:00
jeremystretch
0770aa237a Changelog for #9895, #10060 2022-08-30 14:23:43 -04:00
Jeremy Stretch
d3f2d77961 Merge pull request #10187 from amhn/9895_Remove_JSONFieldInspector
9895 remove json field inspector
2022-08-30 14:21:02 -04:00
Jeremy Stretch
da03b22fe4 Merge pull request #10215 from netbox-community/10060-searchjournal
#10060 add journal entry to global search
2022-08-30 13:57:32 -04:00
Arthur
456647838c #10060 add journal entry to global search 2022-08-30 08:39:51 -07:00
Jeremy Stretch
e192ac34d0 Merge pull request #10213 from kkthxbye-code/10212-add-sidebar-padding
Fixes #10212 - Add padding to the bottom of the sidebar
2022-08-30 10:30:30 -04:00
kkthxbye
6a3ccda12e Add padding to the bottom of the sidebar 2022-08-30 15:57:04 +02:00
jeremystretch
14f79ef85a Closes #10202: Clean up TemplateColumn template code & accessors 2022-08-30 09:55:12 -04:00
jeremystretch
924471ee76 Clean up cable terminations display & edit form 2022-08-30 09:16:17 -04:00
jeremystretch
cd5844b050 Fixes #10208: Fix permissions evaluation for interface actions dropdown menu 2022-08-30 08:59:50 -04:00
jeremystretch
0f6a12b595 Changelog for #9962, #9963, #10178 2022-08-30 08:56:11 -04:00
Jeremy Stretch
d19c6a6afc Merge pull request #10173 from netbox-community/9962-ssonextparam
#9962 add next param to sso url
2022-08-30 08:53:58 -04:00
Jeremy Stretch
3257ea00b5 Merge pull request #10171 from netbox-community/10170-changelog
#10170 optimize change log queries
2022-08-30 08:50:01 -04:00
Jeremy Stretch
f073087379 Merge pull request #10205 from netbox-community/9963-csrftoken
#9963 change csrf token access
2022-08-30 08:48:23 -04:00
Jeremy Stretch
1ba47ae67c Merge pull request #10206 from netbox-community/10178-devicemanufacturer
#10178 add manufacturer to device detail view devicetype
2022-08-30 08:39:11 -04:00
Arthur
8ca3dfd8c9 #10178 add manufacturer to device detail view devicetype 2022-08-29 15:26:38 -07:00
Arthur
cdea30253b #101700 change csrf token access 2022-08-29 14:57:56 -07:00
Arthur
c8ee01ba0f #10170 changes from codereview 2022-08-29 14:53:28 -07:00
Arthur
3ad08c75c0 Revert "#101700 change csrf token access"
This reverts commit c12e545ccd.
2022-08-29 14:48:20 -07:00
Arthur
c12e545ccd #101700 change csrf token access 2022-08-29 14:40:03 -07:00
Jeremy Stretch
6571faad6c Merge pull request #10204 from netbox-community/10164-git-cheat-sheet
Closes #10164: Add a git cheat sheet
2022-08-29 16:01:31 -04:00
jeremystretch
f98f1647da Add "fixing mistakes" section 2022-08-29 15:40:39 -04:00
Arthur
249b8b0363 #10170 changes from code review 2022-08-29 12:20:51 -07:00
jeremystretch
777af35030 Fixes #10177: Correct display of custom fields when editing VM interfaces 2022-08-29 15:10:14 -04:00
Arthur
23fafe1996 Merge branch 'develop' into 10170-changelog 2022-08-29 12:08:39 -07:00
Jeremy Stretch
e0741cc9af Merge pull request #10200 from netbox-community/10181-api-file-upload
Fixes #10181: Restore MultiPartParser (regression from #10031)
2022-08-29 11:40:55 -04:00
jeremystretch
0e4911a575 Fixes #10181: Restore MultiPartParser (regression from #10031) 2022-08-29 11:17:31 -04:00
Jeremy Stretch
28bc76695a Merge pull request #10180 from ibrahimtok/patch-1
Update filtersets.md
2022-08-29 09:35:08 -04:00
Andreas Nieß
4f7287fec5 Add _occupied as BooleanField for nested serializers 2022-08-29 08:03:28 +02:00
Andreas Nieß
0b5478ad2d Fixes #9895: Change DictField serializers to JSONField 2022-08-29 08:02:38 +02:00
Andreas Nieß
cd6911f83c Replace custom JSONField inspector with the one from drf_yasg 2022-08-29 07:52:29 +02:00
itok
173f27cb64 Update filtersets.md
corrected typos on the page,
an issue report has also been submitted at https://github.com/netbox-community/netbox/issues/10179
regards,
2022-08-28 13:53:53 +03:00
jeremystretch
152dcbe522 Initial draft of git cheat sheet 2022-08-26 16:52:00 -04:00
Arthur
2a7bad326d #9962 add next param to sso url 2022-08-26 12:55:35 -07:00
Arthur
cb6d8bf063 #10170 optimize change log queries 2022-08-26 10:46:47 -07:00
jeremystretch
83db8d2072 Clarify that issues must be accepted and assigned prior to PR submission 2022-08-26 13:33:55 -04:00
jeremystretch
3da8e4c1bb Update README & screenshots 2022-08-26 10:31:56 -04:00
jeremystretch
ee111a28d4 Changelog for #10161 2022-08-26 10:05:44 -04:00
Jeremy Stretch
d00d9cb00f Merge pull request #10162 from kkthxbye-code/10161-fix-cf-setnull
Fixes #10161 - Render "Set Null" button for custom fields
2022-08-26 09:44:21 -04:00
Jeremy Stretch
74ba5a61bf Merge pull request #10163 from netbox-community/10156-svg-link-target
Fixes #10156: Avoid forcing SVG image links to open in a new window
2022-08-26 09:41:37 -04:00
jeremystretch
fd81f57e61 Fixes #10156: Avoid forcing SVG image links to open in a new window 2022-08-26 08:45:48 -04:00
kkthxbye-code
e75e189933 Render "Set Null" button for custom fields 2022-08-26 14:32:17 +02:00
Daniel Sheppard
88d2fca2c6 Fixes #10155 - Fix rear port display when editing front port template for module type 2022-08-25 15:34:18 -05:00
jeremystretch
bd615ebf65 PRVB 2022-08-25 16:25:26 -04:00
Jeremy Stretch
44b9e822d9 Merge pull request #10154 from netbox-community/develop
Release v3.3.1
2022-08-25 16:20:52 -04:00
jeremystretch
ed4fe6bd36 Release v3.3.1 2022-08-25 16:07:34 -04:00
jeremystretch
a416ff6314 Fixes #10118: Fix display of connected LLDP neighbors for devices 2022-08-25 15:25:30 -04:00
jeremystretch
61ba5817c9 Add branch naming standard to developer docs 2022-08-25 15:03:16 -04:00
jeremystretch
7bdbdda7f9 Changelog for #10135 2022-08-25 14:51:16 -04:00
Arthur Hanson
a9ddd41729 Fix SSO signon for SAML with idp (#10137)
* #10135 add idp to saml login

* #10135 add idp to saml login

* #10135 add idp to saml login

* #10135 refactor / cleanup
2022-08-25 14:48:46 -04:00
jeremystretch
1379b9c9fb Tweak display of prerequisite model warning 2022-08-25 14:25:47 -04:00
jeremystretch
32615befd5 #10038 & #10039: Changelog & replicate for VM interfaces 2022-08-25 14:25:47 -04:00
Arthur Hanson
7697779abf #10038 add L2VPN termination to interface list context menu (#10152)
Co-authored-by: Jeremy Stretch <jstretch@ns1.com>
2022-08-25 13:41:55 -04:00
Arthur Hanson
bb37ebf4ba #10038 add assign FHRP group to device-interface context menu (#10151) 2022-08-25 13:38:55 -04:00
jeremystretch
5f37699736 Fixes #9663: Omit available IP annotations when filtering prefix child IPs list 2022-08-25 10:34:18 -04:00
Jeremy Stretch
9da9a209a5 Fixes #10087: Correct display of far end in console/power/interface connections tables (#10117) 2022-08-25 08:46:19 -04:00
jeremystretch
482b4b6e95 Fixes #10147: Permit the creation of 0U device types via REST API 2022-08-25 08:37:43 -04:00
Arthur Hanson
ec2e8ad184 #10139 update development documents for githooks and web-ui (#10141)
* #10139 update development documents for githooks and web-ui

* Remove redudant phrase

Co-authored-by: Jeremy Stretch <jstretch@ns1.com>
2022-08-25 08:21:55 -04:00
jeremystretch
6c686af1b7 Changelog for #10070 2022-08-24 17:13:09 -04:00
Arthur Hanson
4132027ada fixes for #10070 make l2vpn slug unique (#10119)
Co-authored-by: jeremystretch <jstretch@ns1.com>
2022-08-24 17:12:14 -04:00
jeremystretch
f70ef7a585 Changelog and cleanup for #9935 2022-08-24 16:44:24 -04:00
Arthur Hanson
eb2bf3469e #9935 add new wireless choices for interfaces (#10116)
* #9935 add new wireless choices for interfaces

* #9935 add new wireless interfaces to constants

* #9935 oops - remove login.html changes
2022-08-24 16:36:38 -04:00
jeremystretch
bfbf97aec9 Closes #10031: Enforce 'application/json' content type for REST API requests 2022-08-24 15:49:36 -04:00
jeremystretch
2baf06e012 Add unique slugs to L2VPNs in relevant tests 2022-08-24 14:46:42 -04:00
jeremystretch
c2c8bd0a76 Closes #10133: Enable nullifying device location during bulk edit 2022-08-24 13:25:54 -04:00
jeremystretch
36729fb6ae Fixes #10134: Custom fields data serializer should return a 400 response for invalid data 2022-08-24 13:08:21 -04:00
jeremystretch
18d5576997 Changelog for #10033, #10037, #10094 2022-08-24 08:59:40 -04:00
Jeremy Stretch
6a7c56d919 Merge pull request #10115 from arthanson/art-10033
#10033 disable Add a Termination button if 2 terminations on L2VPN P2P
2022-08-24 08:57:01 -04:00
Jeremy Stretch
3110765d12 Merge pull request #10103 from arthanson/art-10094
Fix #10094 - Contact Assignments Create and Add Another
2022-08-24 08:55:02 -04:00
Jeremy Stretch
deee36651d Merge pull request #10120 from arthanson/art-10086
fix for #10086 - change capitalization on wireless link table for Interface A, B and Auth Type
2022-08-24 08:46:24 -04:00
Arthur
1c46102c4a #10094 changes from code review 2022-08-23 16:19:43 -07:00
Arthur
439cf1a308 #10033 changes from code review 2022-08-23 16:17:40 -07:00
Jeremy Stretch
219f2eee29 Merge pull request #10101 from arthanson/art-10037
#10037 add Child Interface to context menu
2022-08-23 16:32:15 -04:00
Arthur
374abe5214 #10033 disable Add a Termination button if 2 terminations on L2VPN P2P 2022-08-23 10:34:06 -07:00
Arthur
8b1a462a60 #10094 changes from code review 2022-08-23 09:29:55 -07:00
Arthur
c11ca543e2 #10037 default type to virtual 2022-08-23 09:16:48 -07:00
Jeremy Stretch
bf92e3a9dd Merge pull request #10114 from netbox-community/10109-available-prefixes
Fixes #10109: Fix available prefixes calculation for container prefixes in the global table
2022-08-23 10:51:12 -04:00
jeremystretch
7ba0b420f1 Fixes #10109: Fix available prefixes calculation for container prefixes in the global table 2022-08-23 10:37:38 -04:00
Daniel Sheppard
f3906dd7c4 Fixes #10111 - Wrap search QS to catch ValueError on identifier field 2022-08-23 09:33:36 -05:00
jeremystretch
984d8b8ee6 Fixes #10108: Linkify inside NAT IPs for primary device IPs in UI 2022-08-23 09:17:12 -04:00
Jeremy Stretch
7b4189271c Merge pull request #10065 from atownson/issue_10055
Fixes #10055: NAT Outside evaluations and links
2022-08-23 08:28:39 -04:00
atownson
63e8faeed9 Changed nat_outside to ManyToManyColumn 2022-08-22 20:34:44 -05:00
Arthur
f48aaf1c46 #10094 fix Contact AddAnother 2022-08-22 16:47:40 -07:00
Arthur
41499b189c #10094 fix Contact AddAnother 2022-08-22 16:33:50 -07:00
Arthur
9fddd193b9 #10094 fix Contact AddAnother 2022-08-22 16:31:41 -07:00
Arthur
0c7c61b685 #10037 add Child Interface to context menu 2022-08-22 14:56:31 -07:00
atownson
6179686c81 Corrected IPv6 family 2022-08-22 15:22:53 -05:00
atownson
2bb79e1346 Updated exists evaluation 2022-08-22 15:18:25 -05:00
atownson
71bf5f4697 Updated exists evaluation 2022-08-22 15:17:35 -05:00
jeremystretch
069c2d2fd2 Changelog for #6454, #10057, #10059 2022-08-22 16:11:35 -04:00
Jeremy Stretch
f35ff105ab Merge pull request #10096 from arthanson/art-6454
Fixes #6454 - Adds warning for prerequisite models
2022-08-22 16:09:59 -04:00
Arthur
25ec624e4e #6454 suggested review changes 2022-08-22 12:59:26 -07:00
Arthur
a972174706 #6454 changes from PR review 2022-08-22 11:46:41 -07:00
Jeremy Stretch
646272f9b3 Merge pull request #10097 from arthanson/art-10059
Fix for #10059 - add identifier to L2VPN table
2022-08-22 14:31:11 -04:00
Jeremy Stretch
743106e94f Merge pull request #10095 from arthanson/art-10057
fix for #10057
2022-08-22 14:30:22 -04:00
Jeremy Stretch
f5d81f51c4 Merge pull request #10093 from arthanson/art-10056
Fix for #10056
2022-08-22 14:29:24 -04:00
Arthur
e8f62eb1f9 #10059 change ordering of identifier column 2022-08-22 11:17:01 -07:00
Arthur
ea1467add7 fix for #10086 - change capitalization on wireless link table for Interface A, B and Auth Type 2022-08-22 09:24:52 -07:00
Arthur
917439725a fix for #10059 - add identifier to L2VPN table 2022-08-22 09:08:16 -07:00
jeremystretch
2ef9e2d6fc Closes #10066: Use fixed column widths for custom field values in UI 2022-08-22 11:17:40 -04:00
jeremystretch
c14a5973c7 Fixes #10089: linkify template filter should escape object representation 2022-08-22 11:14:36 -04:00
Arthur
3a7ea62874 fix for #10057 2022-08-18 16:20:24 -07:00
Arthur
0a38c16cc2 Fix for #10056 2022-08-18 16:06:57 -07:00
Arthur
c65a291698 #6454 add L2VPN check 2022-08-18 16:00:17 -07:00
Arthur
38a8ddcd77 #6454 fix merge conflicts 2022-08-18 15:31:54 -07:00
atownson
43ad8e80b9 netbox-community#10055: Added empty text 2022-08-18 17:12:44 -05:00
Arthur
928dff6b68 #6454 add prerequisite alert 2022-08-18 15:11:03 -07:00
atownson
0bdee1d6d8 netbox-community#10055 - Align NAT Outside with NAT Inside 2022-08-18 16:22:22 -05:00
atownson
3f40e15ed5 netbox-community#10055 - Add template for NAT Outside
Fixes 'ipam.IPAddress.None' text
2022-08-18 16:18:29 -05:00
atownson
a687aa1de6 netbox-community#10055 - Add loop for NAT Outside 2022-08-18 16:09:36 -05:00
atownson
c811eb069d netbox-community#10055 - Add loop for NAT Outside 2022-08-18 16:05:29 -05:00
Arthur
eb3d3dcbc4 #6454 add prerequisite alert 2022-08-18 13:58:40 -07:00
jeremystretch
804c064a7e Closes #10061: Replicate type when cloning L2VPN instances 2022-08-18 15:19:34 -04:00
jeremystretch
9059c09627 Refresh development docs 2022-08-18 13:40:44 -04:00
jeremystretch
279253c486 Fixes #10040: Fix exception when ordering prefixes by flat representation 2022-08-18 09:49:45 -04:00
jeremystretch
c7d6fe2d62 Fixes #10053: Custom fields header should not be displayed when editing circuit terminations with no custom fields 2022-08-17 15:37:48 -04:00
Jeremy Stretch
5327857f81 Merge pull request #9744 from candlerb/candlerb/9743
Documentation: distinguish release and git upgrade processes
2022-08-17 15:20:52 -04:00
jeremystretch
3b4dd051f2 PRVB 2022-08-17 14:11:47 -04:00
Brian Candler
587a34442a Documentation: distinguish release and git upgrade processes
Fixes #9743
2022-07-15 17:10:15 +01:00
239 changed files with 5417 additions and 4546 deletions

View File

@@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v3.3.0
placeholder: v3.3.8
validations:
required: true
- type: dropdown

View File

@@ -19,11 +19,15 @@ body:
label: Area
description: To what section of the documentation does this change primarily pertain?
options:
- Installation instructions
- Configuration parameters
- Functionality/features
- REST API
- Administration/development
- Features
- Installation/upgrade
- Getting started
- Configuration
- Customization
- Integrations/API
- Plugins
- Administration
- Development
- Other
validations:
required: true

View File

@@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v3.3.0
placeholder: v3.3.8
validations:
required: true
- type: dropdown

View File

@@ -1,15 +1,17 @@
<!--
Thank you for your interest in contributing to NetBox! Please note
that our contribution policy requires that a feature request or bug
report be opened for approval prior to filing a pull request. This
helps avoid wasting time and effort on something that we might not
be able to accept.
Thank you for your interest in contributing to NetBox! Please note that
our contribution policy requires that a feature request or bug report be
approved and assigned prior to opening a pull request. This helps avoid
waste time and effort on a proposed change that we might not be able to
accept.
Please indicate the relevant feature request or bug report below.
IF YOUR PULL REQUEST DOES NOT REFERENCE AN ACCEPTED BUG REPORT OR
FEATURE REQUEST, IT WILL BE MARKED AS INVALID AND CLOSED.
IF YOUR PULL REQUEST DOES NOT REFERENCE AN ISSUE WHICH HAS BEEN ASSIGNED
TO YOU, IT WILL BE CLOSED AUTOMATICALLY.
Please specify your assigned issue number on the line below.
-->
### Fixes: <ISSUE NUMBER GOES HERE>
### Fixes: #1234
<!--
Please include a summary of the proposed changes below.
-->

View File

@@ -1,5 +1,7 @@
name: CI
on: [push, pull_request]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest

View File

@@ -4,18 +4,18 @@ name: 'Lock threads'
on:
schedule:
- cron: '0 3 * * *'
workflow_dispatch:
permissions:
issues: write
pull-requests: write
jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v2
- uses: dessant/lock-threads@v3
with:
github-token: ${{ github.token }}
issue-lock-inactive-days: '90'
issue-exclude-created-before: ''
issue-exclude-labels: ''
issue-lock-labels: ''
issue-lock-comment: ''
issue-inactive-days: 90
pr-inactive-days: 30
issue-lock-reason: 'resolved'
process-only: 'issues'

View File

@@ -1,14 +1,21 @@
# close-stale-issues (https://github.com/marketplace/actions/close-stale-issues)
name: 'Close stale issues/PRs'
on:
schedule:
- cron: '0 4 * * *'
workflow_dispatch:
permissions:
issues: write
pull-requests: write
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v5
- uses: actions/stale@v6
with:
close-issue-message: >
This issue has been automatically closed due to lack of activity. In an

View File

@@ -102,23 +102,28 @@ appropriate labels will be applied for categorization.
[getting started](https://docs.netbox.dev/en/stable/development/getting-started/)
documentation for tips on setting up your development environment.
* Be sure to open an issue **before** starting work on a pull request, and
discuss your idea with the NetBox maintainers before beginning work. This will
help prevent wasting time on something that might we might not be able to
implement. When suggesting a new feature, also make sure it won't conflict with
any work that's already in progress.
* Be sure to open an issue and wait for it to be assigned to you **before**
starting work on a pull request, and discuss your idea with the NetBox
maintainers before beginning work. This will help prevent wasting time on
proposed changes that we might not be able to accept. When suggesting a new
feature, also make sure it won't conflict with any work that's already in
progress.
* Once you've opened or identified an issue you'd like to work on, ask that it
be assigned to you so that others are aware it's being worked on. A maintainer
will then mark the issue as "accepted."
be assigned to you so that others are aware it's being worked on. If it meets
the acceptance criteria, a maintainer will then mark the issue as "accepted"
and assign it to you. (Note that GitHub requires that a user first comment on
an issue before it can be assigned to that user.)
* Any pull request which does _not_ relate to an **accepted** issue will be closed.
* Any pull request which does not relate to an **assigned** issue will be
closed.
* All new functionality must include relevant tests where applicable.
* When submitting a pull request, please be sure to work off of the `develop`
branch, rather than `master`. The `develop` branch is used for ongoing
development, while `master` is used for tagging stable releases.
development, while `master` is used for tagging stable releases. (If you're
developing for the next minor release, use `feature` instead.)
* In most cases, it is not necessary to add a changelog entry: A maintainer will
take care of this when the PR is merged. (This helps avoid merge conflicts
@@ -136,8 +141,10 @@ these checks):
Only comment on an issue if you are sharing a relevant idea or constructive
feedback. **Do not** comment on an issue just to show your support (give the
top post a :+1: instead) or ask for an ETA. These comments will be deleted to
reduce noise in the discussion.
top post a :+1: instead) or to ask for an update. Doing so generates
unnecessary noise in the discussion, and is especially annoying for people who
have subscribed to updates for the issue. Any comments without substance
relevant to the discussion will be deleted.
## Issue Lifecycle

View File

@@ -2,14 +2,24 @@
<img src="https://raw.githubusercontent.com/netbox-community/netbox/develop/docs/netbox_logo.svg" width="400" alt="NetBox logo" />
</div>
NetBox is the leading solution for modeling and documenting modern networks. By
combining the traditional disciplines of IP address management (IPAM) and
datacenter infrastructure management (DCIM) with powerful APIs and extensions,
NetBox provides the ideal "source of truth" to power network automation.
Available as open source software under the Apache 2.0 license, NetBox is
employed by thousands of organizations around the world.
![Master branch build status](https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=master)
NetBox is an infrastructure resource modeling (IRM) tool designed to empower
network automation, used by thousands of organizations around the world.
Initially conceived by the network engineering team at
[DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically
to address the needs of network and infrastructure engineers. It is intended to
function as a domain-specific source of truth for network operations.
[![Timeline graph](https://images.repography.com/29023055/netbox-community/netbox/recent-activity/31db894eee74b8a5475e3af307a81b6c_timeline.svg)](https://github.com/netbox-community/netbox/commits)
[![Issue status graph](https://images.repography.com/29023055/netbox-community/netbox/recent-activity/31db894eee74b8a5475e3af307a81b6c_issues.svg)](https://github.com/netbox-community/netbox/issues)
[![Pull request status graph](https://images.repography.com/29023055/netbox-community/netbox/recent-activity/31db894eee74b8a5475e3af307a81b6c_prs.svg)](https://github.com/netbox-community/netbox/pulls)
[![Top contributors](https://images.repography.com/29023055/netbox-community/netbox/recent-activity/31db894eee74b8a5475e3af307a81b6c_users.svg)](https://github.com/netbox-community/netbox/graphs/contributors)
<br />Stats via [Repography](https://repography.com)
## About NetBox
![Screenshot of Netbox UI](docs/media/screenshots/netbox-ui.png "NetBox UI")
Myriad infrastructure components can be modeled in NetBox, including:
@@ -21,6 +31,7 @@ Myriad infrastructure components can be modeled in NetBox, including:
* Virtual machines and clusters
* IP prefixes, ranges, and addresses
* VRFs and route targets
* L2VPN and overlays
* FHRP groups (VRRP, HSRP, etc.)
* AS numbers
* VLANs and scoped VLAN groups
@@ -45,14 +56,16 @@ customized and extended through the use of:
NetBox also features a complete REST API as well as a GraphQL API for easily
integrating with other tools and systems.
The complete documentation for NetBox can be found at [docs.netbox.dev](https://docs.netbox.dev/).
A public demo instance is available at [demo.netbox.dev](https://demo.netbox.dev).
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
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 [docs.netbox.dev](https://docs.netbox.dev/). A public demo instance is available at https://demo.netbox.dev.
complete list of requirements, see `requirements.txt`. The code is available
[on GitHub](https://github.com/netbox-community/netbox).
<div align="center">
<h4>Thank you to our sponsors!</h4>
<h3>Thank you to our sponsors!</h3>
[![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;
@@ -90,8 +103,6 @@ our [contributing guide](CONTRIBUTING.md) prior to beginning any work.
### Screenshots
![Screenshot of main page (light mode)](docs/media/screenshots/home-light.png "Main page (light mode)")
![Screenshot of main page (dark mode)](docs/media/screenshots/home-dark.png "Main page (dark mode)")
![Screenshot of rack elevation](docs/media/screenshots/rack.png "Rack elevation")

View File

@@ -68,7 +68,7 @@ drf-yasg[validation]
# Django wrapper for Graphene (GraphQL support)
# https://github.com/graphql-python/graphene-django
graphene_django
graphene_django<3.0
# WSGI HTTP server
# https://gunicorn.org/
@@ -80,7 +80,8 @@ Jinja2
# Simple markup language for rendering HTML
# https://github.com/Python-Markdown/markdown
Markdown
# mkdocs currently requires Markdown v3.3
Markdown<3.4
# File inclusion plugin for Python-Markdown
# https://github.com/cmacmackin/markdown-include

View File

@@ -2,8 +2,8 @@
{% block site_meta %}
{{ super() }}
{# Disable search indexing unless we're building for ReadTheDocs #}
{% if not config.extra.readthedocs %}
{# Disable search indexing unless we're building for ReadTheDocs (see #10496) #}
{% if page.canonical_url != 'https://docs.netbox.dev/' %}
<meta name="robots" content="noindex">
{% endif %}
{% endblock %}

View File

@@ -58,7 +58,7 @@ Email is sent from NetBox only for critical events or if configured for [logging
Default: None
A dictionary of HTTP proxies to use for outbound requests originating from NetBox (e.g. when sending webhook requests). Proxies should be specified by schema (HTTP and HTTPS) as per the [Python requests library documentation](https://2.python-requests.org/en/master/user/advanced/). For example:
A dictionary of HTTP proxies to use for outbound requests originating from NetBox (e.g. when sending webhook requests). Proxies should be specified by schema (HTTP and HTTPS) as per the [Python requests library documentation](https://requests.readthedocs.io/en/latest/user/advanced/#proxies). For example:
```python
HTTP_PROXIES = {

View File

@@ -129,6 +129,19 @@ The Script object provides a set of convenient functions for recording messages
Log messages are returned to the user upon execution of the script. Markdown rendering is supported for log messages.
## Change Logging
To generate the correct change log data when editing an existing object, a snapshot of the object must be taken before making any changes to the object.
```python
if obj.pk and hasattr(obj, 'snapshot'):
obj.snapshot()
obj.property = "New Value"
obj.full_clean()
obj.save()
```
## Variable Reference
### Default Options

View File

@@ -4,12 +4,12 @@
Getting started with NetBox development is pretty straightforward, and should feel very familiar to anyone with Django development experience. There are a few things you'll need:
* A Linux system or environment
* A Linux system or compatible environment
* A PostgreSQL server, which can be installed locally [per the documentation](../installation/1-postgresql.md)
* A Redis server, which can also be [installed locally](../installation/2-redis.md)
* A supported version of Python
* Python 3.8 or later
### Fork the Repo
### 1. Fork the Repo
Assuming you'll be working on your own fork, your first step will be to fork the [official git repository](https://github.com/netbox-community/netbox). (If you're a maintainer who's going to be working directly with the official repo, skip this step.) Click the "fork" button at top right (be sure that you've logged into GitHub first).
@@ -21,7 +21,7 @@ Copy the URL provided in the dialog box.
You can then clone your GitHub fork locally for development:
```no-highlight
```no-highlight hl_lines="1 9"
$ git clone https://github.com/$username/netbox.git
Cloning into 'netbox'...
remote: Enumerating objects: 85949, done.
@@ -35,93 +35,114 @@ base_requirements.txt contrib docs mkdocs.yml NOTICE requ
CHANGELOG.md CONTRIBUTING.md LICENSE.txt netbox README.md scripts
```
### 2. Create a New Branch
The NetBox project utilizes three persistent git branches to track work:
* `master` - Serves as a snapshot of the current stable release
* `develop` - All development on the upcoming stable release occurs here
* `feature` - Tracks work on an upcoming major release
* `develop` - All development on the upcoming stable (patch) release occurs here
* `feature` - Tracks work on an upcoming minor release
Typically, you'll base pull requests off of the `develop` branch, or off of `feature` if you're working on a new major release. **Never** merge pull requests into the `master` branch: This branch only ever merges pull requests from the `develop` branch, to effect a new release.
Typically, you'll base pull requests off of the `develop` branch, or off of `feature` if you're working on a new major release. For example, assume that the current NetBox release is v3.3.5. Work applied to the `develop` branch will appear in v3.3.6, and work done under the `feature` branch will be included in the next minor release (v3.4.0).
For example, assume that the current NetBox release is v3.1.1. Work applied to the `develop` branch will appear in v3.1.2, and work done under the `feature` branch will be included in the next minor release (v3.2.0).
!!! warning
**Never** merge pull requests into the `master` branch: This branch only ever merges pull requests from the `develop` branch, to effect a new release.
### Enable Pre-Commit Hooks
To create a new branch, first ensure that you've checked out the desired base branch, then run:
```no-highlight
git checkout -B $branchname
```
When naming a new git branch, contributors are strongly encouraged to use the relevant issue number followed by a very brief description of the work:
```no-highlight
$issue-$description
```
The description should be just two or three words to imply the focus of the work being performed. For example, bug #1234 to fix a TypeError exception when creating a device might be named `1234-device-typerror`. This ensures that branches are always follow some logical ordering (e.g. when running `git branch -a`) and helps other developers quickly identify the purpose of each.
### 3. Enable Pre-Commit Hooks
NetBox ships with a [git pre-commit hook](https://githooks.com/) script that automatically checks for style compliance and missing database migrations prior to committing changes. This helps avoid erroneous commits that result in CI test failures. You are encouraged to enable it by creating a link to `scripts/git-hooks/pre-commit`:
```no-highlight
$ cd .git/hooks/
$ ln -s ../../scripts/git-hooks/pre-commit
cd .git/hooks/
ln -s ../../scripts/git-hooks/pre-commit
```
For the pre-commit hooks to work, you will also need to install the pycodestyle package:
### Create a Python Virtual Environment
```no-highlight
python -m pip install pycodestyle
```
...and set up the yarn packages as shown in the [Web UI Development Guide](web-ui.md)
### 4. Create a Python Virtual Environment
A [virtual environment](https://docs.python.org/3/tutorial/venv.html) (or "venv" for short) is like a container for a set of Python packages. These allow you to build environments suited to specific projects without interfering with system packages or other projects. When installed per the documentation, NetBox uses a virtual environment in production.
Create a virtual environment using the `venv` Python module:
```no-highlight
$ mkdir ~/.venv
$ python3 -m venv ~/.venv/netbox
mkdir ~/.venv
python3 -m venv ~/.venv/netbox
```
This will create a directory named `.venv/netbox/` in your home directory, which houses a virtual copy of the Python executable and its related libraries and tooling. When running NetBox for development, it will be run using the Python binary at `~/.venv/netbox/bin/python`.
!!! info "Where to Create Your Virtual Environments"
Keeping virtual environments in `~/.venv/` is a common convention but entirely optional: Virtual environments can be created almost wherever you please. Also consider using [`virtualenvwrapper`](https://virtualenvwrapper.readthedocs.io/en/stable/) to simplify the management of multiple venvs.
!!! tip "Virtual Environments"
Keeping virtual environments in `~/.venv/` is a common convention but entirely optional: Virtual environments can be created almost wherever you please. Also consider using [`virtualenvwrapper`](https://virtualenvwrapper.readthedocs.io/en/stable/) to simplify the management of multiple environments.
Once created, activate the virtual environment:
```no-highlight
$ source ~/.venv/netbox/bin/activate
(netbox) $
source ~/.venv/netbox/bin/activate
```
Notice that the console prompt changes to indicate the active environment. This updates the necessary system environment variables to ensure that any Python scripts are run within the virtual environment.
### Install Dependencies
### 5. Install Required Packages
With the virtual environment activated, install the project's required Python packages using the `pip` module:
With the virtual environment activated, install the project's required Python packages using the `pip` module. Required packages are defined in `requirements.txt`. Each line in this file specifies the name and specific version of a required package.
```no-highlight
(netbox) $ python -m pip install -r requirements.txt
Collecting Django==3.1 (from -r requirements.txt (line 1))
Cache entry deserialization failed, entry ignored
Using cached https://files.pythonhosted.org/packages/2b/5a/4bd5624546912082a1bd2709d0edc0685f5c7827a278d806a20cf6adea28/Django-3.1-py3-none-any.whl
...
python -m pip install -r requirements.txt
```
### Configure NetBox
### 6. Configure NetBox
Within the `netbox/netbox/` directory, copy `configuration_example.py` to `configuration.py` and update the following parameters:
* `ALLOWED_HOSTS`: This can be set to `['*']` for development purposes
* `DATABASE`: PostgreSQL database connection parameters
* `REDIS`: Redis configuration, if different from the defaults
* `REDIS`: Redis configuration (if different from the defaults)
* `SECRET_KEY`: Set to a random string (use `generate_secret_key.py` in the parent directory to generate a suitable key)
* `DEBUG`: Set to `True`
* `DEVELOPER`: Set to `True` (this enables the creation of new database migrations)
### Start the Development Server
### 7. Start the Development Server
Django provides a lightweight, auto-updating HTTP/WSGI server for development use. It is started with the `runserver` management command:
Django provides a lightweight, auto-updating [HTTP/WSGI server](https://docs.djangoproject.com/en/stable/ref/django-admin/#runserver) for development use. It is started with the `runserver` management command:
```no-highlight
```no-highlight hl_lines="1"
$ ./manage.py runserver
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
February 18, 2022 - 20:29:57
Django version 4.0.2, using settings 'netbox.settings'
August 18, 2022 - 15:17:52
Django version 4.0.7, using settings 'netbox.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
```
This ensures that your development environment is now complete and operational. Any changes you make to the code base will be automatically adapted by the development server.
This ensures that your development environment is now complete and operational. The development server will monitor the development environment and automatically reload in response to any changes made.
!!! info "IDE Integration"
Some IDEs, such as PyCharm, will integrate with Django's development server and allow you to run it directly within the IDE. This is strongly encouraged as it makes for a much more convenient development environment.
!!! tip "IDE Integration"
Some IDEs, such as the highly-recommended [PyCharm](https://www.jetbrains.com/pycharm/), will integrate with Django's development server and allow you to run it directly within the IDE. This is strongly encouraged as it makes for a much more convenient development environment.
## UI Development
For UI development you will need to review the [Web UI Development Guide](web-ui.md)
## Populating Demo Data
@@ -131,48 +152,51 @@ The demo data is provided in JSON format and loaded into an empty database using
## Running Tests
Prior to committing any substantial changes to the code base, be sure to run NetBox's test suite to catch any potential errors. Tests are run using the `test` management command, which employs Python's [`unittest`](https://docs.python.org/3/library/unittest.html#module-unittest) library. Remember to ensure the Python virtual environment is active before running this command. Also keep in mind that these commands are executed in the `netbox/` directory, not the root directory of the repository.
Prior to committing any substantial changes to the code base, be sure to run NetBox's test suite to catch potential errors. Tests are run using the `test` management command, which employs Python's [`unittest`](https://docs.python.org/3/library/unittest.html#module-unittest) library. Remember to ensure that the Python virtual environment is active before running this command. Also keep in mind that these commands are executed in the `netbox/` directory, not the root directory of the repository.
To avoid potential issues with your local configuration file, set the `NETBOX_CONFIGURATION` to point to the packaged test configuration at `netbox/configuration_testing.py`. This will handle things like ensuring that the dummy plugin is enabled for comprehensive testing.
```no-highlight
$ export NETBOX_CONFIGURATION=netbox.configuration_testing
$ cd netbox/
$ python manage.py test
export NETBOX_CONFIGURATION=netbox.configuration_testing
cd netbox/
python manage.py test
```
In cases where you haven't made any changes to the database schema (which is typical), you can append the `--keepdb` argument to this command to reuse the test database between runs. This cuts down on the time it takes to run the test suite since the database doesn't have to be rebuilt each time. (Note that this argument will cause errors if you've modified any model fields since the previous test run.)
```no-highlight
$ python manage.py test --keepdb
python manage.py test --keepdb
```
You can also reduce testing time by enabling parallel test execution with the `--parallel` flag. (By default, this will run as many parallel tests as you have processors. To avoid sluggishness, it's a good idea to specify a lower number of parallel tests.) This flag can be combined with `--keepdb`, although if you encounter any strange errors, try running the test suite again with parallelization disabled.
```no-highlight
$ python manage.py test --parallel <n>
python manage.py test --parallel <n>
```
Finally, it's possible to limit the run to a specific set of tests, specified by their Python path. For example, to run only IPAM and DCIM view tests:
```no-highlight
$ python manage.py test dcim.tests.test_views ipam.tests.test_views
python manage.py test dcim.tests.test_views ipam.tests.test_views
```
This is handy for instances where just a few tests are failing and you want to re-run them individually.
!!! info
NetBox uses [django-rich](https://github.com/adamchainz/django-rich) to enhance Django's default `test` management command.
## Submitting Pull Requests
Once you're happy with your work and have verified that all tests pass, commit your changes and push it upstream to your fork. Always provide descriptive (but not excessively verbose) commit messages. When working on a specific issue, be sure to prefix your commit message with the word "Fixes" or "Closes" and the issue number (with a hash mark). This tells GitHub to automatically close the referenced issue once the commit has been merged.
Once you're happy with your work and have verified that all tests pass, commit your changes and push it upstream to your fork. Always provide descriptive (but not excessively verbose) commit messages. Be sure to prefix your commit message with the word "Fixes" or "Closes" and the relevant issue number (with a hash mark). This tells GitHub to automatically close the referenced issue once the commit has been merged.
```no-highlight
$ git commit -m "Closes #1234: Add IPv5 support"
$ git push origin
git commit -m "Closes #1234: Add IPv5 support"
git push origin
```
Once your fork has the new commit, submit a [pull request](https://github.com/netbox-community/netbox/compare) to the NetBox repo to propose the changes. Be sure to provide a detailed accounting of the changes being made and the reasons for doing so.
Once submitted, a maintainer will review your pull request and either merge it or request changes. If changes are needed, you can make them via new commits to your fork: The pull request will update automatically.
!!! note "Remember to Open an Issue First"
!!! warning
Remember, pull requests are permitted only for **accepted** issues. If an issue you want to work on hasn't been approved by a maintainer yet, it's best to avoid risking your time and effort on a change that might not be accepted. (The one exception to this is trivial changes to the documentation or other non-critical resources.)

View File

@@ -0,0 +1,388 @@
# git Cheat Sheet
This cheat sheet serves as a convenient reference for NetBox contributors who already somewhat familiar with using git. For a general introduction to the tooling and workflows involved, please see GitHub's guide [Getting started with git](https://docs.github.com/en/get-started/getting-started-with-git/setting-your-username-in-git).
## Common Operations
### Clone a Repo
This copies a remote git repository (e.g. from GitHub) to your local workstation. It will create a new directory bearing the repo's name in the current path.
``` title="Command"
git clone https://github.com/$org-name/$repo-name
```
``` title="Example"
$ git clone https://github.com/netbox-community/netbox
Cloning into 'netbox'...
remote: Enumerating objects: 95112, done.
remote: Counting objects: 100% (682/682), done.
remote: Compressing objects: 100% (246/246), done.
remote: Total 95112 (delta 448), reused 637 (delta 436), pack-reused 94430
Receiving objects: 100% (95112/95112), 60.40 MiB | 45.82 MiB/s, done.
Resolving deltas: 100% (74979/74979), done.
```
### Pull New Commits
To update your local branch with any recent upstream commits, run `git pull`.
``` title="Command"
git pull
```
``` title="Example"
$ git pull
remote: Enumerating objects: 1, done.
remote: Counting objects: 100% (1/1), done.
remote: Total 1 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (1/1), done.
From https://github.com/netbox-community/netbox
28bc76695..e0741cc9a develop -> origin/develop
Updating 28bc76695..e0741cc9a
Fast-forward
docs/release-notes/version-3.3.md | 1 +
netbox/netbox/settings.py | 1 +
2 files changed, 2 insertions(+)
```
### List Branches
`git branch` lists all local branches. Appending `-a` to this command will list both local (green) and remote (red) branches.
``` title="Command"
git branch -a
```
``` title="Example"
$ git branch -a
* develop
remotes/origin/10170-changelog
remotes/origin/HEAD -> origin/develop
remotes/origin/develop
remotes/origin/feature
remotes/origin/master
```
### Switch Branches
To switch to a different branch, use the `checkout` command.
``` title="Command"
git checkout $branchname
```
``` title="Example"
$ git checkout feature
Branch 'feature' set up to track remote branch 'feature' from 'origin'.
Switched to a new branch 'feature'
```
### Create a New Branch
Use the `-b` argument with `checkout` to create a new _local_ branch from the current branch.
``` title="Command"
git checkout -b $newbranch
```
``` title="Example"
$ git checkout -b 123-fix-foo
Switched to a new branch '123-fix-foo'
```
### Rename a Branch
To rename the current branch, use the `git branch` command with the `-m` argument (for "modify").
``` title="Command"
git branch -m $newname
```
``` title="Example"
$ git branch -m jstretch-testing
$ git branch
develop
feature
* jstretch-testing
```
### Merge a Branch
To merge one branch into another, use the `git merge` command. Start by checking out the _destination_ branch, and merge the _source_ branch into it.
``` title="Command"
git merge $sourcebranch
```
``` title="Example"
$ git checkout testing
Switched to branch 'testing'
Your branch is up to date with 'origin/testing'.
$ git merge branch2
Updating 9a12b5b5f..8ee42390b
Fast-forward
newfile.py | 0
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 newfile.py
```
!!! warning "Avoid Merging Remote Branches"
You generally want to avoid merging branches that exist on the remote (upstream) repository, such as `develop` and `feature`: Merges into these branches should be done via a pull request on GitHub. Only merge branches when it is necessary to consolidate work you've done locally.
### Show Pending Changes
After making changes to files in the repo, `git status` will display a summary of created, modified, and deleted files.
``` title="Command"
git status
```
``` title="Example"
$ git status
On branch 123-fix-foo
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: README.md
Untracked files:
(use "git add <file>..." to include in what will be committed)
foo.py
no changes added to commit (use "git add" and/or "git commit -a")
```
### Stage Changed Files
Before creating a new commit, modified files must be staged. This is typically done with the `git add` command. You can specify a particular path, or just append `-A` to automatically staged _all_ changed files within the current directory. Run `git status` again to verify what files have been staged.
``` title="Command"
git add -A
```
``` title="Example"
$ git add -A
$ git status
On branch 123-fix-foo
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: README.md
new file: foo.py
```
### Review Staged Files
It's a good idea to thoroughly review all staged changes immediately prior to creating a new commit. This can be done using the `git diff` command. Appending the `--staged` argument will show staged changes; omitting it will show changes that have not yet been staged.
``` title="Command"
git diff --staged
```
``` title="Example"
$ git diff --staged
diff --git a/README.md b/README.md
index 93e125079..4344fb514 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,8 @@
+
+Added some lines here
+and here
+and here too
+
<div align="center">
<img src="https://raw.githubusercontent.com/netbox-community/netbox/develop/docs/netbox_logo.svg" width="400" alt="NetBox logo" />
</div>
diff --git a/foo.py b/foo.py
new file mode 100644
index 000000000..e69de29bb
```
### Create a New Commit
The `git commit` command records your changes to the current branch. Specify a commit message with the `-m` argument. (If omitted, a file editor will be opened to provide a message.
``` title="Command"
git commit -m "Fixes #123: Fixed the thing that was broken"
```
``` title="Example"
$ git commit -m "Fixes #123: Fixed the thing that was broken"
[123-fix-foo 9a12b5b5f] Fixes #123: Fixed the thing that was broken
2 files changed, 5 insertions(+)
create mode 100644 foo.py
```
!!! tip "Automatically Closing Issues"
GitHub will [automatically close](https://github.blog/2013-01-22-closing-issues-via-commit-messages/) any issues referenced in a commit message by `Fixes:` or `Closes:` when the commit is merged into the repository's default branch. Contributors are strongly encouraged to follow this convention when forming commit messages. (Use "Closes" for feature requests and "Fixes" for bugs.)
### Push a Commit Upstream
Once you've made a commit locally, it needs to be pushed upstream to the _remote_ repository (typically called "origin"). This is done with the `git push` command. If this is a new branch that doesn't yet exist on the remote repository, you'll need to set the upstream for it when pushing.
``` title="Command"
git push -u origin $branchname
```
``` title="Example"
$ git push -u origin testing
Counting objects: 3, done.
Delta compression using up to 16 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 377 bytes | 377.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
remote:
remote: Create a pull request for 'testing' on GitHub by visiting:
remote: https://github.com/netbox-community/netbox/pull/new/testing
remote:
To https://github.com/netbox-community/netbox
* [new branch] testing -> testing
Branch 'testing' set up to track remote branch 'testing' from 'origin'.
```
!!! tip
You can apply the following git configuration to automatically set the upstream for all new branches. This obviates the need to specify `-u origin`.
```
git config --global push.default current
```
## The GitHub CLI Client
GitHub provides a [free CLI client](https://cli.github.com/) to simplify many aspects of interacting with GitHub repositories. Note that this utility is separate from `git`, and must be [installed separately](https://github.com/cli/cli#installation).
This guide provides some examples of common operations, but be sure to check out the [GitHub CLI manual](https://cli.github.com/manual/) for a complete accounting of available commands.
### List Open Pull Requests
``` title="Command"
gh pr list
```
``` title="Example"
$ gh pr list
Showing 3 of 3 open pull requests in netbox-community/netbox
#10223 #7503 API Bulk-Create of Devices does not check Rack-Space 7503-bulkdevice about 17 hours ago
#9716 Closes #9599: Add cursor pagination mode lyuyangh:cursor-pagination about 1 month ago
#9498 Adds replication and adoption for module import sleepinggenius2:issue_9361 about 2 months ago
```
### Check Out a PR
This command will automatically check out the remote branch associated with an open pull request.
``` title="Command"
gh pr checkout $number
```
``` title="Example"
$ gh pr checkout 10223
Branch '7503-bulkdevice' set up to track remote branch '7503-bulkdevice' from 'origin'.
Switched to a new branch '7503-bulkdevice'
```
## Fixing Mistakes
### Modify the Previous Commit
Sometimes you'll find that you've overlooked a necessary change and need to commit again. If you haven't pushed your most recent commit and just need to make a small tweak or two, you can _amend_ your most recent commit instead of creating a new one.
First, stage the desired files with `git add` and verify the changes, the issue the `git commit` command with the `--amend` argument. You can also append the `--no-edit` argument if you would like to keep the previous commit message.
``` title="Command"
git commit --amend --no-edit
```
``` title="Example"
$ git add -A
$ git diff --staged
$ git commit --amend --no-edit
[testing 239b16921] Added a new file
Date: Fri Aug 26 16:30:05 2022 -0400
2 files changed, 1 insertion(+)
create mode 100644 newfile.py
```
!!! danger "Don't Amend After Pushing"
Never amend a commit you've already pushed upstream unless you're **certain** no one else is working on the same branch. Force-pushing will overwrite the change history, which will break any commits from other contributors. When in doubt, create a new commit instead.
### Undo the Last Commit
The `git reset` command can be used to undo the most recent commit. (`HEAD~` is equivalent to `HEAD~1` and references the commit prior to the current HEAD.) After making and staging your changes, commit using `-c ORIG_HEAD` to replace the erroneous commit.
``` title="Command"
git reset HEAD~
```
``` title="Example"
$ git add -A
$ git commit -m "Erroneous commit"
[testing 09ce06736] Erroneous commit
Date: Mon Aug 29 15:20:04 2022 -0400
1 file changed, 1 insertion(+)
create mode 100644 BADCHANGE
$ git reset HEAD~
$ rm BADFILE
$ git add -A
$ git commit -m "Fixed commit"
[testing c585709f3] Fixed commit
Date: Mon Aug 29 15:22:38 2022 -0400
1 file changed, 65 insertions(+), 20 deletions(-)
```
!!! danger "Don't Reset After Pushing"
Resetting only works until you've pushed your local changes upstream. If you've already pushed upstream, use `git revert` instead. This will create a _new_ commit that reverts the erroneous one, but ensures that the git history remains intact.
### Rebase from Upstream
If a change has been pushed to the upstream branch since you most recently pulled it, attempting to push a new local commit will fail:
```
$ git push
To https://github.com/netbox-community/netbox.git
! [rejected] develop -> develop (fetch first)
error: failed to push some refs to 'https://github.com/netbox-community/netbox.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
```
To resolve this, first fetch the upstream branch to update your local copy, and then [rebase](https://git-scm.com/book/en/v2/Git-Branching-Rebasing) your local branch to include the new changes. Once the rebase has completed, you can push your local commits upstream.
``` title="Commands"
git fetch
git rebase origin/$branchname
```
``` title="Example"
$ git fetch
remote: Enumerating objects: 1, done.
remote: Counting objects: 100% (1/1), done.
remote: Total 1 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (1/1), done.
From https://github.com/netbox-community/netbox
815b2d8a2..8c35ebbb7 develop -> origin/develop
$ git rebase origin/develop
First, rewinding head to replay your work on top of it...
Applying: Further tweaks to the PR template
Applying: Changelog for #10176, #10217
$ git push
Counting objects: 9, done.
Delta compression using up to 16 threads.
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 1.02 KiB | 1.02 MiB/s, done.
Total 9 (delta 6), reused 0 (delta 0)
remote: Resolving deltas: 100% (6/6), completed with 5 local objects.
To https://github.com/netbox-community/netbox.git
8c35ebbb7..ada745324 develop -> develop
```

View File

@@ -1,22 +1,18 @@
# NetBox Development
NetBox is maintained as a [GitHub project](https://github.com/netbox-community/netbox) under the Apache 2 license. Users are encouraged to submit GitHub issues for feature requests and bug reports, however we are very selective about pull requests. Each pull request must be preceded by an **approved** issue. Please see the `CONTRIBUTING` guide for more direction on contributing to NetBox.
Thanks for your interest in contributing to NetBox! This introduction covers a few important things to know before you get started.
## Communication
## The Code
There are several official forums for communication among the developers and community members:
NetBox and many of its related projects are maintained on [GitHub](https://github.com/netbox-community/netbox). GitHub also serves as one of our primary discussion forums. While all the code and discussion is publicly accessible, you'll need register for a [free GitHub account](https://github.com/signup) to engage in participation. Most people begin by [forking](https://docs.github.com/en/get-started/quickstart/fork-a-repo) the NetBox repository under their own GitHub account to begin working on the code.
* [GitHub issues](https://github.com/netbox-community/netbox/issues) - All feature requests, bug reports, and other substantial changes to the code base **must** be documented in a GitHub issue.
* [GitHub discussions](https://github.com/netbox-community/netbox/discussions) - The preferred forum for general discussion and support issues. Ideal for shaping a feature request prior to submitting an issue.
* [#netbox on NetDev Community Slack](https://netdev.chat/) - Good for quick chats. Avoid any discussion that might need to be referenced later on, as the chat history is not retained long.
![GitHub](../media/development/github.png)
## Governance
There are three permanent branches in the repository:
NetBox follows the [benevolent dictator](http://oss-watch.ac.uk/resources/benevolentdictatorgovernancemodel) model of governance, with [Jeremy Stretch](https://github.com/jeremystretch) ultimately responsible for all changes to the code base. While community contributions are welcomed and encouraged, the lead maintainer's primary role is to ensure the project's long-term maintainability and continued focus on its primary functions.
## Project Structure
All development of the current NetBox release occurs in the `develop` branch; releases are packaged from the `master` branch. The `master` branch should _always_ represent the current stable release in its entirety, such that installing NetBox by either downloading a packaged release or cloning the `master` branch provides the same code base. Only pull requests representing new releases should be merged into `master`.
* `master` - The current stable release. Individual changes should never be pushed directly to this branch, but rather merged from `develop`.
* `develop` - Active development for the upcoming patch release. Pull requests will typically be based on this branch unless they introduce breaking changes that must be deferred until the next minor release.
* `feature` - New feature work to be introduced in the next minor release (e.g. from v3.3 to v3.4).
NetBox components are arranged into Django apps. Each app holds the models, views, and other resources relevant to a particular function:
@@ -31,3 +27,34 @@ NetBox components are arranged into Django apps. Each app holds the models, view
* `wireless`: Wireless links and LANs
All core functionality is stored within the `netbox/` subdirectory. HTML templates are stored in a common `templates/` directory, with model- and view-specific templates arranged by app. Documentation is kept in the `docs/` root directory.
## Proposing Changes
All substantial changes made to the code base are tracked using [GitHub issues](https://docs.github.com/en/issues). Feature requests, bug reports, and similar proposals must all be filed as issues and approved by a maintainer before work begins. This ensures that all changes to the code base are properly documented for future reference.
To submit a new feature request or bug report for NetBox, select and complete the appropriate [issue template](https://github.com/netbox-community/netbox/issues/new/choose). Once your issue has been approved, you're welcome to submit a [pull request](https://docs.github.com/en/pull-requests) containing your proposed changes.
![Opening a new GitHub issue](../media/development/github_new_issue.png)
Check out our [issue intake policy](https://github.com/netbox-community/netbox/wiki/Issue-Intake-Policy) for an overview of the issue triage and approval processes.
!!! tip
Avoid starting work on a proposal before it has been accepted. Not all proposed changes will be accepted, and we'd hate for you to waste time working on code that might not make it into the project.
## Getting Help
There are two primary forums for getting assistance with NetBox development:
* [GitHub discussions](https://github.com/netbox-community/netbox/discussions) - The preferred forum for general discussion and support issues. Ideal for shaping a feature requests prior to submitting an issue.
* [#netbox on NetDev Community Slack](https://netdev.chat/) - Good for quick chats. Avoid any discussion that might need to be referenced later on, as the chat history is not retained indefinitely.
!!! note
Don't use GitHub issues to ask for help: These are reserved for proposed code changes only.
## Governance
NetBox follows the [benevolent dictator](http://oss-watch.ac.uk/resources/benevolentdictatorgovernancemodel) model of governance, with [Jeremy Stretch](https://github.com/jeremystretch) ultimately responsible for all changes to the code base. While community contributions are welcomed and encouraged, the lead maintainer's primary role is to ensure the project's long-term maintainability and continued focus on its primary functions.
## Licensing
The entire NetBox project is licensed as open source under the [Apache 2.0 license](https://github.com/netbox-community/netbox/blob/master/LICENSE.txt). This is a very permissive license which allows unlimited redistribution of all code within the project. Note that all submissions to the project are subject to the same license.

View File

@@ -2,7 +2,7 @@
## 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.
A NetBox model represents a discrete object type such as a device or IP address. Per [Django convention](https://docs.djangoproject.com/en/stable/topics/db/models/), 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`).

View File

@@ -1,62 +1,62 @@
# Release Checklist
## Minor Version Bumps
This documentation describes the process of packaging and publishing a new NetBox release. There are three types of release:
### Address Pinned Dependencies
* Major release (e.g. v2.11 to v3.0)
* Minor release (e.g. v3.2 to v3.3)
* Patch release (e.g. v3.3.0 to v3.3.1)
Check `base_requirements.txt` for any dependencies pinned to a specific version, and upgrade them to their most stable release (where possible).
While major releases generally introduce some very substantial change to the application, they are typically treated the same as minor version increments for the purpose of release packaging.
### Link to the Release Notes Page
## Minor Version Releases
Add the release notes (`/docs/release-notes/X.Y.md`) to the table of contents within `mkdocs.yml`, and add a summary of the major changes to `index.md`.
### Address Constrained Dependencies
### Manually Perform a New Install
Install `mkdocs` in your local environment, then start the documentation server:
```no-highlight
$ pip install -r docs/requirements.txt
$ mkdocs serve
```
Follow these instructions to perform a new installation of NetBox. This process must _not_ be automated: The goal of this step is to catch any errors or omissions in the documentation, and ensure that it is kept up-to-date for each release. Make any necessary changes to the documentation before proceeding with the release.
### Close the Release Milestone
Close the release milestone on GitHub after ensuring there are no remaining open issues associated with it.
### Merge the Release Branch
Submit a pull request to merge the `feature` branch into the `develop` branch in preparation for its release.
---
## All Releases
### Update Requirements
Required Python packages are maintained in two files. `base_requirements.txt` contains a list of all the packages required by NetBox. Some of them may be pinned to a specific version of the package due to a known issue. For example:
Sometimes it becomes necessary to constrain dependencies to a particular version, e.g. to work around a bug in a newer release or to avoid a breaking change that we have yet to accommodate. (Another common example is to limit the upstream Django release.) For example:
```
# https://github.com/encode/django-rest-framework/issues/6053
djangorestframework==3.8.1
```
The other file is `requirements.txt`, which lists each of the required packages pinned to its current stable version. When NetBox is installed, the Python environment is configured to match this file. This helps ensure that a new release of a dependency doesn't break NetBox.
These version constraints are added to `base_requirements.txt` to ensure that newer packages are not installed when updating the pinned dependencies in `requirements.txt` (see the [Update Requirements](#update-requirements) section below). Before each new minor version of NetBox is released, all such constraints on dependent packages should be addressed if feasible. This guards against the collection of stale constraints over time.
Every release should refresh `requirements.txt` so that it lists the most recent stable release of each package. To do this:
### Close the Release Milestone
1. Create a new virtual environment.
2. Install the latest version of all required packages `pip install -U -r base_requirements.txt`).
3. Run all tests and check that the UI and API function as expected.
4. Review each requirement's release notes for any breaking or otherwise noteworthy changes.
5. Update the package versions in `requirements.txt` as appropriate.
Close the [release milestone](https://github.com/netbox-community/netbox/milestones) on GitHub after ensuring there are no remaining open issues associated with it.
In cases where upgrading a dependency to its most recent release is breaking, it should be pinned to its current minor version in `base_requirements.txt` (with an explanatory comment) and revisited for the next major NetBox release.
### Update the Release Notes
### Verify CI Build Status
Check that a link to the release notes for the new version is present in the navigation menu (defined in `mkdocs.yml`), and that a summary of all major new features has been added to `docs/index.md`.
Ensure that continuous integration testing on the `develop` branch is completing successfully.
### Manually Perform a New Install
Start the documentation server and navigate to the current version of the installation docs:
```no-highlight
mkdocs serve
```
Follow these instructions to perform a new installation of NetBox in a temporary environment. This process must not be automated: The goal of this step is to catch any errors or omissions in the documentation, and ensure that it is kept up-to-date for each release. Make any necessary changes to the documentation before proceeding with the release.
### Merge the Release Branch
Submit a pull request to merge the `feature` branch into the `develop` branch in preparation for its release. Once it has been merged, continue with the section for patch releases below.
---
## Patch Releases
### Update Requirements
Before each release, update each of NetBox's Python dependencies to its most recent stable version. These are defined in `requirements.txt`, which is updated from `base_requirements.txt` using `pip`. To do this:
1. Upgrade the installed version of all required packages in your environment (`pip install -U -r base_requirements.txt`).
2. Run all tests and check that the UI and API function as expected.
3. Review each requirement's release notes for any breaking or otherwise noteworthy changes.
4. Update the package versions in `requirements.txt` as appropriate.
In cases where upgrading a dependency to its most recent release is breaking, it should be constrained to its current minor version in `base_requirements.txt` with an explanatory comment and revisited for the next major NetBox release (see the [Address Constrained Dependencies](#address-constrained-dependencies) section above).
### Update Version and Changelog
@@ -64,28 +64,35 @@ Ensure that continuous integration testing on the `develop` branch is completing
* 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.
Commit these changes to the `develop` branch and push upstream.
### Verify CI Build Status
Ensure that continuous integration testing on the `develop` branch is completing successfully. If it fails, take action to correct the failure before proceding with the release.
### Submit a Pull Request
Submit a pull request title **"Release vX.Y.Z"** to merge the `develop` branch into `master`. Copy the documented release notes into the pull request's body.
Submit a pull request titled **"Release vX.Y.Z"** to merge the `develop` branch into `master`. Copy the documented release notes into the pull request's body.
Once CI has completed on the PR, merge it.
Once CI has completed on the PR, merge it. This effects a new release in the `master` branch.
### Create a New Release
Draft a [new release](https://github.com/netbox-community/netbox/releases/new) with the following parameters.
Create a [new release](https://github.com/netbox-community/netbox/releases/new) on GitHub with the following parameters.
* **Tag:** Current version (e.g. `v2.9.9`)
* **Tag:** Current version (e.g. `v3.3.1`)
* **Target:** `master`
* **Title:** Version and date (e.g. `v2.9.9 - 2020-11-09`)
* **Title:** Version and date (e.g. `v3.3.1 - 2022-08-25`)
* **Description:** Copy from the pull request body
Copy the description from the pull request to the release.
Once created, the release will become available for users to install.
### Update the Development Version
On the `develop` branch, update `VERSION` in `settings.py` to point to the next release. For example, if you just released v2.9.9, set:
On the `develop` branch, update `VERSION` in `settings.py` to point to the next release. For example, if you just released v3.3.1, set:
```
VERSION = 'v2.9.10-dev'
VERSION = 'v3.3.2-dev'
```
Commit this change with the comment "PRVB" (for _post-release version bump_) and push the commit upstream.

View File

@@ -1,34 +1,53 @@
# Style Guide
NetBox generally follows the [Django style guide](https://docs.djangoproject.com/en/stable/internals/contributing/writing-code/coding-style/), which is itself based on [PEP 8](https://www.python.org/dev/peps/pep-0008/). [Pycodestyle](https://github.com/pycqa/pycodestyle) is used to validate code formatting, ignoring certain violations. See `scripts/cibuild.sh` for details.
NetBox generally follows the [Django style guide](https://docs.djangoproject.com/en/stable/internals/contributing/writing-code/coding-style/), which is itself based on [PEP 8](https://www.python.org/dev/peps/pep-0008/). [Pycodestyle](https://github.com/pycqa/pycodestyle) is used to validate code formatting, ignoring certain violations.
## PEP 8 Exceptions
## Code
* Wildcard imports (for example, `from .constants import *`) are acceptable under any of the following conditions:
* The library being import contains only constant declarations (e.g. `constants.py`)
* The library being imported explicitly defines `__all__`
### General Guidance
* Maximum line length is 120 characters (E501)
* This does not apply to HTML templates or to automatically generated code (e.g. database migrations).
* When in doubt, remain consistent: It is better to be consistently incorrect than inconsistently correct. If you notice in the course of unrelated work a pattern that should be corrected, continue to follow the pattern for now and submit a separate bug report so that the entire code base can be evaluated at a later point.
* Line breaks are permitted following binary operators (W504)
* Prioritize readability over concision. Python is a very flexible language that typically offers several multiple options for expressing a given piece of logic, but some may be more friendly to the reader than others. (List comprehensions are particularly vulnerable to over-optimization.) Always remain considerate of the future reader who may need to interpret your code without the benefit of the context within which you are writing it.
## Enforcing Code Style
* Include a newline at the end of every file.
The `pycodestyle` utility (previously `pep8`) is used by the CI process to enforce code style. It is strongly recommended to include as part of your commit process. A git commit hook is provided in the source at `scripts/git-hooks/pre-commit`. Linking to this script from `.git/hooks/` will invoke `pycodestyle` prior to every commit attempt and abort if the validation fails.
* No easter eggs. While they can be fun, NetBox must be considered as a business-critical tool. The potential, however minor, for introducing a bug caused by unnecessary code is best avoided entirely.
```
$ cd .git/hooks/
$ ln -s ../../scripts/git-hooks/pre-commit
```
* Constants (variables which do not change) should be declared in `constants.py` within each app. Wildcard imports from the file are acceptable.
To invoke `pycodestyle` manually, run:
* Every model must have a [docstring](https://peps.python.org/pep-0257/). Every custom method should include an explanation of its function.
* Nested API serializers generate minimal representations of an object. These are stored separately from the primary serializers to avoid circular dependencies. Always import nested serializers from other apps directly. For example, from within the DCIM app you would write `from ipam.api.nested_serializers import NestedIPAddressSerializer`.
### PEP 8 Exceptions
NetBox ignores certain PEP8 assertions. These are listed below.
#### Wildcard Imports
Wildcard imports (for example, `from .constants import *`) are acceptable under any of the following conditions:
* The library being import contains only constant declarations (e.g. `constants.py`)
* The library being imported explicitly defines `__all__`
#### Maximum Line Length (E501)
NetBox does not restrict lines to a maximum length of 79 characters. We use a maximum line length of 120 characters, however this is not enforced by CI. The maximum length does not apply to HTML templates or to automatically generated code (e.g. database migrations).
#### Line Breaks Following Binary Operators (W504)
Line breaks are permitted following binary operators.
### Enforcing Code Style
The [`pycodestyle`](https://pypi.org/project/pycodestyle/) utility (formerly `pep8`) is used by the CI process to enforce code style. A [pre-commit hook](./getting-started.md#2-enable-pre-commit-hooks) which runs this automatically is included with NetBox. To invoke `pycodestyle` manually, run:
```
pycodestyle --ignore=W504,E501 netbox/
```
## Introducing New Dependencies
### Introducing New Dependencies
The introduction of a new dependency is best avoided unless it is absolutely necessary. For small features, it's generally preferable to replicate functionality within the NetBox code base rather than to introduce reliance on an external project. This reduces both the burden of tracking new releases and our exposure to outside bugs and supply chain attacks.
@@ -39,24 +58,22 @@ If there's a strong case for introducing a new dependency, it must meet the foll
* It must be actively maintained, with no longer than one year between releases.
* It must be available via the [Python Package Index](https://pypi.org/) (PyPI).
When adding a new dependency, a short description of the package and the URL of its code repository must be added to `base_requirements.txt`. Additionally, a line specifying the package name pinned to the current stable release must be added to `requirements.txt`. This ensures that NetBox will install only the known-good release and simplify support efforts.
When adding a new dependency, a short description of the package and the URL of its code repository must be added to `base_requirements.txt`. Additionally, a line specifying the package name pinned to the current stable release must be added to `requirements.txt`. This ensures that NetBox will install only the known-good release.
## General Guidance
## Written Works
* When in doubt, remain consistent: It is better to be consistently incorrect than inconsistently correct. If you notice in the course of unrelated work a pattern that should be corrected, continue to follow the pattern for now and submit a separate bug report so that the entire code base can be evaluated at a later point.
### General Guidance
* Prioritize readability over concision. Python is a very flexible language that typically offers several options for expressing a given piece of logic, but some may be more friendly to the reader than others. (List comprehensions are particularly vulnerable to over-optimization.) Always remain considerate of the future reader who may need to interpret your code without the benefit of the context within which you are writing it.
* Written material must always meet a reasonable professional standard, with proper grammar, spelling, and punctuation.
* No easter eggs. While they can be fun, NetBox must be considered as a business-critical tool. The potential, however minor, for introducing a bug caused by unnecessary logic is best avoided entirely.
* Use two line breaks between paragraphs.
* Constants (variables which generally do not change) should be declared in `constants.py` within each app. Wildcard imports from the file are acceptable.
* Use only a single space between sentences.
* Every model should have a docstring. Every custom method should include an explanation of its function.
* All documentation is to be written in [Markdown](../reference/markdown.md), with modest amounts of HTML permitted where needed to overcome technical limitations.
* Nested API serializers generate minimal representations of an object. These are stored separately from the primary serializers to avoid circular dependencies. Always import nested serializers from other apps directly. For example, from within the DCIM app you would write `from ipam.api.nested_serializers import NestedIPAddressSerializer`.
### Branding
## Branding
* When referring to NetBox in writing, use the proper form "NetBox," with the letters N and B capitalized. The lowercase form "netbox" should be used in code, filenames, etc. But never "Netbox" or any other deviation.
* When referring to NetBox in writing, use the proper form "NetBox," with the letters N and B capitalized. The lowercase form "netbox" should be used in code, filenames, etc. but never "Netbox" or any other deviation.
* There is an SVG form of the NetBox logo at [docs/netbox_logo.svg](../netbox_logo.svg). It is preferred to use this logo for all purposes as it scales to arbitrary sizes without loss of resolution. If a raster image is required, the SVG logo should be converted to a PNG image of the prescribed size.

View File

@@ -4,6 +4,8 @@
NetBox is the leading solution for modeling and documenting modern networks. By combining the traditional disciplines of IP address management (IPAM) and datacenter infrastructure management (DCIM) with powerful APIs and extensions, NetBox provides the ideal "source of truth" to power network automation. Read on to discover why thousands of organizations worldwide put NetBox at the heart of their infrastructure.
[![NetBox UI](./media/screenshots/netbox-ui.png)](./media/screenshots/netbox-ui.png)
## :material-server-network: Built for Networks
Unlike general-purpose CMDBs, NetBox has curated a data model which caters specifically to the needs of network engineers and operators. It delivers a wide assortment of object types carefully crafted to best serve the needs of infrastructure design and documentation. These cover all facets of network technology, from IP address managements to cabling to overlays and more:

View File

@@ -7,7 +7,7 @@ This section of the documentation discusses installing and configuring the NetBo
Begin by installing all system packages required by NetBox and its dependencies.
!!! warning "Python 3.8 or later required"
NetBox v3.2 requires Python 3.8, 3.9, or 3.10.
NetBox requires Python 3.8, 3.9, or 3.10.
=== "Ubuntu"
@@ -36,7 +36,7 @@ This documentation provides two options for installing NetBox: from a downloadab
Download the [latest stable release](https://github.com/netbox-community/netbox/releases) from GitHub as a tarball or ZIP archive and extract it to your desired path. In this example, we'll use `/opt/netbox` as the NetBox root.
```no-highlight
sudo wget https://github.com/netbox-community/netbox/archive/vX.Y.Z.tar.gz
sudo wget https://github.com/netbox-community/netbox/archive/refs/tags/vX.Y.Z.tar.gz
sudo tar -xzf vX.Y.Z.tar.gz -C /opt
sudo ln -s /opt/netbox-X.Y.Z/ /opt/netbox
```

View File

@@ -46,7 +46,7 @@ Next, create a file in the same directory as `configuration.py` (typically `/opt
### General Server Configuration
!!! info
When using Windows Server 2012 you may need to specify a port on `AUTH_LDAP_SERVER_URI`. Use `3269` for secure, or `3268` for non-secure.
When using Active Directory you may need to specify a port on `AUTH_LDAP_SERVER_URI` to authenticate users from all domains in the forest. Use `3269` for secure, or `3268` for non-secure access to the GC (Global Catalog).
```python
import ldap
@@ -67,6 +67,16 @@ AUTH_LDAP_BIND_PASSWORD = "demo"
# Note that this is a NetBox-specific setting which sets:
# ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
LDAP_IGNORE_CERT_ERRORS = True
# Include this setting if you want to validate the LDAP server certificates against a CA certificate directory on your server
# Note that this is a NetBox-specific setting which sets:
# ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, LDAP_CA_CERT_DIR)
LDAP_CA_CERT_DIR = '/etc/ssl/certs'
# Include this setting if you want to validate the LDAP server certificates against your own CA.
# Note that this is a NetBox-specific setting which sets:
# ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, LDAP_CA_CERT_FILE)
LDAP_CA_CERT_FILE = '/path/to/example-CA.crt'
```
STARTTLS can be configured by setting `AUTH_LDAP_START_TLS = True` and using the `ldap://` URI scheme.

View File

@@ -2,6 +2,8 @@
The installation instructions provided here have been tested to work on Ubuntu 20.04 and CentOS 8.3. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
<iframe width="560" height="315" src="https://www.youtube.com/embed/_y5JRiW_PLM" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
The following sections detail how to set up a new instance of NetBox:
1. [PostgreSQL database](1-postgresql.md)

View File

@@ -1,10 +1,19 @@
# Upgrading to a New NetBox Release
## Review the Release Notes
Upgrading NetBox to a new version is pretty simple, however users are cautioned to always review the release notes and save a backup of their current deployment prior to beginning an upgrade.
NetBox can generally be upgraded directly to any newer release with no interim steps, with the one exception being incrementing major versions. This can be done only from the most recent _minor_ release of the major version. For example, NetBox v2.11.8 can be upgraded to version 3.3.2 following the steps below. However, a deployment of NetBox v2.10.10 or earlier must first be upgraded to any v2.11 release, and then to any v3.x release. (This is to accommodate the consolidation of database schema migrations effected by a major version change).
[![Upgrade paths](../media/installation/upgrade_paths.png)](../media/installation/upgrade_paths.png)
!!! warning "Perform a Backup"
Always be sure to save a backup of your current NetBox deployment prior to starting the upgrade process.
## 1. Review the Release Notes
Prior to upgrading your NetBox instance, be sure to carefully review all [release notes](../release-notes/index.md) that have been published since your current version was released. Although the upgrade process typically does not involve additional work, certain releases may introduce breaking or backward-incompatible changes. These are called out in the release notes under the release in which the change went into effect.
## Update Dependencies to Required Versions
## 2. Update Dependencies to Required Versions
NetBox v3.0 and later require the following:
@@ -14,10 +23,25 @@ NetBox v3.0 and later require the following:
| PostgreSQL | 10 |
| Redis | 4.0 |
## Install the Latest Release
## 3. Install the Latest Release
As with the initial installation, you can upgrade NetBox by either downloading the latest release package or by cloning the `master` branch of the git repository.
!!! warning
Use the same method as you used to install Netbox originally
If you are not sure how Netbox was installed originally, check with this
command:
```
ls -ld /opt/netbox /opt/netbox/.git
```
If Netbox was installed from a release package, then `/opt/netbox` will be a
symlink pointing to the current version, and `/opt/netbox/.git` will not
exist. If it was installed from git, then `/opt/netbox` and
`/opt/netbox/.git` will both exist as normal directories.
### Option A: Download a Release
Download the [latest stable release](https://github.com/netbox-community/netbox/releases) from GitHub as a tarball or ZIP archive. Extract it to your desired path. In this example, we'll use `/opt/netbox`.
@@ -72,7 +96,7 @@ sudo git pull origin master
sudo git checkout v2.11.11
## Run the Upgrade Script
## 4. Run the Upgrade Script
Once the new code is in place, verify that any optional Python packages required by your deployment (e.g. `napalm` or `django-auth-ldap`) are listed in `local_requirements.txt`. Then, run the upgrade script:
@@ -103,7 +127,7 @@ This script performs the following actions:
been made to your local codebase and should be investigated. Never attempt to create new migrations unless you are
intentionally modifying the database schema.
## Restart the NetBox Services
## 5. Restart the NetBox Services
!!! warning
If you are upgrading from an installation that does not use a Python virtual environment (any release prior to v2.7.9), you'll need to update the systemd service files to reference the new Python and gunicorn executables before restarting the services. These are located in `/opt/netbox/venv/bin/`. See the example service files in `/opt/netbox/contrib/` for reference.
@@ -114,7 +138,7 @@ Finally, restart the gunicorn and RQ services:
sudo systemctl restart netbox netbox-rq
```
## Verify Housekeeping Scheduling
## 6. Verify Housekeeping Scheduling
If upgrading from a release prior to NetBox v3.0, check that a cron task (or similar scheduled process) has been configured to run NetBox's nightly housekeeping command. A shell script which invokes this command is included at `contrib/netbox-housekeeping.sh`. It can be linked from your system's daily cron task directory, or included within the crontab directly. (If NetBox has been installed in a nonstandard path, be sure to update the system paths within this script first.)

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

View File

@@ -34,12 +34,12 @@ To utilize a filter set in a subclass of one of NetBox's generic views (such as
```python
# views.py
from netbox.views.generic import ObjectListView
from .filtersets import MyModelFitlerSet
from .filtersets import MyModelFilterSet
from .models import MyModel
class MyModelListView(ObjectListView):
queryset = MyModel.objects.all()
filterset = MyModelFitlerSet
filterset = MyModelFilterSet
```
To enable a filter set on a REST API endpoint, set the `filterset_class` attribute on the API view:

View File

@@ -144,73 +144,73 @@ class MyModelFilterForm(NetBoxModelFilterSetForm):
In addition to the [form fields provided by Django](https://docs.djangoproject.com/en/stable/ref/forms/fields/), NetBox provides several field classes for use within forms to handle specific types of data. These can be imported from `utilities.forms.fields` and are documented below.
::: utilities.forms.ColorField
selection:
options:
members: false
::: utilities.forms.CommentField
selection:
options:
members: false
::: utilities.forms.JSONField
selection:
options:
members: false
::: utilities.forms.MACAddressField
selection:
options:
members: false
::: utilities.forms.SlugField
selection:
options:
members: false
## Choice Fields
::: utilities.forms.ChoiceField
selection:
options:
members: false
::: utilities.forms.MultipleChoiceField
selection:
options:
members: false
## Dynamic Object Fields
::: utilities.forms.DynamicModelChoiceField
selection:
options:
members: false
::: utilities.forms.DynamicModelMultipleChoiceField
selection:
options:
members: false
## Content Type Fields
::: utilities.forms.ContentTypeChoiceField
selection:
options:
members: false
::: utilities.forms.ContentTypeMultipleChoiceField
selection:
options:
members: false
## CSV Import Fields
::: utilities.forms.CSVChoiceField
selection:
options:
members: false
::: utilities.forms.CSVMultipleChoiceField
selection:
options:
members: false
::: utilities.forms.CSVModelChoiceField
selection:
options:
members: false
::: utilities.forms.CSVContentTypeField
selection:
options:
members: false
::: utilities.forms.CSVMultipleContentTypeField
selection:
options:
members: false

View File

@@ -32,11 +32,11 @@ schema = MyQuery
NetBox provides two object type classes for use by plugins.
::: netbox.graphql.types.BaseObjectType
selection:
options:
members: false
::: netbox.graphql.types.NetBoxObjectType
selection:
options:
members: false
## GraphQL Fields
@@ -44,9 +44,9 @@ NetBox provides two object type classes for use by plugins.
NetBox provides two field classes for use by plugins.
::: netbox.graphql.fields.ObjectField
selection:
options:
members: false
::: netbox.graphql.fields.ObjectListField
selection:
options:
members: false

View File

@@ -112,6 +112,14 @@ NetBox looks for the `config` variable within a plugin's `__init__.py` to load i
All required settings must be configured by the user. If a configuration parameter is listed in both `required_settings` and `default_settings`, the default setting will be ignored.
!!! tip "Accessing Config Parameters"
Plugin configuration parameters can be accessed in `settings.PLUGINS_CONFIG`, mapped by plugin name. For example:
```python
from django.conf import settings
settings.PLUGINS_CONFIG['myplugin']['verbose_name']
```
## Create setup.py
`setup.py` is the [setup script](https://docs.python.org/3.8/distutils/setupscript.html) used to package and install our plugin once it's finished. The primary function of this script is to call the setuptools library's `setup()` function to create a Python distribution package. We can pass a number of keyword arguments to control the package creation as well as to provide metadata about the plugin. An example `setup.py` is below:

View File

@@ -52,38 +52,38 @@ This will automatically apply any user-specific preferences for the table. (If u
The table column classes listed below are supported for use in plugins. These classes can be imported from `netbox.tables.columns`.
::: netbox.tables.BooleanColumn
selection:
options:
members: false
::: netbox.tables.ChoiceFieldColumn
selection:
options:
members: false
::: netbox.tables.ColorColumn
selection:
options:
members: false
::: netbox.tables.ColoredLabelColumn
selection:
options:
members: false
::: netbox.tables.ContentTypeColumn
selection:
options:
members: false
::: netbox.tables.ContentTypesColumn
selection:
options:
members: false
::: netbox.tables.MarkdownColumn
selection:
options:
members: false
::: netbox.tables.TagColumn
selection:
options:
members: false
::: netbox.tables.TemplateColumn
selection:
options:
members:
- __init__

View File

@@ -84,24 +84,24 @@ Below are the class definitions for NetBox's object views. These views handle CR
::: netbox.views.generic.base.BaseObjectView
::: netbox.views.generic.ObjectView
selection:
options:
members:
- get_object
- get_template_name
::: netbox.views.generic.ObjectEditView
selection:
options:
members:
- get_object
- alter_object
::: netbox.views.generic.ObjectDeleteView
selection:
options:
members:
- get_object
::: netbox.views.generic.ObjectChildrenView
selection:
options:
members:
- get_children
- prep_table_data
@@ -113,22 +113,22 @@ Below are the class definitions for NetBox's multi-object views. These views han
::: netbox.views.generic.base.BaseMultiObjectView
::: netbox.views.generic.ObjectListView
selection:
options:
members:
- get_table
- export_table
- export_template
::: netbox.views.generic.BulkImportView
selection:
options:
members: false
::: netbox.views.generic.BulkEditView
selection:
options:
members: false
::: netbox.views.generic.BulkDeleteView
selection:
options:
members:
- get_form
@@ -137,12 +137,12 @@ Below are the class definitions for NetBox's multi-object views. These views han
These views are provided to enable or enhance certain NetBox model features, such as change logging or journaling. These typically do not need to be subclassed: They can be used directly e.g. in a URL path.
::: netbox.views.generic.ObjectChangeLogView
selection:
options:
members:
- get_form
::: netbox.views.generic.ObjectJournalView
selection:
options:
members:
- get_form

View File

@@ -1,5 +1,210 @@
# NetBox v3.3
## v3.3.8 (2022-11-16)
### Enhancements
* [#10356](https://github.com/netbox-community/netbox/issues/10356) - Add backplane Ethernet interface types
* [#10902](https://github.com/netbox-community/netbox/issues/10902) - Add location selector to power feed form
* [#10904](https://github.com/netbox-community/netbox/issues/10904) - Use front/rear port colors in cable trace SVG
* [#10914](https://github.com/netbox-community/netbox/issues/10914) - Include "add module type" button on manufacturer view
* [#10915](https://github.com/netbox-community/netbox/issues/10915) - Add count of L2VPNs to tenant view
* [#10919](https://github.com/netbox-community/netbox/issues/10919) - Include device location under cable view
* [#10920](https://github.com/netbox-community/netbox/issues/10920) - Include request cookies when queuing a custom script
### Bug Fixes
* [#9439](https://github.com/netbox-community/netbox/issues/9439) - Ensure thread safety of change logging functions
* [#10709](https://github.com/netbox-community/netbox/issues/10709) - Correct UI display for `azuread-v2-tenant-oauth2` SSO backend
* [#10829](https://github.com/netbox-community/netbox/issues/10829) - Fix bulk edit/delete buttons ad top of object lists
* [#10837](https://github.com/netbox-community/netbox/issues/10837) - Correct cookie paths when `BASE_PATH` is set
* [#10874](https://github.com/netbox-community/netbox/issues/10874) - Remove erroneous link for contact assignment count
* [#10881](https://github.com/netbox-community/netbox/issues/10881) - Fix dark mode coloring for data on device status page
* [#10891](https://github.com/netbox-community/netbox/issues/10891) - Populate tag selection list for service filter form
* [#10897](https://github.com/netbox-community/netbox/issues/10897) - Fix form widget styling on FHRP group form
* [#10910](https://github.com/netbox-community/netbox/issues/10910) - Fix cable creation links on power port view
---
## v3.3.7 (2022-11-01)
### Bug Fixes
* [#10282](https://github.com/netbox-community/netbox/issues/10282) - Enforce advisory locks when allocating available IP addresses to prevent race conditions
* [#10770](https://github.com/netbox-community/netbox/issues/10282) - Fix social authentication for new users
* [#10791](https://github.com/netbox-community/netbox/issues/10791) - Permit nullifying VLAN group `scope_type` via REST API
* [#10803](https://github.com/netbox-community/netbox/issues/10803) - Fix exception when ordering contacts by number of assignments
* [#10809](https://github.com/netbox-community/netbox/issues/10809) - Permit nullifying site `time_zone` via REST API
---
## v3.3.6 (2022-10-26)
### Enhancements
* [#9584](https://github.com/netbox-community/netbox/issues/9584) - Enable filtering devices by device type slug
* [#9722](https://github.com/netbox-community/netbox/issues/9722) - Add LDAP configuration parameters to specify certificates
* [#10580](https://github.com/netbox-community/netbox/issues/10580) - Link "assigned" checkbox in IP address table to assigned interface
* [#10639](https://github.com/netbox-community/netbox/issues/10639) - Set cookie paths according to configured `BASE_PATH`
* [#10685](https://github.com/netbox-community/netbox/issues/10685) - Position A/Z termination cards above the fold under circuit view
### Bug Fixes
* [#9669](https://github.com/netbox-community/netbox/issues/9669) - Strip colons from usernames when using remote authentication
* [#10575](https://github.com/netbox-community/netbox/issues/10575) - Include OIDC dependencies for python-social-auth
* [#10584](https://github.com/netbox-community/netbox/issues/10584) - Fix service clone link
* [#10610](https://github.com/netbox-community/netbox/issues/10610) - Allow assignment of VC member to LAG on non-master peer
* [#10643](https://github.com/netbox-community/netbox/issues/10643) - Ensure consistent display of custom fields for all model forms
* [#10646](https://github.com/netbox-community/netbox/issues/10646) - Fix filtering of power feed by power panel when connecting a cable
* [#10655](https://github.com/netbox-community/netbox/issues/10655) - Correct display of assigned contacts in object tables
* [#10666](https://github.com/netbox-community/netbox/issues/10666) - Re-evaluate disabled LDAP user when processing API requests
* [#10682](https://github.com/netbox-community/netbox/issues/10682) - Correct home view links to connection lists
* [#10712](https://github.com/netbox-community/netbox/issues/10712) - Fix ModuleNotFoundError exception when generating API schema under Python 3.9+
* [#10716](https://github.com/netbox-community/netbox/issues/10716) - Add left/right page plugin content embeds for tag view
* [#10719](https://github.com/netbox-community/netbox/issues/10719) - Prevent user without sufficient permission from creating an IP address via FHRP group creation
* [#10723](https://github.com/netbox-community/netbox/issues/10723) - Distinguish between inside/outside NAT assignments for device/VM primary IPs
* [#10745](https://github.com/netbox-community/netbox/issues/10745) - Correct display of status field in clusters list
* [#10746](https://github.com/netbox-community/netbox/issues/10746) - Add missing status attribute to cluster view
---
## v3.3.5 (2022-10-05)
### Enhancements
* [#8424](https://github.com/netbox-community/netbox/issues/8424) - Include rack elevation under device view
* [#10352](https://github.com/netbox-community/netbox/issues/10352) - Omit extraneous URL query attributes during search
* [#10465](https://github.com/netbox-community/netbox/issues/10465) - Improve formatting of device heights and rack positions
### Bug Fixes
* [#9497](https://github.com/netbox-community/netbox/issues/9497) - Adjust non-racked device filter on site and location detailed view
* [#10408](https://github.com/netbox-community/netbox/issues/10408) - Fix validation when attempting to add redundant contact assignments
* [#10423](https://github.com/netbox-community/netbox/issues/10423) - Enforce object type validation when creating journal entries
* [#10435](https://github.com/netbox-community/netbox/issues/10435) - Fix exception when filtering VLANs by virtual machine with no cluster assigned
* [#10439](https://github.com/netbox-community/netbox/issues/10439) - Fix form widget styling for DeviceType airflow field
* [#10445](https://github.com/netbox-community/netbox/issues/10445) - Avoid rounding virtual machine memory values
* [#10460](https://github.com/netbox-community/netbox/issues/10460) - Restore missing connection details for device components
* [#10461](https://github.com/netbox-community/netbox/issues/10461) - Enable filtering by read-only custom fields in the UI
* [#10470](https://github.com/netbox-community/netbox/issues/10470) - Omit read-only custom fields from CSV import forms
* [#10480](https://github.com/netbox-community/netbox/issues/10480) - Cable trace SVG links should not force a new window
* [#10491](https://github.com/netbox-community/netbox/issues/10491) - Clarify representation of blocking contact assignments during contact deletion
* [#10513](https://github.com/netbox-community/netbox/issues/10513) - Disable the reassignment of a module to a new device
* [#10517](https://github.com/netbox-community/netbox/issues/10517) - Automatically inherit site assignment from cluster when creating a virtual machine
* [#10559](https://github.com/netbox-community/netbox/issues/10559) - Permit the pinning of a VM to a particular device within a cluster which has no site assignment
* [#10562](https://github.com/netbox-community/netbox/issues/10562) - Correct URL for contacts table tags column
---
## v3.3.4 (2022-09-16)
### Bug Fixes
* [#10383](https://github.com/netbox-community/netbox/issues/10383) - Fix assignment of component templates to module types via web UI
* [#10387](https://github.com/netbox-community/netbox/issues/10387) - Fix `MultiValueDictKeyError` exception when editing a device interface
---
## v3.3.3 (2022-09-15)
### Enhancements
* [#8580](https://github.com/netbox-community/netbox/issues/8580) - Add `occupied` filter for cabled objects to filter by cable or `mark_connected`
* [#9577](https://github.com/netbox-community/netbox/issues/9577) - Add `has_front_image` and `has_rear_image` filters for device types
* [#10268](https://github.com/netbox-community/netbox/issues/10268) - Omit trailing ".0" in device positions within UI
* [#10359](https://github.com/netbox-community/netbox/issues/10359) - Add region and site group columns to the devices table
### Bug Fixes
* [#9231](https://github.com/netbox-community/netbox/issues/9231) - Fix `empty` lookup expression for string filters
* [#10247](https://github.com/netbox-community/netbox/issues/10247) - Allow changing the pre-populated device/VM when creating new components
* [#10250](https://github.com/netbox-community/netbox/issues/10250) - Fix exception when CableTermination validation fails during bulk import of cables
* [#10258](https://github.com/netbox-community/netbox/issues/10258) - Enable the use of reports & scripts packaged in submodules
* [#10259](https://github.com/netbox-community/netbox/issues/10259) - Fix `NoReverseMatch` exception when listing available prefixes with "flat" column displayed
* [#10270](https://github.com/netbox-community/netbox/issues/10270) - Fix custom field validation when creating new services
* [#10278](https://github.com/netbox-community/netbox/issues/10278) - Fix "create & add another" for image attachments
* [#10294](https://github.com/netbox-community/netbox/issues/10294) - Fix spurious changelog diff for interface WWN field
* [#10304](https://github.com/netbox-community/netbox/issues/10304) - Enable cloning for custom fields & custom links
* [#10305](https://github.com/netbox-community/netbox/issues/10305) - Fix Virtual Chassis master field cannot be null according to the API
* [#10307](https://github.com/netbox-community/netbox/issues/10307) - Correct value for "Passive 48V (4-pair)" PoE type selection
* [#10333](https://github.com/netbox-community/netbox/issues/10333) - Show available values for `ui_visibility` field of CustomField for CSV import
* [#10337](https://github.com/netbox-community/netbox/issues/10337) - Display SSO links when local authentication fails
* [#10353](https://github.com/netbox-community/netbox/issues/10353) - Table action buttons should reserve return URL parameters
* [#10362](https://github.com/netbox-community/netbox/issues/10362) - Correct display of custom fields when editing an L2VPN termination
---
## v3.3.2 (2022-09-02)
### Enhancements
* [#9477](https://github.com/netbox-community/netbox/issues/9477) - Enable clearing applied table column ordering
* [#10034](https://github.com/netbox-community/netbox/issues/10034) - Add L2VPN column to interface and VLAN tables
* [#10043](https://github.com/netbox-community/netbox/issues/10043) - Add support for `limit` query parameter to available VLANs API endpoint
* [#10060](https://github.com/netbox-community/netbox/issues/10060) - Add journal entries to global search
* [#10195](https://github.com/netbox-community/netbox/issues/10195) - Enable filtering of device components by rack
* [#10233](https://github.com/netbox-community/netbox/issues/10233) - Enable sorting rack elevations by facility ID
### Bug Fixes
* [#9328](https://github.com/netbox-community/netbox/issues/9328) - Hide available IPs when non-default ordering is applied
* [#9481](https://github.com/netbox-community/netbox/issues/9481) - Update child device location when parent location changes
* [#9832](https://github.com/netbox-community/netbox/issues/9832) - Improve error message when validating rack reservation units
* [#9895](https://github.com/netbox-community/netbox/issues/9895) - Various corrections to OpenAPI spec
* [#9962](https://github.com/netbox-community/netbox/issues/9962) - SSO login should respect `next` URL query parameter
* [#9963](https://github.com/netbox-community/netbox/issues/9963) - Fix support for custom `CSRF_COOKIE_NAME` value
* [#10155](https://github.com/netbox-community/netbox/issues/10155) - Fix rear port display when editing front port template for module type
* [#10156](https://github.com/netbox-community/netbox/issues/10156) - Avoid forcing SVG image links to open in a new window
* [#10161](https://github.com/netbox-community/netbox/issues/10161) - Restore "set null" option for custom fields during bulk edit
* [#10176](https://github.com/netbox-community/netbox/issues/10176) - Correct utilization display for empty racks
* [#10177](https://github.com/netbox-community/netbox/issues/10177) - Correct display of custom fields when editing VM interfaces
* [#10178](https://github.com/netbox-community/netbox/issues/10178) - Display manufacturer name alongside device type under device view
* [#10181](https://github.com/netbox-community/netbox/issues/10181) - Restore MultiPartParser (regression from #10031)
* [#10184](https://github.com/netbox-community/netbox/issues/10184) - Fix vertical alignment when displaying object attributes with buttons
* [#10208](https://github.com/netbox-community/netbox/issues/10208) - Fix permissions evaluation for interface actions dropdown menu
* [#10217](https://github.com/netbox-community/netbox/issues/10217) - Handle exception when trace splits to multiple rear ports
* [#10220](https://github.com/netbox-community/netbox/issues/10220) - Validate IP version when assigning primary IPs to a virtual machine
* [#10231](https://github.com/netbox-community/netbox/issues/10231) - Correct API schema definition for several serializer fields
---
## v3.3.1 (2022-08-25)
### Enhancements
* [#6454](https://github.com/netbox-community/netbox/issues/6454) - Include contextual help when creating first objects in UI
* [#9935](https://github.com/netbox-community/netbox/issues/9935) - Add 802.11ay and "other" wireless interface types
* [#10031](https://github.com/netbox-community/netbox/issues/10031) - Enforce `application/json` content type for REST API requests
* [#10033](https://github.com/netbox-community/netbox/issues/10033) - Disable "add termination" button for point-to-point L2VPNs with two terminations
* [#10037](https://github.com/netbox-community/netbox/issues/10037) - Add "child interface" option to actions dropdown in interfaces list
* [#10038](https://github.com/netbox-community/netbox/issues/10038) - Add "L2VPN termination" option to actions dropdown in interfaces list
* [#10039](https://github.com/netbox-community/netbox/issues/10039) - Add "assign FHRP group" option to actions dropdown in interfaces list
* [#10061](https://github.com/netbox-community/netbox/issues/10061) - Replicate type when cloning L2VPN instances
* [#10066](https://github.com/netbox-community/netbox/issues/10066) - Use fixed column widths for custom field values in UI
* [#10133](https://github.com/netbox-community/netbox/issues/10133) - Enable nullifying device location during bulk edit
### Bug Fixes
* [#9663](https://github.com/netbox-community/netbox/issues/9663) - Omit available IP annotations when filtering prefix child IPs list
* [#10040](https://github.com/netbox-community/netbox/issues/10040) - Fix exception when ordering prefixes by flat representation
* [#10053](https://github.com/netbox-community/netbox/issues/10053) - Custom fields header should not be displayed when editing circuit terminations with no custom fields
* [#10055](https://github.com/netbox-community/netbox/issues/10055) - Fix extraneous NAT indicator by device primary IP
* [#10057](https://github.com/netbox-community/netbox/issues/10057) - Fix AttributeError exception when global search results include rack reservations
* [#10059](https://github.com/netbox-community/netbox/issues/10059) - Add identifier column to L2VPN table
* [#10070](https://github.com/netbox-community/netbox/issues/10070) - Add unique constraint for L2VPN slug
* [#10087](https://github.com/netbox-community/netbox/issues/10087) - Correct display of far end in console/power/interface connections tables
* [#10089](https://github.com/netbox-community/netbox/issues/10089) - `linkify` template filter should escape object representation
* [#10094](https://github.com/netbox-community/netbox/issues/10094) - Fix 404 when using "create and add another" to add contact assignments
* [#10108](https://github.com/netbox-community/netbox/issues/10108) - Linkify inside NAT IPs for primary device IPs in UI
* [#10109](https://github.com/netbox-community/netbox/issues/10109) - Fix available prefixes calculation for container prefixes in the global table
* [#10111](https://github.com/netbox-community/netbox/issues/10111) - Fix ValueError exception when searching for L2VPN objects
* [#10118](https://github.com/netbox-community/netbox/issues/10118) - Fix display of connected LLDP neighbors for devices
* [#10134](https://github.com/netbox-community/netbox/issues/10134) - Custom fields data serializer should return a 400 response for invalid data
* [#10135](https://github.com/netbox-community/netbox/issues/10135) - Fix SSO support for SAML2 IDPs
* [#10147](https://github.com/netbox-community/netbox/issues/10147) - Permit the creation of 0U device types via REST API
---
## v3.3.0 (2022-08-17)
### Breaking Changes

View File

@@ -30,7 +30,7 @@ plugins:
- os.chdir('netbox/')
- os.environ.setdefault("DJANGO_SETTINGS_MODULE", "netbox.settings")
- django.setup()
rendering:
options:
heading_level: 3
members_order: source
show_root_heading: true
@@ -38,7 +38,6 @@ plugins:
show_root_toc_entry: false
show_source: false
extra:
readthedocs: !ENV READTHEDOCS
social:
- icon: fontawesome/brands/github
link: https://github.com/netbox-community/netbox
@@ -249,6 +248,7 @@ nav:
- User Preferences: 'development/user-preferences.md'
- Web UI: 'development/web-ui.md'
- Release Checklist: 'development/release-checklist.md'
- git Cheat Sheet: 'development/git-cheat-sheet.md'
- Release Notes:
- Summary: 'release-notes/index.md'
- Version 3.3: 'release-notes/version-3.3.md'

View File

@@ -76,6 +76,12 @@ class ProviderNetworkForm(NetBoxModelForm):
class CircuitTypeForm(NetBoxModelForm):
slug = SlugField()
fieldsets = (
('Circuit Type', (
'name', 'slug', 'description', 'tags',
)),
)
class Meta:
model = CircuitType
fields = [

View File

@@ -1,3 +1,4 @@
from django.apps import apps
from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import ValidationError
from django.db import models
@@ -136,6 +137,10 @@ class Circuit(NetBoxModel):
def __str__(self):
return self.cid
@classmethod
def get_prerequisite_models(cls):
return [apps.get_model('circuits.Provider'), CircuitType]
def get_absolute_url(self):
return reverse('circuits:circuit', args=[self.pk])

View File

@@ -1,8 +1,9 @@
import django_tables2 as tables
from circuits.models import *
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenancyColumnsMixin
from .columns import CommitRateColumn
__all__ = (
@@ -39,7 +40,7 @@ class CircuitTypeTable(NetBoxTable):
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug')
class CircuitTable(TenancyColumnsMixin, NetBoxTable):
class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
cid = tables.Column(
linkify=True,
verbose_name='Circuit ID'
@@ -58,9 +59,6 @@ class CircuitTable(TenancyColumnsMixin, NetBoxTable):
)
commit_rate = CommitRateColumn()
comments = columns.MarkdownColumn()
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
url_name='circuits:circuit_list'
)

View File

@@ -1,7 +1,8 @@
import django_tables2 as tables
from django_tables2.utils import Accessor
from circuits.models import *
from django_tables2.utils import Accessor
from tenancy.tables import ContactsColumnMixin
from netbox.tables import NetBoxTable, columns
__all__ = (
@@ -10,7 +11,7 @@ __all__ = (
)
class ProviderTable(NetBoxTable):
class ProviderTable(ContactsColumnMixin, NetBoxTable):
name = tables.Column(
linkify=True
)
@@ -31,9 +32,6 @@ class ProviderTable(NetBoxTable):
verbose_name='Circuits'
)
comments = columns.MarkdownColumn()
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
url_name='circuits:provider_list'
)

View File

@@ -344,6 +344,7 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
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(provider=providers[0], type=circuit_types[0], cid='Circuit 7'),
)
Circuit.objects.bulk_create(circuits)
@@ -357,6 +358,7 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
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(circuit=circuits[6], provider_network=provider_networks[0], term_side='A', mark_connected=True),
))
CircuitTermination.objects.bulk_create(circuit_terminations)
@@ -364,7 +366,7 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
def test_term_side(self):
params = {'term_side': 'A'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 7)
def test_port_speed(self):
params = {'port_speed': ['1000', '2000']}
@@ -397,11 +399,19 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
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)
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
def test_cabled(self):
params = {'cabled': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'cabled': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
def test_occupied(self):
params = {'occupied': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
params = {'occupied': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 7)
class ProviderNetworkTestCase(TestCase, ChangeLoggedFilterSetTests):

View File

@@ -316,6 +316,7 @@ class NestedModuleSerializer(WritableNestedSerializer):
class NestedConsoleServerPortSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
device = NestedDeviceSerializer(read_only=True)
_occupied = serializers.BooleanField(required=False, read_only=True)
class Meta:
model = models.ConsoleServerPort
@@ -325,6 +326,7 @@ class NestedConsoleServerPortSerializer(WritableNestedSerializer):
class NestedConsolePortSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
device = NestedDeviceSerializer(read_only=True)
_occupied = serializers.BooleanField(required=False, read_only=True)
class Meta:
model = models.ConsolePort
@@ -334,6 +336,7 @@ class NestedConsolePortSerializer(WritableNestedSerializer):
class NestedPowerOutletSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
device = NestedDeviceSerializer(read_only=True)
_occupied = serializers.BooleanField(required=False, read_only=True)
class Meta:
model = models.PowerOutlet
@@ -343,6 +346,7 @@ class NestedPowerOutletSerializer(WritableNestedSerializer):
class NestedPowerPortSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
device = NestedDeviceSerializer(read_only=True)
_occupied = serializers.BooleanField(required=False, read_only=True)
class Meta:
model = models.PowerPort
@@ -352,6 +356,7 @@ class NestedPowerPortSerializer(WritableNestedSerializer):
class NestedInterfaceSerializer(WritableNestedSerializer):
device = NestedDeviceSerializer(read_only=True)
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
_occupied = serializers.BooleanField(required=False, read_only=True)
class Meta:
model = models.Interface
@@ -361,6 +366,7 @@ class NestedInterfaceSerializer(WritableNestedSerializer):
class NestedRearPortSerializer(WritableNestedSerializer):
device = NestedDeviceSerializer(read_only=True)
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
_occupied = serializers.BooleanField(required=False, read_only=True)
class Meta:
model = models.RearPort
@@ -370,6 +376,7 @@ class NestedRearPortSerializer(WritableNestedSerializer):
class NestedFrontPortSerializer(WritableNestedSerializer):
device = NestedDeviceSerializer(read_only=True)
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
_occupied = serializers.BooleanField(required=False, read_only=True)
class Meta:
model = models.FrontPort
@@ -454,6 +461,7 @@ class NestedPowerPanelSerializer(WritableNestedSerializer):
class NestedPowerFeedSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
_occupied = serializers.BooleanField(required=False, read_only=True)
class Meta:
model = models.PowerFeed

View File

@@ -130,7 +130,7 @@ class SiteSerializer(NetBoxModelSerializer):
region = NestedRegionSerializer(required=False, allow_null=True)
group = NestedSiteGroupSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True)
time_zone = TimeZoneSerializerField(required=False)
time_zone = TimeZoneSerializerField(required=False, allow_null=True)
asns = SerializedPKRelatedField(
queryset=ASN.objects.all(),
serializer=NestedASNSerializer,
@@ -310,7 +310,7 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
max_digits=4,
decimal_places=1,
label='Position (U)',
min_value=decimal.Decimal(0.5),
min_value=0,
default=1.0
)
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False)
@@ -579,7 +579,7 @@ class InventoryItemTemplateSerializer(ValidatedModelSerializer):
'description', 'component_type', 'component_id', 'component', 'created', 'last_updated', '_depth',
]
@swagger_serializer_method(serializer_or_field=serializers.DictField)
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
def get_component(self, obj):
if obj.component is None:
return None
@@ -693,13 +693,13 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
'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.JSONField)
def get_config_context(self, obj):
return obj.get_config_context()
class DeviceNAPALMSerializer(serializers.Serializer):
method = serializers.DictField()
method = serializers.JSONField()
#
@@ -975,7 +975,7 @@ class InventoryItemSerializer(NetBoxModelSerializer):
'custom_fields', 'created', 'last_updated', '_depth',
]
@swagger_serializer_method(serializer_or_field=serializers.DictField)
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
def get_component(self, obj):
if obj.component is None:
return None
@@ -1046,7 +1046,7 @@ class CableTerminationSerializer(NetBoxModelSerializer):
'id', 'url', 'display', 'cable', 'cable_end', 'termination_type', 'termination_id', 'termination'
]
@swagger_serializer_method(serializer_or_field=serializers.DictField)
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
def get_termination(self, obj):
serializer = get_serializer_for_model(obj.termination, prefix=NESTED_SERIALIZER_PREFIX)
context = {'request': self.context['request']}
@@ -1076,7 +1076,7 @@ class CablePathSerializer(serializers.ModelSerializer):
class VirtualChassisSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
master = NestedDeviceSerializer(required=False)
master = NestedDeviceSerializer(required=False, allow_null=True, default=None)
member_count = serializers.IntegerField(read_only=True)
class Meta:

View File

@@ -783,6 +783,17 @@ class InterfaceTypeChoices(ChoiceSet):
TYPE_400GE_QSFP_DD = '400gbase-x-qsfpdd'
TYPE_400GE_OSFP = '400gbase-x-osfp'
# Ethernet Backplane
TYPE_1GE_KX = '1000base-kx'
TYPE_10GE_KR = '10gbase-kr'
TYPE_10GE_KX4 = '10gbase-kx4'
TYPE_25GE_KR = '25gbase-kr'
TYPE_40GE_KR4 = '40gbase-kr4'
TYPE_50GE_KR = '50gbase-kr'
TYPE_100GE_KP4 = '100gbase-kp4'
TYPE_100GE_KR2 = '100gbase-kr2'
TYPE_100GE_KR4 = '100gbase-kr4'
# Wireless
TYPE_80211A = 'ieee802.11a'
TYPE_80211G = 'ieee802.11g'
@@ -790,7 +801,9 @@ class InterfaceTypeChoices(ChoiceSet):
TYPE_80211AC = 'ieee802.11ac'
TYPE_80211AD = 'ieee802.11ad'
TYPE_80211AX = 'ieee802.11ax'
TYPE_80211AY = 'ieee802.11ay'
TYPE_802151 = 'ieee802.15.1'
TYPE_OTHER_WIRELESS = 'other-wireless'
# Cellular
TYPE_GSM = 'gsm'
@@ -909,6 +922,20 @@ class InterfaceTypeChoices(ChoiceSet):
(TYPE_400GE_OSFP, 'OSFP (400GE)'),
)
),
(
'Ethernet (backplane)',
(
(TYPE_1GE_KX, '1000BASE-KX (1GE)'),
(TYPE_10GE_KR, '10GBASE-KR (10GE)'),
(TYPE_10GE_KX4, '10GBASE-KX4 (10GE)'),
(TYPE_25GE_KR, '25GBASE-KR (25GE)'),
(TYPE_40GE_KR4, '40GBASE-KR4 (40GE)'),
(TYPE_50GE_KR, '50GBASE-KR (50GE)'),
(TYPE_100GE_KP4, '100GBASE-KP4 (100GE)'),
(TYPE_100GE_KR2, '100GBASE-KR2 (100GE)'),
(TYPE_100GE_KR4, '100GBASE-KR4 (100GE)'),
)
),
(
'Wireless',
(
@@ -918,7 +945,9 @@ class InterfaceTypeChoices(ChoiceSet):
(TYPE_80211AC, 'IEEE 802.11ac'),
(TYPE_80211AD, 'IEEE 802.11ad'),
(TYPE_80211AX, 'IEEE 802.11ax'),
(TYPE_80211AY, 'IEEE 802.11ay'),
(TYPE_802151, 'IEEE 802.15.1 (Bluetooth)'),
(TYPE_OTHER_WIRELESS, 'Other (Wireless)'),
)
),
(
@@ -1092,7 +1121,7 @@ class InterfacePoETypeChoices(ChoiceSet):
(PASSIVE_24V_2PAIR, 'Passive 24V (2-pair)'),
(PASSIVE_24V_4PAIR, 'Passive 24V (4-pair)'),
(PASSIVE_48V_2PAIR, 'Passive 48V (2-pair)'),
(PASSIVE_48V_2PAIR, 'Passive 48V (4-pair)'),
(PASSIVE_48V_4PAIR, 'Passive 48V (4-pair)'),
)
),
)

View File

@@ -45,6 +45,9 @@ WIRELESS_IFACE_TYPES = [
InterfaceTypeChoices.TYPE_80211AC,
InterfaceTypeChoices.TYPE_80211AD,
InterfaceTypeChoices.TYPE_80211AX,
InterfaceTypeChoices.TYPE_80211AY,
InterfaceTypeChoices.TYPE_802151,
InterfaceTypeChoices.TYPE_OTHER_WIRELESS,
]
NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES

View File

@@ -434,6 +434,14 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
to_field_name='slug',
label='Manufacturer (slug)',
)
has_front_image = django_filters.BooleanFilter(
label='Has a front image',
method='_has_front_image'
)
has_rear_image = django_filters.BooleanFilter(
label='Has a rear image',
method='_has_rear_image'
)
console_ports = django_filters.BooleanFilter(
method='_console_ports',
label='Has console ports',
@@ -487,6 +495,18 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
Q(comments__icontains=value)
)
def _has_front_image(self, queryset, name, value):
if value:
return queryset.exclude(front_image='')
else:
return queryset.filter(front_image='')
def _has_rear_image(self, queryset, name, value):
if value:
return queryset.exclude(rear_image='')
else:
return queryset.filter(rear_image='')
def _console_ports(self, queryset, name, value):
return queryset.exclude(consoleporttemplates__isnull=value)
@@ -780,6 +800,12 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter
to_field_name='slug',
label='Manufacturer (slug)',
)
device_type = django_filters.ModelMultipleChoiceFilter(
field_name='device_type__slug',
queryset=DeviceType.objects.all(),
to_field_name='slug',
label='Device type (slug)',
)
device_type_id = django_filters.ModelMultipleChoiceFilter(
queryset=DeviceType.objects.all(),
label='Device type (ID)',
@@ -1084,6 +1110,17 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
to_field_name='slug',
label='Location (slug)',
)
rack_id = django_filters.ModelMultipleChoiceFilter(
field_name='device__rack',
queryset=Rack.objects.all(),
label='Rack (ID)',
)
rack = django_filters.ModelMultipleChoiceFilter(
field_name='device__rack__name',
queryset=Rack.objects.all(),
to_field_name='name',
label='Rack (name)',
)
device_id = django_filters.ModelMultipleChoiceFilter(
queryset=Device.objects.all(),
label='Device (ID)',
@@ -1133,6 +1170,15 @@ class CabledObjectFilterSet(django_filters.FilterSet):
lookup_expr='isnull',
exclude=True
)
occupied = django_filters.BooleanFilter(
method='filter_occupied'
)
def filter_occupied(self, queryset, name, value):
if value:
return queryset.filter(Q(cable__isnull=False) | Q(mark_connected=True))
else:
return queryset.filter(cable__isnull=True, mark_connected=False)
class PathEndpointFilterSet(django_filters.FilterSet):
@@ -1317,7 +1363,7 @@ class InterfaceFilterSet(
try:
devices = Device.objects.filter(pk__in=id_list)
for device in devices:
vc_interface_ids += device.vc_interfaces().values_list('id', flat=True)
vc_interface_ids += device.vc_interfaces(if_master=False).values_list('id', flat=True)
return queryset.filter(pk__in=vc_interface_ids)
except Device.DoesNotExist:
return queryset.none()

View File

@@ -3,7 +3,7 @@ from django import forms
from dcim.models import *
from extras.forms import CustomFieldsMixin
from extras.models import Tag
from utilities.forms import DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model
from utilities.forms import BootstrapMixin, DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model
from .object_create import ComponentCreateForm
__all__ = (
@@ -24,7 +24,7 @@ __all__ = (
# Device components
#
class DeviceBulkAddComponentForm(CustomFieldsMixin, ComponentCreateForm):
class DeviceBulkAddComponentForm(BootstrapMixin, CustomFieldsMixin, ComponentCreateForm):
pk = forms.ModelMultipleChoiceField(
queryset=Device.objects.all(),
widget=forms.MultipleHiddenInput()
@@ -37,6 +37,7 @@ class DeviceBulkAddComponentForm(CustomFieldsMixin, ComponentCreateForm):
queryset=Tag.objects.all(),
required=False
)
replication_fields = ('name', 'label')
class ConsolePortBulkCreateForm(
@@ -44,7 +45,7 @@ class ConsolePortBulkCreateForm(
DeviceBulkAddComponentForm
):
model = ConsolePort
field_order = ('name_pattern', 'label_pattern', 'type', 'mark_connected', 'description', 'tags')
field_order = ('name', 'label', 'type', 'mark_connected', 'description', 'tags')
class ConsoleServerPortBulkCreateForm(
@@ -52,7 +53,7 @@ class ConsoleServerPortBulkCreateForm(
DeviceBulkAddComponentForm
):
model = ConsoleServerPort
field_order = ('name_pattern', 'label_pattern', 'type', 'speed', 'description', 'tags')
field_order = ('name', 'label', 'type', 'speed', 'description', 'tags')
class PowerPortBulkCreateForm(
@@ -60,7 +61,7 @@ class PowerPortBulkCreateForm(
DeviceBulkAddComponentForm
):
model = PowerPort
field_order = ('name_pattern', 'label_pattern', 'type', 'maximum_draw', 'allocated_draw', 'description', 'tags')
field_order = ('name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'tags')
class PowerOutletBulkCreateForm(
@@ -68,7 +69,7 @@ class PowerOutletBulkCreateForm(
DeviceBulkAddComponentForm
):
model = PowerOutlet
field_order = ('name_pattern', 'label_pattern', 'type', 'feed_leg', 'description', 'tags')
field_order = ('name', 'label', 'type', 'feed_leg', 'description', 'tags')
class InterfaceBulkCreateForm(
@@ -79,7 +80,7 @@ class InterfaceBulkCreateForm(
):
model = Interface
field_order = (
'name_pattern', 'label_pattern', 'type', 'enabled', 'speed', 'duplex', 'mtu', 'mgmt_only', 'poe_mode',
'name', 'label', 'type', 'enabled', 'speed', 'duplex', 'mtu', 'mgmt_only', 'poe_mode',
'poe_type', 'mark_connected', 'description', 'tags',
)
@@ -96,13 +97,13 @@ class RearPortBulkCreateForm(
DeviceBulkAddComponentForm
):
model = RearPort
field_order = ('name_pattern', 'label_pattern', 'type', 'positions', 'mark_connected', 'description', 'tags')
field_order = ('name', 'label', 'type', 'positions', 'mark_connected', 'description', 'tags')
class ModuleBayBulkCreateForm(DeviceBulkAddComponentForm):
model = ModuleBay
field_order = ('name_pattern', 'label_pattern', 'position_pattern', 'description', 'tags')
field_order = ('name', 'label', 'position_pattern', 'description', 'tags')
replication_fields = ('name', 'label', 'position')
position_pattern = ExpandableNameField(
label='Position',
required=False,
@@ -112,7 +113,7 @@ class ModuleBayBulkCreateForm(DeviceBulkAddComponentForm):
class DeviceBayBulkCreateForm(DeviceBulkAddComponentForm):
model = DeviceBay
field_order = ('name_pattern', 'label_pattern', 'description', 'tags')
field_order = ('name', 'label', 'description', 'tags')
class InventoryItemBulkCreateForm(
@@ -121,6 +122,6 @@ class InventoryItemBulkCreateForm(
):
model = InventoryItem
field_order = (
'name_pattern', 'label_pattern', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
'description', 'tags',
)

View File

@@ -480,7 +480,7 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm):
('Hardware', ('manufacturer', 'device_type', 'airflow', 'serial')),
)
nullable_fields = (
'tenant', 'platform', 'serial', 'airflow',
'location', 'tenant', 'platform', 'serial', 'airflow',
)

View File

@@ -108,7 +108,7 @@ def get_cable_form(a_type, b_type):
label='Power Feed',
disabled_indicator='_occupied',
query_params={
'powerpanel_id': f'$termination_{cable_end}_powerpanel',
'power_panel_id': f'$termination_{cable_end}_powerpanel',
}
)

View File

@@ -87,6 +87,15 @@ class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
},
label=_('Location')
)
rack_id = DynamicModelMultipleChoiceField(
queryset=Rack.objects.all(),
required=False,
query_params={
'site_id': '$site_id',
'location_id': '$location_id',
},
label=_('Rack')
)
virtual_chassis_id = DynamicModelMultipleChoiceField(
queryset=VirtualChassis.objects.all(),
required=False,
@@ -356,6 +365,7 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
fieldsets = (
(None, ('q', 'tag')),
('Hardware', ('manufacturer_id', 'part_number', 'subdevice_role', 'airflow')),
('Images', ('has_front_image', 'has_rear_image')),
('Components', (
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
'pass_through_ports', 'device_bays', 'module_bays', 'inventory_items',
@@ -377,6 +387,20 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
choices=add_blank_choice(DeviceAirflowChoices),
required=False
)
has_front_image = forms.NullBooleanField(
required=False,
label='Has a front image',
widget=StaticSelect(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
has_rear_image = forms.NullBooleanField(
required=False,
label='Has a rear image',
widget=StaticSelect(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
console_ports = forms.NullBooleanField(
required=False,
label='Has console ports',
@@ -927,12 +951,37 @@ class PowerFeedFilterForm(NetBoxModelFilterSetForm):
# Device components
#
class ConsolePortFilterForm(DeviceComponentFilterForm):
class CabledFilterForm(forms.Form):
cabled = forms.NullBooleanField(
required=False,
widget=StaticSelect(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
occupied = forms.NullBooleanField(
required=False,
widget=StaticSelect(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
class PathEndpointFilterForm(CabledFilterForm):
connected = forms.NullBooleanField(
required=False,
widget=StaticSelect(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = ConsolePort
fieldsets = (
(None, ('q', 'tag')),
('Attributes', ('name', 'label', 'type', 'speed')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
('Connection', ('cabled', 'connected', 'occupied')),
)
type = MultipleChoiceField(
choices=ConsolePortTypeChoices,
@@ -945,12 +994,13 @@ class ConsolePortFilterForm(DeviceComponentFilterForm):
tag = TagFilterField(model)
class ConsoleServerPortFilterForm(DeviceComponentFilterForm):
class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = ConsoleServerPort
fieldsets = (
(None, ('q', 'tag')),
('Attributes', ('name', 'label', 'type', 'speed')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
('Connection', ('cabled', 'connected', 'occupied')),
)
type = MultipleChoiceField(
choices=ConsolePortTypeChoices,
@@ -963,12 +1013,13 @@ class ConsoleServerPortFilterForm(DeviceComponentFilterForm):
tag = TagFilterField(model)
class PowerPortFilterForm(DeviceComponentFilterForm):
class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = PowerPort
fieldsets = (
(None, ('q', 'tag')),
('Attributes', ('name', 'label', 'type')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
('Connection', ('cabled', 'connected', 'occupied')),
)
type = MultipleChoiceField(
choices=PowerPortTypeChoices,
@@ -977,12 +1028,13 @@ class PowerPortFilterForm(DeviceComponentFilterForm):
tag = TagFilterField(model)
class PowerOutletFilterForm(DeviceComponentFilterForm):
class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = PowerOutlet
fieldsets = (
(None, ('q', 'tag')),
('Attributes', ('name', 'label', 'type')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
('Connection', ('cabled', 'connected', 'occupied')),
)
type = MultipleChoiceField(
choices=PowerOutletTypeChoices,
@@ -991,7 +1043,7 @@ class PowerOutletFilterForm(DeviceComponentFilterForm):
tag = TagFilterField(model)
class InterfaceFilterForm(DeviceComponentFilterForm):
class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = Interface
fieldsets = (
(None, ('q', 'tag')),
@@ -999,7 +1051,8 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
('Addressing', ('vrf_id', 'mac_address', 'wwn')),
('PoE', ('poe_mode', 'poe_type')),
('Wireless', ('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
('Connection', ('cabled', 'connected', 'occupied')),
)
kind = MultipleChoiceField(
choices=InterfaceKindChoices,
@@ -1080,11 +1133,12 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
tag = TagFilterField(model)
class FrontPortFilterForm(DeviceComponentFilterForm):
class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
fieldsets = (
(None, ('q', 'tag')),
('Attributes', ('name', 'label', 'type', 'color')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
('Cable', ('cabled', 'occupied')),
)
model = FrontPort
type = MultipleChoiceField(
@@ -1097,12 +1151,13 @@ class FrontPortFilterForm(DeviceComponentFilterForm):
tag = TagFilterField(model)
class RearPortFilterForm(DeviceComponentFilterForm):
class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
model = RearPort
fieldsets = (
(None, ('q', 'tag')),
('Attributes', ('name', 'label', 'type', 'color')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
('Cable', ('cabled', 'occupied')),
)
type = MultipleChoiceField(
choices=PortTypeChoices,
@@ -1119,7 +1174,7 @@ class ModuleBayFilterForm(DeviceComponentFilterForm):
fieldsets = (
(None, ('q', 'tag')),
('Attributes', ('name', 'label', 'position')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
)
tag = TagFilterField(model)
position = forms.CharField(
@@ -1132,7 +1187,7 @@ class DeviceBayFilterForm(DeviceComponentFilterForm):
fieldsets = (
(None, ('q', 'tag')),
('Attributes', ('name', 'label')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
)
tag = TagFilterField(model)
@@ -1142,7 +1197,7 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
fieldsets = (
(None, ('q', 'tag')),
('Attributes', ('name', 'label', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
)
role_id = DynamicModelMultipleChoiceField(
queryset=InventoryItemRole.objects.all(),

View File

@@ -78,6 +78,12 @@ class RegionForm(NetBoxModelForm):
)
slug = SlugField()
fieldsets = (
('Region', (
'parent', 'name', 'slug', 'description', 'tags',
)),
)
class Meta:
model = Region
fields = (
@@ -92,6 +98,12 @@ class SiteGroupForm(NetBoxModelForm):
)
slug = SlugField()
fieldsets = (
('Site Group', (
'parent', 'name', 'slug', 'description', 'tags',
)),
)
class Meta:
model = SiteGroup
fields = (
@@ -213,6 +225,12 @@ class LocationForm(TenancyForm, NetBoxModelForm):
class RackRoleForm(NetBoxModelForm):
slug = SlugField()
fieldsets = (
('Rack Role', (
'name', 'slug', 'color', 'description', 'tags',
)),
)
class Meta:
model = RackRole
fields = [
@@ -340,6 +358,12 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
class ManufacturerForm(NetBoxModelForm):
slug = SlugField()
fieldsets = (
('Manufacturer', (
'name', 'slug', 'description', 'tags',
)),
)
class Meta:
model = Manufacturer
fields = [
@@ -373,6 +397,7 @@ class DeviceTypeForm(NetBoxModelForm):
'front_image', 'rear_image', 'comments', 'tags',
]
widgets = {
'airflow': StaticSelect(),
'subdevice_role': StaticSelect(),
'front_image': ClearableFileInput(attrs={
'accept': DEVICETYPE_IMAGE_FORMATS
@@ -405,6 +430,12 @@ class ModuleTypeForm(NetBoxModelForm):
class DeviceRoleForm(NetBoxModelForm):
slug = SlugField()
fieldsets = (
('Device Role', (
'name', 'slug', 'color', 'vm_role', 'description', 'tags',
)),
)
class Meta:
model = DeviceRole
fields = [
@@ -421,6 +452,13 @@ class PlatformForm(NetBoxModelForm):
max_length=64
)
fieldsets = (
('Platform', (
'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'tags',
)),
)
class Meta:
model = Platform
fields = [
@@ -678,6 +716,7 @@ class ModuleForm(NetBoxModelForm):
super().__init__(*args, **kwargs)
if self.instance.pk:
self.fields['device'].disabled = True
self.fields['replicate_components'].initial = False
self.fields['replicate_components'].disabled = True
self.fields['adopt_components'].initial = False
@@ -838,10 +877,21 @@ class PowerFeedForm(NetBoxModelForm):
'site_id': '$site'
}
)
location = DynamicModelChoiceField(
queryset=Location.objects.all(),
required=False,
query_params={
'site_id': '$site'
},
initial_params={
'racks': '$rack'
}
)
rack = DynamicModelChoiceField(
queryset=Rack.objects.all(),
required=False,
query_params={
'location_id': '$location',
'site_id': '$site'
}
)
@@ -849,14 +899,14 @@ class PowerFeedForm(NetBoxModelForm):
fieldsets = (
('Power Panel', ('region', 'site', 'power_panel')),
('Power Feed', ('rack', 'name', 'status', 'type', 'mark_connected', 'tags')),
('Power Feed', ('location', 'rack', 'name', 'status', 'type', 'mark_connected', 'tags')),
('Characteristics', ('supply', 'voltage', 'amperage', 'phase', 'max_utilization')),
)
class Meta:
model = PowerFeed
fields = [
'region', 'site_group', 'site', 'power_panel', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply',
'region', 'site_group', 'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply',
'phase', 'voltage', 'amperage', 'max_utilization', 'comments', 'tags',
]
widgets = {
@@ -986,47 +1036,85 @@ class VCMemberSelectForm(BootstrapMixin, forms.Form):
# Device component templates
#
class ComponentTemplateForm(BootstrapMixin, forms.ModelForm):
device_type = DynamicModelChoiceField(
queryset=DeviceType.objects.all()
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Disable reassignment of DeviceType when editing an existing instance
if self.instance.pk:
self.fields['device_type'].disabled = True
class ModularComponentTemplateForm(ComponentTemplateForm):
device_type = DynamicModelChoiceField(
queryset=DeviceType.objects.all().all(),
required=False
)
module_type = DynamicModelChoiceField(
queryset=ModuleType.objects.all(),
required=False
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Disable reassignment of ModuleType when editing an existing instance
if self.instance.pk:
self.fields['module_type'].disabled = True
class ConsolePortTemplateForm(ModularComponentTemplateForm):
fieldsets = (
(None, ('device_type', 'module_type', 'name', 'label', 'type', 'description')),
)
class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = ConsolePortTemplate
fields = [
'device_type', 'module_type', 'name', 'label', 'type', 'description',
]
widgets = {
'device_type': forms.HiddenInput(),
'module_type': forms.HiddenInput(),
'type': StaticSelect,
}
class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
class ConsoleServerPortTemplateForm(ModularComponentTemplateForm):
fieldsets = (
(None, ('device_type', 'module_type', 'name', 'label', 'type', 'description')),
)
class Meta:
model = ConsoleServerPortTemplate
fields = [
'device_type', 'module_type', 'name', 'label', 'type', 'description',
]
widgets = {
'device_type': forms.HiddenInput(),
'module_type': forms.HiddenInput(),
'type': StaticSelect,
}
class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
class PowerPortTemplateForm(ModularComponentTemplateForm):
fieldsets = (
(None, (
'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
)),
)
class Meta:
model = PowerPortTemplate
fields = [
'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
]
widgets = {
'device_type': forms.HiddenInput(),
'module_type': forms.HiddenInput(),
'type': StaticSelect(),
}
class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
class PowerOutletTemplateForm(ModularComponentTemplateForm):
power_port = DynamicModelChoiceField(
queryset=PowerPortTemplate.objects.all(),
required=False,
@@ -1035,43 +1123,56 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
}
)
fieldsets = (
(None, ('device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description')),
)
class Meta:
model = PowerOutletTemplate
fields = [
'device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
]
widgets = {
'device_type': forms.HiddenInput(),
'module_type': forms.HiddenInput(),
'type': StaticSelect(),
'feed_leg': StaticSelect(),
}
class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
class InterfaceTemplateForm(ModularComponentTemplateForm):
fieldsets = (
(None, ('device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description')),
('PoE', ('poe_mode', 'poe_type'))
)
class Meta:
model = InterfaceTemplate
fields = [
'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description', 'poe_mode', 'poe_type',
]
widgets = {
'device_type': forms.HiddenInput(),
'module_type': forms.HiddenInput(),
'type': StaticSelect(),
'poe_mode': StaticSelect(),
'poe_type': StaticSelect(),
}
class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
class FrontPortTemplateForm(ModularComponentTemplateForm):
rear_port = DynamicModelChoiceField(
queryset=RearPortTemplate.objects.all(),
required=False,
query_params={
'devicetype_id': '$device_type',
'moduletype_id': '$module_type',
}
)
fieldsets = (
(None, (
'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
'description',
)),
)
class Meta:
model = FrontPortTemplate
fields = [
@@ -1079,48 +1180,50 @@ class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
'description',
]
widgets = {
'device_type': forms.HiddenInput(),
'module_type': forms.HiddenInput(),
'type': StaticSelect(),
}
class RearPortTemplateForm(BootstrapMixin, forms.ModelForm):
class RearPortTemplateForm(ModularComponentTemplateForm):
fieldsets = (
(None, ('device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description')),
)
class Meta:
model = RearPortTemplate
fields = [
'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description',
]
widgets = {
'device_type': forms.HiddenInput(),
'module_type': forms.HiddenInput(),
'type': StaticSelect(),
}
class ModuleBayTemplateForm(BootstrapMixin, forms.ModelForm):
class ModuleBayTemplateForm(ComponentTemplateForm):
fieldsets = (
(None, ('device_type', 'name', 'label', 'position', 'description')),
)
class Meta:
model = ModuleBayTemplate
fields = [
'device_type', 'name', 'label', 'position', 'description',
]
widgets = {
'device_type': forms.HiddenInput(),
}
class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm):
class DeviceBayTemplateForm(ComponentTemplateForm):
fieldsets = (
(None, ('device_type', 'name', 'label', 'description')),
)
class Meta:
model = DeviceBayTemplate
fields = [
'device_type', 'name', 'label', 'description',
]
widgets = {
'device_type': forms.HiddenInput(),
}
class InventoryItemTemplateForm(BootstrapMixin, forms.ModelForm):
class InventoryItemTemplateForm(ComponentTemplateForm):
parent = DynamicModelChoiceField(
queryset=InventoryItemTemplate.objects.all(),
required=False,
@@ -1147,22 +1250,39 @@ class InventoryItemTemplateForm(BootstrapMixin, forms.ModelForm):
widget=forms.HiddenInput
)
fieldsets = (
(None, (
'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
'component_type', 'component_id',
)),
)
class Meta:
model = InventoryItemTemplate
fields = [
'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
'component_type', 'component_id',
]
widgets = {
'device_type': forms.HiddenInput(),
}
#
# Device components
#
class ConsolePortForm(NetBoxModelForm):
class DeviceComponentForm(NetBoxModelForm):
device = DynamicModelChoiceField(
queryset=Device.objects.all()
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Disable reassignment of Device when editing an existing instance
if self.instance.pk:
self.fields['device'].disabled = True
class ModularDeviceComponentForm(DeviceComponentForm):
module = DynamicModelChoiceField(
queryset=Module.objects.all(),
required=False,
@@ -1171,25 +1291,31 @@ class ConsolePortForm(NetBoxModelForm):
}
)
class ConsolePortForm(ModularDeviceComponentForm):
fieldsets = (
(None, (
'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
)),
)
class Meta:
model = ConsolePort
fields = [
'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
]
widgets = {
'device': forms.HiddenInput(),
'type': StaticSelect(),
'speed': StaticSelect(),
}
class ConsoleServerPortForm(NetBoxModelForm):
module = DynamicModelChoiceField(
queryset=Module.objects.all(),
required=False,
query_params={
'device_id': '$device',
}
class ConsoleServerPortForm(ModularDeviceComponentForm):
fieldsets = (
(None, (
'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
)),
)
class Meta:
@@ -1198,42 +1324,32 @@ class ConsoleServerPortForm(NetBoxModelForm):
'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
]
widgets = {
'device': forms.HiddenInput(),
'type': StaticSelect(),
'speed': StaticSelect(),
}
class PowerPortForm(NetBoxModelForm):
module = DynamicModelChoiceField(
queryset=Module.objects.all(),
required=False,
query_params={
'device_id': '$device',
}
class PowerPortForm(ModularDeviceComponentForm):
fieldsets = (
(None, (
'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
'description', 'tags',
)),
)
class Meta:
model = PowerPort
fields = [
'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
'description',
'tags',
'description', 'tags',
]
widgets = {
'device': forms.HiddenInput(),
'type': StaticSelect(),
}
class PowerOutletForm(NetBoxModelForm):
module = DynamicModelChoiceField(
queryset=Module.objects.all(),
required=False,
query_params={
'device_id': '$device',
}
)
class PowerOutletForm(ModularDeviceComponentForm):
power_port = DynamicModelChoiceField(
queryset=PowerPort.objects.all(),
required=False,
@@ -1242,6 +1358,13 @@ class PowerOutletForm(NetBoxModelForm):
}
)
fieldsets = (
(None, (
'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg', 'mark_connected', 'description',
'tags',
)),
)
class Meta:
model = PowerOutlet
fields = [
@@ -1249,20 +1372,12 @@ class PowerOutletForm(NetBoxModelForm):
'tags',
]
widgets = {
'device': forms.HiddenInput(),
'type': StaticSelect(),
'feed_leg': StaticSelect(),
}
class InterfaceForm(InterfaceCommonForm, NetBoxModelForm):
module = DynamicModelChoiceField(
queryset=Module.objects.all(),
required=False,
query_params={
'device_id': '$device',
}
)
class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
parent = DynamicModelChoiceField(
queryset=Interface.objects.all(),
required=False,
@@ -1330,8 +1445,14 @@ class InterfaceForm(InterfaceCommonForm, NetBoxModelForm):
label='VRF'
)
wwn = forms.CharField(
empty_value=None,
required=False,
label='WWN'
)
fieldsets = (
('Interface', ('device', 'module', 'name', 'type', 'speed', 'duplex', 'label', 'description', 'tags')),
('Interface', ('device', 'module', 'name', 'label', 'type', 'speed', 'duplex', 'description', 'tags')),
('Addressing', ('vrf', 'mac_address', 'wwn')),
('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
('Related Interfaces', ('parent', 'bridge', 'lag')),
@@ -1351,7 +1472,6 @@ class InterfaceForm(InterfaceCommonForm, NetBoxModelForm):
'untagged_vlan', 'tagged_vlans', 'vrf', 'tags',
]
widgets = {
'device': forms.HiddenInput(),
'type': StaticSelect(),
'speed': SelectSpeedWidget(),
'poe_mode': StaticSelect(),
@@ -1370,25 +1490,8 @@ class InterfaceForm(InterfaceCommonForm, NetBoxModelForm):
'rf_channel_width': "Populated by selected channel (if set)",
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Restrict LAG/bridge interface assignment by device/VC
device_id = self.data['device'] if self.is_bound else self.initial.get('device')
device = Device.objects.filter(pk=device_id).first()
if device and device.virtual_chassis and device.virtual_chassis.master:
self.fields['lag'].widget.add_query_param('device_id', device.virtual_chassis.master.pk)
self.fields['bridge'].widget.add_query_param('device_id', device.virtual_chassis.master.pk)
class FrontPortForm(NetBoxModelForm):
module = DynamicModelChoiceField(
queryset=Module.objects.all(),
required=False,
query_params={
'device_id': '$device',
}
)
class FrontPortForm(ModularDeviceComponentForm):
rear_port = DynamicModelChoiceField(
queryset=RearPort.objects.all(),
query_params={
@@ -1396,6 +1499,13 @@ class FrontPortForm(NetBoxModelForm):
}
)
fieldsets = (
(None, (
'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected',
'description', 'tags',
)),
)
class Meta:
model = FrontPort
fields = [
@@ -1403,18 +1513,15 @@ class FrontPortForm(NetBoxModelForm):
'description', 'tags',
]
widgets = {
'device': forms.HiddenInput(),
'type': StaticSelect(),
}
class RearPortForm(NetBoxModelForm):
module = DynamicModelChoiceField(
queryset=Module.objects.all(),
required=False,
query_params={
'device_id': '$device',
}
class RearPortForm(ModularDeviceComponentForm):
fieldsets = (
(None, (
'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags',
)),
)
class Meta:
@@ -1423,33 +1530,32 @@ class RearPortForm(NetBoxModelForm):
'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags',
]
widgets = {
'device': forms.HiddenInput(),
'type': StaticSelect(),
}
class ModuleBayForm(NetBoxModelForm):
class ModuleBayForm(DeviceComponentForm):
fieldsets = (
(None, ('device', 'name', 'label', 'position', 'description', 'tags',)),
)
class Meta:
model = ModuleBay
fields = [
'device', 'name', 'label', 'position', 'description', 'tags',
]
widgets = {
'device': forms.HiddenInput(),
}
class DeviceBayForm(NetBoxModelForm):
class DeviceBayForm(DeviceComponentForm):
fieldsets = (
(None, ('device', 'name', 'label', 'description', 'tags',)),
)
class Meta:
model = DeviceBay
fields = [
'device', 'name', 'label', 'description', 'tags',
]
widgets = {
'device': forms.HiddenInput(),
}
class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
@@ -1472,10 +1578,7 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
).exclude(pk=device_bay.device.pk)
class InventoryItemForm(NetBoxModelForm):
device = DynamicModelChoiceField(
queryset=Device.objects.all()
)
class InventoryItemForm(DeviceComponentForm):
parent = DynamicModelChoiceField(
queryset=InventoryItem.objects.all(),
required=False,
@@ -1522,6 +1625,12 @@ class InventoryItemForm(NetBoxModelForm):
class InventoryItemRoleForm(NetBoxModelForm):
slug = SlugField()
fieldsets = (
('Inventory Item Role', (
'name', 'slug', 'color', 'description', 'tags',
)),
)
class Meta:
model = InventoryItemRole
fields = [

View File

@@ -2,46 +2,56 @@ from django import forms
from dcim.models import *
from netbox.forms import NetBoxModelForm
from utilities.forms import (
BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField,
)
from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField
from . import models as model_forms
__all__ = (
'ComponentTemplateCreateForm',
'DeviceComponentCreateForm',
'ComponentCreateForm',
'ConsolePortCreateForm',
'ConsolePortTemplateCreateForm',
'ConsoleServerPortCreateForm',
'ConsoleServerPortTemplateCreateForm',
'DeviceBayCreateForm',
'DeviceBayTemplateCreateForm',
'FrontPortCreateForm',
'FrontPortTemplateCreateForm',
'InterfaceCreateForm',
'InterfaceTemplateCreateForm',
'InventoryItemCreateForm',
'ModularComponentTemplateCreateForm',
'InventoryItemTemplateCreateForm',
'ModuleBayCreateForm',
'ModuleBayTemplateCreateForm',
'PowerOutletCreateForm',
'PowerOutletTemplateCreateForm',
'PowerPortCreateForm',
'PowerPortTemplateCreateForm',
'RearPortCreateForm',
'RearPortTemplateCreateForm',
'VirtualChassisCreateForm',
)
class ComponentCreateForm(BootstrapMixin, forms.Form):
class ComponentCreateForm(forms.Form):
"""
Subclass this form when facilitating the creation of one or more device component or component templates based on
Subclass this form when facilitating the creation of one or more component or component template objects based on
a name pattern.
"""
name_pattern = ExpandableNameField(
label='Name'
)
label_pattern = ExpandableNameField(
label='Label',
name = ExpandableNameField()
label = ExpandableNameField(
required=False,
help_text='Alphanumeric ranges are supported. (Must match the number of names being created.)'
help_text='Alphanumeric ranges are supported. (Must match the number of objects being created.)'
)
# Identify the fields which support replication (i.e. ExpandableNameFields). This is referenced by
# ComponentCreateView when creating objects.
replication_fields = ('name', 'label')
def clean(self):
super().clean()
# Validate that all patterned fields generate an equal number of values
patterned_fields = [
field_name for field_name in self.fields if field_name.endswith('_pattern')
]
pattern_count = len(self.cleaned_data['name_pattern'])
for field_name in patterned_fields:
# Validate that all replication fields generate an equal number of values
pattern_count = len(self.cleaned_data[self.replication_fields[0]])
for field_name in self.replication_fields:
value_count = len(self.cleaned_data[field_name])
if self.cleaned_data[field_name] and value_count != pattern_count:
raise forms.ValidationError({
@@ -50,56 +60,55 @@ class ComponentCreateForm(BootstrapMixin, forms.Form):
}, code='label_pattern_mismatch')
class ComponentTemplateCreateForm(ComponentCreateForm):
"""
Creation form for component templates that can be assigned only to a DeviceType.
"""
device_type = DynamicModelChoiceField(
queryset=DeviceType.objects.all(),
)
field_order = ('device_type', 'name_pattern', 'label_pattern')
#
# Device component templates
#
class ConsolePortTemplateCreateForm(ComponentCreateForm, model_forms.ConsolePortTemplateForm):
class Meta(model_forms.ConsolePortTemplateForm.Meta):
exclude = ('name', 'label')
class ModularComponentTemplateCreateForm(ComponentCreateForm):
"""
Creation form for component templates that can be assigned to either a DeviceType *or* a ModuleType.
"""
name_pattern = ExpandableNameField(
label='Name',
help_text="""
Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range
are not supported. Example: <code>[ge,xe]-0/0/[0-9]</code>. {module} is accepted as a substitution for
the module bay position.
"""
)
device_type = DynamicModelChoiceField(
queryset=DeviceType.objects.all(),
required=False
)
module_type = DynamicModelChoiceField(
queryset=ModuleType.objects.all(),
required=False
)
field_order = ('device_type', 'module_type', 'name_pattern', 'label_pattern')
class ConsoleServerPortTemplateCreateForm(ComponentCreateForm, model_forms.ConsoleServerPortTemplateForm):
class Meta(model_forms.ConsoleServerPortTemplateForm.Meta):
exclude = ('name', 'label')
class DeviceComponentCreateForm(ComponentCreateForm):
device = DynamicModelChoiceField(
queryset=Device.objects.all()
)
field_order = ('device', 'name_pattern', 'label_pattern')
class PowerPortTemplateCreateForm(ComponentCreateForm, model_forms.PowerPortTemplateForm):
class Meta(model_forms.PowerPortTemplateForm.Meta):
exclude = ('name', 'label')
class FrontPortTemplateCreateForm(ModularComponentTemplateCreateForm):
rear_port_set = forms.MultipleChoiceField(
class PowerOutletTemplateCreateForm(ComponentCreateForm, model_forms.PowerOutletTemplateForm):
class Meta(model_forms.PowerOutletTemplateForm.Meta):
exclude = ('name', 'label')
class InterfaceTemplateCreateForm(ComponentCreateForm, model_forms.InterfaceTemplateForm):
class Meta(model_forms.InterfaceTemplateForm.Meta):
exclude = ('name', 'label')
class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemplateForm):
rear_port = forms.MultipleChoiceField(
choices=[],
label='Rear ports',
help_text='Select one rear port assignment for each front port being created.',
)
field_order = (
'device_type', 'name_pattern', 'label_pattern', 'rear_port_set',
# Override fieldsets from FrontPortTemplateForm to omit rear_port_position
fieldsets = (
(None, ('device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'description')),
)
class Meta(model_forms.FrontPortTemplateForm.Meta):
exclude = ('name', 'label', 'rear_port', 'rear_port_position')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -130,12 +139,12 @@ class FrontPortTemplateCreateForm(ModularComponentTemplateCreateForm):
choices.append(
('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
)
self.fields['rear_port_set'].choices = choices
self.fields['rear_port'].choices = choices
def get_iterative_data(self, iteration):
# Assign rear port and position from selected set
rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':')
rear_port, position = self.cleaned_data['rear_port'][iteration].split(':')
return {
'rear_port': int(rear_port),
@@ -143,16 +152,94 @@ class FrontPortTemplateCreateForm(ModularComponentTemplateCreateForm):
}
class FrontPortCreateForm(DeviceComponentCreateForm):
rear_port_set = forms.MultipleChoiceField(
class RearPortTemplateCreateForm(ComponentCreateForm, model_forms.RearPortTemplateForm):
class Meta(model_forms.RearPortTemplateForm.Meta):
exclude = ('name', 'label')
class DeviceBayTemplateCreateForm(ComponentCreateForm, model_forms.DeviceBayTemplateForm):
class Meta(model_forms.DeviceBayTemplateForm.Meta):
exclude = ('name', 'label')
class ModuleBayTemplateCreateForm(ComponentCreateForm, model_forms.ModuleBayTemplateForm):
position = ExpandableNameField(
label='Position',
required=False,
help_text='Alphanumeric ranges are supported. (Must match the number of objects being created.)'
)
replication_fields = ('name', 'label', 'position')
class Meta(model_forms.ModuleBayTemplateForm.Meta):
exclude = ('name', 'label', 'position')
class InventoryItemTemplateCreateForm(ComponentCreateForm, model_forms.InventoryItemTemplateForm):
class Meta(model_forms.InventoryItemTemplateForm.Meta):
exclude = ('name', 'label')
#
# Device components
#
class ConsolePortCreateForm(ComponentCreateForm, model_forms.ConsolePortForm):
class Meta(model_forms.ConsolePortForm.Meta):
exclude = ('name', 'label')
class ConsoleServerPortCreateForm(ComponentCreateForm, model_forms.ConsoleServerPortForm):
class Meta(model_forms.ConsoleServerPortForm.Meta):
exclude = ('name', 'label')
class PowerPortCreateForm(ComponentCreateForm, model_forms.PowerPortForm):
class Meta(model_forms.PowerPortForm.Meta):
exclude = ('name', 'label')
class PowerOutletCreateForm(ComponentCreateForm, model_forms.PowerOutletForm):
class Meta(model_forms.PowerOutletForm.Meta):
exclude = ('name', 'label')
class InterfaceCreateForm(ComponentCreateForm, model_forms.InterfaceForm):
class Meta(model_forms.InterfaceForm.Meta):
exclude = ('name', 'label')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if 'module' in self.fields:
self.fields['name'].help_text += ' The string <code>{module}</code> will be replaced with the position ' \
'of the assigned module, if any'
class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
rear_port = forms.MultipleChoiceField(
choices=[],
label='Rear ports',
help_text='Select one rear port assignment for each front port being created.',
)
field_order = (
'device', 'name_pattern', 'label_pattern', 'rear_port_set',
# Override fieldsets from FrontPortForm to omit rear_port_position
fieldsets = (
(None, (
'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'mark_connected', 'description', 'tags',
)),
)
class Meta(model_forms.FrontPortForm.Meta):
exclude = ('name', 'label', 'rear_port', 'rear_port_position')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -176,12 +263,12 @@ class FrontPortCreateForm(DeviceComponentCreateForm):
choices.append(
('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
)
self.fields['rear_port_set'].choices = choices
self.fields['rear_port'].choices = choices
def get_iterative_data(self, iteration):
# Assign rear port and position from selected set
rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':')
rear_port, position = self.cleaned_data['rear_port'][iteration].split(':')
return {
'rear_port': int(rear_port),
@@ -189,28 +276,39 @@ class FrontPortCreateForm(DeviceComponentCreateForm):
}
class ModuleBayTemplateCreateForm(ComponentTemplateCreateForm):
position_pattern = ExpandableNameField(
class RearPortCreateForm(ComponentCreateForm, model_forms.RearPortForm):
class Meta(model_forms.RearPortForm.Meta):
exclude = ('name', 'label')
class DeviceBayCreateForm(ComponentCreateForm, model_forms.DeviceBayForm):
class Meta(model_forms.DeviceBayForm.Meta):
exclude = ('name', 'label')
class ModuleBayCreateForm(ComponentCreateForm, model_forms.ModuleBayForm):
position = ExpandableNameField(
label='Position',
required=False,
help_text='Alphanumeric ranges are supported. (Must match the number of names being created.)'
help_text='Alphanumeric ranges are supported. (Must match the number of objects being created.)'
)
field_order = ('device_type', 'name_pattern', 'label_pattern', 'position_pattern')
replication_fields = ('name', 'label', 'position')
class Meta(model_forms.ModuleBayForm.Meta):
exclude = ('name', 'label', 'position')
class ModuleBayCreateForm(DeviceComponentCreateForm):
position_pattern = ExpandableNameField(
label='Position',
required=False,
help_text='Alphanumeric ranges are supported. (Must match the number of names being created.)'
)
field_order = ('device', 'name_pattern', 'label_pattern', 'position_pattern')
class InventoryItemCreateForm(ComponentCreateForm, model_forms.InventoryItemForm):
class Meta(model_forms.InventoryItemForm.Meta):
exclude = ('name', 'label')
class InventoryItemCreateForm(ComponentCreateForm):
# Device is assigned by the model form
field_order = ('name_pattern', 'label_pattern')
#
# Virtual chassis
#
class VirtualChassisCreateForm(NetBoxModelForm):
region = DynamicModelChoiceField(

View File

@@ -281,15 +281,11 @@ class CableTermination(models.Model):
# Validate interface type (if applicable)
if self.termination_type.model == 'interface' and self.termination.type in NONCONNECTABLE_IFACE_TYPES:
raise ValidationError({
'termination': f'Cables cannot be terminated to {self.termination.get_type_display()} interfaces'
})
raise ValidationError(f"Cables cannot be terminated to {self.termination.get_type_display()} interfaces")
# A CircuitTermination attached to a ProviderNetwork cannot have a Cable
if self.termination_type.model == 'circuittermination' and self.termination.provider_network is not None:
raise ValidationError({
'termination': "Circuit terminations attached to a provider network may not be cabled."
})
raise ValidationError("Circuit terminations attached to a provider network may not be cabled.")
def save(self, *args, **kwargs):
@@ -677,6 +673,12 @@ class CablePath(models.Model):
"""
Return all available next segments in a split cable path.
"""
rearports = self.path_objects[-1]
nodes = self.path_objects[-1]
return FrontPort.objects.filter(rear_port__in=rearports)
# RearPort splitting to multiple FrontPorts with no stack position
if type(nodes[0]) is RearPort:
return FrontPort.objects.filter(rear_port__in=nodes)
# Cable terminating to multiple FrontPorts mapped to different
# RearPorts connected to different cables
elif type(nodes[0]) is FrontPort:
return RearPort.objects.filter(pk__in=[fp.rear_port_id for fp in nodes])

View File

@@ -908,18 +908,20 @@ class FrontPort(ModularComponentModel, CabledObjectModel):
def clean(self):
super().clean()
# Validate rear port assignment
if self.rear_port.device != self.device:
raise ValidationError({
"rear_port": f"Rear port ({self.rear_port}) must belong to the same device"
})
if hasattr(self, 'rear_port'):
# Validate rear port position assignment
if self.rear_port_position > self.rear_port.positions:
raise ValidationError({
"rear_port_position": f"Invalid rear port position ({self.rear_port_position}): Rear port "
f"{self.rear_port.name} has only {self.rear_port.positions} positions"
})
# Validate rear port assignment
if self.rear_port.device != self.device:
raise ValidationError({
"rear_port": f"Rear port ({self.rear_port}) must belong to the same device"
})
# Validate rear port position assignment
if self.rear_port_position > self.rear_port.positions:
raise ValidationError({
"rear_port_position": f"Invalid rear port position ({self.rear_port_position}): Rear port "
f"{self.rear_port.name} has only {self.rear_port.positions} positions"
})
class RearPort(ModularComponentModel, CabledObjectModel):

View File

@@ -1,6 +1,8 @@
import decimal
import yaml
from django.apps import apps
from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
@@ -159,9 +161,17 @@ class DeviceType(NetBoxModel):
self._original_front_image = self.front_image
self._original_rear_image = self.rear_image
@classmethod
def get_prerequisite_models(cls):
return [Manufacturer, ]
def get_absolute_url(self):
return reverse('dcim:devicetype', args=[self.pk])
@property
def get_full_name(self):
return f"{ self.manufacturer } { self.model }"
def to_yaml(self):
data = {
'manufacturer': self.manufacturer.name,
@@ -338,6 +348,10 @@ class ModuleType(NetBoxModel):
def __str__(self):
return self.model
@classmethod
def get_prerequisite_models(cls):
return [Manufacturer, ]
def get_absolute_url(self):
return reverse('dcim:moduletype', args=[self.pk])
@@ -658,6 +672,10 @@ class Device(NetBoxModel, ConfigContextModel):
return f'{self.device_type.manufacturer} {self.device_type.model} ({self.pk})'
return super().__str__()
@classmethod
def get_prerequisite_models(cls):
return [apps.get_model('dcim.Site'), DeviceRole, DeviceType, ]
def get_absolute_url(self):
return reverse('dcim:device', args=[self.pk])
@@ -850,6 +868,7 @@ class Device(NetBoxModel, ConfigContextModel):
for device in devices:
device.site = self.site
device.rack = self.rack
device.location = self.location
device.save()
@property
@@ -968,6 +987,14 @@ class Module(NetBoxModel, ConfigContextModel):
def get_absolute_url(self):
return reverse('dcim:module', args=[self.pk])
def clean(self):
super().clean()
if self.module_bay.device != self.device:
raise ValidationError(
f"Module must be installed within a module bay belonging to the assigned device ({self.device})."
)
def save(self, *args, **kwargs):
is_new = self.pk is None

View File

@@ -1,3 +1,4 @@
from django.apps import apps
from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
@@ -54,6 +55,10 @@ class PowerPanel(NetBoxModel):
def __str__(self):
return self.name
@classmethod
def get_prerequisite_models(cls):
return [apps.get_model('dcim.Site'), ]
def get_absolute_url(self):
return reverse('dcim:powerpanel', args=[self.pk])
@@ -138,6 +143,10 @@ class PowerFeed(NetBoxModel, PathEndpoint, CabledObjectModel):
def __str__(self):
return self.name
@classmethod
def get_prerequisite_models(cls):
return [PowerPanel, ]
def get_absolute_url(self):
return reverse('dcim:powerfeed', args=[self.pk])

View File

@@ -1,5 +1,6 @@
import decimal
from django.apps import apps
from django.contrib.auth.models import User
from django.contrib.contenttypes.fields import GenericRelation
from django.contrib.contenttypes.models import ContentType
@@ -201,6 +202,10 @@ class Rack(NetBoxModel):
return f'{self.name} ({self.facility_id})'
return self.name
@classmethod
def get_prerequisite_models(cls):
return [apps.get_model('dcim.Site'), ]
def get_absolute_url(self):
return reverse('dcim:rack', args=[self.pk])
@@ -345,7 +350,7 @@ class Rack(NetBoxModel):
# Remove units without enough space above them to accommodate a device of the specified height
available_units = []
for u in units:
if set(drange(u, u + u_height, 0.5)).issubset(units):
if set(drange(u, u + decimal.Decimal(u_height), 0.5)).issubset(units):
available_units.append(u)
return list(reversed(available_units))
@@ -410,12 +415,13 @@ class Rack(NetBoxModel):
"""
# Determine unoccupied units
total_units = len(list(self.units))
available_units = self.get_available_units()
available_units = self.get_available_units(u_height=0.5)
# Remove reserved units
for u in self.get_reserved_units():
if u in available_units:
available_units.remove(u)
for ru in self.get_reserved_units():
for u in drange(ru, ru + 1, 0.5):
if u in available_units:
available_units.remove(u)
occupied_unit_count = total_units - len(available_units)
percentage = float(occupied_unit_count) / total_units * 100
@@ -477,6 +483,10 @@ class RackReservation(NetBoxModel):
def __str__(self):
return "Reservation for rack {}".format(self.rack)
@classmethod
def get_prerequisite_models(cls):
return [apps.get_model('dcim.Site'), Rack, ]
def get_absolute_url(self):
return reverse('dcim:rackreservation', args=[self.pk])

View File

@@ -411,6 +411,10 @@ class Location(NestedGroupModel):
super().validate_unique(exclude=exclude)
@classmethod
def get_prerequisite_models(cls):
return [Site, ]
def get_absolute_url(self):
return reverse('dcim:location', args=[self.pk])

View File

@@ -35,7 +35,7 @@ class Node(Hyperlink):
"""
def __init__(self, position, width, url, color, labels, radius=10, **extra):
super(Node, self).__init__(href=url, target='_blank', **extra)
super(Node, self).__init__(href=url, target='_parent', **extra)
x, y = position
@@ -94,7 +94,7 @@ class Connector(Group):
self.add(cable)
# Add link
link = Hyperlink(href=url, target='_blank')
link = Hyperlink(href=url, target='_parent')
# Add text label(s)
cursor = start[1]
@@ -166,7 +166,7 @@ class CableTraceSVG:
"""
if hasattr(instance, 'parent_object'):
# Termination
return 'f0f0f0'
return getattr(instance, 'color', 'f0f0f0') or 'f0f0f0'
if hasattr(instance, 'device_role'):
# Device
return instance.device_role.color
@@ -281,7 +281,7 @@ class CableTraceSVG:
self.cursor += PADDING * 2
# Add link
link = Hyperlink(href=f'{self.base_url}{wirelesslink.get_absolute_url()}', target='_blank')
link = Hyperlink(href=f'{self.base_url}{wirelesslink.get_absolute_url()}', target='_parent')
# Add text label(s)
for i, label in enumerate(labels):

View File

@@ -9,6 +9,7 @@ from svgwrite.text import Text
from django.conf import settings
from django.core.exceptions import FieldError
from django.db.models import Q
from django.template.defaultfilters import floatformat
from django.urls import reverse
from django.utils.http import urlencode
@@ -41,7 +42,7 @@ def get_device_description(device):
device.device_role,
device.device_type.manufacturer.name,
device.device_type.model,
device.device_type.u_height,
floatformat(device.device_type.u_height),
device.asset_tag or '',
device.serial or ''
)
@@ -151,7 +152,7 @@ class RackElevationSVG:
css_extra = ' shaded' if is_shaded else ''
# Create hyperlink element
link = Hyperlink(href=f'{self.base_url}{device.get_absolute_url()}', target='_blank')
link = Hyperlink(href=f'{self.base_url}{device.get_absolute_url()}', target="_parent")
link.set_desc(description)
# Add rect element to hyperlink
@@ -235,10 +236,7 @@ class RackElevationSVG:
self.margin_width,
u_height * self.unit_height
)
link = Hyperlink(
href='{}{}'.format(self.base_url, reservation.get_absolute_url()),
target='_blank'
)
link = Hyperlink(href=f'{self.base_url}{reservation.get_absolute_url()}', target='_parent')
link.set_desc(f'Reservation #{reservation.pk}: {reservation.description}')
link.add(
Rect(coords, size, class_='reservation')
@@ -268,7 +266,7 @@ class RackElevationSVG:
y_offset + self.unit_height / 2
)
link = Hyperlink(href=url_string.format(unit), target='_blank')
link = Hyperlink(href=url_string.format(unit), target='_parent')
link.add(Rect((x_offset, y_offset), (self.unit_width, self.unit_height), class_='slot'))
link.add(Text('add device', insert=text_coords, class_='add-device'))

View File

@@ -1,109 +1,8 @@
import django_tables2 as tables
from django_tables2.utils import Accessor
from netbox.tables import BaseTable, columns
from dcim.models import ConsolePort, Interface, PowerPort
from .cables import *
from .connections import *
from .devices import *
from .devicetypes import *
from .modules import *
from .power import *
from .racks import *
from .sites import *
#
# Device connections
#
class ConsoleConnectionTable(BaseTable):
console_server = tables.Column(
accessor=Accessor('_path__destination__device'),
orderable=False,
linkify=True,
verbose_name='Console Server'
)
console_server_port = tables.Column(
accessor=Accessor('_path__destination'),
orderable=False,
linkify=True,
verbose_name='Port'
)
device = tables.Column(
linkify=True
)
name = tables.Column(
linkify=True,
verbose_name='Console Port'
)
reachable = columns.BooleanColumn(
accessor=Accessor('_path__is_active'),
verbose_name='Reachable'
)
class Meta(BaseTable.Meta):
model = ConsolePort
fields = ('device', 'name', 'console_server', 'console_server_port', 'reachable')
class PowerConnectionTable(BaseTable):
pdu = tables.Column(
accessor=Accessor('_path__destination__device'),
orderable=False,
linkify=True,
verbose_name='PDU'
)
outlet = tables.Column(
accessor=Accessor('_path__destination'),
orderable=False,
linkify=True,
verbose_name='Outlet'
)
device = tables.Column(
linkify=True
)
name = tables.Column(
linkify=True,
verbose_name='Power Port'
)
reachable = columns.BooleanColumn(
accessor=Accessor('_path__is_active'),
verbose_name='Reachable'
)
class Meta(BaseTable.Meta):
model = PowerPort
fields = ('device', 'name', 'pdu', 'outlet', 'reachable')
class InterfaceConnectionTable(BaseTable):
device_a = tables.Column(
accessor=Accessor('device'),
linkify=True,
verbose_name='Device A'
)
interface_a = tables.Column(
accessor=Accessor('name'),
linkify=True,
verbose_name='Interface A'
)
device_b = tables.Column(
accessor=Accessor('_path__destination__device'),
orderable=False,
linkify=True,
verbose_name='Device B'
)
interface_b = tables.Column(
accessor=Accessor('_path__destination'),
orderable=False,
linkify=True,
verbose_name='Interface B'
)
reachable = columns.BooleanColumn(
accessor=Accessor('_path__is_active'),
verbose_name='Reachable'
)
class Meta(BaseTable.Meta):
model = Interface
fields = ('device_a', 'interface_a', 'device_b', 'interface_b', 'reachable')

View File

@@ -0,0 +1,71 @@
import django_tables2 as tables
from django_tables2.utils import Accessor
from netbox.tables import BaseTable, columns
from dcim.models import ConsolePort, Interface, PowerPort
from .devices import PathEndpointTable
__all__ = (
'ConsoleConnectionTable',
'InterfaceConnectionTable',
'PowerConnectionTable',
)
#
# Device connections
#
class ConsoleConnectionTable(PathEndpointTable):
device = tables.Column(
linkify=True
)
name = tables.Column(
linkify=True,
verbose_name='Console Port'
)
reachable = columns.BooleanColumn(
accessor=Accessor('_path__is_active'),
verbose_name='Reachable'
)
class Meta(BaseTable.Meta):
model = ConsolePort
fields = ('device', 'name', 'connection', 'reachable')
class PowerConnectionTable(PathEndpointTable):
device = tables.Column(
linkify=True
)
name = tables.Column(
linkify=True,
verbose_name='Power Port'
)
reachable = columns.BooleanColumn(
accessor=Accessor('_path__is_active'),
verbose_name='Reachable'
)
class Meta(BaseTable.Meta):
model = PowerPort
fields = ('device', 'name', 'connection', 'reachable')
class InterfaceConnectionTable(PathEndpointTable):
device = tables.Column(
accessor=Accessor('device'),
linkify=True
)
interface = tables.Column(
accessor=Accessor('name'),
linkify=True
)
reachable = columns.BooleanColumn(
accessor=Accessor('_path__is_active'),
verbose_name='Reachable'
)
class Meta(BaseTable.Meta):
model = Interface
fields = ('device', 'interface', 'connection', 'reachable')

View File

@@ -1,12 +1,26 @@
import django_tables2 as tables
from django_tables2.utils import Accessor
from dcim.models import (
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, FrontPort, Interface, InventoryItem,
InventoryItemRole, ModuleBay, Platform, PowerOutlet, PowerPort, RearPort, VirtualChassis,
ConsolePort,
ConsoleServerPort,
Device,
DeviceBay,
DeviceRole,
FrontPort,
Interface,
InventoryItem,
InventoryItemRole,
ModuleBay,
Platform,
PowerOutlet,
PowerPort,
RearPort,
VirtualChassis,
)
from django_tables2.utils import Accessor
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenancyColumnsMixin
from .template_code import *
__all__ = (
@@ -137,12 +151,21 @@ class PlatformTable(NetBoxTable):
# Devices
#
class DeviceTable(TenancyColumnsMixin, NetBoxTable):
class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
name = tables.TemplateColumn(
order_by=('_name',),
template_code=DEVICE_LINK
)
status = columns.ChoiceFieldColumn()
region = tables.Column(
accessor=Accessor('site__region'),
linkify=True
)
site_group = tables.Column(
accessor=Accessor('site__group'),
linkify=True,
verbose_name='Site Group'
)
site = tables.Column(
linkify=True
)
@@ -152,6 +175,9 @@ class DeviceTable(TenancyColumnsMixin, NetBoxTable):
rack = tables.Column(
linkify=True
)
position = columns.TemplateColumn(
template_code='{{ value|floatformat }}'
)
device_role = columns.ColoredLabelColumn(
verbose_name='Role'
)
@@ -189,9 +215,6 @@ class DeviceTable(TenancyColumnsMixin, NetBoxTable):
verbose_name='VC Priority'
)
comments = columns.MarkdownColumn()
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
url_name='dcim:device_list'
)
@@ -199,10 +222,10 @@ class DeviceTable(TenancyColumnsMixin, NetBoxTable):
class Meta(NetBoxTable.Meta):
model = Device
fields = (
'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'device_role', 'manufacturer', 'device_type', 'platform', 'serial',
'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'primary_ip', 'airflow', 'primary_ip4',
'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'contacts', 'tags',
'created', 'last_updated',
'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'device_role', 'manufacturer', 'device_type',
'platform', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'location', 'rack', 'position', 'face',
'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position',
'vc_priority', 'comments', 'contacts', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type',
@@ -483,6 +506,12 @@ class BaseInterfaceTable(NetBoxTable):
orderable=False,
verbose_name='FHRP Groups'
)
l2vpn = tables.Column(
accessor=tables.A('l2vpn_termination__l2vpn'),
linkify=True,
orderable=False,
verbose_name='L2VPN'
)
untagged_vlan = tables.Column(linkify=True)
tagged_vlans = columns.TemplateColumn(
template_code=INTERFACE_TAGGED_VLANS,
@@ -520,8 +549,8 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'enabled', 'type', 'mgmt_only', 'mtu',
'speed', 'duplex', 'mode', 'mac_address', 'wwn', 'poe_mode', 'poe_type', 'rf_role', 'rf_channel',
'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable',
'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'vrf', 'ip_addresses',
'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created', 'last_updated',
'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'vrf', 'l2vpn',
'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
@@ -554,8 +583,8 @@ class DeviceInterfaceTable(InterfaceTable):
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'enabled', 'type', 'parent', 'bridge', 'lag',
'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_frequency',
'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable', 'cable_color', 'wireless_link',
'wireless_lans', 'link_peer', 'connection', 'tags', 'ip_addresses', 'fhrp_groups', 'untagged_vlan',
'tagged_vlans', 'actions',
'wireless_lans', 'link_peer', 'connection', 'tags', 'vrf', 'l2vpn', 'ip_addresses', 'fhrp_groups',
'untagged_vlan', 'tagged_vlans', 'actions',
)
order_by = ('name',)
default_columns = (

View File

@@ -1,10 +1,22 @@
import django_tables2 as tables
from dcim.models import (
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, DeviceType, FrontPortTemplate, InterfaceTemplate,
InventoryItemTemplate, Manufacturer, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
ConsolePortTemplate,
ConsoleServerPortTemplate,
DeviceBayTemplate,
DeviceType,
FrontPortTemplate,
InterfaceTemplate,
InventoryItemTemplate,
Manufacturer,
ModuleBayTemplate,
PowerOutletTemplate,
PowerPortTemplate,
RearPortTemplate,
)
from tenancy.tables import ContactsColumnMixin
from netbox.tables import NetBoxTable, columns
from .template_code import MODULAR_COMPONENT_TEMPLATE_BUTTONS
__all__ = (
@@ -27,7 +39,7 @@ __all__ = (
# Manufacturers
#
class ManufacturerTable(NetBoxTable):
class ManufacturerTable(ContactsColumnMixin, NetBoxTable):
name = tables.Column(
linkify=True
)
@@ -43,9 +55,6 @@ class ManufacturerTable(NetBoxTable):
verbose_name='Platforms'
)
slug = tables.Column()
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
url_name='dcim:manufacturer_list'
)
@@ -85,6 +94,9 @@ class DeviceTypeTable(NetBoxTable):
tags = columns.TagColumn(
url_name='dcim:devicetype_list'
)
u_height = columns.TemplateColumn(
template_code='{{ value|floatformat }}'
)
class Meta(NetBoxTable.Meta):
model = DeviceType

View File

@@ -1,7 +1,9 @@
import django_tables2 as tables
from dcim.models import PowerFeed, PowerPanel
from tenancy.tables import ContactsColumnMixin
from netbox.tables import NetBoxTable, columns
from .devices import CableTerminationTable
__all__ = (
@@ -14,7 +16,7 @@ __all__ = (
# Power panels
#
class PowerPanelTable(NetBoxTable):
class PowerPanelTable(ContactsColumnMixin, NetBoxTable):
name = tables.Column(
linkify=True
)
@@ -29,9 +31,6 @@ class PowerPanelTable(NetBoxTable):
url_params={'power_panel_id': 'pk'},
verbose_name='Feeds'
)
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
url_name='dcim:powerpanel_list'
)

View File

@@ -1,9 +1,9 @@
import django_tables2 as tables
from django_tables2.utils import Accessor
from dcim.models import Rack, RackReservation, RackRole
from django_tables2.utils import Accessor
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenancyColumnsMixin
__all__ = (
'RackTable',
@@ -37,7 +37,7 @@ class RackRoleTable(NetBoxTable):
# Racks
#
class RackTable(TenancyColumnsMixin, NetBoxTable):
class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
name = tables.Column(
order_by=('_name',),
linkify=True
@@ -51,7 +51,7 @@ class RackTable(TenancyColumnsMixin, NetBoxTable):
status = columns.ChoiceFieldColumn()
role = columns.ColoredLabelColumn()
u_height = tables.TemplateColumn(
template_code="{{ record.u_height }}U",
template_code="{{ value }}U",
verbose_name='Height'
)
comments = columns.MarkdownColumn()
@@ -68,9 +68,6 @@ class RackTable(TenancyColumnsMixin, NetBoxTable):
orderable=False,
verbose_name='Power'
)
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
url_name='dcim:rack_list'
)

View File

@@ -1,8 +1,9 @@
import django_tables2 as tables
from dcim.models import Location, Region, Site, SiteGroup
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenancyColumnsMixin
from .template_code import LOCATION_BUTTONS
__all__ = (
@@ -17,7 +18,7 @@ __all__ = (
# Regions
#
class RegionTable(NetBoxTable):
class RegionTable(ContactsColumnMixin, NetBoxTable):
name = columns.MPTTColumn(
linkify=True
)
@@ -26,9 +27,6 @@ class RegionTable(NetBoxTable):
url_params={'region_id': 'pk'},
verbose_name='Sites'
)
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
url_name='dcim:region_list'
)
@@ -46,7 +44,7 @@ class RegionTable(NetBoxTable):
# Site groups
#
class SiteGroupTable(NetBoxTable):
class SiteGroupTable(ContactsColumnMixin, NetBoxTable):
name = columns.MPTTColumn(
linkify=True
)
@@ -55,9 +53,6 @@ class SiteGroupTable(NetBoxTable):
url_params={'group_id': 'pk'},
verbose_name='Sites'
)
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
url_name='dcim:sitegroup_list'
)
@@ -75,7 +70,7 @@ class SiteGroupTable(NetBoxTable):
# Sites
#
class SiteTable(TenancyColumnsMixin, NetBoxTable):
class SiteTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
name = tables.Column(
linkify=True
)
@@ -97,9 +92,6 @@ class SiteTable(TenancyColumnsMixin, NetBoxTable):
verbose_name='ASN Count'
)
comments = columns.MarkdownColumn()
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
url_name='dcim:site_list'
)
@@ -118,7 +110,7 @@ class SiteTable(TenancyColumnsMixin, NetBoxTable):
# Locations
#
class LocationTable(TenancyColumnsMixin, NetBoxTable):
class LocationTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
name = columns.MPTTColumn(
linkify=True
)
@@ -136,9 +128,6 @@ class LocationTable(TenancyColumnsMixin, NetBoxTable):
url_params={'location_id': 'pk'},
verbose_name='Devices'
)
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
url_name='dcim:location_list'
)

View File

@@ -4,7 +4,7 @@ LINKTERMINATION = """
<a href="{{ termination.parent_object.get_absolute_url }}">{{ termination.parent_object }}</a>
<i class="mdi mdi-chevron-right"></i>
{% endif %}
<a href="{{ termination.get_absolute_url }}">{{ termination }}</a>{% if not forloop.last %},{% endif %}
<a href="{{ termination.get_absolute_url }}">{{ termination }}</a>{% if not forloop.last %}<br />{% endif %}
{% empty %}
{{ ''|placeholder }}
{% endfor %}
@@ -33,7 +33,7 @@ DEVICEBAY_STATUS = """
INTERFACE_IPADDRESSES = """
<div class="table-badge-group">
{% for ip in record.ip_addresses.all %}
{% for ip in value.all %}
{% if ip.status != 'active' %}
<a href="{{ ip.get_absolute_url }}" class="table-badge badge bg-{{ ip.get_status_color }}" data-bs-toggle="tooltip" data-bs-placement="left" title="{{ ip.get_status_display }}">{{ ip }}</a>
{% else %}
@@ -53,7 +53,7 @@ INTERFACE_FHRPGROUPS = """
INTERFACE_TAGGED_VLANS = """
{% if record.mode == 'tagged' %}
{% for vlan in record.tagged_vlans.all %}
{% for vlan in value.all %}
<a href="{{ vlan.get_absolute_url }}">{{ vlan }}</a><br />
{% endfor %}
{% elif record.mode == 'tagged-all' %}
@@ -62,7 +62,7 @@ INTERFACE_TAGGED_VLANS = """
"""
INTERFACE_WIRELESS_LANS = """
{% for wlan in record.wireless_lans.all %}
{% for wlan in value.all %}
<a href="{{ wlan.get_absolute_url }}">{{ wlan }}</a><br />
{% endfor %}
"""
@@ -226,7 +226,7 @@ POWEROUTLET_BUTTONS = """
"""
INTERFACE_BUTTONS = """
{% if perms.ipam.add_ipaddress or perms.dcim.add_inventoryitem %}
{% if perms.dcim.change_interface %}
<span class="dropdown">
<button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Add">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span>
@@ -238,6 +238,15 @@ INTERFACE_BUTTONS = """
{% if perms.dcim.add_inventoryitem %}
<li><a class="dropdown-item" href="{% url 'dcim:inventoryitem_add' %}?device={{ record.device_id }}&component_type={{ record|content_type_id }}&component_id={{ record.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Inventory Item</a></li>
{% endif %}
{% if perms.dcim.add_interface %}
<li><a class="dropdown-item" href="{% url 'dcim:interface_add' %}?device={{ record.device_id }}&parent={{ record.pk }}&name={{ record.name }}.&type=virtual&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Child Interface</a></li>
{% endif %}
{% if perms.ipam.add_l2vpntermination %}
<li><a class="dropdown-item" href="{% url 'ipam:l2vpntermination_add' %}?device={{ object.pk }}&interface={{ record.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">L2VPN Termination</a></li>
{% endif %}
{% if perms.ipam.add_fhrpgroupassignment %}
<li><a class="dropdown-item" href="{% url 'ipam:fhrpgroupassignment_add' %}?interface_type={{ record|content_type_id }}&interface_id={{ record.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Assign FHRP Group</a></li>
{% endif %}
</ul>
</span>
{% endif %}

View File

@@ -461,16 +461,19 @@ class DeviceTypeTest(APIViewTestCases.APIViewTestCase):
'manufacturer': manufacturers[1].pk,
'model': 'Device Type 4',
'slug': 'device-type-4',
'u_height': 0,
},
{
'manufacturer': manufacturers[1].pk,
'model': 'Device Type 5',
'slug': 'device-type-5',
'u_height': 0.5,
},
{
'manufacturer': manufacturers[1].pk,
'model': 'Device Type 6',
'slug': 'device-type-6',
'u_height': 1,
},
]
@@ -2054,6 +2057,7 @@ class VirtualChassisTest(APIViewTestCases.APIViewTestCase):
cls.bulk_update_data = {
'domain': 'newdomain',
'master': None
}

View File

@@ -688,7 +688,7 @@ class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
Manufacturer.objects.bulk_create(manufacturers)
device_types = (
DeviceType(manufacturer=manufacturers[0], model='Model 1', slug='model-1', part_number='Part Number 1', u_height=1, is_full_depth=True),
DeviceType(manufacturer=manufacturers[0], model='Model 1', slug='model-1', part_number='Part Number 1', u_height=1, is_full_depth=True, front_image='front.png', rear_image='rear.png'),
DeviceType(manufacturer=manufacturers[1], model='Model 2', slug='model-2', part_number='Part Number 2', u_height=2, is_full_depth=True, subdevice_role=SubdeviceRoleChoices.ROLE_PARENT, airflow=DeviceAirflowChoices.AIRFLOW_FRONT_TO_REAR),
DeviceType(manufacturer=manufacturers[2], model='Model 3', slug='model-3', part_number='Part Number 3', u_height=3, is_full_depth=False, subdevice_role=SubdeviceRoleChoices.ROLE_CHILD, airflow=DeviceAirflowChoices.AIRFLOW_REAR_TO_FRONT),
)
@@ -753,9 +753,9 @@ class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_is_full_depth(self):
params = {'is_full_depth': 'true'}
params = {'is_full_depth': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'is_full_depth': 'false'}
params = {'is_full_depth': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_subdevice_role(self):
@@ -773,6 +773,18 @@ class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'manufacturer': [manufacturers[0].slug, manufacturers[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_has_front_image(self):
params = {'has_front_image': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
params = {'has_front_image': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_has_rear_image(self):
params = {'has_rear_image': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
params = {'has_rear_image': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_console_ports(self):
params = {'console_ports': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -1631,6 +1643,8 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
device_types = DeviceType.objects.all()[:2]
params = {'device_type_id': [device_types[0].pk, device_types[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'device_type': [device_types[0].slug, device_types[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_devicerole(self):
device_roles = DeviceRole.objects.all()[:2]
@@ -1924,10 +1938,17 @@ class ConsolePortTestCase(TestCase, ChangeLoggedFilterSetTests):
for location in locations:
location.save()
racks = (
Rack(name='Rack 1', site=sites[0]),
Rack(name='Rack 2', site=sites[1]),
Rack(name='Rack 3', site=sites[2]),
)
Rack.objects.bulk_create(racks)
devices = (
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
)
Device.objects.bulk_create(devices)
@@ -1976,12 +1997,6 @@ class ConsolePortTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'description': ['First', 'Second']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_connected(self):
params = {'connected': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'connected': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_region(self):
regions = Region.objects.all()[:2]
params = {'region_id': [regions[0].pk, regions[1].pk]}
@@ -2003,6 +2018,20 @@ class ConsolePortTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'site': [sites[0].slug, sites[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_location(self):
locations = Location.objects.all()[:2]
params = {'location_id': [locations[0].pk, locations[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'location': [locations[0].slug, locations[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_rack(self):
racks = Rack.objects.all()[:2]
params = {'rack_id': [racks[0].pk, racks[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'rack': [racks[0].name, racks[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_device(self):
devices = Device.objects.all()[:2]
params = {'device_id': [devices[0].pk, devices[1].pk]}
@@ -2015,17 +2044,22 @@ class ConsolePortTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'module_id': [modules[0].pk, modules[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_location(self):
locations = Location.objects.all()[:2]
params = {'location_id': [locations[0].pk, locations[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'location': [locations[0].slug, locations[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_cabled(self):
params = {'cabled': 'true'}
params = {'cabled': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'cabled': 'false'}
params = {'cabled': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_occupied(self):
params = {'occupied': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'occupied': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_connected(self):
params = {'connected': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'connected': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
@@ -2071,10 +2105,17 @@ class ConsoleServerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
for location in locations:
location.save()
racks = (
Rack(name='Rack 1', site=sites[0]),
Rack(name='Rack 2', site=sites[1]),
Rack(name='Rack 3', site=sites[2]),
)
Rack.objects.bulk_create(racks)
devices = (
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
)
Device.objects.bulk_create(devices)
@@ -2123,12 +2164,6 @@ class ConsoleServerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'description': ['First', 'Second']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_connected(self):
params = {'connected': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'connected': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_region(self):
regions = Region.objects.all()[:2]
params = {'region_id': [regions[0].pk, regions[1].pk]}
@@ -2157,6 +2192,13 @@ class ConsoleServerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'location': [locations[0].slug, locations[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_rack(self):
racks = Rack.objects.all()[:2]
params = {'rack_id': [racks[0].pk, racks[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'rack': [racks[0].name, racks[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_device(self):
devices = Device.objects.all()[:2]
params = {'device_id': [devices[0].pk, devices[1].pk]}
@@ -2170,9 +2212,21 @@ class ConsoleServerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_cabled(self):
params = {'cabled': 'true'}
params = {'cabled': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'cabled': 'false'}
params = {'cabled': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_occupied(self):
params = {'occupied': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'occupied': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_connected(self):
params = {'connected': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'connected': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
@@ -2218,10 +2272,17 @@ class PowerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
for location in locations:
location.save()
racks = (
Rack(name='Rack 1', site=sites[0]),
Rack(name='Rack 2', site=sites[1]),
Rack(name='Rack 3', site=sites[2]),
)
Rack.objects.bulk_create(racks)
devices = (
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
)
Device.objects.bulk_create(devices)
@@ -2278,12 +2339,6 @@ class PowerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'allocated_draw': [50, 100]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_connected(self):
params = {'connected': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'connected': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_region(self):
regions = Region.objects.all()[:2]
params = {'region_id': [regions[0].pk, regions[1].pk]}
@@ -2312,6 +2367,13 @@ class PowerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'location': [locations[0].slug, locations[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_rack(self):
racks = Rack.objects.all()[:2]
params = {'rack_id': [racks[0].pk, racks[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'rack': [racks[0].name, racks[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_device(self):
devices = Device.objects.all()[:2]
params = {'device_id': [devices[0].pk, devices[1].pk]}
@@ -2325,9 +2387,21 @@ class PowerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_cabled(self):
params = {'cabled': 'true'}
params = {'cabled': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'cabled': 'false'}
params = {'cabled': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_occupied(self):
params = {'occupied': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'occupied': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_connected(self):
params = {'connected': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'connected': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
@@ -2373,10 +2447,17 @@ class PowerOutletTestCase(TestCase, ChangeLoggedFilterSetTests):
for location in locations:
location.save()
racks = (
Rack(name='Rack 1', site=sites[0]),
Rack(name='Rack 2', site=sites[1]),
Rack(name='Rack 3', site=sites[2]),
)
Rack.objects.bulk_create(racks)
devices = (
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
)
Device.objects.bulk_create(devices)
@@ -2429,12 +2510,6 @@ class PowerOutletTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'feed_leg': [PowerOutletFeedLegChoices.FEED_LEG_A, PowerOutletFeedLegChoices.FEED_LEG_B]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_connected(self):
params = {'connected': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'connected': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_region(self):
regions = Region.objects.all()[:2]
params = {'region_id': [regions[0].pk, regions[1].pk]}
@@ -2463,6 +2538,13 @@ class PowerOutletTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'location': [locations[0].slug, locations[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_rack(self):
racks = Rack.objects.all()[:2]
params = {'rack_id': [racks[0].pk, racks[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'rack': [racks[0].name, racks[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_device(self):
devices = Device.objects.all()[:2]
params = {'device_id': [devices[0].pk, devices[1].pk]}
@@ -2476,9 +2558,21 @@ class PowerOutletTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_cabled(self):
params = {'cabled': 'true'}
params = {'cabled': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'cabled': 'false'}
params = {'cabled': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_occupied(self):
params = {'occupied': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'occupied': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_connected(self):
params = {'connected': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'connected': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
@@ -2524,10 +2618,17 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
for location in locations:
location.save()
racks = (
Rack(name='Rack 1', site=sites[0]),
Rack(name='Rack 2', site=sites[1]),
Rack(name='Rack 3', site=sites[2]),
)
Rack.objects.bulk_create(racks)
devices = (
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
)
Device.objects.bulk_create(devices)
@@ -2678,12 +2779,6 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'label': ['A', 'B']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_connected(self):
params = {'connected': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'connected': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_enabled(self):
params = {'enabled': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
@@ -2793,6 +2888,13 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'location': [locations[0].slug, locations[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_rack(self):
racks = Rack.objects.all()[:2]
params = {'rack_id': [racks[0].pk, racks[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'rack': [racks[0].name, racks[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_virtual_chassis_id(self):
params = {'virtual_chassis_id': [VirtualChassis.objects.first().pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -2810,9 +2912,21 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_cabled(self):
params = {'cabled': 'true'}
params = {'cabled': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'cabled': 'false'}
params = {'cabled': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_occupied(self):
params = {'occupied': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'occupied': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_connected(self):
params = {'connected': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'connected': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_kind(self):
@@ -2899,10 +3013,17 @@ class FrontPortTestCase(TestCase, ChangeLoggedFilterSetTests):
for location in locations:
location.save()
racks = (
Rack(name='Rack 1', site=sites[0]),
Rack(name='Rack 2', site=sites[1]),
Rack(name='Rack 3', site=sites[2]),
)
Rack.objects.bulk_create(racks)
devices = (
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
)
Device.objects.bulk_create(devices)
@@ -2994,6 +3115,13 @@ class FrontPortTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'location': [locations[0].slug, locations[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_rack(self):
racks = Rack.objects.all()[:2]
params = {'rack_id': [racks[0].pk, racks[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'rack': [racks[0].name, racks[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_device(self):
devices = Device.objects.all()[:2]
params = {'device_id': [devices[0].pk, devices[1].pk]}
@@ -3007,9 +3135,15 @@ class FrontPortTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_cabled(self):
params = {'cabled': 'true'}
params = {'cabled': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'cabled': 'false'}
params = {'cabled': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_occupied(self):
params = {'occupied': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'occupied': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -3055,10 +3189,17 @@ class RearPortTestCase(TestCase, ChangeLoggedFilterSetTests):
for location in locations:
location.save()
racks = (
Rack(name='Rack 1', site=sites[0]),
Rack(name='Rack 2', site=sites[1]),
Rack(name='Rack 3', site=sites[2]),
)
Rack.objects.bulk_create(racks)
devices = (
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
)
Device.objects.bulk_create(devices)
@@ -3144,6 +3285,13 @@ class RearPortTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'location': [locations[0].slug, locations[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_rack(self):
racks = Rack.objects.all()[:2]
params = {'rack_id': [racks[0].pk, racks[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'rack': [racks[0].name, racks[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_device(self):
devices = Device.objects.all()[:2]
params = {'device_id': [devices[0].pk, devices[1].pk]}
@@ -3157,9 +3305,15 @@ class RearPortTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_cabled(self):
params = {'cabled': 'true'}
params = {'cabled': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'cabled': 'false'}
params = {'cabled': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_occupied(self):
params = {'occupied': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'occupied': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -3204,10 +3358,17 @@ class ModuleBayTestCase(TestCase, ChangeLoggedFilterSetTests):
for location in locations:
location.save()
racks = (
Rack(name='Rack 1', site=sites[0]),
Rack(name='Rack 2', site=sites[1]),
Rack(name='Rack 3', site=sites[2]),
)
Rack.objects.bulk_create(racks)
devices = (
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
)
Device.objects.bulk_create(devices)
@@ -3258,6 +3419,13 @@ class ModuleBayTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'location': [locations[0].slug, locations[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_rack(self):
racks = Rack.objects.all()[:2]
params = {'rack_id': [racks[0].pk, racks[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'rack': [racks[0].name, racks[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_device(self):
devices = Device.objects.all()[:2]
params = {'device_id': [devices[0].pk, devices[1].pk]}
@@ -3307,10 +3475,17 @@ class DeviceBayTestCase(TestCase, ChangeLoggedFilterSetTests):
for location in locations:
location.save()
racks = (
Rack(name='Rack 1', site=sites[0]),
Rack(name='Rack 2', site=sites[1]),
Rack(name='Rack 3', site=sites[2]),
)
Rack.objects.bulk_create(racks)
devices = (
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
)
Device.objects.bulk_create(devices)
@@ -3361,6 +3536,13 @@ class DeviceBayTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'location': [locations[0].slug, locations[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_rack(self):
racks = Rack.objects.all()[:2]
params = {'rack_id': [racks[0].pk, racks[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'rack': [racks[0].name, racks[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_device(self):
devices = Device.objects.all()[:2]
params = {'device_id': [devices[0].pk, devices[1].pk]}
@@ -3416,10 +3598,17 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
for location in locations:
location.save()
racks = (
Rack(name='Rack 1', site=sites[0]),
Rack(name='Rack 2', site=sites[1]),
Rack(name='Rack 3', site=sites[2]),
)
Rack.objects.bulk_create(racks)
devices = (
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
)
Device.objects.bulk_create(devices)
@@ -3503,6 +3692,13 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'location': [locations[0].slug, locations[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_rack(self):
racks = Rack.objects.all()[:2]
params = {'rack_id': [racks[0].pk, racks[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'rack': [racks[0].name, racks[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_device(self):
devices = Device.objects.all()[:2]
params = {'device_id': [devices[0].pk, devices[1].pk]}
@@ -4019,9 +4215,9 @@ class PowerFeedTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_cabled(self):
params = {'cabled': 'true'}
params = {'cabled': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'cabled': 'false'}
params = {'cabled': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_connected(self):

View File

@@ -1,6 +1,6 @@
from django.test import TestCase
from dcim.choices import DeviceFaceChoices, DeviceStatusChoices
from dcim.choices import DeviceFaceChoices, DeviceStatusChoices, InterfaceTypeChoices
from dcim.forms import *
from dcim.models import *
from utilities.testing import create_test_device
@@ -129,10 +129,11 @@ class LabelTestCase(TestCase):
"""
interface_data = {
'device': self.device.pk,
'name_pattern': 'eth[0-9]',
'label_pattern': 'Interface[0-9]',
'name': 'eth[0-9]',
'label': 'Interface[0-9]',
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
}
form = DeviceComponentCreateForm(interface_data)
form = InterfaceCreateForm(interface_data)
self.assertTrue(form.is_valid())
@@ -142,10 +143,11 @@ class LabelTestCase(TestCase):
"""
bad_interface_data = {
'device': self.device.pk,
'name_pattern': 'eth[0-9]',
'label_pattern': 'Interface[0-1]',
'name': 'eth[0-9]',
'label': 'Interface[0-1]',
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
}
form = DeviceComponentCreateForm(bad_interface_data)
form = InterfaceCreateForm(bad_interface_data)
self.assertFalse(form.is_valid())
self.assertIn('label_pattern', form.errors)
self.assertIn('label', form.errors)

View File

@@ -1082,31 +1082,28 @@ front-ports:
class ConsolePortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
model = ConsolePortTemplate
validation_excluded_fields = ('name', 'label')
@classmethod
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
devicetypes = (
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
)
DeviceType.objects.bulk_create(devicetypes)
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
ConsolePortTemplate.objects.bulk_create((
ConsolePortTemplate(device_type=devicetypes[0], name='Console Port Template 1'),
ConsolePortTemplate(device_type=devicetypes[0], name='Console Port Template 2'),
ConsolePortTemplate(device_type=devicetypes[0], name='Console Port Template 3'),
ConsolePortTemplate(device_type=devicetype, name='Console Port Template 1'),
ConsolePortTemplate(device_type=devicetype, name='Console Port Template 2'),
ConsolePortTemplate(device_type=devicetype, name='Console Port Template 3'),
))
cls.form_data = {
'device_type': devicetypes[1].pk,
'device_type': devicetype.pk,
'name': 'Console Port Template X',
'type': ConsolePortTypeChoices.TYPE_RJ45,
}
cls.bulk_create_data = {
'device_type': devicetypes[1].pk,
'name_pattern': 'Console Port Template [4-6]',
'device_type': devicetype.pk,
'name': 'Console Port Template [4-6]',
'type': ConsolePortTypeChoices.TYPE_RJ45,
}
@@ -1117,31 +1114,28 @@ class ConsolePortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestC
class ConsoleServerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
model = ConsoleServerPortTemplate
validation_excluded_fields = ('name', 'label')
@classmethod
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
devicetypes = (
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
)
DeviceType.objects.bulk_create(devicetypes)
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
ConsoleServerPortTemplate.objects.bulk_create((
ConsoleServerPortTemplate(device_type=devicetypes[0], name='Console Server Port Template 1'),
ConsoleServerPortTemplate(device_type=devicetypes[0], name='Console Server Port Template 2'),
ConsoleServerPortTemplate(device_type=devicetypes[0], name='Console Server Port Template 3'),
ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port Template 1'),
ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port Template 2'),
ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port Template 3'),
))
cls.form_data = {
'device_type': devicetypes[1].pk,
'device_type': devicetype.pk,
'name': 'Console Server Port Template X',
'type': ConsolePortTypeChoices.TYPE_RJ45,
}
cls.bulk_create_data = {
'device_type': devicetypes[1].pk,
'name_pattern': 'Console Server Port Template [4-6]',
'device_type': devicetype.pk,
'name': 'Console Server Port Template [4-6]',
'type': ConsolePortTypeChoices.TYPE_RJ45,
}
@@ -1152,24 +1146,21 @@ class ConsoleServerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateVie
class PowerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
model = PowerPortTemplate
validation_excluded_fields = ('name', 'label')
@classmethod
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
devicetypes = (
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
)
DeviceType.objects.bulk_create(devicetypes)
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
PowerPortTemplate.objects.bulk_create((
PowerPortTemplate(device_type=devicetypes[0], name='Power Port Template 1'),
PowerPortTemplate(device_type=devicetypes[0], name='Power Port Template 2'),
PowerPortTemplate(device_type=devicetypes[0], name='Power Port Template 3'),
PowerPortTemplate(device_type=devicetype, name='Power Port Template 1'),
PowerPortTemplate(device_type=devicetype, name='Power Port Template 2'),
PowerPortTemplate(device_type=devicetype, name='Power Port Template 3'),
))
cls.form_data = {
'device_type': devicetypes[1].pk,
'device_type': devicetype.pk,
'name': 'Power Port Template X',
'type': PowerPortTypeChoices.TYPE_IEC_C14,
'maximum_draw': 100,
@@ -1177,8 +1168,8 @@ class PowerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
}
cls.bulk_create_data = {
'device_type': devicetypes[1].pk,
'name_pattern': 'Power Port Template [4-6]',
'device_type': devicetype.pk,
'name': 'Power Port Template [4-6]',
'type': PowerPortTypeChoices.TYPE_IEC_C14,
'maximum_draw': 100,
'allocated_draw': 50,
@@ -1193,6 +1184,7 @@ class PowerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
class PowerOutletTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
model = PowerOutletTemplate
validation_excluded_fields = ('name', 'label')
@classmethod
def setUpTestData(cls):
@@ -1220,7 +1212,7 @@ class PowerOutletTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestC
cls.bulk_create_data = {
'device_type': devicetype.pk,
'name_pattern': 'Power Outlet Template [4-6]',
'name': 'Power Outlet Template [4-6]',
'type': PowerOutletTypeChoices.TYPE_IEC_C13,
'power_port': powerports[0].pk,
'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B,
@@ -1234,34 +1226,31 @@ class PowerOutletTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestC
class InterfaceTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
model = InterfaceTemplate
validation_excluded_fields = ('name', 'label')
@classmethod
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
devicetypes = (
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
)
DeviceType.objects.bulk_create(devicetypes)
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
InterfaceTemplate.objects.bulk_create((
InterfaceTemplate(device_type=devicetypes[0], name='Interface Template 1'),
InterfaceTemplate(device_type=devicetypes[0], name='Interface Template 2'),
InterfaceTemplate(device_type=devicetypes[0], name='Interface Template 3'),
InterfaceTemplate(device_type=devicetype, name='Interface Template 1'),
InterfaceTemplate(device_type=devicetype, name='Interface Template 2'),
InterfaceTemplate(device_type=devicetype, name='Interface Template 3'),
))
cls.form_data = {
'device_type': devicetypes[1].pk,
'device_type': devicetype.pk,
'name': 'Interface Template X',
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
'mgmt_only': True,
}
cls.bulk_create_data = {
'device_type': devicetypes[1].pk,
'name_pattern': 'Interface Template [4-6]',
'device_type': devicetype.pk,
'name': 'Interface Template [4-6]',
# Test that a label can be applied to each generated interface templates
'label_pattern': 'Interface Template Label [3-5]',
'label': 'Interface Template Label [3-5]',
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
'mgmt_only': True,
}
@@ -1274,6 +1263,7 @@ class InterfaceTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
model = FrontPortTemplate
validation_excluded_fields = ('name', 'label', 'rear_port')
@classmethod
def setUpTestData(cls):
@@ -1306,11 +1296,9 @@ class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
cls.bulk_create_data = {
'device_type': devicetype.pk,
'name_pattern': 'Front Port [4-6]',
'name': 'Front Port [4-6]',
'type': PortTypeChoices.TYPE_8P8C,
'rear_port_set': [
'{}:1'.format(rp.pk) for rp in rearports[3:6]
],
'rear_port': [f'{rp.pk}:1' for rp in rearports[3:6]],
}
cls.bulk_edit_data = {
@@ -1320,32 +1308,29 @@ class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
class RearPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
model = RearPortTemplate
validation_excluded_fields = ('name', 'label')
@classmethod
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
devicetypes = (
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
)
DeviceType.objects.bulk_create(devicetypes)
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
RearPortTemplate.objects.bulk_create((
RearPortTemplate(device_type=devicetypes[0], name='Rear Port Template 1'),
RearPortTemplate(device_type=devicetypes[0], name='Rear Port Template 2'),
RearPortTemplate(device_type=devicetypes[0], name='Rear Port Template 3'),
RearPortTemplate(device_type=devicetype, name='Rear Port Template 1'),
RearPortTemplate(device_type=devicetype, name='Rear Port Template 2'),
RearPortTemplate(device_type=devicetype, name='Rear Port Template 3'),
))
cls.form_data = {
'device_type': devicetypes[1].pk,
'device_type': devicetype.pk,
'name': 'Rear Port Template X',
'type': PortTypeChoices.TYPE_8P8C,
'positions': 2,
}
cls.bulk_create_data = {
'device_type': devicetypes[1].pk,
'name_pattern': 'Rear Port Template [4-6]',
'device_type': devicetype.pk,
'name': 'Rear Port Template [4-6]',
'type': PortTypeChoices.TYPE_8P8C,
'positions': 2,
}
@@ -1357,30 +1342,27 @@ class RearPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase
class ModuleBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
model = ModuleBayTemplate
validation_excluded_fields = ('name', 'label')
@classmethod
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
devicetypes = (
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
)
DeviceType.objects.bulk_create(devicetypes)
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
ModuleBayTemplate.objects.bulk_create((
ModuleBayTemplate(device_type=devicetypes[0], name='Module Bay Template 1'),
ModuleBayTemplate(device_type=devicetypes[0], name='Module Bay Template 2'),
ModuleBayTemplate(device_type=devicetypes[0], name='Module Bay Template 3'),
ModuleBayTemplate(device_type=devicetype, name='Module Bay Template 1'),
ModuleBayTemplate(device_type=devicetype, name='Module Bay Template 2'),
ModuleBayTemplate(device_type=devicetype, name='Module Bay Template 3'),
))
cls.form_data = {
'device_type': devicetypes[1].pk,
'device_type': devicetype.pk,
'name': 'Module Bay Template X',
}
cls.bulk_create_data = {
'device_type': devicetypes[1].pk,
'name_pattern': 'Module Bay Template [4-6]',
'device_type': devicetype.pk,
'name': 'Module Bay Template [4-6]',
}
cls.bulk_edit_data = {
@@ -1390,30 +1372,27 @@ class ModuleBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
class DeviceBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
model = DeviceBayTemplate
validation_excluded_fields = ('name', 'label')
@classmethod
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
devicetypes = (
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1', subdevice_role=SubdeviceRoleChoices.ROLE_PARENT),
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2', subdevice_role=SubdeviceRoleChoices.ROLE_PARENT),
)
DeviceType.objects.bulk_create(devicetypes)
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1', subdevice_role=SubdeviceRoleChoices.ROLE_PARENT)
DeviceBayTemplate.objects.bulk_create((
DeviceBayTemplate(device_type=devicetypes[0], name='Device Bay Template 1'),
DeviceBayTemplate(device_type=devicetypes[0], name='Device Bay Template 2'),
DeviceBayTemplate(device_type=devicetypes[0], name='Device Bay Template 3'),
DeviceBayTemplate(device_type=devicetype, name='Device Bay Template 1'),
DeviceBayTemplate(device_type=devicetype, name='Device Bay Template 2'),
DeviceBayTemplate(device_type=devicetype, name='Device Bay Template 3'),
))
cls.form_data = {
'device_type': devicetypes[1].pk,
'device_type': devicetype.pk,
'name': 'Device Bay Template X',
}
cls.bulk_create_data = {
'device_type': devicetypes[1].pk,
'name_pattern': 'Device Bay Template [4-6]',
'device_type': devicetype.pk,
'name': 'Device Bay Template [4-6]',
}
cls.bulk_edit_data = {
@@ -1423,6 +1402,7 @@ class DeviceBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
class InventoryItemTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
model = InventoryItemTemplate
validation_excluded_fields = ('name', 'label')
@classmethod
def setUpTestData(cls):
@@ -1431,30 +1411,25 @@ class InventoryItemTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTes
Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
)
Manufacturer.objects.bulk_create(manufacturers)
devicetypes = (
DeviceType(manufacturer=manufacturers[0], model='Device Type 1', slug='device-type-1'),
DeviceType(manufacturer=manufacturers[0], model='Device Type 2', slug='device-type-2'),
)
DeviceType.objects.bulk_create(devicetypes)
devicetype = DeviceType.objects.create(manufacturer=manufacturers[0], model='Device Type 1', slug='device-type-1')
inventory_item_templates = (
InventoryItemTemplate(device_type=devicetypes[0], name='Inventory Item Template 1', manufacturer=manufacturers[0]),
InventoryItemTemplate(device_type=devicetypes[0], name='Inventory Item Template 2', manufacturer=manufacturers[0]),
InventoryItemTemplate(device_type=devicetypes[0], name='Inventory Item Template 3', manufacturer=manufacturers[0]),
InventoryItemTemplate(device_type=devicetype, name='Inventory Item Template 1', manufacturer=manufacturers[0]),
InventoryItemTemplate(device_type=devicetype, name='Inventory Item Template 2', manufacturer=manufacturers[0]),
InventoryItemTemplate(device_type=devicetype, name='Inventory Item Template 3', manufacturer=manufacturers[0]),
)
for item in inventory_item_templates:
item.save()
cls.form_data = {
'device_type': devicetypes[1].pk,
'device_type': devicetype.pk,
'name': 'Inventory Item Template X',
'manufacturer': manufacturers[1].pk,
}
cls.bulk_create_data = {
'device_type': devicetypes[1].pk,
'name_pattern': 'Inventory Item Template [4-6]',
'device_type': devicetype.pk,
'name': 'Inventory Item Template [4-6]',
'manufacturer': manufacturers[1].pk,
}
@@ -1803,10 +1778,12 @@ class ModuleTestCase(
ModuleBay(device=devices[0], name='Module Bay 2'),
ModuleBay(device=devices[0], name='Module Bay 3'),
ModuleBay(device=devices[0], name='Module Bay 4'),
ModuleBay(device=devices[0], name='Module Bay 5'),
ModuleBay(device=devices[1], name='Module Bay 1'),
ModuleBay(device=devices[1], name='Module Bay 2'),
ModuleBay(device=devices[1], name='Module Bay 3'),
ModuleBay(device=devices[1], name='Module Bay 4'),
ModuleBay(device=devices[1], name='Module Bay 5'),
)
ModuleBay.objects.bulk_create(module_bays)
@@ -1820,7 +1797,7 @@ class ModuleTestCase(
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'device': devices[1].pk,
'device': devices[0].pk,
'module_bay': module_bays[3].pk,
'module_type': module_types[0].pk,
'serial': 'A',
@@ -1892,7 +1869,6 @@ class ModuleTestCase(
self.assertIsNone(interface.module)
# Create a module with adopted components
form_data['module_bay'] = ModuleBay.objects.filter(device=device).first()
form_data['module_type'] = module_type
form_data['replicate_components'] = False
form_data['adopt_components'] = True
@@ -1912,6 +1888,7 @@ class ModuleTestCase(
class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
model = ConsolePort
validation_excluded_fields = ('name', 'label')
@classmethod
def setUpTestData(cls):
@@ -1935,9 +1912,9 @@ class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
cls.bulk_create_data = {
'device': device.pk,
'name_pattern': 'Console Port [4-6]',
'name': 'Console Port [4-6]',
# Test that a label can be applied to each generated console ports
'label_pattern': 'Serial[3-5]',
'label': 'Serial[3-5]',
'type': ConsolePortTypeChoices.TYPE_RJ45,
'description': 'A console port',
'tags': sorted([t.pk for t in tags]),
@@ -1970,6 +1947,7 @@ class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
model = ConsoleServerPort
validation_excluded_fields = ('name', 'label')
@classmethod
def setUpTestData(cls):
@@ -1993,7 +1971,7 @@ class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
cls.bulk_create_data = {
'device': device.pk,
'name_pattern': 'Console Server Port [4-6]',
'name': 'Console Server Port [4-6]',
'type': ConsolePortTypeChoices.TYPE_RJ45,
'description': 'A console server port',
'tags': [t.pk for t in tags],
@@ -2026,6 +2004,7 @@ class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
model = PowerPort
validation_excluded_fields = ('name', 'label')
@classmethod
def setUpTestData(cls):
@@ -2051,7 +2030,7 @@ class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
cls.bulk_create_data = {
'device': device.pk,
'name_pattern': 'Power Port [4-6]]',
'name': 'Power Port [4-6]]',
'type': PowerPortTypeChoices.TYPE_IEC_C14,
'maximum_draw': 100,
'allocated_draw': 50,
@@ -2088,6 +2067,7 @@ class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
model = PowerOutlet
validation_excluded_fields = ('name', 'label')
@classmethod
def setUpTestData(cls):
@@ -2119,7 +2099,7 @@ class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
cls.bulk_create_data = {
'device': device.pk,
'name_pattern': 'Power Outlet [4-6]',
'name': 'Power Outlet [4-6]',
'type': PowerOutletTypeChoices.TYPE_IEC_C13,
'power_port': powerports[1].pk,
'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B,
@@ -2153,6 +2133,7 @@ class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
model = Interface
validation_excluded_fields = ('name', 'label')
@classmethod
def setUpTestData(cls):
@@ -2217,7 +2198,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
cls.bulk_create_data = {
'device': device.pk,
'name_pattern': 'Interface [4-6]',
'name': 'Interface [4-6]',
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
'enabled': False,
'bridge': interfaces[4].pk,
@@ -2277,6 +2258,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
model = FrontPort
validation_excluded_fields = ('name', 'label', 'rear_port')
@classmethod
def setUpTestData(cls):
@@ -2312,11 +2294,9 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
cls.bulk_create_data = {
'device': device.pk,
'name_pattern': 'Front Port [4-6]',
'name': 'Front Port [4-6]',
'type': PortTypeChoices.TYPE_8P8C,
'rear_port_set': [
'{}:1'.format(rp.pk) for rp in rearports[3:6]
],
'rear_port': [f'{rp.pk}:1' for rp in rearports[3:6]],
'description': 'New description',
'tags': [t.pk for t in tags],
}
@@ -2348,6 +2328,7 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
model = RearPort
validation_excluded_fields = ('name', 'label')
@classmethod
def setUpTestData(cls):
@@ -2372,7 +2353,7 @@ class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
cls.bulk_create_data = {
'device': device.pk,
'name_pattern': 'Rear Port [4-6]',
'name': 'Rear Port [4-6]',
'type': PortTypeChoices.TYPE_8P8C,
'positions': 3,
'description': 'A rear port',
@@ -2406,6 +2387,7 @@ class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
class ModuleBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
model = ModuleBay
validation_excluded_fields = ('name', 'label')
@classmethod
def setUpTestData(cls):
@@ -2428,7 +2410,7 @@ class ModuleBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
cls.bulk_create_data = {
'device': device.pk,
'name_pattern': 'Module Bay [4-6]',
'name': 'Module Bay [4-6]',
'description': 'A module bay',
'tags': [t.pk for t in tags],
}
@@ -2447,6 +2429,7 @@ class ModuleBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
model = DeviceBay
validation_excluded_fields = ('name', 'label')
@classmethod
def setUpTestData(cls):
@@ -2472,7 +2455,7 @@ class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
cls.bulk_create_data = {
'device': device.pk,
'name_pattern': 'Device Bay [4-6]',
'name': 'Device Bay [4-6]',
'description': 'A device bay',
'tags': [t.pk for t in tags],
}
@@ -2491,6 +2474,7 @@ class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
model = InventoryItem
validation_excluded_fields = ('name', 'label')
@classmethod
def setUpTestData(cls):
@@ -2525,7 +2509,7 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
cls.bulk_create_data = {
'device': device.pk,
'name_pattern': 'Inventory Item [4-6]',
'name': 'Inventory Item [4-6]',
'role': roles[1].pk,
'manufacturer': manufacturer.pk,
'parent': None,

View File

@@ -355,7 +355,7 @@ class SiteView(generic.ObjectView):
nonracked_devices = Device.objects.filter(
site=instance,
position__isnull=True,
rack__isnull=True,
parent_bay__isnull=True
).prefetch_related('device_type__manufacturer', 'parent_bay', 'device_role')
@@ -450,7 +450,7 @@ class LocationView(generic.ObjectView):
nonracked_devices = Device.objects.filter(
location=instance,
position__isnull=True,
rack__isnull=True,
parent_bay__isnull=True
).prefetch_related('device_type__manufacturer', 'parent_bay', 'device_role')
@@ -589,10 +589,17 @@ class RackElevationListView(generic.ObjectListView):
racks = filtersets.RackFilterSet(request.GET, self.queryset).qs
total_count = racks.count()
# Determine ordering
reverse = bool(request.GET.get('reverse', False))
if reverse:
racks = racks.reverse()
ORDERING_CHOICES = {
'name': 'Name (A-Z)',
'-name': 'Name (Z-A)',
'facility_id': 'Facility ID (A-Z)',
'-facility_id': 'Facility ID (Z-A)',
}
sort = request.GET.get('sort', "name")
if sort not in ORDERING_CHOICES:
sort = 'name'
racks = racks.order_by(sort)
# Pagination
per_page = get_paginate_count(request)
@@ -614,7 +621,9 @@ class RackElevationListView(generic.ObjectListView):
'paginator': paginator,
'page': page,
'total_count': total_count,
'reverse': reverse,
'sort': sort,
'sort_display_name': ORDERING_CHOICES[sort],
'sort_choices': ORDERING_CHOICES,
'rack_face': rack_face,
'filter_form': forms.RackElevationFilterForm(request.GET),
})
@@ -1111,9 +1120,8 @@ class ModuleTypeBulkDeleteView(generic.BulkDeleteView):
class ConsolePortTemplateCreateView(generic.ComponentCreateView):
queryset = ConsolePortTemplate.objects.all()
form = forms.ModularComponentTemplateCreateForm
form = forms.ConsolePortTemplateCreateForm
model_form = forms.ConsolePortTemplateForm
template_name = 'dcim/component_template_create.html'
class ConsolePortTemplateEditView(generic.ObjectEditView):
@@ -1146,9 +1154,8 @@ class ConsolePortTemplateBulkDeleteView(generic.BulkDeleteView):
class ConsoleServerPortTemplateCreateView(generic.ComponentCreateView):
queryset = ConsoleServerPortTemplate.objects.all()
form = forms.ModularComponentTemplateCreateForm
form = forms.ConsoleServerPortTemplateCreateForm
model_form = forms.ConsoleServerPortTemplateForm
template_name = 'dcim/component_template_create.html'
class ConsoleServerPortTemplateEditView(generic.ObjectEditView):
@@ -1181,9 +1188,8 @@ class ConsoleServerPortTemplateBulkDeleteView(generic.BulkDeleteView):
class PowerPortTemplateCreateView(generic.ComponentCreateView):
queryset = PowerPortTemplate.objects.all()
form = forms.ModularComponentTemplateCreateForm
form = forms.PowerPortTemplateCreateForm
model_form = forms.PowerPortTemplateForm
template_name = 'dcim/component_template_create.html'
class PowerPortTemplateEditView(generic.ObjectEditView):
@@ -1216,9 +1222,8 @@ class PowerPortTemplateBulkDeleteView(generic.BulkDeleteView):
class PowerOutletTemplateCreateView(generic.ComponentCreateView):
queryset = PowerOutletTemplate.objects.all()
form = forms.ModularComponentTemplateCreateForm
form = forms.PowerOutletTemplateCreateForm
model_form = forms.PowerOutletTemplateForm
template_name = 'dcim/component_template_create.html'
class PowerOutletTemplateEditView(generic.ObjectEditView):
@@ -1251,9 +1256,8 @@ class PowerOutletTemplateBulkDeleteView(generic.BulkDeleteView):
class InterfaceTemplateCreateView(generic.ComponentCreateView):
queryset = InterfaceTemplate.objects.all()
form = forms.ModularComponentTemplateCreateForm
form = forms.InterfaceTemplateCreateForm
model_form = forms.InterfaceTemplateForm
template_name = 'dcim/component_template_create.html'
class InterfaceTemplateEditView(generic.ObjectEditView):
@@ -1288,15 +1292,6 @@ class FrontPortTemplateCreateView(generic.ComponentCreateView):
queryset = FrontPortTemplate.objects.all()
form = forms.FrontPortTemplateCreateForm
model_form = forms.FrontPortTemplateForm
template_name = 'dcim/frontporttemplate_create.html'
def initialize_forms(self, request):
form, model_form = super().initialize_forms(request)
model_form.fields.pop('rear_port')
model_form.fields.pop('rear_port_position')
return form, model_form
class FrontPortTemplateEditView(generic.ObjectEditView):
@@ -1329,9 +1324,8 @@ class FrontPortTemplateBulkDeleteView(generic.BulkDeleteView):
class RearPortTemplateCreateView(generic.ComponentCreateView):
queryset = RearPortTemplate.objects.all()
form = forms.ModularComponentTemplateCreateForm
form = forms.RearPortTemplateCreateForm
model_form = forms.RearPortTemplateForm
template_name = 'dcim/component_template_create.html'
class RearPortTemplateEditView(generic.ObjectEditView):
@@ -1366,8 +1360,6 @@ class ModuleBayTemplateCreateView(generic.ComponentCreateView):
queryset = ModuleBayTemplate.objects.all()
form = forms.ModuleBayTemplateCreateForm
model_form = forms.ModuleBayTemplateForm
template_name = 'dcim/modulebaytemplate_create.html'
patterned_fields = ('name', 'label', 'position')
class ModuleBayTemplateEditView(generic.ObjectEditView):
@@ -1400,9 +1392,8 @@ class ModuleBayTemplateBulkDeleteView(generic.BulkDeleteView):
class DeviceBayTemplateCreateView(generic.ComponentCreateView):
queryset = DeviceBayTemplate.objects.all()
form = forms.ComponentTemplateCreateForm
form = forms.DeviceBayTemplateCreateForm
model_form = forms.DeviceBayTemplateForm
template_name = 'dcim/component_template_create.html'
class DeviceBayTemplateEditView(generic.ObjectEditView):
@@ -1435,9 +1426,8 @@ class DeviceBayTemplateBulkDeleteView(generic.BulkDeleteView):
class InventoryItemTemplateCreateView(generic.ComponentCreateView):
queryset = InventoryItemTemplate.objects.all()
form = forms.ModularComponentTemplateCreateForm
form = forms.InventoryItemTemplateCreateForm
model_form = forms.InventoryItemTemplateForm
template_name = 'dcim/inventoryitemtemplate_create.html'
def alter_object(self, instance, request):
# Set component (if any)
@@ -1626,6 +1616,7 @@ class DeviceView(generic.ObjectView):
return {
'services': services,
'vc_members': vc_members,
'svg_extra': f'highlight=id:{instance.pk}'
}
@@ -1865,14 +1856,13 @@ class ConsolePortView(generic.ObjectView):
class ConsolePortCreateView(generic.ComponentCreateView):
queryset = ConsolePort.objects.all()
form = forms.DeviceComponentCreateForm
form = forms.ConsolePortCreateForm
model_form = forms.ConsolePortForm
class ConsolePortEditView(generic.ObjectEditView):
queryset = ConsolePort.objects.all()
form = forms.ConsolePortForm
template_name = 'dcim/device_component_edit.html'
class ConsolePortDeleteView(generic.ObjectDeleteView):
@@ -1924,14 +1914,13 @@ class ConsoleServerPortView(generic.ObjectView):
class ConsoleServerPortCreateView(generic.ComponentCreateView):
queryset = ConsoleServerPort.objects.all()
form = forms.DeviceComponentCreateForm
form = forms.ConsoleServerPortCreateForm
model_form = forms.ConsoleServerPortForm
class ConsoleServerPortEditView(generic.ObjectEditView):
queryset = ConsoleServerPort.objects.all()
form = forms.ConsoleServerPortForm
template_name = 'dcim/device_component_edit.html'
class ConsoleServerPortDeleteView(generic.ObjectDeleteView):
@@ -1983,14 +1972,13 @@ class PowerPortView(generic.ObjectView):
class PowerPortCreateView(generic.ComponentCreateView):
queryset = PowerPort.objects.all()
form = forms.DeviceComponentCreateForm
form = forms.PowerPortCreateForm
model_form = forms.PowerPortForm
class PowerPortEditView(generic.ObjectEditView):
queryset = PowerPort.objects.all()
form = forms.PowerPortForm
template_name = 'dcim/device_component_edit.html'
class PowerPortDeleteView(generic.ObjectDeleteView):
@@ -2042,14 +2030,13 @@ class PowerOutletView(generic.ObjectView):
class PowerOutletCreateView(generic.ComponentCreateView):
queryset = PowerOutlet.objects.all()
form = forms.DeviceComponentCreateForm
form = forms.PowerOutletCreateForm
model_form = forms.PowerOutletForm
class PowerOutletEditView(generic.ObjectEditView):
queryset = PowerOutlet.objects.all()
form = forms.PowerOutletForm
template_name = 'dcim/device_component_edit.html'
class PowerOutletDeleteView(generic.ObjectDeleteView):
@@ -2145,42 +2132,13 @@ class InterfaceView(generic.ObjectView):
class InterfaceCreateView(generic.ComponentCreateView):
queryset = Interface.objects.all()
form = forms.DeviceComponentCreateForm
form = forms.InterfaceCreateForm
model_form = forms.InterfaceForm
# template_name = 'dcim/interface_create.html'
# TODO: Figure out what to do with this
# def post(self, request):
# """
# Override inherited post() method to handle request to assign newly created
# interface objects (first object) to an IP Address object.
# """
# form = self.form(request.POST, initial=request.GET)
# new_objs = self.validate_form(request, form)
#
# if form.is_valid() and not form.errors:
# if '_addanother' in request.POST:
# return redirect(request.get_full_path())
# elif new_objs is not None and '_assignip' in request.POST and len(new_objs) >= 1 and \
# request.user.has_perm('ipam.add_ipaddress'):
# first_obj = new_objs[0].pk
# return redirect(
# f'/ipam/ip-addresses/add/?interface={first_obj}&return_url={self.get_return_url(request)}'
# )
# else:
# return redirect(self.get_return_url(request))
#
# return render(request, self.template_name, {
# 'obj_type': self.queryset.model._meta.verbose_name,
# 'form': form,
# 'return_url': self.get_return_url(request),
# })
class InterfaceEditView(generic.ObjectEditView):
queryset = Interface.objects.all()
form = forms.InterfaceForm
template_name = 'dcim/interface_edit.html'
class InterfaceDeleteView(generic.ObjectDeleteView):
@@ -2235,19 +2193,10 @@ class FrontPortCreateView(generic.ComponentCreateView):
form = forms.FrontPortCreateForm
model_form = forms.FrontPortForm
def initialize_forms(self, request):
form, model_form = super().initialize_forms(request)
model_form.fields.pop('rear_port')
model_form.fields.pop('rear_port_position')
return form, model_form
class FrontPortEditView(generic.ObjectEditView):
queryset = FrontPort.objects.all()
form = forms.FrontPortForm
template_name = 'dcim/device_component_edit.html'
class FrontPortDeleteView(generic.ObjectDeleteView):
@@ -2299,14 +2248,13 @@ class RearPortView(generic.ObjectView):
class RearPortCreateView(generic.ComponentCreateView):
queryset = RearPort.objects.all()
form = forms.DeviceComponentCreateForm
form = forms.RearPortCreateForm
model_form = forms.RearPortForm
class RearPortEditView(generic.ObjectEditView):
queryset = RearPort.objects.all()
form = forms.RearPortForm
template_name = 'dcim/device_component_edit.html'
class RearPortDeleteView(generic.ObjectDeleteView):
@@ -2360,13 +2308,11 @@ class ModuleBayCreateView(generic.ComponentCreateView):
queryset = ModuleBay.objects.all()
form = forms.ModuleBayCreateForm
model_form = forms.ModuleBayForm
patterned_fields = ('name', 'label', 'position')
class ModuleBayEditView(generic.ObjectEditView):
queryset = ModuleBay.objects.all()
form = forms.ModuleBayForm
template_name = 'dcim/device_component_edit.html'
class ModuleBayDeleteView(generic.ObjectDeleteView):
@@ -2414,14 +2360,13 @@ class DeviceBayView(generic.ObjectView):
class DeviceBayCreateView(generic.ComponentCreateView):
queryset = DeviceBay.objects.all()
form = forms.DeviceComponentCreateForm
form = forms.DeviceBayCreateForm
model_form = forms.DeviceBayForm
class DeviceBayEditView(generic.ObjectEditView):
queryset = DeviceBay.objects.all()
form = forms.DeviceBayForm
template_name = 'dcim/device_component_edit.html'
class DeviceBayDeleteView(generic.ObjectDeleteView):
@@ -2543,7 +2488,6 @@ class InventoryItemCreateView(generic.ComponentCreateView):
queryset = InventoryItem.objects.all()
form = forms.InventoryItemCreateForm
model_form = forms.InventoryItemForm
template_name = 'dcim/inventoryitem_create.html'
def alter_object(self, instance, request):
# Set component (if any)
@@ -2727,7 +2671,6 @@ class DeviceBulkAddModuleBayView(generic.BulkComponentCreateView):
filterset = filtersets.DeviceFilterSet
table = tables.DeviceTable
default_return_url = 'dcim:device_list'
patterned_fields = ('name', 'label', 'position')
class DeviceBulkAddDeviceBayView(generic.BulkComponentCreateView):
@@ -2893,7 +2836,7 @@ class CableBulkDeleteView(generic.BulkDeleteView):
#
class ConsoleConnectionsListView(generic.ObjectListView):
queryset = ConsolePort.objects.filter(_path__isnull=False).order_by('device')
queryset = ConsolePort.objects.filter(_path__is_complete=True)
filterset = filtersets.ConsoleConnectionFilterSet
filterset_form = forms.ConsoleConnectionFilterForm
table = tables.ConsoleConnectionTable
@@ -2907,7 +2850,7 @@ class ConsoleConnectionsListView(generic.ObjectListView):
class PowerConnectionsListView(generic.ObjectListView):
queryset = PowerPort.objects.filter(_path__isnull=False).order_by('device')
queryset = PowerPort.objects.filter(_path__is_complete=True)
filterset = filtersets.PowerConnectionFilterSet
filterset_form = forms.PowerConnectionFilterForm
table = tables.PowerConnectionTable
@@ -2921,7 +2864,7 @@ class PowerConnectionsListView(generic.ObjectListView):
class InterfaceConnectionsListView(generic.ObjectListView):
queryset = Interface.objects.filter(_path__isnull=False).order_by('device')
queryset = Interface.objects.filter(_path__is_complete=True)
filterset = filtersets.InterfaceConnectionFilterSet
filterset_form = forms.InterfaceConnectionFilterForm
table = tables.InterfaceConnectionTable

View File

@@ -1,5 +1,6 @@
from django.contrib.contenttypes.models import ContentType
from rest_framework.fields import Field
from rest_framework.serializers import ValidationError
from extras.choices import CustomFieldTypeChoices
from extras.models import CustomField
@@ -62,6 +63,12 @@ class CustomFieldsDataField(Field):
return data
def to_internal_value(self, data):
if type(data) is not dict:
raise ValidationError(
"Invalid data format. Custom field data must be passed as a dictionary mapping field names to their "
"values."
)
# If updating an existing instance, start with existing custom_field_data
if self.parent.instance:
data = {**self.parent.instance.custom_field_data, **data}

View File

@@ -192,7 +192,7 @@ class ImageAttachmentSerializer(ValidatedModelSerializer):
return data
@swagger_serializer_method(serializer_or_field=serializers.DictField)
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
def get_parent(self, obj):
serializer = get_serializer_for_model(obj.parent, prefix=NESTED_SERIALIZER_PREFIX)
return serializer(obj.parent, context={'request': self.context['request']}).data
@@ -242,7 +242,7 @@ class JournalEntrySerializer(NetBoxModelSerializer):
return data
@swagger_serializer_method(serializer_or_field=serializers.DictField)
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
def get_assigned_object(self, instance):
serializer = get_serializer_for_model(instance.assigned_object_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX)
context = {'request': self.context['request']}
@@ -403,6 +403,7 @@ class ScriptSerializer(serializers.Serializer):
vars = serializers.SerializerMethodField(read_only=True)
result = NestedJobResultSerializer()
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
def get_vars(self, instance):
return {
k: v.__class__.__name__ for k, v in instance._get_vars().items()
@@ -461,7 +462,7 @@ class ObjectChangeSerializer(BaseModelSerializer):
'changed_object_id', 'changed_object', 'prechange_data', 'postchange_data',
]
@swagger_serializer_method(serializer_or_field=serializers.DictField)
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
def get_changed_object(self, obj):
"""
Serialize a nested representation of the changed object.

View File

@@ -159,7 +159,7 @@ class ReportViewSet(ViewSet):
# Read the PK as "<module>.<report>"
if '.' not in pk:
raise Http404
module_name, report_name = pk.split('.', 1)
module_name, report_name = pk.split('.', maxsplit=1)
# Raise a 404 on an invalid Report module/name
report = get_report(module_name, report_name)
@@ -183,8 +183,8 @@ class ReportViewSet(ViewSet):
}
# Iterate through all available Reports.
for module_name, reports in get_reports():
for report in reports:
for module_name, reports in get_reports().items():
for report in reports.values():
# Attach the relevant JobResult (if any) to each Report.
report.result = results.get(report.full_name, None)
@@ -257,7 +257,7 @@ class ScriptViewSet(ViewSet):
lookup_value_regex = '[^/]+' # Allow dots
def _get_script(self, pk):
module_name, script_name = pk.split('.')
module_name, script_name = pk.split('.', maxsplit=1)
script = get_script(module_name, script_name)
if script is None:
raise Http404

View File

@@ -1,10 +1,6 @@
from contextlib import contextmanager
from django.db.models.signals import m2m_changed, pre_delete, post_save
from extras.signals import clear_webhooks, clear_webhook_queue, handle_changed_object, handle_deleted_object
from netbox import thread_locals
from netbox.request_context import set_request
from netbox.context import current_request, webhooks_queue
from .webhooks import flush_webhooks
@@ -16,27 +12,14 @@ def change_logging(request):
:param request: WSGIRequest object with a unique `id` set
"""
set_request(request)
thread_locals.webhook_queue = []
# Connect our receivers to the post_save and post_delete signals.
post_save.connect(handle_changed_object, dispatch_uid='handle_changed_object')
m2m_changed.connect(handle_changed_object, dispatch_uid='handle_changed_object')
pre_delete.connect(handle_deleted_object, dispatch_uid='handle_deleted_object')
clear_webhooks.connect(clear_webhook_queue, dispatch_uid='clear_webhook_queue')
current_request.set(request)
webhooks_queue.set([])
yield
# Disconnect change logging signals. This is necessary to avoid recording any errant
# changes during test cleanup.
post_save.disconnect(handle_changed_object, dispatch_uid='handle_changed_object')
m2m_changed.disconnect(handle_changed_object, dispatch_uid='handle_changed_object')
pre_delete.disconnect(handle_deleted_object, dispatch_uid='handle_deleted_object')
clear_webhooks.disconnect(clear_webhook_queue, dispatch_uid='clear_webhook_queue')
# Flush queued webhooks to RQ
flush_webhooks(thread_locals.webhook_queue)
del thread_locals.webhook_queue
flush_webhooks(webhooks_queue.get())
# Clear the request from thread-local storage
set_request(None)
# Clear context vars
current_request.set(None)
webhooks_queue.set([])

View File

@@ -3,7 +3,7 @@ from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.forms import SimpleArrayField
from django.utils.safestring import mark_safe
from extras.choices import CustomFieldTypeChoices
from extras.choices import CustomFieldVisibilityChoices, CustomFieldTypeChoices
from extras.models import *
from extras.utils import FeatureQuery
from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelForm, CSVMultipleContentTypeField, SlugField
@@ -38,6 +38,10 @@ class CustomFieldCSVForm(CSVModelForm):
required=False,
help_text='Comma-separated list of field choices'
)
ui_visibility = CSVChoiceField(
choices=CustomFieldVisibilityChoices,
help_text='How the custom field is displayed in the user interface'
)
class Meta:
model = CustomField

View File

@@ -34,7 +34,9 @@ class CustomFieldsMixin:
return ContentType.objects.get_for_model(self.model)
def _get_custom_fields(self, content_type):
return CustomField.objects.filter(content_types=content_type)
return CustomField.objects.filter(content_types=content_type).exclude(
ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_HIDDEN
)
def _get_form_field(self, customfield):
return customfield.to_form_field()
@@ -50,13 +52,6 @@ class CustomFieldsMixin:
field_name = f'cf_{customfield.name}'
self.fields[field_name] = self._get_form_field(customfield)
if customfield.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_READ_ONLY:
self.fields[field_name].disabled = True
if self.fields[field_name].help_text:
self.fields[field_name].help_text += '<br />'
self.fields[field_name].help_text += '<i class="mdi mdi-alert-circle-outline"></i> ' \
'Field is set to read-only.'
# Annotate the field in the list of CustomField form fields
self.custom_fields[field_name] = customfield
if customfield.group_name not in self.custom_field_groups:

View File

@@ -21,8 +21,8 @@ class Command(BaseCommand):
reports = get_reports()
# Run reports
for module_name, report_list in reports:
for report in report_list:
for module_name, report_list in reports.items():
for report in report_list.values():
if module_name in options['reports'] or report.full_name in options['reports']:
# Run the report and create a new JobResult

View File

@@ -14,7 +14,7 @@ from django.utils.safestring import mark_safe
from extras.choices import *
from extras.utils import FeatureQuery
from netbox.models import ChangeLoggedModel
from netbox.models.features import ExportTemplatesMixin, WebhooksMixin
from netbox.models.features import CloningMixin, ExportTemplatesMixin, WebhooksMixin
from utilities import filters
from utilities.forms import (
CSVChoiceField, CSVMultipleChoiceField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
@@ -41,7 +41,7 @@ class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)):
return self.get_queryset().filter(content_types=content_type)
class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
content_types = models.ManyToManyField(
to=ContentType,
related_name='custom_fields',
@@ -143,8 +143,14 @@ class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
verbose_name='UI visibility',
help_text='Specifies the visibility of custom field in the UI'
)
objects = CustomFieldManager()
clone_fields = (
'content_types', 'type', 'object_type', 'group_name', 'description', 'required', 'filter_logic', 'default',
'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choices', 'ui_visibility',
)
class Meta:
ordering = ['group_name', 'weight', 'name']
@@ -291,12 +297,13 @@ class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
return model.objects.filter(pk__in=value)
return value
def to_form_field(self, set_initial=True, enforce_required=True, for_csv_import=False):
def to_form_field(self, set_initial=True, enforce_required=True, enforce_visibility=True, for_csv_import=False):
"""
Return a form field suitable for setting a CustomField's value for an object.
set_initial: Set initial data for the field. This should be False when generating a field for bulk editing.
enforce_required: Honor the value of CustomField.required. Set to False for filtering/bulk editing.
enforce_visibility: Honor the value of CustomField.ui_visibility. Set to False for filtering.
for_csv_import: Return a form field suitable for bulk import of objects in CSV format.
"""
initial = self.default if set_initial else None
@@ -392,6 +399,12 @@ class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
if self.description:
field.help_text = escape(self.description)
# Annotate read-only fields
if enforce_visibility and self.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_READ_ONLY:
field.disabled = True
prepend = '<br />' if field.help_text else ''
field.help_text += f'{prepend}<i class="mdi mdi-alert-circle-outline"></i> Field is set to read-only.'
return field
def to_filter(self, lookup_expr=None):

View File

@@ -21,7 +21,7 @@ from extras.conditions import ConditionSet
from extras.utils import FeatureQuery, image_upload
from netbox.models import ChangeLoggedModel
from netbox.models.features import (
CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, JobResultsMixin, TagsMixin, WebhooksMixin,
CloningMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, JobResultsMixin, TagsMixin, WebhooksMixin,
)
from utilities.querysets import RestrictedQuerySet
from utilities.utils import render_jinja2
@@ -187,7 +187,7 @@ class Webhook(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
return render_jinja2(self.payload_url, context)
class CustomLink(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
class CustomLink(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
"""
A custom link to an external representation of a NetBox object. The link text and URL fields accept Jinja2 template
code to be rendered with an object as context.
@@ -230,6 +230,10 @@ class CustomLink(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
help_text="Force link to open in a new window"
)
clone_fields = (
'content_type', 'enabled', 'weight', 'group_name', 'button_class', 'new_window',
)
class Meta:
ordering = ['group_name', 'weight', 'name']
@@ -459,6 +463,14 @@ class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, WebhooksMixin
def get_absolute_url(self):
return reverse('extras:journalentry', args=[self.pk])
def clean(self):
super().clean()
# Prevent the creation of journal entries on unsupported models
permitted_types = ContentType.objects.filter(FeatureQuery('journaling').get_query())
if self.assigned_object_type not in permitted_types:
raise ValidationError(f"Journaling is not supported for this object type ({self.assigned_object_type}).")
def get_kind_color(self):
return JournalEntryKindChoices.colors.get(self.kind)

View File

@@ -26,20 +26,18 @@ def get_report(module_name, report_name):
"""
Return a specific report from within a module.
"""
file_path = '{}/{}.py'.format(settings.REPORTS_ROOT, module_name)
reports = get_reports()
module = reports.get(module_name)
spec = importlib.util.spec_from_file_location(module_name, file_path)
module = importlib.util.module_from_spec(spec)
try:
spec.loader.exec_module(module)
except FileNotFoundError:
if module is None:
return None
report = getattr(module, report_name, None)
report = module.get(report_name)
if report is None:
return None
return report()
return report
def get_reports():
@@ -52,7 +50,7 @@ def get_reports():
...
]
"""
module_list = []
module_list = {}
# Iterate through all modules within the reports path. These are the user-created files in which reports are
# defined.
@@ -61,7 +59,16 @@ def get_reports():
report_order = getattr(module, "report_order", ())
ordered_reports = [cls() for cls in report_order if is_report(cls)]
unordered_reports = [cls() for _, cls in inspect.getmembers(module, is_report) if cls not in report_order]
module_list.append((module_name, [*ordered_reports, *unordered_reports]))
module_reports = {}
for cls in [*ordered_reports, *unordered_reports]:
# For reports in submodules use the full import path w/o the root module as the name
report_name = cls.full_name.split(".", maxsplit=1)[1]
module_reports[report_name] = cls
if module_reports:
module_list[module_name] = module_reports
return module_list

View File

@@ -299,6 +299,10 @@ class BaseScript:
def module(cls):
return cls.__module__
@classmethod
def root_module(cls):
return cls.__module__.split(".")[0]
@classproperty
def job_timeout(self):
return getattr(self.Meta, 'job_timeout', None)
@@ -514,7 +518,9 @@ def get_scripts(use_names=False):
ordered_scripts = [cls for cls in script_order if is_script(cls)]
unordered_scripts = [cls for _, cls in inspect.getmembers(module, is_script) if cls not in script_order]
for cls in [*ordered_scripts, *unordered_scripts]:
module_scripts[cls.__name__] = cls
# For scripts in submodules use the full import path w/o the root module as the name
script_name = cls.full_name.split(".", maxsplit=1)[1]
module_scripts[script_name] = cls
if module_scripts:
scripts[module_name] = module_scripts

View File

@@ -7,14 +7,14 @@ from django.dispatch import receiver, Signal
from django_prometheus.models import model_deletes, model_inserts, model_updates
from extras.validators import CustomValidator
from netbox import thread_locals
from netbox.config import get_config
from netbox.request_context import get_request
from netbox.context import current_request, webhooks_queue
from netbox.signals import post_clean
from .choices import ObjectChangeActionChoices
from .models import ConfigRevision, CustomField, ObjectChange
from .webhooks import enqueue_object, get_snapshots, serialize_for_webhook
#
# Change logging/webhooks
#
@@ -23,22 +23,32 @@ from .webhooks import enqueue_object, get_snapshots, serialize_for_webhook
clear_webhooks = Signal()
def is_same_object(instance, webhook_data, request_id):
"""
Compare the given instance to the most recent queued webhook object, returning True
if they match. This check is used to avoid creating duplicate webhook entries.
"""
return (
ContentType.objects.get_for_model(instance) == webhook_data['content_type'] and
instance.pk == webhook_data['object_id'] and
request_id == webhook_data['request_id']
)
@receiver((post_save, m2m_changed))
def handle_changed_object(sender, instance, **kwargs):
"""
Fires when an object is created or updated.
"""
m2m_changed = False
if not hasattr(instance, 'to_objectchange'):
return
request = get_request()
m2m_changed = False
def is_same_object(instance, webhook_data):
return (
ContentType.objects.get_for_model(instance) == webhook_data['content_type'] and
instance.pk == webhook_data['object_id'] and
request.id == webhook_data['request_id']
)
# Get the current request, or bail if not set
request = current_request.get()
if request is None:
return
# Determine the type of change being made
if kwargs.get('created'):
@@ -69,13 +79,14 @@ def handle_changed_object(sender, instance, **kwargs):
objectchange.save()
# If this is an M2M change, update the previously queued webhook (from post_save)
webhook_queue = thread_locals.webhook_queue
if m2m_changed and webhook_queue and is_same_object(instance, webhook_queue[-1]):
queue = webhooks_queue.get()
if m2m_changed and queue and is_same_object(instance, queue[-1], request.id):
instance.refresh_from_db() # Ensure that we're working with fresh M2M assignments
webhook_queue[-1]['data'] = serialize_for_webhook(instance)
webhook_queue[-1]['snapshots']['postchange'] = get_snapshots(instance, action)['postchange']
queue[-1]['data'] = serialize_for_webhook(instance)
queue[-1]['snapshots']['postchange'] = get_snapshots(instance, action)['postchange']
else:
enqueue_object(webhook_queue, instance, request.user, request.id, action)
enqueue_object(queue, instance, request.user, request.id, action)
webhooks_queue.set(queue)
# Increment metric counters
if action == ObjectChangeActionChoices.ACTION_CREATE:
@@ -84,6 +95,7 @@ def handle_changed_object(sender, instance, **kwargs):
model_updates.labels(instance._meta.model_name).inc()
@receiver(pre_delete)
def handle_deleted_object(sender, instance, **kwargs):
"""
Fires when an object is deleted.
@@ -91,7 +103,10 @@ def handle_deleted_object(sender, instance, **kwargs):
if not hasattr(instance, 'to_objectchange'):
return
request = get_request()
# Get the current request, or bail if not set
request = current_request.get()
if request is None:
return
# Record an ObjectChange if applicable
if hasattr(instance, 'to_objectchange'):
@@ -101,22 +116,22 @@ def handle_deleted_object(sender, instance, **kwargs):
objectchange.save()
# Enqueue webhooks
webhook_queue = thread_locals.webhook_queue
enqueue_object(webhook_queue, instance, request.user, request.id, ObjectChangeActionChoices.ACTION_DELETE)
queue = webhooks_queue.get()
enqueue_object(queue, instance, request.user, request.id, ObjectChangeActionChoices.ACTION_DELETE)
webhooks_queue.set(queue)
# Increment metric counters
model_deletes.labels(instance._meta.model_name).inc()
@receiver(clear_webhooks)
def clear_webhook_queue(sender, **kwargs):
"""
Delete any queued webhooks (e.g. because of an aborted bulk transaction)
"""
logger = logging.getLogger('webhooks')
webhook_queue = thread_locals.webhook_queue
logger.info(f"Clearing {len(webhook_queue)} queued webhooks ({sender})")
webhook_queue.clear()
logger.info(f"Clearing {len(webhooks_queue.get())} queued webhooks ({sender})")
webhooks_queue.set([])
#

View File

@@ -183,6 +183,7 @@ class ObjectChangeTable(NetBoxTable):
verbose_name='Username'
)
full_name = tables.TemplateColumn(
accessor=tables.A('user'),
template_code=OBJECTCHANGE_FULL_NAME,
verbose_name='Full Name',
orderable=False
@@ -192,6 +193,7 @@ class ObjectChangeTable(NetBoxTable):
verbose_name='Type'
)
object_repr = tables.TemplateColumn(
accessor=tables.A('changed_object'),
template_code=OBJECTCHANGE_OBJECT,
verbose_name='Object'
)

View File

@@ -9,12 +9,12 @@ CONFIGCONTEXT_ACTIONS = """
OBJECTCHANGE_FULL_NAME = """
{% load helpers %}
{{ record.user.get_full_name|placeholder }}
{{ value.get_full_name|placeholder }}
"""
OBJECTCHANGE_OBJECT = """
{% if record.changed_object and record.changed_object.get_absolute_url %}
<a href="{{ record.changed_object.get_absolute_url }}">{{ record.object_repr }}</a>
{% if value and value.get_absolute_url %}
<a href="{{ value.get_absolute_url }}">{{ record.object_repr }}</a>
{% else %}
{{ record.object_repr }}
{% endif %}

View File

@@ -1,4 +1,4 @@
from django.urls import path
from django.urls import path, re_path
from extras import models, views
from netbox.views.generic import ObjectChangeLogView
@@ -100,12 +100,12 @@ urlpatterns = [
# Reports
path('reports/', views.ReportListView.as_view(), name='report_list'),
path('reports/<str:module>.<str:name>/', views.ReportView.as_view(), name='report'),
path('reports/results/<int:job_result_pk>/', views.ReportResultView.as_view(), name='report_result'),
re_path(r'^reports/(?P<module>.([^.]+)).(?P<name>.(.+))/', views.ReportView.as_view(), name='report'),
# Scripts
path('scripts/', views.ScriptListView.as_view(), name='script_list'),
path('scripts/<str:module>.<str:name>/', views.ScriptView.as_view(), name='script'),
path('scripts/results/<int:job_result_pk>/', views.ScriptResultView.as_view(), name='script_result'),
re_path(r'^scripts/(?P<module>.([^.]+)).(?P<name>.(.+))/', views.ScriptView.as_view(), name='script'),
]

View File

@@ -441,6 +441,12 @@ class ImageAttachmentEditView(generic.ObjectEditView):
def get_return_url(self, request, obj=None):
return obj.parent.get_absolute_url() if obj else super().get_return_url(request)
def get_extra_addanother_params(self, request):
return {
'content_type': request.GET.get('content_type'),
'object_id': request.GET.get('object_id'),
}
class ImageAttachmentDeleteView(generic.ObjectDeleteView):
queryset = ImageAttachment.objects.all()
@@ -528,9 +534,10 @@ class ReportListView(ContentTypePermissionRequiredMixin, View):
}
ret = []
for module, report_list in reports:
for module, report_list in reports.items():
module_reports = []
for report in report_list:
for report in report_list.values():
report.result = results.get(report.full_name, None)
module_reports.append(report)
ret.append((module, module_reports))
@@ -607,7 +614,7 @@ class ReportResultView(ContentTypePermissionRequiredMixin, View):
result = get_object_or_404(JobResult.objects.all(), pk=job_result_pk, obj_type=report_content_type)
# Retrieve the Report and attach the JobResult to it
module, report_name = result.name.split('.')
module, report_name = result.name.split('.', maxsplit=1)
report = get_report(module, report_name)
report.result = result

View File

@@ -143,7 +143,7 @@ class FHRPGroupAssignmentSerializer(NetBoxModelSerializer):
'last_updated',
]
@swagger_serializer_method(serializer_or_field=serializers.DictField)
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
def get_interface(self, obj):
if obj.interface is None:
return None
@@ -175,6 +175,7 @@ class VLANGroupSerializer(NetBoxModelSerializer):
queryset=ContentType.objects.filter(
model__in=VLANGROUP_SCOPE_TYPES
),
allow_null=True,
required=False,
default=None
)
@@ -190,6 +191,7 @@ class VLANGroupSerializer(NetBoxModelSerializer):
]
validators = []
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
def get_scope(self, obj):
if obj.scope_id is None:
return None
@@ -373,7 +375,7 @@ class IPAddressSerializer(NetBoxModelSerializer):
'custom_fields', 'created', 'last_updated',
]
@swagger_serializer_method(serializer_or_field=serializers.DictField)
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
def get_assigned_object(self, obj):
if obj.assigned_object is None:
return None
@@ -482,7 +484,7 @@ class L2VPNTerminationSerializer(NetBoxModelSerializer):
'assigned_object', 'tags', 'custom_fields', 'created', 'last_updated'
]
@swagger_serializer_method(serializer_or_field=serializers.DictField)
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
def get_assigned_object(self, instance):
serializer = get_serializer_for_model(instance.assigned_object, prefix=NESTED_SERIALIZER_PREFIX)
context = {'request': self.context['request']}

View File

@@ -112,6 +112,18 @@ class IPAddressViewSet(NetBoxModelViewSet):
serializer_class = serializers.IPAddressSerializer
filterset_class = filtersets.IPAddressFilterSet
@advisory_lock(ADVISORY_LOCK_KEYS['available-ips'])
def create(self, request, *args, **kwargs):
return super().create(request, *args, **kwargs)
@advisory_lock(ADVISORY_LOCK_KEYS['available-ips'])
def update(self, request, *args, **kwargs):
return super().update(request, *args, **kwargs)
@advisory_lock(ADVISORY_LOCK_KEYS['available-ips'])
def destroy(self, request, *args, **kwargs):
return super().destroy(request, *args, **kwargs)
class FHRPGroupViewSet(NetBoxModelViewSet):
queryset = FHRPGroup.objects.prefetch_related('ip_addresses', 'tags')
@@ -174,6 +186,21 @@ class L2VPNTerminationViewSet(NetBoxModelViewSet):
# Views
#
def get_results_limit(request):
"""
Return the lesser of the specified limit (if any) and the configured MAX_PAGE_SIZE.
"""
config = get_config()
try:
limit = int(request.query_params.get('limit', config.PAGINATE_COUNT)) or config.MAX_PAGE_SIZE
except ValueError:
limit = config.PAGINATE_COUNT
if config.MAX_PAGE_SIZE:
limit = min(limit, config.MAX_PAGE_SIZE)
return limit
class AvailablePrefixesView(ObjectValidationMixin, APIView):
queryset = Prefix.objects.all()
@@ -265,16 +292,7 @@ class AvailableIPAddressesView(ObjectValidationMixin, APIView):
@swagger_auto_schema(responses={200: serializers.AvailableIPSerializer(many=True)})
def get(self, request, pk):
parent = self.get_parent(request, pk)
config = get_config()
PAGINATE_COUNT = config.PAGINATE_COUNT
MAX_PAGE_SIZE = config.MAX_PAGE_SIZE
try:
limit = int(request.query_params.get('limit', PAGINATE_COUNT))
except ValueError:
limit = PAGINATE_COUNT
if MAX_PAGE_SIZE:
limit = min(limit, MAX_PAGE_SIZE)
limit = get_results_limit(request)
# Calculate available IPs within the parent
ip_list = []
@@ -357,8 +375,9 @@ class AvailableVLANsView(ObjectValidationMixin, APIView):
@swagger_auto_schema(responses={200: serializers.AvailableVLANSerializer(many=True)})
def get(self, request, pk):
vlangroup = get_object_or_404(VLANGroup.objects.restrict(request.user), pk=pk)
available_vlans = vlangroup.get_available_vids()
limit = get_results_limit(request)
available_vlans = vlangroup.get_available_vids()[:limit]
serializer = serializers.AvailableVLANSerializer(available_vlans, many=True, context={
'request': request,
'group': vlangroup,

View File

@@ -965,7 +965,11 @@ class L2VPNFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = Q(identifier=value) | Q(name__icontains=value) | Q(description__icontains=value)
qs_filter = Q(name__icontains=value) | Q(description__icontains=value)
try:
qs_filter |= Q(identifier=int(value))
except ValueError:
pass
return queryset.filter(qs_filter)
@@ -1071,6 +1075,12 @@ class L2VPNTerminationFilterSet(NetBoxModelFilterSet):
qs_filter = Q(l2vpn__name__icontains=value)
return queryset.filter(qs_filter)
def filter_assigned_object(self, queryset, name, value):
qs = queryset.filter(
Q(**{'{}__in'.format(name): value})
)
return qs
def filter_site(self, queryset, name, value):
qs = queryset.filter(
Q(

View File

@@ -478,6 +478,7 @@ class ServiceTemplateFilterForm(NetBoxModelFilterSetForm):
class ServiceFilterForm(ServiceTemplateFilterForm):
model = Service
tag = TagFilterField(model)
class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):

View File

@@ -88,6 +88,12 @@ class RouteTargetForm(TenancyForm, NetBoxModelForm):
class RIRForm(NetBoxModelForm):
slug = SlugField()
fieldsets = (
('RIR', (
'name', 'slug', 'is_private', 'description', 'tags',
)),
)
class Meta:
model = RIR
fields = [
@@ -164,6 +170,12 @@ class ASNForm(TenancyForm, NetBoxModelForm):
class RoleForm(NetBoxModelForm):
slug = SlugField()
fieldsets = (
('Role', (
'name', 'slug', 'weight', 'description', 'tags',
)),
)
class Meta:
model = Role
fields = [
@@ -537,9 +549,15 @@ class FHRPGroupForm(NetBoxModelForm):
fields = (
'protocol', 'group_id', 'auth_type', 'auth_key', 'description', 'ip_vrf', 'ip_address', 'ip_status', 'tags',
)
widgets = {
'protocol': StaticSelect(),
'auth_type': StaticSelect(),
'ip_status': StaticSelect(),
}
def save(self, *args, **kwargs):
instance = super().save(*args, **kwargs)
user = getattr(instance, '_user', None) # Set under FHRPGroupEditView.alter_object()
# Check if we need to create a new IPAddress for the group
if self.cleaned_data.get('ip_address'):
@@ -553,7 +571,7 @@ class FHRPGroupForm(NetBoxModelForm):
ipaddress.save()
# Check that the new IPAddress conforms with any assigned object-level permissions
if not IPAddress.objects.filter(pk=ipaddress.pk).first():
if not IPAddress.objects.restrict(user, 'add').filter(pk=ipaddress.pk).first():
raise PermissionsViolation()
return instance
@@ -784,6 +802,12 @@ class ServiceTemplateForm(NetBoxModelForm):
help_text="Comma-separated list of one or more port numbers. A range may be specified using a hyphen."
)
fieldsets = (
('Service Template', (
'name', 'protocol', 'ports', 'description', 'tags',
)),
)
class Meta:
model = ServiceTemplate
fields = ('name', 'protocol', 'ports', 'description', 'tags')
@@ -854,6 +878,7 @@ class ServiceCreateForm(ServiceForm):
del self.fields[field].widget.attrs['required']
def clean(self):
super().clean()
if self.cleaned_data['service_template']:
# Create a new Service from the specified template
service_template = self.cleaned_data['service_template']

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.0.7 on 2022-08-22 15:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ipam', '0059_l2vpn'),
]
operations = [
migrations.AlterField(
model_name='l2vpn',
name='slug',
field=models.SlugField(max_length=100, unique=True),
),
]

View File

@@ -35,13 +35,16 @@ class GetAvailablePrefixesMixin:
def get_available_prefixes(self):
"""
Return all available Prefixes within this aggregate as an IPSet.
Return all available prefixes within this Aggregate or Prefix as an IPSet.
"""
prefix = netaddr.IPSet(self.prefix)
child_prefixes = netaddr.IPSet([child.prefix for child in self.get_child_prefixes()])
available_prefixes = prefix - child_prefixes
params = {
'prefix__net_contained': str(self.prefix)
}
if hasattr(self, 'vrf'):
params['vrf'] = self.vrf
return available_prefixes
child_prefixes = Prefix.objects.filter(**params).values_list('prefix', flat=True)
return netaddr.IPSet(self.prefix) - netaddr.IPSet(child_prefixes)
def get_first_available_prefix(self):
"""
@@ -124,6 +127,10 @@ class ASN(NetBoxModel):
def __str__(self):
return f'AS{self.asn_with_asdot}'
@classmethod
def get_prerequisite_models(cls):
return [RIR, ]
def get_absolute_url(self):
return reverse('ipam:asn', args=[self.pk])
@@ -185,6 +192,10 @@ class Aggregate(GetAvailablePrefixesMixin, NetBoxModel):
def __str__(self):
return str(self.prefix)
@classmethod
def get_prerequisite_models(cls):
return [RIR, ]
def get_absolute_url(self):
return reverse('ipam:aggregate', args=[self.pk])

View File

@@ -1,8 +1,10 @@
from django.apps import apps
from django.contrib.contenttypes.fields import GenericRelation, GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.functional import cached_property
from ipam.choices import L2VPNTypeChoices
from ipam.constants import L2VPN_ASSIGNMENT_MODELS
@@ -19,7 +21,10 @@ class L2VPN(NetBoxModel):
max_length=100,
unique=True
)
slug = models.SlugField()
slug = models.SlugField(
max_length=100,
unique=True
)
type = models.CharField(
max_length=50,
choices=L2VPNTypeChoices
@@ -53,6 +58,8 @@ class L2VPN(NetBoxModel):
to='tenancy.ContactAssignment'
)
clone_fields = ('type',)
class Meta:
ordering = ('name', 'identifier')
verbose_name = 'L2VPN'
@@ -65,6 +72,13 @@ class L2VPN(NetBoxModel):
def get_absolute_url(self):
return reverse('ipam:l2vpn', args=[self.pk])
@cached_property
def can_add_termination(self):
if self.type in L2VPNTypeChoices.P2P and self.terminations.count() >= 2:
return False
else:
return True
class L2VPNTermination(NetBoxModel):
l2vpn = models.ForeignKey(
@@ -101,6 +115,10 @@ class L2VPNTermination(NetBoxModel):
return f'{self.assigned_object} <> {self.l2vpn}'
return super().__str__()
@classmethod
def get_prerequisite_models(cls):
return [apps.get_model('ipam.L2VPN'), ]
def get_absolute_url(self):
return reverse('ipam:l2vpntermination', args=[self.pk])

View File

@@ -92,6 +92,8 @@ class Service(ServiceBase, NetBoxModel):
verbose_name='IP addresses'
)
clone_fields = ['protocol', 'ports', 'description', 'device', 'virtual_machine', 'ipaddresses', ]
class Meta:
ordering = ('protocol', 'ports', 'pk') # (protocol, port) may be non-unique

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