Merge branch 'develop' into feature
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -14,7 +14,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox version
|
label: NetBox version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v3.2.0
|
placeholder: v3.2.1
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -14,7 +14,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox version
|
label: NetBox version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v3.2.0
|
placeholder: v3.2.1
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
79
docs/administration/authentication/microsoft-azure-ad.md
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
!!! note "Troubleshooting"
|
||||||
|
If you are redirected to the NetBox UI after authenticating, 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.
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
Local user accounts and groups can be created in NetBox under the "Authentication and Authorization" section of the administrative user interface. This interface is available only to users with the "staff" permission enabled.
|
Local user accounts and groups can be created in NetBox under the "Authentication and Authorization" section of the administrative user interface. This interface is available only to users with the "staff" permission enabled.
|
||||||
|
|
||||||
At a minimum, each user account must have a username and password set. User accounts may also denote a first name, last name, and email address. [Permissions](./permissions.md) may also be assigned to users and/or groups within the admin UI.
|
At a minimum, each user account must have a username and password set. User accounts may also denote a first name, last name, and email address. [Permissions](../permissions.md) may also be assigned to users and/or groups within the admin UI.
|
||||||
|
|
||||||
## Remote Authentication
|
## Remote Authentication
|
||||||
|
|
||||||
@ -16,7 +16,7 @@ NetBox may be configured to provide user authenticate via a remote backend in ad
|
|||||||
REMOTE_AUTH_BACKEND = 'netbox.authentication.LDAPBackend'
|
REMOTE_AUTH_BACKEND = 'netbox.authentication.LDAPBackend'
|
||||||
```
|
```
|
||||||
|
|
||||||
NetBox includes an authentication backend which supports LDAP. See the [LDAP installation docs](../installation/6-ldap.md) for more detail about this backend.
|
NetBox includes an authentication backend which supports LDAP. See the [LDAP installation docs](../../installation/6-ldap.md) for more detail about this backend.
|
||||||
|
|
||||||
### HTTP Header Authentication
|
### HTTP Header Authentication
|
||||||
|
|
@ -4,6 +4,7 @@ NetBox includes a `housekeeping` management command that should be run nightly.
|
|||||||
|
|
||||||
* Clearing expired authentication sessions from the database
|
* Clearing expired authentication sessions from the database
|
||||||
* Deleting changelog records older than the configured [retention time](../configuration/dynamic-settings.md#changelog_retention)
|
* 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.
|
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
|
## 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:
|
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:
|
||||||
|
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 |
@ -1,20 +1,40 @@
|
|||||||
# NetBox v3.2
|
# NetBox v3.2
|
||||||
|
|
||||||
## v3.2.1 (FUTURE)
|
## v3.2.2 (FUTURE)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#9133](https://github.com/netbox-community/netbox/issues/9133) - Upgrade script should require Python 3.8 or later
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v3.2.1 (2022-04-14)
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
* [#5479](https://github.com/netbox-community/netbox/issues/5479) - Allow custom job timeouts for scripts & reports
|
* [#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
|
* [#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
|
### Bug Fixes
|
||||||
|
|
||||||
* [#8931](https://github.com/netbox-community/netbox/issues/8931) - Copy assigned tenant when cloning a location
|
* [#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
|
* [#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
|
* [#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) - Change inheritance order for DeviceComponentFilterSets
|
* [#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
|
* [#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
|
* [#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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ theme:
|
|||||||
icon: material/lightbulb
|
icon: material/lightbulb
|
||||||
name: Switch to Light Mode
|
name: Switch to Light Mode
|
||||||
plugins:
|
plugins:
|
||||||
|
- search
|
||||||
- mkdocstrings:
|
- mkdocstrings:
|
||||||
handlers:
|
handlers:
|
||||||
python:
|
python:
|
||||||
@ -117,7 +118,9 @@ nav:
|
|||||||
- GraphQL API: 'plugins/development/graphql-api.md'
|
- GraphQL API: 'plugins/development/graphql-api.md'
|
||||||
- Background Tasks: 'plugins/development/background-tasks.md'
|
- Background Tasks: 'plugins/development/background-tasks.md'
|
||||||
- Administration:
|
- Administration:
|
||||||
- Authentication: 'administration/authentication.md'
|
- Authentication:
|
||||||
|
- Overview: 'administration/authentication/overview.md'
|
||||||
|
- Microsoft Azure AD: 'administration/authentication/microsoft-azure-ad.md'
|
||||||
- Permissions: 'administration/permissions.md'
|
- Permissions: 'administration/permissions.md'
|
||||||
- Housekeeping: 'administration/housekeeping.md'
|
- Housekeeping: 'administration/housekeeping.md'
|
||||||
- Replicating NetBox: 'administration/replicating-netbox.md'
|
- Replicating NetBox: 'administration/replicating-netbox.md'
|
||||||
|
@ -345,6 +345,10 @@ class PowerPortTypeChoices(ChoiceSet):
|
|||||||
TYPE_DC = 'dc-terminal'
|
TYPE_DC = 'dc-terminal'
|
||||||
# Proprietary
|
# Proprietary
|
||||||
TYPE_SAF_D_GRID = 'saf-d-grid'
|
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'
|
||||||
# Other
|
# Other
|
||||||
TYPE_HARDWIRED = 'hardwired'
|
TYPE_HARDWIRED = 'hardwired'
|
||||||
|
|
||||||
@ -456,6 +460,10 @@ class PowerPortTypeChoices(ChoiceSet):
|
|||||||
)),
|
)),
|
||||||
('Proprietary', (
|
('Proprietary', (
|
||||||
(TYPE_SAF_D_GRID, 'Saf-D-Grid'),
|
(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', (
|
('Other', (
|
||||||
(TYPE_HARDWIRED, 'Hardwired'),
|
(TYPE_HARDWIRED, 'Hardwired'),
|
||||||
@ -561,6 +569,10 @@ class PowerOutletTypeChoices(ChoiceSet):
|
|||||||
# Proprietary
|
# Proprietary
|
||||||
TYPE_HDOT_CX = 'hdot-cx'
|
TYPE_HDOT_CX = 'hdot-cx'
|
||||||
TYPE_SAF_D_GRID = 'saf-d-grid'
|
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
|
# Other
|
||||||
TYPE_HARDWIRED = 'hardwired'
|
TYPE_HARDWIRED = 'hardwired'
|
||||||
|
|
||||||
@ -665,6 +677,10 @@ class PowerOutletTypeChoices(ChoiceSet):
|
|||||||
('Proprietary', (
|
('Proprietary', (
|
||||||
(TYPE_HDOT_CX, 'HDOT Cx'),
|
(TYPE_HDOT_CX, 'HDOT Cx'),
|
||||||
(TYPE_SAF_D_GRID, 'Saf-D-Grid'),
|
(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', (
|
('Other', (
|
||||||
(TYPE_HARDWIRED, 'Hardwired'),
|
(TYPE_HARDWIRED, 'Hardwired'),
|
||||||
|
@ -651,11 +651,11 @@ class InterfaceCSVForm(NetBoxModelCSVForm):
|
|||||||
super().__init__(data, *args, **kwargs)
|
super().__init__(data, *args, **kwargs)
|
||||||
|
|
||||||
if data:
|
if data:
|
||||||
# Limit interface choices for parent, bridge and lag to device only
|
# Limit choices for parent, bridge, and LAG interfaces to the assigned device
|
||||||
params = {}
|
if device := data.get('device'):
|
||||||
if data.get('device'):
|
params = {
|
||||||
params[f"device__{self.fields['device'].to_field_name}"] = data.get('device')
|
f"device__{self.fields['device'].to_field_name}": device
|
||||||
if params:
|
}
|
||||||
self.fields['parent'].queryset = self.fields['parent'].queryset.filter(**params)
|
self.fields['parent'].queryset = self.fields['parent'].queryset.filter(**params)
|
||||||
self.fields['bridge'].queryset = self.fields['bridge'].queryset.filter(**params)
|
self.fields['bridge'].queryset = self.fields['bridge'].queryset.filter(**params)
|
||||||
self.fields['lag'].queryset = self.fields['lag'].queryset.filter(**params)
|
self.fields['lag'].queryset = self.fields['lag'].queryset.filter(**params)
|
||||||
|
@ -124,6 +124,11 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
|
|||||||
return self.name.replace('{module}', module.module_bay.position)
|
return self.name.replace('{module}', module.module_bay.position)
|
||||||
return self.name
|
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):
|
class ConsolePortTemplate(ModularComponentTemplateModel):
|
||||||
"""
|
"""
|
||||||
@ -147,7 +152,7 @@ class ConsolePortTemplate(ModularComponentTemplateModel):
|
|||||||
def instantiate(self, **kwargs):
|
def instantiate(self, **kwargs):
|
||||||
return self.component_model(
|
return self.component_model(
|
||||||
name=self.resolve_name(kwargs.get('module')),
|
name=self.resolve_name(kwargs.get('module')),
|
||||||
label=self.label,
|
label=self.resolve_label(kwargs.get('module')),
|
||||||
type=self.type,
|
type=self.type,
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
@ -175,7 +180,7 @@ class ConsoleServerPortTemplate(ModularComponentTemplateModel):
|
|||||||
def instantiate(self, **kwargs):
|
def instantiate(self, **kwargs):
|
||||||
return self.component_model(
|
return self.component_model(
|
||||||
name=self.resolve_name(kwargs.get('module')),
|
name=self.resolve_name(kwargs.get('module')),
|
||||||
label=self.label,
|
label=self.resolve_label(kwargs.get('module')),
|
||||||
type=self.type,
|
type=self.type,
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
@ -215,7 +220,7 @@ class PowerPortTemplate(ModularComponentTemplateModel):
|
|||||||
def instantiate(self, **kwargs):
|
def instantiate(self, **kwargs):
|
||||||
return self.component_model(
|
return self.component_model(
|
||||||
name=self.resolve_name(kwargs.get('module')),
|
name=self.resolve_name(kwargs.get('module')),
|
||||||
label=self.label,
|
label=self.resolve_label(kwargs.get('module')),
|
||||||
type=self.type,
|
type=self.type,
|
||||||
maximum_draw=self.maximum_draw,
|
maximum_draw=self.maximum_draw,
|
||||||
allocated_draw=self.allocated_draw,
|
allocated_draw=self.allocated_draw,
|
||||||
@ -286,7 +291,7 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
|
|||||||
power_port = None
|
power_port = None
|
||||||
return self.component_model(
|
return self.component_model(
|
||||||
name=self.resolve_name(kwargs.get('module')),
|
name=self.resolve_name(kwargs.get('module')),
|
||||||
label=self.label,
|
label=self.resolve_label(kwargs.get('module')),
|
||||||
type=self.type,
|
type=self.type,
|
||||||
power_port=power_port,
|
power_port=power_port,
|
||||||
feed_leg=self.feed_leg,
|
feed_leg=self.feed_leg,
|
||||||
@ -326,7 +331,7 @@ class InterfaceTemplate(ModularComponentTemplateModel):
|
|||||||
def instantiate(self, **kwargs):
|
def instantiate(self, **kwargs):
|
||||||
return self.component_model(
|
return self.component_model(
|
||||||
name=self.resolve_name(kwargs.get('module')),
|
name=self.resolve_name(kwargs.get('module')),
|
||||||
label=self.label,
|
label=self.resolve_label(kwargs.get('module')),
|
||||||
type=self.type,
|
type=self.type,
|
||||||
mgmt_only=self.mgmt_only,
|
mgmt_only=self.mgmt_only,
|
||||||
**kwargs
|
**kwargs
|
||||||
@ -397,7 +402,7 @@ class FrontPortTemplate(ModularComponentTemplateModel):
|
|||||||
rear_port = None
|
rear_port = None
|
||||||
return self.component_model(
|
return self.component_model(
|
||||||
name=self.resolve_name(kwargs.get('module')),
|
name=self.resolve_name(kwargs.get('module')),
|
||||||
label=self.label,
|
label=self.resolve_label(kwargs.get('module')),
|
||||||
type=self.type,
|
type=self.type,
|
||||||
color=self.color,
|
color=self.color,
|
||||||
rear_port=rear_port,
|
rear_port=rear_port,
|
||||||
@ -437,7 +442,7 @@ class RearPortTemplate(ModularComponentTemplateModel):
|
|||||||
def instantiate(self, **kwargs):
|
def instantiate(self, **kwargs):
|
||||||
return self.component_model(
|
return self.component_model(
|
||||||
name=self.resolve_name(kwargs.get('module')),
|
name=self.resolve_name(kwargs.get('module')),
|
||||||
label=self.label,
|
label=self.resolve_label(kwargs.get('module')),
|
||||||
type=self.type,
|
type=self.type,
|
||||||
color=self.color,
|
color=self.color,
|
||||||
positions=self.positions,
|
positions=self.positions,
|
||||||
|
@ -257,6 +257,7 @@ class DeviceType(NetBoxModel):
|
|||||||
{
|
{
|
||||||
'name': c.name,
|
'name': c.name,
|
||||||
'label': c.label,
|
'label': c.label,
|
||||||
|
'position': c.position,
|
||||||
'description': c.description,
|
'description': c.description,
|
||||||
}
|
}
|
||||||
for c in self.modulebaytemplates.all()
|
for c in self.modulebaytemplates.all()
|
||||||
|
@ -739,13 +739,22 @@ class ModuleBayTable(DeviceComponentTable):
|
|||||||
linkify=True,
|
linkify=True,
|
||||||
verbose_name='Installed module'
|
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(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:modulebay_list'
|
url_name='dcim:modulebay_list'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
model = ModuleBay
|
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')
|
default_columns = ('pk', 'name', 'device', 'label', 'installed_module', 'description')
|
||||||
|
|
||||||
|
|
||||||
@ -756,7 +765,10 @@ class DeviceModuleBayTable(ModuleBayTable):
|
|||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
model = ModuleBay
|
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')
|
default_columns = ('pk', 'name', 'label', 'installed_module', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
@ -342,6 +342,7 @@ class SiteView(generic.ObjectView):
|
|||||||
'device_count',
|
'device_count',
|
||||||
cumulative=True
|
cumulative=True
|
||||||
).restrict(request.user, 'view').filter(site=instance)
|
).restrict(request.user, 'view').filter(site=instance)
|
||||||
|
|
||||||
nonracked_devices = Device.objects.filter(
|
nonracked_devices = Device.objects.filter(
|
||||||
site=instance,
|
site=instance,
|
||||||
position__isnull=True,
|
position__isnull=True,
|
||||||
@ -357,7 +358,8 @@ class SiteView(generic.ObjectView):
|
|||||||
'stats': stats,
|
'stats': stats,
|
||||||
'locations': locations,
|
'locations': locations,
|
||||||
'asns': asns,
|
'asns': asns,
|
||||||
'nonracked_devices': nonracked_devices,
|
'nonracked_devices': nonracked_devices.order_by('-pk')[:10],
|
||||||
|
'total_nonracked_devices_count': nonracked_devices.count(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -435,6 +437,7 @@ class LocationView(generic.ObjectView):
|
|||||||
).filter(pk__in=location_ids).exclude(pk=instance.pk)
|
).filter(pk__in=location_ids).exclude(pk=instance.pk)
|
||||||
child_locations_table = tables.LocationTable(child_locations)
|
child_locations_table = tables.LocationTable(child_locations)
|
||||||
child_locations_table.configure(request)
|
child_locations_table.configure(request)
|
||||||
|
|
||||||
nonracked_devices = Device.objects.filter(
|
nonracked_devices = Device.objects.filter(
|
||||||
location=instance,
|
location=instance,
|
||||||
position__isnull=True,
|
position__isnull=True,
|
||||||
@ -445,7 +448,8 @@ class LocationView(generic.ObjectView):
|
|||||||
'rack_count': rack_count,
|
'rack_count': rack_count,
|
||||||
'device_count': device_count,
|
'device_count': device_count,
|
||||||
'child_locations_table': child_locations_table,
|
'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(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ class ConfigRevisionAdmin(admin.ModelAdmin):
|
|||||||
'fields': ('DEFAULT_USER_PREFERENCES',),
|
'fields': ('DEFAULT_USER_PREFERENCES',),
|
||||||
}),
|
}),
|
||||||
('Miscellaneous', {
|
('Miscellaneous', {
|
||||||
'fields': ('MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'MAPS_URL'),
|
'fields': ('MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'JOBRESULT_RETENTION', 'MAPS_URL'),
|
||||||
}),
|
}),
|
||||||
('Config Revision', {
|
('Config Revision', {
|
||||||
'fields': ('comment',),
|
'fields': ('comment',),
|
||||||
|
@ -179,7 +179,7 @@ class ReportViewSet(ViewSet):
|
|||||||
for r in JobResult.objects.filter(
|
for r in JobResult.objects.filter(
|
||||||
obj_type=report_content_type,
|
obj_type=report_content_type,
|
||||||
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
|
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
|
||||||
).defer('data')
|
).order_by('name', '-created').distinct('name').defer('data')
|
||||||
}
|
}
|
||||||
|
|
||||||
# Iterate through all available Reports.
|
# Iterate through all available Reports.
|
||||||
@ -271,7 +271,7 @@ class ScriptViewSet(ViewSet):
|
|||||||
for r in JobResult.objects.filter(
|
for r in JobResult.objects.filter(
|
||||||
obj_type=script_content_type,
|
obj_type=script_content_type,
|
||||||
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
|
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
|
||||||
).defer('data').order_by('created')
|
).order_by('name', '-created').distinct('name').defer('data')
|
||||||
}
|
}
|
||||||
|
|
||||||
flat_list = []
|
flat_list = []
|
||||||
|
@ -9,6 +9,7 @@ from django.db import DEFAULT_DB_ALIAS
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from packaging import version
|
from packaging import version
|
||||||
|
|
||||||
|
from extras.models import JobResult
|
||||||
from extras.models import ObjectChange
|
from extras.models import ObjectChange
|
||||||
from netbox.config import Config
|
from netbox.config import Config
|
||||||
|
|
||||||
@ -63,6 +64,33 @@ class Command(BaseCommand):
|
|||||||
f"\tSkipping: No retention period specified (CHANGELOG_RETENTION = {config.CHANGELOG_RETENTION})"
|
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)
|
# Check for new releases (if enabled)
|
||||||
if options['verbosity']:
|
if options['verbosity']:
|
||||||
self.stdout.write("[*] Checking for latest release")
|
self.stdout.write("[*] Checking for latest release")
|
||||||
|
@ -113,13 +113,6 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
script_content_type = ContentType.objects.get(app_label='extras', model='script')
|
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
|
# Create the job result
|
||||||
job_result = JobResult.objects.create(
|
job_result = JobResult.objects.create(
|
||||||
name=script.full_name,
|
name=script.full_name,
|
||||||
|
@ -84,15 +84,6 @@ def run_report(job_result, *args, **kwargs):
|
|||||||
job_result.save()
|
job_result.save()
|
||||||
logging.error(f"Error during execution of report {job_result.name}")
|
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):
|
class Report(object):
|
||||||
"""
|
"""
|
||||||
|
@ -481,15 +481,6 @@ def run_script(data, request, commit=True, *args, **kwargs):
|
|||||||
else:
|
else:
|
||||||
_run_script()
|
_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):
|
def get_scripts(use_names=False):
|
||||||
"""
|
"""
|
||||||
@ -497,7 +488,7 @@ def get_scripts(use_names=False):
|
|||||||
defined name in place of the actual module name.
|
defined name in place of the actual module name.
|
||||||
"""
|
"""
|
||||||
scripts = OrderedDict()
|
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.
|
# defined.
|
||||||
for importer, module_name, _ in pkgutil.iter_modules([settings.SCRIPTS_ROOT]):
|
for importer, module_name, _ in pkgutil.iter_modules([settings.SCRIPTS_ROOT]):
|
||||||
# Remove cached module to ensure consistency with filesystem
|
# Remove cached module to ensure consistency with filesystem
|
||||||
|
@ -524,7 +524,7 @@ class ReportListView(ContentTypePermissionRequiredMixin, View):
|
|||||||
for r in JobResult.objects.filter(
|
for r in JobResult.objects.filter(
|
||||||
obj_type=report_content_type,
|
obj_type=report_content_type,
|
||||||
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
|
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
|
||||||
).defer('data')
|
).order_by('name', '-created').distinct('name').defer('data')
|
||||||
}
|
}
|
||||||
|
|
||||||
ret = []
|
ret = []
|
||||||
@ -656,7 +656,7 @@ class ScriptListView(ContentTypePermissionRequiredMixin, View):
|
|||||||
for r in JobResult.objects.filter(
|
for r in JobResult.objects.filter(
|
||||||
obj_type=script_content_type,
|
obj_type=script_content_type,
|
||||||
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
|
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
|
||||||
).defer('data')
|
).order_by('name', '-created').distinct('name').defer('data')
|
||||||
}
|
}
|
||||||
|
|
||||||
for _scripts in scripts.values():
|
for _scripts in scripts.values():
|
||||||
|
@ -535,6 +535,11 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
|||||||
queryset=VMInterface.objects.all(),
|
queryset=VMInterface.objects.all(),
|
||||||
label='VM interface (ID)',
|
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(
|
assigned_to_interface = django_filters.BooleanFilter(
|
||||||
method='_assigned_to_interface',
|
method='_assigned_to_interface',
|
||||||
label='Is assigned to an interface',
|
label='Is assigned to an interface',
|
||||||
@ -613,7 +618,17 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _assigned_to_interface(self, queryset, name, value):
|
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):
|
class FHRPGroupFilterSet(NetBoxModelFilterSet):
|
||||||
|
@ -771,6 +771,12 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
)
|
)
|
||||||
VMInterface.objects.bulk_create(vminterfaces)
|
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 = (
|
tenant_groups = (
|
||||||
TenantGroup(name='Tenant group 1', slug='tenant-group-1'),
|
TenantGroup(name='Tenant group 1', slug='tenant-group-1'),
|
||||||
TenantGroup(name='Tenant group 2', slug='tenant-group-2'),
|
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.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.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.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='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::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::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::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::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(address='2001:db8::1/65', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE),
|
||||||
)
|
)
|
||||||
IPAddress.objects.bulk_create(ipaddresses)
|
IPAddress.objects.bulk_create(ipaddresses)
|
||||||
|
|
||||||
def test_family(self):
|
def test_family(self):
|
||||||
|
params = {'family': '4'}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||||
params = {'family': '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):
|
def test_dns_name(self):
|
||||||
params = {'dns_name': ['ipaddress-a', 'ipaddress-b']}
|
params = {'dns_name': ['ipaddress-a', 'ipaddress-b']}
|
||||||
@ -814,9 +824,9 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
|
|
||||||
def test_parent(self):
|
def test_parent(self):
|
||||||
params = {'parent': '10.0.0.0/24'}
|
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'}
|
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):
|
def test_filter_address(self):
|
||||||
# Check IPv4 and IPv6, with and without a mask
|
# Check IPv4 and IPv6, with and without a mask
|
||||||
@ -835,7 +845,7 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
|
|
||||||
def test_mask_length(self):
|
def test_mask_length(self):
|
||||||
params = {'mask_length': '24'}
|
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):
|
def test_vrf(self):
|
||||||
vrfs = VRF.objects.all()[:2]
|
vrfs = VRF.objects.all()[:2]
|
||||||
@ -872,11 +882,16 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'vminterface': ['Interface 1', 'Interface 2']}
|
params = {'vminterface': ['Interface 1', 'Interface 2']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 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):
|
def test_assigned_to_interface(self):
|
||||||
params = {'assigned_to_interface': 'true'}
|
params = {'assigned_to_interface': 'true'}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||||
params = {'assigned_to_interface': 'false'}
|
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):
|
def test_status(self):
|
||||||
params = {'status': [PrefixStatusChoices.STATUS_DEPRECATED, PrefixStatusChoices.STATUS_RESERVED]}
|
params = {'status': [PrefixStatusChoices.STATUS_DEPRECATED, PrefixStatusChoices.STATUS_RESERVED]}
|
||||||
|
@ -13,8 +13,45 @@ from utilities.permissions import permission_is_exempt, resolve_permission, reso
|
|||||||
|
|
||||||
UserModel = get_user_model()
|
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),
|
||||||
|
'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):
|
def get_all_permissions(self, user_obj, obj=None):
|
||||||
if not user_obj.is_active or user_obj.is_anonymous:
|
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)",
|
description="Days to retain changelog history (set to zero for unlimited)",
|
||||||
field=forms.IntegerField
|
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(
|
ConfigParam(
|
||||||
name='MAPS_URL',
|
name='MAPS_URL',
|
||||||
label='Maps URL',
|
label='Maps URL',
|
||||||
|
@ -19,8 +19,7 @@ from circuits.models import Circuit, Provider
|
|||||||
from dcim.models import (
|
from dcim.models import (
|
||||||
Cable, ConsolePort, Device, DeviceType, Interface, PowerPanel, PowerFeed, PowerPort, Rack, Site,
|
Cable, ConsolePort, Device, DeviceType, Interface, PowerPanel, PowerFeed, PowerPort, Rack, Site,
|
||||||
)
|
)
|
||||||
from extras.choices import JobResultStatusChoices
|
from extras.models import ObjectChange
|
||||||
from extras.models import ObjectChange, JobResult
|
|
||||||
from extras.tables import ObjectChangeTable
|
from extras.tables import ObjectChangeTable
|
||||||
from ipam.models import Aggregate, IPAddress, IPRange, Prefix, VLAN, VRF
|
from ipam.models import Aggregate, IPAddress, IPRange, Prefix, VLAN, VRF
|
||||||
from netbox.constants import SEARCH_MAX_RESULTS, SEARCH_TYPES
|
from netbox.constants import SEARCH_MAX_RESULTS, SEARCH_TYPES
|
||||||
@ -48,13 +47,6 @@ class HomeView(View):
|
|||||||
pk__lt=F('_path__destination_id')
|
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():
|
def build_stats():
|
||||||
org = (
|
org = (
|
||||||
("dcim.view_site", "Sites", Site.objects.restrict(request.user, 'view').count),
|
("dcim.view_site", "Sites", Site.objects.restrict(request.user, 'view').count),
|
||||||
@ -150,7 +142,6 @@ class HomeView(View):
|
|||||||
return render(request, self.template_name, {
|
return render(request, self.template_name, {
|
||||||
'search_form': SearchForm(),
|
'search_form': SearchForm(),
|
||||||
'stats': build_stats(),
|
'stats': build_stats(),
|
||||||
'report_results': report_results,
|
|
||||||
'changelog_table': changelog_table,
|
'changelog_table': changelog_table,
|
||||||
'new_release': new_release,
|
'new_release': new_release,
|
||||||
})
|
})
|
||||||
|
@ -1,40 +1,54 @@
|
|||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">
|
<h5 class="card-header">
|
||||||
Non-Racked Devices
|
Non-Racked Devices
|
||||||
</h5>
|
</h5>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if nonracked_devices %}
|
{% if nonracked_devices %}
|
||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Role</th>
|
<th>Role</th>
|
||||||
<th>Type</th>
|
<th>Type</th>
|
||||||
<th colspan="2">Parent Device</th>
|
<th colspan="2">Parent Device</th>
|
||||||
</tr>
|
</tr>
|
||||||
{% for device in nonracked_devices %}
|
{% for device in nonracked_devices %}
|
||||||
<tr{% if device.device_type.u_height %} class="warning"{% endif %}>
|
<tr{% if device.device_type.u_height %} class="warning"{% endif %}>
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a>
|
<a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ device.device_role }}</td>
|
<td>{{ device.device_role }}</td>
|
||||||
<td>{{ device.device_type }}</td>
|
<td>{{ device.device_type }}</td>
|
||||||
{% if device.parent_bay %}
|
{% if device.parent_bay %}
|
||||||
<td>{{ device.parent_bay.device|linkify }}</td>
|
<td>{{ device.parent_bay.device|linkify }}</td>
|
||||||
<td>{{ device.parent_bay }}</td>
|
<td>{{ device.parent_bay }}</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td colspan="2" class="text-muted">—</td>
|
<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 %}
|
{% endif %}
|
||||||
</tr>
|
{% endif %}
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-muted">
|
<div class="text-muted">
|
||||||
None
|
None
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if perms.dcim.add_device %}
|
{% if perms.dcim.add_device %}
|
||||||
{% if object|meta:'verbose_name' == 'rack' %}
|
{% if object|meta:'verbose_name' == 'rack' %}
|
||||||
<div class="card-footer text-end noprint">
|
<div class="card-footer text-end noprint">
|
||||||
|
@ -52,6 +52,10 @@
|
|||||||
{% if object.installed_module %}
|
{% if object.installed_module %}
|
||||||
{% with module=object.installed_module %}
|
{% with module=object.installed_module %}
|
||||||
<table class="table table-hover attr-table">
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Module</th>
|
||||||
|
<td>{{ module|linkify }}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Manufacturer</th>
|
<th scope="row">Manufacturer</th>
|
||||||
<td>{{ module.module_type.manufacturer|linkify }}</td>
|
<td>{{ module.module_type.manufacturer|linkify }}</td>
|
||||||
@ -60,6 +64,14 @@
|
|||||||
<th scope="row">Module Type</th>
|
<th scope="row">Module Type</th>
|
||||||
<td>{{ module.module_type|linkify }}</td>
|
<td>{{ module.module_type|linkify }}</td>
|
||||||
</tr>
|
</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>
|
</table>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -39,11 +39,13 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# TODO: Improve the design & layout #}
|
|
||||||
{% if auth_backends %}
|
{% if auth_backends %}
|
||||||
<h6 class="mt-4">Or use an SSO provider:</h6>
|
<h6 class="mt-4 mb-3">Or use a single sign-on (SSO) provider:</h6>
|
||||||
{% for name, backend in auth_backends.items %}
|
{% for name, display in auth_backends.items %}
|
||||||
<h4><a href="{% url 'social:begin' backend=name %}" class="my-2">{{ name }}</a></h4>
|
<h5>
|
||||||
|
{% if display.1 %}<i class="mdi mdi-{{ display.1 }}"></i>{% endif %}
|
||||||
|
<a href="{% url 'social:begin' backend=name %}" class="my-2">{{ display.0 }}</a>
|
||||||
|
</h5>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ from social_core.backends.utils import load_backends
|
|||||||
|
|
||||||
from extras.models import ObjectChange
|
from extras.models import ObjectChange
|
||||||
from extras.tables import ObjectChangeTable
|
from extras.tables import ObjectChangeTable
|
||||||
|
from netbox.authentication import get_auth_backend_display
|
||||||
from netbox.config import get_config
|
from netbox.config import get_config
|
||||||
from utilities.forms import ConfirmationForm
|
from utilities.forms import ConfirmationForm
|
||||||
from .forms import LoginForm, PasswordChangeForm, TokenForm, UserConfigForm
|
from .forms import LoginForm, PasswordChangeForm, TokenForm, UserConfigForm
|
||||||
@ -43,9 +44,13 @@ class LoginView(View):
|
|||||||
logger = logging.getLogger('netbox.auth.login')
|
logger = logging.getLogger('netbox.auth.login')
|
||||||
return self.redirect_to_next(request, logger)
|
return self.redirect_to_next(request, logger)
|
||||||
|
|
||||||
|
auth_backends = {
|
||||||
|
name: get_auth_backend_display(name) for name in load_backends(settings.AUTHENTICATION_BACKENDS).keys()
|
||||||
|
}
|
||||||
|
|
||||||
return render(request, self.template_name, {
|
return render(request, self.template_name, {
|
||||||
'form': form,
|
'form': form,
|
||||||
'auth_backends': load_backends(settings.AUTHENTICATION_BACKENDS),
|
'auth_backends': auth_backends,
|
||||||
})
|
})
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
|
@ -144,11 +144,11 @@ def get_selected_values(form, field_name):
|
|||||||
label for value, label in choices if str(value) in filter_data or None in filter_data
|
label for value, label in choices if str(value) in filter_data or None in filter_data
|
||||||
]
|
]
|
||||||
|
|
||||||
if hasattr(field, 'null_option'):
|
# If the field has a `null_option` attribute set and it is selected,
|
||||||
# If the field has a `null_option` attribute set and it is selected,
|
# add it to the field's grouped choices.
|
||||||
# add it to the field's grouped choices.
|
if getattr(field, 'null_option', None) and None in filter_data:
|
||||||
if field.null_option is not None and None in filter_data:
|
values.remove(None)
|
||||||
values.append(field.null_option)
|
values.insert(0, field.null_option)
|
||||||
|
|
||||||
return values
|
return values
|
||||||
|
|
||||||
|
@ -136,6 +136,18 @@ class VMInterfaceCSVForm(NetBoxModelCSVForm):
|
|||||||
'vrf',
|
'vrf',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __init__(self, data=None, *args, **kwargs):
|
||||||
|
super().__init__(data, *args, **kwargs)
|
||||||
|
|
||||||
|
if data:
|
||||||
|
# Limit interface choices for parent & bridge interfaces to the assigned VM
|
||||||
|
if virtual_machine := data.get('virtual_machine'):
|
||||||
|
params = {
|
||||||
|
f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": virtual_machine
|
||||||
|
}
|
||||||
|
self.fields['parent'].queryset = self.fields['parent'].queryset.filter(**params)
|
||||||
|
self.fields['bridge'].queryset = self.fields['bridge'].queryset.filter(**params)
|
||||||
|
|
||||||
def clean_enabled(self):
|
def clean_enabled(self):
|
||||||
# Make sure enabled is True when it's not included in the uploaded data
|
# Make sure enabled is True when it's not included in the uploaded data
|
||||||
if 'enabled' not in self.data:
|
if 'enabled' not in self.data:
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
from dcim.models import Device, Interface, Location, Site
|
from dcim.models import Device, Interface, Location, Region, Site, SiteGroup
|
||||||
from extras.models import Tag
|
from ipam.models import VLAN, VLANGroup
|
||||||
from ipam.models import VLAN
|
|
||||||
from netbox.forms import NetBoxModelForm
|
from netbox.forms import NetBoxModelForm
|
||||||
from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField, StaticSelect
|
from utilities.forms import DynamicModelChoiceField, SlugField, StaticSelect
|
||||||
from wireless.models import *
|
from wireless.models import *
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -31,22 +30,63 @@ class WirelessLANForm(NetBoxModelForm):
|
|||||||
queryset=WirelessLANGroup.objects.all(),
|
queryset=WirelessLANGroup.objects.all(),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
region = DynamicModelChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
required=False,
|
||||||
|
initial_params={
|
||||||
|
'sites': '$site'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
site_group = DynamicModelChoiceField(
|
||||||
|
queryset=SiteGroup.objects.all(),
|
||||||
|
required=False,
|
||||||
|
initial_params={
|
||||||
|
'sites': '$site'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
site = DynamicModelChoiceField(
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
required=False,
|
||||||
|
null_option='None',
|
||||||
|
query_params={
|
||||||
|
'region_id': '$region',
|
||||||
|
'group_id': '$site_group',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
vlan_group = DynamicModelChoiceField(
|
||||||
|
queryset=VLANGroup.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label='VLAN group',
|
||||||
|
null_option='None',
|
||||||
|
query_params={
|
||||||
|
'site': '$site'
|
||||||
|
},
|
||||||
|
initial_params={
|
||||||
|
'vlans': '$vlan'
|
||||||
|
}
|
||||||
|
)
|
||||||
vlan = DynamicModelChoiceField(
|
vlan = DynamicModelChoiceField(
|
||||||
queryset=VLAN.objects.all(),
|
queryset=VLAN.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
label='VLAN'
|
label='VLAN',
|
||||||
|
query_params={
|
||||||
|
'site_id': '$site',
|
||||||
|
'group_id': '$vlan_group',
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('Wireless LAN', ('ssid', 'group', 'description', 'tags')),
|
('Wireless LAN', ('ssid', 'group', 'description', 'tags')),
|
||||||
('VLAN', ('vlan',)),
|
('VLAN', ('region', 'site_group', 'site', 'vlan_group', 'vlan',)),
|
||||||
('Authentication', ('auth_type', 'auth_cipher', 'auth_psk')),
|
('Authentication', ('auth_type', 'auth_cipher', 'auth_psk')),
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = WirelessLAN
|
model = WirelessLAN
|
||||||
fields = [
|
fields = [
|
||||||
'ssid', 'group', 'description', 'vlan', 'auth_type', 'auth_cipher', 'auth_psk', 'tags',
|
'ssid', 'group', 'description', 'region', 'site_group', 'site', 'vlan_group', 'vlan', 'auth_type',
|
||||||
|
'auth_cipher', 'auth_psk', 'tags',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'auth_type': StaticSelect,
|
'auth_type': StaticSelect,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
Django==4.0.3
|
Django==4.0.4
|
||||||
django-cors-headers==3.11.0
|
django-cors-headers==3.11.0
|
||||||
django-debug-toolbar==3.2.4
|
django-debug-toolbar==3.2.4
|
||||||
django-filter==21.1
|
django-filter==21.1
|
||||||
@ -18,7 +18,7 @@ gunicorn==20.1.0
|
|||||||
Jinja2==3.0.3
|
Jinja2==3.0.3
|
||||||
Markdown==3.3.6
|
Markdown==3.3.6
|
||||||
markdown-include==0.6.0
|
markdown-include==0.6.0
|
||||||
mkdocs-material==8.2.8
|
mkdocs-material==8.2.9
|
||||||
mkdocstrings==0.17.0
|
mkdocstrings==0.17.0
|
||||||
netaddr==0.8.0
|
netaddr==0.8.0
|
||||||
Pillow==9.1.0
|
Pillow==9.1.0
|
||||||
@ -27,7 +27,7 @@ PyYAML==6.0
|
|||||||
social-auth-app-django==5.0.0
|
social-auth-app-django==5.0.0
|
||||||
social-auth-core==4.2.0
|
social-auth-core==4.2.0
|
||||||
svgwrite==1.4.2
|
svgwrite==1.4.2
|
||||||
tablib==3.2.0
|
tablib==3.2.1
|
||||||
tzdata==2022.1
|
tzdata==2022.1
|
||||||
|
|
||||||
# Workaround for #7401
|
# Workaround for #7401
|
||||||
|
10
upgrade.sh
@ -3,23 +3,23 @@
|
|||||||
# its most recent release.
|
# its most recent release.
|
||||||
|
|
||||||
# This script will invoke Python with the value of the PYTHON environment
|
# This script will invoke Python with the value of the PYTHON environment
|
||||||
# variable (if set), or fall back to "python3". Note that NetBox v3.0+ requires
|
# variable (if set), or fall back to "python3". Note that NetBox v3.2+ requires
|
||||||
# Python 3.7 or later.
|
# Python 3.8 or later.
|
||||||
|
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
VIRTUALENV="$(pwd -P)/venv"
|
VIRTUALENV="$(pwd -P)/venv"
|
||||||
PYTHON="${PYTHON:-python3}"
|
PYTHON="${PYTHON:-python3}"
|
||||||
|
|
||||||
# Validate the minimum required Python version
|
# Validate the minimum required Python version
|
||||||
COMMAND="${PYTHON} -c 'import sys; exit(1 if sys.version_info < (3, 7) else 0)'"
|
COMMAND="${PYTHON} -c 'import sys; exit(1 if sys.version_info < (3, 8) else 0)'"
|
||||||
PYTHON_VERSION=$(eval "${PYTHON} -V")
|
PYTHON_VERSION=$(eval "${PYTHON} -V")
|
||||||
eval $COMMAND || {
|
eval $COMMAND || {
|
||||||
echo "--------------------------------------------------------------------"
|
echo "--------------------------------------------------------------------"
|
||||||
echo "ERROR: Unsupported Python version: ${PYTHON_VERSION}. NetBox requires"
|
echo "ERROR: Unsupported Python version: ${PYTHON_VERSION}. NetBox requires"
|
||||||
echo "Python 3.7 or later. To specify an alternate Python executable, set"
|
echo "Python 3.8 or later. To specify an alternate Python executable, set"
|
||||||
echo "the PYTHON environment variable. For example:"
|
echo "the PYTHON environment variable. For example:"
|
||||||
echo ""
|
echo ""
|
||||||
echo " sudo PYTHON=/usr/bin/python3.7 ./upgrade.sh"
|
echo " sudo PYTHON=/usr/bin/python3.8 ./upgrade.sh"
|
||||||
echo ""
|
echo ""
|
||||||
echo "To show your current Python version: ${PYTHON} -V"
|
echo "To show your current Python version: ${PYTHON} -V"
|
||||||
echo "--------------------------------------------------------------------"
|
echo "--------------------------------------------------------------------"
|
||||||
|