Update from feature

This commit is contained in:
Daniel Sheppard 2025-04-09 10:24:56 -05:00
commit 738ef63527
365 changed files with 8922 additions and 3114 deletions

View File

@ -1,6 +1,6 @@
# The Python web framework on which NetBox is built # The Python web framework on which NetBox is built
# https://docs.djangoproject.com/en/stable/releases/ # https://docs.djangoproject.com/en/stable/releases/
Django<5.2 Django==5.2.*
# Django middleware which permits cross-domain API requests # Django middleware which permits cross-domain API requests
# https://github.com/adamchainz/django-cors-headers/blob/main/CHANGELOG.rst # https://github.com/adamchainz/django-cors-headers/blob/main/CHANGELOG.rst
@ -42,6 +42,10 @@ django-rich
# https://github.com/rq/django-rq/blob/master/CHANGELOG.md # https://github.com/rq/django-rq/blob/master/CHANGELOG.md
django-rq django-rq
# Provides a variety of storage backends
# https://github.com/jschneier/django-storages/blob/master/CHANGELOG.rst
django-storages
# Abstraction models for rendering and paginating HTML tables # Abstraction models for rendering and paginating HTML tables
# https://github.com/jieter/django-tables2/blob/master/CHANGELOG.md # https://github.com/jieter/django-tables2/blob/master/CHANGELOG.md
django-tables2 django-tables2
@ -78,6 +82,10 @@ gunicorn
# https://jinja.palletsprojects.com/changes/ # https://jinja.palletsprojects.com/changes/
Jinja2 Jinja2
# JSON schema validation
# https://github.com/python-jsonschema/jsonschema/blob/main/CHANGELOG.rst
jsonschema
# Simple markup language for rendering HTML # Simple markup language for rendering HTML
# https://python-markdown.github.io/changelog/ # https://python-markdown.github.io/changelog/
Markdown Markdown

View File

@ -54,7 +54,7 @@ pg_dump --username netbox --password --host localhost -s netbox > netbox_schema.
By default, NetBox stores uploaded files (such as image attachments) in its media directory. To fully replicate an instance of NetBox, you'll need to copy both the database and the media files. By default, NetBox stores uploaded files (such as image attachments) in its media directory. To fully replicate an instance of NetBox, you'll need to copy both the database and the media files.
!!! note !!! note
These operations are not necessary if your installation is utilizing a [remote storage backend](../configuration/system.md#storage_backend). These operations are not necessary if your installation is utilizing a [remote storage backend](../configuration/system.md#storages).
### Archive the Media Directory ### Archive the Media Directory

View File

@ -33,3 +33,21 @@ Note that a plugin must be listed in `PLUGINS` for its configuration to take eff
--- ---
## PLUGINS_CATALOG_CONFIG
Default: Empty
This parameter controls how individual plugins are displayed in the plugins catalog under Admin > System > Plugins. Adding a plugin to the `hidden` list will omit that plugin from the catalog. Adding a plugin to the `static` list will display the plugin, but not link to the plugin details or upgrade instructions.
An example configuration is shown below:
```python
PLUGINS_CATALOG_CONFIG = {
'hidden': [
'plugin1',
],
'static': [
'plugin2',
],
}
```

View File

@ -25,7 +25,30 @@ ALLOWED_HOSTS = ['*']
## DATABASE ## DATABASE
NetBox requires access to a PostgreSQL 13 or later database service to store data. This service can run locally on the NetBox server or on a remote system. The following parameters must be defined within the `DATABASE` dictionary: !!! warning "Legacy Configuration Parameter"
The `DATABASE` configuration parameter is deprecated and will be removed in a future release. Users are advised to adopt the new `DATABASES` (plural) parameter, which allows for the configuration of multiple databases.
See the [`DATABASES`](#databases) configuration below for usage.
---
## DATABASES
!!! info "This parameter was introduced in NetBox v4.3."
NetBox requires access to a PostgreSQL 14 or later database service to store data. This service can run locally on the NetBox server or on a remote system. Databases are defined as named dictionaries:
```python
DATABASES = {
'default': {...},
'external1': {...},
'external2': {...},
}
```
NetBox itself requires only that a `default` database is defined. However, certain plugins may require the configuration of additional databases. (Consider also configuring the [`DATABASE_ROUTERS`](./system.md#database_routers) parameter when multiple databases are in use.)
The following parameters must be defined for each database:
* `NAME` - Database name * `NAME` - Database name
* `USER` - PostgreSQL username * `USER` - PostgreSQL username
@ -38,14 +61,16 @@ NetBox requires access to a PostgreSQL 13 or later database service to store dat
Example: Example:
```python ```python
DATABASE = { DATABASES = {
'ENGINE': 'django.db.backends.postgresql', 'default': {
'NAME': 'netbox', # Database name 'ENGINE': 'django.db.backends.postgresql',
'USER': 'netbox', # PostgreSQL username 'NAME': 'netbox', # Database name
'PASSWORD': 'J5brHrAXFLQSif0K', # PostgreSQL password 'USER': 'netbox', # PostgreSQL username
'HOST': 'localhost', # Database server 'PASSWORD': 'J5brHrAXFLQSif0K', # PostgreSQL password
'PORT': '', # Database port (leave blank for default) 'HOST': 'localhost', # Database server
'CONN_MAX_AGE': 300, # Max database connection age 'PORT': '', # Database port (leave blank for default)
'CONN_MAX_AGE': 300, # Max database connection age
}
} }
``` ```
@ -53,7 +78,7 @@ DATABASE = {
NetBox supports all PostgreSQL database options supported by the underlying Django framework. For a complete list of available parameters, please see [the Django documentation](https://docs.djangoproject.com/en/stable/ref/settings/#databases). NetBox supports all PostgreSQL database options supported by the underlying Django framework. For a complete list of available parameters, please see [the Django documentation](https://docs.djangoproject.com/en/stable/ref/settings/#databases).
!!! warning !!! warning
Make sure to use a PostgreSQL-compatible backend for the ENGINE setting. If you don't specify an ENGINE, the default will be django.db.backends.postgresql. The `ENGINE` parameter must specify a PostgreSQL-compatible database backend. If not defined, the default engine `django.db.backends.postgresql` will be used.
--- ---

View File

@ -2,7 +2,10 @@
## ALLOW_TOKEN_RETRIEVAL ## ALLOW_TOKEN_RETRIEVAL
Default: True Default: False
!!! note
The default value of this parameter changed from true to false in NetBox v4.3.0.
If disabled, the values of API tokens will not be displayed after each token's initial creation. A user **must** record the value of a token prior to its creation, or it will be lost. Note that this affects _all_ users, regardless of assigned permissions. If disabled, the values of API tokens will not be displayed after each token's initial creation. A user **must** record the value of a token prior to its creation, or it will be lost. Note that this affects _all_ users, regardless of assigned permissions.
@ -186,6 +189,17 @@ The lifetime (in seconds) of the authentication cookie issued to a NetBox user u
--- ---
## LOGIN_FORM_HIDDEN
Default: False
Option to hide the login form when only SSO authentication is in use.
!!! warning
If the SSO provider is unreachable, login to NetBox will be impossible if this option is enabled. The only recourse is to disable it in the local configuration and restart the NetBox service.
---
## LOGOUT_REDIRECT_URL ## LOGOUT_REDIRECT_URL
Default: `'home'` Default: `'home'`

View File

@ -12,6 +12,14 @@ BASE_PATH = 'netbox/'
--- ---
## DATABASE_ROUTERS
Default: `[]` (empty list)
An iterable of [database routers](https://docs.djangoproject.com/en/stable/topics/db/multi-db/) to use for automatically selecting the appropriate database(s) for a query. This is useful only when [multiple databases](./required-parameters.md#databases) have been configured.
---
## DEFAULT_LANGUAGE ## DEFAULT_LANGUAGE
Default: `en-us` (US English) Default: `en-us` (US English)
@ -64,7 +72,7 @@ Email is sent from NetBox only for critical events or if configured for [logging
## HTTP_PROXIES ## HTTP_PROXIES
Default: None Default: Empty
A dictionary of HTTP proxies to use for outbound requests originating from NetBox (e.g. when sending webhook requests). Proxies should be specified by schema (HTTP and HTTPS) as per the [Python requests library documentation](https://requests.readthedocs.io/en/latest/user/advanced/#proxies). For example: A dictionary of HTTP proxies to use for outbound requests originating from NetBox (e.g. when sending webhook requests). Proxies should be specified by schema (HTTP and HTTPS) as per the [Python requests library documentation](https://requests.readthedocs.io/en/latest/user/advanced/#proxies). For example:
@ -75,6 +83,8 @@ HTTP_PROXIES = {
} }
``` ```
If more flexibility is needed in determining which proxy to use for a given request, consider implementing one or more custom proxy routers via the [`PROXY_ROUTERS`](#proxy_routers) parameter.
--- ---
## INTERNAL_IPS ## INTERNAL_IPS
@ -160,6 +170,16 @@ The file path to the location where media files (such as image attachments) are
--- ---
## PROXY_ROUTERS
Default: `["utilities.proxy.DefaultProxyRouter"]`
A list of Python classes responsible for determining which proxy server(s) to use for outbound HTTP requests. Each item in the list can be the class itself or the dotted path to the class.
The `route()` method on each class must return a dictionary of candidate proxies arranged by protocol (e.g. `http` and/or `https`), or None if no viable proxy can be determined. The default class, `DefaultProxyRouter`, simply returns the content of [`HTTP_PROXIES`](#http_proxies).
---
## REPORTS_ROOT ## REPORTS_ROOT
Default: `$INSTALL_ROOT/netbox/reports/` Default: `$INSTALL_ROOT/netbox/reports/`
@ -184,23 +204,46 @@ The dotted path to the desired search backend class. `CachedValueSearchBackend`
--- ---
## STORAGE_BACKEND ## STORAGES
Default: None (local storage) The backend storage engine for handling uploaded files such as [image attachments](../models/extras/imageattachment.md) and [custom scripts](../customization/custom-scripts.md). NetBox integrates with the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) and [`django-storage-swift`](https://github.com/dennisv/django-storage-swift) libraries, which provide backends for several popular file storage services. If not configured, local filesystem storage will be used.
The backend storage engine for handling uploaded files (e.g. image attachments). NetBox supports integration with the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) and [`django-storage-swift`](https://github.com/dennisv/django-storage-swift) packages, which provide backends for several popular file storage services. If not configured, local filesystem storage will be used. By default, the following configuration is used:
The configuration parameters for the specified storage backend are defined under the `STORAGE_CONFIG` setting. ```python
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
},
"scripts": {
"BACKEND": "extras.storage.ScriptFileSystemStorage",
},
}
```
--- Within the `STORAGES` dictionary, `"default"` is used for image uploads, "staticfiles" is for static files and `"scripts"` is used for custom scripts.
## STORAGE_CONFIG If using a remote storage like S3, define the config as `STORAGES[key]["OPTIONS"]` for each storage item as needed. For example:
Default: Empty ```python
STORAGES = {
"scripts": {
"BACKEND": "storages.backends.s3boto3.S3Boto3Storage",
"OPTIONS": {
'access_key': 'access key',
'secret_key': 'secret key',
}
},
}
```
A dictionary of configuration parameters for the storage backend configured as `STORAGE_BACKEND`. The specific parameters to be used here are specific to each backend; see the documentation for your selected backend ([`django-storages`](https://django-storages.readthedocs.io/en/stable/) or [`django-storage-swift`](https://github.com/dennisv/django-storage-swift)) for more detail. The specific configuration settings for each storage backend can be found in the [django-storages documentation](https://django-storages.readthedocs.io/en/latest/index.html).
If `STORAGE_BACKEND` is not defined, this setting will be ignored. !!! note
Any keys defined in the `STORAGES` configuration parameter replace those in the default configuration. It is only necessary to define keys within the `STORAGES` for the specific backend(s) you wish to configure.
--- ---

View File

@ -140,6 +140,8 @@ The Script class provides two convenience methods for reading data from files:
These two methods will load data in YAML or JSON format, respectively, from files within the local path (i.e. `SCRIPTS_ROOT`). These two methods will load data in YAML or JSON format, respectively, from files within the local path (i.e. `SCRIPTS_ROOT`).
**Note:** These convenience methods are deprecated and will be removed in NetBox v4.4. These only work if running scripts within the local path, they will not work if using a storage other than ScriptFileSystemStorage.
## Logging ## Logging
The Script object provides a set of convenient functions for recording messages at different severity levels: The Script object provides a set of convenient functions for recording messages at different severity levels:

View File

@ -76,11 +76,13 @@ Create the following for each model:
## 13. GraphQL API components ## 13. GraphQL API components
Create a GraphQL object type for the model in `graphql/types.py` by subclassing the appropriate class from `netbox.graphql.types`. Create the following for each model:
**Note:** GraphQL unit tests may fail citing null values on a non-nullable field if related objects are prefetched. You may need to fix this by setting the type annotation to be `= strawberry_django.field(select_related=["policy"])` or similar. * GraphQL object type for the model in `graphql/types.py` (subclass the appropriate class from `netbox.graphql.types`)
* Add a GraphQL filter for the model in `graphql/filters.py`
* Extend the query class for the app in `graphql/schema.py` with the individual object and object list fields
Also extend the schema class defined in `graphql/schema.py` with the individual object and object list fields per the established convention. **Note:** GraphQL unit tests may fail citing null values on a non-nullable field if related objects are prefetched. You may need to fix this by setting the type annotation to be `= strawberry_django.field(select_related=["foo"])` or similar.
## 14. Add tests ## 14. Add tests

View File

@ -6,7 +6,7 @@ Below is a list of tasks to consider when adding a new field to a core model.
Add the field to the model, taking care to address any of the following conditions. Add the field to the model, taking care to address any of the following conditions.
* When adding a GenericForeignKey field, also add an index under `Meta` for its two concrete fields. For example: * When adding a GenericForeignKey field, you may need add an index under `Meta` for its two concrete fields. (This is required only for non-unique GFK relationships, as the unique constraint introduces its own index.) For example:
```python ```python
class Meta: class Meta:

View File

@ -115,7 +115,7 @@ You may also need to set up the yarn packages as shown in the [Web UI Developmen
Within the `netbox/netbox/` directory, copy `configuration_example.py` to `configuration.py` and update the following parameters: Within the `netbox/netbox/` directory, copy `configuration_example.py` to `configuration.py` and update the following parameters:
* `ALLOWED_HOSTS`: This can be set to `['*']` for development purposes * `ALLOWED_HOSTS`: This can be set to `['*']` for development purposes
* `DATABASE`: PostgreSQL database connection parameters * `DATABASES`: PostgreSQL database connection parameters
* `REDIS`: Redis configuration (if different from the defaults) * `REDIS`: Redis configuration (if different from the defaults)
* `SECRET_KEY`: Set to a random string (use `generate_secret_key.py` in the parent directory to generate a suitable key) * `SECRET_KEY`: Set to a random string (use `generate_secret_key.py` in the parent directory to generate a suitable key)
* `DEBUG`: Set to `True` * `DEBUG`: Set to `True`

View File

@ -2,8 +2,8 @@
This section entails the installation and configuration of a local PostgreSQL database. If you already have a PostgreSQL database service in place, skip to [the next section](2-redis.md). This section entails the installation and configuration of a local PostgreSQL database. If you already have a PostgreSQL database service in place, skip to [the next section](2-redis.md).
!!! warning "PostgreSQL 13 or later required" !!! warning "PostgreSQL 14 or later required"
NetBox requires PostgreSQL 13 or later. Please note that MySQL and other relational databases are **not** supported. NetBox requires PostgreSQL 14 or later. Please note that MySQL and other relational databases are **not** supported.
## Installation ## Installation
@ -34,7 +34,7 @@ This section entails the installation and configuration of a local PostgreSQL da
sudo systemctl enable --now postgresql sudo systemctl enable --now postgresql
``` ```
Before continuing, verify that you have installed PostgreSQL 13 or later: Before continuing, verify that you have installed PostgreSQL 14 or later:
```no-highlight ```no-highlight
psql -V psql -V

View File

@ -128,7 +128,7 @@ sudo cp configuration_example.py configuration.py
Open `configuration.py` with your preferred editor to begin configuring NetBox. NetBox offers [many configuration parameters](../configuration/index.md), but only the following four are required for new installations: Open `configuration.py` with your preferred editor to begin configuring NetBox. NetBox offers [many configuration parameters](../configuration/index.md), but only the following four are required for new installations:
* `ALLOWED_HOSTS` * `ALLOWED_HOSTS`
* `DATABASE` * `DATABASES` (or `DATABASE`)
* `REDIS` * `REDIS`
* `SECRET_KEY` * `SECRET_KEY`
@ -146,18 +146,22 @@ If you are not yet sure what the domain name and/or IP address of the NetBox ins
ALLOWED_HOSTS = ['*'] ALLOWED_HOSTS = ['*']
``` ```
### DATABASE ### DATABASES
This parameter holds the database configuration details. You must define the username and password used when you configured PostgreSQL. If the service is running on a remote host, update the `HOST` and `PORT` parameters accordingly. See the [configuration documentation](../configuration/required-parameters.md#database) for more detail on individual parameters. This parameter holds the PostgreSQL database configuration details. The default database must be defined; additional databases may be defined as needed e.g. by plugins.
A username and password must be defined for the default database. If the service is running on a remote host, update the `HOST` and `PORT` parameters accordingly. See the [configuration documentation](../configuration/required-parameters.md#databases) for more detail on individual parameters.
```python ```python
DATABASE = { DATABASES = {
'NAME': 'netbox', # Database name 'default': {
'USER': 'netbox', # PostgreSQL username 'NAME': 'netbox', # Database name
'PASSWORD': 'J5brHrAXFLQSif0K', # PostgreSQL password 'USER': 'netbox', # PostgreSQL username
'HOST': 'localhost', # Database server 'PASSWORD': 'J5brHrAXFLQSif0K', # PostgreSQL password
'PORT': '', # Database port (leave blank for default) 'HOST': 'localhost', # Database server
'CONN_MAX_AGE': 300, # Max database connection age (seconds) 'PORT': '', # Database port (leave blank for default)
'CONN_MAX_AGE': 300, # Max database connection age (seconds)
}
} }
``` ```
@ -207,7 +211,7 @@ All Python packages required by NetBox are listed in `requirements.txt` and will
### Remote File Storage ### Remote File Storage
By default, NetBox will use the local filesystem to store uploaded files. To use a remote filesystem, install the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) library and configure your [desired storage backend](../configuration/system.md#storage_backend) in `configuration.py`. By default, NetBox will use the local filesystem to store uploaded files. To use a remote filesystem, install the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) library and configure your [desired storage backend](../configuration/system.md#storages) in `configuration.py`.
```no-highlight ```no-highlight
sudo sh -c "echo 'django-storages' >> /opt/netbox/local_requirements.txt" sudo sh -c "echo 'django-storages' >> /opt/netbox/local_requirements.txt"

View File

@ -21,7 +21,7 @@ The following sections detail how to set up a new instance of NetBox:
| Dependency | Supported Versions | | Dependency | Supported Versions |
|------------|--------------------| |------------|--------------------|
| Python | 3.10, 3.11, 3.12 | | Python | 3.10, 3.11, 3.12 |
| PostgreSQL | 13+ | | PostgreSQL | 14+ |
| Redis | 4.0+ | | Redis | 4.0+ |
Below is a simplified overview of the NetBox application stack for reference: Below is a simplified overview of the NetBox application stack for reference:

View File

@ -20,7 +20,7 @@ NetBox requires the following dependencies:
| Dependency | Supported Versions | | Dependency | Supported Versions |
|------------|--------------------| |------------|--------------------|
| Python | 3.10, 3.11, 3.12 | | Python | 3.10, 3.11, 3.12 |
| PostgreSQL | 13+ | | PostgreSQL | 14+ |
| Redis | 4.0+ | | Redis | 4.0+ |
## 3. Install the Latest Release ## 3. Install the Latest Release

View File

@ -11,7 +11,7 @@ curl -H "Authorization: Token $TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-H "Accept: application/json" \ -H "Accept: application/json" \
http://netbox/graphql/ \ http://netbox/graphql/ \
--data '{"query": "query {circuit_list(status:\"active\") {cid provider {name}}}"}' --data '{"query": "query {circuit_list(filters:{status: STATUS_ACTIVE}) {cid provider {name}}}"}'
``` ```
The response will include the requested data formatted as JSON: The response will include the requested data formatted as JSON:
@ -51,19 +51,48 @@ For more detail on constructing GraphQL queries, see the [GraphQL queries docume
## Filtering ## Filtering
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: !!! note "Changed in NetBox v4.3"
The filtering syntax fo the GraphQL API has changed substantially in NetBox v4.3.
Filters can be specified as key-value pairs within parentheses immediately following the query name. For example, the following will return only active sites:
``` ```
query { query {
site_list(filters: {region: "us-nc", status: "active"}) { site_list(
filters: {
status: STATUS_ACTIVE
}
) {
name name
} }
} }
``` ```
In addition, filtering can be done on list of related objects as shown in the following query:
Filters can be combined with logical operators, such as `OR` and `NOT`. For example, the following will return every site that is planned _or_ assigned to a tenant named Foo:
``` ```
{ query {
site_list(
filters: {
status: STATUS_PLANNED,
OR: {
tenant: {
name: {
exact: "Foo"
}
}
}
}
) {
name
}
}
```
Filtering can also be applied to related objects. For example, the following query will return only enabled interfaces for each device:
```
query {
device_list { device_list {
id id
name name
@ -102,6 +131,18 @@ Certain queries can return multiple types of objects, for example cable terminat
``` ```
The field "class_type" is an easy way to distinguish what type of object it is when viewing the returned data, or when filtering. It contains the class name, for example "CircuitTermination" or "ConsoleServerPort". The field "class_type" is an easy way to distinguish what type of object it is when viewing the returned data, or when filtering. It contains the class name, for example "CircuitTermination" or "ConsoleServerPort".
## Pagination
Queries can be paginated by specifying pagination in the query and supplying an offset and optionaly a limit in the query. If no limit is given, a default of 100 is used. Queries are not paginated unless requested in the query. An example paginated query is shown below:
```
query {
device_list(pagination: { offset: 0, limit: 20 }) {
id
}
}
```
## Authentication ## Authentication
NetBox's GraphQL API uses the same API authentication tokens as its REST API. Authentication tokens are included with requests by attaching an `Authorization` HTTP header in the following form: NetBox's GraphQL API uses the same API authentication tokens as its REST API. Authentication tokens are included with requests by attaching an `Authorization` HTTP header in the following form:

View File

@ -79,5 +79,5 @@ NetBox is built on the [Django](https://djangoproject.com/) Python framework and
| HTTP service | nginx or Apache | | HTTP service | nginx or Apache |
| WSGI service | gunicorn or uWSGI | | WSGI service | gunicorn or uWSGI |
| Application | Django/Python | | Application | Django/Python |
| Database | PostgreSQL 13+ | | Database | PostgreSQL 14+ |
| Task queuing | Redis/django-rq | | Task queuing | Redis/django-rq |

View File

@ -44,6 +44,12 @@ A set of rules (one per line) identifying filenames to ignore during synchroniza
| `*.txt` | Ignore any files with a `.txt` extension | | `*.txt` | Ignore any files with a `.txt` extension |
| `data???.json` | Ignore e.g. `data123.json` | | `data???.json` | Ignore e.g. `data123.json` |
### Sync Interval
!!! info "This field was introduced in NetBox v4.3."
The interval at which the data source should automatically synchronize. If not set, the data source must be synchronized manually.
### Last Synced ### Last Synced
The date and time at which the source was most recently synchronized successfully. The date and time at which the source was most recently synchronized successfully.

View File

@ -4,6 +4,10 @@ Devices can be organized by functional roles, which are fully customizable by th
## Fields ## Fields
### Parent
The parent role of which this role is a child (optional).
### Name ### Name
A unique human-friendly name. A unique human-friendly name.

View File

@ -1,5 +1,8 @@
# Inventory Items # Inventory Items
!!! warning "Deprecation Warning"
Beginning in NetBox v4.3, the use of inventory items has been deprecated. They are planned for removal in a future NetBox release. Users are strongly encouraged to begin using [modules](./module.md) and [module types](./moduletype.md) in place of inventory items. Modules provide enhanced functionality and can be configured with user-defined attributes.
Inventory items represent hardware components installed within a device, such as a power supply or CPU or line card. They are intended to be used primarily for inventory purposes. Inventory items represent hardware components installed within a device, such as a power supply or CPU or line card. They are intended to be used primarily for inventory purposes.
Inventory items are hierarchical in nature, such that any individual item may be designated as the parent for other items. For example, an inventory item might be created to represent a line card which houses several SFP optics, each of which exists as a child item within the device. An inventory item may also be associated with a specific component within the same device. For example, you may wish to associate a transceiver with an interface. Inventory items are hierarchical in nature, such that any individual item may be designated as the parent for other items. For example, an inventory item might be created to represent a line card which houses several SFP optics, each of which exists as a child item within the device. An inventory item may also be associated with a specific component within the same device. For example, you may wish to associate a transceiver with an interface.

View File

@ -1,5 +1,8 @@
# Inventory Item Roles # Inventory Item Roles
!!! warning "Deprecation Warning"
Beginning in NetBox v4.3, the use of inventory items has been deprecated. They are planned for removal in a future NetBox release. Users are strongly encouraged to begin using [modules](./module.md) and [module types](./moduletype.md) in place of inventory items. Modules provide enhanced functionality and can be configured with user-defined attributes.
Inventory items can be organized by functional roles, which are fully customizable by the user. For example, you might create roles for power supplies, fans, interface optics, etc. Inventory items can be organized by functional roles, which are fully customizable by the user. For example, you might create roles for power supplies, fans, interface optics, etc.
## Fields ## Fields

View File

@ -1,3 +1,6 @@
# Inventory Item Templates # Inventory Item Templates
!!! warning "Deprecation Warning"
Beginning in NetBox v4.3, the use of inventory items has been deprecated. They are planned for removal in a future NetBox release. Users are strongly encouraged to begin using [modules](./module.md) and [module types](./moduletype.md) in place of inventory items. Modules provide enhanced functionality and can be configured with user-defined attributes.
A template for an inventory item that will be automatically created when instantiating a new device. All attributes of this object will be copied to the new inventory item, including the associations with a parent item and assigned component, if any. See the [inventory item](./inventoryitem.md) documentation for more detail. A template for an inventory item that will be automatically created when instantiating a new device. All attributes of this object will be copied to the new inventory item, including the associations with a parent item and assigned component, if any. See the [inventory item](./inventoryitem.md) documentation for more detail.

View File

@ -43,3 +43,11 @@ The numeric weight of the module, including a unit designation (e.g. 3 kilograms
### Airflow ### Airflow
The direction in which air circulates through the device chassis for cooling. The direction in which air circulates through the device chassis for cooling.
### Profile
The assigned [profile](./moduletypeprofile.md) for the type of module. Profiles can be used to classify module types by function (e.g. power supply, hard disk, etc.), and they support the addition of user-configurable attributes on module types. The assignment of a module type to a profile is optional.
### Attributes
Depending on the module type's assigned [profile](./moduletypeprofile.md) (if any), one or more user-defined attributes may be available to configure.

View File

@ -0,0 +1,40 @@
# Module Type Profiles
!!! info "This model was introduced in NetBox v4.3."
Each [module type](./moduletype.md) may optionally be assigned a profile according to its classification. A profile can extend module types with user-configured attributes. For example, you might want to specify the input current and voltage of a power supply, or the clock speed and number of cores for a processor.
Module type attributes are managed via the configuration of a [JSON schema](https://json-schema.org/) on the profile. For example, the following schema introduces three module type attributes, two of which are designated as required attributes.
```json
{
"properties": {
"type": {
"type": "string",
"title": "Disk type",
"enum": ["HD", "SSD", "NVME"],
"default": "HD"
},
"capacity": {
"type": "integer",
"title": "Capacity (GB)",
"description": "Gross disk size"
},
"speed": {
"type": "integer",
"title": "Speed (RPM)"
}
},
"required": [
"type", "capacity"
]
}
```
The assignment of module types to a profile is optional. The designation of a schema for a profile is also optional: A profile can be used simply as a mechanism for classifying module types if the addition of custom attributes is not needed.
## Fields
### Schema
This field holds the [JSON schema](https://json-schema.org/) for the profile. The configured JSON schema must be valid (or the field must be null).

View File

@ -29,6 +29,19 @@ An alternative physical label identifying the power outlet.
The type of power outlet. The type of power outlet.
### Status
The operational status of the power outlet. By default, the following statuses are available:
* Enabled
* Disabled
* Faulty
!!! tip "Custom power outlet statuses"
Additional power outlet statuses may be defined by setting `PowerOutlet.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
!!! info "This field was introduced in NetBox v4.3."
### Color ### Color
!!! info "This field was introduced in NetBox v4.2." !!! info "This field was introduced in NetBox v4.2."

View File

@ -40,7 +40,7 @@ The number of the numerically lowest unit in the rack. This value defaults to on
### Outer Dimensions ### Outer Dimensions
The external width and depth of the rack can be tracked to aid in floorplan calculations. These measurements must be designated in either millimeters or inches. The external width, height and depth of the rack can be tracked to aid in floorplan calculations. These measurements must be designated in either millimeters or inches.
### Mounting Depth ### Mounting Depth

View File

@ -1,16 +0,0 @@
# Branches
!!! danger "Deprecated Feature"
This feature has been deprecated in NetBox v4.2 and will be removed in a future release. Please consider using the [netbox-branching plugin](https://github.com/netboxlabs/netbox-branching), which provides much more robust functionality.
A branch is a collection of related [staged changes](./stagedchange.md) that have been prepared for merging into the active database. A branch can be merged by executing its `commit()` method. Deleting a branch will delete all its related changes.
## Fields
### Name
The branch's name.
### User
The user to which the branch belongs (optional).

View File

@ -12,10 +12,6 @@ See the [configuration rendering documentation](../../features/configuration-ren
A unique human-friendly name. A unique human-friendly name.
### Weight
A numeric value which influences the order in which context data is merged. Contexts with a lower weight are merged before those with a higher weight.
### Data File ### Data File
Template code may optionally be sourced from a remote [data file](../core/datafile.md), which is synchronized from a remote data source. When designating a data file, there is no need to specify template code: It will be populated automatically from the data file. Template code may optionally be sourced from a remote [data file](../core/datafile.md), which is synchronized from a remote data source. When designating a data file, there is no need to specify template code: It will be populated automatically from the data file.
@ -27,3 +23,27 @@ Jinja2 template code, if being defined locally rather than replicated from a dat
### Environment Parameters ### Environment Parameters
A dictionary of any additional parameters to pass when instantiating the [Jinja2 environment](https://jinja.palletsprojects.com/en/3.1.x/api/#jinja2.Environment). Jinja2 supports various optional parameters which can be used to modify its default behavior. A dictionary of any additional parameters to pass when instantiating the [Jinja2 environment](https://jinja.palletsprojects.com/en/3.1.x/api/#jinja2.Environment). Jinja2 supports various optional parameters which can be used to modify its default behavior.
### MIME Type
!!! info "This field was introduced in NetBox v4.3."
The MIME type to indicate in the response when rendering the configuration template (optional). Defaults to `text/plain`.
### File Name
!!! info "This field was introduced in NetBox v4.3."
The file name to give to the rendered export file (optional).
### File Extension
!!! info "This field was introduced in NetBox v4.3."
The file extension to append to the file name in the response (optional).
### As Attachment
!!! info "This field was introduced in NetBox v4.3."
If selected, the rendered content will be returned as a file attachment, rather than displayed directly in-browser (where supported).

View File

@ -20,10 +20,20 @@ Template code may optionally be sourced from a remote [data file](../core/datafi
Jinja2 template code for rendering the exported data. Jinja2 template code for rendering the exported data.
### Environment Parameters
!!! info "This field was introduced in NetBox v4.3."
A dictionary of any additional parameters to pass when instantiating the [Jinja2 environment](https://jinja.palletsprojects.com/en/3.1.x/api/#jinja2.Environment). Jinja2 supports various optional parameters which can be used to modify its default behavior.
### MIME Type ### MIME Type
The MIME type to indicate in the response when rendering the export template (optional). Defaults to `text/plain`. The MIME type to indicate in the response when rendering the export template (optional). Defaults to `text/plain`.
### File Name
The file name to give to the rendered export file (optional).
### File Extension ### File Extension
The file extension to append to the file name in the response (optional). The file extension to append to the file name in the response (optional).

View File

@ -1,29 +0,0 @@
# Staged Changes
!!! danger "Deprecated Feature"
This feature has been deprecated in NetBox v4.2 and will be removed in a future release. Please consider using the [netbox-branching plugin](https://github.com/netboxlabs/netbox-branching), which provides much more robust functionality.
A staged change represents the creation of a new object or the modification or deletion of an existing object to be performed at some future point. Each change must be assigned to a [branch](./branch.md).
Changes can be applied individually via the `apply()` method, however it is recommended to apply changes in bulk using the parent branch's `commit()` method.
## Fields
!!! warning
Staged changes are not typically created or manipulated directly, but rather effected through the use of the [`checkout()`](../../plugins/development/staged-changes.md) context manager.
### Branch
The [branch](./branch.md) to which this change belongs.
### Action
The type of action this change represents: `create`, `update`, or `delete`.
### Object
A generic foreign key referencing the existing object to which this change applies.
### Data
JSON representation of the changes being made to the object (not applicable for deletions).

View File

@ -16,6 +16,12 @@ A unique URL-friendly identifier. (This value will be used for filtering.) This
The color to use when displaying the tag in the NetBox UI. The color to use when displaying the tag in the NetBox UI.
### Weight
A numeric weight employed to influence the ordering of tags. Tags with a lower weight will be listed before those with higher weights. Values must be within the range **0** to **32767**.
!!! info "This field was introduced in NetBox v4.3."
### Object Types ### Object Types
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. 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.

View File

@ -2,6 +2,12 @@
This model represents an arbitrary range of individual IPv4 or IPv6 addresses, inclusive of its starting and ending addresses. For instance, the range 192.0.2.10 to 192.0.2.20 has eleven members. (The total member count is available as the `size` property on an IPRange instance.) Like [prefixes](./prefix.md) and [IP addresses](./ipaddress.md), each IP range may optionally be assigned to a [VRF](./vrf.md). This model represents an arbitrary range of individual IPv4 or IPv6 addresses, inclusive of its starting and ending addresses. For instance, the range 192.0.2.10 to 192.0.2.20 has eleven members. (The total member count is available as the `size` property on an IPRange instance.) Like [prefixes](./prefix.md) and [IP addresses](./ipaddress.md), each IP range may optionally be assigned to a [VRF](./vrf.md).
Each IP range can be marked as populated, which instructs NetBox to treat the range as though every IP address within it has been created (even though these individual IP addresses don't actually exist in the database). This can be helpful in scenarios where the management of a subset of IP addresses has been deferred to an external system of record, such as a DHCP server. NetBox will prohibit the creation of individual IP addresses within a range that has been marked as populated.
An IP range can also be marked as utilized. This will cause its utilization to always be reported as 100% when viewing the range or when calculating the utilization of a parent prefix. (If not enabled, a range's utilization is calculated based on the number of IP addresses which have been created within it.)
Typically, IP ranges marked as populated should also be marked as utilized, although there may be scenarios where this is undesirable (e.g. when reclaiming old IP space). An IP range which has been marked as populated but _not_ marked as utilized will always report a utilization of 0%, as it cannot contain child IP addresses.
## Fields ## Fields
### VRF ### VRF
@ -29,6 +35,12 @@ The IP range's operational status. Note that the status of a range does _not_ ha
!!! tip !!! tip
Additional statuses may be defined by setting `IPRange.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter. Additional statuses may be defined by setting `IPRange.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
### Mark Populated
!!! note "This field was added in NetBox v4.3."
If enabled, NetBox will treat this IP range as being fully populated when calculating available IP space. It will also prevent the creation of IP addresses which fall within the declared range (and assigned VRF, if any).
### Mark Utilized ### Mark Utilized
If enabled, the IP range will be considered 100% utilized regardless of how many IP addresses are defined within it. This is useful for documenting DHCP ranges, for example. If enabled, the IP range will be considered 100% utilized regardless of how many IP addresses are defined within it. This is useful for documenting DHCP ranges, for example.

View File

@ -4,9 +4,11 @@ A contact represents an individual or group that has been associated with an obj
## Fields ## Fields
### Group ### Groups
The [contact group](./contactgroup.md) to which this contact is assigned (if any). The [contact groups](./contactgroup.md) to which this contact is assigned (if any).
!!! info "This field was renamed from `group` to `groups` in NetBox v4.3, and now supports the assignment of a contact to more than one group."
### Name ### Name

View File

@ -33,6 +33,19 @@ The technology employed in forming and operating the L2VPN. Choices include:
!!! note !!! note
Designating the type as VPWS, EPL, EP-LAN, EP-TREE will limit the L2VPN instance to two terminations. Designating the type as VPWS, EPL, EP-LAN, EP-TREE will limit the L2VPN instance to two terminations.
### Status
The operational status of the L2VPN. By default, the following statuses are available:
* Active (default)
* Planned
* Faulty
!!! tip "Custom L2VPN statuses"
Additional L2VPN statuses may be defined by setting `L2VPN.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
!!! info "This field was introduced in NetBox v4.3."
### Identifier ### Identifier
An optional numeric identifier. This can be used to track a pseudowire ID, for example. An optional numeric identifier. This can be used to track a pseudowire ID, for example.

View File

@ -1,6 +1,6 @@
# Filters & Filter Sets # Filters & Filter Sets
Filter sets define the mechanisms available for filtering or searching through a set of objects in NetBox. For instance, sites can be filtered by their parent region or group, status, facility ID, and so on. The same filter set is used consistently for a model whether the request is made via the UI, REST API, or GraphQL API. NetBox employs the [django-filters2](https://django-tables2.readthedocs.io/en/latest/) library to define filter sets. Filter sets define the mechanisms available for filtering or searching through a set of objects in NetBox. For instance, sites can be filtered by their parent region or group, status, facility ID, and so on. The same filter set is used consistently for a model whether the request is made via the UI or REST API. (Note that the GraphQL API uses a separate filter class.) NetBox employs the [django-filters2](https://django-tables2.readthedocs.io/en/latest/) library to define filter sets.
## FilterSet Classes ## FilterSet Classes

View File

@ -117,6 +117,8 @@ For more information about database migrations, see the [Django documentation](h
::: netbox.models.features.CloningMixin ::: netbox.models.features.CloningMixin
::: netbox.models.features.ContactsMixin
::: netbox.models.features.CustomLinksMixin ::: netbox.models.features.CustomLinksMixin
::: netbox.models.features.CustomFieldsMixin ::: netbox.models.features.CustomFieldsMixin

View File

@ -1,39 +0,0 @@
# Staged Changes
!!! danger "Deprecated Feature"
This feature has been deprecated in NetBox v4.2 and will be removed in a future release. Please consider using the [netbox-branching plugin](https://github.com/netboxlabs/netbox-branching), which provides much more robust functionality.
NetBox provides a programmatic API to stage the creation, modification, and deletion of objects without actually committing those changes to the active database. This can be useful for performing a "dry run" of bulk operations, or preparing a set of changes for administrative approval, for example.
To begin staging changes, first create a [branch](../../models/extras/branch.md):
```python
from extras.models import Branch
branch1 = Branch.objects.create(name='branch1')
```
Then, activate the branch using the `checkout()` context manager and begin making your changes. This initiates a new database transaction.
```python
from extras.models import Branch
from netbox.staging import checkout
branch1 = Branch.objects.get(name='branch1')
with checkout(branch1):
Site.objects.create(name='New Site', slug='new-site')
# ...
```
Upon exiting the context, the database transaction is automatically rolled back and your changes recorded as [staged changes](../../models/extras/stagedchange.md). Re-entering a branch will trigger a new database transaction and automatically apply any staged changes associated with the branch.
To apply the changes within a branch, call the branch's `commit()` method:
```python
from extras.models import Branch
branch1 = Branch.objects.get(name='branch1')
branch1.commit()
```
Committing a branch is an all-or-none operation: Any exceptions will revert the entire set of changes. After successfully committing a branch, all its associated StagedChange objects are automatically deleted (however the branch itself will remain and can be reused).

View File

@ -198,6 +198,7 @@ Plugins can inject custom content into certain areas of core NetBox views. This
| Method | View | Description | | Method | View | Description |
|---------------------|-------------|-----------------------------------------------------| |---------------------|-------------|-----------------------------------------------------|
| `head()` | All | Custom HTML `<head>` block includes |
| `navbar()` | All | Inject content inside the top navigation bar | | `navbar()` | All | Inject content inside the top navigation bar |
| `list_buttons()` | List view | Add buttons to the top of the page | | `list_buttons()` | List view | Add buttons to the top of the page |
| `buttons()` | Object view | Add buttons to the top of the page | | `buttons()` | Object view | Add buttons to the top of the page |

View File

@ -28,12 +28,7 @@ plugins:
- mkdocstrings: - mkdocstrings:
handlers: handlers:
python: python:
setup_commands: paths: ["netbox"]
- import os
- import django
- os.chdir('netbox/')
- os.environ.setdefault("DJANGO_SETTINGS_MODULE", "netbox.settings")
- django.setup()
options: options:
heading_level: 3 heading_level: 3
members_order: source members_order: source
@ -150,7 +145,6 @@ nav:
- GraphQL API: 'plugins/development/graphql-api.md' - GraphQL API: 'plugins/development/graphql-api.md'
- Background Jobs: 'plugins/development/background-jobs.md' - Background Jobs: 'plugins/development/background-jobs.md'
- Dashboard Widgets: 'plugins/development/dashboard-widgets.md' - Dashboard Widgets: 'plugins/development/dashboard-widgets.md'
- Staged Changes: 'plugins/development/staged-changes.md'
- Exceptions: 'plugins/development/exceptions.md' - Exceptions: 'plugins/development/exceptions.md'
- Migrating to v4.0: 'plugins/development/migration-v4.md' - Migrating to v4.0: 'plugins/development/migration-v4.md'
- Administration: - Administration:
@ -226,7 +220,6 @@ nav:
- VirtualDeviceContext: 'models/dcim/virtualdevicecontext.md' - VirtualDeviceContext: 'models/dcim/virtualdevicecontext.md'
- Extras: - Extras:
- Bookmark: 'models/extras/bookmark.md' - Bookmark: 'models/extras/bookmark.md'
- Branch: 'models/extras/branch.md'
- ConfigContext: 'models/extras/configcontext.md' - ConfigContext: 'models/extras/configcontext.md'
- ConfigTemplate: 'models/extras/configtemplate.md' - ConfigTemplate: 'models/extras/configtemplate.md'
- CustomField: 'models/extras/customfield.md' - CustomField: 'models/extras/customfield.md'
@ -239,7 +232,6 @@ nav:
- Notification: 'models/extras/notification.md' - Notification: 'models/extras/notification.md'
- NotificationGroup: 'models/extras/notificationgroup.md' - NotificationGroup: 'models/extras/notificationgroup.md'
- SavedFilter: 'models/extras/savedfilter.md' - SavedFilter: 'models/extras/savedfilter.md'
- StagedChange: 'models/extras/stagedchange.md'
- Subscription: 'models/extras/subscription.md' - Subscription: 'models/extras/subscription.md'
- Tag: 'models/extras/tag.md' - Tag: 'models/extras/tag.md'
- Webhook: 'models/extras/webhook.md' - Webhook: 'models/extras/webhook.md'

View File

@ -8,7 +8,7 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('users', '0004_netboxgroup_netboxuser'), ('users', '0002_squashed_0004'),
] ]
operations = [ operations = [

View File

@ -89,10 +89,12 @@ class LoginView(View):
if request.user.is_authenticated: if request.user.is_authenticated:
logger = logging.getLogger('netbox.auth.login') logger = logging.getLogger('netbox.auth.login')
return self.redirect_to_next(request, logger) return self.redirect_to_next(request, logger)
login_form_hidden = settings.LOGIN_FORM_HIDDEN
return render(request, self.template_name, { return render(request, self.template_name, {
'form': form, 'form': form,
'auth_backends': self.get_auth_backends(request), 'auth_backends': self.get_auth_backends(request),
'login_form_hidden': login_form_hidden,
}) })
def post(self, request): def post(self, request):

View File

@ -0,0 +1,20 @@
import strawberry
from circuits.choices import *
__all__ = (
'CircuitStatusEnum',
'CircuitCommitRateEnum',
'CircuitTerminationSideEnum',
'CircuitTerminationPortSpeedEnum',
'CircuitPriorityEnum',
'VirtualCircuitTerminationRoleEnum',
)
CircuitCommitRateEnum = strawberry.enum(CircuitCommitRateChoices.as_enum())
CircuitPriorityEnum = strawberry.enum(CircuitPriorityChoices.as_enum())
CircuitStatusEnum = strawberry.enum(CircuitStatusChoices.as_enum())
CircuitTerminationSideEnum = strawberry.enum(CircuitTerminationSideChoices.as_enum())
CircuitTerminationPortSpeedEnum = strawberry.enum(CircuitTerminationPortSpeedChoices.as_enum())
VirtualCircuitTerminationRoleEnum = strawberry.enum(VirtualCircuitTerminationRoleChoices.as_enum())

View File

@ -0,0 +1,19 @@
from dataclasses import dataclass
from typing import Annotated, TYPE_CHECKING
import strawberry
import strawberry_django
from netbox.graphql.filter_mixins import OrganizationalModelFilterMixin
if TYPE_CHECKING:
from netbox.graphql.enums import ColorEnum
__all__ = (
'BaseCircuitTypeFilterMixin',
)
@dataclass
class BaseCircuitTypeFilterMixin(OrganizationalModelFilterMixin):
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()

View File

@ -1,7 +1,30 @@
import strawberry_django from datetime import date
from typing import Annotated, TYPE_CHECKING
from circuits import filtersets, models import strawberry
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin import strawberry_django
from strawberry.scalars import ID
from strawberry_django import FilterLookup, DateFilterLookup
from circuits import models
from core.graphql.filter_mixins import BaseObjectTypeFilterMixin, ChangeLogFilterMixin
from dcim.graphql.filter_mixins import CabledObjectModelFilterMixin
from extras.graphql.filter_mixins import CustomFieldsFilterMixin, TagsFilterMixin
from netbox.graphql.filter_mixins import (
DistanceFilterMixin,
ImageAttachmentFilterMixin,
OrganizationalModelFilterMixin,
PrimaryModelFilterMixin,
)
from tenancy.graphql.filter_mixins import ContactFilterMixin, TenancyFilterMixin
from .filter_mixins import BaseCircuitTypeFilterMixin
if TYPE_CHECKING:
from core.graphql.filters import ContentTypeFilter
from dcim.graphql.filters import InterfaceFilter
from ipam.graphql.filters import ASNFilter
from netbox.graphql.filter_lookups import IntegerLookup
from .enums import *
__all__ = ( __all__ = (
'CircuitFilter', 'CircuitFilter',
@ -19,66 +42,160 @@ __all__ = (
@strawberry_django.filter(models.CircuitTermination, lookups=True) @strawberry_django.filter(models.CircuitTermination, lookups=True)
@autotype_decorator(filtersets.CircuitTerminationFilterSet) class CircuitTerminationFilter(
class CircuitTerminationFilter(BaseFilterMixin): BaseObjectTypeFilterMixin,
pass CustomFieldsFilterMixin,
TagsFilterMixin,
ChangeLogFilterMixin,
CabledObjectModelFilterMixin,
):
circuit: Annotated['CircuitFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field()
)
term_side: Annotated['CircuitTerminationSideEnum', strawberry.lazy('circuits.graphql.enums')] | None = (
strawberry_django.filter_field()
)
termination_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
strawberry_django.filter_field()
)
termination_id: ID | None = strawberry_django.filter_field()
port_speed: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
upstream_speed: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
xconnect_id: FilterLookup[str] | None = strawberry_django.filter_field()
pp_info: FilterLookup[str] | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.Circuit, lookups=True) @strawberry_django.filter(models.Circuit, lookups=True)
@autotype_decorator(filtersets.CircuitFilterSet) class CircuitFilter(
class CircuitFilter(BaseFilterMixin): ContactFilterMixin,
pass ImageAttachmentFilterMixin,
DistanceFilterMixin,
TenancyFilterMixin,
PrimaryModelFilterMixin
):
cid: FilterLookup[str] | None = strawberry_django.filter_field()
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field()
)
provider_id: ID | None = strawberry_django.filter_field()
provider_account: Annotated['ProviderAccountFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field()
)
provider_account_id: ID | None = strawberry_django.filter_field()
type: Annotated['CircuitTypeFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field()
)
type_id: ID | None = strawberry_django.filter_field()
status: Annotated['CircuitStatusEnum', strawberry.lazy('circuits.graphql.enums')] | None = (
strawberry_django.filter_field()
)
install_date: DateFilterLookup[date] | None = strawberry_django.filter_field()
termination_date: DateFilterLookup[date] | None = strawberry_django.filter_field()
commit_rate: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter(models.CircuitType, lookups=True) @strawberry_django.filter(models.CircuitType, lookups=True)
@autotype_decorator(filtersets.CircuitTypeFilterSet) class CircuitTypeFilter(BaseCircuitTypeFilterMixin):
class CircuitTypeFilter(BaseFilterMixin):
pass pass
@strawberry_django.filter(models.CircuitGroup, lookups=True) @strawberry_django.filter(models.CircuitGroup, lookups=True)
@autotype_decorator(filtersets.CircuitGroupFilterSet) class CircuitGroupFilter(TenancyFilterMixin, OrganizationalModelFilterMixin):
class CircuitGroupFilter(BaseFilterMixin):
pass pass
@strawberry_django.filter(models.CircuitGroupAssignment, lookups=True) @strawberry_django.filter(models.CircuitGroupAssignment, lookups=True)
@autotype_decorator(filtersets.CircuitGroupAssignmentFilterSet) class CircuitGroupAssignmentFilter(
class CircuitGroupAssignmentFilter(BaseFilterMixin): BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin
pass ):
member_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
strawberry_django.filter_field()
)
member_id: ID | None = strawberry_django.filter_field()
group: Annotated['CircuitGroupFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field()
)
group_id: ID | None = strawberry_django.filter_field()
priority: Annotated['CircuitPriorityEnum', strawberry.lazy('circuits.graphql.enums')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter(models.Provider, lookups=True) @strawberry_django.filter(models.Provider, lookups=True)
@autotype_decorator(filtersets.ProviderFilterSet) class ProviderFilter(ContactFilterMixin, PrimaryModelFilterMixin):
class ProviderFilter(BaseFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field()
pass slug: FilterLookup[str] | None = strawberry_django.filter_field()
asns: Annotated['ASNFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.ProviderAccount, lookups=True) @strawberry_django.filter(models.ProviderAccount, lookups=True)
@autotype_decorator(filtersets.ProviderAccountFilterSet) class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilterMixin):
class ProviderAccountFilter(BaseFilterMixin): provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
pass strawberry_django.filter_field()
)
provider_id: ID | None = strawberry_django.filter_field()
account: FilterLookup[str] | None = strawberry_django.filter_field()
name: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.ProviderNetwork, lookups=True) @strawberry_django.filter(models.ProviderNetwork, lookups=True)
@autotype_decorator(filtersets.ProviderNetworkFilterSet) class ProviderNetworkFilter(PrimaryModelFilterMixin):
class ProviderNetworkFilter(BaseFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field()
pass provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field()
)
provider_id: ID | None = strawberry_django.filter_field()
service_id: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.VirtualCircuitType, lookups=True) @strawberry_django.filter(models.VirtualCircuitType, lookups=True)
@autotype_decorator(filtersets.VirtualCircuitTypeFilterSet) class VirtualCircuitTypeFilter(BaseCircuitTypeFilterMixin):
class VirtualCircuitTypeFilter(BaseFilterMixin):
pass pass
@strawberry_django.filter(models.VirtualCircuit, lookups=True) @strawberry_django.filter(models.VirtualCircuit, lookups=True)
@autotype_decorator(filtersets.VirtualCircuitFilterSet) class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
class VirtualCircuitFilter(BaseFilterMixin): cid: FilterLookup[str] | None = strawberry_django.filter_field()
pass provider_network: Annotated['ProviderNetworkFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field()
)
provider_network_id: ID | None = strawberry_django.filter_field()
provider_account: Annotated['ProviderAccountFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field()
)
provider_account_id: ID | None = strawberry_django.filter_field()
type: Annotated['VirtualCircuitTypeFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field()
)
type_id: ID | None = strawberry_django.filter_field()
status: Annotated['CircuitStatusEnum', strawberry.lazy('circuits.graphql.enums')] | None = (
strawberry_django.filter_field()
)
group_assignments: Annotated['CircuitGroupAssignmentFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter(models.VirtualCircuitTermination, lookups=True) @strawberry_django.filter(models.VirtualCircuitTermination, lookups=True)
@autotype_decorator(filtersets.VirtualCircuitTerminationFilterSet) class VirtualCircuitTerminationFilter(
class VirtualCircuitTerminationFilter(BaseFilterMixin): BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin
pass ):
virtual_circuit: Annotated['VirtualCircuitFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field()
)
virtual_circuit_id: ID | None = strawberry_django.filter_field()
role: Annotated['VirtualCircuitTerminationRoleEnum', strawberry.lazy('circuits.graphql.enums')] | None = (
strawberry_django.filter_field()
)
interface: Annotated['InterfaceFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
interface_id: ID | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field()

View File

@ -1,4 +1,4 @@
from typing import Annotated, List, Union from typing import Annotated, List, TYPE_CHECKING, Union
import strawberry import strawberry
import strawberry_django import strawberry_django
@ -10,11 +10,15 @@ from netbox.graphql.types import BaseObjectType, NetBoxObjectType, ObjectType, O
from tenancy.graphql.types import TenantType from tenancy.graphql.types import TenantType
from .filters import * from .filters import *
if TYPE_CHECKING:
from dcim.graphql.types import InterfaceType, LocationType, RegionType, SiteGroupType, SiteType
from ipam.graphql.types import ASNType
__all__ = ( __all__ = (
'CircuitTerminationType',
'CircuitType',
'CircuitGroupAssignmentType', 'CircuitGroupAssignmentType',
'CircuitGroupType', 'CircuitGroupType',
'CircuitTerminationType',
'CircuitType',
'CircuitTypeType', 'CircuitTypeType',
'ProviderType', 'ProviderType',
'ProviderAccountType', 'ProviderAccountType',
@ -28,7 +32,8 @@ __all__ = (
@strawberry_django.type( @strawberry_django.type(
models.Provider, models.Provider,
fields='__all__', fields='__all__',
filters=ProviderFilter filters=ProviderFilter,
pagination=True
) )
class ProviderType(NetBoxObjectType, ContactsMixin): class ProviderType(NetBoxObjectType, ContactsMixin):
@ -41,7 +46,8 @@ class ProviderType(NetBoxObjectType, ContactsMixin):
@strawberry_django.type( @strawberry_django.type(
models.ProviderAccount, models.ProviderAccount,
fields='__all__', fields='__all__',
filters=ProviderAccountFilter filters=ProviderAccountFilter,
pagination=True
) )
class ProviderAccountType(NetBoxObjectType): class ProviderAccountType(NetBoxObjectType):
provider: Annotated["ProviderType", strawberry.lazy('circuits.graphql.types')] provider: Annotated["ProviderType", strawberry.lazy('circuits.graphql.types')]
@ -52,7 +58,8 @@ class ProviderAccountType(NetBoxObjectType):
@strawberry_django.type( @strawberry_django.type(
models.ProviderNetwork, models.ProviderNetwork,
fields='__all__', fields='__all__',
filters=ProviderNetworkFilter filters=ProviderNetworkFilter,
pagination=True
) )
class ProviderNetworkType(NetBoxObjectType): class ProviderNetworkType(NetBoxObjectType):
provider: Annotated["ProviderType", strawberry.lazy('circuits.graphql.types')] provider: Annotated["ProviderType", strawberry.lazy('circuits.graphql.types')]
@ -62,8 +69,9 @@ class ProviderNetworkType(NetBoxObjectType):
@strawberry_django.type( @strawberry_django.type(
models.CircuitTermination, models.CircuitTermination,
exclude=('termination_type', 'termination_id', '_location', '_region', '_site', '_site_group', '_provider_network'), exclude=['termination_type', 'termination_id', '_location', '_region', '_site', '_site_group', '_provider_network'],
filters=CircuitTerminationFilter filters=CircuitTerminationFilter,
pagination=True
) )
class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, ObjectType): class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, ObjectType):
circuit: Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')] circuit: Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]
@ -82,7 +90,8 @@ class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, Ob
@strawberry_django.type( @strawberry_django.type(
models.CircuitType, models.CircuitType,
fields='__all__', fields='__all__',
filters=CircuitTypeFilter filters=CircuitTypeFilter,
pagination=True
) )
class CircuitTypeType(OrganizationalObjectType): class CircuitTypeType(OrganizationalObjectType):
color: str color: str
@ -93,7 +102,8 @@ class CircuitTypeType(OrganizationalObjectType):
@strawberry_django.type( @strawberry_django.type(
models.Circuit, models.Circuit,
fields='__all__', fields='__all__',
filters=CircuitFilter filters=CircuitFilter,
pagination=True
) )
class CircuitType(NetBoxObjectType, ContactsMixin): class CircuitType(NetBoxObjectType, ContactsMixin):
provider: ProviderType provider: ProviderType
@ -109,7 +119,8 @@ class CircuitType(NetBoxObjectType, ContactsMixin):
@strawberry_django.type( @strawberry_django.type(
models.CircuitGroup, models.CircuitGroup,
fields='__all__', fields='__all__',
filters=CircuitGroupFilter filters=CircuitGroupFilter,
pagination=True
) )
class CircuitGroupType(OrganizationalObjectType): class CircuitGroupType(OrganizationalObjectType):
tenant: TenantType | None tenant: TenantType | None
@ -117,8 +128,9 @@ class CircuitGroupType(OrganizationalObjectType):
@strawberry_django.type( @strawberry_django.type(
models.CircuitGroupAssignment, models.CircuitGroupAssignment,
exclude=('member_type', 'member_id'), exclude=['member_type', 'member_id'],
filters=CircuitGroupAssignmentFilter filters=CircuitGroupAssignmentFilter,
pagination=True
) )
class CircuitGroupAssignmentType(TagsMixin, BaseObjectType): class CircuitGroupAssignmentType(TagsMixin, BaseObjectType):
group: Annotated["CircuitGroupType", strawberry.lazy('circuits.graphql.types')] group: Annotated["CircuitGroupType", strawberry.lazy('circuits.graphql.types')]
@ -134,7 +146,8 @@ class CircuitGroupAssignmentType(TagsMixin, BaseObjectType):
@strawberry_django.type( @strawberry_django.type(
models.VirtualCircuitType, models.VirtualCircuitType,
fields='__all__', fields='__all__',
filters=VirtualCircuitTypeFilter filters=VirtualCircuitTypeFilter,
pagination=True
) )
class VirtualCircuitTypeType(OrganizationalObjectType): class VirtualCircuitTypeType(OrganizationalObjectType):
color: str color: str
@ -145,7 +158,8 @@ class VirtualCircuitTypeType(OrganizationalObjectType):
@strawberry_django.type( @strawberry_django.type(
models.VirtualCircuitTermination, models.VirtualCircuitTermination,
fields='__all__', fields='__all__',
filters=VirtualCircuitTerminationFilter filters=VirtualCircuitTerminationFilter,
pagination=True
) )
class VirtualCircuitTerminationType(CustomFieldsMixin, TagsMixin, ObjectType): class VirtualCircuitTerminationType(CustomFieldsMixin, TagsMixin, ObjectType):
virtual_circuit: Annotated[ virtual_circuit: Annotated[
@ -161,7 +175,8 @@ class VirtualCircuitTerminationType(CustomFieldsMixin, TagsMixin, ObjectType):
@strawberry_django.type( @strawberry_django.type(
models.VirtualCircuit, models.VirtualCircuit,
fields='__all__', fields='__all__',
filters=VirtualCircuitFilter filters=VirtualCircuitFilter,
pagination=True
) )
class VirtualCircuitType(NetBoxObjectType): class VirtualCircuitType(NetBoxObjectType):
provider_network: ProviderNetworkType = strawberry_django.field(select_related=["provider_network"]) provider_network: ProviderNetworkType = strawberry_django.field(select_related=["provider_network"])

View File

@ -5,11 +5,11 @@ import taggit.managers
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('dcim', '0001_initial'), ('dcim', '0001_squashed'),
('contenttypes', '0002_remove_content_type_name'), ('contenttypes', '0002_remove_content_type_name'),
('circuits', '0001_initial'), ('circuits', '0001_squashed'),
('extras', '0001_initial'), ('extras', '0001_squashed'),
('tenancy', '0001_initial'), ('tenancy', '0001_squashed_0012'),
] ]
replaces = [ replaces = [

View File

@ -15,8 +15,8 @@ class Migration(migrations.Migration):
] ]
dependencies = [ dependencies = [
('circuits', '0037_new_cabling_models'), ('circuits', '0003_squashed_0037'),
('dcim', '0160_populate_cable_ends'), ('dcim', '0160_squashed_0166'),
] ]
operations = [ operations = [

View File

@ -6,7 +6,7 @@ import utilities.fields
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('circuits', '0042_provideraccount'), ('circuits', '0038_squashed_0042'),
] ]
operations = [ operations = [

View File

@ -39,9 +39,6 @@ class Migration(migrations.Migration):
name='termination_type', name='termination_type',
field=models.ForeignKey( field=models.ForeignKey(
blank=True, blank=True,
limit_choices_to=models.Q(
('model__in', ('region', 'sitegroup', 'site', 'location', 'providernetwork'))
),
null=True, null=True,
on_delete=django.db.models.deletion.PROTECT, on_delete=django.db.models.deletion.PROTECT,
related_name='+', related_name='+',

View File

@ -51,7 +51,6 @@ class Migration(migrations.Migration):
name='member_type', name='member_type',
field=models.ForeignKey( field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, on_delete=django.db.models.deletion.PROTECT,
limit_choices_to=models.Q(('app_label', 'circuits'), ('model__in', ['circuit', 'virtualcircuit'])),
related_name='+', related_name='+',
to='contenttypes.contenttype', to='contenttypes.contenttype',
blank=True, blank=True,
@ -68,7 +67,6 @@ class Migration(migrations.Migration):
model_name='circuitgroupassignment', model_name='circuitgroupassignment',
name='member_type', name='member_type',
field=models.ForeignKey( field=models.ForeignKey(
limit_choices_to=models.Q(('app_label', 'circuits'), ('model__in', ['circuit', 'virtualcircuit'])),
on_delete=django.db.models.deletion.PROTECT, on_delete=django.db.models.deletion.PROTECT,
related_name='+', related_name='+',
to='contenttypes.contenttype' to='contenttypes.contenttype'

View File

@ -182,7 +182,6 @@ class CircuitGroupAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin,
""" """
member_type = models.ForeignKey( member_type = models.ForeignKey(
to='contenttypes.ContentType', to='contenttypes.ContentType',
limit_choices_to=CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS,
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='+' related_name='+'
) )
@ -249,7 +248,6 @@ class CircuitTermination(
termination_type = models.ForeignKey( termination_type = models.ForeignKey(
to='contenttypes.ContentType', to='contenttypes.ContentType',
on_delete=models.PROTECT, on_delete=models.PROTECT,
limit_choices_to=Q(model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES),
related_name='+', related_name='+',
blank=True, blank=True,
null=True null=True

View File

@ -5,7 +5,6 @@ from django.utils.translation import gettext_lazy as _
from dcim.views import PathTraceView from dcim.views import PathTraceView
from netbox.views import generic from netbox.views import generic
from tenancy.views import ObjectContactsView
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from utilities.query import count_related from utilities.query import count_related
from utilities.views import GetRelatedModelsMixin, register_model_view from utilities.views import GetRelatedModelsMixin, register_model_view
@ -74,11 +73,6 @@ class ProviderBulkDeleteView(generic.BulkDeleteView):
table = tables.ProviderTable table = tables.ProviderTable
@register_model_view(Provider, 'contacts')
class ProviderContactsView(ObjectContactsView):
queryset = Provider.objects.all()
# #
# ProviderAccounts # ProviderAccounts
# #
@ -141,11 +135,6 @@ class ProviderAccountBulkDeleteView(generic.BulkDeleteView):
table = tables.ProviderAccountTable table = tables.ProviderAccountTable
@register_model_view(ProviderAccount, 'contacts')
class ProviderAccountContactsView(ObjectContactsView):
queryset = ProviderAccount.objects.all()
# #
# Provider networks # Provider networks
# #
@ -413,11 +402,6 @@ class CircuitSwapTerminations(generic.ObjectEditView):
}) })
@register_model_view(Circuit, 'contacts')
class CircuitContactsView(ObjectContactsView):
queryset = Circuit.objects.all()
# #
# Circuit terminations # Circuit terminations
# #

View File

@ -26,8 +26,8 @@ class DataSourceSerializer(NetBoxModelSerializer):
model = DataSource model = DataSource
fields = [ fields = [
'id', 'url', 'display_url', 'display', 'name', 'type', 'source_url', 'enabled', 'status', 'description', 'id', 'url', 'display_url', 'display', 'name', 'type', 'source_url', 'enabled', 'status', 'description',
'parameters', 'ignore_rules', 'comments', 'custom_fields', 'created', 'last_updated', 'last_synced', 'sync_interval', 'parameters', 'ignore_rules', 'comments', 'custom_fields', 'created', 'last_updated',
'file_count', 'last_synced', 'file_count',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'description') brief_fields = ('id', 'url', 'display', 'name', 'description')

View File

@ -19,6 +19,7 @@ class CoreConfig(AppConfig):
def ready(self): def ready(self):
from core.api import schema # noqa: F401 from core.api import schema # noqa: F401
from core.checks import check_duplicate_indexes # noqa: F401
from netbox.models.features import register_models from netbox.models.features import register_models
from . import data_backends, events, search # noqa: F401 from . import data_backends, events, search # noqa: F401
from netbox import context_managers # noqa: F401 from netbox import context_managers # noqa: F401

41
netbox/core/checks.py Normal file
View File

@ -0,0 +1,41 @@
from django.core.checks import Error, register, Tags
from django.db.models import Index, UniqueConstraint
from django.apps import apps
__all__ = (
'check_duplicate_indexes',
)
@register(Tags.models)
def check_duplicate_indexes(app_configs, **kwargs):
"""
Check for an index which is redundant to a declared unique constraint.
"""
errors = []
for model in apps.get_models():
if not (meta := getattr(model, "_meta", None)):
continue
index_fields = {
tuple(index.fields) for index in getattr(meta, 'indexes', [])
if isinstance(index, Index)
}
constraint_fields = {
tuple(constraint.fields) for constraint in getattr(meta, 'constraints', [])
if isinstance(constraint, UniqueConstraint)
}
# Find overlapping definitions
if duplicated := index_fields & constraint_fields:
for fields in duplicated:
errors.append(
Error(
f"Model '{model.__name__}' defines the same field set {fields} in both `Meta.indexes` and "
f"`Meta.constraints`.",
obj=model,
)
)
return errors

View File

@ -7,13 +7,13 @@ from pathlib import Path
from urllib.parse import urlparse from urllib.parse import urlparse
from django import forms from django import forms
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from netbox.data_backends import DataBackend from netbox.data_backends import DataBackend
from netbox.utils import register_data_backend from netbox.utils import register_data_backend
from utilities.constants import HTTP_PROXY_SUPPORTED_SCHEMAS, HTTP_PROXY_SUPPORTED_SOCK_SCHEMAS from utilities.constants import HTTP_PROXY_SUPPORTED_SCHEMAS, HTTP_PROXY_SUPPORTED_SOCK_SCHEMAS
from utilities.proxy import resolve_proxies
from utilities.socks import ProxyPoolManager from utilities.socks import ProxyPoolManager
from .exceptions import SyncError from .exceptions import SyncError
@ -70,18 +70,18 @@ class GitBackend(DataBackend):
# Initialize backend config # Initialize backend config
config = ConfigDict() config = ConfigDict()
self.use_socks = False self.socks_proxy = None
# Apply HTTP proxy (if configured) # Apply HTTP proxy (if configured)
if settings.HTTP_PROXIES: proxies = resolve_proxies(url=self.url, context={'client': self}) or {}
if proxy := settings.HTTP_PROXIES.get(self.url_scheme, None): if proxy := proxies.get(self.url_scheme):
if urlparse(proxy).scheme not in HTTP_PROXY_SUPPORTED_SCHEMAS: if urlparse(proxy).scheme not in HTTP_PROXY_SUPPORTED_SCHEMAS:
raise ImproperlyConfigured(f"Unsupported Git DataSource proxy scheme: {urlparse(proxy).scheme}") raise ImproperlyConfigured(f"Unsupported Git DataSource proxy scheme: {urlparse(proxy).scheme}")
if self.url_scheme in ('http', 'https'): if self.url_scheme in ('http', 'https'):
config.set("http", "proxy", proxy) config.set("http", "proxy", proxy)
if urlparse(proxy).scheme in HTTP_PROXY_SUPPORTED_SOCK_SCHEMAS: if urlparse(proxy).scheme in HTTP_PROXY_SUPPORTED_SOCK_SCHEMAS:
self.use_socks = True self.socks_proxy = proxy
return config return config
@ -98,8 +98,8 @@ class GitBackend(DataBackend):
} }
# check if using socks for proxy - if so need to use custom pool_manager # check if using socks for proxy - if so need to use custom pool_manager
if self.use_socks: if self.socks_proxy:
clone_args['pool_manager'] = ProxyPoolManager(settings.HTTP_PROXIES.get(self.url_scheme)) clone_args['pool_manager'] = ProxyPoolManager(self.socks_proxy)
if self.url_scheme in ('http', 'https'): if self.url_scheme in ('http', 'https'):
if self.params.get('username'): if self.params.get('username'):
@ -147,7 +147,7 @@ class S3Backend(DataBackend):
# Initialize backend config # Initialize backend config
return Boto3Config( return Boto3Config(
proxies=settings.HTTP_PROXIES, proxies=resolve_proxies(url=self.url, context={'client': self}),
) )
@contextmanager @contextmanager

View File

@ -1,2 +1,9 @@
from django.core.exceptions import ImproperlyConfigured
class SyncError(Exception): class SyncError(Exception):
pass pass
class IncompatiblePluginError(ImproperlyConfigured):
pass

View File

@ -29,6 +29,10 @@ class DataSourceFilterSet(NetBoxModelFilterSet):
choices=DataSourceStatusChoices, choices=DataSourceStatusChoices,
null_value=None null_value=None
) )
sync_interval = django_filters.MultipleChoiceFilter(
choices=JobIntervalChoices,
null_value=None
)
class Meta: class Meta:
model = DataSource model = DataSource

View File

@ -1,6 +1,7 @@
from django import forms from django import forms
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from core.choices import JobIntervalChoices
from core.models import * from core.models import *
from netbox.forms import NetBoxModelBulkEditForm from netbox.forms import NetBoxModelBulkEditForm
from netbox.utils import get_data_backend_choices from netbox.utils import get_data_backend_choices
@ -29,6 +30,11 @@ class DataSourceBulkEditForm(NetBoxModelBulkEditForm):
max_length=200, max_length=200,
required=False required=False
) )
sync_interval = forms.ChoiceField(
choices=JobIntervalChoices,
required=False,
label=_('Sync interval')
)
comments = CommentField() comments = CommentField()
parameters = forms.JSONField( parameters = forms.JSONField(
label=_('Parameters'), label=_('Parameters'),
@ -42,8 +48,8 @@ class DataSourceBulkEditForm(NetBoxModelBulkEditForm):
model = DataSource model = DataSource
fieldsets = ( fieldsets = (
FieldSet('type', 'enabled', 'description', 'comments', 'parameters', 'ignore_rules'), FieldSet('type', 'enabled', 'description', 'sync_interval', 'parameters', 'ignore_rules', 'comments'),
) )
nullable_fields = ( nullable_fields = (
'description', 'description', 'parameters', 'comments', 'parameters', 'ignore_rules', 'description', 'description', 'sync_interval', 'parameters', 'parameters', 'ignore_rules' 'comments',
) )

View File

@ -11,5 +11,6 @@ class DataSourceImportForm(NetBoxModelImportForm):
class Meta: class Meta:
model = DataSource model = DataSource
fields = ( fields = (
'name', 'type', 'source_url', 'enabled', 'description', 'comments', 'parameters', 'ignore_rules', 'name', 'type', 'source_url', 'enabled', 'description', 'sync_interval', 'parameters', 'ignore_rules',
'comments',
) )

View File

@ -27,7 +27,7 @@ class DataSourceFilterForm(NetBoxModelFilterSetForm):
model = DataSource model = DataSource
fieldsets = ( fieldsets = (
FieldSet('q', 'filter_id'), FieldSet('q', 'filter_id'),
FieldSet('type', 'status', name=_('Data Source')), FieldSet('type', 'status', 'enabled', 'sync_interval', name=_('Data Source')),
) )
type = forms.MultipleChoiceField( type = forms.MultipleChoiceField(
label=_('Type'), label=_('Type'),
@ -46,6 +46,11 @@ class DataSourceFilterForm(NetBoxModelFilterSetForm):
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
sync_interval = forms.ChoiceField(
label=_('Sync interval'),
choices=JobIntervalChoices,
required=False
)
class DataFileFilterForm(NetBoxModelFilterSetForm): class DataFileFilterForm(NetBoxModelFilterSetForm):

View File

@ -36,7 +36,7 @@ class DataSourceForm(NetBoxModelForm):
class Meta: class Meta:
model = DataSource model = DataSource
fields = [ fields = [
'name', 'type', 'source_url', 'enabled', 'description', 'comments', 'ignore_rules', 'tags', 'name', 'type', 'source_url', 'enabled', 'description', 'sync_interval', 'ignore_rules', 'comments', 'tags',
] ]
widgets = { widgets = {
'ignore_rules': forms.Textarea( 'ignore_rules': forms.Textarea(
@ -51,7 +51,10 @@ class DataSourceForm(NetBoxModelForm):
@property @property
def fieldsets(self): def fieldsets(self):
fieldsets = [ fieldsets = [
FieldSet('name', 'type', 'source_url', 'enabled', 'description', 'tags', 'ignore_rules', name=_('Source')), FieldSet(
'name', 'type', 'source_url', 'description', 'tags', 'ignore_rules', name=_('Source')
),
FieldSet('enabled', 'sync_interval', name=_('Sync')),
] ]
if self.backend_fields: if self.backend_fields:
fieldsets.append( fieldsets.append(

View File

@ -0,0 +1,36 @@
from dataclasses import dataclass
from datetime import datetime
from typing import Annotated, TYPE_CHECKING
import strawberry
import strawberry_django
from strawberry import ID
from strawberry_django import DatetimeFilterLookup
if TYPE_CHECKING:
from .filters import *
__all__ = (
'BaseFilterMixin',
'BaseObjectTypeFilterMixin',
'ChangeLogFilterMixin',
)
# @strawberry.input
class BaseFilterMixin: ...
@dataclass
class BaseObjectTypeFilterMixin(BaseFilterMixin):
id: ID | None = strawberry.UNSET
@dataclass
class ChangeLogFilterMixin(BaseFilterMixin):
id: ID | None = strawberry.UNSET
changelog: Annotated['ObjectChangeFilter', strawberry.lazy('core.graphql.filters')] | None = (
strawberry_django.filter_field()
)
created: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()
last_updated: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()

View File

@ -1,28 +1,89 @@
import strawberry_django from datetime import datetime
from typing import Annotated, TYPE_CHECKING
from core import filtersets, models import strawberry
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin import strawberry_django
from django.contrib.contenttypes.models import ContentType as DjangoContentType
from strawberry.scalars import ID
from strawberry_django import DatetimeFilterLookup, FilterLookup
from core import models
from core.graphql.filter_mixins import BaseFilterMixin
from netbox.graphql.filter_mixins import PrimaryModelFilterMixin
if TYPE_CHECKING:
from netbox.graphql.filter_lookups import IntegerLookup, JSONFilter
from users.graphql.filters import UserFilter
__all__ = ( __all__ = (
'DataFileFilter', 'DataFileFilter',
'DataSourceFilter', 'DataSourceFilter',
'ObjectChangeFilter', 'ObjectChangeFilter',
'ContentTypeFilter',
) )
@strawberry_django.filter(models.DataFile, lookups=True) @strawberry_django.filter(models.DataFile, lookups=True)
@autotype_decorator(filtersets.DataFileFilterSet)
class DataFileFilter(BaseFilterMixin): class DataFileFilter(BaseFilterMixin):
pass id: ID | None = strawberry_django.filter_field()
created: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()
last_updated: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()
source: Annotated['DataSourceFilter', strawberry.lazy('core.graphql.filters')] | None = (
strawberry_django.filter_field()
)
source_id: ID | None = strawberry_django.filter_field()
path: FilterLookup[str] | None = strawberry_django.filter_field()
size: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
hash: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.DataSource, lookups=True) @strawberry_django.filter(models.DataSource, lookups=True)
@autotype_decorator(filtersets.DataSourceFilterSet) class DataSourceFilter(PrimaryModelFilterMixin):
class DataSourceFilter(BaseFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field()
pass type: FilterLookup[str] | None = strawberry_django.filter_field()
source_url: FilterLookup[str] | None = strawberry_django.filter_field()
status: FilterLookup[str] | None = strawberry_django.filter_field()
enabled: FilterLookup[bool] | None = strawberry_django.filter_field()
ignore_rules: FilterLookup[str] | None = strawberry_django.filter_field()
parameters: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
last_synced: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()
datafiles: Annotated['DataFileFilter', strawberry.lazy('core.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter(models.ObjectChange, lookups=True) @strawberry_django.filter(models.ObjectChange, lookups=True)
@autotype_decorator(filtersets.ObjectChangeFilterSet)
class ObjectChangeFilter(BaseFilterMixin): class ObjectChangeFilter(BaseFilterMixin):
pass id: ID | None = strawberry_django.filter_field()
time: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()
user: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field()
user_name: FilterLookup[str] | None = strawberry_django.filter_field()
request_id: FilterLookup[str] | None = strawberry_django.filter_field()
action: FilterLookup[str] | None = strawberry_django.filter_field()
changed_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
strawberry_django.filter_field()
)
changed_object_type_id: ID | None = strawberry_django.filter_field()
changed_object_id: ID | None = strawberry_django.filter_field()
related_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
strawberry_django.filter_field()
)
related_object_id: ID | None = strawberry_django.filter_field()
object_repr: FilterLookup[str] | None = strawberry_django.filter_field()
prechange_data: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
postchange_data: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter(DjangoContentType, lookups=True)
class ContentTypeFilter(BaseFilterMixin):
id: ID | None = strawberry_django.filter_field()
app_label: FilterLookup[str] | None = strawberry_django.filter_field()
model: FilterLookup[str] | None = strawberry_django.filter_field()

View File

@ -1,4 +1,4 @@
from typing import Annotated, List from typing import Annotated, List, TYPE_CHECKING
import strawberry import strawberry
import strawberry_django import strawberry_django
@ -6,6 +6,9 @@ from django.contrib.contenttypes.models import ContentType
from core.models import ObjectChange from core.models import ObjectChange
if TYPE_CHECKING:
from netbox.core.graphql.types import ObjectChangeType
__all__ = ( __all__ = (
'ChangelogMixin', 'ChangelogMixin',
) )

View File

@ -2,12 +2,14 @@ from typing import Annotated, List
import strawberry import strawberry
import strawberry_django import strawberry_django
from django.contrib.contenttypes.models import ContentType as DjangoContentType
from core import models from core import models
from netbox.graphql.types import BaseObjectType, NetBoxObjectType from netbox.graphql.types import BaseObjectType, NetBoxObjectType
from .filters import * from .filters import *
__all__ = ( __all__ = (
'ContentType',
'DataFileType', 'DataFileType',
'DataSourceType', 'DataSourceType',
'ObjectChangeType', 'ObjectChangeType',
@ -17,7 +19,8 @@ __all__ = (
@strawberry_django.type( @strawberry_django.type(
models.DataFile, models.DataFile,
exclude=['data',], exclude=['data',],
filters=DataFileFilter filters=DataFileFilter,
pagination=True
) )
class DataFileType(BaseObjectType): class DataFileType(BaseObjectType):
source: Annotated["DataSourceType", strawberry.lazy('core.graphql.types')] source: Annotated["DataSourceType", strawberry.lazy('core.graphql.types')]
@ -26,7 +29,8 @@ class DataFileType(BaseObjectType):
@strawberry_django.type( @strawberry_django.type(
models.DataSource, models.DataSource,
fields='__all__', fields='__all__',
filters=DataSourceFilter filters=DataSourceFilter,
pagination=True
) )
class DataSourceType(NetBoxObjectType): class DataSourceType(NetBoxObjectType):
@ -36,7 +40,17 @@ class DataSourceType(NetBoxObjectType):
@strawberry_django.type( @strawberry_django.type(
models.ObjectChange, models.ObjectChange,
fields='__all__', fields='__all__',
filters=ObjectChangeFilter filters=ObjectChangeFilter,
pagination=True
) )
class ObjectChangeType(BaseObjectType): class ObjectChangeType(BaseObjectType):
pass pass
@strawberry_django.type(
DjangoContentType,
fields='__all__',
pagination=True
)
class ContentType:
pass

View File

@ -5,6 +5,7 @@ import sys
from django.conf import settings from django.conf import settings
from netbox.jobs import JobRunner, system_job from netbox.jobs import JobRunner, system_job
from netbox.search.backends import search_backend from netbox.search.backends import search_backend
from utilities.proxy import resolve_proxies
from .choices import DataSourceStatusChoices, JobIntervalChoices from .choices import DataSourceStatusChoices, JobIntervalChoices
from .exceptions import SyncError from .exceptions import SyncError
from .models import DataSource from .models import DataSource
@ -71,7 +72,7 @@ class SystemHousekeepingJob(JobRunner):
url=settings.CENSUS_URL, url=settings.CENSUS_URL,
params=census_data, params=census_data,
timeout=3, timeout=3,
proxies=settings.HTTP_PROXIES proxies=resolve_proxies(url=settings.CENSUS_URL)
) )
except requests.exceptions.RequestException: except requests.exceptions.RequestException:
pass pass

View File

@ -5,7 +5,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('core', '0005_job_created_auto_now'), ('core', '0001_squashed_0005'),
] ]
operations = [ operations = [

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.6 on 2025-02-26 19:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0012_job_object_type_optional'),
]
operations = [
migrations.AddField(
model_name='datasource',
name='sync_interval',
field=models.PositiveSmallIntegerField(blank=True, null=True),
),
]

View File

@ -0,0 +1,25 @@
# Generated by Django 5.2b1 on 2025-04-03 18:32
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0013_datasource_sync_interval'),
]
operations = [
migrations.RemoveIndex(
model_name='autosyncrecord',
name='core_autosy_object__c17bac_idx',
),
migrations.RemoveIndex(
model_name='datafile',
name='core_datafile_source_path',
),
migrations.RemoveIndex(
model_name='managedfile',
name='core_managedfile_root_path',
),
]

View File

@ -59,6 +59,12 @@ class DataSource(JobsMixin, PrimaryModel):
verbose_name=_('enabled'), verbose_name=_('enabled'),
default=True default=True
) )
sync_interval = models.PositiveSmallIntegerField(
verbose_name=_('sync interval'),
choices=JobIntervalChoices,
blank=True,
null=True
)
ignore_rules = models.TextField( ignore_rules = models.TextField(
verbose_name=_('ignore rules'), verbose_name=_('ignore rules'),
blank=True, blank=True,
@ -304,9 +310,6 @@ class DataFile(models.Model):
name='%(app_label)s_%(class)s_unique_source_path' name='%(app_label)s_%(class)s_unique_source_path'
), ),
) )
indexes = [
models.Index(fields=('source', 'path'), name='core_datafile_source_path'),
]
verbose_name = _('data file') verbose_name = _('data file')
verbose_name_plural = _('data files') verbose_name_plural = _('data files')
@ -351,17 +354,6 @@ class DataFile(models.Model):
return is_modified return is_modified
def write_to_disk(self, path, overwrite=False):
"""
Write the object's data to disk at the specified path
"""
# Check whether file already exists
if os.path.isfile(path) and not overwrite:
raise FileExistsError()
with open(path, 'wb+') as new_file:
new_file.write(self.data)
class AutoSyncRecord(models.Model): class AutoSyncRecord(models.Model):
""" """
@ -392,8 +384,5 @@ class AutoSyncRecord(models.Model):
name='%(app_label)s_%(class)s_object' name='%(app_label)s_%(class)s_object'
), ),
) )
indexes = (
models.Index(fields=('object_type', 'object_id')),
)
verbose_name = _('auto sync record') verbose_name = _('auto sync record')
verbose_name_plural = _('auto sync records') verbose_name_plural = _('auto sync records')

View File

@ -1,13 +1,16 @@
import logging import logging
import os import os
from functools import cached_property
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.core.files.storage import storages
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from ..choices import ManagedFileRootPathChoices from ..choices import ManagedFileRootPathChoices
from extras.storage import ScriptFileSystemStorage
from netbox.models.features import SyncedDataMixin from netbox.models.features import SyncedDataMixin
from utilities.querysets import RestrictedQuerySet from utilities.querysets import RestrictedQuerySet
@ -55,9 +58,6 @@ class ManagedFile(SyncedDataMixin, models.Model):
name='%(app_label)s_%(class)s_unique_root_path' name='%(app_label)s_%(class)s_unique_root_path'
), ),
) )
indexes = [
models.Index(fields=('file_root', 'file_path'), name='core_managedfile_root_path'),
]
verbose_name = _('managed file') verbose_name = _('managed file')
verbose_name_plural = _('managed files') verbose_name_plural = _('managed files')
@ -76,15 +76,35 @@ class ManagedFile(SyncedDataMixin, models.Model):
return os.path.join(self._resolve_root_path(), self.file_path) return os.path.join(self._resolve_root_path(), self.file_path)
def _resolve_root_path(self): def _resolve_root_path(self):
return { storage = self.storage
'scripts': settings.SCRIPTS_ROOT, if isinstance(storage, ScriptFileSystemStorage):
'reports': settings.REPORTS_ROOT, return {
}[self.file_root] 'scripts': settings.SCRIPTS_ROOT,
'reports': settings.REPORTS_ROOT,
}[self.file_root]
else:
return ""
def sync_data(self): def sync_data(self):
if self.data_file: if self.data_file:
self.file_path = os.path.basename(self.data_path) self.file_path = os.path.basename(self.data_path)
self.data_file.write_to_disk(self.full_path, overwrite=True) self._write_to_disk(self.full_path, overwrite=True)
def _write_to_disk(self, path, overwrite=False):
"""
Write the object's data to disk at the specified path
"""
# Check whether file already exists
storage = self.storage
if storage.exists(path) and not overwrite:
raise FileExistsError()
with storage.open(path, 'wb+') as new_file:
new_file.write(self.data)
@cached_property
def storage(self):
return storages.create_storage(storages.backends["scripts"])
def clean(self): def clean(self):
super().clean() super().clean()
@ -104,8 +124,9 @@ class ManagedFile(SyncedDataMixin, models.Model):
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
# Delete file from disk # Delete file from disk
storage = self.storage
try: try:
os.remove(self.full_path) storage.delete(self.full_path)
except FileNotFoundError: except FileNotFoundError:
pass pass

View File

@ -11,6 +11,7 @@ from django.core.cache import cache
from netbox.plugins import PluginConfig from netbox.plugins import PluginConfig
from netbox.registry import registry from netbox.registry import registry
from utilities.datetime import datetime_from_timestamp from utilities.datetime import datetime_from_timestamp
from utilities.proxy import resolve_proxies
USER_AGENT_STRING = f'NetBox/{settings.RELEASE.version} {settings.RELEASE.edition}' USER_AGENT_STRING = f'NetBox/{settings.RELEASE.version} {settings.RELEASE.edition}'
CACHE_KEY_CATALOG_FEED = 'plugins-catalog-feed' CACHE_KEY_CATALOG_FEED = 'plugins-catalog-feed'
@ -64,9 +65,11 @@ class Plugin:
is_certified: bool = False is_certified: bool = False
release_latest: PluginVersion = field(default_factory=PluginVersion) release_latest: PluginVersion = field(default_factory=PluginVersion)
release_recent_history: list[PluginVersion] = field(default_factory=list) release_recent_history: list[PluginVersion] = field(default_factory=list)
is_local: bool = False # extra field for locally installed plugins is_local: bool = False # Indicates that the plugin is listed in settings.PLUGINS (i.e. installed)
is_installed: bool = False is_loaded: bool = False # Indicates whether the plugin successfully loaded at launch
installed_version: str = '' installed_version: str = ''
netbox_min_version: str = ''
netbox_max_version: str = ''
def get_local_plugins(plugins=None): def get_local_plugins(plugins=None):
@ -77,7 +80,7 @@ def get_local_plugins(plugins=None):
local_plugins = {} local_plugins = {}
# Gather all locally-installed plugins # Gather all locally-installed plugins
for plugin_name in registry['plugins']['installed']: for plugin_name in settings.PLUGINS:
plugin = importlib.import_module(plugin_name) plugin = importlib.import_module(plugin_name)
plugin_config: PluginConfig = plugin.config plugin_config: PluginConfig = plugin.config
installed_version = plugin_config.version installed_version = plugin_config.version
@ -91,19 +94,28 @@ def get_local_plugins(plugins=None):
tag_line=plugin_config.description, tag_line=plugin_config.description,
description_short=plugin_config.description, description_short=plugin_config.description,
is_local=True, is_local=True,
is_installed=True, is_loaded=plugin_name in registry['plugins']['installed'],
installed_version=installed_version, installed_version=installed_version,
netbox_min_version=plugin_config.min_version,
netbox_max_version=plugin_config.max_version,
) )
# Update catalog entries for local plugins, or add them to the list if not listed # Update catalog entries for local plugins, or add them to the list if not listed
for k, v in local_plugins.items(): for k, v in local_plugins.items():
if k in plugins: if k in plugins:
plugins[k].is_local = True plugins[k].is_local = v.is_local
plugins[k].is_installed = True plugins[k].is_loaded = v.is_loaded
plugins[k].installed_version = v.installed_version plugins[k].installed_version = v.installed_version
else: else:
plugins[k] = v plugins[k] = v
# Update plugin table config for hidden and static plugins
hidden = settings.PLUGINS_CATALOG_CONFIG.get('hidden', [])
static = settings.PLUGINS_CATALOG_CONFIG.get('static', [])
for k, v in plugins.items():
v.hidden = k in hidden
v.static = k in static
return plugins return plugins
@ -120,10 +132,11 @@ def get_catalog_plugins():
def get_pages(): def get_pages():
# TODO: pagination is currently broken in API # TODO: pagination is currently broken in API
payload = {'page': '1', 'per_page': '50'} payload = {'page': '1', 'per_page': '50'}
proxies = resolve_proxies(url=settings.PLUGIN_CATALOG_URL)
first_page = session.get( first_page = session.get(
settings.PLUGIN_CATALOG_URL, settings.PLUGIN_CATALOG_URL,
headers={'User-Agent': USER_AGENT_STRING}, headers={'User-Agent': USER_AGENT_STRING},
proxies=settings.HTTP_PROXIES, proxies=proxies,
timeout=3, timeout=3,
params=payload params=payload
).json() ).json()
@ -135,7 +148,7 @@ def get_catalog_plugins():
next_page = session.get( next_page = session.get(
settings.PLUGIN_CATALOG_URL, settings.PLUGIN_CATALOG_URL,
headers={'User-Agent': USER_AGENT_STRING}, headers={'User-Agent': USER_AGENT_STRING},
proxies=settings.HTTP_PROXIES, proxies=proxies,
timeout=3, timeout=3,
params=payload params=payload
).json() ).json()

View File

@ -8,16 +8,15 @@ from django.dispatch import receiver, Signal
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_prometheus.models import model_deletes, model_inserts, model_updates from django_prometheus.models import model_deletes, model_inserts, model_updates
from core.choices import ObjectChangeActionChoices from core.choices import JobStatusChoices, ObjectChangeActionChoices
from core.events import * from core.events import *
from core.models import ObjectChange
from extras.events import enqueue_event from extras.events import enqueue_event
from extras.utils import run_validators from extras.utils import run_validators
from netbox.config import get_config from netbox.config import get_config
from netbox.context import current_request, events_queue from netbox.context import current_request, events_queue
from netbox.models.features import ChangeLoggingMixin from netbox.models.features import ChangeLoggingMixin
from utilities.exceptions import AbortRequest from utilities.exceptions import AbortRequest
from .models import ConfigRevision from .models import ConfigRevision, DataSource, ObjectChange
__all__ = ( __all__ = (
'clear_events', 'clear_events',
@ -182,6 +181,25 @@ def clear_events_queue(sender, **kwargs):
# DataSource handlers # DataSource handlers
# #
@receiver(post_save, sender=DataSource)
def enqueue_sync_job(instance, created, **kwargs):
"""
When a DataSource is saved, check its sync_interval and enqueue a sync job if appropriate.
"""
from .jobs import SyncDataSourceJob
if instance.enabled and instance.sync_interval:
SyncDataSourceJob.enqueue_once(instance, interval=instance.sync_interval)
elif not created:
# Delete any previously scheduled recurring jobs for this DataSource
for job in SyncDataSourceJob.get_jobs(instance).defer('data').filter(
interval__isnull=False,
status=JobStatusChoices.STATUS_SCHEDULED
):
# Call delete() per instance to ensure the associated background task is deleted as well
job.delete()
@receiver(post_sync) @receiver(post_sync)
def auto_sync(instance, **kwargs): def auto_sync(instance, **kwargs):
""" """

View File

@ -25,6 +25,9 @@ class DataSourceTable(NetBoxTable):
enabled = columns.BooleanColumn( enabled = columns.BooleanColumn(
verbose_name=_('Enabled'), verbose_name=_('Enabled'),
) )
sync_interval = columns.ChoiceFieldColumn(
verbose_name=_('Sync interval'),
)
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='core:datasource_list' url_name='core:datasource_list'
) )
@ -35,10 +38,10 @@ class DataSourceTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = DataSource model = DataSource
fields = ( fields = (
'pk', 'id', 'name', 'type', 'status', 'enabled', 'source_url', 'description', 'comments', 'parameters', 'pk', 'id', 'name', 'type', 'status', 'enabled', 'source_url', 'description', 'sync_interval', 'comments',
'created', 'last_updated', 'file_count', 'parameters', 'created', 'last_updated', 'file_count',
) )
default_columns = ('pk', 'name', 'type', 'status', 'enabled', 'description', 'file_count') default_columns = ('pk', 'name', 'type', 'status', 'enabled', 'description', 'sync_interval', 'file_count')
class DataFileTable(NetBoxTable): class DataFileTable(NetBoxTable):

View File

@ -1,7 +1,10 @@
import django_tables2 as tables import django_tables2 as tables
from django.urls import reverse
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from netbox.tables import BaseTable, columns from netbox.tables import BaseTable, columns
from .template_code import PLUGIN_IS_INSTALLED
__all__ = ( __all__ = (
'CatalogPluginTable', 'CatalogPluginTable',
@ -40,20 +43,22 @@ class PluginVersionTable(BaseTable):
class CatalogPluginTable(BaseTable): class CatalogPluginTable(BaseTable):
title_long = tables.Column( title_long = tables.Column(
linkify=('core:plugin', [tables.A('config_name')]), verbose_name=_('Name'),
verbose_name=_('Name')
) )
author = tables.Column( author = tables.Column(
accessor=tables.A('author__name'), accessor=tables.A('author__name'),
verbose_name=_('Author') verbose_name=_('Author')
) )
is_local = columns.BooleanColumn( is_local = columns.BooleanColumn(
false_mark=None,
verbose_name=_('Local') verbose_name=_('Local')
) )
is_installed = columns.BooleanColumn( is_installed = columns.TemplateColumn(
verbose_name=_('Installed') verbose_name=_('Active'),
template_code=PLUGIN_IS_INSTALLED
) )
is_certified = columns.BooleanColumn( is_certified = columns.BooleanColumn(
false_mark=None,
verbose_name=_('Certified') verbose_name=_('Certified')
) )
created_at = columns.DateTimeColumn( created_at = columns.DateTimeColumn(
@ -82,3 +87,9 @@ class CatalogPluginTable(BaseTable):
# List installed plugins first, then certified plugins, then # List installed plugins first, then certified plugins, then
# everything else (with each tranche ordered alphabetically) # everything else (with each tranche ordered alphabetically)
order_by = ('-is_installed', '-is_certified', 'name') order_by = ('-is_installed', '-is_certified', 'name')
def render_title_long(self, value, record):
if record.static:
return value
url = reverse('core:plugin', args=[record.config_name])
return mark_safe(f"<a href='{url}'>{value}</a>")

View File

@ -14,3 +14,15 @@ OBJECTCHANGE_OBJECT = """
OBJECTCHANGE_REQUEST_ID = """ OBJECTCHANGE_REQUEST_ID = """
<a href="{% url 'core:objectchange_list' %}?request_id={{ value }}">{{ value }}</a> <a href="{% url 'core:objectchange_list' %}?request_id={{ value }}">{{ value }}</a>
""" """
PLUGIN_IS_INSTALLED = """
{% if record.is_local %}
{% if record.is_loaded %}
<span class="text-success"><i class="mdi mdi-check-bold"></i></span>
{% else %}
<span class="text-danger"><i class="mdi mdi-alert" data-bs-toggle="tooltip" title="Could not load plugin. Version may be incompatible. Min version: {{ record.netbox_min_version }}, max version: {{ record.netbox_max_version }}"></i></span>
{% endif %}
{% else %}
<span class="text-muted">&mdash;</span>
{% endif %}
"""

View File

@ -27,7 +27,8 @@ class DataSourceTestCase(TestCase, ChangeLoggedFilterSetTests):
source_url='file:///var/tmp/source1/', source_url='file:///var/tmp/source1/',
status=DataSourceStatusChoices.NEW, status=DataSourceStatusChoices.NEW,
enabled=True, enabled=True,
description='foobar1' description='foobar1',
sync_interval=JobIntervalChoices.INTERVAL_HOURLY
), ),
DataSource( DataSource(
name='Data Source 2', name='Data Source 2',
@ -35,14 +36,16 @@ class DataSourceTestCase(TestCase, ChangeLoggedFilterSetTests):
source_url='file:///var/tmp/source2/', source_url='file:///var/tmp/source2/',
status=DataSourceStatusChoices.SYNCING, status=DataSourceStatusChoices.SYNCING,
enabled=True, enabled=True,
description='foobar2' description='foobar2',
sync_interval=JobIntervalChoices.INTERVAL_DAILY
), ),
DataSource( DataSource(
name='Data Source 3', name='Data Source 3',
type='git', type='git',
source_url='https://example.com/git/source3', source_url='https://example.com/git/source3',
status=DataSourceStatusChoices.COMPLETED, status=DataSourceStatusChoices.COMPLETED,
enabled=False enabled=False,
sync_interval=JobIntervalChoices.INTERVAL_WEEKLY
), ),
) )
DataSource.objects.bulk_create(data_sources) DataSource.objects.bulk_create(data_sources)
@ -73,6 +76,10 @@ class DataSourceTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'status': [DataSourceStatusChoices.NEW, DataSourceStatusChoices.SYNCING]} params = {'status': [DataSourceStatusChoices.NEW, DataSourceStatusChoices.SYNCING]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_sync_interval(self):
params = {'sync_interval': [JobIntervalChoices.INTERVAL_HOURLY, JobIntervalChoices.INTERVAL_DAILY]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class DataFileTestCase(TestCase, ChangeLoggedFilterSetTests): class DataFileTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = DataFile.objects.all() queryset = DataFile.objects.all()

View File

@ -613,6 +613,8 @@ class PluginListView(BasePluginView):
if q: if q:
plugins = [obj for obj in plugins if q.casefold() in obj.title_short.casefold()] plugins = [obj for obj in plugins if q.casefold() in obj.title_short.casefold()]
plugins = [plugin for plugin in plugins if not plugin.hidden]
table = CatalogPluginTable(plugins, user=request.user) table = CatalogPluginTable(plugins, user=request.user)
table.configure(request) table.configure(request)

View File

@ -1,3 +1,4 @@
from django.utils.translation import gettext as _
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from drf_spectacular.utils import extend_schema_field from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers from rest_framework import serializers
@ -155,10 +156,10 @@ class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, Conne
class Meta: class Meta:
model = PowerOutlet model = PowerOutlet
fields = [ fields = [
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'power_port', 'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'status', 'color',
'feed_leg', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'power_port', 'feed_leg', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers',
'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'link_peers_type', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable',
'created', 'last_updated', '_occupied', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
] ]
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
@ -232,8 +233,56 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
def validate(self, data): def validate(self, data):
# Validate many-to-many VLAN assignments
if not self.nested: if not self.nested:
# Validate 802.1q mode and vlan(s)
mode = None
tagged_vlans = []
# Gather Information
if self.instance:
mode = data.get('mode') if 'mode' in data.keys() else self.instance.mode
untagged_vlan = data.get('untagged_vlan') if 'untagged_vlan' in data.keys() else \
self.instance.untagged_vlan
qinq_svlan = data.get('qinq_svlan') if 'qinq_svlan' in data.keys() else \
self.instance.qinq_svlan
tagged_vlans = data.get('tagged_vlans') if 'tagged_vlans' in data.keys() else \
self.instance.tagged_vlans.all()
else:
mode = data.get('mode', None)
untagged_vlan = data.get('untagged_vlan') if 'untagged_vlan' in data.keys() else None
qinq_svlan = data.get('qinq_svlan') if 'qinq_svlan' in data.keys() else None
tagged_vlans = data.get('tagged_vlans') if 'tagged_vlans' in data.keys() else None
errors = {}
# Non Q-in-Q mode with service vlan set
if mode != InterfaceModeChoices.MODE_Q_IN_Q and qinq_svlan:
errors.update({
'qinq_svlan': _("Interface mode does not support q-in-q service vlan")
})
# Routed mode
if not mode:
# Untagged vlan
if untagged_vlan:
errors.update({
'untagged_vlan': _("Interface mode does not support untagged vlan")
})
# Tagged vlan
if tagged_vlans:
errors.update({
'tagged_vlans': _("Interface mode does not support tagged vlans")
})
# Non-tagged mode
elif mode in (InterfaceModeChoices.MODE_TAGGED_ALL, InterfaceModeChoices.MODE_ACCESS) and tagged_vlans:
errors.update({
'tagged_vlans': _("Interface mode does not support tagged vlans")
})
if errors:
raise serializers.ValidationError(errors)
# Validate many-to-many VLAN assignments
device = self.instance.device if self.instance else data.get('device') device = self.instance.device if self.instance else data.get('device')
for vlan in data.get('tagged_vlans', []): for vlan in data.get('tagged_vlans', []):
if vlan.site not in [device.site, None]: if vlan.site not in [device.site, None]:

View File

@ -4,8 +4,8 @@ from django.utils.translation import gettext as _
from rest_framework import serializers from rest_framework import serializers
from dcim.choices import * from dcim.choices import *
from dcim.models import DeviceType, ModuleType from dcim.models import DeviceType, ModuleType, ModuleTypeProfile
from netbox.api.fields import ChoiceField, RelatedObjectCountField from netbox.api.fields import AttributesField, ChoiceField, RelatedObjectCountField
from netbox.api.serializers import NetBoxModelSerializer from netbox.api.serializers import NetBoxModelSerializer
from netbox.choices import * from netbox.choices import *
from .manufacturers import ManufacturerSerializer from .manufacturers import ManufacturerSerializer
@ -13,6 +13,7 @@ from .platforms import PlatformSerializer
__all__ = ( __all__ = (
'DeviceTypeSerializer', 'DeviceTypeSerializer',
'ModuleTypeProfileSerializer',
'ModuleTypeSerializer', 'ModuleTypeSerializer',
) )
@ -62,7 +63,23 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'slug', 'description', 'device_count') brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'slug', 'description', 'device_count')
class ModuleTypeProfileSerializer(NetBoxModelSerializer):
class Meta:
model = ModuleTypeProfile
fields = [
'id', 'url', 'display_url', 'display', 'name', 'description', 'schema', 'comments', 'tags', 'custom_fields',
'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
class ModuleTypeSerializer(NetBoxModelSerializer): class ModuleTypeSerializer(NetBoxModelSerializer):
profile = ModuleTypeProfileSerializer(
nested=True,
required=False,
allow_null=True
)
manufacturer = ManufacturerSerializer( manufacturer = ManufacturerSerializer(
nested=True nested=True
) )
@ -78,12 +95,17 @@ class ModuleTypeSerializer(NetBoxModelSerializer):
required=False, required=False,
allow_null=True allow_null=True
) )
attributes = AttributesField(
source='attribute_data',
required=False,
allow_null=True
)
class Meta: class Meta:
model = ModuleType model = ModuleType
fields = [ fields = [
'id', 'url', 'display_url', 'display', 'manufacturer', 'model', 'part_number', 'airflow', 'id', 'url', 'display_url', 'display', 'profile', 'manufacturer', 'model', 'part_number', 'airflow',
'weight', 'weight_unit', 'description', 'comments', 'tags', 'custom_fields', 'weight', 'weight_unit', 'description', 'attributes', 'comments', 'tags', 'custom_fields', 'created',
'created', 'last_updated', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'description') brief_fields = ('id', 'url', 'display', 'profile', 'manufacturer', 'model', 'description')

View File

@ -52,6 +52,13 @@ class NestedLocationSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'rack_count', '_depth'] fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'rack_count', '_depth']
class NestedDeviceRoleSerializer(WritableNestedSerializer):
class Meta:
model = models.DeviceRole
fields = ['id', 'url', 'display_url', 'display', 'name']
class NestedDeviceSerializer(WritableNestedSerializer): class NestedDeviceSerializer(WritableNestedSerializer):
class Meta: class Meta:

View File

@ -70,8 +70,8 @@ class RackTypeSerializer(RackBaseSerializer):
model = RackType model = RackType
fields = [ fields = [
'id', 'url', 'display_url', 'display', 'manufacturer', 'model', 'slug', 'description', 'form_factor', 'id', 'url', 'display_url', 'display', 'manufacturer', 'model', 'slug', 'description', 'form_factor',
'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'weight', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_height', 'outer_depth',
'max_weight', 'weight_unit', 'mounting_depth', 'description', 'comments', 'tags', 'outer_unit', 'weight', 'max_weight', 'weight_unit', 'mounting_depth', 'description', 'comments', 'tags',
'custom_fields', 'created', 'last_updated', 'custom_fields', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'slug', 'description') brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'slug', 'description')
@ -129,9 +129,9 @@ class RackSerializer(RackBaseSerializer):
fields = [ fields = [
'id', 'url', 'display_url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status', 'id', 'url', 'display_url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status',
'role', 'serial', 'asset_tag', 'rack_type', 'form_factor', 'width', 'u_height', 'starting_unit', 'weight', 'role', 'serial', 'asset_tag', 'rack_type', 'form_factor', 'width', 'u_height', 'starting_unit', 'weight',
'max_weight', 'weight_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'max_weight', 'weight_unit', 'desc_units', 'outer_width', 'outer_height', 'outer_depth', 'outer_unit',
'airflow', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'mounting_depth', 'airflow', 'description', 'comments', 'tags', 'custom_fields',
'powerfeed_count', 'created', 'last_updated', 'device_count', 'powerfeed_count',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'description', 'device_count') brief_fields = ('id', 'url', 'display', 'name', 'description', 'device_count')

View File

@ -1,7 +1,8 @@
from dcim.models import DeviceRole, InventoryItemRole from dcim.models import DeviceRole, InventoryItemRole
from extras.api.serializers_.configtemplates import ConfigTemplateSerializer from extras.api.serializers_.configtemplates import ConfigTemplateSerializer
from netbox.api.fields import RelatedObjectCountField from netbox.api.fields import RelatedObjectCountField
from netbox.api.serializers import NetBoxModelSerializer from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer
from .nested import NestedDeviceRoleSerializer
__all__ = ( __all__ = (
'DeviceRoleSerializer', 'DeviceRoleSerializer',
@ -9,7 +10,8 @@ __all__ = (
) )
class DeviceRoleSerializer(NetBoxModelSerializer): class DeviceRoleSerializer(NestedGroupModelSerializer):
parent = NestedDeviceRoleSerializer(required=False, allow_null=True, default=None)
config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None) config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None)
# Related object counts # Related object counts
@ -19,10 +21,13 @@ class DeviceRoleSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = DeviceRole model = DeviceRole
fields = [ fields = [
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'vm_role', 'config_template', 'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'vm_role', 'config_template', 'parent',
'description', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count', 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
'comments', '_depth',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'device_count', 'virtualmachine_count') brief_fields = (
'id', 'url', 'display', 'name', 'slug', 'description', 'device_count', 'virtualmachine_count', '_depth'
)
class InventoryItemRoleSerializer(NetBoxModelSerializer): class InventoryItemRoleSerializer(NetBoxModelSerializer):

View File

@ -27,7 +27,7 @@ class RegionSerializer(NestedGroupModelSerializer):
model = Region model = Region
fields = [ fields = [
'id', 'url', 'display_url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'id', 'url', 'display_url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields',
'created', 'last_updated', 'site_count', 'prefix_count', '_depth', 'created', 'last_updated', 'site_count', 'prefix_count', 'comments', '_depth',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth') brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth')
@ -41,7 +41,7 @@ class SiteGroupSerializer(NestedGroupModelSerializer):
model = SiteGroup model = SiteGroup
fields = [ fields = [
'id', 'url', 'display_url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'id', 'url', 'display_url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields',
'created', 'last_updated', 'site_count', 'prefix_count', '_depth', 'created', 'last_updated', 'site_count', 'prefix_count', 'comments', '_depth',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth') brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth')
@ -93,6 +93,6 @@ class LocationSerializer(NestedGroupModelSerializer):
fields = [ fields = [
'id', 'url', 'display_url', 'display', 'name', 'slug', 'site', 'parent', 'status', 'tenant', 'facility', 'id', 'url', 'display_url', 'display', 'name', 'slug', 'site', 'parent', 'status', 'tenant', 'facility',
'description', 'tags', 'custom_fields', 'created', 'last_updated', 'rack_count', 'device_count', 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'rack_count', 'device_count',
'prefix_count', '_depth', 'prefix_count', 'comments', '_depth',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count', '_depth') brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count', '_depth')

View File

@ -21,6 +21,7 @@ router.register('rack-reservations', views.RackReservationViewSet)
router.register('manufacturers', views.ManufacturerViewSet) router.register('manufacturers', views.ManufacturerViewSet)
router.register('device-types', views.DeviceTypeViewSet) router.register('device-types', views.DeviceTypeViewSet)
router.register('module-types', views.ModuleTypeViewSet) router.register('module-types', views.ModuleTypeViewSet)
router.register('module-type-profiles', views.ModuleTypeProfileViewSet)
# Device type components # Device type components
router.register('console-port-templates', views.ConsolePortTemplateViewSet) router.register('console-port-templates', views.ConsolePortTemplateViewSet)

View File

@ -269,6 +269,12 @@ class DeviceTypeViewSet(NetBoxModelViewSet):
filterset_class = filtersets.DeviceTypeFilterSet filterset_class = filtersets.DeviceTypeFilterSet
class ModuleTypeProfileViewSet(NetBoxModelViewSet):
queryset = ModuleTypeProfile.objects.all()
serializer_class = serializers.ModuleTypeProfileSerializer
filterset_class = filtersets.ModuleTypeProfileFilterSet
class ModuleTypeViewSet(NetBoxModelViewSet): class ModuleTypeViewSet(NetBoxModelViewSet):
queryset = ModuleType.objects.all() queryset = ModuleType.objects.all()
serializer_class = serializers.ModuleTypeSerializer serializer_class = serializers.ModuleTypeSerializer

View File

@ -128,14 +128,15 @@ class RackElevationDetailRenderChoices(ChoiceSet):
class RackAirflowChoices(ChoiceSet): class RackAirflowChoices(ChoiceSet):
key = 'Rack.airflow'
FRONT_TO_REAR = 'front-to-rear' FRONT_TO_REAR = 'front-to-rear'
REAR_TO_FRONT = 'rear-to-front' REAR_TO_FRONT = 'rear-to-front'
CHOICES = ( CHOICES = [
(FRONT_TO_REAR, _('Front to rear')), (FRONT_TO_REAR, _('Front to rear')),
(REAR_TO_FRONT, _('Rear to front')), (REAR_TO_FRONT, _('Rear to front')),
) ]
# #
@ -191,6 +192,7 @@ class DeviceStatusChoices(ChoiceSet):
class DeviceAirflowChoices(ChoiceSet): class DeviceAirflowChoices(ChoiceSet):
key = 'Device.airflow'
AIRFLOW_FRONT_TO_REAR = 'front-to-rear' AIRFLOW_FRONT_TO_REAR = 'front-to-rear'
AIRFLOW_REAR_TO_FRONT = 'rear-to-front' AIRFLOW_REAR_TO_FRONT = 'rear-to-front'
@ -203,7 +205,7 @@ class DeviceAirflowChoices(ChoiceSet):
AIRFLOW_PASSIVE = 'passive' AIRFLOW_PASSIVE = 'passive'
AIRFLOW_MIXED = 'mixed' AIRFLOW_MIXED = 'mixed'
CHOICES = ( CHOICES = [
(AIRFLOW_FRONT_TO_REAR, _('Front to rear')), (AIRFLOW_FRONT_TO_REAR, _('Front to rear')),
(AIRFLOW_REAR_TO_FRONT, _('Rear to front')), (AIRFLOW_REAR_TO_FRONT, _('Rear to front')),
(AIRFLOW_LEFT_TO_RIGHT, _('Left to right')), (AIRFLOW_LEFT_TO_RIGHT, _('Left to right')),
@ -214,7 +216,7 @@ class DeviceAirflowChoices(ChoiceSet):
(AIRFLOW_TOP_TO_BOTTOM, _('Top to bottom')), (AIRFLOW_TOP_TO_BOTTOM, _('Top to bottom')),
(AIRFLOW_PASSIVE, _('Passive')), (AIRFLOW_PASSIVE, _('Passive')),
(AIRFLOW_MIXED, _('Mixed')), (AIRFLOW_MIXED, _('Mixed')),
) ]
# #
@ -242,6 +244,7 @@ class ModuleStatusChoices(ChoiceSet):
class ModuleAirflowChoices(ChoiceSet): class ModuleAirflowChoices(ChoiceSet):
key = 'Module.airflow'
FRONT_TO_REAR = 'front-to-rear' FRONT_TO_REAR = 'front-to-rear'
REAR_TO_FRONT = 'rear-to-front' REAR_TO_FRONT = 'rear-to-front'
@ -250,14 +253,14 @@ class ModuleAirflowChoices(ChoiceSet):
SIDE_TO_REAR = 'side-to-rear' SIDE_TO_REAR = 'side-to-rear'
PASSIVE = 'passive' PASSIVE = 'passive'
CHOICES = ( CHOICES = [
(FRONT_TO_REAR, _('Front to rear')), (FRONT_TO_REAR, _('Front to rear')),
(REAR_TO_FRONT, _('Rear to front')), (REAR_TO_FRONT, _('Rear to front')),
(LEFT_TO_RIGHT, _('Left to right')), (LEFT_TO_RIGHT, _('Left to right')),
(RIGHT_TO_LEFT, _('Right to left')), (RIGHT_TO_LEFT, _('Right to left')),
(SIDE_TO_REAR, _('Side to rear')), (SIDE_TO_REAR, _('Side to rear')),
(PASSIVE, _('Passive')), (PASSIVE, _('Passive')),
) ]
# #
@ -1627,6 +1630,23 @@ class PowerFeedPhaseChoices(ChoiceSet):
) )
#
# PowerOutlets
#
class PowerOutletStatusChoices(ChoiceSet):
key = 'PowerOutlet.status'
STATUS_ENABLED = 'enabled'
STATUS_DISABLED = 'disabled'
STATUS_FAULTY = 'faulty'
CHOICES = [
(STATUS_ENABLED, _('Enabled'), 'green'),
(STATUS_DISABLED, _('Disabled'), 'red'),
(STATUS_FAULTY, _('Faulty'), 'gray'),
]
# #
# VDC # VDC
# #

View File

@ -11,7 +11,8 @@ from ipam.filtersets import PrimaryIPFilterSet
from ipam.models import ASN, IPAddress, VLANTranslationPolicy, VRF from ipam.models import ASN, IPAddress, VLANTranslationPolicy, VRF
from netbox.choices import ColorChoices from netbox.choices import ColorChoices
from netbox.filtersets import ( from netbox.filtersets import (
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet, AttributeFiltersMixin, BaseFilterSet, ChangeLoggedModelFilterSet, NestedGroupModelFilterSet, NetBoxModelFilterSet,
OrganizationalModelFilterSet,
) )
from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
from tenancy.models import * from tenancy.models import *
@ -58,6 +59,7 @@ __all__ = (
'ModuleBayTemplateFilterSet', 'ModuleBayTemplateFilterSet',
'ModuleFilterSet', 'ModuleFilterSet',
'ModuleTypeFilterSet', 'ModuleTypeFilterSet',
'ModuleTypeProfileFilterSet',
'PathEndpointFilterSet', 'PathEndpointFilterSet',
'PlatformFilterSet', 'PlatformFilterSet',
'PowerConnectionFilterSet', 'PowerConnectionFilterSet',
@ -81,7 +83,7 @@ __all__ = (
) )
class RegionFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet): class RegionFilterSet(NestedGroupModelFilterSet, ContactModelFilterSet):
parent_id = django_filters.ModelMultipleChoiceFilter( parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
label=_('Parent region (ID)'), label=_('Parent region (ID)'),
@ -111,7 +113,7 @@ class RegionFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
fields = ('id', 'name', 'slug', 'description') fields = ('id', 'name', 'slug', 'description')
class SiteGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet): class SiteGroupFilterSet(NestedGroupModelFilterSet, ContactModelFilterSet):
parent_id = django_filters.ModelMultipleChoiceFilter( parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=SiteGroup.objects.all(), queryset=SiteGroup.objects.all(),
label=_('Parent site group (ID)'), label=_('Parent site group (ID)'),
@ -205,7 +207,7 @@ class SiteFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe
return queryset.filter(qs_filter).distinct() return queryset.filter(qs_filter).distinct()
class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalModelFilterSet): class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, NestedGroupModelFilterSet):
region_id = TreeNodeMultipleChoiceFilter( region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
field_name='site__region', field_name='site__region',
@ -275,13 +277,13 @@ class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalM
fields = ('id', 'name', 'slug', 'facility', 'description') fields = ('id', 'name', 'slug', 'facility', 'description')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): # extended in order to include querying on Location.facility
return queryset queryset = super().search(queryset, name, value)
return queryset.filter(
Q(name__icontains=value) | if value.strip():
Q(facility__icontains=value) | queryset = queryset | queryset.model.objects.filter(facility__icontains=value)
Q(description__icontains=value)
) return queryset
class RackRoleFilterSet(OrganizationalModelFilterSet): class RackRoleFilterSet(OrganizationalModelFilterSet):
@ -312,8 +314,8 @@ class RackTypeFilterSet(NetBoxModelFilterSet):
class Meta: class Meta:
model = RackType model = RackType
fields = ( fields = (
'id', 'model', 'slug', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth', 'id', 'model', 'slug', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_height',
'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description',
) )
def search(self, queryset, name, value): def search(self, queryset, name, value):
@ -425,8 +427,8 @@ class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe
model = Rack model = Rack
fields = ( fields = (
'id', 'name', 'facility_id', 'asset_tag', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'id', 'name', 'facility_id', 'asset_tag', 'u_height', 'starting_unit', 'desc_units', 'outer_width',
'outer_depth', 'outer_unit', 'mounting_depth', 'airflow', 'weight', 'max_weight', 'weight_unit', 'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'airflow', 'weight', 'max_weight',
'description', 'weight_unit', 'description',
) )
def search(self, queryset, name, value): def search(self, queryset, name, value):
@ -673,7 +675,33 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
return queryset.exclude(inventoryitemtemplates__isnull=value) return queryset.exclude(inventoryitemtemplates__isnull=value)
class ModuleTypeFilterSet(NetBoxModelFilterSet): class ModuleTypeProfileFilterSet(NetBoxModelFilterSet):
class Meta:
model = ModuleTypeProfile
fields = ('id', 'name', 'description')
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(description__icontains=value) |
Q(comments__icontains=value)
)
class ModuleTypeFilterSet(AttributeFiltersMixin, NetBoxModelFilterSet):
profile_id = django_filters.ModelMultipleChoiceFilter(
queryset=ModuleTypeProfile.objects.all(),
label=_('Profile (ID)'),
)
profile = django_filters.ModelMultipleChoiceFilter(
field_name='profile__name',
queryset=ModuleTypeProfile.objects.all(),
to_field_name='name',
label=_('Profile (name)'),
)
manufacturer_id = django_filters.ModelMultipleChoiceFilter( manufacturer_id = django_filters.ModelMultipleChoiceFilter(
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
label=_('Manufacturer (ID)'), label=_('Manufacturer (ID)'),
@ -921,6 +949,29 @@ class DeviceRoleFilterSet(OrganizationalModelFilterSet):
queryset=ConfigTemplate.objects.all(), queryset=ConfigTemplate.objects.all(),
label=_('Config template (ID)'), label=_('Config template (ID)'),
) )
parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=DeviceRole.objects.all(),
label=_('Parent device role (ID)'),
)
parent = django_filters.ModelMultipleChoiceFilter(
field_name='parent__slug',
queryset=DeviceRole.objects.all(),
to_field_name='slug',
label=_('Parent device role (slug)'),
)
ancestor_id = TreeNodeMultipleChoiceFilter(
queryset=DeviceRole.objects.all(),
field_name='parent',
lookup_expr='in',
label=_('Parent device role (ID)'),
)
ancestor = TreeNodeMultipleChoiceFilter(
queryset=DeviceRole.objects.all(),
field_name='parent',
lookup_expr='in',
to_field_name='slug',
label=_('Parent device role (slug)'),
)
class Meta: class Meta:
model = DeviceRole model = DeviceRole
@ -989,14 +1040,16 @@ class DeviceFilterSet(
queryset=DeviceType.objects.all(), queryset=DeviceType.objects.all(),
label=_('Device type (ID)'), label=_('Device type (ID)'),
) )
role_id = django_filters.ModelMultipleChoiceFilter( role_id = TreeNodeMultipleChoiceFilter(
field_name='role_id', field_name='role',
queryset=DeviceRole.objects.all(), queryset=DeviceRole.objects.all(),
lookup_expr='in',
label=_('Role (ID)'), label=_('Role (ID)'),
) )
role = django_filters.ModelMultipleChoiceFilter( role = TreeNodeMultipleChoiceFilter(
field_name='role__slug',
queryset=DeviceRole.objects.all(), queryset=DeviceRole.objects.all(),
field_name='role',
lookup_expr='in',
to_field_name='slug', to_field_name='slug',
label=_('Role (slug)'), label=_('Role (slug)'),
) )
@ -1591,11 +1644,15 @@ class PowerOutletFilterSet(
queryset=PowerPort.objects.all(), queryset=PowerPort.objects.all(),
label=_('Power port (ID)'), label=_('Power port (ID)'),
) )
status = django_filters.MultipleChoiceFilter(
choices=PowerOutletStatusChoices,
null_value=None
)
class Meta: class Meta:
model = PowerOutlet model = PowerOutlet
fields = ( fields = (
'id', 'name', 'label', 'feed_leg', 'description', 'color', 'mark_connected', 'cable_end', 'id', 'name', 'status', 'label', 'feed_leg', 'description', 'color', 'mark_connected', 'cable_end',
) )

View File

@ -14,7 +14,9 @@ from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant from tenancy.models import Tenant
from users.models import User from users.models import User
from utilities.forms import BulkEditForm, add_blank_choice, form_from_model from utilities.forms import BulkEditForm, add_blank_choice, form_from_model
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField from utilities.forms.fields import (
ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField,
)
from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups
from utilities.forms.widgets import BulkEditNullBooleanSelect, NumberWithOptions from utilities.forms.widgets import BulkEditNullBooleanSelect, NumberWithOptions
from virtualization.models import Cluster from virtualization.models import Cluster
@ -46,6 +48,7 @@ __all__ = (
'ModuleBayBulkEditForm', 'ModuleBayBulkEditForm',
'ModuleBayTemplateBulkEditForm', 'ModuleBayTemplateBulkEditForm',
'ModuleTypeBulkEditForm', 'ModuleTypeBulkEditForm',
'ModuleTypeProfileBulkEditForm',
'PlatformBulkEditForm', 'PlatformBulkEditForm',
'PowerFeedBulkEditForm', 'PowerFeedBulkEditForm',
'PowerOutletBulkEditForm', 'PowerOutletBulkEditForm',
@ -78,12 +81,13 @@ class RegionBulkEditForm(NetBoxModelBulkEditForm):
max_length=200, max_length=200,
required=False required=False
) )
comments = CommentField()
model = Region model = Region
fieldsets = ( fieldsets = (
FieldSet('parent', 'description'), FieldSet('parent', 'description'),
) )
nullable_fields = ('parent', 'description') nullable_fields = ('parent', 'description', 'comments')
class SiteGroupBulkEditForm(NetBoxModelBulkEditForm): class SiteGroupBulkEditForm(NetBoxModelBulkEditForm):
@ -97,12 +101,13 @@ class SiteGroupBulkEditForm(NetBoxModelBulkEditForm):
max_length=200, max_length=200,
required=False required=False
) )
comments = CommentField()
model = SiteGroup model = SiteGroup
fieldsets = ( fieldsets = (
FieldSet('parent', 'description'), FieldSet('parent', 'description'),
) )
nullable_fields = ('parent', 'description') nullable_fields = ('parent', 'description', 'comments')
class SiteBulkEditForm(NetBoxModelBulkEditForm): class SiteBulkEditForm(NetBoxModelBulkEditForm):
@ -197,12 +202,13 @@ class LocationBulkEditForm(NetBoxModelBulkEditForm):
max_length=200, max_length=200,
required=False required=False
) )
comments = CommentField()
model = Location model = Location
fieldsets = ( fieldsets = (
FieldSet('site', 'parent', 'status', 'tenant', 'description'), FieldSet('site', 'parent', 'status', 'tenant', 'description'),
) )
nullable_fields = ('parent', 'tenant', 'description') nullable_fields = ('parent', 'tenant', 'description', 'comments')
class RackRoleBulkEditForm(NetBoxModelBulkEditForm): class RackRoleBulkEditForm(NetBoxModelBulkEditForm):
@ -257,6 +263,11 @@ class RackTypeBulkEditForm(NetBoxModelBulkEditForm):
required=False, required=False,
min_value=1 min_value=1
) )
outer_height = forms.IntegerField(
label=_('Outer height'),
required=False,
min_value=1
)
outer_depth = forms.IntegerField( outer_depth = forms.IntegerField(
label=_('Outer depth'), label=_('Outer depth'),
required=False, required=False,
@ -299,7 +310,7 @@ class RackTypeBulkEditForm(NetBoxModelBulkEditForm):
fieldsets = ( fieldsets = (
FieldSet('manufacturer', 'description', 'form_factor', 'width', 'u_height', name=_('Rack Type')), FieldSet('manufacturer', 'description', 'form_factor', 'width', 'u_height', name=_('Rack Type')),
FieldSet( FieldSet(
InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')), InlineFields('outer_width', 'outer_height', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')),
InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')), InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
'mounting_depth', 'mounting_depth',
name=_('Dimensions') name=_('Dimensions')
@ -307,7 +318,7 @@ class RackTypeBulkEditForm(NetBoxModelBulkEditForm):
FieldSet('starting_unit', 'desc_units', name=_('Numbering')), FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
) )
nullable_fields = ( nullable_fields = (
'outer_width', 'outer_depth', 'outer_unit', 'weight', 'outer_width', 'outer_height', 'outer_depth', 'outer_unit', 'weight',
'max_weight', 'weight_unit', 'description', 'comments', 'max_weight', 'weight_unit', 'description', 'comments',
) )
@ -401,6 +412,11 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
required=False, required=False,
min_value=1 min_value=1
) )
outer_height = forms.IntegerField(
label=_('Outer height'),
required=False,
min_value=1
)
outer_depth = forms.IntegerField( outer_depth = forms.IntegerField(
label=_('Outer depth'), label=_('Outer depth'),
required=False, required=False,
@ -448,15 +464,13 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
fieldsets = ( fieldsets = (
FieldSet('status', 'role', 'tenant', 'serial', 'asset_tag', 'rack_type', 'description', name=_('Rack')), FieldSet('status', 'role', 'tenant', 'serial', 'asset_tag', 'rack_type', 'description', name=_('Rack')),
FieldSet('region', 'site_group', 'site', 'location', name=_('Location')), FieldSet('region', 'site_group', 'site', 'location', name=_('Location')),
FieldSet( FieldSet('outer_width', 'outer_height', 'outer_depth', 'outer_unit', name=_('Outer Dimensions')),
'form_factor', 'width', 'u_height', 'desc_units', 'airflow', 'outer_width', 'outer_depth', 'outer_unit', FieldSet('form_factor', 'width', 'u_height', 'desc_units', 'airflow', 'mounting_depth', name=_('Hardware')),
'mounting_depth', name=_('Hardware')
),
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')), FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
) )
nullable_fields = ( nullable_fields = (
'location', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_depth', 'outer_unit', 'weight', 'location', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_height', 'outer_depth',
'max_weight', 'weight_unit', 'description', 'comments', 'outer_unit', 'weight', 'max_weight', 'weight_unit', 'description', 'comments',
) )
@ -563,7 +577,31 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm):
nullable_fields = ('part_number', 'airflow', 'weight', 'weight_unit', 'description', 'comments') nullable_fields = ('part_number', 'airflow', 'weight', 'weight_unit', 'description', 'comments')
class ModuleTypeProfileBulkEditForm(NetBoxModelBulkEditForm):
schema = JSONField(
label=_('Schema'),
required=False
)
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
comments = CommentField()
model = ModuleTypeProfile
fieldsets = (
FieldSet('name', 'description', 'schema', name=_('Profile')),
)
nullable_fields = ('description', 'comments')
class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm): class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm):
profile = DynamicModelChoiceField(
label=_('Profile'),
queryset=ModuleTypeProfile.objects.all(),
required=False
)
manufacturer = DynamicModelChoiceField( manufacturer = DynamicModelChoiceField(
label=_('Manufacturer'), label=_('Manufacturer'),
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
@ -598,17 +636,22 @@ class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm):
model = ModuleType model = ModuleType
fieldsets = ( fieldsets = (
FieldSet('manufacturer', 'part_number', 'description', name=_('Module Type')), FieldSet('profile', 'manufacturer', 'part_number', 'description', name=_('Module Type')),
FieldSet( FieldSet(
'airflow', 'airflow',
InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')), InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
name=_('Chassis') name=_('Chassis')
), ),
) )
nullable_fields = ('part_number', 'weight', 'weight_unit', 'description', 'comments') nullable_fields = ('part_number', 'weight', 'weight_unit', 'profile', 'description', 'comments')
class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm): class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm):
parent = DynamicModelChoiceField(
label=_('Parent'),
queryset=DeviceRole.objects.all(),
required=False,
)
color = ColorField( color = ColorField(
label=_('Color'), label=_('Color'),
required=False required=False
@ -628,12 +671,13 @@ class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm):
max_length=200, max_length=200,
required=False required=False
) )
comments = CommentField()
model = DeviceRole model = DeviceRole
fieldsets = ( fieldsets = (
FieldSet('color', 'vm_role', 'config_template', 'description'), FieldSet('parent', 'color', 'vm_role', 'config_template', 'description'),
) )
nullable_fields = ('color', 'config_template', 'description') nullable_fields = ('parent', 'color', 'config_template', 'description', 'comments')
class PlatformBulkEditForm(NetBoxModelBulkEditForm): class PlatformBulkEditForm(NetBoxModelBulkEditForm):
@ -1379,7 +1423,10 @@ class PowerPortBulkEditForm(
class PowerOutletBulkEditForm( class PowerOutletBulkEditForm(
ComponentBulkEditForm, ComponentBulkEditForm,
form_from_model(PowerOutlet, ['label', 'type', 'color', 'feed_leg', 'power_port', 'mark_connected', 'description']) form_from_model(
PowerOutlet,
['label', 'type', 'status', 'color', 'feed_leg', 'power_port', 'mark_connected', 'description']
)
): ):
mark_connected = forms.NullBooleanField( mark_connected = forms.NullBooleanField(
label=_('Mark connected'), label=_('Mark connected'),
@ -1389,7 +1436,7 @@ class PowerOutletBulkEditForm(
model = PowerOutlet model = PowerOutlet
fieldsets = ( fieldsets = (
FieldSet('module', 'type', 'label', 'description', 'mark_connected', 'color'), FieldSet('module', 'type', 'label', 'status', 'description', 'mark_connected', 'color'),
FieldSet('feed_leg', 'power_port', name=_('Power')), FieldSet('feed_leg', 'power_port', name=_('Power')),
) )
nullable_fields = ('module', 'label', 'type', 'feed_leg', 'power_port', 'description') nullable_fields = ('module', 'label', 'type', 'feed_leg', 'power_port', 'description')

View File

@ -39,6 +39,7 @@ __all__ = (
'ModuleImportForm', 'ModuleImportForm',
'ModuleBayImportForm', 'ModuleBayImportForm',
'ModuleTypeImportForm', 'ModuleTypeImportForm',
'ModuleTypeProfileImportForm',
'PlatformImportForm', 'PlatformImportForm',
'PowerFeedImportForm', 'PowerFeedImportForm',
'PowerOutletImportForm', 'PowerOutletImportForm',
@ -68,7 +69,7 @@ class RegionImportForm(NetBoxModelImportForm):
class Meta: class Meta:
model = Region model = Region
fields = ('name', 'slug', 'parent', 'description', 'tags') fields = ('name', 'slug', 'parent', 'description', 'tags', 'comments')
class SiteGroupImportForm(NetBoxModelImportForm): class SiteGroupImportForm(NetBoxModelImportForm):
@ -82,7 +83,7 @@ class SiteGroupImportForm(NetBoxModelImportForm):
class Meta: class Meta:
model = SiteGroup model = SiteGroup
fields = ('name', 'slug', 'parent', 'description') fields = ('name', 'slug', 'parent', 'description', 'comments', 'tags')
class SiteImportForm(NetBoxModelImportForm): class SiteImportForm(NetBoxModelImportForm):
@ -160,7 +161,10 @@ class LocationImportForm(NetBoxModelImportForm):
class Meta: class Meta:
model = Location model = Location
fields = ('site', 'parent', 'name', 'slug', 'status', 'tenant', 'facility', 'description', 'tags') fields = (
'site', 'parent', 'name', 'slug', 'status', 'tenant', 'facility', 'description',
'tags', 'comments',
)
def __init__(self, data=None, *args, **kwargs): def __init__(self, data=None, *args, **kwargs):
super().__init__(data, *args, **kwargs) super().__init__(data, *args, **kwargs)
@ -219,7 +223,7 @@ class RackTypeImportForm(NetBoxModelImportForm):
model = RackType model = RackType
fields = ( fields = (
'manufacturer', 'model', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', 'manufacturer', 'model', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units',
'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'outer_width', 'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight',
'weight_unit', 'description', 'comments', 'tags', 'weight_unit', 'description', 'comments', 'tags',
) )
@ -304,7 +308,7 @@ class RackImportForm(NetBoxModelImportForm):
model = Rack model = Rack
fields = ( fields = (
'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'rack_type', 'form_factor', 'serial', 'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'rack_type', 'form_factor', 'serial',
'asset_tag', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'asset_tag', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_height', 'outer_depth', 'outer_unit',
'mounting_depth', 'airflow', 'weight', 'max_weight', 'weight_unit', 'description', 'comments', 'tags', 'mounting_depth', 'airflow', 'weight', 'max_weight', 'weight_unit', 'description', 'comments', 'tags',
) )
@ -424,7 +428,22 @@ class DeviceTypeImportForm(NetBoxModelImportForm):
] ]
class ModuleTypeProfileImportForm(NetBoxModelImportForm):
class Meta:
model = ModuleTypeProfile
fields = [
'name', 'description', 'schema', 'comments', 'tags',
]
class ModuleTypeImportForm(NetBoxModelImportForm): class ModuleTypeImportForm(NetBoxModelImportForm):
profile = forms.ModelChoiceField(
label=_('Profile'),
queryset=ModuleTypeProfile.objects.all(),
to_field_name='name',
required=False
)
manufacturer = forms.ModelChoiceField( manufacturer = forms.ModelChoiceField(
label=_('Manufacturer'), label=_('Manufacturer'),
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
@ -457,6 +476,16 @@ class ModuleTypeImportForm(NetBoxModelImportForm):
class DeviceRoleImportForm(NetBoxModelImportForm): class DeviceRoleImportForm(NetBoxModelImportForm):
parent = CSVModelChoiceField(
label=_('Parent'),
queryset=DeviceRole.objects.all(),
required=False,
to_field_name='name',
help_text=_('Parent Device Role'),
error_messages={
'invalid_choice': _('Device role not found.'),
}
)
config_template = CSVModelChoiceField( config_template = CSVModelChoiceField(
label=_('Config template'), label=_('Config template'),
queryset=ConfigTemplate.objects.all(), queryset=ConfigTemplate.objects.all(),
@ -468,7 +497,9 @@ class DeviceRoleImportForm(NetBoxModelImportForm):
class Meta: class Meta:
model = DeviceRole model = DeviceRole
fields = ('name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags') fields = (
'name', 'slug', 'parent', 'color', 'vm_role', 'config_template', 'description', 'comments', 'tags'
)
class PlatformImportForm(NetBoxModelImportForm): class PlatformImportForm(NetBoxModelImportForm):

View File

@ -43,20 +43,14 @@ class InterfaceCommonForm(forms.Form):
super().clean() super().clean()
parent_field = 'device' if 'device' in self.cleaned_data else 'virtual_machine' parent_field = 'device' if 'device' in self.cleaned_data else 'virtual_machine'
tagged_vlans = self.cleaned_data.get('tagged_vlans') if 'tagged_vlans' in self.fields.keys():
tagged_vlans = self.cleaned_data.get('tagged_vlans') if self.is_bound else \
# Untagged interfaces cannot be assigned tagged VLANs self.get_initial_for_field(self.fields['tagged_vlans'], 'tagged_vlans')
if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and tagged_vlans: else:
raise forms.ValidationError({ tagged_vlans = []
'mode': _("An access interface cannot have tagged VLANs assigned.")
})
# Remove all tagged VLAN assignments from "tagged all" interfaces
elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED_ALL:
self.cleaned_data['tagged_vlans'] = []
# Validate tagged VLANs; must be a global VLAN or in the same site # Validate tagged VLANs; must be a global VLAN or in the same site
elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED and tagged_vlans: if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED and tagged_vlans:
valid_sites = [None, self.cleaned_data[parent_field].site] valid_sites = [None, self.cleaned_data[parent_field].site]
invalid_vlans = [str(v) for v in tagged_vlans if v.site not in valid_sites] invalid_vlans = [str(v) for v in tagged_vlans if v.site not in valid_sites]

View File

@ -39,6 +39,7 @@ __all__ = (
'ModuleFilterForm', 'ModuleFilterForm',
'ModuleBayFilterForm', 'ModuleBayFilterForm',
'ModuleTypeFilterForm', 'ModuleTypeFilterForm',
'ModuleTypeProfileFilterForm',
'PlatformFilterForm', 'PlatformFilterForm',
'PowerConnectionFilterForm', 'PowerConnectionFilterForm',
'PowerFeedFilterForm', 'PowerFeedFilterForm',
@ -602,11 +603,19 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
) )
class ModuleTypeProfileFilterForm(NetBoxModelFilterSetForm):
model = ModuleTypeProfile
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
)
selector_fields = ('filter_id', 'q')
class ModuleTypeFilterForm(NetBoxModelFilterSetForm): class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
model = ModuleType model = ModuleType
fieldsets = ( fieldsets = (
FieldSet('q', 'filter_id', 'tag'), FieldSet('q', 'filter_id', 'tag'),
FieldSet('manufacturer_id', 'part_number', 'airflow', name=_('Hardware')), FieldSet('profile_id', 'manufacturer_id', 'part_number', 'airflow', name=_('Hardware')),
FieldSet( FieldSet(
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
'pass_through_ports', name=_('Components') 'pass_through_ports', name=_('Components')
@ -614,6 +623,11 @@ class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
FieldSet('weight', 'weight_unit', name=_('Weight')), FieldSet('weight', 'weight_unit', name=_('Weight')),
) )
selector_fields = ('filter_id', 'q', 'manufacturer_id') selector_fields = ('filter_id', 'q', 'manufacturer_id')
profile_id = DynamicModelMultipleChoiceField(
queryset=ModuleTypeProfile.objects.all(),
required=False,
label=_('Profile')
)
manufacturer_id = DynamicModelMultipleChoiceField( manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
required=False, required=False,
@ -689,6 +703,11 @@ class DeviceRoleFilterForm(NetBoxModelFilterSetForm):
required=False, required=False,
label=_('Config template') label=_('Config template')
) )
parent_id = DynamicModelMultipleChoiceField(
queryset=DeviceRole.objects.all(),
required=False,
label=_('Parent')
)
tag = TagFilterField(model) tag = TagFilterField(model)
@ -1305,7 +1324,7 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = PowerOutlet model = PowerOutlet
fieldsets = ( fieldsets = (
FieldSet('q', 'filter_id', 'tag'), FieldSet('q', 'filter_id', 'tag'),
FieldSet('name', 'label', 'type', 'color', name=_('Attributes')), FieldSet('name', 'label', 'type', 'color', 'status', name=_('Attributes')),
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
FieldSet( FieldSet(
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
@ -1323,6 +1342,11 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
label=_('Color'), label=_('Color'),
required=False required=False
) )
status = forms.MultipleChoiceField(
label=_('Status'),
choices=PowerOutletStatusChoices,
required=False
)
class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):

View File

@ -1,5 +1,6 @@
from django import forms from django import forms
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.validators import EMPTY_VALUES
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from timezone_field import TimeZoneFormField from timezone_field import TimeZoneFormField
@ -18,6 +19,7 @@ from utilities.forms.fields import (
) )
from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups
from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK
from utilities.jsonschema import JSONSchemaProperty
from virtualization.models import Cluster, VMInterface from virtualization.models import Cluster, VMInterface
from wireless.models import WirelessLAN, WirelessLANGroup from wireless.models import WirelessLAN, WirelessLANGroup
from .common import InterfaceCommonForm, ModuleCommonForm from .common import InterfaceCommonForm, ModuleCommonForm
@ -48,6 +50,7 @@ __all__ = (
'ModuleBayForm', 'ModuleBayForm',
'ModuleBayTemplateForm', 'ModuleBayTemplateForm',
'ModuleTypeForm', 'ModuleTypeForm',
'ModuleTypeProfileForm',
'PlatformForm', 'PlatformForm',
'PopulateDeviceBayForm', 'PopulateDeviceBayForm',
'PowerFeedForm', 'PowerFeedForm',
@ -78,6 +81,7 @@ class RegionForm(NetBoxModelForm):
required=False required=False
) )
slug = SlugField() slug = SlugField()
comments = CommentField()
fieldsets = ( fieldsets = (
FieldSet('parent', 'name', 'slug', 'description', 'tags'), FieldSet('parent', 'name', 'slug', 'description', 'tags'),
@ -86,7 +90,7 @@ class RegionForm(NetBoxModelForm):
class Meta: class Meta:
model = Region model = Region
fields = ( fields = (
'parent', 'name', 'slug', 'description', 'tags', 'parent', 'name', 'slug', 'description', 'tags', 'comments',
) )
@ -97,6 +101,7 @@ class SiteGroupForm(NetBoxModelForm):
required=False required=False
) )
slug = SlugField() slug = SlugField()
comments = CommentField()
fieldsets = ( fieldsets = (
FieldSet('parent', 'name', 'slug', 'description', 'tags'), FieldSet('parent', 'name', 'slug', 'description', 'tags'),
@ -105,7 +110,7 @@ class SiteGroupForm(NetBoxModelForm):
class Meta: class Meta:
model = SiteGroup model = SiteGroup
fields = ( fields = (
'parent', 'name', 'slug', 'description', 'tags', 'parent', 'name', 'slug', 'description', 'comments', 'tags',
) )
@ -179,6 +184,7 @@ class LocationForm(TenancyForm, NetBoxModelForm):
} }
) )
slug = SlugField() slug = SlugField()
comments = CommentField()
fieldsets = ( fieldsets = (
FieldSet('site', 'parent', 'name', 'slug', 'status', 'facility', 'description', 'tags', name=_('Location')), FieldSet('site', 'parent', 'name', 'slug', 'status', 'facility', 'description', 'tags', name=_('Location')),
@ -188,7 +194,8 @@ class LocationForm(TenancyForm, NetBoxModelForm):
class Meta: class Meta:
model = Location model = Location
fields = ( fields = (
'site', 'parent', 'name', 'slug', 'status', 'description', 'tenant_group', 'tenant', 'facility', 'tags', 'site', 'parent', 'name', 'slug', 'status', 'description', 'tenant_group', 'tenant',
'facility', 'tags', 'comments',
) )
@ -222,7 +229,7 @@ class RackTypeForm(NetBoxModelForm):
FieldSet('manufacturer', 'model', 'slug', 'description', 'form_factor', 'tags', name=_('Rack Type')), FieldSet('manufacturer', 'model', 'slug', 'description', 'form_factor', 'tags', name=_('Rack Type')),
FieldSet( FieldSet(
'width', 'u_height', 'width', 'u_height',
InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')), InlineFields('outer_width', 'outer_height', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')),
InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')), InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
'mounting_depth', name=_('Dimensions') 'mounting_depth', name=_('Dimensions')
), ),
@ -233,8 +240,8 @@ class RackTypeForm(NetBoxModelForm):
model = RackType model = RackType
fields = [ fields = [
'manufacturer', 'model', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', 'manufacturer', 'model', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units',
'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'outer_width', 'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight',
'description', 'comments', 'tags', 'weight_unit', 'description', 'comments', 'tags',
] ]
@ -279,8 +286,8 @@ class RackForm(TenancyForm, NetBoxModelForm):
fields = [ fields = [
'site', 'location', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial', 'site', 'location', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial',
'asset_tag', 'rack_type', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'asset_tag', 'rack_type', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width',
'outer_depth', 'outer_unit', 'mounting_depth', 'airflow', 'weight', 'max_weight', 'weight_unit', 'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'airflow', 'weight', 'max_weight',
'description', 'comments', 'tags', 'weight_unit', 'description', 'comments', 'tags',
] ]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -302,7 +309,8 @@ class RackForm(TenancyForm, NetBoxModelForm):
*self.fieldsets, *self.fieldsets,
FieldSet( FieldSet(
'form_factor', 'width', 'starting_unit', 'u_height', 'form_factor', 'width', 'starting_unit', 'u_height',
InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')), InlineFields('outer_width', 'outer_height', 'outer_depth', 'outer_unit',
label=_('Outer Dimensions')),
InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')), InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
'mounting_depth', 'desc_units', name=_('Dimensions') 'mounting_depth', 'desc_units', name=_('Dimensions')
), ),
@ -399,25 +407,104 @@ class DeviceTypeForm(NetBoxModelForm):
} }
class ModuleTypeProfileForm(NetBoxModelForm):
schema = JSONField(
label=_('Schema'),
required=False,
help_text=_("Enter a valid JSON schema to define supported attributes.")
)
comments = CommentField()
fieldsets = (
FieldSet('name', 'description', 'schema', 'tags', name=_('Profile')),
)
class Meta:
model = ModuleTypeProfile
fields = [
'name', 'description', 'schema', 'comments', 'tags',
]
class ModuleTypeForm(NetBoxModelForm): class ModuleTypeForm(NetBoxModelForm):
profile = forms.ModelChoiceField(
queryset=ModuleTypeProfile.objects.all(),
label=_('Profile'),
required=False,
widget=HTMXSelect()
)
manufacturer = DynamicModelChoiceField( manufacturer = DynamicModelChoiceField(
label=_('Manufacturer'), label=_('Manufacturer'),
queryset=Manufacturer.objects.all() queryset=Manufacturer.objects.all()
) )
comments = CommentField() comments = CommentField()
fieldsets = ( @property
FieldSet('manufacturer', 'model', 'part_number', 'description', 'tags', name=_('Module Type')), def fieldsets(self):
FieldSet('airflow', 'weight', 'weight_unit', name=_('Chassis')) return [
) FieldSet('manufacturer', 'model', 'part_number', 'description', 'tags', name=_('Module Type')),
FieldSet('airflow', 'weight', 'weight_unit', name=_('Hardware')),
FieldSet('profile', *self.attr_fields, name=_('Profile & Attributes'))
]
class Meta: class Meta:
model = ModuleType model = ModuleType
fields = [ fields = [
'manufacturer', 'model', 'part_number', 'airflow', 'weight', 'weight_unit', 'description', 'profile', 'manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit',
'comments', 'tags', 'comments', 'tags',
] ]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Track profile-specific attribute fields
self.attr_fields = []
# Retrieve assigned ModuleTypeProfile, if any
if not (profile_id := get_field_value(self, 'profile')):
return
if not (profile := ModuleTypeProfile.objects.filter(pk=profile_id).first()):
return
# Extend form with fields for profile attributes
for attr, form_field in self._get_attr_form_fields(profile).items():
field_name = f'attr_{attr}'
self.attr_fields.append(field_name)
self.fields[field_name] = form_field
if self.instance.attribute_data:
self.fields[field_name].initial = self.instance.attribute_data.get(attr)
@staticmethod
def _get_attr_form_fields(profile):
"""
Return a dictionary mapping of attribute names to form fields, suitable for extending
the form per the selected ModuleTypeProfile.
"""
if not profile.schema:
return {}
properties = profile.schema.get('properties', {})
required_fields = profile.schema.get('required', [])
attr_fields = {}
for name, options in properties.items():
prop = JSONSchemaProperty(**options)
attr_fields[name] = prop.to_form_field(name, required=name in required_fields)
return dict(sorted(attr_fields.items()))
def _post_clean(self):
# Compile attribute data from the individual form fields
if self.cleaned_data.get('profile'):
self.instance.attribute_data = {
name[5:]: self.cleaned_data[name] # Remove the attr_ prefix
for name in self.attr_fields
if self.cleaned_data.get(name) not in EMPTY_VALUES
}
return super()._post_clean()
class DeviceRoleForm(NetBoxModelForm): class DeviceRoleForm(NetBoxModelForm):
config_template = DynamicModelChoiceField( config_template = DynamicModelChoiceField(
@ -426,17 +513,24 @@ class DeviceRoleForm(NetBoxModelForm):
required=False required=False
) )
slug = SlugField() slug = SlugField()
parent = DynamicModelChoiceField(
label=_('Parent'),
queryset=DeviceRole.objects.all(),
required=False,
)
comments = CommentField()
fieldsets = ( fieldsets = (
FieldSet( FieldSet(
'name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags', name=_('Device Role') 'name', 'slug', 'parent', 'color', 'vm_role', 'config_template', 'description',
'tags', name=_('Device Role')
), ),
) )
class Meta: class Meta:
model = DeviceRole model = DeviceRole
fields = [ fields = [
'name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags', 'name', 'slug', 'parent', 'color', 'vm_role', 'config_template', 'description', 'comments', 'tags',
] ]
@ -1308,7 +1402,7 @@ class PowerOutletForm(ModularDeviceComponentForm):
fieldsets = ( fieldsets = (
FieldSet( FieldSet(
'device', 'module', 'name', 'label', 'type', 'color', 'power_port', 'feed_leg', 'mark_connected', 'device', 'module', 'name', 'label', 'type', 'status', 'color', 'power_port', 'feed_leg', 'mark_connected',
'description', 'tags', 'description', 'tags',
), ),
) )
@ -1316,7 +1410,7 @@ class PowerOutletForm(ModularDeviceComponentForm):
class Meta: class Meta:
model = PowerOutlet model = PowerOutlet
fields = [ fields = [
'device', 'module', 'name', 'label', 'type', 'color', 'power_port', 'feed_leg', 'mark_connected', 'device', 'module', 'name', 'label', 'type', 'status', 'color', 'power_port', 'feed_leg', 'mark_connected',
'description', 'tags', 'description', 'tags',
] ]

View File

@ -0,0 +1,77 @@
import strawberry
from dcim.choices import *
__all__ = (
'CableEndEnum',
'CableLengthUnitEnum',
'CableTypeEnum',
'ConsolePortSpeedEnum',
'ConsolePortTypeEnum',
'DeviceAirflowEnum',
'DeviceFaceEnum',
'DeviceStatusEnum',
'InterfaceDuplexEnum',
'InterfaceModeEnum',
'InterfacePoEModeEnum',
'InterfacePoETypeEnum',
'InterfaceSpeedEnum',
'InterfaceTypeEnum',
'InventoryItemStatusEnum',
'LinkStatusEnum',
'LocationStatusEnum',
'ModuleAirflowEnum',
'ModuleStatusEnum',
'PortTypeEnum',
'PowerFeedPhaseEnum',
'PowerFeedStatusEnum',
'PowerFeedSupplyEnum',
'PowerFeedTypeEnum',
'PowerOutletFeedLegEnum',
'PowerOutletTypeEnum',
'PowerPortTypeEnum',
'RackAirflowEnum',
'RackDimensionUnitEnum',
'RackFormFactorEnum',
'RackStatusEnum',
'RackWidthEnum',
'SiteStatusEnum',
'SubdeviceRoleEnum',
'VirtualDeviceContextStatusEnum',
)
CableEndEnum = strawberry.enum(CableEndChoices.as_enum())
CableLengthUnitEnum = strawberry.enum(CableLengthUnitChoices.as_enum())
CableTypeEnum = strawberry.enum(CableTypeChoices.as_enum())
ConsolePortSpeedEnum = strawberry.enum(ConsolePortSpeedChoices.as_enum())
ConsolePortTypeEnum = strawberry.enum(ConsolePortTypeChoices.as_enum())
DeviceAirflowEnum = strawberry.enum(DeviceAirflowChoices.as_enum())
DeviceFaceEnum = strawberry.enum(DeviceFaceChoices.as_enum())
DeviceStatusEnum = strawberry.enum(DeviceStatusChoices.as_enum())
InterfaceDuplexEnum = strawberry.enum(InterfaceDuplexChoices.as_enum())
InterfaceModeEnum = strawberry.enum(InterfaceModeChoices.as_enum())
InterfacePoEModeEnum = strawberry.enum(InterfacePoEModeChoices.as_enum())
InterfacePoETypeEnum = strawberry.enum(InterfacePoETypeChoices.as_enum())
InterfaceSpeedEnum = strawberry.enum(InterfaceSpeedChoices.as_enum())
InterfaceTypeEnum = strawberry.enum(InterfaceTypeChoices.as_enum())
InventoryItemStatusEnum = strawberry.enum(InventoryItemStatusChoices.as_enum())
LinkStatusEnum = strawberry.enum(LinkStatusChoices.as_enum())
LocationStatusEnum = strawberry.enum(LocationStatusChoices.as_enum())
ModuleAirflowEnum = strawberry.enum(ModuleAirflowChoices.as_enum())
ModuleStatusEnum = strawberry.enum(ModuleStatusChoices.as_enum())
PortTypeEnum = strawberry.enum(PortTypeChoices.as_enum())
PowerFeedPhaseEnum = strawberry.enum(PowerFeedPhaseChoices.as_enum())
PowerFeedStatusEnum = strawberry.enum(PowerFeedStatusChoices.as_enum())
PowerFeedSupplyEnum = strawberry.enum(PowerFeedSupplyChoices.as_enum())
PowerFeedTypeEnum = strawberry.enum(PowerFeedTypeChoices.as_enum())
PowerOutletFeedLegEnum = strawberry.enum(PowerOutletFeedLegChoices.as_enum())
PowerOutletTypeEnum = strawberry.enum(PowerOutletTypeChoices.as_enum())
PowerPortTypeEnum = strawberry.enum(PowerPortTypeChoices.as_enum())
RackAirflowEnum = strawberry.enum(RackAirflowChoices.as_enum())
RackDimensionUnitEnum = strawberry.enum(RackDimensionUnitChoices.as_enum())
RackFormFactorEnum = strawberry.enum(RackFormFactorChoices.as_enum())
RackStatusEnum = strawberry.enum(RackStatusChoices.as_enum())
RackWidthEnum = strawberry.enum(RackWidthChoices.as_enum())
SiteStatusEnum = strawberry.enum(SiteStatusChoices.as_enum())
SubdeviceRoleEnum = strawberry.enum(SubdeviceRoleChoices.as_enum())
VirtualDeviceContextStatusEnum = strawberry.enum(VirtualDeviceContextStatusChoices.as_enum())

View File

@ -0,0 +1,149 @@
from dataclasses import dataclass
from typing import Annotated, TYPE_CHECKING
import strawberry
import strawberry_django
from strawberry import ID
from strawberry_django import FilterLookup
from core.graphql.filter_mixins import BaseFilterMixin, ChangeLogFilterMixin
from core.graphql.filters import ContentTypeFilter
from netbox.graphql.filter_mixins import NetBoxModelFilterMixin, PrimaryModelFilterMixin, WeightFilterMixin
from .enums import *
if TYPE_CHECKING:
from netbox.graphql.filter_lookups import IntegerLookup
from extras.graphql.filters import ConfigTemplateFilter
from ipam.graphql.filters import VLANFilter, VLANTranslationPolicyFilter
from .filters import *
__all__ = (
'CabledObjectModelFilterMixin',
'ComponentModelFilterMixin',
'ComponentTemplateFilterMixin',
'InterfaceBaseFilterMixin',
'ModularComponentModelFilterMixin',
'ModularComponentTemplateFilterMixin',
'RackBaseFilterMixin',
'RenderConfigFilterMixin',
'ScopedFilterMixin',
)
@dataclass
class ScopedFilterMixin(BaseFilterMixin):
scope_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
strawberry_django.filter_field()
)
scope_id: ID | None = strawberry_django.filter_field()
@dataclass
class ComponentModelFilterMixin(NetBoxModelFilterMixin):
device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
device_id: ID | None = strawberry_django.filter_field()
name: FilterLookup[str] | None = strawberry_django.filter_field()
label: FilterLookup[str] | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field()
@dataclass
class ModularComponentModelFilterMixin(ComponentModelFilterMixin):
module: Annotated['ModuleFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
module_id: ID | None = strawberry_django.filter_field()
inventory_items: Annotated['InventoryItemFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@dataclass
class CabledObjectModelFilterMixin(BaseFilterMixin):
cable: Annotated['CableFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
cable_id: ID | None = strawberry_django.filter_field()
cable_end: CableEndEnum | None = strawberry_django.filter_field()
mark_connected: FilterLookup[bool] | None = strawberry_django.filter_field()
@dataclass
class ComponentTemplateFilterMixin(ChangeLogFilterMixin):
device_type: Annotated['DeviceTypeFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
device_type_id: ID | None = strawberry_django.filter_field()
name: FilterLookup[str] | None = strawberry_django.filter_field()
label: FilterLookup[str] | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field()
@dataclass
class ModularComponentTemplateFilterMixin(ComponentTemplateFilterMixin):
module_type: Annotated['ModuleTypeFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@dataclass
class RenderConfigFilterMixin(BaseFilterMixin):
config_template: Annotated['ConfigTemplateFilter', strawberry.lazy('extras.graphql.filters')] | None = (
strawberry_django.filter_field()
)
config_template_id: ID | None = strawberry_django.filter_field()
@dataclass
class InterfaceBaseFilterMixin(BaseFilterMixin):
enabled: FilterLookup[bool] | None = strawberry_django.filter_field()
mtu: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
mode: InterfaceModeEnum | None = strawberry_django.filter_field()
parent: Annotated['InterfaceFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
parent_id: ID | None = strawberry_django.filter_field()
bridge: Annotated['InterfaceFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
bridge_id: ID | None = strawberry_django.filter_field()
untagged_vlan: Annotated['VLANFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field()
)
tagged_vlans: Annotated['VLANFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field()
)
qinq_svlan: Annotated['VLANFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field()
)
vlan_translation_policy: Annotated['VLANTranslationPolicyFilter', strawberry.lazy('ipam.graphql.filters')] | None \
= strawberry_django.filter_field()
primary_mac_address: Annotated['MACAddressFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
primary_mac_address_id: ID | None = strawberry_django.filter_field()
@dataclass
class RackBaseFilterMixin(WeightFilterMixin, PrimaryModelFilterMixin):
width: Annotated['RackWidthEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
u_height: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
starting_unit: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
desc_units: FilterLookup[bool] | None = strawberry_django.filter_field()
outer_width: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
outer_depth: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
outer_unit: Annotated['RackDimensionUnitEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
)
mounting_depth: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
max_weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)

View File

@ -1,7 +1,46 @@
import strawberry_django from typing import Annotated, TYPE_CHECKING
from dcim import filtersets, models import strawberry
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin import strawberry_django
from strawberry.scalars import ID
from strawberry_django import FilterLookup
from core.graphql.filter_mixins import ChangeLogFilterMixin
from dcim import models
from extras.graphql.filter_mixins import ConfigContextFilterMixin
from netbox.graphql.filter_mixins import (
PrimaryModelFilterMixin,
OrganizationalModelFilterMixin,
NestedGroupModelFilterMixin,
ImageAttachmentFilterMixin,
WeightFilterMixin,
)
from tenancy.graphql.filter_mixins import TenancyFilterMixin, ContactFilterMixin
from .filter_mixins import (
CabledObjectModelFilterMixin,
ComponentModelFilterMixin,
ComponentTemplateFilterMixin,
InterfaceBaseFilterMixin,
ModularComponentModelFilterMixin,
ModularComponentTemplateFilterMixin,
RackBaseFilterMixin,
RenderConfigFilterMixin,
)
if TYPE_CHECKING:
from core.graphql.filters import ContentTypeFilter
from extras.graphql.filters import ConfigTemplateFilter, ImageAttachmentFilter
from ipam.graphql.filters import (
ASNFilter, FHRPGroupAssignmentFilter, IPAddressFilter, PrefixFilter, VLANGroupFilter, VRFFilter,
)
from netbox.graphql.enums import ColorEnum
from netbox.graphql.filter_lookups import FloatLookup, IntegerArrayLookup, IntegerLookup, TreeNodeFilter
from users.graphql.filters import UserFilter
from virtualization.graphql.filters import ClusterFilter
from vpn.graphql.filters import L2VPNFilter, TunnelTerminationFilter
from wireless.graphql.enums import WirelessChannelEnum, WirelessRoleEnum
from wireless.graphql.filters import WirelessLANFilter, WirelessLinkFilter
from .enums import *
__all__ = ( __all__ = (
'CableFilter', 'CableFilter',
@ -13,7 +52,6 @@ __all__ = (
'DeviceFilter', 'DeviceFilter',
'DeviceBayFilter', 'DeviceBayFilter',
'DeviceBayTemplateFilter', 'DeviceBayTemplateFilter',
'InventoryItemTemplateFilter',
'DeviceRoleFilter', 'DeviceRoleFilter',
'DeviceTypeFilter', 'DeviceTypeFilter',
'FrontPortFilter', 'FrontPortFilter',
@ -22,6 +60,7 @@ __all__ = (
'InterfaceTemplateFilter', 'InterfaceTemplateFilter',
'InventoryItemFilter', 'InventoryItemFilter',
'InventoryItemRoleFilter', 'InventoryItemRoleFilter',
'InventoryItemTemplateFilter',
'LocationFilter', 'LocationFilter',
'MACAddressFilter', 'MACAddressFilter',
'ManufacturerFilter', 'ManufacturerFilter',
@ -29,6 +68,7 @@ __all__ = (
'ModuleBayFilter', 'ModuleBayFilter',
'ModuleBayTemplateFilter', 'ModuleBayTemplateFilter',
'ModuleTypeFilter', 'ModuleTypeFilter',
'ModuleTypeProfileFilter',
'PlatformFilter', 'PlatformFilter',
'PowerFeedFilter', 'PowerFeedFilter',
'PowerOutletFilter', 'PowerOutletFilter',
@ -51,258 +91,768 @@ __all__ = (
@strawberry_django.filter(models.Cable, lookups=True) @strawberry_django.filter(models.Cable, lookups=True)
@autotype_decorator(filtersets.CableFilterSet) class CableFilter(PrimaryModelFilterMixin, TenancyFilterMixin):
class CableFilter(BaseFilterMixin): type: Annotated['CableTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
pass status: Annotated['LinkStatusEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
label: FilterLookup[str] | None = strawberry_django.filter_field()
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
length: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
length_unit: Annotated['CableLengthUnitEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
)
terminations: Annotated['CableTerminationFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter(models.CableTermination, lookups=True) @strawberry_django.filter(models.CableTermination, lookups=True)
@autotype_decorator(filtersets.CableTerminationFilterSet) class CableTerminationFilter(ChangeLogFilterMixin):
class CableTerminationFilter(BaseFilterMixin): cable: Annotated['CableFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
pass cable_id: ID | None = strawberry_django.filter_field()
cable_end: Annotated['CableEndEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
)
termination_type: Annotated['CableTerminationFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
termination_id: ID | None = strawberry_django.filter_field()
@strawberry_django.filter(models.ConsolePort, lookups=True) @strawberry_django.filter(models.ConsolePort, lookups=True)
@autotype_decorator(filtersets.ConsolePortFilterSet) class ConsolePortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
class ConsolePortFilter(BaseFilterMixin): type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
pass strawberry_django.filter_field()
)
speed: Annotated['ConsolePortSpeedEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter(models.ConsolePortTemplate, lookups=True) @strawberry_django.filter(models.ConsolePortTemplate, lookups=True)
@autotype_decorator(filtersets.ConsolePortTemplateFilterSet) class ConsolePortTemplateFilter(ModularComponentTemplateFilterMixin):
class ConsolePortTemplateFilter(BaseFilterMixin): type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
pass strawberry_django.filter_field()
)
@strawberry_django.filter(models.ConsoleServerPort, lookups=True) @strawberry_django.filter(models.ConsoleServerPort, lookups=True)
@autotype_decorator(filtersets.ConsoleServerPortFilterSet) class ConsoleServerPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
class ConsoleServerPortFilter(BaseFilterMixin): type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
pass strawberry_django.filter_field()
)
speed: Annotated['ConsolePortSpeedEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter(models.ConsoleServerPortTemplate, lookups=True) @strawberry_django.filter(models.ConsoleServerPortTemplate, lookups=True)
@autotype_decorator(filtersets.ConsoleServerPortTemplateFilterSet) class ConsoleServerPortTemplateFilter(ModularComponentTemplateFilterMixin):
class ConsoleServerPortTemplateFilter(BaseFilterMixin): type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
pass strawberry_django.filter_field()
)
@strawberry_django.filter(models.Device, lookups=True) @strawberry_django.filter(models.Device, lookups=True)
@autotype_decorator(filtersets.DeviceFilterSet) class DeviceFilter(
class DeviceFilter(BaseFilterMixin): ContactFilterMixin,
pass TenancyFilterMixin,
ImageAttachmentFilterMixin,
RenderConfigFilterMixin,
ConfigContextFilterMixin,
PrimaryModelFilterMixin,
):
device_type: Annotated['DeviceTypeFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
device_type_id: ID | None = strawberry_django.filter_field()
role: Annotated['DeviceRoleFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
role_id: ID | None = strawberry_django.filter_field()
platform: Annotated['PlatformFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
name: FilterLookup[str] | None = strawberry_django.filter_field()
serial: FilterLookup[str] | None = strawberry_django.filter_field()
asset_tag: FilterLookup[str] | None = strawberry_django.filter_field()
site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
site_id: ID | None = strawberry_django.filter_field()
location: Annotated['LocationFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
location_id: Annotated['TreeNodeFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
rack: Annotated['RackFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
rack_id: ID | None = strawberry_django.filter_field()
position: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
face: Annotated['DeviceFaceEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
status: Annotated['DeviceStatusEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
)
airflow: Annotated['DeviceAirflowEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
)
primary_ip4: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field()
)
primary_ip4_id: ID | None = strawberry_django.filter_field()
primary_ip6: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field()
)
primary_ip6_id: ID | None = strawberry_django.filter_field()
oob_ip: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field()
)
oob_ip_id: ID | None = strawberry_django.filter_field()
cluster: Annotated['ClusterFilter', strawberry.lazy('virtualization.graphql.filters')] | None = (
strawberry_django.filter_field()
)
cluster_id: ID | None = strawberry_django.filter_field()
virtual_chassis: Annotated['VirtualChassisFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
virtual_chassis_id: ID | None = strawberry_django.filter_field()
vc_position: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
vc_priority: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
latitude: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
longitude: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
interfaces: Annotated['InterfaceFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
consoleports: Annotated['ConsolePortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
consoleserverports: Annotated['ConsoleServerPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
poweroutlets: Annotated['PowerOutletFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
powerports: Annotated['PowerPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
devicebays: Annotated['DeviceBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
frontports: Annotated['FrontPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
rearports: Annotated['RearPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
modulebays: Annotated['ModuleBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
modules: Annotated['ModuleFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
console_port_count: FilterLookup[int] | None = strawberry_django.filter_field()
console_server_port_count: FilterLookup[int] | None = strawberry_django.filter_field()
power_port_count: FilterLookup[int] | None = strawberry_django.filter_field()
power_outlet_count: FilterLookup[int] | None = strawberry_django.filter_field()
interface_count: FilterLookup[int] | None = strawberry_django.filter_field()
front_port_count: FilterLookup[int] | None = strawberry_django.filter_field()
rear_port_count: FilterLookup[int] | None = strawberry_django.filter_field()
device_bay_count: FilterLookup[int] | None = strawberry_django.filter_field()
module_bay_count: FilterLookup[int] | None = strawberry_django.filter_field()
inventory_item_count: FilterLookup[int] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.DeviceBay, lookups=True) @strawberry_django.filter(models.DeviceBay, lookups=True)
@autotype_decorator(filtersets.DeviceBayFilterSet) class DeviceBayFilter(ComponentModelFilterMixin):
class DeviceBayFilter(BaseFilterMixin): installed_device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
pass strawberry_django.filter_field()
)
installed_device_id: ID | None = strawberry_django.filter_field()
@strawberry_django.filter(models.DeviceBayTemplate, lookups=True) @strawberry_django.filter(models.DeviceBayTemplate, lookups=True)
@autotype_decorator(filtersets.DeviceBayTemplateFilterSet) class DeviceBayTemplateFilter(ComponentTemplateFilterMixin):
class DeviceBayTemplateFilter(BaseFilterMixin):
pass pass
@strawberry_django.filter(models.InventoryItemTemplate, lookups=True) @strawberry_django.filter(models.InventoryItemTemplate, lookups=True)
@autotype_decorator(filtersets.InventoryItemTemplateFilterSet) class InventoryItemTemplateFilter(ComponentTemplateFilterMixin):
class InventoryItemTemplateFilter(BaseFilterMixin): parent: Annotated['InventoryItemTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
pass strawberry_django.filter_field()
)
component_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
strawberry_django.filter_field()
)
component_id: ID | None = strawberry_django.filter_field()
role: Annotated['InventoryItemRoleFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
role_id: ID | None = strawberry_django.filter_field()
manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
manufacturer_id: ID | None = strawberry_django.filter_field()
part_id: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.DeviceRole, lookups=True) @strawberry_django.filter(models.DeviceRole, lookups=True)
@autotype_decorator(filtersets.DeviceRoleFilterSet) class DeviceRoleFilter(OrganizationalModelFilterMixin, RenderConfigFilterMixin):
class DeviceRoleFilter(BaseFilterMixin): color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
pass vm_role: FilterLookup[bool] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.DeviceType, lookups=True) @strawberry_django.filter(models.DeviceType, lookups=True)
@autotype_decorator(filtersets.DeviceTypeFilterSet) class DeviceTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, WeightFilterMixin):
class DeviceTypeFilter(BaseFilterMixin): manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
pass strawberry_django.filter_field()
)
manufacturer_id: ID | None = strawberry_django.filter_field()
model: FilterLookup[str] | None = strawberry_django.filter_field()
slug: FilterLookup[str] | None = strawberry_django.filter_field()
default_platform: Annotated['PlatformFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
default_platform_id: ID | None = strawberry_django.filter_field()
part_number: FilterLookup[str] | None = strawberry_django.filter_field()
u_height: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
exclude_from_utilization: FilterLookup[bool] | None = strawberry_django.filter_field()
is_full_depth: FilterLookup[bool] | None = strawberry_django.filter_field()
subdevice_role: Annotated['SubdeviceRoleEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
)
airflow: Annotated['DeviceAirflowEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
)
front_image: Annotated['ImageAttachmentFilter', strawberry.lazy('extras.graphql.filters')] | None = (
strawberry_django.filter_field()
)
rear_image: Annotated['ImageAttachmentFilter', strawberry.lazy('extras.graphql.filters')] | None = (
strawberry_django.filter_field()
)
console_port_template_count: FilterLookup[int] | None = strawberry_django.filter_field()
console_server_port_template_count: FilterLookup[int] | None = strawberry_django.filter_field()
power_port_template_count: FilterLookup[int] | None = strawberry_django.filter_field()
power_outlet_template_count: FilterLookup[int] | None = strawberry_django.filter_field()
interface_template_count: FilterLookup[int] | None = strawberry_django.filter_field()
front_port_template_count: FilterLookup[int] | None = strawberry_django.filter_field()
rear_port_template_count: FilterLookup[int] | None = strawberry_django.filter_field()
device_bay_template_count: FilterLookup[int] | None = strawberry_django.filter_field()
module_bay_template_count: FilterLookup[int] | None = strawberry_django.filter_field()
inventory_item_template_count: FilterLookup[int] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.FrontPort, lookups=True) @strawberry_django.filter(models.FrontPort, lookups=True)
@autotype_decorator(filtersets.FrontPortFilterSet) class FrontPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
class FrontPortFilter(BaseFilterMixin): type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
pass color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
rear_port: Annotated['RearPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
rear_port_id: ID | None = strawberry_django.filter_field()
rear_port_position: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter(models.FrontPortTemplate, lookups=True) @strawberry_django.filter(models.FrontPortTemplate, lookups=True)
@autotype_decorator(filtersets.FrontPortTemplateFilterSet) class FrontPortTemplateFilter(ModularComponentTemplateFilterMixin):
class FrontPortTemplateFilter(BaseFilterMixin): type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
pass color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
rear_port: Annotated['RearPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
rear_port_id: ID | None = strawberry_django.filter_field()
rear_port_position: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter(models.MACAddress, lookups=True) @strawberry_django.filter(models.MACAddress, lookups=True)
@autotype_decorator(filtersets.MACAddressFilterSet) class MACAddressFilter(PrimaryModelFilterMixin):
class MACAddressFilter(BaseFilterMixin): mac_address: FilterLookup[str] | None = strawberry_django.filter_field()
pass assigned_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
strawberry_django.filter_field()
)
assigned_object_id: ID | None = strawberry_django.filter_field()
@strawberry_django.filter(models.Interface, lookups=True) @strawberry_django.filter(models.Interface, lookups=True)
@autotype_decorator(filtersets.InterfaceFilterSet) class InterfaceFilter(ModularComponentModelFilterMixin, InterfaceBaseFilterMixin, CabledObjectModelFilterMixin):
class InterfaceFilter(BaseFilterMixin): vcdcs: Annotated['VirtualDeviceContextFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
pass strawberry_django.filter_field()
)
lag: Annotated['InterfaceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
lag_id: ID | None = strawberry_django.filter_field()
type: Annotated['InterfaceTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
)
mgmt_only: FilterLookup[bool] | None = strawberry_django.filter_field()
speed: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
duplex: Annotated['InterfaceDuplexEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
)
wwn: FilterLookup[str] | None = strawberry_django.filter_field()
rf_role: Annotated['WirelessRoleEnum', strawberry.lazy('wireless.graphql.enums')] | None = (
strawberry_django.filter_field()
)
rf_channel: Annotated['WirelessChannelEnum', strawberry.lazy('wireless.graphql.enums')] | None = (
strawberry_django.filter_field()
)
rf_channel_frequency: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
rf_channel_width: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
tx_power: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
poe_mode: Annotated['InterfacePoEModeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
)
poe_type: Annotated['InterfacePoETypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
)
wireless_link: Annotated['WirelessLinkFilter', strawberry.lazy('wireless.graphql.filters')] | None = (
strawberry_django.filter_field()
)
wireless_link_id: ID | None = strawberry_django.filter_field()
wireless_lans: Annotated['WirelessLANFilter', strawberry.lazy('wireless.graphql.filters')] | None = (
strawberry_django.filter_field()
)
vrf: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
vrf_id: ID | None = strawberry_django.filter_field()
ip_addresses: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field()
)
mac_addresses: Annotated['MACAddressFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
fhrp_group_assignments: Annotated['FHRPGroupAssignmentFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field()
)
tunnel_terminations: Annotated['TunnelTerminationFilter', strawberry.lazy('vpn.graphql.filters')] | None = (
strawberry_django.filter_field()
)
l2vpn_terminations: Annotated['L2VPNFilter', strawberry.lazy('vpn.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter(models.InterfaceTemplate, lookups=True) @strawberry_django.filter(models.InterfaceTemplate, lookups=True)
@autotype_decorator(filtersets.InterfaceTemplateFilterSet) class InterfaceTemplateFilter(ModularComponentTemplateFilterMixin):
class InterfaceTemplateFilter(BaseFilterMixin): type: Annotated['InterfaceTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
pass strawberry_django.filter_field()
)
enabled: FilterLookup[bool] | None = strawberry_django.filter_field()
mgmt_only: FilterLookup[bool] | None = strawberry_django.filter_field()
bridge: Annotated['InterfaceTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
bridge_id: ID | None = strawberry_django.filter_field()
poe_mode: Annotated['InterfacePoEModeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
)
poe_type: Annotated['InterfacePoETypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
)
rf_role: Annotated['WirelessRoleEnum', strawberry.lazy('wireless.graphql.enums')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter(models.InventoryItem, lookups=True) @strawberry_django.filter(models.InventoryItem, lookups=True)
@autotype_decorator(filtersets.InventoryItemFilterSet) class InventoryItemFilter(ComponentModelFilterMixin):
class InventoryItemFilter(BaseFilterMixin): parent: Annotated['InventoryItemFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
pass strawberry_django.filter_field()
)
parent_id: ID | None = strawberry_django.filter_field()
component_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
strawberry_django.filter_field()
)
component_id: ID | None = strawberry_django.filter_field()
status: Annotated['InventoryItemStatusEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
)
role: Annotated['InventoryItemRoleFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
role_id: ID | None = strawberry_django.filter_field()
manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
manufacturer_id: ID | None = strawberry_django.filter_field()
part_id: FilterLookup[str] | None = strawberry_django.filter_field()
serial: FilterLookup[str] | None = strawberry_django.filter_field()
asset_tag: FilterLookup[str] | None = strawberry_django.filter_field()
discovered: FilterLookup[bool] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.InventoryItemRole, lookups=True) @strawberry_django.filter(models.InventoryItemRole, lookups=True)
@autotype_decorator(filtersets.InventoryItemRoleFilterSet) class InventoryItemRoleFilter(OrganizationalModelFilterMixin):
class InventoryItemRoleFilter(BaseFilterMixin): color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
pass
@strawberry_django.filter(models.Location, lookups=True) @strawberry_django.filter(models.Location, lookups=True)
@autotype_decorator(filtersets.LocationFilterSet) class LocationFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, NestedGroupModelFilterMixin):
class LocationFilter(BaseFilterMixin): site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
pass site_id: ID | None = strawberry_django.filter_field()
status: Annotated['LocationStatusEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
)
facility: FilterLookup[str] | None = strawberry_django.filter_field()
prefixes: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field()
)
vlan_groups: Annotated['VLANGroupFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter(models.Manufacturer, lookups=True) @strawberry_django.filter(models.Manufacturer, lookups=True)
@autotype_decorator(filtersets.ManufacturerFilterSet) class ManufacturerFilter(ContactFilterMixin, OrganizationalModelFilterMixin):
class ManufacturerFilter(BaseFilterMixin):
pass pass
@strawberry_django.filter(models.Module, lookups=True) @strawberry_django.filter(models.Module, lookups=True)
@autotype_decorator(filtersets.ModuleFilterSet) class ModuleFilter(PrimaryModelFilterMixin, ConfigContextFilterMixin):
class ModuleFilter(BaseFilterMixin): device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
pass device_id: ID | None = strawberry_django.filter_field()
module_bay: Annotated['ModuleBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
module_bay_id: ID | None = strawberry_django.filter_field()
module_type: Annotated['ModuleTypeFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
module_type_id: ID | None = strawberry_django.filter_field()
status: Annotated['ModuleStatusEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
)
serial: FilterLookup[str] | None = strawberry_django.filter_field()
asset_tag: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.ModuleBay, lookups=True) @strawberry_django.filter(models.ModuleBay, lookups=True)
@autotype_decorator(filtersets.ModuleBayFilterSet) class ModuleBayFilter(ModularComponentModelFilterMixin):
class ModuleBayFilter(BaseFilterMixin): parent: Annotated['ModuleBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
pass strawberry_django.filter_field()
)
parent_id: ID | None = strawberry_django.filter_field()
position: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.ModuleBayTemplate, lookups=True) @strawberry_django.filter(models.ModuleBayTemplate, lookups=True)
@autotype_decorator(filtersets.ModuleBayTemplateFilterSet) class ModuleBayTemplateFilter(ModularComponentTemplateFilterMixin):
class ModuleBayTemplateFilter(BaseFilterMixin): position: FilterLookup[str] | None = strawberry_django.filter_field()
pass
@strawberry_django.filter(models.ModuleTypeProfile, lookups=True)
class ModuleTypeProfileFilter(PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.ModuleType, lookups=True) @strawberry_django.filter(models.ModuleType, lookups=True)
@autotype_decorator(filtersets.ModuleTypeFilterSet) class ModuleTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, WeightFilterMixin):
class ModuleTypeFilter(BaseFilterMixin): manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
pass strawberry_django.filter_field()
)
manufacturer_id: ID | None = strawberry_django.filter_field()
model: FilterLookup[str] | None = strawberry_django.filter_field()
part_number: FilterLookup[str] | None = strawberry_django.filter_field()
airflow: Annotated['ModuleAirflowEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter(models.Platform, lookups=True) @strawberry_django.filter(models.Platform, lookups=True)
@autotype_decorator(filtersets.PlatformFilterSet) class PlatformFilter(OrganizationalModelFilterMixin):
class PlatformFilter(BaseFilterMixin): manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
pass strawberry_django.filter_field()
)
manufacturer_id: ID | None = strawberry_django.filter_field()
config_template: Annotated['ConfigTemplateFilter', strawberry.lazy('extras.graphql.filters')] | None = (
strawberry_django.filter_field()
)
config_template_id: ID | None = strawberry_django.filter_field()
@strawberry_django.filter(models.PowerFeed, lookups=True) @strawberry_django.filter(models.PowerFeed, lookups=True)
@autotype_decorator(filtersets.PowerFeedFilterSet) class PowerFeedFilter(CabledObjectModelFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
class PowerFeedFilter(BaseFilterMixin): power_panel: Annotated['PowerPanelFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
pass strawberry_django.filter_field()
)
power_panel_id: ID | None = strawberry_django.filter_field()
rack: Annotated['RackFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
rack_id: ID | None = strawberry_django.filter_field()
name: FilterLookup[str] | None = strawberry_django.filter_field()
status: Annotated['PowerFeedStatusEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
)
type: Annotated['PowerFeedTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
)
supply: Annotated['PowerFeedSupplyEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
)
phase: Annotated['PowerFeedPhaseEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
)
voltage: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
amperage: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
max_utilization: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
available_power: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter(models.PowerOutlet, lookups=True) @strawberry_django.filter(models.PowerOutlet, lookups=True)
@autotype_decorator(filtersets.PowerOutletFilterSet) class PowerOutletFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
class PowerOutletFilter(BaseFilterMixin): type: Annotated['PowerOutletTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
pass strawberry_django.filter_field()
)
power_port: Annotated['PowerPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
power_port_id: ID | None = strawberry_django.filter_field()
feed_leg: Annotated['PowerOutletFeedLegEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
)
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.PowerOutletTemplate, lookups=True) @strawberry_django.filter(models.PowerOutletTemplate, lookups=True)
@autotype_decorator(filtersets.PowerOutletTemplateFilterSet) class PowerOutletTemplateFilter(ModularComponentModelFilterMixin):
class PowerOutletTemplateFilter(BaseFilterMixin): type: Annotated['PowerOutletTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
pass strawberry_django.filter_field()
)
power_port: Annotated['PowerPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
power_port_id: ID | None = strawberry_django.filter_field()
feed_leg: Annotated['PowerOutletFeedLegEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter(models.PowerPanel, lookups=True) @strawberry_django.filter(models.PowerPanel, lookups=True)
@autotype_decorator(filtersets.PowerPanelFilterSet) class PowerPanelFilter(ContactFilterMixin, ImageAttachmentFilterMixin, PrimaryModelFilterMixin):
class PowerPanelFilter(BaseFilterMixin): site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
pass site_id: ID | None = strawberry_django.filter_field()
location: Annotated['LocationFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
location_id: Annotated['TreeNodeFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
name: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.PowerPort, lookups=True) @strawberry_django.filter(models.PowerPort, lookups=True)
@autotype_decorator(filtersets.PowerPortFilterSet) class PowerPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
class PowerPortFilter(BaseFilterMixin): type: Annotated['PowerPortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
pass strawberry_django.filter_field()
)
maximum_draw: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
allocated_draw: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter(models.PowerPortTemplate, lookups=True) @strawberry_django.filter(models.PowerPortTemplate, lookups=True)
@autotype_decorator(filtersets.PowerPortTemplateFilterSet) class PowerPortTemplateFilter(ModularComponentTemplateFilterMixin):
class PowerPortTemplateFilter(BaseFilterMixin): type: Annotated['PowerPortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
pass strawberry_django.filter_field()
)
maximum_draw: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
allocated_draw: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter(models.RackType, lookups=True) @strawberry_django.filter(models.RackType, lookups=True)
@autotype_decorator(filtersets.RackTypeFilterSet) class RackTypeFilter(RackBaseFilterMixin):
class RackTypeFilter(BaseFilterMixin): form_factor: Annotated['RackFormFactorEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
pass strawberry_django.filter_field()
)
manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
manufacturer_id: ID | None = strawberry_django.filter_field()
model: FilterLookup[str] | None = strawberry_django.filter_field()
slug: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.Rack, lookups=True) @strawberry_django.filter(models.Rack, lookups=True)
@autotype_decorator(filtersets.RackFilterSet) class RackFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, RackBaseFilterMixin):
class RackFilter(BaseFilterMixin): form_factor: Annotated['RackFormFactorEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
pass strawberry_django.filter_field()
)
rack_type: Annotated['RackTypeFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
rack_type_id: ID | None = strawberry_django.filter_field()
name: FilterLookup[str] | None = strawberry_django.filter_field()
facility_id: FilterLookup[str] | None = strawberry_django.filter_field()
site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
site_id: ID | None = strawberry_django.filter_field()
location: Annotated['LocationFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
location_id: Annotated['TreeNodeFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
status: Annotated['RackStatusEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
role: Annotated['RackRoleFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
role_id: ID | None = strawberry_django.filter_field()
serial: FilterLookup[str] | None = strawberry_django.filter_field()
asset_tag: FilterLookup[str] | None = strawberry_django.filter_field()
airflow: Annotated['RackAirflowEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
)
vlan_groups: Annotated['VLANGroupFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter(models.RackReservation, lookups=True) @strawberry_django.filter(models.RackReservation, lookups=True)
@autotype_decorator(filtersets.RackReservationFilterSet) class RackReservationFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
class RackReservationFilter(BaseFilterMixin): rack: Annotated['RackFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
pass rack_id: ID | None = strawberry_django.filter_field()
units: Annotated['IntegerArrayLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
user: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field()
user_id: ID | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.RackRole, lookups=True) @strawberry_django.filter(models.RackRole, lookups=True)
@autotype_decorator(filtersets.RackRoleFilterSet) class RackRoleFilter(OrganizationalModelFilterMixin):
class RackRoleFilter(BaseFilterMixin): color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
pass
@strawberry_django.filter(models.RearPort, lookups=True) @strawberry_django.filter(models.RearPort, lookups=True)
@autotype_decorator(filtersets.RearPortFilterSet) class RearPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
class RearPortFilter(BaseFilterMixin): type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
pass color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
positions: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter(models.RearPortTemplate, lookups=True) @strawberry_django.filter(models.RearPortTemplate, lookups=True)
@autotype_decorator(filtersets.RearPortTemplateFilterSet) class RearPortTemplateFilter(ModularComponentTemplateFilterMixin):
class RearPortTemplateFilter(BaseFilterMixin): type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
pass color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
positions: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter(models.Region, lookups=True) @strawberry_django.filter(models.Region, lookups=True)
@autotype_decorator(filtersets.RegionFilterSet) class RegionFilter(ContactFilterMixin, NestedGroupModelFilterMixin):
class RegionFilter(BaseFilterMixin): prefixes: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
pass strawberry_django.filter_field()
)
vlan_groups: Annotated['VLANGroupFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter(models.Site, lookups=True) @strawberry_django.filter(models.Site, lookups=True)
@autotype_decorator(filtersets.SiteFilterSet) class SiteFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
class SiteFilter(BaseFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field()
pass slug: FilterLookup[str] | None = strawberry_django.filter_field()
status: Annotated['SiteStatusEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
region: Annotated['RegionFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
region_id: Annotated['TreeNodeFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
group: Annotated['SiteGroupFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
group_id: Annotated['TreeNodeFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
facility: FilterLookup[str] | None = strawberry_django.filter_field()
asns: Annotated['ASNFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
time_zone: FilterLookup[str] | None = strawberry_django.filter_field()
physical_address: FilterLookup[str] | None = strawberry_django.filter_field()
shipping_address: FilterLookup[str] | None = strawberry_django.filter_field()
latitude: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
longitude: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
prefixes: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field()
)
vlan_groups: Annotated['VLANGroupFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter(models.SiteGroup, lookups=True) @strawberry_django.filter(models.SiteGroup, lookups=True)
@autotype_decorator(filtersets.SiteGroupFilterSet) class SiteGroupFilter(ContactFilterMixin, NestedGroupModelFilterMixin):
class SiteGroupFilter(BaseFilterMixin): prefixes: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
pass strawberry_django.filter_field()
)
vlan_groups: Annotated['VLANGroupFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter(models.VirtualChassis, lookups=True) @strawberry_django.filter(models.VirtualChassis, lookups=True)
@autotype_decorator(filtersets.VirtualChassisFilterSet) class VirtualChassisFilter(PrimaryModelFilterMixin):
class VirtualChassisFilter(BaseFilterMixin): master: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
pass master_id: ID | None = strawberry_django.filter_field()
name: FilterLookup[str] | None = strawberry_django.filter_field()
domain: FilterLookup[str] | None = strawberry_django.filter_field()
member_count: FilterLookup[int] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.VirtualDeviceContext, lookups=True) @strawberry_django.filter(models.VirtualDeviceContext, lookups=True)
@autotype_decorator(filtersets.VirtualDeviceContextFilterSet) class VirtualDeviceContextFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
class VirtualDeviceContextFilter(BaseFilterMixin): device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
pass device_id: ID | None = strawberry_django.filter_field()
name: FilterLookup[str] | None = strawberry_django.filter_field()
status: Annotated['VirtualDeviceContextStatusEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
)
identifier: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
primary_ip4: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field()
)
primary_ip4_id: ID | None = strawberry_django.filter_field()
primary_ip6: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field()
)
primary_ip6_id: ID | None = strawberry_django.filter_field()
comments: FilterLookup[str] | None = strawberry_django.filter_field()

View File

@ -77,6 +77,9 @@ class DCIMQuery:
module_bay_template: ModuleBayTemplateType = strawberry_django.field() module_bay_template: ModuleBayTemplateType = strawberry_django.field()
module_bay_template_list: List[ModuleBayTemplateType] = strawberry_django.field() module_bay_template_list: List[ModuleBayTemplateType] = strawberry_django.field()
module_type_profile: ModuleTypeProfileType = strawberry_django.field()
module_type_profile_list: List[ModuleTypeProfileType] = strawberry_django.field()
module_type: ModuleTypeType = strawberry_django.field() module_type: ModuleTypeType = strawberry_django.field()
module_type_list: List[ModuleTypeType] = strawberry_django.field() module_type_list: List[ModuleTypeType] = strawberry_django.field()

View File

@ -1,4 +1,4 @@
from typing import Annotated, List, Union from typing import Annotated, List, TYPE_CHECKING, Union
import strawberry import strawberry
import strawberry_django import strawberry_django
@ -6,7 +6,11 @@ import strawberry_django
from core.graphql.mixins import ChangelogMixin from core.graphql.mixins import ChangelogMixin
from dcim import models from dcim import models
from extras.graphql.mixins import ( from extras.graphql.mixins import (
ConfigContextMixin, ContactsMixin, CustomFieldsMixin, ImageAttachmentsMixin, TagsMixin, ConfigContextMixin,
ContactsMixin,
CustomFieldsMixin,
ImageAttachmentsMixin,
TagsMixin,
) )
from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin
from netbox.graphql.scalars import BigInt from netbox.graphql.scalars import BigInt
@ -14,6 +18,23 @@ from netbox.graphql.types import BaseObjectType, NetBoxObjectType, Organizationa
from .filters import * from .filters import *
from .mixins import CabledObjectMixin, PathEndpointMixin from .mixins import CabledObjectMixin, PathEndpointMixin
if TYPE_CHECKING:
from circuits.graphql.types import CircuitTerminationType
from extras.graphql.types import ConfigTemplateType
from ipam.graphql.types import (
ASNType,
IPAddressType,
PrefixType,
ServiceType,
VLANTranslationPolicyType,
VLANType,
VRFType,
)
from tenancy.graphql.types import TenantType
from users.graphql.types import UserType
from virtualization.graphql.types import ClusterType, VMInterfaceType, VirtualMachineType
from wireless.graphql.types import WirelessLANType, WirelessLinkType
__all__ = ( __all__ = (
'CableType', 'CableType',
'ComponentType', 'ComponentType',
@ -40,6 +61,7 @@ __all__ = (
'ModuleType', 'ModuleType',
'ModuleBayType', 'ModuleBayType',
'ModuleBayTemplateType', 'ModuleBayTemplateType',
'ModuleTypeProfileType',
'ModuleTypeType', 'ModuleTypeType',
'PlatformType', 'PlatformType',
'PowerFeedType', 'PowerFeedType',
@ -111,8 +133,9 @@ class ModularComponentTemplateType(ComponentTemplateType):
@strawberry_django.type( @strawberry_django.type(
models.CableTermination, models.CableTermination,
exclude=('termination_type', 'termination_id', '_device', '_rack', '_location', '_site'), exclude=['termination_type', 'termination_id', '_device', '_rack', '_location', '_site'],
filters=CableTerminationFilter filters=CableTerminationFilter,
pagination=True
) )
class CableTerminationType(NetBoxObjectType): class CableTerminationType(NetBoxObjectType):
cable: Annotated["CableType", strawberry.lazy('dcim.graphql.types')] | None cable: Annotated["CableType", strawberry.lazy('dcim.graphql.types')] | None
@ -132,7 +155,8 @@ class CableTerminationType(NetBoxObjectType):
@strawberry_django.type( @strawberry_django.type(
models.Cable, models.Cable,
fields='__all__', fields='__all__',
filters=CableFilter filters=CableFilter,
pagination=True
) )
class CableType(NetBoxObjectType): class CableType(NetBoxObjectType):
color: str color: str
@ -167,8 +191,9 @@ class CableType(NetBoxObjectType):
@strawberry_django.type( @strawberry_django.type(
models.ConsolePort, models.ConsolePort,
exclude=('_path',), exclude=['_path'],
filters=ConsolePortFilter filters=ConsolePortFilter,
pagination=True
) )
class ConsolePortType(ModularComponentType, CabledObjectMixin, PathEndpointMixin): class ConsolePortType(ModularComponentType, CabledObjectMixin, PathEndpointMixin):
pass pass
@ -177,7 +202,8 @@ class ConsolePortType(ModularComponentType, CabledObjectMixin, PathEndpointMixin
@strawberry_django.type( @strawberry_django.type(
models.ConsolePortTemplate, models.ConsolePortTemplate,
fields='__all__', fields='__all__',
filters=ConsolePortTemplateFilter filters=ConsolePortTemplateFilter,
pagination=True
) )
class ConsolePortTemplateType(ModularComponentTemplateType): class ConsolePortTemplateType(ModularComponentTemplateType):
pass pass
@ -185,8 +211,9 @@ class ConsolePortTemplateType(ModularComponentTemplateType):
@strawberry_django.type( @strawberry_django.type(
models.ConsoleServerPort, models.ConsoleServerPort,
exclude=('_path',), exclude=['_path'],
filters=ConsoleServerPortFilter filters=ConsoleServerPortFilter,
pagination=True
) )
class ConsoleServerPortType(ModularComponentType, CabledObjectMixin, PathEndpointMixin): class ConsoleServerPortType(ModularComponentType, CabledObjectMixin, PathEndpointMixin):
pass pass
@ -195,7 +222,8 @@ class ConsoleServerPortType(ModularComponentType, CabledObjectMixin, PathEndpoin
@strawberry_django.type( @strawberry_django.type(
models.ConsoleServerPortTemplate, models.ConsoleServerPortTemplate,
fields='__all__', fields='__all__',
filters=ConsoleServerPortTemplateFilter filters=ConsoleServerPortTemplateFilter,
pagination=True
) )
class ConsoleServerPortTemplateType(ModularComponentTemplateType): class ConsoleServerPortTemplateType(ModularComponentTemplateType):
pass pass
@ -204,7 +232,8 @@ class ConsoleServerPortTemplateType(ModularComponentTemplateType):
@strawberry_django.type( @strawberry_django.type(
models.Device, models.Device,
fields='__all__', fields='__all__',
filters=DeviceFilter filters=DeviceFilter,
pagination=True
) )
class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType): class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
console_port_count: BigInt console_port_count: BigInt
@ -259,7 +288,8 @@ class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, NetBo
@strawberry_django.type( @strawberry_django.type(
models.DeviceBay, models.DeviceBay,
fields='__all__', fields='__all__',
filters=DeviceBayFilter filters=DeviceBayFilter,
pagination=True
) )
class DeviceBayType(ComponentType): class DeviceBayType(ComponentType):
installed_device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')] | None installed_device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')] | None
@ -268,7 +298,8 @@ class DeviceBayType(ComponentType):
@strawberry_django.type( @strawberry_django.type(
models.DeviceBayTemplate, models.DeviceBayTemplate,
fields='__all__', fields='__all__',
filters=DeviceBayTemplateFilter filters=DeviceBayTemplateFilter,
pagination=True
) )
class DeviceBayTemplateType(ComponentTemplateType): class DeviceBayTemplateType(ComponentTemplateType):
pass pass
@ -276,8 +307,9 @@ class DeviceBayTemplateType(ComponentTemplateType):
@strawberry_django.type( @strawberry_django.type(
models.InventoryItemTemplate, models.InventoryItemTemplate,
exclude=('component_type', 'component_id', 'parent'), exclude=['component_type', 'component_id', 'parent'],
filters=InventoryItemTemplateFilter filters=InventoryItemTemplateFilter,
pagination=True
) )
class InventoryItemTemplateType(ComponentTemplateType): class InventoryItemTemplateType(ComponentTemplateType):
role: Annotated["InventoryItemRoleType", strawberry.lazy('dcim.graphql.types')] | None role: Annotated["InventoryItemRoleType", strawberry.lazy('dcim.graphql.types')] | None
@ -303,9 +335,12 @@ class InventoryItemTemplateType(ComponentTemplateType):
@strawberry_django.type( @strawberry_django.type(
models.DeviceRole, models.DeviceRole,
fields='__all__', fields='__all__',
filters=DeviceRoleFilter filters=DeviceRoleFilter,
pagination=True
) )
class DeviceRoleType(OrganizationalObjectType): class DeviceRoleType(OrganizationalObjectType):
parent: Annotated['DeviceRoleType', strawberry.lazy('dcim.graphql.types')] | None
children: List[Annotated['DeviceRoleType', strawberry.lazy('dcim.graphql.types')]]
color: str color: str
config_template: Annotated["ConfigTemplateType", strawberry.lazy('extras.graphql.types')] | None config_template: Annotated["ConfigTemplateType", strawberry.lazy('extras.graphql.types')] | None
@ -316,7 +351,8 @@ class DeviceRoleType(OrganizationalObjectType):
@strawberry_django.type( @strawberry_django.type(
models.DeviceType, models.DeviceType,
fields='__all__', fields='__all__',
filters=DeviceTypeFilter filters=DeviceTypeFilter,
pagination=True
) )
class DeviceTypeType(NetBoxObjectType): class DeviceTypeType(NetBoxObjectType):
console_port_template_count: BigInt console_port_template_count: BigInt
@ -350,7 +386,8 @@ class DeviceTypeType(NetBoxObjectType):
@strawberry_django.type( @strawberry_django.type(
models.FrontPort, models.FrontPort,
fields='__all__', fields='__all__',
filters=FrontPortFilter filters=FrontPortFilter,
pagination=True
) )
class FrontPortType(ModularComponentType, CabledObjectMixin): class FrontPortType(ModularComponentType, CabledObjectMixin):
color: str color: str
@ -360,7 +397,8 @@ class FrontPortType(ModularComponentType, CabledObjectMixin):
@strawberry_django.type( @strawberry_django.type(
models.FrontPortTemplate, models.FrontPortTemplate,
fields='__all__', fields='__all__',
filters=FrontPortTemplateFilter filters=FrontPortTemplateFilter,
pagination=True
) )
class FrontPortTemplateType(ModularComponentTemplateType): class FrontPortTemplateType(ModularComponentTemplateType):
color: str color: str
@ -369,8 +407,9 @@ class FrontPortTemplateType(ModularComponentTemplateType):
@strawberry_django.type( @strawberry_django.type(
models.MACAddress, models.MACAddress,
exclude=('assigned_object_type', 'assigned_object_id'), exclude=['assigned_object_type', 'assigned_object_id'],
filters=MACAddressFilter filters=MACAddressFilter,
pagination=True
) )
class MACAddressType(NetBoxObjectType): class MACAddressType(NetBoxObjectType):
mac_address: str mac_address: str
@ -385,8 +424,9 @@ class MACAddressType(NetBoxObjectType):
@strawberry_django.type( @strawberry_django.type(
models.Interface, models.Interface,
exclude=('_path',), exclude=['_path'],
filters=InterfaceFilter filters=InterfaceFilter,
pagination=True
) )
class InterfaceType(IPAddressesMixin, ModularComponentType, CabledObjectMixin, PathEndpointMixin): class InterfaceType(IPAddressesMixin, ModularComponentType, CabledObjectMixin, PathEndpointMixin):
_name: str _name: str
@ -413,7 +453,8 @@ class InterfaceType(IPAddressesMixin, ModularComponentType, CabledObjectMixin, P
@strawberry_django.type( @strawberry_django.type(
models.InterfaceTemplate, models.InterfaceTemplate,
fields='__all__', fields='__all__',
filters=InterfaceTemplateFilter filters=InterfaceTemplateFilter,
pagination=True
) )
class InterfaceTemplateType(ModularComponentTemplateType): class InterfaceTemplateType(ModularComponentTemplateType):
_name: str _name: str
@ -424,8 +465,9 @@ class InterfaceTemplateType(ModularComponentTemplateType):
@strawberry_django.type( @strawberry_django.type(
models.InventoryItem, models.InventoryItem,
exclude=('component_type', 'component_id', 'parent'), exclude=['component_type', 'component_id', 'parent'],
filters=InventoryItemFilter filters=InventoryItemFilter,
pagination=True
) )
class InventoryItemType(ComponentType): class InventoryItemType(ComponentType):
role: Annotated["InventoryItemRoleType", strawberry.lazy('dcim.graphql.types')] | None role: Annotated["InventoryItemRoleType", strawberry.lazy('dcim.graphql.types')] | None
@ -451,7 +493,8 @@ class InventoryItemType(ComponentType):
@strawberry_django.type( @strawberry_django.type(
models.InventoryItemRole, models.InventoryItemRole,
fields='__all__', fields='__all__',
filters=InventoryItemRoleFilter filters=InventoryItemRoleFilter,
pagination=True
) )
class InventoryItemRoleType(OrganizationalObjectType): class InventoryItemRoleType(OrganizationalObjectType):
color: str color: str
@ -463,8 +506,9 @@ class InventoryItemRoleType(OrganizationalObjectType):
@strawberry_django.type( @strawberry_django.type(
models.Location, models.Location,
# fields='__all__', # fields='__all__',
exclude=('parent',), # bug - temp exclude=['parent'], # bug - temp
filters=LocationFilter filters=LocationFilter,
pagination=True
) )
class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, OrganizationalObjectType): class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, OrganizationalObjectType):
site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')] site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]
@ -491,7 +535,8 @@ class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, Organi
@strawberry_django.type( @strawberry_django.type(
models.Manufacturer, models.Manufacturer,
fields='__all__', fields='__all__',
filters=ManufacturerFilter filters=ManufacturerFilter,
pagination=True
) )
class ManufacturerType(OrganizationalObjectType, ContactsMixin): class ManufacturerType(OrganizationalObjectType, ContactsMixin):
@ -505,7 +550,8 @@ class ManufacturerType(OrganizationalObjectType, ContactsMixin):
@strawberry_django.type( @strawberry_django.type(
models.Module, models.Module,
fields='__all__', fields='__all__',
filters=ModuleFilter filters=ModuleFilter,
pagination=True
) )
class ModuleType(NetBoxObjectType): class ModuleType(NetBoxObjectType):
device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')] device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]
@ -524,8 +570,9 @@ class ModuleType(NetBoxObjectType):
@strawberry_django.type( @strawberry_django.type(
models.ModuleBay, models.ModuleBay,
# fields='__all__', # fields='__all__',
exclude=('parent',), exclude=['parent'],
filters=ModuleBayFilter filters=ModuleBayFilter,
pagination=True
) )
class ModuleBayType(ModularComponentType): class ModuleBayType(ModularComponentType):
@ -540,18 +587,31 @@ class ModuleBayType(ModularComponentType):
@strawberry_django.type( @strawberry_django.type(
models.ModuleBayTemplate, models.ModuleBayTemplate,
fields='__all__', fields='__all__',
filters=ModuleBayTemplateFilter filters=ModuleBayTemplateFilter,
pagination=True
) )
class ModuleBayTemplateType(ModularComponentTemplateType): class ModuleBayTemplateType(ModularComponentTemplateType):
pass pass
@strawberry_django.type(
models.ModuleTypeProfile,
fields='__all__',
filters=ModuleTypeProfileFilter,
pagination=True
)
class ModuleTypeProfileType(NetBoxObjectType):
module_types: List[Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type( @strawberry_django.type(
models.ModuleType, models.ModuleType,
fields='__all__', fields='__all__',
filters=ModuleTypeFilter filters=ModuleTypeFilter,
pagination=True
) )
class ModuleTypeType(NetBoxObjectType): class ModuleTypeType(NetBoxObjectType):
profile: Annotated["ModuleTypeProfileType", strawberry.lazy('dcim.graphql.types')] | None
manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')] manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')]
frontporttemplates: List[Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]] frontporttemplates: List[Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]]
@ -567,7 +627,8 @@ class ModuleTypeType(NetBoxObjectType):
@strawberry_django.type( @strawberry_django.type(
models.Platform, models.Platform,
fields='__all__', fields='__all__',
filters=PlatformFilter filters=PlatformFilter,
pagination=True
) )
class PlatformType(OrganizationalObjectType): class PlatformType(OrganizationalObjectType):
manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')] | None manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')] | None
@ -579,8 +640,9 @@ class PlatformType(OrganizationalObjectType):
@strawberry_django.type( @strawberry_django.type(
models.PowerFeed, models.PowerFeed,
exclude=('_path',), exclude=['_path'],
filters=PowerFeedFilter filters=PowerFeedFilter,
pagination=True
) )
class PowerFeedType(NetBoxObjectType, CabledObjectMixin, PathEndpointMixin): class PowerFeedType(NetBoxObjectType, CabledObjectMixin, PathEndpointMixin):
power_panel: Annotated["PowerPanelType", strawberry.lazy('dcim.graphql.types')] power_panel: Annotated["PowerPanelType", strawberry.lazy('dcim.graphql.types')]
@ -590,8 +652,9 @@ class PowerFeedType(NetBoxObjectType, CabledObjectMixin, PathEndpointMixin):
@strawberry_django.type( @strawberry_django.type(
models.PowerOutlet, models.PowerOutlet,
exclude=('_path',), exclude=['_path'],
filters=PowerOutletFilter filters=PowerOutletFilter,
pagination=True
) )
class PowerOutletType(ModularComponentType, CabledObjectMixin, PathEndpointMixin): class PowerOutletType(ModularComponentType, CabledObjectMixin, PathEndpointMixin):
power_port: Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')] | None power_port: Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')] | None
@ -601,7 +664,8 @@ class PowerOutletType(ModularComponentType, CabledObjectMixin, PathEndpointMixin
@strawberry_django.type( @strawberry_django.type(
models.PowerOutletTemplate, models.PowerOutletTemplate,
fields='__all__', fields='__all__',
filters=PowerOutletTemplateFilter filters=PowerOutletTemplateFilter,
pagination=True
) )
class PowerOutletTemplateType(ModularComponentTemplateType): class PowerOutletTemplateType(ModularComponentTemplateType):
power_port: Annotated["PowerPortTemplateType", strawberry.lazy('dcim.graphql.types')] | None power_port: Annotated["PowerPortTemplateType", strawberry.lazy('dcim.graphql.types')] | None
@ -610,7 +674,8 @@ class PowerOutletTemplateType(ModularComponentTemplateType):
@strawberry_django.type( @strawberry_django.type(
models.PowerPanel, models.PowerPanel,
fields='__all__', fields='__all__',
filters=PowerPanelFilter filters=PowerPanelFilter,
pagination=True
) )
class PowerPanelType(NetBoxObjectType, ContactsMixin): class PowerPanelType(NetBoxObjectType, ContactsMixin):
site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')] site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]
@ -621,8 +686,9 @@ class PowerPanelType(NetBoxObjectType, ContactsMixin):
@strawberry_django.type( @strawberry_django.type(
models.PowerPort, models.PowerPort,
exclude=('_path',), exclude=['_path'],
filters=PowerPortFilter filters=PowerPortFilter,
pagination=True
) )
class PowerPortType(ModularComponentType, CabledObjectMixin, PathEndpointMixin): class PowerPortType(ModularComponentType, CabledObjectMixin, PathEndpointMixin):
@ -632,7 +698,8 @@ class PowerPortType(ModularComponentType, CabledObjectMixin, PathEndpointMixin):
@strawberry_django.type( @strawberry_django.type(
models.PowerPortTemplate, models.PowerPortTemplate,
fields='__all__', fields='__all__',
filters=PowerPortTemplateFilter filters=PowerPortTemplateFilter,
pagination=True
) )
class PowerPortTemplateType(ModularComponentTemplateType): class PowerPortTemplateType(ModularComponentTemplateType):
poweroutlet_templates: List[Annotated["PowerOutletTemplateType", strawberry.lazy('dcim.graphql.types')]] poweroutlet_templates: List[Annotated["PowerOutletTemplateType", strawberry.lazy('dcim.graphql.types')]]
@ -641,7 +708,8 @@ class PowerPortTemplateType(ModularComponentTemplateType):
@strawberry_django.type( @strawberry_django.type(
models.RackType, models.RackType,
fields='__all__', fields='__all__',
filters=RackTypeFilter filters=RackTypeFilter,
pagination=True
) )
class RackTypeType(NetBoxObjectType): class RackTypeType(NetBoxObjectType):
manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')] manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')]
@ -650,7 +718,8 @@ class RackTypeType(NetBoxObjectType):
@strawberry_django.type( @strawberry_django.type(
models.Rack, models.Rack,
fields='__all__', fields='__all__',
filters=RackFilter filters=RackFilter,
pagination=True
) )
class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType): class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')] site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]
@ -668,7 +737,8 @@ class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObje
@strawberry_django.type( @strawberry_django.type(
models.RackReservation, models.RackReservation,
fields='__all__', fields='__all__',
filters=RackReservationFilter filters=RackReservationFilter,
pagination=True
) )
class RackReservationType(NetBoxObjectType): class RackReservationType(NetBoxObjectType):
units: List[int] units: List[int]
@ -680,7 +750,8 @@ class RackReservationType(NetBoxObjectType):
@strawberry_django.type( @strawberry_django.type(
models.RackRole, models.RackRole,
fields='__all__', fields='__all__',
filters=RackRoleFilter filters=RackRoleFilter,
pagination=True
) )
class RackRoleType(OrganizationalObjectType): class RackRoleType(OrganizationalObjectType):
color: str color: str
@ -691,7 +762,8 @@ class RackRoleType(OrganizationalObjectType):
@strawberry_django.type( @strawberry_django.type(
models.RearPort, models.RearPort,
fields='__all__', fields='__all__',
filters=RearPortFilter filters=RearPortFilter,
pagination=True
) )
class RearPortType(ModularComponentType, CabledObjectMixin): class RearPortType(ModularComponentType, CabledObjectMixin):
color: str color: str
@ -702,7 +774,8 @@ class RearPortType(ModularComponentType, CabledObjectMixin):
@strawberry_django.type( @strawberry_django.type(
models.RearPortTemplate, models.RearPortTemplate,
fields='__all__', fields='__all__',
filters=RearPortTemplateFilter filters=RearPortTemplateFilter,
pagination=True
) )
class RearPortTemplateType(ModularComponentTemplateType): class RearPortTemplateType(ModularComponentTemplateType):
color: str color: str
@ -712,9 +785,9 @@ class RearPortTemplateType(ModularComponentTemplateType):
@strawberry_django.type( @strawberry_django.type(
models.Region, models.Region,
exclude=('parent',), exclude=['parent'],
# fields='__all__', filters=RegionFilter,
filters=RegionFilter pagination=True
) )
class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType): class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
@ -739,7 +812,8 @@ class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
@strawberry_django.type( @strawberry_django.type(
models.Site, models.Site,
fields='__all__', fields='__all__',
filters=SiteFilter filters=SiteFilter,
pagination=True
) )
class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType): class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
time_zone: str | None time_zone: str | None
@ -772,9 +846,9 @@ class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObje
@strawberry_django.type( @strawberry_django.type(
models.SiteGroup, models.SiteGroup,
# fields='__all__', exclude=['parent'], # bug - temp
exclude=('parent',), # bug - temp filters=SiteGroupFilter,
filters=SiteGroupFilter pagination=True
) )
class SiteGroupType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType): class SiteGroupType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
@ -799,7 +873,8 @@ class SiteGroupType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
@strawberry_django.type( @strawberry_django.type(
models.VirtualChassis, models.VirtualChassis,
fields='__all__', fields='__all__',
filters=VirtualChassisFilter filters=VirtualChassisFilter,
pagination=True
) )
class VirtualChassisType(NetBoxObjectType): class VirtualChassisType(NetBoxObjectType):
member_count: BigInt member_count: BigInt
@ -811,7 +886,8 @@ class VirtualChassisType(NetBoxObjectType):
@strawberry_django.type( @strawberry_django.type(
models.VirtualDeviceContext, models.VirtualDeviceContext,
fields='__all__', fields='__all__',
filters=VirtualDeviceContextFilter filters=VirtualDeviceContextFilter,
pagination=True
) )
class VirtualDeviceContextType(NetBoxObjectType): class VirtualDeviceContextType(NetBoxObjectType):
device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')] | None device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')] | None

View File

@ -7,11 +7,11 @@ import taggit.managers
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('dcim', '0001_initial'), ('dcim', '0001_squashed'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('contenttypes', '0002_remove_content_type_name'), ('contenttypes', '0002_remove_content_type_name'),
('extras', '0001_initial'), ('extras', '0001_squashed'),
('tenancy', '0001_initial'), ('tenancy', '0001_squashed_0012'),
] ]
replaces = [ replaces = [

View File

@ -5,12 +5,12 @@ import taggit.managers
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('dcim', '0002_auto_20160622_1821'), ('dcim', '0002_squashed'),
('virtualization', '0001_virtualization'), ('virtualization', '0001_squashed_0022'),
('contenttypes', '0002_remove_content_type_name'), ('contenttypes', '0002_remove_content_type_name'),
('ipam', '0001_initial'), ('ipam', '0001_squashed'),
('tenancy', '0001_initial'), ('tenancy', '0001_squashed_0012'),
('extras', '0002_custom_fields'), ('extras', '0002_squashed_0059'),
] ]
replaces = [ replaces = [
@ -505,28 +505,6 @@ class Migration(migrations.Migration):
model_name='cable', model_name='cable',
name='termination_a_type', name='termination_a_type',
field=models.ForeignKey( field=models.ForeignKey(
limit_choices_to=models.Q(
models.Q(
models.Q(('app_label', 'circuits'), ('model__in', ('circuittermination',))),
models.Q(
('app_label', 'dcim'),
(
'model__in',
(
'consoleport',
'consoleserverport',
'frontport',
'interface',
'powerfeed',
'poweroutlet',
'powerport',
'rearport',
),
),
),
_connector='OR',
)
),
on_delete=django.db.models.deletion.PROTECT, on_delete=django.db.models.deletion.PROTECT,
related_name='+', related_name='+',
to='contenttypes.contenttype', to='contenttypes.contenttype',
@ -536,28 +514,6 @@ class Migration(migrations.Migration):
model_name='cable', model_name='cable',
name='termination_b_type', name='termination_b_type',
field=models.ForeignKey( field=models.ForeignKey(
limit_choices_to=models.Q(
models.Q(
models.Q(('app_label', 'circuits'), ('model__in', ('circuittermination',))),
models.Q(
('app_label', 'dcim'),
(
'model__in',
(
'consoleport',
'consoleserverport',
'frontport',
'interface',
'powerfeed',
'poweroutlet',
'powerport',
'rearport',
),
),
),
_connector='OR',
)
),
on_delete=django.db.models.deletion.PROTECT, on_delete=django.db.models.deletion.PROTECT,
related_name='+', related_name='+',
to='contenttypes.contenttype', to='contenttypes.contenttype',

Some files were not shown because too many files have changed in this diff Show More