Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50b6ded6f0 | ||
|
|
5ee3ee6181 | ||
|
|
cd3e901a60 | ||
|
|
314c41f47f | ||
|
|
a1c1532614 | ||
|
|
ffef78d426 | ||
|
|
8153406dd0 | ||
|
|
b5613a2cc6 | ||
|
|
6a225e53f5 | ||
|
|
6b73d22da1 | ||
|
|
8b81c10f84 | ||
|
|
a6a1bec437 | ||
|
|
562d1bfcd0 | ||
|
|
4f86d6a690 | ||
|
|
e63a191373 | ||
|
|
74b5e55643 | ||
|
|
405d0ab972 | ||
|
|
84e4156259 | ||
|
|
50428c3f01 | ||
|
|
a91c46b4c0 | ||
|
|
8315883db9 | ||
|
|
d22f9000d6 | ||
|
|
bb99cee48a | ||
|
|
a3805fe04d | ||
|
|
d4f1cb5d6a | ||
|
|
118bf5152c | ||
|
|
41244dc677 | ||
|
|
671e1aed9f | ||
|
|
1636508a6a | ||
|
|
c21db0ff6a | ||
|
|
a889b3a4be | ||
|
|
10c7fdb618 | ||
|
|
7be0a1a55f | ||
|
|
a98b2fabe0 | ||
|
|
7779b66972 | ||
|
|
7463c40c40 | ||
|
|
996221147e | ||
|
|
7cd9bcd3f5 | ||
|
|
fdc018d809 | ||
|
|
fa5cf665ce | ||
|
|
d6df6b444f | ||
|
|
78836389f0 | ||
|
|
fa4b88a504 | ||
|
|
1a374a1669 | ||
|
|
01ba1b8c03 | ||
|
|
f09a5aacae | ||
|
|
95d084d36d | ||
|
|
d35cd18745 | ||
|
|
4e493d7836 | ||
|
|
68b8cca540 | ||
|
|
c216405a81 | ||
|
|
aa2ec3b9c9 | ||
|
|
f13a00b2dd | ||
|
|
8781d03aa7 | ||
|
|
1266a2f753 | ||
|
|
23d2cf1718 | ||
|
|
d11031c694 | ||
|
|
916e976297 | ||
|
|
27a9313396 | ||
|
|
517d0158b6 | ||
|
|
a9e05aec7c | ||
|
|
9b3e43cb21 | ||
|
|
23fddf74b6 | ||
|
|
9b8de19fe6 | ||
|
|
1d8b8aad3b | ||
|
|
84c30580aa | ||
|
|
a5f25726cd | ||
|
|
7a6e047519 | ||
|
|
1e65ef0c1a | ||
|
|
2269bf0167 | ||
|
|
5526f8e3dc | ||
|
|
2781b8535c | ||
|
|
c3d9910e08 | ||
|
|
b9f6a5625f | ||
|
|
f4e78b0ea6 | ||
|
|
d93e944c07 | ||
|
|
6760533a10 | ||
|
|
85e65edb7d | ||
|
|
523390cd8e | ||
|
|
ea197eff5f | ||
|
|
69b4d0d44b | ||
|
|
36d6ae33d1 | ||
|
|
a3f172fc77 |
4
.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.2.0
|
||||
placeholder: v3.2.2
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
@@ -22,9 +22,9 @@ body:
|
||||
label: Python version
|
||||
description: What version of Python are you currently running?
|
||||
options:
|
||||
- "3.7"
|
||||
- "3.8"
|
||||
- "3.9"
|
||||
- "3.10"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
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.2.0
|
||||
placeholder: v3.2.2
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
@@ -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/).
|
||||
|
||||
@@ -99,7 +99,7 @@ appropriate labels will be applied for categorization.
|
||||
## Submitting Pull Requests
|
||||
|
||||
* If you're interested in contributing to NetBox, be sure to check out our
|
||||
[getting started](https://netbox.readthedocs.io/en/stable/development/getting-started/)
|
||||
[getting started](https://docs.netbox.dev/en/stable/development/getting-started/)
|
||||
documentation for tips on setting up your development environment.
|
||||
|
||||
* Be sure to open an issue **before** starting work on a pull request, and
|
||||
@@ -171,7 +171,7 @@ an effort to circumvent the bot: Doing so will not remove the stale label.
|
||||
the understanding that all contributions are submitted under the Apache 2.0
|
||||
license and that your employer may not make claim to any contributions.
|
||||
Contributions include code work, issue management, and community support. All
|
||||
development must be in accordance with our [development guidance](https://netbox.readthedocs.io/en/stable/development/).
|
||||
development must be in accordance with our [development guidance](https://docs.netbox.dev/en/stable/development/).
|
||||
|
||||
* Maintainers are expected to attend (where feasible) our biweekly ~30-minute
|
||||
sync to review agenda items. This meeting provides opportunity to present and
|
||||
|
||||
@@ -49,7 +49,7 @@ NetBox runs as a web application atop the [Django](https://www.djangoproject.com
|
||||
Python framework with a [PostgreSQL](https://www.postgresql.org/) database. For a
|
||||
complete list of requirements, see `requirements.txt`. The code is available [on GitHub](https://github.com/netbox-community/netbox).
|
||||
|
||||
The complete documentation for NetBox can be found at [Read the Docs](https://netbox.readthedocs.io/en/stable/). A public demo instance is available at https://demo.netbox.dev.
|
||||
The complete documentation for NetBox can be found at [docs.netbox.dev](https://docs.netbox.dev/). A public demo instance is available at https://demo.netbox.dev.
|
||||
|
||||
<div align="center">
|
||||
<h4>Thank you to our sponsors!</h4>
|
||||
@@ -71,7 +71,7 @@ The complete documentation for NetBox can be found at [Read the Docs](https://ne
|
||||
|
||||
### Installation
|
||||
|
||||
Please see [the documentation](https://netbox.readthedocs.io/en/stable/) for
|
||||
Please see [the documentation](https://docs.netbox.dev/) for
|
||||
instructions on installing NetBox. To upgrade NetBox, please download the
|
||||
[latest release](https://github.com/netbox-community/netbox/releases) and
|
||||
run `upgrade.sh`.
|
||||
|
||||
@@ -68,8 +68,7 @@ gunicorn
|
||||
|
||||
# Platform-agnostic template rendering engine
|
||||
# https://github.com/pallets/jinja
|
||||
# Pin to v3.0 for mkdocstrings
|
||||
Jinja2<3.1
|
||||
Jinja2
|
||||
|
||||
# Simple markup language for rendering HTML
|
||||
# https://github.com/Python-Markdown/markdown
|
||||
@@ -85,7 +84,7 @@ mkdocs-material
|
||||
|
||||
# Introspection for embedded code
|
||||
# https://github.com/mkdocstrings/mkdocstrings
|
||||
mkdocstrings<=0.17.0
|
||||
mkdocstrings[python-legacy]
|
||||
|
||||
# Library for manipulating IP prefixes and addresses
|
||||
# https://github.com/netaddr/netaddr
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[Unit]
|
||||
Description=NetBox Request Queue Worker
|
||||
Documentation=https://netbox.readthedocs.io/en/stable/
|
||||
Documentation=https://docs.netbox.dev/
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[Unit]
|
||||
Description=NetBox WSGI Service
|
||||
Documentation=https://netbox.readthedocs.io/en/stable/
|
||||
Documentation=https://docs.netbox.dev/
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
|
||||
88
docs/administration/authentication/microsoft-azure-ad.md
Normal 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**.
|
||||
|
||||

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

|
||||
|
||||
Once finished, make note of the application (client) ID; this will be used when configuring NetBox.
|
||||
|
||||

|
||||
|
||||
!!! 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.
|
||||
|
||||

|
||||
|
||||
You can optionally specify a description and select a lifetime for the secret.
|
||||
|
||||

|
||||
|
||||
Once finished, make note of the secret value (not the secret ID); this will be used when configuring NetBox.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

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

|
||||
|
||||
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.
|
||||
70
docs/administration/authentication/okta.md
Normal 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.
|
||||
|
||||

|
||||
|
||||
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/`
|
||||
|
||||

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

|
||||
|
||||
## 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.
|
||||
|
||||

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

|
||||
|
||||
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.
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
Local user accounts and groups can be created in NetBox under the "Authentication and Authorization" section of the administrative user interface. This interface is available only to users with the "staff" permission enabled.
|
||||
|
||||
At a minimum, each user account must have a username and password set. User accounts may also denote a first name, last name, and email address. [Permissions](./permissions.md) may also be assigned to users and/or groups within the admin UI.
|
||||
At a minimum, each user account must have a username and password set. User accounts may also denote a first name, last name, and email address. [Permissions](../permissions.md) may also be assigned to users and/or groups within the admin UI.
|
||||
|
||||
## Remote Authentication
|
||||
|
||||
@@ -16,7 +16,7 @@ NetBox may be configured to provide user authenticate via a remote backend in ad
|
||||
REMOTE_AUTH_BACKEND = 'netbox.authentication.LDAPBackend'
|
||||
```
|
||||
|
||||
NetBox includes an authentication backend which supports LDAP. See the [LDAP installation docs](../installation/6-ldap.md) for more detail about this backend.
|
||||
NetBox includes an authentication backend which supports LDAP. See the [LDAP installation docs](../../installation/6-ldap.md) for more detail about this backend.
|
||||
|
||||
### HTTP Header Authentication
|
||||
|
||||
@@ -4,6 +4,7 @@ NetBox includes a `housekeeping` management command that should be run nightly.
|
||||
|
||||
* Clearing expired authentication sessions from the database
|
||||
* Deleting changelog records older than the configured [retention time](../configuration/dynamic-settings.md#changelog_retention)
|
||||
* Deleting job result records older than the configured [retention time](../configuration/dynamic-settings.md#jobresult_retention)
|
||||
|
||||
This command can be invoked directly, or by using the shell script provided at `/opt/netbox/contrib/netbox-housekeeping.sh`. This script can be linked from your cron scheduler's daily jobs directory (e.g. `/etc/cron.daily`) or referenced directly within the cron configuration file.
|
||||
|
||||
|
||||
@@ -43,6 +43,18 @@ changes in the database indefinitely.
|
||||
|
||||
---
|
||||
|
||||
## JOBRESULT_RETENTION
|
||||
|
||||
Default: 90
|
||||
|
||||
The number of days to retain job results (scripts and reports). Set this to `0` to retain
|
||||
job results in the database indefinitely.
|
||||
|
||||
!!! warning
|
||||
If enabling indefinite job results retention, it is recommended to periodically delete old entries. Otherwise, the database may eventually exceed capacity.
|
||||
|
||||
---
|
||||
|
||||
## CUSTOM_VALIDATORS
|
||||
|
||||
This is a mapping of models to [custom validators](../customization/custom-validation.md) that have been defined locally to enforce custom validation logic. An example is provided below:
|
||||
|
||||
@@ -43,7 +43,7 @@ A mapping of permissions to assign a new user account when created using remote
|
||||
|
||||
Default: `False`
|
||||
|
||||
NetBox can be configured to support remote user authentication by inferring user authentication from an HTTP header set by the HTTP reverse proxy (e.g. nginx or Apache). Set this to `True` to enable this functionality. (Local authentication will still take effect as a fallback.) (`REMOTE_AUTH_DEFAULT_GROUPS` will not function if `REMOTE_AUTH_ENABLED` is enabled)
|
||||
NetBox can be configured to support remote user authentication by inferring user authentication from an HTTP header set by the HTTP reverse proxy (e.g. nginx or Apache). Set this to `True` to enable this functionality. (Local authentication will still take effect as a fallback.) (`REMOTE_AUTH_DEFAULT_GROUPS` will not function if `REMOTE_AUTH_ENABLED` is disabled)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -89,6 +89,12 @@ The checkbox to commit database changes when executing a script is checked by de
|
||||
commit_default = False
|
||||
```
|
||||
|
||||
### `job_timeout`
|
||||
|
||||
Set the maximum allowed runtime for the script. If not set, `RQ_DEFAULT_TIMEOUT` will be used.
|
||||
|
||||
!!! info "This feature was introduced in v3.2.1"
|
||||
|
||||
## Accessing Request Data
|
||||
|
||||
Details of the current HTTP request (the one being made to execute the script) are available as the instance attribute `self.request`. This can be used to infer, for example, the user executing the script and the client IP address:
|
||||
|
||||
@@ -105,11 +105,11 @@ from my_validators import Validator1, Validator2, Validator3
|
||||
|
||||
CUSTOM_VALIDATORS = {
|
||||
'dcim.site': (
|
||||
Validator1,
|
||||
Validator2,
|
||||
Validator1(),
|
||||
Validator2(),
|
||||
),
|
||||
'dcim.device': (
|
||||
Validator3,
|
||||
Validator3(),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -85,6 +85,20 @@ As you can see, reports are completely customizable. Validation logic can be as
|
||||
!!! warning
|
||||
Reports should never alter data: If you find yourself using the `create()`, `save()`, `update()`, or `delete()` methods on objects within reports, stop and re-evaluate what you're trying to accomplish. Note that there are no safeguards against the accidental alteration or destruction of data.
|
||||
|
||||
## Report Attributes
|
||||
|
||||
### `description`
|
||||
|
||||
A human-friendly description of what your report does.
|
||||
|
||||
### `job_timeout`
|
||||
|
||||
Set the maximum allowed runtime for the report. If not set, `RQ_DEFAULT_TIMEOUT` will be used.
|
||||
|
||||
!!! info "This feature was introduced in v3.2.1"
|
||||
|
||||
## Logging
|
||||
|
||||
The following methods are available to log results within a report:
|
||||
|
||||
* log(message)
|
||||
|
||||
@@ -40,7 +40,7 @@ You should see output similar to the following:
|
||||
● netbox.service - NetBox WSGI Service
|
||||
Loaded: loaded (/etc/systemd/system/netbox.service; enabled; vendor preset: enabled)
|
||||
Active: active (running) since Mon 2021-08-30 04:02:36 UTC; 14h ago
|
||||
Docs: https://netbox.readthedocs.io/en/stable/
|
||||
Docs: https://docs.netbox.dev/
|
||||
Main PID: 1140492 (gunicorn)
|
||||
Tasks: 19 (limit: 4683)
|
||||
Memory: 666.2M
|
||||
|
||||
@@ -39,7 +39,7 @@ You can use the command `systemctl status netbox` to verify that the WSGI servic
|
||||
● netbox.service - NetBox WSGI Service
|
||||
Loaded: loaded (/etc/systemd/system/netbox.service; enabled; vendor preset: enabled)
|
||||
Active: active (running) since Sat 2020-10-24 19:23:40 UTC; 25s ago
|
||||
Docs: https://netbox.readthedocs.io/en/stable/
|
||||
Docs: https://docs.netbox.dev/
|
||||
Main PID: 11993 (gunicorn)
|
||||
Tasks: 6 (limit: 2362)
|
||||
CGroup: /system.slice/netbox.service
|
||||
|
||||
BIN
docs/media/authentication/azure_ad_add_app_registration.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
docs/media/authentication/azure_ad_add_client_secret.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
docs/media/authentication/azure_ad_app_registration.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
docs/media/authentication/azure_ad_app_registration_created.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
docs/media/authentication/azure_ad_client_secret.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
docs/media/authentication/azure_ad_client_secret_created.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
docs/media/authentication/azure_ad_login_portal.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
docs/media/authentication/netbox_azure_ad_login.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
docs/media/authentication/netbox_okta_login.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
docs/media/authentication/okta_create_app_registration.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
docs/media/authentication/okta_integration_parameters.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
docs/media/authentication/okta_login_portal.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
docs/media/authentication/okta_web_app_integration.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
@@ -2,7 +2,8 @@
|
||||
|
||||
A virtual chassis represents a set of devices which share a common control plane. A common example of this is a stack of switches which are connected and configured to operate as a single device. A virtual chassis must be assigned a name and may be assigned a domain.
|
||||
|
||||
Each device in the virtual chassis is referred to as a VC member, and assigned a position and (optionally) a priority. VC member devices commonly reside within the same rack, though this is not a requirement. One of the devices may be designated as the VC master: This device will typically be assigned a name, services, and other attributes related to managing the VC.
|
||||
Each device in the virtual chassis is referred to as a VC member, and assigned a position and (optionally) a priority. VC member devices commonly reside within the same rack, though this is not a requirement. One of the devices may be designated as the VC master: This device will typically be assigned a name, services, virtual interfaces, and other attributes related to managing the VC.
|
||||
If a VC master is defined, interfaces from all VC members are displayed when navigating to its device interfaces view. This does not include other members interfaces declared as management-only.
|
||||
|
||||
!!! note
|
||||
It's important to recognize the distinction between a virtual chassis and a chassis-based device. A virtual chassis is **not** suitable for modeling a chassis-based switch with removable line cards (such as the Juniper EX9208), as its line cards are _not_ physically autonomous devices.
|
||||
|
||||
@@ -121,7 +121,7 @@ A new API endpoint has been added at `/api/ipam/prefixes/<pk>/available-ips/`. A
|
||||
|
||||
#### NAPALM Integration ([#1348](https://github.com/netbox-community/netbox/issues/1348))
|
||||
|
||||
The [NAPALM automation](https://github.com/napalm-automation/napalm) library provides an abstracted interface for pulling live data (e.g. uptime, software version, running config, LLDP neighbors, etc.) from network devices. The NetBox API has been extended to support executing read-only NAPALM methods on devices defined in NetBox. To enable this functionality, ensure that NAPALM has been installed (`pip install napalm`) and the `NETBOX_USERNAME` and `NETBOX_PASSWORD` [configuration parameters](https://netbox.readthedocs.io/en/stable/configuration/optional-settings/#netbox_username) have been set in configuration.py.
|
||||
The [NAPALM automation](https://github.com/napalm-automation/napalm) library provides an abstracted interface for pulling live data (e.g. uptime, software version, running config, LLDP neighbors, etc.) from network devices. The NetBox API has been extended to support executing read-only NAPALM methods on devices defined in NetBox. To enable this functionality, ensure that NAPALM has been installed (`pip install napalm`) and the `NETBOX_USERNAME` and `NETBOX_PASSWORD` [configuration parameters](https://docs.netbox.dev/en/stable/configuration/optional-settings/#netbox_username) have been set in configuration.py.
|
||||
|
||||
### Enhancements
|
||||
|
||||
|
||||
@@ -196,7 +196,7 @@ Our second-most popular feature request has arrived! NetBox now supports the cre
|
||||
|
||||
#### Custom Validation Reports ([#1511](https://github.com/netbox-community/netbox/issues/1511))
|
||||
|
||||
Users can now create custom reports which are run to validate data in NetBox. Reports work very similar to Python unit tests: Each report inherits from NetBox's Report class and contains one or more test method. Reports can be run and retrieved via the web UI, API, or CLI. See [the docs](https://netbox.readthedocs.io/en/stable/miscellaneous/reports/) for more info.
|
||||
Users can now create custom reports which are run to validate data in NetBox. Reports work very similar to Python unit tests: Each report inherits from NetBox's Report class and contains one or more test method. Reports can be run and retrieved via the web UI, API, or CLI. See [the docs](https://docs.netbox.dev/en/stable/miscellaneous/reports/) for more info.
|
||||
|
||||
### Enhancements
|
||||
|
||||
|
||||
@@ -295,7 +295,7 @@ This release upgrades the Django framework to version 2.2.
|
||||
|
||||
#### Python 3 Required
|
||||
|
||||
As promised, Python 2 support has been completed removed. Python 3.5 or higher is now required to run NetBox. Please see [our Python 3 migration guide](https://netbox.readthedocs.io/en/stable/installation/migrating-to-python3/) for assistance with upgrading.
|
||||
As promised, Python 2 support has been completed removed. Python 3.5 or higher is now required to run NetBox. Please see [our Python 3 migration guide](https://docs.netbox.dev/en/stable/installation/migrating-to-python3/) for assistance with upgrading.
|
||||
|
||||
#### Removed Deprecated User Activity Log
|
||||
|
||||
|
||||
@@ -218,7 +218,7 @@
|
||||
|
||||
#### Custom Scripts ([#3415](https://github.com/netbox-community/netbox/issues/3415))
|
||||
|
||||
Custom scripts allow for the execution of arbitrary code via the NetBox UI. They can be used to automatically create, manipulate, or clean up objects or perform other tasks within NetBox. Scripts are defined as Python files which contain one or more subclasses of `extras.scripts.Script`. Variable fields can be defined within scripts, which render as form fields within the web UI to prompt the user for input data. Scripts are executed and information is logged via the web UI. Please see [the docs](https://netbox.readthedocs.io/en/stable/customization/custom-scripts/) for more detail.
|
||||
Custom scripts allow for the execution of arbitrary code via the NetBox UI. They can be used to automatically create, manipulate, or clean up objects or perform other tasks within NetBox. Scripts are defined as Python files which contain one or more subclasses of `extras.scripts.Script`. Variable fields can be defined within scripts, which render as form fields within the web UI to prompt the user for input data. Scripts are executed and information is logged via the web UI. Please see [the docs](https://docs.netbox.dev/en/stable/customization/custom-scripts/) for more detail.
|
||||
|
||||
Note: There are currently no API endpoints for this feature. These are planned for the upcoming v2.7 release.
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
|
||||
## v2.7.9 (2020-03-06)
|
||||
|
||||
**Note:** This release will deploy a Python virtual environment on upgrade in the `venv/` directory. This will require modifying the paths to your Python and gunicorn executables in the systemd service files. For more detail, please see the [upgrade instructions](https://netbox.readthedocs.io/en/stable/installation/upgrading/).
|
||||
**Note:** This release will deploy a Python virtual environment on upgrade in the `venv/` directory. This will require modifying the paths to your Python and gunicorn executables in the systemd service files. For more detail, please see the [upgrade instructions](https://docs.netbox.dev/en/stable/installation/upgrading/).
|
||||
|
||||
### Enhancements
|
||||
|
||||
@@ -418,7 +418,7 @@ to another source before upgrading NetBox to v2.7, as any existing topology maps
|
||||
|
||||
#### Supervisor Replaced with systemd ([#2902](https://github.com/netbox-community/netbox/issues/2902))
|
||||
|
||||
The NetBox [installation documentation](https://netbox.readthedocs.io/en/stable/installation/) has been updated to
|
||||
The NetBox [installation documentation](https://docs.netbox.dev/en/stable/installation/) has been updated to
|
||||
provide instructions for managing the WSGI and RQ services using systemd instead of supervisor. This removes the need to
|
||||
install supervisor and simplifies administration of the processes.
|
||||
|
||||
|
||||
@@ -235,14 +235,14 @@ This release introduces support for custom plugins, which can be used to extend
|
||||
* Introduce new API endpoints
|
||||
* Add custom request/response middleware
|
||||
|
||||
For NetBox plugins to be recognized, they must be installed and added by name to the `PLUGINS` configuration parameter. (Plugin support is disabled by default.) Plugins can be configured under the `PLUGINS_CONFIG` parameter. More information can be found the in the [plugins documentation](https://netbox.readthedocs.io/en/stable/plugins/).
|
||||
For NetBox plugins to be recognized, they must be installed and added by name to the `PLUGINS` configuration parameter. (Plugin support is disabled by default.) Plugins can be configured under the `PLUGINS_CONFIG` parameter. More information can be found the in the [plugins documentation](https://docs.netbox.dev/en/stable/plugins/).
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#1754](https://github.com/netbox-community/netbox/issues/1754) - Added support for nested rack groups
|
||||
* [#3939](https://github.com/netbox-community/netbox/issues/3939) - Added support for nested tenant groups
|
||||
* [#4078](https://github.com/netbox-community/netbox/issues/4078) - Standardized description fields across all models
|
||||
* [#4195](https://github.com/netbox-community/netbox/issues/4195) - Enabled application logging (see [logging configuration](https://netbox.readthedocs.io/en/stable/configuration/optional-settings/#logging))
|
||||
* [#4195](https://github.com/netbox-community/netbox/issues/4195) - Enabled application logging (see [logging configuration](https://docs.netbox.dev/en/stable/configuration/optional-settings/#logging))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
|
||||
@@ -1,5 +1,62 @@
|
||||
# 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)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#5479](https://github.com/netbox-community/netbox/issues/5479) - Allow custom job timeouts for scripts & reports
|
||||
* [#8543](https://github.com/netbox-community/netbox/issues/8543) - Improve filtering for wireless LAN VLAN selection
|
||||
* [#8920](https://github.com/netbox-community/netbox/issues/8920) - Limit number of non-racked devices displayed
|
||||
* [#8956](https://github.com/netbox-community/netbox/issues/8956) - Retain old script/report results for configured lifetime
|
||||
* [#8973](https://github.com/netbox-community/netbox/issues/8973) - Display VLAN group count under site view
|
||||
* [#9081](https://github.com/netbox-community/netbox/issues/9081) - Add `fhrpgroup_id` filter for IP addresses
|
||||
* [#9099](https://github.com/netbox-community/netbox/issues/9099) - Enable display of installed module serial & asset tag in module bays list
|
||||
* [#9110](https://github.com/netbox-community/netbox/issues/9110) - Add Neutrik proprietary power connectors
|
||||
* [#9123](https://github.com/netbox-community/netbox/issues/9123) - Improve appearance of SSO login providers
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#8931](https://github.com/netbox-community/netbox/issues/8931) - Copy assigned tenant when cloning a location
|
||||
* [#9055](https://github.com/netbox-community/netbox/issues/9055) - Restore ability to move inventory item to other device
|
||||
* [#9057](https://github.com/netbox-community/netbox/issues/9057) - Fix missing instance counts for module types
|
||||
* [#9061](https://github.com/netbox-community/netbox/issues/9061) - Fix general search for device components
|
||||
* [#9065](https://github.com/netbox-community/netbox/issues/9065) - Min/max VID should not be required when filtering VLAN groups
|
||||
* [#9079](https://github.com/netbox-community/netbox/issues/9079) - Fail validation when an inventory item is assigned as its own parent
|
||||
* [#9096](https://github.com/netbox-community/netbox/issues/9096) - Remove duplicate filter tag when filtering by "none"
|
||||
* [#9100](https://github.com/netbox-community/netbox/issues/9100) - Include position field in module type YAML export
|
||||
* [#9116](https://github.com/netbox-community/netbox/issues/9116) - `assigned_to_interface` filter for IP addresses should not match FHRP group assignments
|
||||
* [#9118](https://github.com/netbox-community/netbox/issues/9118) - Fix validation error when importing VM child interfaces
|
||||
* [#9128](https://github.com/netbox-community/netbox/issues/9128) - Resolve component labels per module bay position when installing modules
|
||||
|
||||
---
|
||||
|
||||
## v3.2.0 (2022-04-05)
|
||||
|
||||
!!! warning "Python 3.8 or Later Required"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
site_name: NetBox Documentation
|
||||
site_dir: netbox/project-static/docs
|
||||
site_url: https://netbox.readthedocs.io/
|
||||
site_url: https://docs.netbox.dev/
|
||||
repo_name: netbox-community/netbox
|
||||
repo_url: https://github.com/netbox-community/netbox
|
||||
theme:
|
||||
@@ -19,6 +19,7 @@ theme:
|
||||
icon: material/lightbulb
|
||||
name: Switch to Light Mode
|
||||
plugins:
|
||||
- search
|
||||
- mkdocstrings:
|
||||
handlers:
|
||||
python:
|
||||
@@ -117,7 +118,10 @@ nav:
|
||||
- GraphQL API: 'plugins/development/graphql-api.md'
|
||||
- Background Tasks: 'plugins/development/background-tasks.md'
|
||||
- Administration:
|
||||
- Authentication: 'administration/authentication.md'
|
||||
- Authentication:
|
||||
- Overview: 'administration/authentication/overview.md'
|
||||
- Microsoft Azure AD: 'administration/authentication/microsoft-azure-ad.md'
|
||||
- Okta: 'administration/authentication/okta.md'
|
||||
- Permissions: 'administration/permissions.md'
|
||||
- Housekeeping: 'administration/housekeeping.md'
|
||||
- Replicating NetBox: 'administration/replicating-netbox.md'
|
||||
|
||||
@@ -345,6 +345,11 @@ class PowerPortTypeChoices(ChoiceSet):
|
||||
TYPE_DC = 'dc-terminal'
|
||||
# Proprietary
|
||||
TYPE_SAF_D_GRID = 'saf-d-grid'
|
||||
TYPE_NEUTRIK_POWERCON_20A = 'neutrik-powercon-20'
|
||||
TYPE_NEUTRIK_POWERCON_32A = 'neutrik-powercon-32'
|
||||
TYPE_NEUTRIK_POWERCON_TRUE1 = 'neutrik-powercon-true1'
|
||||
TYPE_NEUTRIK_POWERCON_TRUE1_TOP = 'neutrik-powercon-true1-top'
|
||||
TYPE_UBIQUITI_SMARTPOWER = 'ubiquiti-smartpower'
|
||||
# Other
|
||||
TYPE_HARDWIRED = 'hardwired'
|
||||
|
||||
@@ -456,6 +461,11 @@ class PowerPortTypeChoices(ChoiceSet):
|
||||
)),
|
||||
('Proprietary', (
|
||||
(TYPE_SAF_D_GRID, 'Saf-D-Grid'),
|
||||
(TYPE_NEUTRIK_POWERCON_20A, 'Neutrik powerCON (20A)'),
|
||||
(TYPE_NEUTRIK_POWERCON_32A, 'Neutrik powerCON (32A)'),
|
||||
(TYPE_NEUTRIK_POWERCON_TRUE1, 'Neutrik powerCON TRUE1'),
|
||||
(TYPE_NEUTRIK_POWERCON_TRUE1_TOP, 'Neutrik powerCON TRUE1 TOP'),
|
||||
(TYPE_UBIQUITI_SMARTPOWER, 'Ubiquiti SmartPower'),
|
||||
)),
|
||||
('Other', (
|
||||
(TYPE_HARDWIRED, 'Hardwired'),
|
||||
@@ -561,6 +571,10 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
# Proprietary
|
||||
TYPE_HDOT_CX = 'hdot-cx'
|
||||
TYPE_SAF_D_GRID = 'saf-d-grid'
|
||||
TYPE_NEUTRIK_POWERCON_20A = 'neutrik-powercon-20a'
|
||||
TYPE_NEUTRIK_POWERCON_32A = 'neutrik-powercon-32a'
|
||||
TYPE_NEUTRIK_POWERCON_TRUE1 = 'neutrik-powercon-true1'
|
||||
TYPE_NEUTRIK_POWERCON_TRUE1_TOP = 'neutrik-powercon-true1-top'
|
||||
# Other
|
||||
TYPE_HARDWIRED = 'hardwired'
|
||||
|
||||
@@ -665,6 +679,10 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
('Proprietary', (
|
||||
(TYPE_HDOT_CX, 'HDOT Cx'),
|
||||
(TYPE_SAF_D_GRID, 'Saf-D-Grid'),
|
||||
(TYPE_NEUTRIK_POWERCON_20A, 'Neutrik powerCON (20A)'),
|
||||
(TYPE_NEUTRIK_POWERCON_32A, 'Neutrik powerCON (32A)'),
|
||||
(TYPE_NEUTRIK_POWERCON_TRUE1, 'Neutrik powerCON TRUE1'),
|
||||
(TYPE_NEUTRIK_POWERCON_TRUE1_TOP, 'Neutrik powerCON TRUE1 TOP'),
|
||||
)),
|
||||
('Other', (
|
||||
(TYPE_HARDWIRED, 'Hardwired'),
|
||||
|
||||
@@ -435,6 +435,10 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
|
||||
method='_device_bays',
|
||||
label='Has device bays',
|
||||
)
|
||||
inventory_items = django_filters.BooleanFilter(
|
||||
method='_inventory_items',
|
||||
label='Has inventory items',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
@@ -479,6 +483,9 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
|
||||
def _device_bays(self, queryset, name, value):
|
||||
return queryset.exclude(devicebaytemplates__isnull=value)
|
||||
|
||||
def _inventory_items(self, queryset, name, value):
|
||||
return queryset.exclude(inventoryitemtemplates__isnull=value)
|
||||
|
||||
|
||||
class ModuleTypeFilterSet(NetBoxModelFilterSet):
|
||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||
@@ -1095,8 +1102,8 @@ class PathEndpointFilterSet(django_filters.FilterSet):
|
||||
|
||||
|
||||
class ConsolePortFilterSet(
|
||||
NetBoxModelFilterSet,
|
||||
ModularDeviceComponentFilterSet,
|
||||
NetBoxModelFilterSet,
|
||||
CableTerminationFilterSet,
|
||||
PathEndpointFilterSet
|
||||
):
|
||||
@@ -1111,8 +1118,8 @@ class ConsolePortFilterSet(
|
||||
|
||||
|
||||
class ConsoleServerPortFilterSet(
|
||||
NetBoxModelFilterSet,
|
||||
ModularDeviceComponentFilterSet,
|
||||
NetBoxModelFilterSet,
|
||||
CableTerminationFilterSet,
|
||||
PathEndpointFilterSet
|
||||
):
|
||||
@@ -1127,8 +1134,8 @@ class ConsoleServerPortFilterSet(
|
||||
|
||||
|
||||
class PowerPortFilterSet(
|
||||
NetBoxModelFilterSet,
|
||||
ModularDeviceComponentFilterSet,
|
||||
NetBoxModelFilterSet,
|
||||
CableTerminationFilterSet,
|
||||
PathEndpointFilterSet
|
||||
):
|
||||
@@ -1143,8 +1150,8 @@ class PowerPortFilterSet(
|
||||
|
||||
|
||||
class PowerOutletFilterSet(
|
||||
NetBoxModelFilterSet,
|
||||
ModularDeviceComponentFilterSet,
|
||||
NetBoxModelFilterSet,
|
||||
CableTerminationFilterSet,
|
||||
PathEndpointFilterSet
|
||||
):
|
||||
@@ -1163,8 +1170,8 @@ class PowerOutletFilterSet(
|
||||
|
||||
|
||||
class InterfaceFilterSet(
|
||||
NetBoxModelFilterSet,
|
||||
ModularDeviceComponentFilterSet,
|
||||
NetBoxModelFilterSet,
|
||||
CableTerminationFilterSet,
|
||||
PathEndpointFilterSet
|
||||
):
|
||||
@@ -1291,8 +1298,8 @@ class InterfaceFilterSet(
|
||||
|
||||
|
||||
class FrontPortFilterSet(
|
||||
NetBoxModelFilterSet,
|
||||
ModularDeviceComponentFilterSet,
|
||||
NetBoxModelFilterSet,
|
||||
CableTerminationFilterSet
|
||||
):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
@@ -1306,8 +1313,8 @@ class FrontPortFilterSet(
|
||||
|
||||
|
||||
class RearPortFilterSet(
|
||||
NetBoxModelFilterSet,
|
||||
ModularDeviceComponentFilterSet,
|
||||
NetBoxModelFilterSet,
|
||||
CableTerminationFilterSet
|
||||
):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
@@ -1320,21 +1327,21 @@ class RearPortFilterSet(
|
||||
fields = ['id', 'name', 'label', 'type', 'color', 'positions', 'description']
|
||||
|
||||
|
||||
class ModuleBayFilterSet(NetBoxModelFilterSet, DeviceComponentFilterSet):
|
||||
class ModuleBayFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ModuleBay
|
||||
fields = ['id', 'name', 'label', 'description']
|
||||
|
||||
|
||||
class DeviceBayFilterSet(NetBoxModelFilterSet, DeviceComponentFilterSet):
|
||||
class DeviceBayFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = DeviceBay
|
||||
fields = ['id', 'name', 'label', 'description']
|
||||
|
||||
|
||||
class InventoryItemFilterSet(NetBoxModelFilterSet, DeviceComponentFilterSet):
|
||||
class InventoryItemFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet):
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=InventoryItem.objects.all(),
|
||||
label='Parent inventory item (ID)',
|
||||
|
||||
@@ -3,7 +3,7 @@ from django import forms
|
||||
from dcim.models import *
|
||||
from extras.forms import CustomFieldsMixin
|
||||
from extras.models import Tag
|
||||
from utilities.forms import DynamicModelMultipleChoiceField, form_from_model
|
||||
from utilities.forms import DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model
|
||||
from .object_create import ComponentCreateForm
|
||||
|
||||
__all__ = (
|
||||
@@ -98,7 +98,13 @@ class RearPortBulkCreateForm(
|
||||
|
||||
class ModuleBayBulkCreateForm(DeviceBulkAddComponentForm):
|
||||
model = ModuleBay
|
||||
field_order = ('name_pattern', 'label_pattern', 'description', 'tags')
|
||||
field_order = ('name_pattern', 'label_pattern', 'position_pattern', 'description', 'tags')
|
||||
|
||||
position_pattern = ExpandableNameField(
|
||||
label='Position',
|
||||
required=False,
|
||||
help_text='Alphanumeric ranges are supported. (Must match the number of names being created.)'
|
||||
)
|
||||
|
||||
|
||||
class DeviceBayBulkCreateForm(DeviceBulkAddComponentForm):
|
||||
|
||||
@@ -1204,6 +1204,10 @@ class InventoryItemBulkEditForm(
|
||||
form_from_model(InventoryItem, ['label', 'role', 'manufacturer', 'part_id', 'description']),
|
||||
NetBoxModelBulkEditForm
|
||||
):
|
||||
device = DynamicModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False
|
||||
)
|
||||
role = DynamicModelChoiceField(
|
||||
queryset=InventoryItemRole.objects.all(),
|
||||
required=False
|
||||
@@ -1215,7 +1219,7 @@ class InventoryItemBulkEditForm(
|
||||
|
||||
model = InventoryItem
|
||||
fieldsets = (
|
||||
(None, ('label', 'role', 'manufacturer', 'part_id', 'description')),
|
||||
(None, ('device', 'label', 'role', 'manufacturer', 'part_id', 'description')),
|
||||
)
|
||||
nullable_fields = ('label', 'role', 'manufacturer', 'part_id', 'description')
|
||||
|
||||
|
||||
@@ -651,11 +651,11 @@ class InterfaceCSVForm(NetBoxModelCSVForm):
|
||||
super().__init__(data, *args, **kwargs)
|
||||
|
||||
if data:
|
||||
# Limit interface choices for parent, bridge and lag to device only
|
||||
params = {}
|
||||
if data.get('device'):
|
||||
params[f"device__{self.fields['device'].to_field_name}"] = data.get('device')
|
||||
if params:
|
||||
# Limit choices for parent, bridge, and LAG interfaces to the assigned device
|
||||
if device := data.get('device'):
|
||||
params = {
|
||||
f"device__{self.fields['device'].to_field_name}": device
|
||||
}
|
||||
self.fields['parent'].queryset = self.fields['parent'].queryset.filter(**params)
|
||||
self.fields['bridge'].queryset = self.fields['bridge'].queryset.filter(**params)
|
||||
self.fields['lag'].queryset = self.fields['lag'].queryset.filter(**params)
|
||||
|
||||
@@ -331,7 +331,7 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
|
||||
('Hardware', ('manufacturer_id', 'part_number', 'subdevice_role', 'airflow')),
|
||||
('Components', (
|
||||
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
|
||||
'pass_through_ports',
|
||||
'pass_through_ports', 'device_bays', 'module_bays', 'inventory_items',
|
||||
)),
|
||||
)
|
||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||
@@ -392,6 +392,27 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
device_bays = forms.NullBooleanField(
|
||||
required=False,
|
||||
label='Has device bays',
|
||||
widget=StaticSelect(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
module_bays = forms.NullBooleanField(
|
||||
required=False,
|
||||
label='Has module bays',
|
||||
widget=StaticSelect(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
inventory_items = forms.NullBooleanField(
|
||||
required=False,
|
||||
label='Has inventory items',
|
||||
widget=StaticSelect(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
|
||||
@@ -385,6 +385,12 @@ class ModuleTypeForm(NetBoxModelForm):
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
fieldsets = (
|
||||
('Module Type', (
|
||||
'manufacturer', 'model', 'part_number', 'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ModuleType
|
||||
fields = [
|
||||
@@ -627,6 +633,15 @@ class ModuleForm(NetBoxModelForm):
|
||||
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:
|
||||
model = Module
|
||||
fields = [
|
||||
@@ -1362,6 +1377,9 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
|
||||
|
||||
|
||||
class InventoryItemForm(NetBoxModelForm):
|
||||
device = DynamicModelChoiceField(
|
||||
queryset=Device.objects.all()
|
||||
)
|
||||
parent = DynamicModelChoiceField(
|
||||
queryset=InventoryItem.objects.all(),
|
||||
required=False,
|
||||
@@ -1399,9 +1417,6 @@ class InventoryItemForm(NetBoxModelForm):
|
||||
'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
|
||||
'description', 'component_type', 'component_id', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from django import forms
|
||||
|
||||
from dcim.models import *
|
||||
from extras.models import Tag
|
||||
from netbox.forms import NetBoxModelForm
|
||||
from utilities.forms import (
|
||||
BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField,
|
||||
@@ -12,6 +11,7 @@ __all__ = (
|
||||
'DeviceComponentCreateForm',
|
||||
'FrontPortCreateForm',
|
||||
'FrontPortTemplateCreateForm',
|
||||
'InventoryItemCreateForm',
|
||||
'ModularComponentTemplateCreateForm',
|
||||
'ModuleBayCreateForm',
|
||||
'ModuleBayTemplateCreateForm',
|
||||
@@ -199,6 +199,11 @@ class ModuleBayCreateForm(DeviceComponentCreateForm):
|
||||
field_order = ('device', 'name_pattern', 'label_pattern', 'position_pattern')
|
||||
|
||||
|
||||
class InventoryItemCreateForm(ComponentCreateForm):
|
||||
# Device is assigned by the model form
|
||||
field_order = ('name_pattern', 'label_pattern')
|
||||
|
||||
|
||||
class VirtualChassisCreateForm(NetBoxModelForm):
|
||||
region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
|
||||
@@ -124,6 +124,11 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
|
||||
return self.name.replace('{module}', module.module_bay.position)
|
||||
return self.name
|
||||
|
||||
def resolve_label(self, module):
|
||||
if module:
|
||||
return self.label.replace('{module}', module.module_bay.position)
|
||||
return self.label
|
||||
|
||||
|
||||
class ConsolePortTemplate(ModularComponentTemplateModel):
|
||||
"""
|
||||
@@ -147,7 +152,7 @@ class ConsolePortTemplate(ModularComponentTemplateModel):
|
||||
def instantiate(self, **kwargs):
|
||||
return self.component_model(
|
||||
name=self.resolve_name(kwargs.get('module')),
|
||||
label=self.label,
|
||||
label=self.resolve_label(kwargs.get('module')),
|
||||
type=self.type,
|
||||
**kwargs
|
||||
)
|
||||
@@ -175,7 +180,7 @@ class ConsoleServerPortTemplate(ModularComponentTemplateModel):
|
||||
def instantiate(self, **kwargs):
|
||||
return self.component_model(
|
||||
name=self.resolve_name(kwargs.get('module')),
|
||||
label=self.label,
|
||||
label=self.resolve_label(kwargs.get('module')),
|
||||
type=self.type,
|
||||
**kwargs
|
||||
)
|
||||
@@ -215,7 +220,7 @@ class PowerPortTemplate(ModularComponentTemplateModel):
|
||||
def instantiate(self, **kwargs):
|
||||
return self.component_model(
|
||||
name=self.resolve_name(kwargs.get('module')),
|
||||
label=self.label,
|
||||
label=self.resolve_label(kwargs.get('module')),
|
||||
type=self.type,
|
||||
maximum_draw=self.maximum_draw,
|
||||
allocated_draw=self.allocated_draw,
|
||||
@@ -286,7 +291,7 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
|
||||
power_port = None
|
||||
return self.component_model(
|
||||
name=self.resolve_name(kwargs.get('module')),
|
||||
label=self.label,
|
||||
label=self.resolve_label(kwargs.get('module')),
|
||||
type=self.type,
|
||||
power_port=power_port,
|
||||
feed_leg=self.feed_leg,
|
||||
@@ -326,7 +331,7 @@ class InterfaceTemplate(ModularComponentTemplateModel):
|
||||
def instantiate(self, **kwargs):
|
||||
return self.component_model(
|
||||
name=self.resolve_name(kwargs.get('module')),
|
||||
label=self.label,
|
||||
label=self.resolve_label(kwargs.get('module')),
|
||||
type=self.type,
|
||||
mgmt_only=self.mgmt_only,
|
||||
**kwargs
|
||||
@@ -397,7 +402,7 @@ class FrontPortTemplate(ModularComponentTemplateModel):
|
||||
rear_port = None
|
||||
return self.component_model(
|
||||
name=self.resolve_name(kwargs.get('module')),
|
||||
label=self.label,
|
||||
label=self.resolve_label(kwargs.get('module')),
|
||||
type=self.type,
|
||||
color=self.color,
|
||||
rear_port=rear_port,
|
||||
@@ -437,7 +442,7 @@ class RearPortTemplate(ModularComponentTemplateModel):
|
||||
def instantiate(self, **kwargs):
|
||||
return self.component_model(
|
||||
name=self.resolve_name(kwargs.get('module')),
|
||||
label=self.label,
|
||||
label=self.resolve_label(kwargs.get('module')),
|
||||
type=self.type,
|
||||
color=self.color,
|
||||
positions=self.positions,
|
||||
|
||||
@@ -77,7 +77,7 @@ class ComponentModel(NetBoxModel):
|
||||
def to_objectchange(self, action):
|
||||
objectchange = super().to_objectchange(action)
|
||||
objectchange.related_object = self.device
|
||||
return super().to_objectchange(action)
|
||||
return objectchange
|
||||
|
||||
@property
|
||||
def parent_object(self):
|
||||
@@ -1070,3 +1070,12 @@ class InventoryItem(MPTTModel, ComponentModel):
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:inventoryitem', kwargs={'pk': self.pk})
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# An InventoryItem cannot be its own parent
|
||||
if self.pk and self.parent_id == self.pk:
|
||||
raise ValidationError({
|
||||
"parent": "Cannot assign self as parent."
|
||||
})
|
||||
|
||||
@@ -257,6 +257,7 @@ class DeviceType(NetBoxModel):
|
||||
{
|
||||
'name': c.name,
|
||||
'label': c.label,
|
||||
'position': c.position,
|
||||
'description': c.description,
|
||||
}
|
||||
for c in self.modulebaytemplates.all()
|
||||
|
||||
@@ -367,7 +367,7 @@ class Location(NestedGroupModel):
|
||||
to='extras.ImageAttachment'
|
||||
)
|
||||
|
||||
clone_fields = ['site', 'parent', 'description']
|
||||
clone_fields = ['site', 'parent', 'tenant', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['site', 'name']
|
||||
|
||||
@@ -739,13 +739,22 @@ class ModuleBayTable(DeviceComponentTable):
|
||||
linkify=True,
|
||||
verbose_name='Installed module'
|
||||
)
|
||||
module_serial = tables.Column(
|
||||
accessor=tables.A('installed_module__serial')
|
||||
)
|
||||
module_asset_tag = tables.Column(
|
||||
accessor=tables.A('installed_module__asset_tag')
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:modulebay_list'
|
||||
)
|
||||
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = ModuleBay
|
||||
fields = ('pk', 'id', 'name', 'device', 'label', 'position', 'installed_module', 'description', 'tags')
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'device', 'label', 'position', 'installed_module', 'module_serial', 'module_asset_tag',
|
||||
'description', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'installed_module', 'description')
|
||||
|
||||
|
||||
@@ -756,7 +765,10 @@ class DeviceModuleBayTable(ModuleBayTable):
|
||||
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = ModuleBay
|
||||
fields = ('pk', 'id', 'name', 'label', 'position', 'installed_module', 'description', 'tags', 'actions')
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'label', 'position', 'installed_module', 'module_serial', 'module_asset_tag',
|
||||
'description', 'tags', 'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'label', 'installed_module', 'description')
|
||||
|
||||
|
||||
|
||||
@@ -698,6 +698,9 @@ class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
DeviceBayTemplate(device_type=device_types[0], name='Device Bay 1'),
|
||||
DeviceBayTemplate(device_type=device_types[1], name='Device Bay 2'),
|
||||
))
|
||||
# Assigned DeviceType must have parent subdevice_role
|
||||
inventory_item = InventoryItemTemplate(device_type=device_types[1], name='Inventory Item 1')
|
||||
inventory_item.save()
|
||||
|
||||
def test_model(self):
|
||||
params = {'model': ['Model 1', 'Model 2']}
|
||||
@@ -784,6 +787,12 @@ class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'module_bays': 'false'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_inventory_items(self):
|
||||
params = {'inventory_items': 'true'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
params = {'inventory_items': 'false'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = ModuleType.objects.all()
|
||||
|
||||
@@ -14,7 +14,7 @@ from django.views.generic import View
|
||||
|
||||
from circuits.models import Circuit
|
||||
from extras.views import ObjectConfigContextView
|
||||
from ipam.models import ASN, IPAddress, Prefix, Service, VLAN
|
||||
from ipam.models import ASN, IPAddress, Prefix, Service, VLAN, VLANGroup
|
||||
from ipam.tables import AssignedIPAddressesTable, InterfaceVLANTable
|
||||
from netbox.views import generic
|
||||
from utilities.forms import ConfirmationForm
|
||||
@@ -320,6 +320,10 @@ class SiteView(generic.ObjectView):
|
||||
'rack_count': Rack.objects.restrict(request.user, 'view').filter(site=instance).count(),
|
||||
'device_count': Device.objects.restrict(request.user, 'view').filter(site=instance).count(),
|
||||
'prefix_count': Prefix.objects.restrict(request.user, 'view').filter(site=instance).count(),
|
||||
'vlangroup_count': VLANGroup.objects.restrict(request.user, 'view').filter(
|
||||
scope_type=ContentType.objects.get_for_model(Site),
|
||||
scope_id=instance.pk
|
||||
).count(),
|
||||
'vlan_count': VLAN.objects.restrict(request.user, 'view').filter(site=instance).count(),
|
||||
'circuit_count': Circuit.objects.restrict(request.user, 'view').filter(terminations__site=instance).count(),
|
||||
'vm_count': VirtualMachine.objects.restrict(request.user, 'view').filter(cluster__site=instance).count(),
|
||||
@@ -338,6 +342,7 @@ class SiteView(generic.ObjectView):
|
||||
'device_count',
|
||||
cumulative=True
|
||||
).restrict(request.user, 'view').filter(site=instance)
|
||||
|
||||
nonracked_devices = Device.objects.filter(
|
||||
site=instance,
|
||||
position__isnull=True,
|
||||
@@ -353,7 +358,8 @@ class SiteView(generic.ObjectView):
|
||||
'stats': stats,
|
||||
'locations': locations,
|
||||
'asns': asns,
|
||||
'nonracked_devices': nonracked_devices,
|
||||
'nonracked_devices': nonracked_devices.order_by('-pk')[:10],
|
||||
'total_nonracked_devices_count': nonracked_devices.count(),
|
||||
}
|
||||
|
||||
|
||||
@@ -431,6 +437,7 @@ class LocationView(generic.ObjectView):
|
||||
).filter(pk__in=location_ids).exclude(pk=instance.pk)
|
||||
child_locations_table = tables.LocationTable(child_locations)
|
||||
child_locations_table.configure(request)
|
||||
|
||||
nonracked_devices = Device.objects.filter(
|
||||
location=instance,
|
||||
position__isnull=True,
|
||||
@@ -441,7 +448,8 @@ class LocationView(generic.ObjectView):
|
||||
'rack_count': rack_count,
|
||||
'device_count': device_count,
|
||||
'child_locations_table': child_locations_table,
|
||||
'nonracked_devices': nonracked_devices,
|
||||
'nonracked_devices': nonracked_devices.order_by('-pk')[:10],
|
||||
'total_nonracked_devices_count': nonracked_devices.count(),
|
||||
}
|
||||
|
||||
|
||||
@@ -960,7 +968,7 @@ class DeviceTypeBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class ModuleTypeListView(generic.ObjectListView):
|
||||
queryset = ModuleType.objects.prefetch_related('manufacturer').annotate(
|
||||
# instance_count=count_related(Module, 'module_type')
|
||||
instance_count=count_related(Module, 'module_type')
|
||||
)
|
||||
filterset = filtersets.ModuleTypeFilterSet
|
||||
filterset_form = forms.ModuleTypeFilterForm
|
||||
@@ -1066,7 +1074,7 @@ class ModuleTypeImportView(generic.ObjectImportView):
|
||||
|
||||
class ModuleTypeBulkEditView(generic.BulkEditView):
|
||||
queryset = ModuleType.objects.prefetch_related('manufacturer').annotate(
|
||||
# instance_count=count_related(Module, 'module_type')
|
||||
instance_count=count_related(Module, 'module_type')
|
||||
)
|
||||
filterset = filtersets.ModuleTypeFilterSet
|
||||
table = tables.ModuleTypeTable
|
||||
@@ -1075,7 +1083,7 @@ class ModuleTypeBulkEditView(generic.BulkEditView):
|
||||
|
||||
class ModuleTypeBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = ModuleType.objects.prefetch_related('manufacturer').annotate(
|
||||
# instance_count=count_related(Module, 'module_type')
|
||||
instance_count=count_related(Module, 'module_type')
|
||||
)
|
||||
filterset = filtersets.ModuleTypeFilterSet
|
||||
table = tables.ModuleTypeTable
|
||||
@@ -2513,7 +2521,7 @@ class InventoryItemEditView(generic.ObjectEditView):
|
||||
|
||||
class InventoryItemCreateView(generic.ComponentCreateView):
|
||||
queryset = InventoryItem.objects.all()
|
||||
form = forms.DeviceComponentCreateForm
|
||||
form = forms.InventoryItemCreateForm
|
||||
model_form = forms.InventoryItemForm
|
||||
template_name = 'dcim/inventoryitem_create.html'
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ class ConfigRevisionAdmin(admin.ModelAdmin):
|
||||
'fields': ('DEFAULT_USER_PREFERENCES',),
|
||||
}),
|
||||
('Miscellaneous', {
|
||||
'fields': ('MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'MAPS_URL'),
|
||||
'fields': ('MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'JOBRESULT_RETENTION', 'MAPS_URL'),
|
||||
}),
|
||||
('Config Revision', {
|
||||
'fields': ('comment',),
|
||||
|
||||
@@ -179,7 +179,7 @@ class ReportViewSet(ViewSet):
|
||||
for r in JobResult.objects.filter(
|
||||
obj_type=report_content_type,
|
||||
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
|
||||
).defer('data')
|
||||
).order_by('name', '-created').distinct('name').defer('data')
|
||||
}
|
||||
|
||||
# Iterate through all available Reports.
|
||||
@@ -236,7 +236,8 @@ class ReportViewSet(ViewSet):
|
||||
run_report,
|
||||
report.full_name,
|
||||
report_content_type,
|
||||
request.user
|
||||
request.user,
|
||||
job_timeout=report.job_timeout
|
||||
)
|
||||
report.result = job_result
|
||||
|
||||
@@ -270,7 +271,7 @@ class ScriptViewSet(ViewSet):
|
||||
for r in JobResult.objects.filter(
|
||||
obj_type=script_content_type,
|
||||
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
|
||||
).defer('data').order_by('created')
|
||||
).order_by('name', '-created').distinct('name').defer('data')
|
||||
}
|
||||
|
||||
flat_list = []
|
||||
@@ -320,7 +321,8 @@ class ScriptViewSet(ViewSet):
|
||||
request.user,
|
||||
data=data,
|
||||
request=copy_safe_request(request),
|
||||
commit=commit
|
||||
commit=commit,
|
||||
job_timeout=script.job_timeout,
|
||||
)
|
||||
script.result = job_result
|
||||
serializer = serializers.ScriptDetailSerializer(script, context={'request': request})
|
||||
|
||||
@@ -9,6 +9,7 @@ from django.db import DEFAULT_DB_ALIAS
|
||||
from django.utils import timezone
|
||||
from packaging import version
|
||||
|
||||
from extras.models import JobResult
|
||||
from extras.models import ObjectChange
|
||||
from netbox.config import Config
|
||||
|
||||
@@ -63,6 +64,33 @@ class Command(BaseCommand):
|
||||
f"\tSkipping: No retention period specified (CHANGELOG_RETENTION = {config.CHANGELOG_RETENTION})"
|
||||
)
|
||||
|
||||
# Delete expired JobResults
|
||||
if options['verbosity']:
|
||||
self.stdout.write("[*] Checking for expired jobresult records")
|
||||
if config.JOBRESULT_RETENTION:
|
||||
cutoff = timezone.now() - timedelta(days=config.JOBRESULT_RETENTION)
|
||||
if options['verbosity'] >= 2:
|
||||
self.stdout.write(f"\tRetention period: {config.JOBRESULT_RETENTION} days")
|
||||
self.stdout.write(f"\tCut-off time: {cutoff}")
|
||||
expired_records = JobResult.objects.filter(created__lt=cutoff).count()
|
||||
if expired_records:
|
||||
if options['verbosity']:
|
||||
self.stdout.write(
|
||||
f"\tDeleting {expired_records} expired records... ",
|
||||
self.style.WARNING,
|
||||
ending=""
|
||||
)
|
||||
self.stdout.flush()
|
||||
JobResult.objects.filter(created__lt=cutoff)._raw_delete(using=DEFAULT_DB_ALIAS)
|
||||
if options['verbosity']:
|
||||
self.stdout.write("Done.", self.style.SUCCESS)
|
||||
elif options['verbosity']:
|
||||
self.stdout.write("\tNo expired records found.", self.style.SUCCESS)
|
||||
elif options['verbosity']:
|
||||
self.stdout.write(
|
||||
f"\tSkipping: No retention period specified (JOBRESULT_RETENTION = {config.JOBRESULT_RETENTION})"
|
||||
)
|
||||
|
||||
# Check for new releases (if enabled)
|
||||
if options['verbosity']:
|
||||
self.stdout.write("[*] Checking for latest release")
|
||||
|
||||
@@ -35,7 +35,8 @@ class Command(BaseCommand):
|
||||
run_report,
|
||||
report.full_name,
|
||||
report_content_type,
|
||||
None
|
||||
None,
|
||||
job_timeout=report.job_timeout
|
||||
)
|
||||
|
||||
# Wait on the job to finish
|
||||
|
||||
@@ -113,13 +113,6 @@ class Command(BaseCommand):
|
||||
|
||||
script_content_type = ContentType.objects.get(app_label='extras', model='script')
|
||||
|
||||
# Delete any previous terminal state results
|
||||
JobResult.objects.filter(
|
||||
obj_type=script_content_type,
|
||||
name=script.full_name,
|
||||
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
|
||||
).delete()
|
||||
|
||||
# Create the job result
|
||||
job_result = JobResult.objects.create(
|
||||
name=script.full_name,
|
||||
|
||||
@@ -13,6 +13,7 @@ from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.formats import date_format
|
||||
from rest_framework.utils.encoders import JSONEncoder
|
||||
import django_rq
|
||||
|
||||
from extras.choices import *
|
||||
from extras.constants import *
|
||||
@@ -550,7 +551,8 @@ class JobResult(models.Model):
|
||||
job_id=uuid.uuid4()
|
||||
)
|
||||
|
||||
func.delay(*args, job_id=str(job_result.job_id), job_result=job_result, **kwargs)
|
||||
queue = django_rq.get_queue("default")
|
||||
queue.enqueue(func, job_id=str(job_result.job_id), job_result=job_result, **kwargs)
|
||||
|
||||
return job_result
|
||||
|
||||
|
||||
@@ -84,15 +84,6 @@ def run_report(job_result, *args, **kwargs):
|
||||
job_result.save()
|
||||
logging.error(f"Error during execution of report {job_result.name}")
|
||||
|
||||
# Delete any previous terminal state results
|
||||
JobResult.objects.filter(
|
||||
obj_type=job_result.obj_type,
|
||||
name=job_result.name,
|
||||
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
|
||||
).exclude(
|
||||
pk=job_result.pk
|
||||
).delete()
|
||||
|
||||
|
||||
class Report(object):
|
||||
"""
|
||||
@@ -119,6 +110,7 @@ class Report(object):
|
||||
}
|
||||
"""
|
||||
description = None
|
||||
job_timeout = None
|
||||
|
||||
def __init__(self):
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import os
|
||||
import pkgutil
|
||||
import sys
|
||||
import traceback
|
||||
import threading
|
||||
from collections import OrderedDict
|
||||
|
||||
import yaml
|
||||
@@ -13,11 +14,9 @@ from django.conf import settings
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db import transaction
|
||||
from django.utils.functional import classproperty
|
||||
from django_rq import job
|
||||
|
||||
from extras.api.serializers import ScriptOutputSerializer
|
||||
from extras.choices import JobResultStatusChoices, LogLevelChoices
|
||||
from extras.models import JobResult
|
||||
from ipam.formfields import IPAddressFormField, IPNetworkFormField
|
||||
from ipam.validators import MaxPrefixLengthValidator, MinPrefixLengthValidator, prefix_validator
|
||||
from utilities.exceptions import AbortTransaction
|
||||
@@ -42,6 +41,8 @@ __all__ = [
|
||||
'TextVar',
|
||||
]
|
||||
|
||||
lock = threading.Lock()
|
||||
|
||||
|
||||
#
|
||||
# Script variables
|
||||
@@ -298,6 +299,10 @@ class BaseScript:
|
||||
def module(cls):
|
||||
return cls.__module__
|
||||
|
||||
@classproperty
|
||||
def job_timeout(self):
|
||||
return getattr(self.Meta, 'job_timeout', None)
|
||||
|
||||
@classmethod
|
||||
def _get_vars(cls):
|
||||
vars = {}
|
||||
@@ -414,7 +419,6 @@ def is_variable(obj):
|
||||
return isinstance(obj, ScriptVariable)
|
||||
|
||||
|
||||
@job('default')
|
||||
def run_script(data, request, commit=True, *args, **kwargs):
|
||||
"""
|
||||
A wrapper for calling Script.run(). This performs error handling and provides a hook for committing changes. It
|
||||
@@ -478,15 +482,6 @@ def run_script(data, request, commit=True, *args, **kwargs):
|
||||
else:
|
||||
_run_script()
|
||||
|
||||
# Delete any previous terminal state results
|
||||
JobResult.objects.filter(
|
||||
obj_type=job_result.obj_type,
|
||||
name=job_result.name,
|
||||
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
|
||||
).exclude(
|
||||
pk=job_result.pk
|
||||
).delete()
|
||||
|
||||
|
||||
def get_scripts(use_names=False):
|
||||
"""
|
||||
@@ -494,14 +489,17 @@ def get_scripts(use_names=False):
|
||||
defined name in place of the actual module name.
|
||||
"""
|
||||
scripts = OrderedDict()
|
||||
# Iterate through all modules within the reports path. These are the user-created files in which reports are
|
||||
# Iterate through all modules within the scripts path. These are the user-created files in which reports are
|
||||
# defined.
|
||||
for importer, module_name, _ in pkgutil.iter_modules([settings.SCRIPTS_ROOT]):
|
||||
# Remove cached module to ensure consistency with filesystem
|
||||
if module_name in sys.modules:
|
||||
del sys.modules[module_name]
|
||||
# Use a lock as removing and loading modules is not thread safe
|
||||
with lock:
|
||||
# Remove cached module to ensure consistency with filesystem
|
||||
if module_name in sys.modules:
|
||||
del sys.modules[module_name]
|
||||
|
||||
module = importer.find_module(module_name).load_module(module_name)
|
||||
|
||||
module = importer.find_module(module_name).load_module(module_name)
|
||||
if use_names and hasattr(module, 'name'):
|
||||
module_name = module.name
|
||||
module_scripts = OrderedDict()
|
||||
|
||||
@@ -524,7 +524,7 @@ class ReportListView(ContentTypePermissionRequiredMixin, View):
|
||||
for r in JobResult.objects.filter(
|
||||
obj_type=report_content_type,
|
||||
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
|
||||
).defer('data')
|
||||
).order_by('name', '-created').distinct('name').defer('data')
|
||||
}
|
||||
|
||||
ret = []
|
||||
@@ -588,7 +588,8 @@ class ReportView(ContentTypePermissionRequiredMixin, View):
|
||||
run_report,
|
||||
report.full_name,
|
||||
report_content_type,
|
||||
request.user
|
||||
request.user,
|
||||
job_timeout=report.job_timeout
|
||||
)
|
||||
|
||||
return redirect('extras:report_result', job_result_pk=job_result.pk)
|
||||
@@ -655,7 +656,7 @@ class ScriptListView(ContentTypePermissionRequiredMixin, View):
|
||||
for r in JobResult.objects.filter(
|
||||
obj_type=script_content_type,
|
||||
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
|
||||
).defer('data')
|
||||
).order_by('name', '-created').distinct('name').defer('data')
|
||||
}
|
||||
|
||||
for _scripts in scripts.values():
|
||||
@@ -708,6 +709,7 @@ class ScriptView(ContentTypePermissionRequiredMixin, GetScriptMixin, View):
|
||||
commit = form.cleaned_data.pop('_commit')
|
||||
|
||||
script_content_type = ContentType.objects.get(app_label='extras', model='script')
|
||||
|
||||
job_result = JobResult.enqueue_job(
|
||||
run_script,
|
||||
script.full_name,
|
||||
@@ -715,7 +717,8 @@ class ScriptView(ContentTypePermissionRequiredMixin, GetScriptMixin, View):
|
||||
request.user,
|
||||
data=form.cleaned_data,
|
||||
request=copy_safe_request(request),
|
||||
commit=commit
|
||||
commit=commit,
|
||||
job_timeout=script.job_timeout,
|
||||
)
|
||||
|
||||
return redirect('extras:script_result', job_result_pk=job_result.pk)
|
||||
|
||||
@@ -535,6 +535,11 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
queryset=VMInterface.objects.all(),
|
||||
label='VM interface (ID)',
|
||||
)
|
||||
fhrpgroup_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='fhrpgroup',
|
||||
queryset=FHRPGroup.objects.all(),
|
||||
label='FHRP group (ID)',
|
||||
)
|
||||
assigned_to_interface = django_filters.BooleanFilter(
|
||||
method='_assigned_to_interface',
|
||||
label='Is assigned to an interface',
|
||||
@@ -613,7 +618,17 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
)
|
||||
|
||||
def _assigned_to_interface(self, queryset, name, value):
|
||||
return queryset.exclude(assigned_object_id__isnull=value)
|
||||
content_types = ContentType.objects.get_for_models(Interface, VMInterface).values()
|
||||
if value:
|
||||
return queryset.filter(
|
||||
assigned_object_type__in=content_types,
|
||||
assigned_object_id__isnull=False
|
||||
)
|
||||
else:
|
||||
return queryset.exclude(
|
||||
assigned_object_type__in=content_types,
|
||||
assigned_object_id__isnull=False
|
||||
)
|
||||
|
||||
|
||||
class FHRPGroupFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
@@ -377,12 +377,16 @@ class VLANGroupFilterForm(NetBoxModelFilterSetForm):
|
||||
label=_('Rack')
|
||||
)
|
||||
min_vid = forms.IntegerField(
|
||||
required=False,
|
||||
min_value=VLAN_VID_MIN,
|
||||
max_value=VLAN_VID_MAX,
|
||||
label='Minimum VID'
|
||||
)
|
||||
max_vid = forms.IntegerField(
|
||||
required=False,
|
||||
min_value=VLAN_VID_MIN,
|
||||
max_value=VLAN_VID_MAX,
|
||||
label='Maximum VID'
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# Ensure that VRFs are imported before IPs/prefixes so dumpdata & loaddata work correctly
|
||||
from .fhrp import *
|
||||
from .vrfs import *
|
||||
from .ip import *
|
||||
from .services import *
|
||||
from .vlans import *
|
||||
from .vrfs import *
|
||||
|
||||
__all__ = (
|
||||
'ASN',
|
||||
|
||||
@@ -507,16 +507,20 @@ class Prefix(GetAvailablePrefixesMixin, NetBoxModel):
|
||||
child_ranges.add(iprange.range)
|
||||
available_ips = prefix - child_ips - child_ranges
|
||||
|
||||
# IPv6, pool, or IPv4 /31-/32 sets are fully usable
|
||||
if self.family == 6 or self.is_pool or (self.family == 4 and self.prefix.prefixlen >= 31):
|
||||
# IPv6 /127's, pool, or IPv4 /31-/32 sets are fully usable
|
||||
if (self.family == 6 and self.prefix.prefixlen >= 127) or self.is_pool or (self.family == 4 and self.prefix.prefixlen >= 31):
|
||||
return available_ips
|
||||
|
||||
# For "normal" IPv4 prefixes, omit first and last addresses
|
||||
available_ips -= netaddr.IPSet([
|
||||
netaddr.IPAddress(self.prefix.first),
|
||||
netaddr.IPAddress(self.prefix.last),
|
||||
])
|
||||
|
||||
if self.family == 4:
|
||||
# For "normal" IPv4 prefixes, omit first and last addresses
|
||||
available_ips -= netaddr.IPSet([
|
||||
netaddr.IPAddress(self.prefix.first),
|
||||
netaddr.IPAddress(self.prefix.last),
|
||||
])
|
||||
else:
|
||||
# For IPv6 prefixes, omit the Subnet-Router anycast address
|
||||
# per RFC 4291
|
||||
available_ips -= netaddr.IPSet([netaddr.IPAddress(self.prefix.first)])
|
||||
return available_ips
|
||||
|
||||
def get_first_available_ip(self):
|
||||
|
||||
@@ -771,6 +771,12 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
)
|
||||
VMInterface.objects.bulk_create(vminterfaces)
|
||||
|
||||
fhrp_groups = (
|
||||
FHRPGroup(protocol=FHRPGroupProtocolChoices.PROTOCOL_VRRP2, group_id=101),
|
||||
FHRPGroup(protocol=FHRPGroupProtocolChoices.PROTOCOL_VRRP2, group_id=102),
|
||||
)
|
||||
FHRPGroup.objects.bulk_create(fhrp_groups)
|
||||
|
||||
tenant_groups = (
|
||||
TenantGroup(name='Tenant group 1', slug='tenant-group-1'),
|
||||
TenantGroup(name='Tenant group 2', slug='tenant-group-2'),
|
||||
@@ -791,18 +797,22 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
IPAddress(address='10.0.0.2/24', tenant=tenants[0], vrf=vrfs[0], assigned_object=interfaces[0], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'),
|
||||
IPAddress(address='10.0.0.3/24', tenant=tenants[1], vrf=vrfs[1], assigned_object=interfaces[1], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'),
|
||||
IPAddress(address='10.0.0.4/24', tenant=tenants[2], vrf=vrfs[2], assigned_object=interfaces[2], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'),
|
||||
IPAddress(address='10.0.0.5/24', tenant=None, vrf=None, assigned_object=fhrp_groups[0], status=IPAddressStatusChoices.STATUS_ACTIVE),
|
||||
IPAddress(address='10.0.0.1/25', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE),
|
||||
IPAddress(address='2001:db8::1/64', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a', description='foobar2'),
|
||||
IPAddress(address='2001:db8::2/64', tenant=tenants[0], vrf=vrfs[0], assigned_object=vminterfaces[0], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'),
|
||||
IPAddress(address='2001:db8::3/64', tenant=tenants[1], vrf=vrfs[1], assigned_object=vminterfaces[1], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'),
|
||||
IPAddress(address='2001:db8::4/64', tenant=tenants[2], vrf=vrfs[2], assigned_object=vminterfaces[2], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'),
|
||||
IPAddress(address='2001:db8::5/64', tenant=None, vrf=None, assigned_object=fhrp_groups[1], status=IPAddressStatusChoices.STATUS_ACTIVE),
|
||||
IPAddress(address='2001:db8::1/65', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE),
|
||||
)
|
||||
IPAddress.objects.bulk_create(ipaddresses)
|
||||
|
||||
def test_family(self):
|
||||
params = {'family': '4'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
params = {'family': '6'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
|
||||
def test_dns_name(self):
|
||||
params = {'dns_name': ['ipaddress-a', 'ipaddress-b']}
|
||||
@@ -814,9 +824,9 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
|
||||
def test_parent(self):
|
||||
params = {'parent': '10.0.0.0/24'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
params = {'parent': '2001:db8::/64'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
|
||||
def test_filter_address(self):
|
||||
# Check IPv4 and IPv6, with and without a mask
|
||||
@@ -835,7 +845,7 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
|
||||
def test_mask_length(self):
|
||||
params = {'mask_length': '24'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
|
||||
def test_vrf(self):
|
||||
vrfs = VRF.objects.all()[:2]
|
||||
@@ -872,11 +882,16 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'vminterface': ['Interface 1', 'Interface 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_fhrpgroup(self):
|
||||
fhrp_groups = FHRPGroup.objects.all()[:2]
|
||||
params = {'fhrpgroup_id': [fhrp_groups[0].pk, fhrp_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_assigned_to_interface(self):
|
||||
params = {'assigned_to_interface': 'true'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
params = {'assigned_to_interface': 'false'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
|
||||
def test_status(self):
|
||||
params = {'status': [PrefixStatusChoices.STATUS_DEPRECATED, PrefixStatusChoices.STATUS_RESERVED]}
|
||||
|
||||
@@ -185,6 +185,18 @@ class TestPrefix(TestCase):
|
||||
IPAddress.objects.create(address=IPNetwork('10.0.0.4/24'))
|
||||
self.assertEqual(parent_prefix.get_first_available_ip(), '10.0.0.5/24')
|
||||
|
||||
def test_get_first_available_ip_ipv6(self):
|
||||
parent_prefix = Prefix.objects.create(prefix=IPNetwork('2001:db8:500::/64'))
|
||||
self.assertEqual(parent_prefix.get_first_available_ip(), '2001:db8:500::1/64')
|
||||
|
||||
def test_get_first_available_ip_ipv6_rfc3627(self):
|
||||
parent_prefix = Prefix.objects.create(prefix=IPNetwork('2001:db8:500:4::/126'))
|
||||
self.assertEqual(parent_prefix.get_first_available_ip(), '2001:db8:500:4::1/126')
|
||||
|
||||
def test_get_first_available_ip_ipv6_rfc6164(self):
|
||||
parent_prefix = Prefix.objects.create(prefix=IPNetwork('2001:db8:500:5::/127'))
|
||||
self.assertEqual(parent_prefix.get_first_available_ip(), '2001:db8:500:5::/127')
|
||||
|
||||
def test_get_utilization_container(self):
|
||||
prefixes = (
|
||||
Prefix(prefix=IPNetwork('10.0.0.0/24'), status=PrefixStatusChoices.STATUS_CONTAINER),
|
||||
|
||||
@@ -158,8 +158,8 @@ class RIRView(generic.ObjectView):
|
||||
queryset = RIR.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
aggregates = Aggregate.objects.restrict(request.user, 'view').filter(
|
||||
rir=instance
|
||||
aggregates = Aggregate.objects.restrict(request.user, 'view').filter(rir=instance).annotate(
|
||||
child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ())
|
||||
)
|
||||
aggregates_table = tables.AggregateTable(aggregates, exclude=('rir', 'utilization'))
|
||||
aggregates_table.configure(request)
|
||||
|
||||
@@ -13,8 +13,46 @@ from utilities.permissions import permission_is_exempt, resolve_permission, reso
|
||||
|
||||
UserModel = get_user_model()
|
||||
|
||||
AUTH_BACKEND_ATTRS = {
|
||||
# backend name: title, MDI icon name
|
||||
'amazon': ('Amazon AWS', 'aws'),
|
||||
'apple': ('Apple', 'apple'),
|
||||
'auth0': ('Auth0', None),
|
||||
'azuread-oauth2': ('Microsoft Azure AD', 'microsoft'),
|
||||
'azuread-b2c-oauth2': ('Microsoft Azure AD', 'microsoft'),
|
||||
'azuread-tenant-oauth2': ('Microsoft Azure AD', 'microsoft'),
|
||||
'bitbucket': ('BitBucket', 'bitbucket'),
|
||||
'bitbucket-oauth2': ('BitBucket', 'bitbucket'),
|
||||
'digitalocean': ('DigitalOcean', 'digital-ocean'),
|
||||
'docker': ('Docker', 'docker'),
|
||||
'github': ('GitHub', 'docker'),
|
||||
'github-app': ('GitHub', 'github'),
|
||||
'github-org': ('GitHub', 'github'),
|
||||
'github-team': ('GitHub', 'github'),
|
||||
'github-enterprise': ('GitHub Enterprise', 'github'),
|
||||
'github-enterprise-org': ('GitHub Enterprise', 'github'),
|
||||
'github-enterprise-team': ('GitHub Enterprise', 'github'),
|
||||
'gitlab': ('GitLab', 'gitlab'),
|
||||
'google-oauth2': ('Google', 'google'),
|
||||
'google-openidconnect': ('Google', 'google'),
|
||||
'hubspot': ('HubSpot', 'hubspot'),
|
||||
'keycloak': ('Keycloak', None),
|
||||
'microsoft-graph': ('Microsoft Graph', 'microsoft'),
|
||||
'okta': ('Okta', None),
|
||||
'okta-openidconnect': ('Okta (OIDC)', None),
|
||||
'salesforce-oauth2': ('Salesforce', 'salesforce'),
|
||||
}
|
||||
|
||||
class ObjectPermissionMixin():
|
||||
|
||||
def get_auth_backend_display(name):
|
||||
"""
|
||||
Return the user-friendly name and icon name for a remote authentication backend, if known. Defaults to the
|
||||
raw backend name and no icon.
|
||||
"""
|
||||
return AUTH_BACKEND_ATTRS.get(name, (name, None))
|
||||
|
||||
|
||||
class ObjectPermissionMixin:
|
||||
|
||||
def get_all_permissions(self, user_obj, obj=None):
|
||||
if not user_obj.is_active or user_obj.is_anonymous:
|
||||
|
||||
@@ -187,6 +187,13 @@ PARAMS = (
|
||||
description="Days to retain changelog history (set to zero for unlimited)",
|
||||
field=forms.IntegerField
|
||||
),
|
||||
ConfigParam(
|
||||
name='JOBRESULT_RETENTION',
|
||||
label='Job result retention',
|
||||
default=90,
|
||||
description="Days to retain job result history (set to zero for unlimited)",
|
||||
field=forms.IntegerField
|
||||
),
|
||||
ConfigParam(
|
||||
name='MAPS_URL',
|
||||
label='Maps URL',
|
||||
|
||||
@@ -61,6 +61,8 @@ class NetBoxModelCSVForm(CSVModelForm, NetBoxModelForm):
|
||||
"""
|
||||
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):
|
||||
return customfield.to_form_field(for_csv_import=True)
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ django.utils.encoding.force_text = force_str
|
||||
# Environment setup
|
||||
#
|
||||
|
||||
VERSION = '3.2.0'
|
||||
VERSION = '3.2.2'
|
||||
|
||||
# Hostname
|
||||
HOSTNAME = platform.node()
|
||||
|
||||
@@ -19,8 +19,7 @@ from circuits.models import Circuit, Provider
|
||||
from dcim.models import (
|
||||
Cable, ConsolePort, Device, DeviceType, Interface, PowerPanel, PowerFeed, PowerPort, Rack, Site,
|
||||
)
|
||||
from extras.choices import JobResultStatusChoices
|
||||
from extras.models import ObjectChange, JobResult
|
||||
from extras.models import ObjectChange
|
||||
from extras.tables import ObjectChangeTable
|
||||
from ipam.models import Aggregate, IPAddress, IPRange, Prefix, VLAN, VRF
|
||||
from netbox.constants import SEARCH_MAX_RESULTS, SEARCH_TYPES
|
||||
@@ -48,13 +47,6 @@ class HomeView(View):
|
||||
pk__lt=F('_path__destination_id')
|
||||
)
|
||||
|
||||
# Report Results
|
||||
report_content_type = ContentType.objects.get(app_label='extras', model='report')
|
||||
report_results = JobResult.objects.filter(
|
||||
obj_type=report_content_type,
|
||||
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
|
||||
).defer('data')[:10]
|
||||
|
||||
def build_stats():
|
||||
org = (
|
||||
("dcim.view_site", "Sites", Site.objects.restrict(request.user, 'view').count),
|
||||
@@ -150,7 +142,6 @@ class HomeView(View):
|
||||
return render(request, self.template_name, {
|
||||
'search_form': SearchForm(),
|
||||
'stats': build_stats(),
|
||||
'report_results': report_results,
|
||||
'changelog_table': changelog_table,
|
||||
'new_release': new_release,
|
||||
})
|
||||
|
||||
2
netbox/project-static/dist/netbox.js
vendored
2
netbox/project-static/dist/netbox.js.map
vendored
@@ -570,8 +570,9 @@ export class APISelect {
|
||||
* additional paginated options.
|
||||
*/
|
||||
private handleScroll(): void {
|
||||
// Floor scrollTop as chrome can return fractions on some zoom levels.
|
||||
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;
|
||||
|
||||
if (this.atBottom && !atBottom) {
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceConsolePortTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceConsolePortTable_config" %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body" id="object_list">
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceConsoleServerPortTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceConsoleServerPortTable_config" %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body" id="object_list">
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceDeviceBayTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceDeviceBayTable_config" %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body" id="object_list">
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceFrontPortTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceFrontPortTable_config" %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body" id="object_list">
|
||||
|
||||
@@ -4,44 +4,46 @@
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="row mb-3 justify-content-between">
|
||||
<div class="col col-12 col-lg-4 my-3 my-lg-0 d-flex noprint table-controls">
|
||||
<div class="input-group input-group-sm">
|
||||
<input
|
||||
type="text"
|
||||
name="q"
|
||||
class="form-control"
|
||||
placeholder="Quick search"
|
||||
hx-get="{{ request.full_path }}"
|
||||
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 class="row mb-3 justify-content-between">
|
||||
<div class="col col-12 col-lg-4 my-3 my-lg-0 d-flex noprint table-controls">
|
||||
<div class="input-group input-group-sm">
|
||||
<input
|
||||
type="text"
|
||||
name="q"
|
||||
class="form-control"
|
||||
placeholder="Quick search"
|
||||
hx-get="{{ request.full_path }}"
|
||||
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>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body" id="object_list">
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceInventoryItemTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceInventoryItemTable_config" %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body" id="object_list">
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceModuleBayTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceModuleBayTable_config" %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body" id="object_list">
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DevicePowerOutletTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DevicePowerOutletTable_config" %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body" id="object_list">
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DevicePowerPortTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DevicePowerPortTable_config" %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body" id="object_list">
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceRearPortTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceRearPortTable_config" %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body" id="object_list">
|
||||
|
||||
@@ -32,7 +32,11 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Circuit</td>
|
||||
<td>{{ termination.|linkify }} ({{ termination }})</td>
|
||||
<td>{{ termination.circuit|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Termination</td>
|
||||
<td>{{ termination }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
|
||||
@@ -1,40 +1,54 @@
|
||||
{% load helpers %}
|
||||
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Non-Racked Devices
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
{% if nonracked_devices %}
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Role</th>
|
||||
<th>Type</th>
|
||||
<th colspan="2">Parent Device</th>
|
||||
</tr>
|
||||
{% for device in nonracked_devices %}
|
||||
<tr{% if device.device_type.u_height %} class="warning"{% endif %}>
|
||||
<td>
|
||||
<a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a>
|
||||
</td>
|
||||
<td>{{ device.device_role }}</td>
|
||||
<td>{{ device.device_type }}</td>
|
||||
{% if device.parent_bay %}
|
||||
<td>{{ device.parent_bay.device|linkify }}</td>
|
||||
<td>{{ device.parent_bay }}</td>
|
||||
{% else %}
|
||||
<td colspan="2" class="text-muted">—</td>
|
||||
<h5 class="card-header">
|
||||
Non-Racked Devices
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
{% if nonracked_devices %}
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Role</th>
|
||||
<th>Type</th>
|
||||
<th colspan="2">Parent Device</th>
|
||||
</tr>
|
||||
{% for device in nonracked_devices %}
|
||||
<tr{% if device.device_type.u_height %} class="warning"{% endif %}>
|
||||
<td>
|
||||
<a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a>
|
||||
</td>
|
||||
<td>{{ device.device_role }}</td>
|
||||
<td>{{ device.device_type }}</td>
|
||||
{% if device.parent_bay %}
|
||||
<td>{{ device.parent_bay.device|linkify }}</td>
|
||||
<td>{{ device.parent_bay }}</td>
|
||||
{% else %}
|
||||
<td colspan="2" class="text-muted">—</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
{% if total_nonracked_devices_count > nonracked_devices.count %}
|
||||
{% if object|meta:'verbose_name' == 'site' %}
|
||||
<div class="text-muted">
|
||||
Displaying {{ nonracked_devices.count }} of {{ total_nonracked_devices_count }} devices (<a href="{% url 'dcim:device_list' %}?site_id={{ object.pk }}&rack_id=null">View full list</a>)
|
||||
</div>
|
||||
{% elif object|meta:'verbose_name' == 'location' %}
|
||||
<div class="text-muted">
|
||||
Displaying {{ nonracked_devices.count }} of {{ total_nonracked_devices_count }} devices (<a href="{% url 'dcim:device_list' %}?location_id={{ object.pk }}&rack_id=null">View full list</a>)
|
||||
</div>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<div class="text-muted">
|
||||
None
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if perms.dcim.add_device %}
|
||||
{% if object|meta:'verbose_name' == 'rack' %}
|
||||
<div class="card-footer text-end noprint">
|
||||
|
||||
@@ -52,6 +52,10 @@
|
||||
{% if object.installed_module %}
|
||||
{% with module=object.installed_module %}
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Module</th>
|
||||
<td>{{ module|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Manufacturer</th>
|
||||
<td>{{ module.module_type.manufacturer|linkify }}</td>
|
||||
@@ -60,6 +64,14 @@
|
||||
<th scope="row">Module Type</th>
|
||||
<td>{{ module.module_type|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Serial Number</th>
|
||||
<td class="font-monospace">{{ module.serial|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Asset Tag</th>
|
||||
<td class="font-monospace">{{ module.asset_tag|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
|
||||
@@ -188,6 +188,16 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">VLAN Groups</th>
|
||||
<td class="text-end">
|
||||
{% if stats.vlangroup_count %}
|
||||
<a href="{% url 'ipam:vlangroup_list' %}?site={{ object.pk }}">{{ stats.vlangroup_count }}</a>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">VLANs</th>
|
||||
<td class="text-end">
|
||||
|
||||
@@ -21,7 +21,10 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
<th scope="row">Description</th>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<small><a href="{{ new_release.url }}">NetBox v{{ new_release.version }}</a> is available.</small>
|
||||
<hr class="my-2" />
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="PrefixTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="PrefixTable_config" %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body" id="object_list">
|
||||
|
||||
@@ -10,9 +10,10 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="IPAddressTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="IPAddressTable_config" %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body" id="object_list">
|
||||
|
||||
@@ -10,9 +10,10 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="IPAddressTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="IPAddressTable_config" %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body" id="object_list">
|
||||
|
||||
@@ -10,9 +10,10 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="IPRangeTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="IPRangeTable_config" %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body" id="object_list">
|
||||
|
||||