Compare commits

...

52 Commits

Author SHA1 Message Date
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
108 changed files with 792 additions and 348 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.0
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.0
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

@@ -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

@@ -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

@@ -144,8 +144,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

@@ -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) |

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

@@ -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

@@ -28,8 +28,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,15 @@
# NetBox v3.5
## v3.5-beta2 (2023-04-18)
## 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 +29,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 +72,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

@@ -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

@@ -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

@@ -10,8 +10,10 @@ from drf_spectacular.plumbing import (
ComponentRegistry,
ResolvedComponent,
build_basic_type,
build_choice_field,
build_media_type_object,
build_object_type,
get_doc,
is_serializer,
)
from drf_spectacular.types import OpenApiTypes
@@ -38,7 +40,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 +152,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 +171,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 +233,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

@@ -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

@@ -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):

View File

@@ -12,7 +12,7 @@ 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
__all__ = (
'CableBulkEditForm',
@@ -470,6 +470,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 +485,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):
@@ -1169,8 +1173,9 @@ class InterfaceBulkEditForm(
)
speed = forms.IntegerField(
required=False,
widget=SelectSpeedWidget(),
label=_('Speed')
widget=NumberWithOptions(
options=InterfaceSpeedChoices
)
)
mgmt_only = forms.NullBooleanField(
required=False,
@@ -1305,6 +1310,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 +1326,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__ = (
@@ -1154,8 +1154,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

@@ -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

@@ -133,11 +133,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',
)

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

@@ -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

@@ -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

@@ -229,7 +229,7 @@ class ObjectListWidget(DashboardWidget):
htmx_url = None
if parameters := self.config.get('url_params'):
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, {

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

@@ -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

@@ -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

@@ -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

@@ -193,6 +193,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

@@ -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.0'
# 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')
@@ -493,6 +510,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 +616,16 @@ REST_FRAMEWORK = {
#
SPECTACULAR_SETTINGS = {
"TITLE": "NetBox API",
"DESCRIPTION": "API to access NetBox",
"LICENSE": {"name": "Apache v2 License"},
"VERSION": VERSION,
'TITLE': 'NetBox API',
'DESCRIPTION': 'API to access NetBox',
'LICENSE': {'name': 'Apache v2 License'},
'VERSION': VERSION,
'COMPONENT_SPLIT_REQUEST': True,
'REDOC_DIST': 'SIDECAR',
'SERVERS': [{'url': f'/{BASE_PATH}api'}],
'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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,7 @@ import { initReslug } from './reslug';
import { initSelectAll } from './selectAll';
import { initSelectMultiple } from './selectMultiple';
import { initMarkdownPreviews } from './markdownPreview';
import { initSecretToggle } from './secretToggle';
export function initButtons(): void {
for (const func of [
@@ -15,6 +16,7 @@ export function initButtons(): void {
initSelectMultiple,
initMoveButtons,
initMarkdownPreviews,
initSecretToggle,
]) {
func();
}

View File

@@ -0,0 +1,77 @@
import { secretState } from '../stores';
import { getElement, getElements, isTruthy } from '../util';
import type { StateManager } from '../state';
type SecretState = { hidden: boolean };
/**
* Change toggle button's text and attribute to reflect the current state.
*
* @param hidden `true` if the current state is hidden, `false` otherwise.
* @param button Toggle element.
*/
function toggleSecretButton(hidden: boolean, button: HTMLButtonElement): void {
button.setAttribute('data-secret-visibility', hidden ? 'hidden' : 'shown');
button.innerText = hidden ? 'Show Secret' : 'Hide Secret';
}
/**
* Show secret.
*/
function showSecret(): void {
const secret = getElement('secret');
if (isTruthy(secret)) {
const value = secret.getAttribute('data-secret');
if (isTruthy(value)) {
secret.innerText = value;
}
}
}
/**
* Hide secret.
*/
function hideSecret(): void {
const secret = getElement('secret');
if (isTruthy(secret)) {
const value = secret.getAttribute('data-secret');
if (isTruthy(value)) {
secret.innerText = '••••••••';
}
}
}
/**
* Update secret state and visualization when the button is clicked.
*
* @param state State instance.
* @param button Toggle element.
*/
function handleSecretToggle(state: StateManager<SecretState>, button: HTMLButtonElement): void {
state.set('hidden', !state.get('hidden'));
const hidden = state.get('hidden');
if (hidden) {
hideSecret();
} else {
showSecret();
}
toggleSecretButton(hidden, button);
}
/**
* Initialize secret toggle button.
*/
export function initSecretToggle(): void {
hideSecret();
for (const button of getElements<HTMLButtonElement>('button.toggle-secret')) {
button.addEventListener(
'click',
event => {
handleSecretToggle(secretState, event.currentTarget as HTMLButtonElement);
},
false,
);
}
}

View File

@@ -4,7 +4,7 @@ import { getElements } from '../util';
* Set the value of the number input field based on the selection of the dropdown.
*/
export function initSpeedSelector(): void {
for (const element of getElements<HTMLAnchorElement>('a.set_speed')) {
for (const element of getElements<HTMLAnchorElement>('a.set_field_value')) {
if (element !== null) {
function handleClick(event: Event) {
// Don't reload the page (due to href="#").

View File

@@ -1,3 +1,4 @@
export * from './objectDepth';
export * from './rackImages';
export * from './previousPkCheck';
export * from './secret';

View File

@@ -0,0 +1,6 @@
import { createState } from '../state';
export const secretState = createState<{ hidden: boolean }>(
{ hidden: true },
{ persist: true, key: 'netbox-secret' },
);

View File

@@ -30,7 +30,14 @@
</td>
</tr>
<tr>
<th scope="row">Account</th>
<th scope="row">
Account <i
class="mdi mdi-alert-box text-warning"
data-bs-toggle="tooltip"
data-bs-placement="right"
title="This field has been deprecated, and will be removed in NetBox v3.5."
></i>
</th>
<td>{{ object.account|placeholder }}</td>
</tr>
<tr>

View File

@@ -43,10 +43,36 @@
<th scope="row">Config Template</th>
<td>{{ object.config_template|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">
NAPALM Driver
<i
class="mdi mdi-alert-box text-warning"
data-bs-toggle="tooltip"
data-bs-placement="right"
title="This field has been deprecated, and will be removed in NetBox v3.6."
></i>
</th>
<td>{{ object.napalm_driver|placeholder }}</td>
</tr>
</table>
</div>
</div>
{% include 'inc/panels/tags.html' %}
<div class="card">
<h5 class="card-header">
NAPALM Arguments
<i
class="mdi mdi-alert-box text-warning"
data-bs-toggle="tooltip"
data-bs-placement="right"
title="This field has been deprecated, and will be removed in NetBox v3.6."
></i>
</h5>
<div class="card-body">
<pre>{{ object.napalm_args|json }}</pre>
</div>
</div>
{% plugin_left_page object %}
</div>
<div class="col col-md-6">

View File

@@ -14,10 +14,29 @@
{% endblock %}
{% block extra_controls %}
<a {% if prev_rack %}href="{% url 'dcim:rack' pk=prev_rack.pk %}{% endif %}" class="btn btn-sm btn-primary{% if not prev_rack %} disabled{% endif %}">
<i class="mdi mdi-chevron-left" aria-hidden="true"></i> Previous
</a>
<a {% if next_rack %}href="{% url 'dcim:rack' pk=next_rack.pk %}{% endif %}" class="btn btn-sm btn-primary{% if not next_rack %} disabled{% endif %}">
<i class="mdi mdi-chevron-right" aria-hidden="true"></i> Next
</a>
<div class="btn-group" role="group" aria-label="RackNavigation">
{% if prev_rack %}
<a href="{{ prev_rack.get_absolute_url }}" class="btn btn-sm btn-primary">
<i class="mdi mdi-chevron-left" aria-hidden="true"></i> {{ prev_rack }}
</a>
{% endif %}
{% if peer_racks %}
<div class="btn-group" role="group">
<button class="btn btn-sm btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">{{ object }}</button>
<ul class="dropdown-menu">
{% for peer_rack in peer_racks %}
<li><a class="dropdown-item{% if peer_rack.pk == object.pk %} active{% endif %}" href="{{ peer_rack.get_absolute_url }}">{{ peer_rack }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if next_rack %}
<a href="{{ next_rack.get_absolute_url }}" class="btn btn-sm btn-primary">
{{ next_rack }} <i class="mdi mdi-chevron-right" aria-hidden="true"></i>
</a>
{% endif %}
</div>
{% endblock %}

View File

@@ -14,7 +14,12 @@
</tr>
<tr>
<th scope="row">PSK</th>
<td class="font-monospace">{{ object.auth_psk|placeholder }}</td>
<td>
<span id="secret" class="font-monospace" data-secret="{{ object.auth_psk }}">{{ object.auth_psk|placeholder }}</span>
{% if object.auth_psk %}
<button type="button" class="btn btn-sm btn-primary toggle-secret float-end" data-bs-toggle="button">Show Secret</button>
{% endif %}
</td>
</tr>
</table>
</div>

View File

@@ -6,6 +6,7 @@ from django.utils.html import mark_safe
from django.utils.translation import gettext as _
from ipam.formfields import IPNetworkFormField
from ipam.validators import prefix_validator
from netbox.preferences import PREFERENCES
from utilities.forms import BootstrapMixin
from utilities.forms.widgets import DateTimePicker
@@ -105,7 +106,7 @@ class TokenForm(BootstrapMixin, forms.ModelForm):
help_text=_("If no key is provided, one will be generated automatically.")
)
allowed_ips = SimpleArrayField(
base_field=IPNetworkFormField(),
base_field=IPNetworkFormField(validators=[prefix_validator]),
required=False,
label=_('Allowed IPs'),
help_text=_('Allowed IPv4/IPv6 networks from where the token can be used. Leave blank for no restrictions. '

View File

@@ -6,14 +6,14 @@ import yaml
from django import forms
from django.utils.translation import gettext as _
from extras.forms.mixins import SyncedDataMixin
from core.forms.mixins import SyncedDataMixin
from utilities.choices import ImportFormatChoices
from utilities.forms.utils import parse_csv
from .mixins import BootstrapMixin
from ..choices import ImportMethodChoices
from .forms import BootstrapMixin
class ImportForm(BootstrapMixin, SyncedDataMixin, forms.Form):
class BulkImportForm(BootstrapMixin, SyncedDataMixin, forms.Form):
import_method = forms.ChoiceField(
choices=ImportMethodChoices,
required=False

View File

@@ -3,6 +3,7 @@ from django import forms
__all__ = (
'ClearableFileInput',
'MarkdownWidget',
'NumberWithOptions',
'SlugWidget',
)
@@ -21,6 +22,22 @@ class MarkdownWidget(forms.Textarea):
template_name = 'widgets/markdown_input.html'
class NumberWithOptions(forms.NumberInput):
"""
Number field with a dropdown pre-populated with common values for convenience.
"""
template_name = 'widgets/number_with_options.html'
def __init__(self, options, attrs=None):
self.options = options
super().__init__(attrs)
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
context['widget']['options'] = self.options
return context
class SlugWidget(forms.TextInput):
"""
Subclass TextInput and add a slug regeneration button next to the form field.

View File

@@ -7,8 +7,6 @@ __all__ = (
'BulkEditNullBooleanSelect',
'ColorSelect',
'HTMXSelect',
'SelectDurationWidget',
'SelectSpeedWidget',
'SelectWithPK',
)
@@ -63,17 +61,3 @@ class SelectWithPK(forms.Select):
Include the primary key of each option in the option label (e.g. "Router7 (4721)").
"""
option_template_name = 'widgets/select_option_with_pk.html'
class SelectDurationWidget(forms.NumberInput):
"""
Dropdown to select one of several common options for a time duration (in minutes).
"""
template_name = 'widgets/select_duration.html'
class SelectSpeedWidget(forms.NumberInput):
"""
Speed field with dropdown selections for convenience.
"""
template_name = 'widgets/select_speed.html'

View File

@@ -0,0 +1,11 @@
<div class="input-group">
{% include 'django/forms/widgets/number.html' %}
<button type="button" class="btn btn-outline-dark border-input dropdown-toggle" data-bs-toggle="dropdown"></button>
<ul class="dropdown-menu dropdown-menu-end">
{% for value, label in widget.options %}
<li>
<a href="#" target="id_{{ widget.name }}" data="{{ value }}" class="set_field_value dropdown-item">{{ label }}</a>
</li>
{% endfor %}
</ul>
</div>

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