mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-27 10:58:37 -06:00
Merge in latest feature
This commit is contained in:
commit
a9aa0cbaa0
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -1,7 +1,7 @@
|
||||
---
|
||||
name: 🐛 Bug Report
|
||||
description: Report a reproducible bug in the current release of NetBox
|
||||
labels: ["type: bug"]
|
||||
labels: ["type: bug", "needs triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
@ -1,7 +1,7 @@
|
||||
---
|
||||
name: 📖 Documentation Change
|
||||
description: Suggest an addition or modification to the NetBox documentation
|
||||
labels: ["type: documentation"]
|
||||
labels: ["type: documentation", "needs triage"]
|
||||
body:
|
||||
- type: dropdown
|
||||
attributes:
|
||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -1,7 +1,7 @@
|
||||
---
|
||||
name: ✨ Feature Request
|
||||
description: Propose a new NetBox feature or enhancement
|
||||
labels: ["type: feature"]
|
||||
labels: ["type: feature", "needs triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
20
.github/workflows/auto-assign-issue.yml
vendored
Normal file
20
.github/workflows/auto-assign-issue.yml
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
# auto-assign-issue (https://github.com/marketplace/actions/auto-assign-issue)
|
||||
name: Issue assignment
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
auto-assign:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: pozil/auto-assign-issue@v1
|
||||
if: "contains(github.event.issue.labels.*.name, 'type: bug') || contains(github.event.issue.labels.*.name, 'type: feature')"
|
||||
with:
|
||||
assignees: abhi1693,arthanson,DanSheps,jeffgdotorg,jeremystretch
|
||||
numOfAssignee: 1
|
||||
abortIfPreviousAssignees: true
|
@ -1,5 +1,5 @@
|
||||
# close-stale-issues (https://github.com/marketplace/actions/close-stale-issues)
|
||||
name: 'Close stale issues/PRs'
|
||||
name: Close stale issues/PRs
|
||||
|
||||
on:
|
||||
schedule:
|
||||
@ -12,10 +12,9 @@ permissions:
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v8
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
close-issue-message: >
|
||||
This issue has been automatically closed due to lack of activity. In an
|
@ -1,5 +1,5 @@
|
||||
# lock-threads (https://github.com/marketplace/actions/lock-threads)
|
||||
name: 'Lock threads'
|
||||
name: Lock threads
|
||||
|
||||
on:
|
||||
schedule:
|
@ -1,8 +1,8 @@
|
||||
version: 2
|
||||
build:
|
||||
os: ubuntu-20.04
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3.9"
|
||||
python: "3.12"
|
||||
mkdocs:
|
||||
configuration: mkdocs.yml
|
||||
python:
|
||||
|
@ -84,7 +84,7 @@ NetBox automatically logs the creation, modification, and deletion of all manage
|
||||
|
||||
<p align="center">
|
||||
<a href="https://netboxlabs.com/netbox-cloud/"><img src="docs/media/misc/netbox_cloud.png" alt="NetBox Cloud" /></a><br />
|
||||
Looking for an enterprise solution? Check out <strong><a href="https://netboxlabs.com/netbox-cloud/">NetBox Cloud</a></strong>!
|
||||
Looking for a managed solution? Check out <strong><a href="https://netboxlabs.com/netbox-cloud/">NetBox Cloud</a></strong> or <strong><a href="https://netboxlabs.com/netbox-enterprise/">NetBox Enterprise</a></strong>!
|
||||
</p>
|
||||
|
||||
## Get Involved
|
||||
|
@ -14,18 +14,13 @@ django-debug-toolbar
|
||||
# https://github.com/carltongibson/django-filter/blob/main/CHANGES.rst
|
||||
django-filter
|
||||
|
||||
# Django debug toolbar extension with support for GraphiQL
|
||||
# https://github.com/flavors/django-graphiql-debug-toolbar/blob/main/CHANGES.rst
|
||||
django-graphiql-debug-toolbar
|
||||
|
||||
# HTMX utilities for Django
|
||||
# https://django-htmx.readthedocs.io/en/latest/changelog.html
|
||||
django-htmx
|
||||
|
||||
# Modified Preorder Tree Traversal (recursive nesting of objects)
|
||||
# Pinned to 0.14.0; 0.15.0 requires Python 3.9+
|
||||
# https://github.com/django-mptt/django-mptt/blob/main/CHANGELOG.rst
|
||||
django-mptt==0.14.0
|
||||
django-mptt
|
||||
|
||||
# Context managers for PostgreSQL advisory locks
|
||||
# https://github.com/Xof/django-pglocks/blob/master/CHANGES.txt
|
||||
@ -75,11 +70,6 @@ drf-spectacular-sidecar
|
||||
# https://github.com/kurtmckee/feedparser/blob/develop/CHANGELOG.rst
|
||||
feedparser
|
||||
|
||||
# Django wrapper for Graphene (GraphQL support)
|
||||
# https://github.com/graphql-python/graphene-django/releases
|
||||
# Pinned to v3.0.0 for GraphiQL UI issue (see #12762)
|
||||
graphene_django==3.0.0
|
||||
|
||||
# WSGI HTTP server
|
||||
# https://docs.gunicorn.org/en/latest/news.html
|
||||
gunicorn
|
||||
@ -136,8 +126,17 @@ social-auth-core
|
||||
# https://github.com/python-social-auth/social-app-django/blob/master/CHANGELOG.md
|
||||
social-auth-app-django
|
||||
|
||||
# Strawberry GraphQL
|
||||
# https://github.com/strawberry-graphql/strawberry/blob/main/CHANGELOG.md
|
||||
strawberry-graphql
|
||||
|
||||
# Strawberry GraphQL Django extension
|
||||
# https://github.com/strawberry-graphql/strawberry-django/blob/main/CHANGELOG.md
|
||||
# Pinned per #15574
|
||||
strawberry-graphql-django==0.34.0
|
||||
|
||||
# SVG image rendering (used for rack elevations)
|
||||
# hhttps://github.com/mozman/svgwrite/blob/master/NEWS.rst
|
||||
# https://github.com/mozman/svgwrite/blob/master/NEWS.rst
|
||||
svgwrite
|
||||
|
||||
# Tabular dataset library (for table-based exports)
|
||||
|
@ -1,5 +1,7 @@
|
||||
{
|
||||
"type": "object",
|
||||
"$id": "urn:devicetype-library:generated-schema",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"airflow": {
|
||||
|
@ -12,8 +12,12 @@ Group=netbox
|
||||
PIDFile=/var/tmp/netbox.pid
|
||||
WorkingDirectory=/opt/netbox
|
||||
|
||||
# Remove the following line if using uWSGI instead of Gunicorn
|
||||
ExecStart=/opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/netbox --config /opt/netbox/gunicorn.py netbox.wsgi
|
||||
|
||||
# Uncomment the following line if using uWSGI instead of Gunicorn
|
||||
#ExecStart=/opt/netbox/venv/bin/uwsgi --ini /opt/netbox/uwsgi.ini
|
||||
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
PrivateTmp=true
|
||||
|
@ -14,10 +14,20 @@ server {
|
||||
}
|
||||
|
||||
location / {
|
||||
# Remove these lines if using uWSGI instead of Gunicorn
|
||||
proxy_pass http://127.0.0.1:8001;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Uncomment these lines if using uWSGI instead of Gunicorn
|
||||
# include uwsgi_params;
|
||||
# uwsgi_pass 127.0.0.1:8001;
|
||||
# uwsgi_param Host $host;
|
||||
# uwsgi_param X-Real-IP $remote_addr;
|
||||
# uwsgi_param X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# uwsgi_param X-Forwarded-Proto $http_x_forwarded_proto;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
18
contrib/uwsgi.ini
Normal file
18
contrib/uwsgi.ini
Normal file
@ -0,0 +1,18 @@
|
||||
[uwsgi]
|
||||
; bind to the specified UNIX/TCP socket and port (usually localhost)
|
||||
socket = 127.0.0.1:8001
|
||||
|
||||
; fail to start if any parameter in the configuration file isn’t explicitly understood by uWSGI.
|
||||
strict = true
|
||||
|
||||
; re-spawn and pre-fork workers
|
||||
master = true
|
||||
|
||||
; clear environment on exit
|
||||
vacuum = true
|
||||
|
||||
; exit if no app can be loaded
|
||||
need-app = true
|
||||
|
||||
; do not use multiple interpreters
|
||||
single-interpreter = true
|
@ -70,8 +70,6 @@ The `$user` token can be used only as a constraint value, or as an item within a
|
||||
|
||||
### Default Permissions
|
||||
|
||||
!!! info "This feature was introduced in NetBox v3.6."
|
||||
|
||||
While permissions are typically assigned to specific groups and/or users, it is also possible to define a set of default permissions that are applied to _all_ authenticated users. This is done using the [`DEFAULT_PERMISSIONS`](../configuration/security.md#default_permissions) configuration parameter. Note that statically configuring permissions for specific users or groups is **not** supported.
|
||||
|
||||
### Example Constraint Definitions
|
||||
|
@ -92,8 +92,6 @@ CSRF_TRUSTED_ORIGINS = (
|
||||
|
||||
## DEFAULT_PERMISSIONS
|
||||
|
||||
!!! info "This parameter was introduced in NetBox v3.6."
|
||||
|
||||
Default:
|
||||
|
||||
```python
|
||||
|
@ -4,7 +4,7 @@ NetBox validates every object prior to it being written to the database to ensur
|
||||
|
||||
## Custom Validation Rules
|
||||
|
||||
Custom validation rules are expressed as a mapping of model attributes to a set of rules to which that attribute must conform. For example:
|
||||
Custom validation rules are expressed as a mapping of object attributes to a set of rules to which that attribute must conform. For example:
|
||||
|
||||
```json
|
||||
{
|
||||
@ -17,6 +17,8 @@ Custom validation rules are expressed as a mapping of model attributes to a set
|
||||
|
||||
This defines a custom validator which checks that the length of the `name` attribute for an object is at least five characters long, and no longer than 30 characters. This validation is executed _after_ NetBox has performed its own internal validation.
|
||||
|
||||
### Validation Types
|
||||
|
||||
The `CustomValidator` class supports several validation types:
|
||||
|
||||
* `min`: Minimum value
|
||||
@ -36,14 +38,14 @@ The `min` and `max` types should be defined for numeric values, whereas `min_len
|
||||
|
||||
### Custom Validation Logic
|
||||
|
||||
There may be instances where the provided validation types are insufficient. NetBox provides a `CustomValidator` class which can be extended to enforce arbitrary validation logic by overriding its `validate()` method, and calling `fail()` when an unsatisfactory condition is detected.
|
||||
There may be instances where the provided validation types are insufficient. NetBox provides a `CustomValidator` class which can be extended to enforce arbitrary validation logic by overriding its `validate()` method, and calling `fail()` when an unsatisfactory condition is detected. The `validate()` method should accept an instance (the object being saved) as well as the current request effecting the change.
|
||||
|
||||
```python
|
||||
from extras.validators import CustomValidator
|
||||
|
||||
class MyValidator(CustomValidator):
|
||||
|
||||
def validate(self, instance):
|
||||
def validate(self, instance, request):
|
||||
if instance.status == 'active' and not instance.description:
|
||||
self.fail("Active sites must have a description set!", field='status')
|
||||
```
|
||||
@ -82,7 +84,42 @@ CUSTOM_VALIDATORS = {
|
||||
}
|
||||
```
|
||||
|
||||
### Dotted Path
|
||||
#### Referencing Related Object Attributes
|
||||
|
||||
!!! info "This feature was introduced in NetBox v4.0."
|
||||
|
||||
The attributes of a related object can be referenced by specifying a dotted path. For example, to reference the name of a region to which a site is assigned, use `region.name`:
|
||||
|
||||
```python
|
||||
CUSTOM_VALIDATORS = {
|
||||
"dcim.site": [
|
||||
{
|
||||
"region.name": {
|
||||
"neq": "New York"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Validating Request Parameters
|
||||
|
||||
!!! info "This feature was introduced in NetBox v4.0."
|
||||
|
||||
In addition to validating object attributes, custom validators can also match against parameters of the current request (where available). For example, the following rule will permit only the user named "admin" to modify an object:
|
||||
|
||||
```json
|
||||
{
|
||||
"request.user.username": {
|
||||
"eq": "admin"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
Custom validation should generally not be used to enforce permissions. NetBox provides a robust [object-based permissions](../administration/permissions.md) mechanism which should be used for this purpose.
|
||||
|
||||
### Dotted Path to Class
|
||||
|
||||
In instances where a custom validator class is needed, it can be referenced by its Python path (relative to NetBox's working directory):
|
||||
|
||||
|
@ -20,8 +20,6 @@ GET /api/dcim/devices/?tag=monitored&tag=deprecated
|
||||
|
||||
## Bookmarks
|
||||
|
||||
!!! info "This feature was introduced in NetBox v3.6."
|
||||
|
||||
Users can bookmark their most commonly visited objects for convenient access. Bookmarks are listed under a user's profile, and can be displayed with custom filtering and ordering on the user's personal dashboard.
|
||||
|
||||
## Custom Fields
|
||||
|
@ -1,10 +1,13 @@
|
||||
# Gunicorn
|
||||
|
||||
Like most Django applications, NetBox runs as a [WSGI application](https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface) behind an HTTP server. This documentation shows how to install and configure [gunicorn](http://gunicorn.org/) (which is automatically installed with NetBox) for this role, however other WSGI servers are available and should work similarly well. [uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/) is a popular alternative.
|
||||
!!! tip
|
||||
This page provides instructions for setting up the [gunicorn](http://gunicorn.org/) WSGI server. If you plan to use [uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/) instead, go [here](./4b-uwsgi.md).
|
||||
|
||||
NetBox runs as a [WSGI application](https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface) behind an HTTP server. This documentation shows how to install and configure [gunicorn](http://gunicorn.org/) (which is automatically installed with NetBox) for this role, however other WSGI servers are available and should work similarly well.
|
||||
|
||||
## Configuration
|
||||
|
||||
NetBox ships with a default configuration file for gunicorn. To use it, copy `/opt/netbox/contrib/gunicorn.py` to `/opt/netbox/gunicorn.py`. (We make a copy of this file rather than pointing to it directly to ensure that any local changes to it do not get overwritten by a future upgrade.)
|
||||
NetBox ships with a default configuration file for gunicorn. To use it, copy `/opt/netbox/contrib/gunicorn.py` to `/opt/netbox/gunicorn.py`. (We make a copy of this file rather than pointing to it directly to ensure that any local changes to it do not get overwritten during a future NetBox upgrade.)
|
||||
|
||||
```no-highlight
|
||||
sudo cp /opt/netbox/contrib/gunicorn.py /opt/netbox/gunicorn.py
|
104
docs/installation/4b-uwsgi.md
Normal file
104
docs/installation/4b-uwsgi.md
Normal file
@ -0,0 +1,104 @@
|
||||
# uWSGI
|
||||
|
||||
!!! tip
|
||||
This page provides instructions for setting up the [uWSGI](https://uwsgi-docs.readthedocs.io/) WSGI server. If you plan to use [gunicorn](http://gunicorn.org/) instead, go [here](./4a-gunicorn.md).
|
||||
|
||||
NetBox runs as a [WSGI application](https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface) behind an HTTP server. This documentation shows how to install and configure [uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/) for this role, however other WSGI servers are available and should work similarly well.
|
||||
|
||||
## Installation
|
||||
|
||||
Activate the Python virtual environment and install the `pyuwsgi` package using pip:
|
||||
|
||||
```no-highlight
|
||||
source /opt/netbox/venv/bin/activate
|
||||
pip3 install pyuwsgi
|
||||
```
|
||||
|
||||
Once installed, add the package to `local_requirements.txt` to ensure it is re-installed during future rebuilds of the virtual environment:
|
||||
|
||||
```no-highlight
|
||||
sudo sh -c "echo 'pyuwgsi' >> /opt/netbox/local_requirements.txt"
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
NetBox ships with a default configuration file for uWSGI. To use it, copy `/opt/netbox/contrib/uwsgi.ini` to `/opt/netbox/uwsgi.ini`. (We make a copy of this file rather than pointing to it directly to ensure that any local changes to it do not get overwritten during a future NetBox upgrade.)
|
||||
|
||||
```no-highlight
|
||||
sudo cp /opt/netbox/contrib/uwsgi.ini /opt/netbox/uwsgi.ini
|
||||
```
|
||||
|
||||
While the provided configuration should suffice for most initial installations, you may wish to edit this file to change the bound IP address and/or port number, or to make performance-related adjustments. See [the uWSGI documentation](https://uwsgi-docs-additions.readthedocs.io/en/latest/Options.html) for the available configuration parameters and take a minute to review the [Things to know](https://uwsgi-docs.readthedocs.io/en/latest/ThingsToKnow.html) page. Django also provides [additional documentation](https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/uwsgi/) on configuring uWSGI with a Django app.
|
||||
|
||||
## systemd Setup
|
||||
|
||||
We'll use systemd to control both uWSGI and NetBox's background worker process. First, copy `contrib/netbox.service` and `contrib/netbox-rq.service` to the `/etc/systemd/system/` directory.
|
||||
|
||||
```no-highlight
|
||||
sudo cp -v /opt/netbox/contrib/*.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
```
|
||||
|
||||
The reference configuration assumes that gunicorn is in use, so we need to update it. Edit the `netbox.service` file to remove the line beginning with `ExecStart=/opt/netbox/venv/bin/gunicorn` and uncomment the line below it.
|
||||
|
||||
!!! warning "Check user & group assignment"
|
||||
The stock service configuration files packaged with NetBox assume that the service will run with the `netbox` user and group names. If these differ on your installation, be sure to update the service files accordingly.
|
||||
|
||||
Once the configuration file has been saved, reload the service:
|
||||
|
||||
```no-highlight
|
||||
sudo systemctl daemon-reload
|
||||
```
|
||||
|
||||
Then, start the `netbox` and `netbox-rq` services and enable them to initiate at boot time:
|
||||
|
||||
```no-highlight
|
||||
sudo systemctl enable --now netbox netbox-rq
|
||||
```
|
||||
|
||||
You can use the command `systemctl status netbox` to verify that the WSGI service is running:
|
||||
|
||||
```no-highlight
|
||||
systemctl status netbox.service
|
||||
```
|
||||
|
||||
You should see output similar to the following:
|
||||
|
||||
```no-highlight
|
||||
● netbox.service - NetBox WSGI Service
|
||||
Loaded: loaded (/etc/systemd/system/netbox.service; enabled; vendor preset: enabled)
|
||||
Active: active (running) since Mon 2021-08-30 04:02:36 UTC; 14h ago
|
||||
Docs: https://docs.netbox.dev/
|
||||
Main PID: 1140492 (uwsgi)
|
||||
Tasks: 19 (limit: 4683)
|
||||
Memory: 666.2M
|
||||
CGroup: /system.slice/netbox.service
|
||||
├─1061 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/uwsgi --ini /opt/netbox/uwsgi.ini
|
||||
├─1976 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/uwsgi --ini /opt/netbox/uwsgi.ini
|
||||
...
|
||||
```
|
||||
|
||||
!!! note
|
||||
If the NetBox service fails to start, issue the command `journalctl -eu netbox` to check for log messages that may indicate the problem.
|
||||
|
||||
Once you've verified that the WSGI workers are up and running, move on to HTTP server setup.
|
||||
|
||||
## HTTP Server Installation
|
||||
|
||||
For server installation, you will want to follow the NetBox [HTTP Server Setup](5-http-server.md) guide, however after copying the configuration file, you will need to edit the file and change the `location` section to uncomment the uWSGI parameters:
|
||||
|
||||
```no-highlight
|
||||
location / {
|
||||
# proxy_pass http://127.0.0.1:8001;
|
||||
# proxy_set_header X-Forwarded-Host $http_host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# comment the lines above and uncomment the lines below if using uWSGI
|
||||
include uwsgi_params;
|
||||
uwsgi_pass 127.0.0.1:8001;
|
||||
uwsgi_param Host $host;
|
||||
uwsgi_param X-Real-IP $remote_addr;
|
||||
uwsgi_param X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
uwsgi_param X-Forwarded-Proto $http_x_forwarded_proto;
|
||||
}
|
||||
```
|
@ -35,6 +35,9 @@ Once nginx is installed, copy the nginx configuration file provided by NetBox to
|
||||
sudo cp /opt/netbox/contrib/nginx.conf /etc/nginx/sites-available/netbox
|
||||
```
|
||||
|
||||
!!! tip "gunicorn vs. uWSGI"
|
||||
The reference nginx configuration file assumes that gunicorn is in use. If using uWSGI instead, you'll need to remove the gunicorn-specific configuration (lines beginning with `proxy_pass` and `proxy_set_header`) and uncomment the uWSGI section below them before proceeding.
|
||||
|
||||
Then, delete `/etc/nginx/sites-enabled/default` and create a symlink in the `sites-enabled` directory to the configuration file you just created.
|
||||
|
||||
```no-highlight
|
||||
|
@ -54,7 +54,11 @@ For more detail on constructing GraphQL queries, see the [Graphene documentation
|
||||
The GraphQL API employs the same filtering logic as the UI and REST API. Filters can be specified as key-value pairs within parentheses immediately following the query name. For example, the following will return only sites within the North Carolina region with a status of active:
|
||||
|
||||
```
|
||||
{"query": "query {site_list(region:\"north-carolina\", status:\"active\") {name}}"}
|
||||
query {
|
||||
site_list(filters: {region: "us-nc", status: "active"}) {
|
||||
name
|
||||
}
|
||||
}
|
||||
```
|
||||
In addition, filtering can be done on list of related objects as shown in the following query:
|
||||
|
||||
@ -63,7 +67,7 @@ In addition, filtering can be done on list of related objects as shown in the fo
|
||||
device_list {
|
||||
id
|
||||
name
|
||||
interfaces(enabled: true) {
|
||||
interfaces(filters: {enabled: true}) {
|
||||
name
|
||||
}
|
||||
}
|
||||
|
@ -647,18 +647,20 @@ Note that we are _not_ passing an existing REST API token with this request. If
|
||||
{
|
||||
"id": 6,
|
||||
"url": "https://netbox/api/users/tokens/6/",
|
||||
"display": "3c9cb9 (hankhill)",
|
||||
"display": "**********************************3c9cb9",
|
||||
"user": {
|
||||
"id": 2,
|
||||
"url": "https://netbox/api/users/users/2/",
|
||||
"display": "hankhill",
|
||||
"username": "hankhill"
|
||||
},
|
||||
"created": "2021-06-11T20:09:13.339367Z",
|
||||
"created": "2024-03-11T20:09:13.339367Z",
|
||||
"expires": null,
|
||||
"last_used": null,
|
||||
"key": "9fc9b897abec9ada2da6aec9dbc34596293c9cb9",
|
||||
"write_enabled": true,
|
||||
"description": ""
|
||||
"description": "",
|
||||
"allowed_ips": []
|
||||
}
|
||||
```
|
||||
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 15 KiB |
@ -1,7 +1,5 @@
|
||||
# Bookmarks
|
||||
|
||||
!!! info "This feature was introduced in NetBox v3.6."
|
||||
|
||||
A user can bookmark individual objects for convenient access. Bookmarks are listed under a user's profile and can be displayed using a dashboard widget.
|
||||
|
||||
## Fields
|
||||
|
@ -1,7 +1,5 @@
|
||||
# Custom Field Choice Sets
|
||||
|
||||
!!! info "This feature was introduced in NetBox v3.6."
|
||||
|
||||
Single- and multi-selection [custom fields](../../customization/custom-fields.md) must define a set of valid choices from which the user may choose when defining the field value. These choices are defined as sets that may be reused among multiple custom fields.
|
||||
|
||||
A choice set must define a base choice set and/or a set of arbitrary extra choices.
|
||||
|
@ -18,8 +18,6 @@ The color to use when displaying the tag in the NetBox UI.
|
||||
|
||||
### Object Types
|
||||
|
||||
!!! info "This feature was introduced in NetBox v3.6."
|
||||
|
||||
The assignment of a tag may be limited to a prescribed set of objects. For example, it may be desirable to limit the application of a specific tag to only devices and virtual machines.
|
||||
|
||||
If no object types are specified, the tag will be assignable to any type of object.
|
||||
|
@ -8,23 +8,32 @@ A plugin can extend NetBox's GraphQL API by registering its own schema class. By
|
||||
|
||||
```python
|
||||
# graphql.py
|
||||
import graphene
|
||||
from netbox.graphql.types import NetBoxObjectType
|
||||
from netbox.graphql.fields import ObjectField, ObjectListField
|
||||
from . import filtersets, models
|
||||
from typing import List
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
class MyModelType(NetBoxObjectType):
|
||||
from . import models
|
||||
|
||||
class Meta:
|
||||
model = models.MyModel
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.MyModelFilterSet
|
||||
|
||||
class MyQuery(graphene.ObjectType):
|
||||
mymodel = ObjectField(MyModelType)
|
||||
mymodel_list = ObjectListField(MyModelType)
|
||||
@strawberry_django.type(
|
||||
models.MyModel,
|
||||
fields='__all__',
|
||||
)
|
||||
class MyModelType:
|
||||
pass
|
||||
|
||||
schema = MyQuery
|
||||
|
||||
@strawberry.type
|
||||
class MyQuery:
|
||||
@strawberry.field
|
||||
def dummymodel(self, id: int) -> DummyModelType:
|
||||
return None
|
||||
dummymodel_list: List[DummyModelType] = strawberry_django.field()
|
||||
|
||||
|
||||
schema = [
|
||||
MyQuery,
|
||||
]
|
||||
```
|
||||
|
||||
## GraphQL Objects
|
||||
@ -38,15 +47,3 @@ NetBox provides two object type classes for use by plugins.
|
||||
::: netbox.graphql.types.NetBoxObjectType
|
||||
options:
|
||||
members: false
|
||||
|
||||
## GraphQL Fields
|
||||
|
||||
NetBox provides two field classes for use by plugins.
|
||||
|
||||
::: netbox.graphql.fields.ObjectField
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: netbox.graphql.fields.ObjectListField
|
||||
options:
|
||||
members: false
|
||||
|
349
docs/plugins/development/migration-v4.md
Normal file
349
docs/plugins/development/migration-v4.md
Normal file
@ -0,0 +1,349 @@
|
||||
# Migrating Your Plugin to NetBox v4.0
|
||||
|
||||
This document serves as a handbook for maintainers of plugins that were written prior to the release of NetBox v4.0. It serves to capture all the changes recommended to ensure a plugin is compatible with NetBox v4.0 and later releases.
|
||||
|
||||
## General
|
||||
|
||||
### Python support
|
||||
|
||||
NetBox v4.0 drops support for Python 3.8 and 3.9, and introduces support for Python 3.12. You may need to update your CI/CD processes and/or packaging to reflect this.
|
||||
|
||||
### Plugin resources relocated
|
||||
|
||||
All plugin Python resources were moved from `extras.plugins` to `netbox.plugins` in NetBox v3.7 (see [#14036](https://github.com/netbox-community/netbox/issues/14036)), and support for importing these resources from their old locations has been removed.
|
||||
|
||||
```python title="Old"
|
||||
from extras.plugins import PluginConfig
|
||||
```
|
||||
|
||||
```python title="New"
|
||||
from netbox.plugins import PluginConfig
|
||||
```
|
||||
|
||||
### ContentType renamed to ObjectType
|
||||
|
||||
NetBox's proxy model for Django's [ContentType model](https://docs.djangoproject.com/en/5.0/ref/contrib/contenttypes/#the-contenttype-model) has been renamed to ObjectType for clarity. In general, plugins should use the ObjectType proxy when referencing content types, as it includes several custom manager methods. The one exception to this is when defining [generic foreign keys](https://docs.djangoproject.com/en/5.0/ref/contrib/contenttypes/#generic-relations): The ForeignKey field used for a GFK should point to Django's native ContentType.
|
||||
|
||||
Additionally, plugin maintainers are strongly encouraged to adopt the "object type" terminology for field and filter names wherever feasible to be consistent with NetBox core (however this is not required for compatibility).
|
||||
|
||||
```python title="Old"
|
||||
content_types = models.ManyToManyField(
|
||||
to='contenttypes.ContentType',
|
||||
related_name='event_rules'
|
||||
)
|
||||
```
|
||||
|
||||
```python title="New"
|
||||
object_types = models.ManyToManyField(
|
||||
to='core.ObjectType',
|
||||
related_name='event_rules'
|
||||
)
|
||||
```
|
||||
|
||||
## Views
|
||||
|
||||
### View actions must be dictionaries
|
||||
|
||||
The format for declaring view actions & permissions was updated in NetBox v3.7 (see [#13550](https://github.com/netbox-community/netbox/issues/13550)), and NetBox v4.0 drops support for the old format. Views which inherit `ActionsMixin` must declare a single `actions` map.
|
||||
|
||||
```python title="Old"
|
||||
actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete')
|
||||
action_perms = defaultdict(set, **{
|
||||
'add': {'add'},
|
||||
'import': {'add'},
|
||||
'bulk_edit': {'change'},
|
||||
'bulk_delete': {'delete'},
|
||||
})
|
||||
```
|
||||
|
||||
```python title="New"
|
||||
actions = {
|
||||
'add': {'add'},
|
||||
'import': {'add'},
|
||||
'export': set(),
|
||||
'bulk_edit': {'change'},
|
||||
'bulk_delete': {'delete'},
|
||||
}
|
||||
```
|
||||
|
||||
## Forms
|
||||
|
||||
### Remove `BootstrapMixin`
|
||||
|
||||
The `BootstrapMixin` class is no longer available or needed and can be removed from all forms.
|
||||
|
||||
```python title="Old"
|
||||
from django import forms
|
||||
from utilities.forms import BootstrapMixin
|
||||
|
||||
class MyForm(BootstrapMixin, forms.Form):
|
||||
```
|
||||
|
||||
```python title="New"
|
||||
from django import forms
|
||||
|
||||
class MyForm(forms.Form):
|
||||
```
|
||||
|
||||
### Update Fieldset Definitions
|
||||
|
||||
NetBox v4.0 introduces [several new classes](./forms.md#form-rendering) for advanced form rendering, including FieldSet. Fieldset definitions on forms should use this new class instead of a tuple or list.
|
||||
|
||||
Notably, the name of a fieldset is now optional, and passed as a keyword argument rather than as the first item in the set.
|
||||
|
||||
```python title="Old"
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from netbox.forms import NetBoxModelForm
|
||||
|
||||
class CircuitForm(NetBoxModelForm):
|
||||
...
|
||||
fieldsets = (
|
||||
(_('Circuit'), ('cid', 'type', 'status', 'description', 'tags')),
|
||||
(_('Service Parameters'), ('install_date', 'termination_date', 'commit_rate')),
|
||||
(_('Tenancy'), ('tenant_group', 'tenant')),
|
||||
)
|
||||
```
|
||||
|
||||
```python title="New"
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from netbox.forms import NetBoxModelForm
|
||||
from utilities.forms.rendering import FieldSet
|
||||
|
||||
class CircuitForm(NetBoxModelForm):
|
||||
...
|
||||
fieldsets = (
|
||||
FieldSet('cid', 'type', 'status', 'description', 'tags', name=_('Circuit')),
|
||||
FieldSet('install_date', 'termination_date', 'commit_rate', name=_('Service Parameters')),
|
||||
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
|
||||
)
|
||||
```
|
||||
|
||||
## Navigation
|
||||
|
||||
### Remove button colors
|
||||
|
||||
NetBox no longer applies color to buttons within navigation menu items. Although this functionality is still supported, you might want to remove color from any buttons to ensure consistency with the updated design.
|
||||
|
||||
```python title="Old"
|
||||
PluginMenuButton(
|
||||
link='myplugin:foo_add',
|
||||
title='Add a new Foo',
|
||||
icon_class='mdi mdi-plus-thick',
|
||||
color=ButtonColorChoices.GREEN
|
||||
)
|
||||
```
|
||||
|
||||
```python title="New"
|
||||
PluginMenuButton(
|
||||
link='myplugin:foo_add',
|
||||
title='Add a new Foo',
|
||||
icon_class='mdi mdi-plus-thick'
|
||||
)
|
||||
```
|
||||
|
||||
## UI Layout
|
||||
|
||||
### Renamed template blocks
|
||||
|
||||
The following template blocks have been renamed or removed:
|
||||
|
||||
| Template | Old name | New name |
|
||||
|---------------------|-------------------|---------------------------|
|
||||
| generic/object.html | `header` | `page-header` |
|
||||
| generic/object.html | `controls` | `control-buttons` |
|
||||
| base/layout.html | `content-wrapper` | _Removed_ (use `content`) |
|
||||
|
||||
### Utilize flex controls
|
||||
|
||||
Ditch any legacy "float" controls (e.g. `float-end`) in favor of Bootstrap's new [flex behaviors](https://getbootstrap.com/docs/5.3/utilities/flex/) for controlling the layout and sizing of elements horizontally. For example, the following will align two items against the left and right sides of the parent element:
|
||||
|
||||
```html
|
||||
<div class="d-flex justify-content-between">
|
||||
<h3>Title text</h3>
|
||||
<i class="mdi mdi-close"></i>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Check column offsets
|
||||
|
||||
When using [offset columns](https://getbootstrap.com/docs/5.3/layout/columns/#offsetting-columns) (e.g. `class="col-offset-3"`), be sure to also set the column width (e.g. `class="col-9 col-offset-3"`) to avoid horizontal scrolling.
|
||||
|
||||
### Tables inside cards
|
||||
|
||||
Tables inside cards should be embedded directly, not nested inside a `card-body` element.
|
||||
|
||||
```html title="Old"
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
...
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
```html title="New"
|
||||
<div class="card">
|
||||
<table class="table table-hover attr-table">
|
||||
...
|
||||
</table>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Remove `btn-sm` class from buttons
|
||||
|
||||
The `btn-sm` (small) class is no longer typically needed on general-purpose buttons.
|
||||
|
||||
```html title="Old"
|
||||
<a href="#" class="btn btn-sm btn-primary">Text</a>
|
||||
```
|
||||
|
||||
```html title="New"
|
||||
<a href="#" class="btn btn-primary">Text</a>
|
||||
```
|
||||
|
||||
### Update `bg-$color` classes
|
||||
|
||||
Foreground (text) color is no longer automatically adjusted by `bg-$color` classes. To ensure sufficient contrast with the background color, use the [`text-bg-$color`](https://getbootstrap.com/docs/5.3/helpers/color-background/) form of the class instead, or set the text color separately with `text-$color`.
|
||||
|
||||
```html title="Old"
|
||||
<span class="badge bg-primary">Text</span>
|
||||
```
|
||||
|
||||
```html title="New"
|
||||
<span class="badge text-bg-primary">Text</span>
|
||||
```
|
||||
|
||||
### Obsolete custom CSS classes
|
||||
|
||||
The following custom CSS classes have been removed:
|
||||
|
||||
* `object-subtitle` (use `text-secondary` instead)
|
||||
|
||||
## REST API
|
||||
|
||||
### Extend serializer for brief mode
|
||||
|
||||
NetBox now uses a single API serializer for both normal and "brief" modes (i.e. `GET /api/dcim/sites/?brief=true`); nested serializer classes are no longer required. Two changes to API serializers are necessary to support brief mode:
|
||||
|
||||
1. Define `brief_fields` under its `Meta` class. These are the fields which will be included when brief mode is used.
|
||||
2. For any nested objects, switch to using the primary serializer and pass `nested=True`.
|
||||
|
||||
Any nested serializers which are no longer needed can be removed.
|
||||
|
||||
```python title="Old"
|
||||
class SiteSerializer(NetBoxModelSerializer):
|
||||
region = NestedRegionSerializer(required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = ('id', 'url', 'display', 'name', 'slug', 'status', 'region', 'time_zone', ...)
|
||||
```
|
||||
|
||||
```python title="New"
|
||||
class SiteSerializer(NetBoxModelSerializer):
|
||||
region = RegionSerializer(nested=True, required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = ('id', 'url', 'display', 'name', 'slug', 'status', 'region', 'time_zone', ...)
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description', 'slug')
|
||||
```
|
||||
|
||||
### Include description fields in brief mode
|
||||
|
||||
NetBox now includes the `description` the field in "brief" mode for all models which have one. This is not required for plugins, but you may opt to do the same for consistency.
|
||||
|
||||
## GraphQL
|
||||
|
||||
NetBox has replaced [Graphene-Django](https://github.com/graphql-python/graphene-django) with [Strawberry](https://strawberry.rocks/) which requires any GraphQL code to be updated.
|
||||
|
||||
### Change schema.py
|
||||
|
||||
Strawberry uses [python typing](https://docs.python.org/3/library/typing.html) and generally only requires a small refactoring of the schema definition to update:
|
||||
|
||||
```python title="Old"
|
||||
import graphene
|
||||
from netbox.graphql.fields import ObjectField, ObjectListField
|
||||
from utilities.graphql_optimizer import gql_query_optimizer
|
||||
|
||||
class CircuitsQuery(graphene.ObjectType):
|
||||
circuit = ObjectField(CircuitType)
|
||||
circuit_list = ObjectListField(CircuitType)
|
||||
|
||||
def resolve_circuit_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.Circuit.objects.all(), info)
|
||||
```
|
||||
|
||||
```python title="New"
|
||||
from typing import List
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
@strawberry.type
|
||||
class CircuitsQuery:
|
||||
@strawberry.field
|
||||
def circuit(self, id: int) -> CircuitType:
|
||||
return models.Circuit.objects.get(pk=id)
|
||||
circuit_list: List[CircuitType] = strawberry_django.field()
|
||||
```
|
||||
|
||||
### Change types.py
|
||||
|
||||
Type conversion is also fairly straight-forward, but Strawberry requires FK and M2M references to be explicitly defined to pick up the right typing.
|
||||
|
||||
1. The `class Meta` options need to be moved up to the Strawberry decorator
|
||||
2. Add `@strawberry_django.field` definitions for any FK and M2M references in the model
|
||||
|
||||
```python title="Old"
|
||||
import graphene
|
||||
|
||||
class CircuitType(NetBoxObjectType, ContactsMixin):
|
||||
class Meta:
|
||||
model = models.Circuit
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.CircuitFilterSet
|
||||
```
|
||||
|
||||
```python title="New"
|
||||
from typing import Annotated, List
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
@strawberry_django.type(
|
||||
models.CircuitType,
|
||||
fields='__all__',
|
||||
filters=CircuitTypeFilter
|
||||
)
|
||||
class CircuitTypeType(OrganizationalObjectType):
|
||||
color: str
|
||||
|
||||
@strawberry_django.field
|
||||
def circuits(self) -> List[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]:
|
||||
return self.circuits.all()
|
||||
```
|
||||
|
||||
### Change filters.py
|
||||
|
||||
Strawberry currently doesn't directly support django-filter, so an explicit filters.py file will need to be created. NetBox includes a new `autotype_decorator` used to automatically wrap FilterSets to reduce the required code to a minimum.
|
||||
|
||||
```python title="New"
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from circuits import filtersets, models
|
||||
|
||||
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
|
||||
|
||||
__all__ = (
|
||||
'CircuitFilter',
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Circuit, lookups=True)
|
||||
@autotype_decorator(filtersets.CircuitFilterSet)
|
||||
class CircuitFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
```
|
@ -49,8 +49,8 @@ menu_items = (item1, item2, item3)
|
||||
Each menu item represents a link and (optionally) a set of buttons comprising one entry in NetBox's navigation menu. Menu items are defined as PluginMenuItem instances. An example is shown below.
|
||||
|
||||
```python title="navigation.py"
|
||||
from netbox.choices import ButtonColorChoices
|
||||
from netbox.plugins import PluginMenuButton, PluginMenuItem
|
||||
from utilities.choices import ButtonColorChoices
|
||||
|
||||
item1 = PluginMenuItem(
|
||||
link='plugins:myplugin:myview',
|
||||
@ -72,8 +72,6 @@ A `PluginMenuItem` has the following attributes:
|
||||
| `staff_only` | - | Display only for users who have `is_staff` set to true (any specified permissions will also be required) |
|
||||
| `buttons` | - | An iterable of PluginMenuButton instances to include |
|
||||
|
||||
!!! info "The `staff_only` attribute was introduced in NetBox v3.6.1."
|
||||
|
||||
## Menu Buttons
|
||||
|
||||
Each menu item can include a set of buttons. These can be handy for providing shortcuts related to the menu item. For instance, most items in NetBox's navigation menu include buttons to create and import new objects.
|
||||
|
@ -90,8 +90,6 @@ The table column classes listed below are supported for use in plugins. These cl
|
||||
|
||||
## Extending Core Tables
|
||||
|
||||
!!! info "This feature was introduced in NetBox v3.7."
|
||||
|
||||
Plugins can register their own custom columns on core tables using the `register_table_column()` utility function. This allows a plugin to attach additional information, such as relationships to its own models, to built-in object lists.
|
||||
|
||||
```python
|
||||
|
@ -82,10 +82,10 @@ Plugins may package static files to be served directly by the HTTP front end. En
|
||||
|
||||
### Restart WSGI Service
|
||||
|
||||
Restart the WSGI service to load the new plugin:
|
||||
Restart the WSGI service and RQ workers to load the new plugin:
|
||||
|
||||
```no-highlight
|
||||
# sudo systemctl restart netbox
|
||||
# sudo systemctl restart netbox netbox-rq
|
||||
```
|
||||
|
||||
## Removing Plugins
|
||||
|
@ -1,353 +1,254 @@
|
||||
---
|
||||
hide:
|
||||
- toc
|
||||
---
|
||||
|
||||
# Markdown
|
||||
|
||||
NetBox supports markdown rendering for certain text fields.
|
||||
NetBox supports Markdown rendering for certain text fields. Some common examples are provided below. For a complete Markdown reference, please see [Markdownguide.org](https://www.markdownguide.org/basic-syntax/).
|
||||
|
||||
## Syntax
|
||||
|
||||
##### Table of Contents
|
||||
[Headers](#headers)
|
||||
[Emphasis](#emphasis)
|
||||
[Lists](#lists)
|
||||
[Links](#links)
|
||||
[Images](#images)
|
||||
[Code Blocks](#code)
|
||||
[Tables](#tables)
|
||||
[Blockquotes](#blockquotes)
|
||||
[Inline HTML](#html)
|
||||
[Horizontal Rule](#hr)
|
||||
[Line Breaks](#lines)
|
||||
|
||||
<a name="headers"></a>
|
||||
|
||||
## Headers
|
||||
## Headings
|
||||
|
||||
```no-highlight
|
||||
# H1
|
||||
## H2
|
||||
### H3
|
||||
#### H4
|
||||
##### H5
|
||||
###### H6
|
||||
# Heading 1
|
||||
## Heading 2
|
||||
### Heading 3
|
||||
#### Heading 4
|
||||
##### Heading 5
|
||||
###### Heading 6
|
||||
```
|
||||
|
||||
<h1>Heading 1</h1>
|
||||
<h2>Heading 2</h2>
|
||||
<h3>Heading 3</h3>
|
||||
<h4>Heading 4</h4>
|
||||
<h5>Heading 5</h5>
|
||||
<h6>Heading 6</h6>
|
||||
|
||||
Alternatively, for H1 and H2, an underline-ish style:
|
||||
|
||||
Alt-H1
|
||||
======
|
||||
```no-highlight
|
||||
Heading 1
|
||||
=========
|
||||
|
||||
Alt-H2
|
||||
------
|
||||
Heading 2
|
||||
---------
|
||||
```
|
||||
|
||||
# H1
|
||||
## H2
|
||||
### H3
|
||||
#### H4
|
||||
##### H5
|
||||
###### H6
|
||||
<h1>Heading 1</h1>
|
||||
<h2>Heading 2</h2>
|
||||
|
||||
<a name="emphasis"></a>
|
||||
|
||||
## Emphasis
|
||||
## Text
|
||||
|
||||
```no-highlight
|
||||
Emphasis, aka italics, with *asterisks* or _underscores_.
|
||||
|
||||
Strong emphasis, aka bold, with **asterisks** or __underscores__.
|
||||
|
||||
Combined emphasis with **asterisks and _underscores_**.
|
||||
|
||||
Strikethrough uses two tildes. ~~Scratch this.~~
|
||||
Italicize text with *asterisks* or _underscores_.
|
||||
```
|
||||
|
||||
Emphasis, aka italics, with *asterisks* or _underscores_.
|
||||
|
||||
Strong emphasis, aka bold, with **asterisks** or __underscores__.
|
||||
|
||||
Combined emphasis with **asterisks and _underscores_**.
|
||||
|
||||
Strikethrough uses two tildes. ~~Scratch this.~~
|
||||
|
||||
|
||||
<a name="lists"></a>
|
||||
|
||||
## Lists
|
||||
|
||||
(In this example, leading and trailing spaces are shown with with dots: ⋅)
|
||||
Italicize text with *asterisks* or _underscores_.
|
||||
|
||||
```no-highlight
|
||||
1. First ordered list item
|
||||
2. Another item
|
||||
⋅⋅* Unordered sub-list.
|
||||
1. Actual numbers don't matter, just that it's a number
|
||||
⋅⋅1. Ordered sub-list
|
||||
4. And another item.
|
||||
|
||||
⋅⋅⋅You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown).
|
||||
|
||||
⋅⋅⋅To have a line break without a paragraph, you will need to use two trailing spaces.⋅⋅
|
||||
⋅⋅⋅Note that this line is separate, but within the same paragraph.⋅⋅
|
||||
⋅⋅⋅(This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.)
|
||||
|
||||
* Unordered list can use asterisks
|
||||
- Or minuses
|
||||
+ Or pluses
|
||||
Bold text with two **asterisks** or __underscores__.
|
||||
```
|
||||
|
||||
1. First ordered list item
|
||||
2. Another item
|
||||
* Unordered sub-list.
|
||||
1. Actual numbers don't matter, just that it's a number
|
||||
1. Ordered sub-list
|
||||
4. And another item.
|
||||
|
||||
You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown).
|
||||
|
||||
To have a line break without a paragraph, you will need to use two trailing spaces.
|
||||
Note that this line is separate, but within the same paragraph.
|
||||
(This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.)
|
||||
|
||||
* Unordered list can use asterisks
|
||||
- Or minuses
|
||||
+ Or pluses
|
||||
|
||||
<a name="links"></a>
|
||||
|
||||
## Links
|
||||
|
||||
There are two ways to create links.
|
||||
Bold text with two **asterisks** or __underscores__.
|
||||
|
||||
```no-highlight
|
||||
[I'm an inline-style link](https://www.google.com)
|
||||
|
||||
[I'm an inline-style link with title](https://www.google.com "Google's Homepage")
|
||||
|
||||
[I'm a reference-style link][Arbitrary case-insensitive reference text]
|
||||
|
||||
[You can use numbers for reference-style link definitions][1]
|
||||
|
||||
Or leave it empty and use the [link text itself].
|
||||
|
||||
URLs and URLs in angle brackets will automatically get turned into links.
|
||||
http://www.example.com or <http://www.example.com> and sometimes
|
||||
example.com (but not on Github, for example).
|
||||
|
||||
Some text to show that the reference links can follow later.
|
||||
|
||||
[arbitrary case-insensitive reference text]: https://www.mozilla.org
|
||||
[1]: http://slashdot.org
|
||||
[link text itself]: http://www.reddit.com
|
||||
Strike text with two tildes. ~~Deleted text.~~
|
||||
```
|
||||
|
||||
[I'm an inline-style link](https://www.google.com)
|
||||
|
||||
[I'm an inline-style link with title](https://www.google.com "Google's Homepage")
|
||||
|
||||
[I'm a reference-style link][Arbitrary case-insensitive reference text]
|
||||
|
||||
[You can use numbers for reference-style link definitions][1]
|
||||
|
||||
Or leave it empty and use the [link text itself].
|
||||
|
||||
URLs and URLs in angle brackets will automatically get turned into links.
|
||||
http://www.example.com or <http://www.example.com> and sometimes
|
||||
example.com (but not on Github, for example).
|
||||
|
||||
Some text to show that the reference links can follow later.
|
||||
|
||||
[arbitrary case-insensitive reference text]: https://www.mozilla.org
|
||||
[1]: http://slashdot.org
|
||||
[link text itself]: http://www.reddit.com
|
||||
|
||||
<a name="images"></a>
|
||||
|
||||
## Images
|
||||
|
||||
```
|
||||
Here's the NetBox logo (hover to see the title text):
|
||||
|
||||
Inline-style:
|
||||

|
||||
|
||||
Reference-style:
|
||||
![alt text][logo]
|
||||
|
||||
[logo]: /media/misc/netbox_logo.png "Logo Title Text 2"
|
||||
```
|
||||
|
||||
Here's the NetBox logo (hover to see the title text):
|
||||
|
||||
Inline-style:
|
||||

|
||||
|
||||
Reference-style:
|
||||
![alt text][logo]
|
||||
|
||||
[logo]: ../media/misc/netbox_logo.png "Logo Title Text 2"
|
||||
|
||||
<a name="code"></a>
|
||||
|
||||
## Code blocks
|
||||
|
||||
```
|
||||
Inline `code` has `back-ticks around` it.
|
||||
```
|
||||
|
||||
Inline `code` has `back-ticks around` it.
|
||||
|
||||
Blocks of code are fenced by lines with three back-ticks <code>```</code>
|
||||
|
||||
````
|
||||
```
|
||||
var s = "Code block";
|
||||
alert(s);
|
||||
```
|
||||
````
|
||||
|
||||
```
|
||||
var s = "Code block";
|
||||
alert(s);
|
||||
```
|
||||
|
||||
<a name="tables"></a>
|
||||
|
||||
## Tables
|
||||
|
||||
```no-highlight
|
||||
Colons can be used to align columns.
|
||||
|
||||
| Tables | Are | Cool |
|
||||
| ------------- |:-------------:| -----:|
|
||||
| col 3 is | right-aligned | $1600 |
|
||||
| col 2 is | centered | $12 |
|
||||
| zebra stripes | are neat | $1 |
|
||||
|
||||
There must be at least 3 dashes separating each header cell.
|
||||
The outer pipes (|) are optional, and you don't need to make the
|
||||
raw Markdown line up prettily. You can also use inline Markdown.
|
||||
|
||||
Markdown | Less | Pretty
|
||||
--- | --- | ---
|
||||
*Still* | `renders` | **nicely**
|
||||
1 | 2 | 3
|
||||
```
|
||||
|
||||
Colons can be used to align columns.
|
||||
|
||||
| Tables | Are | Cool |
|
||||
| ------------- |:-------------:| -----:|
|
||||
| col 3 is | right-aligned | $1600 |
|
||||
| col 2 is | centered | $12 |
|
||||
| zebra stripes | are neat | $1 |
|
||||
|
||||
There must be at least 3 dashes separating each header cell. The outer pipes (|) are optional, and you don't need to make the raw Markdown line up prettily. You can also use inline Markdown.
|
||||
|
||||
Markdown | Less | Pretty
|
||||
--- | --- | ---
|
||||
*Still* | `renders` | **nicely**
|
||||
1 | 2 | 3
|
||||
|
||||
<a name="blockquotes"></a>
|
||||
|
||||
## Blockquotes
|
||||
|
||||
```no-highlight
|
||||
> Blockquotes are very handy in email to emulate reply text.
|
||||
> This line is part of the same quote.
|
||||
|
||||
Quote break.
|
||||
|
||||
> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote.
|
||||
```
|
||||
|
||||
> Blockquotes are very handy in email to emulate reply text.
|
||||
> This line is part of the same quote.
|
||||
|
||||
Quote break.
|
||||
|
||||
> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote.
|
||||
|
||||
<a name="html"></a>
|
||||
|
||||
## Inline HTML
|
||||
|
||||
You can also use raw HTML in your Markdown, and it'll mostly work pretty well.
|
||||
|
||||
```no-highlight
|
||||
<dl>
|
||||
<dt>Definition list</dt>
|
||||
<dd>Is something people use sometimes.</dd>
|
||||
|
||||
<dt>Markdown in HTML</dt>
|
||||
<dd>Does *not* work **very** well. Use HTML <em>tags</em>.</dd>
|
||||
</dl>
|
||||
```
|
||||
|
||||
<dl>
|
||||
<dt>Definition list</dt>
|
||||
<dd>Is something people use sometimes.</dd>
|
||||
|
||||
<dt>Markdown in HTML</dt>
|
||||
<dd>Does *not* work **very** well. Use HTML <em>tags</em>.</dd>
|
||||
</dl>
|
||||
|
||||
<a name="hr"></a>
|
||||
|
||||
## Horizontal Rule
|
||||
|
||||
```
|
||||
Three or more...
|
||||
|
||||
---
|
||||
|
||||
Hyphens
|
||||
|
||||
***
|
||||
|
||||
Asterisks
|
||||
|
||||
___
|
||||
|
||||
Underscores
|
||||
```
|
||||
|
||||
Three or more...
|
||||
|
||||
---
|
||||
|
||||
Hyphens
|
||||
|
||||
***
|
||||
|
||||
Asterisks
|
||||
|
||||
___
|
||||
|
||||
Underscores
|
||||
|
||||
<a name="lines"></a>
|
||||
Strike text with two tildes. ~~Deleted text.~~
|
||||
|
||||
## Line Breaks
|
||||
|
||||
By default, Markdown will remove line breaks between successive lines of text. For example:
|
||||
|
||||
```
|
||||
Here's a line for us to start with.
|
||||
|
||||
This line is separated from the one above by two newlines, so it will be a *separate paragraph*.
|
||||
|
||||
This line is also a separate paragraph, but...
|
||||
This line is only separated by a single newline, so it's a separate line in the *same paragraph*.
|
||||
```no-highlight
|
||||
This is one line.
|
||||
And this is another line.
|
||||
One more line here.
|
||||
```
|
||||
|
||||
Here's a line for us to start with.
|
||||
This is one line.
|
||||
And this is another line.
|
||||
One more line here.
|
||||
|
||||
This line is separated from the one above by two newlines, so it will be a *separate paragraph*.
|
||||
To preserve line breaks, append two spaces to each line (represented below with the `⋅` character).
|
||||
|
||||
This line is also begins a separate paragraph, but...
|
||||
This line is only separated by a single newline, so it's a separate line in the *same paragraph*.
|
||||
```no-highlight
|
||||
This is one line.⋅⋅
|
||||
And this is another line.⋅⋅
|
||||
One more line here.
|
||||
```
|
||||
|
||||
Based on [Markdown-Cheatsheet](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) by [adam-p](https://github.com/adam-p) licensed under [CC-BY](https://creativecommons.org/licenses/by/3.0/)
|
||||
This is one line.
|
||||
And this is another line.
|
||||
One more line here.
|
||||
|
||||
## Lists
|
||||
|
||||
Use asterisks or hyphens for unordered lists. Indent items by four spaces to start a child list.
|
||||
|
||||
```no-highlight
|
||||
* Alpha
|
||||
* Bravo
|
||||
* Charlie
|
||||
* Child item 1
|
||||
* Child item 2
|
||||
* Delta
|
||||
```
|
||||
|
||||
* Alpha
|
||||
* Bravo
|
||||
* Charlie
|
||||
* Child item 1
|
||||
* Child item 2
|
||||
* Delta
|
||||
|
||||
Use digits followed by periods for ordered (numbered) lists.
|
||||
|
||||
```no-highlight
|
||||
1. Red
|
||||
2. Green
|
||||
3. Blue
|
||||
1. Light blue
|
||||
2. Dark blue
|
||||
4. Orange
|
||||
```
|
||||
|
||||
1. Red
|
||||
2. Green
|
||||
3. Blue
|
||||
1. Light blue
|
||||
2. Dark blue
|
||||
4. Orange
|
||||
|
||||
## Links
|
||||
|
||||
Text can be rendered as a hyperlink by encasing it in square brackets, followed by a URL in parentheses. A title (text displayed on hover) may optionally be included as well.
|
||||
|
||||
```no-highlight
|
||||
Here's an [example](https://www.example.com) of a link.
|
||||
|
||||
And here's [another link](https://www.example.com "Click me!"), this time with a title.
|
||||
```
|
||||
|
||||
Here's an [example](https://www.example.com) of a link.
|
||||
|
||||
And here's [another link](https://www.example.com "Click me!"), with a title.
|
||||
|
||||
## Images
|
||||
|
||||
The syntax for embedding an image is very similar to that used for a hyperlink. Alternate text should always be provided; this will be displayed if the image fails to load. As with hyperlinks, title text is optional.
|
||||
|
||||
```no-highlight
|
||||

|
||||
```
|
||||
|
||||
## Code Blocks
|
||||
|
||||
Single backticks can be used to annotate code inline. Text enclosed by lines of three backticks will be displayed as a code block.
|
||||
|
||||
```no-highlight
|
||||
Paragraphs are rendered in HTML using `<p>` and `</p>` tags.
|
||||
```
|
||||
|
||||
Paragraphs are rendered in HTML using `<p>` and `</p>` tags.
|
||||
|
||||
````
|
||||
```
|
||||
def my_func(foo, bar):
|
||||
# Do something
|
||||
return foo * bar
|
||||
```
|
||||
````
|
||||
|
||||
```no-highlight
|
||||
def my_func(foo, bar):
|
||||
# Do something
|
||||
return foo * bar
|
||||
```
|
||||
|
||||
## Tables
|
||||
|
||||
Simple tables can be constructed using the pipe character (`|`) to denote columns, and hyphens (`-`) to denote the heading. Inline Markdown can be used to style text within columns.
|
||||
|
||||
```no-highlight
|
||||
| Heading 1 | Heading 2 | Heading 3 |
|
||||
|-----------|-----------|-----------|
|
||||
| Row 1 | Alpha | Red |
|
||||
| Row 2 | **Bravo** | Green |
|
||||
| Row 3 | Charlie | ~~Blue~~ |
|
||||
```
|
||||
|
||||
| Heading 1 | Heading 2 | Heading 3 |
|
||||
|-----------|-----------|-----------|
|
||||
| _Row 1_ | Alpha | Red |
|
||||
| Row 2 | **Bravo** | Green |
|
||||
| Row 3 | Charlie | ~~Blue~~ |
|
||||
|
||||
Colons can be used to align text to the left or right side of a column.
|
||||
|
||||
```no-highlight
|
||||
| Left-aligned | Centered | Right-aligned |
|
||||
|:-------------|:--------:|--------------:|
|
||||
| Text | Text | Text |
|
||||
| Text | Text | Text |
|
||||
| Text | Text | Text |
|
||||
```
|
||||
|
||||
| Left-aligned | Centered | Right-aligned |
|
||||
|:-------------|:--------:|--------------:|
|
||||
| Text | Text | Text |
|
||||
| Text | Text | Text |
|
||||
| Text | Text | Text |
|
||||
|
||||
## Blockquotes
|
||||
|
||||
Text can be wrapped in a blockquote by prepending a right angle bracket (`>`) before each line.
|
||||
|
||||
```no-highlight
|
||||
> I think that I shall never see
|
||||
> a graph more lovely than a tree.
|
||||
> A tree whose crucial property
|
||||
> is loop-free connectivity.
|
||||
```
|
||||
|
||||
> I think that I shall never see
|
||||
> a graph more lovely than a tree.
|
||||
> A tree whose crucial property
|
||||
> is loop-free connectivity.
|
||||
|
||||
Markdown removes line breaks by default. To preserve line breaks, append two spaces to each line (represented below with the `⋅` character).
|
||||
|
||||
```no-highlight
|
||||
> I think that I shall never see⋅⋅
|
||||
> a graph more lovely than a tree.⋅⋅
|
||||
> A tree whose crucial property⋅⋅
|
||||
> is loop-free connectivity.
|
||||
```
|
||||
|
||||
> I think that I shall never see
|
||||
> a graph more lovely than a tree.
|
||||
> A tree whose crucial property
|
||||
> is loop-free connectivity.
|
||||
|
||||
## Horizontal Rule
|
||||
|
||||
A horizontal rule is a single line rendered across the width of the page using a series of three or more hyphens or asterisks. It can be useful for separating sections of content.
|
||||
|
||||
```no-highlight
|
||||
Content
|
||||
|
||||
---
|
||||
|
||||
More content
|
||||
|
||||
***
|
||||
|
||||
Final content
|
||||
```
|
||||
|
||||
Content
|
||||
|
||||
---
|
||||
|
||||
More content
|
||||
|
||||
***
|
||||
|
||||
Final content
|
||||
|
@ -10,6 +10,14 @@ Minor releases are published in April, August, and December of each calendar yea
|
||||
|
||||
This page contains a history of all major and minor releases since NetBox v2.0. For more detail on a specific patch release, please see the release notes page for that specific minor release.
|
||||
|
||||
#### [Version 4.0](./version-4.0.md) (April 2024)
|
||||
|
||||
* Complete UI Refresh ([#12128](https://github.com/netbox-community/netbox/issues/12128))
|
||||
* Dynamic REST API Fields ([#15087](https://github.com/netbox-community/netbox/issues/15087))
|
||||
* Strawberry GraphQL Engine ([#9856](https://github.com/netbox-community/netbox/issues/9856))
|
||||
* Advanced Form Rendering Functionality ([#14739](https://github.com/netbox-community/netbox/issues/14739))
|
||||
* Legacy Admin UI Disabled ([#12325](https://github.com/netbox-community/netbox/issues/12325))
|
||||
|
||||
#### [Version 3.7](./version-3.7.md) (December 2023)
|
||||
|
||||
* VPN Tunnels ([#9816](https://github.com/netbox-community/netbox/issues/9816))
|
||||
|
@ -2,6 +2,11 @@
|
||||
|
||||
## v3.7.5 (FUTURE)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#14799](https://github.com/netbox-community/netbox/issues/14799) - Avoid caching modified reports & scripts
|
||||
* [#15502](https://github.com/netbox-community/netbox/issues/15502) - Fix KeyError exception when modifying an IP address assigned to a virtual machine
|
||||
|
||||
---
|
||||
|
||||
## v3.7.4 (2024-03-13)
|
||||
|
@ -1,46 +1,104 @@
|
||||
# NetBox v4.0
|
||||
|
||||
## v4.0.0 (FUTURE)
|
||||
## v4.0-beta2 (FUTURE)
|
||||
|
||||
**WARNING:** This is a beta release of NetBox intended for testing and evaluation. **Do not use this software in production.** Also be aware that no upgrade path is provided to future releases.
|
||||
|
||||
!!! tip "Plugin Maintainers"
|
||||
Please see the dedicated [plugin migration guide](../plugins/development/migration-v4.md) for a checklist of changes that may be needed to ensure compatibility with NetBox v4.0.
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* Support for Python 3.8 and 3.9 has been removed.
|
||||
* The format for GraphQL query filters has changed. Please see the GraphQL documentation for details and examples.
|
||||
* The deprecated `device_role` & `device_role_id` filters for devices have been removed. (Use `role` and `role_id` instead.)
|
||||
* The obsolete `device_role` field has been removed from the REST API serializer for devices. (Use `role` instead.)
|
||||
* The legacy reports functionality has been dropped. Reports will be automatically converted to custom scripts on upgrade.
|
||||
* The `parent` and `parent_id` filters for locations now return only immediate children of the specified location. (Use `ancestor` and `ancestor_id` to return _all_ descendants.)
|
||||
* The `object_type` field on the CustomField model has been renamed to `related_object_type`.
|
||||
* The `utilities.utils` module has been removed and its resources reorganized into separate modules organized by function.
|
||||
* The obsolete `NullableCharField` class has been removed. (Use Django's stock `CharField` class with `null=True` instead.)
|
||||
|
||||
### New Features
|
||||
|
||||
#### Complete UI Refresh ([#12128](https://github.com/netbox-community/netbox/issues/12128))
|
||||
|
||||
The NetBox user interface has been completely refreshed and updated.
|
||||
The NetBox user interface has been completely refreshed and updated. This massive effort entailed:
|
||||
|
||||
* Refactoring the base HTML templates
|
||||
* Moving from Boostrap 5.0 to Bootstrap 5.3
|
||||
* Adopting the [Tabler](https://tabler.io/) UI theme
|
||||
* Replacing slim-select with [Tom-Select](https://tom-select.js.org/)
|
||||
* Displaying additional object attributes in dropdown form fields
|
||||
* Enabling opt-in HTMX-powered navigation (see [#14736](https://github.com/netbox-community/netbox/issues/14736))
|
||||
* Widespread cleanup & standardization of UI components
|
||||
|
||||
#### Dynamic REST API Fields ([#15087](https://github.com/netbox-community/netbox/issues/15087))
|
||||
|
||||
The REST API now supports specifying which fields to include in the response data.
|
||||
The REST API now supports specifying which fields to include in the response data. For example, the response to a request for
|
||||
|
||||
#### Advanced FieldSet Functionality ([#14739](https://github.com/netbox-community/netbox/issues/14739))
|
||||
```
|
||||
GET /api/dcim/sites/?fields=name,status,region,tenant
|
||||
```
|
||||
|
||||
New resources have been introduced to enable advanced form rendering without a need for custom HTML templates.
|
||||
will include only the four specified fields in the representation of each site. Additionally, the underlying database queries effected by such requests have been optimized to omit fields which are not included in the response, resulting in a substantial performance improvement.
|
||||
|
||||
#### Strawberry GraphQL Engine ([#9856](https://github.com/netbox-community/netbox/issues/9856))
|
||||
|
||||
The GraphQL engine has been changed from using Graphene-Django to Strawberry-Django. Changes include:
|
||||
|
||||
* Queryset Optimizer - reduces the number of database queries when querying related tables
|
||||
* Updated GraphiQL Browser
|
||||
* The format for GraphQL query filters and lookups has changed. Please see the GraphQL documentation for details and examples.
|
||||
|
||||
#### Advanced Form Rendering Functionality ([#14739](https://github.com/netbox-community/netbox/issues/14739))
|
||||
|
||||
New resources have been introduced to enable advanced form rendering without a need for custom HTML templates. These include:
|
||||
|
||||
* FieldSet - Represents a grouping of form fields (replaces the use of lists/tuples)
|
||||
* InlineFields - Multiple fields rendered on a single row
|
||||
* TabbedGroups - Fieldsets rendered under navigable tabs within a form
|
||||
* ObjectAttribute - Renders a read-only representation of a particular object attribute (for reference)
|
||||
|
||||
#### Legacy Admin UI Disabled ([#12325](https://github.com/netbox-community/netbox/issues/12325))
|
||||
|
||||
The legacy admin user interface is now disabled by default, and the few remaining views it provided have been relocated to the primary UI. NetBox deployments which still depend on the legacy admin functionality for plugins can enable it by setting the `DJANGO_ADMIN_ENABLED` configuration parameter to true.
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#12851](https://github.com/netbox-community/netbox/issues/12851) - Replace bleach HTML sanitization library with nh3
|
||||
* [#13283](https://github.com/netbox-community/netbox/issues/13283) - Display additional context on API-backed dropdown fields
|
||||
* [#12776](https://github.com/netbox-community/netbox/issues/12776) - Introduce the `htmx_talble` template tag to simplify the rendering of embedded tables
|
||||
* [#12851](https://github.com/netbox-community/netbox/issues/12851) - Replace the deprecated Bleach HTML sanitization library with nh3
|
||||
* [#13283](https://github.com/netbox-community/netbox/issues/13283) - Display additional context on API-backed dropdown form fields (e.g. object descriptions)
|
||||
* [#13918](https://github.com/netbox-community/netbox/issues/13918) - Add `facility` field to Location model
|
||||
* [#14237](https://github.com/netbox-community/netbox/issues/14237) - Automatically clear dependent selection fields when modifying a parent selection
|
||||
* [#14454](https://github.com/netbox-community/netbox/issues/14454) - Include member devices for virtual chassis in REST API
|
||||
* [#14237](https://github.com/netbox-community/netbox/issues/14237) - Automatically clear dependent selection form fields when modifying a parent selection
|
||||
* [#14279](https://github.com/netbox-community/netbox/issues/14279) - Make the current request available as context when running custom validators
|
||||
* [#14454](https://github.com/netbox-community/netbox/issues/14454) - Include member devices in the REST API representation of virtual chassis
|
||||
* [#14637](https://github.com/netbox-community/netbox/issues/14637) - Upgrade to Django 5.0
|
||||
* [#14672](https://github.com/netbox-community/netbox/issues/14672) - Add support for Python 3.12
|
||||
* [#14728](https://github.com/netbox-community/netbox/issues/14728) - The plugins list view has been moved from the legacy admin UI to the main NetBox UI
|
||||
* [#14729](https://github.com/netbox-community/netbox/issues/14729) - All background task views have been moved from the legacy admin UI to the main NetBox UI
|
||||
* [#14736](https://github.com/netbox-community/netbox/issues/14736) - Introduce a user preference to enable HTMX-powered navigation
|
||||
* [#14438](https://github.com/netbox-community/netbox/issues/14438) - Track individual custom scripts as database objects
|
||||
* [#15131](https://github.com/netbox-community/netbox/issues/15131) - Automatically annotate related object counts on REST API querysets
|
||||
* [#15237](https://github.com/netbox-community/netbox/issues/15237) - Ensure consistent filtering ability for all model fields
|
||||
* [#15237](https://github.com/netbox-community/netbox/issues/15237) - Ensure consistent filtering ability for all model fields by testing for missing/incorrect filters
|
||||
* [#15238](https://github.com/netbox-community/netbox/issues/15238) - Include the `description` field in "brief" REST API serializations
|
||||
* [#15278](https://github.com/netbox-community/netbox/issues/15278) - BaseModelSerializer now takes a `nested` keyword argument allowing it to represent a related object
|
||||
* [#15383](https://github.com/netbox-community/netbox/issues/15383) - Standardize filtering logic for the parents of recursively-nested models (parent & ancestor filters)
|
||||
* [#15413](https://github.com/netbox-community/netbox/issues/15413) - The global search engine now supports caching of non-field object attributes
|
||||
* [#15490](https://github.com/netbox-community/netbox/issues/15490) - Custom validators can now reference related object attributes via dotted paths
|
||||
|
||||
### Bug Fixes (from Beta1)
|
||||
|
||||
* [#15605](https://github.com/netbox-community/netbox/issues/15605) - Fix `ProgrammingError` exception when applying migrations to older databases
|
||||
* [#15616](https://github.com/netbox-community/netbox/issues/15616) - Fix button style for invalid custom links
|
||||
* [#15617](https://github.com/netbox-community/netbox/issues/15617) - Fix rack elevation styling under dark mode
|
||||
* [#15619](https://github.com/netbox-community/netbox/issues/15619) - Enforce a minimum width for progress bars
|
||||
* [#15637](https://github.com/netbox-community/netbox/issues/15637) - Correct nonfunctional links within embedded tables when HTMX enabled
|
||||
* [#15638](https://github.com/netbox-community/netbox/issues/15638) - Correct parameter used to retrieve saved filters for a model
|
||||
|
||||
### Other Changes
|
||||
|
||||
* [#10587](https://github.com/netbox-community/netbox/issues/10587) - Enable pagination and filtering for custom script logs
|
||||
* [#12325](https://github.com/netbox-community/netbox/issues/12325) - The Django admin UI is now disabled by default (set `DJANGO_ADMIN_ENABLED` to True to enable it)
|
||||
* [#12510](https://github.com/netbox-community/netbox/issues/12510) - Dropped support for legacy reports
|
||||
* [#12795](https://github.com/netbox-community/netbox/issues/12795) - NetBox now uses custom User and Group models rather than the stock models provided by Django
|
||||
@ -48,43 +106,59 @@ New resources have been introduced to enable advanced form rendering without a n
|
||||
* [#14092](https://github.com/netbox-community/netbox/issues/14092) - Remove backward compatibility for importing plugin resources from `extras.plugins` (now `netbox.plugins`)
|
||||
* [#14638](https://github.com/netbox-community/netbox/issues/14638) - Drop support for Python 3.8 and 3.9
|
||||
* [#14657](https://github.com/netbox-community/netbox/issues/14657) - Remove backward compatibility for old permissions mapping under `ActionsMixin`
|
||||
* [#14658](https://github.com/netbox-community/netbox/issues/14658) - Remove backward compatibility for importing `process_webhook()` (now `extras.webhooks.send_webhook()`)
|
||||
* [#14658](https://github.com/netbox-community/netbox/issues/14658) - Remove backward compatibility for importing `process_webhook()` from `extras.webhooks_worker` (now `extras.webhooks.send_webhook()`)
|
||||
* [#14740](https://github.com/netbox-community/netbox/issues/14740) - Remove the obsolete `BootstrapMixin` form mixin class
|
||||
* [#15042](https://github.com/netbox-community/netbox/issues/15042) - Rearchitect the logic for registering models & model features
|
||||
* [#15042](https://github.com/netbox-community/netbox/issues/15042) - The logic for registering models & model features now executes under the `ready()` method of individual app configs, rather than relying on the `class_prepared` signal
|
||||
* [#15099](https://github.com/netbox-community/netbox/issues/15099) - Remove obsolete `device_role` and `device_role_id` filters for devices
|
||||
* [#15100](https://github.com/netbox-community/netbox/issues/15100) - Remove obsolete `NullableCharField` class
|
||||
* [#15154](https://github.com/netbox-community/netbox/issues/15154) - The installation documentation been extended to include instructions and an example configuration file for uWSGI as an alternative to gunicorn
|
||||
* [#15193](https://github.com/netbox-community/netbox/issues/15193) - Switch to compiled distribution of the `psycopg` library
|
||||
* [#15277](https://github.com/netbox-community/netbox/issues/15277) - Replace references to ContentType without ObjectType proxy model & standardize field names
|
||||
* [#15292](https://github.com/netbox-community/netbox/issues/15292) - Remove obsolete `device_role` attribute from Device model (this field was renamed to `role` in v3.6)
|
||||
* [#15357](https://github.com/netbox-community/netbox/issues/15357) - The `object_type` field on the CustomField model has been renamed to `related_object_type` to avoid confusion with its `object_types` field
|
||||
* [#15401](https://github.com/netbox-community/netbox/issues/15401) - PostgreSQL indexes and sequence tables for the relocated L2VPN models (see [#14311](https://github.com/netbox-community/netbox/issues/14311)) have been renamed
|
||||
* [#15462](https://github.com/netbox-community/netbox/issues/15462) - Relocate resources from the `utilities.utils` module
|
||||
* [#15464](https://github.com/netbox-community/netbox/issues/15464) - The many-to-many relationships for ObjectPermission are now defined on the custom User and Group models
|
||||
|
||||
### REST API Changes
|
||||
|
||||
* The `/api/extras/content-types/` endpoint has moved to `/api/extras/object-types/`
|
||||
* The `/api/extras/reports/` endpoint has been removed
|
||||
* The `description` field is now included by default when using "brief mode" for all relevant models
|
||||
* dcim.Device
|
||||
* The obsolete read-only attribute `device_role` has been removed (replaced by `role` in v3.6)
|
||||
* The obsolete read-only attribute `device_role` has been removed (replaced by `role` in v3.6)
|
||||
* dcim.Location
|
||||
* Added the optional `location` field
|
||||
* dcim.VirtualChassis
|
||||
* Added `members` field to list the member devices
|
||||
* extras.CustomField
|
||||
* `content_types` has been renamed to `object_types`
|
||||
* The `content_types` filter is now `object_type`
|
||||
* The `content_type_id` filter is now `object_type_id`
|
||||
* `content_types` has been renamed to `object_types`
|
||||
* `object_type` has been renamed to `related_object_type`
|
||||
* The `content_types` filter is now `object_type`
|
||||
* The `content_type_id` filter is now `object_type_id`
|
||||
* extras.CustomLink
|
||||
* `content_types` has been renamed to `object_types`
|
||||
* The `content_types` filter is now `object_type`
|
||||
* The `content_type_id` filter is now `object_type_id`
|
||||
* `content_types` has been renamed to `object_types`
|
||||
* The `content_types` filter is now `object_type`
|
||||
* The `content_type_id` filter is now `object_type_id`
|
||||
* extras.EventRule
|
||||
* `content_types` has been renamed to `object_types`
|
||||
* The `content_types` filter is now `object_type`
|
||||
* The `content_type_id` filter is now `object_type_id`
|
||||
* `content_types` has been renamed to `object_types`
|
||||
* The `content_types` filter is now `object_type`
|
||||
* The `content_type_id` filter is now `object_type_id`
|
||||
* extras.ExportTemplate
|
||||
* `content_types` has been renamed to `object_types`
|
||||
* The `content_types` filter is now `object_type`
|
||||
* The `content_type_id` filter is now `object_type_id`
|
||||
* `content_types` has been renamed to `object_types`
|
||||
* The `content_types` filter is now `object_type`
|
||||
* The `content_type_id` filter is now `object_type_id`
|
||||
* extras.ImageAttachment
|
||||
* `content_type` has been renamed to `object_type`
|
||||
* The `content_type` filter is now `object_type`
|
||||
* `content_type` has been renamed to `object_type`
|
||||
* The `content_type` filter is now `object_type`
|
||||
* extras.SavedFilter
|
||||
* `content_types` has been renamed to `object_types`
|
||||
* The `content_types` filter is now `object_type`
|
||||
* The `content_type_id` filter is now `object_type_id`
|
||||
* `content_types` has been renamed to `object_types`
|
||||
* The `content_types` filter is now `object_type`
|
||||
* The `content_type_id` filter is now `object_type_id`
|
||||
* tenancy.ContactAssignment
|
||||
* `content_type` has been renamed to `object_type`
|
||||
* The `content_type_id` filter is now `object_type_id`
|
||||
* `content_type` has been renamed to `object_type`
|
||||
* The `content_type_id` filter is now `object_type_id`
|
||||
* users.Group
|
||||
* Added the `permissions` field
|
||||
* users.User
|
||||
* Added the `permissions` field
|
||||
|
@ -94,7 +94,8 @@ nav:
|
||||
- 1. PostgreSQL: 'installation/1-postgresql.md'
|
||||
- 2. Redis: 'installation/2-redis.md'
|
||||
- 3. NetBox: 'installation/3-netbox.md'
|
||||
- 4. Gunicorn: 'installation/4-gunicorn.md'
|
||||
- 4a. Gunicorn: 'installation/4a-gunicorn.md'
|
||||
- 4b. uWSGI: 'installation/4b-uwsgi.md'
|
||||
- 5. HTTP Server: 'installation/5-http-server.md'
|
||||
- 6. LDAP (Optional): 'installation/6-ldap.md'
|
||||
- Upgrading NetBox: 'installation/upgrading.md'
|
||||
@ -146,6 +147,7 @@ nav:
|
||||
- Dashboard Widgets: 'plugins/development/dashboard-widgets.md'
|
||||
- Staged Changes: 'plugins/development/staged-changes.md'
|
||||
- Exceptions: 'plugins/development/exceptions.md'
|
||||
- Migrating to v4.0: 'plugins/development/migration-v4.md'
|
||||
- Administration:
|
||||
- Authentication:
|
||||
- Overview: 'administration/authentication/overview.md'
|
||||
|
50
netbox/circuits/graphql/filters.py
Normal file
50
netbox/circuits/graphql/filters.py
Normal file
@ -0,0 +1,50 @@
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from circuits import filtersets, models
|
||||
|
||||
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
|
||||
|
||||
__all__ = (
|
||||
'CircuitTerminationFilter',
|
||||
'CircuitFilter',
|
||||
'CircuitTypeFilter',
|
||||
'ProviderFilter',
|
||||
'ProviderAccountFilter',
|
||||
'ProviderNetworkFilter',
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.CircuitTermination, lookups=True)
|
||||
@autotype_decorator(filtersets.CircuitTerminationFilterSet)
|
||||
class CircuitTerminationFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Circuit, lookups=True)
|
||||
@autotype_decorator(filtersets.CircuitFilterSet)
|
||||
class CircuitFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.CircuitType, lookups=True)
|
||||
@autotype_decorator(filtersets.CircuitTypeFilterSet)
|
||||
class CircuitTypeFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Provider, lookups=True)
|
||||
@autotype_decorator(filtersets.ProviderFilterSet)
|
||||
class ProviderFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ProviderAccount, lookups=True)
|
||||
@autotype_decorator(filtersets.ProviderAccountFilterSet)
|
||||
class ProviderAccountFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ProviderNetwork, lookups=True)
|
||||
@autotype_decorator(filtersets.ProviderNetworkFilterSet)
|
||||
class ProviderNetworkFilter(BaseFilterMixin):
|
||||
pass
|
@ -1,41 +1,40 @@
|
||||
import graphene
|
||||
from typing import List
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from circuits import models
|
||||
from netbox.graphql.fields import ObjectField, ObjectListField
|
||||
from .types import *
|
||||
from utilities.graphql_optimizer import gql_query_optimizer
|
||||
|
||||
|
||||
class CircuitsQuery(graphene.ObjectType):
|
||||
circuit = ObjectField(CircuitType)
|
||||
circuit_list = ObjectListField(CircuitType)
|
||||
@strawberry.type
|
||||
class CircuitsQuery:
|
||||
@strawberry.field
|
||||
def circuit(self, id: int) -> CircuitType:
|
||||
return models.Circuit.objects.get(pk=id)
|
||||
circuit_list: List[CircuitType] = strawberry_django.field()
|
||||
|
||||
def resolve_circuit_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.Circuit.objects.all(), info)
|
||||
@strawberry.field
|
||||
def circuit_termination(self, id: int) -> CircuitTerminationType:
|
||||
return models.CircuitTermination.objects.get(pk=id)
|
||||
circuit_termination_list: List[CircuitTerminationType] = strawberry_django.field()
|
||||
|
||||
circuit_termination = ObjectField(CircuitTerminationType)
|
||||
circuit_termination_list = ObjectListField(CircuitTerminationType)
|
||||
@strawberry.field
|
||||
def circuit_type(self, id: int) -> CircuitTypeType:
|
||||
return models.CircuitType.objects.get(pk=id)
|
||||
circuit_type_list: List[CircuitTypeType] = strawberry_django.field()
|
||||
|
||||
def resolve_circuit_termination_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.CircuitTermination.objects.all(), info)
|
||||
@strawberry.field
|
||||
def provider(self, id: int) -> ProviderType:
|
||||
return models.Provider.objects.get(pk=id)
|
||||
provider_list: List[ProviderType] = strawberry_django.field()
|
||||
|
||||
circuit_type = ObjectField(CircuitTypeType)
|
||||
circuit_type_list = ObjectListField(CircuitTypeType)
|
||||
@strawberry.field
|
||||
def provider_account(self, id: int) -> ProviderAccountType:
|
||||
return models.ProviderAccount.objects.get(pk=id)
|
||||
provider_account_list: List[ProviderAccountType] = strawberry_django.field()
|
||||
|
||||
def resolve_circuit_type_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.CircuitType.objects.all(), info)
|
||||
|
||||
provider = ObjectField(ProviderType)
|
||||
provider_list = ObjectListField(ProviderType)
|
||||
|
||||
def resolve_provider_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.Provider.objects.all(), info)
|
||||
|
||||
provider_account = ObjectField(ProviderAccountType)
|
||||
provider_account_list = ObjectListField(ProviderAccountType)
|
||||
|
||||
provider_network = ObjectField(ProviderNetworkType)
|
||||
provider_network_list = ObjectListField(ProviderNetworkType)
|
||||
|
||||
def resolve_provider_network_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.ProviderNetwork.objects.all(), info)
|
||||
@strawberry.field
|
||||
def provider_network(self, id: int) -> ProviderNetworkType:
|
||||
return models.ProviderNetwork.objects.get(pk=id)
|
||||
provider_network_list: List[ProviderNetworkType] = strawberry_django.field()
|
||||
|
@ -1,9 +1,14 @@
|
||||
import graphene
|
||||
from typing import Annotated, List
|
||||
|
||||
from circuits import filtersets, models
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from circuits import models
|
||||
from dcim.graphql.mixins import CabledObjectMixin
|
||||
from extras.graphql.mixins import CustomFieldsMixin, TagsMixin, ContactsMixin
|
||||
from netbox.graphql.types import ObjectType, OrganizationalObjectType, NetBoxObjectType
|
||||
from extras.graphql.mixins import ContactsMixin, CustomFieldsMixin, TagsMixin
|
||||
from netbox.graphql.types import NetBoxObjectType, ObjectType, OrganizationalObjectType
|
||||
from tenancy.graphql.types import TenantType
|
||||
from .filters import *
|
||||
|
||||
__all__ = (
|
||||
'CircuitTerminationType',
|
||||
@ -15,48 +20,74 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, ObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.CircuitTermination
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.CircuitTerminationFilterSet
|
||||
|
||||
|
||||
class CircuitType(NetBoxObjectType, ContactsMixin):
|
||||
class Meta:
|
||||
model = models.Circuit
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.CircuitFilterSet
|
||||
|
||||
|
||||
class CircuitTypeType(OrganizationalObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.CircuitType
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.CircuitTypeFilterSet
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.Provider,
|
||||
fields='__all__',
|
||||
filters=ProviderFilter
|
||||
)
|
||||
class ProviderType(NetBoxObjectType, ContactsMixin):
|
||||
|
||||
class Meta:
|
||||
model = models.Provider
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.ProviderFilterSet
|
||||
networks: List[Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')]]
|
||||
circuits: List[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]
|
||||
asns: List[Annotated["ASNType", strawberry.lazy('ipam.graphql.types')]]
|
||||
accounts: List[Annotated["ProviderAccountType", strawberry.lazy('circuits.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.ProviderAccount,
|
||||
fields='__all__',
|
||||
filters=ProviderAccountFilter
|
||||
)
|
||||
class ProviderAccountType(NetBoxObjectType):
|
||||
provider: Annotated["ProviderType", strawberry.lazy('circuits.graphql.types')]
|
||||
|
||||
class Meta:
|
||||
model = models.ProviderAccount
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.ProviderAccountFilterSet
|
||||
circuits: List[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.ProviderNetwork,
|
||||
fields='__all__',
|
||||
filters=ProviderNetworkFilter
|
||||
)
|
||||
class ProviderNetworkType(NetBoxObjectType):
|
||||
provider: Annotated["ProviderType", strawberry.lazy('circuits.graphql.types')]
|
||||
|
||||
class Meta:
|
||||
model = models.ProviderNetwork
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.ProviderNetworkFilterSet
|
||||
circuit_terminations: List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.CircuitTermination,
|
||||
fields='__all__',
|
||||
filters=CircuitTerminationFilter
|
||||
)
|
||||
class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, ObjectType):
|
||||
circuit: Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]
|
||||
provider_network: Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')] | None
|
||||
site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.CircuitType,
|
||||
fields='__all__',
|
||||
filters=CircuitTypeFilter
|
||||
)
|
||||
class CircuitTypeType(OrganizationalObjectType):
|
||||
color: str
|
||||
|
||||
circuits: List[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.Circuit,
|
||||
fields='__all__',
|
||||
filters=CircuitFilter
|
||||
)
|
||||
class CircuitType(NetBoxObjectType, ContactsMixin):
|
||||
provider: ProviderType
|
||||
provider_account: ProviderAccountType | None
|
||||
termination_a: CircuitTerminationType | None
|
||||
termination_z: CircuitTerminationType | None
|
||||
type: CircuitTypeType
|
||||
tenant: TenantType | None
|
||||
|
||||
terminations: List[CircuitTerminationType]
|
||||
|
@ -6,7 +6,7 @@ from dcim.views import PathTraceView
|
||||
from netbox.views import generic
|
||||
from tenancy.views import ObjectContactsView
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.utils import count_related
|
||||
from utilities.query import count_related
|
||||
from utilities.views import register_model_view
|
||||
from . import filtersets, forms, tables
|
||||
from .models import *
|
||||
|
@ -156,8 +156,6 @@ class NetBoxAutoSchema(AutoSchema):
|
||||
remove_fields.append(child_name)
|
||||
if isinstance(child, (ChoiceField, WritableNestedSerializer)):
|
||||
properties[child_name] = None
|
||||
elif isinstance(child, ManyRelatedField) and isinstance(child.child_relation, SerializedPKRelatedField):
|
||||
properties[child_name] = None
|
||||
|
||||
if not properties:
|
||||
return None
|
||||
|
21
netbox/core/graphql/filters.py
Normal file
21
netbox/core/graphql/filters.py
Normal file
@ -0,0 +1,21 @@
|
||||
import strawberry_django
|
||||
|
||||
from core import filtersets, models
|
||||
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
|
||||
|
||||
__all__ = (
|
||||
'DataFileFilter',
|
||||
'DataSourceFilter',
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.DataFile, lookups=True)
|
||||
@autotype_decorator(filtersets.DataFileFilterSet)
|
||||
class DataFileFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.DataSource, lookups=True)
|
||||
@autotype_decorator(filtersets.DataSourceFilterSet)
|
||||
class DataSourceFilter(BaseFilterMixin):
|
||||
pass
|
@ -1,20 +1,20 @@
|
||||
import graphene
|
||||
from typing import List
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from core import models
|
||||
from netbox.graphql.fields import ObjectField, ObjectListField
|
||||
from .types import *
|
||||
from utilities.graphql_optimizer import gql_query_optimizer
|
||||
|
||||
|
||||
class CoreQuery(graphene.ObjectType):
|
||||
data_file = ObjectField(DataFileType)
|
||||
data_file_list = ObjectListField(DataFileType)
|
||||
@strawberry.type
|
||||
class CoreQuery:
|
||||
@strawberry.field
|
||||
def data_file(self, id: int) -> DataFileType:
|
||||
return models.DataFile.objects.get(pk=id)
|
||||
data_file_list: List[DataFileType] = strawberry_django.field()
|
||||
|
||||
def resolve_data_file_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.DataFile.objects.all(), info)
|
||||
|
||||
data_source = ObjectField(DataSourceType)
|
||||
data_source_list = ObjectListField(DataSourceType)
|
||||
|
||||
def resolve_data_source_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.DataSource.objects.all(), info)
|
||||
@strawberry.field
|
||||
def data_source(self, id: int) -> DataSourceType:
|
||||
return models.DataSource.objects.get(pk=id)
|
||||
data_source_list: List[DataSourceType] = strawberry_django.field()
|
||||
|
@ -1,5 +1,11 @@
|
||||
from core import filtersets, models
|
||||
from typing import Annotated, List
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from core import models
|
||||
from netbox.graphql.types import BaseObjectType, NetBoxObjectType
|
||||
from .filters import *
|
||||
|
||||
__all__ = (
|
||||
'DataFileType',
|
||||
@ -7,15 +13,20 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.DataFile,
|
||||
exclude=['data',],
|
||||
filters=DataFileFilter
|
||||
)
|
||||
class DataFileType(BaseObjectType):
|
||||
class Meta:
|
||||
model = models.DataFile
|
||||
exclude = ('data',)
|
||||
filterset_class = filtersets.DataFileFilterSet
|
||||
source: Annotated["DataSourceType", strawberry.lazy('core.graphql.types')]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.DataSource,
|
||||
fields='__all__',
|
||||
filters=DataSourceFilter
|
||||
)
|
||||
class DataSourceType(NetBoxObjectType):
|
||||
class Meta:
|
||||
model = models.DataSource
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.DataSourceFilterSet
|
||||
|
||||
datafiles: List[Annotated["DataFileType", strawberry.lazy('core.graphql.types')]]
|
||||
|
@ -1,3 +1,4 @@
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import yaml
|
||||
@ -18,7 +19,6 @@ from netbox.constants import CENSOR_TOKEN, CENSOR_TOKEN_CHANGED
|
||||
from netbox.models import PrimaryModel
|
||||
from netbox.models.features import JobsMixin
|
||||
from netbox.registry import registry
|
||||
from utilities.files import sha256_hash
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from ..choices import *
|
||||
from ..exceptions import SyncError
|
||||
@ -357,7 +357,8 @@ class DataFile(models.Model):
|
||||
has changed.
|
||||
"""
|
||||
file_path = os.path.join(source_root, self.path)
|
||||
file_hash = sha256_hash(file_path).hexdigest()
|
||||
with open(file_path, 'rb') as f:
|
||||
file_hash = hashlib.sha256(f.read()).hexdigest()
|
||||
|
||||
# Update instance file attributes & data
|
||||
if is_modified := file_hash != self.hash:
|
||||
|
@ -89,6 +89,9 @@ class ManagedFile(SyncedDataMixin, models.Model):
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
if self.data_file and not self.file_path:
|
||||
self.file_path = os.path.basename(self.data_path)
|
||||
|
||||
# Ensure that the file root and path make a unique pair
|
||||
if self._meta.model.objects.filter(file_root=self.file_root, file_path=self.file_path).exclude(pk=self.pk).exists():
|
||||
raise ValidationError(
|
||||
|
@ -25,7 +25,8 @@ from netbox.views import generic
|
||||
from netbox.views.generic.base import BaseObjectView
|
||||
from netbox.views.generic.mixins import TableMixin
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.utils import count_related
|
||||
from utilities.htmx import htmx_partial
|
||||
from utilities.query import count_related
|
||||
from utilities.views import ContentTypePermissionRequiredMixin, register_model_view
|
||||
from . import filtersets, forms, tables
|
||||
from .models import *
|
||||
@ -320,7 +321,7 @@ class BackgroundTaskListView(TableMixin, BaseRQView):
|
||||
table = self.get_table(data, request, False)
|
||||
|
||||
# If this is an HTMX request, return only the rendered table HTML
|
||||
if request.htmx:
|
||||
if htmx_partial(request):
|
||||
return render(request, 'htmx/table.html', {
|
||||
'table': table,
|
||||
})
|
||||
@ -489,8 +490,8 @@ class WorkerListView(TableMixin, BaseRQView):
|
||||
table = self.get_table(data, request, False)
|
||||
|
||||
# If this is an HTMX request, return only the rendered table HTML
|
||||
if request.htmx:
|
||||
if request.htmx.target != 'object_list':
|
||||
if htmx_partial(request):
|
||||
if not request.htmx.target:
|
||||
table.embedded = True
|
||||
# Hide selection checkboxes
|
||||
if 'pk' in table.base_columns:
|
||||
|
@ -1,3 +1,5 @@
|
||||
import decimal
|
||||
|
||||
from django.utils.translation import gettext as _
|
||||
from rest_framework import serializers
|
||||
|
||||
@ -22,7 +24,7 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
|
||||
max_digits=4,
|
||||
decimal_places=1,
|
||||
label=_('Position (U)'),
|
||||
min_value=0,
|
||||
min_value=decimal.Decimal(0),
|
||||
default=1.0
|
||||
)
|
||||
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False, allow_null=True)
|
||||
|
@ -10,12 +10,12 @@ from extras.filtersets import LocalConfigContextFilterSet
|
||||
from extras.models import ConfigTemplate
|
||||
from ipam.filtersets import PrimaryIPFilterSet
|
||||
from ipam.models import ASN, IPAddress, VRF
|
||||
from netbox.choices import ColorChoices
|
||||
from netbox.filtersets import (
|
||||
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet,
|
||||
)
|
||||
from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
|
||||
from tenancy.models import *
|
||||
from utilities.choices import ColorChoices
|
||||
from utilities.filters import (
|
||||
ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter,
|
||||
NumericArrayFilter, TreeNodeMultipleChoiceFilter,
|
||||
|
294
netbox/dcim/graphql/filters.py
Normal file
294
netbox/dcim/graphql/filters.py
Normal file
@ -0,0 +1,294 @@
|
||||
import strawberry_django
|
||||
|
||||
from dcim import filtersets, models
|
||||
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
|
||||
|
||||
__all__ = (
|
||||
'CableFilter',
|
||||
'CableTerminationFilter',
|
||||
'ConsolePortFilter',
|
||||
'ConsolePortTemplateFilter',
|
||||
'ConsoleServerPortFilter',
|
||||
'ConsoleServerPortTemplateFilter',
|
||||
'DeviceFilter',
|
||||
'DeviceBayFilter',
|
||||
'DeviceBayTemplateFilter',
|
||||
'InventoryItemTemplateFilter',
|
||||
'DeviceRoleFilter',
|
||||
'DeviceTypeFilter',
|
||||
'FrontPortFilter',
|
||||
'FrontPortTemplateFilter',
|
||||
'InterfaceFilter',
|
||||
'InterfaceTemplateFilter',
|
||||
'InventoryItemFilter',
|
||||
'InventoryItemRoleFilter',
|
||||
'LocationFilter',
|
||||
'ManufacturerFilter',
|
||||
'ModuleFilter',
|
||||
'ModuleBayFilter',
|
||||
'ModuleBayTemplateFilter',
|
||||
'ModuleTypeFilter',
|
||||
'PlatformFilter',
|
||||
'PowerFeedFilter',
|
||||
'PowerOutletFilter',
|
||||
'PowerOutletTemplateFilter',
|
||||
'PowerPanelFilter',
|
||||
'PowerPortFilter',
|
||||
'PowerPortTemplateFilter',
|
||||
'RackFilter',
|
||||
'RackReservationFilter',
|
||||
'RackRoleFilter',
|
||||
'RearPortFilter',
|
||||
'RearPortTemplateFilter',
|
||||
'RegionFilter',
|
||||
'SiteFilter',
|
||||
'SiteGroupFilter',
|
||||
'VirtualChassisFilter',
|
||||
'VirtualDeviceContextFilter',
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Cable, lookups=True)
|
||||
@autotype_decorator(filtersets.CableFilterSet)
|
||||
class CableFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.CableTermination, lookups=True)
|
||||
@autotype_decorator(filtersets.CableTerminationFilterSet)
|
||||
class CableTerminationFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ConsolePort, lookups=True)
|
||||
@autotype_decorator(filtersets.ConsolePortFilterSet)
|
||||
class ConsolePortFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ConsolePortTemplate, lookups=True)
|
||||
@autotype_decorator(filtersets.ConsolePortTemplateFilterSet)
|
||||
class ConsolePortTemplateFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ConsoleServerPort, lookups=True)
|
||||
@autotype_decorator(filtersets.ConsoleServerPortFilterSet)
|
||||
class ConsoleServerPortFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ConsoleServerPortTemplate, lookups=True)
|
||||
@autotype_decorator(filtersets.ConsoleServerPortTemplateFilterSet)
|
||||
class ConsoleServerPortTemplateFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Device, lookups=True)
|
||||
@autotype_decorator(filtersets.DeviceFilterSet)
|
||||
class DeviceFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.DeviceBay, lookups=True)
|
||||
@autotype_decorator(filtersets.DeviceBayFilterSet)
|
||||
class DeviceBayFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.DeviceBayTemplate, lookups=True)
|
||||
@autotype_decorator(filtersets.DeviceBayTemplateFilterSet)
|
||||
class DeviceBayTemplateFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.InventoryItemTemplate, lookups=True)
|
||||
@autotype_decorator(filtersets.InventoryItemTemplateFilterSet)
|
||||
class InventoryItemTemplateFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.DeviceRole, lookups=True)
|
||||
@autotype_decorator(filtersets.DeviceRoleFilterSet)
|
||||
class DeviceRoleFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.DeviceType, lookups=True)
|
||||
@autotype_decorator(filtersets.DeviceTypeFilterSet)
|
||||
class DeviceTypeFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.FrontPort, lookups=True)
|
||||
@autotype_decorator(filtersets.FrontPortFilterSet)
|
||||
class FrontPortFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.FrontPortTemplate, lookups=True)
|
||||
@autotype_decorator(filtersets.FrontPortTemplateFilterSet)
|
||||
class FrontPortTemplateFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Interface, lookups=True)
|
||||
@autotype_decorator(filtersets.InterfaceFilterSet)
|
||||
class InterfaceFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.InterfaceTemplate, lookups=True)
|
||||
@autotype_decorator(filtersets.InterfaceTemplateFilterSet)
|
||||
class InterfaceTemplateFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.InventoryItem, lookups=True)
|
||||
@autotype_decorator(filtersets.InventoryItemFilterSet)
|
||||
class InventoryItemFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.InventoryItemRole, lookups=True)
|
||||
@autotype_decorator(filtersets.InventoryItemRoleFilterSet)
|
||||
class InventoryItemRoleFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Location, lookups=True)
|
||||
@autotype_decorator(filtersets.LocationFilterSet)
|
||||
class LocationFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Manufacturer, lookups=True)
|
||||
@autotype_decorator(filtersets.ManufacturerFilterSet)
|
||||
class ManufacturerFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Module, lookups=True)
|
||||
@autotype_decorator(filtersets.ModuleFilterSet)
|
||||
class ModuleFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ModuleBay, lookups=True)
|
||||
@autotype_decorator(filtersets.ModuleBayFilterSet)
|
||||
class ModuleBayFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ModuleBayTemplate, lookups=True)
|
||||
@autotype_decorator(filtersets.ModuleBayTemplateFilterSet)
|
||||
class ModuleBayTemplateFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ModuleType, lookups=True)
|
||||
@autotype_decorator(filtersets.ModuleTypeFilterSet)
|
||||
class ModuleTypeFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Platform, lookups=True)
|
||||
@autotype_decorator(filtersets.PlatformFilterSet)
|
||||
class PlatformFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.PowerFeed, lookups=True)
|
||||
@autotype_decorator(filtersets.PowerFeedFilterSet)
|
||||
class PowerFeedFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.PowerOutlet, lookups=True)
|
||||
@autotype_decorator(filtersets.PowerOutletFilterSet)
|
||||
class PowerOutletFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.PowerOutletTemplate, lookups=True)
|
||||
@autotype_decorator(filtersets.PowerOutletTemplateFilterSet)
|
||||
class PowerOutletTemplateFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.PowerPanel, lookups=True)
|
||||
@autotype_decorator(filtersets.PowerPanelFilterSet)
|
||||
class PowerPanelFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.PowerPort, lookups=True)
|
||||
@autotype_decorator(filtersets.PowerPortFilterSet)
|
||||
class PowerPortFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.PowerPortTemplate, lookups=True)
|
||||
@autotype_decorator(filtersets.PowerPortTemplateFilterSet)
|
||||
class PowerPortTemplateFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Rack, lookups=True)
|
||||
@autotype_decorator(filtersets.RackFilterSet)
|
||||
class RackFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.RackReservation, lookups=True)
|
||||
@autotype_decorator(filtersets.RackReservationFilterSet)
|
||||
class RackReservationFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.RackRole, lookups=True)
|
||||
@autotype_decorator(filtersets.RackRoleFilterSet)
|
||||
class RackRoleFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.RearPort, lookups=True)
|
||||
@autotype_decorator(filtersets.RearPortFilterSet)
|
||||
class RearPortFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.RearPortTemplate, lookups=True)
|
||||
@autotype_decorator(filtersets.RearPortTemplateFilterSet)
|
||||
class RearPortTemplateFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Region, lookups=True)
|
||||
@autotype_decorator(filtersets.RegionFilterSet)
|
||||
class RegionFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Site, lookups=True)
|
||||
@autotype_decorator(filtersets.SiteFilterSet)
|
||||
class SiteFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.SiteGroup, lookups=True)
|
||||
@autotype_decorator(filtersets.SiteGroupFilterSet)
|
||||
class SiteGroupFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VirtualChassis, lookups=True)
|
||||
@autotype_decorator(filtersets.VirtualChassisFilterSet)
|
||||
class VirtualChassisFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VirtualDeviceContext, lookups=True)
|
||||
@autotype_decorator(filtersets.VirtualDeviceContextFilterSet)
|
||||
class VirtualDeviceContextFilter(BaseFilterMixin):
|
||||
pass
|
@ -1,4 +1,3 @@
|
||||
import graphene
|
||||
from circuits.graphql.types import CircuitTerminationType, ProviderNetworkType
|
||||
from circuits.models import CircuitTermination, ProviderNetwork
|
||||
from dcim.graphql.types import (
|
||||
@ -37,79 +36,7 @@ from dcim.models import (
|
||||
)
|
||||
|
||||
|
||||
class LinkPeerType(graphene.Union):
|
||||
class Meta:
|
||||
types = (
|
||||
CircuitTerminationType,
|
||||
ConsolePortType,
|
||||
ConsoleServerPortType,
|
||||
FrontPortType,
|
||||
InterfaceType,
|
||||
PowerFeedType,
|
||||
PowerOutletType,
|
||||
PowerPortType,
|
||||
RearPortType,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def resolve_type(cls, instance, info):
|
||||
if type(instance) is CircuitTermination:
|
||||
return CircuitTerminationType
|
||||
if type(instance) is ConsolePortType:
|
||||
return ConsolePortType
|
||||
if type(instance) is ConsoleServerPort:
|
||||
return ConsoleServerPortType
|
||||
if type(instance) is FrontPort:
|
||||
return FrontPortType
|
||||
if type(instance) is Interface:
|
||||
return InterfaceType
|
||||
if type(instance) is PowerFeed:
|
||||
return PowerFeedType
|
||||
if type(instance) is PowerOutlet:
|
||||
return PowerOutletType
|
||||
if type(instance) is PowerPort:
|
||||
return PowerPortType
|
||||
if type(instance) is RearPort:
|
||||
return RearPortType
|
||||
|
||||
|
||||
class CableTerminationTerminationType(graphene.Union):
|
||||
class Meta:
|
||||
types = (
|
||||
CircuitTerminationType,
|
||||
ConsolePortType,
|
||||
ConsoleServerPortType,
|
||||
FrontPortType,
|
||||
InterfaceType,
|
||||
PowerFeedType,
|
||||
PowerOutletType,
|
||||
PowerPortType,
|
||||
RearPortType,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def resolve_type(cls, instance, info):
|
||||
if type(instance) is CircuitTermination:
|
||||
return CircuitTerminationType
|
||||
if type(instance) is ConsolePortType:
|
||||
return ConsolePortType
|
||||
if type(instance) is ConsoleServerPort:
|
||||
return ConsoleServerPortType
|
||||
if type(instance) is FrontPort:
|
||||
return FrontPortType
|
||||
if type(instance) is Interface:
|
||||
return InterfaceType
|
||||
if type(instance) is PowerFeed:
|
||||
return PowerFeedType
|
||||
if type(instance) is PowerOutlet:
|
||||
return PowerOutletType
|
||||
if type(instance) is PowerPort:
|
||||
return PowerPortType
|
||||
if type(instance) is RearPort:
|
||||
return RearPortType
|
||||
|
||||
|
||||
class InventoryItemTemplateComponentType(graphene.Union):
|
||||
class InventoryItemTemplateComponentType:
|
||||
class Meta:
|
||||
types = (
|
||||
ConsolePortTemplateType,
|
||||
@ -139,7 +66,7 @@ class InventoryItemTemplateComponentType(graphene.Union):
|
||||
return RearPortTemplateType
|
||||
|
||||
|
||||
class InventoryItemComponentType(graphene.Union):
|
||||
class InventoryItemComponentType:
|
||||
class Meta:
|
||||
types = (
|
||||
ConsolePortType,
|
||||
@ -169,7 +96,7 @@ class InventoryItemComponentType(graphene.Union):
|
||||
return RearPortType
|
||||
|
||||
|
||||
class ConnectedEndpointType(graphene.Union):
|
||||
class ConnectedEndpointType:
|
||||
class Meta:
|
||||
types = (
|
||||
CircuitTerminationType,
|
||||
|
@ -1,20 +1,43 @@
|
||||
import graphene
|
||||
from typing import Annotated, List, Union
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
__all__ = (
|
||||
'CabledObjectMixin',
|
||||
'PathEndpointMixin',
|
||||
)
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class CabledObjectMixin:
|
||||
link_peers = graphene.List('dcim.graphql.gfk_mixins.LinkPeerType')
|
||||
cable: Annotated["CableType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
|
||||
def resolve_cable_end(self, info):
|
||||
# Handle empty values
|
||||
return self.cable_end or None
|
||||
|
||||
def resolve_link_peers(self, info):
|
||||
return self.link_peers
|
||||
link_peers: List[Annotated[Union[
|
||||
Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')],
|
||||
Annotated["ConsolePortType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["ConsoleServerPortType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["PowerFeedType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["PowerOutletType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')],
|
||||
], strawberry.union("LinkPeerType")]]
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class PathEndpointMixin:
|
||||
connected_endpoints = graphene.List('dcim.graphql.gfk_mixins.ConnectedEndpointType')
|
||||
|
||||
def resolve_connected_endpoints(self, info):
|
||||
# Handle empty values
|
||||
return self.connected_endpoints or None
|
||||
connected_endpoints: List[Annotated[Union[
|
||||
Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')],
|
||||
Annotated["ConsolePortType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["ConsoleServerPortType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["PowerFeedType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["PowerOutletType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')],
|
||||
Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')],
|
||||
], strawberry.union("ConnectedEndpointType")]]
|
||||
|
@ -1,249 +1,210 @@
|
||||
import graphene
|
||||
from typing import List
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from netbox.graphql.fields import ObjectField, ObjectListField
|
||||
from .types import *
|
||||
from dcim import models
|
||||
from .types import VirtualDeviceContextType
|
||||
from utilities.graphql_optimizer import gql_query_optimizer
|
||||
|
||||
|
||||
class DCIMQuery(graphene.ObjectType):
|
||||
cable = ObjectField(CableType)
|
||||
cable_list = ObjectListField(CableType)
|
||||
|
||||
def resolve_cable_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.Cable.objects.all(), info)
|
||||
|
||||
console_port = ObjectField(ConsolePortType)
|
||||
console_port_list = ObjectListField(ConsolePortType)
|
||||
|
||||
def resolve_console_port_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.ConsolePort.objects.all(), info)
|
||||
|
||||
console_port_template = ObjectField(ConsolePortTemplateType)
|
||||
console_port_template_list = ObjectListField(ConsolePortTemplateType)
|
||||
|
||||
def resolve_console_port_template_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.ConsolePortTemplate.objects.all(), info)
|
||||
|
||||
console_server_port = ObjectField(ConsoleServerPortType)
|
||||
console_server_port_list = ObjectListField(ConsoleServerPortType)
|
||||
|
||||
def resolve_console_server_port_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.ConsoleServerPort.objects.all(), info)
|
||||
|
||||
console_server_port_template = ObjectField(ConsoleServerPortTemplateType)
|
||||
console_server_port_template_list = ObjectListField(ConsoleServerPortTemplateType)
|
||||
|
||||
def resolve_console_server_port_template_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.ConsoleServerPortTemplate.objects.all(), info)
|
||||
|
||||
device = ObjectField(DeviceType)
|
||||
device_list = ObjectListField(DeviceType)
|
||||
|
||||
def resolve_device_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.Device.objects.all(), info)
|
||||
|
||||
device_bay = ObjectField(DeviceBayType)
|
||||
device_bay_list = ObjectListField(DeviceBayType)
|
||||
|
||||
def resolve_device_bay_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.DeviceBay.objects.all(), info)
|
||||
|
||||
device_bay_template = ObjectField(DeviceBayTemplateType)
|
||||
device_bay_template_list = ObjectListField(DeviceBayTemplateType)
|
||||
|
||||
def resolve_device_bay_template_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.DeviceBayTemplate.objects.all(), info)
|
||||
|
||||
device_role = ObjectField(DeviceRoleType)
|
||||
device_role_list = ObjectListField(DeviceRoleType)
|
||||
|
||||
def resolve_device_role_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.DeviceRole.objects.all(), info)
|
||||
|
||||
device_type = ObjectField(DeviceTypeType)
|
||||
device_type_list = ObjectListField(DeviceTypeType)
|
||||
|
||||
def resolve_device_type_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.DeviceType.objects.all(), info)
|
||||
|
||||
front_port = ObjectField(FrontPortType)
|
||||
front_port_list = ObjectListField(FrontPortType)
|
||||
|
||||
def resolve_front_port_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.FrontPort.objects.all(), info)
|
||||
|
||||
front_port_template = ObjectField(FrontPortTemplateType)
|
||||
front_port_template_list = ObjectListField(FrontPortTemplateType)
|
||||
|
||||
def resolve_front_port_template_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.FrontPortTemplate.objects.all(), info)
|
||||
|
||||
interface = ObjectField(InterfaceType)
|
||||
interface_list = ObjectListField(InterfaceType)
|
||||
|
||||
def resolve_interface_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.Interface.objects.all(), info)
|
||||
|
||||
interface_template = ObjectField(InterfaceTemplateType)
|
||||
interface_template_list = ObjectListField(InterfaceTemplateType)
|
||||
|
||||
def resolve_interface_template_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.InterfaceTemplate.objects.all(), info)
|
||||
|
||||
inventory_item = ObjectField(InventoryItemType)
|
||||
inventory_item_list = ObjectListField(InventoryItemType)
|
||||
|
||||
def resolve_inventory_item_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.InventoryItem.objects.all(), info)
|
||||
|
||||
inventory_item_role = ObjectField(InventoryItemRoleType)
|
||||
inventory_item_role_list = ObjectListField(InventoryItemRoleType)
|
||||
|
||||
def resolve_inventory_item_role_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.InventoryItemRole.objects.all(), info)
|
||||
|
||||
inventory_item_template = ObjectField(InventoryItemTemplateType)
|
||||
inventory_item_template_list = ObjectListField(InventoryItemTemplateType)
|
||||
|
||||
def resolve_inventory_item_template_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.InventoryItemTemplate.objects.all(), info)
|
||||
|
||||
location = ObjectField(LocationType)
|
||||
location_list = ObjectListField(LocationType)
|
||||
|
||||
def resolve_location_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.Location.objects.all(), info)
|
||||
|
||||
manufacturer = ObjectField(ManufacturerType)
|
||||
manufacturer_list = ObjectListField(ManufacturerType)
|
||||
|
||||
def resolve_manufacturer_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.Manufacturer.objects.all(), info)
|
||||
|
||||
module = ObjectField(ModuleType)
|
||||
module_list = ObjectListField(ModuleType)
|
||||
|
||||
def resolve_module_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.Module.objects.all(), info)
|
||||
|
||||
module_bay = ObjectField(ModuleBayType)
|
||||
module_bay_list = ObjectListField(ModuleBayType)
|
||||
|
||||
def resolve_module_bay_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.ModuleBay.objects.all(), info)
|
||||
|
||||
module_bay_template = ObjectField(ModuleBayTemplateType)
|
||||
module_bay_template_list = ObjectListField(ModuleBayTemplateType)
|
||||
|
||||
def resolve_module_bay_template_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.ModuleBayTemplate.objects.all(), info)
|
||||
|
||||
module_type = ObjectField(ModuleTypeType)
|
||||
module_type_list = ObjectListField(ModuleTypeType)
|
||||
|
||||
def resolve_module_type_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.ModuleType.objects.all(), info)
|
||||
|
||||
platform = ObjectField(PlatformType)
|
||||
platform_list = ObjectListField(PlatformType)
|
||||
|
||||
def resolve_platform_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.Platform.objects.all(), info)
|
||||
|
||||
power_feed = ObjectField(PowerFeedType)
|
||||
power_feed_list = ObjectListField(PowerFeedType)
|
||||
|
||||
def resolve_power_feed_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.PowerFeed.objects.all(), info)
|
||||
|
||||
power_outlet = ObjectField(PowerOutletType)
|
||||
power_outlet_list = ObjectListField(PowerOutletType)
|
||||
|
||||
def resolve_power_outlet_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.PowerOutlet.objects.all(), info)
|
||||
|
||||
power_outlet_template = ObjectField(PowerOutletTemplateType)
|
||||
power_outlet_template_list = ObjectListField(PowerOutletTemplateType)
|
||||
|
||||
def resolve_power_outlet_template_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.PowerOutletTemplate.objects.all(), info)
|
||||
|
||||
power_panel = ObjectField(PowerPanelType)
|
||||
power_panel_list = ObjectListField(PowerPanelType)
|
||||
|
||||
def resolve_power_panel_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.PowerPanel.objects.all(), info)
|
||||
|
||||
power_port = ObjectField(PowerPortType)
|
||||
power_port_list = ObjectListField(PowerPortType)
|
||||
|
||||
def resolve_power_port_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.PowerPort.objects.all(), info)
|
||||
|
||||
power_port_template = ObjectField(PowerPortTemplateType)
|
||||
power_port_template_list = ObjectListField(PowerPortTemplateType)
|
||||
|
||||
def resolve_power_port_template_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.PowerPortTemplate.objects.all(), info)
|
||||
|
||||
rack = ObjectField(RackType)
|
||||
rack_list = ObjectListField(RackType)
|
||||
|
||||
def resolve_rack_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.Rack.objects.all(), info)
|
||||
|
||||
rack_reservation = ObjectField(RackReservationType)
|
||||
rack_reservation_list = ObjectListField(RackReservationType)
|
||||
|
||||
def resolve_rack_reservation_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.RackReservation.objects.all(), info)
|
||||
|
||||
rack_role = ObjectField(RackRoleType)
|
||||
rack_role_list = ObjectListField(RackRoleType)
|
||||
|
||||
def resolve_rack_role_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.RackRole.objects.all(), info)
|
||||
|
||||
rear_port = ObjectField(RearPortType)
|
||||
rear_port_list = ObjectListField(RearPortType)
|
||||
|
||||
def resolve_rear_port_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.RearPort.objects.all(), info)
|
||||
|
||||
rear_port_template = ObjectField(RearPortTemplateType)
|
||||
rear_port_template_list = ObjectListField(RearPortTemplateType)
|
||||
|
||||
def resolve_rear_port_template_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.RearPortTemplate.objects.all(), info)
|
||||
|
||||
region = ObjectField(RegionType)
|
||||
region_list = ObjectListField(RegionType)
|
||||
|
||||
def resolve_region_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.Region.objects.all(), info)
|
||||
|
||||
site = ObjectField(SiteType)
|
||||
site_list = ObjectListField(SiteType)
|
||||
|
||||
def resolve_site_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.Site.objects.all(), info)
|
||||
|
||||
site_group = ObjectField(SiteGroupType)
|
||||
site_group_list = ObjectListField(SiteGroupType)
|
||||
|
||||
def resolve_site_group_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.SiteGroup.objects.all(), info)
|
||||
|
||||
virtual_chassis = ObjectField(VirtualChassisType)
|
||||
virtual_chassis_list = ObjectListField(VirtualChassisType)
|
||||
|
||||
def resolve_virtual_chassis_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.VirtualChassis.objects.all(), info)
|
||||
|
||||
virtual_device_context = ObjectField(VirtualDeviceContextType)
|
||||
virtual_device_context_list = ObjectListField(VirtualDeviceContextType)
|
||||
|
||||
def resolve_virtual_device_context_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.VirtualDeviceContext.objects.all(), info)
|
||||
from .types import *
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class DCIMQuery:
|
||||
@strawberry.field
|
||||
def cable(self, id: int) -> CableType:
|
||||
return models.Cable.objects.get(pk=id)
|
||||
cable_list: List[CableType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def console_port(self, id: int) -> ConsolePortType:
|
||||
return models.ConsolePort.objects.get(pk=id)
|
||||
console_port_list: List[ConsolePortType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def console_port_template(self, id: int) -> ConsolePortTemplateType:
|
||||
return models.ConsolePortTemplate.objects.get(pk=id)
|
||||
console_port_template_list: List[ConsolePortTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def console_server_port(self, id: int) -> ConsoleServerPortType:
|
||||
return models.ConsoleServerPort.objects.get(pk=id)
|
||||
console_server_port_list: List[ConsoleServerPortType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def console_server_port_template(self, id: int) -> ConsoleServerPortTemplateType:
|
||||
return models.ConsoleServerPortTemplate.objects.get(pk=id)
|
||||
console_server_port_template_list: List[ConsoleServerPortTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def device(self, id: int) -> DeviceType:
|
||||
return models.Device.objects.get(pk=id)
|
||||
device_list: List[DeviceType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def device_bay(self, id: int) -> DeviceBayType:
|
||||
return models.DeviceBay.objects.get(pk=id)
|
||||
device_bay_list: List[DeviceBayType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def device_bay_template(self, id: int) -> DeviceBayTemplateType:
|
||||
return models.DeviceBayTemplate.objects.get(pk=id)
|
||||
device_bay_template_list: List[DeviceBayTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def device_role(self, id: int) -> DeviceRoleType:
|
||||
return models.DeviceRole.objects.get(pk=id)
|
||||
device_role_list: List[DeviceRoleType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def device_type(self, id: int) -> DeviceTypeType:
|
||||
return models.DeviceType.objects.get(pk=id)
|
||||
device_type_list: List[DeviceTypeType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def front_port(self, id: int) -> FrontPortType:
|
||||
return models.FrontPort.objects.get(pk=id)
|
||||
front_port_list: List[FrontPortType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def front_port_template(self, id: int) -> FrontPortTemplateType:
|
||||
return models.FrontPortTemplate.objects.get(pk=id)
|
||||
front_port_template_list: List[FrontPortTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def interface(self, id: int) -> InterfaceType:
|
||||
return models.Interface.objects.get(pk=id)
|
||||
interface_list: List[InterfaceType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def interface_template(self, id: int) -> InterfaceTemplateType:
|
||||
return models.InterfaceTemplate.objects.get(pk=id)
|
||||
interface_template_list: List[InterfaceTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def inventory_item(self, id: int) -> InventoryItemType:
|
||||
return models.InventoryItem.objects.get(pk=id)
|
||||
inventory_item_list: List[InventoryItemType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def inventory_item_role(self, id: int) -> InventoryItemRoleType:
|
||||
return models.InventoryItemRole.objects.get(pk=id)
|
||||
inventory_item_role_list: List[InventoryItemRoleType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def inventory_item_template(self, id: int) -> InventoryItemTemplateType:
|
||||
return models.InventoryItemTemplate.objects.get(pk=id)
|
||||
inventory_item_template_list: List[InventoryItemTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def location(self, id: int) -> LocationType:
|
||||
return models.Location.objects.get(pk=id)
|
||||
location_list: List[LocationType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def manufacturer(self, id: int) -> ManufacturerType:
|
||||
return models.Manufacturer.objects.get(pk=id)
|
||||
manufacturer_list: List[ManufacturerType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def module(self, id: int) -> ModuleType:
|
||||
return models.Module.objects.get(pk=id)
|
||||
module_list: List[ModuleType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def module_bay(self, id: int) -> ModuleBayType:
|
||||
return models.ModuleBay.objects.get(pk=id)
|
||||
module_bay_list: List[ModuleBayType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def module_bay_template(self, id: int) -> ModuleBayTemplateType:
|
||||
return models.ModuleBayTemplate.objects.get(pk=id)
|
||||
module_bay_template_list: List[ModuleBayTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def module_type(self, id: int) -> ModuleTypeType:
|
||||
return models.ModuleType.objects.get(pk=id)
|
||||
module_type_list: List[ModuleTypeType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def platform(self, id: int) -> PlatformType:
|
||||
return models.Platform.objects.get(pk=id)
|
||||
platform_list: List[PlatformType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def power_feed(self, id: int) -> PowerFeedType:
|
||||
return models.PowerFeed.objects.get(pk=id)
|
||||
power_feed_list: List[PowerFeedType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def power_outlet(self, id: int) -> PowerOutletType:
|
||||
return models.PowerOutlet.objects.get(pk=id)
|
||||
power_outlet_list: List[PowerOutletType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def power_outlet_template(self, id: int) -> PowerOutletTemplateType:
|
||||
return models.PowerOutletTemplate.objects.get(pk=id)
|
||||
power_outlet_template_list: List[PowerOutletTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def power_panel(self, id: int) -> PowerPanelType:
|
||||
return models.PowerPanel.objects.get(id=id)
|
||||
power_panel_list: List[PowerPanelType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def power_port(self, id: int) -> PowerPortType:
|
||||
return models.PowerPort.objects.get(id=id)
|
||||
power_port_list: List[PowerPortType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def power_port_template(self, id: int) -> PowerPortTemplateType:
|
||||
return models.PowerPortTemplate.objects.get(id=id)
|
||||
power_port_template_list: List[PowerPortTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def rack(self, id: int) -> RackType:
|
||||
return models.Rack.objects.get(id=id)
|
||||
rack_list: List[RackType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def rack_reservation(self, id: int) -> RackReservationType:
|
||||
return models.RackReservation.objects.get(id=id)
|
||||
rack_reservation_list: List[RackReservationType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def rack_role(self, id: int) -> RackRoleType:
|
||||
return models.RackRole.objects.get(id=id)
|
||||
rack_role_list: List[RackRoleType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def rear_port(self, id: int) -> RearPortType:
|
||||
return models.RearPort.objects.get(id=id)
|
||||
rear_port_list: List[RearPortType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def rear_port_template(self, id: int) -> RearPortTemplateType:
|
||||
return models.RearPortTemplate.objects.get(id=id)
|
||||
rear_port_template_list: List[RearPortTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def region(self, id: int) -> RegionType:
|
||||
return models.Region.objects.get(id=id)
|
||||
region_list: List[RegionType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def site(self, id: int) -> SiteType:
|
||||
return models.Site.objects.get(id=id)
|
||||
site_list: List[SiteType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def site_group(self, id: int) -> SiteGroupType:
|
||||
return models.SiteGroup.objects.get(id=id)
|
||||
site_group_list: List[SiteGroupType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def virtual_chassis(self, id: int) -> VirtualChassisType:
|
||||
return models.VirtualChassis.objects.get(id=id)
|
||||
virtual_chassis_list: List[VirtualChassisType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def virtual_device_context(self, id: int) -> VirtualDeviceContextType:
|
||||
return models.VirtualDeviceContext.objects.get(id=id)
|
||||
virtual_device_context_list: List[VirtualDeviceContextType] = strawberry_django.field()
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -15,9 +15,9 @@ from dcim.constants import *
|
||||
from dcim.fields import PathField
|
||||
from dcim.utils import decompile_path_node, object_to_path_node
|
||||
from netbox.models import ChangeLoggedModel, PrimaryModel
|
||||
from utilities.conversion import to_meters
|
||||
from utilities.fields import ColorField
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.utils import to_meters
|
||||
from wireless.models import WirelessLink
|
||||
from .device_components import FrontPort, RearPort, PathEndpoint
|
||||
|
||||
|
@ -12,8 +12,8 @@ from mptt.models import MPTTModel, TreeForeignKey
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.fields import MACAddressField, WWNField
|
||||
from netbox.choices import ColorChoices
|
||||
from netbox.models import OrganizationalModel, NetBoxModel
|
||||
from utilities.choices import ColorChoices
|
||||
from utilities.fields import ColorField, NaturalOrderingField
|
||||
from utilities.mptt import TreeManager
|
||||
from utilities.ordering import naturalize_interface
|
||||
|
@ -18,10 +18,10 @@ from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from extras.models import ConfigContextModel, CustomField
|
||||
from extras.querysets import ConfigContextModelQuerySet
|
||||
from netbox.choices import ColorChoices
|
||||
from netbox.config import ConfigItem
|
||||
from netbox.models import OrganizationalModel, PrimaryModel
|
||||
from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
|
||||
from utilities.choices import ColorChoices
|
||||
from utilities.fields import ColorField, CounterCacheField, NaturalOrderingField
|
||||
from utilities.tracking import TrackingModelMixin
|
||||
from .device_components import *
|
||||
|
@ -2,7 +2,7 @@ from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from dcim.choices import *
|
||||
from utilities.utils import to_grams
|
||||
from utilities.conversion import to_grams
|
||||
|
||||
__all__ = (
|
||||
'RenderConfigMixin',
|
||||
|
@ -14,11 +14,12 @@ from django.utils.translation import gettext_lazy as _
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.svg import RackElevationSVG
|
||||
from netbox.choices import ColorChoices
|
||||
from netbox.models import OrganizationalModel, PrimaryModel
|
||||
from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
|
||||
from utilities.choices import ColorChoices
|
||||
from utilities.conversion import to_grams
|
||||
from utilities.data import array_to_string, drange
|
||||
from utilities.fields import ColorField, NaturalOrderingField
|
||||
from utilities.utils import array_to_string, drange, to_grams
|
||||
from .device_components import PowerPort
|
||||
from .devices import Device, Module
|
||||
from .mixins import WeightMixin
|
||||
|
@ -6,7 +6,7 @@ from svgwrite.text import Text
|
||||
from django.conf import settings
|
||||
|
||||
from dcim.constants import CABLE_TRACE_SVG_DEFAULT_WIDTH
|
||||
from utilities.utils import foreground_color
|
||||
from utilities.html import foreground_color
|
||||
|
||||
|
||||
__all__ = (
|
||||
|
@ -14,7 +14,8 @@ from django.urls import reverse
|
||||
from django.utils.http import urlencode
|
||||
|
||||
from netbox.config import get_config
|
||||
from utilities.utils import foreground_color, array_to_ranges
|
||||
from utilities.data import array_to_ranges
|
||||
from utilities.html import foreground_color
|
||||
from dcim.constants import RACK_ELEVATION_BORDER_WIDTH
|
||||
|
||||
|
||||
|
@ -6,13 +6,12 @@ from dcim.choices import *
|
||||
from dcim.filtersets import *
|
||||
from dcim.models import *
|
||||
from ipam.models import ASN, IPAddress, RIR, VRF
|
||||
from netbox.choices import ColorChoices
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.choices import ColorChoices
|
||||
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device
|
||||
from virtualization.models import Cluster, ClusterType
|
||||
from wireless.choices import WirelessChannelChoices, WirelessRoleChoices
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
|
@ -7,7 +7,7 @@ from dcim.choices import *
|
||||
from dcim.models import *
|
||||
from extras.models import CustomField
|
||||
from tenancy.models import Tenant
|
||||
from utilities.utils import drange
|
||||
from utilities.data import drange
|
||||
|
||||
|
||||
class LocationTestCase(TestCase):
|
||||
|
@ -11,12 +11,11 @@ from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import *
|
||||
from ipam.models import ASN, RIR, VLAN, VRF
|
||||
from netbox.choices import CSVDelimiterChoices, ImportFormatChoices
|
||||
from tenancy.models import Tenant
|
||||
from utilities.choices import CSVDelimiterChoices, ImportFormatChoices
|
||||
from utilities.testing import ViewTestCases, create_tags, create_test_device, post_data
|
||||
from wireless.models import WirelessLAN
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
|
@ -25,8 +25,8 @@ from tenancy.views import ObjectContactsView
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
||||
from utilities.permissions import get_permission_for_model
|
||||
from utilities.query import count_related
|
||||
from utilities.query_functions import CollateAsChar
|
||||
from utilities.utils import count_related
|
||||
from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin, ViewTab, register_model_view
|
||||
from virtualization.models import VirtualMachine
|
||||
from . import filtersets, forms, tables
|
||||
|
@ -20,7 +20,7 @@ from netbox.api.metadata import ContentTypeMetadata
|
||||
from netbox.api.renderers import TextRenderer
|
||||
from netbox.api.viewsets import NetBoxModelViewSet
|
||||
from utilities.exceptions import RQWorkerNotRunningException
|
||||
from utilities.utils import copy_safe_request
|
||||
from utilities.request import copy_safe_request
|
||||
from . import serializers
|
||||
from .mixins import ConfigTemplateRenderMixin
|
||||
|
||||
|
@ -2,7 +2,8 @@ import logging
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from utilities.choices import ButtonColorChoices, ChoiceSet
|
||||
from netbox.choices import ButtonColorChoices
|
||||
from utilities.choices import ChoiceSet
|
||||
|
||||
|
||||
#
|
||||
|
@ -14,10 +14,12 @@ from django.utils.translation import gettext as _
|
||||
|
||||
from core.models import ObjectType
|
||||
from extras.choices import BookmarkOrderingChoices
|
||||
from utilities.choices import ButtonColorChoices
|
||||
from netbox.choices import ButtonColorChoices
|
||||
from utilities.object_types import object_type_identifier, object_type_name
|
||||
from utilities.permissions import get_permission_for_model
|
||||
from utilities.querydict import dict_to_querydict
|
||||
from utilities.templatetags.builtins.filters import render_markdown
|
||||
from utilities.utils import content_type_identifier, content_type_name, dict_to_querydict, get_viewname
|
||||
from utilities.views import get_viewname
|
||||
from .utils import register_widget
|
||||
|
||||
__all__ = (
|
||||
@ -33,15 +35,15 @@ __all__ = (
|
||||
|
||||
def get_object_type_choices():
|
||||
return [
|
||||
(content_type_identifier(ct), content_type_name(ct))
|
||||
for ct in ObjectType.objects.public().order_by('app_label', 'model')
|
||||
(object_type_identifier(ot), object_type_name(ot))
|
||||
for ot in ObjectType.objects.public().order_by('app_label', 'model')
|
||||
]
|
||||
|
||||
|
||||
def get_bookmarks_object_type_choices():
|
||||
return [
|
||||
(content_type_identifier(ct), content_type_name(ct))
|
||||
for ct in ObjectType.objects.with_feature('bookmarks').order_by('app_label', 'model')
|
||||
(object_type_identifier(ot), object_type_name(ot))
|
||||
for ot in ObjectType.objects.with_feature('bookmarks').order_by('app_label', 'model')
|
||||
]
|
||||
|
||||
|
||||
|
@ -1,9 +1,6 @@
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.utils import timezone
|
||||
from django.utils.module_loading import import_string
|
||||
from django.utils.translation import gettext as _
|
||||
@ -15,9 +12,9 @@ from netbox.constants import RQ_QUEUE_DEFAULT
|
||||
from netbox.registry import registry
|
||||
from utilities.api import get_serializer_for_model
|
||||
from utilities.rqworker import get_rq_retry
|
||||
from utilities.utils import serialize_object
|
||||
from utilities.serialization import serialize_object
|
||||
from .choices import *
|
||||
from .models import EventRule, ScriptModule
|
||||
from .models import EventRule
|
||||
|
||||
logger = logging.getLogger('netbox.events_processor')
|
||||
|
||||
|
@ -3,7 +3,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from extras.choices import DurationChoices
|
||||
from utilities.forms.widgets import DateTimePicker, NumberWithOptions
|
||||
from utilities.utils import local_now
|
||||
from utilities.datetime import local_now
|
||||
|
||||
__all__ = (
|
||||
'ReportForm',
|
||||
|
@ -3,7 +3,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from extras.choices import DurationChoices
|
||||
from utilities.forms.widgets import DateTimePicker, NumberWithOptions
|
||||
from utilities.utils import local_now
|
||||
from utilities.datetime import local_now
|
||||
|
||||
__all__ = (
|
||||
'ScriptForm',
|
||||
|
98
netbox/extras/graphql/filters.py
Normal file
98
netbox/extras/graphql/filters.py
Normal file
@ -0,0 +1,98 @@
|
||||
import strawberry_django
|
||||
|
||||
from extras import filtersets, models
|
||||
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
|
||||
|
||||
__all__ = (
|
||||
'ConfigContextFilter',
|
||||
'ConfigTemplateFilter',
|
||||
'CustomFieldFilter',
|
||||
'CustomFieldChoiceSetFilter',
|
||||
'CustomLinkFilter',
|
||||
'EventRuleFilter',
|
||||
'ExportTemplateFilter',
|
||||
'ImageAttachmentFilter',
|
||||
'JournalEntryFilter',
|
||||
'ObjectChangeFilter',
|
||||
'SavedFilterFilter',
|
||||
'TagFilter',
|
||||
'WebhookFilter',
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ConfigContext, lookups=True)
|
||||
@autotype_decorator(filtersets.ConfigContextFilterSet)
|
||||
class ConfigContextFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ConfigTemplate, lookups=True)
|
||||
@autotype_decorator(filtersets.ConfigTemplateFilterSet)
|
||||
class ConfigTemplateFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.CustomField, lookups=True)
|
||||
@autotype_decorator(filtersets.CustomFieldFilterSet)
|
||||
class CustomFieldFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.CustomFieldChoiceSet, lookups=True)
|
||||
@autotype_decorator(filtersets.CustomFieldChoiceSetFilterSet)
|
||||
class CustomFieldChoiceSetFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.CustomLink, lookups=True)
|
||||
@autotype_decorator(filtersets.CustomLinkFilterSet)
|
||||
class CustomLinkFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ExportTemplate, lookups=True)
|
||||
@autotype_decorator(filtersets.ExportTemplateFilterSet)
|
||||
class ExportTemplateFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ImageAttachment, lookups=True)
|
||||
@autotype_decorator(filtersets.ImageAttachmentFilterSet)
|
||||
class ImageAttachmentFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.JournalEntry, lookups=True)
|
||||
@autotype_decorator(filtersets.JournalEntryFilterSet)
|
||||
class JournalEntryFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ObjectChange, lookups=True)
|
||||
@autotype_decorator(filtersets.ObjectChangeFilterSet)
|
||||
class ObjectChangeFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.SavedFilter, lookups=True)
|
||||
@autotype_decorator(filtersets.SavedFilterFilterSet)
|
||||
class SavedFilterFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Tag, lookups=True)
|
||||
@autotype_decorator(filtersets.TagFilterSet)
|
||||
class TagFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Webhook, lookups=True)
|
||||
@autotype_decorator(filtersets.WebhookFilterSet)
|
||||
class WebhookFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.EventRule, lookups=True)
|
||||
@autotype_decorator(filtersets.EventRuleFilterSet)
|
||||
class EventRuleFilter(BaseFilterMixin):
|
||||
pass
|
@ -1,6 +1,8 @@
|
||||
import graphene
|
||||
from typing import TYPE_CHECKING, Annotated, List
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from graphene.types.generic import GenericScalar
|
||||
|
||||
from extras.models import ObjectChange
|
||||
|
||||
@ -14,56 +16,63 @@ __all__ = (
|
||||
'TagsMixin',
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .types import ImageAttachmentType, JournalEntryType, ObjectChangeType, TagType
|
||||
from tenancy.graphql.types import ContactAssignmentType
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class ChangelogMixin:
|
||||
changelog = graphene.List('extras.graphql.types.ObjectChangeType')
|
||||
|
||||
def resolve_changelog(self, info):
|
||||
@strawberry_django.field
|
||||
def changelog(self, info) -> List[Annotated["ObjectChangeType", strawberry.lazy('.types')]]:
|
||||
content_type = ContentType.objects.get_for_model(self)
|
||||
object_changes = ObjectChange.objects.filter(
|
||||
changed_object_type=content_type,
|
||||
changed_object_id=self.pk
|
||||
)
|
||||
return object_changes.restrict(info.context.user, 'view')
|
||||
return object_changes.restrict(info.context.request.user, 'view')
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class ConfigContextMixin:
|
||||
config_context = GenericScalar()
|
||||
|
||||
def resolve_config_context(self, info):
|
||||
@strawberry_django.field
|
||||
def config_context(self) -> strawberry.scalars.JSON:
|
||||
return self.get_config_context()
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class CustomFieldsMixin:
|
||||
custom_fields = GenericScalar()
|
||||
|
||||
def resolve_custom_fields(self, info):
|
||||
@strawberry_django.field
|
||||
def custom_fields(self) -> strawberry.scalars.JSON:
|
||||
return self.custom_field_data
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class ImageAttachmentsMixin:
|
||||
image_attachments = graphene.List('extras.graphql.types.ImageAttachmentType')
|
||||
|
||||
def resolve_image_attachments(self, info):
|
||||
return self.images.restrict(info.context.user, 'view')
|
||||
@strawberry_django.field
|
||||
def image_attachments(self, info) -> List[Annotated["ImageAttachmentType", strawberry.lazy('.types')]]:
|
||||
return self.images.restrict(info.context.request.user, 'view')
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class JournalEntriesMixin:
|
||||
journal_entries = graphene.List('extras.graphql.types.JournalEntryType')
|
||||
|
||||
def resolve_journal_entries(self, info):
|
||||
return self.journal_entries.restrict(info.context.user, 'view')
|
||||
@strawberry_django.field
|
||||
def journal_entries(self, info) -> List[Annotated["JournalEntryType", strawberry.lazy('.types')]]:
|
||||
return self.journal_entries.all()
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class TagsMixin:
|
||||
tags = graphene.List('extras.graphql.types.TagType')
|
||||
|
||||
def resolve_tags(self, info):
|
||||
return self.tags.all()
|
||||
tags: List[Annotated["TagType", strawberry.lazy('.types')]]
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class ContactsMixin:
|
||||
contacts = graphene.List('tenancy.graphql.types.ContactAssignmentType')
|
||||
|
||||
def resolve_contacts(self, info):
|
||||
return list(self.contacts.all())
|
||||
contacts: List[Annotated["ContactAssignmentType", strawberry.lazy('tenancy.graphql.types')]]
|
||||
|
@ -1,80 +1,70 @@
|
||||
import graphene
|
||||
from typing import List
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from extras import models
|
||||
from netbox.graphql.fields import ObjectField, ObjectListField
|
||||
from .types import *
|
||||
from utilities.graphql_optimizer import gql_query_optimizer
|
||||
|
||||
|
||||
class ExtrasQuery(graphene.ObjectType):
|
||||
config_context = ObjectField(ConfigContextType)
|
||||
config_context_list = ObjectListField(ConfigContextType)
|
||||
@strawberry.type
|
||||
class ExtrasQuery:
|
||||
@strawberry.field
|
||||
def config_context(self, id: int) -> ConfigContextType:
|
||||
return models.ConfigContext.objects.get(pk=id)
|
||||
config_context_list: List[ConfigContextType] = strawberry_django.field()
|
||||
|
||||
def resolve_config_context_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.ConfigContext.objects.all(), info)
|
||||
@strawberry.field
|
||||
def config_template(self, id: int) -> ConfigTemplateType:
|
||||
return models.ConfigTemplate.objects.get(pk=id)
|
||||
config_template_list: List[ConfigTemplateType] = strawberry_django.field()
|
||||
|
||||
config_template = ObjectField(ConfigTemplateType)
|
||||
config_template_list = ObjectListField(ConfigTemplateType)
|
||||
@strawberry.field
|
||||
def custom_field(self, id: int) -> CustomFieldType:
|
||||
return models.CustomField.objects.get(pk=id)
|
||||
custom_field_list: List[CustomFieldType] = strawberry_django.field()
|
||||
|
||||
def resolve_config_template_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.ConfigTemplate.objects.all(), info)
|
||||
@strawberry.field
|
||||
def custom_field_choice_set(self, id: int) -> CustomFieldChoiceSetType:
|
||||
return models.CustomFieldChoiceSet.objects.get(pk=id)
|
||||
custom_field_choice_set_list: List[CustomFieldChoiceSetType] = strawberry_django.field()
|
||||
|
||||
custom_field = ObjectField(CustomFieldType)
|
||||
custom_field_list = ObjectListField(CustomFieldType)
|
||||
@strawberry.field
|
||||
def custom_link(self, id: int) -> CustomLinkType:
|
||||
return models.CustomLink.objects.get(pk=id)
|
||||
custom_link_list: List[CustomLinkType] = strawberry_django.field()
|
||||
|
||||
def resolve_custom_field_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.CustomField.objects.all(), info)
|
||||
@strawberry.field
|
||||
def export_template(self, id: int) -> ExportTemplateType:
|
||||
return models.ExportTemplate.objects.get(pk=id)
|
||||
export_template_list: List[ExportTemplateType] = strawberry_django.field()
|
||||
|
||||
custom_field_choice_set = ObjectField(CustomFieldChoiceSetType)
|
||||
custom_field_choice_set_list = ObjectListField(CustomFieldChoiceSetType)
|
||||
@strawberry.field
|
||||
def image_attachment(self, id: int) -> ImageAttachmentType:
|
||||
return models.ImageAttachment.objects.get(pk=id)
|
||||
image_attachment_list: List[ImageAttachmentType] = strawberry_django.field()
|
||||
|
||||
def resolve_custom_field_choices_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.CustomFieldChoiceSet.objects.all(), info)
|
||||
@strawberry.field
|
||||
def saved_filter(self, id: int) -> SavedFilterType:
|
||||
return models.SavedFilter.objects.get(pk=id)
|
||||
saved_filter_list: List[SavedFilterType] = strawberry_django.field()
|
||||
|
||||
custom_link = ObjectField(CustomLinkType)
|
||||
custom_link_list = ObjectListField(CustomLinkType)
|
||||
@strawberry.field
|
||||
def journal_entry(self, id: int) -> JournalEntryType:
|
||||
return models.JournalEntry.objects.get(pk=id)
|
||||
journal_entry_list: List[JournalEntryType] = strawberry_django.field()
|
||||
|
||||
def resolve_custom_link_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.CustomLink.objects.all(), info)
|
||||
@strawberry.field
|
||||
def tag(self, id: int) -> TagType:
|
||||
return models.Tag.objects.get(pk=id)
|
||||
tag_list: List[TagType] = strawberry_django.field()
|
||||
|
||||
export_template = ObjectField(ExportTemplateType)
|
||||
export_template_list = ObjectListField(ExportTemplateType)
|
||||
@strawberry.field
|
||||
def webhook(self, id: int) -> WebhookType:
|
||||
return models.Webhook.objects.get(pk=id)
|
||||
webhook_list: List[WebhookType] = strawberry_django.field()
|
||||
|
||||
def resolve_export_template_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.ExportTemplate.objects.all(), info)
|
||||
|
||||
image_attachment = ObjectField(ImageAttachmentType)
|
||||
image_attachment_list = ObjectListField(ImageAttachmentType)
|
||||
|
||||
def resolve_image_attachment_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.ImageAttachment.objects.all(), info)
|
||||
|
||||
saved_filter = ObjectField(SavedFilterType)
|
||||
saved_filter_list = ObjectListField(SavedFilterType)
|
||||
|
||||
def resolve_saved_filter_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.SavedFilter.objects.all(), info)
|
||||
|
||||
journal_entry = ObjectField(JournalEntryType)
|
||||
journal_entry_list = ObjectListField(JournalEntryType)
|
||||
|
||||
def resolve_journal_entry_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.JournalEntry.objects.all(), info)
|
||||
|
||||
tag = ObjectField(TagType)
|
||||
tag_list = ObjectListField(TagType)
|
||||
|
||||
def resolve_tag_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.Tag.objects.all(), info)
|
||||
|
||||
webhook = ObjectField(WebhookType)
|
||||
webhook_list = ObjectListField(WebhookType)
|
||||
|
||||
def resolve_webhook_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.Webhook.objects.all(), info)
|
||||
|
||||
event_rule = ObjectField(EventRuleType)
|
||||
event_rule_list = ObjectListField(EventRuleType)
|
||||
|
||||
def resolve_eventrule_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.EventRule.objects.all(), info)
|
||||
@strawberry.field
|
||||
def event_rule(self, id: int) -> EventRuleType:
|
||||
return models.EventRule.objects.get(pk=id)
|
||||
event_rule_list: List[EventRuleType] = strawberry_django.field()
|
||||
|
@ -1,6 +1,12 @@
|
||||
from extras import filtersets, models
|
||||
from typing import Annotated, List
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from extras import models
|
||||
from extras.graphql.mixins import CustomFieldsMixin, TagsMixin
|
||||
from netbox.graphql.types import BaseObjectType, ObjectType, OrganizationalObjectType
|
||||
from netbox.graphql.types import BaseObjectType, ContentTypeType, ObjectType, OrganizationalObjectType
|
||||
from .filters import *
|
||||
|
||||
__all__ = (
|
||||
'ConfigContextType',
|
||||
@ -19,104 +25,146 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.ConfigContext,
|
||||
fields='__all__',
|
||||
filters=ConfigContextFilter
|
||||
)
|
||||
class ConfigContextType(ObjectType):
|
||||
data_source: Annotated["DataSourceType", strawberry.lazy('core.graphql.types')] | None
|
||||
data_file: Annotated["DataFileType", strawberry.lazy('core.graphql.types')] | None
|
||||
|
||||
class Meta:
|
||||
model = models.ConfigContext
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.ConfigContextFilterSet
|
||||
roles: List[Annotated["DeviceRoleType", strawberry.lazy('dcim.graphql.types')]]
|
||||
device_types: List[Annotated["DeviceTypeType", strawberry.lazy('dcim.graphql.types')]]
|
||||
tags: List[Annotated["TagType", strawberry.lazy('extras.graphql.types')]]
|
||||
platforms: List[Annotated["PlatformType", strawberry.lazy('dcim.graphql.types')]]
|
||||
regions: List[Annotated["RegionType", strawberry.lazy('dcim.graphql.types')]]
|
||||
cluster_groups: List[Annotated["ClusterGroupType", strawberry.lazy('virtualization.graphql.types')]]
|
||||
tenant_groups: List[Annotated["TenantGroupType", strawberry.lazy('tenancy.graphql.types')]]
|
||||
cluster_types: List[Annotated["ClusterTypeType", strawberry.lazy('virtualization.graphql.types')]]
|
||||
clusters: List[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]
|
||||
locations: List[Annotated["LocationType", strawberry.lazy('dcim.graphql.types')]]
|
||||
sites: List[Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]]
|
||||
tenants: List[Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')]]
|
||||
site_groups: List[Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.ConfigTemplate,
|
||||
fields='__all__',
|
||||
filters=ConfigTemplateFilter
|
||||
)
|
||||
class ConfigTemplateType(TagsMixin, ObjectType):
|
||||
data_source: Annotated["DataSourceType", strawberry.lazy('core.graphql.types')] | None
|
||||
data_file: Annotated["DataFileType", strawberry.lazy('core.graphql.types')] | None
|
||||
|
||||
class Meta:
|
||||
model = models.ConfigTemplate
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.ConfigTemplateFilterSet
|
||||
virtualmachines: List[Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')]]
|
||||
devices: List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]
|
||||
platforms: List[Annotated["PlatformType", strawberry.lazy('dcim.graphql.types')]]
|
||||
device_roles: List[Annotated["DeviceRoleType", strawberry.lazy('dcim.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.CustomField,
|
||||
fields='__all__',
|
||||
filters=CustomFieldFilter
|
||||
)
|
||||
class CustomFieldType(ObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.CustomField
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.CustomFieldFilterSet
|
||||
related_object_type: Annotated["ContentTypeType", strawberry.lazy('netbox.graphql.types')] | None
|
||||
choice_set: Annotated["CustomFieldChoiceSetType", strawberry.lazy('extras.graphql.types')] | None
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.CustomFieldChoiceSet,
|
||||
exclude=('extra_choices', ),
|
||||
filters=CustomFieldChoiceSetFilter
|
||||
)
|
||||
class CustomFieldChoiceSetType(ObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.CustomFieldChoiceSet
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.CustomFieldChoiceSetFilterSet
|
||||
choices_for: List[Annotated["CustomFieldType", strawberry.lazy('extras.graphql.types')]]
|
||||
extra_choices: List[str] | None
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.CustomLink,
|
||||
fields='__all__',
|
||||
filters=CustomLinkFilter
|
||||
)
|
||||
class CustomLinkType(ObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.CustomLink
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.CustomLinkFilterSet
|
||||
|
||||
|
||||
class EventRuleType(OrganizationalObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.EventRule
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.EventRuleFilterSet
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.ExportTemplate,
|
||||
fields='__all__',
|
||||
filters=ExportTemplateFilter
|
||||
)
|
||||
class ExportTemplateType(ObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.ExportTemplate
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.ExportTemplateFilterSet
|
||||
data_source: Annotated["DataSourceType", strawberry.lazy('core.graphql.types')] | None
|
||||
data_file: Annotated["DataFileType", strawberry.lazy('core.graphql.types')] | None
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.ImageAttachment,
|
||||
fields='__all__',
|
||||
filters=ImageAttachmentFilter
|
||||
)
|
||||
class ImageAttachmentType(BaseObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.ImageAttachment
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.ImageAttachmentFilterSet
|
||||
object_type: Annotated["ContentTypeType", strawberry.lazy('netbox.graphql.types')] | None
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.JournalEntry,
|
||||
fields='__all__',
|
||||
filters=JournalEntryFilter
|
||||
)
|
||||
class JournalEntryType(CustomFieldsMixin, TagsMixin, ObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.JournalEntry
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.JournalEntryFilterSet
|
||||
assigned_object_type: Annotated["ContentTypeType", strawberry.lazy('netbox.graphql.types')] | None
|
||||
created_by: Annotated["UserType", strawberry.lazy('users.graphql.types')] | None
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.ObjectChange,
|
||||
fields='__all__',
|
||||
filters=ObjectChangeFilter
|
||||
)
|
||||
class ObjectChangeType(BaseObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.ObjectChange
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.ObjectChangeFilterSet
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.SavedFilter,
|
||||
exclude=['content_types',],
|
||||
filters=SavedFilterFilter
|
||||
)
|
||||
class SavedFilterType(ObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.SavedFilter
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.SavedFilterFilterSet
|
||||
user: Annotated["UserType", strawberry.lazy('users.graphql.types')] | None
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.Tag,
|
||||
exclude=['extras_taggeditem_items', ],
|
||||
filters=TagFilter
|
||||
)
|
||||
class TagType(ObjectType):
|
||||
color: str
|
||||
|
||||
class Meta:
|
||||
model = models.Tag
|
||||
exclude = ('extras_taggeditem_items',)
|
||||
filterset_class = filtersets.TagFilterSet
|
||||
object_types: List[ContentTypeType]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.Webhook,
|
||||
exclude=['content_types',],
|
||||
filters=WebhookFilter
|
||||
)
|
||||
class WebhookType(OrganizationalObjectType):
|
||||
pass
|
||||
|
||||
class Meta:
|
||||
model = models.Webhook
|
||||
filterset_class = filtersets.WebhookFilterSet
|
||||
|
||||
@strawberry_django.type(
|
||||
models.EventRule,
|
||||
exclude=['content_types',],
|
||||
filters=EventRuleFilter
|
||||
)
|
||||
class EventRuleType(OrganizationalObjectType):
|
||||
action_object_type: Annotated["ContentTypeType", strawberry.lazy('netbox.graphql.types')] | None
|
||||
|
@ -14,7 +14,7 @@ from extras.context_managers import event_tracking
|
||||
from extras.scripts import get_module_and_script
|
||||
from extras.signals import clear_events
|
||||
from utilities.exceptions import AbortTransaction
|
||||
from utilities.utils import NetBoxFakeRequest
|
||||
from utilities.request import NetBoxFakeRequest
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
@ -27,7 +27,11 @@ class Migration(migrations.Migration):
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='core.objecttype'),
|
||||
),
|
||||
migrations.RunSQL(
|
||||
"ALTER TABLE extras_customfield_content_types_id_seq RENAME TO extras_customfield_object_types_id_seq"
|
||||
"ALTER TABLE IF EXISTS extras_customfield_content_types_id_seq RENAME TO extras_customfield_object_types_id_seq"
|
||||
),
|
||||
# Pre-v2.10 sequence name (see #15605)
|
||||
migrations.RunSQL(
|
||||
"ALTER TABLE IF EXISTS extras_customfield_obj_type_id_seq RENAME TO extras_customfield_object_types_id_seq"
|
||||
),
|
||||
|
||||
# Custom links
|
||||
|
@ -9,11 +9,11 @@ from jinja2.sandbox import SandboxedEnvironment
|
||||
|
||||
from extras.querysets import ConfigContextQuerySet
|
||||
from netbox.config import get_config
|
||||
from netbox.registry import registry
|
||||
from netbox.models import ChangeLoggedModel
|
||||
from netbox.models.features import CloningMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin
|
||||
from utilities.jinja2 import ConfigTemplateLoader
|
||||
from utilities.utils import deepmerge
|
||||
from netbox.registry import registry
|
||||
from utilities.data import deepmerge
|
||||
from utilities.jinja2 import DataFileLoader
|
||||
|
||||
__all__ = (
|
||||
'ConfigContext',
|
||||
@ -290,7 +290,7 @@ class ConfigTemplate(SyncedDataMixin, CustomLinksMixin, ExportTemplatesMixin, Ta
|
||||
"""
|
||||
# Initialize the template loader & cache the base template code (if applicable)
|
||||
if self.data_file:
|
||||
loader = ConfigTemplateLoader(data_source=self.data_source)
|
||||
loader = DataFileLoader(data_source=self.data_source)
|
||||
loader.cache_templates({
|
||||
self.data_file.path: self.template_code
|
||||
})
|
||||
|
@ -22,8 +22,10 @@ from netbox.models import ChangeLoggedModel
|
||||
from netbox.models.features import (
|
||||
CloningMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin,
|
||||
)
|
||||
from utilities.html import clean_html
|
||||
from utilities.querydict import dict_to_querydict
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.utils import clean_html, dict_to_querydict, render_jinja2
|
||||
from utilities.jinja2 import render_jinja2
|
||||
|
||||
__all__ = (
|
||||
'Bookmark',
|
||||
|
@ -108,7 +108,7 @@ class ScriptModule(PythonModuleMixin, JobsMixin, ManagedFile):
|
||||
def __str__(self):
|
||||
return self.python_name
|
||||
|
||||
@cached_property
|
||||
@property
|
||||
def module_scripts(self):
|
||||
|
||||
def _get_name(cls):
|
||||
@ -137,9 +137,13 @@ class ScriptModule(PythonModuleMixin, JobsMixin, ManagedFile):
|
||||
Syncs the file-based module to the database, adding and removing individual Script objects
|
||||
in the database as needed.
|
||||
"""
|
||||
db_classes = {
|
||||
script.name: script for script in self.scripts.all()
|
||||
}
|
||||
if self.id:
|
||||
db_classes = {
|
||||
script.name: script for script in self.scripts.all()
|
||||
}
|
||||
else:
|
||||
db_classes = {}
|
||||
|
||||
db_classes_set = set(db_classes.keys())
|
||||
module_classes_set = set(self.module_scripts.keys())
|
||||
|
||||
@ -158,10 +162,10 @@ class ScriptModule(PythonModuleMixin, JobsMixin, ManagedFile):
|
||||
|
||||
def sync_data(self):
|
||||
super().sync_data()
|
||||
self.sync_classes()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.file_root = ManagedFileRootPathChoices.SCRIPTS
|
||||
self.sync_classes()
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
|
@ -4,9 +4,7 @@ from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.search.utils import get_indexer
|
||||
from netbox.registry import registry
|
||||
from utilities.fields import RestrictedGenericForeignKey
|
||||
from utilities.utils import content_type_identifier
|
||||
from ..fields import CachedValueField
|
||||
|
||||
__all__ = (
|
||||
|
@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from extras.choices import ChangeActionChoices
|
||||
from netbox.models import ChangeLoggedModel
|
||||
from netbox.models.features import *
|
||||
from utilities.utils import deserialize_object
|
||||
from utilities.serialization import deserialize_object
|
||||
|
||||
__all__ = (
|
||||
'Branch',
|
||||
|
@ -5,9 +5,9 @@ from django.utils.text import slugify
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from taggit.models import TagBase, GenericTaggedItemBase
|
||||
|
||||
from netbox.choices import ColorChoices
|
||||
from netbox.models import ChangeLoggedModel
|
||||
from netbox.models.features import CloningMixin, ExportTemplatesMixin
|
||||
from utilities.choices import ColorChoices
|
||||
from utilities.fields import ColorField
|
||||
|
||||
__all__ = (
|
||||
@ -37,7 +37,7 @@ class Tag(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel, TagBase):
|
||||
to='core.ObjectType',
|
||||
related_name='+',
|
||||
blank=True,
|
||||
help_text=_("The object type(s) to which this this tag can be applied.")
|
||||
help_text=_("The object type(s) to which this tag can be applied.")
|
||||
)
|
||||
|
||||
clone_fields = (
|
||||
|
@ -1,7 +1,8 @@
|
||||
import importlib
|
||||
import logging
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.exceptions import ImproperlyConfigured, ValidationError
|
||||
from django.db.models.fields.reverse_related import ManyToManyRel
|
||||
from django.db.models.signals import m2m_changed, post_save, pre_delete
|
||||
from django.dispatch import receiver, Signal
|
||||
@ -13,7 +14,6 @@ from core.signals import job_end, job_start
|
||||
from extras.constants import EVENT_JOB_END, EVENT_JOB_START
|
||||
from extras.events import process_event_rules
|
||||
from extras.models import EventRule
|
||||
from extras.validators import run_validators
|
||||
from netbox.config import get_config
|
||||
from netbox.context import current_request, events_queue
|
||||
from netbox.models.features import ChangeLoggingMixin
|
||||
@ -22,6 +22,30 @@ from utilities.exceptions import AbortRequest
|
||||
from .choices import ObjectChangeActionChoices
|
||||
from .events import enqueue_object, get_snapshots, serialize_for_event
|
||||
from .models import CustomField, ObjectChange, TaggedItem
|
||||
from .validators import CustomValidator
|
||||
|
||||
|
||||
def run_validators(instance, validators):
|
||||
"""
|
||||
Run the provided iterable of validators for the instance.
|
||||
"""
|
||||
request = current_request.get()
|
||||
for validator in validators:
|
||||
|
||||
# Loading a validator class by dotted path
|
||||
if type(validator) is str:
|
||||
module, cls = validator.rsplit('.', 1)
|
||||
validator = getattr(importlib.import_module(module), cls)()
|
||||
|
||||
# Constructing a new instance on the fly from a ruleset
|
||||
elif type(validator) is dict:
|
||||
validator = CustomValidator(validator)
|
||||
|
||||
elif not issubclass(validator.__class__, CustomValidator):
|
||||
raise ImproperlyConfigured(f"Invalid value for custom validator: {validator}")
|
||||
|
||||
validator(instance, request)
|
||||
|
||||
|
||||
#
|
||||
# Change logging/webhooks
|
||||
|
@ -65,7 +65,7 @@ def custom_links(context, obj):
|
||||
rendered['link'], rendered['link_target'], cl.button_class, rendered['text']
|
||||
)
|
||||
except Exception as e:
|
||||
template_code += f'<a class="btn btn-sm btn-outline-dark" disabled="disabled" title="{e}">' \
|
||||
template_code += f'<a class="btn btn-sm btn-outline-secondary" disabled="disabled" title="{e}">' \
|
||||
f'<i class="mdi mdi-alert"></i> {cl.name}</a>\n'
|
||||
|
||||
# Add grouped links to template
|
||||
|
@ -5,7 +5,7 @@ from circuits.api.serializers import ProviderSerializer
|
||||
from circuits.forms import ProviderForm
|
||||
from circuits.models import Provider
|
||||
from ipam.models import ASN, RIR
|
||||
from utilities.choices import CSVDelimiterChoices, ImportFormatChoices
|
||||
from netbox.choices import CSVDelimiterChoices, ImportFormatChoices
|
||||
from utilities.testing import APITestCase, ModelViewTestCase, create_tags, post_data
|
||||
|
||||
|
||||
|
@ -12,7 +12,7 @@ from dcim.models import Manufacturer, Rack, Site
|
||||
from extras.choices import *
|
||||
from extras.models import CustomField, CustomFieldChoiceSet
|
||||
from ipam.models import VLAN
|
||||
from utilities.choices import CSVDelimiterChoices, ImportFormatChoices
|
||||
from netbox.choices import CSVDelimiterChoices, ImportFormatChoices
|
||||
from utilities.testing import APITestCase, TestCase
|
||||
from virtualization.models import VirtualMachine
|
||||
|
||||
|
@ -3,11 +3,13 @@ from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from ipam.models import ASN, RIR
|
||||
from dcim.choices import SiteStatusChoices
|
||||
from dcim.models import Site
|
||||
from dcim.models import Site, Region
|
||||
from extras.validators import CustomValidator
|
||||
from ipam.models import ASN, RIR
|
||||
from users.models import User
|
||||
from utilities.exceptions import AbortRequest
|
||||
from utilities.request import NetBoxFakeRequest
|
||||
|
||||
|
||||
class MyValidator(CustomValidator):
|
||||
@ -79,6 +81,20 @@ prohibited_validator = CustomValidator({
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
region_validator = CustomValidator({
|
||||
'region.name': {
|
||||
'eq': 'Bar',
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
request_validator = CustomValidator({
|
||||
'request.user.username': {
|
||||
'eq': 'Bob'
|
||||
}
|
||||
})
|
||||
|
||||
custom_validator = MyValidator()
|
||||
|
||||
|
||||
@ -145,6 +161,20 @@ class CustomValidatorTest(TestCase):
|
||||
def test_valid(self):
|
||||
Site(name='abcdef123', slug='abcdef123').clean()
|
||||
|
||||
@override_settings(CUSTOM_VALIDATORS={'dcim.site': [region_validator]})
|
||||
def test_valid(self):
|
||||
region1 = Region(name='Foo', slug='foo')
|
||||
region1.save()
|
||||
region2 = Region(name='Bar', slug='bar')
|
||||
region2.save()
|
||||
|
||||
# Invalid region
|
||||
with self.assertRaises(ValidationError):
|
||||
Site(name='abcdef123', slug='abcdef123', region=region1).clean()
|
||||
|
||||
# Valid region
|
||||
Site(name='abcdef123', slug='abcdef123', region=region2).clean()
|
||||
|
||||
@override_settings(CUSTOM_VALIDATORS={'dcim.site': [custom_validator]})
|
||||
def test_custom_invalid(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
@ -154,6 +184,28 @@ class CustomValidatorTest(TestCase):
|
||||
def test_custom_valid(self):
|
||||
Site(name='foo', slug='foo').clean()
|
||||
|
||||
@override_settings(CUSTOM_VALIDATORS={'dcim.site': [request_validator]})
|
||||
def test_request_validation(self):
|
||||
alice = User.objects.create(username='Alice')
|
||||
bob = User.objects.create(username='Bob')
|
||||
request = NetBoxFakeRequest({
|
||||
'META': {},
|
||||
'POST': {},
|
||||
'GET': {},
|
||||
'FILES': {},
|
||||
'user': alice,
|
||||
'path': '',
|
||||
})
|
||||
site = Site(name='abc', slug='abc')
|
||||
|
||||
# Attempt to create the Site as Alice
|
||||
with self.assertRaises(ValidationError):
|
||||
request_validator(site, request)
|
||||
|
||||
# Creating the Site as Bob should succeed
|
||||
request.user = bob
|
||||
request_validator(site, request)
|
||||
|
||||
|
||||
class CustomValidatorConfigTest(TestCase):
|
||||
|
||||
@ -176,7 +228,7 @@ class CustomValidatorConfigTest(TestCase):
|
||||
@override_settings(
|
||||
CUSTOM_VALIDATORS={
|
||||
'dcim.site': (
|
||||
'extras.tests.test_customvalidation.MyValidator',
|
||||
'extras.tests.test_customvalidators.MyValidator',
|
||||
)
|
||||
}
|
||||
)
|
||||
@ -223,7 +275,7 @@ class ProtectionRulesConfigTest(TestCase):
|
||||
@override_settings(
|
||||
PROTECTION_RULES={
|
||||
'dcim.site': (
|
||||
'extras.tests.test_customvalidation.MyValidator',
|
||||
'extras.tests.test_customvalidators.MyValidator',
|
||||
)
|
||||
}
|
||||
)
|
@ -1,4 +1,5 @@
|
||||
import importlib
|
||||
import inspect
|
||||
import operator
|
||||
|
||||
from django.core import validators
|
||||
from django.core.exceptions import ValidationError
|
||||
@ -74,6 +75,8 @@ class CustomValidator:
|
||||
|
||||
:param validation_rules: A dictionary mapping object attributes to validation rules
|
||||
"""
|
||||
REQUEST_TOKEN = 'request'
|
||||
|
||||
VALIDATORS = {
|
||||
'eq': IsEqualValidator,
|
||||
'neq': IsNotEqualValidator,
|
||||
@ -88,25 +91,56 @@ class CustomValidator:
|
||||
|
||||
def __init__(self, validation_rules=None):
|
||||
self.validation_rules = validation_rules or {}
|
||||
assert type(self.validation_rules) is dict, "Validation rules must be passed as a dictionary"
|
||||
if type(self.validation_rules) is not dict:
|
||||
raise ValueError(_("Validation rules must be passed as a dictionary"))
|
||||
|
||||
def __call__(self, instance):
|
||||
# Validate instance attributes per validation rules
|
||||
for attr_name, rules in self.validation_rules.items():
|
||||
attr = self._getattr(instance, attr_name)
|
||||
def __call__(self, instance, request=None):
|
||||
"""
|
||||
Validate the instance and (optional) request against the validation rule(s).
|
||||
"""
|
||||
for attr_path, rules in self.validation_rules.items():
|
||||
|
||||
# The rule applies to the current request
|
||||
if attr_path.split('.')[0] == self.REQUEST_TOKEN:
|
||||
# Skip if no request has been provided (we can't validate)
|
||||
if request is None:
|
||||
continue
|
||||
attr = self._get_request_attr(request, attr_path)
|
||||
# The rule applies to the instance
|
||||
else:
|
||||
attr = self._get_instance_attr(instance, attr_path)
|
||||
|
||||
# Validate the attribute's value against each of the rules defined for it
|
||||
for descriptor, value in rules.items():
|
||||
validator = self.get_validator(descriptor, value)
|
||||
try:
|
||||
validator(attr)
|
||||
except ValidationError as exc:
|
||||
# Re-package the raised ValidationError to associate it with the specific attr
|
||||
raise ValidationError({attr_name: exc})
|
||||
raise ValidationError(
|
||||
_("Custom validation failed for {attribute}: {exception}").format(
|
||||
attribute=attr_path, exception=exc
|
||||
)
|
||||
)
|
||||
|
||||
# Execute custom validation logic (if any)
|
||||
self.validate(instance)
|
||||
# TODO: Remove in v4.1
|
||||
# Inspect the validate() method, which may have been overridden, to determine
|
||||
# whether we should pass the request (maintains backward compatibility for pre-v4.0)
|
||||
if 'request' in inspect.signature(self.validate).parameters:
|
||||
self.validate(instance, request)
|
||||
else:
|
||||
self.validate(instance)
|
||||
|
||||
@staticmethod
|
||||
def _getattr(instance, name):
|
||||
def _get_request_attr(request, name):
|
||||
name = name.split('.', maxsplit=1)[1] # Remove token
|
||||
try:
|
||||
return operator.attrgetter(name)(request)
|
||||
except AttributeError:
|
||||
raise ValidationError(_('Invalid attribute "{name}" for request').format(name=name))
|
||||
|
||||
@staticmethod
|
||||
def _get_instance_attr(instance, name):
|
||||
# Attempt to resolve many-to-many fields to their stored values
|
||||
m2m_fields = [f.name for f in instance._meta.local_many_to_many]
|
||||
if name in m2m_fields:
|
||||
@ -117,14 +151,14 @@ class CustomValidator:
|
||||
return []
|
||||
|
||||
# Raise a ValidationError for unknown attributes
|
||||
if not hasattr(instance, name):
|
||||
try:
|
||||
return operator.attrgetter(name)(instance)
|
||||
except AttributeError:
|
||||
raise ValidationError(_('Invalid attribute "{name}" for {model}').format(
|
||||
name=name,
|
||||
model=instance.__class__.__name__
|
||||
))
|
||||
|
||||
return getattr(instance, name)
|
||||
|
||||
def get_validator(self, descriptor, value):
|
||||
"""
|
||||
Instantiate and return the appropriate validator based on the descriptor given. For
|
||||
@ -137,7 +171,7 @@ class CustomValidator:
|
||||
validator_cls = self.VALIDATORS.get(descriptor)
|
||||
return validator_cls(value)
|
||||
|
||||
def validate(self, instance):
|
||||
def validate(self, instance, request):
|
||||
"""
|
||||
Custom validation method, to be overridden by the user. Validation failures should
|
||||
raise a ValidationError exception.
|
||||
@ -151,21 +185,3 @@ class CustomValidator:
|
||||
if field is not None:
|
||||
raise ValidationError({field: message})
|
||||
raise ValidationError(message)
|
||||
|
||||
|
||||
def run_validators(instance, validators):
|
||||
"""
|
||||
Run the provided iterable of validators for the instance.
|
||||
"""
|
||||
for validator in validators:
|
||||
|
||||
# Loading a validator class by dotted path
|
||||
if type(validator) is str:
|
||||
module, cls = validator.rsplit('.', 1)
|
||||
validator = getattr(importlib.import_module(module), cls)()
|
||||
|
||||
# Constructing a new instance on the fly from a ruleset
|
||||
elif type(validator) is dict:
|
||||
validator = CustomValidator(validator)
|
||||
|
||||
validator(instance)
|
||||
|
@ -18,12 +18,16 @@ from extras.dashboard.utils import get_widget_class
|
||||
from netbox.constants import DEFAULT_ACTION_PERMISSIONS
|
||||
from netbox.views import generic
|
||||
from netbox.views.generic.mixins import TableMixin
|
||||
from utilities.data import shallow_compare_dict
|
||||
from utilities.forms import ConfirmationForm, get_field_value
|
||||
from utilities.htmx import htmx_partial
|
||||
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
||||
from utilities.query import count_related
|
||||
from utilities.querydict import normalize_querydict
|
||||
from utilities.request import copy_safe_request
|
||||
from utilities.rqworker import get_workers_for_queue
|
||||
from utilities.templatetags.builtins.filters import render_markdown
|
||||
from utilities.utils import copy_safe_request, count_related, get_viewname, normalize_querydict, shallow_compare_dict
|
||||
from utilities.views import ContentTypePermissionRequiredMixin, register_model_view
|
||||
from utilities.views import ContentTypePermissionRequiredMixin, get_viewname, register_model_view
|
||||
from . import filtersets, forms, tables
|
||||
from .models import *
|
||||
from .scripts import run_script
|
||||
@ -1221,7 +1225,7 @@ class ScriptResultView(TableMixin, generic.ObjectView):
|
||||
}
|
||||
|
||||
# If this is an HTMX request, return only the result HTML
|
||||
if request.htmx:
|
||||
if htmx_partial(request):
|
||||
response = render(request, 'extras/htmx/script_result.html', context)
|
||||
if job.completed or not job.started:
|
||||
response.status_code = 286
|
||||
|
@ -378,7 +378,7 @@ class IPAddressImportForm(NetBoxModelImportForm):
|
||||
|
||||
# Set as primary for device/VM
|
||||
if self.cleaned_data.get('is_primary'):
|
||||
parent = self.cleaned_data['device'] or self.cleaned_data['virtual_machine']
|
||||
parent = self.cleaned_data.get('device') or self.cleaned_data.get('virtual_machine')
|
||||
if self.instance.address.version == 4:
|
||||
parent.primary_ip4 = ipaddress
|
||||
elif self.instance.address.version == 6:
|
||||
|
119
netbox/ipam/graphql/filters.py
Normal file
119
netbox/ipam/graphql/filters.py
Normal file
@ -0,0 +1,119 @@
|
||||
import strawberry_django
|
||||
|
||||
from ipam import filtersets, models
|
||||
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
|
||||
|
||||
__all__ = (
|
||||
'ASNFilter',
|
||||
'ASNRangeFilter',
|
||||
'AggregateFilter',
|
||||
'FHRPGroupFilter',
|
||||
'FHRPGroupAssignmentFilter',
|
||||
'IPAddressFilter',
|
||||
'IPRangeFilter',
|
||||
'PrefixFilter',
|
||||
'RIRFilter',
|
||||
'RoleFilter',
|
||||
'RouteTargetFilter',
|
||||
'ServiceFilter',
|
||||
'ServiceTemplateFilter',
|
||||
'VLANFilter',
|
||||
'VLANGroupFilter',
|
||||
'VRFFilter',
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ASN, lookups=True)
|
||||
@autotype_decorator(filtersets.ASNFilterSet)
|
||||
class ASNFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ASNRange, lookups=True)
|
||||
@autotype_decorator(filtersets.ASNRangeFilterSet)
|
||||
class ASNRangeFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Aggregate, lookups=True)
|
||||
@autotype_decorator(filtersets.AggregateFilterSet)
|
||||
class AggregateFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.FHRPGroup, lookups=True)
|
||||
@autotype_decorator(filtersets.FHRPGroupFilterSet)
|
||||
class FHRPGroupFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.FHRPGroupAssignment, lookups=True)
|
||||
@autotype_decorator(filtersets.FHRPGroupAssignmentFilterSet)
|
||||
class FHRPGroupAssignmentFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.IPAddress, lookups=True)
|
||||
@autotype_decorator(filtersets.IPAddressFilterSet)
|
||||
class IPAddressFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.IPRange, lookups=True)
|
||||
@autotype_decorator(filtersets.IPRangeFilterSet)
|
||||
class IPRangeFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Prefix, lookups=True)
|
||||
@autotype_decorator(filtersets.PrefixFilterSet)
|
||||
class PrefixFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.RIR, lookups=True)
|
||||
@autotype_decorator(filtersets.RIRFilterSet)
|
||||
class RIRFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Role, lookups=True)
|
||||
@autotype_decorator(filtersets.RoleFilterSet)
|
||||
class RoleFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.RouteTarget, lookups=True)
|
||||
@autotype_decorator(filtersets.RouteTargetFilterSet)
|
||||
class RouteTargetFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Service, lookups=True)
|
||||
@autotype_decorator(filtersets.ServiceFilterSet)
|
||||
class ServiceFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ServiceTemplate, lookups=True)
|
||||
@autotype_decorator(filtersets.ServiceTemplateFilterSet)
|
||||
class ServiceTemplateFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VLAN, lookups=True)
|
||||
@autotype_decorator(filtersets.VLANFilterSet)
|
||||
class VLANFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VLANGroup, lookups=True)
|
||||
@autotype_decorator(filtersets.VLANGroupFilterSet)
|
||||
class VLANGroupFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VRF, lookups=True)
|
||||
@autotype_decorator(filtersets.VRFFilterSet)
|
||||
class VRFFilter(BaseFilterMixin):
|
||||
pass
|
@ -1,95 +0,0 @@
|
||||
import graphene
|
||||
from dcim.graphql.types import (
|
||||
InterfaceType,
|
||||
LocationType,
|
||||
RackType,
|
||||
RegionType,
|
||||
SiteGroupType,
|
||||
SiteType,
|
||||
)
|
||||
from dcim.models import Interface, Location, Rack, Region, Site, SiteGroup
|
||||
from ipam.graphql.types import FHRPGroupType, VLANType
|
||||
from ipam.models import VLAN, FHRPGroup
|
||||
from virtualization.graphql.types import ClusterGroupType, ClusterType, VMInterfaceType
|
||||
from virtualization.models import Cluster, ClusterGroup, VMInterface
|
||||
|
||||
|
||||
class IPAddressAssignmentType(graphene.Union):
|
||||
class Meta:
|
||||
types = (
|
||||
InterfaceType,
|
||||
FHRPGroupType,
|
||||
VMInterfaceType,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def resolve_type(cls, instance, info):
|
||||
if type(instance) is Interface:
|
||||
return InterfaceType
|
||||
if type(instance) is FHRPGroup:
|
||||
return FHRPGroupType
|
||||
if type(instance) is VMInterface:
|
||||
return VMInterfaceType
|
||||
|
||||
|
||||
class L2VPNAssignmentType(graphene.Union):
|
||||
class Meta:
|
||||
types = (
|
||||
InterfaceType,
|
||||
VLANType,
|
||||
VMInterfaceType,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def resolve_type(cls, instance, info):
|
||||
if type(instance) is Interface:
|
||||
return InterfaceType
|
||||
if type(instance) is VLAN:
|
||||
return VLANType
|
||||
if type(instance) is VMInterface:
|
||||
return VMInterfaceType
|
||||
|
||||
|
||||
class FHRPGroupInterfaceType(graphene.Union):
|
||||
class Meta:
|
||||
types = (
|
||||
InterfaceType,
|
||||
VMInterfaceType,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def resolve_type(cls, instance, info):
|
||||
if type(instance) is Interface:
|
||||
return InterfaceType
|
||||
if type(instance) is VMInterface:
|
||||
return VMInterfaceType
|
||||
|
||||
|
||||
class VLANGroupScopeType(graphene.Union):
|
||||
class Meta:
|
||||
types = (
|
||||
ClusterType,
|
||||
ClusterGroupType,
|
||||
LocationType,
|
||||
RackType,
|
||||
RegionType,
|
||||
SiteType,
|
||||
SiteGroupType,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def resolve_type(cls, instance, info):
|
||||
if type(instance) is Cluster:
|
||||
return ClusterType
|
||||
if type(instance) is ClusterGroup:
|
||||
return ClusterGroupType
|
||||
if type(instance) is Location:
|
||||
return LocationType
|
||||
if type(instance) is Rack:
|
||||
return RackType
|
||||
if type(instance) is Region:
|
||||
return RegionType
|
||||
if type(instance) is Site:
|
||||
return SiteType
|
||||
if type(instance) is SiteGroup:
|
||||
return SiteGroupType
|
@ -1,4 +1,7 @@
|
||||
import graphene
|
||||
from typing import Annotated, List
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
__all__ = (
|
||||
'IPAddressesMixin',
|
||||
@ -6,15 +9,11 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class IPAddressesMixin:
|
||||
ip_addresses = graphene.List('ipam.graphql.types.IPAddressType')
|
||||
|
||||
def resolve_ip_addresses(self, info):
|
||||
return self.ip_addresses.restrict(info.context.user, 'view')
|
||||
ip_addresses: List[Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class VLANGroupsMixin:
|
||||
vlan_groups = graphene.List('ipam.graphql.types.VLANGroupType')
|
||||
|
||||
def resolve_vlan_groups(self, info):
|
||||
return self.vlan_groups.restrict(info.context.user, 'view')
|
||||
vlan_groups: List[Annotated["VLANGroupType", strawberry.lazy('ipam.graphql.types')]]
|
||||
|
@ -1,104 +1,90 @@
|
||||
import graphene
|
||||
from typing import List
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from ipam import models
|
||||
from netbox.graphql.fields import ObjectField, ObjectListField
|
||||
from utilities.graphql_optimizer import gql_query_optimizer
|
||||
from .types import *
|
||||
|
||||
|
||||
class IPAMQuery(graphene.ObjectType):
|
||||
asn = ObjectField(ASNType)
|
||||
asn_list = ObjectListField(ASNType)
|
||||
@strawberry.type
|
||||
class IPAMQuery:
|
||||
@strawberry.field
|
||||
def asn(self, id: int) -> ASNType:
|
||||
return models.ASN.objects.get(pk=id)
|
||||
asn_list: List[ASNType] = strawberry_django.field()
|
||||
|
||||
def resolve_asn_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.ASN.objects.all(), info)
|
||||
@strawberry.field
|
||||
def asn_range(self, id: int) -> ASNRangeType:
|
||||
return models.ASNRange.objects.get(pk=id)
|
||||
asn_range_list: List[ASNRangeType] = strawberry_django.field()
|
||||
|
||||
asn_range = ObjectField(ASNRangeType)
|
||||
asn_range_list = ObjectListField(ASNRangeType)
|
||||
@strawberry.field
|
||||
def aggregate(self, id: int) -> AggregateType:
|
||||
return models.Aggregate.objects.get(pk=id)
|
||||
aggregate_list: List[AggregateType] = strawberry_django.field()
|
||||
|
||||
def resolve_asn_range_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.ASNRange.objects.all(), info)
|
||||
@strawberry.field
|
||||
def ip_address(self, id: int) -> IPAddressType:
|
||||
return models.IPAddress.objects.get(pk=id)
|
||||
ip_address_list: List[IPAddressType] = strawberry_django.field()
|
||||
|
||||
aggregate = ObjectField(AggregateType)
|
||||
aggregate_list = ObjectListField(AggregateType)
|
||||
@strawberry.field
|
||||
def ip_range(self, id: int) -> IPRangeType:
|
||||
return models.IPRange.objects.get(pk=id)
|
||||
ip_range_list: List[IPRangeType] = strawberry_django.field()
|
||||
|
||||
def resolve_aggregate_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.Aggregate.objects.all(), info)
|
||||
@strawberry.field
|
||||
def prefix(self, id: int) -> PrefixType:
|
||||
return models.Prefix.objects.get(pk=id)
|
||||
prefix_list: List[PrefixType] = strawberry_django.field()
|
||||
|
||||
ip_address = ObjectField(IPAddressType)
|
||||
ip_address_list = ObjectListField(IPAddressType)
|
||||
@strawberry.field
|
||||
def rir(self, id: int) -> RIRType:
|
||||
return models.RIR.objects.get(pk=id)
|
||||
rir_list: List[RIRType] = strawberry_django.field()
|
||||
|
||||
def resolve_ip_address_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.IPAddress.objects.all(), info)
|
||||
@strawberry.field
|
||||
def role(self, id: int) -> RoleType:
|
||||
return models.Role.objects.get(pk=id)
|
||||
role_list: List[RoleType] = strawberry_django.field()
|
||||
|
||||
ip_range = ObjectField(IPRangeType)
|
||||
ip_range_list = ObjectListField(IPRangeType)
|
||||
@strawberry.field
|
||||
def route_target(self, id: int) -> RouteTargetType:
|
||||
return models.RouteTarget.objects.get(pk=id)
|
||||
route_target_list: List[RouteTargetType] = strawberry_django.field()
|
||||
|
||||
def resolve_ip_range_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.IPRange.objects.all(), info)
|
||||
@strawberry.field
|
||||
def service(self, id: int) -> ServiceType:
|
||||
return models.Service.objects.get(pk=id)
|
||||
service_list: List[ServiceType] = strawberry_django.field()
|
||||
|
||||
prefix = ObjectField(PrefixType)
|
||||
prefix_list = ObjectListField(PrefixType)
|
||||
@strawberry.field
|
||||
def service_template(self, id: int) -> ServiceTemplateType:
|
||||
return models.ServiceTemplate.objects.get(pk=id)
|
||||
service_template_list: List[ServiceTemplateType] = strawberry_django.field()
|
||||
|
||||
def resolve_prefix_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.Prefix.objects.all(), info)
|
||||
@strawberry.field
|
||||
def fhrp_group(self, id: int) -> FHRPGroupType:
|
||||
return models.FHRPGroup.objects.get(pk=id)
|
||||
fhrp_group_list: List[FHRPGroupType] = strawberry_django.field()
|
||||
|
||||
rir = ObjectField(RIRType)
|
||||
rir_list = ObjectListField(RIRType)
|
||||
@strawberry.field
|
||||
def fhrp_group_assignment(self, id: int) -> FHRPGroupAssignmentType:
|
||||
return models.FHRPGroupAssignment.objects.get(pk=id)
|
||||
fhrp_group_assignment_list: List[FHRPGroupAssignmentType] = strawberry_django.field()
|
||||
|
||||
def resolve_rir_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.RIR.objects.all(), info)
|
||||
@strawberry.field
|
||||
def vlan(self, id: int) -> VLANType:
|
||||
return models.VLAN.objects.get(pk=id)
|
||||
vlan_list: List[VLANType] = strawberry_django.field()
|
||||
|
||||
role = ObjectField(RoleType)
|
||||
role_list = ObjectListField(RoleType)
|
||||
@strawberry.field
|
||||
def vlan_group(self, id: int) -> VLANGroupType:
|
||||
return models.VLANGroup.objects.get(pk=id)
|
||||
vlan_group_list: List[VLANGroupType] = strawberry_django.field()
|
||||
|
||||
def resolve_role_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.Role.objects.all(), info)
|
||||
|
||||
route_target = ObjectField(RouteTargetType)
|
||||
route_target_list = ObjectListField(RouteTargetType)
|
||||
|
||||
def resolve_route_target_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.RouteTarget.objects.all(), info)
|
||||
|
||||
service = ObjectField(ServiceType)
|
||||
service_list = ObjectListField(ServiceType)
|
||||
|
||||
def resolve_service_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.Service.objects.all(), info)
|
||||
|
||||
service_template = ObjectField(ServiceTemplateType)
|
||||
service_template_list = ObjectListField(ServiceTemplateType)
|
||||
|
||||
def resolve_service_template_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.ServiceTemplate.objects.all(), info)
|
||||
|
||||
fhrp_group = ObjectField(FHRPGroupType)
|
||||
fhrp_group_list = ObjectListField(FHRPGroupType)
|
||||
|
||||
def resolve_fhrp_group_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.FHRPGroup.objects.all(), info)
|
||||
|
||||
fhrp_group_assignment = ObjectField(FHRPGroupAssignmentType)
|
||||
fhrp_group_assignment_list = ObjectListField(FHRPGroupAssignmentType)
|
||||
|
||||
def resolve_fhrp_group_assignment_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.FHRPGroupAssignment.objects.all(), info)
|
||||
|
||||
vlan = ObjectField(VLANType)
|
||||
vlan_list = ObjectListField(VLANType)
|
||||
|
||||
def resolve_vlan_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.VLAN.objects.all(), info)
|
||||
|
||||
vlan_group = ObjectField(VLANGroupType)
|
||||
vlan_group_list = ObjectListField(VLANGroupType)
|
||||
|
||||
def resolve_vlan_group_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.VLANGroup.objects.all(), info)
|
||||
|
||||
vrf = ObjectField(VRFType)
|
||||
vrf_list = ObjectListField(VRFType)
|
||||
|
||||
def resolve_vrf_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.VRF.objects.all(), info)
|
||||
@strawberry.field
|
||||
def vrf(self, id: int) -> VRFType:
|
||||
return models.VRF.objects.get(pk=id)
|
||||
vrf_list: List[VRFType] = strawberry_django.field()
|
||||
|
@ -1,9 +1,15 @@
|
||||
import graphene
|
||||
from typing import Annotated, List, Union
|
||||
|
||||
from ipam import filtersets, models
|
||||
from .mixins import IPAddressesMixin
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from circuits.graphql.types import ProviderType
|
||||
from dcim.graphql.types import SiteType
|
||||
from ipam import models
|
||||
from netbox.graphql.scalars import BigInt
|
||||
from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType
|
||||
from netbox.graphql.types import BaseObjectType, NetBoxObjectType, OrganizationalObjectType
|
||||
from .filters import *
|
||||
from .mixins import IPAddressesMixin
|
||||
|
||||
__all__ = (
|
||||
'ASNType',
|
||||
@ -25,164 +31,252 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class IPAddressFamilyType(graphene.ObjectType):
|
||||
|
||||
value = graphene.Int()
|
||||
label = graphene.String()
|
||||
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
self.label = f'IPv{value}'
|
||||
@strawberry.type
|
||||
class IPAddressFamilyType:
|
||||
value: int
|
||||
label: str
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class BaseIPAddressFamilyType:
|
||||
"""
|
||||
Base type for models that need to expose their IPAddress family type.
|
||||
"""
|
||||
family = graphene.Field(IPAddressFamilyType)
|
||||
|
||||
def resolve_family(self, _):
|
||||
@strawberry.field
|
||||
def family(self) -> IPAddressFamilyType:
|
||||
# Note that self, is an instance of models.IPAddress
|
||||
# thus resolves to the address family value.
|
||||
return IPAddressFamilyType(self.family)
|
||||
return IPAddressFamilyType(value=self.family, label=f'IPv{self.family}')
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.ASN,
|
||||
fields='__all__',
|
||||
filters=ASNFilter
|
||||
)
|
||||
class ASNType(NetBoxObjectType):
|
||||
asn = graphene.Field(BigInt)
|
||||
asn: BigInt
|
||||
rir: Annotated["RIRType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
|
||||
|
||||
class Meta:
|
||||
model = models.ASN
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.ASNFilterSet
|
||||
sites: List[SiteType]
|
||||
providers: List[ProviderType]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.ASNRange,
|
||||
fields='__all__',
|
||||
filters=ASNRangeFilter
|
||||
)
|
||||
class ASNRangeType(NetBoxObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.ASNRange
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.ASNRangeFilterSet
|
||||
start: BigInt
|
||||
end: BigInt
|
||||
rir: Annotated["RIRType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.Aggregate,
|
||||
fields='__all__',
|
||||
filters=AggregateFilter
|
||||
)
|
||||
class AggregateType(NetBoxObjectType, BaseIPAddressFamilyType):
|
||||
|
||||
class Meta:
|
||||
model = models.Aggregate
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.AggregateFilterSet
|
||||
prefix: str
|
||||
rir: Annotated["RIRType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.FHRPGroup,
|
||||
fields='__all__',
|
||||
filters=FHRPGroupFilter
|
||||
)
|
||||
class FHRPGroupType(NetBoxObjectType, IPAddressesMixin):
|
||||
|
||||
class Meta:
|
||||
model = models.FHRPGroup
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.FHRPGroupFilterSet
|
||||
|
||||
def resolve_auth_type(self, info):
|
||||
return self.auth_type or None
|
||||
fhrpgroupassignment_set: List[Annotated["FHRPGroupAssignmentType", strawberry.lazy('ipam.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.FHRPGroupAssignment,
|
||||
exclude=('interface_type', 'interface_id'),
|
||||
filters=FHRPGroupAssignmentFilter
|
||||
)
|
||||
class FHRPGroupAssignmentType(BaseObjectType):
|
||||
interface = graphene.Field('ipam.graphql.gfk_mixins.FHRPGroupInterfaceType')
|
||||
group: Annotated["FHRPGroupType", strawberry.lazy('ipam.graphql.types')]
|
||||
|
||||
class Meta:
|
||||
model = models.FHRPGroupAssignment
|
||||
exclude = ('interface_type', 'interface_id')
|
||||
filterset_class = filtersets.FHRPGroupAssignmentFilterSet
|
||||
@strawberry_django.field
|
||||
def interface(self) -> Annotated[Union[
|
||||
Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')],
|
||||
], strawberry.union("FHRPGroupInterfaceType")]:
|
||||
return self.interface
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.IPAddress,
|
||||
exclude=('assigned_object_type', 'assigned_object_id', 'address'),
|
||||
filters=IPAddressFilter
|
||||
)
|
||||
class IPAddressType(NetBoxObjectType, BaseIPAddressFamilyType):
|
||||
assigned_object = graphene.Field('ipam.graphql.gfk_mixins.IPAddressAssignmentType')
|
||||
address: str
|
||||
vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
|
||||
nat_inside: Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
|
||||
class Meta:
|
||||
model = models.IPAddress
|
||||
exclude = ('assigned_object_type', 'assigned_object_id')
|
||||
filterset_class = filtersets.IPAddressFilterSet
|
||||
nat_outside: List[Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')]]
|
||||
tunnel_terminations: List[Annotated["TunnelTerminationType", strawberry.lazy('vpn.graphql.types')]]
|
||||
services: List[Annotated["ServiceType", strawberry.lazy('ipam.graphql.types')]]
|
||||
|
||||
def resolve_role(self, info):
|
||||
return self.role or None
|
||||
@strawberry_django.field
|
||||
def assigned_object(self) -> Annotated[Union[
|
||||
Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["FHRPGroupType", strawberry.lazy('ipam.graphql.types')],
|
||||
Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')],
|
||||
], strawberry.union("IPAddressAssignmentType")]:
|
||||
return self.assigned_object
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.IPRange,
|
||||
fields='__all__',
|
||||
filters=IPRangeFilter
|
||||
)
|
||||
class IPRangeType(NetBoxObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.IPRange
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.IPRangeFilterSet
|
||||
|
||||
def resolve_role(self, info):
|
||||
return self.role or None
|
||||
start_address: str
|
||||
end_address: str
|
||||
vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
|
||||
role: Annotated["RoleType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.Prefix,
|
||||
fields='__all__',
|
||||
filters=PrefixFilter
|
||||
)
|
||||
class PrefixType(NetBoxObjectType, BaseIPAddressFamilyType):
|
||||
|
||||
class Meta:
|
||||
model = models.Prefix
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.PrefixFilterSet
|
||||
prefix: str
|
||||
site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
|
||||
vlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
role: Annotated["RoleType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.RIR,
|
||||
fields='__all__',
|
||||
filters=RIRFilter
|
||||
)
|
||||
class RIRType(OrganizationalObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.RIR
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.RIRFilterSet
|
||||
asn_ranges: List[Annotated["ASNRangeType", strawberry.lazy('ipam.graphql.types')]]
|
||||
asns: List[Annotated["ASNType", strawberry.lazy('ipam.graphql.types')]]
|
||||
aggregates: List[Annotated["AggregateType", strawberry.lazy('ipam.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.Role,
|
||||
fields='__all__',
|
||||
filters=RoleFilter
|
||||
)
|
||||
class RoleType(OrganizationalObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.Role
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.RoleFilterSet
|
||||
prefixes: List[Annotated["PrefixType", strawberry.lazy('ipam.graphql.types')]]
|
||||
ip_ranges: List[Annotated["IPRangeType", strawberry.lazy('ipam.graphql.types')]]
|
||||
vlans: List[Annotated["VLANType", strawberry.lazy('ipam.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.RouteTarget,
|
||||
fields='__all__',
|
||||
filters=RouteTargetFilter
|
||||
)
|
||||
class RouteTargetType(NetBoxObjectType):
|
||||
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
|
||||
|
||||
class Meta:
|
||||
model = models.RouteTarget
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.RouteTargetFilterSet
|
||||
importing_l2vpns: List[Annotated["L2VPNType", strawberry.lazy('vpn.graphql.types')]]
|
||||
exporting_l2vpns: List[Annotated["L2VPNType", strawberry.lazy('vpn.graphql.types')]]
|
||||
importing_vrfs: List[Annotated["VRFType", strawberry.lazy('ipam.graphql.types')]]
|
||||
exporting_vrfs: List[Annotated["VRFType", strawberry.lazy('ipam.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.Service,
|
||||
fields='__all__',
|
||||
filters=ServiceFilter
|
||||
)
|
||||
class ServiceType(NetBoxObjectType):
|
||||
ports: List[int]
|
||||
device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
virtual_machine: Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')] | None
|
||||
|
||||
class Meta:
|
||||
model = models.Service
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.ServiceFilterSet
|
||||
ipaddresses: List[Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.ServiceTemplate,
|
||||
fields='__all__',
|
||||
filters=ServiceTemplateFilter
|
||||
)
|
||||
class ServiceTemplateType(NetBoxObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.ServiceTemplate
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.ServiceTemplateFilterSet
|
||||
ports: List[int]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.VLAN,
|
||||
fields='__all__',
|
||||
filters=VLANFilter
|
||||
)
|
||||
class VLANType(NetBoxObjectType):
|
||||
site: Annotated["SiteType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
group: Annotated["VLANGroupType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
|
||||
role: Annotated["RoleType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
|
||||
class Meta:
|
||||
model = models.VLAN
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.VLANFilterSet
|
||||
interfaces_as_untagged: List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]
|
||||
vminterfaces_as_untagged: List[Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')]]
|
||||
wirelesslan_set: List[Annotated["WirelessLANType", strawberry.lazy('wireless.graphql.types')]]
|
||||
prefixes: List[Annotated["PrefixType", strawberry.lazy('ipam.graphql.types')]]
|
||||
interfaces_as_tagged: List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]
|
||||
vminterfaces_as_tagged: List[Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.VLANGroup,
|
||||
exclude=('scope_type', 'scope_id'),
|
||||
filters=VLANGroupFilter
|
||||
)
|
||||
class VLANGroupType(OrganizationalObjectType):
|
||||
scope = graphene.Field('ipam.graphql.gfk_mixins.VLANGroupScopeType')
|
||||
|
||||
class Meta:
|
||||
model = models.VLANGroup
|
||||
exclude = ('scope_type', 'scope_id')
|
||||
filterset_class = filtersets.VLANGroupFilterSet
|
||||
vlans: List[VLANType]
|
||||
|
||||
@strawberry_django.field
|
||||
def scope(self) -> Annotated[Union[
|
||||
Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')],
|
||||
Annotated["ClusterGroupType", strawberry.lazy('virtualization.graphql.types')],
|
||||
Annotated["LocationType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["RackType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["RegionType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["SiteType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')],
|
||||
], strawberry.union("VLANGroupScopeType")]:
|
||||
return self.scope
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.VRF,
|
||||
fields='__all__',
|
||||
filters=VRFFilter
|
||||
)
|
||||
class VRFType(NetBoxObjectType):
|
||||
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
|
||||
|
||||
class Meta:
|
||||
model = models.VRF
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.VRFFilterSet
|
||||
interfaces: List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]
|
||||
ip_addresses: List[Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')]]
|
||||
vminterfaces: List[Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')]]
|
||||
ip_ranges: List[Annotated["IPRangeType", strawberry.lazy('ipam.graphql.types')]]
|
||||
export_targets: List[Annotated["RouteTargetType", strawberry.lazy('ipam.graphql.types')]]
|
||||
import_targets: List[Annotated["RouteTargetType", strawberry.lazy('ipam.graphql.types')]]
|
||||
prefixes: List[Annotated["PrefixType", strawberry.lazy('ipam.graphql.types')]]
|
||||
|
@ -8,8 +8,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from ipam.choices import *
|
||||
from ipam.constants import *
|
||||
from netbox.models import PrimaryModel
|
||||
from utilities.utils import array_to_string
|
||||
|
||||
from utilities.data import array_to_string
|
||||
|
||||
__all__ = (
|
||||
'Service',
|
||||
|
@ -3,8 +3,8 @@ from django.db.models import Count, F, OuterRef, Q, Subquery, Value
|
||||
from django.db.models.expressions import RawSQL
|
||||
from django.db.models.functions import Round
|
||||
|
||||
from utilities.query import count_related
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.utils import count_related
|
||||
|
||||
__all__ = (
|
||||
'ASNRangeQuerySet',
|
||||
|
@ -9,8 +9,8 @@ from circuits.models import Provider
|
||||
from dcim.filtersets import InterfaceFilterSet
|
||||
from dcim.models import Interface, Site
|
||||
from netbox.views import generic
|
||||
from utilities.query import count_related
|
||||
from utilities.tables import get_table_ordering
|
||||
from utilities.utils import count_related
|
||||
from utilities.views import ViewTab, register_model_view
|
||||
from virtualization.filtersets import VMInterfaceFilterSet
|
||||
from virtualization.models import VMInterface
|
||||
|
@ -27,9 +27,13 @@ class BaseModelSerializer(serializers.ModelSerializer):
|
||||
self.nested = nested
|
||||
self._requested_fields = fields
|
||||
|
||||
# Disable validators for nested objects (which already exist)
|
||||
if self.nested:
|
||||
self.validators = []
|
||||
|
||||
# If this serializer is nested but no fields have been specified,
|
||||
# default to using Meta.brief_fields (if set)
|
||||
if nested and not fields:
|
||||
if self.nested and not fields:
|
||||
self._requested_fields = getattr(self.Meta, 'brief_fields', None)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -81,8 +85,9 @@ class ValidatedModelSerializer(BaseModelSerializer):
|
||||
attrs.pop('custom_fields', None)
|
||||
|
||||
# Skip ManyToManyFields
|
||||
opts = self.Meta.model._meta
|
||||
m2m_values = {}
|
||||
for field in self.Meta.model._meta.local_many_to_many:
|
||||
for field in [*opts.local_many_to_many, *opts.related_objects]:
|
||||
if field.name in attrs:
|
||||
m2m_values[field.name] = attrs.pop(field.name)
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user