mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 01:41:22 -06:00
Merge pull request #19371 from netbox-community/release-v4.3.0
Release v4.3.0
This commit is contained in:
commit
c95098c4f0
@ -15,7 +15,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox version
|
label: NetBox version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v4.2.9
|
placeholder: v4.3.0
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
2
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
@ -27,7 +27,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox Version
|
label: NetBox Version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v4.2.9
|
placeholder: v4.3.0
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
@ -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,7 +61,8 @@ NetBox requires access to a PostgreSQL 13 or later database service to store dat
|
|||||||
Example:
|
Example:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
DATABASE = {
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.postgresql',
|
'ENGINE': 'django.db.backends.postgresql',
|
||||||
'NAME': 'netbox', # Database name
|
'NAME': 'netbox', # Database name
|
||||||
'USER': 'netbox', # PostgreSQL username
|
'USER': 'netbox', # PostgreSQL username
|
||||||
@ -46,6 +70,7 @@ DATABASE = {
|
|||||||
'HOST': 'localhost', # Database server
|
'HOST': 'localhost', # Database server
|
||||||
'PORT': '', # Database port (leave blank for default)
|
'PORT': '', # Database port (leave blank for default)
|
||||||
'CONN_MAX_AGE': 300, # Max database connection age
|
'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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -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'`
|
||||||
|
@ -12,6 +12,16 @@ BASE_PATH = 'netbox/'
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## DATABASE_ROUTERS
|
||||||
|
|
||||||
|
!!! info "This parameter was introduced in NetBox v4.3."
|
||||||
|
|
||||||
|
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)
|
||||||
@ -75,6 +85,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 +172,18 @@ The file path to the location where media files (such as image attachments) are
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## PROXY_ROUTERS
|
||||||
|
|
||||||
|
!!! info "This parameter was introduced in NetBox v4.3."
|
||||||
|
|
||||||
|
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 +208,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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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`
|
||||||
|
@ -2,39 +2,17 @@
|
|||||||
|
|
||||||
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
|
||||||
|
|
||||||
=== "Ubuntu"
|
```no-highlight
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y postgresql
|
||||||
|
```
|
||||||
|
|
||||||
```no-highlight
|
Before continuing, verify that you have installed PostgreSQL 14 or later:
|
||||||
sudo apt update
|
|
||||||
sudo apt install -y postgresql
|
|
||||||
```
|
|
||||||
|
|
||||||
=== "CentOS"
|
|
||||||
|
|
||||||
```no-highlight
|
|
||||||
sudo yum install -y postgresql-server
|
|
||||||
sudo postgresql-setup --initdb
|
|
||||||
```
|
|
||||||
|
|
||||||
CentOS configures ident host-based authentication for PostgreSQL by default. Because NetBox will need to authenticate using a username and password, modify `/var/lib/pgsql/data/pg_hba.conf` to support MD5 authentication by changing `ident` to `md5` for the lines below:
|
|
||||||
|
|
||||||
```no-highlight
|
|
||||||
host all all 127.0.0.1/32 md5
|
|
||||||
host all all ::1/128 md5
|
|
||||||
```
|
|
||||||
|
|
||||||
Once PostgreSQL has been installed, start the service and enable it to run at boot:
|
|
||||||
|
|
||||||
```no-highlight
|
|
||||||
sudo systemctl enable --now postgresql
|
|
||||||
```
|
|
||||||
|
|
||||||
Before continuing, verify that you have installed PostgreSQL 13 or later:
|
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
psql -V
|
psql -V
|
||||||
|
@ -4,18 +4,9 @@
|
|||||||
|
|
||||||
[Redis](https://redis.io/) is an in-memory key-value store which NetBox employs for caching and queuing. This section entails the installation and configuration of a local Redis instance. If you already have a Redis service in place, skip to [the next section](3-netbox.md).
|
[Redis](https://redis.io/) is an in-memory key-value store which NetBox employs for caching and queuing. This section entails the installation and configuration of a local Redis instance. If you already have a Redis service in place, skip to [the next section](3-netbox.md).
|
||||||
|
|
||||||
=== "Ubuntu"
|
```no-highlight
|
||||||
|
sudo apt install -y redis-server
|
||||||
```no-highlight
|
```
|
||||||
sudo apt install -y redis-server
|
|
||||||
```
|
|
||||||
|
|
||||||
=== "CentOS"
|
|
||||||
|
|
||||||
```no-highlight
|
|
||||||
sudo yum install -y redis
|
|
||||||
sudo systemctl enable --now redis
|
|
||||||
```
|
|
||||||
|
|
||||||
Before continuing, verify that your installed version of Redis is at least v4.0:
|
Before continuing, verify that your installed version of Redis is at least v4.0:
|
||||||
|
|
||||||
|
@ -9,17 +9,11 @@ Begin by installing all system packages required by NetBox and its dependencies.
|
|||||||
!!! warning "Python 3.10 or later required"
|
!!! warning "Python 3.10 or later required"
|
||||||
NetBox supports Python 3.10, 3.11, and 3.12.
|
NetBox supports Python 3.10, 3.11, and 3.12.
|
||||||
|
|
||||||
=== "Ubuntu"
|
```no-highlight
|
||||||
|
sudo apt install -y python3 python3-pip python3-venv python3-dev \
|
||||||
```no-highlight
|
build-essential libxml2-dev libxslt1-dev libffi-dev libpq-dev \
|
||||||
sudo apt install -y python3 python3-pip python3-venv python3-dev build-essential libxml2-dev libxslt1-dev libffi-dev libpq-dev libssl-dev zlib1g-dev
|
libssl-dev zlib1g-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "CentOS"
|
|
||||||
|
|
||||||
```no-highlight
|
|
||||||
sudo yum install -y gcc libxml2-devel libxslt-devel libffi-devel libpq-devel openssl-devel redhat-rpm-config
|
|
||||||
```
|
|
||||||
|
|
||||||
Before continuing, check that your installed Python version is at least 3.10:
|
Before continuing, check that your installed Python version is at least 3.10:
|
||||||
|
|
||||||
@ -55,17 +49,9 @@ cd /opt/netbox/
|
|||||||
|
|
||||||
If `git` is not already installed, install it:
|
If `git` is not already installed, install it:
|
||||||
|
|
||||||
=== "Ubuntu"
|
```no-highlight
|
||||||
|
sudo apt install -y git
|
||||||
```no-highlight
|
```
|
||||||
sudo apt install -y git
|
|
||||||
```
|
|
||||||
|
|
||||||
=== "CentOS"
|
|
||||||
|
|
||||||
```no-highlight
|
|
||||||
sudo yum install -y git
|
|
||||||
```
|
|
||||||
|
|
||||||
Next, clone the git repository:
|
Next, clone the git repository:
|
||||||
|
|
||||||
@ -97,24 +83,12 @@ Using this installation method enables easy upgrades in the future by simply che
|
|||||||
|
|
||||||
Create a system user account named `netbox`. We'll configure the WSGI and HTTP services to run under this account. We'll also assign this user ownership of the media directory. This ensures that NetBox will be able to save uploaded files.
|
Create a system user account named `netbox`. We'll configure the WSGI and HTTP services to run under this account. We'll also assign this user ownership of the media directory. This ensures that NetBox will be able to save uploaded files.
|
||||||
|
|
||||||
=== "Ubuntu"
|
```
|
||||||
|
sudo adduser --system --group netbox
|
||||||
```
|
sudo chown --recursive netbox /opt/netbox/netbox/media/
|
||||||
sudo adduser --system --group netbox
|
sudo chown --recursive netbox /opt/netbox/netbox/reports/
|
||||||
sudo chown --recursive netbox /opt/netbox/netbox/media/
|
sudo chown --recursive netbox /opt/netbox/netbox/scripts/
|
||||||
sudo chown --recursive netbox /opt/netbox/netbox/reports/
|
```
|
||||||
sudo chown --recursive netbox /opt/netbox/netbox/scripts/
|
|
||||||
```
|
|
||||||
|
|
||||||
=== "CentOS"
|
|
||||||
|
|
||||||
```
|
|
||||||
sudo groupadd --system netbox
|
|
||||||
sudo adduser --system -g netbox netbox
|
|
||||||
sudo chown --recursive netbox /opt/netbox/netbox/media/
|
|
||||||
sudo chown --recursive netbox /opt/netbox/netbox/reports/
|
|
||||||
sudo chown --recursive netbox /opt/netbox/netbox/scripts/
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
@ -128,7 +102,7 @@ sudo cp configuration_example.py configuration.py
|
|||||||
Open `configuration.py` with your preferred editor to begin configuring NetBox. NetBox offers [many configuration parameters](../configuration/index.md), but only the following four are required for new installations:
|
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 +120,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 = {
|
||||||
|
'default': {
|
||||||
'NAME': 'netbox', # Database name
|
'NAME': 'netbox', # Database name
|
||||||
'USER': 'netbox', # PostgreSQL username
|
'USER': 'netbox', # PostgreSQL username
|
||||||
'PASSWORD': 'J5brHrAXFLQSif0K', # PostgreSQL password
|
'PASSWORD': 'J5brHrAXFLQSif0K', # PostgreSQL password
|
||||||
'HOST': 'localhost', # Database server
|
'HOST': 'localhost', # Database server
|
||||||
'PORT': '', # Database port (leave blank for default)
|
'PORT': '', # Database port (leave blank for default)
|
||||||
'CONN_MAX_AGE': 300, # Max database connection age (seconds)
|
'CONN_MAX_AGE': 300, # Max database connection age (seconds)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -207,7 +185,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"
|
||||||
|
@ -6,18 +6,10 @@ This guide explains how to implement LDAP authentication using an external serve
|
|||||||
|
|
||||||
### Install System Packages
|
### Install System Packages
|
||||||
|
|
||||||
On Ubuntu:
|
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
sudo apt install -y libldap2-dev libsasl2-dev libssl-dev
|
sudo apt install -y libldap2-dev libsasl2-dev libssl-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
On CentOS:
|
|
||||||
|
|
||||||
```no-highlight
|
|
||||||
sudo yum install -y openldap-devel python3-devel
|
|
||||||
```
|
|
||||||
|
|
||||||
### Install django-auth-ldap
|
### Install django-auth-ldap
|
||||||
|
|
||||||
Activate the Python virtual environment and install the `django-auth-ldap` package using pip:
|
Activate the Python virtual environment and install the `django-auth-ldap` package using pip:
|
||||||
|
@ -1,9 +1,18 @@
|
|||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
!!! info "NetBox Cloud"
|
<div class="grid cards" markdown>
|
||||||
The instructions below are for installing NetBox as a standalone, self-hosted application. For a Cloud-delivered solution, check out [NetBox Cloud](https://netboxlabs.com/netbox-cloud/) by NetBox Labs.
|
|
||||||
|
|
||||||
The installation instructions provided here have been tested to work on Ubuntu 22.04 and CentOS 8.3. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
|
- :material-clock-fast:{ .lg .middle } __Eager to Get Started?__
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Check out the [NetBox Cloud Free Plan](https://netboxlabs.com/free-netbox-cloud/)! Skip the installation process and grab your own NetBox Cloud instance, preconfigured and ready to go in minutes. Completely free!
|
||||||
|
|
||||||
|
[:octicons-arrow-right-24: Sign Up](https://signup.netboxlabs.com/)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
The installation instructions provided here have been tested to work on Ubuntu 22.04. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
|
||||||
|
|
||||||
The following sections detail how to set up a new instance of NetBox:
|
The following sections detail how to set up a new instance of NetBox:
|
||||||
|
|
||||||
@ -19,7 +28,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:
|
||||||
|
@ -17,52 +17,50 @@ Prior to upgrading your NetBox instance, be sure to carefully review all [releas
|
|||||||
|
|
||||||
NetBox requires the following dependencies:
|
NetBox requires the following dependencies:
|
||||||
|
|
||||||
=== "Current Version"
|
| Dependency | Supported Versions |
|
||||||
|
|------------|--------------------|
|
||||||
|
| Python | 3.10, 3.11, 3.12 |
|
||||||
|
| PostgreSQL | 14+ |
|
||||||
|
| Redis | 4.0+ |
|
||||||
|
|
||||||
| Dependency | Supported Versions |
|
### Version History
|
||||||
|------------|--------------------|
|
|
||||||
| Python | 3.10, 3.11, 3.12 |
|
|
||||||
| PostgreSQL | 13+ |
|
|
||||||
| Redis | 4.0+ |
|
|
||||||
|
|
||||||
=== "All Versions"
|
|
||||||
|
|
||||||
| NetBox Version | Python min | Python max | PostgreSQL min | Redis min | Documentation |
|
|
||||||
|:--------------:|:----------:|:----------:|:--------------:|:---------:|:-------------------------------------------------------------------------------------------------:|
|
|
||||||
| 4.2 | 3.10 | 3.12 | **13** | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v4.2.0/docs/installation/index.md) |
|
|
||||||
| 4.1 | 3.10 | 3.12 | 12 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v4.1.0/docs/installation/index.md) |
|
|
||||||
| 4.0 | **3.10** | **3.12** | 12 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v4.0.0/docs/installation/index.md) |
|
|
||||||
| 3.7 | 3.8 | 3.11 | 12 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.7.0/docs/installation/index.md) |
|
|
||||||
| 3.6 | 3.8 | **3.11** | **12** | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.6.0/docs/installation/index.md) |
|
|
||||||
| 3.5 | 3.8 | 3.10 | 11 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.5.0/docs/installation/index.md) |
|
|
||||||
| 3.4 | 3.8 | 3.10 | **11** | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.4.0/docs/installation/index.md) |
|
|
||||||
| 3.3 | 3.8 | 3.10 | 10 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.3.0/docs/installation/index.md) |
|
|
||||||
| 3.2 | **3.8** | **3.10** | 10 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.2.0/docs/installation/index.md) |
|
|
||||||
| 3.1 | 3.7 | 3.9 | **10** | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.1.0/docs/installation/index.md) |
|
|
||||||
| 3.0 | **3.7** | 3.9 | 9.6 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.0.0/docs/installation/index.md) |
|
|
||||||
| 2.11 | 3.6 | **3.9** | 9.6 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v2.11.0/docs/installation/index.md) |
|
|
||||||
| 2.10 | 3.6 | 3.8 | **9.6** | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v2.10.0/docs/installation/index.md) |
|
|
||||||
| 2.9 | 3.6 | 3.8 | 9.5 | **4.0** | [Link](https://github.com/netbox-community/netbox/blob/v2.9.0/docs/installation/index.md) |
|
|
||||||
| 2.8 | **3.6** | **3.8** | **9.5** | **3.4** | [Link](https://github.com/netbox-community/netbox/blob/v2.8.0/docs/installation/index.md) |
|
|
||||||
| 2.7 | 3.5 | 3.7 | 9.4 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.7.0/docs/installation/index.md) |
|
|
||||||
| 2.6 | 3.5 | 3.7 | 9.4 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.6.0/docs/installation/index.md) |
|
|
||||||
| 2.5 | **3.5** | 3.7 | 9.4 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.5.0/docs/installation/index.md) |
|
|
||||||
| 2.4 | **3.4** | **3.7** | 9.4 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.4.0/docs/installation/index.md) |
|
|
||||||
| 2.3 | 2.7 | 3.6 | 9.4 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.3.0/docs/installation/postgresql.md) |
|
|
||||||
| 2.2 | 2.7 | 3.6 | **9.4** | - | [Link](https://github.com/netbox-community/netbox/blob/v2.2.0/docs/installation/postgresql.md) |
|
|
||||||
| 2.1 | 2.7 | 3.6 | 9.3 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.1.0/docs/installation/postgresql.md) |
|
|
||||||
| 2.0 | 2.7 | **3.6** | **9.3** | - | [Link](https://github.com/netbox-community/netbox/blob/v2.0.0/docs/installation/postgresql.md) |
|
|
||||||
| 1.9 | 2.7 | 3.5 | 9.2 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.9.0-r1/docs/installation/postgresql.md) |
|
|
||||||
| 1.8 | 2.7 | 3.5 | 9.2 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.8.0/docs/installation/postgresql.md) |
|
|
||||||
| 1.7 | 2.7 | 3.5 | 9.2 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.7.0/docs/installation/postgresql.md) |
|
|
||||||
| 1.6 | 2.7 | 3.5 | 9.2 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.6.0/docs/installation/postgresql.md) |
|
|
||||||
| 1.5 | 2.7 | 3.5 | **9.2** | - | [Link](https://github.com/netbox-community/netbox/blob/v1.5.0/docs/installation/postgresql.md) |
|
|
||||||
| 1.4 | 2.7 | 3.5 | 9.1 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.4.0/docs/installation/postgresql.md) |
|
|
||||||
| 1.3 | 2.7 | 3.5 | 9.1 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.3.0/docs/installation/postgresql.md) |
|
|
||||||
| 1.2 | 2.7 | 3.5 | 9.1 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.2.0/docs/installation/postgresql.md) |
|
|
||||||
| 1.1 | 2.7 | 3.5 | 9.1 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.1.0/docs/getting-started.md) |
|
|
||||||
| 1.0 | 2.7 | 3.5 | 9.1 | - | [Link](https://github.com/netbox-community/netbox/blob/1.0.0/docs/getting-started.md) |
|
|
||||||
|
|
||||||
|
| NetBox Version | Python min | Python max | PostgreSQL min | Redis min | Documentation |
|
||||||
|
|:--------------:|:----------:|:----------:|:--------------:|:---------:|:-------------------------------------------------------------------------------------------------:|
|
||||||
|
| 4.3 | 3.10 | 3.12 | 14 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v4.3.0/docs/installation/index.md) |
|
||||||
|
| 4.2 | 3.10 | 3.12 | 13 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v4.2.0/docs/installation/index.md) |
|
||||||
|
| 4.1 | 3.10 | 3.12 | 12 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v4.1.0/docs/installation/index.md) |
|
||||||
|
| 4.0 | 3.10 | 3.12 | 12 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v4.0.0/docs/installation/index.md) |
|
||||||
|
| 3.7 | 3.8 | 3.11 | 12 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.7.0/docs/installation/index.md) |
|
||||||
|
| 3.6 | 3.8 | 3.11 | 12 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.6.0/docs/installation/index.md) |
|
||||||
|
| 3.5 | 3.8 | 3.10 | 11 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.5.0/docs/installation/index.md) |
|
||||||
|
| 3.4 | 3.8 | 3.10 | 11 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.4.0/docs/installation/index.md) |
|
||||||
|
| 3.3 | 3.8 | 3.10 | 10 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.3.0/docs/installation/index.md) |
|
||||||
|
| 3.2 | 3.8 | 3.10 | 10 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.2.0/docs/installation/index.md) |
|
||||||
|
| 3.1 | 3.7 | 3.9 | 10 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.1.0/docs/installation/index.md) |
|
||||||
|
| 3.0 | 3.7 | 3.9 | 9.6 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.0.0/docs/installation/index.md) |
|
||||||
|
| 2.11 | 3.6 | 3.9 | 9.6 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v2.11.0/docs/installation/index.md) |
|
||||||
|
| 2.10 | 3.6 | 3.8 | 9.6 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v2.10.0/docs/installation/index.md) |
|
||||||
|
| 2.9 | 3.6 | 3.8 | 9.5 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v2.9.0/docs/installation/index.md) |
|
||||||
|
| 2.8 | 3.6 | 3.8 | 9.5 | 3.4 | [Link](https://github.com/netbox-community/netbox/blob/v2.8.0/docs/installation/index.md) |
|
||||||
|
| 2.7 | 3.5 | 3.7 | 9.4 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.7.0/docs/installation/index.md) |
|
||||||
|
| 2.6 | 3.5 | 3.7 | 9.4 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.6.0/docs/installation/index.md) |
|
||||||
|
| 2.5 | 3.5 | 3.7 | 9.4 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.5.0/docs/installation/index.md) |
|
||||||
|
| 2.4 | 3.4 | 3.7 | 9.4 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.4.0/docs/installation/index.md) |
|
||||||
|
| 2.3 | 2.7 | 3.6 | 9.4 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.3.0/docs/installation/postgresql.md) |
|
||||||
|
| 2.2 | 2.7 | 3.6 | 9.4 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.2.0/docs/installation/postgresql.md) |
|
||||||
|
| 2.1 | 2.7 | 3.6 | 9.3 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.1.0/docs/installation/postgresql.md) |
|
||||||
|
| 2.0 | 2.7 | 3.6 | 9.3 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.0.0/docs/installation/postgresql.md) |
|
||||||
|
| 1.9 | 2.7 | 3.5 | 9.2 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.9.0-r1/docs/installation/postgresql.md) |
|
||||||
|
| 1.8 | 2.7 | 3.5 | 9.2 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.8.0/docs/installation/postgresql.md) |
|
||||||
|
| 1.7 | 2.7 | 3.5 | 9.2 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.7.0/docs/installation/postgresql.md) |
|
||||||
|
| 1.6 | 2.7 | 3.5 | 9.2 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.6.0/docs/installation/postgresql.md) |
|
||||||
|
| 1.5 | 2.7 | 3.5 | 9.2 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.5.0/docs/installation/postgresql.md) |
|
||||||
|
| 1.4 | 2.7 | 3.5 | 9.1 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.4.0/docs/installation/postgresql.md) |
|
||||||
|
| 1.3 | 2.7 | 3.5 | 9.1 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.3.0/docs/installation/postgresql.md) |
|
||||||
|
| 1.2 | 2.7 | 3.5 | 9.1 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.2.0/docs/installation/postgresql.md) |
|
||||||
|
| 1.1 | 2.7 | 3.5 | 9.1 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.1.0/docs/getting-started.md) |
|
||||||
|
| 1.0 | 2.7 | 3.5 | 9.1 | - | [Link](https://github.com/netbox-community/netbox/blob/1.0.0/docs/getting-started.md) |
|
||||||
|
|
||||||
## 3. Install the Latest Release
|
## 3. Install the Latest Release
|
||||||
|
|
||||||
|
@ -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,20 +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
|
||||||
@ -103,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:
|
||||||
|
@ -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 |
|
||||||
|
@ -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.
|
||||||
|
@ -4,6 +4,12 @@ Devices can be organized by functional roles, which are fully customizable by th
|
|||||||
|
|
||||||
## Fields
|
## Fields
|
||||||
|
|
||||||
|
### Parent
|
||||||
|
|
||||||
|
!!! info "This field was introduced in NetBox v4.3."
|
||||||
|
|
||||||
|
The parent role of which this role is a child (optional).
|
||||||
|
|
||||||
### Name
|
### Name
|
||||||
|
|
||||||
A unique human-friendly name.
|
A unique human-friendly name.
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
|
@ -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.
|
||||||
|
40
docs/models/dcim/moduletypeprofile.md
Normal file
40
docs/models/dcim/moduletypeprofile.md
Normal 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).
|
@ -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."
|
||||||
|
@ -40,7 +40,9 @@ 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.
|
||||||
|
|
||||||
|
!!! info "The `outer_height` field was introduced in NetBox v4.3."
|
||||||
|
|
||||||
### Mounting Depth
|
### Mounting Depth
|
||||||
|
|
||||||
|
@ -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).
|
|
@ -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).
|
@ -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).
|
||||||
|
@ -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).
|
|
43
docs/models/extras/tableconfig.md
Normal file
43
docs/models/extras/tableconfig.md
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# Table Configs
|
||||||
|
|
||||||
|
This object represents the saved configuration of an object table in NetBox. Table configs can be crafted, saved, and shared among users to apply specific views within object lists. Each table config can specify which table columns to display, the order in which to display them, and which columns are used for sorting.
|
||||||
|
|
||||||
|
For example, you might wish to create a table config for the devices list to assist in inventory tasks. This view might show the device name, location, serial number, and asset tag, but omit operational details like IP addresses. Once applied, this table config can be saved for reuse in future audits.
|
||||||
|
|
||||||
|
## Fields
|
||||||
|
|
||||||
|
### Name
|
||||||
|
|
||||||
|
A human-friendly name for the table config.
|
||||||
|
|
||||||
|
### User
|
||||||
|
|
||||||
|
The user to which this filter belongs. The current user will be assigned automatically when saving a table config via the UI, and cannot be changed.
|
||||||
|
|
||||||
|
### Object Type
|
||||||
|
|
||||||
|
The type of NetBox object to which the table config pertains.
|
||||||
|
|
||||||
|
### Table
|
||||||
|
|
||||||
|
The name of the specific table to which the table config pertains. (Some NetBox object use multiple tables.)
|
||||||
|
|
||||||
|
### Weight
|
||||||
|
|
||||||
|
A numeric weight used to influence the order in which table configs are listed. Table configs with a lower weight will be listed before those with a higher weight. Table configs having the same weight will be ordered alphabetically.
|
||||||
|
|
||||||
|
### Enabled
|
||||||
|
|
||||||
|
Determines whether this table config can be used. Disabled table configs will not appear as options in the UI, however they will be included in API results.
|
||||||
|
|
||||||
|
### Shared
|
||||||
|
|
||||||
|
Determines whether this table config is intended for use by all users or only its owner. Note that deselecting this option does **not** hide the table config from other users; it is merely excluded from the list of available table configs in UI object list views.
|
||||||
|
|
||||||
|
### Ordering
|
||||||
|
|
||||||
|
A list of column names by which the table is to be ordered. If left blank, the table's default ordering will be used.
|
||||||
|
|
||||||
|
### Columns
|
||||||
|
|
||||||
|
A list of columns to be displayed in the table. The table will render these columns in the order they appear in the list. At least one column must be selected.
|
@ -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.
|
||||||
|
@ -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.
|
||||||
|
@ -6,6 +6,15 @@ To aid in the efficient creation of services, users may opt to first create a [s
|
|||||||
|
|
||||||
## Fields
|
## Fields
|
||||||
|
|
||||||
|
### Parent
|
||||||
|
|
||||||
|
The parent object to which the service is assigned. This must be one of [Device](../dcim/device.md),
|
||||||
|
[VirtualMachine](../virtualization/virtualmachine.md), or [FHRP Group](./fhrpgroup.md).
|
||||||
|
|
||||||
|
!!! note "Changed in NetBox v4.3"
|
||||||
|
|
||||||
|
Previously, `parent` was a property that pointed to either a Device or Virtual Machine. With the capability to assign services to FHRP groups, this is a unified in a concrete field.
|
||||||
|
|
||||||
### Name
|
### Name
|
||||||
|
|
||||||
A service or protocol name.
|
A service or protocol name.
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
|
||||||
@ -61,6 +61,11 @@ class MyModelViewSet(...):
|
|||||||
|
|
||||||
The `TagFilter` class is available for all models which support tag assignment (those which inherit from `NetBoxModel` or `TagsMixin`). This filter subclasses django-filter's `ModelMultipleChoiceFilter` to work with NetBox's `TaggedItem` class.
|
The `TagFilter` class is available for all models which support tag assignment (those which inherit from `NetBoxModel` or `TagsMixin`). This filter subclasses django-filter's `ModelMultipleChoiceFilter` to work with NetBox's `TaggedItem` class.
|
||||||
|
|
||||||
|
This class filters `tags` using the `slug` field. For example:
|
||||||
|
|
||||||
|
`GET /api/dcim/sites/?tag=alpha&tag=bravo`
|
||||||
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from django_filters import FilterSet
|
from django_filters import FilterSet
|
||||||
from extras.filters import TagFilter
|
from extras.filters import TagFilter
|
||||||
@ -68,3 +73,19 @@ from extras.filters import TagFilter
|
|||||||
class MyModelFilterSet(FilterSet):
|
class MyModelFilterSet(FilterSet):
|
||||||
tag = TagFilter()
|
tag = TagFilter()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### TagIDFilter
|
||||||
|
|
||||||
|
The `TagIDFilter` class is available for all models which support tag assignment (those which inherit from `NetBoxModel` or `TagsMixin`). This filter subclasses django-filter's `ModelMultipleChoiceFilter` to work with NetBox's `TaggedItem` class.
|
||||||
|
|
||||||
|
This class filters `tags` using the `id` field. For example:
|
||||||
|
|
||||||
|
`GET /api/dcim/sites/?tag_id=100&tag_id=200`
|
||||||
|
|
||||||
|
```python
|
||||||
|
from django_filters import FilterSet
|
||||||
|
from extras.filters import TagIDFilter
|
||||||
|
|
||||||
|
class MyModelFilterSet(FilterSet):
|
||||||
|
tag_id = TagIDFilter()
|
||||||
|
```
|
||||||
|
@ -103,6 +103,7 @@ NetBox looks for the `config` variable within a plugin's `__init__.py` to load i
|
|||||||
| `name` | Raw plugin name; same as the plugin's source directory |
|
| `name` | Raw plugin name; same as the plugin's source directory |
|
||||||
| `verbose_name` | Human-friendly name for the plugin |
|
| `verbose_name` | Human-friendly name for the plugin |
|
||||||
| `version` | Current release ([semantic versioning](https://semver.org/) is encouraged) |
|
| `version` | Current release ([semantic versioning](https://semver.org/) is encouraged) |
|
||||||
|
| `release_track` | An alternate release track (e.g. `dev` or `beta`) to which a release belongs |
|
||||||
| `description` | Brief description of the plugin's purpose |
|
| `description` | Brief description of the plugin's purpose |
|
||||||
| `author` | Name of plugin's author |
|
| `author` | Name of plugin's author |
|
||||||
| `author_email` | Author's public email address |
|
| `author_email` | Author's public email address |
|
||||||
|
@ -117,6 +117,10 @@ For more information about database migrations, see the [Django documentation](h
|
|||||||
|
|
||||||
::: netbox.models.features.CloningMixin
|
::: netbox.models.features.CloningMixin
|
||||||
|
|
||||||
|
::: netbox.models.features.ContactsMixin
|
||||||
|
|
||||||
|
!!! info "Plugin support for ContactsMixin was introduced in NetBox v4.3."
|
||||||
|
|
||||||
::: netbox.models.features.CustomLinksMixin
|
::: netbox.models.features.CustomLinksMixin
|
||||||
|
|
||||||
::: netbox.models.features.CustomFieldsMixin
|
::: netbox.models.features.CustomFieldsMixin
|
||||||
@ -125,9 +129,6 @@ For more information about database migrations, see the [Django documentation](h
|
|||||||
|
|
||||||
::: netbox.models.features.EventRulesMixin
|
::: netbox.models.features.EventRulesMixin
|
||||||
|
|
||||||
!!! note
|
|
||||||
`EventRulesMixin` was renamed from `WebhooksMixin` in NetBox v3.7.
|
|
||||||
|
|
||||||
::: netbox.models.features.ExportTemplatesMixin
|
::: netbox.models.features.ExportTemplatesMixin
|
||||||
|
|
||||||
::: netbox.models.features.JobsMixin
|
::: netbox.models.features.JobsMixin
|
||||||
|
@ -65,10 +65,11 @@ item1 = PluginMenuItem(
|
|||||||
A `PluginMenuItem` has the following attributes:
|
A `PluginMenuItem` has the following attributes:
|
||||||
|
|
||||||
| Attribute | Required | Description |
|
| Attribute | Required | Description |
|
||||||
|---------------|----------|----------------------------------------------------------------------------------------------------------|
|
|-----------------|----------|----------------------------------------------------------------------------------------------------------|
|
||||||
| `link` | Yes | Name of the URL path to which this menu item links |
|
| `link` | Yes | Name of the URL path to which this menu item links |
|
||||||
| `link_text` | Yes | The text presented to the user |
|
| `link_text` | Yes | The text presented to the user |
|
||||||
| `permissions` | - | A list of permissions required to display this link |
|
| `permissions` | - | A list of permissions required to display this link |
|
||||||
|
| `auth_required` | - | Display only for authenticated users |
|
||||||
| `staff_only` | - | Display only for users who have `is_staff` set to true (any specified permissions will also be required) |
|
| `staff_only` | - | Display only for users who have `is_staff` set to true (any specified permissions will also be required) |
|
||||||
| `buttons` | - | An iterable of PluginMenuButton instances to include |
|
| `buttons` | - | An iterable of PluginMenuButton instances to include |
|
||||||
|
|
||||||
|
@ -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).
|
|
@ -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 |
|
||||||
|
@ -10,6 +10,15 @@ Minor releases are published in April, August, and December of each calendar yea
|
|||||||
|
|
||||||
This page contains a history of all major and minor releases since NetBox v2.0. For more detail on a specific patch release, please see the release notes page for that specific minor release.
|
This page contains a history of all major and minor releases since NetBox v2.0. For more detail on a specific patch release, please see the release notes page for that specific minor release.
|
||||||
|
|
||||||
|
#### [Version 4.3](./version-4.3.md) (May 2025)
|
||||||
|
|
||||||
|
* Module Type Profiles & Custom Attributes ([#19002](https://github.com/netbox-community/netbox/issues/19002))
|
||||||
|
* Reusable Table Configurations ([#14591](https://github.com/netbox-community/netbox/issues/14591))
|
||||||
|
* Option to Treat IP Ranges as Fully Populated ([#9763](https://github.com/netbox-community/netbox/issues/9763))
|
||||||
|
* Hierarchical Device Roles ([#18245](https://github.com/netbox-community/netbox/issues/18245))
|
||||||
|
* Periodic Synchronization of Data Sources ([#18287](https://github.com/netbox-community/netbox/issues/18287))
|
||||||
|
* Proxy Routing ([#18627](https://github.com/netbox-community/netbox/issues/18627))
|
||||||
|
|
||||||
#### [Version 4.2](./version-4.2.md) (January 2025)
|
#### [Version 4.2](./version-4.2.md) (January 2025)
|
||||||
|
|
||||||
* Assign Multiple MAC Addresses per Interface ([#4867](https://github.com/netbox-community/netbox/issues/4867))
|
* Assign Multiple MAC Addresses per Interface ([#4867](https://github.com/netbox-community/netbox/issues/4867))
|
||||||
|
134
docs/release-notes/version-4.3.md
Normal file
134
docs/release-notes/version-4.3.md
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
## v4.3.0 (2025-05-01)
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
* The GraphQL API Now uses an advanced syntax for filtering, to enable e.g. logical AND/OR filtering and custom field lookups.
|
||||||
|
* PostgreSQL 13 is no longer supported. NetBox v4.3 requires PostgreSQL 14.0 or later.
|
||||||
|
* The `ALLOW_TOKEN_RETRIEVAL` configuration parameter now defaults to False.
|
||||||
|
* The `device` and `virtual_machine` foreign keys on the Service model have been replaced with a generic `parent` relationship to support the assignment of services to FHRP groups as well.
|
||||||
|
* The `group` foreign key on the Contact model has been replaced with a many-to-many `groups` field.
|
||||||
|
* `django-storages` is now a required dependency. (It will be installed automatically on upgrade.)
|
||||||
|
* PluginTemplateExtension no longer supports registration via the singular `model` attribute (use `models` instead).
|
||||||
|
* The legacy staged changes functionality has been removed.
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
|
||||||
|
#### Module Type Profiles & Custom Attributes ([#19002](https://github.com/netbox-community/netbox/issues/19002))
|
||||||
|
|
||||||
|
The new [module type profile](../models/dcim/moduletypeprofile.md) model enables users to declare custom profiles for module types, with the ability to define custom attributes for each profile according to its functional role. For example, a CPU module type might declare architecture and clock speed attributes; a hard disk profile might declare attributes for type and speed.
|
||||||
|
|
||||||
|
Attributes can be declared on each profile using [JSON schema](https://json-schema.org/), which allows for attributes to be declared as strings (text), integers, decimals, booleans, or choice fields. Profile attributes render as individual form fields when modifying a module type. Several profiles have been included by default to serve as examples, however these may be modified or removed.
|
||||||
|
|
||||||
|
#### Reusable Table Configurations ([#14591](https://github.com/netbox-community/netbox/issues/14591))
|
||||||
|
|
||||||
|
After modifying the displayed columns and/or ordering for a specific object table in the user interface, users now have the option to save that configuration so that it can be reused in the future. Similar to saved filters, table configs can be shared with other users to easily replicate table layouts crafted to serve specific use cases.
|
||||||
|
|
||||||
|
#### Option to Treat IP Ranges as Fully Populated ([#9763](https://github.com/netbox-community/netbox/issues/9763))
|
||||||
|
|
||||||
|
A new `mark_populated` boolean field has been added to the IPRange model. If set to true, NetBox will consider the IP range to be fully populated, and will not permit the creation of individual IP addresses within the range. For example, you might defer the management of an IP range to an external DHCP server, and wish for NetBox to treat the range as a opaque monolithic block for planning and allocation purposes.
|
||||||
|
|
||||||
|
#### Hierarchical Device Roles ([#18245](https://github.com/netbox-community/netbox/issues/18245))
|
||||||
|
|
||||||
|
Device roles can now be arranged hierarchically, with one role optionally serving as a parent to one or more child roles. For example, you might wish to create a generic "Server" role for devices with "Application Server" and "Database Server" roles beneath it. A device could then be assigned to any of these three roles.
|
||||||
|
|
||||||
|
#### Periodic Synchronization of Data Sources ([#18287](https://github.com/netbox-community/netbox/issues/18287))
|
||||||
|
|
||||||
|
Data sources can now be configured to synchronize automatically at a specified interval, as indicated by the new `sync_interval` field. No additional system configuration is necessary to support this functionality; background jobs will be scheduled automatically by the RQ worker process.
|
||||||
|
|
||||||
|
#### Proxy Routing ([#18627](https://github.com/netbox-community/netbox/issues/18627))
|
||||||
|
|
||||||
|
User can now declare one or more proxy routers via the `PROXY_ROUTERS` configuration parameter to control the use of specific proxy servers for various outbound connections. For example, it is now possible to configure NetBox to use different proxies based on the type of outbound traffic or its destination.
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#7598](https://github.com/netbox-community/netbox/issues/7598) - Adopt advanced query filtering in GraphQL API to support filtering by custom fields
|
||||||
|
* [#8423](https://github.com/netbox-community/netbox/issues/8423) - Enable assigning services to FHRP groups
|
||||||
|
* [#15842](https://github.com/netbox-community/netbox/issues/15842) - Introduce the `LOGIN_FORM_HIDDEN` configuration parameter
|
||||||
|
* [#16224](https://github.com/netbox-community/netbox/issues/16224) - Implement pagination support for the GraphQL API
|
||||||
|
* [#17170](https://github.com/netbox-community/netbox/issues/17170) - Enable the assignment of a contact to multiple contact groups
|
||||||
|
* [#17443](https://github.com/netbox-community/netbox/issues/17443) - Add a `file_name` field to the export template model
|
||||||
|
* [#17602](https://github.com/netbox-community/netbox/issues/17602) - Add a `comments` field to all nested group models (Region, SiteGroup, Location, ContactGroup, TenantGroup, and WirelessLANGroup)
|
||||||
|
* [#17608](https://github.com/netbox-community/netbox/issues/17608) - Add a `status` field to the L2VPN model
|
||||||
|
* [#17653](https://github.com/netbox-community/netbox/issues/17653) - Enable declaring Jinja environment parameters on export templates (similar to config templates)
|
||||||
|
* [#17793](https://github.com/netbox-community/netbox/issues/17793) - Introduce a REST API endpoint for tagged objects (`/api/extras/tagged-objects/`)
|
||||||
|
* [#17841](https://github.com/netbox-community/netbox/issues/17841) - Add a `weight` field to the Tag model to influence ordering
|
||||||
|
* [#18296](https://github.com/netbox-community/netbox/issues/18296) - Add a `tenant` field to the VLAN group model
|
||||||
|
* [#18352](https://github.com/netbox-community/netbox/issues/18352) - Add a `status` field to the power outlet model
|
||||||
|
* [#18417](https://github.com/netbox-community/netbox/issues/18417) - Add an `outer_height` field to the rack & rack type models
|
||||||
|
* [#18535](https://github.com/netbox-community/netbox/issues/18535) - The presence of incompatible plugins will no longer prevent NetBox from starting
|
||||||
|
* [#18780](https://github.com/netbox-community/netbox/issues/18780) - Introduce `DATABASES` and `DATABASE_ROUTERS` configuration parameters to enable defining connections to external databases (e.g. for plugins)
|
||||||
|
* [#18783](https://github.com/netbox-community/netbox/issues/18783) - Enable filtering all applicable models by tag ID
|
||||||
|
* [#18785](https://github.com/netbox-community/netbox/issues/18785) - Enable custom choices for rack, device, and module airflow
|
||||||
|
* [#18896](https://github.com/netbox-community/netbox/issues/18896) - Enable the use of remote storage for custom scripts
|
||||||
|
|
||||||
|
### Plugins
|
||||||
|
|
||||||
|
* [#16630](https://github.com/netbox-community/netbox/issues/16630) - Plugins can now inject content within the HTML `<head>` block via the new `plugin_head()` method on PluginTemplateExtension
|
||||||
|
* [#17424](https://github.com/netbox-community/netbox/issues/17424) - Extend ViewTab with a `visible` argument to control tab rendering
|
||||||
|
* [#17857](https://github.com/netbox-community/netbox/issues/17857) - Added a `release_track` attribute to PluginConfig
|
||||||
|
* [#18305](https://github.com/netbox-community/netbox/issues/18305) - Introduce plugin support for ContactsMixin
|
||||||
|
* [#19073](https://github.com/netbox-community/netbox/issues/19073) - Allow installed plugins to be omitted from the plugins list
|
||||||
|
|
||||||
|
### Other Changes
|
||||||
|
|
||||||
|
* [#18071](https://github.com/netbox-community/netbox/issues/18071) - Removed legacy staged changed functionality in favor of the [netbox-branching](https://github.com/netboxlabs/netbox-branching) plugin
|
||||||
|
* [#18072](https://github.com/netbox-community/netbox/issues/18072) - Drop support for the singular `model` attribute on PluginTemplateExtension (use `models` instead)
|
||||||
|
* [#18191](https://github.com/netbox-community/netbox/issues/18191) - Remove redundant PostgreSQL indexes
|
||||||
|
* [#18236](https://github.com/netbox-community/netbox/issues/18236) - Upgrade the HTMX library to v2.0
|
||||||
|
* [#18540](https://github.com/netbox-community/netbox/issues/18540) - Operational plugins are now recorded in the application registry
|
||||||
|
* [#18623](https://github.com/netbox-community/netbox/issues/18623) - Upgrade the Tabler CSS theme to v1.2
|
||||||
|
* [#18743](https://github.com/netbox-community/netbox/issues/18743) - Upgrade Django to v5.2
|
||||||
|
* [#18751](https://github.com/netbox-community/netbox/issues/18751) - Change the default value for `ALLOW_TOKEN_RETRIEVAL` to False
|
||||||
|
* [#18808](https://github.com/netbox-community/netbox/issues/18808) - Squashed migration dependencies have been altered to rectify an issue with Django's `sqlmigrate` management command
|
||||||
|
* [#18820](https://github.com/netbox-community/netbox/issues/18820) - PostgreSQL 13 is no longer supported
|
||||||
|
* [#19004](https://github.com/netbox-community/netbox/issues/19004) - The use of inventory items has been deprecated in favor of modules. Inventory items and roles may be removed in a future NetBox release.
|
||||||
|
|
||||||
|
### REST API Changes
|
||||||
|
|
||||||
|
* Added the following endpoints:
|
||||||
|
* `/api/extras/table-configs/`
|
||||||
|
* `/api/extras/tagged-objects/`
|
||||||
|
* `/api/dcim/module-type-profiles/`
|
||||||
|
* core.DataSource
|
||||||
|
* Added the optional `sync_interval` field
|
||||||
|
* dcim.DeviceRole
|
||||||
|
* Added the optional `parent` recursive foreign key field to effect hierarchical ordering
|
||||||
|
* Added a `comments` field
|
||||||
|
* dcim.Location
|
||||||
|
* Added a `comments` field
|
||||||
|
* dcim.ModuleType
|
||||||
|
* Added the optional `profile` foreign key to the new ModuleTypeProfile model
|
||||||
|
* dcim.PowerOutlet
|
||||||
|
* Added a `status` field
|
||||||
|
* dcim.Rack
|
||||||
|
* Added the optional `outer_height` field
|
||||||
|
* dcim.RackType
|
||||||
|
* Added the optional `outer_height` field
|
||||||
|
* dcim.Region
|
||||||
|
* Added a `comments` field
|
||||||
|
* dcim.SiteGroup
|
||||||
|
* Added a `comments` field
|
||||||
|
* extras.ConfigTemplate
|
||||||
|
* Added optional fields `mime_type`, `file_name`, `file_extension` and `as_attachment`
|
||||||
|
* extras.ExportTemplate
|
||||||
|
* Added optional fields `file_name` and `environment_params` (JSON)
|
||||||
|
* extras.Tag
|
||||||
|
* Added a `weight` field
|
||||||
|
* ipam.IPRange
|
||||||
|
* Added a `mark_populaed` boolean field
|
||||||
|
* ipam.L2VPN
|
||||||
|
* Added a `status` field
|
||||||
|
* ipam.Service
|
||||||
|
* Removed the `device` and `virtual_machine` foreign key fields
|
||||||
|
* Added the `parent_object_type`, `parent_object_id`, and (read-only) `parent` fields
|
||||||
|
* ipam.VLANGroup
|
||||||
|
* Added the optional `tenant` foreign key field
|
||||||
|
* tenancy.Contact
|
||||||
|
* Removed the `group` foreign key field
|
||||||
|
* Added the `groups` many-to-many field
|
||||||
|
* tenancy.ContactGroup
|
||||||
|
* Added a `comments` field
|
||||||
|
* tenancy.TenantGroup
|
||||||
|
* Added a `comments` field
|
||||||
|
* wireless.WirelessLANGroup
|
||||||
|
* Added a `comments` field
|
@ -49,6 +49,7 @@ markdown_extensions:
|
|||||||
- admonition
|
- admonition
|
||||||
- attr_list
|
- attr_list
|
||||||
- footnotes
|
- footnotes
|
||||||
|
- md_in_html
|
||||||
- pymdownx.emoji:
|
- pymdownx.emoji:
|
||||||
emoji_index: !!python/name:material.extensions.emoji.twemoji
|
emoji_index: !!python/name:material.extensions.emoji.twemoji
|
||||||
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
||||||
@ -147,7 +148,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:
|
||||||
@ -203,6 +203,7 @@ nav:
|
|||||||
- ModuleBay: 'models/dcim/modulebay.md'
|
- ModuleBay: 'models/dcim/modulebay.md'
|
||||||
- ModuleBayTemplate: 'models/dcim/modulebaytemplate.md'
|
- ModuleBayTemplate: 'models/dcim/modulebaytemplate.md'
|
||||||
- ModuleType: 'models/dcim/moduletype.md'
|
- ModuleType: 'models/dcim/moduletype.md'
|
||||||
|
- ModuleTypeProfile: 'models/dcim/moduletypeprofile.md'
|
||||||
- Platform: 'models/dcim/platform.md'
|
- Platform: 'models/dcim/platform.md'
|
||||||
- PowerFeed: 'models/dcim/powerfeed.md'
|
- PowerFeed: 'models/dcim/powerfeed.md'
|
||||||
- PowerOutlet: 'models/dcim/poweroutlet.md'
|
- PowerOutlet: 'models/dcim/poweroutlet.md'
|
||||||
@ -223,7 +224,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'
|
||||||
@ -236,8 +236,8 @@ 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'
|
||||||
|
- TableConfig: 'models/extras/tableconfig.md'
|
||||||
- Tag: 'models/extras/tag.md'
|
- Tag: 'models/extras/tag.md'
|
||||||
- Webhook: 'models/extras/webhook.md'
|
- Webhook: 'models/extras/webhook.md'
|
||||||
- IPAM:
|
- IPAM:
|
||||||
@ -309,6 +309,7 @@ nav:
|
|||||||
- git Cheat Sheet: 'development/git-cheat-sheet.md'
|
- git Cheat Sheet: 'development/git-cheat-sheet.md'
|
||||||
- Release Notes:
|
- Release Notes:
|
||||||
- Summary: 'release-notes/index.md'
|
- Summary: 'release-notes/index.md'
|
||||||
|
- Version 4.3: 'release-notes/version-4.3.md'
|
||||||
- Version 4.2: 'release-notes/version-4.2.md'
|
- Version 4.2: 'release-notes/version-4.2.md'
|
||||||
- Version 4.1: 'release-notes/version-4.1.md'
|
- Version 4.1: 'release-notes/version-4.1.md'
|
||||||
- Version 4.0: 'release-notes/version-4.0.md'
|
- Version 4.0: 'release-notes/version-4.0.md'
|
||||||
|
@ -8,7 +8,7 @@ class Migration(migrations.Migration):
|
|||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('users', '0004_netboxgroup_netboxuser'),
|
('users', '0002_squashed_0004'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
@ -91,10 +91,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):
|
||||||
|
16
netbox/circuits/graphql/enums.py
Normal file
16
netbox/circuits/graphql/enums.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import strawberry
|
||||||
|
|
||||||
|
from circuits.choices import *
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'CircuitStatusEnum',
|
||||||
|
'CircuitTerminationSideEnum',
|
||||||
|
'CircuitPriorityEnum',
|
||||||
|
'VirtualCircuitTerminationRoleEnum',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
CircuitPriorityEnum = strawberry.enum(CircuitPriorityChoices.as_enum(prefix='priority'))
|
||||||
|
CircuitStatusEnum = strawberry.enum(CircuitStatusChoices.as_enum('status'))
|
||||||
|
CircuitTerminationSideEnum = strawberry.enum(CircuitTerminationSideChoices.as_enum(prefix='side'))
|
||||||
|
VirtualCircuitTerminationRoleEnum = strawberry.enum(VirtualCircuitTerminationRoleChoices.as_enum(prefix='role'))
|
19
netbox/circuits/graphql/filter_mixins.py
Normal file
19
netbox/circuits/graphql/filter_mixins.py
Normal 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()
|
@ -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, LocationFilter, RegionFilter, SiteFilter, SiteGroupFilter
|
||||||
|
from ipam.graphql.filters import ASNFilter
|
||||||
|
from netbox.graphql.filter_lookups import IntegerLookup
|
||||||
|
from .enums import *
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'CircuitFilter',
|
'CircuitFilter',
|
||||||
@ -19,66 +42,183 @@ __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()
|
||||||
|
|
||||||
|
# Cached relations
|
||||||
|
_provider_network: Annotated['ProviderNetworkFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||||
|
strawberry_django.filter_field(name='provider_network')
|
||||||
|
)
|
||||||
|
_location: Annotated['LocationFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||||
|
strawberry_django.filter_field(name='location')
|
||||||
|
)
|
||||||
|
_region: Annotated['RegionFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||||
|
strawberry_django.filter_field(name='region')
|
||||||
|
)
|
||||||
|
_site_group: Annotated['SiteGroupFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||||
|
strawberry_django.filter_field(name='site_group')
|
||||||
|
)
|
||||||
|
_site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||||
|
strawberry_django.filter_field(name='site')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@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()
|
||||||
|
)
|
||||||
|
terminations: Annotated['CircuitTerminationFilter', strawberry.lazy('circuits.graphql.filters')] | 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()
|
||||||
|
circuits: Annotated['CircuitFilter', strawberry.lazy('circuits.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()
|
||||||
|
@ -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(ContactsMixin, NetBoxObjectType):
|
class ProviderAccountType(ContactsMixin, NetBoxObjectType):
|
||||||
provider: Annotated["ProviderType", strawberry.lazy('circuits.graphql.types')]
|
provider: Annotated["ProviderType", strawberry.lazy('circuits.graphql.types')]
|
||||||
@ -52,7 +58,8 @@ class ProviderAccountType(ContactsMixin, 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"])
|
||||||
|
@ -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 = [
|
||||||
|
@ -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 = [
|
||||||
|
@ -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 = [
|
||||||
|
@ -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='+',
|
||||||
|
@ -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'
|
||||||
|
@ -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
|
||||||
|
@ -6,7 +6,6 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from dcim.views import PathTraceView
|
from dcim.views import PathTraceView
|
||||||
from ipam.models import ASN
|
from ipam.models import ASN
|
||||||
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
|
||||||
@ -89,11 +88,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
|
||||||
#
|
#
|
||||||
@ -156,11 +150,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
|
||||||
#
|
#
|
||||||
@ -433,11 +422,6 @@ class CircuitSwapTerminations(generic.ObjectEditView):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Circuit, 'contacts')
|
|
||||||
class CircuitContactsView(ObjectContactsView):
|
|
||||||
queryset = Circuit.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Circuit terminations
|
# Circuit terminations
|
||||||
#
|
#
|
||||||
|
@ -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')
|
||||||
|
|
||||||
|
@ -22,6 +22,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
41
netbox/core/checks.py
Normal 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
|
@ -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
|
||||||
|
@ -1,2 +1,9 @@
|
|||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
||||||
|
|
||||||
class SyncError(Exception):
|
class SyncError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class IncompatiblePluginError(ImproperlyConfigured):
|
||||||
|
pass
|
||||||
|
@ -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
|
||||||
|
@ -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',
|
||||||
)
|
)
|
||||||
|
@ -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',
|
||||||
)
|
)
|
||||||
|
@ -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):
|
||||||
|
@ -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(
|
||||||
|
36
netbox/core/graphql/filter_mixins.py
Normal file
36
netbox/core/graphql/filter_mixins.py
Normal 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()
|
@ -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()
|
||||||
|
@ -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',
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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 = [
|
||||||
|
16
netbox/core/migrations/0014_datasource_sync_interval.py
Normal file
16
netbox/core/migrations/0014_datasource_sync_interval.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0013_job_data_encoder'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='datasource',
|
||||||
|
name='sync_interval',
|
||||||
|
field=models.PositiveSmallIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
23
netbox/core/migrations/0015_remove_redundant_indexes.py
Normal file
23
netbox/core/migrations/0015_remove_redundant_indexes.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0014_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',
|
||||||
|
),
|
||||||
|
]
|
@ -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')
|
||||||
|
@ -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):
|
||||||
|
storage = self.storage
|
||||||
|
if isinstance(storage, ScriptFileSystemStorage):
|
||||||
return {
|
return {
|
||||||
'scripts': settings.SCRIPTS_ROOT,
|
'scripts': settings.SCRIPTS_ROOT,
|
||||||
'reports': settings.REPORTS_ROOT,
|
'reports': settings.REPORTS_ROOT,
|
||||||
}[self.file_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
|
||||||
|
|
||||||
|
@ -9,7 +9,9 @@ from django.conf import settings
|
|||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
|
||||||
from netbox.plugins import PluginConfig
|
from netbox.plugins import PluginConfig
|
||||||
|
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 +66,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):
|
||||||
@ -80,6 +84,9 @@ def get_local_plugins(plugins=None):
|
|||||||
for plugin_name in settings.PLUGINS:
|
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
|
||||||
|
if plugin_config.release_track:
|
||||||
|
installed_version = f'{installed_version}-{plugin_config.release_track}'
|
||||||
|
|
||||||
if plugin_config.author:
|
if plugin_config.author:
|
||||||
author = PluginAuthor(
|
author = PluginAuthor(
|
||||||
@ -95,20 +102,29 @@ 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,
|
||||||
|
netbox_min_version=plugin_config.min_version,
|
||||||
|
netbox_max_version=plugin_config.max_version,
|
||||||
author=author,
|
author=author,
|
||||||
installed_version=plugin_config.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
|
||||||
|
|
||||||
|
|
||||||
@ -125,10 +141,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()
|
||||||
@ -140,7 +157,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()
|
||||||
|
@ -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',
|
||||||
@ -188,6 +187,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):
|
||||||
"""
|
"""
|
||||||
|
@ -14,10 +14,10 @@ __all__ = (
|
|||||||
class DataSourceTable(NetBoxTable):
|
class DataSourceTable(NetBoxTable):
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
verbose_name=_('Name'),
|
verbose_name=_('Name'),
|
||||||
linkify=True
|
linkify=True,
|
||||||
)
|
)
|
||||||
type = BackendTypeColumn(
|
type = BackendTypeColumn(
|
||||||
verbose_name=_('Type')
|
verbose_name=_('Type'),
|
||||||
)
|
)
|
||||||
status = columns.ChoiceFieldColumn(
|
status = columns.ChoiceFieldColumn(
|
||||||
verbose_name=_('Status'),
|
verbose_name=_('Status'),
|
||||||
@ -25,20 +25,26 @@ class DataSourceTable(NetBoxTable):
|
|||||||
enabled = columns.BooleanColumn(
|
enabled = columns.BooleanColumn(
|
||||||
verbose_name=_('Enabled'),
|
verbose_name=_('Enabled'),
|
||||||
)
|
)
|
||||||
tags = columns.TagColumn(
|
sync_interval = columns.ChoiceFieldColumn(
|
||||||
url_name='core:datasource_list'
|
verbose_name=_('Sync interval'),
|
||||||
|
)
|
||||||
|
last_synced = tables.DateTimeColumn(
|
||||||
|
verbose_name=_('Last Synced'),
|
||||||
)
|
)
|
||||||
file_count = tables.Column(
|
file_count = tables.Column(
|
||||||
verbose_name='Files'
|
verbose_name=_('Files'),
|
||||||
|
)
|
||||||
|
tags = columns.TagColumn(
|
||||||
|
url_name='core:datasource_list',
|
||||||
)
|
)
|
||||||
|
|
||||||
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', 'last_synced', '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):
|
||||||
|
@ -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',
|
||||||
@ -54,12 +57,15 @@ class CatalogPluginTable(BaseTable):
|
|||||||
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(
|
||||||
@ -88,3 +94,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>")
|
||||||
|
@ -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">—</span>
|
||||||
|
{% endif %}
|
||||||
|
"""
|
||||||
|
@ -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()
|
||||||
|
@ -22,6 +22,7 @@ from rq.worker_registration import clean_worker_registry
|
|||||||
|
|
||||||
from core.utils import delete_rq_job, enqueue_rq_job, get_rq_jobs_from_status, requeue_rq_job, stop_rq_job
|
from core.utils import delete_rq_job, enqueue_rq_job, get_rq_jobs_from_status, requeue_rq_job, stop_rq_job
|
||||||
from netbox.config import get_config, PARAMS
|
from netbox.config import get_config, PARAMS
|
||||||
|
from netbox.registry import registry
|
||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
from netbox.views.generic.base import BaseObjectView
|
from netbox.views.generic.base import BaseObjectView
|
||||||
from netbox.views.generic.mixins import TableMixin
|
from netbox.views.generic.mixins import TableMixin
|
||||||
@ -560,7 +561,7 @@ class SystemView(UserPassesTestMixin, View):
|
|||||||
params = [param.name for param in PARAMS]
|
params = [param.name for param in PARAMS]
|
||||||
data = {
|
data = {
|
||||||
**stats,
|
**stats,
|
||||||
'plugins': settings.PLUGINS,
|
'plugins': registry['plugins']['installed'],
|
||||||
'config': {
|
'config': {
|
||||||
k: getattr(config, k) for k in sorted(params)
|
k: getattr(config, k) for k in sorted(params)
|
||||||
},
|
},
|
||||||
@ -612,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)
|
||||||
|
|
||||||
|
@ -152,14 +152,15 @@ class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, Conne
|
|||||||
required=False,
|
required=False,
|
||||||
allow_null=True
|
allow_null=True
|
||||||
)
|
)
|
||||||
|
status = ChoiceField(choices=PowerOutletStatusChoices, required=False)
|
||||||
|
|
||||||
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')
|
||||||
|
|
||||||
|
@ -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')
|
||||||
|
@ -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:
|
||||||
|
@ -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')
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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')
|
||||||
|
@ -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)
|
||||||
|
@ -270,6 +270,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
|
||||||
|
@ -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')),
|
||||||
)
|
]
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -1635,6 +1638,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
|
||||||
#
|
#
|
||||||
|
@ -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
|
||||||
|
queryset = super().search(queryset, name, value)
|
||||||
|
|
||||||
|
if value.strip():
|
||||||
|
queryset = queryset | queryset.model.objects.filter(facility__icontains=value)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(
|
|
||||||
Q(name__icontains=value) |
|
|
||||||
Q(facility__icontains=value) |
|
|
||||||
Q(description__icontains=value)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
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)'),
|
||||||
)
|
)
|
||||||
@ -1664,11 +1717,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',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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')
|
||||||
|
@ -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):
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user