mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-30 13:48:16 +01:00
Compare commits
1 Commits
main
...
20902-git-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7eedefb2df |
43
.github/ISSUE_TEMPLATE/03-performance.yaml
vendored
43
.github/ISSUE_TEMPLATE/03-performance.yaml
vendored
@@ -1,43 +0,0 @@
|
||||
---
|
||||
name: 🏁 Performance
|
||||
type: Performance
|
||||
description: An opportunity to improve application performance
|
||||
labels: ["netbox", "type: performance", "status: needs triage"]
|
||||
body:
|
||||
- type: input
|
||||
attributes:
|
||||
label: NetBox Version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.5.1
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Python Version
|
||||
description: What version of Python are you currently running?
|
||||
options:
|
||||
- "3.12"
|
||||
- "3.13"
|
||||
- "3.14"
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Area(s) of Concern
|
||||
description: Which application interface(s) are affected?
|
||||
options:
|
||||
- label: User Interface
|
||||
- label: REST API
|
||||
- label: GraphQL API
|
||||
- label: Python ORM
|
||||
- label: Other
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Details
|
||||
description: >
|
||||
Describe in detail the operations being performed and the indications of a performance issue.
|
||||
Include any relevant testing parameters, benchmarks, and expected results.
|
||||
validations:
|
||||
required: true
|
||||
@@ -3,43 +3,31 @@
|
||||
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:
|
||||
|
||||
```
|
||||
cd /opt/netbox
|
||||
source /opt/netbox/venv/bin/activate
|
||||
python3 netbox/manage.py nbshell
|
||||
./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 preloaded. (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 pre-loaded. (If desired, the stock Django shell is also available by executing `./manage.py shell`.)
|
||||
|
||||
```
|
||||
(venv) $ python3 netbox/manage.py nbshell
|
||||
$ ./manage.py nbshell
|
||||
### NetBox interactive shell (localhost)
|
||||
### 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.
|
||||
### Python 3.7.10 | Django 3.2.5 | NetBox 3.0
|
||||
### 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:
|
||||
dcim.Cable
|
||||
dcim.CableTermination
|
||||
dcim.ConsolePort
|
||||
dcim.ConsolePortTemplate
|
||||
dcim.ConsoleServerPort
|
||||
dcim.ConsoleServerPortTemplate
|
||||
dcim.Device
|
||||
ConsolePort
|
||||
ConsolePortTemplate
|
||||
ConsoleServerPort
|
||||
ConsoleServerPortTemplate
|
||||
Device
|
||||
...
|
||||
```
|
||||
|
||||
To exit the NetBox shell, type `exit()` or press `Ctrl+D`.
|
||||
|
||||
```
|
||||
>>> exit()
|
||||
(venv) $
|
||||
```
|
||||
|
||||
!!! warning
|
||||
The NetBox shell affords direct access to NetBox data and function with very little validation in place. As such, it is crucial to ensure that only authorized, knowledgeable users are ever granted access to it. Never perform any action in the management shell without having a full backup in place.
|
||||
|
||||
@@ -126,7 +114,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 latter of which is case-insensitive).
|
||||
Character fields can be filtered against partial matches using the `contains` or `icontains` field lookup (the later 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 > 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:
|
||||
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:
|
||||
|
||||
* [`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:
|
||||
psql (16.11 (Ubuntu 16.11-0ubuntu0.24.04.1))
|
||||
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
|
||||
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)
|
||||
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, compression: off)
|
||||
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, 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 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.
|
||||
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.
|
||||
|
||||
### Option B: Clone the Git Repository
|
||||
|
||||
@@ -63,12 +63,12 @@ This command should generate output similar to the following:
|
||||
|
||||
```
|
||||
Cloning into '.'...
|
||||
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.
|
||||
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.
|
||||
```
|
||||
|
||||
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,8 +102,7 @@ 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`
|
||||
* `API_TOKEN_PEPPERS`
|
||||
* `DATABASES`
|
||||
* `DATABASES` (or `DATABASE`)
|
||||
* `REDIS`
|
||||
* `SECRET_KEY`
|
||||
|
||||
@@ -159,7 +158,7 @@ DATABASES = {
|
||||
|
||||
### REDIS
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -253,7 +252,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
|
||||
@@ -296,12 +295,13 @@ 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).
|
||||
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/
|
||||
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/
|
||||
Quit the server with CONTROL-C.
|
||||
```
|
||||
|
||||
|
||||
@@ -43,22 +43,16 @@ You should see output similar to the following:
|
||||
|
||||
```no-highlight
|
||||
● netbox.service - NetBox WSGI Service
|
||||
Loaded: loaded (/etc/systemd/system/netbox.service; enabled; preset: enabled)
|
||||
Active: active (running) since Mon 2026-01-26 11:00:00 CST; 7s ago
|
||||
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
|
||||
Docs: https://docs.netbox.dev/
|
||||
Main PID: 7283 (gunicorn)
|
||||
Tasks: 6 (limit: 4545)
|
||||
Memory: 556.1M (peak: 556.3M)
|
||||
CPU: 3.387s
|
||||
Main PID: 1140492 (gunicorn)
|
||||
Tasks: 19 (limit: 4683)
|
||||
Memory: 666.2M
|
||||
CGroup: /system.slice/netbox.service
|
||||
├─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.
|
||||
├─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>
|
||||
...
|
||||
```
|
||||
|
||||
|
||||
@@ -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 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.
|
||||
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.
|
||||
|
||||
## Obtain an SSL Certificate
|
||||
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
|
||||
</div>
|
||||
|
||||
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 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 following sections detail how to set up a new instance of NetBox:
|
||||
|
||||
1. [PostgreSQL database](1-postgresql.md)
|
||||
2. [Redis](2-redis.md)
|
||||
1. [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=4.5.0
|
||||
NEWVER=3.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=4.4.10
|
||||
OLDVER=3.4.9
|
||||
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.5.0
|
||||
sudo git checkout v4.2.7
|
||||
```
|
||||
|
||||
## 4. Run the Upgrade Script
|
||||
@@ -128,7 +128,7 @@ sudo ./upgrade.sh
|
||||
```
|
||||
|
||||
!!! warning
|
||||
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:
|
||||
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:
|
||||
|
||||
```no-highlight
|
||||
sudo PYTHON=/usr/bin/python3.12 ./upgrade.sh
|
||||
|
||||
@@ -215,51 +215,9 @@ http://netbox/api/ipam/ip-addresses/ \
|
||||
|
||||
If we wanted to assign this IP address to a virtual machine interface instead, we would have set `assigned_object_type` to `virtualization.vminterface` and updated the object ID appropriately.
|
||||
|
||||
### Specifying Fields
|
||||
### Brief Format
|
||||
|
||||
A REST API response will include all available fields for the object type by default. If you wish to return only a subset of the available fields, you can append `?fields=` to the URL followed by a comma-separated list of field names. For example, the following request will return only the `id`, `name`, `status`, and `region` fields for each site in the response.
|
||||
|
||||
```
|
||||
GET /api/dcim/sites/?fields=id,name,status,region
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"name": "DM-NYC",
|
||||
"status": {
|
||||
"value": "active",
|
||||
"label": "Active"
|
||||
},
|
||||
"region": {
|
||||
"id": 43,
|
||||
"url": "http://netbox:8000/api/dcim/regions/43/",
|
||||
"display": "New York",
|
||||
"name": "New York",
|
||||
"slug": "us-ny",
|
||||
"description": "",
|
||||
"site_count": 0,
|
||||
"_depth": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Similarly, you can opt to omit only specific fields by passing the `omit` parameter:
|
||||
|
||||
```
|
||||
GET /api/dcim/sites/?omit=circuit_count,device_count,virtualmachine_count
|
||||
```
|
||||
|
||||
!!! note "The `omit` parameter was introduced in NetBox v4.5.2."
|
||||
|
||||
Strategic use of the `fields` and `omit` parameters can drastically improve REST API performance, as the exclusion of fields which reference related objects reduces the number and complexity of underlying database queries needed to generate the response.
|
||||
|
||||
!!! note
|
||||
The `fields` and `omit` parameters should be considered mutually exclusive. If both are passed, `fields` takes precedence.
|
||||
|
||||
#### Brief Format
|
||||
|
||||
Most API endpoints support an optional "brief" format, which returns only a minimal representation of each object in the response. This is useful when you need only a list of available objects without any related data, such as when populating a drop-down list in a form. It's also more convenient than listing out individual fields via the `fields` or `omit` parameters. As an example, the default (complete) format of a prefix looks like this:
|
||||
Most API endpoints support an optional "brief" format, which returns only a minimal representation of each object in the response. This is useful when you need only a list of available objects without any related data, such as when populating a drop-down list in a form. As an example, the default (complete) format of a prefix looks like this:
|
||||
|
||||
```no-highlight
|
||||
GET /api/ipam/prefixes/13980/
|
||||
@@ -312,10 +270,10 @@ GET /api/ipam/prefixes/13980/
|
||||
}
|
||||
```
|
||||
|
||||
The brief format includes only a few fields:
|
||||
The brief format is much more terse:
|
||||
|
||||
```no-highlight
|
||||
GET /api/ipam/prefixes/13980/?brief=true
|
||||
GET /api/ipam/prefixes/13980/?brief=1
|
||||
```
|
||||
|
||||
```json
|
||||
|
||||
@@ -34,10 +34,9 @@ __all__ = (
|
||||
class ProviderFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||
model = Provider
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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(
|
||||
@@ -70,9 +69,8 @@ class ProviderFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||
class ProviderAccountFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||
model = ProviderAccount
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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(
|
||||
@@ -90,9 +88,8 @@ class ProviderAccountFilterForm(ContactModelFilterForm, PrimaryModelFilterSetFor
|
||||
class ProviderNetworkFilterForm(PrimaryModelFilterSetForm):
|
||||
model = ProviderNetwork
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('provider_id', 'service_id', name=_('Attributes')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
provider_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
@@ -110,9 +107,8 @@ class ProviderNetworkFilterForm(PrimaryModelFilterSetForm):
|
||||
class CircuitTypeFilterForm(OrganizationalModelFilterSetForm):
|
||||
model = CircuitType
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('color', name=_('Attributes')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -125,7 +121,7 @@ class CircuitTypeFilterForm(OrganizationalModelFilterSetForm):
|
||||
class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||
model = Circuit
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
|
||||
FieldSet(
|
||||
'type_id', 'status', 'install_date', 'termination_date', 'commit_rate', 'distance', 'distance_unit',
|
||||
@@ -133,7 +129,6 @@ 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')
|
||||
@@ -279,9 +274,8 @@ class CircuitTerminationFilterForm(NetBoxModelFilterSetForm):
|
||||
class CircuitGroupFilterForm(TenancyFilterForm, OrganizationalModelFilterSetForm):
|
||||
model = CircuitGroup
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -318,9 +312,8 @@ class CircuitGroupAssignmentFilterForm(NetBoxModelFilterSetForm):
|
||||
class VirtualCircuitTypeFilterForm(OrganizationalModelFilterSetForm):
|
||||
model = VirtualCircuitType
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('color', name=_('Attributes')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -333,11 +326,10 @@ class VirtualCircuitTypeFilterForm(OrganizationalModelFilterSetForm):
|
||||
class VirtualCircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||
model = VirtualCircuit
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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(
|
||||
|
||||
@@ -31,8 +31,7 @@ class JobSerializer(BaseModelSerializer):
|
||||
model = Job
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'object_type', 'object_id', 'object', 'name', 'status', 'created',
|
||||
'scheduled', 'interval', 'started', 'completed', 'user', 'data', 'error', 'job_id', 'queue_name',
|
||||
'log_entries',
|
||||
'scheduled', 'interval', 'started', 'completed', 'user', 'data', 'error', 'job_id', 'log_entries',
|
||||
]
|
||||
brief_fields = ('url', 'created', 'completed', 'user', 'status')
|
||||
|
||||
|
||||
@@ -102,7 +102,10 @@ class GitBackend(DataBackend):
|
||||
clone_args['pool_manager'] = ProxyPoolManager(self.socks_proxy)
|
||||
|
||||
if self.url_scheme in ('http', 'https'):
|
||||
if self.params.get('username'):
|
||||
# Only pass explicit credentials if URL doesn't already contain embedded username
|
||||
# to avoid credential conflicts
|
||||
parsed_url = urlparse(self.url)
|
||||
if not parsed_url.username and self.params.get('username'):
|
||||
clone_args.update(
|
||||
{
|
||||
"username": self.params.get('username'),
|
||||
|
||||
@@ -129,14 +129,10 @@ class JobFilterSet(BaseFilterSet):
|
||||
choices=JobStatusChoices,
|
||||
null_value=None
|
||||
)
|
||||
queue_name = django_filters.CharFilter()
|
||||
|
||||
class Meta:
|
||||
model = Job
|
||||
fields = (
|
||||
'id', 'object_type', 'object_type_id', 'object_id', 'name', 'interval', 'status', 'user', 'job_id',
|
||||
'queue_name',
|
||||
)
|
||||
fields = ('id', 'object_type', 'object_type_id', 'object_id', 'name', 'interval', 'status', 'user', 'job_id')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
|
||||
@@ -26,9 +26,8 @@ __all__ = (
|
||||
class DataSourceFilterForm(PrimaryModelFilterSetForm):
|
||||
model = DataSource
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('type', 'status', 'enabled', 'sync_interval', name=_('Data Source')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
label=_('Type'),
|
||||
@@ -72,7 +71,7 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
|
||||
model = Job
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id'),
|
||||
FieldSet('object_type_id', 'status', 'queue_name', name=_('Attributes')),
|
||||
FieldSet('object_type_id', 'status', name=_('Attributes')),
|
||||
FieldSet(
|
||||
'created__before', 'created__after', 'scheduled__before', 'scheduled__after', 'started__before',
|
||||
'started__after', 'completed__before', 'completed__after', 'user', name=_('Creation')
|
||||
@@ -88,10 +87,6 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
|
||||
choices=JobStatusChoices,
|
||||
required=False
|
||||
)
|
||||
queue_name = forms.CharField(
|
||||
label=_('Queue'),
|
||||
required=False
|
||||
)
|
||||
created__after = forms.DateTimeField(
|
||||
label=_('Created after'),
|
||||
required=False,
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.2.9 on 2026-01-27 00:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0020_owner'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='job',
|
||||
name='queue_name',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
]
|
||||
@@ -112,12 +112,6 @@ class Job(models.Model):
|
||||
verbose_name=_('job ID'),
|
||||
unique=True
|
||||
)
|
||||
queue_name = models.CharField(
|
||||
verbose_name=_('queue name'),
|
||||
max_length=100,
|
||||
blank=True,
|
||||
help_text=_('Name of the queue in which this job was enqueued')
|
||||
)
|
||||
log_entries = ArrayField(
|
||||
verbose_name=_('log entries'),
|
||||
base_field=models.JSONField(
|
||||
@@ -185,15 +179,11 @@ class Job(models.Model):
|
||||
return f"{int(minutes)} minutes, {seconds:.2f} seconds"
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
# Use the stored queue name, or fall back to get_queue_for_model for legacy jobs
|
||||
rq_queue_name = self.queue_name or get_queue_for_model(self.object_type.model if self.object_type else None)
|
||||
rq_job_id = str(self.job_id)
|
||||
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
# Cancel the RQ job using the stored queue name
|
||||
rq_queue_name = get_queue_for_model(self.object_type.model if self.object_type else None)
|
||||
queue = django_rq.get_queue(rq_queue_name)
|
||||
job = queue.fetch_job(rq_job_id)
|
||||
job = queue.fetch_job(str(self.job_id))
|
||||
|
||||
if job:
|
||||
try:
|
||||
@@ -298,8 +288,7 @@ class Job(models.Model):
|
||||
scheduled=schedule_at,
|
||||
interval=interval,
|
||||
user=user,
|
||||
job_id=uuid.uuid4(),
|
||||
queue_name=rq_queue_name
|
||||
job_id=uuid.uuid4()
|
||||
)
|
||||
job.full_clean()
|
||||
job.save()
|
||||
|
||||
@@ -9,7 +9,6 @@ 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
|
||||
@@ -71,12 +70,6 @@ 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.
|
||||
@@ -103,10 +96,6 @@ 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):
|
||||
|
||||
@@ -42,9 +42,6 @@ class JobTable(NetBoxTable):
|
||||
completed = columns.DateTimeColumn(
|
||||
verbose_name=_('Completed'),
|
||||
)
|
||||
queue_name = tables.Column(
|
||||
verbose_name=_('Queue'),
|
||||
)
|
||||
log_entries = tables.Column(
|
||||
verbose_name=_('Log Entries'),
|
||||
)
|
||||
@@ -56,7 +53,7 @@ class JobTable(NetBoxTable):
|
||||
model = Job
|
||||
fields = (
|
||||
'pk', 'id', 'object_type', 'object', 'name', 'status', 'created', 'scheduled', 'interval', 'started',
|
||||
'completed', 'user', 'queue_name', 'log_entries', 'error', 'job_id',
|
||||
'completed', 'user', 'error', 'job_id',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'id', 'object_type', 'object', 'name', 'status', 'created', 'started', 'completed', 'user',
|
||||
|
||||
59
netbox/core/tests/test_data_backends.py
Normal file
59
netbox/core/tests/test_data_backends.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from core.data_backends import GitBackend
|
||||
|
||||
|
||||
class GitBackendCredentialTests(TestCase):
|
||||
|
||||
def _get_clone_kwargs(self, url, **params):
|
||||
backend = GitBackend(url=url, **params)
|
||||
|
||||
with patch('dulwich.porcelain.clone') as mock_clone, \
|
||||
patch('dulwich.porcelain.NoneStream'):
|
||||
try:
|
||||
with backend.fetch():
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if mock_clone.called:
|
||||
return mock_clone.call_args.kwargs
|
||||
return {}
|
||||
|
||||
def test_url_with_embedded_username_skips_explicit_credentials(self):
|
||||
kwargs = self._get_clone_kwargs(
|
||||
url='https://myuser@bitbucket.org/workspace/repo.git',
|
||||
username='myuser',
|
||||
password='my-api-key'
|
||||
)
|
||||
|
||||
self.assertEqual(kwargs.get('username'), None)
|
||||
self.assertEqual(kwargs.get('password'), None)
|
||||
|
||||
def test_url_without_embedded_username_passes_explicit_credentials(self):
|
||||
kwargs = self._get_clone_kwargs(
|
||||
url='https://bitbucket.org/workspace/repo.git',
|
||||
username='myuser',
|
||||
password='my-api-key'
|
||||
)
|
||||
|
||||
self.assertEqual(kwargs.get('username'), 'myuser')
|
||||
self.assertEqual(kwargs.get('password'), 'my-api-key')
|
||||
|
||||
def test_url_with_embedded_username_no_explicit_credentials(self):
|
||||
kwargs = self._get_clone_kwargs(
|
||||
url='https://myuser@bitbucket.org/workspace/repo.git'
|
||||
)
|
||||
|
||||
self.assertEqual(kwargs.get('username'), None)
|
||||
self.assertEqual(kwargs.get('password'), None)
|
||||
|
||||
def test_public_repo_no_credentials(self):
|
||||
kwargs = self._get_clone_kwargs(
|
||||
url='https://github.com/public/repo.git'
|
||||
)
|
||||
|
||||
self.assertEqual(kwargs.get('username'), None)
|
||||
self.assertEqual(kwargs.get('password'), None)
|
||||
@@ -1,10 +1,8 @@
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.test import TestCase
|
||||
|
||||
from core.models import DataSource, Job, ObjectType
|
||||
from core.models import DataSource, ObjectType
|
||||
from core.choices import ObjectChangeActionChoices
|
||||
from dcim.models import Site, Location, Device
|
||||
from netbox.constants import CENSOR_TOKEN, CENSOR_TOKEN_CHANGED
|
||||
@@ -202,38 +200,3 @@ class ObjectTypeTest(TestCase):
|
||||
bookmarks_ots = ObjectType.objects.with_feature('bookmarks')
|
||||
self.assertIn(ObjectType.objects.get_by_natural_key('dcim', 'site'), bookmarks_ots)
|
||||
self.assertNotIn(ObjectType.objects.get_by_natural_key('dcim', 'cabletermination'), bookmarks_ots)
|
||||
|
||||
|
||||
class JobTest(TestCase):
|
||||
|
||||
@patch('core.models.jobs.django_rq.get_queue')
|
||||
def test_delete_cancels_job_from_correct_queue(self, mock_get_queue):
|
||||
"""
|
||||
Test that when a job is deleted, it's canceled from the correct queue.
|
||||
"""
|
||||
mock_queue = MagicMock()
|
||||
mock_rq_job = MagicMock()
|
||||
mock_queue.fetch_job.return_value = mock_rq_job
|
||||
mock_get_queue.return_value = mock_queue
|
||||
|
||||
def dummy_func(**kwargs):
|
||||
pass
|
||||
|
||||
# Enqueue a job with a custom queue name
|
||||
custom_queue = 'my_custom_queue'
|
||||
job = Job.enqueue(
|
||||
func=dummy_func,
|
||||
name='Test Job',
|
||||
queue_name=custom_queue
|
||||
)
|
||||
|
||||
# Reset mock to clear enqueue call
|
||||
mock_get_queue.reset_mock()
|
||||
|
||||
# Delete the job
|
||||
job.delete()
|
||||
|
||||
# Verify the correct queue was used for cancellation
|
||||
mock_get_queue.assert_called_with(custom_queue)
|
||||
mock_queue.fetch_job.assert_called_with(str(job.job_id))
|
||||
mock_rq_job.cancel.assert_called_once()
|
||||
|
||||
@@ -12,12 +12,11 @@ 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 User
|
||||
from users.models import Owner, User
|
||||
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
|
||||
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
|
||||
from utilities.forms.fields import ColorField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.widgets import NumberWithOptions
|
||||
from virtualization.models import Cluster, ClusterGroup, VirtualMachine
|
||||
@@ -71,11 +70,11 @@ __all__ = (
|
||||
'SiteFilterForm',
|
||||
'SiteGroupFilterForm',
|
||||
'VirtualChassisFilterForm',
|
||||
'VirtualDeviceContextFilterForm',
|
||||
'VirtualDeviceContextFilterForm'
|
||||
)
|
||||
|
||||
|
||||
class DeviceComponentFilterForm(OwnerFilterMixin, NetBoxModelFilterSetForm):
|
||||
class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
|
||||
name = forms.CharField(
|
||||
label=_('Name'),
|
||||
required=False
|
||||
@@ -158,14 +157,18 @@ class DeviceComponentFilterForm(OwnerFilterMixin, 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'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('parent_id', name=_('Region')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts'))
|
||||
)
|
||||
parent_id = DynamicModelMultipleChoiceField(
|
||||
@@ -179,9 +182,8 @@ class RegionFilterForm(ContactModelFilterForm, NestedGroupModelFilterSetForm):
|
||||
class SiteGroupFilterForm(ContactModelFilterForm, NestedGroupModelFilterSetForm):
|
||||
model = SiteGroup
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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(
|
||||
@@ -195,10 +197,9 @@ class SiteGroupFilterForm(ContactModelFilterForm, NestedGroupModelFilterSetForm)
|
||||
class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||
model = Site
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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')
|
||||
@@ -228,10 +229,9 @@ class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilt
|
||||
class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NestedGroupModelFilterSetForm):
|
||||
model = Location
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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,8 +277,7 @@ class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NestedGroupM
|
||||
class RackRoleFilterForm(OrganizationalModelFilterSetForm):
|
||||
model = RackRole
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -329,11 +328,10 @@ class RackBaseFilterForm(PrimaryModelFilterSetForm):
|
||||
class RackTypeFilterForm(RackBaseFilterForm):
|
||||
model = RackType
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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(
|
||||
@@ -352,14 +350,13 @@ class RackTypeFilterForm(RackBaseFilterForm):
|
||||
class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, RackBaseFilterForm):
|
||||
model = Rack
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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')
|
||||
@@ -436,10 +433,9 @@ 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('weight', 'max_weight', 'weight_unit', name=_('Weight')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
|
||||
)
|
||||
id = DynamicModelMultipleChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
@@ -455,11 +451,10 @@ class RackElevationFilterForm(RackFilterForm):
|
||||
class RackReservationFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = RackReservation
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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(),
|
||||
@@ -514,8 +509,7 @@ class RackReservationFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
class ManufacturerFilterForm(ContactModelFilterForm, OrganizationalModelFilterSetForm):
|
||||
model = Manufacturer
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts'))
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
@@ -524,7 +518,7 @@ class ManufacturerFilterForm(ContactModelFilterForm, OrganizationalModelFilterSe
|
||||
class DeviceTypeFilterForm(PrimaryModelFilterSetForm):
|
||||
model = DeviceType
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet(
|
||||
'manufacturer_id', 'default_platform_id', 'part_number', 'device_count',
|
||||
'subdevice_role', 'airflow', name=_('Hardware')
|
||||
@@ -535,7 +529,6 @@ 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(
|
||||
@@ -659,8 +652,7 @@ class DeviceTypeFilterForm(PrimaryModelFilterSetForm):
|
||||
class ModuleTypeProfileFilterForm(PrimaryModelFilterSetForm):
|
||||
model = ModuleTypeProfile
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q')
|
||||
tag = TagFilterField(model)
|
||||
@@ -669,7 +661,7 @@ class ModuleTypeProfileFilterForm(PrimaryModelFilterSetForm):
|
||||
class ModuleTypeFilterForm(PrimaryModelFilterSetForm):
|
||||
model = ModuleType
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet(
|
||||
'profile_id', 'manufacturer_id', 'part_number', 'module_count',
|
||||
'airflow', name=_('Hardware')
|
||||
@@ -679,7 +671,6 @@ 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(
|
||||
@@ -763,9 +754,8 @@ class ModuleTypeFilterForm(PrimaryModelFilterSetForm):
|
||||
class DeviceRoleFilterForm(NestedGroupModelFilterSetForm):
|
||||
model = DeviceRole
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('parent_id', 'config_template_id', name=_('Device Role')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('parent_id', 'config_template_id', name=_('Device Role'))
|
||||
)
|
||||
config_template_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ConfigTemplate.objects.all(),
|
||||
@@ -783,9 +773,8 @@ class DeviceRoleFilterForm(NestedGroupModelFilterSetForm):
|
||||
class PlatformFilterForm(NestedGroupModelFilterSetForm):
|
||||
model = Platform
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('manufacturer_id', 'parent_id', 'config_template_id', name=_('Platform')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('manufacturer_id', 'parent_id', 'config_template_id', name=_('Platform'))
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'manufacturer_id')
|
||||
parent_id = DynamicModelMultipleChoiceField(
|
||||
@@ -814,12 +803,11 @@ class DeviceFilterForm(
|
||||
):
|
||||
model = Device
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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',
|
||||
@@ -1008,10 +996,9 @@ class DeviceFilterForm(
|
||||
class VirtualDeviceContextFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = VirtualDeviceContext
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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(),
|
||||
@@ -1036,10 +1023,9 @@ class VirtualDeviceContextFilterForm(TenancyFilterForm, PrimaryModelFilterSetFor
|
||||
class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = Module
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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(),
|
||||
@@ -1120,10 +1106,9 @@ class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, PrimaryM
|
||||
class VirtualChassisFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = VirtualChassis
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
|
||||
FieldSet('tenant_id', name=_('Tenant')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
@@ -1150,11 +1135,10 @@ class VirtualChassisFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
class CableFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = Cable
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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(),
|
||||
@@ -1240,9 +1224,8 @@ class CableFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
class PowerPanelFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||
model = PowerPanel
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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')
|
||||
@@ -1280,11 +1263,10 @@ class PowerPanelFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||
class PowerFeedFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = PowerFeed
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'power_panel_id', 'rack_id', name=_('Location')),
|
||||
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')),
|
||||
FieldSet('status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', name=_('Attributes')),
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
@@ -1408,7 +1390,7 @@ class PathEndpointFilterForm(CabledFilterForm):
|
||||
class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
model = ConsolePort
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet(
|
||||
@@ -1416,7 +1398,6 @@ 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'),
|
||||
@@ -1448,7 +1429,7 @@ class ConsolePortTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
||||
class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
model = ConsoleServerPort
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet(
|
||||
@@ -1456,7 +1437,6 @@ 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'),
|
||||
@@ -1488,7 +1468,7 @@ class ConsoleServerPortTemplateFilterForm(ModularDeviceComponentTemplateFilterFo
|
||||
class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
model = PowerPort
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('name', 'label', 'type', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet(
|
||||
@@ -1496,7 +1476,6 @@ 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'),
|
||||
@@ -1523,7 +1502,7 @@ class PowerPortTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
||||
class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
model = PowerOutlet
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('name', 'label', 'type', 'color', 'status', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet(
|
||||
@@ -1531,7 +1510,6 @@ 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'),
|
||||
@@ -1567,7 +1545,7 @@ class PowerOutletTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
||||
class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
model = Interface
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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')),
|
||||
@@ -1580,7 +1558,6 @@ 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(
|
||||
@@ -1739,7 +1716,7 @@ class InterfaceTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
||||
|
||||
class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet(
|
||||
@@ -1747,7 +1724,6 @@ class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
||||
name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'occupied', name=_('Cable')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
model = FrontPort
|
||||
type = forms.MultipleChoiceField(
|
||||
@@ -1783,7 +1759,7 @@ class FrontPortTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
||||
class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
||||
model = RearPort
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet(
|
||||
@@ -1791,7 +1767,6 @@ class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
||||
name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'occupied', name=_('Cable')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
label=_('Type'),
|
||||
@@ -1826,14 +1801,13 @@ class RearPortTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
||||
class ModuleBayFilterForm(DeviceComponentFilterForm):
|
||||
model = ModuleBay
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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(
|
||||
@@ -1858,14 +1832,13 @@ class ModuleBayTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
||||
class DeviceBayFilterForm(DeviceComponentFilterForm):
|
||||
model = DeviceBay
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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)
|
||||
|
||||
@@ -1882,7 +1855,7 @@ class DeviceBayTemplateFilterForm(DeviceComponentTemplateFilterForm):
|
||||
class InventoryItemFilterForm(DeviceComponentFilterForm):
|
||||
model = InventoryItem
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet(
|
||||
'name', 'label', 'status', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered',
|
||||
name=_('Attributes')
|
||||
@@ -1892,7 +1865,6 @@ 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(),
|
||||
@@ -1953,8 +1925,7 @@ class InventoryItemTemplateFilterForm(DeviceComponentTemplateFilterForm):
|
||||
class InventoryItemRoleFilterForm(OrganizationalModelFilterSetForm):
|
||||
model = InventoryItemRole
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -1966,10 +1937,9 @@ class InventoryItemRoleFilterForm(OrganizationalModelFilterSetForm):
|
||||
class MACAddressFilterForm(PrimaryModelFilterSetForm):
|
||||
model = MACAddress
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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 self.instance.pk and scope_type_id != self.instance.scope_type_id:
|
||||
if self.instance and scope_type_id != self.instance.scope_type_id:
|
||||
self.initial['scope'] = None
|
||||
|
||||
else:
|
||||
|
||||
@@ -20,9 +20,7 @@ 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, ClearableSelect, HTMXSelect, NumberWithOptions, SelectWithPK,
|
||||
)
|
||||
from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK
|
||||
from utilities.jsonschema import JSONSchemaProperty
|
||||
from virtualization.models import Cluster, VMInterface
|
||||
from wireless.models import WirelessLAN, WirelessLANGroup
|
||||
@@ -594,14 +592,6 @@ 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,7 +3,6 @@ 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
|
||||
@@ -272,7 +271,7 @@ class InventoryItemCreateForm(ComponentCreateForm, model_forms.InventoryItemForm
|
||||
# Virtual chassis
|
||||
#
|
||||
|
||||
class VirtualChassisCreateForm(OwnerMixin, NetBoxModelForm):
|
||||
class VirtualChassisCreateForm(NetBoxModelForm):
|
||||
region = DynamicModelChoiceField(
|
||||
label=_('Region'),
|
||||
queryset=Region.objects.all(),
|
||||
|
||||
@@ -550,10 +550,6 @@ 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:
|
||||
@@ -893,7 +889,7 @@ class PowerPortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedM
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.RackType, lookups=True)
|
||||
class RackTypeFilter(ImageAttachmentFilterMixin, RackFilterMixin, WeightFilterMixin, PrimaryModelFilter):
|
||||
class RackTypeFilter(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(ImageAttachmentsMixin, PrimaryObjectType):
|
||||
class RackTypeType(PrimaryObjectType):
|
||||
rack_count: BigInt
|
||||
manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')]
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from jsonschema.exceptions import ValidationError as JSONValidationError
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.utils import create_port_mappings, update_interface_bridges
|
||||
from dcim.utils import update_interface_bridges
|
||||
from extras.models import ConfigContextModel, CustomField
|
||||
from netbox.models import PrimaryModel
|
||||
from netbox.models.features import ImageAttachmentsMixin
|
||||
@@ -155,8 +155,6 @@ 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,
|
||||
}
|
||||
|
||||
@@ -361,7 +359,5 @@ class Module(TrackingModelMixin, PrimaryModel, ConfigContextModel):
|
||||
update_fields=update_fields
|
||||
)
|
||||
|
||||
# Replicate any front/rear port mappings from the ModuleType
|
||||
create_port_mappings(self.device, self.module_type, self)
|
||||
# Interface bridges have to be set after interface instantiation
|
||||
update_interface_bridges(self.device, self.module_type.interfacetemplates, self)
|
||||
|
||||
@@ -122,7 +122,7 @@ class RackBase(WeightMixin, PrimaryModel):
|
||||
abstract = True
|
||||
|
||||
|
||||
class RackType(ImageAttachmentsMixin, RackBase):
|
||||
class RackType(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,7 +27,6 @@ __all__ = (
|
||||
'DeviceTable',
|
||||
'FrontPortTable',
|
||||
'InterfaceTable',
|
||||
'InterfaceLAGMemberTable',
|
||||
'InventoryItemRoleTable',
|
||||
'InventoryItemTable',
|
||||
'MACAddressTable',
|
||||
@@ -690,33 +689,6 @@ 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,24 +24,6 @@ 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 %}
|
||||
|
||||
@@ -875,142 +875,6 @@ class ModuleBayTestCase(TestCase):
|
||||
self.assertIsNone(bay2.parent)
|
||||
self.assertIsNone(bay2.module)
|
||||
|
||||
def test_module_installation_creates_port_mappings(self):
|
||||
"""
|
||||
Test that installing a module with front/rear port templates correctly
|
||||
creates PortMapping instances for the device.
|
||||
"""
|
||||
device = Device.objects.first()
|
||||
manufacturer = Manufacturer.objects.first()
|
||||
module_bay = ModuleBay.objects.create(device=device, name='Test Bay PortMapping 1')
|
||||
|
||||
# Create a module type with a rear port template
|
||||
module_type_with_mappings = ModuleType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='Module Type With Mappings',
|
||||
)
|
||||
|
||||
# Create a rear port template with 12 positions (splice)
|
||||
rear_port_template = RearPortTemplate.objects.create(
|
||||
module_type=module_type_with_mappings,
|
||||
name='Rear Port 1',
|
||||
type=PortTypeChoices.TYPE_SPLICE,
|
||||
positions=12,
|
||||
)
|
||||
|
||||
# Create 12 front port templates mapped to the rear port
|
||||
front_port_templates = []
|
||||
for i in range(1, 13):
|
||||
front_port_template = FrontPortTemplate.objects.create(
|
||||
module_type=module_type_with_mappings,
|
||||
name=f'port {i}',
|
||||
type=PortTypeChoices.TYPE_LC,
|
||||
positions=1,
|
||||
)
|
||||
front_port_templates.append(front_port_template)
|
||||
|
||||
# Create port template mapping
|
||||
PortTemplateMapping.objects.create(
|
||||
device_type=None,
|
||||
module_type=module_type_with_mappings,
|
||||
front_port=front_port_template,
|
||||
front_port_position=1,
|
||||
rear_port=rear_port_template,
|
||||
rear_port_position=i,
|
||||
)
|
||||
|
||||
# Install the module
|
||||
module = Module.objects.create(
|
||||
device=device,
|
||||
module_bay=module_bay,
|
||||
module_type=module_type_with_mappings,
|
||||
status=ModuleStatusChoices.STATUS_ACTIVE,
|
||||
)
|
||||
|
||||
# Verify that front ports were created
|
||||
front_ports = FrontPort.objects.filter(device=device, module=module)
|
||||
self.assertEqual(front_ports.count(), 12)
|
||||
|
||||
# Verify that the rear port was created
|
||||
rear_ports = RearPort.objects.filter(device=device, module=module)
|
||||
self.assertEqual(rear_ports.count(), 1)
|
||||
rear_port = rear_ports.first()
|
||||
self.assertEqual(rear_port.positions, 12)
|
||||
|
||||
# Verify that port mappings were created
|
||||
port_mappings = PortMapping.objects.filter(front_port__module=module)
|
||||
self.assertEqual(port_mappings.count(), 12)
|
||||
|
||||
# Verify each mapping is correct
|
||||
for i, front_port_template in enumerate(front_port_templates, start=1):
|
||||
front_port = FrontPort.objects.get(
|
||||
device=device,
|
||||
name=front_port_template.name,
|
||||
module=module,
|
||||
)
|
||||
|
||||
# Check that a mapping exists for this front port
|
||||
mapping = PortMapping.objects.get(
|
||||
device=device,
|
||||
front_port=front_port,
|
||||
front_port_position=1,
|
||||
)
|
||||
|
||||
self.assertEqual(mapping.rear_port, rear_port)
|
||||
self.assertEqual(mapping.front_port_position, 1)
|
||||
self.assertEqual(mapping.rear_port_position, i)
|
||||
|
||||
def test_module_installation_without_mappings(self):
|
||||
"""
|
||||
Test that installing a module without port template mappings
|
||||
doesn't create any PortMapping instances.
|
||||
"""
|
||||
device = Device.objects.first()
|
||||
manufacturer = Manufacturer.objects.first()
|
||||
module_bay = ModuleBay.objects.create(device=device, name='Test Bay PortMapping 2')
|
||||
|
||||
# Create a module type without any port template mappings
|
||||
module_type_no_mappings = ModuleType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='Module Type Without Mappings',
|
||||
)
|
||||
|
||||
# Create a rear port template
|
||||
RearPortTemplate.objects.create(
|
||||
module_type=module_type_no_mappings,
|
||||
name='Rear Port 1',
|
||||
type=PortTypeChoices.TYPE_SPLICE,
|
||||
positions=12,
|
||||
)
|
||||
|
||||
# Create front port templates but DO NOT create PortTemplateMapping rows
|
||||
for i in range(1, 13):
|
||||
FrontPortTemplate.objects.create(
|
||||
module_type=module_type_no_mappings,
|
||||
name=f'port {i}',
|
||||
type=PortTypeChoices.TYPE_LC,
|
||||
positions=1,
|
||||
)
|
||||
|
||||
# Install the module
|
||||
module = Module.objects.create(
|
||||
device=device,
|
||||
module_bay=module_bay,
|
||||
module_type=module_type_no_mappings,
|
||||
status=ModuleStatusChoices.STATUS_ACTIVE,
|
||||
)
|
||||
|
||||
# Verify no port mappings were created for this module
|
||||
port_mappings = PortMapping.objects.filter(
|
||||
device=device,
|
||||
front_port__module=module,
|
||||
front_port_position=1,
|
||||
)
|
||||
self.assertEqual(port_mappings.count(), 0)
|
||||
self.assertEqual(FrontPort.objects.filter(module=module).count(), 12)
|
||||
self.assertEqual(RearPort.objects.filter(module=module).count(), 1)
|
||||
self.assertEqual(PortMapping.objects.filter(front_port__module=module).count(), 0)
|
||||
|
||||
|
||||
class CableTestCase(TestCase):
|
||||
|
||||
|
||||
@@ -85,13 +85,13 @@ def update_interface_bridges(device, interface_templates, module=None):
|
||||
interface.save()
|
||||
|
||||
|
||||
def create_port_mappings(device, device_or_module_type, module=None):
|
||||
def create_port_mappings(device, device_type, module=None):
|
||||
"""
|
||||
Replicate all front/rear port mappings from a DeviceType or ModuleType to the given device.
|
||||
Replicate all front/rear port mappings from a DeviceType to the given device.
|
||||
"""
|
||||
from dcim.models import FrontPort, PortMapping, RearPort
|
||||
|
||||
templates = device_or_module_type.port_mappings.prefetch_related('front_port', 'rear_port')
|
||||
templates = device_type.port_mappings.prefetch_related('front_port', 'rear_port')
|
||||
|
||||
# Cache front & rear ports for efficient lookups by name
|
||||
front_ports = {
|
||||
|
||||
@@ -880,7 +880,6 @@ class RackTypeView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
panels.RackWeightPanel(title=_('Weight'), exclude=['total_weight']),
|
||||
CustomFieldsPanel(),
|
||||
RelatedObjectsPanel(),
|
||||
ImageAttachmentsPanel(),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -3136,14 +3135,6 @@ 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:
|
||||
@@ -3173,7 +3164,6 @@ 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 UserDict, defaultdict
|
||||
from collections import defaultdict
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
@@ -12,6 +12,7 @@ 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
|
||||
@@ -22,21 +23,6 @@ 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.
|
||||
@@ -80,42 +66,37 @@ 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] = 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,
|
||||
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,
|
||||
# Legacy request attributes for backward compatibility
|
||||
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)
|
||||
'username': request.user.username,
|
||||
'request_id': request.id,
|
||||
}
|
||||
|
||||
|
||||
def process_event_rules(event_rules, object_type, event):
|
||||
"""
|
||||
Process a list of EventRules against an event.
|
||||
"""
|
||||
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
|
||||
|
||||
for event_rule in event_rules:
|
||||
|
||||
# Evaluate event rule conditions (if any)
|
||||
if not event_rule.eval_conditions(event['data']):
|
||||
if not event_rule.eval_conditions(data):
|
||||
continue
|
||||
|
||||
# Compile event data
|
||||
event_data = event_rule.action_data or {}
|
||||
event_data.update(event['data'])
|
||||
event_data.update(data)
|
||||
|
||||
# Webhooks
|
||||
if event_rule.action_type == EventRuleActionChoices.WEBHOOK:
|
||||
@@ -128,20 +109,25 @@ def process_event_rules(event_rules, object_type, event):
|
||||
params = {
|
||||
"event_rule": event_rule,
|
||||
"object_type": object_type,
|
||||
"event_type": event['event_type'],
|
||||
"event_type": event_type,
|
||||
"data": event_data,
|
||||
"snapshots": event.get('snapshots'),
|
||||
"snapshots": snapshots,
|
||||
"timestamp": timezone.now().isoformat(),
|
||||
"username": event['username'],
|
||||
"username": username,
|
||||
"retry": get_rq_retry()
|
||||
}
|
||||
if 'request' in event:
|
||||
if snapshots:
|
||||
params["snapshots"] = snapshots
|
||||
if request:
|
||||
# Exclude FILES - webhooks don't need uploaded files,
|
||||
# which can cause pickle errors with Pillow.
|
||||
params['request'] = copy_safe_request(event['request'], include_files=False)
|
||||
params["request"] = copy_safe_request(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:
|
||||
@@ -153,16 +139,16 @@ def process_event_rules(event_rules, object_type, event):
|
||||
params = {
|
||||
"instance": event_rule.action_object,
|
||||
"name": script.name,
|
||||
"user": event['user'],
|
||||
"user": user,
|
||||
"data": event_data
|
||||
}
|
||||
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)
|
||||
if snapshots:
|
||||
params["snapshots"] = snapshots
|
||||
if request:
|
||||
params["request"] = copy_safe_request(request)
|
||||
ScriptJob.enqueue(
|
||||
**params
|
||||
)
|
||||
|
||||
# Notification groups
|
||||
elif event_rule.action_type == EventRuleActionChoices.NOTIFICATION:
|
||||
@@ -171,7 +157,7 @@ def process_event_rules(event_rules, object_type, event):
|
||||
object_type=object_type,
|
||||
object_id=event_data['id'],
|
||||
object_repr=event_data.get('display'),
|
||||
event_type=event['event_type']
|
||||
event_type=event_type
|
||||
)
|
||||
|
||||
else:
|
||||
@@ -183,8 +169,6 @@ def process_event_rules(event_rules, object_type, event):
|
||||
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)
|
||||
|
||||
@@ -204,7 +188,11 @@ def process_event_queue(events):
|
||||
process_event_rules(
|
||||
event_rules=event_rules,
|
||||
object_type=object_type,
|
||||
event=event,
|
||||
event_type=event['event_type'],
|
||||
data=event['data'],
|
||||
username=event['username'],
|
||||
snapshots=event['snapshots'],
|
||||
request=event['request'],
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -7,12 +7,13 @@ 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 OwnerFilterMixin, SavedFiltersMixin
|
||||
from netbox.forms.mixins import SavedFiltersMixin
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from users.models import Group, User
|
||||
from users.models import Group, Owner, User
|
||||
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
|
||||
from utilities.forms.fields import (
|
||||
ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField,
|
||||
ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
||||
TagFilterField,
|
||||
)
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.widgets import DateTimePicker
|
||||
@@ -38,7 +39,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class CustomFieldFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm):
|
||||
class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
|
||||
model = CustomField
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id'),
|
||||
@@ -46,7 +47,6 @@ class CustomFieldFilterForm(OwnerFilterMixin, 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,14 +119,18 @@ class CustomFieldFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm):
|
||||
label=_('Validation regex'),
|
||||
required=False
|
||||
)
|
||||
owner_id = DynamicModelChoiceField(
|
||||
queryset=Owner.objects.all(),
|
||||
required=False,
|
||||
label=_('Owner'),
|
||||
)
|
||||
|
||||
|
||||
class CustomFieldChoiceSetFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm):
|
||||
class CustomFieldChoiceSetFilterForm(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,
|
||||
@@ -135,14 +139,18 @@ class CustomFieldChoiceSetFilterForm(OwnerFilterMixin, SavedFiltersMixin, Filter
|
||||
choice = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
owner_id = DynamicModelChoiceField(
|
||||
queryset=Owner.objects.all(),
|
||||
required=False,
|
||||
label=_('Owner'),
|
||||
)
|
||||
|
||||
|
||||
class CustomLinkFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm):
|
||||
class CustomLinkFilterForm(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'),
|
||||
@@ -167,15 +175,19 @@ class CustomLinkFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm):
|
||||
label=_('Weight'),
|
||||
required=False
|
||||
)
|
||||
owner_id = DynamicModelChoiceField(
|
||||
queryset=Owner.objects.all(),
|
||||
required=False,
|
||||
label=_('Owner'),
|
||||
)
|
||||
|
||||
|
||||
class ExportTemplateFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm):
|
||||
class ExportTemplateFilterForm(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(),
|
||||
@@ -214,6 +226,11 @@ class ExportTemplateFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm):
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
owner_id = DynamicModelChoiceField(
|
||||
queryset=Owner.objects.all(),
|
||||
required=False,
|
||||
label=_('Owner'),
|
||||
)
|
||||
|
||||
|
||||
class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
|
||||
@@ -233,12 +250,11 @@ class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
|
||||
)
|
||||
|
||||
|
||||
class SavedFilterFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm):
|
||||
class SavedFilterFilterForm(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'),
|
||||
@@ -263,6 +279,11 @@ class SavedFilterFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm):
|
||||
label=_('Weight'),
|
||||
required=False
|
||||
)
|
||||
owner_id = DynamicModelChoiceField(
|
||||
queryset=Owner.objects.all(),
|
||||
required=False,
|
||||
label=_('Owner'),
|
||||
)
|
||||
|
||||
|
||||
class TableConfigFilterForm(SavedFiltersMixin, FilterForm):
|
||||
@@ -296,12 +317,11 @@ class TableConfigFilterForm(SavedFiltersMixin, FilterForm):
|
||||
)
|
||||
|
||||
|
||||
class WebhookFilterForm(OwnerFilterMixin, NetBoxModelFilterSetForm):
|
||||
class WebhookFilterForm(NetBoxModelFilterSetForm):
|
||||
model = Webhook
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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'),
|
||||
@@ -316,15 +336,19 @@ class WebhookFilterForm(OwnerFilterMixin, NetBoxModelFilterSetForm):
|
||||
required=False,
|
||||
label=_('HTTP method')
|
||||
)
|
||||
owner_id = DynamicModelChoiceField(
|
||||
queryset=Owner.objects.all(),
|
||||
required=False,
|
||||
label=_('Owner'),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class EventRuleFilterForm(OwnerFilterMixin, NetBoxModelFilterSetForm):
|
||||
class EventRuleFilterForm(NetBoxModelFilterSetForm):
|
||||
model = EventRule
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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'),
|
||||
@@ -348,16 +372,16 @@ class EventRuleFilterForm(OwnerFilterMixin, NetBoxModelFilterSetForm):
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
owner_id = DynamicModelChoiceField(
|
||||
queryset=Owner.objects.all(),
|
||||
required=False,
|
||||
label=_('Owner'),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class TagFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm):
|
||||
class TagFilterForm(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,
|
||||
@@ -368,6 +392,11 @@ class TagFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm):
|
||||
required=False,
|
||||
label=_('Allowed object type')
|
||||
)
|
||||
owner_id = DynamicModelChoiceField(
|
||||
queryset=Owner.objects.all(),
|
||||
required=False,
|
||||
label=_('Owner'),
|
||||
)
|
||||
|
||||
|
||||
class ConfigContextProfileFilterForm(PrimaryModelFilterSetForm):
|
||||
@@ -375,7 +404,6 @@ 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(),
|
||||
@@ -392,17 +420,16 @@ class ConfigContextProfileFilterForm(PrimaryModelFilterSetForm):
|
||||
)
|
||||
|
||||
|
||||
class ConfigContextFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm):
|
||||
class ConfigContextFilterForm(SavedFiltersMixin, FilterForm):
|
||||
model = ConfigContext
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag_id'),
|
||||
FieldSet('profile_id', name=_('Config Context')),
|
||||
FieldSet('profile', 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('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant'))
|
||||
)
|
||||
profile_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ConfigContextProfile.objects.all(),
|
||||
@@ -487,15 +514,19 @@ class ConfigContextFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm):
|
||||
required=False,
|
||||
label=_('Tags')
|
||||
)
|
||||
owner_id = DynamicModelChoiceField(
|
||||
queryset=Owner.objects.all(),
|
||||
required=False,
|
||||
label=_('Owner'),
|
||||
)
|
||||
|
||||
|
||||
class ConfigTemplateFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm):
|
||||
class ConfigTemplateFilterForm(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('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('mime_type', 'file_name', 'file_extension', 'as_attachment', name=_('Rendering'))
|
||||
)
|
||||
data_source_id = DynamicModelMultipleChoiceField(
|
||||
queryset=DataSource.objects.all(),
|
||||
@@ -537,6 +568,11 @@ class ConfigTemplateFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm):
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
owner_id = DynamicModelChoiceField(
|
||||
queryset=Owner.objects.all(),
|
||||
required=False,
|
||||
label=_('Owner'),
|
||||
)
|
||||
|
||||
|
||||
class LocalConfigContextFilterForm(forms.Form):
|
||||
|
||||
@@ -178,13 +178,6 @@ 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.error(f"Failed to load script: {self.python_name} error: {e}")
|
||||
logger.debug(f"Failed to load script: {self.python_name} error: {e}")
|
||||
module = None
|
||||
|
||||
scripts = {}
|
||||
|
||||
@@ -61,7 +61,7 @@ class ScriptVariable:
|
||||
self.field_attrs['label'] = label
|
||||
if description:
|
||||
self.field_attrs['help_text'] = description
|
||||
if default is not None:
|
||||
if default:
|
||||
self.field_attrs['initial'] = default
|
||||
if widget:
|
||||
self.field_attrs['widget'] = widget
|
||||
|
||||
@@ -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 EventContext, process_event_rules
|
||||
from extras.events import process_event_rules
|
||||
from extras.models import EventRule, Notification, Subscription
|
||||
from netbox.config import get_config
|
||||
from netbox.models.features import has_feature
|
||||
@@ -102,12 +102,14 @@ def process_job_start_event_rules(sender, **kwargs):
|
||||
enabled=True,
|
||||
object_types=sender.object_type
|
||||
)
|
||||
event = EventContext(
|
||||
username = sender.user.username if sender.user else None
|
||||
process_event_rules(
|
||||
event_rules=event_rules,
|
||||
object_type=sender.object_type,
|
||||
event_type=JOB_STARTED,
|
||||
data=sender.data,
|
||||
user=sender.user,
|
||||
username=username
|
||||
)
|
||||
process_event_rules(event_rules, sender.object_type, event)
|
||||
|
||||
|
||||
@receiver(job_end)
|
||||
@@ -120,12 +122,14 @@ def process_job_end_event_rules(sender, **kwargs):
|
||||
enabled=True,
|
||||
object_types=sender.object_type
|
||||
)
|
||||
event = EventContext(
|
||||
username = sender.user.username if sender.user else None
|
||||
process_event_rules(
|
||||
event_rules=event_rules,
|
||||
object_type=sender.object_type,
|
||||
event_type=JOB_COMPLETED,
|
||||
data=sender.data,
|
||||
user=sender.user,
|
||||
username=username
|
||||
)
|
||||
process_event_rules(event_rules, sender.object_type, event)
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -43,7 +43,7 @@ IMAGEATTACHMENT_IMAGE = """
|
||||
<a href="{{ record.image.url }}" target="_blank" class="image-preview" data-bs-placement="top">
|
||||
<i class="mdi mdi-image"></i></a>
|
||||
{% endif %}
|
||||
<a href="{{ record.get_absolute_url }}">{{ record.filename|truncate_middle:16 }}</a>
|
||||
<a href="{{ record.get_absolute_url }}">{{ record }}</a>
|
||||
"""
|
||||
|
||||
NOTIFICATION_ICON = """
|
||||
|
||||
@@ -6,7 +6,7 @@ from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.forms import ValidationError
|
||||
from django.test import tag, TestCase
|
||||
|
||||
from core.models import AutoSyncRecord, DataSource, ObjectType
|
||||
from core.models import DataSource, ObjectType
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Location, Manufacturer, Platform, Region, Site, SiteGroup
|
||||
from extras.models import ConfigContext, ConfigContextProfile, ConfigTemplate, ImageAttachment, Tag, TaggedItem
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
@@ -754,53 +754,3 @@ class ConfigTemplateTest(TestCase):
|
||||
@tag('regression')
|
||||
def test_config_template_with_data_source_nested_templates(self):
|
||||
self.assertEqual(self.BASE_TEMPLATE, self.main_config_template.render({}))
|
||||
|
||||
@tag('regression')
|
||||
def test_autosyncrecord_cleanup_on_detach(self):
|
||||
"""Test that AutoSyncRecord is deleted when detaching from DataSource."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
templates_dir = Path(temp_dir) / "templates"
|
||||
templates_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
self._create_template_file(templates_dir, 'test.j2', 'Test content')
|
||||
|
||||
data_source = DataSource(
|
||||
name="Test DataSource for Detach",
|
||||
type="local",
|
||||
source_url=str(templates_dir),
|
||||
)
|
||||
data_source.save()
|
||||
data_source.sync()
|
||||
|
||||
data_file = data_source.datafiles.filter(path__endswith='test.j2').first()
|
||||
|
||||
# Create a ConfigTemplate with data_file and auto_sync_enabled
|
||||
config_template = ConfigTemplate(
|
||||
name="TestTemplateForDetach",
|
||||
data_file=data_file,
|
||||
auto_sync_enabled=True
|
||||
)
|
||||
config_template.clean()
|
||||
config_template.save()
|
||||
|
||||
# Verify AutoSyncRecord was created
|
||||
object_type = ObjectType.objects.get_for_model(ConfigTemplate)
|
||||
autosync_records = AutoSyncRecord.objects.filter(
|
||||
object_type=object_type,
|
||||
object_id=config_template.pk
|
||||
)
|
||||
self.assertEqual(autosync_records.count(), 1, "AutoSyncRecord should be created")
|
||||
|
||||
# Detach from DataSource
|
||||
config_template.data_file = None
|
||||
config_template.data_source = None
|
||||
config_template.auto_sync_enabled = False
|
||||
config_template.clean()
|
||||
config_template.save()
|
||||
|
||||
# Verify AutoSyncRecord was deleted
|
||||
autosync_records = AutoSyncRecord.objects.filter(
|
||||
object_type=object_type,
|
||||
object_id=config_template.pk
|
||||
)
|
||||
self.assertEqual(autosync_records.count(), 0, "AutoSyncRecord should be deleted after detaching")
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.urls import reverse
|
||||
from django.test import tag
|
||||
from unittest.mock import patch, PropertyMock
|
||||
|
||||
from core.choices import ManagedFileRootPathChoices
|
||||
from core.events import *
|
||||
@@ -907,7 +906,7 @@ class ScriptValidationErrorTest(TestCase):
|
||||
user_permissions = ['extras.view_script', 'extras.run_script']
|
||||
|
||||
class TestScriptMixin:
|
||||
bar = IntegerVar(min_value=0, max_value=30)
|
||||
bar = IntegerVar(min_value=0, max_value=30, default=30)
|
||||
|
||||
class TestScriptClass(TestScriptMixin, PythonClass):
|
||||
class Meta:
|
||||
@@ -931,6 +930,8 @@ class ScriptValidationErrorTest(TestCase):
|
||||
|
||||
@tag('regression')
|
||||
def test_script_validation_error_displays_message(self):
|
||||
from unittest.mock import patch
|
||||
|
||||
url = reverse('extras:script', kwargs={'pk': self.script.pk})
|
||||
|
||||
with patch('extras.views.get_workers_for_queue', return_value=['worker']):
|
||||
@@ -943,6 +944,8 @@ class ScriptValidationErrorTest(TestCase):
|
||||
|
||||
@tag('regression')
|
||||
def test_script_validation_error_no_toast_for_fieldset_fields(self):
|
||||
from unittest.mock import patch, PropertyMock
|
||||
|
||||
class FieldsetScript(PythonClass):
|
||||
class Meta:
|
||||
name = 'Fieldset test'
|
||||
@@ -964,42 +967,3 @@ class ScriptValidationErrorTest(TestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
messages = list(response.context['messages'])
|
||||
self.assertEqual(len(messages), 0)
|
||||
|
||||
|
||||
class ScriptDefaultValuesTest(TestCase):
|
||||
user_permissions = ['extras.view_script', 'extras.run_script']
|
||||
|
||||
class TestScriptClass(PythonClass):
|
||||
class Meta:
|
||||
name = 'Test script'
|
||||
commit_default = False
|
||||
|
||||
bool_default_true = BooleanVar(default=True)
|
||||
bool_default_false = BooleanVar(default=False)
|
||||
int_with_default = IntegerVar(default=0)
|
||||
int_without_default = IntegerVar(required=False)
|
||||
|
||||
def run(self, data, commit):
|
||||
return "Complete"
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
module = ScriptModule.objects.create(file_root=ManagedFileRootPathChoices.SCRIPTS, file_path='test_script.py')
|
||||
cls.script = Script.objects.create(module=module, name='Test script', is_executable=True)
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
Script.python_class = property(lambda self: ScriptDefaultValuesTest.TestScriptClass)
|
||||
|
||||
def test_default_values_are_used(self):
|
||||
url = reverse('extras:script', kwargs={'pk': self.script.pk})
|
||||
|
||||
with patch('extras.views.get_workers_for_queue', return_value=['worker']):
|
||||
with patch('extras.jobs.ScriptJob.enqueue') as mock_enqueue:
|
||||
mock_enqueue.return_value.pk = 1
|
||||
self.client.post(url, {})
|
||||
call_kwargs = mock_enqueue.call_args.kwargs
|
||||
self.assertEqual(call_kwargs['data']['bool_default_true'], True)
|
||||
self.assertEqual(call_kwargs['data']['bool_default_false'], False)
|
||||
self.assertEqual(call_kwargs['data']['int_with_default'], 0)
|
||||
self.assertIsNone(call_kwargs['data']['int_without_default'])
|
||||
|
||||
@@ -1511,13 +1511,7 @@ class ScriptView(BaseScriptView):
|
||||
'script': script,
|
||||
})
|
||||
|
||||
# Populate missing variables with their default values, if defined
|
||||
post_data = request.POST.copy()
|
||||
for name, var in script_class._get_vars().items():
|
||||
if name not in post_data and (initial := var.field_attrs.get('initial')) is not None:
|
||||
post_data[name] = initial
|
||||
|
||||
form = script_class.as_form(post_data, request.FILES)
|
||||
form = script_class.as_form(request.POST, request.FILES)
|
||||
|
||||
# Allow execution only if RQ worker process is running
|
||||
if not get_workers_for_queue('default'):
|
||||
|
||||
@@ -45,10 +45,9 @@ IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice([
|
||||
class VRFFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = VRF
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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(),
|
||||
@@ -66,10 +65,9 @@ class VRFFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
class RouteTargetFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = RouteTarget
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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(),
|
||||
@@ -87,9 +85,8 @@ class RouteTargetFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
class RIRFilterForm(OrganizationalModelFilterSetForm):
|
||||
model = RIR
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('is_private', name=_('RIR')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
is_private = forms.NullBooleanField(
|
||||
required=False,
|
||||
@@ -104,10 +101,9 @@ class RIRFilterForm(OrganizationalModelFilterSetForm):
|
||||
class AggregateFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = Aggregate
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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(
|
||||
@@ -126,10 +122,9 @@ class AggregateFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryMode
|
||||
class ASNRangeFilterForm(TenancyFilterForm, OrganizationalModelFilterSetForm):
|
||||
model = ASNRange
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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(),
|
||||
@@ -150,10 +145,9 @@ class ASNRangeFilterForm(TenancyFilterForm, OrganizationalModelFilterSetForm):
|
||||
class ASNFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = ASN
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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(),
|
||||
@@ -176,8 +170,7 @@ class ASNFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
class RoleFilterForm(OrganizationalModelFilterSetForm):
|
||||
model = Role
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -185,7 +178,7 @@ class RoleFilterForm(OrganizationalModelFilterSetForm):
|
||||
class PrefixFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = Prefix
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet(
|
||||
'within_include', 'family', 'status', 'role_id', 'mask_length', 'is_pool', 'mark_utilized',
|
||||
name=_('Addressing')
|
||||
@@ -194,7 +187,6 @@ 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(
|
||||
@@ -292,10 +284,9 @@ class PrefixFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFi
|
||||
class IPRangeFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = IPRange
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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(
|
||||
@@ -340,15 +331,14 @@ class IPRangeFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelF
|
||||
class IPAddressFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = IPAddress
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet(
|
||||
'parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface', 'dns_name',
|
||||
name=_('Attributes')
|
||||
),
|
||||
FieldSet('vrf_id', 'present_in_vrf_id', name=_('VRF')),
|
||||
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('device_id', 'virtual_machine_id', name=_('Device/VM')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'region_id', 'group_id', 'parent', 'status', 'role')
|
||||
@@ -419,10 +409,9 @@ class IPAddressFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryMode
|
||||
class FHRPGroupFilterForm(PrimaryModelFilterSetForm):
|
||||
model = FHRPGroup
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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'),
|
||||
@@ -452,12 +441,11 @@ class FHRPGroupFilterForm(PrimaryModelFilterSetForm):
|
||||
|
||||
class VLANGroupFilterForm(TenancyFilterForm, OrganizationalModelFilterSetForm):
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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(
|
||||
@@ -507,9 +495,8 @@ class VLANGroupFilterForm(TenancyFilterForm, OrganizationalModelFilterSetForm):
|
||||
class VLANTranslationPolicyFilterForm(PrimaryModelFilterSetForm):
|
||||
model = VLANTranslationPolicy
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('name', name=_('Attributes')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
name = forms.CharField(
|
||||
required=False,
|
||||
@@ -545,12 +532,11 @@ class VLANTranslationRuleFilterForm(NetBoxModelFilterSetForm):
|
||||
class VLANFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = VLAN
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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(
|
||||
@@ -618,9 +604,8 @@ class VLANFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
class ServiceTemplateFilterForm(PrimaryModelFilterSetForm):
|
||||
model = ServiceTemplate
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('protocol', 'port', name=_('Attributes')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
protocol = forms.ChoiceField(
|
||||
label=_('Protocol'),
|
||||
@@ -637,10 +622,9 @@ class ServiceTemplateFilterForm(PrimaryModelFilterSetForm):
|
||||
class ServiceFilterForm(ContactModelFilterForm, ServiceTemplateFilterForm):
|
||||
model = Service
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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,11 +370,6 @@ 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
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ PREFIX_LINK = """
|
||||
{% if record.pk %}
|
||||
<a href="{{ record.get_absolute_url }}" id="prefix_{{ record.pk }}">{{ record.prefix }}</a>
|
||||
{% else %}
|
||||
<a href="{% url 'ipam:prefix_add' %}?prefix={{ record }}{% if object.vrf %}&vrf={{ object.vrf.pk }}{% endif %}{% if object.scope %}&scope_type={{ object.scope_type.pk }}&scope={{ object.scope.pk }}{% endif %}{% if object.tenant %}&tenant_group={{ object.tenant.group.pk }}&tenant={{ object.tenant.pk }}{% endif %}">{{ record.prefix }}</a>
|
||||
<a href="{% url 'ipam:prefix_add' %}?prefix={{ record }}{% if object.vrf %}&vrf={{ object.vrf.pk }}{% endif %}{% if object.site %}&site={{ object.site.pk }}{% endif %}{% if object.tenant %}&tenant_group={{ object.tenant.group.pk }}&tenant={{ object.tenant.pk }}{% endif %}">{{ record.prefix }}</a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ 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
|
||||
@@ -160,26 +159,11 @@ 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', 'link_peer', 'connection', 'tagged', 'actions')
|
||||
default_columns = ('device', 'name', 'connection', 'tagged', 'actions')
|
||||
exclude = ('id',)
|
||||
fields = ('device', 'name', 'tagged', 'actions')
|
||||
exclude = ('id', )
|
||||
|
||||
|
||||
class VLANVirtualMachinesTable(VLANMembersTable):
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
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,9 +49,6 @@ 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
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from functools import cached_property
|
||||
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import serializers
|
||||
from rest_framework.utils.serializer_helpers import BindingDict
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
|
||||
from utilities.api import get_related_object_by_attrs
|
||||
from .fields import NetBoxAPIHyperlinkedIdentityField, NetBoxURLHyperlinkedIdentityField
|
||||
@@ -18,18 +19,16 @@ class BaseModelSerializer(serializers.ModelSerializer):
|
||||
display_url = NetBoxURLHyperlinkedIdentityField()
|
||||
display = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
def __init__(self, *args, nested=False, fields=None, omit=None, **kwargs):
|
||||
def __init__(self, *args, nested=False, fields=None, **kwargs):
|
||||
"""
|
||||
Extends the base __init__() method to support dynamic fields.
|
||||
|
||||
:param nested: Set to True if this serializer is being employed within a parent serializer
|
||||
:param fields: An iterable of fields to include when rendering the serialized object, If nested is
|
||||
True but no fields are specified, Meta.brief_fields will be used.
|
||||
:param omit: An iterable of fields to omit from the serialized object
|
||||
"""
|
||||
self.nested = nested
|
||||
self._include_fields = fields or []
|
||||
self._omit_fields = omit or []
|
||||
self._requested_fields = fields
|
||||
|
||||
# Disable validators for nested objects (which already exist)
|
||||
if self.nested:
|
||||
@@ -37,8 +36,8 @@ class BaseModelSerializer(serializers.ModelSerializer):
|
||||
|
||||
# If this serializer is nested but no fields have been specified,
|
||||
# default to using Meta.brief_fields (if set)
|
||||
if self.nested and not fields and not omit:
|
||||
self._include_fields = getattr(self.Meta, 'brief_fields', None)
|
||||
if self.nested and not fields:
|
||||
self._requested_fields = getattr(self.Meta, 'brief_fields', None)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -55,19 +54,16 @@ class BaseModelSerializer(serializers.ModelSerializer):
|
||||
@cached_property
|
||||
def fields(self):
|
||||
"""
|
||||
Override the fields property to return only specifically requested fields if needed.
|
||||
Override the fields property to check for requested fields. If defined,
|
||||
return only the applicable fields.
|
||||
"""
|
||||
fields = super().fields
|
||||
|
||||
# Include only requested fields
|
||||
if self._include_fields:
|
||||
for field_name in set(fields) - set(self._include_fields):
|
||||
fields.pop(field_name, None)
|
||||
|
||||
# Remove omitted fields
|
||||
for field_name in set(self._omit_fields):
|
||||
fields.pop(field_name, None)
|
||||
if not self._requested_fields:
|
||||
return super().fields
|
||||
|
||||
fields = BindingDict(self)
|
||||
for key, value in self.get_fields().items():
|
||||
if key in self._requested_fields:
|
||||
fields[key] = value
|
||||
return fields
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
|
||||
@@ -5,13 +5,13 @@ from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
||||
from django.db import router, transaction
|
||||
from django.db.models import ProtectedError, RestrictedError
|
||||
from django_pglocks import advisory_lock
|
||||
from netbox.constants import ADVISORY_LOCK_KEYS
|
||||
from rest_framework import mixins as drf_mixins
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
|
||||
from netbox.api.serializers.features import ChangeLogMessageSerializer
|
||||
from netbox.constants import ADVISORY_LOCK_KEYS
|
||||
from utilities.api import get_annotations_for_serializer, get_prefetches_for_serializer
|
||||
from utilities.exceptions import AbortRequest
|
||||
from utilities.query import reapply_model_ordering
|
||||
@@ -59,38 +59,33 @@ class BaseViewSet(GenericViewSet):
|
||||
serializer_class = self.get_serializer_class()
|
||||
|
||||
# Dynamically resolve prefetches for included serializer fields and attach them to the queryset
|
||||
if prefetch := get_prefetches_for_serializer(serializer_class, **self.field_kwargs):
|
||||
if prefetch := get_prefetches_for_serializer(serializer_class, fields_to_include=self.requested_fields):
|
||||
qs = qs.prefetch_related(*prefetch)
|
||||
|
||||
# Dynamically resolve annotations for RelatedObjectCountFields on the serializer and attach them to the queryset
|
||||
if annotations := get_annotations_for_serializer(serializer_class, **self.field_kwargs):
|
||||
if annotations := get_annotations_for_serializer(serializer_class, fields_to_include=self.requested_fields):
|
||||
qs = qs.annotate(**annotations)
|
||||
|
||||
return qs
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
# Pass the fields/omit kwargs (if specified by the request) to the serializer
|
||||
kwargs.update(**self.field_kwargs)
|
||||
|
||||
# If specific fields have been requested, pass them to the serializer
|
||||
if self.requested_fields:
|
||||
kwargs['fields'] = self.requested_fields
|
||||
|
||||
return super().get_serializer(*args, **kwargs)
|
||||
|
||||
@cached_property
|
||||
def field_kwargs(self):
|
||||
"""Return a dictionary of keyword arguments to be passed when instantiating the serializer."""
|
||||
def requested_fields(self):
|
||||
# An explicit list of fields was requested
|
||||
if requested_fields := self.request.query_params.get('fields'):
|
||||
return {'fields': requested_fields.split(',')}
|
||||
|
||||
# An explicit list of fields to omit was requested
|
||||
if omit_fields := self.request.query_params.get('omit'):
|
||||
return {'omit': omit_fields.split(',')}
|
||||
|
||||
return requested_fields.split(',')
|
||||
# Brief mode has been enabled for this request
|
||||
if self.brief:
|
||||
elif self.brief:
|
||||
serializer_class = self.get_serializer_class()
|
||||
if brief_fields := getattr(serializer_class.Meta, 'brief_fields', None):
|
||||
return {'fields': brief_fields}
|
||||
|
||||
return {}
|
||||
return getattr(serializer_class.Meta, 'brief_fields', None)
|
||||
return None
|
||||
|
||||
|
||||
class NetBoxReadOnlyModelViewSet(
|
||||
|
||||
@@ -3,10 +3,8 @@ 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,7 +1,6 @@
|
||||
from collections import defaultdict
|
||||
from contextlib import contextmanager
|
||||
|
||||
from netbox.context import current_request, events_queue, query_cache
|
||||
from netbox.context import current_request, events_queue
|
||||
from netbox.utils import register_request_processor
|
||||
from extras.events import flush_events
|
||||
|
||||
@@ -17,7 +16,6 @@ def event_tracking(request):
|
||||
"""
|
||||
current_request.set(request)
|
||||
events_queue.set({})
|
||||
query_cache.set(defaultdict(dict))
|
||||
|
||||
yield
|
||||
|
||||
@@ -28,4 +26,3 @@ def event_tracking(request):
|
||||
# Clear context vars
|
||||
current_request.set(None)
|
||||
events_queue.set({})
|
||||
query_cache.set(None)
|
||||
|
||||
@@ -3,9 +3,10 @@ from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from extras.choices import *
|
||||
from utilities.forms.fields import QueryField
|
||||
from users.models import Owner
|
||||
from utilities.forms.fields import DynamicModelChoiceField, QueryField
|
||||
from utilities.forms.mixins import FilterModifierMixin
|
||||
from .mixins import CustomFieldsMixin, OwnerFilterMixin, SavedFiltersMixin
|
||||
from .mixins import CustomFieldsMixin, SavedFiltersMixin
|
||||
|
||||
__all__ = (
|
||||
'NestedGroupModelFilterSetForm',
|
||||
@@ -46,6 +47,14 @@ 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,14 +4,13 @@ from django.utils.translation import gettext as _
|
||||
from core.models import ObjectType
|
||||
from extras.choices import *
|
||||
from extras.models import *
|
||||
from users.models import OwnerGroup, Owner
|
||||
from users.models import Owner
|
||||
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
||||
|
||||
__all__ = (
|
||||
'ChangelogMessageMixin',
|
||||
'CustomFieldsMixin',
|
||||
'OwnerMixin',
|
||||
'OwnerFilterMixin',
|
||||
'SavedFiltersMixin',
|
||||
'TagsMixin',
|
||||
)
|
||||
@@ -23,7 +22,7 @@ class ChangelogMessageMixin(forms.Form):
|
||||
"""
|
||||
changelog_message = forms.CharField(
|
||||
required=False,
|
||||
max_length=200,
|
||||
max_length=200
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -43,7 +42,6 @@ class CustomFieldsMixin:
|
||||
Attributes:
|
||||
model: The model class
|
||||
"""
|
||||
|
||||
model = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -88,20 +86,13 @@ 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):
|
||||
@@ -116,13 +107,6 @@ 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,
|
||||
@@ -140,47 +124,10 @@ class TagsMixin(forms.Form):
|
||||
|
||||
class OwnerMixin(forms.Form):
|
||||
"""
|
||||
Mixin for forms which adds ownership fields.
|
||||
|
||||
Include this mixin in forms for models which
|
||||
support owner and/or owner group assignment.
|
||||
Add an `owner` field to forms for models which support Owner 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'),
|
||||
)
|
||||
|
||||
@@ -569,6 +569,7 @@ class SyncedDataMixin(models.Model):
|
||||
)
|
||||
else:
|
||||
AutoSyncRecord.objects.filter(
|
||||
datafile=self.data_file,
|
||||
object_type=object_type,
|
||||
object_id=self.pk
|
||||
).delete()
|
||||
@@ -581,6 +582,7 @@ class SyncedDataMixin(models.Model):
|
||||
# Delete AutoSyncRecord
|
||||
object_type = ObjectType.objects.get_for_model(self)
|
||||
AutoSyncRecord.objects.filter(
|
||||
datafile=self.data_file,
|
||||
object_type=object_type,
|
||||
object_id=self.pk
|
||||
).delete()
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from functools import cache
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.registry import registry
|
||||
@@ -411,10 +409,60 @@ ADMIN_MENU = Menu(
|
||||
MenuGroup(
|
||||
label=_('Authentication'),
|
||||
items=(
|
||||
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']),
|
||||
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'])
|
||||
),
|
||||
),
|
||||
),
|
||||
MenuGroup(
|
||||
@@ -453,49 +501,40 @@ 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,
|
||||
]
|
||||
|
||||
@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,
|
||||
# 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 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
|
||||
# Add the admin menu last
|
||||
MENUS.append(ADMIN_MENU)
|
||||
|
||||
@@ -271,14 +271,9 @@ 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'),
|
||||
@@ -286,14 +281,9 @@ 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'),
|
||||
@@ -301,14 +291,9 @@ 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'),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import re
|
||||
from collections import namedtuple
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
@@ -29,8 +28,6 @@ __all__ = (
|
||||
'SearchView',
|
||||
)
|
||||
|
||||
logger = logging.getLogger(f'netbox.{__name__}')
|
||||
|
||||
Link = namedtuple('Link', ('label', 'viewname', 'permission', 'count'))
|
||||
|
||||
|
||||
@@ -53,14 +50,7 @@ class HomeView(ConditionalLoginRequiredMixin, View):
|
||||
# Check whether a new release is available. (Only for superusers.)
|
||||
new_release = None
|
||||
if request.user.is_superuser:
|
||||
# cache.get() can raise an exception if the cached value can't be unpickled after dependency upgrades
|
||||
try:
|
||||
latest_release = cache.get('latest_release')
|
||||
except Exception:
|
||||
logger.debug("Failed to read 'latest_release' from cache; deleting key", exc_info=True)
|
||||
cache.delete('latest_release')
|
||||
latest_release = None
|
||||
|
||||
latest_release = cache.get('latest_release')
|
||||
if latest_release:
|
||||
release_version, release_url = latest_release
|
||||
if release_version > version.parse(settings.RELEASE.version):
|
||||
|
||||
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
@@ -1,40 +0,0 @@
|
||||
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,10 +1,9 @@
|
||||
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, initClearField]) {
|
||||
for (const func of [initFormElements, initSpeedSelector, initFilterModifiers]) {
|
||||
func();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,10 +59,6 @@
|
||||
<th scope="row">{% trans "Completed" %}</th>
|
||||
<td>{{ object.completed|isodatetime|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Queue" %}</th>
|
||||
<td>{{ object.queue_name|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -101,9 +101,8 @@
|
||||
|
||||
<div class="field-group mb-5">
|
||||
<div class="row">
|
||||
<h2 class="col-9 offset-3">{% trans "Ownership" %}</h2>
|
||||
<h2 class="col-9 offset-3">{% trans "Owner" %}</h2>
|
||||
</div>
|
||||
{% render_field form.owner_group %}
|
||||
{% render_field form.owner %}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -80,9 +80,8 @@
|
||||
|
||||
<div class="field-group mb-5">
|
||||
<div class="row">
|
||||
<h2 class="col-9 offset-3">{% trans "Ownership" %}</h2>
|
||||
<h2 class="col-9 offset-3">{% trans "Owner" %}</h2>
|
||||
</div>
|
||||
{% render_field form.owner_group %}
|
||||
{% render_field form.owner %}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -370,6 +370,33 @@
|
||||
</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 %}
|
||||
@@ -414,13 +441,6 @@
|
||||
{% 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,9 +36,8 @@
|
||||
|
||||
<div class="field-group mb-5">
|
||||
<div class="row">
|
||||
<h2 class="col-9 offset-3">{% trans "Ownership" %}</h2>
|
||||
<h2 class="col-9 offset-3">{% trans "Owner" %}</h2>
|
||||
</div>
|
||||
{% render_field vc_form.owner_group %}
|
||||
{% render_field vc_form.owner %}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -121,7 +121,6 @@
|
||||
<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,11 +62,8 @@ Context:
|
||||
{% if form.owner %}
|
||||
<div class="field-group mb-5">
|
||||
<div class="row">
|
||||
<h2 class="col-9 offset-3">{% trans "Ownership" %}</h2>
|
||||
<h2 class="col-9 offset-3">{% trans "Owner" %}</h2>
|
||||
</div>
|
||||
{% if form.owner_group %}
|
||||
{% render_field form.owner_group %}
|
||||
{% endif %}
|
||||
{% render_field form.owner bulk_nullable=True %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -27,9 +27,6 @@
|
||||
<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 %}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
{% include 'ipam/inc/max_depth.html' %}
|
||||
{% include 'ipam/inc/max_length.html' %}
|
||||
{% if perms.ipam.add_prefix and first_available_prefix %}
|
||||
<a href="{% url 'ipam:prefix_add' %}?prefix={{ first_available_prefix }}{% if object.vrf %}&vrf={{ object.vrf.pk }}{% endif %}{% if object.scope %}&scope_type={{ object.scope_type.pk }}&scope={{ object.scope.pk }}{% endif %}{% if object.tenant %}&tenant_group={{ object.tenant.group.pk }}&tenant={{ object.tenant.pk }}{% endif %}" class="btn btn-primary">
|
||||
<a href="{% url 'ipam:prefix_add' %}?prefix={{ first_available_prefix }}&vrf={{ object.vrf.pk }}&site={{ object.site.pk }}&tenant_group={{ object.tenant.group.pk }}&tenant={{ object.tenant.pk }}" class="btn btn-primary">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Prefix" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
@@ -67,9 +67,8 @@
|
||||
|
||||
<div class="field-group mb-5">
|
||||
<div class="row">
|
||||
<h2 class="col-9 offset-3">{% trans "Ownership" %}</h2>
|
||||
<h2 class="col-9 offset-3">{% trans "Owner" %}</h2>
|
||||
</div>
|
||||
{% render_field form.owner_group %}
|
||||
{% render_field form.owner %}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -31,9 +31,8 @@ __all__ = (
|
||||
class TenantGroupFilterForm(NestedGroupModelFilterSetForm):
|
||||
model = TenantGroup
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('parent_id', name=_('Tenant Group')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
parent_id = DynamicModelMultipleChoiceField(
|
||||
queryset=TenantGroup.objects.all(),
|
||||
@@ -46,9 +45,8 @@ class TenantGroupFilterForm(NestedGroupModelFilterSetForm):
|
||||
class TenantFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||
model = Tenant
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('group_id', name=_('Tenant')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts'))
|
||||
)
|
||||
group_id = DynamicModelMultipleChoiceField(
|
||||
@@ -67,9 +65,8 @@ class TenantFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||
class ContactGroupFilterForm(NestedGroupModelFilterSetForm):
|
||||
model = ContactGroup
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('parent_id', name=_('Contact Group')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
parent_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ContactGroup.objects.all(),
|
||||
@@ -82,8 +79,7 @@ class ContactGroupFilterForm(NestedGroupModelFilterSetForm):
|
||||
class ContactRoleFilterForm(OrganizationalModelFilterSetForm):
|
||||
model = ContactRole
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -91,9 +87,8 @@ class ContactRoleFilterForm(OrganizationalModelFilterSetForm):
|
||||
class ContactFilterForm(PrimaryModelFilterSetForm):
|
||||
model = Contact
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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 OwnerGroup, Owner
|
||||
from users.models import Owner
|
||||
|
||||
__all__ = (
|
||||
'OwnerFilterMixin',
|
||||
@@ -12,17 +12,6 @@ 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)'),
|
||||
|
||||
@@ -93,23 +93,18 @@ def get_view_name(view):
|
||||
return drf_get_view_name(view)
|
||||
|
||||
|
||||
def get_prefetches_for_serializer(serializer_class, fields=None, omit=None):
|
||||
def get_prefetches_for_serializer(serializer_class, fields_to_include=None):
|
||||
"""
|
||||
Compile and return a list of fields which should be prefetched on the queryset for a serializer.
|
||||
"""
|
||||
if fields is not None and omit is not None:
|
||||
raise TypeError("Cannot specify both 'fields' and 'omit' parameters.")
|
||||
|
||||
model = serializer_class.Meta.model
|
||||
|
||||
# If fields are not specified, default to all
|
||||
fields_to_include = fields or serializer_class.Meta.fields
|
||||
fields_to_omit = omit or []
|
||||
if not fields_to_include:
|
||||
fields_to_include = serializer_class.Meta.fields
|
||||
|
||||
prefetch_fields = []
|
||||
for field_name in fields_to_include:
|
||||
if field_name in fields_to_omit:
|
||||
continue
|
||||
serializer_field = serializer_class._declared_fields.get(field_name)
|
||||
|
||||
# Determine the name of the model field referenced by the serializer field
|
||||
@@ -137,23 +132,19 @@ def get_prefetches_for_serializer(serializer_class, fields=None, omit=None):
|
||||
return prefetch_fields
|
||||
|
||||
|
||||
def get_annotations_for_serializer(serializer_class, fields=None, omit=None):
|
||||
def get_annotations_for_serializer(serializer_class, fields_to_include=None):
|
||||
"""
|
||||
Return a mapping of field names to annotations to be applied to the queryset for a serializer.
|
||||
"""
|
||||
if fields is not None and omit is not None:
|
||||
raise TypeError("Cannot specify both 'fields' and 'omit' parameters.")
|
||||
annotations = {}
|
||||
|
||||
# If specific fields are not specified, default to all
|
||||
if not fields_to_include:
|
||||
fields_to_include = serializer_class.Meta.fields
|
||||
|
||||
model = serializer_class.Meta.model
|
||||
|
||||
# If fields are not specified, default to all
|
||||
fields_to_include = fields or serializer_class.Meta.fields
|
||||
fields_to_omit = omit or []
|
||||
|
||||
annotations = {}
|
||||
for field_name, field in serializer_class._declared_fields.items():
|
||||
if field_name in fields_to_omit:
|
||||
continue
|
||||
if field_name in fields_to_include and type(field) is RelatedObjectCountField:
|
||||
related_field = getattr(model, field.relation).field
|
||||
annotations[field_name] = count_related(related_field.model, related_field.name)
|
||||
|
||||
@@ -5,7 +5,6 @@ from ..utils import add_blank_choice
|
||||
|
||||
__all__ = (
|
||||
'BulkEditNullBooleanSelect',
|
||||
'ClearableSelect',
|
||||
'ColorSelect',
|
||||
'HTMXSelect',
|
||||
'SelectWithPK',
|
||||
@@ -29,21 +28,6 @@ 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>.
|
||||
|
||||
@@ -252,16 +252,3 @@ def isodatetime(value, spec='seconds'):
|
||||
else:
|
||||
return ''
|
||||
return mark_safe(f'<span title="{naturaltime(value)}">{text}</span>')
|
||||
|
||||
|
||||
@register.filter
|
||||
def truncate_middle(value, length):
|
||||
if len(value) <= length:
|
||||
return value
|
||||
|
||||
# Calculate split points for the two parts
|
||||
half_len = (length - 1) // 2 # 1 for the ellipsis
|
||||
first_part = value[:half_len]
|
||||
second_part = value[len(value) - (length - 1 - half_len):]
|
||||
|
||||
return mark_safe(f"{first_part}…{second_part}")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from django import template
|
||||
|
||||
from netbox.navigation.menu import get_menus
|
||||
from netbox.navigation.menu import 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 get_menus():
|
||||
for menu in 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 DynamicModelMultipleChoiceField, TagFilterField
|
||||
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from virtualization.choices import *
|
||||
from virtualization.models import *
|
||||
@@ -29,8 +29,7 @@ __all__ = (
|
||||
class ClusterTypeFilterForm(OrganizationalModelFilterSetForm):
|
||||
model = ClusterType
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -39,8 +38,7 @@ class ClusterGroupFilterForm(ContactModelFilterForm, OrganizationalModelFilterSe
|
||||
model = ClusterGroup
|
||||
tag = TagFilterField(model)
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
)
|
||||
|
||||
@@ -48,11 +46,10 @@ class ClusterGroupFilterForm(ContactModelFilterForm, OrganizationalModelFilterSe
|
||||
class ClusterFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||
model = Cluster
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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')
|
||||
@@ -108,7 +105,7 @@ class VirtualMachineFilterForm(
|
||||
):
|
||||
model = VirtualMachine
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('cluster_group_id', 'cluster_type_id', 'cluster_id', 'device_id', name=_('Cluster')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
|
||||
FieldSet(
|
||||
@@ -116,7 +113,6 @@ 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(
|
||||
@@ -209,15 +205,14 @@ class VirtualMachineFilterForm(
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class VMInterfaceFilterForm(OwnerFilterMixin, NetBoxModelFilterSetForm):
|
||||
class VMInterfaceFilterForm(NetBoxModelFilterSetForm):
|
||||
model = VMInterface
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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(
|
||||
@@ -264,16 +259,20 @@ class VMInterfaceFilterForm(OwnerFilterMixin, NetBoxModelFilterSetForm):
|
||||
required=False,
|
||||
label=_('VLAN Translation Policy')
|
||||
)
|
||||
owner_id = DynamicModelChoiceField(
|
||||
queryset=Owner.objects.all(),
|
||||
required=False,
|
||||
label=_('Owner'),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class VirtualDiskFilterForm(OwnerFilterMixin, NetBoxModelFilterSetForm):
|
||||
class VirtualDiskFilterForm(NetBoxModelFilterSetForm):
|
||||
model = VirtualDisk
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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(),
|
||||
@@ -285,4 +284,9 @@ class VirtualDiskFilterForm(OwnerFilterMixin, NetBoxModelFilterSetForm):
|
||||
required=False,
|
||||
min_value=1
|
||||
)
|
||||
owner_id = DynamicModelChoiceField(
|
||||
queryset=Owner.objects.all(),
|
||||
required=False,
|
||||
label=_('Owner'),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -33,8 +33,7 @@ __all__ = (
|
||||
class TunnelGroupFilterForm(ContactModelFilterForm, OrganizationalModelFilterSetForm):
|
||||
model = TunnelGroup
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
@@ -43,11 +42,10 @@ class TunnelGroupFilterForm(ContactModelFilterForm, OrganizationalModelFilterSet
|
||||
class TunnelFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = Tunnel
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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(
|
||||
@@ -99,11 +97,10 @@ class TunnelTerminationFilterForm(NetBoxModelFilterSetForm):
|
||||
class IKEProposalFilterForm(PrimaryModelFilterSetForm):
|
||||
model = IKEProposal
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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'),
|
||||
@@ -131,9 +128,8 @@ class IKEProposalFilterForm(PrimaryModelFilterSetForm):
|
||||
class IKEPolicyFilterForm(PrimaryModelFilterSetForm):
|
||||
model = IKEPolicy
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('version', 'mode', 'proposal_id', name=_('Parameters')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
version = forms.MultipleChoiceField(
|
||||
label=_('IKE version'),
|
||||
@@ -156,9 +152,8 @@ class IKEPolicyFilterForm(PrimaryModelFilterSetForm):
|
||||
class IPSecProposalFilterForm(PrimaryModelFilterSetForm):
|
||||
model = IPSecProposal
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('encryption_algorithm', 'authentication_algorithm', name=_('Parameters')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
encryption_algorithm = forms.MultipleChoiceField(
|
||||
label=_('Encryption algorithm'),
|
||||
@@ -176,9 +171,8 @@ class IPSecProposalFilterForm(PrimaryModelFilterSetForm):
|
||||
class IPSecPolicyFilterForm(PrimaryModelFilterSetForm):
|
||||
model = IPSecPolicy
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('proposal_id', 'pfs_group', name=_('Parameters')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
proposal_id = DynamicModelMultipleChoiceField(
|
||||
queryset=IKEProposal.objects.all(),
|
||||
@@ -196,9 +190,8 @@ class IPSecPolicyFilterForm(PrimaryModelFilterSetForm):
|
||||
class IPSecProfileFilterForm(PrimaryModelFilterSetForm):
|
||||
model = IPSecProfile
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('mode', 'ike_policy_id', 'ipsec_policy_id', name=_('Profile')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
mode = forms.MultipleChoiceField(
|
||||
label=_('Mode'),
|
||||
@@ -221,10 +214,9 @@ class IPSecProfileFilterForm(PrimaryModelFilterSetForm):
|
||||
class L2VPNFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = L2VPN
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
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,9 +22,8 @@ __all__ = (
|
||||
class WirelessLANGroupFilterForm(NestedGroupModelFilterSetForm):
|
||||
model = WirelessLANGroup
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('parent_id', name=_('Wireless LAN group')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
parent_id = DynamicModelMultipleChoiceField(
|
||||
queryset=WirelessLANGroup.objects.all(),
|
||||
@@ -37,12 +36,11 @@ class WirelessLANGroupFilterForm(NestedGroupModelFilterSetForm):
|
||||
class WirelessLANFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = WirelessLAN
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('ssid', 'group_id', 'status', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Scope')),
|
||||
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')),
|
||||
FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')),
|
||||
)
|
||||
ssid = forms.CharField(
|
||||
required=False,
|
||||
@@ -104,11 +102,10 @@ class WirelessLANFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
class WirelessLinkFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = WirelessLink
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('ssid', 'status', 'distance', 'distance_unit', name=_('Attributes')),
|
||||
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')),
|
||||
FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')),
|
||||
)
|
||||
ssid = forms.CharField(
|
||||
required=False,
|
||||
|
||||
Reference in New Issue
Block a user