Merge pull request #9253 from netbox-community/develop

Release v3.2.2
This commit is contained in:
Jeremy Stretch 2022-04-28 15:00:11 -04:00 committed by GitHub
commit 50b6ded6f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
81 changed files with 433 additions and 123 deletions

View File

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

View File

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

View File

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

View File

@ -99,7 +99,7 @@ appropriate labels will be applied for categorization.
## Submitting Pull Requests ## Submitting Pull Requests
* If you're interested in contributing to NetBox, be sure to check out our * If you're interested in contributing to NetBox, be sure to check out our
[getting started](https://netbox.readthedocs.io/en/stable/development/getting-started/) [getting started](https://docs.netbox.dev/en/stable/development/getting-started/)
documentation for tips on setting up your development environment. documentation for tips on setting up your development environment.
* Be sure to open an issue **before** starting work on a pull request, and * Be sure to open an issue **before** starting work on a pull request, and
@ -171,7 +171,7 @@ an effort to circumvent the bot: Doing so will not remove the stale label.
the understanding that all contributions are submitted under the Apache 2.0 the understanding that all contributions are submitted under the Apache 2.0
license and that your employer may not make claim to any contributions. license and that your employer may not make claim to any contributions.
Contributions include code work, issue management, and community support. All Contributions include code work, issue management, and community support. All
development must be in accordance with our [development guidance](https://netbox.readthedocs.io/en/stable/development/). development must be in accordance with our [development guidance](https://docs.netbox.dev/en/stable/development/).
* Maintainers are expected to attend (where feasible) our biweekly ~30-minute * Maintainers are expected to attend (where feasible) our biweekly ~30-minute
sync to review agenda items. This meeting provides opportunity to present and sync to review agenda items. This meeting provides opportunity to present and

View File

@ -49,7 +49,7 @@ NetBox runs as a web application atop the [Django](https://www.djangoproject.com
Python framework with a [PostgreSQL](https://www.postgresql.org/) database. For a Python framework with a [PostgreSQL](https://www.postgresql.org/) database. For a
complete list of requirements, see `requirements.txt`. The code is available [on GitHub](https://github.com/netbox-community/netbox). complete list of requirements, see `requirements.txt`. The code is available [on GitHub](https://github.com/netbox-community/netbox).
The complete documentation for NetBox can be found at [Read the Docs](https://netbox.readthedocs.io/en/stable/). A public demo instance is available at https://demo.netbox.dev. The complete documentation for NetBox can be found at [docs.netbox.dev](https://docs.netbox.dev/). A public demo instance is available at https://demo.netbox.dev.
<div align="center"> <div align="center">
<h4>Thank you to our sponsors!</h4> <h4>Thank you to our sponsors!</h4>
@ -71,7 +71,7 @@ The complete documentation for NetBox can be found at [Read the Docs](https://ne
### Installation ### Installation
Please see [the documentation](https://netbox.readthedocs.io/en/stable/) for Please see [the documentation](https://docs.netbox.dev/) for
instructions on installing NetBox. To upgrade NetBox, please download the instructions on installing NetBox. To upgrade NetBox, please download the
[latest release](https://github.com/netbox-community/netbox/releases) and [latest release](https://github.com/netbox-community/netbox/releases) and
run `upgrade.sh`. run `upgrade.sh`.

View File

@ -68,8 +68,7 @@ gunicorn
# Platform-agnostic template rendering engine # Platform-agnostic template rendering engine
# https://github.com/pallets/jinja # https://github.com/pallets/jinja
# Pin to v3.0 for mkdocstrings Jinja2
Jinja2<3.1
# Simple markup language for rendering HTML # Simple markup language for rendering HTML
# https://github.com/Python-Markdown/markdown # https://github.com/Python-Markdown/markdown
@ -85,7 +84,7 @@ mkdocs-material
# Introspection for embedded code # Introspection for embedded code
# https://github.com/mkdocstrings/mkdocstrings # https://github.com/mkdocstrings/mkdocstrings
mkdocstrings<=0.17.0 mkdocstrings[python-legacy]
# Library for manipulating IP prefixes and addresses # Library for manipulating IP prefixes and addresses
# https://github.com/netaddr/netaddr # https://github.com/netaddr/netaddr

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,32 @@
# NetBox v3.2 # NetBox v3.2
## v3.2.2 (2022-04-28)
### Enhancements
* [#9060](https://github.com/netbox-community/netbox/issues/9060) - Add device type filters for device bays, module bays, and inventory items
* [#9152](https://github.com/netbox-community/netbox/issues/9152) - Annotate related object type under custom field view
* [#9192](https://github.com/netbox-community/netbox/issues/9192) - Add Ubiquiti SmartPower connector type
* [#9214](https://github.com/netbox-community/netbox/issues/9214) - Linkify cluster counts in cluster type & group tables
### Bug Fixes
* [#4264](https://github.com/netbox-community/netbox/issues/4264) - Treat 0th IP as unusable for IPv6 prefixes (excluding /127s)
* [#8941](https://github.com/netbox-community/netbox/issues/8941) - Fix dynamic dropdown behavior when browser is zoomed
* [#8959](https://github.com/netbox-community/netbox/issues/8959) - Prevent exception when refreshing scripts list (avoid race condition)
* [#9132](https://github.com/netbox-community/netbox/issues/9132) - Limit location options by selected site when creating a wireless link
* [#9133](https://github.com/netbox-community/netbox/issues/9133) - Upgrade script should require Python 3.8 or later
* [#9138](https://github.com/netbox-community/netbox/issues/9138) - Avoid inadvertent form submission when utilizing quick search field on object lists
* [#9151](https://github.com/netbox-community/netbox/issues/9151) - Child prefix counts not annotated on aggregates list under RIR view
* [#9156](https://github.com/netbox-community/netbox/issues/9156) - Fix loading UserConfig data from fixtures
* [#9158](https://github.com/netbox-community/netbox/issues/9158) - Do not list tags field for CSV forms which do not support tag assignment
* [#9194](https://github.com/netbox-community/netbox/issues/9194) - Support position assignment when add module bays to multiple devices
* [#9206](https://github.com/netbox-community/netbox/issues/9206) - Show header for comments field under module & module type creation views
* [#9222](https://github.com/netbox-community/netbox/issues/9222) - Fix circuit ID display under cable view
* [#9227](https://github.com/netbox-community/netbox/issues/9227) - Fix related object assignment when recording change record for interfaces
---
## v3.2.1 (2022-04-14) ## v3.2.1 (2022-04-14)
### Enhancements ### Enhancements

View File

@ -1,6 +1,6 @@
site_name: NetBox Documentation site_name: NetBox Documentation
site_dir: netbox/project-static/docs site_dir: netbox/project-static/docs
site_url: https://netbox.readthedocs.io/ site_url: https://docs.netbox.dev/
repo_name: netbox-community/netbox repo_name: netbox-community/netbox
repo_url: https://github.com/netbox-community/netbox repo_url: https://github.com/netbox-community/netbox
theme: theme:
@ -19,6 +19,7 @@ theme:
icon: material/lightbulb icon: material/lightbulb
name: Switch to Light Mode name: Switch to Light Mode
plugins: plugins:
- search
- mkdocstrings: - mkdocstrings:
handlers: handlers:
python: python:
@ -117,7 +118,10 @@ nav:
- GraphQL API: 'plugins/development/graphql-api.md' - GraphQL API: 'plugins/development/graphql-api.md'
- Background Tasks: 'plugins/development/background-tasks.md' - Background Tasks: 'plugins/development/background-tasks.md'
- Administration: - Administration:
- Authentication: 'administration/authentication.md' - Authentication:
- Overview: 'administration/authentication/overview.md'
- Microsoft Azure AD: 'administration/authentication/microsoft-azure-ad.md'
- Okta: 'administration/authentication/okta.md'
- Permissions: 'administration/permissions.md' - Permissions: 'administration/permissions.md'
- Housekeeping: 'administration/housekeeping.md' - Housekeeping: 'administration/housekeeping.md'
- Replicating NetBox: 'administration/replicating-netbox.md' - Replicating NetBox: 'administration/replicating-netbox.md'

View File

@ -349,6 +349,7 @@ class PowerPortTypeChoices(ChoiceSet):
TYPE_NEUTRIK_POWERCON_32A = 'neutrik-powercon-32' TYPE_NEUTRIK_POWERCON_32A = 'neutrik-powercon-32'
TYPE_NEUTRIK_POWERCON_TRUE1 = 'neutrik-powercon-true1' TYPE_NEUTRIK_POWERCON_TRUE1 = 'neutrik-powercon-true1'
TYPE_NEUTRIK_POWERCON_TRUE1_TOP = 'neutrik-powercon-true1-top' TYPE_NEUTRIK_POWERCON_TRUE1_TOP = 'neutrik-powercon-true1-top'
TYPE_UBIQUITI_SMARTPOWER = 'ubiquiti-smartpower'
# Other # Other
TYPE_HARDWIRED = 'hardwired' TYPE_HARDWIRED = 'hardwired'
@ -464,6 +465,7 @@ class PowerPortTypeChoices(ChoiceSet):
(TYPE_NEUTRIK_POWERCON_32A, 'Neutrik powerCON (32A)'), (TYPE_NEUTRIK_POWERCON_32A, 'Neutrik powerCON (32A)'),
(TYPE_NEUTRIK_POWERCON_TRUE1, 'Neutrik powerCON TRUE1'), (TYPE_NEUTRIK_POWERCON_TRUE1, 'Neutrik powerCON TRUE1'),
(TYPE_NEUTRIK_POWERCON_TRUE1_TOP, 'Neutrik powerCON TRUE1 TOP'), (TYPE_NEUTRIK_POWERCON_TRUE1_TOP, 'Neutrik powerCON TRUE1 TOP'),
(TYPE_UBIQUITI_SMARTPOWER, 'Ubiquiti SmartPower'),
)), )),
('Other', ( ('Other', (
(TYPE_HARDWIRED, 'Hardwired'), (TYPE_HARDWIRED, 'Hardwired'),

View File

@ -435,6 +435,10 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
method='_device_bays', method='_device_bays',
label='Has device bays', label='Has device bays',
) )
inventory_items = django_filters.BooleanFilter(
method='_inventory_items',
label='Has inventory items',
)
class Meta: class Meta:
model = DeviceType model = DeviceType
@ -479,6 +483,9 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
def _device_bays(self, queryset, name, value): def _device_bays(self, queryset, name, value):
return queryset.exclude(devicebaytemplates__isnull=value) return queryset.exclude(devicebaytemplates__isnull=value)
def _inventory_items(self, queryset, name, value):
return queryset.exclude(inventoryitemtemplates__isnull=value)
class ModuleTypeFilterSet(NetBoxModelFilterSet): class ModuleTypeFilterSet(NetBoxModelFilterSet):
manufacturer_id = django_filters.ModelMultipleChoiceFilter( manufacturer_id = django_filters.ModelMultipleChoiceFilter(

View File

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

View File

@ -331,7 +331,7 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
('Hardware', ('manufacturer_id', 'part_number', 'subdevice_role', 'airflow')), ('Hardware', ('manufacturer_id', 'part_number', 'subdevice_role', 'airflow')),
('Components', ( ('Components', (
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
'pass_through_ports', 'pass_through_ports', 'device_bays', 'module_bays', 'inventory_items',
)), )),
) )
manufacturer_id = DynamicModelMultipleChoiceField( manufacturer_id = DynamicModelMultipleChoiceField(
@ -392,6 +392,27 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
device_bays = forms.NullBooleanField(
required=False,
label='Has device bays',
widget=StaticSelect(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
module_bays = forms.NullBooleanField(
required=False,
label='Has module bays',
widget=StaticSelect(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
inventory_items = forms.NullBooleanField(
required=False,
label='Has inventory items',
widget=StaticSelect(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
tag = TagFilterField(model) tag = TagFilterField(model)

View File

@ -385,6 +385,12 @@ class ModuleTypeForm(NetBoxModelForm):
) )
comments = CommentField() comments = CommentField()
fieldsets = (
('Module Type', (
'manufacturer', 'model', 'part_number', 'tags',
)),
)
class Meta: class Meta:
model = ModuleType model = ModuleType
fields = [ fields = [
@ -627,6 +633,15 @@ class ModuleForm(NetBoxModelForm):
help_text="Automatically populate components associated with this module type" help_text="Automatically populate components associated with this module type"
) )
fieldsets = (
('Module', (
'device', 'module_bay', 'manufacturer', 'module_type', 'tags',
)),
('Hardware', (
'serial', 'asset_tag', 'replicate_components',
)),
)
class Meta: class Meta:
model = Module model = Module
fields = [ fields = [

View File

@ -77,7 +77,7 @@ class ComponentModel(NetBoxModel):
def to_objectchange(self, action): def to_objectchange(self, action):
objectchange = super().to_objectchange(action) objectchange = super().to_objectchange(action)
objectchange.related_object = self.device objectchange.related_object = self.device
return super().to_objectchange(action) return objectchange
@property @property
def parent_object(self): def parent_object(self):

View File

@ -698,6 +698,9 @@ class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
DeviceBayTemplate(device_type=device_types[0], name='Device Bay 1'), DeviceBayTemplate(device_type=device_types[0], name='Device Bay 1'),
DeviceBayTemplate(device_type=device_types[1], name='Device Bay 2'), DeviceBayTemplate(device_type=device_types[1], name='Device Bay 2'),
)) ))
# Assigned DeviceType must have parent subdevice_role
inventory_item = InventoryItemTemplate(device_type=device_types[1], name='Inventory Item 1')
inventory_item.save()
def test_model(self): def test_model(self):
params = {'model': ['Model 1', 'Model 2']} params = {'model': ['Model 1', 'Model 2']}
@ -784,6 +787,12 @@ class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'module_bays': 'false'} params = {'module_bays': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_inventory_items(self):
params = {'inventory_items': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
params = {'inventory_items': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests): class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = ModuleType.objects.all() queryset = ModuleType.objects.all()

View File

@ -5,6 +5,7 @@ import os
import pkgutil import pkgutil
import sys import sys
import traceback import traceback
import threading
from collections import OrderedDict from collections import OrderedDict
import yaml import yaml
@ -13,11 +14,9 @@ from django.conf import settings
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.db import transaction from django.db import transaction
from django.utils.functional import classproperty from django.utils.functional import classproperty
from django_rq import job
from extras.api.serializers import ScriptOutputSerializer from extras.api.serializers import ScriptOutputSerializer
from extras.choices import JobResultStatusChoices, LogLevelChoices from extras.choices import JobResultStatusChoices, LogLevelChoices
from extras.models import JobResult
from ipam.formfields import IPAddressFormField, IPNetworkFormField from ipam.formfields import IPAddressFormField, IPNetworkFormField
from ipam.validators import MaxPrefixLengthValidator, MinPrefixLengthValidator, prefix_validator from ipam.validators import MaxPrefixLengthValidator, MinPrefixLengthValidator, prefix_validator
from utilities.exceptions import AbortTransaction from utilities.exceptions import AbortTransaction
@ -42,6 +41,8 @@ __all__ = [
'TextVar', 'TextVar',
] ]
lock = threading.Lock()
# #
# Script variables # Script variables
@ -491,11 +492,14 @@ def get_scripts(use_names=False):
# Iterate through all modules within the scripts path. These are the user-created files in which reports are # Iterate through all modules within the scripts path. These are the user-created files in which reports are
# defined. # defined.
for importer, module_name, _ in pkgutil.iter_modules([settings.SCRIPTS_ROOT]): for importer, module_name, _ in pkgutil.iter_modules([settings.SCRIPTS_ROOT]):
# Remove cached module to ensure consistency with filesystem # Use a lock as removing and loading modules is not thread safe
if module_name in sys.modules: with lock:
del sys.modules[module_name] # Remove cached module to ensure consistency with filesystem
if module_name in sys.modules:
del sys.modules[module_name]
module = importer.find_module(module_name).load_module(module_name)
module = importer.find_module(module_name).load_module(module_name)
if use_names and hasattr(module, 'name'): if use_names and hasattr(module, 'name'):
module_name = module.name module_name = module.name
module_scripts = OrderedDict() module_scripts = OrderedDict()

View File

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

View File

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

View File

@ -158,8 +158,8 @@ class RIRView(generic.ObjectView):
queryset = RIR.objects.all() queryset = RIR.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
aggregates = Aggregate.objects.restrict(request.user, 'view').filter( aggregates = Aggregate.objects.restrict(request.user, 'view').filter(rir=instance).annotate(
rir=instance child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ())
) )
aggregates_table = tables.AggregateTable(aggregates, exclude=('rir', 'utilization')) aggregates_table = tables.AggregateTable(aggregates, exclude=('rir', 'utilization'))
aggregates_table.configure(request) aggregates_table.configure(request)

View File

@ -39,6 +39,7 @@ AUTH_BACKEND_ATTRS = {
'keycloak': ('Keycloak', None), 'keycloak': ('Keycloak', None),
'microsoft-graph': ('Microsoft Graph', 'microsoft'), 'microsoft-graph': ('Microsoft Graph', 'microsoft'),
'okta': ('Okta', None), 'okta': ('Okta', None),
'okta-openidconnect': ('Okta (OIDC)', None),
'salesforce-oauth2': ('Salesforce', 'salesforce'), 'salesforce-oauth2': ('Salesforce', 'salesforce'),
} }

View File

@ -61,6 +61,8 @@ class NetBoxModelCSVForm(CSVModelForm, NetBoxModelForm):
""" """
Base form for creating a NetBox objects from CSV data. Used for bulk importing. Base form for creating a NetBox objects from CSV data. Used for bulk importing.
""" """
tags = None # Temporary fix in lieu of tag import support (see #9158)
def _get_form_field(self, customfield): def _get_form_field(self, customfield):
return customfield.to_form_field(for_csv_import=True) return customfield.to_form_field(for_csv_import=True)

View File

@ -26,7 +26,7 @@ django.utils.encoding.force_text = force_str
# Environment setup # Environment setup
# #
VERSION = '3.2.1' VERSION = '3.2.2'
# Hostname # Hostname
HOSTNAME = platform.node() HOSTNAME = platform.node()

Binary file not shown.

Binary file not shown.

View File

@ -570,8 +570,9 @@ export class APISelect {
* additional paginated options. * additional paginated options.
*/ */
private handleScroll(): void { private handleScroll(): void {
// Floor scrollTop as chrome can return fractions on some zoom levels.
const atBottom = const atBottom =
this.slim.slim.list.scrollTop + this.slim.slim.list.offsetHeight === Math.floor(this.slim.slim.list.scrollTop) + this.slim.slim.list.offsetHeight ===
this.slim.slim.list.scrollHeight; this.slim.slim.list.scrollHeight;
if (this.atBottom && !atBottom) { if (this.atBottom && !atBottom) {

View File

@ -4,9 +4,10 @@
{% load static %} {% load static %}
{% block content %} {% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceConsolePortTable_config" %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceConsolePortTable_config" %}
<div class="card"> <div class="card">
<div class="card-body" id="object_list"> <div class="card-body" id="object_list">

View File

@ -4,9 +4,10 @@
{% load static %} {% load static %}
{% block content %} {% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceConsoleServerPortTable_config" %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceConsoleServerPortTable_config" %}
<div class="card"> <div class="card">
<div class="card-body" id="object_list"> <div class="card-body" id="object_list">

View File

@ -4,9 +4,10 @@
{% load static %} {% load static %}
{% block content %} {% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceDeviceBayTable_config" %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceDeviceBayTable_config" %}
<div class="card"> <div class="card">
<div class="card-body" id="object_list"> <div class="card-body" id="object_list">

View File

@ -4,9 +4,10 @@
{% load static %} {% load static %}
{% block content %} {% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceFrontPortTable_config" %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceFrontPortTable_config" %}
<div class="card"> <div class="card">
<div class="card-body" id="object_list"> <div class="card-body" id="object_list">

View File

@ -4,44 +4,46 @@
{% load static %} {% load static %}
{% block content %} {% block content %}
<form method="post"> <div class="row mb-3 justify-content-between">
{% csrf_token %} <div class="col col-12 col-lg-4 my-3 my-lg-0 d-flex noprint table-controls">
<div class="row mb-3 justify-content-between"> <div class="input-group input-group-sm">
<div class="col col-12 col-lg-4 my-3 my-lg-0 d-flex noprint table-controls"> <input
<div class="input-group input-group-sm"> type="text"
<input name="q"
type="text" class="form-control"
name="q" placeholder="Quick search"
class="form-control" hx-get="{{ request.full_path }}"
placeholder="Quick search" hx-target="#object_list"
hx-get="{{ request.full_path }}" hx-trigger="keyup changed delay:500ms"
hx-target="#object_list" />
hx-trigger="keyup changed delay:500ms"
/>
</div>
</div>
<div class="col col-md-3 mb-0 d-flex noprint table-controls">
<div class="input-group input-group-sm justify-content-end">
{% if request.user.is_authenticated %}
<button
type="button"
class="btn btn-sm btn-outline-dark"
data-bs-toggle="modal"
data-bs-target="#DeviceInterfaceTable_config"
title="Configure Table">
<i class="mdi mdi-cog"></i> Configure Table
</button>
{% endif %}
<button class="btn btn-sm btn-outline-dark dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="mdi mdi-eye"></i>
</button>
<ul class="dropdown-menu">
<button type="button" class="dropdown-item toggle-enabled" data-state="show">Hide Enabled</button>
<button type="button" class="dropdown-item toggle-disabled" data-state="show">Hide Disabled</button>
</ul>
</div>
</div> </div>
</div> </div>
<div class="col col-md-3 mb-0 d-flex noprint table-controls">
<div class="input-group input-group-sm justify-content-end">
{% if request.user.is_authenticated %}
<button
type="button"
class="btn btn-sm btn-outline-dark"
data-bs-toggle="modal"
data-bs-target="#DeviceInterfaceTable_config"
title="Configure Table">
<i class="mdi mdi-cog"></i> Configure Table
</button>
{% endif %}
<button class="btn btn-sm btn-outline-dark dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="mdi mdi-eye"></i>
</button>
<ul class="dropdown-menu">
<button type="button" class="dropdown-item toggle-enabled" data-state="show">Hide Enabled</button>
<button type="button" class="dropdown-item toggle-disabled" data-state="show">Hide Disabled</button>
</ul>
</div>
</div>
</div>
<form method="post">
{% csrf_token %}
<div class="card"> <div class="card">
<div class="card-body" id="object_list"> <div class="card-body" id="object_list">

View File

@ -4,9 +4,10 @@
{% load static %} {% load static %}
{% block content %} {% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceInventoryItemTable_config" %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceInventoryItemTable_config" %}
<div class="card"> <div class="card">
<div class="card-body" id="object_list"> <div class="card-body" id="object_list">

View File

@ -4,9 +4,10 @@
{% load static %} {% load static %}
{% block content %} {% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceModuleBayTable_config" %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceModuleBayTable_config" %}
<div class="card"> <div class="card">
<div class="card-body" id="object_list"> <div class="card-body" id="object_list">

View File

@ -4,9 +4,10 @@
{% load static %} {% load static %}
{% block content %} {% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="DevicePowerOutletTable_config" %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{% include 'inc/table_controls_htmx.html' with table_modal="DevicePowerOutletTable_config" %}
<div class="card"> <div class="card">
<div class="card-body" id="object_list"> <div class="card-body" id="object_list">

View File

@ -4,9 +4,10 @@
{% load static %} {% load static %}
{% block content %} {% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="DevicePowerPortTable_config" %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{% include 'inc/table_controls_htmx.html' with table_modal="DevicePowerPortTable_config" %}
<div class="card"> <div class="card">
<div class="card-body" id="object_list"> <div class="card-body" id="object_list">

View File

@ -4,9 +4,10 @@
{% load helpers %} {% load helpers %}
{% block content %} {% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceRearPortTable_config" %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceRearPortTable_config" %}
<div class="card"> <div class="card">
<div class="card-body" id="object_list"> <div class="card-body" id="object_list">

View File

@ -32,7 +32,11 @@
</tr> </tr>
<tr> <tr>
<td>Circuit</td> <td>Circuit</td>
<td>{{ termination.|linkify }} ({{ termination }})</td> <td>{{ termination.circuit|linkify }}</td>
</tr>
<tr>
<td>Termination</td>
<td>{{ termination }}</td>
</tr> </tr>
{% endif %} {% endif %}
</table> </table>

View File

@ -21,7 +21,10 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Type</th> <th scope="row">Type</th>
<td>{{ object.get_type_display }}</td> <td>
{{ object.get_type_display }}
{% if object.object_type %}({{ object.object_type.model|bettertitle }}){% endif %}
</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Description</th> <th scope="row">Description</th>

View File

@ -13,7 +13,7 @@
<small><a href="{{ new_release.url }}">NetBox v{{ new_release.version }}</a> is available.</small> <small><a href="{{ new_release.url }}">NetBox v{{ new_release.version }}</a> is available.</small>
<hr class="my-2" /> <hr class="my-2" />
<small class="mb-0"> <small class="mb-0">
<a href="https://netbox.readthedocs.io/en/stable/installation/upgrading/">Upgrade Instructions</a> <a href="https://docs.netbox.dev/en/stable/installation/upgrading/">Upgrade Instructions</a>
</small> </small>
</div> </div>
</div> </div>

View File

@ -12,9 +12,10 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="PrefixTable_config" %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{% include 'inc/table_controls_htmx.html' with table_modal="PrefixTable_config" %}
<div class="card"> <div class="card">
<div class="card-body" id="object_list"> <div class="card-body" id="object_list">

View File

@ -10,9 +10,10 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="IPAddressTable_config" %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{% include 'inc/table_controls_htmx.html' with table_modal="IPAddressTable_config" %}
<div class="card"> <div class="card">
<div class="card-body" id="object_list"> <div class="card-body" id="object_list">

View File

@ -10,9 +10,10 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="IPAddressTable_config" %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{% include 'inc/table_controls_htmx.html' with table_modal="IPAddressTable_config" %}
<div class="card"> <div class="card">
<div class="card-body" id="object_list"> <div class="card-body" id="object_list">

View File

@ -10,9 +10,10 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="IPRangeTable_config" %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{% include 'inc/table_controls_htmx.html' with table_modal="IPRangeTable_config" %}
<div class="card"> <div class="card">
<div class="card-body" id="object_list"> <div class="card-body" id="object_list">

View File

@ -12,9 +12,10 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="PrefixTable_config" %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{% include 'inc/table_controls_htmx.html' with table_modal="PrefixTable_config" %}
<div class="card"> <div class="card">
<div class="card-body" id="object_list"> <div class="card-body" id="object_list">

View File

@ -2,9 +2,10 @@
{% load helpers %} {% load helpers %}
{% block content %} {% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="VLANDevicesTable_config" %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{% include 'inc/table_controls_htmx.html' with table_modal="VLANDevicesTable_config" %}
<div class="card"> <div class="card">
<div class="card-body" id="object_list"> <div class="card-body" id="object_list">
{% include 'htmx/table.html' %} {% include 'htmx/table.html' %}

View File

@ -2,9 +2,10 @@
{% load helpers %} {% load helpers %}
{% block content %} {% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="VLANVirtualMachinesTable_config" %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{% include 'inc/table_controls_htmx.html' with table_modal="VLANVirtualMachinesTable_config" %}
<div class="card"> <div class="card">
<div class="card-body" id="object_list"> <div class="card-body" id="object_list">
{% include 'htmx/table.html' %} {% include 'htmx/table.html' %}

View File

@ -29,7 +29,7 @@
</li> </li>
<li class="tip"> <li class="tip">
The HTTP service (e.g. nginx or Apache) is configured to serve files from the <code>STATIC_ROOT</code> path. The HTTP service (e.g. nginx or Apache) is configured to serve files from the <code>STATIC_ROOT</code> path.
Refer to <a href="https://netbox.readthedocs.io/en/stable/installation/">the installation Refer to <a href="https://docs.netbox.dev/en/stable/installation/">the installation
documentation</a> for further guidance. documentation</a> for further guidance.
<ul> <ul>
{% if request.user.is_staff or request.user.is_superuser %} {% if request.user.is_staff or request.user.is_superuser %}

View File

@ -3,9 +3,10 @@
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% block content %} {% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceTable_config" %}
<form action="{% url 'virtualization:cluster_remove_devices' pk=object.pk %}" method="post"> <form action="{% url 'virtualization:cluster_remove_devices' pk=object.pk %}" method="post">
{% csrf_token %} {% csrf_token %}
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceTable_config" %}
<div class="card"> <div class="card">
<div class="card-body" id="object_list"> <div class="card-body" id="object_list">
{% include 'htmx/table.html' %} {% include 'htmx/table.html' %}

View File

@ -3,9 +3,10 @@
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% block content %} {% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="VirtualMachineTable_config" %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{% include 'inc/table_controls_htmx.html' with table_modal="VirtualMachineTable_config" %}
<div class="card"> <div class="card">
<div class="card-body" id="object_list"> <div class="card-body" id="object_list">
{% include 'htmx/table.html' %} {% include 'htmx/table.html' %}

View File

@ -3,9 +3,10 @@
{% load helpers %} {% load helpers %}
{% block content %} {% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="VirtualMachineVMInterfaceTable_config" %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{% include 'inc/table_controls_htmx.html' with table_modal="VirtualMachineVMInterfaceTable_config" %}
<div class="card"> <div class="card">
<div class="card-body" id="object_list"> <div class="card-body" id="object_list">

View File

@ -173,11 +173,11 @@ class UserConfig(models.Model):
@receiver(post_save, sender=User) @receiver(post_save, sender=User)
def create_userconfig(instance, created, **kwargs): def create_userconfig(instance, created, raw=False, **kwargs):
""" """
Automatically create a new UserConfig when a new User is created. Automatically create a new UserConfig when a new User is created. Skip this if importing a user from a fixture.
""" """
if created: if created and not raw:
config = get_config() config = get_config()
UserConfig(user=instance, data=config.DEFAULT_USER_PREFERENCES).save() UserConfig(user=instance, data=config.DEFAULT_USER_PREFERENCES).save()

View File

@ -14,7 +14,9 @@ class ClusterTypeTable(NetBoxTable):
name = tables.Column( name = tables.Column(
linkify=True linkify=True
) )
cluster_count = tables.Column( cluster_count = columns.LinkedCountColumn(
viewname='virtualization:cluster_list',
url_params={'type_id': 'pk'},
verbose_name='Clusters' verbose_name='Clusters'
) )
tags = columns.TagColumn( tags = columns.TagColumn(
@ -33,7 +35,9 @@ class ClusterGroupTable(NetBoxTable):
name = tables.Column( name = tables.Column(
linkify=True linkify=True
) )
cluster_count = tables.Column( cluster_count = columns.LinkedCountColumn(
viewname='virtualization:cluster_list',
url_params={'group_id': 'pk'},
verbose_name='Clusters' verbose_name='Clusters'
) )
contacts = tables.ManyToManyColumn( contacts = tables.ManyToManyColumn(

View File

@ -105,6 +105,9 @@ class WirelessLinkForm(NetBoxModelForm):
) )
location_a = DynamicModelChoiceField( location_a = DynamicModelChoiceField(
queryset=Location.objects.all(), queryset=Location.objects.all(),
query_params={
'site_id': '$site_a',
},
required=False, required=False,
label='Location', label='Location',
initial_params={ initial_params={
@ -142,6 +145,9 @@ class WirelessLinkForm(NetBoxModelForm):
) )
location_b = DynamicModelChoiceField( location_b = DynamicModelChoiceField(
queryset=Location.objects.all(), queryset=Location.objects.all(),
query_params={
'site_id': '$site_b',
},
required=False, required=False,
label='Location', label='Location',
initial_params={ initial_params={

View File

@ -15,11 +15,11 @@ djangorestframework==3.13.1
drf-yasg[validation]==1.20.0 drf-yasg[validation]==1.20.0
graphene-django==2.15.0 graphene-django==2.15.0
gunicorn==20.1.0 gunicorn==20.1.0
Jinja2==3.0.3 Jinja2==3.1.2
Markdown==3.3.6 Markdown==3.3.6
markdown-include==0.6.0 markdown-include==0.6.0
mkdocs-material==8.2.9 mkdocs-material==8.2.11
mkdocstrings==0.17.0 mkdocstrings[python-legacy]==0.18.1
netaddr==0.8.0 netaddr==0.8.0
Pillow==9.1.0 Pillow==9.1.0
psycopg2-binary==2.9.3 psycopg2-binary==2.9.3

View File

@ -3,23 +3,23 @@
# its most recent release. # its most recent release.
# This script will invoke Python with the value of the PYTHON environment # This script will invoke Python with the value of the PYTHON environment
# variable (if set), or fall back to "python3". Note that NetBox v3.0+ requires # variable (if set), or fall back to "python3". Note that NetBox v3.2+ requires
# Python 3.7 or later. # Python 3.8 or later.
cd "$(dirname "$0")" cd "$(dirname "$0")"
VIRTUALENV="$(pwd -P)/venv" VIRTUALENV="$(pwd -P)/venv"
PYTHON="${PYTHON:-python3}" PYTHON="${PYTHON:-python3}"
# Validate the minimum required Python version # Validate the minimum required Python version
COMMAND="${PYTHON} -c 'import sys; exit(1 if sys.version_info < (3, 7) else 0)'" COMMAND="${PYTHON} -c 'import sys; exit(1 if sys.version_info < (3, 8) else 0)'"
PYTHON_VERSION=$(eval "${PYTHON} -V") PYTHON_VERSION=$(eval "${PYTHON} -V")
eval $COMMAND || { eval $COMMAND || {
echo "--------------------------------------------------------------------" echo "--------------------------------------------------------------------"
echo "ERROR: Unsupported Python version: ${PYTHON_VERSION}. NetBox requires" echo "ERROR: Unsupported Python version: ${PYTHON_VERSION}. NetBox requires"
echo "Python 3.7 or later. To specify an alternate Python executable, set" echo "Python 3.8 or later. To specify an alternate Python executable, set"
echo "the PYTHON environment variable. For example:" echo "the PYTHON environment variable. For example:"
echo "" echo ""
echo " sudo PYTHON=/usr/bin/python3.7 ./upgrade.sh" echo " sudo PYTHON=/usr/bin/python3.8 ./upgrade.sh"
echo "" echo ""
echo "To show your current Python version: ${PYTHON} -V" echo "To show your current Python version: ${PYTHON} -V"
echo "--------------------------------------------------------------------" echo "--------------------------------------------------------------------"