mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-27 04:08:14 +01:00
Compare commits
99 Commits
v3.5-beta2
...
v3.5.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f184f2435 | ||
|
|
56a4d0333e | ||
|
|
6794742213 | ||
|
|
a29a07ed26 | ||
|
|
42c80f69e6 | ||
|
|
ca0e7be637 | ||
|
|
42346702a1 | ||
|
|
9909213c0d | ||
|
|
7a38f601de | ||
|
|
abdcfdecf5 | ||
|
|
9d62174e1e | ||
|
|
a96b76a8d1 | ||
|
|
ab69faab87 | ||
|
|
f3826e6235 | ||
|
|
3eba65b5c2 | ||
|
|
683ef30af4 | ||
|
|
46914d9479 | ||
|
|
ea8a0135ad | ||
|
|
25142e037a | ||
|
|
93b912c2da | ||
|
|
4df517e4da | ||
|
|
2c756873aa | ||
|
|
01fa6e28cd | ||
|
|
5036020dc0 | ||
|
|
78ec3a6411 | ||
|
|
24650d9118 | ||
|
|
b14a514b47 | ||
|
|
7aa8434575 | ||
|
|
fbcf4c268b | ||
|
|
a566a56a64 | ||
|
|
cc3b95bdb0 | ||
|
|
2099cd0fdc | ||
|
|
3bdbf67b8f | ||
|
|
20f0464824 | ||
|
|
1952707702 | ||
|
|
9319cffb1c | ||
|
|
261f5e4995 | ||
|
|
8cede0daf8 | ||
|
|
139ef7ef4c | ||
|
|
47f3023401 | ||
|
|
a0f0b29432 | ||
|
|
f86f4f9257 | ||
|
|
3fc7c0edc7 | ||
|
|
528fb21a7e | ||
|
|
6206d226ae | ||
|
|
aabaeec1d7 | ||
|
|
25dc7e234d | ||
|
|
eac7d01977 | ||
|
|
a5bc9d4a2d | ||
|
|
b3efb14176 | ||
|
|
a2f4fce5b3 | ||
|
|
6109bef700 | ||
|
|
72767fb5b7 | ||
|
|
84089ab8c5 | ||
|
|
9a788349a9 | ||
|
|
b7140a0e4a | ||
|
|
d39c796828 | ||
|
|
4a92f6867a | ||
|
|
4355085124 | ||
|
|
5d4ef5e9e5 | ||
|
|
f49e4ee512 | ||
|
|
f867cb3ae0 | ||
|
|
a49fdad5e1 | ||
|
|
1ad029712e | ||
|
|
d87235af2f | ||
|
|
99af126fac | ||
|
|
83cea218b5 | ||
|
|
9eb38ab7b2 | ||
|
|
d3206d9bf9 | ||
|
|
adb9673f09 | ||
|
|
b693123f6e | ||
|
|
e7663b7e39 | ||
|
|
053be952ba | ||
|
|
390619ca99 | ||
|
|
b1130ff9b6 | ||
|
|
89fa546a14 | ||
|
|
c8988bac8a | ||
|
|
55385dd0db | ||
|
|
38a0ed5e24 | ||
|
|
15d80f4e1b | ||
|
|
2fe5592c3c | ||
|
|
8cf0a79dee | ||
|
|
183c5ca667 | ||
|
|
d5c4b1e27c | ||
|
|
2fcdc0ae6a | ||
|
|
12bb0ec1fe | ||
|
|
8b7ee0a0db | ||
|
|
274cd5d56c | ||
|
|
ab3531558a | ||
|
|
dda56f21f3 | ||
|
|
31c909c368 | ||
|
|
164b2a5016 | ||
|
|
7b374e4cf6 | ||
|
|
13625325f5 | ||
|
|
b84ac184c2 | ||
|
|
d5d2431cbd | ||
|
|
c08c7dda50 | ||
|
|
b807198e6d | ||
|
|
9caa7f6b7c |
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -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
24
.github/ISSUE_TEMPLATE/deprecation.yaml
vendored
Normal 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
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@@ -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
|
||||
|
||||
11
README.md
11
README.md
@@ -29,9 +29,18 @@ as the cornerstone for network automation in thousands of organizations.
|
||||
|
||||
## Getting Started
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/netbox-community/netbox)
|
||||
|
||||
[](https://github.com/netbox-community/netbox-docker)
|
||||
|
||||
[](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
|
||||
|
||||
@@ -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
|
||||
|
||||
17
contrib/netbox-housekeeping.service
Normal file
17
contrib/netbox-housekeeping.service
Normal 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
|
||||
13
contrib/netbox-housekeeping.timer
Normal file
13
contrib/netbox-housekeeping.timer
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -638,7 +638,7 @@ $ curl -X POST \
|
||||
https://netbox/api/users/tokens/provision/ \
|
||||
--data '{
|
||||
"username": "hankhill",
|
||||
"password": "I<3C3H8",
|
||||
"password": "I<3C3H8"
|
||||
}'
|
||||
```
|
||||
|
||||
|
||||
@@ -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 |
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)'),
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
),
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
25
netbox/core/forms/mixins.py
Normal file
25
netbox/core/forms/mixins.py
Normal 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',
|
||||
}
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')),
|
||||
|
||||
@@ -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]):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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."
|
||||
})
|
||||
|
||||
@@ -172,6 +172,7 @@ class PlatformIndex(SearchIndex):
|
||||
fields = (
|
||||
('name', 100),
|
||||
('slug', 110),
|
||||
('napalm_driver', 300),
|
||||
('description', 500),
|
||||
)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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]}
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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})
|
||||
|
||||
|
||||
@@ -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
|
||||
#
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
|
||||
@@ -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')),
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)")
|
||||
)
|
||||
|
||||
|
||||
@@ -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)")
|
||||
)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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', [])
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
32
netbox/ipam/api/field_serializers.py
Normal file
32
netbox/ipam/api/field_serializers.py
Normal 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)
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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=[]),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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': [],
|
||||
}
|
||||
|
||||
#
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
7
netbox/project-static/dist/config.js
vendored
7
netbox/project-static/dist/config.js
vendored
File diff suppressed because one or more lines are too long
6
netbox/project-static/dist/config.js.map
vendored
6
netbox/project-static/dist/config.js.map
vendored
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
Reference in New Issue
Block a user