mirror of
https://github.com/netbox-community/netbox.git
synced 2026-02-04 06:16:23 -06:00
Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2a581da47 | ||
|
|
bec5ecf6a9 | ||
|
|
c98f55dbd2 | ||
|
|
359179fd4a | ||
|
|
c44e8606f7 | ||
|
|
8e620ef325 | ||
|
|
905d17294a | ||
|
|
44e5a4c177 | ||
|
|
1526e437f1 | ||
|
|
0b507eb207 | ||
|
|
5a36e79215 | ||
|
|
2a0f26623b | ||
|
|
1a603981b2 | ||
|
|
245495b2fe | ||
|
|
8d3eb69055 | ||
|
|
7e3b60f194 | ||
|
|
5338c842b8 | ||
|
|
9186b0edaa | ||
|
|
d883be9e56 | ||
|
|
6fc7fa6c64 | ||
|
|
3a33df0e43 | ||
|
|
433f46746e | ||
|
|
8f5f91fcfe | ||
|
|
1a2175127e | ||
|
|
e859807d1d | ||
|
|
a8c997ff29 | ||
|
|
4a28ab98f4 | ||
|
|
3636d55017 | ||
|
|
aa69e96818 | ||
|
|
1745d2ae93 | ||
|
|
e097a848dc | ||
|
|
595be6dcd4 | ||
|
|
a9e50238eb | ||
|
|
de19447317 | ||
|
|
f195af206b | ||
|
|
cedbeb7b19 | ||
|
|
a45b6b170d | ||
|
|
b0ac55ed6a | ||
|
|
91ab818411 | ||
|
|
62b9367ae3 | ||
|
|
0c091aa80e | ||
|
|
94836e5a37 | ||
|
|
c92912ff03 | ||
|
|
ef0bc18095 | ||
|
|
99f727e685 | ||
|
|
6a5aced4bc | ||
|
|
46f9a12a87 | ||
|
|
42ecf3cac0 | ||
|
|
af8e53d8fb | ||
|
|
be1a008216 | ||
|
|
c4c3518bb4 | ||
|
|
5a1282e326 | ||
|
|
cb13eb277f | ||
|
|
24642be351 | ||
|
|
89af9efd85 | ||
|
|
99d678502f | ||
|
|
e6300ee06d |
@@ -3,29 +3,41 @@
|
|||||||
NetBox includes a Python management shell within which objects can be directly queried, created, modified, and deleted. To enter the shell, run the following command:
|
NetBox includes a Python management shell within which objects can be directly queried, created, modified, and deleted. To enter the shell, run the following command:
|
||||||
|
|
||||||
```
|
```
|
||||||
./manage.py nbshell
|
cd /opt/netbox
|
||||||
|
source /opt/netbox/venv/bin/activate
|
||||||
|
python3 netbox/manage.py nbshell
|
||||||
```
|
```
|
||||||
|
|
||||||
This will launch a lightly customized version of [the built-in Django shell](https://docs.djangoproject.com/en/stable/ref/django-admin/#shell) with all relevant NetBox models pre-loaded. (If desired, the stock Django shell is also available by executing `./manage.py shell`.)
|
This will launch a lightly customized version of [the built-in Django shell](https://docs.djangoproject.com/en/stable/ref/django-admin/#shell) with all relevant NetBox models preloaded. (If desired, the stock Django shell is also available by executing `./manage.py shell`.)
|
||||||
|
|
||||||
```
|
```
|
||||||
$ ./manage.py nbshell
|
(venv) $ python3 netbox/manage.py nbshell
|
||||||
### NetBox interactive shell (localhost)
|
### NetBox interactive shell (localhost)
|
||||||
### Python 3.7.10 | Django 3.2.5 | NetBox 3.0
|
### Python v3.12.3 | Django v5.2.10 | NetBox Community v4.5.1
|
||||||
### lsmodels() will show available models. Use help(<model>) for more info.
|
### lsapps() & lsmodels() will show available models. Use help(<model>) for more info.
|
||||||
```
|
```
|
||||||
|
|
||||||
The function `lsmodels()` will print a list of all available NetBox models:
|
The function `lsmodels()` will print a list of all available NetBox models:
|
||||||
|
|
||||||
```
|
```
|
||||||
>>> lsmodels()
|
>>> lsmodels()
|
||||||
DCIM:
|
|
||||||
ConsolePort
|
|
||||||
ConsolePortTemplate
|
|
||||||
ConsoleServerPort
|
|
||||||
ConsoleServerPortTemplate
|
|
||||||
Device
|
|
||||||
...
|
...
|
||||||
|
DCIM:
|
||||||
|
dcim.Cable
|
||||||
|
dcim.CableTermination
|
||||||
|
dcim.ConsolePort
|
||||||
|
dcim.ConsolePortTemplate
|
||||||
|
dcim.ConsoleServerPort
|
||||||
|
dcim.ConsoleServerPortTemplate
|
||||||
|
dcim.Device
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
To exit the NetBox shell, type `exit()` or press `Ctrl+D`.
|
||||||
|
|
||||||
|
```
|
||||||
|
>>> exit()
|
||||||
|
(venv) $
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
@@ -114,7 +126,7 @@ Reverse relationships can be traversed as well. For example, the following will
|
|||||||
>>> Device.objects.filter(interfaces__name="em0")
|
>>> Device.objects.filter(interfaces__name="em0")
|
||||||
```
|
```
|
||||||
|
|
||||||
Character fields can be filtered against partial matches using the `contains` or `icontains` field lookup (the later of which is case-insensitive).
|
Character fields can be filtered against partial matches using the `contains` or `icontains` field lookup (the latter of which is case-insensitive).
|
||||||
|
|
||||||
```
|
```
|
||||||
>>> Device.objects.filter(name__icontains="testdevice")
|
>>> Device.objects.filter(name__icontains="testdevice")
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ This is a mapping of models to [custom validators](../customization/custom-valid
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
CUSTOM_VALIDATORS = {
|
CUSTOM_VALIDATORS = {
|
||||||
"dcim.site": [
|
"dcim.Site": [
|
||||||
{
|
{
|
||||||
"name": {
|
"name": {
|
||||||
"min_length": 5,
|
"min_length": 5,
|
||||||
@@ -17,12 +17,15 @@ CUSTOM_VALIDATORS = {
|
|||||||
},
|
},
|
||||||
"my_plugin.validators.Validator1"
|
"my_plugin.validators.Validator1"
|
||||||
],
|
],
|
||||||
"dcim.device": [
|
"dcim.Device": [
|
||||||
"my_plugin.validators.Validator1"
|
"my_plugin.validators.Validator1"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
!!! info "Case-Insensitive Model Names"
|
||||||
|
Model identifiers are case-insensitive. Both `dcim.site` and `dcim.Site` are valid and equivalent.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## FIELD_CHOICES
|
## FIELD_CHOICES
|
||||||
@@ -53,6 +56,9 @@ FIELD_CHOICES = {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
!!! info "Case-Insensitive Field Identifiers"
|
||||||
|
Field identifiers are case-insensitive. Both `dcim.Site.status` and `dcim.site.status` are valid and equivalent.
|
||||||
|
|
||||||
The following model fields support configurable choices:
|
The following model fields support configurable choices:
|
||||||
|
|
||||||
* `circuits.Circuit.status`
|
* `circuits.Circuit.status`
|
||||||
@@ -98,7 +104,7 @@ This is a mapping of models to [custom validators](../customization/custom-valid
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
PROTECTION_RULES = {
|
PROTECTION_RULES = {
|
||||||
"dcim.site": [
|
"dcim.Site": [
|
||||||
{
|
{
|
||||||
"status": {
|
"status": {
|
||||||
"eq": "decommissioning"
|
"eq": "decommissioning"
|
||||||
@@ -108,3 +114,6 @@ PROTECTION_RULES = {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
!!! info "Case-Insensitive Model Names"
|
||||||
|
Model identifiers are case-insensitive. Both `dcim.site` and `dcim.Site` are valid and equivalent.
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ Some configuration parameters may alternatively be defined either in `configurat
|
|||||||
|
|
||||||
## Dynamic Configuration Parameters
|
## Dynamic Configuration Parameters
|
||||||
|
|
||||||
Some configuration parameters are primarily controlled via NetBox's admin interface (under Admin > Extras > Configuration Revisions). These are noted where applicable in the documentation. These settings may also be overridden in `configuration.py` to prevent them from being modified via the UI. A complete list of supported parameters is provided below:
|
Some configuration parameters are primarily controlled via NetBox's admin interface (under Admin > System > Configuration History). These are noted where applicable in the documentation. These settings may also be overridden in `configuration.py` to prevent them from being modified via the UI. A complete list of supported parameters is provided below:
|
||||||
|
|
||||||
* [`ALLOWED_URL_SCHEMES`](./security.md#allowed_url_schemes)
|
* [`ALLOWED_URL_SCHEMES`](./security.md#allowed_url_schemes)
|
||||||
* [`BANNER_BOTTOM`](./miscellaneous.md#banner_bottom)
|
* [`BANNER_BOTTOM`](./miscellaneous.md#banner_bottom)
|
||||||
|
|||||||
@@ -51,14 +51,14 @@ You can verify that authentication works by executing the `psql` command and pas
|
|||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
$ psql --username netbox --password --host localhost netbox
|
$ psql --username netbox --password --host localhost netbox
|
||||||
Password for user netbox:
|
Password:
|
||||||
psql (12.5 (Ubuntu 12.5-0ubuntu0.20.04.1))
|
psql (16.11 (Ubuntu 16.11-0ubuntu0.24.04.1))
|
||||||
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
|
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
|
||||||
Type "help" for help.
|
Type "help" for help.
|
||||||
|
|
||||||
netbox=> \conninfo
|
netbox=> \conninfo
|
||||||
You are connected to database "netbox" as user "netbox" on host "localhost" (address "127.0.0.1") at port "5432".
|
You are connected to database "netbox" as user "netbox" on host "localhost" (address "127.0.0.1") at port "5432".
|
||||||
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
|
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
|
||||||
netbox=> \q
|
netbox=> \q
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ sudo ln -s /opt/netbox-X.Y.Z/ /opt/netbox
|
|||||||
```
|
```
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
It is recommended to install NetBox in a directory named for its version number. For example, NetBox v3.0.0 would be installed into `/opt/netbox-3.0.0`, and a symlink from `/opt/netbox/` would point to this location. (You can verify this configuration with the command `ls -l /opt | grep netbox`.) This allows for future releases to be installed in parallel without interrupting the current installation. When changing to the new release, only the symlink needs to be updated.
|
It is recommended to install NetBox in a directory named for its version number. For example, NetBox v4.0.0 would be installed into `/opt/netbox-4.0.0`, and a symlink from `/opt/netbox/` would point to this location. (You can verify this configuration with the command `ls -l /opt | grep netbox`.) This allows for future releases to be installed in parallel without interrupting the current installation. When changing to the new release, only the symlink needs to be updated.
|
||||||
|
|
||||||
### Option B: Clone the Git Repository
|
### Option B: Clone the Git Repository
|
||||||
|
|
||||||
@@ -63,12 +63,12 @@ This command should generate output similar to the following:
|
|||||||
|
|
||||||
```
|
```
|
||||||
Cloning into '.'...
|
Cloning into '.'...
|
||||||
remote: Enumerating objects: 996, done.
|
remote: Enumerating objects: 148317, done.
|
||||||
remote: Counting objects: 100% (996/996), done.
|
remote: Counting objects: 100% (183/183), done.
|
||||||
remote: Compressing objects: 100% (935/935), done.
|
remote: Compressing objects: 100% (115/115), done.
|
||||||
remote: Total 996 (delta 148), reused 386 (delta 34), pack-reused 0
|
remote: Total 148317 (delta 127), reused 68 (delta 68), pack-reused 148134 (from 3)
|
||||||
Receiving objects: 100% (996/996), 4.26 MiB | 9.81 MiB/s, done.
|
Receiving objects: 100% (148317/148317), 165.12 MiB | 28.71 MiB/s, done.
|
||||||
Resolving deltas: 100% (148/148), done.
|
Resolving deltas: 100% (116428/116428), done.
|
||||||
```
|
```
|
||||||
|
|
||||||
Finally, check out the tag for the desired release. You can find these on our [releases page](https://github.com/netbox-community/netbox/releases). Replace `vX.Y.Z` with your selected release tag below.
|
Finally, check out the tag for the desired release. You can find these on our [releases page](https://github.com/netbox-community/netbox/releases). Replace `vX.Y.Z` with your selected release tag below.
|
||||||
@@ -102,7 +102,8 @@ sudo cp configuration_example.py configuration.py
|
|||||||
Open `configuration.py` with your preferred editor to begin configuring NetBox. NetBox offers [many configuration parameters](../configuration/index.md), but only the following four are required for new installations:
|
Open `configuration.py` with your preferred editor to begin configuring NetBox. NetBox offers [many configuration parameters](../configuration/index.md), but only the following four are required for new installations:
|
||||||
|
|
||||||
* `ALLOWED_HOSTS`
|
* `ALLOWED_HOSTS`
|
||||||
* `DATABASES` (or `DATABASE`)
|
* `API_TOKEN_PEPPERS`
|
||||||
|
* `DATABASES`
|
||||||
* `REDIS`
|
* `REDIS`
|
||||||
* `SECRET_KEY`
|
* `SECRET_KEY`
|
||||||
|
|
||||||
@@ -158,7 +159,7 @@ DATABASES = {
|
|||||||
|
|
||||||
### REDIS
|
### REDIS
|
||||||
|
|
||||||
Redis is a in-memory key-value store used by NetBox for caching and background task queuing. Redis typically requires minimal configuration; the values below should suffice for most installations. See the [configuration documentation](../configuration/required-parameters.md#redis) for more detail on individual parameters.
|
Redis is an in-memory key-value store used by NetBox for caching and background task queuing. Redis typically requires minimal configuration; the values below should suffice for most installations. See the [configuration documentation](../configuration/required-parameters.md#redis) for more detail on individual parameters.
|
||||||
|
|
||||||
Note that NetBox requires the specification of two separate Redis databases: `tasks` and `caching`. These may both be provided by the same Redis service, however each should have a unique numeric database ID.
|
Note that NetBox requires the specification of two separate Redis databases: `tasks` and `caching`. These may both be provided by the same Redis service, however each should have a unique numeric database ID.
|
||||||
|
|
||||||
@@ -252,7 +253,7 @@ Once NetBox has been configured, we're ready to proceed with the actual installa
|
|||||||
sudo /opt/netbox/upgrade.sh
|
sudo /opt/netbox/upgrade.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that **Python 3.12 or later is required** for NetBox v4.5 and later releases. If the default Python installation on your server is set to a lesser version, pass the path to the supported installation as an environment variable named `PYTHON`. (Note that the environment variable must be passed _after_ the `sudo` command.)
|
Note that **Python 3.12 or later is required** for NetBox v4.5 and later releases. If the default Python installation on your server is set to a lesser version, pass the path to the supported installation as an environment variable named `PYTHON`. (Note that the environment variable must be passed _after_ the `sudo` command.)
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
sudo PYTHON=/usr/bin/python3.12 /opt/netbox/upgrade.sh
|
sudo PYTHON=/usr/bin/python3.12 /opt/netbox/upgrade.sh
|
||||||
@@ -295,13 +296,12 @@ python3 manage.py runserver 0.0.0.0:8000 --insecure
|
|||||||
If successful, you should see output similar to the following:
|
If successful, you should see output similar to the following:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
Watching for file changes with StatReloader
|
|
||||||
Performing system checks...
|
Performing system checks...
|
||||||
|
|
||||||
System check identified no issues (0 silenced).
|
System check identified no issues (0 silenced).
|
||||||
August 30, 2021 - 18:02:23
|
January 26, 2026 - 17:00:00
|
||||||
Django version 3.2.6, using settings 'netbox.settings'
|
Django version 5.2.10, using settings 'netbox.settings'
|
||||||
Starting development server at http://127.0.0.1:8000/
|
Starting development server at http://0.0.0.0:8000/
|
||||||
Quit the server with CONTROL-C.
|
Quit the server with CONTROL-C.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -43,16 +43,22 @@ You should see output similar to the following:
|
|||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
● netbox.service - NetBox WSGI Service
|
● netbox.service - NetBox WSGI Service
|
||||||
Loaded: loaded (/etc/systemd/system/netbox.service; enabled; vendor preset: enabled)
|
Loaded: loaded (/etc/systemd/system/netbox.service; enabled; preset: enabled)
|
||||||
Active: active (running) since Mon 2021-08-30 04:02:36 UTC; 14h ago
|
Active: active (running) since Mon 2026-01-26 11:00:00 CST; 7s ago
|
||||||
Docs: https://docs.netbox.dev/
|
Docs: https://docs.netbox.dev/
|
||||||
Main PID: 1140492 (gunicorn)
|
Main PID: 7283 (gunicorn)
|
||||||
Tasks: 19 (limit: 4683)
|
Tasks: 6 (limit: 4545)
|
||||||
Memory: 666.2M
|
Memory: 556.1M (peak: 556.3M)
|
||||||
|
CPU: 3.387s
|
||||||
CGroup: /system.slice/netbox.service
|
CGroup: /system.slice/netbox.service
|
||||||
├─1140492 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /va>
|
├─7283 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/netbox>
|
||||||
├─1140513 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /va>
|
├─7285 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/netbox>
|
||||||
├─1140514 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /va>
|
├─7286 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/netbox>
|
||||||
|
├─7287 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/netbox>
|
||||||
|
├─7288 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/netbox>
|
||||||
|
└─7289 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/netbox>
|
||||||
|
|
||||||
|
Jan 26 11:00:00 netbox systemd[1]: Started netbox.service - NetBox WSGI Service.
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
This documentation provides example configurations for both [nginx](https://www.nginx.com/resources/wiki/) and [Apache](https://httpd.apache.org/docs/current/), though any HTTP server which supports WSGI should be compatible.
|
This documentation provides example configurations for both [nginx](https://www.nginx.com/resources/wiki/) and [Apache](https://httpd.apache.org/docs/current/), though any HTTP server which supports WSGI should be compatible.
|
||||||
|
|
||||||
!!! info
|
!!! info
|
||||||
For the sake of brevity, only Ubuntu 20.04 instructions are provided here. These tasks are not unique to NetBox and should carry over to other distributions with minimal changes. Please consult your distribution's documentation for assistance if needed.
|
For the sake of brevity, only Ubuntu 24.04 instructions are provided here. These tasks are not unique to NetBox and should carry over to other distributions with minimal changes. Please consult your distribution's documentation for assistance if needed.
|
||||||
|
|
||||||
## Obtain an SSL Certificate
|
## Obtain an SSL Certificate
|
||||||
|
|
||||||
|
|||||||
@@ -12,12 +12,12 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
The installation instructions provided here have been tested to work on Ubuntu 22.04. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
|
The installation instructions provided here have been tested to work on Ubuntu 24.04. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
|
||||||
|
|
||||||
The following sections detail how to set up a new instance of NetBox:
|
The following sections detail how to set up a new instance of NetBox:
|
||||||
|
|
||||||
1. [PostgreSQL database](1-postgresql.md)
|
1. [PostgreSQL database](1-postgresql.md)
|
||||||
1. [Redis](2-redis.md)
|
2. [Redis](2-redis.md)
|
||||||
3. [NetBox components](3-netbox.md)
|
3. [NetBox components](3-netbox.md)
|
||||||
4. [Gunicorn](4a-gunicorn.md) or [uWSGI](4b-uwsgi.md)
|
4. [Gunicorn](4a-gunicorn.md) or [uWSGI](4b-uwsgi.md)
|
||||||
5. [HTTP server](5-http-server.md)
|
5. [HTTP server](5-http-server.md)
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ Download and extract the latest version:
|
|||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# Set $NEWVER to the NetBox version being installed
|
# Set $NEWVER to the NetBox version being installed
|
||||||
NEWVER=3.5.0
|
NEWVER=4.5.0
|
||||||
wget https://github.com/netbox-community/netbox/archive/v$NEWVER.tar.gz
|
wget https://github.com/netbox-community/netbox/archive/v$NEWVER.tar.gz
|
||||||
sudo tar -xzf v$NEWVER.tar.gz -C /opt
|
sudo tar -xzf v$NEWVER.tar.gz -C /opt
|
||||||
sudo ln -sfn /opt/netbox-$NEWVER/ /opt/netbox
|
sudo ln -sfn /opt/netbox-$NEWVER/ /opt/netbox
|
||||||
@@ -75,7 +75,7 @@ Copy `local_requirements.txt`, `configuration.py`, and `ldap_config.py` (if pres
|
|||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# Set $OLDVER to the NetBox version currently installed
|
# Set $OLDVER to the NetBox version currently installed
|
||||||
OLDVER=3.4.9
|
OLDVER=4.4.10
|
||||||
sudo cp /opt/netbox-$OLDVER/local_requirements.txt /opt/netbox/
|
sudo cp /opt/netbox-$OLDVER/local_requirements.txt /opt/netbox/
|
||||||
sudo cp /opt/netbox-$OLDVER/netbox/netbox/configuration.py /opt/netbox/netbox/netbox/
|
sudo cp /opt/netbox-$OLDVER/netbox/netbox/configuration.py /opt/netbox/netbox/netbox/
|
||||||
sudo cp /opt/netbox-$OLDVER/netbox/netbox/ldap_config.py /opt/netbox/netbox/netbox/
|
sudo cp /opt/netbox-$OLDVER/netbox/netbox/ldap_config.py /opt/netbox/netbox/netbox/
|
||||||
@@ -116,7 +116,7 @@ Check out the desired release by specifying its tag. For example:
|
|||||||
```
|
```
|
||||||
cd /opt/netbox && \
|
cd /opt/netbox && \
|
||||||
sudo git fetch --tags && \
|
sudo git fetch --tags && \
|
||||||
sudo git checkout v4.2.7
|
sudo git checkout v4.5.0
|
||||||
```
|
```
|
||||||
|
|
||||||
## 4. Run the Upgrade Script
|
## 4. Run the Upgrade Script
|
||||||
@@ -128,7 +128,7 @@ sudo ./upgrade.sh
|
|||||||
```
|
```
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
If the default version of Python is not at least 3.10, you'll need to pass the path to a supported Python version as an environment variable when calling the upgrade script. For example:
|
If the default version of Python is not **at least 3.12**, you'll need to pass the path to a supported Python version as an environment variable when calling the upgrade script. For example:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
sudo PYTHON=/usr/bin/python3.12 ./upgrade.sh
|
sudo PYTHON=/usr/bin/python3.12 ./upgrade.sh
|
||||||
|
|||||||
@@ -215,9 +215,51 @@ http://netbox/api/ipam/ip-addresses/ \
|
|||||||
|
|
||||||
If we wanted to assign this IP address to a virtual machine interface instead, we would have set `assigned_object_type` to `virtualization.vminterface` and updated the object ID appropriately.
|
If we wanted to assign this IP address to a virtual machine interface instead, we would have set `assigned_object_type` to `virtualization.vminterface` and updated the object ID appropriately.
|
||||||
|
|
||||||
### Brief Format
|
### Specifying Fields
|
||||||
|
|
||||||
Most API endpoints support an optional "brief" format, which returns only a minimal representation of each object in the response. This is useful when you need only a list of available objects without any related data, such as when populating a drop-down list in a form. As an example, the default (complete) format of a prefix looks like this:
|
A REST API response will include all available fields for the object type by default. If you wish to return only a subset of the available fields, you can append `?fields=` to the URL followed by a comma-separated list of field names. For example, the following request will return only the `id`, `name`, `status`, and `region` fields for each site in the response.
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/dcim/sites/?fields=id,name,status,region
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "DM-NYC",
|
||||||
|
"status": {
|
||||||
|
"value": "active",
|
||||||
|
"label": "Active"
|
||||||
|
},
|
||||||
|
"region": {
|
||||||
|
"id": 43,
|
||||||
|
"url": "http://netbox:8000/api/dcim/regions/43/",
|
||||||
|
"display": "New York",
|
||||||
|
"name": "New York",
|
||||||
|
"slug": "us-ny",
|
||||||
|
"description": "",
|
||||||
|
"site_count": 0,
|
||||||
|
"_depth": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Similarly, you can opt to omit only specific fields by passing the `omit` parameter:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/dcim/sites/?omit=circuit_count,device_count,virtualmachine_count
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! note "The `omit` parameter was introduced in NetBox v4.5.2."
|
||||||
|
|
||||||
|
Strategic use of the `fields` and `omit` parameters can drastically improve REST API performance, as the exclusion of fields which reference related objects reduces the number and complexity of underlying database queries needed to generate the response.
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
The `fields` and `omit` parameters should be considered mutually exclusive. If both are passed, `fields` takes precedence.
|
||||||
|
|
||||||
|
#### Brief Format
|
||||||
|
|
||||||
|
Most API endpoints support an optional "brief" format, which returns only a minimal representation of each object in the response. This is useful when you need only a list of available objects without any related data, such as when populating a drop-down list in a form. It's also more convenient than listing out individual fields via the `fields` or `omit` parameters. As an example, the default (complete) format of a prefix looks like this:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
GET /api/ipam/prefixes/13980/
|
GET /api/ipam/prefixes/13980/
|
||||||
@@ -270,10 +312,10 @@ GET /api/ipam/prefixes/13980/
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The brief format is much more terse:
|
The brief format includes only a few fields:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
GET /api/ipam/prefixes/13980/?brief=1
|
GET /api/ipam/prefixes/13980/?brief=true
|
||||||
```
|
```
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -34,9 +34,10 @@ __all__ = (
|
|||||||
class ProviderFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
class ProviderFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = Provider
|
model = Provider
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
|
||||||
FieldSet('asn_id', name=_('ASN')),
|
FieldSet('asn_id', name=_('ASN')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||||
)
|
)
|
||||||
region_id = DynamicModelMultipleChoiceField(
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
@@ -69,8 +70,9 @@ class ProviderFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
|||||||
class ProviderAccountFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
class ProviderAccountFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = ProviderAccount
|
model = ProviderAccount
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('provider_id', 'account', name=_('Attributes')),
|
FieldSet('provider_id', 'account', name=_('Attributes')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||||
)
|
)
|
||||||
provider_id = DynamicModelMultipleChoiceField(
|
provider_id = DynamicModelMultipleChoiceField(
|
||||||
@@ -88,8 +90,9 @@ class ProviderAccountFilterForm(ContactModelFilterForm, PrimaryModelFilterSetFor
|
|||||||
class ProviderNetworkFilterForm(PrimaryModelFilterSetForm):
|
class ProviderNetworkFilterForm(PrimaryModelFilterSetForm):
|
||||||
model = ProviderNetwork
|
model = ProviderNetwork
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('provider_id', 'service_id', name=_('Attributes')),
|
FieldSet('provider_id', 'service_id', name=_('Attributes')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
provider_id = DynamicModelMultipleChoiceField(
|
provider_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Provider.objects.all(),
|
queryset=Provider.objects.all(),
|
||||||
@@ -107,8 +110,9 @@ class ProviderNetworkFilterForm(PrimaryModelFilterSetForm):
|
|||||||
class CircuitTypeFilterForm(OrganizationalModelFilterSetForm):
|
class CircuitTypeFilterForm(OrganizationalModelFilterSetForm):
|
||||||
model = CircuitType
|
model = CircuitType
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('color', name=_('Attributes')),
|
FieldSet('color', name=_('Attributes')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
@@ -121,7 +125,7 @@ class CircuitTypeFilterForm(OrganizationalModelFilterSetForm):
|
|||||||
class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm):
|
class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = Circuit
|
model = Circuit
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
|
FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'type_id', 'status', 'install_date', 'termination_date', 'commit_rate', 'distance', 'distance_unit',
|
'type_id', 'status', 'install_date', 'termination_date', 'commit_rate', 'distance', 'distance_unit',
|
||||||
@@ -129,6 +133,7 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelF
|
|||||||
),
|
),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||||
)
|
)
|
||||||
selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'provider_id', 'provider_network_id')
|
selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'provider_id', 'provider_network_id')
|
||||||
@@ -274,8 +279,9 @@ class CircuitTerminationFilterForm(NetBoxModelFilterSetForm):
|
|||||||
class CircuitGroupFilterForm(TenancyFilterForm, OrganizationalModelFilterSetForm):
|
class CircuitGroupFilterForm(TenancyFilterForm, OrganizationalModelFilterSetForm):
|
||||||
model = CircuitGroup
|
model = CircuitGroup
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
@@ -312,8 +318,9 @@ class CircuitGroupAssignmentFilterForm(NetBoxModelFilterSetForm):
|
|||||||
class VirtualCircuitTypeFilterForm(OrganizationalModelFilterSetForm):
|
class VirtualCircuitTypeFilterForm(OrganizationalModelFilterSetForm):
|
||||||
model = VirtualCircuitType
|
model = VirtualCircuitType
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('color', name=_('Attributes')),
|
FieldSet('color', name=_('Attributes')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
@@ -326,10 +333,11 @@ class VirtualCircuitTypeFilterForm(OrganizationalModelFilterSetForm):
|
|||||||
class VirtualCircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm):
|
class VirtualCircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = VirtualCircuit
|
model = VirtualCircuit
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
|
FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
|
||||||
FieldSet('type_id', 'status', name=_('Attributes')),
|
FieldSet('type_id', 'status', name=_('Attributes')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
selector_fields = ('filter_id', 'q', 'provider_id', 'provider_network_id')
|
selector_fields = ('filter_id', 'q', 'provider_id', 'provider_network_id')
|
||||||
provider_id = DynamicModelMultipleChoiceField(
|
provider_id = DynamicModelMultipleChoiceField(
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ class JobSerializer(BaseModelSerializer):
|
|||||||
model = Job
|
model = Job
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'object_type', 'object_id', 'object', 'name', 'status', 'created',
|
'id', 'url', 'display_url', 'display', 'object_type', 'object_id', 'object', 'name', 'status', 'created',
|
||||||
'scheduled', 'interval', 'started', 'completed', 'user', 'data', 'error', 'job_id', 'log_entries',
|
'scheduled', 'interval', 'started', 'completed', 'user', 'data', 'error', 'job_id', 'queue_name',
|
||||||
|
'log_entries',
|
||||||
]
|
]
|
||||||
brief_fields = ('url', 'created', 'completed', 'user', 'status')
|
brief_fields = ('url', 'created', 'completed', 'user', 'status')
|
||||||
|
|
||||||
|
|||||||
@@ -129,10 +129,14 @@ class JobFilterSet(BaseFilterSet):
|
|||||||
choices=JobStatusChoices,
|
choices=JobStatusChoices,
|
||||||
null_value=None
|
null_value=None
|
||||||
)
|
)
|
||||||
|
queue_name = django_filters.CharFilter()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Job
|
model = Job
|
||||||
fields = ('id', 'object_type', 'object_type_id', 'object_id', 'name', 'interval', 'status', 'user', 'job_id')
|
fields = (
|
||||||
|
'id', 'object_type', 'object_type_id', 'object_id', 'name', 'interval', 'status', 'user', 'job_id',
|
||||||
|
'queue_name',
|
||||||
|
)
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
|
|||||||
@@ -26,8 +26,9 @@ __all__ = (
|
|||||||
class DataSourceFilterForm(PrimaryModelFilterSetForm):
|
class DataSourceFilterForm(PrimaryModelFilterSetForm):
|
||||||
model = DataSource
|
model = DataSource
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('type', 'status', 'enabled', 'sync_interval', name=_('Data Source')),
|
FieldSet('type', 'status', 'enabled', 'sync_interval', name=_('Data Source')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
type = forms.MultipleChoiceField(
|
type = forms.MultipleChoiceField(
|
||||||
label=_('Type'),
|
label=_('Type'),
|
||||||
@@ -71,7 +72,7 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
model = Job
|
model = Job
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id'),
|
FieldSet('q', 'filter_id'),
|
||||||
FieldSet('object_type_id', 'status', name=_('Attributes')),
|
FieldSet('object_type_id', 'status', 'queue_name', name=_('Attributes')),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'created__before', 'created__after', 'scheduled__before', 'scheduled__after', 'started__before',
|
'created__before', 'created__after', 'scheduled__before', 'scheduled__after', 'started__before',
|
||||||
'started__after', 'completed__before', 'completed__after', 'user', name=_('Creation')
|
'started__after', 'completed__before', 'completed__after', 'user', name=_('Creation')
|
||||||
@@ -87,6 +88,10 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
choices=JobStatusChoices,
|
choices=JobStatusChoices,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
queue_name = forms.CharField(
|
||||||
|
label=_('Queue'),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
created__after = forms.DateTimeField(
|
created__after = forms.DateTimeField(
|
||||||
label=_('Created after'),
|
label=_('Created after'),
|
||||||
required=False,
|
required=False,
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.9 on 2026-01-27 00:39
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0020_owner'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='job',
|
||||||
|
name='queue_name',
|
||||||
|
field=models.CharField(blank=True, max_length=100),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -112,6 +112,12 @@ class Job(models.Model):
|
|||||||
verbose_name=_('job ID'),
|
verbose_name=_('job ID'),
|
||||||
unique=True
|
unique=True
|
||||||
)
|
)
|
||||||
|
queue_name = models.CharField(
|
||||||
|
verbose_name=_('queue name'),
|
||||||
|
max_length=100,
|
||||||
|
blank=True,
|
||||||
|
help_text=_('Name of the queue in which this job was enqueued')
|
||||||
|
)
|
||||||
log_entries = ArrayField(
|
log_entries = ArrayField(
|
||||||
verbose_name=_('log entries'),
|
verbose_name=_('log entries'),
|
||||||
base_field=models.JSONField(
|
base_field=models.JSONField(
|
||||||
@@ -179,11 +185,15 @@ class Job(models.Model):
|
|||||||
return f"{int(minutes)} minutes, {seconds:.2f} seconds"
|
return f"{int(minutes)} minutes, {seconds:.2f} seconds"
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
|
# Use the stored queue name, or fall back to get_queue_for_model for legacy jobs
|
||||||
|
rq_queue_name = self.queue_name or get_queue_for_model(self.object_type.model if self.object_type else None)
|
||||||
|
rq_job_id = str(self.job_id)
|
||||||
|
|
||||||
super().delete(*args, **kwargs)
|
super().delete(*args, **kwargs)
|
||||||
|
|
||||||
rq_queue_name = get_queue_for_model(self.object_type.model if self.object_type else None)
|
# Cancel the RQ job using the stored queue name
|
||||||
queue = django_rq.get_queue(rq_queue_name)
|
queue = django_rq.get_queue(rq_queue_name)
|
||||||
job = queue.fetch_job(str(self.job_id))
|
job = queue.fetch_job(rq_job_id)
|
||||||
|
|
||||||
if job:
|
if job:
|
||||||
try:
|
try:
|
||||||
@@ -288,7 +298,8 @@ class Job(models.Model):
|
|||||||
scheduled=schedule_at,
|
scheduled=schedule_at,
|
||||||
interval=interval,
|
interval=interval,
|
||||||
user=user,
|
user=user,
|
||||||
job_id=uuid.uuid4()
|
job_id=uuid.uuid4(),
|
||||||
|
queue_name=rq_queue_name
|
||||||
)
|
)
|
||||||
job.full_clean()
|
job.full_clean()
|
||||||
job.save()
|
job.save()
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from django.db import connection, models
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
from netbox.context import query_cache
|
||||||
from netbox.plugins import PluginConfig
|
from netbox.plugins import PluginConfig
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
from utilities.string import title
|
from utilities.string import title
|
||||||
@@ -70,6 +71,12 @@ class ObjectTypeManager(models.Manager):
|
|||||||
"""
|
"""
|
||||||
from netbox.models.features import get_model_features, model_is_public
|
from netbox.models.features import get_model_features, model_is_public
|
||||||
|
|
||||||
|
# Check the request cache before hitting the database
|
||||||
|
cache = query_cache.get()
|
||||||
|
if cache is not None:
|
||||||
|
if ot := cache['object_types'].get((model._meta.model, for_concrete_model)):
|
||||||
|
return ot
|
||||||
|
|
||||||
# TODO: Remove this in NetBox v5.0
|
# TODO: Remove this in NetBox v5.0
|
||||||
# If the ObjectType table has not yet been provisioned (e.g. because we're in a pre-v4.4 migration),
|
# If the ObjectType table has not yet been provisioned (e.g. because we're in a pre-v4.4 migration),
|
||||||
# fall back to ContentType.
|
# fall back to ContentType.
|
||||||
@@ -96,6 +103,10 @@ class ObjectTypeManager(models.Manager):
|
|||||||
features=get_model_features(model),
|
features=get_model_features(model),
|
||||||
)[0]
|
)[0]
|
||||||
|
|
||||||
|
# Populate the request cache to avoid redundant lookups
|
||||||
|
if cache is not None:
|
||||||
|
cache['object_types'][(model._meta.model, for_concrete_model)] = ot
|
||||||
|
|
||||||
return ot
|
return ot
|
||||||
|
|
||||||
def get_for_models(self, *models, for_concrete_models=True):
|
def get_for_models(self, *models, for_concrete_models=True):
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from extras.events import enqueue_event
|
|||||||
from extras.models import Tag
|
from extras.models import Tag
|
||||||
from extras.utils import run_validators
|
from extras.utils import run_validators
|
||||||
from netbox.config import get_config
|
from netbox.config import get_config
|
||||||
|
from utilities.data import get_config_value_ci
|
||||||
from netbox.context import current_request, events_queue
|
from netbox.context import current_request, events_queue
|
||||||
from netbox.models.features import ChangeLoggingMixin, get_model_features, model_is_public
|
from netbox.models.features import ChangeLoggingMixin, get_model_features, model_is_public
|
||||||
from utilities.exceptions import AbortRequest
|
from utilities.exceptions import AbortRequest
|
||||||
@@ -168,7 +169,7 @@ def handle_deleted_object(sender, instance, **kwargs):
|
|||||||
# to queueing any events for the object being deleted, in case a validation error is
|
# to queueing any events for the object being deleted, in case a validation error is
|
||||||
# raised, causing the deletion to fail.
|
# raised, causing the deletion to fail.
|
||||||
model_name = f'{sender._meta.app_label}.{sender._meta.model_name}'
|
model_name = f'{sender._meta.app_label}.{sender._meta.model_name}'
|
||||||
validators = get_config().PROTECTION_RULES.get(model_name, [])
|
validators = get_config_value_ci(get_config().PROTECTION_RULES, model_name, default=[])
|
||||||
try:
|
try:
|
||||||
run_validators(instance, validators)
|
run_validators(instance, validators)
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ class JobTable(NetBoxTable):
|
|||||||
completed = columns.DateTimeColumn(
|
completed = columns.DateTimeColumn(
|
||||||
verbose_name=_('Completed'),
|
verbose_name=_('Completed'),
|
||||||
)
|
)
|
||||||
|
queue_name = tables.Column(
|
||||||
|
verbose_name=_('Queue'),
|
||||||
|
)
|
||||||
log_entries = tables.Column(
|
log_entries = tables.Column(
|
||||||
verbose_name=_('Log Entries'),
|
verbose_name=_('Log Entries'),
|
||||||
)
|
)
|
||||||
@@ -53,7 +56,7 @@ class JobTable(NetBoxTable):
|
|||||||
model = Job
|
model = Job
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'object_type', 'object', 'name', 'status', 'created', 'scheduled', 'interval', 'started',
|
'pk', 'id', 'object_type', 'object', 'name', 'status', 'created', 'scheduled', 'interval', 'started',
|
||||||
'completed', 'user', 'error', 'job_id',
|
'completed', 'user', 'queue_name', 'log_entries', 'error', 'job_id',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'id', 'object_type', 'object', 'name', 'status', 'created', 'started', 'completed', 'user',
|
'pk', 'id', 'object_type', 'object', 'name', 'status', 'created', 'started', 'completed', 'user',
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from core.models import DataSource, ObjectType
|
from core.models import DataSource, Job, ObjectType
|
||||||
from core.choices import ObjectChangeActionChoices
|
from core.choices import ObjectChangeActionChoices
|
||||||
from dcim.models import Site, Location, Device
|
from dcim.models import Site, Location, Device
|
||||||
from netbox.constants import CENSOR_TOKEN, CENSOR_TOKEN_CHANGED
|
from netbox.constants import CENSOR_TOKEN, CENSOR_TOKEN_CHANGED
|
||||||
@@ -200,3 +202,38 @@ class ObjectTypeTest(TestCase):
|
|||||||
bookmarks_ots = ObjectType.objects.with_feature('bookmarks')
|
bookmarks_ots = ObjectType.objects.with_feature('bookmarks')
|
||||||
self.assertIn(ObjectType.objects.get_by_natural_key('dcim', 'site'), bookmarks_ots)
|
self.assertIn(ObjectType.objects.get_by_natural_key('dcim', 'site'), bookmarks_ots)
|
||||||
self.assertNotIn(ObjectType.objects.get_by_natural_key('dcim', 'cabletermination'), bookmarks_ots)
|
self.assertNotIn(ObjectType.objects.get_by_natural_key('dcim', 'cabletermination'), bookmarks_ots)
|
||||||
|
|
||||||
|
|
||||||
|
class JobTest(TestCase):
|
||||||
|
|
||||||
|
@patch('core.models.jobs.django_rq.get_queue')
|
||||||
|
def test_delete_cancels_job_from_correct_queue(self, mock_get_queue):
|
||||||
|
"""
|
||||||
|
Test that when a job is deleted, it's canceled from the correct queue.
|
||||||
|
"""
|
||||||
|
mock_queue = MagicMock()
|
||||||
|
mock_rq_job = MagicMock()
|
||||||
|
mock_queue.fetch_job.return_value = mock_rq_job
|
||||||
|
mock_get_queue.return_value = mock_queue
|
||||||
|
|
||||||
|
def dummy_func(**kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Enqueue a job with a custom queue name
|
||||||
|
custom_queue = 'my_custom_queue'
|
||||||
|
job = Job.enqueue(
|
||||||
|
func=dummy_func,
|
||||||
|
name='Test Job',
|
||||||
|
queue_name=custom_queue
|
||||||
|
)
|
||||||
|
|
||||||
|
# Reset mock to clear enqueue call
|
||||||
|
mock_get_queue.reset_mock()
|
||||||
|
|
||||||
|
# Delete the job
|
||||||
|
job.delete()
|
||||||
|
|
||||||
|
# Verify the correct queue was used for cancellation
|
||||||
|
mock_get_queue.assert_called_with(custom_queue)
|
||||||
|
mock_queue.fetch_job.assert_called_with(str(job.job_id))
|
||||||
|
mock_rq_job.cancel.assert_called_once()
|
||||||
|
|||||||
@@ -12,11 +12,12 @@ from netbox.forms import (
|
|||||||
NestedGroupModelFilterSetForm, NetBoxModelFilterSetForm, OrganizationalModelFilterSetForm,
|
NestedGroupModelFilterSetForm, NetBoxModelFilterSetForm, OrganizationalModelFilterSetForm,
|
||||||
PrimaryModelFilterSetForm,
|
PrimaryModelFilterSetForm,
|
||||||
)
|
)
|
||||||
|
from netbox.forms.mixins import OwnerFilterMixin
|
||||||
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
|
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from users.models import Owner, User
|
from users.models import User
|
||||||
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
|
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
|
||||||
from utilities.forms.fields import ColorField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField
|
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
|
||||||
from utilities.forms.rendering import FieldSet
|
from utilities.forms.rendering import FieldSet
|
||||||
from utilities.forms.widgets import NumberWithOptions
|
from utilities.forms.widgets import NumberWithOptions
|
||||||
from virtualization.models import Cluster, ClusterGroup, VirtualMachine
|
from virtualization.models import Cluster, ClusterGroup, VirtualMachine
|
||||||
@@ -70,11 +71,11 @@ __all__ = (
|
|||||||
'SiteFilterForm',
|
'SiteFilterForm',
|
||||||
'SiteGroupFilterForm',
|
'SiteGroupFilterForm',
|
||||||
'VirtualChassisFilterForm',
|
'VirtualChassisFilterForm',
|
||||||
'VirtualDeviceContextFilterForm'
|
'VirtualDeviceContextFilterForm',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
|
class DeviceComponentFilterForm(OwnerFilterMixin, NetBoxModelFilterSetForm):
|
||||||
name = forms.CharField(
|
name = forms.CharField(
|
||||||
label=_('Name'),
|
label=_('Name'),
|
||||||
required=False
|
required=False
|
||||||
@@ -157,18 +158,14 @@ class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label=_('Device Status'),
|
label=_('Device Status'),
|
||||||
)
|
)
|
||||||
owner_id = DynamicModelChoiceField(
|
|
||||||
queryset=Owner.objects.all(),
|
|
||||||
required=False,
|
|
||||||
label=_('Owner'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RegionFilterForm(ContactModelFilterForm, NestedGroupModelFilterSetForm):
|
class RegionFilterForm(ContactModelFilterForm, NestedGroupModelFilterSetForm):
|
||||||
model = Region
|
model = Region
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('parent_id', name=_('Region')),
|
FieldSet('parent_id', name=_('Region')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts'))
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts'))
|
||||||
)
|
)
|
||||||
parent_id = DynamicModelMultipleChoiceField(
|
parent_id = DynamicModelMultipleChoiceField(
|
||||||
@@ -182,8 +179,9 @@ class RegionFilterForm(ContactModelFilterForm, NestedGroupModelFilterSetForm):
|
|||||||
class SiteGroupFilterForm(ContactModelFilterForm, NestedGroupModelFilterSetForm):
|
class SiteGroupFilterForm(ContactModelFilterForm, NestedGroupModelFilterSetForm):
|
||||||
model = SiteGroup
|
model = SiteGroup
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('parent_id', name=_('Site Group')),
|
FieldSet('parent_id', name=_('Site Group')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts'))
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts'))
|
||||||
)
|
)
|
||||||
parent_id = DynamicModelMultipleChoiceField(
|
parent_id = DynamicModelMultipleChoiceField(
|
||||||
@@ -197,9 +195,10 @@ class SiteGroupFilterForm(ContactModelFilterForm, NestedGroupModelFilterSetForm)
|
|||||||
class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm):
|
class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = Site
|
model = Site
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('status', 'region_id', 'group_id', 'asn_id', name=_('Attributes')),
|
FieldSet('status', 'region_id', 'group_id', 'asn_id', name=_('Attributes')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||||
)
|
)
|
||||||
selector_fields = ('filter_id', 'q', 'region_id', 'group_id')
|
selector_fields = ('filter_id', 'q', 'region_id', 'group_id')
|
||||||
@@ -229,9 +228,10 @@ class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilt
|
|||||||
class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NestedGroupModelFilterSetForm):
|
class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NestedGroupModelFilterSetForm):
|
||||||
model = Location
|
model = Location
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'parent_id', 'status', 'facility', name=_('Attributes')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'parent_id', 'status', 'facility', name=_('Attributes')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||||
)
|
)
|
||||||
region_id = DynamicModelMultipleChoiceField(
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
@@ -277,7 +277,8 @@ class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NestedGroupM
|
|||||||
class RackRoleFilterForm(OrganizationalModelFilterSetForm):
|
class RackRoleFilterForm(OrganizationalModelFilterSetForm):
|
||||||
model = RackRole
|
model = RackRole
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
@@ -328,10 +329,11 @@ class RackBaseFilterForm(PrimaryModelFilterSetForm):
|
|||||||
class RackTypeFilterForm(RackBaseFilterForm):
|
class RackTypeFilterForm(RackBaseFilterForm):
|
||||||
model = RackType
|
model = RackType
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('manufacturer_id', 'form_factor', 'width', 'u_height', 'rack_count', name=_('Rack Type')),
|
FieldSet('manufacturer_id', 'form_factor', 'width', 'u_height', 'rack_count', name=_('Rack Type')),
|
||||||
FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
|
FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
|
||||||
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
|
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
selector_fields = ('filter_id', 'q', 'manufacturer_id')
|
selector_fields = ('filter_id', 'q', 'manufacturer_id')
|
||||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||||
@@ -350,13 +352,14 @@ class RackTypeFilterForm(RackBaseFilterForm):
|
|||||||
class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, RackBaseFilterForm):
|
class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, RackBaseFilterForm):
|
||||||
model = Rack
|
model = Rack
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
|
||||||
FieldSet('status', 'role_id', 'manufacturer_id', 'rack_type_id', 'serial', 'asset_tag', name=_('Rack')),
|
FieldSet('status', 'role_id', 'manufacturer_id', 'rack_type_id', 'serial', 'asset_tag', name=_('Rack')),
|
||||||
FieldSet('form_factor', 'width', 'u_height', 'airflow', name=_('Hardware')),
|
FieldSet('form_factor', 'width', 'u_height', 'airflow', name=_('Hardware')),
|
||||||
FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
|
FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
|
||||||
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
|
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
|
||||||
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||||
)
|
)
|
||||||
selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'location_id')
|
selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'location_id')
|
||||||
@@ -433,9 +436,10 @@ class RackElevationFilterForm(RackFilterForm):
|
|||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'id', name=_('Location')),
|
||||||
FieldSet('status', 'role_id', name=_('Function')),
|
FieldSet('status', 'role_id', name=_('Function')),
|
||||||
FieldSet('type', 'width', 'serial', 'asset_tag', name=_('Hardware')),
|
FieldSet('type', 'width', 'serial', 'asset_tag', name=_('Hardware')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
|
||||||
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
|
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
|
||||||
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||||
)
|
)
|
||||||
id = DynamicModelMultipleChoiceField(
|
id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Rack.objects.all(),
|
queryset=Rack.objects.all(),
|
||||||
@@ -451,10 +455,11 @@ class RackElevationFilterForm(RackFilterForm):
|
|||||||
class RackReservationFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
class RackReservationFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = RackReservation
|
model = RackReservation
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('status', 'user_id', name=_('Reservation')),
|
FieldSet('status', 'user_id', name=_('Reservation')),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Rack')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Rack')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
region_id = DynamicModelMultipleChoiceField(
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
@@ -509,7 +514,8 @@ class RackReservationFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
|||||||
class ManufacturerFilterForm(ContactModelFilterForm, OrganizationalModelFilterSetForm):
|
class ManufacturerFilterForm(ContactModelFilterForm, OrganizationalModelFilterSetForm):
|
||||||
model = Manufacturer
|
model = Manufacturer
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts'))
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts'))
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
@@ -518,7 +524,7 @@ class ManufacturerFilterForm(ContactModelFilterForm, OrganizationalModelFilterSe
|
|||||||
class DeviceTypeFilterForm(PrimaryModelFilterSetForm):
|
class DeviceTypeFilterForm(PrimaryModelFilterSetForm):
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'manufacturer_id', 'default_platform_id', 'part_number', 'device_count',
|
'manufacturer_id', 'default_platform_id', 'part_number', 'device_count',
|
||||||
'subdevice_role', 'airflow', name=_('Hardware')
|
'subdevice_role', 'airflow', name=_('Hardware')
|
||||||
@@ -529,6 +535,7 @@ class DeviceTypeFilterForm(PrimaryModelFilterSetForm):
|
|||||||
'pass_through_ports', 'device_bays', 'module_bays', 'inventory_items', name=_('Components')
|
'pass_through_ports', 'device_bays', 'module_bays', 'inventory_items', name=_('Components')
|
||||||
),
|
),
|
||||||
FieldSet('weight', 'weight_unit', name=_('Weight')),
|
FieldSet('weight', 'weight_unit', name=_('Weight')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
selector_fields = ('filter_id', 'q', 'manufacturer_id')
|
selector_fields = ('filter_id', 'q', 'manufacturer_id')
|
||||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||||
@@ -652,7 +659,8 @@ class DeviceTypeFilterForm(PrimaryModelFilterSetForm):
|
|||||||
class ModuleTypeProfileFilterForm(PrimaryModelFilterSetForm):
|
class ModuleTypeProfileFilterForm(PrimaryModelFilterSetForm):
|
||||||
model = ModuleTypeProfile
|
model = ModuleTypeProfile
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
selector_fields = ('filter_id', 'q')
|
selector_fields = ('filter_id', 'q')
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
@@ -661,7 +669,7 @@ class ModuleTypeProfileFilterForm(PrimaryModelFilterSetForm):
|
|||||||
class ModuleTypeFilterForm(PrimaryModelFilterSetForm):
|
class ModuleTypeFilterForm(PrimaryModelFilterSetForm):
|
||||||
model = ModuleType
|
model = ModuleType
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'profile_id', 'manufacturer_id', 'part_number', 'module_count',
|
'profile_id', 'manufacturer_id', 'part_number', 'module_count',
|
||||||
'airflow', name=_('Hardware')
|
'airflow', name=_('Hardware')
|
||||||
@@ -671,6 +679,7 @@ class ModuleTypeFilterForm(PrimaryModelFilterSetForm):
|
|||||||
'pass_through_ports', name=_('Components')
|
'pass_through_ports', name=_('Components')
|
||||||
),
|
),
|
||||||
FieldSet('weight', 'weight_unit', name=_('Weight')),
|
FieldSet('weight', 'weight_unit', name=_('Weight')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
selector_fields = ('filter_id', 'q', 'manufacturer_id')
|
selector_fields = ('filter_id', 'q', 'manufacturer_id')
|
||||||
profile_id = DynamicModelMultipleChoiceField(
|
profile_id = DynamicModelMultipleChoiceField(
|
||||||
@@ -754,8 +763,9 @@ class ModuleTypeFilterForm(PrimaryModelFilterSetForm):
|
|||||||
class DeviceRoleFilterForm(NestedGroupModelFilterSetForm):
|
class DeviceRoleFilterForm(NestedGroupModelFilterSetForm):
|
||||||
model = DeviceRole
|
model = DeviceRole
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('parent_id', 'config_template_id', name=_('Device Role'))
|
FieldSet('parent_id', 'config_template_id', name=_('Device Role')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
config_template_id = DynamicModelMultipleChoiceField(
|
config_template_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=ConfigTemplate.objects.all(),
|
queryset=ConfigTemplate.objects.all(),
|
||||||
@@ -773,8 +783,9 @@ class DeviceRoleFilterForm(NestedGroupModelFilterSetForm):
|
|||||||
class PlatformFilterForm(NestedGroupModelFilterSetForm):
|
class PlatformFilterForm(NestedGroupModelFilterSetForm):
|
||||||
model = Platform
|
model = Platform
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('manufacturer_id', 'parent_id', 'config_template_id', name=_('Platform'))
|
FieldSet('manufacturer_id', 'parent_id', 'config_template_id', name=_('Platform')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
selector_fields = ('filter_id', 'q', 'manufacturer_id')
|
selector_fields = ('filter_id', 'q', 'manufacturer_id')
|
||||||
parent_id = DynamicModelMultipleChoiceField(
|
parent_id = DynamicModelMultipleChoiceField(
|
||||||
@@ -803,11 +814,12 @@ class DeviceFilterForm(
|
|||||||
):
|
):
|
||||||
model = Device
|
model = Device
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||||
FieldSet('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address', name=_('Operation')),
|
FieldSet('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address', name=_('Operation')),
|
||||||
FieldSet('manufacturer_id', 'device_type_id', 'platform_id', name=_('Hardware')),
|
FieldSet('manufacturer_id', 'device_type_id', 'platform_id', name=_('Hardware')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports',
|
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports',
|
||||||
@@ -996,9 +1008,10 @@ class DeviceFilterForm(
|
|||||||
class VirtualDeviceContextFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
class VirtualDeviceContextFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = VirtualDeviceContext
|
model = VirtualDeviceContext
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('device', 'status', 'has_primary_ip', name=_('Attributes')),
|
FieldSet('device', 'status', 'has_primary_ip', name=_('Attributes')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
device = DynamicModelMultipleChoiceField(
|
device = DynamicModelMultipleChoiceField(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
@@ -1023,9 +1036,10 @@ class VirtualDeviceContextFilterForm(TenancyFilterForm, PrimaryModelFilterSetFor
|
|||||||
class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
|
class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = Module
|
model = Module
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'device_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'device_id', name=_('Location')),
|
||||||
FieldSet('manufacturer_id', 'module_type_id', 'status', 'serial', 'asset_tag', name=_('Hardware')),
|
FieldSet('manufacturer_id', 'module_type_id', 'status', 'serial', 'asset_tag', name=_('Hardware')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
device_id = DynamicModelMultipleChoiceField(
|
device_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
@@ -1106,9 +1120,10 @@ class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, PrimaryM
|
|||||||
class VirtualChassisFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
class VirtualChassisFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = VirtualChassis
|
model = VirtualChassis
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_id', name=_('Tenant')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
region_id = DynamicModelMultipleChoiceField(
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
@@ -1135,10 +1150,11 @@ class VirtualChassisFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
|||||||
class CableFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
class CableFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = Cable
|
model = Cable
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('site_id', 'location_id', 'rack_id', 'device_id', name=_('Location')),
|
FieldSet('site_id', 'location_id', 'rack_id', 'device_id', name=_('Location')),
|
||||||
FieldSet('type', 'status', 'profile', 'color', 'length', 'length_unit', 'unterminated', name=_('Attributes')),
|
FieldSet('type', 'status', 'profile', 'color', 'length', 'length_unit', 'unterminated', name=_('Attributes')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
region_id = DynamicModelMultipleChoiceField(
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
@@ -1224,8 +1240,9 @@ class CableFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
|||||||
class PowerPanelFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
class PowerPanelFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = PowerPanel
|
model = PowerPanel
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||||
)
|
)
|
||||||
selector_fields = ('filter_id', 'q', 'site_id', 'location_id')
|
selector_fields = ('filter_id', 'q', 'site_id', 'location_id')
|
||||||
@@ -1263,10 +1280,11 @@ class PowerPanelFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
|||||||
class PowerFeedFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
class PowerFeedFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = PowerFeed
|
model = PowerFeed
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'power_panel_id', 'rack_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'power_panel_id', 'rack_id', name=_('Location')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
|
||||||
FieldSet('status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', name=_('Attributes')),
|
FieldSet('status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', name=_('Attributes')),
|
||||||
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
region_id = DynamicModelMultipleChoiceField(
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
@@ -1390,7 +1408,7 @@ class PathEndpointFilterForm(CabledFilterForm):
|
|||||||
class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||||
model = ConsolePort
|
model = ConsolePort
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')),
|
FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
@@ -1398,6 +1416,7 @@ class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
|||||||
name=_('Device')
|
name=_('Device')
|
||||||
),
|
),
|
||||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
type = forms.MultipleChoiceField(
|
type = forms.MultipleChoiceField(
|
||||||
label=_('Type'),
|
label=_('Type'),
|
||||||
@@ -1429,7 +1448,7 @@ class ConsolePortTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
|||||||
class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||||
model = ConsoleServerPort
|
model = ConsoleServerPort
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')),
|
FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
@@ -1437,6 +1456,7 @@ class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterF
|
|||||||
name=_('Device')
|
name=_('Device')
|
||||||
),
|
),
|
||||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
type = forms.MultipleChoiceField(
|
type = forms.MultipleChoiceField(
|
||||||
label=_('Type'),
|
label=_('Type'),
|
||||||
@@ -1468,7 +1488,7 @@ class ConsoleServerPortTemplateFilterForm(ModularDeviceComponentTemplateFilterFo
|
|||||||
class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||||
model = PowerPort
|
model = PowerPort
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('name', 'label', 'type', name=_('Attributes')),
|
FieldSet('name', 'label', 'type', name=_('Attributes')),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
@@ -1476,6 +1496,7 @@ class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
|||||||
name=_('Device')
|
name=_('Device')
|
||||||
),
|
),
|
||||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
type = forms.MultipleChoiceField(
|
type = forms.MultipleChoiceField(
|
||||||
label=_('Type'),
|
label=_('Type'),
|
||||||
@@ -1502,7 +1523,7 @@ class PowerPortTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
|||||||
class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||||
model = PowerOutlet
|
model = PowerOutlet
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('name', 'label', 'type', 'color', 'status', name=_('Attributes')),
|
FieldSet('name', 'label', 'type', 'color', 'status', name=_('Attributes')),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
@@ -1510,6 +1531,7 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
|||||||
name=_('Device')
|
name=_('Device')
|
||||||
),
|
),
|
||||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
type = forms.MultipleChoiceField(
|
type = forms.MultipleChoiceField(
|
||||||
label=_('Type'),
|
label=_('Type'),
|
||||||
@@ -1545,7 +1567,7 @@ class PowerOutletTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
|||||||
class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||||
model = Interface
|
model = Interface
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only', name=_('Attributes')),
|
FieldSet('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only', name=_('Attributes')),
|
||||||
FieldSet('vrf_id', 'l2vpn_id', 'mac_address', 'wwn', name=_('Addressing')),
|
FieldSet('vrf_id', 'l2vpn_id', 'mac_address', 'wwn', name=_('Addressing')),
|
||||||
FieldSet('poe_mode', 'poe_type', name=_('PoE')),
|
FieldSet('poe_mode', 'poe_type', name=_('PoE')),
|
||||||
@@ -1558,6 +1580,7 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
|||||||
name=_('Device')
|
name=_('Device')
|
||||||
),
|
),
|
||||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
selector_fields = ('filter_id', 'q', 'device_id')
|
selector_fields = ('filter_id', 'q', 'device_id')
|
||||||
vdc_id = DynamicModelMultipleChoiceField(
|
vdc_id = DynamicModelMultipleChoiceField(
|
||||||
@@ -1716,7 +1739,7 @@ class InterfaceTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
|||||||
|
|
||||||
class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
|
FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
@@ -1724,6 +1747,7 @@ class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
|||||||
name=_('Device')
|
name=_('Device')
|
||||||
),
|
),
|
||||||
FieldSet('cabled', 'occupied', name=_('Cable')),
|
FieldSet('cabled', 'occupied', name=_('Cable')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
model = FrontPort
|
model = FrontPort
|
||||||
type = forms.MultipleChoiceField(
|
type = forms.MultipleChoiceField(
|
||||||
@@ -1759,7 +1783,7 @@ class FrontPortTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
|||||||
class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
||||||
model = RearPort
|
model = RearPort
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
|
FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
@@ -1767,6 +1791,7 @@ class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
|||||||
name=_('Device')
|
name=_('Device')
|
||||||
),
|
),
|
||||||
FieldSet('cabled', 'occupied', name=_('Cable')),
|
FieldSet('cabled', 'occupied', name=_('Cable')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
type = forms.MultipleChoiceField(
|
type = forms.MultipleChoiceField(
|
||||||
label=_('Type'),
|
label=_('Type'),
|
||||||
@@ -1801,13 +1826,14 @@ class RearPortTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
|||||||
class ModuleBayFilterForm(DeviceComponentFilterForm):
|
class ModuleBayFilterForm(DeviceComponentFilterForm):
|
||||||
model = ModuleBay
|
model = ModuleBay
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('name', 'label', 'position', name=_('Attributes')),
|
FieldSet('name', 'label', 'position', name=_('Attributes')),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||||
name=_('Device')
|
name=_('Device')
|
||||||
),
|
),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
position = forms.CharField(
|
position = forms.CharField(
|
||||||
@@ -1832,13 +1858,14 @@ class ModuleBayTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
|||||||
class DeviceBayFilterForm(DeviceComponentFilterForm):
|
class DeviceBayFilterForm(DeviceComponentFilterForm):
|
||||||
model = DeviceBay
|
model = DeviceBay
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('name', 'label', name=_('Attributes')),
|
FieldSet('name', 'label', name=_('Attributes')),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||||
name=_('Device')
|
name=_('Device')
|
||||||
),
|
),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
@@ -1855,7 +1882,7 @@ class DeviceBayTemplateFilterForm(DeviceComponentTemplateFilterForm):
|
|||||||
class InventoryItemFilterForm(DeviceComponentFilterForm):
|
class InventoryItemFilterForm(DeviceComponentFilterForm):
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'name', 'label', 'status', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered',
|
'name', 'label', 'status', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered',
|
||||||
name=_('Attributes')
|
name=_('Attributes')
|
||||||
@@ -1865,6 +1892,7 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
|
|||||||
'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||||
name=_('Device')
|
name=_('Device')
|
||||||
),
|
),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
role_id = DynamicModelMultipleChoiceField(
|
role_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=InventoryItemRole.objects.all(),
|
queryset=InventoryItemRole.objects.all(),
|
||||||
@@ -1925,7 +1953,8 @@ class InventoryItemTemplateFilterForm(DeviceComponentTemplateFilterForm):
|
|||||||
class InventoryItemRoleFilterForm(OrganizationalModelFilterSetForm):
|
class InventoryItemRoleFilterForm(OrganizationalModelFilterSetForm):
|
||||||
model = InventoryItemRole
|
model = InventoryItemRole
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
@@ -1937,9 +1966,10 @@ class InventoryItemRoleFilterForm(OrganizationalModelFilterSetForm):
|
|||||||
class MACAddressFilterForm(PrimaryModelFilterSetForm):
|
class MACAddressFilterForm(PrimaryModelFilterSetForm):
|
||||||
model = MACAddress
|
model = MACAddress
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('mac_address', name=_('Attributes')),
|
FieldSet('mac_address', name=_('Attributes')),
|
||||||
FieldSet('device_id', 'virtual_machine_id', 'assigned', 'primary', name=_('Assignments')),
|
FieldSet('device_id', 'virtual_machine_id', 'assigned', 'primary', name=_('Assignments')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
selector_fields = ('filter_id', 'q', 'device_id', 'virtual_machine_id')
|
selector_fields = ('filter_id', 'q', 'device_id', 'virtual_machine_id')
|
||||||
mac_address = forms.CharField(
|
mac_address = forms.CharField(
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ class ScopedForm(forms.Form):
|
|||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if self.instance and scope_type_id != self.instance.scope_type_id:
|
if self.instance and self.instance.pk and scope_type_id != self.instance.scope_type_id:
|
||||||
self.initial['scope'] = None
|
self.initial['scope'] = None
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
|
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from netbox.forms import NetBoxModelForm
|
from netbox.forms import NetBoxModelForm
|
||||||
|
from netbox.forms.mixins import OwnerMixin
|
||||||
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField
|
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField
|
||||||
from utilities.forms.rendering import FieldSet, TabbedGroups
|
from utilities.forms.rendering import FieldSet, TabbedGroups
|
||||||
from utilities.forms.widgets import APISelect
|
from utilities.forms.widgets import APISelect
|
||||||
@@ -271,7 +272,7 @@ class InventoryItemCreateForm(ComponentCreateForm, model_forms.InventoryItemForm
|
|||||||
# Virtual chassis
|
# Virtual chassis
|
||||||
#
|
#
|
||||||
|
|
||||||
class VirtualChassisCreateForm(NetBoxModelForm):
|
class VirtualChassisCreateForm(OwnerMixin, NetBoxModelForm):
|
||||||
region = DynamicModelChoiceField(
|
region = DynamicModelChoiceField(
|
||||||
label=_('Region'),
|
label=_('Region'),
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
|
|||||||
@@ -550,6 +550,10 @@ class InterfaceFilter(
|
|||||||
strawberry_django.filter_field()
|
strawberry_django.filter_field()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@strawberry_django.filter_field
|
||||||
|
def cabled(self, value: bool, prefix: str):
|
||||||
|
return Q(**{f'{prefix}cable__isnull': (not value)})
|
||||||
|
|
||||||
@strawberry_django.filter_field
|
@strawberry_django.filter_field
|
||||||
def connected(self, queryset, value: bool, prefix: str):
|
def connected(self, queryset, value: bool, prefix: str):
|
||||||
if value is True:
|
if value is True:
|
||||||
@@ -889,7 +893,7 @@ class PowerPortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedM
|
|||||||
|
|
||||||
|
|
||||||
@strawberry_django.filter_type(models.RackType, lookups=True)
|
@strawberry_django.filter_type(models.RackType, lookups=True)
|
||||||
class RackTypeFilter(RackFilterMixin, WeightFilterMixin, PrimaryModelFilter):
|
class RackTypeFilter(ImageAttachmentFilterMixin, RackFilterMixin, WeightFilterMixin, PrimaryModelFilter):
|
||||||
form_factor: BaseFilterLookup[Annotated['RackFormFactorEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
form_factor: BaseFilterLookup[Annotated['RackFormFactorEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||||
strawberry_django.filter_field()
|
strawberry_django.filter_field()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -734,7 +734,7 @@ class PowerPortTemplateType(ModularComponentTemplateType):
|
|||||||
filters=RackTypeFilter,
|
filters=RackTypeFilter,
|
||||||
pagination=True
|
pagination=True
|
||||||
)
|
)
|
||||||
class RackTypeType(PrimaryObjectType):
|
class RackTypeType(ImageAttachmentsMixin, PrimaryObjectType):
|
||||||
rack_count: BigInt
|
rack_count: BigInt
|
||||||
manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')]
|
manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')]
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from jsonschema.exceptions import ValidationError as JSONValidationError
|
from jsonschema.exceptions import ValidationError as JSONValidationError
|
||||||
|
|
||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.utils import update_interface_bridges
|
from dcim.utils import create_port_mappings, update_interface_bridges
|
||||||
from extras.models import ConfigContextModel, CustomField
|
from extras.models import ConfigContextModel, CustomField
|
||||||
from netbox.models import PrimaryModel
|
from netbox.models import PrimaryModel
|
||||||
from netbox.models.features import ImageAttachmentsMixin
|
from netbox.models.features import ImageAttachmentsMixin
|
||||||
@@ -155,6 +155,8 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
|
|||||||
'description': self.description,
|
'description': self.description,
|
||||||
'weight': float(self.weight) if self.weight is not None else None,
|
'weight': float(self.weight) if self.weight is not None else None,
|
||||||
'weight_unit': self.weight_unit,
|
'weight_unit': self.weight_unit,
|
||||||
|
'airflow': self.airflow,
|
||||||
|
'attribute_data': self.attribute_data,
|
||||||
'comments': self.comments,
|
'comments': self.comments,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,5 +361,7 @@ class Module(TrackingModelMixin, PrimaryModel, ConfigContextModel):
|
|||||||
update_fields=update_fields
|
update_fields=update_fields
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Replicate any front/rear port mappings from the ModuleType
|
||||||
|
create_port_mappings(self.device, self.module_type, self)
|
||||||
# Interface bridges have to be set after interface instantiation
|
# Interface bridges have to be set after interface instantiation
|
||||||
update_interface_bridges(self.device, self.module_type.interfacetemplates, self)
|
update_interface_bridges(self.device, self.module_type.interfacetemplates, self)
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ class RackBase(WeightMixin, PrimaryModel):
|
|||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
class RackType(RackBase):
|
class RackType(ImageAttachmentsMixin, RackBase):
|
||||||
"""
|
"""
|
||||||
Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
|
Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
|
||||||
Each Rack is assigned to a Site and (optionally) a Location.
|
Each Rack is assigned to a Site and (optionally) a Location.
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ __all__ = (
|
|||||||
'DeviceTable',
|
'DeviceTable',
|
||||||
'FrontPortTable',
|
'FrontPortTable',
|
||||||
'InterfaceTable',
|
'InterfaceTable',
|
||||||
|
'InterfaceLAGMemberTable',
|
||||||
'InventoryItemRoleTable',
|
'InventoryItemRoleTable',
|
||||||
'InventoryItemTable',
|
'InventoryItemTable',
|
||||||
'MACAddressTable',
|
'MACAddressTable',
|
||||||
@@ -689,6 +690,33 @@ class InterfaceTable(BaseInterfaceTable, ModularDeviceComponentTable, PathEndpoi
|
|||||||
default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
|
default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
class InterfaceLAGMemberTable(PathEndpointTable, NetBoxTable):
|
||||||
|
parent = tables.Column(
|
||||||
|
verbose_name=_('Parent'),
|
||||||
|
accessor=Accessor('device'),
|
||||||
|
linkify=True,
|
||||||
|
)
|
||||||
|
name = tables.Column(
|
||||||
|
verbose_name=_('Name'),
|
||||||
|
linkify=True,
|
||||||
|
order_by=('_name',),
|
||||||
|
)
|
||||||
|
connection = columns.TemplateColumn(
|
||||||
|
accessor='connected_endpoints',
|
||||||
|
template_code=INTERFACE_LAG_MEMBERS_LINKTERMINATION,
|
||||||
|
verbose_name=_('Peer'),
|
||||||
|
orderable=False,
|
||||||
|
)
|
||||||
|
tags = columns.TagColumn(
|
||||||
|
url_name='dcim:interface_list'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta(NetBoxTable.Meta):
|
||||||
|
model = models.Interface
|
||||||
|
fields = ('pk', 'parent', 'name', 'type', 'connection')
|
||||||
|
default_columns = ('pk', 'parent', 'name', 'type', 'connection')
|
||||||
|
|
||||||
|
|
||||||
class DeviceInterfaceTable(InterfaceTable):
|
class DeviceInterfaceTable(InterfaceTable):
|
||||||
name = tables.TemplateColumn(
|
name = tables.TemplateColumn(
|
||||||
verbose_name=_('Name'),
|
verbose_name=_('Name'),
|
||||||
|
|||||||
@@ -24,6 +24,24 @@ INTERFACE_LINKTERMINATION = """
|
|||||||
{% else %}""" + LINKTERMINATION + """{% endif %}
|
{% else %}""" + LINKTERMINATION + """{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
INTERFACE_LAG_MEMBERS_LINKTERMINATION = """
|
||||||
|
{% for termination in value %}
|
||||||
|
{% if termination.parent_object %}
|
||||||
|
<a href="{{ termination.parent_object.get_absolute_url }}">{{ termination.parent_object }}</a>
|
||||||
|
<i class="mdi mdi-chevron-right"></i>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{{ termination.get_absolute_url }}">{{ termination }}</a>
|
||||||
|
{% if termination.lag %}
|
||||||
|
<i class="mdi mdi-chevron-right"></i>
|
||||||
|
<a href="{{ termination.lag.get_absolute_url }}">{{ termination.lag }}</a>
|
||||||
|
<span class="text-muted">(LAG)</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if not forloop.last %}<br />{% endif %}
|
||||||
|
{% empty %}
|
||||||
|
{{ ''|placeholder }}
|
||||||
|
{% endfor %}
|
||||||
|
"""
|
||||||
|
|
||||||
CABLE_LENGTH = """
|
CABLE_LENGTH = """
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
{% if record.length %}{{ record.length|floatformat:"-2" }} {{ record.length_unit }}{% endif %}
|
{% if record.length %}{{ record.length|floatformat:"-2" }} {{ record.length_unit }}{% endif %}
|
||||||
|
|||||||
@@ -875,6 +875,142 @@ class ModuleBayTestCase(TestCase):
|
|||||||
self.assertIsNone(bay2.parent)
|
self.assertIsNone(bay2.parent)
|
||||||
self.assertIsNone(bay2.module)
|
self.assertIsNone(bay2.module)
|
||||||
|
|
||||||
|
def test_module_installation_creates_port_mappings(self):
|
||||||
|
"""
|
||||||
|
Test that installing a module with front/rear port templates correctly
|
||||||
|
creates PortMapping instances for the device.
|
||||||
|
"""
|
||||||
|
device = Device.objects.first()
|
||||||
|
manufacturer = Manufacturer.objects.first()
|
||||||
|
module_bay = ModuleBay.objects.create(device=device, name='Test Bay PortMapping 1')
|
||||||
|
|
||||||
|
# Create a module type with a rear port template
|
||||||
|
module_type_with_mappings = ModuleType.objects.create(
|
||||||
|
manufacturer=manufacturer,
|
||||||
|
model='Module Type With Mappings',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a rear port template with 12 positions (splice)
|
||||||
|
rear_port_template = RearPortTemplate.objects.create(
|
||||||
|
module_type=module_type_with_mappings,
|
||||||
|
name='Rear Port 1',
|
||||||
|
type=PortTypeChoices.TYPE_SPLICE,
|
||||||
|
positions=12,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create 12 front port templates mapped to the rear port
|
||||||
|
front_port_templates = []
|
||||||
|
for i in range(1, 13):
|
||||||
|
front_port_template = FrontPortTemplate.objects.create(
|
||||||
|
module_type=module_type_with_mappings,
|
||||||
|
name=f'port {i}',
|
||||||
|
type=PortTypeChoices.TYPE_LC,
|
||||||
|
positions=1,
|
||||||
|
)
|
||||||
|
front_port_templates.append(front_port_template)
|
||||||
|
|
||||||
|
# Create port template mapping
|
||||||
|
PortTemplateMapping.objects.create(
|
||||||
|
device_type=None,
|
||||||
|
module_type=module_type_with_mappings,
|
||||||
|
front_port=front_port_template,
|
||||||
|
front_port_position=1,
|
||||||
|
rear_port=rear_port_template,
|
||||||
|
rear_port_position=i,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Install the module
|
||||||
|
module = Module.objects.create(
|
||||||
|
device=device,
|
||||||
|
module_bay=module_bay,
|
||||||
|
module_type=module_type_with_mappings,
|
||||||
|
status=ModuleStatusChoices.STATUS_ACTIVE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify that front ports were created
|
||||||
|
front_ports = FrontPort.objects.filter(device=device, module=module)
|
||||||
|
self.assertEqual(front_ports.count(), 12)
|
||||||
|
|
||||||
|
# Verify that the rear port was created
|
||||||
|
rear_ports = RearPort.objects.filter(device=device, module=module)
|
||||||
|
self.assertEqual(rear_ports.count(), 1)
|
||||||
|
rear_port = rear_ports.first()
|
||||||
|
self.assertEqual(rear_port.positions, 12)
|
||||||
|
|
||||||
|
# Verify that port mappings were created
|
||||||
|
port_mappings = PortMapping.objects.filter(front_port__module=module)
|
||||||
|
self.assertEqual(port_mappings.count(), 12)
|
||||||
|
|
||||||
|
# Verify each mapping is correct
|
||||||
|
for i, front_port_template in enumerate(front_port_templates, start=1):
|
||||||
|
front_port = FrontPort.objects.get(
|
||||||
|
device=device,
|
||||||
|
name=front_port_template.name,
|
||||||
|
module=module,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that a mapping exists for this front port
|
||||||
|
mapping = PortMapping.objects.get(
|
||||||
|
device=device,
|
||||||
|
front_port=front_port,
|
||||||
|
front_port_position=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(mapping.rear_port, rear_port)
|
||||||
|
self.assertEqual(mapping.front_port_position, 1)
|
||||||
|
self.assertEqual(mapping.rear_port_position, i)
|
||||||
|
|
||||||
|
def test_module_installation_without_mappings(self):
|
||||||
|
"""
|
||||||
|
Test that installing a module without port template mappings
|
||||||
|
doesn't create any PortMapping instances.
|
||||||
|
"""
|
||||||
|
device = Device.objects.first()
|
||||||
|
manufacturer = Manufacturer.objects.first()
|
||||||
|
module_bay = ModuleBay.objects.create(device=device, name='Test Bay PortMapping 2')
|
||||||
|
|
||||||
|
# Create a module type without any port template mappings
|
||||||
|
module_type_no_mappings = ModuleType.objects.create(
|
||||||
|
manufacturer=manufacturer,
|
||||||
|
model='Module Type Without Mappings',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a rear port template
|
||||||
|
RearPortTemplate.objects.create(
|
||||||
|
module_type=module_type_no_mappings,
|
||||||
|
name='Rear Port 1',
|
||||||
|
type=PortTypeChoices.TYPE_SPLICE,
|
||||||
|
positions=12,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create front port templates but DO NOT create PortTemplateMapping rows
|
||||||
|
for i in range(1, 13):
|
||||||
|
FrontPortTemplate.objects.create(
|
||||||
|
module_type=module_type_no_mappings,
|
||||||
|
name=f'port {i}',
|
||||||
|
type=PortTypeChoices.TYPE_LC,
|
||||||
|
positions=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Install the module
|
||||||
|
module = Module.objects.create(
|
||||||
|
device=device,
|
||||||
|
module_bay=module_bay,
|
||||||
|
module_type=module_type_no_mappings,
|
||||||
|
status=ModuleStatusChoices.STATUS_ACTIVE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify no port mappings were created for this module
|
||||||
|
port_mappings = PortMapping.objects.filter(
|
||||||
|
device=device,
|
||||||
|
front_port__module=module,
|
||||||
|
front_port_position=1,
|
||||||
|
)
|
||||||
|
self.assertEqual(port_mappings.count(), 0)
|
||||||
|
self.assertEqual(FrontPort.objects.filter(module=module).count(), 12)
|
||||||
|
self.assertEqual(RearPort.objects.filter(module=module).count(), 1)
|
||||||
|
self.assertEqual(PortMapping.objects.filter(front_port__module=module).count(), 0)
|
||||||
|
|
||||||
|
|
||||||
class CableTestCase(TestCase):
|
class CableTestCase(TestCase):
|
||||||
|
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ class DeviceManagementPanel(panels.ObjectAttributesPanel):
|
|||||||
class DeviceDimensionsPanel(panels.ObjectAttributesPanel):
|
class DeviceDimensionsPanel(panels.ObjectAttributesPanel):
|
||||||
title = _('Dimensions')
|
title = _('Dimensions')
|
||||||
|
|
||||||
height = attrs.TextAttr('device_type.u_height', format_string='{}U')
|
height = attrs.TemplatedAttr('device_type.u_height', template_name='dcim/devicetype/attrs/height.html')
|
||||||
total_weight = attrs.TemplatedAttr('total_weight', template_name='dcim/device/attrs/total_weight.html')
|
total_weight = attrs.TemplatedAttr('total_weight', template_name='dcim/device/attrs/total_weight.html')
|
||||||
|
|
||||||
|
|
||||||
@@ -135,7 +135,7 @@ class DeviceTypePanel(panels.ObjectAttributesPanel):
|
|||||||
part_number = attrs.TextAttr('part_number')
|
part_number = attrs.TextAttr('part_number')
|
||||||
default_platform = attrs.RelatedObjectAttr('default_platform', linkify=True)
|
default_platform = attrs.RelatedObjectAttr('default_platform', linkify=True)
|
||||||
description = attrs.TextAttr('description')
|
description = attrs.TextAttr('description')
|
||||||
height = attrs.TextAttr('u_height', format_string='{}U', label=_('Height'))
|
height = attrs.TemplatedAttr('u_height', template_name='dcim/devicetype/attrs/height.html')
|
||||||
exclude_from_utilization = attrs.BooleanAttr('exclude_from_utilization')
|
exclude_from_utilization = attrs.BooleanAttr('exclude_from_utilization')
|
||||||
full_depth = attrs.BooleanAttr('is_full_depth')
|
full_depth = attrs.BooleanAttr('is_full_depth')
|
||||||
weight = attrs.NumericAttr('weight', unit_accessor='get_weight_unit_display')
|
weight = attrs.NumericAttr('weight', unit_accessor='get_weight_unit_display')
|
||||||
|
|||||||
@@ -85,13 +85,13 @@ def update_interface_bridges(device, interface_templates, module=None):
|
|||||||
interface.save()
|
interface.save()
|
||||||
|
|
||||||
|
|
||||||
def create_port_mappings(device, device_type, module=None):
|
def create_port_mappings(device, device_or_module_type, module=None):
|
||||||
"""
|
"""
|
||||||
Replicate all front/rear port mappings from a DeviceType to the given device.
|
Replicate all front/rear port mappings from a DeviceType or ModuleType to the given device.
|
||||||
"""
|
"""
|
||||||
from dcim.models import FrontPort, PortMapping, RearPort
|
from dcim.models import FrontPort, PortMapping, RearPort
|
||||||
|
|
||||||
templates = device_type.port_mappings.prefetch_related('front_port', 'rear_port')
|
templates = device_or_module_type.port_mappings.prefetch_related('front_port', 'rear_port')
|
||||||
|
|
||||||
# Cache front & rear ports for efficient lookups by name
|
# Cache front & rear ports for efficient lookups by name
|
||||||
front_ports = {
|
front_ports = {
|
||||||
|
|||||||
@@ -880,6 +880,7 @@ class RackTypeView(GetRelatedModelsMixin, generic.ObjectView):
|
|||||||
panels.RackWeightPanel(title=_('Weight'), exclude=['total_weight']),
|
panels.RackWeightPanel(title=_('Weight'), exclude=['total_weight']),
|
||||||
CustomFieldsPanel(),
|
CustomFieldsPanel(),
|
||||||
RelatedObjectsPanel(),
|
RelatedObjectsPanel(),
|
||||||
|
ImageAttachmentsPanel(),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -3135,6 +3136,14 @@ class InterfaceView(generic.ObjectView):
|
|||||||
)
|
)
|
||||||
child_interfaces_table.configure(request)
|
child_interfaces_table.configure(request)
|
||||||
|
|
||||||
|
# Get LAG interfaces
|
||||||
|
lag_interfaces = Interface.objects.restrict(request.user, 'view').filter(lag=instance)
|
||||||
|
lag_interfaces_table = tables.InterfaceLAGMemberTable(
|
||||||
|
lag_interfaces,
|
||||||
|
orderable=False
|
||||||
|
)
|
||||||
|
lag_interfaces_table.configure(request)
|
||||||
|
|
||||||
# Get assigned VLANs and annotate whether each is tagged or untagged
|
# Get assigned VLANs and annotate whether each is tagged or untagged
|
||||||
vlans = []
|
vlans = []
|
||||||
if instance.untagged_vlan is not None:
|
if instance.untagged_vlan is not None:
|
||||||
@@ -3164,6 +3173,7 @@ class InterfaceView(generic.ObjectView):
|
|||||||
'bridge_interfaces': bridge_interfaces,
|
'bridge_interfaces': bridge_interfaces,
|
||||||
'bridge_interfaces_table': bridge_interfaces_table,
|
'bridge_interfaces_table': bridge_interfaces_table,
|
||||||
'child_interfaces_table': child_interfaces_table,
|
'child_interfaces_table': child_interfaces_table,
|
||||||
|
'lag_interfaces_table': lag_interfaces_table,
|
||||||
'vlan_table': vlan_table,
|
'vlan_table': vlan_table,
|
||||||
'vlan_translation_table': vlan_translation_table,
|
'vlan_translation_table': vlan_translation_table,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,10 +75,11 @@ def get_bookmarks_object_type_choices():
|
|||||||
def get_models_from_content_types(content_types):
|
def get_models_from_content_types(content_types):
|
||||||
"""
|
"""
|
||||||
Return a list of models corresponding to the given content types, identified by natural key.
|
Return a list of models corresponding to the given content types, identified by natural key.
|
||||||
|
Accepts both lowercase (e.g. "dcim.site") and PascalCase (e.g. "dcim.Site") model names.
|
||||||
"""
|
"""
|
||||||
models = []
|
models = []
|
||||||
for content_type_id in content_types:
|
for content_type_id in content_types:
|
||||||
app_label, model_name = content_type_id.split('.')
|
app_label, model_name = content_type_id.lower().split('.')
|
||||||
try:
|
try:
|
||||||
content_type = ObjectType.objects.get_by_natural_key(app_label, model_name)
|
content_type = ObjectType.objects.get_by_natural_key(app_label, model_name)
|
||||||
if content_type.model_class():
|
if content_type.model_class():
|
||||||
|
|||||||
+54
-46
@@ -1,5 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
from collections import defaultdict
|
from collections import UserDict, defaultdict
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@@ -12,7 +12,6 @@ from core.models import ObjectType
|
|||||||
from netbox.config import get_config
|
from netbox.config import get_config
|
||||||
from netbox.constants import RQ_QUEUE_DEFAULT
|
from netbox.constants import RQ_QUEUE_DEFAULT
|
||||||
from netbox.models.features import has_feature
|
from netbox.models.features import has_feature
|
||||||
from users.models import User
|
|
||||||
from utilities.api import get_serializer_for_model
|
from utilities.api import get_serializer_for_model
|
||||||
from utilities.request import copy_safe_request
|
from utilities.request import copy_safe_request
|
||||||
from utilities.rqworker import get_rq_retry
|
from utilities.rqworker import get_rq_retry
|
||||||
@@ -23,6 +22,21 @@ from .models import EventRule
|
|||||||
logger = logging.getLogger('netbox.events_processor')
|
logger = logging.getLogger('netbox.events_processor')
|
||||||
|
|
||||||
|
|
||||||
|
class EventContext(UserDict):
|
||||||
|
"""
|
||||||
|
A custom dictionary that automatically serializes its associated object on demand.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# We're emulating a dictionary here (rather than using a custom class) because prior to NetBox v4.5.2, events were
|
||||||
|
# queued as dictionaries for processing by handles in EVENTS_PIPELINE. We need to avoid introducing any breaking
|
||||||
|
# changes until a suitable minor release.
|
||||||
|
def __getitem__(self, item):
|
||||||
|
if item == 'data' and 'data' not in self:
|
||||||
|
data = serialize_for_event(self['object'])
|
||||||
|
self.__setitem__('data', data)
|
||||||
|
return super().__getitem__(item)
|
||||||
|
|
||||||
|
|
||||||
def serialize_for_event(instance):
|
def serialize_for_event(instance):
|
||||||
"""
|
"""
|
||||||
Return a serialized representation of the given instance suitable for use in a queued event.
|
Return a serialized representation of the given instance suitable for use in a queued event.
|
||||||
@@ -66,37 +80,42 @@ def enqueue_event(queue, instance, request, event_type):
|
|||||||
assert instance.pk is not None
|
assert instance.pk is not None
|
||||||
key = f'{app_label}.{model_name}:{instance.pk}'
|
key = f'{app_label}.{model_name}:{instance.pk}'
|
||||||
if key in queue:
|
if key in queue:
|
||||||
queue[key]['data'] = serialize_for_event(instance)
|
|
||||||
queue[key]['snapshots']['postchange'] = get_snapshots(instance, event_type)['postchange']
|
queue[key]['snapshots']['postchange'] = get_snapshots(instance, event_type)['postchange']
|
||||||
# If the object is being deleted, update any prior "update" event to "delete"
|
# If the object is being deleted, update any prior "update" event to "delete"
|
||||||
if event_type == OBJECT_DELETED:
|
if event_type == OBJECT_DELETED:
|
||||||
queue[key]['event_type'] = event_type
|
queue[key]['event_type'] = event_type
|
||||||
else:
|
else:
|
||||||
queue[key] = {
|
queue[key] = EventContext(
|
||||||
'object_type': ObjectType.objects.get_for_model(instance),
|
object_type=ObjectType.objects.get_for_model(instance),
|
||||||
'object_id': instance.pk,
|
object_id=instance.pk,
|
||||||
'event_type': event_type,
|
object=instance,
|
||||||
'data': serialize_for_event(instance),
|
event_type=event_type,
|
||||||
'snapshots': get_snapshots(instance, event_type),
|
snapshots=get_snapshots(instance, event_type),
|
||||||
'request': request,
|
request=request,
|
||||||
|
user=request.user,
|
||||||
# Legacy request attributes for backward compatibility
|
# Legacy request attributes for backward compatibility
|
||||||
'username': request.user.username,
|
username=request.user.username,
|
||||||
'request_id': request.id,
|
request_id=request.id,
|
||||||
}
|
)
|
||||||
|
# Force serialization of objects prior to them actually being deleted
|
||||||
|
if event_type == OBJECT_DELETED:
|
||||||
|
queue[key]['data'] = serialize_for_event(instance)
|
||||||
|
|
||||||
|
|
||||||
def process_event_rules(event_rules, object_type, event_type, data, username=None, snapshots=None, request=None):
|
def process_event_rules(event_rules, object_type, event):
|
||||||
user = None # To be resolved from the username if needed
|
"""
|
||||||
|
Process a list of EventRules against an event.
|
||||||
|
"""
|
||||||
|
|
||||||
for event_rule in event_rules:
|
for event_rule in event_rules:
|
||||||
|
|
||||||
# Evaluate event rule conditions (if any)
|
# Evaluate event rule conditions (if any)
|
||||||
if not event_rule.eval_conditions(data):
|
if not event_rule.eval_conditions(event['data']):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Compile event data
|
# Compile event data
|
||||||
event_data = event_rule.action_data or {}
|
event_data = event_rule.action_data or {}
|
||||||
event_data.update(data)
|
event_data.update(event['data'])
|
||||||
|
|
||||||
# Webhooks
|
# Webhooks
|
||||||
if event_rule.action_type == EventRuleActionChoices.WEBHOOK:
|
if event_rule.action_type == EventRuleActionChoices.WEBHOOK:
|
||||||
@@ -109,50 +128,41 @@ def process_event_rules(event_rules, object_type, event_type, data, username=Non
|
|||||||
params = {
|
params = {
|
||||||
"event_rule": event_rule,
|
"event_rule": event_rule,
|
||||||
"object_type": object_type,
|
"object_type": object_type,
|
||||||
"event_type": event_type,
|
"event_type": event['event_type'],
|
||||||
"data": event_data,
|
"data": event_data,
|
||||||
"snapshots": snapshots,
|
"snapshots": event.get('snapshots'),
|
||||||
"timestamp": timezone.now().isoformat(),
|
"timestamp": timezone.now().isoformat(),
|
||||||
"username": username,
|
"username": event['username'],
|
||||||
"retry": get_rq_retry()
|
"retry": get_rq_retry()
|
||||||
}
|
}
|
||||||
if snapshots:
|
if 'request' in event:
|
||||||
params["snapshots"] = snapshots
|
|
||||||
if request:
|
|
||||||
# Exclude FILES - webhooks don't need uploaded files,
|
# Exclude FILES - webhooks don't need uploaded files,
|
||||||
# which can cause pickle errors with Pillow.
|
# which can cause pickle errors with Pillow.
|
||||||
params["request"] = copy_safe_request(request, include_files=False)
|
params['request'] = copy_safe_request(event['request'], include_files=False)
|
||||||
|
|
||||||
# Enqueue the task
|
# Enqueue the task
|
||||||
rq_queue.enqueue(
|
rq_queue.enqueue('extras.webhooks.send_webhook', **params)
|
||||||
"extras.webhooks.send_webhook",
|
|
||||||
**params
|
|
||||||
)
|
|
||||||
|
|
||||||
# Scripts
|
# Scripts
|
||||||
elif event_rule.action_type == EventRuleActionChoices.SCRIPT:
|
elif event_rule.action_type == EventRuleActionChoices.SCRIPT:
|
||||||
# Resolve the script from action parameters
|
# Resolve the script from action parameters
|
||||||
script = event_rule.action_object.python_class()
|
script = event_rule.action_object.python_class()
|
||||||
|
|
||||||
# Retrieve the User if not already resolved
|
|
||||||
if user is None:
|
|
||||||
user = User.objects.get(username=username)
|
|
||||||
|
|
||||||
# Enqueue a Job to record the script's execution
|
# Enqueue a Job to record the script's execution
|
||||||
from extras.jobs import ScriptJob
|
from extras.jobs import ScriptJob
|
||||||
params = {
|
params = {
|
||||||
"instance": event_rule.action_object,
|
"instance": event_rule.action_object,
|
||||||
"name": script.name,
|
"name": script.name,
|
||||||
"user": user,
|
"user": event['user'],
|
||||||
"data": event_data
|
"data": event_data
|
||||||
}
|
}
|
||||||
if snapshots:
|
if 'snapshots' in event:
|
||||||
params["snapshots"] = snapshots
|
params['snapshots'] = event['snapshots']
|
||||||
if request:
|
if 'request' in event:
|
||||||
params["request"] = copy_safe_request(request)
|
params['request'] = copy_safe_request(event['request'])
|
||||||
ScriptJob.enqueue(
|
|
||||||
**params
|
# Enqueue the job
|
||||||
)
|
ScriptJob.enqueue(**params)
|
||||||
|
|
||||||
# Notification groups
|
# Notification groups
|
||||||
elif event_rule.action_type == EventRuleActionChoices.NOTIFICATION:
|
elif event_rule.action_type == EventRuleActionChoices.NOTIFICATION:
|
||||||
@@ -161,7 +171,7 @@ def process_event_rules(event_rules, object_type, event_type, data, username=Non
|
|||||||
object_type=object_type,
|
object_type=object_type,
|
||||||
object_id=event_data['id'],
|
object_id=event_data['id'],
|
||||||
object_repr=event_data.get('display'),
|
object_repr=event_data.get('display'),
|
||||||
event_type=event_type
|
event_type=event['event_type']
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -173,6 +183,8 @@ def process_event_rules(event_rules, object_type, event_type, data, username=Non
|
|||||||
def process_event_queue(events):
|
def process_event_queue(events):
|
||||||
"""
|
"""
|
||||||
Flush a list of object representation to RQ for EventRule processing.
|
Flush a list of object representation to RQ for EventRule processing.
|
||||||
|
|
||||||
|
This is the default processor listed in EVENTS_PIPELINE.
|
||||||
"""
|
"""
|
||||||
events_cache = defaultdict(dict)
|
events_cache = defaultdict(dict)
|
||||||
|
|
||||||
@@ -192,11 +204,7 @@ def process_event_queue(events):
|
|||||||
process_event_rules(
|
process_event_rules(
|
||||||
event_rules=event_rules,
|
event_rules=event_rules,
|
||||||
object_type=object_type,
|
object_type=object_type,
|
||||||
event_type=event['event_type'],
|
event=event,
|
||||||
data=event['data'],
|
|
||||||
username=event['username'],
|
|
||||||
snapshots=event['snapshots'],
|
|
||||||
request=event['request'],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,13 +7,12 @@ from extras.choices import *
|
|||||||
from extras.models import *
|
from extras.models import *
|
||||||
from netbox.events import get_event_type_choices
|
from netbox.events import get_event_type_choices
|
||||||
from netbox.forms import NetBoxModelFilterSetForm, PrimaryModelFilterSetForm
|
from netbox.forms import NetBoxModelFilterSetForm, PrimaryModelFilterSetForm
|
||||||
from netbox.forms.mixins import SavedFiltersMixin
|
from netbox.forms.mixins import OwnerFilterMixin, SavedFiltersMixin
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from users.models import Group, Owner, User
|
from users.models import Group, User
|
||||||
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
|
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
|
||||||
from utilities.forms.fields import (
|
from utilities.forms.fields import (
|
||||||
ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField,
|
||||||
TagFilterField,
|
|
||||||
)
|
)
|
||||||
from utilities.forms.rendering import FieldSet
|
from utilities.forms.rendering import FieldSet
|
||||||
from utilities.forms.widgets import DateTimePicker
|
from utilities.forms.widgets import DateTimePicker
|
||||||
@@ -39,7 +38,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
|
class CustomFieldFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm):
|
||||||
model = CustomField
|
model = CustomField
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id'),
|
FieldSet('q', 'filter_id'),
|
||||||
@@ -47,6 +46,7 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
FieldSet('choice_set_id', 'related_object_type_id', name=_('Type Options')),
|
FieldSet('choice_set_id', 'related_object_type_id', name=_('Type Options')),
|
||||||
FieldSet('ui_visible', 'ui_editable', 'is_cloneable', name=_('Behavior')),
|
FieldSet('ui_visible', 'ui_editable', 'is_cloneable', name=_('Behavior')),
|
||||||
FieldSet('validation_minimum', 'validation_maximum', 'validation_regex', name=_('Validation')),
|
FieldSet('validation_minimum', 'validation_maximum', 'validation_regex', name=_('Validation')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
object_type_id = ContentTypeMultipleChoiceField(
|
object_type_id = ContentTypeMultipleChoiceField(
|
||||||
queryset=ObjectType.objects.with_feature('custom_fields'),
|
queryset=ObjectType.objects.with_feature('custom_fields'),
|
||||||
@@ -119,18 +119,14 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
label=_('Validation regex'),
|
label=_('Validation regex'),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
owner_id = DynamicModelChoiceField(
|
|
||||||
queryset=Owner.objects.all(),
|
|
||||||
required=False,
|
|
||||||
label=_('Owner'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm):
|
class CustomFieldChoiceSetFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm):
|
||||||
model = CustomFieldChoiceSet
|
model = CustomFieldChoiceSet
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id'),
|
FieldSet('q', 'filter_id'),
|
||||||
FieldSet('base_choices', 'choice', name=_('Choices')),
|
FieldSet('base_choices', 'choice', name=_('Choices')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
base_choices = forms.MultipleChoiceField(
|
base_choices = forms.MultipleChoiceField(
|
||||||
choices=CustomFieldChoiceSetBaseChoices,
|
choices=CustomFieldChoiceSetBaseChoices,
|
||||||
@@ -139,18 +135,14 @@ class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
choice = forms.CharField(
|
choice = forms.CharField(
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
owner_id = DynamicModelChoiceField(
|
|
||||||
queryset=Owner.objects.all(),
|
|
||||||
required=False,
|
|
||||||
label=_('Owner'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CustomLinkFilterForm(SavedFiltersMixin, FilterForm):
|
class CustomLinkFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm):
|
||||||
model = CustomLink
|
model = CustomLink
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id'),
|
FieldSet('q', 'filter_id'),
|
||||||
FieldSet('object_type_id', 'enabled', 'new_window', 'weight', name=_('Attributes')),
|
FieldSet('object_type_id', 'enabled', 'new_window', 'weight', name=_('Attributes')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
object_type_id = ContentTypeMultipleChoiceField(
|
object_type_id = ContentTypeMultipleChoiceField(
|
||||||
label=_('Object types'),
|
label=_('Object types'),
|
||||||
@@ -175,19 +167,15 @@ class CustomLinkFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
label=_('Weight'),
|
label=_('Weight'),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
owner_id = DynamicModelChoiceField(
|
|
||||||
queryset=Owner.objects.all(),
|
|
||||||
required=False,
|
|
||||||
label=_('Owner'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
|
class ExportTemplateFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm):
|
||||||
model = ExportTemplate
|
model = ExportTemplate
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'object_type_id'),
|
FieldSet('q', 'filter_id', 'object_type_id'),
|
||||||
FieldSet('data_source_id', 'data_file_id', name=_('Data')),
|
FieldSet('data_source_id', 'data_file_id', name=_('Data')),
|
||||||
FieldSet('mime_type', 'file_name', 'file_extension', 'as_attachment', name=_('Rendering')),
|
FieldSet('mime_type', 'file_name', 'file_extension', 'as_attachment', name=_('Rendering')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
data_source_id = DynamicModelMultipleChoiceField(
|
data_source_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=DataSource.objects.all(),
|
queryset=DataSource.objects.all(),
|
||||||
@@ -226,11 +214,6 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
owner_id = DynamicModelChoiceField(
|
|
||||||
queryset=Owner.objects.all(),
|
|
||||||
required=False,
|
|
||||||
label=_('Owner'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
|
class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
|
||||||
@@ -250,11 +233,12 @@ class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
|
class SavedFilterFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm):
|
||||||
model = SavedFilter
|
model = SavedFilter
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id'),
|
FieldSet('q', 'filter_id'),
|
||||||
FieldSet('object_type_id', 'enabled', 'shared', 'weight', name=_('Attributes')),
|
FieldSet('object_type_id', 'enabled', 'shared', 'weight', name=_('Attributes')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
object_type_id = ContentTypeMultipleChoiceField(
|
object_type_id = ContentTypeMultipleChoiceField(
|
||||||
label=_('Object types'),
|
label=_('Object types'),
|
||||||
@@ -279,11 +263,6 @@ class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
label=_('Weight'),
|
label=_('Weight'),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
owner_id = DynamicModelChoiceField(
|
|
||||||
queryset=Owner.objects.all(),
|
|
||||||
required=False,
|
|
||||||
label=_('Owner'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TableConfigFilterForm(SavedFiltersMixin, FilterForm):
|
class TableConfigFilterForm(SavedFiltersMixin, FilterForm):
|
||||||
@@ -317,11 +296,12 @@ class TableConfigFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class WebhookFilterForm(NetBoxModelFilterSetForm):
|
class WebhookFilterForm(OwnerFilterMixin, NetBoxModelFilterSetForm):
|
||||||
model = Webhook
|
model = Webhook
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('payload_url', 'http_method', 'http_content_type', name=_('Attributes')),
|
FieldSet('payload_url', 'http_method', 'http_content_type', name=_('Attributes')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
http_content_type = forms.CharField(
|
http_content_type = forms.CharField(
|
||||||
label=_('HTTP content type'),
|
label=_('HTTP content type'),
|
||||||
@@ -336,19 +316,15 @@ class WebhookFilterForm(NetBoxModelFilterSetForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label=_('HTTP method')
|
label=_('HTTP method')
|
||||||
)
|
)
|
||||||
owner_id = DynamicModelChoiceField(
|
|
||||||
queryset=Owner.objects.all(),
|
|
||||||
required=False,
|
|
||||||
label=_('Owner'),
|
|
||||||
)
|
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
class EventRuleFilterForm(NetBoxModelFilterSetForm):
|
class EventRuleFilterForm(OwnerFilterMixin, NetBoxModelFilterSetForm):
|
||||||
model = EventRule
|
model = EventRule
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('object_type_id', 'event_type', 'action_type', 'enabled', name=_('Attributes')),
|
FieldSet('object_type_id', 'event_type', 'action_type', 'enabled', name=_('Attributes')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
object_type_id = ContentTypeMultipleChoiceField(
|
object_type_id = ContentTypeMultipleChoiceField(
|
||||||
queryset=ObjectType.objects.with_feature('event_rules'),
|
queryset=ObjectType.objects.with_feature('event_rules'),
|
||||||
@@ -372,16 +348,16 @@ class EventRuleFilterForm(NetBoxModelFilterSetForm):
|
|||||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
owner_id = DynamicModelChoiceField(
|
|
||||||
queryset=Owner.objects.all(),
|
|
||||||
required=False,
|
|
||||||
label=_('Owner'),
|
|
||||||
)
|
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
class TagFilterForm(SavedFiltersMixin, FilterForm):
|
class TagFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm):
|
||||||
model = Tag
|
model = Tag
|
||||||
|
fieldsets = (
|
||||||
|
FieldSet('q', 'filter_id'),
|
||||||
|
FieldSet('content_type_id', 'for_object_type_id', name=_('Attributes')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
|
)
|
||||||
content_type_id = ContentTypeMultipleChoiceField(
|
content_type_id = ContentTypeMultipleChoiceField(
|
||||||
queryset=ObjectType.objects.with_feature('tags'),
|
queryset=ObjectType.objects.with_feature('tags'),
|
||||||
required=False,
|
required=False,
|
||||||
@@ -392,11 +368,6 @@ class TagFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label=_('Allowed object type')
|
label=_('Allowed object type')
|
||||||
)
|
)
|
||||||
owner_id = DynamicModelChoiceField(
|
|
||||||
queryset=Owner.objects.all(),
|
|
||||||
required=False,
|
|
||||||
label=_('Owner'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigContextProfileFilterForm(PrimaryModelFilterSetForm):
|
class ConfigContextProfileFilterForm(PrimaryModelFilterSetForm):
|
||||||
@@ -404,6 +375,7 @@ class ConfigContextProfileFilterForm(PrimaryModelFilterSetForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id'),
|
FieldSet('q', 'filter_id'),
|
||||||
FieldSet('data_source_id', 'data_file_id', name=_('Data')),
|
FieldSet('data_source_id', 'data_file_id', name=_('Data')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
data_source_id = DynamicModelMultipleChoiceField(
|
data_source_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=DataSource.objects.all(),
|
queryset=DataSource.objects.all(),
|
||||||
@@ -420,16 +392,17 @@ class ConfigContextProfileFilterForm(PrimaryModelFilterSetForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ConfigContextFilterForm(SavedFiltersMixin, FilterForm):
|
class ConfigContextFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm):
|
||||||
model = ConfigContext
|
model = ConfigContext
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag_id'),
|
FieldSet('q', 'filter_id', 'tag_id'),
|
||||||
FieldSet('profile', name=_('Config Context')),
|
FieldSet('profile_id', name=_('Config Context')),
|
||||||
FieldSet('data_source_id', 'data_file_id', name=_('Data')),
|
FieldSet('data_source_id', 'data_file_id', name=_('Data')),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')),
|
||||||
FieldSet('device_type_id', 'platform_id', 'device_role_id', name=_('Device')),
|
FieldSet('device_type_id', 'platform_id', 'device_role_id', name=_('Device')),
|
||||||
FieldSet('cluster_type_id', 'cluster_group_id', 'cluster_id', name=_('Cluster')),
|
FieldSet('cluster_type_id', 'cluster_group_id', 'cluster_id', name=_('Cluster')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant'))
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
profile_id = DynamicModelMultipleChoiceField(
|
profile_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=ConfigContextProfile.objects.all(),
|
queryset=ConfigContextProfile.objects.all(),
|
||||||
@@ -514,19 +487,15 @@ class ConfigContextFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label=_('Tags')
|
label=_('Tags')
|
||||||
)
|
)
|
||||||
owner_id = DynamicModelChoiceField(
|
|
||||||
queryset=Owner.objects.all(),
|
|
||||||
required=False,
|
|
||||||
label=_('Owner'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigTemplateFilterForm(SavedFiltersMixin, FilterForm):
|
class ConfigTemplateFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm):
|
||||||
model = ConfigTemplate
|
model = ConfigTemplate
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('data_source_id', 'data_file_id', 'auto_sync_enabled', name=_('Data')),
|
FieldSet('data_source_id', 'data_file_id', 'auto_sync_enabled', name=_('Data')),
|
||||||
FieldSet('mime_type', 'file_name', 'file_extension', 'as_attachment', name=_('Rendering'))
|
FieldSet('mime_type', 'file_name', 'file_extension', 'as_attachment', name=_('Rendering')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
data_source_id = DynamicModelMultipleChoiceField(
|
data_source_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=DataSource.objects.all(),
|
queryset=DataSource.objects.all(),
|
||||||
@@ -568,11 +537,6 @@ class ConfigTemplateFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
owner_id = DynamicModelChoiceField(
|
|
||||||
queryset=Owner.objects.all(),
|
|
||||||
required=False,
|
|
||||||
label=_('Owner'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class LocalConfigContextFilterForm(forms.Form):
|
class LocalConfigContextFilterForm(forms.Form):
|
||||||
|
|||||||
@@ -178,6 +178,13 @@ class CustomFieldChoiceSetForm(ChangelogMessageMixin, OwnerMixin, forms.ModelFor
|
|||||||
) + ' <code>choice1:First Choice</code>')
|
) + ' <code>choice1:First Choice</code>')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
FieldSet(
|
||||||
|
'name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically',
|
||||||
|
name=_('Custom Field Choice Set')
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CustomFieldChoiceSet
|
model = CustomFieldChoiceSet
|
||||||
fields = ('name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically', 'owner')
|
fields = ('name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically', 'owner')
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ class ScriptModule(PythonModuleMixin, JobsMixin, ManagedFile):
|
|||||||
module = self.get_module()
|
module = self.get_module()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.error = e
|
self.error = e
|
||||||
logger.debug(f"Failed to load script: {self.python_name} error: {e}")
|
logger.error(f"Failed to load script: {self.python_name} error: {e}")
|
||||||
module = None
|
module = None
|
||||||
|
|
||||||
scripts = {}
|
scripts = {}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class ScriptVariable:
|
|||||||
self.field_attrs['label'] = label
|
self.field_attrs['label'] = label
|
||||||
if description:
|
if description:
|
||||||
self.field_attrs['help_text'] = description
|
self.field_attrs['help_text'] = description
|
||||||
if default:
|
if default is not None:
|
||||||
self.field_attrs['initial'] = default
|
self.field_attrs['initial'] = default
|
||||||
if widget:
|
if widget:
|
||||||
self.field_attrs['widget'] = widget
|
self.field_attrs['widget'] = widget
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ from django.dispatch import receiver
|
|||||||
|
|
||||||
from core.events import *
|
from core.events import *
|
||||||
from core.signals import job_end, job_start
|
from core.signals import job_end, job_start
|
||||||
from extras.events import process_event_rules
|
from extras.events import EventContext, process_event_rules
|
||||||
from extras.models import EventRule, Notification, Subscription
|
from extras.models import EventRule, Notification, Subscription
|
||||||
from netbox.config import get_config
|
from netbox.config import get_config
|
||||||
from netbox.models.features import has_feature
|
from netbox.models.features import has_feature
|
||||||
from netbox.signals import post_clean
|
from netbox.signals import post_clean
|
||||||
|
from utilities.data import get_config_value_ci
|
||||||
from utilities.exceptions import AbortRequest
|
from utilities.exceptions import AbortRequest
|
||||||
from .models import CustomField, TaggedItem
|
from .models import CustomField, TaggedItem
|
||||||
from .utils import run_validators
|
from .utils import run_validators
|
||||||
@@ -65,7 +66,7 @@ def run_save_validators(sender, instance, **kwargs):
|
|||||||
Run any custom validation rules for the model prior to calling save().
|
Run any custom validation rules for the model prior to calling save().
|
||||||
"""
|
"""
|
||||||
model_name = f'{sender._meta.app_label}.{sender._meta.model_name}'
|
model_name = f'{sender._meta.app_label}.{sender._meta.model_name}'
|
||||||
validators = get_config().CUSTOM_VALIDATORS.get(model_name, [])
|
validators = get_config_value_ci(get_config().CUSTOM_VALIDATORS, model_name, default=[])
|
||||||
|
|
||||||
run_validators(instance, validators)
|
run_validators(instance, validators)
|
||||||
|
|
||||||
@@ -102,14 +103,12 @@ def process_job_start_event_rules(sender, **kwargs):
|
|||||||
enabled=True,
|
enabled=True,
|
||||||
object_types=sender.object_type
|
object_types=sender.object_type
|
||||||
)
|
)
|
||||||
username = sender.user.username if sender.user else None
|
event = EventContext(
|
||||||
process_event_rules(
|
|
||||||
event_rules=event_rules,
|
|
||||||
object_type=sender.object_type,
|
|
||||||
event_type=JOB_STARTED,
|
event_type=JOB_STARTED,
|
||||||
data=sender.data,
|
data=sender.data,
|
||||||
username=username
|
user=sender.user,
|
||||||
)
|
)
|
||||||
|
process_event_rules(event_rules, sender.object_type, event)
|
||||||
|
|
||||||
|
|
||||||
@receiver(job_end)
|
@receiver(job_end)
|
||||||
@@ -122,14 +121,12 @@ def process_job_end_event_rules(sender, **kwargs):
|
|||||||
enabled=True,
|
enabled=True,
|
||||||
object_types=sender.object_type
|
object_types=sender.object_type
|
||||||
)
|
)
|
||||||
username = sender.user.username if sender.user else None
|
event = EventContext(
|
||||||
process_event_rules(
|
|
||||||
event_rules=event_rules,
|
|
||||||
object_type=sender.object_type,
|
|
||||||
event_type=JOB_COMPLETED,
|
event_type=JOB_COMPLETED,
|
||||||
data=sender.data,
|
data=sender.data,
|
||||||
username=username
|
user=sender.user,
|
||||||
)
|
)
|
||||||
|
process_event_rules(event_rules, sender.object_type, event)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.test import tag
|
from django.test import tag
|
||||||
|
from unittest.mock import patch, PropertyMock
|
||||||
|
|
||||||
from core.choices import ManagedFileRootPathChoices
|
from core.choices import ManagedFileRootPathChoices
|
||||||
from core.events import *
|
from core.events import *
|
||||||
@@ -906,7 +907,7 @@ class ScriptValidationErrorTest(TestCase):
|
|||||||
user_permissions = ['extras.view_script', 'extras.run_script']
|
user_permissions = ['extras.view_script', 'extras.run_script']
|
||||||
|
|
||||||
class TestScriptMixin:
|
class TestScriptMixin:
|
||||||
bar = IntegerVar(min_value=0, max_value=30, default=30)
|
bar = IntegerVar(min_value=0, max_value=30)
|
||||||
|
|
||||||
class TestScriptClass(TestScriptMixin, PythonClass):
|
class TestScriptClass(TestScriptMixin, PythonClass):
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -930,8 +931,6 @@ class ScriptValidationErrorTest(TestCase):
|
|||||||
|
|
||||||
@tag('regression')
|
@tag('regression')
|
||||||
def test_script_validation_error_displays_message(self):
|
def test_script_validation_error_displays_message(self):
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
url = reverse('extras:script', kwargs={'pk': self.script.pk})
|
url = reverse('extras:script', kwargs={'pk': self.script.pk})
|
||||||
|
|
||||||
with patch('extras.views.get_workers_for_queue', return_value=['worker']):
|
with patch('extras.views.get_workers_for_queue', return_value=['worker']):
|
||||||
@@ -944,8 +943,6 @@ class ScriptValidationErrorTest(TestCase):
|
|||||||
|
|
||||||
@tag('regression')
|
@tag('regression')
|
||||||
def test_script_validation_error_no_toast_for_fieldset_fields(self):
|
def test_script_validation_error_no_toast_for_fieldset_fields(self):
|
||||||
from unittest.mock import patch, PropertyMock
|
|
||||||
|
|
||||||
class FieldsetScript(PythonClass):
|
class FieldsetScript(PythonClass):
|
||||||
class Meta:
|
class Meta:
|
||||||
name = 'Fieldset test'
|
name = 'Fieldset test'
|
||||||
@@ -967,3 +964,42 @@ class ScriptValidationErrorTest(TestCase):
|
|||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
messages = list(response.context['messages'])
|
messages = list(response.context['messages'])
|
||||||
self.assertEqual(len(messages), 0)
|
self.assertEqual(len(messages), 0)
|
||||||
|
|
||||||
|
|
||||||
|
class ScriptDefaultValuesTest(TestCase):
|
||||||
|
user_permissions = ['extras.view_script', 'extras.run_script']
|
||||||
|
|
||||||
|
class TestScriptClass(PythonClass):
|
||||||
|
class Meta:
|
||||||
|
name = 'Test script'
|
||||||
|
commit_default = False
|
||||||
|
|
||||||
|
bool_default_true = BooleanVar(default=True)
|
||||||
|
bool_default_false = BooleanVar(default=False)
|
||||||
|
int_with_default = IntegerVar(default=0)
|
||||||
|
int_without_default = IntegerVar(required=False)
|
||||||
|
|
||||||
|
def run(self, data, commit):
|
||||||
|
return "Complete"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
module = ScriptModule.objects.create(file_root=ManagedFileRootPathChoices.SCRIPTS, file_path='test_script.py')
|
||||||
|
cls.script = Script.objects.create(module=module, name='Test script', is_executable=True)
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
Script.python_class = property(lambda self: ScriptDefaultValuesTest.TestScriptClass)
|
||||||
|
|
||||||
|
def test_default_values_are_used(self):
|
||||||
|
url = reverse('extras:script', kwargs={'pk': self.script.pk})
|
||||||
|
|
||||||
|
with patch('extras.views.get_workers_for_queue', return_value=['worker']):
|
||||||
|
with patch('extras.jobs.ScriptJob.enqueue') as mock_enqueue:
|
||||||
|
mock_enqueue.return_value.pk = 1
|
||||||
|
self.client.post(url, {})
|
||||||
|
call_kwargs = mock_enqueue.call_args.kwargs
|
||||||
|
self.assertEqual(call_kwargs['data']['bool_default_true'], True)
|
||||||
|
self.assertEqual(call_kwargs['data']['bool_default_false'], False)
|
||||||
|
self.assertEqual(call_kwargs['data']['int_with_default'], 0)
|
||||||
|
self.assertIsNone(call_kwargs['data']['int_without_default'])
|
||||||
|
|||||||
@@ -1511,7 +1511,13 @@ class ScriptView(BaseScriptView):
|
|||||||
'script': script,
|
'script': script,
|
||||||
})
|
})
|
||||||
|
|
||||||
form = script_class.as_form(request.POST, request.FILES)
|
# Populate missing variables with their default values, if defined
|
||||||
|
post_data = request.POST.copy()
|
||||||
|
for name, var in script_class._get_vars().items():
|
||||||
|
if name not in post_data and (initial := var.field_attrs.get('initial')) is not None:
|
||||||
|
post_data[name] = initial
|
||||||
|
|
||||||
|
form = script_class.as_form(post_data, request.FILES)
|
||||||
|
|
||||||
# Allow execution only if RQ worker process is running
|
# Allow execution only if RQ worker process is running
|
||||||
if not get_workers_for_queue('default'):
|
if not get_workers_for_queue('default'):
|
||||||
|
|||||||
@@ -45,9 +45,10 @@ IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice([
|
|||||||
class VRFFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
class VRFFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = VRF
|
model = VRF
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('import_target_id', 'export_target_id', name=_('Route Targets')),
|
FieldSet('import_target_id', 'export_target_id', name=_('Route Targets')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
import_target_id = DynamicModelMultipleChoiceField(
|
import_target_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=RouteTarget.objects.all(),
|
queryset=RouteTarget.objects.all(),
|
||||||
@@ -65,9 +66,10 @@ class VRFFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
|||||||
class RouteTargetFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
class RouteTargetFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = RouteTarget
|
model = RouteTarget
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('importing_vrf_id', 'exporting_vrf_id', name=_('VRF')),
|
FieldSet('importing_vrf_id', 'exporting_vrf_id', name=_('VRF')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
importing_vrf_id = DynamicModelMultipleChoiceField(
|
importing_vrf_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=VRF.objects.all(),
|
queryset=VRF.objects.all(),
|
||||||
@@ -85,8 +87,9 @@ class RouteTargetFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
|||||||
class RIRFilterForm(OrganizationalModelFilterSetForm):
|
class RIRFilterForm(OrganizationalModelFilterSetForm):
|
||||||
model = RIR
|
model = RIR
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('is_private', name=_('RIR')),
|
FieldSet('is_private', name=_('RIR')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
is_private = forms.NullBooleanField(
|
is_private = forms.NullBooleanField(
|
||||||
required=False,
|
required=False,
|
||||||
@@ -101,9 +104,10 @@ class RIRFilterForm(OrganizationalModelFilterSetForm):
|
|||||||
class AggregateFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
|
class AggregateFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = Aggregate
|
model = Aggregate
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('family', 'rir_id', name=_('Attributes')),
|
FieldSet('family', 'rir_id', name=_('Attributes')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||||
)
|
)
|
||||||
family = forms.ChoiceField(
|
family = forms.ChoiceField(
|
||||||
@@ -122,9 +126,10 @@ class AggregateFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryMode
|
|||||||
class ASNRangeFilterForm(TenancyFilterForm, OrganizationalModelFilterSetForm):
|
class ASNRangeFilterForm(TenancyFilterForm, OrganizationalModelFilterSetForm):
|
||||||
model = ASNRange
|
model = ASNRange
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('rir_id', 'start', 'end', name=_('Range')),
|
FieldSet('rir_id', 'start', 'end', name=_('Range')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
rir_id = DynamicModelMultipleChoiceField(
|
rir_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=RIR.objects.all(),
|
queryset=RIR.objects.all(),
|
||||||
@@ -145,9 +150,10 @@ class ASNRangeFilterForm(TenancyFilterForm, OrganizationalModelFilterSetForm):
|
|||||||
class ASNFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
class ASNFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = ASN
|
model = ASN
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('rir_id', 'site_group_id', 'site_id', name=_('Assignment')),
|
FieldSet('rir_id', 'site_group_id', 'site_id', name=_('Assignment')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
rir_id = DynamicModelMultipleChoiceField(
|
rir_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=RIR.objects.all(),
|
queryset=RIR.objects.all(),
|
||||||
@@ -170,7 +176,8 @@ class ASNFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
|||||||
class RoleFilterForm(OrganizationalModelFilterSetForm):
|
class RoleFilterForm(OrganizationalModelFilterSetForm):
|
||||||
model = Role
|
model = Role
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
@@ -178,7 +185,7 @@ class RoleFilterForm(OrganizationalModelFilterSetForm):
|
|||||||
class PrefixFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
|
class PrefixFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = Prefix
|
model = Prefix
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'within_include', 'family', 'status', 'role_id', 'mask_length', 'is_pool', 'mark_utilized',
|
'within_include', 'family', 'status', 'role_id', 'mask_length', 'is_pool', 'mark_utilized',
|
||||||
name=_('Addressing')
|
name=_('Addressing')
|
||||||
@@ -187,6 +194,7 @@ class PrefixFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFi
|
|||||||
FieldSet('vrf_id', 'present_in_vrf_id', name=_('VRF')),
|
FieldSet('vrf_id', 'present_in_vrf_id', name=_('VRF')),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Scope')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Scope')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||||
)
|
)
|
||||||
mask_length__lte = forms.IntegerField(
|
mask_length__lte = forms.IntegerField(
|
||||||
@@ -284,9 +292,10 @@ class PrefixFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFi
|
|||||||
class IPRangeFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
|
class IPRangeFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = IPRange
|
model = IPRange
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('family', 'vrf_id', 'status', 'role_id', 'mark_populated', 'mark_utilized', name=_('Attributes')),
|
FieldSet('family', 'vrf_id', 'status', 'role_id', 'mark_populated', 'mark_utilized', name=_('Attributes')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||||
)
|
)
|
||||||
family = forms.ChoiceField(
|
family = forms.ChoiceField(
|
||||||
@@ -331,14 +340,15 @@ class IPRangeFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelF
|
|||||||
class IPAddressFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
|
class IPAddressFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface', 'dns_name',
|
'parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface', 'dns_name',
|
||||||
name=_('Attributes')
|
name=_('Attributes')
|
||||||
),
|
),
|
||||||
FieldSet('vrf_id', 'present_in_vrf_id', name=_('VRF')),
|
FieldSet('vrf_id', 'present_in_vrf_id', name=_('VRF')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
|
||||||
FieldSet('device_id', 'virtual_machine_id', name=_('Device/VM')),
|
FieldSet('device_id', 'virtual_machine_id', name=_('Device/VM')),
|
||||||
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||||
)
|
)
|
||||||
selector_fields = ('filter_id', 'q', 'region_id', 'group_id', 'parent', 'status', 'role')
|
selector_fields = ('filter_id', 'q', 'region_id', 'group_id', 'parent', 'status', 'role')
|
||||||
@@ -409,9 +419,10 @@ class IPAddressFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryMode
|
|||||||
class FHRPGroupFilterForm(PrimaryModelFilterSetForm):
|
class FHRPGroupFilterForm(PrimaryModelFilterSetForm):
|
||||||
model = FHRPGroup
|
model = FHRPGroup
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('name', 'protocol', 'group_id', name=_('Attributes')),
|
FieldSet('name', 'protocol', 'group_id', name=_('Attributes')),
|
||||||
FieldSet('auth_type', 'auth_key', name=_('Authentication')),
|
FieldSet('auth_type', 'auth_key', name=_('Authentication')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
name = forms.CharField(
|
name = forms.CharField(
|
||||||
label=_('Name'),
|
label=_('Name'),
|
||||||
@@ -441,11 +452,12 @@ class FHRPGroupFilterForm(PrimaryModelFilterSetForm):
|
|||||||
|
|
||||||
class VLANGroupFilterForm(TenancyFilterForm, OrganizationalModelFilterSetForm):
|
class VLANGroupFilterForm(TenancyFilterForm, OrganizationalModelFilterSetForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('region', 'site_group', 'site', 'location', 'rack', name=_('Location')),
|
FieldSet('region', 'site_group', 'site', 'location', 'rack', name=_('Location')),
|
||||||
FieldSet('cluster_group', 'cluster', name=_('Cluster')),
|
FieldSet('cluster_group', 'cluster', name=_('Cluster')),
|
||||||
FieldSet('contains_vid', name=_('VLANs')),
|
FieldSet('contains_vid', name=_('VLANs')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
model = VLANGroup
|
model = VLANGroup
|
||||||
region = DynamicModelMultipleChoiceField(
|
region = DynamicModelMultipleChoiceField(
|
||||||
@@ -495,8 +507,9 @@ class VLANGroupFilterForm(TenancyFilterForm, OrganizationalModelFilterSetForm):
|
|||||||
class VLANTranslationPolicyFilterForm(PrimaryModelFilterSetForm):
|
class VLANTranslationPolicyFilterForm(PrimaryModelFilterSetForm):
|
||||||
model = VLANTranslationPolicy
|
model = VLANTranslationPolicy
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('name', name=_('Attributes')),
|
FieldSet('name', name=_('Attributes')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
name = forms.CharField(
|
name = forms.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
@@ -532,11 +545,12 @@ class VLANTranslationRuleFilterForm(NetBoxModelFilterSetForm):
|
|||||||
class VLANFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
class VLANFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = VLAN
|
model = VLAN
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
|
||||||
FieldSet('group_id', 'status', 'role_id', 'vid', 'l2vpn_id', name=_('Attributes')),
|
FieldSet('group_id', 'status', 'role_id', 'vid', 'l2vpn_id', name=_('Attributes')),
|
||||||
FieldSet('qinq_role', 'qinq_svlan_id', name=_('Q-in-Q/802.1ad')),
|
FieldSet('qinq_role', 'qinq_svlan_id', name=_('Q-in-Q/802.1ad')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
selector_fields = ('filter_id', 'q', 'group_id')
|
selector_fields = ('filter_id', 'q', 'group_id')
|
||||||
region_id = DynamicModelMultipleChoiceField(
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
@@ -604,8 +618,9 @@ class VLANFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
|||||||
class ServiceTemplateFilterForm(PrimaryModelFilterSetForm):
|
class ServiceTemplateFilterForm(PrimaryModelFilterSetForm):
|
||||||
model = ServiceTemplate
|
model = ServiceTemplate
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('protocol', 'port', name=_('Attributes')),
|
FieldSet('protocol', 'port', name=_('Attributes')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
protocol = forms.ChoiceField(
|
protocol = forms.ChoiceField(
|
||||||
label=_('Protocol'),
|
label=_('Protocol'),
|
||||||
@@ -622,9 +637,10 @@ class ServiceTemplateFilterForm(PrimaryModelFilterSetForm):
|
|||||||
class ServiceFilterForm(ContactModelFilterForm, ServiceTemplateFilterForm):
|
class ServiceFilterForm(ContactModelFilterForm, ServiceTemplateFilterForm):
|
||||||
model = Service
|
model = Service
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('protocol', 'port', name=_('Attributes')),
|
FieldSet('protocol', 'port', name=_('Attributes')),
|
||||||
FieldSet('device_id', 'virtual_machine_id', 'fhrpgroup_id', name=_('Assignment')),
|
FieldSet('device_id', 'virtual_machine_id', 'fhrpgroup_id', name=_('Assignment')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||||
)
|
)
|
||||||
device_id = DynamicModelMultipleChoiceField(
|
device_id = DynamicModelMultipleChoiceField(
|
||||||
|
|||||||
@@ -370,6 +370,11 @@ class AnnotatedIPAddressTable(IPAddressTable):
|
|||||||
verbose_name=_('IP Address')
|
verbose_name=_('IP Address')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def render_pk(self, value, record, bound_column):
|
||||||
|
if type(record) is not self._meta.model:
|
||||||
|
return ''
|
||||||
|
return bound_column.column.render(value, bound_column, record)
|
||||||
|
|
||||||
class Meta(IPAddressTable.Meta):
|
class Meta(IPAddressTable.Meta):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ PREFIX_LINK = """
|
|||||||
{% if record.pk %}
|
{% if record.pk %}
|
||||||
<a href="{{ record.get_absolute_url }}" id="prefix_{{ record.pk }}">{{ record.prefix }}</a>
|
<a href="{{ record.get_absolute_url }}" id="prefix_{{ record.pk }}">{{ record.prefix }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{% url 'ipam:prefix_add' %}?prefix={{ record }}{% if object.vrf %}&vrf={{ object.vrf.pk }}{% endif %}{% if object.site %}&site={{ object.site.pk }}{% endif %}{% if object.tenant %}&tenant_group={{ object.tenant.group.pk }}&tenant={{ object.tenant.pk }}{% endif %}">{{ record.prefix }}</a>
|
<a href="{% url 'ipam:prefix_add' %}?prefix={{ record }}{% if object.vrf %}&vrf={{ object.vrf.pk }}{% endif %}{% if object.scope %}&scope_type={{ object.scope_type.pk }}&scope={{ object.scope.pk }}{% endif %}{% if object.tenant %}&tenant_group={{ object.tenant.group.pk }}&tenant={{ object.tenant.pk }}{% endif %}">{{ record.prefix }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from django_tables2.utils import Accessor
|
from django_tables2.utils import Accessor
|
||||||
|
|
||||||
from dcim.models import Interface
|
from dcim.models import Interface
|
||||||
|
from dcim.tables.template_code import INTERFACE_LINKTERMINATION, LINKTERMINATION
|
||||||
from ipam.models import *
|
from ipam.models import *
|
||||||
from netbox.tables import NetBoxTable, OrganizationalModelTable, PrimaryModelTable, columns
|
from netbox.tables import NetBoxTable, OrganizationalModelTable, PrimaryModelTable, columns
|
||||||
from tenancy.tables import TenancyColumnsMixin, TenantColumn
|
from tenancy.tables import TenancyColumnsMixin, TenantColumn
|
||||||
@@ -159,11 +160,26 @@ class VLANDevicesTable(VLANMembersTable):
|
|||||||
actions = columns.ActionsColumn(
|
actions = columns.ActionsColumn(
|
||||||
actions=('edit',)
|
actions=('edit',)
|
||||||
)
|
)
|
||||||
|
link_peer = columns.TemplateColumn(
|
||||||
|
accessor='link_peers',
|
||||||
|
template_code=LINKTERMINATION,
|
||||||
|
orderable=False,
|
||||||
|
verbose_name=_('Link Peers'),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Override PathEndpointTable.connection to accommodate virtual circuits
|
||||||
|
connection = columns.TemplateColumn(
|
||||||
|
accessor='_path__destinations',
|
||||||
|
template_code=INTERFACE_LINKTERMINATION,
|
||||||
|
orderable=False,
|
||||||
|
verbose_name=_('Connection'),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = ('device', 'name', 'tagged', 'actions')
|
fields = ('device', 'name', 'link_peer', 'connection', 'tagged', 'actions')
|
||||||
exclude = ('id', )
|
default_columns = ('device', 'name', 'connection', 'tagged', 'actions')
|
||||||
|
exclude = ('id',)
|
||||||
|
|
||||||
|
|
||||||
class VLANVirtualMachinesTable(VLANMembersTable):
|
class VLANVirtualMachinesTable(VLANMembersTable):
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
from django.test import RequestFactory, TestCase
|
||||||
|
from netaddr import IPNetwork
|
||||||
|
|
||||||
|
from ipam.models import IPAddress, IPRange, Prefix
|
||||||
|
from ipam.tables import AnnotatedIPAddressTable
|
||||||
|
from ipam.utils import annotate_ip_space
|
||||||
|
|
||||||
|
|
||||||
|
class AnnotatedIPAddressTableTest(TestCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
cls.prefix = Prefix.objects.create(
|
||||||
|
prefix=IPNetwork('10.1.1.0/24'),
|
||||||
|
status='active'
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.ip_address = IPAddress.objects.create(
|
||||||
|
address='10.1.1.1/24',
|
||||||
|
status='active'
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.ip_range = IPRange.objects.create(
|
||||||
|
start_address=IPNetwork('10.1.1.2/24'),
|
||||||
|
end_address=IPNetwork('10.1.1.10/24'),
|
||||||
|
status='active'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_ipaddress_has_checkbox_iprange_does_not(self):
|
||||||
|
data = annotate_ip_space(self.prefix)
|
||||||
|
table = AnnotatedIPAddressTable(data, orderable=False)
|
||||||
|
table.columns.show('pk')
|
||||||
|
|
||||||
|
request = RequestFactory().get('/')
|
||||||
|
html = table.as_html(request)
|
||||||
|
|
||||||
|
ipaddress_checkbox_count = html.count(f'name="pk" value="{self.ip_address.pk}"')
|
||||||
|
self.assertEqual(ipaddress_checkbox_count, 1)
|
||||||
|
|
||||||
|
iprange_checkbox_count = html.count(f'name="pk" value="{self.ip_range.pk}"')
|
||||||
|
self.assertEqual(iprange_checkbox_count, 0)
|
||||||
@@ -49,6 +49,9 @@ def add_requested_prefixes(parent, prefix_list, show_available=True, show_assign
|
|||||||
if prefix_list and show_available:
|
if prefix_list and show_available:
|
||||||
|
|
||||||
# Find all unallocated space, add fake Prefix objects to child_prefixes.
|
# Find all unallocated space, add fake Prefix objects to child_prefixes.
|
||||||
|
# IMPORTANT: These are unsaved Prefix instances (pk=None). If this is ever changed to use
|
||||||
|
# saved Prefix instances with real pks, bulk delete will fail for mixed-type selections
|
||||||
|
# due to single-model form validation. See: https://github.com/netbox-community/netbox/issues/21176
|
||||||
available_prefixes = netaddr.IPSet(parent) ^ netaddr.IPSet([p.prefix for p in prefix_list])
|
available_prefixes = netaddr.IPSet(parent) ^ netaddr.IPSet([p.prefix for p in prefix_list])
|
||||||
available_prefixes = [Prefix(prefix=p, status=None) for p in available_prefixes.iter_cidrs()]
|
available_prefixes = [Prefix(prefix=p, status=None) for p in available_prefixes.iter_cidrs()]
|
||||||
child_prefixes = child_prefixes + available_prefixes
|
child_prefixes = child_prefixes + available_prefixes
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
|
||||||
from rest_framework import serializers
|
|
||||||
from rest_framework.utils.serializer_helpers import BindingDict
|
|
||||||
from drf_spectacular.utils import extend_schema_field
|
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
|
from drf_spectacular.utils import extend_schema_field
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
from utilities.api import get_related_object_by_attrs
|
from utilities.api import get_related_object_by_attrs
|
||||||
from .fields import NetBoxAPIHyperlinkedIdentityField, NetBoxURLHyperlinkedIdentityField
|
from .fields import NetBoxAPIHyperlinkedIdentityField, NetBoxURLHyperlinkedIdentityField
|
||||||
@@ -19,16 +18,18 @@ class BaseModelSerializer(serializers.ModelSerializer):
|
|||||||
display_url = NetBoxURLHyperlinkedIdentityField()
|
display_url = NetBoxURLHyperlinkedIdentityField()
|
||||||
display = serializers.SerializerMethodField(read_only=True)
|
display = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
def __init__(self, *args, nested=False, fields=None, **kwargs):
|
def __init__(self, *args, nested=False, fields=None, omit=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Extends the base __init__() method to support dynamic fields.
|
Extends the base __init__() method to support dynamic fields.
|
||||||
|
|
||||||
:param nested: Set to True if this serializer is being employed within a parent serializer
|
:param nested: Set to True if this serializer is being employed within a parent serializer
|
||||||
:param fields: An iterable of fields to include when rendering the serialized object, If nested is
|
:param fields: An iterable of fields to include when rendering the serialized object, If nested is
|
||||||
True but no fields are specified, Meta.brief_fields will be used.
|
True but no fields are specified, Meta.brief_fields will be used.
|
||||||
|
:param omit: An iterable of fields to omit from the serialized object
|
||||||
"""
|
"""
|
||||||
self.nested = nested
|
self.nested = nested
|
||||||
self._requested_fields = fields
|
self._include_fields = fields or []
|
||||||
|
self._omit_fields = omit or []
|
||||||
|
|
||||||
# Disable validators for nested objects (which already exist)
|
# Disable validators for nested objects (which already exist)
|
||||||
if self.nested:
|
if self.nested:
|
||||||
@@ -36,8 +37,8 @@ class BaseModelSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
# If this serializer is nested but no fields have been specified,
|
# If this serializer is nested but no fields have been specified,
|
||||||
# default to using Meta.brief_fields (if set)
|
# default to using Meta.brief_fields (if set)
|
||||||
if self.nested and not fields:
|
if self.nested and not fields and not omit:
|
||||||
self._requested_fields = getattr(self.Meta, 'brief_fields', None)
|
self._include_fields = getattr(self.Meta, 'brief_fields', None)
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
@@ -54,16 +55,19 @@ class BaseModelSerializer(serializers.ModelSerializer):
|
|||||||
@cached_property
|
@cached_property
|
||||||
def fields(self):
|
def fields(self):
|
||||||
"""
|
"""
|
||||||
Override the fields property to check for requested fields. If defined,
|
Override the fields property to return only specifically requested fields if needed.
|
||||||
return only the applicable fields.
|
|
||||||
"""
|
"""
|
||||||
if not self._requested_fields:
|
fields = super().fields
|
||||||
return super().fields
|
|
||||||
|
# Include only requested fields
|
||||||
|
if self._include_fields:
|
||||||
|
for field_name in set(fields) - set(self._include_fields):
|
||||||
|
fields.pop(field_name, None)
|
||||||
|
|
||||||
|
# Remove omitted fields
|
||||||
|
for field_name in set(self._omit_fields):
|
||||||
|
fields.pop(field_name, None)
|
||||||
|
|
||||||
fields = BindingDict(self)
|
|
||||||
for key, value in self.get_fields().items():
|
|
||||||
if key in self._requested_fields:
|
|
||||||
fields[key] = value
|
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
@extend_schema_field(OpenApiTypes.STR)
|
@extend_schema_field(OpenApiTypes.STR)
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
|||||||
from django.db import router, transaction
|
from django.db import router, transaction
|
||||||
from django.db.models import ProtectedError, RestrictedError
|
from django.db.models import ProtectedError, RestrictedError
|
||||||
from django_pglocks import advisory_lock
|
from django_pglocks import advisory_lock
|
||||||
from netbox.constants import ADVISORY_LOCK_KEYS
|
|
||||||
from rest_framework import mixins as drf_mixins
|
from rest_framework import mixins as drf_mixins
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.viewsets import GenericViewSet
|
from rest_framework.viewsets import GenericViewSet
|
||||||
|
|
||||||
from netbox.api.serializers.features import ChangeLogMessageSerializer
|
from netbox.api.serializers.features import ChangeLogMessageSerializer
|
||||||
|
from netbox.constants import ADVISORY_LOCK_KEYS
|
||||||
from utilities.api import get_annotations_for_serializer, get_prefetches_for_serializer
|
from utilities.api import get_annotations_for_serializer, get_prefetches_for_serializer
|
||||||
from utilities.exceptions import AbortRequest
|
from utilities.exceptions import AbortRequest
|
||||||
from utilities.query import reapply_model_ordering
|
from utilities.query import reapply_model_ordering
|
||||||
@@ -59,33 +59,38 @@ class BaseViewSet(GenericViewSet):
|
|||||||
serializer_class = self.get_serializer_class()
|
serializer_class = self.get_serializer_class()
|
||||||
|
|
||||||
# Dynamically resolve prefetches for included serializer fields and attach them to the queryset
|
# Dynamically resolve prefetches for included serializer fields and attach them to the queryset
|
||||||
if prefetch := get_prefetches_for_serializer(serializer_class, fields_to_include=self.requested_fields):
|
if prefetch := get_prefetches_for_serializer(serializer_class, **self.field_kwargs):
|
||||||
qs = qs.prefetch_related(*prefetch)
|
qs = qs.prefetch_related(*prefetch)
|
||||||
|
|
||||||
# Dynamically resolve annotations for RelatedObjectCountFields on the serializer and attach them to the queryset
|
# Dynamically resolve annotations for RelatedObjectCountFields on the serializer and attach them to the queryset
|
||||||
if annotations := get_annotations_for_serializer(serializer_class, fields_to_include=self.requested_fields):
|
if annotations := get_annotations_for_serializer(serializer_class, **self.field_kwargs):
|
||||||
qs = qs.annotate(**annotations)
|
qs = qs.annotate(**annotations)
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
def get_serializer(self, *args, **kwargs):
|
def get_serializer(self, *args, **kwargs):
|
||||||
|
# Pass the fields/omit kwargs (if specified by the request) to the serializer
|
||||||
# If specific fields have been requested, pass them to the serializer
|
kwargs.update(**self.field_kwargs)
|
||||||
if self.requested_fields:
|
|
||||||
kwargs['fields'] = self.requested_fields
|
|
||||||
|
|
||||||
return super().get_serializer(*args, **kwargs)
|
return super().get_serializer(*args, **kwargs)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def requested_fields(self):
|
def field_kwargs(self):
|
||||||
|
"""Return a dictionary of keyword arguments to be passed when instantiating the serializer."""
|
||||||
# An explicit list of fields was requested
|
# An explicit list of fields was requested
|
||||||
if requested_fields := self.request.query_params.get('fields'):
|
if requested_fields := self.request.query_params.get('fields'):
|
||||||
return requested_fields.split(',')
|
return {'fields': requested_fields.split(',')}
|
||||||
|
|
||||||
|
# An explicit list of fields to omit was requested
|
||||||
|
if omit_fields := self.request.query_params.get('omit'):
|
||||||
|
return {'omit': omit_fields.split(',')}
|
||||||
|
|
||||||
# Brief mode has been enabled for this request
|
# Brief mode has been enabled for this request
|
||||||
elif self.brief:
|
if self.brief:
|
||||||
serializer_class = self.get_serializer_class()
|
serializer_class = self.get_serializer_class()
|
||||||
return getattr(serializer_class.Meta, 'brief_fields', None)
|
if brief_fields := getattr(serializer_class.Meta, 'brief_fields', None):
|
||||||
return None
|
return {'fields': brief_fields}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
class NetBoxReadOnlyModelViewSet(
|
class NetBoxReadOnlyModelViewSet(
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ from contextvars import ContextVar
|
|||||||
__all__ = (
|
__all__ = (
|
||||||
'current_request',
|
'current_request',
|
||||||
'events_queue',
|
'events_queue',
|
||||||
|
'query_cache',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
current_request = ContextVar('current_request', default=None)
|
current_request = ContextVar('current_request', default=None)
|
||||||
events_queue = ContextVar('events_queue', default=dict())
|
events_queue = ContextVar('events_queue', default=dict())
|
||||||
|
query_cache = ContextVar('query_cache', default=None)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
from collections import defaultdict
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
|
||||||
from netbox.context import current_request, events_queue
|
from netbox.context import current_request, events_queue, query_cache
|
||||||
from netbox.utils import register_request_processor
|
from netbox.utils import register_request_processor
|
||||||
from extras.events import flush_events
|
from extras.events import flush_events
|
||||||
|
|
||||||
@@ -16,6 +17,7 @@ def event_tracking(request):
|
|||||||
"""
|
"""
|
||||||
current_request.set(request)
|
current_request.set(request)
|
||||||
events_queue.set({})
|
events_queue.set({})
|
||||||
|
query_cache.set(defaultdict(dict))
|
||||||
|
|
||||||
yield
|
yield
|
||||||
|
|
||||||
@@ -26,3 +28,4 @@ def event_tracking(request):
|
|||||||
# Clear context vars
|
# Clear context vars
|
||||||
current_request.set(None)
|
current_request.set(None)
|
||||||
events_queue.set({})
|
events_queue.set({})
|
||||||
|
query_cache.set(None)
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ from django.db.models import Q
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from users.models import Owner
|
from utilities.forms.fields import QueryField
|
||||||
from utilities.forms.fields import DynamicModelChoiceField, QueryField
|
|
||||||
from utilities.forms.mixins import FilterModifierMixin
|
from utilities.forms.mixins import FilterModifierMixin
|
||||||
from .mixins import CustomFieldsMixin, SavedFiltersMixin
|
from .mixins import CustomFieldsMixin, OwnerFilterMixin, SavedFiltersMixin
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'NestedGroupModelFilterSetForm',
|
'NestedGroupModelFilterSetForm',
|
||||||
@@ -47,14 +46,6 @@ class NetBoxModelFilterSetForm(FilterModifierMixin, CustomFieldsMixin, SavedFilt
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class OwnerFilterMixin(forms.Form):
|
|
||||||
owner_id = DynamicModelChoiceField(
|
|
||||||
queryset=Owner.objects.all(),
|
|
||||||
required=False,
|
|
||||||
label=_('Owner'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PrimaryModelFilterSetForm(OwnerFilterMixin, NetBoxModelFilterSetForm):
|
class PrimaryModelFilterSetForm(OwnerFilterMixin, NetBoxModelFilterSetForm):
|
||||||
"""
|
"""
|
||||||
FilterSet form for models which inherit from PrimaryModel.
|
FilterSet form for models which inherit from PrimaryModel.
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ from django.utils.translation import gettext as _
|
|||||||
from core.models import ObjectType
|
from core.models import ObjectType
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.models import *
|
from extras.models import *
|
||||||
from users.models import Owner
|
from users.models import OwnerGroup, Owner
|
||||||
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ChangelogMessageMixin',
|
'ChangelogMessageMixin',
|
||||||
'CustomFieldsMixin',
|
'CustomFieldsMixin',
|
||||||
'OwnerMixin',
|
'OwnerMixin',
|
||||||
|
'OwnerFilterMixin',
|
||||||
'SavedFiltersMixin',
|
'SavedFiltersMixin',
|
||||||
'TagsMixin',
|
'TagsMixin',
|
||||||
)
|
)
|
||||||
@@ -22,7 +23,7 @@ class ChangelogMessageMixin(forms.Form):
|
|||||||
"""
|
"""
|
||||||
changelog_message = forms.CharField(
|
changelog_message = forms.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
max_length=200
|
max_length=200,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -42,6 +43,7 @@ class CustomFieldsMixin:
|
|||||||
Attributes:
|
Attributes:
|
||||||
model: The model class
|
model: The model class
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model = None
|
model = None
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -86,13 +88,20 @@ class CustomFieldsMixin:
|
|||||||
|
|
||||||
|
|
||||||
class SavedFiltersMixin(forms.Form):
|
class SavedFiltersMixin(forms.Form):
|
||||||
|
"""
|
||||||
|
Form mixin for forms that support saved filters.
|
||||||
|
|
||||||
|
Provides a field for selecting a saved filter,
|
||||||
|
with options limited to those applicable to the form's model.
|
||||||
|
"""
|
||||||
|
|
||||||
filter_id = DynamicModelMultipleChoiceField(
|
filter_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=SavedFilter.objects.all(),
|
queryset=SavedFilter.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Saved Filter'),
|
label=_('Saved Filter'),
|
||||||
query_params={
|
query_params={
|
||||||
'usable': True,
|
'usable': True,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -107,6 +116,13 @@ class SavedFiltersMixin(forms.Form):
|
|||||||
|
|
||||||
|
|
||||||
class TagsMixin(forms.Form):
|
class TagsMixin(forms.Form):
|
||||||
|
"""
|
||||||
|
Mixin for forms that support tagging.
|
||||||
|
|
||||||
|
Provides a field for selecting tags,
|
||||||
|
with options limited to those applicable to the form's model.
|
||||||
|
"""
|
||||||
|
|
||||||
tags = DynamicModelMultipleChoiceField(
|
tags = DynamicModelMultipleChoiceField(
|
||||||
queryset=Tag.objects.all(),
|
queryset=Tag.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@@ -124,10 +140,47 @@ class TagsMixin(forms.Form):
|
|||||||
|
|
||||||
class OwnerMixin(forms.Form):
|
class OwnerMixin(forms.Form):
|
||||||
"""
|
"""
|
||||||
Add an `owner` field to forms for models which support Owner assignment.
|
Mixin for forms which adds ownership fields.
|
||||||
|
|
||||||
|
Include this mixin in forms for models which
|
||||||
|
support owner and/or owner group assignment.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
owner_group = DynamicModelChoiceField(
|
||||||
|
label=_('Owner group'),
|
||||||
|
queryset=OwnerGroup.objects.all(),
|
||||||
|
required=False,
|
||||||
|
null_option='None',
|
||||||
|
initial_params={'members': '$owner'},
|
||||||
|
)
|
||||||
owner = DynamicModelChoiceField(
|
owner = DynamicModelChoiceField(
|
||||||
queryset=Owner.objects.all(),
|
queryset=Owner.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
|
query_params={'group_id': '$owner_group'},
|
||||||
|
label=_('Owner'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class OwnerFilterMixin(forms.Form):
|
||||||
|
"""
|
||||||
|
Mixin for filterset forms which adds owner and owner group filtering.
|
||||||
|
|
||||||
|
Include this mixin in filterset forms for models
|
||||||
|
which support owner and/or owner group assignment.
|
||||||
|
"""
|
||||||
|
|
||||||
|
owner_group_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=OwnerGroup.objects.all(),
|
||||||
|
required=False,
|
||||||
|
null_option='None',
|
||||||
|
label=_('Owner Group'),
|
||||||
|
)
|
||||||
|
owner_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Owner.objects.all(),
|
||||||
|
required=False,
|
||||||
|
null_option='None',
|
||||||
|
query_params={
|
||||||
|
'group_id': '$owner_group_id'
|
||||||
|
},
|
||||||
label=_('Owner'),
|
label=_('Owner'),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from functools import cache
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
@@ -409,60 +411,10 @@ ADMIN_MENU = Menu(
|
|||||||
MenuGroup(
|
MenuGroup(
|
||||||
label=_('Authentication'),
|
label=_('Authentication'),
|
||||||
items=(
|
items=(
|
||||||
MenuItem(
|
get_model_item('users', 'user', _('Users')),
|
||||||
link='users:user_list',
|
get_model_item('users', 'group', _('Groups')),
|
||||||
link_text=_('Users'),
|
get_model_item('users', 'token', _('API Tokens')),
|
||||||
staff_only=True,
|
get_model_item('users', 'objectpermission', _('Permissions'), actions=['add']),
|
||||||
permissions=['users.view_user'],
|
|
||||||
buttons=(
|
|
||||||
MenuItemButton(
|
|
||||||
link='users:user_add',
|
|
||||||
title='Add',
|
|
||||||
icon_class='mdi mdi-plus-thick',
|
|
||||||
permissions=['users.add_user']
|
|
||||||
),
|
|
||||||
MenuItemButton(
|
|
||||||
link='users:user_bulk_import',
|
|
||||||
title='Import',
|
|
||||||
icon_class='mdi mdi-upload',
|
|
||||||
permissions=['users.add_user']
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
MenuItem(
|
|
||||||
link='users:group_list',
|
|
||||||
link_text=_('Groups'),
|
|
||||||
staff_only=True,
|
|
||||||
permissions=['users.view_group'],
|
|
||||||
buttons=(
|
|
||||||
MenuItemButton(
|
|
||||||
link='users:group_add',
|
|
||||||
title='Add',
|
|
||||||
icon_class='mdi mdi-plus-thick',
|
|
||||||
permissions=['users.add_group']
|
|
||||||
),
|
|
||||||
MenuItemButton(
|
|
||||||
link='users:group_bulk_import',
|
|
||||||
title='Import',
|
|
||||||
icon_class='mdi mdi-upload',
|
|
||||||
permissions=['users.add_group']
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
MenuItem(
|
|
||||||
link='users:token_list',
|
|
||||||
link_text=_('API Tokens'),
|
|
||||||
staff_only=True,
|
|
||||||
permissions=['users.view_token'],
|
|
||||||
buttons=get_model_buttons('users', 'token')
|
|
||||||
),
|
|
||||||
MenuItem(
|
|
||||||
link='users:objectpermission_list',
|
|
||||||
link_text=_('Permissions'),
|
|
||||||
staff_only=True,
|
|
||||||
permissions=['users.view_objectpermission'],
|
|
||||||
buttons=get_model_buttons('users', 'objectpermission', actions=['add'])
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
MenuGroup(
|
MenuGroup(
|
||||||
@@ -501,40 +453,49 @@ ADMIN_MENU = Menu(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
MENUS = [
|
|
||||||
ORGANIZATION_MENU,
|
|
||||||
RACKS_MENU,
|
|
||||||
DEVICES_MENU,
|
|
||||||
CONNECTIONS_MENU,
|
|
||||||
WIRELESS_MENU,
|
|
||||||
IPAM_MENU,
|
|
||||||
VPN_MENU,
|
|
||||||
VIRTUALIZATION_MENU,
|
|
||||||
CIRCUITS_MENU,
|
|
||||||
POWER_MENU,
|
|
||||||
PROVISIONING_MENU,
|
|
||||||
CUSTOMIZATION_MENU,
|
|
||||||
OPERATIONS_MENU,
|
|
||||||
]
|
|
||||||
|
|
||||||
# Add top-level plugin menus
|
@cache
|
||||||
for menu in registry['plugins']['menus']:
|
def get_menus():
|
||||||
MENUS.append(menu)
|
"""
|
||||||
|
Dynamically build and return the list of navigation menus.
|
||||||
# Add the default "plugins" menu
|
This ensures plugin menus registered during app initialization are included.
|
||||||
if registry['plugins']['menu_items']:
|
The result is cached since menus don't change without a Django restart.
|
||||||
|
"""
|
||||||
# Build the default plugins menu
|
menus = [
|
||||||
groups = [
|
ORGANIZATION_MENU,
|
||||||
MenuGroup(label=label, items=items)
|
RACKS_MENU,
|
||||||
for label, items in registry['plugins']['menu_items'].items()
|
DEVICES_MENU,
|
||||||
|
CONNECTIONS_MENU,
|
||||||
|
WIRELESS_MENU,
|
||||||
|
IPAM_MENU,
|
||||||
|
VPN_MENU,
|
||||||
|
VIRTUALIZATION_MENU,
|
||||||
|
CIRCUITS_MENU,
|
||||||
|
POWER_MENU,
|
||||||
|
PROVISIONING_MENU,
|
||||||
|
CUSTOMIZATION_MENU,
|
||||||
|
OPERATIONS_MENU,
|
||||||
]
|
]
|
||||||
plugins_menu = Menu(
|
|
||||||
label=_("Plugins"),
|
|
||||||
icon_class="mdi mdi-puzzle",
|
|
||||||
groups=groups
|
|
||||||
)
|
|
||||||
MENUS.append(plugins_menu)
|
|
||||||
|
|
||||||
# Add the admin menu last
|
# Add top-level plugin menus
|
||||||
MENUS.append(ADMIN_MENU)
|
for menu in registry['plugins']['menus']:
|
||||||
|
menus.append(menu)
|
||||||
|
|
||||||
|
# Add the default "plugins" menu
|
||||||
|
if registry['plugins']['menu_items']:
|
||||||
|
# Build the default plugins menu
|
||||||
|
groups = [
|
||||||
|
MenuGroup(label=label, items=items)
|
||||||
|
for label, items in registry['plugins']['menu_items'].items()
|
||||||
|
]
|
||||||
|
plugins_menu = Menu(
|
||||||
|
label=_("Plugins"),
|
||||||
|
icon_class="mdi mdi-puzzle",
|
||||||
|
groups=groups
|
||||||
|
)
|
||||||
|
menus.append(plugins_menu)
|
||||||
|
|
||||||
|
# Add the admin menu last
|
||||||
|
menus.append(ADMIN_MENU)
|
||||||
|
|
||||||
|
return menus
|
||||||
|
|||||||
@@ -271,9 +271,14 @@ class NetBoxTable(BaseTable):
|
|||||||
|
|
||||||
|
|
||||||
class PrimaryModelTable(NetBoxTable):
|
class PrimaryModelTable(NetBoxTable):
|
||||||
|
owner_group = tables.Column(
|
||||||
|
accessor='owner__group',
|
||||||
|
linkify=True,
|
||||||
|
verbose_name=_('Owner Group'),
|
||||||
|
)
|
||||||
owner = tables.Column(
|
owner = tables.Column(
|
||||||
linkify=True,
|
linkify=True,
|
||||||
verbose_name=_('Owner')
|
verbose_name=_('Owner'),
|
||||||
)
|
)
|
||||||
comments = columns.MarkdownColumn(
|
comments = columns.MarkdownColumn(
|
||||||
verbose_name=_('Comments'),
|
verbose_name=_('Comments'),
|
||||||
@@ -281,9 +286,14 @@ class PrimaryModelTable(NetBoxTable):
|
|||||||
|
|
||||||
|
|
||||||
class OrganizationalModelTable(NetBoxTable):
|
class OrganizationalModelTable(NetBoxTable):
|
||||||
|
owner_group = tables.Column(
|
||||||
|
accessor='owner__group',
|
||||||
|
linkify=True,
|
||||||
|
verbose_name=_('Owner Group'),
|
||||||
|
)
|
||||||
owner = tables.Column(
|
owner = tables.Column(
|
||||||
linkify=True,
|
linkify=True,
|
||||||
verbose_name=_('Owner')
|
verbose_name=_('Owner'),
|
||||||
)
|
)
|
||||||
comments = columns.MarkdownColumn(
|
comments = columns.MarkdownColumn(
|
||||||
verbose_name=_('Comments'),
|
verbose_name=_('Comments'),
|
||||||
@@ -291,9 +301,14 @@ class OrganizationalModelTable(NetBoxTable):
|
|||||||
|
|
||||||
|
|
||||||
class NestedGroupModelTable(NetBoxTable):
|
class NestedGroupModelTable(NetBoxTable):
|
||||||
|
owner_group = tables.Column(
|
||||||
|
accessor='owner__group',
|
||||||
|
linkify=True,
|
||||||
|
verbose_name=_('Owner Group'),
|
||||||
|
)
|
||||||
owner = tables.Column(
|
owner = tables.Column(
|
||||||
linkify=True,
|
linkify=True,
|
||||||
verbose_name=_('Owner')
|
verbose_name=_('Owner'),
|
||||||
)
|
)
|
||||||
name = columns.MPTTColumn(
|
name = columns.MPTTColumn(
|
||||||
verbose_name=_('Name'),
|
verbose_name=_('Name'),
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ class TextAttr(ObjectAttribute):
|
|||||||
def get_value(self, obj):
|
def get_value(self, obj):
|
||||||
value = resolve_attr_path(obj, self.accessor)
|
value = resolve_attr_path(obj, self.accessor)
|
||||||
# Apply format string (if any)
|
# Apply format string (if any)
|
||||||
if value and self.format_string:
|
if value is not None and value != '' and self.format_string:
|
||||||
return self.format_string.format(value)
|
return self.format_string.format(value)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import re
|
import re
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
import logging
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
@@ -28,6 +29,8 @@ __all__ = (
|
|||||||
'SearchView',
|
'SearchView',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(f'netbox.{__name__}')
|
||||||
|
|
||||||
Link = namedtuple('Link', ('label', 'viewname', 'permission', 'count'))
|
Link = namedtuple('Link', ('label', 'viewname', 'permission', 'count'))
|
||||||
|
|
||||||
|
|
||||||
@@ -50,7 +53,14 @@ class HomeView(ConditionalLoginRequiredMixin, View):
|
|||||||
# Check whether a new release is available. (Only for superusers.)
|
# Check whether a new release is available. (Only for superusers.)
|
||||||
new_release = None
|
new_release = None
|
||||||
if request.user.is_superuser:
|
if request.user.is_superuser:
|
||||||
latest_release = cache.get('latest_release')
|
# cache.get() can raise an exception if the cached value can't be unpickled after dependency upgrades
|
||||||
|
try:
|
||||||
|
latest_release = cache.get('latest_release')
|
||||||
|
except Exception:
|
||||||
|
logger.debug("Failed to read 'latest_release' from cache; deleting key", exc_info=True)
|
||||||
|
cache.delete('latest_release')
|
||||||
|
latest_release = None
|
||||||
|
|
||||||
if latest_release:
|
if latest_release:
|
||||||
release_version, release_url = latest_release
|
release_version, release_url = latest_release
|
||||||
if release_version > version.parse(settings.RELEASE.version):
|
if release_version > version.parse(settings.RELEASE.version):
|
||||||
|
|||||||
@@ -59,6 +59,10 @@
|
|||||||
<th scope="row">{% trans "Completed" %}</th>
|
<th scope="row">{% trans "Completed" %}</th>
|
||||||
<td>{{ object.completed|isodatetime|placeholder }}</td>
|
<td>{{ object.completed|isodatetime|placeholder }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Queue" %}</th>
|
||||||
|
<td>{{ object.queue_name|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -101,8 +101,9 @@
|
|||||||
|
|
||||||
<div class="field-group mb-5">
|
<div class="field-group mb-5">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<h2 class="col-9 offset-3">{% trans "Owner" %}</h2>
|
<h2 class="col-9 offset-3">{% trans "Ownership" %}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
{% render_field form.owner_group %}
|
||||||
{% render_field form.owner %}
|
{% render_field form.owner %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
{{ value|floatformat }}U
|
||||||
@@ -80,8 +80,9 @@
|
|||||||
|
|
||||||
<div class="field-group mb-5">
|
<div class="field-group mb-5">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<h2 class="col-9 offset-3">{% trans "Owner" %}</h2>
|
<h2 class="col-9 offset-3">{% trans "Ownership" %}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
{% render_field form.owner_group %}
|
||||||
{% render_field form.owner %}
|
{% render_field form.owner %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -370,33 +370,6 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if object.is_lag %}
|
|
||||||
<div class="card">
|
|
||||||
<h2 class="card-header">{% trans "LAG Members" %}</h2>
|
|
||||||
<table class="table table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "Parent" %}</th>
|
|
||||||
<th>{% trans "Interface" %}</th>
|
|
||||||
<th>{% trans "Type" %}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for member in object.member_interfaces.all %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ member.device|linkify }}</td>
|
|
||||||
<td>{{ member|linkify }}</td>
|
|
||||||
<td>{{ member.get_type_display }}</td>
|
|
||||||
</tr>
|
|
||||||
{% empty %}
|
|
||||||
<tr>
|
|
||||||
<td colspan="3" class="text-muted">{% trans "No member interfaces" %}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% include 'ipam/inc/panels/fhrp_groups.html' %}
|
{% include 'ipam/inc/panels/fhrp_groups.html' %}
|
||||||
{% include 'dcim/inc/panels/inventory_items.html' %}
|
{% include 'dcim/inc/panels/inventory_items.html' %}
|
||||||
{% plugin_right_page object %}
|
{% plugin_right_page object %}
|
||||||
@@ -441,6 +414,13 @@
|
|||||||
{% include 'inc/panel_table.html' with table=vlan_table heading="VLANs" %}
|
{% include 'inc/panel_table.html' with table=vlan_table heading="VLANs" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if object.is_lag %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col col-md-12">
|
||||||
|
{% include 'inc/panel_table.html' with table=lag_interfaces_table heading="LAG Members" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% if object.vlan_translation_policy %}
|
{% if object.vlan_translation_policy %}
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col col-md-12">
|
<div class="col col-md-12">
|
||||||
|
|||||||
@@ -36,8 +36,9 @@
|
|||||||
|
|
||||||
<div class="field-group mb-5">
|
<div class="field-group mb-5">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<h2 class="col-9 offset-3">{% trans "Owner" %}</h2>
|
<h2 class="col-9 offset-3">{% trans "Ownership" %}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
{% render_field vc_form.owner_group %}
|
||||||
{% render_field vc_form.owner %}
|
{% render_field vc_form.owner %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -121,6 +121,7 @@
|
|||||||
<div class="alert alert-warning" role="alert">
|
<div class="alert alert-warning" role="alert">
|
||||||
<i class="mdi mdi-alert"></i>
|
<i class="mdi mdi-alert"></i>
|
||||||
{% blocktrans with module=module.name %}Could not load scripts from module {{ module }}{% endblocktrans %}
|
{% blocktrans with module=module.name %}Could not load scripts from module {{ module }}{% endblocktrans %}
|
||||||
|
{% if module.error %}<code>{{ module.error }}</code>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -62,8 +62,11 @@ Context:
|
|||||||
{% if form.owner %}
|
{% if form.owner %}
|
||||||
<div class="field-group mb-5">
|
<div class="field-group mb-5">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<h2 class="col-9 offset-3">{% trans "Owner" %}</h2>
|
<h2 class="col-9 offset-3">{% trans "Ownership" %}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
{% if form.owner_group %}
|
||||||
|
{% render_field form.owner_group %}
|
||||||
|
{% endif %}
|
||||||
{% render_field form.owner bulk_nullable=True %}
|
{% render_field form.owner bulk_nullable=True %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -27,6 +27,9 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<h2 class="col-9 offset-3">{% trans "Ownership" %}</h2>
|
<h2 class="col-9 offset-3">{% trans "Ownership" %}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
{% if form.owner_group %}
|
||||||
|
{% render_field form.owner_group %}
|
||||||
|
{% endif %}
|
||||||
{% render_field form.owner %}
|
{% render_field form.owner %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
{% include 'ipam/inc/max_depth.html' %}
|
{% include 'ipam/inc/max_depth.html' %}
|
||||||
{% include 'ipam/inc/max_length.html' %}
|
{% include 'ipam/inc/max_length.html' %}
|
||||||
{% if perms.ipam.add_prefix and first_available_prefix %}
|
{% if perms.ipam.add_prefix and first_available_prefix %}
|
||||||
<a href="{% url 'ipam:prefix_add' %}?prefix={{ first_available_prefix }}&vrf={{ object.vrf.pk }}&site={{ object.site.pk }}&tenant_group={{ object.tenant.group.pk }}&tenant={{ object.tenant.pk }}" class="btn btn-primary">
|
<a href="{% url 'ipam:prefix_add' %}?prefix={{ first_available_prefix }}{% if object.vrf %}&vrf={{ object.vrf.pk }}{% endif %}{% if object.scope %}&scope_type={{ object.scope_type.pk }}&scope={{ object.scope.pk }}{% endif %}{% if object.tenant %}&tenant_group={{ object.tenant.group.pk }}&tenant={{ object.tenant.pk }}{% endif %}" class="btn btn-primary">
|
||||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Prefix" %}
|
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Prefix" %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -67,8 +67,9 @@
|
|||||||
|
|
||||||
<div class="field-group mb-5">
|
<div class="field-group mb-5">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<h2 class="col-9 offset-3">{% trans "Owner" %}</h2>
|
<h2 class="col-9 offset-3">{% trans "Ownership" %}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
{% render_field form.owner_group %}
|
||||||
{% render_field form.owner %}
|
{% render_field form.owner %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -31,8 +31,9 @@ __all__ = (
|
|||||||
class TenantGroupFilterForm(NestedGroupModelFilterSetForm):
|
class TenantGroupFilterForm(NestedGroupModelFilterSetForm):
|
||||||
model = TenantGroup
|
model = TenantGroup
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('parent_id', name=_('Tenant Group')),
|
FieldSet('parent_id', name=_('Tenant Group')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
parent_id = DynamicModelMultipleChoiceField(
|
parent_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=TenantGroup.objects.all(),
|
queryset=TenantGroup.objects.all(),
|
||||||
@@ -45,8 +46,9 @@ class TenantGroupFilterForm(NestedGroupModelFilterSetForm):
|
|||||||
class TenantFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
class TenantFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = Tenant
|
model = Tenant
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('group_id', name=_('Tenant')),
|
FieldSet('group_id', name=_('Tenant')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts'))
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts'))
|
||||||
)
|
)
|
||||||
group_id = DynamicModelMultipleChoiceField(
|
group_id = DynamicModelMultipleChoiceField(
|
||||||
@@ -65,8 +67,9 @@ class TenantFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
|||||||
class ContactGroupFilterForm(NestedGroupModelFilterSetForm):
|
class ContactGroupFilterForm(NestedGroupModelFilterSetForm):
|
||||||
model = ContactGroup
|
model = ContactGroup
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('parent_id', name=_('Contact Group')),
|
FieldSet('parent_id', name=_('Contact Group')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
parent_id = DynamicModelMultipleChoiceField(
|
parent_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=ContactGroup.objects.all(),
|
queryset=ContactGroup.objects.all(),
|
||||||
@@ -79,7 +82,8 @@ class ContactGroupFilterForm(NestedGroupModelFilterSetForm):
|
|||||||
class ContactRoleFilterForm(OrganizationalModelFilterSetForm):
|
class ContactRoleFilterForm(OrganizationalModelFilterSetForm):
|
||||||
model = ContactRole
|
model = ContactRole
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
@@ -87,8 +91,9 @@ class ContactRoleFilterForm(OrganizationalModelFilterSetForm):
|
|||||||
class ContactFilterForm(PrimaryModelFilterSetForm):
|
class ContactFilterForm(PrimaryModelFilterSetForm):
|
||||||
model = Contact
|
model = Contact
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('group_id', name=_('Contact')),
|
FieldSet('group_id', name=_('Contact')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
group_id = DynamicModelMultipleChoiceField(
|
group_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=ContactGroup.objects.all(),
|
queryset=ContactGroup.objects.all(),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from users.models import Owner
|
from users.models import OwnerGroup, Owner
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'OwnerFilterMixin',
|
'OwnerFilterMixin',
|
||||||
@@ -12,6 +12,17 @@ class OwnerFilterMixin(django_filters.FilterSet):
|
|||||||
"""
|
"""
|
||||||
Adds owner & owner_id filters for models which inherit from OwnerMixin.
|
Adds owner & owner_id filters for models which inherit from OwnerMixin.
|
||||||
"""
|
"""
|
||||||
|
owner_group_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
queryset=OwnerGroup.objects.all(),
|
||||||
|
field_name='owner__group',
|
||||||
|
label=_('Owner Group (ID)'),
|
||||||
|
)
|
||||||
|
owner_group = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
queryset=OwnerGroup.objects.all(),
|
||||||
|
field_name='owner__group__name',
|
||||||
|
to_field_name='name',
|
||||||
|
label=_('Owner Group (name)'),
|
||||||
|
)
|
||||||
owner_id = django_filters.ModelMultipleChoiceFilter(
|
owner_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Owner.objects.all(),
|
queryset=Owner.objects.all(),
|
||||||
label=_('Owner (ID)'),
|
label=_('Owner (ID)'),
|
||||||
|
|||||||
+18
-9
@@ -93,18 +93,23 @@ def get_view_name(view):
|
|||||||
return drf_get_view_name(view)
|
return drf_get_view_name(view)
|
||||||
|
|
||||||
|
|
||||||
def get_prefetches_for_serializer(serializer_class, fields_to_include=None):
|
def get_prefetches_for_serializer(serializer_class, fields=None, omit=None):
|
||||||
"""
|
"""
|
||||||
Compile and return a list of fields which should be prefetched on the queryset for a serializer.
|
Compile and return a list of fields which should be prefetched on the queryset for a serializer.
|
||||||
"""
|
"""
|
||||||
|
if fields is not None and omit is not None:
|
||||||
|
raise TypeError("Cannot specify both 'fields' and 'omit' parameters.")
|
||||||
|
|
||||||
model = serializer_class.Meta.model
|
model = serializer_class.Meta.model
|
||||||
|
|
||||||
# If fields are not specified, default to all
|
# If fields are not specified, default to all
|
||||||
if not fields_to_include:
|
fields_to_include = fields or serializer_class.Meta.fields
|
||||||
fields_to_include = serializer_class.Meta.fields
|
fields_to_omit = omit or []
|
||||||
|
|
||||||
prefetch_fields = []
|
prefetch_fields = []
|
||||||
for field_name in fields_to_include:
|
for field_name in fields_to_include:
|
||||||
|
if field_name in fields_to_omit:
|
||||||
|
continue
|
||||||
serializer_field = serializer_class._declared_fields.get(field_name)
|
serializer_field = serializer_class._declared_fields.get(field_name)
|
||||||
|
|
||||||
# Determine the name of the model field referenced by the serializer field
|
# Determine the name of the model field referenced by the serializer field
|
||||||
@@ -132,19 +137,23 @@ def get_prefetches_for_serializer(serializer_class, fields_to_include=None):
|
|||||||
return prefetch_fields
|
return prefetch_fields
|
||||||
|
|
||||||
|
|
||||||
def get_annotations_for_serializer(serializer_class, fields_to_include=None):
|
def get_annotations_for_serializer(serializer_class, fields=None, omit=None):
|
||||||
"""
|
"""
|
||||||
Return a mapping of field names to annotations to be applied to the queryset for a serializer.
|
Return a mapping of field names to annotations to be applied to the queryset for a serializer.
|
||||||
"""
|
"""
|
||||||
annotations = {}
|
if fields is not None and omit is not None:
|
||||||
|
raise TypeError("Cannot specify both 'fields' and 'omit' parameters.")
|
||||||
# If specific fields are not specified, default to all
|
|
||||||
if not fields_to_include:
|
|
||||||
fields_to_include = serializer_class.Meta.fields
|
|
||||||
|
|
||||||
model = serializer_class.Meta.model
|
model = serializer_class.Meta.model
|
||||||
|
|
||||||
|
# If fields are not specified, default to all
|
||||||
|
fields_to_include = fields or serializer_class.Meta.fields
|
||||||
|
fields_to_omit = omit or []
|
||||||
|
|
||||||
|
annotations = {}
|
||||||
for field_name, field in serializer_class._declared_fields.items():
|
for field_name, field in serializer_class._declared_fields.items():
|
||||||
|
if field_name in fields_to_omit:
|
||||||
|
continue
|
||||||
if field_name in fields_to_include and type(field) is RelatedObjectCountField:
|
if field_name in fields_to_include and type(field) is RelatedObjectCountField:
|
||||||
related_field = getattr(model, field.relation).field
|
related_field = getattr(model, field.relation).field
|
||||||
annotations[field_name] = count_related(related_field.model, related_field.name)
|
annotations[field_name] = count_related(related_field.model, related_field.name)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import enum
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from utilities.data import get_config_value_ci
|
||||||
from utilities.string import enum_key
|
from utilities.string import enum_key
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@@ -24,13 +25,14 @@ class ChoiceSetMeta(type):
|
|||||||
).format(name=name)
|
).format(name=name)
|
||||||
app = attrs['__module__'].split('.', 1)[0]
|
app = attrs['__module__'].split('.', 1)[0]
|
||||||
replace_key = f'{app}.{key}'
|
replace_key = f'{app}.{key}'
|
||||||
extend_key = f'{replace_key}+' if replace_key else None
|
replace_choices = get_config_value_ci(settings.FIELD_CHOICES, replace_key)
|
||||||
if replace_key and replace_key in settings.FIELD_CHOICES:
|
if replace_choices is not None:
|
||||||
# Replace the stock choices
|
attrs['CHOICES'] = replace_choices
|
||||||
attrs['CHOICES'] = settings.FIELD_CHOICES[replace_key]
|
else:
|
||||||
elif extend_key and extend_key in settings.FIELD_CHOICES:
|
extend_key = f'{replace_key}+'
|
||||||
# Extend the stock choices
|
extend_choices = get_config_value_ci(settings.FIELD_CHOICES, extend_key)
|
||||||
attrs['CHOICES'].extend(settings.FIELD_CHOICES[extend_key])
|
if extend_choices is not None:
|
||||||
|
attrs['CHOICES'].extend(extend_choices)
|
||||||
|
|
||||||
# Define choice tuples and color maps
|
# Define choice tuples and color maps
|
||||||
attrs['_choices'] = []
|
attrs['_choices'] = []
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ __all__ = (
|
|||||||
'deepmerge',
|
'deepmerge',
|
||||||
'drange',
|
'drange',
|
||||||
'flatten_dict',
|
'flatten_dict',
|
||||||
|
'get_config_value_ci',
|
||||||
'ranges_to_string',
|
'ranges_to_string',
|
||||||
'ranges_to_string_list',
|
'ranges_to_string_list',
|
||||||
'resolve_attr_path',
|
'resolve_attr_path',
|
||||||
@@ -22,6 +23,19 @@ __all__ = (
|
|||||||
# Dictionary utilities
|
# Dictionary utilities
|
||||||
#
|
#
|
||||||
|
|
||||||
|
def get_config_value_ci(config_dict, key, default=None):
|
||||||
|
"""
|
||||||
|
Retrieve a value from a dictionary using case-insensitive key matching.
|
||||||
|
"""
|
||||||
|
if key in config_dict:
|
||||||
|
return config_dict[key]
|
||||||
|
key_lower = key.lower()
|
||||||
|
for config_key, value in config_dict.items():
|
||||||
|
if config_key.lower() == key_lower:
|
||||||
|
return value
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
def deepmerge(original, new):
|
def deepmerge(original, new):
|
||||||
"""
|
"""
|
||||||
Deep merge two dictionaries (new into original) and return a new dict
|
Deep merge two dictionaries (new into original) and return a new dict
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from django import template
|
from django import template
|
||||||
|
|
||||||
from netbox.navigation.menu import MENUS
|
from netbox.navigation.menu import get_menus
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'nav',
|
'nav',
|
||||||
@@ -19,7 +19,7 @@ def nav(context):
|
|||||||
nav_items = []
|
nav_items = []
|
||||||
|
|
||||||
# Construct the navigation menu based upon the current user's permissions
|
# Construct the navigation menu based upon the current user's permissions
|
||||||
for menu in MENUS:
|
for menu in get_menus():
|
||||||
groups = []
|
groups = []
|
||||||
for group in menu.groups:
|
for group in menu.groups:
|
||||||
items = []
|
items = []
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase, override_settings
|
||||||
|
|
||||||
from utilities.choices import ChoiceSet
|
from utilities.choices import ChoiceSet
|
||||||
|
|
||||||
@@ -30,3 +30,29 @@ class ChoiceSetTestCase(TestCase):
|
|||||||
|
|
||||||
def test_values(self):
|
def test_values(self):
|
||||||
self.assertListEqual(ExampleChoices.values(), ['a', 'b', 'c', 1, 2, 3])
|
self.assertListEqual(ExampleChoices.values(), ['a', 'b', 'c', 1, 2, 3])
|
||||||
|
|
||||||
|
|
||||||
|
class FieldChoicesCaseInsensitiveTestCase(TestCase):
|
||||||
|
"""
|
||||||
|
Integration tests for FIELD_CHOICES case-insensitive key lookup.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_replace_choices_with_different_casing(self):
|
||||||
|
"""Test that replacement works when config key casing differs."""
|
||||||
|
# Config uses lowercase, but code constructs PascalCase key
|
||||||
|
with override_settings(FIELD_CHOICES={'utilities.teststatus': [('new', 'New')]}):
|
||||||
|
class TestStatusChoices(ChoiceSet):
|
||||||
|
key = 'TestStatus' # Code will look up 'utilities.TestStatus'
|
||||||
|
CHOICES = [('old', 'Old')]
|
||||||
|
|
||||||
|
self.assertEqual(TestStatusChoices.CHOICES, [('new', 'New')])
|
||||||
|
|
||||||
|
def test_extend_choices_with_different_casing(self):
|
||||||
|
"""Test that extension works with the + suffix under casing differences."""
|
||||||
|
# Config uses lowercase with + suffix
|
||||||
|
with override_settings(FIELD_CHOICES={'utilities.teststatus+': [('extra', 'Extra')]}):
|
||||||
|
class TestStatusChoices(ChoiceSet):
|
||||||
|
key = 'TestStatus' # Code will look up 'utilities.TestStatus+'
|
||||||
|
CHOICES = [('base', 'Base')]
|
||||||
|
|
||||||
|
self.assertEqual(TestStatusChoices.CHOICES, [('base', 'Base'), ('extra', 'Extra')])
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from django.db.backends.postgresql.psycopg_any import NumericRange
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from utilities.data import (
|
from utilities.data import (
|
||||||
check_ranges_overlap,
|
check_ranges_overlap,
|
||||||
|
get_config_value_ci,
|
||||||
ranges_to_string,
|
ranges_to_string,
|
||||||
ranges_to_string_list,
|
ranges_to_string_list,
|
||||||
string_to_ranges,
|
string_to_ranges,
|
||||||
@@ -96,3 +97,25 @@ class RangeFunctionsTestCase(TestCase):
|
|||||||
string_to_ranges('2-10, a-b'),
|
string_to_ranges('2-10, a-b'),
|
||||||
None # Fails to convert
|
None # Fails to convert
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GetConfigValueCITestCase(TestCase):
|
||||||
|
|
||||||
|
def test_exact_match(self):
|
||||||
|
config = {'dcim.site': 'value1', 'dcim.Device': 'value2'}
|
||||||
|
self.assertEqual(get_config_value_ci(config, 'dcim.site'), 'value1')
|
||||||
|
self.assertEqual(get_config_value_ci(config, 'dcim.Device'), 'value2')
|
||||||
|
|
||||||
|
def test_case_insensitive_match(self):
|
||||||
|
config = {'dcim.Site': 'value1', 'ipam.IPAddress': 'value2'}
|
||||||
|
self.assertEqual(get_config_value_ci(config, 'dcim.site'), 'value1')
|
||||||
|
self.assertEqual(get_config_value_ci(config, 'ipam.ipaddress'), 'value2')
|
||||||
|
|
||||||
|
def test_default_value(self):
|
||||||
|
config = {'dcim.site': 'value1'}
|
||||||
|
self.assertIsNone(get_config_value_ci(config, 'nonexistent'))
|
||||||
|
self.assertEqual(get_config_value_ci(config, 'nonexistent', default=[]), [])
|
||||||
|
|
||||||
|
def test_empty_dict(self):
|
||||||
|
self.assertIsNone(get_config_value_ci({}, 'any.key'))
|
||||||
|
self.assertEqual(get_config_value_ci({}, 'any.key', default=[]), [])
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ from extras.forms import LocalConfigContextFilterForm
|
|||||||
from extras.models import ConfigTemplate
|
from extras.models import ConfigTemplate
|
||||||
from ipam.models import VRF, VLANTranslationPolicy
|
from ipam.models import VRF, VLANTranslationPolicy
|
||||||
from netbox.forms import NetBoxModelFilterSetForm, OrganizationalModelFilterSetForm, PrimaryModelFilterSetForm
|
from netbox.forms import NetBoxModelFilterSetForm, OrganizationalModelFilterSetForm, PrimaryModelFilterSetForm
|
||||||
|
from netbox.forms.mixins import OwnerFilterMixin
|
||||||
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
|
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
|
||||||
from users.models import Owner
|
|
||||||
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES
|
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES
|
||||||
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField
|
from utilities.forms.fields import DynamicModelMultipleChoiceField, TagFilterField
|
||||||
from utilities.forms.rendering import FieldSet
|
from utilities.forms.rendering import FieldSet
|
||||||
from virtualization.choices import *
|
from virtualization.choices import *
|
||||||
from virtualization.models import *
|
from virtualization.models import *
|
||||||
@@ -29,7 +29,8 @@ __all__ = (
|
|||||||
class ClusterTypeFilterForm(OrganizationalModelFilterSetForm):
|
class ClusterTypeFilterForm(OrganizationalModelFilterSetForm):
|
||||||
model = ClusterType
|
model = ClusterType
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
@@ -38,7 +39,8 @@ class ClusterGroupFilterForm(ContactModelFilterForm, OrganizationalModelFilterSe
|
|||||||
model = ClusterGroup
|
model = ClusterGroup
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -46,10 +48,11 @@ class ClusterGroupFilterForm(ContactModelFilterForm, OrganizationalModelFilterSe
|
|||||||
class ClusterFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm):
|
class ClusterFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = Cluster
|
model = Cluster
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('group_id', 'type_id', 'status', name=_('Attributes')),
|
FieldSet('group_id', 'type_id', 'status', name=_('Attributes')),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Scope')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Scope')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||||
)
|
)
|
||||||
selector_fields = ('filter_id', 'q', 'group_id')
|
selector_fields = ('filter_id', 'q', 'group_id')
|
||||||
@@ -105,7 +108,7 @@ class VirtualMachineFilterForm(
|
|||||||
):
|
):
|
||||||
model = VirtualMachine
|
model = VirtualMachine
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('cluster_group_id', 'cluster_type_id', 'cluster_id', 'device_id', name=_('Cluster')),
|
FieldSet('cluster_group_id', 'cluster_type_id', 'cluster_id', 'device_id', name=_('Cluster')),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
@@ -113,6 +116,7 @@ class VirtualMachineFilterForm(
|
|||||||
'local_context_data', 'serial', name=_('Attributes')
|
'local_context_data', 'serial', name=_('Attributes')
|
||||||
),
|
),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||||
)
|
)
|
||||||
cluster_group_id = DynamicModelMultipleChoiceField(
|
cluster_group_id = DynamicModelMultipleChoiceField(
|
||||||
@@ -205,14 +209,15 @@ class VirtualMachineFilterForm(
|
|||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
class VMInterfaceFilterForm(NetBoxModelFilterSetForm):
|
class VMInterfaceFilterForm(OwnerFilterMixin, NetBoxModelFilterSetForm):
|
||||||
model = VMInterface
|
model = VMInterface
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('cluster_id', 'virtual_machine_id', name=_('Virtual Machine')),
|
FieldSet('cluster_id', 'virtual_machine_id', name=_('Virtual Machine')),
|
||||||
FieldSet('enabled', name=_('Attributes')),
|
FieldSet('enabled', name=_('Attributes')),
|
||||||
FieldSet('vrf_id', 'l2vpn_id', 'mac_address', name=_('Addressing')),
|
FieldSet('vrf_id', 'l2vpn_id', 'mac_address', name=_('Addressing')),
|
||||||
FieldSet('mode', 'vlan_translation_policy_id', name=_('802.1Q Switching')),
|
FieldSet('mode', 'vlan_translation_policy_id', name=_('802.1Q Switching')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
selector_fields = ('filter_id', 'q', 'virtual_machine_id')
|
selector_fields = ('filter_id', 'q', 'virtual_machine_id')
|
||||||
cluster_id = DynamicModelMultipleChoiceField(
|
cluster_id = DynamicModelMultipleChoiceField(
|
||||||
@@ -259,20 +264,16 @@ class VMInterfaceFilterForm(NetBoxModelFilterSetForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label=_('VLAN Translation Policy')
|
label=_('VLAN Translation Policy')
|
||||||
)
|
)
|
||||||
owner_id = DynamicModelChoiceField(
|
|
||||||
queryset=Owner.objects.all(),
|
|
||||||
required=False,
|
|
||||||
label=_('Owner'),
|
|
||||||
)
|
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
class VirtualDiskFilterForm(NetBoxModelFilterSetForm):
|
class VirtualDiskFilterForm(OwnerFilterMixin, NetBoxModelFilterSetForm):
|
||||||
model = VirtualDisk
|
model = VirtualDisk
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('virtual_machine_id', name=_('Virtual Machine')),
|
FieldSet('virtual_machine_id', name=_('Virtual Machine')),
|
||||||
FieldSet('size', name=_('Attributes')),
|
FieldSet('size', name=_('Attributes')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
virtual_machine_id = DynamicModelMultipleChoiceField(
|
virtual_machine_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=VirtualMachine.objects.all(),
|
queryset=VirtualMachine.objects.all(),
|
||||||
@@ -284,9 +285,4 @@ class VirtualDiskFilterForm(NetBoxModelFilterSetForm):
|
|||||||
required=False,
|
required=False,
|
||||||
min_value=1
|
min_value=1
|
||||||
)
|
)
|
||||||
owner_id = DynamicModelChoiceField(
|
|
||||||
queryset=Owner.objects.all(),
|
|
||||||
required=False,
|
|
||||||
label=_('Owner'),
|
|
||||||
)
|
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ __all__ = (
|
|||||||
class TunnelGroupFilterForm(ContactModelFilterForm, OrganizationalModelFilterSetForm):
|
class TunnelGroupFilterForm(ContactModelFilterForm, OrganizationalModelFilterSetForm):
|
||||||
model = TunnelGroup
|
model = TunnelGroup
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
@@ -42,10 +43,11 @@ class TunnelGroupFilterForm(ContactModelFilterForm, OrganizationalModelFilterSet
|
|||||||
class TunnelFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
|
class TunnelFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = Tunnel
|
model = Tunnel
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('status', 'encapsulation', 'tunnel_id', name=_('Tunnel')),
|
FieldSet('status', 'encapsulation', 'tunnel_id', name=_('Tunnel')),
|
||||||
FieldSet('ipsec_profile_id', name=_('Security')),
|
FieldSet('ipsec_profile_id', name=_('Security')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenancy')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenancy')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||||
)
|
)
|
||||||
status = forms.MultipleChoiceField(
|
status = forms.MultipleChoiceField(
|
||||||
@@ -97,10 +99,11 @@ class TunnelTerminationFilterForm(NetBoxModelFilterSetForm):
|
|||||||
class IKEProposalFilterForm(PrimaryModelFilterSetForm):
|
class IKEProposalFilterForm(PrimaryModelFilterSetForm):
|
||||||
model = IKEProposal
|
model = IKEProposal
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'authentication_method', 'encryption_algorithm', 'authentication_algorithm', 'group', name=_('Parameters')
|
'authentication_method', 'encryption_algorithm', 'authentication_algorithm', 'group', name=_('Parameters')
|
||||||
),
|
),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
authentication_method = forms.MultipleChoiceField(
|
authentication_method = forms.MultipleChoiceField(
|
||||||
label=_('Authentication method'),
|
label=_('Authentication method'),
|
||||||
@@ -128,8 +131,9 @@ class IKEProposalFilterForm(PrimaryModelFilterSetForm):
|
|||||||
class IKEPolicyFilterForm(PrimaryModelFilterSetForm):
|
class IKEPolicyFilterForm(PrimaryModelFilterSetForm):
|
||||||
model = IKEPolicy
|
model = IKEPolicy
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('version', 'mode', 'proposal_id', name=_('Parameters')),
|
FieldSet('version', 'mode', 'proposal_id', name=_('Parameters')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
version = forms.MultipleChoiceField(
|
version = forms.MultipleChoiceField(
|
||||||
label=_('IKE version'),
|
label=_('IKE version'),
|
||||||
@@ -152,8 +156,9 @@ class IKEPolicyFilterForm(PrimaryModelFilterSetForm):
|
|||||||
class IPSecProposalFilterForm(PrimaryModelFilterSetForm):
|
class IPSecProposalFilterForm(PrimaryModelFilterSetForm):
|
||||||
model = IPSecProposal
|
model = IPSecProposal
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('encryption_algorithm', 'authentication_algorithm', name=_('Parameters')),
|
FieldSet('encryption_algorithm', 'authentication_algorithm', name=_('Parameters')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
encryption_algorithm = forms.MultipleChoiceField(
|
encryption_algorithm = forms.MultipleChoiceField(
|
||||||
label=_('Encryption algorithm'),
|
label=_('Encryption algorithm'),
|
||||||
@@ -171,8 +176,9 @@ class IPSecProposalFilterForm(PrimaryModelFilterSetForm):
|
|||||||
class IPSecPolicyFilterForm(PrimaryModelFilterSetForm):
|
class IPSecPolicyFilterForm(PrimaryModelFilterSetForm):
|
||||||
model = IPSecPolicy
|
model = IPSecPolicy
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('proposal_id', 'pfs_group', name=_('Parameters')),
|
FieldSet('proposal_id', 'pfs_group', name=_('Parameters')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
proposal_id = DynamicModelMultipleChoiceField(
|
proposal_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=IKEProposal.objects.all(),
|
queryset=IKEProposal.objects.all(),
|
||||||
@@ -190,8 +196,9 @@ class IPSecPolicyFilterForm(PrimaryModelFilterSetForm):
|
|||||||
class IPSecProfileFilterForm(PrimaryModelFilterSetForm):
|
class IPSecProfileFilterForm(PrimaryModelFilterSetForm):
|
||||||
model = IPSecProfile
|
model = IPSecProfile
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('mode', 'ike_policy_id', 'ipsec_policy_id', name=_('Profile')),
|
FieldSet('mode', 'ike_policy_id', 'ipsec_policy_id', name=_('Profile')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
mode = forms.MultipleChoiceField(
|
mode = forms.MultipleChoiceField(
|
||||||
label=_('Mode'),
|
label=_('Mode'),
|
||||||
@@ -214,9 +221,10 @@ class IPSecProfileFilterForm(PrimaryModelFilterSetForm):
|
|||||||
class L2VPNFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
|
class L2VPNFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = L2VPN
|
model = L2VPN
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('type', 'status', 'import_target_id', 'export_target_id', name=_('Attributes')),
|
FieldSet('type', 'status', 'import_target_id', 'export_target_id', name=_('Attributes')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||||
)
|
)
|
||||||
status = forms.MultipleChoiceField(
|
status = forms.MultipleChoiceField(
|
||||||
|
|||||||
@@ -22,8 +22,9 @@ __all__ = (
|
|||||||
class WirelessLANGroupFilterForm(NestedGroupModelFilterSetForm):
|
class WirelessLANGroupFilterForm(NestedGroupModelFilterSetForm):
|
||||||
model = WirelessLANGroup
|
model = WirelessLANGroup
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('parent_id', name=_('Wireless LAN group')),
|
FieldSet('parent_id', name=_('Wireless LAN group')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
parent_id = DynamicModelMultipleChoiceField(
|
parent_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=WirelessLANGroup.objects.all(),
|
queryset=WirelessLANGroup.objects.all(),
|
||||||
@@ -36,11 +37,12 @@ class WirelessLANGroupFilterForm(NestedGroupModelFilterSetForm):
|
|||||||
class WirelessLANFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
class WirelessLANFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = WirelessLAN
|
model = WirelessLAN
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('ssid', 'group_id', 'status', name=_('Attributes')),
|
FieldSet('ssid', 'group_id', 'status', name=_('Attributes')),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Scope')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Scope')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
|
||||||
FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')),
|
FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')),
|
||||||
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
ssid = forms.CharField(
|
ssid = forms.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
@@ -102,10 +104,11 @@ class WirelessLANFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
|||||||
class WirelessLinkFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
class WirelessLinkFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = WirelessLink
|
model = WirelessLink
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('ssid', 'status', 'distance', 'distance_unit', name=_('Attributes')),
|
FieldSet('ssid', 'status', 'distance', 'distance_unit', name=_('Attributes')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
|
||||||
FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')),
|
FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')),
|
||||||
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
|
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||||
)
|
)
|
||||||
ssid = forms.CharField(
|
ssid = forms.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
|
|||||||
Reference in New Issue
Block a user