Compare commits

...

243 Commits

Author SHA1 Message Date
Jeremy Stretch
7c09259b7d Merge pull request #9569 from netbox-community/develop
Release v3.2.5
2022-06-20 11:35:48 -04:00
jeremystretch
7ba268946a Release v3.2.5 2022-06-20 11:22:36 -04:00
jeremystretch
8074ca95bd Closes #9453: Disable default loggers when running tests 2022-06-20 11:17:15 -04:00
jeremystretch
d691ea92d0 #9525: Add button colors 2022-06-20 09:44:59 -04:00
jeremystretch
903a3e1a9c Changelog for #8944, #9108, #9556 2022-06-20 08:34:05 -04:00
Jeremy Stretch
4109113319 Merge pull request #9558 from kkthxbye-code/fix-9556
Fixes #9556 - Don't close select field when multiple select (v2)
2022-06-20 08:25:49 -04:00
Jeremy Stretch
872c11502f Merge pull request #9522 from kkthxbye-code/fix-9108
Fixes #9108 & #8944 - Sanitize HTML after rendering markdown
2022-06-20 08:16:43 -04:00
jeremystretch
10cb4f359a Changelog for #9417, #9517, #9525 2022-06-20 08:06:49 -04:00
Jeremy Stretch
45babf162e Merge pull request #9562 from huntabyte/feature-9525
Closes: #9525 - Add split button functionality to table rows
2022-06-20 08:03:20 -04:00
Jeremy Stretch
3434428357 Merge pull request #9563 from huntabyte/feature-9417
Closes #9417: Pre-populate manufacturer when adding modules to devices
2022-06-20 07:49:10 -04:00
Jeremy Stretch
25128bd06f Merge pull request #9564 from huntabyte/feature-9517
Closes #9517: Linkify Power Port on Power Outlet Object View
2022-06-20 07:45:30 -04:00
Hunter Johnston
e7620b0dd0 Closes #9517: Linkify Power Port on Power Outlet Object View 2022-06-19 22:10:01 -04:00
Hunter Johnston
65683d0df1 Closes #9417: Pre-populate manufacturer when adding modules to devices 2022-06-19 20:00:15 -04:00
Hunter Johnston
ff2ccfd670 Closes #9525: Added split button functionality to ActionsColumn 2022-06-19 19:12:52 -04:00
Hunter Johnston
b1ec703ba9 Implemented feature #9525 2022-06-18 23:08:06 -04:00
Hunter Johnston
3d785d836d Implemented feature #9525 2022-06-18 23:05:18 -04:00
kkthxbye-code
7c79c90cd2 Sanitize HTML after rendering markdown 2022-06-17 23:16:57 +02:00
kkthxbye-code
a6e285316a Don't close select field when multiple select 2022-06-17 22:53:51 +02:00
jeremystretch
e6018cd38f Closes #9534: Add VLAN group selector to interface bulk edit forms 2022-06-17 14:51:45 -04:00
jeremystretch
92a6523bf3 Fixes #9549: Fix device counts for rack list under rack role view 2022-06-17 14:40:37 -04:00
jeremystretch
2815eca260 Fixes #9503: Hyperlinks in ack elevation SVGs must always use absolute URLs 2022-06-17 14:36:55 -04:00
jeremystretch
896ebf01b1 Changelog for #8704, #9533, #9374, #9466, #9537 2022-06-17 14:04:57 -04:00
Jeremy Stretch
a71b2e231b Merge pull request #9309 from CroogQT/multiselect
Multiselect
2022-06-17 14:00:54 -04:00
Jeremy Stretch
56f3aaf7c8 Merge pull request #9473 from kkthxbye-code/fix-9466
Fixes #9466 - Clear webhook queue on script failure
2022-06-17 13:58:26 -04:00
Jeremy Stretch
87a9cc0b9e Merge pull request #9527 from kkthxbye-code/fix-9374
Partially fixes #9374 - Implement a custom paginator for DeviceViewSet to improve config_context load times
2022-06-17 13:44:37 -04:00
Jeremy Stretch
972a1fdd14 Merge pull request #9543 from hagbarddenstore/fix-9537
Replace None in templates with placeholder filter
2022-06-17 13:16:46 -04:00
Jeremy Stretch
723954f0d9 Merge pull request #9547 from kkthxbye-code/fix-9533
Fixes #9533 - Move markdown documentation to docs
2022-06-17 13:14:44 -04:00
kkthxbye-code
cf76d5c46a Move markdown documentation to docs 2022-06-16 22:26:37 +02:00
Kim Johansson
e8b970608e Replace None in templates with placeholder filter
To be consistent, all uses of — or None is replaced with the
placeholder filter.

Fixes #9537
2022-06-15 22:33:21 +02:00
Jeremy Stretch
83fdfaa0eb Fixes #9524: Correct order of VLAN fields under VM interface creation form 2022-06-13 19:14:29 -04:00
Jeremy Stretch
86c35a403a Changelog for #9501, #9512 2022-06-13 19:05:16 -04:00
Jeremy Stretch
e96c382138 Merge pull request #9520 from kkthxbye-code/fix-9501
Fixes #9501 - Add configuration option JINJA2_FILTERS
2022-06-13 18:55:25 -04:00
kkthxbye-code
8ef74192ec Implement a custom paginator for DeviceViewSet
Running count on the annotated query for loading config_context is slow. The custom paginator removes the annotation before getting the count.
2022-06-13 20:45:08 +02:00
Jeremy Stretch
135ce93d03 Merge pull request #9523 from kkthxbye-code/fix-9512
Fixes #9512 -  Add distinct to Site search to prevent duplicates when search matches ASN
2022-06-13 12:12:34 -04:00
kkthxbye
f13b090b5c Add distinct to Site search to prevent duplicates when search matches ASN 2022-06-13 07:56:31 +02:00
kkthxbye-code
d1aa820856 Add configuration option JINJA2_FILTERS 2022-06-10 23:13:49 +02:00
jeremystretch
c81c3d11ed Fixes #9495: Correct link to contacts in contact groups table column 2022-06-09 10:20:44 -04:00
jeremystretch
36c65b7b22 Closes #8893: Include count of IP ranges under tenant view 2022-06-07 11:12:40 -04:00
jeremystretch
8a4c808be5 Closes #8882: Support filtering IP addresses by multiple parent prefixes 2022-06-07 11:00:14 -04:00
jeremystretch
6ed2dbf172 Fixes #9486: Fix redirect URL when adding device components from the module view 2022-06-07 10:06:19 -04:00
jeremystretch
1b8350fe48 Add warning against bumping stale issues 2022-06-07 09:59:59 -04:00
jeremystretch
15080aad66 Changelog for #9480, #9484 2022-06-07 08:51:53 -04:00
Jeremy Stretch
7a7f7c5dec Merge pull request #9483 from kkthxbye-code/fix-9480
Fixes #9480 - Make the Service and ServiceTemplate tables sortable by ports
2022-06-07 08:46:13 -04:00
Jeremy Stretch
c958208c47 Merge pull request #9485 from kkthxbye-code/fix-9484
Fixes #9484 - List services listening on all IPs in IPAddressView
2022-06-07 08:43:14 -04:00
kkthxbye-code
9f4e565b8e List services listening on all IPs in IPAddressView 2022-06-06 16:28:33 +02:00
kkthxbye-code
bb2d21abdd Make the Service and ServiceTemplate tables sortable by ports 2022-06-05 10:31:21 +02:00
kkthxbye
e2eb7fdfb5 Clear webhook queue on script failure 2022-06-03 13:03:58 +02:00
jeremystretch
3fbf1f7e71 PRVB 2022-05-31 15:31:22 -04:00
Jeremy Stretch
9d308e6246 Merge pull request #9454 from netbox-community/develop
Release v3.2.4
2022-05-31 15:28:13 -04:00
jeremystretch
6c035eb13d Release v3.2.4 2022-05-31 15:08:33 -04:00
jeremystretch
b0a56a71bb Fixes #9291: Improve data validation for MultiObjectVar script fields 2022-05-31 13:37:14 -04:00
jeremystretch
201b9f635e Fixes #9402: Fix custom field population when creating a virtual chassis 2022-05-31 13:26:25 -04:00
jeremystretch
f1d0d8e57a Fixes #9407: Clean up display of prefixes values when exporting prefixes list 2022-05-31 12:23:22 -04:00
jeremystretch
5838a9f3a0 Closes #9451: Add export_raw argument for TemplateColumn 2022-05-31 12:20:39 -04:00
jeremystretch
998a392bd3 Fixes #9425: Fix bulk import for object and multi-object custom fields 2022-05-31 11:37:30 -04:00
jeremystretch
a0a87fc4c0 Changelog for #9420, #9430 2022-05-31 09:14:23 -04:00
Jeremy Stretch
6c0b4c66c0 Merge pull request #9438 from kkthxbye-code/fix-9420
Fixes #9420: Allow script inheritance
2022-05-31 09:12:49 -04:00
Jeremy Stretch
2c8a1ed69c Merge pull request #9435 from kkthxbye-code/fix-9430
Fixes #9435 - Make sure initial data is passed as array for DynamicModelChoiceFields
2022-05-31 09:09:42 -04:00
kkthxbye-code
fe899d9d7c Iterate base classes when searching for ScriptVariables 2022-05-28 11:29:18 +02:00
kkthxbye-code
6d3cded579 Make sure initial data is passed as array for DynamicModelChoiceFields 2022-05-27 20:41:50 +02:00
jeremystretch
2e5a5f71ba Changelog for #9277 2022-05-24 16:00:18 -04:00
Jeremy Stretch
72516c00fb Merge pull request #9415 from tyler-8/csrf_cookie_setting
Add optional CSRF_COOKIE_NAME setting, update example config, and docs.
2022-05-24 15:58:00 -04:00
tyler-8
d34d5869be Add optional CSRF_COOKIE_NAME setting, update example config, and docs. 2022-05-24 10:57:38 -04:00
jeremystretch
72726c784a Clean up imports 2022-05-24 09:56:14 -04:00
jeremystretch
662b02e2d8 Closes #9347: Include services in global search 2022-05-24 09:49:36 -04:00
jeremystretch
a9ec1a7b4e Closes #9379: Redirect to virtual chassis view after adding a member device 2022-05-24 09:20:05 -04:00
jeremystretch
f03c5037c4 Fixes #9387: Ensure ActionsColumn extra_buttons are always displayed 2022-05-24 09:14:25 -04:00
Jeremy Stretch
a52c68f4c2 Merge pull request #9406 from lastorel/9365-rolefilter
add `role_id` attribute to filter form of InventoryItem
2022-05-24 08:52:06 -04:00
jeremystretch
a73dda35e8 Bump stale to v5 2022-05-24 08:39:43 -04:00
lastorel
0570203891 add role attribute to filter inventoryitems 2022-05-22 17:22:28 +03:00
jeremystretch
3b3247592e Changelog for #9098 2022-05-18 08:42:20 -04:00
Jeremy Stretch
17292324a3 Merge pull request #9383 from bluikko/patch-1
Add other power, front/rear port types
2022-05-18 08:41:01 -04:00
bluikko
e5aa9d47f7 Add other power, front/rear port types
Fixes #9098
2022-05-18 15:08:08 +07:00
jeremystretch
9e1d8beaf0 Changelog for #9239, #9358 2022-05-16 09:56:02 -04:00
jeremystretch
17fb562740 #9239: Organize contact form fields 2022-05-16 09:55:17 -04:00
Jeremy Stretch
2910aaeec0 Merge pull request #9362 from kkthxbye-code/fix-9358
Fixes #9358 - Annotate provider table in ASN view with count_circuits
2022-05-16 09:31:45 -04:00
Jeremy Stretch
aeef12cdc0 Merge pull request #9364 from kkthxbye-code/fix-9239
Fixes #9239 - Add contact_group to ContactModelFilterSet
2022-05-16 09:28:46 -04:00
kkthxbye-code
8ad203f97a Added contact_group to region, site, manufacturer, tenant filters 2022-05-14 17:53:40 +02:00
kkthxbye-code
aba4e03d3b Add contact_group to ContactModelFilterSet 2022-05-14 17:48:37 +02:00
kkthxbye-code
6a99b36cce Fix provider table in ASN view when ordering by circuit_count 2022-05-14 12:01:49 +02:00
Daniel Sheppard
f415d81049 Fixes #8374 - Display device type and asset tag if name is blank but asset tag is populated 2022-05-13 09:49:07 -05:00
Daniel Sheppard
24ff360ee0 Fixes #8922 - Add service list to IP address view 2022-05-13 09:40:24 -05:00
Daniel Sheppard
2a4c728375 Merge remote-tracking branch 'origin/develop' into develop 2022-05-13 09:29:12 -05:00
Daniel Sheppard
752a497218 Fixes #9094 - Fix partial address search within Prefix and Aggregate filters 2022-05-13 09:28:24 -05:00
Daniel Sheppard
1d4409c703 Fixes #9094 - Fix partial address search within Prefix and Aggregate filters 2022-05-13 09:08:00 -05:00
jeremystretch
3c7c8c8776 PRVB 2022-05-12 14:14:40 -04:00
Jeremy Stretch
bb2235b05e Merge pull request #9354 from netbox-community/develop
Release v3.2.3
2022-05-12 14:09:30 -04:00
jeremystretch
a6aec9ebac Release v3.2.3 2022-05-12 13:53:26 -04:00
jeremystretch
5f3695d2d0 Closes #8805: Add "mixed" option for device airflow indication 2022-05-12 12:18:58 -04:00
jeremystretch
ad12ad4a77 Closes #9221: Add definition list support for Markdown 2022-05-12 11:05:34 -04:00
jeremystretch
37903776fd Fixes #9296: Improve Markdown link sanitization 2022-05-12 10:41:29 -04:00
jeremystretch
c4c93ee346 Closes #9343: Add Ubiquiti SmartPower power outlet type 2022-05-12 10:17:29 -04:00
jeremystretch
72b2ab03cc #9340: Introduce config parameters for Sentry sampling rates 2022-05-12 10:00:57 -04:00
jeremystretch
4cefe26f80 #9340: Add default Sentry DSN 2022-05-12 09:35:13 -04:00
jeremystretch
991950650b Add Sentry as a sponsor 2022-05-11 16:44:26 -04:00
Jeremy Stretch
8cc94689d8 Merge pull request #9342 from netbox-community/9340-sentry
Closes #9340: Enable Sentry integration
2022-05-11 16:26:46 -04:00
jeremystretch
312d6c890e Add sentry-sdk as a dependency 2022-05-11 15:20:18 -04:00
jeremystretch
c146596564 Implement a custom 404 handler to enable Sentry reporting 2022-05-11 14:27:18 -04:00
jeremystretch
6f5c2f1e29 Enable & document Sentry integration 2022-05-11 14:13:50 -04:00
jeremystretch
1726593fb0 Introduce MODULE_TOKEN constant 2022-05-11 10:37:04 -04:00
jeremystretch
e8575495db Changelog for #9190, #9314 2022-05-11 10:31:04 -04:00
devon-mar
cffc064a33 Add device & vm to FHRPGroupAssignmentFilterSet (#9314)
* Add device & vm to `FHRPGroupAssignmentFilterSet`

* Apply suggestions from code review

* Update netbox/ipam/tests/test_filtersets.py

* Update netbox/ipam/filtersets.py

Co-authored-by: Jeremy Stretch <jstretch@ns1.com>
2022-05-11 10:27:50 -04:00
Jeremy Stretch
3dda7e2da2 Merge pull request #9331 from kkthxbye-code/fix-9001
Fixes #9001 & #9190 - Add form validation to model installation
2022-05-11 10:04:06 -04:00
jeremystretch
22f1863475 Add security document 2022-05-11 09:12:07 -04:00
jeremystretch
bdb21da26e Fixes #9330: Add missing module_type field to REST API serializers for modular device component templates 2022-05-11 08:57:19 -04:00
jeremystretch
e759e123ac Fixes #9333: Annotate unit on interface speed field 2022-05-11 08:09:51 -04:00
kkthxbye-code
d858eceb38 Fix pep8 2022-05-10 17:53:01 +02:00
kkthxbye-code
af126fe7e3 Added form validation to model installation
Raises a ValidationError whenever installation would cause a foreign key violation.
2022-05-10 17:50:33 +02:00
CroogQT
124e93f737 yarn bundle. 2022-05-06 12:16:45 -07:00
CroogQT
fbd933b56a prettier fixes 2022-05-06 11:44:34 -07:00
CroogQT
9c5355a300 added JSDoc comments 2022-05-06 11:43:18 -07:00
CroogQT
491a4e7d78 various punctuation and spacing fixes 2022-05-06 11:33:00 -07:00
jeremystretch
39a9ebaeee Fixes #9313: Remove HTML code from CSV output of many-to-many relationships 2022-05-06 10:26:02 -04:00
jeremystretch
9b4e016fe4 Fixes #9306: Include VC master interfaces when selecting a LAG/bridge for a VC member interface 2022-05-06 09:47:52 -04:00
jeremystretch
422ec7ecec Fixes #9311: Permit creating contact assignment without a priority via the REST API 2022-05-06 09:25:40 -04:00
kkthxbye
a06a280534 Merge pull request #9312 from kkthxbye-code/fix-9310
Fixes #9310 - Remove stray characters from Config Context tab
2022-05-06 13:58:47 +02:00
kkthxbye
1358469375 Remove stray characters from Config Context tab 2022-05-06 08:01:15 +02:00
CroogQT
90d8395a2c Fixed variable type issue...i think. 2022-05-05 15:24:16 -07:00
CroogQT
11f7e3099d fixed text deselection and refactor 2022-05-05 15:01:40 -07:00
CroogQT
ef29bffb72 is this supposed to be ignored? 2022-05-05 13:27:09 -07:00
CroogQT
3effa37fa7 click event calls multiselect function 2022-05-05 13:24:50 -07:00
CroogQT
1493c920fd silly text highlight workaround... 2022-05-05 13:24:12 -07:00
CroogQT
ea9258d36c added main multi-select function 2022-05-05 13:23:43 -07:00
CroogQT
db142061ff clicking a PkCheckbox updates state 2022-05-05 12:37:28 -07:00
CroogQT
c536944a10 now exports multiselect function 2022-05-05 12:36:17 -07:00
CroogQT
ae7ddecaa6 now exports previousPkCheck.ts 2022-05-05 12:14:15 -07:00
CroogQT
2e38e62101 create store to store previously checked element 2022-05-05 12:13:02 -07:00
CroogQT
2979a64ce3 add file, skeleton from "select all" 2022-05-05 12:11:02 -07:00
jeremystretch
bddca8e232 Changelog for #9280 2022-05-05 14:14:49 -04:00
Jeremy Stretch
e9bf6a7bc5 Merge pull request #9281 from kkthxbye-code/adopt-module-component
Fixes #9280 - Add option to adopt existing DeviceComponents
2022-05-05 10:29:20 -04:00
kkthxbye-code
9c3dfdfd14 Fix test_module_component_adoption 2022-05-05 09:30:13 +02:00
kkthxbye-code
c52aa2196d Prefetch installed components when adding modules 2022-05-04 23:21:03 +02:00
kkthxbye-code
81c7fe2084 Don't adopt components already belonging to a module 2022-05-04 22:59:28 +02:00
jeremystretch
0301aec409 Closes #9260: Apply user preferences to tables under object detail views 2022-05-04 15:46:13 -04:00
jeremystretch
015bc48345 #8998: Add region filter for rack reservations; Add filter tests 2022-05-04 14:29:36 -04:00
jeremystretch
da1aabdfc1 Changelog for #8894, #8998, #9122; PEP8 fix 2022-05-04 14:19:09 -04:00
Jeremy Stretch
c2fe2ba56f Merge pull request #9147 from minitriga/issue_8998
Closes #8998: Add site group filter to racks
2022-05-04 14:16:35 -04:00
Jeremy Stretch
52b18393eb Merge pull request #9150 from minitriga/issue_8894
Closes #8894: Add first and last name username user api select
2022-05-04 14:09:38 -04:00
Jeremy Stretch
b172ae65d2 Merge pull request #9256 from kkthxbye-code/fix-9122
Fix #9122 - Clear the cache when running the upgrade script
2022-05-04 14:03:48 -04:00
jeremystretch
eab187fb6b Changelog for #9267, #9278 2022-05-04 13:59:38 -04:00
Jeremy Stretch
502a14e820 Merge pull request #9288 from huntabyte/develop
Closes #9278: Linkify device type in manufacturer table
2022-05-04 13:43:16 -04:00
kkthxbye
7de27c69c0 Fix PEP8 2022-05-04 09:16:19 +02:00
kkthxbye
f455f91ea3 Add view test for module component adoption 2022-05-04 08:58:42 +02:00
Hunter Johnston
bdaefc0e4d Closes #9278: Linkify device type in manufacturer table 2022-05-03 18:34:32 -04:00
kkthxbye-code
8040804c75 Allow mixture of component replication and adoption 2022-05-03 22:03:12 +02:00
minitriga
7cd840610b Update netbox/dcim/forms/filtersets.py
Co-authored-by: Jeremy Stretch <jstretch@ns1.com>
2022-05-03 11:47:37 +01:00
minitriga
15e91908e8 Update netbox/dcim/forms/filtersets.py
Co-authored-by: Jeremy Stretch <jstretch@ns1.com>
2022-05-03 11:47:32 +01:00
Alex Gittings
0a9ba3b2e6 add get_display to users serializer 2022-05-03 10:45:08 +00:00
minitriga
535606a185 Update netbox/users/api/nested_serializers.py
Co-authored-by: Jeremy Stretch <jstretch@ns1.com>
2022-05-03 09:01:06 +01:00
minitriga
25c266e4de Update netbox/users/api/nested_serializers.py
Co-authored-by: Jeremy Stretch <jstretch@ns1.com>
2022-05-03 09:00:52 +01:00
kkthxbye-code
977ccb01f2 Formatting: Remove whitespace on blank line 2022-05-02 21:55:34 +02:00
kkthxbye-code
c2a6a1c125 Create module components in bulk 2022-05-02 21:37:37 +02:00
Jeremy Stretch
f6402a8b62 Merge pull request #9275 from kkthxbye-code/fix-9267
Fixes #9267 - Fix early terminated tuple in IPAddressRoleChoices
2022-05-02 09:19:11 -04:00
kkthxbye
30d4097fd8 Fix early terminated tuple in IPAddressRoleChoices 2022-05-02 12:09:49 +02:00
kkthxbye-code
3fb967b482 Add ability to adopt components when adding a module 2022-04-30 02:19:11 +02:00
kkthxbye
9f3846ec5f Clear the cache when running the upgrade script 2022-04-29 09:19:37 +02:00
kkthxbye
7b5625a722 Add management command for clearing cache 2022-04-29 09:19:19 +02:00
jeremystretch
152d5a3b9a PRVB 2022-04-28 15:06:27 -04:00
Jeremy Stretch
50b6ded6f0 Merge pull request #9253 from netbox-community/develop
Release v3.2.2
2022-04-28 15:00:11 -04:00
jeremystretch
5ee3ee6181 Release v3.2.2 2022-04-28 14:39:02 -04:00
Jeremy Stretch
cd3e901a60 Merge pull request #9248 from kkthxbye-code/fix-9138
Fixes #9138 - Prevent searching when pressing enter in Quick Search
2022-04-28 12:24:07 -04:00
kkthxbye
314c41f47f Prevent searching when pressing enter in Quick Search 2022-04-28 07:47:04 +02:00
jeremystretch
a1c1532614 Changelog for #4264 2022-04-27 15:36:29 -04:00
Jeremy Stretch
ffef78d426 Merge pull request #9112 from sc68cal/rfc_4291
Closes #4264: Do not allocate subnet router anycast for IPv6 prefixes
2022-04-27 15:30:05 -04:00
jeremystretch
8153406dd0 Fixes #9227: Fix related object assignment when recording change record for interfaces 2022-04-27 14:12:20 -04:00
Sean M. Collins
b5613a2cc6 Do not allocate subnet router anycast in certain IPv6 prefixes 2022-04-26 14:54:52 -04:00
jeremystretch
6a225e53f5 Fixes #9222: Fix circuit ID display under cable view 2022-04-25 21:09:20 -04:00
jeremystretch
6b73d22da1 Changelog for #8959 2022-04-25 10:11:51 -04:00
Jeremy Stretch
8b81c10f84 Merge pull request #9201 from kkthxbye-code/fix-8959
Fixes #8959: Add lock around script loading to prevent race condition
2022-04-25 10:10:06 -04:00
jeremystretch
a6a1bec437 Closes #9218: Update documentation links with docs.netbox.dev 2022-04-25 09:48:39 -04:00
jeremystretch
562d1bfcd0 Fixes #9194: Support position assignment when add module bays to multiple devices 2022-04-25 08:41:38 -04:00
jeremystretch
4f86d6a690 Fixes #9206: Show header for comments field under module & module type creation views 2022-04-25 08:33:41 -04:00
jeremystretch
e63a191373 Closes #9214: Linkify cluster counts in cluster type & group tables 2022-04-25 08:23:21 -04:00
Jeremy Stretch
74b5e55643 Merge pull request #9200 from kkthxbye-code/fix-9189
Fixes #9189:  Correct custom validators docs
2022-04-25 08:11:15 -04:00
Jeremy Stretch
405d0ab972 Merge pull request #9199 from kkthxbye-code/fix-8941
Fixes #8941: Fix apiSelect scrolling while zoomed in chrome
2022-04-25 08:09:04 -04:00
kkthxbye-code
84e4156259 Add lock around script loading to prevent race condition 2022-04-22 21:21:01 +02:00
kkthxbye-code
50428c3f01 Correct custom validators docs 2022-04-22 20:42:29 +02:00
kkthxbye-code
a91c46b4c0 UI: Fix apiSelect scrolling while zoomed in chrome 2022-04-22 20:33:46 +02:00
Kevin Meijer
8315883db9 Adds Ubiquiti SmartPower to the power port types (#9193)
Co-authored-by: Kevin Meijer <kevinmeijer@brightfish.nl>
2022-04-22 08:11:31 -04:00
jeremystretch
d22f9000d6 Add troubleshooting section to Azure AD guide 2022-04-19 10:00:41 -04:00
jeremystretch
bb99cee48a Changelog & test for #9060 2022-04-18 13:14:30 -04:00
minitriga
a3805fe04d Closes #9060: Implement modulebay, iventory items and device bay filters (#9146)
* Closes #9060: Implement modulebay, iventory items and device bay filters

* add blank line
2022-04-18 13:07:41 -04:00
jeremystretch
d4f1cb5d6a Fixes #9158: Do not list tags field for CSV forms which do not support tag assignment 2022-04-18 09:39:36 -04:00
jeremystretch
118bf5152c Fixes #9132: Limit location options by selected site when creating a wireless link 2022-04-18 09:02:03 -04:00
jeremystretch
41244dc677 Closes #9152: Annotate related object type under custom field view 2022-04-18 08:56:28 -04:00
jeremystretch
671e1aed9f Fixes #9151: Child prefix counts not annotated on aggregates list under RIR view 2022-04-18 08:43:46 -04:00
jeremystretch
1636508a6a Fixes #9156: Fix loading UserConfig data from fixtures 2022-04-18 08:36:41 -04:00
Alex Gittings
bc2491e6b7 Closes #8894: Add first and last name to APISelect widget if set 2022-04-15 21:50:24 +00:00
Alex Gittings
69a1cc8759 Closes #8998: Add site group filter to racks 2022-04-15 20:36:40 +00:00
jeremystretch
c21db0ff6a Closes #9137: Add SSO configuration guide for Okta 2022-04-15 16:03:36 -04:00
jeremystretch
a889b3a4be Add title for missing okta-openidconnect backend 2022-04-15 15:04:44 -04:00
jeremystretch
10c7fdb618 Closes #9136: Add SSO configuration guide for Microsoft Azure AD 2022-04-14 21:42:47 -04:00
jeremystretch
7be0a1a55f Changelog for #9133 2022-04-14 15:51:26 -04:00
Jeremy Stretch
a98b2fabe0 Merge pull request #9134 from markkuleinio/python-version
Fixes #9133: Require Python 3.8+ to run upgrade.sh
2022-04-14 15:50:18 -04:00
Markku Leiniö
7779b66972 Require Python 3.8+ to run upgrade.sh 2022-04-14 22:01:21 +03:00
jeremystretch
7463c40c40 Restore documentation search function 2022-04-14 14:24:34 -04:00
jeremystretch
996221147e PRVB 2022-04-14 14:15:48 -04:00
Jeremy Stretch
7cd9bcd3f5 Merge pull request #9131 from netbox-community/develop
Release v3.2.1
2022-04-14 14:12:24 -04:00
jeremystretch
fdc018d809 Release v3.2.1 2022-04-14 13:58:18 -04:00
jeremystretch
fa5cf665ce Fixes #9128: Resolve component labels per module bay position when installing modules 2022-04-14 11:13:04 -04:00
jeremystretch
d6df6b444f Closes #9123: Improve appearance of SSO login providers 2022-04-14 10:54:07 -04:00
jeremystretch
78836389f0 Changelog for #8543 & cleanup 2022-04-14 08:37:58 -04:00
Jeremy Stretch
fa4b88a504 Merge pull request #9124 from tyler-8/wlan_site_filter_3_2
Add filters to VLAN selection group for WirelessLAN Form
2022-04-14 08:33:51 -04:00
Tyler Bigler
1a374a1669 Add filters to VLAN selection group 2022-04-14 00:58:17 -04:00
jeremystretch
01ba1b8c03 Fixes #9118: Fix validation error when importing VM child interfaces 2022-04-13 13:43:18 -04:00
jeremystretch
f09a5aacae Changelog for #9110 2022-04-12 14:17:08 -04:00
Jeremy Stretch
95d084d36d Merge pull request #9110 from JoeIzzard/develop
Closes #8415: Adds Neutrik power connectors
2022-04-12 12:40:31 -04:00
Joe Izzard
d35cd18745 fix: standardisation 2022-04-12 17:06:33 +01:00
jeremystretch
4e493d7836 Changelog for #8920, #8956 2022-04-12 12:05:09 -04:00
Jeremy Stretch
68b8cca540 Merge pull request #8957 from kkthxbye-code/save-job-results
Fix #8956: Save old JobResults
2022-04-12 11:57:48 -04:00
kkthxbye-code
c216405a81 Change default JOBRESULT_RETENTION from 0 to 90 2022-04-12 11:42:47 -04:00
kkthxbye
aa2ec3b9c9 Add dynamic config JOBRESULT_RETENTION
and cleanup functionality to the housekeeping script
2022-04-12 11:42:47 -04:00
kkthxbye
f13a00b2dd Save old JobResults 2022-04-12 11:42:47 -04:00
Jeremy Stretch
8781d03aa7 Merge pull request #9067 from jasonyates/FR8920-nonrackeddevices
Limit amount of nonracked devices displayed
2022-04-12 11:38:14 -04:00
jeremystretch
1266a2f753 Fixes #9116: assigned_to_interface filter for IP addresses should not match FHRP group assignments 2022-04-12 10:07:31 -04:00
jeremystretch
23d2cf1718 Closes #9081: Add fhrpgroup_id filter for IP addresses 2022-04-12 09:56:33 -04:00
jeremystretch
d11031c694 Closes #9099: Enable display of installed module serial & asset tag in module bays list 2022-04-12 09:00:19 -04:00
jeremystretch
916e976297 Tweak changelog for #9061 2022-04-12 08:22:17 -04:00
jeremystretch
27a9313396 #9096: Correct getattr() call 2022-04-12 08:13:08 -04:00
jeremystretch
517d0158b6 Fixes #9096: Remove duplicate filter tag when filtering by "none" 2022-04-11 08:51:11 -04:00
jeremystretch
a9e05aec7c Fixes #9100: Include position field in module type YAML export 2022-04-11 08:28:10 -04:00
Joe Izzard
9b3e43cb21 Closes #8415: Adds Neutrik power connectors 2022-04-11 12:20:44 +01:00
Jason Yates
23fddf74b6 Updating to use a single queryset
Updating to use a single queryset for both template variables
2022-04-10 09:06:14 +01:00
Jeremy Stretch
9b8de19fe6 Merge pull request #9085 from stephanblanke/8913-improve-documentation-regarding-vcs
Closes #8913: Improve documentation regarding VC masters interfaces
2022-04-08 14:17:35 -04:00
jeremystretch
1d8b8aad3b Changelog & documentation for #5479 2022-04-08 14:10:21 -04:00
Jeremy Stretch
84c30580aa Merge pull request #9028 from kkthxbye-code/job-timeout
Fixes #5479 - Allow setting individual timeouts on scripts and reports
2022-04-08 13:54:08 -04:00
Stephan Blanke
a5f25726cd Closes #8913: Improve documentation regarding VC masters interfaces 2022-04-08 19:39:18 +02:00
Jeremy Stretch
7a6e047519 Merge pull request #9082 from danielestevez/doc-remote-auth-default-group
Conflicting documentation on remote default group
2022-04-08 10:44:38 -04:00
jeremystretch
1e65ef0c1a Fixes #9055: Restore ability to move inventory item to other device 2022-04-08 09:41:37 -04:00
jeremystretch
2269bf0167 Fixes #9079: Fail validation when an inventory item is assigned as its own parent 2022-04-08 09:08:55 -04:00
jeremystretch
5526f8e3dc Fix dumpdata ordering for VRFs 2022-04-07 21:19:38 -04:00
Jason Yates
2781b8535c Update nonracked_devices.html
Fixing issue where Displaying 10 of 10 would appear when it's not required.
2022-04-07 08:32:50 +01:00
Jason Yates
c3d9910e08 Limit amount of nonracked devices displayed
Fixes #8920

Limits the amount of non-racked devices on Site and Location view to 10 and provides a link to the device list this is pre-filtered to the relevant site or location.
2022-04-07 08:21:13 +01:00
jeremystretch
b9f6a5625f Update supported Python versions 2022-04-06 20:30:29 -04:00
jeremystretch
f4e78b0ea6 Fixes #9065: Min/max VID should not be required when filtering VLAN groups 2022-04-06 20:29:28 -04:00
jeremystretch
d93e944c07 Closes #8973: Display VLAN group count under site view 2022-04-06 20:12:14 -04:00
jeremystretch
6760533a10 Fixes #8931: Copy assigned tenant when cloning a location 2022-04-06 20:01:09 -04:00
Daniel Sheppard
85e65edb7d Fixes #9057 - Fix missing instance counts for module types 2022-04-06 13:46:31 -05:00
Daniel Sheppard
523390cd8e #9061 - Change inheritance order for DeviceComponentFilterSets 2022-04-06 13:35:22 -05:00
jeremystretch
ea197eff5f PRVB 2022-04-05 17:12:13 -04:00
kkthxbye-code
69b4d0d44b #9028 - Fix pep8 syntax 2022-04-04 18:13:13 +02:00
kkthxbye-code
36d6ae33d1 Allow setting individual timeouts on scripts and reports 2022-04-04 18:00:38 +02:00
Daniel Estévez
a3f172fc77 Conflicting documentation on remote default group
In the current documentation we have two seemingly conflicting sentences:
* REMOTE_AUTH_DEFAULT_GROUPS: (Requires REMOTE_AUTH_ENABLED.)
* REMOTE_AUTH_ENABLED: (REMOTE_AUTH_DEFAULT_GROUPS will not function if REMOTE_AUTH_ENABLED is enabled)
2022-02-25 11:30:13 -05:00
201 changed files with 2792 additions and 723 deletions

View File

@@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v3.2.0
placeholder: v3.2.5
validations:
required: true
- type: dropdown
@@ -22,9 +22,9 @@ body:
label: Python version
description: What version of Python are you currently running?
options:
- "3.7"
- "3.8"
- "3.9"
- "3.10"
validations:
required: true
- type: textarea

View File

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

View File

@@ -8,7 +8,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v4
- uses: actions/stale@v5
with:
close-issue-message: >
This issue has been automatically closed due to lack of activity. In an
@@ -27,7 +27,10 @@ jobs:
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. NetBox
is governed by a small group of core maintainers which means not all opened
issues may receive direct feedback. Please see our [contributing guide](https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md).
issues may receive direct feedback. **Do not** attempt to circumvent this
process by "bumping" the issue; doing so will result in its immediate closure
and you may be barred from participating in any future discussions. Please see
our [contributing guide](https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md).
stale-pr-label: 'pending closure'
stale-pr-message: >
This PR has been automatically marked as stale because it has not had

View File

@@ -1 +1 @@
The changelog has been moved to the [project release notes](https://netbox.readthedocs.io/en/stable/release-notes/).
The changelog has been moved to the [project release notes](https://docs.netbox.dev/en/stable/release-notes/).

View File

@@ -99,7 +99,7 @@ appropriate labels will be applied for categorization.
## Submitting Pull Requests
* If you're interested in contributing to NetBox, be sure to check out our
[getting started](https://netbox.readthedocs.io/en/stable/development/getting-started/)
[getting started](https://docs.netbox.dev/en/stable/development/getting-started/)
documentation for tips on setting up your development environment.
* Be sure to open an issue **before** starting work on a pull request, and
@@ -160,9 +160,9 @@ to aid in issue management.
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 an issue that has been marked stale in
an effort to circumvent the bot: Doing so will not remove the stale label.
(Stale labels can be removed only by maintainers.)
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.
## Maintainer Guidance
@@ -171,7 +171,7 @@ an effort to circumvent the bot: Doing so will not remove the stale label.
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://netbox.readthedocs.io/en/stable/development/).
development must be in accordance with our [development guidance](https://docs.netbox.dev/en/stable/development/).
* Maintainers are expected to attend (where feasible) our biweekly ~30-minute
sync to review agenda items. This meeting provides opportunity to present and

View File

@@ -49,7 +49,7 @@ NetBox runs as a web application atop the [Django](https://www.djangoproject.com
Python framework with a [PostgreSQL](https://www.postgresql.org/) database. For a
complete list of requirements, see `requirements.txt`. The code is available [on GitHub](https://github.com/netbox-community/netbox).
The complete documentation for NetBox can be found at [Read the Docs](https://netbox.readthedocs.io/en/stable/). A public demo instance is available at https://demo.netbox.dev.
The complete documentation for NetBox can be found at [docs.netbox.dev](https://docs.netbox.dev/). A public demo instance is available at https://demo.netbox.dev.
<div align="center">
<h4>Thank you to our sponsors!</h4>
@@ -60,6 +60,8 @@ The complete documentation for NetBox can be found at [Read the Docs](https://ne
&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/)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
[![Stellar Technologies](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/stellar.png)](https://stellar.tech/)
</div>
@@ -71,7 +73,7 @@ The complete documentation for NetBox can be found at [Read the Docs](https://ne
### Installation
Please see [the documentation](https://netbox.readthedocs.io/en/stable/) for
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`.

31
SECURITY.md Normal file
View File

@@ -0,0 +1,31 @@
# Security Policy
## No Warranty
Per the terms of the Apache 2 license, NetBox is offered "as is" and without any guarantee or warranty pertaining to its operation. While every reasonable effort is made by its maintainers to ensure the product remains free of security vulnerabilities, users are ultimately responsible for conducting their own evaluations of each software release.
## Recommendations
Administrators are encouraged to adhere to industry best practices concerning the secure operation of software, such as:
* Do not expose your NetBox installation to the public Internet
* Do not permit multiple users to share an account
* Enforce minimum password complexity requirements for local accounts
* Prohibit access to your database from clients other than the NetBox application
* Keep your deployment updated to the most recent stable release
## Reporting a Suspected Vulnerability
If you believe you've uncovered a security vulnerability and wish to report it confidentially, you may do so via email. Please note that any reported vulnerabilities **MUST** meet all the following conditions:
* Affects the most recent stable release of NetBox, or a current beta release
* Affects a NetBox instance installed and configured per the official documentation
* Is reproducible following a prescribed set of instructions
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.
### Bug Bounties
As NetBox is provided as free open source software, we do not offer any monetary compensation for vulnerability or bug reports, however your contributions are greatly appreciated.

View File

@@ -44,7 +44,8 @@ django-tables2
# User-defined tags for objects
# https://github.com/alex/django-taggit
django-taggit
# Will evaluate v3.0 during NetBox v3.3 beta
django-taggit>=2.1.0,<3.0
# A Django field for representing time zones
# https://github.com/mfogel/django-timezone-field/
@@ -68,8 +69,7 @@ gunicorn
# Platform-agnostic template rendering engine
# https://github.com/pallets/jinja
# Pin to v3.0 for mkdocstrings
Jinja2<3.1
Jinja2
# Simple markup language for rendering HTML
# https://github.com/Python-Markdown/markdown
@@ -85,7 +85,7 @@ mkdocs-material
# Introspection for embedded code
# https://github.com/mkdocstrings/mkdocstrings
mkdocstrings<=0.17.0
mkdocstrings[python-legacy]
# Library for manipulating IP prefixes and addresses
# https://github.com/netaddr/netaddr
@@ -103,6 +103,10 @@ psycopg2-binary
# https://github.com/yaml/pyyaml
PyYAML
# Sentry SDK
# https://github.com/getsentry/sentry-python
sentry-sdk
# Social authentication framework
# https://github.com/python-social-auth/social-core
social-auth-core
@@ -122,3 +126,7 @@ tablib
# Timezone data (required by django-timezone-field on Python 3.9+)
# https://github.com/python/tzdata
tzdata
# HTML sanitizer
# https://github.com/mozilla/bleach
bleach

View File

@@ -1,6 +1,6 @@
[Unit]
Description=NetBox Request Queue Worker
Documentation=https://netbox.readthedocs.io/en/stable/
Documentation=https://docs.netbox.dev/
After=network-online.target
Wants=network-online.target

View File

@@ -1,6 +1,6 @@
[Unit]
Description=NetBox WSGI Service
Documentation=https://netbox.readthedocs.io/en/stable/
Documentation=https://docs.netbox.dev/
After=network-online.target
Wants=network-online.target

View File

@@ -0,0 +1,88 @@
# Microsoft Azure AD
This guide explains how to configure single sign-on (SSO) support for NetBox using [Microsoft Azure Active Directory (AD)](https://azure.microsoft.com/en-us/services/active-directory/) as an authentication backend.
## Azure AD Configuration
### 1. Create a test user (optional)
Create a new user in AD to be used for testing. You can skip this step if you already have a suitable account created.
### 2. Create an app registration
Under the Azure Active Directory dashboard, navigate to **Add > App registration**.
![Add an app registration](../../media/authentication/azure_ad_add_app_registration.png)
Enter a name for the registration (e.g. "NetBox") and ensure that the "single tenant" option is selected.
Under "Redirect URI", select "Web" for the platform and enter the path to your NetBox installation, ending with `/oauth/complete/azuread-oauth2/`. Note that this URI **must** begin with `https://` unless you are referencing localhost (for development purposes).
![App registration parameters](../../media/authentication/azure_ad_app_registration.png)
Once finished, make note of the application (client) ID; this will be used when configuring NetBox.
![Completed app registration](../../media/authentication/azure_ad_app_registration_created.png)
!!! tip "Multitenant authentication"
NetBox also supports multitenant authentication via Azure AD, however it requires a different backend and an additional configuration parameter. Please see the [`python-social-auth` documentation](https://python-social-auth.readthedocs.io/en/latest/backends/azuread.html#tenant-support) for details concerning multitenant authentication.
### 3. Create a secret
When viewing the newly-created app registration, click the "Add a certificate or secret" link under "Client credentials". Under the "Client secrets" tab, click the "New client secret" button.
![Add a client secret](../../media/authentication/azure_ad_add_client_secret.png)
You can optionally specify a description and select a lifetime for the secret.
![Client secret parameters](../../media/authentication/azure_ad_client_secret.png)
Once finished, make note of the secret value (not the secret ID); this will be used when configuring NetBox.
![Client secret parameters](../../media/authentication/azure_ad_client_secret_created.png)
## NetBox Configuration
### 1. Enter configuration parameters
Enter the following configuration parameters in `configuration.py`, substituting your own values:
```python
REMOTE_AUTH_BACKEND = 'social_core.backends.azuread.AzureADOAuth2'
SOCIAL_AUTH_AZUREAD_OAUTH2_KEY = '{APPLICATION_ID}'
SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET = '{SECRET_VALUE}'
```
### 2. Restart NetBox
Restart the NetBox services so that the new configuration takes effect. This is typically done with the command below:
```no-highlight
sudo systemctl restart netbox
```
## Testing
Log out of NetBox if already authenticated, and click the "Log In" button at top right. You should see the normal login form as well as an option to authenticate using Azure AD. Click that link.
![NetBox Azure AD login form](../../media/authentication/netbox_azure_ad_login.png)
You should be redirected to Microsoft's authentication portal. Enter the username/email and password of your test account to continue. You may also be prompted to grant this application access to your account.
![NetBox Azure AD login form](../../media/authentication/azure_ad_login_portal.png)
If successful, you will be redirected back to the NetBox UI, and will be logged in as the AD user. You can verify this by navigating to your profile (using the button at top right).
This user account has been replicated locally to NetBox, and can now be assigned groups and permissions within the NetBox admin UI.
## Troubleshooting
### Redirect URI does not Match
Azure requires that the authenticating client request a redirect URI that matches what you've configured for the app in step two. This URI **must** begin with `https://` (unless using `localhost` for the domain).
If Azure complains that the requested URI starts with `http://` (not HTTPS), it's likely that your HTTP server is misconfigured or sitting behind a load balancer, so NetBox is not aware that HTTPS is being use. To force the use of an HTTPS redirect URI, set `SOCIAL_AUTH_REDIRECT_IS_HTTPS = True` in `configuration.py` per the [python-social-auth docs](https://python-social-auth.readthedocs.io/en/latest/configuration/settings.html#processing-redirects-and-urlopen).
### Not Logged in After Authenticating
If you are redirected to the NetBox UI after authenticating successfully, but are _not_ logged in, double-check the configured backend and app registration. The instructions in this guide pertain only to the `azuread.AzureADOAuth2` backend using a single-tenant app registration.

View File

@@ -0,0 +1,70 @@
# Okta
This guide explains how to configure single sign-on (SSO) support for NetBox using [Okta](https://www.okta.com/) as an authentication backend.
## Okta Configuration
!!! tip "Okta developer account"
Okta offers free developer accounts at <https://developer.okta.com/>.
### 1. Create a test user (optional)
Create a new user in the Okta admin portal to be used for testing. You can skip this step if you already have a suitable account created.
### 2. Create an app registration
Within the Okta administration dashboard, navigate to **Applications > Applications**, and click the "Create App Integration" button. Select "OIDC" as the sign-in method, and "Web application" for the application type.
![Create an app registration](../../media/authentication/okta_create_app_registration.png)
On the next page, give the app integration a name (e.g. "NetBox") and specify the sign-in and sign-out URIs. These URIs should follow the formats below:
* Sign-in URI: `https://{netbox}/oauth/complete/okta-openidconnect/`
* Sign-out URI: `https://{netbox}/oauth/disconnect/okta-openidconnect/`
![Web app integration](../../media/authentication/okta_web_app_integration.png)
Under "Assignments," select the controlled access setting most appropriate for your organization. Click "Save" to complete the creation.
Once finished, note the following parameters. These will be used to configured NetBox.
* Client ID
* Client secret
* Okta domain
![Okta integration parameters](../../media/authentication/okta_integration_parameters.png)
## NetBox Configuration
### 1. Enter configuration parameters
Enter the following configuration parameters in `configuration.py`, substituting your own values:
```python
REMOTE_AUTH_BACKEND = 'social_core.backends.okta_openidconnect.OktaOpenIdConnect'
SOCIAL_AUTH_OKTA_OPENIDCONNECT_KEY = '{Client ID}'
SOCIAL_AUTH_OKTA_OPENIDCONNECT_SECRET = '{Client secret}'
SOCIAL_AUTH_OKTA_OPENIDCONNECT_API_URL = 'https://{Okta domain}/oauth2/'
```
### 2. Restart NetBox
Restart the NetBox services so that the new configuration takes effect. This is typically done with the command below:
```no-highlight
sudo systemctl restart netbox
```
## Testing
Log out of NetBox if already authenticated, and click the "Log In" button at top right. You should see the normal login form as well as an option to authenticate using Okta. Click that link.
![NetBox Okta login form](../../media/authentication/netbox_okta_login.png)
You should be redirected to Okta's authentication portal. Enter the username/email and password of your test account to continue. You may also be prompted to grant this application access to your account.
![Okta login portal](../../media/authentication/okta_login_portal.png)
If successful, you will be redirected back to the NetBox UI, and will be logged in as the Okta user. You can verify this by navigating to your profile (using the button at top right).
This user account has been replicated locally to NetBox, and can now be assigned groups and permissions within the NetBox admin UI.

View File

@@ -4,7 +4,7 @@
Local user accounts and groups can be created in NetBox under the "Authentication and Authorization" section of the administrative user interface. This interface is available only to users with the "staff" permission enabled.
At a minimum, each user account must have a username and password set. User accounts may also denote a first name, last name, and email address. [Permissions](./permissions.md) may also be assigned to users and/or groups within the admin UI.
At a minimum, each user account must have a username and password set. User accounts may also denote a first name, last name, and email address. [Permissions](../permissions.md) may also be assigned to users and/or groups within the admin UI.
## Remote Authentication
@@ -16,7 +16,7 @@ NetBox may be configured to provide user authenticate via a remote backend in ad
REMOTE_AUTH_BACKEND = 'netbox.authentication.LDAPBackend'
```
NetBox includes an authentication backend which supports LDAP. See the [LDAP installation docs](../installation/6-ldap.md) for more detail about this backend.
NetBox includes an authentication backend which supports LDAP. See the [LDAP installation docs](../../installation/6-ldap.md) for more detail about this backend.
### HTTP Header Authentication

View File

@@ -0,0 +1,46 @@
# Error Reporting
## Sentry
### Enabling Error Reporting
NetBox v3.2.3 and later support native integration with [Sentry](https://sentry.io/) for automatic error reporting. To enable this functionality, simply set `SENTRY_ENABLED` to True in `configuration.py`. Errors will be sent to a Sentry ingestor maintained by the NetBox team for analysis.
```python
SENTRY_ENABLED = True
```
### Using a Custom DSN
If you prefer instead to use your own Sentry ingestor, you'll need to first create a new project under your Sentry account to represent your NetBox deployment and obtain its corresponding data source name (DSN). This looks like a URL similar to the example below:
```
https://examplePublicKey@o0.ingest.sentry.io/0
```
Once you have obtained a DSN, configure Sentry in NetBox's `configuration.py` file with the following parameters:
```python
SENTRY_ENABLED = True
SENTRY_DSN = "https://examplePublicKey@o0.ingest.sentry.io/0"
```
### Assigning Tags
You can optionally attach one or more arbitrary tags to the outgoing error reports if desired by setting the `SENTRY_TAGS` parameter:
```python
SENTRY_TAGS = {
"custom.foo": "123",
"custom.bar": "abc",
}
```
!!! warning "Reserved tag prefixes"
Avoid using any tag names which begin with `netbox.`, as this prefix is reserved by the NetBox application.
### Testing
Once the configuration has been saved, restart the NetBox service.
To test Sentry operation, try generating a 404 (page not found) error by navigating to an invalid URL, such as `https://netbox/404-error-testing`. (Be sure that debug mode has been disabled.) After receiving a 404 response from the NetBox server, you should see the issue appear shortly in Sentry.

View File

@@ -4,6 +4,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/dynamic-settings.md#changelog_retention)
* Deleting job result records older than the configured [retention time](../configuration/dynamic-settings.md#jobresult_retention)
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

@@ -43,6 +43,18 @@ changes in the database indefinitely.
---
## JOBRESULT_RETENTION
Default: 90
The number of days to retain job results (scripts and reports). Set this to `0` to retain
job results in the database indefinitely.
!!! warning
If enabling indefinite job results retention, it is recommended to periodically delete old entries. Otherwise, the database may eventually exceed capacity.
---
## CUSTOM_VALIDATORS
This is a mapping of models to [custom validators](../customization/custom-validation.md) that have been defined locally to enforce custom validation logic. An example is provided below:

View File

@@ -0,0 +1,54 @@
# Error Reporting Settings
## SENTRY_DSN
Default: None
Defines a Sentry data source name (DSN) for automated error reporting. `SENTRY_ENABLED` must be True for this parameter to take effect. For example:
```
SENTRY_DSN = "https://examplePublicKey@o0.ingest.sentry.io/0"
```
---
## SENTRY_ENABLED
Default: False
Set to True to enable automatic error reporting via [Sentry](https://sentry.io/).
---
## SENTRY_SAMPLE_RATE
Default: 1.0 (all)
The sampling rate for errors. Must be a value between 0 (disabled) and 1.0 (report on all errors).
---
## SENTRY_TAGS
An optional dictionary of tag names and values to apply to Sentry error reports.For example:
```
SENTRY_TAGS = {
"custom.foo": "123",
"custom.bar": "abc",
}
```
!!! warning "Reserved tag prefixes"
Avoid using any tag names which begin with `netbox.`, as this prefix is reserved by the NetBox application.
---
## SENTRY_TRACES_SAMPLE_RATE
Default: 0 (disabled)
The sampling rate for transactions. Must be a value between 0 (disabled) and 1.0 (report on all transactions).
!!! warning "Consider performance implications"
A high sampling rate for transactions can induce significant performance penalties. If transaction reporting is desired, it is recommended to use a relatively low sample rate of 10% to 20% (0.1 to 0.2).

View File

@@ -66,6 +66,14 @@ CORS_ORIGIN_WHITELIST = [
---
## CSRF_COOKIE_NAME
Default: `csrftoken`
The name of the cookie to use for the cross-site request forgery (CSRF) authentication token. See the [Django documentation](https://docs.djangoproject.com/en/stable/ref/settings/#csrf-cookie-name) for more detail.
---
## CSRF_TRUSTED_ORIGINS
Default: `[]`
@@ -247,6 +255,23 @@ HTTP_PROXIES = {
---
## JINJA2_FILTERS
Default: `{}`
A dictionary of custom jinja2 filters with the key being the filter name and the value being a callable. For more information see the [Jinja2 documentation](https://jinja.palletsprojects.com/en/3.1.x/api/#custom-filters). For example:
```python
def uppercase(x):
return str(x).upper()
JINJA2_FILTERS = {
'uppercase': uppercase,
}
```
---
## INTERNAL_IPS
Default: `('127.0.0.1', '::1')`

View File

@@ -43,7 +43,7 @@ A mapping of permissions to assign a new user account when created using remote
Default: `False`
NetBox can be configured to support remote user authentication by inferring user authentication from an HTTP header set by the HTTP reverse proxy (e.g. nginx or Apache). Set this to `True` to enable this functionality. (Local authentication will still take effect as a fallback.) (`REMOTE_AUTH_DEFAULT_GROUPS` will not function if `REMOTE_AUTH_ENABLED` is enabled)
NetBox can be configured to support remote user authentication by inferring user authentication from an HTTP header set by the HTTP reverse proxy (e.g. nginx or Apache). Set this to `True` to enable this functionality. (Local authentication will still take effect as a fallback.) (`REMOTE_AUTH_DEFAULT_GROUPS` will not function if `REMOTE_AUTH_ENABLED` is disabled)
---

View File

@@ -89,6 +89,12 @@ The checkbox to commit database changes when executing a script is checked by de
commit_default = False
```
### `job_timeout`
Set the maximum allowed runtime for the script. If not set, `RQ_DEFAULT_TIMEOUT` will be used.
!!! info "This feature was introduced in v3.2.1"
## Accessing Request Data
Details of the current HTTP request (the one being made to execute the script) are available as the instance attribute `self.request`. This can be used to infer, for example, the user executing the script and the client IP address:

View File

@@ -105,11 +105,11 @@ from my_validators import Validator1, Validator2, Validator3
CUSTOM_VALIDATORS = {
'dcim.site': (
Validator1,
Validator2,
Validator1(),
Validator2(),
),
'dcim.device': (
Validator3,
Validator3(),
)
}
```

View File

@@ -85,6 +85,20 @@ As you can see, reports are completely customizable. Validation logic can be as
!!! warning
Reports should never alter data: If you find yourself using the `create()`, `save()`, `update()`, or `delete()` methods on objects within reports, stop and re-evaluate what you're trying to accomplish. Note that there are no safeguards against the accidental alteration or destruction of data.
## Report Attributes
### `description`
A human-friendly description of what your report does.
### `job_timeout`
Set the maximum allowed runtime for the report. If not set, `RQ_DEFAULT_TIMEOUT` will be used.
!!! info "This feature was introduced in v3.2.1"
## Logging
The following methods are available to log results within a report:
* log(message)

View File

@@ -40,7 +40,7 @@ You should see output similar to the following:
● netbox.service - NetBox WSGI Service
Loaded: loaded (/etc/systemd/system/netbox.service; enabled; vendor preset: enabled)
Active: active (running) since Mon 2021-08-30 04:02:36 UTC; 14h ago
Docs: https://netbox.readthedocs.io/en/stable/
Docs: https://docs.netbox.dev/
Main PID: 1140492 (gunicorn)
Tasks: 19 (limit: 4683)
Memory: 666.2M

View File

@@ -39,7 +39,7 @@ You can use the command `systemctl status netbox` to verify that the WSGI servic
● netbox.service - NetBox WSGI Service
Loaded: loaded (/etc/systemd/system/netbox.service; enabled; vendor preset: enabled)
Active: active (running) since Sat 2020-10-24 19:23:40 UTC; 25s ago
Docs: https://netbox.readthedocs.io/en/stable/
Docs: https://docs.netbox.dev/
Main PID: 11993 (gunicorn)
Tasks: 6 (limit: 2362)
CGroup: /system.slice/netbox.service

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View File

@@ -2,7 +2,8 @@
A virtual chassis represents a set of devices which share a common control plane. A common example of this is a stack of switches which are connected and configured to operate as a single device. A virtual chassis must be assigned a name and may be assigned a domain.
Each device in the virtual chassis is referred to as a VC member, and assigned a position and (optionally) a priority. VC member devices commonly reside within the same rack, though this is not a requirement. One of the devices may be designated as the VC master: This device will typically be assigned a name, services, and other attributes related to managing the VC.
Each device in the virtual chassis is referred to as a VC member, and assigned a position and (optionally) a priority. VC member devices commonly reside within the same rack, though this is not a requirement. One of the devices may be designated as the VC master: This device will typically be assigned a name, services, virtual interfaces, and other attributes related to managing the VC.
If a VC master is defined, interfaces from all VC members are displayed when navigating to its device interfaces view. This does not include other members interfaces declared as management-only.
!!! note
It's important to recognize the distinction between a virtual chassis and a chassis-based device. A virtual chassis is **not** suitable for modeling a chassis-based switch with removable line cards (such as the Juniper EX9208), as its line cards are _not_ physically autonomous devices.

View File

@@ -85,4 +85,5 @@ The table column classes listed below are supported for use in plugins. These cl
::: netbox.tables.TemplateColumn
selection:
members: false
members:
- __init__

353
docs/reference/markdown.md Normal file
View File

@@ -0,0 +1,353 @@
---
hide:
- toc
---
# Markdown
NetBox supports markdown rendering for certain text fields.
## Syntax
##### Table of Contents
[Headers](#headers)
[Emphasis](#emphasis)
[Lists](#lists)
[Links](#links)
[Images](#images)
[Code Blocks](#code)
[Tables](#tables)
[Blockquotes](#blockquotes)
[Inline HTML](#html)
[Horizontal Rule](#hr)
[Line Breaks](#lines)
<a name="headers"></a>
## Headers
```no-highlight
# H1
## H2
### H3
#### H4
##### H5
###### H6
Alternatively, for H1 and H2, an underline-ish style:
Alt-H1
======
Alt-H2
------
```
# H1
## H2
### H3
#### H4
##### H5
###### H6
<a name="emphasis"></a>
## Emphasis
```no-highlight
Emphasis, aka italics, with *asterisks* or _underscores_.
Strong emphasis, aka bold, with **asterisks** or __underscores__.
Combined emphasis with **asterisks and _underscores_**.
Strikethrough uses two tildes. ~~Scratch this.~~
```
Emphasis, aka italics, with *asterisks* or _underscores_.
Strong emphasis, aka bold, with **asterisks** or __underscores__.
Combined emphasis with **asterisks and _underscores_**.
Strikethrough uses two tildes. ~~Scratch this.~~
<a name="lists"></a>
## Lists
(In this example, leading and trailing spaces are shown with with dots: ⋅)
```no-highlight
1. First ordered list item
2. Another item
⋅⋅* Unordered sub-list.
1. Actual numbers don't matter, just that it's a number
⋅⋅1. Ordered sub-list
4. And another item.
⋅⋅⋅You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown).
⋅⋅⋅To have a line break without a paragraph, you will need to use two trailing spaces.⋅⋅
⋅⋅⋅Note that this line is separate, but within the same paragraph.⋅⋅
⋅⋅⋅(This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.)
* Unordered list can use asterisks
- Or minuses
+ Or pluses
```
1. First ordered list item
2. Another item
* Unordered sub-list.
1. Actual numbers don't matter, just that it's a number
1. Ordered sub-list
4. And another item.
You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown).
To have a line break without a paragraph, you will need to use two trailing spaces.
Note that this line is separate, but within the same paragraph.
(This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.)
* Unordered list can use asterisks
- Or minuses
+ Or pluses
<a name="links"></a>
## Links
There are two ways to create links.
```no-highlight
[I'm an inline-style link](https://www.google.com)
[I'm an inline-style link with title](https://www.google.com "Google's Homepage")
[I'm a reference-style link][Arbitrary case-insensitive reference text]
[You can use numbers for reference-style link definitions][1]
Or leave it empty and use the [link text itself].
URLs and URLs in angle brackets will automatically get turned into links.
http://www.example.com or <http://www.example.com> and sometimes
example.com (but not on Github, for example).
Some text to show that the reference links can follow later.
[arbitrary case-insensitive reference text]: https://www.mozilla.org
[1]: http://slashdot.org
[link text itself]: http://www.reddit.com
```
[I'm an inline-style link](https://www.google.com)
[I'm an inline-style link with title](https://www.google.com "Google's Homepage")
[I'm a reference-style link][Arbitrary case-insensitive reference text]
[You can use numbers for reference-style link definitions][1]
Or leave it empty and use the [link text itself].
URLs and URLs in angle brackets will automatically get turned into links.
http://www.example.com or <http://www.example.com> and sometimes
example.com (but not on Github, for example).
Some text to show that the reference links can follow later.
[arbitrary case-insensitive reference text]: https://www.mozilla.org
[1]: http://slashdot.org
[link text itself]: http://www.reddit.com
<a name="images"></a>
## Images
```
Here's the Netbox logo (hover to see the title text):
Inline-style:
![alt text](/static/netbox_logo.png "Logo Title Text 1")
Reference-style:
![alt text][logo]
[logo]: /static/netbox_logo.png "Logo Title Text 2"
```
Here's the Netbox logo (hover to see the title text):
Inline-style:
![alt text](/static/netbox_logo.png "Logo Title Text 1")
Reference-style:
![alt text][logo]
[logo]: /static/netbox_logo.png "Logo Title Text 2"
<a name="code"></a>
## Code blocks
```
Inline `code` has `back-ticks around` it.
```
Inline `code` has `back-ticks around` it.
Blocks of code are fenced by lines with three back-ticks <code>```</code>
````
```
var s = "Code block";
alert(s);
```
````
```
var s = "Code block";
alert(s);
```
<a name="tables"></a>
## Tables
```no-highlight
Colons can be used to align columns.
| Tables | Are | Cool |
| ------------- |:-------------:| -----:|
| col 3 is | right-aligned | $1600 |
| col 2 is | centered | $12 |
| zebra stripes | are neat | $1 |
There must be at least 3 dashes separating each header cell.
The outer pipes (|) are optional, and you don't need to make the
raw Markdown line up prettily. You can also use inline Markdown.
Markdown | Less | Pretty
--- | --- | ---
*Still* | `renders` | **nicely**
1 | 2 | 3
```
Colons can be used to align columns.
| Tables | Are | Cool |
| ------------- |:-------------:| -----:|
| col 3 is | right-aligned | $1600 |
| col 2 is | centered | $12 |
| zebra stripes | are neat | $1 |
There must be at least 3 dashes separating each header cell. The outer pipes (|) are optional, and you don't need to make the raw Markdown line up prettily. You can also use inline Markdown.
Markdown | Less | Pretty
--- | --- | ---
*Still* | `renders` | **nicely**
1 | 2 | 3
<a name="blockquotes"></a>
## Blockquotes
```no-highlight
> Blockquotes are very handy in email to emulate reply text.
> This line is part of the same quote.
Quote break.
> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote.
```
> Blockquotes are very handy in email to emulate reply text.
> This line is part of the same quote.
Quote break.
> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote.
<a name="html"></a>
## Inline HTML
You can also use raw HTML in your Markdown, and it'll mostly work pretty well.
```no-highlight
<dl>
<dt>Definition list</dt>
<dd>Is something people use sometimes.</dd>
<dt>Markdown in HTML</dt>
<dd>Does *not* work **very** well. Use HTML <em>tags</em>.</dd>
</dl>
```
<dl>
<dt>Definition list</dt>
<dd>Is something people use sometimes.</dd>
<dt>Markdown in HTML</dt>
<dd>Does *not* work **very** well. Use HTML <em>tags</em>.</dd>
</dl>
<a name="hr"></a>
## Horizontal Rule
```
Three or more...
---
Hyphens
***
Asterisks
___
Underscores
```
Three or more...
---
Hyphens
***
Asterisks
___
Underscores
<a name="lines"></a>
## Line Breaks
```
Here's a line for us to start with.
This line is separated from the one above by two newlines, so it will be a *separate paragraph*.
This line is also a separate paragraph, but...
This line is only separated by a single newline, so it's a separate line in the *same paragraph*.
```
Here's a line for us to start with.
This line is separated from the one above by two newlines, so it will be a *separate paragraph*.
This line is also begins a separate paragraph, but...
This line is only separated by a single newline, so it's a separate line in the *same paragraph*.
Based on [Markdown-Cheatsheet](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) by [adam-p](https://github.com/adam-p) licensed under [CC-BY](https://creativecommons.org/licenses/by/3.0/)

View File

@@ -121,7 +121,7 @@ A new API endpoint has been added at `/api/ipam/prefixes/<pk>/available-ips/`. A
#### NAPALM Integration ([#1348](https://github.com/netbox-community/netbox/issues/1348))
The [NAPALM automation](https://github.com/napalm-automation/napalm) library provides an abstracted interface for pulling live data (e.g. uptime, software version, running config, LLDP neighbors, etc.) from network devices. The NetBox API has been extended to support executing read-only NAPALM methods on devices defined in NetBox. To enable this functionality, ensure that NAPALM has been installed (`pip install napalm`) and the `NETBOX_USERNAME` and `NETBOX_PASSWORD` [configuration parameters](https://netbox.readthedocs.io/en/stable/configuration/optional-settings/#netbox_username) have been set in configuration.py.
The [NAPALM automation](https://github.com/napalm-automation/napalm) library provides an abstracted interface for pulling live data (e.g. uptime, software version, running config, LLDP neighbors, etc.) from network devices. The NetBox API has been extended to support executing read-only NAPALM methods on devices defined in NetBox. To enable this functionality, ensure that NAPALM has been installed (`pip install napalm`) and the `NETBOX_USERNAME` and `NETBOX_PASSWORD` [configuration parameters](https://docs.netbox.dev/en/stable/configuration/optional-settings/#netbox_username) have been set in configuration.py.
### Enhancements

View File

@@ -196,7 +196,7 @@ Our second-most popular feature request has arrived! NetBox now supports the cre
#### Custom Validation Reports ([#1511](https://github.com/netbox-community/netbox/issues/1511))
Users can now create custom reports which are run to validate data in NetBox. Reports work very similar to Python unit tests: Each report inherits from NetBox's Report class and contains one or more test method. Reports can be run and retrieved via the web UI, API, or CLI. See [the docs](https://netbox.readthedocs.io/en/stable/miscellaneous/reports/) for more info.
Users can now create custom reports which are run to validate data in NetBox. Reports work very similar to Python unit tests: Each report inherits from NetBox's Report class and contains one or more test method. Reports can be run and retrieved via the web UI, API, or CLI. See [the docs](https://docs.netbox.dev/en/stable/miscellaneous/reports/) for more info.
### Enhancements

View File

@@ -295,7 +295,7 @@ This release upgrades the Django framework to version 2.2.
#### Python 3 Required
As promised, Python 2 support has been completed removed. Python 3.5 or higher is now required to run NetBox. Please see [our Python 3 migration guide](https://netbox.readthedocs.io/en/stable/installation/migrating-to-python3/) for assistance with upgrading.
As promised, Python 2 support has been completed removed. Python 3.5 or higher is now required to run NetBox. Please see [our Python 3 migration guide](https://docs.netbox.dev/en/stable/installation/migrating-to-python3/) for assistance with upgrading.
#### Removed Deprecated User Activity Log

View File

@@ -218,7 +218,7 @@
#### Custom Scripts ([#3415](https://github.com/netbox-community/netbox/issues/3415))
Custom scripts allow for the execution of arbitrary code via the NetBox UI. They can be used to automatically create, manipulate, or clean up objects or perform other tasks within NetBox. Scripts are defined as Python files which contain one or more subclasses of `extras.scripts.Script`. Variable fields can be defined within scripts, which render as form fields within the web UI to prompt the user for input data. Scripts are executed and information is logged via the web UI. Please see [the docs](https://netbox.readthedocs.io/en/stable/customization/custom-scripts/) for more detail.
Custom scripts allow for the execution of arbitrary code via the NetBox UI. They can be used to automatically create, manipulate, or clean up objects or perform other tasks within NetBox. Scripts are defined as Python files which contain one or more subclasses of `extras.scripts.Script`. Variable fields can be defined within scripts, which render as form fields within the web UI to prompt the user for input data. Scripts are executed and information is logged via the web UI. Please see [the docs](https://docs.netbox.dev/en/stable/customization/custom-scripts/) for more detail.
Note: There are currently no API endpoints for this feature. These are planned for the upcoming v2.7 release.

View File

@@ -67,7 +67,7 @@
## v2.7.9 (2020-03-06)
**Note:** This release will deploy a Python virtual environment on upgrade in the `venv/` directory. This will require modifying the paths to your Python and gunicorn executables in the systemd service files. For more detail, please see the [upgrade instructions](https://netbox.readthedocs.io/en/stable/installation/upgrading/).
**Note:** This release will deploy a Python virtual environment on upgrade in the `venv/` directory. This will require modifying the paths to your Python and gunicorn executables in the systemd service files. For more detail, please see the [upgrade instructions](https://docs.netbox.dev/en/stable/installation/upgrading/).
### Enhancements
@@ -418,7 +418,7 @@ to another source before upgrading NetBox to v2.7, as any existing topology maps
#### Supervisor Replaced with systemd ([#2902](https://github.com/netbox-community/netbox/issues/2902))
The NetBox [installation documentation](https://netbox.readthedocs.io/en/stable/installation/) has been updated to
The NetBox [installation documentation](https://docs.netbox.dev/en/stable/installation/) has been updated to
provide instructions for managing the WSGI and RQ services using systemd instead of supervisor. This removes the need to
install supervisor and simplifies administration of the processes.

View File

@@ -235,14 +235,14 @@ This release introduces support for custom plugins, which can be used to extend
* Introduce new API endpoints
* Add custom request/response middleware
For NetBox plugins to be recognized, they must be installed and added by name to the `PLUGINS` configuration parameter. (Plugin support is disabled by default.) Plugins can be configured under the `PLUGINS_CONFIG` parameter. More information can be found the in the [plugins documentation](https://netbox.readthedocs.io/en/stable/plugins/).
For NetBox plugins to be recognized, they must be installed and added by name to the `PLUGINS` configuration parameter. (Plugin support is disabled by default.) Plugins can be configured under the `PLUGINS_CONFIG` parameter. More information can be found the in the [plugins documentation](https://docs.netbox.dev/en/stable/plugins/).
### Enhancements
* [#1754](https://github.com/netbox-community/netbox/issues/1754) - Added support for nested rack groups
* [#3939](https://github.com/netbox-community/netbox/issues/3939) - Added support for nested tenant groups
* [#4078](https://github.com/netbox-community/netbox/issues/4078) - Standardized description fields across all models
* [#4195](https://github.com/netbox-community/netbox/issues/4195) - Enabled application logging (see [logging configuration](https://netbox.readthedocs.io/en/stable/configuration/optional-settings/#logging))
* [#4195](https://github.com/netbox-community/netbox/issues/4195) - Enabled application logging (see [logging configuration](https://docs.netbox.dev/en/stable/configuration/optional-settings/#logging))
### Bug Fixes

View File

@@ -1,5 +1,150 @@
# NetBox v3.2
## v3.2.5 (2022-06-20)
### Enhancements
* [#8704](https://github.com/netbox-community/netbox/issues/8704) - Shift-click to select multiple objects in a list
* [#8882](https://github.com/netbox-community/netbox/issues/8882) - Support filtering IP addresses by multiple parent prefixes
* [#8893](https://github.com/netbox-community/netbox/issues/8893) - Include count of IP ranges under tenant view
* [#9417](https://github.com/netbox-community/netbox/issues/9417) - Initialize manufacturer selection when inserting a new module
* [#9501](https://github.com/netbox-community/netbox/issues/9501) - Add support for custom Jinja2 filters
* [#9517](https://github.com/netbox-community/netbox/issues/9517) - Linkify related power port on power outlet view
* [#9525](https://github.com/netbox-community/netbox/issues/9525) - Provide one-click edit link for objects in tables
* [#9533](https://github.com/netbox-community/netbox/issues/9533) - Move Markdown reference to local documentation
* [#9534](https://github.com/netbox-community/netbox/issues/9534) - Add VLAN group selector to interface bulk edit forms
* [#9556](https://github.com/netbox-community/netbox/issues/9556) - Leave dropdown open upon selection for multi-select fields
### Bug Fixes
* [#8944](https://github.com/netbox-community/netbox/issues/8944) - Fix rendering of Markdown links with colons
* [#9108](https://github.com/netbox-community/netbox/issues/9108) - Fix rendering of bracketed Markdown links
* [#9374](https://github.com/netbox-community/netbox/issues/9374) - Improve performance when retrieving devices/VMs with config context data
* [#9466](https://github.com/netbox-community/netbox/issues/9466) - Avoid sending webhooks after script/report failure
* [#9480](https://github.com/netbox-community/netbox/issues/9480) - Fix sorting services & service templates by port numbers
* [#9484](https://github.com/netbox-community/netbox/issues/9484) - Include services listening on "all IPs" under IP address view
* [#9486](https://github.com/netbox-community/netbox/issues/9486) - Fix redirect URL when adding device components from the module view
* [#9495](https://github.com/netbox-community/netbox/issues/9495) - Correct link to contacts in contact groups table column
* [#9503](https://github.com/netbox-community/netbox/issues/9503) - Hyperlinks in ack elevation SVGs must always use absolute URLs
* [#9512](https://github.com/netbox-community/netbox/issues/9512) - Fix duplicate site results when searching by ASN
* [#9524](https://github.com/netbox-community/netbox/issues/9524) - Correct order of VLAN fields under VM interface creation form
* [#9537](https://github.com/netbox-community/netbox/issues/9537) - Ensure consistent use of placeholder tag throughout UI
* [#9549](https://github.com/netbox-community/netbox/issues/9549) - Fix device counts for rack list under rack role view
---
## v3.2.4 (2022-05-31)
### Enhancements
* [#8374](https://github.com/netbox-community/netbox/issues/8374) - Display device type and asset tag if name is blank but asset tag is populated
* [#8922](https://github.com/netbox-community/netbox/issues/8922) - Add service list to IP address view
* [#9098](https://github.com/netbox-community/netbox/issues/9098) - Add "other" types for power ports/outlets, pass-through ports
* [#9239](https://github.com/netbox-community/netbox/issues/9239) - Enable filtering by contact group for all models which support contact assignment
* [#9277](https://github.com/netbox-community/netbox/issues/9277) - Introduce `CSRF_COOKIE_NAME` configuration parameter
* [#9347](https://github.com/netbox-community/netbox/issues/9347) - Include services in global search
* [#9379](https://github.com/netbox-community/netbox/issues/9379) - Redirect to virtual chassis view after adding a member device
* [#9451](https://github.com/netbox-community/netbox/issues/9451) - Add `export_raw` argument for TemplateColumn
### Bug Fixes
* [#9094](https://github.com/netbox-community/netbox/issues/9094) - Fix partial address search within Prefix and Aggregate filters
* [#9291](https://github.com/netbox-community/netbox/issues/9291) - Improve data validation for MultiObjectVar script fields
* [#9358](https://github.com/netbox-community/netbox/issues/9358) - Annotate circuit count for providers list under ASN view
* [#9387](https://github.com/netbox-community/netbox/issues/9387) - Ensure ActionsColumn `extra_buttons` are always displayed
* [#9402](https://github.com/netbox-community/netbox/issues/9402) - Fix custom field population when creating a virtual chassis
* [#9407](https://github.com/netbox-community/netbox/issues/9407) - Clean up display of prefixes values when exporting prefixes list
* [#9420](https://github.com/netbox-community/netbox/issues/9420) - Fix custom script class inheritance
* [#9425](https://github.com/netbox-community/netbox/issues/9425) - Fix bulk import for object and multi-object custom fields
* [#9430](https://github.com/netbox-community/netbox/issues/9430) - Fix passing of initial form data for DynamicModelChoiceFields
---
## v3.2.3 (2022-05-12)
### Enhancements
* [#8805](https://github.com/netbox-community/netbox/issues/8805) - Add "mixed" option for device airflow indication
* [#8894](https://github.com/netbox-community/netbox/issues/8894) - Include full names when listing users
* [#8998](https://github.com/netbox-community/netbox/issues/8998) - Enable filtering racks & reservations by site group
* [#9122](https://github.com/netbox-community/netbox/issues/9122) - Introduce `clearcache` management command & clear cache during upgrade
* [#9221](https://github.com/netbox-community/netbox/issues/9221) - Add definition list support for Markdown
* [#9260](https://github.com/netbox-community/netbox/issues/9260) - Apply user preferences to tables under object detail views
* [#9278](https://github.com/netbox-community/netbox/issues/9278) - Linkify device types count under manufacturers list
* [#9280](https://github.com/netbox-community/netbox/issues/9280) - Allow adopting existing components when installing a module
* [#9314](https://github.com/netbox-community/netbox/issues/9314) - Add device and VM filters for FHRP group assignments
* [#9340](https://github.com/netbox-community/netbox/issues/9340) - Introduce support for error reporting via Sentry
* [#9343](https://github.com/netbox-community/netbox/issues/9343) - Add Ubiquiti SmartPower power outlet type
### Bug Fixes
* [#9190](https://github.com/netbox-community/netbox/issues/9190) - Prevent exception when attempting to instantiate module components which already exist on the parent device
* [#9267](https://github.com/netbox-community/netbox/issues/9267) - Remove invalid entry in IP address role choices
* [#9296](https://github.com/netbox-community/netbox/issues/9296) - Improve Markdown link sanitization
* [#9306](https://github.com/netbox-community/netbox/issues/9306) - Include VC master interfaces when selecting a LAG/bridge for a VC member interface
* [#9311](https://github.com/netbox-community/netbox/issues/9311) - Permit creating contact assignment without a priority via the REST API
* [#9313](https://github.com/netbox-community/netbox/issues/9313) - Remove HTML code from CSV output of many-to-many relationships
* [#9330](https://github.com/netbox-community/netbox/issues/9330) - Add missing `module_type` field to REST API serializers for modular device component templates
---
## v3.2.2 (2022-04-28)
### Enhancements
* [#9060](https://github.com/netbox-community/netbox/issues/9060) - Add device type filters for device bays, module bays, and inventory items
* [#9152](https://github.com/netbox-community/netbox/issues/9152) - Annotate related object type under custom field view
* [#9192](https://github.com/netbox-community/netbox/issues/9192) - Add Ubiquiti SmartPower connector type
* [#9214](https://github.com/netbox-community/netbox/issues/9214) - Linkify cluster counts in cluster type & group tables
### Bug Fixes
* [#4264](https://github.com/netbox-community/netbox/issues/4264) - Treat 0th IP as unusable for IPv6 prefixes (excluding /127s)
* [#8941](https://github.com/netbox-community/netbox/issues/8941) - Fix dynamic dropdown behavior when browser is zoomed
* [#8959](https://github.com/netbox-community/netbox/issues/8959) - Prevent exception when refreshing scripts list (avoid race condition)
* [#9132](https://github.com/netbox-community/netbox/issues/9132) - Limit location options by selected site when creating a wireless link
* [#9133](https://github.com/netbox-community/netbox/issues/9133) - Upgrade script should require Python 3.8 or later
* [#9138](https://github.com/netbox-community/netbox/issues/9138) - Avoid inadvertent form submission when utilizing quick search field on object lists
* [#9151](https://github.com/netbox-community/netbox/issues/9151) - Child prefix counts not annotated on aggregates list under RIR view
* [#9156](https://github.com/netbox-community/netbox/issues/9156) - Fix loading UserConfig data from fixtures
* [#9158](https://github.com/netbox-community/netbox/issues/9158) - Do not list tags field for CSV forms which do not support tag assignment
* [#9194](https://github.com/netbox-community/netbox/issues/9194) - Support position assignment when add module bays to multiple devices
* [#9206](https://github.com/netbox-community/netbox/issues/9206) - Show header for comments field under module & module type creation views
* [#9222](https://github.com/netbox-community/netbox/issues/9222) - Fix circuit ID display under cable view
* [#9227](https://github.com/netbox-community/netbox/issues/9227) - Fix related object assignment when recording change record for interfaces
---
## v3.2.1 (2022-04-14)
### Enhancements
* [#5479](https://github.com/netbox-community/netbox/issues/5479) - Allow custom job timeouts for scripts & reports
* [#8543](https://github.com/netbox-community/netbox/issues/8543) - Improve filtering for wireless LAN VLAN selection
* [#8920](https://github.com/netbox-community/netbox/issues/8920) - Limit number of non-racked devices displayed
* [#8956](https://github.com/netbox-community/netbox/issues/8956) - Retain old script/report results for configured lifetime
* [#8973](https://github.com/netbox-community/netbox/issues/8973) - Display VLAN group count under site view
* [#9081](https://github.com/netbox-community/netbox/issues/9081) - Add `fhrpgroup_id` filter for IP addresses
* [#9099](https://github.com/netbox-community/netbox/issues/9099) - Enable display of installed module serial & asset tag in module bays list
* [#9110](https://github.com/netbox-community/netbox/issues/9110) - Add Neutrik proprietary power connectors
* [#9123](https://github.com/netbox-community/netbox/issues/9123) - Improve appearance of SSO login providers
### Bug Fixes
* [#8931](https://github.com/netbox-community/netbox/issues/8931) - Copy assigned tenant when cloning a location
* [#9055](https://github.com/netbox-community/netbox/issues/9055) - Restore ability to move inventory item to other device
* [#9057](https://github.com/netbox-community/netbox/issues/9057) - Fix missing instance counts for module types
* [#9061](https://github.com/netbox-community/netbox/issues/9061) - Fix general search for device components
* [#9065](https://github.com/netbox-community/netbox/issues/9065) - Min/max VID should not be required when filtering VLAN groups
* [#9079](https://github.com/netbox-community/netbox/issues/9079) - Fail validation when an inventory item is assigned as its own parent
* [#9096](https://github.com/netbox-community/netbox/issues/9096) - Remove duplicate filter tag when filtering by "none"
* [#9100](https://github.com/netbox-community/netbox/issues/9100) - Include position field in module type YAML export
* [#9116](https://github.com/netbox-community/netbox/issues/9116) - `assigned_to_interface` filter for IP addresses should not match FHRP group assignments
* [#9118](https://github.com/netbox-community/netbox/issues/9118) - Fix validation error when importing VM child interfaces
* [#9128](https://github.com/netbox-community/netbox/issues/9128) - Resolve component labels per module bay position when installing modules
---
## v3.2.0 (2022-04-05)
!!! warning "Python 3.8 or Later Required"

View File

@@ -1,6 +1,6 @@
site_name: NetBox Documentation
site_dir: netbox/project-static/docs
site_url: https://netbox.readthedocs.io/
site_url: https://docs.netbox.dev/
repo_name: netbox-community/netbox
repo_url: https://github.com/netbox-community/netbox
theme:
@@ -19,6 +19,7 @@ theme:
icon: material/lightbulb
name: Switch to Light Mode
plugins:
- search
- mkdocstrings:
handlers:
python:
@@ -72,6 +73,7 @@ nav:
- Required Settings: 'configuration/required-settings.md'
- Optional Settings: 'configuration/optional-settings.md'
- Dynamic Settings: 'configuration/dynamic-settings.md'
- Error Reporting: 'configuration/error-reporting.md'
- Remote Authentication: 'configuration/remote-authentication.md'
- Core Functionality:
- IP Address Management: 'core-functionality/ipam.md'
@@ -117,8 +119,12 @@ nav:
- GraphQL API: 'plugins/development/graphql-api.md'
- Background Tasks: 'plugins/development/background-tasks.md'
- Administration:
- Authentication: 'administration/authentication.md'
- Authentication:
- Overview: 'administration/authentication/overview.md'
- Microsoft Azure AD: 'administration/authentication/microsoft-azure-ad.md'
- Okta: 'administration/authentication/okta.md'
- Permissions: 'administration/permissions.md'
- Error Reporting: 'administration/error-reporting.md'
- Housekeeping: 'administration/housekeeping.md'
- Replicating NetBox: 'administration/replicating-netbox.md'
- NetBox Shell: 'administration/netbox-shell.md'
@@ -130,6 +136,7 @@ nav:
- Overview: 'graphql-api/overview.md'
- Reference:
- Conditions: 'reference/conditions.md'
- Markdown: 'reference/markdown.md'
- Development:
- Introduction: 'development/index.md'
- Getting Started: 'development/getting-started.md'

View File

@@ -23,7 +23,7 @@ class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
(None, ('q', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id')),
('ASN', ('asn',)),
('Contacts', ('contact', 'contact_role')),
('Contacts', ('contact', 'contact_role', 'contact_group')),
)
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
@@ -87,7 +87,7 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
('Attributes', ('type_id', 'status', 'commit_rate')),
('Location', ('region_id', 'site_group_id', 'site_id')),
('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role')),
('Contacts', ('contact', 'contact_role', 'contact_group')),
)
type_id = DynamicModelMultipleChoiceField(
queryset=CircuitType.objects.all(),

View File

@@ -59,7 +59,7 @@ class CircuitTable(NetBoxTable):
)
commit_rate = CommitRateColumn()
comments = columns.MarkdownColumn()
contacts = tables.ManyToManyColumn(
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(

View File

@@ -14,7 +14,7 @@ class ProviderTable(NetBoxTable):
name = tables.Column(
linkify=True
)
asns = tables.ManyToManyColumn(
asns = columns.ManyToManyColumn(
linkify_item=True,
verbose_name='ASNs'
)
@@ -31,7 +31,7 @@ class ProviderTable(NetBoxTable):
verbose_name='Circuits'
)
comments = columns.MarkdownColumn()
contacts = tables.ManyToManyColumn(
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(

View File

@@ -32,7 +32,7 @@ class ProviderView(generic.ObjectView):
).prefetch_related(
'type', 'tenant', 'terminations__site'
)
circuits_table = tables.CircuitTable(circuits, exclude=('provider',))
circuits_table = tables.CircuitTable(circuits, user=request.user, exclude=('provider',))
circuits_table.configure(request)
return {
@@ -93,7 +93,7 @@ class ProviderNetworkView(generic.ObjectView):
).prefetch_related(
'type', 'tenant', 'terminations__site'
)
circuits_table = tables.CircuitTable(circuits)
circuits_table = tables.CircuitTable(circuits, user=request.user)
circuits_table.configure(request)
return {
@@ -147,7 +147,7 @@ class CircuitTypeView(generic.ObjectView):
def get_extra_context(self, request, instance):
circuits = Circuit.objects.restrict(request.user, 'view').filter(type=instance)
circuits_table = tables.CircuitTable(circuits, exclude=('type',))
circuits_table = tables.CircuitTable(circuits, user=request.user, exclude=('type',))
circuits_table.configure(request)
return {

View File

@@ -315,7 +315,16 @@ class ModuleTypeSerializer(NetBoxModelSerializer):
class ConsolePortTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleporttemplate-detail')
device_type = NestedDeviceTypeSerializer()
device_type = NestedDeviceTypeSerializer(
required=False,
allow_null=True,
default=None
)
module_type = NestedModuleTypeSerializer(
required=False,
allow_null=True,
default=None
)
type = ChoiceField(
choices=ConsolePortTypeChoices,
allow_blank=True,
@@ -325,13 +334,23 @@ class ConsolePortTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = ConsolePortTemplate
fields = [
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'description', 'created', 'last_updated',
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'description', 'created',
'last_updated',
]
class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverporttemplate-detail')
device_type = NestedDeviceTypeSerializer()
device_type = NestedDeviceTypeSerializer(
required=False,
allow_null=True,
default=None
)
module_type = NestedModuleTypeSerializer(
required=False,
allow_null=True,
default=None
)
type = ChoiceField(
choices=ConsolePortTypeChoices,
allow_blank=True,
@@ -341,13 +360,23 @@ class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = ConsoleServerPortTemplate
fields = [
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'description', 'created', 'last_updated',
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'description', 'created',
'last_updated',
]
class PowerPortTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerporttemplate-detail')
device_type = NestedDeviceTypeSerializer()
device_type = NestedDeviceTypeSerializer(
required=False,
allow_null=True,
default=None
)
module_type = NestedModuleTypeSerializer(
required=False,
allow_null=True,
default=None
)
type = ChoiceField(
choices=PowerPortTypeChoices,
allow_blank=True,
@@ -357,14 +386,23 @@ class PowerPortTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = PowerPortTemplate
fields = [
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw',
'description', 'created', 'last_updated',
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw',
'allocated_draw', 'description', 'created', 'last_updated',
]
class PowerOutletTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlettemplate-detail')
device_type = NestedDeviceTypeSerializer()
device_type = NestedDeviceTypeSerializer(
required=False,
allow_null=True,
default=None
)
module_type = NestedModuleTypeSerializer(
required=False,
allow_null=True,
default=None
)
type = ChoiceField(
choices=PowerOutletTypeChoices,
allow_blank=True,
@@ -383,48 +421,75 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = PowerOutletTemplate
fields = [
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
'created', 'last_updated',
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg',
'description', 'created', 'last_updated',
]
class InterfaceTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfacetemplate-detail')
device_type = NestedDeviceTypeSerializer()
device_type = NestedDeviceTypeSerializer(
required=False,
allow_null=True,
default=None
)
module_type = NestedModuleTypeSerializer(
required=False,
allow_null=True,
default=None
)
type = ChoiceField(choices=InterfaceTypeChoices)
class Meta:
model = InterfaceTemplate
fields = [
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'mgmt_only', 'description', 'created',
'last_updated',
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description',
'created', 'last_updated',
]
class RearPortTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearporttemplate-detail')
device_type = NestedDeviceTypeSerializer()
device_type = NestedDeviceTypeSerializer(
required=False,
allow_null=True,
default=None
)
module_type = NestedModuleTypeSerializer(
required=False,
allow_null=True,
default=None
)
type = ChoiceField(choices=PortTypeChoices)
class Meta:
model = RearPortTemplate
fields = [
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'color', 'positions', 'description',
'created', 'last_updated',
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions',
'description', 'created', 'last_updated',
]
class FrontPortTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontporttemplate-detail')
device_type = NestedDeviceTypeSerializer()
device_type = NestedDeviceTypeSerializer(
required=False,
allow_null=True,
default=None
)
module_type = NestedModuleTypeSerializer(
required=False,
allow_null=True,
default=None
)
type = ChoiceField(choices=PortTypeChoices)
rear_port = NestedRearPortTemplateSerializer()
class Meta:
model = FrontPortTemplate
fields = [
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
'description', 'created', 'last_updated',
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port',
'rear_port_position', 'description', 'created', 'last_updated',
]

View File

@@ -19,6 +19,7 @@ from ipam.models import Prefix, VLAN
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
from netbox.api.exceptions import ServiceUnavailable
from netbox.api.metadata import ContentTypeMetadata
from netbox.api.pagination import StripCountAnnotationsPaginator
from netbox.api.viewsets import NetBoxModelViewSet
from netbox.config import get_config
from utilities.api import get_serializer_for_model
@@ -392,6 +393,7 @@ class DeviceViewSet(ConfigContextQuerySetMixin, NetBoxModelViewSet):
'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags',
)
filterset_class = filtersets.DeviceFilterSet
pagination_class = StripCountAnnotationsPaginator
def get_serializer_class(self):
"""

View File

@@ -159,6 +159,7 @@ class DeviceAirflowChoices(ChoiceSet):
AIRFLOW_RIGHT_TO_LEFT = 'right-to-left'
AIRFLOW_SIDE_TO_REAR = 'side-to-rear'
AIRFLOW_PASSIVE = 'passive'
AIRFLOW_MIXED = 'mixed'
CHOICES = (
(AIRFLOW_FRONT_TO_REAR, 'Front to rear'),
@@ -167,6 +168,7 @@ class DeviceAirflowChoices(ChoiceSet):
(AIRFLOW_RIGHT_TO_LEFT, 'Right to left'),
(AIRFLOW_SIDE_TO_REAR, 'Side to rear'),
(AIRFLOW_PASSIVE, 'Passive'),
(AIRFLOW_MIXED, 'Mixed'),
)
@@ -345,8 +347,14 @@ class PowerPortTypeChoices(ChoiceSet):
TYPE_DC = 'dc-terminal'
# Proprietary
TYPE_SAF_D_GRID = 'saf-d-grid'
TYPE_NEUTRIK_POWERCON_20A = 'neutrik-powercon-20'
TYPE_NEUTRIK_POWERCON_32A = 'neutrik-powercon-32'
TYPE_NEUTRIK_POWERCON_TRUE1 = 'neutrik-powercon-true1'
TYPE_NEUTRIK_POWERCON_TRUE1_TOP = 'neutrik-powercon-true1-top'
TYPE_UBIQUITI_SMARTPOWER = 'ubiquiti-smartpower'
# Other
TYPE_HARDWIRED = 'hardwired'
TYPE_OTHER = 'other'
CHOICES = (
('IEC 60320', (
@@ -456,9 +464,15 @@ class PowerPortTypeChoices(ChoiceSet):
)),
('Proprietary', (
(TYPE_SAF_D_GRID, 'Saf-D-Grid'),
(TYPE_NEUTRIK_POWERCON_20A, 'Neutrik powerCON (20A)'),
(TYPE_NEUTRIK_POWERCON_32A, 'Neutrik powerCON (32A)'),
(TYPE_NEUTRIK_POWERCON_TRUE1, 'Neutrik powerCON TRUE1'),
(TYPE_NEUTRIK_POWERCON_TRUE1_TOP, 'Neutrik powerCON TRUE1 TOP'),
(TYPE_UBIQUITI_SMARTPOWER, 'Ubiquiti SmartPower'),
)),
('Other', (
(TYPE_HARDWIRED, 'Hardwired'),
(TYPE_OTHER, 'Other'),
)),
)
@@ -561,8 +575,14 @@ class PowerOutletTypeChoices(ChoiceSet):
# Proprietary
TYPE_HDOT_CX = 'hdot-cx'
TYPE_SAF_D_GRID = 'saf-d-grid'
TYPE_NEUTRIK_POWERCON_20A = 'neutrik-powercon-20a'
TYPE_NEUTRIK_POWERCON_32A = 'neutrik-powercon-32a'
TYPE_NEUTRIK_POWERCON_TRUE1 = 'neutrik-powercon-true1'
TYPE_NEUTRIK_POWERCON_TRUE1_TOP = 'neutrik-powercon-true1-top'
TYPE_UBIQUITI_SMARTPOWER = 'ubiquiti-smartpower'
# Other
TYPE_HARDWIRED = 'hardwired'
TYPE_OTHER = 'other'
CHOICES = (
('IEC 60320', (
@@ -665,9 +685,15 @@ class PowerOutletTypeChoices(ChoiceSet):
('Proprietary', (
(TYPE_HDOT_CX, 'HDOT Cx'),
(TYPE_SAF_D_GRID, 'Saf-D-Grid'),
(TYPE_NEUTRIK_POWERCON_20A, 'Neutrik powerCON (20A)'),
(TYPE_NEUTRIK_POWERCON_32A, 'Neutrik powerCON (32A)'),
(TYPE_NEUTRIK_POWERCON_TRUE1, 'Neutrik powerCON TRUE1'),
(TYPE_NEUTRIK_POWERCON_TRUE1_TOP, 'Neutrik powerCON TRUE1 TOP'),
(TYPE_UBIQUITI_SMARTPOWER, 'Ubiquiti SmartPower'),
)),
('Other', (
(TYPE_HARDWIRED, 'Hardwired'),
(TYPE_OTHER, 'Other'),
)),
)
@@ -1025,6 +1051,7 @@ class PortTypeChoices(ChoiceSet):
TYPE_URM_P2 = 'urm-p2'
TYPE_URM_P4 = 'urm-p4'
TYPE_URM_P8 = 'urm-p8'
TYPE_OTHER = 'other'
CHOICES = (
(
@@ -1077,6 +1104,12 @@ class PortTypeChoices(ChoiceSet):
(TYPE_URM_P4, 'URM-P4'),
(TYPE_URM_P8, 'URM-P8'),
(TYPE_SPLICE, 'Splice'),
),
),
(
'Other',
(
(TYPE_OTHER, 'Other'),
)
)
)

View File

@@ -62,6 +62,8 @@ POWERFEED_MAX_UTILIZATION_DEFAULT = 80 # Percentage
# Device components
#
MODULE_TOKEN = '{module}'
MODULAR_COMPONENT_TEMPLATE_MODELS = Q(
app_label='dcim',
model__in=(

View File

@@ -163,7 +163,7 @@ class SiteFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe
qs_filter |= Q(asns__asn=int(value.strip()))
except ValueError:
pass
return queryset.filter(qs_filter)
return queryset.filter(qs_filter).distinct()
class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalModelFilterSet):
@@ -346,6 +346,32 @@ class RackReservationFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
to_field_name='slug',
label='Site (slug)',
)
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='rack__site__region',
lookup_expr='in',
label='Region (ID)',
)
region = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='rack__site__region',
lookup_expr='in',
to_field_name='slug',
label='Region (slug)',
)
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='rack__site__group',
lookup_expr='in',
label='Site group (ID)',
)
site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='rack__site__group',
lookup_expr='in',
to_field_name='slug',
label='Site group (slug)',
)
location_id = TreeNodeMultipleChoiceFilter(
queryset=Location.objects.all(),
field_name='rack__location',
@@ -435,6 +461,10 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
method='_device_bays',
label='Has device bays',
)
inventory_items = django_filters.BooleanFilter(
method='_inventory_items',
label='Has inventory items',
)
class Meta:
model = DeviceType
@@ -479,6 +509,9 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
def _device_bays(self, queryset, name, value):
return queryset.exclude(devicebaytemplates__isnull=value)
def _inventory_items(self, queryset, name, value):
return queryset.exclude(inventoryitemtemplates__isnull=value)
class ModuleTypeFilterSet(NetBoxModelFilterSet):
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
@@ -1095,8 +1128,8 @@ class PathEndpointFilterSet(django_filters.FilterSet):
class ConsolePortFilterSet(
NetBoxModelFilterSet,
ModularDeviceComponentFilterSet,
NetBoxModelFilterSet,
CableTerminationFilterSet,
PathEndpointFilterSet
):
@@ -1111,8 +1144,8 @@ class ConsolePortFilterSet(
class ConsoleServerPortFilterSet(
NetBoxModelFilterSet,
ModularDeviceComponentFilterSet,
NetBoxModelFilterSet,
CableTerminationFilterSet,
PathEndpointFilterSet
):
@@ -1127,8 +1160,8 @@ class ConsoleServerPortFilterSet(
class PowerPortFilterSet(
NetBoxModelFilterSet,
ModularDeviceComponentFilterSet,
NetBoxModelFilterSet,
CableTerminationFilterSet,
PathEndpointFilterSet
):
@@ -1143,8 +1176,8 @@ class PowerPortFilterSet(
class PowerOutletFilterSet(
NetBoxModelFilterSet,
ModularDeviceComponentFilterSet,
NetBoxModelFilterSet,
CableTerminationFilterSet,
PathEndpointFilterSet
):
@@ -1163,8 +1196,8 @@ class PowerOutletFilterSet(
class InterfaceFilterSet(
NetBoxModelFilterSet,
ModularDeviceComponentFilterSet,
NetBoxModelFilterSet,
CableTerminationFilterSet,
PathEndpointFilterSet
):
@@ -1291,8 +1324,8 @@ class InterfaceFilterSet(
class FrontPortFilterSet(
NetBoxModelFilterSet,
ModularDeviceComponentFilterSet,
NetBoxModelFilterSet,
CableTerminationFilterSet
):
type = django_filters.MultipleChoiceFilter(
@@ -1306,8 +1339,8 @@ class FrontPortFilterSet(
class RearPortFilterSet(
NetBoxModelFilterSet,
ModularDeviceComponentFilterSet,
NetBoxModelFilterSet,
CableTerminationFilterSet
):
type = django_filters.MultipleChoiceFilter(
@@ -1320,21 +1353,21 @@ class RearPortFilterSet(
fields = ['id', 'name', 'label', 'type', 'color', 'positions', 'description']
class ModuleBayFilterSet(NetBoxModelFilterSet, DeviceComponentFilterSet):
class ModuleBayFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet):
class Meta:
model = ModuleBay
fields = ['id', 'name', 'label', 'description']
class DeviceBayFilterSet(NetBoxModelFilterSet, DeviceComponentFilterSet):
class DeviceBayFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet):
class Meta:
model = DeviceBay
fields = ['id', 'name', 'label', 'description']
class InventoryItemFilterSet(NetBoxModelFilterSet, DeviceComponentFilterSet):
class InventoryItemFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet):
parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=InventoryItem.objects.all(),
label='Parent inventory item (ID)',

View File

@@ -3,7 +3,7 @@ from django import forms
from dcim.models import *
from extras.forms import CustomFieldsMixin
from extras.models import Tag
from utilities.forms import DynamicModelMultipleChoiceField, form_from_model
from utilities.forms import DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model
from .object_create import ComponentCreateForm
__all__ = (
@@ -98,7 +98,13 @@ class RearPortBulkCreateForm(
class ModuleBayBulkCreateForm(DeviceBulkAddComponentForm):
model = ModuleBay
field_order = ('name_pattern', 'label_pattern', 'description', 'tags')
field_order = ('name_pattern', 'label_pattern', 'position_pattern', 'description', 'tags')
position_pattern = ExpandableNameField(
label='Position',
required=False,
help_text='Alphanumeric ranges are supported. (Must match the number of names being created.)'
)
class DeviceBayBulkCreateForm(DeviceBulkAddComponentForm):

View File

@@ -6,7 +6,7 @@ from timezone_field import TimeZoneFormField
from dcim.choices import *
from dcim.constants import *
from dcim.models import *
from ipam.models import ASN, VLAN, VRF
from ipam.models import ASN, VLAN, VLANGroup, VRF
from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant
from utilities.forms import (
@@ -1067,13 +1067,32 @@ class InterfaceBulkEditForm(
required=False,
widget=BulkEditNullBooleanSelect
)
mode = forms.ChoiceField(
choices=add_blank_choice(InterfaceModeChoices),
required=False,
initial='',
widget=StaticSelect()
)
vlan_group = DynamicModelChoiceField(
queryset=VLANGroup.objects.all(),
required=False,
label='VLAN group'
)
untagged_vlan = DynamicModelChoiceField(
queryset=VLAN.objects.all(),
required=False
required=False,
query_params={
'group_id': '$vlan_group',
},
label='Untagged VLAN'
)
tagged_vlans = DynamicModelMultipleChoiceField(
queryset=VLAN.objects.all(),
required=False
required=False,
query_params={
'group_id': '$vlan_group',
},
label='Tagged VLANs'
)
vrf = DynamicModelChoiceField(
queryset=VRF.objects.all(),
@@ -1087,13 +1106,13 @@ class InterfaceBulkEditForm(
('Addressing', ('vrf', 'mac_address', 'wwn')),
('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
('Related Interfaces', ('parent', 'bridge', 'lag')),
('802.1Q Switching', ('mode', 'untagged_vlan', 'tagged_vlans')),
('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',
'mode', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan', 'tagged_vlans',
'vrf',
'mode', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'vlan_group', 'untagged_vlan',
'tagged_vlans', 'vrf',
)
def __init__(self, *args, **kwargs):
@@ -1204,6 +1223,10 @@ class InventoryItemBulkEditForm(
form_from_model(InventoryItem, ['label', 'role', 'manufacturer', 'part_id', 'description']),
NetBoxModelBulkEditForm
):
device = DynamicModelChoiceField(
queryset=Device.objects.all(),
required=False
)
role = DynamicModelChoiceField(
queryset=InventoryItemRole.objects.all(),
required=False
@@ -1215,7 +1238,7 @@ class InventoryItemBulkEditForm(
model = InventoryItem
fieldsets = (
(None, ('label', 'role', 'manufacturer', 'part_id', 'description')),
(None, ('device', 'label', 'role', 'manufacturer', 'part_id', 'description')),
)
nullable_fields = ('label', 'role', 'manufacturer', 'part_id', 'description')

View File

@@ -651,11 +651,11 @@ class InterfaceCSVForm(NetBoxModelCSVForm):
super().__init__(data, *args, **kwargs)
if data:
# Limit interface choices for parent, bridge and lag to device only
params = {}
if data.get('device'):
params[f"device__{self.fields['device'].to_field_name}"] = data.get('device')
if params:
# Limit choices for parent, bridge, and LAG interfaces to the assigned device
if device := data.get('device'):
params = {
f"device__{self.fields['device'].to_field_name}": device
}
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)

View File

@@ -108,7 +108,7 @@ class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Region
fieldsets = (
(None, ('q', 'tag', 'parent_id')),
('Contacts', ('contact', 'contact_role'))
('Contacts', ('contact', 'contact_role', 'contact_group'))
)
parent_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
@@ -122,7 +122,7 @@ class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = SiteGroup
fieldsets = (
(None, ('q', 'tag', 'parent_id')),
('Contacts', ('contact', 'contact_role'))
('Contacts', ('contact', 'contact_role', 'contact_group'))
)
parent_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
@@ -138,7 +138,7 @@ class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
(None, ('q', 'tag')),
('Attributes', ('status', 'region_id', 'group_id', 'asn_id')),
('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role')),
('Contacts', ('contact', 'contact_role', 'contact_group')),
)
status = MultipleChoiceField(
choices=SiteStatusChoices,
@@ -168,7 +168,7 @@ class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelF
(None, ('q', 'tag')),
('Parent', ('region_id', 'site_group_id', 'site_id', 'parent_id')),
('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role')),
('Contacts', ('contact', 'contact_role', 'contact_group')),
)
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
@@ -210,11 +210,11 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
model = Rack
fieldsets = (
(None, ('q', 'tag')),
('Location', ('region_id', 'site_id', 'location_id')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')),
('Function', ('status', 'role_id')),
('Hardware', ('type', 'width', 'serial', 'asset_tag')),
('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role')),
('Contacts', ('contact', 'contact_role', 'contact_group')),
)
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
@@ -229,6 +229,11 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
},
label=_('Site')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group')
)
location_id = DynamicModelMultipleChoiceField(
queryset=Location.objects.all(),
required=False,
@@ -282,7 +287,7 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
fieldsets = (
(None, ('q', 'tag')),
('User', ('user_id',)),
('Rack', ('region_id', 'site_id', 'location_id')),
('Rack', ('region_id', 'site_group_id', 'site_id', 'location_id')),
('Tenant', ('tenant_group_id', 'tenant_id')),
)
region_id = DynamicModelMultipleChoiceField(
@@ -298,6 +303,11 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
},
label=_('Site')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group')
)
location_id = DynamicModelMultipleChoiceField(
queryset=Location.objects.prefetch_related('site'),
required=False,
@@ -319,7 +329,7 @@ class ManufacturerFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Manufacturer
fieldsets = (
(None, ('q', 'tag')),
('Contacts', ('contact', 'contact_role'))
('Contacts', ('contact', 'contact_role', 'contact_group'))
)
tag = TagFilterField(model)
@@ -331,7 +341,7 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
('Hardware', ('manufacturer_id', 'part_number', 'subdevice_role', 'airflow')),
('Components', (
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
'pass_through_ports',
'pass_through_ports', 'device_bays', 'module_bays', 'inventory_items',
)),
)
manufacturer_id = DynamicModelMultipleChoiceField(
@@ -392,6 +402,27 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
device_bays = forms.NullBooleanField(
required=False,
label='Has device bays',
widget=StaticSelect(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
module_bays = forms.NullBooleanField(
required=False,
label='Has module bays',
widget=StaticSelect(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
inventory_items = forms.NullBooleanField(
required=False,
label='Has inventory items',
widget=StaticSelect(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
tag = TagFilterField(model)
@@ -487,7 +518,7 @@ class DeviceFilterForm(
('Operation', ('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address')),
('Hardware', ('manufacturer_id', 'device_type_id', 'platform_id')),
('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role')),
('Contacts', ('contact', 'contact_role', 'contact_group')),
('Components', (
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports',
)),
@@ -757,7 +788,7 @@ class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
fieldsets = (
(None, ('q', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')),
('Contacts', ('contact', 'contact_role')),
('Contacts', ('contact', 'contact_role', 'contact_group')),
)
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
@@ -1071,7 +1102,7 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
model = InventoryItem
fieldsets = (
(None, ('q', 'tag')),
('Attributes', ('name', 'label', 'manufacturer_id', 'serial', 'asset_tag', 'discovered')),
('Attributes', ('name', 'label', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
)
role_id = DynamicModelMultipleChoiceField(

View File

@@ -385,6 +385,12 @@ class ModuleTypeForm(NetBoxModelForm):
)
comments = CommentField()
fieldsets = (
('Module Type', (
'manufacturer', 'model', 'part_number', 'tags',
)),
)
class Meta:
model = ModuleType
fields = [
@@ -627,11 +633,26 @@ class ModuleForm(NetBoxModelForm):
help_text="Automatically populate components associated with this module type"
)
adopt_components = forms.BooleanField(
required=False,
initial=False,
help_text="Adopt already existing components"
)
fieldsets = (
('Module', (
'device', 'module_bay', 'manufacturer', 'module_type', 'tags',
)),
('Hardware', (
'serial', 'asset_tag', 'replicate_components', 'adopt_components',
)),
)
class Meta:
model = Module
fields = [
'device', 'module_bay', 'manufacturer', 'module_type', 'serial', 'asset_tag', 'tags',
'replicate_components', 'comments',
'replicate_components', 'adopt_components', 'comments',
]
def __init__(self, *args, **kwargs):
@@ -640,6 +661,8 @@ class ModuleForm(NetBoxModelForm):
if self.instance.pk:
self.fields['replicate_components'].initial = False
self.fields['replicate_components'].disabled = True
self.fields['adopt_components'].initial = False
self.fields['adopt_components'].disabled = True
def save(self, *args, **kwargs):
@@ -647,8 +670,62 @@ class ModuleForm(NetBoxModelForm):
if self.instance.pk or not self.cleaned_data['replicate_components']:
self.instance._disable_replication = True
if self.cleaned_data['adopt_components']:
self.instance._adopt_components = True
return super().save(*args, **kwargs)
def clean(self):
super().clean()
replicate_components = self.cleaned_data.get("replicate_components")
adopt_components = self.cleaned_data.get("adopt_components")
device = self.cleaned_data['device']
module_type = self.cleaned_data['module_type']
module_bay = self.cleaned_data['module_bay']
# Bail out if we are not installing a new module or if we are not replicating components
if self.instance.pk or not replicate_components:
return
for templates, component_attribute in [
("consoleporttemplates", "consoleports"),
("consoleserverporttemplates", "consoleserverports"),
("interfacetemplates", "interfaces"),
("powerporttemplates", "powerports"),
("poweroutlettemplates", "poweroutlets"),
("rearporttemplates", "rearports"),
("frontporttemplates", "frontports")
]:
# Prefetch installed components
installed_components = {
component.name: component for component in getattr(device, component_attribute).all()
}
# Get the templates for the module type.
for template in getattr(module_type, templates).all():
# Installing modules with placeholders require that the bay has a position value
if MODULE_TOKEN in template.name and not module_bay.position:
raise forms.ValidationError(
"Cannot install module with placeholder values in a module bay with no position defined"
)
resolved_name = template.name.replace(MODULE_TOKEN, module_bay.position)
existing_item = installed_components.get(resolved_name)
# It is not possible to adopt components already belonging to a module
if adopt_components and existing_item and existing_item.module:
raise forms.ValidationError(
f"Cannot adopt {template.component_model.__name__} '{resolved_name}' as it already belongs "
f"to a module"
)
# If we are not adopting components we error if the component exists
if not adopt_components and resolved_name in installed_components:
raise forms.ValidationError(
f"{template.component_model.__name__} - {resolved_name} already exists"
)
class CableForm(TenancyForm, NetBoxModelForm):
@@ -1269,6 +1346,16 @@ class InterfaceForm(InterfaceCommonForm, NetBoxModelForm):
'rf_channel_width': "Populated by selected channel (if set)",
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Restrict LAG/bridge interface assignment by device/VC
device_id = self.data['device'] if self.is_bound else self.initial.get('device')
device = Device.objects.filter(pk=device_id).first()
if device and device.virtual_chassis and device.virtual_chassis.master:
self.fields['lag'].widget.add_query_param('device_id', device.virtual_chassis.master.pk)
self.fields['bridge'].widget.add_query_param('device_id', device.virtual_chassis.master.pk)
class FrontPortForm(NetBoxModelForm):
module = DynamicModelChoiceField(
@@ -1362,6 +1449,9 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
class InventoryItemForm(NetBoxModelForm):
device = DynamicModelChoiceField(
queryset=Device.objects.all()
)
parent = DynamicModelChoiceField(
queryset=InventoryItem.objects.all(),
required=False,
@@ -1399,9 +1489,6 @@ class InventoryItemForm(NetBoxModelForm):
'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
'description', 'component_type', 'component_id', 'tags',
]
widgets = {
'device': forms.HiddenInput(),
}
#

View File

@@ -1,7 +1,6 @@
from django import forms
from dcim.models import *
from extras.models import Tag
from netbox.forms import NetBoxModelForm
from utilities.forms import (
BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField,
@@ -12,6 +11,7 @@ __all__ = (
'DeviceComponentCreateForm',
'FrontPortCreateForm',
'FrontPortTemplateCreateForm',
'InventoryItemCreateForm',
'ModularComponentTemplateCreateForm',
'ModuleBayCreateForm',
'ModuleBayTemplateCreateForm',
@@ -199,6 +199,11 @@ class ModuleBayCreateForm(DeviceComponentCreateForm):
field_order = ('device', 'name_pattern', 'label_pattern', 'position_pattern')
class InventoryItemCreateForm(ComponentCreateForm):
# Device is assigned by the model form
field_order = ('name_pattern', 'label_pattern')
class VirtualChassisCreateForm(NetBoxModelForm):
region = DynamicModelChoiceField(
queryset=Region.objects.all(),
@@ -251,6 +256,8 @@ class VirtualChassisCreateForm(NetBoxModelForm):
]
def clean(self):
super().clean()
if self.cleaned_data['members'] and self.cleaned_data['initial_position'] is None:
raise forms.ValidationError({
'initial_position': "A position must be specified for the first VC member."

View File

@@ -121,9 +121,14 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
def resolve_name(self, module):
if module:
return self.name.replace('{module}', module.module_bay.position)
return self.name.replace(MODULE_TOKEN, module.module_bay.position)
return self.name
def resolve_label(self, module):
if module:
return self.label.replace(MODULE_TOKEN, module.module_bay.position)
return self.label
class ConsolePortTemplate(ModularComponentTemplateModel):
"""
@@ -147,7 +152,7 @@ class ConsolePortTemplate(ModularComponentTemplateModel):
def instantiate(self, **kwargs):
return self.component_model(
name=self.resolve_name(kwargs.get('module')),
label=self.label,
label=self.resolve_label(kwargs.get('module')),
type=self.type,
**kwargs
)
@@ -175,7 +180,7 @@ class ConsoleServerPortTemplate(ModularComponentTemplateModel):
def instantiate(self, **kwargs):
return self.component_model(
name=self.resolve_name(kwargs.get('module')),
label=self.label,
label=self.resolve_label(kwargs.get('module')),
type=self.type,
**kwargs
)
@@ -215,7 +220,7 @@ class PowerPortTemplate(ModularComponentTemplateModel):
def instantiate(self, **kwargs):
return self.component_model(
name=self.resolve_name(kwargs.get('module')),
label=self.label,
label=self.resolve_label(kwargs.get('module')),
type=self.type,
maximum_draw=self.maximum_draw,
allocated_draw=self.allocated_draw,
@@ -286,7 +291,7 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
power_port = None
return self.component_model(
name=self.resolve_name(kwargs.get('module')),
label=self.label,
label=self.resolve_label(kwargs.get('module')),
type=self.type,
power_port=power_port,
feed_leg=self.feed_leg,
@@ -326,7 +331,7 @@ class InterfaceTemplate(ModularComponentTemplateModel):
def instantiate(self, **kwargs):
return self.component_model(
name=self.resolve_name(kwargs.get('module')),
label=self.label,
label=self.resolve_label(kwargs.get('module')),
type=self.type,
mgmt_only=self.mgmt_only,
**kwargs
@@ -397,7 +402,7 @@ class FrontPortTemplate(ModularComponentTemplateModel):
rear_port = None
return self.component_model(
name=self.resolve_name(kwargs.get('module')),
label=self.label,
label=self.resolve_label(kwargs.get('module')),
type=self.type,
color=self.color,
rear_port=rear_port,
@@ -437,7 +442,7 @@ class RearPortTemplate(ModularComponentTemplateModel):
def instantiate(self, **kwargs):
return self.component_model(
name=self.resolve_name(kwargs.get('module')),
label=self.label,
label=self.resolve_label(kwargs.get('module')),
type=self.type,
color=self.color,
positions=self.positions,

View File

@@ -77,7 +77,7 @@ class ComponentModel(NetBoxModel):
def to_objectchange(self, action):
objectchange = super().to_objectchange(action)
objectchange.related_object = self.device
return super().to_objectchange(action)
return objectchange
@property
def parent_object(self):
@@ -543,7 +543,8 @@ class Interface(ModularComponentModel, BaseInterface, LinkTermination, PathEndpo
)
speed = models.PositiveIntegerField(
blank=True,
null=True
null=True,
verbose_name='Speed (Kbps)'
)
duplex = models.CharField(
max_length=50,
@@ -1070,3 +1071,12 @@ class InventoryItem(MPTTModel, ComponentModel):
def get_absolute_url(self):
return reverse('dcim:inventoryitem', kwargs={'pk': self.pk})
def clean(self):
super().clean()
# An InventoryItem cannot be its own parent
if self.pk and self.parent_id == self.pk:
raise ValidationError({
"parent": "Cannot assign self as parent."
})

View File

@@ -257,6 +257,7 @@ class DeviceType(NetBoxModel):
{
'name': c.name,
'label': c.label,
'position': c.position,
'description': c.description,
}
for c in self.modulebaytemplates.all()
@@ -747,8 +748,12 @@ class Device(NetBoxModel, ConfigContextModel):
return f'{self.name} ({self.asset_tag})'
elif self.name:
return self.name
elif self.virtual_chassis and self.asset_tag:
return f'{self.virtual_chassis.name}:{self.vc_position} ({self.asset_tag})'
elif self.virtual_chassis:
return f'{self.virtual_chassis.name}:{self.vc_position} ({self.pk})'
elif self.device_type and self.asset_tag:
return f'{self.device_type.manufacturer} {self.device_type.model} ({self.asset_tag})'
elif self.device_type:
return f'{self.device_type.manufacturer} {self.device_type.model} ({self.pk})'
return super().__str__()
@@ -1064,30 +1069,52 @@ class Module(NetBoxModel, ConfigContextModel):
super().save(*args, **kwargs)
# If this is a new Module and component replication has not been disabled, instantiate all its
# related components per the ModuleType definition
if is_new and not getattr(self, '_disable_replication', False):
ConsolePort.objects.bulk_create(
[x.instantiate(device=self.device, module=self) for x in self.module_type.consoleporttemplates.all()]
)
ConsoleServerPort.objects.bulk_create(
[x.instantiate(device=self.device, module=self) for x in self.module_type.consoleserverporttemplates.all()]
)
PowerPort.objects.bulk_create(
[x.instantiate(device=self.device, module=self) for x in self.module_type.powerporttemplates.all()]
)
PowerOutlet.objects.bulk_create(
[x.instantiate(device=self.device, module=self) for x in self.module_type.poweroutlettemplates.all()]
)
Interface.objects.bulk_create(
[x.instantiate(device=self.device, module=self) for x in self.module_type.interfacetemplates.all()]
)
RearPort.objects.bulk_create(
[x.instantiate(device=self.device, module=self) for x in self.module_type.rearporttemplates.all()]
)
FrontPort.objects.bulk_create(
[x.instantiate(device=self.device, module=self) for x in self.module_type.frontporttemplates.all()]
)
adopt_components = getattr(self, '_adopt_components', False)
disable_replication = getattr(self, '_disable_replication', False)
# We skip adding components if the module is being edited or
# both replication and component adoption is disabled
if not is_new or (disable_replication and not adopt_components):
return
# Iterate all component types
for templates, component_attribute, component_model in [
("consoleporttemplates", "consoleports", ConsolePort),
("consoleserverporttemplates", "consoleserverports", ConsoleServerPort),
("interfacetemplates", "interfaces", Interface),
("powerporttemplates", "powerports", PowerPort),
("poweroutlettemplates", "poweroutlets", PowerOutlet),
("rearporttemplates", "rearports", RearPort),
("frontporttemplates", "frontports", FrontPort)
]:
create_instances = []
update_instances = []
# Prefetch installed components
installed_components = {
component.name: component for component in getattr(self.device, component_attribute).filter(module__isnull=True)
}
# Get the template for the module type.
for template in getattr(self.module_type, templates).all():
template_instance = template.instantiate(device=self.device, module=self)
if adopt_components:
existing_item = installed_components.get(template_instance.name)
# Check if there's a component with the same name already
if existing_item:
# Assign it to the module
existing_item.module = self
update_instances.append(existing_item)
continue
# Only create new components if replication is enabled
if not disable_replication:
create_instances.append(template_instance)
component_model.objects.bulk_create(create_instances)
component_model.objects.bulk_update(update_instances, ['module'])
#

View File

@@ -367,7 +367,7 @@ class Location(NestedGroupModel):
to='extras.ImageAttachment'
)
clone_fields = ['site', 'parent', 'description']
clone_fields = ['site', 'parent', 'tenant', 'description']
class Meta:
ordering = ['site', 'name']

View File

@@ -114,7 +114,7 @@ class RackElevationSVG:
# Embed front device type image if one exists
if self.include_images and device.device_type.front_image:
image = drawing.image(
href=device.device_type.front_image.url,
href='{}{}'.format(self.base_url, device.device_type.front_image.url),
insert=start,
size=end,
class_='device-image'
@@ -140,7 +140,7 @@ class RackElevationSVG:
# Embed rear device type image if one exists
if self.include_images and device.device_type.rear_image:
image = drawing.image(
href=device.device_type.rear_image.url,
href='{}{}'.format(self.base_url, device.device_type.rear_image.url),
insert=start,
size=end,
class_='device-image'
@@ -151,9 +151,9 @@ class RackElevationSVG:
stroke_width='0.2em', stroke_linejoin='round', class_='device-image-label'))
link.add(drawing.text(get_device_name(device), insert=text, fill='white', class_='device-image-label'))
@staticmethod
def _draw_empty(drawing, rack, start, end, text, id_, face_id, class_, reservation):
link_url = '{}?{}'.format(
def _draw_empty(self, drawing, rack, start, end, text, id_, face_id, class_, reservation):
link_url = '{}{}?{}'.format(
self.base_url,
reverse('dcim:device_add'),
urlencode({
'site': rack.site.pk,

View File

@@ -190,7 +190,7 @@ class DeviceTable(NetBoxTable):
verbose_name='VC Priority'
)
comments = columns.MarkdownColumn()
contacts = tables.ManyToManyColumn(
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
@@ -739,13 +739,22 @@ class ModuleBayTable(DeviceComponentTable):
linkify=True,
verbose_name='Installed module'
)
module_serial = tables.Column(
accessor=tables.A('installed_module__serial')
)
module_asset_tag = tables.Column(
accessor=tables.A('installed_module__asset_tag')
)
tags = columns.TagColumn(
url_name='dcim:modulebay_list'
)
class Meta(DeviceComponentTable.Meta):
model = ModuleBay
fields = ('pk', 'id', 'name', 'device', 'label', 'position', 'installed_module', 'description', 'tags')
fields = (
'pk', 'id', 'name', 'device', 'label', 'position', 'installed_module', 'module_serial', 'module_asset_tag',
'description', 'tags',
)
default_columns = ('pk', 'name', 'device', 'label', 'installed_module', 'description')
@@ -756,7 +765,10 @@ class DeviceModuleBayTable(ModuleBayTable):
class Meta(DeviceComponentTable.Meta):
model = ModuleBay
fields = ('pk', 'id', 'name', 'label', 'position', 'installed_module', 'description', 'tags', 'actions')
fields = (
'pk', 'id', 'name', 'label', 'position', 'installed_module', 'module_serial', 'module_asset_tag',
'description', 'tags', 'actions',
)
default_columns = ('pk', 'name', 'label', 'installed_module', 'description')

View File

@@ -31,7 +31,9 @@ class ManufacturerTable(NetBoxTable):
name = tables.Column(
linkify=True
)
devicetype_count = tables.Column(
devicetype_count = columns.LinkedCountColumn(
viewname='dcim:devicetype_list',
url_params={'manufacturer_id': 'pk'},
verbose_name='Device Types'
)
inventoryitem_count = tables.Column(
@@ -41,7 +43,7 @@ class ManufacturerTable(NetBoxTable):
verbose_name='Platforms'
)
slug = tables.Column()
contacts = tables.ManyToManyColumn(
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(

View File

@@ -26,7 +26,7 @@ class PowerPanelTable(NetBoxTable):
url_params={'power_panel_id': 'pk'},
verbose_name='Feeds'
)
contacts = tables.ManyToManyColumn(
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(

View File

@@ -69,7 +69,7 @@ class RackTable(NetBoxTable):
orderable=False,
verbose_name='Power'
)
contacts = tables.ManyToManyColumn(
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(

View File

@@ -26,7 +26,7 @@ class RegionTable(NetBoxTable):
url_params={'region_id': 'pk'},
verbose_name='Sites'
)
contacts = tables.ManyToManyColumn(
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
@@ -55,7 +55,7 @@ class SiteGroupTable(NetBoxTable):
url_params={'group_id': 'pk'},
verbose_name='Sites'
)
contacts = tables.ManyToManyColumn(
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
@@ -86,7 +86,7 @@ class SiteTable(NetBoxTable):
group = tables.Column(
linkify=True
)
asns = tables.ManyToManyColumn(
asns = columns.ManyToManyColumn(
linkify_item=True,
verbose_name='ASNs'
)
@@ -98,7 +98,7 @@ class SiteTable(NetBoxTable):
)
tenant = TenantColumn()
comments = columns.MarkdownColumn()
contacts = tables.ManyToManyColumn(
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
@@ -137,7 +137,7 @@ class LocationTable(NetBoxTable):
url_params={'location_id': 'pk'},
verbose_name='Devices'
)
contacts = tables.ManyToManyColumn(
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(

View File

@@ -385,7 +385,7 @@ MODULEBAY_BUTTONS = """
<i class="mdi mdi-server-minus" aria-hidden="true" title="Remove module"></i>
</a>
{% else %}
<a href="{% url 'dcim:module_add' %}?device={{ record.device.pk }}&module_bay={{ record.pk }}&return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-success btn-sm">
<a href="{% url 'dcim:module_add' %}?device={{ record.device.pk }}&module_bay={{ record.pk }}&manufacturer={{ object.device_type.manufacturer_id }}&return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-success btn-sm">
<i class="mdi mdi-server-plus" aria-hidden="true" title="Install module"></i>
</a>
{% endif %}

View File

@@ -523,6 +523,9 @@ class ConsolePortTemplateTest(APIViewTestCases.APIViewTestCase):
devicetype = DeviceType.objects.create(
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
)
moduletype = ModuleType.objects.create(
manufacturer=manufacturer, model='Module Type 1'
)
console_port_templates = (
ConsolePortTemplate(device_type=devicetype, name='Console Port Template 1'),
@@ -541,9 +544,13 @@ class ConsolePortTemplateTest(APIViewTestCases.APIViewTestCase):
'name': 'Console Port Template 5',
},
{
'device_type': devicetype.pk,
'module_type': moduletype.pk,
'name': 'Console Port Template 6',
},
{
'module_type': moduletype.pk,
'name': 'Console Port Template 7',
},
]
@@ -560,6 +567,9 @@ class ConsoleServerPortTemplateTest(APIViewTestCases.APIViewTestCase):
devicetype = DeviceType.objects.create(
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
)
moduletype = ModuleType.objects.create(
manufacturer=manufacturer, model='Module Type 1'
)
console_server_port_templates = (
ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port Template 1'),
@@ -578,9 +588,13 @@ class ConsoleServerPortTemplateTest(APIViewTestCases.APIViewTestCase):
'name': 'Console Server Port Template 5',
},
{
'device_type': devicetype.pk,
'module_type': moduletype.pk,
'name': 'Console Server Port Template 6',
},
{
'module_type': moduletype.pk,
'name': 'Console Server Port Template 7',
},
]
@@ -597,6 +611,9 @@ class PowerPortTemplateTest(APIViewTestCases.APIViewTestCase):
devicetype = DeviceType.objects.create(
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
)
moduletype = ModuleType.objects.create(
manufacturer=manufacturer, model='Module Type 1'
)
power_port_templates = (
PowerPortTemplate(device_type=devicetype, name='Power Port Template 1'),
@@ -615,9 +632,13 @@ class PowerPortTemplateTest(APIViewTestCases.APIViewTestCase):
'name': 'Power Port Template 5',
},
{
'device_type': devicetype.pk,
'module_type': moduletype.pk,
'name': 'Power Port Template 6',
},
{
'module_type': moduletype.pk,
'name': 'Power Port Template 7',
},
]
@@ -634,6 +655,9 @@ class PowerOutletTemplateTest(APIViewTestCases.APIViewTestCase):
devicetype = DeviceType.objects.create(
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
)
moduletype = ModuleType.objects.create(
manufacturer=manufacturer, model='Module Type 1'
)
power_port_templates = (
PowerPortTemplate(device_type=devicetype, name='Power Port Template 1'),
@@ -664,6 +688,14 @@ class PowerOutletTemplateTest(APIViewTestCases.APIViewTestCase):
'name': 'Power Outlet Template 6',
'power_port': None,
},
{
'module_type': moduletype.pk,
'name': 'Power Outlet Template 7',
},
{
'module_type': moduletype.pk,
'name': 'Power Outlet Template 8',
},
]
@@ -680,6 +712,9 @@ class InterfaceTemplateTest(APIViewTestCases.APIViewTestCase):
devicetype = DeviceType.objects.create(
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
)
moduletype = ModuleType.objects.create(
manufacturer=manufacturer, model='Module Type 1'
)
interface_templates = (
InterfaceTemplate(device_type=devicetype, name='Interface Template 1', type='1000base-t'),
@@ -700,10 +735,15 @@ class InterfaceTemplateTest(APIViewTestCases.APIViewTestCase):
'type': '1000base-t',
},
{
'device_type': devicetype.pk,
'module_type': moduletype.pk,
'name': 'Interface Template 6',
'type': '1000base-t',
},
{
'module_type': moduletype.pk,
'name': 'Interface Template 7',
'type': '1000base-t',
},
]
@@ -720,14 +760,19 @@ class FrontPortTemplateTest(APIViewTestCases.APIViewTestCase):
devicetype = DeviceType.objects.create(
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
)
moduletype = ModuleType.objects.create(
manufacturer=manufacturer, model='Module Type 1'
)
rear_port_templates = (
RearPortTemplate(device_type=devicetype, name='Rear Port Template 1', type=PortTypeChoices.TYPE_8P8C),
RearPortTemplate(device_type=devicetype, name='Rear Port Template 2', type=PortTypeChoices.TYPE_8P8C),
RearPortTemplate(device_type=devicetype, name='Rear Port Template 3', type=PortTypeChoices.TYPE_8P8C),
RearPortTemplate(device_type=devicetype, name='Rear Port Template 4', type=PortTypeChoices.TYPE_8P8C),
RearPortTemplate(device_type=devicetype, name='Rear Port Template 5', type=PortTypeChoices.TYPE_8P8C),
RearPortTemplate(device_type=devicetype, name='Rear Port Template 6', type=PortTypeChoices.TYPE_8P8C),
RearPortTemplate(module_type=moduletype, name='Rear Port Template 5', type=PortTypeChoices.TYPE_8P8C),
RearPortTemplate(module_type=moduletype, name='Rear Port Template 6', type=PortTypeChoices.TYPE_8P8C),
RearPortTemplate(module_type=moduletype, name='Rear Port Template 7', type=PortTypeChoices.TYPE_8P8C),
RearPortTemplate(module_type=moduletype, name='Rear Port Template 8', type=PortTypeChoices.TYPE_8P8C),
)
RearPortTemplate.objects.bulk_create(rear_port_templates)
@@ -745,15 +790,28 @@ class FrontPortTemplateTest(APIViewTestCases.APIViewTestCase):
rear_port=rear_port_templates[1]
),
FrontPortTemplate(
device_type=devicetype,
name='Front Port Template 3',
module_type=moduletype,
name='Front Port Template 5',
type=PortTypeChoices.TYPE_8P8C,
rear_port=rear_port_templates[2]
rear_port=rear_port_templates[4]
),
FrontPortTemplate(
module_type=moduletype,
name='Front Port Template 6',
type=PortTypeChoices.TYPE_8P8C,
rear_port=rear_port_templates[5]
),
)
FrontPortTemplate.objects.bulk_create(front_port_templates)
cls.create_data = [
{
'device_type': devicetype.pk,
'name': 'Front Port Template 3',
'type': PortTypeChoices.TYPE_8P8C,
'rear_port': rear_port_templates[2].pk,
'rear_port_position': 1,
},
{
'device_type': devicetype.pk,
'name': 'Front Port Template 4',
@@ -762,17 +820,17 @@ class FrontPortTemplateTest(APIViewTestCases.APIViewTestCase):
'rear_port_position': 1,
},
{
'device_type': devicetype.pk,
'name': 'Front Port Template 5',
'module_type': moduletype.pk,
'name': 'Front Port Template 7',
'type': PortTypeChoices.TYPE_8P8C,
'rear_port': rear_port_templates[4].pk,
'rear_port': rear_port_templates[6].pk,
'rear_port_position': 1,
},
{
'device_type': devicetype.pk,
'name': 'Front Port Template 6',
'module_type': moduletype.pk,
'name': 'Front Port Template 8',
'type': PortTypeChoices.TYPE_8P8C,
'rear_port': rear_port_templates[5].pk,
'rear_port': rear_port_templates[7].pk,
'rear_port_position': 1,
},
]
@@ -791,6 +849,9 @@ class RearPortTemplateTest(APIViewTestCases.APIViewTestCase):
devicetype = DeviceType.objects.create(
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
)
moduletype = ModuleType.objects.create(
manufacturer=manufacturer, model='Module Type 1'
)
rear_port_templates = (
RearPortTemplate(device_type=devicetype, name='Rear Port Template 1', type=PortTypeChoices.TYPE_8P8C),
@@ -811,10 +872,15 @@ class RearPortTemplateTest(APIViewTestCases.APIViewTestCase):
'type': PortTypeChoices.TYPE_8P8C,
},
{
'device_type': devicetype.pk,
'module_type': moduletype.pk,
'name': 'Rear Port Template 6',
'type': PortTypeChoices.TYPE_8P8C,
},
{
'module_type': moduletype.pk,
'name': 'Rear Port Template 7',
'type': PortTypeChoices.TYPE_8P8C,
},
]

View File

@@ -521,10 +521,26 @@ class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests):
@classmethod
def setUpTestData(cls):
regions = (
Region(name='Region 1', slug='region-1'),
Region(name='Region 2', slug='region-2'),
Region(name='Region 3', slug='region-3'),
)
for region in regions:
region.save()
groups = (
SiteGroup(name='Site Group 1', slug='site-group-1'),
SiteGroup(name='Site Group 2', slug='site-group-2'),
SiteGroup(name='Site Group 3', slug='site-group-3'),
)
for group in groups:
group.save()
sites = (
Site(name='Site 1', slug='site-1'),
Site(name='Site 2', slug='site-2'),
Site(name='Site 3', slug='site-3'),
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
)
Site.objects.bulk_create(sites)
@@ -572,6 +588,20 @@ class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests):
)
RackReservation.objects.bulk_create(reservations)
def test_region(self):
regions = Region.objects.all()[:2]
params = {'region_id': [regions[0].pk, regions[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'region': [regions[0].slug, regions[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_site_group(self):
site_groups = SiteGroup.objects.all()[:2]
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_site(self):
sites = Site.objects.all()[:2]
params = {'site_id': [sites[0].pk, sites[1].pk]}
@@ -698,6 +728,9 @@ class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
DeviceBayTemplate(device_type=device_types[0], name='Device Bay 1'),
DeviceBayTemplate(device_type=device_types[1], name='Device Bay 2'),
))
# Assigned DeviceType must have parent subdevice_role
inventory_item = InventoryItemTemplate(device_type=device_types[1], name='Inventory Item 1')
inventory_item.save()
def test_model(self):
params = {'model': ['Model 1', 'Model 2']}
@@ -784,6 +817,12 @@ class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'module_bays': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_inventory_items(self):
params = {'inventory_items': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
params = {'inventory_items': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = ModuleType.objects.all()

View File

@@ -1869,6 +1869,44 @@ class ModuleTestCase(
self.assertHttpStatus(self.client.post(**request), 302)
self.assertEqual(Interface.objects.filter(device=device).count(), 5)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_module_component_adoption(self):
self.add_permissions('dcim.add_module')
interface_name = "Interface-1"
# Add an interface to the ModuleType
module_type = ModuleType.objects.first()
InterfaceTemplate(module_type=module_type, name=interface_name).save()
form_data = self.form_data.copy()
device = Device.objects.get(pk=form_data['device'])
# Create an interface to be adopted
interface = Interface(device=device, name=interface_name, type=InterfaceTypeChoices.TYPE_10GE_FIXED)
interface.save()
# Ensure that interface is created with no module
self.assertIsNone(interface.module)
# Create a module with adopted components
form_data['module_bay'] = ModuleBay.objects.filter(device=device).first()
form_data['module_type'] = module_type
form_data['replicate_components'] = False
form_data['adopt_components'] = True
request = {
'path': self._get_url('add'),
'data': post_data(form_data),
}
self.assertHttpStatus(self.client.post(**request), 302)
# Re-retrieve interface to get new module id
interface.refresh_from_db()
# Check that the Interface now has a module
self.assertIsNotNone(interface.module)
class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
model = ConsolePort

View File

@@ -14,7 +14,7 @@ from django.views.generic import View
from circuits.models import Circuit
from extras.views import ObjectConfigContextView
from ipam.models import ASN, IPAddress, Prefix, Service, VLAN
from ipam.models import ASN, IPAddress, Prefix, Service, VLAN, VLANGroup
from ipam.tables import AssignedIPAddressesTable, InterfaceVLANTable
from netbox.views import generic
from utilities.forms import ConfirmationForm
@@ -166,7 +166,7 @@ class RegionView(generic.ObjectView):
sites = Site.objects.restrict(request.user, 'view').filter(
region=instance
)
sites_table = tables.SiteTable(sites, exclude=('region',))
sites_table = tables.SiteTable(sites, user=request.user, exclude=('region',))
sites_table.configure(request)
return {
@@ -251,7 +251,7 @@ class SiteGroupView(generic.ObjectView):
sites = Site.objects.restrict(request.user, 'view').filter(
group=instance
)
sites_table = tables.SiteTable(sites, exclude=('group',))
sites_table = tables.SiteTable(sites, user=request.user, exclude=('group',))
sites_table.configure(request)
return {
@@ -320,6 +320,10 @@ class SiteView(generic.ObjectView):
'rack_count': Rack.objects.restrict(request.user, 'view').filter(site=instance).count(),
'device_count': Device.objects.restrict(request.user, 'view').filter(site=instance).count(),
'prefix_count': Prefix.objects.restrict(request.user, 'view').filter(site=instance).count(),
'vlangroup_count': VLANGroup.objects.restrict(request.user, 'view').filter(
scope_type=ContentType.objects.get_for_model(Site),
scope_id=instance.pk
).count(),
'vlan_count': VLAN.objects.restrict(request.user, 'view').filter(site=instance).count(),
'circuit_count': Circuit.objects.restrict(request.user, 'view').filter(terminations__site=instance).count(),
'vm_count': VirtualMachine.objects.restrict(request.user, 'view').filter(cluster__site=instance).count(),
@@ -338,6 +342,7 @@ class SiteView(generic.ObjectView):
'device_count',
cumulative=True
).restrict(request.user, 'view').filter(site=instance)
nonracked_devices = Device.objects.filter(
site=instance,
position__isnull=True,
@@ -353,7 +358,8 @@ class SiteView(generic.ObjectView):
'stats': stats,
'locations': locations,
'asns': asns,
'nonracked_devices': nonracked_devices,
'nonracked_devices': nonracked_devices.order_by('-pk')[:10],
'total_nonracked_devices_count': nonracked_devices.count(),
}
@@ -429,8 +435,9 @@ class LocationView(generic.ObjectView):
'rack_count',
cumulative=True
).filter(pk__in=location_ids).exclude(pk=instance.pk)
child_locations_table = tables.LocationTable(child_locations)
child_locations_table = tables.LocationTable(child_locations, user=request.user)
child_locations_table.configure(request)
nonracked_devices = Device.objects.filter(
location=instance,
position__isnull=True,
@@ -441,7 +448,8 @@ class LocationView(generic.ObjectView):
'rack_count': rack_count,
'device_count': device_count,
'child_locations_table': child_locations_table,
'nonracked_devices': nonracked_devices,
'nonracked_devices': nonracked_devices.order_by('-pk')[:10],
'total_nonracked_devices_count': nonracked_devices.count(),
}
@@ -502,11 +510,13 @@ class RackRoleView(generic.ObjectView):
queryset = RackRole.objects.all()
def get_extra_context(self, request, instance):
racks = Rack.objects.restrict(request.user, 'view').filter(
role=instance
racks = Rack.objects.restrict(request.user, 'view').filter(role=instance).annotate(
device_count=count_related(Device, 'rack')
)
racks_table = tables.RackTable(racks, exclude=('role', 'get_utilization', 'get_power_utilization'))
racks_table = tables.RackTable(racks, user=request.user, exclude=(
'role', 'get_utilization', 'get_power_utilization',
))
racks_table.configure(request)
return {
@@ -759,7 +769,7 @@ class ManufacturerView(generic.ObjectView):
manufacturer=instance
)
devicetypes_table = tables.DeviceTypeTable(device_types, exclude=('manufacturer',))
devicetypes_table = tables.DeviceTypeTable(device_types, user=request.user, exclude=('manufacturer',))
devicetypes_table.configure(request)
return {
@@ -960,7 +970,7 @@ class DeviceTypeBulkDeleteView(generic.BulkDeleteView):
class ModuleTypeListView(generic.ObjectListView):
queryset = ModuleType.objects.prefetch_related('manufacturer').annotate(
# instance_count=count_related(Module, 'module_type')
instance_count=count_related(Module, 'module_type')
)
filterset = filtersets.ModuleTypeFilterSet
filterset_form = forms.ModuleTypeFilterForm
@@ -1066,7 +1076,7 @@ class ModuleTypeImportView(generic.ObjectImportView):
class ModuleTypeBulkEditView(generic.BulkEditView):
queryset = ModuleType.objects.prefetch_related('manufacturer').annotate(
# instance_count=count_related(Module, 'module_type')
instance_count=count_related(Module, 'module_type')
)
filterset = filtersets.ModuleTypeFilterSet
table = tables.ModuleTypeTable
@@ -1075,7 +1085,7 @@ class ModuleTypeBulkEditView(generic.BulkEditView):
class ModuleTypeBulkDeleteView(generic.BulkDeleteView):
queryset = ModuleType.objects.prefetch_related('manufacturer').annotate(
# instance_count=count_related(Module, 'module_type')
instance_count=count_related(Module, 'module_type')
)
filterset = filtersets.ModuleTypeFilterSet
table = tables.ModuleTypeTable
@@ -1472,7 +1482,7 @@ class DeviceRoleView(generic.ObjectView):
devices = Device.objects.restrict(request.user, 'view').filter(
device_role=instance
)
devices_table = tables.DeviceTable(devices, exclude=('device_role',))
devices_table = tables.DeviceTable(devices, user=request.user, exclude=('device_role',))
devices_table.configure(request)
return {
@@ -1536,7 +1546,7 @@ class PlatformView(generic.ObjectView):
devices = Device.objects.restrict(request.user, 'view').filter(
platform=instance
)
devices_table = tables.DeviceTable(devices, exclude=('platform',))
devices_table = tables.DeviceTable(devices, user=request.user, exclude=('platform',))
devices_table.configure(request)
return {
@@ -2513,7 +2523,7 @@ class InventoryItemEditView(generic.ObjectEditView):
class InventoryItemCreateView(generic.ComponentCreateView):
queryset = InventoryItem.objects.all()
form = forms.DeviceComponentCreateForm
form = forms.InventoryItemCreateForm
model_form = forms.InventoryItemForm
template_name = 'dcim/inventoryitem_create.html'

View File

@@ -40,7 +40,7 @@ class ConfigRevisionAdmin(admin.ModelAdmin):
'fields': ('DEFAULT_USER_PREFERENCES',),
}),
('Miscellaneous', {
'fields': ('MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'MAPS_URL'),
'fields': ('MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'JOBRESULT_RETENTION', 'MAPS_URL'),
}),
('Config Revision', {
'fields': ('comment',),

View File

@@ -179,7 +179,7 @@ class ReportViewSet(ViewSet):
for r in JobResult.objects.filter(
obj_type=report_content_type,
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
).defer('data')
).order_by('name', '-created').distinct('name').defer('data')
}
# Iterate through all available Reports.
@@ -236,7 +236,8 @@ class ReportViewSet(ViewSet):
run_report,
report.full_name,
report_content_type,
request.user
request.user,
job_timeout=report.job_timeout
)
report.result = job_result
@@ -270,7 +271,7 @@ class ScriptViewSet(ViewSet):
for r in JobResult.objects.filter(
obj_type=script_content_type,
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
).defer('data').order_by('created')
).order_by('name', '-created').distinct('name').defer('data')
}
flat_list = []
@@ -320,7 +321,8 @@ class ScriptViewSet(ViewSet):
request.user,
data=data,
request=copy_safe_request(request),
commit=commit
commit=commit,
job_timeout=script.job_timeout,
)
script.result = job_result
serializer = serializers.ScriptDetailSerializer(script, context={'request': request})

View File

@@ -27,6 +27,12 @@ class CustomFieldCSVForm(CSVModelForm):
choices=CustomFieldTypeChoices,
help_text='Field data type (e.g. text, integer, etc.)'
)
object_type = CSVContentTypeField(
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_fields'),
required=False,
help_text="Object type (for object or multi-object fields)"
)
choices = SimpleArrayField(
base_field=forms.CharField(),
required=False,
@@ -36,8 +42,9 @@ class CustomFieldCSVForm(CSVModelForm):
class Meta:
model = CustomField
fields = (
'name', 'label', 'type', 'content_types', 'required', 'description', 'weight', 'filter_logic', 'default',
'choices', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex',
'name', 'label', 'type', 'content_types', 'object_type', 'required', 'description', 'weight',
'filter_logic', 'default', 'choices', 'weight', 'validation_minimum', 'validation_maximum',
'validation_regex',
)

View File

@@ -0,0 +1,11 @@
from django.core.cache import cache
from django.core.management.base import BaseCommand
class Command(BaseCommand):
"""Command to clear the entire cache."""
help = 'Clears the cache.'
def handle(self, *args, **kwargs):
cache.clear()
self.stdout.write('Cache has been cleared.', ending="\n")

View File

@@ -9,6 +9,7 @@ from django.db import DEFAULT_DB_ALIAS
from django.utils import timezone
from packaging import version
from extras.models import JobResult
from extras.models import ObjectChange
from netbox.config import Config
@@ -63,6 +64,33 @@ class Command(BaseCommand):
f"\tSkipping: No retention period specified (CHANGELOG_RETENTION = {config.CHANGELOG_RETENTION})"
)
# Delete expired JobResults
if options['verbosity']:
self.stdout.write("[*] Checking for expired jobresult records")
if config.JOBRESULT_RETENTION:
cutoff = timezone.now() - timedelta(days=config.JOBRESULT_RETENTION)
if options['verbosity'] >= 2:
self.stdout.write(f"\tRetention period: {config.JOBRESULT_RETENTION} days")
self.stdout.write(f"\tCut-off time: {cutoff}")
expired_records = JobResult.objects.filter(created__lt=cutoff).count()
if expired_records:
if options['verbosity']:
self.stdout.write(
f"\tDeleting {expired_records} expired records... ",
self.style.WARNING,
ending=""
)
self.stdout.flush()
JobResult.objects.filter(created__lt=cutoff)._raw_delete(using=DEFAULT_DB_ALIAS)
if options['verbosity']:
self.stdout.write("Done.", self.style.SUCCESS)
elif options['verbosity']:
self.stdout.write("\tNo expired records found.", self.style.SUCCESS)
elif options['verbosity']:
self.stdout.write(
f"\tSkipping: No retention period specified (JOBRESULT_RETENTION = {config.JOBRESULT_RETENTION})"
)
# Check for new releases (if enabled)
if options['verbosity']:
self.stdout.write("[*] Checking for latest release")

View File

@@ -35,7 +35,8 @@ class Command(BaseCommand):
run_report,
report.full_name,
report_content_type,
None
None,
job_timeout=report.job_timeout
)
# Wait on the job to finish

View File

@@ -14,6 +14,7 @@ from extras.choices import JobResultStatusChoices
from extras.context_managers import change_logging
from extras.models import JobResult
from extras.scripts import get_script
from extras.signals import clear_webhooks
from utilities.exceptions import AbortTransaction
from utilities.utils import NetBoxFakeRequest
@@ -49,7 +50,7 @@ class Command(BaseCommand):
except AbortTransaction:
script.log_info("Database changes have been reverted automatically.")
clear_webhooks.send(request)
except Exception as e:
stacktrace = traceback.format_exc()
script.log_failure(
@@ -58,7 +59,7 @@ class Command(BaseCommand):
script.log_info("Database changes have been reverted due to error.")
logger.error(f"Exception raised during script execution: {e}")
job_result.set_status(JobResultStatusChoices.STATUS_ERRORED)
clear_webhooks.send(request)
finally:
job_result.data = ScriptOutputSerializer(script).data
job_result.save()
@@ -113,13 +114,6 @@ class Command(BaseCommand):
script_content_type = ContentType.objects.get(app_label='extras', model='script')
# Delete any previous terminal state results
JobResult.objects.filter(
obj_type=script_content_type,
name=script.full_name,
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
).delete()
# Create the job result
job_result = JobResult.objects.create(
name=script.full_name,

View File

@@ -13,6 +13,7 @@ from django.urls import reverse
from django.utils import timezone
from django.utils.formats import date_format
from rest_framework.utils.encoders import JSONEncoder
import django_rq
from extras.choices import *
from extras.constants import *
@@ -550,7 +551,8 @@ class JobResult(models.Model):
job_id=uuid.uuid4()
)
func.delay(*args, job_id=str(job_result.job_id), job_result=job_result, **kwargs)
queue = django_rq.get_queue("default")
queue.enqueue(func, job_id=str(job_result.job_id), job_result=job_result, **kwargs)
return job_result

View File

@@ -84,15 +84,6 @@ def run_report(job_result, *args, **kwargs):
job_result.save()
logging.error(f"Error during execution of report {job_result.name}")
# Delete any previous terminal state results
JobResult.objects.filter(
obj_type=job_result.obj_type,
name=job_result.name,
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
).exclude(
pk=job_result.pk
).delete()
class Report(object):
"""
@@ -119,6 +110,7 @@ class Report(object):
}
"""
description = None
job_timeout = None
def __init__(self):

View File

@@ -5,6 +5,7 @@ import os
import pkgutil
import sys
import traceback
import threading
from collections import OrderedDict
import yaml
@@ -13,11 +14,10 @@ from django.conf import settings
from django.core.validators import RegexValidator
from django.db import transaction
from django.utils.functional import classproperty
from django_rq import job
from extras.api.serializers import ScriptOutputSerializer
from extras.choices import JobResultStatusChoices, LogLevelChoices
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
@@ -42,6 +42,8 @@ __all__ = [
'TextVar',
]
lock = threading.Lock()
#
# Script variables
@@ -298,12 +300,23 @@ class BaseScript:
def module(cls):
return cls.__module__
@classproperty
def job_timeout(self):
return getattr(self.Meta, 'job_timeout', None)
@classmethod
def _get_vars(cls):
vars = {}
for name, attr in cls.__dict__.items():
if name not in vars and issubclass(attr.__class__, ScriptVariable):
vars[name] = attr
# Iterate all base classes looking for ScriptVariables
for base_class in inspect.getmro(cls):
# When object is reached there's no reason to continue
if base_class is object:
break
for name, attr in base_class.__dict__.items():
if name not in vars and issubclass(attr.__class__, ScriptVariable):
vars[name] = attr
# Order variables according to field_order
field_order = getattr(cls.Meta, 'field_order', None)
@@ -414,7 +427,6 @@ def is_variable(obj):
return isinstance(obj, ScriptVariable)
@job('default')
def run_script(data, request, commit=True, *args, **kwargs):
"""
A wrapper for calling Script.run(). This performs error handling and provides a hook for committing changes. It
@@ -454,7 +466,7 @@ 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 Exception as e:
stacktrace = traceback.format_exc()
script.log_failure(
@@ -463,7 +475,7 @@ def run_script(data, request, commit=True, *args, **kwargs):
script.log_info("Database changes have been reverted due to error.")
logger.error(f"Exception raised during script execution: {e}")
job_result.set_status(JobResultStatusChoices.STATUS_ERRORED)
clear_webhooks.send(request)
finally:
job_result.data = ScriptOutputSerializer(script).data
job_result.save()
@@ -478,15 +490,6 @@ def run_script(data, request, commit=True, *args, **kwargs):
else:
_run_script()
# Delete any previous terminal state results
JobResult.objects.filter(
obj_type=job_result.obj_type,
name=job_result.name,
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
).exclude(
pk=job_result.pk
).delete()
def get_scripts(use_names=False):
"""
@@ -494,14 +497,17 @@ def get_scripts(use_names=False):
defined name in place of the actual module name.
"""
scripts = OrderedDict()
# Iterate through all modules within the reports path. These are the user-created files in which reports are
# Iterate through all modules within the scripts path. These are the user-created files in which reports are
# defined.
for importer, module_name, _ in pkgutil.iter_modules([settings.SCRIPTS_ROOT]):
# Remove cached module to ensure consistency with filesystem
if module_name in sys.modules:
del sys.modules[module_name]
# 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:
del sys.modules[module_name]
module = importer.find_module(module_name).load_module(module_name)
module = importer.find_module(module_name).load_module(module_name)
if use_names and hasattr(module, 'name'):
module_name = module.name
module_scripts = OrderedDict()

View File

@@ -39,10 +39,11 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase):
}
cls.csv_data = (
'name,label,type,content_types,weight,filter_logic,choices,validation_minimum,validation_maximum,validation_regex',
'field4,Field 4,text,dcim.site,100,exact,,,,[a-z]{3}',
'field5,Field 5,integer,dcim.site,100,exact,,1,100,',
'field6,Field 6,select,dcim.site,100,exact,"A,B,C",,,',
'name,label,type,content_types,object_type,weight,filter_logic,choices,validation_minimum,validation_maximum,validation_regex',
'field4,Field 4,text,dcim.site,,100,exact,,,,[a-z]{3}',
'field5,Field 5,integer,dcim.site,,100,exact,,1,100,',
'field6,Field 6,select,dcim.site,,100,exact,"A,B,C",,,',
'field7,Field 7,object,dcim.site,dcim.region,100,exact,,,,',
)
cls.bulk_edit_data = {

View File

@@ -524,7 +524,7 @@ class ReportListView(ContentTypePermissionRequiredMixin, View):
for r in JobResult.objects.filter(
obj_type=report_content_type,
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
).defer('data')
).order_by('name', '-created').distinct('name').defer('data')
}
ret = []
@@ -588,7 +588,8 @@ class ReportView(ContentTypePermissionRequiredMixin, View):
run_report,
report.full_name,
report_content_type,
request.user
request.user,
job_timeout=report.job_timeout
)
return redirect('extras:report_result', job_result_pk=job_result.pk)
@@ -655,7 +656,7 @@ class ScriptListView(ContentTypePermissionRequiredMixin, View):
for r in JobResult.objects.filter(
obj_type=script_content_type,
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
).defer('data')
).order_by('name', '-created').distinct('name').defer('data')
}
for _scripts in scripts.values():
@@ -708,6 +709,7 @@ class ScriptView(ContentTypePermissionRequiredMixin, GetScriptMixin, View):
commit = form.cleaned_data.pop('_commit')
script_content_type = ContentType.objects.get(app_label='extras', model='script')
job_result = JobResult.enqueue_job(
run_script,
script.full_name,
@@ -715,7 +717,8 @@ class ScriptView(ContentTypePermissionRequiredMixin, GetScriptMixin, View):
request.user,
data=form.cleaned_data,
request=copy_safe_request(request),
commit=commit
commit=commit,
job_timeout=script.job_timeout,
)
return redirect('extras:script_result', job_result_pk=job_result.pk)

View File

@@ -91,7 +91,7 @@ class IPAddressRoleChoices(ChoiceSet):
(ROLE_VRRP, 'VRRP', 'green'),
(ROLE_HSRP, 'HSRP', 'green'),
(ROLE_GLBP, 'GLBP', 'green'),
(ROLE_CARP, 'CARP'), 'green',
(ROLE_CARP, 'CARP', 'green'),
)

View File

@@ -145,9 +145,11 @@ class AggregateFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
if not value.strip():
return queryset
qs_filter = Q(description__icontains=value)
qs_filter |= Q(prefix__contains=value.strip())
try:
prefix = str(netaddr.IPNetwork(value.strip()).cidr)
qs_filter |= Q(prefix__net_contains_or_equals=prefix)
qs_filter |= Q(prefix__contains=value.strip())
except (AddrFormatError, ValueError):
pass
return queryset.filter(qs_filter)
@@ -334,9 +336,11 @@ class PrefixFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
if not value.strip():
return queryset
qs_filter = Q(description__icontains=value)
qs_filter |= Q(prefix__contains=value.strip())
try:
prefix = str(netaddr.IPNetwork(value.strip()).cidr)
qs_filter |= Q(prefix__net_contains_or_equals=prefix)
qs_filter |= Q(prefix__contains=value.strip())
except (AddrFormatError, ValueError):
pass
return queryset.filter(qs_filter)
@@ -460,7 +464,7 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
field_name='address',
lookup_expr='family'
)
parent = django_filters.CharFilter(
parent = MultiValueCharFilter(
method='search_by_parent',
label='Parent prefix',
)
@@ -535,6 +539,11 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
queryset=VMInterface.objects.all(),
label='VM interface (ID)',
)
fhrpgroup_id = django_filters.ModelMultipleChoiceFilter(
field_name='fhrpgroup',
queryset=FHRPGroup.objects.all(),
label='FHRP group (ID)',
)
assigned_to_interface = django_filters.BooleanFilter(
method='_assigned_to_interface',
label='Is assigned to an interface',
@@ -562,14 +571,16 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
return queryset.filter(qs_filter)
def search_by_parent(self, queryset, name, value):
value = value.strip()
if not value:
return queryset
try:
query = str(netaddr.IPNetwork(value.strip()).cidr)
return queryset.filter(address__net_host_contained=query)
except (AddrFormatError, ValueError):
return queryset.none()
q = Q()
for prefix in value:
try:
query = str(netaddr.IPNetwork(prefix.strip()).cidr)
q |= Q(address__net_host_contained=query)
except (AddrFormatError, ValueError):
return queryset.none()
return queryset.filter(q)
def filter_address(self, queryset, name, value):
try:
@@ -613,7 +624,17 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
)
def _assigned_to_interface(self, queryset, name, value):
return queryset.exclude(assigned_object_id__isnull=value)
content_types = ContentType.objects.get_for_models(Interface, VMInterface).values()
if value:
return queryset.filter(
assigned_object_type__in=content_types,
assigned_object_id__isnull=False
)
else:
return queryset.exclude(
assigned_object_type__in=content_types,
assigned_object_id__isnull=False
)
class FHRPGroupFilterSet(NetBoxModelFilterSet):
@@ -666,11 +687,53 @@ class FHRPGroupAssignmentFilterSet(ChangeLoggedModelFilterSet):
queryset=FHRPGroup.objects.all(),
label='Group (ID)',
)
device = MultiValueCharFilter(
method='filter_device',
field_name='name',
label='Device (name)',
)
device_id = MultiValueNumberFilter(
method='filter_device',
field_name='pk',
label='Device (ID)',
)
virtual_machine = MultiValueCharFilter(
method='filter_virtual_machine',
field_name='name',
label='Virtual machine (name)',
)
virtual_machine_id = MultiValueNumberFilter(
method='filter_virtual_machine',
field_name='pk',
label='Virtual machine (ID)',
)
class Meta:
model = FHRPGroupAssignment
fields = ['id', 'group_id', 'interface_type', 'interface_id', 'priority']
def filter_device(self, queryset, name, value):
devices = Device.objects.filter(**{f'{name}__in': value})
if not devices.exists():
return queryset.none()
interface_ids = []
for device in devices:
interface_ids.extend(device.vc_interfaces().values_list('id', flat=True))
return queryset.filter(
Q(interface_type=ContentType.objects.get_for_model(Interface), interface_id__in=interface_ids)
)
def filter_virtual_machine(self, queryset, name, value):
virtual_machines = VirtualMachine.objects.filter(**{f'{name}__in': value})
if not virtual_machines.exists():
return queryset.none()
interface_ids = []
for vm in virtual_machines:
interface_ids.extend(vm.interfaces.values_list('id', flat=True))
return queryset.filter(
Q(interface_type=ContentType.objects.get_for_model(VMInterface), interface_id__in=interface_ids)
)
class VLANGroupFilterSet(OrganizationalModelFilterSet):
scope_type = ContentTypeFilter()

View File

@@ -377,12 +377,16 @@ class VLANGroupFilterForm(NetBoxModelFilterSetForm):
label=_('Rack')
)
min_vid = forms.IntegerField(
required=False,
min_value=VLAN_VID_MIN,
max_value=VLAN_VID_MAX,
label='Minimum VID'
)
max_vid = forms.IntegerField(
required=False,
min_value=VLAN_VID_MIN,
max_value=VLAN_VID_MAX,
label='Maximum VID'
)
tag = TagFilterField(model)

View File

@@ -1,8 +1,9 @@
# Ensure that VRFs are imported before IPs/prefixes so dumpdata & loaddata work correctly
from .fhrp import *
from .vrfs import *
from .ip import *
from .services import *
from .vlans import *
from .vrfs import *
__all__ = (
'ASN',

View File

@@ -507,16 +507,20 @@ class Prefix(GetAvailablePrefixesMixin, NetBoxModel):
child_ranges.add(iprange.range)
available_ips = prefix - child_ips - child_ranges
# IPv6, pool, or IPv4 /31-/32 sets are fully usable
if self.family == 6 or self.is_pool or (self.family == 4 and self.prefix.prefixlen >= 31):
# IPv6 /127's, pool, or IPv4 /31-/32 sets are fully usable
if (self.family == 6 and self.prefix.prefixlen >= 127) or self.is_pool or (self.family == 4 and self.prefix.prefixlen >= 31):
return available_ips
# For "normal" IPv4 prefixes, omit first and last addresses
available_ips -= netaddr.IPSet([
netaddr.IPAddress(self.prefix.first),
netaddr.IPAddress(self.prefix.last),
])
if self.family == 4:
# For "normal" IPv4 prefixes, omit first and last addresses
available_ips -= netaddr.IPSet([
netaddr.IPAddress(self.prefix.first),
netaddr.IPAddress(self.prefix.last),
])
else:
# For IPv6 prefixes, omit the Subnet-Router anycast address
# per RFC 4291
available_ips -= netaddr.IPSet([netaddr.IPAddress(self.prefix.first)])
return available_ips
def get_first_available_ip(self):

View File

@@ -118,7 +118,7 @@ class ASNTable(NetBoxTable):
url_params={'asn_id': 'pk'},
verbose_name='Provider Count'
)
sites = tables.ManyToManyColumn(
sites = columns.ManyToManyColumn(
linkify_item=True,
verbose_name='Sites'
)
@@ -226,8 +226,9 @@ class PrefixUtilizationColumn(columns.UtilizationColumn):
class PrefixTable(NetBoxTable):
prefix = tables.TemplateColumn(
prefix = columns.TemplateColumn(
template_code=PREFIX_LINK,
export_raw=True,
attrs={'td': {'class': 'text-nowrap'}}
)
prefix_flat = tables.TemplateColumn(

View File

@@ -14,7 +14,8 @@ class ServiceTemplateTable(NetBoxTable):
linkify=True
)
ports = tables.Column(
accessor=tables.A('port_list')
accessor=tables.A('port_list'),
order_by=tables.A('ports'),
)
tags = columns.TagColumn(
url_name='ipam:servicetemplate_list'
@@ -35,7 +36,8 @@ class ServiceTable(NetBoxTable):
order_by=('device', 'virtual_machine')
)
ports = tables.Column(
accessor=tables.A('port_list')
accessor=tables.A('port_list'),
order_by=tables.A('ports'),
)
tags = columns.TagColumn(
url_name='ipam:service_list'

View File

@@ -771,6 +771,12 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
)
VMInterface.objects.bulk_create(vminterfaces)
fhrp_groups = (
FHRPGroup(protocol=FHRPGroupProtocolChoices.PROTOCOL_VRRP2, group_id=101),
FHRPGroup(protocol=FHRPGroupProtocolChoices.PROTOCOL_VRRP2, group_id=102),
)
FHRPGroup.objects.bulk_create(fhrp_groups)
tenant_groups = (
TenantGroup(name='Tenant group 1', slug='tenant-group-1'),
TenantGroup(name='Tenant group 2', slug='tenant-group-2'),
@@ -791,18 +797,22 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
IPAddress(address='10.0.0.2/24', tenant=tenants[0], vrf=vrfs[0], assigned_object=interfaces[0], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'),
IPAddress(address='10.0.0.3/24', tenant=tenants[1], vrf=vrfs[1], assigned_object=interfaces[1], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'),
IPAddress(address='10.0.0.4/24', tenant=tenants[2], vrf=vrfs[2], assigned_object=interfaces[2], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'),
IPAddress(address='10.0.0.5/24', tenant=None, vrf=None, assigned_object=fhrp_groups[0], status=IPAddressStatusChoices.STATUS_ACTIVE),
IPAddress(address='10.0.0.1/25', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE),
IPAddress(address='2001:db8::1/64', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a', description='foobar2'),
IPAddress(address='2001:db8::2/64', tenant=tenants[0], vrf=vrfs[0], assigned_object=vminterfaces[0], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'),
IPAddress(address='2001:db8::3/64', tenant=tenants[1], vrf=vrfs[1], assigned_object=vminterfaces[1], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'),
IPAddress(address='2001:db8::4/64', tenant=tenants[2], vrf=vrfs[2], assigned_object=vminterfaces[2], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'),
IPAddress(address='2001:db8::5/64', tenant=None, vrf=None, assigned_object=fhrp_groups[1], status=IPAddressStatusChoices.STATUS_ACTIVE),
IPAddress(address='2001:db8::1/65', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE),
)
IPAddress.objects.bulk_create(ipaddresses)
def test_family(self):
params = {'family': '4'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
params = {'family': '6'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
def test_dns_name(self):
params = {'dns_name': ['ipaddress-a', 'ipaddress-b']}
@@ -813,10 +823,8 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_parent(self):
params = {'parent': '10.0.0.0/24'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
params = {'parent': '2001:db8::/64'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
params = {'parent': ['10.0.0.0/30', '2001:db8::/126']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
def test_filter_address(self):
# Check IPv4 and IPv6, with and without a mask
@@ -835,7 +843,7 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
def test_mask_length(self):
params = {'mask_length': '24'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
def test_vrf(self):
vrfs = VRF.objects.all()[:2]
@@ -872,11 +880,16 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'vminterface': ['Interface 1', 'Interface 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_fhrpgroup(self):
fhrp_groups = FHRPGroup.objects.all()[:2]
params = {'fhrpgroup_id': [fhrp_groups[0].pk, fhrp_groups[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_assigned_to_interface(self):
params = {'assigned_to_interface': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
params = {'assigned_to_interface': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
def test_status(self):
params = {'status': [PrefixStatusChoices.STATUS_DEPRECATED, PrefixStatusChoices.STATUS_RESERVED]}
@@ -1009,6 +1022,20 @@ class FHRPGroupAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'priority': [10, 20]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_device(self):
device = Device.objects.first()
params = {'device': [device.name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
params = {'device_id': [device.pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
def test_virtual_machine(self):
vm = VirtualMachine.objects.first()
params = {'virtual_machine': [vm.name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
params = {'virtual_machine_id': [vm.pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
class VLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = VLANGroup.objects.all()

View File

@@ -185,6 +185,18 @@ class TestPrefix(TestCase):
IPAddress.objects.create(address=IPNetwork('10.0.0.4/24'))
self.assertEqual(parent_prefix.get_first_available_ip(), '10.0.0.5/24')
def test_get_first_available_ip_ipv6(self):
parent_prefix = Prefix.objects.create(prefix=IPNetwork('2001:db8:500::/64'))
self.assertEqual(parent_prefix.get_first_available_ip(), '2001:db8:500::1/64')
def test_get_first_available_ip_ipv6_rfc3627(self):
parent_prefix = Prefix.objects.create(prefix=IPNetwork('2001:db8:500:4::/126'))
self.assertEqual(parent_prefix.get_first_available_ip(), '2001:db8:500:4::1/126')
def test_get_first_available_ip_ipv6_rfc6164(self):
parent_prefix = Prefix.objects.create(prefix=IPNetwork('2001:db8:500:5::/127'))
self.assertEqual(parent_prefix.get_first_available_ip(), '2001:db8:500:5::/127')
def test_get_utilization_container(self):
prefixes = (
Prefix(prefix=IPNetwork('10.0.0.0/24'), status=PrefixStatusChoices.STATUS_CONTAINER),

View File

@@ -4,15 +4,15 @@ from django.db.models.expressions import RawSQL
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from circuits.models import Provider
from circuits.models import Provider, Circuit
from circuits.tables import ProviderTable
from dcim.filtersets import InterfaceFilterSet
from dcim.models import Interface, Site
from dcim.models import Interface, Site, Device
from dcim.tables import SiteTable
from netbox.views import generic
from utilities.utils import count_related
from virtualization.filtersets import VMInterfaceFilterSet
from virtualization.models import VMInterface
from virtualization.models import VMInterface, VirtualMachine
from . import filtersets, forms, tables
from .constants import *
from .models import *
@@ -158,10 +158,10 @@ class RIRView(generic.ObjectView):
queryset = RIR.objects.all()
def get_extra_context(self, request, instance):
aggregates = Aggregate.objects.restrict(request.user, 'view').filter(
rir=instance
aggregates = Aggregate.objects.restrict(request.user, 'view').filter(rir=instance).annotate(
child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ())
)
aggregates_table = tables.AggregateTable(aggregates, exclude=('rir', 'utilization'))
aggregates_table = tables.AggregateTable(aggregates, user=request.user, exclude=('rir', 'utilization'))
aggregates_table.configure(request)
return {
@@ -221,12 +221,14 @@ class ASNView(generic.ObjectView):
def get_extra_context(self, request, instance):
# Gather assigned Sites
sites = instance.sites.restrict(request.user, 'view')
sites_table = SiteTable(sites)
sites_table = SiteTable(sites, user=request.user)
sites_table.configure(request)
# Gather assigned Providers
providers = instance.providers.restrict(request.user, 'view')
providers_table = ProviderTable(providers)
providers = instance.providers.restrict(request.user, 'view').annotate(
count_circuits=count_related(Circuit, 'provider')
)
providers_table = ProviderTable(providers, user=request.user)
providers_table.configure(request)
return {
@@ -366,7 +368,7 @@ class RoleView(generic.ObjectView):
role=instance
)
prefixes_table = tables.PrefixTable(prefixes, exclude=('role', 'utilization'))
prefixes_table = tables.PrefixTable(prefixes, user=request.user, exclude=('role', 'utilization'))
prefixes_table.configure(request)
return {
@@ -674,11 +676,26 @@ class IPAddressView(generic.ObjectView):
related_ips_table = tables.IPAddressTable(related_ips, orderable=False)
related_ips_table.configure(request)
# Find services belonging to the IP
service_filter = Q(ipaddresses=instance)
# Find services listening on all IPs on the assigned device/vm
if instance.assigned_object and instance.assigned_object.parent_object:
parent_object = instance.assigned_object.parent_object
if isinstance(parent_object, VirtualMachine):
service_filter |= (Q(virtual_machine=parent_object) & Q(ipaddresses=None))
elif isinstance(parent_object, Device):
service_filter |= (Q(device=parent_object) & Q(ipaddresses=None))
services = Service.objects.restrict(request.user, 'view').filter(service_filter)
return {
'parent_prefixes_table': parent_prefixes_table,
'duplicate_ips_table': duplicate_ips_table,
'more_duplicate_ips': duplicate_ips.count() > 10,
'related_ips_table': related_ips_table,
'services': services,
}
@@ -805,7 +822,7 @@ class VLANGroupView(generic.ObjectView):
vlans_count = vlans.count()
vlans = add_available_vlans(vlans, vlan_group=instance)
vlans_table = tables.VLANTable(vlans, exclude=('group',))
vlans_table = tables.VLANTable(vlans, user=request.user, exclude=('group',))
if request.user.has_perm('ipam.change_vlan') or request.user.has_perm('ipam.delete_vlan'):
vlans_table.columns.show('pk')
vlans_table.configure(request)

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