Compare commits

...

99 Commits

Author SHA1 Message Date
Jeremy Stretch
5f184f2435 Merge pull request #12507 from netbox-community/develop
Release v3.5.1
2023-05-05 12:50:30 -04:00
jeremystretch
56a4d0333e Formatting fix 2023-05-05 12:31:12 -04:00
jeremystretch
6794742213 Release v3.5.1 2023-05-05 12:29:10 -04:00
jeremystretch
a29a07ed26 Fixes #12463: Fix the association of completed jobs with reports & scripts in the REST API 2023-05-05 10:17:13 -04:00
jeremystretch
42c80f69e6 Changelog for #11715, #11801, #11932, #12122, #12245, #12416, #12476, #12483, #12496 2023-05-05 10:01:54 -04:00
Abhimanyu Saharan
ca0e7be637 Adds bulk import for journal entry (#12485)
* adds bulk import for journal entry #12122

* lint fix

* Add kind as CSVChoiceField on JournalEntryImportForm

---------

Co-authored-by: jeremystretch <jstretch@netboxlabs.com>
2023-05-05 09:57:09 -04:00
Abhimanyu Saharan
42346702a1 Adds image attachment list view (#12487)
* adds image attachment list view #11932

* fixed typo

* Update netbox/extras/tables/tables.py

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

* Update netbox/extras/forms/filtersets.py

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

* changes as per review

* Disable ordering by size (not stored in database)

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2023-05-05 09:48:13 -04:00
Arthur Hanson
9909213c0d 12416 warning for missing script file (#12456)
* 12416 warning for missing script file

* 12416 widen exception catching for internal script error

* Update netbox/extras/models/scripts.py

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

* 12416 update from review feedback

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2023-05-05 09:37:38 -04:00
Daniel Sheppard
7a38f601de Fixes: #11715 - Fix Parent Prefix table display (#12448)
* Fixes: #11715 - Fix Parent Prefix table display of global vrf prefixes that are **not** containers.

* Combine AND into a single Q object

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

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2023-05-05 09:35:05 -04:00
Abhimanyu Saharan
abdcfdecf5 Adds description to elevation device tooltip (#12488)
* adds description to elevation device tooltip #11801

* changes as per review

* changes as per review

* Rearrange attrs, add headings, and update docstring

---------

Co-authored-by: jeremystretch <jstretch@netboxlabs.com>
2023-05-05 09:09:21 -04:00
Luke Anderson
9d62174e1e Fix #12245 - Add WLAN to InterfaceBulkEditForm 2023-05-05 08:41:40 -04:00
Patrick Hurrelmann
a96b76a8d1 Fixes #12496: Remove account field from provider template 2023-05-05 08:27:29 -04:00
Tobias Genannt
ab69faab87 Fix #12483: Using mutable dulwich configuration 2023-05-04 16:56:45 -04:00
Arthur
f3826e6235 fix runscript command 2023-05-04 16:18:44 -04:00
jeremystretch
3eba65b5c2 Changelog for #10757, #11652, #11791, #11190, #12363, #12464 2023-05-04 14:14:21 -04:00
Arthur Hanson
683ef30af4 12363 update paragraph spacing on journal table (#12435)
* 12363 update paragraph spacing on journal table

* 12363 make css rule generic

* 12363 change p tag to only effect last-child

* 12363 change p table spacing to .5em

* 12363 move comment
2023-05-04 14:05:32 -04:00
Daniel Sheppard
46914d9479 Fixes: #10757 - Change IP interface assignment to use new selector (#12452)
* Fixes: #10757 - Change interface assignment to use new selector.  Perform the same change to the NAT assignment as well.

* Remove nat_vrf from form and remove query_params that are not required anymore
2023-05-04 13:59:28 -04:00
Abhimanyu Saharan
ea8a0135ad Adds module status to module bay table (#12455)
* adds module status to module bay table #11652

* removed the extra line

* updated field name
2023-05-04 13:46:31 -04:00
Abhimanyu Saharan
25142e037a Adds housekeeping systemd files (#12457)
* adds housekeeping systemd files #11190

* Clean up Markdown

---------

Co-authored-by: jeremystretch <jstretch@netboxlabs.com>
2023-05-04 13:43:40 -04:00
Abhimanyu Saharan
93b912c2da Adds ENGINE to database config (#12458)
* adds ENGINE to database config #11791

* fixed lint issues

* updated doc
2023-05-04 13:36:52 -04:00
Tobias Genannt
4df517e4da Fix #12464: Only set username, password for HTTP/s (#12482) 2023-05-04 13:34:04 -04:00
jeremystretch
2c756873aa Fix broken links 2023-05-04 08:43:17 -04:00
jeremystretch
01fa6e28cd Changelog for #10759, #11422, #11504, #12433 2023-05-03 09:32:48 -04:00
Arthur Hanson
5036020dc0 12433 update object list widget to correctly parameterize urls (#12434)
* 12433 update object list widget to correctly parameterize urls

* Update netbox/extras/dashboard/widgets.py

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

* 12433 fix indent

* Correct per_page query parameter

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2023-05-03 09:20:53 -04:00
Arthur
78ec3a6411 11504 add rack filter to elevation 2023-05-03 08:40:43 -04:00
Arthur
24650d9118 11422 add power-panel-name to quick search of power feeds 2023-05-03 08:36:12 -04:00
Austin de Coup-Crank
b14a514b47 Closes 10759: Enable markdown support for custom field descriptions (#12344)
* enable markdown in custom field descriptions

* Closes #10759: Enable markdown support for custom field descriptions

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2023-05-02 16:47:22 -04:00
jeremystretch
7aa8434575 Changelog for #12367, #12410, #12436 2023-05-02 16:40:56 -04:00
Arthur
fbcf4c268b 12436 remove add button from contactassignment list view 2023-05-02 16:35:23 -04:00
Abhimanyu Saharan
a566a56a64 fixes RelatedObjectDoesNotExist #12367 2023-05-02 09:48:16 -04:00
jeremystretch
cc3b95bdb0 Fixes #12400: Validate default values for object and multi-object custom fields 2023-05-02 09:35:25 -04:00
jeremystretch
2099cd0fdc #12415: Bump django-rq to v2.8.0 2023-05-02 09:35:25 -04:00
Jop Zinkweg
3bdbf67b8f Update examples to reflect obj rename to object
#8684 missed some references to ```obj```
2023-05-02 08:31:33 -04:00
jeremystretch
20f0464824 Fix OpanAPI schema base path 2023-05-01 17:01:51 -04:00
jeremystretch
1952707702 Fixes #12401: Support the creation of front ports without a pre-populated device ID 2023-05-01 16:51:10 -04:00
jeremystretch
9319cffb1c Fixes #12384: Add a three-second timeout for RSS reader widget 2023-05-01 16:19:39 -04:00
jeremystretch
261f5e4995 Fixes #12415: Pin rq to v1.13.0 to fix ImportError exception from django-rq 2023-05-01 14:38:51 -04:00
jeremystretch
8cede0daf8 Changelog for #12380, #12395, #12396, #12405, #12412 2023-05-01 14:19:56 -04:00
jeremystretch
139ef7ef4c Fixes #12395: Fix "create & add another" action for objects with custom fields 2023-05-01 14:17:50 -04:00
jeremystretch
47f3023401 Fixes #12412: Device/VM interface MAC addresses can be nullified via REST API 2023-05-01 13:06:10 -04:00
Jeremy Stretch
a0f0b29432 Merge pull request #12382 from kkthxbye-code/12380-widget-fix-missing-ct
Fixes #12380 - Add ObjectChange as a valid option for ObjectListWidget and ObjectCountsWidget
2023-05-01 10:17:11 -04:00
Jeremy Stretch
f86f4f9257 Merge pull request #12406 from kkthxbye-code/12405-fix-related-vlangroup
Fixes #12405 - Fix the filtervar being set to site_id instead of site in SiteView related_models
2023-05-01 10:14:29 -04:00
Jeremy Stretch
3fc7c0edc7 Merge pull request #12407 from kkthxbye-code/12396-fix-provideraccount
Fixes #12396 - Mark provider_account as not required in API and bulk import
2023-05-01 10:12:10 -04:00
kkthxbye-code
528fb21a7e Mark provider_account as not required in API and bulk import 2023-05-01 07:44:15 +02:00
kkthxbye-code
6206d226ae Fix the filtervar being set to site_id instead of site in SiteView related_models 2023-05-01 07:15:00 +02:00
kkthxbye-code
aabaeec1d7 Add ObjectChange as a valid option for ObjectListWidget and ObjectCountsWidget 2023-04-28 12:17:39 +02:00
jeremystretch
25dc7e234d PRVB 2023-04-27 14:52:07 -04:00
Jeremy Stretch
eac7d01977 Merge pull request #12371 from netbox-community/develop
Release v3.5.0
2023-04-27 14:38:56 -04:00
jeremystretch
a5bc9d4a2d Release v3.5.0 2023-04-27 14:24:44 -04:00
jeremystretch
b3efb14176 Remove pinned version dependencies 2023-04-27 14:09:16 -04:00
Jeremy Stretch
a2f4fce5b3 Merge pull request #12370 from netbox-community/feature
Prepare for v3.5.0 release
2023-04-27 14:01:23 -04:00
jeremystretch
6109bef700 Merge branch 'develop' into feature 2023-04-27 12:11:08 -04:00
Jeremy Stretch
72767fb5b7 Merge pull request #12366 from netbox-community/develop
Release v3.4.10
2023-04-27 12:03:16 -04:00
jeremystretch
84089ab8c5 Release v3.4.10 2023-04-27 11:47:42 -04:00
Arthur Hanson
9a788349a9 12252 allow sorting on object in search (#12357)
* 12252 allow sorting on object in search

* 12252 code review changes
2023-04-27 11:26:14 -04:00
jeremystretch
b7140a0e4a Closes #12343: Enforce a minimum length for SECRET_KEY configuration parameter 2023-04-27 10:35:39 -04:00
Jeremy Stretch
d39c796828 Merge pull request #12356 from netbox-community/11607-custom-field-cable
11607 make CableSerializer use WritableNestedSerializer
2023-04-27 09:30:30 -04:00
Jeremy Stretch
4a92f6867a Merge pull request #12358 from x64x6a/develop
Add additional characters to exclude from url encode
2023-04-27 08:50:45 -04:00
x64x6a
4355085124 Fixes #12355: Exclude additional characters from url encode 2023-04-26 15:53:14 -07:00
Arthur
5d4ef5e9e5 11607 make CableSerializer WritableNestedSerializer for to_internal value instantiation 2023-04-26 13:27:55 -07:00
jeremystretch
f49e4ee512 Merge branch 'develop' into feature 2023-04-26 15:09:51 -04:00
Jeremy Stretch
f867cb3ae0 Merge pull request #12354 from netbox-community/develop
Release v3.4.9
2023-04-26 14:54:08 -04:00
jeremystretch
a49fdad5e1 Release v3.4.9 2023-04-26 14:33:23 -04:00
Arthur
1ad029712e #11902 validate device on inventory item import 2023-04-26 14:22:37 -04:00
jeremystretch
d87235af2f Closes #12337: Enable anonymized reporting of census data 2023-04-26 10:44:56 -04:00
jeremystretch
99af126fac Closes #11386: Introduce CSRF_COOKIE_SECURE, SECURE_SSL_REDIRECT, and SESSION_COOKIE_SECURE configuration parameters 2023-04-25 16:29:01 -04:00
jeremystretch
83cea218b5 Changelog for #12195, #12218, #12278 2023-04-25 14:49:43 -04:00
jeremystretch
9eb38ab7b2 Delete obsolete static resources 2023-04-25 14:48:00 -04:00
Abhimanyu Saharan
d3206d9bf9 Added method to update viewset description (#12218)
* wip

* wip

* updated description on viewset

* fixed model name

* Update schema.py

* Update schema.py
2023-04-25 14:37:05 -04:00
Austin de Coup-Crank
adb9673f09 Fixes #11623: obfuscate Wi-Fi PSKs (#12244)
* Fixes #11623: obfuscate Wi-Fi PSKs

* yarn linting fixes

* include static files
2023-04-24 12:13:28 -04:00
PieterL75
b693123f6e Fixes #10987: Show rack-list dropdown in rack (#11779)
* Intial. 2 ways the racknavigation displayed

* show active rack in dropdown

* auto hide/show when viewport reduces

* Dropdown only

* Update links to use get_absolute_url()

---------

Co-authored-by: Pieter Lambrecht <pieter.lambrecht@sentia.com>
Co-authored-by: jeremystretch <jstretch@netboxlabs.com>
2023-04-24 12:01:33 -04:00
jeremystretch
e7663b7e39 Mark Provider.account as deprecated 2023-04-21 16:21:04 -04:00
jeremystretch
053be952ba Fixes #12238: Improve error message for API token IP prefix validation failures 2023-04-21 16:06:33 -04:00
jeremystretch
390619ca99 Changelog for #11383, #12205, #12226, #12255 2023-04-21 15:40:34 -04:00
jeremystretch
b1130ff9b6 Add an issue template for deprecations 2023-04-21 15:38:27 -04:00
Darek
89fa546a14 Merge pull request from GHSA-92x4-vfjf-rmf7 2023-04-21 15:08:04 -04:00
jeremystretch
c8988bac8a Add graphics 2023-04-21 13:46:07 -04:00
Arthur Hanson
55385dd0db 12278 add ipaddressfield serializer for OpenAPI spectacular typing (#12285)
* #12278 add serializer for ipaddressfield to remove spectacular warnings

* #12278 add ipaddressfieldserializer to nested serializers

* #12278 fix to_internal_value to_representation in serializer

* #12278 to_internal_value is called before validation! need to raise validation error if incorrect format

* #12278 to_internal_value needs to return value doh

* #12278 move IPAddressField to field_serializers

* #12278 remove old import

* 12278 remove validator
2023-04-21 12:41:03 -04:00
Arthur Hanson
38a0ed5e24 12255 inventory item device change (#12311)
* #12255 allow inventory items to change devices

* #12255 allow inventory item template to change devices

* #12255 fix init

* 12255 remove can_swtich from template model

* 12255 change to check module list
2023-04-21 12:36:11 -04:00
Arthur
15d80f4e1b 12195 remove incorrect null=True on choice fields 2023-04-21 12:23:07 -04:00
jeremystretch
2fe5592c3c Fixes #12299: Fix object list widget support for filtering by multiple values 2023-04-21 11:54:28 -04:00
jeremystretch
8cf0a79dee Changelog for #12149, #12256, #12288 2023-04-21 11:40:28 -04:00
jeremystretch
183c5ca667 Update screenshots 2023-04-21 11:35:35 -04:00
jeremystretch
d5c4b1e27c #10520: Remove obsolete NAPALM documentation 2023-04-21 11:22:38 -04:00
jeremystretch
2fcdc0ae6a #10520: Restore ability to edit platform NAPALM fields via UI 2023-04-21 11:21:42 -04:00
Janik H
12bb0ec1fe Fix typo in api token auth 2023-04-21 10:01:13 -04:00
Austin de Coup-Crank
8b7ee0a0db 11383 fix search order (#12251)
* Fixes #11383: Sorting search by type doesn't work

* Fixes #11383: Sorting search by type doesn't work; more reliable approach
2023-04-20 17:04:47 -04:00
Arthur Hanson
274cd5d56c 12288 add servers to spectacular settings (#12300)
* 12288 add servers to spectacular settings

* 12288 standardize quotes

* Account for BASE_PATH

---------

Co-authored-by: jeremystretch <jstretch@netboxlabs.com>
2023-04-20 15:56:21 -04:00
Luke Anderson
ab3531558a Closes #12226: Add Profile Data Headers to Remote Authentication Middleware (#12253)
* Closes #12226: Add Profile Data Headers to Remote Authentication Middleware

* Tweak documentation

---------

Co-authored-by: jeremystretch <jstretch@netboxlabs.com>
2023-04-20 15:49:54 -04:00
Arthur
dda56f21f3 #12256 remove read-only fields from writable serializers 2023-04-20 14:11:55 -04:00
Arthur
31c909c368 #12149 remove spectacular choice mapping fixup internal one 2023-04-20 14:10:05 -04:00
jeremystretch
164b2a5016 Fixes #12270: Fix pre-population of list values when creating a saved filter 2023-04-19 17:41:38 -04:00
jeremystretch
7b374e4cf6 Fixes #12296: Fix 'mark connected' form field for bulk editing front & rear ports 2023-04-19 17:25:32 -04:00
jeremystretch
13625325f5 #8684: Fix test 2023-04-19 11:31:26 -04:00
jeremystretch
b84ac184c2 #8684: Remove obsolete 'obj' var from custom link context 2023-04-19 09:58:18 -04:00
jeremystretch
d5d2431cbd Docs cleanup for v3.5 2023-04-18 16:58:53 -04:00
jeremystretch
c08c7dda50 Closes #12292: Replace SelectSpeedWidget and SelectDurationWidget with NumberWithOptions 2023-04-18 16:33:43 -04:00
jeremystretch
b807198e6d Rename ImportForm to BulkImportForm 2023-04-18 15:27:05 -04:00
jeremystretch
9caa7f6b7c Move SyncedDataMixin from extras to core 2023-04-18 15:18:19 -04:00
143 changed files with 1256 additions and 685 deletions

View File

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

24
.github/ISSUE_TEMPLATE/deprecation.yaml vendored Normal file
View File

@@ -0,0 +1,24 @@
---
name: 🗑️ Deprecation
description: The removal of an existing feature or resource
labels: ["type: deprecation"]
body:
- type: textarea
attributes:
label: Proposed Changes
description: >
Describe in detail the proposed changes. What is being removed?
validations:
required: true
- type: textarea
attributes:
label: Justification
description: Please provide justification for the proposed change(s).
validations:
required: true
- type: textarea
attributes:
label: Impact
description: List all areas of the application that will be affected by this change.
validations:
required: true

View File

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

View File

@@ -29,9 +29,18 @@ as the cornerstone for network automation in thousands of organizations.
## Getting Started
<div align="center">
[![NetBox logo](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/deploy/deploy1.png)](https://github.com/netbox-community/netbox)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
[![Docker logo](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/deploy/deploy2.png)](https://github.com/netbox-community/netbox-docker)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
[![NetBox Labs logo](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/deploy/deploy3.png)](https://netboxlabs.com/netbox-cloud/)
</div>
* Just want to explore? Check out [our public demo](https://demo.netbox.dev/) right now!
* The [official documentation](https://docs.netbox.dev) offers a comprehensive introduction.
* Choose your deployment: [self-hosted](https://github.com/netbox-community/netbox), [Docker](https://github.com/netbox-community/netbox-docker), or [NetBox Cloud](https://netboxlabs.com/netbox-cloud/).
* Check out [our wiki](https://github.com/netbox-community/netbox/wiki/Community-Contributions) for even more projects to get the most out of NetBox!
## Get Involved

View File

@@ -1,6 +1,6 @@
# HTML sanitizer
# https://github.com/mozilla/bleach/blob/main/CHANGES
bleach<6.0
bleach
# Python client for Amazon AWS API
# https://github.com/boto/boto3/blob/develop/CHANGELOG.rst
@@ -137,8 +137,7 @@ social-auth-core
# Django app for social-auth-core
# https://github.com/python-social-auth/social-app-django/blob/master/CHANGELOG.md
# See https://github.com/python-social-auth/social-app-django/issues/429
social-auth-app-django==5.0.0
social-auth-app-django
# SVG image rendering (used for rack elevations)
# hhttps://github.com/mozman/svgwrite/blob/master/NEWS.rst

View File

@@ -0,0 +1,17 @@
[Unit]
Description=NetBox Housekeeping Service
Documentation=https://docs.netbox.dev/
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=netbox
Group=netbox
WorkingDirectory=/opt/netbox
ExecStart=/opt/netbox/venv/bin/python /opt/netbox/netbox/manage.py housekeeping
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,13 @@
[Unit]
Description=NetBox Housekeeping Timer
Documentation=https://docs.netbox.dev/
After=network-online.target
Wants=network-online.target
[Timer]
OnCalendar=daily
AccuracySec=1h
Persistent=true
[Install]
WantedBy=multi-user.target

View File

@@ -26,6 +26,8 @@ REMOTE_AUTH_BACKEND = 'netbox.authentication.RemoteUserBackend'
Another option for remote authentication in NetBox is to enable HTTP header-based user assignment. The front end HTTP server (e.g. nginx or Apache) performs client authentication as a process external to NetBox, and passes information about the authenticated user via HTTP headers. By default, the user is assigned via the `REMOTE_USER` header, but this can be customized via the `REMOTE_AUTH_HEADER` configuration parameter.
Optionally, user profile information can be supplied by `REMOTE_USER_FIRST_NAME`, `REMOTE_USER_LAST_NAME` and `REMOTE_USER_EMAIL` headers. These are saved to the users profile during the authentication process. These headers can be customized like the `REMOTE_USER` header.
### Single Sign-On (SSO)
```python

View File

@@ -4,7 +4,7 @@
### 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.
NetBox supports 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

View File

@@ -7,7 +7,13 @@ NetBox includes a `housekeeping` management command that should be run nightly.
* Deleting job result records older than the configured [retention time](../configuration/miscellaneous.md#job_retention)
* Check for new NetBox releases (if [`RELEASE_CHECK_URL`](../configuration/miscellaneous.md#release_check_url) is set)
This command can be invoked directly, or by using the shell script provided at `/opt/netbox/contrib/netbox-housekeeping.sh`. This script can be linked from your cron scheduler's daily jobs directory (e.g. `/etc/cron.daily`) or referenced directly within the cron configuration file.
This command can be invoked directly, or by using the shell script provided at `/opt/netbox/contrib/netbox-housekeeping.sh`.
## Scheduling
### Using Cron
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.
```shell
sudo ln -s /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/netbox-housekeeping
@@ -16,4 +22,28 @@ sudo ln -s /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/netbox-hou
!!! note
On Debian-based systems, be sure to omit the `.sh` file extension when linking to the script from within a cron directory. Otherwise, the task may not run.
The `housekeeping` command can also be run manually at any time: Running the command outside scheduled execution times will not interfere with its operation.
### Using Systemd
First, create symbolic links for the systemd service and timer files. Link the existing service and timer files from the `/opt/netbox/contrib/` directory to the `/etc/systemd/system/` directory:
```bash
sudo ln -s /opt/netbox/contrib/netbox-housekeeping.service /etc/systemd/system/netbox-housekeeping.service
sudo ln -s /opt/netbox/contrib/netbox-housekeeping.timer /etc/systemd/system/netbox-housekeeping.timer
```
Then, reload the systemd configuration and enable the timer to start automatically at boot:
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now netbox-housekeeping.timer
```
Check the status of your timer by running:
```bash
sudo systemctl list-timers --all
```
This command will show a list of all timers, including your `netbox-housekeeping.timer`. Make sure the timer is active and properly scheduled.
That's it! Your NetBox housekeeping service is now configured to run daily using systemd.

View File

@@ -1,6 +1,6 @@
# Object-Based Permissions
NetBox v2.9 introduced a new object-based permissions framework, which replaces Django's built-in permissions model. Object-based permissions enable an administrator to grant users or groups the ability to perform an action on arbitrary subsets of objects in NetBox, rather than all objects of a certain type. For example, it is possible to grant a user permission to view only sites within a particular region, or to modify only VLANs with a numeric ID within a certain range.
NetBox employs a new object-based permissions framework, which replaces Django's built-in permissions model. Object-based permissions enable an administrator to grant users or groups the ability to perform an action on arbitrary subsets of objects in NetBox, rather than all objects of a certain type. For example, it is possible to grant a user permission to view only sites within a particular region, or to modify only VLANs with a numeric ID within a certain range.
A permission in NetBox represents a relationship shared by several components:
@@ -20,7 +20,7 @@ There are four core actions that can be permitted for each type of object within
* **Change** - Modify an existing object
* **Delete** - Delete an existing object
In addition to these, permissions can also grant custom actions that may be required by a specific model or plugin. For example, the `napalm_read` permission on the device model allows a user to execute NAPALM queries on a device via NetBox's REST API. These can be specified when granting a permission in the "additional actions" field.
In addition to these, permissions can also grant custom actions that may be required by a specific model or plugin. For example, the `run` permission for scripts allows a user to execute custom scripts. These can be specified when granting a permission in the "additional actions" field.
!!! note
Internally, all actions granted by a permission (both built-in and custom) are stored as strings in an array field named `actions`.

View File

@@ -30,10 +30,6 @@ Some configuration parameters are primarily controlled via NetBox's admin interf
* [`MAINTENANCE_MODE`](./miscellaneous.md#maintenance_mode)
* [`MAPS_URL`](./miscellaneous.md#maps_url)
* [`MAX_PAGE_SIZE`](./miscellaneous.md#max_page_size)
* [`NAPALM_ARGS`](./napalm.md#napalm_args)
* [`NAPALM_PASSWORD`](./napalm.md#napalm_password)
* [`NAPALM_TIMEOUT`](./napalm.md#napalm_timeout)
* [`NAPALM_USERNAME`](./napalm.md#napalm_username)
* [`PAGINATE_COUNT`](./default-values.md#paginate_count)
* [`POWERFEED_DEFAULT_AMPERAGE`](./default-values.md#powerfeed_default_amperage)
* [`POWERFEED_DEFAULT_MAX_UTILIZATION`](./default-values.md#powerfeed_default_max_utilization)

View File

@@ -45,6 +45,16 @@ Sets content for the top banner in the user interface.
---
## CENSUS_REPORTING_ENABLED
Default: True
Enables anonymous census reporting. To opt out of census reporting, set this to False.
This data enables the project maintainers to estimate how many NetBox deployments exist and track the adoption of new versions over time. Census reporting effects a single HTTP request each time a worker starts. The only data reported by this function are the NetBox version, Python version, and a pseudorandom unique identifier.
---
## CHANGELOG_RETENTION
!!! tip "Dynamic Configuration Parameter"

View File

@@ -1,53 +0,0 @@
# NAPALM Parameters
!!! **Note:** As of NetBox v3.5, NAPALM integration has been moved to a plugin and these configuration parameters are now deprecated.
## NAPALM_USERNAME
## NAPALM_PASSWORD
!!! tip "Dynamic Configuration Parameter"
NetBox will use these credentials when authenticating to remote devices via the supported [NAPALM integration](../integrations/napalm.md), if installed. Both parameters are optional.
!!! note
If SSH public key authentication has been set up on the remote device(s) for the system account under which NetBox runs, these parameters are not needed.
---
## NAPALM_ARGS
!!! tip "Dynamic Configuration Parameter"
A dictionary of optional arguments to pass to NAPALM when instantiating a network driver. See the NAPALM documentation for a [complete list of optional arguments](https://napalm.readthedocs.io/en/latest/support/#optional-arguments). An example:
```python
NAPALM_ARGS = {
'api_key': '472071a93b60a1bd1fafb401d9f8ef41',
'port': 2222,
}
```
Some platforms (e.g. Cisco IOS) require an argument named `secret` to be passed in addition to the normal password. If desired, you can use the configured `NAPALM_PASSWORD` as the value for this argument:
```python
NAPALM_USERNAME = 'username'
NAPALM_PASSWORD = 'MySecretPassword'
NAPALM_ARGS = {
'secret': NAPALM_PASSWORD,
# Include any additional args here
}
```
---
## NAPALM_TIMEOUT
!!! tip "Dynamic Configuration Parameter"
Default: 30 seconds
The amount of time (in seconds) to wait for NAPALM to connect to a device.
---

View File

@@ -79,6 +79,30 @@ When remote user authentication is in use, this is the name of the HTTP header w
---
## REMOTE_AUTH_USER_EMAIL
Default: `'HTTP_REMOTE_USER_EMAIL'`
When remote user authentication is in use, this is the name of the HTTP header which informs NetBox of the email address of the currently authenticated user. For example, to use the request header `X-Remote-User-Email` it needs to be set to `HTTP_X_REMOTE_USER_EMAIL`. (Requires `REMOTE_AUTH_ENABLED`.)
---
## REMOTE_AUTH_USER_FIRST_NAME
Default: `'HTTP_REMOTE_USER_FIRST_NAME'`
When remote user authentication is in use, this is the name of the HTTP header which informs NetBox of the first name of the currently authenticated user. For example, to use the request header `X-Remote-User-First-Name` it needs to be set to `HTTP_X_REMOTE_USER_FIRST_NAME`. (Requires `REMOTE_AUTH_ENABLED`.)
---
## REMOTE_AUTH_USER_LAST_NAME
Default: `'HTTP_REMOTE_USER_LAST_NAME'`
When remote user authentication is in use, this is the name of the HTTP header which informs NetBox of the last name of the currently authenticated user. For example, to use the request header `X-Remote-User-Last-Name` it needs to be set to `HTTP_X_REMOTE_USER_LAST_NAME`. (Requires `REMOTE_AUTH_ENABLED`.)
---
## REMOTE_AUTH_SUPERUSER_GROUPS
Default: `[]` (Empty list)

View File

@@ -33,11 +33,13 @@ NetBox requires access to a PostgreSQL 11 or later database service to store dat
* `HOST` - Name or IP address of the database server (use `localhost` if running locally)
* `PORT` - TCP port of the PostgreSQL service; leave blank for default port (TCP/5432)
* `CONN_MAX_AGE` - Lifetime of a [persistent database connection](https://docs.djangoproject.com/en/stable/ref/databases/#persistent-connections), in seconds (300 is the default)
* `ENGINE` - The database backend to use; must be a PostgreSQL-compatible backend (e.g. `django.db.backends.postgresql`)
Example:
```python
DATABASE = {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'netbox', # Database name
'USER': 'netbox', # PostgreSQL username
'PASSWORD': 'J5brHrAXFLQSif0K', # PostgreSQL password
@@ -50,6 +52,9 @@ DATABASE = {
!!! note
NetBox supports all PostgreSQL database options supported by the underlying Django framework. For a complete list of available parameters, please see [the Django documentation](https://docs.djangoproject.com/en/stable/ref/settings/#databases).
!!! warning
Make sure to use a PostgreSQL-compatible backend for the ENGINE setting. If you don't specify an ENGINE, the default will be django.db.backends.postgresql.
---
## REDIS
@@ -144,8 +149,6 @@ REDIS = {
## SECRET_KEY
This is a secret, random string used to assist in the creation new cryptographic hashes for passwords and HTTP cookies. The key defined here should not be shared outside of the configuration file. `SECRET_KEY` can be changed at any time, however be aware that doing so will invalidate all existing sessions.
This is a secret, pseudorandom string used to assist in the creation new cryptographic hashes for passwords and HTTP cookies. The key defined here should not be shared outside the configuration file. `SECRET_KEY` can be changed at any time without impacting stored data, however be aware that doing so will invalidate all existing user sessions. NetBox deployments comprising multiple nodes must have the same secret key configured on all nodes.
Please note that this key is **not** used directly for hashing user passwords or for the encrypted storage of secret data in NetBox.
`SECRET_KEY` should be at least 50 characters in length and contain a random mix of letters, digits, and symbols. The script located at `$INSTALL_ROOT/netbox/generate_secret_key.py` may be used to generate a suitable key.
`SECRET_KEY` **must** be at least 50 characters in length, and should contain a mix of letters, digits, and symbols. The script located at `$INSTALL_ROOT/netbox/generate_secret_key.py` may be used to generate a suitable key. Please note that this key is **not** used directly for hashing user passwords or for the encrypted storage of secret data in NetBox.

View File

@@ -67,6 +67,12 @@ The name of the cookie to use for the cross-site request forgery (CSRF) authenti
---
## CSRF_COOKIE_SECURE
Default: False
If true, the cookie employed for cross-site request forgery (CSRF) protection will be marked as secure, meaning that it can only be sent across an HTTPS connection.
---
## CSRF_TRUSTED_ORIGINS
@@ -145,6 +151,17 @@ The view name or URL to which a user is redirected after logging out.
---
## SECURE_SSL_REDIRECT
Default: False
If true, all non-HTTPS requests will be automatically redirected to use HTTPS.
!!! warning
Ensure that your frontend HTTP daemon has been configured to forward the HTTP scheme correctly before enabling this option. An incorrectly configured frontend may result in a looping redirect.
---
## SESSION_COOKIE_NAME
Default: `sessionid`
@@ -153,6 +170,14 @@ The name used for the session cookie. See the [Django documentation](https://doc
---
## SESSION_COOKIE_SECURE
Default: False
If true, the cookie employed for session authentication will be marked as secure, meaning that it can only be sent across an HTTPS connection.
---
## SESSION_FILE_PATH
Default: None

View File

@@ -2,12 +2,12 @@
Custom links allow users to display arbitrary hyperlinks to external content within NetBox object views. These are helpful for cross-referencing related records in systems outside NetBox. For example, you might create a custom link on the device view which links to the current device in a Network Monitoring System (NMS).
Custom links are created by navigating to Customization > Custom Links. Each link is associated with a particular NetBox object type (site, device, prefix, etc.) and will be displayed on relevant views. Each link has display text and a URL, and data from the NetBox item being viewed can be included in the link using [Jinja2 template code](https://jinja2docs.readthedocs.io/en/stable/) through the variable `obj`, and custom fields through `obj.cf`.
Custom links are created by navigating to Customization > Custom Links. Each link is associated with a particular NetBox object type (site, device, prefix, etc.) and will be displayed on relevant views. Each link has display text and a URL, and data from the NetBox item being viewed can be included in the link using [Jinja2 template code](https://jinja2docs.readthedocs.io/en/stable/) through the variable `object`, and custom fields through `object.cf`.
For example, you might define a link like this:
* Text: `View NMS`
* URL: `https://nms.example.com/nodes/?name={{ obj.name }}`
* URL: `https://nms.example.com/nodes/?name={{ object.name }}`
When viewing a device named Router4, this link would render as:
@@ -27,7 +27,6 @@ The following context data is available within the template when rendering a cus
| Variable | Description |
|-----------|-------------------------------------------------------------------------------------------------------------------|
| `object` | The NetBox object being displayed |
| `obj` | Same as `object`; maintained for backward compatability until NetBox v3.5 |
| `debug` | A boolean indicating whether debugging is enabled |
| `request` | The current WSGI request |
| `user` | The current user (if authenticated) |
@@ -44,7 +43,7 @@ Only links which render with non-empty text are included on the page. You can em
For example, if you only want to display a link for active devices, you could set the link text to
```jinja2
{% if obj.status == 'active' %}View NMS{% endif %}
{% if object.status == 'active' %}View NMS{% endif %}
```
The link will not appear when viewing a device with any status other than "active."
@@ -52,7 +51,7 @@ The link will not appear when viewing a device with any status other than "activ
As another example, if you wanted to show only devices belonging to a certain manufacturer, you could do something like this:
```jinja2
{% if obj.device_type.manufacturer.name == 'Cisco' %}View NMS{% endif %}
{% if object.device_type.manufacturer.name == 'Cisco' %}View NMS{% endif %}
```
The link will only appear when viewing a device with a manufacturer name of "Cisco."

View File

@@ -35,12 +35,9 @@ class MyScript(Script):
The `run()` method should accept two arguments:
* `data` - A dictionary containing all of the variable data passed via the web form.
* `data` - A dictionary containing all the variable data passed via the web form.
* `commit` - A boolean indicating whether database changes will be committed.
!!! note
The `commit` argument was introduced in NetBox v2.7.8. Backward compatibility is maintained for scripts which accept only the `data` argument, however beginning with v2.10 NetBox will require the `run()` method of every script to accept both arguments. (Either argument may still be ignored within the method.)
Defining script variables is optional: You may create a script with only a `run()` method if no user input is needed.
Any output generated by the script during its execution will be displayed under the "output" tab in the UI.

View File

@@ -32,7 +32,7 @@ These are considered the "core" application models which are used to model netwo
* [circuits.Circuit](../models/circuits/circuit.md)
* [circuits.Provider](../models/circuits/provider.md)
* [circuits.ProviderAccount](../models/circuits/provideracount.md)
* [circuits.ProviderAccount](../models/circuits/provideraccount.md)
* [circuits.ProviderNetwork](../models/circuits/providernetwork.md)
* [core.DataSource](../models/core/datasource.md)
* [dcim.Cable](../models/dcim/cable.md)

View File

@@ -30,14 +30,6 @@ A webhook is a mechanism for conveying to some external system a change that too
To learn more about this feature, check out the [webhooks documentation](../integrations/webhooks.md).
## NAPALM
[NAPALM](https://github.com/napalm-automation/napalm) is a Python library which enables direct interaction with network devices of various platforms. When configured, NetBox supports fetching live operational and status data directly from network devices to be compared to what has been defined in NetBox. This allows for easily validating the device's operational state against its desired state. Additionally, NetBox's REST API can act as a sort of proxy for NAPALM commands, allowing external clients to interact with network devices by sending HTTP requests to the appropriate API endpoint.
To learn more about this feature, check out the [NAPALM documentation](../integrations/napalm.md).
As of NetBox v3.5, NAPALM integration has been moved to a plugin. Please see the [netbox_napalm_plugin](https://github.com/netbox-community/netbox-napalm) for installation instructions.
## Prometheus Metrics
NetBox includes a special `/metrics` view which exposes metrics for a [Prometheus](https://prometheus.io/) scraper, powered by the open source [django-prometheus](https://github.com/korfuri/django-prometheus) library. To learn more about this feature, check out the [Prometheus metrics documentation](../integrations/prometheus-metrics.md).

View File

@@ -1,5 +1,7 @@
# Configuration Rendering
!!! info "This feature was introduced in NetBox v3.5."
One of the critical aspects of operating a network is ensuring that every network node is configured correctly. By leveraging configuration templates and [context data](./context-data.md), NetBox can render complete configuration files for each device on your network.
```mermaid

View File

@@ -4,9 +4,6 @@
[Redis](https://redis.io/) is an in-memory key-value store which NetBox employs for caching and queuing. This section entails the installation and configuration of a local Redis instance. If you already have a Redis service in place, skip to [the next section](3-netbox.md).
!!! warning "Redis v4.0 or later required"
NetBox v2.9.0 and later require Redis v4.0 or higher. If your distribution does not offer a recent enough release, you will need to build Redis from source. Please see [the Redis installation documentation](https://github.com/redis/redis) for further details.
=== "Ubuntu"
```no-highlight

View File

@@ -97,7 +97,7 @@ sudo git pull origin master
## 4. Run the Upgrade Script
Once the new code is in place, verify that any optional Python packages required by your deployment (e.g. `napalm` or `django-auth-ldap`) are listed in `local_requirements.txt`. Then, run the upgrade script:
Once the new code is in place, verify that any optional Python packages required by your deployment (e.g. `django-auth-ldap`) are listed in `local_requirements.txt`. Then, run the upgrade script:
```no-highlight
sudo ./upgrade.sh

View File

@@ -1,3 +0,0 @@
# NAPALM
As of NetBox v3.5, NAPALM integration has been moved to a plugin. Please see the [netbox_napalm_plugin](https://github.com/netbox-community/netbox-napalm) for installation instructions. **Note:** All previously entered NAPALM configuration data will be saved and automatically imported by the new plugin.

View File

@@ -638,7 +638,7 @@ $ curl -X POST \
https://netbox/api/users/tokens/provision/ \
--data '{
"username": "hankhill",
"password": "I<3C3H8",
"password": "I<3C3H8"
}'
```

View File

@@ -37,7 +37,6 @@ NetBox was built specifically to serve the needs of network engineers and operat
* Robust object-based permissions
* Detailed, automatic change logging
* Global search engine
* NAPALM integration
## What NetBox Is Not
@@ -78,4 +77,3 @@ NetBox is built on the [Django](https://djangoproject.com/) Python framework and
| Application | Django/Python |
| Database | PostgreSQL 11+ |
| Task queuing | Redis/django-rq |
| Live device access | NAPALM (optional) |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 171 KiB

View File

@@ -1,17 +1,19 @@
# Provider Accounts
This model can be used to represent individual accounts associated with a provider.
## Fields
### Provider
The [provider](./provider.md) the account belongs to.
### Name
A human-friendly name, unique to the provider.
### Account Number
The administrative account identifier tied to this provider for your organization.
# Provider Accounts
!!! info "This model was introduced in NetBox v3.5."
This model can be used to represent individual accounts associated with a provider.
## Fields
### Provider
The [provider](./provider.md) the account belongs to.
### Name
A human-friendly name, unique to the provider.
### Account Number
The administrative account identifier tied to this provider for your organization.

View File

@@ -4,8 +4,6 @@ A platform defines the type of software running on a [device](./device.md) or [v
Platforms may optionally be limited by [manufacturer](./manufacturer.md): If a platform is assigned to a particular manufacturer, it can only be assigned to devices with a type belonging to that manufacturer.
The platform model is also used to indicate which [NAPALM driver](../../integrations/napalm.md) (if any) and any associated arguments NetBox should use when connecting to a remote device. The name of the driver along with optional parameters are stored with the platform.
The assignment of platforms to devices is an optional feature, and may be disregarded if not desired.
## Fields
@@ -28,8 +26,14 @@ The default [configuration template](../extras/configtemplate.md) for devices as
### NAPALM Driver
!!! warning "Deprecated Field"
NAPALM integration was removed from NetBox core in v3.5 and is now available as a [plugin](https://github.com/netbox-community/netbox-napalm). This field will be removed in NetBox v3.6.
The [NAPALM driver](https://napalm.readthedocs.io/en/latest/support/index.html) associated with this platform.
### NAPALM Arguments
!!! warning "Deprecated Field"
NAPALM integration was removed from NetBox core in v3.5 and is now available as a [plugin](https://github.com/netbox-community/netbox-napalm). This field will be removed in NetBox v3.6.
Any additional arguments to send when invoking the NAPALM driver assigned to this platform.

View File

@@ -1,5 +1,7 @@
# ASN Ranges
!!! info "This model was introduced in NetBox v3.5."
Ranges can be defined to group [AS numbers](./asn.md) numerically and to facilitate their automatic provisioning. Each range must be assigned to a [RIR](./rir.md).
## Fields

View File

@@ -1,7 +1,6 @@
# Dashboard Widgets
!!! note "Introduced in v3.5"
Support for custom dashboard widgets was introduced in NetBox v3.5.
!!! info "This feature was introduced in NetBox v3.5."
Each NetBox user can customize his or her personal dashboard by adding and removing widgets and by manipulating the size and position of each. Plugins can register their own dashboard widgets to complement those already available natively.

View File

@@ -2,8 +2,6 @@
Plugins are packaged [Django](https://docs.djangoproject.com/) apps that can be installed alongside NetBox to provide custom functionality not present in the core application. Plugins can introduce their own models and views, but cannot interfere with existing components. A NetBox user may opt to install plugins provided by the community or build his or her own.
Plugins are supported on NetBox v2.8 and later.
## Capabilities
The NetBox plugin architecture allows for the following:

View File

@@ -10,6 +10,16 @@ Minor releases are published in April, August, and December of each calendar yea
This page contains a history of all major and minor releases since NetBox v2.0. For more detail on a specific patch release, please see the release notes page for that specific minor release.
#### [Version 3.5](./version-3.5.md) (April 2023)
* Customizable Dashboard ([#9416](https://github.com/netbox-community/netbox/issues/9416))
* Remote Data Sources ([#11558](https://github.com/netbox-community/netbox/issues/11558))
* Configuration Template Rendering ([#11559](https://github.com/netbox-community/netbox/issues/11559))
* NAPALM Integration Plugin ([#10520](https://github.com/netbox-community/netbox/issues/10520))
* ASN Ranges ([#8550](https://github.com/netbox-community/netbox/issues/8550))
* Provider Accounts ([#9047](https://github.com/netbox-community/netbox/issues/9047))
* Job-Triggered Webhooks ([#8958](https://github.com/netbox-community/netbox/issues/8958))
#### [Version 3.4](./version-3.4.md) (December 2022)
* New Global Search ([#10560](https://github.com/netbox-community/netbox/issues/10560))

View File

@@ -1,6 +1,34 @@
# NetBox v3.4
## v3.4.9 (FUTURE)
## v3.4.10 (2023-04-27)
### Bug Fixes
* [#11607](https://github.com/netbox-community/netbox/issues/11607) - Fix custom object field assignments made via REST API for for cables
* [#12252](https://github.com/netbox-community/netbox/issues/12252) - Fix ordering of search results when sorting by object name
* [#12355](https://github.com/netbox-community/netbox/issues/12355) - Fix escaping of certain characters in URL when rendering custom links
---
## v3.4.9 (2023-04-26)
### Enhancements
* [#10987](https://github.com/netbox-community/netbox/issues/10987) - Show peer racks as a dropdown list under rack view
* [#11386](https://github.com/netbox-community/netbox/issues/11386) - Introduce `CSRF_COOKIE_SECURE`, `SECURE_SSL_REDIRECT`, and `SESSION_COOKIE_SECURE` configuration parameters
* [#11623](https://github.com/netbox-community/netbox/issues/11623) - Hide PSK strings under wireless LAN & link views
* [#12205](https://github.com/netbox-community/netbox/issues/12205) - Sanitize rendered custom links to mitigate malicious links
* [#12226](https://github.com/netbox-community/netbox/issues/12226) - Enable setting user name & email values via remote authenticate headers
* [#12337](https://github.com/netbox-community/netbox/issues/12337) - Enable anonymized reporting of census data
### Bug Fixes
* [#11383](https://github.com/netbox-community/netbox/issues/11383) - Fix ordering of global search results by object type
* [#11902](https://github.com/netbox-community/netbox/issues/11902) - Fix import of inventory items for devices with duplicated names
* [#12238](https://github.com/netbox-community/netbox/issues/12238) - Improve error message for API token IP prefix validation failures
* [#12255](https://github.com/netbox-community/netbox/issues/12255) - Restore the ability to move inventory items among devices
* [#12270](https://github.com/netbox-community/netbox/issues/12270) - Fix pre-population of list values when creating a saved filter
* [#12296](https://github.com/netbox-community/netbox/issues/12296) - Fix "mark connected" form field for bulk editing front & rear ports
---

View File

@@ -1,13 +1,57 @@
# NetBox v3.5
## v3.5-beta2 (2023-04-18)
## v3.5.1 (2023-05-05)
### Enhancements
* [#10759](https://github.com/netbox-community/netbox/issues/10759) - Support Markdown rendering for custom field descriptions
* [#11190](https://github.com/netbox-community/netbox/issues/11190) - Including systemd service & timer configurations for housekeeping tasks
* [#11422](https://github.com/netbox-community/netbox/issues/11422) - Match on power panel name when searching for power feeds
* [#11504](https://github.com/netbox-community/netbox/issues/11504) - Add filter to select individual racks under rack elevations view
* [#11652](https://github.com/netbox-community/netbox/issues/11652) - Add a module status column to module bay tables
* [#11791](https://github.com/netbox-community/netbox/issues/11791) - Enable configuration of custom database backend via `ENGINE` parameter
* [#11801](https://github.com/netbox-community/netbox/issues/11801) - Include device description within rack elevation tooltip
* [#11932](https://github.com/netbox-community/netbox/issues/11932) - Introduce a list view for image attachments, orderable by date and other attributes
* [#12122](https://github.com/netbox-community/netbox/issues/12122) - Enable bulk import oj journal entries
* [#12245](https://github.com/netbox-community/netbox/issues/12245) - Enable the assignment of wireless LANs to interfaces under bulk edit
### Bug Fixes
* [#10757](https://github.com/netbox-community/netbox/issues/10757) - Simplify IP address interface and NAT IP assignment form fields to avoid confusion
* [#11715](https://github.com/netbox-community/netbox/issues/11715) - Prefix within a VRF should list global prefixes as parents only if they are containers
* [#12363](https://github.com/netbox-community/netbox/issues/12363) - Fix whitespace for paragraph elements in Markdown-rendered table columns
* [#12367](https://github.com/netbox-community/netbox/issues/12367) - Fix `RelatedObjectDoesNotExist` exception under certain conditions (regression from #11550)
* [#12380](https://github.com/netbox-community/netbox/issues/12380) - Allow selecting object change as model under object list widget configuration
* [#12384](https://github.com/netbox-community/netbox/issues/12384) - Add a three-second timeout for RSS reader widget
* [#12395](https://github.com/netbox-community/netbox/issues/12395) - Fix "create & add another" action for objects with custom fields
* [#12396](https://github.com/netbox-community/netbox/issues/12396) - Provider account should not be a required field in REST API serializer
* [#12400](https://github.com/netbox-community/netbox/issues/12400) - Validate default values for object and multi-object custom fields
* [#12401](https://github.com/netbox-community/netbox/issues/12401) - Support the creation of front ports without a pre-populated device ID
* [#12405](https://github.com/netbox-community/netbox/issues/12405) - Fix filtering for VLAN groups displayed under site view
* [#12410](https://github.com/netbox-community/netbox/issues/12410) - Fix base path for OpenAPI schema (fixes Swagger UI requests)
* [#12416](https://github.com/netbox-community/netbox/issues/12416) - Fix `FileNotFoundError` exception when a managed script file is missing from disk
* [#12412](https://github.com/netbox-community/netbox/issues/12412) - Device/VM interface MAC addresses can be nullified via REST API
* [#12415](https://github.com/netbox-community/netbox/issues/12415) - Fix `ImportError` exception when running RQ worker
* [#12433](https://github.com/netbox-community/netbox/issues/12433) - Correct the application of URL query parameters for object list dashboard widgets
* [#12436](https://github.com/netbox-community/netbox/issues/12436) - Remove extraneous "add" button from contact assignments list
* [#12463](https://github.com/netbox-community/netbox/issues/12463) - Fix the association of completed jobs with reports & scripts in the REST API
* [#12464](https://github.com/netbox-community/netbox/issues/12464) - Apply credentials for git data source only when connecting via HTTP/S
* [#12476](https://github.com/netbox-community/netbox/issues/12476) - Fix `TypeError` exception when running the `runscript` management command
* [#12483](https://github.com/netbox-community/netbox/issues/12483) - Fix git remote data syncing when with HTTP proxies defined
* [#12496](https://github.com/netbox-community/netbox/issues/12496) - Remove obsolete account field from provider UI view
---
## v3.5.0 (2023-04-27)
### Breaking Changes
* The `account` field has been removed from the provider model. This information is now tracked using the new provider account model. Multiple accounts can be assigned per provider.
* A minimum length of 50 characters is now enforced for the `SECRET_KEY` configuration parameter.
* The JobResult model has been moved from the `extras` app to `core` and renamed to Job. Accordingly, its REST API endpoint has been moved from `/api/extras/job-results/` to `/api/core/jobs/`.
* The `obj_type` field on the Job model (previously JobResult) has been renamed to `object_type` for consistency with other models.
* The `JOBRESULT_RETENTION` configuration parameter has been renamed to `JOB_RETENTION`.
* The `obj` context variable is no longer passed when rendering custom links: Use `object` instead.
* The REST API schema is now generated using the OpenAPI 3.0 spec
* The URLs for the REST API schema documentation have changed:
* `/api/docs/` is now `/api/schema/swagger-ui/`
@@ -27,7 +71,7 @@ NetBox now has the ability to synchronize arbitrary data from external sources t
This release introduces the ability to render device configurations from Jinja2 templates natively within NetBox, via both the UI and REST API. The new [ConfigTemplate](../models/extras/configtemplate.md) model stores template code (which may be defined locally or sourced from remote data files). The rendering engine passes data gleaned from both config contexts and request parameters to generate complete configurations suitable for direct application to network devices.
#### NAPALM Plugin ([#10520](https://github.com/netbox-community/netbox/issues/10520))
#### NAPALM Integration Plugin ([#10520](https://github.com/netbox-community/netbox/issues/10520))
The NAPALM integration feature found in previous NetBox releases has been moved from the core application to a [dedicated plugin](https://github.com/netbox-community/netbox-napalm). This allows greater control over the feature's configuration and will unlock additional potential as a separate project.
@@ -70,21 +114,17 @@ Two new webhook trigger events have been introduced: `job_start` and `job_end`.
* [#11968](https://github.com/netbox-community/netbox/issues/11968) - Add navigation menu buttons to create device & VM components
* [#12068](https://github.com/netbox-community/netbox/issues/12068) - Enable generic foreign key relationships from jobs to NetBox objects
* [#12085](https://github.com/netbox-community/netbox/issues/12085) - Add a file source view for reports
* [#12218](https://github.com/netbox-community/netbox/issues/12218) - Provide more relevant API endpoint descriptions in schema
* [#12343](https://github.com/netbox-community/netbox/issues/12343) - Enforce a minimum length for `SECRET_KEY` configuration parameter
### Bug Fixes (From Beta1)
### Bug Fixes (From Beta2)
* [#12103](https://github.com/netbox-community/netbox/issues/12103) - Limit the types of objects available for object count & list widgets
* [#12105](https://github.com/netbox-community/netbox/issues/12105) - Prevent data sources from becoming stuck in "syncing" status when an exception is raised
* [#12106](https://github.com/netbox-community/netbox/issues/12106) - Fix exception when saving dashboard widget with minimum width/height
* [#12108](https://github.com/netbox-community/netbox/issues/12108) - Limit the draggable area of widgets to their headers
* [#12109](https://github.com/netbox-community/netbox/issues/12109) - Fix migration error when replicating more than 100 job results
* [#12112](https://github.com/netbox-community/netbox/issues/12112) - Do not link data source URL for local paths
* [#12115](https://github.com/netbox-community/netbox/issues/12115) - Fix rendering config templates from a data file
* [#12144](https://github.com/netbox-community/netbox/issues/12144) - Ensure consistent treatment of context data when rendering config templates via UI & API
* [#12145](https://github.com/netbox-community/netbox/issues/12145) - Employ `HTMXSelect` widget to fix inclusion of `<select>` field values during form regeneration
* [#12146](https://github.com/netbox-community/netbox/issues/12146) - Do not display object selector for disabled fields
* [#12151](https://github.com/netbox-community/netbox/issues/12151) - Remove incorrect OpenAPI string mapping for choice fields
* [#12167](https://github.com/netbox-community/netbox/issues/12167) - Catch and report on exceptions raised when rendering a config template
* [#12149](https://github.com/netbox-community/netbox/issues/12149) - Fix OpenAPI schema warnings relating to enum collisions
* [#12195](https://github.com/netbox-community/netbox/issues/12195) - Fix exception when setting IP address role to null via REST API
* [#12256](https://github.com/netbox-community/netbox/issues/12256) - Fix OpenAPI schema warnings relating to nested serializers
* [#12278](https://github.com/netbox-community/netbox/issues/12278) - Fix schema warnings related to IPAddressField
* [#12288](https://github.com/netbox-community/netbox/issues/12288) - Include `servers` definition in OpenAPI spec
* [#12299](https://github.com/netbox-community/netbox/issues/12299) - Fix object list widget support for filtering by multiple values
### Other Changes

View File

@@ -108,7 +108,6 @@ nav:
- Default Values: 'configuration/default-values.md'
- Error Reporting: 'configuration/error-reporting.md'
- Plugins: 'configuration/plugins.md'
- NAPALM: 'configuration/napalm.md'
- Date & Time: 'configuration/date-time.md'
- Miscellaneous: 'configuration/miscellaneous.md'
- Development: 'configuration/development.md'
@@ -124,7 +123,6 @@ nav:
- GraphQL API: 'integrations/graphql-api.md'
- Webhooks: 'integrations/webhooks.md'
- Synchronized Data: 'integrations/synchronized-data.md'
- NAPALM: 'integrations/napalm.md'
- Prometheus Metrics: 'integrations/prometheus-metrics.md'
- Plugins:
- Using Plugins: 'plugins/index.md'

View File

@@ -106,7 +106,7 @@ class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
class CircuitSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
provider = NestedProviderSerializer()
provider_account = NestedProviderAccountSerializer()
provider_account = NestedProviderAccountSerializer(required=False, allow_null=True)
status = ChoiceField(choices=CircuitStatusChoices, required=False)
type = NestedCircuitTypeSerializer()
tenant = NestedTenantSerializer(required=False, allow_null=True)

View File

@@ -25,6 +25,22 @@ class CircuitStatusChoices(ChoiceSet):
]
class CircuitCommitRateChoices(ChoiceSet):
key = 'Circuit.commit_rate'
CHOICES = [
(10000, '10 Mbps'),
(100000, '100 Mbps'),
(1000000, '1 Gbps'),
(10000000, '10 Gbps'),
(25000000, '25 Gbps'),
(40000000, '40 Gbps'),
(100000000, '100 Gbps'),
(1544, 'T1 (1.544 Mbps)'),
(2048, 'E1 (2.048 Mbps)'),
]
#
# CircuitTerminations
#
@@ -38,3 +54,19 @@ class CircuitTerminationSideChoices(ChoiceSet):
(SIDE_A, 'A'),
(SIDE_Z, 'Z')
)
class CircuitTerminationPortSpeedChoices(ChoiceSet):
key = 'CircuitTermination.port_speed'
CHOICES = [
(10000, '10 Mbps'),
(100000, '100 Mbps'),
(1000000, '1 Gbps'),
(10000000, '10 Gbps'),
(25000000, '25 Gbps'),
(40000000, '40 Gbps'),
(100000000, '100 Gbps'),
(1544, 'T1 (1.544 Mbps)'),
(2048, 'E1 (2.048 Mbps)'),
]

View File

@@ -1,14 +1,14 @@
from django import forms
from django.utils.translation import gettext as _
from circuits.choices import CircuitStatusChoices
from circuits.choices import CircuitCommitRateChoices, CircuitStatusChoices
from circuits.models import *
from ipam.models import ASN
from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant
from utilities.forms import add_blank_choice
from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
from utilities.forms.widgets import DatePicker
from utilities.forms.widgets import DatePicker, NumberWithOptions
__all__ = (
'CircuitBulkEditForm',
@@ -139,7 +139,10 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
)
commit_rate = forms.IntegerField(
required=False,
label=_('Commit rate (Kbps)')
label=_('Commit rate (Kbps)'),
widget=NumberWithOptions(
options=CircuitCommitRateChoices
)
)
description = forms.CharField(
max_length=100,

View File

@@ -74,7 +74,8 @@ class CircuitImportForm(NetBoxModelImportForm):
provider_account = CSVModelChoiceField(
queryset=ProviderAccount.objects.all(),
to_field_name='name',
help_text=_('Assigned provider account')
help_text=_('Assigned provider account'),
required=False
)
type = CSVModelChoiceField(
queryset=CircuitType.objects.all(),

View File

@@ -1,14 +1,14 @@
from django import forms
from django.utils.translation import gettext as _
from circuits.choices import CircuitStatusChoices
from circuits.choices import CircuitCommitRateChoices, CircuitStatusChoices
from circuits.models import *
from dcim.models import Region, Site, SiteGroup
from ipam.models import ASN
from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import TenancyFilterForm, ContactModelFilterForm
from utilities.forms.fields import DynamicModelMultipleChoiceField, TagFilterField
from utilities.forms.widgets import DatePicker
from utilities.forms.widgets import DatePicker, NumberWithOptions
__all__ = (
'CircuitFilterForm',
@@ -168,6 +168,9 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
commit_rate = forms.IntegerField(
required=False,
min_value=0,
label=_('Commit rate (Kbps)')
label=_('Commit rate (Kbps)'),
widget=NumberWithOptions(
options=CircuitCommitRateChoices
)
)
tag = TagFilterField(model)

View File

@@ -1,12 +1,13 @@
from django.utils.translation import gettext as _
from circuits.choices import CircuitCommitRateChoices, CircuitTerminationPortSpeedChoices
from circuits.models import *
from dcim.models import Site
from ipam.models import ASN
from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm
from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
from utilities.forms.widgets import DatePicker, SelectSpeedWidget
from utilities.forms.widgets import DatePicker, NumberWithOptions
__all__ = (
'CircuitForm',
@@ -116,7 +117,9 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
widgets = {
'install_date': DatePicker(),
'termination_date': DatePicker(),
'commit_rate': SelectSpeedWidget(),
'commit_rate': NumberWithOptions(
options=CircuitCommitRateChoices
),
}
@@ -143,6 +146,10 @@ class CircuitTerminationForm(NetBoxModelForm):
'xconnect_id', 'pp_info', 'description', 'tags',
]
widgets = {
'port_speed': SelectSpeedWidget(),
'upstream_speed': SelectSpeedWidget(),
'port_speed': NumberWithOptions(
options=CircuitTerminationPortSpeedChoices
),
'upstream_speed': NumberWithOptions(
options=CircuitTerminationPortSpeedChoices
),
}

View File

@@ -1,21 +1,12 @@
import re
import typing
from drf_spectacular.extensions import (
OpenApiSerializerFieldExtension,
OpenApiViewExtension,
)
from drf_spectacular.extensions import OpenApiSerializerFieldExtension
from drf_spectacular.openapi import AutoSchema
from drf_spectacular.plumbing import (
ComponentRegistry,
ResolvedComponent,
build_basic_type,
build_media_type_object,
build_object_type,
is_serializer,
build_basic_type, build_choice_field, build_media_type_object, build_object_type, get_doc,
)
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema
from rest_framework.relations import ManyRelatedField
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
@@ -38,7 +29,7 @@ class ChoiceFieldFix(OpenApiSerializerFieldExtension):
def map_serializer_field(self, auto_schema, direction):
if direction == 'request':
return build_basic_type(OpenApiTypes.STR)
return build_choice_field(self.target)
elif direction == "response":
return build_object_type(
@@ -150,8 +141,12 @@ class NetBoxAutoSchema(AutoSchema):
def get_writable_class(self, serializer):
properties = {}
fields = {} if hasattr(serializer, 'child') else serializer.fields
remove_fields = []
for child_name, child in fields.items():
# read_only fields don't need to be in writable (write only) serializers
if 'read_only' in dir(child) and child.read_only:
remove_fields.append(child_name)
if isinstance(child, (ChoiceField, WritableNestedSerializer)):
properties[child_name] = None
elif isinstance(child, ManyRelatedField) and isinstance(child.child_relation, SerializedPKRelatedField):
@@ -165,7 +160,12 @@ class NetBoxAutoSchema(AutoSchema):
meta_class = getattr(type(serializer), 'Meta', None)
if meta_class:
ref_name = 'Writable' + self.get_serializer_ref_name(serializer)
writable_meta = type('Meta', (meta_class,), {'ref_name': ref_name})
# remove read_only fields from write-only serializers
fields = list(meta_class.fields)
for field in remove_fields:
fields.remove(field)
writable_meta = type('Meta', (meta_class,), {'ref_name': ref_name, 'fields': fields})
properties['Meta'] = writable_meta
self.writable_serializers[type(serializer)] = type(writable_name, (type(serializer),), properties)
@@ -222,3 +222,31 @@ class NetBoxAutoSchema(AutoSchema):
if request_body_required:
request_body['required'] = request_body_required
return request_body
def get_description(self):
"""
Return a string description for the ViewSet.
"""
# If a docstring is provided, use it.
if self.view.__doc__:
return get_doc(self.view.__class__)
# When the action method is decorated with @action, use the docstring of the method.
action_or_method = getattr(self.view, getattr(self.view, 'action', self.method.lower()), None)
if action_or_method and action_or_method.__doc__:
return get_doc(action_or_method)
# Else, generate a description from the class name.
return self._generate_description()
def _generate_description(self):
"""
Generate a docstring for the method. It also takes into account whether the method is for list or detail.
"""
model_name = self.view.queryset.model._meta.verbose_name
# Determine if the method is for list or detail.
if '{id}' in self.path:
return f"{self.method.capitalize()} a {model_name} object."
return f"{self.method.capitalize()} a list of {model_name} objects."

View File

@@ -12,7 +12,7 @@ from django import forms
from django.conf import settings
from django.utils.translation import gettext as _
from dulwich import porcelain
from dulwich.config import StackedConfig
from dulwich.config import ConfigDict
from netbox.registry import registry
from .choices import DataSourceTypeChoices
@@ -31,6 +31,7 @@ def register_backend(name):
"""
Decorator for registering a DataBackend class.
"""
def _wrapper(cls):
registry['data_backends'][name] = cls
return cls
@@ -56,7 +57,6 @@ class DataBackend:
@register_backend(DataSourceTypeChoices.LOCAL)
class LocalBackend(DataBackend):
@contextmanager
def fetch(self):
logger.debug(f"Data source type is local; skipping fetch")
@@ -71,12 +71,14 @@ class GitBackend(DataBackend):
'username': forms.CharField(
required=False,
label=_('Username'),
widget=forms.TextInput(attrs={'class': 'form-control'})
widget=forms.TextInput(attrs={'class': 'form-control'}),
help_text=_("Only used for cloning with HTTP / HTTPS"),
),
'password': forms.CharField(
required=False,
label=_('Password'),
widget=forms.TextInput(attrs={'class': 'form-control'})
widget=forms.TextInput(attrs={'class': 'form-control'}),
help_text=_("Only used for cloning with HTTP / HTTPS"),
),
'branch': forms.CharField(
required=False,
@@ -89,10 +91,22 @@ class GitBackend(DataBackend):
def fetch(self):
local_path = tempfile.TemporaryDirectory()
username = self.params.get('username')
password = self.params.get('password')
branch = self.params.get('branch')
config = StackedConfig.default()
config = ConfigDict()
clone_args = {
"branch": self.params.get('branch'),
"config": config,
"depth": 1,
"errstream": porcelain.NoneStream(),
"quiet": True,
}
if self.url_scheme in ('http', 'https'):
clone_args.update(
{
"username": self.params.get('username'),
"password": self.params.get('password'),
}
)
if settings.HTTP_PROXIES and self.url_scheme in ('http', 'https'):
if proxy := settings.HTTP_PROXIES.get(self.url_scheme):
@@ -100,10 +114,7 @@ class GitBackend(DataBackend):
logger.debug(f"Cloning git repo: {self.url}")
try:
porcelain.clone(
self.url, local_path.name, depth=1, branch=branch, username=username, password=password,
config=config, quiet=True, errstream=porcelain.NoneStream()
)
porcelain.clone(self.url, local_path.name, **clone_args)
except BaseException as e:
raise SyncError(f"Fetching remote data failed ({type(e).__name__}): {e}")

View File

@@ -0,0 +1,25 @@
from django import forms
from django.utils.translation import gettext as _
from core.models import DataFile, DataSource
from utilities.forms.fields import DynamicModelChoiceField
__all__ = (
'SyncedDataMixin',
)
class SyncedDataMixin(forms.Form):
data_source = DynamicModelChoiceField(
queryset=DataSource.objects.all(),
required=False,
label=_('Data source')
)
data_file = DynamicModelChoiceField(
queryset=DataFile.objects.all(),
required=False,
label=_('File'),
query_params={
'source_id': '$data_source',
}
)

View File

@@ -2,8 +2,8 @@ import copy
from django import forms
from core.forms.mixins import SyncedDataMixin
from core.models import *
from extras.forms.mixins import SyncedDataMixin
from netbox.forms import NetBoxModelForm
from netbox.registry import registry
from utilities.forms import get_field_value

View File

@@ -456,7 +456,7 @@ class NestedInventoryItemRoleSerializer(WritableNestedSerializer):
# Cables
#
class NestedCableSerializer(BaseModelSerializer):
class NestedCableSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
class Meta:

View File

@@ -904,7 +904,11 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
)
count_ipaddresses = serializers.IntegerField(read_only=True)
count_fhrp_groups = serializers.IntegerField(read_only=True)
mac_address = serializers.CharField(required=False, default=None)
mac_address = serializers.CharField(
required=False,
default=None,
allow_null=True
)
wwn = serializers.CharField(required=False, default=None)
class Meta:

View File

@@ -1096,6 +1096,20 @@ class InterfaceTypeChoices(ChoiceSet):
)
class InterfaceSpeedChoices(ChoiceSet):
key = 'Interface.speed'
CHOICES = [
(10000, '10 Mbps'),
(100000, '100 Mbps'),
(1000000, '1 Gbps'),
(10000000, '10 Gbps'),
(25000000, '25 Gbps'),
(40000000, '40 Gbps'),
(100000000, '100 Gbps'),
]
class InterfaceDuplexChoices(ChoiceSet):
DUPLEX_HALF = 'half'

View File

@@ -811,7 +811,7 @@ class PlatformFilterSet(OrganizationalModelFilterSet):
class Meta:
model = Platform
fields = ['id', 'name', 'slug', 'description']
fields = ['id', 'name', 'slug', 'napalm_driver', 'description']
class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet, LocalConfigContextFilterSet):
@@ -1900,6 +1900,7 @@ class PowerFeedFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet, PathEndpoi
return queryset
qs_filter = (
Q(name__icontains=value) |
Q(power_panel__name__icontains=value) |
Q(comments__icontains=value)
)
return queryset.filter(qs_filter)

View File

@@ -12,7 +12,8 @@ from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant
from utilities.forms import BulkEditForm, add_blank_choice, form_from_model
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
from utilities.forms.widgets import BulkEditNullBooleanSelect, SelectSpeedWidget
from utilities.forms.widgets import BulkEditNullBooleanSelect, NumberWithOptions
from wireless.models import WirelessLAN, WirelessLANGroup
__all__ = (
'CableBulkEditForm',
@@ -470,6 +471,10 @@ class PlatformBulkEditForm(NetBoxModelBulkEditForm):
queryset=Manufacturer.objects.all(),
required=False
)
napalm_driver = forms.CharField(
max_length=50,
required=False
)
config_template = DynamicModelChoiceField(
queryset=ConfigTemplate.objects.all(),
required=False
@@ -481,9 +486,9 @@ class PlatformBulkEditForm(NetBoxModelBulkEditForm):
model = Platform
fieldsets = (
(None, ('manufacturer', 'config_template', 'description')),
(None, ('manufacturer', 'config_template', 'napalm_driver', 'description')),
)
nullable_fields = ('manufacturer', 'config_template', 'description')
nullable_fields = ('manufacturer', 'config_template', 'napalm_driver', 'description')
class DeviceBulkEditForm(NetBoxModelBulkEditForm):
@@ -1135,7 +1140,7 @@ class InterfaceBulkEditForm(
form_from_model(Interface, [
'label', 'type', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'mtu', 'mgmt_only',
'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width',
'tx_power',
'tx_power', 'wireless_lans'
]),
ComponentBulkEditForm
):
@@ -1169,8 +1174,9 @@ class InterfaceBulkEditForm(
)
speed = forms.IntegerField(
required=False,
widget=SelectSpeedWidget(),
label=_('Speed')
widget=NumberWithOptions(
options=InterfaceSpeedChoices
)
)
mgmt_only = forms.NullBooleanField(
required=False,
@@ -1224,6 +1230,19 @@ class InterfaceBulkEditForm(
required=False,
label=_('VRF')
)
wireless_lan_group = DynamicModelChoiceField(
queryset=WirelessLANGroup.objects.all(),
required=False,
label=_('Wireless LAN group')
)
wireless_lans = DynamicModelMultipleChoiceField(
queryset=WirelessLAN.objects.all(),
required=False,
label=_('Wireless LANs'),
query_params={
'group_id': '$wireless_lan_group',
}
)
model = Interface
fieldsets = (
@@ -1233,12 +1252,14 @@ class InterfaceBulkEditForm(
('PoE', ('poe_mode', 'poe_type')),
('Related Interfaces', ('parent', 'bridge', 'lag')),
('802.1Q Switching', ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
('Wireless', ('rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width')),
('Wireless', (
'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', 'wireless_lans',
)),
)
nullable_fields = (
'module', 'label', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'vdcs', 'mtu', 'description',
'poe_mode', 'poe_type', 'mode', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power',
'vlan_group', 'untagged_vlan', 'tagged_vlans', 'vrf',
'vlan_group', 'untagged_vlan', 'tagged_vlans', 'vrf', 'wireless_lans'
)
def __init__(self, *args, **kwargs):
@@ -1305,6 +1326,11 @@ class FrontPortBulkEditForm(
form_from_model(FrontPort, ['label', 'type', 'color', 'mark_connected', 'description']),
ComponentBulkEditForm
):
mark_connected = forms.NullBooleanField(
required=False,
widget=BulkEditNullBooleanSelect
)
model = FrontPort
fieldsets = (
(None, ('module', 'type', 'label', 'color', 'description', 'mark_connected')),
@@ -1316,6 +1342,11 @@ class RearPortBulkEditForm(
form_from_model(RearPort, ['label', 'type', 'color', 'mark_connected', 'description']),
ComponentBulkEditForm
):
mark_connected = forms.NullBooleanField(
required=False,
widget=BulkEditNullBooleanSelect
)
model = RearPort
fieldsets = (
(None, ('module', 'type', 'label', 'color', 'description', 'mark_connected')),

View File

@@ -347,7 +347,7 @@ class PlatformImportForm(NetBoxModelImportForm):
class Meta:
model = Platform
fields = (
'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags',
'name', 'slug', 'manufacturer', 'config_template', 'napalm_driver', 'napalm_args', 'description', 'tags',
)
@@ -947,7 +947,7 @@ class InventoryItemImportForm(NetBoxModelImportForm):
component_name = self.cleaned_data.get('component_name')
device = self.cleaned_data.get("device")
if not device and hasattr(self, 'instance'):
if not device and hasattr(self, 'instance') and hasattr(self.instance, 'device'):
device = self.instance.device
if not all([device, content_type, component_name]):

View File

@@ -12,7 +12,7 @@ from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
from utilities.forms.widgets import APISelectMultiple, SelectSpeedWidget
from utilities.forms.widgets import APISelectMultiple, NumberWithOptions
from wireless.choices import *
__all__ = (
@@ -298,6 +298,15 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
class RackElevationFilterForm(RackFilterForm):
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'id')),
('Function', ('status', 'role_id')),
('Hardware', ('type', 'width', 'serial', 'asset_tag')),
('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role', 'contact_group')),
('Weight', ('weight', 'max_weight', 'weight_unit')),
)
id = DynamicModelMultipleChoiceField(
queryset=Rack.objects.all(),
label=_('Rack'),
@@ -1154,8 +1163,9 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
)
speed = forms.IntegerField(
required=False,
label='Speed',
widget=SelectSpeedWidget()
widget=NumberWithOptions(
options=InterfaceSpeedChoices
)
)
duplex = forms.MultipleChoiceField(
choices=InterfaceDuplexChoices,

View File

@@ -16,7 +16,7 @@ from utilities.forms.fields import (
CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField,
NumericArrayField, SlugField,
)
from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, SelectSpeedWidget, SelectWithPK
from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK
from virtualization.models import Cluster
from wireless.models import WirelessLAN, WirelessLANGroup
from .common import InterfaceCommonForm, ModuleCommonForm
@@ -361,15 +361,18 @@ class PlatformForm(NetBoxModelForm):
fieldsets = (
('Platform', (
'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags',
'name', 'slug', 'manufacturer', 'config_template', 'napalm_driver', 'napalm_args', 'description', 'tags',
)),
)
class Meta:
model = Platform
fields = [
'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags',
'name', 'slug', 'manufacturer', 'config_template', 'napalm_driver', 'napalm_args', 'description', 'tags',
]
widgets = {
'napalm_args': forms.Textarea(),
}
class DeviceForm(TenancyForm, NetBoxModelForm):
@@ -1136,7 +1139,9 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
'untagged_vlan', 'tagged_vlans', 'vrf', 'tags',
]
widgets = {
'speed': SelectSpeedWidget(),
'speed': NumberWithOptions(
options=InterfaceSpeedChoices
),
'mode': HTMXSelect(),
}
labels = {

View File

@@ -4,6 +4,7 @@ from django.utils.translation import gettext as _
from dcim.models import *
from netbox.forms import NetBoxModelForm
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField
from utilities.forms.widgets import APISelect
from . import model_forms
__all__ = (
@@ -225,6 +226,18 @@ class InterfaceCreateForm(ComponentCreateForm, model_forms.InterfaceForm):
class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
device = DynamicModelChoiceField(
queryset=Device.objects.all(),
selector=True,
widget=APISelect(
# TODO: Clean up the application of HTMXSelect attributes
attrs={
'hx-get': '.',
'hx-include': f'#form_fields',
'hx-target': f'#form_fields',
}
)
)
rear_port = forms.MultipleChoiceField(
choices=[],
label=_('Rear ports'),
@@ -244,9 +257,10 @@ class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
device = Device.objects.get(
pk=self.initial.get('device') or self.data.get('device')
)
if device_id := self.data.get('device') or self.initial.get('device'):
device = Device.objects.get(pk=device_id)
else:
return
# Determine which rear port positions are occupied. These will be excluded from the list of available
# mappings.

View File

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

View File

@@ -97,7 +97,8 @@ class ComponentModel(NetBoxModel):
def clean(self):
super().clean()
if self.pk is not None and self._original_device != self.device_id:
# Check list of Modules that allow device field to be changed
if (type(self) not in [InventoryItem]) and (self.pk is not None) and (self._original_device != self.device_id):
raise ValidationError({
"device": "Components cannot be moved to a different device."
})

View File

@@ -172,6 +172,7 @@ class PlatformIndex(SearchIndex):
fields = (
('name', 100),
('slug', 110),
('napalm_driver', 300),
('description', 500),
)

View File

@@ -37,15 +37,28 @@ def get_device_name(device):
def get_device_description(device):
return '{} ({}) — {} {} ({}U) {} {}'.format(
device.name,
device.device_role,
device.device_type.manufacturer.name,
device.device_type.model,
floatformat(device.device_type.u_height),
device.asset_tag or '',
device.serial or ''
)
"""
Return a description for a device to be rendered in the rack elevation in the following format
Name: <name>
Role: <device_role>
Device Type: <manufacturer> <model> (<u_height>)
Asset tag: <asset_tag> (if defined)
Serial: <serial> (if defined)
Description: <description> (if defined)
"""
description = f'Name: {device.name}'
description += f'\nRole: {device.device_role}'
u_height = f'{floatformat(device.device_type.u_height)}U'
description += f'\nDevice Type: {device.device_type.manufacturer.name} {device.device_type.model} ({u_height})'
if device.asset_tag:
description += f'\nAsset tag: {device.asset_tag}'
if device.serial:
description += f'\nSerial: {device.serial}'
if device.description:
description += f'\nDescription: {device.description}'
return description
class RackElevationSVG:

View File

@@ -39,6 +39,10 @@ __all__ = (
'VirtualDeviceContextTable'
)
MODULEBAY_STATUS = """
{% badge record.installed_module.get_status_display bg_color=record.installed_module.get_status_color %}
"""
def get_cabletermination_row_class(record):
if record.mark_connected:
@@ -133,11 +137,11 @@ class PlatformTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = models.Platform
fields = (
'pk', 'id', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'config_template', 'description',
'tags', 'actions', 'created', 'last_updated',
'pk', 'id', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'config_template', 'napalm_driver',
'napalm_args', 'description', 'tags', 'actions', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'description',
'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'napalm_driver', 'description',
)
@@ -781,14 +785,17 @@ class ModuleBayTable(DeviceComponentTable):
tags = columns.TagColumn(
url_name='dcim:modulebay_list'
)
module_status = columns.TemplateColumn(
template_code=MODULEBAY_STATUS
)
class Meta(DeviceComponentTable.Meta):
model = models.ModuleBay
fields = (
'pk', 'id', 'name', 'device', 'label', 'position', 'installed_module', 'module_serial', 'module_asset_tag',
'description', 'tags',
'pk', 'id', 'name', 'device', 'label', 'position', 'installed_module', 'module_status', 'module_serial',
'module_asset_tag', 'description', 'tags',
)
default_columns = ('pk', 'name', 'device', 'label', 'installed_module', 'description')
default_columns = ('pk', 'name', 'device', 'label', 'installed_module', 'module_status', 'description')
class DeviceModuleBayTable(ModuleBayTable):
@@ -799,10 +806,10 @@ class DeviceModuleBayTable(ModuleBayTable):
class Meta(DeviceComponentTable.Meta):
model = models.ModuleBay
fields = (
'pk', 'id', 'name', 'label', 'position', 'installed_module', 'module_serial', 'module_asset_tag',
'pk', 'id', 'name', 'label', 'position', 'installed_module', 'module_status', 'module_serial', 'module_asset_tag',
'description', 'tags', 'actions',
)
default_columns = ('pk', 'name', 'label', 'installed_module', 'description')
default_columns = ('pk', 'name', 'label', 'installed_module', 'module_status', 'description')
class InventoryItemTable(DeviceComponentTable):

View File

@@ -1498,9 +1498,9 @@ class PlatformTestCase(TestCase, ChangeLoggedFilterSetTests):
Manufacturer.objects.bulk_create(manufacturers)
platforms = (
Platform(name='Platform 1', slug='platform-1', manufacturer=manufacturers[0], description='A'),
Platform(name='Platform 2', slug='platform-2', manufacturer=manufacturers[1], description='B'),
Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturers[2], description='C'),
Platform(name='Platform 1', slug='platform-1', manufacturer=manufacturers[0], napalm_driver='driver-1', description='A'),
Platform(name='Platform 2', slug='platform-2', manufacturer=manufacturers[1], napalm_driver='driver-2', description='B'),
Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturers[2], napalm_driver='driver-3', description='C'),
)
Platform.objects.bulk_create(platforms)
@@ -1516,6 +1516,10 @@ class PlatformTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'description': ['A', 'B']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_napalm_driver(self):
params = {'napalm_driver': ['driver-1', 'driver-2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_manufacturer(self):
manufacturers = Manufacturer.objects.all()[:2]
params = {'manufacturer_id': [manufacturers[0].pk, manufacturers[1].pk]}

View File

@@ -1591,6 +1591,8 @@ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
'name': 'Platform X',
'slug': 'platform-x',
'manufacturer': manufacturer.pk,
'napalm_driver': 'junos',
'napalm_args': None,
'description': 'A new platform',
'tags': [t.pk for t in tags],
}
@@ -1610,6 +1612,7 @@ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
)
cls.bulk_edit_data = {
'napalm_driver': 'ios',
'description': 'New description',
}

View File

@@ -371,7 +371,7 @@ class SiteView(generic.ObjectView):
(VLANGroup.objects.restrict(request.user, 'view').filter(
scope_type=ContentType.objects.get_for_model(Site),
scope_id=instance.pk
), 'site_id'),
), 'site'),
(VLAN.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'),
# Circuits
(Circuit.objects.restrict(request.user, 'view').filter(terminations__site=instance).distinct(), 'site_id'),
@@ -687,6 +687,7 @@ class RackView(generic.ObjectView):
'next_rack': next_rack,
'prev_rack': prev_rack,
'svg_extra': svg_extra,
'peer_racks': peer_racks,
}

View File

@@ -187,11 +187,10 @@ class ReportViewSet(ViewSet):
"""
Compile all reports and their related results (if any). Result data is deferred in the list view.
"""
report_content_type = ContentType.objects.get(app_label='extras', model='report')
results = {
r.name: r
for r in Job.objects.filter(
object_type=report_content_type,
job.name: job
for job in Job.objects.filter(
object_type=ContentType.objects.get(app_label='extras', model='reportmodule'),
status__in=JobStatusChoices.TERMINAL_STATE_CHOICES
).order_by('name', '-created').distinct('name').defer('data')
}
@@ -202,7 +201,7 @@ class ReportViewSet(ViewSet):
# Attach Job objects to each report (if any)
for report in report_list:
report.result = results.get(report.full_name, None)
report.result = results.get(report.name, None)
serializer = serializers.ReportSerializer(report_list, many=True, context={
'request': request,
@@ -290,12 +289,10 @@ class ScriptViewSet(ViewSet):
return module, script
def list(self, request):
script_content_type = ContentType.objects.get(app_label='extras', model='script')
results = {
r.name: r
for r in Job.objects.filter(
object_type=script_content_type,
job.name: job
for job in Job.objects.filter(
object_type=ContentType.objects.get(app_label='extras', model='scriptmodule'),
status__in=JobStatusChoices.TERMINAL_STATE_CHOICES
).order_by('name', '-created').distinct('name').defer('data')
}
@@ -306,7 +303,7 @@ class ScriptViewSet(ViewSet):
# Attach Job objects to each script (if any)
for script in script_list:
script.result = results.get(script.full_name, None)
script.result = results.get(script.name, None)
serializer = serializers.ScriptSerializer(script_list, many=True, context={'request': request})

View File

@@ -116,7 +116,7 @@ class JournalEntryKindChoices(ChoiceSet):
#
# Log Levels for Reports and Scripts
# Reports and Scripts
#
class LogLevelChoices(ChoiceSet):
@@ -136,6 +136,17 @@ class LogLevelChoices(ChoiceSet):
)
class DurationChoices(ChoiceSet):
CHOICES = (
(60, 'Hourly'),
(720, '12 hours'),
(1440, 'Daily'),
(10080, 'Weekly'),
(43200, '30 days'),
)
#
# Job results
#

View File

@@ -4,10 +4,12 @@ from hashlib import sha256
from urllib.parse import urlencode
import feedparser
import requests
from django import forms
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.cache import cache
from django.db.models import Q
from django.template.loader import render_to_string
from django.urls import NoReverseMatch, reverse
from django.utils.translation import gettext as _
@@ -33,7 +35,7 @@ def get_content_type_labels():
return [
(content_type_identifier(ct), content_type_name(ct))
for ct in ContentType.objects.filter(
FeatureQuery('export_templates').get_query()
FeatureQuery('export_templates').get_query() | Q(app_label='extras', model='objectchange')
).order_by('app_label', 'model')
]
@@ -227,16 +229,19 @@ class ObjectListWidget(DashboardWidget):
htmx_url = reverse(viewname)
except NoReverseMatch:
htmx_url = None
if parameters := self.config.get('url_params'):
parameters = self.config.get('url_params') or {}
if page_size := self.config.get('page_size'):
parameters['per_page'] = page_size
if parameters:
try:
htmx_url = f'{htmx_url}?{urlencode(parameters)}'
htmx_url = f'{htmx_url}?{urlencode(parameters, doseq=True)}'
except ValueError:
pass
return render_to_string(self.template_name, {
'viewname': viewname,
'has_permission': has_permission,
'htmx_url': htmx_url,
'page_size': self.config.get('page_size'),
})
@@ -268,12 +273,9 @@ class RSSFeedWidget(DashboardWidget):
)
def render(self, request):
url = self.config['feed_url']
feed = self.get_feed()
return render_to_string(self.template_name, {
'url': url,
'feed': feed,
'url': self.config['feed_url'],
**self.get_feed()
})
@cached_property
@@ -285,17 +287,33 @@ class RSSFeedWidget(DashboardWidget):
def get_feed(self):
# Fetch RSS content from cache if available
if feed_content := cache.get(self.cache_key):
feed = feedparser.FeedParserDict(feed_content)
else:
feed = feedparser.parse(
self.config['feed_url'],
request_headers={'User-Agent': f'NetBox/{settings.VERSION}'}
)
if not feed.bozo:
# Cap number of entries
max_entries = self.config.get('max_entries')
feed['entries'] = feed['entries'][:max_entries]
# Cache the feed content
cache.set(self.cache_key, dict(feed), self.config.get('cache_timeout'))
return {
'feed': feedparser.FeedParserDict(feed_content),
}
return feed
# Fetch feed content from remote server
try:
response = requests.get(
url=self.config['feed_url'],
headers={'User-Agent': f'NetBox/{settings.VERSION}'},
proxies=settings.HTTP_PROXIES,
timeout=3
)
response.raise_for_status()
except requests.exceptions.RequestException as e:
return {
'error': e,
}
# Parse feed content
feed = feedparser.parse(response.content)
if not feed.bozo:
# Cap number of entries
max_entries = self.config.get('max_entries')
feed['entries'] = feed['entries'][:max_entries]
# Cache the feed content
cache.set(self.cache_key, dict(feed), self.config.get('cache_timeout'))
return {
'feed': feed,
}

View File

@@ -4,9 +4,10 @@ from django.contrib.postgres.forms import SimpleArrayField
from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _
from extras.choices import CustomFieldVisibilityChoices, CustomFieldTypeChoices
from extras.choices import CustomFieldVisibilityChoices, CustomFieldTypeChoices, JournalEntryKindChoices
from extras.models import *
from extras.utils import FeatureQuery
from netbox.forms import NetBoxModelImportForm
from utilities.forms import CSVModelForm
from utilities.forms.fields import CSVChoiceField, CSVContentTypeField, CSVMultipleContentTypeField, SlugField
@@ -15,6 +16,7 @@ __all__ = (
'CustomFieldImportForm',
'CustomLinkImportForm',
'ExportTemplateImportForm',
'JournalEntryImportForm',
'SavedFilterImportForm',
'TagImportForm',
'WebhookImportForm',
@@ -132,3 +134,20 @@ class TagImportForm(CSVModelForm):
help_texts = {
'color': mark_safe(_('RGB color in hexadecimal (e.g. <code>00ff00</code>)')),
}
class JournalEntryImportForm(NetBoxModelImportForm):
assigned_object_type = CSVContentTypeField(
queryset=ContentType.objects.all(),
label=_('Assigned object type'),
)
kind = CSVChoiceField(
choices=JournalEntryKindChoices,
help_text=_('The classification of entry')
)
class Meta:
model = JournalEntry
fields = (
'assigned_object_type', 'assigned_object_id', 'created_by', 'kind', 'comments', 'tags'
)

View File

@@ -11,7 +11,7 @@ from extras.utils import FeatureQuery
from netbox.forms.base import NetBoxModelFilterSetForm
from tenancy.models import Tenant, TenantGroup
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
from utilities.forms.fields import ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField
from utilities.forms.fields import ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField
from utilities.forms.widgets import APISelectMultiple, DateTimePicker
from virtualization.models import Cluster, ClusterGroup, ClusterType
from .mixins import SavedFiltersMixin
@@ -22,6 +22,7 @@ __all__ = (
'CustomFieldFilterForm',
'CustomLinkFilterForm',
'ExportTemplateFilterForm',
'ImageAttachmentFilterForm',
'JournalEntryFilterForm',
'LocalConfigContextFilterForm',
'ObjectChangeFilterForm',
@@ -137,6 +138,20 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
)
class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
(None, ('q', 'filter_id')),
('Attributes', ('content_type_id', 'name',)),
)
content_type_id = ContentTypeChoiceField(
queryset=ContentType.objects.filter(FeatureQuery('custom_fields').get_query()),
required=False
)
name = forms.CharField(
required=False
)
class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
(None, ('q', 'filter_id')),

View File

@@ -1,16 +1,14 @@
from django.contrib.contenttypes.models import ContentType
from django import forms
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext as _
from core.models import DataFile, DataSource
from extras.models import *
from extras.choices import CustomFieldVisibilityChoices
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField
from extras.models import *
from utilities.forms.fields import DynamicModelMultipleChoiceField
__all__ = (
'CustomFieldsMixin',
'SavedFiltersMixin',
'SyncedDataMixin',
)
@@ -74,19 +72,3 @@ class SavedFiltersMixin(forms.Form):
'usable': True,
}
)
class SyncedDataMixin(forms.Form):
data_source = DynamicModelChoiceField(
queryset=DataSource.objects.all(),
required=False,
label=_('Data source')
)
data_file = DynamicModelChoiceField(
queryset=DataFile.objects.all(),
required=False,
label=_('File'),
query_params={
'source_id': '$data_source',
}
)

View File

@@ -5,9 +5,9 @@ from django.db.models import Q
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext as _
from core.forms.mixins import SyncedDataMixin
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
from extras.choices import *
from extras.forms.mixins import SyncedDataMixin
from extras.models import *
from extras.utils import FeatureQuery
from netbox.forms import NetBoxModelForm

View File

@@ -1,8 +1,9 @@
from django import forms
from django.utils.translation import gettext as _
from extras.choices import DurationChoices
from utilities.forms import BootstrapMixin
from utilities.forms.widgets import DateTimePicker, SelectDurationWidget
from utilities.forms.widgets import DateTimePicker, NumberWithOptions
from utilities.utils import local_now
__all__ = (
@@ -21,7 +22,9 @@ class ReportForm(BootstrapMixin, forms.Form):
required=False,
min_value=1,
label=_("Recurs every"),
widget=SelectDurationWidget(),
widget=NumberWithOptions(
options=DurationChoices
),
help_text=_("Interval at which this report is re-run (in minutes)")
)

View File

@@ -1,8 +1,9 @@
from django import forms
from django.utils.translation import gettext as _
from extras.choices import DurationChoices
from utilities.forms import BootstrapMixin
from utilities.forms.widgets import DateTimePicker, SelectDurationWidget
from utilities.forms.widgets import DateTimePicker, NumberWithOptions
from utilities.utils import local_now
__all__ = (
@@ -27,7 +28,9 @@ class ScriptForm(BootstrapMixin, forms.Form):
required=False,
min_value=1,
label=_("Recurs every"),
widget=SelectDurationWidget(),
widget=NumberWithOptions(
options=DurationChoices
),
help_text=_("Interval at which this script is re-run (in minutes)")
)

View File

@@ -111,7 +111,7 @@ class Command(BaseCommand):
# Create the job
job = Job.objects.create(
instance=module,
object=module,
name=script.name,
user=User.objects.filter(is_superuser=True).order_by('pk')[0],
job_id=uuid.uuid4()

View File

@@ -606,5 +606,18 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
f"Invalid choice(s) ({', '.join(value)}). Available choices are: {', '.join(self.choices)}"
)
# Validate selected object
elif self.type == CustomFieldTypeChoices.TYPE_OBJECT:
if type(value) is not int:
raise ValidationError(f"Value must be an object ID, not {type(value).__name__}")
# Validate selected objects
elif self.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
if type(value) is not list:
raise ValidationError(f"Value must be a list of object IDs, not {type(value).__name__}")
for id in value:
if type(id) is not int:
raise ValidationError(f"Found invalid object ID: {id}")
elif self.required:
raise ValidationError("Required field cannot be empty.")

View File

@@ -1,4 +1,5 @@
import json
import urllib.parse
from django.conf import settings
from django.contrib import admin
@@ -19,12 +20,13 @@ from extras.choices import *
from extras.conditions import ConditionSet
from extras.constants import *
from extras.utils import FeatureQuery, image_upload
from netbox.config import get_config
from netbox.models import ChangeLoggedModel
from netbox.models.features import (
CloningMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin,
)
from utilities.querysets import RestrictedQuerySet
from utilities.utils import render_jinja2
from utilities.utils import clean_html, render_jinja2
__all__ = (
'ConfigRevision',
@@ -278,6 +280,18 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
link = render_jinja2(self.link_url, context)
link_target = ' target="_blank"' if self.new_window else ''
# Sanitize link text
allowed_schemes = get_config().ALLOWED_URL_SCHEMES
text = clean_html(text, allowed_schemes)
# Sanitize link
link = urllib.parse.quote_plus(link, safe='/:?&=%+[]@#')
# Verify link scheme is allowed
result = urllib.parse.urlparse(link)
if result.scheme and result.scheme not in allowed_schemes:
link = ""
return {
'text': text,
'link': link,

View File

@@ -1,4 +1,5 @@
import inspect
import logging
from functools import cached_property
from django.db import models
@@ -16,6 +17,8 @@ __all__ = (
'ScriptModule',
)
logger = logging.getLogger('netbox.data_backends')
class Script(WebhooksMixin, models.Model):
"""
@@ -53,7 +56,12 @@ class ScriptModule(PythonModuleMixin, JobsMixin, ManagedFile):
# For child objects in submodules use the full import path w/o the root module as the name
return cls.full_name.split(".", maxsplit=1)[1]
module = self.get_module()
try:
module = self.get_module()
except Exception as e:
logger.debug(f"Failed to load script: {self.python_name} error: {e}")
module = None
scripts = {}
ordered = getattr(module, 'script_order', [])

View File

@@ -13,6 +13,7 @@ __all__ = (
'CustomFieldTable',
'CustomLinkTable',
'ExportTemplateTable',
'ImageAttachmentTable',
'JournalEntryTable',
'ObjectChangeTable',
'SavedFilterTable',
@@ -29,6 +30,7 @@ class CustomFieldTable(NetBoxTable):
content_types = columns.ContentTypesColumn()
required = columns.BooleanColumn()
ui_visibility = columns.ChoiceFieldColumn(verbose_name="UI visibility")
description = columns.MarkdownColumn()
is_cloneable = columns.BooleanColumn()
class Meta(NetBoxTable.Meta):
@@ -85,6 +87,28 @@ class ExportTemplateTable(NetBoxTable):
)
class ImageAttachmentTable(NetBoxTable):
id = tables.Column(
linkify=False
)
content_type = columns.ContentTypeColumn()
parent = tables.Column(
linkify=True
)
size = tables.Column(
orderable=False,
verbose_name='Size (bytes)'
)
class Meta(NetBoxTable.Meta):
model = ImageAttachment
fields = (
'pk', 'content_type', 'parent', 'image', 'name', 'image_height', 'image_width', 'size', 'created',
'last_updated',
)
default_columns = ('content_type', 'parent', 'image', 'name', 'size', 'created')
class SavedFilterTable(NetBoxTable):
name = tables.Column(
linkify=True

View File

@@ -40,7 +40,6 @@ def custom_links(context, obj):
# Pass select context data when rendering the CustomLink
link_context = {
'object': obj,
'obj': obj, # TODO: Remove in NetBox v3.5
'debug': context.get('debug', False), # django.template.context_processors.debug
'request': context['request'], # django.template.context_processors.request
'user': context['user'], # django.contrib.auth.context_processors.auth

View File

@@ -479,8 +479,8 @@ class CustomLinkTest(TestCase):
def test_view_object_with_custom_link(self):
customlink = CustomLink(
name='Test',
link_text='FOO {{ obj.name }} BAR',
link_url='http://example.com/?site={{ obj.slug }}',
link_text='FOO {{ object.name }} BAR',
link_url='http://example.com/?site={{ object.slug }}',
new_window=False
)
customlink.save()

View File

@@ -73,6 +73,7 @@ urlpatterns = [
path('config-templates/<int:pk>/', include(get_model_urls('extras', 'configtemplate'))),
# Image attachments
path('image-attachments/', views.ImageAttachmentListView.as_view(), name='imageattachment_list'),
path('image-attachments/add/', views.ImageAttachmentEditView.as_view(), name='imageattachment_add'),
path('image-attachments/<int:pk>/', include(get_model_urls('extras', 'imageattachment'))),
@@ -81,6 +82,7 @@ urlpatterns = [
path('journal-entries/add/', views.JournalEntryEditView.as_view(), name='journalentry_add'),
path('journal-entries/edit/', views.JournalEntryBulkEditView.as_view(), name='journalentry_bulk_edit'),
path('journal-entries/delete/', views.JournalEntryBulkDeleteView.as_view(), name='journalentry_bulk_delete'),
path('journal-entries/import/', views.JournalEntryBulkImportView.as_view(), name='journalentry_import'),
path('journal-entries/<int:pk>/', include(get_model_urls('extras', 'journalentry'))),
# Change logging

View File

@@ -577,6 +577,14 @@ class ObjectChangeView(generic.ObjectView):
# Image attachments
#
class ImageAttachmentListView(generic.ObjectListView):
queryset = ImageAttachment.objects.all()
filterset = filtersets.ImageAttachmentFilterSet
filterset_form = forms.ImageAttachmentFilterForm
table = tables.ImageAttachmentTable
actions = ('export',)
@register_model_view(ImageAttachment, 'edit')
class ImageAttachmentEditView(generic.ObjectEditView):
queryset = ImageAttachment.objects.all()
@@ -617,7 +625,7 @@ class JournalEntryListView(generic.ObjectListView):
filterset = filtersets.JournalEntryFilterSet
filterset_form = forms.JournalEntryFilterForm
table = tables.JournalEntryTable
actions = ('export', 'bulk_edit', 'bulk_delete')
actions = ('import', 'export', 'bulk_edit', 'bulk_delete')
@register_model_view(JournalEntry)
@@ -666,6 +674,11 @@ class JournalEntryBulkDeleteView(generic.BulkDeleteView):
table = tables.JournalEntryTable
class JournalEntryBulkImportView(generic.BulkImportView):
queryset = JournalEntry.objects.all()
model_form = forms.JournalEntryImportForm
#
# Dashboard & widgets
#
@@ -1033,7 +1046,6 @@ class ScriptView(ContentTypePermissionRequiredMixin, View):
return 'extras.view_script'
def get(self, request, module, name):
print(module)
module = get_object_or_404(ScriptModule.objects.restrict(request.user), file_path__startswith=module)
script = module.scripts[name]()
form = script.as_form(initial=normalize_querydict(request.GET))

View File

@@ -0,0 +1,32 @@
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from ipam import models
from netaddr import AddrFormatError, IPNetwork
__all__ = [
'IPAddressField',
]
#
# IP address field
#
class IPAddressField(serializers.CharField):
"""IPAddressField with mask"""
default_error_messages = {
'invalid': _('Enter a valid IPv4 or IPv6 address with optional mask.'),
}
def to_internal_value(self, data):
try:
return IPNetwork(data)
except AddrFormatError:
raise serializers.ValidationError("Invalid IP address format: {}".format(data))
except (TypeError, ValueError) as e:
raise serializers.ValidationError(e)
def to_representation(self, value):
return str(value)

View File

@@ -4,6 +4,7 @@ from rest_framework import serializers
from ipam import models
from ipam.models.l2vpn import L2VPNTermination, L2VPN
from netbox.api.serializers import WritableNestedSerializer
from .field_serializers import IPAddressField
__all__ = [
'NestedAggregateSerializer',
@@ -182,6 +183,8 @@ class NestedPrefixSerializer(WritableNestedSerializer):
class NestedIPRangeSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:iprange-detail')
family = serializers.IntegerField(read_only=True)
start_address = IPAddressField()
end_address = IPAddressField()
class Meta:
model = models.IPRange
@@ -195,6 +198,7 @@ class NestedIPRangeSerializer(WritableNestedSerializer):
class NestedIPAddressSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
family = serializers.IntegerField(read_only=True)
address = IPAddressField()
class Meta:
model = models.IPAddress

View File

@@ -13,6 +13,7 @@ from tenancy.api.nested_serializers import NestedTenantSerializer
from utilities.api import get_serializer_for_model
from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
from .nested_serializers import *
from .field_serializers import IPAddressField
#
@@ -369,6 +370,8 @@ class AvailablePrefixSerializer(serializers.Serializer):
class IPRangeSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:iprange-detail')
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
start_address = IPAddressField()
end_address = IPAddressField()
vrf = NestedVRFSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True)
status = ChoiceField(choices=IPRangeStatusChoices, required=False)
@@ -391,10 +394,11 @@ class IPRangeSerializer(NetBoxModelSerializer):
class IPAddressSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
address = IPAddressField()
vrf = NestedVRFSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True)
status = ChoiceField(choices=IPAddressStatusChoices, required=False)
role = ChoiceField(choices=IPAddressRoleChoices, allow_blank=True, required=False, allow_null=True)
role = ChoiceField(choices=IPAddressRoleChoices, allow_blank=True, required=False)
assigned_object_type = ContentTypeField(
queryset=ContentType.objects.filter(IPADDRESS_ASSIGNMENT_MODELS),
required=False,

View File

@@ -262,38 +262,21 @@ class IPRangeForm(TenancyForm, NetBoxModelForm):
class IPAddressForm(TenancyForm, NetBoxModelForm):
device = DynamicModelChoiceField(
queryset=Device.objects.all(),
required=False,
initial_params={
'interfaces': '$interface'
}
)
interface = DynamicModelChoiceField(
queryset=Interface.objects.all(),
required=False,
query_params={
'device_id': '$device'
}
)
virtual_machine = DynamicModelChoiceField(
queryset=VirtualMachine.objects.all(),
required=False,
initial_params={
'interfaces': '$vminterface'
}
selector=True,
)
vminterface = DynamicModelChoiceField(
queryset=VMInterface.objects.all(),
required=False,
selector=True,
label=_('Interface'),
query_params={
'virtual_machine_id': '$virtual_machine'
}
)
fhrpgroup = DynamicModelChoiceField(
queryset=FHRPGroup.objects.all(),
required=False,
selector=True,
label=_('FHRP Group')
)
vrf = DynamicModelChoiceField(
@@ -301,33 +284,11 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
required=False,
label=_('VRF')
)
nat_device = DynamicModelChoiceField(
queryset=Device.objects.all(),
required=False,
selector=True,
label=_('Device')
)
nat_virtual_machine = DynamicModelChoiceField(
queryset=VirtualMachine.objects.all(),
required=False,
selector=True,
label=_('Virtual Machine')
)
nat_vrf = DynamicModelChoiceField(
queryset=VRF.objects.all(),
required=False,
selector=True,
label=_('VRF')
)
nat_inside = DynamicModelChoiceField(
queryset=IPAddress.objects.all(),
required=False,
selector=True,
label=_('IP Address'),
query_params={
'device_id': '$nat_device',
'virtual_machine_id': '$nat_virtual_machine',
'vrf_id': '$nat_vrf',
}
)
primary_for_parent = forms.BooleanField(
required=False,
@@ -338,8 +299,8 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
class Meta:
model = IPAddress
fields = [
'address', 'vrf', 'status', 'role', 'dns_name', 'primary_for_parent', 'nat_device', 'nat_virtual_machine',
'nat_vrf', 'nat_inside', 'tenant_group', 'tenant', 'description', 'comments', 'tags',
'address', 'vrf', 'status', 'role', 'dns_name', 'primary_for_parent', 'nat_inside', 'tenant_group',
'tenant', 'description', 'comments', 'tags',
]
def __init__(self, *args, **kwargs):
@@ -354,17 +315,6 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
initial['vminterface'] = instance.assigned_object
elif type(instance.assigned_object) is FHRPGroup:
initial['fhrpgroup'] = instance.assigned_object
if instance.nat_inside:
nat_inside_parent = instance.nat_inside.assigned_object
if type(nat_inside_parent) is Interface:
initial['nat_site'] = nat_inside_parent.device.site.pk
if nat_inside_parent.device.rack:
initial['nat_rack'] = nat_inside_parent.device.rack.pk
initial['nat_device'] = nat_inside_parent.device.pk
elif type(nat_inside_parent) is VMInterface:
if cluster := nat_inside_parent.virtual_machine.cluster:
initial['nat_cluster'] = cluster.pk
initial['nat_virtual_machine'] = nat_inside_parent.virtual_machine.pk
kwargs['initial'] = initial
super().__init__(*args, **kwargs)

View File

@@ -14,6 +14,7 @@ from utilities.views import ViewTab, register_model_view
from virtualization.filtersets import VMInterfaceFilterSet
from virtualization.models import VMInterface
from . import filtersets, forms, tables
from .choices import PrefixStatusChoices
from .constants import *
from .models import *
from .tables.l2vpn import L2VPNTable, L2VPNTerminationTable
@@ -495,7 +496,7 @@ class PrefixView(generic.ObjectView):
# Parent prefixes table
parent_prefixes = Prefix.objects.restrict(request.user, 'view').filter(
Q(vrf=instance.vrf) | Q(vrf__isnull=True)
Q(vrf=instance.vrf) | Q(vrf__isnull=True, status=PrefixStatusChoices.STATUS_CONTAINER)
).filter(
prefix__net_contains=str(instance.prefix)
).prefetch_related(

View File

@@ -14,35 +14,13 @@ __all__ = (
class CustomFieldModelSerializer(serializers.Serializer):
"""
Introduces support for custom field assignment. Adds `custom_fields` serialization and ensures
that custom field data is populated upon initialization.
Introduces support for custom field assignment and representation.
"""
custom_fields = CustomFieldsDataField(
source='custom_field_data',
default=CreateOnlyDefault(CustomFieldDefaultValues())
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance is not None:
# Retrieve the set of CustomFields which apply to this type of object
content_type = ContentType.objects.get_for_model(self.Meta.model)
fields = CustomField.objects.filter(content_types=content_type)
# Populate custom field values for each instance from database
if type(self.instance) in (list, tuple):
for obj in self.instance:
self._populate_custom_fields(obj, fields)
else:
self._populate_custom_fields(self.instance, fields)
def _populate_custom_fields(self, instance, custom_fields):
instance.custom_fields = {}
for field in custom_fields:
instance.custom_fields[field.name] = instance.cf.get(field.name)
class TaggableModelSerializer(serializers.Serializer):
"""

View File

@@ -13,6 +13,7 @@ ALLOWED_HOSTS = []
# PostgreSQL database configuration. See the Django documentation for a complete list of available parameters:
# https://docs.djangoproject.com/en/stable/ref/settings/#databases
DATABASE = {
'ENGINE': 'django.db.backends.postgresql', # Database engine
'NAME': 'netbox', # Database name
'USER': '', # PostgreSQL username
'PASSWORD': '', # PostgreSQL password
@@ -193,6 +194,9 @@ PLUGINS = []
REMOTE_AUTH_ENABLED = False
REMOTE_AUTH_BACKEND = 'netbox.authentication.RemoteUserBackend'
REMOTE_AUTH_HEADER = 'HTTP_REMOTE_USER'
REMOTE_AUTH_USER_FIRST_NAME = 'HTTP_REMOTE_USER_FIRST_NAME'
REMOTE_AUTH_USER_LAST_NAME = 'HTTP_REMOTE_USER_LAST_NAME'
REMOTE_AUTH_USER_EMAIL = 'HTTP_REMOTE_USER_EMAIL'
REMOTE_AUTH_AUTO_CREATE_USER = True
REMOTE_AUTH_DEFAULT_GROUPS = []
REMOTE_AUTH_DEFAULT_PERMISSIONS = {}

View File

@@ -139,7 +139,17 @@ class RemoteUserMiddleware(RemoteUserMiddleware_):
else:
user = auth.authenticate(request, remote_user=username)
if user:
# User is valid. Set request.user and persist user in the session
# User is valid.
# Update the User's Profile if set by request headers
if settings.REMOTE_AUTH_USER_FIRST_NAME in request.META:
user.first_name = request.META[settings.REMOTE_AUTH_USER_FIRST_NAME]
if settings.REMOTE_AUTH_USER_LAST_NAME in request.META:
user.last_name = request.META[settings.REMOTE_AUTH_USER_LAST_NAME]
if settings.REMOTE_AUTH_USER_EMAIL in request.META:
user.email = request.META[settings.REMOTE_AUTH_USER_EMAIL]
user.save()
# Set request.user and persist user in the session
# by logging the user in.
request.user = user
auth.login(request, user)

View File

@@ -67,8 +67,8 @@ class NetBoxModel(CloningMixin, NetBoxFeatureSet, models.Model):
for field in self._meta.get_fields():
if isinstance(field, GenericForeignKey):
ct_value = getattr(self, field.ct_field)
fk_value = getattr(self, field.fk_field)
ct_value = getattr(self, field.ct_field, None)
fk_value = getattr(self, field.fk_field, None)
if ct_value is None and fk_value is not None:
raise ValidationError({

View File

@@ -292,6 +292,7 @@ CUSTOMIZATION_MENU = Menu(
get_model_item('extras', 'exporttemplate', _('Export Templates')),
get_model_item('extras', 'savedfilter', _('Saved Filters')),
get_model_item('extras', 'tag', 'Tags'),
get_model_item('extras', 'imageattachment', _('Image Attachments'), actions=()),
),
),
MenuGroup(
@@ -336,7 +337,7 @@ OPERATIONS_MENU = Menu(
MenuGroup(
label=_('Logging'),
items=(
get_model_item('extras', 'journalentry', _('Journal Entries'), actions=[]),
get_model_item('extras', 'journalentry', _('Journal Entries'), actions=['import']),
get_model_item('extras', 'objectchange', _('Change Log'), actions=[]),
),
),

View File

@@ -145,9 +145,12 @@ class CachedValueSearchBackend(SearchBackend):
)
# Omit any results pertaining to an object the user does not have permission to view
return [
r for r in results if r.object is not None
]
ret = []
for r in results:
if r.object is not None:
r.name = str(r.object)
ret.append(r)
return ret
def cache(self, instances, indexer=None, remove_existing=True):
content_type = None

View File

@@ -3,9 +3,10 @@ import importlib
import importlib.util
import os
import platform
import requests
import sys
import warnings
from urllib.parse import urlsplit
from urllib.parse import urlencode, urlsplit
import django
import sentry_sdk
@@ -24,7 +25,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW
# Environment setup
#
VERSION = '3.5-beta2'
VERSION = '3.5.1'
# Hostname
HOSTNAME = platform.node()
@@ -67,6 +68,15 @@ DATABASE = getattr(configuration, 'DATABASE')
REDIS = getattr(configuration, 'REDIS')
SECRET_KEY = getattr(configuration, 'SECRET_KEY')
# Enforce minimum length for SECRET_KEY
if type(SECRET_KEY) is not str:
raise ImproperlyConfigured(f"SECRET_KEY must be a string (found {type(SECRET_KEY).__name__})")
if len(SECRET_KEY) < 50:
raise ImproperlyConfigured(
f"SECRET_KEY must be at least 50 characters in length. To generate a suitable key, run the following command:\n"
f" python {BASE_DIR}/generate_secret_key.py"
)
# Calculate a unique deployment ID from the secret key
DEPLOYMENT_ID = hashlib.sha256(SECRET_KEY.encode('utf-8')).hexdigest()[:16]
@@ -78,10 +88,12 @@ BASE_PATH = getattr(configuration, 'BASE_PATH', '')
if BASE_PATH:
BASE_PATH = BASE_PATH.strip('/') + '/' # Enforce trailing slash only
CSRF_COOKIE_PATH = LANGUAGE_COOKIE_PATH = SESSION_COOKIE_PATH = f'/{BASE_PATH.rstrip("/")}'
CENSUS_REPORTING_ENABLED = getattr(configuration, 'CENSUS_REPORTING_ENABLED', True)
CORS_ORIGIN_ALLOW_ALL = getattr(configuration, 'CORS_ORIGIN_ALLOW_ALL', False)
CORS_ORIGIN_REGEX_WHITELIST = getattr(configuration, 'CORS_ORIGIN_REGEX_WHITELIST', [])
CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', [])
CSRF_COOKIE_NAME = getattr(configuration, 'CSRF_COOKIE_NAME', 'csrftoken')
CSRF_COOKIE_SECURE = getattr(configuration, 'CSRF_COOKIE_SECURE', False)
CSRF_TRUSTED_ORIGINS = getattr(configuration, 'CSRF_TRUSTED_ORIGINS', [])
DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y')
DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
@@ -115,6 +127,9 @@ REMOTE_AUTH_DEFAULT_GROUPS = getattr(configuration, 'REMOTE_AUTH_DEFAULT_GROUPS'
REMOTE_AUTH_DEFAULT_PERMISSIONS = getattr(configuration, 'REMOTE_AUTH_DEFAULT_PERMISSIONS', {})
REMOTE_AUTH_ENABLED = getattr(configuration, 'REMOTE_AUTH_ENABLED', False)
REMOTE_AUTH_HEADER = getattr(configuration, 'REMOTE_AUTH_HEADER', 'HTTP_REMOTE_USER')
REMOTE_AUTH_USER_FIRST_NAME = getattr(configuration, 'REMOTE_AUTH_USER_FIRST_NAME', 'HTTP_REMOTE_USER_FIRST_NAME')
REMOTE_AUTH_USER_LAST_NAME = getattr(configuration, 'REMOTE_AUTH_USER_LAST_NAME', 'HTTP_REMOTE_USER_LAST_NAME')
REMOTE_AUTH_USER_EMAIL = getattr(configuration, 'REMOTE_AUTH_USER_EMAIL', 'HTTP_REMOTE_USER_EMAIL')
REMOTE_AUTH_GROUP_HEADER = getattr(configuration, 'REMOTE_AUTH_GROUP_HEADER', 'HTTP_REMOTE_USER_GROUP')
REMOTE_AUTH_GROUP_SYNC_ENABLED = getattr(configuration, 'REMOTE_AUTH_GROUP_SYNC_ENABLED', False)
REMOTE_AUTH_SUPERUSER_GROUPS = getattr(configuration, 'REMOTE_AUTH_SUPERUSER_GROUPS', [])
@@ -126,6 +141,7 @@ REPORTS_ROOT = getattr(configuration, 'REPORTS_ROOT', os.path.join(BASE_DIR, 're
RQ_DEFAULT_TIMEOUT = getattr(configuration, 'RQ_DEFAULT_TIMEOUT', 300)
SCRIPTS_ROOT = getattr(configuration, 'SCRIPTS_ROOT', os.path.join(BASE_DIR, 'scripts')).rstrip('/')
SEARCH_BACKEND = getattr(configuration, 'SEARCH_BACKEND', 'netbox.search.backends.CachedValueSearchBackend')
SECURE_SSL_REDIRECT = getattr(configuration, 'SECURE_SSL_REDIRECT', False)
SENTRY_DSN = getattr(configuration, 'SENTRY_DSN', DEFAULT_SENTRY_DSN)
SENTRY_ENABLED = getattr(configuration, 'SENTRY_ENABLED', False)
SENTRY_SAMPLE_RATE = getattr(configuration, 'SENTRY_SAMPLE_RATE', 1.0)
@@ -133,6 +149,7 @@ SENTRY_TRACES_SAMPLE_RATE = getattr(configuration, 'SENTRY_TRACES_SAMPLE_RATE',
SENTRY_TAGS = getattr(configuration, 'SENTRY_TAGS', {})
SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None)
SESSION_COOKIE_NAME = getattr(configuration, 'SESSION_COOKIE_NAME', 'sessionid')
SESSION_COOKIE_SECURE = getattr(configuration, 'SESSION_COOKIE_SECURE', False)
SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d')
SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s')
@@ -165,15 +182,16 @@ if RELEASE_CHECK_URL:
# Database
#
# Only PostgreSQL is supported
if METRICS_ENABLED:
DATABASE.update({
'ENGINE': 'django_prometheus.db.backends.postgresql'
})
else:
DATABASE.update({
'ENGINE': 'django.db.backends.postgresql'
})
if 'ENGINE' not in DATABASE:
# Only PostgreSQL is supported
if METRICS_ENABLED:
DATABASE.update({
'ENGINE': 'django_prometheus.db.backends.postgresql'
})
else:
DATABASE.update({
'ENGINE': 'django.db.backends.postgresql'
})
DATABASES = {
'default': DATABASE,
@@ -493,6 +511,24 @@ if SENTRY_ENABLED:
sentry_sdk.set_tag('netbox.deployment_id', DEPLOYMENT_ID)
#
# Census collection
#
CENSUS_URL = 'https://census.netbox.dev/api/v1/'
CENSUS_PARAMS = {
'version': VERSION,
'python_version': sys.version.split()[0],
'deployment_id': DEPLOYMENT_ID,
}
if CENSUS_REPORTING_ENABLED and not DEBUG and 'test' not in sys.argv:
try:
# Report anonymous census data
requests.get(f'{CENSUS_URL}?{urlencode(CENSUS_PARAMS)}', timeout=3, proxies=HTTP_PROXIES)
except requests.exceptions.RequestException:
pass
#
# Django social auth
#
@@ -581,14 +617,18 @@ REST_FRAMEWORK = {
#
SPECTACULAR_SETTINGS = {
"TITLE": "NetBox API",
"DESCRIPTION": "API to access NetBox",
"LICENSE": {"name": "Apache v2 License"},
"VERSION": VERSION,
'TITLE': 'NetBox REST API',
'LICENSE': {'name': 'Apache v2 License'},
'VERSION': VERSION,
'COMPONENT_SPLIT_REQUEST': True,
'REDOC_DIST': 'SIDECAR',
'SERVERS': [{
'url': BASE_PATH,
'description': 'NetBox',
}],
'SWAGGER_UI_DIST': 'SIDECAR',
'SWAGGER_UI_FAVICON_HREF': 'SIDECAR',
'REDOC_DIST': 'SIDECAR',
'POSTPROCESSING_HOOKS': [],
}
#

View File

@@ -215,10 +215,12 @@ class NetBoxTable(BaseTable):
class SearchTable(tables.Table):
object_type = columns.ContentTypeColumn(
verbose_name=_('Type')
verbose_name=_('Type'),
order_by="object___meta__verbose_name",
)
object = tables.Column(
linkify=True
linkify=True,
order_by=('name', )
)
field = tables.Column()
value = tables.Column()

View File

@@ -151,6 +151,29 @@ class ExternalAuthenticationTestCase(TestCase):
self.assertEqual(int(self.client.session.get(
'_auth_user_id')), self.user.pk, msg='Authentication failed')
@override_settings(
REMOTE_AUTH_ENABLED=True,
LOGIN_REQUIRED=True
)
def test_remote_auth_user_profile(self):
"""
Test remote authentication with user profile details.
"""
headers = {
'HTTP_REMOTE_USER': 'remoteuser1',
'HTTP_REMOTE_USER_FIRST_NAME': 'John',
'HTTP_REMOTE_USER_LAST_NAME': 'Smith',
'HTTP_REMOTE_USER_EMAIL': 'johnsmith@example.com',
}
response = self.client.get(reverse('home'), follow=True, **headers)
self.assertEqual(response.status_code, 200)
self.user = User.objects.get(username='remoteuser1')
self.assertEqual(self.user.first_name, "John", msg='User first name was not updated')
self.assertEqual(self.user.last_name, "Smith", msg='User last name was not updated')
self.assertEqual(self.user.email, "johnsmith@example.com", msg='User email was not updated')
@override_settings(
REMOTE_AUTH_ENABLED=True,
REMOTE_AUTH_AUTO_CREATE_USER=True,

View File

@@ -20,7 +20,7 @@ from extras.signals import clear_webhooks
from utilities.error_handlers import handle_protectederror
from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation
from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fields
from utilities.forms.bulk_import import ImportForm
from utilities.forms.bulk_import import BulkImportForm
from utilities.htmx import is_embedded, is_htmx
from utilities.permissions import get_permission_for_model
from utilities.utils import get_viewname
@@ -425,7 +425,7 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
#
def get(self, request):
form = ImportForm()
form = BulkImportForm()
return render(request, self.template_name, {
'model': self.model_form._meta.model,
@@ -438,7 +438,7 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
def post(self, request):
logger = logging.getLogger('netbox.views.BulkImportView')
model = self.model_form._meta.model
form = ImportForm(request.POST, request.FILES)
form = BulkImportForm(request.POST, request.FILES)
if form.is_valid():
logger.debug("Import form validation was successful")

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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