mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 09:51:22 -06:00
Merge branch 'main' of https://github.com/netbox-community/netbox into 15016-fix-assertionerror-when-saving-cable
This commit is contained in:
commit
e11ee36f7a
16
.github/ISSUE_TEMPLATE/01-feature_request.yaml
vendored
16
.github/ISSUE_TEMPLATE/01-feature_request.yaml
vendored
@ -1,5 +1,6 @@
|
||||
---
|
||||
name: ✨ Feature Request
|
||||
type: Feature
|
||||
description: Propose a new NetBox feature or enhancement
|
||||
labels: ["type: feature", "status: needs triage"]
|
||||
body:
|
||||
@ -14,7 +15,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.1.8
|
||||
placeholder: v4.2.4
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
@ -27,19 +28,6 @@ body:
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Triage priority
|
||||
description: >
|
||||
Issue triage may be prioritized in some cases. Select whichever of the following
|
||||
conditions applies, if any.
|
||||
options:
|
||||
- I volunteer to perform this work (if approved)
|
||||
- I'm a NetBox Labs customer
|
||||
- N/A
|
||||
default: 2
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Proposed functionality
|
||||
|
16
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
16
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
@ -1,5 +1,6 @@
|
||||
---
|
||||
name: 🐛 Bug Report
|
||||
type: Bug
|
||||
description: Report a reproducible bug in the current release of NetBox
|
||||
labels: ["type: bug", "status: needs triage"]
|
||||
body:
|
||||
@ -22,24 +23,11 @@ body:
|
||||
- Self-hosted
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Triage priority
|
||||
description: >
|
||||
Issue triage may be prioritized in some cases. Select whichever of the following
|
||||
conditions applies, if any.
|
||||
options:
|
||||
- I volunteer to perform this work (if approved)
|
||||
- I'm a NetBox Labs customer
|
||||
- N/A
|
||||
default: 2
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: NetBox Version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.1.8
|
||||
placeholder: v4.2.4
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
@ -1,5 +1,6 @@
|
||||
---
|
||||
name: 📖 Documentation Change
|
||||
type: Documentation
|
||||
description: Suggest an addition or modification to the NetBox documentation
|
||||
labels: ["type: documentation", "status: needs triage"]
|
||||
body:
|
||||
|
1
.github/ISSUE_TEMPLATE/04-translation.yaml
vendored
1
.github/ISSUE_TEMPLATE/04-translation.yaml
vendored
@ -1,5 +1,6 @@
|
||||
---
|
||||
name: 🌍 Translation
|
||||
type: Translation
|
||||
description: Request support for a new language in the user interface
|
||||
labels: ["type: translation"]
|
||||
body:
|
||||
|
1
.github/ISSUE_TEMPLATE/05-housekeeping.yaml
vendored
1
.github/ISSUE_TEMPLATE/05-housekeeping.yaml
vendored
@ -1,5 +1,6 @@
|
||||
---
|
||||
name: 🏡 Housekeeping
|
||||
type: Housekeeping
|
||||
description: A change pertaining to the codebase itself (developers only)
|
||||
labels: ["type: housekeeping"]
|
||||
body:
|
||||
|
1
.github/ISSUE_TEMPLATE/06-deprecation.yaml
vendored
1
.github/ISSUE_TEMPLATE/06-deprecation.yaml
vendored
@ -1,5 +1,6 @@
|
||||
---
|
||||
name: 🗑️ Deprecation
|
||||
type: Deprecation
|
||||
description: The removal of an existing feature or resource
|
||||
labels: ["type: deprecation"]
|
||||
body:
|
||||
|
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -2,7 +2,7 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 📖 Contributing Policy
|
||||
url: https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md
|
||||
url: https://github.com/netbox-community/netbox/blob/main/CONTRIBUTING.md
|
||||
about: "Please read through our contributing policy before opening an issue or pull request."
|
||||
- name: ❓ Discussion
|
||||
url: https://github.com/netbox-community/netbox/discussions
|
||||
|
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@ -3,11 +3,15 @@ name: CI
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- '.github/ISSUE_TEMPLATE/**'
|
||||
- '.github/PULL_REQUEST_TEMPLATE.md'
|
||||
- 'contrib/**'
|
||||
- 'docs/**'
|
||||
- 'netbox/translations/**'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '.github/ISSUE_TEMPLATE/**'
|
||||
- '.github/PULL_REQUEST_TEMPLATE.md'
|
||||
- 'contrib/**'
|
||||
- 'docs/**'
|
||||
- 'netbox/translations/**'
|
||||
@ -28,7 +32,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.10', '3.11', '3.12']
|
||||
node-version: ['18.x']
|
||||
node-version: ['20.x']
|
||||
services:
|
||||
redis:
|
||||
image: redis
|
||||
|
2
.github/workflows/close-stale-issues.yml
vendored
2
.github/workflows/close-stale-issues.yml
vendored
@ -38,7 +38,7 @@ jobs:
|
||||
issues may receive direct feedback. **Do not** attempt to circumvent this
|
||||
process by "bumping" the issue; doing so will result in its immediate closure
|
||||
and you may be barred from participating in any future discussions. Please see
|
||||
our [contributing guide](https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md).
|
||||
our [contributing guide](https://github.com/netbox-community/netbox/blob/main/CONTRIBUTING.md).
|
||||
|
||||
# Pull request parameters
|
||||
close-pr-message: >
|
||||
|
@ -1,7 +1,7 @@
|
||||
[main]
|
||||
host = https://app.transifex.com
|
||||
|
||||
[o:netbox-community:p:netbox:r:9cbf4fcf95b3d92e4ebbf1a5e5d1caee]
|
||||
[o:netbox-community:p:netbox:r:034999968a7366ba27a8bdf1ab63bf42]
|
||||
file_filter = netbox/translations/<lang>/LC_MESSAGES/django.po
|
||||
source_file = netbox/translations/en/LC_MESSAGES/django.po
|
||||
type = PO
|
||||
|
@ -84,7 +84,7 @@ intake policy](https://github.com/netbox-community/netbox/wiki/Issue-Intake-Poli
|
||||
|
||||
* It's very important that you not submit a pull request until a relevant issue has been opened **and** assigned to you. Otherwise, you risk wasting time on work that may ultimately not be needed.
|
||||
|
||||
* New pull requests should generally be based off of the `develop` branch, rather than `master`. The `develop` branch is used for ongoing development, while `master` is used for tracking stable releases. (If you're developing for an upcoming minor release, use `feature` instead.)
|
||||
* New pull requests should generally be based off of the `main` branch. This branch, in keeping with the [trunk-based development](https://trunkbaseddevelopment.com/) approach, is used for ongoing development and bug fixes and always represents the newest stable code, from which releases are periodically branched. (If you're developing for an upcoming minor release, use `feature` instead.)
|
||||
|
||||
* In most cases, it is not necessary to add a changelog entry: A maintainer will take care of this when the PR is merged. (This helps avoid merge conflicts resulting from multiple PRs being submitted simultaneously.)
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
<div align="center">
|
||||
<img src="https://raw.githubusercontent.com/netbox-community/netbox/develop/docs/netbox_logo_light.svg" width="400" alt="NetBox logo" />
|
||||
<img src="https://raw.githubusercontent.com/netbox-community/netbox/main/docs/netbox_logo_light.svg" width="400" alt="NetBox logo" />
|
||||
<p><strong>The cornerstone of every automated network</strong></p>
|
||||
<a href="https://github.com/netbox-community/netbox/releases"><img src="https://img.shields.io/github/v/release/netbox-community/netbox" alt="Latest release" /></a>
|
||||
<a href="https://github.com/netbox-community/netbox/blob/master/LICENSE.txt"><img src="https://img.shields.io/badge/license-Apache_2.0-blue.svg" alt="License" /></a>
|
||||
<a href="https://github.com/netbox-community/netbox/blob/main/LICENSE.txt"><img src="https://img.shields.io/badge/license-Apache_2.0-blue.svg" alt="License" /></a>
|
||||
<a href="https://github.com/netbox-community/netbox/graphs/contributors"><img src="https://img.shields.io/github/contributors/netbox-community/netbox?color=blue" alt="Contributors" /></a>
|
||||
<a href="https://github.com/netbox-community/netbox/stargazers"><img src="https://img.shields.io/github/stars/netbox-community/netbox?style=flat" alt="GitHub stars" /></a>
|
||||
<a href="https://explore.transifex.com/netbox-community/netbox/"><img src="https://img.shields.io/badge/languages-15-blue" alt="Languages supported" /></a>
|
||||
<a href="https://github.com/netbox-community/netbox/actions/workflows/ci.yml"><img src="https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=master" alt="CI status" /></a>
|
||||
<a href="https://github.com/netbox-community/netbox/actions/workflows/ci.yml"><img src="https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=main" alt="CI status" /></a>
|
||||
<p>
|
||||
<strong><a href="https://github.com/netbox-community/netbox/">NetBox Community</a></strong> |
|
||||
<strong><a href="https://netboxlabs.com/netbox-cloud/">NetBox Cloud</a></strong> |
|
||||
|
@ -1,6 +1,6 @@
|
||||
# The Python web framework on which NetBox is built
|
||||
# https://docs.djangoproject.com/en/stable/releases/
|
||||
Django<5.1
|
||||
Django<5.2
|
||||
|
||||
# Django middleware which permits cross-domain API requests
|
||||
# https://github.com/adamchainz/django-cors-headers/blob/main/CHANGELOG.rst
|
||||
@ -8,8 +8,6 @@ django-cors-headers
|
||||
|
||||
# Runtime UI tool for debugging Django
|
||||
# https://github.com/jazzband/django-debug-toolbar/blob/main/docs/changes.rst
|
||||
# Pinned for DNS looukp bug; see https://github.com/netbox-community/netbox/issues/16454
|
||||
# and https://github.com/jazzband/django-debug-toolbar/issues/1927
|
||||
django-debug-toolbar
|
||||
|
||||
# Library for writing reusable URL query filters
|
||||
@ -90,7 +88,8 @@ mkdocs-material
|
||||
|
||||
# Introspection for embedded code
|
||||
# https://github.com/mkdocstrings/mkdocstrings/blob/main/CHANGELOG.md
|
||||
mkdocstrings[python-legacy]
|
||||
# See #18568
|
||||
mkdocstrings[python-legacy]==0.27.0
|
||||
|
||||
# Library for manipulating IP prefixes and addresses
|
||||
# https://github.com/netaddr/netaddr/blob/master/CHANGELOG.rst
|
||||
@ -101,7 +100,7 @@ netaddr
|
||||
nh3
|
||||
|
||||
# Fork of PIL (Python Imaging Library) for image processing
|
||||
# https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst
|
||||
# https://github.com/python-pillow/Pillow/releases
|
||||
Pillow
|
||||
|
||||
# PostgreSQL database adapter for Python
|
||||
@ -134,7 +133,8 @@ strawberry-graphql
|
||||
|
||||
# Strawberry GraphQL Django extension
|
||||
# https://github.com/strawberry-graphql/strawberry-django/releases
|
||||
strawberry-graphql-django
|
||||
# Pinned to v0.52.0 for suspected upstream bug; see #18329
|
||||
strawberry-graphql-django==0.52.0
|
||||
|
||||
# SVG image rendering (used for rack elevations)
|
||||
# https://github.com/mozman/svgwrite/blob/master/NEWS.rst
|
||||
|
@ -96,14 +96,6 @@ The maximum size (in bytes) of an incoming HTTP request (i.e. `GET` or `POST` da
|
||||
|
||||
---
|
||||
|
||||
## DJANGO_ADMIN_ENABLED
|
||||
|
||||
Default: False
|
||||
|
||||
Setting this to True installs the `django.contrib.admin` app and enables the [Django admin UI](https://docs.djangoproject.com/en/5.0/ref/contrib/admin/). This may be necessary to support older plugins which do not integrate with the native NetBox interface.
|
||||
|
||||
---
|
||||
|
||||
## ENFORCE_GLOBAL_UNIQUE
|
||||
|
||||
!!! tip "Dynamic Configuration Parameter"
|
||||
@ -114,6 +106,16 @@ By default, NetBox will prevent the creation of duplicate prefixes and IP addres
|
||||
|
||||
---
|
||||
|
||||
## EVENTS_PIPELINE
|
||||
|
||||
!!! info "This parameter was introduced in NetBox v4.2."
|
||||
|
||||
Default: `['extras.events.process_event_queue',]`
|
||||
|
||||
NetBox will call dotted paths to the functions listed here for events (create, update, delete) on models as well as when custom EventRules are fired.
|
||||
|
||||
---
|
||||
|
||||
## FILE_UPLOAD_MAX_MEMORY_SIZE
|
||||
|
||||
Default: `2621440` (2.5 MB)
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
## ALLOWED_HOSTS
|
||||
|
||||
This is a list of valid fully-qualified domain names (FQDNs) and/or IP addresses that can be used to reach the NetBox service. Usually this is the same as the hostname for the NetBox server, but can also be different; for example, when using a reverse proxy serving the NetBox website under a different FQDN than the hostname of the NetBox server. To help guard against [HTTP Host header attackes](https://docs.djangoproject.com/en/3.0/topics/security/#host-headers-virtual-hosting), NetBox will not permit access to the server via any other hostnames (or IPs).
|
||||
This is a list of valid fully-qualified domain names (FQDNs) and/or IP addresses that can be used to reach the NetBox service. Usually this is the same as the hostname for the NetBox server, but can also be different; for example, when using a reverse proxy serving the NetBox website under a different FQDN than the hostname of the NetBox server. To help guard against [HTTP Host header attacks](https://docs.djangoproject.com/en/3.0/topics/security/#host-headers-virtual-hosting), NetBox will not permit access to the server via any other hostnames (or IPs).
|
||||
|
||||
!!! note
|
||||
This parameter must always be defined as a list or tuple, even if only a single value is provided.
|
||||
@ -25,7 +25,7 @@ ALLOWED_HOSTS = ['*']
|
||||
|
||||
## DATABASE
|
||||
|
||||
NetBox requires access to a PostgreSQL 12 or later database service to store data. This service can run locally on the NetBox server or on a remote system. The following parameters must be defined within the `DATABASE` dictionary:
|
||||
NetBox requires access to a PostgreSQL 13 or later database service to store data. This service can run locally on the NetBox server or on a remote system. The following parameters must be defined within the `DATABASE` dictionary:
|
||||
|
||||
* `NAME` - Database name
|
||||
* `USER` - PostgreSQL username
|
||||
|
@ -89,8 +89,6 @@ addresses (and [`DEBUG`](./development.md#debug) is true).
|
||||
|
||||
## ISOLATED_DEPLOYMENT
|
||||
|
||||
!!! info "This feature was introduced in NetBox v4.1."
|
||||
|
||||
Default: False
|
||||
|
||||
Set this configuration parameter to True for NetBox deployments which do not have Internet access. This will disable miscellaneous functionality which depends on access to the Internet.
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
Custom links allow users to display arbitrary hyperlinks to external content within NetBox object views. These are helpful for cross-referencing related records in systems outside NetBox. For example, you might create a custom link on the device view which links to the current device in a Network Monitoring System (NMS).
|
||||
|
||||
Custom links are created by navigating to Customization > Custom Links. Each link is associated with a particular NetBox object type (site, device, prefix, etc.) and will be displayed on relevant views. Each link has display text and a URL, and data from the NetBox item being viewed can be included in the link using [Jinja2 template code](https://jinja2docs.readthedocs.io/en/stable/) through the variable `object`, and custom fields through `object.cf`.
|
||||
Custom links are created by navigating to Customization > Custom Links. Each link is associated with a particular NetBox object type (site, device, prefix, etc.) and will be displayed on relevant views. Each link has display text and a URL, and data from the NetBox item being viewed can be included in the link using [Jinja template code](https://jinja.palletsprojects.com/en/stable/) through the variable `object`, and custom fields through `object.cf`.
|
||||
|
||||
For example, you might define a link like this:
|
||||
|
||||
|
@ -8,7 +8,7 @@ Each model should define, at a minimum:
|
||||
|
||||
* A `Meta` class specifying a deterministic ordering (if ordered by fields other than the primary ID)
|
||||
* A `__str__()` method returning a user-friendly string representation of the instance
|
||||
* A `get_absolute_url()` method returning an instance's direct URL (using `reverse()`)
|
||||
* A `get_absolute_url()` method if necessary; a standard version of the method is defined in the `NetBoxFeatureSet` base class, but you will need to provide your own (returning an instance's direct URL using `reverse()`) if not subclassing that base class
|
||||
|
||||
## 2. Define field choices
|
||||
|
||||
@ -78,6 +78,8 @@ Create the following for each model:
|
||||
|
||||
Create a GraphQL object type for the model in `graphql/types.py` by subclassing the appropriate class from `netbox.graphql.types`.
|
||||
|
||||
**Note:** GraphQL unit tests may fail citing null values on a non-nullable field if related objects are prefetched. You may need to fix this by setting the type annotation to be `= strawberry_django.field(select_related=["policy"])` or similar.
|
||||
|
||||
Also extend the schema class defined in `graphql/schema.py` with the individual object and object list fields per the established convention.
|
||||
|
||||
## 14. Add tests
|
||||
|
@ -37,16 +37,12 @@ CHANGELOG.md CONTRIBUTING.md LICENSE.txt netbox README.md scri
|
||||
|
||||
### 2. Create a New Branch
|
||||
|
||||
The NetBox project utilizes three persistent git branches to track work:
|
||||
The NetBox project utilizes two persistent git branches to track work:
|
||||
|
||||
* `master` - Serves as a snapshot of the current stable release
|
||||
* `develop` - All development on the upcoming stable (patch) release occurs here
|
||||
* `feature` - Tracks work on an upcoming minor release
|
||||
* `main` - All development on the upcoming stable (patch) release occurs here. Releases are published from this branch.
|
||||
* `feature` - All work planned for the upcoming minor release is done here.
|
||||
|
||||
Typically, you'll base pull requests off of the `develop` branch, or off of `feature` if you're working on a new major release. For example, assume that the current NetBox release is v3.3.5. Work applied to the `develop` branch will appear in v3.3.6, and work done under the `feature` branch will be included in the next minor release (v3.4.0).
|
||||
|
||||
!!! warning
|
||||
**Never** merge pull requests into the `master` branch: This branch only ever merges pull requests from the `develop` branch, to effect a new release.
|
||||
Typically, you'll base pull requests off of the `main` branch, or off of `feature` if you're working on the upcoming minor or major release. For example, assume that the current NetBox release is v4.2.3. Work applied to the `main` branch will appear in v4.2.4, and work done under the `feature` branch will be included in the next minor release (v4.3.0).
|
||||
|
||||
To create a new branch, first ensure that you've checked out the desired base branch, then run:
|
||||
|
||||
|
@ -128,7 +128,7 @@ Fast-forward
|
||||
```
|
||||
|
||||
!!! warning "Avoid Merging Remote Branches"
|
||||
You generally want to avoid merging branches that exist on the remote (upstream) repository, such as `develop` and `feature`: Merges into these branches should be done via a pull request on GitHub. Only merge branches when it is necessary to consolidate work you've done locally.
|
||||
You generally want to avoid merging branches that exist on the remote (upstream) repository, namely `main` and `feature`: Merges into these branches should be done via a pull request on GitHub. Only merge branches when it is necessary to consolidate work you've done locally.
|
||||
|
||||
### Show Pending Changes
|
||||
|
||||
@ -196,7 +196,7 @@ index 93e125079..4344fb514 100644
|
||||
+and here too
|
||||
+
|
||||
<div align="center">
|
||||
<img src="https://raw.githubusercontent.com/netbox-community/netbox/develop/docs/netbox_logo.svg" width="400" alt="NetBox logo" />
|
||||
<img src="https://raw.githubusercontent.com/netbox-community/netbox/main/docs/netbox_logo.svg" width="400" alt="NetBox logo" />
|
||||
</div>
|
||||
diff --git a/foo.py b/foo.py
|
||||
new file mode 100644
|
||||
|
@ -8,11 +8,10 @@ NetBox and many of its related projects are maintained on [GitHub](https://githu
|
||||
|
||||

|
||||
|
||||
There are three permanent branches in the repository:
|
||||
There are two permanent branches in the repository:
|
||||
|
||||
* `master` - The current stable release. Individual changes should never be pushed directly to this branch, but rather merged from `develop`.
|
||||
* `develop` - Active development for the upcoming patch release. Pull requests will typically be based on this branch unless they introduce breaking changes that must be deferred until the next minor release.
|
||||
* `feature` - New feature work to be introduced in the next minor release (e.g. from v3.3 to v3.4).
|
||||
* `main` - Active development for the upcoming patch release. Pull requests will typically be based on this branch unless they introduce breaking changes that must be deferred until the next minor release.
|
||||
* `feature` - New feature work to be introduced in the next minor release (e.g. from v4.2 to v4.3).
|
||||
|
||||
NetBox components are arranged into Django apps. Each app holds the models, views, and other resources relevant to a particular function:
|
||||
|
||||
@ -57,4 +56,4 @@ NetBox follows the [benevolent dictator](http://oss-watch.ac.uk/resources/benevo
|
||||
|
||||
## Licensing
|
||||
|
||||
The entire NetBox project is licensed as open source under the [Apache 2.0 license](https://github.com/netbox-community/netbox/blob/master/LICENSE.txt). This is a very permissive license which allows unlimited redistribution of all code within the project. Note that all submissions to the project are subject to the same license.
|
||||
The entire NetBox project is licensed as open source under the [Apache 2.0 license](https://github.com/netbox-community/netbox/blob/main/LICENSE.txt). This is a very permissive license which allows unlimited redistribution of all code within the project. Note that all submissions to the project are subject to the same license.
|
||||
|
@ -8,6 +8,8 @@ This documentation describes the process of packaging and publishing a new NetBo
|
||||
|
||||
While major releases generally introduce some very substantial change to the application, they are typically treated the same as minor version increments for the purpose of release packaging.
|
||||
|
||||
For patch releases (e.g. upgrading from v4.2.2 to v4.2.3), begin at the [patch releases](#patch-releases) heading below. For minor or major releases, complete the entire checklist.
|
||||
|
||||
## Minor Version Releases
|
||||
|
||||
### Address Constrained Dependencies
|
||||
@ -43,9 +45,9 @@ Follow these instructions to perform a new installation of NetBox in a temporary
|
||||
|
||||
Upgrading from a previous version typically involves database migrations, which must work without errors. Supported upgrade paths include from one minor version to another within the same major version (i.e. 4.0 to 4.1), as well as from the latest patch version of the previous minor version (i.e. 3.7 to 4.0 or to 4.1). Prior to release, test all these supported paths by loading demo data from the source version and performing a `./manage.py migrate`.
|
||||
|
||||
### Merge the Release Branch
|
||||
### Merge the `feature` Branch
|
||||
|
||||
Submit a pull request to merge the `feature` branch into the `develop` branch in preparation for its release. Once it has been merged, continue with the section for patch releases below.
|
||||
Submit a pull request to merge the `feature` branch into the `main` branch in preparation for its release. Once it has been merged, continue with the section for patch releases below.
|
||||
|
||||
### Rebuild Demo Data (After Release)
|
||||
|
||||
@ -55,6 +57,15 @@ After the release of a new minor version, generate a new demo data snapshot comp
|
||||
|
||||
## Patch Releases
|
||||
|
||||
### Create a Release Branch
|
||||
|
||||
Begin by creating a new branch (based off of `main`) to effect the release. This will comprise the changes listed below.
|
||||
|
||||
```
|
||||
git checkout main
|
||||
git checkout -B release-vX.Y.Z
|
||||
```
|
||||
|
||||
### Notify netbox-docker Project of Any Relevant Changes
|
||||
|
||||
Notify the [`netbox-docker`](https://github.com/netbox-community/netbox-docker) maintainers (in **#netbox-docker**) of any changes that may be relevant to their build process, including:
|
||||
@ -76,7 +87,20 @@ In cases where upgrading a dependency to its most recent release is breaking, it
|
||||
|
||||
### Update UI Dependencies
|
||||
|
||||
Check whether any UI dependencies (JavaScript packages, fonts, etc.) need to be updated by running `yarn outdated` from within the `project-static/` directory. [Upgrade these dependencies](./web-ui.md#updating-dependencies) as necessary, then run `yarn bundle` to generate the necessary files for distribution.
|
||||
Check whether any UI dependencies (JavaScript packages, fonts, etc.) need to be updated by running `yarn outdated` from within the `project-static/` directory. [Upgrade these dependencies](./web-ui.md#updating-dependencies) as necessary, then run `yarn bundle` to generate the necessary files for distribution:
|
||||
|
||||
```
|
||||
$ yarn bundle
|
||||
yarn run v1.22.19
|
||||
$ node bundle.js
|
||||
✅ Bundled source file 'styles/external.scss' to 'netbox-external.css'
|
||||
✅ Bundled source file 'styles/netbox.scss' to 'netbox.css'
|
||||
✅ Bundled source file 'styles/svg/rack_elevation.scss' to 'rack_elevation.css'
|
||||
✅ Bundled source file 'styles/svg/cable_trace.scss' to 'cable_trace.css'
|
||||
✅ Bundled source file 'index.ts' to 'netbox.js'
|
||||
✅ Copied graphiql files
|
||||
Done in 1.00s.
|
||||
```
|
||||
|
||||
### Rebuild the Device Type Definition Schema
|
||||
|
||||
@ -107,29 +131,29 @@ Then, compile these portable (`.po`) files for use in the application:
|
||||
|
||||
### Update Version and Changelog
|
||||
|
||||
* Update the version and published date in `release.yaml` with the current version & date. Add a designation (e.g.g `beta1`) if applicable.
|
||||
* Update the version number and date in `netbox/release.yaml`. Add or remove the designation (e.g. `beta1`) if applicable.
|
||||
* Update the example version numbers in the feature request and bug report templates under `.github/ISSUE_TEMPLATES/`.
|
||||
* Replace the "FUTURE" placeholder in the release notes with the current date.
|
||||
* Add a section for this release at the top of the changelog page for the minor version (e.g. `docs/release-notes/version-4.2.md`) listing all relevant changes made in this release.
|
||||
|
||||
Commit these changes to the `develop` branch and push upstream.
|
||||
|
||||
### Verify CI Build Status
|
||||
|
||||
Ensure that continuous integration testing on the `develop` branch is completing successfully. If it fails, take action to correct the failure before proceeding with the release.
|
||||
!!! 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 key words or phrases (such as exception names) that can be easily searched.
|
||||
|
||||
### Submit a Pull Request
|
||||
|
||||
Submit a pull request titled **"Release vX.Y.Z"** to merge the `develop` branch into `master`. Copy the documented release notes into the pull request's body.
|
||||
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.
|
||||
|
||||
Once CI has completed on the PR, merge it. This effects a new release in the `master` branch.
|
||||
Once CI has completed and a colleague has reviewed the PR, merge it. This effects a new release in the `main` branch.
|
||||
|
||||
!!! warning
|
||||
To ensure a streamlined review process, the pull request for a release **must** be limited to the changes outlined in this document. A release PR must never include functional changes to the application: Any unrelated "cleanup" needs to be captured in a separate PR prior to the release being shipped.
|
||||
|
||||
### Create a New Release
|
||||
|
||||
Create a [new release](https://github.com/netbox-community/netbox/releases/new) on GitHub with the following parameters.
|
||||
|
||||
* **Tag:** Current version (e.g. `v3.3.1`)
|
||||
* **Target:** `master`
|
||||
* **Title:** Version and date (e.g. `v3.3.1 - 2022-08-25`)
|
||||
* **Tag:** Current version (e.g. `v4.2.1`)
|
||||
* **Target:** `main`
|
||||
* **Title:** Version and date (e.g. `v4.2.1 - 2025-01-17`)
|
||||
* **Description:** Copy from the pull request body, then promote the `###` headers to `##` ones
|
||||
|
||||
Once created, the release will become available for users to install.
|
||||
|
@ -22,7 +22,7 @@ NetBox generally follows the [Django style guide](https://docs.djangoproject.com
|
||||
|
||||
### Linting
|
||||
|
||||
The [ruff](https://docs.astral.sh/ruff/) linter is used to enforce code style. A [pre-commit hook](./getting-started.md#3-enable-pre-commit-hooks) which runs this automatically is included with NetBox. To invoke `ruff` manually, run:
|
||||
The [ruff](https://docs.astral.sh/ruff/) linter is used to enforce code style, and is run automatically by [pre-commit](./getting-started.md#5-install-pre-commit). To invoke `ruff` manually, run:
|
||||
|
||||
```
|
||||
ruff check netbox/
|
||||
|
@ -14,10 +14,10 @@ To update the English `.po` file from which all translations are derived, use th
|
||||
./manage.py makemessages -l en -i "project-static/*"
|
||||
```
|
||||
|
||||
Then, commit the change and push to the `develop` branch on GitHub. Any new strings will appear for translation on Transifex automatically.
|
||||
Then, commit the change and push to the `main` branch on GitHub. Any new strings will appear for translation on Transifex automatically.
|
||||
|
||||
!!! note
|
||||
It is typically not necessary to update source strings manually, as this is done nightly by a [GitHub action](https://github.com/netbox-community/netbox/blob/develop/.github/workflows/update-translation-strings.yml).
|
||||
It is typically not necessary to update source strings manually, as this is done nightly by a [GitHub action](https://github.com/netbox-community/netbox/blob/main/.github/workflows/update-translation-strings.yml).
|
||||
|
||||
## Updating Translated Strings
|
||||
|
||||
@ -30,7 +30,7 @@ To download translated strings automatically, you'll need to:
|
||||
1. Install the [Transifex CLI client](https://github.com/transifex/cli)
|
||||
2. Generate a [Transifex API token](https://app.transifex.com/user/settings/api/)
|
||||
|
||||
Once you have the client set up, run the following command:
|
||||
Once you have the client set up, run the following command from the project root (e.g. `/opt/netbox/`):
|
||||
|
||||
```no-highlight
|
||||
TX_TOKEN=$TOKEN tx pull
|
||||
@ -46,6 +46,9 @@ Once retrieved, the updated strings need to be compiled into new `.mo` files so
|
||||
|
||||
Once any new `.mo` files have been generated, they need to be committed and pushed back up to GitHub. (Again, this is typically done as part of publishing a new NetBox release.)
|
||||
|
||||
!!! tip
|
||||
Run `git status` to check that both `*.mo` & `*.po` files have been updated as expected.
|
||||
|
||||
## Proposing New Languages
|
||||
|
||||
If you'd like to add support for a new language to NetBox, the first step is to [submit a GitHub issue](https://github.com/netbox-community/netbox/issues/new?assignees=&labels=type%3A+translation&projects=&template=translation.yaml) to capture the proposal. While we'd like to add as many languages as possible, we do need to limit the rate at which new languages are added. New languages will be selected according to community interest and the number of volunteers who sign up as translators.
|
||||
|
@ -46,7 +46,7 @@ Regions will always be listed alphabetically by name within each parent, and the
|
||||
|
||||
Like regions, site groups can be arranged in a recursive hierarchy for grouping sites. However, whereas regions are intended for geographic organization, site groups may be used for functional grouping. For example, you might classify sites as corporate, branch, or customer sites in addition to where they are physically located.
|
||||
|
||||
The use of both regions and site groups affords to independent but complementary dimensions across which sites can be organized.
|
||||
The use of both regions and site groups affords two independent but complementary dimensions across which sites can be organized.
|
||||
|
||||
## Sites
|
||||
|
||||
|
@ -1,7 +1,5 @@
|
||||
# Notifications
|
||||
|
||||
!!! info "This feature was introduced in NetBox v4.1."
|
||||
|
||||
NetBox includes a system for generating user notifications, which can be marked as read or deleted by individual users. There are two built-in mechanisms for generating a notification:
|
||||
|
||||
* A user can subscribe to an object. When that object is modified, a notification is created to inform the user of the change.
|
||||
|
@ -2,8 +2,8 @@
|
||||
|
||||
This section entails the installation and configuration of a local PostgreSQL database. If you already have a PostgreSQL database service in place, skip to [the next section](2-redis.md).
|
||||
|
||||
!!! warning "PostgreSQL 12 or later required"
|
||||
NetBox requires PostgreSQL 12 or later. Please note that MySQL and other relational databases are **not** supported.
|
||||
!!! warning "PostgreSQL 13 or later required"
|
||||
NetBox requires PostgreSQL 13 or later. Please note that MySQL and other relational databases are **not** supported.
|
||||
|
||||
## Installation
|
||||
|
||||
@ -34,7 +34,7 @@ This section entails the installation and configuration of a local PostgreSQL da
|
||||
sudo systemctl enable --now postgresql
|
||||
```
|
||||
|
||||
Before continuing, verify that you have installed PostgreSQL 12 or later:
|
||||
Before continuing, verify that you have installed PostgreSQL 13 or later:
|
||||
|
||||
```no-highlight
|
||||
psql -V
|
||||
@ -62,6 +62,9 @@ GRANT CREATE ON SCHEMA public TO netbox;
|
||||
!!! danger "Use a strong password"
|
||||
**Do not use the password from the example.** Choose a strong, random password to ensure secure database authentication for your NetBox installation.
|
||||
|
||||
!!! danger "Use UTF8 encoding"
|
||||
Make sure that your database uses `UTF8` encoding (the default for new installations). Especially do not use `SQL_ASCII` encoding, as it can lead to unpredictable and unrecoverable errors. Enter `\l` to check your encoding.
|
||||
|
||||
Once complete, enter `\q` to exit the PostgreSQL shell.
|
||||
|
||||
## Verify Service Status
|
||||
|
@ -29,7 +29,7 @@ python3 -V
|
||||
|
||||
## Download NetBox
|
||||
|
||||
This documentation provides two options for installing NetBox: from a downloadable archive, or from the git repository. Installing from a package (option A below) requires manually fetching and extracting the archive for every future update, whereas installation via git (option B) allows for seamless upgrades by re-pulling the `master` branch.
|
||||
This documentation provides two options for installing NetBox: from a downloadable archive, or from the git repository. Installing from a package (option A below) requires manually fetching and extracting the archive for every future update, whereas installation via git (option B) allows for seamless upgrades by checking out the latest release tag.
|
||||
|
||||
### Option A: Download a Release Archive
|
||||
|
||||
@ -67,16 +67,13 @@ If `git` is not already installed, install it:
|
||||
sudo yum install -y git
|
||||
```
|
||||
|
||||
Next, clone the **master** branch of the NetBox GitHub repository into the current directory. (This branch always holds the current stable release.)
|
||||
Next, clone the git repository:
|
||||
|
||||
```no-highlight
|
||||
sudo git clone -b master --depth 1 https://github.com/netbox-community/netbox.git .
|
||||
sudo git clone https://github.com/netbox-community/netbox.git .
|
||||
```
|
||||
|
||||
!!! note
|
||||
The `git clone` command above utilizes a "shallow clone" to retrieve only the most recent commit. If you need to download the entire history, omit the `--depth 1` argument.
|
||||
|
||||
The `git clone` command should generate output similar to the following:
|
||||
This command should generate output similar to the following:
|
||||
|
||||
```
|
||||
Cloning into '.'...
|
||||
@ -88,8 +85,13 @@ Receiving objects: 100% (996/996), 4.26 MiB | 9.81 MiB/s, done.
|
||||
Resolving deltas: 100% (148/148), done.
|
||||
```
|
||||
|
||||
!!! note
|
||||
Installation via git also allows you to easily try out different versions of NetBox. To check out a [specific NetBox release](https://github.com/netbox-community/netbox/releases), use the `git checkout` command with the desired release tag. For example, `git checkout v3.0.8`.
|
||||
Finally, check out the tag for the desired release. You can find these on our [releases page](https://github.com/netbox-community/netbox/releases). Replace `vX.Y.Z` with your selected release tag below.
|
||||
|
||||
```
|
||||
sudo git checkout vX.Y.Z
|
||||
```
|
||||
|
||||
Using this installation method enables easy upgrades in the future by simply checking out the latest release tag.
|
||||
|
||||
## Create the NetBox System User
|
||||
|
||||
|
@ -21,7 +21,7 @@ The following sections detail how to set up a new instance of NetBox:
|
||||
| Dependency | Supported Versions |
|
||||
|------------|--------------------|
|
||||
| Python | 3.10, 3.11, 3.12 |
|
||||
| PostgreSQL | 12+ |
|
||||
| PostgreSQL | 13+ |
|
||||
| Redis | 4.0+ |
|
||||
|
||||
Below is a simplified overview of the NetBox application stack for reference:
|
||||
|
@ -20,15 +20,15 @@ NetBox requires the following dependencies:
|
||||
| Dependency | Supported Versions |
|
||||
|------------|--------------------|
|
||||
| Python | 3.10, 3.11, 3.12 |
|
||||
| PostgreSQL | 12+ |
|
||||
| PostgreSQL | 13+ |
|
||||
| Redis | 4.0+ |
|
||||
|
||||
## 3. Install the Latest Release
|
||||
|
||||
As with the initial installation, you can upgrade NetBox by either downloading the latest release package or by cloning the `master` branch of the git repository.
|
||||
As with the initial installation, you can upgrade NetBox by either downloading the latest release package or by checking out the latest production release from the git repository.
|
||||
|
||||
!!! warning
|
||||
Use the same method as you used to install NetBox originally
|
||||
Use the same method as you used to install NetBox originally.
|
||||
|
||||
If you are not sure how NetBox was installed originally, check with this command:
|
||||
|
||||
@ -36,10 +36,7 @@ If you are not sure how NetBox was installed originally, check with this command
|
||||
ls -ld /opt/netbox /opt/netbox/.git
|
||||
```
|
||||
|
||||
If NetBox was installed from a release package, then `/opt/netbox` will be a
|
||||
symlink pointing to the current version, and `/opt/netbox/.git` will not
|
||||
exist. If it was installed from git, then `/opt/netbox` and
|
||||
`/opt/netbox/.git` will both exist as normal directories.
|
||||
If NetBox was installed from a release package, then `/opt/netbox` will be a symlink pointing to the current version, and `/opt/netbox/.git` will not exist. If it was installed from git, then `/opt/netbox` and `/opt/netbox/.git` will both exist as normal directories.
|
||||
|
||||
### Option A: Download a Release
|
||||
|
||||
@ -84,20 +81,20 @@ If you followed the original installation guide to set up gunicorn, be sure to c
|
||||
sudo cp /opt/netbox-$OLDVER/gunicorn.py /opt/netbox/
|
||||
```
|
||||
|
||||
### Option B: Clone the Git Repository
|
||||
### Option B: Check Out a Git Release
|
||||
|
||||
This guide assumes that NetBox is installed at `/opt/netbox`. Pull down the most recent iteration of the master branch:
|
||||
This guide assumes that NetBox is installed at `/opt/netbox`. First, determine the latest release either by visiting our [releases page](https://github.com/netbox-community/netbox/releases) or by running the following `git` commands:
|
||||
|
||||
```no-highlight
|
||||
cd /opt/netbox
|
||||
sudo git checkout master
|
||||
sudo git pull origin master
|
||||
```
|
||||
sudo git fetch --tags
|
||||
git describe --tags $(git rev-list --tags --max-count=1)
|
||||
```
|
||||
|
||||
!!! info "Checking out an older release"
|
||||
If you need to upgrade to an older version rather than the current stable release, you can check out any valid [git tag](https://github.com/netbox-community/netbox/tags), each of which represents a release. For example, to checkout the code for NetBox v2.11.11, do:
|
||||
Check out the desired release by specifying its tag:
|
||||
|
||||
sudo git checkout v2.11.11
|
||||
```
|
||||
sudo git checkout v4.2.0
|
||||
```
|
||||
|
||||
## 4. Run the Upgrade Script
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
# GraphQL API Overview
|
||||
|
||||
NetBox provides a read-only [GraphQL](https://graphql.org/) API to complement its REST API. This API is powered by [Strawberry Django](https://strawberry-graphql.github.io/strawberry-django/).
|
||||
NetBox provides a read-only [GraphQL](https://graphql.org/) API to complement its REST API. This API is powered by [Strawberry Django](https://strawberry.rocks/).
|
||||
|
||||
## Queries
|
||||
|
||||
@ -47,7 +47,7 @@ NetBox provides both a singular and plural query field for each object type:
|
||||
|
||||
For example, query `device(id:123)` to fetch a specific device (identified by its unique ID), and query `device_list` (with an optional set of filters) to fetch all devices.
|
||||
|
||||
For more detail on constructing GraphQL queries, see the [GraphQL queries documentation](https://graphql.org/learn/queries/). For filtering and lookup syntax, please refer to the [Strawberry Django documentation](https://strawberry-graphql.github.io/strawberry-django/guide/filters/).
|
||||
For more detail on constructing GraphQL queries, see the [GraphQL queries documentation](https://graphql.org/learn/queries/). For filtering and lookup syntax, please refer to the [Strawberry Django documentation](https://strawberry.rocks/docs/django/guide/filters).
|
||||
|
||||
## Filtering
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
## What is a REST API?
|
||||
|
||||
REST stands for [representational state transfer](https://en.wikipedia.org/wiki/Representational_state_transfer). It's a particular type of API which employs HTTP requests and [JavaScript Object Notation (JSON)](https://www.json.org/) to facilitate create, retrieve, update, and delete (CRUD) operations on objects within an application. Each type of operation is associated with a particular HTTP verb:
|
||||
REST stands for [representational state transfer](https://en.wikipedia.org/wiki/REST). It's a particular type of API which employs HTTP requests and [JavaScript Object Notation (JSON)](https://www.json.org/) to facilitate create, retrieve, update, and delete (CRUD) operations on objects within an application. Each type of operation is associated with a particular HTTP verb:
|
||||
|
||||
* `GET`: Retrieve an object or list of objects
|
||||
* `POST`: Create an object
|
||||
|
@ -79,5 +79,5 @@ NetBox is built on the [Django](https://djangoproject.com/) Python framework and
|
||||
| HTTP service | nginx or Apache |
|
||||
| WSGI service | gunicorn or uWSGI |
|
||||
| Application | Django/Python |
|
||||
| Database | PostgreSQL 12+ |
|
||||
| Database | PostgreSQL 13+ |
|
||||
| Task queuing | Redis/django-rq |
|
||||
|
@ -36,6 +36,12 @@ The operational status of the circuit. By default, the following statuses are av
|
||||
!!! tip "Custom circuit statuses"
|
||||
Additional circuit statuses may be defined by setting `Circuit.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
|
||||
|
||||
### Distance
|
||||
|
||||
!!! info "This field was introduced in NetBox v4.2."
|
||||
|
||||
The distance between the circuit's two endpoints, including a unit designation (e.g. 100 meters or 25 feet).
|
||||
|
||||
### Description
|
||||
|
||||
A brief description of the circuit.
|
||||
|
@ -1,7 +1,5 @@
|
||||
# Circuit Groups
|
||||
|
||||
!!! info "This feature was introduced in NetBox v4.1."
|
||||
|
||||
[Circuits](./circuit.md) can be arranged into administrative groups for organization. The assignment of a circuit to a group is optional.
|
||||
|
||||
## Fields
|
||||
|
@ -8,9 +8,9 @@ Circuits can be assigned to [circuit groups](./circuitgroup.md) for correlation
|
||||
|
||||
The [circuit group](./circuitgroup.md) being assigned.
|
||||
|
||||
### Circuit
|
||||
### Member
|
||||
|
||||
The [circuit](./circuit.md) that is being assigned to the group.
|
||||
The [circuit](./circuit.md) or [virtual circuit](./virtualcircuit.md) assigned to the group.
|
||||
|
||||
### Priority
|
||||
|
||||
|
@ -21,13 +21,11 @@ Designates the termination as forming either the A or Z end of the circuit.
|
||||
|
||||
If selected, the circuit termination will be considered "connected" even if no cable has been connected to it in NetBox.
|
||||
|
||||
### Site
|
||||
### Termination
|
||||
|
||||
The [site](../dcim/site.md) with which this circuit termination is associated. Once created, a cable can be connected between the circuit termination and a device interface (or similar component).
|
||||
!!! info "This field replaced the `site` and `provider_network` fields in NetBox v4.2."
|
||||
|
||||
### Provider Network
|
||||
|
||||
Circuits which do not connect to a site modeled by NetBox can instead be terminated to a [provider network](./providernetwork.md) representing an unknown network operated by a [provider](./provider.md).
|
||||
The [region](../dcim/region.md), [site group](../dcim/sitegroup.md), [site](../dcim/site.md), [location](../dcim/location.md) or [provider network](./providernetwork.md) with which this circuit termination is associated. Once created, a cable can be connected between the circuit termination and a device interface (or similar component).
|
||||
|
||||
### Port Speed
|
||||
|
||||
|
39
docs/models/circuits/virtualcircuit.md
Normal file
39
docs/models/circuits/virtualcircuit.md
Normal file
@ -0,0 +1,39 @@
|
||||
# Virtual Circuits
|
||||
|
||||
!!! info "This feature was introduced in NetBox v4.2."
|
||||
|
||||
A virtual circuit can connect two or more interfaces atop a set of decoupled physical connections. For example, it's very common to form a virtual connection between two virtual interfaces, each of which is bound to a physical interface on its respective device and physically connected to a [provider network](./providernetwork.md) via an independent [physical circuit](./circuit.md).
|
||||
|
||||
## Fields
|
||||
|
||||
### Provider Network
|
||||
|
||||
The [provider network](./providernetwork.md) across which the virtual circuit is formed.
|
||||
|
||||
### Provider Account
|
||||
|
||||
The [provider account](./provideraccount.md) with which the virtual circuit is associated (if any).
|
||||
|
||||
### Circuit ID
|
||||
|
||||
The unique identifier assigned to the virtual circuit by its [provider](./provider.md).
|
||||
|
||||
### Type
|
||||
|
||||
The assigned [virtual circuit type](./virtualcircuittype.md).
|
||||
|
||||
### Status
|
||||
|
||||
The operational status of the virtual circuit. By default, the following statuses are available:
|
||||
|
||||
| Name |
|
||||
|----------------|
|
||||
| Planned |
|
||||
| Provisioning |
|
||||
| Active |
|
||||
| Offline |
|
||||
| Deprovisioning |
|
||||
| Decommissioned |
|
||||
|
||||
!!! tip "Custom circuit statuses"
|
||||
Additional circuit statuses may be defined by setting `Circuit.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
|
23
docs/models/circuits/virtualcircuittermination.md
Normal file
23
docs/models/circuits/virtualcircuittermination.md
Normal file
@ -0,0 +1,23 @@
|
||||
# Virtual Circuit Terminations
|
||||
|
||||
!!! info "This feature was introduced in NetBox v4.2."
|
||||
|
||||
This model represents the connection of a virtual [interface](../dcim/interface.md) to a [virtual circuit](./virtualcircuit.md).
|
||||
|
||||
## Fields
|
||||
|
||||
### Virtual Circuit
|
||||
|
||||
The [virtual circuit](./virtualcircuit.md) to which the interface is connected.
|
||||
|
||||
### Interface
|
||||
|
||||
The [interface](../dcim/interface.md) connected to the virtual circuit.
|
||||
|
||||
### Role
|
||||
|
||||
The functional role of the termination. This depends on the virtual circuit's topology, which is typically either peer-to-peer or hub-and-spoke (multipoint). Valid choices include:
|
||||
|
||||
* Peer
|
||||
* Hub
|
||||
* Spoke
|
13
docs/models/circuits/virtualcircuittype.md
Normal file
13
docs/models/circuits/virtualcircuittype.md
Normal file
@ -0,0 +1,13 @@
|
||||
# Virtual Circuit Types
|
||||
|
||||
Like physical [circuits](./circuit.md), [virtual circuits](./virtualcircuit.md) are classified by functional type. These types are completely customizable, and can help categorize circuits by function or technology.
|
||||
|
||||
## Fields
|
||||
|
||||
### Name
|
||||
|
||||
A unique human-friendly name.
|
||||
|
||||
### Slug
|
||||
|
||||
A unique URL-friendly identifier. (This value can be used for filtering.)
|
@ -45,9 +45,12 @@ The operation duplex (full, half, or auto).
|
||||
|
||||
The [virtual routing and forwarding](../ipam/vrf.md) instance to which this interface is assigned.
|
||||
|
||||
### MAC Address
|
||||
### Primary MAC Address
|
||||
|
||||
The 48-bit MAC address (for Ethernet interfaces).
|
||||
The [MAC address](./macaddress.md) assigned to this interface which is designated as its primary.
|
||||
|
||||
!!! note "Changed in NetBox v4.2"
|
||||
The MAC address of an interface (formerly a concrete database field) is available as a property, `mac_address`, which reflects the value of the primary linked [MAC address](./macaddress.md) object.
|
||||
|
||||
### WWN
|
||||
|
||||
@ -109,6 +112,7 @@ For switched Ethernet interfaces, this identifies the 802.1Q encapsulation strat
|
||||
* **Access:** All traffic is assigned to a single VLAN, with no tagging.
|
||||
* **Tagged:** One untagged "native" VLAN is allowed, as well as any number of tagged VLANs.
|
||||
* **Tagged (all):** Implies that all VLANs are carried by the interface. One untagged VLAN may be designated.
|
||||
* **Q-in-Q:** Q-in-Q (IEEE 802.1ad) encapsulation is performed using the assigned SVLAN.
|
||||
|
||||
This field must be left blank for routed interfaces which do employ 802.1Q encapsulation.
|
||||
|
||||
@ -120,6 +124,12 @@ The "native" (untagged) VLAN for the interface. Valid only when one of the above
|
||||
|
||||
The tagged VLANs which are configured to be carried by this interface. Valid only for the "tagged" 802.1Q mode above.
|
||||
|
||||
### Q-in-Q SVLAN
|
||||
|
||||
!!! info "This field was introduced in NetBox v4.2."
|
||||
|
||||
The assigned service VLAN (for Q-in-Q/802.1ad interfaces).
|
||||
|
||||
### Wireless Role
|
||||
|
||||
Indicates the configured role for wireless interfaces (access point or station).
|
||||
@ -142,3 +152,9 @@ The configured channel width of a wireless interface, in MHz. This is typically
|
||||
### Wireless LANs
|
||||
|
||||
The [wireless LANs](../wireless/wirelesslan.md) for which this interface carries traffic. (Valid for wireless interfaces only.)
|
||||
|
||||
### VLAN Translation Policy
|
||||
|
||||
!!! info "This field was introduced in NetBox v4.2."
|
||||
|
||||
The [VLAN translation policy](../ipam/vlantranslationpolicy.md) that applies to this interface (optional).
|
||||
|
@ -25,6 +25,12 @@ The inventory item's name. If the inventory item is assigned to a parent item, i
|
||||
|
||||
An alternative physical label identifying the inventory item.
|
||||
|
||||
### Status
|
||||
|
||||
!!! info "This field was introduced in NetBox v4.2."
|
||||
|
||||
The inventory item's operational status.
|
||||
|
||||
### Role
|
||||
|
||||
The functional [role](./inventoryitemrole.md) assigned to this inventory item.
|
||||
|
13
docs/models/dcim/macaddress.md
Normal file
13
docs/models/dcim/macaddress.md
Normal file
@ -0,0 +1,13 @@
|
||||
# MAC Addresses
|
||||
|
||||
!!! info "This feature was introduced in NetBox v4.2."
|
||||
|
||||
A MAC address object in NetBox comprises a single Ethernet link layer address, and represents a MAC address as reported by or assigned to a network interface. MAC addresses can be assigned to [device](../dcim/device.md) and [virtual machine](../virtualization/virtualmachine.md) interfaces. A MAC address can be specified as the primary MAC address for a given device or VM interface.
|
||||
|
||||
Most interfaces have only a single MAC address, hard-coded at the factory. However, on some devices (particularly virtual interfaces) it is possible to assign additional MAC addresses or change existing ones. For this reason NetBox allows multiple MACAddress objects to be assigned to a single interface.
|
||||
|
||||
## Fields
|
||||
|
||||
### MAC Address
|
||||
|
||||
The 48-bit MAC address, in colon-hexadecimal notation (e.g. `aa:bb:cc:11:22:33`).
|
@ -16,8 +16,6 @@ The device to which this module bay belongs.
|
||||
|
||||
### Module
|
||||
|
||||
!!! info "This feature was introduced in NetBox v4.1."
|
||||
|
||||
The module to which this bay belongs (optional).
|
||||
|
||||
### Name
|
||||
|
@ -42,6 +42,4 @@ The numeric weight of the module, including a unit designation (e.g. 3 kilograms
|
||||
|
||||
### Airflow
|
||||
|
||||
!!! info "The `airflow` field was introduced in NetBox v4.1."
|
||||
|
||||
The direction in which air circulates through the device chassis for cooling.
|
||||
|
@ -29,6 +29,12 @@ An alternative physical label identifying the power outlet.
|
||||
|
||||
The type of power outlet.
|
||||
|
||||
### Color
|
||||
|
||||
!!! info "This field was introduced in NetBox v4.2."
|
||||
|
||||
The power outlet's color (optional).
|
||||
|
||||
### Power Port
|
||||
|
||||
When modeling a device which redistributes power from an upstream supply, such as a power distribution unit (PDU), each power outlet should be mapped to the respective [power port](./powerport.md) on the device which supplies power. For example, a 24-outlet PDU may two power ports, each distributing power to 12 of its outlets.
|
||||
|
@ -1,7 +1,5 @@
|
||||
# Rack Types
|
||||
|
||||
!!! info "This feature was introduced in NetBox v4.1."
|
||||
|
||||
A rack type defines the physical characteristics of a particular model of [rack](./rack.md).
|
||||
|
||||
## Fields
|
||||
|
@ -1,5 +1,8 @@
|
||||
# Branches
|
||||
|
||||
!!! danger "Deprecated Feature"
|
||||
This feature has been deprecated in NetBox v4.2 and will be removed in a future release. Please consider using the [netbox-branching plugin](https://github.com/netboxlabs/netbox-branching), which provides much more robust functionality.
|
||||
|
||||
A branch is a collection of related [staged changes](./stagedchange.md) that have been prepared for merging into the active database. A branch can be merged by executing its `commit()` method. Deleting a branch will delete all its related changes.
|
||||
|
||||
## Fields
|
||||
|
@ -44,8 +44,6 @@ For object and multiple-object fields only. Designates the type of NetBox object
|
||||
|
||||
### Related Object Filter
|
||||
|
||||
!!! info "This field was introduced in NetBox v4.1."
|
||||
|
||||
For object and multi-object custom fields, a filter may be defined to limit the available objects when populating a field value. This filter maps object attributes to values. For example, `{"status": "active"}` will include only objects with a status of "active."
|
||||
|
||||
!!! warning
|
||||
|
@ -10,7 +10,7 @@ See the [event rules documentation](../../features/event-rules.md) for more inf
|
||||
|
||||
A unique human-friendly name.
|
||||
|
||||
### Content Types
|
||||
### Object Types
|
||||
|
||||
The type(s) of object in NetBox that will trigger the rule.
|
||||
|
||||
@ -38,3 +38,15 @@ The event types which will trigger the rule. At least one event type must be sel
|
||||
### Conditions
|
||||
|
||||
A set of [prescribed conditions](../../reference/conditions.md) against which the triggering object will be evaluated. If the conditions are defined but not met by the object, no action will be taken. An event rule that does not define any conditions will _always_ trigger.
|
||||
|
||||
### Action Type
|
||||
|
||||
The type of action to take when the rule triggers. This must be one of the following choices:
|
||||
|
||||
* Webhook
|
||||
* Custom script
|
||||
* Notification
|
||||
|
||||
### Action Data
|
||||
|
||||
An optional dictionary of JSON data to pass when executing the rule. This can be useful to include additional context data, e.g. when transmitting a webhook.
|
||||
|
@ -1,5 +1,8 @@
|
||||
# Staged Changes
|
||||
|
||||
!!! danger "Deprecated Feature"
|
||||
This feature has been deprecated in NetBox v4.2 and will be removed in a future release. Please consider using the [netbox-branching plugin](https://github.com/netboxlabs/netbox-branching), which provides much more robust functionality.
|
||||
|
||||
A staged change represents the creation of a new object or the modification or deletion of an existing object to be performed at some future point. Each change must be assigned to a [branch](./branch.md).
|
||||
|
||||
Changes can be applied individually via the `apply()` method, however it is recommended to apply changes in bulk using the parent branch's `commit()` method.
|
||||
|
@ -34,9 +34,11 @@ Designates whether the prefix should be treated as a pool. If selected, the firs
|
||||
|
||||
If selected, this prefix will report 100% utilization regardless of how many child objects have been defined within it.
|
||||
|
||||
### Site
|
||||
### Scope
|
||||
|
||||
The [site](../dcim/site.md) to which this prefix is assigned (optional).
|
||||
!!! info "This field replaced the `site` field in NetBox v4.2."
|
||||
|
||||
The [region](../dcim/region.md), [site](../dcim/site.md), [site group](../dcim/sitegroup.md) or [location](../dcim/location.md) to which the prefix is assigned (optional).
|
||||
|
||||
### VLAN
|
||||
|
||||
|
@ -26,3 +26,15 @@ The user-defined functional [role](./role.md) assigned to the VLAN.
|
||||
### VLAN Group or Site
|
||||
|
||||
The [VLAN group](./vlangroup.md) or [site](../dcim/site.md) to which the VLAN is assigned.
|
||||
|
||||
### Q-in-Q Role
|
||||
|
||||
!!! info "This field was introduced in NetBox v4.2."
|
||||
|
||||
For VLANs which comprise a Q-in-Q/IEEE 802.1ad topology, this field indicates whether the VLAN is treated as a service or customer VLAN.
|
||||
|
||||
### Q-in-Q Service VLAN
|
||||
|
||||
!!! info "This field was introduced in NetBox v4.2."
|
||||
|
||||
The designated parent service VLAN for a Q-in-Q customer VLAN. This may be set only for Q-in-Q custom VLANs.
|
||||
|
@ -16,8 +16,6 @@ A unique URL-friendly identifier. (This value can be used for filtering.)
|
||||
|
||||
### VLAN ID Ranges
|
||||
|
||||
!!! info "This field replaced the legacy `min_vid` and `max_vid` fields in NetBox v4.1."
|
||||
|
||||
The set of VLAN IDs which are encompassed by the group. By default, this will be the entire range of valid IEEE 802.1Q VLAN IDs (1 to 4094, inclusive). VLANs created within a group must have a VID that falls within one of these ranges. Ranges may not overlap.
|
||||
|
||||
### Scope
|
||||
|
28
docs/models/ipam/vlantranslationpolicy.md
Normal file
28
docs/models/ipam/vlantranslationpolicy.md
Normal file
@ -0,0 +1,28 @@
|
||||
# VLAN Translation Policies
|
||||
|
||||
!!! info "This feature was introduced in NetBox v4.2."
|
||||
|
||||
VLAN translation is a feature that consists of VLAN translation policies and [VLAN translation rules](./vlantranslationrule.md). Many rules can belong to a policy, and each rule defines a mapping of a local to remote VLAN ID (VID). A policy can then be assigned to an [Interface](../dcim/interface.md) or [VMInterface](../virtualization/vminterface.md), and all VLAN translation rules associated with that policy will be visible in the interface details.
|
||||
|
||||
There are uniqueness constraints on `(policy, local_vid)` and on `(policy, remote_vid)` in the `VLANTranslationRule` model. Thus, you cannot have multiple rules linked to the same policy that have the same local VID or the same remote VID. A set of policies and rules might look like this:
|
||||
|
||||
Policy 1:
|
||||
- Rule: 100 -> 200
|
||||
- Rule: 101 -> 201
|
||||
|
||||
Policy 2:
|
||||
- Rule: 100 -> 300
|
||||
- Rule: 101 -> 301
|
||||
|
||||
However this is not allowed:
|
||||
|
||||
Policy 3:
|
||||
- Rule: 100 -> 200
|
||||
- Rule: 100 -> 300
|
||||
|
||||
|
||||
## Fields
|
||||
|
||||
### Name
|
||||
|
||||
A unique human-friendly name.
|
21
docs/models/ipam/vlantranslationrule.md
Normal file
21
docs/models/ipam/vlantranslationrule.md
Normal file
@ -0,0 +1,21 @@
|
||||
# VLAN Translation Rules
|
||||
|
||||
!!! info "This feature was introduced in NetBox v4.2."
|
||||
|
||||
A VLAN translation rule represents a one-to-one mapping of a local VLAN ID (VID) to a remote VID. Many rules can belong to a single policy.
|
||||
|
||||
See [VLAN translation policies](./vlantranslationpolicy.md) for an overview of the VLAN Translation feature.
|
||||
|
||||
## Fields
|
||||
|
||||
### Policy
|
||||
|
||||
The [VLAN Translation Policy](./vlantranslationpolicy.md) to which this rule belongs.
|
||||
|
||||
### Local VID
|
||||
|
||||
VLAN ID (1-4094) in the local network which is to be translated to a remote VID.
|
||||
|
||||
### Remote VID
|
||||
|
||||
VLAN ID (1-4094) in the remote network to which the local VID will be translated.
|
@ -23,6 +23,8 @@ The cluster's operational status.
|
||||
!!! tip
|
||||
Additional statuses may be defined by setting `Cluster.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
|
||||
|
||||
### Site
|
||||
### Scope
|
||||
|
||||
The [site](../dcim/site.md) with which the cluster is associated.
|
||||
!!! info "This field replaced the `site` field in NetBox v4.2."
|
||||
|
||||
The [region](../dcim/region.md), [site](../dcim/site.md), [site group](../dcim/sitegroup.md) or [location](../dcim/location.md) with which this cluster is associated.
|
||||
|
@ -57,6 +57,4 @@ The amount of disk storage provisioned, in megabytes.
|
||||
|
||||
### Serial Number
|
||||
|
||||
!!! info "This field was introduced in NetBox v4.1."
|
||||
|
||||
Optional serial number assigned to this virtual machine. Unlike devices, uniqueness is not enforced for virtual machine serial numbers.
|
||||
|
@ -27,9 +27,12 @@ An interface on the same VM with which this interface is bridged.
|
||||
|
||||
If not selected, this interface will be treated as disabled/inoperative.
|
||||
|
||||
### MAC Address
|
||||
### Primary MAC Address
|
||||
|
||||
The 48-bit MAC address (for Ethernet interfaces).
|
||||
The [MAC address](../dcim/macaddress.md) assigned to this interface which is designated as its primary.
|
||||
|
||||
!!! note "Changed in NetBox v4.2"
|
||||
The MAC address of an interface (formerly a concrete database field) is available as a property, `mac_address`, which reflects the value of the primary linked [MAC address](../dcim/macaddress.md) object.
|
||||
|
||||
### MTU
|
||||
|
||||
@ -42,6 +45,7 @@ For switched Ethernet interfaces, this identifies the 802.1Q encapsulation strat
|
||||
* **Access:** All traffic is assigned to a single VLAN, with no tagging.
|
||||
* **Tagged:** One untagged "native" VLAN is allowed, as well as any number of tagged VLANs.
|
||||
* **Tagged (all):** Implies that all VLANs are carried by the interface. One untagged VLAN may be designated.
|
||||
* **Q-in-Q:** Q-in-Q (IEEE 802.1ad) encapsulation is performed using the assigned SVLAN.
|
||||
|
||||
This field must be left blank for routed interfaces which do employ 802.1Q encapsulation.
|
||||
|
||||
@ -53,6 +57,18 @@ The "native" (untagged) VLAN for the interface. Valid only when one of the above
|
||||
|
||||
The tagged VLANs which are configured to be carried by this interface. Valid only for the "tagged" 802.1Q mode above.
|
||||
|
||||
### Q-in-Q SVLAN
|
||||
|
||||
!!! info "This field was introduced in NetBox v4.2."
|
||||
|
||||
The assigned service VLAN (for Q-in-Q/802.1ad interfaces).
|
||||
|
||||
### VRF
|
||||
|
||||
The [virtual routing and forwarding](../ipam/vrf.md) instance to which this interface is assigned.
|
||||
|
||||
### VLAN Translation Policy
|
||||
|
||||
!!! info "This field was introduced in NetBox v4.2."
|
||||
|
||||
The [VLAN translation policy](../ipam/vlantranslationpolicy.md) that applies to this interface (optional).
|
||||
|
@ -43,3 +43,9 @@ The security cipher used to apply wireless authentication. Options include:
|
||||
### Pre-Shared Key
|
||||
|
||||
The security key configured on each client to grant access to the secured wireless LAN. This applies only to certain authentication types.
|
||||
|
||||
### Scope
|
||||
|
||||
!!! info "This field was introduced in NetBox v4.2."
|
||||
|
||||
The [region](../dcim/region.md), [site](../dcim/site.md), [site group](../dcim/sitegroup.md) or [location](../dcim/location.md) with which this wireless LAN is associated.
|
||||
|
@ -22,8 +22,6 @@ The service set identifier (SSID) for the wireless link (optional).
|
||||
|
||||
### Distance
|
||||
|
||||
!!! info "This field was introduced in NetBox v4.1."
|
||||
|
||||
The distance between the link's two endpoints, including a unit designation (e.g. 100 meters or 25 feet).
|
||||
|
||||
### Authentication Type
|
||||
|
@ -1,7 +1,5 @@
|
||||
# Background Jobs
|
||||
|
||||
!!! info "This feature was introduced in NetBox v4.1."
|
||||
|
||||
NetBox plugins can defer certain operations by enqueuing [background jobs](../../features/background-jobs.md), which are executed asynchronously by background workers. This is helpful for decoupling long-running processes from the user-facing request-response cycle.
|
||||
|
||||
For example, your plugin might need to fetch data from a remote system. Depending on the amount of data and the responsiveness of the remote server, this could take a few minutes. Deferring this task to a queued job ensures that it can be completed in the background, without interrupting the user. The data it fetches can be made available once the job has completed.
|
||||
@ -29,6 +27,9 @@ class MyTestJob(JobRunner):
|
||||
|
||||
You can schedule the background job from within your code (e.g. from a model's `save()` method or a view) by calling `MyTestJob.enqueue()`. This method passes through all arguments to `Job.enqueue()`. However, no `name` argument must be passed, as the background job name will be used instead.
|
||||
|
||||
!!! tip
|
||||
A set of predefined intervals is available at `core.choices.JobIntervalChoices` for convenience.
|
||||
|
||||
### Attributes
|
||||
|
||||
`JobRunner` attributes are defined under a class named `Meta` within the job. These are optional, but encouraged.
|
||||
@ -46,26 +47,57 @@ As described above, jobs can be scheduled for immediate execution or at any late
|
||||
|
||||
#### Example
|
||||
|
||||
```python title="models.py"
|
||||
from django.db import models
|
||||
from core.choices import JobIntervalChoices
|
||||
from netbox.models import NetBoxModel
|
||||
from .jobs import MyTestJob
|
||||
|
||||
class MyModel(NetBoxModel):
|
||||
foo = models.CharField()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
MyTestJob.enqueue_once(instance=self, interval=JobIntervalChoices.INTERVAL_HOURLY)
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def sync(self):
|
||||
MyTestJob.enqueue(instance=self)
|
||||
```
|
||||
|
||||
|
||||
### System Jobs
|
||||
|
||||
!!! info "This feature was introduced in NetBox v4.2."
|
||||
|
||||
Some plugins may implement background jobs that are decoupled from the request/response cycle. Typical use cases would be housekeeping tasks or synchronization jobs. These can be registered as _system jobs_ using the `system_job()` decorator. The job interval must be passed as an integer (in minutes) when registering a system job. System jobs are scheduled automatically when the RQ worker (`manage.py rqworker`) is run.
|
||||
|
||||
#### Example
|
||||
|
||||
```python title="jobs.py"
|
||||
from netbox.jobs import JobRunner
|
||||
|
||||
from core.choices import JobIntervalChoices
|
||||
from netbox.jobs import JobRunner, system_job
|
||||
from .models import MyModel
|
||||
|
||||
# Specify a predefined choice or an integer indicating
|
||||
# the number of minutes between job executions
|
||||
@system_job(interval=JobIntervalChoices.INTERVAL_HOURLY)
|
||||
class MyHousekeepingJob(JobRunner):
|
||||
class Meta:
|
||||
name = "Housekeeping"
|
||||
name = "My Housekeeping Job"
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
# your logic goes here
|
||||
MyModel.objects.filter(foo='bar').delete()
|
||||
```
|
||||
|
||||
```python title="__init__.py"
|
||||
from netbox.plugins import PluginConfig
|
||||
!!! note
|
||||
Ensure that any system jobs are imported on initialization. Otherwise, they won't be registered. This can be achieved by extending the PluginConfig's `ready()` method. For example:
|
||||
|
||||
class MyPluginConfig(PluginConfig):
|
||||
```python
|
||||
def ready(self):
|
||||
super().ready()
|
||||
|
||||
from .jobs import MyHousekeepingJob
|
||||
MyHousekeepingJob.setup(interval=60)
|
||||
```
|
||||
```
|
||||
|
||||
## Task queues
|
||||
|
||||
|
@ -18,6 +18,6 @@ backends = [MyDataBackend]
|
||||
```
|
||||
|
||||
!!! tip
|
||||
The path to the list of search indexes can be modified by setting `data_backends` in the PluginConfig instance.
|
||||
The path to the list of data backends can be modified by setting `data_backends` in the PluginConfig instance.
|
||||
|
||||
::: netbox.data_backends.DataBackend
|
||||
|
@ -1,7 +1,5 @@
|
||||
# Event Types
|
||||
|
||||
!!! info "This feature was introduced in NetBox v4.1."
|
||||
|
||||
Plugins can register their own custom event types for use with NetBox [event rules](../../models/extras/eventrule.md). This is accomplished by calling the `register()` method on an instance of the `EventType` class. This can be done anywhere within the plugin. An example is provided below.
|
||||
|
||||
```python
|
||||
|
@ -99,7 +99,7 @@ NetBox looks for the `config` variable within a plugin's `__init__.py` to load i
|
||||
### PluginConfig Attributes
|
||||
|
||||
| Name | Description |
|
||||
|-----------------------|--------------------------------------------------------------------------------------------------------------------------|
|
||||
|-----------------------|------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `name` | Raw plugin name; same as the plugin's source directory |
|
||||
| `verbose_name` | Human-friendly name for the plugin |
|
||||
| `version` | Current release ([semantic versioning](https://semver.org/) is encouraged) |
|
||||
@ -114,6 +114,7 @@ NetBox looks for the `config` variable within a plugin's `__init__.py` to load i
|
||||
| `max_version` | Maximum version of NetBox with which the plugin is compatible |
|
||||
| `middleware` | A list of middleware classes to append after NetBox's build-in middleware |
|
||||
| `queues` | A list of custom background task queues to create |
|
||||
| `events_pipeline` | A list of handlers to add to [`EVENTS_PIPELINE`](../../configuration/miscellaneous.md#events_pipeline), identified by dotted paths |
|
||||
| `search_extensions` | The dotted path to the list of search index classes (default: `search.indexes`) |
|
||||
| `data_backends` | The dotted path to the list of data source backend classes (default: `data_backends.backends`) |
|
||||
| `template_extensions` | The dotted path to the list of template extension classes (default: `template_content.template_extensions`) |
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Staged Changes
|
||||
|
||||
!!! danger "Experimental Feature"
|
||||
This feature is still under active development and considered experimental in nature. Its use in production is strongly discouraged at this time.
|
||||
!!! danger "Deprecated Feature"
|
||||
This feature has been deprecated in NetBox v4.2 and will be removed in a future release. Please consider using the [netbox-branching plugin](https://github.com/netboxlabs/netbox-branching), which provides much more robust functionality.
|
||||
|
||||
NetBox provides a programmatic API to stage the creation, modification, and deletion of objects without actually committing those changes to the active database. This can be useful for performing a "dry run" of bulk operations, or preparing a set of changes for administrative approval, for example.
|
||||
|
||||
|
@ -185,6 +185,9 @@ class MyView(generic.ObjectView):
|
||||
)
|
||||
```
|
||||
|
||||
!!! note "Changed in NetBox v4.2"
|
||||
The `register_model_view()` function was extended in NetBox v4.2 to support registration of list views by passing `detail=False`.
|
||||
|
||||
::: utilities.views.register_model_view
|
||||
|
||||
::: utilities.views.ViewTab
|
||||
@ -203,8 +206,6 @@ Plugins can inject custom content into certain areas of core NetBox views. This
|
||||
| `right_page()` | Object view | Inject content on the right side of the page |
|
||||
| `full_width_page()` | Object view | Inject content across the entire bottom of the page |
|
||||
|
||||
!!! info "The `navbar()` and `alerts()` methods were introduced in NetBox v4.1."
|
||||
|
||||
Additionally, a `render()` method is available for convenience. This method accepts the name of a template to render, and any additional context data you want to pass. Its use is optional, however.
|
||||
|
||||
To control where the custom content is injected, plugin authors can specify an iterable of models by overriding the `models` attribute on the subclass. Extensions which do not specify a set of models will be invoked on every view, where supported.
|
||||
|
@ -10,6 +10,14 @@ Minor releases are published in April, August, and December of each calendar yea
|
||||
|
||||
This page contains a history of all major and minor releases since NetBox v2.0. For more detail on a specific patch release, please see the release notes page for that specific minor release.
|
||||
|
||||
#### [Version 4.2](./version-4.2.md) (January 2025)
|
||||
|
||||
* Assign Multiple MAC Addresses per Interface ([#4867](https://github.com/netbox-community/netbox/issues/4867))
|
||||
* Quick Add UI Widget ([#5858](https://github.com/netbox-community/netbox/issues/5858))
|
||||
* VLAN Translation ([#7336](https://github.com/netbox-community/netbox/issues/7336))
|
||||
* Virtual Circuits ([#13086](https://github.com/netbox-community/netbox/issues/13086))
|
||||
* Q-in-Q Encapsulation ([#13428](https://github.com/netbox-community/netbox/issues/13428))
|
||||
|
||||
#### [Version 4.1](./version-4.1.md) (September 2024)
|
||||
|
||||
* Circuit Groups ([#7025](https://github.com/netbox-community/netbox/issues/7025))
|
||||
|
@ -1,5 +1,44 @@
|
||||
# NetBox v4.1
|
||||
|
||||
## v4.1.11 (2025-01-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#17771](https://github.com/netbox-community/netbox/issues/17771) - Fix duplicate entries appearing on VLAN list when filtering by interface assignment
|
||||
* [#18222](https://github.com/netbox-community/netbox/issues/18222) - Pass event rule action data to webhooks as context data
|
||||
* [#18263](https://github.com/netbox-community/netbox/issues/18263) - Fix recalculation of cable paths when modifying cable terminations via the REST API
|
||||
* [#18271](https://github.com/netbox-community/netbox/issues/18271) - Require only encryption _or_ authentication algorithm when creating an IPSec proposal via the REST API
|
||||
* [#18289](https://github.com/netbox-community/netbox/issues/18289) - Enable ordering modules and module types by created & last updated times
|
||||
|
||||
---
|
||||
|
||||
## v4.1.10 (2024-12-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#18260](https://github.com/netbox-community/netbox/issues/18260) - Fix object change logging
|
||||
|
||||
---
|
||||
|
||||
## v4.1.9 (2024-12-17)
|
||||
|
||||
!!! danger "Do Not Use"
|
||||
This release contains a regression which breaks change logging. Please use release v4.1.10 instead.
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#17215](https://github.com/netbox-community/netbox/issues/17215) - Change the highlighted color of disabled interfaces in interface lists
|
||||
* [#18224](https://github.com/netbox-community/netbox/issues/18224) - Apply all registered request processors when running custom scripts
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#16757](https://github.com/netbox-community/netbox/issues/16757) - Fix rendering of IP addresses table when assigning an existing IP address to an interface with global HTMX navigation enabled
|
||||
* [#17868](https://github.com/netbox-community/netbox/issues/17868) - Fix `ZeroDivisionError` exception under specific circumstances when generating a cable trace
|
||||
* [#18124](https://github.com/netbox-community/netbox/issues/18124) - Enable referencing cable attributes when querying a `cabletermination_set` via the GraphQL API
|
||||
* [#18230](https://github.com/netbox-community/netbox/issues/18230) - Fix `AttributeError` exception when attempting to edit an IP address assigned to a virtual machine interface
|
||||
|
||||
---
|
||||
|
||||
## v4.1.8 (2024-12-12)
|
||||
|
||||
### Enhancements
|
||||
|
204
docs/release-notes/version-4.2.md
Normal file
204
docs/release-notes/version-4.2.md
Normal file
@ -0,0 +1,204 @@
|
||||
# NetBox v4.2
|
||||
|
||||
## v4.2.4 (2025-02-21)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#17309](https://github.com/netbox-community/netbox/issues/17309) - Omit empty counts in related object tables
|
||||
* [#18277](https://github.com/netbox-community/netbox/issues/18277) - Improve multi-table inheritance in serialization of change-logged models
|
||||
* [#18286](https://github.com/netbox-community/netbox/issues/18286) - Add more job duration choices
|
||||
* [#18357](https://github.com/netbox-community/netbox/issues/18357) - Display author name in plugin list for locally installed plugins
|
||||
* [#18408](https://github.com/netbox-community/netbox/issues/18408) - Add Paused status for virtual machines
|
||||
* [#18584](https://github.com/netbox-community/netbox/issues/18584) - Add rack type column to manufacturer list
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#17436](https://github.com/netbox-community/netbox/issues/17436) - Fix {module} replacement in module bays
|
||||
* [#18013](https://github.com/netbox-community/netbox/issues/18013) - Limit object type to selected object in change log filter
|
||||
* [#18241](https://github.com/netbox-community/netbox/issues/18241) - Default logging level of custom scripts changed to INFO
|
||||
* [#18247](https://github.com/netbox-community/netbox/issues/18247) - Fix visibility of disabled cable paths in dark mode
|
||||
* [#18480](https://github.com/netbox-community/netbox/issues/18480) - Clean data passed to script in runscript command
|
||||
* [#18555](https://github.com/netbox-community/netbox/issues/18555) - Add default get_absolute_url method to plugin models
|
||||
* [#18585](https://github.com/netbox-community/netbox/issues/18585) - Fix filtering circuits by location
|
||||
* [#18593](https://github.com/netbox-community/netbox/issues/18593) - Fix "Create & Add Another" IP Address workflow
|
||||
* [#18594](https://github.com/netbox-community/netbox/issues/18594) - Enable sorting by ASN count on site and provider lists
|
||||
* [#18619](https://github.com/netbox-community/netbox/issues/18619) - Ensure shift-click selection selects only visible list items
|
||||
* [#18674](https://github.com/netbox-community/netbox/issues/18674) - Preserve form values when selecting speed on circuit termination
|
||||
|
||||
---
|
||||
|
||||
## v4.2.3 (2025-02-04)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#18518](https://github.com/netbox-community/netbox/issues/18518) - Add a "hostname" `<meta>` tag to the page header
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#18497](https://github.com/netbox-community/netbox/issues/18497) - Fix unhandled `FieldDoesNotExist` exception when search results include virtual circuit
|
||||
* [#18433](https://github.com/netbox-community/netbox/issues/18433) - Fix MAC address not shown as "primary for interface" in MAC address detail view
|
||||
* [#18154](https://github.com/netbox-community/netbox/issues/18154) - Allow anonymous users to change default table preferences
|
||||
* [#18515](https://github.com/netbox-community/netbox/issues/18515) - Fix Django `collectstatic` management command in debug mode with Redis not running
|
||||
* [#18456](https://github.com/netbox-community/netbox/issues/18456) - Avoid duplicate MAC Address column in interface tables
|
||||
* [#18447](https://github.com/netbox-community/netbox/issues/18447) - Fix `FieldError` exception when sorting interface tables on MAC Address columns
|
||||
* [#18438](https://github.com/netbox-community/netbox/issues/18438) - Improve performance in IPAM migration `0072_prefix_cached_relations` when upgrading from v4.1 or earlier
|
||||
* [#18436](https://github.com/netbox-community/netbox/issues/18436) - Reset primary MAC address when unassigning MAC address from interface
|
||||
* [#18181](https://github.com/netbox-community/netbox/issues/18181) - Fix "Create & Add Another" workflow when adding IP addresses to interfaces
|
||||
|
||||
---
|
||||
|
||||
## v4.2.2 (2025-01-17)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#18336](https://github.com/netbox-community/netbox/issues/18336) - Validate new rack height against installed devices when changing a rack's type
|
||||
* [#18350](https://github.com/netbox-community/netbox/issues/18350) - Fix `FieldDoesNotExist` exception when global search results include a circuit termination
|
||||
* [#18353](https://github.com/netbox-community/netbox/issues/18353) - Disable fetching of plugin catalog data when `ISOLATED_DEPLOYMENT` is enabled
|
||||
* [#18362](https://github.com/netbox-community/netbox/issues/18362) - Avoid transmitting census data on every worker restart
|
||||
* [#18363](https://github.com/netbox-community/netbox/issues/18363) - Fix support for assigning a MAC address to an interface via the REST API
|
||||
* [#18368](https://github.com/netbox-community/netbox/issues/18368) - Restore missing attributes from REST API serializer for MAC addresses (`tags`, `created`, `last_updated`, and custom fields)
|
||||
* [#18369](https://github.com/netbox-community/netbox/issues/18369) - Fix `TypeError` exception when rendering the system configuration view with one or more custom classes defined under `PROTECTION_RULES`
|
||||
* [#18373](https://github.com/netbox-community/netbox/issues/18373) - Fix `AttributeError` exception when attempting to assign host devices to a cluster
|
||||
* [#18376](https://github.com/netbox-community/netbox/issues/18376) - Fix the display of tagged VLANs in interfaces list for Q-in-Q interfaces
|
||||
* [#18379](https://github.com/netbox-community/netbox/issues/18379) - Ensure RSS feed dashboard widget content is sanitized
|
||||
* [#18392](https://github.com/netbox-community/netbox/issues/18392) - Virtual machines should not inherit config contexts assigned to locations
|
||||
* [#18400](https://github.com/netbox-community/netbox/issues/18400) - Fix support for `STORAGE_BACKEND` configuration parameter
|
||||
* [#18406](https://github.com/netbox-community/netbox/issues/18406) - Scope column headers in object lists should not be orderable
|
||||
|
||||
---
|
||||
|
||||
## v4.2.1 (2025-01-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#18282](https://github.com/netbox-community/netbox/issues/18282) - Fix ordering of prefixes list by assigned VLAN
|
||||
* [#18314](https://github.com/netbox-community/netbox/issues/18314) - Fix KeyError exception when rendering pre-saved dashboard (`requires_internet` missing)
|
||||
* [#18316](https://github.com/netbox-community/netbox/issues/18316) - Fix AttributeError exception when global search results include prefixes and/or clusters
|
||||
* [#18318](https://github.com/netbox-community/netbox/issues/18318) - Correct navigation breadcrumbs for module type UI view
|
||||
* [#18324](https://github.com/netbox-community/netbox/issues/18324) - Correct filtering for certain related object listings
|
||||
* [#18329](https://github.com/netbox-community/netbox/issues/18329) - Address upstream bug in GraphQL API where only one primary IP address is returned within a device/VM list
|
||||
|
||||
---
|
||||
|
||||
## v4.2.0 (2025-01-06)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* Support for the Django admin UI has been completely removed. (The Django admin UI was disabled by default in NetBox v4.0.)
|
||||
* This release drops support for PostgreSQL 12. PostgreSQL 13 or later is required to run this release.
|
||||
* NetBox has adopted collation-based natural ordering for many models. This may alter the order in which some objects are listed by default.
|
||||
* Automatic redirects from pre-v4.1 UI views for virtual disks have been removed.
|
||||
* The `site` and `provider_network` foreign key fields on `circuits.CircuitTermination` have been replaced by the `termination` generic foreign key.
|
||||
* The `site` foreign key field on `ipam.Prefix` has been replaced by the `scope` generic foreign key.
|
||||
* The `site` foreign key field on `virtualization.Cluster` has been replaced by the `scope` generic foreign key.
|
||||
* The `circuit` foreign key field on `circuits.CircuitGroupAssignment` has been replaced by the `member` generic foreign key.
|
||||
* Obsolete nested REST API serializers have been removed. These were deprecated in NetBox v4.1 under [#17143](https://github.com/netbox-community/netbox/issues/17143).
|
||||
|
||||
### New Features
|
||||
|
||||
#### Assign Multiple MAC Addresses per Interface ([#4867](https://github.com/netbox-community/netbox/issues/4867))
|
||||
|
||||
MAC addresses are now managed as independent objects, rather than attributes on device and VM interfaces. NetBox now supports the assignment of multiple MAC addresses per interface, and allows a primary MAC address to be designated for each.
|
||||
|
||||
#### Quick Add UI Widget ([#5858](https://github.com/netbox-community/netbox/issues/5858))
|
||||
|
||||
A new UI widget has been introduced to enable conveniently creating new related objects while creating or editing an object. For instance, it is now possible to create and assign a new device role when creating or editing a device from within the device form.
|
||||
|
||||
#### VLAN Translation ([#7336](https://github.com/netbox-community/netbox/issues/7336))
|
||||
|
||||
User can now define policies which track the translation of VLAN IDs on IEEE 802.1Q-encapsulated interfaces. Translation policies can be reused across multiple interfaces.
|
||||
|
||||
#### Virtual Circuits ([#13086](https://github.com/netbox-community/netbox/issues/13086))
|
||||
|
||||
New models have been introduced to support the documentation of virtual circuits as an extension to the physical circuit modeling already supported. This enables users to accurately reflect point-to-point or multipoint virtual circuits atop infrastructure comprising physical circuits and cables.
|
||||
|
||||
#### Q-in-Q Encapsulation ([#13428](https://github.com/netbox-community/netbox/issues/13428))
|
||||
|
||||
NetBox now supports the designation of customer VLANs (CVLANs) and service VLANs (SVLANs) to support IEEE 802.1ad/Q-in-Q encapsulation. Each interface can now have it mode designated "Q-in-Q" and be assigned an SVLAN.
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#6414](https://github.com/netbox-community/netbox/issues/6414) - Prefixes can now be scoped by region, site group, site, or location
|
||||
* [#7699](https://github.com/netbox-community/netbox/issues/7699) - Virtualization clusters can now be scoped by region, site group, site, or location
|
||||
* [#9604](https://github.com/netbox-community/netbox/issues/9604) - The scope of a circuit termination now include a region, site group, site, location, or provider network
|
||||
* [#10711](https://github.com/netbox-community/netbox/issues/10711) - Wireless LANs can now be scoped by region, site group, site, or location
|
||||
* [#11279](https://github.com/netbox-community/netbox/issues/11279) - Improved the use of natural ordering for various models throughout the application
|
||||
* [#12596](https://github.com/netbox-community/netbox/issues/12596) - Extended the virtualization clusters REST API endpoint to report on allocated VM resources
|
||||
* [#16547](https://github.com/netbox-community/netbox/issues/16547) - Add a geographic distance field for circuits
|
||||
* [#16783](https://github.com/netbox-community/netbox/issues/16783) - Add an operational status field for inventory items
|
||||
* [#17195](https://github.com/netbox-community/netbox/issues/17195) - Add a color field for power outlets
|
||||
|
||||
### Plugins
|
||||
|
||||
* [#15093](https://github.com/netbox-community/netbox/issues/15093) - Introduced the `events_pipeline` configuration parameter, which allows plugins to hook into NetBox event processing
|
||||
* [#16546](https://github.com/netbox-community/netbox/issues/16546) - NetBoxModel now provides a default `get_absolute_url()` method
|
||||
* [#16971](https://github.com/netbox-community/netbox/issues/16971) - Plugins can now easily register system jobs to perform background tasks
|
||||
* [#17029](https://github.com/netbox-community/netbox/issues/17029) - Registering a `PluginTemplateExtension` subclass for a single model has been deprecated (replace `model` with `models`)
|
||||
* [#18023](https://github.com/netbox-community/netbox/issues/18023) - Extend `register_model_view()` to handle list views
|
||||
|
||||
### Other Changes
|
||||
|
||||
* [#16136](https://github.com/netbox-community/netbox/issues/16136) - Removed support for the Django admin UI
|
||||
* [#17165](https://github.com/netbox-community/netbox/issues/17165) - All obsolete nested REST API serializers have been removed
|
||||
* [#17472](https://github.com/netbox-community/netbox/issues/17472) - The legacy staged changes API has been deprecated, and will be removed in Netbox v4.3
|
||||
* [#17476](https://github.com/netbox-community/netbox/issues/17476) - Upgrade to Django 5.1
|
||||
* [#17752](https://github.com/netbox-community/netbox/issues/17752) - Bulk object import URL paths have been renamed from `*_import` to `*_bulk_import`
|
||||
* [#17761](https://github.com/netbox-community/netbox/issues/17761) - Optional choice fields now store empty values as null (rather than empty strings) in the database
|
||||
* [#18093](https://github.com/netbox-community/netbox/issues/18093) - Redirects for pre-v4.1 virtual disk UI views have been removed
|
||||
|
||||
### REST API Changes
|
||||
|
||||
* Added the following endpoints:
|
||||
* `/api/circuits/virtual-circuits/`
|
||||
* `/api/circuits/virtual-circuit-terminations/`
|
||||
* `/api/dcim/mac-addresses/`
|
||||
* `/api/ipam/vlan-translation-policies/`
|
||||
* `/api/ipam/vlan-translation-rules/`
|
||||
* circuits.Circuit
|
||||
* Added the optional `distance` and `distance_unit` fields
|
||||
* circuits.CircuitGroupAssignment
|
||||
* Replaced the `circuit` field with `member_type` and `member_id` to support virtual circuit assignment
|
||||
* circuits.CircuitTermination
|
||||
* Removed the `site` & `provider_network` fields
|
||||
* Added the `termination_type` & `termination_id` fields to facilitate termination assignment
|
||||
* Added the read-only `termination` field
|
||||
* dcim.Interface
|
||||
* The `mac_address` field is now read-only
|
||||
* Added the `primary_mac_address` relation to dcim.MACAddress
|
||||
* Added the read-only `mac_addresses` list
|
||||
* Added the `qinq_svlan` relation to ipam.VLAN
|
||||
* Added the `vlan_translation_policy` relation to ipam.VLANTranslationPolicy
|
||||
* Added `mode` choice "Q-in-Q"
|
||||
* dcim.InventoryItem
|
||||
* Added the optional `status` choice field
|
||||
* dcim.Location
|
||||
* Added the read-only `prefix_count` field
|
||||
* dcim.PowerOutlet
|
||||
* Added the optional `color` field
|
||||
* dcim.Region
|
||||
* Added the read-only `prefix_count` field
|
||||
* dcim.SiteGroup
|
||||
* Added the read-only `prefix_count` field
|
||||
* ipam.Prefix
|
||||
* Removed the `site` field
|
||||
* Added the `scope_type` & `scope_id` fields to facilitate scope assignment
|
||||
* Added the read-only `scope` field
|
||||
* ipam.VLAN
|
||||
* Added the optional `qinq_role` selection field
|
||||
* Added the `qinq_svlan` recursive relation
|
||||
* virtualization.Cluster
|
||||
* Removed the `site` field
|
||||
* Added the `scope_type` & `scope_id` fields to facilitate scope assignment
|
||||
* Added the read-only `scope` field
|
||||
* virtualization.Cluster
|
||||
* Added the read-only fields `allocated_vcpus`, `allocated_memory`, and `allocated_disk`
|
||||
* virtualization.VMInterface
|
||||
* The `mac_address` field is now read-only
|
||||
* Added the `primary_mac_address` relation to dcim.MACAddress
|
||||
* Added the read-only `mac_addresses` list
|
||||
* Added the `qinq_svlan` relation to ipam.VLAN
|
||||
* Added the `vlan_translation_policy` relation to ipam.VLANTranslationPolicy
|
||||
* Added `mode` choice "Q-in-Q"
|
||||
* wireless.WirelessLAN
|
||||
* Added the `scope_type` & `scope_id` fields to support scope assignment
|
||||
* Added the read-only `scope` field
|
@ -64,6 +64,8 @@ markdown_extensions:
|
||||
format: !!python/name:pymdownx.superfences.fence_code_format
|
||||
- pymdownx.tabbed:
|
||||
alternate_style: true
|
||||
not_in_nav: |
|
||||
/index.md
|
||||
nav:
|
||||
- Introduction: 'introduction.md'
|
||||
- Features:
|
||||
@ -174,6 +176,9 @@ nav:
|
||||
- Provider: 'models/circuits/provider.md'
|
||||
- Provider Account: 'models/circuits/provideraccount.md'
|
||||
- Provider Network: 'models/circuits/providernetwork.md'
|
||||
- Virtual Circuit: 'models/circuits/virtualcircuit.md'
|
||||
- Virtual Circuit Termination: 'models/circuits/virtualcircuittermination.md'
|
||||
- Virtual Circuit Type: 'models/circuits/virtualcircuittype.md'
|
||||
- Core:
|
||||
- DataFile: 'models/core/datafile.md'
|
||||
- DataSource: 'models/core/datasource.md'
|
||||
@ -197,6 +202,7 @@ nav:
|
||||
- InventoryItemRole: 'models/dcim/inventoryitemrole.md'
|
||||
- InventoryItemTemplate: 'models/dcim/inventoryitemtemplate.md'
|
||||
- Location: 'models/dcim/location.md'
|
||||
- MACAddress: 'models/dcim/macaddress.md'
|
||||
- Manufacturer: 'models/dcim/manufacturer.md'
|
||||
- Module: 'models/dcim/module.md'
|
||||
- ModuleBay: 'models/dcim/modulebay.md'
|
||||
@ -255,6 +261,8 @@ nav:
|
||||
- ServiceTemplate: 'models/ipam/servicetemplate.md'
|
||||
- VLAN: 'models/ipam/vlan.md'
|
||||
- VLANGroup: 'models/ipam/vlangroup.md'
|
||||
- VLANTranslationPolicy: 'models/ipam/vlantranslationpolicy.md'
|
||||
- VLANTranslationRule: 'models/ipam/vlantranslationrule.md'
|
||||
- VRF: 'models/ipam/vrf.md'
|
||||
- Tenancy:
|
||||
- Contact: 'models/tenancy/contact.md'
|
||||
@ -306,6 +314,7 @@ nav:
|
||||
- git Cheat Sheet: 'development/git-cheat-sheet.md'
|
||||
- Release Notes:
|
||||
- Summary: 'release-notes/index.md'
|
||||
- Version 4.2: 'release-notes/version-4.2.md'
|
||||
- Version 4.1: 'release-notes/version-4.1.md'
|
||||
- Version 4.0: 'release-notes/version-4.0.md'
|
||||
- Version 3.7: 'release-notes/version-3.7.md'
|
||||
|
@ -1,79 +0,0 @@
|
||||
import warnings
|
||||
|
||||
from drf_spectacular.utils import extend_schema_serializer
|
||||
|
||||
from circuits.models import *
|
||||
from netbox.api.fields import RelatedObjectCountField
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
from .serializers_.nested import NestedProviderAccountSerializer
|
||||
|
||||
__all__ = [
|
||||
'NestedCircuitSerializer',
|
||||
'NestedCircuitTerminationSerializer',
|
||||
'NestedCircuitTypeSerializer',
|
||||
'NestedProviderNetworkSerializer',
|
||||
'NestedProviderSerializer',
|
||||
'NestedProviderAccountSerializer',
|
||||
]
|
||||
|
||||
# TODO: Remove in v4.2
|
||||
warnings.warn(
|
||||
"Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
|
||||
DeprecationWarning
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Provider networks
|
||||
#
|
||||
|
||||
class NestedProviderNetworkSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ProviderNetwork
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name']
|
||||
|
||||
|
||||
#
|
||||
# Providers
|
||||
#
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('circuit_count',),
|
||||
)
|
||||
class NestedProviderSerializer(WritableNestedSerializer):
|
||||
circuit_count = RelatedObjectCountField('circuits')
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'circuit_count']
|
||||
|
||||
|
||||
#
|
||||
# Circuits
|
||||
#
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('circuit_count',),
|
||||
)
|
||||
class NestedCircuitTypeSerializer(WritableNestedSerializer):
|
||||
circuit_count = RelatedObjectCountField('circuits')
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'circuit_count']
|
||||
|
||||
|
||||
class NestedCircuitSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = ['id', 'url', 'display_url', 'display', 'cid']
|
||||
|
||||
|
||||
class NestedCircuitTerminationSerializer(WritableNestedSerializer):
|
||||
circuit = NestedCircuitSerializer()
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = ['id', 'url', 'display_url', 'display', 'circuit', 'term_side', 'cable', '_occupied']
|
@ -1,11 +1,20 @@
|
||||
from circuits.choices import CircuitPriorityChoices, CircuitStatusChoices
|
||||
from circuits.models import Circuit, CircuitGroup, CircuitGroupAssignment, CircuitTermination, CircuitType
|
||||
from dcim.api.serializers_.cables import CabledObjectSerializer
|
||||
from dcim.api.serializers_.sites import SiteSerializer
|
||||
from netbox.api.fields import ChoiceField, RelatedObjectCountField
|
||||
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
|
||||
from tenancy.api.serializers_.tenants import TenantSerializer
|
||||
from 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
|
||||
from circuits.constants import CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS, CIRCUIT_TERMINATION_TERMINATION_TYPES
|
||||
from circuits.models import (
|
||||
Circuit, CircuitGroup, CircuitGroupAssignment, CircuitTermination, CircuitType, VirtualCircuit,
|
||||
VirtualCircuitTermination, VirtualCircuitType,
|
||||
)
|
||||
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.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__ = (
|
||||
@ -14,6 +23,9 @@ __all__ = (
|
||||
'CircuitGroupSerializer',
|
||||
'CircuitTerminationSerializer',
|
||||
'CircuitTypeSerializer',
|
||||
'VirtualCircuitSerializer',
|
||||
'VirtualCircuitTerminationSerializer',
|
||||
'VirtualCircuitTypeSerializer',
|
||||
)
|
||||
|
||||
|
||||
@ -32,16 +44,32 @@ class CircuitTypeSerializer(NetBoxModelSerializer):
|
||||
|
||||
|
||||
class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
|
||||
site = SiteSerializer(nested=True, allow_null=True)
|
||||
provider_network = ProviderNetworkSerializer(nested=True, allow_null=True)
|
||||
termination_type = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(
|
||||
model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES
|
||||
),
|
||||
allow_null=True,
|
||||
required=False,
|
||||
default=None
|
||||
)
|
||||
termination_id = serializers.IntegerField(allow_null=True, required=False, default=None)
|
||||
termination = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'site', 'provider_network', 'port_speed', 'upstream_speed',
|
||||
'xconnect_id', 'description',
|
||||
'id', 'url', 'display_url', 'display', 'termination_type', 'termination_id', 'termination', 'port_speed',
|
||||
'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(NetBoxModelSerializer):
|
||||
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||
@ -76,6 +104,7 @@ class CircuitSerializer(NetBoxModelSerializer):
|
||||
provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True, default=None)
|
||||
status = ChoiceField(choices=CircuitStatusChoices, required=False)
|
||||
type = CircuitTypeSerializer(nested=True)
|
||||
distance_unit = ChoiceField(choices=DistanceUnitChoices, allow_blank=True, required=False, allow_null=True)
|
||||
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||
termination_a = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
|
||||
termination_z = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
|
||||
@ -85,33 +114,107 @@ class CircuitSerializer(NetBoxModelSerializer):
|
||||
model = Circuit
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant',
|
||||
'install_date', 'termination_date', 'commit_rate', 'description', 'termination_a', 'termination_z',
|
||||
'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'assignments',
|
||||
'install_date', 'termination_date', 'commit_rate', 'description', 'distance', 'distance_unit',
|
||||
'termination_a', 'termination_z', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'assignments',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'provider', 'cid', 'description')
|
||||
|
||||
|
||||
class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
||||
circuit = CircuitSerializer(nested=True)
|
||||
site = SiteSerializer(nested=True, required=False, allow_null=True)
|
||||
provider_network = ProviderNetworkSerializer(nested=True, required=False, allow_null=True)
|
||||
termination_type = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(
|
||||
model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES
|
||||
),
|
||||
allow_null=True,
|
||||
required=False,
|
||||
default=None
|
||||
)
|
||||
termination_id = serializers.IntegerField(allow_null=True, required=False, default=None)
|
||||
termination = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed',
|
||||
'upstream_speed', 'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'cable_end',
|
||||
'link_peers', 'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
'id', 'url', 'display_url', 'display', 'circuit', 'term_side', 'termination_type', 'termination_id',
|
||||
'termination', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', 'description', 'mark_connected',
|
||||
'cable', 'cable_end', 'link_peers', 'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'_occupied',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'circuit', 'term_side', 'description', 'cable', '_occupied')
|
||||
|
||||
@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_):
|
||||
circuit = CircuitSerializer(nested=True)
|
||||
member_type = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS)
|
||||
)
|
||||
member = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroupAssignment
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'group', 'circuit', 'priority', 'tags', 'created', 'last_updated',
|
||||
'id', 'url', 'display_url', 'display', 'group', 'member_type', 'member_id', 'member', 'priority', 'tags',
|
||||
'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'group', 'circuit', 'priority')
|
||||
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(NetBoxModelSerializer):
|
||||
|
||||
# Related object counts
|
||||
virtual_circuit_count = RelatedObjectCountField('virtual_circuits')
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuitType
|
||||
fields = [
|
||||
'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(NetBoxModelSerializer):
|
||||
provider_network = ProviderNetworkSerializer(nested=True)
|
||||
provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True, default=None)
|
||||
type = VirtualCircuitTypeSerializer(nested=True)
|
||||
status = ChoiceField(choices=CircuitStatusChoices, required=False)
|
||||
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuit
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'cid', 'provider_network', 'provider_account', 'type', 'status',
|
||||
'tenant', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'provider_network', 'cid', 'description')
|
||||
|
||||
|
||||
class VirtualCircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
||||
virtual_circuit = VirtualCircuitSerializer(nested=True)
|
||||
role = ChoiceField(choices=VirtualCircuitTerminationRoleChoices, required=False)
|
||||
interface = InterfaceSerializer(nested=True)
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuitTermination
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'virtual_circuit', 'role', 'interface', 'description', 'tags',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'virtual_circuit', 'role', 'interface', 'description')
|
||||
|
@ -17,5 +17,10 @@ router.register('circuit-terminations', views.CircuitTerminationViewSet)
|
||||
router.register('circuit-groups', views.CircuitGroupViewSet)
|
||||
router.register('circuit-group-assignments', views.CircuitGroupAssignmentViewSet)
|
||||
|
||||
# Virtual circuits
|
||||
router.register('virtual-circuits', views.VirtualCircuitViewSet)
|
||||
router.register('virtual-circuit-types', views.VirtualCircuitTypeViewSet)
|
||||
router.register('virtual-circuit-terminations', views.VirtualCircuitTerminationViewSet)
|
||||
|
||||
app_name = 'circuits-api'
|
||||
urlpatterns = router.urls
|
||||
|
@ -93,3 +93,33 @@ class ProviderNetworkViewSet(NetBoxModelViewSet):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
serializer_class = serializers.ProviderNetworkSerializer
|
||||
filterset_class = filtersets.ProviderNetworkFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Virtual circuit types
|
||||
#
|
||||
|
||||
class VirtualCircuitTypeViewSet(NetBoxModelViewSet):
|
||||
queryset = VirtualCircuitType.objects.all()
|
||||
serializer_class = serializers.VirtualCircuitTypeSerializer
|
||||
filterset_class = filtersets.VirtualCircuitTypeFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Virtual circuits
|
||||
#
|
||||
|
||||
class VirtualCircuitViewSet(NetBoxModelViewSet):
|
||||
queryset = VirtualCircuit.objects.all()
|
||||
serializer_class = serializers.VirtualCircuitSerializer
|
||||
filterset_class = filtersets.VirtualCircuitFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Virtual circuit terminations
|
||||
#
|
||||
|
||||
class VirtualCircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
||||
queryset = VirtualCircuitTermination.objects.all()
|
||||
serializer_class = serializers.VirtualCircuitTerminationSerializer
|
||||
filterset_class = filtersets.VirtualCircuitTerminationFilterSet
|
||||
|
@ -92,3 +92,19 @@ class CircuitPriorityChoices(ChoiceSet):
|
||||
(PRIORITY_TERTIARY, _('Tertiary')),
|
||||
(PRIORITY_INACTIVE, _('Inactive')),
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Virtual circuits
|
||||
#
|
||||
|
||||
class VirtualCircuitTerminationRoleChoices(ChoiceSet):
|
||||
ROLE_PEER = 'peer'
|
||||
ROLE_HUB = 'hub'
|
||||
ROLE_SPOKE = 'spoke'
|
||||
|
||||
CHOICES = [
|
||||
(ROLE_PEER, _('Peer'), 'green'),
|
||||
(ROLE_HUB, _('Hub'), 'blue'),
|
||||
(ROLE_SPOKE, _('Spoke'), 'orange'),
|
||||
]
|
||||
|
12
netbox/circuits/constants.py
Normal file
12
netbox/circuits/constants.py
Normal file
@ -0,0 +1,12 @@
|
||||
from django.db.models import Q
|
||||
|
||||
|
||||
# models values for ContentTypes which may be CircuitTermination termination types
|
||||
CIRCUIT_TERMINATION_TERMINATION_TYPES = (
|
||||
'region', 'sitegroup', 'site', 'location', 'providernetwork',
|
||||
)
|
||||
|
||||
CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS = Q(
|
||||
app_label='circuits',
|
||||
model__in=['circuit', 'virtualcircuit']
|
||||
)
|
@ -1,13 +1,16 @@
|
||||
import django_filters
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from dcim.filtersets import CabledObjectFilterSet
|
||||
from dcim.models import Region, Site, SiteGroup
|
||||
from dcim.models import Interface, Location, Region, Site, SiteGroup
|
||||
from ipam.models import ASN
|
||||
from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet
|
||||
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
|
||||
from utilities.filters import TreeNodeMultipleChoiceFilter
|
||||
from utilities.filters import (
|
||||
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
from .choices import *
|
||||
from .models import *
|
||||
|
||||
@ -20,43 +23,46 @@ __all__ = (
|
||||
'ProviderNetworkFilterSet',
|
||||
'ProviderAccountFilterSet',
|
||||
'ProviderFilterSet',
|
||||
'VirtualCircuitFilterSet',
|
||||
'VirtualCircuitTerminationFilterSet',
|
||||
'VirtualCircuitTypeFilterSet',
|
||||
)
|
||||
|
||||
|
||||
class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='circuits__terminations__site__region',
|
||||
field_name='circuits__terminations___region',
|
||||
lookup_expr='in',
|
||||
label=_('Region (ID)'),
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='circuits__terminations__site__region',
|
||||
field_name='circuits__terminations___region',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label=_('Region (slug)'),
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='circuits__terminations__site__group',
|
||||
field_name='circuits__terminations___site_group',
|
||||
lookup_expr='in',
|
||||
label=_('Site group (ID)'),
|
||||
)
|
||||
site_group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='circuits__terminations__site__group',
|
||||
field_name='circuits__terminations___site_group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label=_('Site group (slug)'),
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='circuits__terminations__site',
|
||||
field_name='circuits__terminations___site',
|
||||
queryset=Site.objects.all(),
|
||||
label=_('Site'),
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='circuits__terminations__site__slug',
|
||||
field_name='circuits__terminations___site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label=_('Site (slug)'),
|
||||
@ -173,7 +179,7 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
|
||||
label=_('Provider account (account)'),
|
||||
)
|
||||
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='terminations__provider_network',
|
||||
field_name='terminations___provider_network',
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
label=_('Provider network (ID)'),
|
||||
)
|
||||
@ -193,41 +199,46 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='terminations__site__region',
|
||||
field_name='terminations___region',
|
||||
lookup_expr='in',
|
||||
label=_('Region (ID)'),
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='terminations__site__region',
|
||||
field_name='terminations___region',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label=_('Region (slug)'),
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='terminations__site__group',
|
||||
field_name='terminations___site_group',
|
||||
lookup_expr='in',
|
||||
label=_('Site group (ID)'),
|
||||
)
|
||||
site_group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='terminations__site__group',
|
||||
field_name='terminations___site_group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label=_('Site group (slug)'),
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='terminations__site',
|
||||
field_name='terminations___site',
|
||||
queryset=Site.objects.all(),
|
||||
label=_('Site (ID)'),
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='terminations__site__slug',
|
||||
field_name='terminations___site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label=_('Site (slug)'),
|
||||
)
|
||||
location_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='terminations___location',
|
||||
label=_('Location (ID)'),
|
||||
queryset=Location.objects.all(),
|
||||
)
|
||||
termination_a_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=CircuitTermination.objects.all(),
|
||||
label=_('Termination A (ID)'),
|
||||
@ -239,7 +250,9 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = ('id', 'cid', 'description', 'install_date', 'termination_date', 'commit_rate')
|
||||
fields = (
|
||||
'id', 'cid', 'description', 'install_date', 'termination_date', 'commit_rate', 'distance', 'distance_unit',
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@ -263,18 +276,60 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
|
||||
queryset=Circuit.objects.all(),
|
||||
label=_('Circuit'),
|
||||
)
|
||||
termination_type = ContentTypeFilter()
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='_region',
|
||||
lookup_expr='in',
|
||||
label=_('Region (ID)'),
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='_region',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label=_('Region (slug)'),
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='_site_group',
|
||||
lookup_expr='in',
|
||||
label=_('Site group (ID)'),
|
||||
)
|
||||
site_group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='_site_group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label=_('Site group (slug)'),
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
field_name='_site',
|
||||
label=_('Site (ID)'),
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='site__slug',
|
||||
field_name='_site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label=_('Site (slug)'),
|
||||
)
|
||||
location_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Location.objects.all(),
|
||||
field_name='_location',
|
||||
lookup_expr='in',
|
||||
label=_('Location (ID)'),
|
||||
)
|
||||
location = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Location.objects.all(),
|
||||
field_name='_location',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label=_('Location (slug)'),
|
||||
)
|
||||
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
field_name='_provider_network',
|
||||
label=_('ProviderNetwork (ID)'),
|
||||
)
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
@ -292,8 +347,8 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = (
|
||||
'id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description', 'mark_connected',
|
||||
'pp_info', 'cable_end',
|
||||
'id', 'termination_id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description',
|
||||
'mark_connected', 'pp_info', 'cable_end',
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
@ -319,26 +374,36 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
|
||||
method='search',
|
||||
label=_('Search'),
|
||||
)
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='circuit__provider',
|
||||
queryset=Provider.objects.all(),
|
||||
label=_('Provider (ID)'),
|
||||
member_type = ContentTypeFilter()
|
||||
circuit = MultiValueCharFilter(
|
||||
method='filter_circuit',
|
||||
field_name='cid',
|
||||
label=_('Circuit (CID)'),
|
||||
)
|
||||
provider = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='circuit__provider__slug',
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='slug',
|
||||
label=_('Provider (slug)'),
|
||||
)
|
||||
circuit_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Circuit.objects.all(),
|
||||
circuit_id = MultiValueNumberFilter(
|
||||
method='filter_circuit',
|
||||
field_name='pk',
|
||||
label=_('Circuit (ID)'),
|
||||
)
|
||||
circuit = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='circuit__cid',
|
||||
queryset=Circuit.objects.all(),
|
||||
to_field_name='cid',
|
||||
label=_('Circuit (CID)'),
|
||||
virtual_circuit = MultiValueCharFilter(
|
||||
method='filter_virtual_circuit',
|
||||
field_name='cid',
|
||||
label=_('Virtual circuit (CID)'),
|
||||
)
|
||||
virtual_circuit_id = MultiValueNumberFilter(
|
||||
method='filter_virtual_circuit',
|
||||
field_name='pk',
|
||||
label=_('Virtual circuit (ID)'),
|
||||
)
|
||||
provider = MultiValueCharFilter(
|
||||
method='filter_provider',
|
||||
field_name='slug',
|
||||
label=_('Provider (name)'),
|
||||
)
|
||||
provider_id = MultiValueNumberFilter(
|
||||
method='filter_provider',
|
||||
field_name='pk',
|
||||
label=_('Provider (ID)'),
|
||||
)
|
||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=CircuitGroup.objects.all(),
|
||||
@ -353,12 +418,173 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroupAssignment
|
||||
fields = ('id', 'priority')
|
||||
fields = ('id', 'member_id', 'priority')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(circuit__cid__icontains=value) |
|
||||
Q(member__cid__icontains=value) |
|
||||
Q(group__name__icontains=value)
|
||||
)
|
||||
|
||||
def filter_circuit(self, queryset, name, value):
|
||||
circuits = Circuit.objects.filter(**{f'{name}__in': value})
|
||||
if not circuits.exists():
|
||||
return queryset.none()
|
||||
return queryset.filter(
|
||||
Q(
|
||||
member_type=ContentType.objects.get_for_model(Circuit),
|
||||
member_id__in=circuits
|
||||
)
|
||||
)
|
||||
|
||||
def filter_virtual_circuit(self, queryset, name, value):
|
||||
virtual_circuits = VirtualCircuit.objects.filter(**{f'{name}__in': value})
|
||||
if not virtual_circuits.exists():
|
||||
return queryset.none()
|
||||
return queryset.filter(
|
||||
Q(
|
||||
member_type=ContentType.objects.get_for_model(VirtualCircuit),
|
||||
member_id__in=virtual_circuits
|
||||
)
|
||||
)
|
||||
|
||||
def filter_provider(self, queryset, name, value):
|
||||
providers = Provider.objects.filter(**{f'{name}__in': value})
|
||||
if not providers.exists():
|
||||
return queryset.none()
|
||||
circuits = Circuit.objects.filter(provider__in=providers)
|
||||
virtual_circuits = VirtualCircuit.objects.filter(provider_network__provider__in=providers)
|
||||
return queryset.filter(
|
||||
Q(
|
||||
member_type=ContentType.objects.get_for_model(Circuit),
|
||||
member_id__in=circuits
|
||||
) |
|
||||
Q(
|
||||
member_type=ContentType.objects.get_for_model(VirtualCircuit),
|
||||
member_id__in=virtual_circuits
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class VirtualCircuitTypeFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuitType
|
||||
fields = ('id', 'name', 'slug', 'color', 'description')
|
||||
|
||||
|
||||
class VirtualCircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider_network__provider',
|
||||
queryset=Provider.objects.all(),
|
||||
label=_('Provider (ID)'),
|
||||
)
|
||||
provider = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider_network__provider__slug',
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='slug',
|
||||
label=_('Provider (slug)'),
|
||||
)
|
||||
provider_account_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider_account',
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
label=_('Provider account (ID)'),
|
||||
)
|
||||
provider_account = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider_account__account',
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='account',
|
||||
label=_('Provider account (account)'),
|
||||
)
|
||||
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
label=_('Provider network (ID)'),
|
||||
)
|
||||
type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=VirtualCircuitType.objects.all(),
|
||||
label=_('Virtual circuit type (ID)'),
|
||||
)
|
||||
type = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='type__slug',
|
||||
queryset=VirtualCircuitType.objects.all(),
|
||||
to_field_name='slug',
|
||||
label=_('Virtual circuit type (slug)'),
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=CircuitStatusChoices,
|
||||
null_value=None
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuit
|
||||
fields = ('id', 'cid', 'description')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(cid__icontains=value) |
|
||||
Q(description__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
).distinct()
|
||||
|
||||
|
||||
class VirtualCircuitTerminationFilterSet(NetBoxModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label=_('Search'),
|
||||
)
|
||||
virtual_circuit_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=VirtualCircuit.objects.all(),
|
||||
label=_('Virtual circuit'),
|
||||
)
|
||||
role = django_filters.MultipleChoiceFilter(
|
||||
choices=VirtualCircuitTerminationRoleChoices,
|
||||
null_value=None
|
||||
)
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='virtual_circuit__provider_network__provider',
|
||||
queryset=Provider.objects.all(),
|
||||
label=_('Provider (ID)'),
|
||||
)
|
||||
provider = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='virtual_circuit__provider_network__provider__slug',
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='slug',
|
||||
label=_('Provider (slug)'),
|
||||
)
|
||||
provider_account_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='virtual_circuit__provider_account',
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
label=_('Provider account (ID)'),
|
||||
)
|
||||
provider_account = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='virtual_circuit__provider_account__account',
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
to_field_name='account',
|
||||
label=_('Provider account (account)'),
|
||||
)
|
||||
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
field_name='virtual_circuit__provider_network',
|
||||
label=_('Provider network (ID)'),
|
||||
)
|
||||
interface_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Interface.objects.all(),
|
||||
field_name='interface',
|
||||
label=_('Interface (ID)'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuitTermination
|
||||
fields = ('id', 'interface_id', 'description')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(virtual_circuit__cid__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
).distinct()
|
||||
|
@ -1,16 +1,25 @@
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from circuits.choices import CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices
|
||||
from circuits.choices import (
|
||||
CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices, VirtualCircuitTerminationRoleChoices,
|
||||
)
|
||||
from circuits.constants import CIRCUIT_TERMINATION_TERMINATION_TYPES
|
||||
from circuits.models import *
|
||||
from dcim.models import Site
|
||||
from ipam.models import ASN
|
||||
from netbox.choices import DistanceUnitChoices
|
||||
from netbox.forms import NetBoxModelBulkEditForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import add_blank_choice
|
||||
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
||||
from utilities.forms.rendering import FieldSet, TabbedGroups
|
||||
from utilities.forms.widgets import BulkEditNullBooleanSelect, DatePicker, NumberWithOptions
|
||||
from utilities.forms import add_blank_choice, get_field_value
|
||||
from utilities.forms.fields import (
|
||||
ColorField, CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
||||
)
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.widgets import BulkEditNullBooleanSelect, DatePicker, HTMXSelect, NumberWithOptions
|
||||
from utilities.templatetags.builtins.filters import bettertitle
|
||||
|
||||
__all__ = (
|
||||
'CircuitBulkEditForm',
|
||||
@ -21,6 +30,9 @@ __all__ = (
|
||||
'ProviderBulkEditForm',
|
||||
'ProviderAccountBulkEditForm',
|
||||
'ProviderNetworkBulkEditForm',
|
||||
'VirtualCircuitBulkEditForm',
|
||||
'VirtualCircuitTerminationBulkEditForm',
|
||||
'VirtualCircuitTypeBulkEditForm',
|
||||
)
|
||||
|
||||
|
||||
@ -160,6 +172,17 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
|
||||
options=CircuitCommitRateChoices
|
||||
)
|
||||
)
|
||||
distance = forms.DecimalField(
|
||||
label=_('Distance'),
|
||||
min_value=0,
|
||||
required=False
|
||||
)
|
||||
distance_unit = forms.ChoiceField(
|
||||
label=_('Distance unit'),
|
||||
choices=add_blank_choice(DistanceUnitChoices),
|
||||
required=False,
|
||||
initial=''
|
||||
)
|
||||
description = forms.CharField(
|
||||
label=_('Description'),
|
||||
max_length=100,
|
||||
@ -171,6 +194,7 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
|
||||
fieldsets = (
|
||||
FieldSet('provider', 'type', 'status', 'description', name=_('Circuit')),
|
||||
FieldSet('provider_account', 'install_date', 'termination_date', 'commit_rate', name=_('Service Parameters')),
|
||||
FieldSet('distance', 'distance_unit', name=_('Attributes')),
|
||||
FieldSet('tenant', name=_('Tenancy')),
|
||||
)
|
||||
nullable_fields = (
|
||||
@ -184,15 +208,18 @@ class CircuitTerminationBulkEditForm(NetBoxModelBulkEditForm):
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
label=_('Site'),
|
||||
queryset=Site.objects.all(),
|
||||
required=False
|
||||
termination_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.filter(model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES),
|
||||
widget=HTMXSelect(method='post', attrs={'hx-select': '#form_fields'}),
|
||||
required=False,
|
||||
label=_('Termination type')
|
||||
)
|
||||
provider_network = DynamicModelChoiceField(
|
||||
label=_('Provider Network'),
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
required=False
|
||||
termination = DynamicModelChoiceField(
|
||||
label=_('Termination'),
|
||||
queryset=Site.objects.none(), # Initial queryset
|
||||
required=False,
|
||||
disabled=True,
|
||||
selector=True
|
||||
)
|
||||
port_speed = forms.IntegerField(
|
||||
required=False,
|
||||
@ -212,15 +239,26 @@ class CircuitTerminationBulkEditForm(NetBoxModelBulkEditForm):
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
'description',
|
||||
TabbedGroups(
|
||||
FieldSet('site', name=_('Site')),
|
||||
FieldSet('provider_network', name=_('Provider Network')),
|
||||
),
|
||||
'termination_type', 'termination',
|
||||
'mark_connected', name=_('Circuit Termination')
|
||||
),
|
||||
FieldSet('port_speed', 'upstream_speed', name=_('Termination Details')),
|
||||
)
|
||||
nullable_fields = ('description')
|
||||
nullable_fields = ('description', 'termination')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if termination_type_id := get_field_value(self, 'termination_type'):
|
||||
try:
|
||||
termination_type = ContentType.objects.get(pk=termination_type_id)
|
||||
model = termination_type.model_class()
|
||||
self.fields['termination'].queryset = model.objects.all()
|
||||
self.fields['termination'].widget.attrs['selector'] = model._meta.label_lower
|
||||
self.fields['termination'].disabled = False
|
||||
self.fields['termination'].label = _(bettertitle(model._meta.verbose_name))
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
|
||||
|
||||
class CircuitGroupBulkEditForm(NetBoxModelBulkEditForm):
|
||||
@ -242,7 +280,7 @@ class CircuitGroupBulkEditForm(NetBoxModelBulkEditForm):
|
||||
|
||||
|
||||
class CircuitGroupAssignmentBulkEditForm(NetBoxModelBulkEditForm):
|
||||
circuit = DynamicModelChoiceField(
|
||||
member = DynamicModelChoiceField(
|
||||
label=_('Circuit'),
|
||||
queryset=Circuit.objects.all(),
|
||||
required=False
|
||||
@ -255,6 +293,88 @@ class CircuitGroupAssignmentBulkEditForm(NetBoxModelBulkEditForm):
|
||||
|
||||
model = CircuitGroupAssignment
|
||||
fieldsets = (
|
||||
FieldSet('circuit', 'priority'),
|
||||
FieldSet('member', 'priority'),
|
||||
)
|
||||
nullable_fields = ('priority',)
|
||||
|
||||
|
||||
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')
|
||||
|
||||
|
||||
class VirtualCircuitBulkEditForm(NetBoxModelBulkEditForm):
|
||||
provider_network = DynamicModelChoiceField(
|
||||
label=_('Provider network'),
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
required=False
|
||||
)
|
||||
provider_account = DynamicModelChoiceField(
|
||||
label=_('Provider account'),
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
required=False
|
||||
)
|
||||
type = DynamicModelChoiceField(
|
||||
label=_('Type'),
|
||||
queryset=VirtualCircuitType.objects.all(),
|
||||
required=False
|
||||
)
|
||||
status = forms.ChoiceField(
|
||||
label=_('Status'),
|
||||
choices=add_blank_choice(CircuitStatusChoices),
|
||||
required=False,
|
||||
initial=''
|
||||
)
|
||||
tenant = DynamicModelChoiceField(
|
||||
label=_('Tenant'),
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False
|
||||
)
|
||||
description = forms.CharField(
|
||||
label=_('Description'),
|
||||
max_length=100,
|
||||
required=False
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
model = VirtualCircuit
|
||||
fieldsets = (
|
||||
FieldSet('provider_network', 'provider_account', 'status', 'description', name=_('Virtual circuit')),
|
||||
FieldSet('tenant', name=_('Tenancy')),
|
||||
)
|
||||
nullable_fields = (
|
||||
'provider_account', 'tenant', 'description', 'comments',
|
||||
)
|
||||
|
||||
|
||||
class VirtualCircuitTerminationBulkEditForm(NetBoxModelBulkEditForm):
|
||||
role = forms.ChoiceField(
|
||||
label=_('Role'),
|
||||
choices=add_blank_choice(VirtualCircuitTerminationRoleChoices),
|
||||
required=False,
|
||||
initial=''
|
||||
)
|
||||
description = forms.CharField(
|
||||
label=_('Description'),
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
|
||||
model = VirtualCircuitTermination
|
||||
fieldsets = (
|
||||
FieldSet('role', 'description'),
|
||||
)
|
||||
nullable_fields = ('description',)
|
||||
|
@ -1,12 +1,15 @@
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from circuits.choices import *
|
||||
from circuits.constants import *
|
||||
from circuits.models import *
|
||||
from dcim.models import Site
|
||||
from dcim.models import Interface
|
||||
from netbox.choices import DistanceUnitChoices
|
||||
from netbox.forms import NetBoxModelImportForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, SlugField
|
||||
from utilities.forms.fields import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, SlugField
|
||||
|
||||
__all__ = (
|
||||
'CircuitImportForm',
|
||||
@ -18,6 +21,10 @@ __all__ = (
|
||||
'ProviderImportForm',
|
||||
'ProviderAccountImportForm',
|
||||
'ProviderNetworkImportForm',
|
||||
'VirtualCircuitImportForm',
|
||||
'VirtualCircuitTerminationImportForm',
|
||||
'VirtualCircuitTerminationImportRelatedForm',
|
||||
'VirtualCircuitTypeImportForm',
|
||||
)
|
||||
|
||||
|
||||
@ -94,6 +101,12 @@ class CircuitImportForm(NetBoxModelImportForm):
|
||||
choices=CircuitStatusChoices,
|
||||
help_text=_('Operational status')
|
||||
)
|
||||
distance_unit = CSVChoiceField(
|
||||
label=_('Distance unit'),
|
||||
choices=DistanceUnitChoices,
|
||||
required=False,
|
||||
help_text=_('Distance unit')
|
||||
)
|
||||
tenant = CSVModelChoiceField(
|
||||
label=_('Tenant'),
|
||||
queryset=Tenant.objects.all(),
|
||||
@ -106,7 +119,7 @@ class CircuitImportForm(NetBoxModelImportForm):
|
||||
model = Circuit
|
||||
fields = [
|
||||
'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date', 'termination_date',
|
||||
'commit_rate', 'description', 'comments', 'tags'
|
||||
'commit_rate', 'distance', 'distance_unit', 'description', 'comments', 'tags'
|
||||
]
|
||||
|
||||
|
||||
@ -120,17 +133,10 @@ class BaseCircuitTerminationImportForm(forms.ModelForm):
|
||||
label=_('Termination'),
|
||||
choices=CircuitTerminationSideChoices,
|
||||
)
|
||||
site = CSVModelChoiceField(
|
||||
label=_('Site'),
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False
|
||||
)
|
||||
provider_network = CSVModelChoiceField(
|
||||
label=_('Provider network'),
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False
|
||||
termination_type = CSVContentTypeField(
|
||||
queryset=ContentType.objects.filter(model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES),
|
||||
required=False,
|
||||
label=_('Termination type (app & model)')
|
||||
)
|
||||
|
||||
|
||||
@ -138,9 +144,12 @@ class CircuitTerminationImportRelatedForm(BaseCircuitTerminationImportForm):
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
|
||||
'circuit', 'term_side', 'termination_type', 'termination_id', 'port_speed', 'upstream_speed', 'xconnect_id',
|
||||
'pp_info', 'description'
|
||||
]
|
||||
labels = {
|
||||
'termination_id': _('Termination ID'),
|
||||
}
|
||||
|
||||
|
||||
class CircuitTerminationImportForm(NetBoxModelImportForm, BaseCircuitTerminationImportForm):
|
||||
@ -148,9 +157,12 @@ class CircuitTerminationImportForm(NetBoxModelImportForm, BaseCircuitTermination
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
|
||||
'circuit', 'term_side', 'termination_type', 'termination_id', 'port_speed', 'upstream_speed', 'xconnect_id',
|
||||
'pp_info', 'description', 'tags'
|
||||
]
|
||||
labels = {
|
||||
'termination_id': _('Termination ID'),
|
||||
}
|
||||
|
||||
|
||||
class CircuitGroupImportForm(NetBoxModelImportForm):
|
||||
@ -168,7 +180,101 @@ class CircuitGroupImportForm(NetBoxModelImportForm):
|
||||
|
||||
|
||||
class CircuitGroupAssignmentImportForm(NetBoxModelImportForm):
|
||||
member_type = CSVContentTypeField(
|
||||
queryset=ContentType.objects.filter(CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS),
|
||||
label=_('Circuit type (app & model)')
|
||||
)
|
||||
priority = CSVChoiceField(
|
||||
label=_('Priority'),
|
||||
choices=CircuitPriorityChoices,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroupAssignment
|
||||
fields = ('circuit', 'group', 'priority')
|
||||
fields = ('member_type', 'member_id', 'group', 'priority')
|
||||
|
||||
|
||||
class VirtualCircuitTypeImportForm(NetBoxModelImportForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuitType
|
||||
fields = ('name', 'slug', 'color', 'description', 'tags')
|
||||
|
||||
|
||||
class VirtualCircuitImportForm(NetBoxModelImportForm):
|
||||
provider_network = CSVModelChoiceField(
|
||||
label=_('Provider network'),
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text=_('The network to which this virtual circuit belongs')
|
||||
)
|
||||
provider_account = CSVModelChoiceField(
|
||||
label=_('Provider account'),
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
to_field_name='account',
|
||||
help_text=_('Assigned provider account (if any)'),
|
||||
required=False
|
||||
)
|
||||
type = CSVModelChoiceField(
|
||||
label=_('Type'),
|
||||
queryset=VirtualCircuitType.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text=_('Type of virtual circuit')
|
||||
)
|
||||
status = CSVChoiceField(
|
||||
label=_('Status'),
|
||||
choices=CircuitStatusChoices,
|
||||
help_text=_('Operational status')
|
||||
)
|
||||
tenant = CSVModelChoiceField(
|
||||
label=_('Tenant'),
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text=_('Assigned tenant')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuit
|
||||
fields = [
|
||||
'cid', 'provider_network', 'provider_account', 'type', 'status', 'tenant', 'description', 'comments',
|
||||
'tags',
|
||||
]
|
||||
|
||||
|
||||
class BaseVirtualCircuitTerminationImportForm(forms.ModelForm):
|
||||
virtual_circuit = CSVModelChoiceField(
|
||||
label=_('Virtual circuit'),
|
||||
queryset=VirtualCircuit.objects.all(),
|
||||
to_field_name='cid',
|
||||
)
|
||||
role = CSVChoiceField(
|
||||
label=_('Role'),
|
||||
choices=VirtualCircuitTerminationRoleChoices,
|
||||
help_text=_('Operational role')
|
||||
)
|
||||
interface = CSVModelChoiceField(
|
||||
label=_('Interface'),
|
||||
queryset=Interface.objects.all(),
|
||||
to_field_name='pk',
|
||||
)
|
||||
|
||||
|
||||
class VirtualCircuitTerminationImportRelatedForm(BaseVirtualCircuitTerminationImportForm):
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuitTermination
|
||||
fields = [
|
||||
'virtual_circuit', 'role', 'interface', 'description',
|
||||
]
|
||||
|
||||
|
||||
class VirtualCircuitTerminationImportForm(NetBoxModelImportForm, BaseVirtualCircuitTerminationImportForm):
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuitTermination
|
||||
fields = [
|
||||
'virtual_circuit', 'role', 'interface', 'description', 'tags',
|
||||
]
|
||||
|
@ -1,12 +1,17 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from circuits.choices import CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices, CircuitTerminationSideChoices
|
||||
from circuits.choices import (
|
||||
CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices, CircuitTerminationSideChoices,
|
||||
VirtualCircuitTerminationRoleChoices,
|
||||
)
|
||||
from circuits.models import *
|
||||
from dcim.models import Region, Site, SiteGroup
|
||||
from dcim.models import Location, Region, Site, SiteGroup
|
||||
from ipam.models import ASN
|
||||
from netbox.choices import DistanceUnitChoices
|
||||
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
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.widgets import DatePicker, NumberWithOptions
|
||||
@ -20,6 +25,9 @@ __all__ = (
|
||||
'ProviderFilterForm',
|
||||
'ProviderAccountFilterForm',
|
||||
'ProviderNetworkFilterForm',
|
||||
'VirtualCircuitFilterForm',
|
||||
'VirtualCircuitTerminationFilterForm',
|
||||
'VirtualCircuitTypeFilterForm',
|
||||
)
|
||||
|
||||
|
||||
@ -114,8 +122,11 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
|
||||
FieldSet('type_id', 'status', 'install_date', 'termination_date', 'commit_rate', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
|
||||
FieldSet(
|
||||
'type_id', 'status', 'install_date', 'termination_date', 'commit_rate', 'distance', 'distance_unit',
|
||||
name=_('Attributes')
|
||||
),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
)
|
||||
@ -170,6 +181,11 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
|
||||
},
|
||||
label=_('Site')
|
||||
)
|
||||
location_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
required=False,
|
||||
label=_('Location')
|
||||
)
|
||||
install_date = forms.DateField(
|
||||
label=_('Install date'),
|
||||
required=False,
|
||||
@ -188,6 +204,15 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
|
||||
options=CircuitCommitRateChoices
|
||||
)
|
||||
)
|
||||
distance = forms.DecimalField(
|
||||
label=_('Distance'),
|
||||
required=False,
|
||||
)
|
||||
distance_unit = forms.ChoiceField(
|
||||
label=_('Distance unit'),
|
||||
choices=add_blank_choice(DistanceUnitChoices),
|
||||
required=False
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
@ -196,18 +221,29 @@ class CircuitTerminationFilterForm(NetBoxModelFilterSetForm):
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('circuit_id', 'term_side', name=_('Circuit')),
|
||||
FieldSet('provider_id', 'provider_network_id', name=_('Provider')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
|
||||
FieldSet('provider_id', name=_('Provider')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Termination')),
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region')
|
||||
)
|
||||
site_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site group')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$region_id',
|
||||
'site_group_id': '$site_group_id',
|
||||
},
|
||||
label=_('Site')
|
||||
)
|
||||
location_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
required=False,
|
||||
label=_('Location')
|
||||
)
|
||||
circuit_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Circuit.objects.all(),
|
||||
required=False,
|
||||
@ -247,14 +283,14 @@ class CircuitGroupAssignmentFilterForm(NetBoxModelFilterSetForm):
|
||||
model = CircuitGroupAssignment
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('provider_id', 'circuit_id', 'group_id', 'priority', name=_('Assignment')),
|
||||
FieldSet('provider_id', 'member_id', 'group_id', 'priority', name=_('Assignment')),
|
||||
)
|
||||
provider_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
required=False,
|
||||
label=_('Provider')
|
||||
)
|
||||
circuit_id = DynamicModelMultipleChoiceField(
|
||||
member_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Circuit.objects.all(),
|
||||
required=False,
|
||||
label=_('Circuit')
|
||||
@ -270,3 +306,93 @@ class CircuitGroupAssignmentFilterForm(NetBoxModelFilterSetForm):
|
||||
required=False
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class VirtualCircuitTypeFilterForm(NetBoxModelFilterSetForm):
|
||||
model = VirtualCircuitType
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('color', name=_('Attributes')),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
color = ColorField(
|
||||
label=_('Color'),
|
||||
required=False
|
||||
)
|
||||
|
||||
|
||||
class VirtualCircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
model = VirtualCircuit
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
|
||||
FieldSet('type', 'status', name=_('Attributes')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'provider_id', 'provider_network_id')
|
||||
provider_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
required=False,
|
||||
label=_('Provider')
|
||||
)
|
||||
provider_account_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'provider_id': '$provider_id'
|
||||
},
|
||||
label=_('Provider account')
|
||||
)
|
||||
provider_network_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'provider_id': '$provider_id'
|
||||
},
|
||||
label=_('Provider network')
|
||||
)
|
||||
type_id = DynamicModelMultipleChoiceField(
|
||||
queryset=VirtualCircuitType.objects.all(),
|
||||
required=False,
|
||||
label=_('Type')
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
label=_('Status'),
|
||||
choices=CircuitStatusChoices,
|
||||
required=False
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class VirtualCircuitTerminationFilterForm(NetBoxModelFilterSetForm):
|
||||
model = VirtualCircuitTermination
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('virtual_circuit_id', 'role', name=_('Virtual circuit')),
|
||||
FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
|
||||
)
|
||||
virtual_circuit_id = DynamicModelMultipleChoiceField(
|
||||
queryset=VirtualCircuit.objects.all(),
|
||||
required=False,
|
||||
label=_('Virtual circuit')
|
||||
)
|
||||
role = forms.MultipleChoiceField(
|
||||
label=_('Role'),
|
||||
choices=VirtualCircuitTerminationRoleChoices,
|
||||
required=False
|
||||
)
|
||||
provider_network_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'provider_id': '$provider_id'
|
||||
},
|
||||
label=_('Provider network')
|
||||
)
|
||||
provider_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
required=False,
|
||||
label=_('Provider')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
@ -1,14 +1,24 @@
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from circuits.choices import CircuitCommitRateChoices, CircuitTerminationPortSpeedChoices
|
||||
from circuits.choices import (
|
||||
CircuitCommitRateChoices, CircuitTerminationPortSpeedChoices, VirtualCircuitTerminationRoleChoices,
|
||||
)
|
||||
from circuits.constants import *
|
||||
from circuits.models import *
|
||||
from dcim.models import Site
|
||||
from dcim.models import Interface, Site
|
||||
from ipam.models import ASN
|
||||
from netbox.forms import NetBoxModelForm
|
||||
from tenancy.forms import TenancyForm
|
||||
from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
|
||||
from utilities.forms.rendering import FieldSet, TabbedGroups
|
||||
from utilities.forms.widgets import DatePicker, NumberWithOptions
|
||||
from utilities.forms import get_field_value
|
||||
from utilities.forms.fields import (
|
||||
CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField,
|
||||
)
|
||||
from utilities.forms.rendering import FieldSet, InlineFields
|
||||
from utilities.forms.widgets import DatePicker, HTMXSelect, NumberWithOptions
|
||||
from utilities.templatetags.builtins.filters import bettertitle
|
||||
|
||||
__all__ = (
|
||||
'CircuitForm',
|
||||
@ -19,6 +29,9 @@ __all__ = (
|
||||
'ProviderForm',
|
||||
'ProviderAccountForm',
|
||||
'ProviderNetworkForm',
|
||||
'VirtualCircuitForm',
|
||||
'VirtualCircuitTerminationForm',
|
||||
'VirtualCircuitTypeForm',
|
||||
)
|
||||
|
||||
|
||||
@ -45,7 +58,9 @@ class ProviderForm(NetBoxModelForm):
|
||||
class ProviderAccountForm(NetBoxModelForm):
|
||||
provider = DynamicModelChoiceField(
|
||||
label=_('Provider'),
|
||||
queryset=Provider.objects.all()
|
||||
queryset=Provider.objects.all(),
|
||||
selector=True,
|
||||
quick_add=True
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
@ -59,7 +74,9 @@ class ProviderAccountForm(NetBoxModelForm):
|
||||
class ProviderNetworkForm(NetBoxModelForm):
|
||||
provider = DynamicModelChoiceField(
|
||||
label=_('Provider'),
|
||||
queryset=Provider.objects.all()
|
||||
queryset=Provider.objects.all(),
|
||||
selector=True,
|
||||
quick_add=True
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
@ -92,7 +109,8 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
|
||||
provider = DynamicModelChoiceField(
|
||||
label=_('Provider'),
|
||||
queryset=Provider.objects.all(),
|
||||
selector=True
|
||||
selector=True,
|
||||
quick_add=True
|
||||
)
|
||||
provider_account = DynamicModelChoiceField(
|
||||
label=_('Provider account'),
|
||||
@ -103,12 +121,23 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
|
||||
}
|
||||
)
|
||||
type = DynamicModelChoiceField(
|
||||
queryset=CircuitType.objects.all()
|
||||
queryset=CircuitType.objects.all(),
|
||||
quick_add=True
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('provider', 'provider_account', 'cid', 'type', 'status', 'description', 'tags', name=_('Circuit')),
|
||||
FieldSet(
|
||||
'provider',
|
||||
'provider_account',
|
||||
'cid',
|
||||
'type',
|
||||
'status',
|
||||
InlineFields('distance', 'distance_unit', label=_('Distance')),
|
||||
'description',
|
||||
'tags',
|
||||
name=_('Circuit')
|
||||
),
|
||||
FieldSet('install_date', 'termination_date', 'commit_rate', name=_('Service Parameters')),
|
||||
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
|
||||
)
|
||||
@ -117,7 +146,7 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
|
||||
model = Circuit
|
||||
fields = [
|
||||
'cid', 'type', 'provider', 'provider_account', 'status', 'install_date', 'termination_date', 'commit_rate',
|
||||
'description', 'tenant_group', 'tenant', 'comments', 'tags',
|
||||
'distance', 'distance_unit', 'description', 'tenant_group', 'tenant', 'comments', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'install_date': DatePicker(),
|
||||
@ -134,26 +163,24 @@ class CircuitTerminationForm(NetBoxModelForm):
|
||||
queryset=Circuit.objects.all(),
|
||||
selector=True
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
label=_('Site'),
|
||||
queryset=Site.objects.all(),
|
||||
termination_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.filter(model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES),
|
||||
widget=HTMXSelect(),
|
||||
required=False,
|
||||
selector=True
|
||||
label=_('Termination type')
|
||||
)
|
||||
provider_network = DynamicModelChoiceField(
|
||||
label=_('Provider network'),
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
termination = DynamicModelChoiceField(
|
||||
label=_('Termination'),
|
||||
queryset=Site.objects.none(), # Initial queryset
|
||||
required=False,
|
||||
disabled=True,
|
||||
selector=True
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
'circuit', 'term_side', 'description', 'tags',
|
||||
TabbedGroups(
|
||||
FieldSet('site', name=_('Site')),
|
||||
FieldSet('provider_network', name=_('Provider Network')),
|
||||
),
|
||||
'termination_type', 'termination',
|
||||
'mark_connected', name=_('Circuit Termination')
|
||||
),
|
||||
FieldSet('port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', name=_('Termination Details')),
|
||||
@ -162,7 +189,7 @@ class CircuitTerminationForm(NetBoxModelForm):
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'circuit', 'term_side', 'site', 'provider_network', 'mark_connected', 'port_speed', 'upstream_speed',
|
||||
'circuit', 'term_side', 'termination_type', 'mark_connected', 'port_speed', 'upstream_speed',
|
||||
'xconnect_id', 'pp_info', 'description', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
@ -174,6 +201,36 @@ class CircuitTerminationForm(NetBoxModelForm):
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
instance = kwargs.get('instance')
|
||||
initial = kwargs.get('initial', {})
|
||||
|
||||
if instance is not None and instance.termination:
|
||||
initial['termination'] = instance.termination
|
||||
kwargs['initial'] = initial
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if termination_type_id := get_field_value(self, 'termination_type'):
|
||||
try:
|
||||
termination_type = ContentType.objects.get(pk=termination_type_id)
|
||||
model = termination_type.model_class()
|
||||
self.fields['termination'].queryset = model.objects.all()
|
||||
self.fields['termination'].widget.attrs['selector'] = model._meta.label_lower
|
||||
self.fields['termination'].disabled = False
|
||||
self.fields['termination'].label = _(bettertitle(model._meta.verbose_name))
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
|
||||
if self.instance and termination_type_id != self.instance.termination_type_id:
|
||||
self.initial['termination'] = None
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Assign the selected termination (if any)
|
||||
self.instance.termination = self.cleaned_data.get('termination')
|
||||
|
||||
|
||||
class CircuitGroupForm(TenancyForm, NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
@ -195,14 +252,137 @@ class CircuitGroupAssignmentForm(NetBoxModelForm):
|
||||
label=_('Group'),
|
||||
queryset=CircuitGroup.objects.all(),
|
||||
)
|
||||
circuit = DynamicModelChoiceField(
|
||||
member_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.filter(CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS),
|
||||
widget=HTMXSelect(),
|
||||
required=False,
|
||||
label=_('Circuit type')
|
||||
)
|
||||
member = DynamicModelChoiceField(
|
||||
label=_('Circuit'),
|
||||
queryset=Circuit.objects.all(),
|
||||
queryset=Circuit.objects.none(), # Initial queryset
|
||||
required=False,
|
||||
disabled=True,
|
||||
selector=True
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('group', 'member_type', 'member', 'priority', 'tags', name=_('Group Assignment')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroupAssignment
|
||||
fields = [
|
||||
'group', 'circuit', 'priority', 'tags',
|
||||
'group', 'member_type', 'priority', 'tags',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
instance = kwargs.get('instance')
|
||||
initial = kwargs.get('initial', {})
|
||||
|
||||
if instance is not None and instance.member:
|
||||
initial['member'] = instance.member
|
||||
kwargs['initial'] = initial
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if member_type_id := get_field_value(self, 'member_type'):
|
||||
try:
|
||||
model = ContentType.objects.get(pk=member_type_id).model_class()
|
||||
self.fields['member'].queryset = model.objects.all()
|
||||
self.fields['member'].widget.attrs['selector'] = model._meta.label_lower
|
||||
self.fields['member'].disabled = False
|
||||
self.fields['member'].label = _(bettertitle(model._meta.verbose_name))
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
|
||||
if self.instance.pk and member_type_id != self.instance.member_type_id:
|
||||
self.initial['member'] = None
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Assign the selected circuit (if any)
|
||||
self.instance.member = self.cleaned_data.get('member')
|
||||
|
||||
|
||||
class VirtualCircuitTypeForm(NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('name', 'slug', 'color', 'description', 'tags'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuitType
|
||||
fields = [
|
||||
'name', 'slug', 'color', 'description', 'tags',
|
||||
]
|
||||
|
||||
|
||||
class VirtualCircuitForm(TenancyForm, NetBoxModelForm):
|
||||
provider_network = DynamicModelChoiceField(
|
||||
label=_('Provider network'),
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
selector=True
|
||||
)
|
||||
provider_account = DynamicModelChoiceField(
|
||||
label=_('Provider account'),
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
required=False
|
||||
)
|
||||
type = DynamicModelChoiceField(
|
||||
queryset=VirtualCircuitType.objects.all(),
|
||||
quick_add=True
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
'provider_network', 'provider_account', 'cid', 'type', 'status', 'description', 'tags',
|
||||
name=_('Virtual circuit'),
|
||||
),
|
||||
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuit
|
||||
fields = [
|
||||
'cid', 'provider_network', 'provider_account', 'type', 'status', 'description', 'tenant_group', 'tenant',
|
||||
'comments', 'tags',
|
||||
]
|
||||
|
||||
|
||||
class VirtualCircuitTerminationForm(NetBoxModelForm):
|
||||
virtual_circuit = DynamicModelChoiceField(
|
||||
label=_('Virtual circuit'),
|
||||
queryset=VirtualCircuit.objects.all(),
|
||||
selector=True
|
||||
)
|
||||
role = forms.ChoiceField(
|
||||
choices=VirtualCircuitTerminationRoleChoices,
|
||||
widget=HTMXSelect(),
|
||||
label=_('Role')
|
||||
)
|
||||
interface = DynamicModelChoiceField(
|
||||
label=_('Interface'),
|
||||
queryset=Interface.objects.all(),
|
||||
selector=True,
|
||||
query_params={
|
||||
'kind': 'virtual',
|
||||
'virtual_circuit_termination_id': 'null',
|
||||
},
|
||||
context={
|
||||
'parent': 'device',
|
||||
}
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('virtual_circuit', 'role', 'interface', 'description', 'tags'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuitTermination
|
||||
fields = [
|
||||
'virtual_circuit', 'role', 'interface', 'description', 'tags',
|
||||
]
|
||||
|
@ -4,14 +4,17 @@ from circuits import filtersets, models
|
||||
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
|
||||
|
||||
__all__ = (
|
||||
'CircuitTerminationFilter',
|
||||
'CircuitFilter',
|
||||
'CircuitGroupAssignmentFilter',
|
||||
'CircuitGroupFilter',
|
||||
'CircuitTerminationFilter',
|
||||
'CircuitTypeFilter',
|
||||
'ProviderFilter',
|
||||
'ProviderAccountFilter',
|
||||
'ProviderNetworkFilter',
|
||||
'VirtualCircuitFilter',
|
||||
'VirtualCircuitTerminationFilter',
|
||||
'VirtualCircuitTypeFilter',
|
||||
)
|
||||
|
||||
|
||||
@ -61,3 +64,21 @@ class ProviderAccountFilter(BaseFilterMixin):
|
||||
@autotype_decorator(filtersets.ProviderNetworkFilterSet)
|
||||
class ProviderNetworkFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VirtualCircuitType, lookups=True)
|
||||
@autotype_decorator(filtersets.VirtualCircuitTypeFilterSet)
|
||||
class VirtualCircuitTypeFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VirtualCircuit, lookups=True)
|
||||
@autotype_decorator(filtersets.VirtualCircuitFilterSet)
|
||||
class VirtualCircuitFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VirtualCircuitTermination, lookups=True)
|
||||
@autotype_decorator(filtersets.VirtualCircuitTerminationFilterSet)
|
||||
class VirtualCircuitTerminationFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
@ -31,3 +31,12 @@ class CircuitsQuery:
|
||||
|
||||
provider_network: ProviderNetworkType = strawberry_django.field()
|
||||
provider_network_list: List[ProviderNetworkType] = strawberry_django.field()
|
||||
|
||||
virtual_circuit: VirtualCircuitType = strawberry_django.field()
|
||||
virtual_circuit_list: List[VirtualCircuitType] = strawberry_django.field()
|
||||
|
||||
virtual_circuit_termination: VirtualCircuitTerminationType = strawberry_django.field()
|
||||
virtual_circuit_termination_list: List[VirtualCircuitTerminationType] = strawberry_django.field()
|
||||
|
||||
virtual_circuit_type: VirtualCircuitTypeType = strawberry_django.field()
|
||||
virtual_circuit_type_list: List[VirtualCircuitTypeType] = strawberry_django.field()
|
||||
|
@ -1,4 +1,4 @@
|
||||
from typing import Annotated, List
|
||||
from typing import Annotated, List, Union
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
@ -19,6 +19,9 @@ __all__ = (
|
||||
'ProviderType',
|
||||
'ProviderAccountType',
|
||||
'ProviderNetworkType',
|
||||
'VirtualCircuitTerminationType',
|
||||
'VirtualCircuitType',
|
||||
'VirtualCircuitTypeType',
|
||||
)
|
||||
|
||||
|
||||
@ -59,13 +62,21 @@ class ProviderNetworkType(NetBoxObjectType):
|
||||
|
||||
@strawberry_django.type(
|
||||
models.CircuitTermination,
|
||||
fields='__all__',
|
||||
exclude=('termination_type', 'termination_id', '_location', '_region', '_site', '_site_group', '_provider_network'),
|
||||
filters=CircuitTerminationFilter
|
||||
)
|
||||
class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, ObjectType):
|
||||
circuit: Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]
|
||||
provider_network: Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')] | None
|
||||
site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
|
||||
@strawberry_django.field
|
||||
def termination(self) -> Annotated[Union[
|
||||
Annotated["LocationType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["RegionType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["SiteType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')],
|
||||
], strawberry.union("CircuitTerminationTerminationType")] | None:
|
||||
return self.termination
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
@ -106,9 +117,58 @@ class CircuitGroupType(OrganizationalObjectType):
|
||||
|
||||
@strawberry_django.type(
|
||||
models.CircuitGroupAssignment,
|
||||
fields='__all__',
|
||||
exclude=('member_type', 'member_id'),
|
||||
filters=CircuitGroupAssignmentFilter
|
||||
)
|
||||
class CircuitGroupAssignmentType(TagsMixin, BaseObjectType):
|
||||
group: Annotated["CircuitGroupType", strawberry.lazy('circuits.graphql.types')]
|
||||
circuit: Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]
|
||||
|
||||
@strawberry_django.field
|
||||
def member(self) -> Annotated[Union[
|
||||
Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')],
|
||||
Annotated["VirtualCircuitType", strawberry.lazy('circuits.graphql.types')],
|
||||
], strawberry.union("CircuitGroupAssignmentMemberType")] | None:
|
||||
return self.member
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.VirtualCircuitType,
|
||||
fields='__all__',
|
||||
filters=VirtualCircuitTypeFilter
|
||||
)
|
||||
class VirtualCircuitTypeType(OrganizationalObjectType):
|
||||
color: str
|
||||
|
||||
virtual_circuits: List[Annotated["VirtualCircuitType", strawberry.lazy('circuits.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.VirtualCircuitTermination,
|
||||
fields='__all__',
|
||||
filters=VirtualCircuitTerminationFilter
|
||||
)
|
||||
class VirtualCircuitTerminationType(CustomFieldsMixin, TagsMixin, ObjectType):
|
||||
virtual_circuit: Annotated[
|
||||
"VirtualCircuitType",
|
||||
strawberry.lazy('circuits.graphql.types')
|
||||
] = strawberry_django.field(select_related=["virtual_circuit"])
|
||||
interface: Annotated[
|
||||
"InterfaceType",
|
||||
strawberry.lazy('dcim.graphql.types')
|
||||
] = strawberry_django.field(select_related=["interface"])
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.VirtualCircuit,
|
||||
fields='__all__',
|
||||
filters=VirtualCircuitFilter
|
||||
)
|
||||
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]
|
||||
|
@ -5,11 +5,9 @@ import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
dependencies = []
|
||||
|
||||
replaces = [
|
||||
('circuits', '0001_initial'),
|
||||
@ -98,7 +96,12 @@ class Migration(migrations.Migration):
|
||||
('name', models.CharField(max_length=100)),
|
||||
('description', models.CharField(blank=True, max_length=200)),
|
||||
('comments', models.TextField(blank=True)),
|
||||
('provider', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='networks', to='circuits.provider')),
|
||||
(
|
||||
'provider',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT, related_name='networks', to='circuits.provider'
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'ordering': ('provider', 'name'),
|
||||
|
@ -4,7 +4,6 @@ import taggit.managers
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0001_initial'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
@ -58,32 +57,56 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='_cable_peer_type',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='contenttypes.contenttype'),
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name='+',
|
||||
to='contenttypes.contenttype',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='cable',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.cable'),
|
||||
field=models.ForeignKey(
|
||||
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.cable'
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='circuit',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='terminations', to='circuits.circuit'),
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, related_name='terminations', to='circuits.circuit'
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='provider_network',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuit_terminations', to='circuits.providernetwork'),
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='circuit_terminations',
|
||||
to='circuits.providernetwork',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='site',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuit_terminations', to='dcim.site'),
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='circuit_terminations',
|
||||
to='dcim.site',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='provider',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.provider'),
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.provider'
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
@ -93,26 +116,50 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='tenant',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='tenancy.tenant'),
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='circuits',
|
||||
to='tenancy.tenant',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='termination_a',
|
||||
field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='circuits.circuittermination'),
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
editable=False,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name='+',
|
||||
to='circuits.circuittermination',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='termination_z',
|
||||
field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='circuits.circuittermination'),
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
editable=False,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name='+',
|
||||
to='circuits.circuittermination',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='type',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.circuittype'),
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.circuittype'
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='providernetwork',
|
||||
constraint=models.UniqueConstraint(fields=('provider', 'name'), name='circuits_providernetwork_provider_name'),
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=('provider', 'name'), name='circuits_providernetwork_provider_name'
|
||||
),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='providernetwork',
|
||||
|
@ -5,7 +5,6 @@ import utilities.json
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [
|
||||
('circuits', '0003_extend_tag_support'),
|
||||
('circuits', '0004_rename_cable_peer'),
|
||||
@ -14,7 +13,7 @@ class Migration(migrations.Migration):
|
||||
('circuits', '0034_created_datetimefield'),
|
||||
('circuits', '0035_provider_asns'),
|
||||
('circuits', '0036_circuit_termination_date_tags_custom_fields'),
|
||||
('circuits', '0037_new_cabling_models')
|
||||
('circuits', '0037_new_cabling_models'),
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
|
@ -6,13 +6,12 @@ import utilities.json
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [
|
||||
('circuits', '0038_cabling_cleanup'),
|
||||
('circuits', '0039_unique_constraints'),
|
||||
('circuits', '0040_provider_remove_deprecated_fields'),
|
||||
('circuits', '0041_standardize_description_comments'),
|
||||
('circuits', '0042_provideraccount')
|
||||
('circuits', '0042_provideraccount'),
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
@ -51,11 +50,15 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='circuittermination',
|
||||
constraint=models.UniqueConstraint(fields=('circuit', 'term_side'), name='circuits_circuittermination_unique_circuit_term_side'),
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=('circuit', 'term_side'), name='circuits_circuittermination_unique_circuit_term_side'
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='providernetwork',
|
||||
constraint=models.UniqueConstraint(fields=('provider', 'name'), name='circuits_providernetwork_unique_provider_name'),
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=('provider', 'name'), name='circuits_providernetwork_unique_provider_name'
|
||||
),
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='provider',
|
||||
@ -84,12 +87,20 @@ class Migration(migrations.Migration):
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
|
||||
(
|
||||
'custom_field_data',
|
||||
models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
|
||||
),
|
||||
('description', models.CharField(blank=True, max_length=200)),
|
||||
('comments', models.TextField(blank=True)),
|
||||
('account', models.CharField(max_length=100)),
|
||||
('name', models.CharField(blank=True, max_length=100)),
|
||||
('provider', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='accounts', to='circuits.provider')),
|
||||
(
|
||||
'provider',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT, related_name='accounts', to='circuits.provider'
|
||||
),
|
||||
),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
],
|
||||
options={
|
||||
@ -98,11 +109,17 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='provideraccount',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('name', ''), _negated=True), fields=('provider', 'name'), name='circuits_provideraccount_unique_provider_name'),
|
||||
constraint=models.UniqueConstraint(
|
||||
condition=models.Q(('name', ''), _negated=True),
|
||||
fields=('provider', 'name'),
|
||||
name='circuits_provideraccount_unique_provider_name',
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='provideraccount',
|
||||
constraint=models.UniqueConstraint(fields=('provider', 'account'), name='circuits_provideraccount_unique_provider_account'),
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=('provider', 'account'), name='circuits_provideraccount_unique_provider_account'
|
||||
),
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='provider',
|
||||
@ -111,7 +128,13 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='provider_account',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.provideraccount'),
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='circuits',
|
||||
to='circuits.provideraccount',
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
@ -120,6 +143,8 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='circuit',
|
||||
constraint=models.UniqueConstraint(fields=('provider_account', 'cid'), name='circuits_circuit_unique_provideraccount_cid'),
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=('provider_account', 'cid'), name='circuits_circuit_unique_provideraccount_cid'
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -5,7 +5,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0043_circuittype_color'),
|
||||
('extras', '0119_notifications'),
|
||||
|
27
netbox/circuits/migrations/0045_circuit_distance.py
Normal file
27
netbox/circuits/migrations/0045_circuit_distance.py
Normal file
@ -0,0 +1,27 @@
|
||||
# Generated by Django 5.0.9 on 2024-09-26 22:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('circuits', '0044_circuit_groups'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='_abs_distance',
|
||||
field=models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='distance',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='distance_unit',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
]
|
39
netbox/circuits/migrations/0046_charfield_null_choices.py
Normal file
39
netbox/circuits/migrations/0046_charfield_null_choices.py
Normal file
@ -0,0 +1,39 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def set_null_values(apps, schema_editor):
|
||||
"""
|
||||
Replace empty strings with null values.
|
||||
"""
|
||||
Circuit = apps.get_model('circuits', 'Circuit')
|
||||
CircuitGroupAssignment = apps.get_model('circuits', 'CircuitGroupAssignment')
|
||||
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
|
||||
|
||||
Circuit.objects.filter(distance_unit='').update(distance_unit=None)
|
||||
CircuitGroupAssignment.objects.filter(priority='').update(priority=None)
|
||||
CircuitTermination.objects.filter(cable_end='').update(cable_end=None)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('circuits', '0045_circuit_distance'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='circuit',
|
||||
name='distance_unit',
|
||||
field=models.CharField(blank=True, max_length=50, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuitgroupassignment',
|
||||
name='priority',
|
||||
field=models.CharField(blank=True, max_length=50, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuittermination',
|
||||
name='cable_end',
|
||||
field=models.CharField(blank=True, max_length=1, null=True),
|
||||
),
|
||||
migrations.RunPython(code=set_null_values, reverse_code=migrations.RunPython.noop),
|
||||
]
|
@ -0,0 +1,53 @@
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def copy_site_assignments(apps, schema_editor):
|
||||
"""
|
||||
Copy site ForeignKey values to the Termination GFK.
|
||||
"""
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
|
||||
Site = apps.get_model('dcim', 'Site')
|
||||
|
||||
CircuitTermination.objects.filter(site__isnull=False).update(
|
||||
termination_type=ContentType.objects.get_for_model(Site), termination_id=models.F('site_id')
|
||||
)
|
||||
|
||||
ProviderNetwork = apps.get_model('circuits', 'ProviderNetwork')
|
||||
CircuitTermination.objects.filter(provider_network__isnull=False).update(
|
||||
termination_type=ContentType.objects.get_for_model(ProviderNetwork),
|
||||
termination_id=models.F('provider_network_id'),
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('circuits', '0046_charfield_null_choices'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('dcim', '0193_poweroutlet_color'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='termination_id',
|
||||
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='termination_type',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
limit_choices_to=models.Q(
|
||||
('model__in', ('region', 'sitegroup', 'site', 'location', 'providernetwork'))
|
||||
),
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='+',
|
||||
to='contenttypes.contenttype',
|
||||
),
|
||||
),
|
||||
# Copy over existing site assignments
|
||||
migrations.RunPython(code=copy_site_assignments, reverse_code=migrations.RunPython.noop),
|
||||
]
|
@ -0,0 +1,83 @@
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def populate_denormalized_fields(apps, schema_editor):
|
||||
"""
|
||||
Copy site ForeignKey values to the Termination GFK.
|
||||
"""
|
||||
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
|
||||
|
||||
terminations = CircuitTermination.objects.filter(site__isnull=False).prefetch_related('site')
|
||||
for termination in terminations:
|
||||
termination._region_id = termination.site.region_id
|
||||
termination._site_group_id = termination.site.group_id
|
||||
termination._site_id = termination.site_id
|
||||
# Note: Location cannot be set prior to migration
|
||||
|
||||
CircuitTermination.objects.bulk_update(terminations, ['_region', '_site_group', '_site'], batch_size=100)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('circuits', '0047_circuittermination__termination'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='_location',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='circuit_terminations',
|
||||
to='dcim.location',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='_region',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='circuit_terminations',
|
||||
to='dcim.region',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='_site',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='circuit_terminations',
|
||||
to='dcim.site',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='_site_group',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='circuit_terminations',
|
||||
to='dcim.sitegroup',
|
||||
),
|
||||
),
|
||||
# Populate denormalized FK values
|
||||
migrations.RunPython(code=populate_denormalized_fields, reverse_code=migrations.RunPython.noop),
|
||||
# Delete the site ForeignKey
|
||||
migrations.RemoveField(
|
||||
model_name='circuittermination',
|
||||
name='site',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='circuittermination',
|
||||
old_name='provider_network',
|
||||
new_name='_provider_network',
|
||||
),
|
||||
]
|
21
netbox/circuits/migrations/0049_natural_ordering.py
Normal file
21
netbox/circuits/migrations/0049_natural_ordering.py
Normal file
@ -0,0 +1,21 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('circuits', '0048_circuitterminations_cached_relations'),
|
||||
('dcim', '0197_natural_sort_collation'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='provider',
|
||||
name='name',
|
||||
field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='providernetwork',
|
||||
name='name',
|
||||
field=models.CharField(db_collation='natural_sort', max_length=100),
|
||||
),
|
||||
]
|
147
netbox/circuits/migrations/0050_virtual_circuits.py
Normal file
147
netbox/circuits/migrations/0050_virtual_circuits.py
Normal file
@ -0,0 +1,147 @@
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
from django.db import migrations, models
|
||||
|
||||
import utilities.fields
|
||||
import utilities.json
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('circuits', '0049_natural_ordering'),
|
||||
('dcim', '0196_qinq_svlan'),
|
||||
('extras', '0122_charfield_null_choices'),
|
||||
('tenancy', '0016_charfield_null_choices'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='VirtualCircuitType',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
('custom_field_data', models.JSONField(
|
||||
blank=True,
|
||||
default=dict,
|
||||
encoder=utilities.json.CustomFieldJSONEncoder
|
||||
)),
|
||||
('name', models.CharField(max_length=100, unique=True)),
|
||||
('slug', models.SlugField(max_length=100, unique=True)),
|
||||
('description', models.CharField(blank=True, max_length=200)),
|
||||
('color', utilities.fields.ColorField(blank=True, max_length=6)),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'virtual circuit type',
|
||||
'verbose_name_plural': 'virtual circuit types',
|
||||
'ordering': ('name',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='VirtualCircuit',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
(
|
||||
'custom_field_data',
|
||||
models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
|
||||
),
|
||||
('description', models.CharField(blank=True, max_length=200)),
|
||||
('comments', models.TextField(blank=True)),
|
||||
('cid', models.CharField(max_length=100)),
|
||||
('status', models.CharField(default='active', max_length=50)),
|
||||
(
|
||||
'provider_account',
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='virtual_circuits',
|
||||
to='circuits.provideraccount',
|
||||
),
|
||||
),
|
||||
(
|
||||
'provider_network',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='virtual_circuits',
|
||||
to='circuits.providernetwork',
|
||||
),
|
||||
),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
(
|
||||
'type',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='virtual_circuits',
|
||||
to='circuits.virtualcircuittype'
|
||||
)
|
||||
),
|
||||
(
|
||||
'tenant',
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='virtual_circuits',
|
||||
to='tenancy.tenant',
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'circuit',
|
||||
'verbose_name_plural': 'circuits',
|
||||
'ordering': ['provider_network', 'provider_account', 'cid'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='VirtualCircuitTermination',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
(
|
||||
'custom_field_data',
|
||||
models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
|
||||
),
|
||||
('role', models.CharField(default='peer', max_length=50)),
|
||||
('description', models.CharField(blank=True, max_length=200)),
|
||||
(
|
||||
'interface',
|
||||
models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='virtual_circuit_termination',
|
||||
to='dcim.interface',
|
||||
),
|
||||
),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
(
|
||||
'virtual_circuit',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='terminations',
|
||||
to='circuits.virtualcircuit',
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'virtual circuit termination',
|
||||
'verbose_name_plural': 'virtual circuit terminations',
|
||||
'ordering': ['virtual_circuit', 'role', 'pk'],
|
||||
},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='virtualcircuit',
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=('provider_network', 'cid'), name='circuits_virtualcircuit_unique_provider_network_cid'
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='virtualcircuit',
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=('provider_account', 'cid'), name='circuits_virtualcircuit_unique_provideraccount_cid'
|
||||
),
|
||||
),
|
||||
]
|
@ -0,0 +1,85 @@
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def set_member_type(apps, schema_editor):
|
||||
"""
|
||||
Set member_type on any existing CircuitGroupAssignments to the content type for Circuit.
|
||||
"""
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
Circuit = apps.get_model('circuits', 'Circuit')
|
||||
CircuitGroupAssignment = apps.get_model('circuits', 'CircuitGroupAssignment')
|
||||
|
||||
CircuitGroupAssignment.objects.update(
|
||||
member_type=ContentType.objects.get_for_model(Circuit)
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0050_virtual_circuits'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('extras', '0122_charfield_null_choices'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveConstraint(
|
||||
model_name='circuitgroupassignment',
|
||||
name='circuits_circuitgroupassignment_unique_circuit_group',
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='circuitgroupassignment',
|
||||
options={'ordering': ('group', 'member_type', 'member_id', 'priority', 'pk')},
|
||||
),
|
||||
|
||||
# Change member_id to an integer field for the member GFK
|
||||
migrations.RenameField(
|
||||
model_name='circuitgroupassignment',
|
||||
old_name='circuit',
|
||||
new_name='member_id',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuitgroupassignment',
|
||||
name='member_id',
|
||||
field=models.PositiveBigIntegerField(),
|
||||
),
|
||||
|
||||
# Add content type pointer for the member GFK
|
||||
migrations.AddField(
|
||||
model_name='circuitgroupassignment',
|
||||
name='member_type',
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
limit_choices_to=models.Q(('app_label', 'circuits'), ('model__in', ['circuit', 'virtualcircuit'])),
|
||||
related_name='+',
|
||||
to='contenttypes.contenttype',
|
||||
blank=True,
|
||||
null=True
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
|
||||
# Populate member_type for any existing assignments
|
||||
migrations.RunPython(code=set_member_type, reverse_code=migrations.RunPython.noop),
|
||||
|
||||
# Disallow null values for member_type
|
||||
migrations.AlterField(
|
||||
model_name='circuitgroupassignment',
|
||||
name='member_type',
|
||||
field=models.ForeignKey(
|
||||
limit_choices_to=models.Q(('app_label', 'circuits'), ('model__in', ['circuit', 'virtualcircuit'])),
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='+',
|
||||
to='contenttypes.contenttype'
|
||||
),
|
||||
),
|
||||
|
||||
migrations.AddConstraint(
|
||||
model_name='circuitgroupassignment',
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=('member_type', 'member_id', 'group'),
|
||||
name='circuits_circuitgroupassignment_unique_member_group'
|
||||
),
|
||||
),
|
||||
]
|
@ -1,2 +1,3 @@
|
||||
from .circuits import *
|
||||
from .providers import *
|
||||
from .virtual_circuits import *
|
||||
|
23
netbox/circuits/models/base.py
Normal file
23
netbox/circuits/models/base.py
Normal file
@ -0,0 +1,23 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.models import OrganizationalModel
|
||||
from utilities.fields import ColorField
|
||||
|
||||
__all__ = (
|
||||
'BaseCircuitType',
|
||||
)
|
||||
|
||||
|
||||
class BaseCircuitType(OrganizationalModel):
|
||||
"""
|
||||
Abstract base model to represent a type of physical or virtual circuit.
|
||||
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
|
||||
"Long Haul," "Metro," or "Out-of-Band".
|
||||
"""
|
||||
color = ColorField(
|
||||
verbose_name=_('color'),
|
||||
blank=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
@ -1,13 +1,19 @@
|
||||
from django.apps import apps
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
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.features import ContactsMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, ImageAttachmentsMixin, TagsMixin
|
||||
from utilities.fields import ColorField
|
||||
from netbox.models.mixins import DistanceMixin
|
||||
from netbox.models.features import (
|
||||
ContactsMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, ImageAttachmentsMixin, TagsMixin,
|
||||
)
|
||||
from .base import BaseCircuitType
|
||||
|
||||
__all__ = (
|
||||
'Circuit',
|
||||
@ -18,26 +24,18 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class CircuitType(OrganizationalModel):
|
||||
class CircuitType(BaseCircuitType):
|
||||
"""
|
||||
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
|
||||
"Long Haul," "Metro," or "Out-of-Band".
|
||||
"""
|
||||
color = ColorField(
|
||||
verbose_name=_('color'),
|
||||
blank=True
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:circuittype', args=[self.pk])
|
||||
|
||||
class Meta:
|
||||
ordering = ('name',)
|
||||
verbose_name = _('circuit type')
|
||||
verbose_name_plural = _('circuit types')
|
||||
|
||||
|
||||
class Circuit(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
|
||||
class Circuit(ContactsMixin, ImageAttachmentsMixin, DistanceMixin, PrimaryModel):
|
||||
"""
|
||||
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
|
||||
circuits. Each circuit is also assigned a CircuitType and a Site, and may optionally be assigned to a particular
|
||||
@ -61,7 +59,7 @@ class Circuit(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
|
||||
null=True
|
||||
)
|
||||
type = models.ForeignKey(
|
||||
to='CircuitType',
|
||||
to='circuits.CircuitType',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='circuits'
|
||||
)
|
||||
@ -113,6 +111,13 @@ class Circuit(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
|
||||
null=True
|
||||
)
|
||||
|
||||
group_assignments = GenericRelation(
|
||||
to='circuits.CircuitGroupAssignment',
|
||||
content_type_field='member_type',
|
||||
object_id_field='member_id',
|
||||
related_query_name='circuit'
|
||||
)
|
||||
|
||||
clone_fields = (
|
||||
'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate',
|
||||
'description',
|
||||
@ -140,9 +145,6 @@ class Circuit(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
|
||||
def __str__(self):
|
||||
return self.cid
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:circuit', args=[self.pk])
|
||||
|
||||
def get_status_color(self):
|
||||
return CircuitStatusChoices.colors.get(self.status)
|
||||
|
||||
@ -173,21 +175,26 @@ class CircuitGroup(OrganizationalModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:circuitgroup', args=[self.pk])
|
||||
|
||||
|
||||
class CircuitGroupAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):
|
||||
"""
|
||||
Assignment of a Circuit to a CircuitGroup with an optional priority.
|
||||
Assignment of a physical or virtual circuit to a CircuitGroup with an optional priority.
|
||||
"""
|
||||
circuit = models.ForeignKey(
|
||||
Circuit,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='assignments'
|
||||
member_type = models.ForeignKey(
|
||||
to='contenttypes.ContentType',
|
||||
limit_choices_to=CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='+'
|
||||
)
|
||||
member_id = models.PositiveBigIntegerField(
|
||||
verbose_name=_('member ID')
|
||||
)
|
||||
member = GenericForeignKey(
|
||||
ct_field='member_type',
|
||||
fk_field='member_id'
|
||||
)
|
||||
group = models.ForeignKey(
|
||||
CircuitGroup,
|
||||
to='circuits.CircuitGroup',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='assignments'
|
||||
)
|
||||
@ -195,19 +202,19 @@ class CircuitGroupAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin,
|
||||
verbose_name=_('priority'),
|
||||
max_length=50,
|
||||
choices=CircuitPriorityChoices,
|
||||
blank=True
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
prerequisite_models = (
|
||||
'circuits.Circuit',
|
||||
'circuits.CircuitGroup',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ('group', 'circuit', 'priority', 'pk')
|
||||
ordering = ('group', 'member_type', 'member_id', 'priority', 'pk')
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('circuit', 'group'),
|
||||
name='%(app_label)s_%(class)s_unique_circuit_group'
|
||||
fields=('member_type', 'member_id', 'group'),
|
||||
name='%(app_label)s_%(class)s_unique_member_group'
|
||||
),
|
||||
)
|
||||
verbose_name = _('Circuit group assignment')
|
||||
@ -237,22 +244,24 @@ class CircuitTermination(
|
||||
term_side = models.CharField(
|
||||
max_length=1,
|
||||
choices=CircuitTerminationSideChoices,
|
||||
verbose_name=_('termination')
|
||||
verbose_name=_('termination side')
|
||||
)
|
||||
site = models.ForeignKey(
|
||||
to='dcim.Site',
|
||||
termination_type = models.ForeignKey(
|
||||
to='contenttypes.ContentType',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='circuit_terminations',
|
||||
limit_choices_to=Q(model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES),
|
||||
related_name='+',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
provider_network = models.ForeignKey(
|
||||
to='circuits.ProviderNetwork',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='circuit_terminations',
|
||||
termination_id = models.PositiveBigIntegerField(
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
termination = GenericForeignKey(
|
||||
ct_field='termination_type',
|
||||
fk_field='termination_id'
|
||||
)
|
||||
port_speed = models.PositiveIntegerField(
|
||||
verbose_name=_('port speed (Kbps)'),
|
||||
blank=True,
|
||||
@ -283,6 +292,43 @@ class CircuitTermination(
|
||||
blank=True
|
||||
)
|
||||
|
||||
# Cached associations to enable efficient filtering
|
||||
_provider_network = models.ForeignKey(
|
||||
to='circuits.ProviderNetwork',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='circuit_terminations',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
_location = models.ForeignKey(
|
||||
to='dcim.Location',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='circuit_terminations',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
_site = models.ForeignKey(
|
||||
to='dcim.Site',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='circuit_terminations',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
_region = models.ForeignKey(
|
||||
to='dcim.Region',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='circuit_terminations',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
_site_group = models.ForeignKey(
|
||||
to='dcim.SiteGroup',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='circuit_terminations',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['circuit', 'term_side']
|
||||
constraints = (
|
||||
@ -303,11 +349,35 @@ class CircuitTermination(
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Must define either site *or* provider network
|
||||
if self.site is None and self.provider_network is None:
|
||||
raise ValidationError(_("A circuit termination must attach to either a site or a provider network."))
|
||||
if self.site and self.provider_network:
|
||||
raise ValidationError(_("A circuit termination cannot attach to both a site and a provider network."))
|
||||
if self.termination is None:
|
||||
raise ValidationError(_("A circuit termination must attach to a terminating object."))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Cache objects associated with the terminating object (for filtering)
|
||||
self.cache_related_objects()
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def cache_related_objects(self):
|
||||
self._provider_network = self._region = self._site_group = self._site = self._location = None
|
||||
if self.termination_type:
|
||||
termination_type = self.termination_type.model_class()
|
||||
if termination_type == apps.get_model('dcim', 'region'):
|
||||
self._region = self.termination
|
||||
elif termination_type == apps.get_model('dcim', 'sitegroup'):
|
||||
self._site_group = self.termination
|
||||
elif termination_type == apps.get_model('dcim', 'site'):
|
||||
self._region = self.termination.region
|
||||
self._site_group = self.termination.group
|
||||
self._site = self.termination
|
||||
elif termination_type == apps.get_model('dcim', 'location'):
|
||||
self._region = self.termination.site.region
|
||||
self._site_group = self.termination.site.group
|
||||
self._site = self.termination.site
|
||||
self._location = self.termination
|
||||
elif termination_type == apps.get_model('circuits', 'providernetwork'):
|
||||
self._provider_network = self.termination
|
||||
cache_related_objects.alters_data = True
|
||||
|
||||
def to_objectchange(self, action):
|
||||
objectchange = super().to_objectchange(action)
|
||||
@ -321,7 +391,7 @@ class CircuitTermination(
|
||||
def get_peer_termination(self):
|
||||
peer_side = 'Z' if self.term_side == 'A' else 'A'
|
||||
try:
|
||||
return CircuitTermination.objects.prefetch_related('site').get(
|
||||
return CircuitTermination.objects.prefetch_related('termination').get(
|
||||
circuit=self.circuit,
|
||||
term_side=peer_side
|
||||
)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user