Compare commits

..

1 Commits

607 changed files with 138839 additions and 79706 deletions

View File

@@ -2,7 +2,7 @@
name: ✨ Feature Request
type: Feature
description: Propose a new NetBox feature or enhancement
labels: ["netbox", "type: feature", "status: needs triage"]
labels: ["type: feature", "status: needs triage"]
body:
- type: markdown
attributes:
@@ -15,7 +15,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v4.4.8
placeholder: v4.4.2
validations:
required: true
- type: dropdown

View File

@@ -2,7 +2,7 @@
name: 🐛 Bug Report
type: Bug
description: Report a reproducible bug in the current release of NetBox
labels: ["netbox", "type: bug", "status: needs triage"]
labels: ["type: bug", "status: needs triage"]
body:
- type: markdown
attributes:
@@ -27,7 +27,7 @@ body:
attributes:
label: NetBox Version
description: What version of NetBox are you currently running?
placeholder: v4.4.8
placeholder: v4.4.2
validations:
required: true
- type: dropdown
@@ -35,9 +35,9 @@ body:
label: Python Version
description: What version of Python are you currently running?
options:
- "3.10"
- "3.11"
- "3.12"
- "3.13"
- "3.14"
validations:
required: true
- type: textarea

View File

@@ -2,7 +2,7 @@
name: 📖 Documentation Change
type: Documentation
description: Suggest an addition or modification to the NetBox documentation
labels: ["netbox", "type: documentation", "status: needs triage"]
labels: ["type: documentation", "status: needs triage"]
body:
- type: dropdown
attributes:

View File

@@ -2,7 +2,7 @@
name: 🌍 Translation
type: Translation
description: Request support for a new language in the user interface
labels: ["netbox", "type: translation"]
labels: ["type: translation"]
body:
- type: markdown
attributes:

View File

@@ -2,7 +2,7 @@
name: 🏡 Housekeeping
type: Housekeeping
description: A change pertaining to the codebase itself (developers only)
labels: ["netbox", "type: housekeeping"]
labels: ["type: housekeeping"]
body:
- type: markdown
attributes:

View File

@@ -2,7 +2,7 @@
name: 🗑️ Deprecation
type: Deprecation
description: The removal of an existing feature or resource
labels: ["netbox", "type: deprecation"]
labels: ["type: deprecation"]
body:
- type: textarea
attributes:

View File

@@ -1,11 +1,3 @@
paths-ignore:
# Ignore compiled JS
- netbox/project-static/dist
query-filters:
# Exclude py/url-redirection: NetBox uses safe_for_redirect() wrapper function
# which validates all redirects via Django's url_has_allowed_host_and_scheme().
# CodeQL's taint tracking doesn't recognize wrapper functions without custom
# query configuration. See #20484.
- exclude:
id: py/url-redirection

View File

@@ -31,7 +31,7 @@ jobs:
NETBOX_CONFIGURATION: netbox.configuration_testing
strategy:
matrix:
python-version: ['3.12', '3.13', '3.14']
python-version: ['3.10', '3.11', '3.12']
node-version: ['20.x']
services:
redis:

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.1
rev: v0.6.9
hooks:
- id: ruff
name: "Ruff linter"
@@ -21,6 +21,14 @@ repos:
language: system
pass_filenames: false
types: [python]
- id: openapi-check
name: "Validate OpenAPI schema"
description: "Check for any unexpected changes to the OpenAPI schema"
files: api/.*\.py$
entry: scripts/verify-openapi.sh
language: system
pass_filenames: false
types: [python]
- id: mkdocs-build
name: "Build documentation"
description: "Build the documentation with mkdocs"

View File

@@ -12,7 +12,9 @@ django-cors-headers
# Runtime UI tool for debugging Django
# https://github.com/jazzband/django-debug-toolbar/blob/main/docs/changes.rst
django-debug-toolbar
# django-debug-toolbar v6.0.0 raises "Attribute Error at /: 'function' object has no attribute 'set'"
# see https://github.com/netbox-community/netbox/issues/19974
django-debug-toolbar==5.2.0
# Library for writing reusable URL query filters
# https://github.com/carltongibson/django-filter/blob/main/CHANGES.rst
@@ -69,8 +71,7 @@ django-timezone-field
# A REST API framework for Django projects
# https://www.django-rest-framework.org/community/release-notes/
# TODO: Re-evaluate the monkey-patch of get_unique_validators() before upgrading
djangorestframework==3.16.1
djangorestframework
# Sane and flexible OpenAPI 3 schema generation for Django REST framework.
# https://github.com/tfranzel/drf-spectacular/blob/master/CHANGELOG.rst
@@ -166,8 +167,7 @@ strawberry-graphql-django
svgwrite
# Tabular dataset library (for table-based exports)
# Current: https://github.com/jazzband/tablib/releases
# Previous: https://github.com/jazzband/tablib/blob/master/HISTORY.md
# https://github.com/jazzband/tablib/blob/master/HISTORY.md
tablib
# Timezone data (required by django-timezone-field on Python 3.9+)

View File

@@ -186,7 +186,6 @@
"usb-3-micro-b",
"molex-micro-fit-1x2",
"molex-micro-fit-2x2",
"molex-micro-fit-2x3",
"molex-micro-fit-2x4",
"dc-terminal",
"saf-d-grid",
@@ -294,7 +293,6 @@
"usb-c",
"molex-micro-fit-1x2",
"molex-micro-fit-2x2",
"molex-micro-fit-2x3",
"molex-micro-fit-2x4",
"dc-terminal",
"eaton-c39",

File diff suppressed because one or more lines are too long

View File

@@ -2,7 +2,7 @@
## Local Authentication
Local user accounts and groups can be created in NetBox under the "Authentication" section in the "Admin" menu.
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 individual users and/or groups as needed.

View File

@@ -1,15 +1,5 @@
# GraphQL API Parameters
## GRAPHQL_DEFAULT_VERSION
!!! note "This parameter was introduced in NetBox v4.5."
Default: `1`
Designates the default version of the GraphQL API served by `/graphql/`. To access a specific version, append the version number to the URL, e.g. `/graphql/v2/`.
---
## GRAPHQL_ENABLED
!!! tip "Dynamic Configuration Parameter"

View File

@@ -35,7 +35,6 @@ Some configuration parameters are primarily controlled via NetBox's admin interf
* [`POWERFEED_DEFAULT_MAX_UTILIZATION`](./default-values.md#powerfeed_default_max_utilization)
* [`POWERFEED_DEFAULT_VOLTAGE`](./default-values.md#powerfeed_default_voltage)
* [`PREFER_IPV4`](./miscellaneous.md#prefer_ipv4)
* [`PROTECTION_RULES`](./data-validation.md#protection_rules)
* [`RACK_ELEVATION_DEFAULT_UNIT_HEIGHT`](./default-values.md#rack_elevation_default_unit_height)
* [`RACK_ELEVATION_DEFAULT_UNIT_WIDTH`](./default-values.md#rack_elevation_default_unit_width)

View File

@@ -53,16 +53,6 @@ Sets content for the top banner in the user interface.
---
## COPILOT_ENABLED
!!! tip "Dynamic Configuration Parameter"
Default: `True`
Enables or disables the [NetBox Copilot](https://netboxlabs.com/docs/copilot/) agent globally. When enabled, users can opt to toggle the agent individually.
---
## CENSUS_REPORTING_ENABLED
Default: `True`

View File

@@ -127,3 +127,19 @@ The list of groups that promote an remote User to Superuser on Login. If group i
Default: `[]` (Empty list)
The list of users that get promoted to Superuser on Login. If user isn't present in list on next Login, the Role gets revoked. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` )
---
## REMOTE_AUTH_STAFF_GROUPS
Default: `[]` (Empty list)
The list of groups that promote an remote User to Staff on Login. If group isn't present on next Login, the Role gets revoked. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` )
---
## REMOTE_AUTH_STAFF_USERS
Default: `[]` (Empty list)
The list of users that get promoted to Staff on Login. If user isn't present in list on next Login, the Role gets revoked. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` )

View File

@@ -23,31 +23,6 @@ ALLOWED_HOSTS = ['*']
---
## API_TOKEN_PEPPERS
!!! info "This parameter was introduced in NetBox v4.5."
[Cryptographic peppers](https://en.wikipedia.org/wiki/Pepper_(cryptography)) are employed to generate hashes of sensitive values on the server. This parameter defines the peppers used to hash v2 API tokens in NetBox. You must define at least one pepper before creating a v2 API token. See the [API documentation](../integrations/rest-api.md#authentication) for further information about how peppers are used.
```python
API_TOKEN_PEPPERS = {
# DO NOT USE THIS EXAMPLE PEPPER IN PRODUCTION
1: 'kp7ht*76fiQAhUi5dHfASLlYUE_S^gI^(7J^K5M!LfoH@vl&b_',
}
```
!!! warning "Peppers are sensitive"
Treat pepper values as extremely sensitive. Consider populating peppers from environment variables at initialization time rather than defining them in the configuration file, if feasible.
Peppers must be at least 50 characters in length and should comprise a random string with a diverse character set. Consider using the Python script at `$INSTALL_ROOT/netbox/generate_secret_key.py` to generate a pepper value.
It is recommended to start with a pepper ID of `1`. Additional peppers can be introduced later as needed to begin rotating token hashes.
!!! tip
Although NetBox will run without `API_TOKEN_PEPPERS` defined, the use of v2 API tokens will be unavailable.
---
## DATABASE
!!! warning "Legacy Configuration Parameter"

View File

@@ -1,5 +1,16 @@
# Security & Authentication Parameters
## ALLOW_TOKEN_RETRIEVAL
Default: `False`
!!! note
The default value of this parameter changed from `True` to `False` in NetBox v4.3.0.
If disabled, the values of API tokens will not be displayed after each token's initial creation. A user **must** record the value of a token prior to its creation, or it will be lost. Note that this affects _all_ users, regardless of assigned permissions.
---
## ALLOWED_URL_SCHEMES
!!! tip "Dynamic Configuration Parameter"
@@ -81,7 +92,7 @@ If `True`, the cookie employed for cross-site request forgery (CSRF) protection
Default: `[]`
Defines a list of trusted origins for unsafe (e.g. `POST`) requests. This is a pass-through to Django's [`CSRF_TRUSTED_ORIGINS`](https://docs.djangoproject.com/en/stable/ref/settings/#csrf-trusted-origins) setting. Note that each host listed must specify a scheme (e.g. `http://` or `https://`).
Defines a list of trusted origins for unsafe (e.g. `POST`) requests. This is a pass-through to Django's [`CSRF_TRUSTED_ORIGINS`](https://docs.djangoproject.com/en/stable/ref/settings/#csrf-trusted-origins) setting. Note that each host listed must specify a scheme (e.g. `http://` or `https://).
```python
CSRF_TRUSTED_ORIGINS = (

View File

@@ -232,9 +232,6 @@ STORAGES = {
},
"scripts": {
"BACKEND": "extras.storage.ScriptFileSystemStorage",
"OPTIONS": {
"allow_overwrite": True,
},
},
}
```
@@ -250,7 +247,6 @@ STORAGES = {
"OPTIONS": {
'access_key': 'access key',
'secret_key': 'secret key',
"allow_overwrite": True,
}
},
}

View File

@@ -95,7 +95,7 @@ An example fieldset definition is provided below:
```python
class MyScript(Script):
class Meta(Script.Meta):
class Meta:
fieldsets = (
('First group', ('field1', 'field2', 'field3')),
('Second group', ('field4', 'field5')),
@@ -131,6 +131,17 @@ self.log_info(f"Running as user {username} (IP: {ip_address})...")
For a complete list of available request parameters, please see the [Django documentation](https://docs.djangoproject.com/en/stable/ref/request-response/).
## Reading Data from Files
The Script class provides two convenience methods for reading data from files:
* `load_yaml`
* `load_json`
These two methods will load data in YAML or JSON format, respectively, from files within the local path (i.e. `SCRIPTS_ROOT`).
**Note:** These convenience methods are deprecated and will be removed in NetBox v4.4. These only work if running scripts within the local path, they will not work if using a storage other than ScriptFileSystemStorage.
## Logging
The Script object provides a set of convenient functions for recording messages at different severity levels:
@@ -393,61 +404,6 @@ A complete date & time. Returns a `datetime.datetime` object.
Custom scripts can be run via the web UI by navigating to the script, completing any required form data, and clicking the "run script" button. It is possible to schedule a script to be executed at specified time in the future. A scheduled script can be canceled by deleting the associated job result object.
#### Prefilling variables via URL parameters
Script form fields can be prefilled by appending query parameters to the script URL. Each parameter name must match the variable name defined on the script class. Prefilled values are treated as initial values and can be edited before execution. Multiple values can be supplied by repeating the same parameter. Query values must be percentencoded where required (for example, spaces as `%20`).
Examples:
For string and integer variables, when a script defines:
```python
from extras.scripts import Script, StringVar, IntegerVar
class MyScript(Script):
name = StringVar()
count = IntegerVar()
```
the following URL prefills the `name` and `count` fields:
```
https://<netbox>/extras/scripts/<script_id>/?name=Branch42&count=3
```
For object variables (`ObjectVar`), supply the objects primary key (PK):
```
https://<netbox>/extras/scripts/<script_id>/?device=1
```
If an object ID cannot be resolved or the object is not visible to the requesting user, the field remains unpopulated.
Supported variable types:
| Variable class | Expected input | Example query string |
|--------------------------|---------------------------------|---------------------------------------------|
| `StringVar` | string (percentencoded) | `?name=Branch42` |
| `TextVar` | string (percentencoded) | `?notes=Initial%20value` |
| `IntegerVar` | integer | `?count=3` |
| `DecimalVar` | decimal number | `?ratio=0.75` |
| `BooleanVar` | value → `True`; empty → `False` | `?enabled=true` (True), `?enabled=` (False) |
| `ChoiceVar` | choice value (not label) | `?role=edge` |
| `MultiChoiceVar` | choice values (repeat) | `?roles=edge&roles=core` |
| `ObjectVar(Device)` | PK (integer) | `?device=1` |
| `MultiObjectVar(Device)` | PKs (repeat) | `?devices=1&devices=2` |
| `IPAddressVar` | IP address | `?ip=198.51.100.10` |
| `IPAddressWithMaskVar` | IP address with mask | `?addr=192.0.2.1/24` |
| `IPNetworkVar` | IP network prefix | `?network=2001:db8::/64` |
| `DateVar` | date `YYYY-MM-DD` | `?date=2025-01-05` |
| `DateTimeVar` | ISO datetime | `?when=2025-01-05T14:30:00` |
| `FileVar` | — (not supported) | — |
!!! note
- The parameter names above are examples; use the actual variable attribute names defined by the script.
- For `BooleanVar`, only an empty value (`?enabled=`) unchecks the box; any other value including `false` or `0` checks it.
- File uploads (`FileVar`) cannot be prefilled via URL parameters.
### Via the API
To run a script via the REST API, issue a POST request to the script's endpoint specifying the form data and commitment. For example, to run a script named `example.MyReport`, we would make a request such as the following:
@@ -499,7 +455,7 @@ from extras.scripts import *
class NewBranchScript(Script):
class Meta(Script.Meta):
class Meta:
name = "New Branch"
description = "Provision a new branch site"
field_order = ['site_name', 'switch_count', 'switch_model']

View File

@@ -20,10 +20,6 @@ A dictionary mapping data backend types to their respective classes. These are u
Stores registration made using `netbox.denormalized.register()`. For each model, a list of related models and their field mappings is maintained to facilitate automatic updates.
### `filtersets`
A dictionary mapping each model (identified by its app and label) to its filterset class, if one has been registered for it. Filtersets are registered using the `@register_filterset` decorator.
### `model_features`
A dictionary of model features (e.g. custom fields, tags, etc.) mapped to the functions used to qualify a model as supporting each feature. Model features are registered using the `register_model_feature()` function in `netbox.utils`.

View File

@@ -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.12 or later
* Python 3.10 or later
### 1. Fork the Repo

View File

@@ -12,7 +12,7 @@ Depending on its classification, each NetBox model may support various features
| Feature | Feature Mixin | Registry Key | Description |
|------------------------------------------------------------|-------------------------|---------------------|-----------------------------------------------------------------------------------------|
| [Bookmarks](../features/user-preferences.md#bookmarks) | `BookmarksMixin` | `bookmarks` | These models can be bookmarked natively in the user interface |
| [Bookmarks](../features/customization.md#bookmarks) | `BookmarksMixin` | `bookmarks` | These models can be bookmarked natively in the user interface |
| [Change logging](../features/change-logging.md) | `ChangeLoggingMixin` | `change_logging` | Changes to these objects are automatically recorded in the change log |
| Cloning | `CloningMixin` | `cloning` | Provides the `clone()` method to prepare a copy |
| [Contacts](../features/contacts.md) | `ContactsMixin` | `contacts` | Contacts can be associated with these models |

View File

@@ -6,14 +6,10 @@ For enduser guidance on resetting saved table layouts, see [Features > User P
## Available Preferences
| Name | Description |
|----------------------------|---------------------------------------------------------------|
| `csv_delimiter` | The delimiting character used when exporting CSV data |
| `data_format` | Preferred format when rendering raw data (JSON or YAML) |
| `locale.language` | The language selected for UI translation |
| `pagination.per_page` | The number of items to display per page of a paginated table |
| `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.copilot_enabled` | Toggles the NetBox Copilot AI agent |
| `ui.tables.striping` | Toggles visual striping of tables in the UI |
| Name | Description |
|--------------------------|---------------------------------------------------------------|
| data_format | Preferred format when rendering raw data (JSON or YAML) |
| pagination.per_page | The number of items to display per page of a paginated table |
| 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 |

View File

@@ -8,7 +8,7 @@ NetBox's REST API, powered by the [Django REST Framework](https://www.django-res
```no-highlight
curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Authorization: Token $TOKEN" \
-H "Content-Type: application/json" \
http://netbox/api/ipam/prefixes/ \
--data '{"prefix": "192.0.2.0/24", "site": {"name": "Branch 12"}}'

View File

@@ -90,10 +90,3 @@ http://netbox:8000/api/extras/config-templates/123/render/ \
"bar": 123
}'
```
!!! note "Permissions"
Rendering configuration templates via the REST API requires appropriate permissions for the relevant object type:
* To render a device's configuration via `/api/dcim/devices/{id}/render-config/`, assign a permission for "DCIM > Device" with the `render_config` action.
* To render a virtual machine's configuration via `/api/virtualization/virtual-machines/{id}/render-config/`, assign a permission for "Virtualization > Virtual Machine" with the `render_config` action.
* To render a config template directly via `/api/extras/config-templates/{id}/render/`, assign a permission for "Extras > Config Template" with the `render` action.

View File

@@ -1,10 +0,0 @@
# Resource Ownership
!!! info "This feature was introduced in NetBox v4.5."
Most objects in NetBox can be assigned an owner. An owner is a set of users and/or groups who are responsible for the administration of associated objects. For example, you might designate the operations team at a site as the owner for all prefixes and VLANs deployed at that site. The users and groups assigned to an owner are referred to as its members.
!!! note
Ownership of an object should not be confused with the concept of [tenancy](./tenancy.md), which indicates the dedication of an object to a specific tenant. For instance, a tenant might represent a customer served by the object, whereas an owner typically represents a set of internal users responsible for the management of the object.
Owners can be organized into groups for easier management.

View File

@@ -1,6 +1,6 @@
# Tenancy
Most core objects within NetBox's data model support _tenancy_. This is the association of an object with a particular tenant to convey assignment or dependency. For example, an enterprise might represent its internal business units as tenants, whereas a managed services provider might create a tenant in NetBox to represent each of its customers.
Most core objects within NetBox's data model support _tenancy_. This is the association of an object with a particular tenant to convey ownership or dependency. For example, an enterprise might represent its internal business units as tenants, whereas a managed services provider might create a tenant in NetBox to represent each of its customers.
```mermaid
flowchart TD
@@ -19,36 +19,20 @@ Tenants can be grouped by any logic that your use case demands, and groups can b
Typically, the tenant model is used to represent a customer or internal organization, however it can be used for whatever purpose meets your needs.
Most core objects within NetBox can be assigned to a particular tenant, so this model provides a very convenient way to correlate resource allocation across object types. For example, each of your customers might have its own racks, devices, IP addresses, circuits and so on: These can all be easily tracked via tenant assignment.
Most core objects within NetBox can be assigned to particular tenant, so this model provides a very convenient way to correlate ownership across object types. For example, each of your customers might have its own racks, devices, IP addresses, circuits and so on: These can all be easily tracked via tenant assignment.
The following objects can be assigned to tenants:
* Circuits
* Circuit groups
* Virtual circuits
* Cables
* Devices
* Virtual device contexts
* Power feeds
* Sites
* Racks
* Rack reservations
* Sites
* Locations
* ASNs
* ASN ranges
* Aggregates
* Devices
* VRFs
* Prefixes
* IP ranges
* IP addresses
* VLANs
* VLAN groups
* VRFs
* Route targets
* Circuits
* Clusters
* Virtual machines
* L2VPNs
* Tunnels
* Wireless LANs
* Wireless links
Tenancy represents the dedication of an object to a specific tenant. As such, each object may only be assigned to a single tenant. For example, if you have a firewall dedicated to a particular customer, you would assign it to the tenant which represents that customer. However, if the firewall serves multiple customers, it doesn't *belong* to any particular customer, so the assignment of a tenant would not be appropriate.
Tenant assignment is used to signify the ownership of an object in NetBox. As such, each object may only be owned by a single tenant. For example, if you have a firewall dedicated to a particular customer, you would assign it to the tenant which represents that customer. However, if the firewall serves multiple customers, it doesn't *belong* to any particular customer, so tenant assignment would not be appropriate.

View File

@@ -34,6 +34,9 @@ Sets the default number of rows displayed on paginated tables.
### Paginator placement
Controls where pagination controls are rendered relative to a table.
### HTMX navigation (experimental)
Enables partialpage navigation for supported views. Disable this preference if unexpected behavior is observed.
### Striped table rows
Toggles alternating row backgrounds on tables.

View File

@@ -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.12 or later required"
NetBox supports only Python 3.12 or later.
!!! warning "Python 3.10 or later required"
NetBox supports Python 3.10, 3.11, and 3.12.
```no-highlight
sudo apt install -y python3 python3-pip python3-venv python3-dev \
@@ -15,7 +15,7 @@ build-essential libxml2-dev libxslt1-dev libffi-dev libpq-dev \
libssl-dev zlib1g-dev
```
Before continuing, check that your installed Python version is at least 3.12:
Before continuing, check that your installed Python version is at least 3.10:
```no-highlight
python3 -V
@@ -120,23 +120,6 @@ If you are not yet sure what the domain name and/or IP address of the NetBox ins
ALLOWED_HOSTS = ['*']
```
### API_TOKEN_PEPPERS
Define at least one random cryptographic pepper, identified by a numeric ID starting at 1. This will be used to generate SHA256 checksums for API tokens.
```python
API_TOKEN_PEPPERS = {
# DO NOT USE THIS EXAMPLE PEPPER IN PRODUCTION
1: 'kp7ht*76fiQAhUi5dHfASLlYUE_S^gI^(7J^K5M!LfoH@vl&b_',
}
```
!!! tip
As with [`SECRET_KEY`](#secret_key) below, you can use the `generate_secret_key.py` script to generate a random pepper:
```no-highlight
python3 ../generate_secret_key.py
```
### DATABASES
This parameter holds the PostgreSQL database configuration details. The default database must be defined; additional databases may be defined as needed e.g. by plugins.
@@ -252,10 +235,10 @@ Once NetBox has been configured, we're ready to proceed with the actual installa
sudo /opt/netbox/upgrade.sh
```
Note that **Python 3.12 or later is required** for NetBox v4.5 and later releases. If the default Python installation on your server is set to a lesser version, pass the path to the supported installation as an environment variable named `PYTHON`. (Note that the environment variable must be passed _after_ the `sudo` command.)
Note that **Python 3.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.12 /opt/netbox/upgrade.sh
sudo PYTHON=/usr/bin/python3.10 /opt/netbox/upgrade.sh
```
!!! note

View File

@@ -60,3 +60,6 @@ You should see output similar to the following:
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.
!!! note
There is a bug in the current stable release of gunicorn (v21.2.0) where automatic restarts of the worker processes can result in 502 errors under heavy load. (See [gunicorn bug #3038](https://github.com/benoitc/gunicorn/issues/3038) for more detail.) Users who encounter this issue may opt to downgrade to an earlier, unaffected release of gunicorn (`pip install gunicorn==20.1.0`). Note, however, that this earlier release does not officially support Python 3.11.

View File

@@ -121,6 +121,7 @@ AUTH_LDAP_MIRROR_GROUPS = True
# Define special user types using groups. Exercise great caution when assigning superuser status.
AUTH_LDAP_USER_FLAGS_BY_GROUP = {
"is_active": "cn=active,ou=groups,dc=example,dc=com",
"is_staff": "cn=staff,ou=groups,dc=example,dc=com",
"is_superuser": "cn=superuser,ou=groups,dc=example,dc=com"
}
@@ -133,6 +134,7 @@ AUTH_LDAP_CACHE_TIMEOUT = 3600
```
* `is_active` - All users must be mapped to at least this group to enable authentication. Without this, users cannot log in.
* `is_staff` - Users mapped to this group are enabled for access to the administration tools; this is the equivalent of checking the "staff status" box on a manually created user. This doesn't grant any specific permissions.
* `is_superuser` - Users mapped to this group will be granted superuser status. Superusers are implicitly granted all permissions.
!!! warning
@@ -246,6 +248,7 @@ AUTH_LDAP_MIRROR_GROUPS = True
# Define special user types using groups. Exercise great caution when assigning superuser status.
AUTH_LDAP_USER_FLAGS_BY_GROUP = {
"is_active": "cn=active,ou=groups,dc=example,dc=com",
"is_staff": "cn=staff,ou=groups,dc=example,dc=com",
"is_superuser": "cn=superuser,ou=groups,dc=example,dc=com"
}

View File

@@ -27,7 +27,7 @@ The following sections detail how to set up a new instance of NetBox:
| Dependency | Supported Versions |
|------------|--------------------|
| Python | 3.12, 3.13, 3.14 |
| Python | 3.10, 3.11, 3.12 |
| PostgreSQL | 14+ |
| Redis | 4.0+ |

View File

@@ -19,7 +19,7 @@ NetBox requires the following dependencies:
| Dependency | Supported Versions |
|------------|--------------------|
| Python | 3.12, 3.13, 3.14 |
| Python | 3.10, 3.11, 3.12 |
| PostgreSQL | 14+ |
| Redis | 4.0+ |
@@ -27,7 +27,6 @@ NetBox requires the following dependencies:
| NetBox Version | Python min | Python max | PostgreSQL min | Redis min | Documentation |
|:--------------:|:----------:|:----------:|:--------------:|:---------:|:-----------------------------------------------------------------------------------------:|
| 4.5 | 3.12 | 3.14 | 14 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v4.5.0/docs/installation/index.md) |
| 4.4 | 3.10 | 3.12 | 14 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v4.4.0/docs/installation/index.md) |
| 4.3 | 3.10 | 3.12 | 14 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v4.3.0/docs/installation/index.md) |
| 4.2 | 3.10 | 3.12 | 13 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v4.2.0/docs/installation/index.md) |
@@ -131,7 +130,7 @@ sudo ./upgrade.sh
If the default version of Python is not at least 3.10, you'll need to pass the path to a supported Python version as an environment variable when calling the upgrade script. For example:
```no-highlight
sudo PYTHON=/usr/bin/python3.12 ./upgrade.sh
sudo PYTHON=/usr/bin/python3.10 ./upgrade.sh
```
!!! note

View File

@@ -80,7 +80,7 @@ Likewise, the site, rack, and device objects are located under the "DCIM" applic
The full hierarchy of available endpoints can be viewed by navigating to the API root in a web browser.
Each model generally has two views associated with it: a list view and a detail view. The list view is used to retrieve a list of multiple objects and to create new objects. The detail view is used to retrieve, update, or delete a single existing object. All objects are referenced by their numeric primary key (`id`).
Each model generally has two views associated with it: a list view and a detail view. The list view is used to retrieve a list of multiple objects and to create new objects. The detail view is used to retrieve, update, or delete an single existing object. All objects are referenced by their numeric primary key (`id`).
* `/api/dcim/devices/` - List existing devices or create a new device
* `/api/dcim/devices/123/` - Retrieve, update, or delete the device with ID 123
@@ -653,22 +653,18 @@ The NetBox REST API primarily employs token-based authentication. For convenienc
### Tokens
A token is a secret, unique identifier mapped to a NetBox user account. Each user may have one or more tokens which he or she can use for authentication when making REST API requests. To create a token, navigate to the API tokens page under your user profile. When creating a token, NetBox will automatically populate a randomly-generated token value.
!!! note "Tokens cannot be retrieved once created"
Once a token has been created, its plaintext value cannot be retrieved. For this reason, you must take care to securely record the token locally immediately upon its creation. If a token plaintext is lost, it cannot be recovered: A new token must be created.
A token is a unique identifier mapped to a NetBox user account. Each user may have one or more tokens which he or she can use for authentication when making REST API requests. To create a token, navigate to the API tokens page under your user profile.
By default, all users can create and manage their own REST API tokens under the user control panel in the UI or via the REST API. This ability can be disabled by overriding the [`DEFAULT_PERMISSIONS`](../configuration/security.md#default_permissions) configuration parameter.
Each token contains a 160-bit key represented as 40 hexadecimal characters. When creating a token, you'll typically leave the key field blank so that a random key will be automatically generated. However, NetBox allows you to specify a key in case you need to restore a previously deleted token to operation.
Additionally, a token can be set to expire at a specific time. This can be useful if an external client needs to be granted temporary access to NetBox.
#### v1 and v2 Tokens
!!! info "Restricting Token Retrieval"
The ability to retrieve the key value of a previously-created API token can be restricted by disabling the [`ALLOW_TOKEN_RETRIEVAL`](../configuration/security.md#allow_token_retrieval) configuration parameter.
Beginning with NetBox v4.5, two versions of API token are supported, denoted as v1 and v2. Users are strongly encouraged to create only v2 tokens and to discontinue the use of v1 tokens. Support for v1 tokens will be removed in a future NetBox release.
v2 API tokens offer much stronger security. The token plaintext given at creation time is hashed together with a configured [cryptographic pepper](../configuration/required-parameters.md#api_token_peppers) to generate a unique checksum. This checksum is irreversible; the token plaintext is never stored on the server and thus cannot be retrieved even with database-level access.
#### Restricting Write Operations
### Restricting Write Operations
By default, a token can be used to perform all actions via the API that a user would be permitted to do via the web UI. Deselecting the "write enabled" option will restrict API requests made with the token to read operations (e.g. GET) only.
@@ -685,22 +681,10 @@ It is possible to provision authentication tokens for other users via the REST A
### Authenticating to the API
An authentication token is included with a request in its `Authorization` header. The format of the header value depends on the version of token in use. v2 tokens use the following form, concatenating the token's prefix (`nbt_`) and key with its plaintext value, separated by a period:
An authentication token is attached to a request by setting the `Authorization` header to the string `Token` followed by a space and the user's token:
```
Authorization: Bearer nbt_<key>.<token>
```
Legacy v1 tokens use the prefix `Token` rather than `Bearer`, and include only the token plaintext. (v1 tokens do not have a key.)
```
Authorization: Token <token>
```
Below is an example REST API request utilizing a v2 token.
```
$ curl -H "Authorization: Bearer nbt_4F9DAouzURLb.zjebxBPzICiPbWz0Wtx0fTL7bCKXKGTYhNzkgC2S" \
$ curl -H "Authorization: Token $TOKEN" \
-H "Accept: application/json; indent=4" \
https://netbox/api/dcim/sites/
{

View File

@@ -21,21 +21,6 @@ The cable's operational status. Choices include:
* Planned
* Decommissioning
### Profile
!!! note "This field was introduced in NetBox v4.5."
The profile to which the cable conforms. The profile determines the mapping of termination between the two ends and enables logical tracing across complex connections, such as breakout cables. Supported profiles are listed below.
* Straight (single position)
* Straight (multi-position)
* Shuffle (2x2 MPO8)
* Shuffle (4x4 MPO8)
A single-position cable is allowed only one termination point at each end. There is no limit to the number of terminations a multi-position cable may have. Each end of a cable must have the same number of terminations, unless connected to a pass-through port or to a circuit termination.
The assignment of a cable profile is optional. If no profile is assigned, legacy tracing behavior will be preserved.
### Type
The cable's physical medium or classification.

View File

@@ -1,23 +0,0 @@
# Owner
An owner is a set of users and/or groups who are responsible for the administration of certain resources within NetBox. The users and groups assigned to an owner are referred to as its members. Owner assignments are useful for indicating which parties are responsible for the administration of a particular object.
Most objects within NetBox can be assigned an owner, although this is not required.
## Fields
### Name
The owner's name.
### Group
The [group](./ownergroup.md) to which the owner is assigned. The assignment of an owner to a group is optional.
### User Groups
Groups of users that are members of the owner.
### Users
Individual users that are members of the owner.

View File

@@ -1,9 +0,0 @@
# Owner Groups
Groups are used to correlate and organize [owners](./owner.md). The assignment of an owner to a group has no bearing on the relationship of owned objects to their owners.
## Fields
### Name
The name of the group.

View File

@@ -21,13 +21,6 @@ The VM's operational status.
!!! tip
Additional statuses may be defined by setting `VirtualMachine.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
### Start on boot
The start on boot setting from the hypervisor.
!!! tip
Additional statuses may be defined by setting `VirtualMachine.start_on_boot` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
### Site & Cluster
The [site](../dcim/site.md) and/or [cluster](./cluster.md) to which the VM is assigned.

View File

@@ -60,13 +60,6 @@ Four of the standard Python logging levels are supported:
Log entries recorded using the runner's logger will be saved in the job's log in the database in addition to being processed by other [system logging handlers](../../configuration/system.md#logging).
### Jobs running for Model instances
A Job can be executed for a specific instance of a Model.
To enable this functionality, the model must include the `JobsMixin`.
When enqueuing a Job, you can associate it with a particular instance by passing that instance to the `instance` parameter.
### Scheduled Jobs
As described above, jobs can be scheduled for immediate execution or at any later time using the `enqueue()` method. However, for management purposes, the `enqueue_once()` method allows a job to be scheduled exactly once avoiding duplicates. If a job is already scheduled for a particular instance, a second one won't be scheduled, respecting thread safety. An example use case would be to schedule a periodic task that is bound to an instance in general, but not to any event of that instance (such as updates). The parameters of the `enqueue_once()` method are identical to those of `enqueue()`.
@@ -80,10 +73,9 @@ As described above, jobs can be scheduled for immediate execution or at any late
from django.db import models
from core.choices import JobIntervalChoices
from netbox.models import NetBoxModel
from netbox.models.features import JobsMixin
from .jobs import MyTestJob
class MyModel(JobsMixin, NetBoxModel):
class MyModel(NetBoxModel):
foo = models.CharField()
def save(self, *args, **kwargs):

View File

@@ -1,22 +1,17 @@
# Filters & Filter Sets
Filter sets define the mechanisms available for filtering or searching through a set of objects in NetBox. For instance, sites can be filtered by their parent region or group, status, facility ID, and so on. The same filter set is used consistently for a model whether the request is made via the UI or REST API. (Note that the GraphQL API uses a separate filter class.) NetBox employs the [django-filter](https://django-filter.readthedocs.io/en/stable/) library to define filter sets.
Filter sets define the mechanisms available for filtering or searching through a set of objects in NetBox. For instance, sites can be filtered by their parent region or group, status, facility ID, and so on. The same filter set is used consistently for a model whether the request is made via the UI or REST API. (Note that the GraphQL API uses a separate filter class.) NetBox employs the [django-filters2](https://django-tables2.readthedocs.io/en/latest/) library to define filter sets.
## FilterSet Classes
To support additional functionality standard to NetBox models, such as tag assignment and custom field support, the `NetBoxModelFilterSet` class is available for use by plugins. This should be used as the base filter set class for plugin models which inherit from `NetBoxModel`. Within this class, individual filters can be declared as directed by the `django-filters` documentation. An example is provided below.
!!! info "New in NetBox v4.5: FilterSet Registration"
NetBox v4.5 introduced the `register_filterset()` utility function. This enables plugins to register their filtersets to receive advanced functionality, such as the automatic attachment of field-specific lookup modifiers on the filter form. Registration is optional: Unregistered filtersets will continue to work as before, but will not receive the enhanced functionality.
```python
# filtersets.py
import django_filters
from netbox.filtersets import NetBoxModelFilterSet
from utilities.filtersets import register_filterset
from .models import MyModel
@register_filterset
class MyFilterSet(NetBoxModelFilterSet):
status = django_filters.MultipleChoiceFilter(
choices=(
@@ -32,14 +27,6 @@ class MyFilterSet(NetBoxModelFilterSet):
fields = ('some', 'other', 'fields')
```
In addition to the base NetBoxModelFilterSet class, the following filterset classes are also available for subclasses of standard base models.
| Model Class | FilterSet Class |
|-----------------------|--------------------------------------------------|
| `PrimaryModel` | `netbox.filtersets.PrimaryModelFilterSet` |
| `OrganizationalModel` | `netbox.filtersets.OrganizationalModelFilterSet` |
| `NestedGroupModel` | `netbox.filtersets.NestedGroupModelFilterSet` |
### Declaring Filter Sets
To utilize a filter set in a subclass of one of NetBox's generic views (such as `ObjectListView` or `BulkEditView`), define the `filterset` attribute on the view class:
@@ -55,7 +42,7 @@ class MyModelListView(ObjectListView):
filterset = MyModelFilterSet
```
To enable a filter set on a REST API endpoint, set the `filterset_class` attribute on the API view:
To enable a filter set on a REST API endpoint, set the `filterset_class` attribute on the API view:
```python
# api/views.py
@@ -68,29 +55,6 @@ class MyModelViewSet(...):
filterset_class = filtersets.MyModelFilterSet
```
### Implementing Quick Search
The `ObjectListView` has a field called Quick Search. For Quick Search to work the corresponding FilterSet has to override the `search` method that is implemented in `NetBoxModelFilterSet`. This function takes a queryset and can perform arbitrary operations on it and return it. A common use-case is to search for the given search value in multiple fields:
```python
from django.db.models import Q
from netbox.filtersets import NetBoxModelFilterSet
from utilities.filtersets import register_filterset
@register_filterset
class MyFilterSet(NetBoxModelFilterSet):
...
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(description__icontains=value)
)
```
The `search` method is also used by the `q` filter in `NetBoxModelFilterSet` which in turn is used by the Search field in the filters tab.
## Filter Classes
### TagFilter
@@ -105,9 +69,7 @@ This class filters `tags` using the `slug` field. For example:
```python
from django_filters import FilterSet
from extras.filters import TagFilter
from utilities.filtersets import register_filterset
@register_filterset
class MyModelFilterSet(FilterSet):
tag = TagFilter()
```
@@ -123,9 +85,7 @@ This class filters `tags` using the `id` field. For example:
```python
from django_filters import FilterSet
from extras.filters import TagIDFilter
from utilities.filtersets import register_filterset
@register_filterset
class MyModelFilterSet(FilterSet):
tag_id = TagIDFilter()
```

View File

@@ -2,7 +2,7 @@
## Form Classes
NetBox provides several base form classes for use by plugins. Additional form classes are also available for other standard base model classes (PrimaryModel, OrganizationalModel, and NestedGroupModel).
NetBox provides several base form classes for use by plugins.
| Form Class | Purpose |
|----------------------------|--------------------------------------|
@@ -19,17 +19,7 @@ This is the base form for creating and editing NetBox models. It extends Django'
|-------------|---------------------------------------------------------------------------------------|
| `fieldsets` | A tuple of `FieldSet` instances which control how form fields are rendered (optional) |
#### Subclasses
The corresponding model-specific subclasses of `NetBoxModelForm` are documented below.
| Model Class | Form Class |
|-----------------------|---------------------------|
| `PrimaryModel` | `PrimaryModelForm` |
| `OrganizationalModel` | `OrganizationalModelForm` |
| `NestedGroupModel` | `NestedGroupModelForm` |
#### Example
**Example**
```python
from django.utils.translation import gettext_lazy as _
@@ -59,19 +49,9 @@ class MyModelForm(NetBoxModelForm):
### `NetBoxModelImportForm`
This form facilitates the bulk import of new objects from CSV, JSON, or YAML data. As with model forms, you'll need to declare a `Meta` subclass specifying the associated `model` and `fields`. NetBox also provides several form fields suitable for importing various types of CSV data, listed [below](#csv-import-fields).
This form facilitates the bulk import of new objects from CSV, JSON, or YAML data. As with model forms, you'll need to declare a `Meta` subclass specifying the associated `model` and `fields`. NetBox also provides several form fields suitable for import various types of CSV data, listed below.
#### Subclasses
The corresponding model-specific subclasses of `NetBoxModelImportForm` are documented below.
| Model Class | Form Class |
|-----------------------|---------------------------------|
| `PrimaryModel` | `PrimaryModelImportForm` |
| `OrganizationalModel` | `OrganizationalModelImportForm` |
| `NestedGroupModel` | `NestedGroupModelImportForm` |
#### Example
**Example**
```python
from django.utils.translation import gettext_lazy as _
@@ -103,17 +83,7 @@ This form facilitates editing multiple objects in bulk. Unlike a model form, thi
| `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) |
#### Subclasses
The corresponding model-specific subclasses of `NetBoxModelBulkEditForm` are documented below.
| Model Class | Form Class |
|-----------------------|-----------------------------------|
| `PrimaryModel` | `PrimaryModelBulkEditForm` |
| `OrganizationalModel` | `OrganizationalModelBulkEditForm` |
| `NestedGroupModel` | `NestedGroupModelBulkEditForm` |
#### Example
**Example**
```python
from django import forms
@@ -155,17 +125,7 @@ This form class is used to render a form expressly for filtering a list of objec
| `model` | The model of object being edited |
| `fieldsets` | A tuple of `FieldSet` instances which control how form fields are rendered (optional) |
#### Subclasses
The corresponding model-specific subclasses of `NetBoxModelFilterSetForm` are documented below.
| Model Class | Form Class |
|-----------------------|------------------------------------|
| `PrimaryModel` | `PrimaryModelFilterSetForm` |
| `OrganizationalModel` | `OrganizationalModelFilterSetForm` |
| `NestedGroupModel` | `NestedGroupModelFilterSetForm` |
#### Example
**Example**
```python
from dcim.models import Site

View File

@@ -46,19 +46,3 @@ NetBox provides two object type classes for use by plugins.
::: netbox.graphql.types.NetBoxObjectType
options:
members: false
## GraphQL Filters
NetBox provides a base filter class for use by plugins which employ subclasseses of `NetBoxModel`.
::: netbox.graphql.filters.NetBoxModelFilter
options:
members: false
Additionally, the following filter classes are available for subclasses of standard base models.
| Model Class | FilterSet Class |
|-----------------------|----------------------------------------------------|
| `PrimaryModel` | `netbox.graphql.filters.PrimaryModelFilter` |
| `OrganizationalModel` | `netbox.graphql.filters.OrganizationalModelFilter` |
| `NestedGroupModel` | `netbox.graphql.filters.NestedGroupModelFilter` |

View File

@@ -173,12 +173,12 @@ classifiers=[
'Intended Audience :: Developers',
'Natural Language :: English',
"Programming Language :: Python :: 3 :: Only",
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3.13',
'Programming Language :: Python :: 3.14',
]
requires-python = ">=3.12.0"
requires-python = ">=3.10.0"
```
@@ -195,7 +195,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.12/site-packages/netbox.pth
echo /opt/netbox/netbox > $VENV/lib/python3.10/site-packages/netbox.pth
```
## Development Installation

View File

@@ -325,14 +325,14 @@ class CircuitTypeType(OrganizationalObjectType):
### Change filters.py
Filter classes should inherit from `netbox.graphql.filters.BaseModelFilter`.
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.filters import BaseModelFilter
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
__all__ = (
'CircuitFilter',
@@ -340,7 +340,8 @@ __all__ = (
@strawberry_django.filter(models.Circuit, lookups=True)
class CircuitFilter(BaseModelFilter):
@autotype_decorator(filtersets.CircuitFilterSet)
class CircuitFilter(BaseFilterMixin):
pass
```

View File

@@ -67,46 +67,6 @@ class MyModel(ExportTemplatesMixin, TagsMixin, models.Model):
...
```
### Additional Models
In addition to the base NetBoxModel class, the following additional classes are provided for convenience.
!!! info "These model classes were added to the plugins API in NetBox v4.5."
#### PrimaryModel
PrimaryModel is the go-to class for most object types. It extends NetBoxModel with `description` and `comments` fields, and it introduces support for ownership assignment.
| Field | Required | Unique | Description |
|---------------|----------|--------|---------------------------------------------|
| `owner` | No | No | The object's owner |
| `description` | No | No | A human-friendly description for the object |
| `comments` | No | No | General comments |
#### OrganizationalModel
OrganizationalModel is used by object types whose function is primarily the organization of other objects.
| Field | Required | Unique | Description |
|---------------|----------|--------|---------------------------------------------|
| `name` | Yes | Yes | The name of the object |
| `slug` | Yes | Yes | A unique URL-friendly identifier |
| `owner` | No | No | The object's owner |
| `description` | No | No | A human-friendly description for the object |
#### NestedGroupModel
NestedGroupModel is used for objects which arrange into a recursive hierarchy (like regions and locations) via its self-referential `parent` foreign key.
| Field | Required | Unique | Description |
|---------------|----------|--------|-----------------------------------------------------------------|
| `name` | Yes | Yes | The name of the object |
| `slug` | Yes | Yes | A unique URL-friendly identifier |
| `parent` | No | No | The object (of the same type) under which this object is nested |
| `owner` | No | No | The object's owner |
| `description` | No | No | A human-friendly description for the object |
| `comments` | No | No | General comments |
## Database Migrations
Once you have completed defining the model(s) for your plugin, you'll need to create the database schema migrations. A migration file is essentially a set of instructions for manipulating the PostgreSQL database to support your new model, or to alter existing models. Creating migrations can usually be done automatically using Django's `makemigrations` management command. (Ensure that your plugin has been installed and enabled first, otherwise it won't be found.)

View File

@@ -64,17 +64,14 @@ item1 = PluginMenuItem(
A `PluginMenuItem` has the following attributes:
| Attribute | Required | Description |
|-----------------|----------|------------------------------------------------------|
| `link` | Yes | Name of the URL path to which this menu item links |
| `link_text` | Yes | The text presented to the user |
| `permissions` | - | A list of permissions required to display this link |
| `auth_required` | - | Display only for authenticated users |
| `staff_only` | - | Display only for superusers |
| `buttons` | - | An iterable of PluginMenuButton instances to include |
!!! note "Changed in NetBox v4.5"
In releases prior to NetBox v4.5, `staff_only` restricted display of a menu item to only users with `is_staff` set to True. In NetBox v4.5, the `is_staff` flag was removed from the user model. Menu items with `staff_only` set to True are now displayed only for superusers.
| Attribute | Required | Description |
|-----------------|----------|----------------------------------------------------------------------------------------------------------|
| `link` | Yes | Name of the URL path to which this menu item links |
| `link_text` | Yes | The text presented to the user |
| `permissions` | - | A list of permissions required to display this link |
| `auth_required` | - | Display only for authenticated users |
| `staff_only` | - | Display only for users who have `is_staff` set to true (any specified permissions will also be required) |
| `buttons` | - | An iterable of PluginMenuButton instances to include |
## Menu Buttons

View File

@@ -27,14 +27,6 @@ Serializers are responsible for converting Python objects to JSON data suitable
The default nested representation of an object is defined by the `brief_fields` attributes under the serializer's `Meta` class. (Older versions of NetBox required the definition of a separate nested serializer.)
In addition to the base NetBoxModelSerializer class, the following serializer classes are also available for subclasses of standard base models.
| Model Class | Serializer Class |
|-----------------------|--------------------------------------------------------|
| `PrimaryModel` | `netbox.api.serializers.PrimaryModelSerializer` |
| `OrganizationalModel` | `netbox.api.serializers.OrganizationalModelSerializer` |
| `NestedGroupModel` | `netbox.api.serializers.NestedGroupModelSerializer` |
#### Example
To create a serializer for a plugin model, subclass `NetBoxModelSerializer` in `api/serializers.py`. Specify the model class and the fields to include within the serializer's `Meta` class.

View File

@@ -36,14 +36,6 @@ class MyModelTable(NetBoxTable):
default_columns = ('pk', 'name', ...)
```
In addition to the base NetBoxTable class, the following table classes are also available for subclasses of standard base models.
| Model Class | Table Class |
|-----------------------|------------------------------------------|
| `PrimaryModel` | `netbox.tables.PrimaryModelTable` |
| `OrganizationalModel` | `netbox.tables.OrganizationalModelTable` |
| `NestedGroupModel` | `netbox.tables.NestedGroupModelTable` |
### Table Configuration
The NetBoxTable class features dynamic configuration to allow users to change their column display and ordering preferences. To configure a table for a specific request, simply call its `configure()` method and pass the current HTTPRequest object. For example:

View File

@@ -1,148 +0,0 @@
# UI Components
!!! note "New in NetBox v4.5"
All UI components described here were introduced in NetBox v4.5. Be sure to set the minimum NetBox version to 4.5.0 for your plugin before incorporating any of these resources.
!!! danger "Beta Feature"
UI components are considered a beta feature, and are still under active development. Please be aware that the API for resources on this page is subject to change in future releases.
To simply the process of designing your plugin's user interface, and to encourage a consistent look and feel throughout the entire application, NetBox provides a set of components that enable programmatic UI design. These make it possible to declare complex page layouts with little or no custom HTML.
## Page Layout
A layout defines the general arrangement of content on a page into rows and columns. The layout is defined under the [view](./views.md) and declares a set of rows, each of which may have one or more columns. Below is an example layout.
```
+-------+-------+-------+
| Col 1 | Col 2 | Col 3 |
+-------+-------+-------+
| Col 4 |
+-----------+-----------+
| Col 5 | Col 6 |
+-----------+-----------+
```
The above layout can be achieved with the following declaration under a view:
```python
from netbox.ui import layout
from netbox.views import generic
class MyView(generic.ObjectView):
layout = layout.Layout(
layout.Row(
layout.Column(),
layout.Column(),
layout.Column(),
),
layout.Row(
layout.Column(),
),
layout.Row(
layout.Column(),
layout.Column(),
),
)
```
!!! note
Currently, layouts are supported only for subclasses of [`generic.ObjectView`](./views.md#netbox.views.generic.ObjectView).
::: netbox.ui.layout.Layout
::: netbox.ui.layout.SimpleLayout
::: netbox.ui.layout.Row
::: netbox.ui.layout.Column
## Panels
Within each column, related blocks of content are arranged into panels. Each panel has a title and may have a set of associated actions, but the content within is otherwise arbitrary.
Plugins can define their own panels by inheriting from the base class `netbox.ui.panels.Panel`. Override the `get_context()` method to pass additional context to your custom panel template. An example is provided below.
```python
from django.utils.translation import gettext_lazy as _
from netbox.ui.panels import Panel
class RecentChangesPanel(Panel):
template_name = 'my_plugin/panels/recent_changes.html'
title = _('Recent Changes')
def get_context(self, context):
return {
**super().get_context(context),
'changes': get_changes()[:10],
}
```
NetBox also includes a set of panels suite for specific uses, such as display object details or embedding a table of related objects. These are listed below.
::: netbox.ui.panels.Panel
::: netbox.ui.panels.ObjectPanel
::: netbox.ui.panels.ObjectAttributesPanel
#### Object Attributes
The following classes are available to represent object attributes within an ObjectAttributesPanel. Additionally, plugins can subclass `netbox.ui.attrs.ObjectAttribute` to create custom classes.
| Class | Description |
|--------------------------------------|--------------------------------------------------|
| `netbox.ui.attrs.AddressAttr` | A physical or mailing address. |
| `netbox.ui.attrs.BooleanAttr` | A boolean value |
| `netbox.ui.attrs.ColorAttr` | A color expressed in RGB |
| `netbox.ui.attrs.ChoiceAttr` | A selection from a set of choices |
| `netbox.ui.attrs.GPSCoordinatesAttr` | GPS coordinates (latitude and longitude) |
| `netbox.ui.attrs.ImageAttr` | An attached image (displays the image) |
| `netbox.ui.attrs.NestedObjectAttr` | A related nested object |
| `netbox.ui.attrs.NumericAttr` | An integer or float value |
| `netbox.ui.attrs.RelatedObjectAttr` | A related object |
| `netbox.ui.attrs.TemplatedAttr` | Renders an attribute using a custom template |
| `netbox.ui.attrs.TextAttr` | A string (text) value |
| `netbox.ui.attrs.TimezoneAttr` | A timezone with annotated offset |
| `netbox.ui.attrs.UtilizationAttr` | A numeric value expressed as a utilization graph |
::: netbox.ui.panels.OrganizationalObjectPanel
::: netbox.ui.panels.NestedGroupObjectPanel
::: netbox.ui.panels.CommentsPanel
::: netbox.ui.panels.JSONPanel
::: netbox.ui.panels.RelatedObjectsPanel
::: netbox.ui.panels.ObjectsTablePanel
::: netbox.ui.panels.TemplatePanel
::: netbox.ui.panels.PluginContentPanel
## Panel Actions
Each panel may have actions associated with it. These render as links or buttons within the panel header, opposite the panel's title. For example, a common use case is to include an "Add" action on a panel which displays a list of objects. Below is an example of this.
```python
from django.utils.translation import gettext_lazy as _
from netbox.ui import actions, panels
panels.ObjectsTablePanel(
model='dcim.Region',
title=_('Child Regions'),
filters={'parent_id': lambda ctx: ctx['object'].pk},
actions=[
actions.AddObject('dcim.Region', url_params={'parent': lambda ctx: ctx['object'].pk}),
],
),
```
::: netbox.ui.actions.PanelAction
::: netbox.ui.actions.LinkAction
::: netbox.ui.actions.AddObject
::: netbox.ui.actions.CopyContent

View File

@@ -10,14 +10,6 @@ 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.5](./version-4.5.md) (January 2026)
* Lookup Modifiers in Filter Forms ([#7604](https://github.com/netbox-community/netbox/issues/7604))
* Improved API Authentication Tokens ([#20210](https://github.com/netbox-community/netbox/issues/20210))
* Object Ownership ([#20304](https://github.com/netbox-community/netbox/issues/20304))
* Advanced Port Mappings ([#20564](https://github.com/netbox-community/netbox/issues/20564))
* Cable Profiles ([#20788](https://github.com/netbox-community/netbox/issues/20788))
#### [Version 4.4](./version-4.4.md) (September 2025)
* Background Jobs for Bulk Operations ([#19589](https://github.com/netbox-community/netbox/issues/19589), [#19891](https://github.com/netbox-community/netbox/issues/19891))

View File

@@ -1,150 +1,5 @@
# NetBox v4.4
## v4.4.8 (2025-12-09)
### Enhancements
* [#20068](https://github.com/netbox-community/netbox/issues/20068) - Support the assignment of module type profile attributes via bulk import
* [#20914](https://github.com/netbox-community/netbox/issues/20914) - Enable filtering device components by tenant assigned to device
### Bug Fixes
* [#19918](https://github.com/netbox-community/netbox/issues/19918) - Fix support for `{module}` resolution of components of child modules
* [#20759](https://github.com/netbox-community/netbox/issues/20759) - Improve legibility of object types in permissions form
* [#20860](https://github.com/netbox-community/netbox/issues/20860) - Ensure user-provided changelog message is recorded when creating device components via the UI
* [#20878](https://github.com/netbox-community/netbox/issues/20878) - Use the active database connection when executing custom scripts
* [#20888](https://github.com/netbox-community/netbox/issues/20888) - Resolve warnings about non-decimal values for min/max latitude & longitude fields
---
## v4.4.7 (2025-11-25)
### Enhancements
* [#20371](https://github.com/netbox-community/netbox/issues/20371) - Add Molex Micro-Fit 2x3 for power ports & power outlets
* [#20731](https://github.com/netbox-community/netbox/issues/20731) - Enable specifying `data_source` & `data_file` when bulk import config templates
* [#20820](https://github.com/netbox-community/netbox/issues/20820) - Enable filtering of custom fields by object type
* [#20823](https://github.com/netbox-community/netbox/issues/20823) - Disallow creation of API tokens with an expiration date in the past
* [#20841](https://github.com/netbox-community/netbox/issues/20841) - Support advanced filtering for available rack types when creating/editing a rack
### Bug Fixes
* [#20134](https://github.com/netbox-community/netbox/issues/20134) - Prevent out-of-band HTMX content swaps in embedded tables
* [#20432](https://github.com/netbox-community/netbox/issues/20432) - Fix tracing of cables across multiple circuits in parallel
* [#20465](https://github.com/netbox-community/netbox/issues/20465) - Ensure that scripts are updated immediately when a new file is uploaded
* [#20638](https://github.com/netbox-community/netbox/issues/20638) - Correct OpenAPI schema for bulk create operations
* [#20649](https://github.com/netbox-community/netbox/issues/20649) - Enforce view permissions on REST API endpoint for custom scripts
* [#20740](https://github.com/netbox-community/netbox/issues/20740) - Ensure permissions constraints are enforced when executing custom scripts via the REST API
* [#20743](https://github.com/netbox-community/netbox/issues/20743) - Pass request context to custom script when triggered by an event rule
* [#20766](https://github.com/netbox-community/netbox/issues/20766) - Fix inadvertent translations on server error page
* [#20775](https://github.com/netbox-community/netbox/issues/20775) - Fix `TypeError` exception when bulk renaming unnamed devices
* [#20822](https://github.com/netbox-community/netbox/issues/20822) - Add missing `auto_sync_enabled` field in bulk edit forms
* [#20827](https://github.com/netbox-community/netbox/issues/20827) - Fix UI styling issue when toggling between light and dark mode
* [#20839](https://github.com/netbox-community/netbox/issues/20839) - Fix filtering by object type in UI for custom links and saved filters
* [#20840](https://github.com/netbox-community/netbox/issues/20840) - Remove extraneous references to airflow for RackType model
* [#20844](https://github.com/netbox-community/netbox/issues/20844) - Fix object type filter for L2VPN terminations
* [#20859](https://github.com/netbox-community/netbox/issues/20859) - Prevent dashboard crash due to exception raised by a widget
* [#20865](https://github.com/netbox-community/netbox/issues/20865) - Enforce proper min/max values for latitude & longitude fields
---
## v4.4.6 (2025-11-11)
### Enhancements
* [#14171](https://github.com/netbox-community/netbox/issues/14171) - Support VLAN assignment for device & VM interfaces being bulk imported
* [#20297](https://github.com/netbox-community/netbox/issues/20297) - Introduce additional coaxial cable types
### Bug Fixes
* [#20378](https://github.com/netbox-community/netbox/issues/20378) - Prevent exception when attempting to delete a data source utilized by a custom script
* [#20645](https://github.com/netbox-community/netbox/issues/20645) - CSVChoiceField should defer to model field's default value when CSV field is empty
* [#20647](https://github.com/netbox-community/netbox/issues/20647) - Improve handling of empty strings during bulk imports
* [#20653](https://github.com/netbox-community/netbox/issues/20653) - Fix filtering of jobs by object type ID
* [#20660](https://github.com/netbox-community/netbox/issues/20660) - Optimize loading of custom script modules from remote storage
* [#20670](https://github.com/netbox-community/netbox/issues/20670) - Improve validation of related objects during bulk import
* [#20688](https://github.com/netbox-community/netbox/issues/20688) - Suppress non-harmful "No active configuration revision found" warning message
* [#20697](https://github.com/netbox-community/netbox/issues/20697) - Prevent duplication of signals which increment/decrement related object counts
* [#20699](https://github.com/netbox-community/netbox/issues/20699) - Ensure proper ordering of changelog entries resulting from cascading deletions
* [#20713](https://github.com/netbox-community/netbox/issues/20713) - Ensure a pre-change snapshot is recorded on virtual chassis members being added/removed
* [#20721](https://github.com/netbox-community/netbox/issues/20721) - Fix breadcrumb navigation links in UI for background tasks
* [#20738](https://github.com/netbox-community/netbox/issues/20738) - Deleting a virtual chassis should nullify the `vc_position` of all former members
* [#20750](https://github.com/netbox-community/netbox/issues/20750) - Fix cloning of permissions when only one action is enabled
* [#20755](https://github.com/netbox-community/netbox/issues/20755) - Prevent duplicate results under certain conditions when filtering providers
* [#20771](https://github.com/netbox-community/netbox/issues/20771) - Comments are required when creating a new journal entry
* [#20774](https://github.com/netbox-community/netbox/issues/20774) - Bulk action button labels should be translated
---
## v4.4.5 (2025-10-28)
### Enhancements
* [#19751](https://github.com/netbox-community/netbox/issues/19751) - Disable occupied module bays in form dropdowns when installing a new module
* [#20301](https://github.com/netbox-community/netbox/issues/20301) - Add a "dismiss all" option to the notifications dropdown
* [#20399](https://github.com/netbox-community/netbox/issues/20399) - Add `assigned` and `primary` boolean filters for MAC addresses
* [#20567](https://github.com/netbox-community/netbox/issues/20567) - Add contacts column to services table
* [#20675](https://github.com/netbox-community/netbox/issues/20675) - Enable [NetBox Copilot](https://netboxlabs.com/products/netbox-copilot/) integration
* [#20692](https://github.com/netbox-community/netbox/issues/20692) - Add contacts column to IP addresses table
* [#20700](https://github.com/netbox-community/netbox/issues/20700) - Add contacts table column for various additional models
### Bug Fixes
* [#19872](https://github.com/netbox-community/netbox/issues/19872) - Ensure custom script validation failures display error messages
* [#20389](https://github.com/netbox-community/netbox/issues/20389) - Fix "select all" behavior for bulk rename views
* [#20422](https://github.com/netbox-community/netbox/issues/20422) - Enable filtering of aggregates and prefixes by family in GraphQL API
* [#20459](https://github.com/netbox-community/netbox/issues/20459) - Fix validation of `is_oob` & `is_primary` fields under IP address bulk import
* [#20466](https://github.com/netbox-community/netbox/issues/20466) - Fix querying of devices with a primary IP assigned in GraphQL API
* [#20498](https://github.com/netbox-community/netbox/issues/20498) - Enforce the validation regex (if set) for custom URL fields
* [#20524](https://github.com/netbox-community/netbox/issues/20524) - Raise a validation error when attempting to schedule a custom script for a past date/time
* [#20541](https://github.com/netbox-community/netbox/issues/20541) - Fix resolution of GraphQL object fields which rely on custom filters
* [#20551](https://github.com/netbox-community/netbox/issues/20551) - Fix automatic slug generation in quick-add UI form
* [#20606](https://github.com/netbox-community/netbox/issues/20606) - Enable copying of values from table columns rendered as badges
* [#20641](https://github.com/netbox-community/netbox/issues/20641) - Fix `AttributeError` exception raised by the object changes REST API endpoint
* [#20646](https://github.com/netbox-community/netbox/issues/20646) - Prevent cables from connecting to objects marked as connected
* [#20655](https://github.com/netbox-community/netbox/issues/20655) - Fix `FieldError` exception when attempting to sort permissions list by actions
---
## v4.4.4 (2025-10-15)
### Bug Fixes
* [#20554](https://github.com/netbox-community/netbox/issues/20554) - Fix generic relation filters to accept `<app>.<model>` format matching POST requests
* [#20574](https://github.com/netbox-community/netbox/issues/20574) - Fix excessive storage initialization overhead when listing scripts with remote backends
* [#20584](https://github.com/netbox-community/netbox/issues/20584) - Enforce PoE mode requirement on interface templates when PoE type is set
* [#20585](https://github.com/netbox-community/netbox/issues/20585) - Fix API schema generation crash for models with single-field UniqueConstraints
* [#20587](https://github.com/netbox-community/netbox/issues/20587) - Fix upgrade.sh failure when removing stale content types
---
## v4.4.3 (2025-10-14)
### Enhancements
* [#20426](https://github.com/netbox-community/netbox/issues/20426) - Add a copy-to-clipboard button for custom script output
* [#20516](https://github.com/netbox-community/netbox/issues/20516) - Improve rendering of VLAN ID ranges in VLAN group tables
### Bug Fixes
* [#19302](https://github.com/netbox-community/netbox/issues/19302) - Fix uniqueness validation in REST API for nullable fields
* [#19615](https://github.com/netbox-community/netbox/issues/19615) - Fix support for static file parameters in templates when external storage is in use
* [#19818](https://github.com/netbox-community/netbox/issues/19818) - Hide primary IP assignment fields when creating a new virtual machine in the UI
* [#19825](https://github.com/netbox-community/netbox/issues/19825) - Prevent cache for config revisions from being erroneously overwritten when debugging is enabled
* [#20140](https://github.com/netbox-community/netbox/issues/20140) - Changing a site's region or group should update any associated circuit terminations
* [#20156](https://github.com/netbox-community/netbox/issues/20156) - Fix display of rack elevation labels
* [#20290](https://github.com/netbox-community/netbox/issues/20290) - Fix migration error when upgrading to NetBox v4.4 from releases earlier than v4.3
* [#20471](https://github.com/netbox-community/netbox/issues/20471) - Saving an unmodified VLAN group should not generate a change record
* [#20475](https://github.com/netbox-community/netbox/issues/20475) - Collapse singleton VLAN IDs in VLAN group display
* [#20494](https://github.com/netbox-community/netbox/issues/20494) - Correct OpenAPI schema definition for `IntegerRangeSerializer`
* [#20496](https://github.com/netbox-community/netbox/issues/20496) - REST API should always honor `MAX_PAGE_SIZE` value
* [#20497](https://github.com/netbox-community/netbox/issues/20497) - Fix filtering of VLAN groups by VLAN ID range in GraphQL API
* [#20507](https://github.com/netbox-community/netbox/issues/20507) - Fix support for fetching ASN contacts via GraphQL API
* [#20523](https://github.com/netbox-community/netbox/issues/20523) - Hide password change form for users authenticated via SSO
* [#20542](https://github.com/netbox-community/netbox/issues/20542) - Fix the creation of MAC addresses using the "quick add" form
---
## v4.4.2 (2025-09-30)
### Enhancements

View File

@@ -1,150 +0,0 @@
## v4.5.0 (FUTURE)
### Breaking Changes
* Python 3.10 and 3.11 are no longer supported. NetBox now requires Python 3.12, 3.13, or 3.14.
* GraphQL API queries which filter by object IDs or enums must now specify a filter lookup similar to other fields. For example, `id: 123` becomes `id: {exact: 123 }`.
* Rendering a device or virtual machine configuration is now restricted to users with the `render_config` permission for the applicable object type.
* Retrieval of API token plaintexts is no longer supported. The `ALLOW_TOKEN_RETRIEVAL` config parameter has been removed.
* API tokens can no longer be reassigned from one user to another.
* A config context assigned to a platform will now also apply to any children of that platform. (Although this is typically desired behavior, it may introduce unanticipated changes for existing deployments.)
* The `/api/dcim/cable-terminations/` REST API endpoint is now read-only. Cable terminations must be set on cables directly via the `/api/dcim/cables/` endpoint.
* The UI view dedicated to swapping A/Z circuit terminations has been removed.
* The experimental HTMX navigation feature has been removed.
* The obsolete boolean field `is_staff` has been removed from the `User` model.
* Removal of deprecated behavior
* The `/api/extras/object-types/` REST API endpoint has been removed. (Use `/api/core/object-types/` instead.)
* Webhooks no longer specify a `model` in payload data. (Reference `object_type` instead, which includes the parent app label.)
* The obsolete module `core.models.contenttypes` has been removed (replaced in v4.4 by `core.models.object_types`).
* The `load_yaml()` and `load_json()` utility methods have been removed from the base class for custom scripts.
### New Features
#### Lookup Modifiers in Filter Forms ([#7604](https://github.com/netbox-community/netbox/issues/7604))
Most object list filters within the UI have been extended to include optional lookup modifiers to support more complex queries. For instance, filters for numeric values now include a dropdown where a user can select "less than," "greater than," or "not" in addition to the default equivalency match. The specific modifiers available depend on the type of each filter.
(Note that this feature does not introduce any new filters. Rather, it makes available in the UI filters which already exist.)
#### Improved API Authentication Tokens ([#20210](https://github.com/netbox-community/netbox/issues/20210))
This release introduces a new version of API token (v2) which implements several security improvements. HMAC hashing with a cryptographic pepper is used to authenticate these tokens, obviating the need to store plaintexts. The new tokens also employ a non-sensitive key which can be shared to identify tokens without divulging their plaintexts. We've also adopted the standard "bearer" HTTP header format, as shown below.
```
# v1 token header
Authorization: Token <TOKEN>
# v2 token header
Authorization: Bearer nbt_<KEY>.<TOKEN>
```
Note that v2 token keys are prefixed with the fixed string `nbt_`, which can be used to aid in secret detection.
Backward compatibility with legacy (v1) tokens is retained in this release. However, users are strongly encouraged to begin using only v2 tokens, as support for legacy tokens will be removed in NetBox v4.7.
#### Object Ownership ([#20304](https://github.com/netbox-community/netbox/issues/20304))
An optional `owner` foreign key field has been added to most models. This enables the assignment of objects to a new Owner model, which represents a set of users and/or groups. Through this relationship, we can now convey ownership of objects within NetBox natively, without needing to rely on the assignment of tags or custom fields.
(Note that ownership differs significantly in function from tenancy. Ownership determines the parties responsible for the maintenance of an object, whereas as tenancy conveys an operational dependency.)
#### Advanced Port Mappings ([#20564](https://github.com/netbox-community/netbox/issues/20564))
The previous many-to-one mapping of front to rear ports has been expanded to support bidirectional mappings. The `rear_port` and `rear_port_position` fields on the FrontPort model have been replaced with an intermediary PortMapping model, which supports any number of assignments between front port/position pair and a rear port/position pair. This change unlocks the ability to model complex inline devices that swap individual fiber pairs between cables.
#### Cable Profiles ([#20788](https://github.com/netbox-community/netbox/issues/20788))
Cables can now be assigned profiles which determine how they are treated for path tracing. A profile indicates the number of discrete parallel channels or lanes carried by the cable among its endpoints. For example, a 1-to-4 breakout cable has four lanes, shared at one end via a common termination and split out at the other end to four separate terminations. Profiles, when assigned, enable NetBox to more accurately trace a specific connection within a cable, rather than the cable as a whole.
The assignment of cable profiles is optional: Cable tracing will continue to operate as before for cables with no profile assigned.
### Enhancements
* [#16681](https://github.com/netbox-community/netbox/issues/16681) - Introduce a `render_config` permission, which is now required to render a device or virtual machine configuration
* [#18658](https://github.com/netbox-community/netbox/issues/18658) - Add a `start_on_boot` choice field for virtual machines
* [#19095](https://github.com/netbox-community/netbox/issues/19095) - Add support for Python 3.13 and 3.14
* [#19338](https://github.com/netbox-community/netbox/issues/19338) - Enable filter lookups for object IDs and enums in GraphQL API queries
* [#19523](https://github.com/netbox-community/netbox/issues/19523) - Cache the number of instances for device, module, and rack types, and enable filtering by these counts
* [#20417](https://github.com/netbox-community/netbox/issues/20417) - Add an optional `color` field for device type power outlets
* [#20476](https://github.com/netbox-community/netbox/issues/20476) - Once provisioned, the owner of an API token cannot be changed
* [#20492](https://github.com/netbox-community/netbox/issues/20492) - Completely disabled the means to retrieve legacy API token plaintexts (removed the `ALLOW_TOKEN_RETRIEVAL` config parameter)
* [#20639](https://github.com/netbox-community/netbox/issues/20639) - Apply config contexts to devices/VMs assigned any child platform of the parent platform
* [#20834](https://github.com/netbox-community/netbox/issues/20834) - Add an `enabled` boolean field to API tokens
* [#20917](https://github.com/netbox-community/netbox/issues/20917) - Include usage reference on API token views
* [#20925](https://github.com/netbox-community/netbox/issues/20925) - Add optional `comments` field to all subclasses of `OrganizationalModel`
* [#20929](https://github.com/netbox-community/netbox/issues/20929) - Require the `render_config` permission to view a rendered device/VM configuration in the UI
* [#20936](https://github.com/netbox-community/netbox/issues/20936) - Introduce the `/api/authentication-check/` REST API endpoint for validating authentication tokens
* [#20959](https://github.com/netbox-community/netbox/issues/20959) - Include a count of related module types for a manufacturer in the REST API
### Plugins
* [#13182](https://github.com/netbox-community/netbox/issues/13182) - Added `PrimaryModel`, `OrganizationalModel`, and `NestedGroupModel` to the plugins API, as well as their respective base classes for various resources
### Other Changes
* [#16137](https://github.com/netbox-community/netbox/issues/16137) - Remove the obsolete boolean field `is_staff` from the `User` model
* [#17571](https://github.com/netbox-community/netbox/issues/17571) - Remove the experimental HTMX navigation feature
* [#17936](https://github.com/netbox-community/netbox/issues/17936) - Introduce a dedicated `GFKSerializerField` for representing generic foreign keys in API serializers
* [#19889](https://github.com/netbox-community/netbox/issues/19889) - Drop support for Python 3.10 and 3.11
* [#19898](https://github.com/netbox-community/netbox/issues/19898) - Remove the obsolete REST API endpoint `/api/extras/object-types/`
* [#20088](https://github.com/netbox-community/netbox/issues/20088) - Remove the non-deterministic `model` key from webhook payload data
* [#20095](https://github.com/netbox-community/netbox/issues/20095) - Remove the obsolete module `core.models.contenttypes`
* [#20096](https://github.com/netbox-community/netbox/issues/20096) - Remove the `load_yaml()` and `load_json()` utility methods from the `BaseScript` class
* [#20204](https://github.com/netbox-community/netbox/issues/20204) - Started migrating object views from custom HTML templates to declarative layouts
* [#20295](https://github.com/netbox-community/netbox/issues/20295) - Cable terminations may be modified via the REST API only by modifying the cable itself
* [#20617](https://github.com/netbox-community/netbox/issues/20617) - Introduce `BaseModel` as the global base class for models
* [#20683](https://github.com/netbox-community/netbox/issues/20683) - Remove the UI view dedicated to swapping A/Z circuit terminations
* [#20926](https://github.com/netbox-community/netbox/issues/20926) - Standardize naming of GraphQL filters
### REST API Changes
* Most objects now include an optional `owner` foreign key field.
* The `/api/dcim/cable-terminations` endpoint is now read-only.
* Introduced the `/api/authentication-check/` endpoint to test REST API credentials
* `circuits.CircuitGroup`
* Add optional `comments` field
* `circuits.CircuitType`
* Add optional `comments` field
* `circuits.VirtualCircuitType`
* Add optional `comments` field
* `dcim.Cable`
* Add the optional `profile` choice field
* `dcim.FrontPort`
* Removed the `rear_port` and `rear_port_position` fields
* Add the `positions` integer field
* Add the `rear_ports` list for port mappings
* `dcim.InventoryItemRole`
* Add optional `comments` field
* `dcim.Manufacturer`
* Add optional `comments` field
* Add read-only `moduletype_count` integer field
* `dcim.ModuleType`
* Add read-only `module_count` integer field
* `dcim.PowerOutletTemplate`
* Add optional `color` field
* `dcim.RackRole`
* Add optional `comments` field
* `dcim.RackType`
* Add read-only `rack_count` integer field
* `dcim.RearPort`
* Add the `front_ports` list for port mappings
* `ipam.ASNRange`
* Add optional `comments` field
* `ipam.RIR`
* Add optional `comments` field
* `ipam.Role`
* Add optional `comments` field
* `ipam.VLANGroup`
* Add optional `comments` field
* `tenancy.ContactRole`
* Add optional `comments` field
* `users.Token`
* Add `enabled` boolean field
* `virtualization.ClusterGroup`
* Add optional `comments` field
* `virtualization.ClusterType`
* Add optional `comments` field
* `virtualization.VirtualMachine`
* Add optional `start_on_boot` choice field
* `vpn.TunnelGroup`
* Add optional `comments` field

View File

@@ -77,7 +77,6 @@ nav:
- Wireless: 'features/wireless.md'
- Virtualization: 'features/virtualization.md'
- VPN Tunnels: 'features/vpn-tunnels.md'
- Resource Ownership: 'features/resource-ownership.md'
- Tenancy: 'features/tenancy.md'
- Contacts: 'features/contacts.md'
- Search: 'features/search.md'
@@ -143,7 +142,6 @@ nav:
- Getting Started: 'plugins/development/index.md'
- Models: 'plugins/development/models.md'
- Views: 'plugins/development/views.md'
- UI Components: 'plugins/development/ui-components.md'
- Navigation: 'plugins/development/navigation.md'
- Templates: 'plugins/development/templates.md'
- Tables: 'plugins/development/tables.md'
@@ -275,9 +273,6 @@ nav:
- ContactRole: 'models/tenancy/contactrole.md'
- Tenant: 'models/tenancy/tenant.md'
- TenantGroup: 'models/tenancy/tenantgroup.md'
- Users:
- Owner: 'models/users/owner.md'
- OwnerGroup: 'models/users/ownergroup.md'
- Virtualization:
- Cluster: 'models/virtualization/cluster.md'
- ClusterGroup: 'models/virtualization/clustergroup.md'
@@ -322,7 +317,6 @@ nav:
- git Cheat Sheet: 'development/git-cheat-sheet.md'
- Release Notes:
- Summary: 'release-notes/index.md'
- Version 4.5: 'release-notes/version-4.5.md'
- Version 4.4: 'release-notes/version-4.4.md'
- Version 4.3: 'release-notes/version-4.3.md'
- Version 4.2: 'release-notes/version-4.2.md'

57
netbox/account/tables.py Normal file
View File

@@ -0,0 +1,57 @@
from django.utils.translation import gettext as _
from account.models import UserToken
from netbox.tables import NetBoxTable, columns
__all__ = (
'UserTokenTable',
)
TOKEN = """<samp><span id="token_{{ record.pk }}">{{ record }}</span></samp>"""
ALLOWED_IPS = """{{ value|join:", " }}"""
COPY_BUTTON = """
{% if settings.ALLOW_TOKEN_RETRIEVAL %}
{% copy_content record.pk prefix="token_" color="success" %}
{% endif %}
"""
class UserTokenTable(NetBoxTable):
"""
Table for users to manager their own API tokens under account views.
"""
key = columns.TemplateColumn(
verbose_name=_('Key'),
template_code=TOKEN,
)
write_enabled = columns.BooleanColumn(
verbose_name=_('Write Enabled')
)
created = columns.DateTimeColumn(
timespec='minutes',
verbose_name=_('Created'),
)
expires = columns.DateTimeColumn(
timespec='minutes',
verbose_name=_('Expires'),
)
last_used = columns.DateTimeColumn(
verbose_name=_('Last Used'),
)
allowed_ips = columns.TemplateColumn(
verbose_name=_('Allowed IPs'),
template_code=ALLOWED_IPS
)
actions = columns.ActionsColumn(
actions=('edit', 'delete'),
extra_buttons=COPY_BUTTON
)
class Meta(NetBoxTable.Meta):
model = UserToken
fields = (
'pk', 'id', 'key', 'description', 'write_enabled', 'created', 'expires', 'last_used', 'allowed_ips',
)

View File

@@ -25,12 +25,9 @@ from extras.models import Bookmark
from extras.tables import BookmarkTable, NotificationTable, SubscriptionTable
from netbox.authentication import get_auth_backend_display, get_saml_idps
from netbox.config import get_config
from netbox.ui import layout
from netbox.views import generic
from users import forms
from users import forms, tables
from users.models import UserConfig
from users.tables import TokenTable
from users.ui.panels import TokenExamplePanel, TokenPanel
from utilities.request import safe_for_redirect
from utilities.string import remove_linebreaks
from utilities.views import register_model_view
@@ -331,8 +328,7 @@ class UserTokenListView(LoginRequiredMixin, View):
def get(self, request):
tokens = UserToken.objects.filter(user=request.user)
table = TokenTable(tokens)
table.columns.hide('user')
table = tables.UserTokenTable(tokens)
table.configure(request)
return render(request, 'account/token_list.html', {
@@ -344,21 +340,14 @@ class UserTokenListView(LoginRequiredMixin, View):
@register_model_view(UserToken)
class UserTokenView(LoginRequiredMixin, View):
layout = layout.SimpleLayout(
left_panels=[
TokenPanel(),
],
right_panels=[
TokenExamplePanel(),
],
)
def get(self, request, pk):
token = get_object_or_404(UserToken.objects.filter(user=request.user), pk=pk)
key = token.key if settings.ALLOW_TOKEN_RETRIEVAL else None
return render(request, 'account/token.html', {
'object': token,
'layout': self.layout,
'key': key,
})

View File

@@ -1,4 +1,5 @@
from django.contrib.contenttypes.models import ContentType
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from circuits.choices import CircuitPriorityChoices, CircuitStatusChoices, VirtualCircuitTerminationRoleChoices
@@ -10,12 +11,10 @@ from circuits.models import (
from dcim.api.serializers_.device_components import InterfaceSerializer
from dcim.api.serializers_.cables import CabledObjectSerializer
from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
from netbox.api.gfk_fields import GFKSerializerField
from netbox.api.serializers import (
NetBoxModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer, WritableNestedSerializer,
)
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
from netbox.choices import DistanceUnitChoices
from tenancy.api.serializers_.tenants import TenantSerializer
from utilities.api import get_serializer_for_model
from .providers import ProviderAccountSerializer, ProviderNetworkSerializer, ProviderSerializer
__all__ = (
@@ -30,7 +29,7 @@ __all__ = (
)
class CircuitTypeSerializer(OrganizationalModelSerializer):
class CircuitTypeSerializer(NetBoxModelSerializer):
# Related object counts
circuit_count = RelatedObjectCountField('circuits')
@@ -38,8 +37,8 @@ class CircuitTypeSerializer(OrganizationalModelSerializer):
class Meta:
model = CircuitType
fields = [
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'owner', 'comments', 'tags',
'custom_fields', 'created', 'last_updated', 'circuit_count',
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields',
'created', 'last_updated', 'circuit_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count')
@@ -54,7 +53,7 @@ class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
default=None
)
termination_id = serializers.IntegerField(allow_null=True, required=False, default=None)
termination = GFKSerializerField(read_only=True)
termination = serializers.SerializerMethodField(read_only=True)
class Meta:
model = CircuitTermination
@@ -63,16 +62,24 @@ class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
'upstream_speed', 'xconnect_id', 'description',
]
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_termination(self, obj):
if obj.termination_id is None:
return None
serializer = get_serializer_for_model(obj.termination)
context = {'request': self.context['request']}
return serializer(obj.termination, nested=True, context=context).data
class CircuitGroupSerializer(OrganizationalModelSerializer):
class CircuitGroupSerializer(NetBoxModelSerializer):
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
circuit_count = RelatedObjectCountField('assignments')
class Meta:
model = CircuitGroup
fields = [
'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'tenant', 'owner', 'comments', 'tags',
'custom_fields', 'created', 'last_updated', 'circuit_count'
'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'tenant',
'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count'
]
brief_fields = ('id', 'url', 'display', 'name')
@@ -92,7 +99,7 @@ class CircuitGroupAssignmentSerializer_(NetBoxModelSerializer):
brief_fields = ('id', 'url', 'display', 'group', 'priority')
class CircuitSerializer(PrimaryModelSerializer):
class CircuitSerializer(NetBoxModelSerializer):
provider = ProviderSerializer(nested=True)
provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True, default=None)
status = ChoiceField(choices=CircuitStatusChoices, required=False)
@@ -108,7 +115,7 @@ class CircuitSerializer(PrimaryModelSerializer):
fields = [
'id', 'url', 'display_url', 'display', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant',
'install_date', 'termination_date', 'commit_rate', 'description', 'distance', 'distance_unit',
'termination_a', 'termination_z', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
'termination_a', 'termination_z', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
'assignments',
]
brief_fields = ('id', 'url', 'display', 'provider', 'cid', 'description')
@@ -125,7 +132,7 @@ class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer
default=None
)
termination_id = serializers.IntegerField(allow_null=True, required=False, default=None)
termination = GFKSerializerField(read_only=True)
termination = serializers.SerializerMethodField(read_only=True)
class Meta:
model = CircuitTermination
@@ -137,12 +144,20 @@ class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer
]
brief_fields = ('id', 'url', 'display', 'circuit', 'term_side', 'description', 'cable', '_occupied')
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_termination(self, obj):
if obj.termination_id is None:
return None
serializer = get_serializer_for_model(obj.termination)
context = {'request': self.context['request']}
return serializer(obj.termination, nested=True, context=context).data
class CircuitGroupAssignmentSerializer(CircuitGroupAssignmentSerializer_):
member_type = ContentTypeField(
queryset=ContentType.objects.filter(CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS)
)
member = GFKSerializerField(read_only=True)
member = serializers.SerializerMethodField(read_only=True)
class Meta:
model = CircuitGroupAssignment
@@ -152,8 +167,16 @@ class CircuitGroupAssignmentSerializer(CircuitGroupAssignmentSerializer_):
]
brief_fields = ('id', 'url', 'display', 'group', 'member_type', 'member_id', 'member', 'priority')
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_member(self, obj):
if obj.member_id is None:
return None
serializer = get_serializer_for_model(obj.member)
context = {'request': self.context['request']}
return serializer(obj.member, nested=True, context=context).data
class VirtualCircuitTypeSerializer(OrganizationalModelSerializer):
class VirtualCircuitTypeSerializer(NetBoxModelSerializer):
# Related object counts
virtual_circuit_count = RelatedObjectCountField('virtual_circuits')
@@ -161,13 +184,13 @@ class VirtualCircuitTypeSerializer(OrganizationalModelSerializer):
class Meta:
model = VirtualCircuitType
fields = [
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'owner', 'comments', 'tags',
'custom_fields', 'created', 'last_updated', 'virtual_circuit_count',
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields',
'created', 'last_updated', 'virtual_circuit_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'virtual_circuit_count')
class VirtualCircuitSerializer(PrimaryModelSerializer):
class VirtualCircuitSerializer(NetBoxModelSerializer):
provider_network = ProviderNetworkSerializer(nested=True)
provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True, default=None)
type = VirtualCircuitTypeSerializer(nested=True)
@@ -178,7 +201,7 @@ class VirtualCircuitSerializer(PrimaryModelSerializer):
model = VirtualCircuit
fields = [
'id', 'url', 'display_url', 'display', 'cid', 'provider_network', 'provider_account', 'type', 'status',
'tenant', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
'tenant', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'provider_network', 'cid', 'description')

View File

@@ -4,7 +4,7 @@ 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 PrimaryModelSerializer
from netbox.api.serializers import NetBoxModelSerializer
from .nested import NestedProviderAccountSerializer
__all__ = (
@@ -14,7 +14,7 @@ __all__ = (
)
class ProviderSerializer(PrimaryModelSerializer):
class ProviderSerializer(NetBoxModelSerializer):
accounts = SerializedPKRelatedField(
queryset=ProviderAccount.objects.all(),
serializer=NestedProviderAccountSerializer,
@@ -35,32 +35,32 @@ class ProviderSerializer(PrimaryModelSerializer):
class Meta:
model = Provider
fields = [
'id', 'url', 'display_url', 'display', 'name', 'slug', 'accounts', 'description', 'owner', 'comments',
'id', 'url', 'display_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(PrimaryModelSerializer):
class ProviderAccountSerializer(NetBoxModelSerializer):
provider = ProviderSerializer(nested=True)
name = serializers.CharField(allow_blank=True, max_length=100, required=False, default='')
class Meta:
model = ProviderAccount
fields = [
'id', 'url', 'display_url', 'display', 'provider', 'name', 'account', 'description', 'owner', 'comments',
'tags', 'custom_fields', 'created', 'last_updated',
'id', 'url', 'display_url', 'display', 'provider', 'name', 'account', 'description', 'comments', 'tags',
'custom_fields', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'account', 'description')
class ProviderNetworkSerializer(PrimaryModelSerializer):
class ProviderNetworkSerializer(NetBoxModelSerializer):
provider = ProviderSerializer(nested=True)
class Meta:
model = ProviderNetwork
fields = [
'id', 'url', 'display_url', 'display', 'provider', 'name', 'service_id', 'description', 'owner', 'comments',
'tags', 'custom_fields', 'created', 'last_updated',
'id', 'url', 'display_url', 'display', 'provider', 'name', 'service_id', 'description', 'comments', 'tags',
'custom_fields', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')

View File

@@ -1,7 +1,5 @@
from django.apps import AppConfig
from netbox import denormalized
class CircuitsConfig(AppConfig):
name = "circuits"
@@ -10,16 +8,6 @@ class CircuitsConfig(AppConfig):
def ready(self):
from netbox.models.features import register_models
from . import signals, search # noqa: F401
from .models import CircuitTermination
# Register models
register_models(*self.get_models())
denormalized.register(CircuitTermination, '_site', {
'_region': 'region',
'_site_group': 'group',
})
denormalized.register(CircuitTermination, '_location', {
'_site': 'site',
})

View File

@@ -6,12 +6,11 @@ from django.utils.translation import gettext as _
from dcim.filtersets import CabledObjectFilterSet
from dcim.models import Interface, Location, Region, Site, SiteGroup
from ipam.models import ASN
from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet
from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
from utilities.filters import (
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, TreeNodeMultipleChoiceFilter,
)
from utilities.filtersets import register_filterset
from .choices import *
from .models import *
@@ -30,8 +29,7 @@ __all__ = (
)
@register_filterset
class ProviderFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='circuits__terminations___region',
@@ -91,12 +89,13 @@ class ProviderFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
return queryset.filter(
Q(name__icontains=value) |
Q(description__icontains=value) |
Q(accounts__account__icontains=value) |
Q(accounts__name__icontains=value) |
Q(comments__icontains=value)
)
@register_filterset
class ProviderAccountFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
class ProviderAccountFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
provider_id = django_filters.ModelMultipleChoiceFilter(
queryset=Provider.objects.all(),
label=_('Provider (ID)'),
@@ -123,8 +122,7 @@ class ProviderAccountFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
).distinct()
@register_filterset
class ProviderNetworkFilterSet(PrimaryModelFilterSet):
class ProviderNetworkFilterSet(NetBoxModelFilterSet):
provider_id = django_filters.ModelMultipleChoiceFilter(
queryset=Provider.objects.all(),
label=_('Provider (ID)'),
@@ -151,7 +149,6 @@ class ProviderNetworkFilterSet(PrimaryModelFilterSet):
).distinct()
@register_filterset
class CircuitTypeFilterSet(OrganizationalModelFilterSet):
class Meta:
@@ -159,8 +156,7 @@ class CircuitTypeFilterSet(OrganizationalModelFilterSet):
fields = ('id', 'name', 'slug', 'color', 'description')
@register_filterset
class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
provider_id = django_filters.ModelMultipleChoiceFilter(
queryset=Provider.objects.all(),
label=_('Provider (ID)'),
@@ -271,7 +267,6 @@ class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilt
).distinct()
@register_filterset
class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
q = django_filters.CharFilter(
method='search',
@@ -353,7 +348,7 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
model = CircuitTermination
fields = (
'id', 'termination_id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description',
'mark_connected', 'pp_info', 'cable_end', 'cable_connector',
'mark_connected', 'pp_info', 'cable_end',
)
def search(self, queryset, name, value):
@@ -367,7 +362,6 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
).distinct()
@register_filterset
class CircuitGroupFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
class Meta:
@@ -375,7 +369,6 @@ class CircuitGroupFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
fields = ('id', 'name', 'slug', 'description')
@register_filterset
class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
q = django_filters.CharFilter(
method='search',
@@ -475,7 +468,6 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
)
@register_filterset
class VirtualCircuitTypeFilterSet(OrganizationalModelFilterSet):
class Meta:
@@ -483,8 +475,7 @@ class VirtualCircuitTypeFilterSet(OrganizationalModelFilterSet):
fields = ('id', 'name', 'slug', 'color', 'description')
@register_filterset
class VirtualCircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
class VirtualCircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
provider_id = django_filters.ModelMultipleChoiceFilter(
field_name='provider_network__provider',
queryset=Provider.objects.all(),
@@ -540,7 +531,6 @@ class VirtualCircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
).distinct()
@register_filterset
class VirtualCircuitTerminationFilterSet(NetBoxModelFilterSet):
q = django_filters.CharFilter(
method='search',

View File

@@ -11,11 +11,11 @@ from circuits.models import *
from dcim.models import Site
from ipam.models import ASN
from netbox.choices import DistanceUnitChoices
from netbox.forms import NetBoxModelBulkEditForm, OrganizationalModelBulkEditForm, PrimaryModelBulkEditForm
from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant
from utilities.forms import add_blank_choice, get_field_value
from utilities.forms.fields import (
ColorField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
ColorField, CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
)
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import BulkEditNullBooleanSelect, DatePicker, HTMXSelect, NumberWithOptions
@@ -36,12 +36,18 @@ __all__ = (
)
class ProviderBulkEditForm(PrimaryModelBulkEditForm):
class ProviderBulkEditForm(NetBoxModelBulkEditForm):
asns = DynamicModelMultipleChoiceField(
queryset=ASN.objects.all(),
label=_('ASNs'),
required=False
)
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
comments = CommentField()
model = Provider
fieldsets = (
@@ -52,12 +58,18 @@ class ProviderBulkEditForm(PrimaryModelBulkEditForm):
)
class ProviderAccountBulkEditForm(PrimaryModelBulkEditForm):
class ProviderAccountBulkEditForm(NetBoxModelBulkEditForm):
provider = DynamicModelChoiceField(
label=_('Provider'),
queryset=Provider.objects.all(),
required=False
)
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
comments = CommentField()
model = ProviderAccount
fieldsets = (
@@ -68,7 +80,7 @@ class ProviderAccountBulkEditForm(PrimaryModelBulkEditForm):
)
class ProviderNetworkBulkEditForm(PrimaryModelBulkEditForm):
class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm):
provider = DynamicModelChoiceField(
label=_('Provider'),
queryset=Provider.objects.all(),
@@ -79,6 +91,12 @@ class ProviderNetworkBulkEditForm(PrimaryModelBulkEditForm):
required=False,
label=_('Service ID')
)
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
comments = CommentField()
model = ProviderNetwork
fieldsets = (
@@ -89,20 +107,25 @@ class ProviderNetworkBulkEditForm(PrimaryModelBulkEditForm):
)
class CircuitTypeBulkEditForm(OrganizationalModelBulkEditForm):
class CircuitTypeBulkEditForm(NetBoxModelBulkEditForm):
color = ColorField(
label=_('Color'),
required=False
)
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
model = CircuitType
fieldsets = (
FieldSet('color', 'description'),
)
nullable_fields = ('color', 'description', 'comments')
nullable_fields = ('color', 'description')
class CircuitBulkEditForm(PrimaryModelBulkEditForm):
class CircuitBulkEditForm(NetBoxModelBulkEditForm):
type = DynamicModelChoiceField(
label=_('Type'),
queryset=CircuitType.objects.all(),
@@ -160,6 +183,12 @@ class CircuitBulkEditForm(PrimaryModelBulkEditForm):
required=False,
initial=''
)
description = forms.CharField(
label=_('Description'),
max_length=100,
required=False
)
comments = CommentField()
model = Circuit
fieldsets = (
@@ -232,7 +261,12 @@ class CircuitTerminationBulkEditForm(NetBoxModelBulkEditForm):
pass
class CircuitGroupBulkEditForm(OrganizationalModelBulkEditForm):
class CircuitGroupBulkEditForm(NetBoxModelBulkEditForm):
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
tenant = DynamicModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(),
@@ -241,7 +275,7 @@ class CircuitGroupBulkEditForm(OrganizationalModelBulkEditForm):
model = CircuitGroup
nullable_fields = (
'description', 'tenant', 'comments',
'description', 'tenant',
)
@@ -264,20 +298,25 @@ class CircuitGroupAssignmentBulkEditForm(NetBoxModelBulkEditForm):
nullable_fields = ('priority',)
class VirtualCircuitTypeBulkEditForm(OrganizationalModelBulkEditForm):
class VirtualCircuitTypeBulkEditForm(NetBoxModelBulkEditForm):
color = ColorField(
label=_('Color'),
required=False
)
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
model = VirtualCircuitType
fieldsets = (
FieldSet('color', 'description'),
)
nullable_fields = ('color', 'description', 'comments')
nullable_fields = ('color', 'description')
class VirtualCircuitBulkEditForm(PrimaryModelBulkEditForm):
class VirtualCircuitBulkEditForm(NetBoxModelBulkEditForm):
provider_network = DynamicModelChoiceField(
label=_('Provider network'),
queryset=ProviderNetwork.objects.all(),
@@ -304,6 +343,12 @@ class VirtualCircuitBulkEditForm(PrimaryModelBulkEditForm):
queryset=Tenant.objects.all(),
required=False
)
description = forms.CharField(
label=_('Description'),
max_length=100,
required=False
)
comments = CommentField()
model = VirtualCircuit
fieldsets = (

View File

@@ -7,7 +7,7 @@ from circuits.constants import *
from circuits.models import *
from dcim.models import Interface
from netbox.choices import DistanceUnitChoices
from netbox.forms import NetBoxModelImportForm, OrganizationalModelImportForm, PrimaryModelImportForm
from netbox.forms import NetBoxModelImportForm
from tenancy.models import Tenant
from utilities.forms.fields import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, SlugField
@@ -28,17 +28,17 @@ __all__ = (
)
class ProviderImportForm(PrimaryModelImportForm):
class ProviderImportForm(NetBoxModelImportForm):
slug = SlugField()
class Meta:
model = Provider
fields = (
'name', 'slug', 'description', 'owner', 'comments', 'tags',
'name', 'slug', 'description', 'comments', 'tags',
)
class ProviderAccountImportForm(PrimaryModelImportForm):
class ProviderAccountImportForm(NetBoxModelImportForm):
provider = CSVModelChoiceField(
label=_('Provider'),
queryset=Provider.objects.all(),
@@ -49,11 +49,11 @@ class ProviderAccountImportForm(PrimaryModelImportForm):
class Meta:
model = ProviderAccount
fields = (
'provider', 'name', 'account', 'description', 'owner', 'comments', 'tags',
'provider', 'name', 'account', 'description', 'comments', 'tags',
)
class ProviderNetworkImportForm(PrimaryModelImportForm):
class ProviderNetworkImportForm(NetBoxModelImportForm):
provider = CSVModelChoiceField(
label=_('Provider'),
queryset=Provider.objects.all(),
@@ -64,19 +64,19 @@ class ProviderNetworkImportForm(PrimaryModelImportForm):
class Meta:
model = ProviderNetwork
fields = [
'provider', 'name', 'service_id', 'description', 'owner', 'comments', 'tags'
'provider', 'name', 'service_id', 'description', 'comments', 'tags'
]
class CircuitTypeImportForm(OrganizationalModelImportForm):
class CircuitTypeImportForm(NetBoxModelImportForm):
slug = SlugField()
class Meta:
model = CircuitType
fields = ('name', 'slug', 'color', 'description', 'owner', 'comments', 'tags')
fields = ('name', 'slug', 'color', 'description', 'tags')
class CircuitImportForm(PrimaryModelImportForm):
class CircuitImportForm(NetBoxModelImportForm):
provider = CSVModelChoiceField(
label=_('Provider'),
queryset=Provider.objects.all(),
@@ -119,7 +119,7 @@ class CircuitImportForm(PrimaryModelImportForm):
model = Circuit
fields = [
'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date', 'termination_date',
'commit_rate', 'distance', 'distance_unit', 'description', 'owner', 'comments', 'tags'
'commit_rate', 'distance', 'distance_unit', 'description', 'comments', 'tags'
]
@@ -165,7 +165,7 @@ class CircuitTerminationImportForm(NetBoxModelImportForm, BaseCircuitTermination
}
class CircuitGroupImportForm(OrganizationalModelImportForm):
class CircuitGroupImportForm(NetBoxModelImportForm):
tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(),
@@ -176,7 +176,7 @@ class CircuitGroupImportForm(OrganizationalModelImportForm):
class Meta:
model = CircuitGroup
fields = ('name', 'slug', 'description', 'tenant', 'owner', 'comments', 'tags')
fields = ('name', 'slug', 'description', 'tenant', 'tags')
class CircuitGroupAssignmentImportForm(NetBoxModelImportForm):
@@ -195,14 +195,15 @@ class CircuitGroupAssignmentImportForm(NetBoxModelImportForm):
fields = ('member_type', 'member_id', 'group', 'priority')
class VirtualCircuitTypeImportForm(OrganizationalModelImportForm):
class VirtualCircuitTypeImportForm(NetBoxModelImportForm):
slug = SlugField()
class Meta:
model = VirtualCircuitType
fields = ('name', 'slug', 'color', 'description', 'owner', 'comments', 'tags')
fields = ('name', 'slug', 'color', 'description', 'tags')
class VirtualCircuitImportForm(PrimaryModelImportForm):
class VirtualCircuitImportForm(NetBoxModelImportForm):
provider_network = CSVModelChoiceField(
label=_('Provider network'),
queryset=ProviderNetwork.objects.all(),
@@ -238,8 +239,8 @@ class VirtualCircuitImportForm(PrimaryModelImportForm):
class Meta:
model = VirtualCircuit
fields = [
'cid', 'provider_network', 'provider_account', 'type', 'status', 'tenant', 'description', 'owner',
'comments', 'tags',
'cid', 'provider_network', 'provider_account', 'type', 'status', 'tenant', 'description', 'comments',
'tags',
]

View File

@@ -9,7 +9,7 @@ from circuits.models import *
from dcim.models import Location, Region, Site, SiteGroup
from ipam.models import ASN
from netbox.choices import DistanceUnitChoices
from netbox.forms import NetBoxModelFilterSetForm, OrganizationalModelFilterSetForm, PrimaryModelFilterSetForm
from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import TenancyFilterForm, ContactModelFilterForm
from utilities.forms import add_blank_choice
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
@@ -31,10 +31,10 @@ __all__ = (
)
class ProviderFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Provider
fieldsets = (
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
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')),
@@ -66,10 +66,10 @@ class ProviderFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
tag = TagFilterField(model)
class ProviderAccountFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
class ProviderAccountFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = ProviderAccount
fieldsets = (
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
FieldSet('q', 'filter_id', 'tag'),
FieldSet('provider_id', 'account', name=_('Attributes')),
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
)
@@ -85,10 +85,10 @@ class ProviderAccountFilterForm(ContactModelFilterForm, PrimaryModelFilterSetFor
tag = TagFilterField(model)
class ProviderNetworkFilterForm(PrimaryModelFilterSetForm):
class ProviderNetworkFilterForm(NetBoxModelFilterSetForm):
model = ProviderNetwork
fieldsets = (
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
FieldSet('q', 'filter_id', 'tag'),
FieldSet('provider_id', 'service_id', name=_('Attributes')),
)
provider_id = DynamicModelMultipleChoiceField(
@@ -104,10 +104,10 @@ class ProviderNetworkFilterForm(PrimaryModelFilterSetForm):
tag = TagFilterField(model)
class CircuitTypeFilterForm(OrganizationalModelFilterSetForm):
class CircuitTypeFilterForm(NetBoxModelFilterSetForm):
model = CircuitType
fieldsets = (
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
FieldSet('q', 'filter_id', 'tag'),
FieldSet('color', name=_('Attributes')),
)
tag = TagFilterField(model)
@@ -118,10 +118,10 @@ class CircuitTypeFilterForm(OrganizationalModelFilterSetForm):
)
class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm):
class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Circuit
fieldsets = (
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
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', 'distance', 'distance_unit',
@@ -271,10 +271,10 @@ class CircuitTerminationFilterForm(NetBoxModelFilterSetForm):
tag = TagFilterField(model)
class CircuitGroupFilterForm(TenancyFilterForm, OrganizationalModelFilterSetForm):
class CircuitGroupFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = CircuitGroup
fieldsets = (
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
FieldSet('q', 'filter_id', 'tag'),
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
)
tag = TagFilterField(model)
@@ -309,10 +309,10 @@ class CircuitGroupAssignmentFilterForm(NetBoxModelFilterSetForm):
tag = TagFilterField(model)
class VirtualCircuitTypeFilterForm(OrganizationalModelFilterSetForm):
class VirtualCircuitTypeFilterForm(NetBoxModelFilterSetForm):
model = VirtualCircuitType
fieldsets = (
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
FieldSet('q', 'filter_id', 'tag'),
FieldSet('color', name=_('Attributes')),
)
tag = TagFilterField(model)
@@ -323,10 +323,10 @@ class VirtualCircuitTypeFilterForm(OrganizationalModelFilterSetForm):
)
class VirtualCircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm):
class VirtualCircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
model = VirtualCircuit
fieldsets = (
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
FieldSet('q', 'filter_id', 'tag'),
FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
FieldSet('type_id', 'status', name=_('Attributes')),
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),

View File

@@ -10,11 +10,11 @@ from circuits.constants import *
from circuits.models import *
from dcim.models import Interface, Site
from ipam.models import ASN
from netbox.forms import NetBoxModelForm, OrganizationalModelForm, PrimaryModelForm
from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm
from utilities.forms import get_field_value
from utilities.forms.fields import (
ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField,
CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField,
)
from utilities.forms.mixins import DistanceValidationMixin
from utilities.forms.rendering import FieldSet, InlineFields
@@ -36,13 +36,14 @@ __all__ = (
)
class ProviderForm(PrimaryModelForm):
class ProviderForm(NetBoxModelForm):
slug = SlugField()
asns = DynamicModelMultipleChoiceField(
queryset=ASN.objects.all(),
label=_('ASNs'),
required=False
)
comments = CommentField()
fieldsets = (
FieldSet('name', 'slug', 'asns', 'description', 'tags'),
@@ -51,32 +52,34 @@ class ProviderForm(PrimaryModelForm):
class Meta:
model = Provider
fields = [
'name', 'slug', 'asns', 'description', 'owner', 'comments', 'tags',
'name', 'slug', 'asns', 'description', 'comments', 'tags',
]
class ProviderAccountForm(PrimaryModelForm):
class ProviderAccountForm(NetBoxModelForm):
provider = DynamicModelChoiceField(
label=_('Provider'),
queryset=Provider.objects.all(),
selector=True,
quick_add=True
)
comments = CommentField()
class Meta:
model = ProviderAccount
fields = [
'provider', 'name', 'account', 'description', 'owner', 'comments', 'tags',
'provider', 'name', 'account', 'description', 'comments', 'tags',
]
class ProviderNetworkForm(PrimaryModelForm):
class ProviderNetworkForm(NetBoxModelForm):
provider = DynamicModelChoiceField(
label=_('Provider'),
queryset=Provider.objects.all(),
selector=True,
quick_add=True
)
comments = CommentField()
fieldsets = (
FieldSet('provider', 'name', 'service_id', 'description', 'tags'),
@@ -85,23 +88,25 @@ class ProviderNetworkForm(PrimaryModelForm):
class Meta:
model = ProviderNetwork
fields = [
'provider', 'name', 'service_id', 'description', 'owner', 'comments', 'tags',
'provider', 'name', 'service_id', 'description', 'comments', 'tags',
]
class CircuitTypeForm(OrganizationalModelForm):
class CircuitTypeForm(NetBoxModelForm):
slug = SlugField()
fieldsets = (
FieldSet('name', 'slug', 'color', 'description', 'owner', 'tags'),
FieldSet('name', 'slug', 'color', 'description', 'tags'),
)
class Meta:
model = CircuitType
fields = [
'name', 'slug', 'color', 'description', 'comments', 'tags',
'name', 'slug', 'color', 'description', 'tags',
]
class CircuitForm(DistanceValidationMixin, TenancyForm, PrimaryModelForm):
class CircuitForm(DistanceValidationMixin, TenancyForm, NetBoxModelForm):
provider = DynamicModelChoiceField(
label=_('Provider'),
queryset=Provider.objects.all(),
@@ -120,6 +125,7 @@ class CircuitForm(DistanceValidationMixin, TenancyForm, PrimaryModelForm):
queryset=CircuitType.objects.all(),
quick_add=True
)
comments = CommentField()
fieldsets = (
FieldSet(
@@ -141,7 +147,7 @@ class CircuitForm(DistanceValidationMixin, TenancyForm, PrimaryModelForm):
model = Circuit
fields = [
'cid', 'type', 'provider', 'provider_account', 'status', 'install_date', 'termination_date', 'commit_rate',
'distance', 'distance_unit', 'description', 'tenant_group', 'tenant', 'owner', 'comments', 'tags',
'distance', 'distance_unit', 'description', 'tenant_group', 'tenant', 'comments', 'tags',
]
widgets = {
'install_date': DatePicker(),
@@ -227,7 +233,9 @@ class CircuitTerminationForm(NetBoxModelForm):
self.instance.termination = self.cleaned_data.get('termination')
class CircuitGroupForm(TenancyForm, OrganizationalModelForm):
class CircuitGroupForm(TenancyForm, NetBoxModelForm):
slug = SlugField()
fieldsets = (
FieldSet('name', 'slug', 'description', 'tags', name=_('Circuit Group')),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
@@ -236,7 +244,7 @@ class CircuitGroupForm(TenancyForm, OrganizationalModelForm):
class Meta:
model = CircuitGroup
fields = [
'name', 'slug', 'description', 'tenant_group', 'tenant', 'owner', 'comments', 'tags',
'name', 'slug', 'description', 'tenant_group', 'tenant', 'tags',
]
@@ -299,7 +307,9 @@ class CircuitGroupAssignmentForm(NetBoxModelForm):
self.instance.member = self.cleaned_data.get('member')
class VirtualCircuitTypeForm(OrganizationalModelForm):
class VirtualCircuitTypeForm(NetBoxModelForm):
slug = SlugField()
fieldsets = (
FieldSet('name', 'slug', 'color', 'description', 'tags'),
)
@@ -307,11 +317,11 @@ class VirtualCircuitTypeForm(OrganizationalModelForm):
class Meta:
model = VirtualCircuitType
fields = [
'name', 'slug', 'color', 'description', 'owner', 'comments', 'tags',
'name', 'slug', 'color', 'description', 'tags',
]
class VirtualCircuitForm(TenancyForm, PrimaryModelForm):
class VirtualCircuitForm(TenancyForm, NetBoxModelForm):
provider_network = DynamicModelChoiceField(
label=_('Provider network'),
queryset=ProviderNetwork.objects.all(),
@@ -326,6 +336,7 @@ class VirtualCircuitForm(TenancyForm, PrimaryModelForm):
queryset=VirtualCircuitType.objects.all(),
quick_add=True
)
comments = CommentField()
fieldsets = (
FieldSet(
@@ -339,7 +350,7 @@ class VirtualCircuitForm(TenancyForm, PrimaryModelForm):
model = VirtualCircuit
fields = [
'cid', 'provider_network', 'provider_account', 'type', 'status', 'description', 'tenant_group', 'tenant',
'owner', 'comments', 'tags',
'comments', 'tags',
]

View File

@@ -3,18 +3,17 @@ from typing import Annotated, TYPE_CHECKING
import strawberry
import strawberry_django
from strawberry_django import BaseFilterLookup
from netbox.graphql.filter_mixins import OrganizationalModelFilterMixin
if TYPE_CHECKING:
from netbox.graphql.enums import ColorEnum
__all__ = (
'CircuitTypeFilterMixin',
'BaseCircuitTypeFilterMixin',
)
@dataclass
class CircuitTypeFilterMixin:
color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
class BaseCircuitTypeFilterMixin(OrganizationalModelFilterMixin):
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()

View File

@@ -4,15 +4,20 @@ from typing import Annotated, TYPE_CHECKING
import strawberry
import strawberry_django
from strawberry.scalars import ID
from strawberry_django import BaseFilterLookup, FilterLookup, DateFilterLookup
from strawberry_django import FilterLookup, DateFilterLookup
from circuits import models
from circuits.graphql.filter_mixins import CircuitTypeFilterMixin
from core.graphql.filter_mixins import BaseObjectTypeFilterMixin, ChangeLogFilterMixin
from dcim.graphql.filter_mixins import CabledObjectModelFilterMixin
from extras.graphql.filter_mixins import CustomFieldsFilterMixin, TagsFilterMixin
from netbox.graphql.filter_mixins import DistanceFilterMixin, ImageAttachmentFilterMixin
from netbox.graphql.filters import ChangeLoggedModelFilter, OrganizationalModelFilter, PrimaryModelFilter
from netbox.graphql.filter_mixins import (
DistanceFilterMixin,
ImageAttachmentFilterMixin,
OrganizationalModelFilterMixin,
PrimaryModelFilterMixin,
)
from tenancy.graphql.filter_mixins import ContactFilterMixin, TenancyFilterMixin
from .filter_mixins import BaseCircuitTypeFilterMixin
if TYPE_CHECKING:
from core.graphql.filters import ContentTypeFilter
@@ -38,17 +43,16 @@ __all__ = (
@strawberry_django.filter_type(models.CircuitTermination, lookups=True)
class CircuitTerminationFilter(
BaseObjectTypeFilterMixin,
CustomFieldsFilterMixin,
TagsFilterMixin,
ChangeLoggedModelFilter,
ChangeLogFilterMixin,
CabledObjectModelFilterMixin,
):
circuit: Annotated['CircuitFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field()
)
term_side: (
BaseFilterLookup[Annotated['CircuitTerminationSideEnum', strawberry.lazy('circuits.graphql.enums')]] | None
) = (
term_side: Annotated['CircuitTerminationSideEnum', strawberry.lazy('circuits.graphql.enums')] | None = (
strawberry_django.filter_field()
)
termination_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
@@ -89,7 +93,7 @@ class CircuitFilter(
ImageAttachmentFilterMixin,
DistanceFilterMixin,
TenancyFilterMixin,
PrimaryModelFilter
PrimaryModelFilterMixin
):
cid: FilterLookup[str] | None = strawberry_django.filter_field()
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
@@ -104,7 +108,7 @@ class CircuitFilter(
strawberry_django.filter_field()
)
type_id: ID | None = strawberry_django.filter_field()
status: BaseFilterLookup[Annotated['CircuitStatusEnum', strawberry.lazy('circuits.graphql.enums')]] | None = (
status: Annotated['CircuitStatusEnum', strawberry.lazy('circuits.graphql.enums')] | None = (
strawberry_django.filter_field()
)
install_date: DateFilterLookup[date] | None = strawberry_django.filter_field()
@@ -118,17 +122,19 @@ class CircuitFilter(
@strawberry_django.filter_type(models.CircuitType, lookups=True)
class CircuitTypeFilter(CircuitTypeFilterMixin, OrganizationalModelFilter):
class CircuitTypeFilter(BaseCircuitTypeFilterMixin):
pass
@strawberry_django.filter_type(models.CircuitGroup, lookups=True)
class CircuitGroupFilter(TenancyFilterMixin, OrganizationalModelFilter):
class CircuitGroupFilter(TenancyFilterMixin, OrganizationalModelFilterMixin):
pass
@strawberry_django.filter_type(models.CircuitGroupAssignment, lookups=True)
class CircuitGroupAssignmentFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
class CircuitGroupAssignmentFilter(
BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin
):
member_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@@ -137,13 +143,13 @@ class CircuitGroupAssignmentFilter(CustomFieldsFilterMixin, TagsFilterMixin, Cha
strawberry_django.filter_field()
)
group_id: ID | None = strawberry_django.filter_field()
priority: BaseFilterLookup[Annotated['CircuitPriorityEnum', strawberry.lazy('circuits.graphql.enums')]] | None = (
priority: Annotated['CircuitPriorityEnum', strawberry.lazy('circuits.graphql.enums')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter_type(models.Provider, lookups=True)
class ProviderFilter(ContactFilterMixin, PrimaryModelFilter):
class ProviderFilter(ContactFilterMixin, PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field()
slug: FilterLookup[str] | None = strawberry_django.filter_field()
asns: Annotated['ASNFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
@@ -153,7 +159,7 @@ class ProviderFilter(ContactFilterMixin, PrimaryModelFilter):
@strawberry_django.filter_type(models.ProviderAccount, lookups=True)
class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilter):
class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilterMixin):
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@@ -163,7 +169,7 @@ class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilter):
@strawberry_django.filter_type(models.ProviderNetwork, lookups=True)
class ProviderNetworkFilter(PrimaryModelFilter):
class ProviderNetworkFilter(PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field()
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field()
@@ -173,12 +179,12 @@ class ProviderNetworkFilter(PrimaryModelFilter):
@strawberry_django.filter_type(models.VirtualCircuitType, lookups=True)
class VirtualCircuitTypeFilter(CircuitTypeFilterMixin, OrganizationalModelFilter):
class VirtualCircuitTypeFilter(BaseCircuitTypeFilterMixin):
pass
@strawberry_django.filter_type(models.VirtualCircuit, lookups=True)
class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilter):
class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
cid: FilterLookup[str] | None = strawberry_django.filter_field()
provider_network: Annotated['ProviderNetworkFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field()
@@ -192,7 +198,7 @@ class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilter):
strawberry_django.filter_field()
)
type_id: ID | None = strawberry_django.filter_field()
status: BaseFilterLookup[Annotated['CircuitStatusEnum', strawberry.lazy('circuits.graphql.enums')]] | None = (
status: Annotated['CircuitStatusEnum', strawberry.lazy('circuits.graphql.enums')] | None = (
strawberry_django.filter_field()
)
group_assignments: Annotated['CircuitGroupAssignmentFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
@@ -201,16 +207,14 @@ class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilter):
@strawberry_django.filter_type(models.VirtualCircuitTermination, lookups=True)
class VirtualCircuitTerminationFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
class VirtualCircuitTerminationFilter(
BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin
):
virtual_circuit: Annotated['VirtualCircuitFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field()
)
virtual_circuit_id: ID | None = strawberry_django.filter_field()
role: (
BaseFilterLookup[
Annotated['VirtualCircuitTerminationRoleEnum', strawberry.lazy('circuits.graphql.enums')]
] | None
) = (
role: Annotated['VirtualCircuitTerminationRoleEnum', strawberry.lazy('circuits.graphql.enums')] | None = (
strawberry_django.filter_field()
)
interface: Annotated['InterfaceFilter', strawberry.lazy('dcim.graphql.filters')] | None = (

View File

@@ -6,7 +6,7 @@ import strawberry_django
from circuits import models
from dcim.graphql.mixins import CabledObjectMixin
from extras.graphql.mixins import ContactsMixin, CustomFieldsMixin, TagsMixin
from netbox.graphql.types import BaseObjectType, ObjectType, OrganizationalObjectType, PrimaryObjectType
from netbox.graphql.types import BaseObjectType, NetBoxObjectType, ObjectType, OrganizationalObjectType
from tenancy.graphql.types import TenantType
from .filters import *
@@ -35,7 +35,8 @@ __all__ = (
filters=ProviderFilter,
pagination=True
)
class ProviderType(ContactsMixin, PrimaryObjectType):
class ProviderType(NetBoxObjectType, ContactsMixin):
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')]]
@@ -48,8 +49,9 @@ class ProviderType(ContactsMixin, PrimaryObjectType):
filters=ProviderAccountFilter,
pagination=True
)
class ProviderAccountType(ContactsMixin, PrimaryObjectType):
class ProviderAccountType(ContactsMixin, NetBoxObjectType):
provider: Annotated["ProviderType", strawberry.lazy('circuits.graphql.types')]
circuits: List[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]
@@ -59,8 +61,9 @@ class ProviderAccountType(ContactsMixin, PrimaryObjectType):
filters=ProviderNetworkFilter,
pagination=True
)
class ProviderNetworkType(PrimaryObjectType):
class ProviderNetworkType(NetBoxObjectType):
provider: Annotated["ProviderType", strawberry.lazy('circuits.graphql.types')]
circuit_terminations: List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]
@@ -102,13 +105,14 @@ class CircuitTypeType(OrganizationalObjectType):
filters=CircuitFilter,
pagination=True
)
class CircuitType(PrimaryObjectType, ContactsMixin):
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]
@@ -174,11 +178,12 @@ class VirtualCircuitTerminationType(CustomFieldsMixin, TagsMixin, ObjectType):
filters=VirtualCircuitFilter,
pagination=True
)
class VirtualCircuitType(PrimaryObjectType):
class VirtualCircuitType(NetBoxObjectType):
provider_network: ProviderNetworkType = strawberry_django.field(select_related=["provider_network"])
provider_account: ProviderAccountType | None
type: Annotated["VirtualCircuitTypeType", strawberry.lazy('circuits.graphql.types')] = strawberry_django.field(
select_related=["type"]
)
tenant: TenantType | None
terminations: List[VirtualCircuitTerminationType]

View File

@@ -1,68 +0,0 @@
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0052_extend_circuit_abs_distance_upper_limit'),
('users', '0015_owner'),
]
operations = [
migrations.AddField(
model_name='circuit',
name='owner',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner'
),
),
migrations.AddField(
model_name='circuitgroup',
name='owner',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner'
),
),
migrations.AddField(
model_name='circuittype',
name='owner',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner'
),
),
migrations.AddField(
model_name='provider',
name='owner',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner'
),
),
migrations.AddField(
model_name='provideraccount',
name='owner',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner'
),
),
migrations.AddField(
model_name='providernetwork',
name='owner',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner'
),
),
migrations.AddField(
model_name='virtualcircuit',
name='owner',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner'
),
),
migrations.AddField(
model_name='virtualcircuittype',
name='owner',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner'
),
),
]

View File

@@ -1,39 +0,0 @@
import django.contrib.postgres.fields
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0053_owner'),
]
operations = [
migrations.AddField(
model_name='circuittermination',
name='cable_connector',
field=models.PositiveSmallIntegerField(
blank=True,
null=True,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(256)
],
),
),
migrations.AddField(
model_name='circuittermination',
name='cable_positions',
field=django.contrib.postgres.fields.ArrayField(
base_field=models.PositiveSmallIntegerField(
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(1024),
]
),
blank=True,
null=True,
size=None,
),
),
]

View File

@@ -1,28 +0,0 @@
# Generated by Django 5.2.8 on 2025-12-08 17:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0054_cable_connector_positions'),
]
operations = [
migrations.AddField(
model_name='circuitgroup',
name='comments',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='circuittype',
name='comments',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='virtualcircuittype',
name='comments',
field=models.TextField(blank=True),
),
]

View File

@@ -1,17 +0,0 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0055_add_comments_to_organizationalmodel'),
('contenttypes', '0002_remove_content_type_name'),
('dcim', '0224_add_comments_to_organizationalmodel'),
('extras', '0134_owner'),
]
operations = [
migrations.AddIndex(
model_name='circuittermination',
index=models.Index(fields=['termination_type', 'termination_id'], name='circuits_ci_termina_505dda_idx'),
),
]

View File

@@ -335,9 +335,6 @@ class CircuitTermination(
name='%(app_label)s_%(class)s_unique_circuit_term_side'
),
)
indexes = (
models.Index(fields=('termination_type', 'termination_id')),
)
verbose_name = _('circuit termination')
verbose_name_plural = _('circuit terminations')

View File

@@ -20,7 +20,6 @@ class CircuitGroupIndex(SearchIndex):
('name', 100),
('slug', 110),
('description', 500),
('comments', 5000),
)
display_attrs = ('description',)
@@ -45,7 +44,6 @@ class CircuitTypeIndex(SearchIndex):
('name', 100),
('slug', 110),
('description', 500),
('comments', 5000),
)
display_attrs = ('description',)
@@ -111,6 +109,5 @@ class VirtualCircuitTypeIndex(SearchIndex):
('name', 100),
('slug', 110),
('description', 500),
('comments', 5000),
)
display_attrs = ('description',)

View File

@@ -1,9 +1,11 @@
import django_tables2 as tables
from django.utils.translation import gettext_lazy as _
import django_tables2 as tables
from circuits.models import *
from netbox.tables import NetBoxTable, OrganizationalModelTable, PrimaryModelTable, columns
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
from netbox.tables import NetBoxTable, columns
from .columns import CommitRateColumn
__all__ = (
@@ -22,7 +24,7 @@ CIRCUITTERMINATION_LINK = """
"""
class CircuitTypeTable(OrganizationalModelTable):
class CircuitTypeTable(NetBoxTable):
name = tables.Column(
linkify=True,
verbose_name=_('Name'),
@@ -37,16 +39,16 @@ class CircuitTypeTable(OrganizationalModelTable):
verbose_name=_('Circuits')
)
class Meta(OrganizationalModelTable.Meta):
class Meta(NetBoxTable.Meta):
model = CircuitType
fields = (
'pk', 'id', 'name', 'circuit_count', 'color', 'description', 'slug', 'comments', 'tags', 'created',
'last_updated', 'actions',
'pk', 'id', 'name', 'circuit_count', 'color', 'description', 'slug', 'tags', 'created', 'last_updated',
'actions',
)
default_columns = ('pk', 'name', 'circuit_count', 'color', 'description')
class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
cid = tables.Column(
linkify=True,
verbose_name=_('Circuit ID')
@@ -77,6 +79,9 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
verbose_name=_('Commit Rate')
)
distance = columns.DistanceColumn()
comments = columns.MarkdownColumn(
verbose_name=_('Comments')
)
tags = columns.TagColumn(
url_name='circuits:circuit_list'
)
@@ -85,7 +90,7 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
linkify_item=True
)
class Meta(PrimaryModelTable.Meta):
class Meta(NetBoxTable.Meta):
model = Circuit
fields = (
'pk', 'id', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'tenant_group',
@@ -158,7 +163,7 @@ class CircuitTerminationTable(NetBoxTable):
)
class CircuitGroupTable(OrganizationalModelTable):
class CircuitGroupTable(NetBoxTable):
name = tables.Column(
verbose_name=_('Name'),
linkify=True
@@ -172,10 +177,10 @@ class CircuitGroupTable(OrganizationalModelTable):
url_name='circuits:circuitgroup_list'
)
class Meta(OrganizationalModelTable.Meta):
class Meta(NetBoxTable.Meta):
model = CircuitGroup
fields = (
'pk', 'name', 'description', 'circuit_group_assignment_count', 'comments', 'tags',
'pk', 'name', 'description', 'circuit_group_assignment_count', 'tags',
'created', 'last_updated', 'actions',
)
default_columns = ('pk', 'name', 'description', 'circuit_group_assignment_count')

View File

@@ -1,11 +1,11 @@
import django_tables2 as tables
from django.utils.translation import gettext_lazy as _
from django_tables2.utils import Accessor
from circuits.models import *
from netbox.tables import PrimaryModelTable, columns
from django_tables2.utils import Accessor
from tenancy.tables import ContactsColumnMixin
from netbox.tables import NetBoxTable, columns
__all__ = (
'ProviderTable',
'ProviderAccountTable',
@@ -13,7 +13,7 @@ __all__ = (
)
class ProviderTable(ContactsColumnMixin, PrimaryModelTable):
class ProviderTable(ContactsColumnMixin, NetBoxTable):
name = tables.Column(
verbose_name=_('Name'),
linkify=True
@@ -42,11 +42,14 @@ class ProviderTable(ContactsColumnMixin, PrimaryModelTable):
url_params={'provider_id': 'pk'},
verbose_name=_('Circuits')
)
comments = columns.MarkdownColumn(
verbose_name=_('Comments'),
)
tags = columns.TagColumn(
url_name='circuits:provider_list'
)
class Meta(PrimaryModelTable.Meta):
class Meta(NetBoxTable.Meta):
model = Provider
fields = (
'pk', 'id', 'name', 'accounts', 'account_count', 'asns', 'asn_count', 'circuit_count', 'description',
@@ -55,7 +58,7 @@ class ProviderTable(ContactsColumnMixin, PrimaryModelTable):
default_columns = ('pk', 'name', 'account_count', 'circuit_count')
class ProviderAccountTable(ContactsColumnMixin, PrimaryModelTable):
class ProviderAccountTable(ContactsColumnMixin, NetBoxTable):
account = tables.Column(
linkify=True,
verbose_name=_('Account'),
@@ -73,11 +76,14 @@ class ProviderAccountTable(ContactsColumnMixin, PrimaryModelTable):
url_params={'provider_account_id': 'pk'},
verbose_name=_('Circuits')
)
comments = columns.MarkdownColumn(
verbose_name=_('Comments'),
)
tags = columns.TagColumn(
url_name='circuits:provideraccount_list'
)
class Meta(PrimaryModelTable.Meta):
class Meta(NetBoxTable.Meta):
model = ProviderAccount
fields = (
'pk', 'id', 'account', 'name', 'provider', 'circuit_count', 'comments', 'contacts', 'tags', 'created',
@@ -86,7 +92,7 @@ class ProviderAccountTable(ContactsColumnMixin, PrimaryModelTable):
default_columns = ('pk', 'account', 'name', 'provider', 'circuit_count')
class ProviderNetworkTable(PrimaryModelTable):
class ProviderNetworkTable(NetBoxTable):
name = tables.Column(
verbose_name=_('Name'),
linkify=True
@@ -95,11 +101,14 @@ class ProviderNetworkTable(PrimaryModelTable):
verbose_name=_('Provider'),
linkify=True
)
comments = columns.MarkdownColumn(
verbose_name=_('Comments'),
)
tags = columns.TagColumn(
url_name='circuits:providernetwork_list'
)
class Meta(PrimaryModelTable.Meta):
class Meta(NetBoxTable.Meta):
model = ProviderNetwork
fields = (
'pk', 'id', 'name', 'provider', 'service_id', 'description', 'comments', 'created', 'last_updated', 'tags',

View File

@@ -2,7 +2,7 @@ import django_tables2 as tables
from django.utils.translation import gettext_lazy as _
from circuits.models import *
from netbox.tables import NetBoxTable, OrganizationalModelTable, PrimaryModelTable, columns
from netbox.tables import NetBoxTable, columns
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
__all__ = (
@@ -12,7 +12,7 @@ __all__ = (
)
class VirtualCircuitTypeTable(OrganizationalModelTable):
class VirtualCircuitTypeTable(NetBoxTable):
name = tables.Column(
linkify=True,
verbose_name=_('Name'),
@@ -27,7 +27,7 @@ class VirtualCircuitTypeTable(OrganizationalModelTable):
verbose_name=_('Circuits')
)
class Meta(OrganizationalModelTable.Meta):
class Meta(NetBoxTable.Meta):
model = VirtualCircuitType
fields = (
'pk', 'id', 'name', 'virtual_circuit_count', 'color', 'description', 'slug', 'tags', 'created',
@@ -36,7 +36,7 @@ class VirtualCircuitTypeTable(OrganizationalModelTable):
default_columns = ('pk', 'name', 'virtual_circuit_count', 'color', 'description')
class VirtualCircuitTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
class VirtualCircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
cid = tables.Column(
linkify=True,
verbose_name=_('Circuit ID')
@@ -63,11 +63,14 @@ class VirtualCircuitTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModel
url_params={'virtual_circuit_id': 'pk'},
verbose_name=_('Terminations')
)
comments = columns.MarkdownColumn(
verbose_name=_('Comments')
)
tags = columns.TagColumn(
url_name='circuits:virtualcircuit_list'
)
class Meta(PrimaryModelTable.Meta):
class Meta(NetBoxTable.Meta):
model = VirtualCircuit
fields = (
'pk', 'id', 'cid', 'provider', 'provider_account', 'provider_network', 'type', 'status', 'tenant',

View File

@@ -433,7 +433,7 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = CircuitTermination.objects.all()
filterset = CircuitTerminationFilterSet
ignore_fields = ('cable', 'cable_positions')
ignore_fields = ('cable',)
@classmethod
def setUpTestData(cls):

View File

@@ -18,6 +18,11 @@ urlpatterns = [
path('circuit-types/<int:pk>/', include(get_model_urls('circuits', 'circuittype'))),
path('circuits/', include(get_model_urls('circuits', 'circuit', detail=False))),
path(
'circuits/<int:pk>/terminations/swap/',
views.CircuitSwapTerminations.as_view(),
name='circuit_terminations_swap'
),
path('circuits/<int:pk>/', include(get_model_urls('circuits', 'circuit'))),
path('circuit-terminations/', include(get_model_urls('circuits', 'circuittermination', detail=False))),

View File

@@ -1,8 +1,13 @@
from django.contrib import messages
from django.db import router, transaction
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.translation import gettext_lazy as _
from dcim.views import PathTraceView
from ipam.models import ASN
from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport
from netbox.views import generic
from utilities.forms import ConfirmationForm
from utilities.query import count_related
from utilities.views import GetRelatedModelsMixin, register_model_view
from . import filtersets, forms, tables
@@ -78,7 +83,6 @@ class ProviderBulkEditView(generic.BulkEditView):
@register_model_view(Provider, 'bulk_rename', path='rename', detail=False)
class ProviderBulkRenameView(generic.BulkRenameView):
queryset = Provider.objects.all()
filterset = filtersets.ProviderFilterSet
@register_model_view(Provider, 'bulk_delete', path='delete', detail=False)
@@ -146,7 +150,6 @@ class ProviderAccountBulkEditView(generic.BulkEditView):
@register_model_view(ProviderAccount, 'bulk_rename', path='rename', detail=False)
class ProviderAccountBulkRenameView(generic.BulkRenameView):
queryset = ProviderAccount.objects.all()
filterset = filtersets.ProviderAccountFilterSet
@register_model_view(ProviderAccount, 'bulk_delete', path='delete', detail=False)
@@ -223,7 +226,6 @@ class ProviderNetworkBulkEditView(generic.BulkEditView):
@register_model_view(ProviderNetwork, 'bulk_rename', path='rename', detail=False)
class ProviderNetworkBulkRenameView(generic.BulkRenameView):
queryset = ProviderNetwork.objects.all()
filterset = filtersets.ProviderNetworkFilterSet
@register_model_view(ProviderNetwork, 'bulk_delete', path='delete', detail=False)
@@ -288,7 +290,6 @@ class CircuitTypeBulkEditView(generic.BulkEditView):
@register_model_view(CircuitType, 'bulk_rename', path='rename', detail=False)
class CircuitTypeBulkRenameView(generic.BulkRenameView):
queryset = CircuitType.objects.all()
filterset = filtersets.CircuitTypeFilterSet
@register_model_view(CircuitType, 'bulk_delete', path='delete', detail=False)
@@ -361,7 +362,6 @@ class CircuitBulkEditView(generic.BulkEditView):
class CircuitBulkRenameView(generic.BulkRenameView):
queryset = Circuit.objects.all()
field_name = 'cid'
filterset = filtersets.CircuitFilterSet
@register_model_view(Circuit, 'bulk_delete', path='delete', detail=False)
@@ -373,6 +373,82 @@ class CircuitBulkDeleteView(generic.BulkDeleteView):
table = tables.CircuitTable
class CircuitSwapTerminations(generic.ObjectEditView):
"""
Swap the A and Z terminations of a circuit.
"""
queryset = Circuit.objects.all()
def get(self, request, pk):
circuit = get_object_or_404(self.queryset, pk=pk)
form = ConfirmationForm()
# Circuit must have at least one termination to swap
if not circuit.termination_a and not circuit.termination_z:
messages.error(request, _(
"No terminations have been defined for circuit {circuit}."
).format(circuit=circuit))
return redirect('circuits:circuit', pk=circuit.pk)
return render(request, 'circuits/circuit_terminations_swap.html', {
'circuit': circuit,
'termination_a': circuit.termination_a,
'termination_z': circuit.termination_z,
'form': form,
'panel_class': 'light',
'button_class': 'primary',
'return_url': circuit.get_absolute_url(),
})
def post(self, request, pk):
circuit = get_object_or_404(self.queryset, pk=pk)
form = ConfirmationForm(request.POST)
if form.is_valid():
termination_a = CircuitTermination.objects.filter(pk=circuit.termination_a_id).first()
termination_z = CircuitTermination.objects.filter(pk=circuit.termination_z_id).first()
if termination_a and termination_z:
# Use a placeholder to avoid an IntegrityError on the (circuit, term_side) unique constraint
with transaction.atomic(using=router.db_for_write(CircuitTermination)):
termination_a.term_side = '_'
termination_a.save()
termination_z.term_side = 'A'
termination_z.save()
termination_a.term_side = 'Z'
termination_a.save()
circuit.refresh_from_db()
circuit.termination_a = termination_z
circuit.termination_z = termination_a
circuit.save()
elif termination_a:
termination_a.term_side = 'Z'
termination_a.save()
circuit.refresh_from_db()
circuit.termination_a = None
circuit.save()
else:
termination_z.term_side = 'A'
termination_z.save()
circuit.refresh_from_db()
circuit.termination_z = None
circuit.save()
messages.success(request, _("Swapped terminations for circuit {circuit}.").format(circuit=circuit))
return redirect('circuits:circuit', pk=circuit.pk)
return render(request, 'circuits/circuit_terminations_swap.html', {
'circuit': circuit,
'termination_a': circuit.termination_a,
'termination_z': circuit.termination_z,
'form': form,
'panel_class': 'default',
'button_class': 'primary',
'return_url': circuit.get_absolute_url(),
})
#
# Circuit terminations
#
@@ -481,7 +557,6 @@ class CircuitGroupBulkEditView(generic.BulkEditView):
@register_model_view(CircuitGroup, 'bulk_rename', path='rename', detail=False)
class CircuitGroupBulkRenameView(generic.BulkRenameView):
queryset = CircuitGroup.objects.all()
filterset = filtersets.CircuitGroupFilterSet
@register_model_view(CircuitGroup, 'bulk_delete', path='delete', detail=False)
@@ -597,7 +672,6 @@ class VirtualCircuitTypeBulkEditView(generic.BulkEditView):
@register_model_view(VirtualCircuitType, 'bulk_rename', path='rename', detail=False)
class VirtualCircuitTypeBulkRenameView(generic.BulkRenameView):
queryset = VirtualCircuitType.objects.all()
filterset = filtersets.VirtualCircuitTypeFilterSet
@register_model_view(VirtualCircuitType, 'bulk_delete', path='delete', detail=False)
@@ -670,7 +744,6 @@ class VirtualCircuitBulkEditView(generic.BulkEditView):
class VirtualCircuitBulkRenameView(generic.BulkRenameView):
queryset = VirtualCircuit.objects.all()
field_name = 'cid'
filterset = filtersets.VirtualCircuitFilterSet
@register_model_view(VirtualCircuit, 'bulk_delete', path='delete', detail=False)

View File

@@ -12,7 +12,6 @@ from drf_spectacular.utils import Direction
from netbox.api.fields import ChoiceField
from netbox.api.serializers import WritableNestedSerializer
from netbox.api.viewsets import NetBoxModelViewSet
# see netbox.api.routers.NetBoxRouter
BULK_ACTIONS = ("bulk_destroy", "bulk_partial_update", "bulk_update")
@@ -50,11 +49,6 @@ class ChoiceFieldFix(OpenApiSerializerFieldExtension):
)
def viewset_handles_bulk_create(view):
"""Check if view automatically provides list-based bulk create"""
return isinstance(view, NetBoxModelViewSet)
class NetBoxAutoSchema(AutoSchema):
"""
Overrides to drf_spectacular.openapi.AutoSchema to fix following issues:
@@ -134,36 +128,6 @@ class NetBoxAutoSchema(AutoSchema):
return response_serializers
def _get_request_for_media_type(self, serializer, direction='request'):
"""
Override to generate oneOf schema for serializers that support both
single object and array input (NetBoxModelViewSet POST operations).
Refs: #20638
"""
# Get the standard schema first
schema, required = super()._get_request_for_media_type(serializer, direction)
# If this serializer supports arrays (marked in get_request_serializer),
# wrap the schema in oneOf to allow single object OR array
if (
direction == 'request' and
schema is not None and
getattr(self.view, 'action', None) == 'create' and
viewset_handles_bulk_create(self.view)
):
return {
'oneOf': [
schema, # Single object
{
'type': 'array',
'items': schema, # Array of objects
}
]
}, required
return schema, required
def _get_serializer_name(self, serializer, direction, bypass_extensions=False) -> str:
name = super()._get_serializer_name(serializer, direction, bypass_extensions)
@@ -318,18 +282,18 @@ class FixSerializedPKRelatedField(OpenApiSerializerFieldExtension):
class FixIntegerRangeSerializerSchema(OpenApiSerializerExtension):
target_class = 'netbox.api.fields.IntegerRangeSerializer'
match_subclasses = True
def map_serializer(self, auto_schema: 'AutoSchema', direction: Direction) -> _SchemaType:
# One range = two integers; many=True will wrap this in an outer array
return {
'type': 'array',
'items': {
'type': 'integer',
'type': 'array',
'items': {
'type': 'integer',
},
'minItems': 2,
'maxItems': 2,
},
'minItems': 2,
'maxItems': 2,
'example': [10, 20],
}

View File

@@ -1,11 +1,13 @@
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from core.choices import *
from core.models import ObjectChange
from netbox.api.exceptions import SerializerNotFound
from netbox.api.fields import ChoiceField, ContentTypeField
from netbox.api.gfk_fields import GFKSerializerField
from netbox.api.serializers import BaseModelSerializer
from users.api.serializers_.users import UserSerializer
from utilities.api import get_serializer_for_model
__all__ = (
'ObjectChangeSerializer',
@@ -24,10 +26,7 @@ class ObjectChangeSerializer(BaseModelSerializer):
changed_object_type = ContentTypeField(
read_only=True
)
changed_object = GFKSerializerField(
read_only=True
)
object_repr = serializers.CharField(
changed_object = serializers.SerializerMethodField(
read_only=True
)
prechange_data = serializers.JSONField(
@@ -45,6 +44,22 @@ class ObjectChangeSerializer(BaseModelSerializer):
model = ObjectChange
fields = [
'id', 'url', 'display_url', 'display', 'time', 'user', 'user_name', 'request_id', 'action',
'changed_object_type', 'changed_object_id', 'changed_object', 'object_repr', 'message',
'prechange_data', 'postchange_data',
'changed_object_type', 'changed_object_id', 'changed_object', 'message', 'prechange_data',
'postchange_data',
]
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_changed_object(self, obj):
"""
Serialize a nested representation of the changed object.
"""
if obj.changed_object is None:
return None
try:
serializer = get_serializer_for_model(obj.changed_object)
except SerializerNotFound:
return obj.object_repr
data = serializer(obj.changed_object, nested=True, context={'request': self.context['request']}).data
return data

View File

@@ -1,7 +1,7 @@
from core.choices import *
from core.models import DataFile, DataSource
from netbox.api.fields import ChoiceField, RelatedObjectCountField
from netbox.api.serializers import NetBoxModelSerializer, PrimaryModelSerializer
from netbox.api.serializers import NetBoxModelSerializer
from netbox.utils import get_data_backend_choices
__all__ = (
@@ -10,7 +10,7 @@ __all__ = (
)
class DataSourceSerializer(PrimaryModelSerializer):
class DataSourceSerializer(NetBoxModelSerializer):
type = ChoiceField(
choices=get_data_backend_choices()
)
@@ -26,8 +26,8 @@ class DataSourceSerializer(PrimaryModelSerializer):
model = DataSource
fields = [
'id', 'url', 'display_url', 'display', 'name', 'type', 'source_url', 'enabled', 'status', 'description',
'sync_interval', 'parameters', 'ignore_rules', 'owner', 'comments', 'custom_fields', 'created',
'last_updated', 'last_synced', 'file_count',
'sync_interval', 'parameters', 'ignore_rules', 'comments', 'custom_fields', 'created', 'last_updated',
'last_synced', 'file_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')

View File

@@ -1,13 +1,8 @@
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from core.choices import *
from core.models import Job
from netbox.api.exceptions import SerializerNotFound
from netbox.api.fields import ChoiceField, ContentTypeField
from netbox.api.serializers import BaseModelSerializer
from users.api.serializers_.users import UserSerializer
from utilities.api import get_serializer_for_model
__all__ = (
'JobSerializer',
@@ -23,28 +18,11 @@ class JobSerializer(BaseModelSerializer):
object_type = ContentTypeField(
read_only=True
)
object = serializers.SerializerMethodField(
read_only=True
)
class Meta:
model = Job
fields = [
'id', 'url', 'display_url', 'display', 'object_type', 'object_id', 'object', 'name', 'status', 'created',
'scheduled', 'interval', 'started', 'completed', 'user', 'data', 'error', 'job_id', 'log_entries',
'id', 'url', 'display_url', 'display', 'object_type', 'object_id', 'name', 'status', 'created', 'scheduled',
'interval', 'started', 'completed', 'user', 'data', 'error', 'job_id', 'log_entries',
]
brief_fields = ('url', 'created', 'completed', 'user', 'status')
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_object(self, obj):
"""
Serialize a nested representation of the object.
"""
if obj.object is None:
return None
try:
serializer = get_serializer_for_model(obj.object)
except SerializerNotFound:
return obj.object_repr
context = {'request': self.context['request']}
return serializer(obj.object, nested=True, context=context).data

View File

@@ -9,6 +9,7 @@ from drf_spectacular.utils import OpenApiParameter, extend_schema
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from rest_framework.permissions import IsAdminUser
from rest_framework.response import Response
from rest_framework.routers import APIRootView
from rest_framework.viewsets import ReadOnlyModelViewSet
@@ -23,7 +24,7 @@ from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
from netbox.api.metadata import ContentTypeMetadata
from netbox.api.pagination import LimitOffsetListPagination
from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet
from utilities.api import IsSuperuser
from . import serializers
@@ -99,7 +100,7 @@ class BaseRQViewSet(viewsets.ViewSet):
"""
Base class for RQ view sets. Provides a list() method. Subclasses must implement get_data().
"""
permission_classes = [IsSuperuser]
permission_classes = [IsAdminUser]
serializer_class = None
def get_data(self):

View File

@@ -3,11 +3,10 @@ from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.utils.translation import gettext as _
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, PrimaryModelFilterSet
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet
from netbox.utils import get_data_backend_choices
from users.models import User
from utilities.filters import ContentTypeFilter
from utilities.filtersets import register_filterset
from .choices import *
from .models import *
@@ -21,8 +20,7 @@ __all__ = (
)
@register_filterset
class DataSourceFilterSet(PrimaryModelFilterSet):
class DataSourceFilterSet(NetBoxModelFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=get_data_backend_choices,
null_value=None
@@ -50,7 +48,6 @@ class DataSourceFilterSet(PrimaryModelFilterSet):
)
@register_filterset
class DataFileFilterSet(ChangeLoggedModelFilterSet):
q = django_filters.CharFilter(
method='search'
@@ -78,17 +75,11 @@ class DataFileFilterSet(ChangeLoggedModelFilterSet):
)
@register_filterset
class JobFilterSet(BaseFilterSet):
q = django_filters.CharFilter(
method='search',
label=_('Search'),
)
object_type_id = django_filters.ModelMultipleChoiceFilter(
queryset=ObjectType.objects.with_feature('jobs'),
field_name='object_type_id',
)
object_type = ContentTypeFilter()
created = django_filters.DateTimeFilter()
created__before = django_filters.DateTimeFilter(
field_name='created',
@@ -132,7 +123,7 @@ class JobFilterSet(BaseFilterSet):
class Meta:
model = Job
fields = ('id', 'object_type', 'object_type_id', 'object_id', 'name', 'interval', 'status', 'user', 'job_id')
fields = ('id', 'object_type', 'object_id', 'name', 'interval', 'status', 'user', 'job_id')
def search(self, queryset, name, value):
if not value.strip():
@@ -143,7 +134,6 @@ class JobFilterSet(BaseFilterSet):
)
@register_filterset
class ObjectTypeFilterSet(BaseFilterSet):
q = django_filters.CharFilter(
method='search',
@@ -169,7 +159,6 @@ class ObjectTypeFilterSet(BaseFilterSet):
return queryset.filter(features__icontains=value)
@register_filterset
class ObjectChangeFilterSet(BaseFilterSet):
q = django_filters.CharFilter(
method='search',
@@ -180,7 +169,6 @@ class ObjectChangeFilterSet(BaseFilterSet):
changed_object_type_id = django_filters.ModelMultipleChoiceFilter(
queryset=ContentType.objects.all()
)
related_object_type = ContentTypeFilter()
user_id = django_filters.ModelMultipleChoiceFilter(
queryset=User.objects.all(),
label=_('User (ID)'),
@@ -209,7 +197,6 @@ class ObjectChangeFilterSet(BaseFilterSet):
)
@register_filterset
class ConfigRevisionFilterSet(BaseFilterSet):
q = django_filters.CharFilter(
method='search',

View File

@@ -3,8 +3,9 @@ from django.utils.translation import gettext_lazy as _
from core.choices import JobIntervalChoices
from core.models import *
from netbox.forms import PrimaryModelBulkEditForm
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
@@ -13,7 +14,7 @@ __all__ = (
)
class DataSourceBulkEditForm(PrimaryModelBulkEditForm):
class DataSourceBulkEditForm(NetBoxModelBulkEditForm):
type = forms.ChoiceField(
label=_('Type'),
choices=get_data_backend_choices,
@@ -24,11 +25,17 @@ class DataSourceBulkEditForm(PrimaryModelBulkEditForm):
widget=BulkEditNullBooleanSelect(),
label=_('Enabled')
)
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
sync_interval = forms.ChoiceField(
choices=JobIntervalChoices,
required=False,
label=_('Sync interval')
)
comments = CommentField()
parameters = forms.JSONField(
label=_('Parameters'),
required=False

View File

@@ -1,16 +1,16 @@
from core.models import *
from netbox.forms import PrimaryModelImportForm
from netbox.forms import NetBoxModelImportForm
__all__ = (
'DataSourceImportForm',
)
class DataSourceImportForm(PrimaryModelImportForm):
class DataSourceImportForm(NetBoxModelImportForm):
class Meta:
model = DataSource
fields = (
'name', 'type', 'source_url', 'enabled', 'description', 'sync_interval', 'parameters', 'ignore_rules',
'owner', 'comments',
'comments',
)

View File

@@ -3,13 +3,13 @@ from django.utils.translation import gettext_lazy as _
from core.choices import *
from core.models import *
from netbox.forms import NetBoxModelFilterSetForm, PrimaryModelFilterSetForm
from netbox.forms import NetBoxModelFilterSetForm
from netbox.forms.mixins import SavedFiltersMixin
from netbox.utils import get_data_backend_choices
from users.models import User
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
from utilities.forms.fields import (
ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField,
ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField,
)
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import DateTimePicker
@@ -23,10 +23,10 @@ __all__ = (
)
class DataSourceFilterForm(PrimaryModelFilterSetForm):
class DataSourceFilterForm(NetBoxModelFilterSetForm):
model = DataSource
fieldsets = (
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
FieldSet('q', 'filter_id'),
FieldSet('type', 'status', 'enabled', 'sync_interval', name=_('Data Source')),
)
type = forms.MultipleChoiceField(
@@ -51,7 +51,6 @@ class DataSourceFilterForm(PrimaryModelFilterSetForm):
choices=JobIntervalChoices,
required=False
)
tag = TagFilterField(model)
class DataFileFilterForm(NetBoxModelFilterSetForm):
@@ -71,13 +70,13 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
model = Job
fieldsets = (
FieldSet('q', 'filter_id'),
FieldSet('object_type_id', 'status', name=_('Attributes')),
FieldSet('object_type', 'status', name=_('Attributes')),
FieldSet(
'created__before', 'created__after', 'scheduled__before', 'scheduled__after', 'started__before',
'started__after', 'completed__before', 'completed__after', 'user', name=_('Creation')
),
)
object_type_id = ContentTypeChoiceField(
object_type = ContentTypeChoiceField(
label=_('Object Type'),
queryset=ObjectType.objects.with_feature('jobs'),
required=False,

View File

@@ -9,11 +9,11 @@ from django.utils.translation import gettext_lazy as _
from core.forms.mixins import SyncedDataMixin
from core.models import *
from netbox.config import get_config, PARAMS
from netbox.forms import NetBoxModelForm, PrimaryModelForm
from netbox.forms import NetBoxModelForm
from netbox.registry import registry
from netbox.utils import get_data_backend_choices
from utilities.forms import get_field_value
from utilities.forms.fields import JSONField
from utilities.forms.fields import CommentField, JSONField
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import HTMXSelect
@@ -26,17 +26,17 @@ __all__ = (
EMPTY_VALUES = ('', None, [], ())
class DataSourceForm(PrimaryModelForm):
class DataSourceForm(NetBoxModelForm):
type = forms.ChoiceField(
choices=get_data_backend_choices,
widget=HTMXSelect()
)
comments = CommentField()
class Meta:
model = DataSource
fields = [
'name', 'type', 'source_url', 'enabled', 'description', 'sync_interval', 'ignore_rules', 'owner',
'comments', 'tags',
'name', 'type', 'source_url', 'enabled', 'description', 'sync_interval', 'ignore_rules', 'comments', 'tags',
]
widgets = {
'ignore_rules': forms.Textarea(
@@ -166,8 +166,8 @@ class ConfigRevisionForm(forms.ModelForm, metaclass=ConfigFormMetaclass):
FieldSet('CUSTOM_VALIDATORS', 'PROTECTION_RULES', name=_('Validation')),
FieldSet('DEFAULT_USER_PREFERENCES', name=_('User Preferences')),
FieldSet(
'MAINTENANCE_MODE', 'COPILOT_ENABLED', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'JOB_RETENTION',
'MAPS_URL', name=_('Miscellaneous'),
'MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'JOB_RETENTION', 'MAPS_URL',
name=_('Miscellaneous')
),
FieldSet('comment', name=_('Config Revision'))
)

View File

@@ -1,11 +0,0 @@
import strawberry
from core.choices import *
__all__ = (
'DataSourceStatusEnum',
'ObjectChangeActionEnum',
)
DataSourceStatusEnum = strawberry.enum(DataSourceStatusChoices.as_enum(prefix='status'))
ObjectChangeActionEnum = strawberry.enum(ObjectChangeActionChoices.as_enum(prefix='action'))

View File

@@ -4,19 +4,31 @@ from typing import Annotated, TYPE_CHECKING
import strawberry
import strawberry_django
from strawberry import ID
from strawberry_django import DatetimeFilterLookup
if TYPE_CHECKING:
from .filters import *
__all__ = (
'ChangeLoggingMixin',
'BaseFilterMixin',
'BaseObjectTypeFilterMixin',
'ChangeLogFilterMixin',
)
# @strawberry.input
class BaseFilterMixin: ...
@dataclass
class ChangeLoggingMixin:
# TODO: "changelog" is not a valid field name; needs to be updated for ObjectChange
class BaseObjectTypeFilterMixin(BaseFilterMixin):
id: ID | None = strawberry.UNSET
@dataclass
class ChangeLogFilterMixin(BaseFilterMixin):
id: ID | None = strawberry.UNSET
changelog: Annotated['ObjectChangeFilter', strawberry.lazy('core.graphql.filters')] | None = (
strawberry_django.filter_field()
)

View File

@@ -5,11 +5,11 @@ import strawberry
import strawberry_django
from django.contrib.contenttypes.models import ContentType as DjangoContentType
from strawberry.scalars import ID
from strawberry_django import BaseFilterLookup, DatetimeFilterLookup, FilterLookup
from strawberry_django import DatetimeFilterLookup, FilterLookup
from core import models
from netbox.graphql.filters import BaseModelFilter, PrimaryModelFilter
from .enums import *
from core.graphql.filter_mixins import BaseFilterMixin
from netbox.graphql.filter_mixins import PrimaryModelFilterMixin
if TYPE_CHECKING:
from netbox.graphql.filter_lookups import IntegerLookup, JSONFilter
@@ -24,7 +24,8 @@ __all__ = (
@strawberry_django.filter_type(models.DataFile, lookups=True)
class DataFileFilter(BaseModelFilter):
class DataFileFilter(BaseFilterMixin):
id: ID | None = strawberry_django.filter_field()
created: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()
last_updated: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()
source: Annotated['DataSourceFilter', strawberry.lazy('core.graphql.filters')] | None = (
@@ -39,13 +40,11 @@ class DataFileFilter(BaseModelFilter):
@strawberry_django.filter_type(models.DataSource, lookups=True)
class DataSourceFilter(PrimaryModelFilter):
class DataSourceFilter(PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field()
type: FilterLookup[str] | None = strawberry_django.filter_field()
source_url: FilterLookup[str] | None = strawberry_django.filter_field()
status: (
BaseFilterLookup[Annotated['DataSourceStatusEnum', strawberry.lazy('core.graphql.enums')]] | None
) = strawberry_django.filter_field()
status: FilterLookup[str] | None = strawberry_django.filter_field()
enabled: FilterLookup[bool] | None = strawberry_django.filter_field()
ignore_rules: FilterLookup[str] | None = strawberry_django.filter_field()
parameters: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
@@ -58,14 +57,13 @@ class DataSourceFilter(PrimaryModelFilter):
@strawberry_django.filter_type(models.ObjectChange, lookups=True)
class ObjectChangeFilter(BaseModelFilter):
class ObjectChangeFilter(BaseFilterMixin):
id: ID | None = strawberry_django.filter_field()
time: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()
user: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field()
user_name: FilterLookup[str] | None = strawberry_django.filter_field()
request_id: FilterLookup[str] | None = strawberry_django.filter_field()
action: (
BaseFilterLookup[Annotated['ObjectChangeActionEnum', strawberry.lazy('core.graphql.enums')]] | None
) = strawberry_django.filter_field()
action: FilterLookup[str] | None = strawberry_django.filter_field()
changed_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@@ -85,6 +83,7 @@ class ObjectChangeFilter(BaseModelFilter):
@strawberry_django.filter_type(DjangoContentType, lookups=True)
class ContentTypeFilter(BaseModelFilter):
class ContentTypeFilter(BaseFilterMixin):
id: ID | None = strawberry_django.filter_field()
app_label: FilterLookup[str] | None = strawberry_django.filter_field()
model: FilterLookup[str] | None = strawberry_django.filter_field()

View File

@@ -3,12 +3,12 @@ from typing import Annotated, List, TYPE_CHECKING
import strawberry
import strawberry_django
from django.contrib.contenttypes.models import ContentType
from strawberry.types import Info
from core.models import ObjectChange
if TYPE_CHECKING:
from core.graphql.types import DataFileType, DataSourceType, ObjectChangeType
from core.graphql.types import DataFileType, DataSourceType
from netbox.core.graphql.types import ObjectChangeType
__all__ = (
'ChangelogMixin',
@@ -20,7 +20,7 @@ __all__ = (
class ChangelogMixin:
@strawberry_django.field
def changelog(self, info: Info) -> List[Annotated['ObjectChangeType', strawberry.lazy('.types')]]: # noqa: F821
def changelog(self, info) -> List[Annotated["ObjectChangeType", strawberry.lazy('.types')]]: # noqa: F821
content_type = ContentType.objects.get_for_model(self)
object_changes = ObjectChange.objects.filter(
changed_object_type=content_type,
@@ -31,5 +31,5 @@ class ChangelogMixin:
@strawberry.type
class SyncedDataMixin:
data_source: Annotated['DataSourceType', strawberry.lazy('core.graphql.types')] | None
data_file: Annotated['DataFileType', strawberry.lazy('core.graphql.types')] | None
data_source: Annotated["DataSourceType", strawberry.lazy('core.graphql.types')] | None
data_file: Annotated["DataFileType", strawberry.lazy('core.graphql.types')] | None

View File

@@ -5,7 +5,7 @@ import strawberry_django
from django.contrib.contenttypes.models import ContentType as DjangoContentType
from core import models
from netbox.graphql.types import BaseObjectType, PrimaryObjectType
from netbox.graphql.types import BaseObjectType, NetBoxObjectType
from .filters import *
__all__ = (
@@ -32,7 +32,8 @@ class DataFileType(BaseObjectType):
filters=DataSourceFilter,
pagination=True
)
class DataSourceType(PrimaryObjectType):
class DataSourceType(NetBoxObjectType):
datafiles: List[Annotated["DataFileType", strawberry.lazy('core.graphql.types')]]

View File

@@ -1,48 +0,0 @@
# Generated by Django 5.2.5 on 2025-09-09 16:48
from django.db import migrations, models
def get_active(apps, schema_editor):
from django.core.cache import cache
ConfigRevision = apps.get_model('core', 'ConfigRevision')
version = None
revision = None
# Try and get the latest version from cache
try:
version = cache.get('config_version')
except Exception:
pass
# If there is a version in cache, attempt to set revision to the current version from cache
# If the version in cache does not exist or there is no version, try the lastest revision in the database
if not version or (version and not (revision := ConfigRevision.objects.filter(pk=version).first())):
revision = ConfigRevision.objects.order_by('-created').first()
# If there is a revision set, set the active revision
if revision:
revision.active = True
revision.save()
class Migration(migrations.Migration):
dependencies = [
('core', '0018_concrete_objecttype'),
]
operations = [
migrations.AddField(
model_name='configrevision',
name='active',
field=models.BooleanField(default=False),
),
migrations.RunPython(code=get_active, reverse_code=migrations.RunPython.noop),
migrations.AddConstraint(
model_name='configrevision',
constraint=models.UniqueConstraint(
condition=models.Q(('active', True)), fields=('active',), name='unique_active_config_revision'
),
),
]

View File

@@ -1,19 +0,0 @@
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0019_configrevision_active'),
('users', '0015_owner'),
]
operations = [
migrations.AddField(
model_name='datasource',
name='owner',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner'
),
),
]

View File

@@ -14,9 +14,6 @@ class ConfigRevision(models.Model):
"""
An atomic revision of NetBox's configuration.
"""
active = models.BooleanField(
default=False
)
created = models.DateTimeField(
verbose_name=_('created'),
auto_now_add=True
@@ -38,13 +35,6 @@ class ConfigRevision(models.Model):
ordering = ['-created']
verbose_name = _('config revision')
verbose_name_plural = _('config revisions')
constraints = [
models.UniqueConstraint(
fields=('active',),
condition=models.Q(active=True),
name='unique_active_config_revision',
)
]
def __str__(self):
if not self.pk:
@@ -69,13 +59,8 @@ class ConfigRevision(models.Model):
"""
cache.set('config', self.data, None)
cache.set('config_version', self.pk, None)
# Set all instances of ConfigRevision to false and set this instance to true
ConfigRevision.objects.all().update(active=False)
ConfigRevision.objects.filter(pk=self.pk).update(active=True)
activate.alters_data = True
@property
def is_active(self):
return self.active
return cache.get('config_version') == self.pk

View File

@@ -0,0 +1,3 @@
# TODO: Remove this module in NetBox v4.5
# Provided for backward compatibility
from .object_types import *

View File

@@ -6,6 +6,7 @@ from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.core.files.storage import storages
from django.urls import reverse
from django.utils.translation import gettext as _
from ..choices import ManagedFileRootPathChoices
@@ -63,6 +64,9 @@ class ManagedFile(SyncedDataMixin, models.Model):
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('core:managedfile', args=[self.pk])
@property
def name(self):
return self.file_path

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