Merge pull request #15972 from netbox-community/feature
Prep for v4.0 release
|
@ -9,8 +9,8 @@ jobs:
|
|||
NETBOX_CONFIGURATION: netbox.configuration_testing
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.8', '3.9', '3.10', '3.11']
|
||||
node-version: ['14.x']
|
||||
python-version: ['3.10', '3.11', '3.12']
|
||||
node-version: ['18.x']
|
||||
services:
|
||||
redis:
|
||||
image: redis
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
version: 2
|
||||
build:
|
||||
os: ubuntu-20.04
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3.9"
|
||||
python: "3.12"
|
||||
mkdocs:
|
||||
configuration: mkdocs.yml
|
||||
python:
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
# HTML sanitizer
|
||||
# https://github.com/mozilla/bleach/blob/main/CHANGES
|
||||
bleach
|
||||
|
||||
# The Python web framework on which NetBox is built
|
||||
# https://docs.djangoproject.com/en/stable/releases/
|
||||
Django<5.0
|
||||
Django<5.1
|
||||
|
||||
# Django middleware which permits cross-domain API requests
|
||||
# https://github.com/adamchainz/django-cors-headers/blob/main/CHANGELOG.rst
|
||||
|
@ -18,14 +14,13 @@ django-debug-toolbar
|
|||
# https://github.com/carltongibson/django-filter/blob/main/CHANGES.rst
|
||||
django-filter
|
||||
|
||||
# Django debug toolbar extension with support for GraphiQL
|
||||
# https://github.com/flavors/django-graphiql-debug-toolbar/blob/main/CHANGES.rst
|
||||
django-graphiql-debug-toolbar
|
||||
# HTMX utilities for Django
|
||||
# https://django-htmx.readthedocs.io/en/latest/changelog.html
|
||||
django-htmx
|
||||
|
||||
# Modified Preorder Tree Traversal (recursive nesting of objects)
|
||||
# Pinned to 0.14.0; 0.15.0 requires Python 3.9+
|
||||
# https://github.com/django-mptt/django-mptt/blob/main/CHANGELOG.rst
|
||||
django-mptt==0.14.0
|
||||
django-mptt
|
||||
|
||||
# Context managers for PostgreSQL advisory locks
|
||||
# https://github.com/Xof/django-pglocks/blob/master/CHANGES.txt
|
||||
|
@ -61,8 +56,7 @@ django-timezone-field
|
|||
|
||||
# A REST API framework for Django projects
|
||||
# https://www.django-rest-framework.org/community/release-notes/
|
||||
# Pinned to 3.14 for NetBox v3.7
|
||||
djangorestframework<3.15
|
||||
djangorestframework
|
||||
|
||||
# Sane and flexible OpenAPI 3 schema generation for Django REST framework.
|
||||
# https://github.com/tfranzel/drf-spectacular/blob/master/CHANGELOG.rst
|
||||
|
@ -76,11 +70,6 @@ drf-spectacular-sidecar
|
|||
# https://github.com/kurtmckee/feedparser/blob/develop/CHANGELOG.rst
|
||||
feedparser
|
||||
|
||||
# Django wrapper for Graphene (GraphQL support)
|
||||
# https://github.com/graphql-python/graphene-django/releases
|
||||
# Pinned to v3.0.0 for GraphiQL UI issue (see #12762)
|
||||
graphene_django==3.0.0
|
||||
|
||||
# WSGI HTTP server
|
||||
# https://docs.gunicorn.org/en/latest/news.html
|
||||
gunicorn
|
||||
|
@ -109,6 +98,10 @@ mkdocstrings[python-legacy]
|
|||
# https://github.com/netaddr/netaddr/blob/master/CHANGELOG.rst
|
||||
netaddr
|
||||
|
||||
# Python bindings to the ammonia HTML sanitization library.
|
||||
# https://github.com/messense/nh3
|
||||
nh3
|
||||
|
||||
# Fork of PIL (Python Imaging Library) for image processing
|
||||
# https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst
|
||||
Pillow
|
||||
|
@ -133,8 +126,17 @@ social-auth-core
|
|||
# https://github.com/python-social-auth/social-app-django/blob/master/CHANGELOG.md
|
||||
social-auth-app-django
|
||||
|
||||
# Strawberry GraphQL
|
||||
# https://github.com/strawberry-graphql/strawberry/blob/main/CHANGELOG.md
|
||||
strawberry-graphql
|
||||
|
||||
# Strawberry GraphQL Django extension
|
||||
# https://github.com/strawberry-graphql/strawberry-django/blob/main/CHANGELOG.md
|
||||
# Pinned per #15574
|
||||
strawberry-graphql-django==0.34.0
|
||||
|
||||
# SVG image rendering (used for rack elevations)
|
||||
# hhttps://github.com/mozman/svgwrite/blob/master/NEWS.rst
|
||||
# https://github.com/mozman/svgwrite/blob/master/NEWS.rst
|
||||
svgwrite
|
||||
|
||||
# Tabular dataset library (for table-based exports)
|
||||
|
|
|
@ -12,8 +12,12 @@ Group=netbox
|
|||
PIDFile=/var/tmp/netbox.pid
|
||||
WorkingDirectory=/opt/netbox
|
||||
|
||||
# Remove the following line if using uWSGI instead of Gunicorn
|
||||
ExecStart=/opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/netbox --config /opt/netbox/gunicorn.py netbox.wsgi
|
||||
|
||||
# Uncomment the following line if using uWSGI instead of Gunicorn
|
||||
#ExecStart=/opt/netbox/venv/bin/uwsgi --ini /opt/netbox/uwsgi.ini
|
||||
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
PrivateTmp=true
|
||||
|
|
|
@ -14,10 +14,20 @@ server {
|
|||
}
|
||||
|
||||
location / {
|
||||
# Remove these lines if using uWSGI instead of Gunicorn
|
||||
proxy_pass http://127.0.0.1:8001;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Uncomment these lines if using uWSGI instead of Gunicorn
|
||||
# include uwsgi_params;
|
||||
# uwsgi_pass 127.0.0.1:8001;
|
||||
# uwsgi_param Host $host;
|
||||
# uwsgi_param X-Real-IP $remote_addr;
|
||||
# uwsgi_param X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# uwsgi_param X-Forwarded-Proto $http_x_forwarded_proto;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
[uwsgi]
|
||||
; bind to the specified UNIX/TCP socket and port (usually localhost)
|
||||
socket = 127.0.0.1:8001
|
||||
|
||||
; fail to start if any parameter in the configuration file isn’t explicitly understood by uWSGI.
|
||||
strict = true
|
||||
|
||||
; re-spawn and pre-fork workers
|
||||
master = true
|
||||
|
||||
; clear environment on exit
|
||||
vacuum = true
|
||||
|
||||
; exit if no app can be loaded
|
||||
need-app = true
|
||||
|
||||
; do not use multiple interpreters
|
||||
single-interpreter = true
|
|
@ -73,7 +73,7 @@ You should be redirected to Microsoft's authentication portal. Enter the usernam
|
|||
|
||||
If successful, you will be redirected back to the NetBox UI, and will be logged in as the AD user. You can verify this by navigating to your profile (using the button at top right).
|
||||
|
||||
This user account has been replicated locally to NetBox, and can now be assigned groups and permissions by navigating to Admin > Permissions.
|
||||
This user account has been replicated locally to NetBox, and can now be assigned groups and permissions.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
|
|
@ -67,4 +67,4 @@ You should be redirected to Okta's authentication portal. Enter the username/ema
|
|||
|
||||
If successful, you will be redirected back to the NetBox UI, and will be logged in as the Okta user. You can verify this by navigating to your profile (using the button at top right).
|
||||
|
||||
This user account has been replicated locally to NetBox, and can now be assigned groups and permissions by navigating to Admin > Permissions.
|
||||
This user account has been replicated locally to NetBox, and can now be assigned groups and permissions.
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
Local user accounts and groups can be created in NetBox under the "Authentication" section in the "Admin" menu. This section is available only to users with the "staff" permission enabled.
|
||||
|
||||
At a minimum, each user account must have a username and password set. User accounts may also denote a first name, last name, and email address. [Permissions](../permissions.md) may also be assigned to users and/or groups under Admin > Permissions.
|
||||
At a minimum, each user account must have a username and password set. User accounts may also denote a first name, last name, and email address. [Permissions](../permissions.md) may also be assigned to individual users and/or groups as needed.
|
||||
|
||||
## Remote Authentication
|
||||
|
||||
|
|
|
@ -70,8 +70,6 @@ The `$user` token can be used only as a constraint value, or as an item within a
|
|||
|
||||
### Default Permissions
|
||||
|
||||
!!! info "This feature was introduced in NetBox v3.6."
|
||||
|
||||
While permissions are typically assigned to specific groups and/or users, it is also possible to define a set of default permissions that are applied to _all_ authenticated users. This is done using the [`DEFAULT_PERMISSIONS`](../configuration/security.md#default_permissions) configuration parameter. Note that statically configuring permissions for specific users or groups is **not** supported.
|
||||
|
||||
### Example Constraint Definitions
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
# Date & Time Parameters
|
||||
|
||||
## TIME_ZONE
|
||||
|
||||
Default: UTC
|
||||
|
||||
The time zone NetBox will use when dealing with dates and times. It is recommended to use UTC time unless you have a specific need to use a local time zone. Please see the [list of available time zones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones).
|
||||
|
||||
## Date and Time Formatting
|
||||
|
||||
You may define custom formatting for date and times. For detailed instructions on writing format strings, please see [the Django documentation](https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date). Default formats are listed below.
|
||||
|
||||
!!! note
|
||||
These system defaults will be overridden by a user's selected language/locale when [localization](./system.md#enable_localization) is enabled.
|
||||
|
||||
```python
|
||||
DATE_FORMAT = 'N j, Y' # June 26, 2016
|
||||
SHORT_DATE_FORMAT = 'Y-m-d' # 2016-06-26
|
||||
TIME_FORMAT = 'g:i a' # 1:23 p.m.
|
||||
SHORT_TIME_FORMAT = 'H:i:s' # 13:23:00
|
||||
DATETIME_FORMAT = 'N j, Y g:i a' # June 26, 2016 1:23 p.m.
|
||||
SHORT_DATETIME_FORMAT = 'Y-m-d H:i' # 2016-06-26 13:23
|
||||
```
|
|
@ -33,9 +33,6 @@ This defines custom content to be displayed on the login page above the login fo
|
|||
|
||||
!!! tip "Dynamic Configuration Parameter"
|
||||
|
||||
!!! note
|
||||
This parameter was added in NetBox v3.5.
|
||||
|
||||
This adds a banner to the top of every page when maintenance mode is enabled. HTML is allowed.
|
||||
|
||||
---
|
||||
|
@ -99,6 +96,14 @@ The maximum size (in bytes) of an incoming HTTP request (i.e. `GET` or `POST` da
|
|||
|
||||
---
|
||||
|
||||
## DJANGO_ADMIN_ENABLED
|
||||
|
||||
Default: False
|
||||
|
||||
Setting this to True installs the `django.contrib.admin` app and enables the [Django admin UI](https://docs.djangoproject.com/en/5.0/ref/contrib/admin/). This may be necessary to support older plugins which do not integrate with the native NetBox interface.
|
||||
|
||||
---
|
||||
|
||||
## ENFORCE_GLOBAL_UNIQUE
|
||||
|
||||
!!! tip "Dynamic Configuration Parameter"
|
||||
|
@ -107,9 +112,6 @@ Default: True
|
|||
|
||||
By default, NetBox will prevent the creation of duplicate prefixes and IP addresses in the global table (that is, those which are not assigned to any VRF). This validation can be disabled by setting `ENFORCE_GLOBAL_UNIQUE` to False.
|
||||
|
||||
!!! info "Changed in v3.7"
|
||||
The default value for this parameter was changed from False to True in NetBox v3.7.
|
||||
|
||||
---
|
||||
|
||||
## FILE_UPLOAD_MAX_MEMORY_SIZE
|
||||
|
@ -134,9 +136,6 @@ Setting this to False will disable the GraphQL API.
|
|||
|
||||
!!! tip "Dynamic Configuration Parameter"
|
||||
|
||||
!!! note
|
||||
This parameter was renamed from `JOBRESULT_RETENTION` in NetBox v3.5.
|
||||
|
||||
Default: 90
|
||||
|
||||
The number of days to retain job results (scripts and reports). Set this to `0` to retain job results in the database indefinitely.
|
||||
|
@ -231,9 +230,6 @@ The maximum execution time of a background task (such as running a custom script
|
|||
|
||||
## RQ_RETRY_INTERVAL
|
||||
|
||||
!!! note
|
||||
This parameter was added in NetBox v3.5.
|
||||
|
||||
Default: `60`
|
||||
|
||||
This parameter controls how frequently a failed job is retried, up to the maximum number of times specified by `RQ_RETRY_MAX`. This must be either an integer specifying the number of seconds to wait between successive attempts, or a list of such values. For example, `[60, 300, 3600]` will retry the task after 1 minute, 5 minutes, and 1 hour.
|
||||
|
@ -242,9 +238,6 @@ This parameter controls how frequently a failed job is retried, up to the maximu
|
|||
|
||||
## RQ_RETRY_MAX
|
||||
|
||||
!!! note
|
||||
This parameter was added in NetBox v3.5.
|
||||
|
||||
Default: `0` (retries disabled)
|
||||
|
||||
The maximum number of times a background task will be retried before being marked as failed.
|
||||
|
|
|
@ -92,8 +92,6 @@ CSRF_TRUSTED_ORIGINS = (
|
|||
|
||||
## DEFAULT_PERMISSIONS
|
||||
|
||||
!!! info "This parameter was introduced in NetBox v3.6."
|
||||
|
||||
Default:
|
||||
|
||||
```python
|
||||
|
|
|
@ -62,14 +62,6 @@ Email is sent from NetBox only for critical events or if configured for [logging
|
|||
|
||||
---
|
||||
|
||||
## ENABLE_LOCALIZATION
|
||||
|
||||
Default: False
|
||||
|
||||
Determines if localization features are enabled or not. This should only be enabled for development or testing purposes as netbox is not yet fully localized. Turning this on will localize numeric and date formats (overriding any configured [system defaults](./date-time.md#date-and-time-formatting)) based on the browser locale as well as translate certain strings from third party modules.
|
||||
|
||||
---
|
||||
|
||||
## HTTP_PROXIES
|
||||
|
||||
Default: None
|
||||
|
@ -200,3 +192,9 @@ A dictionary of configuration parameters for the storage backend configured as `
|
|||
If `STORAGE_BACKEND` is not defined, this setting will be ignored.
|
||||
|
||||
---
|
||||
|
||||
## TIME_ZONE
|
||||
|
||||
Default: UTC
|
||||
|
||||
The time zone NetBox will use when dealing with dates and times. It is recommended to use UTC time unless you have a specific need to use a local time zone. Please see the [list of available time zones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones).
|
||||
|
|
|
@ -42,8 +42,6 @@ This parameter has no effect on the API representation of custom field data.
|
|||
|
||||
### Visibility & Editing
|
||||
|
||||
!!! info "This feature was improved in NetBox v3.7."
|
||||
|
||||
When creating a custom field, users can control the conditions under which it may be displayed and edited within the NetBox user interface. The following choices are available for controlling the display of a custom field on an object:
|
||||
|
||||
* **Always** (default): The custom field is included when viewing an object.
|
||||
|
|
|
@ -5,8 +5,17 @@ Custom scripting was introduced to provide a way for users to execute custom log
|
|||
* Automatically populate new devices and cables in preparation for a new site deployment
|
||||
* Create a range of new reserved prefixes or IP addresses
|
||||
* Fetch data from an external source and import it to NetBox
|
||||
* Update objects with invalid or incomplete data
|
||||
|
||||
Custom scripts are Python code and exist outside of the official NetBox code base, so they can be updated and changed without interfering with the core NetBox installation. And because they're completely custom, there is no inherent limitation on what a script can accomplish.
|
||||
They can also be used as a mechanism for validating the integrity of data within NetBox. Script authors can define test to check object against specific rules and conditions. For example, you can write script to check that:
|
||||
|
||||
* All top-of-rack switches have a console connection
|
||||
* Every router has a loopback interface with an IP address assigned
|
||||
* Each interface description conforms to a standard format
|
||||
* Every site has a minimum set of VLANs defined
|
||||
* All IP addresses have a parent prefix
|
||||
|
||||
Custom scripts are Python code which exists outside the NetBox code base, so they can be updated and changed without interfering with the core NetBox installation. And because they're completely custom, there is no inherent limitation on what a script can accomplish.
|
||||
|
||||
## Writing Custom Scripts
|
||||
|
||||
|
@ -135,13 +144,73 @@ These two methods will load data in YAML or JSON format, respectively, from file
|
|||
|
||||
The Script object provides a set of convenient functions for recording messages at different severity levels:
|
||||
|
||||
* `log_debug`
|
||||
* `log_success`
|
||||
* `log_info`
|
||||
* `log_warning`
|
||||
* `log_failure`
|
||||
* `log_debug(message, object=None)`
|
||||
* `log_success(message, object=None)`
|
||||
* `log_info(message, object=None)`
|
||||
* `log_warning(message, object=None)`
|
||||
* `log_failure(message, object=None)`
|
||||
|
||||
Log messages are returned to the user upon execution of the script. Markdown rendering is supported for log messages.
|
||||
Log messages are returned to the user upon execution of the script. Markdown rendering is supported for log messages. A message may optionally be associated with a particular object by passing it as the second argument to the logging method.
|
||||
|
||||
## Test Methods
|
||||
|
||||
A script can define one or more test methods to report on certain conditions. All test methods must have a name beginning with `test_` and accept no arguments beyond `self`.
|
||||
|
||||
These methods are detected and run automatically when the script is executed, unless its `run()` method has been overridden. (When overriding `run()`, `run_tests()` can be called to run all test methods present in the script.)
|
||||
|
||||
!!! info
|
||||
This functionality was ported from [legacy reports](./reports.md) in NetBox v4.0.
|
||||
|
||||
### Example
|
||||
|
||||
```
|
||||
from dcim.choices import DeviceStatusChoices
|
||||
from dcim.models import ConsolePort, Device, PowerPort
|
||||
from extras.scripts import Script
|
||||
|
||||
|
||||
class DeviceConnectionsReport(Script):
|
||||
description = "Validate the minimum physical connections for each device"
|
||||
|
||||
def test_console_connection(self):
|
||||
|
||||
# Check that every console port for every active device has a connection defined.
|
||||
active = DeviceStatusChoices.STATUS_ACTIVE
|
||||
for console_port in ConsolePort.objects.prefetch_related('device').filter(device__status=active):
|
||||
if not console_port.connected_endpoints:
|
||||
self.log_failure(
|
||||
f"No console connection defined for {console_port.name}",
|
||||
console_port.device,
|
||||
)
|
||||
elif not console_port.connection_status:
|
||||
self.log_warning(
|
||||
f"Console connection for {console_port.name} marked as planned",
|
||||
console_port.device,
|
||||
)
|
||||
else:
|
||||
self.log_success("Passed", console_port.device)
|
||||
|
||||
def test_power_connections(self):
|
||||
|
||||
# Check that every active device has at least two connected power supplies.
|
||||
for device in Device.objects.filter(status=DeviceStatusChoices.STATUS_ACTIVE):
|
||||
connected_ports = 0
|
||||
for power_port in PowerPort.objects.filter(device=device):
|
||||
if power_port.connected_endpoints:
|
||||
connected_ports += 1
|
||||
if not power_port.path.is_active:
|
||||
self.log_warning(
|
||||
f"Power connection for {power_port.name} marked as planned",
|
||||
device,
|
||||
)
|
||||
if connected_ports < 2:
|
||||
self.log_failure(
|
||||
f"{connected_ports} connected power supplies found (2 needed)",
|
||||
device,
|
||||
)
|
||||
else:
|
||||
self.log_success("Passed", device)
|
||||
```
|
||||
|
||||
## Change Logging
|
||||
|
||||
|
@ -235,6 +304,7 @@ A particular object within NetBox. Each ObjectVar must specify a particular mode
|
|||
|
||||
* `model` - The model class
|
||||
* `query_params` - A dictionary of query parameters to use when retrieving available options (optional)
|
||||
* `context` - A custom dictionary mapping template context variables to fields, used when rendering `<option>` elements within the dropdown menu (optional; see below)
|
||||
* `null_option` - A label representing a "null" or empty choice (optional)
|
||||
|
||||
To limit the selections available within the list, additional query parameters can be passed as the `query_params` dictionary. For example, to show only devices with an "active" status:
|
||||
|
@ -262,6 +332,22 @@ site = ObjectVar(
|
|||
)
|
||||
```
|
||||
|
||||
#### Context Variables
|
||||
|
||||
Custom context variables can be passed to override the default attribute names or to display additional information, such as a parent object.
|
||||
|
||||
| Name | Default | Description |
|
||||
|---------------|-----------------|------------------------------------------------------------------------------|
|
||||
| `value` | `"id"` | The attribute which contains the option's value |
|
||||
| `label` | `"display"` | The attribute used as the option's human-friendly label |
|
||||
| `description` | `"description"` | The attribute to use as a description |
|
||||
| `depth`[^1] | `"_depth"` | The attribute which indicates an object's depth within a recursive hierarchy |
|
||||
| `disabled` | -- | The attribute which, if true, signifies that the option should be disabled |
|
||||
| `parent` | -- | The attribute which represents the object's parent object |
|
||||
| `count`[^1] | -- | The attribute which contains a numeric count of related objects |
|
||||
|
||||
[^1]: The value of this attribute must be a positive integer
|
||||
|
||||
### MultiObjectVar
|
||||
|
||||
Similar to `ObjectVar`, but allows for the selection of multiple objects.
|
||||
|
@ -398,7 +484,7 @@ class NewBranchScript(Script):
|
|||
name=f'{site.slug}-switch{i}',
|
||||
site=site,
|
||||
status=DeviceStatusChoices.STATUS_PLANNED,
|
||||
device_role=switch_role
|
||||
role=switch_role
|
||||
)
|
||||
switch.full_clean()
|
||||
switch.save()
|
||||
|
|
|
@ -4,7 +4,7 @@ NetBox validates every object prior to it being written to the database to ensur
|
|||
|
||||
## Custom Validation Rules
|
||||
|
||||
Custom validation rules are expressed as a mapping of model attributes to a set of rules to which that attribute must conform. For example:
|
||||
Custom validation rules are expressed as a mapping of object attributes to a set of rules to which that attribute must conform. For example:
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -17,6 +17,8 @@ Custom validation rules are expressed as a mapping of model attributes to a set
|
|||
|
||||
This defines a custom validator which checks that the length of the `name` attribute for an object is at least five characters long, and no longer than 30 characters. This validation is executed _after_ NetBox has performed its own internal validation.
|
||||
|
||||
### Validation Types
|
||||
|
||||
The `CustomValidator` class supports several validation types:
|
||||
|
||||
* `min`: Minimum value
|
||||
|
@ -36,14 +38,14 @@ The `min` and `max` types should be defined for numeric values, whereas `min_len
|
|||
|
||||
### Custom Validation Logic
|
||||
|
||||
There may be instances where the provided validation types are insufficient. NetBox provides a `CustomValidator` class which can be extended to enforce arbitrary validation logic by overriding its `validate()` method, and calling `fail()` when an unsatisfactory condition is detected.
|
||||
There may be instances where the provided validation types are insufficient. NetBox provides a `CustomValidator` class which can be extended to enforce arbitrary validation logic by overriding its `validate()` method, and calling `fail()` when an unsatisfactory condition is detected. The `validate()` method should accept an instance (the object being saved) as well as the current request effecting the change.
|
||||
|
||||
```python
|
||||
from extras.validators import CustomValidator
|
||||
|
||||
class MyValidator(CustomValidator):
|
||||
|
||||
def validate(self, instance):
|
||||
def validate(self, instance, request):
|
||||
if instance.status == 'active' and not instance.description:
|
||||
self.fail("Active sites must have a description set!", field='status')
|
||||
```
|
||||
|
@ -82,7 +84,42 @@ CUSTOM_VALIDATORS = {
|
|||
}
|
||||
```
|
||||
|
||||
### Dotted Path
|
||||
#### Referencing Related Object Attributes
|
||||
|
||||
!!! info "This feature was introduced in NetBox v4.0."
|
||||
|
||||
The attributes of a related object can be referenced by specifying a dotted path. For example, to reference the name of a region to which a site is assigned, use `region.name`:
|
||||
|
||||
```python
|
||||
CUSTOM_VALIDATORS = {
|
||||
"dcim.site": [
|
||||
{
|
||||
"region.name": {
|
||||
"neq": "New York"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Validating Request Parameters
|
||||
|
||||
!!! info "This feature was introduced in NetBox v4.0."
|
||||
|
||||
In addition to validating object attributes, custom validators can also match against parameters of the current request (where available). For example, the following rule will permit only the user named "admin" to modify an object:
|
||||
|
||||
```json
|
||||
{
|
||||
"request.user.username": {
|
||||
"eq": "admin"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
Custom validation should generally not be used to enforce permissions. NetBox provides a robust [object-based permissions](../administration/permissions.md) mechanism which should be used for this purpose.
|
||||
|
||||
### Dotted Path to Class
|
||||
|
||||
In instances where a custom validator class is needed, it can be referenced by its Python path (relative to NetBox's working directory):
|
||||
|
||||
|
|
|
@ -1,167 +1,63 @@
|
|||
# NetBox Reports
|
||||
|
||||
A NetBox report is a mechanism for validating the integrity of data within NetBox. Running a report allows the user to verify that the objects defined within NetBox meet certain arbitrary conditions. For example, you can write reports to check that:
|
||||
|
||||
* All top-of-rack switches have a console connection
|
||||
* Every router has a loopback interface with an IP address assigned
|
||||
* Each interface description conforms to a standard format
|
||||
* Every site has a minimum set of VLANs defined
|
||||
* All IP addresses have a parent prefix
|
||||
|
||||
...and so on. Reports are completely customizable, so there's practically no limit to what you can test for.
|
||||
|
||||
## Writing Reports
|
||||
|
||||
Reports must be saved as files in the [`REPORTS_ROOT`](../configuration/system.md#reports_root) path (which defaults to `netbox/reports/`). Each file created within this path is considered a separate module. Each module holds one or more reports (Python classes), each of which performs a certain function. The logic of each report is broken into discrete test methods, each of which applies a small portion of the logic comprising the overall test.
|
||||
|
||||
!!! warning
|
||||
The reports path includes a file named `__init__.py`, which registers the path as a Python module. Do not delete this file.
|
||||
Reports are deprecated beginning with NetBox v4.0, and their functionality has been merged with [custom scripts](./custom-scripts.md). While backward compatibility has been maintained, users are advised to convert legacy reports into custom scripts soon, as support for legacy reports will be removed in a future release.
|
||||
|
||||
For example, we can create a module named `devices.py` to hold all of our reports which pertain to devices in NetBox. Within that module, we might define several reports. Each report is defined as a Python class inheriting from `extras.reports.Report`.
|
||||
## Converting Reports to Scripts
|
||||
|
||||
```
|
||||
### Step 1: Update Class Definition
|
||||
|
||||
Change the parent class from `Report` to `Script`:
|
||||
|
||||
```python title="Old code"
|
||||
from extras.reports import Report
|
||||
|
||||
class DeviceConnectionsReport(Report):
|
||||
description = "Validate the minimum physical connections for each device"
|
||||
|
||||
class DeviceIPsReport(Report):
|
||||
description = "Check that every device has a primary IP address assigned"
|
||||
class MyReport(Report):
|
||||
```
|
||||
|
||||
Within each report class, we'll create a number of test methods to execute our report's logic. In DeviceConnectionsReport, for instance, we want to ensure that every live device has a console connection, an out-of-band management connection, and two power connections.
|
||||
```python title="New code"
|
||||
from extras.scripts import Script
|
||||
|
||||
```
|
||||
from dcim.choices import DeviceStatusChoices
|
||||
from dcim.models import ConsolePort, Device, PowerPort
|
||||
from extras.reports import Report
|
||||
|
||||
|
||||
class DeviceConnectionsReport(Report):
|
||||
description = "Validate the minimum physical connections for each device"
|
||||
|
||||
def test_console_connection(self):
|
||||
|
||||
# Check that every console port for every active device has a connection defined.
|
||||
active = DeviceStatusChoices.STATUS_ACTIVE
|
||||
for console_port in ConsolePort.objects.prefetch_related('device').filter(device__status=active):
|
||||
if not console_port.connected_endpoints:
|
||||
self.log_failure(
|
||||
console_port.device,
|
||||
"No console connection defined for {}".format(console_port.name)
|
||||
)
|
||||
elif not console_port.connection_status:
|
||||
self.log_warning(
|
||||
console_port.device,
|
||||
"Console connection for {} marked as planned".format(console_port.name)
|
||||
)
|
||||
else:
|
||||
self.log_success(console_port.device)
|
||||
|
||||
def test_power_connections(self):
|
||||
|
||||
# Check that every active device has at least two connected power supplies.
|
||||
for device in Device.objects.filter(status=DeviceStatusChoices.STATUS_ACTIVE):
|
||||
connected_ports = 0
|
||||
for power_port in PowerPort.objects.filter(device=device):
|
||||
if power_port.connected_endpoints:
|
||||
connected_ports += 1
|
||||
if not power_port.path.is_active:
|
||||
self.log_warning(
|
||||
device,
|
||||
"Power connection for {} marked as planned".format(power_port.name)
|
||||
)
|
||||
if connected_ports < 2:
|
||||
self.log_failure(
|
||||
device,
|
||||
"{} connected power supplies found (2 needed)".format(connected_ports)
|
||||
)
|
||||
else:
|
||||
self.log_success(device)
|
||||
class MyReport(Script):
|
||||
```
|
||||
|
||||
As you can see, reports are completely customizable. Validation logic can be as simple or as complex as needed. Also note that the `description` attribute support markdown syntax. It will be rendered in the report list page.
|
||||
### Step 2: Update Logging Calls
|
||||
|
||||
!!! warning
|
||||
Reports should never alter data: If you find yourself using the `create()`, `save()`, `update()`, or `delete()` methods on objects within reports, stop and re-evaluate what you're trying to accomplish. Note that there are no safeguards against the accidental alteration or destruction of data.
|
||||
Reports and scripts both provide logging methods, however their signatures differ. All script logging methods accept a message as the first parameter, and accept an object as an optional second parameter.
|
||||
|
||||
## Report Attributes
|
||||
Additionally, the Report class' generic `log()` method is **not** available on Script. Users are advised to replace calls of this method with `log_info()`.
|
||||
|
||||
### `description`
|
||||
Use the table below as a reference when updating these methods.
|
||||
|
||||
A human-friendly description of what your report does.
|
||||
| Report (old) | Script (New) |
|
||||
|-------------------------------|-----------------------------|
|
||||
| `log(message)` | `log_info(message)` |
|
||||
| `log_debug(obj, message)`[^1] | `log_debug(message, obj)` |
|
||||
| `log_info(obj, message)` | `log_info(message, obj)` |
|
||||
| `log_success(obj, message)` | `log_success(message, obj)` |
|
||||
| `log_warning(obj, message)` | `log_warning(message, obj)` |
|
||||
| `log_failure(obj, message)` | `log_failure(message, obj)` |
|
||||
|
||||
### `scheduling_enabled`
|
||||
[^1]: `log_debug()` was added to the Report class in v4.0 to avoid confusion with the same method on Script
|
||||
|
||||
By default, a report can be scheduled for execution at a later time. Setting `scheduling_enabled` to False disables this ability: Only immediate execution will be possible. (This also disables the ability to set a recurring execution interval.)
|
||||
|
||||
### `job_timeout`
|
||||
|
||||
Set the maximum allowed runtime for the report. If not set, `RQ_DEFAULT_TIMEOUT` will be used.
|
||||
|
||||
## Logging
|
||||
|
||||
The following methods are available to log results within a report:
|
||||
|
||||
* log(message)
|
||||
* log_success(object, message=None)
|
||||
* log_info(object, message)
|
||||
* log_warning(object, message)
|
||||
* log_failure(object, message)
|
||||
|
||||
The recording of one or more failure messages will automatically flag a report as failed. It is advised to log a success for each object that is evaluated so that the results will reflect how many objects are being reported on. (The inclusion of a log message is optional for successes.) Messages recorded with `log()` will appear in a report's results but are not associated with a particular object or status. Log messages also support using markdown syntax and will be rendered on the report result page.
|
||||
|
||||
To perform additional tasks, such as sending an email or calling a webhook, before or after a report is run, extend the `pre_run()` and/or `post_run()` methods, respectively.
|
||||
|
||||
By default, reports within a module are ordered alphabetically in the reports list page. To return reports in a specific order, you can define the `report_order` variable at the end of your module. The `report_order` variable is a tuple which contains each Report class in the desired order. Any reports that are omitted from this list will be listed last.
|
||||
|
||||
```
|
||||
from extras.reports import Report
|
||||
|
||||
class DeviceConnectionsReport(Report)
|
||||
pass
|
||||
|
||||
class DeviceIPsReport(Report)
|
||||
pass
|
||||
|
||||
report_order = (DeviceIPsReport, DeviceConnectionsReport)
|
||||
```python title="Old code"
|
||||
self.log_failure(
|
||||
console_port.device,
|
||||
f"No console connection defined for {console_port.name}"
|
||||
)
|
||||
```
|
||||
|
||||
Once you have created a report, it will appear in the reports list. Initially, reports will have no results associated with them. To generate results, run the report.
|
||||
|
||||
## Running Reports
|
||||
|
||||
!!! note
|
||||
To run a report, a user must be assigned permissions for `Extras > Report`, `Extras > Report Module`, and `Core > Managed File` objects. They must also be assigned the `extras.run_report` permission. This is achieved by assigning the user (or group) a permission on the Report object and specifying the `run` action in "Permissions" as shown below.
|
||||
|
||||
![Adding the run action to a permission](../media/run_permission.png)
|
||||
|
||||
### Via the Web UI
|
||||
|
||||
Reports can be run via the web UI by navigating to the report and clicking the "run report" button at top right. Once a report has been run, its associated results will be included in the report view. It is possible to schedule a report to be executed at specified time in the future. A scheduled report can be canceled by deleting the associated job result object.
|
||||
|
||||
### Via the API
|
||||
|
||||
To run a report via the API, simply issue a POST request to its `run` endpoint. Reports are identified by their module and class name.
|
||||
|
||||
```
|
||||
POST /api/extras/reports/<module>.<name>/run/
|
||||
```python title="New code"
|
||||
self.log_failure(
|
||||
f"No console connection defined for {console_port.name}",
|
||||
obj=console_port.device,
|
||||
)
|
||||
```
|
||||
|
||||
Our example report above would be called as:
|
||||
### Other Notes
|
||||
|
||||
```
|
||||
POST /api/extras/reports/devices.DeviceConnectionsReport/run/
|
||||
```
|
||||
Existing reports will be converted to scripts automatically upon upgrading to NetBox v4.0, and previous job history will be retained. However, users are advised to convert legacy reports into custom scripts at the earliest opportunity, as support for legacy reports will be removed in a future release.
|
||||
|
||||
Optionally `schedule_at` can be passed in the form data with a datetime string to schedule a script at the specified date and time.
|
||||
The `pre_run()` and `post_run()` Report methods have been carried over to Script. These are called automatically by Script's `run()` method. (Note that if you opt to override this method, you are responsible for calling `pre_run()` and `post_run()` where applicable.)
|
||||
|
||||
### Via the CLI
|
||||
|
||||
Reports can be run on the CLI by invoking the management command:
|
||||
|
||||
```
|
||||
python3 manage.py runreport <module>
|
||||
```
|
||||
|
||||
where ``<module>`` is the name of the python file in the ``reports`` directory without the ``.py`` extension. One or more report modules may be specified.
|
||||
The `is_valid()` method on Report is no longer needed and has been removed.
|
||||
|
|
|
@ -7,7 +7,7 @@ Getting started with NetBox development is pretty straightforward, and should fe
|
|||
* A Linux system or compatible environment
|
||||
* A PostgreSQL server, which can be installed locally [per the documentation](../installation/1-postgresql.md)
|
||||
* A Redis server, which can also be [installed locally](../installation/2-redis.md)
|
||||
* Python 3.8 or later
|
||||
* Python 3.10 or later
|
||||
|
||||
### 1. Fork the Repo
|
||||
|
||||
|
|
|
@ -62,10 +62,11 @@ class Circuit(PrimaryModel):
|
|||
|
||||
1. Import `gettext_lazy` as `_`.
|
||||
2. All form fields must specify a `label` wrapped with `gettext_lazy()`.
|
||||
3. All headers under a form's `fieldsets` property must be wrapped with `gettext_lazy()`.
|
||||
3. The name of each FieldSet on a form must be wrapped with `gettext_lazy()`.
|
||||
|
||||
```python
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from utilities.forms.rendering import FieldSet
|
||||
|
||||
class CircuitBulkEditForm(NetBoxModelBulkEditForm):
|
||||
description = forms.CharField(
|
||||
|
@ -74,7 +75,7 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
|
|||
)
|
||||
|
||||
fieldsets = (
|
||||
(_('Circuit'), ('provider', 'type', 'status', 'description')),
|
||||
FieldSet('provider', 'type', 'status', 'description', name=_('Circuit')),
|
||||
)
|
||||
```
|
||||
|
||||
|
|
|
@ -59,7 +59,7 @@ Notify the [`netbox-docker`](https://github.com/netbox-community/netbox-docker)
|
|||
* Increases in minimum versions for service dependencies (PostgreSQL, Redis, etc.)
|
||||
* Any changes to the reference installation
|
||||
|
||||
### Update Requirements
|
||||
### Update Python Dependencies
|
||||
|
||||
Before each release, update each of NetBox's Python dependencies to its most recent stable version. These are defined in `requirements.txt`, which is updated from `base_requirements.txt` using `pip`. To do this:
|
||||
|
||||
|
@ -70,6 +70,10 @@ Before each release, update each of NetBox's Python dependencies to its most rec
|
|||
|
||||
In cases where upgrading a dependency to its most recent release is breaking, it should be constrained to its current minor version in `base_requirements.txt` with an explanatory comment and revisited for the next major NetBox release (see the [Address Constrained Dependencies](#address-constrained-dependencies) section above).
|
||||
|
||||
### Update UI Dependencies
|
||||
|
||||
Check whether any UI dependencies (JavaScript packages, fonts, etc.) need to be updated by running `yarn outdated` from within the `project-static/` directory. [Upgrade these dependencies](http://0.0.0.0:9000/development/web-ui/#updating-dependencies) as necessary, then run `yarn bundle` to generate the necessary files for distribution.
|
||||
|
||||
### Rebuild the Device Type Definition Schema
|
||||
|
||||
Run the following command to update the device type definition validation schema:
|
||||
|
|
|
@ -11,4 +11,3 @@ The `users.UserConfig` model holds individual preferences for each user in the f
|
|||
| pagination.placement | Where to display the paginator controls relative to the table |
|
||||
| tables.${table}.columns | The ordered list of columns to display when viewing the table |
|
||||
| tables.${table}.ordering | A list of column names by which the table should be ordered |
|
||||
| ui.colormode | Light or dark mode in the user interface |
|
||||
|
|
|
@ -1,25 +1,37 @@
|
|||
# Web UI Development
|
||||
|
||||
## Code Structure
|
||||
|
||||
Most static resources for the NetBox UI are housed within the `netbox/project-static/` directory.
|
||||
|
||||
| Path | Description |
|
||||
|-----------|----------------------------------------------------|
|
||||
| `dist/` | Destination path for installed dependencies |
|
||||
| `docs/` | Local build path for documentation |
|
||||
| `img/` | Image files |
|
||||
| `js/` | Miscellaneous JavaScript resources served directly |
|
||||
| `src/` | TypeScript resources (to be compiled into JS) |
|
||||
| `styles/` | Sass resources (to be compiled into CSS) |
|
||||
|
||||
## Front End Technologies
|
||||
|
||||
The NetBox UI is built on languages and frameworks:
|
||||
Front end scripting is written in [TypeScript](https://www.typescriptlang.org/), which is a strongly-typed extension to JavaScript. TypeScript is "transpiled" into JavaScript resources which are served to and executed by the client web browser.
|
||||
|
||||
### Styling & HTML Elements
|
||||
All UI styling is written in [Sass](https://sass-lang.com/), which is an extension to browser-native [Cascading Stylesheets (CSS)](https://developer.mozilla.org/en-US/docs/Web/CSS). Similar to how TypeScript content is transpiled to JavaScript, Sass resources (`.scss` files) are compiled to CSS.
|
||||
|
||||
#### [Bootstrap](https://getbootstrap.com/) 5
|
||||
## Dependencies
|
||||
|
||||
The majority of the NetBox UI is made up of stock Bootstrap components, with some styling modifications and custom components added on an as-needed basis. Bootstrap uses [Sass](https://sass-lang.com/), and NetBox extends Bootstrap's core Sass files for theming and customization.
|
||||
The following software is employed by the NetBox user interface.
|
||||
|
||||
### Client-side Scripting
|
||||
|
||||
#### [TypeScript](https://www.typescriptlang.org/)
|
||||
|
||||
All client-side scripting is transpiled from TypeScript to JavaScript and served by Django. In development, TypeScript is an _extremely_ effective tool for accurately describing and checking the code, which leads to significantly fewer bugs, a better development experience, and more predictable/readable code.
|
||||
|
||||
As part of the [bundling](#bundling) process, Bootstrap's JavaScript plugins are imported and bundled alongside NetBox's front-end code.
|
||||
|
||||
!!! danger "NetBox is jQuery-free"
|
||||
Following the Bootstrap team's deprecation of jQuery in Bootstrap 5, NetBox also no longer uses jQuery in front-end code.
|
||||
* [Bootstrap 5](https://getbootstrap.com/) - A popular CSS & JS framework
|
||||
* [clipboard.js](https://clipboardjs.com/) - A lightweight package for enabling copy-to-clipboard functionality
|
||||
* [flatpickr](https://flatpickr.js.org/) - A lightweight date & time selection widget
|
||||
* [gridstack.js](https://gridstackjs.com/) - Enables interactive grid layouts (for the dashboard)
|
||||
* [HTMX](https://htmx.org/) - Enables dynamic web interfaces through the use of HTML element attributes
|
||||
* [Material Design Icons](https://pictogrammers.com/library/mdi/) - An extensive open source collection of graphical icons, delivered as a web font
|
||||
* [query-string](https://www.npmjs.com/package/query-string) - Assists with parsing URL query strings
|
||||
* [Tabler](https://tabler.io/) - A web application UI toolkit & theme based on Bootstrap 5
|
||||
* [Tom Select](https://tom-select.js.org/) - Provides dynamic selection form fields
|
||||
|
||||
## Guidance
|
||||
|
||||
|
@ -54,6 +66,41 @@ $ yarn
|
|||
!!! warning "Check Your Working Directory"
|
||||
You need to be in the `netbox/project-static` directory to run the below `yarn` commands.
|
||||
|
||||
### Updating Dependencies
|
||||
|
||||
Run `yarn outdated` to identify outdated dependencies.
|
||||
|
||||
```
|
||||
$ yarn outdated
|
||||
yarn outdated v1.22.19
|
||||
info Color legend :
|
||||
"<red>" : Major Update backward-incompatible updates
|
||||
"<yellow>" : Minor Update backward-compatible features
|
||||
"<green>" : Patch Update backward-compatible bug fixes
|
||||
Package Current Wanted Latest Workspace Package Type URL
|
||||
bootstrap 5.3.1 5.3.1 5.3.3 netbox dependencies https://getbootstrap.com/
|
||||
```
|
||||
|
||||
Run `yarn upgrade --latest` to automatically upgrade these packages to their most recent versions.
|
||||
|
||||
```
|
||||
$ yarn upgrade bootstrap --latest
|
||||
yarn upgrade v1.22.19
|
||||
[1/4] Resolving packages...
|
||||
[2/4] Fetching packages...
|
||||
[3/4] Linking dependencies...
|
||||
[4/4] Rebuilding all packages...
|
||||
success Saved lockfile.
|
||||
success Saved 1 new dependency.
|
||||
info Direct dependencies
|
||||
└─ bootstrap@5.3.3
|
||||
info All dependencies
|
||||
└─ bootstrap@5.3.3
|
||||
Done in 0.95s.
|
||||
```
|
||||
|
||||
`package.json` will be updated to reflect the new package versions automatically.
|
||||
|
||||
### Bundling
|
||||
|
||||
In order for the TypeScript and Sass (SCSS) source files to be usable by a browser, they must first be transpiled (TypeScript → JavaScript, Sass → CSS), bundled, and minified. After making changes to TypeScript or Sass source files, run `yarn bundle`.
|
||||
|
|
|
@ -20,8 +20,6 @@ GET /api/dcim/devices/?tag=monitored&tag=deprecated
|
|||
|
||||
## Bookmarks
|
||||
|
||||
!!! info "This feature was introduced in NetBox v3.6."
|
||||
|
||||
Users can bookmark their most commonly visited objects for convenient access. Bookmarks are listed under a user's profile, and can be displayed with custom filtering and ordering on the user's personal dashboard.
|
||||
|
||||
## Custom Fields
|
||||
|
|
|
@ -28,4 +28,4 @@ For more detail, see the reference documentation for NetBox's [conditional logic
|
|||
|
||||
## Event Rule Processing
|
||||
|
||||
When a change is detected, any resulting events are placed into a Redis queue for processing. This allows the user's request to complete without needing to wait for the outgoing event(s) to be processed. The events are then extracted from the queue by the `rqworker` process. The current event queue and any failed events can be inspected in the admin UI under System > Background Tasks.
|
||||
When a change is detected, any resulting events are placed into a Redis queue for processing. This allows the user's request to complete without needing to wait for the outgoing event(s) to be processed. The events are then extracted from the queue by the `rqworker` process. The current event queue and any failed events can be inspected under System > Background Tasks.
|
||||
|
|
|
@ -6,8 +6,8 @@ This section of the documentation discusses installing and configuring the NetBo
|
|||
|
||||
Begin by installing all system packages required by NetBox and its dependencies.
|
||||
|
||||
!!! warning "Python 3.8 or later required"
|
||||
NetBox requires Python 3.8, 3.9, 3.10 or 3.11.
|
||||
!!! warning "Python 3.10 or later required"
|
||||
NetBox supports Python 3.10, 3.11, and 3.12.
|
||||
|
||||
=== "Ubuntu"
|
||||
|
||||
|
@ -21,7 +21,7 @@ Begin by installing all system packages required by NetBox and its dependencies.
|
|||
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.8:
|
||||
Before continuing, check that your installed Python version is at least 3.10:
|
||||
|
||||
```no-highlight
|
||||
python3 -V
|
||||
|
@ -255,10 +255,10 @@ Once NetBox has been configured, we're ready to proceed with the actual installa
|
|||
sudo /opt/netbox/upgrade.sh
|
||||
```
|
||||
|
||||
Note that **Python 3.8 or later is required** for NetBox v3.2 and later releases. If the default Python installation on your server is set to a lesser version, pass the path to the supported installation as an environment variable named `PYTHON`. (Note that the environment variable must be passed _after_ the `sudo` command.)
|
||||
Note that **Python 3.10 or later is required** for NetBox v4.0 and later releases. If the default Python installation on your server is set to a lesser version, pass the path to the supported installation as an environment variable named `PYTHON`. (Note that the environment variable must be passed _after_ the `sudo` command.)
|
||||
|
||||
```no-highlight
|
||||
sudo PYTHON=/usr/bin/python3.8 /opt/netbox/upgrade.sh
|
||||
sudo PYTHON=/usr/bin/python3.10 /opt/netbox/upgrade.sh
|
||||
```
|
||||
|
||||
!!! note
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
# Gunicorn
|
||||
|
||||
Like most Django applications, NetBox runs as a [WSGI application](https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface) behind an HTTP server. This documentation shows how to install and configure [gunicorn](http://gunicorn.org/) (which is automatically installed with NetBox) for this role, however other WSGI servers are available and should work similarly well. [uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/) is a popular alternative.
|
||||
!!! tip
|
||||
This page provides instructions for setting up the [gunicorn](http://gunicorn.org/) WSGI server. If you plan to use [uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/) instead, go [here](./4b-uwsgi.md).
|
||||
|
||||
NetBox runs as a [WSGI application](https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface) behind an HTTP server. This documentation shows how to install and configure [gunicorn](http://gunicorn.org/) (which is automatically installed with NetBox) for this role, however other WSGI servers are available and should work similarly well.
|
||||
|
||||
## Configuration
|
||||
|
||||
NetBox ships with a default configuration file for gunicorn. To use it, copy `/opt/netbox/contrib/gunicorn.py` to `/opt/netbox/gunicorn.py`. (We make a copy of this file rather than pointing to it directly to ensure that any local changes to it do not get overwritten by a future upgrade.)
|
||||
NetBox ships with a default configuration file for gunicorn. To use it, copy `/opt/netbox/contrib/gunicorn.py` to `/opt/netbox/gunicorn.py`. (We make a copy of this file rather than pointing to it directly to ensure that any local changes to it do not get overwritten during a future NetBox upgrade.)
|
||||
|
||||
```no-highlight
|
||||
sudo cp /opt/netbox/contrib/gunicorn.py /opt/netbox/gunicorn.py
|
|
@ -0,0 +1,104 @@
|
|||
# uWSGI
|
||||
|
||||
!!! tip
|
||||
This page provides instructions for setting up the [uWSGI](https://uwsgi-docs.readthedocs.io/) WSGI server. If you plan to use [gunicorn](http://gunicorn.org/) instead, go [here](./4a-gunicorn.md).
|
||||
|
||||
NetBox runs as a [WSGI application](https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface) behind an HTTP server. This documentation shows how to install and configure [uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/) for this role, however other WSGI servers are available and should work similarly well.
|
||||
|
||||
## Installation
|
||||
|
||||
Activate the Python virtual environment and install the `pyuwsgi` package using pip:
|
||||
|
||||
```no-highlight
|
||||
source /opt/netbox/venv/bin/activate
|
||||
pip3 install pyuwsgi
|
||||
```
|
||||
|
||||
Once installed, add the package to `local_requirements.txt` to ensure it is re-installed during future rebuilds of the virtual environment:
|
||||
|
||||
```no-highlight
|
||||
sudo sh -c "echo 'pyuwgsi' >> /opt/netbox/local_requirements.txt"
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
NetBox ships with a default configuration file for uWSGI. To use it, copy `/opt/netbox/contrib/uwsgi.ini` to `/opt/netbox/uwsgi.ini`. (We make a copy of this file rather than pointing to it directly to ensure that any local changes to it do not get overwritten during a future NetBox upgrade.)
|
||||
|
||||
```no-highlight
|
||||
sudo cp /opt/netbox/contrib/uwsgi.ini /opt/netbox/uwsgi.ini
|
||||
```
|
||||
|
||||
While the provided configuration should suffice for most initial installations, you may wish to edit this file to change the bound IP address and/or port number, or to make performance-related adjustments. See [the uWSGI documentation](https://uwsgi-docs-additions.readthedocs.io/en/latest/Options.html) for the available configuration parameters and take a minute to review the [Things to know](https://uwsgi-docs.readthedocs.io/en/latest/ThingsToKnow.html) page. Django also provides [additional documentation](https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/uwsgi/) on configuring uWSGI with a Django app.
|
||||
|
||||
## systemd Setup
|
||||
|
||||
We'll use systemd to control both uWSGI and NetBox's background worker process. First, copy `contrib/netbox.service` and `contrib/netbox-rq.service` to the `/etc/systemd/system/` directory.
|
||||
|
||||
```no-highlight
|
||||
sudo cp -v /opt/netbox/contrib/*.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
```
|
||||
|
||||
The reference configuration assumes that gunicorn is in use, so we need to update it. Edit the `netbox.service` file to remove the line beginning with `ExecStart=/opt/netbox/venv/bin/gunicorn` and uncomment the line below it.
|
||||
|
||||
!!! warning "Check user & group assignment"
|
||||
The stock service configuration files packaged with NetBox assume that the service will run with the `netbox` user and group names. If these differ on your installation, be sure to update the service files accordingly.
|
||||
|
||||
Once the configuration file has been saved, reload the service:
|
||||
|
||||
```no-highlight
|
||||
sudo systemctl daemon-reload
|
||||
```
|
||||
|
||||
Then, start the `netbox` and `netbox-rq` services and enable them to initiate at boot time:
|
||||
|
||||
```no-highlight
|
||||
sudo systemctl enable --now netbox netbox-rq
|
||||
```
|
||||
|
||||
You can use the command `systemctl status netbox` to verify that the WSGI service is running:
|
||||
|
||||
```no-highlight
|
||||
systemctl status netbox.service
|
||||
```
|
||||
|
||||
You should see output similar to the following:
|
||||
|
||||
```no-highlight
|
||||
● netbox.service - NetBox WSGI Service
|
||||
Loaded: loaded (/etc/systemd/system/netbox.service; enabled; vendor preset: enabled)
|
||||
Active: active (running) since Mon 2021-08-30 04:02:36 UTC; 14h ago
|
||||
Docs: https://docs.netbox.dev/
|
||||
Main PID: 1140492 (uwsgi)
|
||||
Tasks: 19 (limit: 4683)
|
||||
Memory: 666.2M
|
||||
CGroup: /system.slice/netbox.service
|
||||
├─1061 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/uwsgi --ini /opt/netbox/uwsgi.ini
|
||||
├─1976 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/uwsgi --ini /opt/netbox/uwsgi.ini
|
||||
...
|
||||
```
|
||||
|
||||
!!! note
|
||||
If the NetBox service fails to start, issue the command `journalctl -eu netbox` to check for log messages that may indicate the problem.
|
||||
|
||||
Once you've verified that the WSGI workers are up and running, move on to HTTP server setup.
|
||||
|
||||
## HTTP Server Installation
|
||||
|
||||
For server installation, you will want to follow the NetBox [HTTP Server Setup](5-http-server.md) guide, however after copying the configuration file, you will need to edit the file and change the `location` section to uncomment the uWSGI parameters:
|
||||
|
||||
```no-highlight
|
||||
location / {
|
||||
# proxy_pass http://127.0.0.1:8001;
|
||||
# proxy_set_header X-Forwarded-Host $http_host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# comment the lines above and uncomment the lines below if using uWSGI
|
||||
include uwsgi_params;
|
||||
uwsgi_pass 127.0.0.1:8001;
|
||||
uwsgi_param Host $host;
|
||||
uwsgi_param X-Real-IP $remote_addr;
|
||||
uwsgi_param X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
uwsgi_param X-Forwarded-Proto $http_x_forwarded_proto;
|
||||
}
|
||||
```
|
|
@ -35,6 +35,9 @@ Once nginx is installed, copy the nginx configuration file provided by NetBox to
|
|||
sudo cp /opt/netbox/contrib/nginx.conf /etc/nginx/sites-available/netbox
|
||||
```
|
||||
|
||||
!!! tip "gunicorn vs. uWSGI"
|
||||
The reference nginx configuration file assumes that gunicorn is in use. If using uWSGI instead, you'll need to remove the gunicorn-specific configuration (lines beginning with `proxy_pass` and `proxy_set_header`) and uncomment the uWSGI section below them before proceeding.
|
||||
|
||||
Then, delete `/etc/nginx/sites-enabled/default` and create a symlink in the `sites-enabled` directory to the configuration file you just created.
|
||||
|
||||
```no-highlight
|
||||
|
|
|
@ -12,17 +12,17 @@ The following sections detail how to set up a new instance of NetBox:
|
|||
1. [PostgreSQL database](1-postgresql.md)
|
||||
1. [Redis](2-redis.md)
|
||||
3. [NetBox components](3-netbox.md)
|
||||
4. [Gunicorn](4-gunicorn.md)
|
||||
4. [Gunicorn](4a-gunicorn.md) or [uWSGI](4b-uwsgi.md)
|
||||
5. [HTTP server](5-http-server.md)
|
||||
6. [LDAP authentication](6-ldap.md) (optional)
|
||||
|
||||
## Requirements
|
||||
|
||||
| Dependency | Minimum Version |
|
||||
|------------|-----------------|
|
||||
| Python | 3.8 |
|
||||
| PostgreSQL | 12 |
|
||||
| Redis | 4.0 |
|
||||
| Dependency | Supported Versions |
|
||||
|------------|--------------------|
|
||||
| Python | 3.10, 3.11, 3.12 |
|
||||
| PostgreSQL | 12+ |
|
||||
| Redis | 4.0+ |
|
||||
|
||||
Below is a simplified overview of the NetBox application stack for reference:
|
||||
|
||||
|
|
|
@ -17,11 +17,11 @@ Prior to upgrading your NetBox instance, be sure to carefully review all [releas
|
|||
|
||||
NetBox requires the following dependencies:
|
||||
|
||||
| Dependency | Minimum Version |
|
||||
|------------|-----------------|
|
||||
| Python | 3.8 |
|
||||
| PostgreSQL | 12 |
|
||||
| Redis | 4.0 |
|
||||
| Dependency | Supported Versions |
|
||||
|------------|--------------------|
|
||||
| Python | 3.10, 3.11, 3.12 |
|
||||
| PostgreSQL | 12+ |
|
||||
| Redis | 4.0+ |
|
||||
|
||||
## 3. Install the Latest Release
|
||||
|
||||
|
@ -108,10 +108,10 @@ sudo ./upgrade.sh
|
|||
```
|
||||
|
||||
!!! warning
|
||||
If the default version of Python is not at least 3.8, you'll need to pass the path to a supported Python version as an environment variable when calling the upgrade script. For example:
|
||||
If the default version of Python is not at least 3.10, you'll need to pass the path to a supported Python version as an environment variable when calling the upgrade script. For example:
|
||||
|
||||
```no-highlight
|
||||
sudo PYTHON=/usr/bin/python3.8 ./upgrade.sh
|
||||
sudo PYTHON=/usr/bin/python3.10 ./upgrade.sh
|
||||
```
|
||||
|
||||
This script performs the following actions:
|
||||
|
|
|
@ -54,7 +54,11 @@ For more detail on constructing GraphQL queries, see the [Graphene documentation
|
|||
The GraphQL API employs the same filtering logic as the UI and REST API. Filters can be specified as key-value pairs within parentheses immediately following the query name. For example, the following will return only sites within the North Carolina region with a status of active:
|
||||
|
||||
```
|
||||
{"query": "query {site_list(region:\"north-carolina\", status:\"active\") {name}}"}
|
||||
query {
|
||||
site_list(filters: {region: "us-nc", status: "active"}) {
|
||||
name
|
||||
}
|
||||
}
|
||||
```
|
||||
In addition, filtering can be done on list of related objects as shown in the following query:
|
||||
|
||||
|
@ -63,7 +67,7 @@ In addition, filtering can be done on list of related objects as shown in the fo
|
|||
device_list {
|
||||
id
|
||||
name
|
||||
interfaces(enabled: true) {
|
||||
interfaces(filters: {enabled: true}) {
|
||||
name
|
||||
}
|
||||
}
|
||||
|
|
|
@ -73,9 +73,9 @@ If no body template is specified, the request body will be populated with a JSON
|
|||
|
||||
## Webhook Processing
|
||||
|
||||
Using [Event Rules](../features/event-rules.md), when a change is detected, any resulting webhooks are placed into a Redis queue for processing. This allows the user's request to complete without needing to wait for the outgoing webhook(s) to be processed. The webhooks are then extracted from the queue by the `rqworker` process and HTTP requests are sent to their respective destinations. The current webhook queue and any failed webhooks can be inspected in the admin UI under System > Background Tasks.
|
||||
Using [Event Rules](../features/event-rules.md), when a change is detected, any resulting webhooks are placed into a Redis queue for processing. This allows the user's request to complete without needing to wait for the outgoing webhook(s) to be processed. The webhooks are then extracted from the queue by the `rqworker` process and HTTP requests are sent to their respective destinations. The current webhook queue and any failed webhooks can be inspected under System > Background Tasks.
|
||||
|
||||
A request is considered successful if the response has a 2XX status code; otherwise, the request is marked as having failed. Failed requests may be retried manually via the admin UI.
|
||||
A request is considered successful if the response has a 2XX status code; otherwise, the request is marked as having failed. Failed requests may be requeued manually under System > Background Tasks.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
|
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 207 KiB After Width: | Height: | Size: 193 KiB |
Before Width: | Height: | Size: 316 KiB After Width: | Height: | Size: 422 KiB |
Before Width: | Height: | Size: 309 KiB After Width: | Height: | Size: 433 KiB |
Before Width: | Height: | Size: 356 KiB After Width: | Height: | Size: 510 KiB |
Before Width: | Height: | Size: 235 KiB After Width: | Height: | Size: 341 KiB |
|
@ -18,9 +18,9 @@ When a device has one or more interfaces with IP addresses assigned, a primary I
|
|||
|
||||
The device's configured name. This field is optional; devices can be unnamed. However, if set, the name must be unique to the assigned site and tenant.
|
||||
|
||||
### Device Role
|
||||
### Role
|
||||
|
||||
The functional [role](./devicerole.md) assigned to this device.
|
||||
The functional [device role](./devicerole.md) assigned to this device.
|
||||
|
||||
### Device Type
|
||||
|
||||
|
|
|
@ -26,3 +26,7 @@ The location's operational status.
|
|||
|
||||
!!! tip
|
||||
Additional statuses may be defined by setting `Location.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
|
||||
|
||||
### Facility
|
||||
|
||||
Data center or facility designation for identifying the location.
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
# Bookmarks
|
||||
|
||||
!!! info "This feature was introduced in NetBox v3.6."
|
||||
|
||||
A user can bookmark individual objects for convenient access. Bookmarks are listed under a user's profile and can be displayed using a dashboard widget.
|
||||
|
||||
## Fields
|
||||
|
|
|
@ -38,7 +38,7 @@ The type of data this field holds. This must be one of the following:
|
|||
| Object | A single NetBox object of the type defined by `object_type` |
|
||||
| Multiple object | One or more NetBox objects of the type defined by `object_type` |
|
||||
|
||||
### Object Type
|
||||
### Related Object Type
|
||||
|
||||
For object and multiple-object fields only. Designates the type of NetBox object being referenced.
|
||||
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
# Custom Field Choice Sets
|
||||
|
||||
!!! info "This feature was introduced in NetBox v3.6."
|
||||
|
||||
Single- and multi-selection [custom fields](../../customization/custom-fields.md) must define a set of valid choices from which the user may choose when defining the field value. These choices are defined as sets that may be reused among multiple custom fields.
|
||||
|
||||
A choice set must define a base choice set and/or a set of arbitrary extra choices.
|
||||
|
|
|
@ -18,8 +18,6 @@ The color to use when displaying the tag in the NetBox UI.
|
|||
|
||||
### Object Types
|
||||
|
||||
!!! info "This feature was introduced in NetBox v3.6."
|
||||
|
||||
The assignment of a tag may be limited to a prescribed set of objects. For example, it may be desirable to limit the application of a specific tag to only devices and virtual machines.
|
||||
|
||||
If no object types are specified, the tag will be assignable to any type of object.
|
||||
|
|
|
@ -15,16 +15,18 @@ NetBox provides several base form classes for use by plugins.
|
|||
|
||||
This is the base form for creating and editing NetBox models. It extends Django's ModelForm to add support for tags and custom fields.
|
||||
|
||||
| Attribute | Description |
|
||||
|-------------|-------------------------------------------------------------|
|
||||
| `fieldsets` | A tuple of two-tuples defining the form's layout (optional) |
|
||||
| Attribute | Description |
|
||||
|-------------|---------------------------------------------------------------------------------------|
|
||||
| `fieldsets` | A tuple of `FieldSet` instances which control how form fields are rendered (optional) |
|
||||
|
||||
**Example**
|
||||
|
||||
```python
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from dcim.models import Site
|
||||
from netbox.forms import NetBoxModelForm
|
||||
from utilities.forms.fields import CommentField, DynamicModelChoiceField
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from .models import MyModel
|
||||
|
||||
class MyModelForm(NetBoxModelForm):
|
||||
|
@ -33,8 +35,8 @@ class MyModelForm(NetBoxModelForm):
|
|||
)
|
||||
comments = CommentField()
|
||||
fieldsets = (
|
||||
('Model Stuff', ('name', 'status', 'site', 'tags')),
|
||||
('Tenancy', ('tenant_group', 'tenant')),
|
||||
FieldSet('name', 'status', 'site', 'tags', name=_('Model Stuff')),
|
||||
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
@ -52,6 +54,7 @@ This form facilitates the bulk import of new objects from CSV, JSON, or YAML dat
|
|||
**Example**
|
||||
|
||||
```python
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from dcim.models import Site
|
||||
from netbox.forms import NetBoxModelImportForm
|
||||
from utilities.forms import CSVModelChoiceField
|
||||
|
@ -77,16 +80,18 @@ This form facilitates editing multiple objects in bulk. Unlike a model form, thi
|
|||
| Attribute | Description |
|
||||
|-------------------|---------------------------------------------------------------------------------------------|
|
||||
| `model` | The model of object being edited |
|
||||
| `fieldsets` | A tuple of two-tuples defining the form's layout (optional) |
|
||||
| `fieldsets` | A tuple of `FieldSet` instances which control how form fields are rendered (optional) |
|
||||
| `nullable_fields` | A tuple of fields which can be nullified (set to empty) using the bulk edit form (optional) |
|
||||
|
||||
**Example**
|
||||
|
||||
```python
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from dcim.models import Site
|
||||
from netbox.forms import NetBoxModelImportForm
|
||||
from utilities.forms import CommentField, DynamicModelChoiceField
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from .models import MyModel, MyModelStatusChoices
|
||||
|
||||
|
||||
|
@ -106,7 +111,7 @@ class MyModelEditForm(NetBoxModelImportForm):
|
|||
|
||||
model = MyModel
|
||||
fieldsets = (
|
||||
('Model Stuff', ('name', 'status', 'site')),
|
||||
FieldSet('name', 'status', 'site', name=_('Model Stuff')),
|
||||
)
|
||||
nullable_fields = ('site', 'comments')
|
||||
```
|
||||
|
@ -115,10 +120,10 @@ class MyModelEditForm(NetBoxModelImportForm):
|
|||
|
||||
This form class is used to render a form expressly for filtering a list of objects. Its fields should correspond to filters defined on the model's filter set.
|
||||
|
||||
| Attribute | Description |
|
||||
|-------------------|-------------------------------------------------------------|
|
||||
| `model` | The model of object being edited |
|
||||
| `fieldsets` | A tuple of two-tuples defining the form's layout (optional) |
|
||||
| Attribute | Description |
|
||||
|-------------|---------------------------------------------------------------------------------------|
|
||||
| `model` | The model of object being edited |
|
||||
| `fieldsets` | A tuple of `FieldSet` instances which control how form fields are rendered (optional) |
|
||||
|
||||
**Example**
|
||||
|
||||
|
@ -206,3 +211,13 @@ In addition to the [form fields provided by Django](https://docs.djangoproject.c
|
|||
::: utilities.forms.fields.CSVMultipleContentTypeField
|
||||
options:
|
||||
members: false
|
||||
|
||||
## Form Rendering
|
||||
|
||||
::: utilities.forms.rendering.FieldSet
|
||||
|
||||
::: utilities.forms.rendering.InlineFields
|
||||
|
||||
::: utilities.forms.rendering.TabbedGroups
|
||||
|
||||
::: utilities.forms.rendering.ObjectAttribute
|
||||
|
|
|
@ -8,23 +8,31 @@ A plugin can extend NetBox's GraphQL API by registering its own schema class. By
|
|||
|
||||
```python
|
||||
# graphql.py
|
||||
import graphene
|
||||
from netbox.graphql.types import NetBoxObjectType
|
||||
from netbox.graphql.fields import ObjectField, ObjectListField
|
||||
from . import filtersets, models
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
class MyModelType(NetBoxObjectType):
|
||||
from . import models
|
||||
|
||||
class Meta:
|
||||
model = models.MyModel
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.MyModelFilterSet
|
||||
|
||||
class MyQuery(graphene.ObjectType):
|
||||
mymodel = ObjectField(MyModelType)
|
||||
mymodel_list = ObjectListField(MyModelType)
|
||||
@strawberry_django.type(
|
||||
models.MyModel,
|
||||
fields='__all__',
|
||||
)
|
||||
class MyModelType:
|
||||
pass
|
||||
|
||||
schema = MyQuery
|
||||
|
||||
@strawberry.type
|
||||
class MyQuery:
|
||||
@strawberry.field
|
||||
def dummymodel(self, id: int) -> DummyModelType:
|
||||
return None
|
||||
dummymodel_list: list[DummyModelType] = strawberry_django.field()
|
||||
|
||||
|
||||
schema = [
|
||||
MyQuery,
|
||||
]
|
||||
```
|
||||
|
||||
## GraphQL Objects
|
||||
|
@ -38,15 +46,3 @@ NetBox provides two object type classes for use by plugins.
|
|||
::: netbox.graphql.types.NetBoxObjectType
|
||||
options:
|
||||
members: false
|
||||
|
||||
## GraphQL Fields
|
||||
|
||||
NetBox provides two field classes for use by plugins.
|
||||
|
||||
::: netbox.graphql.fields.ObjectField
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: netbox.graphql.fields.ObjectListField
|
||||
options:
|
||||
members: false
|
||||
|
|
|
@ -138,7 +138,7 @@ Any additional apps must be installed within the same Python environment as NetB
|
|||
|
||||
## Create setup.py
|
||||
|
||||
`setup.py` is the [setup script](https://docs.python.org/3.8/distutils/setupscript.html) used to package and install our plugin once it's finished. The primary function of this script is to call the setuptools library's `setup()` function to create a Python distribution package. We can pass a number of keyword arguments to control the package creation as well as to provide metadata about the plugin. An example `setup.py` is below:
|
||||
`setup.py` is the [setup script](https://docs.python.org/3.10/distutils/setupscript.html) used to package and install our plugin once it's finished. The primary function of this script is to call the setuptools library's `setup()` function to create a Python distribution package. We can pass a number of keyword arguments to control the package creation as well as to provide metadata about the plugin. An example `setup.py` is below:
|
||||
|
||||
```python
|
||||
from setuptools import find_packages, setup
|
||||
|
@ -173,7 +173,7 @@ python3 -m venv ~/.virtualenvs/my_plugin
|
|||
You can make NetBox available within this environment by creating a path file pointing to its location. This will add NetBox to the Python path upon activation. (Be sure to adjust the command below to specify your actual virtual environment path, Python version, and NetBox installation.)
|
||||
|
||||
```shell
|
||||
echo /opt/netbox/netbox > $VENV/lib/python3.8/site-packages/netbox.pth
|
||||
echo /opt/netbox/netbox > $VENV/lib/python3.10/site-packages/netbox.pth
|
||||
```
|
||||
|
||||
## Development Installation
|
||||
|
|
|
@ -0,0 +1,347 @@
|
|||
# Migrating Your Plugin to NetBox v4.0
|
||||
|
||||
This document serves as a handbook for maintainers of plugins that were written prior to the release of NetBox v4.0. It serves to capture all the changes recommended to ensure a plugin is compatible with NetBox v4.0 and later releases.
|
||||
|
||||
## General
|
||||
|
||||
### Python support
|
||||
|
||||
NetBox v4.0 drops support for Python 3.8 and 3.9, and introduces support for Python 3.12. You may need to update your CI/CD processes and/or packaging to reflect this.
|
||||
|
||||
### Plugin resources relocated
|
||||
|
||||
All plugin Python resources were moved from `extras.plugins` to `netbox.plugins` in NetBox v3.7 (see [#14036](https://github.com/netbox-community/netbox/issues/14036)), and support for importing these resources from their old locations has been removed.
|
||||
|
||||
```python title="Old"
|
||||
from extras.plugins import PluginConfig
|
||||
```
|
||||
|
||||
```python title="New"
|
||||
from netbox.plugins import PluginConfig
|
||||
```
|
||||
|
||||
### ContentType renamed to ObjectType
|
||||
|
||||
NetBox's proxy model for Django's [ContentType model](https://docs.djangoproject.com/en/5.0/ref/contrib/contenttypes/#the-contenttype-model) has been renamed to ObjectType for clarity. In general, plugins should use the ObjectType proxy when referencing content types, as it includes several custom manager methods. The one exception to this is when defining [generic foreign keys](https://docs.djangoproject.com/en/5.0/ref/contrib/contenttypes/#generic-relations): The ForeignKey field used for a GFK should point to Django's native ContentType.
|
||||
|
||||
Additionally, plugin maintainers are strongly encouraged to adopt the "object type" terminology for field and filter names wherever feasible to be consistent with NetBox core (however this is not required for compatibility).
|
||||
|
||||
```python title="Old"
|
||||
content_types = models.ManyToManyField(
|
||||
to='contenttypes.ContentType',
|
||||
related_name='event_rules'
|
||||
)
|
||||
```
|
||||
|
||||
```python title="New"
|
||||
object_types = models.ManyToManyField(
|
||||
to='core.ObjectType',
|
||||
related_name='event_rules'
|
||||
)
|
||||
```
|
||||
|
||||
## Views
|
||||
|
||||
### View actions must be dictionaries
|
||||
|
||||
The format for declaring view actions & permissions was updated in NetBox v3.7 (see [#13550](https://github.com/netbox-community/netbox/issues/13550)), and NetBox v4.0 drops support for the old format. Views which inherit `ActionsMixin` must declare a single `actions` map.
|
||||
|
||||
```python title="Old"
|
||||
actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete')
|
||||
action_perms = defaultdict(set, **{
|
||||
'add': {'add'},
|
||||
'import': {'add'},
|
||||
'bulk_edit': {'change'},
|
||||
'bulk_delete': {'delete'},
|
||||
})
|
||||
```
|
||||
|
||||
```python title="New"
|
||||
actions = {
|
||||
'add': {'add'},
|
||||
'import': {'add'},
|
||||
'export': set(),
|
||||
'bulk_edit': {'change'},
|
||||
'bulk_delete': {'delete'},
|
||||
}
|
||||
```
|
||||
|
||||
## Forms
|
||||
|
||||
### Remove `BootstrapMixin`
|
||||
|
||||
The `BootstrapMixin` class is no longer available or needed and can be removed from all forms.
|
||||
|
||||
```python title="Old"
|
||||
from django import forms
|
||||
from utilities.forms import BootstrapMixin
|
||||
|
||||
class MyForm(BootstrapMixin, forms.Form):
|
||||
```
|
||||
|
||||
```python title="New"
|
||||
from django import forms
|
||||
|
||||
class MyForm(forms.Form):
|
||||
```
|
||||
|
||||
### Update Fieldset definitions
|
||||
|
||||
NetBox v4.0 introduces [several new classes](./forms.md#form-rendering) for advanced form rendering, including FieldSet. Fieldset definitions on forms should use this new class instead of a tuple or list.
|
||||
|
||||
Notably, the name of a fieldset is now optional, and passed as a keyword argument rather than as the first item in the set.
|
||||
|
||||
```python title="Old"
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from netbox.forms import NetBoxModelForm
|
||||
|
||||
class CircuitForm(NetBoxModelForm):
|
||||
...
|
||||
fieldsets = (
|
||||
(_('Circuit'), ('cid', 'type', 'status', 'description', 'tags')),
|
||||
(_('Service Parameters'), ('install_date', 'termination_date', 'commit_rate')),
|
||||
(_('Tenancy'), ('tenant_group', 'tenant')),
|
||||
)
|
||||
```
|
||||
|
||||
```python title="New"
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from netbox.forms import NetBoxModelForm
|
||||
from utilities.forms.rendering import FieldSet
|
||||
|
||||
class CircuitForm(NetBoxModelForm):
|
||||
...
|
||||
fieldsets = (
|
||||
FieldSet('cid', 'type', 'status', 'description', 'tags', name=_('Circuit')),
|
||||
FieldSet('install_date', 'termination_date', 'commit_rate', name=_('Service Parameters')),
|
||||
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
|
||||
)
|
||||
```
|
||||
|
||||
## Navigation
|
||||
|
||||
### Remove button colors
|
||||
|
||||
NetBox no longer applies color to buttons within navigation menu items. Although this functionality is still supported, you might want to remove color from any buttons to ensure consistency with the updated design.
|
||||
|
||||
```python title="Old"
|
||||
PluginMenuButton(
|
||||
link='myplugin:foo_add',
|
||||
title='Add a new Foo',
|
||||
icon_class='mdi mdi-plus-thick',
|
||||
color=ButtonColorChoices.GREEN
|
||||
)
|
||||
```
|
||||
|
||||
```python title="New"
|
||||
PluginMenuButton(
|
||||
link='myplugin:foo_add',
|
||||
title='Add a new Foo',
|
||||
icon_class='mdi mdi-plus-thick'
|
||||
)
|
||||
```
|
||||
|
||||
## UI Layout
|
||||
|
||||
### Renamed template blocks
|
||||
|
||||
The following template blocks have been renamed or removed:
|
||||
|
||||
| Template | Old name | New name |
|
||||
|---------------------|-------------------|---------------------------|
|
||||
| generic/object.html | `header` | `page-header` |
|
||||
| generic/object.html | `controls` | `control-buttons` |
|
||||
| base/layout.html | `content-wrapper` | _Removed_ (use `content`) |
|
||||
|
||||
### Utilize flex controls
|
||||
|
||||
Ditch any legacy "float" controls (e.g. `float-end`) in favor of Bootstrap's new [flex behaviors](https://getbootstrap.com/docs/5.3/utilities/flex/) for controlling the layout and sizing of elements horizontally. For example, the following will align two items against the left and right sides of the parent element:
|
||||
|
||||
```html
|
||||
<div class="d-flex justify-content-between">
|
||||
<h3>Title text</h3>
|
||||
<i class="mdi mdi-close"></i>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Check column offsets
|
||||
|
||||
When using [offset columns](https://getbootstrap.com/docs/5.3/layout/columns/#offsetting-columns) (e.g. `class="col-offset-3"`), be sure to also set the column width (e.g. `class="col-9 col-offset-3"`) to avoid horizontal scrolling.
|
||||
|
||||
### Tables inside cards
|
||||
|
||||
Tables inside cards should be embedded directly, not nested inside a `card-body` element.
|
||||
|
||||
```html title="Old"
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
...
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
```html title="New"
|
||||
<div class="card">
|
||||
<table class="table table-hover attr-table">
|
||||
...
|
||||
</table>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Remove `btn-sm` class from buttons
|
||||
|
||||
The `btn-sm` (small) class is no longer typically needed on general-purpose buttons.
|
||||
|
||||
```html title="Old"
|
||||
<a href="#" class="btn btn-sm btn-primary">Text</a>
|
||||
```
|
||||
|
||||
```html title="New"
|
||||
<a href="#" class="btn btn-primary">Text</a>
|
||||
```
|
||||
|
||||
### Update `bg-$color` classes
|
||||
|
||||
Foreground (text) color is no longer automatically adjusted by `bg-$color` classes. To ensure sufficient contrast with the background color, use the [`text-bg-$color`](https://getbootstrap.com/docs/5.3/helpers/color-background/) form of the class instead, or set the text color separately with `text-$color`.
|
||||
|
||||
```html title="Old"
|
||||
<span class="badge bg-primary">Text</span>
|
||||
```
|
||||
|
||||
```html title="New"
|
||||
<span class="badge text-bg-primary">Text</span>
|
||||
```
|
||||
|
||||
### Obsolete custom CSS classes
|
||||
|
||||
The following custom CSS classes have been removed:
|
||||
|
||||
* `object-subtitle` (use `text-secondary` instead)
|
||||
|
||||
## REST API
|
||||
|
||||
### Extend serializer for brief mode
|
||||
|
||||
NetBox now uses a single API serializer for both normal and "brief" modes (i.e. `GET /api/dcim/sites/?brief=true`); nested serializer classes are no longer required. Two changes to API serializers are necessary to support brief mode:
|
||||
|
||||
1. Define `brief_fields` under its `Meta` class. These are the fields which will be included when brief mode is used.
|
||||
2. For any nested objects, switch to using the primary serializer and pass `nested=True`.
|
||||
|
||||
Any nested serializers which are no longer needed can be removed.
|
||||
|
||||
```python title="Old"
|
||||
class SiteSerializer(NetBoxModelSerializer):
|
||||
region = NestedRegionSerializer(required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = ('id', 'url', 'display', 'name', 'slug', 'status', 'region', 'time_zone', ...)
|
||||
```
|
||||
|
||||
```python title="New"
|
||||
class SiteSerializer(NetBoxModelSerializer):
|
||||
region = RegionSerializer(nested=True, required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = ('id', 'url', 'display', 'name', 'slug', 'status', 'region', 'time_zone', ...)
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description', 'slug')
|
||||
```
|
||||
|
||||
### Include description fields in brief mode
|
||||
|
||||
NetBox now includes the `description` field in "brief" mode for all models which have one. This is not required for plugins, but you may opt to do the same for consistency.
|
||||
|
||||
## GraphQL
|
||||
|
||||
NetBox has replaced [Graphene-Django](https://github.com/graphql-python/graphene-django) with [Strawberry](https://strawberry.rocks/) which requires any GraphQL code to be updated.
|
||||
|
||||
### Change schema.py
|
||||
|
||||
Strawberry uses [Python typing](https://docs.python.org/3/library/typing.html) and generally only requires a small refactoring of the schema definition to update:
|
||||
|
||||
```python title="Old"
|
||||
import graphene
|
||||
from netbox.graphql.fields import ObjectField, ObjectListField
|
||||
from utilities.graphql_optimizer import gql_query_optimizer
|
||||
|
||||
class CircuitsQuery(graphene.ObjectType):
|
||||
circuit = ObjectField(CircuitType)
|
||||
circuit_list = ObjectListField(CircuitType)
|
||||
|
||||
def resolve_circuit_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.Circuit.objects.all(), info)
|
||||
```
|
||||
|
||||
```python title="New"
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
@strawberry.type
|
||||
class CircuitsQuery:
|
||||
@strawberry.field
|
||||
def circuit(self, id: int) -> CircuitType:
|
||||
return models.Circuit.objects.get(pk=id)
|
||||
circuit_list: list[CircuitType] = strawberry_django.field()
|
||||
```
|
||||
|
||||
### Change types.py
|
||||
|
||||
Type conversion is also fairly straight-forward, but Strawberry requires FK and M2M references to be explicitly defined to pick up the right typing.
|
||||
|
||||
1. The `class Meta` options need to be moved up to the Strawberry decorator
|
||||
2. Add `@strawberry_django.field` definitions for any FK and M2M references in the model
|
||||
|
||||
```python title="Old"
|
||||
import graphene
|
||||
|
||||
class CircuitType(NetBoxObjectType, ContactsMixin):
|
||||
class Meta:
|
||||
model = models.Circuit
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.CircuitFilterSet
|
||||
```
|
||||
|
||||
```python title="New"
|
||||
from typing import Annotated
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
@strawberry_django.type(
|
||||
models.CircuitType,
|
||||
fields='__all__',
|
||||
filters=CircuitTypeFilter
|
||||
)
|
||||
class CircuitTypeType(OrganizationalObjectType):
|
||||
color: str
|
||||
|
||||
@strawberry_django.field
|
||||
def circuits(self) -> list[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]:
|
||||
return self.circuits.all()
|
||||
```
|
||||
|
||||
### Change filters.py
|
||||
|
||||
Strawberry currently doesn't directly support django-filter, so an explicit filters.py file will need to be created. NetBox includes a new `autotype_decorator` used to automatically wrap FilterSets to reduce the required code to a minimum.
|
||||
|
||||
```python title="New"
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from circuits import filtersets, models
|
||||
|
||||
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
|
||||
|
||||
__all__ = (
|
||||
'CircuitFilter',
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Circuit, lookups=True)
|
||||
@autotype_decorator(filtersets.CircuitFilterSet)
|
||||
class CircuitFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
```
|
|
@ -49,8 +49,8 @@ menu_items = (item1, item2, item3)
|
|||
Each menu item represents a link and (optionally) a set of buttons comprising one entry in NetBox's navigation menu. Menu items are defined as PluginMenuItem instances. An example is shown below.
|
||||
|
||||
```python title="navigation.py"
|
||||
from netbox.choices import ButtonColorChoices
|
||||
from netbox.plugins import PluginMenuButton, PluginMenuItem
|
||||
from utilities.choices import ButtonColorChoices
|
||||
|
||||
item1 = PluginMenuItem(
|
||||
link='plugins:myplugin:myview',
|
||||
|
@ -72,8 +72,6 @@ A `PluginMenuItem` has the following attributes:
|
|||
| `staff_only` | - | Display only for users who have `is_staff` set to true (any specified permissions will also be required) |
|
||||
| `buttons` | - | An iterable of PluginMenuButton instances to include |
|
||||
|
||||
!!! info "The `staff_only` attribute was introduced in NetBox v3.6.1."
|
||||
|
||||
## Menu Buttons
|
||||
|
||||
Each menu item can include a set of buttons. These can be handy for providing shortcuts related to the menu item. For instance, most items in NetBox's navigation menu include buttons to create and import new objects.
|
||||
|
|
|
@ -90,8 +90,6 @@ The table column classes listed below are supported for use in plugins. These cl
|
|||
|
||||
## Extending Core Tables
|
||||
|
||||
!!! info "This feature was introduced in NetBox v3.7."
|
||||
|
||||
Plugins can register their own custom columns on core tables using the `register_table_column()` utility function. This allows a plugin to attach additional information, such as relationships to its own models, to built-in object lists.
|
||||
|
||||
```python
|
||||
|
|
|
@ -10,6 +10,14 @@ Minor releases are published in April, August, and December of each calendar yea
|
|||
|
||||
This page contains a history of all major and minor releases since NetBox v2.0. For more detail on a specific patch release, please see the release notes page for that specific minor release.
|
||||
|
||||
#### [Version 4.0](./version-4.0.md) (April 2024)
|
||||
|
||||
* Complete UI Refresh ([#12128](https://github.com/netbox-community/netbox/issues/12128))
|
||||
* Dynamic REST API Fields ([#15087](https://github.com/netbox-community/netbox/issues/15087))
|
||||
* Strawberry GraphQL Engine ([#9856](https://github.com/netbox-community/netbox/issues/9856))
|
||||
* Advanced Form Rendering Functionality ([#14739](https://github.com/netbox-community/netbox/issues/14739))
|
||||
* Legacy Admin UI Disabled ([#12325](https://github.com/netbox-community/netbox/issues/12325))
|
||||
|
||||
#### [Version 3.7](./version-3.7.md) (December 2023)
|
||||
|
||||
* VPN Tunnels ([#9816](https://github.com/netbox-community/netbox/issues/9816))
|
||||
|
|
|
@ -0,0 +1,180 @@
|
|||
# NetBox v4.0
|
||||
|
||||
## v4.0-beta2 (2024-04-22)
|
||||
|
||||
**WARNING:** This is a beta release of NetBox intended for testing and evaluation. **Do not use this software in production.** Also be aware that no upgrade path is provided to future releases.
|
||||
|
||||
!!! tip "Plugin Maintainers"
|
||||
Please see the dedicated [plugin migration guide](../plugins/development/migration-v4.md) for a checklist of changes that may be needed to ensure compatibility with NetBox v4.0.
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* Support for Python 3.8 and 3.9 has been removed.
|
||||
* The format for GraphQL query filters has changed. Please see the GraphQL documentation for details and examples.
|
||||
* The deprecated `device_role` & `device_role_id` filters for devices have been removed. (Use `role` and `role_id` instead.)
|
||||
* The obsolete `device_role` field has been removed from the REST API serializer for devices. (Use `role` instead.)
|
||||
* The legacy reports functionality has been dropped. Reports will be automatically converted to custom scripts on upgrade.
|
||||
* The `parent` and `parent_id` filters for locations now return only immediate children of the specified location. (Use `ancestor` and `ancestor_id` to return _all_ descendants.)
|
||||
* The `object_type` field on the CustomField model has been renamed to `related_object_type`.
|
||||
* The `utilities.utils` module has been removed and its resources reorganized into separate modules organized by function.
|
||||
* The obsolete `NullableCharField` class has been removed. (Use Django's stock `CharField` class with `null=True` instead.)
|
||||
* The `annotated_date` template filter and `annotated_now` template tag have been removed.
|
||||
|
||||
### New Features
|
||||
|
||||
#### Complete UI Refresh ([#12128](https://github.com/netbox-community/netbox/issues/12128))
|
||||
|
||||
The NetBox user interface has been completely refreshed and updated. This massive effort entailed:
|
||||
|
||||
* Refactoring the base HTML templates
|
||||
* Moving from Boostrap 5.0 to Bootstrap 5.3
|
||||
* Adopting the [Tabler](https://tabler.io/) UI theme
|
||||
* Replacing slim-select with [Tom-Select](https://tom-select.js.org/)
|
||||
* Displaying additional object attributes in dropdown form fields
|
||||
* Enabling opt-in HTMX-powered navigation (see [#14736](https://github.com/netbox-community/netbox/issues/14736))
|
||||
* Widespread cleanup & standardization of UI components
|
||||
|
||||
#### Dynamic REST API Fields ([#15087](https://github.com/netbox-community/netbox/issues/15087))
|
||||
|
||||
The REST API now supports specifying which fields to include in the response data. For example, the response to a request for
|
||||
|
||||
```
|
||||
GET /api/dcim/sites/?fields=name,status,region,tenant
|
||||
```
|
||||
|
||||
will include only the four specified fields in the representation of each site. Additionally, the underlying database queries effected by such requests have been optimized to omit fields which are not included in the response, resulting in a substantial performance improvement.
|
||||
|
||||
#### Strawberry GraphQL Engine ([#9856](https://github.com/netbox-community/netbox/issues/9856))
|
||||
|
||||
The GraphQL engine has been changed from using Graphene-Django to Strawberry-Django. Changes include:
|
||||
|
||||
* Queryset Optimizer - reduces the number of database queries when querying related tables
|
||||
* Updated GraphiQL Browser
|
||||
* The format for GraphQL query filters and lookups has changed. Please see the GraphQL documentation for details and examples.
|
||||
|
||||
#### Advanced Form Rendering Functionality ([#14739](https://github.com/netbox-community/netbox/issues/14739))
|
||||
|
||||
New resources have been introduced to enable advanced form rendering without a need for custom HTML templates. These include:
|
||||
|
||||
* FieldSet - Represents a grouping of form fields (replaces the use of lists/tuples)
|
||||
* InlineFields - Multiple fields rendered on a single row
|
||||
* TabbedGroups - Fieldsets rendered under navigable tabs within a form
|
||||
* ObjectAttribute - Renders a read-only representation of a particular object attribute (for reference)
|
||||
|
||||
#### Legacy Admin UI Disabled ([#12325](https://github.com/netbox-community/netbox/issues/12325))
|
||||
|
||||
The legacy admin user interface is now disabled by default, and the few remaining views it provided have been relocated to the primary UI. NetBox deployments which still depend on the legacy admin functionality for plugins can enable it by setting the `DJANGO_ADMIN_ENABLED` configuration parameter to true.
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#12776](https://github.com/netbox-community/netbox/issues/12776) - Introduce the `htmx_talble` template tag to simplify the rendering of embedded tables
|
||||
* [#12851](https://github.com/netbox-community/netbox/issues/12851) - Replace the deprecated Bleach HTML sanitization library with nh3
|
||||
* [#13283](https://github.com/netbox-community/netbox/issues/13283) - Display additional context on API-backed dropdown form fields (e.g. object descriptions)
|
||||
* [#13918](https://github.com/netbox-community/netbox/issues/13918) - Add `facility` field to Location model
|
||||
* [#14237](https://github.com/netbox-community/netbox/issues/14237) - Automatically clear dependent selection form fields when modifying a parent selection
|
||||
* [#14279](https://github.com/netbox-community/netbox/issues/14279) - Make the current request available as context when running custom validators
|
||||
* [#14454](https://github.com/netbox-community/netbox/issues/14454) - Include member devices in the REST API representation of virtual chassis
|
||||
* [#14637](https://github.com/netbox-community/netbox/issues/14637) - Upgrade to Django 5.0
|
||||
* [#14672](https://github.com/netbox-community/netbox/issues/14672) - Add support for Python 3.12
|
||||
* [#14728](https://github.com/netbox-community/netbox/issues/14728) - The plugins list view has been moved from the legacy admin UI to the main NetBox UI
|
||||
* [#14729](https://github.com/netbox-community/netbox/issues/14729) - All background task views have been moved from the legacy admin UI to the main NetBox UI
|
||||
* [#14736](https://github.com/netbox-community/netbox/issues/14736) - Introduce a user preference to enable HTMX-powered navigation
|
||||
* [#14438](https://github.com/netbox-community/netbox/issues/14438) - Track individual custom scripts as database objects
|
||||
* [#15131](https://github.com/netbox-community/netbox/issues/15131) - Automatically annotate related object counts on REST API querysets
|
||||
* [#15237](https://github.com/netbox-community/netbox/issues/15237) - Ensure consistent filtering ability for all model fields by testing for missing/incorrect filters
|
||||
* [#15238](https://github.com/netbox-community/netbox/issues/15238) - Include the `description` field in "brief" REST API serializations
|
||||
* [#15278](https://github.com/netbox-community/netbox/issues/15278) - BaseModelSerializer now takes a `nested` keyword argument allowing it to represent a related object
|
||||
* [#15383](https://github.com/netbox-community/netbox/issues/15383) - Standardize filtering logic for the parents of recursively-nested models (parent & ancestor filters)
|
||||
* [#15413](https://github.com/netbox-community/netbox/issues/15413) - The global search engine now supports caching of non-field object attributes
|
||||
* [#15490](https://github.com/netbox-community/netbox/issues/15490) - Custom validators can now reference related object attributes via dotted paths
|
||||
* [#15547](https://github.com/netbox-community/netbox/issues/15547) - Add comments field to CustomField model
|
||||
* [#15712](https://github.com/netbox-community/netbox/issues/15712) - Enable image attachments for virtual machines
|
||||
* [#15735](https://github.com/netbox-community/netbox/issues/15735) - Display all dates & times in ISO 8601 format consistently
|
||||
* [#15754](https://github.com/netbox-community/netbox/issues/15754) - Remove `is_staff` restriction on admin menu items
|
||||
* [#15764](https://github.com/netbox-community/netbox/issues/15764) - Increase maximum value of Device `vc_position` field
|
||||
* [#15915](https://github.com/netbox-community/netbox/issues/15915) - Provide a comprehensive system status view with export functionality
|
||||
|
||||
### Bug Fixes (from Beta2)
|
||||
|
||||
* [#15630](https://github.com/netbox-community/netbox/issues/15630) - Ensure consistent toggling between light & dark UI modes
|
||||
* [#15802](https://github.com/netbox-community/netbox/issues/15802) - Improve hyperlink color contrast in dark mode
|
||||
* [#15809](https://github.com/netbox-community/netbox/issues/15809) - Fix GraphQL union support for nullable fields
|
||||
* [#15815](https://github.com/netbox-community/netbox/issues/15815) - Convert dashboard widgets referencing old user/group models
|
||||
* [#15826](https://github.com/netbox-community/netbox/issues/15826) - Update `EXEMPT_EXCLUDE_MODELS` to reference new user & group models
|
||||
* [#15831](https://github.com/netbox-community/netbox/issues/15831) - Fix LDAP group mirroring
|
||||
* [#15838](https://github.com/netbox-community/netbox/issues/15838) - Fix AttributeError exception when rendering custom date fields
|
||||
* [#15852](https://github.com/netbox-community/netbox/issues/15852) - Update total results count when filtering object lists
|
||||
* [#15853](https://github.com/netbox-community/netbox/issues/15853) - Correct background color for cable trace SVG images in dark mode
|
||||
* [#15855](https://github.com/netbox-community/netbox/issues/15855) - Fix AttributeError exception when creating an event rule tied to a custom script
|
||||
* [#15944](https://github.com/netbox-community/netbox/issues/15944) - Fix styling of paginator when displayed above an object list
|
||||
|
||||
### Other Changes
|
||||
|
||||
* [#10587](https://github.com/netbox-community/netbox/issues/10587) - Enable pagination and filtering for custom script logs
|
||||
* [#12325](https://github.com/netbox-community/netbox/issues/12325) - The Django admin UI is now disabled by default (set `DJANGO_ADMIN_ENABLED` to True to enable it)
|
||||
* [#12510](https://github.com/netbox-community/netbox/issues/12510) - Dropped support for legacy reports
|
||||
* [#12795](https://github.com/netbox-community/netbox/issues/12795) - NetBox now uses custom User and Group models rather than the stock models provided by Django
|
||||
* [#13647](https://github.com/netbox-community/netbox/issues/13647) - Squash all database migrations prior to v3.7
|
||||
* [#14092](https://github.com/netbox-community/netbox/issues/14092) - Remove backward compatibility for importing plugin resources from `extras.plugins` (now `netbox.plugins`)
|
||||
* [#14638](https://github.com/netbox-community/netbox/issues/14638) - Drop support for Python 3.8 and 3.9
|
||||
* [#14657](https://github.com/netbox-community/netbox/issues/14657) - Remove backward compatibility for old permissions mapping under `ActionsMixin`
|
||||
* [#14658](https://github.com/netbox-community/netbox/issues/14658) - Remove backward compatibility for importing `process_webhook()` from `extras.webhooks_worker` (now `extras.webhooks.send_webhook()`)
|
||||
* [#14740](https://github.com/netbox-community/netbox/issues/14740) - Remove the obsolete `BootstrapMixin` form mixin class
|
||||
* [#15042](https://github.com/netbox-community/netbox/issues/15042) - The logic for registering models & model features now executes under the `ready()` method of individual app configs, rather than relying on the `class_prepared` signal
|
||||
* [#15099](https://github.com/netbox-community/netbox/issues/15099) - Remove obsolete `device_role` and `device_role_id` filters for devices
|
||||
* [#15100](https://github.com/netbox-community/netbox/issues/15100) - Remove obsolete `NullableCharField` class
|
||||
* [#15154](https://github.com/netbox-community/netbox/issues/15154) - The installation documentation been extended to include instructions and an example configuration file for uWSGI as an alternative to gunicorn
|
||||
* [#15193](https://github.com/netbox-community/netbox/issues/15193) - Switch to compiled distribution of the `psycopg` library
|
||||
* [#15277](https://github.com/netbox-community/netbox/issues/15277) - Replace references to ContentType without ObjectType proxy model & standardize field names
|
||||
* [#15292](https://github.com/netbox-community/netbox/issues/15292) - Remove obsolete `device_role` attribute from Device model (this field was renamed to `role` in v3.6)
|
||||
* [#15357](https://github.com/netbox-community/netbox/issues/15357) - The `object_type` field on the CustomField model has been renamed to `related_object_type` to avoid confusion with its `object_types` field
|
||||
* [#15401](https://github.com/netbox-community/netbox/issues/15401) - PostgreSQL indexes and sequence tables for the relocated L2VPN models (see [#14311](https://github.com/netbox-community/netbox/issues/14311)) have been renamed
|
||||
* [#15462](https://github.com/netbox-community/netbox/issues/15462) - Relocate resources from the `utilities.utils` module
|
||||
* [#15464](https://github.com/netbox-community/netbox/issues/15464) - The many-to-many relationships for ObjectPermission are now defined on the custom User and Group models
|
||||
* [#15736](https://github.com/netbox-community/netbox/issues/15736) - Remove obsolete `annotated_date` template filter & `annotated_now` template tag
|
||||
* [#15738](https://github.com/netbox-community/netbox/issues/15738) - Remove obsolete configuration parameters for date & time formatting
|
||||
* [#15752](https://github.com/netbox-community/netbox/issues/15752) - Remove the obsolete `ENABLE_LOCALIZATION` configuration parameter
|
||||
* [#15942](https://github.com/netbox-community/netbox/issues/15942) - Refactor `settings_and_registry()` context processor
|
||||
|
||||
### REST API Changes
|
||||
|
||||
* The `/api/extras/content-types/` endpoint has moved to `/api/extras/object-types/`
|
||||
* The `/api/extras/reports/` endpoint has been removed
|
||||
* The `description` field is now included by default when using "brief mode" for all relevant models
|
||||
* dcim.Device
|
||||
* The obsolete read-only attribute `device_role` has been removed (replaced by `role` in v3.6)
|
||||
* dcim.Location
|
||||
* Added the optional `location` field
|
||||
* dcim.VirtualChassis
|
||||
* Added `members` field to list the member devices
|
||||
* extras.CustomField
|
||||
* `content_types` has been renamed to `object_types`
|
||||
* `object_type` has been renamed to `related_object_type`
|
||||
* The `content_types` filter is now `object_type`
|
||||
* The `content_type_id` filter is now `object_type_id`
|
||||
* extras.CustomLink
|
||||
* `content_types` has been renamed to `object_types`
|
||||
* The `content_types` filter is now `object_type`
|
||||
* The `content_type_id` filter is now `object_type_id`
|
||||
* extras.EventRule
|
||||
* `content_types` has been renamed to `object_types`
|
||||
* The `content_types` filter is now `object_type`
|
||||
* The `content_type_id` filter is now `object_type_id`
|
||||
* extras.ExportTemplate
|
||||
* `content_types` has been renamed to `object_types`
|
||||
* The `content_types` filter is now `object_type`
|
||||
* The `content_type_id` filter is now `object_type_id`
|
||||
* extras.ImageAttachment
|
||||
* `content_type` has been renamed to `object_type`
|
||||
* The `content_type` filter is now `object_type`
|
||||
* extras.SavedFilter
|
||||
* `content_types` has been renamed to `object_types`
|
||||
* The `content_types` filter is now `object_type`
|
||||
* The `content_type_id` filter is now `object_type_id`
|
||||
* tenancy.ContactAssignment
|
||||
* `content_type` has been renamed to `object_type`
|
||||
* The `content_type_id` filter is now `object_type_id`
|
||||
* users.Group
|
||||
* Added the `permissions` field
|
||||
* users.User
|
||||
* Added the `permissions` field
|
|
@ -53,6 +53,7 @@ extra_css:
|
|||
markdown_extensions:
|
||||
- admonition
|
||||
- attr_list
|
||||
- footnotes
|
||||
- pymdownx.emoji:
|
||||
emoji_index: !!python/name:material.extensions.emoji.twemoji
|
||||
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
||||
|
@ -94,7 +95,8 @@ nav:
|
|||
- 1. PostgreSQL: 'installation/1-postgresql.md'
|
||||
- 2. Redis: 'installation/2-redis.md'
|
||||
- 3. NetBox: 'installation/3-netbox.md'
|
||||
- 4. Gunicorn: 'installation/4-gunicorn.md'
|
||||
- 4a. Gunicorn: 'installation/4a-gunicorn.md'
|
||||
- 4b. uWSGI: 'installation/4b-uwsgi.md'
|
||||
- 5. HTTP Server: 'installation/5-http-server.md'
|
||||
- 6. LDAP (Optional): 'installation/6-ldap.md'
|
||||
- Upgrading NetBox: 'installation/upgrading.md'
|
||||
|
@ -111,7 +113,6 @@ nav:
|
|||
- Default Values: 'configuration/default-values.md'
|
||||
- Error Reporting: 'configuration/error-reporting.md'
|
||||
- Plugins: 'configuration/plugins.md'
|
||||
- Date & Time: 'configuration/date-time.md'
|
||||
- Miscellaneous: 'configuration/miscellaneous.md'
|
||||
- Development: 'configuration/development.md'
|
||||
- Customization:
|
||||
|
@ -148,6 +149,7 @@ nav:
|
|||
- Dashboard Widgets: 'plugins/development/dashboard-widgets.md'
|
||||
- Staged Changes: 'plugins/development/staged-changes.md'
|
||||
- Exceptions: 'plugins/development/exceptions.md'
|
||||
- Migrating to v4.0: 'plugins/development/migration-v4.md'
|
||||
- Administration:
|
||||
- Authentication:
|
||||
- Overview: 'administration/authentication/overview.md'
|
||||
|
@ -294,6 +296,7 @@ nav:
|
|||
- git Cheat Sheet: 'development/git-cheat-sheet.md'
|
||||
- Release Notes:
|
||||
- Summary: 'release-notes/index.md'
|
||||
- Version 4.0: 'release-notes/version-4.0.md'
|
||||
- Version 3.7: 'release-notes/version-3.7.md'
|
||||
- Version 3.6: 'release-notes/version-3.6.md'
|
||||
- Version 3.5: 'release-notes/version-3.5.md'
|
||||
|
|
|
@ -30,10 +30,12 @@ class UserTokenTable(NetBoxTable):
|
|||
write_enabled = columns.BooleanColumn(
|
||||
verbose_name=_('Write Enabled')
|
||||
)
|
||||
created = columns.DateColumn(
|
||||
created = columns.DateTimeColumn(
|
||||
timespec='minutes',
|
||||
verbose_name=_('Created'),
|
||||
)
|
||||
expires = columns.DateColumn(
|
||||
expires = columns.DateTimeColumn(
|
||||
timespec='minutes',
|
||||
verbose_name=_('Expires'),
|
||||
)
|
||||
last_used = columns.DateTimeColumn(
|
||||
|
|
|
@ -2,8 +2,8 @@ import logging
|
|||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import login as auth_login, logout as auth_logout
|
||||
from django.contrib.auth import update_session_auth_hash
|
||||
from django.contrib.auth import login as auth_login, logout as auth_logout, update_session_auth_hash
|
||||
from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.models import update_last_login
|
||||
from django.contrib.auth.signals import user_logged_in
|
||||
|
@ -72,7 +72,7 @@ class LoginView(View):
|
|||
return auth_backends
|
||||
|
||||
def get(self, request):
|
||||
form = forms.LoginForm(request)
|
||||
form = AuthenticationForm(request)
|
||||
|
||||
if request.user.is_authenticated:
|
||||
logger = logging.getLogger('netbox.auth.login')
|
||||
|
@ -85,7 +85,7 @@ class LoginView(View):
|
|||
|
||||
def post(self, request):
|
||||
logger = logging.getLogger('netbox.auth.login')
|
||||
form = forms.LoginForm(request, data=request.POST)
|
||||
form = AuthenticationForm(request, data=request.POST)
|
||||
|
||||
if form.is_valid():
|
||||
logger.debug("Login form validation was successful")
|
||||
|
@ -220,7 +220,7 @@ class ChangePasswordView(LoginRequiredMixin, View):
|
|||
messages.warning(request, "LDAP-authenticated user credentials cannot be changed within NetBox.")
|
||||
return redirect('account:profile')
|
||||
|
||||
form = forms.PasswordChangeForm(user=request.user)
|
||||
form = PasswordChangeForm(user=request.user)
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'form': form,
|
||||
|
@ -228,7 +228,7 @@ class ChangePasswordView(LoginRequiredMixin, View):
|
|||
})
|
||||
|
||||
def post(self, request):
|
||||
form = forms.PasswordChangeForm(user=request.user, data=request.POST)
|
||||
form = PasswordChangeForm(user=request.user, data=request.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
update_session_auth_hash(request, form.user)
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
from drf_spectacular.utils import extend_schema_field, extend_schema_serializer
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema_serializer
|
||||
from rest_framework import serializers
|
||||
|
||||
from circuits.models import *
|
||||
from netbox.api.fields import RelatedObjectCountField
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
|
||||
__all__ = [
|
||||
|
@ -36,7 +36,7 @@ class NestedProviderNetworkSerializer(WritableNestedSerializer):
|
|||
)
|
||||
class NestedProviderSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
|
||||
circuit_count = serializers.IntegerField(read_only=True)
|
||||
circuit_count = RelatedObjectCountField('circuits')
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
|
@ -64,7 +64,7 @@ class NestedProviderAccountSerializer(WritableNestedSerializer):
|
|||
)
|
||||
class NestedCircuitTypeSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
|
||||
circuit_count = serializers.IntegerField(read_only=True)
|
||||
circuit_count = RelatedObjectCountField('circuits')
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
|
|
|
@ -1,137 +1,3 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from circuits.choices import CircuitStatusChoices
|
||||
from circuits.models import *
|
||||
from dcim.api.nested_serializers import NestedSiteSerializer
|
||||
from dcim.api.serializers import CabledObjectSerializer
|
||||
from ipam.models import ASN
|
||||
from ipam.api.nested_serializers import NestedASNSerializer
|
||||
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
|
||||
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||
from .serializers_.providers import *
|
||||
from .serializers_.circuits import *
|
||||
from .nested_serializers import *
|
||||
|
||||
|
||||
#
|
||||
# Providers
|
||||
#
|
||||
|
||||
class ProviderSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
|
||||
accounts = SerializedPKRelatedField(
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
serializer=NestedProviderAccountSerializer,
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
asns = SerializedPKRelatedField(
|
||||
queryset=ASN.objects.all(),
|
||||
serializer=NestedASNSerializer,
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
|
||||
# Related object counts
|
||||
circuit_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'accounts', 'description', 'comments', 'asns', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'circuit_count',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Provider Accounts
|
||||
#
|
||||
|
||||
class ProviderAccountSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provideraccount-detail')
|
||||
provider = NestedProviderSerializer()
|
||||
|
||||
class Meta:
|
||||
model = ProviderAccount
|
||||
fields = [
|
||||
'id', 'url', 'display', 'provider', 'name', 'account', 'description', 'comments', 'tags', 'custom_fields',
|
||||
'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Provider networks
|
||||
#
|
||||
|
||||
class ProviderNetworkSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail')
|
||||
provider = NestedProviderSerializer()
|
||||
|
||||
class Meta:
|
||||
model = ProviderNetwork
|
||||
fields = [
|
||||
'id', 'url', 'display', 'provider', 'name', 'service_id', 'description', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Circuits
|
||||
#
|
||||
|
||||
class CircuitTypeSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
|
||||
circuit_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'circuit_count',
|
||||
]
|
||||
|
||||
|
||||
class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
||||
site = NestedSiteSerializer(allow_null=True)
|
||||
provider_network = NestedProviderNetworkSerializer(allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'id', 'url', 'display', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
|
||||
'description',
|
||||
]
|
||||
|
||||
|
||||
class CircuitSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
|
||||
provider = NestedProviderSerializer()
|
||||
provider_account = NestedProviderAccountSerializer(required=False, allow_null=True)
|
||||
status = ChoiceField(choices=CircuitStatusChoices, required=False)
|
||||
type = NestedCircuitTypeSerializer()
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
termination_a = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
|
||||
termination_z = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = [
|
||||
'id', 'url', 'display', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date',
|
||||
'termination_date', 'commit_rate', 'description', 'termination_a', 'termination_z', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
||||
circuit = NestedCircuitSerializer()
|
||||
site = NestedSiteSerializer(required=False, allow_null=True)
|
||||
provider_network = NestedProviderNetworkSerializer(required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'id', 'url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed',
|
||||
'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers',
|
||||
'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
]
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from circuits.choices import CircuitStatusChoices
|
||||
from circuits.models import Circuit, CircuitTermination, CircuitType
|
||||
from dcim.api.serializers_.cables import CabledObjectSerializer
|
||||
from dcim.api.serializers_.sites import SiteSerializer
|
||||
from netbox.api.fields import ChoiceField, RelatedObjectCountField
|
||||
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
|
||||
from tenancy.api.serializers_.tenants import TenantSerializer
|
||||
|
||||
from .providers import ProviderAccountSerializer, ProviderNetworkSerializer, ProviderSerializer
|
||||
|
||||
__all__ = (
|
||||
'CircuitSerializer',
|
||||
'CircuitTerminationSerializer',
|
||||
'CircuitTypeSerializer',
|
||||
)
|
||||
|
||||
|
||||
class CircuitTypeSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
|
||||
|
||||
# Related object counts
|
||||
circuit_count = RelatedObjectCountField('circuits')
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created',
|
||||
'last_updated', 'circuit_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count')
|
||||
|
||||
|
||||
class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
||||
site = SiteSerializer(nested=True, allow_null=True)
|
||||
provider_network = ProviderNetworkSerializer(nested=True, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'id', 'url', 'display', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
|
||||
'description',
|
||||
]
|
||||
|
||||
|
||||
class CircuitSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
|
||||
provider = ProviderSerializer(nested=True)
|
||||
provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True)
|
||||
status = ChoiceField(choices=CircuitStatusChoices, required=False)
|
||||
type = CircuitTypeSerializer(nested=True)
|
||||
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||
termination_a = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
|
||||
termination_z = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = [
|
||||
'id', 'url', 'display', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date',
|
||||
'termination_date', 'commit_rate', 'description', 'termination_a', 'termination_z', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'cid', 'description')
|
||||
|
||||
|
||||
class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
||||
circuit = CircuitSerializer(nested=True)
|
||||
site = SiteSerializer(nested=True, required=False, allow_null=True)
|
||||
provider_network = ProviderNetworkSerializer(nested=True, required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'id', 'url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed',
|
||||
'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers',
|
||||
'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'circuit', 'term_side', 'description', 'cable', '_occupied')
|
|
@ -0,0 +1,68 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from circuits.models import Provider, ProviderAccount, ProviderNetwork
|
||||
from ipam.api.serializers_.asns import ASNSerializer
|
||||
from ipam.models import ASN
|
||||
from netbox.api.fields import RelatedObjectCountField, SerializedPKRelatedField
|
||||
from netbox.api.serializers import NetBoxModelSerializer
|
||||
from ..nested_serializers import *
|
||||
|
||||
__all__ = (
|
||||
'ProviderAccountSerializer',
|
||||
'ProviderNetworkSerializer',
|
||||
'ProviderSerializer',
|
||||
)
|
||||
|
||||
|
||||
class ProviderSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
|
||||
accounts = SerializedPKRelatedField(
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
serializer=NestedProviderAccountSerializer,
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
asns = SerializedPKRelatedField(
|
||||
queryset=ASN.objects.all(),
|
||||
serializer=ASNSerializer,
|
||||
nested=True,
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
|
||||
# Related object counts
|
||||
circuit_count = RelatedObjectCountField('circuits')
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'accounts', 'description', 'comments', 'asns', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'circuit_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count')
|
||||
|
||||
|
||||
class ProviderAccountSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provideraccount-detail')
|
||||
provider = ProviderSerializer(nested=True)
|
||||
|
||||
class Meta:
|
||||
model = ProviderAccount
|
||||
fields = [
|
||||
'id', 'url', 'display', 'provider', 'name', 'account', 'description', 'comments', 'tags', 'custom_fields',
|
||||
'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'account', 'description')
|
||||
|
||||
|
||||
class ProviderNetworkSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail')
|
||||
provider = ProviderSerializer(nested=True)
|
||||
|
||||
class Meta:
|
||||
model = ProviderNetwork
|
||||
fields = [
|
||||
'id', 'url', 'display', 'provider', 'name', 'service_id', 'description', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
|
@ -4,7 +4,6 @@ from circuits import filtersets
|
|||
from circuits.models import *
|
||||
from dcim.api.views import PassThroughPortMixin
|
||||
from netbox.api.viewsets import NetBoxModelViewSet
|
||||
from utilities.utils import count_related
|
||||
from . import serializers
|
||||
|
||||
|
||||
|
@ -21,9 +20,7 @@ class CircuitsRootView(APIRootView):
|
|||
#
|
||||
|
||||
class ProviderViewSet(NetBoxModelViewSet):
|
||||
queryset = Provider.objects.prefetch_related('asns', 'tags').annotate(
|
||||
circuit_count=count_related(Circuit, 'provider')
|
||||
)
|
||||
queryset = Provider.objects.all()
|
||||
serializer_class = serializers.ProviderSerializer
|
||||
filterset_class = filtersets.ProviderFilterSet
|
||||
|
||||
|
@ -33,9 +30,7 @@ class ProviderViewSet(NetBoxModelViewSet):
|
|||
#
|
||||
|
||||
class CircuitTypeViewSet(NetBoxModelViewSet):
|
||||
queryset = CircuitType.objects.prefetch_related('tags').annotate(
|
||||
circuit_count=count_related(Circuit, 'type')
|
||||
)
|
||||
queryset = CircuitType.objects.all()
|
||||
serializer_class = serializers.CircuitTypeSerializer
|
||||
filterset_class = filtersets.CircuitTypeFilterSet
|
||||
|
||||
|
@ -45,9 +40,7 @@ class CircuitTypeViewSet(NetBoxModelViewSet):
|
|||
#
|
||||
|
||||
class CircuitViewSet(NetBoxModelViewSet):
|
||||
queryset = Circuit.objects.prefetch_related(
|
||||
'type', 'tenant', 'provider', 'provider_account', 'termination_a', 'termination_z'
|
||||
).prefetch_related('tags')
|
||||
queryset = Circuit.objects.all()
|
||||
serializer_class = serializers.CircuitSerializer
|
||||
filterset_class = filtersets.CircuitFilterSet
|
||||
|
||||
|
@ -57,12 +50,9 @@ class CircuitViewSet(NetBoxModelViewSet):
|
|||
#
|
||||
|
||||
class CircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
||||
queryset = CircuitTermination.objects.prefetch_related(
|
||||
'circuit', 'site', 'provider_network', 'cable__terminations'
|
||||
)
|
||||
queryset = CircuitTermination.objects.all()
|
||||
serializer_class = serializers.CircuitTerminationSerializer
|
||||
filterset_class = filtersets.CircuitTerminationFilterSet
|
||||
brief_prefetch_fields = ['circuit']
|
||||
|
||||
|
||||
#
|
||||
|
@ -70,7 +60,7 @@ class CircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
|||
#
|
||||
|
||||
class ProviderAccountViewSet(NetBoxModelViewSet):
|
||||
queryset = ProviderAccount.objects.prefetch_related('provider', 'tags')
|
||||
queryset = ProviderAccount.objects.all()
|
||||
serializer_class = serializers.ProviderAccountSerializer
|
||||
filterset_class = filtersets.ProviderAccountFilterSet
|
||||
|
||||
|
@ -80,6 +70,6 @@ class ProviderAccountViewSet(NetBoxModelViewSet):
|
|||
#
|
||||
|
||||
class ProviderNetworkViewSet(NetBoxModelViewSet):
|
||||
queryset = ProviderNetwork.objects.prefetch_related('tags')
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
serializer_class = serializers.ProviderNetworkSerializer
|
||||
filterset_class = filtersets.ProviderNetworkFilterSet
|
||||
|
|
|
@ -6,4 +6,8 @@ class CircuitsConfig(AppConfig):
|
|||
verbose_name = "Circuits"
|
||||
|
||||
def ready(self):
|
||||
from netbox.models.features import register_models
|
||||
from . import signals, search
|
||||
|
||||
# Register models
|
||||
register_models(*self.get_models())
|
||||
|
|
|
@ -73,7 +73,7 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
|
|||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
fields = ('id', 'name', 'slug', 'description')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
|
@ -101,7 +101,7 @@ class ProviderAccountFilterSet(NetBoxModelFilterSet):
|
|||
|
||||
class Meta:
|
||||
model = ProviderAccount
|
||||
fields = ['id', 'name', 'account', 'description']
|
||||
fields = ('id', 'name', 'account', 'description')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
|
@ -128,7 +128,7 @@ class ProviderNetworkFilterSet(NetBoxModelFilterSet):
|
|||
|
||||
class Meta:
|
||||
model = ProviderNetwork
|
||||
fields = ['id', 'name', 'service_id', 'description']
|
||||
fields = ('id', 'name', 'service_id', 'description')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
|
@ -145,7 +145,7 @@ class CircuitTypeFilterSet(OrganizationalModelFilterSet):
|
|||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = ['id', 'name', 'slug', 'color', 'description']
|
||||
fields = ('id', 'name', 'slug', 'color', 'description')
|
||||
|
||||
|
||||
class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||
|
@ -164,6 +164,12 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
|
|||
queryset=ProviderAccount.objects.all(),
|
||||
label=_('Provider account (ID)'),
|
||||
)
|
||||
provider_account = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider_account__account',
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='account',
|
||||
label=_('Provider account (account)'),
|
||||
)
|
||||
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='terminations__provider_network',
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
|
@ -220,10 +226,18 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
|
|||
to_field_name='slug',
|
||||
label=_('Site (slug)'),
|
||||
)
|
||||
termination_a_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=CircuitTermination.objects.all(),
|
||||
label=_('Termination A (ID)'),
|
||||
)
|
||||
termination_z_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=CircuitTermination.objects.all(),
|
||||
label=_('Termination A (ID)'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = ['id', 'cid', 'description', 'install_date', 'termination_date', 'commit_rate']
|
||||
fields = ('id', 'cid', 'description', 'install_date', 'termination_date', 'commit_rate')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
|
@ -264,7 +278,10 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
|
|||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = ['id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description', 'cable_end']
|
||||
fields = (
|
||||
'id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description', 'mark_connected',
|
||||
'pp_info', 'cable_end',
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
|
|
|
@ -8,6 +8,7 @@ from netbox.forms import NetBoxModelBulkEditForm
|
|||
from tenancy.models import Tenant
|
||||
from utilities.forms import add_blank_choice
|
||||
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.widgets import DatePicker, NumberWithOptions
|
||||
|
||||
__all__ = (
|
||||
|
@ -34,7 +35,7 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
|
|||
|
||||
model = Provider
|
||||
fieldsets = (
|
||||
(None, ('asns', 'description')),
|
||||
FieldSet('asns', 'description'),
|
||||
)
|
||||
nullable_fields = (
|
||||
'asns', 'description', 'comments',
|
||||
|
@ -56,7 +57,7 @@ class ProviderAccountBulkEditForm(NetBoxModelBulkEditForm):
|
|||
|
||||
model = ProviderAccount
|
||||
fieldsets = (
|
||||
(None, ('provider', 'description')),
|
||||
FieldSet('provider', 'description'),
|
||||
)
|
||||
nullable_fields = (
|
||||
'description', 'comments',
|
||||
|
@ -83,7 +84,7 @@ class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm):
|
|||
|
||||
model = ProviderNetwork
|
||||
fieldsets = (
|
||||
(None, ('provider', 'service_id', 'description')),
|
||||
FieldSet('provider', 'service_id', 'description'),
|
||||
)
|
||||
nullable_fields = (
|
||||
'service_id', 'description', 'comments',
|
||||
|
@ -103,7 +104,7 @@ class CircuitTypeBulkEditForm(NetBoxModelBulkEditForm):
|
|||
|
||||
model = CircuitType
|
||||
fieldsets = (
|
||||
(None, ('color', 'description')),
|
||||
FieldSet('color', 'description'),
|
||||
)
|
||||
nullable_fields = ('color', 'description')
|
||||
|
||||
|
@ -164,9 +165,9 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
|
|||
|
||||
model = Circuit
|
||||
fieldsets = (
|
||||
(_('Circuit'), ('provider', 'type', 'status', 'description')),
|
||||
(_('Service Parameters'), ('provider_account', 'install_date', 'termination_date', 'commit_rate')),
|
||||
(_('Tenancy'), ('tenant',)),
|
||||
FieldSet('provider', 'type', 'status', 'description', name=_('Circuit')),
|
||||
FieldSet('provider_account', 'install_date', 'termination_date', 'commit_rate', name=_('Service Parameters')),
|
||||
FieldSet('tenant', name=_('Tenancy')),
|
||||
)
|
||||
nullable_fields = (
|
||||
'tenant', 'commit_rate', 'description', 'comments',
|
||||
|
|
|
@ -7,7 +7,6 @@ from django.utils.safestring import mark_safe
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
from netbox.forms import NetBoxModelImportForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import BootstrapMixin
|
||||
from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, SlugField
|
||||
|
||||
__all__ = (
|
||||
|
@ -112,7 +111,7 @@ class CircuitImportForm(NetBoxModelImportForm):
|
|||
]
|
||||
|
||||
|
||||
class CircuitTerminationImportForm(BootstrapMixin, forms.ModelForm):
|
||||
class CircuitTerminationImportForm(forms.ModelForm):
|
||||
site = CSVModelChoiceField(
|
||||
label=_('Site'),
|
||||
queryset=Site.objects.all(),
|
||||
|
|
|
@ -8,6 +8,7 @@ from ipam.models import ASN
|
|||
from netbox.forms import NetBoxModelFilterSetForm
|
||||
from tenancy.forms import TenancyFilterForm, ContactModelFilterForm
|
||||
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.widgets import DatePicker, NumberWithOptions
|
||||
|
||||
__all__ = (
|
||||
|
@ -22,10 +23,10 @@ __all__ = (
|
|||
class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
model = Provider
|
||||
fieldsets = (
|
||||
(None, ('q', 'filter_id', 'tag')),
|
||||
(_('Location'), ('region_id', 'site_group_id', 'site_id')),
|
||||
(_('ASN'), ('asn_id',)),
|
||||
(_('Contacts'), ('contact', 'contact_role', 'contact_group')),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
|
||||
FieldSet('asn_id', name=_('ASN')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
|
@ -57,8 +58,8 @@ class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
|||
class ProviderAccountFilterForm(NetBoxModelFilterSetForm):
|
||||
model = ProviderAccount
|
||||
fieldsets = (
|
||||
(None, ('q', 'filter_id', 'tag')),
|
||||
(_('Attributes'), ('provider_id', 'account')),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('provider_id', 'account', name=_('Attributes')),
|
||||
)
|
||||
provider_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
|
@ -75,8 +76,8 @@ class ProviderAccountFilterForm(NetBoxModelFilterSetForm):
|
|||
class ProviderNetworkFilterForm(NetBoxModelFilterSetForm):
|
||||
model = ProviderNetwork
|
||||
fieldsets = (
|
||||
(None, ('q', 'filter_id', 'tag')),
|
||||
(_('Attributes'), ('provider_id', 'service_id')),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('provider_id', 'service_id', name=_('Attributes')),
|
||||
)
|
||||
provider_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
|
@ -94,8 +95,8 @@ class ProviderNetworkFilterForm(NetBoxModelFilterSetForm):
|
|||
class CircuitTypeFilterForm(NetBoxModelFilterSetForm):
|
||||
model = CircuitType
|
||||
fieldsets = (
|
||||
(None, ('q', 'filter_id', 'tag')),
|
||||
(_('Attributes'), ('color',)),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('color', name=_('Attributes')),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
@ -108,12 +109,12 @@ class CircuitTypeFilterForm(NetBoxModelFilterSetForm):
|
|||
class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
model = Circuit
|
||||
fieldsets = (
|
||||
(None, ('q', 'filter_id', 'tag')),
|
||||
(_('Provider'), ('provider_id', 'provider_account_id', 'provider_network_id')),
|
||||
(_('Attributes'), ('type_id', 'status', 'install_date', 'termination_date', 'commit_rate')),
|
||||
(_('Location'), ('region_id', 'site_group_id', 'site_id')),
|
||||
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
|
||||
(_('Contacts'), ('contact', 'contact_role', 'contact_group')),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
|
||||
FieldSet('type_id', 'status', 'install_date', 'termination_date', 'commit_rate', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'provider_id', 'provider_network_id')
|
||||
type_id = DynamicModelMultipleChoiceField(
|
||||
|
|
|
@ -7,6 +7,7 @@ from ipam.models import ASN
|
|||
from netbox.forms import NetBoxModelForm
|
||||
from tenancy.forms import TenancyForm
|
||||
from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
|
||||
from utilities.forms.rendering import FieldSet, TabbedGroups
|
||||
from utilities.forms.widgets import DatePicker, NumberWithOptions
|
||||
|
||||
__all__ = (
|
||||
|
@ -29,7 +30,7 @@ class ProviderForm(NetBoxModelForm):
|
|||
comments = CommentField()
|
||||
|
||||
fieldsets = (
|
||||
(_('Provider'), ('name', 'slug', 'asns', 'description', 'tags')),
|
||||
FieldSet('name', 'slug', 'asns', 'description', 'tags'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
@ -61,7 +62,7 @@ class ProviderNetworkForm(NetBoxModelForm):
|
|||
comments = CommentField()
|
||||
|
||||
fieldsets = (
|
||||
(_('Provider Network'), ('provider', 'name', 'service_id', 'description', 'tags')),
|
||||
FieldSet('provider', 'name', 'service_id', 'description', 'tags'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
@ -75,9 +76,7 @@ class CircuitTypeForm(NetBoxModelForm):
|
|||
slug = SlugField()
|
||||
|
||||
fieldsets = (
|
||||
(_('Circuit Type'), (
|
||||
'name', 'slug', 'color', 'description', 'tags',
|
||||
)),
|
||||
FieldSet('name', 'slug', 'color', 'description', 'tags'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
@ -107,9 +106,9 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
|
|||
comments = CommentField()
|
||||
|
||||
fieldsets = (
|
||||
(_('Circuit'), ('provider', 'provider_account', 'cid', 'type', 'status', 'description', 'tags')),
|
||||
(_('Service Parameters'), ('install_date', 'termination_date', 'commit_rate')),
|
||||
(_('Tenancy'), ('tenant_group', 'tenant')),
|
||||
FieldSet('provider', 'provider_account', 'cid', 'type', 'status', 'description', 'tags', name=_('Circuit')),
|
||||
FieldSet('install_date', 'termination_date', 'commit_rate', name=_('Service Parameters')),
|
||||
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
@ -146,6 +145,18 @@ class CircuitTerminationForm(NetBoxModelForm):
|
|||
selector=True
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
'circuit', 'term_side', 'description', 'tags',
|
||||
TabbedGroups(
|
||||
FieldSet('site', name=_('Site')),
|
||||
FieldSet('provider_network', name=_('Provider Network')),
|
||||
),
|
||||
'mark_connected', name=_('Circuit Termination')
|
||||
),
|
||||
FieldSet('port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', name=_('Termination Details')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
import strawberry
|
||||
import strawberry_django
|
||||
from circuits import filtersets, models
|
||||
|
||||
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
|
||||
|
||||
__all__ = (
|
||||
'CircuitTerminationFilter',
|
||||
'CircuitFilter',
|
||||
'CircuitTypeFilter',
|
||||
'ProviderFilter',
|
||||
'ProviderAccountFilter',
|
||||
'ProviderNetworkFilter',
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.CircuitTermination, lookups=True)
|
||||
@autotype_decorator(filtersets.CircuitTerminationFilterSet)
|
||||
class CircuitTerminationFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Circuit, lookups=True)
|
||||
@autotype_decorator(filtersets.CircuitFilterSet)
|
||||
class CircuitFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.CircuitType, lookups=True)
|
||||
@autotype_decorator(filtersets.CircuitTypeFilterSet)
|
||||
class CircuitTypeFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Provider, lookups=True)
|
||||
@autotype_decorator(filtersets.ProviderFilterSet)
|
||||
class ProviderFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ProviderAccount, lookups=True)
|
||||
@autotype_decorator(filtersets.ProviderAccountFilterSet)
|
||||
class ProviderAccountFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ProviderNetwork, lookups=True)
|
||||
@autotype_decorator(filtersets.ProviderNetworkFilterSet)
|
||||
class ProviderNetworkFilter(BaseFilterMixin):
|
||||
pass
|
|
@ -1,41 +1,40 @@
|
|||
import graphene
|
||||
from typing import List
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from circuits import models
|
||||
from netbox.graphql.fields import ObjectField, ObjectListField
|
||||
from .types import *
|
||||
from utilities.graphql_optimizer import gql_query_optimizer
|
||||
|
||||
|
||||
class CircuitsQuery(graphene.ObjectType):
|
||||
circuit = ObjectField(CircuitType)
|
||||
circuit_list = ObjectListField(CircuitType)
|
||||
@strawberry.type
|
||||
class CircuitsQuery:
|
||||
@strawberry.field
|
||||
def circuit(self, id: int) -> CircuitType:
|
||||
return models.Circuit.objects.get(pk=id)
|
||||
circuit_list: List[CircuitType] = strawberry_django.field()
|
||||
|
||||
def resolve_circuit_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.Circuit.objects.all(), info)
|
||||
@strawberry.field
|
||||
def circuit_termination(self, id: int) -> CircuitTerminationType:
|
||||
return models.CircuitTermination.objects.get(pk=id)
|
||||
circuit_termination_list: List[CircuitTerminationType] = strawberry_django.field()
|
||||
|
||||
circuit_termination = ObjectField(CircuitTerminationType)
|
||||
circuit_termination_list = ObjectListField(CircuitTerminationType)
|
||||
@strawberry.field
|
||||
def circuit_type(self, id: int) -> CircuitTypeType:
|
||||
return models.CircuitType.objects.get(pk=id)
|
||||
circuit_type_list: List[CircuitTypeType] = strawberry_django.field()
|
||||
|
||||
def resolve_circuit_termination_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.CircuitTermination.objects.all(), info)
|
||||
@strawberry.field
|
||||
def provider(self, id: int) -> ProviderType:
|
||||
return models.Provider.objects.get(pk=id)
|
||||
provider_list: List[ProviderType] = strawberry_django.field()
|
||||
|
||||
circuit_type = ObjectField(CircuitTypeType)
|
||||
circuit_type_list = ObjectListField(CircuitTypeType)
|
||||
@strawberry.field
|
||||
def provider_account(self, id: int) -> ProviderAccountType:
|
||||
return models.ProviderAccount.objects.get(pk=id)
|
||||
provider_account_list: List[ProviderAccountType] = strawberry_django.field()
|
||||
|
||||
def resolve_circuit_type_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.CircuitType.objects.all(), info)
|
||||
|
||||
provider = ObjectField(ProviderType)
|
||||
provider_list = ObjectListField(ProviderType)
|
||||
|
||||
def resolve_provider_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.Provider.objects.all(), info)
|
||||
|
||||
provider_account = ObjectField(ProviderAccountType)
|
||||
provider_account_list = ObjectListField(ProviderAccountType)
|
||||
|
||||
provider_network = ObjectField(ProviderNetworkType)
|
||||
provider_network_list = ObjectListField(ProviderNetworkType)
|
||||
|
||||
def resolve_provider_network_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.ProviderNetwork.objects.all(), info)
|
||||
@strawberry.field
|
||||
def provider_network(self, id: int) -> ProviderNetworkType:
|
||||
return models.ProviderNetwork.objects.get(pk=id)
|
||||
provider_network_list: List[ProviderNetworkType] = strawberry_django.field()
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
import graphene
|
||||
from typing import Annotated, List
|
||||
|
||||
from circuits import filtersets, models
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from circuits import models
|
||||
from dcim.graphql.mixins import CabledObjectMixin
|
||||
from extras.graphql.mixins import CustomFieldsMixin, TagsMixin, ContactsMixin
|
||||
from netbox.graphql.types import ObjectType, OrganizationalObjectType, NetBoxObjectType
|
||||
from extras.graphql.mixins import ContactsMixin, CustomFieldsMixin, TagsMixin
|
||||
from netbox.graphql.types import NetBoxObjectType, ObjectType, OrganizationalObjectType
|
||||
from tenancy.graphql.types import TenantType
|
||||
from .filters import *
|
||||
|
||||
__all__ = (
|
||||
'CircuitTerminationType',
|
||||
|
@ -15,48 +20,74 @@ __all__ = (
|
|||
)
|
||||
|
||||
|
||||
class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, ObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.CircuitTermination
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.CircuitTerminationFilterSet
|
||||
|
||||
|
||||
class CircuitType(NetBoxObjectType, ContactsMixin):
|
||||
class Meta:
|
||||
model = models.Circuit
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.CircuitFilterSet
|
||||
|
||||
|
||||
class CircuitTypeType(OrganizationalObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.CircuitType
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.CircuitTypeFilterSet
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.Provider,
|
||||
fields='__all__',
|
||||
filters=ProviderFilter
|
||||
)
|
||||
class ProviderType(NetBoxObjectType, ContactsMixin):
|
||||
|
||||
class Meta:
|
||||
model = models.Provider
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.ProviderFilterSet
|
||||
networks: List[Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')]]
|
||||
circuits: List[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]
|
||||
asns: List[Annotated["ASNType", strawberry.lazy('ipam.graphql.types')]]
|
||||
accounts: List[Annotated["ProviderAccountType", strawberry.lazy('circuits.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.ProviderAccount,
|
||||
fields='__all__',
|
||||
filters=ProviderAccountFilter
|
||||
)
|
||||
class ProviderAccountType(NetBoxObjectType):
|
||||
provider: Annotated["ProviderType", strawberry.lazy('circuits.graphql.types')]
|
||||
|
||||
class Meta:
|
||||
model = models.ProviderAccount
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.ProviderAccountFilterSet
|
||||
circuits: List[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.ProviderNetwork,
|
||||
fields='__all__',
|
||||
filters=ProviderNetworkFilter
|
||||
)
|
||||
class ProviderNetworkType(NetBoxObjectType):
|
||||
provider: Annotated["ProviderType", strawberry.lazy('circuits.graphql.types')]
|
||||
|
||||
class Meta:
|
||||
model = models.ProviderNetwork
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.ProviderNetworkFilterSet
|
||||
circuit_terminations: List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.CircuitTermination,
|
||||
fields='__all__',
|
||||
filters=CircuitTerminationFilter
|
||||
)
|
||||
class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, ObjectType):
|
||||
circuit: Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]
|
||||
provider_network: Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')] | None
|
||||
site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.CircuitType,
|
||||
fields='__all__',
|
||||
filters=CircuitTypeFilter
|
||||
)
|
||||
class CircuitTypeType(OrganizationalObjectType):
|
||||
color: str
|
||||
|
||||
circuits: List[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.Circuit,
|
||||
fields='__all__',
|
||||
filters=CircuitFilter
|
||||
)
|
||||
class CircuitType(NetBoxObjectType, ContactsMixin):
|
||||
provider: ProviderType
|
||||
provider_account: ProviderAccountType | None
|
||||
termination_a: CircuitTerminationType | None
|
||||
termination_z: CircuitTerminationType | None
|
||||
type: CircuitTypeType
|
||||
tenant: TenantType | None
|
||||
|
||||
terminations: List[CircuitTerminationType]
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
# Generated by Django 3.2.8 on 2021-10-21 14:50
|
||||
|
||||
from django.db import migrations
|
||||
import taggit.managers
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0062_clear_secrets_changelog'),
|
||||
('circuits', '0002_squashed_0029'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='circuittype',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,127 @@
|
|||
import taggit.managers
|
||||
from django.db import migrations, models
|
||||
|
||||
import utilities.json
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [
|
||||
('circuits', '0003_extend_tag_support'),
|
||||
('circuits', '0004_rename_cable_peer'),
|
||||
('circuits', '0032_provider_service_id'),
|
||||
('circuits', '0033_standardize_id_fields'),
|
||||
('circuits', '0034_created_datetimefield'),
|
||||
('circuits', '0035_provider_asns'),
|
||||
('circuits', '0036_circuit_termination_date_tags_custom_fields'),
|
||||
('circuits', '0037_new_cabling_models')
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0047_squashed_0053'),
|
||||
('extras', '0002_squashed_0059'),
|
||||
('circuits', '0002_squashed_0029'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='circuittype',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='circuittermination',
|
||||
old_name='_cable_peer_id',
|
||||
new_name='_link_peer_id',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='circuittermination',
|
||||
old_name='_cable_peer_type',
|
||||
new_name='_link_peer_type',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='providernetwork',
|
||||
name='service_id',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuit',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuittermination',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuittype',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='provider',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='providernetwork',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuittermination',
|
||||
name='_link_peer_id',
|
||||
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuit',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuittermination',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuittype',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='provider',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='providernetwork',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='provider',
|
||||
name='asns',
|
||||
field=models.ManyToManyField(blank=True, related_name='providers', to='ipam.asn'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='termination_date',
|
||||
field=models.DateField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='custom_field_data',
|
||||
field=models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='cable_end',
|
||||
field=models.CharField(blank=True, max_length=1),
|
||||
),
|
||||
]
|
|
@ -1,21 +0,0 @@
|
|||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0003_extend_tag_support'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='circuittermination',
|
||||
old_name='_cable_peer_id',
|
||||
new_name='_link_peer_id',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='circuittermination',
|
||||
old_name='_cable_peer_type',
|
||||
new_name='_link_peer_type',
|
||||
),
|
||||
]
|
|
@ -1,17 +0,0 @@
|
|||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0004_rename_cable_peer'),
|
||||
('dcim', '0145_site_remove_deprecated_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='providernetwork',
|
||||
name='service_id',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
]
|
|
@ -1,44 +0,0 @@
|
|||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0032_provider_service_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# Model IDs
|
||||
migrations.AlterField(
|
||||
model_name='circuit',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuittermination',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuittype',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='provider',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='providernetwork',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||
),
|
||||
|
||||
# GFK IDs
|
||||
migrations.AlterField(
|
||||
model_name='circuittermination',
|
||||
name='_link_peer_id',
|
||||
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||
),
|
||||
]
|
|
@ -1,38 +0,0 @@
|
|||
# Generated by Django 4.0.2 on 2022-02-08 18:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0033_standardize_id_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='circuit',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuittermination',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuittype',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='provider',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='providernetwork',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||
),
|
||||
]
|
|
@ -1,19 +0,0 @@
|
|||
# Generated by Django 4.0.3 on 2022-03-30 20:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0057_created_datetimefield'),
|
||||
('circuits', '0034_created_datetimefield'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='provider',
|
||||
name='asns',
|
||||
field=models.ManyToManyField(blank=True, related_name='providers', to='ipam.asn'),
|
||||
),
|
||||
]
|
|
@ -1,28 +0,0 @@
|
|||
from utilities.json import CustomFieldJSONEncoder
|
||||
from django.db import migrations, models
|
||||
import taggit.managers
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0035_provider_asns'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='termination_date',
|
||||
field=models.DateField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='custom_field_data',
|
||||
field=models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||
),
|
||||
]
|
|
@ -1,16 +0,0 @@
|
|||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0036_circuit_termination_date_tags_custom_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='cable_end',
|
||||
field=models.CharField(blank=True, max_length=1),
|
||||
),
|
||||
]
|
|
@ -1,20 +0,0 @@
|
|||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0037_new_cabling_models'),
|
||||
('dcim', '0160_populate_cable_ends'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='circuittermination',
|
||||
name='_link_peer_id',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='circuittermination',
|
||||
name='_link_peer_type',
|
||||
),
|
||||
]
|
|
@ -1,46 +1,83 @@
|
|||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
from django.db import migrations, models
|
||||
|
||||
import utilities.json
|
||||
|
||||
|
||||
def create_provideraccounts_from_providers(apps, schema_editor):
|
||||
"""
|
||||
Migrate Account in Provider model to separate account model
|
||||
"""
|
||||
Provider = apps.get_model('circuits', 'Provider')
|
||||
ProviderAccount = apps.get_model('circuits', 'ProviderAccount')
|
||||
|
||||
provider_accounts = []
|
||||
for provider in Provider.objects.all():
|
||||
if provider.account:
|
||||
provider_accounts.append(ProviderAccount(
|
||||
provider=provider,
|
||||
account=provider.account
|
||||
))
|
||||
ProviderAccount.objects.bulk_create(provider_accounts, batch_size=100)
|
||||
|
||||
|
||||
def restore_providers_from_provideraccounts(apps, schema_editor):
|
||||
"""
|
||||
Restore Provider account values from auto-generated ProviderAccounts
|
||||
"""
|
||||
ProviderAccount = apps.get_model('circuits', 'ProviderAccount')
|
||||
provider_accounts = ProviderAccount.objects.order_by('pk')
|
||||
for provideraccount in provider_accounts:
|
||||
if provider_accounts.filter(provider=provideraccount.provider)[0] == provideraccount:
|
||||
provideraccount.provider.account = provideraccount.account
|
||||
provideraccount.provider.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0084_staging'),
|
||||
replaces = [
|
||||
('circuits', '0038_cabling_cleanup'),
|
||||
('circuits', '0039_unique_constraints'),
|
||||
('circuits', '0040_provider_remove_deprecated_fields'),
|
||||
('circuits', '0041_standardize_description_comments'),
|
||||
('circuits', '0042_provideraccount')
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0037_new_cabling_models'),
|
||||
('dcim', '0160_populate_cable_ends'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='circuittermination',
|
||||
name='_link_peer_id',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='circuittermination',
|
||||
name='_link_peer_type',
|
||||
),
|
||||
migrations.RemoveConstraint(
|
||||
model_name='providernetwork',
|
||||
name='circuits_providernetwork_provider_name',
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='circuit',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='circuittermination',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='providernetwork',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='circuit',
|
||||
constraint=models.UniqueConstraint(fields=('provider', 'cid'), name='circuits_circuit_unique_provider_cid'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='circuittermination',
|
||||
constraint=models.UniqueConstraint(fields=('circuit', 'term_side'), name='circuits_circuittermination_unique_circuit_term_side'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='providernetwork',
|
||||
constraint=models.UniqueConstraint(fields=('provider', 'name'), name='circuits_providernetwork_unique_provider_name'),
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='provider',
|
||||
name='admin_contact',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='provider',
|
||||
name='asn',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='provider',
|
||||
name='noc_contact',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='provider',
|
||||
name='portal_url',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='provider',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ProviderAccount',
|
||||
fields=[
|
||||
|
@ -67,9 +104,6 @@ class Migration(migrations.Migration):
|
|||
model_name='provideraccount',
|
||||
constraint=models.UniqueConstraint(fields=('provider', 'account'), name='circuits_provideraccount_unique_provider_account'),
|
||||
),
|
||||
migrations.RunPython(
|
||||
create_provideraccounts_from_providers, restore_providers_from_provideraccounts
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='provider',
|
||||
name='account',
|
||||
|
@ -77,7 +111,7 @@ class Migration(migrations.Migration):
|
|||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='provider_account',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.provideraccount', null=True, blank=True),
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.provideraccount'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterModelOptions(
|
|
@ -1,39 +0,0 @@
|
|||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0038_cabling_cleanup'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveConstraint(
|
||||
model_name='providernetwork',
|
||||
name='circuits_providernetwork_provider_name',
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='circuit',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='circuittermination',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='providernetwork',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='circuit',
|
||||
constraint=models.UniqueConstraint(fields=('provider', 'cid'), name='circuits_circuit_unique_provider_cid'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='circuittermination',
|
||||
constraint=models.UniqueConstraint(fields=('circuit', 'term_side'), name='circuits_circuittermination_unique_circuit_term_side'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='providernetwork',
|
||||
constraint=models.UniqueConstraint(fields=('provider', 'name'), name='circuits_providernetwork_unique_provider_name'),
|
||||
),
|
||||
]
|
|
@ -1,59 +0,0 @@
|
|||
import os
|
||||
|
||||
from django.db import migrations
|
||||
from django.db.utils import DataError
|
||||
|
||||
|
||||
def check_legacy_data(apps, schema_editor):
|
||||
"""
|
||||
Abort the migration if any legacy provider fields still contain data.
|
||||
"""
|
||||
Provider = apps.get_model('circuits', 'Provider')
|
||||
|
||||
provider_count = Provider.objects.exclude(asn__isnull=True).count()
|
||||
if provider_count and 'NETBOX_DELETE_LEGACY_DATA' not in os.environ:
|
||||
raise DataError(
|
||||
f"Unable to proceed with deleting asn field from Provider model: Found {provider_count} "
|
||||
f"providers with legacy ASN data. Please ensure all legacy provider ASN data has been "
|
||||
f"migrated to ASN objects before proceeding. Or, set the NETBOX_DELETE_LEGACY_DATA "
|
||||
f"environment variable to bypass this safeguard and delete all legacy provider ASN data."
|
||||
)
|
||||
|
||||
provider_count = Provider.objects.exclude(admin_contact='', noc_contact='', portal_url='').count()
|
||||
if provider_count and 'NETBOX_DELETE_LEGACY_DATA' not in os.environ:
|
||||
raise DataError(
|
||||
f"Unable to proceed with deleting contact fields from Provider model: Found {provider_count} "
|
||||
f"providers with legacy contact data. Please ensure all legacy provider contact data has been "
|
||||
f"migrated to contact objects before proceeding. Or, set the NETBOX_DELETE_LEGACY_DATA "
|
||||
f"environment variable to bypass this safeguard and delete all legacy provider contact data."
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0039_unique_constraints'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
code=check_legacy_data,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='provider',
|
||||
name='admin_contact',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='provider',
|
||||
name='asn',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='provider',
|
||||
name='noc_contact',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='provider',
|
||||
name='portal_url',
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 4.1.2 on 2022-11-03 18:24
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0040_provider_remove_deprecated_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='provider',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
]
|
|
@ -18,7 +18,7 @@ class AppTest(APITestCase):
|
|||
|
||||
class ProviderTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Provider
|
||||
brief_fields = ['circuit_count', 'display', 'id', 'name', 'slug', 'url']
|
||||
brief_fields = ['circuit_count', 'description', 'display', 'id', 'name', 'slug', 'url']
|
||||
bulk_update_data = {
|
||||
'comments': 'New comments',
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ class ProviderTest(APIViewTestCases.APIViewTestCase):
|
|||
|
||||
class CircuitTypeTest(APIViewTestCases.APIViewTestCase):
|
||||
model = CircuitType
|
||||
brief_fields = ['circuit_count', 'display', 'id', 'name', 'slug', 'url']
|
||||
brief_fields = ['circuit_count', 'description', 'display', 'id', 'name', 'slug', 'url']
|
||||
create_data = (
|
||||
{
|
||||
'name': 'Circuit Type 4',
|
||||
|
@ -92,7 +92,7 @@ class CircuitTypeTest(APIViewTestCases.APIViewTestCase):
|
|||
|
||||
class CircuitTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Circuit
|
||||
brief_fields = ['cid', 'display', 'id', 'url']
|
||||
brief_fields = ['cid', 'description', 'display', 'id', 'url']
|
||||
bulk_update_data = {
|
||||
'status': 'planned',
|
||||
}
|
||||
|
@ -149,7 +149,7 @@ class CircuitTest(APIViewTestCases.APIViewTestCase):
|
|||
|
||||
class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
|
||||
model = CircuitTermination
|
||||
brief_fields = ['_occupied', 'cable', 'circuit', 'display', 'id', 'term_side', 'url']
|
||||
brief_fields = ['_occupied', 'cable', 'circuit', 'description', 'display', 'id', 'term_side', 'url']
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
@ -208,7 +208,7 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
|
|||
|
||||
class ProviderAccountTest(APIViewTestCases.APIViewTestCase):
|
||||
model = ProviderAccount
|
||||
brief_fields = ['account', 'display', 'id', 'name', 'url']
|
||||
brief_fields = ['account', 'description', 'display', 'id', 'name', 'url']
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
@ -251,7 +251,7 @@ class ProviderAccountTest(APIViewTestCases.APIViewTestCase):
|
|||
|
||||
class ProviderNetworkTest(APIViewTestCases.APIViewTestCase):
|
||||
model = ProviderNetwork
|
||||
brief_fields = ['display', 'id', 'name', 'url']
|
||||
brief_fields = ['description', 'display', 'id', 'name', 'url']
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
|
|
@ -332,6 +332,7 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||
class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = CircuitTermination.objects.all()
|
||||
filterset = CircuitTerminationFilterSet
|
||||
ignore_fields = ('cable',)
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
|
|
@ -6,7 +6,7 @@ from dcim.views import PathTraceView
|
|||
from netbox.views import generic
|
||||
from tenancy.views import ObjectContactsView
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.utils import count_related
|
||||
from utilities.query import count_related
|
||||
from utilities.views import register_model_view
|
||||
from . import filtersets, forms, tables
|
||||
from .models import *
|
||||
|
@ -412,7 +412,6 @@ class CircuitContactsView(ObjectContactsView):
|
|||
class CircuitTerminationEditView(generic.ObjectEditView):
|
||||
queryset = CircuitTermination.objects.all()
|
||||
form = forms.CircuitTerminationForm
|
||||
template_name = 'circuits/circuittermination_edit.html'
|
||||
|
||||
|
||||
@register_model_view(CircuitTermination, 'delete')
|
||||
|
|
|
@ -4,7 +4,7 @@ from core.choices import JobStatusChoices
|
|||
from core.models import *
|
||||
from netbox.api.fields import ChoiceField
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
from users.api.nested_serializers import NestedUserSerializer
|
||||
from users.api.serializers import UserSerializer
|
||||
|
||||
__all__ = (
|
||||
'NestedDataFileSerializer',
|
||||
|
@ -32,7 +32,8 @@ class NestedDataFileSerializer(WritableNestedSerializer):
|
|||
class NestedJobSerializer(serializers.ModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail')
|
||||
status = ChoiceField(choices=JobStatusChoices)
|
||||
user = NestedUserSerializer(
|
||||
user = UserSerializer(
|
||||
nested=True,
|
||||
read_only=True
|
||||
)
|
||||
|
||||
|
|
|
@ -156,8 +156,6 @@ class NetBoxAutoSchema(AutoSchema):
|
|||
remove_fields.append(child_name)
|
||||
if isinstance(child, (ChoiceField, WritableNestedSerializer)):
|
||||
properties[child_name] = None
|
||||
elif isinstance(child, ManyRelatedField) and isinstance(child.child_relation, SerializedPKRelatedField):
|
||||
properties[child_name] = None
|
||||
|
||||
if not properties:
|
||||
return None
|
||||
|
|
|
@ -1,73 +1,3 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from core.choices import *
|
||||
from core.models import *
|
||||
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||
from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer
|
||||
from netbox.utils import get_data_backend_choices
|
||||
from users.api.nested_serializers import NestedUserSerializer
|
||||
from .serializers_.data import *
|
||||
from .serializers_.jobs import *
|
||||
from .nested_serializers import *
|
||||
|
||||
__all__ = (
|
||||
'DataFileSerializer',
|
||||
'DataSourceSerializer',
|
||||
'JobSerializer',
|
||||
)
|
||||
|
||||
|
||||
class DataSourceSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name='core-api:datasource-detail'
|
||||
)
|
||||
type = ChoiceField(
|
||||
choices=get_data_backend_choices()
|
||||
)
|
||||
status = ChoiceField(
|
||||
choices=DataSourceStatusChoices,
|
||||
read_only=True
|
||||
)
|
||||
|
||||
# Related object counts
|
||||
file_count = serializers.IntegerField(
|
||||
read_only=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = DataSource
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'type', 'source_url', 'enabled', 'status', 'description', 'comments',
|
||||
'parameters', 'ignore_rules', 'custom_fields', 'created', 'last_updated', 'file_count',
|
||||
]
|
||||
|
||||
|
||||
class DataFileSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name='core-api:datafile-detail'
|
||||
)
|
||||
source = NestedDataSourceSerializer(
|
||||
read_only=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = DataFile
|
||||
fields = [
|
||||
'id', 'url', 'display', 'source', 'path', 'last_updated', 'size', 'hash',
|
||||
]
|
||||
|
||||
|
||||
class JobSerializer(BaseModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail')
|
||||
user = NestedUserSerializer(
|
||||
read_only=True
|
||||
)
|
||||
status = ChoiceField(choices=JobStatusChoices, read_only=True)
|
||||
object_type = ContentTypeField(
|
||||
read_only=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Job
|
||||
fields = [
|
||||
'id', 'url', 'display', 'object_type', 'object_id', 'name', 'status', 'created', 'scheduled', 'interval',
|
||||
'started', 'completed', 'user', 'data', 'error', 'job_id',
|
||||
]
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from core.choices import *
|
||||
from core.models import DataFile, DataSource
|
||||
from netbox.api.fields import ChoiceField, RelatedObjectCountField
|
||||
from netbox.api.serializers import NetBoxModelSerializer
|
||||
from netbox.utils import get_data_backend_choices
|
||||
|
||||
__all__ = (
|
||||
'DataFileSerializer',
|
||||
'DataSourceSerializer',
|
||||
)
|
||||
|
||||
|
||||
class DataSourceSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name='core-api:datasource-detail'
|
||||
)
|
||||
type = ChoiceField(
|
||||
choices=get_data_backend_choices()
|
||||
)
|
||||
status = ChoiceField(
|
||||
choices=DataSourceStatusChoices,
|
||||
read_only=True
|
||||
)
|
||||
|
||||
# Related object counts
|
||||
file_count = RelatedObjectCountField('datafiles')
|
||||
|
||||
class Meta:
|
||||
model = DataSource
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'type', 'source_url', 'enabled', 'status', 'description', 'comments',
|
||||
'parameters', 'ignore_rules', 'custom_fields', 'created', 'last_updated', 'file_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||
|
||||
|
||||
class DataFileSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name='core-api:datafile-detail'
|
||||
)
|
||||
source = DataSourceSerializer(
|
||||
nested=True,
|
||||
read_only=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = DataFile
|
||||
fields = [
|
||||
'id', 'url', 'display', 'source', 'path', 'last_updated', 'size', 'hash',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'path')
|
|
@ -0,0 +1,31 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from core.choices import *
|
||||
from core.models import Job
|
||||
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||
from netbox.api.serializers import BaseModelSerializer
|
||||
from users.api.serializers_.users import UserSerializer
|
||||
|
||||
__all__ = (
|
||||
'JobSerializer',
|
||||
)
|
||||
|
||||
|
||||
class JobSerializer(BaseModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail')
|
||||
user = UserSerializer(
|
||||
nested=True,
|
||||
read_only=True
|
||||
)
|
||||
status = ChoiceField(choices=JobStatusChoices, read_only=True)
|
||||
object_type = ContentTypeField(
|
||||
read_only=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Job
|
||||
fields = [
|
||||
'id', 'url', 'display', 'object_type', 'object_id', 'name', 'status', 'created', 'scheduled', 'interval',
|
||||
'started', 'completed', 'user', 'data', 'error', 'job_id',
|
||||
]
|
||||
brief_fields = ('url', 'created', 'completed', 'user', 'status')
|
|
@ -9,7 +9,6 @@ from rest_framework.viewsets import ReadOnlyModelViewSet
|
|||
from core import filtersets
|
||||
from core.models import *
|
||||
from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet
|
||||
from utilities.utils import count_related
|
||||
from . import serializers
|
||||
|
||||
|
||||
|
@ -22,9 +21,7 @@ class CoreRootView(APIRootView):
|
|||
|
||||
|
||||
class DataSourceViewSet(NetBoxModelViewSet):
|
||||
queryset = DataSource.objects.annotate(
|
||||
file_count=count_related(DataFile, 'source')
|
||||
)
|
||||
queryset = DataSource.objects.all()
|
||||
serializer_class = serializers.DataSourceSerializer
|
||||
filterset_class = filtersets.DataSourceFilterSet
|
||||
|
||||
|
@ -45,7 +42,7 @@ class DataSourceViewSet(NetBoxModelViewSet):
|
|||
|
||||
|
||||
class DataFileViewSet(NetBoxReadOnlyModelViewSet):
|
||||
queryset = DataFile.objects.defer('data').prefetch_related('source')
|
||||
queryset = DataFile.objects.defer('data')
|
||||
serializer_class = serializers.DataFileSerializer
|
||||
filterset_class = filtersets.DataFileFilterSet
|
||||
|
||||
|
@ -54,6 +51,6 @@ class JobViewSet(ReadOnlyModelViewSet):
|
|||
"""
|
||||
Retrieve a list of job results
|
||||
"""
|
||||
queryset = Job.objects.prefetch_related('user')
|
||||
queryset = Job.objects.all()
|
||||
serializer_class = serializers.JobSerializer
|
||||
filterset_class = filtersets.JobFilterSet
|
||||
|
|
|
@ -16,5 +16,9 @@ class CoreConfig(AppConfig):
|
|||
name = "core"
|
||||
|
||||
def ready(self):
|
||||
from core.api import schema # noqa
|
||||
from netbox.models.features import register_models
|
||||
from . import data_backends, search
|
||||
from core.api import schema # noqa: E402
|
||||
|
||||
# Register models
|
||||
register_models(*self.get_models())
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rq.job import JobStatus
|
||||
|
||||
__all__ = (
|
||||
'RQ_TASK_STATUSES',
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Status:
|
||||
label: str
|
||||
color: str
|
||||
|
||||
|
||||
RQ_TASK_STATUSES = {
|
||||
JobStatus.QUEUED: Status(_('Queued'), 'cyan'),
|
||||
JobStatus.FINISHED: Status(_('Finished'), 'green'),
|
||||
JobStatus.FAILED: Status(_('Failed'), 'red'),
|
||||
JobStatus.STARTED: Status(_('Started'), 'blue'),
|
||||
JobStatus.DEFERRED: Status(_('Deferred'), 'gray'),
|
||||
JobStatus.SCHEDULED: Status(_('Scheduled'), 'purple'),
|
||||
JobStatus.STOPPED: Status(_('Stopped'), 'orange'),
|
||||
JobStatus.CANCELED: Status(_('Cancelled'), 'yellow'),
|
||||
}
|
|
@ -28,7 +28,7 @@ class DataSourceFilterSet(NetBoxModelFilterSet):
|
|||
|
||||
class Meta:
|
||||
model = DataSource
|
||||
fields = ('id', 'name', 'enabled', 'description')
|
||||
fields = ('id', 'name', 'enabled', 'description', 'source_url', 'last_synced')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
|
@ -115,7 +115,7 @@ class JobFilterSet(BaseFilterSet):
|
|||
|
||||
class Meta:
|
||||
model = Job
|
||||
fields = ('id', 'object_type', 'object_id', 'name', 'interval', 'status', 'user')
|
||||
fields = ('id', 'object_type', 'object_id', 'name', 'interval', 'status', 'user', 'job_id')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
|
@ -134,9 +134,7 @@ class ConfigRevisionFilterSet(BaseFilterSet):
|
|||
|
||||
class Meta:
|
||||
model = ConfigRevision
|
||||
fields = [
|
||||
'id',
|
||||
]
|
||||
fields = ('id', 'created', 'comment')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
|
|
|
@ -5,6 +5,7 @@ from core.models import *
|
|||
from netbox.forms import NetBoxModelBulkEditForm
|
||||
from netbox.utils import get_data_backend_choices
|
||||
from utilities.forms.fields import CommentField
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.widgets import BulkEditNullBooleanSelect
|
||||
|
||||
__all__ = (
|
||||
|
@ -41,7 +42,7 @@ class DataSourceBulkEditForm(NetBoxModelBulkEditForm):
|
|||
|
||||
model = DataSource
|
||||
fieldsets = (
|
||||
(None, ('type', 'enabled', 'description', 'comments', 'parameters', 'ignore_rules')),
|
||||
FieldSet('type', 'enabled', 'description', 'comments', 'parameters', 'ignore_rules'),
|
||||
)
|
||||
nullable_fields = (
|
||||
'description', 'description', 'parameters', 'comments', 'parameters', 'ignore_rules',
|
||||
|
|
|
@ -9,7 +9,8 @@ from netbox.forms.mixins import SavedFiltersMixin
|
|||
from netbox.utils import get_data_backend_choices
|
||||
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm
|
||||
from utilities.forms.fields import ContentTypeChoiceField, DynamicModelMultipleChoiceField
|
||||
from utilities.forms.widgets import APISelectMultiple, DateTimePicker
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.widgets import DateTimePicker
|
||||
|
||||
__all__ = (
|
||||
'ConfigRevisionFilterForm',
|
||||
|
@ -22,8 +23,8 @@ __all__ = (
|
|||
class DataSourceFilterForm(NetBoxModelFilterSetForm):
|
||||
model = DataSource
|
||||
fieldsets = (
|
||||
(None, ('q', 'filter_id')),
|
||||
(_('Data Source'), ('type', 'status')),
|
||||
FieldSet('q', 'filter_id'),
|
||||
FieldSet('type', 'status', name=_('Data Source')),
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
label=_('Type'),
|
||||
|
@ -47,8 +48,8 @@ class DataSourceFilterForm(NetBoxModelFilterSetForm):
|
|||
class DataFileFilterForm(NetBoxModelFilterSetForm):
|
||||
model = DataFile
|
||||
fieldsets = (
|
||||
(None, ('q', 'filter_id')),
|
||||
(_('File'), ('source_id',)),
|
||||
FieldSet('q', 'filter_id'),
|
||||
FieldSet('source_id', name=_('File')),
|
||||
)
|
||||
source_id = DynamicModelMultipleChoiceField(
|
||||
queryset=DataSource.objects.all(),
|
||||
|
@ -59,16 +60,16 @@ class DataFileFilterForm(NetBoxModelFilterSetForm):
|
|||
|
||||
class JobFilterForm(SavedFiltersMixin, FilterForm):
|
||||
fieldsets = (
|
||||
(None, ('q', 'filter_id')),
|
||||
(_('Attributes'), ('object_type', 'status')),
|
||||
(_('Creation'), (
|
||||
FieldSet('q', 'filter_id'),
|
||||
FieldSet('object_type', 'status', name=_('Attributes')),
|
||||
FieldSet(
|
||||
'created__before', 'created__after', 'scheduled__before', 'scheduled__after', 'started__before',
|
||||
'started__after', 'completed__before', 'completed__after', 'user',
|
||||
)),
|
||||
'started__after', 'completed__before', 'completed__after', 'user', name=_('Creation')
|
||||
),
|
||||
)
|
||||
object_type = ContentTypeChoiceField(
|
||||
label=_('Object Type'),
|
||||
queryset=ContentType.objects.with_feature('jobs'),
|
||||
queryset=ObjectType.objects.with_feature('jobs'),
|
||||
required=False,
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
|
@ -119,14 +120,11 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
|
|||
user = DynamicModelMultipleChoiceField(
|
||||
queryset=get_user_model().objects.all(),
|
||||
required=False,
|
||||
label=_('User'),
|
||||
widget=APISelectMultiple(
|
||||
api_url='/api/users/users/',
|
||||
)
|
||||
label=_('User')
|
||||
)
|
||||
|
||||
|
||||
class ConfigRevisionFilterForm(SavedFiltersMixin, FilterForm):
|
||||
fieldsets = (
|
||||
(None, ('q', 'filter_id')),
|
||||
FieldSet('q', 'filter_id'),
|
||||
)
|
||||
|
|