Compare commits

...

234 Commits

Author SHA1 Message Date
Jeremy Stretch
3c91331e16 Merge pull request #12234 from netbox-community/develop
Release v3.4.8
2023-04-12 15:29:09 -04:00
jeremystretch
eef38257b9 Release v3.4.8 2023-04-12 14:06:09 -04:00
Austin de Coup-Crank
bb9a125934 Closes #12040: fix bulk import tab selection 2023-04-12 13:01:04 -04:00
Arthur Hanson
8de252e34e 11432 device field (#11567)
* 11432 make device field on interface read-only on api edit call

* 11432 make device field on interface read-only on api edit call

* 11432 extend serializer change to mixin

* 11432 add readonlydevicemixin to template serializers

* 11432 change subclass ordering

* 11432 fix device_type for template serializers

* 11432 DRY

* 11432 DRY

* 11432 make internal var

* 11432 change to model-level validation

* 11432 fix fk accessor

* Clean up validation error messages

---------

Co-authored-by: jeremystretch <jstretch@netboxlabs.com>
2023-04-12 10:25:54 -04:00
Arthur Hanson
9e305c6181 Closes #12207: Establish a permission for creating API tokens on behalf of other users (#12192)
* 11091 add permission to allow user to create api tokens for other users

* 11091 update docs

* 11091 fix for test

* 11091 fix for test

* 11091 test case for invalid token creation

* 11091 add test for permission grant

* Cleanup & fix serializer validation

---------

Co-authored-by: jeremystretch <jstretch@netboxlabs.com>
2023-04-12 10:25:06 -04:00
pobradovic08
97ed6439ce Fixes #12227: L2VPN Bulk import not setting Tenant field 2023-04-12 09:24:31 -04:00
jeremystretch
6820796c10 Closes #10414: Enable general purpose image attachments for device types 2023-04-10 16:43:40 -04:00
jeremystretch
4a331b560f Closes #11015: Remove unit from commit rate column header in circuits table 2023-04-10 16:35:21 -04:00
jeremystretch
4c9cf9032c Changelog for #10221, #10600, #11431, #11454 2023-04-10 14:06:36 -04:00
jeremystretch
ada01b39cc #10221: Tweak variable names & error message 2023-04-10 14:03:59 -04:00
Abhimanyu Saharan
b41f8755df Fixes GenericForeignKey validation (#11550)
* added model validation for GenericForeignKey

* added ct_field and fk_field null validation

* applied suggestion
2023-04-10 14:02:32 -04:00
Abhimanyu Saharan
2c07762b7a Added optional user and group on custom field (#12206)
* added group and user model to object_type

* Update netbox/utilities/utils.py

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

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2023-04-10 12:40:13 -04:00
kkthxbye
278f2b173a Fixes #11431 - Disallow changing customfield type after creation (#11449)
* Disallow changing customfield type after creation

* Fix test_api.CustomFieldTest

---------

Co-authored-by: kkthxbye-code <>
2023-04-10 10:13:08 -04:00
jeremystretch
768d6f624e Fixes #12191: Change absolute image path to relative 2023-04-10 09:17:13 -04:00
jeremystretch
1146aaff89 Closes #11453: Display a warning banner when DEBUG is enabled 2023-04-10 09:12:04 -04:00
Arthur Hanson
5a4feb7099 10615 filter cable termination_id with cable_end (#12182)
* 10615 filter cable termination_id with cable_end

* 10615 filter distinct

* 10615 filter distinct
2023-04-07 14:13:58 -04:00
jeremystretch
a6fd0ab09a #12007: Move vlan & vlan_id filter methods to CommonInterfaceFilterSet 2023-04-07 13:58:12 -04:00
jeremystretch
9f71cf79e6 Changelog for #12007, #12118 2023-04-07 11:54:43 -04:00
Abhimanyu Saharan
c26fe266cc Moved interface filterset under common class (#12200)
* moved interface filterset under common class #12007

* lint fix
2023-04-07 10:54:39 -04:00
jeremystretch
085cfc58f4 Fixes #12184: Fix filtered bulk deletion for various models 2023-04-07 10:25:36 -04:00
jeremystretch
63a0ec7a79 Fixes #12190: Fix form layout for plugin textarea fields 2023-04-07 10:03:47 -04:00
jeremystretch
ccfdc216a5 Fixes #12118: Refactor bulk creation logic under _instantiate_components() 2023-04-06 17:01:46 -04:00
Jeremy Stretch
2bf9acfb19 Closes #12193: Clean up tests (#12197)
* Fix skipped API tests

* Remove invalid tests

* Correct logger name
2023-04-06 16:35:27 -04:00
jeremystretch
74d8baea30 Remove NS1 from sponsors list 2023-04-06 14:32:57 -04:00
jeremystretch
f8d40ae824 Changelog for #11746, #12011, #12087 2023-04-05 13:32:18 -04:00
Daniel Sheppard
41c92483a0 #12087 - Fix Bulk Edit update when M2M operations are present. (#12169)
* #12087 - Fix Bulk Edit update when M2M operations are present.

* #12087 - Minor tweaks

* Change .set() to .clear()

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

* #12087 - Update comments

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2023-04-05 13:29:32 -04:00
Arthur Hanson
94c2a2e56c 11746 fix delete custom field (#12092)
* 11746 delete custom field

* 11746 use filter instead of exclude
2023-04-04 16:38:48 -04:00
Arthur
0a2ae90411 12011 fix module bay bulk create 2023-04-04 16:34:51 -04:00
jeremystretch
b032742418 Closes #12133: Move any instance mutations inside clean() to save() 2023-04-03 16:26:07 -04:00
jeremystretch
8a684adf66 Changelog for #12074, #12117 2023-04-03 15:38:05 -04:00
Arthur Hanson
bca00cd97a 12117 remove clone from cable (#12130)
* 12117 remove clone from cable

* 12117 remove clone button if no params

* Update clone.html

* Update clone.html

* Update clone.html

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2023-04-03 15:27:13 -04:00
Arthur Hanson
2883fa14de Fixes #12074: Move automatic location assignment out of clean()
* 12074 fix full clean

* 12074 move device location setting to save from clean

* 12074 fix set location only if present in rack

* Update base.py

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2023-04-03 15:22:36 -04:00
jeremystretch
3264636b7a Changelog for #12084, #12095 2023-03-30 17:02:23 -04:00
gdprdatasubect
fbc23424a6 Update models.py
Change default MIME-Type as discussed under #12095
2023-03-30 16:58:41 -04:00
jeremystretch
6f08c4a4be Fixes #11846: Update database creation instructions for PostgreSQL 14+ 2023-03-30 16:35:56 -04:00
jeremystretch
0ac8419005 Fixes #12104: Restore copy-to-clipboard & footer navigation in docs 2023-03-30 16:29:54 -04:00
Arthur Hanson
e467589730 12084 saved filters (#12090)
* 12084 change back saved filter saving

* 12084 doc string

* 12084 add test

* Pass SavedFilter initial parameters as JSON & relocate test

---------

Co-authored-by: jeremystretch <jstretch@netboxlabs.com>
2023-03-30 11:00:40 -04:00
jeremystretch
0330c652bd PRVB 2023-03-28 14:13:39 -04:00
jeremystretch
5a6005cdfa Merge branch 'master' into develop 2023-03-28 14:09:47 -04:00
jeremystretch
4eaba7993f Release v3.4.7 2023-03-28 14:08:04 -04:00
kkthxbye
2840f9d71d Fixes #11991 - Add vdcs to InterfaceImportForm and InterfaceBulkEditForm (#11996)
* Add vdcs to InterfaceImportForm and InterfaceBulkEditForm

* Filter vdcs queryset by device when bulk importing interfaces
2023-03-28 14:08:04 -04:00
jeremystretch
9946ae2981 Update changelog for #11645, #12029, #12038 2023-03-28 14:08:04 -04:00
Abhimanyu Saharan
420ec6791f Updated _schedule_at to use local time when _interval is set (#12006)
* updated _schedule_at to use local time when _interval is set

* updated schedule_at to use local time when interval is set
2023-03-28 14:08:04 -04:00
Arthur
47234f1607 12038 show vc priority with placeholder 2023-03-28 14:08:04 -04:00
Arthur
b058bd9cea 12029 add description to virtual description add 2023-03-28 14:08:04 -04:00
jeremystretch
5b03636c88 Update changelog 2023-03-28 14:08:04 -04:00
jeremystretch
be55bb43ad #12058: Fix initial JSON population 2023-03-28 14:08:04 -04:00
Arthur
293afab730 12058 add clone to config context 2023-03-28 14:08:04 -04:00
Arthur Hanson
6b622fd9bf 11933 saved filters clone of content-types and add m2m field cloning (#12014)
* 11933 saved filters clone of content-types and add m2m field cloning

* Fix JSON rendering

* Add content_types to CustomLink.clone()

---------

Co-authored-by: jeremystretch <jstretch@netboxlabs.com>
2023-03-28 14:08:04 -04:00
Arthur
7280dfacab 12038 fix clone tag 2023-03-28 14:08:04 -04:00
Arthur
4428a446d0 12008 make export templates cloneable 2023-03-28 14:08:04 -04:00
Austin de Coup-Crank
2eedcac383 Fixes #11977: Multiple remote authentication backends (#12012)
* Add suppport for REMOTE_AUTH_BACKEND as iterable

* Closes #11977: Support for multiple auth backends

* Tweak list casting

---------

Co-authored-by: jeremystretch <jstretch@netboxlabs.com>
2023-03-28 14:08:04 -04:00
Arthur
35af1d7b61 12049 fix passsword typo 2023-03-28 14:08:04 -04:00
jeremystretch
1b92958870 Closes #11682: Remove lateral padding from highlighted text 2023-03-28 14:08:04 -04:00
Brian Candler
795669113f Improve error reporting for duplicate CSV column headings
Fixes #11990
2023-03-28 14:08:04 -04:00
kkthxbye-code
de57446f36 Use ssid for the string representation of WirelessLinks if available 2023-03-28 14:08:04 -04:00
kkthxbye-code
3b13cef0c8 Render the parameters column as JSON in SavedFiltersTable 2023-03-28 14:08:04 -04:00
kkthxbye-code
497f3145fa Add parameters to the SavedFilterTable 2023-03-28 14:08:04 -04:00
jeremystretch
f597b76ddc Fixes #11979: Correct URL for tags in route targets list 2023-03-28 14:08:04 -04:00
Daniel W. Anner
ebaac82560 Removed type2-ieee802.3at as per described in #11984 2023-03-28 14:08:04 -04:00
Ryan Merolle
371764fecd Add fieldsets functionality to scripts to allow for form field groupings (#11880)
* update script template

* update docs

* introduce default_fieldset

* correct custom script docs

* default to use fieldsets in scripts

* update scripts docs for new behavior

* Misc cleanup

---------

Co-authored-by: jeremystretch <jstretch@netboxlabs.com>
2023-03-28 14:08:04 -04:00
jeremystretch
f67deb0dea PRVB 2023-03-28 14:08:04 -04:00
jeremystretch
d3c5f1e744 Release v3.4.7 2023-03-28 13:46:43 -04:00
kkthxbye
b12551c64b Fixes #11991 - Add vdcs to InterfaceImportForm and InterfaceBulkEditForm (#11996)
* Add vdcs to InterfaceImportForm and InterfaceBulkEditForm

* Filter vdcs queryset by device when bulk importing interfaces
2023-03-28 13:20:23 -04:00
jeremystretch
9995fad513 Update changelog for #11645, #12029, #12038 2023-03-28 10:21:38 -04:00
Abhimanyu Saharan
1d2335d578 Updated _schedule_at to use local time when _interval is set (#12006)
* updated _schedule_at to use local time when _interval is set

* updated schedule_at to use local time when interval is set
2023-03-28 10:19:18 -04:00
Arthur
ad03061edf 12038 show vc priority with placeholder 2023-03-28 10:13:51 -04:00
Arthur
87eabdbffb 12029 add description to virtual description add 2023-03-28 10:10:05 -04:00
jeremystretch
19787dd21d Update changelog 2023-03-28 10:02:29 -04:00
jeremystretch
7d64e5bc62 #12058: Fix initial JSON population 2023-03-28 10:00:15 -04:00
Arthur
a2c7452f90 12058 add clone to config context 2023-03-28 09:56:06 -04:00
Arthur Hanson
654e32cbbe 11933 saved filters clone of content-types and add m2m field cloning (#12014)
* 11933 saved filters clone of content-types and add m2m field cloning

* Fix JSON rendering

* Add content_types to CustomLink.clone()

---------

Co-authored-by: jeremystretch <jstretch@netboxlabs.com>
2023-03-28 09:53:57 -04:00
Arthur
879aabe2f9 12038 fix clone tag 2023-03-28 09:48:05 -04:00
Arthur
835af32213 12008 make export templates cloneable 2023-03-28 09:45:44 -04:00
Austin de Coup-Crank
571d33e660 Fixes #11977: Multiple remote authentication backends (#12012)
* Add suppport for REMOTE_AUTH_BACKEND as iterable

* Closes #11977: Support for multiple auth backends

* Tweak list casting

---------

Co-authored-by: jeremystretch <jstretch@netboxlabs.com>
2023-03-28 08:44:24 -04:00
Arthur
96eb89a469 12049 fix passsword typo 2023-03-24 20:19:58 -04:00
jeremystretch
8d25d7812c Closes #11682: Remove lateral padding from highlighted text 2023-03-17 16:04:55 -04:00
Brian Candler
a864e8127b Improve error reporting for duplicate CSV column headings
Fixes #11990
2023-03-17 09:21:40 -04:00
kkthxbye-code
fc482ed096 Use ssid for the string representation of WirelessLinks if available 2023-03-17 09:19:07 -04:00
kkthxbye-code
85f40bcbe0 Render the parameters column as JSON in SavedFiltersTable 2023-03-16 09:12:48 -04:00
kkthxbye-code
e7ed280790 Add parameters to the SavedFilterTable 2023-03-16 09:12:48 -04:00
jeremystretch
3d14a79428 Fixes #11979: Correct URL for tags in route targets list 2023-03-16 08:20:52 -04:00
Daniel W. Anner
d93a24d0bb Removed type2-ieee802.3at as per described in #11984 2023-03-16 08:16:49 -04:00
Ryan Merolle
4286d74d44 Add fieldsets functionality to scripts to allow for form field groupings (#11880)
* update script template

* update docs

* introduce default_fieldset

* correct custom script docs

* default to use fieldsets in scripts

* update scripts docs for new behavior

* Misc cleanup

---------

Co-authored-by: jeremystretch <jstretch@netboxlabs.com>
2023-03-14 15:50:49 -04:00
jeremystretch
016eff52c0 PRVB 2023-03-13 11:59:57 -04:00
Jeremy Stretch
6b6ea36b4c Merge pull request #11965 from netbox-community/develop
Release v3.4.6
2023-03-13 11:49:41 -04:00
jeremystretch
520493c714 Release v3.4.6 2023-03-13 11:16:31 -04:00
kkthxbye
e459c46dad Fixes #11929 - Strip whitespace from csv headers (#11956)
* Strip whitespace from csv headers

* Move strip() call to parse_csv()

---------

Co-authored-by: jeremystretch <jstretch@netboxlabs.com>
2023-03-13 10:55:18 -04:00
jeremystretch
a71a59c088 Fixes #11631: Fix filtering changelog & journal entries by multiple content type IDs 2023-03-13 10:00:05 -04:00
jeremystretch
267a14264b Fixes #11927: Correct loading of plugin resources with custom paths 2023-03-13 08:52:38 -04:00
jeremystretch
065738473e Changelog for #11850, #11851 2023-03-13 08:38:57 -04:00
kkthxbye-code
f698c42c41 Fix loading of CSV files with BOM 2023-03-13 08:13:59 -04:00
rmanyari
ab303db3dd Closes #11851: Add family field to IPAddress queries in GraphQL (#11870)
* Closes #11851: Add family field to IPAddress queries in GraphQL

* Add family field support to Prefix and Aggregate, fix tests
2023-03-10 14:48:45 -05:00
rganascim
07b0b93256 Closes #11638: add http redirect to apache 2023-03-10 09:55:22 -05:00
Jeremy Stretch
d880875e67 Changelog for #11294, #11819 2023-03-09 08:37:03 -05:00
Aron Bergur Jóhannsson
fa60f9d2a8 Closes #11294: Markdown Preview (#11894)
* MarkdownWidget

* Change border and color of active markdown tab

* Fix template name typo

* Add render markdown endpoint

* Static assets for markdown widget

* widget style fix and unique ids based on name

* Replace SmallTextArea with SmallMarkdownWidget

* Clear innerHTML before swapping

* render markdown directly in template

* change render markdown view path

* remove small markdown widget

* Simplify rendering logic

* Use a form to clean input Markdown data

---------

Co-authored-by: Jeremy Stretch <jstretch@ns1.com>
2023-03-09 08:21:13 -05:00
Abhimanyu Saharan
33286aad39 added the missing filterset 2023-03-07 17:42:23 -05:00
jeremystretch
d48a8770de Fixes #11903: Fix escaping of return URL values for action buttons in tables 2023-03-07 09:34:25 -05:00
Charly Forot
ee5b707e68 README.md: typo
infrasucture -> infrastructure
2023-03-06 10:49:02 -05:00
jose_d
d29a4a60f9 README.md: typo 2023-03-03 11:29:47 -05:00
Ximalas
07b39fe44a Update choices.py: Adding Cisco StackWise-1T (#11886)
Cisco Catalyst 9300X Series adds Cisco StackWise-1T.
https://www.cisco.com/c/en/us/products/collateral/switches/catalyst-9300-series-switches/nb-06-cat9300-ser-data-sheet-cte-en.html
2023-03-02 08:59:08 -05:00
jeremystretch
e270cb20ba Changelog for #11470, #11871 2023-03-01 17:34:57 -05:00
rmanyari
6640fc9eb7 Fixes #11470: Validation and user friendly message on invalid address query param (#11858)
* Fixes #11470: Validation and user friendly message on invalid address query param

* Update invalid input handling to return empty set instead of raising exception
2023-03-01 16:49:40 -05:00
Daniel W. Anner
189668fbfb Implemented PoE choice for IEEE 802.3az 2023-03-01 15:30:19 -05:00
jeremystretch
c9e5a4c996 Changelog for #11011 2023-02-27 15:38:21 -05:00
jeremystretch
ed5fd140eb Optimize shallow_compare_dict() 2023-02-27 15:38:21 -05:00
aron bergur jóhannsson
4f12eccde6 Update toggle caption for vif 2023-02-27 14:53:52 -05:00
aron bergur jóhannsson
1f0db6d2fa include static assets 2023-02-27 14:53:52 -05:00
aron bergur jóhannsson
eed6990b39 Closes #11011: Hide virtual interfaces 2023-02-27 14:53:52 -05:00
jeremystretch
a554164d1d Changelog for #10058, #11565, #11758, #11817 2023-02-27 14:46:03 -05:00
jeremystretch
6ea30798bf #10058: Enable primary IP search for virtual machines too 2023-02-27 14:41:34 -05:00
Pieter Lambrecht
3418b7adf6 remove DeviceIndex search for ipaddresses 2023-02-27 14:36:56 -05:00
Pieter Lambrecht
88d5119c59 Search device by primary IP address 2023-02-27 14:36:56 -05:00
Marc
6e7d2f53aa Change Interpreter in shebang to python3 2023-02-27 14:09:10 -05:00
Simon Toft
559a318584 Fixes #11565 - Populate custom field defaults when creating FHRP groups with VIP 2023-02-27 14:02:22 -05:00
Sebastian Himmler
67499cbf06 add conntected_enpoints property to graphql 2023-02-27 12:52:05 -05:00
Rafael Ganascim
0744ff2fa0 Fixes #11758 - replace unsafe chars in menu label (#11831)
* Fixes #11758 - replace unsafe chars in menu label

* Fixes #11758 - replace unsafe chars in menu label
2023-02-27 11:42:30 -05:00
jeremystretch
cfa6b28ceb Closes #11807: Restore default page size when navigating between views 2023-02-27 09:22:48 -05:00
jeremystretch
ed77c03830 Fixes #11796: When importing devices, restrict rack by location only if the location field is specified 2023-02-27 08:26:32 -05:00
jeremystretch
561f1eadfc PRVB 2023-02-21 09:03:19 -05:00
Jeremy Stretch
6638fd88b4 Merge pull request #11793 from netbox-community/develop
Release v3.4.5
2023-02-21 09:01:01 -05:00
jeremystretch
c280ca35d6 Release v3.4.5 2023-02-21 08:45:52 -05:00
jeremystretch
3586cf79d4 Arrange parameters alphabetically 2023-02-21 08:42:39 -05:00
jeremystretch
972ba7bfdc #11685: Fix migration 2023-02-20 10:27:30 -05:00
jeremystretch
5a4d8a7107 Closes #11787: Rebuild any missing search cache entires after upgrade 2023-02-20 09:49:13 -05:00
jeremystretch
3e946c78d0 #11685: Clear cached search records for relevant IPAM objects 2023-02-20 09:02:58 -05:00
jeremystretch
0855ff8b42 Skip clearing cache when handling new objects 2023-02-20 08:17:39 -05:00
jeremystretch
cd09501d4d #11685: Omit no-op migration 2023-02-19 20:08:57 -05:00
jeremystretch
e635e3e959 Fixes #11658: Remove reindex command call from search migration 2023-02-19 18:57:27 -05:00
jeremystretch
9efc4689cc Changelog for #11685 2023-02-19 18:57:27 -05:00
kkthxbye-code
25278becef Change Prefix and Aggregate search index weights to better order search results. 2023-02-19 18:50:24 -05:00
kkthxbye-code
fc7cb106c1 Address feedback 2023-02-19 18:50:24 -05:00
kkthxbye-code
18ea7d1e13 pep8 fixes 2023-02-19 18:50:24 -05:00
kkthxbye-code
eed1b8f412 Create CachedValueField to contain search specific lookups 2023-02-19 18:50:24 -05:00
kkthxbye-code
a61e7e7c04 Fix typo in search query 2023-02-19 18:50:24 -05:00
kkthxbye-code
ce166b12ce Proof of concept for showing containing prefixes when searching for ip-addresses. 2023-02-19 18:50:24 -05:00
jeremystretch
315371bf7c Fixes #11786: List only applicable object types in form widget when filtering custom fields 2023-02-19 16:17:57 -05:00
jeremystretch
afc752b4ce Fixes #11723: Circuit terminations should link to their associated circuits (rather than site or provider network) 2023-02-17 21:31:19 -05:00
jeremystretch
126f9ba05f Raise stale timers from 60/30 to 90/30 2023-02-17 16:57:52 -05:00
jeremystretch
c031951f4b Closes #11110: Add start_address and end_address filters for IP ranges 2023-02-17 16:50:10 -05:00
jeremystretch
c36e7a1d0b Update introduction doc 2023-02-17 10:11:39 -05:00
jeremystretch
3a4fee4e6e Changelog for #11226, #11335, #11473, #11592 2023-02-16 20:15:48 -05:00
Aron Bergur Jóhannsson
2db181ea49 Closes #11592: Expose FILE_UPLOAD_MAX_MEMORY_SIZE as a setting (#11742)
* Closes #11592: Expose FILE_UPLOAD_MAX_MEMOMORY_SIZE as a setting

* change configuration settings to alphabetic order

* Small example and documentation

---------

Co-authored-by: aron bergur jóhannsson <aronnemi@gmail.com>
2023-02-16 11:26:22 -05:00
aron bergur jóhannsson
eee1a0e10a change empty list to qs.none() 2023-02-16 11:06:57 -05:00
aron bergur jóhannsson
9594049804 Fixes #11473 graphql invalid tag filter returns all devices/interfaces 2023-02-16 11:06:57 -05:00
kkthxbye-code
c78022a74c Change the way we invalidate the module cache to support reloading code from subpackages 2023-02-16 10:50:38 -05:00
Jeremy Stretch
3150c1f8b3 Changelog for #11459, #11711 2023-02-13 17:58:41 -05:00
Jeremy Stretch
9f91b89467 #11711: Use CSVModelChoiceField for custom object fields during CSV import 2023-02-13 17:53:01 -05:00
kkthxbye
d748851027 Fixes #11711 - Use CSVModelMultipleChoiceField when importing custom multiple object fields (#11712)
* Fixes #11711 - Use CSVModelMultipleChoiceField when importing custom multiple object fields

* Fix pep8

---------

Co-authored-by: kkthxbye-code <>
2023-02-13 17:49:08 -05:00
kkthxbye
df499ea8ac Fixes #11459 - Allow using null in conditions (#11722)
* Fixes #11459 - Allow using null in conditions
- Update docs to reflect this
- Change docs example from primary_ip to primary_ip4 as computed properties are not serialized when queuing webhooks

* Update netbox/extras/conditions.py

---------

Co-authored-by: Simon Toft <SITO@telenor.dk>
Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2023-02-13 17:44:35 -05:00
jeremystretch
b5da383a17 Changelog for #11032, #11582, #11601 2023-02-08 14:56:14 -05:00
kkthxbye-code
f9237285fd Fixes #11601 - Add partial lookup to IPRangeFilterSet 2023-02-08 14:50:22 -05:00
kkthxbye-code
3c970c331c Fixes #11582: Fix missing VC form errors
### Fixes: #11582

Not sure if this is the correct fix or not. The reason that the custom field errors were not shown is that messages.html only shows non_field_errors if the form passed to the context is named form. This is probably an issue in more places, but not sure how to make it generic. A change to messages.html would also need to support formsets.

Any input appreciated @jeremystretch or @arthanson
2023-02-08 14:40:46 -05:00
kkthxbye
91705aa9fd Fixes #11032 - Replication fields broken in custom validation (#11698)
* Fixes #11032 - Replication fields broken in custom validation

* Use getattr instead of hasattr to make sure custom validation is triggered as normal

---------

Co-authored-by: kkthxbye-code <>
2023-02-08 14:36:20 -05:00
jeremystretch
56c7a238a4 Fixes #11683: Fix CSV header attribute detection when auto-detecting import format 2023-02-07 17:24:26 -05:00
jeremystretch
3f28d6aef3 Add step for creating search index 2023-02-07 16:55:50 -05:00
jeremystretch
edbd597bf2 Update housekeeping command docs 2023-02-07 16:52:54 -05:00
jeremystretch
5e1bb20f32 Display login message as success 2023-02-07 16:49:07 -05:00
jeremystretch
7ebfa4c1d1 PRVB 2023-02-02 15:41:24 -05:00
Jeremy Stretch
65417dbf9e Merge pull request #11655 from netbox-community/develop
Release v3.4.4
2023-02-02 15:39:38 -05:00
jeremystretch
37d0135cab Release v3.4.4 2023-02-02 15:24:54 -05:00
Maximilian Wilhelm
699edd049c Closes #11152: Add support to abort custom script gracefully (#11621)
Signed-off-by: Maximilian Wilhelm <max@sdn.clinic>
2023-02-02 15:22:55 -05:00
jeremystretch
95b2acb603 Fixes #11650: Display error message when attempting to create device component with duplicate name 2023-02-02 14:59:16 -05:00
jeremystretch
98a2f3e497 Refresh the README 2023-02-02 14:18:32 -05:00
Abhimanyu Saharan
fb2771370c handled scripts error when only interval is used 2023-02-02 10:25:19 -05:00
jeremystretch
a137cd6cbe Fixes #11635: Pre-populate assigned VRF when following "first available IP" link from prefix view 2023-02-01 12:28:54 -05:00
Arthur
10e27cfa00 11620 fix interface poe type filter 2023-02-01 10:24:20 -05:00
jeremystretch
46ede62f3f Fix rendering of example code 2023-01-30 10:25:20 -05:00
jeremystretch
e7ad6eeb74 Fixes #11613: Correct plugin import logic fix from #11267 2023-01-27 19:56:12 -05:00
jeremystretch
892fd95b5f Update NetBox Cloud link 2023-01-27 16:46:49 -05:00
jeremystretch
0da518e83d Changelog for #11267 2023-01-27 16:45:20 -05:00
Jeremy Stretch
fbc9fea0a5 Fixes #11267: Avoid catching ImportError exceptions when loading plugins (#11566)
* Avoid catching ImportErrors when loading plugin URLs

* Avoid catching ImportErrors when loading plugin resources
2023-01-27 16:44:10 -05:00
jeremystretch
ccc108a217 Closes #11598: Add buttons to easily switch between rack list and elevations views 2023-01-26 10:53:59 -05:00
jeremystretch
22a9df82e6 Closes #11554: Add module types count to manufacturers list 2023-01-26 08:46:25 -05:00
jeremystretch
9cb75e9834 Closes #11585: Add IP address filters for services 2023-01-25 21:25:25 -05:00
jeremystretch
55b1549895 Closes #10762: Permit selection custom fields to have only one choice 2023-01-25 10:27:05 -05:00
jeremystretch
6f74c5ec03 Fixes #11528: Show edit/delete buttons in user tokens table 2023-01-25 10:09:37 -05:00
jeremystretch
b8de9c0875 Fixes #11528: Permit import of devices using uploaded file 2023-01-25 09:55:45 -05:00
jeremystretch
d5ccda355f Fixes #11562: Correct ordering of virtual chassis interfaces with duplicate names 2023-01-24 15:44:02 -05:00
jeremystretch
b79a2976f7 Closes #10888, #10889: Add supplementary notes to installation docs 2023-01-24 14:40:09 -05:00
jeremystretch
39087d10eb Add NetBox Labs as a sponsor 2023-01-23 10:44:42 -05:00
jeremystretch
6a793087b4 Reference GitHub advisory reporting 2023-01-23 10:23:49 -05:00
jeremystretch
0f9a303963 Changelog for #11487 2023-01-23 10:21:11 -05:00
Arthur Hanson
eca624b13d 11487 remove set null from read-only custom fields bulk edit (#11552)
* 11487 remove set null from read-only custom fields bulk edit

* 11487 removes unreleased sentry-sdk
2023-01-23 08:48:14 -05:00
jeremystretch
a4d8169df8 Changelog for #11537 2023-01-20 16:48:22 -05:00
jeremystretch
5f7e310305 Fixes #11555: Avoid inadvertent interpretation of search query as regular expression under global search 2023-01-20 16:47:19 -05:00
jeremystretch
d5e6829eff PRVB 2023-01-20 14:21:03 -05:00
Jeremy Stretch
504800a7db Merge pull request #11551 from netbox-community/develop
Release v3.4.3
2023-01-20 14:19:15 -05:00
jeremystretch
97723b1f96 Correct pinned sentry-sdk version 2023-01-20 13:53:28 -05:00
jeremystretch
5911041777 #11516: Tweak fix to ensure proper highlighting 2023-01-20 13:43:47 -05:00
jeremystretch
fcd0481b09 Release v3.4.3 2023-01-20 13:10:21 -05:00
jeremystretch
cc350165dd Fixes #11544: Catch ValidationError exception when filtering by invalid MAC address 2023-01-20 12:06:34 -05:00
Arthur
db7e1b8a97 11537 remove connection from power feed table 2023-01-20 11:52:56 -05:00
jeremystretch
188f773081 Changelog for #11118, #11227, #11228 2023-01-20 10:24:57 -05:00
reishoku
6271f81cff Add 800GbE interface support: QSFP-DD OSFP (#11429)
Signed-off-by: KOSHIKAWA Kenichi <reishoku.misc@pm.me>

Signed-off-by: KOSHIKAWA Kenichi <reishoku.misc@pm.me>
2023-01-20 10:09:53 -05:00
jeremystretch
4bfc3bf412 #11118: Extend L2VPN filters to device & VM interfaces 2023-01-20 09:58:58 -05:00
Abhimanyu Saharan
d5a92104d1 added l2vpn_termination on vlan filterset (#11501)
* added l2vpn_termination on vlan filterset

* added l2vpn to vlan filterset
2023-01-20 09:34:41 -05:00
Abhimanyu Saharan
ddd4f805a5 added device and vm tab on device role (#11500)
* added vm tab on device role

* added blank lines

* updated templates

* fixed lint issues
2023-01-20 09:30:18 -05:00
Jeremy Stretch
a1c1b19482 Changelog for #11433, #11516 2023-01-17 21:22:02 -05:00
Abhimanyu Saharan
426bc15065 fixed AttributeError: object of class Schema has no attribute fields 2023-01-17 21:12:06 -05:00
kkthxbye-code
df5febf6e7 Add re.escape to highlight_string 2023-01-17 20:42:17 -05:00
jeremystretch
9e09e46700 Fixes #11522: Correct tag links under contact & tenant list views 2023-01-17 10:32:22 -05:00
jeremystretch
ba0e9bb1d2 Changelog for #11488, #11497 2023-01-17 10:27:53 -05:00
jeremystretch
19da92b510 #11488: Additional cleanup 2023-01-17 10:26:34 -05:00
Abhimanyu Saharan
beb1f4e172 added missing description field on the api serializers 2023-01-17 10:20:34 -05:00
kkthxbye-code
fb3d1ef399 Check for the extras.run_script permission when running scripts via. the API 2023-01-17 10:13:18 -05:00
jeremystretch
d7c37d9dd6 Fixes #11483: Apply configured formatting to custom date fields 2023-01-13 08:23:57 -05:00
jeremystretch
24de404fbc Changelog for #11416 2023-01-12 09:37:52 -05:00
jeremystretch
8565d175f9 Fixes #11467: Correct count on interfaces tab when viewing a VC master device 2023-01-12 09:05:55 -05:00
Arthur
8d9e151030 11416 fix circuit termination deletion 2023-01-11 16:09:39 -05:00
jeremystretch
758c5347fb Fixes #10201: Fix AssertionError exception when removing some terminations from an existing cable 2023-01-11 14:42:25 -05:00
Jonathan Senecal
1e54eee631 Update ipaddress.md
Missing `ipam` before `IPAddress.status`
2023-01-11 09:45:28 -05:00
jeremystretch
448760a2fe Add items to contributing guide 2023-01-10 15:47:33 -05:00
jeremystretch
e44b22f7d1 Refresh contributing guide 2023-01-10 08:41:06 -05:00
jeremystretch
30379c3f52 Changelog for #11438, #11444 2023-01-09 10:58:23 -05:00
jeremystretch
8729d60c1c Fixes #11402: Avoid LookupError exception when running scripts with commit disabled 2023-01-09 10:57:13 -05:00
kkthxbye
effcdb8723 Snapshot DeviceBay before populating/depopulating. 2023-01-09 08:39:54 -05:00
kkthxbye
1354947434 Get the queue from QUEUE_MAPPINGS when deleting JobResults 2023-01-09 08:22:40 -05:00
jeremystretch
864ce0088e Changelog for #10486, #11210, #11340, #11379 2023-01-06 16:25:41 -05:00
jeremystretch
93ac0b77c9 Fixes #11379: Fix TypeError exception when bulk editing custom date fields 2023-01-06 16:23:38 -05:00
Mario
ea327e6b37 Closes #10486: Add buttons to edit cables (#11414)
* Added buttons to edit cables

* Revert change that did not address this branch

* Consolidated buttons

* moved back trace button / added permission checks

* reverted disabled trace button
2023-01-06 15:11:29 -05:00
kkthxbye-code
f7b85ab941 Return no terminations if the cable is unsaved 2023-01-06 14:57:07 -05:00
Arthur Hanson
ce9933da85 11340 cable termination setter (#11341)
* 11340 update _terminations_modified only if modified

* 11340 update _terminations_modified only if modified
2023-01-06 10:15:43 -05:00
Robin Schneider
0662f0dab4 Add summary release notes for v3.4 2023-01-06 10:07:21 -05:00
jeremystretch
0669fda1fd Fixes #11384: Correct current time display on script & report forms 2023-01-06 09:42:13 -05:00
jeremystretch
b88fcb6625 Closes #11406: Correct example JSON 2023-01-05 16:38:29 -05:00
jeremystretch
69be24cd5f Changelog for #9996, #11150, #11245, #11371, #11403 2023-01-05 16:29:17 -05:00
Renato Almeida de Oliveira
35273cc87f Add ExportTemplatesMixin to JournalEntry model (#11251)
* Add ExportTemplatesMixin to JournalEntry model

* Move mixin ahead of base class

Co-authored-by: Jeremy Stretch <jstretch@ns1.com>
2023-01-05 16:26:48 -05:00
Daniel W. Anner
5af73e9bf7 #11371 - Add various 100Mb Interface Types (#11377)
* Added 100base-fx (aka fast ethernet over fiber optic)

* Added 100BASE-T1 (single pair fast ethernet) as well as 100BASE‑LFX (fast ethernet over fiber, non standard)

* Update choices.py

Updated the placing of the 100base-fx and lfx choices

* Update netbox/dcim/choices.py

Co-authored-by: Jeremy Stretch <jstretch@ns1.com>
2023-01-05 16:26:26 -05:00
Patrick Kerwood
128ccb4330 feat: added setting redis certificate authority path 2023-01-05 16:15:26 -05:00
Robin Schneider
07df622b59 NetBox should always be referred to as NetBox [DATALAD RUNCMD]
=== Do not change lines below ===
{
 "chain": [],
 "cmd": "git ls-files -z . | xargs --null -I '()' find './()' -type f -not -name 'style-guide.md' -print0 | xargs --null sed --in-place --regexp-extended 's/\\bNetbox\\b/NetBox/g;'",
 "exit": 0,
 "extra_inputs": [],
 "inputs": [],
 "outputs": [],
 "pwd": "."
}
^^^ Do not change lines above ^^^
2023-01-05 16:06:00 -05:00
Michaël Arnauts
5d22260589 #11150: Add a filter for device.primary_ip4 and primary_ip6 (#11382)
* Closes #11150: Add a filter for device.primary_ip4 and primary_ip6

* Tweaked tests to query for multiple IDs

Co-authored-by: jeremystretch <jstretch@ns1.com>
2023-01-05 14:25:15 -05:00
kkthxbye-code
39985ebdd1 Fix exception when scheduling a job in the past 2023-01-05 13:52:07 -05:00
jeremystretch
92ec06c694 PRVB 2023-01-03 16:30:17 -05:00
199 changed files with 2191 additions and 1052 deletions

View File

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

View File

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

View File

@@ -24,7 +24,7 @@ jobs:
necessary.
close-pr-message: >
This PR has been automatically closed due to lack of activity.
days-before-stale: 60
days-before-stale: 90
days-before-close: 30
exempt-issue-labels: 'status: accepted,status: blocked,status: needs milestone'
operations-per-run: 100

View File

@@ -1,188 +1,115 @@
## Getting Help
**Looking for help?** NetBox has a vast, active community of fellow users that may be able to provide assistance. Just [start a discussion](https://github.com/netbox-community/netbox/discussions/new) right here on GitHub! Or if you'd prefer to chat, join us live in the `#netbox` channel on the [NetDev Community Slack](https://netdev.chat/)!
If you encounter any issues installing or using NetBox, try one of the
following resources to get assistance. Please **do not** open a GitHub issue
except to report bugs or request features.
<div align="center">
<h3>
:bug: <a href="#bug-reporting-bugs">Report a bug</a> &middot;
:bulb: <a href="#bulb-feature-requests">Suggest a feature</a> &middot;
:arrow_heading_up: <a href="#arrow_heading_up-submitting-pull-requests">Submit a pull request</a>
</h3>
<h3>
:jigsaw: <a href="#jigsaw-creating-plugins">Create a plugin</a> &middot;
:rescue_worker_helmet: <a href="#rescue_worker_helmet-become-a-maintainer">Become a maintainer</a> &middot;
:heart: <a href="#heart-other-ways-to-contribute">Other ideas</a>
</h3>
</div>
<h3></h3>
### GitHub Discussions
Some general tips for engaging here on GitHub:
GitHub's discussions are the best place to get help or propose rough ideas for
new functionality. Their integration with GitHub allows for easily cross-
referencing and converting posts to issues as needed. There are several
categories for discussions:
* Register for a free [GitHub account](https://github.com/signup) if you haven't already.
* You can use [GitHub Markdown](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax) for formatting text and adding images.
* To help mitigate notification spam, please avoid "bumping" issues with no activity. (To vote an issue up or down, use a :thumbsup: or :thumbsdown: reaction.)
* Please avoid pinging members with `@` unless they've previously expressed interest or involvement with that particular issue.
* **General** - General community discussion
* **Ideas** - Ideas for new functionality that isn't yet ready for a formal
feature request
* **Q&A** - Request help with installing or using NetBox
## :bug: Reporting Bugs
### Slack
* First, ensure that you're running the [latest stable version](https://github.com/netbox-community/netbox/releases) of NetBox. If you're running an older version, it's likely that the bug has already been fixed.
For real-time chat, you can join the **#netbox** Slack channel on [NetDev Community](https://netdev.chat/).
Unfortunately, the Slack channel does not provide long-term retention of chat
history, so try to avoid it for any discussions would benefit from being
preserved for future reference.
* Next, search our [issues list](https://github.com/netbox-community/netbox/issues?q=is%3Aissue) to see if the bug you've found has already been reported. If you come across a bug report that seems to match, please click "add a reaction" in the top right corner of the issue and add a thumbs up (:thumbsup:). This will help draw more attention to it. Any comments you can add to provide additional information or context would also be much appreciated.
## Reporting Bugs
* If you can't find any existing issues (open or closed) that seem to match yours, you're welcome to [submit a new bug report](https://github.com/netbox-community/netbox/issues/new?label=type%3A+bug&template=bug_report.yaml). Be sure to complete the entire report template, including detailed steps that someone triaging your issue can follow to confirm the reported behavior. (If we're not able to replicate the bug based on the information provided, we'll ask for additional detail.)
* First, ensure that you're running the [latest stable version](https://github.com/netbox-community/netbox/releases)
of NetBox. If you're running an older version, it's possible that the bug has
already been fixed.
* Next, check the GitHub [issues list](https://github.com/netbox-community/netbox/issues)
to see if the bug you've found has already been reported. If you think you may
be experiencing a reported issue that hasn't already been resolved, please
click "add a reaction" in the top right corner of the issue and add a thumbs
up (+1). You might also want to add a comment describing how it's affecting your
installation. This will allow us to prioritize bugs based on how many users are
affected.
* When submitting an issue, please be as descriptive as possible. Be sure to
provide all information request in the issue template, including:
* The environment in which NetBox is running
* The exact steps that can be taken to reproduce the issue
* Expected and observed behavior
* Any error messages generated
* Screenshots (if applicable)
* Please avoid prepending any sort of tag (e.g. "[Bug]") to the issue title.
The issue will be reviewed by a maintainer after submission and the appropriate
labels will be applied for categorization.
* Keep in mind that we prioritize bugs based on their severity and how much
work is required to resolve them. It may take some time for someone to address
your issue.
* Some other tips to keep in mind:
* Error messages and screenshots are especially helpful.
* Don't prepend your issue title with a label like `[Bug]`; the proper label will be assigned automatically.
* Ensure that your reproduction instructions don't reference data in our [demo instance](https://demo.netbox.dev/), which gets rebuilt nightly.
* Verify that you have GitHub notifications enabled and are subscribed to your issue after submitting.
* We appreciate your patience as bugs are prioritized by their severity, impact, and difficulty to resolve.
* For more information on how bug reports are handled, please see our [issue
intake policy](https://github.com/netbox-community/netbox/wiki/Issue-Intake-Policy).
## Feature Requests
## :bulb: Feature Requests
* First, check the GitHub [issues list](https://github.com/netbox-community/netbox/issues)
to see if the feature you're requesting is already listed. (Be sure to search
closed issues as well, since some feature requests have been rejected.) If the
feature you'd like to see has already been requested and is open, click "add a
reaction" in the top right corner of the issue and add a thumbs up (+1). This
ensures that the issue has a better chance of receiving attention. Also feel
free to add a comment with any additional justification for the feature.
(However, note that comments with no substance other than a "+1" will be
deleted. Please use GitHub's reactions feature to indicate your support.)
* First, check the GitHub [issues list](https://github.com/netbox-community/netbox/issues?q=is%3Aissue) to see if the feature you have in mind has already been proposed. If you happen to find an open feature request that matches your idea, click "add a reaction" in the top right corner of the issue and add a thumbs up (:thumbsup:). This ensures that the issue has a better chance of receiving attention. Also feel free to add a comment with any additional justification for the feature.
* Before filing a new feature request, consider raising your idea in a
[GitHub discussion](https://github.com/netbox-community/netbox/discussions)
first. Feedback you receive there will help validate and shape the proposed
feature before filing a formal issue.
* If you have a rough idea that's not quite ready for formal submission yet, start a [GitHub discussion](https://github.com/netbox-community/netbox/discussions) instead. This is a great way to test the viability and narrow down the scope of a new feature prior to submitting a formal proposal, and can serve to generate interest in your idea from other community members.
* Good feature requests are very narrowly defined. Be sure to thoroughly
describe the functionality and data model(s) being proposed. The more effort
you put into writing a feature request, the better its chance is of being
implemented. Overly broad feature requests will be closed.
* Once you're ready, submit a feature request [using this template](https://github.com/netbox-community/netbox/issues/new?label=type%3A+feature&template=feature_request.yaml). Be sure to provide sufficient context and detail to convey exactly what you're proposing and why. The stronger your use case, the better chance your proposal has of being accepted.
* When submitting a feature request on GitHub, be sure to include all
information requested by the issue template, including:
* Some other tips to keep in mind:
* Don't prepend your issue title with a label like `[Feature]`; the proper label will be assigned automatically.
* Try to anticipate any likely questions about your proposal and provide that information proactively.
* Verify that you have GitHub notifications enabled and are subscribed to your issue after submitting.
* You're welcome to volunteer to implement your FR, but don't submit a pull request until it has been approved.
* A detailed description of the proposed functionality
* A use case for the feature; who would use it and what value it would add
to NetBox
* A rough description of changes necessary to the database schema (if
applicable)
* Any third-party libraries or other resources which would be involved
* For more information on how feature requests are handled, please see our [issue intake policy](https://github.com/netbox-community/netbox/wiki/Issue-Intake-Policy).
* Please avoid prepending any sort of tag (e.g. "[Feature]") to the issue
title. The issue will be reviewed by a moderator after submission and the
appropriate labels will be applied for categorization.
## :arrow_heading_up: Submitting Pull Requests
* For more information on how feature requests are handled, please see our
[issue intake policy](https://github.com/netbox-community/netbox/wiki/Issue-Intake-Policy).
* [Pull requests](https://docs.github.com/en/pull-requests) (a feature of GitHub) are used to propose changes to NetBox's code base. Our process generally goes like this:
* A user opens a new issue (bug report or feature request)
* A maintainer triages the issue and may mark it as needing an owner
* The issue's author can volunteer to own it, or someone else can
* A maintainer assigns the issue to whomever volunteers
* The issue owner submits a pull request that will resolve the issue
* A maintainer reviews and merges the pull request, closing the issue
## Submitting Pull Requests
* It's very important that you not submit a pull request until a relevant issue has been opened **and** assigned to you. Otherwise, you risk wasting time on work that may ultimately not be needed.
* If you're interested in contributing to NetBox, be sure to check out our
[getting started](https://docs.netbox.dev/en/stable/development/getting-started/)
documentation for tips on setting up your development environment.
* New pull requests should generally be based off of the `develop` branch, rather than `master`. The `develop` branch is used for ongoing development, while `master` is used for tracking stable releases. (If you're developing for an upcoming minor release, use `feature` instead.)
* 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.
* 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 resulting from multiple PRs being submitted simultaneously.)
* 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. 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 **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. (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
resulting from multiple PRs being submitted simultaneously.)
* All code submissions should meet the following criteria (CI will enforce
these checks):
* Python syntax is valid
* All tests pass when run with `./manage.py test`
* PEP 8 compliance is enforced, with the exception that lines may be
* All code submissions should meet the following criteria (CI will enforce these checks):
* Python syntax is valid
* All tests pass when run with `./manage.py test`
* PEP 8 compliance is enforced, with the exception that lines may be
greater than 80 characters in length
## Commenting
* Some other tips to keep in mind:
* If you'd like to volunteer for someone else's issue, please post a comment on that issue letting us know. (This will allow the maintainers to assign it to you.)
* Check out our [developer docs](https://docs.netbox.dev/en/stable/development/getting-started/) for tips on setting up your development environment.
* All new functionality must include relevant tests where applicable.
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 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.
## :jigsaw: Creating Plugins
## Issue Lifecycle
Do you have an idea for something you'd like to build in NetBox, but might not be right for the core project? NetBox includes a powerful and extensive [plugins framework](https://docs.netbox.dev/en/stable/plugins/) that enables users to develop their own custom data models and integrations.
New issues are handled according to our [issue intake policy](https://github.com/netbox-community/netbox/wiki/Issue-Intake-Policy).
Maintainers will assign label(s) and/or close new issues as the policy
dictates. This helps ensure a productive development environment and avoid
accumulating a large backlog of work.
Check out our [plugin development tutorial](https://github.com/netbox-community/netbox-plugin-tutorial) to get started!
The core maintainers group has chosen to make use of GitHub's [Stale bot](https://github.com/apps/stale)
to aid in issue management.
## :rescue_worker_helmet: Become a Maintainer
* Issues will be marked as stale after 60 days of no activity.
* If the stable label is not removed in the following 30 days, the issue will
be closed automatically.
* Any issue bearing one of the following labels will be exempt from all Stale
bot actions:
* `status: accepted`
* `status: blocked`
* `status: needs milestone`
We're always looking for motivated individuals to join the maintainers team and help drive NetBox's long-term development. Some of our most sought-after skills include:
It is natural that some new issues get more attention than others. The stale
bot helps bring renewed attention to potentially valuable issues that may have
been overlooked. **Do not** comment on a stale issue merely to "bump" it in an
effort to circumvent the bot: This will result in the immediate closure of the
issue, and you may be barred from participating in future discussions.
* Python development with a strong focus on the [Django](https://www.djangoproject.com/) framework
* Expertise working with PostgreSQL databases
* Javascript & TypeScript proficiency
* A knack for web application design (HTML & CSS)
* Familiarity with git and software development best practices
* Excellent attention to detail
* Working experience in the field of network operations & engineering
## Maintainer Guidance
We generally ask that maintainers dedicate around four hours of work to the project each week on average, which includes both hands-on development and project management tasks such as issue triage. Maintainers are also encouraged (but not required) to attend our bi-weekly Zoom call to catch up on recent items.
* Maintainers are expected to contribute at least four hours per week to the
project on average. This can be employer-sponsored or individual time, with
the understanding that all contributions are submitted under the Apache 2.0
license and that your employer may not make claim to any contributions.
Contributions include code work, issue management, and community support. All
development must be in accordance with our [development guidance](https://docs.netbox.dev/en/stable/development/).
Many maintainers petition their employer to grant some of their paid time to work on NetBox. In doing so, your employer becomes eligible to be featured as a [NetBox sponsor](https://github.com/netbox-community/netbox/wiki/Sponsorship).
* Maintainers are expected to attend (where feasible) our biweekly ~30-minute
sync to review agenda items. This meeting provides opportunity to present and
discuss pressing topics. Meetings are held as virtual audio/video conferences.
Interested? You can contact our lead maintainer, Jeremy Stretch, at jeremy@netbox.dev or on the [NetDev Community Slack](https://netdev.chat/). We'd love to have you on the team!
* Maintainers with no substantial recorded activity in a 60-day period will be
removed from the project.
## :heart: Other Ways to Contribute
You don't have to be a developer to contribute to NetBox: There are plenty of other ways you can add value to the community! Below are just a few examples:
* Help answer questions and provide feedback in our [GitHub discussions](https://github.com/netbox-community/netbox/discussions) and on [Slack](https://netdev.chat/).
* Write a blog article or record a YouTube video demonstrating how NetBox is used at your organization.
* Help grow our [library of device & module type definitions](https://github.com/netbox-community/devicetype-library).

129
README.md
View File

@@ -1,107 +1,71 @@
<div align="center">
<img src="https://raw.githubusercontent.com/netbox-community/netbox/develop/docs/netbox_logo.svg" width="400" alt="NetBox logo" />
The premiere source of truth powering network automation
</div>
![Master branch build status](https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=master)
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.
Available as open source software under the Apache 2.0 license, NetBox serves
as the cornerstone for network automation in thousands of organizations.
![Master branch build status](https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=master)
* **Physical infrastructure:** Accurately model the physical world, from global regions down to individual racks of gear. Then connect everything - network, console, and power!
* **Modern IPAM:** All the standard IPAM functionality you expect, plus VRF import/export tracking, VLAN management, and overlay support.
* **Data circuits:** Confidently manage the delivery of critical circuits from various service providers, modeled seamlessly alongside your own infrastructure.
* **Power tracking:** Map the distribution of power from upstream sources to individual feeds and outlets.
* **Organization:** Manage tenant and contact assignments natively.
* **Powerful search:** Easily find anything you need using a single global search function.
* **Comprehensive logging:** Leverage both automatic change logging and user-submitted journal entries to track your network's growth over time.
* **Endless customization:** Custom fields, custom links, tags, export templates, custom validation, reports, scripts, and more!
* **Flexible permissions:** An advanced permissions systems enables very flexible delegation of permissions.
* **Integrations:** Easily connect NetBox to your other tooling via its REST & GraphQL APIs.
* **Plugins:** Not finding what you need in the core application? Try one of many community plugins - or build your own!
[![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)
![Screenshot of NetBox UI](docs/media/screenshots/netbox-ui.png "NetBox UI")
## About NetBox
## Getting Started
![Screenshot of Netbox UI](docs/media/screenshots/netbox-ui.png "NetBox UI")
* Just want to explore? Check out [our public demo](https://demo.netbox.dev/) right now!
* The [official documentation](https://docs.netbox.dev) offers a comprehensive introduction.
* Choose your deployment: [self-hosted](https://github.com/netbox-community/netbox), [Docker](https://github.com/netbox-community/netbox-docker), or [NetBox Cloud](https://netboxlabs.com/netbox-cloud/).
* Check out [our wiki](https://github.com/netbox-community/netbox/wiki/Community-Contributions) for even more projects to get the most out of NetBox!
Myriad infrastructure components can be modeled in NetBox, including:
## Get Involved
* Hierarchical regions, site groups, sites, and locations
* Racks, devices, and device components
* Cables and wireless connections
* Power distribution
* Data circuits and providers
* 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
* Organizational tenants and contacts
* Follow [@NetBoxOfficial](https://twitter.com/NetBoxOfficial) on Twitter!
* Join the conversation on [the discussion forum](https://github.com/netbox-community/netbox/discussions) and [Slack](https://netdev.chat/)!
* Already a power user? You can [suggest a feature](https://github.com/netbox-community/netbox/issues/new?assignees=&labels=type%3A+feature&template=feature_request.yaml) or [report a bug](https://github.com/netbox-community/netbox/issues/new?assignees=&labels=type%3A+bug&template=bug_report.yaml) on GitHub.
* Contributions from the community are encouraged and appreciated! Check out our [contributing guide](CONTRIBUTING.md) to get started.
In addition to its extensive built-in models and functionality, NetBox can be
customized and extended through the use of:
* Custom fields
* Custom links
* Configuration contexts
* Custom model validation rules
* Reports
* Custom scripts
* Export templates
* Conditional webhooks
* Plugins
* Single sign-on (SSO) authentication
* NAPALM integration
* Detailed change logging
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).
## Project Stats
<div align="center">
<h3>Thank you to our sponsors!</h3>
<a href="https://github.com/netbox-community/netbox/commits"><img src="https://images.repography.com/29023055/netbox-community/netbox/recent-activity/31db894eee74b8a5475e3af307a81b6c_timeline.svg" alt="Timeline graph"></a>
<a href="https://github.com/netbox-community/netbox/issues"><img src="https://images.repography.com/29023055/netbox-community/netbox/recent-activity/31db894eee74b8a5475e3af307a81b6c_issues.svg" alt="Issues graph"></a>
<a href="https://github.com/netbox-community/netbox/pulls"><img src="https://images.repography.com/29023055/netbox-community/netbox/recent-activity/31db894eee74b8a5475e3af307a81b6c_prs.svg" alt="Pull requests graph"></a>
<a href="https://github.com/netbox-community/netbox/graphs/contributors"><img src="https://images.repography.com/29023055/netbox-community/netbox/recent-activity/31db894eee74b8a5475e3af307a81b6c_users.svg" alt="Top contributors"></a>
<br />Stats via <a href="https://repography.com">Repography</a>
</div>
## Sponsors
<div align="center">
[![NetBox Labs](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/netbox_labs.png)](https://netboxlabs.com)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
[![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;
[![Equinix Metal](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/equinix.png)](https://metal.equinix.com/)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
[![NS1](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/ns1.png)](https://ns1.com/)
<br />
[![Sentry](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/sentry.png)](https://sentry.io/)
[![Sentry](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/sentry.png)](https://sentry.io)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
[![Stellar Technologies](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/stellar.png)](https://stellar.tech/)
[![Equinix Metal](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/equinix.png)](https://metal.equinix.com)
</div>
### Discussion
* [GitHub Discussions](https://github.com/netbox-community/netbox/discussions) - Discussion forum hosted by GitHub; ideal for Q&A and other structured discussions
* [Slack](https://netdev.chat/) - Real-time chat hosted by the NetDev Community; best for unstructured discussion or just hanging out
### Installation
Please see [the documentation](https://docs.netbox.dev/) for
instructions on installing NetBox. To upgrade NetBox, please download the
[latest release](https://github.com/netbox-community/netbox/releases) and
run `upgrade.sh`.
### Providing Feedback
The best platform for general feedback, assistance, and other discussion is our
[GitHub discussions](https://github.com/netbox-community/netbox/discussions).
To report a bug or request a specific feature, please open a GitHub issue using
the [appropriate template](https://github.com/netbox-community/netbox/issues/new/choose).
If you are interested in contributing to the development of NetBox, please read
our [contributing guide](CONTRIBUTING.md) prior to beginning any work.
### Screenshots
## Screenshots
![Screenshot of main page (dark mode)](docs/media/screenshots/home-dark.png "Main page (dark mode)")
@@ -110,8 +74,3 @@ our [contributing guide](CONTRIBUTING.md) prior to beginning any work.
![Screenshot of prefixes hierarchy](docs/media/screenshots/prefixes-list.png "Prefixes hierarchy")
![Screenshot of cable trace](docs/media/screenshots/cable-trace.png "Cable tracing")
### Related projects
Please see [our wiki](https://github.com/netbox-community/netbox/wiki/Community-Contributions)
for a list of relevant community projects.

View File

@@ -24,7 +24,7 @@ If you believe you've uncovered a security vulnerability and wish to report it c
Please note that we **DO NOT** accept reports generated by automated tooling which merely suggest that a file or file(s) _may_ be vulnerable under certain conditions, as these are most often innocuous.
If you believe that you've found a vulnerability which meets all of these conditions, please email a brief description of the suspected bug and instructions for reproduction to **security@netbox.dev**. For any security concerns regarding NetBox deployed via Docker, please see the [netbox-docker](https://github.com/netbox-community/netbox-docker) project.
If you believe that you've found a vulnerability which meets all of these conditions, please [submit a draft security advisory](https://github.com/netbox-community/netbox/security/advisories/new) on GitHub, or email a brief description of the suspected bug and instructions for reproduction to **security@netbox.dev**. For any security concerns regarding NetBox deployed via Docker, please see the [netbox-docker](https://github.com/netbox-community/netbox-docker) project.
### Bug Bounties

View File

@@ -1,6 +1,6 @@
# HTML sanitizer
# https://github.com/mozilla/bleach
bleach
bleach<6.0
# The Python web framework on which NetBox is built
# https://github.com/django/django
@@ -121,7 +121,8 @@ social-auth-core
# Django app for social-auth-core
# https://github.com/python-social-auth/social-app-django
social-auth-app-django
# See https://github.com/python-social-auth/social-app-django/issues/429
social-auth-app-django==5.0.0
# SVG image rendering (used for rack elevations)
# https://github.com/mozman/svgwrite

View File

@@ -1,3 +1,12 @@
<VirtualHost *:80>
# CHANGE THIS TO YOUR SERVER'S NAME
ServerName netbox.example.com
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^/?(.*) https://%{SERVER_NAME}/$1 [R,L]
</VirtualHost>
<VirtualHost *:443>
ProxyPreserveHost On

View File

@@ -1,4 +1,4 @@
# The IP address (typically localhost) and port that the Netbox WSGI process should listen on
# The IP address (typically localhost) and port that the NetBox WSGI process should listen on
bind = '127.0.0.1:8001'
# Number of gunicorn workers to spawn. This should typically be 2n+1, where

View File

@@ -5,6 +5,7 @@ NetBox includes a `housekeeping` management command that should be run nightly.
* Clearing expired authentication sessions from the database
* Deleting changelog records older than the configured [retention time](../configuration/miscellaneous.md#changelog_retention)
* Deleting job result records older than the configured [retention time](../configuration/miscellaneous.md#jobresult_retention)
* Check for new NetBox releases (if [`RELEASE_CHECK_URL`](../configuration/miscellaneous.md#release_check_url) is set)
This command can be invoked directly, or by using the shell script provided at `/opt/netbox/contrib/netbox-housekeeping.sh`. This script can be linked from your cron scheduler's daily jobs directory (e.g. `/etc/cron.daily`) or referenced directly within the cron configuration file.

View File

@@ -18,4 +18,4 @@ interface.
Default: False
This parameter serves as a safeguard to prevent some potentially dangerous behavior, such as generating new database schema migrations. Set this to `True` **only** if you are actively developing the NetBox code base.
This parameter serves as a safeguard to prevent some potentially dangerous behavior, such as generating new database schema migrations. Additionally, enabling this setting disables the debug warning banner in the UI. Set this to `True` **only** if you are actively developing the NetBox code base.

View File

@@ -69,6 +69,14 @@ By default, NetBox will permit users to create duplicate prefixes and IP address
---
## `FILE_UPLOAD_MAX_MEMORY_SIZE`
Default: `2621440` (2.5 MB).
The maximum amount (in bytes) of uploaded data that will be held in memory before being written to the filesystem. Changing this setting can be useful for example to be able to upload files bigger than 2.5MB to custom scripts for processing.
---
## GRAPHQL_ENABLED
!!! tip "Dynamic Configuration Parameter"

View File

@@ -16,7 +16,7 @@ If true, NetBox will automatically create local accounts for users authenticated
Default: `'netbox.authentication.RemoteUserBackend'`
This is the Python path to the custom [Django authentication backend](https://docs.djangoproject.com/en/stable/topics/auth/customizing/) to use for external user authentication. NetBox provides two built-in backends (listed below), though custom authentication backends may also be provided by other packages or plugins.
This is the Python path to the custom [Django authentication backend](https://docs.djangoproject.com/en/stable/topics/auth/customizing/) to use for external user authentication. NetBox provides two built-in backends (listed below), though custom authentication backends may also be provided by other packages or plugins. Provide a string for a single backend, or an iterable for multiple backends, which will be attempted in the order given.
* `netbox.authentication.RemoteUserBackend`
* `netbox.authentication.LDAPBackend`

View File

@@ -38,7 +38,7 @@ In order to send email, NetBox needs an email server configured. The following i
* `SERVER` - Hostname or IP address of the email server (use `localhost` if running locally)
* `PORT` - TCP port to use for the connection (default: `25`)
* `USERNAME` - Username with which to authenticate
* `PASSSWORD` - Password with which to authenticate
* `PASSWORD` - Password with which to authenticate
* `USE_SSL` - Use SSL when connecting to the server (default: `False`)
* `USE_TLS` - Use TLS when connecting to the server (default: `False`)
* `SSL_CERTFILE` - Path to the PEM-formatted SSL certificate file (optional)

View File

@@ -79,7 +79,22 @@ A human-friendly description of what your script does.
### `field_order`
By default, script variables will be ordered in the form as they are defined in the script. `field_order` may be defined as an iterable of field names to determine the order in which variables are rendered. Any fields not included in this iterable be listed last.
By default, script variables will be ordered in the form as they are defined in the script. `field_order` may be defined as an iterable of field names to determine the order in which variables are rendered within a default "Script Data" group. Any fields not included in this iterable be listed last. If `fieldsets` is defined, `field_order` will be ignored. A fieldset group for "Script Execution Parameters" will be added to the end of the form by default for the user.
### `fieldsets`
`fieldsets` may be defined as an iterable of field groups and their field names to determine the order in which variables are group and rendered. Any fields not included in this iterable will not be displayed in the form. If `fieldsets` is defined, `field_order` will be ignored. A fieldset group for "Script Execution Parameters" will be added to the end of the fieldsets by default for the user.
An example fieldset definition is provided below:
```python
class MyScript(Script):
class Meta:
fieldsets = (
('First group', ('field1', 'field2', 'field3')),
('Second group', ('field4', 'field5')),
)
```
### `commit_default`
@@ -142,6 +157,19 @@ obj.full_clean()
obj.save()
```
## Error handling
Sometimes things go wrong and a script will run into an `Exception`. If that happens and an uncaught exception is raised by the custom script, the execution is aborted and a full stack trace is reported.
Although this is helpful for debugging, in some situations it might be required to cleanly abort the execution of a custom script (e.g. because of invalid input data) and thereby make sure no changes are performed on the database. In this case the script can throw an `AbortScript` exception, which will prevent the stack trace from being reported, but still terminating the script's execution and reporting a given error message.
```python
from utilities.exceptions import AbortScript
if some_error:
raise AbortScript("Some meaningful error message")
```
## Variable Reference
### Default Options
@@ -289,7 +317,7 @@ Optionally `schedule_at` can be passed in the form data with a datetime string t
Scripts can be run on the CLI by invoking the management command:
```
python3 manage.py runscript [--commit] [--loglevel {debug,info,warning,error,critical}] [--data "<data>"] <module>.<script>
python3 manage.py runscript [--commit] [--loglevel {debug,info,warning,error,critical}] [--data "<data>"] <module>.<script>
```
The required ``<module>.<script>`` argument is the script to run where ``<module>`` is the name of the python file in the ``scripts`` directory without the ``.py`` extension and ``<script>`` is the name of the script class in the ``<module>`` to run.

View File

@@ -132,7 +132,7 @@ Once you have created a report, it will appear in the reports list. Initially, r
!!! note
To run a report, a user must be assigned the `extras.run_report` permission. This is achieved by assigning the user (or group) a permission on the Report object and specifying the `run` action in the admin UI as shown below.
![Adding the run action to a permission](/media/admin_ui_run_permission.png)
![Adding the run action to a permission](../media/admin_ui_run_permission.png)
### Via the Web UI

View File

@@ -54,15 +54,19 @@ Each model should have a corresponding FilterSet class defined. This is used to
Create a table class for the model in `tables.py` by subclassing `utilities.tables.BaseTable`. Under the table's `Meta` class, be sure to list both the fields and default columns.
## 9. Create the object template
## 9. Create a SearchIndex subclass
If this model will be included in global search results, create a subclass of `netbox.search.SearchIndex` for it and specify the fields to be indexed.
## 10. Create the object template
Create the HTML template for the object view. (The other views each typically employ a generic template.) This template should extend `generic/object.html`.
## 10. Add the model to the navigation menu
## 11. Add the model to the navigation menu
Add the relevant navigation menu items in `netbox/netbox/navigation/menu.py`.
## 11. REST API components
## 12. REST API components
Create the following for each model:
@@ -71,13 +75,13 @@ Create the following for each model:
* API view in `api/views.py`
* Endpoint route in `api/urls.py`
## 12. GraphQL API components
## 13. GraphQL API components
Create a Graphene object type for the model in `graphql/types.py` by subclassing the appropriate class from `netbox.graphql.types`.
Also extend the schema class defined in `graphql/schema.py` with the individual object and object list fields per the established convention.
## 13. Add tests
## 14. Add tests
Add tests for the following:
@@ -85,7 +89,7 @@ Add tests for the following:
* API views
* Filter sets
## 14. Documentation
## 15. Documentation
Create a new documentation page for the model in `docs/models/<app_label>/<model_name>.md`. Include this file under the "features" documentation where appropriate.

View File

@@ -52,4 +52,4 @@ NetBox is built on the enormously popular [Django](http://www.djangoproject.com/
* Try out our [public demo](https://demo.netbox.dev/) if you want to jump right in
* The [installation guide](./installation/index.md) will help you get your own deployment up and running
* Or try the community [Docker image](https://github.com/netbox-community/netbox-docker) for a low-touch approach
* [NetBox Cloud](https://www.getnetbox.io/) is a hosted solution offered by NS1
* [NetBox Cloud](https://netboxlabs.com/netbox-cloud) is a managed solution offered by [NetBox Labs](https://netboxlabs.com/)

View File

@@ -54,7 +54,7 @@ Within the shell, enter the following commands to create the database and user (
```postgresql
CREATE DATABASE netbox;
CREATE USER netbox WITH PASSWORD 'J5brHrAXFLQSif0K';
GRANT ALL PRIVILEGES ON DATABASE netbox TO netbox;
ALTER DATABASE netbox OWNER TO netbox;
```
!!! danger "Use a strong password"

View File

@@ -272,7 +272,10 @@ See the [housekeeping documentation](../administration/housekeeping.md) for furt
## Test the Application
At this point, we should be able to run NetBox's development server for testing. We can check by starting a development instance:
At this point, we should be able to run NetBox's development server for testing. We can check by starting a development instance locally.
!!! tip
Check that the Python virtual environment is still active before attempting to run the server.
```no-highlight
python3 manage.py runserver 0.0.0.0:8000 --insecure

View File

@@ -14,7 +14,10 @@ While the provided configuration should suffice for most initial installations,
## systemd Setup
We'll use systemd to control both gunicorn and NetBox's background worker process. First, copy `contrib/netbox.service` and `contrib/netbox-rq.service` to the `/etc/systemd/system/` directory and reload the systemd daemon:
We'll use systemd to control both gunicorn and NetBox's background worker process. First, copy `contrib/netbox.service` and `contrib/netbox-rq.service` to the `/etc/systemd/system/` directory and reload the systemd daemon.
!!! warning "Check user & group assignment"
The stock service configuration files packaged with NetBox assume that the service will run with the `netbox` user and group names. If these differ on your installation, be sure to update the service files accordingly.
```no-highlight
sudo cp -v /opt/netbox/contrib/*.service /etc/systemd/system/

View File

@@ -65,7 +65,7 @@ sudo cp /opt/netbox/contrib/apache.conf /etc/apache2/sites-available/netbox.conf
Finally, ensure that the required Apache modules are enabled, enable the `netbox` site, and reload Apache:
```no-highlight
sudo a2enmod ssl proxy proxy_http headers
sudo a2enmod ssl proxy proxy_http headers rewrite
sudo a2ensite netbox
sudo systemctl restart apache2
```

View File

@@ -584,11 +584,16 @@ Additionally, a token can be set to expire at a specific time. This can be usefu
#### Client IP Restriction
!!! note
This feature was introduced in NetBox v3.3.
Each API token can optionally be restricted by client IP address. If one or more allowed IP prefixes/addresses is defined for a token, authentication will fail for any client connecting from an IP address outside the defined range(s). This enables restricting the use a token to a specific client. (By default, any client IP address is permitted.)
#### Creating Tokens for Other Users
It is possible to provision authentication tokens for other users via the REST API. To do, so the requesting user must have the `users.grant_token` permission assigned. While all users have inherent permission to create their own tokens, this permission is required to enable the creation of tokens for other users.
![Adding the grant action to a permission](../media/admin_ui_grant_permission.png)
!!! warning "Exercise Caution"
The ability to create tokens on behalf of other users enables the requestor to access the created token. This ability is intended e.g. for the provisioning of tokens by automated services, and should be used with extreme caution to avoid a security compromise.
### Authenticating to the API

View File

@@ -4,7 +4,7 @@
NetBox was originally developed by its lead maintainer, [Jeremy Stretch](https://github.com/jeremystretch), while he was working as a network engineer at [DigitalOcean](https://www.digitalocean.com/) in 2015 as part of an effort to automate their network provisioning. Recognizing the new tool's potential, DigitalOcean agreed to release it as an open source project in June 2016.
Since then, thousands of organizations around the world have embraced NetBox as their central network source of truth to empower both network operators and automation.
Since then, thousands of organizations around the world have embraced NetBox as their central network source of truth to empower both network operators and automation. Today, the open source project is stewarded by [NetBox Labs](https://netboxlabs.com/) and a team of volunteer maintainers. Beyond the core product, myriad [plugins](https://netbox.dev/plugins/) have been developed by the NetBox community to enhance and expand its feature set.
## Key Features
@@ -17,6 +17,7 @@ NetBox was built specifically to serve the needs of network engineers and operat
* AS number (ASN) management
* Rack elevations with SVG rendering
* Device modeling using pre-defined types
* Virtual chassis and device contexts
* Network, power, and console cabling with SVG traces
* Power distribution modeling
* Data circuit and provider tracking
@@ -29,12 +30,13 @@ NetBox was built specifically to serve the needs of network engineers and operat
* Tenant ownership assignment
* Device & VM configuration contexts for advanced configuration rendering
* Custom fields for data model extension
* Support for custom validation rules
* Custom validation rules
* Custom reports & scripts executable directly within the UI
* Extensive plugin framework for adding custom functionality
* Single sign-on (SSO) authentication
* Robust object-based permissions
* Detailed, automatic change logging
* Global search engine
* NAPALM integration
## What NetBox Is Not

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -40,8 +40,8 @@ is represented in JSON as
```json
{
'tag': ['alpha', 'bravo'],
'status': 'active',
'region_id': 51
"tag": ["alpha", "bravo"],
"status": "active",
"region_id": 51
}
```

View File

@@ -23,7 +23,7 @@ The IPv4 or IPv6 address and mask, in CIDR notation (e.g. `192.0.2.0/24`).
The operational status of the IP address.
!!! tip
Additional statuses may be defined by setting `IPAddress.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
Additional statuses may be defined by setting `ipam.IPAddress.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
### Role

View File

@@ -51,7 +51,7 @@ menu_items = (item1, item2, item3)
Each menu item represents a link and (optionally) a set of buttons comprising one entry in NetBox's navigation menu. Menu items are defined as PluginMenuItem instances. An example is shown below.
```python filename="navigation.py"
```python title="navigation.py"
from extras.plugins import PluginMenuButton, PluginMenuItem
from utilities.choices import ButtonColorChoices

View File

@@ -97,7 +97,7 @@ Multiple conditions can be combined into nested sets using AND or OR logic. This
### Examples
`status` is "active" and `primary_ip` is defined _or_ the "exempt" tag is applied.
`status` is "active" and `primary_ip4` is defined _or_ the "exempt" tag is applied.
```json
{
@@ -109,8 +109,8 @@ Multiple conditions can be combined into nested sets using AND or OR logic. This
"value": "active"
},
{
"attr": "primary_ip",
"value": "",
"attr": "primary_ip4",
"value": null,
"negate": true
}
]

View File

@@ -168,7 +168,7 @@ Some text to show that the reference links can follow later.
## Images
```
Here's the Netbox logo (hover to see the title text):
Here's the NetBox logo (hover to see the title text):
Inline-style:
![alt text](/static/netbox_logo.png "Logo Title Text 1")
@@ -179,7 +179,7 @@ Reference-style:
[logo]: /static/netbox_logo.png "Logo Title Text 2"
```
Here's the Netbox logo (hover to see the title text):
Here's the NetBox logo (hover to see the title text):
Inline-style:
![alt text](/static/netbox_logo.png "Logo Title Text 1")

View File

@@ -10,6 +10,16 @@ Minor releases are published in April, August, and December of each calendar yea
This page contains a history of all major and minor releases since NetBox v2.0. For more detail on a specific patch release, please see the release notes page for that specific minor release.
#### [Version 3.4](./version-3.4.md) (December 2022)
* New Global Search ([#10560](https://github.com/netbox-community/netbox/issues/10560))
* Virtual Device Contexts ([#7854](https://github.com/netbox-community/netbox/issues/7854))
* Saved Filters ([#9623](https://github.com/netbox-community/netbox/issues/9623))
* JSON/YAML Bulk Imports ([#4347](https://github.com/netbox-community/netbox/issues/4347))
* Update Existing Objects via Bulk Import ([#7961](https://github.com/netbox-community/netbox/issues/7961))
* Scheduled Reports & Scripts ([#8366](https://github.com/netbox-community/netbox/issues/8366))
* API for Staged Changes ([#10851](https://github.com/netbox-community/netbox/issues/10851))
#### [Version 3.3](./version-3.3.md) (August 2022)
* Multi-object Cable Terminations ([#9102](https://github.com/netbox-community/netbox/issues/9102))

View File

@@ -1,5 +1,177 @@
# NetBox v3.4
## v3.4.8 (2023-04-12)
### Enhancements
* [#10414](https://github.com/netbox-community/netbox/issues/10414) - Enable general purpose image attachments for device types
* [#10600](https://github.com/netbox-community/netbox/issues/10600) - Allow custom object fields to reference a user or group
* [#11015](https://github.com/netbox-community/netbox/issues/11015) - Remove unit from commit rate column header in circuits table
* [#11431](https://github.com/netbox-community/netbox/issues/11431) - Disallow changing custom field type after creation
* [#11453](https://github.com/netbox-community/netbox/issues/11453) - Display a warning banner when `DEBUG` is enabled
* [#12007](https://github.com/netbox-community/netbox/issues/12007) - Enable filtering of VM Interfaces by assigned VLAN
* [#12095](https://github.com/netbox-community/netbox/issues/12095) - Specify UTF-8 encoding for default export template MIME type
* [#12207](https://github.com/netbox-community/netbox/issues/12207) - Introduce the `grant_token` permission for controlling the creation of API tokens on behalf of other users
### Bug Fixes
* [#10221](https://github.com/netbox-community/netbox/issues/10221) - Validate generic foreign key relations assigned via REST API requests
* [#11432](https://github.com/netbox-community/netbox/issues/11432) - Prevent existing components & component templates from being reassigned to different devices/device types via the REST API
* [#11454](https://github.com/netbox-community/netbox/issues/11454) - Raise validation error if generic foreign key assignment does not specify both object type and ID
* [#11746](https://github.com/netbox-community/netbox/issues/11746) - Fix cleanup of object data when deleting a custom field
* [#12011](https://github.com/netbox-community/netbox/issues/12011) - Fix KeyError exception when attempting to add module bays in bulk
* [#12040](https://github.com/netbox-community/netbox/issues/12040) - Display relevant UI tab upon bulk import validation failure
* [#12074](https://github.com/netbox-community/netbox/issues/12074) - Fix the automatic assignment of racks to devices via the REST API
* [#12084](https://github.com/netbox-community/netbox/issues/12084) - Fix exception when attempting to create a saved filter for applied filters
* [#12087](https://github.com/netbox-community/netbox/issues/12087) - Fix bulk editing of many-to-many relationships
* [#12117](https://github.com/netbox-community/netbox/issues/12117) - Hide clone button for objects with no clonable attributes
* [#12118](https://github.com/netbox-community/netbox/issues/12118) - Fix instantiation of nested inventory item templates when creating a device
* [#12184](https://github.com/netbox-community/netbox/issues/12184) - Fix filtered bulk deletion for various models
* [#12190](https://github.com/netbox-community/netbox/issues/12190) - Fix form layout for plugin textarea fields
* [#12227](https://github.com/netbox-community/netbox/issues/12227) - Fix tenant assignment on bulk import of L2VPNs
---
## v3.4.7 (2023-03-28)
### Enhancements
* [#11645](https://github.com/netbox-community/netbox/issues/11645) - Automatically set the scheduled time when executing reports/scripts at a recurring interval
* [#11833](https://github.com/netbox-community/netbox/issues/11833) - Add fieldset support for custom script forms
* [#11973](https://github.com/netbox-community/netbox/issues/11833) - Use SSID for representing wireless links, if set
* [#11977](https://github.com/netbox-community/netbox/issues/11977) - Support designating multiple backends via `REMOTE_AUTH_BACKEND` config parameter
* [#11990](https://github.com/netbox-community/netbox/issues/11990) - Improve error reporting for duplicate CSV column headings
* [#11991](https://github.com/netbox-community/netbox/issues/11991) - Enable VDC assignment during bulk import/edit of interfaces
### Bug Fixes
* [#11914](https://github.com/netbox-community/netbox/issues/11914) - Include parameters when exporting saved filters
* [#11933](https://github.com/netbox-community/netbox/issues/11933) - Fix cloning of saved filters
* [#11984](https://github.com/netbox-community/netbox/issues/11984) - Remove erroneous 802.3az PoE type
* [#11979](https://github.com/netbox-community/netbox/issues/11979) - Correct URL for tags in route targets list
* [#12008](https://github.com/netbox-community/netbox/issues/12008) - Enable cloning of export templates
* [#12029](https://github.com/netbox-community/netbox/issues/12029) - Restore missing description field on virtual chassis form
* [#12038](https://github.com/netbox-community/netbox/issues/12038) - Correct display of zero values for virtual chassis member priority
* [#12048](https://github.com/netbox-community/netbox/issues/12048) - Enable cloning of tags
* [#12058](https://github.com/netbox-community/netbox/issues/12058) - Enable cloning of config contexts
---
## v3.4.6 (2023-03-13)
### Enhancements
* [#10058](https://github.com/netbox-community/netbox/issues/10058) - Enable searching for devices/VMs by primary IP address
* [#11011](https://github.com/netbox-community/netbox/issues/11011) - Add ability to toggle visibility of virtual interfaces under device view
* [#11294](https://github.com/netbox-community/netbox/issues/11294) - Enable live preview of Markdown content
* [#11807](https://github.com/netbox-community/netbox/issues/11807) - Restore default page size when navigating between views
* [#11817](https://github.com/netbox-community/netbox/issues/11817) - Add `connected_endpoints` field to GraphQL API for cabled objects
* [#11851](https://github.com/netbox-community/netbox/issues/11851) - Include IP version in GraphQL API representations of aggregates, prefixes, and IP addresses
* [#11862](https://github.com/netbox-community/netbox/issues/11862) - Add Cisco StackWise 1T interface type
* [#11871](https://github.com/netbox-community/netbox/issues/11871) - Add IEEE 802.3az PoE type for interfaces
* [#11929](https://github.com/netbox-community/netbox/issues/11929) - Strip whitespace from CSV headers prior to validation
### Bug Fixes
* [#11470](https://github.com/netbox-community/netbox/issues/11470) - Avoid raising exception when filtering IPs by an invalid address
* [#11565](https://github.com/netbox-community/netbox/issues/11565) - Apply custom field defaults to IP address created during FHRP group creation
* [#11631](https://github.com/netbox-community/netbox/issues/11631) - Fix filtering changelog & journal entries by multiple content type IDs
* [#11758](https://github.com/netbox-community/netbox/issues/11758) - Support non-URL-safe characters in plugin menu titles
* [#11796](https://github.com/netbox-community/netbox/issues/11796) - When importing devices, restrict rack by location only if the location field is specified
* [#11819](https://github.com/netbox-community/netbox/issues/11819) - Fix filtering of cable terminations by object type
* [#11850](https://github.com/netbox-community/netbox/issues/11850) - Fix loading of CSV files containing a byte order mark
* [#11903](https://github.com/netbox-community/netbox/issues/11903) - Fix escaping of return URL values for action buttons in tables
* [#11927](https://github.com/netbox-community/netbox/issues/11927) - Correct loading of plugin resources with custom paths
---
## v3.4.5 (2023-02-21)
### Enhancements
* [#11110](https://github.com/netbox-community/netbox/issues/11110) - Add `start_address` and `end_address` filters for IP ranges
* [#11592](https://github.com/netbox-community/netbox/issues/11592) - Introduce `FILE_UPLOAD_MAX_MEMORY_SIZE` configuration parameter
* [#11685](https://github.com/netbox-community/netbox/issues/11685) - Match on containing prefixes and aggregates when querying for IP addresses using global search
* [#11787](https://github.com/netbox-community/netbox/issues/11787) - Upgrade script will automatically rebuild missing search cache
### Bug Fixes
* [#11032](https://github.com/netbox-community/netbox/issues/11032) - Fix false custom validation errors during component creation
* [#11226](https://github.com/netbox-community/netbox/issues/11226) - Ensure scripts and reports within submodules are automatically reloaded
* [#11459](https://github.com/netbox-community/netbox/issues/11459) - Enable evaluating null values in custom validation rules
* [#11473](https://github.com/netbox-community/netbox/issues/11473) - GraphQL requests specifying an invalid filter should return an empty queryset
* [#11582](https://github.com/netbox-community/netbox/issues/11582) - Ensure form validation errors are displayed when adding virtual chassis members
* [#11601](https://github.com/netbox-community/netbox/issues/11601) - Fix partial matching of start/end addresses for IP range search
* [#11683](https://github.com/netbox-community/netbox/issues/11683) - Fix CSV header attribute detection when auto-detecting import format
* [#11711](https://github.com/netbox-community/netbox/issues/11711) - Fix CSV import for multiple-object custom fields
* [#11723](https://github.com/netbox-community/netbox/issues/11723) - Circuit terminations should link to their associated circuits (rather than site or provider network)
* [#11775](https://github.com/netbox-community/netbox/issues/11775) - Skip checking for old search cache records when creating a new object
* [#11786](https://github.com/netbox-community/netbox/issues/11786) - List only applicable object types in form widget when filtering custom fields
---
## v3.4.4 (2023-02-02)
### Enhancements
* [#10762](https://github.com/netbox-community/netbox/issues/10762) - Permit selection custom fields to have only one choice
* [#11152](https://github.com/netbox-community/netbox/issues/11152) - Introduce AbortScript exception to elegantly abort scripts
* [#11554](https://github.com/netbox-community/netbox/issues/11554) - Add module types count to manufacturers list
* [#11585](https://github.com/netbox-community/netbox/issues/11585) - Add IP address filters for services
* [#11598](https://github.com/netbox-community/netbox/issues/11598) - Add buttons to easily switch between rack list and elevations views
### Bug Fixes
* [#11267](https://github.com/netbox-community/netbox/issues/11267) - Avoid catching ImportErrors when loading plugin resources
* [#11487](https://github.com/netbox-community/netbox/issues/11487) - Remove "set null" option from non-writable custom fields during bulk edit
* [#11491](https://github.com/netbox-community/netbox/issues/11491) - Show edit/delete buttons in user tokens table
* [#11528](https://github.com/netbox-community/netbox/issues/11528) - Permit import of devices using uploaded file
* [#11555](https://github.com/netbox-community/netbox/issues/11555) - Avoid inadvertent interpretation of search query as regular expression under global search (previously [#11516](https://github.com/netbox-community/netbox/issues/11516))
* [#11562](https://github.com/netbox-community/netbox/issues/11562) - Correct ordering of virtual chassis interfaces with duplicate names
* [#11574](https://github.com/netbox-community/netbox/issues/11574) - Fix exception when attempting to schedule reports/scripts
* [#11620](https://github.com/netbox-community/netbox/issues/11620) - Correct available filter choices for interface PoE type
* [#11635](https://github.com/netbox-community/netbox/issues/11635) - Pre-populate assigned VRF when following "first available IP" link from prefix view
* [#11650](https://github.com/netbox-community/netbox/issues/11650) - Display error message when attempting to create device component with duplicate name
---
## v3.4.3 (2023-01-20)
### Enhancements
* [#9996](https://github.com/netbox-community/netbox/issues/9996) - Introduce `CA_CERT_PATH` parameter to define SSL CA path for Redis servers
* [#10486](https://github.com/netbox-community/netbox/issues/10486) - Add a cable edit button for connected components in component lists
* [#11118](https://github.com/netbox-community/netbox/issues/11118) - Add L2VPN filters for VLANs and interfaces
* [#11150](https://github.com/netbox-community/netbox/issues/11150) - Add primary IPv4/v6 address filters for devices
* [#11227](https://github.com/netbox-community/netbox/issues/11227) - Add 800GE interface types
* [#11228](https://github.com/netbox-community/netbox/issues/11228) - List both devices & VMs under device role view
* [#11245](https://github.com/netbox-community/netbox/issues/11245) - Enable export templates for journal entries
* [#11371](https://github.com/netbox-community/netbox/issues/11371) - Introduce additional 100M Ethernet interface types
### Bug Fixes
* [#10201](https://github.com/netbox-community/netbox/issues/10201) - Fix AssertionError exception when removing some terminations from an existing cable
* [#11210](https://github.com/netbox-community/netbox/issues/11210) - Fix ValueError exception when attempting to bulk import cables attached to occupied terminations
* [#11340](https://github.com/netbox-community/netbox/issues/11340) - Avoid flagging cable termination changes erroneously
* [#11379](https://github.com/netbox-community/netbox/issues/11379) - Fix TypeError exception when bulk editing custom date fields
* [#11384](https://github.com/netbox-community/netbox/issues/11384) - Correct current time display on script & report forms
* [#11402](https://github.com/netbox-community/netbox/issues/11402) - Avoid LookupError exception when running scripts with commit disabled
* [#11403](https://github.com/netbox-community/netbox/issues/11403) - Fix exception when scheduling a job in the past
* [#11416](https://github.com/netbox-community/netbox/issues/11416) - Avoid AttributeError exception when deleting a cabled circuit termination
* [#11433](https://github.com/netbox-community/netbox/issues/11433) - Avoid AttributeError exception when generating API schema for views with custom schema
* [#11438](https://github.com/netbox-community/netbox/issues/11438) - Fix deletion of scheduled job using non-default queues
* [#11444](https://github.com/netbox-community/netbox/issues/11444) - Adding/removing a device from a device bay should record a pre-change snapshot on the device bay
* [#11467](https://github.com/netbox-community/netbox/issues/11467) - Correct count on interfaces tab when viewing a VC master device
* [#11483](https://github.com/netbox-community/netbox/issues/11483) - Apply configured formatting to custom date fields
* [#11488](https://github.com/netbox-community/netbox/issues/11488) - Add missing `description` fields to several REST API serializers
* [#11497](https://github.com/netbox-community/netbox/issues/11497) - Enforce `run_script` permission when executing scripts via REST API
* ~[#11516](https://github.com/netbox-community/netbox/issues/11516) - Prevent text highlight utility from interpreting match as regex~
* [#11522](https://github.com/netbox-community/netbox/issues/11522) - Correct tag links under contact & tenant list views
* [#11537](https://github.com/netbox-community/netbox/issues/11537) - Remove obsolete "Connection" column from power feeds table
* [#11544](https://github.com/netbox-community/netbox/issues/11544) - Catch ValidationError exception when filtering by invalid MAC address
---
## v3.4.2 (2023-01-03)
### Enhancements

View File

@@ -8,6 +8,9 @@ theme:
custom_dir: docs/_theme/
icon:
repo: fontawesome/brands/github
features:
- content.code.copy
- navigation.footer
palette:
- media: "(prefers-color-scheme: light)"
scheme: default
@@ -20,7 +23,8 @@ theme:
icon: material/lightbulb
name: Switch to Light Mode
plugins:
- search
- search:
lang: en
- mkdocstrings:
handlers:
python:

View File

@@ -77,6 +77,7 @@ class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
model = CircuitTermination
fields = [
'id', 'url', 'display', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
'description',
]

View File

@@ -7,7 +7,7 @@ from ipam.models import ASN
from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant
from utilities.forms import (
add_blank_choice, CommentField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SmallTextarea,
add_blank_choice, CommentField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
StaticSelect,
)
@@ -35,7 +35,6 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label=_('Comments')
)
@@ -63,7 +62,6 @@ class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label=_('Comments')
)
@@ -125,7 +123,6 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label=_('Comments')
)

View File

@@ -196,12 +196,10 @@ class CircuitTermination(
)
def __str__(self):
return f'Termination {self.term_side}: {self.site or self.provider_network}'
return f'{self.circuit}: Termination {self.term_side}'
def get_absolute_url(self):
if self.site:
return self.site.get_absolute_url()
return self.provider_network.get_absolute_url()
return self.circuit.get_absolute_url()
def clean(self):
super().clean()

View File

@@ -57,7 +57,9 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
template_code=CIRCUITTERMINATION_LINK,
verbose_name='Side Z'
)
commit_rate = CommitRateColumn()
commit_rate = CommitRateColumn(
verbose_name='Commit Rate'
)
comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='circuits:circuit_list'

View File

@@ -196,6 +196,7 @@ class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
queryset = CircuitType.objects.annotate(
circuit_count=count_related(Circuit, 'type')
)
filterset = filtersets.CircuitTypeFilterSet
table = tables.CircuitTypeTable

View File

@@ -672,6 +672,22 @@ class DeviceSerializer(NetBoxModelSerializer):
return data
class DeviceWithConfigContextSerializer(DeviceSerializer):
config_context = serializers.SerializerMethodField()
class Meta(DeviceSerializer.Meta):
fields = [
'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'airflow', 'primary_ip',
'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'description',
'comments', 'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
]
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
def get_config_context(self, obj):
return obj.get_config_context()
class VirtualDeviceContextSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
device = NestedDeviceSerializer()
@@ -687,7 +703,8 @@ class VirtualDeviceContextSerializer(NetBoxModelSerializer):
model = VirtualDeviceContext
fields = [
'id', 'url', 'display', 'name', 'device', 'identifier', 'tenant', 'primary_ip', 'primary_ip4',
'primary_ip6', 'status', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'interface_count',
'primary_ip6', 'status', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
'interface_count',
]
@@ -706,22 +723,6 @@ class ModuleSerializer(NetBoxModelSerializer):
]
class DeviceWithConfigContextSerializer(DeviceSerializer):
config_context = serializers.SerializerMethodField()
class Meta(DeviceSerializer.Meta):
fields = [
'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'airflow', 'primary_ip',
'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments',
'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
]
@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.JSONField()
@@ -935,7 +936,7 @@ class FrontPortRearPortSerializer(WritableNestedSerializer):
class Meta:
model = RearPort
fields = ['id', 'url', 'display', 'name', 'label']
fields = ['id', 'url', 'display', 'name', 'label', 'description']
class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
@@ -1059,7 +1060,7 @@ class TracedCableSerializer(serializers.ModelSerializer):
class Meta:
model = Cable
fields = [
'id', 'url', 'type', 'status', 'label', 'color', 'length', 'length_unit',
'id', 'url', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'description',
]

View File

@@ -785,7 +785,10 @@ class InterfaceTypeChoices(ChoiceSet):
TYPE_LAG = 'lag'
# Ethernet
TYPE_100ME_FX = '100base-fx'
TYPE_100ME_LFX = '100base-lfx'
TYPE_100ME_FIXED = '100base-tx'
TYPE_100ME_T1 = '100base-t1'
TYPE_1GE_FIXED = '1000base-t'
TYPE_1GE_GBIC = '1000base-x-gbic'
TYPE_1GE_SFP = '1000base-x-sfp'
@@ -810,6 +813,8 @@ class InterfaceTypeChoices(ChoiceSet):
TYPE_200GE_QSFP56 = '200gbase-x-qsfp56'
TYPE_400GE_QSFP_DD = '400gbase-x-qsfpdd'
TYPE_400GE_OSFP = '400gbase-x-osfp'
TYPE_800GE_QSFP_DD = '800gbase-x-qsfpdd'
TYPE_800GE_OSFP = '800gbase-x-osfp'
# Ethernet Backplane
TYPE_1GE_KX = '1000base-kx'
@@ -897,6 +902,7 @@ class InterfaceTypeChoices(ChoiceSet):
TYPE_STACKWISE160 = 'cisco-stackwise-160'
TYPE_STACKWISE320 = 'cisco-stackwise-320'
TYPE_STACKWISE480 = 'cisco-stackwise-480'
TYPE_STACKWISE1T = 'cisco-stackwise-1t'
TYPE_JUNIPER_VCP = 'juniper-vcp'
TYPE_SUMMITSTACK = 'extreme-summitstack'
TYPE_SUMMITSTACK128 = 'extreme-summitstack-128'
@@ -918,7 +924,10 @@ class InterfaceTypeChoices(ChoiceSet):
(
'Ethernet (fixed)',
(
(TYPE_100ME_FX, '100BASE-FX (10/100ME FIBER)'),
(TYPE_100ME_LFX, '100BASE-LFX (10/100ME FIBER)'),
(TYPE_100ME_FIXED, '100BASE-TX (10/100ME)'),
(TYPE_100ME_T1, '100BASE-T1 (10/100ME Single Pair)'),
(TYPE_1GE_FIXED, '1000BASE-T (1GE)'),
(TYPE_2GE_FIXED, '2.5GBASE-T (2.5GE)'),
(TYPE_5GE_FIXED, '5GBASE-T (5GE)'),
@@ -948,6 +957,8 @@ class InterfaceTypeChoices(ChoiceSet):
(TYPE_200GE_QSFP56, 'QSFP56 (200GE)'),
(TYPE_400GE_QSFP_DD, 'QSFP-DD (400GE)'),
(TYPE_400GE_OSFP, 'OSFP (400GE)'),
(TYPE_800GE_QSFP_DD, 'QSFP-DD (800GE)'),
(TYPE_800GE_OSFP, 'OSFP (800GE)'),
)
),
(
@@ -1068,6 +1079,7 @@ class InterfaceTypeChoices(ChoiceSet):
(TYPE_STACKWISE160, 'Cisco StackWise-160'),
(TYPE_STACKWISE320, 'Cisco StackWise-320'),
(TYPE_STACKWISE480, 'Cisco StackWise-480'),
(TYPE_STACKWISE1T, 'Cisco StackWise-1T'),
(TYPE_JUNIPER_VCP, 'Juniper VCP'),
(TYPE_SUMMITSTACK, 'Extreme SummitStack'),
(TYPE_SUMMITSTACK128, 'Extreme SummitStack-128'),

View File

@@ -3,7 +3,7 @@ from django.contrib.auth.models import User
from django.utils.translation import gettext as _
from extras.filtersets import LocalConfigContextFilterSet
from ipam.models import ASN, VRF
from ipam.models import ASN, L2VPN, IPAddress, VRF
from netbox.filtersets import (
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet,
)
@@ -24,6 +24,7 @@ __all__ = (
'CableFilterSet',
'CabledObjectFilterSet',
'CableTerminationFilterSet',
'CommonInterfaceFilterSet',
'ConsoleConnectionFilterSet',
'ConsolePortFilterSet',
'ConsolePortTemplateFilterSet',
@@ -958,6 +959,16 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter
method='_device_bays',
label=_('Has device bays'),
)
primary_ip4_id = django_filters.ModelMultipleChoiceFilter(
field_name='primary_ip4',
queryset=IPAddress.objects.all(),
label=_('Primary IPv4 (ID)'),
)
primary_ip6_id = django_filters.ModelMultipleChoiceFilter(
field_name='primary_ip6',
queryset=IPAddress.objects.all(),
label=_('Primary IPv6 (ID)'),
)
class Meta:
model = Device
@@ -971,7 +982,9 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter
Q(serial__icontains=value.strip()) |
Q(inventoryitems__serial__icontains=value.strip()) |
Q(asset_tag__icontains=value.strip()) |
Q(comments__icontains=value)
Q(comments__icontains=value) |
Q(primary_ip4__address__startswith=value) |
Q(primary_ip6__address__startswith=value)
).distinct()
def _has_primary_ip(self, queryset, name, value):
@@ -1309,11 +1322,63 @@ class PowerOutletFilterSet(
fields = ['id', 'name', 'label', 'feed_leg', 'description', 'cable_end']
class CommonInterfaceFilterSet(django_filters.FilterSet):
vlan_id = django_filters.CharFilter(
method='filter_vlan_id',
label=_('Assigned VLAN')
)
vlan = django_filters.CharFilter(
method='filter_vlan',
label=_('Assigned VID')
)
vrf_id = django_filters.ModelMultipleChoiceFilter(
field_name='vrf',
queryset=VRF.objects.all(),
label=_('VRF'),
)
vrf = django_filters.ModelMultipleChoiceFilter(
field_name='vrf__rd',
queryset=VRF.objects.all(),
to_field_name='rd',
label=_('VRF (RD)'),
)
l2vpn_id = django_filters.ModelMultipleChoiceFilter(
field_name='l2vpn_terminations__l2vpn',
queryset=L2VPN.objects.all(),
label=_('L2VPN (ID)'),
)
l2vpn = django_filters.ModelMultipleChoiceFilter(
field_name='l2vpn_terminations__l2vpn__identifier',
queryset=L2VPN.objects.all(),
to_field_name='identifier',
label=_('L2VPN'),
)
def filter_vlan_id(self, queryset, name, value):
value = value.strip()
if not value:
return queryset
return queryset.filter(
Q(untagged_vlan_id=value) |
Q(tagged_vlans=value)
)
def filter_vlan(self, queryset, name, value):
value = value.strip()
if not value:
return queryset
return queryset.filter(
Q(untagged_vlan_id__vid=value) |
Q(tagged_vlans__vid=value)
)
class InterfaceFilterSet(
ModularDeviceComponentFilterSet,
NetBoxModelFilterSet,
CabledObjectFilterSet,
PathEndpointFilterSet
PathEndpointFilterSet,
CommonInterfaceFilterSet
):
# Override device and device_id filters from DeviceComponentFilterSet to match against any peer virtual chassis
# members
@@ -1358,14 +1423,6 @@ class InterfaceFilterSet(
poe_type = django_filters.MultipleChoiceFilter(
choices=InterfacePoETypeChoices
)
vlan_id = django_filters.CharFilter(
method='filter_vlan_id',
label=_('Assigned VLAN')
)
vlan = django_filters.CharFilter(
method='filter_vlan',
label=_('Assigned VID')
)
type = django_filters.MultipleChoiceFilter(
choices=InterfaceTypeChoices,
null_value=None
@@ -1376,17 +1433,6 @@ class InterfaceFilterSet(
rf_channel = django_filters.MultipleChoiceFilter(
choices=WirelessChannelChoices
)
vrf_id = django_filters.ModelMultipleChoiceFilter(
field_name='vrf',
queryset=VRF.objects.all(),
label=_('VRF'),
)
vrf = django_filters.ModelMultipleChoiceFilter(
field_name='vrf__rd',
queryset=VRF.objects.all(),
to_field_name='rd',
label=_('VRF (RD)'),
)
vdc_id = django_filters.ModelMultipleChoiceFilter(
field_name='vdcs',
queryset=VirtualDeviceContext.objects.all(),
@@ -1433,24 +1479,6 @@ class InterfaceFilterSet(
except Device.DoesNotExist:
return queryset.none()
def filter_vlan_id(self, queryset, name, value):
value = value.strip()
if not value:
return queryset
return queryset.filter(
Q(untagged_vlan_id=value) |
Q(tagged_vlans=value)
)
def filter_vlan(self, queryset, name, value):
value = value.strip()
if not value:
return queryset
return queryset.filter(
Q(untagged_vlan_id__vid=value) |
Q(tagged_vlans__vid=value)
)
def filter_kind(self, queryset, name, value):
value = value.strip().lower()
return {
@@ -1639,12 +1667,14 @@ class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
field_name='terminations__termination_type'
)
termination_a_id = MultiValueNumberFilter(
method='filter_by_cable_end_a',
field_name='terminations__termination_id'
)
termination_b_type = ContentTypeFilter(
field_name='terminations__termination_type'
)
termination_b_id = MultiValueNumberFilter(
method='filter_by_cable_end_b',
field_name='terminations__termination_id'
)
type = django_filters.MultipleChoiceFilter(
@@ -1702,8 +1732,21 @@ class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
# Supported objects: device, rack, location, site
return queryset.filter(**{f'terminations___{name}__in': value}).distinct()
def filter_by_cable_end(self, queryset, name, value, side):
# Filter by termination id and cable_end type
return queryset.filter(**{f'{name}__in': value, 'terminations__cable_end': side}).distinct()
def filter_by_cable_end_a(self, queryset, name, value):
# Filter by termination id and cable_end type
return self.filter_by_cable_end(queryset, name, value, CableEndChoices.SIDE_A)
def filter_by_cable_end_b(self, queryset, name, value):
# Filter by termination id and cable_end type
return self.filter_by_cable_end(queryset, name, value, CableEndChoices.SIDE_B)
class CableTerminationFilterSet(BaseFilterSet):
termination_type = ContentTypeFilter()
class Meta:
model = CableTermination

View File

@@ -103,9 +103,9 @@ class RearPortBulkCreateForm(
class ModuleBayBulkCreateForm(DeviceBulkAddComponentForm):
model = ModuleBay
field_order = ('name', 'label', 'position_pattern', 'description', 'tags')
field_order = ('name', 'label', 'position', 'description', 'tags')
replication_fields = ('name', 'label', 'position')
position_pattern = ExpandableNameField(
position = ExpandableNameField(
label=_('Position'),
required=False,
help_text=_('Alphanumeric ranges are supported. (Must match the number of names being created.)')

View File

@@ -11,7 +11,7 @@ from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant
from utilities.forms import (
add_blank_choice, BulkEditForm, BulkEditNullBooleanSelect, ColorField, CommentField, DynamicModelChoiceField,
DynamicModelMultipleChoiceField, form_from_model, SmallTextarea, StaticSelect, SelectSpeedWidget,
DynamicModelMultipleChoiceField, form_from_model, StaticSelect, SelectSpeedWidget
)
__all__ = (
@@ -138,7 +138,6 @@ class SiteBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@@ -309,7 +308,6 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@@ -345,7 +343,6 @@ class RackReservationBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@@ -406,7 +403,6 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@@ -441,7 +437,6 @@ class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@@ -551,7 +546,6 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@@ -594,7 +588,6 @@ class ModuleBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@@ -644,7 +637,6 @@ class CableBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@@ -668,7 +660,6 @@ class VirtualChassisBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@@ -714,7 +705,6 @@ class PowerPanelBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@@ -776,7 +766,6 @@ class PowerFeedBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label=_('Comments')
)
@@ -1186,6 +1175,14 @@ class InterfaceBulkEditForm(
},
label=_('LAG')
)
vdcs = DynamicModelMultipleChoiceField(
queryset=VirtualDeviceContext.objects.all(),
required=False,
label='Virtual Device Contexts',
query_params={
'device_id': '$device',
}
)
speed = forms.IntegerField(
required=False,
widget=SelectSpeedWidget(),
@@ -1251,14 +1248,14 @@ class InterfaceBulkEditForm(
fieldsets = (
(None, ('module', 'type', 'label', 'speed', 'duplex', 'description')),
('Addressing', ('vrf', 'mac_address', 'wwn')),
('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
('Operation', ('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
('PoE', ('poe_mode', 'poe_type')),
('Related Interfaces', ('parent', 'bridge', 'lag')),
('802.1Q Switching', ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
('Wireless', ('rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width')),
)
nullable_fields = (
'module', 'label', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'mtu', 'description',
'module', 'label', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'vdcs', 'mtu', 'description',
'poe_mode', 'poe_type', 'mode', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power',
'vlan_group', 'untagged_vlan', 'tagged_vlans', 'vrf',
)

View File

@@ -11,14 +11,15 @@ from dcim.models import *
from ipam.models import VRF
from netbox.forms import NetBoxModelImportForm
from tenancy.models import Tenant
from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVTypedChoiceField, SlugField
from utilities.forms import (
CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVTypedChoiceField, SlugField, CSVModelMultipleChoiceField
)
from virtualization.models import Cluster
from wireless.choices import WirelessRoleChoices
from .common import ModuleCommonForm
__all__ = (
'CableImportForm',
'ChildDeviceImportForm',
'ConsolePortImportForm',
'ConsoleServerPortImportForm',
'DeviceBayImportForm',
@@ -413,6 +414,18 @@ class DeviceImportForm(BaseDeviceImportForm):
required=False,
help_text=_('Mounted rack face')
)
parent = CSVModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name',
required=False,
help_text=_('Parent device (for child devices)')
)
device_bay = CSVModelChoiceField(
queryset=DeviceBay.objects.all(),
to_field_name='name',
required=False,
help_text=_('Device bay in which this device is installed (for child devices)')
)
airflow = CSVChoiceField(
choices=DeviceAirflowChoices,
required=False,
@@ -422,8 +435,8 @@ class DeviceImportForm(BaseDeviceImportForm):
class Meta(BaseDeviceImportForm.Meta):
fields = [
'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
'site', 'location', 'rack', 'position', 'face', 'airflow', 'virtual_chassis', 'vc_position', 'vc_priority',
'cluster', 'description', 'comments', 'tags',
'site', 'location', 'rack', 'position', 'face', 'parent', 'device_bay', 'airflow', 'virtual_chassis',
'vc_position', 'vc_priority', 'cluster', 'description', 'comments', 'tags',
]
def __init__(self, data=None, *args, **kwargs):
@@ -434,14 +447,35 @@ class DeviceImportForm(BaseDeviceImportForm):
# Limit location queryset by assigned site
params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
self.fields['parent'].queryset = self.fields['parent'].queryset.filter(**params)
# Limit rack queryset by assigned site and group
# Limit rack queryset by assigned site and location
params = {
f"site__{self.fields['site'].to_field_name}": data.get('site'),
f"location__{self.fields['location'].to_field_name}": data.get('location'),
}
if 'location' in data:
params.update({
f"location__{self.fields['location'].to_field_name}": data.get('location'),
})
self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)
# Limit device bay queryset by parent device
if parent := data.get('parent'):
params = {f"device__{self.fields['parent'].to_field_name}": parent}
self.fields['device_bay'].queryset = self.fields['device_bay'].queryset.filter(**params)
def clean(self):
super().clean()
# Inherit site and rack from parent device
if parent := self.cleaned_data.get('parent'):
self.instance.site = parent.site
self.instance.rack = parent.rack
# Set parent_bay reverse relationship
if device_bay := self.cleaned_data.get('device_bay'):
self.instance.parent_bay = device_bay
class ModuleImportForm(ModuleCommonForm, NetBoxModelImportForm):
device = CSVModelChoiceField(
@@ -495,48 +529,6 @@ class ModuleImportForm(ModuleCommonForm, NetBoxModelImportForm):
return self.cleaned_data['replicate_components']
class ChildDeviceImportForm(BaseDeviceImportForm):
parent = CSVModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name',
help_text=_('Parent device')
)
device_bay = CSVModelChoiceField(
queryset=DeviceBay.objects.all(),
to_field_name='name',
help_text=_('Device bay in which this device is installed')
)
class Meta(BaseDeviceImportForm.Meta):
fields = [
'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
'parent', 'device_bay', 'virtual_chassis', 'vc_position', 'vc_priority', 'cluster', 'comments', 'tags'
]
def __init__(self, data=None, *args, **kwargs):
super().__init__(data, *args, **kwargs)
if data:
# Limit device bay queryset by parent device
params = {f"device__{self.fields['parent'].to_field_name}": data.get('parent')}
self.fields['device_bay'].queryset = self.fields['device_bay'].queryset.filter(**params)
def clean(self):
super().clean()
# Set parent_bay reverse relationship
device_bay = self.cleaned_data.get('device_bay')
if device_bay:
self.instance.parent_bay = device_bay
# Inherit site and rack from parent device
parent = self.cleaned_data.get('parent')
if parent:
self.instance.site = parent.site
self.instance.rack = parent.rack
#
# Device components
#
@@ -677,6 +669,12 @@ class InterfaceImportForm(NetBoxModelImportForm):
to_field_name='name',
help_text=_('Parent LAG interface')
)
vdcs = CSVModelMultipleChoiceField(
queryset=VirtualDeviceContext.objects.all(),
required=False,
to_field_name='name',
help_text='VDC names separated by commas, encased with double quotes (e.g. "vdc1, vdc2, vdc3")'
)
type = CSVChoiceField(
choices=InterfaceTypeChoices,
help_text=_('Physical medium')
@@ -716,7 +714,7 @@ class InterfaceImportForm(NetBoxModelImportForm):
model = Interface
fields = (
'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'speed', 'duplex', 'enabled',
'mark_connected', 'mac_address', 'wwn', 'mtu', 'mgmt_only', 'description', 'poe_mode', 'poe_type', 'mode',
'mark_connected', 'mac_address', 'wwn', 'vdcs', 'mtu', 'mgmt_only', 'description', 'poe_mode', 'poe_type', 'mode',
'vrf', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'tags'
)
@@ -732,6 +730,7 @@ class InterfaceImportForm(NetBoxModelImportForm):
self.fields['parent'].queryset = self.fields['parent'].queryset.filter(**params)
self.fields['bridge'].queryset = self.fields['bridge'].queryset.filter(**params)
self.fields['lag'].queryset = self.fields['lag'].queryset.filter(**params)
self.fields['vdcs'].queryset = self.fields['vdcs'].queryset.filter(**params)
def clean_enabled(self):
# Make sure enabled is True when it's not included in the uploaded data
@@ -740,6 +739,12 @@ class InterfaceImportForm(NetBoxModelImportForm):
else:
return self.cleaned_data['enabled']
def clean_vdcs(self):
for vdc in self.cleaned_data['vdcs']:
if vdc.device != self.cleaned_data['device']:
raise forms.ValidationError(f"VDC {vdc} is not assigned to device {self.cleaned_data['device']}")
return self.cleaned_data['vdcs']
class FrontPortImportForm(NetBoxModelImportForm):
device = CSVModelChoiceField(

View File

@@ -6,7 +6,7 @@ from dcim.choices import *
from dcim.constants import *
from dcim.models import *
from extras.forms import LocalConfigContextFilterForm
from ipam.models import ASN, VRF
from ipam.models import ASN, L2VPN, VRF
from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
from utilities.forms import (
@@ -1112,7 +1112,7 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Attributes', ('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only')),
('Addressing', ('vrf_id', 'mac_address', 'wwn')),
('Addressing', ('vrf_id', 'l2vpn_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', 'rack_id', 'virtual_chassis_id',
@@ -1170,7 +1170,7 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
label='PoE mode'
)
poe_type = MultipleChoiceField(
choices=InterfacePoEModeChoices,
choices=InterfacePoETypeChoices,
required=False,
label='PoE type'
)
@@ -1203,6 +1203,11 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
required=False,
label='VRF'
)
l2vpn_id = DynamicModelMultipleChoiceField(
queryset=L2VPN.objects.all(),
required=False,
label=_('L2VPN')
)
tag = TagFilterField(model)

View File

@@ -359,7 +359,7 @@ class VirtualChassisCreateForm(NetBoxModelForm):
class Meta:
model = VirtualChassis
fields = [
'name', 'domain', 'region', 'site_group', 'site', 'rack', 'members', 'initial_position', 'tags',
'name', 'domain', 'description', 'region', 'site_group', 'site', 'rack', 'members', 'initial_position', 'tags',
]
def clean(self):

View File

@@ -10,3 +10,11 @@ class CabledObjectMixin:
def resolve_link_peers(self, info):
return self.link_peers
class PathEndpointMixin:
connected_endpoints = graphene.List('dcim.graphql.gfk_mixins.LinkPeerType')
def resolve_connected_endpoints(self, info):
# Handle empty values
return self.connected_endpoints or None

View File

@@ -7,7 +7,7 @@ from extras.graphql.mixins import (
from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin
from netbox.graphql.scalars import BigInt
from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType
from .mixins import CabledObjectMixin
from .mixins import CabledObjectMixin, PathEndpointMixin
__all__ = (
'CableType',
@@ -117,7 +117,7 @@ class CableTerminationType(NetBoxObjectType):
filterset_class = filtersets.CableTerminationFilterSet
class ConsolePortType(ComponentObjectType, CabledObjectMixin):
class ConsolePortType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin):
class Meta:
model = models.ConsolePort
@@ -139,7 +139,7 @@ class ConsolePortTemplateType(ComponentTemplateObjectType):
return self.type or None
class ConsoleServerPortType(ComponentObjectType, CabledObjectMixin):
class ConsoleServerPortType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin):
class Meta:
model = models.ConsoleServerPort
@@ -241,7 +241,7 @@ class FrontPortTemplateType(ComponentTemplateObjectType):
filterset_class = filtersets.FrontPortTemplateFilterSet
class InterfaceType(IPAddressesMixin, ComponentObjectType, CabledObjectMixin):
class InterfaceType(IPAddressesMixin, ComponentObjectType, CabledObjectMixin, PathEndpointMixin):
class Meta:
model = models.Interface
@@ -354,7 +354,7 @@ class PlatformType(OrganizationalObjectType):
filterset_class = filtersets.PlatformFilterSet
class PowerFeedType(NetBoxObjectType, CabledObjectMixin):
class PowerFeedType(NetBoxObjectType, CabledObjectMixin, PathEndpointMixin):
class Meta:
model = models.PowerFeed
@@ -362,7 +362,7 @@ class PowerFeedType(NetBoxObjectType, CabledObjectMixin):
filterset_class = filtersets.PowerFeedFilterSet
class PowerOutletType(ComponentObjectType, CabledObjectMixin):
class PowerOutletType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin):
class Meta:
model = models.PowerOutlet
@@ -398,7 +398,7 @@ class PowerPanelType(NetBoxObjectType, ContactsMixin):
filterset_class = filtersets.PowerPanelFilterSet
class PowerPortType(ComponentObjectType, CabledObjectMixin):
class PowerPortType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin):
class Meta:
model = models.PowerPort

View File

@@ -112,6 +112,10 @@ class Cable(PrimaryModel):
def a_terminations(self):
if hasattr(self, '_a_terminations'):
return self._a_terminations
if not self.pk:
return []
# Query self.terminations.all() to leverage cached results
return [
ct.termination for ct in self.terminations.all() if ct.cable_end == CableEndChoices.SIDE_A
@@ -119,13 +123,18 @@ class Cable(PrimaryModel):
@a_terminations.setter
def a_terminations(self, value):
self._terminations_modified = True
if not self.pk or self.a_terminations != list(value):
self._terminations_modified = True
self._a_terminations = value
@property
def b_terminations(self):
if hasattr(self, '_b_terminations'):
return self._b_terminations
if not self.pk:
return []
# Query self.terminations.all() to leverage cached results
return [
ct.termination for ct in self.terminations.all() if ct.cable_end == CableEndChoices.SIDE_B
@@ -133,7 +142,8 @@ class Cable(PrimaryModel):
@b_terminations.setter
def b_terminations(self, value):
self._terminations_modified = True
if not self.pk or self.b_terminations != list(value):
self._terminations_modified = True
self._b_terminations = value
def clean(self):
@@ -142,8 +152,6 @@ class Cable(PrimaryModel):
# Validate length and length_unit
if self.length is not None and not self.length_unit:
raise ValidationError("Must specify a unit when setting a cable length")
elif self.length is None:
self.length_unit = ''
if self.pk is None and (not self.a_terminations or not self.b_terminations):
raise ValidationError("Must define A and B terminations when creating a new cable.")
@@ -177,6 +185,10 @@ class Cable(PrimaryModel):
else:
self._abs_length = None
# Clear length_unit if no length is defined
if self.length is None:
self.length_unit = ''
super().save(*args, **kwargs)
# Update the private pk used in __str__ in case this is a new object (i.e. just got its pk)
@@ -527,7 +539,7 @@ class CablePath(models.Model):
# Step 5: Record the far-end termination object(s)
path.append([
object_to_path_node(t) for t in remote_terminations
object_to_path_node(t) for t in remote_terminations if t is not None
])
# Step 6: Determine the "next hop" terminations, if applicable

View File

@@ -120,6 +120,12 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
),
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Cache the original DeviceType ID for reference under clean()
self._original_device_type = self.device_type_id
def to_objectchange(self, action):
objectchange = super().to_objectchange(action)
if self.device_type is not None:
@@ -131,6 +137,11 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
def clean(self):
super().clean()
if self.pk is not None and self._original_device_type != self.device_type_id:
raise ValidationError({
"device_type": "Component templates cannot be moved to a different device type."
})
# A component template must belong to a DeviceType *or* to a ModuleType
if self.device_type and self.module_type:
raise ValidationError(

View File

@@ -78,6 +78,12 @@ class ComponentModel(NetBoxModel):
),
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Cache the original Device ID for reference under clean()
self._original_device = self.device_id
def __str__(self):
if self.label:
return f"{self.name} ({self.label})"
@@ -88,6 +94,14 @@ class ComponentModel(NetBoxModel):
objectchange.related_object = self.device
return objectchange
def clean(self):
super().clean()
if self.pk is not None and self._original_device != self.device_id:
raise ValidationError({
"device": "Components cannot be moved to a different device."
})
@property
def parent_object(self):
return self.device
@@ -794,8 +808,6 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
raise ValidationError({
'rf_channel_frequency': "Cannot specify custom frequency with channel selected.",
})
elif self.rf_channel:
self.rf_channel_frequency = get_channel_attr(self.rf_channel, 'frequency')
# Validate channel width against interface type and selected channel (if any)
if self.rf_channel_width:
@@ -803,8 +815,6 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
raise ValidationError({'rf_channel_width': "Channel width may be set only on wireless interfaces."})
if self.rf_channel and self.rf_channel_width != get_channel_attr(self.rf_channel, 'width'):
raise ValidationError({'rf_channel_width': "Cannot specify custom width with channel selected."})
elif self.rf_channel:
self.rf_channel_width = get_channel_attr(self.rf_channel, 'width')
# VLAN validation
@@ -815,6 +825,16 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
f"interface's parent device, or it must be global."
})
def save(self, *args, **kwargs):
# Set absolute channel attributes from selected options
if self.rf_channel and not self.rf_channel_frequency:
self.rf_channel_frequency = get_channel_attr(self.rf_channel, 'frequency')
if self.rf_channel and not self.rf_channel_width:
self.rf_channel_width = get_channel_attr(self.rf_channel, 'width')
super().save(*args, **kwargs)
@property
def _occupied(self):
return super()._occupied or bool(self.wireless_link_id)

View File

@@ -120,6 +120,10 @@ class DeviceType(PrimaryModel, WeightMixin):
blank=True
)
images = GenericRelation(
to='extras.ImageAttachment'
)
clone_fields = (
'manufacturer', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit'
)
@@ -659,8 +663,6 @@ class Device(PrimaryModel, ConfigContextModel):
raise ValidationError({
'rack': f"Rack {self.rack} does not belong to location {self.location}.",
})
elif self.rack:
self.location = self.rack.location
if self.rack is None:
if self.face:
@@ -776,8 +778,10 @@ class Device(PrimaryModel, ConfigContextModel):
bulk_create: If True, bulk_create() will be called to create all components in a single query
(default). Otherwise, save() will be called on each instance individually.
"""
components = [obj.instantiate(device=self) for obj in queryset]
if components and bulk_create:
if bulk_create:
components = [obj.instantiate(device=self) for obj in queryset]
if not components:
return
model = components[0]._meta.model
model.objects.bulk_create(components)
# Manually send the post_save signal for each of the newly created components
@@ -790,8 +794,9 @@ class Device(PrimaryModel, ConfigContextModel):
using='default',
update_fields=None
)
elif components:
for component in components:
else:
for obj in queryset:
component = obj.instantiate(device=self)
component.save()
def save(self, *args, **kwargs):
@@ -801,6 +806,9 @@ class Device(PrimaryModel, ConfigContextModel):
if is_new and not self.airflow:
self.airflow = self.device_type.airflow
if self.rack and self.rack.location:
self.location = self.rack.location
super().save(*args, **kwargs)
# If this is a new Device, instantiate all the related components per the DeviceType definition

View File

@@ -222,8 +222,6 @@ class Rack(PrimaryModel, WeightMixin):
# Validate outer dimensions and unit
if (self.outer_width is not None or self.outer_depth is not None) and not self.outer_unit:
raise ValidationError("Must specify a unit when setting an outer width/depth")
elif self.outer_width is None and self.outer_depth is None:
self.outer_unit = ''
# Validate max_weight and weight_unit
if self.max_weight and not self.weight_unit:
@@ -259,6 +257,10 @@ class Rack(PrimaryModel, WeightMixin):
else:
self._abs_max_weight = None
# Clear unit if outer width & depth are not set
if self.outer_width is None and self.outer_depth is None:
self.outer_unit = ''
super().save(*args, **kwargs)
@property

View File

@@ -124,6 +124,9 @@ def nullify_connected_endpoints(instance, **kwargs):
model.objects.filter(pk=instance.termination_id).update(cable=None, cable_end='')
for cablepath in CablePath.objects.filter(_nodes__contains=instance.cable):
# Remove the deleted CableTermination if it's one of the path's originating nodes
if instance.termination in cablepath.origins:
cablepath.origins.remove(instance.termination)
cablepath.retrace()

View File

@@ -580,7 +580,6 @@ class DeviceInterfaceTable(InterfaceTable):
'wireless_lans', 'link_peer', 'connection', 'tags', 'vdcs', 'vrf', 'l2vpn', 'ip_addresses', 'fhrp_groups',
'untagged_vlan', 'tagged_vlans', 'actions',
)
order_by = ('name',)
default_columns = (
'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mtu', 'mode', 'description', 'ip_addresses',
'cable', 'connection',
@@ -589,6 +588,7 @@ class DeviceInterfaceTable(InterfaceTable):
'class': get_interface_row_class,
'data-name': lambda record: record.name,
'data-enabled': get_interface_state_attribute,
'data-type': lambda record: record.type,
}

View File

@@ -34,10 +34,19 @@ class ManufacturerTable(ContactsColumnMixin, NetBoxTable):
url_params={'manufacturer_id': 'pk'},
verbose_name='Device Types'
)
inventoryitem_count = tables.Column(
moduletype_count = columns.LinkedCountColumn(
viewname='dcim:moduletype_list',
url_params={'manufacturer_id': 'pk'},
verbose_name='Module Types'
)
inventoryitem_count = columns.LinkedCountColumn(
viewname='dcim:inventoryitem_list',
url_params={'manufacturer_id': 'pk'},
verbose_name='Inventory Items'
)
platform_count = tables.Column(
platform_count = columns.LinkedCountColumn(
viewname='dcim:platform_list',
url_params={'manufacturer_id': 'pk'},
verbose_name='Platforms'
)
slug = tables.Column()
@@ -48,11 +57,12 @@ class ManufacturerTable(ContactsColumnMixin, NetBoxTable):
class Meta(NetBoxTable.Meta):
model = models.Manufacturer
fields = (
'pk', 'id', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug',
'tags', 'contacts', 'actions', 'created', 'last_updated',
'pk', 'id', 'name', 'devicetype_count', 'moduletype_count', 'inventoryitem_count', 'platform_count',
'description', 'slug', 'tags', 'contacts', 'actions', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug',
'pk', 'name', 'devicetype_count', 'moduletype_count', 'inventoryitem_count', 'platform_count',
'description', 'slug',
)

View File

@@ -78,7 +78,7 @@ class PowerFeedTable(CableTerminationTable):
model = PowerFeed
fields = (
'pk', 'id', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
'max_utilization', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'available_power',
'max_utilization', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'available_power',
'description', 'comments', 'tags', 'created', 'last_updated',
)
default_columns = (

View File

@@ -115,10 +115,28 @@ CONSOLEPORT_BUTTONS = """
{% if record.cable %}
<a href="{% url 'dcim:consoleport_trace' pk=record.pk %}" class="btn btn-primary btn-sm" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
{% if perms.dcim.delete_cable %}
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-sm">
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i>
</a>
{% if perms.dcim.change_cable or perms.dcim.delete_cable %}
<span class="dropdown">
<button type="button" class="btn btn-warning btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
</button>
<ul class="dropdown-menu dropdown-menu-end">
{% if perms.dcim.change_cable %}
<li><a class="dropdown-item" href="{% url 'dcim:cable_edit' pk=record.cable.pk %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}">
<i class="mdi mdi-pencil-outline"></i>
Edit cable
</a>
</li>
{% endif %}
{% if perms.dcim.delete_cable %}
<li><a class="dropdown-item" href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}">
<i class="mdi mdi-trash-can-outline"></i>
Delete cable
</a>
</li>
{% endif %}
</ul>
</span>
{% endif %}
{% elif perms.dcim.add_cable %}
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
@@ -147,10 +165,28 @@ CONSOLESERVERPORT_BUTTONS = """
{% if record.cable %}
<a href="{% url 'dcim:consoleserverport_trace' pk=record.pk %}" class="btn btn-primary btn-sm" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
{% if perms.dcim.delete_cable %}
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-sm">
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i>
</a>
{% if perms.dcim.change_cable or perms.dcim.delete_cable %}
<span class="dropdown">
<button type="button" class="btn btn-warning btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
</button>
<ul class="dropdown-menu dropdown-menu-end">
{% if perms.dcim.change_cable %}
<li><a class="dropdown-item" href="{% url 'dcim:cable_edit' pk=record.cable.pk %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}">
<i class="mdi mdi-pencil-outline"></i>
Edit cable
</a>
</li>
{% endif %}
{% if perms.dcim.delete_cable %}
<li><a class="dropdown-item" href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}">
<i class="mdi mdi-trash-can-outline"></i>
Delete cable
</a>
</li>
{% endif %}
</ul>
</span>
{% endif %}
{% elif perms.dcim.add_cable %}
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
@@ -179,10 +215,28 @@ POWERPORT_BUTTONS = """
{% if record.cable %}
<a href="{% url 'dcim:powerport_trace' pk=record.pk %}" class="btn btn-primary btn-sm" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
{% if perms.dcim.delete_cable %}
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-sm">
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i>
</a>
{% if perms.dcim.change_cable or perms.dcim.delete_cable %}
<span class="dropdown">
<button type="button" class="btn btn-warning btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
</button>
<ul class="dropdown-menu dropdown-menu-end">
{% if perms.dcim.change_cable %}
<li><a class="dropdown-item" href="{% url 'dcim:cable_edit' pk=record.cable.pk %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}">
<i class="mdi mdi-pencil-outline"></i>
Edit cable
</a>
</li>
{% endif %}
{% if perms.dcim.delete_cable %}
<li><a class="dropdown-item" href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}">
<i class="mdi mdi-trash-can-outline"></i>
Delete cable
</a>
</li>
{% endif %}
</ul>
</span>
{% endif %}
{% elif perms.dcim.add_cable %}
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
@@ -210,10 +264,28 @@ POWEROUTLET_BUTTONS = """
{% if record.cable %}
<a href="{% url 'dcim:poweroutlet_trace' pk=record.pk %}" class="btn btn-primary btn-sm" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
{% if perms.dcim.delete_cable %}
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-sm">
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i>
</a>
{% if perms.dcim.change_cable or perms.dcim.delete_cable %}
<span class="dropdown">
<button type="button" class="btn btn-warning btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
</button>
<ul class="dropdown-menu dropdown-menu-end">
{% if perms.dcim.change_cable %}
<li><a class="dropdown-item" href="{% url 'dcim:cable_edit' pk=record.cable.pk %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}">
<i class="mdi mdi-pencil-outline"></i>
Edit cable
</a>
</li>
{% endif %}
{% if perms.dcim.delete_cable %}
<li><a class="dropdown-item" href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}">
<i class="mdi mdi-trash-can-outline"></i>
Delete cable
</a>
</li>
{% endif %}
</ul>
</span>
{% endif %}
{% elif perms.dcim.add_cable %}
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
@@ -258,10 +330,28 @@ INTERFACE_BUTTONS = """
{% endif %}
{% if record.cable %}
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
{% if perms.dcim.delete_cable %}
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-sm">
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i>
</a>
{% if perms.dcim.change_cable or perms.dcim.delete_cable %}
<span class="dropdown">
<button type="button" class="btn btn-warning btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
</button>
<ul class="dropdown-menu dropdown-menu-end">
{% if perms.dcim.change_cable %}
<li><a class="dropdown-item" href="{% url 'dcim:cable_edit' pk=record.cable.pk %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}">
<i class="mdi mdi-pencil-outline"></i>
Edit cable
</a>
</li>
{% endif %}
{% if perms.dcim.delete_cable %}
<li><a class="dropdown-item" href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}">
<i class="mdi mdi-trash-can-outline"></i>
Delete cable
</a>
</li>
{% endif %}
</ul>
</span>
{% endif %}
{% elif record.wireless_link %}
{% if perms.wireless.delete_wirelesslink %}
@@ -303,10 +393,28 @@ FRONTPORT_BUTTONS = """
{% if record.cable %}
<a href="{% url 'dcim:frontport_trace' pk=record.pk %}" class="btn btn-primary btn-sm" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
{% if perms.dcim.delete_cable %}
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-sm">
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i>
</a>
{% if perms.dcim.change_cable or perms.dcim.delete_cable %}
<span class="dropdown">
<button type="button" class="btn btn-warning btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
</button>
<ul class="dropdown-menu dropdown-menu-end">
{% if perms.dcim.change_cable %}
<li><a class="dropdown-item" href="{% url 'dcim:cable_edit' pk=record.cable.pk %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}">
<i class="mdi mdi-pencil-outline"></i>
Edit cable
</a>
</li>
{% endif %}
{% if perms.dcim.delete_cable %}
<li><a class="dropdown-item" href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}">
<i class="mdi mdi-trash-can-outline"></i>
Delete cable
</a>
</li>
{% endif %}
</ul>
</span>
{% endif %}
{% elif perms.dcim.add_cable %}
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
@@ -340,10 +448,28 @@ REARPORT_BUTTONS = """
{% if record.cable %}
<a href="{% url 'dcim:rearport_trace' pk=record.pk %}" class="btn btn-primary btn-sm" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
{% if perms.dcim.delete_cable %}
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-sm">
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i>
</a>
{% if perms.dcim.change_cable or perms.dcim.delete_cable %}
<span class="dropdown">
<button type="button" class="btn btn-warning btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
</button>
<ul class="dropdown-menu dropdown-menu-end">
{% if perms.dcim.change_cable %}
<li><a class="dropdown-item" href="{% url 'dcim:cable_edit' pk=record.cable.pk %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}">
<i class="mdi mdi-pencil-outline"></i>
Edit cable
</a>
</li>
{% endif %}
{% if perms.dcim.delete_cable %}
<li><a class="dropdown-item" href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}">
<i class="mdi mdi-trash-can-outline"></i>
Delete cable
</a>
</li>
{% endif %}
</ul>
</span>
{% endif %}
{% elif perms.dcim.add_cable %}
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>

View File

@@ -1804,3 +1804,44 @@ class CablePathTestCase(TestCase):
is_active=True
)
self.assertEqual(CablePath.objects.count(), 2)
def test_303_remove_termination_from_existing_cable(self):
"""
[IF1] --C1-- [IF2]
[IF3]
"""
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
interface3 = Interface.objects.create(device=self.device, name='Interface 3')
# Create cables 1
cable1 = Cable(
a_terminations=[interface1],
b_terminations=[interface2, interface3]
)
cable1.save()
self.assertPathExists(
(interface1, cable1, [interface2, interface3]),
is_complete=True,
is_active=True
)
self.assertPathExists(
([interface2, interface3], cable1, interface1),
is_complete=True,
is_active=True
)
# Remove the termination to interface 3
cable1 = Cable.objects.first()
cable1.b_terminations = [interface2]
cable1.save()
self.assertPathExists(
(interface1, cable1, interface2),
is_complete=True,
is_active=True
)
self.assertPathExists(
(interface2, cable1, interface1),
is_complete=True,
is_active=True
)

View File

@@ -1626,10 +1626,14 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
ipaddresses = (
IPAddress(address='192.0.2.1/24', assigned_object=interfaces[0]),
IPAddress(address='192.0.2.2/24', assigned_object=interfaces[1]),
IPAddress(address='192.0.2.3/24', assigned_object=None),
IPAddress(address='2001:db8::1/64', assigned_object=interfaces[0]),
IPAddress(address='2001:db8::2/64', assigned_object=interfaces[1]),
IPAddress(address='2001:db8::3/64', assigned_object=None),
)
IPAddress.objects.bulk_create(ipaddresses)
Device.objects.filter(pk=devices[0].pk).update(primary_ip4=ipaddresses[0])
Device.objects.filter(pk=devices[1].pk).update(primary_ip4=ipaddresses[1])
Device.objects.filter(pk=devices[0].pk).update(primary_ip4=ipaddresses[0], primary_ip6=ipaddresses[3])
Device.objects.filter(pk=devices[1].pk).update(primary_ip4=ipaddresses[1], primary_ip6=ipaddresses[4])
# VirtualChassis assignment for filtering
virtual_chassis = VirtualChassis.objects.create(master=devices[0])
@@ -1761,6 +1765,20 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'has_primary_ip': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_primary_ip4(self):
addresses = IPAddress.objects.filter(address__family=4)
params = {'primary_ip4_id': [addresses[0].pk, addresses[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'primary_ip4_id': [addresses[2].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
def test_primary_ip6(self):
addresses = IPAddress.objects.filter(address__family=6)
params = {'primary_ip6_id': [addresses[0].pk, addresses[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'primary_ip6_id': [addresses[2].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
def test_virtual_chassis_id(self):
params = {'virtual_chassis_id': [VirtualChassis.objects.first().pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

View File

@@ -177,7 +177,6 @@ urlpatterns = [
path('devices/', views.DeviceListView.as_view(), name='device_list'),
path('devices/add/', views.DeviceEditView.as_view(), name='device_add'),
path('devices/import/', views.DeviceBulkImportView.as_view(), name='device_import'),
path('devices/import/child-devices/', views.ChildDeviceBulkImportView.as_view(), name='device_import_child'),
path('devices/edit/', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'),
path('devices/rename/', views.DeviceBulkRenameView.as_view(), name='device_bulk_rename'),
path('devices/delete/', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'),

View File

@@ -21,7 +21,9 @@ from utilities.paginator import EnhancedPaginator, get_paginate_count
from utilities.permissions import get_permission_for_model
from utilities.utils import count_related
from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin, ViewTab, register_model_view
from virtualization.filtersets import VirtualMachineFilterSet
from virtualization.models import VirtualMachine
from virtualization.tables import VirtualMachineTable
from . import filtersets, forms, tables
from .choices import DeviceFaceChoices
from .constants import NONCONNECTABLE_IFACE_TYPES
@@ -626,6 +628,7 @@ class RackRoleBulkDeleteView(generic.BulkDeleteView):
queryset = RackRole.objects.annotate(
rack_count=count_related(Rack, 'role')
)
filterset = filtersets.RackRoleFilterSet
table = tables.RackRoleTable
@@ -640,6 +643,7 @@ class RackListView(generic.ObjectListView):
filterset = filtersets.RackFilterSet
filterset_form = forms.RackFilterForm
table = tables.RackTable
template_name = 'dcim/rack_list.html'
class RackElevationListView(generic.ObjectListView):
@@ -840,6 +844,7 @@ class RackReservationBulkDeleteView(generic.BulkDeleteView):
class ManufacturerListView(generic.ObjectListView):
queryset = Manufacturer.objects.annotate(
devicetype_count=count_related(DeviceType, 'manufacturer'),
moduletype_count=count_related(ModuleType, 'manufacturer'),
inventoryitem_count=count_related(InventoryItem, 'manufacturer'),
platform_count=count_related(Platform, 'manufacturer')
)
@@ -905,6 +910,7 @@ class ManufacturerBulkDeleteView(generic.BulkDeleteView):
queryset = Manufacturer.objects.annotate(
devicetype_count=count_related(DeviceType, 'manufacturer')
)
filterset = filtersets.ManufacturerFilterSet
table = tables.ManufacturerTable
@@ -1736,6 +1742,42 @@ class DeviceRoleView(generic.ObjectView):
}
@register_model_view(DeviceRole, 'devices', path='devices')
class DeviceRoleDevicesView(generic.ObjectChildrenView):
queryset = DeviceRole.objects.all()
child_model = Device
table = tables.DeviceTable
filterset = filtersets.DeviceFilterSet
template_name = 'dcim/devicerole/devices.html'
tab = ViewTab(
label=_('Devices'),
badge=lambda obj: obj.devices.count(),
permission='dcim.view_device',
weight=400
)
def get_children(self, request, parent):
return Device.objects.restrict(request.user, 'view').filter(device_role=parent)
@register_model_view(DeviceRole, 'virtual_machines', path='virtual-machines')
class DeviceRoleVirtualMachinesView(generic.ObjectChildrenView):
queryset = DeviceRole.objects.all()
child_model = VirtualMachine
table = VirtualMachineTable
filterset = VirtualMachineFilterSet
template_name = 'dcim/devicerole/virtual_machines.html'
tab = ViewTab(
label=_('Virtual machines'),
badge=lambda obj: obj.virtual_machines.count(),
permission='virtualization.view_virtualmachine',
weight=500
)
def get_children(self, request, parent):
return VirtualMachine.objects.restrict(request.user, 'view').filter(role=parent)
@register_model_view(DeviceRole, 'edit')
class DeviceRoleEditView(generic.ObjectEditView):
queryset = DeviceRole.objects.all()
@@ -1768,6 +1810,7 @@ class DeviceRoleBulkDeleteView(generic.BulkDeleteView):
device_count=count_related(Device, 'device_role'),
vm_count=count_related(VirtualMachine, 'role')
)
filterset = filtersets.DeviceRoleFilterSet
table = tables.DeviceRoleTable
@@ -1828,6 +1871,7 @@ class PlatformBulkEditView(generic.BulkEditView):
class PlatformBulkDeleteView(generic.BulkDeleteView):
queryset = Platform.objects.all()
filterset = filtersets.PlatformFilterSet
table = tables.PlatformTable
@@ -1949,7 +1993,7 @@ class DeviceInterfacesView(DeviceComponentsView):
template_name = 'dcim/device/interfaces.html'
tab = ViewTab(
label=_('Interfaces'),
badge=lambda obj: obj.interfaces.count(),
badge=lambda obj: obj.vc_interfaces().count(),
permission='dcim.view_interface',
weight=520,
hide_if_empty=True
@@ -2052,22 +2096,15 @@ class DeviceBulkImportView(generic.BulkImportView):
queryset = Device.objects.all()
model_form = forms.DeviceImportForm
table = tables.DeviceImportTable
template_name = 'dcim/device_import.html'
class ChildDeviceBulkImportView(generic.BulkImportView):
queryset = Device.objects.all()
model_form = forms.ChildDeviceImportForm
table = tables.DeviceImportTable
template_name = 'dcim/device_import_child.html'
def save_object(self, object_form, request):
obj = object_form.save()
# Save the reverse relation to the parent device bay
device_bay = obj.parent_bay
device_bay.installed_device = obj
device_bay.save()
# For child devices, save the reverse relation to the parent device bay
if getattr(obj, 'parent_bay', None):
device_bay = obj.parent_bay
device_bay.installed_device = obj
device_bay.save()
return obj
@@ -2820,7 +2857,7 @@ class DeviceBayPopulateView(generic.ObjectEditView):
form = forms.PopulateDeviceBayForm(device_bay, request.POST)
if form.is_valid():
device_bay.snapshot()
device_bay.installed_device = form.cleaned_data['installed_device']
device_bay.save()
messages.success(request, "Added {} to {}.".format(device_bay.installed_device, device_bay))
@@ -2854,7 +2891,7 @@ class DeviceBayDepopulateView(generic.ObjectEditView):
form = ConfirmationForm(request.POST)
if form.is_valid():
device_bay.snapshot()
removed_device = device_bay.installed_device
device_bay.installed_device = None
device_bay.save()
@@ -2948,6 +2985,7 @@ class InventoryItemBulkRenameView(generic.BulkRenameView):
class InventoryItemBulkDeleteView(generic.BulkDeleteView):
queryset = InventoryItem.objects.all()
filterset = filtersets.InventoryItemFilterSet
table = tables.InventoryItemTable
template_name = 'dcim/inventoryitem_bulk_delete.html'
@@ -3005,6 +3043,7 @@ class InventoryItemRoleBulkDeleteView(generic.BulkDeleteView):
queryset = InventoryItemRole.objects.annotate(
inventoryitem_count=count_related(InventoryItem, 'role'),
)
filterset = filtersets.InventoryItemRoleFilterSet
table = tables.InventoryItemRoleTable

View File

@@ -97,6 +97,12 @@ class CustomFieldSerializer(ValidatedModelSerializer):
'validation_minimum', 'validation_maximum', 'validation_regex', 'choices', 'created', 'last_updated',
]
def validate_type(self, value):
if self.instance and self.instance.type != value:
raise serializers.ValidationError('Changing the type of custom fields is not supported.')
return value
def get_data_type(self, obj):
types = CustomFieldTypeChoices
if obj.type == types.TYPE_INTEGER:

View File

@@ -318,6 +318,10 @@ class ScriptViewSet(ViewSet):
"""
Run a Script identified as "<module>.<script>" and return the pending JobResult as the result
"""
if not request.user.has_perm('extras.run_script'):
raise PermissionDenied("This user does not have permission to run scripts.")
script = self._get_script(pk)()
input_serializer = serializers.ScriptInputSerializer(data=request.data)

View File

@@ -44,7 +44,8 @@ class Condition:
bool: (EQ, CONTAINS),
int: (EQ, GT, GTE, LT, LTE, CONTAINS),
float: (EQ, GT, GTE, LT, LTE, CONTAINS),
list: (EQ, IN, CONTAINS)
list: (EQ, IN, CONTAINS),
type(None): (EQ,)
}
def __init__(self, attr, value, op=EQ, negate=False):

8
netbox/extras/fields.py Normal file
View File

@@ -0,0 +1,8 @@
from django.db.models import TextField
class CachedValueField(TextField):
"""
Currently a dummy field to prevent custom lookups being applied globally to TextField.
"""
pass

View File

@@ -210,6 +210,9 @@ class ImageAttachmentFilterSet(BaseFilterSet):
class JournalEntryFilterSet(NetBoxModelFilterSet):
created = django_filters.DateTimeFromToRangeFilter()
assigned_object_type = ContentTypeFilter()
assigned_object_type_id = django_filters.ModelMultipleChoiceFilter(
queryset=ContentType.objects.all()
)
created_by_id = django_filters.ModelMultipleChoiceFilter(
queryset=User.objects.all(),
label=_('User (ID)'),
@@ -458,6 +461,9 @@ class ObjectChangeFilterSet(BaseFilterSet):
)
time = django_filters.DateTimeFromToRangeFilter()
changed_object_type = ContentTypeFilter()
changed_object_type_id = django_filters.ModelMultipleChoiceFilter(
queryset=ContentType.objects.all()
)
user_id = django_filters.ModelMultipleChoiceFilter(
queryset=User.objects.all(),
label=_('User (ID)'),

View File

@@ -2,6 +2,7 @@ from .model_forms import *
from .filtersets import *
from .bulk_edit import *
from .bulk_import import *
from .misc import *
from .mixins import *
from .config import *
from .scripts import *

View File

@@ -38,8 +38,7 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
('Attributes', ('type', 'content_type_id', 'group_name', 'weight', 'required', 'ui_visibility')),
)
content_type_id = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_fields'),
queryset=ContentType.objects.filter(FeatureQuery('custom_fields').get_query()),
required=False,
label=_('Object type')
)
@@ -79,8 +78,7 @@ class JobResultFilterForm(SavedFiltersMixin, FilterForm):
)
obj_type = ContentTypeChoiceField(
label=_('Object Type'),
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('job_results'), # TODO: This doesn't actually work
queryset=ContentType.objects.filter(FeatureQuery('job_results').get_query()),
required=False,
)
status = MultipleChoiceField(
@@ -135,8 +133,7 @@ class CustomLinkFilterForm(SavedFiltersMixin, FilterForm):
('Attributes', ('content_types', 'enabled', 'new_window', 'weight')),
)
content_types = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_links'),
queryset=ContentType.objects.filter(FeatureQuery('custom_links').get_query()),
required=False
)
enabled = forms.NullBooleanField(
@@ -162,8 +159,7 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
('Attributes', ('content_types', 'mime_type', 'file_extension', 'as_attachment')),
)
content_types = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('export_templates'),
queryset=ContentType.objects.filter(FeatureQuery('export_templates').get_query()),
required=False
)
mime_type = forms.CharField(
@@ -187,8 +183,7 @@ class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
('Attributes', ('content_types', 'enabled', 'shared', 'weight')),
)
content_types = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('export_templates'),
queryset=ContentType.objects.filter(FeatureQuery('export_templates').get_query()),
required=False
)
enabled = forms.NullBooleanField(
@@ -215,8 +210,7 @@ class WebhookFilterForm(SavedFiltersMixin, FilterForm):
('Events', ('type_create', 'type_update', 'type_delete')),
)
content_type_id = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('webhooks'),
queryset=ContentType.objects.filter(FeatureQuery('webhooks').get_query()),
required=False,
label=_('Object type')
)

View File

@@ -0,0 +1,14 @@
from django import forms
__all__ = (
'RenderMarkdownForm',
)
class RenderMarkdownForm(forms.Form):
"""
Provides basic validation for markup to be rendered.
"""
text = forms.CharField(
required=False
)

View File

@@ -1,6 +1,8 @@
import json
from django import forms
from django.db.models import Q
from django.contrib.contenttypes.models import ContentType
from django.http import QueryDict
from django.utils.translation import gettext as _
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
@@ -36,7 +38,7 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
object_type = ContentTypeChoiceField(
queryset=ContentType.objects.all(),
# TODO: Come up with a canonical way to register suitable models
limit_choices_to=FeatureQuery('webhooks'),
limit_choices_to=FeatureQuery('webhooks').get_query() | Q(app_label='auth', model__in=['user', 'group']),
required=False,
help_text=_("Type of the related object (for object/multi-object fields only)")
)
@@ -63,6 +65,13 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
'ui_visibility': StaticSelect(),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Disable changing the type of a CustomField as it almost universally causes errors if custom field data is already present.
if self.instance.pk:
self.fields['type'].disabled = True
class CustomLinkForm(BootstrapMixin, forms.ModelForm):
content_types = ContentTypeMultipleChoiceField(
@@ -128,11 +137,10 @@ class SavedFilterForm(BootstrapMixin, forms.ModelForm):
def __init__(self, *args, initial=None, **kwargs):
# Convert any parameters delivered via initial data to a dictionary
# Convert any parameters delivered via initial data to JSON data
if initial and 'parameters' in initial:
if type(initial['parameters']) is str:
# TODO: Make a utility function for this
initial['parameters'] = dict(QueryDict(initial['parameters']).lists())
initial['parameters'] = json.loads(initial['parameters'])
super().__init__(*args, initial=initial, **kwargs)
@@ -254,6 +262,15 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
'tenants', 'tags',
)
def __init__(self, *args, initial=None, **kwargs):
# Convert data delivered via initial data to JSON data
if initial and 'data' in initial:
if type(initial['data']) is str:
initial['data'] = json.loads(initial['data'])
super().__init__(*args, initial=initial, **kwargs)
class ImageAttachmentForm(BootstrapMixin, forms.ModelForm):

View File

@@ -3,6 +3,7 @@ from django.utils import timezone
from django.utils.translation import gettext as _
from utilities.forms import BootstrapMixin, DateTimePicker, SelectDurationWidget
from utilities.utils import local_now
__all__ = (
'ReportForm',
@@ -24,16 +25,20 @@ class ReportForm(BootstrapMixin, forms.Form):
help_text=_("Interval at which this report is re-run (in minutes)")
)
def clean_schedule_at(self):
def clean(self):
scheduled_time = self.cleaned_data['schedule_at']
if scheduled_time and scheduled_time < timezone.now():
if scheduled_time and scheduled_time < local_now():
raise forms.ValidationError(_('Scheduled time must be in the future.'))
return scheduled_time
# When interval is used without schedule at, raise an exception
if self.cleaned_data['interval'] and not scheduled_time:
self.cleaned_data['schedule_at'] = local_now()
return self.cleaned_data
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Annotate the current system time for reference
now = timezone.now().strftime('%Y-%m-%d %H:%M:%S')
now = local_now().strftime('%Y-%m-%d %H:%M:%S')
self.fields['schedule_at'].help_text += f' (current time: <strong>{now}</strong>)'

View File

@@ -1,8 +1,8 @@
from django import forms
from django.utils import timezone
from django.utils.translation import gettext as _
from utilities.forms import BootstrapMixin, DateTimePicker, SelectDurationWidget
from utilities.utils import local_now
__all__ = (
'ScriptForm',
@@ -34,7 +34,7 @@ class ScriptForm(BootstrapMixin, forms.Form):
super().__init__(*args, **kwargs)
# Annotate the current system time for reference
now = timezone.now().strftime('%Y-%m-%d %H:%M:%S')
now = local_now().strftime('%Y-%m-%d %H:%M:%S')
self.fields['_schedule_at'].help_text += f' (current time: <strong>{now}</strong>)'
# Move _commit and _schedule_at to the end of the form
@@ -45,14 +45,16 @@ class ScriptForm(BootstrapMixin, forms.Form):
self.fields['_interval'] = interval
self.fields['_commit'] = commit
def clean__schedule_at(self):
def clean(self):
scheduled_time = self.cleaned_data['_schedule_at']
if scheduled_time and scheduled_time < timezone.now():
raise forms.ValidationError({
'_schedule_at': _('Scheduled time must be in the future.')
})
if scheduled_time and scheduled_time < local_now():
raise forms.ValidationError(_('Scheduled time must be in the future.'))
return scheduled_time
# When interval is used without schedule at, raise an exception
if self.cleaned_data['_interval'] and not scheduled_time:
self.cleaned_data['_schedule_at'] = local_now()
return self.cleaned_data
@property
def requires_input(self):

View File

@@ -1,4 +1,5 @@
from django.db.models import CharField, Lookup
from django.db.models import CharField, TextField, Lookup
from .fields import CachedValueField
class Empty(Lookup):
@@ -14,4 +15,18 @@ class Empty(Lookup):
return 'CAST(LENGTH(%s) AS BOOLEAN) != %s' % (lhs, rhs), params
class NetContainsOrEquals(Lookup):
"""
This lookup has the same functionality as the one from the ipam app except lhs is cast to inet
"""
lookup_name = 'net_contains_or_equals'
def as_sql(self, qn, connection):
lhs, lhs_params = self.process_lhs(qn, connection)
rhs, rhs_params = self.process_rhs(qn, connection)
params = lhs_params + rhs_params
return 'CAST(%s AS INET) >>= %s' % (lhs, rhs), params
CharField.register_lookup(Empty)
CachedValueField.register_lookup(NetContainsOrEquals)

View File

@@ -37,7 +37,7 @@ class Command(BaseCommand):
f"clearing sessions; skipping."
)
# Delete expired ObjectRecords
# Delete expired ObjectChanges
if options['verbosity']:
self.stdout.write("[*] Checking for expired changelog records")
if config.CHANGELOG_RETENTION:

View File

@@ -15,6 +15,11 @@ class Command(BaseCommand):
nargs='*',
help='One or more apps or models to reindex',
)
parser.add_argument(
'--lazy',
action='store_true',
help="For each model, reindex objects only if no cache entries already exist"
)
def _get_indexers(self, *model_names):
indexers = {}
@@ -60,14 +65,15 @@ class Command(BaseCommand):
raise CommandError("No indexers found!")
self.stdout.write(f'Reindexing {len(indexers)} models.')
# Clear all cached values for the specified models
self.stdout.write('Clearing cached values... ', ending='')
self.stdout.flush()
content_types = [
ContentType.objects.get_for_model(model) for model in indexers.keys()
]
deleted_count = search_backend.clear(content_types)
self.stdout.write(f'{deleted_count} entries deleted.')
# Clear all cached values for the specified models (if not being lazy)
if not kwargs['lazy']:
self.stdout.write('Clearing cached values... ', ending='')
self.stdout.flush()
content_types = [
ContentType.objects.get_for_model(model) for model in indexers.keys()
]
deleted_count = search_backend.clear(content_types)
self.stdout.write(f'{deleted_count} entries deleted.')
# Index models
self.stdout.write('Indexing models')
@@ -76,11 +82,18 @@ class Command(BaseCommand):
model_name = model._meta.model_name
self.stdout.write(f' {app_label}.{model_name}... ', ending='')
self.stdout.flush()
if kwargs['lazy']:
content_type = ContentType.objects.get_for_model(model)
if cached_count := search_backend.count(object_types=[content_type]):
self.stdout.write(f'Skipping (found {cached_count} existing).')
continue
i = search_backend.cache(model.objects.iterator(), remove_existing=False)
if i:
self.stdout.write(f'{i} entries cached.')
else:
self.stdout.write(f'None found.')
self.stdout.write(f'No objects found.')
msg = f'Completed.'
if total_count := search_backend.size:

View File

@@ -20,7 +20,7 @@ from utilities.utils import NetBoxFakeRequest
class Command(BaseCommand):
help = "Run a script in Netbox"
help = "Run a script in NetBox"
def add_arguments(self, parser):
parser.add_argument(

View File

@@ -1,25 +1,9 @@
import sys
import uuid
import django.db.models.deletion
import django.db.models.lookups
from django.core import management
from django.db import migrations, models
def reindex(apps, schema_editor):
# Build the search index (except during tests)
if 'test' not in sys.argv:
management.call_command(
'reindex',
'circuits',
'dcim',
'extras',
'ipam',
'tenancy',
'virtualization',
'wireless',
)
import extras.fields
class Migration(migrations.Migration):
@@ -49,7 +33,7 @@ class Migration(migrations.Migration):
('object_id', models.PositiveBigIntegerField()),
('field', models.CharField(max_length=200)),
('type', models.CharField(max_length=30)),
('value', models.TextField()),
('value', extras.fields.CachedValueField()),
('weight', models.PositiveSmallIntegerField(default=1000)),
('object_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='contenttypes.contenttype')),
],
@@ -57,8 +41,4 @@ class Migration(migrations.Migration):
'ordering': ('weight', 'object_type', 'object_id'),
},
),
migrations.RunPython(
code=reindex,
reverse_code=migrations.RunPython.noop
),
]

View File

@@ -5,7 +5,7 @@ from django.urls import reverse
from extras.querysets import ConfigContextQuerySet
from netbox.models import ChangeLoggedModel
from netbox.models.features import WebhooksMixin
from netbox.models.features import CloningMixin, WebhooksMixin
from utilities.utils import deepmerge
@@ -19,7 +19,7 @@ __all__ = (
# Config contexts
#
class ConfigContext(WebhooksMixin, ChangeLoggedModel):
class ConfigContext(CloningMixin, WebhooksMixin, ChangeLoggedModel):
"""
A ConfigContext represents a set of arbitrary data available to any Device or VirtualMachine matching its assigned
qualifiers (region, site, etc.). For example, the data stored in a ConfigContext assigned to site A and tenant B
@@ -108,6 +108,12 @@ class ConfigContext(WebhooksMixin, ChangeLoggedModel):
objects = ConfigContextQuerySet.as_manager()
clone_fields = (
'weight', 'is_active', 'regions', 'site_groups', 'sites', 'locations', 'device_types',
'roles', 'platforms', 'cluster_types', 'cluster_groups', 'clusters', 'tenant_groups',
'tenants', 'tags', 'data',
)
class Meta:
ordering = ['weight', 'name']

View File

@@ -20,10 +20,12 @@ from netbox.models import ChangeLoggedModel
from netbox.models.features import CloningMixin, ExportTemplatesMixin, WebhooksMixin
from netbox.search import FieldTypes
from utilities import filters
from utilities.forms import (
CSVChoiceField, CSVMultipleChoiceField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
JSONField, LaxURLField, StaticSelectMultiple, StaticSelect, add_blank_choice,
from utilities.forms.fields import (
CSVChoiceField, CSVModelChoiceField, CSVModelMultipleChoiceField, CSVMultipleChoiceField, DynamicModelChoiceField,
DynamicModelMultipleChoiceField, JSONField, LaxURLField,
)
from utilities.forms.widgets import DatePicker, StaticSelectMultiple, StaticSelect
from utilities.forms.utils import add_blank_choice
from utilities.querysets import RestrictedQuerySet
from utilities.validators import validate_regex
@@ -213,7 +215,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
"""
for ct in content_types:
model = ct.model_class()
instances = model.objects.filter(**{f'custom_field_data__{self.name}__isnull': False})
instances = model.objects.filter(custom_field_data__has_key=self.name)
for instance in instances:
del instance.custom_field_data[self.name]
model.objects.bulk_update(instances, ['custom_field_data'], batch_size=100)
@@ -273,10 +275,13 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
'choices': "Choices may be set only for custom selection fields."
})
# A selection field must have at least two choices defined
if self.type == CustomFieldTypeChoices.TYPE_SELECT and self.choices and len(self.choices) < 2:
# Selection fields must have at least one choice defined
if self.type in (
CustomFieldTypeChoices.TYPE_SELECT,
CustomFieldTypeChoices.TYPE_MULTISELECT
) and not self.choices:
raise ValidationError({
'choices': "Selection fields must specify at least two choices."
'choices': "Selection fields must specify at least one choice."
})
# A selection field's default (if any) must be present in its available choices
@@ -410,7 +415,8 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
# Object
elif self.type == CustomFieldTypeChoices.TYPE_OBJECT:
model = self.object_type.model_class()
field = DynamicModelChoiceField(
field_class = CSVModelChoiceField if for_csv_import else DynamicModelChoiceField
field = field_class(
queryset=model.objects.all(),
required=required,
initial=initial
@@ -419,10 +425,11 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
# Multiple objects
elif self.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
model = self.object_type.model_class()
field = DynamicModelMultipleChoiceField(
field_class = CSVModelMultipleChoiceField if for_csv_import else DynamicModelMultipleChoiceField
field = field_class(
queryset=model.objects.all(),
required=required,
initial=initial
initial=initial,
)
# Text

View File

@@ -245,7 +245,7 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogged
)
clone_fields = (
'enabled', 'weight', 'group_name', 'button_class', 'new_window',
'content_types', 'enabled', 'weight', 'group_name', 'button_class', 'new_window',
)
class Meta:
@@ -280,7 +280,7 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogged
}
class ExportTemplate(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
class ExportTemplate(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
content_types = models.ManyToManyField(
to=ContentType,
related_name='export_templates',
@@ -301,7 +301,7 @@ class ExportTemplate(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
max_length=50,
blank=True,
verbose_name='MIME type',
help_text=_('Defaults to <code>text/plain</code>')
help_text=_('Defaults to <code>text/plain; charset=utf-8</code>')
)
file_extension = models.CharField(
max_length=15,
@@ -313,6 +313,10 @@ class ExportTemplate(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
help_text=_("Download file as attachment")
)
clone_fields = (
'content_types', 'template_code', 'mime_type', 'file_extension', 'as_attachment',
)
class Meta:
ordering = ('name',)
@@ -353,7 +357,7 @@ class ExportTemplate(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
Render the template to an HTTP response, delivered as a named file attachment
"""
output = self.render(queryset)
mime_type = 'text/plain' if not self.mime_type else self.mime_type
mime_type = 'text/plain; charset=utf-8' if not self.mime_type else self.mime_type
# Build the response
response = HttpResponse(output, content_type=mime_type)
@@ -406,7 +410,7 @@ class SavedFilter(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
parameters = models.JSONField()
clone_fields = (
'enabled', 'weight',
'content_types', 'weight', 'enabled', 'parameters',
)
class Meta:
@@ -514,7 +518,7 @@ class ImageAttachment(WebhooksMixin, ChangeLoggedModel):
return objectchange
class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, WebhooksMixin, ChangeLoggedModel):
class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, WebhooksMixin, ExportTemplatesMixin, ChangeLoggedModel):
"""
A historical remark concerning an object; collectively, these form an object's journal. The journal is used to
preserve historical context around an object, and complements NetBox's built-in change logging. For example, you
@@ -634,7 +638,8 @@ class JobResult(models.Model):
def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
queue = django_rq.get_queue("default")
rq_queue_name = get_config().QUEUE_MAPPINGS.get(self.obj_type.name, RQ_QUEUE_DEFAULT)
queue = django_rq.get_queue(rq_queue_name)
job = queue.fetch_job(str(self.job_id))
if job:

View File

@@ -4,6 +4,7 @@ from django.contrib.contenttypes.models import ContentType
from django.db import models
from utilities.fields import RestrictedGenericForeignKey
from ..fields import CachedValueField
__all__ = (
'CachedValue',
@@ -36,7 +37,7 @@ class CachedValue(models.Model):
type = models.CharField(
max_length=30
)
value = models.TextField()
value = CachedValueField()
weight = models.PositiveSmallIntegerField(
default=1000
)

View File

@@ -5,7 +5,7 @@ from django.utils.text import slugify
from taggit.models import TagBase, GenericTaggedItemBase
from netbox.models import ChangeLoggedModel
from netbox.models.features import ExportTemplatesMixin, WebhooksMixin
from netbox.models.features import CloningMixin, ExportTemplatesMixin, WebhooksMixin
from utilities.choices import ColorChoices
from utilities.fields import ColorField
@@ -14,7 +14,7 @@ from utilities.fields import ColorField
# Tags
#
class Tag(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel, TagBase):
class Tag(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel, TagBase):
id = models.BigAutoField(
primary_key=True
)
@@ -26,6 +26,10 @@ class Tag(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel, TagBase):
blank=True,
)
clone_fields = (
'color', 'description',
)
class Meta:
ordering = ['name']

View File

@@ -1,4 +1,5 @@
import collections
from importlib import import_module
from django.apps import AppConfig
from django.conf import settings
@@ -21,6 +22,15 @@ registry['plugins'] = {
'template_extensions': collections.defaultdict(list),
}
DEFAULT_RESOURCE_PATHS = {
'search_indexes': 'search.indexes',
'graphql_schema': 'graphql.schema',
'menu': 'navigation.menu',
'menu_items': 'navigation.menu_items',
'template_extensions': 'template_content.template_extensions',
'user_preferences': 'preferences.preferences',
}
#
# Plugin AppConfig class
@@ -58,58 +68,53 @@ class PluginConfig(AppConfig):
# Django apps to append to INSTALLED_APPS when plugin requires them.
django_apps = []
# Default integration paths. Plugin authors can override these to customize the paths to
# integrated components.
search_indexes = 'search.indexes'
graphql_schema = 'graphql.schema'
menu = 'navigation.menu'
menu_items = 'navigation.menu_items'
template_extensions = 'template_content.template_extensions'
user_preferences = 'preferences.preferences'
# Optional plugin resources
search_indexes = None
graphql_schema = None
menu = None
menu_items = None
template_extensions = None
user_preferences = None
def _load_resource(self, name):
# Import from the configured path, if defined.
if path := getattr(self, name, None):
return import_string(f"{self.__module__}.{path}")
# Fall back to the resource's default path. Return None if the module has not been provided.
default_path = f'{self.__module__}.{DEFAULT_RESOURCE_PATHS[name]}'
default_module, resource_name = default_path.rsplit('.', 1)
try:
module = import_module(default_module)
return getattr(module, resource_name, None)
except ModuleNotFoundError:
pass
def ready(self):
plugin_name = self.name.rsplit('.', 1)[-1]
# Register search extensions (if defined)
try:
search_indexes = import_string(f"{self.__module__}.{self.search_indexes}")
for idx in search_indexes:
register_search(idx)
except ImportError:
pass
search_indexes = self._load_resource('search_indexes') or []
for idx in search_indexes:
register_search(idx)
# Register template content (if defined)
try:
template_extensions = import_string(f"{self.__module__}.{self.template_extensions}")
if template_extensions := self._load_resource('template_extensions'):
register_template_extensions(template_extensions)
except ImportError:
pass
# Register navigation menu and/or menu items (if defined)
try:
menu = import_string(f"{self.__module__}.{self.menu}")
if menu := self._load_resource('menu'):
register_menu(menu)
except ImportError:
pass
try:
menu_items = import_string(f"{self.__module__}.{self.menu_items}")
if menu_items := self._load_resource('menu_items'):
register_menu_items(self.verbose_name, menu_items)
except ImportError:
pass
# Register GraphQL schema (if defined)
try:
graphql_schema = import_string(f"{self.__module__}.{self.graphql_schema}")
if graphql_schema := self._load_resource('graphql_schema'):
register_graphql_schema(graphql_schema)
except ImportError:
pass
# Register user preferences (if defined)
try:
user_preferences = import_string(f"{self.__module__}.{self.user_preferences}")
if user_preferences := self._load_resource('user_preferences'):
register_user_preferences(plugin_name, user_preferences)
except ImportError:
pass
@classmethod
def validate(cls, user_config, netbox_version):

View File

@@ -1,5 +1,6 @@
from netbox.navigation import MenuGroup
from utilities.choices import ButtonColorChoices
from django.utils.text import slugify
__all__ = (
'PluginMenu',
@@ -21,7 +22,7 @@ class PluginMenu:
@property
def name(self):
return self.label.replace(' ', '_')
return slugify(self.label)
class PluginMenuItem:

View File

@@ -1,9 +1,11 @@
from importlib import import_module
from django.apps import apps
from django.conf import settings
from django.conf.urls import include
from django.contrib.admin.views.decorators import staff_member_required
from django.urls import path
from django.utils.module_loading import import_string
from django.utils.module_loading import import_string, module_has_submodule
from . import views
@@ -19,24 +21,21 @@ plugin_admin_patterns = [
# Register base/API URL patterns for each plugin
for plugin_path in settings.PLUGINS:
plugin = import_module(plugin_path)
plugin_name = plugin_path.split('.')[-1]
app = apps.get_app_config(plugin_name)
base_url = getattr(app, 'base_url') or app.label
# Check if the plugin specifies any base URLs
try:
if module_has_submodule(plugin, 'urls'):
urlpatterns = import_string(f"{plugin_path}.urls.urlpatterns")
plugin_patterns.append(
path(f"{base_url}/", include((urlpatterns, app.label)))
)
except ImportError:
pass
# Check if the plugin specifies any API URLs
try:
if module_has_submodule(plugin, 'api.urls'):
urlpatterns = import_string(f"{plugin_path}.api.urls.urlpatterns")
plugin_api_patterns.append(
path(f"{base_url}/", include((urlpatterns, f"{app.label}-api")))
)
except ImportError:
pass

View File

@@ -21,7 +21,7 @@ from extras.models import JobResult
from extras.signals import clear_webhooks
from ipam.formfields import IPAddressFormField, IPNetworkFormField
from ipam.validators import MaxPrefixLengthValidator, MinPrefixLengthValidator, prefix_validator
from utilities.exceptions import AbortTransaction
from utilities.exceptions import AbortScript, AbortTransaction
from utilities.forms import add_blank_choice, DynamicModelChoiceField, DynamicModelMultipleChoiceField
from .context_managers import change_logging
from .forms import ScriptForm
@@ -352,6 +352,18 @@ class BaseScript:
# Set initial "commit" checkbox state based on the script's Meta parameter
form.fields['_commit'].initial = getattr(self.Meta, 'commit_default', True)
# Append the default fieldset if defined in the Meta class
default_fieldset = (
('Script Execution Parameters', ('_schedule_at', '_interval', '_commit')),
)
if not hasattr(self.Meta, 'fieldsets'):
fields = (
name for name, _ in self._get_vars().items()
)
self.Meta.fieldsets = (('Script Data', fields),)
self.Meta.fieldsets += default_fieldset
return form
# Logging
@@ -470,6 +482,14 @@ def run_script(data, request, commit=True, *args, **kwargs):
except AbortTransaction:
script.log_info("Database changes have been reverted automatically.")
clear_webhooks.send(request)
except AbortScript as e:
script.log_failure(
f"Script aborted with error: {e}"
)
script.log_info("Database changes have been reverted due to error.")
logger.error(f"Script aborted with error: {e}")
job_result.set_status(JobResultStatusChoices.STATUS_ERRORED)
clear_webhooks.send(request)
except Exception as e:
stacktrace = traceback.format_exc()
script.log_failure(
@@ -516,27 +536,39 @@ def get_scripts(use_names=False):
defined name in place of the actual module name.
"""
scripts = {}
# Iterate through all modules within the scripts path. These are the user-created files in which reports are
# Get all modules within the scripts path. These are the user-created files in which scripts are
# defined.
for importer, module_name, _ in pkgutil.iter_modules([settings.SCRIPTS_ROOT]):
# Use a lock as removing and loading modules is not thread safe
with lock:
# Remove cached module to ensure consistency with filesystem
if module_name in sys.modules:
modules = list(pkgutil.iter_modules([settings.SCRIPTS_ROOT]))
modules_bases = set([name.split(".")[0] for _, name, _ in modules])
# Deleting from sys.modules needs to done behind a lock to prevent race conditions where a module is
# removed from sys.modules while another thread is importing
with lock:
for module_name in list(sys.modules.keys()):
# Everything sharing a base module path with a module in the script folder is removed.
# We also remove all modules with a base module called "scripts". This allows modifying imported
# non-script modules without having to reload the RQ worker.
module_base = module_name.split(".")[0]
if module_base == "scripts" or module_base in modules_bases:
del sys.modules[module_name]
module = importer.find_module(module_name).load_module(module_name)
for importer, module_name, _ in modules:
module = importer.find_module(module_name).load_module(module_name)
if use_names and hasattr(module, 'name'):
module_name = module.name
module_scripts = {}
script_order = getattr(module, "script_order", ())
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]:
# 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

@@ -1,3 +1,5 @@
import json
import django_tables2 as tables
from django.conf import settings
from django.utils.translation import gettext as _
@@ -110,11 +112,14 @@ class SavedFilterTable(NetBoxTable):
enabled = columns.BooleanColumn()
shared = columns.BooleanColumn()
def value_parameters(self, value):
return json.dumps(value)
class Meta(NetBoxTable.Meta):
model = SavedFilter
fields = (
'pk', 'id', 'name', 'slug', 'content_types', 'description', 'user', 'weight', 'enabled', 'shared',
'created', 'last_updated',
'created', 'last_updated', 'parameters'
)
default_columns = (
'pk', 'name', 'content_types', 'user', 'description', 'enabled', 'shared',

View File

@@ -1,13 +1,10 @@
import datetime
from unittest import skipIf
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from django.utils.timezone import make_aware
from django_rq.queues import get_connection
from rest_framework import status
from rq import Worker
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Location, RackRole, Site
from extras.api.views import ReportViewSet, ScriptViewSet
@@ -16,8 +13,6 @@ from extras.reports import Report
from extras.scripts import BooleanVar, IntegerVar, Script, StringVar
from utilities.testing import APITestCase, APIViewTestCases
rq_worker_running = Worker.count(get_connection('default'))
class AppTest(APITestCase):
@@ -101,11 +96,17 @@ class CustomFieldTest(APIViewTestCases.APIViewTestCase):
'content_types': ['dcim.site'],
'name': 'cf6',
'type': 'select',
'choices': ['A', 'B', 'C']
},
]
bulk_update_data = {
'description': 'New description',
}
update_data = {
'content_types': ['dcim.device'],
'name': 'New_Name',
'description': 'New description',
}
@classmethod
def setUpTestData(cls):
@@ -538,16 +539,6 @@ class ReportTest(APITestCase):
self.assertEqual(response.data['name'], self.TestReport.__name__)
@skipIf(not rq_worker_running, "RQ worker not running")
def test_run_report(self):
self.add_permissions('extras.run_script')
url = reverse('extras-api:report-run', kwargs={'pk': None})
response = self.client.post(url, {}, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(response.data['result']['status']['value'], 'pending')
class ScriptTest(APITestCase):
@@ -588,26 +579,6 @@ class ScriptTest(APITestCase):
self.assertEqual(response.data['vars']['var2'], 'IntegerVar')
self.assertEqual(response.data['vars']['var3'], 'BooleanVar')
@skipIf(not rq_worker_running, "RQ worker not running")
def test_run_script(self):
script_data = {
'var1': 'FooBar',
'var2': 123,
'var3': False,
}
data = {
'data': script_data,
'commit': True,
}
url = reverse('extras-api:script-detail', kwargs={'pk': None})
response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(response.data['result']['status']['value'], 'pending')
class CreatedUpdatedFilterTest(APITestCase):

View File

@@ -126,6 +126,16 @@ class ConditionSetTest(TestCase):
with self.assertRaises(ValueError):
ConditionSet({'foo': []})
def test_null_value(self):
cs = ConditionSet({
'and': [
{'attr': 'a', 'value': None, 'op': 'eq', 'negate': True},
]
})
self.assertFalse(cs.eval({'a': None}))
self.assertTrue(cs.eval({'a': "string"}))
self.assertTrue(cs.eval({'a': {"key": "value"}}))
def test_and_single_depth(self):
cs = ConditionSet({
'and': [

View File

@@ -502,7 +502,7 @@ class JournalEntryTestCase(TestCase, ChangeLoggedFilterSetTests):
def test_assigned_object_type(self):
params = {'assigned_object_type': 'dcim.site'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
params = {'assigned_object_type_id': ContentType.objects.get(app_label='dcim', model='site').pk}
params = {'assigned_object_type_id': [ContentType.objects.get(app_label='dcim', model='site').pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
def test_assigned_object(self):
@@ -876,7 +876,5 @@ class ObjectChangeTestCase(TestCase, BaseFilterSetTests):
def test_changed_object_type(self):
params = {'changed_object_type': 'dcim.site'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
def test_changed_object_type_id(self):
params = {'changed_object_type_id': ContentType.objects.get(app_label='dcim', model='site').pk}
params = {'changed_object_type_id': [ContentType.objects.get(app_label='dcim', model='site').pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)

View File

@@ -4,6 +4,7 @@ from django.test import TestCase
from dcim.forms import SiteForm
from dcim.models import Site
from extras.choices import CustomFieldTypeChoices
from extras.forms import SavedFilterForm
from extras.models import CustomField
@@ -77,3 +78,24 @@ class CustomFieldModelFormTest(TestCase):
for field_type, _ in CustomFieldTypeChoices.CHOICES:
self.assertIn(field_type, instance.custom_field_data)
self.assertIsNone(instance.custom_field_data[field_type])
class SavedFilterFormTest(TestCase):
def test_basic_submit(self):
"""
Test form submission and validation
"""
form = SavedFilterForm({
'name': 'test-sf',
'slug': 'test-sf',
'content_types': [ContentType.objects.get_for_model(Site).pk],
'weight': 100,
'parameters': {
"status": [
"active"
]
}
})
self.assertTrue(form.is_valid())
form.save()

View File

@@ -92,4 +92,6 @@ urlpatterns = [
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'),
# Markdown
path('render/markdown/', views.RenderMarkdownView.as_view(), name="render_markdown")
]

View File

@@ -1,7 +1,7 @@
from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
from django.db.models import Count, Q
from django.http import Http404, HttpResponseForbidden
from django.http import Http404, HttpResponseBadRequest, HttpResponseForbidden, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.views.generic import View
@@ -10,6 +10,7 @@ from rq import Worker
from netbox.views import generic
from utilities.htmx import is_htmx
from utilities.templatetags.builtins.filters import render_markdown
from utilities.utils import copy_safe_request, count_related, get_viewname, normalize_querydict, shallow_compare_dict
from utilities.views import ContentTypePermissionRequiredMixin, register_model_view
from . import filtersets, forms, tables
@@ -413,6 +414,7 @@ class ConfigContextDeleteView(generic.ObjectDeleteView):
class ConfigContextBulkDeleteView(generic.BulkDeleteView):
queryset = ConfigContext.objects.all()
filterset = filtersets.ConfigContextFilterSet
table = tables.ConfigContextTable
@@ -885,3 +887,18 @@ class JobResultBulkDeleteView(generic.BulkDeleteView):
queryset = JobResult.objects.all()
filterset = filtersets.JobResultFilterSet
table = tables.JobResultTable
#
# Markdown
#
class RenderMarkdownView(View):
def post(self, request):
form = forms.RenderMarkdownForm(request.POST)
if not form.is_valid():
HttpResponseBadRequest()
rendered = render_markdown(form.cleaned_data['text'])
return HttpResponse(rendered)

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/env python3
# This script will generate a random 50-character string suitable for use as a SECRET_KEY.
import secrets

View File

@@ -16,6 +16,7 @@ from virtualization.models import VirtualMachine, VMInterface
from .choices import *
from .models import *
from rest_framework import serializers
__all__ = (
'AggregateFilterSet',
@@ -405,6 +406,14 @@ class IPRangeFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
field_name='start_address',
lookup_expr='family'
)
start_address = MultiValueCharFilter(
method='filter_address',
label=_('Address'),
)
end_address = MultiValueCharFilter(
method='filter_address',
label=_('Address'),
)
contains = django_filters.CharFilter(
method='search_contains',
label=_('Ranges which contain this prefix or IP'),
@@ -441,9 +450,9 @@ class IPRangeFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = Q(description__icontains=value)
qs_filter = Q(description__icontains=value) | Q(start_address__contains=value) | Q(end_address__contains=value)
try:
ipaddress = str(netaddr.IPNetwork(value.strip()).cidr)
ipaddress = str(netaddr.IPNetwork(value.strip()))
qs_filter |= Q(start_address=ipaddress)
qs_filter |= Q(end_address=ipaddress)
except (AddrFormatError, ValueError):
@@ -461,6 +470,12 @@ class IPRangeFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
except (AddrFormatError, ValueError):
return queryset.none()
def filter_address(self, queryset, name, value):
try:
return queryset.filter(**{f'{name}__net_in': value})
except ValidationError:
return queryset.none()
class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
family = django_filters.NumberFilter(
@@ -585,7 +600,33 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
return queryset.none()
return queryset.filter(q)
def parse_inet_addresses(self, value):
'''
Parse networks or IP addresses and cast to a format
acceptable by the Postgres inet type.
Skips invalid values.
'''
parsed = []
for addr in value:
if netaddr.valid_ipv4(addr) or netaddr.valid_ipv6(addr):
parsed.append(addr)
continue
try:
network = netaddr.IPNetwork(addr)
parsed.append(str(network))
except (AddrFormatError, ValueError):
continue
return parsed
def filter_address(self, queryset, name, value):
# Let's first parse the addresses passed
# as argument. If they are all invalid,
# we return an empty queryset
value = self.parse_inet_addresses(value)
if (len(value) == 0):
return queryset.none()
try:
return queryset.filter(address__net_in=value)
except ValidationError:
@@ -852,6 +893,17 @@ class VLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
queryset=VirtualMachine.objects.all(),
method='get_for_virtualmachine'
)
l2vpn_id = django_filters.ModelMultipleChoiceFilter(
field_name='l2vpn_terminations__l2vpn',
queryset=L2VPN.objects.all(),
label=_('L2VPN (ID)'),
)
l2vpn = django_filters.ModelMultipleChoiceFilter(
field_name='l2vpn_terminations__l2vpn__identifier',
queryset=L2VPN.objects.all(),
to_field_name='identifier',
label=_('L2VPN'),
)
class Meta:
model = VLAN
@@ -912,6 +964,18 @@ class ServiceFilterSet(NetBoxModelFilterSet):
to_field_name='name',
label=_('Virtual machine (name)'),
)
ipaddress_id = django_filters.ModelMultipleChoiceFilter(
field_name='ipaddresses',
queryset=IPAddress.objects.all(),
label=_('IP address (ID)'),
)
ipaddress = django_filters.ModelMultipleChoiceFilter(
field_name='ipaddresses__address',
queryset=IPAddress.objects.all(),
to_field_name='address',
label=_('IP address'),
)
port = NumericArrayFilter(
field_name='ports',
lookup_expr='contains'

View File

@@ -10,7 +10,7 @@ from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant
from utilities.forms import (
add_blank_choice, BulkEditNullBooleanSelect, CommentField, DynamicModelChoiceField, NumericArrayField,
SmallTextarea, StaticSelect, DynamicModelMultipleChoiceField,
StaticSelect, DynamicModelMultipleChoiceField
)
__all__ = (
@@ -48,7 +48,6 @@ class VRFBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@@ -69,7 +68,6 @@ class RouteTargetBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@@ -116,7 +114,6 @@ class ASNBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@@ -145,7 +142,6 @@ class AggregateBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@@ -227,7 +223,6 @@ class PrefixBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@@ -266,7 +261,6 @@ class IPRangeBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@@ -314,7 +308,6 @@ class IPAddressBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@@ -359,7 +352,6 @@ class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@@ -442,7 +434,6 @@ class VLANBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@@ -474,7 +465,6 @@ class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@@ -504,7 +494,6 @@ class L2VPNBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)

View File

@@ -443,7 +443,8 @@ class L2VPNImportForm(NetBoxModelImportForm):
class Meta:
model = L2VPN
fields = ('identifier', 'name', 'slug', 'type', 'description', 'comments', 'tags')
fields = ('identifier', 'name', 'slug', 'tenant', 'type', 'description',
'comments', 'tags')
class L2VPNTerminationImportForm(NetBoxModelImportForm):

View File

@@ -413,7 +413,7 @@ class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id')),
('Attributes', ('group_id', 'status', 'role_id', 'vid')),
('Attributes', ('group_id', 'status', 'role_id', 'vid', 'l2vpn_id')),
('Tenant', ('tenant_group_id', 'tenant_id')),
)
region_id = DynamicModelMultipleChoiceField(
@@ -458,6 +458,11 @@ class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
required=False,
label='VLAN ID'
)
l2vpn_id = DynamicModelMultipleChoiceField(
queryset=L2VPN.objects.all(),
required=False,
label=_('L2VPN')
)
tag = TagFilterField(model)

View File

@@ -578,6 +578,7 @@ class FHRPGroupForm(NetBoxModelForm):
role=FHRP_PROTOCOL_ROLE_MAPPINGS.get(self.cleaned_data['protocol'], IPAddressRoleChoices.ROLE_VIP),
assigned_object=instance
)
ipaddress.populate_custom_field_defaults()
ipaddress.save()
# Check that the new IPAddress conforms with any assigned object-level permissions

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