mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-28 08:07:45 -06:00
Compare commits
24 Commits
feature
...
feature-ip
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ada585129 | ||
|
|
b03158f1de | ||
|
|
bdde4b7e94 | ||
|
|
905656f13e | ||
|
|
42c2dc57f8 | ||
|
|
56673f4d88 | ||
|
|
955c64b68c | ||
|
|
912e6e4fb1 | ||
|
|
90d277610c | ||
|
|
b1bc933e98 | ||
|
|
b54196f595 | ||
|
|
0d31449df8 | ||
|
|
76e85683ac | ||
|
|
f844ec5703 | ||
|
|
7eb3a8d379 | ||
|
|
ade4354ca4 | ||
|
|
697d5bd876 | ||
|
|
b19f81cede | ||
|
|
c5e7b21147 | ||
|
|
c211b624d0 | ||
|
|
4c8301b3a5 | ||
|
|
68d0b58293 | ||
|
|
738ef63527 | ||
|
|
747fef0bc2 |
@@ -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.0
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
20
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
20
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
@@ -2,32 +2,32 @@
|
||||
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:
|
||||
value: >
|
||||
**NOTE:** This form is only for reporting _reproducible bugs_ in a current NetBox
|
||||
release. If you're having trouble with installation or just looking for assistance
|
||||
using NetBox, please visit our
|
||||
installation. If you're having trouble with installation or just looking for
|
||||
assistance with using NetBox, please visit our
|
||||
[discussion forum](https://github.com/netbox-community/netbox/discussions) instead.
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: NetBox Edition
|
||||
label: Deployment Type
|
||||
description: >
|
||||
Users of [NetBox Cloud](https://netboxlabs.com/netbox-cloud/) or
|
||||
[NetBox Enterprise](https://netboxlabs.com/netbox-enterprise/), please contact the
|
||||
[NetBox Labs](https://netboxlabs.com/) support team for assistance to ensure your
|
||||
request receives immediate attention.
|
||||
How are you running NetBox? (For issues with the Docker image, please go to the
|
||||
[netbox-docker](https://github.com/netbox-community/netbox-docker) repo.)
|
||||
options:
|
||||
- NetBox Community
|
||||
- NetBox Cloud
|
||||
- NetBox Enterprise
|
||||
- Self-hosted
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: NetBox Version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.4.8
|
||||
placeholder: v4.4.0
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
@@ -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:
|
||||
@@ -25,12 +25,9 @@ body:
|
||||
- Getting started
|
||||
- Configuration
|
||||
- Customization
|
||||
- Best practices
|
||||
- Integrations/API
|
||||
- Plugins
|
||||
- Administration
|
||||
- Data model
|
||||
- Reference
|
||||
- Development
|
||||
- Other
|
||||
validations:
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/04-translation.yaml
vendored
2
.github/ISSUE_TEMPLATE/04-translation.yaml
vendored
@@ -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:
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/05-housekeeping.yaml
vendored
2
.github/ISSUE_TEMPLATE/05-housekeeping.yaml
vendored
@@ -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:
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/06-deprecation.yaml
vendored
2
.github/ISSUE_TEMPLATE/06-deprecation.yaml
vendored
@@ -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:
|
||||
|
||||
3
.github/ISSUE_TEMPLATE/config.yml
vendored
3
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -13,6 +13,9 @@ contact_links:
|
||||
- name: 🌎 Correct a Translation
|
||||
url: https://explore.transifex.com/netbox-community/netbox/
|
||||
about: "Spot an incorrect translation? You can propose a fix on Transifex."
|
||||
- name: 💡 Plugin Idea
|
||||
url: https://plugin-ideas.netbox.dev
|
||||
about: "Have an idea for a plugin? Head over to the ideas board!"
|
||||
- name: 💬 Community Slack
|
||||
url: https://netdev.chat
|
||||
about: "Join #netbox on the NetDev Community Slack for assistance with installation issues and other problems."
|
||||
|
||||
8
.github/codeql/codeql-config.yml
vendored
8
.github/codeql/codeql-config.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
NETBOX_CONFIGURATION: netbox.configuration_testing
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.12', '3.13', '3.14']
|
||||
python-version: ['3.12', '3.13']
|
||||
node-version: ['20.x']
|
||||
services:
|
||||
redis:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -91,6 +91,7 @@ NetBox automatically logs the creation, modification, and deletion of all manage
|
||||
* Join the conversation on [the discussion forum](https://github.com/netbox-community/netbox/discussions) and [Slack](https://netdev.chat/)!
|
||||
* Already a power user? You can [suggest a feature](https://github.com/netbox-community/netbox/issues/new?assignees=&labels=type%3A+feature&template=feature_request.yaml) or [report a bug](https://github.com/netbox-community/netbox/issues/new?assignees=&labels=type%3A+bug&template=bug_report.yaml) on GitHub.
|
||||
* Contributions from the community are encouraged and appreciated! Check out our [contributing guide](CONTRIBUTING.md) to get started.
|
||||
* [Share your idea](https://plugin-ideas.netbox.dev/) for a new plugin, or [learn how to build one](https://github.com/netbox-community/netbox-plugin-tutorial) yourself!
|
||||
|
||||
## Screenshots
|
||||
|
||||
|
||||
@@ -34,4 +34,4 @@ For any security concerns regarding the community-maintained Docker image for Ne
|
||||
|
||||
### Bug Bounties
|
||||
|
||||
As NetBox is provided as free open source software, we do not offer any monetary compensation for vulnerability or bug reports; however, your contributions are greatly appreciated.
|
||||
As NetBox is provided as free open source software, we do not offer any monetary compensation for vulnerability or bug reports, however your contributions are greatly appreciated.
|
||||
|
||||
@@ -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
|
||||
@@ -28,13 +30,17 @@ django-htmx
|
||||
|
||||
# Modified Preorder Tree Traversal (recursive nesting of objects)
|
||||
# https://github.com/django-mptt/django-mptt/blob/main/CHANGELOG.rst
|
||||
# v0.18.0 introduces errant migrations which need to be resolved
|
||||
django-mptt==0.17.0
|
||||
django-mptt
|
||||
|
||||
# Context managers for PostgreSQL advisory locks
|
||||
# https://github.com/Xof/django-pglocks/blob/master/CHANGES.txt
|
||||
django-pglocks
|
||||
|
||||
|
||||
# Manager for managing PostgreSQL triggers
|
||||
# https://github.com/AmbitionEng/django-pgtrigger/blob/main/CHANGELOG.md
|
||||
django-pgtrigger
|
||||
|
||||
# Prometheus metrics library for Django
|
||||
# https://github.com/korfuri/django-prometheus/blob/master/CHANGELOG.md
|
||||
django-prometheus
|
||||
@@ -69,8 +75,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
|
||||
@@ -139,7 +144,8 @@ requests
|
||||
|
||||
# rq
|
||||
# https://github.com/rq/rq/blob/master/CHANGES.md
|
||||
rq
|
||||
# RQ v2.5 drops support for Redis < 5.0
|
||||
rq==2.4.1
|
||||
|
||||
# Django app for social-auth-core
|
||||
# https://github.com/python-social-auth/social-app-django/blob/master/CHANGELOG.md
|
||||
@@ -166,8 +172,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+)
|
||||
|
||||
@@ -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",
|
||||
@@ -332,120 +330,46 @@
|
||||
"100base-lfx",
|
||||
"100base-tx",
|
||||
"100base-t1",
|
||||
"1000base-bx10-d",
|
||||
"1000base-bx10-u",
|
||||
"1000base-cwdm",
|
||||
"1000base-cx",
|
||||
"1000base-dwdm",
|
||||
"1000base-ex",
|
||||
"1000base-lsx",
|
||||
"1000base-lx",
|
||||
"1000base-lx10",
|
||||
"1000base-sx",
|
||||
"1000base-t",
|
||||
"1000base-sx",
|
||||
"1000base-lx",
|
||||
"1000base-tx",
|
||||
"1000base-zx",
|
||||
"2.5gbase-t",
|
||||
"5gbase-t",
|
||||
"10gbase-br-d",
|
||||
"10gbase-br-u",
|
||||
"10gbase-cx4",
|
||||
"10gbase-er",
|
||||
"10gbase-lr",
|
||||
"10gbase-lrm",
|
||||
"10gbase-lx4",
|
||||
"10gbase-sr",
|
||||
"10gbase-t",
|
||||
"10gbase-zr",
|
||||
"25gbase-cr",
|
||||
"25gbase-er",
|
||||
"25gbase-lr",
|
||||
"25gbase-sr",
|
||||
"25gbase-t",
|
||||
"40gbase-cr4",
|
||||
"40gbase-er4",
|
||||
"40gbase-fr4",
|
||||
"40gbase-lr4",
|
||||
"40gbase-sr4",
|
||||
"50gbase-cr",
|
||||
"50gbase-er",
|
||||
"50gbase-fr",
|
||||
"50gbase-lr",
|
||||
"50gbase-sr",
|
||||
"100gbase-cr1",
|
||||
"100gbase-cr2",
|
||||
"100gbase-cr4",
|
||||
"100gbase-cr10",
|
||||
"100gbase-cwdm4",
|
||||
"100gbase-dr",
|
||||
"100gbase-er4",
|
||||
"100gbase-fr1",
|
||||
"100gbase-lr1",
|
||||
"100gbase-lr4",
|
||||
"100gbase-sr1",
|
||||
"100gbase-sr1.2",
|
||||
"100gbase-sr2",
|
||||
"100gbase-sr4",
|
||||
"100gbase-sr10",
|
||||
"100gbase-zr",
|
||||
"200gbase-cr2",
|
||||
"200gbase-cr4",
|
||||
"200gbase-dr4",
|
||||
"200gbase-er4",
|
||||
"200gbase-fr4",
|
||||
"200gbase-lr4",
|
||||
"200gbase-sr2",
|
||||
"200gbase-sr4",
|
||||
"200gbase-vr2",
|
||||
"400gbase-cr4",
|
||||
"400gbase-dr4",
|
||||
"400gbase-er8",
|
||||
"400gbase-fr4",
|
||||
"400gbase-fr8",
|
||||
"400gbase-lr4",
|
||||
"400gbase-lr8",
|
||||
"400gbase-sr4",
|
||||
"400gbase-sr4_2",
|
||||
"400gbase-sr8",
|
||||
"400gbase-sr16",
|
||||
"400gbase-vr4",
|
||||
"400gbase-zr",
|
||||
"800gbase-cr8",
|
||||
"800gbase-dr8",
|
||||
"800gbase-sr8",
|
||||
"800gbase-vr8",
|
||||
"10gbase-cx4",
|
||||
"100base-x-sfp",
|
||||
"1000base-x-gbic",
|
||||
"1000base-x-sfp",
|
||||
"10gbase-x-sfpp",
|
||||
"10gbase-x-xenpak",
|
||||
"10gbase-x-xfp",
|
||||
"10gbase-x-xenpak",
|
||||
"10gbase-x-x2",
|
||||
"25gbase-x-sfp28",
|
||||
"50gbase-x-sfp56",
|
||||
"40gbase-x-qsfpp",
|
||||
"50gbase-x-sfp28",
|
||||
"50gbase-x-sfp56",
|
||||
"100gbase-x-cfp",
|
||||
"100gbase-x-cfp2",
|
||||
"200gbase-x-cfp2",
|
||||
"400gbase-x-cfp2",
|
||||
"100gbase-x-cfp4",
|
||||
"100gbase-x-cxp",
|
||||
"100gbase-x-cpak",
|
||||
"100gbase-x-dsfp",
|
||||
"100gbase-x-sfpdd",
|
||||
"100gbase-x-qsfp28",
|
||||
"100gbase-x-qsfpdd",
|
||||
"100gbase-x-sfpdd",
|
||||
"200gbase-x-cfp2",
|
||||
"200gbase-x-qsfp56",
|
||||
"200gbase-x-qsfpdd",
|
||||
"400gbase-x-qsfp112",
|
||||
"400gbase-x-qsfpdd",
|
||||
"400gbase-x-cdfp",
|
||||
"400gbase-x-cfp2",
|
||||
"400gbase-x-cfp8",
|
||||
"400gbase-x-osfp",
|
||||
"400gbase-x-osfp-rhs",
|
||||
"800gbase-x-osfp",
|
||||
"400gbase-x-cdfp",
|
||||
"400gbase-x-cfp8",
|
||||
"800gbase-x-qsfpdd",
|
||||
"800gbase-x-osfp",
|
||||
"1000base-kx",
|
||||
"2.5gbase-kx",
|
||||
"5gbase-kr",
|
||||
|
||||
279484
contrib/openapi.json
279484
contrib/openapi.json
File diff suppressed because one or more lines are too long
@@ -25,7 +25,7 @@ Once finished, make note of the application (client) ID; this will be used when
|
||||

|
||||
|
||||
!!! tip "Multitenant authentication"
|
||||
NetBox also supports multitenant authentication via Azure AD; however, it requires a different backend and an additional configuration parameter. Please see the [`python-social-auth` documentation](https://python-social-auth.readthedocs.io/en/latest/backends/azuread.html#tenant-support) for details concerning multitenant authentication.
|
||||
NetBox also supports multitenant authentication via Azure AD, however it requires a different backend and an additional configuration parameter. Please see the [`python-social-auth` documentation](https://python-social-auth.readthedocs.io/en/latest/backends/azuread.html#tenant-support) for details concerning multitenant authentication.
|
||||
|
||||
### 3. Create a secret
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ This approach can span multiple levels of relations. For example, the following
|
||||
```
|
||||
|
||||
!!! note
|
||||
While the above query is functional, it's not very efficient. There are ways to optimize such requests; however, they are out of scope for this document. For more information, see the [Django queryset method reference](https://docs.djangoproject.com/en/stable/ref/models/querysets/) documentation.
|
||||
While the above query is functional, it's not very efficient. There are ways to optimize such requests, however they are out of scope for this document. For more information, see the [Django queryset method reference](https://docs.djangoproject.com/en/stable/ref/models/querysets/) documentation.
|
||||
|
||||
Reverse relationships can be traversed as well. For example, the following will find all devices with an interface named "em0":
|
||||
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
# Modeling Pluggable Transceivers
|
||||
|
||||
## Use Case
|
||||
|
||||
Many network devices utilize field-swappable [small-form factor pluggable transceivers (SFPs)](https://en.wikipedia.org/wiki/Small_Form-factor_Pluggable) to enable changing the physical media type of a fixed interface. For example, a 10 Gigabit Ethernet interface might be connected using copper, multimode fiber, or single-mode fiber, each of which requires a different type of SFP+ transceiver.
|
||||
|
||||
It can be challenging to model SFPs given their dynamic nature. This guide intends to capture the recommended strategy for modeling SFPs on NetBox v4.4 and later.
|
||||
|
||||
## Modeling Strategy
|
||||
|
||||
Pluggable transceivers are most accurately represented in NetBox as discrete [modules](../models/dcim/module.md) which are installed within [module bays](../models/dcim/modulebay.md). A module can deliver one or more [interfaces](../models/dcim/interface.md) (or other components) to the device in which it is installed. This approach ensures that a new interface is automatically created on the device when the module is installed, and deleted when the module is removed.
|
||||
|
||||
```mermaid
|
||||
flowchart BT
|
||||
interface1[Interface 1/1]--> module1[SFP]
|
||||
interface2[Interface 2/1]--> module2[SFP]
|
||||
interface3[Interface 3/1] & interface4[Interface 3/2]--> module3[SFP]
|
||||
module1 --> modulebay1[Module Bay 1]
|
||||
module2 --> modulebay2[Module Bay 2]
|
||||
module3 --> modulebay3[Module Bay 3]
|
||||
modulebay1 & modulebay2 & modulebay3 --> device[Device]
|
||||
```
|
||||
|
||||
### 1. Create an SFP Module Type Profile
|
||||
|
||||
If one has not already been defined, create a [module type profile](../models/dcim/moduletypeprofile.md) for SFPs. This profile will be assigned for all module types which represent a pluggable transceiver. Typically, you will need only one profile for all pluggable transceivers.
|
||||
|
||||
You might opt to define custom attributes for the profile by defining a custom [JSON schema](https://json-schema.org/). Profile attributes might be used to define characteristics unique to transceivers, such as optical wavelength and power ranges. Adding profile attributes is optional, and can be done at a later point.
|
||||
|
||||
!!! note
|
||||
Creating a module type profile is optional, but recommended as it allows for defining custom module attributes.
|
||||
|
||||
### 2. Create a Module Type for Each SFP Model in Inventory
|
||||
|
||||
Next, create a [module type](../models/dcim/moduletype.md) to represent each unique SFP model present in your network. Each module type should define a manufacturer and a unique model name, and may also include a part number. For example, you might create a module type for each of the following transceivers:
|
||||
|
||||
| Manufacturer | Model | Media Type |
|
||||
|--------------|------------------|------------|
|
||||
| Cisco | SFP-10G-SR | 10GE MMF |
|
||||
| Cisco | SFP-10G-LR | 10GE SMF |
|
||||
| Juniper | QFX-QSFP-40G-SR4 | 40GE MMF |
|
||||
| Juniper | JNP-QSFP-DAC-5M | 40GE DAC |
|
||||
|
||||
### 3. Add an Interface to the Module Type
|
||||
|
||||
After creating each module type, create an interface template on it to represent its physical interface. The definition of this interface template will depend on the transceiver's physical media type. (Reference the table above for examples.) When a new module is "installed" within a module bay on a device, its templated interface(s) will be automatically instantiated on that device as child interfaces of the module.
|
||||
|
||||
Determining which name to use for the transceiver's interface can be tricky, as the interface name might depend on the type of device in which the SFP is installed. To avoid having to rename interfaces, consider using the `{module}` token in place of a static interface name. The interface's name will inherit the position of the bay in which its parent module is installed. If creating multiple interfaces on a module, be sure to append a unique ID (e.g. `{module}:1`) to ensure each interface gets assigned a unique name.
|
||||
|
||||
### 4. Create Device Types
|
||||
|
||||
If you haven't already, create a [device type](../models/dcim/devicetype.md) to represent each unique device model in your network.
|
||||
|
||||
!!! note
|
||||
Skip this step if you've already created the necessary device types.
|
||||
|
||||
### 5. Add Module Bays to the Device Type
|
||||
|
||||
Once you've created a device type, add the appropriate number of module bays on each device type to represent its SFP slots. For example, a Juniper QFX5110 would have module bays numbered `0/0/0` through `0/0/55`: 48 SFP+ bays and 8 QSFP28 bays (56 total).
|
||||
|
||||
Be sure to define both the name **and position** of each module bay with a unique value. The module bay's position will be used to automatically name SFP interfaces.
|
||||
|
||||
### 6. Create a Device
|
||||
|
||||
Create a new device using the device type added in the previous step. The module bays (and any other components) defined on the device type will be instantiated on the new device automatically.
|
||||
|
||||
!!! note
|
||||
If you've already created the necessary devices in NetBox, you'll need to add their module bays manually. You can add multiple module bays at once by selecting the desired devices from the device list and selecting **Add Components > Module Bays** at the bottom of the page.
|
||||
|
||||
### 7. Add the SFP Modules
|
||||
|
||||
Finally, create each SFP in the new device by "installing" a new module of the appropriate type in each module bay. The interface(s) defined on the selected module type will be automatically populated on the new module. If present, the `{module}` token in the name of each interface template will be replaced with the position of the bay in which the module is being installed. For example, an interface template with the name `et-{module}` being created on a module installed in a bay with position `0/0/14` will create an interface named `et-0/0/14`.
|
||||
|
||||
When adding many modules at once, you may find it helpful to utilize NetBox's bulk import functionality. This allows you to create many modules at once from CSV, JSON, or YAML data.
|
||||
@@ -1,187 +0,0 @@
|
||||
# Performance Handbook
|
||||
|
||||
The purpose of this handbook is to help users and administrators use NetBox efficiently. It contains assorted recommendations and best practices compiled over time, intending to serve a wide variety of use cases.
|
||||
|
||||
## Server Configuration
|
||||
|
||||
### WSGI Server Configuration
|
||||
|
||||
NetBox operates as a [Web Server Gateway Interface (WSGI)](https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface) application, which sits behind a frontend HTTP server such as nginx or Apache. The HTTP server handles low-level HTTP request processing and serving static assets, and forwards application-level requests to NetBox via WSGI.
|
||||
|
||||
A backend WSGI server (typically [Gunicorn](https://gunicorn.org/) or [uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/)) is responsible for running the NetBox application. This is accomplished by initializing a number of WSGI worker processes which accept WSGI requests relayed from the frontend HTTP server.
|
||||
|
||||
Tuning your WSGI server is crucial to realizing optimal performance from NetBox. Below are some recommended configuration parameters.
|
||||
|
||||
#### Provision Multiple Workers
|
||||
|
||||
General guidance is to set the number of worker processes to double the number of CPU cores available, plus one (`2 * CPUs + 1`).
|
||||
|
||||
#### Limit the Worker Lifetime
|
||||
|
||||
Set a maximum number of requests that a worker can service before being respawned. This helps protect against potential memory leaks.
|
||||
|
||||
#### Set a Request Timeout
|
||||
|
||||
Limit the time a worker may spend processing any request. This prevents a long-running request from tying up a worker beyond an acceptable threshold. We suggest a limit of 120 seconds as a reasonable safeguard.
|
||||
|
||||
#### Bind Using a Unix Socket
|
||||
|
||||
When running the HTTP frontend and WSGI server on the same machine, binding via a Unix socket (instead of a TCP socket) may yield slight performance gains.
|
||||
|
||||
### NetBox Configuration
|
||||
|
||||
NetBox ships with a reasonable default configuration for most environments, but administrators are encouraged to explore all the [available parameters](../configuration/index.md) to tune their installation. Some of the most notable parameters impacting performance are called out below.
|
||||
|
||||
#### Reduce the Maximum Page Size
|
||||
|
||||
NetBox paginates large result sets to reduce the overall response size. The [`MAX_PAGE_SIZE`](../configuration/miscellaneous.md#max_page_size) parameter specifies the maximum number of results per page that a client can request. This is set to 1,000 by default. Consider lowering this number if you find that API clients are frequently requesting very large result sets.
|
||||
|
||||
#### Limit GraphQL Aliases
|
||||
|
||||
By default, NetBox restricts a GraphQL query to 10 aliases. Consider reducing this number by setting [`GRAPHQL_MAX_ALIASES`](../configuration/graphql-api.md#graphql_max_aliases) to a lower value.
|
||||
|
||||
#### Designate Isolated Deployments
|
||||
|
||||
If your NetBox installation does not have Internet access, set [`ISOLATED_DEPLOYMENT`](../configuration/system.md#isolated_deployment) to True. This will prevent the application from attempting routine external requests.
|
||||
|
||||
#### Reduce Sentry Sampling
|
||||
|
||||
If [Sentry](https://sentry.io/) has been enabled for error reporting and analytics, consider lowering its sampling rate. This can be accomplished by modifying the values for `sample_rate` and `traces_sample_rate` under [`SENTRY_CONFIG`](../configuration/error-reporting.md#sentry_config).
|
||||
|
||||
#### Remove Unneeded Event Handlers
|
||||
|
||||
Check whether any custom event handlers have been added under [`EVENTS_PIPELINE`](../configuration/miscellaneous.md#events_pipeline). Remove any that are no longer needed.
|
||||
|
||||
### Background Task Workers
|
||||
|
||||
NetBox defers the execution of certain tasks to background workers via Redis queues serviced by one or more background workers. These workers operate asynchronously from the frontend WSGI workers, and process tasks in the order they are enqueued.
|
||||
|
||||
NetBox creates three default queues for background tasks: `high`, `default`, and `low`. Additional queues can be configured via the [`QUEUE_MAPPINGS`](../configuration/miscellaneous.md#queue_mappings) configuration parameter.
|
||||
|
||||
By default, a background worker (spawned via `manage.py rqworker`) will listen to all available queues. To improve responsiveness to high-priority background tasks, consider dedicating one or more workers to service the `high` queue only:
|
||||
|
||||
```
|
||||
$ ./manage.py rqworker high
|
||||
19:31:20 Worker 861be45b32214afc95c235beeb19c9fa: started with PID 2300029, version 2.6.0
|
||||
19:31:20 Worker 861be45b32214afc95c235beeb19c9fa: subscribing to channel rq:pubsub:861be45b32214afc95c235beeb19c9fa
|
||||
19:31:20 *** Listening on high...
|
||||
19:31:20 Worker 861be45b32214afc95c235beeb19c9fa: cleaning registries for queue: high
|
||||
19:31:20 Scheduler for high started with PID 2300096
|
||||
```
|
||||
|
||||
## API Clients
|
||||
|
||||
### REST API
|
||||
|
||||
NetBox's [REST API](../integrations/rest-api.md) is the primary means of integration with external systems, allowing full create, read, update, and delete (CRUD) operations. There are a few performance considerations to keep in mind when dealing with very large data sets.
|
||||
|
||||
#### Use "Brief" Mode for Simple Lists
|
||||
|
||||
In cases where you need to retrieve only a minimal representation of objects, append `?brief=True` to the URL. This instructs NetBox to omit all fields except the following:
|
||||
|
||||
* ID
|
||||
* URL
|
||||
* Display text
|
||||
* Name (or similar identifier)
|
||||
* Slug (if present)
|
||||
* Description
|
||||
* Counts of notable related objects (where applicable)
|
||||
|
||||
For example, a site fetched using brief mode returns only the following:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 2,
|
||||
"url": "https://netbox/api/dcim/sites/2/",
|
||||
"display": "DM-Akron",
|
||||
"name": "DM-Akron",
|
||||
"slug": "dm-akron",
|
||||
"description": ""
|
||||
}
|
||||
```
|
||||
|
||||
Omitting all other fields (especially those which fetch and return related objects) often results in much faster queries.
|
||||
|
||||
#### Declare Selected Fields
|
||||
|
||||
If you need more flexibility regarding the fields to be returned for an object type, you can specify a list of fields to include using the `fields` query parameter. For example, a request for `/api/dcim/sites/?fields=id,name,status,region` will return the following:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 2,
|
||||
"name": "DM-Akron",
|
||||
"status": {
|
||||
"value": "active",
|
||||
"label": "Active"
|
||||
},
|
||||
"region": {
|
||||
"id": 51,
|
||||
"url": "https://netbox/api/dcim/regions/51/",
|
||||
"display": "Ohio",
|
||||
"name": "Ohio",
|
||||
"slug": "us-oh",
|
||||
"description": "",
|
||||
"site_count": 0,
|
||||
"_depth": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Like brief mode, this approach can significantly reduce the response time of an API request by omitting unneeded data.
|
||||
|
||||
#### Employ Pagination
|
||||
|
||||
Like the user interface, the REST API employs pagination to limit the number of objects returned in a single response. If a page size is not specified by the request (i.e. by passing `?limit=10`), NetBox will use the default size defined by [`PAGINATE_COUNT`](../configuration/default-values.md#paginate_count). The default page size is 50.
|
||||
|
||||
For some requests, especially those using brief mode or a minimal selection of fields, it may be desirable to specify a higher page size, so that fewer requests are needed to retrieve all objects. Appending `?limit=0` to the request effectively seeks to disable pagination. (Note, however, that the requested page size cannot exceed the value of [`MAX_PAGE_SIZE`](../configuration/miscellaneous.md#max_page_size), which defaults to 1,000.)
|
||||
|
||||
Complex API requests, which pull in many related objects, generate a relatively high load on the application, and generally benefit from reduced page size. If you find that your API requests are taking an inordinate amount of time, try reducing the page size from the default value so that fewer objects need to be returned for each request.
|
||||
|
||||
### GraphQL API
|
||||
|
||||
NetBox's read-only [GraphQL API](../integrations/graphql-api.md) offers an alternative to its REST API, and provides a very flexible means of retrieving data. GraphQL enables the client to request any object from a single endpoint, specifying only the desired attributes and relations. Many users prefer this to the more rigid structure of the REST API, but it's important to understand the trade-offs of crafting complex queries.
|
||||
|
||||
#### Request Only the Necessary Fields
|
||||
|
||||
For optimal performance, craft your GraphQL queries to return only the fields needed by the client. This will reduce the overall query time, especially when omitting related objects.
|
||||
|
||||
#### Avoid Overly Complex Queries
|
||||
|
||||
The primary benefit of the GraphQL API is that it allows the client to offload to the server the work of stitching together various related objects, which would require the client to make multiple requests to different endpoints if using the REST API. However, this advantage does not come for free: The more information that is requested in a single query, the more work the server needs to do to fetch the raw data from the database and render it into a GraphQL response. Very complex queries can yield dozens or hundreds of SQL queries on the backend, which increase the time it takes to render a response.
|
||||
|
||||
While it can be tempting to pack as much data as possible into a single GraphQL query, realize that there is a balance to be struck between minimizing the number of queries needed and avoiding complexity in the interest of performance. For example, while it is possible to retrieve via a single GraphQL API request all the IP addresses and all attached cables for every device in a site, it is probably more efficient (often _much_ more efficient) to make two or three separate requests and correlate the data locally.
|
||||
|
||||
#### Use Filters
|
||||
|
||||
You can specify filters when making a GraphQL query to limit the set of objects returned. This works a bit differently from the REST API, as filters are declared inside the query statement rather than appended to the URL, but the concept is the same. For example, to return only active sites:
|
||||
|
||||
```graphql
|
||||
query {
|
||||
site_list(
|
||||
filters: {
|
||||
status: STATUS_ACTIVE
|
||||
}
|
||||
) {
|
||||
name
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This returns only sites with a status of "active" and avoid needing to parse through all the others. For further information about filters, see the [GraphQL API documentation](../integrations/graphql-api.md).
|
||||
|
||||
#### Employ Pagination
|
||||
|
||||
Like the REST API, the GraphQL API supports pagination. Queries which return a large number of objects should employ pagination to limit the size of each response.
|
||||
|
||||
```graphql
|
||||
{
|
||||
device_list(
|
||||
pagination: {limit: 100}
|
||||
) {
|
||||
id
|
||||
name
|
||||
serial
|
||||
status
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -17,7 +17,7 @@ CUSTOM_VALIDATORS = {
|
||||
},
|
||||
"my_plugin.validators.Validator1"
|
||||
],
|
||||
"dcim.device": [
|
||||
"dim.device": [
|
||||
"my_plugin.validators.Validator1"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
This parameter controls the content and layout of user's default dashboard. Once the dashboard has been created, the user is free to customize it as they please by adding, removing, and reconfiguring widgets.
|
||||
|
||||
This parameter must specify an iterable of dictionaries, each representing a discrete dashboard widget and its configuration. The following widget attributes are supported:
|
||||
This parameter must specify an iterable of dictionaries, each representing a discrete dashboard widget and its configuration. The follow widget attributes are supported:
|
||||
|
||||
* `widget`: Dotted path to the Python class (required)
|
||||
* `width`: Default widget width (between 1 and 12, inclusive)
|
||||
@@ -63,8 +63,6 @@ DEFAULT_USER_PREFERENCES = {
|
||||
|
||||
For a complete list of available preferences, log into NetBox and navigate to `/user/preferences/`. A period in a preference name indicates a level of nesting in the JSON data. The example above maps to `pagination.per_page`.
|
||||
|
||||
See also: [Clearing table preferences](../features/user-preferences.md#clearing-table-preferences) for resolving errors caused by saved table columns or ordering.
|
||||
|
||||
---
|
||||
|
||||
## PAGINATE_COUNT
|
||||
|
||||
@@ -1,32 +1,7 @@
|
||||
# Error Reporting Settings
|
||||
|
||||
## SENTRY_CONFIG
|
||||
|
||||
A dictionary mapping keyword arguments to values, to be passed to `sentry_sdk.init()`. See the [Sentry Python SDK documentation](https://docs.sentry.io/platforms/python/) for more information on supported parameters.
|
||||
|
||||
The default configuration is shown below:
|
||||
|
||||
```python
|
||||
{
|
||||
"sample_rate": 1.0,
|
||||
"send_default_pii": False,
|
||||
"traces_sample_rate": 0,
|
||||
}
|
||||
```
|
||||
|
||||
Additionally, `http_proxy` and `https_proxy` are set to the HTTP and HTTPS proxies, respectively, configured for NetBox (if any).
|
||||
|
||||
## SENTRY_DSN
|
||||
|
||||
!!! warning "This parameter will be removed in NetBox v4.5."
|
||||
Set this using `SENTRY_CONFIG` instead:
|
||||
|
||||
```
|
||||
SENTRY_CONFIG = {
|
||||
"dsn": "https://examplePublicKey@o0.ingest.sentry.io/0",
|
||||
}
|
||||
```
|
||||
|
||||
Default: `None`
|
||||
|
||||
Defines a Sentry data source name (DSN) for automated error reporting. `SENTRY_ENABLED` must be `True` for this parameter to take effect. For example:
|
||||
@@ -50,15 +25,6 @@ Set to `True` to enable automatic error reporting via [Sentry](https://sentry.io
|
||||
|
||||
## SENTRY_SAMPLE_RATE
|
||||
|
||||
!!! warning "This parameter will be removed in NetBox v4.5."
|
||||
Set this using `SENTRY_CONFIG` instead:
|
||||
|
||||
```
|
||||
SENTRY_CONFIG = {
|
||||
"sample_rate": 0.2,
|
||||
}
|
||||
```
|
||||
|
||||
Default: `1.0` (all)
|
||||
|
||||
The sampling rate for errors. Must be a value between 0 (disabled) and 1.0 (report on all errors).
|
||||
@@ -67,15 +33,6 @@ The sampling rate for errors. Must be a value between 0 (disabled) and 1.0 (repo
|
||||
|
||||
## SENTRY_SEND_DEFAULT_PII
|
||||
|
||||
!!! warning "This parameter will be removed in NetBox v4.5."
|
||||
Set this using `SENTRY_CONFIG` instead:
|
||||
|
||||
```
|
||||
SENTRY_CONFIG = {
|
||||
"send_default_pii": True,
|
||||
}
|
||||
```
|
||||
|
||||
Default: `False`
|
||||
|
||||
Maps to the Sentry SDK's [`send_default_pii`](https://docs.sentry.io/platforms/python/configuration/options/#send-default-pii) parameter. If enabled, certain personally identifiable information (PII) is added.
|
||||
@@ -103,15 +60,6 @@ SENTRY_TAGS = {
|
||||
|
||||
## SENTRY_TRACES_SAMPLE_RATE
|
||||
|
||||
!!! warning "This parameter will be removed in NetBox v4.5."
|
||||
Set this using `SENTRY_CONFIG` instead:
|
||||
|
||||
```
|
||||
SENTRY_CONFIG = {
|
||||
"traces_sample_rate": 0.2,
|
||||
}
|
||||
```
|
||||
|
||||
Default: `0` (disabled)
|
||||
|
||||
The sampling rate for transactions. Must be a value between 0 (disabled) and 1.0 (report on all transactions).
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -261,46 +257,6 @@ The specific configuration settings for each storage backend can be found in the
|
||||
!!! note
|
||||
Any keys defined in the `STORAGES` configuration parameter replace those in the default configuration. It is only necessary to define keys within the `STORAGES` for the specific backend(s) you wish to configure.
|
||||
|
||||
### Environment Variables and Third-Party Libraries
|
||||
|
||||
NetBox uses an explicit Python configuration approach rather than automatic environment variable detection. While this provides clear configuration management and version control capabilities, it affects how some third-party libraries like `django-storages` function within NetBox's context.
|
||||
|
||||
Many Django libraries (including `django-storages`) expect to automatically detect environment variables like `AWS_STORAGE_BUCKET_NAME` or `AWS_S3_ACCESS_KEY_ID`. However, NetBox's configuration processing prevents this automatic detection from working as documented in some of these libraries.
|
||||
|
||||
When using third-party libraries that rely on environment variable detection, you may need to explicitly read environment variables in your NetBox `configuration.py`:
|
||||
|
||||
```python
|
||||
import os
|
||||
|
||||
STORAGES = {
|
||||
'default': {
|
||||
'BACKEND': 'storages.backends.s3.S3Storage',
|
||||
'OPTIONS': {
|
||||
'bucket_name': os.environ.get('AWS_STORAGE_BUCKET_NAME'),
|
||||
'access_key': os.environ.get('AWS_S3_ACCESS_KEY_ID'),
|
||||
'secret_key': os.environ.get('AWS_S3_SECRET_ACCESS_KEY'),
|
||||
'endpoint_url': os.environ.get('AWS_S3_ENDPOINT_URL'),
|
||||
'location': 'media/',
|
||||
}
|
||||
},
|
||||
'staticfiles': {
|
||||
'BACKEND': 'storages.backends.s3.S3Storage',
|
||||
'OPTIONS': {
|
||||
'bucket_name': os.environ.get('AWS_STORAGE_BUCKET_NAME'),
|
||||
'access_key': os.environ.get('AWS_S3_ACCESS_KEY_ID'),
|
||||
'secret_key': os.environ.get('AWS_S3_SECRET_ACCESS_KEY'),
|
||||
'endpoint_url': os.environ.get('AWS_S3_ENDPOINT_URL'),
|
||||
'location': 'static/',
|
||||
}
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
This approach works because the environment variables are resolved during NetBox's configuration processing, before the third-party library attempts its own environment variable detection.
|
||||
|
||||
!!! warning "Configuration Behavior"
|
||||
Simply setting environment variables like `AWS_STORAGE_BUCKET_NAME` without explicitly reading them in your configuration will not work. The variables must be read using `os.environ.get()` within your `configuration.py` file.
|
||||
|
||||
---
|
||||
|
||||
## TIME_ZONE
|
||||
|
||||
@@ -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 percent‑encoded 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 object’s 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 (percent‑encoded) | `?name=Branch42` |
|
||||
| `TextVar` | string (percent‑encoded) | `?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']
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -123,6 +123,16 @@ $ node bundle.js
|
||||
Done in 1.00s.
|
||||
```
|
||||
|
||||
### Rebuild the Device Type Definition Schema
|
||||
|
||||
Run the following command to update the device type definition validation schema:
|
||||
|
||||
```nohighlight
|
||||
./manage.py buildschema --write
|
||||
```
|
||||
|
||||
This will automatically update the schema file at `contrib/generated_schema.json`.
|
||||
|
||||
### Update & Compile Translations
|
||||
|
||||
Updated language translations should be pulled from [Transifex](https://app.transifex.com/netbox-community/netbox/dashboard/) and re-compiled for each new release. First, retrieve any updated translation files using the Transifex CLI client:
|
||||
@@ -150,24 +160,6 @@ Then, compile these portable (`.po`) files for use in the application:
|
||||
!!! tip
|
||||
Put yourself in the shoes of the user when recording change notes. Focus on the effect that each change has for the end user, rather than the specific bits of code that were modified in a PR. Ensure that each message conveys meaning absent context of the initial feature request or bug report. Remember to include keywords or phrases (such as exception names) that can be easily searched.
|
||||
|
||||
### Rebuild the Device Type Definition Schema
|
||||
|
||||
Run the following command to update the device type definition validation schema:
|
||||
|
||||
```nohighlight
|
||||
./manage.py buildschema --write
|
||||
```
|
||||
|
||||
This will automatically update the schema file at `contrib/generated_schema.json`.
|
||||
|
||||
### Update the OpenAPI Schema
|
||||
|
||||
Update the static OpenAPI schema definition at `contrib/openapi.json` with the management command below. If the schema file is up-to-date, only the NetBox version will be changed.
|
||||
|
||||
```nohighlight
|
||||
./manage.py spectacular --format openapi-json > ../contrib/openapi.json
|
||||
```
|
||||
|
||||
### Submit a Pull Request
|
||||
|
||||
Commit the above changes and submit a pull request titled **"Release vX.Y.Z"** to merge the current release branch (e.g. `release-vX.Y.Z`) into `main`. Copy the documented release notes into the pull request's body.
|
||||
|
||||
@@ -2,18 +2,12 @@
|
||||
|
||||
The `users.UserConfig` model holds individual preferences for each user in the form of JSON data. This page serves as a manifest of all recognized user preferences in NetBox.
|
||||
|
||||
For end‑user guidance on resetting saved table layouts, see [Features > User Preferences](../features/user-preferences.md#clearing-table-preferences).
|
||||
|
||||
## 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 |
|
||||
|
||||
@@ -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"}}'
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
While NetBox strives to meet the needs of every network, the needs of users to cater to their own unique environments cannot be ignored. NetBox was built with this in mind, and can be customized in many ways to better suit your particular needs.
|
||||
|
||||
For end‑user personalization topics (bookmarks, table preferences, language, CSV delimiter, and more), see [Features > User Preferences](../features/user-preferences.md).
|
||||
|
||||
## Tags
|
||||
|
||||
Most objects in NetBox can be assigned user-created tags to aid with organization and filtering. Tag values are completely arbitrary: They may be used to store data in key-value pairs, or they may be employed simply as labels against which objects can be filtered. Each tag can also be assigned a color for quicker differentiation in the user interface.
|
||||
@@ -20,6 +18,10 @@ The `tag` filter can be specified multiple times to match only objects which hav
|
||||
GET /api/dcim/devices/?tag=monitored&tag=deprecated
|
||||
```
|
||||
|
||||
## Bookmarks
|
||||
|
||||
Users can bookmark their most commonly visited objects for convenient access. Bookmarks are listed under a user's profile, and can be displayed with custom filtering and ordering on the user's personal dashboard.
|
||||
|
||||
## Custom Fields
|
||||
|
||||
While NetBox provides a rather extensive data model out of the box, the need may arise to store certain additional data associated with NetBox objects. For example, you might need to record the invoice ID alongside an installed device, or record an approving authority when creating a new IP prefix. NetBox administrators can create custom fields on built-in objects to meet these needs.
|
||||
@@ -36,7 +38,7 @@ Custom links allow you to conveniently reference external resources related to N
|
||||
http://server.local/vms/?name={{ object.name }}
|
||||
```
|
||||
|
||||
Now, when viewing a virtual machine in NetBox, a user will see a handy button with the chosen title and link (complete with the name of the VM being viewed). Both the text and URL of custom links can be templatized in this manner, and custom links can be grouped together into dropdowns for a more efficient display.
|
||||
Now, when viewing a virtual machine in NetBox, a user will see a handy button with the chosen title and link (complete with the name of the VM being viewed). Both the text and URL of custom links can be templatized in this manner, and custom links can be grouped together into dropdowns for more efficient display.
|
||||
|
||||
To learn more about this feature, check out the [custom link documentation](../customization/custom-links.md).
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
# User Preferences
|
||||
|
||||
NetBox stores per‑user options that control aspects of the web interface and data display. Preferences persist across sessions and can be managed under **User → Preferences**.
|
||||
|
||||
## Table configurations
|
||||
|
||||
When a list view is configured using **Configure**, NetBox records the selected columns and ordering as per‑user table preferences for that table. These preferences are applied automatically on subsequent visits.
|
||||
|
||||
### Clearing table preferences
|
||||
|
||||
Saved table preferences may need to be reset, for example, if a table fails to render or after an upgrade that changes available columns.
|
||||
|
||||
To clear saved preferences for one or more tables:
|
||||
|
||||
1. Click the username in the top‑right corner.
|
||||
2. Select **Preferences** from the dropdown.
|
||||
3. Scroll to the **Table Configurations** section.
|
||||
4. Select the tables to reset.
|
||||
5. Click **Submit** to clear the selected preferences.
|
||||
|
||||
After clearing preferences, reopen the list view and use **Configure** to set the desired columns and ordering.
|
||||
|
||||
!!! note
|
||||
Per‑user table preferences are distinct from **Table Configs**, which are named, reusable configurations managed under *Customization → Table Configs*. Clearing preferences does not delete any Table Configs. See [Table Configs](../models/extras/tableconfig.md) for details.
|
||||
|
||||
## Other preferences
|
||||
|
||||
### Language
|
||||
Selects the user interface language from installed translations (subject to system configuration).
|
||||
|
||||
### Page length
|
||||
Sets the default number of rows displayed on paginated tables.
|
||||
|
||||
### Paginator placement
|
||||
Controls where pagination controls are rendered relative to a table.
|
||||
|
||||
### Striped table rows
|
||||
Toggles alternating row backgrounds on tables.
|
||||
|
||||
### Data format (raw views)
|
||||
Sets the default format (JSON or YAML) when rendering raw data blocks.
|
||||
|
||||
### CSV delimiter
|
||||
Overrides the delimiter used when exporting CSV data.
|
||||
|
||||
## Bookmarks
|
||||
|
||||
Users can bookmark frequently visited objects for convenient access. Bookmarks appear under the user menu and can be displayed on the personal dashboard using the bookmarks' widget. See [Bookmark](../models/extras/bookmark.md) for model details.
|
||||
|
||||
## Notifications and subscriptions
|
||||
|
||||
Users may subscribe to objects to receive notifications when changes occur. Notifications are listed under the user menu and can be marked as read or deleted. See [Features > Notifications](notifications.md) and the data‑model references for [Subscription](../models/extras/subscription.md) and [Notification](../models/extras/notification.md).
|
||||
|
||||
## Admin defaults
|
||||
|
||||
Administrators can define defaults for new users via [`DEFAULT_USER_PREFERENCES`](../configuration/default-values.md#default_user_preferences). Users may override these values under their own preferences.
|
||||
|
||||
## See also
|
||||
|
||||
- [Development > User Preferences](../development/user-preferences.md) (manifest of recognized preference keys)
|
||||
@@ -17,7 +17,7 @@ Dedicate some time to take stock of your own sources of truth for your infrastru
|
||||
|
||||
* **Multiple conflicting sources** for a given domain. For example, there may be multiple versions of a spreadsheet circulating, each of which asserts a conflicting set of data.
|
||||
* **Sources with no domain defined.** You may encounter that different teams within your organization use different tools for the same purpose, with no normal definition of when either should be used.
|
||||
* **Inaccessible data formatting.** Some tools are better suited for programmatic usage than others. For example, spreadsheets are generally very easy to parse and export; however, free-form notes on wiki or similar application are much more difficult to consume.
|
||||
* **Inaccessible data formatting.** Some tools are better suited for programmatic usage than others. For example, spreadsheets are generally very easy to parse and export, however free-form notes on wiki or similar application are much more difficult to consume.
|
||||
* **There is no source of truth.** Sometimes you'll find that a source of truth simply doesn't exist for a domain. For example, when assigning IP addresses, operators may be just using any (presumed) available IP from a subnet without ever recording its usage.
|
||||
|
||||
See if you can identify each domain of infrastructure data for your organization, and the source of truth for each. Once you have these compiled, you'll need to determine what belongs in NetBox.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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/
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -4,9 +4,6 @@ This object represents the saved configuration of an object table in NetBox. Tab
|
||||
|
||||
For example, you might wish to create a table config for the devices list to assist in inventory tasks. This view might show the device name, location, serial number, and asset tag, but omit operational details like IP addresses. Once applied, this table config can be saved for reuse in future audits.
|
||||
|
||||
!!! note
|
||||
Per‑user table preferences (columns and ordering remembered for an individual user) are distinct from Table Configs. If a list view fails to render due to outdated saved preferences, see [Clearing table preferences](../../features/user-preferences.md#clearing-table-preferences).
|
||||
|
||||
## Fields
|
||||
|
||||
### Name
|
||||
@@ -23,7 +20,7 @@ The type of NetBox object to which the table config pertains.
|
||||
|
||||
### Table
|
||||
|
||||
The name of the specific table to which the table config pertains. (Some NetBox objects use multiple tables.)
|
||||
The name of the specific table to which the table config pertains. (Some NetBox object use multiple tables.)
|
||||
|
||||
### Weight
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -66,7 +66,7 @@ The top level is the project root, which can have any name that you like. Immedi
|
||||
* `README.md` - A brief introduction to your plugin, how to install and configure it, where to find help, and any other pertinent information. It is recommended to write `README` files using a markup language such as Markdown to enable human-friendly display.
|
||||
* The plugin source directory. This must be a valid Python package name, typically comprising only lowercase letters, numbers, and underscores.
|
||||
|
||||
The plugin source directory contains all the actual Python code and other resources used by your plugin. Its structure is left to the author's discretion; however, it is recommended to follow best practices as outlined in the [Django documentation](https://docs.djangoproject.com/en/stable/intro/reusable-apps/). At a minimum, this directory **must** contain an `__init__.py` file containing an instance of NetBox's `PluginConfig` class, discussed below.
|
||||
The plugin source directory contains all the actual Python code and other resources used by your plugin. Its structure is left to the author's discretion, however it is recommended to follow best practices as outlined in the [Django documentation](https://docs.djangoproject.com/en/stable/intro/reusable-apps/). At a minimum, this directory **must** contain an `__init__.py` file containing an instance of NetBox's `PluginConfig` class, discussed below.
|
||||
|
||||
**Note:** The [Cookiecutter NetBox Plugin](https://github.com/netbox-community/cookiecutter-netbox-plugin) can be used to auto-generate all the needed directories and files for a new plugin.
|
||||
|
||||
@@ -186,7 +186,7 @@ Many of these are self-explanatory, but for more information, see the [pyproject
|
||||
|
||||
## Create a Virtual Environment
|
||||
|
||||
It is strongly recommended to create a Python [virtual environment](https://docs.python.org/3/tutorial/venv.html) for the development of your plugin, as opposed to using system-wide packages. This will afford you complete control over the installed versions of all dependencies and avoid conflict with system packages. This environment can live wherever you'd like;however, it should be excluded from revision control. (A popular convention is to keep all virtual environments in the user's home directory, e.g. `~/.virtualenvs/`.)
|
||||
It is strongly recommended to create a Python [virtual environment](https://docs.python.org/3/tutorial/venv.html) for the development of your plugin, as opposed to using system-wide packages. This will afford you complete control over the installed versions of all dependencies and avoid conflict with system packages. This environment can live wherever you'd like, however it should be excluded from revision control. (A popular convention is to keep all virtual environments in the user's home directory, e.g. `~/.virtualenvs/`.)
|
||||
|
||||
```shell
|
||||
python3 -m venv ~/.virtualenvs/my_plugin
|
||||
|
||||
@@ -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
|
||||
|
||||
```
|
||||
|
||||
@@ -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.)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
@@ -55,11 +47,6 @@ table.configure(request)
|
||||
|
||||
This will automatically apply any user-specific preferences for the table. (If using a generic view provided by NetBox, table configuration is handled automatically.)
|
||||
|
||||
|
||||
### Bulk Edit and Delete Actions
|
||||
|
||||
Bulk edit and delete buttons are automatically added to the table, if there is an appropriate view registered to the `${modelname}_bulk_edit` or `${modelname}_bulk_delete` path name.
|
||||
|
||||
## Columns
|
||||
|
||||
The table column classes listed below are supported for use in plugins. These classes can be imported from `netbox.tables.columns`.
|
||||
|
||||
@@ -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
|
||||
@@ -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))
|
||||
|
||||
@@ -357,7 +357,7 @@ And the response:
|
||||
...
|
||||
```
|
||||
|
||||
All GraphQL requests are made at the `/graphql` URL (which also serves the GraphiQL UI). The API is currently read-only; however, users who wish to disable it until needed can do so by setting the `GRAPHQL_ENABLED` configuration parameter to False. For more detail on NetBox's GraphQL implementation, see [the GraphQL API documentation](../integrations/graphql-api.md).
|
||||
All GraphQL requests are made at the `/graphql` URL (which also serves the GraphiQL UI). The API is currently read-only, however users who wish to disable it until needed can do so by setting the `GRAPHQL_ENABLED` configuration parameter to False. For more detail on NetBox's GraphQL implementation, see [the GraphQL API documentation](../integrations/graphql-api.md).
|
||||
|
||||
#### IP Ranges ([#834](https://github.com/netbox-community/netbox/issues/834))
|
||||
|
||||
|
||||
@@ -1,221 +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
|
||||
|
||||
* [#17010](https://github.com/netbox-community/netbox/issues/17010) - Show admin navigation menu items only for staff & superusers
|
||||
* [#19590](https://github.com/netbox-community/netbox/issues/19590) - Add columns for device site & location to device component tables
|
||||
* [#19765](https://github.com/netbox-community/netbox/issues/19765) - Linkify assigned object types under saved filter view
|
||||
* [#20308](https://github.com/netbox-community/netbox/issues/20308) - Add a hotkey (`/`) for the global search field
|
||||
* [#20332](https://github.com/netbox-community/netbox/issues/20332) - Add a "none" option to object tag filters
|
||||
* [#20380](https://github.com/netbox-community/netbox/issues/20380) - Introduce the `SENTRY_CONFIG` configuration parameter
|
||||
* [#20412](https://github.com/netbox-community/netbox/issues/20412) - Linkify cluster type on virtual machine detail view
|
||||
* [#20438](https://github.com/netbox-community/netbox/issues/20438) - Add `facility` field to bulk edit forms for sites and locations
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#18878](https://github.com/netbox-community/netbox/issues/18878) - Automatically assign a designated primary MAC address upon creation of a new interface
|
||||
* [#20243](https://github.com/netbox-community/netbox/issues/20243) - Prevent scheduled system jobs from re-running multiple times
|
||||
* [#20253](https://github.com/netbox-community/netbox/issues/20253) - Fix support for filtering object contact assignments in GraphQL API
|
||||
* [#20365](https://github.com/netbox-community/netbox/issues/20365) - Address various inaccuracies in generated OpenAPI schema
|
||||
* [#20375](https://github.com/netbox-community/netbox/issues/20375) - Preserve filter parameters when performing bulk operations
|
||||
* [#20390](https://github.com/netbox-community/netbox/issues/20390) - Fix styling of page size selection dropdown
|
||||
* [#20392](https://github.com/netbox-community/netbox/issues/20392) - Clean up ordering of interface type options
|
||||
* [#20398](https://github.com/netbox-community/netbox/issues/20398) - Fix misleading error reporting for min/max custom field values
|
||||
* [#20419](https://github.com/netbox-community/netbox/issues/20419) - Correct action buttons for child object views
|
||||
* [#20425](https://github.com/netbox-community/netbox/issues/20425) - Fix Markdown preview functionality within "quick add" modal
|
||||
* [#20441](https://github.com/netbox-community/netbox/issues/20441) - Fix display of the "groups" column in contact assignments table
|
||||
|
||||
---
|
||||
|
||||
## v4.4.1 (2025-09-16)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#15492](https://github.com/netbox-community/netbox/issues/15492) - Enable cloning of permissions
|
||||
* [#16381](https://github.com/netbox-community/netbox/issues/16381) - Display script result timestamps in system timezone
|
||||
* [#19262](https://github.com/netbox-community/netbox/issues/19262) - No longer restrict FHRP group assignment by assigned IP address
|
||||
* [#19408](https://github.com/netbox-community/netbox/issues/19408) - Support export templates for circuit terminations and virtual circuit terminations
|
||||
* [#19428](https://github.com/netbox-community/netbox/issues/19428) - Add an optional U height field to the devices table
|
||||
* [#19547](https://github.com/netbox-community/netbox/issues/19547) - Add individual "sync" buttons in data sources table
|
||||
* [#19865](https://github.com/netbox-community/netbox/issues/19865) - Reorganize cable type groupings
|
||||
* [#20222](https://github.com/netbox-community/netbox/issues/20222) - Enable the `HttpOnly` flag for CSRF cookie
|
||||
* [#20237](https://github.com/netbox-community/netbox/issues/20237) - Include VPN tunnel groups in global search results
|
||||
* [#20241](https://github.com/netbox-community/netbox/issues/20241) - Record A & B terminations in cable changelog data
|
||||
* [#20277](https://github.com/netbox-community/netbox/issues/20277) - Add support for attribute assignment to `deserialize_object()` utility
|
||||
* [#20321](https://github.com/netbox-community/netbox/issues/20321) - Add physical media types for transceiver interfaces
|
||||
* [#20347](https://github.com/netbox-community/netbox/issues/20347) - Add Wi-Fi Alliance aliases to 802.11 interface types
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#19729](https://github.com/netbox-community/netbox/issues/19729) - Restore `kind` filter for interfaces in GraphQL API
|
||||
* [#19744](https://github.com/netbox-community/netbox/issues/19744) - Plugins list should be orderable by "active" column
|
||||
* [#19851](https://github.com/netbox-community/netbox/issues/19851) - Fix `ValueError` complaining of missing `scope` when bulk importing wireless LANs
|
||||
* [#19896](https://github.com/netbox-community/netbox/issues/19896) - Min/max values for decimal custom fields should accept decimal values
|
||||
* [#20197](https://github.com/netbox-community/netbox/issues/20197) - Correct validation for virtual chassis parent interface
|
||||
* [#20215](https://github.com/netbox-community/netbox/issues/20215) - All GraphQL filters for config contexts should be optional
|
||||
* [#20217](https://github.com/netbox-community/netbox/issues/20217) - Remove "0 VLANs available" row at end of VLAN range table
|
||||
* [#20221](https://github.com/netbox-community/netbox/issues/20221) - JSON fields should not coerce empty dictionaries to null
|
||||
* [#20227](https://github.com/netbox-community/netbox/issues/20227) - Ensure consistent padding of Markdown content
|
||||
* [#20234](https://github.com/netbox-community/netbox/issues/20234) - Fix "add" button link for prerequisite object warning in UI
|
||||
* [#20236](https://github.com/netbox-community/netbox/issues/20236) - Strip invalid characters from uploaded image file names
|
||||
* [#20238](https://github.com/netbox-community/netbox/issues/20238) - Fix support for outside IP assignment during bulk import of tunnel terminations
|
||||
* [#20242](https://github.com/netbox-community/netbox/issues/20242) - Avoid `AttributeError` exception on background jobs with no request ID
|
||||
* [#20252](https://github.com/netbox-community/netbox/issues/20252) - Remove generic AddObject from ObjectChildrenView to prevent duplicate "add" buttons
|
||||
* [#20264](https://github.com/netbox-community/netbox/issues/20264) - Fix rendering of default icon in plugins list
|
||||
* [#20272](https://github.com/netbox-community/netbox/issues/20272) - ConfigContexts assigned to ancestor locations should apply to device/VM
|
||||
* [#20282](https://github.com/netbox-community/netbox/issues/20282) - Fix styling of prerequisite objects warning
|
||||
* [#20298](https://github.com/netbox-community/netbox/issues/20298) - Display a placeholder when an image thumbnail fails to load
|
||||
* [#20327](https://github.com/netbox-community/netbox/issues/20327) - Avoid calling `distinct()` on device/VM queryset when fetching config context data
|
||||
|
||||
---
|
||||
|
||||
## v4.4.0 (2025-09-02)
|
||||
|
||||
### New Features
|
||||
|
||||
@@ -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
|
||||
10
mkdocs.yml
10
mkdocs.yml
@@ -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'
|
||||
@@ -87,7 +86,6 @@ nav:
|
||||
- Change Logging: 'features/change-logging.md'
|
||||
- Journaling: 'features/journaling.md'
|
||||
- Event Rules: 'features/event-rules.md'
|
||||
- User Preferences: 'features/user-preferences.md'
|
||||
- Notifications: 'features/notifications.md'
|
||||
- Background Jobs: 'features/background-jobs.md'
|
||||
- Auth & Permissions: 'features/authentication-permissions.md'
|
||||
@@ -126,9 +124,6 @@ nav:
|
||||
- Export Templates: 'customization/export-templates.md'
|
||||
- Reports: 'customization/reports.md'
|
||||
- Custom Scripts: 'customization/custom-scripts.md'
|
||||
- Best Practices:
|
||||
- Modeling Pluggable Transceivers: 'best-practices/modeling-pluggable-transceivers.md'
|
||||
- Performance Handbook: 'best-practices/performance-handbook.md'
|
||||
- Integrations:
|
||||
- REST API: 'integrations/rest-api.md'
|
||||
- GraphQL API: 'integrations/graphql-api.md'
|
||||
@@ -143,7 +138,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 +269,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 +313,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
57
netbox/account/tables.py
Normal 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',
|
||||
)
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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')),
|
||||
|
||||
@@ -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',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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'
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -6,6 +6,7 @@ from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from circuits.choices import *
|
||||
from circuits.constants import *
|
||||
from dcim.models import CabledObjectModel
|
||||
from netbox.models import ChangeLoggedModel, OrganizationalModel, PrimaryModel
|
||||
from netbox.models.mixins import DistanceMixin
|
||||
@@ -230,7 +231,6 @@ class CircuitGroupAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin,
|
||||
class CircuitTermination(
|
||||
CustomFieldsMixin,
|
||||
CustomLinksMixin,
|
||||
ExportTemplatesMixin,
|
||||
TagsMixin,
|
||||
ChangeLoggedModel,
|
||||
CabledObjectModel
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from circuits.choices import *
|
||||
from netbox.models import ChangeLoggedModel, PrimaryModel
|
||||
from netbox.models.features import CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, TagsMixin
|
||||
from netbox.models.features import CustomFieldsMixin, CustomLinksMixin, TagsMixin
|
||||
from .base import BaseCircuitType
|
||||
|
||||
__all__ = (
|
||||
@@ -121,7 +121,6 @@ class VirtualCircuit(PrimaryModel):
|
||||
class VirtualCircuitTermination(
|
||||
CustomFieldsMixin,
|
||||
CustomLinksMixin,
|
||||
ExportTemplatesMixin,
|
||||
TagsMixin,
|
||||
ChangeLoggedModel
|
||||
):
|
||||
|
||||
@@ -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',)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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))),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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],
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -13,7 +13,7 @@ class BackgroundTaskSerializer(serializers.Serializer):
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name='core-api:rqtask-detail',
|
||||
lookup_field='id',
|
||||
lookup_url_kwarg='id'
|
||||
lookup_url_kwarg='pk'
|
||||
)
|
||||
description = serializers.CharField()
|
||||
origin = serializers.CharField()
|
||||
|
||||
@@ -5,7 +5,7 @@ from django_rq.queues import get_redis_connection
|
||||
from django_rq.settings import QUEUES_LIST
|
||||
from django_rq.utils import get_statistics
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiParameter, extend_schema
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
@@ -117,49 +117,29 @@ class BaseRQViewSet(viewsets.ViewSet):
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
"""
|
||||
Return the serializer instance that should be used for validating and
|
||||
deserializing input and for serializing output.
|
||||
deserializing input, and for serializing output.
|
||||
"""
|
||||
serializer_class = self.get_serializer_class()
|
||||
kwargs['context'] = self.get_serializer_context()
|
||||
return serializer_class(*args, **kwargs)
|
||||
|
||||
def get_serializer_class(self):
|
||||
"""
|
||||
Return the class to use for the serializer.
|
||||
"""
|
||||
return self.serializer_class
|
||||
|
||||
def get_serializer_context(self):
|
||||
"""
|
||||
Extra context provided to the serializer class.
|
||||
"""
|
||||
return {
|
||||
'request': self.request,
|
||||
'format': self.format_kwarg,
|
||||
'view': self,
|
||||
}
|
||||
|
||||
|
||||
class BackgroundQueueViewSet(BaseRQViewSet):
|
||||
"""
|
||||
Retrieve a list of RQ Queues.
|
||||
Note: Queue names are not URL safe, so not returning a detail view.
|
||||
Note: Queue names are not URL safe so not returning a detail view.
|
||||
"""
|
||||
serializer_class = serializers.BackgroundQueueSerializer
|
||||
lookup_field = 'name'
|
||||
lookup_value_regex = r'[\w.@+-]+'
|
||||
|
||||
def get_view_name(self):
|
||||
return 'Background Queues'
|
||||
return "Background Queues"
|
||||
|
||||
def get_data(self):
|
||||
return get_statistics(run_maintenance_tasks=True)['queues']
|
||||
return get_statistics(run_maintenance_tasks=True)["queues"]
|
||||
|
||||
@extend_schema(
|
||||
operation_id='core_background_queues_retrieve_by_name',
|
||||
parameters=[OpenApiParameter(name='name', type=OpenApiTypes.STR, location=OpenApiParameter.PATH)],
|
||||
responses={200: OpenApiTypes.OBJECT},
|
||||
)
|
||||
@extend_schema(responses={200: OpenApiTypes.OBJECT})
|
||||
def retrieve(self, request, name):
|
||||
data = self.get_data()
|
||||
if not data:
|
||||
@@ -181,17 +161,12 @@ class BackgroundWorkerViewSet(BaseRQViewSet):
|
||||
lookup_field = 'name'
|
||||
|
||||
def get_view_name(self):
|
||||
return 'Background Workers'
|
||||
return "Background Workers"
|
||||
|
||||
def get_data(self):
|
||||
config = QUEUES_LIST[0]
|
||||
return Worker.all(get_redis_connection(config['connection_config']))
|
||||
|
||||
@extend_schema(
|
||||
operation_id='core_background_workers_retrieve_by_name',
|
||||
parameters=[OpenApiParameter(name='name', type=OpenApiTypes.STR, location=OpenApiParameter.PATH)],
|
||||
responses={200: OpenApiTypes.OBJECT},
|
||||
)
|
||||
def retrieve(self, request, name):
|
||||
# all the RQ queues should use the same connection
|
||||
config = QUEUES_LIST[0]
|
||||
@@ -209,10 +184,9 @@ class BackgroundTaskViewSet(BaseRQViewSet):
|
||||
Retrieve a list of RQ Tasks.
|
||||
"""
|
||||
serializer_class = serializers.BackgroundTaskSerializer
|
||||
lookup_field = 'id'
|
||||
|
||||
def get_view_name(self):
|
||||
return 'Background Tasks'
|
||||
return "Background Tasks"
|
||||
|
||||
def get_data(self):
|
||||
return get_rq_jobs()
|
||||
@@ -225,53 +199,45 @@ class BackgroundTaskViewSet(BaseRQViewSet):
|
||||
|
||||
return task
|
||||
|
||||
@extend_schema(
|
||||
operation_id='core_background_tasks_retrieve_by_id',
|
||||
parameters=[OpenApiParameter(name='id', type=OpenApiTypes.STR, location=OpenApiParameter.PATH)],
|
||||
responses={200: OpenApiTypes.OBJECT},
|
||||
)
|
||||
def retrieve(self, request, id):
|
||||
@extend_schema(responses={200: OpenApiTypes.OBJECT})
|
||||
def retrieve(self, request, pk):
|
||||
"""
|
||||
Retrieve the details of the specified RQ Task.
|
||||
"""
|
||||
task = self.get_task_from_id(id)
|
||||
task = self.get_task_from_id(pk)
|
||||
serializer = self.serializer_class(task, context={'request': request})
|
||||
return Response(serializer.data)
|
||||
|
||||
@extend_schema(parameters=[OpenApiParameter(name='id', type=OpenApiTypes.STR, location=OpenApiParameter.PATH)])
|
||||
@action(methods=['POST'], detail=True)
|
||||
def delete(self, request, id):
|
||||
@action(methods=["POST"], detail=True)
|
||||
def delete(self, request, pk):
|
||||
"""
|
||||
Delete the specified RQ Task.
|
||||
"""
|
||||
delete_rq_job(id)
|
||||
delete_rq_job(pk)
|
||||
return HttpResponse(status=200)
|
||||
|
||||
@extend_schema(parameters=[OpenApiParameter(name='id', type=OpenApiTypes.STR, location=OpenApiParameter.PATH)])
|
||||
@action(methods=['POST'], detail=True)
|
||||
def requeue(self, request, id):
|
||||
@action(methods=["POST"], detail=True)
|
||||
def requeue(self, request, pk):
|
||||
"""
|
||||
Requeues the specified RQ Task.
|
||||
"""
|
||||
requeue_rq_job(id)
|
||||
requeue_rq_job(pk)
|
||||
return HttpResponse(status=200)
|
||||
|
||||
@extend_schema(parameters=[OpenApiParameter(name='id', type=OpenApiTypes.STR, location=OpenApiParameter.PATH)])
|
||||
@action(methods=['POST'], detail=True)
|
||||
def enqueue(self, request, id):
|
||||
@action(methods=["POST"], detail=True)
|
||||
def enqueue(self, request, pk):
|
||||
"""
|
||||
Enqueues the specified RQ Task.
|
||||
"""
|
||||
enqueue_rq_job(id)
|
||||
enqueue_rq_job(pk)
|
||||
return HttpResponse(status=200)
|
||||
|
||||
@extend_schema(parameters=[OpenApiParameter(name='id', type=OpenApiTypes.STR, location=OpenApiParameter.PATH)])
|
||||
@action(methods=['POST'], detail=True)
|
||||
def stop(self, request, id):
|
||||
@action(methods=["POST"], detail=True)
|
||||
def stop(self, request, pk):
|
||||
"""
|
||||
Stops the specified RQ Task.
|
||||
"""
|
||||
stopped_jobs = stop_rq_job(id)
|
||||
stopped_jobs = stop_rq_job(pk)
|
||||
if len(stopped_jobs) == 1:
|
||||
return HttpResponse(status=200)
|
||||
else:
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'))
|
||||
)
|
||||
|
||||
@@ -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'))
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user