mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-27 15:47:46 -06:00
Compare commits
76 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e68901022 | ||
|
|
179c06ec20 | ||
|
|
bd8cf64ded | ||
|
|
67b42710ef | ||
|
|
67d62a2089 | ||
|
|
e24fa2ee4d | ||
|
|
5fe5b2e7c4 | ||
|
|
d68f42140f | ||
|
|
95d0ca56a7 | ||
|
|
ecb8656723 | ||
|
|
065511fca2 | ||
|
|
77f0eeb7bf | ||
|
|
f45b671fc9 | ||
|
|
b1cbdbe079 | ||
|
|
e5e7a66cb9 | ||
|
|
357ae44cde | ||
|
|
b62f2347c5 | ||
|
|
0c6726d40f | ||
|
|
cc099e86e1 | ||
|
|
a97b438b7e | ||
|
|
d7672ab260 | ||
|
|
b3d318cbe1 | ||
|
|
2804359cdd | ||
|
|
e8d08c4d38 | ||
|
|
98d9e7f8d5 | ||
|
|
51d046b1f5 | ||
|
|
88565e8f68 | ||
|
|
a2a8779ebc | ||
|
|
03ff535772 | ||
|
|
e6d364b250 | ||
|
|
be07f222f6 | ||
|
|
21f5fe873c | ||
|
|
83dc65acb5 | ||
|
|
b6c8502408 | ||
|
|
4795fab16f | ||
|
|
de2e2b5c82 | ||
|
|
cf7ab43f39 | ||
|
|
1700a9265c | ||
|
|
39b03abe72 | ||
|
|
b497b85665 | ||
|
|
0d29e5776c | ||
|
|
cbe14b76c0 | ||
|
|
3d1334a798 | ||
|
|
408550d3c7 | ||
|
|
6b9b5c4184 | ||
|
|
59dce87ba0 | ||
|
|
f6a85775d7 | ||
|
|
33887e7c69 | ||
|
|
b57ceca2fd | ||
|
|
8e13f2a9ec | ||
|
|
6af4f5d7ee | ||
|
|
6054f8197d | ||
|
|
fc98294812 | ||
|
|
4b58678823 | ||
|
|
abeed474f6 | ||
|
|
d1303f49e6 | ||
|
|
127452f4d5 | ||
|
|
2979067b65 | ||
|
|
6c07aeeded | ||
|
|
76aa255f07 | ||
|
|
0c04a8d301 | ||
|
|
6665810a6d | ||
|
|
8baf15771a | ||
|
|
045417c45c | ||
|
|
aac333a6d4 | ||
|
|
145ee11a3f | ||
|
|
94618a9dfb | ||
|
|
21e813cee2 | ||
|
|
2c014bade5 | ||
|
|
b17bfef7e5 | ||
|
|
88f7b6508c | ||
|
|
bd4f1e7d2f | ||
|
|
6e49cee718 | ||
|
|
4868818576 | ||
|
|
7cd5dc0c84 | ||
|
|
aea51df06c |
@@ -15,7 +15,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.3.0
|
||||
placeholder: v4.3.2
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
@@ -27,7 +27,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox Version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.3.0
|
||||
placeholder: v4.3.2
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
@@ -14,6 +14,12 @@ Administrators are encouraged to adhere to industry best practices concerning th
|
||||
* Prohibit access to your database from clients other than the NetBox application
|
||||
* Keep your deployment updated to the most recent stable release
|
||||
|
||||
## Compliance Reporting
|
||||
|
||||
Please note that security compliance reports (e.g. SOC 2) are provided by NetBox Labs only to customers using NetBox Cloud or NetBox Enterprise. They are not available to users of self-hosted NetBox Community Edition.
|
||||
|
||||
If you would like to consider upgrading to NetBox Cloud or Enterprise, please contact `sales@netboxlabs.com`.
|
||||
|
||||
## Reporting a Suspected Vulnerability
|
||||
|
||||
If you believe you've uncovered a security vulnerability and wish to report it confidentially, you may do so by emailing `security@netboxlabs.com`. Please ensure that your report meets all the following conditions:
|
||||
|
||||
@@ -329,6 +329,7 @@
|
||||
"100base-tx",
|
||||
"100base-t1",
|
||||
"1000base-t",
|
||||
"1000base-sx",
|
||||
"1000base-lx",
|
||||
"1000base-tx",
|
||||
"2.5gbase-t",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
Default: `None`
|
||||
|
||||
Defines a Sentry data source name (DSN) for automated error reporting. `SENTRY_ENABLED` must be True for this parameter to take effect. For example:
|
||||
Defines a Sentry data source name (DSN) for automated error reporting. `SENTRY_ENABLED` must be `True` for this parameter to take effect. For example:
|
||||
|
||||
```
|
||||
SENTRY_DSN = "https://examplePublicKey@o0.ingest.sentry.io/0"
|
||||
@@ -16,7 +16,7 @@ SENTRY_DSN = "https://examplePublicKey@o0.ingest.sentry.io/0"
|
||||
|
||||
Default: `False`
|
||||
|
||||
Set to True to enable automatic error reporting via [Sentry](https://sentry.io/).
|
||||
Set to `True` to enable automatic error reporting via [Sentry](https://sentry.io/).
|
||||
|
||||
!!! note
|
||||
The `sentry-sdk` Python package is required to enable Sentry integration.
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
Default: `True`
|
||||
|
||||
Setting this to False will disable the GraphQL API.
|
||||
Setting this to `False` will disable the GraphQL API.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ Sets content for the top banner in the user interface.
|
||||
|
||||
Default: `True`
|
||||
|
||||
Enables anonymous census reporting. To opt out of census reporting, set this to False.
|
||||
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.
|
||||
|
||||
@@ -102,7 +102,7 @@ The maximum size (in bytes) of an incoming HTTP request (i.e. `GET` or `POST` da
|
||||
|
||||
Default: `True`
|
||||
|
||||
By default, NetBox will prevent the creation of duplicate prefixes and IP addresses in the global table (that is, those which are not assigned to any VRF). This validation can be disabled by setting `ENFORCE_GLOBAL_UNIQUE` to False.
|
||||
By default, NetBox will prevent the creation of duplicate prefixes and IP addresses in the global table (that is, those which are not assigned to any VRF). This validation can be disabled by setting `ENFORCE_GLOBAL_UNIQUE` to `False`.
|
||||
|
||||
---
|
||||
|
||||
@@ -143,7 +143,7 @@ The number of days to retain job results (scripts and reports). Set this to `0`
|
||||
|
||||
Default: `False`
|
||||
|
||||
Setting this to True will display a "maintenance mode" banner at the top of every page. Additionally, NetBox will no longer update a user's "last active" time upon login. This is to allow new logins when the database is in a read-only state. Recording of login times will resume when maintenance mode is disabled.
|
||||
Setting this to `True` will display a "maintenance mode" banner at the top of every page. Additionally, NetBox will no longer update a user's "last active" time upon login. This is to allow new logins when the database is in a read-only state. Recording of login times will resume when maintenance mode is disabled.
|
||||
|
||||
---
|
||||
|
||||
@@ -181,7 +181,7 @@ Toggle the availability Prometheus-compatible metrics at `/metrics`. See the [Pr
|
||||
|
||||
Default: `False`
|
||||
|
||||
When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to prefer IPv4 instead.
|
||||
When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to `True` to prefer IPv4 instead.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ Note that a plugin must be listed in `PLUGINS` for its configuration to take eff
|
||||
|
||||
## PLUGINS_CATALOG_CONFIG
|
||||
|
||||
Default: Empty
|
||||
Default: `{}` (Empty)
|
||||
|
||||
This parameter controls how individual plugins are displayed in the plugins catalog under Admin > System > Plugins. Adding a plugin to the `hidden` list will omit that plugin from the catalog. Adding a plugin to the `static` list will display the plugin, but not link to the plugin details or upgrade instructions.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Remote Authentication Settings
|
||||
|
||||
The configuration parameters listed here control remote authentication for NetBox. Note that `REMOTE_AUTH_ENABLED` must be true in order for these settings to take effect.
|
||||
The configuration parameters listed here control remote authentication for NetBox. Note that `REMOTE_AUTH_ENABLED` must be `True` in order for these settings to take effect.
|
||||
|
||||
---
|
||||
|
||||
@@ -8,7 +8,7 @@ The configuration parameters listed here control remote authentication for NetBo
|
||||
|
||||
Default: `False`
|
||||
|
||||
If true, NetBox will automatically create groups specified in the `REMOTE_AUTH_GROUP_HEADER` header if they don't already exist. (Requires `REMOTE_AUTH_ENABLED`.)
|
||||
If `True`, NetBox will automatically create groups specified in the `REMOTE_AUTH_GROUP_HEADER` header if they don't already exist. (Requires `REMOTE_AUTH_ENABLED`.)
|
||||
|
||||
---
|
||||
|
||||
@@ -16,7 +16,7 @@ If true, NetBox will automatically create groups specified in the `REMOTE_AUTH_G
|
||||
|
||||
Default: `False`
|
||||
|
||||
If true, NetBox will automatically create local accounts for users authenticated via a remote service. (Requires `REMOTE_AUTH_ENABLED`.)
|
||||
If `True`, NetBox will automatically create local accounts for users authenticated via a remote service. (Requires `REMOTE_AUTH_ENABLED`.)
|
||||
|
||||
---
|
||||
|
||||
@@ -43,7 +43,7 @@ The list of groups to assign a new user account when created using remote authen
|
||||
|
||||
Default: `{}` (Empty dictionary)
|
||||
|
||||
A mapping of permissions to assign a new user account when created using remote authentication. Each key in the dictionary should be set to a dictionary of the attributes to be applied to the permission, or `None` to allow all objects. (Requires `REMOTE_AUTH_ENABLED` as True and `REMOTE_AUTH_GROUP_SYNC_ENABLED` as False.)
|
||||
A mapping of permissions to assign a new user account when created using remote authentication. Each key in the dictionary should be set to a dictionary of the attributes to be applied to the permission, or `None` to allow all objects. (Requires `REMOTE_AUTH_ENABLED` as `True` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` as `False`.)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
## ALLOWED_HOSTS
|
||||
|
||||
This is a list of valid fully-qualified domain names (FQDNs) and/or IP addresses that can be used to reach the NetBox service. Usually this is the same as the hostname for the NetBox server, but can also be different; for example, when using a reverse proxy serving the NetBox website under a different FQDN than the hostname of the NetBox server. To help guard against [HTTP Host header attacks](https://docs.djangoproject.com/en/3.0/topics/security/#host-headers-virtual-hosting), NetBox will not permit access to the server via any other hostnames (or IPs).
|
||||
This is a list of valid fully-qualified domain names (FQDNs) and/or IP addresses that can be used to reach the NetBox service. Usually this is the same as the hostname for the NetBox server, but can also be different; for example, when using a reverse proxy serving the NetBox website under a different FQDN than the hostname of the NetBox server. To help guard against [HTTP Host header attacks](https://docs.djangoproject.com/en/stable/topics/security/#host-headers-virtual-hosting), NetBox will not permit access to the server via any other hostnames (or IPs).
|
||||
|
||||
!!! note
|
||||
This parameter must always be defined as a list or tuple, even if only a single value is provided.
|
||||
|
||||
The value of this option is also used to set `CSRF_TRUSTED_ORIGINS`, which restricts POST requests to the same set of hosts (more about this [here](https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-CSRF_TRUSTED_ORIGINS)). Keep in mind that NetBox, by default, sets `USE_X_FORWARDED_HOST` to true, which means that if you're using a reverse proxy, it's the FQDN used to reach that reverse proxy which needs to be in this list (more about this [here](https://docs.djangoproject.com/en/stable/ref/settings/#allowed-hosts)).
|
||||
The value of this option is also used to set `CSRF_TRUSTED_ORIGINS`, which restricts POST requests to the same set of hosts (more about this [here](https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-CSRF_TRUSTED_ORIGINS)). Keep in mind that NetBox, by default, sets `USE_X_FORWARDED_HOST` to `True`, which means that if you're using a reverse proxy, it's the FQDN used to reach that reverse proxy which needs to be in this list (more about this [here](https://docs.djangoproject.com/en/stable/ref/settings/#allowed-hosts)).
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
Default: `False`
|
||||
|
||||
!!! note
|
||||
The default value of this parameter changed from true to false in NetBox v4.3.0.
|
||||
The default value of this parameter changed from `True` to `False` in NetBox v4.3.0.
|
||||
|
||||
If disabled, the values of API tokens will not be displayed after each token's initial creation. A user **must** record the value of a token prior to its creation, or it will be lost. Note that this affects _all_ users, regardless of assigned permissions.
|
||||
|
||||
@@ -52,7 +52,7 @@ Although it is not recommended, the default validation rules can be disabled by
|
||||
|
||||
Default: `False`
|
||||
|
||||
If True, cross-origin resource sharing (CORS) requests will be accepted from all origins. If False, a whitelist will be used (see below).
|
||||
If `True`, cross-origin resource sharing (CORS) requests will be accepted from all origins. If False, a whitelist will be used (see below).
|
||||
|
||||
---
|
||||
|
||||
@@ -62,7 +62,7 @@ If True, cross-origin resource sharing (CORS) requests will be accepted from all
|
||||
|
||||
These settings specify a list of origins that are authorized to make cross-site API requests. Use
|
||||
`CORS_ORIGIN_WHITELIST` to define a list of exact hostnames, or `CORS_ORIGIN_REGEX_WHITELIST` to define a set of regular
|
||||
expressions. (These settings have no effect if `CORS_ORIGIN_ALLOW_ALL` is True.) For example:
|
||||
expressions. (These settings have no effect if `CORS_ORIGIN_ALLOW_ALL` is `True`.) For example:
|
||||
|
||||
```python
|
||||
CORS_ORIGIN_WHITELIST = [
|
||||
@@ -84,7 +84,7 @@ The name of the cookie to use for the cross-site request forgery (CSRF) authenti
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -92,7 +92,7 @@ If true, the cookie employed for cross-site request forgery (CSRF) protection wi
|
||||
|
||||
Default: `[]`
|
||||
|
||||
Defines a list of trusted origins for unsafe (e.g. `POST`) requests. This is a pass-through to Django's [`CSRF_TRUSTED_ORIGINS`](https://docs.djangoproject.com/en/4.0/ref/settings/#std:setting-CSRF_TRUSTED_ORIGINS) setting. Note that each host listed must specify a scheme (e.g. `http://` or `https://).
|
||||
Defines a list of trusted origins for unsafe (e.g. `POST`) requests. This is a pass-through to Django's [`CSRF_TRUSTED_ORIGINS`](https://docs.djangoproject.com/en/stable/ref/settings/#csrf-trusted-origins) setting. Note that each host listed must specify a scheme (e.g. `http://` or `https://).
|
||||
|
||||
```python
|
||||
CSRF_TRUSTED_ORIGINS = (
|
||||
@@ -135,7 +135,7 @@ DEFAULT_PERMISSIONS = {
|
||||
|
||||
## EXEMPT_VIEW_PERMISSIONS
|
||||
|
||||
Default: Empty list
|
||||
Default: `[]` (Empty list)
|
||||
|
||||
A list of NetBox models to exempt from the enforcement of view permissions. Models listed here will be viewable by all users, both authenticated and anonymous.
|
||||
|
||||
@@ -164,7 +164,7 @@ EXEMPT_VIEW_PERMISSIONS = ['*']
|
||||
|
||||
Default: `False`
|
||||
|
||||
If true, the lifetime of a user's authentication session will be automatically reset upon each valid request. For example, if [`LOGIN_TIMEOUT`](#login_timeout) is configured to 14 days (the default), and a user whose session is due to expire in five days makes a NetBox request (with a valid session cookie), the session's lifetime will be reset to 14 days.
|
||||
If `True`, the lifetime of a user's authentication session will be automatically reset upon each valid request. For example, if [`LOGIN_TIMEOUT`](#login_timeout) is configured to 14 days (the default), and a user whose session is due to expire in five days makes a NetBox request (with a valid session cookie), the session's lifetime will be reset to 14 days.
|
||||
|
||||
Note that enabling this setting causes NetBox to update a user's session in the database (or file, as configured per [`SESSION_FILE_PATH`](#session_file_path)) with each request, which may introduce significant overhead in very active environments. It also permits an active user to remain authenticated to NetBox indefinitely.
|
||||
|
||||
@@ -191,7 +191,7 @@ The lifetime (in seconds) of the authentication cookie issued to a NetBox user u
|
||||
|
||||
## LOGIN_FORM_HIDDEN
|
||||
|
||||
Default: False
|
||||
Default: `False`
|
||||
|
||||
Option to hide the login form when only SSO authentication is in use.
|
||||
|
||||
@@ -212,7 +212,7 @@ The view name or URL to which a user is redirected after logging out.
|
||||
|
||||
Default: `False`
|
||||
|
||||
If true, the `includeSubDomains` directive will be included in the HTTP Strict Transport Security (HSTS) header. This directive instructs the browser to apply the HSTS policy to all subdomains of the current domain.
|
||||
If `True`, the `includeSubDomains` directive will be included in the HTTP Strict Transport Security (HSTS) header. This directive instructs the browser to apply the HSTS policy to all subdomains of the current domain.
|
||||
|
||||
---
|
||||
|
||||
@@ -220,7 +220,7 @@ If true, the `includeSubDomains` directive will be included in the HTTP Strict T
|
||||
|
||||
Default: `False`
|
||||
|
||||
If true, the `preload` directive will be included in the HTTP Strict Transport Security (HSTS) header. This directive instructs the browser to preload the site in HTTPS. Browsers that use the HSTS preload list will force the site to be accessed via HTTPS even if the user types HTTP in the address bar.
|
||||
If `True`, the `preload` directive will be included in the HTTP Strict Transport Security (HSTS) header. This directive instructs the browser to preload the site in HTTPS. Browsers that use the HSTS preload list will force the site to be accessed via HTTPS even if the user types HTTP in the address bar.
|
||||
|
||||
---
|
||||
|
||||
@@ -236,7 +236,7 @@ If set to a non-zero integer value, the SecurityMiddleware sets the HTTP Strict
|
||||
|
||||
Default: `False`
|
||||
|
||||
If true, all non-HTTPS requests will be automatically redirected to use HTTPS.
|
||||
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.
|
||||
@@ -255,7 +255,7 @@ The name used for the session cookie. See the [Django documentation](https://doc
|
||||
|
||||
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.
|
||||
If `True`, the cookie employed for session authentication will be marked as secure, meaning that it can only be sent across an HTTPS connection.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ Default: `('127.0.0.1', '::1')`
|
||||
|
||||
A list of IP addresses recognized as internal to the system, used to control the display of debugging output. For
|
||||
example, the debugging toolbar will be viewable only when a client is accessing NetBox from one of the listed IP
|
||||
addresses (and [`DEBUG`](./development.md#debug) is true).
|
||||
addresses (and [`DEBUG`](./development.md#debug) is `True`).
|
||||
|
||||
---
|
||||
|
||||
@@ -103,7 +103,7 @@ addresses (and [`DEBUG`](./development.md#debug) is true).
|
||||
|
||||
Default: `False`
|
||||
|
||||
Set this configuration parameter to True for NetBox deployments which do not have Internet access. This will disable miscellaneous functionality which depends on access to the Internet.
|
||||
Set this configuration parameter to `True` for NetBox deployments which do not have Internet access. This will disable miscellaneous functionality which depends on access to the Internet.
|
||||
|
||||
!!! note
|
||||
If Internet access is available via a proxy, set [`HTTP_PROXIES`](#http_proxies) instead.
|
||||
@@ -114,7 +114,7 @@ Set this configuration parameter to True for NetBox deployments which do not hav
|
||||
|
||||
Default: `{}`
|
||||
|
||||
A dictionary of custom jinja2 filters with the key being the filter name and the value being a callable. For more information see the [Jinja2 documentation](https://jinja.palletsprojects.com/en/3.1.x/api/#custom-filters). For example:
|
||||
A dictionary of custom Jinja2 filters with the key being the filter name and the value being a callable. For more information see the [Jinja2 documentation](https://jinja.palletsprojects.com/en/3.1.x/api/#custom-filters). For example:
|
||||
|
||||
```python
|
||||
def uppercase(x):
|
||||
|
||||
@@ -53,6 +53,8 @@ If a new Django release is adopted or other major dependencies (Python, PostgreS
|
||||
|
||||
* Update the installation guide (`docs/installation/index.md`) with the new minimum versions.
|
||||
* Update the upgrade guide (`docs/installation/upgrading.md`) for the current version accordingly.
|
||||
* Update the minimum PostgreSQL version in the programming error template (`netbox/templates/exceptions/programming_error.html`).
|
||||
* Update the minimum and supported Python versions in the project metadata file (`pyproject.toml`)
|
||||
|
||||
### Manually Perform a New Install
|
||||
|
||||
@@ -164,7 +166,7 @@ Then, compile these portable (`.po`) files for use in the application:
|
||||
|
||||
### Update Version and Changelog
|
||||
|
||||
* Update the version number and date in `netbox/release.yaml`. Add or remove the designation (e.g. `beta1`) if applicable.
|
||||
* Update the version number and date in `netbox/release.yaml` and `pyproject.toml`. Add or remove the designation (e.g. `beta1`) if applicable.
|
||||
* Update the example version numbers in the feature request and bug report templates under `.github/ISSUE_TEMPLATES/`.
|
||||
* Add a section for this release at the top of the changelog page for the minor version (e.g. `docs/release-notes/version-4.2.md`) listing all relevant changes made in this release.
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ NetBox is the leading solution for modeling and documenting modern networks. By
|
||||
|
||||
## :material-server-network: Built for Networks
|
||||
|
||||
Unlike general-purpose CMDBs, NetBox has curated a data model which caters specifically to the needs of network engineers and operators. It delivers a wide assortment of object types carefully crafted to best serve the needs of infrastructure design and documentation. These cover all facets of network technology, from IP address managements to cabling to overlays and more:
|
||||
Unlike general-purpose configuration management databases (CMDBs), NetBox has curated a data model which caters specifically to the needs of network engineers and operators. It delivers a wide assortment of object types carefully crafted to best serve the needs of infrastructure design and documentation. These cover all facets of network technology, from IP address managements to cabling to overlays and more:
|
||||
|
||||
* Hierarchical regions, sites, and locations
|
||||
* Racks, devices, and device components
|
||||
|
||||
@@ -108,7 +108,7 @@ Open `configuration.py` with your preferred editor to begin configuring NetBox.
|
||||
|
||||
### ALLOWED_HOSTS
|
||||
|
||||
This is a list of the valid hostnames and IP addresses by which this server can be reached. You must specify at least one name or IP address. (Note that this does not restrict the locations from which NetBox may be accessed: It is merely for [HTTP host header validation](https://docs.djangoproject.com/en/3.0/topics/security/#host-headers-virtual-hosting).)
|
||||
This is a list of the valid hostnames and IP addresses by which this server can be reached. You must specify at least one name or IP address. (Note that this does not restrict the locations from which NetBox may be accessed: It is merely for [HTTP host header validation](https://docs.djangoproject.com/en/stable/topics/security/#host-headers-virtual-hosting).)
|
||||
|
||||
```python
|
||||
ALLOWED_HOSTS = ['netbox.example.com', '192.0.2.123']
|
||||
|
||||
@@ -28,7 +28,7 @@ NetBox ships with a default configuration file for uWSGI. To use it, copy `/opt/
|
||||
sudo cp /opt/netbox/contrib/uwsgi.ini /opt/netbox/uwsgi.ini
|
||||
```
|
||||
|
||||
While the provided configuration should suffice for most initial installations, you may wish to edit this file to change the bound IP address and/or port number, or to make performance-related adjustments. See [the uWSGI documentation](https://uwsgi-docs-additions.readthedocs.io/en/latest/Options.html) for the available configuration parameters and take a minute to review the [Things to know](https://uwsgi-docs.readthedocs.io/en/latest/ThingsToKnow.html) page. Django also provides [additional documentation](https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/uwsgi/) on configuring uWSGI with a Django app.
|
||||
While the provided configuration should suffice for most initial installations, you may wish to edit this file to change the bound IP address and/or port number, or to make performance-related adjustments. See [the uWSGI documentation](https://uwsgi-docs-additions.readthedocs.io/en/latest/Options.html) for the available configuration parameters and take a minute to review the [Things to know](https://uwsgi-docs.readthedocs.io/en/latest/ThingsToKnow.html) page. Django also provides [additional documentation](https://docs.djangoproject.com/en/stable/howto/deployment/wsgi/uwsgi/) on configuring uWSGI with a Django app.
|
||||
|
||||
## systemd Setup
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ sudo cp /opt/netbox-$OLDVER/gunicorn.py /opt/netbox/
|
||||
|
||||
### Option B: Check Out a Git Release
|
||||
|
||||
This guide assumes that NetBox is installed at `/opt/netbox`. First, determine the latest release either by visiting our [releases page](https://github.com/netbox-community/netbox/releases) or by running the following command:
|
||||
This guide assumes that NetBox is installed in `/opt/netbox`. First, determine the latest release either by visiting our [releases page](https://github.com/netbox-community/netbox/releases) or by running the following command:
|
||||
|
||||
```
|
||||
git ls-remote --tags https://github.com/netbox-community/netbox.git \
|
||||
@@ -134,6 +134,8 @@ git ls-remote --tags https://github.com/netbox-community/netbox.git \
|
||||
Check out the desired release by specifying its tag. For example:
|
||||
|
||||
```
|
||||
cd /opt/netbox && \
|
||||
sudo git fetch && \
|
||||
sudo git checkout v4.2.7
|
||||
```
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ from netbox.plugins import PluginConfig
|
||||
|
||||
### ContentType renamed to ObjectType
|
||||
|
||||
NetBox's proxy model for Django's [ContentType model](https://docs.djangoproject.com/en/5.0/ref/contrib/contenttypes/#the-contenttype-model) has been renamed to ObjectType for clarity. In general, plugins should use the ObjectType proxy when referencing content types, as it includes several custom manager methods. The one exception to this is when defining [generic foreign keys](https://docs.djangoproject.com/en/5.0/ref/contrib/contenttypes/#generic-relations): The ForeignKey field used for a GFK should point to Django's native ContentType.
|
||||
NetBox's proxy model for Django's [ContentType model](https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/#the-contenttype-model) has been renamed to ObjectType for clarity. In general, plugins should use the ObjectType proxy when referencing content types, as it includes several custom manager methods. The one exception to this is when defining [generic foreign keys](https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/#generic-relations): The ForeignKey field used for a GFK should point to Django's native ContentType.
|
||||
|
||||
Additionally, plugin maintainers are strongly encouraged to adopt the "object type" terminology for field and filter names wherever feasible to be consistent with NetBox core (however this is not required for compatibility).
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Views
|
||||
|
||||
## Writing Views
|
||||
## Writing Basic Views
|
||||
|
||||
If your plugin will provide its own page or pages within the NetBox web UI, you'll need to define views. A view is a piece of business logic which performs an action and/or renders a page when a request is made to a particular URL. HTML content is rendered using a [template](./templates.md). Views are typically defined in `views.py`, and URL patterns in `urls.py`.
|
||||
|
||||
@@ -47,9 +47,13 @@ A URL pattern has three components:
|
||||
|
||||
This makes our view accessible at the URL `/plugins/animal-sounds/random/`. (Remember, our `AnimalSoundsConfig` class sets our plugin's base URL to `animal-sounds`.) Viewing this URL should show the base NetBox template with our custom content inside it.
|
||||
|
||||
## NetBox Model Views
|
||||
|
||||
NetBox provides several generic view classes and additional helper functions, to simplify the implementation of plugin logic. These are recommended to be used whenever possible to keep the maintenance overhead of plugins low.
|
||||
|
||||
### View Classes
|
||||
|
||||
NetBox provides several generic view classes (documented below) to facilitate common operations, such as creating, viewing, modifying, and deleting objects. Plugins can subclass these views for their own use.
|
||||
Generic view classes (documented below) facilitate common operations, such as creating, viewing, modifying, and deleting objects. Plugins can subclass these views for their own use.
|
||||
|
||||
| View Class | Description |
|
||||
|----------------------|--------------------------------------------------------|
|
||||
@@ -65,18 +69,51 @@ NetBox provides several generic view classes (documented below) to facilitate co
|
||||
!!! warning
|
||||
Please note that only the classes which appear in this documentation are currently supported. Although other classes may be present within the `views.generic` module, they are not yet supported for use by plugins.
|
||||
|
||||
#### Example Usage
|
||||
### URL registration
|
||||
|
||||
The NetBox URL registration process has two parts:
|
||||
|
||||
1. View classes can be decorated with `@register_model_view()`. This registers a new URL for the model.
|
||||
2. All of a model's URLs can be included in `urls.py` using the `get_model_urls()` function. This call is usually required twice: once to import general views for the model and again to import model detail views tied to the object's primary key.
|
||||
|
||||
::: utilities.views.register_model_view
|
||||
|
||||
!!! note "Changed in NetBox v4.2"
|
||||
In NetBox v4.2, the `register_model_view()` function was extended to support the registration of list views by passing `detail=False`.
|
||||
|
||||
::: utilities.urls.get_model_urls
|
||||
|
||||
!!! note "Changed in NetBox v4.2"
|
||||
In NetBox v4.2, the `get_model_urls()` function was extended to support retrieving registered general model views (e.g. for listing objects) by passing `detail=False`.
|
||||
|
||||
### Example Usage
|
||||
|
||||
```python
|
||||
# views.py
|
||||
from netbox.views.generic import ObjectEditView
|
||||
from utilities.views import register_model_view
|
||||
from .models import Thing
|
||||
|
||||
@register_model_view(Thing, name='add', detail=False)
|
||||
@register_model_view(Thing, name='edit')
|
||||
class ThingEditView(ObjectEditView):
|
||||
queryset = Thing.objects.all()
|
||||
template_name = 'myplugin/thing.html'
|
||||
...
|
||||
```
|
||||
|
||||
```python
|
||||
# urls.py
|
||||
from django.urls import include, path
|
||||
from utilities.urls import get_model_urls
|
||||
|
||||
urlpatterns = [
|
||||
path('thing/', include(get_model_urls('myplugin', 'thing', detail=False))),
|
||||
path('thing/<int:pk>/', include(get_model_urls('myplugin', 'thing'))),
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
## Object Views
|
||||
|
||||
Below are the class definitions for NetBox's object views. These views handle CRUD actions for individual objects. The view, add/edit, and delete views each inherit from `BaseObjectView`, which is not intended to be used directly.
|
||||
@@ -143,6 +180,9 @@ Below are the class definitions for NetBox's multi-object views. These views han
|
||||
|
||||
These views are provided to enable or enhance certain NetBox model features, such as change logging or journaling. These typically do not need to be subclassed: They can be used directly e.g. in a URL path.
|
||||
|
||||
!!! note
|
||||
These feature views are automatically registered for all models that implement the respective feature. There is usually no need to override them. However, if that's the case, the URL must be registered manually in `urls.py` instead of using the `register_model_view()` function or decorator.
|
||||
|
||||
::: netbox.views.generic.ObjectChangeLogView
|
||||
options:
|
||||
members:
|
||||
@@ -157,7 +197,7 @@ These views are provided to enable or enhance certain NetBox model features, suc
|
||||
|
||||
### Additional Tabs
|
||||
|
||||
Plugins can "attach" a custom view to a core NetBox model by registering it with `register_model_view()`. To include a tab for this view within the NetBox UI, declare a TabView instance named `tab`, and add it to the template context dict:
|
||||
Plugins can "attach" a custom view to a NetBox model by registering it with `register_model_view()`. To include a tab for this view within the NetBox UI, declare a TabView instance named `tab`, and add it to the template context dict:
|
||||
|
||||
```python
|
||||
from dcim.models import Site
|
||||
@@ -185,11 +225,6 @@ class MyView(generic.ObjectView):
|
||||
)
|
||||
```
|
||||
|
||||
!!! note "Changed in NetBox v4.2"
|
||||
The `register_model_view()` function was extended in NetBox v4.2 to support registration of list views by passing `detail=False`.
|
||||
|
||||
::: utilities.views.register_model_view
|
||||
|
||||
::: utilities.views.ViewTab
|
||||
|
||||
### Extra Template Content
|
||||
|
||||
@@ -1,3 +1,61 @@
|
||||
# NetBox v4.3
|
||||
|
||||
## v4.3.2 (2025-06-05)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#19200](https://github.com/netbox-community/netbox/issues/19200) - Display assigned virtual chassis (if any) on device view
|
||||
* [#19461](https://github.com/netbox-community/netbox/issues/19461) - Add color backgrounds for virtual circuit types
|
||||
* [#19605](https://github.com/netbox-community/netbox/issues/19605) - Enable filtering IP addresses by family in GraphQL API
|
||||
* [#19627](https://github.com/netbox-community/netbox/issues/19627) - Introduce object change migrators
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#19415](https://github.com/netbox-community/netbox/issues/19415) - Increase maximum supported distance for circuits and wireless links
|
||||
* [#19475](https://github.com/netbox-community/netbox/issues/19475) - VLANs belonging to the same location as a VM's cluster should be eligible for assignment to interfaces on that VM
|
||||
* [#19486](https://github.com/netbox-community/netbox/issues/19486) - Fix connection card rendering for console server ports
|
||||
* [#19487](https://github.com/netbox-community/netbox/issues/19487) - Fix `FieldError` exception when ordering circuit or tunnel terminations by the terminating object
|
||||
* [#19490](https://github.com/netbox-community/netbox/issues/19490) - Fix inclusion support for config templates populated via a data source
|
||||
* [#19496](https://github.com/netbox-community/netbox/issues/19496) - Fix `AttributeError` exception when rendering a config template with no output
|
||||
* [#19510](https://github.com/netbox-community/netbox/issues/19510) - Restore GraphQL API filtering for assigned IP addresses
|
||||
* [#19520](https://github.com/netbox-community/netbox/issues/19520) - Restore ability to alter prefix scope via the REST API
|
||||
* [#19587](https://github.com/netbox-community/netbox/issues/19587) - The `occupied` filter should include interfaces terminating a wireless link
|
||||
* [#19599](https://github.com/netbox-community/netbox/issues/19599) - Fix `AttributeError` exception when sorting change history under user view
|
||||
* [#19610](https://github.com/netbox-community/netbox/issues/19610) - Fix `FieldError` exception when sorting tunnel terminations by tenant
|
||||
* [#19623](https://github.com/netbox-community/netbox/issues/19623) - Display description under provider account view
|
||||
|
||||
---
|
||||
|
||||
## v4.3.1 (2025-05-13)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#17073](https://github.com/netbox-community/netbox/issues/17073) - Enable global search for tags
|
||||
* [#18419](https://github.com/netbox-community/netbox/issues/18419) - Enable specifying a queue name when calling `Job.enqueue()`
|
||||
* [#19416](https://github.com/netbox-community/netbox/issues/19416) - Add the 1000BASE-SX interface type
|
||||
* [#19434](https://github.com/netbox-community/netbox/issues/19434) - Add pre-populated interface speed choices for 2.5 and 5 Gbps
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#17107](https://github.com/netbox-community/netbox/issues/17107) - Fix cosmetic issue in cable traces ending at a provider network
|
||||
* [#19309](https://github.com/netbox-community/netbox/issues/19309) - Improve REST API query performance for prefixes and IP addresses
|
||||
* [#19361](https://github.com/netbox-community/netbox/issues/19361) - Fix incorrect GraphQL object types
|
||||
* [#19375](https://github.com/netbox-community/netbox/issues/19375) - Fix table configuration after applying a saved table config
|
||||
* [#19376](https://github.com/netbox-community/netbox/issues/19376) - Fix `FieldDoesNotExist` exception when global search results include a contact
|
||||
* [#19380](https://github.com/netbox-community/netbox/issues/19380) - Fix column selections for child object tables
|
||||
* [#19381](https://github.com/netbox-community/netbox/issues/19381) - Fix syncing of custom scripts from a remote data source
|
||||
* [#19396](https://github.com/netbox-community/netbox/issues/19396) - Enable nullifying VLAN `qinq_role` via the REST API
|
||||
* [#19397](https://github.com/netbox-community/netbox/issues/19397) - Correct enum type for IPRangeFilter in GraphQL API
|
||||
* [#19432](https://github.com/netbox-community/netbox/issues/19432) - Update minimum required PostgreSQL version referenced by server error page
|
||||
* [#19440](https://github.com/netbox-community/netbox/issues/19440) - Ensure data migrations use the correct database connection
|
||||
* [#19444](https://github.com/netbox-community/netbox/issues/19444) - Fix change logging for contact group assignments
|
||||
* [#19463](https://github.com/netbox-community/netbox/issues/19463) - Hide button dropdown for tables which do not support saved configs
|
||||
* [#19464](https://github.com/netbox-community/netbox/issues/19464) - Fix bulk editing of inventory items from device view
|
||||
* [#19465](https://github.com/netbox-community/netbox/issues/19465) - Fix ability to clear assigned prefix scope in UI
|
||||
* [#19472](https://github.com/netbox-community/netbox/issues/19472) - Fix device column rendering in virtual device contexts table
|
||||
|
||||
---
|
||||
|
||||
## v4.3.0 (2025-05-01)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
@@ -191,12 +191,10 @@ class ProfileView(LoginRequiredMixin, View):
|
||||
def get(self, request):
|
||||
|
||||
# Compile changelog table
|
||||
changelog = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(
|
||||
user=request.user
|
||||
).prefetch_related(
|
||||
'changed_object_type'
|
||||
)[:20]
|
||||
changelog = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(user=request.user)[:20]
|
||||
changelog_table = ObjectChangeTable(changelog)
|
||||
changelog_table.orderable = False
|
||||
changelog_table.configure(request)
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'changelog_table': changelog_table,
|
||||
|
||||
@@ -16,6 +16,7 @@ from utilities.forms import get_field_value
|
||||
from utilities.forms.fields import (
|
||||
CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField,
|
||||
)
|
||||
from utilities.forms.mixins import DistanceValidationMixin
|
||||
from utilities.forms.rendering import FieldSet, InlineFields
|
||||
from utilities.forms.widgets import DatePicker, HTMXSelect, NumberWithOptions
|
||||
from utilities.templatetags.builtins.filters import bettertitle
|
||||
@@ -105,7 +106,7 @@ class CircuitTypeForm(NetBoxModelForm):
|
||||
]
|
||||
|
||||
|
||||
class CircuitForm(TenancyForm, NetBoxModelForm):
|
||||
class CircuitForm(DistanceValidationMixin, TenancyForm, NetBoxModelForm):
|
||||
provider = DynamicModelChoiceField(
|
||||
label=_('Provider'),
|
||||
queryset=Provider.objects.all(),
|
||||
|
||||
@@ -41,7 +41,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.CircuitTermination, lookups=True)
|
||||
@strawberry_django.filter_type(models.CircuitTermination, lookups=True)
|
||||
class CircuitTerminationFilter(
|
||||
BaseObjectTypeFilterMixin,
|
||||
CustomFieldsFilterMixin,
|
||||
@@ -87,7 +87,7 @@ class CircuitTerminationFilter(
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Circuit, lookups=True)
|
||||
@strawberry_django.filter_type(models.Circuit, lookups=True)
|
||||
class CircuitFilter(
|
||||
ContactFilterMixin,
|
||||
ImageAttachmentFilterMixin,
|
||||
@@ -121,17 +121,17 @@ class CircuitFilter(
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.CircuitType, lookups=True)
|
||||
@strawberry_django.filter_type(models.CircuitType, lookups=True)
|
||||
class CircuitTypeFilter(BaseCircuitTypeFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.CircuitGroup, lookups=True)
|
||||
@strawberry_django.filter_type(models.CircuitGroup, lookups=True)
|
||||
class CircuitGroupFilter(TenancyFilterMixin, OrganizationalModelFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.CircuitGroupAssignment, lookups=True)
|
||||
@strawberry_django.filter_type(models.CircuitGroupAssignment, lookups=True)
|
||||
class CircuitGroupAssignmentFilter(
|
||||
BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin
|
||||
):
|
||||
@@ -148,7 +148,7 @@ class CircuitGroupAssignmentFilter(
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Provider, lookups=True)
|
||||
@strawberry_django.filter_type(models.Provider, lookups=True)
|
||||
class ProviderFilter(ContactFilterMixin, PrimaryModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@@ -158,7 +158,7 @@ class ProviderFilter(ContactFilterMixin, PrimaryModelFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ProviderAccount, lookups=True)
|
||||
@strawberry_django.filter_type(models.ProviderAccount, lookups=True)
|
||||
class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilterMixin):
|
||||
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -168,7 +168,7 @@ class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ProviderNetwork, lookups=True)
|
||||
@strawberry_django.filter_type(models.ProviderNetwork, lookups=True)
|
||||
class ProviderNetworkFilter(PrimaryModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||
@@ -178,12 +178,12 @@ class ProviderNetworkFilter(PrimaryModelFilterMixin):
|
||||
service_id: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VirtualCircuitType, lookups=True)
|
||||
@strawberry_django.filter_type(models.VirtualCircuitType, lookups=True)
|
||||
class VirtualCircuitTypeFilter(BaseCircuitTypeFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VirtualCircuit, lookups=True)
|
||||
@strawberry_django.filter_type(models.VirtualCircuit, lookups=True)
|
||||
class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
cid: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
provider_network: Annotated['ProviderNetworkFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||
@@ -206,7 +206,7 @@ class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VirtualCircuitTermination, lookups=True)
|
||||
@strawberry_django.filter_type(models.VirtualCircuitTermination, lookups=True)
|
||||
class VirtualCircuitTerminationFilter(
|
||||
BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin
|
||||
):
|
||||
|
||||
@@ -8,10 +8,11 @@ def set_null_values(apps, schema_editor):
|
||||
Circuit = apps.get_model('circuits', 'Circuit')
|
||||
CircuitGroupAssignment = apps.get_model('circuits', 'CircuitGroupAssignment')
|
||||
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
Circuit.objects.filter(distance_unit='').update(distance_unit=None)
|
||||
CircuitGroupAssignment.objects.filter(priority='').update(priority=None)
|
||||
CircuitTermination.objects.filter(cable_end='').update(cable_end=None)
|
||||
Circuit.objects.using(db_alias).filter(distance_unit='').update(distance_unit=None)
|
||||
CircuitGroupAssignment.objects.using(db_alias).filter(priority='').update(priority=None)
|
||||
CircuitTermination.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import django.db.models.deletion
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@@ -8,14 +9,15 @@ def copy_site_assignments(apps, schema_editor):
|
||||
"""
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
|
||||
ProviderNetwork = apps.get_model('circuits', 'ProviderNetwork')
|
||||
Site = apps.get_model('dcim', 'Site')
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
CircuitTermination.objects.filter(site__isnull=False).update(
|
||||
CircuitTermination.objects.using(db_alias).filter(site__isnull=False).update(
|
||||
termination_type=ContentType.objects.get_for_model(Site), termination_id=models.F('site_id')
|
||||
)
|
||||
|
||||
ProviderNetwork = apps.get_model('circuits', 'ProviderNetwork')
|
||||
CircuitTermination.objects.filter(provider_network__isnull=False).update(
|
||||
CircuitTermination.objects.using(db_alias).filter(provider_network__isnull=False).update(
|
||||
termination_type=ContentType.objects.get_for_model(ProviderNetwork),
|
||||
termination_id=models.F('provider_network_id'),
|
||||
)
|
||||
@@ -48,3 +50,26 @@ class Migration(migrations.Migration):
|
||||
# Copy over existing site assignments
|
||||
migrations.RunPython(code=copy_site_assignments, reverse_code=migrations.RunPython.noop),
|
||||
]
|
||||
|
||||
|
||||
def oc_circuittermination_termination(objectchange, reverting):
|
||||
site_ct = ContentType.objects.get_by_natural_key('dcim', 'site').pk
|
||||
provider_network_ct = ContentType.objects.get_by_natural_key('circuits', 'providernetwork').pk
|
||||
for data in (objectchange.prechange_data, objectchange.postchange_data):
|
||||
if data is None:
|
||||
continue
|
||||
if site_id := data.get('site'):
|
||||
data.update({
|
||||
'termination_type': site_ct,
|
||||
'termination_id': site_id,
|
||||
})
|
||||
elif provider_network_id := data.get('provider_network'):
|
||||
data.update({
|
||||
'termination_type': provider_network_ct,
|
||||
'termination_id': provider_network_id,
|
||||
})
|
||||
|
||||
|
||||
objectchange_migrators = {
|
||||
'circuits.circuittermination': oc_circuittermination_termination,
|
||||
}
|
||||
|
||||
@@ -7,15 +7,20 @@ def populate_denormalized_fields(apps, schema_editor):
|
||||
Copy site ForeignKey values to the Termination GFK.
|
||||
"""
|
||||
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
terminations = CircuitTermination.objects.filter(site__isnull=False).prefetch_related('site')
|
||||
terminations = CircuitTermination.objects.using(db_alias).filter(site__isnull=False).prefetch_related('site')
|
||||
for termination in terminations:
|
||||
termination._region_id = termination.site.region_id
|
||||
termination._site_group_id = termination.site.group_id
|
||||
termination._site_id = termination.site_id
|
||||
# Note: Location cannot be set prior to migration
|
||||
|
||||
CircuitTermination.objects.bulk_update(terminations, ['_region', '_site_group', '_site'], batch_size=100)
|
||||
CircuitTermination.objects.using(db_alias).bulk_update(
|
||||
terminations,
|
||||
['_region', '_site_group', '_site'],
|
||||
batch_size=100
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -81,3 +86,15 @@ class Migration(migrations.Migration):
|
||||
new_name='_provider_network',
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def oc_circuittermination_remove_fields(objectchange, reverting):
|
||||
for data in (objectchange.prechange_data, objectchange.postchange_data):
|
||||
if data is not None:
|
||||
data.pop('site', None)
|
||||
data.pop('provider_network', None)
|
||||
|
||||
|
||||
objectchange_migrators = {
|
||||
'circuits.circuittermination': oc_circuittermination_remove_fields,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import django.db.models.deletion
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@@ -9,8 +10,9 @@ def set_member_type(apps, schema_editor):
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
Circuit = apps.get_model('circuits', 'Circuit')
|
||||
CircuitGroupAssignment = apps.get_model('circuits', 'CircuitGroupAssignment')
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
CircuitGroupAssignment.objects.update(
|
||||
CircuitGroupAssignment.objects.using(db_alias).update(
|
||||
member_type=ContentType.objects.get_for_model(Circuit)
|
||||
)
|
||||
|
||||
@@ -81,3 +83,21 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def oc_circuitgroupassignment_member(objectchange, reverting):
|
||||
circuit_ct = ContentType.objects.get_by_natural_key('circuits', 'circuit').pk
|
||||
for data in (objectchange.prechange_data, objectchange.postchange_data):
|
||||
if data is None:
|
||||
continue
|
||||
if circuit_id := data.get('circuit'):
|
||||
data.update({
|
||||
'member_type': circuit_ct,
|
||||
'member_id': circuit_id,
|
||||
})
|
||||
data.pop('circuit', None)
|
||||
|
||||
|
||||
objectchange_migrators = {
|
||||
'circuits.circuitgroupassignment': oc_circuitgroupassignment_member,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0051_virtualcircuit_group_assignment'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='circuit',
|
||||
name='_abs_distance',
|
||||
field=models.DecimalField(blank=True, decimal_places=4, max_digits=13, null=True),
|
||||
),
|
||||
]
|
||||
@@ -120,7 +120,8 @@ class CircuitTerminationTable(NetBoxTable):
|
||||
)
|
||||
termination = tables.Column(
|
||||
verbose_name=_('Termination Point'),
|
||||
linkify=True
|
||||
linkify=True,
|
||||
orderable=False,
|
||||
)
|
||||
|
||||
# Termination types
|
||||
@@ -132,7 +133,7 @@ class CircuitTerminationTable(NetBoxTable):
|
||||
site_group = tables.Column(
|
||||
verbose_name=_('Site Group'),
|
||||
linkify=True,
|
||||
accessor='_sitegroup'
|
||||
accessor='_site_group'
|
||||
)
|
||||
region = tables.Column(
|
||||
verbose_name=_('Region'),
|
||||
|
||||
@@ -54,9 +54,8 @@ class VirtualCircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable)
|
||||
linkify=True,
|
||||
verbose_name=_('Account')
|
||||
)
|
||||
type = tables.Column(
|
||||
type = columns.ColoredLabelColumn(
|
||||
verbose_name=_('Type'),
|
||||
linkify=True
|
||||
)
|
||||
status = columns.ChoiceFieldColumn()
|
||||
termination_count = columns.LinkedCountColumn(
|
||||
|
||||
23
netbox/circuits/tests/test_tables.py
Normal file
23
netbox/circuits/tests/test_tables.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from django.test import RequestFactory, tag, TestCase
|
||||
|
||||
from circuits.models import CircuitTermination
|
||||
from circuits.tables import CircuitTerminationTable
|
||||
|
||||
|
||||
@tag('regression')
|
||||
class CircuitTerminationTableTest(TestCase):
|
||||
def test_every_orderable_field_does_not_throw_exception(self):
|
||||
terminations = CircuitTermination.objects.all()
|
||||
disallowed = {'actions', }
|
||||
|
||||
orderable_columns = [
|
||||
column.name for column in CircuitTerminationTable(terminations).columns
|
||||
if column.orderable and column.name not in disallowed
|
||||
]
|
||||
fake_request = RequestFactory().get("/")
|
||||
|
||||
for col in orderable_columns:
|
||||
for dir in ('-', ''):
|
||||
table = CircuitTerminationTable(terminations)
|
||||
table.order_by = f'{dir}{col}'
|
||||
table.as_html(fake_request)
|
||||
@@ -23,7 +23,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.DataFile, lookups=True)
|
||||
@strawberry_django.filter_type(models.DataFile, lookups=True)
|
||||
class DataFileFilter(BaseFilterMixin):
|
||||
id: ID | None = strawberry_django.filter_field()
|
||||
created: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()
|
||||
@@ -39,7 +39,7 @@ class DataFileFilter(BaseFilterMixin):
|
||||
hash: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.DataSource, lookups=True)
|
||||
@strawberry_django.filter_type(models.DataSource, lookups=True)
|
||||
class DataSourceFilter(PrimaryModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
type: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@@ -56,7 +56,7 @@ class DataSourceFilter(PrimaryModelFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ObjectChange, lookups=True)
|
||||
@strawberry_django.filter_type(models.ObjectChange, lookups=True)
|
||||
class ObjectChangeFilter(BaseFilterMixin):
|
||||
id: ID | None = strawberry_django.filter_field()
|
||||
time: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()
|
||||
@@ -82,7 +82,7 @@ class ObjectChangeFilter(BaseFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(DjangoContentType, lookups=True)
|
||||
@strawberry_django.filter_type(DjangoContentType, lookups=True)
|
||||
class ContentTypeFilter(BaseFilterMixin):
|
||||
id: ID | None = strawberry_django.filter_field()
|
||||
app_label: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
@@ -88,19 +88,11 @@ class ManagedFile(SyncedDataMixin, models.Model):
|
||||
def sync_data(self):
|
||||
if self.data_file:
|
||||
self.file_path = os.path.basename(self.data_path)
|
||||
self._write_to_disk(self.full_path, overwrite=True)
|
||||
|
||||
def _write_to_disk(self, path, overwrite=False):
|
||||
"""
|
||||
Write the object's data to disk at the specified path
|
||||
"""
|
||||
# Check whether file already exists
|
||||
storage = self.storage
|
||||
if storage.exists(path) and not overwrite:
|
||||
raise FileExistsError()
|
||||
storage = self.storage
|
||||
|
||||
with storage.open(path, 'wb+') as new_file:
|
||||
new_file.write(self.data)
|
||||
with storage.open(self.full_path, 'wb+') as new_file:
|
||||
new_file.write(self.data_file.data)
|
||||
|
||||
@cached_property
|
||||
def storage(self):
|
||||
|
||||
@@ -215,6 +215,7 @@ class Job(models.Model):
|
||||
schedule_at=None,
|
||||
interval=None,
|
||||
immediate=False,
|
||||
queue_name=None,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
@@ -238,7 +239,7 @@ class Job(models.Model):
|
||||
object_id = instance.pk
|
||||
else:
|
||||
object_type = object_id = None
|
||||
rq_queue_name = get_queue_for_model(object_type.model if object_type else None)
|
||||
rq_queue_name = queue_name if queue_name else get_queue_for_model(object_type.model if object_type else None)
|
||||
queue = django_rq.get_queue(rq_queue_name)
|
||||
status = JobStatusChoices.STATUS_SCHEDULED if schedule_at else JobStatusChoices.STATUS_PENDING
|
||||
job = Job(
|
||||
|
||||
@@ -9,6 +9,7 @@ from rq.registry import FailedJobRegistry, StartedJobRegistry
|
||||
|
||||
from users.models import Token, User
|
||||
from utilities.testing import APITestCase, APIViewTestCases, TestCase
|
||||
from utilities.testing.utils import disable_logging
|
||||
from ..models import *
|
||||
|
||||
|
||||
@@ -189,7 +190,8 @@ class BackgroundTaskTestCase(TestCase):
|
||||
# Enqueue & run a job that will fail
|
||||
job = queue.enqueue(self.dummy_job_failing)
|
||||
worker = get_worker('default')
|
||||
worker.work(burst=True)
|
||||
with disable_logging():
|
||||
worker.work(burst=True)
|
||||
self.assertTrue(job.is_failed)
|
||||
|
||||
# Re-enqueue the failed job and check that its status has been reset
|
||||
@@ -231,7 +233,8 @@ class BackgroundTaskTestCase(TestCase):
|
||||
self.assertEqual(job.get_status(), JobStatus.STARTED)
|
||||
response = self.client.post(reverse('core-api:rqtask-stop', args=[job.id]), **self.header)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
worker.monitor_work_horse(job, queue) # Sets the job as Failed and removes from Started
|
||||
with disable_logging():
|
||||
worker.monitor_work_horse(job, queue) # Sets the job as Failed and removes from Started
|
||||
started_job_registry = StartedJobRegistry(queue.name, connection=queue.connection)
|
||||
self.assertEqual(len(started_job_registry), 0)
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ from core.choices import ObjectChangeActionChoices
|
||||
from core.models import *
|
||||
from dcim.models import Site
|
||||
from users.models import User
|
||||
from utilities.testing import TestCase, ViewTestCases, create_tags
|
||||
from utilities.testing import TestCase, ViewTestCases, create_tags, disable_logging
|
||||
|
||||
|
||||
class DataSourceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
@@ -271,7 +271,8 @@ class BackgroundTaskTestCase(TestCase):
|
||||
# Enqueue & run a job that will fail
|
||||
job = queue.enqueue(self.dummy_job_failing)
|
||||
worker = get_worker('default')
|
||||
worker.work(burst=True)
|
||||
with disable_logging():
|
||||
worker.work(burst=True)
|
||||
self.assertTrue(job.is_failed)
|
||||
|
||||
# Re-enqueue the failed job and check that its status has been reset
|
||||
@@ -317,7 +318,8 @@ class BackgroundTaskTestCase(TestCase):
|
||||
self.assertEqual(len(started_job_registry), 1)
|
||||
response = self.client.get(reverse('core:background_task_stop', args=[job.id]))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
worker.monitor_work_horse(job, queue) # Sets the job as Failed and removes from Started
|
||||
with disable_logging():
|
||||
worker.monitor_work_horse(job, queue) # Sets the job as Failed and removes from Started
|
||||
self.assertEqual(len(started_job_registry), 0)
|
||||
|
||||
canceled_job_registry = FailedJobRegistry(queue.name, connection=queue.connection)
|
||||
|
||||
@@ -223,6 +223,7 @@ class ObjectChangeView(generic.ObjectView):
|
||||
data=related_changes[:50],
|
||||
orderable=False
|
||||
)
|
||||
related_changes_table.configure(request)
|
||||
|
||||
objectchanges = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(
|
||||
changed_object_type=instance.changed_object_type,
|
||||
|
||||
@@ -461,6 +461,7 @@ class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||
Interface.objects.select_related("device", "cable"),
|
||||
],
|
||||
),
|
||||
'virtual_circuit_termination',
|
||||
'l2vpn_terminations', # Referenced by InterfaceSerializer.l2vpn_termination
|
||||
'ip_addresses', # Referenced by Interface.count_ipaddresses()
|
||||
'fhrp_group_assignments', # Referenced by Interface.count_fhrp_groups()
|
||||
|
||||
@@ -874,6 +874,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
TYPE_100ME_T1 = '100base-t1'
|
||||
TYPE_100ME_SFP = '100base-x-sfp'
|
||||
TYPE_1GE_FIXED = '1000base-t'
|
||||
TYPE_1GE_SX_FIXED = '1000base-sx'
|
||||
TYPE_1GE_LX_FIXED = '1000base-lx'
|
||||
TYPE_1GE_TX_FIXED = '1000base-tx'
|
||||
TYPE_1GE_GBIC = '1000base-x-gbic'
|
||||
@@ -1038,6 +1039,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
(TYPE_100ME_FIXED, '100BASE-TX (10/100ME)'),
|
||||
(TYPE_100ME_T1, '100BASE-T1 (10/100ME Single Pair)'),
|
||||
(TYPE_1GE_FIXED, '1000BASE-T (1GE)'),
|
||||
(TYPE_1GE_SX_FIXED, '1000BASE-SX (1GE)'),
|
||||
(TYPE_1GE_LX_FIXED, '1000BASE-LX (1GE)'),
|
||||
(TYPE_1GE_TX_FIXED, '1000BASE-TX (1GE)'),
|
||||
(TYPE_2GE_FIXED, '2.5GBASE-T (2.5GE)'),
|
||||
@@ -1238,6 +1240,8 @@ class InterfaceSpeedChoices(ChoiceSet):
|
||||
(10000, '10 Mbps'),
|
||||
(100000, '100 Mbps'),
|
||||
(1000000, '1 Gbps'),
|
||||
(2500000, '2.5 Gbps'),
|
||||
(5000000, '5 Gbps'),
|
||||
(10000000, '10 Gbps'),
|
||||
(25000000, '25 Gbps'),
|
||||
(40000000, '40 Gbps'),
|
||||
|
||||
@@ -2012,6 +2012,21 @@ class InterfaceFilterSet(
|
||||
'wireless': queryset.filter(type__in=WIRELESS_IFACE_TYPES),
|
||||
}.get(value, queryset.none())
|
||||
|
||||
# Override the method on CabledObjectFilterSet to also check for wireless links
|
||||
def filter_occupied(self, queryset, name, value):
|
||||
if value:
|
||||
return queryset.filter(
|
||||
Q(cable__isnull=False) |
|
||||
Q(wireless_link__isnull=False) |
|
||||
Q(mark_connected=True)
|
||||
)
|
||||
else:
|
||||
return queryset.filter(
|
||||
cable__isnull=True,
|
||||
wireless_link__isnull=True,
|
||||
mark_connected=False
|
||||
)
|
||||
|
||||
|
||||
class FrontPortFilterSet(
|
||||
ModularDeviceComponentFilterSet,
|
||||
|
||||
@@ -1779,6 +1779,13 @@ class InventoryItemBulkEditForm(
|
||||
)
|
||||
nullable_fields = ('label', 'role', 'manufacturer', 'part_id', 'description')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Remove parent device passed as context to avoid conflicts with the actual device field
|
||||
# on this form (see bug #19464)
|
||||
self.initial.pop('device', None)
|
||||
|
||||
|
||||
#
|
||||
# Device component roles
|
||||
|
||||
@@ -66,6 +66,10 @@ class ScopedForm(forms.Form):
|
||||
if self.instance and scope_type_id != self.instance.scope_type_id:
|
||||
self.initial['scope'] = None
|
||||
|
||||
else:
|
||||
# Clear the initial scope value if scope_type is not set
|
||||
self.initial['scope'] = None
|
||||
|
||||
|
||||
class ScopedBulkEditForm(forms.Form):
|
||||
scope_type = ContentTypeChoiceField(
|
||||
|
||||
@@ -90,7 +90,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Cable, lookups=True)
|
||||
@strawberry_django.filter_type(models.Cable, lookups=True)
|
||||
class CableFilter(PrimaryModelFilterMixin, TenancyFilterMixin):
|
||||
type: Annotated['CableTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
status: Annotated['LinkStatusEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
@@ -107,7 +107,7 @@ class CableFilter(PrimaryModelFilterMixin, TenancyFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.CableTermination, lookups=True)
|
||||
@strawberry_django.filter_type(models.CableTermination, lookups=True)
|
||||
class CableTerminationFilter(ChangeLogFilterMixin):
|
||||
cable: Annotated['CableFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
cable_id: ID | None = strawberry_django.filter_field()
|
||||
@@ -120,7 +120,7 @@ class CableTerminationFilter(ChangeLogFilterMixin):
|
||||
termination_id: ID | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ConsolePort, lookups=True)
|
||||
@strawberry_django.filter_type(models.ConsolePort, lookups=True)
|
||||
class ConsolePortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
|
||||
type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -130,14 +130,14 @@ class ConsolePortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilte
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ConsolePortTemplate, lookups=True)
|
||||
@strawberry_django.filter_type(models.ConsolePortTemplate, lookups=True)
|
||||
class ConsolePortTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ConsoleServerPort, lookups=True)
|
||||
@strawberry_django.filter_type(models.ConsoleServerPort, lookups=True)
|
||||
class ConsoleServerPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
|
||||
type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -147,14 +147,14 @@ class ConsoleServerPortFilter(ModularComponentModelFilterMixin, CabledObjectMode
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ConsoleServerPortTemplate, lookups=True)
|
||||
@strawberry_django.filter_type(models.ConsoleServerPortTemplate, lookups=True)
|
||||
class ConsoleServerPortTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Device, lookups=True)
|
||||
@strawberry_django.filter_type(models.Device, lookups=True)
|
||||
class DeviceFilter(
|
||||
ContactFilterMixin,
|
||||
TenancyFilterMixin,
|
||||
@@ -271,7 +271,7 @@ class DeviceFilter(
|
||||
inventory_item_count: FilterLookup[int] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.DeviceBay, lookups=True)
|
||||
@strawberry_django.filter_type(models.DeviceBay, lookups=True)
|
||||
class DeviceBayFilter(ComponentModelFilterMixin):
|
||||
installed_device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -279,12 +279,12 @@ class DeviceBayFilter(ComponentModelFilterMixin):
|
||||
installed_device_id: ID | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.DeviceBayTemplate, lookups=True)
|
||||
@strawberry_django.filter_type(models.DeviceBayTemplate, lookups=True)
|
||||
class DeviceBayTemplateFilter(ComponentTemplateFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.InventoryItemTemplate, lookups=True)
|
||||
@strawberry_django.filter_type(models.InventoryItemTemplate, lookups=True)
|
||||
class InventoryItemTemplateFilter(ComponentTemplateFilterMixin):
|
||||
parent: Annotated['InventoryItemTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -304,13 +304,13 @@ class InventoryItemTemplateFilter(ComponentTemplateFilterMixin):
|
||||
part_id: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.DeviceRole, lookups=True)
|
||||
@strawberry_django.filter_type(models.DeviceRole, lookups=True)
|
||||
class DeviceRoleFilter(OrganizationalModelFilterMixin, RenderConfigFilterMixin):
|
||||
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
vm_role: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.DeviceType, lookups=True)
|
||||
@strawberry_django.filter_type(models.DeviceType, lookups=True)
|
||||
class DeviceTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, WeightFilterMixin):
|
||||
manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -382,7 +382,7 @@ class DeviceTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, Weig
|
||||
inventory_item_template_count: FilterLookup[int] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.FrontPort, lookups=True)
|
||||
@strawberry_django.filter_type(models.FrontPort, lookups=True)
|
||||
class FrontPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
|
||||
type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
@@ -395,7 +395,7 @@ class FrontPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterM
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.FrontPortTemplate, lookups=True)
|
||||
@strawberry_django.filter_type(models.FrontPortTemplate, lookups=True)
|
||||
class FrontPortTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
@@ -408,7 +408,7 @@ class FrontPortTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.MACAddress, lookups=True)
|
||||
@strawberry_django.filter_type(models.MACAddress, lookups=True)
|
||||
class MACAddressFilter(PrimaryModelFilterMixin):
|
||||
mac_address: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
assigned_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
@@ -417,7 +417,7 @@ class MACAddressFilter(PrimaryModelFilterMixin):
|
||||
assigned_object_id: ID | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Interface, lookups=True)
|
||||
@strawberry_django.filter_type(models.Interface, lookups=True)
|
||||
class InterfaceFilter(ModularComponentModelFilterMixin, InterfaceBaseFilterMixin, CabledObjectModelFilterMixin):
|
||||
vcdcs: Annotated['VirtualDeviceContextFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -486,7 +486,7 @@ class InterfaceFilter(ModularComponentModelFilterMixin, InterfaceBaseFilterMixin
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.InterfaceTemplate, lookups=True)
|
||||
@strawberry_django.filter_type(models.InterfaceTemplate, lookups=True)
|
||||
class InterfaceTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
type: Annotated['InterfaceTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -508,7 +508,7 @@ class InterfaceTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.InventoryItem, lookups=True)
|
||||
@strawberry_django.filter_type(models.InventoryItem, lookups=True)
|
||||
class InventoryItemFilter(ComponentModelFilterMixin):
|
||||
parent: Annotated['InventoryItemFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -535,12 +535,12 @@ class InventoryItemFilter(ComponentModelFilterMixin):
|
||||
discovered: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.InventoryItemRole, lookups=True)
|
||||
@strawberry_django.filter_type(models.InventoryItemRole, lookups=True)
|
||||
class InventoryItemRoleFilter(OrganizationalModelFilterMixin):
|
||||
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Location, lookups=True)
|
||||
@strawberry_django.filter_type(models.Location, lookups=True)
|
||||
class LocationFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, NestedGroupModelFilterMixin):
|
||||
site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
site_id: ID | None = strawberry_django.filter_field()
|
||||
@@ -556,12 +556,12 @@ class LocationFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilt
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Manufacturer, lookups=True)
|
||||
@strawberry_django.filter_type(models.Manufacturer, lookups=True)
|
||||
class ManufacturerFilter(ContactFilterMixin, OrganizationalModelFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Module, lookups=True)
|
||||
@strawberry_django.filter_type(models.Module, lookups=True)
|
||||
class ModuleFilter(PrimaryModelFilterMixin, ConfigContextFilterMixin):
|
||||
device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
device_id: ID | None = strawberry_django.filter_field()
|
||||
@@ -610,7 +610,7 @@ class ModuleFilter(PrimaryModelFilterMixin, ConfigContextFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ModuleBay, lookups=True)
|
||||
@strawberry_django.filter_type(models.ModuleBay, lookups=True)
|
||||
class ModuleBayFilter(ModularComponentModelFilterMixin):
|
||||
parent: Annotated['ModuleBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -619,17 +619,17 @@ class ModuleBayFilter(ModularComponentModelFilterMixin):
|
||||
position: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ModuleBayTemplate, lookups=True)
|
||||
@strawberry_django.filter_type(models.ModuleBayTemplate, lookups=True)
|
||||
class ModuleBayTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
position: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ModuleTypeProfile, lookups=True)
|
||||
@strawberry_django.filter_type(models.ModuleTypeProfile, lookups=True)
|
||||
class ModuleTypeProfileFilter(PrimaryModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ModuleType, lookups=True)
|
||||
@strawberry_django.filter_type(models.ModuleType, lookups=True)
|
||||
class ModuleTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, WeightFilterMixin):
|
||||
manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -676,7 +676,7 @@ class ModuleTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, Weig
|
||||
) = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Platform, lookups=True)
|
||||
@strawberry_django.filter_type(models.Platform, lookups=True)
|
||||
class PlatformFilter(OrganizationalModelFilterMixin):
|
||||
manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -688,7 +688,7 @@ class PlatformFilter(OrganizationalModelFilterMixin):
|
||||
config_template_id: ID | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.PowerFeed, lookups=True)
|
||||
@strawberry_django.filter_type(models.PowerFeed, lookups=True)
|
||||
class PowerFeedFilter(CabledObjectModelFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
power_panel: Annotated['PowerPanelFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -723,7 +723,7 @@ class PowerFeedFilter(CabledObjectModelFilterMixin, TenancyFilterMixin, PrimaryM
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.PowerOutlet, lookups=True)
|
||||
@strawberry_django.filter_type(models.PowerOutlet, lookups=True)
|
||||
class PowerOutletFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
|
||||
type: Annotated['PowerOutletTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -738,7 +738,7 @@ class PowerOutletFilter(ModularComponentModelFilterMixin, CabledObjectModelFilte
|
||||
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.PowerOutletTemplate, lookups=True)
|
||||
@strawberry_django.filter_type(models.PowerOutletTemplate, lookups=True)
|
||||
class PowerOutletTemplateFilter(ModularComponentModelFilterMixin):
|
||||
type: Annotated['PowerOutletTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -752,7 +752,7 @@ class PowerOutletTemplateFilter(ModularComponentModelFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.PowerPanel, lookups=True)
|
||||
@strawberry_django.filter_type(models.PowerPanel, lookups=True)
|
||||
class PowerPanelFilter(ContactFilterMixin, ImageAttachmentFilterMixin, PrimaryModelFilterMixin):
|
||||
site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
site_id: ID | None = strawberry_django.filter_field()
|
||||
@@ -765,7 +765,7 @@ class PowerPanelFilter(ContactFilterMixin, ImageAttachmentFilterMixin, PrimaryMo
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.PowerPort, lookups=True)
|
||||
@strawberry_django.filter_type(models.PowerPort, lookups=True)
|
||||
class PowerPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
|
||||
type: Annotated['PowerPortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -778,7 +778,7 @@ class PowerPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterM
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.PowerPortTemplate, lookups=True)
|
||||
@strawberry_django.filter_type(models.PowerPortTemplate, lookups=True)
|
||||
class PowerPortTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
type: Annotated['PowerPortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -791,7 +791,7 @@ class PowerPortTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.RackType, lookups=True)
|
||||
@strawberry_django.filter_type(models.RackType, lookups=True)
|
||||
class RackTypeFilter(RackBaseFilterMixin):
|
||||
form_factor: Annotated['RackFormFactorEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -804,7 +804,7 @@ class RackTypeFilter(RackBaseFilterMixin):
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Rack, lookups=True)
|
||||
@strawberry_django.filter_type(models.Rack, lookups=True)
|
||||
class RackFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, RackBaseFilterMixin):
|
||||
form_factor: Annotated['RackFormFactorEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -836,7 +836,7 @@ class RackFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMi
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.RackReservation, lookups=True)
|
||||
@strawberry_django.filter_type(models.RackReservation, lookups=True)
|
||||
class RackReservationFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
rack: Annotated['RackFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
rack_id: ID | None = strawberry_django.filter_field()
|
||||
@@ -848,12 +848,12 @@ class RackReservationFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.RackRole, lookups=True)
|
||||
@strawberry_django.filter_type(models.RackRole, lookups=True)
|
||||
class RackRoleFilter(OrganizationalModelFilterMixin):
|
||||
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.RearPort, lookups=True)
|
||||
@strawberry_django.filter_type(models.RearPort, lookups=True)
|
||||
class RearPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
|
||||
type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
@@ -862,7 +862,7 @@ class RearPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMi
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.RearPortTemplate, lookups=True)
|
||||
@strawberry_django.filter_type(models.RearPortTemplate, lookups=True)
|
||||
class RearPortTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
@@ -871,7 +871,7 @@ class RearPortTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Region, lookups=True)
|
||||
@strawberry_django.filter_type(models.Region, lookups=True)
|
||||
class RegionFilter(ContactFilterMixin, NestedGroupModelFilterMixin):
|
||||
prefixes: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -881,7 +881,7 @@ class RegionFilter(ContactFilterMixin, NestedGroupModelFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Site, lookups=True)
|
||||
@strawberry_django.filter_type(models.Site, lookups=True)
|
||||
class SiteFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@@ -915,7 +915,7 @@ class SiteFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMi
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.SiteGroup, lookups=True)
|
||||
@strawberry_django.filter_type(models.SiteGroup, lookups=True)
|
||||
class SiteGroupFilter(ContactFilterMixin, NestedGroupModelFilterMixin):
|
||||
prefixes: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -925,7 +925,7 @@ class SiteGroupFilter(ContactFilterMixin, NestedGroupModelFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VirtualChassis, lookups=True)
|
||||
@strawberry_django.filter_type(models.VirtualChassis, lookups=True)
|
||||
class VirtualChassisFilter(PrimaryModelFilterMixin):
|
||||
master: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
master_id: ID | None = strawberry_django.filter_field()
|
||||
@@ -937,7 +937,7 @@ class VirtualChassisFilter(PrimaryModelFilterMixin):
|
||||
member_count: FilterLookup[int] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VirtualDeviceContext, lookups=True)
|
||||
@strawberry_django.filter_type(models.VirtualDeviceContext, lookups=True)
|
||||
class VirtualDeviceContextFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
device_id: ID | None = strawberry_django.filter_field()
|
||||
|
||||
@@ -541,10 +541,10 @@ class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, Organi
|
||||
class ManufacturerType(OrganizationalObjectType, ContactsMixin):
|
||||
|
||||
platforms: List[Annotated["PlatformType", strawberry.lazy('dcim.graphql.types')]]
|
||||
device_types: List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]
|
||||
device_types: List[Annotated["DeviceTypeType", strawberry.lazy('dcim.graphql.types')]]
|
||||
inventory_item_templates: List[Annotated["InventoryItemTemplateType", strawberry.lazy('dcim.graphql.types')]]
|
||||
inventory_items: List[Annotated["InventoryItemType", strawberry.lazy('dcim.graphql.types')]]
|
||||
module_types: List[Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')]]
|
||||
module_types: List[Annotated["ModuleTypeType", strawberry.lazy('dcim.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
@@ -617,11 +617,11 @@ class ModuleTypeType(NetBoxObjectType):
|
||||
frontporttemplates: List[Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]]
|
||||
consoleserverporttemplates: List[Annotated["ConsoleServerPortTemplateType", strawberry.lazy('dcim.graphql.types')]]
|
||||
interfacetemplates: List[Annotated["InterfaceTemplateType", strawberry.lazy('dcim.graphql.types')]]
|
||||
powerporttemplates: List[Annotated["PowerOutletTemplateType", strawberry.lazy('dcim.graphql.types')]]
|
||||
powerporttemplates: List[Annotated["PowerPortTemplateType", strawberry.lazy('dcim.graphql.types')]]
|
||||
poweroutlettemplates: List[Annotated["PowerOutletTemplateType", strawberry.lazy('dcim.graphql.types')]]
|
||||
rearporttemplates: List[Annotated["RearPortTemplateType", strawberry.lazy('dcim.graphql.types')]]
|
||||
instances: List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]
|
||||
consoleporttemplates: List[Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')]]
|
||||
instances: List[Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')]]
|
||||
consoleporttemplates: List[Annotated["ConsolePortTemplateType", strawberry.lazy('dcim.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
|
||||
@@ -100,3 +100,16 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def oc_rename_type(objectchange, reverting):
|
||||
for data in (objectchange.prechange_data, objectchange.postchange_data):
|
||||
if data is None:
|
||||
continue
|
||||
if 'type' in data:
|
||||
data['form_factor'] = data.pop('type')
|
||||
|
||||
|
||||
objectchange_migrators = {
|
||||
'dcim.rack': oc_rename_type,
|
||||
}
|
||||
|
||||
@@ -26,49 +26,50 @@ def set_null_values(apps, schema_editor):
|
||||
RackType = apps.get_model('dcim', 'RackType')
|
||||
RearPort = apps.get_model('dcim', 'RearPort')
|
||||
Site = apps.get_model('dcim', 'Site')
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
Cable.objects.filter(length_unit='').update(length_unit=None)
|
||||
Cable.objects.filter(type='').update(type=None)
|
||||
ConsolePort.objects.filter(cable_end='').update(cable_end=None)
|
||||
ConsolePort.objects.filter(type='').update(type=None)
|
||||
ConsolePortTemplate.objects.filter(type='').update(type=None)
|
||||
ConsoleServerPort.objects.filter(cable_end='').update(cable_end=None)
|
||||
ConsoleServerPort.objects.filter(type='').update(type=None)
|
||||
ConsoleServerPortTemplate.objects.filter(type='').update(type=None)
|
||||
Device.objects.filter(airflow='').update(airflow=None)
|
||||
Device.objects.filter(face='').update(face=None)
|
||||
DeviceType.objects.filter(airflow='').update(airflow=None)
|
||||
DeviceType.objects.filter(subdevice_role='').update(subdevice_role=None)
|
||||
DeviceType.objects.filter(weight_unit='').update(weight_unit=None)
|
||||
FrontPort.objects.filter(cable_end='').update(cable_end=None)
|
||||
Interface.objects.filter(cable_end='').update(cable_end=None)
|
||||
Interface.objects.filter(mode='').update(mode=None)
|
||||
Interface.objects.filter(poe_mode='').update(poe_mode=None)
|
||||
Interface.objects.filter(poe_type='').update(poe_type=None)
|
||||
Interface.objects.filter(rf_channel='').update(rf_channel=None)
|
||||
Interface.objects.filter(rf_role='').update(rf_role=None)
|
||||
InterfaceTemplate.objects.filter(poe_mode='').update(poe_mode=None)
|
||||
InterfaceTemplate.objects.filter(poe_type='').update(poe_type=None)
|
||||
InterfaceTemplate.objects.filter(rf_role='').update(rf_role=None)
|
||||
ModuleType.objects.filter(airflow='').update(airflow=None)
|
||||
ModuleType.objects.filter(weight_unit='').update(weight_unit=None)
|
||||
PowerFeed.objects.filter(cable_end='').update(cable_end=None)
|
||||
PowerOutlet.objects.filter(cable_end='').update(cable_end=None)
|
||||
PowerOutlet.objects.filter(feed_leg='').update(feed_leg=None)
|
||||
PowerOutlet.objects.filter(type='').update(type=None)
|
||||
PowerOutletTemplate.objects.filter(feed_leg='').update(feed_leg=None)
|
||||
PowerOutletTemplate.objects.filter(type='').update(type=None)
|
||||
PowerPort.objects.filter(cable_end='').update(cable_end=None)
|
||||
PowerPort.objects.filter(type='').update(type=None)
|
||||
PowerPortTemplate.objects.filter(type='').update(type=None)
|
||||
Rack.objects.filter(airflow='').update(airflow=None)
|
||||
Rack.objects.filter(form_factor='').update(form_factor=None)
|
||||
Rack.objects.filter(outer_unit='').update(outer_unit=None)
|
||||
Rack.objects.filter(weight_unit='').update(weight_unit=None)
|
||||
RackType.objects.filter(outer_unit='').update(outer_unit=None)
|
||||
RackType.objects.filter(weight_unit='').update(weight_unit=None)
|
||||
RearPort.objects.filter(cable_end='').update(cable_end=None)
|
||||
Site.objects.filter(time_zone='').update(time_zone=None)
|
||||
Cable.objects.using(db_alias).filter(length_unit='').update(length_unit=None)
|
||||
Cable.objects.using(db_alias).filter(type='').update(type=None)
|
||||
ConsolePort.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
|
||||
ConsolePort.objects.using(db_alias).filter(type='').update(type=None)
|
||||
ConsolePortTemplate.objects.using(db_alias).filter(type='').update(type=None)
|
||||
ConsoleServerPort.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
|
||||
ConsoleServerPort.objects.using(db_alias).filter(type='').update(type=None)
|
||||
ConsoleServerPortTemplate.objects.using(db_alias).filter(type='').update(type=None)
|
||||
Device.objects.using(db_alias).filter(airflow='').update(airflow=None)
|
||||
Device.objects.using(db_alias).filter(face='').update(face=None)
|
||||
DeviceType.objects.using(db_alias).filter(airflow='').update(airflow=None)
|
||||
DeviceType.objects.using(db_alias).filter(subdevice_role='').update(subdevice_role=None)
|
||||
DeviceType.objects.using(db_alias).filter(weight_unit='').update(weight_unit=None)
|
||||
FrontPort.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
|
||||
Interface.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
|
||||
Interface.objects.using(db_alias).filter(mode='').update(mode=None)
|
||||
Interface.objects.using(db_alias).filter(poe_mode='').update(poe_mode=None)
|
||||
Interface.objects.using(db_alias).filter(poe_type='').update(poe_type=None)
|
||||
Interface.objects.using(db_alias).filter(rf_channel='').update(rf_channel=None)
|
||||
Interface.objects.using(db_alias).filter(rf_role='').update(rf_role=None)
|
||||
InterfaceTemplate.objects.using(db_alias).filter(poe_mode='').update(poe_mode=None)
|
||||
InterfaceTemplate.objects.using(db_alias).filter(poe_type='').update(poe_type=None)
|
||||
InterfaceTemplate.objects.using(db_alias).filter(rf_role='').update(rf_role=None)
|
||||
ModuleType.objects.using(db_alias).filter(airflow='').update(airflow=None)
|
||||
ModuleType.objects.using(db_alias).filter(weight_unit='').update(weight_unit=None)
|
||||
PowerFeed.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
|
||||
PowerOutlet.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
|
||||
PowerOutlet.objects.using(db_alias).filter(feed_leg='').update(feed_leg=None)
|
||||
PowerOutlet.objects.using(db_alias).filter(type='').update(type=None)
|
||||
PowerOutletTemplate.objects.using(db_alias).filter(feed_leg='').update(feed_leg=None)
|
||||
PowerOutletTemplate.objects.using(db_alias).filter(type='').update(type=None)
|
||||
PowerPort.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
|
||||
PowerPort.objects.using(db_alias).filter(type='').update(type=None)
|
||||
PowerPortTemplate.objects.using(db_alias).filter(type='').update(type=None)
|
||||
Rack.objects.using(db_alias).filter(airflow='').update(airflow=None)
|
||||
Rack.objects.using(db_alias).filter(form_factor='').update(form_factor=None)
|
||||
Rack.objects.using(db_alias).filter(outer_unit='').update(outer_unit=None)
|
||||
Rack.objects.using(db_alias).filter(weight_unit='').update(weight_unit=None)
|
||||
RackType.objects.using(db_alias).filter(outer_unit='').update(outer_unit=None)
|
||||
RackType.objects.using(db_alias).filter(weight_unit='').update(weight_unit=None)
|
||||
RearPort.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
|
||||
Site.objects.using(db_alias).filter(time_zone='').update(time_zone=None)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import django.db.models.deletion
|
||||
from django.apps import apps
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@@ -6,19 +8,26 @@ def populate_mac_addresses(apps, schema_editor):
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
Interface = apps.get_model('dcim', 'Interface')
|
||||
MACAddress = apps.get_model('dcim', 'MACAddress')
|
||||
db_alias = schema_editor.connection.alias
|
||||
interface_ct = ContentType.objects.get_for_model(Interface)
|
||||
|
||||
mac_addresses = [
|
||||
MACAddress(
|
||||
mac_address=interface.mac_address, assigned_object_type=interface_ct, assigned_object_id=interface.pk
|
||||
mac_address=interface.mac_address,
|
||||
assigned_object_type=interface_ct,
|
||||
assigned_object_id=interface.pk
|
||||
)
|
||||
for interface in Interface.objects.filter(mac_address__isnull=False)
|
||||
for interface in Interface.objects.using(db_alias).filter(mac_address__isnull=False)
|
||||
]
|
||||
MACAddress.objects.bulk_create(mac_addresses, batch_size=100)
|
||||
MACAddress.objects.using(db_alias).bulk_create(mac_addresses, batch_size=100)
|
||||
|
||||
# TODO: Optimize interface updates
|
||||
for mac_address in mac_addresses:
|
||||
Interface.objects.filter(pk=mac_address.assigned_object_id).update(primary_mac_address=mac_address)
|
||||
Interface.objects.using(db_alias).filter(
|
||||
pk=mac_address.assigned_object_id
|
||||
).update(
|
||||
primary_mac_address=mac_address
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -44,3 +53,43 @@ class Migration(migrations.Migration):
|
||||
name='mac_address',
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
# See peer migrator in virtualization.0048_populate_mac_addresses before making changes
|
||||
def oc_interface_primary_mac_address(objectchange, reverting):
|
||||
MACAddress = apps.get_model('dcim', 'MACAddress')
|
||||
interface_ct = ContentType.objects.get_by_natural_key('dcim', 'interface')
|
||||
|
||||
# Swap data order if the change is being reverted
|
||||
if not reverting:
|
||||
before, after = objectchange.prechange_data, objectchange.postchange_data
|
||||
else:
|
||||
before, after = objectchange.postchange_data, objectchange.prechange_data
|
||||
|
||||
if after.get('mac_address') != before.get('mac_address'):
|
||||
# Create & assign the new MACAddress (if any)
|
||||
if after.get('mac_address'):
|
||||
mac = MACAddress.objects.create(
|
||||
mac_address=after['mac_address'],
|
||||
assigned_object_type=interface_ct,
|
||||
assigned_object_id=objectchange.changed_object_id,
|
||||
)
|
||||
after['primary_mac_address'] = mac.pk
|
||||
else:
|
||||
after['primary_mac_address'] = None
|
||||
# Delete the old MACAddress (if any)
|
||||
if before.get('mac_address'):
|
||||
MACAddress.objects.filter(
|
||||
mac_address=before['mac_address'],
|
||||
assigned_object_type=interface_ct,
|
||||
assigned_object_id=objectchange.changed_object_id,
|
||||
).delete()
|
||||
before['primary_mac_address'] = None
|
||||
|
||||
before.pop('mac_address', None)
|
||||
after.pop('mac_address', None)
|
||||
|
||||
|
||||
objectchange_migrators = {
|
||||
'dcim.interface': oc_interface_primary_mac_address,
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ def load_initial_data(apps, schema_editor):
|
||||
Load initial ModuleTypeProfile objects from file.
|
||||
"""
|
||||
ModuleTypeProfile = apps.get_model('dcim', 'ModuleTypeProfile')
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
initial_profiles = (
|
||||
'cpu',
|
||||
'fan',
|
||||
@@ -25,7 +27,7 @@ def load_initial_data(apps, schema_editor):
|
||||
with file_path.open('r') as f:
|
||||
data = json.load(f)
|
||||
try:
|
||||
ModuleTypeProfile.objects.create(**data)
|
||||
ModuleTypeProfile.objects.using(db_alias).create(**data)
|
||||
except Exception as e:
|
||||
print(f"Error loading data from {file_path}")
|
||||
raise e
|
||||
|
||||
@@ -85,7 +85,7 @@ class CachedScopeMixin(models.Model):
|
||||
abstract = True
|
||||
|
||||
def clean(self):
|
||||
if self.scope_type and not self.scope:
|
||||
if self.scope_type and not (self.scope or self.scope_id):
|
||||
scope_type = self.scope_type.model_class()
|
||||
raise ValidationError({
|
||||
'scope': _(
|
||||
|
||||
@@ -329,11 +329,9 @@ class CableTraceSVG:
|
||||
|
||||
# Draw attachment (line)
|
||||
start = (OFFSET + self.center, OFFSET + self.cursor)
|
||||
height = PADDING * 2 + LINE_HEIGHT + PADDING * 2
|
||||
end = (start[0], start[1] + height)
|
||||
end = (start[0], start[1] + CABLE_HEIGHT)
|
||||
line = Line(start=start, end=end, class_='attachment')
|
||||
group.add(line)
|
||||
self.cursor += PADDING * 4
|
||||
|
||||
return group
|
||||
|
||||
@@ -358,10 +356,10 @@ class CableTraceSVG:
|
||||
# Else: No need to draw parent objects (parent objects are drawn in last "round" as the far-end!)
|
||||
|
||||
near_terminations = self.draw_terminations(near_ends, parent_object_nodes)
|
||||
self.cursor += CABLE_HEIGHT
|
||||
|
||||
# Connector (a Cable or WirelessLink)
|
||||
if links and far_ends:
|
||||
self.cursor += CABLE_HEIGHT
|
||||
|
||||
obj_list = {end.parent_object for end in far_ends}
|
||||
parent_object_nodes, far_terminations = self.draw_far_objects(obj_list, far_ends)
|
||||
@@ -449,6 +447,7 @@ class CableTraceSVG:
|
||||
# Attachment
|
||||
attachment = self.draw_attachment()
|
||||
self.connectors.append(attachment)
|
||||
self.cursor += CABLE_HEIGHT
|
||||
|
||||
# Object
|
||||
parent_object_nodes = self.draw_parent_objects(far_ends)
|
||||
|
||||
@@ -1091,10 +1091,9 @@ class VirtualDeviceContextTable(TenancyColumnsMixin, NetBoxTable):
|
||||
verbose_name=_('Name'),
|
||||
linkify=True
|
||||
)
|
||||
device = tables.TemplateColumn(
|
||||
device = tables.Column(
|
||||
verbose_name=_('Device'),
|
||||
order_by=('device___name',),
|
||||
template_code=DEVICE_LINK,
|
||||
linkify=True
|
||||
)
|
||||
status = columns.ChoiceFieldColumn(
|
||||
|
||||
@@ -14,7 +14,7 @@ from ipam.models import ASN, RIR, VLAN, VRF
|
||||
from netbox.api.serializers import GenericObjectSerializer
|
||||
from tenancy.models import Tenant
|
||||
from users.models import User
|
||||
from utilities.testing import APITestCase, APIViewTestCases, create_test_device
|
||||
from utilities.testing import APITestCase, APIViewTestCases, create_test_device, disable_logging
|
||||
from virtualization.models import Cluster, ClusterType
|
||||
from wireless.choices import WirelessChannelChoices
|
||||
from wireless.models import WirelessLAN
|
||||
@@ -1858,7 +1858,8 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
|
||||
|
||||
# Attempt to delete only the parent interface
|
||||
url = self._get_detail_url(interface1)
|
||||
self.client.delete(url, **self.header)
|
||||
with disable_logging():
|
||||
self.client.delete(url, **self.header)
|
||||
self.assertEqual(device.interfaces.count(), 4) # Parent was not deleted
|
||||
|
||||
# Attempt to bulk delete parent & child together
|
||||
|
||||
@@ -12,6 +12,7 @@ from users.models import User
|
||||
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device, create_test_virtualmachine
|
||||
from virtualization.models import Cluster, ClusterType, ClusterGroup, VMInterface, VirtualMachine
|
||||
from wireless.choices import WirelessChannelChoices, WirelessRoleChoices
|
||||
from wireless.models import WirelessLink
|
||||
|
||||
|
||||
class DeviceComponentFilterSetTests:
|
||||
@@ -4496,7 +4497,9 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
# Cables
|
||||
Cable(a_terminations=[interfaces[0]], b_terminations=[interfaces[5]]).save()
|
||||
Cable(a_terminations=[interfaces[1]], b_terminations=[interfaces[6]]).save()
|
||||
# Third pair is not connected
|
||||
|
||||
# Wireless links
|
||||
WirelessLink(interface_a=interfaces[7], interface_b=interfaces[8]).save()
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Interface 1', 'Interface 2']}
|
||||
@@ -4684,15 +4687,15 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
|
||||
def test_occupied(self):
|
||||
params = {'occupied': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
params = {'occupied': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_connected(self):
|
||||
params = {'connected': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
params = {'connected': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_kind(self):
|
||||
params = {'kind': 'physical'}
|
||||
|
||||
@@ -2793,6 +2793,7 @@ class InterfaceView(generic.ObjectView):
|
||||
),
|
||||
orderable=False
|
||||
)
|
||||
vdc_table.configure(request)
|
||||
|
||||
# Get bridge interfaces
|
||||
bridge_interfaces = Interface.objects.restrict(request.user, 'view').filter(bridge=instance)
|
||||
@@ -2801,6 +2802,7 @@ class InterfaceView(generic.ObjectView):
|
||||
exclude=('device', 'parent'),
|
||||
orderable=False
|
||||
)
|
||||
bridge_interfaces_table.configure(request)
|
||||
|
||||
# Get child interfaces
|
||||
child_interfaces = Interface.objects.restrict(request.user, 'view').filter(parent=instance)
|
||||
@@ -2809,6 +2811,7 @@ class InterfaceView(generic.ObjectView):
|
||||
exclude=('device', 'parent'),
|
||||
orderable=False
|
||||
)
|
||||
child_interfaces_table.configure(request)
|
||||
|
||||
# Get assigned VLANs and annotate whether each is tagged or untagged
|
||||
vlans = []
|
||||
@@ -2823,6 +2826,7 @@ class InterfaceView(generic.ObjectView):
|
||||
data=vlans,
|
||||
orderable=False
|
||||
)
|
||||
vlan_table.configure(request)
|
||||
|
||||
# Get VLAN translation rules
|
||||
vlan_translation_table = None
|
||||
@@ -2831,6 +2835,7 @@ class InterfaceView(generic.ObjectView):
|
||||
data=instance.vlan_translation_policy.rules.all(),
|
||||
orderable=False
|
||||
)
|
||||
vlan_translation_table.configure(request)
|
||||
|
||||
return {
|
||||
'vdc_table': vdc_table,
|
||||
|
||||
@@ -40,7 +40,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ConfigContext, lookups=True)
|
||||
@strawberry_django.filter_type(models.ConfigContext, lookups=True)
|
||||
class ConfigContextFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, ChangeLogFilterMixin):
|
||||
name: FilterLookup[str] = strawberry_django.filter_field()
|
||||
weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
@@ -97,7 +97,7 @@ class ConfigContextFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, Chan
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ConfigTemplate, lookups=True)
|
||||
@strawberry_django.filter_type(models.ConfigTemplate, lookups=True)
|
||||
class ConfigTemplateFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, ChangeLogFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@@ -111,7 +111,7 @@ class ConfigTemplateFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, Cha
|
||||
as_attachment: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.CustomField, lookups=True)
|
||||
@strawberry_django.filter_type(models.CustomField, lookups=True)
|
||||
class CustomFieldFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
type: Annotated['CustomFieldTypeEnum', strawberry.lazy('extras.graphql.enums')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -164,7 +164,7 @@ class CustomFieldFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
comments: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.CustomFieldChoiceSet, lookups=True)
|
||||
@strawberry_django.filter_type(models.CustomFieldChoiceSet, lookups=True)
|
||||
class CustomFieldChoiceSetFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@@ -177,7 +177,7 @@ class CustomFieldChoiceSetFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin
|
||||
order_alphabetically: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.CustomLink, lookups=True)
|
||||
@strawberry_django.filter_type(models.CustomLink, lookups=True)
|
||||
class CustomLinkFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
enabled: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
@@ -193,7 +193,7 @@ class CustomLinkFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
new_window: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ExportTemplate, lookups=True)
|
||||
@strawberry_django.filter_type(models.ExportTemplate, lookups=True)
|
||||
class ExportTemplateFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, ChangeLogFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@@ -207,7 +207,7 @@ class ExportTemplateFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, Cha
|
||||
as_attachment: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ImageAttachment, lookups=True)
|
||||
@strawberry_django.filter_type(models.ImageAttachment, lookups=True)
|
||||
class ImageAttachmentFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -222,7 +222,7 @@ class ImageAttachmentFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.JournalEntry, lookups=True)
|
||||
@strawberry_django.filter_type(models.JournalEntry, lookups=True)
|
||||
class JournalEntryFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin):
|
||||
assigned_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -238,7 +238,7 @@ class JournalEntryFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, Tag
|
||||
comments: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.NotificationGroup, lookups=True)
|
||||
@strawberry_django.filter_type(models.NotificationGroup, lookups=True)
|
||||
class NotificationGroupFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@@ -246,7 +246,7 @@ class NotificationGroupFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
users: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.SavedFilter, lookups=True)
|
||||
@strawberry_django.filter_type(models.SavedFilter, lookups=True)
|
||||
class SavedFilterFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@@ -263,7 +263,7 @@ class SavedFilterFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.TableConfig, lookups=True)
|
||||
@strawberry_django.filter_type(models.TableConfig, lookups=True)
|
||||
class TableConfigFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@@ -276,13 +276,13 @@ class TableConfigFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
shared: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Tag, lookups=True)
|
||||
@strawberry_django.filter_type(models.Tag, lookups=True)
|
||||
class TagFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin, TagBaseFilterMixin):
|
||||
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Webhook, lookups=True)
|
||||
@strawberry_django.filter_type(models.Webhook, lookups=True)
|
||||
class WebhookFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@@ -301,7 +301,7 @@ class WebhookFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilt
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.EventRule, lookups=True)
|
||||
@strawberry_django.filter_type(models.EventRule, lookups=True)
|
||||
class EventRuleFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
@@ -4,11 +4,12 @@ from django.db import migrations
|
||||
def convert_reportmodule_jobs(apps, schema_editor):
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
Job = apps.get_model('core', 'Job')
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
# Convert all ReportModule jobs to ScriptModule jobs
|
||||
if reportmodule_ct := ContentType.objects.filter(app_label='extras', model='reportmodule').first():
|
||||
scriptmodule_ct = ContentType.objects.get(app_label='extras', model='scriptmodule')
|
||||
Job.objects.filter(object_type_id=reportmodule_ct.id).update(object_type_id=scriptmodule_ct.id)
|
||||
if reportmodule_ct := ContentType.objects.using(db_alias).filter(app_label='extras', model='reportmodule').first():
|
||||
scriptmodule_ct = ContentType.objects.using(db_alias).get(app_label='extras', model='scriptmodule')
|
||||
Job.objects.using(db_alias).filter(object_type_id=reportmodule_ct.id).update(object_type_id=scriptmodule_ct.id)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@@ -88,24 +88,33 @@ def update_scripts(apps, schema_editor):
|
||||
ScriptModule = apps.get_model('extras', 'ScriptModule')
|
||||
ReportModule = apps.get_model('extras', 'ReportModule')
|
||||
Job = apps.get_model('core', 'Job')
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
script_ct = ContentType.objects.get_for_model(Script, for_concrete_model=False)
|
||||
scriptmodule_ct = ContentType.objects.get_for_model(ScriptModule, for_concrete_model=False)
|
||||
reportmodule_ct = ContentType.objects.get_for_model(ReportModule, for_concrete_model=False)
|
||||
|
||||
for module in ScriptModule.objects.all():
|
||||
for module in ScriptModule.objects.using(db_alias).all():
|
||||
for script_name in get_module_scripts(module):
|
||||
script = Script.objects.create(
|
||||
script = Script.objects.using(db_alias).create(
|
||||
name=script_name,
|
||||
module=module,
|
||||
)
|
||||
|
||||
# Update all Jobs associated with this ScriptModule & script name to point to the new Script object
|
||||
Job.objects.filter(object_type_id=scriptmodule_ct.id, object_id=module.pk, name=script_name).update(
|
||||
Job.objects.using(db_alias).filter(
|
||||
object_type_id=scriptmodule_ct.id,
|
||||
object_id=module.pk,
|
||||
name=script_name
|
||||
).update(
|
||||
object_type_id=script_ct.id, object_id=script.pk
|
||||
)
|
||||
# Update all Jobs associated with this ScriptModule & script name to point to the new Script object
|
||||
Job.objects.filter(object_type_id=reportmodule_ct.id, object_id=module.pk, name=script_name).update(
|
||||
Job.objects.using(db_alias).filter(
|
||||
object_type_id=reportmodule_ct.id,
|
||||
object_id=module.pk,
|
||||
name=script_name
|
||||
).update(
|
||||
object_type_id=script_ct.id, object_id=script.pk
|
||||
)
|
||||
|
||||
@@ -119,16 +128,22 @@ def update_event_rules(apps, schema_editor):
|
||||
Script = apps.get_model('extras', 'Script')
|
||||
ScriptModule = apps.get_model('extras', 'ScriptModule')
|
||||
EventRule = apps.get_model('extras', 'EventRule')
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
script_ct = ContentType.objects.get_for_model(Script)
|
||||
scriptmodule_ct = ContentType.objects.get_for_model(ScriptModule)
|
||||
|
||||
for eventrule in EventRule.objects.filter(action_object_type=scriptmodule_ct):
|
||||
for eventrule in EventRule.objects.using(db_alias).filter(action_object_type=scriptmodule_ct):
|
||||
name = eventrule.action_parameters.get('script_name')
|
||||
obj, __ = Script.objects.get_or_create(
|
||||
module_id=eventrule.action_object_id, name=name, defaults={'is_executable': False}
|
||||
obj, __ = Script.objects.using(db_alias).get_or_create(
|
||||
module_id=eventrule.action_object_id,
|
||||
name=name,
|
||||
defaults={'is_executable': False}
|
||||
)
|
||||
EventRule.objects.using(db_alias).filter(pk=eventrule.pk).update(
|
||||
action_object_type=script_ct,
|
||||
action_object_id=obj.id
|
||||
)
|
||||
EventRule.objects.filter(pk=eventrule.pk).update(action_object_type=script_ct, action_object_id=obj.id)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
# Generated by Django 5.0.4 on 2024-04-24 20:09
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def update_dashboard_widgets(apps, schema_editor):
|
||||
Dashboard = apps.get_model('extras', 'Dashboard')
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
for dashboard in Dashboard.objects.all():
|
||||
for dashboard in Dashboard.objects.using(db_alias).all():
|
||||
for key, widget in dashboard.config.items():
|
||||
if models := widget['config'].get('models'):
|
||||
models = list(map(lambda x: x.replace('users.netboxgroup', 'users.group'), models))
|
||||
|
||||
@@ -3,7 +3,9 @@ from django.db import migrations, models
|
||||
|
||||
def update_link_buttons(apps, schema_editor):
|
||||
CustomLink = apps.get_model('extras', 'CustomLink')
|
||||
CustomLink.objects.filter(button_class='outline-dark').update(button_class='default')
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
CustomLink.objects.using(db_alias).filter(button_class='outline-dark').update(button_class='default')
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@@ -3,19 +3,21 @@ from django.db import migrations
|
||||
|
||||
def update_content_types(apps, schema_editor):
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
# Delete the new ContentTypes effected by the new model in the core app
|
||||
ContentType.objects.filter(app_label='core', model='objectchange').delete()
|
||||
ContentType.objects.using(db_alias).filter(app_label='core', model='objectchange').delete()
|
||||
|
||||
# Update the app labels of the original ContentTypes for extras.ObjectChange to ensure that any
|
||||
# foreign key references are preserved
|
||||
ContentType.objects.filter(app_label='extras', model='objectchange').update(app_label='core')
|
||||
ContentType.objects.using(db_alias).filter(app_label='extras', model='objectchange').update(app_label='core')
|
||||
|
||||
|
||||
def update_dashboard_widgets(apps, schema_editor):
|
||||
Dashboard = apps.get_model('extras', 'Dashboard')
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
for dashboard in Dashboard.objects.all():
|
||||
for dashboard in Dashboard.objects.using(db_alias).all():
|
||||
for key, widget in dashboard.config.items():
|
||||
if widget['config'].get('model') == 'extras.objectchange':
|
||||
widget['config']['model'] = 'core.objectchange'
|
||||
|
||||
@@ -6,8 +6,9 @@ from core.events import *
|
||||
|
||||
def set_event_types(apps, schema_editor):
|
||||
EventRule = apps.get_model('extras', 'EventRule')
|
||||
event_rules = EventRule.objects.all()
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
event_rules = EventRule.objects.using(db_alias).all()
|
||||
for event_rule in event_rules:
|
||||
event_rule.event_types = []
|
||||
if event_rule.type_create:
|
||||
|
||||
@@ -6,8 +6,9 @@ def set_null_values(apps, schema_editor):
|
||||
Replace empty strings with null values.
|
||||
"""
|
||||
CustomFieldChoiceSet = apps.get_model('extras', 'CustomFieldChoiceSet')
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
CustomFieldChoiceSet.objects.filter(base_choices='').update(base_choices=None)
|
||||
CustomFieldChoiceSet.objects.using(db_alias).filter(base_choices='').update(base_choices=None)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@@ -8,7 +8,9 @@ def set_kind_default(apps, schema_editor):
|
||||
Set kind to "info" on any entries with no kind assigned.
|
||||
"""
|
||||
JournalEntry = apps.get_model('extras', 'JournalEntry')
|
||||
JournalEntry.objects.filter(kind='').update(kind=JournalEntryKindChoices.KIND_INFO)
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
JournalEntry.objects.using(db_alias).filter(kind='').update(kind=JournalEntryKindChoices.KIND_INFO)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@@ -131,7 +131,7 @@ class RenderTemplateMixin(models.Model):
|
||||
"""
|
||||
context = self.get_context(context=context, queryset=queryset)
|
||||
env_params = self.environment_params or {}
|
||||
output = render_jinja2(self.template_code, context, env_params)
|
||||
output = render_jinja2(self.template_code, context, env_params, getattr(self, 'data_file', None))
|
||||
|
||||
# Replace CRLF-style line terminators
|
||||
output = output.replace('\r\n', '\n')
|
||||
|
||||
@@ -24,6 +24,17 @@ class JournalEntryIndex(SearchIndex):
|
||||
display_attrs = ('kind', 'created_by')
|
||||
|
||||
|
||||
@register_search
|
||||
class TagIndex(SearchIndex):
|
||||
model = models.Tag
|
||||
fields = (
|
||||
('name', 100),
|
||||
('slug', 110),
|
||||
('description', 500),
|
||||
)
|
||||
display_attrs = ('description',)
|
||||
|
||||
|
||||
@register_search
|
||||
class WebhookEntryIndex(SearchIndex):
|
||||
model = models.Webhook
|
||||
|
||||
@@ -2,7 +2,7 @@ import datetime
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import make_aware
|
||||
from django.utils.timezone import make_aware, now
|
||||
from rest_framework import status
|
||||
|
||||
from core.choices import ManagedFileRootPathChoices
|
||||
@@ -991,6 +991,10 @@ class SubscriptionTest(APIViewTestCases.APIViewTestCase):
|
||||
},
|
||||
]
|
||||
|
||||
cls.bulk_update_data = {
|
||||
'user': users[3].pk,
|
||||
}
|
||||
|
||||
|
||||
class NotificationGroupTest(APIViewTestCases.APIViewTestCase):
|
||||
model = NotificationGroup
|
||||
@@ -1072,6 +1076,9 @@ class NotificationGroupTest(APIViewTestCases.APIViewTestCase):
|
||||
class NotificationTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Notification
|
||||
brief_fields = ['display', 'event_type', 'id', 'object_id', 'object_type', 'read', 'url', 'user']
|
||||
bulk_update_data = {
|
||||
'read': now(),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
from django.forms import ValidationError
|
||||
from django.test import TestCase
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from core.models import ObjectType
|
||||
from django.forms import ValidationError
|
||||
from django.test import tag, TestCase
|
||||
|
||||
from core.models import DataSource, ObjectType
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Location, Manufacturer, Platform, Region, Site, SiteGroup
|
||||
from extras.models import ConfigContext, Tag
|
||||
from extras.models import ConfigContext, ConfigTemplate, Tag
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.exceptions import AbortRequest
|
||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
||||
@@ -33,8 +36,8 @@ class TagTest(TestCase):
|
||||
]
|
||||
|
||||
site = Site.objects.create(name='Site 1')
|
||||
for tag in tags:
|
||||
site.tags.add(tag)
|
||||
for _tag in tags:
|
||||
site.tags.add(_tag)
|
||||
site.save()
|
||||
|
||||
site = Site.objects.first()
|
||||
@@ -540,3 +543,66 @@ class ConfigContextTest(TestCase):
|
||||
device.local_context_data = 'foo'
|
||||
with self.assertRaises(ValidationError):
|
||||
device.clean()
|
||||
|
||||
|
||||
class ConfigTemplateTest(TestCase):
|
||||
"""
|
||||
TODO: These test cases deal with the weighting, ordering, and deep merge logic of config context data.
|
||||
"""
|
||||
MAIN_TEMPLATE = """
|
||||
{%- include 'base.j2' %}
|
||||
""".strip()
|
||||
BASE_TEMPLATE = """
|
||||
Hi
|
||||
""".strip()
|
||||
|
||||
@classmethod
|
||||
def _create_template_file(cls, templates_dir, file_name, content):
|
||||
template_file_name = file_name
|
||||
if not template_file_name.endswith('j2'):
|
||||
template_file_name += '.j2'
|
||||
temp_file_path = templates_dir / template_file_name
|
||||
|
||||
with open(temp_file_path, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
temp_dir = tempfile.TemporaryDirectory()
|
||||
templates_dir = Path(temp_dir.name) / "templates"
|
||||
templates_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
cls._create_template_file(templates_dir, 'base.j2', cls.BASE_TEMPLATE)
|
||||
cls._create_template_file(templates_dir, 'main.j2', cls.MAIN_TEMPLATE)
|
||||
|
||||
data_source = DataSource(
|
||||
name="Test DataSource",
|
||||
type="local",
|
||||
source_url=str(templates_dir),
|
||||
)
|
||||
data_source.save()
|
||||
data_source.sync()
|
||||
|
||||
base_config_template = ConfigTemplate(
|
||||
name="BaseTemplate",
|
||||
data_file=data_source.datafiles.filter(path__endswith='base.j2').first()
|
||||
)
|
||||
base_config_template.clean()
|
||||
base_config_template.save()
|
||||
cls.base_config_template = base_config_template
|
||||
|
||||
main_config_template = ConfigTemplate(
|
||||
name="MainTemplate",
|
||||
data_file=data_source.datafiles.filter(path__endswith='main.j2').first()
|
||||
)
|
||||
main_config_template.clean()
|
||||
main_config_template.save()
|
||||
cls.main_config_template = main_config_template
|
||||
|
||||
@tag('regression')
|
||||
def test_config_template_with_data_source(self):
|
||||
self.assertEqual(self.BASE_TEMPLATE, self.base_config_template.render({}))
|
||||
|
||||
@tag('regression')
|
||||
def test_config_template_with_data_source_nested_templates(self):
|
||||
self.assertEqual(self.BASE_TEMPLATE, self.main_config_template.render({}))
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import logging
|
||||
import tempfile
|
||||
from datetime import date, datetime, timezone
|
||||
|
||||
@@ -7,6 +8,7 @@ from netaddr import IPAddress, IPNetwork
|
||||
|
||||
from dcim.models import DeviceRole
|
||||
from extras.scripts import *
|
||||
from utilities.testing import disable_logging
|
||||
|
||||
CHOICES = (
|
||||
('ff0000', 'Red'),
|
||||
@@ -39,7 +41,8 @@ class ScriptTest(TestCase):
|
||||
datafile.write(bytes(YAML_DATA, 'UTF-8'))
|
||||
datafile.seek(0)
|
||||
|
||||
data = Script().load_yaml(datafile.name)
|
||||
with disable_logging(level=logging.WARNING):
|
||||
data = Script().load_yaml(datafile.name)
|
||||
self.assertEqual(data, {
|
||||
'Foo': 123,
|
||||
'Bar': 456,
|
||||
@@ -51,7 +54,8 @@ class ScriptTest(TestCase):
|
||||
datafile.write(bytes(JSON_DATA, 'UTF-8'))
|
||||
datafile.seek(0)
|
||||
|
||||
data = Script().load_json(datafile.name)
|
||||
with disable_logging(level=logging.WARNING):
|
||||
data = Script().load_json(datafile.name)
|
||||
self.assertEqual(data, {
|
||||
'Foo': 123,
|
||||
'Bar': 456,
|
||||
|
||||
@@ -966,7 +966,7 @@ class ObjectRenderConfigView(generic.ObjectView):
|
||||
|
||||
# Render the config template
|
||||
rendered_config = None
|
||||
error_message = None
|
||||
error_message = ''
|
||||
if config_template := instance.get_config_template():
|
||||
try:
|
||||
rendered_config = config_template.render(context=context_data)
|
||||
|
||||
@@ -147,8 +147,7 @@ class IPRangeSerializer(NetBoxModelSerializer):
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'family', 'start_address', 'end_address', 'size', 'vrf', 'tenant',
|
||||
'status', 'role', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'mark_populated', 'mark_utilized', 'description', 'comments', 'tags', 'custom_fields', 'created',
|
||||
'last_updated',
|
||||
'mark_populated', 'mark_utilized',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'family', 'start_address', 'end_address', 'description')
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ class VLANSerializer(NetBoxModelSerializer):
|
||||
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||
status = ChoiceField(choices=VLANStatusChoices, required=False)
|
||||
role = RoleSerializer(nested=True, required=False, allow_null=True)
|
||||
qinq_role = ChoiceField(choices=VLANQinQRoleChoices, required=False)
|
||||
qinq_role = ChoiceField(choices=VLANQinQRoleChoices, required=False, allow_null=True)
|
||||
qinq_svlan = NestedVLANSerializer(required=False, allow_null=True, default=None)
|
||||
l2vpn_termination = L2VPNTerminationSerializer(nested=True, read_only=True, allow_null=True)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from copy import deepcopy
|
||||
|
||||
from django.contrib.contenttypes.prefetch import GenericPrefetch
|
||||
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
||||
from django.db import transaction
|
||||
from django.shortcuts import get_object_or_404
|
||||
@@ -13,6 +14,7 @@ from rest_framework.response import Response
|
||||
from rest_framework.routers import APIRootView
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from dcim.models import Interface
|
||||
from ipam import filtersets
|
||||
from ipam.models import *
|
||||
from ipam.utils import get_next_available_prefix
|
||||
@@ -21,6 +23,7 @@ from netbox.api.viewsets.mixins import ObjectValidationMixin
|
||||
from netbox.config import get_config
|
||||
from netbox.constants import ADVISORY_LOCK_KEYS
|
||||
from utilities.api import get_serializer_for_model
|
||||
from virtualization.models import VMInterface
|
||||
from . import serializers
|
||||
|
||||
|
||||
@@ -79,7 +82,7 @@ class RoleViewSet(NetBoxModelViewSet):
|
||||
|
||||
|
||||
class PrefixViewSet(NetBoxModelViewSet):
|
||||
queryset = Prefix.objects.all()
|
||||
queryset = Prefix.objects.prefetch_related("scope")
|
||||
serializer_class = serializers.PrefixSerializer
|
||||
filterset_class = filtersets.PrefixFilterSet
|
||||
|
||||
@@ -100,7 +103,17 @@ class IPRangeViewSet(NetBoxModelViewSet):
|
||||
|
||||
|
||||
class IPAddressViewSet(NetBoxModelViewSet):
|
||||
queryset = IPAddress.objects.all()
|
||||
queryset = IPAddress.objects.prefetch_related(
|
||||
GenericPrefetch(
|
||||
"assigned_object",
|
||||
[
|
||||
# serializers are taken according to IPADDRESS_ASSIGNMENT_MODELS
|
||||
FHRPGroup.objects.all(),
|
||||
Interface.objects.select_related("cable", "device"),
|
||||
VMInterface.objects.select_related("virtual_machine"),
|
||||
],
|
||||
),
|
||||
)
|
||||
serializer_class = serializers.IPAddressSerializer
|
||||
filterset_class = filtersets.IPAddressFilterSet
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ASN, lookups=True)
|
||||
@strawberry_django.filter_type(models.ASN, lookups=True)
|
||||
class ASNFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
rir: Annotated['RIRFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
rir_id: ID | None = strawberry_django.filter_field()
|
||||
@@ -61,7 +61,7 @@ class ASNFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
) = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ASNRange, lookups=True)
|
||||
@strawberry_django.filter_type(models.ASNRange, lookups=True)
|
||||
class ASNRangeFilter(TenancyFilterMixin, OrganizationalModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@@ -75,7 +75,7 @@ class ASNRangeFilter(TenancyFilterMixin, OrganizationalModelFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Aggregate, lookups=True)
|
||||
@strawberry_django.filter_type(models.Aggregate, lookups=True)
|
||||
class AggregateFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
prefix: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
prefix_id: ID | None = strawberry_django.filter_field()
|
||||
@@ -84,7 +84,7 @@ class AggregateFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter
|
||||
date_added: DateFilterLookup[date] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.FHRPGroup, lookups=True)
|
||||
@strawberry_django.filter_type(models.FHRPGroup, lookups=True)
|
||||
class FHRPGroupFilter(PrimaryModelFilterMixin):
|
||||
group_id: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -102,7 +102,7 @@ class FHRPGroupFilter(PrimaryModelFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.FHRPGroupAssignment, lookups=True)
|
||||
@strawberry_django.filter_type(models.FHRPGroupAssignment, lookups=True)
|
||||
class FHRPGroupAssignmentFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
interface_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -117,7 +117,7 @@ class FHRPGroupAssignmentFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin)
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.IPAddress, lookups=True)
|
||||
@strawberry_django.filter_type(models.IPAddress, lookups=True)
|
||||
class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
address: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
vrf: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
@@ -142,6 +142,10 @@ class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter
|
||||
nat_outside_id: ID | None = strawberry_django.filter_field()
|
||||
dns_name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
@strawberry_django.filter_field()
|
||||
def assigned(self, value: bool, prefix) -> Q:
|
||||
return Q(assigned_object_id__isnull=(not value))
|
||||
|
||||
@strawberry_django.filter_field()
|
||||
def parent(self, value: list[str], prefix) -> Q:
|
||||
if not value:
|
||||
@@ -155,8 +159,16 @@ class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter
|
||||
return Q()
|
||||
return q
|
||||
|
||||
@strawberry_django.filter_field()
|
||||
def family(
|
||||
self,
|
||||
value: Annotated['IPAddressFamilyEnum', strawberry.lazy('ipam.graphql.enums')],
|
||||
prefix,
|
||||
) -> Q:
|
||||
return Q(**{f"{prefix}address__family": value.value})
|
||||
|
||||
@strawberry_django.filter(models.IPRange, lookups=True)
|
||||
|
||||
@strawberry_django.filter_type(models.IPRange, lookups=True)
|
||||
class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
start_address: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
end_address: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@@ -168,9 +180,7 @@ class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMi
|
||||
status: Annotated['IPRangeStatusEnum', strawberry.lazy('ipam.graphql.enums')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
role: Annotated['IPAddressRoleEnum', strawberry.lazy('ipam.graphql.enums')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
role: Annotated['RoleFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
mark_utilized: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
|
||||
@strawberry_django.filter_field()
|
||||
@@ -187,7 +197,7 @@ class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMi
|
||||
return q
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Prefix, lookups=True)
|
||||
@strawberry_django.filter_type(models.Prefix, lookups=True)
|
||||
class PrefixFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
prefix: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
vrf: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
@@ -203,19 +213,19 @@ class PrefixFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, Pr
|
||||
mark_utilized: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.RIR, lookups=True)
|
||||
@strawberry_django.filter_type(models.RIR, lookups=True)
|
||||
class RIRFilter(OrganizationalModelFilterMixin):
|
||||
is_private: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Role, lookups=True)
|
||||
@strawberry_django.filter_type(models.Role, lookups=True)
|
||||
class RoleFilter(OrganizationalModelFilterMixin):
|
||||
weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.RouteTarget, lookups=True)
|
||||
@strawberry_django.filter_type(models.RouteTarget, lookups=True)
|
||||
class RouteTargetFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
importing_vrfs: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
@@ -232,7 +242,7 @@ class RouteTargetFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Service, lookups=True)
|
||||
@strawberry_django.filter_type(models.Service, lookups=True)
|
||||
class ServiceFilter(ContactFilterMixin, ServiceBaseFilterMixin, PrimaryModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
ip_addresses: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
@@ -244,12 +254,12 @@ class ServiceFilter(ContactFilterMixin, ServiceBaseFilterMixin, PrimaryModelFilt
|
||||
parent_object_id: ID | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ServiceTemplate, lookups=True)
|
||||
@strawberry_django.filter_type(models.ServiceTemplate, lookups=True)
|
||||
class ServiceTemplateFilter(ServiceBaseFilterMixin, PrimaryModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VLAN, lookups=True)
|
||||
@strawberry_django.filter_type(models.VLAN, lookups=True)
|
||||
class VLANFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
site_id: ID | None = strawberry_django.filter_field()
|
||||
@@ -279,19 +289,19 @@ class VLANFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VLANGroup, lookups=True)
|
||||
@strawberry_django.filter_type(models.VLANGroup, lookups=True)
|
||||
class VLANGroupFilter(ScopedFilterMixin, OrganizationalModelFilterMixin):
|
||||
vid_ranges: Annotated['IntegerArrayLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VLANTranslationPolicy, lookups=True)
|
||||
@strawberry_django.filter_type(models.VLANTranslationPolicy, lookups=True)
|
||||
class VLANTranslationPolicyFilter(PrimaryModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VLANTranslationRule, lookups=True)
|
||||
@strawberry_django.filter_type(models.VLANTranslationRule, lookups=True)
|
||||
class VLANTranslationRuleFilter(NetBoxModelFilterMixin):
|
||||
policy: Annotated['VLANTranslationPolicyFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -306,7 +316,7 @@ class VLANTranslationRuleFilter(NetBoxModelFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VRF, lookups=True)
|
||||
@strawberry_django.filter_type(models.VRF, lookups=True)
|
||||
class VRFFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
rd: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
@@ -11,7 +11,9 @@ def set_vid_ranges(apps, schema_editor):
|
||||
Convert the min_vid & max_vid fields to a range in the new vid_ranges ArrayField.
|
||||
"""
|
||||
VLANGroup = apps.get_model('ipam', 'VLANGroup')
|
||||
for group in VLANGroup.objects.all():
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
for group in VLANGroup.objects.using(db_alias).all():
|
||||
group.vid_ranges = [NumericRange(group.min_vid, group.max_vid, bounds='[]')]
|
||||
group._total_vlan_ids = group.max_vid - group.min_vid + 1
|
||||
group.save()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import django.db.models.deletion
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@@ -9,9 +10,11 @@ def copy_site_assignments(apps, schema_editor):
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
Prefix = apps.get_model('ipam', 'Prefix')
|
||||
Site = apps.get_model('dcim', 'Site')
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
Prefix.objects.filter(site__isnull=False).update(
|
||||
scope_type=ContentType.objects.get_for_model(Site), scope_id=models.F('site_id')
|
||||
Prefix.objects.using(db_alias).filter(site__isnull=False).update(
|
||||
scope_type=ContentType.objects.get_for_model(Site),
|
||||
scope_id=models.F('site_id')
|
||||
)
|
||||
|
||||
|
||||
@@ -42,3 +45,20 @@ class Migration(migrations.Migration):
|
||||
# Copy over existing site assignments
|
||||
migrations.RunPython(code=copy_site_assignments, reverse_code=migrations.RunPython.noop),
|
||||
]
|
||||
|
||||
|
||||
def oc_prefix_scope(objectchange, reverting):
|
||||
site_ct = ContentType.objects.get_by_natural_key('dcim', 'site').pk
|
||||
for data in (objectchange.prechange_data, objectchange.postchange_data):
|
||||
if data is None:
|
||||
continue
|
||||
if site_id := data.get('site'):
|
||||
data.update({
|
||||
'scope_type': site_ct,
|
||||
'scope_id': site_id,
|
||||
})
|
||||
|
||||
|
||||
objectchange_migrators = {
|
||||
'ipam.prefix': oc_prefix_scope,
|
||||
}
|
||||
|
||||
@@ -7,15 +7,16 @@ def populate_denormalized_fields(apps, schema_editor):
|
||||
Copy site ForeignKey values to the scope GFK.
|
||||
"""
|
||||
Prefix = apps.get_model('ipam', 'Prefix')
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
prefixes = Prefix.objects.filter(site__isnull=False).prefetch_related('site')
|
||||
prefixes = Prefix.objects.using(db_alias).filter(site__isnull=False).prefetch_related('site')
|
||||
for prefix in prefixes:
|
||||
prefix._region_id = prefix.site.region_id
|
||||
prefix._site_group_id = prefix.site.group_id
|
||||
prefix._site_id = prefix.site_id
|
||||
# Note: Location cannot be set prior to migration
|
||||
|
||||
Prefix.objects.bulk_update(prefixes, ['_region', '_site_group', '_site'], batch_size=100)
|
||||
Prefix.objects.using(db_alias).bulk_update(prefixes, ['_region', '_site_group', '_site'], batch_size=100)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -59,3 +60,14 @@ class Migration(migrations.Migration):
|
||||
name='site',
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def oc_prefix_remove_fields(objectchange, reverting):
|
||||
for data in (objectchange.prechange_data, objectchange.postchange_data):
|
||||
if data is not None:
|
||||
data.pop('site', None)
|
||||
|
||||
|
||||
objectchange_migrators = {
|
||||
'ipam.prefix': oc_prefix_remove_fields,
|
||||
}
|
||||
|
||||
@@ -7,9 +7,10 @@ def set_null_values(apps, schema_editor):
|
||||
"""
|
||||
FHRPGroup = apps.get_model('ipam', 'FHRPGroup')
|
||||
IPAddress = apps.get_model('ipam', 'IPAddress')
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
FHRPGroup.objects.filter(auth_type='').update(auth_type=None)
|
||||
IPAddress.objects.filter(role='').update(role=None)
|
||||
FHRPGroup.objects.using(db_alias).filter(auth_type='').update(auth_type=None)
|
||||
IPAddress.objects.using(db_alias).filter(role='').update(role=None)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@@ -1,37 +1,40 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import migrations
|
||||
from django.db.models import F
|
||||
|
||||
|
||||
def populate_service_parent_gfk(apps, schema_config):
|
||||
def populate_service_parent_gfk(apps, schema_editor):
|
||||
Service = apps.get_model('ipam', 'Service')
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
Device = apps.get_model('dcim', 'device')
|
||||
VirtualMachine = apps.get_model('virtualization', 'virtualmachine')
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
Service.objects.filter(device_id__isnull=False).update(
|
||||
Service.objects.using(db_alias).filter(device_id__isnull=False).update(
|
||||
parent_object_type=ContentType.objects.get_for_model(Device),
|
||||
parent_object_id=F('device_id'),
|
||||
)
|
||||
|
||||
Service.objects.filter(virtual_machine_id__isnull=False).update(
|
||||
Service.objects.using(db_alias).filter(virtual_machine_id__isnull=False).update(
|
||||
parent_object_type=ContentType.objects.get_for_model(VirtualMachine),
|
||||
parent_object_id=F('virtual_machine_id'),
|
||||
)
|
||||
|
||||
|
||||
def repopulate_device_and_virtualmachine_relations(apps, schemaconfig):
|
||||
def repopulate_device_and_virtualmachine_relations(apps, schema_editor):
|
||||
Service = apps.get_model('ipam', 'Service')
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
Device = apps.get_model('dcim', 'device')
|
||||
VirtualMachine = apps.get_model('virtualization', 'virtualmachine')
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
Service.objects.filter(
|
||||
Service.objects.using(db_alias).filter(
|
||||
parent_object_type=ContentType.objects.get_for_model(Device),
|
||||
).update(
|
||||
device_id=F('parent_object_id')
|
||||
)
|
||||
|
||||
Service.objects.filter(
|
||||
Service.objects.using(db_alias).filter(
|
||||
parent_object_type=ContentType.objects.get_for_model(VirtualMachine),
|
||||
).update(
|
||||
virtual_machine_id=F('parent_object_id')
|
||||
@@ -52,3 +55,26 @@ class Migration(migrations.Migration):
|
||||
reverse_code=repopulate_device_and_virtualmachine_relations,
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def oc_service_parent(objectchange, reverting):
|
||||
device_ct = ContentType.objects.get_by_natural_key('dcim', 'device').pk
|
||||
virtual_machine_ct = ContentType.objects.get_by_natural_key('virtualization', 'virtualmachine').pk
|
||||
for data in (objectchange.prechange_data, objectchange.postchange_data):
|
||||
if data is None:
|
||||
continue
|
||||
if device_id := data.get('device'):
|
||||
data.update({
|
||||
'parent_object_type': device_ct,
|
||||
'parent_object_id': device_id,
|
||||
})
|
||||
elif virtual_machine_id := data.get('virtual_machine'):
|
||||
data.update({
|
||||
'parent_object_type': virtual_machine_ct,
|
||||
'parent_object_id': virtual_machine_id,
|
||||
})
|
||||
|
||||
|
||||
objectchange_migrators = {
|
||||
'ipam.service': oc_service_parent,
|
||||
}
|
||||
|
||||
@@ -37,3 +37,15 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def oc_service_remove_fields(objectchange, reverting):
|
||||
for data in (objectchange.prechange_data, objectchange.postchange_data):
|
||||
if data is not None:
|
||||
data.pop('device', None)
|
||||
data.pop('virtual_machine', None)
|
||||
|
||||
|
||||
objectchange_migrators = {
|
||||
'ipam.service': oc_service_remove_fields,
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ class VLANQuerySet(RestrictedQuerySet):
|
||||
|
||||
# Find all relevant VLANGroups
|
||||
q = Q()
|
||||
site = vm.site or vm.cluster._site
|
||||
site = vm.site
|
||||
if vm.cluster:
|
||||
# Add VLANGroups scoped to the assigned cluster (or its group)
|
||||
q |= Q(
|
||||
@@ -160,6 +160,30 @@ class VLANQuerySet(RestrictedQuerySet):
|
||||
scope_type=ContentType.objects.get_by_natural_key('virtualization', 'clustergroup'),
|
||||
scope_id=vm.cluster.group_id
|
||||
)
|
||||
# Looking all possible cluster scopes
|
||||
if vm.cluster.scope_type == ContentType.objects.get_by_natural_key('dcim', 'location'):
|
||||
site = site or vm.cluster.scope.site
|
||||
q |= Q(
|
||||
scope_type=ContentType.objects.get_by_natural_key('dcim', 'location'),
|
||||
scope_id__in=vm.cluster.scope.get_ancestors(include_self=True)
|
||||
)
|
||||
elif vm.cluster.scope_type == ContentType.objects.get_by_natural_key('dcim', 'site'):
|
||||
site = site or vm.cluster.scope
|
||||
q |= Q(
|
||||
scope_type=ContentType.objects.get_by_natural_key('dcim', 'site'),
|
||||
scope_id=vm.cluster.scope.pk
|
||||
)
|
||||
elif vm.cluster.scope_type == ContentType.objects.get_by_natural_key('dcim', 'sitegroup'):
|
||||
q |= Q(
|
||||
scope_type=ContentType.objects.get_by_natural_key('dcim', 'sitegroup'),
|
||||
scope_id__in=vm.cluster.scope.get_ancestors(include_self=True)
|
||||
)
|
||||
elif vm.cluster.scope_type == ContentType.objects.get_by_natural_key('dcim', 'region'):
|
||||
q |= Q(
|
||||
scope_type=ContentType.objects.get_by_natural_key('dcim', 'region'),
|
||||
scope_id__in=vm.cluster.scope.get_ancestors(include_self=True)
|
||||
)
|
||||
# VM can be assigned to a site without a cluster so checking assigned site independently
|
||||
if site:
|
||||
# Add VLANGroups scoped to the assigned site (or its group or region)
|
||||
q |= Q(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from django.test import tag
|
||||
from django.urls import reverse
|
||||
from netaddr import IPNetwork
|
||||
from rest_framework import status
|
||||
@@ -9,7 +11,7 @@ from ipam.choices import *
|
||||
from ipam.models import *
|
||||
from tenancy.models import Tenant
|
||||
from utilities.data import string_to_ranges
|
||||
from utilities.testing import APITestCase, APIViewTestCases, create_test_device, disable_warnings
|
||||
from utilities.testing import APITestCase, APIViewTestCases, create_test_device, disable_logging
|
||||
|
||||
|
||||
class AppTest(APITestCase):
|
||||
@@ -382,6 +384,18 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
|
||||
)
|
||||
Prefix.objects.bulk_create(prefixes)
|
||||
|
||||
@tag('regression')
|
||||
def test_clean_validates_scope(self):
|
||||
prefix = Prefix.objects.first()
|
||||
site = Site.objects.create(name='Test Site', slug='test-site')
|
||||
|
||||
data = {'scope_type': 'dcim.site', 'scope_id': site.id}
|
||||
url = reverse('ipam-api:prefix-detail', kwargs={'pk': prefix.pk})
|
||||
self.add_permissions('ipam.change_prefix')
|
||||
|
||||
response = self.client.patch(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
|
||||
def test_list_available_prefixes(self):
|
||||
"""
|
||||
Test retrieval of all available prefixes within a parent prefix.
|
||||
@@ -1026,7 +1040,7 @@ class VLANTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
self.add_permissions('ipam.delete_vlan')
|
||||
url = reverse('ipam-api:vlan-detail', kwargs={'pk': vlan.pk})
|
||||
with disable_warnings('netbox.api.views.ModelViewSet'):
|
||||
with disable_logging(level=logging.WARNING):
|
||||
response = self.client.delete(url, **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_409_CONFLICT)
|
||||
|
||||
@@ -1849,6 +1849,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
Cluster(name='Cluster 1', type=cluster_type, group=cluster_groups[0], scope=sites[0]),
|
||||
Cluster(name='Cluster 2', type=cluster_type, group=cluster_groups[1], scope=sites[1]),
|
||||
Cluster(name='Cluster 3', type=cluster_type, group=cluster_groups[2], scope=sites[2]),
|
||||
Cluster(name='Cluster 4', type=cluster_type, group=cluster_groups[0], scope=locations[0]),
|
||||
)
|
||||
for cluster in clusters:
|
||||
cluster.save()
|
||||
@@ -1857,6 +1858,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
VirtualMachine(name='Virtual Machine 1', cluster=clusters[0]),
|
||||
VirtualMachine(name='Virtual Machine 2', cluster=clusters[1]),
|
||||
VirtualMachine(name='Virtual Machine 3', cluster=clusters[2]),
|
||||
VirtualMachine(name='Virtual Machine 4', cluster=clusters[3]),
|
||||
)
|
||||
VirtualMachine.objects.bulk_create(virtual_machines)
|
||||
|
||||
@@ -1864,6 +1866,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
VMInterface(virtual_machine=virtual_machines[0], name='VM Interface 1'),
|
||||
VMInterface(virtual_machine=virtual_machines[1], name='VM Interface 2'),
|
||||
VMInterface(virtual_machine=virtual_machines[2], name='VM Interface 3'),
|
||||
VMInterface(virtual_machine=virtual_machines[3], name='VM Interface 4'),
|
||||
)
|
||||
VMInterface.objects.bulk_create(vm_interfaces)
|
||||
|
||||
@@ -1890,6 +1893,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
VLANGroup(name='Cluster 1', slug='cluster-1', scope=clusters[0]),
|
||||
VLANGroup(name='Cluster 2', slug='cluster-2', scope=clusters[1]),
|
||||
VLANGroup(name='Cluster 3', slug='cluster-3', scope=clusters[2]),
|
||||
VLANGroup(name='Cluster 4', slug='cluster-4', scope=clusters[3]),
|
||||
|
||||
# General purpose VLAN groups
|
||||
VLANGroup(name='VLAN Group 1', slug='vlan-group-1'),
|
||||
@@ -1944,11 +1948,12 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
VLAN(vid=19, name='Cluster 1', group=groups[18]),
|
||||
VLAN(vid=20, name='Cluster 2', group=groups[19]),
|
||||
VLAN(vid=21, name='Cluster 3', group=groups[20]),
|
||||
VLAN(vid=22, name='Cluster 4', group=groups[21]),
|
||||
VLAN(
|
||||
vid=101,
|
||||
name='VLAN 101',
|
||||
site=sites[3],
|
||||
group=groups[21],
|
||||
group=groups[22],
|
||||
role=roles[0],
|
||||
tenant=tenants[0],
|
||||
status=VLANStatusChoices.STATUS_ACTIVE,
|
||||
@@ -1957,7 +1962,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
vid=102,
|
||||
name='VLAN 102',
|
||||
site=sites[3],
|
||||
group=groups[21],
|
||||
group=groups[22],
|
||||
role=roles[0],
|
||||
tenant=tenants[0],
|
||||
status=VLANStatusChoices.STATUS_ACTIVE,
|
||||
@@ -1966,7 +1971,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
vid=201,
|
||||
name='VLAN 201',
|
||||
site=sites[4],
|
||||
group=groups[22],
|
||||
group=groups[23],
|
||||
role=roles[1],
|
||||
tenant=tenants[1],
|
||||
status=VLANStatusChoices.STATUS_DEPRECATED,
|
||||
@@ -1975,7 +1980,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
vid=202,
|
||||
name='VLAN 202',
|
||||
site=sites[4],
|
||||
group=groups[22],
|
||||
group=groups[23],
|
||||
role=roles[1],
|
||||
tenant=tenants[1],
|
||||
status=VLANStatusChoices.STATUS_DEPRECATED,
|
||||
@@ -1984,7 +1989,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
vid=301,
|
||||
name='VLAN 301',
|
||||
site=sites[5],
|
||||
group=groups[23],
|
||||
group=groups[24],
|
||||
role=roles[2],
|
||||
tenant=tenants[2],
|
||||
status=VLANStatusChoices.STATUS_RESERVED,
|
||||
@@ -1993,13 +1998,13 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
vid=302,
|
||||
name='VLAN 302',
|
||||
site=sites[5],
|
||||
group=groups[23],
|
||||
group=groups[24],
|
||||
role=roles[2],
|
||||
tenant=tenants[2],
|
||||
status=VLANStatusChoices.STATUS_RESERVED,
|
||||
),
|
||||
# Create one globally available VLAN on a VLAN group
|
||||
VLAN(vid=500, name='VLAN Group 1', group=groups[24]),
|
||||
VLAN(vid=500, name='VLAN Group 1', group=groups[25]),
|
||||
# Create one globally available VLAN
|
||||
VLAN(vid=1000, name='Global VLAN'),
|
||||
# Create some Q-in-Q service VLANs
|
||||
@@ -2130,6 +2135,9 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
vm_id = VirtualMachine.objects.first().pk
|
||||
params = {'available_on_virtualmachine': vm_id}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 7) # 5 scoped + 1 global group + 1 global
|
||||
vm_id = VirtualMachine.objects.get(name='Virtual Machine 4').pk
|
||||
params = {'available_on_virtualmachine': vm_id}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8) # 6 scoped + 1 global group + 1 global
|
||||
|
||||
def test_available_at_site(self):
|
||||
site_id = Site.objects.first().pk
|
||||
|
||||
@@ -45,10 +45,13 @@ class VRFView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
instance.import_targets.all(),
|
||||
orderable=False
|
||||
)
|
||||
import_targets_table.configure(request)
|
||||
|
||||
export_targets_table = tables.RouteTargetTable(
|
||||
instance.export_targets.all(),
|
||||
orderable=False
|
||||
)
|
||||
export_targets_table.configure(request)
|
||||
|
||||
return {
|
||||
'related_models': self.get_related_models(request, instance, omit=[Interface, VMInterface]),
|
||||
@@ -530,6 +533,7 @@ class PrefixView(generic.ObjectView):
|
||||
exclude=('vrf', 'utilization'),
|
||||
orderable=False
|
||||
)
|
||||
parent_prefix_table.configure(request)
|
||||
|
||||
# Duplicate prefixes table
|
||||
duplicate_prefixes = Prefix.objects.restrict(request.user, 'view').filter(
|
||||
@@ -544,6 +548,7 @@ class PrefixView(generic.ObjectView):
|
||||
exclude=('vrf', 'utilization'),
|
||||
orderable=False
|
||||
)
|
||||
duplicate_prefix_table.configure(request)
|
||||
|
||||
return {
|
||||
'aggregate': aggregate,
|
||||
@@ -709,6 +714,7 @@ class IPRangeView(generic.ObjectView):
|
||||
exclude=('vrf', 'utilization'),
|
||||
orderable=False
|
||||
)
|
||||
parent_prefixes_table.configure(request)
|
||||
|
||||
return {
|
||||
'parent_prefixes_table': parent_prefixes_table,
|
||||
@@ -796,6 +802,7 @@ class IPAddressView(generic.ObjectView):
|
||||
exclude=('vrf', 'utilization'),
|
||||
orderable=False
|
||||
)
|
||||
parent_prefixes_table.configure(request)
|
||||
|
||||
# Duplicate IPs table
|
||||
duplicate_ips = IPAddress.objects.restrict(request.user, 'view').filter(
|
||||
@@ -811,6 +818,7 @@ class IPAddressView(generic.ObjectView):
|
||||
duplicate_ips = duplicate_ips.exclude(role=IPAddressRoleChoices.ROLE_ANYCAST)
|
||||
# Limit to a maximum of 10 duplicates displayed here
|
||||
duplicate_ips_table = tables.IPAddressTable(duplicate_ips[:10], orderable=False)
|
||||
duplicate_ips_table.configure(request)
|
||||
|
||||
return {
|
||||
'parent_prefixes_table': parent_prefixes_table,
|
||||
@@ -888,6 +896,7 @@ class IPAddressAssignView(generic.ObjectView):
|
||||
# Limit to 100 results
|
||||
addresses = filtersets.IPAddressFilterSet(request.POST, addresses).qs[:100]
|
||||
table = tables.IPAddressAssignTable(addresses)
|
||||
table.configure(request)
|
||||
|
||||
return render(request, 'ipam/ipaddress_assign.html', {
|
||||
'form': form,
|
||||
@@ -1053,6 +1062,8 @@ class VLANTranslationPolicyView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
data=instance.rules.all(),
|
||||
orderable=False
|
||||
)
|
||||
vlan_translation_table.configure(request)
|
||||
|
||||
return {
|
||||
'vlan_translation_table': vlan_translation_table,
|
||||
}
|
||||
@@ -1170,6 +1181,7 @@ class FHRPGroupView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
data=FHRPGroupAssignment.objects.restrict(request.user, 'view').filter(group=instance),
|
||||
orderable=False
|
||||
)
|
||||
members_table.configure(request)
|
||||
members_table.columns.hide('group')
|
||||
|
||||
return {
|
||||
@@ -1289,6 +1301,7 @@ class VLANView(generic.ObjectView):
|
||||
'vrf', 'scope', 'role', 'tenant'
|
||||
)
|
||||
prefix_table = tables.PrefixTable(list(prefixes), exclude=('vlan', 'utilization'), orderable=False)
|
||||
prefix_table.configure(request)
|
||||
|
||||
return {
|
||||
'prefix_table': prefix_table,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.choices import *
|
||||
from utilities.conversion import to_grams, to_meters
|
||||
|
||||
@@ -58,7 +59,7 @@ class DistanceMixin(models.Model):
|
||||
max_digits=8,
|
||||
decimal_places=2,
|
||||
blank=True,
|
||||
null=True
|
||||
null=True,
|
||||
)
|
||||
distance_unit = models.CharField(
|
||||
verbose_name=_('distance unit'),
|
||||
@@ -69,7 +70,7 @@ class DistanceMixin(models.Model):
|
||||
)
|
||||
# Stores the normalized distance (in meters) for database ordering
|
||||
_abs_distance = models.DecimalField(
|
||||
max_digits=10,
|
||||
max_digits=13,
|
||||
decimal_places=4,
|
||||
blank=True,
|
||||
null=True
|
||||
|
||||
File diff suppressed because one or more lines are too long
2
netbox/project-static/dist/netbox.css
vendored
2
netbox/project-static/dist/netbox.css
vendored
File diff suppressed because one or more lines are too long
12
netbox/project-static/dist/netbox.js
vendored
12
netbox/project-static/dist/netbox.js
vendored
File diff suppressed because one or more lines are too long
6
netbox/project-static/dist/netbox.js.map
vendored
6
netbox/project-static/dist/netbox.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -23,14 +23,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "7.4.47",
|
||||
"@tabler/core": "1.2.0",
|
||||
"bootstrap": "5.3.5",
|
||||
"@tabler/core": "1.3.2",
|
||||
"bootstrap": "5.3.6",
|
||||
"clipboard": "2.0.11",
|
||||
"flatpickr": "4.6.13",
|
||||
"gridstack": "12.1.1",
|
||||
"gridstack": "12.2.1",
|
||||
"htmx.org": "2.0.4",
|
||||
"query-string": "9.1.1",
|
||||
"sass": "1.87.0",
|
||||
"query-string": "9.2.0",
|
||||
"sass": "1.89.1",
|
||||
"tom-select": "2.4.3",
|
||||
"typeface-inter": "3.18.1",
|
||||
"typeface-roboto-mono": "1.1.13"
|
||||
|
||||
@@ -106,7 +106,8 @@ function handleSubmit(event: Event): void {
|
||||
const toast = createToast('danger', 'Error Updating Table Configuration', res.error);
|
||||
toast.show();
|
||||
} else {
|
||||
location.reload();
|
||||
// Strip any URL query parameters & reload the page
|
||||
window.location.href = window.location.origin + window.location.pathname;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -757,13 +757,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8"
|
||||
integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==
|
||||
|
||||
"@tabler/core@1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@tabler/core/-/core-1.2.0.tgz#cc61cd60d0bc644709bd708f1dd917e760203b4e"
|
||||
integrity sha512-Zrisg/pMi3c/X8AFbmwY6GNlWS/XPlW/jzt6grMar8ICOT7jO0weU9f/KCVgA49I1jMg2ev0uGxcpI5DP3CNdQ==
|
||||
"@tabler/core@1.3.2":
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@tabler/core/-/core-1.3.2.tgz#85e4a47b661bca4cd50e26039fc25c4bdb4aff34"
|
||||
integrity sha512-QDVJbv48YJrahBLdxYkLi6NutQv4jGbkUWyzxE2NcNJ3s3GGpRx98JmbAoN92NZKNmf26vZdW6k2Q5haVKlS4A==
|
||||
dependencies:
|
||||
"@popperjs/core" "^2.11.8"
|
||||
bootstrap "5.3.5"
|
||||
bootstrap "5.3.6"
|
||||
|
||||
"@tanstack/react-virtual@^3.0.0-beta.60":
|
||||
version "3.5.0"
|
||||
@@ -1053,10 +1053,10 @@ binary-extensions@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
|
||||
integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==
|
||||
|
||||
bootstrap@5.3.5:
|
||||
version "5.3.5"
|
||||
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.3.5.tgz#be42cfe0d580e97ee1abb7d38ce94f5c393c9bb6"
|
||||
integrity sha512-ct1CHKtiobRimyGzmsSldEtM03E8fcEX4Tb3dGXz1V8faRwM50+vfHwTzOxB3IlKO7m+9vTH3s/3C6T2EAPeTA==
|
||||
bootstrap@5.3.6:
|
||||
version "5.3.6"
|
||||
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.3.6.tgz#fbd91ebaff093f5b191a1c01a8c866d24f9fa6e1"
|
||||
integrity sha512-jX0GAcRzvdwISuvArXn3m7KZscWWFAf1MKBcnzaN02qWMb3jpMoUX4/qgeiGzqyIb4ojulRzs89UCUmGcFSzTA==
|
||||
|
||||
brace-expansion@^1.1.7:
|
||||
version "1.1.11"
|
||||
@@ -1903,10 +1903,10 @@ graphql@16.10.0:
|
||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.10.0.tgz#24c01ae0af6b11ea87bf55694429198aaa8e220c"
|
||||
integrity sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==
|
||||
|
||||
gridstack@12.1.1:
|
||||
version "12.1.1"
|
||||
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-12.1.1.tgz#623ea5b6560cc9509252db66fd7a529d70bd2d26"
|
||||
integrity sha512-wpfNUkzVBuHJftRRMRQDpH8DPIO5NBdfE0ioIIVoXFePBzqqVTpfgttSs5IJYqO4Uj5LfnJ2fjOmsFEBqpeSwg==
|
||||
gridstack@12.2.1:
|
||||
version "12.2.1"
|
||||
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-12.2.1.tgz#0e82e3d9d11e5229388d73bd57f8ef1a0e7059c4"
|
||||
integrity sha512-xU69tThmmVxgMHTuM/z3rIKiiGm0zW4tcB6yRcuwiOUUBiwb3tslzFOrUjWz+PwaxoAW+JChT4fqOLl+oKAxZA==
|
||||
|
||||
has-bigints@^1.0.1, has-bigints@^1.0.2:
|
||||
version "1.0.2"
|
||||
@@ -2514,10 +2514,10 @@ punycode@^2.1.0:
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
|
||||
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
|
||||
|
||||
query-string@9.1.1:
|
||||
version "9.1.1"
|
||||
resolved "https://registry.yarnpkg.com/query-string/-/query-string-9.1.1.tgz#dbfebb4196aeb2919915f2b2b81b91b965cf03a0"
|
||||
integrity sha512-MWkCOVIcJP9QSKU52Ngow6bsAWAPlPK2MludXvcrS2bGZSl+T1qX9MZvRIkqUIkGLJquMJHWfsT6eRqUpp4aWg==
|
||||
query-string@9.2.0:
|
||||
version "9.2.0"
|
||||
resolved "https://registry.yarnpkg.com/query-string/-/query-string-9.2.0.tgz#bf9909412689117865aac4e05c10422c4839828f"
|
||||
integrity sha512-YIRhrHujoQxhexwRLxfy3VSjOXmvZRd2nyw1PwL1UUqZ/ys1dEZd1+NSgXkne2l/4X/7OXkigEAuhTX0g/ivJQ==
|
||||
dependencies:
|
||||
decode-uri-component "^0.4.1"
|
||||
filter-obj "^5.1.0"
|
||||
@@ -2660,10 +2660,10 @@ safe-regex-test@^1.0.3:
|
||||
es-errors "^1.3.0"
|
||||
is-regex "^1.1.4"
|
||||
|
||||
sass@1.87.0:
|
||||
version "1.87.0"
|
||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.87.0.tgz#8cceb36fa63fb48a8d5d7f2f4c13b49c524b723e"
|
||||
integrity sha512-d0NoFH4v6SjEK7BoX810Jsrhj7IQSYHAHLi/iSpgqKc7LaIDshFRlSg5LOymf9FqQhxEHs2W5ZQXlvy0KD45Uw==
|
||||
sass@1.89.1:
|
||||
version "1.89.1"
|
||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.89.1.tgz#9281c52c85b4be54264d310fef63a811dfcfb9d9"
|
||||
integrity sha512-eMLLkl+qz7tx/0cJ9wI+w09GQ2zodTkcE/aVfywwdlRcI3EO19xGnbmJwg/JMIm+5MxVJ6outddLZ4Von4E++Q==
|
||||
dependencies:
|
||||
chokidar "^4.0.0"
|
||||
immutable "^5.0.2"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version: "4.3.0"
|
||||
version: "4.3.2"
|
||||
edition: "Community"
|
||||
published: "2025-05-01"
|
||||
published: "2025-06-05"
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
{% extends 'account/base.html' %}
|
||||
{% load helpers %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "User Profile" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Account Details" %}</h2>
|
||||
@@ -64,12 +62,7 @@
|
||||
{% if perms.core.view_objectchange %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<h2 class="card-header text-center">{% trans "Recent Activity" %}</h2>
|
||||
<div class="table-responsive">
|
||||
{% render_table changelog_table 'inc/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'users/inc/user_activity.html' with user=user table=changelog_table %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -28,6 +28,10 @@
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
|
||||
@@ -53,7 +53,6 @@
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Connection" %}</h2>
|
||||
<div class="card-body">
|
||||
{% if object.mark_connected %}
|
||||
<div class="card-body">
|
||||
<span class="text-success"><i class="mdi mdi-check-bold"></i></span>
|
||||
|
||||
@@ -26,6 +26,12 @@
|
||||
<th scope="row">{% trans "Location" %}</th>
|
||||
<td>{% nested_tree object.location %}</td>
|
||||
</tr>
|
||||
{% if object.virtual_chassis %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "Virtual Chassis" %}</th>
|
||||
<td>{{ object.virtual_chassis|linkify }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "Rack" %}</th>
|
||||
<td class="d-flex justify-content-between align-items-start">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<i class="mdi mdi-alert"></i>
|
||||
<strong>{% trans "Unsupported PostgreSQL version" %}.</strong>
|
||||
{% blocktrans trimmed %}
|
||||
Ensure that PostgreSQL version 12 or later is in use. You can check this by connecting to the database using
|
||||
Ensure that PostgreSQL version 14 or later is in use. You can check this by connecting to the database using
|
||||
NetBox's credentials and issuing a query for <code>SELECT VERSION()</code>.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
@@ -63,11 +63,15 @@
|
||||
</h2>
|
||||
<pre class="card-body" id="rendered_config">{{ rendered_config }}</pre>
|
||||
</div>
|
||||
{% else %}
|
||||
{% elif error_message %}
|
||||
<div class="alert alert-warning">
|
||||
<h4 class="alert-title mb-1">{% trans "Error rendering template" %}</h4>
|
||||
{% trans error_message %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning">
|
||||
<h4 class="alert-title mb-1">{% trans "Template output is empty" %}</h4>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
|
||||
@@ -30,20 +30,24 @@
|
||||
<button type="button" data-bs-toggle="modal" title="{% trans "Configure Table" %}" data-bs-target="#{{ table_modal }}" class="btn">
|
||||
<i class="mdi mdi-cog"></i> {% trans "Configure Table" %}
|
||||
</button>
|
||||
<button type="button" class="btn dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<span class="visually-hidden">Toggle Dropdown</span>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
{% if table.config_params %}
|
||||
<a class="dropdown-item" href="{% url 'extras:tableconfig_add' %}?{{ table.config_params }}&return_url={{ request.path }}" id="table_save_link">Save</a>
|
||||
{% endif %}
|
||||
{% if table_configs %}
|
||||
<hr class="dropdown-divider">
|
||||
{% for config in table_configs %}
|
||||
<a class="dropdown-item" href="?tableconfig_id={{ config.pk }}">{{ config }}</a>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if table.config_params or table_configs %}
|
||||
<button type="button" class="btn dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<span class="visually-hidden">{% trans "Toggle Dropdown" %}</span>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
{% if table.config_params %}
|
||||
<a class="dropdown-item" href="{% url 'extras:tableconfig_add' %}?{{ table.config_params }}&return_url={{ request.path }}" id="table_save_link">Save</a>
|
||||
{% endif %}
|
||||
{% if table.config_params and table_configs %}
|
||||
<hr class="dropdown-divider">
|
||||
{% endif %}
|
||||
{% if table_configs %}
|
||||
{% for config in table_configs %}
|
||||
<a class="dropdown-item" href="?tableconfig_id={{ config.pk }}">{{ config }}</a>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
16
netbox/templates/users/inc/user_activity.html
Normal file
16
netbox/templates/users/inc/user_activity.html
Normal file
@@ -0,0 +1,16 @@
|
||||
{% load i18n %}
|
||||
{% load render_table from django_tables2 %}
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-header text-center">
|
||||
{% trans "Recent Activity" %}
|
||||
<div class="card-actions">
|
||||
<a href="{% url 'core:objectchange_list' %}?user_id={{ user.pk }}" class="btn btn-ghost-primary btn-sm">
|
||||
<i class="mdi mdi-arrow-right-thick" aria-hidden="true"></i> {% trans "View All" %}
|
||||
</a>
|
||||
</div>
|
||||
</h2>
|
||||
<div class="table-responsive">
|
||||
{% render_table table 'inc/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user