mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-26 19:58:14 +01:00
Compare commits
52 Commits
v3.5-beta2
...
v3.5.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eac7d01977 | ||
|
|
a5bc9d4a2d | ||
|
|
b3efb14176 | ||
|
|
a2f4fce5b3 | ||
|
|
6109bef700 | ||
|
|
72767fb5b7 | ||
|
|
84089ab8c5 | ||
|
|
9a788349a9 | ||
|
|
b7140a0e4a | ||
|
|
d39c796828 | ||
|
|
4a92f6867a | ||
|
|
4355085124 | ||
|
|
5d4ef5e9e5 | ||
|
|
f49e4ee512 | ||
|
|
f867cb3ae0 | ||
|
|
a49fdad5e1 | ||
|
|
1ad029712e | ||
|
|
d87235af2f | ||
|
|
99af126fac | ||
|
|
83cea218b5 | ||
|
|
9eb38ab7b2 | ||
|
|
d3206d9bf9 | ||
|
|
adb9673f09 | ||
|
|
b693123f6e | ||
|
|
e7663b7e39 | ||
|
|
053be952ba | ||
|
|
390619ca99 | ||
|
|
b1130ff9b6 | ||
|
|
89fa546a14 | ||
|
|
c8988bac8a | ||
|
|
55385dd0db | ||
|
|
38a0ed5e24 | ||
|
|
15d80f4e1b | ||
|
|
2fe5592c3c | ||
|
|
8cf0a79dee | ||
|
|
183c5ca667 | ||
|
|
d5c4b1e27c | ||
|
|
2fcdc0ae6a | ||
|
|
12bb0ec1fe | ||
|
|
8b7ee0a0db | ||
|
|
274cd5d56c | ||
|
|
ab3531558a | ||
|
|
dda56f21f3 | ||
|
|
31c909c368 | ||
|
|
164b2a5016 | ||
|
|
7b374e4cf6 | ||
|
|
13625325f5 | ||
|
|
b84ac184c2 | ||
|
|
d5d2431cbd | ||
|
|
c08c7dda50 | ||
|
|
b807198e6d | ||
|
|
9caa7f6b7c |
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.5-beta2
|
||||
placeholder: v3.5.0
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
24
.github/ISSUE_TEMPLATE/deprecation.yaml
vendored
Normal file
24
.github/ISSUE_TEMPLATE/deprecation.yaml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
name: 🗑️ Deprecation
|
||||
description: The removal of an existing feature or resource
|
||||
labels: ["type: deprecation"]
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Proposed Changes
|
||||
description: >
|
||||
Describe in detail the proposed changes. What is being removed?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Justification
|
||||
description: Please provide justification for the proposed change(s).
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Impact
|
||||
description: List all areas of the application that will be affected by this change.
|
||||
validations:
|
||||
required: true
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.5-beta2
|
||||
placeholder: v3.5.0
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
11
README.md
11
README.md
@@ -29,9 +29,18 @@ as the cornerstone for network automation in thousands of organizations.
|
||||
|
||||
## Getting Started
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/netbox-community/netbox)
|
||||
|
||||
[](https://github.com/netbox-community/netbox-docker)
|
||||
|
||||
[](https://netboxlabs.com/netbox-cloud/)
|
||||
|
||||
</div>
|
||||
|
||||
* Just want to explore? Check out [our public demo](https://demo.netbox.dev/) right now!
|
||||
* The [official documentation](https://docs.netbox.dev) offers a comprehensive introduction.
|
||||
* Choose your deployment: [self-hosted](https://github.com/netbox-community/netbox), [Docker](https://github.com/netbox-community/netbox-docker), or [NetBox Cloud](https://netboxlabs.com/netbox-cloud/).
|
||||
* Check out [our wiki](https://github.com/netbox-community/netbox/wiki/Community-Contributions) for even more projects to get the most out of NetBox!
|
||||
|
||||
## Get Involved
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# HTML sanitizer
|
||||
# https://github.com/mozilla/bleach/blob/main/CHANGES
|
||||
bleach<6.0
|
||||
bleach
|
||||
|
||||
# Python client for Amazon AWS API
|
||||
# https://github.com/boto/boto3/blob/develop/CHANGELOG.rst
|
||||
@@ -137,8 +137,7 @@ social-auth-core
|
||||
|
||||
# Django app for social-auth-core
|
||||
# https://github.com/python-social-auth/social-app-django/blob/master/CHANGELOG.md
|
||||
# See https://github.com/python-social-auth/social-app-django/issues/429
|
||||
social-auth-app-django==5.0.0
|
||||
social-auth-app-django
|
||||
|
||||
# SVG image rendering (used for rack elevations)
|
||||
# hhttps://github.com/mozman/svgwrite/blob/master/NEWS.rst
|
||||
|
||||
@@ -26,6 +26,8 @@ REMOTE_AUTH_BACKEND = 'netbox.authentication.RemoteUserBackend'
|
||||
|
||||
Another option for remote authentication in NetBox is to enable HTTP header-based user assignment. The front end HTTP server (e.g. nginx or Apache) performs client authentication as a process external to NetBox, and passes information about the authenticated user via HTTP headers. By default, the user is assigned via the `REMOTE_USER` header, but this can be customized via the `REMOTE_AUTH_HEADER` configuration parameter.
|
||||
|
||||
Optionally, user profile information can be supplied by `REMOTE_USER_FIRST_NAME`, `REMOTE_USER_LAST_NAME` and `REMOTE_USER_EMAIL` headers. These are saved to the users profile during the authentication process. These headers can be customized like the `REMOTE_USER` header.
|
||||
|
||||
### Single Sign-On (SSO)
|
||||
|
||||
```python
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
### Enabling Error Reporting
|
||||
|
||||
NetBox v3.2.3 and later support native integration with [Sentry](https://sentry.io/) for automatic error reporting. To enable this functionality, simply set `SENTRY_ENABLED` to True in `configuration.py`. Errors will be sent to a Sentry ingestor maintained by the NetBox team for analysis.
|
||||
NetBox supports native integration with [Sentry](https://sentry.io/) for automatic error reporting. To enable this functionality, simply set `SENTRY_ENABLED` to True in `configuration.py`. Errors will be sent to a Sentry ingestor maintained by the NetBox team for analysis.
|
||||
|
||||
```python
|
||||
SENTRY_ENABLED = True
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Object-Based Permissions
|
||||
|
||||
NetBox v2.9 introduced a new object-based permissions framework, which replaces Django's built-in permissions model. Object-based permissions enable an administrator to grant users or groups the ability to perform an action on arbitrary subsets of objects in NetBox, rather than all objects of a certain type. For example, it is possible to grant a user permission to view only sites within a particular region, or to modify only VLANs with a numeric ID within a certain range.
|
||||
NetBox employs a new object-based permissions framework, which replaces Django's built-in permissions model. Object-based permissions enable an administrator to grant users or groups the ability to perform an action on arbitrary subsets of objects in NetBox, rather than all objects of a certain type. For example, it is possible to grant a user permission to view only sites within a particular region, or to modify only VLANs with a numeric ID within a certain range.
|
||||
|
||||
A permission in NetBox represents a relationship shared by several components:
|
||||
|
||||
@@ -20,7 +20,7 @@ There are four core actions that can be permitted for each type of object within
|
||||
* **Change** - Modify an existing object
|
||||
* **Delete** - Delete an existing object
|
||||
|
||||
In addition to these, permissions can also grant custom actions that may be required by a specific model or plugin. For example, the `napalm_read` permission on the device model allows a user to execute NAPALM queries on a device via NetBox's REST API. These can be specified when granting a permission in the "additional actions" field.
|
||||
In addition to these, permissions can also grant custom actions that may be required by a specific model or plugin. For example, the `run` permission for scripts allows a user to execute custom scripts. These can be specified when granting a permission in the "additional actions" field.
|
||||
|
||||
!!! note
|
||||
Internally, all actions granted by a permission (both built-in and custom) are stored as strings in an array field named `actions`.
|
||||
|
||||
@@ -30,10 +30,6 @@ Some configuration parameters are primarily controlled via NetBox's admin interf
|
||||
* [`MAINTENANCE_MODE`](./miscellaneous.md#maintenance_mode)
|
||||
* [`MAPS_URL`](./miscellaneous.md#maps_url)
|
||||
* [`MAX_PAGE_SIZE`](./miscellaneous.md#max_page_size)
|
||||
* [`NAPALM_ARGS`](./napalm.md#napalm_args)
|
||||
* [`NAPALM_PASSWORD`](./napalm.md#napalm_password)
|
||||
* [`NAPALM_TIMEOUT`](./napalm.md#napalm_timeout)
|
||||
* [`NAPALM_USERNAME`](./napalm.md#napalm_username)
|
||||
* [`PAGINATE_COUNT`](./default-values.md#paginate_count)
|
||||
* [`POWERFEED_DEFAULT_AMPERAGE`](./default-values.md#powerfeed_default_amperage)
|
||||
* [`POWERFEED_DEFAULT_MAX_UTILIZATION`](./default-values.md#powerfeed_default_max_utilization)
|
||||
|
||||
@@ -45,6 +45,16 @@ Sets content for the top banner in the user interface.
|
||||
|
||||
---
|
||||
|
||||
## CENSUS_REPORTING_ENABLED
|
||||
|
||||
Default: True
|
||||
|
||||
Enables anonymous census reporting. To opt out of census reporting, set this to False.
|
||||
|
||||
This data enables the project maintainers to estimate how many NetBox deployments exist and track the adoption of new versions over time. Census reporting effects a single HTTP request each time a worker starts. The only data reported by this function are the NetBox version, Python version, and a pseudorandom unique identifier.
|
||||
|
||||
---
|
||||
|
||||
## CHANGELOG_RETENTION
|
||||
|
||||
!!! tip "Dynamic Configuration Parameter"
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
# NAPALM Parameters
|
||||
|
||||
!!! **Note:** As of NetBox v3.5, NAPALM integration has been moved to a plugin and these configuration parameters are now deprecated.
|
||||
|
||||
## NAPALM_USERNAME
|
||||
|
||||
## NAPALM_PASSWORD
|
||||
|
||||
!!! tip "Dynamic Configuration Parameter"
|
||||
|
||||
NetBox will use these credentials when authenticating to remote devices via the supported [NAPALM integration](../integrations/napalm.md), if installed. Both parameters are optional.
|
||||
|
||||
!!! note
|
||||
If SSH public key authentication has been set up on the remote device(s) for the system account under which NetBox runs, these parameters are not needed.
|
||||
|
||||
---
|
||||
|
||||
## NAPALM_ARGS
|
||||
|
||||
!!! tip "Dynamic Configuration Parameter"
|
||||
|
||||
A dictionary of optional arguments to pass to NAPALM when instantiating a network driver. See the NAPALM documentation for a [complete list of optional arguments](https://napalm.readthedocs.io/en/latest/support/#optional-arguments). An example:
|
||||
|
||||
```python
|
||||
NAPALM_ARGS = {
|
||||
'api_key': '472071a93b60a1bd1fafb401d9f8ef41',
|
||||
'port': 2222,
|
||||
}
|
||||
```
|
||||
|
||||
Some platforms (e.g. Cisco IOS) require an argument named `secret` to be passed in addition to the normal password. If desired, you can use the configured `NAPALM_PASSWORD` as the value for this argument:
|
||||
|
||||
```python
|
||||
NAPALM_USERNAME = 'username'
|
||||
NAPALM_PASSWORD = 'MySecretPassword'
|
||||
NAPALM_ARGS = {
|
||||
'secret': NAPALM_PASSWORD,
|
||||
# Include any additional args here
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## NAPALM_TIMEOUT
|
||||
|
||||
!!! tip "Dynamic Configuration Parameter"
|
||||
|
||||
Default: 30 seconds
|
||||
|
||||
The amount of time (in seconds) to wait for NAPALM to connect to a device.
|
||||
|
||||
---
|
||||
|
||||
@@ -79,6 +79,30 @@ When remote user authentication is in use, this is the name of the HTTP header w
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_USER_EMAIL
|
||||
|
||||
Default: `'HTTP_REMOTE_USER_EMAIL'`
|
||||
|
||||
When remote user authentication is in use, this is the name of the HTTP header which informs NetBox of the email address of the currently authenticated user. For example, to use the request header `X-Remote-User-Email` it needs to be set to `HTTP_X_REMOTE_USER_EMAIL`. (Requires `REMOTE_AUTH_ENABLED`.)
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_USER_FIRST_NAME
|
||||
|
||||
Default: `'HTTP_REMOTE_USER_FIRST_NAME'`
|
||||
|
||||
When remote user authentication is in use, this is the name of the HTTP header which informs NetBox of the first name of the currently authenticated user. For example, to use the request header `X-Remote-User-First-Name` it needs to be set to `HTTP_X_REMOTE_USER_FIRST_NAME`. (Requires `REMOTE_AUTH_ENABLED`.)
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_USER_LAST_NAME
|
||||
|
||||
Default: `'HTTP_REMOTE_USER_LAST_NAME'`
|
||||
|
||||
When remote user authentication is in use, this is the name of the HTTP header which informs NetBox of the last name of the currently authenticated user. For example, to use the request header `X-Remote-User-Last-Name` it needs to be set to `HTTP_X_REMOTE_USER_LAST_NAME`. (Requires `REMOTE_AUTH_ENABLED`.)
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_SUPERUSER_GROUPS
|
||||
|
||||
Default: `[]` (Empty list)
|
||||
|
||||
@@ -144,8 +144,6 @@ REDIS = {
|
||||
|
||||
## SECRET_KEY
|
||||
|
||||
This is a secret, random string used to assist in the creation new cryptographic hashes for passwords and HTTP cookies. The key defined here should not be shared outside of the configuration file. `SECRET_KEY` can be changed at any time, however be aware that doing so will invalidate all existing sessions.
|
||||
This is a secret, pseudorandom string used to assist in the creation new cryptographic hashes for passwords and HTTP cookies. The key defined here should not be shared outside the configuration file. `SECRET_KEY` can be changed at any time without impacting stored data, however be aware that doing so will invalidate all existing user sessions. NetBox deployments comprising multiple nodes must have the same secret key configured on all nodes.
|
||||
|
||||
Please note that this key is **not** used directly for hashing user passwords or for the encrypted storage of secret data in NetBox.
|
||||
|
||||
`SECRET_KEY` should be at least 50 characters in length and contain a random mix of letters, digits, and symbols. The script located at `$INSTALL_ROOT/netbox/generate_secret_key.py` may be used to generate a suitable key.
|
||||
`SECRET_KEY` **must** be at least 50 characters in length, and should contain a mix of letters, digits, and symbols. The script located at `$INSTALL_ROOT/netbox/generate_secret_key.py` may be used to generate a suitable key. Please note that this key is **not** used directly for hashing user passwords or for the encrypted storage of secret data in NetBox.
|
||||
|
||||
@@ -67,6 +67,12 @@ The name of the cookie to use for the cross-site request forgery (CSRF) authenti
|
||||
|
||||
---
|
||||
|
||||
## CSRF_COOKIE_SECURE
|
||||
|
||||
Default: False
|
||||
|
||||
If true, the cookie employed for cross-site request forgery (CSRF) protection will be marked as secure, meaning that it can only be sent across an HTTPS connection.
|
||||
|
||||
---
|
||||
|
||||
## CSRF_TRUSTED_ORIGINS
|
||||
@@ -145,6 +151,17 @@ The view name or URL to which a user is redirected after logging out.
|
||||
|
||||
---
|
||||
|
||||
## SECURE_SSL_REDIRECT
|
||||
|
||||
Default: False
|
||||
|
||||
If true, all non-HTTPS requests will be automatically redirected to use HTTPS.
|
||||
|
||||
!!! warning
|
||||
Ensure that your frontend HTTP daemon has been configured to forward the HTTP scheme correctly before enabling this option. An incorrectly configured frontend may result in a looping redirect.
|
||||
|
||||
---
|
||||
|
||||
## SESSION_COOKIE_NAME
|
||||
|
||||
Default: `sessionid`
|
||||
@@ -153,6 +170,14 @@ The name used for the session cookie. See the [Django documentation](https://doc
|
||||
|
||||
---
|
||||
|
||||
## SESSION_COOKIE_SECURE
|
||||
|
||||
Default: False
|
||||
|
||||
If true, the cookie employed for session authentication will be marked as secure, meaning that it can only be sent across an HTTPS connection.
|
||||
|
||||
---
|
||||
|
||||
## SESSION_FILE_PATH
|
||||
|
||||
Default: None
|
||||
|
||||
@@ -27,7 +27,6 @@ The following context data is available within the template when rendering a cus
|
||||
| Variable | Description |
|
||||
|-----------|-------------------------------------------------------------------------------------------------------------------|
|
||||
| `object` | The NetBox object being displayed |
|
||||
| `obj` | Same as `object`; maintained for backward compatability until NetBox v3.5 |
|
||||
| `debug` | A boolean indicating whether debugging is enabled |
|
||||
| `request` | The current WSGI request |
|
||||
| `user` | The current user (if authenticated) |
|
||||
|
||||
@@ -35,12 +35,9 @@ class MyScript(Script):
|
||||
|
||||
The `run()` method should accept two arguments:
|
||||
|
||||
* `data` - A dictionary containing all of the variable data passed via the web form.
|
||||
* `data` - A dictionary containing all the variable data passed via the web form.
|
||||
* `commit` - A boolean indicating whether database changes will be committed.
|
||||
|
||||
!!! note
|
||||
The `commit` argument was introduced in NetBox v2.7.8. Backward compatibility is maintained for scripts which accept only the `data` argument, however beginning with v2.10 NetBox will require the `run()` method of every script to accept both arguments. (Either argument may still be ignored within the method.)
|
||||
|
||||
Defining script variables is optional: You may create a script with only a `run()` method if no user input is needed.
|
||||
|
||||
Any output generated by the script during its execution will be displayed under the "output" tab in the UI.
|
||||
|
||||
@@ -30,14 +30,6 @@ A webhook is a mechanism for conveying to some external system a change that too
|
||||
|
||||
To learn more about this feature, check out the [webhooks documentation](../integrations/webhooks.md).
|
||||
|
||||
## NAPALM
|
||||
|
||||
[NAPALM](https://github.com/napalm-automation/napalm) is a Python library which enables direct interaction with network devices of various platforms. When configured, NetBox supports fetching live operational and status data directly from network devices to be compared to what has been defined in NetBox. This allows for easily validating the device's operational state against its desired state. Additionally, NetBox's REST API can act as a sort of proxy for NAPALM commands, allowing external clients to interact with network devices by sending HTTP requests to the appropriate API endpoint.
|
||||
|
||||
To learn more about this feature, check out the [NAPALM documentation](../integrations/napalm.md).
|
||||
|
||||
As of NetBox v3.5, NAPALM integration has been moved to a plugin. Please see the [netbox_napalm_plugin](https://github.com/netbox-community/netbox-napalm) for installation instructions.
|
||||
|
||||
## Prometheus Metrics
|
||||
|
||||
NetBox includes a special `/metrics` view which exposes metrics for a [Prometheus](https://prometheus.io/) scraper, powered by the open source [django-prometheus](https://github.com/korfuri/django-prometheus) library. To learn more about this feature, check out the [Prometheus metrics documentation](../integrations/prometheus-metrics.md).
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Configuration Rendering
|
||||
|
||||
!!! info "This feature was introduced in NetBox v3.5."
|
||||
|
||||
One of the critical aspects of operating a network is ensuring that every network node is configured correctly. By leveraging configuration templates and [context data](./context-data.md), NetBox can render complete configuration files for each device on your network.
|
||||
|
||||
```mermaid
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
|
||||
[Redis](https://redis.io/) is an in-memory key-value store which NetBox employs for caching and queuing. This section entails the installation and configuration of a local Redis instance. If you already have a Redis service in place, skip to [the next section](3-netbox.md).
|
||||
|
||||
!!! warning "Redis v4.0 or later required"
|
||||
NetBox v2.9.0 and later require Redis v4.0 or higher. If your distribution does not offer a recent enough release, you will need to build Redis from source. Please see [the Redis installation documentation](https://github.com/redis/redis) for further details.
|
||||
|
||||
=== "Ubuntu"
|
||||
|
||||
```no-highlight
|
||||
|
||||
@@ -97,7 +97,7 @@ sudo git pull origin master
|
||||
|
||||
## 4. Run the Upgrade Script
|
||||
|
||||
Once the new code is in place, verify that any optional Python packages required by your deployment (e.g. `napalm` or `django-auth-ldap`) are listed in `local_requirements.txt`. Then, run the upgrade script:
|
||||
Once the new code is in place, verify that any optional Python packages required by your deployment (e.g. `django-auth-ldap`) are listed in `local_requirements.txt`. Then, run the upgrade script:
|
||||
|
||||
```no-highlight
|
||||
sudo ./upgrade.sh
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# NAPALM
|
||||
|
||||
As of NetBox v3.5, NAPALM integration has been moved to a plugin. Please see the [netbox_napalm_plugin](https://github.com/netbox-community/netbox-napalm) for installation instructions. **Note:** All previously entered NAPALM configuration data will be saved and automatically imported by the new plugin.
|
||||
@@ -638,7 +638,7 @@ $ curl -X POST \
|
||||
https://netbox/api/users/tokens/provision/ \
|
||||
--data '{
|
||||
"username": "hankhill",
|
||||
"password": "I<3C3H8",
|
||||
"password": "I<3C3H8"
|
||||
}'
|
||||
```
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ NetBox was built specifically to serve the needs of network engineers and operat
|
||||
* Robust object-based permissions
|
||||
* Detailed, automatic change logging
|
||||
* Global search engine
|
||||
* NAPALM integration
|
||||
|
||||
## What NetBox Is Not
|
||||
|
||||
@@ -78,4 +77,3 @@ NetBox is built on the [Django](https://djangoproject.com/) Python framework and
|
||||
| Application | Django/Python |
|
||||
| Database | PostgreSQL 11+ |
|
||||
| Task queuing | Redis/django-rq |
|
||||
| Live device access | NAPALM (optional) |
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 173 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 171 KiB |
@@ -1,17 +1,19 @@
|
||||
# Provider Accounts
|
||||
|
||||
This model can be used to represent individual accounts associated with a provider.
|
||||
|
||||
## Fields
|
||||
|
||||
### Provider
|
||||
|
||||
The [provider](./provider.md) the account belongs to.
|
||||
|
||||
### Name
|
||||
|
||||
A human-friendly name, unique to the provider.
|
||||
|
||||
### Account Number
|
||||
|
||||
The administrative account identifier tied to this provider for your organization.
|
||||
# Provider Accounts
|
||||
|
||||
!!! info "This model was introduced in NetBox v3.5."
|
||||
|
||||
This model can be used to represent individual accounts associated with a provider.
|
||||
|
||||
## Fields
|
||||
|
||||
### Provider
|
||||
|
||||
The [provider](./provider.md) the account belongs to.
|
||||
|
||||
### Name
|
||||
|
||||
A human-friendly name, unique to the provider.
|
||||
|
||||
### Account Number
|
||||
|
||||
The administrative account identifier tied to this provider for your organization.
|
||||
|
||||
@@ -28,8 +28,14 @@ The default [configuration template](../extras/configtemplate.md) for devices as
|
||||
|
||||
### NAPALM Driver
|
||||
|
||||
!!! warning "Deprecated Field"
|
||||
NAPALM integration was removed from NetBox core in v3.5 and is now available as a [plugin](https://github.com/netbox-community/netbox-napalm). This field will be removed in NetBox v3.6.
|
||||
|
||||
The [NAPALM driver](https://napalm.readthedocs.io/en/latest/support/index.html) associated with this platform.
|
||||
|
||||
### NAPALM Arguments
|
||||
|
||||
!!! warning "Deprecated Field"
|
||||
NAPALM integration was removed from NetBox core in v3.5 and is now available as a [plugin](https://github.com/netbox-community/netbox-napalm). This field will be removed in NetBox v3.6.
|
||||
|
||||
Any additional arguments to send when invoking the NAPALM driver assigned to this platform.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# ASN Ranges
|
||||
|
||||
!!! info "This model was introduced in NetBox v3.5."
|
||||
|
||||
Ranges can be defined to group [AS numbers](./asn.md) numerically and to facilitate their automatic provisioning. Each range must be assigned to a [RIR](./rir.md).
|
||||
|
||||
## Fields
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Dashboard Widgets
|
||||
|
||||
!!! note "Introduced in v3.5"
|
||||
Support for custom dashboard widgets was introduced in NetBox v3.5.
|
||||
!!! info "This feature was introduced in NetBox v3.5."
|
||||
|
||||
Each NetBox user can customize his or her personal dashboard by adding and removing widgets and by manipulating the size and position of each. Plugins can register their own dashboard widgets to complement those already available natively.
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
Plugins are packaged [Django](https://docs.djangoproject.com/) apps that can be installed alongside NetBox to provide custom functionality not present in the core application. Plugins can introduce their own models and views, but cannot interfere with existing components. A NetBox user may opt to install plugins provided by the community or build his or her own.
|
||||
|
||||
Plugins are supported on NetBox v2.8 and later.
|
||||
|
||||
## Capabilities
|
||||
|
||||
The NetBox plugin architecture allows for the following:
|
||||
|
||||
@@ -10,6 +10,16 @@ Minor releases are published in April, August, and December of each calendar yea
|
||||
|
||||
This page contains a history of all major and minor releases since NetBox v2.0. For more detail on a specific patch release, please see the release notes page for that specific minor release.
|
||||
|
||||
#### [Version 3.5](./version-3.5.md) (April 2023)
|
||||
|
||||
* Customizable Dashboard ([#9416](https://github.com/netbox-community/netbox/issues/9416))
|
||||
* Remote Data Sources ([#11558](https://github.com/netbox-community/netbox/issues/11558))
|
||||
* Configuration Template Rendering ([#11559](https://github.com/netbox-community/netbox/issues/11559))
|
||||
* NAPALM Integration Plugin ([#10520](https://github.com/netbox-community/netbox/issues/10520))
|
||||
* ASN Ranges ([#8550](https://github.com/netbox-community/netbox/issues/8550))
|
||||
* Provider Accounts ([#9047](https://github.com/netbox-community/netbox/issues/9047))
|
||||
* Job-Triggered Webhooks ([#8958](https://github.com/netbox-community/netbox/issues/8958))
|
||||
|
||||
#### [Version 3.4](./version-3.4.md) (December 2022)
|
||||
|
||||
* New Global Search ([#10560](https://github.com/netbox-community/netbox/issues/10560))
|
||||
|
||||
@@ -1,6 +1,34 @@
|
||||
# NetBox v3.4
|
||||
|
||||
## v3.4.9 (FUTURE)
|
||||
## v3.4.10 (2023-04-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#11607](https://github.com/netbox-community/netbox/issues/11607) - Fix custom object field assignments made via REST API for for cables
|
||||
* [#12252](https://github.com/netbox-community/netbox/issues/12252) - Fix ordering of search results when sorting by object name
|
||||
* [#12355](https://github.com/netbox-community/netbox/issues/12355) - Fix escaping of certain characters in URL when rendering custom links
|
||||
|
||||
---
|
||||
|
||||
## v3.4.9 (2023-04-26)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#10987](https://github.com/netbox-community/netbox/issues/10987) - Show peer racks as a dropdown list under rack view
|
||||
* [#11386](https://github.com/netbox-community/netbox/issues/11386) - Introduce `CSRF_COOKIE_SECURE`, `SECURE_SSL_REDIRECT`, and `SESSION_COOKIE_SECURE` configuration parameters
|
||||
* [#11623](https://github.com/netbox-community/netbox/issues/11623) - Hide PSK strings under wireless LAN & link views
|
||||
* [#12205](https://github.com/netbox-community/netbox/issues/12205) - Sanitize rendered custom links to mitigate malicious links
|
||||
* [#12226](https://github.com/netbox-community/netbox/issues/12226) - Enable setting user name & email values via remote authenticate headers
|
||||
* [#12337](https://github.com/netbox-community/netbox/issues/12337) - Enable anonymized reporting of census data
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#11383](https://github.com/netbox-community/netbox/issues/11383) - Fix ordering of global search results by object type
|
||||
* [#11902](https://github.com/netbox-community/netbox/issues/11902) - Fix import of inventory items for devices with duplicated names
|
||||
* [#12238](https://github.com/netbox-community/netbox/issues/12238) - Improve error message for API token IP prefix validation failures
|
||||
* [#12255](https://github.com/netbox-community/netbox/issues/12255) - Restore the ability to move inventory items among devices
|
||||
* [#12270](https://github.com/netbox-community/netbox/issues/12270) - Fix pre-population of list values when creating a saved filter
|
||||
* [#12296](https://github.com/netbox-community/netbox/issues/12296) - Fix "mark connected" form field for bulk editing front & rear ports
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
# NetBox v3.5
|
||||
|
||||
## v3.5-beta2 (2023-04-18)
|
||||
## v3.5.0 (2023-04-27)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* The `account` field has been removed from the provider model. This information is now tracked using the new provider account model. Multiple accounts can be assigned per provider.
|
||||
* A minimum length of 50 characters is now enforced for the `SECRET_KEY` configuration parameter.
|
||||
* The JobResult model has been moved from the `extras` app to `core` and renamed to Job. Accordingly, its REST API endpoint has been moved from `/api/extras/job-results/` to `/api/core/jobs/`.
|
||||
* The `obj_type` field on the Job model (previously JobResult) has been renamed to `object_type` for consistency with other models.
|
||||
* The `JOBRESULT_RETENTION` configuration parameter has been renamed to `JOB_RETENTION`.
|
||||
* The `obj` context variable is no longer passed when rendering custom links: Use `object` instead.
|
||||
* The REST API schema is now generated using the OpenAPI 3.0 spec
|
||||
* The URLs for the REST API schema documentation have changed:
|
||||
* `/api/docs/` is now `/api/schema/swagger-ui/`
|
||||
@@ -27,7 +29,7 @@ NetBox now has the ability to synchronize arbitrary data from external sources t
|
||||
|
||||
This release introduces the ability to render device configurations from Jinja2 templates natively within NetBox, via both the UI and REST API. The new [ConfigTemplate](../models/extras/configtemplate.md) model stores template code (which may be defined locally or sourced from remote data files). The rendering engine passes data gleaned from both config contexts and request parameters to generate complete configurations suitable for direct application to network devices.
|
||||
|
||||
#### NAPALM Plugin ([#10520](https://github.com/netbox-community/netbox/issues/10520))
|
||||
#### NAPALM Integration Plugin ([#10520](https://github.com/netbox-community/netbox/issues/10520))
|
||||
|
||||
The NAPALM integration feature found in previous NetBox releases has been moved from the core application to a [dedicated plugin](https://github.com/netbox-community/netbox-napalm). This allows greater control over the feature's configuration and will unlock additional potential as a separate project.
|
||||
|
||||
@@ -70,21 +72,17 @@ Two new webhook trigger events have been introduced: `job_start` and `job_end`.
|
||||
* [#11968](https://github.com/netbox-community/netbox/issues/11968) - Add navigation menu buttons to create device & VM components
|
||||
* [#12068](https://github.com/netbox-community/netbox/issues/12068) - Enable generic foreign key relationships from jobs to NetBox objects
|
||||
* [#12085](https://github.com/netbox-community/netbox/issues/12085) - Add a file source view for reports
|
||||
* [#12218](https://github.com/netbox-community/netbox/issues/12218) - Provide more relevant API endpoint descriptions in schema
|
||||
* [#12343](https://github.com/netbox-community/netbox/issues/12343) - Enforce a minimum length for `SECRET_KEY` configuration parameter
|
||||
|
||||
### Bug Fixes (From Beta1)
|
||||
### Bug Fixes (From Beta2)
|
||||
|
||||
* [#12103](https://github.com/netbox-community/netbox/issues/12103) - Limit the types of objects available for object count & list widgets
|
||||
* [#12105](https://github.com/netbox-community/netbox/issues/12105) - Prevent data sources from becoming stuck in "syncing" status when an exception is raised
|
||||
* [#12106](https://github.com/netbox-community/netbox/issues/12106) - Fix exception when saving dashboard widget with minimum width/height
|
||||
* [#12108](https://github.com/netbox-community/netbox/issues/12108) - Limit the draggable area of widgets to their headers
|
||||
* [#12109](https://github.com/netbox-community/netbox/issues/12109) - Fix migration error when replicating more than 100 job results
|
||||
* [#12112](https://github.com/netbox-community/netbox/issues/12112) - Do not link data source URL for local paths
|
||||
* [#12115](https://github.com/netbox-community/netbox/issues/12115) - Fix rendering config templates from a data file
|
||||
* [#12144](https://github.com/netbox-community/netbox/issues/12144) - Ensure consistent treatment of context data when rendering config templates via UI & API
|
||||
* [#12145](https://github.com/netbox-community/netbox/issues/12145) - Employ `HTMXSelect` widget to fix inclusion of `<select>` field values during form regeneration
|
||||
* [#12146](https://github.com/netbox-community/netbox/issues/12146) - Do not display object selector for disabled fields
|
||||
* [#12151](https://github.com/netbox-community/netbox/issues/12151) - Remove incorrect OpenAPI string mapping for choice fields
|
||||
* [#12167](https://github.com/netbox-community/netbox/issues/12167) - Catch and report on exceptions raised when rendering a config template
|
||||
* [#12149](https://github.com/netbox-community/netbox/issues/12149) - Fix OpenAPI schema warnings relating to enum collisions
|
||||
* [#12195](https://github.com/netbox-community/netbox/issues/12195) - Fix exception when setting IP address role to null via REST API
|
||||
* [#12256](https://github.com/netbox-community/netbox/issues/12256) - Fix OpenAPI schema warnings relating to nested serializers
|
||||
* [#12278](https://github.com/netbox-community/netbox/issues/12278) - Fix schema warnings related to IPAddressField
|
||||
* [#12288](https://github.com/netbox-community/netbox/issues/12288) - Include `servers` definition in OpenAPI spec
|
||||
* [#12299](https://github.com/netbox-community/netbox/issues/12299) - Fix object list widget support for filtering by multiple values
|
||||
|
||||
### Other Changes
|
||||
|
||||
|
||||
@@ -108,7 +108,6 @@ nav:
|
||||
- Default Values: 'configuration/default-values.md'
|
||||
- Error Reporting: 'configuration/error-reporting.md'
|
||||
- Plugins: 'configuration/plugins.md'
|
||||
- NAPALM: 'configuration/napalm.md'
|
||||
- Date & Time: 'configuration/date-time.md'
|
||||
- Miscellaneous: 'configuration/miscellaneous.md'
|
||||
- Development: 'configuration/development.md'
|
||||
@@ -124,7 +123,6 @@ nav:
|
||||
- GraphQL API: 'integrations/graphql-api.md'
|
||||
- Webhooks: 'integrations/webhooks.md'
|
||||
- Synchronized Data: 'integrations/synchronized-data.md'
|
||||
- NAPALM: 'integrations/napalm.md'
|
||||
- Prometheus Metrics: 'integrations/prometheus-metrics.md'
|
||||
- Plugins:
|
||||
- Using Plugins: 'plugins/index.md'
|
||||
|
||||
@@ -25,6 +25,22 @@ class CircuitStatusChoices(ChoiceSet):
|
||||
]
|
||||
|
||||
|
||||
class CircuitCommitRateChoices(ChoiceSet):
|
||||
key = 'Circuit.commit_rate'
|
||||
|
||||
CHOICES = [
|
||||
(10000, '10 Mbps'),
|
||||
(100000, '100 Mbps'),
|
||||
(1000000, '1 Gbps'),
|
||||
(10000000, '10 Gbps'),
|
||||
(25000000, '25 Gbps'),
|
||||
(40000000, '40 Gbps'),
|
||||
(100000000, '100 Gbps'),
|
||||
(1544, 'T1 (1.544 Mbps)'),
|
||||
(2048, 'E1 (2.048 Mbps)'),
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# CircuitTerminations
|
||||
#
|
||||
@@ -38,3 +54,19 @@ class CircuitTerminationSideChoices(ChoiceSet):
|
||||
(SIDE_A, 'A'),
|
||||
(SIDE_Z, 'Z')
|
||||
)
|
||||
|
||||
|
||||
class CircuitTerminationPortSpeedChoices(ChoiceSet):
|
||||
key = 'CircuitTermination.port_speed'
|
||||
|
||||
CHOICES = [
|
||||
(10000, '10 Mbps'),
|
||||
(100000, '100 Mbps'),
|
||||
(1000000, '1 Gbps'),
|
||||
(10000000, '10 Gbps'),
|
||||
(25000000, '25 Gbps'),
|
||||
(40000000, '40 Gbps'),
|
||||
(100000000, '100 Gbps'),
|
||||
(1544, 'T1 (1.544 Mbps)'),
|
||||
(2048, 'E1 (2.048 Mbps)'),
|
||||
]
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from circuits.choices import CircuitStatusChoices
|
||||
from circuits.choices import CircuitCommitRateChoices, CircuitStatusChoices
|
||||
from circuits.models import *
|
||||
from ipam.models import ASN
|
||||
from netbox.forms import NetBoxModelBulkEditForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import add_blank_choice
|
||||
from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
||||
from utilities.forms.widgets import DatePicker
|
||||
from utilities.forms.widgets import DatePicker, NumberWithOptions
|
||||
|
||||
__all__ = (
|
||||
'CircuitBulkEditForm',
|
||||
@@ -139,7 +139,10 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
|
||||
)
|
||||
commit_rate = forms.IntegerField(
|
||||
required=False,
|
||||
label=_('Commit rate (Kbps)')
|
||||
label=_('Commit rate (Kbps)'),
|
||||
widget=NumberWithOptions(
|
||||
options=CircuitCommitRateChoices
|
||||
)
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=100,
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from circuits.choices import CircuitStatusChoices
|
||||
from circuits.choices import CircuitCommitRateChoices, CircuitStatusChoices
|
||||
from circuits.models import *
|
||||
from dcim.models import Region, Site, SiteGroup
|
||||
from ipam.models import ASN
|
||||
from netbox.forms import NetBoxModelFilterSetForm
|
||||
from tenancy.forms import TenancyFilterForm, ContactModelFilterForm
|
||||
from utilities.forms.fields import DynamicModelMultipleChoiceField, TagFilterField
|
||||
from utilities.forms.widgets import DatePicker
|
||||
from utilities.forms.widgets import DatePicker, NumberWithOptions
|
||||
|
||||
__all__ = (
|
||||
'CircuitFilterForm',
|
||||
@@ -168,6 +168,9 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
|
||||
commit_rate = forms.IntegerField(
|
||||
required=False,
|
||||
min_value=0,
|
||||
label=_('Commit rate (Kbps)')
|
||||
label=_('Commit rate (Kbps)'),
|
||||
widget=NumberWithOptions(
|
||||
options=CircuitCommitRateChoices
|
||||
)
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from circuits.choices import CircuitCommitRateChoices, CircuitTerminationPortSpeedChoices
|
||||
from circuits.models import *
|
||||
from dcim.models import Site
|
||||
from ipam.models import ASN
|
||||
from netbox.forms import NetBoxModelForm
|
||||
from tenancy.forms import TenancyForm
|
||||
from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
|
||||
from utilities.forms.widgets import DatePicker, SelectSpeedWidget
|
||||
from utilities.forms.widgets import DatePicker, NumberWithOptions
|
||||
|
||||
__all__ = (
|
||||
'CircuitForm',
|
||||
@@ -116,7 +117,9 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
|
||||
widgets = {
|
||||
'install_date': DatePicker(),
|
||||
'termination_date': DatePicker(),
|
||||
'commit_rate': SelectSpeedWidget(),
|
||||
'commit_rate': NumberWithOptions(
|
||||
options=CircuitCommitRateChoices
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -143,6 +146,10 @@ class CircuitTerminationForm(NetBoxModelForm):
|
||||
'xconnect_id', 'pp_info', 'description', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'port_speed': SelectSpeedWidget(),
|
||||
'upstream_speed': SelectSpeedWidget(),
|
||||
'port_speed': NumberWithOptions(
|
||||
options=CircuitTerminationPortSpeedChoices
|
||||
),
|
||||
'upstream_speed': NumberWithOptions(
|
||||
options=CircuitTerminationPortSpeedChoices
|
||||
),
|
||||
}
|
||||
|
||||
@@ -10,8 +10,10 @@ from drf_spectacular.plumbing import (
|
||||
ComponentRegistry,
|
||||
ResolvedComponent,
|
||||
build_basic_type,
|
||||
build_choice_field,
|
||||
build_media_type_object,
|
||||
build_object_type,
|
||||
get_doc,
|
||||
is_serializer,
|
||||
)
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
@@ -38,7 +40,7 @@ class ChoiceFieldFix(OpenApiSerializerFieldExtension):
|
||||
|
||||
def map_serializer_field(self, auto_schema, direction):
|
||||
if direction == 'request':
|
||||
return build_basic_type(OpenApiTypes.STR)
|
||||
return build_choice_field(self.target)
|
||||
|
||||
elif direction == "response":
|
||||
return build_object_type(
|
||||
@@ -150,8 +152,12 @@ class NetBoxAutoSchema(AutoSchema):
|
||||
def get_writable_class(self, serializer):
|
||||
properties = {}
|
||||
fields = {} if hasattr(serializer, 'child') else serializer.fields
|
||||
remove_fields = []
|
||||
|
||||
for child_name, child in fields.items():
|
||||
# read_only fields don't need to be in writable (write only) serializers
|
||||
if 'read_only' in dir(child) and child.read_only:
|
||||
remove_fields.append(child_name)
|
||||
if isinstance(child, (ChoiceField, WritableNestedSerializer)):
|
||||
properties[child_name] = None
|
||||
elif isinstance(child, ManyRelatedField) and isinstance(child.child_relation, SerializedPKRelatedField):
|
||||
@@ -165,7 +171,12 @@ class NetBoxAutoSchema(AutoSchema):
|
||||
meta_class = getattr(type(serializer), 'Meta', None)
|
||||
if meta_class:
|
||||
ref_name = 'Writable' + self.get_serializer_ref_name(serializer)
|
||||
writable_meta = type('Meta', (meta_class,), {'ref_name': ref_name})
|
||||
# remove read_only fields from write-only serializers
|
||||
fields = list(meta_class.fields)
|
||||
for field in remove_fields:
|
||||
fields.remove(field)
|
||||
writable_meta = type('Meta', (meta_class,), {'ref_name': ref_name, 'fields': fields})
|
||||
|
||||
properties['Meta'] = writable_meta
|
||||
|
||||
self.writable_serializers[type(serializer)] = type(writable_name, (type(serializer),), properties)
|
||||
@@ -222,3 +233,31 @@ class NetBoxAutoSchema(AutoSchema):
|
||||
if request_body_required:
|
||||
request_body['required'] = request_body_required
|
||||
return request_body
|
||||
|
||||
def get_description(self):
|
||||
"""
|
||||
Return a string description for the ViewSet.
|
||||
"""
|
||||
|
||||
# If a docstring is provided, use it.
|
||||
if self.view.__doc__:
|
||||
return get_doc(self.view.__class__)
|
||||
|
||||
# When the action method is decorated with @action, use the docstring of the method.
|
||||
action_or_method = getattr(self.view, getattr(self.view, 'action', self.method.lower()), None)
|
||||
if action_or_method and action_or_method.__doc__:
|
||||
return get_doc(action_or_method)
|
||||
|
||||
# Else, generate a description from the class name.
|
||||
return self._generate_description()
|
||||
|
||||
def _generate_description(self):
|
||||
"""
|
||||
Generate a docstring for the method. It also takes into account whether the method is for list or detail.
|
||||
"""
|
||||
model_name = self.view.queryset.model._meta.verbose_name
|
||||
|
||||
# Determine if the method is for list or detail.
|
||||
if '{id}' in self.path:
|
||||
return f"{self.method.capitalize()} a {model_name} object."
|
||||
return f"{self.method.capitalize()} a list of {model_name} objects."
|
||||
|
||||
25
netbox/core/forms/mixins.py
Normal file
25
netbox/core/forms/mixins.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from core.models import DataFile, DataSource
|
||||
from utilities.forms.fields import DynamicModelChoiceField
|
||||
|
||||
__all__ = (
|
||||
'SyncedDataMixin',
|
||||
)
|
||||
|
||||
|
||||
class SyncedDataMixin(forms.Form):
|
||||
data_source = DynamicModelChoiceField(
|
||||
queryset=DataSource.objects.all(),
|
||||
required=False,
|
||||
label=_('Data source')
|
||||
)
|
||||
data_file = DynamicModelChoiceField(
|
||||
queryset=DataFile.objects.all(),
|
||||
required=False,
|
||||
label=_('File'),
|
||||
query_params={
|
||||
'source_id': '$data_source',
|
||||
}
|
||||
)
|
||||
@@ -2,8 +2,8 @@ import copy
|
||||
|
||||
from django import forms
|
||||
|
||||
from core.forms.mixins import SyncedDataMixin
|
||||
from core.models import *
|
||||
from extras.forms.mixins import SyncedDataMixin
|
||||
from netbox.forms import NetBoxModelForm
|
||||
from netbox.registry import registry
|
||||
from utilities.forms import get_field_value
|
||||
|
||||
@@ -456,7 +456,7 @@ class NestedInventoryItemRoleSerializer(WritableNestedSerializer):
|
||||
# Cables
|
||||
#
|
||||
|
||||
class NestedCableSerializer(BaseModelSerializer):
|
||||
class NestedCableSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -1096,6 +1096,20 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
)
|
||||
|
||||
|
||||
class InterfaceSpeedChoices(ChoiceSet):
|
||||
key = 'Interface.speed'
|
||||
|
||||
CHOICES = [
|
||||
(10000, '10 Mbps'),
|
||||
(100000, '100 Mbps'),
|
||||
(1000000, '1 Gbps'),
|
||||
(10000000, '10 Gbps'),
|
||||
(25000000, '25 Gbps'),
|
||||
(40000000, '40 Gbps'),
|
||||
(100000000, '100 Gbps'),
|
||||
]
|
||||
|
||||
|
||||
class InterfaceDuplexChoices(ChoiceSet):
|
||||
|
||||
DUPLEX_HALF = 'half'
|
||||
|
||||
@@ -811,7 +811,7 @@ class PlatformFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Platform
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
fields = ['id', 'name', 'slug', 'napalm_driver', 'description']
|
||||
|
||||
|
||||
class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet, LocalConfigContextFilterSet):
|
||||
|
||||
@@ -12,7 +12,7 @@ from netbox.forms import NetBoxModelBulkEditForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import BulkEditForm, add_blank_choice, form_from_model
|
||||
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
||||
from utilities.forms.widgets import BulkEditNullBooleanSelect, SelectSpeedWidget
|
||||
from utilities.forms.widgets import BulkEditNullBooleanSelect, NumberWithOptions
|
||||
|
||||
__all__ = (
|
||||
'CableBulkEditForm',
|
||||
@@ -470,6 +470,10 @@ class PlatformBulkEditForm(NetBoxModelBulkEditForm):
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False
|
||||
)
|
||||
napalm_driver = forms.CharField(
|
||||
max_length=50,
|
||||
required=False
|
||||
)
|
||||
config_template = DynamicModelChoiceField(
|
||||
queryset=ConfigTemplate.objects.all(),
|
||||
required=False
|
||||
@@ -481,9 +485,9 @@ class PlatformBulkEditForm(NetBoxModelBulkEditForm):
|
||||
|
||||
model = Platform
|
||||
fieldsets = (
|
||||
(None, ('manufacturer', 'config_template', 'description')),
|
||||
(None, ('manufacturer', 'config_template', 'napalm_driver', 'description')),
|
||||
)
|
||||
nullable_fields = ('manufacturer', 'config_template', 'description')
|
||||
nullable_fields = ('manufacturer', 'config_template', 'napalm_driver', 'description')
|
||||
|
||||
|
||||
class DeviceBulkEditForm(NetBoxModelBulkEditForm):
|
||||
@@ -1169,8 +1173,9 @@ class InterfaceBulkEditForm(
|
||||
)
|
||||
speed = forms.IntegerField(
|
||||
required=False,
|
||||
widget=SelectSpeedWidget(),
|
||||
label=_('Speed')
|
||||
widget=NumberWithOptions(
|
||||
options=InterfaceSpeedChoices
|
||||
)
|
||||
)
|
||||
mgmt_only = forms.NullBooleanField(
|
||||
required=False,
|
||||
@@ -1305,6 +1310,11 @@ class FrontPortBulkEditForm(
|
||||
form_from_model(FrontPort, ['label', 'type', 'color', 'mark_connected', 'description']),
|
||||
ComponentBulkEditForm
|
||||
):
|
||||
mark_connected = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect
|
||||
)
|
||||
|
||||
model = FrontPort
|
||||
fieldsets = (
|
||||
(None, ('module', 'type', 'label', 'color', 'description', 'mark_connected')),
|
||||
@@ -1316,6 +1326,11 @@ class RearPortBulkEditForm(
|
||||
form_from_model(RearPort, ['label', 'type', 'color', 'mark_connected', 'description']),
|
||||
ComponentBulkEditForm
|
||||
):
|
||||
mark_connected = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect
|
||||
)
|
||||
|
||||
model = RearPort
|
||||
fieldsets = (
|
||||
(None, ('module', 'type', 'label', 'color', 'description', 'mark_connected')),
|
||||
|
||||
@@ -347,7 +347,7 @@ class PlatformImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = Platform
|
||||
fields = (
|
||||
'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags',
|
||||
'name', 'slug', 'manufacturer', 'config_template', 'napalm_driver', 'napalm_args', 'description', 'tags',
|
||||
)
|
||||
|
||||
|
||||
@@ -947,7 +947,7 @@ class InventoryItemImportForm(NetBoxModelImportForm):
|
||||
component_name = self.cleaned_data.get('component_name')
|
||||
device = self.cleaned_data.get("device")
|
||||
|
||||
if not device and hasattr(self, 'instance'):
|
||||
if not device and hasattr(self, 'instance') and hasattr(self.instance, 'device'):
|
||||
device = self.instance.device
|
||||
|
||||
if not all([device, content_type, component_name]):
|
||||
|
||||
@@ -12,7 +12,7 @@ from netbox.forms import NetBoxModelFilterSetForm
|
||||
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
|
||||
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
|
||||
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
|
||||
from utilities.forms.widgets import APISelectMultiple, SelectSpeedWidget
|
||||
from utilities.forms.widgets import APISelectMultiple, NumberWithOptions
|
||||
from wireless.choices import *
|
||||
|
||||
__all__ = (
|
||||
@@ -1154,8 +1154,9 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
)
|
||||
speed = forms.IntegerField(
|
||||
required=False,
|
||||
label='Speed',
|
||||
widget=SelectSpeedWidget()
|
||||
widget=NumberWithOptions(
|
||||
options=InterfaceSpeedChoices
|
||||
)
|
||||
)
|
||||
duplex = forms.MultipleChoiceField(
|
||||
choices=InterfaceDuplexChoices,
|
||||
|
||||
@@ -16,7 +16,7 @@ from utilities.forms.fields import (
|
||||
CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField,
|
||||
NumericArrayField, SlugField,
|
||||
)
|
||||
from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, SelectSpeedWidget, SelectWithPK
|
||||
from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK
|
||||
from virtualization.models import Cluster
|
||||
from wireless.models import WirelessLAN, WirelessLANGroup
|
||||
from .common import InterfaceCommonForm, ModuleCommonForm
|
||||
@@ -361,15 +361,18 @@ class PlatformForm(NetBoxModelForm):
|
||||
|
||||
fieldsets = (
|
||||
('Platform', (
|
||||
'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags',
|
||||
'name', 'slug', 'manufacturer', 'config_template', 'napalm_driver', 'napalm_args', 'description', 'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Platform
|
||||
fields = [
|
||||
'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags',
|
||||
'name', 'slug', 'manufacturer', 'config_template', 'napalm_driver', 'napalm_args', 'description', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'napalm_args': forms.Textarea(),
|
||||
}
|
||||
|
||||
|
||||
class DeviceForm(TenancyForm, NetBoxModelForm):
|
||||
@@ -1136,7 +1139,9 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
||||
'untagged_vlan', 'tagged_vlans', 'vrf', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'speed': SelectSpeedWidget(),
|
||||
'speed': NumberWithOptions(
|
||||
options=InterfaceSpeedChoices
|
||||
),
|
||||
'mode': HTMXSelect(),
|
||||
}
|
||||
labels = {
|
||||
|
||||
@@ -80,11 +80,25 @@ class ComponentTemplateModel(ChangeLoggedModel):
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Cache the original DeviceType ID for reference under clean()
|
||||
self._original_device_type = self.device_type_id
|
||||
|
||||
def to_objectchange(self, action):
|
||||
objectchange = super().to_objectchange(action)
|
||||
objectchange.related_object = self.device_type
|
||||
return objectchange
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
if self.pk is not None and self._original_device_type != self.device_type_id:
|
||||
raise ValidationError({
|
||||
"device_type": "Component templates cannot be moved to a different device type."
|
||||
})
|
||||
|
||||
|
||||
class ModularComponentTemplateModel(ComponentTemplateModel):
|
||||
"""
|
||||
@@ -119,12 +133,6 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
|
||||
),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Cache the original DeviceType ID for reference under clean()
|
||||
self._original_device_type = self.device_type_id
|
||||
|
||||
def to_objectchange(self, action):
|
||||
objectchange = super().to_objectchange(action)
|
||||
if self.device_type is not None:
|
||||
@@ -136,11 +144,6 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
if self.pk is not None and self._original_device_type != self.device_type_id:
|
||||
raise ValidationError({
|
||||
"device_type": "Component templates cannot be moved to a different device type."
|
||||
})
|
||||
|
||||
# A component template must belong to a DeviceType *or* to a ModuleType
|
||||
if self.device_type and self.module_type:
|
||||
raise ValidationError(
|
||||
|
||||
@@ -97,7 +97,8 @@ class ComponentModel(NetBoxModel):
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
if self.pk is not None and self._original_device != self.device_id:
|
||||
# Check list of Modules that allow device field to be changed
|
||||
if (type(self) not in [InventoryItem]) and (self.pk is not None) and (self._original_device != self.device_id):
|
||||
raise ValidationError({
|
||||
"device": "Components cannot be moved to a different device."
|
||||
})
|
||||
|
||||
@@ -172,6 +172,7 @@ class PlatformIndex(SearchIndex):
|
||||
fields = (
|
||||
('name', 100),
|
||||
('slug', 110),
|
||||
('napalm_driver', 300),
|
||||
('description', 500),
|
||||
)
|
||||
|
||||
|
||||
@@ -133,11 +133,11 @@ class PlatformTable(NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = models.Platform
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'config_template', 'description',
|
||||
'tags', 'actions', 'created', 'last_updated',
|
||||
'pk', 'id', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'config_template', 'napalm_driver',
|
||||
'napalm_args', 'description', 'tags', 'actions', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'description',
|
||||
'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'napalm_driver', 'description',
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1498,9 +1498,9 @@ class PlatformTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
Manufacturer.objects.bulk_create(manufacturers)
|
||||
|
||||
platforms = (
|
||||
Platform(name='Platform 1', slug='platform-1', manufacturer=manufacturers[0], description='A'),
|
||||
Platform(name='Platform 2', slug='platform-2', manufacturer=manufacturers[1], description='B'),
|
||||
Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturers[2], description='C'),
|
||||
Platform(name='Platform 1', slug='platform-1', manufacturer=manufacturers[0], napalm_driver='driver-1', description='A'),
|
||||
Platform(name='Platform 2', slug='platform-2', manufacturer=manufacturers[1], napalm_driver='driver-2', description='B'),
|
||||
Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturers[2], napalm_driver='driver-3', description='C'),
|
||||
)
|
||||
Platform.objects.bulk_create(platforms)
|
||||
|
||||
@@ -1516,6 +1516,10 @@ class PlatformTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'description': ['A', 'B']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_napalm_driver(self):
|
||||
params = {'napalm_driver': ['driver-1', 'driver-2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_manufacturer(self):
|
||||
manufacturers = Manufacturer.objects.all()[:2]
|
||||
params = {'manufacturer_id': [manufacturers[0].pk, manufacturers[1].pk]}
|
||||
|
||||
@@ -1591,6 +1591,8 @@ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
'name': 'Platform X',
|
||||
'slug': 'platform-x',
|
||||
'manufacturer': manufacturer.pk,
|
||||
'napalm_driver': 'junos',
|
||||
'napalm_args': None,
|
||||
'description': 'A new platform',
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
@@ -1610,6 +1612,7 @@ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'napalm_driver': 'ios',
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
|
||||
@@ -687,6 +687,7 @@ class RackView(generic.ObjectView):
|
||||
'next_rack': next_rack,
|
||||
'prev_rack': prev_rack,
|
||||
'svg_extra': svg_extra,
|
||||
'peer_racks': peer_racks,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ class JournalEntryKindChoices(ChoiceSet):
|
||||
|
||||
|
||||
#
|
||||
# Log Levels for Reports and Scripts
|
||||
# Reports and Scripts
|
||||
#
|
||||
|
||||
class LogLevelChoices(ChoiceSet):
|
||||
@@ -136,6 +136,17 @@ class LogLevelChoices(ChoiceSet):
|
||||
)
|
||||
|
||||
|
||||
class DurationChoices(ChoiceSet):
|
||||
|
||||
CHOICES = (
|
||||
(60, 'Hourly'),
|
||||
(720, '12 hours'),
|
||||
(1440, 'Daily'),
|
||||
(10080, 'Weekly'),
|
||||
(43200, '30 days'),
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Job results
|
||||
#
|
||||
|
||||
@@ -229,7 +229,7 @@ class ObjectListWidget(DashboardWidget):
|
||||
htmx_url = None
|
||||
if parameters := self.config.get('url_params'):
|
||||
try:
|
||||
htmx_url = f'{htmx_url}?{urlencode(parameters)}'
|
||||
htmx_url = f'{htmx_url}?{urlencode(parameters, doseq=True)}'
|
||||
except ValueError:
|
||||
pass
|
||||
return render_to_string(self.template_name, {
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from core.models import DataFile, DataSource
|
||||
from extras.models import *
|
||||
from extras.choices import CustomFieldVisibilityChoices
|
||||
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
||||
from extras.models import *
|
||||
from utilities.forms.fields import DynamicModelMultipleChoiceField
|
||||
|
||||
__all__ = (
|
||||
'CustomFieldsMixin',
|
||||
'SavedFiltersMixin',
|
||||
'SyncedDataMixin',
|
||||
)
|
||||
|
||||
|
||||
@@ -74,19 +72,3 @@ class SavedFiltersMixin(forms.Form):
|
||||
'usable': True,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class SyncedDataMixin(forms.Form):
|
||||
data_source = DynamicModelChoiceField(
|
||||
queryset=DataSource.objects.all(),
|
||||
required=False,
|
||||
label=_('Data source')
|
||||
)
|
||||
data_file = DynamicModelChoiceField(
|
||||
queryset=DataFile.objects.all(),
|
||||
required=False,
|
||||
label=_('File'),
|
||||
query_params={
|
||||
'source_id': '$data_source',
|
||||
}
|
||||
)
|
||||
|
||||
@@ -5,9 +5,9 @@ from django.db.models import Q
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from core.forms.mixins import SyncedDataMixin
|
||||
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
|
||||
from extras.choices import *
|
||||
from extras.forms.mixins import SyncedDataMixin
|
||||
from extras.models import *
|
||||
from extras.utils import FeatureQuery
|
||||
from netbox.forms import NetBoxModelForm
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from extras.choices import DurationChoices
|
||||
from utilities.forms import BootstrapMixin
|
||||
from utilities.forms.widgets import DateTimePicker, SelectDurationWidget
|
||||
from utilities.forms.widgets import DateTimePicker, NumberWithOptions
|
||||
from utilities.utils import local_now
|
||||
|
||||
__all__ = (
|
||||
@@ -21,7 +22,9 @@ class ReportForm(BootstrapMixin, forms.Form):
|
||||
required=False,
|
||||
min_value=1,
|
||||
label=_("Recurs every"),
|
||||
widget=SelectDurationWidget(),
|
||||
widget=NumberWithOptions(
|
||||
options=DurationChoices
|
||||
),
|
||||
help_text=_("Interval at which this report is re-run (in minutes)")
|
||||
)
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from extras.choices import DurationChoices
|
||||
from utilities.forms import BootstrapMixin
|
||||
from utilities.forms.widgets import DateTimePicker, SelectDurationWidget
|
||||
from utilities.forms.widgets import DateTimePicker, NumberWithOptions
|
||||
from utilities.utils import local_now
|
||||
|
||||
__all__ = (
|
||||
@@ -27,7 +28,9 @@ class ScriptForm(BootstrapMixin, forms.Form):
|
||||
required=False,
|
||||
min_value=1,
|
||||
label=_("Recurs every"),
|
||||
widget=SelectDurationWidget(),
|
||||
widget=NumberWithOptions(
|
||||
options=DurationChoices
|
||||
),
|
||||
help_text=_("Interval at which this script is re-run (in minutes)")
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
import urllib.parse
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
@@ -19,12 +20,13 @@ from extras.choices import *
|
||||
from extras.conditions import ConditionSet
|
||||
from extras.constants import *
|
||||
from extras.utils import FeatureQuery, image_upload
|
||||
from netbox.config import get_config
|
||||
from netbox.models import ChangeLoggedModel
|
||||
from netbox.models.features import (
|
||||
CloningMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin,
|
||||
)
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.utils import render_jinja2
|
||||
from utilities.utils import clean_html, render_jinja2
|
||||
|
||||
__all__ = (
|
||||
'ConfigRevision',
|
||||
@@ -278,6 +280,18 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
link = render_jinja2(self.link_url, context)
|
||||
link_target = ' target="_blank"' if self.new_window else ''
|
||||
|
||||
# Sanitize link text
|
||||
allowed_schemes = get_config().ALLOWED_URL_SCHEMES
|
||||
text = clean_html(text, allowed_schemes)
|
||||
|
||||
# Sanitize link
|
||||
link = urllib.parse.quote_plus(link, safe='/:?&=%+[]@#')
|
||||
|
||||
# Verify link scheme is allowed
|
||||
result = urllib.parse.urlparse(link)
|
||||
if result.scheme and result.scheme not in allowed_schemes:
|
||||
link = ""
|
||||
|
||||
return {
|
||||
'text': text,
|
||||
'link': link,
|
||||
|
||||
@@ -40,7 +40,6 @@ def custom_links(context, obj):
|
||||
# Pass select context data when rendering the CustomLink
|
||||
link_context = {
|
||||
'object': obj,
|
||||
'obj': obj, # TODO: Remove in NetBox v3.5
|
||||
'debug': context.get('debug', False), # django.template.context_processors.debug
|
||||
'request': context['request'], # django.template.context_processors.request
|
||||
'user': context['user'], # django.contrib.auth.context_processors.auth
|
||||
|
||||
@@ -479,8 +479,8 @@ class CustomLinkTest(TestCase):
|
||||
def test_view_object_with_custom_link(self):
|
||||
customlink = CustomLink(
|
||||
name='Test',
|
||||
link_text='FOO {{ obj.name }} BAR',
|
||||
link_url='http://example.com/?site={{ obj.slug }}',
|
||||
link_text='FOO {{ object.name }} BAR',
|
||||
link_url='http://example.com/?site={{ object.slug }}',
|
||||
new_window=False
|
||||
)
|
||||
customlink.save()
|
||||
|
||||
32
netbox/ipam/api/field_serializers.py
Normal file
32
netbox/ipam/api/field_serializers.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from ipam import models
|
||||
from netaddr import AddrFormatError, IPNetwork
|
||||
|
||||
__all__ = [
|
||||
'IPAddressField',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# IP address field
|
||||
#
|
||||
|
||||
class IPAddressField(serializers.CharField):
|
||||
"""IPAddressField with mask"""
|
||||
|
||||
default_error_messages = {
|
||||
'invalid': _('Enter a valid IPv4 or IPv6 address with optional mask.'),
|
||||
}
|
||||
|
||||
def to_internal_value(self, data):
|
||||
try:
|
||||
return IPNetwork(data)
|
||||
except AddrFormatError:
|
||||
raise serializers.ValidationError("Invalid IP address format: {}".format(data))
|
||||
except (TypeError, ValueError) as e:
|
||||
raise serializers.ValidationError(e)
|
||||
|
||||
def to_representation(self, value):
|
||||
return str(value)
|
||||
@@ -4,6 +4,7 @@ from rest_framework import serializers
|
||||
from ipam import models
|
||||
from ipam.models.l2vpn import L2VPNTermination, L2VPN
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
from .field_serializers import IPAddressField
|
||||
|
||||
__all__ = [
|
||||
'NestedAggregateSerializer',
|
||||
@@ -182,6 +183,8 @@ class NestedPrefixSerializer(WritableNestedSerializer):
|
||||
class NestedIPRangeSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:iprange-detail')
|
||||
family = serializers.IntegerField(read_only=True)
|
||||
start_address = IPAddressField()
|
||||
end_address = IPAddressField()
|
||||
|
||||
class Meta:
|
||||
model = models.IPRange
|
||||
@@ -195,6 +198,7 @@ class NestedIPRangeSerializer(WritableNestedSerializer):
|
||||
class NestedIPAddressSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
|
||||
family = serializers.IntegerField(read_only=True)
|
||||
address = IPAddressField()
|
||||
|
||||
class Meta:
|
||||
model = models.IPAddress
|
||||
|
||||
@@ -13,6 +13,7 @@ from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||
from utilities.api import get_serializer_for_model
|
||||
from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
|
||||
from .nested_serializers import *
|
||||
from .field_serializers import IPAddressField
|
||||
|
||||
|
||||
#
|
||||
@@ -369,6 +370,8 @@ class AvailablePrefixSerializer(serializers.Serializer):
|
||||
class IPRangeSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:iprange-detail')
|
||||
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
||||
start_address = IPAddressField()
|
||||
end_address = IPAddressField()
|
||||
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
status = ChoiceField(choices=IPRangeStatusChoices, required=False)
|
||||
@@ -391,10 +394,11 @@ class IPRangeSerializer(NetBoxModelSerializer):
|
||||
class IPAddressSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
|
||||
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
||||
address = IPAddressField()
|
||||
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
status = ChoiceField(choices=IPAddressStatusChoices, required=False)
|
||||
role = ChoiceField(choices=IPAddressRoleChoices, allow_blank=True, required=False, allow_null=True)
|
||||
role = ChoiceField(choices=IPAddressRoleChoices, allow_blank=True, required=False)
|
||||
assigned_object_type = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(IPADDRESS_ASSIGNMENT_MODELS),
|
||||
required=False,
|
||||
|
||||
@@ -193,6 +193,9 @@ PLUGINS = []
|
||||
REMOTE_AUTH_ENABLED = False
|
||||
REMOTE_AUTH_BACKEND = 'netbox.authentication.RemoteUserBackend'
|
||||
REMOTE_AUTH_HEADER = 'HTTP_REMOTE_USER'
|
||||
REMOTE_AUTH_USER_FIRST_NAME = 'HTTP_REMOTE_USER_FIRST_NAME'
|
||||
REMOTE_AUTH_USER_LAST_NAME = 'HTTP_REMOTE_USER_LAST_NAME'
|
||||
REMOTE_AUTH_USER_EMAIL = 'HTTP_REMOTE_USER_EMAIL'
|
||||
REMOTE_AUTH_AUTO_CREATE_USER = True
|
||||
REMOTE_AUTH_DEFAULT_GROUPS = []
|
||||
REMOTE_AUTH_DEFAULT_PERMISSIONS = {}
|
||||
|
||||
@@ -139,7 +139,17 @@ class RemoteUserMiddleware(RemoteUserMiddleware_):
|
||||
else:
|
||||
user = auth.authenticate(request, remote_user=username)
|
||||
if user:
|
||||
# User is valid. Set request.user and persist user in the session
|
||||
# User is valid.
|
||||
# Update the User's Profile if set by request headers
|
||||
if settings.REMOTE_AUTH_USER_FIRST_NAME in request.META:
|
||||
user.first_name = request.META[settings.REMOTE_AUTH_USER_FIRST_NAME]
|
||||
if settings.REMOTE_AUTH_USER_LAST_NAME in request.META:
|
||||
user.last_name = request.META[settings.REMOTE_AUTH_USER_LAST_NAME]
|
||||
if settings.REMOTE_AUTH_USER_EMAIL in request.META:
|
||||
user.email = request.META[settings.REMOTE_AUTH_USER_EMAIL]
|
||||
user.save()
|
||||
|
||||
# Set request.user and persist user in the session
|
||||
# by logging the user in.
|
||||
request.user = user
|
||||
auth.login(request, user)
|
||||
|
||||
@@ -145,9 +145,12 @@ class CachedValueSearchBackend(SearchBackend):
|
||||
)
|
||||
|
||||
# Omit any results pertaining to an object the user does not have permission to view
|
||||
return [
|
||||
r for r in results if r.object is not None
|
||||
]
|
||||
ret = []
|
||||
for r in results:
|
||||
if r.object is not None:
|
||||
r.name = str(r.object)
|
||||
ret.append(r)
|
||||
return ret
|
||||
|
||||
def cache(self, instances, indexer=None, remove_existing=True):
|
||||
content_type = None
|
||||
|
||||
@@ -3,9 +3,10 @@ import importlib
|
||||
import importlib.util
|
||||
import os
|
||||
import platform
|
||||
import requests
|
||||
import sys
|
||||
import warnings
|
||||
from urllib.parse import urlsplit
|
||||
from urllib.parse import urlencode, urlsplit
|
||||
|
||||
import django
|
||||
import sentry_sdk
|
||||
@@ -24,7 +25,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW
|
||||
# Environment setup
|
||||
#
|
||||
|
||||
VERSION = '3.5-beta2'
|
||||
VERSION = '3.5.0'
|
||||
|
||||
# Hostname
|
||||
HOSTNAME = platform.node()
|
||||
@@ -67,6 +68,15 @@ DATABASE = getattr(configuration, 'DATABASE')
|
||||
REDIS = getattr(configuration, 'REDIS')
|
||||
SECRET_KEY = getattr(configuration, 'SECRET_KEY')
|
||||
|
||||
# Enforce minimum length for SECRET_KEY
|
||||
if type(SECRET_KEY) is not str:
|
||||
raise ImproperlyConfigured(f"SECRET_KEY must be a string (found {type(SECRET_KEY).__name__})")
|
||||
if len(SECRET_KEY) < 50:
|
||||
raise ImproperlyConfigured(
|
||||
f"SECRET_KEY must be at least 50 characters in length. To generate a suitable key, run the following command:\n"
|
||||
f" python {BASE_DIR}/generate_secret_key.py"
|
||||
)
|
||||
|
||||
# Calculate a unique deployment ID from the secret key
|
||||
DEPLOYMENT_ID = hashlib.sha256(SECRET_KEY.encode('utf-8')).hexdigest()[:16]
|
||||
|
||||
@@ -78,10 +88,12 @@ BASE_PATH = getattr(configuration, 'BASE_PATH', '')
|
||||
if BASE_PATH:
|
||||
BASE_PATH = BASE_PATH.strip('/') + '/' # Enforce trailing slash only
|
||||
CSRF_COOKIE_PATH = LANGUAGE_COOKIE_PATH = SESSION_COOKIE_PATH = f'/{BASE_PATH.rstrip("/")}'
|
||||
CENSUS_REPORTING_ENABLED = getattr(configuration, 'CENSUS_REPORTING_ENABLED', True)
|
||||
CORS_ORIGIN_ALLOW_ALL = getattr(configuration, 'CORS_ORIGIN_ALLOW_ALL', False)
|
||||
CORS_ORIGIN_REGEX_WHITELIST = getattr(configuration, 'CORS_ORIGIN_REGEX_WHITELIST', [])
|
||||
CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', [])
|
||||
CSRF_COOKIE_NAME = getattr(configuration, 'CSRF_COOKIE_NAME', 'csrftoken')
|
||||
CSRF_COOKIE_SECURE = getattr(configuration, 'CSRF_COOKIE_SECURE', False)
|
||||
CSRF_TRUSTED_ORIGINS = getattr(configuration, 'CSRF_TRUSTED_ORIGINS', [])
|
||||
DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y')
|
||||
DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
|
||||
@@ -115,6 +127,9 @@ REMOTE_AUTH_DEFAULT_GROUPS = getattr(configuration, 'REMOTE_AUTH_DEFAULT_GROUPS'
|
||||
REMOTE_AUTH_DEFAULT_PERMISSIONS = getattr(configuration, 'REMOTE_AUTH_DEFAULT_PERMISSIONS', {})
|
||||
REMOTE_AUTH_ENABLED = getattr(configuration, 'REMOTE_AUTH_ENABLED', False)
|
||||
REMOTE_AUTH_HEADER = getattr(configuration, 'REMOTE_AUTH_HEADER', 'HTTP_REMOTE_USER')
|
||||
REMOTE_AUTH_USER_FIRST_NAME = getattr(configuration, 'REMOTE_AUTH_USER_FIRST_NAME', 'HTTP_REMOTE_USER_FIRST_NAME')
|
||||
REMOTE_AUTH_USER_LAST_NAME = getattr(configuration, 'REMOTE_AUTH_USER_LAST_NAME', 'HTTP_REMOTE_USER_LAST_NAME')
|
||||
REMOTE_AUTH_USER_EMAIL = getattr(configuration, 'REMOTE_AUTH_USER_EMAIL', 'HTTP_REMOTE_USER_EMAIL')
|
||||
REMOTE_AUTH_GROUP_HEADER = getattr(configuration, 'REMOTE_AUTH_GROUP_HEADER', 'HTTP_REMOTE_USER_GROUP')
|
||||
REMOTE_AUTH_GROUP_SYNC_ENABLED = getattr(configuration, 'REMOTE_AUTH_GROUP_SYNC_ENABLED', False)
|
||||
REMOTE_AUTH_SUPERUSER_GROUPS = getattr(configuration, 'REMOTE_AUTH_SUPERUSER_GROUPS', [])
|
||||
@@ -126,6 +141,7 @@ REPORTS_ROOT = getattr(configuration, 'REPORTS_ROOT', os.path.join(BASE_DIR, 're
|
||||
RQ_DEFAULT_TIMEOUT = getattr(configuration, 'RQ_DEFAULT_TIMEOUT', 300)
|
||||
SCRIPTS_ROOT = getattr(configuration, 'SCRIPTS_ROOT', os.path.join(BASE_DIR, 'scripts')).rstrip('/')
|
||||
SEARCH_BACKEND = getattr(configuration, 'SEARCH_BACKEND', 'netbox.search.backends.CachedValueSearchBackend')
|
||||
SECURE_SSL_REDIRECT = getattr(configuration, 'SECURE_SSL_REDIRECT', False)
|
||||
SENTRY_DSN = getattr(configuration, 'SENTRY_DSN', DEFAULT_SENTRY_DSN)
|
||||
SENTRY_ENABLED = getattr(configuration, 'SENTRY_ENABLED', False)
|
||||
SENTRY_SAMPLE_RATE = getattr(configuration, 'SENTRY_SAMPLE_RATE', 1.0)
|
||||
@@ -133,6 +149,7 @@ SENTRY_TRACES_SAMPLE_RATE = getattr(configuration, 'SENTRY_TRACES_SAMPLE_RATE',
|
||||
SENTRY_TAGS = getattr(configuration, 'SENTRY_TAGS', {})
|
||||
SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None)
|
||||
SESSION_COOKIE_NAME = getattr(configuration, 'SESSION_COOKIE_NAME', 'sessionid')
|
||||
SESSION_COOKIE_SECURE = getattr(configuration, 'SESSION_COOKIE_SECURE', False)
|
||||
SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d')
|
||||
SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
|
||||
SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s')
|
||||
@@ -493,6 +510,24 @@ if SENTRY_ENABLED:
|
||||
sentry_sdk.set_tag('netbox.deployment_id', DEPLOYMENT_ID)
|
||||
|
||||
|
||||
#
|
||||
# Census collection
|
||||
#
|
||||
|
||||
CENSUS_URL = 'https://census.netbox.dev/api/v1/'
|
||||
CENSUS_PARAMS = {
|
||||
'version': VERSION,
|
||||
'python_version': sys.version.split()[0],
|
||||
'deployment_id': DEPLOYMENT_ID,
|
||||
}
|
||||
if CENSUS_REPORTING_ENABLED and not DEBUG and 'test' not in sys.argv:
|
||||
try:
|
||||
# Report anonymous census data
|
||||
requests.get(f'{CENSUS_URL}?{urlencode(CENSUS_PARAMS)}', timeout=3, proxies=HTTP_PROXIES)
|
||||
except requests.exceptions.RequestException:
|
||||
pass
|
||||
|
||||
|
||||
#
|
||||
# Django social auth
|
||||
#
|
||||
@@ -581,14 +616,16 @@ REST_FRAMEWORK = {
|
||||
#
|
||||
|
||||
SPECTACULAR_SETTINGS = {
|
||||
"TITLE": "NetBox API",
|
||||
"DESCRIPTION": "API to access NetBox",
|
||||
"LICENSE": {"name": "Apache v2 License"},
|
||||
"VERSION": VERSION,
|
||||
'TITLE': 'NetBox API',
|
||||
'DESCRIPTION': 'API to access NetBox',
|
||||
'LICENSE': {'name': 'Apache v2 License'},
|
||||
'VERSION': VERSION,
|
||||
'COMPONENT_SPLIT_REQUEST': True,
|
||||
'REDOC_DIST': 'SIDECAR',
|
||||
'SERVERS': [{'url': f'/{BASE_PATH}api'}],
|
||||
'SWAGGER_UI_DIST': 'SIDECAR',
|
||||
'SWAGGER_UI_FAVICON_HREF': 'SIDECAR',
|
||||
'REDOC_DIST': 'SIDECAR',
|
||||
'POSTPROCESSING_HOOKS': [],
|
||||
}
|
||||
|
||||
#
|
||||
|
||||
@@ -215,10 +215,12 @@ class NetBoxTable(BaseTable):
|
||||
|
||||
class SearchTable(tables.Table):
|
||||
object_type = columns.ContentTypeColumn(
|
||||
verbose_name=_('Type')
|
||||
verbose_name=_('Type'),
|
||||
order_by="object___meta__verbose_name",
|
||||
)
|
||||
object = tables.Column(
|
||||
linkify=True
|
||||
linkify=True,
|
||||
order_by=('name', )
|
||||
)
|
||||
field = tables.Column()
|
||||
value = tables.Column()
|
||||
|
||||
@@ -151,6 +151,29 @@ class ExternalAuthenticationTestCase(TestCase):
|
||||
self.assertEqual(int(self.client.session.get(
|
||||
'_auth_user_id')), self.user.pk, msg='Authentication failed')
|
||||
|
||||
@override_settings(
|
||||
REMOTE_AUTH_ENABLED=True,
|
||||
LOGIN_REQUIRED=True
|
||||
)
|
||||
def test_remote_auth_user_profile(self):
|
||||
"""
|
||||
Test remote authentication with user profile details.
|
||||
"""
|
||||
headers = {
|
||||
'HTTP_REMOTE_USER': 'remoteuser1',
|
||||
'HTTP_REMOTE_USER_FIRST_NAME': 'John',
|
||||
'HTTP_REMOTE_USER_LAST_NAME': 'Smith',
|
||||
'HTTP_REMOTE_USER_EMAIL': 'johnsmith@example.com',
|
||||
}
|
||||
|
||||
response = self.client.get(reverse('home'), follow=True, **headers)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.user = User.objects.get(username='remoteuser1')
|
||||
self.assertEqual(self.user.first_name, "John", msg='User first name was not updated')
|
||||
self.assertEqual(self.user.last_name, "Smith", msg='User last name was not updated')
|
||||
self.assertEqual(self.user.email, "johnsmith@example.com", msg='User email was not updated')
|
||||
|
||||
@override_settings(
|
||||
REMOTE_AUTH_ENABLED=True,
|
||||
REMOTE_AUTH_AUTO_CREATE_USER=True,
|
||||
|
||||
@@ -20,7 +20,7 @@ from extras.signals import clear_webhooks
|
||||
from utilities.error_handlers import handle_protectederror
|
||||
from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation
|
||||
from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fields
|
||||
from utilities.forms.bulk_import import ImportForm
|
||||
from utilities.forms.bulk_import import BulkImportForm
|
||||
from utilities.htmx import is_embedded, is_htmx
|
||||
from utilities.permissions import get_permission_for_model
|
||||
from utilities.utils import get_viewname
|
||||
@@ -425,7 +425,7 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
|
||||
#
|
||||
|
||||
def get(self, request):
|
||||
form = ImportForm()
|
||||
form = BulkImportForm()
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'model': self.model_form._meta.model,
|
||||
@@ -438,7 +438,7 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
|
||||
def post(self, request):
|
||||
logger = logging.getLogger('netbox.views.BulkImportView')
|
||||
model = self.model_form._meta.model
|
||||
form = ImportForm(request.POST, request.FILES)
|
||||
form = BulkImportForm(request.POST, request.FILES)
|
||||
|
||||
if form.is_valid():
|
||||
logger.debug("Import form validation was successful")
|
||||
|
||||
7
netbox/project-static/dist/config.js
vendored
7
netbox/project-static/dist/config.js
vendored
File diff suppressed because one or more lines are too long
6
netbox/project-static/dist/config.js.map
vendored
6
netbox/project-static/dist/config.js.map
vendored
File diff suppressed because one or more lines are too long
7
netbox/project-static/dist/lldp.js
vendored
7
netbox/project-static/dist/lldp.js
vendored
File diff suppressed because one or more lines are too long
6
netbox/project-static/dist/lldp.js.map
vendored
6
netbox/project-static/dist/lldp.js.map
vendored
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
16
netbox/project-static/dist/netbox.js
vendored
16
netbox/project-static/dist/netbox.js
vendored
File diff suppressed because one or more lines are too long
4
netbox/project-static/dist/netbox.js.map
vendored
4
netbox/project-static/dist/netbox.js.map
vendored
File diff suppressed because one or more lines are too long
7
netbox/project-static/dist/status.js
vendored
7
netbox/project-static/dist/status.js
vendored
File diff suppressed because one or more lines are too long
6
netbox/project-static/dist/status.js.map
vendored
6
netbox/project-static/dist/status.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -5,6 +5,7 @@ import { initReslug } from './reslug';
|
||||
import { initSelectAll } from './selectAll';
|
||||
import { initSelectMultiple } from './selectMultiple';
|
||||
import { initMarkdownPreviews } from './markdownPreview';
|
||||
import { initSecretToggle } from './secretToggle';
|
||||
|
||||
export function initButtons(): void {
|
||||
for (const func of [
|
||||
@@ -15,6 +16,7 @@ export function initButtons(): void {
|
||||
initSelectMultiple,
|
||||
initMoveButtons,
|
||||
initMarkdownPreviews,
|
||||
initSecretToggle,
|
||||
]) {
|
||||
func();
|
||||
}
|
||||
|
||||
77
netbox/project-static/src/buttons/secretToggle.ts
Normal file
77
netbox/project-static/src/buttons/secretToggle.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { secretState } from '../stores';
|
||||
import { getElement, getElements, isTruthy } from '../util';
|
||||
|
||||
import type { StateManager } from '../state';
|
||||
|
||||
type SecretState = { hidden: boolean };
|
||||
|
||||
/**
|
||||
* Change toggle button's text and attribute to reflect the current state.
|
||||
*
|
||||
* @param hidden `true` if the current state is hidden, `false` otherwise.
|
||||
* @param button Toggle element.
|
||||
*/
|
||||
function toggleSecretButton(hidden: boolean, button: HTMLButtonElement): void {
|
||||
button.setAttribute('data-secret-visibility', hidden ? 'hidden' : 'shown');
|
||||
button.innerText = hidden ? 'Show Secret' : 'Hide Secret';
|
||||
}
|
||||
|
||||
/**
|
||||
* Show secret.
|
||||
*/
|
||||
function showSecret(): void {
|
||||
const secret = getElement('secret');
|
||||
if (isTruthy(secret)) {
|
||||
const value = secret.getAttribute('data-secret');
|
||||
if (isTruthy(value)) {
|
||||
secret.innerText = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide secret.
|
||||
*/
|
||||
function hideSecret(): void {
|
||||
const secret = getElement('secret');
|
||||
if (isTruthy(secret)) {
|
||||
const value = secret.getAttribute('data-secret');
|
||||
if (isTruthy(value)) {
|
||||
secret.innerText = '••••••••';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update secret state and visualization when the button is clicked.
|
||||
*
|
||||
* @param state State instance.
|
||||
* @param button Toggle element.
|
||||
*/
|
||||
function handleSecretToggle(state: StateManager<SecretState>, button: HTMLButtonElement): void {
|
||||
state.set('hidden', !state.get('hidden'));
|
||||
const hidden = state.get('hidden');
|
||||
|
||||
if (hidden) {
|
||||
hideSecret();
|
||||
} else {
|
||||
showSecret();
|
||||
}
|
||||
toggleSecretButton(hidden, button);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize secret toggle button.
|
||||
*/
|
||||
export function initSecretToggle(): void {
|
||||
hideSecret();
|
||||
for (const button of getElements<HTMLButtonElement>('button.toggle-secret')) {
|
||||
button.addEventListener(
|
||||
'click',
|
||||
event => {
|
||||
handleSecretToggle(secretState, event.currentTarget as HTMLButtonElement);
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { getElements } from '../util';
|
||||
* Set the value of the number input field based on the selection of the dropdown.
|
||||
*/
|
||||
export function initSpeedSelector(): void {
|
||||
for (const element of getElements<HTMLAnchorElement>('a.set_speed')) {
|
||||
for (const element of getElements<HTMLAnchorElement>('a.set_field_value')) {
|
||||
if (element !== null) {
|
||||
function handleClick(event: Event) {
|
||||
// Don't reload the page (due to href="#").
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './objectDepth';
|
||||
export * from './rackImages';
|
||||
export * from './previousPkCheck';
|
||||
export * from './secret';
|
||||
|
||||
6
netbox/project-static/src/stores/secret.ts
Normal file
6
netbox/project-static/src/stores/secret.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createState } from '../state';
|
||||
|
||||
export const secretState = createState<{ hidden: boolean }>(
|
||||
{ hidden: true },
|
||||
{ persist: true, key: 'netbox-secret' },
|
||||
);
|
||||
@@ -30,7 +30,14 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Account</th>
|
||||
<th scope="row">
|
||||
Account <i
|
||||
class="mdi mdi-alert-box text-warning"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="right"
|
||||
title="This field has been deprecated, and will be removed in NetBox v3.5."
|
||||
></i>
|
||||
</th>
|
||||
<td>{{ object.account|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -43,10 +43,36 @@
|
||||
<th scope="row">Config Template</th>
|
||||
<td>{{ object.config_template|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
NAPALM Driver
|
||||
<i
|
||||
class="mdi mdi-alert-box text-warning"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="right"
|
||||
title="This field has been deprecated, and will be removed in NetBox v3.6."
|
||||
></i>
|
||||
</th>
|
||||
<td>{{ object.napalm_driver|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
NAPALM Arguments
|
||||
<i
|
||||
class="mdi mdi-alert-box text-warning"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="right"
|
||||
title="This field has been deprecated, and will be removed in NetBox v3.6."
|
||||
></i>
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<pre>{{ object.napalm_args|json }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
|
||||
@@ -14,10 +14,29 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_controls %}
|
||||
<a {% if prev_rack %}href="{% url 'dcim:rack' pk=prev_rack.pk %}{% endif %}" class="btn btn-sm btn-primary{% if not prev_rack %} disabled{% endif %}">
|
||||
<i class="mdi mdi-chevron-left" aria-hidden="true"></i> Previous
|
||||
</a>
|
||||
<a {% if next_rack %}href="{% url 'dcim:rack' pk=next_rack.pk %}{% endif %}" class="btn btn-sm btn-primary{% if not next_rack %} disabled{% endif %}">
|
||||
<i class="mdi mdi-chevron-right" aria-hidden="true"></i> Next
|
||||
</a>
|
||||
<div class="btn-group" role="group" aria-label="RackNavigation">
|
||||
{% if prev_rack %}
|
||||
<a href="{{ prev_rack.get_absolute_url }}" class="btn btn-sm btn-primary">
|
||||
<i class="mdi mdi-chevron-left" aria-hidden="true"></i> {{ prev_rack }}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if peer_racks %}
|
||||
<div class="btn-group" role="group">
|
||||
<button class="btn btn-sm btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">{{ object }}</button>
|
||||
<ul class="dropdown-menu">
|
||||
{% for peer_rack in peer_racks %}
|
||||
<li><a class="dropdown-item{% if peer_rack.pk == object.pk %} active{% endif %}" href="{{ peer_rack.get_absolute_url }}">{{ peer_rack }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if next_rack %}
|
||||
<a href="{{ next_rack.get_absolute_url }}" class="btn btn-sm btn-primary">
|
||||
{{ next_rack }} <i class="mdi mdi-chevron-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -14,7 +14,12 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">PSK</th>
|
||||
<td class="font-monospace">{{ object.auth_psk|placeholder }}</td>
|
||||
<td>
|
||||
<span id="secret" class="font-monospace" data-secret="{{ object.auth_psk }}">{{ object.auth_psk|placeholder }}</span>
|
||||
{% if object.auth_psk %}
|
||||
<button type="button" class="btn btn-sm btn-primary toggle-secret float-end" data-bs-toggle="button">Show Secret</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.utils.html import mark_safe
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from ipam.formfields import IPNetworkFormField
|
||||
from ipam.validators import prefix_validator
|
||||
from netbox.preferences import PREFERENCES
|
||||
from utilities.forms import BootstrapMixin
|
||||
from utilities.forms.widgets import DateTimePicker
|
||||
@@ -105,7 +106,7 @@ class TokenForm(BootstrapMixin, forms.ModelForm):
|
||||
help_text=_("If no key is provided, one will be generated automatically.")
|
||||
)
|
||||
allowed_ips = SimpleArrayField(
|
||||
base_field=IPNetworkFormField(),
|
||||
base_field=IPNetworkFormField(validators=[prefix_validator]),
|
||||
required=False,
|
||||
label=_('Allowed IPs'),
|
||||
help_text=_('Allowed IPv4/IPv6 networks from where the token can be used. Leave blank for no restrictions. '
|
||||
|
||||
@@ -6,14 +6,14 @@ import yaml
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from extras.forms.mixins import SyncedDataMixin
|
||||
from core.forms.mixins import SyncedDataMixin
|
||||
from utilities.choices import ImportFormatChoices
|
||||
from utilities.forms.utils import parse_csv
|
||||
from .mixins import BootstrapMixin
|
||||
from ..choices import ImportMethodChoices
|
||||
from .forms import BootstrapMixin
|
||||
|
||||
|
||||
class ImportForm(BootstrapMixin, SyncedDataMixin, forms.Form):
|
||||
class BulkImportForm(BootstrapMixin, SyncedDataMixin, forms.Form):
|
||||
import_method = forms.ChoiceField(
|
||||
choices=ImportMethodChoices,
|
||||
required=False
|
||||
|
||||
@@ -3,6 +3,7 @@ from django import forms
|
||||
__all__ = (
|
||||
'ClearableFileInput',
|
||||
'MarkdownWidget',
|
||||
'NumberWithOptions',
|
||||
'SlugWidget',
|
||||
)
|
||||
|
||||
@@ -21,6 +22,22 @@ class MarkdownWidget(forms.Textarea):
|
||||
template_name = 'widgets/markdown_input.html'
|
||||
|
||||
|
||||
class NumberWithOptions(forms.NumberInput):
|
||||
"""
|
||||
Number field with a dropdown pre-populated with common values for convenience.
|
||||
"""
|
||||
template_name = 'widgets/number_with_options.html'
|
||||
|
||||
def __init__(self, options, attrs=None):
|
||||
self.options = options
|
||||
super().__init__(attrs)
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
context = super().get_context(name, value, attrs)
|
||||
context['widget']['options'] = self.options
|
||||
return context
|
||||
|
||||
|
||||
class SlugWidget(forms.TextInput):
|
||||
"""
|
||||
Subclass TextInput and add a slug regeneration button next to the form field.
|
||||
|
||||
@@ -7,8 +7,6 @@ __all__ = (
|
||||
'BulkEditNullBooleanSelect',
|
||||
'ColorSelect',
|
||||
'HTMXSelect',
|
||||
'SelectDurationWidget',
|
||||
'SelectSpeedWidget',
|
||||
'SelectWithPK',
|
||||
)
|
||||
|
||||
@@ -63,17 +61,3 @@ class SelectWithPK(forms.Select):
|
||||
Include the primary key of each option in the option label (e.g. "Router7 (4721)").
|
||||
"""
|
||||
option_template_name = 'widgets/select_option_with_pk.html'
|
||||
|
||||
|
||||
class SelectDurationWidget(forms.NumberInput):
|
||||
"""
|
||||
Dropdown to select one of several common options for a time duration (in minutes).
|
||||
"""
|
||||
template_name = 'widgets/select_duration.html'
|
||||
|
||||
|
||||
class SelectSpeedWidget(forms.NumberInput):
|
||||
"""
|
||||
Speed field with dropdown selections for convenience.
|
||||
"""
|
||||
template_name = 'widgets/select_speed.html'
|
||||
|
||||
11
netbox/utilities/templates/widgets/number_with_options.html
Normal file
11
netbox/utilities/templates/widgets/number_with_options.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<div class="input-group">
|
||||
{% include 'django/forms/widgets/number.html' %}
|
||||
<button type="button" class="btn btn-outline-dark border-input dropdown-toggle" data-bs-toggle="dropdown"></button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
{% for value, label in widget.options %}
|
||||
<li>
|
||||
<a href="#" target="id_{{ widget.name }}" data="{{ value }}" class="set_field_value dropdown-item">{{ label }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user