mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-28 12:48:15 +01:00
Compare commits
27 Commits
19129-mac-
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a603981b2 | ||
|
|
245495b2fe | ||
|
|
8d3eb69055 | ||
|
|
7e3b60f194 | ||
|
|
5338c842b8 | ||
|
|
9186b0edaa | ||
|
|
d883be9e56 | ||
|
|
6fc7fa6c64 | ||
|
|
3a33df0e43 | ||
|
|
433f46746e | ||
|
|
8f5f91fcfe | ||
|
|
1a2175127e | ||
|
|
e859807d1d | ||
|
|
a8c997ff29 | ||
|
|
4a28ab98f4 | ||
|
|
3636d55017 | ||
|
|
aa69e96818 | ||
|
|
1745d2ae93 | ||
|
|
e097a848dc | ||
|
|
595be6dcd4 | ||
|
|
a9e50238eb | ||
|
|
a9a300197a | ||
|
|
3dcca73ecc | ||
|
|
cedbeb7b19 | ||
|
|
a45b6b170d | ||
|
|
42ecf3cac0 | ||
|
|
af8e53d8fb |
@@ -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:
|
||||
|
||||
```
|
||||
./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)
|
||||
### Python 3.7.10 | Django 3.2.5 | NetBox 3.0
|
||||
### lsmodels() will show available models. Use help(<model>) for more info.
|
||||
### Python v3.12.3 | Django v5.2.10 | NetBox Community v4.5.1
|
||||
### lsapps() & lsmodels() will show available models. Use help(<model>) for more info.
|
||||
```
|
||||
|
||||
The function `lsmodels()` will print a list of all available NetBox models:
|
||||
|
||||
```
|
||||
>>> 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
|
||||
@@ -114,7 +126,7 @@ Reverse relationships can be traversed as well. For example, the following will
|
||||
>>> 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")
|
||||
|
||||
@@ -15,7 +15,7 @@ Some configuration parameters may alternatively be defined either in `configurat
|
||||
|
||||
## 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)
|
||||
* [`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
|
||||
$ psql --username netbox --password --host localhost netbox
|
||||
Password for user netbox:
|
||||
psql (12.5 (Ubuntu 12.5-0ubuntu0.20.04.1))
|
||||
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
|
||||
Password:
|
||||
psql (16.11 (Ubuntu 16.11-0ubuntu0.24.04.1))
|
||||
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
|
||||
Type "help" for help.
|
||||
|
||||
netbox=> \conninfo
|
||||
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
|
||||
```
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ sudo ln -s /opt/netbox-X.Y.Z/ /opt/netbox
|
||||
```
|
||||
|
||||
!!! 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
|
||||
|
||||
@@ -63,12 +63,12 @@ This command should generate output similar to the following:
|
||||
|
||||
```
|
||||
Cloning into '.'...
|
||||
remote: Enumerating objects: 996, done.
|
||||
remote: Counting objects: 100% (996/996), done.
|
||||
remote: Compressing objects: 100% (935/935), done.
|
||||
remote: Total 996 (delta 148), reused 386 (delta 34), pack-reused 0
|
||||
Receiving objects: 100% (996/996), 4.26 MiB | 9.81 MiB/s, done.
|
||||
Resolving deltas: 100% (148/148), done.
|
||||
remote: Enumerating objects: 148317, done.
|
||||
remote: Counting objects: 100% (183/183), done.
|
||||
remote: Compressing objects: 100% (115/115), done.
|
||||
remote: Total 148317 (delta 127), reused 68 (delta 68), pack-reused 148134 (from 3)
|
||||
Receiving objects: 100% (148317/148317), 165.12 MiB | 28.71 MiB/s, 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.
|
||||
@@ -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:
|
||||
|
||||
* `ALLOWED_HOSTS`
|
||||
* `DATABASES` (or `DATABASE`)
|
||||
* `API_TOKEN_PEPPERS`
|
||||
* `DATABASES`
|
||||
* `REDIS`
|
||||
* `SECRET_KEY`
|
||||
|
||||
@@ -158,7 +159,7 @@ DATABASES = {
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -252,7 +253,7 @@ Once NetBox has been configured, we're ready to proceed with the actual installa
|
||||
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
|
||||
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:
|
||||
|
||||
```no-highlight
|
||||
Watching for file changes with StatReloader
|
||||
Performing system checks...
|
||||
|
||||
System check identified no issues (0 silenced).
|
||||
August 30, 2021 - 18:02:23
|
||||
Django version 3.2.6, using settings 'netbox.settings'
|
||||
Starting development server at http://127.0.0.1:8000/
|
||||
January 26, 2026 - 17:00:00
|
||||
Django version 5.2.10, using settings 'netbox.settings'
|
||||
Starting development server at http://0.0.0.0:8000/
|
||||
Quit the server with CONTROL-C.
|
||||
```
|
||||
|
||||
|
||||
@@ -43,16 +43,22 @@ You should see output similar to the following:
|
||||
|
||||
```no-highlight
|
||||
● netbox.service - NetBox WSGI Service
|
||||
Loaded: loaded (/etc/systemd/system/netbox.service; enabled; vendor preset: enabled)
|
||||
Active: active (running) since Mon 2021-08-30 04:02:36 UTC; 14h ago
|
||||
Loaded: loaded (/etc/systemd/system/netbox.service; enabled; preset: enabled)
|
||||
Active: active (running) since Mon 2026-01-26 11:00:00 CST; 7s ago
|
||||
Docs: https://docs.netbox.dev/
|
||||
Main PID: 1140492 (gunicorn)
|
||||
Tasks: 19 (limit: 4683)
|
||||
Memory: 666.2M
|
||||
Main PID: 7283 (gunicorn)
|
||||
Tasks: 6 (limit: 4545)
|
||||
Memory: 556.1M (peak: 556.3M)
|
||||
CPU: 3.387s
|
||||
CGroup: /system.slice/netbox.service
|
||||
├─1140492 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /va>
|
||||
├─1140513 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /va>
|
||||
├─1140514 /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>
|
||||
├─7285 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/netbox>
|
||||
├─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.
|
||||
|
||||
!!! 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
|
||||
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
|
||||
</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:
|
||||
|
||||
1. [PostgreSQL database](1-postgresql.md)
|
||||
1. [Redis](2-redis.md)
|
||||
2. [Redis](2-redis.md)
|
||||
3. [NetBox components](3-netbox.md)
|
||||
4. [Gunicorn](4a-gunicorn.md) or [uWSGI](4b-uwsgi.md)
|
||||
5. [HTTP server](5-http-server.md)
|
||||
|
||||
@@ -65,7 +65,7 @@ Download and extract the latest version:
|
||||
|
||||
```no-highlight
|
||||
# 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
|
||||
sudo tar -xzf v$NEWVER.tar.gz -C /opt
|
||||
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
|
||||
# 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/netbox/netbox/configuration.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 && \
|
||||
sudo git fetch --tags && \
|
||||
sudo git checkout v4.2.7
|
||||
sudo git checkout v4.5.0
|
||||
```
|
||||
|
||||
## 4. Run the Upgrade Script
|
||||
@@ -128,7 +128,7 @@ sudo ./upgrade.sh
|
||||
```
|
||||
|
||||
!!! 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
|
||||
sudo PYTHON=/usr/bin/python3.12 ./upgrade.sh
|
||||
|
||||
@@ -34,9 +34,10 @@ __all__ = (
|
||||
class ProviderFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||
model = Provider
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
|
||||
FieldSet('asn_id', name=_('ASN')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
@@ -69,8 +70,9 @@ class ProviderFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||
class ProviderAccountFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||
model = ProviderAccount
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('provider_id', 'account', name=_('Attributes')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
)
|
||||
provider_id = DynamicModelMultipleChoiceField(
|
||||
@@ -88,8 +90,9 @@ class ProviderAccountFilterForm(ContactModelFilterForm, PrimaryModelFilterSetFor
|
||||
class ProviderNetworkFilterForm(PrimaryModelFilterSetForm):
|
||||
model = ProviderNetwork
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('provider_id', 'service_id', name=_('Attributes')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
provider_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
@@ -107,8 +110,9 @@ class ProviderNetworkFilterForm(PrimaryModelFilterSetForm):
|
||||
class CircuitTypeFilterForm(OrganizationalModelFilterSetForm):
|
||||
model = CircuitType
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('color', name=_('Attributes')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -121,7 +125,7 @@ class CircuitTypeFilterForm(OrganizationalModelFilterSetForm):
|
||||
class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||
model = Circuit
|
||||
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(
|
||||
'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('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
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')
|
||||
@@ -274,8 +279,9 @@ class CircuitTerminationFilterForm(NetBoxModelFilterSetForm):
|
||||
class CircuitGroupFilterForm(TenancyFilterForm, OrganizationalModelFilterSetForm):
|
||||
model = CircuitGroup
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -312,8 +318,9 @@ class CircuitGroupAssignmentFilterForm(NetBoxModelFilterSetForm):
|
||||
class VirtualCircuitTypeFilterForm(OrganizationalModelFilterSetForm):
|
||||
model = VirtualCircuitType
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('color', name=_('Attributes')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -326,10 +333,11 @@ class VirtualCircuitTypeFilterForm(OrganizationalModelFilterSetForm):
|
||||
class VirtualCircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||
model = VirtualCircuit
|
||||
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('type_id', 'status', name=_('Attributes')),
|
||||
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')
|
||||
provider_id = DynamicModelMultipleChoiceField(
|
||||
|
||||
@@ -26,8 +26,9 @@ __all__ = (
|
||||
class DataSourceFilterForm(PrimaryModelFilterSetForm):
|
||||
model = DataSource
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('type', 'status', 'enabled', 'sync_interval', name=_('Data Source')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
label=_('Type'),
|
||||
|
||||
@@ -9,6 +9,7 @@ from django.db import connection, models
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from netbox.context import query_cache
|
||||
from netbox.plugins import PluginConfig
|
||||
from netbox.registry import registry
|
||||
from utilities.string import title
|
||||
@@ -70,6 +71,12 @@ class ObjectTypeManager(models.Manager):
|
||||
"""
|
||||
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
|
||||
# If the ObjectType table has not yet been provisioned (e.g. because we're in a pre-v4.4 migration),
|
||||
# fall back to ContentType.
|
||||
@@ -96,6 +103,10 @@ class ObjectTypeManager(models.Manager):
|
||||
features=get_model_features(model),
|
||||
)[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
|
||||
|
||||
def get_for_models(self, *models, for_concrete_models=True):
|
||||
|
||||
@@ -12,11 +12,12 @@ from netbox.forms import (
|
||||
NestedGroupModelFilterSetForm, NetBoxModelFilterSetForm, OrganizationalModelFilterSetForm,
|
||||
PrimaryModelFilterSetForm,
|
||||
)
|
||||
from netbox.forms.mixins import OwnerFilterMixin
|
||||
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
|
||||
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.fields import ColorField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField
|
||||
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.widgets import NumberWithOptions
|
||||
from virtualization.models import Cluster, ClusterGroup, VirtualMachine
|
||||
@@ -70,11 +71,11 @@ __all__ = (
|
||||
'SiteFilterForm',
|
||||
'SiteGroupFilterForm',
|
||||
'VirtualChassisFilterForm',
|
||||
'VirtualDeviceContextFilterForm'
|
||||
'VirtualDeviceContextFilterForm',
|
||||
)
|
||||
|
||||
|
||||
class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
|
||||
class DeviceComponentFilterForm(OwnerFilterMixin, NetBoxModelFilterSetForm):
|
||||
name = forms.CharField(
|
||||
label=_('Name'),
|
||||
required=False
|
||||
@@ -157,18 +158,14 @@ class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
|
||||
required=False,
|
||||
label=_('Device Status'),
|
||||
)
|
||||
owner_id = DynamicModelChoiceField(
|
||||
queryset=Owner.objects.all(),
|
||||
required=False,
|
||||
label=_('Owner'),
|
||||
)
|
||||
|
||||
|
||||
class RegionFilterForm(ContactModelFilterForm, NestedGroupModelFilterSetForm):
|
||||
model = Region
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('parent_id', name=_('Region')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts'))
|
||||
)
|
||||
parent_id = DynamicModelMultipleChoiceField(
|
||||
@@ -182,8 +179,9 @@ class RegionFilterForm(ContactModelFilterForm, NestedGroupModelFilterSetForm):
|
||||
class SiteGroupFilterForm(ContactModelFilterForm, NestedGroupModelFilterSetForm):
|
||||
model = SiteGroup
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('parent_id', name=_('Site Group')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts'))
|
||||
)
|
||||
parent_id = DynamicModelMultipleChoiceField(
|
||||
@@ -197,9 +195,10 @@ class SiteGroupFilterForm(ContactModelFilterForm, NestedGroupModelFilterSetForm)
|
||||
class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||
model = Site
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('status', 'region_id', 'group_id', 'asn_id', name=_('Attributes')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'region_id', 'group_id')
|
||||
@@ -229,9 +228,10 @@ class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilt
|
||||
class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NestedGroupModelFilterSetForm):
|
||||
model = Location
|
||||
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('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
@@ -277,7 +277,8 @@ class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NestedGroupM
|
||||
class RackRoleFilterForm(OrganizationalModelFilterSetForm):
|
||||
model = RackRole
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -328,10 +329,11 @@ class RackBaseFilterForm(PrimaryModelFilterSetForm):
|
||||
class RackTypeFilterForm(RackBaseFilterForm):
|
||||
model = RackType
|
||||
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('starting_unit', 'desc_units', name=_('Numbering')),
|
||||
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'manufacturer_id')
|
||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||
@@ -350,13 +352,14 @@ class RackTypeFilterForm(RackBaseFilterForm):
|
||||
class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, RackBaseFilterForm):
|
||||
model = Rack
|
||||
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('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('status', 'role_id', 'manufacturer_id', 'rack_type_id', 'serial', 'asset_tag', name=_('Rack')),
|
||||
FieldSet('form_factor', 'width', 'u_height', 'airflow', name=_('Hardware')),
|
||||
FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
|
||||
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')),
|
||||
)
|
||||
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('status', 'role_id', name=_('Function')),
|
||||
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('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(
|
||||
queryset=Rack.objects.all(),
|
||||
@@ -451,10 +455,11 @@ class RackElevationFilterForm(RackFilterForm):
|
||||
class RackReservationFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = RackReservation
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('status', 'user_id', name=_('Reservation')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Rack')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
@@ -509,7 +514,8 @@ class RackReservationFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
class ManufacturerFilterForm(ContactModelFilterForm, OrganizationalModelFilterSetForm):
|
||||
model = Manufacturer
|
||||
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'))
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
@@ -518,7 +524,7 @@ class ManufacturerFilterForm(ContactModelFilterForm, OrganizationalModelFilterSe
|
||||
class DeviceTypeFilterForm(PrimaryModelFilterSetForm):
|
||||
model = DeviceType
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet(
|
||||
'manufacturer_id', 'default_platform_id', 'part_number', 'device_count',
|
||||
'subdevice_role', 'airflow', name=_('Hardware')
|
||||
@@ -529,6 +535,7 @@ class DeviceTypeFilterForm(PrimaryModelFilterSetForm):
|
||||
'pass_through_ports', 'device_bays', 'module_bays', 'inventory_items', name=_('Components')
|
||||
),
|
||||
FieldSet('weight', 'weight_unit', name=_('Weight')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'manufacturer_id')
|
||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||
@@ -652,7 +659,8 @@ class DeviceTypeFilterForm(PrimaryModelFilterSetForm):
|
||||
class ModuleTypeProfileFilterForm(PrimaryModelFilterSetForm):
|
||||
model = ModuleTypeProfile
|
||||
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')
|
||||
tag = TagFilterField(model)
|
||||
@@ -661,7 +669,7 @@ class ModuleTypeProfileFilterForm(PrimaryModelFilterSetForm):
|
||||
class ModuleTypeFilterForm(PrimaryModelFilterSetForm):
|
||||
model = ModuleType
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet(
|
||||
'profile_id', 'manufacturer_id', 'part_number', 'module_count',
|
||||
'airflow', name=_('Hardware')
|
||||
@@ -671,6 +679,7 @@ class ModuleTypeFilterForm(PrimaryModelFilterSetForm):
|
||||
'pass_through_ports', name=_('Components')
|
||||
),
|
||||
FieldSet('weight', 'weight_unit', name=_('Weight')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'manufacturer_id')
|
||||
profile_id = DynamicModelMultipleChoiceField(
|
||||
@@ -754,8 +763,9 @@ class ModuleTypeFilterForm(PrimaryModelFilterSetForm):
|
||||
class DeviceRoleFilterForm(NestedGroupModelFilterSetForm):
|
||||
model = DeviceRole
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('parent_id', 'config_template_id', name=_('Device Role'))
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('parent_id', 'config_template_id', name=_('Device Role')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
config_template_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ConfigTemplate.objects.all(),
|
||||
@@ -773,8 +783,9 @@ class DeviceRoleFilterForm(NestedGroupModelFilterSetForm):
|
||||
class PlatformFilterForm(NestedGroupModelFilterSetForm):
|
||||
model = Platform
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('manufacturer_id', 'parent_id', 'config_template_id', name=_('Platform'))
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
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')
|
||||
parent_id = DynamicModelMultipleChoiceField(
|
||||
@@ -803,11 +814,12 @@ class DeviceFilterForm(
|
||||
):
|
||||
model = Device
|
||||
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('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address', name=_('Operation')),
|
||||
FieldSet('manufacturer_id', 'device_type_id', 'platform_id', name=_('Hardware')),
|
||||
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(
|
||||
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports',
|
||||
@@ -996,9 +1008,10 @@ class DeviceFilterForm(
|
||||
class VirtualDeviceContextFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = VirtualDeviceContext
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('device', 'status', 'has_primary_ip', name=_('Attributes')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
device = DynamicModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
@@ -1023,9 +1036,10 @@ class VirtualDeviceContextFilterForm(TenancyFilterForm, PrimaryModelFilterSetFor
|
||||
class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = Module
|
||||
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('manufacturer_id', 'module_type_id', 'status', 'serial', 'asset_tag', name=_('Hardware')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
device_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
@@ -1106,9 +1120,10 @@ class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, PrimaryM
|
||||
class VirtualChassisFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = VirtualChassis
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
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(
|
||||
queryset=Region.objects.all(),
|
||||
@@ -1135,10 +1150,11 @@ class VirtualChassisFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
class CableFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = Cable
|
||||
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('type', 'status', 'profile', 'color', 'length', 'length_unit', 'unterminated', name=_('Attributes')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
@@ -1224,8 +1240,9 @@ class CableFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
class PowerPanelFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||
model = PowerPanel
|
||||
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('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'site_id', 'location_id')
|
||||
@@ -1263,10 +1280,11 @@ class PowerPanelFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||
class PowerFeedFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = PowerFeed
|
||||
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('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
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(
|
||||
queryset=Region.objects.all(),
|
||||
@@ -1390,7 +1408,7 @@ class PathEndpointFilterForm(CabledFilterForm):
|
||||
class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
model = ConsolePort
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet(
|
||||
@@ -1398,6 +1416,7 @@ class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
label=_('Type'),
|
||||
@@ -1429,7 +1448,7 @@ class ConsolePortTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
||||
class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
model = ConsoleServerPort
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet(
|
||||
@@ -1437,6 +1456,7 @@ class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterF
|
||||
name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
label=_('Type'),
|
||||
@@ -1468,7 +1488,7 @@ class ConsoleServerPortTemplateFilterForm(ModularDeviceComponentTemplateFilterFo
|
||||
class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
model = PowerPort
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', 'label', 'type', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet(
|
||||
@@ -1476,6 +1496,7 @@ class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
label=_('Type'),
|
||||
@@ -1502,7 +1523,7 @@ class PowerPortTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
||||
class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
model = PowerOutlet
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', 'label', 'type', 'color', 'status', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet(
|
||||
@@ -1510,6 +1531,7 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
label=_('Type'),
|
||||
@@ -1545,7 +1567,7 @@ class PowerOutletTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
||||
class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
model = Interface
|
||||
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('vrf_id', 'l2vpn_id', 'mac_address', 'wwn', name=_('Addressing')),
|
||||
FieldSet('poe_mode', 'poe_type', name=_('PoE')),
|
||||
@@ -1558,6 +1580,7 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'device_id')
|
||||
vdc_id = DynamicModelMultipleChoiceField(
|
||||
@@ -1716,7 +1739,7 @@ class InterfaceTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
||||
|
||||
class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet(
|
||||
@@ -1724,6 +1747,7 @@ class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
||||
name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'occupied', name=_('Cable')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
model = FrontPort
|
||||
type = forms.MultipleChoiceField(
|
||||
@@ -1759,7 +1783,7 @@ class FrontPortTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
||||
class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
||||
model = RearPort
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet(
|
||||
@@ -1767,6 +1791,7 @@ class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
||||
name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'occupied', name=_('Cable')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
label=_('Type'),
|
||||
@@ -1801,13 +1826,14 @@ class RearPortTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
||||
class ModuleBayFilterForm(DeviceComponentFilterForm):
|
||||
model = ModuleBay
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', 'label', 'position', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet(
|
||||
'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
name=_('Device')
|
||||
),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
position = forms.CharField(
|
||||
@@ -1832,13 +1858,14 @@ class ModuleBayTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
||||
class DeviceBayFilterForm(DeviceComponentFilterForm):
|
||||
model = DeviceBay
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', 'label', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet(
|
||||
'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
name=_('Device')
|
||||
),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -1855,7 +1882,7 @@ class DeviceBayTemplateFilterForm(DeviceComponentTemplateFilterForm):
|
||||
class InventoryItemFilterForm(DeviceComponentFilterForm):
|
||||
model = InventoryItem
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet(
|
||||
'name', 'label', 'status', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered',
|
||||
name=_('Attributes')
|
||||
@@ -1865,6 +1892,7 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
|
||||
'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
name=_('Device')
|
||||
),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
role_id = DynamicModelMultipleChoiceField(
|
||||
queryset=InventoryItemRole.objects.all(),
|
||||
@@ -1925,7 +1953,8 @@ class InventoryItemTemplateFilterForm(DeviceComponentTemplateFilterForm):
|
||||
class InventoryItemRoleFilterForm(OrganizationalModelFilterSetForm):
|
||||
model = InventoryItemRole
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -1937,9 +1966,10 @@ class InventoryItemRoleFilterForm(OrganizationalModelFilterSetForm):
|
||||
class MACAddressFilterForm(PrimaryModelFilterSetForm):
|
||||
model = MACAddress
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('mac_address', name=_('Attributes')),
|
||||
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')
|
||||
mac_address = forms.CharField(
|
||||
|
||||
@@ -75,7 +75,7 @@ class ScopedForm(forms.Form):
|
||||
except ObjectDoesNotExist:
|
||||
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
|
||||
|
||||
else:
|
||||
|
||||
@@ -20,7 +20,9 @@ from utilities.forms.fields import (
|
||||
DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SlugField,
|
||||
)
|
||||
from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups
|
||||
from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK
|
||||
from utilities.forms.widgets import (
|
||||
APISelect, ClearableFileInput, ClearableSelect, HTMXSelect, NumberWithOptions, SelectWithPK,
|
||||
)
|
||||
from utilities.jsonschema import JSONSchemaProperty
|
||||
from virtualization.models import Cluster, VMInterface
|
||||
from wireless.models import WirelessLAN, WirelessLANGroup
|
||||
@@ -592,6 +594,14 @@ class DeviceForm(TenancyForm, PrimaryModelForm):
|
||||
},
|
||||
)
|
||||
)
|
||||
face = forms.ChoiceField(
|
||||
label=_('Face'),
|
||||
choices=add_blank_choice(DeviceFaceChoices),
|
||||
required=False,
|
||||
widget=ClearableSelect(
|
||||
requires_fields=['rack']
|
||||
)
|
||||
)
|
||||
device_type = DynamicModelChoiceField(
|
||||
label=_('Device type'),
|
||||
queryset=DeviceType.objects.all(),
|
||||
|
||||
@@ -3,6 +3,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim.models import *
|
||||
from netbox.forms import NetBoxModelForm
|
||||
from netbox.forms.mixins import OwnerMixin
|
||||
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField
|
||||
from utilities.forms.rendering import FieldSet, TabbedGroups
|
||||
from utilities.forms.widgets import APISelect
|
||||
@@ -271,7 +272,7 @@ class InventoryItemCreateForm(ComponentCreateForm, model_forms.InventoryItemForm
|
||||
# Virtual chassis
|
||||
#
|
||||
|
||||
class VirtualChassisCreateForm(NetBoxModelForm):
|
||||
class VirtualChassisCreateForm(OwnerMixin, NetBoxModelForm):
|
||||
region = DynamicModelChoiceField(
|
||||
label=_('Region'),
|
||||
queryset=Region.objects.all(),
|
||||
|
||||
@@ -550,6 +550,10 @@ class InterfaceFilter(
|
||||
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
|
||||
def connected(self, queryset, value: bool, prefix: str):
|
||||
if value is True:
|
||||
@@ -889,7 +893,7 @@ class PowerPortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedM
|
||||
|
||||
|
||||
@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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
@@ -734,7 +734,7 @@ class PowerPortTemplateType(ModularComponentTemplateType):
|
||||
filters=RackTypeFilter,
|
||||
pagination=True
|
||||
)
|
||||
class RackTypeType(PrimaryObjectType):
|
||||
class RackTypeType(ImageAttachmentsMixin, PrimaryObjectType):
|
||||
rack_count: BigInt
|
||||
manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')]
|
||||
|
||||
|
||||
@@ -155,6 +155,8 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
|
||||
'description': self.description,
|
||||
'weight': float(self.weight) if self.weight is not None else None,
|
||||
'weight_unit': self.weight_unit,
|
||||
'airflow': self.airflow,
|
||||
'attribute_data': self.attribute_data,
|
||||
'comments': self.comments,
|
||||
}
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ class RackBase(WeightMixin, PrimaryModel):
|
||||
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.
|
||||
Each Rack is assigned to a Site and (optionally) a Location.
|
||||
|
||||
@@ -27,6 +27,7 @@ __all__ = (
|
||||
'DeviceTable',
|
||||
'FrontPortTable',
|
||||
'InterfaceTable',
|
||||
'InterfaceLAGMemberTable',
|
||||
'InventoryItemRoleTable',
|
||||
'InventoryItemTable',
|
||||
'MACAddressTable',
|
||||
@@ -689,6 +690,33 @@ class InterfaceTable(BaseInterfaceTable, ModularDeviceComponentTable, PathEndpoi
|
||||
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):
|
||||
name = tables.TemplateColumn(
|
||||
verbose_name=_('Name'),
|
||||
|
||||
@@ -24,6 +24,24 @@ INTERFACE_LINKTERMINATION = """
|
||||
{% 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 = """
|
||||
{% load helpers %}
|
||||
{% if record.length %}{{ record.length|floatformat:"-2" }} {{ record.length_unit }}{% endif %}
|
||||
|
||||
@@ -880,6 +880,7 @@ class RackTypeView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
panels.RackWeightPanel(title=_('Weight'), exclude=['total_weight']),
|
||||
CustomFieldsPanel(),
|
||||
RelatedObjectsPanel(),
|
||||
ImageAttachmentsPanel(),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -3135,6 +3136,14 @@ class InterfaceView(generic.ObjectView):
|
||||
)
|
||||
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
|
||||
vlans = []
|
||||
if instance.untagged_vlan is not None:
|
||||
@@ -3164,6 +3173,7 @@ class InterfaceView(generic.ObjectView):
|
||||
'bridge_interfaces': bridge_interfaces,
|
||||
'bridge_interfaces_table': bridge_interfaces_table,
|
||||
'child_interfaces_table': child_interfaces_table,
|
||||
'lag_interfaces_table': lag_interfaces_table,
|
||||
'vlan_table': vlan_table,
|
||||
'vlan_translation_table': vlan_translation_table,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from collections import UserDict, defaultdict
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
@@ -12,7 +12,6 @@ from core.models import ObjectType
|
||||
from netbox.config import get_config
|
||||
from netbox.constants import RQ_QUEUE_DEFAULT
|
||||
from netbox.models.features import has_feature
|
||||
from users.models import User
|
||||
from utilities.api import get_serializer_for_model
|
||||
from utilities.request import copy_safe_request
|
||||
from utilities.rqworker import get_rq_retry
|
||||
@@ -23,6 +22,21 @@ from .models import EventRule
|
||||
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):
|
||||
"""
|
||||
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
|
||||
key = f'{app_label}.{model_name}:{instance.pk}'
|
||||
if key in queue:
|
||||
queue[key]['data'] = serialize_for_event(instance)
|
||||
queue[key]['snapshots']['postchange'] = get_snapshots(instance, event_type)['postchange']
|
||||
# If the object is being deleted, update any prior "update" event to "delete"
|
||||
if event_type == OBJECT_DELETED:
|
||||
queue[key]['event_type'] = event_type
|
||||
else:
|
||||
queue[key] = {
|
||||
'object_type': ObjectType.objects.get_for_model(instance),
|
||||
'object_id': instance.pk,
|
||||
'event_type': event_type,
|
||||
'data': serialize_for_event(instance),
|
||||
'snapshots': get_snapshots(instance, event_type),
|
||||
'request': request,
|
||||
queue[key] = EventContext(
|
||||
object_type=ObjectType.objects.get_for_model(instance),
|
||||
object_id=instance.pk,
|
||||
object=instance,
|
||||
event_type=event_type,
|
||||
snapshots=get_snapshots(instance, event_type),
|
||||
request=request,
|
||||
user=request.user,
|
||||
# Legacy request attributes for backward compatibility
|
||||
'username': request.user.username,
|
||||
'request_id': request.id,
|
||||
}
|
||||
username=request.user.username,
|
||||
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):
|
||||
user = User.objects.get(username=username) if username else None
|
||||
def process_event_rules(event_rules, object_type, event):
|
||||
"""
|
||||
Process a list of EventRules against an event.
|
||||
"""
|
||||
|
||||
for event_rule in event_rules:
|
||||
|
||||
# Evaluate event rule conditions (if any)
|
||||
if not event_rule.eval_conditions(data):
|
||||
if not event_rule.eval_conditions(event['data']):
|
||||
continue
|
||||
|
||||
# Compile event data
|
||||
event_data = event_rule.action_data or {}
|
||||
event_data.update(data)
|
||||
event_data.update(event['data'])
|
||||
|
||||
# Webhooks
|
||||
if event_rule.action_type == EventRuleActionChoices.WEBHOOK:
|
||||
@@ -109,25 +128,20 @@ def process_event_rules(event_rules, object_type, event_type, data, username=Non
|
||||
params = {
|
||||
"event_rule": event_rule,
|
||||
"object_type": object_type,
|
||||
"event_type": event_type,
|
||||
"event_type": event['event_type'],
|
||||
"data": event_data,
|
||||
"snapshots": snapshots,
|
||||
"snapshots": event.get('snapshots'),
|
||||
"timestamp": timezone.now().isoformat(),
|
||||
"username": username,
|
||||
"username": event['username'],
|
||||
"retry": get_rq_retry()
|
||||
}
|
||||
if snapshots:
|
||||
params["snapshots"] = snapshots
|
||||
if request:
|
||||
if 'request' in event:
|
||||
# Exclude FILES - webhooks don't need uploaded files,
|
||||
# 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
|
||||
rq_queue.enqueue(
|
||||
"extras.webhooks.send_webhook",
|
||||
**params
|
||||
)
|
||||
rq_queue.enqueue('extras.webhooks.send_webhook', **params)
|
||||
|
||||
# Scripts
|
||||
elif event_rule.action_type == EventRuleActionChoices.SCRIPT:
|
||||
@@ -139,16 +153,16 @@ def process_event_rules(event_rules, object_type, event_type, data, username=Non
|
||||
params = {
|
||||
"instance": event_rule.action_object,
|
||||
"name": script.name,
|
||||
"user": user,
|
||||
"user": event['user'],
|
||||
"data": event_data
|
||||
}
|
||||
if snapshots:
|
||||
params["snapshots"] = snapshots
|
||||
if request:
|
||||
params["request"] = copy_safe_request(request)
|
||||
ScriptJob.enqueue(
|
||||
**params
|
||||
)
|
||||
if 'snapshots' in event:
|
||||
params['snapshots'] = event['snapshots']
|
||||
if 'request' in event:
|
||||
params['request'] = copy_safe_request(event['request'])
|
||||
|
||||
# Enqueue the job
|
||||
ScriptJob.enqueue(**params)
|
||||
|
||||
# Notification groups
|
||||
elif event_rule.action_type == EventRuleActionChoices.NOTIFICATION:
|
||||
@@ -157,7 +171,7 @@ def process_event_rules(event_rules, object_type, event_type, data, username=Non
|
||||
object_type=object_type,
|
||||
object_id=event_data['id'],
|
||||
object_repr=event_data.get('display'),
|
||||
event_type=event_type
|
||||
event_type=event['event_type']
|
||||
)
|
||||
|
||||
else:
|
||||
@@ -169,6 +183,8 @@ def process_event_rules(event_rules, object_type, event_type, data, username=Non
|
||||
def process_event_queue(events):
|
||||
"""
|
||||
Flush a list of object representation to RQ for EventRule processing.
|
||||
|
||||
This is the default processor listed in EVENTS_PIPELINE.
|
||||
"""
|
||||
events_cache = defaultdict(dict)
|
||||
|
||||
@@ -188,11 +204,7 @@ def process_event_queue(events):
|
||||
process_event_rules(
|
||||
event_rules=event_rules,
|
||||
object_type=object_type,
|
||||
event_type=event['event_type'],
|
||||
data=event['data'],
|
||||
username=event['username'],
|
||||
snapshots=event['snapshots'],
|
||||
request=event['request'],
|
||||
event=event,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -7,13 +7,12 @@ from extras.choices import *
|
||||
from extras.models import *
|
||||
from netbox.events import get_event_type_choices
|
||||
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 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.fields import (
|
||||
ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
||||
TagFilterField,
|
||||
ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField,
|
||||
)
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.widgets import DateTimePicker
|
||||
@@ -39,7 +38,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
|
||||
class CustomFieldFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm):
|
||||
model = CustomField
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id'),
|
||||
@@ -47,6 +46,7 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
|
||||
FieldSet('choice_set_id', 'related_object_type_id', name=_('Type Options')),
|
||||
FieldSet('ui_visible', 'ui_editable', 'is_cloneable', name=_('Behavior')),
|
||||
FieldSet('validation_minimum', 'validation_maximum', 'validation_regex', name=_('Validation')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
object_type_id = ContentTypeMultipleChoiceField(
|
||||
queryset=ObjectType.objects.with_feature('custom_fields'),
|
||||
@@ -119,18 +119,14 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
|
||||
label=_('Validation regex'),
|
||||
required=False
|
||||
)
|
||||
owner_id = DynamicModelChoiceField(
|
||||
queryset=Owner.objects.all(),
|
||||
required=False,
|
||||
label=_('Owner'),
|
||||
)
|
||||
|
||||
|
||||
class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm):
|
||||
class CustomFieldChoiceSetFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm):
|
||||
model = CustomFieldChoiceSet
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id'),
|
||||
FieldSet('base_choices', 'choice', name=_('Choices')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
base_choices = forms.MultipleChoiceField(
|
||||
choices=CustomFieldChoiceSetBaseChoices,
|
||||
@@ -139,18 +135,14 @@ class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm):
|
||||
choice = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
owner_id = DynamicModelChoiceField(
|
||||
queryset=Owner.objects.all(),
|
||||
required=False,
|
||||
label=_('Owner'),
|
||||
)
|
||||
|
||||
|
||||
class CustomLinkFilterForm(SavedFiltersMixin, FilterForm):
|
||||
class CustomLinkFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm):
|
||||
model = CustomLink
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id'),
|
||||
FieldSet('object_type_id', 'enabled', 'new_window', 'weight', name=_('Attributes')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
object_type_id = ContentTypeMultipleChoiceField(
|
||||
label=_('Object types'),
|
||||
@@ -175,19 +167,15 @@ class CustomLinkFilterForm(SavedFiltersMixin, FilterForm):
|
||||
label=_('Weight'),
|
||||
required=False
|
||||
)
|
||||
owner_id = DynamicModelChoiceField(
|
||||
queryset=Owner.objects.all(),
|
||||
required=False,
|
||||
label=_('Owner'),
|
||||
)
|
||||
|
||||
|
||||
class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
|
||||
class ExportTemplateFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm):
|
||||
model = ExportTemplate
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'object_type_id'),
|
||||
FieldSet('data_source_id', 'data_file_id', name=_('Data')),
|
||||
FieldSet('mime_type', 'file_name', 'file_extension', 'as_attachment', name=_('Rendering')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
data_source_id = DynamicModelMultipleChoiceField(
|
||||
queryset=DataSource.objects.all(),
|
||||
@@ -226,11 +214,6 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
owner_id = DynamicModelChoiceField(
|
||||
queryset=Owner.objects.all(),
|
||||
required=False,
|
||||
label=_('Owner'),
|
||||
)
|
||||
|
||||
|
||||
class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
|
||||
@@ -250,11 +233,12 @@ class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
|
||||
)
|
||||
|
||||
|
||||
class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
|
||||
class SavedFilterFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm):
|
||||
model = SavedFilter
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id'),
|
||||
FieldSet('object_type_id', 'enabled', 'shared', 'weight', name=_('Attributes')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
object_type_id = ContentTypeMultipleChoiceField(
|
||||
label=_('Object types'),
|
||||
@@ -279,11 +263,6 @@ class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
|
||||
label=_('Weight'),
|
||||
required=False
|
||||
)
|
||||
owner_id = DynamicModelChoiceField(
|
||||
queryset=Owner.objects.all(),
|
||||
required=False,
|
||||
label=_('Owner'),
|
||||
)
|
||||
|
||||
|
||||
class TableConfigFilterForm(SavedFiltersMixin, FilterForm):
|
||||
@@ -317,11 +296,12 @@ class TableConfigFilterForm(SavedFiltersMixin, FilterForm):
|
||||
)
|
||||
|
||||
|
||||
class WebhookFilterForm(NetBoxModelFilterSetForm):
|
||||
class WebhookFilterForm(OwnerFilterMixin, NetBoxModelFilterSetForm):
|
||||
model = Webhook
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('payload_url', 'http_method', 'http_content_type', name=_('Attributes')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
http_content_type = forms.CharField(
|
||||
label=_('HTTP content type'),
|
||||
@@ -336,19 +316,15 @@ class WebhookFilterForm(NetBoxModelFilterSetForm):
|
||||
required=False,
|
||||
label=_('HTTP method')
|
||||
)
|
||||
owner_id = DynamicModelChoiceField(
|
||||
queryset=Owner.objects.all(),
|
||||
required=False,
|
||||
label=_('Owner'),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class EventRuleFilterForm(NetBoxModelFilterSetForm):
|
||||
class EventRuleFilterForm(OwnerFilterMixin, NetBoxModelFilterSetForm):
|
||||
model = EventRule
|
||||
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('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
object_type_id = ContentTypeMultipleChoiceField(
|
||||
queryset=ObjectType.objects.with_feature('event_rules'),
|
||||
@@ -372,16 +348,16 @@ class EventRuleFilterForm(NetBoxModelFilterSetForm):
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
owner_id = DynamicModelChoiceField(
|
||||
queryset=Owner.objects.all(),
|
||||
required=False,
|
||||
label=_('Owner'),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class TagFilterForm(SavedFiltersMixin, FilterForm):
|
||||
class TagFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm):
|
||||
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(
|
||||
queryset=ObjectType.objects.with_feature('tags'),
|
||||
required=False,
|
||||
@@ -392,11 +368,6 @@ class TagFilterForm(SavedFiltersMixin, FilterForm):
|
||||
required=False,
|
||||
label=_('Allowed object type')
|
||||
)
|
||||
owner_id = DynamicModelChoiceField(
|
||||
queryset=Owner.objects.all(),
|
||||
required=False,
|
||||
label=_('Owner'),
|
||||
)
|
||||
|
||||
|
||||
class ConfigContextProfileFilterForm(PrimaryModelFilterSetForm):
|
||||
@@ -404,6 +375,7 @@ class ConfigContextProfileFilterForm(PrimaryModelFilterSetForm):
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id'),
|
||||
FieldSet('data_source_id', 'data_file_id', name=_('Data')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
data_source_id = DynamicModelMultipleChoiceField(
|
||||
queryset=DataSource.objects.all(),
|
||||
@@ -420,16 +392,17 @@ class ConfigContextProfileFilterForm(PrimaryModelFilterSetForm):
|
||||
)
|
||||
|
||||
|
||||
class ConfigContextFilterForm(SavedFiltersMixin, FilterForm):
|
||||
class ConfigContextFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm):
|
||||
model = ConfigContext
|
||||
fieldsets = (
|
||||
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('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')),
|
||||
FieldSet('device_type_id', 'platform_id', 'device_role_id', name=_('Device')),
|
||||
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(
|
||||
queryset=ConfigContextProfile.objects.all(),
|
||||
@@ -514,19 +487,15 @@ class ConfigContextFilterForm(SavedFiltersMixin, FilterForm):
|
||||
required=False,
|
||||
label=_('Tags')
|
||||
)
|
||||
owner_id = DynamicModelChoiceField(
|
||||
queryset=Owner.objects.all(),
|
||||
required=False,
|
||||
label=_('Owner'),
|
||||
)
|
||||
|
||||
|
||||
class ConfigTemplateFilterForm(SavedFiltersMixin, FilterForm):
|
||||
class ConfigTemplateFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm):
|
||||
model = ConfigTemplate
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
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(
|
||||
queryset=DataSource.objects.all(),
|
||||
@@ -568,11 +537,6 @@ class ConfigTemplateFilterForm(SavedFiltersMixin, FilterForm):
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
owner_id = DynamicModelChoiceField(
|
||||
queryset=Owner.objects.all(),
|
||||
required=False,
|
||||
label=_('Owner'),
|
||||
)
|
||||
|
||||
|
||||
class LocalConfigContextFilterForm(forms.Form):
|
||||
|
||||
@@ -178,6 +178,13 @@ class CustomFieldChoiceSetForm(ChangelogMessageMixin, OwnerMixin, forms.ModelFor
|
||||
) + ' <code>choice1:First Choice</code>')
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
'name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically',
|
||||
name=_('Custom Field Choice Set')
|
||||
),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CustomFieldChoiceSet
|
||||
fields = ('name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically', 'owner')
|
||||
|
||||
@@ -137,7 +137,7 @@ class ScriptModule(PythonModuleMixin, JobsMixin, ManagedFile):
|
||||
module = self.get_module()
|
||||
except Exception as 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
|
||||
|
||||
scripts = {}
|
||||
|
||||
@@ -4,7 +4,7 @@ from django.dispatch import receiver
|
||||
|
||||
from core.events import *
|
||||
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 netbox.config import get_config
|
||||
from netbox.models.features import has_feature
|
||||
@@ -102,14 +102,12 @@ def process_job_start_event_rules(sender, **kwargs):
|
||||
enabled=True,
|
||||
object_types=sender.object_type
|
||||
)
|
||||
username = sender.user.username if sender.user else None
|
||||
process_event_rules(
|
||||
event_rules=event_rules,
|
||||
object_type=sender.object_type,
|
||||
event = EventContext(
|
||||
event_type=JOB_STARTED,
|
||||
data=sender.data,
|
||||
username=username
|
||||
user=sender.user,
|
||||
)
|
||||
process_event_rules(event_rules, sender.object_type, event)
|
||||
|
||||
|
||||
@receiver(job_end)
|
||||
@@ -122,14 +120,12 @@ def process_job_end_event_rules(sender, **kwargs):
|
||||
enabled=True,
|
||||
object_types=sender.object_type
|
||||
)
|
||||
username = sender.user.username if sender.user else None
|
||||
process_event_rules(
|
||||
event_rules=event_rules,
|
||||
object_type=sender.object_type,
|
||||
event = EventContext(
|
||||
event_type=JOB_COMPLETED,
|
||||
data=sender.data,
|
||||
username=username
|
||||
user=sender.user,
|
||||
)
|
||||
process_event_rules(event_rules, sender.object_type, event)
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -45,9 +45,10 @@ IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice([
|
||||
class VRFFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = VRF
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('import_target_id', 'export_target_id', name=_('Route Targets')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
import_target_id = DynamicModelMultipleChoiceField(
|
||||
queryset=RouteTarget.objects.all(),
|
||||
@@ -65,9 +66,10 @@ class VRFFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
class RouteTargetFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = RouteTarget
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('importing_vrf_id', 'exporting_vrf_id', name=_('VRF')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
importing_vrf_id = DynamicModelMultipleChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
@@ -85,8 +87,9 @@ class RouteTargetFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
class RIRFilterForm(OrganizationalModelFilterSetForm):
|
||||
model = RIR
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('is_private', name=_('RIR')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
is_private = forms.NullBooleanField(
|
||||
required=False,
|
||||
@@ -101,9 +104,10 @@ class RIRFilterForm(OrganizationalModelFilterSetForm):
|
||||
class AggregateFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = Aggregate
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('family', 'rir_id', name=_('Attributes')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
)
|
||||
family = forms.ChoiceField(
|
||||
@@ -122,9 +126,10 @@ class AggregateFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryMode
|
||||
class ASNRangeFilterForm(TenancyFilterForm, OrganizationalModelFilterSetForm):
|
||||
model = ASNRange
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('rir_id', 'start', 'end', name=_('Range')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
rir_id = DynamicModelMultipleChoiceField(
|
||||
queryset=RIR.objects.all(),
|
||||
@@ -145,9 +150,10 @@ class ASNRangeFilterForm(TenancyFilterForm, OrganizationalModelFilterSetForm):
|
||||
class ASNFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = ASN
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('rir_id', 'site_group_id', 'site_id', name=_('Assignment')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
rir_id = DynamicModelMultipleChoiceField(
|
||||
queryset=RIR.objects.all(),
|
||||
@@ -170,7 +176,8 @@ class ASNFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
class RoleFilterForm(OrganizationalModelFilterSetForm):
|
||||
model = Role
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -178,7 +185,7 @@ class RoleFilterForm(OrganizationalModelFilterSetForm):
|
||||
class PrefixFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = Prefix
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet(
|
||||
'within_include', 'family', 'status', 'role_id', 'mask_length', 'is_pool', 'mark_utilized',
|
||||
name=_('Addressing')
|
||||
@@ -187,6 +194,7 @@ class PrefixFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFi
|
||||
FieldSet('vrf_id', 'present_in_vrf_id', name=_('VRF')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Scope')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
)
|
||||
mask_length__lte = forms.IntegerField(
|
||||
@@ -284,9 +292,10 @@ class PrefixFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFi
|
||||
class IPRangeFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = IPRange
|
||||
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('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
)
|
||||
family = forms.ChoiceField(
|
||||
@@ -331,14 +340,15 @@ class IPRangeFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelF
|
||||
class IPAddressFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = IPAddress
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet(
|
||||
'parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface', 'dns_name',
|
||||
name=_('Attributes')
|
||||
),
|
||||
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('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'region_id', 'group_id', 'parent', 'status', 'role')
|
||||
@@ -409,9 +419,10 @@ class IPAddressFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryMode
|
||||
class FHRPGroupFilterForm(PrimaryModelFilterSetForm):
|
||||
model = FHRPGroup
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', 'protocol', 'group_id', name=_('Attributes')),
|
||||
FieldSet('auth_type', 'auth_key', name=_('Authentication')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
name = forms.CharField(
|
||||
label=_('Name'),
|
||||
@@ -441,11 +452,12 @@ class FHRPGroupFilterForm(PrimaryModelFilterSetForm):
|
||||
|
||||
class VLANGroupFilterForm(TenancyFilterForm, OrganizationalModelFilterSetForm):
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('region', 'site_group', 'site', 'location', 'rack', name=_('Location')),
|
||||
FieldSet('cluster_group', 'cluster', name=_('Cluster')),
|
||||
FieldSet('contains_vid', name=_('VLANs')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
model = VLANGroup
|
||||
region = DynamicModelMultipleChoiceField(
|
||||
@@ -495,8 +507,9 @@ class VLANGroupFilterForm(TenancyFilterForm, OrganizationalModelFilterSetForm):
|
||||
class VLANTranslationPolicyFilterForm(PrimaryModelFilterSetForm):
|
||||
model = VLANTranslationPolicy
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', name=_('Attributes')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
name = forms.CharField(
|
||||
required=False,
|
||||
@@ -532,11 +545,12 @@ class VLANTranslationRuleFilterForm(NetBoxModelFilterSetForm):
|
||||
class VLANFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = VLAN
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
|
||||
FieldSet('group_id', 'status', 'role_id', 'vid', 'l2vpn_id', name=_('Attributes')),
|
||||
FieldSet('qinq_role', 'qinq_svlan_id', name=_('Q-in-Q/802.1ad')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'group_id')
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
@@ -604,8 +618,9 @@ class VLANFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
class ServiceTemplateFilterForm(PrimaryModelFilterSetForm):
|
||||
model = ServiceTemplate
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('protocol', 'port', name=_('Attributes')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
protocol = forms.ChoiceField(
|
||||
label=_('Protocol'),
|
||||
@@ -622,9 +637,10 @@ class ServiceTemplateFilterForm(PrimaryModelFilterSetForm):
|
||||
class ServiceFilterForm(ContactModelFilterForm, ServiceTemplateFilterForm):
|
||||
model = Service
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('protocol', 'port', name=_('Attributes')),
|
||||
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')),
|
||||
)
|
||||
device_id = DynamicModelMultipleChoiceField(
|
||||
|
||||
@@ -370,6 +370,11 @@ class AnnotatedIPAddressTable(IPAddressTable):
|
||||
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):
|
||||
pass
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from dcim.models import Interface
|
||||
from dcim.tables.template_code import INTERFACE_LINKTERMINATION, LINKTERMINATION
|
||||
from ipam.models import *
|
||||
from netbox.tables import NetBoxTable, OrganizationalModelTable, PrimaryModelTable, columns
|
||||
from tenancy.tables import TenancyColumnsMixin, TenantColumn
|
||||
@@ -159,11 +160,26 @@ class VLANDevicesTable(VLANMembersTable):
|
||||
actions = columns.ActionsColumn(
|
||||
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):
|
||||
model = Interface
|
||||
fields = ('device', 'name', 'tagged', 'actions')
|
||||
exclude = ('id', )
|
||||
fields = ('device', 'name', 'link_peer', 'connection', 'tagged', 'actions')
|
||||
default_columns = ('device', 'name', 'connection', 'tagged', 'actions')
|
||||
exclude = ('id',)
|
||||
|
||||
|
||||
class VLANVirtualMachinesTable(VLANMembersTable):
|
||||
|
||||
41
netbox/ipam/tests/test_tables.py
Normal file
41
netbox/ipam/tests/test_tables.py
Normal file
@@ -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:
|
||||
|
||||
# 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 = [Prefix(prefix=p, status=None) for p in available_prefixes.iter_cidrs()]
|
||||
child_prefixes = child_prefixes + available_prefixes
|
||||
|
||||
@@ -3,8 +3,10 @@ from contextvars import ContextVar
|
||||
__all__ = (
|
||||
'current_request',
|
||||
'events_queue',
|
||||
'query_cache',
|
||||
)
|
||||
|
||||
|
||||
current_request = ContextVar('current_request', default=None)
|
||||
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 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 extras.events import flush_events
|
||||
|
||||
@@ -16,6 +17,7 @@ def event_tracking(request):
|
||||
"""
|
||||
current_request.set(request)
|
||||
events_queue.set({})
|
||||
query_cache.set(defaultdict(dict))
|
||||
|
||||
yield
|
||||
|
||||
@@ -26,3 +28,4 @@ def event_tracking(request):
|
||||
# Clear context vars
|
||||
current_request.set(None)
|
||||
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 extras.choices import *
|
||||
from users.models import Owner
|
||||
from utilities.forms.fields import DynamicModelChoiceField, QueryField
|
||||
from utilities.forms.fields import QueryField
|
||||
from utilities.forms.mixins import FilterModifierMixin
|
||||
from .mixins import CustomFieldsMixin, SavedFiltersMixin
|
||||
from .mixins import CustomFieldsMixin, OwnerFilterMixin, SavedFiltersMixin
|
||||
|
||||
__all__ = (
|
||||
'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):
|
||||
"""
|
||||
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 extras.choices import *
|
||||
from extras.models import *
|
||||
from users.models import Owner
|
||||
from users.models import OwnerGroup, Owner
|
||||
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
||||
|
||||
__all__ = (
|
||||
'ChangelogMessageMixin',
|
||||
'CustomFieldsMixin',
|
||||
'OwnerMixin',
|
||||
'OwnerFilterMixin',
|
||||
'SavedFiltersMixin',
|
||||
'TagsMixin',
|
||||
)
|
||||
@@ -22,7 +23,7 @@ class ChangelogMessageMixin(forms.Form):
|
||||
"""
|
||||
changelog_message = forms.CharField(
|
||||
required=False,
|
||||
max_length=200
|
||||
max_length=200,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -42,6 +43,7 @@ class CustomFieldsMixin:
|
||||
Attributes:
|
||||
model: The model class
|
||||
"""
|
||||
|
||||
model = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -86,13 +88,20 @@ class CustomFieldsMixin:
|
||||
|
||||
|
||||
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(
|
||||
queryset=SavedFilter.objects.all(),
|
||||
required=False,
|
||||
label=_('Saved Filter'),
|
||||
query_params={
|
||||
'usable': True,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -107,6 +116,13 @@ class SavedFiltersMixin(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(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False,
|
||||
@@ -124,10 +140,47 @@ class TagsMixin(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(
|
||||
queryset=Owner.objects.all(),
|
||||
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'),
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from functools import cache
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.registry import registry
|
||||
@@ -409,60 +411,10 @@ ADMIN_MENU = Menu(
|
||||
MenuGroup(
|
||||
label=_('Authentication'),
|
||||
items=(
|
||||
MenuItem(
|
||||
link='users:user_list',
|
||||
link_text=_('Users'),
|
||||
staff_only=True,
|
||||
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'])
|
||||
),
|
||||
get_model_item('users', 'user', _('Users')),
|
||||
get_model_item('users', 'group', _('Groups')),
|
||||
get_model_item('users', 'token', _('API Tokens')),
|
||||
get_model_item('users', 'objectpermission', _('Permissions'), actions=['add']),
|
||||
),
|
||||
),
|
||||
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
|
||||
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()
|
||||
@cache
|
||||
def get_menus():
|
||||
"""
|
||||
Dynamically build and return the list of navigation menus.
|
||||
This ensures plugin menus registered during app initialization are included.
|
||||
The result is cached since menus don't change without a Django restart.
|
||||
"""
|
||||
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,
|
||||
]
|
||||
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)
|
||||
# Add top-level plugin menus
|
||||
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):
|
||||
owner_group = tables.Column(
|
||||
accessor='owner__group',
|
||||
linkify=True,
|
||||
verbose_name=_('Owner Group'),
|
||||
)
|
||||
owner = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name=_('Owner')
|
||||
verbose_name=_('Owner'),
|
||||
)
|
||||
comments = columns.MarkdownColumn(
|
||||
verbose_name=_('Comments'),
|
||||
@@ -281,9 +286,14 @@ class PrimaryModelTable(NetBoxTable):
|
||||
|
||||
|
||||
class OrganizationalModelTable(NetBoxTable):
|
||||
owner_group = tables.Column(
|
||||
accessor='owner__group',
|
||||
linkify=True,
|
||||
verbose_name=_('Owner Group'),
|
||||
)
|
||||
owner = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name=_('Owner')
|
||||
verbose_name=_('Owner'),
|
||||
)
|
||||
comments = columns.MarkdownColumn(
|
||||
verbose_name=_('Comments'),
|
||||
@@ -291,9 +301,14 @@ class OrganizationalModelTable(NetBoxTable):
|
||||
|
||||
|
||||
class NestedGroupModelTable(NetBoxTable):
|
||||
owner_group = tables.Column(
|
||||
accessor='owner__group',
|
||||
linkify=True,
|
||||
verbose_name=_('Owner Group'),
|
||||
)
|
||||
owner = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name=_('Owner')
|
||||
verbose_name=_('Owner'),
|
||||
)
|
||||
name = columns.MPTTColumn(
|
||||
verbose_name=_('Name'),
|
||||
|
||||
8
netbox/project-static/dist/netbox.js
vendored
8
netbox/project-static/dist/netbox.js
vendored
File diff suppressed because one or more lines are too long
8
netbox/project-static/dist/netbox.js.map
vendored
8
netbox/project-static/dist/netbox.js.map
vendored
File diff suppressed because one or more lines are too long
40
netbox/project-static/src/forms/clearField.ts
Normal file
40
netbox/project-static/src/forms/clearField.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import TomSelect from 'tom-select';
|
||||
import { getElements } from '../util';
|
||||
|
||||
/**
|
||||
* Initialize clear-field dependencies.
|
||||
* When a required field is cleared, dependent fields with data-requires-fields attribute will also be cleared.
|
||||
*/
|
||||
export function initClearField(): void {
|
||||
// Find all fields with data-requires-fields attribute
|
||||
for (const field of getElements<HTMLSelectElement>('[data-requires-fields]')) {
|
||||
const requiredFieldsAttr = field.getAttribute('data-requires-fields');
|
||||
if (!requiredFieldsAttr) continue;
|
||||
|
||||
// Parse the comma-separated list of required field names
|
||||
const requiredFields = requiredFieldsAttr.split(',').map(name => name.trim());
|
||||
|
||||
// Set up listeners for each required field
|
||||
for (const requiredFieldName of requiredFields) {
|
||||
const requiredField = document.querySelector<HTMLSelectElement>(
|
||||
`[name="${requiredFieldName}"]`,
|
||||
);
|
||||
if (!requiredField) continue;
|
||||
|
||||
// Listen for changes on the required field
|
||||
requiredField.addEventListener('change', () => {
|
||||
// If required field is cleared, also clear this dependent field
|
||||
if (!requiredField.value || requiredField.value === '') {
|
||||
// Check if this field uses TomSelect
|
||||
const tomselect = (field as HTMLSelectElement & { tomselect?: TomSelect }).tomselect;
|
||||
if (tomselect) {
|
||||
tomselect.clear();
|
||||
} else {
|
||||
// Regular select field
|
||||
field.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { initClearField } from './clearField';
|
||||
import { initFormElements } from './elements';
|
||||
import { initFilterModifiers } from './filterModifiers';
|
||||
import { initSpeedSelector } from './speedSelector';
|
||||
|
||||
export function initForms(): void {
|
||||
for (const func of [initFormElements, initSpeedSelector, initFilterModifiers]) {
|
||||
for (const func of [initFormElements, initSpeedSelector, initFilterModifiers, initClearField]) {
|
||||
func();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,8 +101,9 @@
|
||||
|
||||
<div class="field-group mb-5">
|
||||
<div class="row">
|
||||
<h2 class="col-9 offset-3">{% trans "Owner" %}</h2>
|
||||
<h2 class="col-9 offset-3">{% trans "Ownership" %}</h2>
|
||||
</div>
|
||||
{% render_field form.owner_group %}
|
||||
{% render_field form.owner %}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -80,8 +80,9 @@
|
||||
|
||||
<div class="field-group mb-5">
|
||||
<div class="row">
|
||||
<h2 class="col-9 offset-3">{% trans "Owner" %}</h2>
|
||||
<h2 class="col-9 offset-3">{% trans "Ownership" %}</h2>
|
||||
</div>
|
||||
{% render_field form.owner_group %}
|
||||
{% render_field form.owner %}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -370,33 +370,6 @@
|
||||
</table>
|
||||
</div>
|
||||
{% 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 'dcim/inc/panels/inventory_items.html' %}
|
||||
{% plugin_right_page object %}
|
||||
@@ -441,6 +414,13 @@
|
||||
{% include 'inc/panel_table.html' with table=vlan_table heading="VLANs" %}
|
||||
</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 %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
|
||||
@@ -36,8 +36,9 @@
|
||||
|
||||
<div class="field-group mb-5">
|
||||
<div class="row">
|
||||
<h2 class="col-9 offset-3">{% trans "Owner" %}</h2>
|
||||
<h2 class="col-9 offset-3">{% trans "Ownership" %}</h2>
|
||||
</div>
|
||||
{% render_field vc_form.owner_group %}
|
||||
{% render_field vc_form.owner %}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -121,6 +121,7 @@
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<i class="mdi mdi-alert"></i>
|
||||
{% blocktrans with module=module.name %}Could not load scripts from module {{ module }}{% endblocktrans %}
|
||||
{% if module.error %}<code>{{ module.error }}</code>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -62,8 +62,11 @@ Context:
|
||||
{% if form.owner %}
|
||||
<div class="field-group mb-5">
|
||||
<div class="row">
|
||||
<h2 class="col-9 offset-3">{% trans "Owner" %}</h2>
|
||||
<h2 class="col-9 offset-3">{% trans "Ownership" %}</h2>
|
||||
</div>
|
||||
{% if form.owner_group %}
|
||||
{% render_field form.owner_group %}
|
||||
{% endif %}
|
||||
{% render_field form.owner bulk_nullable=True %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -27,6 +27,9 @@
|
||||
<div class="row">
|
||||
<h2 class="col-9 offset-3">{% trans "Ownership" %}</h2>
|
||||
</div>
|
||||
{% if form.owner_group %}
|
||||
{% render_field form.owner_group %}
|
||||
{% endif %}
|
||||
{% render_field form.owner %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -67,8 +67,9 @@
|
||||
|
||||
<div class="field-group mb-5">
|
||||
<div class="row">
|
||||
<h2 class="col-9 offset-3">{% trans "Owner" %}</h2>
|
||||
<h2 class="col-9 offset-3">{% trans "Ownership" %}</h2>
|
||||
</div>
|
||||
{% render_field form.owner_group %}
|
||||
{% render_field form.owner %}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -31,8 +31,9 @@ __all__ = (
|
||||
class TenantGroupFilterForm(NestedGroupModelFilterSetForm):
|
||||
model = TenantGroup
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('parent_id', name=_('Tenant Group')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
parent_id = DynamicModelMultipleChoiceField(
|
||||
queryset=TenantGroup.objects.all(),
|
||||
@@ -45,8 +46,9 @@ class TenantGroupFilterForm(NestedGroupModelFilterSetForm):
|
||||
class TenantFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||
model = Tenant
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('group_id', name=_('Tenant')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts'))
|
||||
)
|
||||
group_id = DynamicModelMultipleChoiceField(
|
||||
@@ -65,8 +67,9 @@ class TenantFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||
class ContactGroupFilterForm(NestedGroupModelFilterSetForm):
|
||||
model = ContactGroup
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('parent_id', name=_('Contact Group')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
parent_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ContactGroup.objects.all(),
|
||||
@@ -79,7 +82,8 @@ class ContactGroupFilterForm(NestedGroupModelFilterSetForm):
|
||||
class ContactRoleFilterForm(OrganizationalModelFilterSetForm):
|
||||
model = ContactRole
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -87,8 +91,9 @@ class ContactRoleFilterForm(OrganizationalModelFilterSetForm):
|
||||
class ContactFilterForm(PrimaryModelFilterSetForm):
|
||||
model = Contact
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('group_id', name=_('Contact')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ContactGroup.objects.all(),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
import django_filters
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from users.models import Owner
|
||||
from users.models import OwnerGroup, Owner
|
||||
|
||||
__all__ = (
|
||||
'OwnerFilterMixin',
|
||||
@@ -12,6 +12,17 @@ class OwnerFilterMixin(django_filters.FilterSet):
|
||||
"""
|
||||
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(
|
||||
queryset=Owner.objects.all(),
|
||||
label=_('Owner (ID)'),
|
||||
|
||||
@@ -5,6 +5,7 @@ from ..utils import add_blank_choice
|
||||
|
||||
__all__ = (
|
||||
'BulkEditNullBooleanSelect',
|
||||
'ClearableSelect',
|
||||
'ColorSelect',
|
||||
'HTMXSelect',
|
||||
'SelectWithPK',
|
||||
@@ -28,6 +29,21 @@ class BulkEditNullBooleanSelect(forms.NullBooleanSelect):
|
||||
)
|
||||
|
||||
|
||||
class ClearableSelect(forms.Select):
|
||||
"""
|
||||
A Select widget that will be automatically cleared when one or more required fields are cleared.
|
||||
|
||||
Args:
|
||||
requires_fields: A list of field names that this field depends on. When any of these fields
|
||||
are cleared, this field will also be cleared automatically via JavaScript.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, requires_fields=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if requires_fields:
|
||||
self.attrs['data-requires-fields'] = ','.join(requires_fields)
|
||||
|
||||
|
||||
class ColorSelect(forms.Select):
|
||||
"""
|
||||
Extends the built-in Select widget to colorize each <option>.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from django import template
|
||||
|
||||
from netbox.navigation.menu import MENUS
|
||||
from netbox.navigation.menu import get_menus
|
||||
|
||||
__all__ = (
|
||||
'nav',
|
||||
@@ -19,7 +19,7 @@ def nav(context):
|
||||
nav_items = []
|
||||
|
||||
# Construct the navigation menu based upon the current user's permissions
|
||||
for menu in MENUS:
|
||||
for menu in get_menus():
|
||||
groups = []
|
||||
for group in menu.groups:
|
||||
items = []
|
||||
|
||||
@@ -7,10 +7,10 @@ from extras.forms import LocalConfigContextFilterForm
|
||||
from extras.models import ConfigTemplate
|
||||
from ipam.models import VRF, VLANTranslationPolicy
|
||||
from netbox.forms import NetBoxModelFilterSetForm, OrganizationalModelFilterSetForm, PrimaryModelFilterSetForm
|
||||
from netbox.forms.mixins import OwnerFilterMixin
|
||||
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
|
||||
from users.models import Owner
|
||||
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 virtualization.choices import *
|
||||
from virtualization.models import *
|
||||
@@ -29,7 +29,8 @@ __all__ = (
|
||||
class ClusterTypeFilterForm(OrganizationalModelFilterSetForm):
|
||||
model = ClusterType
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -38,7 +39,8 @@ class ClusterGroupFilterForm(ContactModelFilterForm, OrganizationalModelFilterSe
|
||||
model = ClusterGroup
|
||||
tag = TagFilterField(model)
|
||||
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')),
|
||||
)
|
||||
|
||||
@@ -46,10 +48,11 @@ class ClusterGroupFilterForm(ContactModelFilterForm, OrganizationalModelFilterSe
|
||||
class ClusterFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||
model = Cluster
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('group_id', 'type_id', 'status', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Scope')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'group_id')
|
||||
@@ -105,7 +108,7 @@ class VirtualMachineFilterForm(
|
||||
):
|
||||
model = VirtualMachine
|
||||
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('region_id', 'site_group_id', 'site_id', name=_('Location')),
|
||||
FieldSet(
|
||||
@@ -113,6 +116,7 @@ class VirtualMachineFilterForm(
|
||||
'local_context_data', 'serial', name=_('Attributes')
|
||||
),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
)
|
||||
cluster_group_id = DynamicModelMultipleChoiceField(
|
||||
@@ -205,14 +209,15 @@ class VirtualMachineFilterForm(
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class VMInterfaceFilterForm(NetBoxModelFilterSetForm):
|
||||
class VMInterfaceFilterForm(OwnerFilterMixin, NetBoxModelFilterSetForm):
|
||||
model = VMInterface
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('cluster_id', 'virtual_machine_id', name=_('Virtual Machine')),
|
||||
FieldSet('enabled', name=_('Attributes')),
|
||||
FieldSet('vrf_id', 'l2vpn_id', 'mac_address', name=_('Addressing')),
|
||||
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')
|
||||
cluster_id = DynamicModelMultipleChoiceField(
|
||||
@@ -259,20 +264,16 @@ class VMInterfaceFilterForm(NetBoxModelFilterSetForm):
|
||||
required=False,
|
||||
label=_('VLAN Translation Policy')
|
||||
)
|
||||
owner_id = DynamicModelChoiceField(
|
||||
queryset=Owner.objects.all(),
|
||||
required=False,
|
||||
label=_('Owner'),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class VirtualDiskFilterForm(NetBoxModelFilterSetForm):
|
||||
class VirtualDiskFilterForm(OwnerFilterMixin, NetBoxModelFilterSetForm):
|
||||
model = VirtualDisk
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('virtual_machine_id', name=_('Virtual Machine')),
|
||||
FieldSet('size', name=_('Attributes')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
virtual_machine_id = DynamicModelMultipleChoiceField(
|
||||
queryset=VirtualMachine.objects.all(),
|
||||
@@ -284,9 +285,4 @@ class VirtualDiskFilterForm(NetBoxModelFilterSetForm):
|
||||
required=False,
|
||||
min_value=1
|
||||
)
|
||||
owner_id = DynamicModelChoiceField(
|
||||
queryset=Owner.objects.all(),
|
||||
required=False,
|
||||
label=_('Owner'),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -33,7 +33,8 @@ __all__ = (
|
||||
class TunnelGroupFilterForm(ContactModelFilterForm, OrganizationalModelFilterSetForm):
|
||||
model = TunnelGroup
|
||||
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')),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
@@ -42,10 +43,11 @@ class TunnelGroupFilterForm(ContactModelFilterForm, OrganizationalModelFilterSet
|
||||
class TunnelFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = Tunnel
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('status', 'encapsulation', 'tunnel_id', name=_('Tunnel')),
|
||||
FieldSet('ipsec_profile_id', name=_('Security')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenancy')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
@@ -97,10 +99,11 @@ class TunnelTerminationFilterForm(NetBoxModelFilterSetForm):
|
||||
class IKEProposalFilterForm(PrimaryModelFilterSetForm):
|
||||
model = IKEProposal
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet(
|
||||
'authentication_method', 'encryption_algorithm', 'authentication_algorithm', 'group', name=_('Parameters')
|
||||
),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
authentication_method = forms.MultipleChoiceField(
|
||||
label=_('Authentication method'),
|
||||
@@ -128,8 +131,9 @@ class IKEProposalFilterForm(PrimaryModelFilterSetForm):
|
||||
class IKEPolicyFilterForm(PrimaryModelFilterSetForm):
|
||||
model = IKEPolicy
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('version', 'mode', 'proposal_id', name=_('Parameters')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
version = forms.MultipleChoiceField(
|
||||
label=_('IKE version'),
|
||||
@@ -152,8 +156,9 @@ class IKEPolicyFilterForm(PrimaryModelFilterSetForm):
|
||||
class IPSecProposalFilterForm(PrimaryModelFilterSetForm):
|
||||
model = IPSecProposal
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('encryption_algorithm', 'authentication_algorithm', name=_('Parameters')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
encryption_algorithm = forms.MultipleChoiceField(
|
||||
label=_('Encryption algorithm'),
|
||||
@@ -171,8 +176,9 @@ class IPSecProposalFilterForm(PrimaryModelFilterSetForm):
|
||||
class IPSecPolicyFilterForm(PrimaryModelFilterSetForm):
|
||||
model = IPSecPolicy
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('proposal_id', 'pfs_group', name=_('Parameters')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
proposal_id = DynamicModelMultipleChoiceField(
|
||||
queryset=IKEProposal.objects.all(),
|
||||
@@ -190,8 +196,9 @@ class IPSecPolicyFilterForm(PrimaryModelFilterSetForm):
|
||||
class IPSecProfileFilterForm(PrimaryModelFilterSetForm):
|
||||
model = IPSecProfile
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('mode', 'ike_policy_id', 'ipsec_policy_id', name=_('Profile')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
mode = forms.MultipleChoiceField(
|
||||
label=_('Mode'),
|
||||
@@ -214,9 +221,10 @@ class IPSecProfileFilterForm(PrimaryModelFilterSetForm):
|
||||
class L2VPNFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = L2VPN
|
||||
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('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
|
||||
@@ -22,8 +22,9 @@ __all__ = (
|
||||
class WirelessLANGroupFilterForm(NestedGroupModelFilterSetForm):
|
||||
model = WirelessLANGroup
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('parent_id', name=_('Wireless LAN group')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
parent_id = DynamicModelMultipleChoiceField(
|
||||
queryset=WirelessLANGroup.objects.all(),
|
||||
@@ -36,11 +37,12 @@ class WirelessLANGroupFilterForm(NestedGroupModelFilterSetForm):
|
||||
class WirelessLANFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = WirelessLAN
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('ssid', 'group_id', 'status', name=_('Attributes')),
|
||||
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('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
ssid = forms.CharField(
|
||||
required=False,
|
||||
@@ -102,10 +104,11 @@ class WirelessLANFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
class WirelessLinkFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = WirelessLink
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
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('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
ssid = forms.CharField(
|
||||
required=False,
|
||||
|
||||
Reference in New Issue
Block a user