mirror of
https://github.com/netbox-community/netbox.git
synced 2026-02-04 06:16:23 -06:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6211a21423 |
+1
-1
@@ -15,7 +15,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox version
|
label: NetBox version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v4.5.1
|
placeholder: v4.4.7
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox Version
|
label: NetBox Version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v4.5.1
|
placeholder: v4.4.7
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
|||||||
-43
@@ -1,43 +0,0 @@
|
|||||||
---
|
|
||||||
name: 🏁 Performance
|
|
||||||
type: Performance
|
|
||||||
description: An opportunity to improve application performance
|
|
||||||
labels: ["netbox", "type: performance", "status: needs triage"]
|
|
||||||
body:
|
|
||||||
- type: input
|
|
||||||
attributes:
|
|
||||||
label: NetBox Version
|
|
||||||
description: What version of NetBox are you currently running?
|
|
||||||
placeholder: v4.5.1
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: dropdown
|
|
||||||
attributes:
|
|
||||||
label: Python Version
|
|
||||||
description: What version of Python are you currently running?
|
|
||||||
options:
|
|
||||||
- "3.12"
|
|
||||||
- "3.13"
|
|
||||||
- "3.14"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: checkboxes
|
|
||||||
attributes:
|
|
||||||
label: Area(s) of Concern
|
|
||||||
description: Which application interface(s) are affected?
|
|
||||||
options:
|
|
||||||
- label: User Interface
|
|
||||||
- label: REST API
|
|
||||||
- label: GraphQL API
|
|
||||||
- label: Python ORM
|
|
||||||
- label: Other
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Details
|
|
||||||
description: >
|
|
||||||
Describe in detail the operations being performed and the indications of a performance issue.
|
|
||||||
Include any relevant testing parameters, benchmarks, and expected results.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
name: 🗑️ Deprecation
|
||||||
|
type: Deprecation
|
||||||
|
description: The removal of an existing feature or resource
|
||||||
|
labels: ["netbox", "type: deprecation"]
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Proposed Changes
|
||||||
|
description: >
|
||||||
|
Describe in detail the proposed changes. What is being removed?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Justification
|
||||||
|
description: Please provide justification for the proposed change(s).
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Impact
|
||||||
|
description: List all areas of the application that will be affected by this change.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
-31
@@ -1,31 +0,0 @@
|
|||||||
---
|
|
||||||
name: ⚠️ Deprecation
|
|
||||||
type: Deprecation
|
|
||||||
description: Designation of a feature or behavior that will be removed in a future release
|
|
||||||
labels: ["netbox", "type: deprecation"]
|
|
||||||
body:
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Deprecated Functionality
|
|
||||||
description: >
|
|
||||||
Describe the feature(s) and/or behavior that is being flagged for deprecation.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: input
|
|
||||||
attributes:
|
|
||||||
label: Scheduled removal
|
|
||||||
description: In what future release will the deprecated functionality be removed?
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Justification
|
|
||||||
description: Please provide justification for the deprecation.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Impact
|
|
||||||
description: List all areas of the application that will be affected by this change.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
-20
@@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
name: 🗑️ Feature Removal
|
|
||||||
type: Removal
|
|
||||||
description: The removal of a deprecated feature or resource
|
|
||||||
labels: ["netbox", "type: removal"]
|
|
||||||
body:
|
|
||||||
- type: input
|
|
||||||
attributes:
|
|
||||||
label: Deprecation Issue
|
|
||||||
description: Specify the issue in which this deprecation was announced.
|
|
||||||
placeholder: "#1234"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Summary of Changes
|
|
||||||
description: >
|
|
||||||
List all changes necessary to remove the deprecated feature or resource.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
Vendored
+1
-1
@@ -31,7 +31,7 @@ jobs:
|
|||||||
NETBOX_CONFIGURATION: netbox.configuration_testing
|
NETBOX_CONFIGURATION: netbox.configuration_testing
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ['3.12', '3.13', '3.14']
|
python-version: ['3.12', '3.13']
|
||||||
node-version: ['20.x']
|
node-version: ['20.x']
|
||||||
services:
|
services:
|
||||||
redis:
|
redis:
|
||||||
|
|||||||
Vendored
+2
-2
@@ -30,13 +30,13 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v4
|
uses: github/codeql-action/init@v3
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
build-mode: ${{ matrix.build-mode }}
|
build-mode: ${{ matrix.build-mode }}
|
||||||
config-file: .github/codeql/codeql-config.yml
|
config-file: .github/codeql/codeql-config.yml
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v4
|
uses: github/codeql-action/analyze@v3
|
||||||
with:
|
with:
|
||||||
category: "/language:${{matrix.language}}"
|
category: "/language:${{matrix.language}}"
|
||||||
|
|||||||
+1
-1
@@ -34,7 +34,7 @@ jobs:
|
|||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.12
|
python-version: 3.11
|
||||||
|
|
||||||
- name: Install system dependencies
|
- name: Install system dependencies
|
||||||
run: sudo apt install -y gettext
|
run: sudo apt install -y gettext
|
||||||
|
|||||||
Vendored
+1
-2
@@ -9,8 +9,7 @@ yarn-error.log*
|
|||||||
/netbox/netbox/configuration.py
|
/netbox/netbox/configuration.py
|
||||||
/netbox/netbox/ldap_config.py
|
/netbox/netbox/ldap_config.py
|
||||||
/netbox/local/*
|
/netbox/local/*
|
||||||
/netbox/media/*
|
/netbox/media
|
||||||
!/netbox/media/.gitkeep
|
|
||||||
/netbox/reports/*
|
/netbox/reports/*
|
||||||
!/netbox/reports/__init__.py
|
!/netbox/reports/__init__.py
|
||||||
/netbox/scripts/*
|
/netbox/scripts/*
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<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/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/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://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-16-blue" alt="Languages supported" /></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/actions/workflows/ci.yml/badge.svg" alt="CI status" /></a>
|
<a href="https://github.com/netbox-community/netbox/actions/workflows/ci.yml"><img src="https://github.com/netbox-community/netbox/actions/workflows/ci.yml/badge.svg" alt="CI status" /></a>
|
||||||
<p>
|
<p>
|
||||||
<strong><a href="https://netboxlabs.com/community/">NetBox Community</a></strong> |
|
<strong><a href="https://netboxlabs.com/community/">NetBox Community</a></strong> |
|
||||||
|
|||||||
+1323
-9619
File diff suppressed because it is too large
Load Diff
@@ -3,43 +3,31 @@
|
|||||||
NetBox includes a Python management shell within which objects can be directly queried, created, modified, and deleted. To enter the shell, run the following command:
|
NetBox includes a Python management shell within which objects can be directly queried, created, modified, and deleted. To enter the shell, run the following command:
|
||||||
|
|
||||||
```
|
```
|
||||||
cd /opt/netbox
|
./manage.py nbshell
|
||||||
source /opt/netbox/venv/bin/activate
|
|
||||||
python3 netbox/manage.py nbshell
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This will launch a lightly customized version of [the built-in Django shell](https://docs.djangoproject.com/en/stable/ref/django-admin/#shell) with all relevant NetBox models preloaded. (If desired, the stock Django shell is also available by executing `./manage.py shell`.)
|
This will launch a lightly customized version of [the built-in Django shell](https://docs.djangoproject.com/en/stable/ref/django-admin/#shell) with all relevant NetBox models pre-loaded. (If desired, the stock Django shell is also available by executing `./manage.py shell`.)
|
||||||
|
|
||||||
```
|
```
|
||||||
(venv) $ python3 netbox/manage.py nbshell
|
$ ./manage.py nbshell
|
||||||
### NetBox interactive shell (localhost)
|
### NetBox interactive shell (localhost)
|
||||||
### Python v3.12.3 | Django v5.2.10 | NetBox Community v4.5.1
|
### Python 3.7.10 | Django 3.2.5 | NetBox 3.0
|
||||||
### lsapps() & lsmodels() will show available models. Use help(<model>) for more info.
|
### lsmodels() will show available models. Use help(<model>) for more info.
|
||||||
```
|
```
|
||||||
|
|
||||||
The function `lsmodels()` will print a list of all available NetBox models:
|
The function `lsmodels()` will print a list of all available NetBox models:
|
||||||
|
|
||||||
```
|
```
|
||||||
>>> lsmodels()
|
>>> lsmodels()
|
||||||
...
|
|
||||||
DCIM:
|
DCIM:
|
||||||
dcim.Cable
|
ConsolePort
|
||||||
dcim.CableTermination
|
ConsolePortTemplate
|
||||||
dcim.ConsolePort
|
ConsoleServerPort
|
||||||
dcim.ConsolePortTemplate
|
ConsoleServerPortTemplate
|
||||||
dcim.ConsoleServerPort
|
Device
|
||||||
dcim.ConsoleServerPortTemplate
|
|
||||||
dcim.Device
|
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
To exit the NetBox shell, type `exit()` or press `Ctrl+D`.
|
|
||||||
|
|
||||||
```
|
|
||||||
>>> exit()
|
|
||||||
(venv) $
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
The NetBox shell affords direct access to NetBox data and function with very little validation in place. As such, it is crucial to ensure that only authorized, knowledgeable users are ever granted access to it. Never perform any action in the management shell without having a full backup in place.
|
The NetBox shell affords direct access to NetBox data and function with very little validation in place. As such, it is crucial to ensure that only authorized, knowledgeable users are ever granted access to it. Never perform any action in the management shell without having a full backup in place.
|
||||||
|
|
||||||
@@ -126,7 +114,7 @@ Reverse relationships can be traversed as well. For example, the following will
|
|||||||
>>> Device.objects.filter(interfaces__name="em0")
|
>>> Device.objects.filter(interfaces__name="em0")
|
||||||
```
|
```
|
||||||
|
|
||||||
Character fields can be filtered against partial matches using the `contains` or `icontains` field lookup (the latter of which is case-insensitive).
|
Character fields can be filtered against partial matches using the `contains` or `icontains` field lookup (the later of which is case-insensitive).
|
||||||
|
|
||||||
```
|
```
|
||||||
>>> Device.objects.filter(name__icontains="testdevice")
|
>>> Device.objects.filter(name__icontains="testdevice")
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ While permissions are typically assigned to specific groups and/or users, it is
|
|||||||
|
|
||||||
### Viewing Objects
|
### Viewing Objects
|
||||||
|
|
||||||
Object-based permissions work by filtering the database query generated by a user's request to restrict the set of objects returned. When a request is received, NetBox first determines whether the user is authenticated and has been granted permission to perform the requested action. For example, if the requested URL is `/dcim/devices/`, NetBox will check for the `dcim.view_device` permission. If the user has not been assigned this permission (either directly or via a group assignment), NetBox will return a 403 (forbidden) HTTP response.
|
Object-based permissions work by filtering the database query generated by a user's request to restrict the set of objects returned. When a request is received, NetBox first determines whether the user is authenticated and has been granted to perform the requested action. For example, if the requested URL is `/dcim/devices/`, NetBox will check for the `dcim.view_device` permission. If the user has not been assigned this permission (either directly or via a group assignment), NetBox will return a 403 (forbidden) HTTP response.
|
||||||
|
|
||||||
If the permission _has_ been granted, NetBox will compile any specified constraints for the model and action. For example, suppose two permissions have been assigned to the user granting view access to the device model, with the following constraints:
|
If the permission _has_ been granted, NetBox will compile any specified constraints for the model and action. For example, suppose two permissions have been assigned to the user granting view access to the device model, with the following constraints:
|
||||||
|
|
||||||
@@ -102,9 +102,9 @@ If the permission _has_ been granted, NetBox will compile any specified constrai
|
|||||||
This grants the user access to view any device that is assigned to a site named NYC1 or NYC2, **or** which has a status of "offline" and has no tenant assigned. These constraints are equivalent to the following ORM query:
|
This grants the user access to view any device that is assigned to a site named NYC1 or NYC2, **or** which has a status of "offline" and has no tenant assigned. These constraints are equivalent to the following ORM query:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
Device.objects.filter(
|
Site.objects.filter(
|
||||||
Q(site__name__in=['NYC1', 'NYC2']),
|
Q(site__name__in=['NYC1', 'NYC2']),
|
||||||
Q(status='offline', tenant__isnull=True)
|
Q(status='active', tenant__isnull=True)
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ This is a mapping of models to [custom validators](../customization/custom-valid
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
CUSTOM_VALIDATORS = {
|
CUSTOM_VALIDATORS = {
|
||||||
"dcim.Site": [
|
"dcim.site": [
|
||||||
{
|
{
|
||||||
"name": {
|
"name": {
|
||||||
"min_length": 5,
|
"min_length": 5,
|
||||||
@@ -17,15 +17,12 @@ CUSTOM_VALIDATORS = {
|
|||||||
},
|
},
|
||||||
"my_plugin.validators.Validator1"
|
"my_plugin.validators.Validator1"
|
||||||
],
|
],
|
||||||
"dcim.Device": [
|
"dcim.device": [
|
||||||
"my_plugin.validators.Validator1"
|
"my_plugin.validators.Validator1"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! info "Case-Insensitive Model Names"
|
|
||||||
Model identifiers are case-insensitive. Both `dcim.site` and `dcim.Site` are valid and equivalent.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## FIELD_CHOICES
|
## FIELD_CHOICES
|
||||||
@@ -56,9 +53,6 @@ FIELD_CHOICES = {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! info "Case-Insensitive Field Identifiers"
|
|
||||||
Field identifiers are case-insensitive. Both `dcim.Site.status` and `dcim.site.status` are valid and equivalent.
|
|
||||||
|
|
||||||
The following model fields support configurable choices:
|
The following model fields support configurable choices:
|
||||||
|
|
||||||
* `circuits.Circuit.status`
|
* `circuits.Circuit.status`
|
||||||
@@ -104,7 +98,7 @@ This is a mapping of models to [custom validators](../customization/custom-valid
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
PROTECTION_RULES = {
|
PROTECTION_RULES = {
|
||||||
"dcim.Site": [
|
"dcim.site": [
|
||||||
{
|
{
|
||||||
"status": {
|
"status": {
|
||||||
"eq": "decommissioning"
|
"eq": "decommissioning"
|
||||||
@@ -114,6 +108,3 @@ PROTECTION_RULES = {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! info "Case-Insensitive Model Names"
|
|
||||||
Model identifiers are case-insensitive. Both `dcim.site` and `dcim.Site` are valid and equivalent.
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ Some configuration parameters may alternatively be defined either in `configurat
|
|||||||
|
|
||||||
## Dynamic Configuration Parameters
|
## Dynamic Configuration Parameters
|
||||||
|
|
||||||
Some configuration parameters are primarily controlled via NetBox's admin interface (under Admin > System > Configuration History). These are noted where applicable in the documentation. These settings may also be overridden in `configuration.py` to prevent them from being modified via the UI. A complete list of supported parameters is provided below:
|
Some configuration parameters are primarily controlled via NetBox's admin interface (under Admin > Extras > Configuration Revisions). These are noted where applicable in the documentation. These settings may also be overridden in `configuration.py` to prevent them from being modified via the UI. A complete list of supported parameters is provided below:
|
||||||
|
|
||||||
* [`ALLOWED_URL_SCHEMES`](./security.md#allowed_url_schemes)
|
* [`ALLOWED_URL_SCHEMES`](./security.md#allowed_url_schemes)
|
||||||
* [`BANNER_BOTTOM`](./miscellaneous.md#banner_bottom)
|
* [`BANNER_BOTTOM`](./miscellaneous.md#banner_bottom)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ Depending on its classification, each NetBox model may support various features
|
|||||||
|
|
||||||
| Feature | Feature Mixin | Registry Key | Description |
|
| Feature | Feature Mixin | Registry Key | Description |
|
||||||
|------------------------------------------------------------|-------------------------|---------------------|-----------------------------------------------------------------------------------------|
|
|------------------------------------------------------------|-------------------------|---------------------|-----------------------------------------------------------------------------------------|
|
||||||
| [Bookmarks](../features/user-preferences.md#bookmarks) | `BookmarksMixin` | `bookmarks` | These models can be bookmarked natively in the user interface |
|
| [Bookmarks](../features/customization.md#bookmarks) | `BookmarksMixin` | `bookmarks` | These models can be bookmarked natively in the user interface |
|
||||||
| [Change logging](../features/change-logging.md) | `ChangeLoggingMixin` | `change_logging` | Changes to these objects are automatically recorded in the change log |
|
| [Change logging](../features/change-logging.md) | `ChangeLoggingMixin` | `change_logging` | Changes to these objects are automatically recorded in the change log |
|
||||||
| Cloning | `CloningMixin` | `cloning` | Provides the `clone()` method to prepare a copy |
|
| Cloning | `CloningMixin` | `cloning` | Provides the `clone()` method to prepare a copy |
|
||||||
| [Contacts](../features/contacts.md) | `ContactsMixin` | `contacts` | Contacts can be associated with these models |
|
| [Contacts](../features/contacts.md) | `ContactsMixin` | `contacts` | Contacts can be associated with these models |
|
||||||
|
|||||||
@@ -10,11 +10,9 @@ Change records are exposed in the API via the read-only endpoint `/api/extras/ob
|
|||||||
|
|
||||||
## User Messages
|
## User Messages
|
||||||
|
|
||||||
When creating, modifying, or deleting an object in NetBox, a user has the option of recording an arbitrary message (up to 200 characters) that will appear in the change record. This can be helpful to capture additional context, such as the reason for a change or a reference to an external ticket.
|
!!! info "This feature was introduced in NetBox v4.4."
|
||||||
|
|
||||||
When editing an object via the web UI, the "Changelog message" field appears at the bottom of the form. This field is optional. The changelog message field is available in object create forms, object edit forms, delete confirmation dialogs, and bulk operations.
|
When creating, modifying, or deleting an object in NetBox, a user has the option of recording an arbitrary message that will appear in the change record. This can be helpful to capture additional context, such as the reason for the change.
|
||||||
|
|
||||||
For information on including changelog messages when making changes via the REST API, see [Changelog Messages](../integrations/rest-api.md#changelog-messages).
|
|
||||||
|
|
||||||
## Correlating Changes by Request
|
## Correlating Changes by Request
|
||||||
|
|
||||||
|
|||||||
@@ -51,14 +51,14 @@ You can verify that authentication works by executing the `psql` command and pas
|
|||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
$ psql --username netbox --password --host localhost netbox
|
$ psql --username netbox --password --host localhost netbox
|
||||||
Password:
|
Password for user netbox:
|
||||||
psql (16.11 (Ubuntu 16.11-0ubuntu0.24.04.1))
|
psql (12.5 (Ubuntu 12.5-0ubuntu0.20.04.1))
|
||||||
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
|
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
|
||||||
Type "help" for help.
|
Type "help" for help.
|
||||||
|
|
||||||
netbox=> \conninfo
|
netbox=> \conninfo
|
||||||
You are connected to database "netbox" as user "netbox" on host "localhost" (address "127.0.0.1") at port "5432".
|
You are connected to database "netbox" as user "netbox" on host "localhost" (address "127.0.0.1") at port "5432".
|
||||||
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
|
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
|
||||||
netbox=> \q
|
netbox=> \q
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ sudo ln -s /opt/netbox-X.Y.Z/ /opt/netbox
|
|||||||
```
|
```
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
It is recommended to install NetBox in a directory named for its version number. For example, NetBox v4.0.0 would be installed into `/opt/netbox-4.0.0`, and a symlink from `/opt/netbox/` would point to this location. (You can verify this configuration with the command `ls -l /opt | grep netbox`.) This allows for future releases to be installed in parallel without interrupting the current installation. When changing to the new release, only the symlink needs to be updated.
|
It is recommended to install NetBox in a directory named for its version number. For example, NetBox v3.0.0 would be installed into `/opt/netbox-3.0.0`, and a symlink from `/opt/netbox/` would point to this location. (You can verify this configuration with the command `ls -l /opt | grep netbox`.) This allows for future releases to be installed in parallel without interrupting the current installation. When changing to the new release, only the symlink needs to be updated.
|
||||||
|
|
||||||
### Option B: Clone the Git Repository
|
### Option B: Clone the Git Repository
|
||||||
|
|
||||||
@@ -63,12 +63,12 @@ This command should generate output similar to the following:
|
|||||||
|
|
||||||
```
|
```
|
||||||
Cloning into '.'...
|
Cloning into '.'...
|
||||||
remote: Enumerating objects: 148317, done.
|
remote: Enumerating objects: 996, done.
|
||||||
remote: Counting objects: 100% (183/183), done.
|
remote: Counting objects: 100% (996/996), done.
|
||||||
remote: Compressing objects: 100% (115/115), done.
|
remote: Compressing objects: 100% (935/935), done.
|
||||||
remote: Total 148317 (delta 127), reused 68 (delta 68), pack-reused 148134 (from 3)
|
remote: Total 996 (delta 148), reused 386 (delta 34), pack-reused 0
|
||||||
Receiving objects: 100% (148317/148317), 165.12 MiB | 28.71 MiB/s, done.
|
Receiving objects: 100% (996/996), 4.26 MiB | 9.81 MiB/s, done.
|
||||||
Resolving deltas: 100% (116428/116428), done.
|
Resolving deltas: 100% (148/148), done.
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
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.
|
||||||
@@ -102,8 +102,7 @@ sudo cp configuration_example.py configuration.py
|
|||||||
Open `configuration.py` with your preferred editor to begin configuring NetBox. NetBox offers [many configuration parameters](../configuration/index.md), but only the following four are required for new installations:
|
Open `configuration.py` with your preferred editor to begin configuring NetBox. NetBox offers [many configuration parameters](../configuration/index.md), but only the following four are required for new installations:
|
||||||
|
|
||||||
* `ALLOWED_HOSTS`
|
* `ALLOWED_HOSTS`
|
||||||
* `API_TOKEN_PEPPERS`
|
* `DATABASES` (or `DATABASE`)
|
||||||
* `DATABASES`
|
|
||||||
* `REDIS`
|
* `REDIS`
|
||||||
* `SECRET_KEY`
|
* `SECRET_KEY`
|
||||||
|
|
||||||
@@ -159,7 +158,7 @@ DATABASES = {
|
|||||||
|
|
||||||
### REDIS
|
### REDIS
|
||||||
|
|
||||||
Redis is an in-memory key-value store used by NetBox for caching and background task queuing. Redis typically requires minimal configuration; the values below should suffice for most installations. See the [configuration documentation](../configuration/required-parameters.md#redis) for more detail on individual parameters.
|
Redis is a in-memory key-value store used by NetBox for caching and background task queuing. Redis typically requires minimal configuration; the values below should suffice for most installations. See the [configuration documentation](../configuration/required-parameters.md#redis) for more detail on individual parameters.
|
||||||
|
|
||||||
Note that NetBox requires the specification of two separate Redis databases: `tasks` and `caching`. These may both be provided by the same Redis service, however each should have a unique numeric database ID.
|
Note that NetBox requires the specification of two separate Redis databases: `tasks` and `caching`. These may both be provided by the same Redis service, however each should have a unique numeric database ID.
|
||||||
|
|
||||||
@@ -253,7 +252,7 @@ Once NetBox has been configured, we're ready to proceed with the actual installa
|
|||||||
sudo /opt/netbox/upgrade.sh
|
sudo /opt/netbox/upgrade.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that **Python 3.12 or later is required** for NetBox v4.5 and later releases. If the default Python installation on your server is set to a lesser version, pass the path to the supported installation as an environment variable named `PYTHON`. (Note that the environment variable must be passed _after_ the `sudo` command.)
|
Note that **Python 3.12 or later is required** for NetBox v4.5 and later releases. If the default Python installation on your server is set to a lesser version, pass the path to the supported installation as an environment variable named `PYTHON`. (Note that the environment variable must be passed _after_ the `sudo` command.)
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
sudo PYTHON=/usr/bin/python3.12 /opt/netbox/upgrade.sh
|
sudo PYTHON=/usr/bin/python3.12 /opt/netbox/upgrade.sh
|
||||||
@@ -296,12 +295,13 @@ python3 manage.py runserver 0.0.0.0:8000 --insecure
|
|||||||
If successful, you should see output similar to the following:
|
If successful, you should see output similar to the following:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
|
Watching for file changes with StatReloader
|
||||||
Performing system checks...
|
Performing system checks...
|
||||||
|
|
||||||
System check identified no issues (0 silenced).
|
System check identified no issues (0 silenced).
|
||||||
January 26, 2026 - 17:00:00
|
August 30, 2021 - 18:02:23
|
||||||
Django version 5.2.10, using settings 'netbox.settings'
|
Django version 3.2.6, using settings 'netbox.settings'
|
||||||
Starting development server at http://0.0.0.0:8000/
|
Starting development server at http://127.0.0.1:8000/
|
||||||
Quit the server with CONTROL-C.
|
Quit the server with CONTROL-C.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -43,22 +43,16 @@ You should see output similar to the following:
|
|||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
● netbox.service - NetBox WSGI Service
|
● netbox.service - NetBox WSGI Service
|
||||||
Loaded: loaded (/etc/systemd/system/netbox.service; enabled; preset: enabled)
|
Loaded: loaded (/etc/systemd/system/netbox.service; enabled; vendor preset: enabled)
|
||||||
Active: active (running) since Mon 2026-01-26 11:00:00 CST; 7s ago
|
Active: active (running) since Mon 2021-08-30 04:02:36 UTC; 14h ago
|
||||||
Docs: https://docs.netbox.dev/
|
Docs: https://docs.netbox.dev/
|
||||||
Main PID: 7283 (gunicorn)
|
Main PID: 1140492 (gunicorn)
|
||||||
Tasks: 6 (limit: 4545)
|
Tasks: 19 (limit: 4683)
|
||||||
Memory: 556.1M (peak: 556.3M)
|
Memory: 666.2M
|
||||||
CPU: 3.387s
|
|
||||||
CGroup: /system.slice/netbox.service
|
CGroup: /system.slice/netbox.service
|
||||||
├─7283 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/netbox>
|
├─1140492 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /va>
|
||||||
├─7285 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/netbox>
|
├─1140513 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /va>
|
||||||
├─7286 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/netbox>
|
├─1140514 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /va>
|
||||||
├─7287 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/netbox>
|
|
||||||
├─7288 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/netbox>
|
|
||||||
└─7289 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/netbox>
|
|
||||||
|
|
||||||
Jan 26 11:00:00 netbox systemd[1]: Started netbox.service - NetBox WSGI Service.
|
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
This documentation provides example configurations for both [nginx](https://www.nginx.com/resources/wiki/) and [Apache](https://httpd.apache.org/docs/current/), though any HTTP server which supports WSGI should be compatible.
|
This documentation provides example configurations for both [nginx](https://www.nginx.com/resources/wiki/) and [Apache](https://httpd.apache.org/docs/current/), though any HTTP server which supports WSGI should be compatible.
|
||||||
|
|
||||||
!!! info
|
!!! info
|
||||||
For the sake of brevity, only Ubuntu 24.04 instructions are provided here. These tasks are not unique to NetBox and should carry over to other distributions with minimal changes. Please consult your distribution's documentation for assistance if needed.
|
For the sake of brevity, only Ubuntu 20.04 instructions are provided here. These tasks are not unique to NetBox and should carry over to other distributions with minimal changes. Please consult your distribution's documentation for assistance if needed.
|
||||||
|
|
||||||
## Obtain an SSL Certificate
|
## Obtain an SSL Certificate
|
||||||
|
|
||||||
|
|||||||
@@ -12,12 +12,12 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
The installation instructions provided here have been tested to work on Ubuntu 24.04. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
|
The installation instructions provided here have been tested to work on Ubuntu 22.04. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
|
||||||
|
|
||||||
The following sections detail how to set up a new instance of NetBox:
|
The following sections detail how to set up a new instance of NetBox:
|
||||||
|
|
||||||
1. [PostgreSQL database](1-postgresql.md)
|
1. [PostgreSQL database](1-postgresql.md)
|
||||||
2. [Redis](2-redis.md)
|
1. [Redis](2-redis.md)
|
||||||
3. [NetBox components](3-netbox.md)
|
3. [NetBox components](3-netbox.md)
|
||||||
4. [Gunicorn](4a-gunicorn.md) or [uWSGI](4b-uwsgi.md)
|
4. [Gunicorn](4a-gunicorn.md) or [uWSGI](4b-uwsgi.md)
|
||||||
5. [HTTP server](5-http-server.md)
|
5. [HTTP server](5-http-server.md)
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ Download and extract the latest version:
|
|||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# Set $NEWVER to the NetBox version being installed
|
# Set $NEWVER to the NetBox version being installed
|
||||||
NEWVER=4.5.0
|
NEWVER=3.5.0
|
||||||
wget https://github.com/netbox-community/netbox/archive/v$NEWVER.tar.gz
|
wget https://github.com/netbox-community/netbox/archive/v$NEWVER.tar.gz
|
||||||
sudo tar -xzf v$NEWVER.tar.gz -C /opt
|
sudo tar -xzf v$NEWVER.tar.gz -C /opt
|
||||||
sudo ln -sfn /opt/netbox-$NEWVER/ /opt/netbox
|
sudo ln -sfn /opt/netbox-$NEWVER/ /opt/netbox
|
||||||
@@ -75,7 +75,7 @@ Copy `local_requirements.txt`, `configuration.py`, and `ldap_config.py` (if pres
|
|||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# Set $OLDVER to the NetBox version currently installed
|
# Set $OLDVER to the NetBox version currently installed
|
||||||
OLDVER=4.4.10
|
OLDVER=3.4.9
|
||||||
sudo cp /opt/netbox-$OLDVER/local_requirements.txt /opt/netbox/
|
sudo cp /opt/netbox-$OLDVER/local_requirements.txt /opt/netbox/
|
||||||
sudo cp /opt/netbox-$OLDVER/netbox/netbox/configuration.py /opt/netbox/netbox/netbox/
|
sudo cp /opt/netbox-$OLDVER/netbox/netbox/configuration.py /opt/netbox/netbox/netbox/
|
||||||
sudo cp /opt/netbox-$OLDVER/netbox/netbox/ldap_config.py /opt/netbox/netbox/netbox/
|
sudo cp /opt/netbox-$OLDVER/netbox/netbox/ldap_config.py /opt/netbox/netbox/netbox/
|
||||||
@@ -116,7 +116,7 @@ Check out the desired release by specifying its tag. For example:
|
|||||||
```
|
```
|
||||||
cd /opt/netbox && \
|
cd /opt/netbox && \
|
||||||
sudo git fetch --tags && \
|
sudo git fetch --tags && \
|
||||||
sudo git checkout v4.5.0
|
sudo git checkout v4.2.7
|
||||||
```
|
```
|
||||||
|
|
||||||
## 4. Run the Upgrade Script
|
## 4. Run the Upgrade Script
|
||||||
@@ -128,7 +128,7 @@ sudo ./upgrade.sh
|
|||||||
```
|
```
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
If the default version of Python is not **at least 3.12**, you'll need to pass the path to a supported Python version as an environment variable when calling the upgrade script. For example:
|
If the default version of Python is not at least 3.10, you'll need to pass the path to a supported Python version as an environment variable when calling the upgrade script. For example:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
sudo PYTHON=/usr/bin/python3.12 ./upgrade.sh
|
sudo PYTHON=/usr/bin/python3.12 ./upgrade.sh
|
||||||
|
|||||||
@@ -215,51 +215,9 @@ http://netbox/api/ipam/ip-addresses/ \
|
|||||||
|
|
||||||
If we wanted to assign this IP address to a virtual machine interface instead, we would have set `assigned_object_type` to `virtualization.vminterface` and updated the object ID appropriately.
|
If we wanted to assign this IP address to a virtual machine interface instead, we would have set `assigned_object_type` to `virtualization.vminterface` and updated the object ID appropriately.
|
||||||
|
|
||||||
### Specifying Fields
|
### Brief Format
|
||||||
|
|
||||||
A REST API response will include all available fields for the object type by default. If you wish to return only a subset of the available fields, you can append `?fields=` to the URL followed by a comma-separated list of field names. For example, the following request will return only the `id`, `name`, `status`, and `region` fields for each site in the response.
|
Most API endpoints support an optional "brief" format, which returns only a minimal representation of each object in the response. This is useful when you need only a list of available objects without any related data, such as when populating a drop-down list in a form. As an example, the default (complete) format of a prefix looks like this:
|
||||||
|
|
||||||
```
|
|
||||||
GET /api/dcim/sites/?fields=id,name,status,region
|
|
||||||
```
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"name": "DM-NYC",
|
|
||||||
"status": {
|
|
||||||
"value": "active",
|
|
||||||
"label": "Active"
|
|
||||||
},
|
|
||||||
"region": {
|
|
||||||
"id": 43,
|
|
||||||
"url": "http://netbox:8000/api/dcim/regions/43/",
|
|
||||||
"display": "New York",
|
|
||||||
"name": "New York",
|
|
||||||
"slug": "us-ny",
|
|
||||||
"description": "",
|
|
||||||
"site_count": 0,
|
|
||||||
"_depth": 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Similarly, you can opt to omit only specific fields by passing the `omit` parameter:
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /api/dcim/sites/?omit=circuit_count,device_count,virtualmachine_count
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! note "The `omit` parameter was introduced in NetBox v4.5.2."
|
|
||||||
|
|
||||||
Strategic use of the `fields` and `omit` parameters can drastically improve REST API performance, as the exclusion of fields which reference related objects reduces the number and complexity of underlying database queries needed to generate the response.
|
|
||||||
|
|
||||||
!!! note
|
|
||||||
The `fields` and `omit` parameters should be considered mutually exclusive. If both are passed, `fields` takes precedence.
|
|
||||||
|
|
||||||
#### Brief Format
|
|
||||||
|
|
||||||
Most API endpoints support an optional "brief" format, which returns only a minimal representation of each object in the response. This is useful when you need only a list of available objects without any related data, such as when populating a drop-down list in a form. It's also more convenient than listing out individual fields via the `fields` or `omit` parameters. As an example, the default (complete) format of a prefix looks like this:
|
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
GET /api/ipam/prefixes/13980/
|
GET /api/ipam/prefixes/13980/
|
||||||
@@ -312,10 +270,10 @@ GET /api/ipam/prefixes/13980/
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The brief format includes only a few fields:
|
The brief format is much more terse:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
GET /api/ipam/prefixes/13980/?brief=true
|
GET /api/ipam/prefixes/13980/?brief=1
|
||||||
```
|
```
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -652,7 +610,9 @@ http://netbox/api/dcim/sites/ \
|
|||||||
|
|
||||||
## Changelog Messages
|
## Changelog Messages
|
||||||
|
|
||||||
Most objects in NetBox support [change logging](../features/change-logging.md), which generates a detailed record each time an object is created, modified, or deleted. Additionally, users can attach a message to the change record as well. This is accomplished via the REST API by including a `changelog_message` field in the object representation.
|
!!! info "This feature was introduced in NetBox v4.4."
|
||||||
|
|
||||||
|
Most objects in NetBox support [change logging](../features/change-logging.md), which generates a detailed record each time an object is created, modified, or deleted. Beginning in NetBox v4.4, users can attach a message to the change record as well. This is accomplished via the REST API by including a `changelog_message` field in the object representation.
|
||||||
|
|
||||||
For example, the following API request will create a new site and record a message in the resulting changelog entry:
|
For example, the following API request will create a new site and record a message in the resulting changelog entry:
|
||||||
|
|
||||||
@@ -668,7 +628,7 @@ http://netbox/api/dcim/sites/ \
|
|||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
This approach works when creating, modifying, or deleting objects, either individually or in bulk. For more information about change logging, see [Change Logging](../features/change-logging.md).
|
This approach works when creating, modifying, or deleting objects, either individually or in bulk.
|
||||||
|
|
||||||
## Uploading Files
|
## Uploading Files
|
||||||
|
|
||||||
|
|||||||
@@ -32,14 +32,6 @@ class MyFilterSet(NetBoxModelFilterSet):
|
|||||||
fields = ('some', 'other', 'fields')
|
fields = ('some', 'other', 'fields')
|
||||||
```
|
```
|
||||||
|
|
||||||
In addition to the base NetBoxModelFilterSet class, the following filterset classes are also available for subclasses of standard base models.
|
|
||||||
|
|
||||||
| Model Class | FilterSet Class |
|
|
||||||
|-----------------------|--------------------------------------------------|
|
|
||||||
| `PrimaryModel` | `netbox.filtersets.PrimaryModelFilterSet` |
|
|
||||||
| `OrganizationalModel` | `netbox.filtersets.OrganizationalModelFilterSet` |
|
|
||||||
| `NestedGroupModel` | `netbox.filtersets.NestedGroupModelFilterSet` |
|
|
||||||
|
|
||||||
### Declaring Filter Sets
|
### Declaring Filter Sets
|
||||||
|
|
||||||
To utilize a filter set in a subclass of one of NetBox's generic views (such as `ObjectListView` or `BulkEditView`), define the `filterset` attribute on the view class:
|
To utilize a filter set in a subclass of one of NetBox's generic views (such as `ObjectListView` or `BulkEditView`), define the `filterset` attribute on the view class:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Form Classes
|
## Form Classes
|
||||||
|
|
||||||
NetBox provides several base form classes for use by plugins. Additional form classes are also available for other standard base model classes (PrimaryModel, OrganizationalModel, and NestedGroupModel).
|
NetBox provides several base form classes for use by plugins.
|
||||||
|
|
||||||
| Form Class | Purpose |
|
| Form Class | Purpose |
|
||||||
|----------------------------|--------------------------------------|
|
|----------------------------|--------------------------------------|
|
||||||
@@ -19,17 +19,7 @@ This is the base form for creating and editing NetBox models. It extends Django'
|
|||||||
|-------------|---------------------------------------------------------------------------------------|
|
|-------------|---------------------------------------------------------------------------------------|
|
||||||
| `fieldsets` | A tuple of `FieldSet` instances which control how form fields are rendered (optional) |
|
| `fieldsets` | A tuple of `FieldSet` instances which control how form fields are rendered (optional) |
|
||||||
|
|
||||||
#### Subclasses
|
**Example**
|
||||||
|
|
||||||
The corresponding model-specific subclasses of `NetBoxModelForm` are documented below.
|
|
||||||
|
|
||||||
| Model Class | Form Class |
|
|
||||||
|-----------------------|---------------------------|
|
|
||||||
| `PrimaryModel` | `PrimaryModelForm` |
|
|
||||||
| `OrganizationalModel` | `OrganizationalModelForm` |
|
|
||||||
| `NestedGroupModel` | `NestedGroupModelForm` |
|
|
||||||
|
|
||||||
#### Example
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@@ -59,19 +49,9 @@ class MyModelForm(NetBoxModelForm):
|
|||||||
|
|
||||||
### `NetBoxModelImportForm`
|
### `NetBoxModelImportForm`
|
||||||
|
|
||||||
This form facilitates the bulk import of new objects from CSV, JSON, or YAML data. As with model forms, you'll need to declare a `Meta` subclass specifying the associated `model` and `fields`. NetBox also provides several form fields suitable for importing various types of CSV data, listed [below](#csv-import-fields).
|
This form facilitates the bulk import of new objects from CSV, JSON, or YAML data. As with model forms, you'll need to declare a `Meta` subclass specifying the associated `model` and `fields`. NetBox also provides several form fields suitable for import various types of CSV data, listed below.
|
||||||
|
|
||||||
#### Subclasses
|
**Example**
|
||||||
|
|
||||||
The corresponding model-specific subclasses of `NetBoxModelImportForm` are documented below.
|
|
||||||
|
|
||||||
| Model Class | Form Class |
|
|
||||||
|-----------------------|---------------------------------|
|
|
||||||
| `PrimaryModel` | `PrimaryModelImportForm` |
|
|
||||||
| `OrganizationalModel` | `OrganizationalModelImportForm` |
|
|
||||||
| `NestedGroupModel` | `NestedGroupModelImportForm` |
|
|
||||||
|
|
||||||
#### Example
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@@ -103,17 +83,7 @@ This form facilitates editing multiple objects in bulk. Unlike a model form, thi
|
|||||||
| `fieldsets` | A tuple of `FieldSet` instances which control how form fields are rendered (optional) |
|
| `fieldsets` | A tuple of `FieldSet` instances which control how form fields are rendered (optional) |
|
||||||
| `nullable_fields` | A tuple of fields which can be nullified (set to empty) using the bulk edit form (optional) |
|
| `nullable_fields` | A tuple of fields which can be nullified (set to empty) using the bulk edit form (optional) |
|
||||||
|
|
||||||
#### Subclasses
|
**Example**
|
||||||
|
|
||||||
The corresponding model-specific subclasses of `NetBoxModelBulkEditForm` are documented below.
|
|
||||||
|
|
||||||
| Model Class | Form Class |
|
|
||||||
|-----------------------|-----------------------------------|
|
|
||||||
| `PrimaryModel` | `PrimaryModelBulkEditForm` |
|
|
||||||
| `OrganizationalModel` | `OrganizationalModelBulkEditForm` |
|
|
||||||
| `NestedGroupModel` | `NestedGroupModelBulkEditForm` |
|
|
||||||
|
|
||||||
#### Example
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from django import forms
|
from django import forms
|
||||||
@@ -155,17 +125,7 @@ This form class is used to render a form expressly for filtering a list of objec
|
|||||||
| `model` | The model of object being edited |
|
| `model` | The model of object being edited |
|
||||||
| `fieldsets` | A tuple of `FieldSet` instances which control how form fields are rendered (optional) |
|
| `fieldsets` | A tuple of `FieldSet` instances which control how form fields are rendered (optional) |
|
||||||
|
|
||||||
#### Subclasses
|
**Example**
|
||||||
|
|
||||||
The corresponding model-specific subclasses of `NetBoxModelFilterSetForm` are documented below.
|
|
||||||
|
|
||||||
| Model Class | Form Class |
|
|
||||||
|-----------------------|------------------------------------|
|
|
||||||
| `PrimaryModel` | `PrimaryModelFilterSetForm` |
|
|
||||||
| `OrganizationalModel` | `OrganizationalModelFilterSetForm` |
|
|
||||||
| `NestedGroupModel` | `NestedGroupModelFilterSetForm` |
|
|
||||||
|
|
||||||
#### Example
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from dcim.models import Site
|
from dcim.models import Site
|
||||||
|
|||||||
@@ -46,19 +46,3 @@ NetBox provides two object type classes for use by plugins.
|
|||||||
::: netbox.graphql.types.NetBoxObjectType
|
::: netbox.graphql.types.NetBoxObjectType
|
||||||
options:
|
options:
|
||||||
members: false
|
members: false
|
||||||
|
|
||||||
## GraphQL Filters
|
|
||||||
|
|
||||||
NetBox provides a base filter class for use by plugins which employ subclasseses of `NetBoxModel`.
|
|
||||||
|
|
||||||
::: netbox.graphql.filters.NetBoxModelFilter
|
|
||||||
options:
|
|
||||||
members: false
|
|
||||||
|
|
||||||
Additionally, the following filter classes are available for subclasses of standard base models.
|
|
||||||
|
|
||||||
| Model Class | FilterSet Class |
|
|
||||||
|-----------------------|----------------------------------------------------|
|
|
||||||
| `PrimaryModel` | `netbox.graphql.filters.PrimaryModelFilter` |
|
|
||||||
| `OrganizationalModel` | `netbox.graphql.filters.OrganizationalModelFilter` |
|
|
||||||
| `NestedGroupModel` | `netbox.graphql.filters.NestedGroupModelFilter` |
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ The plugin source directory contains all the actual Python code and other resour
|
|||||||
|
|
||||||
The `PluginConfig` class is a NetBox-specific wrapper around Django's built-in [`AppConfig`](https://docs.djangoproject.com/en/stable/ref/applications/) class. It is used to declare NetBox plugin functionality within a Python package. Each plugin should provide its own subclass, defining its name, metadata, and default and required configuration parameters. An example is below:
|
The `PluginConfig` class is a NetBox-specific wrapper around Django's built-in [`AppConfig`](https://docs.djangoproject.com/en/stable/ref/applications/) class. It is used to declare NetBox plugin functionality within a Python package. Each plugin should provide its own subclass, defining its name, metadata, and default and required configuration parameters. An example is below:
|
||||||
|
|
||||||
```python title="__init__.py"
|
```python
|
||||||
from netbox.plugins import PluginConfig
|
from netbox.plugins import PluginConfig
|
||||||
|
|
||||||
class FooBarConfig(PluginConfig):
|
class FooBarConfig(PluginConfig):
|
||||||
@@ -151,7 +151,7 @@ Any additional apps must be installed within the same Python environment as NetB
|
|||||||
|
|
||||||
An example `pyproject.toml` is below:
|
An example `pyproject.toml` is below:
|
||||||
|
|
||||||
```toml title="pyproject.toml"
|
```
|
||||||
# See PEP 518 for the spec of this file
|
# See PEP 518 for the spec of this file
|
||||||
# https://www.python.org/dev/peps/pep-0518/
|
# https://www.python.org/dev/peps/pep-0518/
|
||||||
|
|
||||||
@@ -179,24 +179,11 @@ classifiers=[
|
|||||||
]
|
]
|
||||||
|
|
||||||
requires-python = ">=3.12.0"
|
requires-python = ">=3.12.0"
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Many of these are self-explanatory, but for more information, see the [pyproject.toml documentation](https://packaging.python.org/en/latest/specifications/pyproject-toml/).
|
Many of these are self-explanatory, but for more information, see the [pyproject.toml documentation](https://packaging.python.org/en/latest/specifications/pyproject-toml/).
|
||||||
|
|
||||||
## Compatibility Matrix
|
|
||||||
|
|
||||||
Consider adding a file named `COMPATIBILITY.md` to your plugin project root (alongside `pyproject.toml`). This file should contain a table listing the minimum and maximum supported versions of NetBox (`min_version` and `max_version`) for each release. This serves as a handy reference for users who are upgrading from a previous version of your plugin. An example is shown below:
|
|
||||||
|
|
||||||
```markdown title="COMPATIBILITY.md"
|
|
||||||
# Compatibility Matrix
|
|
||||||
|
|
||||||
| Release | Minimum NetBox Version | Maximum NetBox Version |
|
|
||||||
|---------|------------------------|------------------------|
|
|
||||||
| 0.2.0 | 4.4.0 | 4.5.x |
|
|
||||||
| 0.1.1 | 4.3.0 | 4.4.x |
|
|
||||||
| 0.1.0 | 4.3.0 | 4.4.x |
|
|
||||||
```
|
|
||||||
|
|
||||||
## Create a Virtual Environment
|
## Create a Virtual Environment
|
||||||
|
|
||||||
It is strongly recommended to create a Python [virtual environment](https://docs.python.org/3/tutorial/venv.html) for the development of your plugin, as opposed to using system-wide packages. This will afford you complete control over the installed versions of all dependencies and avoid conflict with system packages. This environment can live wherever you'd like;however, it should be excluded from revision control. (A popular convention is to keep all virtual environments in the user's home directory, e.g. `~/.virtualenvs/`.)
|
It is strongly recommended to create a Python [virtual environment](https://docs.python.org/3/tutorial/venv.html) for the development of your plugin, as opposed to using system-wide packages. This will afford you complete control over the installed versions of all dependencies and avoid conflict with system packages. This environment can live wherever you'd like;however, it should be excluded from revision control. (A popular convention is to keep all virtual environments in the user's home directory, e.g. `~/.virtualenvs/`.)
|
||||||
|
|||||||
@@ -67,46 +67,6 @@ class MyModel(ExportTemplatesMixin, TagsMixin, models.Model):
|
|||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
### Additional Models
|
|
||||||
|
|
||||||
In addition to the base NetBoxModel class, the following additional classes are provided for convenience.
|
|
||||||
|
|
||||||
!!! info "These model classes were added to the plugins API in NetBox v4.5."
|
|
||||||
|
|
||||||
#### PrimaryModel
|
|
||||||
|
|
||||||
PrimaryModel is the go-to class for most object types. It extends NetBoxModel with `description` and `comments` fields, and it introduces support for ownership assignment.
|
|
||||||
|
|
||||||
| Field | Required | Unique | Description |
|
|
||||||
|---------------|----------|--------|---------------------------------------------|
|
|
||||||
| `owner` | No | No | The object's owner |
|
|
||||||
| `description` | No | No | A human-friendly description for the object |
|
|
||||||
| `comments` | No | No | General comments |
|
|
||||||
|
|
||||||
#### OrganizationalModel
|
|
||||||
|
|
||||||
OrganizationalModel is used by object types whose function is primarily the organization of other objects.
|
|
||||||
|
|
||||||
| Field | Required | Unique | Description |
|
|
||||||
|---------------|----------|--------|---------------------------------------------|
|
|
||||||
| `name` | Yes | Yes | The name of the object |
|
|
||||||
| `slug` | Yes | Yes | A unique URL-friendly identifier |
|
|
||||||
| `owner` | No | No | The object's owner |
|
|
||||||
| `description` | No | No | A human-friendly description for the object |
|
|
||||||
|
|
||||||
#### NestedGroupModel
|
|
||||||
|
|
||||||
NestedGroupModel is used for objects which arrange into a recursive hierarchy (like regions and locations) via its self-referential `parent` foreign key.
|
|
||||||
|
|
||||||
| Field | Required | Unique | Description |
|
|
||||||
|---------------|----------|--------|-----------------------------------------------------------------|
|
|
||||||
| `name` | Yes | Yes | The name of the object |
|
|
||||||
| `slug` | Yes | Yes | A unique URL-friendly identifier |
|
|
||||||
| `parent` | No | No | The object (of the same type) under which this object is nested |
|
|
||||||
| `owner` | No | No | The object's owner |
|
|
||||||
| `description` | No | No | A human-friendly description for the object |
|
|
||||||
| `comments` | No | No | General comments |
|
|
||||||
|
|
||||||
## Database Migrations
|
## Database Migrations
|
||||||
|
|
||||||
Once you have completed defining the model(s) for your plugin, you'll need to create the database schema migrations. A migration file is essentially a set of instructions for manipulating the PostgreSQL database to support your new model, or to alter existing models. Creating migrations can usually be done automatically using Django's `makemigrations` management command. (Ensure that your plugin has been installed and enabled first, otherwise it won't be found.)
|
Once you have completed defining the model(s) for your plugin, you'll need to create the database schema migrations. A migration file is essentially a set of instructions for manipulating the PostgreSQL database to support your new model, or to alter existing models. Creating migrations can usually be done automatically using Django's `makemigrations` management command. (Ensure that your plugin has been installed and enabled first, otherwise it won't be found.)
|
||||||
|
|||||||
@@ -27,14 +27,6 @@ Serializers are responsible for converting Python objects to JSON data suitable
|
|||||||
|
|
||||||
The default nested representation of an object is defined by the `brief_fields` attributes under the serializer's `Meta` class. (Older versions of NetBox required the definition of a separate nested serializer.)
|
The default nested representation of an object is defined by the `brief_fields` attributes under the serializer's `Meta` class. (Older versions of NetBox required the definition of a separate nested serializer.)
|
||||||
|
|
||||||
In addition to the base NetBoxModelSerializer class, the following serializer classes are also available for subclasses of standard base models.
|
|
||||||
|
|
||||||
| Model Class | Serializer Class |
|
|
||||||
|-----------------------|--------------------------------------------------------|
|
|
||||||
| `PrimaryModel` | `netbox.api.serializers.PrimaryModelSerializer` |
|
|
||||||
| `OrganizationalModel` | `netbox.api.serializers.OrganizationalModelSerializer` |
|
|
||||||
| `NestedGroupModel` | `netbox.api.serializers.NestedGroupModelSerializer` |
|
|
||||||
|
|
||||||
#### Example
|
#### Example
|
||||||
|
|
||||||
To create a serializer for a plugin model, subclass `NetBoxModelSerializer` in `api/serializers.py`. Specify the model class and the fields to include within the serializer's `Meta` class.
|
To create a serializer for a plugin model, subclass `NetBoxModelSerializer` in `api/serializers.py`. Specify the model class and the fields to include within the serializer's `Meta` class.
|
||||||
|
|||||||
@@ -36,14 +36,6 @@ class MyModelTable(NetBoxTable):
|
|||||||
default_columns = ('pk', 'name', ...)
|
default_columns = ('pk', 'name', ...)
|
||||||
```
|
```
|
||||||
|
|
||||||
In addition to the base NetBoxTable class, the following table classes are also available for subclasses of standard base models.
|
|
||||||
|
|
||||||
| Model Class | Table Class |
|
|
||||||
|-----------------------|------------------------------------------|
|
|
||||||
| `PrimaryModel` | `netbox.tables.PrimaryModelTable` |
|
|
||||||
| `OrganizationalModel` | `netbox.tables.OrganizationalModelTable` |
|
|
||||||
| `NestedGroupModel` | `netbox.tables.NestedGroupModelTable` |
|
|
||||||
|
|
||||||
### Table Configuration
|
### Table Configuration
|
||||||
|
|
||||||
The NetBoxTable class features dynamic configuration to allow users to change their column display and ordering preferences. To configure a table for a specific request, simply call its `configure()` method and pass the current HTTPRequest object. For example:
|
The NetBoxTable class features dynamic configuration to allow users to change their column display and ordering preferences. To configure a table for a specific request, simply call its `configure()` method and pass the current HTTPRequest object. For example:
|
||||||
|
|||||||
@@ -10,14 +10,6 @@ Minor releases are published in April, August, and December of each calendar yea
|
|||||||
|
|
||||||
This page contains a history of all major and minor releases since NetBox v2.0. For more detail on a specific patch release, please see the release notes page for that specific minor release.
|
This page contains a history of all major and minor releases since NetBox v2.0. For more detail on a specific patch release, please see the release notes page for that specific minor release.
|
||||||
|
|
||||||
#### [Version 4.5](./version-4.5.md) (January 2026)
|
|
||||||
|
|
||||||
* Lookup Modifiers in Filter Forms ([#7604](https://github.com/netbox-community/netbox/issues/7604))
|
|
||||||
* Improved API Authentication Tokens ([#20210](https://github.com/netbox-community/netbox/issues/20210))
|
|
||||||
* Object Ownership ([#20304](https://github.com/netbox-community/netbox/issues/20304))
|
|
||||||
* Advanced Port Mappings ([#20564](https://github.com/netbox-community/netbox/issues/20564))
|
|
||||||
* Cable Profiles ([#20788](https://github.com/netbox-community/netbox/issues/20788))
|
|
||||||
|
|
||||||
#### [Version 4.4](./version-4.4.md) (September 2025)
|
#### [Version 4.4](./version-4.4.md) (September 2025)
|
||||||
|
|
||||||
* Background Jobs for Bulk Operations ([#19589](https://github.com/netbox-community/netbox/issues/19589), [#19891](https://github.com/netbox-community/netbox/issues/19891))
|
* Background Jobs for Bulk Operations ([#19589](https://github.com/netbox-community/netbox/issues/19589), [#19891](https://github.com/netbox-community/netbox/issues/19891))
|
||||||
|
|||||||
@@ -1,69 +1,5 @@
|
|||||||
# NetBox v4.4
|
# NetBox v4.4
|
||||||
|
|
||||||
## v4.4.10 (2026-01-06)
|
|
||||||
|
|
||||||
### Enhancements
|
|
||||||
|
|
||||||
* [#20953](https://github.com/netbox-community/netbox/issues/20953) - Show reverse bridge relationships on interface detail pages
|
|
||||||
* [#21071](https://github.com/netbox-community/netbox/issues/21071) - Include request method & URL when displaying server errors
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* [#19506](https://github.com/netbox-community/netbox/issues/19506) - Add filter forms for component templates to ensure object selector support
|
|
||||||
* [#20044](https://github.com/netbox-community/netbox/issues/20044) - Fix dark mode support for rack elevations
|
|
||||||
* [#20320](https://github.com/netbox-community/netbox/issues/20320) - Restore support for selecting related interfaces when bulk editing device interfaces
|
|
||||||
* [#20817](https://github.com/netbox-community/netbox/issues/20817) - Re-enable sync button when disabling scheduled syncing for a data source
|
|
||||||
* [#21045](https://github.com/netbox-community/netbox/issues/21045) - Fix `ValueError` exception when saving a site with an assigned prefix
|
|
||||||
* [#21049](https://github.com/netbox-community/netbox/issues/21049) - Ignore stale custom field data when validating an object
|
|
||||||
* [#21063](https://github.com/netbox-community/netbox/issues/21063) - Check for duplicate choice values when validating a custom field choice set
|
|
||||||
* [#21064](https://github.com/netbox-community/netbox/issues/21064) - Ensures that extra choices in custom field choice sets preserve escaped colons
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v4.4.9 (2025-12-23)
|
|
||||||
|
|
||||||
### Enhancements
|
|
||||||
|
|
||||||
* [#20309](https://github.com/netbox-community/netbox/issues/20309) - Support ASDOT notation for ASN ranges
|
|
||||||
* [#20720](https://github.com/netbox-community/netbox/issues/20720) - Add Latvian translations
|
|
||||||
* [#20900](https://github.com/netbox-community/netbox/issues/20900) - Allow filtering custom choice fields by multiple values in the UI
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* [#17976](https://github.com/netbox-community/netbox/issues/17976) - Remove `devicetype_count` from nested manufacturer to correct OpenAPI schema
|
|
||||||
* [#20011](https://github.com/netbox-community/netbox/issues/20011) - Provide a clear message when encountering duplicate object IDs during bulk import
|
|
||||||
* [#20114](https://github.com/netbox-community/netbox/issues/20114) - Preserve `parent_bay` during device bulk import when tags are present
|
|
||||||
* [#20491](https://github.com/netbox-community/netbox/issues/20491) - Improve handling of numeric ranges in tests
|
|
||||||
* [#20873](https://github.com/netbox-community/netbox/issues/20873) - Fix `AttributeError` exception triggered by event rules associated with an object that supports file attachments
|
|
||||||
* [#20875](https://github.com/netbox-community/netbox/issues/20875) - Ensure that parent object relations are cached (for filtering) on device/module components during instantiation
|
|
||||||
* [#20876](https://github.com/netbox-community/netbox/issues/20876) - Allow editing an IP address that resides within a range marked as populated
|
|
||||||
* [#20912](https://github.com/netbox-community/netbox/issues/20912) - Fix inconsistent clearing of `module` field on ModuleBay
|
|
||||||
* [#20944](https://github.com/netbox-community/netbox/issues/20944) - Ensure cached scope is updated on child objects when a parent region/site/location is changed
|
|
||||||
* [#20948](https://github.com/netbox-community/netbox/issues/20948) - Handle the deletion of related objects with `on_delete=RESTRICT` the same as `CASCADE`
|
|
||||||
* [#20966](https://github.com/netbox-community/netbox/issues/20966) - Fix UI rendering issue when scrolling list of object types in permissions form
|
|
||||||
* [#20969](https://github.com/netbox-community/netbox/issues/20969) - Fix querying of front port templates by `rear_port_id`
|
|
||||||
* [#21011](https://github.com/netbox-community/netbox/issues/21011) - Avoid writing to the database when loading active ConfigRevision
|
|
||||||
* [#21032](https://github.com/netbox-community/netbox/issues/21032) - Avoid SQL subquery in RestrictedQuerySet where unnecessary
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v4.4.8 (2025-12-09)
|
|
||||||
|
|
||||||
### Enhancements
|
|
||||||
|
|
||||||
* [#20068](https://github.com/netbox-community/netbox/issues/20068) - Support the assignment of module type profile attributes via bulk import
|
|
||||||
* [#20914](https://github.com/netbox-community/netbox/issues/20914) - Enable filtering device components by tenant assigned to device
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* [#19918](https://github.com/netbox-community/netbox/issues/19918) - Fix support for `{module}` resolution of components of child modules
|
|
||||||
* [#20759](https://github.com/netbox-community/netbox/issues/20759) - Improve legibility of object types in permissions form
|
|
||||||
* [#20860](https://github.com/netbox-community/netbox/issues/20860) - Ensure user-provided changelog message is recorded when creating device components via the UI
|
|
||||||
* [#20878](https://github.com/netbox-community/netbox/issues/20878) - Use the active database connection when executing custom scripts
|
|
||||||
* [#20888](https://github.com/netbox-community/netbox/issues/20888) - Resolve warnings about non-decimal values for min/max latitude & longitude fields
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v4.4.7 (2025-11-25)
|
## v4.4.7 (2025-11-25)
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|||||||
@@ -1,103 +1,37 @@
|
|||||||
# NetBox v4.5
|
## v4.5.0 (FUTURE)
|
||||||
|
|
||||||
## v4.5.1 (2026-01-20)
|
|
||||||
|
|
||||||
### Enhancements
|
|
||||||
|
|
||||||
* [#21018](https://github.com/netbox-community/netbox/issues/21018) - Enable filtering prefixes by location/site/site group/region directly via GraphQL API
|
|
||||||
* [#21142](https://github.com/netbox-community/netbox/issues/21142) - Enable filtering device components by site/location/rack directly via GraphQL API
|
|
||||||
* [#21144](https://github.com/netbox-community/netbox/issues/21144) - Enable specifying a prefix length for IP addresses when utilizing the `/api/ipam/prefixes/<id>/available-ips/` REST API endpoint
|
|
||||||
* [#21165](https://github.com/netbox-community/netbox/issues/21165) - VLAN selector should default to group (instead of site)
|
|
||||||
* [#21178](https://github.com/netbox-community/netbox/issues/21178) - Improve consistency of rack measurements in UI
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* [#19901](https://github.com/netbox-community/netbox/issues/19901) - Fix `RelatedObjectDoesNotExist` exception when importing modules into unnamed devices
|
|
||||||
* [#20239](https://github.com/netbox-community/netbox/issues/20239) - Prevent shared mutable state in PluginMenuItem & PluginMenuButton
|
|
||||||
* [#20933](https://github.com/netbox-community/netbox/issues/20933) - Fix writable `data_file` assignment for ConfigContext and ConfigContextProfile via the REST API
|
|
||||||
* [#21039](https://github.com/netbox-community/netbox/issues/21039) - Fix support for AVIF image uploads
|
|
||||||
* [#21050](https://github.com/netbox-community/netbox/issues/21050) - Clear device OOB IP assignments when reassigning IP addresses
|
|
||||||
* [#21051](https://github.com/netbox-community/netbox/issues/21051) - Remove irrelevant object types from permissions form
|
|
||||||
* [#21097](https://github.com/netbox-community/netbox/issues/21097) - Fix comparison lookups for ID filters in GraphQL API
|
|
||||||
* [#21102](https://github.com/netbox-community/netbox/issues/21102) - Fix GraphiQL explorer UI
|
|
||||||
* [#21117](https://github.com/netbox-community/netbox/issues/21117) - Avoid `ValueError` exception when `API_TOKEN_PEPPERS` is not defined
|
|
||||||
* [#21118](https://github.com/netbox-community/netbox/issues/21118) - Address performance issue when saving sites with many assigned objects
|
|
||||||
* [#21124](https://github.com/netbox-community/netbox/issues/21124) - Fix front/rear port mapping for module types
|
|
||||||
* [#21134](https://github.com/netbox-community/netbox/issues/21134) - Fix bulk renaming for module types
|
|
||||||
* [#21139](https://github.com/netbox-community/netbox/issues/21139) - Support `fields` parameter for job, object change, and object type REST API endpoints
|
|
||||||
* [#21140](https://github.com/netbox-community/netbox/issues/21140) - Restore translation for object attribute labels on several UI views
|
|
||||||
* [#21160](https://github.com/netbox-community/netbox/issues/21160) - Fix performance issue loading UI views caused by unintended `APISelect` choices resolution
|
|
||||||
* [#21166](https://github.com/netbox-community/netbox/issues/21166) - Fix support for 32-bit ASN filtering in GraphQL API
|
|
||||||
* [#21175](https://github.com/netbox-community/netbox/issues/21175) - Fix pending migrations warning when `DEFAULT_LANGUAGE` is set
|
|
||||||
* [#21181](https://github.com/netbox-community/netbox/issues/21181) - Handle `AuthenticationFailed` exception when using an invalid API token to fetch media files
|
|
||||||
* [#21213](https://github.com/netbox-community/netbox/issues/21213) - Tag weight field should be marked as required in UI forms
|
|
||||||
* [#21231](https://github.com/netbox-community/netbox/issues/21231) - Presence of object types table should be checked only during migration (performance improvement)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v4.5.0 (2026-01-06)
|
|
||||||
|
|
||||||
### Breaking Changes
|
### Breaking Changes
|
||||||
|
|
||||||
* Python 3.10 and 3.11 are no longer supported. NetBox now requires Python 3.12, 3.13, or 3.14.
|
* Python 3.10 and 3.11 are no longer supported. NetBox now requires Python 3.12 or later.
|
||||||
* GraphQL API queries which filter by object IDs or enums must now specify a filter lookup similar to other fields. For example, `id: 123` becomes `id: {exact: 123 }`.
|
* GraphQL API queries which filter by object IDs or enums must now specify a filter lookup similar to other fields. (For example, `id: 123` becomes `id: {exact: 123 }`.)
|
||||||
* Rendering a device or virtual machine configuration is now restricted to users with the `render_config` permission for the applicable object type.
|
* Rendering a device or virtual machine configuration is now restricted to users with the `render_config` permission for the applicable object type.
|
||||||
* Retrieval of API token plaintexts is no longer supported. The `ALLOW_TOKEN_RETRIEVAL` config parameter has been removed.
|
* Retrieval of API token plaintexts is no longer supported. The `ALLOW_TOKEN_RETRIEVAL` config parameter has been removed.
|
||||||
* API tokens can no longer be reassigned from one user to another.
|
* The owner of an API token can no longer be changed once it has been created.
|
||||||
* A config context assigned to a platform will now also apply to any children of that platform. (Although this is typically desired behavior, it may introduce unanticipated changes for existing deployments.)
|
* Config contexts now apply to all child platforms of a parent platform.
|
||||||
* The `/api/dcim/cable-terminations/` REST API endpoint is now read-only. Cable terminations must be set on cables directly via the `/api/dcim/cables/` endpoint.
|
* The `/api/extras/object-types/` REST API endpoint has been removed. (Use `/api/core/object-types/` instead.)
|
||||||
* The UI view dedicated to swapping A/Z circuit terminations has been removed.
|
* The `/api/dcim/cable-terminations/` REST API endpoint is now read-only. Cable terminations must be set on cables directly.
|
||||||
|
* The UI view dedicated to swaping A/Z circuit terminations has been removed.
|
||||||
|
* Webhooks no longer specify a `model` in payload data. (Reference `object_type` instead, which includes the parent app label.)
|
||||||
|
* The obsolete module `core.models.contenttypes` has been removed (replaced in v4.4 by `core.models.object_types`).
|
||||||
|
* The `load_yaml()` and `load_json()` utility methods have been removed from the base class for custom scripts.
|
||||||
* The experimental HTMX navigation feature has been removed.
|
* The experimental HTMX navigation feature has been removed.
|
||||||
* The obsolete boolean field `is_staff` has been removed from the `User` model.
|
* The obsolete field `is_staff` has been removed from the `User` model.
|
||||||
* Removal of deprecated behavior
|
|
||||||
* The `/api/extras/object-types/` REST API endpoint has been removed. (Use `/api/core/object-types/` instead.)
|
|
||||||
* Webhooks no longer specify a `model` in payload data. (Reference `object_type` instead, which includes the parent app label.)
|
|
||||||
* The obsolete module `core.models.contenttypes` has been removed (replaced in v4.4 by `core.models.object_types`).
|
|
||||||
* The `load_yaml()` and `load_json()` utility methods have been removed from the base class for custom scripts.
|
|
||||||
|
|
||||||
### New Features
|
### New Features
|
||||||
|
|
||||||
#### Lookup Modifiers in Filter Forms ([#7604](https://github.com/netbox-community/netbox/issues/7604))
|
#### Lookup Modifiers in Filter Forms ([#7604](https://github.com/netbox-community/netbox/issues/7604))
|
||||||
|
|
||||||
Most object list filters within the UI have been extended to include optional lookup modifiers to support more complex queries. For instance, filters for numeric values now include a dropdown where a user can select "less than," "greater than," or "not" in addition to the default equivalency match. The specific modifiers available depend on the type of each filter. Plugins can register their own filtersets using the `register_filterset()` decorator to enable this new functionality.
|
|
||||||
|
|
||||||
(Note that this feature does not introduce any new filters. Rather, it makes available in the UI filters which already exist.)
|
|
||||||
|
|
||||||
#### Improved API Authentication Tokens ([#20210](https://github.com/netbox-community/netbox/issues/20210))
|
#### Improved API Authentication Tokens ([#20210](https://github.com/netbox-community/netbox/issues/20210))
|
||||||
|
|
||||||
This release introduces a new version of API token (v2) which implements several security improvements. HMAC hashing with a cryptographic pepper is used to authenticate these tokens, obviating the need to store plaintexts. The new tokens also employ a non-sensitive key which can be shared to identify tokens without divulging their plaintexts. We've also adopted the standard "bearer" HTTP header format, as shown below.
|
|
||||||
|
|
||||||
```
|
|
||||||
# v1 token header
|
|
||||||
Authorization: Token <TOKEN>
|
|
||||||
|
|
||||||
# v2 token header
|
|
||||||
Authorization: Bearer nbt_<KEY>.<TOKEN>
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that v2 token keys are prefixed with the fixed string `nbt_`, which can be used to aid in secret detection.
|
|
||||||
|
|
||||||
Backward compatibility with legacy (v1) tokens is retained in this release. However, users are strongly encouraged to begin using only v2 tokens, as support for legacy tokens will be removed in NetBox v4.7.
|
|
||||||
|
|
||||||
#### Object Ownership ([#20304](https://github.com/netbox-community/netbox/issues/20304))
|
#### Object Ownership ([#20304](https://github.com/netbox-community/netbox/issues/20304))
|
||||||
|
|
||||||
An optional `owner` foreign key field has been added to most models. This enables the assignment of objects to a new Owner model, which represents a set of users and/or groups. Through this relationship, we can now convey ownership of objects within NetBox natively, without needing to rely on the assignment of tags or custom fields.
|
|
||||||
|
|
||||||
(Note that ownership differs significantly in function from tenancy. Ownership determines the parties responsible for the maintenance of an object, whereas as tenancy conveys an operational dependency.)
|
|
||||||
|
|
||||||
#### Advanced Port Mappings ([#20564](https://github.com/netbox-community/netbox/issues/20564))
|
#### Advanced Port Mappings ([#20564](https://github.com/netbox-community/netbox/issues/20564))
|
||||||
|
|
||||||
The previous many-to-one mapping of front to rear ports has been expanded to support bidirectional mappings. The `rear_port` and `rear_port_position` fields on the FrontPort model have been replaced with an intermediary PortMapping model, which supports any number of assignments between front port/position pair and a rear port/position pair. This change unlocks the ability to model complex inline devices that swap individual fiber pairs between cables.
|
|
||||||
|
|
||||||
#### Cable Profiles ([#20788](https://github.com/netbox-community/netbox/issues/20788))
|
#### Cable Profiles ([#20788](https://github.com/netbox-community/netbox/issues/20788))
|
||||||
|
|
||||||
Cables can now be assigned profiles which determine how they are treated for path tracing. A profile indicates the number of discrete parallel channels or lanes carried by the cable among its endpoints. For example, a 1-to-4 breakout cable has four lanes, shared at one end via a common termination and split out at the other end to four separate terminations. Profiles, when assigned, enable NetBox to more accurately trace a specific connection within a cable, rather than the cable as a whole.
|
|
||||||
|
|
||||||
The assignment of cable profiles is optional: Cable tracing will continue to operate as before for cables with no profile assigned.
|
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
* [#16681](https://github.com/netbox-community/netbox/issues/16681) - Introduce a `render_config` permission, which is now required to render a device or virtual machine configuration
|
* [#16681](https://github.com/netbox-community/netbox/issues/16681) - Introduce a `render_config` permission, which is noq required to render a device or virtual machine configuration
|
||||||
* [#18658](https://github.com/netbox-community/netbox/issues/18658) - Add a `start_on_boot` choice field for virtual machines
|
* [#18658](https://github.com/netbox-community/netbox/issues/18658) - Add a `start_on_boot` choice field for virtual machines
|
||||||
* [#19095](https://github.com/netbox-community/netbox/issues/19095) - Add support for Python 3.13 and 3.14
|
* [#19095](https://github.com/netbox-community/netbox/issues/19095) - Add support for Python 3.13 and 3.14
|
||||||
* [#19338](https://github.com/netbox-community/netbox/issues/19338) - Enable filter lookups for object IDs and enums in GraphQL API queries
|
* [#19338](https://github.com/netbox-community/netbox/issues/19338) - Enable filter lookups for object IDs and enums in GraphQL API queries
|
||||||
@@ -109,9 +43,7 @@ The assignment of cable profiles is optional: Cable tracing will continue to ope
|
|||||||
* [#20834](https://github.com/netbox-community/netbox/issues/20834) - Add an `enabled` boolean field to API tokens
|
* [#20834](https://github.com/netbox-community/netbox/issues/20834) - Add an `enabled` boolean field to API tokens
|
||||||
* [#20917](https://github.com/netbox-community/netbox/issues/20917) - Include usage reference on API token views
|
* [#20917](https://github.com/netbox-community/netbox/issues/20917) - Include usage reference on API token views
|
||||||
* [#20925](https://github.com/netbox-community/netbox/issues/20925) - Add optional `comments` field to all subclasses of `OrganizationalModel`
|
* [#20925](https://github.com/netbox-community/netbox/issues/20925) - Add optional `comments` field to all subclasses of `OrganizationalModel`
|
||||||
* [#20929](https://github.com/netbox-community/netbox/issues/20929) - Require the `render_config` permission to view a rendered device/VM configuration in the UI
|
|
||||||
* [#20936](https://github.com/netbox-community/netbox/issues/20936) - Introduce the `/api/authentication-check/` REST API endpoint for validating authentication tokens
|
* [#20936](https://github.com/netbox-community/netbox/issues/20936) - Introduce the `/api/authentication-check/` REST API endpoint for validating authentication tokens
|
||||||
* [#20959](https://github.com/netbox-community/netbox/issues/20959) - Include a count of related module types for a manufacturer in the REST API
|
|
||||||
|
|
||||||
### Plugins
|
### Plugins
|
||||||
|
|
||||||
@@ -128,60 +60,52 @@ The assignment of cable profiles is optional: Cable tracing will continue to ope
|
|||||||
* [#20095](https://github.com/netbox-community/netbox/issues/20095) - Remove the obsolete module `core.models.contenttypes`
|
* [#20095](https://github.com/netbox-community/netbox/issues/20095) - Remove the obsolete module `core.models.contenttypes`
|
||||||
* [#20096](https://github.com/netbox-community/netbox/issues/20096) - Remove the `load_yaml()` and `load_json()` utility methods from the `BaseScript` class
|
* [#20096](https://github.com/netbox-community/netbox/issues/20096) - Remove the `load_yaml()` and `load_json()` utility methods from the `BaseScript` class
|
||||||
* [#20204](https://github.com/netbox-community/netbox/issues/20204) - Started migrating object views from custom HTML templates to declarative layouts
|
* [#20204](https://github.com/netbox-community/netbox/issues/20204) - Started migrating object views from custom HTML templates to declarative layouts
|
||||||
* [#20295](https://github.com/netbox-community/netbox/issues/20295) - Cable terminations may be modified via the REST API only by modifying the cable itself
|
|
||||||
* [#20617](https://github.com/netbox-community/netbox/issues/20617) - Introduce `BaseModel` as the global base class for models
|
* [#20617](https://github.com/netbox-community/netbox/issues/20617) - Introduce `BaseModel` as the global base class for models
|
||||||
* [#20683](https://github.com/netbox-community/netbox/issues/20683) - Remove the UI view dedicated to swapping A/Z circuit terminations
|
* [#20683](https://github.com/netbox-community/netbox/issues/20683) - Remove the UI view dedicated to swaping A/Z circuit terminations
|
||||||
* [#20926](https://github.com/netbox-community/netbox/issues/20926) - Standardize naming of GraphQL filters
|
* [#20926](https://github.com/netbox-community/netbox/issues/20926) - Standardize naming of GraphQL filters
|
||||||
|
|
||||||
### REST API Changes
|
### REST API Changes
|
||||||
|
|
||||||
* Most objects now include an optional `owner` foreign key field.
|
* Most objects now include an optional `owner` foreign key field.
|
||||||
* The `/api/dcim/cable-terminations` endpoint is now read-only.
|
* The `/api/dcim/cable-terminations` endpoint is now read-only.
|
||||||
* Introduced the `/api/authentication-check/` endpoint to test REST API credentials
|
* Introduced the `/api/authentication-check/` endpoint.
|
||||||
* `circuits.CircuitGroup`
|
* `circuits.CircuitGroup`
|
||||||
* Add optional `comments` field
|
* Add optional `comments` field
|
||||||
* `circuits.CircuitType`
|
* `circuits.CircuitType`
|
||||||
* Add optional `comments` field
|
* Add optional `comments` field
|
||||||
* `circuits.VirtualCircuitType`
|
* `circuits.VirtualCircuitType`
|
||||||
* Add optional `comments` field
|
* Add optional `comments` field
|
||||||
* `dcim.Cable`
|
* `dcim.Cable`
|
||||||
* Add the optional `profile` choice field
|
* Add the optional `profile` choice field
|
||||||
* `dcim.FrontPort`
|
|
||||||
* Removed the `rear_port` and `rear_port_position` fields
|
|
||||||
* Add the `positions` integer field
|
|
||||||
* Add the `rear_ports` list for port mappings
|
|
||||||
* `dcim.InventoryItemRole`
|
* `dcim.InventoryItemRole`
|
||||||
* Add optional `comments` field
|
* Add optional `comments` field
|
||||||
* `dcim.Manufacturer`
|
* `dcim.Manufacturer`
|
||||||
* Add optional `comments` field
|
* Add optional `comments` field
|
||||||
* Add read-only `moduletype_count` integer field
|
|
||||||
* `dcim.ModuleType`
|
* `dcim.ModuleType`
|
||||||
* Add read-only `module_count` integer field
|
* Add read-only `module_count` integer field
|
||||||
* `dcim.PowerOutletTemplate`
|
* `dcim.PowerOutletTemplate`
|
||||||
* Add optional `color` field
|
* Add optional `color` field
|
||||||
* `dcim.RackRole`
|
* `dcim.RackRole`
|
||||||
* Add optional `comments` field
|
* Add optional `comments` field
|
||||||
* `dcim.RackType`
|
* `dcim.RackType`
|
||||||
* Add read-only `rack_count` integer field
|
* Add read-only `rack_count` integer field
|
||||||
* `dcim.RearPort`
|
|
||||||
* Add the `front_ports` list for port mappings
|
|
||||||
* `ipam.ASNRange`
|
* `ipam.ASNRange`
|
||||||
* Add optional `comments` field
|
* Add optional `comments` field
|
||||||
* `ipam.RIR`
|
* `ipam.RIR`
|
||||||
* Add optional `comments` field
|
* Add optional `comments` field
|
||||||
* `ipam.Role`
|
* `ipam.Role`
|
||||||
* Add optional `comments` field
|
* Add optional `comments` field
|
||||||
* `ipam.VLANGroup`
|
* `ipam.VLANGroup`
|
||||||
* Add optional `comments` field
|
* Add optional `comments` field
|
||||||
* `tenancy.ContactRole`
|
* `tenancy.ContactRole`
|
||||||
* Add optional `comments` field
|
* Add optional `comments` field
|
||||||
* `users.Token`
|
* `users.Token`
|
||||||
* Add `enabled` boolean field
|
* Add `enabled` boolean field
|
||||||
* `virtualization.ClusterGroup`
|
* `virtualization.ClusterGroup`
|
||||||
* Add optional `comments` field
|
* Add optional `comments` field
|
||||||
* `virtualization.ClusterType`
|
* `virtualization.ClusterType`
|
||||||
* Add optional `comments` field
|
* Add optional `comments` field
|
||||||
* `virtualization.VirtualMachine`
|
* `virtualization.VirtualMachine`
|
||||||
* Add optional `start_on_boot` choice field
|
* Add optional `start_on_boot` choice field
|
||||||
* `vpn.TunnelGroup`
|
* `vpn.TunnelGroup`
|
||||||
* Add optional `comments` field
|
* Add optional `comments` field
|
||||||
|
|||||||
@@ -322,7 +322,6 @@ nav:
|
|||||||
- git Cheat Sheet: 'development/git-cheat-sheet.md'
|
- git Cheat Sheet: 'development/git-cheat-sheet.md'
|
||||||
- Release Notes:
|
- Release Notes:
|
||||||
- Summary: 'release-notes/index.md'
|
- Summary: 'release-notes/index.md'
|
||||||
- Version 4.5: 'release-notes/version-4.5.md'
|
|
||||||
- Version 4.4: 'release-notes/version-4.4.md'
|
- Version 4.4: 'release-notes/version-4.4.md'
|
||||||
- Version 4.3: 'release-notes/version-4.3.md'
|
- Version 4.3: 'release-notes/version-4.3.md'
|
||||||
- Version 4.2: 'release-notes/version-4.2.md'
|
- Version 4.2: 'release-notes/version-4.2.md'
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class CircuitTypeSerializer(OrganizationalModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitType
|
model = CircuitType
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'owner', 'comments', 'tags',
|
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'owner', 'tags',
|
||||||
'custom_fields', 'created', 'last_updated', 'circuit_count',
|
'custom_fields', 'created', 'last_updated', 'circuit_count',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count')
|
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count')
|
||||||
@@ -71,7 +71,7 @@ class CircuitGroupSerializer(OrganizationalModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitGroup
|
model = CircuitGroup
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'tenant', 'owner', 'comments', 'tags',
|
'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'tenant', 'owner', 'tags',
|
||||||
'custom_fields', 'created', 'last_updated', 'circuit_count'
|
'custom_fields', 'created', 'last_updated', 'circuit_count'
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'name')
|
brief_fields = ('id', 'url', 'display', 'name')
|
||||||
@@ -161,7 +161,7 @@ class VirtualCircuitTypeSerializer(OrganizationalModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = VirtualCircuitType
|
model = VirtualCircuitType
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'owner', 'comments', 'tags',
|
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'owner', 'tags',
|
||||||
'custom_fields', 'created', 'last_updated', 'virtual_circuit_count',
|
'custom_fields', 'created', 'last_updated', 'virtual_circuit_count',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'virtual_circuit_count')
|
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'virtual_circuit_count')
|
||||||
|
|||||||
@@ -353,7 +353,7 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
|
|||||||
model = CircuitTermination
|
model = CircuitTermination
|
||||||
fields = (
|
fields = (
|
||||||
'id', 'termination_id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description',
|
'id', 'termination_id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description',
|
||||||
'mark_connected', 'pp_info', 'cable_end', 'cable_connector',
|
'mark_connected', 'pp_info', 'cable_end', 'cable_position',
|
||||||
)
|
)
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ class CircuitTypeBulkEditForm(OrganizationalModelBulkEditForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('color', 'description'),
|
FieldSet('color', 'description'),
|
||||||
)
|
)
|
||||||
nullable_fields = ('color', 'description', 'comments')
|
nullable_fields = ('color', 'description')
|
||||||
|
|
||||||
|
|
||||||
class CircuitBulkEditForm(PrimaryModelBulkEditForm):
|
class CircuitBulkEditForm(PrimaryModelBulkEditForm):
|
||||||
@@ -241,7 +241,7 @@ class CircuitGroupBulkEditForm(OrganizationalModelBulkEditForm):
|
|||||||
|
|
||||||
model = CircuitGroup
|
model = CircuitGroup
|
||||||
nullable_fields = (
|
nullable_fields = (
|
||||||
'description', 'tenant', 'comments',
|
'description', 'tenant',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -274,7 +274,7 @@ class VirtualCircuitTypeBulkEditForm(OrganizationalModelBulkEditForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('color', 'description'),
|
FieldSet('color', 'description'),
|
||||||
)
|
)
|
||||||
nullable_fields = ('color', 'description', 'comments')
|
nullable_fields = ('color', 'description')
|
||||||
|
|
||||||
|
|
||||||
class VirtualCircuitBulkEditForm(PrimaryModelBulkEditForm):
|
class VirtualCircuitBulkEditForm(PrimaryModelBulkEditForm):
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ class CircuitTypeImportForm(OrganizationalModelImportForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitType
|
model = CircuitType
|
||||||
fields = ('name', 'slug', 'color', 'description', 'owner', 'comments', 'tags')
|
fields = ('name', 'slug', 'color', 'description', 'owner', 'tags')
|
||||||
|
|
||||||
|
|
||||||
class CircuitImportForm(PrimaryModelImportForm):
|
class CircuitImportForm(PrimaryModelImportForm):
|
||||||
@@ -176,7 +176,7 @@ class CircuitGroupImportForm(OrganizationalModelImportForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitGroup
|
model = CircuitGroup
|
||||||
fields = ('name', 'slug', 'description', 'tenant', 'owner', 'comments', 'tags')
|
fields = ('name', 'slug', 'description', 'tenant', 'owner', 'tags')
|
||||||
|
|
||||||
|
|
||||||
class CircuitGroupAssignmentImportForm(NetBoxModelImportForm):
|
class CircuitGroupAssignmentImportForm(NetBoxModelImportForm):
|
||||||
@@ -199,7 +199,7 @@ class VirtualCircuitTypeImportForm(OrganizationalModelImportForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VirtualCircuitType
|
model = VirtualCircuitType
|
||||||
fields = ('name', 'slug', 'color', 'description', 'owner', 'comments', 'tags')
|
fields = ('name', 'slug', 'color', 'description', 'owner', 'tags')
|
||||||
|
|
||||||
|
|
||||||
class VirtualCircuitImportForm(PrimaryModelImportForm):
|
class VirtualCircuitImportForm(PrimaryModelImportForm):
|
||||||
|
|||||||
@@ -34,10 +34,9 @@ __all__ = (
|
|||||||
class ProviderFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
class ProviderFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = Provider
|
model = Provider
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
|
||||||
FieldSet('asn_id', name=_('ASN')),
|
FieldSet('asn_id', name=_('ASN')),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||||
)
|
)
|
||||||
region_id = DynamicModelMultipleChoiceField(
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
@@ -70,9 +69,8 @@ class ProviderFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
|||||||
class ProviderAccountFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
class ProviderAccountFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = ProviderAccount
|
model = ProviderAccount
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('provider_id', 'account', name=_('Attributes')),
|
FieldSet('provider_id', 'account', name=_('Attributes')),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||||
)
|
)
|
||||||
provider_id = DynamicModelMultipleChoiceField(
|
provider_id = DynamicModelMultipleChoiceField(
|
||||||
@@ -90,9 +88,8 @@ class ProviderAccountFilterForm(ContactModelFilterForm, PrimaryModelFilterSetFor
|
|||||||
class ProviderNetworkFilterForm(PrimaryModelFilterSetForm):
|
class ProviderNetworkFilterForm(PrimaryModelFilterSetForm):
|
||||||
model = ProviderNetwork
|
model = ProviderNetwork
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('provider_id', 'service_id', name=_('Attributes')),
|
FieldSet('provider_id', 'service_id', name=_('Attributes')),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
)
|
)
|
||||||
provider_id = DynamicModelMultipleChoiceField(
|
provider_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Provider.objects.all(),
|
queryset=Provider.objects.all(),
|
||||||
@@ -110,9 +107,8 @@ class ProviderNetworkFilterForm(PrimaryModelFilterSetForm):
|
|||||||
class CircuitTypeFilterForm(OrganizationalModelFilterSetForm):
|
class CircuitTypeFilterForm(OrganizationalModelFilterSetForm):
|
||||||
model = CircuitType
|
model = CircuitType
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('color', name=_('Attributes')),
|
FieldSet('color', name=_('Attributes')),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
@@ -125,7 +121,7 @@ class CircuitTypeFilterForm(OrganizationalModelFilterSetForm):
|
|||||||
class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm):
|
class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = Circuit
|
model = Circuit
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
|
FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'type_id', 'status', 'install_date', 'termination_date', 'commit_rate', 'distance', 'distance_unit',
|
'type_id', 'status', 'install_date', 'termination_date', 'commit_rate', 'distance', 'distance_unit',
|
||||||
@@ -133,7 +129,6 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelF
|
|||||||
),
|
),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||||
)
|
)
|
||||||
selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'provider_id', 'provider_network_id')
|
selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'provider_id', 'provider_network_id')
|
||||||
@@ -279,9 +274,8 @@ class CircuitTerminationFilterForm(NetBoxModelFilterSetForm):
|
|||||||
class CircuitGroupFilterForm(TenancyFilterForm, OrganizationalModelFilterSetForm):
|
class CircuitGroupFilterForm(TenancyFilterForm, OrganizationalModelFilterSetForm):
|
||||||
model = CircuitGroup
|
model = CircuitGroup
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
@@ -318,9 +312,8 @@ class CircuitGroupAssignmentFilterForm(NetBoxModelFilterSetForm):
|
|||||||
class VirtualCircuitTypeFilterForm(OrganizationalModelFilterSetForm):
|
class VirtualCircuitTypeFilterForm(OrganizationalModelFilterSetForm):
|
||||||
model = VirtualCircuitType
|
model = VirtualCircuitType
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('color', name=_('Attributes')),
|
FieldSet('color', name=_('Attributes')),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
@@ -333,11 +326,10 @@ class VirtualCircuitTypeFilterForm(OrganizationalModelFilterSetForm):
|
|||||||
class VirtualCircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm):
|
class VirtualCircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = VirtualCircuit
|
model = VirtualCircuit
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
|
FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
|
||||||
FieldSet('type_id', 'status', name=_('Attributes')),
|
FieldSet('type_id', 'status', name=_('Attributes')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
)
|
)
|
||||||
selector_fields = ('filter_id', 'q', 'provider_id', 'provider_network_id')
|
selector_fields = ('filter_id', 'q', 'provider_id', 'provider_network_id')
|
||||||
provider_id = DynamicModelMultipleChoiceField(
|
provider_id = DynamicModelMultipleChoiceField(
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ class CircuitTypeForm(OrganizationalModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitType
|
model = CircuitType
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'slug', 'color', 'description', 'comments', 'tags',
|
'name', 'slug', 'color', 'description', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -236,7 +236,7 @@ class CircuitGroupForm(TenancyForm, OrganizationalModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitGroup
|
model = CircuitGroup
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'slug', 'description', 'tenant_group', 'tenant', 'owner', 'comments', 'tags',
|
'name', 'slug', 'description', 'tenant_group', 'tenant', 'owner', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -307,7 +307,7 @@ class VirtualCircuitTypeForm(OrganizationalModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = VirtualCircuitType
|
model = VirtualCircuitType
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'slug', 'color', 'description', 'owner', 'comments', 'tags',
|
'name', 'slug', 'color', 'description', 'owner', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
import django.contrib.postgres.fields
|
|
||||||
import django.core.validators
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
('circuits', '0053_owner'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='circuittermination',
|
|
||||||
name='cable_connector',
|
|
||||||
field=models.PositiveSmallIntegerField(
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
validators=[
|
|
||||||
django.core.validators.MinValueValidator(1),
|
|
||||||
django.core.validators.MaxValueValidator(256)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='circuittermination',
|
|
||||||
name='cable_positions',
|
|
||||||
field=django.contrib.postgres.fields.ArrayField(
|
|
||||||
base_field=models.PositiveSmallIntegerField(
|
|
||||||
validators=[
|
|
||||||
django.core.validators.MinValueValidator(1),
|
|
||||||
django.core.validators.MaxValueValidator(1024),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
size=None,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('circuits', '0053_owner'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='circuittermination',
|
||||||
|
name='cable_position',
|
||||||
|
field=models.PositiveIntegerField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.MinValueValidator(1),
|
||||||
|
django.core.validators.MaxValueValidator(1024),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# Generated by Django 5.2.8 on 2025-12-08 17:38
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('circuits', '0054_cable_connector_positions'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='circuitgroup',
|
|
||||||
name='comments',
|
|
||||||
field=models.TextField(blank=True),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='circuittype',
|
|
||||||
name='comments',
|
|
||||||
field=models.TextField(blank=True),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='virtualcircuittype',
|
|
||||||
name='comments',
|
|
||||||
field=models.TextField(blank=True),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
('circuits', '0055_add_comments_to_organizationalmodel'),
|
|
||||||
('contenttypes', '0002_remove_content_type_name'),
|
|
||||||
('dcim', '0224_add_comments_to_organizationalmodel'),
|
|
||||||
('extras', '0134_owner'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name='circuittermination',
|
|
||||||
index=models.Index(fields=['termination_type', 'termination_id'], name='circuits_ci_termina_505dda_idx'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -335,9 +335,6 @@ class CircuitTermination(
|
|||||||
name='%(app_label)s_%(class)s_unique_circuit_term_side'
|
name='%(app_label)s_%(class)s_unique_circuit_term_side'
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
indexes = (
|
|
||||||
models.Index(fields=('termination_type', 'termination_id')),
|
|
||||||
)
|
|
||||||
verbose_name = _('circuit termination')
|
verbose_name = _('circuit termination')
|
||||||
verbose_name_plural = _('circuit terminations')
|
verbose_name_plural = _('circuit terminations')
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ class CircuitGroupIndex(SearchIndex):
|
|||||||
('name', 100),
|
('name', 100),
|
||||||
('slug', 110),
|
('slug', 110),
|
||||||
('description', 500),
|
('description', 500),
|
||||||
('comments', 5000),
|
|
||||||
)
|
)
|
||||||
display_attrs = ('description',)
|
display_attrs = ('description',)
|
||||||
|
|
||||||
@@ -45,7 +44,6 @@ class CircuitTypeIndex(SearchIndex):
|
|||||||
('name', 100),
|
('name', 100),
|
||||||
('slug', 110),
|
('slug', 110),
|
||||||
('description', 500),
|
('description', 500),
|
||||||
('comments', 5000),
|
|
||||||
)
|
)
|
||||||
display_attrs = ('description',)
|
display_attrs = ('description',)
|
||||||
|
|
||||||
@@ -111,6 +109,5 @@ class VirtualCircuitTypeIndex(SearchIndex):
|
|||||||
('name', 100),
|
('name', 100),
|
||||||
('slug', 110),
|
('slug', 110),
|
||||||
('description', 500),
|
('description', 500),
|
||||||
('comments', 5000),
|
|
||||||
)
|
)
|
||||||
display_attrs = ('description',)
|
display_attrs = ('description',)
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ class CircuitTypeTable(OrganizationalModelTable):
|
|||||||
class Meta(OrganizationalModelTable.Meta):
|
class Meta(OrganizationalModelTable.Meta):
|
||||||
model = CircuitType
|
model = CircuitType
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'circuit_count', 'color', 'description', 'slug', 'comments', 'tags', 'created',
|
'pk', 'id', 'name', 'circuit_count', 'color', 'description', 'slug', 'tags', 'created', 'last_updated',
|
||||||
'last_updated', 'actions',
|
'actions',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'circuit_count', 'color', 'description')
|
default_columns = ('pk', 'name', 'circuit_count', 'color', 'description')
|
||||||
|
|
||||||
@@ -175,7 +175,7 @@ class CircuitGroupTable(OrganizationalModelTable):
|
|||||||
class Meta(OrganizationalModelTable.Meta):
|
class Meta(OrganizationalModelTable.Meta):
|
||||||
model = CircuitGroup
|
model = CircuitGroup
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'name', 'description', 'circuit_group_assignment_count', 'comments', 'tags',
|
'pk', 'name', 'description', 'circuit_group_assignment_count', 'tags',
|
||||||
'created', 'last_updated', 'actions',
|
'created', 'last_updated', 'actions',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'description', 'circuit_group_assignment_count')
|
default_columns = ('pk', 'name', 'description', 'circuit_group_assignment_count')
|
||||||
|
|||||||
@@ -433,7 +433,7 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = CircuitTermination.objects.all()
|
queryset = CircuitTermination.objects.all()
|
||||||
filterset = CircuitTerminationFilterSet
|
filterset = CircuitTerminationFilterSet
|
||||||
ignore_fields = ('cable', 'cable_positions')
|
ignore_fields = ('cable',)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
|
|||||||
@@ -44,4 +44,3 @@ class DataFileSerializer(NetBoxModelSerializer):
|
|||||||
'id', 'url', 'display_url', 'display', 'source', 'path', 'last_updated', 'size', 'hash',
|
'id', 'url', 'display_url', 'display', 'source', 'path', 'last_updated', 'size', 'hash',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'path')
|
brief_fields = ('id', 'url', 'display', 'path')
|
||||||
read_only_fields = ['path', 'last_updated', 'size', 'hash']
|
|
||||||
|
|||||||
@@ -31,8 +31,7 @@ class JobSerializer(BaseModelSerializer):
|
|||||||
model = Job
|
model = Job
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'object_type', 'object_id', 'object', 'name', 'status', 'created',
|
'id', 'url', 'display_url', 'display', 'object_type', 'object_id', 'object', 'name', 'status', 'created',
|
||||||
'scheduled', 'interval', 'started', 'completed', 'user', 'data', 'error', 'job_id', 'queue_name',
|
'scheduled', 'interval', 'started', 'completed', 'user', 'data', 'error', 'job_id', 'log_entries',
|
||||||
'log_entries',
|
|
||||||
]
|
]
|
||||||
brief_fields = ('url', 'created', 'completed', 'user', 'status')
|
brief_fields = ('url', 'created', 'completed', 'user', 'status')
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from rest_framework.decorators import action
|
|||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.routers import APIRootView
|
from rest_framework.routers import APIRootView
|
||||||
|
from rest_framework.viewsets import ReadOnlyModelViewSet
|
||||||
from rq.job import Job as RQ_Job
|
from rq.job import Job as RQ_Job
|
||||||
from rq.worker import Worker
|
from rq.worker import Worker
|
||||||
|
|
||||||
@@ -63,7 +64,7 @@ class DataFileViewSet(NetBoxReadOnlyModelViewSet):
|
|||||||
filterset_class = filtersets.DataFileFilterSet
|
filterset_class = filtersets.DataFileFilterSet
|
||||||
|
|
||||||
|
|
||||||
class JobViewSet(NetBoxReadOnlyModelViewSet):
|
class JobViewSet(ReadOnlyModelViewSet):
|
||||||
"""
|
"""
|
||||||
Retrieve a list of job results
|
Retrieve a list of job results
|
||||||
"""
|
"""
|
||||||
@@ -72,20 +73,19 @@ class JobViewSet(NetBoxReadOnlyModelViewSet):
|
|||||||
filterset_class = filtersets.JobFilterSet
|
filterset_class = filtersets.JobFilterSet
|
||||||
|
|
||||||
|
|
||||||
class ObjectChangeViewSet(NetBoxReadOnlyModelViewSet):
|
class ObjectChangeViewSet(ReadOnlyModelViewSet):
|
||||||
"""
|
"""
|
||||||
Retrieve a list of recent changes.
|
Retrieve a list of recent changes.
|
||||||
"""
|
"""
|
||||||
metadata_class = ContentTypeMetadata
|
metadata_class = ContentTypeMetadata
|
||||||
queryset = ObjectChange.objects.all()
|
|
||||||
serializer_class = serializers.ObjectChangeSerializer
|
serializer_class = serializers.ObjectChangeSerializer
|
||||||
filterset_class = filtersets.ObjectChangeFilterSet
|
filterset_class = filtersets.ObjectChangeFilterSet
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return super().get_queryset().valid_models()
|
return ObjectChange.objects.valid_models()
|
||||||
|
|
||||||
|
|
||||||
class ObjectTypeViewSet(NetBoxReadOnlyModelViewSet):
|
class ObjectTypeViewSet(ReadOnlyModelViewSet):
|
||||||
"""
|
"""
|
||||||
Read-only list of ObjectTypes.
|
Read-only list of ObjectTypes.
|
||||||
"""
|
"""
|
||||||
@@ -94,16 +94,6 @@ class ObjectTypeViewSet(NetBoxReadOnlyModelViewSet):
|
|||||||
serializer_class = serializers.ObjectTypeSerializer
|
serializer_class = serializers.ObjectTypeSerializer
|
||||||
filterset_class = filtersets.ObjectTypeFilterSet
|
filterset_class = filtersets.ObjectTypeFilterSet
|
||||||
|
|
||||||
def initial(self, request, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Override initial() to skip the restrict() call since ObjectType (a ContentType proxy)
|
|
||||||
doesn't use RestrictedQuerySet and is publicly accessible metadata.
|
|
||||||
"""
|
|
||||||
# Call GenericViewSet.initial() directly, skipping BaseViewSet.initial()
|
|
||||||
# which would try to call restrict() on the queryset
|
|
||||||
from rest_framework.viewsets import GenericViewSet
|
|
||||||
GenericViewSet.initial(self, request, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseRQViewSet(viewsets.ViewSet):
|
class BaseRQViewSet(viewsets.ViewSet):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -129,14 +129,10 @@ class JobFilterSet(BaseFilterSet):
|
|||||||
choices=JobStatusChoices,
|
choices=JobStatusChoices,
|
||||||
null_value=None
|
null_value=None
|
||||||
)
|
)
|
||||||
queue_name = django_filters.CharFilter()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Job
|
model = Job
|
||||||
fields = (
|
fields = ('id', 'object_type', 'object_type_id', 'object_id', 'name', 'interval', 'status', 'user', 'job_id')
|
||||||
'id', 'object_type', 'object_type_id', 'object_id', 'name', 'interval', 'status', 'user', 'job_id',
|
|
||||||
'queue_name',
|
|
||||||
)
|
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
|
|||||||
@@ -26,9 +26,8 @@ __all__ = (
|
|||||||
class DataSourceFilterForm(PrimaryModelFilterSetForm):
|
class DataSourceFilterForm(PrimaryModelFilterSetForm):
|
||||||
model = DataSource
|
model = DataSource
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('type', 'status', 'enabled', 'sync_interval', name=_('Data Source')),
|
FieldSet('type', 'status', 'enabled', 'sync_interval', name=_('Data Source')),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
)
|
)
|
||||||
type = forms.MultipleChoiceField(
|
type = forms.MultipleChoiceField(
|
||||||
label=_('Type'),
|
label=_('Type'),
|
||||||
@@ -72,7 +71,7 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
model = Job
|
model = Job
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id'),
|
FieldSet('q', 'filter_id'),
|
||||||
FieldSet('object_type_id', 'status', 'queue_name', name=_('Attributes')),
|
FieldSet('object_type_id', 'status', name=_('Attributes')),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'created__before', 'created__after', 'scheduled__before', 'scheduled__after', 'started__before',
|
'created__before', 'created__after', 'scheduled__before', 'scheduled__after', 'started__before',
|
||||||
'started__after', 'completed__before', 'completed__after', 'user', name=_('Creation')
|
'started__after', 'completed__before', 'completed__after', 'user', name=_('Creation')
|
||||||
@@ -88,10 +87,6 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
choices=JobStatusChoices,
|
choices=JobStatusChoices,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
queue_name = forms.CharField(
|
|
||||||
label=_('Queue'),
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
created__after = forms.DateTimeField(
|
created__after = forms.DateTimeField(
|
||||||
label=_('Created after'),
|
label=_('Created after'),
|
||||||
required=False,
|
required=False,
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 5.2.9 on 2026-01-27 00:39
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('core', '0020_owner'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='job',
|
|
||||||
name='queue_name',
|
|
||||||
field=models.CharField(blank=True, max_length=100),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -63,20 +63,16 @@ class ConfigRevision(models.Model):
|
|||||||
return reverse('core:config') # Default config view
|
return reverse('core:config') # Default config view
|
||||||
return reverse('core:configrevision', args=[self.pk])
|
return reverse('core:configrevision', args=[self.pk])
|
||||||
|
|
||||||
def activate(self, update_db=True):
|
def activate(self):
|
||||||
"""
|
"""
|
||||||
Cache the configuration data.
|
Cache the configuration data.
|
||||||
|
|
||||||
Parameters:
|
|
||||||
update_db: Mark the ConfigRevision as active in the database (default: True)
|
|
||||||
"""
|
"""
|
||||||
cache.set('config', self.data, None)
|
cache.set('config', self.data, None)
|
||||||
cache.set('config_version', self.pk, None)
|
cache.set('config_version', self.pk, None)
|
||||||
|
|
||||||
if update_db:
|
# Set all instances of ConfigRevision to false and set this instance to true
|
||||||
# Set all instances of ConfigRevision to false and set this instance to true
|
ConfigRevision.objects.all().update(active=False)
|
||||||
ConfigRevision.objects.all().update(active=False)
|
ConfigRevision.objects.filter(pk=self.pk).update(active=True)
|
||||||
ConfigRevision.objects.filter(pk=self.pk).update(active=True)
|
|
||||||
|
|
||||||
activate.alters_data = True
|
activate.alters_data = True
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from django.core.validators import RegexValidator
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from netbox.constants import CENSOR_TOKEN, CENSOR_TOKEN_CHANGED
|
from netbox.constants import CENSOR_TOKEN, CENSOR_TOKEN_CHANGED
|
||||||
from netbox.models import PrimaryModel
|
from netbox.models import PrimaryModel
|
||||||
@@ -128,24 +128,9 @@ class DataSource(JobsMixin, PrimaryModel):
|
|||||||
# Ensure URL scheme matches selected type
|
# Ensure URL scheme matches selected type
|
||||||
if self.backend_class.is_local and self.url_scheme not in ('file', ''):
|
if self.backend_class.is_local and self.url_scheme not in ('file', ''):
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'source_url': _("URLs for local sources must start with {scheme} (or specify no scheme)").format(
|
'source_url': "URLs for local sources must start with file:// (or specify no scheme)"
|
||||||
scheme='file://'
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
|
|
||||||
# If recurring sync is disabled for an existing DataSource, clear any pending sync jobs for it and reset its
|
|
||||||
# "queued" status
|
|
||||||
if not self._state.adding and not self.sync_interval:
|
|
||||||
self.jobs.filter(status=JobStatusChoices.STATUS_PENDING).delete()
|
|
||||||
if self.status == DataSourceStatusChoices.QUEUED and self.last_synced:
|
|
||||||
self.status = DataSourceStatusChoices.COMPLETED
|
|
||||||
elif self.status == DataSourceStatusChoices.QUEUED:
|
|
||||||
self.status = DataSourceStatusChoices.NEW
|
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
def to_objectchange(self, action):
|
def to_objectchange(self, action):
|
||||||
objectchange = super().to_objectchange(action)
|
objectchange = super().to_objectchange(action)
|
||||||
|
|
||||||
|
|||||||
@@ -112,12 +112,6 @@ class Job(models.Model):
|
|||||||
verbose_name=_('job ID'),
|
verbose_name=_('job ID'),
|
||||||
unique=True
|
unique=True
|
||||||
)
|
)
|
||||||
queue_name = models.CharField(
|
|
||||||
verbose_name=_('queue name'),
|
|
||||||
max_length=100,
|
|
||||||
blank=True,
|
|
||||||
help_text=_('Name of the queue in which this job was enqueued')
|
|
||||||
)
|
|
||||||
log_entries = ArrayField(
|
log_entries = ArrayField(
|
||||||
verbose_name=_('log entries'),
|
verbose_name=_('log entries'),
|
||||||
base_field=models.JSONField(
|
base_field=models.JSONField(
|
||||||
@@ -185,15 +179,11 @@ class Job(models.Model):
|
|||||||
return f"{int(minutes)} minutes, {seconds:.2f} seconds"
|
return f"{int(minutes)} minutes, {seconds:.2f} seconds"
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
# Use the stored queue name, or fall back to get_queue_for_model for legacy jobs
|
|
||||||
rq_queue_name = self.queue_name or get_queue_for_model(self.object_type.model if self.object_type else None)
|
|
||||||
rq_job_id = str(self.job_id)
|
|
||||||
|
|
||||||
super().delete(*args, **kwargs)
|
super().delete(*args, **kwargs)
|
||||||
|
|
||||||
# Cancel the RQ job using the stored queue name
|
rq_queue_name = get_queue_for_model(self.object_type.model if self.object_type else None)
|
||||||
queue = django_rq.get_queue(rq_queue_name)
|
queue = django_rq.get_queue(rq_queue_name)
|
||||||
job = queue.fetch_job(rq_job_id)
|
job = queue.fetch_job(str(self.job_id))
|
||||||
|
|
||||||
if job:
|
if job:
|
||||||
try:
|
try:
|
||||||
@@ -298,8 +288,7 @@ class Job(models.Model):
|
|||||||
scheduled=schedule_at,
|
scheduled=schedule_at,
|
||||||
interval=interval,
|
interval=interval,
|
||||||
user=user,
|
user=user,
|
||||||
job_id=uuid.uuid4(),
|
job_id=uuid.uuid4()
|
||||||
queue_name=rq_queue_name
|
|
||||||
)
|
)
|
||||||
job.full_clean()
|
job.full_clean()
|
||||||
job.save()
|
job.save()
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ from django.db import connection, models
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from netbox.context import query_cache
|
|
||||||
from netbox.plugins import PluginConfig
|
from netbox.plugins import PluginConfig
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
from utilities.string import title
|
from utilities.string import title
|
||||||
@@ -36,10 +35,6 @@ class ObjectTypeQuerySet(models.QuerySet):
|
|||||||
|
|
||||||
class ObjectTypeManager(models.Manager):
|
class ObjectTypeManager(models.Manager):
|
||||||
|
|
||||||
# TODO: Remove this in NetBox v5.0
|
|
||||||
# Cache the result of introspection to avoid repeated queries.
|
|
||||||
_table_exists = False
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return ObjectTypeQuerySet(self.model, using=self._db)
|
return ObjectTypeQuerySet(self.model, using=self._db)
|
||||||
|
|
||||||
@@ -71,21 +66,13 @@ class ObjectTypeManager(models.Manager):
|
|||||||
"""
|
"""
|
||||||
from netbox.models.features import get_model_features, model_is_public
|
from netbox.models.features import get_model_features, model_is_public
|
||||||
|
|
||||||
# Check the request cache before hitting the database
|
|
||||||
cache = query_cache.get()
|
|
||||||
if cache is not None:
|
|
||||||
if ot := cache['object_types'].get((model._meta.model, for_concrete_model)):
|
|
||||||
return ot
|
|
||||||
|
|
||||||
# TODO: Remove this in NetBox v5.0
|
# TODO: Remove this in NetBox v5.0
|
||||||
# If the ObjectType table has not yet been provisioned (e.g. because we're in a pre-v4.4 migration),
|
# If the ObjectType table has not yet been provisioned (e.g. because we're in a pre-v4.4 migration),
|
||||||
# fall back to ContentType.
|
# fall back to ContentType.
|
||||||
if not ObjectTypeManager._table_exists:
|
if 'core_objecttype' not in connection.introspection.table_names():
|
||||||
if 'core_objecttype' not in connection.introspection.table_names():
|
ct = ContentType.objects.get_for_model(model, for_concrete_model=for_concrete_model)
|
||||||
ct = ContentType.objects.get_for_model(model, for_concrete_model=for_concrete_model)
|
ct.features = get_model_features(ct.model_class())
|
||||||
ct.features = get_model_features(ct.model_class())
|
return ct
|
||||||
return ct
|
|
||||||
ObjectTypeManager._table_exists = True
|
|
||||||
|
|
||||||
if not inspect.isclass(model):
|
if not inspect.isclass(model):
|
||||||
model = model.__class__
|
model = model.__class__
|
||||||
@@ -103,10 +90,6 @@ class ObjectTypeManager(models.Manager):
|
|||||||
features=get_model_features(model),
|
features=get_model_features(model),
|
||||||
)[0]
|
)[0]
|
||||||
|
|
||||||
# Populate the request cache to avoid redundant lookups
|
|
||||||
if cache is not None:
|
|
||||||
cache['object_types'][(model._meta.model, for_concrete_model)] = ot
|
|
||||||
|
|
||||||
return ot
|
return ot
|
||||||
|
|
||||||
def get_for_models(self, *models, for_concrete_models=True):
|
def get_for_models(self, *models, for_concrete_models=True):
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from threading import local
|
|||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||||
from django.db.models import CASCADE, RESTRICT
|
from django.db.models import CASCADE
|
||||||
from django.db.models.fields.reverse_related import ManyToManyRel, ManyToOneRel
|
from django.db.models.fields.reverse_related import ManyToManyRel, ManyToOneRel
|
||||||
from django.db.models.signals import m2m_changed, post_migrate, post_save, pre_delete
|
from django.db.models.signals import m2m_changed, post_migrate, post_save, pre_delete
|
||||||
from django.dispatch import receiver, Signal
|
from django.dispatch import receiver, Signal
|
||||||
@@ -18,7 +18,6 @@ from extras.events import enqueue_event
|
|||||||
from extras.models import Tag
|
from extras.models import Tag
|
||||||
from extras.utils import run_validators
|
from extras.utils import run_validators
|
||||||
from netbox.config import get_config
|
from netbox.config import get_config
|
||||||
from utilities.data import get_config_value_ci
|
|
||||||
from netbox.context import current_request, events_queue
|
from netbox.context import current_request, events_queue
|
||||||
from netbox.models.features import ChangeLoggingMixin, get_model_features, model_is_public
|
from netbox.models.features import ChangeLoggingMixin, get_model_features, model_is_public
|
||||||
from utilities.exceptions import AbortRequest
|
from utilities.exceptions import AbortRequest
|
||||||
@@ -169,7 +168,7 @@ def handle_deleted_object(sender, instance, **kwargs):
|
|||||||
# to queueing any events for the object being deleted, in case a validation error is
|
# to queueing any events for the object being deleted, in case a validation error is
|
||||||
# raised, causing the deletion to fail.
|
# raised, causing the deletion to fail.
|
||||||
model_name = f'{sender._meta.app_label}.{sender._meta.model_name}'
|
model_name = f'{sender._meta.app_label}.{sender._meta.model_name}'
|
||||||
validators = get_config_value_ci(get_config().PROTECTION_RULES, model_name, default=[])
|
validators = get_config().PROTECTION_RULES.get(model_name, [])
|
||||||
try:
|
try:
|
||||||
run_validators(instance, validators)
|
run_validators(instance, validators)
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
@@ -222,7 +221,7 @@ def handle_deleted_object(sender, instance, **kwargs):
|
|||||||
obj.snapshot() # Ensure the change record includes the "before" state
|
obj.snapshot() # Ensure the change record includes the "before" state
|
||||||
if type(relation) is ManyToManyRel:
|
if type(relation) is ManyToManyRel:
|
||||||
getattr(obj, related_field_name).remove(instance)
|
getattr(obj, related_field_name).remove(instance)
|
||||||
elif type(relation) is ManyToOneRel and relation.null and relation.on_delete not in (CASCADE, RESTRICT):
|
elif type(relation) is ManyToOneRel and relation.null and relation.on_delete is not CASCADE:
|
||||||
setattr(obj, related_field_name, None)
|
setattr(obj, related_field_name, None)
|
||||||
obj.save()
|
obj.save()
|
||||||
|
|
||||||
|
|||||||
@@ -42,9 +42,6 @@ class JobTable(NetBoxTable):
|
|||||||
completed = columns.DateTimeColumn(
|
completed = columns.DateTimeColumn(
|
||||||
verbose_name=_('Completed'),
|
verbose_name=_('Completed'),
|
||||||
)
|
)
|
||||||
queue_name = tables.Column(
|
|
||||||
verbose_name=_('Queue'),
|
|
||||||
)
|
|
||||||
log_entries = tables.Column(
|
log_entries = tables.Column(
|
||||||
verbose_name=_('Log Entries'),
|
verbose_name=_('Log Entries'),
|
||||||
)
|
)
|
||||||
@@ -56,7 +53,7 @@ class JobTable(NetBoxTable):
|
|||||||
model = Job
|
model = Job
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'object_type', 'object', 'name', 'status', 'created', 'scheduled', 'interval', 'started',
|
'pk', 'id', 'object_type', 'object', 'name', 'status', 'created', 'scheduled', 'interval', 'started',
|
||||||
'completed', 'user', 'queue_name', 'log_entries', 'error', 'job_id',
|
'completed', 'user', 'error', 'job_id',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'id', 'object_type', 'object', 'name', 'status', 'created', 'started', 'completed', 'user',
|
'pk', 'id', 'object_type', 'object', 'name', 'status', 'created', 'started', 'completed', 'user',
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
from unittest.mock import patch, MagicMock
|
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from core.models import DataSource, Job, ObjectType
|
from core.models import DataSource, ObjectType
|
||||||
from core.choices import ObjectChangeActionChoices
|
from core.choices import ObjectChangeActionChoices
|
||||||
from dcim.models import Site, Location, Device
|
from dcim.models import Site, Location, Device
|
||||||
from netbox.constants import CENSOR_TOKEN, CENSOR_TOKEN_CHANGED
|
from netbox.constants import CENSOR_TOKEN, CENSOR_TOKEN_CHANGED
|
||||||
@@ -202,38 +200,3 @@ class ObjectTypeTest(TestCase):
|
|||||||
bookmarks_ots = ObjectType.objects.with_feature('bookmarks')
|
bookmarks_ots = ObjectType.objects.with_feature('bookmarks')
|
||||||
self.assertIn(ObjectType.objects.get_by_natural_key('dcim', 'site'), bookmarks_ots)
|
self.assertIn(ObjectType.objects.get_by_natural_key('dcim', 'site'), bookmarks_ots)
|
||||||
self.assertNotIn(ObjectType.objects.get_by_natural_key('dcim', 'cabletermination'), bookmarks_ots)
|
self.assertNotIn(ObjectType.objects.get_by_natural_key('dcim', 'cabletermination'), bookmarks_ots)
|
||||||
|
|
||||||
|
|
||||||
class JobTest(TestCase):
|
|
||||||
|
|
||||||
@patch('core.models.jobs.django_rq.get_queue')
|
|
||||||
def test_delete_cancels_job_from_correct_queue(self, mock_get_queue):
|
|
||||||
"""
|
|
||||||
Test that when a job is deleted, it's canceled from the correct queue.
|
|
||||||
"""
|
|
||||||
mock_queue = MagicMock()
|
|
||||||
mock_rq_job = MagicMock()
|
|
||||||
mock_queue.fetch_job.return_value = mock_rq_job
|
|
||||||
mock_get_queue.return_value = mock_queue
|
|
||||||
|
|
||||||
def dummy_func(**kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Enqueue a job with a custom queue name
|
|
||||||
custom_queue = 'my_custom_queue'
|
|
||||||
job = Job.enqueue(
|
|
||||||
func=dummy_func,
|
|
||||||
name='Test Job',
|
|
||||||
queue_name=custom_queue
|
|
||||||
)
|
|
||||||
|
|
||||||
# Reset mock to clear enqueue call
|
|
||||||
mock_get_queue.reset_mock()
|
|
||||||
|
|
||||||
# Delete the job
|
|
||||||
job.delete()
|
|
||||||
|
|
||||||
# Verify the correct queue was used for cancellation
|
|
||||||
mock_get_queue.assert_called_with(custom_queue)
|
|
||||||
mock_queue.fetch_job.assert_called_with(str(job.job_id))
|
|
||||||
mock_rq_job.cancel.assert_called_once()
|
|
||||||
|
|||||||
@@ -2,12 +2,10 @@ from drf_spectacular.types import OpenApiTypes
|
|||||||
from drf_spectacular.utils import extend_schema_field
|
from drf_spectacular.utils import extend_schema_field
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from dcim.models import FrontPort, FrontPortTemplate, PortMapping, PortTemplateMapping, RearPort, RearPortTemplate
|
|
||||||
from utilities.api import get_serializer_for_model
|
from utilities.api import get_serializer_for_model
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ConnectedEndpointsSerializer',
|
'ConnectedEndpointsSerializer',
|
||||||
'PortSerializer',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -37,53 +35,3 @@ class ConnectedEndpointsSerializer(serializers.ModelSerializer):
|
|||||||
@extend_schema_field(serializers.BooleanField)
|
@extend_schema_field(serializers.BooleanField)
|
||||||
def get_connected_endpoints_reachable(self, obj):
|
def get_connected_endpoints_reachable(self, obj):
|
||||||
return obj._path and obj._path.is_complete and obj._path.is_active
|
return obj._path and obj._path.is_complete and obj._path.is_active
|
||||||
|
|
||||||
|
|
||||||
class PortSerializer(serializers.ModelSerializer):
|
|
||||||
"""
|
|
||||||
Base serializer for front & rear port and port templates.
|
|
||||||
"""
|
|
||||||
@property
|
|
||||||
def _mapper(self):
|
|
||||||
"""
|
|
||||||
Return the model and ForeignKey field name used to track port mappings for this model.
|
|
||||||
"""
|
|
||||||
if self.Meta.model is FrontPort:
|
|
||||||
return PortMapping, 'front_port'
|
|
||||||
if self.Meta.model is RearPort:
|
|
||||||
return PortMapping, 'rear_port'
|
|
||||||
if self.Meta.model is FrontPortTemplate:
|
|
||||||
return PortTemplateMapping, 'front_port'
|
|
||||||
if self.Meta.model is RearPortTemplate:
|
|
||||||
return PortTemplateMapping, 'rear_port'
|
|
||||||
raise ValueError(f"Could not determine mapping details for {self.__class__}")
|
|
||||||
|
|
||||||
def create(self, validated_data):
|
|
||||||
mappings = validated_data.pop('mappings', [])
|
|
||||||
instance = super().create(validated_data)
|
|
||||||
|
|
||||||
# Create port mappings
|
|
||||||
mapping_model, fk_name = self._mapper
|
|
||||||
for attrs in mappings:
|
|
||||||
mapping_model.objects.create(**{
|
|
||||||
fk_name: instance,
|
|
||||||
**attrs,
|
|
||||||
})
|
|
||||||
|
|
||||||
return instance
|
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
|
||||||
mappings = validated_data.pop('mappings', None)
|
|
||||||
instance = super().update(instance, validated_data)
|
|
||||||
|
|
||||||
if mappings is not None:
|
|
||||||
# Update port mappings
|
|
||||||
mapping_model, fk_name = self._mapper
|
|
||||||
mapping_model.objects.filter(**{fk_name: instance}).delete()
|
|
||||||
for attrs in mappings:
|
|
||||||
mapping_model.objects.create(**{
|
|
||||||
fk_name: instance,
|
|
||||||
**attrs,
|
|
||||||
})
|
|
||||||
|
|
||||||
return instance
|
|
||||||
|
|||||||
@@ -61,12 +61,11 @@ class CableTerminationSerializer(NetBoxModelSerializer):
|
|||||||
model = CableTermination
|
model = CableTermination
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'cable', 'cable_end', 'termination_type', 'termination_id',
|
'id', 'url', 'display', 'cable', 'cable_end', 'termination_type', 'termination_id',
|
||||||
'termination', 'connector', 'positions', 'created', 'last_updated',
|
'termination', 'position', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
brief_fields = (
|
brief_fields = (
|
||||||
'id', 'url', 'display', 'cable', 'cable_end', 'connector', 'positions', 'termination_type',
|
'id', 'url', 'display', 'cable', 'cable_end', 'position', 'termination_type', 'termination_id',
|
||||||
'termination_id',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,25 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.models import (
|
from dcim.models import (
|
||||||
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, ModuleBay, PortMapping,
|
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, ModuleBay, PowerOutlet, PowerPort,
|
||||||
PowerOutlet, PowerPort, RearPort, VirtualDeviceContext,
|
RearPort, VirtualDeviceContext,
|
||||||
)
|
)
|
||||||
from ipam.api.serializers_.vlans import VLANSerializer, VLANTranslationPolicySerializer
|
from ipam.api.serializers_.vlans import VLANSerializer, VLANTranslationPolicySerializer
|
||||||
from ipam.api.serializers_.vrfs import VRFSerializer
|
from ipam.api.serializers_.vrfs import VRFSerializer
|
||||||
from ipam.models import VLAN
|
from ipam.models import VLAN
|
||||||
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
||||||
from netbox.api.gfk_fields import GFKSerializerField
|
from netbox.api.gfk_fields import GFKSerializerField
|
||||||
from netbox.api.serializers import NetBoxModelSerializer
|
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
|
||||||
from users.api.serializers_.mixins import OwnerMixin
|
|
||||||
from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer
|
from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer
|
||||||
from wireless.api.serializers_.nested import NestedWirelessLinkSerializer
|
from wireless.api.serializers_.nested import NestedWirelessLinkSerializer
|
||||||
from wireless.api.serializers_.wirelesslans import WirelessLANSerializer
|
from wireless.api.serializers_.wirelesslans import WirelessLANSerializer
|
||||||
from wireless.choices import *
|
from wireless.choices import *
|
||||||
from wireless.models import WirelessLAN
|
from wireless.models import WirelessLAN
|
||||||
from .base import ConnectedEndpointsSerializer, PortSerializer
|
from .base import ConnectedEndpointsSerializer
|
||||||
from .cables import CabledObjectSerializer
|
from .cables import CabledObjectSerializer
|
||||||
from .devices import DeviceSerializer, MACAddressSerializer, ModuleSerializer, VirtualDeviceContextSerializer
|
from .devices import DeviceSerializer, MACAddressSerializer, ModuleSerializer, VirtualDeviceContextSerializer
|
||||||
from .manufacturers import ManufacturerSerializer
|
from .manufacturers import ManufacturerSerializer
|
||||||
@@ -41,12 +40,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortSerializer(
|
class ConsoleServerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
|
||||||
OwnerMixin,
|
|
||||||
NetBoxModelSerializer,
|
|
||||||
CabledObjectSerializer,
|
|
||||||
ConnectedEndpointsSerializer
|
|
||||||
):
|
|
||||||
device = DeviceSerializer(nested=True)
|
device = DeviceSerializer(nested=True)
|
||||||
module = ModuleSerializer(
|
module = ModuleSerializer(
|
||||||
nested=True,
|
nested=True,
|
||||||
@@ -70,18 +64,13 @@ class ConsoleServerPortSerializer(
|
|||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description',
|
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description',
|
||||||
'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints',
|
'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints',
|
||||||
'connected_endpoints_type', 'connected_endpoints_reachable', 'owner', 'tags', 'custom_fields', 'created',
|
'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
|
||||||
'last_updated', '_occupied',
|
'last_updated', '_occupied',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortSerializer(
|
class ConsolePortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
|
||||||
OwnerMixin,
|
|
||||||
NetBoxModelSerializer,
|
|
||||||
CabledObjectSerializer,
|
|
||||||
ConnectedEndpointsSerializer
|
|
||||||
):
|
|
||||||
device = DeviceSerializer(nested=True)
|
device = DeviceSerializer(nested=True)
|
||||||
module = ModuleSerializer(
|
module = ModuleSerializer(
|
||||||
nested=True,
|
nested=True,
|
||||||
@@ -105,18 +94,13 @@ class ConsolePortSerializer(
|
|||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description',
|
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description',
|
||||||
'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints',
|
'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints',
|
||||||
'connected_endpoints_type', 'connected_endpoints_reachable', 'owner', 'tags', 'custom_fields', 'created',
|
'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
|
||||||
'last_updated', '_occupied',
|
'last_updated', '_occupied',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
||||||
|
|
||||||
|
|
||||||
class PowerPortSerializer(
|
class PowerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
|
||||||
OwnerMixin,
|
|
||||||
NetBoxModelSerializer,
|
|
||||||
CabledObjectSerializer,
|
|
||||||
ConnectedEndpointsSerializer
|
|
||||||
):
|
|
||||||
device = DeviceSerializer(nested=True)
|
device = DeviceSerializer(nested=True)
|
||||||
module = ModuleSerializer(
|
module = ModuleSerializer(
|
||||||
nested=True,
|
nested=True,
|
||||||
@@ -136,18 +120,13 @@ class PowerPortSerializer(
|
|||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'maximum_draw',
|
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'maximum_draw',
|
||||||
'allocated_draw', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
|
'allocated_draw', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
|
||||||
'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'owner', 'tags',
|
'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields',
|
||||||
'custom_fields', 'created', 'last_updated', '_occupied',
|
'created', 'last_updated', '_occupied',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletSerializer(
|
class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
|
||||||
OwnerMixin,
|
|
||||||
NetBoxModelSerializer,
|
|
||||||
CabledObjectSerializer,
|
|
||||||
ConnectedEndpointsSerializer
|
|
||||||
):
|
|
||||||
device = DeviceSerializer(nested=True)
|
device = DeviceSerializer(nested=True)
|
||||||
module = ModuleSerializer(
|
module = ModuleSerializer(
|
||||||
nested=True,
|
nested=True,
|
||||||
@@ -180,17 +159,12 @@ class PowerOutletSerializer(
|
|||||||
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'status', 'color',
|
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'status', 'color',
|
||||||
'power_port', 'feed_leg', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers',
|
'power_port', 'feed_leg', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers',
|
||||||
'link_peers_type', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable',
|
'link_peers_type', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable',
|
||||||
'owner', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
||||||
|
|
||||||
|
|
||||||
class InterfaceSerializer(
|
class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
|
||||||
OwnerMixin,
|
|
||||||
NetBoxModelSerializer,
|
|
||||||
CabledObjectSerializer,
|
|
||||||
ConnectedEndpointsSerializer
|
|
||||||
):
|
|
||||||
device = DeviceSerializer(nested=True)
|
device = DeviceSerializer(nested=True)
|
||||||
vdcs = SerializedPKRelatedField(
|
vdcs = SerializedPKRelatedField(
|
||||||
queryset=VirtualDeviceContext.objects.all(),
|
queryset=VirtualDeviceContext.objects.all(),
|
||||||
@@ -208,7 +182,6 @@ class InterfaceSerializer(
|
|||||||
type = ChoiceField(choices=InterfaceTypeChoices)
|
type = ChoiceField(choices=InterfaceTypeChoices)
|
||||||
parent = NestedInterfaceSerializer(required=False, allow_null=True)
|
parent = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||||
bridge = NestedInterfaceSerializer(required=False, allow_null=True)
|
bridge = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||||
bridge_interfaces = NestedInterfaceSerializer(many=True, read_only=True)
|
|
||||||
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||||
mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_blank=True)
|
mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_blank=True)
|
||||||
duplex = ChoiceField(choices=InterfaceDuplexChoices, required=False, allow_blank=True, allow_null=True)
|
duplex = ChoiceField(choices=InterfaceDuplexChoices, required=False, allow_blank=True, allow_null=True)
|
||||||
@@ -248,13 +221,13 @@ class InterfaceSerializer(
|
|||||||
model = Interface
|
model = Interface
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'device', 'vdcs', 'module', 'name', 'label', 'type', 'enabled',
|
'id', 'url', 'display_url', 'display', 'device', 'vdcs', 'module', 'name', 'label', 'type', 'enabled',
|
||||||
'parent', 'bridge', 'bridge_interfaces', 'lag', 'mtu', 'mac_address', 'primary_mac_address',
|
'parent', 'bridge', 'lag', 'mtu', 'mac_address', 'primary_mac_address', 'mac_addresses', 'speed', 'duplex',
|
||||||
'mac_addresses', 'speed', 'duplex', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel',
|
'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'poe_mode', 'poe_type',
|
||||||
'poe_mode', 'poe_type', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan',
|
'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan',
|
||||||
'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy', 'mark_connected', 'cable', 'cable_end',
|
'vlan_translation_policy', 'mark_connected', 'cable', 'cable_end', 'wireless_link', 'link_peers',
|
||||||
'wireless_link', 'link_peers', 'link_peers_type', 'wireless_lans', 'vrf', 'l2vpn_termination',
|
'link_peers_type', 'wireless_lans', 'vrf', 'l2vpn_termination', 'connected_endpoints',
|
||||||
'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'owner', 'tags',
|
'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
|
||||||
'custom_fields', 'created', 'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
|
'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
||||||
|
|
||||||
@@ -321,20 +294,7 @@ class InterfaceSerializer(
|
|||||||
return super().validate(data)
|
return super().validate(data)
|
||||||
|
|
||||||
|
|
||||||
class RearPortMappingSerializer(serializers.ModelSerializer):
|
class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
||||||
position = serializers.IntegerField(
|
|
||||||
source='rear_port_position'
|
|
||||||
)
|
|
||||||
front_port = serializers.PrimaryKeyRelatedField(
|
|
||||||
queryset=FrontPort.objects.all(),
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = PortMapping
|
|
||||||
fields = ('position', 'front_port', 'front_port_position')
|
|
||||||
|
|
||||||
|
|
||||||
class RearPortSerializer(OwnerMixin, NetBoxModelSerializer, CabledObjectSerializer, PortSerializer):
|
|
||||||
device = DeviceSerializer(nested=True)
|
device = DeviceSerializer(nested=True)
|
||||||
module = ModuleSerializer(
|
module = ModuleSerializer(
|
||||||
nested=True,
|
nested=True,
|
||||||
@@ -343,36 +303,28 @@ class RearPortSerializer(OwnerMixin, NetBoxModelSerializer, CabledObjectSerializ
|
|||||||
allow_null=True
|
allow_null=True
|
||||||
)
|
)
|
||||||
type = ChoiceField(choices=PortTypeChoices)
|
type = ChoiceField(choices=PortTypeChoices)
|
||||||
front_ports = RearPortMappingSerializer(
|
|
||||||
source='mappings',
|
|
||||||
many=True,
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RearPort
|
model = RearPort
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions',
|
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions',
|
||||||
'front_ports', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
|
'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'tags',
|
||||||
'owner', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
'custom_fields', 'created', 'last_updated', '_occupied',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
||||||
|
|
||||||
|
|
||||||
class FrontPortMappingSerializer(serializers.ModelSerializer):
|
class FrontPortRearPortSerializer(WritableNestedSerializer):
|
||||||
position = serializers.IntegerField(
|
"""
|
||||||
source='front_port_position'
|
NestedRearPortSerializer but with parent device omitted (since front and rear ports must belong to same device)
|
||||||
)
|
"""
|
||||||
rear_port = serializers.PrimaryKeyRelatedField(
|
|
||||||
queryset=RearPort.objects.all(),
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PortMapping
|
model = RearPort
|
||||||
fields = ('position', 'rear_port', 'rear_port_position')
|
fields = ['id', 'url', 'display_url', 'display', 'name', 'label', 'description']
|
||||||
|
|
||||||
|
|
||||||
class FrontPortSerializer(OwnerMixin, NetBoxModelSerializer, CabledObjectSerializer, PortSerializer):
|
class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
||||||
device = DeviceSerializer(nested=True)
|
device = DeviceSerializer(nested=True)
|
||||||
module = ModuleSerializer(
|
module = ModuleSerializer(
|
||||||
nested=True,
|
nested=True,
|
||||||
@@ -381,23 +333,19 @@ class FrontPortSerializer(OwnerMixin, NetBoxModelSerializer, CabledObjectSeriali
|
|||||||
allow_null=True
|
allow_null=True
|
||||||
)
|
)
|
||||||
type = ChoiceField(choices=PortTypeChoices)
|
type = ChoiceField(choices=PortTypeChoices)
|
||||||
rear_ports = FrontPortMappingSerializer(
|
rear_port = FrontPortRearPortSerializer()
|
||||||
source='mappings',
|
|
||||||
many=True,
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = FrontPort
|
model = FrontPort
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions',
|
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port',
|
||||||
'rear_ports', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
|
'rear_port_position', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers',
|
||||||
'owner', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
||||||
|
|
||||||
|
|
||||||
class ModuleBaySerializer(OwnerMixin, NetBoxModelSerializer):
|
class ModuleBaySerializer(NetBoxModelSerializer):
|
||||||
device = DeviceSerializer(nested=True)
|
device = DeviceSerializer(nested=True)
|
||||||
module = ModuleSerializer(
|
module = ModuleSerializer(
|
||||||
nested=True,
|
nested=True,
|
||||||
@@ -417,12 +365,12 @@ class ModuleBaySerializer(OwnerMixin, NetBoxModelSerializer):
|
|||||||
model = ModuleBay
|
model = ModuleBay
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'installed_module', 'label', 'position',
|
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'installed_module', 'label', 'position',
|
||||||
'description', 'owner', 'tags', 'custom_fields', 'created', 'last_updated',
|
'description', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'installed_module', 'name', 'description')
|
brief_fields = ('id', 'url', 'display', 'installed_module', 'name', 'description')
|
||||||
|
|
||||||
|
|
||||||
class DeviceBaySerializer(OwnerMixin, NetBoxModelSerializer):
|
class DeviceBaySerializer(NetBoxModelSerializer):
|
||||||
device = DeviceSerializer(nested=True)
|
device = DeviceSerializer(nested=True)
|
||||||
installed_device = DeviceSerializer(nested=True, required=False, allow_null=True)
|
installed_device = DeviceSerializer(nested=True, required=False, allow_null=True)
|
||||||
|
|
||||||
@@ -430,12 +378,12 @@ class DeviceBaySerializer(OwnerMixin, NetBoxModelSerializer):
|
|||||||
model = DeviceBay
|
model = DeviceBay
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'device', 'name', 'label', 'description', 'installed_device',
|
'id', 'url', 'display_url', 'display', 'device', 'name', 'label', 'description', 'installed_device',
|
||||||
'owner', 'tags', 'custom_fields', 'created', 'last_updated',
|
'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description')
|
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description')
|
||||||
|
|
||||||
|
|
||||||
class InventoryItemSerializer(OwnerMixin, NetBoxModelSerializer):
|
class InventoryItemSerializer(NetBoxModelSerializer):
|
||||||
device = DeviceSerializer(nested=True)
|
device = DeviceSerializer(nested=True)
|
||||||
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
|
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
|
||||||
role = InventoryItemRoleSerializer(nested=True, required=False, allow_null=True)
|
role = InventoryItemRoleSerializer(nested=True, required=False, allow_null=True)
|
||||||
@@ -454,6 +402,6 @@ class InventoryItemSerializer(OwnerMixin, NetBoxModelSerializer):
|
|||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'device', 'parent', 'name', 'label', 'status', 'role',
|
'id', 'url', 'display_url', 'display', 'device', 'parent', 'name', 'label', 'status', 'role',
|
||||||
'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description', 'component_type',
|
'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description', 'component_type',
|
||||||
'component_id', 'component', 'owner', 'tags', 'custom_fields', 'created', 'last_updated', '_depth',
|
'component_id', 'component', 'tags', 'custom_fields', 'created', 'last_updated', '_depth',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', '_depth')
|
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', '_depth')
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
|
|||||||
'id', 'url', 'display_url', 'display', 'name', 'device_type', 'role', 'tenant', 'platform', 'serial',
|
'id', 'url', 'display_url', 'display', 'name', 'device_type', 'role', 'tenant', 'platform', 'serial',
|
||||||
'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device',
|
'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device',
|
||||||
'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis',
|
'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis',
|
||||||
'vc_position', 'vc_priority', 'description', 'owner', 'comments', 'config_template', 'config_context',
|
'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'config_context',
|
||||||
'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', 'console_port_count',
|
'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', 'console_port_count',
|
||||||
'console_server_port_count', 'power_port_count', 'power_outlet_count', 'interface_count',
|
'console_server_port_count', 'power_port_count', 'power_outlet_count', 'interface_count',
|
||||||
'front_port_count', 'rear_port_count', 'device_bay_count', 'module_bay_count', 'inventory_item_count',
|
'front_port_count', 'rear_port_count', 'device_bay_count', 'module_bay_count', 'inventory_item_count',
|
||||||
|
|||||||
@@ -5,14 +5,12 @@ from dcim.choices import *
|
|||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.models import (
|
from dcim.models import (
|
||||||
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate, InterfaceTemplate,
|
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate, InterfaceTemplate,
|
||||||
InventoryItemTemplate, ModuleBayTemplate, PortTemplateMapping, PowerOutletTemplate, PowerPortTemplate,
|
InventoryItemTemplate, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
|
||||||
RearPortTemplate,
|
|
||||||
)
|
)
|
||||||
from netbox.api.fields import ChoiceField, ContentTypeField
|
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||||
from netbox.api.gfk_fields import GFKSerializerField
|
from netbox.api.gfk_fields import GFKSerializerField
|
||||||
from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer
|
from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer
|
||||||
from wireless.choices import *
|
from wireless.choices import *
|
||||||
from .base import PortSerializer
|
|
||||||
from .devicetypes import DeviceTypeSerializer, ModuleTypeSerializer
|
from .devicetypes import DeviceTypeSerializer, ModuleTypeSerializer
|
||||||
from .manufacturers import ManufacturerSerializer
|
from .manufacturers import ManufacturerSerializer
|
||||||
from .nested import NestedInterfaceTemplateSerializer
|
from .nested import NestedInterfaceTemplateSerializer
|
||||||
@@ -207,20 +205,7 @@ class InterfaceTemplateSerializer(ComponentTemplateSerializer):
|
|||||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||||
|
|
||||||
|
|
||||||
class RearPortTemplateMappingSerializer(serializers.ModelSerializer):
|
class RearPortTemplateSerializer(ComponentTemplateSerializer):
|
||||||
position = serializers.IntegerField(
|
|
||||||
source='rear_port_position'
|
|
||||||
)
|
|
||||||
front_port = serializers.PrimaryKeyRelatedField(
|
|
||||||
queryset=FrontPortTemplate.objects.all(),
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = PortTemplateMapping
|
|
||||||
fields = ('position', 'front_port', 'front_port_position')
|
|
||||||
|
|
||||||
|
|
||||||
class RearPortTemplateSerializer(ComponentTemplateSerializer, PortSerializer):
|
|
||||||
device_type = DeviceTypeSerializer(
|
device_type = DeviceTypeSerializer(
|
||||||
required=False,
|
required=False,
|
||||||
nested=True,
|
nested=True,
|
||||||
@@ -234,35 +219,17 @@ class RearPortTemplateSerializer(ComponentTemplateSerializer, PortSerializer):
|
|||||||
default=None
|
default=None
|
||||||
)
|
)
|
||||||
type = ChoiceField(choices=PortTypeChoices)
|
type = ChoiceField(choices=PortTypeChoices)
|
||||||
front_ports = RearPortTemplateMappingSerializer(
|
|
||||||
source='mappings',
|
|
||||||
many=True,
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RearPortTemplate
|
model = RearPortTemplate
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions',
|
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color',
|
||||||
'front_ports', 'description', 'created', 'last_updated',
|
'positions', 'description', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||||
|
|
||||||
|
|
||||||
class FrontPortTemplateMappingSerializer(serializers.ModelSerializer):
|
class FrontPortTemplateSerializer(ComponentTemplateSerializer):
|
||||||
position = serializers.IntegerField(
|
|
||||||
source='front_port_position'
|
|
||||||
)
|
|
||||||
rear_port = serializers.PrimaryKeyRelatedField(
|
|
||||||
queryset=RearPortTemplate.objects.all(),
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = PortTemplateMapping
|
|
||||||
fields = ('position', 'rear_port', 'rear_port_position')
|
|
||||||
|
|
||||||
|
|
||||||
class FrontPortTemplateSerializer(ComponentTemplateSerializer, PortSerializer):
|
|
||||||
device_type = DeviceTypeSerializer(
|
device_type = DeviceTypeSerializer(
|
||||||
nested=True,
|
nested=True,
|
||||||
required=False,
|
required=False,
|
||||||
@@ -276,17 +243,13 @@ class FrontPortTemplateSerializer(ComponentTemplateSerializer, PortSerializer):
|
|||||||
default=None
|
default=None
|
||||||
)
|
)
|
||||||
type = ChoiceField(choices=PortTypeChoices)
|
type = ChoiceField(choices=PortTypeChoices)
|
||||||
rear_ports = FrontPortTemplateMappingSerializer(
|
rear_port = RearPortTemplateSerializer(nested=True)
|
||||||
source='mappings',
|
|
||||||
many=True,
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = FrontPortTemplate
|
model = FrontPortTemplate
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions',
|
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color',
|
||||||
'rear_ports', 'description', 'created', 'last_updated',
|
'rear_port', 'rear_port_position', 'description', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||||
|
|
||||||
|
|||||||
@@ -11,15 +11,13 @@ class ManufacturerSerializer(OrganizationalModelSerializer):
|
|||||||
|
|
||||||
# Related object counts
|
# Related object counts
|
||||||
devicetype_count = RelatedObjectCountField('device_types')
|
devicetype_count = RelatedObjectCountField('device_types')
|
||||||
moduletype_count = RelatedObjectCountField('module_types')
|
|
||||||
inventoryitem_count = RelatedObjectCountField('inventory_items')
|
inventoryitem_count = RelatedObjectCountField('inventory_items')
|
||||||
platform_count = RelatedObjectCountField('platforms')
|
platform_count = RelatedObjectCountField('platforms')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Manufacturer
|
model = Manufacturer
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'owner', 'comments', 'tags',
|
'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'owner', 'tags', 'custom_fields',
|
||||||
'custom_fields', 'created', 'last_updated', 'devicetype_count', 'moduletype_count', 'inventoryitem_count',
|
'created', 'last_updated', 'devicetype_count', 'inventoryitem_count', 'platform_count',
|
||||||
'platform_count',
|
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description')
|
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'devicetype_count')
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class RackRoleSerializer(OrganizationalModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = RackRole
|
model = RackRole
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'owner', 'comments', 'tags',
|
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'owner', 'tags',
|
||||||
'custom_fields', 'created', 'last_updated', 'rack_count',
|
'custom_fields', 'created', 'last_updated', 'rack_count',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count')
|
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count')
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class InventoryItemRoleSerializer(OrganizationalModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = InventoryItemRole
|
model = InventoryItemRole
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'owner', 'comments', 'tags',
|
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'owner', 'tags',
|
||||||
'custom_fields', 'created', 'last_updated', 'inventoryitem_count',
|
'custom_fields', 'created', 'last_updated', 'inventoryitem_count',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'inventoryitem_count')
|
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'inventoryitem_count')
|
||||||
|
|||||||
+68
-350
@@ -1,390 +1,108 @@
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from dcim.choices import CableEndChoices
|
|
||||||
from dcim.models import CableTermination
|
from dcim.models import CableTermination
|
||||||
|
|
||||||
|
|
||||||
class BaseCableProfile:
|
class BaseCableProfile:
|
||||||
"""Base class for representing a cable profile."""
|
# Maximum number of terminations allowed per side
|
||||||
|
a_max_connections = None
|
||||||
# Mappings of connectors to the number of positions presented by each, at either end of the cable. For example, a
|
b_max_connections = None
|
||||||
# 12-strand MPO fiber cable would have one connector at either end with six positions (six bidirectional fiber
|
|
||||||
# pairs).
|
|
||||||
a_connectors = {}
|
|
||||||
b_connectors = {}
|
|
||||||
|
|
||||||
# Defined a mapping of A/B connector & position pairings. If not defined, all positions are presumed to be
|
|
||||||
# symmetrical (i.e. 1:1 on side A maps to 1:1 on side B). If defined, it must be constructed as a dictionary of
|
|
||||||
# two-item tuples, e.g. {(1, 1): (1, 1)}.
|
|
||||||
_mapping = None
|
|
||||||
|
|
||||||
def clean(self, cable):
|
def clean(self, cable):
|
||||||
# Enforce maximum terminations limits
|
# Enforce maximum connection limits
|
||||||
a_terminations_count = len(cable.a_terminations)
|
if self.a_max_connections and len(cable.a_terminations) > self.a_max_connections:
|
||||||
b_terminations_count = len(cable.b_terminations)
|
|
||||||
max_a_terminations = len(self.a_connectors)
|
|
||||||
max_b_terminations = len(self.b_connectors)
|
|
||||||
if a_terminations_count > max_a_terminations:
|
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'a_terminations': _(
|
'a_terminations': _(
|
||||||
'A side of cable has {count} terminations but only {max} are permitted for profile {profile}'
|
'Maximum A side connections for profile {profile}: {max}'
|
||||||
).format(
|
).format(
|
||||||
count=a_terminations_count,
|
|
||||||
profile=cable.get_profile_display(),
|
profile=cable.get_profile_display(),
|
||||||
max=max_a_terminations,
|
max=self.a_max_connections,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
if b_terminations_count > max_b_terminations:
|
if self.b_max_connections and len(cable.b_terminations) > self.b_max_connections:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'b_terminations': _(
|
'b_terminations': _(
|
||||||
'B side of cable has {count} terminations but only {max} are permitted for profile {profile}'
|
'Maximum B side connections for profile {profile}: {max}'
|
||||||
).format(
|
).format(
|
||||||
count=b_terminations_count,
|
|
||||||
profile=cable.get_profile_display(),
|
profile=cable.get_profile_display(),
|
||||||
max=max_b_terminations,
|
max=self.b_max_connections,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
def get_mapped_position(self, side, connector, position):
|
def get_mapped_position(self, side, position):
|
||||||
"""
|
"""
|
||||||
Return the mapped far-end connector & position for a given cable end the local connector & position.
|
Return the mapped position for a given cable end and position.
|
||||||
|
|
||||||
|
By default, assume all positions are symmetrical.
|
||||||
"""
|
"""
|
||||||
# By default, assume all positions are symmetrical.
|
return position
|
||||||
if self._mapping:
|
|
||||||
return self._mapping.get((connector, position))
|
|
||||||
return connector, position
|
|
||||||
|
|
||||||
def get_peer_termination(self, termination, position):
|
def get_peer_terminations(self, terminations, position_stack):
|
||||||
"""
|
local_end = terminations[0].cable_end
|
||||||
Given a terminating object, return the peer terminating object (if any) on the opposite end of the cable.
|
qs = CableTermination.objects.filter(
|
||||||
"""
|
cable=terminations[0].cable,
|
||||||
try:
|
cable_end=terminations[0].opposite_cable_end
|
||||||
connector, position = self.get_mapped_position(
|
)
|
||||||
termination.cable_end,
|
|
||||||
termination.cable_connector,
|
|
||||||
position
|
|
||||||
)
|
|
||||||
except TypeError:
|
|
||||||
raise ValueError(
|
|
||||||
f"Could not map connector {termination.cable_connector} position {position} on side "
|
|
||||||
f"{termination.cable_end}"
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
ct = CableTermination.objects.get(
|
|
||||||
cable=termination.cable,
|
|
||||||
cable_end=termination.opposite_cable_end,
|
|
||||||
connector=connector,
|
|
||||||
positions__contains=[position],
|
|
||||||
)
|
|
||||||
return ct.termination, position
|
|
||||||
except CableTermination.DoesNotExist:
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
@staticmethod
|
# TODO: Optimize this to use a single query under any condition
|
||||||
def get_position_list(n):
|
if position_stack:
|
||||||
"""Return a list of integers from 1 to n, inclusive."""
|
# Attempt to find a peer termination at the same position currently in the stack. Pop the stack only if
|
||||||
return list(range(1, n + 1))
|
# we find one. Otherwise, return any peer terminations with a null position.
|
||||||
|
position = self.get_mapped_position(local_end, position_stack[-1][0])
|
||||||
|
if peers := qs.filter(position=position):
|
||||||
|
position_stack.pop()
|
||||||
|
return peers
|
||||||
|
|
||||||
|
return qs.filter(position=None)
|
||||||
|
|
||||||
|
|
||||||
# Profile naming:
|
class StraightSingleCableProfile(BaseCableProfile):
|
||||||
# - Single: One connector per side, with one or more positions
|
a_max_connections = 1
|
||||||
# - Trunk: Two or more connectors per side, with one or more positions per connector
|
b_max_connections = 1
|
||||||
# - Breakout: One or more connectors on the A side which map to a greater number of B side connectors
|
|
||||||
# - Shuffle: A cable with nonlinear position mappings between sides
|
|
||||||
|
|
||||||
class Single1C1PCableProfile(BaseCableProfile):
|
|
||||||
a_connectors = {
|
class StraightMultiCableProfile(BaseCableProfile):
|
||||||
|
a_max_connections = None
|
||||||
|
b_max_connections = None
|
||||||
|
|
||||||
|
|
||||||
|
class Shuffle2x2MPO8CableProfile(BaseCableProfile):
|
||||||
|
a_max_connections = 8
|
||||||
|
b_max_connections = 8
|
||||||
|
_mapping = {
|
||||||
1: 1,
|
1: 1,
|
||||||
}
|
|
||||||
b_connectors = a_connectors
|
|
||||||
|
|
||||||
|
|
||||||
class Single1C2PCableProfile(BaseCableProfile):
|
|
||||||
a_connectors = {
|
|
||||||
1: 2,
|
|
||||||
}
|
|
||||||
b_connectors = a_connectors
|
|
||||||
|
|
||||||
|
|
||||||
class Single1C4PCableProfile(BaseCableProfile):
|
|
||||||
a_connectors = {
|
|
||||||
1: 4,
|
|
||||||
}
|
|
||||||
b_connectors = a_connectors
|
|
||||||
|
|
||||||
|
|
||||||
class Single1C6PCableProfile(BaseCableProfile):
|
|
||||||
a_connectors = {
|
|
||||||
1: 6,
|
|
||||||
}
|
|
||||||
b_connectors = a_connectors
|
|
||||||
|
|
||||||
|
|
||||||
class Single1C8PCableProfile(BaseCableProfile):
|
|
||||||
a_connectors = {
|
|
||||||
1: 8,
|
|
||||||
}
|
|
||||||
b_connectors = a_connectors
|
|
||||||
|
|
||||||
|
|
||||||
class Single1C12PCableProfile(BaseCableProfile):
|
|
||||||
a_connectors = {
|
|
||||||
1: 12,
|
|
||||||
}
|
|
||||||
b_connectors = a_connectors
|
|
||||||
|
|
||||||
|
|
||||||
class Single1C16PCableProfile(BaseCableProfile):
|
|
||||||
a_connectors = {
|
|
||||||
1: 16,
|
|
||||||
}
|
|
||||||
b_connectors = a_connectors
|
|
||||||
|
|
||||||
|
|
||||||
class Trunk2C1PCableProfile(BaseCableProfile):
|
|
||||||
a_connectors = {
|
|
||||||
1: 1,
|
|
||||||
2: 1,
|
|
||||||
}
|
|
||||||
b_connectors = a_connectors
|
|
||||||
|
|
||||||
|
|
||||||
class Trunk2C2PCableProfile(BaseCableProfile):
|
|
||||||
a_connectors = {
|
|
||||||
1: 2,
|
|
||||||
2: 2,
|
2: 2,
|
||||||
}
|
3: 5,
|
||||||
b_connectors = a_connectors
|
|
||||||
|
|
||||||
|
|
||||||
class Trunk2C4PCableProfile(BaseCableProfile):
|
|
||||||
a_connectors = {
|
|
||||||
1: 4,
|
|
||||||
2: 4,
|
|
||||||
}
|
|
||||||
b_connectors = a_connectors
|
|
||||||
|
|
||||||
|
|
||||||
class Trunk2C6PCableProfile(BaseCableProfile):
|
|
||||||
a_connectors = {
|
|
||||||
1: 6,
|
|
||||||
2: 6,
|
|
||||||
}
|
|
||||||
b_connectors = a_connectors
|
|
||||||
|
|
||||||
|
|
||||||
class Trunk2C8PCableProfile(BaseCableProfile):
|
|
||||||
a_connectors = {
|
|
||||||
1: 8,
|
|
||||||
2: 8,
|
|
||||||
}
|
|
||||||
b_connectors = a_connectors
|
|
||||||
|
|
||||||
|
|
||||||
class Trunk2C12PCableProfile(BaseCableProfile):
|
|
||||||
a_connectors = {
|
|
||||||
1: 12,
|
|
||||||
2: 12,
|
|
||||||
}
|
|
||||||
b_connectors = a_connectors
|
|
||||||
|
|
||||||
|
|
||||||
class Trunk4C1PCableProfile(BaseCableProfile):
|
|
||||||
a_connectors = {
|
|
||||||
1: 1,
|
|
||||||
2: 1,
|
|
||||||
3: 1,
|
|
||||||
4: 1,
|
|
||||||
}
|
|
||||||
b_connectors = a_connectors
|
|
||||||
|
|
||||||
|
|
||||||
class Trunk4C2PCableProfile(BaseCableProfile):
|
|
||||||
a_connectors = {
|
|
||||||
1: 2,
|
|
||||||
2: 2,
|
|
||||||
3: 2,
|
|
||||||
4: 2,
|
|
||||||
}
|
|
||||||
b_connectors = a_connectors
|
|
||||||
|
|
||||||
|
|
||||||
class Trunk4C4PCableProfile(BaseCableProfile):
|
|
||||||
a_connectors = {
|
|
||||||
1: 4,
|
|
||||||
2: 4,
|
|
||||||
3: 4,
|
|
||||||
4: 4,
|
|
||||||
}
|
|
||||||
b_connectors = a_connectors
|
|
||||||
|
|
||||||
|
|
||||||
class Trunk4C6PCableProfile(BaseCableProfile):
|
|
||||||
a_connectors = {
|
|
||||||
1: 6,
|
|
||||||
2: 6,
|
|
||||||
3: 6,
|
|
||||||
4: 6,
|
4: 6,
|
||||||
}
|
5: 3,
|
||||||
b_connectors = a_connectors
|
|
||||||
|
|
||||||
|
|
||||||
class Trunk4C8PCableProfile(BaseCableProfile):
|
|
||||||
a_connectors = {
|
|
||||||
1: 8,
|
|
||||||
2: 8,
|
|
||||||
3: 8,
|
|
||||||
4: 8,
|
|
||||||
}
|
|
||||||
b_connectors = a_connectors
|
|
||||||
|
|
||||||
|
|
||||||
class Trunk8C4PCableProfile(BaseCableProfile):
|
|
||||||
a_connectors = {
|
|
||||||
1: 4,
|
|
||||||
2: 4,
|
|
||||||
3: 4,
|
|
||||||
4: 4,
|
|
||||||
5: 4,
|
|
||||||
6: 4,
|
6: 4,
|
||||||
7: 4,
|
7: 7,
|
||||||
8: 4,
|
8: 8,
|
||||||
}
|
|
||||||
b_connectors = a_connectors
|
|
||||||
|
|
||||||
|
|
||||||
class Breakout1C4Px4C1PCableProfile(BaseCableProfile):
|
|
||||||
a_connectors = {
|
|
||||||
1: 4,
|
|
||||||
}
|
|
||||||
b_connectors = {
|
|
||||||
1: 1,
|
|
||||||
2: 1,
|
|
||||||
3: 1,
|
|
||||||
4: 1,
|
|
||||||
}
|
|
||||||
_mapping = {
|
|
||||||
(1, 1): (1, 1),
|
|
||||||
(1, 2): (2, 1),
|
|
||||||
(1, 3): (3, 1),
|
|
||||||
(1, 4): (4, 1),
|
|
||||||
(2, 1): (1, 2),
|
|
||||||
(3, 1): (1, 3),
|
|
||||||
(4, 1): (1, 4),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def get_mapped_position(self, side, position):
|
||||||
class Breakout1C6Px6C1PCableProfile(BaseCableProfile):
|
return self._mapping.get(position)
|
||||||
a_connectors = {
|
|
||||||
1: 6,
|
|
||||||
}
|
|
||||||
b_connectors = {
|
|
||||||
1: 1,
|
|
||||||
2: 1,
|
|
||||||
3: 1,
|
|
||||||
4: 1,
|
|
||||||
5: 1,
|
|
||||||
6: 1,
|
|
||||||
}
|
|
||||||
_mapping = {
|
|
||||||
(1, 1): (1, 1),
|
|
||||||
(1, 2): (2, 1),
|
|
||||||
(1, 3): (3, 1),
|
|
||||||
(1, 4): (4, 1),
|
|
||||||
(1, 5): (5, 1),
|
|
||||||
(1, 6): (6, 1),
|
|
||||||
(2, 1): (1, 2),
|
|
||||||
(3, 1): (1, 3),
|
|
||||||
(4, 1): (1, 4),
|
|
||||||
(5, 1): (1, 5),
|
|
||||||
(6, 1): (1, 6),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Trunk2C4PShuffleCableProfile(BaseCableProfile):
|
class Shuffle4x4MPO8CableProfile(BaseCableProfile):
|
||||||
a_connectors = {
|
a_max_connections = 8
|
||||||
1: 4,
|
b_max_connections = 8
|
||||||
2: 4,
|
# A side to B side position mapping
|
||||||
}
|
|
||||||
b_connectors = a_connectors
|
|
||||||
_mapping = {
|
|
||||||
(1, 1): (1, 1),
|
|
||||||
(1, 2): (1, 2),
|
|
||||||
(1, 3): (2, 1),
|
|
||||||
(1, 4): (2, 2),
|
|
||||||
(2, 1): (1, 3),
|
|
||||||
(2, 2): (1, 4),
|
|
||||||
(2, 3): (2, 3),
|
|
||||||
(2, 4): (2, 4),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Trunk4C4PShuffleCableProfile(BaseCableProfile):
|
|
||||||
a_connectors = {
|
|
||||||
1: 4,
|
|
||||||
2: 4,
|
|
||||||
3: 4,
|
|
||||||
4: 4,
|
|
||||||
}
|
|
||||||
b_connectors = a_connectors
|
|
||||||
_mapping = {
|
|
||||||
(1, 1): (1, 1),
|
|
||||||
(1, 2): (2, 1),
|
|
||||||
(1, 3): (3, 1),
|
|
||||||
(1, 4): (4, 1),
|
|
||||||
(2, 1): (1, 2),
|
|
||||||
(2, 2): (2, 2),
|
|
||||||
(2, 3): (3, 2),
|
|
||||||
(2, 4): (4, 2),
|
|
||||||
(3, 1): (1, 3),
|
|
||||||
(3, 2): (2, 3),
|
|
||||||
(3, 3): (3, 3),
|
|
||||||
(3, 4): (4, 3),
|
|
||||||
(4, 1): (1, 4),
|
|
||||||
(4, 2): (2, 4),
|
|
||||||
(4, 3): (3, 4),
|
|
||||||
(4, 4): (4, 4),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Breakout2C4Px8C1PShuffleCableProfile(BaseCableProfile):
|
|
||||||
a_connectors = {
|
|
||||||
1: 4,
|
|
||||||
2: 4,
|
|
||||||
}
|
|
||||||
b_connectors = {
|
|
||||||
1: 1,
|
|
||||||
2: 1,
|
|
||||||
3: 1,
|
|
||||||
4: 1,
|
|
||||||
5: 1,
|
|
||||||
6: 1,
|
|
||||||
7: 1,
|
|
||||||
8: 1,
|
|
||||||
}
|
|
||||||
_a_mapping = {
|
_a_mapping = {
|
||||||
(1, 1): (1, 1),
|
1: 1,
|
||||||
(1, 2): (2, 1),
|
2: 3,
|
||||||
(1, 3): (5, 1),
|
3: 5,
|
||||||
(1, 4): (6, 1),
|
4: 7,
|
||||||
(2, 1): (3, 1),
|
5: 2,
|
||||||
(2, 2): (4, 1),
|
6: 4,
|
||||||
(2, 3): (7, 1),
|
7: 6,
|
||||||
(2, 4): (8, 1),
|
8: 8,
|
||||||
}
|
|
||||||
_b_mapping = {
|
|
||||||
(1, 1): (1, 1),
|
|
||||||
(2, 1): (1, 2),
|
|
||||||
(3, 1): (2, 1),
|
|
||||||
(4, 1): (2, 2),
|
|
||||||
(5, 1): (1, 3),
|
|
||||||
(6, 1): (1, 4),
|
|
||||||
(7, 1): (2, 3),
|
|
||||||
(8, 1): (2, 4),
|
|
||||||
}
|
}
|
||||||
|
# B side to A side position mapping (reverse of _a_mapping)
|
||||||
|
_b_mapping = {v: k for k, v in _a_mapping.items()}
|
||||||
|
|
||||||
def get_mapped_position(self, side, connector, position):
|
def get_mapped_position(self, side, position):
|
||||||
if side.upper() == CableEndChoices.SIDE_A:
|
if side.lower() == 'b':
|
||||||
return self._a_mapping.get((connector, position))
|
return self._b_mapping.get(position)
|
||||||
return self._b_mapping.get((connector, position))
|
return self._a_mapping.get(position)
|
||||||
|
|||||||
+8
-66
@@ -1722,74 +1722,16 @@ class PortTypeChoices(ChoiceSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class CableProfileChoices(ChoiceSet):
|
class CableProfileChoices(ChoiceSet):
|
||||||
# Singles
|
STRAIGHT_SINGLE = 'straight-single'
|
||||||
SINGLE_1C1P = 'single-1c1p'
|
STRAIGHT_MULTI = 'straight-multi'
|
||||||
SINGLE_1C2P = 'single-1c2p'
|
SHUFFLE_2X2_MPO8 = 'shuffle-2x2-mpo8'
|
||||||
SINGLE_1C4P = 'single-1c4p'
|
SHUFFLE_4X4_MPO8 = 'shuffle-4x4-mpo8'
|
||||||
SINGLE_1C6P = 'single-1c6p'
|
|
||||||
SINGLE_1C8P = 'single-1c8p'
|
|
||||||
SINGLE_1C12P = 'single-1c12p'
|
|
||||||
SINGLE_1C16P = 'single-1c16p'
|
|
||||||
# Trunks
|
|
||||||
TRUNK_2C1P = 'trunk-2c1p'
|
|
||||||
TRUNK_2C2P = 'trunk-2c2p'
|
|
||||||
TRUNK_2C4P = 'trunk-2c4p'
|
|
||||||
TRUNK_2C4P_SHUFFLE = 'trunk-2c4p-shuffle'
|
|
||||||
TRUNK_2C6P = 'trunk-2c6p'
|
|
||||||
TRUNK_2C8P = 'trunk-2c8p'
|
|
||||||
TRUNK_2C12P = 'trunk-2c12p'
|
|
||||||
TRUNK_4C1P = 'trunk-4c1p'
|
|
||||||
TRUNK_4C2P = 'trunk-4c2p'
|
|
||||||
TRUNK_4C4P = 'trunk-4c4p'
|
|
||||||
TRUNK_4C4P_SHUFFLE = 'trunk-4c4p-shuffle'
|
|
||||||
TRUNK_4C6P = 'trunk-4c6p'
|
|
||||||
TRUNK_4C8P = 'trunk-4c8p'
|
|
||||||
TRUNK_8C4P = 'trunk-8c4p'
|
|
||||||
# Breakouts
|
|
||||||
BREAKOUT_1C4P_4C1P = 'breakout-1c4p-4c1p'
|
|
||||||
BREAKOUT_1C6P_6C1P = 'breakout-1c6p-6c1p'
|
|
||||||
BREAKOUT_2C4P_8C1P_SHUFFLE = 'breakout-2c4p-8c1p-shuffle'
|
|
||||||
|
|
||||||
CHOICES = (
|
CHOICES = (
|
||||||
(
|
(STRAIGHT_SINGLE, _('Straight (single position)')),
|
||||||
_('Single'),
|
(STRAIGHT_MULTI, _('Straight (multi-position)')),
|
||||||
(
|
(SHUFFLE_2X2_MPO8, _('Shuffle (2x2 MPO8)')),
|
||||||
(SINGLE_1C1P, _('1C1P')),
|
(SHUFFLE_4X4_MPO8, _('Shuffle (4x4 MPO8)')),
|
||||||
(SINGLE_1C2P, _('1C2P')),
|
|
||||||
(SINGLE_1C4P, _('1C4P')),
|
|
||||||
(SINGLE_1C6P, _('1C6P')),
|
|
||||||
(SINGLE_1C8P, _('1C8P')),
|
|
||||||
(SINGLE_1C12P, _('1C12P')),
|
|
||||||
(SINGLE_1C16P, _('1C16P')),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
_('Trunk'),
|
|
||||||
(
|
|
||||||
(TRUNK_2C1P, _('2C1P trunk')),
|
|
||||||
(TRUNK_2C2P, _('2C2P trunk')),
|
|
||||||
(TRUNK_2C4P, _('2C4P trunk')),
|
|
||||||
(TRUNK_2C4P_SHUFFLE, _('2C4P trunk (shuffle)')),
|
|
||||||
(TRUNK_2C6P, _('2C6P trunk')),
|
|
||||||
(TRUNK_2C8P, _('2C8P trunk')),
|
|
||||||
(TRUNK_2C12P, _('2C12P trunk')),
|
|
||||||
(TRUNK_4C1P, _('4C1P trunk')),
|
|
||||||
(TRUNK_4C2P, _('4C2P trunk')),
|
|
||||||
(TRUNK_4C4P, _('4C4P trunk')),
|
|
||||||
(TRUNK_4C4P_SHUFFLE, _('4C4P trunk (shuffle)')),
|
|
||||||
(TRUNK_4C6P, _('4C6P trunk')),
|
|
||||||
(TRUNK_4C8P, _('4C8P trunk')),
|
|
||||||
(TRUNK_8C4P, _('8C4P trunk')),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
_('Breakout'),
|
|
||||||
(
|
|
||||||
(BREAKOUT_1C4P_4C1P, _('1C4P:4C1P breakout')),
|
|
||||||
(BREAKOUT_1C6P_6C1P, _('1C6P:6C1P breakout')),
|
|
||||||
(BREAKOUT_2C4P_8C1P_SHUFFLE, _('2C4P:8C1P breakout (shuffle)')),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -24,9 +24,6 @@ RACK_STARTING_UNIT_DEFAULT = 1
|
|||||||
# Cables
|
# Cables
|
||||||
#
|
#
|
||||||
|
|
||||||
CABLE_CONNECTOR_MIN = 1
|
|
||||||
CABLE_CONNECTOR_MAX = 256
|
|
||||||
|
|
||||||
CABLE_POSITION_MIN = 1
|
CABLE_POSITION_MIN = 1
|
||||||
CABLE_POSITION_MAX = 1024
|
CABLE_POSITION_MAX = 1024
|
||||||
|
|
||||||
@@ -35,8 +32,8 @@ CABLE_POSITION_MAX = 1024
|
|||||||
# RearPorts
|
# RearPorts
|
||||||
#
|
#
|
||||||
|
|
||||||
PORT_POSITION_MIN = 1
|
REARPORT_POSITIONS_MIN = 1
|
||||||
PORT_POSITION_MAX = 1024
|
REARPORT_POSITIONS_MAX = 1024
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|||||||
+13
-46
@@ -904,15 +904,12 @@ class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo
|
|||||||
null_value=None
|
null_value=None
|
||||||
)
|
)
|
||||||
rear_port_id = django_filters.ModelMultipleChoiceFilter(
|
rear_port_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='mappings__rear_port',
|
queryset=RearPort.objects.all()
|
||||||
queryset=RearPortTemplate.objects.all(),
|
|
||||||
to_field_name='rear_port',
|
|
||||||
label=_('Rear port (ID)'),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = FrontPortTemplate
|
model = FrontPortTemplate
|
||||||
fields = ('id', 'name', 'label', 'type', 'color', 'positions', 'description')
|
fields = ('id', 'name', 'label', 'type', 'color', 'rear_port_position', 'description')
|
||||||
|
|
||||||
|
|
||||||
@register_filterset
|
@register_filterset
|
||||||
@@ -921,12 +918,6 @@ class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCom
|
|||||||
choices=PortTypeChoices,
|
choices=PortTypeChoices,
|
||||||
null_value=None
|
null_value=None
|
||||||
)
|
)
|
||||||
front_port_id = django_filters.ModelMultipleChoiceFilter(
|
|
||||||
field_name='mappings__front_port',
|
|
||||||
queryset=FrontPort.objects.all(),
|
|
||||||
to_field_name='front_port',
|
|
||||||
label=_('Front port (ID)'),
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RearPortTemplate
|
model = RearPortTemplate
|
||||||
@@ -1673,17 +1664,6 @@ class DeviceComponentFilterSet(OwnerFilterMixin, NetBoxModelFilterSet):
|
|||||||
choices=DeviceStatusChoices,
|
choices=DeviceStatusChoices,
|
||||||
field_name='device__status',
|
field_name='device__status',
|
||||||
)
|
)
|
||||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
|
||||||
field_name='device__tenant',
|
|
||||||
queryset=Tenant.objects.all(),
|
|
||||||
label=_('Tenant (ID)'),
|
|
||||||
)
|
|
||||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
|
||||||
field_name='device__tenant__slug',
|
|
||||||
queryset=Tenant.objects.all(),
|
|
||||||
to_field_name='slug',
|
|
||||||
label=_('Tenant (slug)'),
|
|
||||||
)
|
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@@ -1748,9 +1728,7 @@ class ConsolePortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSe
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsolePort
|
model = ConsolePort
|
||||||
fields = (
|
fields = ('id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end', 'cable_position')
|
||||||
'id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end', 'cable_connector',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@register_filterset
|
@register_filterset
|
||||||
@@ -1762,9 +1740,7 @@ class ConsoleServerPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFi
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsoleServerPort
|
model = ConsoleServerPort
|
||||||
fields = (
|
fields = ('id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end', 'cable_position')
|
||||||
'id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end', 'cable_connector',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@register_filterset
|
@register_filterset
|
||||||
@@ -1778,7 +1754,7 @@ class PowerPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet,
|
|||||||
model = PowerPort
|
model = PowerPort
|
||||||
fields = (
|
fields = (
|
||||||
'id', 'name', 'label', 'maximum_draw', 'allocated_draw', 'description', 'mark_connected', 'cable_end',
|
'id', 'name', 'label', 'maximum_draw', 'allocated_draw', 'description', 'mark_connected', 'cable_end',
|
||||||
'cable_connector',
|
'cable_position',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1805,7 +1781,7 @@ class PowerOutletFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSe
|
|||||||
model = PowerOutlet
|
model = PowerOutlet
|
||||||
fields = (
|
fields = (
|
||||||
'id', 'name', 'status', 'label', 'feed_leg', 'description', 'color', 'mark_connected', 'cable_end',
|
'id', 'name', 'status', 'label', 'feed_leg', 'description', 'color', 'mark_connected', 'cable_end',
|
||||||
'cable_connector',
|
'cable_position',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -2115,7 +2091,7 @@ class InterfaceFilterSet(
|
|||||||
fields = (
|
fields = (
|
||||||
'id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'poe_mode', 'poe_type', 'mode', 'rf_role',
|
'id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'poe_mode', 'poe_type', 'mode', 'rf_role',
|
||||||
'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected',
|
'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected',
|
||||||
'cable_id', 'cable_end', 'cable_connector',
|
'cable_id', 'cable_end', 'cable_position',
|
||||||
)
|
)
|
||||||
|
|
||||||
def filter_virtual_chassis_member_or_master(self, queryset, name, value):
|
def filter_virtual_chassis_member_or_master(self, queryset, name, value):
|
||||||
@@ -2161,17 +2137,14 @@ class FrontPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet)
|
|||||||
null_value=None
|
null_value=None
|
||||||
)
|
)
|
||||||
rear_port_id = django_filters.ModelMultipleChoiceFilter(
|
rear_port_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='mappings__rear_port',
|
queryset=RearPort.objects.all()
|
||||||
queryset=RearPort.objects.all(),
|
|
||||||
to_field_name='rear_port',
|
|
||||||
label=_('Rear port (ID)'),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = FrontPort
|
model = FrontPort
|
||||||
fields = (
|
fields = (
|
||||||
'id', 'name', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable_end',
|
'id', 'name', 'label', 'type', 'color', 'rear_port_position', 'description', 'mark_connected', 'cable_end',
|
||||||
'cable_connector',
|
'cable_position',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -2181,18 +2154,12 @@ class RearPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet):
|
|||||||
choices=PortTypeChoices,
|
choices=PortTypeChoices,
|
||||||
null_value=None
|
null_value=None
|
||||||
)
|
)
|
||||||
front_port_id = django_filters.ModelMultipleChoiceFilter(
|
|
||||||
field_name='mappings__front_port',
|
|
||||||
queryset=FrontPort.objects.all(),
|
|
||||||
to_field_name='front_port',
|
|
||||||
label=_('Front port (ID)'),
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RearPort
|
model = RearPort
|
||||||
fields = (
|
fields = (
|
||||||
'id', 'name', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable_end',
|
'id', 'name', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable_end',
|
||||||
'cable_connector',
|
'cable_position',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -2548,7 +2515,7 @@ class CableTerminationFilterSet(ChangeLoggedModelFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CableTermination
|
model = CableTermination
|
||||||
fields = ('id', 'cable', 'cable_end', 'termination_type', 'termination_id')
|
fields = ('id', 'cable', 'cable_end', 'position', 'termination_type', 'termination_id')
|
||||||
|
|
||||||
|
|
||||||
@register_filterset
|
@register_filterset
|
||||||
@@ -2667,7 +2634,7 @@ class PowerFeedFilterSet(PrimaryModelFilterSet, CabledObjectFilterSet, PathEndpo
|
|||||||
model = PowerFeed
|
model = PowerFeed
|
||||||
fields = (
|
fields = (
|
||||||
'id', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization',
|
'id', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization',
|
||||||
'available_power', 'mark_connected', 'cable_end', 'cable_connector', 'description',
|
'available_power', 'mark_connected', 'cable_end', 'cable_position', 'description',
|
||||||
)
|
)
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ class RackRoleBulkEditForm(OrganizationalModelBulkEditForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('color', 'description'),
|
FieldSet('color', 'description'),
|
||||||
)
|
)
|
||||||
nullable_fields = ('color', 'description', 'comments')
|
nullable_fields = ('color', 'description')
|
||||||
|
|
||||||
|
|
||||||
class RackTypeBulkEditForm(PrimaryModelBulkEditForm):
|
class RackTypeBulkEditForm(PrimaryModelBulkEditForm):
|
||||||
@@ -474,7 +474,7 @@ class ManufacturerBulkEditForm(OrganizationalModelBulkEditForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('description'),
|
FieldSet('description'),
|
||||||
)
|
)
|
||||||
nullable_fields = ('description', 'comments')
|
nullable_fields = ('description',)
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeBulkEditForm(PrimaryModelBulkEditForm):
|
class DeviceTypeBulkEditForm(PrimaryModelBulkEditForm):
|
||||||
@@ -1719,7 +1719,7 @@ class InventoryItemRoleBulkEditForm(OrganizationalModelBulkEditForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('color', 'description'),
|
FieldSet('color', 'description'),
|
||||||
)
|
)
|
||||||
nullable_fields = ('color', 'description', 'comments')
|
nullable_fields = ('color', 'description')
|
||||||
|
|
||||||
|
|
||||||
class VirtualDeviceContextBulkEditForm(PrimaryModelBulkEditForm):
|
class VirtualDeviceContextBulkEditForm(PrimaryModelBulkEditForm):
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ class RackRoleImportForm(OrganizationalModelImportForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RackRole
|
model = RackRole
|
||||||
fields = ('name', 'slug', 'color', 'description', 'owner', 'comments', 'tags')
|
fields = ('name', 'slug', 'color', 'description', 'owner', 'tags')
|
||||||
|
|
||||||
|
|
||||||
class RackTypeImportForm(PrimaryModelImportForm):
|
class RackTypeImportForm(PrimaryModelImportForm):
|
||||||
@@ -400,7 +400,7 @@ class ManufacturerImportForm(OrganizationalModelImportForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Manufacturer
|
model = Manufacturer
|
||||||
fields = ('name', 'slug', 'description', 'owner', 'comments', 'tags')
|
fields = ('name', 'slug', 'description', 'owner', 'tags')
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeImportForm(PrimaryModelImportForm):
|
class DeviceTypeImportForm(PrimaryModelImportForm):
|
||||||
@@ -476,30 +476,14 @@ class ModuleTypeImportForm(PrimaryModelImportForm):
|
|||||||
required=False,
|
required=False,
|
||||||
help_text=_('Unit for module weight')
|
help_text=_('Unit for module weight')
|
||||||
)
|
)
|
||||||
attribute_data = forms.JSONField(
|
|
||||||
label=_('Attributes'),
|
|
||||||
required=False,
|
|
||||||
help_text=_('Attribute values for the assigned profile, passed as a dictionary')
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ModuleType
|
model = ModuleType
|
||||||
fields = [
|
fields = [
|
||||||
'manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit', 'profile',
|
'manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit', 'profile',
|
||||||
'attribute_data', 'owner', 'comments', 'tags',
|
'owner', 'comments', 'tags'
|
||||||
]
|
]
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
super().clean()
|
|
||||||
|
|
||||||
# Attribute data may be included only if a profile is specified
|
|
||||||
if self.cleaned_data.get('attribute_data') and not self.cleaned_data.get('profile'):
|
|
||||||
raise forms.ValidationError(_("Profile must be specified if attribute data is provided."))
|
|
||||||
|
|
||||||
# Default attribute_data to an empty dictionary if a profile is specified (to enforce schema validation)
|
|
||||||
if self.cleaned_data.get('profile') and not self.cleaned_data.get('attribute_data'):
|
|
||||||
self.cleaned_data['attribute_data'] = {}
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceRoleImportForm(NestedGroupModelImportForm):
|
class DeviceRoleImportForm(NestedGroupModelImportForm):
|
||||||
parent = CSVModelChoiceField(
|
parent = CSVModelChoiceField(
|
||||||
@@ -1091,6 +1075,12 @@ class FrontPortImportForm(OwnerCSVMixin, NetBoxModelImportForm):
|
|||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
to_field_name='name'
|
to_field_name='name'
|
||||||
)
|
)
|
||||||
|
rear_port = CSVModelChoiceField(
|
||||||
|
label=_('Rear port'),
|
||||||
|
queryset=RearPort.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
help_text=_('Corresponding rear port')
|
||||||
|
)
|
||||||
type = CSVChoiceField(
|
type = CSVChoiceField(
|
||||||
label=_('Type'),
|
label=_('Type'),
|
||||||
choices=PortTypeChoices,
|
choices=PortTypeChoices,
|
||||||
@@ -1100,9 +1090,32 @@ class FrontPortImportForm(OwnerCSVMixin, NetBoxModelImportForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = FrontPort
|
model = FrontPort
|
||||||
fields = (
|
fields = (
|
||||||
'device', 'name', 'label', 'type', 'color', 'mark_connected', 'positions', 'description', 'owner', 'tags'
|
'device', 'name', 'label', 'type', 'color', 'mark_connected', 'rear_port', 'rear_port_position',
|
||||||
|
'description', 'owner', 'tags'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Limit RearPort choices to those belonging to this device (or VC master)
|
||||||
|
if self.is_bound and 'device' in self.data:
|
||||||
|
try:
|
||||||
|
device = self.fields['device'].to_python(self.data['device'])
|
||||||
|
except forms.ValidationError:
|
||||||
|
device = None
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
device = self.instance.device
|
||||||
|
except Device.DoesNotExist:
|
||||||
|
device = None
|
||||||
|
|
||||||
|
if device:
|
||||||
|
self.fields['rear_port'].queryset = RearPort.objects.filter(
|
||||||
|
device__in=[device, device.get_vc_master()]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.fields['rear_port'].queryset = RearPort.objects.none()
|
||||||
|
|
||||||
|
|
||||||
class RearPortImportForm(OwnerCSVMixin, NetBoxModelImportForm):
|
class RearPortImportForm(OwnerCSVMixin, NetBoxModelImportForm):
|
||||||
device = CSVModelChoiceField(
|
device = CSVModelChoiceField(
|
||||||
@@ -1298,7 +1311,7 @@ class InventoryItemRoleImportForm(OrganizationalModelImportForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InventoryItemRole
|
model = InventoryItemRole
|
||||||
fields = ('name', 'slug', 'color', 'description', 'owner', 'comments')
|
fields = ('name', 'slug', 'color', 'description')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|||||||
+59
-305
@@ -12,12 +12,10 @@ from netbox.forms import (
|
|||||||
NestedGroupModelFilterSetForm, NetBoxModelFilterSetForm, OrganizationalModelFilterSetForm,
|
NestedGroupModelFilterSetForm, NetBoxModelFilterSetForm, OrganizationalModelFilterSetForm,
|
||||||
PrimaryModelFilterSetForm,
|
PrimaryModelFilterSetForm,
|
||||||
)
|
)
|
||||||
from netbox.forms.mixins import OwnerFilterMixin
|
|
||||||
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
|
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
|
||||||
from tenancy.models import Tenant
|
from users.models import Owner, User
|
||||||
from users.models import User
|
|
||||||
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
|
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
|
||||||
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
|
from utilities.forms.fields import ColorField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField
|
||||||
from utilities.forms.rendering import FieldSet
|
from utilities.forms.rendering import FieldSet
|
||||||
from utilities.forms.widgets import NumberWithOptions
|
from utilities.forms.widgets import NumberWithOptions
|
||||||
from virtualization.models import Cluster, ClusterGroup, VirtualMachine
|
from virtualization.models import Cluster, ClusterGroup, VirtualMachine
|
||||||
@@ -28,54 +26,44 @@ __all__ = (
|
|||||||
'CableFilterForm',
|
'CableFilterForm',
|
||||||
'ConsoleConnectionFilterForm',
|
'ConsoleConnectionFilterForm',
|
||||||
'ConsolePortFilterForm',
|
'ConsolePortFilterForm',
|
||||||
'ConsolePortTemplateFilterForm',
|
|
||||||
'ConsoleServerPortFilterForm',
|
'ConsoleServerPortFilterForm',
|
||||||
'ConsoleServerPortTemplateFilterForm',
|
|
||||||
'DeviceBayFilterForm',
|
'DeviceBayFilterForm',
|
||||||
'DeviceBayTemplateFilterForm',
|
|
||||||
'DeviceFilterForm',
|
'DeviceFilterForm',
|
||||||
'DeviceRoleFilterForm',
|
'DeviceRoleFilterForm',
|
||||||
'DeviceTypeFilterForm',
|
'DeviceTypeFilterForm',
|
||||||
'FrontPortFilterForm',
|
'FrontPortFilterForm',
|
||||||
'FrontPortTemplateFilterForm',
|
|
||||||
'InterfaceConnectionFilterForm',
|
'InterfaceConnectionFilterForm',
|
||||||
'InterfaceFilterForm',
|
'InterfaceFilterForm',
|
||||||
'InterfaceTemplateFilterForm',
|
|
||||||
'InventoryItemFilterForm',
|
'InventoryItemFilterForm',
|
||||||
'InventoryItemTemplateFilterForm',
|
|
||||||
'InventoryItemRoleFilterForm',
|
'InventoryItemRoleFilterForm',
|
||||||
'LocationFilterForm',
|
'LocationFilterForm',
|
||||||
'MACAddressFilterForm',
|
'MACAddressFilterForm',
|
||||||
'ManufacturerFilterForm',
|
'ManufacturerFilterForm',
|
||||||
'ModuleFilterForm',
|
'ModuleFilterForm',
|
||||||
'ModuleBayFilterForm',
|
'ModuleBayFilterForm',
|
||||||
'ModuleBayTemplateFilterForm',
|
|
||||||
'ModuleTypeFilterForm',
|
'ModuleTypeFilterForm',
|
||||||
'ModuleTypeProfileFilterForm',
|
'ModuleTypeProfileFilterForm',
|
||||||
'PlatformFilterForm',
|
'PlatformFilterForm',
|
||||||
'PowerConnectionFilterForm',
|
'PowerConnectionFilterForm',
|
||||||
'PowerFeedFilterForm',
|
'PowerFeedFilterForm',
|
||||||
'PowerOutletFilterForm',
|
'PowerOutletFilterForm',
|
||||||
'PowerOutletTemplateFilterForm',
|
|
||||||
'PowerPanelFilterForm',
|
'PowerPanelFilterForm',
|
||||||
'PowerPortFilterForm',
|
'PowerPortFilterForm',
|
||||||
'PowerPortTemplateFilterForm',
|
|
||||||
'RackFilterForm',
|
'RackFilterForm',
|
||||||
'RackElevationFilterForm',
|
'RackElevationFilterForm',
|
||||||
'RackReservationFilterForm',
|
'RackReservationFilterForm',
|
||||||
'RackRoleFilterForm',
|
'RackRoleFilterForm',
|
||||||
'RackTypeFilterForm',
|
'RackTypeFilterForm',
|
||||||
'RearPortFilterForm',
|
'RearPortFilterForm',
|
||||||
'RearPortTemplateFilterForm',
|
|
||||||
'RegionFilterForm',
|
'RegionFilterForm',
|
||||||
'SiteFilterForm',
|
'SiteFilterForm',
|
||||||
'SiteGroupFilterForm',
|
'SiteGroupFilterForm',
|
||||||
'VirtualChassisFilterForm',
|
'VirtualChassisFilterForm',
|
||||||
'VirtualDeviceContextFilterForm',
|
'VirtualDeviceContextFilterForm'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DeviceComponentFilterForm(OwnerFilterMixin, NetBoxModelFilterSetForm):
|
class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
|
||||||
name = forms.CharField(
|
name = forms.CharField(
|
||||||
label=_('Name'),
|
label=_('Name'),
|
||||||
required=False
|
required=False
|
||||||
@@ -135,11 +123,6 @@ class DeviceComponentFilterForm(OwnerFilterMixin, NetBoxModelFilterSetForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label=_('Device role')
|
label=_('Device role')
|
||||||
)
|
)
|
||||||
tenant_id = DynamicModelMultipleChoiceField(
|
|
||||||
queryset=Tenant.objects.all(),
|
|
||||||
required=False,
|
|
||||||
label=_('Tenant')
|
|
||||||
)
|
|
||||||
device_id = DynamicModelMultipleChoiceField(
|
device_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@@ -148,8 +131,7 @@ class DeviceComponentFilterForm(OwnerFilterMixin, NetBoxModelFilterSetForm):
|
|||||||
'location_id': '$location_id',
|
'location_id': '$location_id',
|
||||||
'virtual_chassis_id': '$virtual_chassis_id',
|
'virtual_chassis_id': '$virtual_chassis_id',
|
||||||
'device_type_id': '$device_type_id',
|
'device_type_id': '$device_type_id',
|
||||||
'role_id': '$role_id',
|
'role_id': '$role_id'
|
||||||
'tenant_id': '$tenant_id'
|
|
||||||
},
|
},
|
||||||
label=_('Device')
|
label=_('Device')
|
||||||
)
|
)
|
||||||
@@ -158,14 +140,18 @@ class DeviceComponentFilterForm(OwnerFilterMixin, NetBoxModelFilterSetForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label=_('Device Status'),
|
label=_('Device Status'),
|
||||||
)
|
)
|
||||||
|
owner_id = DynamicModelChoiceField(
|
||||||
|
queryset=Owner.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Owner'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RegionFilterForm(ContactModelFilterForm, NestedGroupModelFilterSetForm):
|
class RegionFilterForm(ContactModelFilterForm, NestedGroupModelFilterSetForm):
|
||||||
model = Region
|
model = Region
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('parent_id', name=_('Region')),
|
FieldSet('parent_id', name=_('Region')),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts'))
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts'))
|
||||||
)
|
)
|
||||||
parent_id = DynamicModelMultipleChoiceField(
|
parent_id = DynamicModelMultipleChoiceField(
|
||||||
@@ -179,9 +165,8 @@ class RegionFilterForm(ContactModelFilterForm, NestedGroupModelFilterSetForm):
|
|||||||
class SiteGroupFilterForm(ContactModelFilterForm, NestedGroupModelFilterSetForm):
|
class SiteGroupFilterForm(ContactModelFilterForm, NestedGroupModelFilterSetForm):
|
||||||
model = SiteGroup
|
model = SiteGroup
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('parent_id', name=_('Site Group')),
|
FieldSet('parent_id', name=_('Site Group')),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts'))
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts'))
|
||||||
)
|
)
|
||||||
parent_id = DynamicModelMultipleChoiceField(
|
parent_id = DynamicModelMultipleChoiceField(
|
||||||
@@ -195,10 +180,9 @@ class SiteGroupFilterForm(ContactModelFilterForm, NestedGroupModelFilterSetForm)
|
|||||||
class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm):
|
class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = Site
|
model = Site
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('status', 'region_id', 'group_id', 'asn_id', name=_('Attributes')),
|
FieldSet('status', 'region_id', 'group_id', 'asn_id', name=_('Attributes')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||||
)
|
)
|
||||||
selector_fields = ('filter_id', 'q', 'region_id', 'group_id')
|
selector_fields = ('filter_id', 'q', 'region_id', 'group_id')
|
||||||
@@ -228,10 +212,9 @@ class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilt
|
|||||||
class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NestedGroupModelFilterSetForm):
|
class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NestedGroupModelFilterSetForm):
|
||||||
model = Location
|
model = Location
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'parent_id', 'status', 'facility', name=_('Attributes')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'parent_id', 'status', 'facility', name=_('Attributes')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||||
)
|
)
|
||||||
region_id = DynamicModelMultipleChoiceField(
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
@@ -277,8 +260,7 @@ class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NestedGroupM
|
|||||||
class RackRoleFilterForm(OrganizationalModelFilterSetForm):
|
class RackRoleFilterForm(OrganizationalModelFilterSetForm):
|
||||||
model = RackRole
|
model = RackRole
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
@@ -329,11 +311,10 @@ class RackBaseFilterForm(PrimaryModelFilterSetForm):
|
|||||||
class RackTypeFilterForm(RackBaseFilterForm):
|
class RackTypeFilterForm(RackBaseFilterForm):
|
||||||
model = RackType
|
model = RackType
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('manufacturer_id', 'form_factor', 'width', 'u_height', 'rack_count', name=_('Rack Type')),
|
FieldSet('manufacturer_id', 'form_factor', 'width', 'u_height', 'rack_count', name=_('Rack Type')),
|
||||||
FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
|
FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
|
||||||
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
|
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
)
|
)
|
||||||
selector_fields = ('filter_id', 'q', 'manufacturer_id')
|
selector_fields = ('filter_id', 'q', 'manufacturer_id')
|
||||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||||
@@ -352,14 +333,13 @@ class RackTypeFilterForm(RackBaseFilterForm):
|
|||||||
class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, RackBaseFilterForm):
|
class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, RackBaseFilterForm):
|
||||||
model = Rack
|
model = Rack
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')),
|
||||||
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
FieldSet('status', 'role_id', 'manufacturer_id', 'rack_type_id', 'serial', 'asset_tag', name=_('Rack')),
|
FieldSet('status', 'role_id', 'manufacturer_id', 'rack_type_id', 'serial', 'asset_tag', name=_('Rack')),
|
||||||
FieldSet('form_factor', 'width', 'u_height', 'airflow', name=_('Hardware')),
|
FieldSet('form_factor', 'width', 'u_height', 'airflow', name=_('Hardware')),
|
||||||
FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
|
FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
|
||||||
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
|
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||||
)
|
)
|
||||||
selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'location_id')
|
selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'location_id')
|
||||||
@@ -436,10 +416,9 @@ class RackElevationFilterForm(RackFilterForm):
|
|||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'id', name=_('Location')),
|
||||||
FieldSet('status', 'role_id', name=_('Function')),
|
FieldSet('status', 'role_id', name=_('Function')),
|
||||||
FieldSet('type', 'width', 'serial', 'asset_tag', name=_('Hardware')),
|
FieldSet('type', 'width', 'serial', 'asset_tag', name=_('Hardware')),
|
||||||
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
|
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||||
|
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
|
||||||
)
|
)
|
||||||
id = DynamicModelMultipleChoiceField(
|
id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Rack.objects.all(),
|
queryset=Rack.objects.all(),
|
||||||
@@ -455,11 +434,10 @@ class RackElevationFilterForm(RackFilterForm):
|
|||||||
class RackReservationFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
class RackReservationFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = RackReservation
|
model = RackReservation
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('status', 'user_id', name=_('Reservation')),
|
FieldSet('status', 'user_id', name=_('Reservation')),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Rack')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Rack')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
)
|
)
|
||||||
region_id = DynamicModelMultipleChoiceField(
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
@@ -514,8 +492,7 @@ class RackReservationFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
|||||||
class ManufacturerFilterForm(ContactModelFilterForm, OrganizationalModelFilterSetForm):
|
class ManufacturerFilterForm(ContactModelFilterForm, OrganizationalModelFilterSetForm):
|
||||||
model = Manufacturer
|
model = Manufacturer
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts'))
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts'))
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
@@ -524,7 +501,7 @@ class ManufacturerFilterForm(ContactModelFilterForm, OrganizationalModelFilterSe
|
|||||||
class DeviceTypeFilterForm(PrimaryModelFilterSetForm):
|
class DeviceTypeFilterForm(PrimaryModelFilterSetForm):
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'manufacturer_id', 'default_platform_id', 'part_number', 'device_count',
|
'manufacturer_id', 'default_platform_id', 'part_number', 'device_count',
|
||||||
'subdevice_role', 'airflow', name=_('Hardware')
|
'subdevice_role', 'airflow', name=_('Hardware')
|
||||||
@@ -535,7 +512,6 @@ class DeviceTypeFilterForm(PrimaryModelFilterSetForm):
|
|||||||
'pass_through_ports', 'device_bays', 'module_bays', 'inventory_items', name=_('Components')
|
'pass_through_ports', 'device_bays', 'module_bays', 'inventory_items', name=_('Components')
|
||||||
),
|
),
|
||||||
FieldSet('weight', 'weight_unit', name=_('Weight')),
|
FieldSet('weight', 'weight_unit', name=_('Weight')),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
)
|
)
|
||||||
selector_fields = ('filter_id', 'q', 'manufacturer_id')
|
selector_fields = ('filter_id', 'q', 'manufacturer_id')
|
||||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||||
@@ -659,8 +635,7 @@ class DeviceTypeFilterForm(PrimaryModelFilterSetForm):
|
|||||||
class ModuleTypeProfileFilterForm(PrimaryModelFilterSetForm):
|
class ModuleTypeProfileFilterForm(PrimaryModelFilterSetForm):
|
||||||
model = ModuleTypeProfile
|
model = ModuleTypeProfile
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
)
|
)
|
||||||
selector_fields = ('filter_id', 'q')
|
selector_fields = ('filter_id', 'q')
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
@@ -669,7 +644,7 @@ class ModuleTypeProfileFilterForm(PrimaryModelFilterSetForm):
|
|||||||
class ModuleTypeFilterForm(PrimaryModelFilterSetForm):
|
class ModuleTypeFilterForm(PrimaryModelFilterSetForm):
|
||||||
model = ModuleType
|
model = ModuleType
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'profile_id', 'manufacturer_id', 'part_number', 'module_count',
|
'profile_id', 'manufacturer_id', 'part_number', 'module_count',
|
||||||
'airflow', name=_('Hardware')
|
'airflow', name=_('Hardware')
|
||||||
@@ -679,7 +654,6 @@ class ModuleTypeFilterForm(PrimaryModelFilterSetForm):
|
|||||||
'pass_through_ports', name=_('Components')
|
'pass_through_ports', name=_('Components')
|
||||||
),
|
),
|
||||||
FieldSet('weight', 'weight_unit', name=_('Weight')),
|
FieldSet('weight', 'weight_unit', name=_('Weight')),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
)
|
)
|
||||||
selector_fields = ('filter_id', 'q', 'manufacturer_id')
|
selector_fields = ('filter_id', 'q', 'manufacturer_id')
|
||||||
profile_id = DynamicModelMultipleChoiceField(
|
profile_id = DynamicModelMultipleChoiceField(
|
||||||
@@ -763,9 +737,8 @@ class ModuleTypeFilterForm(PrimaryModelFilterSetForm):
|
|||||||
class DeviceRoleFilterForm(NestedGroupModelFilterSetForm):
|
class DeviceRoleFilterForm(NestedGroupModelFilterSetForm):
|
||||||
model = DeviceRole
|
model = DeviceRole
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('parent_id', 'config_template_id', name=_('Device Role')),
|
FieldSet('parent_id', 'config_template_id', name=_('Device Role'))
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
)
|
)
|
||||||
config_template_id = DynamicModelMultipleChoiceField(
|
config_template_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=ConfigTemplate.objects.all(),
|
queryset=ConfigTemplate.objects.all(),
|
||||||
@@ -783,9 +756,8 @@ class DeviceRoleFilterForm(NestedGroupModelFilterSetForm):
|
|||||||
class PlatformFilterForm(NestedGroupModelFilterSetForm):
|
class PlatformFilterForm(NestedGroupModelFilterSetForm):
|
||||||
model = Platform
|
model = Platform
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('manufacturer_id', 'parent_id', 'config_template_id', name=_('Platform')),
|
FieldSet('manufacturer_id', 'parent_id', 'config_template_id', name=_('Platform'))
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
)
|
)
|
||||||
selector_fields = ('filter_id', 'q', 'manufacturer_id')
|
selector_fields = ('filter_id', 'q', 'manufacturer_id')
|
||||||
parent_id = DynamicModelMultipleChoiceField(
|
parent_id = DynamicModelMultipleChoiceField(
|
||||||
@@ -814,12 +786,11 @@ class DeviceFilterForm(
|
|||||||
):
|
):
|
||||||
model = Device
|
model = Device
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||||
FieldSet('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address', name=_('Operation')),
|
FieldSet('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address', name=_('Operation')),
|
||||||
FieldSet('manufacturer_id', 'device_type_id', 'platform_id', name=_('Hardware')),
|
FieldSet('manufacturer_id', 'device_type_id', 'platform_id', name=_('Hardware')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports',
|
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports',
|
||||||
@@ -1008,10 +979,9 @@ class DeviceFilterForm(
|
|||||||
class VirtualDeviceContextFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
class VirtualDeviceContextFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = VirtualDeviceContext
|
model = VirtualDeviceContext
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('device', 'status', 'has_primary_ip', name=_('Attributes')),
|
FieldSet('device', 'status', 'has_primary_ip', name=_('Attributes')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
)
|
)
|
||||||
device = DynamicModelMultipleChoiceField(
|
device = DynamicModelMultipleChoiceField(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
@@ -1036,10 +1006,9 @@ class VirtualDeviceContextFilterForm(TenancyFilterForm, PrimaryModelFilterSetFor
|
|||||||
class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
|
class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = Module
|
model = Module
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'device_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'device_id', name=_('Location')),
|
||||||
FieldSet('manufacturer_id', 'module_type_id', 'status', 'serial', 'asset_tag', name=_('Hardware')),
|
FieldSet('manufacturer_id', 'module_type_id', 'status', 'serial', 'asset_tag', name=_('Hardware')),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
)
|
)
|
||||||
device_id = DynamicModelMultipleChoiceField(
|
device_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
@@ -1120,10 +1089,9 @@ class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, PrimaryM
|
|||||||
class VirtualChassisFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
class VirtualChassisFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = VirtualChassis
|
model = VirtualChassis
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
|
||||||
FieldSet('tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
)
|
)
|
||||||
region_id = DynamicModelMultipleChoiceField(
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
@@ -1150,11 +1118,10 @@ class VirtualChassisFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
|||||||
class CableFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
class CableFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = Cable
|
model = Cable
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('site_id', 'location_id', 'rack_id', 'device_id', name=_('Location')),
|
FieldSet('site_id', 'location_id', 'rack_id', 'device_id', name=_('Location')),
|
||||||
FieldSet('type', 'status', 'profile', 'color', 'length', 'length_unit', 'unterminated', name=_('Attributes')),
|
FieldSet('type', 'status', 'profile', 'color', 'length', 'length_unit', 'unterminated', name=_('Attributes')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
)
|
)
|
||||||
region_id = DynamicModelMultipleChoiceField(
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
@@ -1240,9 +1207,8 @@ class CableFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
|||||||
class PowerPanelFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
class PowerPanelFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = PowerPanel
|
model = PowerPanel
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||||
)
|
)
|
||||||
selector_fields = ('filter_id', 'q', 'site_id', 'location_id')
|
selector_fields = ('filter_id', 'q', 'site_id', 'location_id')
|
||||||
@@ -1280,11 +1246,10 @@ class PowerPanelFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
|||||||
class PowerFeedFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
class PowerFeedFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||||
model = PowerFeed
|
model = PowerFeed
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'power_panel_id', 'rack_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'power_panel_id', 'rack_id', name=_('Location')),
|
||||||
FieldSet('status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', name=_('Attributes')),
|
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
FieldSet('status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', name=_('Attributes')),
|
||||||
)
|
)
|
||||||
region_id = DynamicModelMultipleChoiceField(
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
@@ -1361,23 +1326,6 @@ class PowerFeedFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
|||||||
# Device components
|
# Device components
|
||||||
#
|
#
|
||||||
|
|
||||||
class DeviceComponentTemplateFilterForm(NetBoxModelFilterSetForm):
|
|
||||||
device_type_id = DynamicModelMultipleChoiceField(
|
|
||||||
queryset=DeviceType.objects.all(),
|
|
||||||
required=False,
|
|
||||||
label=_('Device type'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ModularDeviceComponentTemplateFilterForm(DeviceComponentTemplateFilterForm):
|
|
||||||
module_type_id = DynamicModelMultipleChoiceField(
|
|
||||||
queryset=ModuleType.objects.all(),
|
|
||||||
required=False,
|
|
||||||
query_params={'manufacturer_id': '$manufacturer_id'},
|
|
||||||
label=_('Module Type'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CabledFilterForm(forms.Form):
|
class CabledFilterForm(forms.Form):
|
||||||
cabled = forms.NullBooleanField(
|
cabled = forms.NullBooleanField(
|
||||||
label=_('Cabled'),
|
label=_('Cabled'),
|
||||||
@@ -1408,15 +1356,13 @@ class PathEndpointFilterForm(CabledFilterForm):
|
|||||||
class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||||
model = ConsolePort
|
model = ConsolePort
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')),
|
FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', name=_('Device')
|
||||||
name=_('Device')
|
|
||||||
),
|
),
|
||||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
)
|
)
|
||||||
type = forms.MultipleChoiceField(
|
type = forms.MultipleChoiceField(
|
||||||
label=_('Type'),
|
label=_('Type'),
|
||||||
@@ -1431,32 +1377,17 @@ class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
|||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
|
||||||
model = ConsolePortTemplate
|
|
||||||
fieldsets = (
|
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
|
||||||
FieldSet('name', 'label', 'type', name=_('Attributes')),
|
|
||||||
FieldSet('device_type_id', 'module_type_id', name=_('Device')),
|
|
||||||
)
|
|
||||||
type = forms.MultipleChoiceField(
|
|
||||||
label=_('Type'),
|
|
||||||
choices=ConsolePortTypeChoices,
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||||
model = ConsoleServerPort
|
model = ConsoleServerPort
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')),
|
FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||||
name=_('Device')
|
name=_('Device')
|
||||||
),
|
),
|
||||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
)
|
)
|
||||||
type = forms.MultipleChoiceField(
|
type = forms.MultipleChoiceField(
|
||||||
label=_('Type'),
|
label=_('Type'),
|
||||||
@@ -1471,32 +1402,16 @@ class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterF
|
|||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
|
||||||
model = ConsoleServerPortTemplate
|
|
||||||
fieldsets = (
|
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
|
||||||
FieldSet('name', 'label', 'type', name=_('Attributes')),
|
|
||||||
FieldSet('device_type_id', 'module_type_id', name=_('Device')),
|
|
||||||
)
|
|
||||||
type = forms.MultipleChoiceField(
|
|
||||||
label=_('Type'),
|
|
||||||
choices=ConsolePortTypeChoices,
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||||
model = PowerPort
|
model = PowerPort
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('name', 'label', 'type', name=_('Attributes')),
|
FieldSet('name', 'label', 'type', name=_('Attributes')),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', name=_('Device')
|
||||||
name=_('Device')
|
|
||||||
),
|
),
|
||||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
)
|
)
|
||||||
type = forms.MultipleChoiceField(
|
type = forms.MultipleChoiceField(
|
||||||
label=_('Type'),
|
label=_('Type'),
|
||||||
@@ -1506,32 +1421,17 @@ class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
|||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
class PowerPortTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
|
||||||
model = PowerPortTemplate
|
|
||||||
fieldsets = (
|
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
|
||||||
FieldSet('name', 'label', 'type', name=_('Attributes')),
|
|
||||||
FieldSet('device_type_id', 'module_type_id', name=_('Device')),
|
|
||||||
)
|
|
||||||
type = forms.MultipleChoiceField(
|
|
||||||
label=_('Type'),
|
|
||||||
choices=PowerPortTypeChoices,
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||||
model = PowerOutlet
|
model = PowerOutlet
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('name', 'label', 'type', 'color', 'status', name=_('Attributes')),
|
FieldSet('name', 'label', 'type', 'color', 'status', name=_('Attributes')),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||||
name=_('Device')
|
name=_('Device')
|
||||||
),
|
),
|
||||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
)
|
)
|
||||||
type = forms.MultipleChoiceField(
|
type = forms.MultipleChoiceField(
|
||||||
label=_('Type'),
|
label=_('Type'),
|
||||||
@@ -1550,24 +1450,10 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
|
||||||
model = PowerOutletTemplate
|
|
||||||
fieldsets = (
|
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
|
||||||
FieldSet('name', 'label', 'type', name=_('Attributes')),
|
|
||||||
FieldSet('device_type_id', 'module_type_id', name=_('Device')),
|
|
||||||
)
|
|
||||||
type = forms.MultipleChoiceField(
|
|
||||||
label=_('Type'),
|
|
||||||
choices=PowerOutletTypeChoices,
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||||
model = Interface
|
model = Interface
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only', name=_('Attributes')),
|
FieldSet('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only', name=_('Attributes')),
|
||||||
FieldSet('vrf_id', 'l2vpn_id', 'mac_address', 'wwn', name=_('Addressing')),
|
FieldSet('vrf_id', 'l2vpn_id', 'mac_address', 'wwn', name=_('Addressing')),
|
||||||
FieldSet('poe_mode', 'poe_type', name=_('PoE')),
|
FieldSet('poe_mode', 'poe_type', name=_('PoE')),
|
||||||
@@ -1575,12 +1461,10 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
|||||||
FieldSet('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power', name=_('Wireless')),
|
FieldSet('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power', name=_('Wireless')),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', 'vdc_id',
|
||||||
'vdc_id',
|
|
||||||
name=_('Device')
|
name=_('Device')
|
||||||
),
|
),
|
||||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
)
|
)
|
||||||
selector_fields = ('filter_id', 'q', 'device_id')
|
selector_fields = ('filter_id', 'q', 'device_id')
|
||||||
vdc_id = DynamicModelMultipleChoiceField(
|
vdc_id = DynamicModelMultipleChoiceField(
|
||||||
@@ -1692,62 +1576,15 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
|||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
class InterfaceTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
|
||||||
model = InterfaceTemplate
|
|
||||||
fieldsets = (
|
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
|
||||||
FieldSet('name', 'label', 'type', 'enabled', 'mgmt_only', name=_('Attributes')),
|
|
||||||
FieldSet('poe_mode', 'poe_type', name=_('PoE')),
|
|
||||||
FieldSet('rf_role', name=_('Wireless')),
|
|
||||||
FieldSet('device_type_id', 'module_type_id', name=_('Device')),
|
|
||||||
)
|
|
||||||
type = forms.MultipleChoiceField(
|
|
||||||
label=_('Type'),
|
|
||||||
choices=InterfaceTypeChoices,
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
enabled = forms.NullBooleanField(
|
|
||||||
label=_('Enabled'),
|
|
||||||
required=False,
|
|
||||||
widget=forms.Select(
|
|
||||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
|
||||||
)
|
|
||||||
)
|
|
||||||
mgmt_only = forms.NullBooleanField(
|
|
||||||
label=_('Management only'),
|
|
||||||
required=False,
|
|
||||||
widget=forms.Select(
|
|
||||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
|
||||||
)
|
|
||||||
)
|
|
||||||
poe_mode = forms.MultipleChoiceField(
|
|
||||||
choices=InterfacePoEModeChoices,
|
|
||||||
required=False,
|
|
||||||
label=_('PoE mode')
|
|
||||||
)
|
|
||||||
poe_type = forms.MultipleChoiceField(
|
|
||||||
choices=InterfacePoETypeChoices,
|
|
||||||
required=False,
|
|
||||||
label=_('PoE type')
|
|
||||||
)
|
|
||||||
rf_role = forms.MultipleChoiceField(
|
|
||||||
choices=WirelessRoleChoices,
|
|
||||||
required=False,
|
|
||||||
label=_('Wireless role')
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
|
FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', name=_('Device')
|
||||||
name=_('Device')
|
|
||||||
),
|
),
|
||||||
FieldSet('cabled', 'occupied', name=_('Cable')),
|
FieldSet('cabled', 'occupied', name=_('Cable')),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
)
|
)
|
||||||
model = FrontPort
|
model = FrontPort
|
||||||
type = forms.MultipleChoiceField(
|
type = forms.MultipleChoiceField(
|
||||||
@@ -1762,36 +1599,17 @@ class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
|||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
class FrontPortTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
|
||||||
model = FrontPortTemplate
|
|
||||||
fieldsets = (
|
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
|
||||||
FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
|
|
||||||
FieldSet('device_type_id', 'module_type_id', name=_('Device')),
|
|
||||||
)
|
|
||||||
type = forms.MultipleChoiceField(
|
|
||||||
label=_('Type'),
|
|
||||||
choices=PortTypeChoices,
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
color = ColorField(
|
|
||||||
label=_('Color'),
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
||||||
model = RearPort
|
model = RearPort
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
|
FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||||
name=_('Device')
|
name=_('Device')
|
||||||
),
|
),
|
||||||
FieldSet('cabled', 'occupied', name=_('Cable')),
|
FieldSet('cabled', 'occupied', name=_('Cable')),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
)
|
)
|
||||||
type = forms.MultipleChoiceField(
|
type = forms.MultipleChoiceField(
|
||||||
label=_('Type'),
|
label=_('Type'),
|
||||||
@@ -1805,35 +1623,16 @@ class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
|||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
class RearPortTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
|
||||||
model = RearPortTemplate
|
|
||||||
fieldsets = (
|
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
|
||||||
FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
|
|
||||||
FieldSet('device_type_id', 'module_type_id', name=_('Device')),
|
|
||||||
)
|
|
||||||
type = forms.MultipleChoiceField(
|
|
||||||
label=_('Type'),
|
|
||||||
choices=PortTypeChoices,
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
color = ColorField(
|
|
||||||
label=_('Color'),
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ModuleBayFilterForm(DeviceComponentFilterForm):
|
class ModuleBayFilterForm(DeviceComponentFilterForm):
|
||||||
model = ModuleBay
|
model = ModuleBay
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('name', 'label', 'position', name=_('Attributes')),
|
FieldSet('name', 'label', 'position', name=_('Attributes')),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||||
name=_('Device')
|
name=_('Device')
|
||||||
),
|
),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
position = forms.CharField(
|
position = forms.CharField(
|
||||||
@@ -1842,57 +1641,33 @@ class ModuleBayFilterForm(DeviceComponentFilterForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ModuleBayTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
|
||||||
model = ModuleBayTemplate
|
|
||||||
fieldsets = (
|
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
|
||||||
FieldSet('name', 'label', 'position', name=_('Attributes')),
|
|
||||||
FieldSet('device_type_id', 'module_type_id', name=_('Device')),
|
|
||||||
)
|
|
||||||
position = forms.CharField(
|
|
||||||
label=_('Position'),
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayFilterForm(DeviceComponentFilterForm):
|
class DeviceBayFilterForm(DeviceComponentFilterForm):
|
||||||
model = DeviceBay
|
model = DeviceBay
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('name', 'label', name=_('Attributes')),
|
FieldSet('name', 'label', name=_('Attributes')),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||||
name=_('Device')
|
name=_('Device')
|
||||||
),
|
),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayTemplateFilterForm(DeviceComponentTemplateFilterForm):
|
|
||||||
model = DeviceBayTemplate
|
|
||||||
fieldsets = (
|
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
|
||||||
FieldSet('name', 'label', name=_('Attributes')),
|
|
||||||
FieldSet('device_type_id', name=_('Device')),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class InventoryItemFilterForm(DeviceComponentFilterForm):
|
class InventoryItemFilterForm(DeviceComponentFilterForm):
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'name', 'label', 'status', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered',
|
'name', 'label', 'status', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered',
|
||||||
name=_('Attributes')
|
name=_('Attributes')
|
||||||
),
|
),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||||
name=_('Device')
|
name=_('Device')
|
||||||
),
|
),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
)
|
)
|
||||||
role_id = DynamicModelMultipleChoiceField(
|
role_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=InventoryItemRole.objects.all(),
|
queryset=InventoryItemRole.objects.all(),
|
||||||
@@ -1927,25 +1702,6 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
|
|||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
class InventoryItemTemplateFilterForm(DeviceComponentTemplateFilterForm):
|
|
||||||
model = InventoryItemTemplate
|
|
||||||
fieldsets = (
|
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
|
||||||
FieldSet('name', 'label', 'role_id', 'manufacturer_id', name=_('Attributes')),
|
|
||||||
FieldSet('device_type_id', name=_('Device')),
|
|
||||||
)
|
|
||||||
role_id = DynamicModelMultipleChoiceField(
|
|
||||||
queryset=InventoryItemRole.objects.all(),
|
|
||||||
required=False,
|
|
||||||
label=_('Role')
|
|
||||||
)
|
|
||||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
|
||||||
queryset=Manufacturer.objects.all(),
|
|
||||||
required=False,
|
|
||||||
label=_('Manufacturer')
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Device component roles
|
# Device component roles
|
||||||
#
|
#
|
||||||
@@ -1953,8 +1709,7 @@ class InventoryItemTemplateFilterForm(DeviceComponentTemplateFilterForm):
|
|||||||
class InventoryItemRoleFilterForm(OrganizationalModelFilterSetForm):
|
class InventoryItemRoleFilterForm(OrganizationalModelFilterSetForm):
|
||||||
model = InventoryItemRole
|
model = InventoryItemRole
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
@@ -1966,10 +1721,9 @@ class InventoryItemRoleFilterForm(OrganizationalModelFilterSetForm):
|
|||||||
class MACAddressFilterForm(PrimaryModelFilterSetForm):
|
class MACAddressFilterForm(PrimaryModelFilterSetForm):
|
||||||
model = MACAddress
|
model = MACAddress
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
FieldSet('mac_address', name=_('Attributes')),
|
FieldSet('mac_address', name=_('Attributes')),
|
||||||
FieldSet('device_id', 'virtual_machine_id', 'assigned', 'primary', name=_('Assignments')),
|
FieldSet('device_id', 'virtual_machine_id', 'assigned', 'primary', name=_('Assignments')),
|
||||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
|
||||||
)
|
)
|
||||||
selector_fields = ('filter_id', 'q', 'device_id', 'virtual_machine_id')
|
selector_fields = ('filter_id', 'q', 'device_id', 'virtual_machine_id')
|
||||||
mac_address = forms.CharField(
|
mac_address = forms.CharField(
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||||
from django.db import connection
|
|
||||||
from django.db.models.signals import post_save
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from dcim.constants import LOCATION_SCOPE_TYPES
|
from dcim.constants import LOCATION_SCOPE_TYPES
|
||||||
from dcim.models import PortMapping, PortTemplateMapping, Site
|
from dcim.models import Site
|
||||||
from utilities.forms import get_field_value
|
from utilities.forms import get_field_value
|
||||||
from utilities.forms.fields import (
|
from utilities.forms.fields import (
|
||||||
ContentTypeChoiceField, CSVContentTypeField, DynamicModelChoiceField,
|
ContentTypeChoiceField, CSVContentTypeField, DynamicModelChoiceField,
|
||||||
@@ -15,7 +13,6 @@ from utilities.templatetags.builtins.filters import bettertitle
|
|||||||
from utilities.forms.widgets import HTMXSelect
|
from utilities.forms.widgets import HTMXSelect
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'FrontPortFormMixin',
|
|
||||||
'ScopedBulkEditForm',
|
'ScopedBulkEditForm',
|
||||||
'ScopedForm',
|
'ScopedForm',
|
||||||
'ScopedImportForm',
|
'ScopedImportForm',
|
||||||
@@ -75,7 +72,7 @@ class ScopedForm(forms.Form):
|
|||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if self.instance and self.instance.pk and scope_type_id != self.instance.scope_type_id:
|
if self.instance and scope_type_id != self.instance.scope_type_id:
|
||||||
self.initial['scope'] = None
|
self.initial['scope'] = None
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -131,91 +128,3 @@ class ScopedImportForm(forms.Form):
|
|||||||
"Please select a {scope_type}."
|
"Please select a {scope_type}."
|
||||||
).format(scope_type=scope_type.model_class()._meta.model_name)
|
).format(scope_type=scope_type.model_class()._meta.model_name)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class FrontPortFormMixin(forms.Form):
|
|
||||||
rear_ports = forms.MultipleChoiceField(
|
|
||||||
choices=[],
|
|
||||||
label=_('Rear ports'),
|
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8})
|
|
||||||
)
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
super().clean()
|
|
||||||
|
|
||||||
# Check that the total number of FrontPorts and positions matches the selected number of RearPort:position
|
|
||||||
# mappings. Note that `name` will be a list under FrontPortCreateForm, in which cases we multiply the number of
|
|
||||||
# FrontPorts being creation by the number of positions.
|
|
||||||
positions = self.cleaned_data['positions']
|
|
||||||
frontport_count = len(self.cleaned_data['name']) if type(self.cleaned_data['name']) is list else 1
|
|
||||||
rearport_count = len(self.cleaned_data['rear_ports'])
|
|
||||||
if frontport_count * positions != rearport_count:
|
|
||||||
raise forms.ValidationError({
|
|
||||||
'rear_ports': _(
|
|
||||||
"The total number of front port positions ({frontport_count}) must match the selected number of "
|
|
||||||
"rear port positions ({rearport_count})."
|
|
||||||
).format(
|
|
||||||
frontport_count=frontport_count,
|
|
||||||
rearport_count=rearport_count
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
def _save_m2m(self):
|
|
||||||
super()._save_m2m()
|
|
||||||
|
|
||||||
# TODO: Can this be made more efficient?
|
|
||||||
# Delete existing rear port mappings
|
|
||||||
self.port_mapping_model.objects.filter(front_port_id=self.instance.pk).delete()
|
|
||||||
|
|
||||||
# Create new rear port mappings
|
|
||||||
mappings = []
|
|
||||||
if self.port_mapping_model is PortTemplateMapping:
|
|
||||||
params = {
|
|
||||||
'device_type_id': self.instance.device_type_id,
|
|
||||||
'module_type_id': self.instance.module_type_id,
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
params = {
|
|
||||||
'device_id': self.instance.device_id,
|
|
||||||
}
|
|
||||||
for i, rp_position in enumerate(self.cleaned_data['rear_ports'], start=1):
|
|
||||||
rear_port_id, rear_port_position = rp_position.split(':')
|
|
||||||
mappings.append(
|
|
||||||
self.port_mapping_model(**{
|
|
||||||
**params,
|
|
||||||
'front_port_id': self.instance.pk,
|
|
||||||
'front_port_position': i,
|
|
||||||
'rear_port_id': rear_port_id,
|
|
||||||
'rear_port_position': rear_port_position,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
self.port_mapping_model.objects.bulk_create(mappings)
|
|
||||||
# Send post_save signals
|
|
||||||
for mapping in mappings:
|
|
||||||
post_save.send(
|
|
||||||
sender=PortMapping,
|
|
||||||
instance=mapping,
|
|
||||||
created=True,
|
|
||||||
raw=False,
|
|
||||||
using=connection,
|
|
||||||
update_fields=None
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_rear_port_choices(self, parent_filter, front_port):
|
|
||||||
"""
|
|
||||||
Return a list of choices representing each available rear port & position pair on the parent object (identified
|
|
||||||
by a Q filter), excluding those assigned to the specified instance.
|
|
||||||
"""
|
|
||||||
occupied_rear_port_positions = [
|
|
||||||
f'{mapping.rear_port_id}:{mapping.rear_port_position}'
|
|
||||||
for mapping in self.port_mapping_model.objects.filter(parent_filter).exclude(front_port=front_port.pk)
|
|
||||||
]
|
|
||||||
|
|
||||||
choices = []
|
|
||||||
for rear_port in self.rear_port_model.objects.filter(parent_filter):
|
|
||||||
for i in range(1, rear_port.positions + 1):
|
|
||||||
pair_id = f'{rear_port.pk}:{i}'
|
|
||||||
if pair_id not in occupied_rear_port_positions:
|
|
||||||
pair_label = f'{rear_port.name}:{i}'
|
|
||||||
choices.append((pair_id, pair_label))
|
|
||||||
return choices
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from timezone_field import TimeZoneFormField
|
|||||||
|
|
||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.forms.mixins import FrontPortFormMixin
|
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from extras.models import ConfigTemplate
|
from extras.models import ConfigTemplate
|
||||||
from ipam.choices import VLANQinQRoleChoices
|
from ipam.choices import VLANQinQRoleChoices
|
||||||
@@ -20,9 +19,7 @@ from utilities.forms.fields import (
|
|||||||
DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SlugField,
|
DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SlugField,
|
||||||
)
|
)
|
||||||
from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups
|
from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups
|
||||||
from utilities.forms.widgets import (
|
from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK
|
||||||
APISelect, ClearableFileInput, ClearableSelect, HTMXSelect, NumberWithOptions, SelectWithPK,
|
|
||||||
)
|
|
||||||
from utilities.jsonschema import JSONSchemaProperty
|
from utilities.jsonschema import JSONSchemaProperty
|
||||||
from virtualization.models import Cluster, VMInterface
|
from virtualization.models import Cluster, VMInterface
|
||||||
from wireless.models import WirelessLAN, WirelessLANGroup
|
from wireless.models import WirelessLAN, WirelessLANGroup
|
||||||
@@ -204,7 +201,7 @@ class RackRoleForm(OrganizationalModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = RackRole
|
model = RackRole
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'slug', 'color', 'description', 'owner', 'comments', 'tags',
|
'name', 'slug', 'color', 'description', 'owner', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -347,7 +344,7 @@ class ManufacturerForm(OrganizationalModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Manufacturer
|
model = Manufacturer
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'slug', 'description', 'owner', 'comments', 'tags',
|
'name', 'slug', 'description', 'owner', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -594,14 +591,6 @@ class DeviceForm(TenancyForm, PrimaryModelForm):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
face = forms.ChoiceField(
|
|
||||||
label=_('Face'),
|
|
||||||
choices=add_blank_choice(DeviceFaceChoices),
|
|
||||||
required=False,
|
|
||||||
widget=ClearableSelect(
|
|
||||||
requires_fields=['rack']
|
|
||||||
)
|
|
||||||
)
|
|
||||||
device_type = DynamicModelChoiceField(
|
device_type = DynamicModelChoiceField(
|
||||||
label=_('Device type'),
|
label=_('Device type'),
|
||||||
queryset=DeviceType.objects.all(),
|
queryset=DeviceType.objects.all(),
|
||||||
@@ -1123,45 +1112,34 @@ class InterfaceTemplateForm(ModularComponentTemplateForm):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class FrontPortTemplateForm(FrontPortFormMixin, ModularComponentTemplateForm):
|
class FrontPortTemplateForm(ModularComponentTemplateForm):
|
||||||
|
rear_port = DynamicModelChoiceField(
|
||||||
|
label=_('Rear port'),
|
||||||
|
queryset=RearPortTemplate.objects.all(),
|
||||||
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'device_type_id': '$device_type',
|
||||||
|
'module_type_id': '$module_type',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet(
|
FieldSet(
|
||||||
TabbedGroups(
|
TabbedGroups(
|
||||||
FieldSet('device_type', name=_('Device Type')),
|
FieldSet('device_type', name=_('Device Type')),
|
||||||
FieldSet('module_type', name=_('Module Type')),
|
FieldSet('module_type', name=_('Module Type')),
|
||||||
),
|
),
|
||||||
'name', 'label', 'type', 'positions', 'rear_ports', 'description',
|
'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
port_mapping_model = PortTemplateMapping
|
|
||||||
rear_port_model = RearPortTemplate
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = FrontPortTemplate
|
model = FrontPortTemplate
|
||||||
fields = [
|
fields = [
|
||||||
'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description',
|
'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
|
||||||
|
'description',
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
# Populate rear port choices based on parent DeviceType or ModuleType
|
|
||||||
if device_type_id := self.data.get('device_type') or self.initial.get('device_type'):
|
|
||||||
parent_filter = Q(device_type=device_type_id)
|
|
||||||
elif module_type_id := self.data.get('module_type') or self.initial.get('module_type'):
|
|
||||||
parent_filter = Q(module_type=module_type_id)
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
self.fields['rear_ports'].choices = self._get_rear_port_choices(parent_filter, self.instance)
|
|
||||||
|
|
||||||
# Set initial rear port mappings
|
|
||||||
if self.instance.pk:
|
|
||||||
self.initial['rear_ports'] = [
|
|
||||||
f'{mapping.rear_port_id}:{mapping.rear_port_position}'
|
|
||||||
for mapping in PortTemplateMapping.objects.filter(front_port_id=self.instance.pk)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class RearPortTemplateForm(ModularComponentTemplateForm):
|
class RearPortTemplateForm(ModularComponentTemplateForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
@@ -1600,41 +1578,28 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class FrontPortForm(FrontPortFormMixin, ModularDeviceComponentForm):
|
class FrontPortForm(ModularDeviceComponentForm):
|
||||||
|
rear_port = DynamicModelChoiceField(
|
||||||
|
queryset=RearPort.objects.all(),
|
||||||
|
query_params={
|
||||||
|
'device_id': '$device',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'rear_ports', 'mark_connected',
|
'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected',
|
||||||
'description', 'tags',
|
'description', 'tags',
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
port_mapping_model = PortMapping
|
|
||||||
rear_port_model = RearPort
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = FrontPort
|
model = FrontPort
|
||||||
fields = [
|
fields = [
|
||||||
'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'owner',
|
'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected',
|
||||||
'tags',
|
'description', 'owner', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
# Populate rear port choices
|
|
||||||
if device_id := self.data.get('device') or self.initial.get('device'):
|
|
||||||
parent_filter = Q(device=device_id)
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
self.fields['rear_ports'].choices = self._get_rear_port_choices(parent_filter, self.instance)
|
|
||||||
|
|
||||||
# Set initial rear port mappings
|
|
||||||
if self.instance.pk:
|
|
||||||
self.initial['rear_ports'] = [
|
|
||||||
f'{mapping.rear_port_id}:{mapping.rear_port_position}'
|
|
||||||
for mapping in PortMapping.objects.filter(front_port_id=self.instance.pk)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class RearPortForm(ModularDeviceComponentForm):
|
class RearPortForm(ModularDeviceComponentForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
@@ -1850,7 +1815,7 @@ class InventoryItemRoleForm(OrganizationalModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = InventoryItemRole
|
model = InventoryItemRole
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'slug', 'color', 'description', 'owner', 'comments', 'tags',
|
'name', 'slug', 'color', 'description', 'owner', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
|
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from netbox.forms import NetBoxModelForm
|
from netbox.forms import NetBoxModelForm
|
||||||
from netbox.forms.mixins import OwnerMixin
|
|
||||||
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField
|
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField
|
||||||
from utilities.forms.rendering import FieldSet, TabbedGroups
|
from utilities.forms.rendering import FieldSet, TabbedGroups
|
||||||
from utilities.forms.widgets import APISelect
|
from utilities.forms.widgets import APISelect
|
||||||
@@ -110,30 +109,85 @@ class InterfaceTemplateCreateForm(ComponentCreateForm, model_forms.InterfaceTemp
|
|||||||
|
|
||||||
|
|
||||||
class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemplateForm):
|
class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemplateForm):
|
||||||
|
rear_port = forms.MultipleChoiceField(
|
||||||
|
choices=[],
|
||||||
|
label=_('Rear ports'),
|
||||||
|
help_text=_('Select one rear port assignment for each front port being created.'),
|
||||||
|
widget=forms.SelectMultiple(attrs={'size': 6})
|
||||||
|
)
|
||||||
|
|
||||||
# Override fieldsets from FrontPortTemplateForm
|
# Override fieldsets from FrontPortTemplateForm to omit rear_port_position
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet(
|
FieldSet(
|
||||||
TabbedGroups(
|
TabbedGroups(
|
||||||
FieldSet('device_type', name=_('Device Type')),
|
FieldSet('device_type', name=_('Device Type')),
|
||||||
FieldSet('module_type', name=_('Module Type')),
|
FieldSet('module_type', name=_('Module Type')),
|
||||||
),
|
),
|
||||||
'name', 'label', 'type', 'color', 'positions', 'rear_ports', 'description',
|
'name', 'label', 'type', 'color', 'rear_port', 'description',
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta(model_forms.FrontPortTemplateForm.Meta):
|
||||||
model = FrontPortTemplate
|
exclude = ('name', 'label', 'rear_port', 'rear_port_position')
|
||||||
fields = (
|
|
||||||
'device_type', 'module_type', 'type', 'color', 'positions', 'description',
|
def __init__(self, *args, **kwargs):
|
||||||
)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# TODO: This needs better validation
|
||||||
|
if 'device_type' in self.initial or self.data.get('device_type'):
|
||||||
|
parent = DeviceType.objects.get(
|
||||||
|
pk=self.initial.get('device_type') or self.data.get('device_type')
|
||||||
|
)
|
||||||
|
elif 'module_type' in self.initial or self.data.get('module_type'):
|
||||||
|
parent = ModuleType.objects.get(
|
||||||
|
pk=self.initial.get('module_type') or self.data.get('module_type')
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Determine which rear port positions are occupied. These will be excluded from the list of available mappings.
|
||||||
|
occupied_port_positions = [
|
||||||
|
(front_port.rear_port_id, front_port.rear_port_position)
|
||||||
|
for front_port in parent.frontporttemplates.all()
|
||||||
|
]
|
||||||
|
|
||||||
|
# Populate rear port choices
|
||||||
|
choices = []
|
||||||
|
rear_ports = parent.rearporttemplates.all()
|
||||||
|
for rear_port in rear_ports:
|
||||||
|
for i in range(1, rear_port.positions + 1):
|
||||||
|
if (rear_port.pk, i) not in occupied_port_positions:
|
||||||
|
choices.append(
|
||||||
|
('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
|
||||||
|
)
|
||||||
|
self.fields['rear_port'].choices = choices
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
|
# Check that the number of FrontPortTemplates to be created matches the selected number of RearPortTemplate
|
||||||
|
# positions
|
||||||
|
frontport_count = len(self.cleaned_data['name'])
|
||||||
|
rearport_count = len(self.cleaned_data['rear_port'])
|
||||||
|
if frontport_count != rearport_count:
|
||||||
|
raise forms.ValidationError({
|
||||||
|
'rear_port': _(
|
||||||
|
"The number of front port templates to be created ({frontport_count}) must match the selected "
|
||||||
|
"number of rear port positions ({rearport_count})."
|
||||||
|
).format(
|
||||||
|
frontport_count=frontport_count,
|
||||||
|
rearport_count=rearport_count
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
def get_iterative_data(self, iteration):
|
def get_iterative_data(self, iteration):
|
||||||
positions = self.cleaned_data['positions']
|
|
||||||
offset = positions * iteration
|
# Assign rear port and position from selected set
|
||||||
|
rear_port, position = self.cleaned_data['rear_port'][iteration].split(':')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'rear_ports': self.cleaned_data['rear_ports'][offset:offset + positions]
|
'rear_port': int(rear_port),
|
||||||
|
'rear_port_position': int(position),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -215,26 +269,74 @@ class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
rear_port = forms.MultipleChoiceField(
|
||||||
|
choices=[],
|
||||||
|
label=_('Rear ports'),
|
||||||
|
help_text=_('Select one rear port assignment for each front port being created.'),
|
||||||
|
widget=forms.SelectMultiple(attrs={'size': 6})
|
||||||
|
)
|
||||||
|
|
||||||
# Override fieldsets from FrontPortForm to omit rear_port_position
|
# Override fieldsets from FrontPortForm to omit rear_port_position
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'rear_ports', 'mark_connected',
|
'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'mark_connected', 'description', 'tags',
|
||||||
'description', 'tags',
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta(model_forms.FrontPortForm.Meta):
|
||||||
model = FrontPort
|
exclude = ('name', 'label', 'rear_port', 'rear_port_position')
|
||||||
fields = [
|
|
||||||
'device', 'module', 'type', 'color', 'positions', 'mark_connected', 'description', 'owner', 'tags',
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
if device_id := self.data.get('device') or self.initial.get('device'):
|
||||||
|
device = Device.objects.get(pk=device_id)
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Determine which rear port positions are occupied. These will be excluded from the list of available
|
||||||
|
# mappings.
|
||||||
|
occupied_port_positions = [
|
||||||
|
(front_port.rear_port_id, front_port.rear_port_position)
|
||||||
|
for front_port in device.frontports.all()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Populate rear port choices
|
||||||
|
choices = []
|
||||||
|
rear_ports = RearPort.objects.filter(device=device)
|
||||||
|
for rear_port in rear_ports:
|
||||||
|
for i in range(1, rear_port.positions + 1):
|
||||||
|
if (rear_port.pk, i) not in occupied_port_positions:
|
||||||
|
choices.append(
|
||||||
|
('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
|
||||||
|
)
|
||||||
|
self.fields['rear_port'].choices = choices
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
|
# Check that the number of FrontPorts to be created matches the selected number of RearPort positions
|
||||||
|
frontport_count = len(self.cleaned_data['name'])
|
||||||
|
rearport_count = len(self.cleaned_data['rear_port'])
|
||||||
|
if frontport_count != rearport_count:
|
||||||
|
raise forms.ValidationError({
|
||||||
|
'rear_port': _(
|
||||||
|
"The number of front ports to be created ({frontport_count}) must match the selected number of "
|
||||||
|
"rear port positions ({rearport_count})."
|
||||||
|
).format(
|
||||||
|
frontport_count=frontport_count,
|
||||||
|
rearport_count=rearport_count
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
def get_iterative_data(self, iteration):
|
def get_iterative_data(self, iteration):
|
||||||
positions = self.cleaned_data['positions']
|
|
||||||
offset = positions * iteration
|
# Assign rear port and position from selected set
|
||||||
|
rear_port, position = self.cleaned_data['rear_port'][iteration].split(':')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'rear_ports': self.cleaned_data['rear_ports'][offset:offset + positions]
|
'rear_port': int(rear_port),
|
||||||
|
'rear_port_position': int(position),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -272,7 +374,7 @@ class InventoryItemCreateForm(ComponentCreateForm, model_forms.InventoryItemForm
|
|||||||
# Virtual chassis
|
# Virtual chassis
|
||||||
#
|
#
|
||||||
|
|
||||||
class VirtualChassisCreateForm(OwnerMixin, NetBoxModelForm):
|
class VirtualChassisCreateForm(NetBoxModelForm):
|
||||||
region = DynamicModelChoiceField(
|
region = DynamicModelChoiceField(
|
||||||
label=_('Region'),
|
label=_('Region'),
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ __all__ = (
|
|||||||
'InterfaceTemplateImportForm',
|
'InterfaceTemplateImportForm',
|
||||||
'InventoryItemTemplateImportForm',
|
'InventoryItemTemplateImportForm',
|
||||||
'ModuleBayTemplateImportForm',
|
'ModuleBayTemplateImportForm',
|
||||||
'PortTemplateMappingImportForm',
|
|
||||||
'PowerOutletTemplateImportForm',
|
'PowerOutletTemplateImportForm',
|
||||||
'PowerPortTemplateImportForm',
|
'PowerPortTemplateImportForm',
|
||||||
'RearPortTemplateImportForm',
|
'RearPortTemplateImportForm',
|
||||||
@@ -114,11 +113,31 @@ class FrontPortTemplateImportForm(forms.ModelForm):
|
|||||||
label=_('Type'),
|
label=_('Type'),
|
||||||
choices=PortTypeChoices.CHOICES
|
choices=PortTypeChoices.CHOICES
|
||||||
)
|
)
|
||||||
|
rear_port = forms.ModelChoiceField(
|
||||||
|
label=_('Rear port'),
|
||||||
|
queryset=RearPortTemplate.objects.all(),
|
||||||
|
to_field_name='name'
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean_device_type(self):
|
||||||
|
if device_type := self.cleaned_data['device_type']:
|
||||||
|
rear_port = self.fields['rear_port']
|
||||||
|
rear_port.queryset = rear_port.queryset.filter(device_type=device_type)
|
||||||
|
|
||||||
|
return device_type
|
||||||
|
|
||||||
|
def clean_module_type(self):
|
||||||
|
if module_type := self.cleaned_data['module_type']:
|
||||||
|
rear_port = self.fields['rear_port']
|
||||||
|
rear_port.queryset = rear_port.queryset.filter(module_type=module_type)
|
||||||
|
|
||||||
|
return module_type
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = FrontPortTemplate
|
model = FrontPortTemplate
|
||||||
fields = [
|
fields = [
|
||||||
'device_type', 'module_type', 'name', 'type', 'color', 'positions', 'label', 'description',
|
'device_type', 'module_type', 'name', 'type', 'color', 'rear_port', 'rear_port_position', 'label',
|
||||||
|
'description',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -135,25 +154,6 @@ class RearPortTemplateImportForm(forms.ModelForm):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class PortTemplateMappingImportForm(forms.ModelForm):
|
|
||||||
front_port = forms.ModelChoiceField(
|
|
||||||
label=_('Front port'),
|
|
||||||
queryset=FrontPortTemplate.objects.all(),
|
|
||||||
to_field_name='name',
|
|
||||||
)
|
|
||||||
rear_port = forms.ModelChoiceField(
|
|
||||||
label=_('Rear port'),
|
|
||||||
queryset=RearPortTemplate.objects.all(),
|
|
||||||
to_field_name='name',
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = PortTemplateMapping
|
|
||||||
fields = [
|
|
||||||
'front_port', 'front_port_position', 'rear_port', 'rear_port_position',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class ModuleBayTemplateImportForm(forms.ModelForm):
|
class ModuleBayTemplateImportForm(forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ if TYPE_CHECKING:
|
|||||||
from netbox.graphql.filter_lookups import IntegerLookup
|
from netbox.graphql.filter_lookups import IntegerLookup
|
||||||
from extras.graphql.filters import ConfigTemplateFilter
|
from extras.graphql.filters import ConfigTemplateFilter
|
||||||
from ipam.graphql.filters import VLANFilter, VLANTranslationPolicyFilter
|
from ipam.graphql.filters import VLANFilter, VLANTranslationPolicyFilter
|
||||||
from dcim.graphql.filters import LocationFilter, RegionFilter, SiteFilter, SiteGroupFilter
|
|
||||||
from .filters import *
|
from .filters import *
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@@ -36,32 +35,9 @@ class ScopedFilterMixin:
|
|||||||
)
|
)
|
||||||
scope_id: ID | None = strawberry_django.filter_field()
|
scope_id: ID | None = strawberry_django.filter_field()
|
||||||
|
|
||||||
# Cached relations
|
|
||||||
_location: Annotated['LocationFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
|
||||||
strawberry_django.filter_field(name='location')
|
|
||||||
)
|
|
||||||
_region: Annotated['RegionFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
|
||||||
strawberry_django.filter_field(name='region')
|
|
||||||
)
|
|
||||||
_site_group: Annotated['SiteGroupFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
|
||||||
strawberry_django.filter_field(name='site_group')
|
|
||||||
)
|
|
||||||
_site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
|
||||||
strawberry_django.filter_field(name='site')
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ComponentModelFilterMixin:
|
class ComponentModelFilterMixin:
|
||||||
_site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
|
||||||
strawberry_django.filter_field(name='site')
|
|
||||||
)
|
|
||||||
_location: Annotated['LocationFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
|
||||||
strawberry_django.filter_field(name='location')
|
|
||||||
)
|
|
||||||
_rack: Annotated['RackFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
|
||||||
strawberry_django.filter_field(name='rack')
|
|
||||||
)
|
|
||||||
device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||||
device_id: ID | None = strawberry_django.filter_field()
|
device_id: ID | None = strawberry_django.filter_field()
|
||||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||||
|
|||||||
@@ -16,8 +16,7 @@ from dcim.graphql.filter_mixins import (
|
|||||||
from extras.graphql.filter_mixins import ConfigContextFilterMixin
|
from extras.graphql.filter_mixins import ConfigContextFilterMixin
|
||||||
from netbox.graphql.filter_mixins import ImageAttachmentFilterMixin, WeightFilterMixin
|
from netbox.graphql.filter_mixins import ImageAttachmentFilterMixin, WeightFilterMixin
|
||||||
from netbox.graphql.filters import (
|
from netbox.graphql.filters import (
|
||||||
BaseModelFilter, ChangeLoggedModelFilter, NestedGroupModelFilter, OrganizationalModelFilter, PrimaryModelFilter,
|
ChangeLoggedModelFilter, NestedGroupModelFilter, OrganizationalModelFilter, PrimaryModelFilter, NetBoxModelFilter,
|
||||||
NetBoxModelFilter,
|
|
||||||
)
|
)
|
||||||
from tenancy.graphql.filter_mixins import ContactFilterMixin, TenancyFilterMixin
|
from tenancy.graphql.filter_mixins import ContactFilterMixin, TenancyFilterMixin
|
||||||
from virtualization.models import VMInterface
|
from virtualization.models import VMInterface
|
||||||
@@ -71,8 +70,6 @@ __all__ = (
|
|||||||
'ModuleTypeFilter',
|
'ModuleTypeFilter',
|
||||||
'ModuleTypeProfileFilter',
|
'ModuleTypeProfileFilter',
|
||||||
'PlatformFilter',
|
'PlatformFilter',
|
||||||
'PortMappingFilter',
|
|
||||||
'PortTemplateMappingFilter',
|
|
||||||
'PowerFeedFilter',
|
'PowerFeedFilter',
|
||||||
'PowerOutletFilter',
|
'PowerOutletFilter',
|
||||||
'PowerOutletTemplateFilter',
|
'PowerOutletTemplateFilter',
|
||||||
@@ -407,6 +404,13 @@ class FrontPortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixin,
|
|||||||
color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
|
color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
|
||||||
strawberry_django.filter_field()
|
strawberry_django.filter_field()
|
||||||
)
|
)
|
||||||
|
rear_port: Annotated['RearPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||||
|
strawberry_django.filter_field()
|
||||||
|
)
|
||||||
|
rear_port_id: ID | None = strawberry_django.filter_field()
|
||||||
|
rear_port_position: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||||
|
strawberry_django.filter_field()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.filter_type(models.FrontPortTemplate, lookups=True)
|
@strawberry_django.filter_type(models.FrontPortTemplate, lookups=True)
|
||||||
@@ -417,37 +421,13 @@ class FrontPortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedM
|
|||||||
color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
|
color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
|
||||||
strawberry_django.filter_field()
|
strawberry_django.filter_field()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.filter_type(models.PortMapping, lookups=True)
|
|
||||||
class PortMappingFilter(BaseModelFilter):
|
|
||||||
device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
|
||||||
front_port: Annotated['FrontPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
|
||||||
strawberry_django.filter_field()
|
|
||||||
)
|
|
||||||
rear_port: Annotated['RearPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
|
||||||
strawberry_django.filter_field()
|
|
||||||
)
|
|
||||||
front_port_position: FilterLookup[int] | None = strawberry_django.filter_field()
|
|
||||||
rear_port_position: FilterLookup[int] | None = strawberry_django.filter_field()
|
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.filter_type(models.PortTemplateMapping, lookups=True)
|
|
||||||
class PortTemplateMappingFilter(BaseModelFilter):
|
|
||||||
device_type: Annotated['DeviceTypeFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
|
||||||
strawberry_django.filter_field()
|
|
||||||
)
|
|
||||||
module_type: Annotated['ModuleTypeFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
|
||||||
strawberry_django.filter_field()
|
|
||||||
)
|
|
||||||
front_port: Annotated['FrontPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
|
||||||
strawberry_django.filter_field()
|
|
||||||
)
|
|
||||||
rear_port: Annotated['RearPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
rear_port: Annotated['RearPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||||
strawberry_django.filter_field()
|
strawberry_django.filter_field()
|
||||||
)
|
)
|
||||||
front_port_position: FilterLookup[int] | None = strawberry_django.filter_field()
|
rear_port_id: ID | None = strawberry_django.filter_field()
|
||||||
rear_port_position: FilterLookup[int] | None = strawberry_django.filter_field()
|
rear_port_position: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||||
|
strawberry_django.filter_field()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.filter_type(models.MACAddress, lookups=True)
|
@strawberry_django.filter_type(models.MACAddress, lookups=True)
|
||||||
@@ -550,10 +530,6 @@ class InterfaceFilter(
|
|||||||
strawberry_django.filter_field()
|
strawberry_django.filter_field()
|
||||||
)
|
)
|
||||||
|
|
||||||
@strawberry_django.filter_field
|
|
||||||
def cabled(self, value: bool, prefix: str):
|
|
||||||
return Q(**{f'{prefix}cable__isnull': (not value)})
|
|
||||||
|
|
||||||
@strawberry_django.filter_field
|
@strawberry_django.filter_field
|
||||||
def connected(self, queryset, value: bool, prefix: str):
|
def connected(self, queryset, value: bool, prefix: str):
|
||||||
if value is True:
|
if value is True:
|
||||||
@@ -893,7 +869,7 @@ class PowerPortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedM
|
|||||||
|
|
||||||
|
|
||||||
@strawberry_django.filter_type(models.RackType, lookups=True)
|
@strawberry_django.filter_type(models.RackType, lookups=True)
|
||||||
class RackTypeFilter(ImageAttachmentFilterMixin, RackFilterMixin, WeightFilterMixin, PrimaryModelFilter):
|
class RackTypeFilter(RackFilterMixin, WeightFilterMixin, PrimaryModelFilter):
|
||||||
form_factor: BaseFilterLookup[Annotated['RackFormFactorEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
form_factor: BaseFilterLookup[Annotated['RackFormFactorEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||||
strawberry_django.filter_field()
|
strawberry_django.filter_field()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -385,8 +385,7 @@ class DeviceTypeType(PrimaryObjectType):
|
|||||||
)
|
)
|
||||||
class FrontPortType(ModularComponentType, CabledObjectMixin):
|
class FrontPortType(ModularComponentType, CabledObjectMixin):
|
||||||
color: str
|
color: str
|
||||||
|
rear_port: Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')]
|
||||||
mappings: List[Annotated["PortMappingType", strawberry.lazy('dcim.graphql.types')]]
|
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.type(
|
@strawberry_django.type(
|
||||||
@@ -397,8 +396,7 @@ class FrontPortType(ModularComponentType, CabledObjectMixin):
|
|||||||
)
|
)
|
||||||
class FrontPortTemplateType(ModularComponentTemplateType):
|
class FrontPortTemplateType(ModularComponentTemplateType):
|
||||||
color: str
|
color: str
|
||||||
|
rear_port: Annotated["RearPortTemplateType", strawberry.lazy('dcim.graphql.types')]
|
||||||
mappings: List[Annotated["PortMappingTemplateType", strawberry.lazy('dcim.graphql.types')]]
|
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.type(
|
@strawberry_django.type(
|
||||||
@@ -638,28 +636,6 @@ class PlatformType(NestedGroupObjectType):
|
|||||||
devices: List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]
|
devices: List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.type(
|
|
||||||
models.PortMapping,
|
|
||||||
fields='__all__',
|
|
||||||
filters=PortMappingFilter,
|
|
||||||
pagination=True
|
|
||||||
)
|
|
||||||
class PortMappingType(ModularComponentTemplateType):
|
|
||||||
front_port: Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')]
|
|
||||||
rear_port: Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')]
|
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.type(
|
|
||||||
models.PortTemplateMapping,
|
|
||||||
fields='__all__',
|
|
||||||
filters=PortTemplateMappingFilter,
|
|
||||||
pagination=True
|
|
||||||
)
|
|
||||||
class PortMappingTemplateType(ModularComponentTemplateType):
|
|
||||||
front_port: Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]
|
|
||||||
rear_port: Annotated["RearPortTemplateType", strawberry.lazy('dcim.graphql.types')]
|
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.type(
|
@strawberry_django.type(
|
||||||
models.PowerFeed,
|
models.PowerFeed,
|
||||||
exclude=['_path'],
|
exclude=['_path'],
|
||||||
@@ -734,7 +710,7 @@ class PowerPortTemplateType(ModularComponentTemplateType):
|
|||||||
filters=RackTypeFilter,
|
filters=RackTypeFilter,
|
||||||
pagination=True
|
pagination=True
|
||||||
)
|
)
|
||||||
class RackTypeType(ImageAttachmentsMixin, PrimaryObjectType):
|
class RackTypeType(PrimaryObjectType):
|
||||||
rack_count: BigInt
|
rack_count: BigInt
|
||||||
manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')]
|
manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')]
|
||||||
|
|
||||||
@@ -792,7 +768,7 @@ class RackRoleType(OrganizationalObjectType):
|
|||||||
class RearPortType(ModularComponentType, CabledObjectMixin):
|
class RearPortType(ModularComponentType, CabledObjectMixin):
|
||||||
color: str
|
color: str
|
||||||
|
|
||||||
mappings: List[Annotated["PortMappingType", strawberry.lazy('dcim.graphql.types')]]
|
frontports: List[Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')]]
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.type(
|
@strawberry_django.type(
|
||||||
@@ -804,7 +780,7 @@ class RearPortType(ModularComponentType, CabledObjectMixin):
|
|||||||
class RearPortTemplateType(ModularComponentTemplateType):
|
class RearPortTemplateType(ModularComponentTemplateType):
|
||||||
color: str
|
color: str
|
||||||
|
|
||||||
mappings: List[Annotated["PortMappingTemplateType", strawberry.lazy('dcim.graphql.types')]]
|
frontport_templates: List[Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]]
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.type(
|
@strawberry_django.type(
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import decimal
|
|
||||||
|
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
@@ -19,8 +17,8 @@ class Migration(migrations.Migration):
|
|||||||
max_digits=8,
|
max_digits=8,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[
|
validators=[
|
||||||
django.core.validators.MinValueValidator(decimal.Decimal('-90.0')),
|
django.core.validators.MinValueValidator(-90.0),
|
||||||
django.core.validators.MaxValueValidator(decimal.Decimal('90.0'))
|
django.core.validators.MaxValueValidator(90.0),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -33,8 +31,8 @@ class Migration(migrations.Migration):
|
|||||||
max_digits=9,
|
max_digits=9,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[
|
validators=[
|
||||||
django.core.validators.MinValueValidator(decimal.Decimal('-180.0')),
|
django.core.validators.MinValueValidator(-180.0),
|
||||||
django.core.validators.MaxValueValidator(decimal.Decimal('180.0'))
|
django.core.validators.MaxValueValidator(180.0),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -47,8 +45,8 @@ class Migration(migrations.Migration):
|
|||||||
max_digits=8,
|
max_digits=8,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[
|
validators=[
|
||||||
django.core.validators.MinValueValidator(decimal.Decimal('-90.0')),
|
django.core.validators.MinValueValidator(-90.0),
|
||||||
django.core.validators.MaxValueValidator(decimal.Decimal('90.0'))
|
django.core.validators.MaxValueValidator(90.0),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -61,8 +59,8 @@ class Migration(migrations.Migration):
|
|||||||
max_digits=9,
|
max_digits=9,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[
|
validators=[
|
||||||
django.core.validators.MinValueValidator(decimal.Decimal('-180.0')),
|
django.core.validators.MinValueValidator(-180.0),
|
||||||
django.core.validators.MaxValueValidator(decimal.Decimal('180.0'))
|
django.core.validators.MaxValueValidator(180.0),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import django.contrib.postgres.fields
|
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
@@ -17,40 +16,25 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='cabletermination',
|
model_name='cabletermination',
|
||||||
name='connector',
|
name='position',
|
||||||
field=models.PositiveSmallIntegerField(
|
field=models.PositiveIntegerField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[
|
validators=[
|
||||||
django.core.validators.MinValueValidator(1),
|
django.core.validators.MinValueValidator(1),
|
||||||
django.core.validators.MaxValueValidator(256)
|
django.core.validators.MaxValueValidator(1024),
|
||||||
]
|
],
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='cabletermination',
|
|
||||||
name='positions',
|
|
||||||
field=django.contrib.postgres.fields.ArrayField(
|
|
||||||
base_field=models.PositiveSmallIntegerField(
|
|
||||||
validators=[
|
|
||||||
django.core.validators.MinValueValidator(1),
|
|
||||||
django.core.validators.MaxValueValidator(1024)
|
|
||||||
]
|
|
||||||
),
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
size=None
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='cabletermination',
|
name='cabletermination',
|
||||||
options={'ordering': ('cable', 'cable_end', 'connector', 'pk')}, # connector may be null
|
options={'ordering': ('cable', 'cable_end', 'position', 'pk')},
|
||||||
),
|
),
|
||||||
migrations.AddConstraint(
|
migrations.AddConstraint(
|
||||||
model_name='cabletermination',
|
model_name='cabletermination',
|
||||||
constraint=models.UniqueConstraint(
|
constraint=models.UniqueConstraint(
|
||||||
fields=('cable', 'cable_end', 'connector'),
|
fields=('cable', 'cable_end', 'position'),
|
||||||
name='dcim_cabletermination_unique_connector'
|
name='dcim_cabletermination_unique_position'
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,228 +0,0 @@
|
|||||||
import django.contrib.postgres.fields
|
|
||||||
import django.core.validators
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
('dcim', '0220_cable_profile'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='consoleport',
|
|
||||||
name='cable_connector',
|
|
||||||
field=models.PositiveSmallIntegerField(
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
validators=[
|
|
||||||
django.core.validators.MinValueValidator(1),
|
|
||||||
django.core.validators.MaxValueValidator(256)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='consoleport',
|
|
||||||
name='cable_positions',
|
|
||||||
field=django.contrib.postgres.fields.ArrayField(
|
|
||||||
base_field=models.PositiveSmallIntegerField(
|
|
||||||
validators=[
|
|
||||||
django.core.validators.MinValueValidator(1),
|
|
||||||
django.core.validators.MaxValueValidator(1024),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
size=None,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='consoleserverport',
|
|
||||||
name='cable_connector',
|
|
||||||
field=models.PositiveSmallIntegerField(
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
validators=[
|
|
||||||
django.core.validators.MinValueValidator(1),
|
|
||||||
django.core.validators.MaxValueValidator(256)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='consoleserverport',
|
|
||||||
name='cable_positions',
|
|
||||||
field=django.contrib.postgres.fields.ArrayField(
|
|
||||||
base_field=models.PositiveSmallIntegerField(
|
|
||||||
validators=[
|
|
||||||
django.core.validators.MinValueValidator(1),
|
|
||||||
django.core.validators.MaxValueValidator(1024),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
size=None,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='frontport',
|
|
||||||
name='cable_connector',
|
|
||||||
field=models.PositiveSmallIntegerField(
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
validators=[
|
|
||||||
django.core.validators.MinValueValidator(1),
|
|
||||||
django.core.validators.MaxValueValidator(256)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='frontport',
|
|
||||||
name='cable_positions',
|
|
||||||
field=django.contrib.postgres.fields.ArrayField(
|
|
||||||
base_field=models.PositiveSmallIntegerField(
|
|
||||||
validators=[
|
|
||||||
django.core.validators.MinValueValidator(1),
|
|
||||||
django.core.validators.MaxValueValidator(1024),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
size=None,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='interface',
|
|
||||||
name='cable_connector',
|
|
||||||
field=models.PositiveSmallIntegerField(
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
validators=[
|
|
||||||
django.core.validators.MinValueValidator(1),
|
|
||||||
django.core.validators.MaxValueValidator(256)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='interface',
|
|
||||||
name='cable_positions',
|
|
||||||
field=django.contrib.postgres.fields.ArrayField(
|
|
||||||
base_field=models.PositiveSmallIntegerField(
|
|
||||||
validators=[
|
|
||||||
django.core.validators.MinValueValidator(1),
|
|
||||||
django.core.validators.MaxValueValidator(1024),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
size=None,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='powerfeed',
|
|
||||||
name='cable_connector',
|
|
||||||
field=models.PositiveSmallIntegerField(
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
validators=[
|
|
||||||
django.core.validators.MinValueValidator(1),
|
|
||||||
django.core.validators.MaxValueValidator(256)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='powerfeed',
|
|
||||||
name='cable_positions',
|
|
||||||
field=django.contrib.postgres.fields.ArrayField(
|
|
||||||
base_field=models.PositiveSmallIntegerField(
|
|
||||||
validators=[
|
|
||||||
django.core.validators.MinValueValidator(1),
|
|
||||||
django.core.validators.MaxValueValidator(1024),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
size=None,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='poweroutlet',
|
|
||||||
name='cable_connector',
|
|
||||||
field=models.PositiveSmallIntegerField(
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
validators=[
|
|
||||||
django.core.validators.MinValueValidator(1),
|
|
||||||
django.core.validators.MaxValueValidator(256)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='poweroutlet',
|
|
||||||
name='cable_positions',
|
|
||||||
field=django.contrib.postgres.fields.ArrayField(
|
|
||||||
base_field=models.PositiveSmallIntegerField(
|
|
||||||
validators=[
|
|
||||||
django.core.validators.MinValueValidator(1),
|
|
||||||
django.core.validators.MaxValueValidator(1024),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
size=None,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='powerport',
|
|
||||||
name='cable_connector',
|
|
||||||
field=models.PositiveSmallIntegerField(
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
validators=[
|
|
||||||
django.core.validators.MinValueValidator(1),
|
|
||||||
django.core.validators.MaxValueValidator(256)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='powerport',
|
|
||||||
name='cable_positions',
|
|
||||||
field=django.contrib.postgres.fields.ArrayField(
|
|
||||||
base_field=models.PositiveSmallIntegerField(
|
|
||||||
validators=[
|
|
||||||
django.core.validators.MinValueValidator(1),
|
|
||||||
django.core.validators.MaxValueValidator(1024),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
size=None,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='rearport',
|
|
||||||
name='cable_connector',
|
|
||||||
field=models.PositiveSmallIntegerField(
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
validators=[
|
|
||||||
django.core.validators.MinValueValidator(1),
|
|
||||||
django.core.validators.MaxValueValidator(256)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='rearport',
|
|
||||||
name='cable_positions',
|
|
||||||
field=django.contrib.postgres.fields.ArrayField(
|
|
||||||
base_field=models.PositiveSmallIntegerField(
|
|
||||||
validators=[
|
|
||||||
django.core.validators.MinValueValidator(1),
|
|
||||||
django.core.validators.MaxValueValidator(1024),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
size=None,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0220_cable_profile'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='consoleport',
|
||||||
|
name='cable_position',
|
||||||
|
field=models.PositiveIntegerField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.MinValueValidator(1),
|
||||||
|
django.core.validators.MaxValueValidator(1024),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='consoleserverport',
|
||||||
|
name='cable_position',
|
||||||
|
field=models.PositiveIntegerField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.MinValueValidator(1),
|
||||||
|
django.core.validators.MaxValueValidator(1024),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='frontport',
|
||||||
|
name='cable_position',
|
||||||
|
field=models.PositiveIntegerField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.MinValueValidator(1),
|
||||||
|
django.core.validators.MaxValueValidator(1024),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='interface',
|
||||||
|
name='cable_position',
|
||||||
|
field=models.PositiveIntegerField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.MinValueValidator(1),
|
||||||
|
django.core.validators.MaxValueValidator(1024),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='powerfeed',
|
||||||
|
name='cable_position',
|
||||||
|
field=models.PositiveIntegerField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.MinValueValidator(1),
|
||||||
|
django.core.validators.MaxValueValidator(1024),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='poweroutlet',
|
||||||
|
name='cable_position',
|
||||||
|
field=models.PositiveIntegerField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.MinValueValidator(1),
|
||||||
|
django.core.validators.MaxValueValidator(1024),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='powerport',
|
||||||
|
name='cable_position',
|
||||||
|
field=models.PositiveIntegerField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.MinValueValidator(1),
|
||||||
|
django.core.validators.MaxValueValidator(1024),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='rearport',
|
||||||
|
name='cable_position',
|
||||||
|
field=models.PositiveIntegerField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.MinValueValidator(1),
|
||||||
|
django.core.validators.MaxValueValidator(1024),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,219 +0,0 @@
|
|||||||
import django.core.validators
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations
|
|
||||||
from django.db import models
|
|
||||||
from itertools import islice
|
|
||||||
|
|
||||||
|
|
||||||
def chunked(iterable, size):
|
|
||||||
"""
|
|
||||||
Yield successive chunks of a given size from an iterator.
|
|
||||||
"""
|
|
||||||
iterator = iter(iterable)
|
|
||||||
while chunk := list(islice(iterator, size)):
|
|
||||||
yield chunk
|
|
||||||
|
|
||||||
|
|
||||||
def populate_port_template_mappings(apps, schema_editor):
|
|
||||||
FrontPortTemplate = apps.get_model('dcim', 'FrontPortTemplate')
|
|
||||||
PortTemplateMapping = apps.get_model('dcim', 'PortTemplateMapping')
|
|
||||||
|
|
||||||
front_ports = FrontPortTemplate.objects.iterator(chunk_size=1000)
|
|
||||||
|
|
||||||
def generate_copies():
|
|
||||||
for front_port in front_ports:
|
|
||||||
yield PortTemplateMapping(
|
|
||||||
device_type_id=front_port.device_type_id,
|
|
||||||
module_type_id=front_port.module_type_id,
|
|
||||||
front_port_id=front_port.pk,
|
|
||||||
front_port_position=1,
|
|
||||||
rear_port_id=front_port.rear_port_id,
|
|
||||||
rear_port_position=front_port.rear_port_position,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Bulk insert in streaming batches
|
|
||||||
for chunk in chunked(generate_copies(), 1000):
|
|
||||||
PortTemplateMapping.objects.bulk_create(chunk, batch_size=1000)
|
|
||||||
|
|
||||||
|
|
||||||
def populate_port_mappings(apps, schema_editor):
|
|
||||||
FrontPort = apps.get_model('dcim', 'FrontPort')
|
|
||||||
PortMapping = apps.get_model('dcim', 'PortMapping')
|
|
||||||
|
|
||||||
front_ports = FrontPort.objects.iterator(chunk_size=1000)
|
|
||||||
|
|
||||||
def generate_copies():
|
|
||||||
for front_port in front_ports:
|
|
||||||
yield PortMapping(
|
|
||||||
device_id=front_port.device_id,
|
|
||||||
front_port_id=front_port.pk,
|
|
||||||
front_port_position=1,
|
|
||||||
rear_port_id=front_port.rear_port_id,
|
|
||||||
rear_port_position=front_port.rear_port_position,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Bulk insert in streaming batches
|
|
||||||
for chunk in chunked(generate_copies(), 1000):
|
|
||||||
PortMapping.objects.bulk_create(chunk, batch_size=1000)
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
('dcim', '0221_cable_connector_positions'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
# Create PortTemplateMapping model (for DeviceTypes)
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='PortTemplateMapping',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
|
||||||
(
|
|
||||||
'front_port_position',
|
|
||||||
models.PositiveSmallIntegerField(
|
|
||||||
default=1,
|
|
||||||
validators=[
|
|
||||||
django.core.validators.MinValueValidator(1),
|
|
||||||
django.core.validators.MaxValueValidator(1024)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'rear_port_position',
|
|
||||||
models.PositiveSmallIntegerField(
|
|
||||||
default=1,
|
|
||||||
validators=[
|
|
||||||
django.core.validators.MinValueValidator(1),
|
|
||||||
django.core.validators.MaxValueValidator(1024)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'device_type',
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to='dcim.devicetype',
|
|
||||||
related_name='port_mappings',
|
|
||||||
blank=True,
|
|
||||||
null=True
|
|
||||||
)
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'module_type',
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to='dcim.moduletype',
|
|
||||||
related_name='port_mappings',
|
|
||||||
blank=True,
|
|
||||||
null=True
|
|
||||||
)
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'front_port',
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to='dcim.frontporttemplate',
|
|
||||||
related_name='mappings'
|
|
||||||
)
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'rear_port',
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to='dcim.rearporttemplate',
|
|
||||||
related_name='mappings'
|
|
||||||
)
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.AddConstraint(
|
|
||||||
model_name='porttemplatemapping',
|
|
||||||
constraint=models.UniqueConstraint(
|
|
||||||
fields=('front_port', 'front_port_position'),
|
|
||||||
name='dcim_porttemplatemapping_unique_front_port_position'
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddConstraint(
|
|
||||||
model_name='porttemplatemapping',
|
|
||||||
constraint=models.UniqueConstraint(
|
|
||||||
fields=('rear_port', 'rear_port_position'),
|
|
||||||
name='dcim_porttemplatemapping_unique_rear_port_position'
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
# Create PortMapping model (for Devices)
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='PortMapping',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
|
||||||
(
|
|
||||||
'front_port_position',
|
|
||||||
models.PositiveSmallIntegerField(
|
|
||||||
default=1,
|
|
||||||
validators=[
|
|
||||||
django.core.validators.MinValueValidator(1),
|
|
||||||
django.core.validators.MaxValueValidator(1024)
|
|
||||||
]
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'rear_port_position',
|
|
||||||
models.PositiveSmallIntegerField(
|
|
||||||
default=1,
|
|
||||||
validators=[
|
|
||||||
django.core.validators.MinValueValidator(1),
|
|
||||||
django.core.validators.MaxValueValidator(1024),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'device',
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to='dcim.device',
|
|
||||||
related_name='port_mappings'
|
|
||||||
)
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'front_port',
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to='dcim.frontport',
|
|
||||||
related_name='mappings'
|
|
||||||
)
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'rear_port',
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to='dcim.rearport',
|
|
||||||
related_name='mappings'
|
|
||||||
)
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.AddConstraint(
|
|
||||||
model_name='portmapping',
|
|
||||||
constraint=models.UniqueConstraint(
|
|
||||||
fields=('front_port', 'front_port_position'),
|
|
||||||
name='dcim_portmapping_unique_front_port_position'
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddConstraint(
|
|
||||||
model_name='portmapping',
|
|
||||||
constraint=models.UniqueConstraint(
|
|
||||||
fields=('rear_port', 'rear_port_position'),
|
|
||||||
name='dcim_portmapping_unique_rear_port_position'
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
# Data migration
|
|
||||||
migrations.RunPython(
|
|
||||||
code=populate_port_template_mappings,
|
|
||||||
reverse_code=migrations.RunPython.noop
|
|
||||||
),
|
|
||||||
migrations.RunPython(
|
|
||||||
code=populate_port_mappings,
|
|
||||||
reverse_code=migrations.RunPython.noop
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import django.core.validators
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('dcim', '0222_port_mappings'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
# Remove rear_port & rear_port_position from FrontPortTemplate
|
|
||||||
migrations.RemoveConstraint(
|
|
||||||
model_name='frontporttemplate',
|
|
||||||
name='dcim_frontporttemplate_unique_rear_port_position',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='frontporttemplate',
|
|
||||||
name='rear_port',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='frontporttemplate',
|
|
||||||
name='rear_port_position',
|
|
||||||
),
|
|
||||||
|
|
||||||
# Add positions on FrontPortTemplate
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='frontporttemplate',
|
|
||||||
name='positions',
|
|
||||||
field=models.PositiveSmallIntegerField(
|
|
||||||
default=1,
|
|
||||||
validators=[
|
|
||||||
django.core.validators.MinValueValidator(1),
|
|
||||||
django.core.validators.MaxValueValidator(1024)
|
|
||||||
]
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
# Remove rear_port & rear_port_position from FrontPort
|
|
||||||
migrations.RemoveConstraint(
|
|
||||||
model_name='frontport',
|
|
||||||
name='dcim_frontport_unique_rear_port_position',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='frontport',
|
|
||||||
name='rear_port',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='frontport',
|
|
||||||
name='rear_port_position',
|
|
||||||
),
|
|
||||||
|
|
||||||
# Add positions on FrontPort
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='frontport',
|
|
||||||
name='positions',
|
|
||||||
field=models.PositiveSmallIntegerField(
|
|
||||||
default=1,
|
|
||||||
validators=[
|
|
||||||
django.core.validators.MinValueValidator(1),
|
|
||||||
django.core.validators.MaxValueValidator(1024)
|
|
||||||
]
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# Generated by Django 5.2.8 on 2025-12-08 17:38
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('dcim', '0223_frontport_positions'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='inventoryitemrole',
|
|
||||||
name='comments',
|
|
||||||
field=models.TextField(blank=True),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='manufacturer',
|
|
||||||
name='comments',
|
|
||||||
field=models.TextField(blank=True),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='rackrole',
|
|
||||||
name='comments',
|
|
||||||
field=models.TextField(blank=True),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
('contenttypes', '0002_remove_content_type_name'),
|
|
||||||
('dcim', '0224_add_comments_to_organizationalmodel'),
|
|
||||||
('extras', '0134_owner'),
|
|
||||||
('users', '0015_owner'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name='macaddress',
|
|
||||||
index=models.Index(
|
|
||||||
fields=['assigned_object_type', 'assigned_object_id'], name='dcim_macadd_assigne_54115d_idx'
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
|
||||||
from django.db import models
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from dcim.constants import PORT_POSITION_MAX, PORT_POSITION_MIN
|
|
||||||
|
|
||||||
__all__ = (
|
|
||||||
'PortMappingBase',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PortMappingBase(models.Model):
|
|
||||||
"""
|
|
||||||
Base class for PortMapping and PortTemplateMapping
|
|
||||||
"""
|
|
||||||
front_port_position = models.PositiveSmallIntegerField(
|
|
||||||
default=1,
|
|
||||||
validators=(
|
|
||||||
MinValueValidator(PORT_POSITION_MIN),
|
|
||||||
MaxValueValidator(PORT_POSITION_MAX),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
rear_port_position = models.PositiveSmallIntegerField(
|
|
||||||
default=1,
|
|
||||||
validators=(
|
|
||||||
MinValueValidator(PORT_POSITION_MIN),
|
|
||||||
MaxValueValidator(PORT_POSITION_MAX),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
_netbox_private = True
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
abstract = True
|
|
||||||
constraints = (
|
|
||||||
models.UniqueConstraint(
|
|
||||||
fields=('front_port', 'front_port_position'),
|
|
||||||
name='%(app_label)s_%(class)s_unique_front_port_position'
|
|
||||||
),
|
|
||||||
models.UniqueConstraint(
|
|
||||||
fields=('rear_port', 'rear_port_position'),
|
|
||||||
name='%(app_label)s_%(class)s_unique_rear_port_position'
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
super().clean()
|
|
||||||
|
|
||||||
# Validate rear port position
|
|
||||||
if self.rear_port_position > self.rear_port.positions:
|
|
||||||
raise ValidationError({
|
|
||||||
"rear_port_position": _(
|
|
||||||
"Invalid rear port position ({rear_port_position}): Rear port {name} has only {positions} "
|
|
||||||
"positions."
|
|
||||||
).format(
|
|
||||||
rear_port_position=self.rear_port_position,
|
|
||||||
name=self.rear_port.name,
|
|
||||||
positions=self.rear_port.positions
|
|
||||||
)
|
|
||||||
})
|
|
||||||
+81
-155
@@ -1,9 +1,7 @@
|
|||||||
import itertools
|
import itertools
|
||||||
import logging
|
|
||||||
|
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.contrib.postgres.fields import ArrayField
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@@ -24,7 +22,7 @@ from utilities.fields import ColorField, GenericArrayForeignKey
|
|||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
from utilities.serialization import deserialize_object, serialize_object
|
from utilities.serialization import deserialize_object, serialize_object
|
||||||
from wireless.models import WirelessLink
|
from wireless.models import WirelessLink
|
||||||
from .device_components import FrontPort, PathEndpoint, PortMapping, RearPort
|
from .device_components import FrontPort, PathEndpoint, RearPort
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'Cable',
|
'Cable',
|
||||||
@@ -32,8 +30,6 @@ __all__ = (
|
|||||||
'CableTermination',
|
'CableTermination',
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(f'netbox.{__name__}')
|
|
||||||
|
|
||||||
trace_paths = Signal()
|
trace_paths = Signal()
|
||||||
|
|
||||||
|
|
||||||
@@ -115,9 +111,8 @@ class Cable(PrimaryModel):
|
|||||||
# A copy of the PK to be used by __str__ in case the object is deleted
|
# A copy of the PK to be used by __str__ in case the object is deleted
|
||||||
self._pk = self.__dict__.get('id')
|
self._pk = self.__dict__.get('id')
|
||||||
|
|
||||||
# Cache the original profile & status so we can check later whether either has been changed
|
# Cache the original status so we can check later if it's been changed
|
||||||
self._orig_status = self.__dict__.get('status')
|
self._orig_status = self.__dict__.get('status')
|
||||||
self._orig_profile = self.__dict__.get('profile')
|
|
||||||
|
|
||||||
self._terminations_modified = False
|
self._terminations_modified = False
|
||||||
|
|
||||||
@@ -138,30 +133,10 @@ class Cable(PrimaryModel):
|
|||||||
def profile_class(self):
|
def profile_class(self):
|
||||||
from dcim import cable_profiles
|
from dcim import cable_profiles
|
||||||
return {
|
return {
|
||||||
CableProfileChoices.SINGLE_1C1P: cable_profiles.Single1C1PCableProfile,
|
CableProfileChoices.STRAIGHT_SINGLE: cable_profiles.StraightSingleCableProfile,
|
||||||
CableProfileChoices.SINGLE_1C2P: cable_profiles.Single1C2PCableProfile,
|
CableProfileChoices.STRAIGHT_MULTI: cable_profiles.StraightMultiCableProfile,
|
||||||
CableProfileChoices.SINGLE_1C4P: cable_profiles.Single1C4PCableProfile,
|
CableProfileChoices.SHUFFLE_2X2_MPO8: cable_profiles.Shuffle2x2MPO8CableProfile,
|
||||||
CableProfileChoices.SINGLE_1C6P: cable_profiles.Single1C6PCableProfile,
|
CableProfileChoices.SHUFFLE_4X4_MPO8: cable_profiles.Shuffle4x4MPO8CableProfile,
|
||||||
CableProfileChoices.SINGLE_1C8P: cable_profiles.Single1C8PCableProfile,
|
|
||||||
CableProfileChoices.SINGLE_1C12P: cable_profiles.Single1C12PCableProfile,
|
|
||||||
CableProfileChoices.SINGLE_1C16P: cable_profiles.Single1C16PCableProfile,
|
|
||||||
CableProfileChoices.TRUNK_2C1P: cable_profiles.Trunk2C1PCableProfile,
|
|
||||||
CableProfileChoices.TRUNK_2C2P: cable_profiles.Trunk2C2PCableProfile,
|
|
||||||
CableProfileChoices.TRUNK_2C4P: cable_profiles.Trunk2C4PCableProfile,
|
|
||||||
CableProfileChoices.TRUNK_2C4P_SHUFFLE: cable_profiles.Trunk2C4PShuffleCableProfile,
|
|
||||||
CableProfileChoices.TRUNK_2C6P: cable_profiles.Trunk2C6PCableProfile,
|
|
||||||
CableProfileChoices.TRUNK_2C8P: cable_profiles.Trunk2C8PCableProfile,
|
|
||||||
CableProfileChoices.TRUNK_2C12P: cable_profiles.Trunk2C12PCableProfile,
|
|
||||||
CableProfileChoices.TRUNK_4C1P: cable_profiles.Trunk4C1PCableProfile,
|
|
||||||
CableProfileChoices.TRUNK_4C2P: cable_profiles.Trunk4C2PCableProfile,
|
|
||||||
CableProfileChoices.TRUNK_4C4P: cable_profiles.Trunk4C4PCableProfile,
|
|
||||||
CableProfileChoices.TRUNK_4C4P_SHUFFLE: cable_profiles.Trunk4C4PShuffleCableProfile,
|
|
||||||
CableProfileChoices.TRUNK_4C6P: cable_profiles.Trunk4C6PCableProfile,
|
|
||||||
CableProfileChoices.TRUNK_4C8P: cable_profiles.Trunk4C8PCableProfile,
|
|
||||||
CableProfileChoices.TRUNK_8C4P: cable_profiles.Trunk8C4PCableProfile,
|
|
||||||
CableProfileChoices.BREAKOUT_1C4P_4C1P: cable_profiles.Breakout1C4Px4C1PCableProfile,
|
|
||||||
CableProfileChoices.BREAKOUT_1C6P_6C1P: cable_profiles.Breakout1C6Px6C1PCableProfile,
|
|
||||||
CableProfileChoices.BREAKOUT_2C4P_8C1P_SHUFFLE: cable_profiles.Breakout2C4Px8C1PShuffleCableProfile,
|
|
||||||
}.get(self.profile)
|
}.get(self.profile)
|
||||||
|
|
||||||
def _get_x_terminations(self, side):
|
def _get_x_terminations(self, side):
|
||||||
@@ -291,10 +266,7 @@ class Cable(PrimaryModel):
|
|||||||
# Update the private PK used in __str__()
|
# Update the private PK used in __str__()
|
||||||
self._pk = self.pk
|
self._pk = self.pk
|
||||||
|
|
||||||
if self._orig_profile != self.profile:
|
if self._terminations_modified:
|
||||||
print(f'profile changed from {self._orig_profile} to {self.profile}')
|
|
||||||
self.update_terminations(force=True)
|
|
||||||
elif self._terminations_modified:
|
|
||||||
self.update_terminations()
|
self.update_terminations()
|
||||||
|
|
||||||
super().save(*args, force_update=True, using=using, update_fields=update_fields)
|
super().save(*args, force_update=True, using=using, update_fields=update_fields)
|
||||||
@@ -348,52 +320,29 @@ class Cable(PrimaryModel):
|
|||||||
|
|
||||||
return a_terminations, b_terminations
|
return a_terminations, b_terminations
|
||||||
|
|
||||||
def update_terminations(self, force=False):
|
def update_terminations(self):
|
||||||
"""
|
"""
|
||||||
Create/delete CableTerminations for this Cable to reflect its current state.
|
Create/delete CableTerminations for this Cable to reflect its current state.
|
||||||
|
|
||||||
Args:
|
|
||||||
force: Force the recreation of all CableTerminations, even if no changes have been made. Needed e.g. when
|
|
||||||
altering a Cable's assigned profile.
|
|
||||||
"""
|
"""
|
||||||
a_terminations, b_terminations = self.get_terminations()
|
a_terminations, b_terminations = self.get_terminations()
|
||||||
|
|
||||||
# Delete any stale CableTerminations
|
# Delete any stale CableTerminations
|
||||||
for termination, ct in a_terminations.items():
|
for termination, ct in a_terminations.items():
|
||||||
if force or (termination.pk and termination not in self.a_terminations):
|
if termination.pk and termination not in self.a_terminations:
|
||||||
ct.delete()
|
ct.delete()
|
||||||
for termination, ct in b_terminations.items():
|
for termination, ct in b_terminations.items():
|
||||||
if force or (termination.pk and termination not in self.b_terminations):
|
if termination.pk and termination not in self.b_terminations:
|
||||||
ct.delete()
|
ct.delete()
|
||||||
|
|
||||||
# Save any new CableTerminations
|
# Save any new CableTerminations
|
||||||
profile = self.profile_class() if self.profile else None
|
|
||||||
for i, termination in enumerate(self.a_terminations, start=1):
|
for i, termination in enumerate(self.a_terminations, start=1):
|
||||||
if force or not termination.pk or termination not in a_terminations:
|
if not termination.pk or termination not in a_terminations:
|
||||||
connector = positions = None
|
position = i if self.profile and isinstance(termination, PathEndpoint) else None
|
||||||
if profile:
|
CableTermination(cable=self, cable_end='A', position=position, termination=termination).save()
|
||||||
connector = i
|
|
||||||
positions = profile.get_position_list(profile.a_connectors[i])
|
|
||||||
CableTermination(
|
|
||||||
cable=self,
|
|
||||||
cable_end=CableEndChoices.SIDE_A,
|
|
||||||
connector=connector,
|
|
||||||
positions=positions,
|
|
||||||
termination=termination
|
|
||||||
).save()
|
|
||||||
for i, termination in enumerate(self.b_terminations, start=1):
|
for i, termination in enumerate(self.b_terminations, start=1):
|
||||||
if force or not termination.pk or termination not in b_terminations:
|
if not termination.pk or termination not in b_terminations:
|
||||||
connector = positions = None
|
position = i if self.profile and isinstance(termination, PathEndpoint) else None
|
||||||
if profile:
|
CableTermination(cable=self, cable_end='B', position=position, termination=termination).save()
|
||||||
connector = i
|
|
||||||
positions = profile.get_position_list(profile.b_connectors[i])
|
|
||||||
CableTermination(
|
|
||||||
cable=self,
|
|
||||||
cable_end=CableEndChoices.SIDE_B,
|
|
||||||
connector=connector,
|
|
||||||
positions=positions,
|
|
||||||
termination=termination
|
|
||||||
).save()
|
|
||||||
|
|
||||||
|
|
||||||
class CableTermination(ChangeLoggedModel):
|
class CableTermination(ChangeLoggedModel):
|
||||||
@@ -420,23 +369,13 @@ class CableTermination(ChangeLoggedModel):
|
|||||||
ct_field='termination_type',
|
ct_field='termination_type',
|
||||||
fk_field='termination_id'
|
fk_field='termination_id'
|
||||||
)
|
)
|
||||||
connector = models.PositiveSmallIntegerField(
|
position = models.PositiveIntegerField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
validators=(
|
validators=(
|
||||||
MinValueValidator(CABLE_CONNECTOR_MIN),
|
MinValueValidator(CABLE_POSITION_MIN),
|
||||||
MaxValueValidator(CABLE_CONNECTOR_MAX)
|
MaxValueValidator(CABLE_POSITION_MAX)
|
||||||
),
|
)
|
||||||
)
|
|
||||||
positions = ArrayField(
|
|
||||||
base_field=models.PositiveSmallIntegerField(
|
|
||||||
validators=(
|
|
||||||
MinValueValidator(CABLE_POSITION_MIN),
|
|
||||||
MaxValueValidator(CABLE_POSITION_MAX)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Cached associations to enable efficient filtering
|
# Cached associations to enable efficient filtering
|
||||||
@@ -468,15 +407,15 @@ class CableTermination(ChangeLoggedModel):
|
|||||||
objects = RestrictedQuerySet.as_manager()
|
objects = RestrictedQuerySet.as_manager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('cable', 'cable_end', 'connector', 'pk')
|
ordering = ('cable', 'cable_end', 'position', 'pk')
|
||||||
constraints = (
|
constraints = (
|
||||||
models.UniqueConstraint(
|
models.UniqueConstraint(
|
||||||
fields=('termination_type', 'termination_id'),
|
fields=('termination_type', 'termination_id'),
|
||||||
name='%(app_label)s_%(class)s_unique_termination'
|
name='%(app_label)s_%(class)s_unique_termination'
|
||||||
),
|
),
|
||||||
models.UniqueConstraint(
|
models.UniqueConstraint(
|
||||||
fields=('cable', 'cable_end', 'connector'),
|
fields=('cable', 'cable_end', 'position'),
|
||||||
name='%(app_label)s_%(class)s_unique_connector'
|
name='%(app_label)s_%(class)s_unique_position'
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
verbose_name = _('cable termination')
|
verbose_name = _('cable termination')
|
||||||
@@ -539,7 +478,9 @@ class CableTermination(ChangeLoggedModel):
|
|||||||
# Set the cable on the terminating object
|
# Set the cable on the terminating object
|
||||||
termination = self.termination._meta.model.objects.get(pk=self.termination_id)
|
termination = self.termination._meta.model.objects.get(pk=self.termination_id)
|
||||||
termination.snapshot()
|
termination.snapshot()
|
||||||
termination.set_cable_termination(self)
|
termination.cable = self.cable
|
||||||
|
termination.cable_end = self.cable_end
|
||||||
|
termination.cable_position = self.position
|
||||||
termination.save()
|
termination.save()
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
@@ -547,7 +488,9 @@ class CableTermination(ChangeLoggedModel):
|
|||||||
# Delete the cable association on the terminating object
|
# Delete the cable association on the terminating object
|
||||||
termination = self.termination._meta.model.objects.get(pk=self.termination_id)
|
termination = self.termination._meta.model.objects.get(pk=self.termination_id)
|
||||||
termination.snapshot()
|
termination.snapshot()
|
||||||
termination.clear_cable_termination(self)
|
termination.cable = None
|
||||||
|
termination.cable_end = None
|
||||||
|
termination.cable_position = None
|
||||||
termination.save()
|
termination.save()
|
||||||
|
|
||||||
super().delete(*args, **kwargs)
|
super().delete(*args, **kwargs)
|
||||||
@@ -723,13 +666,7 @@ class CablePath(models.Model):
|
|||||||
is_active = True
|
is_active = True
|
||||||
is_split = False
|
is_split = False
|
||||||
|
|
||||||
logger.debug(f'Tracing cable path from {terminations}...')
|
|
||||||
|
|
||||||
segment = 0
|
|
||||||
while terminations:
|
while terminations:
|
||||||
segment += 1
|
|
||||||
logger.debug(f'[Path segment #{segment}] Position stack: {position_stack}')
|
|
||||||
logger.debug(f'[Path segment #{segment}] Local terminations: {terminations}')
|
|
||||||
|
|
||||||
# Terminations must all be of the same type
|
# Terminations must all be of the same type
|
||||||
if not all(isinstance(t, type(terminations[0])) for t in terminations[1:]):
|
if not all(isinstance(t, type(terminations[0])) for t in terminations[1:]):
|
||||||
@@ -755,15 +692,12 @@ class CablePath(models.Model):
|
|||||||
path.append([
|
path.append([
|
||||||
object_to_path_node(t) for t in terminations
|
object_to_path_node(t) for t in terminations
|
||||||
])
|
])
|
||||||
# If not null, push cable position onto the stack
|
# If not null, push cable_position onto the stack
|
||||||
if isinstance(terminations[0], PathEndpoint) and terminations[0].cable_positions:
|
if terminations[0].cable_position is not None:
|
||||||
position_stack.append([terminations[0].cable_positions[0]])
|
position_stack.append([terminations[0].cable_position])
|
||||||
|
|
||||||
# Step 2: Determine the attached links (Cable or WirelessLink), if any
|
# Step 2: Determine the attached links (Cable or WirelessLink), if any
|
||||||
links = list(dict.fromkeys(
|
links = [termination.link for termination in terminations if termination.link is not None]
|
||||||
termination.link for termination in terminations if termination.link is not None
|
|
||||||
))
|
|
||||||
logger.debug(f'[Path segment #{segment}] Links: {links}')
|
|
||||||
if len(links) == 0:
|
if len(links) == 0:
|
||||||
if len(path) == 1:
|
if len(path) == 1:
|
||||||
# If this is the start of the path and no link exists, return None
|
# If this is the start of the path and no link exists, return None
|
||||||
@@ -798,10 +732,8 @@ class CablePath(models.Model):
|
|||||||
# Profile-based tracing
|
# Profile-based tracing
|
||||||
if links[0].profile:
|
if links[0].profile:
|
||||||
cable_profile = links[0].profile_class()
|
cable_profile = links[0].profile_class()
|
||||||
position = position_stack.pop()[0] if position_stack else None
|
peer_cable_terminations = cable_profile.get_peer_terminations(terminations, position_stack)
|
||||||
term, position = cable_profile.get_peer_termination(terminations[0], position)
|
remote_terminations = [ct.termination for ct in peer_cable_terminations]
|
||||||
remote_terminations = [term]
|
|
||||||
position_stack.append([position])
|
|
||||||
|
|
||||||
# Legacy (positionless) behavior
|
# Legacy (positionless) behavior
|
||||||
else:
|
else:
|
||||||
@@ -828,13 +760,10 @@ class CablePath(models.Model):
|
|||||||
link.interface_b if link.interface_a is terminations[0] else link.interface_a for link in links
|
link.interface_b if link.interface_a is terminations[0] else link.interface_a for link in links
|
||||||
]
|
]
|
||||||
|
|
||||||
logger.debug(f'[Path segment #{segment}] Remote terminations: {remote_terminations}')
|
|
||||||
|
|
||||||
# Remote Terminations must all be of the same type, otherwise return a split path
|
# Remote Terminations must all be of the same type, otherwise return a split path
|
||||||
if not all(isinstance(t, type(remote_terminations[0])) for t in remote_terminations[1:]):
|
if not all(isinstance(t, type(remote_terminations[0])) for t in remote_terminations[1:]):
|
||||||
is_complete = False
|
is_complete = False
|
||||||
is_split = True
|
is_split = True
|
||||||
logger.debug('Remote termination types differ; aborting trace.')
|
|
||||||
break
|
break
|
||||||
|
|
||||||
# Step 7: Record the far-end termination object(s)
|
# Step 7: Record the far-end termination object(s)
|
||||||
@@ -848,53 +777,58 @@ class CablePath(models.Model):
|
|||||||
|
|
||||||
if isinstance(remote_terminations[0], FrontPort):
|
if isinstance(remote_terminations[0], FrontPort):
|
||||||
# Follow FrontPorts to their corresponding RearPorts
|
# Follow FrontPorts to their corresponding RearPorts
|
||||||
if remote_terminations[0].positions > 1 and position_stack:
|
rear_ports = RearPort.objects.filter(
|
||||||
positions = position_stack.pop()
|
pk__in=[t.rear_port_id for t in remote_terminations]
|
||||||
q_filter = Q()
|
)
|
||||||
for rt in remote_terminations:
|
if len(rear_ports) > 1 or rear_ports[0].positions > 1:
|
||||||
q_filter |= Q(front_port=rt, front_port_position__in=positions)
|
position_stack.append([fp.rear_port_position for fp in remote_terminations])
|
||||||
port_mappings = PortMapping.objects.filter(q_filter)
|
|
||||||
elif remote_terminations[0].positions > 1:
|
|
||||||
is_split = True
|
|
||||||
logger.debug(
|
|
||||||
'Encountered front port mapped to multiple rear ports but position stack is empty; aborting '
|
|
||||||
'trace.'
|
|
||||||
)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
port_mappings = PortMapping.objects.filter(front_port__in=remote_terminations)
|
|
||||||
if not port_mappings:
|
|
||||||
break
|
|
||||||
|
|
||||||
# Compile the list of RearPorts without duplication or altering their ordering
|
terminations = rear_ports
|
||||||
terminations = list(dict.fromkeys(mapping.rear_port for mapping in port_mappings))
|
|
||||||
if any(t.positions > 1 for t in terminations):
|
|
||||||
position_stack.append([mapping.rear_port_position for mapping in port_mappings])
|
|
||||||
|
|
||||||
elif isinstance(remote_terminations[0], RearPort):
|
elif isinstance(remote_terminations[0], RearPort):
|
||||||
# Follow RearPorts to their corresponding FrontPorts
|
if len(remote_terminations) == 1 and remote_terminations[0].positions == 1:
|
||||||
if remote_terminations[0].positions > 1 and position_stack:
|
front_ports = FrontPort.objects.filter(
|
||||||
|
rear_port_id__in=[rp.pk for rp in remote_terminations],
|
||||||
|
rear_port_position=1
|
||||||
|
)
|
||||||
|
# Obtain the individual front ports based on the termination and all positions
|
||||||
|
elif len(remote_terminations) > 1 and position_stack:
|
||||||
positions = position_stack.pop()
|
positions = position_stack.pop()
|
||||||
|
|
||||||
|
# Ensure we have a number of positions equal to the amount of remote terminations
|
||||||
|
if len(remote_terminations) != len(positions):
|
||||||
|
raise UnsupportedCablePath(
|
||||||
|
_("All positions counts within the path on opposite ends of links must match")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get our front ports
|
||||||
q_filter = Q()
|
q_filter = Q()
|
||||||
for rt in remote_terminations:
|
for rt in remote_terminations:
|
||||||
q_filter |= Q(rear_port=rt, rear_port_position__in=positions)
|
position = positions.pop()
|
||||||
port_mappings = PortMapping.objects.filter(q_filter)
|
q_filter |= Q(rear_port_id=rt.pk, rear_port_position=position)
|
||||||
elif remote_terminations[0].positions > 1:
|
if q_filter is Q():
|
||||||
is_split = True
|
raise UnsupportedCablePath(_("Remote termination position filter is missing"))
|
||||||
logger.debug(
|
front_ports = FrontPort.objects.filter(q_filter)
|
||||||
'Encountered rear port mapped to multiple front ports but position stack is empty; aborting '
|
# Obtain the individual front ports based on the termination and position
|
||||||
'trace.'
|
elif position_stack:
|
||||||
|
front_ports = FrontPort.objects.filter(
|
||||||
|
rear_port_id=remote_terminations[0].pk,
|
||||||
|
rear_port_position__in=position_stack.pop()
|
||||||
)
|
)
|
||||||
break
|
# If all rear ports have a single position, we can just get the front ports
|
||||||
|
elif all([rp.positions == 1 for rp in remote_terminations]):
|
||||||
|
front_ports = FrontPort.objects.filter(rear_port_id__in=[rp.pk for rp in remote_terminations])
|
||||||
|
|
||||||
|
if len(front_ports) != len(remote_terminations):
|
||||||
|
# Some rear ports does not have a front port
|
||||||
|
is_split = True
|
||||||
|
break
|
||||||
else:
|
else:
|
||||||
port_mappings = PortMapping.objects.filter(rear_port__in=remote_terminations)
|
# No position indicated: path has split, so we stop at the RearPorts
|
||||||
if not port_mappings:
|
is_split = True
|
||||||
break
|
break
|
||||||
|
|
||||||
# Compile the list of FrontPorts without duplication or altering their ordering
|
terminations = front_ports
|
||||||
terminations = list(dict.fromkeys(mapping.front_port for mapping in port_mappings))
|
|
||||||
if any(t.positions > 1 for t in terminations):
|
|
||||||
position_stack.append([mapping.front_port_position for mapping in port_mappings])
|
|
||||||
|
|
||||||
elif isinstance(remote_terminations[0], CircuitTermination):
|
elif isinstance(remote_terminations[0], CircuitTermination):
|
||||||
# Follow a CircuitTermination to its corresponding CircuitTermination (A to Z or vice versa)
|
# Follow a CircuitTermination to its corresponding CircuitTermination (A to Z or vice versa)
|
||||||
@@ -942,7 +876,6 @@ class CablePath(models.Model):
|
|||||||
# Unsupported topology, mark as split and exit
|
# Unsupported topology, mark as split and exit
|
||||||
is_complete = False
|
is_complete = False
|
||||||
is_split = True
|
is_split = True
|
||||||
logger.warning('Encountered an unsupported topology; aborting trace.')
|
|
||||||
break
|
break
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
@@ -1021,23 +954,16 @@ class CablePath(models.Model):
|
|||||||
|
|
||||||
# RearPort splitting to multiple FrontPorts with no stack position
|
# RearPort splitting to multiple FrontPorts with no stack position
|
||||||
if type(nodes[0]) is RearPort:
|
if type(nodes[0]) is RearPort:
|
||||||
return [
|
return FrontPort.objects.filter(rear_port__in=nodes)
|
||||||
mapping.front_port for mapping in
|
|
||||||
PortMapping.objects.filter(rear_port__in=nodes).prefetch_related('front_port')
|
|
||||||
]
|
|
||||||
# Cable terminating to multiple FrontPorts mapped to different
|
# Cable terminating to multiple FrontPorts mapped to different
|
||||||
# RearPorts connected to different cables
|
# RearPorts connected to different cables
|
||||||
if type(nodes[0]) is FrontPort:
|
elif type(nodes[0]) is FrontPort:
|
||||||
return [
|
return RearPort.objects.filter(pk__in=[fp.rear_port_id for fp in nodes])
|
||||||
mapping.rear_port for mapping in
|
|
||||||
PortMapping.objects.filter(front_port__in=nodes).prefetch_related('rear_port')
|
|
||||||
]
|
|
||||||
# Cable terminating to multiple CircuitTerminations
|
# Cable terminating to multiple CircuitTerminations
|
||||||
if type(nodes[0]) is CircuitTermination:
|
elif type(nodes[0]) is CircuitTermination:
|
||||||
return [
|
return [
|
||||||
ct.get_peer_termination() for ct in nodes
|
ct.get_peer_termination() for ct in nodes
|
||||||
]
|
]
|
||||||
return []
|
|
||||||
|
|
||||||
def get_asymmetric_nodes(self):
|
def get_asymmetric_nodes(self):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ from mptt.models import MPTTModel, TreeForeignKey
|
|||||||
|
|
||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.models.base import PortMappingBase
|
|
||||||
from dcim.models.mixins import InterfaceValidationMixin
|
from dcim.models.mixins import InterfaceValidationMixin
|
||||||
from netbox.models import ChangeLoggedModel
|
from netbox.models import ChangeLoggedModel
|
||||||
from utilities.fields import ColorField, NaturalOrderingField
|
from utilities.fields import ColorField, NaturalOrderingField
|
||||||
@@ -29,7 +28,6 @@ __all__ = (
|
|||||||
'InterfaceTemplate',
|
'InterfaceTemplate',
|
||||||
'InventoryItemTemplate',
|
'InventoryItemTemplate',
|
||||||
'ModuleBayTemplate',
|
'ModuleBayTemplate',
|
||||||
'PortTemplateMapping',
|
|
||||||
'PowerOutletTemplate',
|
'PowerOutletTemplate',
|
||||||
'PowerPortTemplate',
|
'PowerPortTemplate',
|
||||||
'RearPortTemplate',
|
'RearPortTemplate',
|
||||||
@@ -520,53 +518,6 @@ class InterfaceTemplate(InterfaceValidationMixin, ModularComponentTemplateModel)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class PortTemplateMapping(PortMappingBase):
|
|
||||||
"""
|
|
||||||
Maps a FrontPortTemplate & position to a RearPortTemplate & position.
|
|
||||||
"""
|
|
||||||
device_type = models.ForeignKey(
|
|
||||||
to='dcim.DeviceType',
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name='port_mappings',
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
)
|
|
||||||
module_type = models.ForeignKey(
|
|
||||||
to='dcim.ModuleType',
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name='port_mappings',
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
)
|
|
||||||
front_port = models.ForeignKey(
|
|
||||||
to='dcim.FrontPortTemplate',
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name='mappings',
|
|
||||||
)
|
|
||||||
rear_port = models.ForeignKey(
|
|
||||||
to='dcim.RearPortTemplate',
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name='mappings',
|
|
||||||
)
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
super().clean()
|
|
||||||
|
|
||||||
# Validate rear port assignment
|
|
||||||
if self.front_port.device_type_id != self.rear_port.device_type_id:
|
|
||||||
raise ValidationError({
|
|
||||||
"rear_port": _("Rear port ({rear_port}) must belong to the same device type").format(
|
|
||||||
rear_port=self.rear_port
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
# Associate the mapping with the parent DeviceType/ModuleType
|
|
||||||
self.device_type = self.front_port.device_type
|
|
||||||
self.module_type = self.front_port.module_type
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class FrontPortTemplate(ModularComponentTemplateModel):
|
class FrontPortTemplate(ModularComponentTemplateModel):
|
||||||
"""
|
"""
|
||||||
Template for a pass-through port on the front of a new Device.
|
Template for a pass-through port on the front of a new Device.
|
||||||
@@ -580,13 +531,18 @@ class FrontPortTemplate(ModularComponentTemplateModel):
|
|||||||
verbose_name=_('color'),
|
verbose_name=_('color'),
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
positions = models.PositiveSmallIntegerField(
|
rear_port = models.ForeignKey(
|
||||||
verbose_name=_('positions'),
|
to='dcim.RearPortTemplate',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='frontport_templates'
|
||||||
|
)
|
||||||
|
rear_port_position = models.PositiveSmallIntegerField(
|
||||||
|
verbose_name=_('rear port position'),
|
||||||
default=1,
|
default=1,
|
||||||
validators=[
|
validators=[
|
||||||
MinValueValidator(PORT_POSITION_MIN),
|
MinValueValidator(REARPORT_POSITIONS_MIN),
|
||||||
MaxValueValidator(PORT_POSITION_MAX)
|
MaxValueValidator(REARPORT_POSITIONS_MAX)
|
||||||
],
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
component_model = FrontPort
|
component_model = FrontPort
|
||||||
@@ -601,6 +557,10 @@ class FrontPortTemplate(ModularComponentTemplateModel):
|
|||||||
fields=('module_type', 'name'),
|
fields=('module_type', 'name'),
|
||||||
name='%(app_label)s_%(class)s_unique_module_type_name'
|
name='%(app_label)s_%(class)s_unique_module_type_name'
|
||||||
),
|
),
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=('rear_port', 'rear_port_position'),
|
||||||
|
name='%(app_label)s_%(class)s_unique_rear_port_position'
|
||||||
|
),
|
||||||
)
|
)
|
||||||
verbose_name = _('front port template')
|
verbose_name = _('front port template')
|
||||||
verbose_name_plural = _('front port templates')
|
verbose_name_plural = _('front port templates')
|
||||||
@@ -608,23 +568,40 @@ class FrontPortTemplate(ModularComponentTemplateModel):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
# Check that positions is greater than or equal to the number of associated RearPortTemplates
|
try:
|
||||||
if not self._state.adding:
|
|
||||||
mapping_count = self.mappings.count()
|
# Validate rear port assignment
|
||||||
if self.positions < mapping_count:
|
if self.rear_port.device_type != self.device_type:
|
||||||
raise ValidationError({
|
raise ValidationError(
|
||||||
"positions": _(
|
_("Rear port ({name}) must belong to the same device type").format(name=self.rear_port)
|
||||||
"The number of positions cannot be less than the number of mapped rear port templates ({count})"
|
)
|
||||||
).format(count=mapping_count)
|
|
||||||
})
|
# Validate rear port position assignment
|
||||||
|
if self.rear_port_position > self.rear_port.positions:
|
||||||
|
raise ValidationError(
|
||||||
|
_("Invalid rear port position ({position}); rear port {name} has only {count} positions").format(
|
||||||
|
position=self.rear_port_position,
|
||||||
|
name=self.rear_port.name,
|
||||||
|
count=self.rear_port.positions
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
except RearPortTemplate.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
def instantiate(self, **kwargs):
|
def instantiate(self, **kwargs):
|
||||||
|
if self.rear_port:
|
||||||
|
rear_port_name = self.rear_port.resolve_name(kwargs.get('module'))
|
||||||
|
rear_port = RearPort.objects.get(name=rear_port_name, **kwargs)
|
||||||
|
else:
|
||||||
|
rear_port = None
|
||||||
return self.component_model(
|
return self.component_model(
|
||||||
name=self.resolve_name(kwargs.get('module')),
|
name=self.resolve_name(kwargs.get('module')),
|
||||||
label=self.resolve_label(kwargs.get('module')),
|
label=self.resolve_label(kwargs.get('module')),
|
||||||
type=self.type,
|
type=self.type,
|
||||||
color=self.color,
|
color=self.color,
|
||||||
positions=self.positions,
|
rear_port=rear_port,
|
||||||
|
rear_port_position=self.rear_port_position,
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
instantiate.do_not_call_in_templates = True
|
instantiate.do_not_call_in_templates = True
|
||||||
@@ -634,7 +611,8 @@ class FrontPortTemplate(ModularComponentTemplateModel):
|
|||||||
'name': self.name,
|
'name': self.name,
|
||||||
'type': self.type,
|
'type': self.type,
|
||||||
'color': self.color,
|
'color': self.color,
|
||||||
'positions': self.positions,
|
'rear_port': self.rear_port.name,
|
||||||
|
'rear_port_position': self.rear_port_position,
|
||||||
'label': self.label,
|
'label': self.label,
|
||||||
'description': self.description,
|
'description': self.description,
|
||||||
}
|
}
|
||||||
@@ -657,9 +635,9 @@ class RearPortTemplate(ModularComponentTemplateModel):
|
|||||||
verbose_name=_('positions'),
|
verbose_name=_('positions'),
|
||||||
default=1,
|
default=1,
|
||||||
validators=[
|
validators=[
|
||||||
MinValueValidator(PORT_POSITION_MIN),
|
MinValueValidator(REARPORT_POSITIONS_MIN),
|
||||||
MaxValueValidator(PORT_POSITION_MAX)
|
MaxValueValidator(REARPORT_POSITIONS_MAX)
|
||||||
],
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
component_model = RearPort
|
component_model = RearPort
|
||||||
@@ -668,20 +646,6 @@ class RearPortTemplate(ModularComponentTemplateModel):
|
|||||||
verbose_name = _('rear port template')
|
verbose_name = _('rear port template')
|
||||||
verbose_name_plural = _('rear port templates')
|
verbose_name_plural = _('rear port templates')
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
super().clean()
|
|
||||||
|
|
||||||
# Check that positions is greater than or equal to the number of associated FrontPortTemplates
|
|
||||||
if not self._state.adding:
|
|
||||||
mapping_count = self.mappings.count()
|
|
||||||
if self.positions < mapping_count:
|
|
||||||
raise ValidationError({
|
|
||||||
"positions": _(
|
|
||||||
"The number of positions cannot be less than the number of mapped front port templates "
|
|
||||||
"({count})"
|
|
||||||
).format(count=mapping_count)
|
|
||||||
})
|
|
||||||
|
|
||||||
def instantiate(self, **kwargs):
|
def instantiate(self, **kwargs):
|
||||||
return self.component_model(
|
return self.component_model(
|
||||||
name=self.resolve_name(kwargs.get('module')),
|
name=self.resolve_name(kwargs.get('module')),
|
||||||
@@ -723,8 +687,8 @@ class ModuleBayTemplate(ModularComponentTemplateModel):
|
|||||||
|
|
||||||
def instantiate(self, **kwargs):
|
def instantiate(self, **kwargs):
|
||||||
return self.component_model(
|
return self.component_model(
|
||||||
name=self.resolve_name(kwargs.get('module')),
|
name=self.name,
|
||||||
label=self.resolve_label(kwargs.get('module')),
|
label=self.label,
|
||||||
position=self.position,
|
position=self.position,
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
||||||
from django.contrib.postgres.fields import ArrayField
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@@ -12,7 +11,6 @@ from mptt.models import MPTTModel, TreeForeignKey
|
|||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.fields import WWNField
|
from dcim.fields import WWNField
|
||||||
from dcim.models.base import PortMappingBase
|
|
||||||
from dcim.models.mixins import InterfaceValidationMixin
|
from dcim.models.mixins import InterfaceValidationMixin
|
||||||
from netbox.choices import ColorChoices
|
from netbox.choices import ColorChoices
|
||||||
from netbox.models import OrganizationalModel, NetBoxModel
|
from netbox.models import OrganizationalModel, NetBoxModel
|
||||||
@@ -37,7 +35,6 @@ __all__ = (
|
|||||||
'InventoryItemRole',
|
'InventoryItemRole',
|
||||||
'ModuleBay',
|
'ModuleBay',
|
||||||
'PathEndpoint',
|
'PathEndpoint',
|
||||||
'PortMapping',
|
|
||||||
'PowerOutlet',
|
'PowerOutlet',
|
||||||
'PowerPort',
|
'PowerPort',
|
||||||
'RearPort',
|
'RearPort',
|
||||||
@@ -178,24 +175,15 @@ class CabledObjectModel(models.Model):
|
|||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
cable_connector = models.PositiveSmallIntegerField(
|
cable_position = models.PositiveIntegerField(
|
||||||
|
verbose_name=_('cable position'),
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
validators=(
|
validators=(
|
||||||
MinValueValidator(CABLE_CONNECTOR_MIN),
|
MinValueValidator(CABLE_POSITION_MIN),
|
||||||
MaxValueValidator(CABLE_CONNECTOR_MAX)
|
MaxValueValidator(CABLE_POSITION_MAX)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
cable_positions = ArrayField(
|
|
||||||
base_field=models.PositiveSmallIntegerField(
|
|
||||||
validators=(
|
|
||||||
MinValueValidator(CABLE_POSITION_MIN),
|
|
||||||
MaxValueValidator(CABLE_POSITION_MAX)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
)
|
|
||||||
mark_connected = models.BooleanField(
|
mark_connected = models.BooleanField(
|
||||||
verbose_name=_('mark connected'),
|
verbose_name=_('mark connected'),
|
||||||
default=False,
|
default=False,
|
||||||
@@ -220,31 +208,22 @@ class CabledObjectModel(models.Model):
|
|||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
"cable_end": _("Must specify cable end (A or B) when attaching a cable.")
|
"cable_end": _("Must specify cable end (A or B) when attaching a cable.")
|
||||||
})
|
})
|
||||||
if self.cable_connector and not self.cable_positions:
|
if not self.cable_position:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
"cable_positions": _("Must specify position(s) when specifying a cable connector.")
|
"cable_position": _("Must specify cable termination position when attaching a cable.")
|
||||||
})
|
|
||||||
if self.cable_positions and not self.cable_connector:
|
|
||||||
raise ValidationError({
|
|
||||||
"cable_positions": _("Cable positions cannot be set without a cable connector.")
|
|
||||||
})
|
|
||||||
if self.mark_connected:
|
|
||||||
raise ValidationError({
|
|
||||||
"mark_connected": _("Cannot mark as connected with a cable attached.")
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
if self.cable_end:
|
|
||||||
raise ValidationError({
|
|
||||||
"cable_end": _("Cable end must not be set without a cable.")
|
|
||||||
})
|
|
||||||
if self.cable_connector:
|
|
||||||
raise ValidationError({
|
|
||||||
"cable_connector": _("Cable connector must not be set without a cable.")
|
|
||||||
})
|
|
||||||
if self.cable_positions:
|
|
||||||
raise ValidationError({
|
|
||||||
"cable_positions": _("Cable termination positions must not be set without a cable.")
|
|
||||||
})
|
})
|
||||||
|
if self.cable_end and not self.cable:
|
||||||
|
raise ValidationError({
|
||||||
|
"cable_end": _("Cable end must not be set without a cable.")
|
||||||
|
})
|
||||||
|
if self.cable_position and not self.cable:
|
||||||
|
raise ValidationError({
|
||||||
|
"cable_position": _("Cable termination position must not be set without a cable.")
|
||||||
|
})
|
||||||
|
if self.mark_connected and self.cable:
|
||||||
|
raise ValidationError({
|
||||||
|
"mark_connected": _("Cannot mark as connected with a cable attached.")
|
||||||
|
})
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def link(self):
|
def link(self):
|
||||||
@@ -279,22 +258,6 @@ class CabledObjectModel(models.Model):
|
|||||||
return None
|
return None
|
||||||
return CableEndChoices.SIDE_A if self.cable_end == CableEndChoices.SIDE_B else CableEndChoices.SIDE_B
|
return CableEndChoices.SIDE_A if self.cable_end == CableEndChoices.SIDE_B else CableEndChoices.SIDE_B
|
||||||
|
|
||||||
def set_cable_termination(self, termination):
|
|
||||||
"""Save attributes from the given CableTermination on the terminating object."""
|
|
||||||
self.cable = termination.cable
|
|
||||||
self.cable_end = termination.cable_end
|
|
||||||
self.cable_connector = termination.connector
|
|
||||||
self.cable_positions = termination.positions
|
|
||||||
set_cable_termination.alters_data = True
|
|
||||||
|
|
||||||
def clear_cable_termination(self, termination):
|
|
||||||
"""Clear all cable termination attributes from the terminating object."""
|
|
||||||
self.cable = None
|
|
||||||
self.cable_end = None
|
|
||||||
self.cable_connector = None
|
|
||||||
self.cable_positions = None
|
|
||||||
clear_cable_termination.alters_data = True
|
|
||||||
|
|
||||||
|
|
||||||
class PathEndpoint(models.Model):
|
class PathEndpoint(models.Model):
|
||||||
"""
|
"""
|
||||||
@@ -1106,43 +1069,6 @@ class Interface(
|
|||||||
# Pass-through ports
|
# Pass-through ports
|
||||||
#
|
#
|
||||||
|
|
||||||
class PortMapping(PortMappingBase):
|
|
||||||
"""
|
|
||||||
Maps a FrontPort & position to a RearPort & position.
|
|
||||||
"""
|
|
||||||
device = models.ForeignKey(
|
|
||||||
to='dcim.Device',
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name='port_mappings',
|
|
||||||
)
|
|
||||||
front_port = models.ForeignKey(
|
|
||||||
to='dcim.FrontPort',
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name='mappings',
|
|
||||||
)
|
|
||||||
rear_port = models.ForeignKey(
|
|
||||||
to='dcim.RearPort',
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name='mappings',
|
|
||||||
)
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
super().clean()
|
|
||||||
|
|
||||||
# Both ports must belong to the same device
|
|
||||||
if self.front_port.device_id != self.rear_port.device_id:
|
|
||||||
raise ValidationError({
|
|
||||||
"rear_port": _("Rear port ({rear_port}) must belong to the same device").format(
|
|
||||||
rear_port=self.rear_port
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
# Associate the mapping with the parent Device
|
|
||||||
self.device = self.front_port.device
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
|
class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
|
||||||
"""
|
"""
|
||||||
A pass-through port on the front of a Device.
|
A pass-through port on the front of a Device.
|
||||||
@@ -1156,16 +1082,22 @@ class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
|
|||||||
verbose_name=_('color'),
|
verbose_name=_('color'),
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
positions = models.PositiveSmallIntegerField(
|
rear_port = models.ForeignKey(
|
||||||
verbose_name=_('positions'),
|
to='dcim.RearPort',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='frontports'
|
||||||
|
)
|
||||||
|
rear_port_position = models.PositiveSmallIntegerField(
|
||||||
|
verbose_name=_('rear port position'),
|
||||||
default=1,
|
default=1,
|
||||||
validators=[
|
validators=[
|
||||||
MinValueValidator(PORT_POSITION_MIN),
|
MinValueValidator(REARPORT_POSITIONS_MIN),
|
||||||
MaxValueValidator(PORT_POSITION_MAX)
|
MaxValueValidator(REARPORT_POSITIONS_MAX)
|
||||||
],
|
],
|
||||||
|
help_text=_('Mapped position on corresponding rear port')
|
||||||
)
|
)
|
||||||
|
|
||||||
clone_fields = ('device', 'type', 'color', 'positions')
|
clone_fields = ('device', 'type', 'color')
|
||||||
|
|
||||||
class Meta(ModularComponentModel.Meta):
|
class Meta(ModularComponentModel.Meta):
|
||||||
constraints = (
|
constraints = (
|
||||||
@@ -1173,6 +1105,10 @@ class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
|
|||||||
fields=('device', 'name'),
|
fields=('device', 'name'),
|
||||||
name='%(app_label)s_%(class)s_unique_device_name'
|
name='%(app_label)s_%(class)s_unique_device_name'
|
||||||
),
|
),
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=('rear_port', 'rear_port_position'),
|
||||||
|
name='%(app_label)s_%(class)s_unique_rear_port_position'
|
||||||
|
),
|
||||||
)
|
)
|
||||||
verbose_name = _('front port')
|
verbose_name = _('front port')
|
||||||
verbose_name_plural = _('front ports')
|
verbose_name_plural = _('front ports')
|
||||||
@@ -1180,14 +1116,27 @@ class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
# Check that positions is greater than or equal to the number of associated RearPorts
|
if hasattr(self, 'rear_port'):
|
||||||
if not self._state.adding:
|
|
||||||
mapping_count = self.mappings.count()
|
# Validate rear port assignment
|
||||||
if self.positions < mapping_count:
|
if self.rear_port.device != self.device:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
"positions": _(
|
"rear_port": _(
|
||||||
"The number of positions cannot be less than the number of mapped rear ports ({count})"
|
"Rear port ({rear_port}) must belong to the same device"
|
||||||
).format(count=mapping_count)
|
).format(rear_port=self.rear_port)
|
||||||
|
})
|
||||||
|
|
||||||
|
# Validate rear port position assignment
|
||||||
|
if self.rear_port_position > self.rear_port.positions:
|
||||||
|
raise ValidationError({
|
||||||
|
"rear_port_position": _(
|
||||||
|
"Invalid rear port position ({rear_port_position}): Rear port {name} has only {positions} "
|
||||||
|
"positions."
|
||||||
|
).format(
|
||||||
|
rear_port_position=self.rear_port_position,
|
||||||
|
name=self.rear_port.name,
|
||||||
|
positions=self.rear_port.positions
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -1208,11 +1157,11 @@ class RearPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
|
|||||||
verbose_name=_('positions'),
|
verbose_name=_('positions'),
|
||||||
default=1,
|
default=1,
|
||||||
validators=[
|
validators=[
|
||||||
MinValueValidator(PORT_POSITION_MIN),
|
MinValueValidator(REARPORT_POSITIONS_MIN),
|
||||||
MaxValueValidator(PORT_POSITION_MAX)
|
MaxValueValidator(REARPORT_POSITIONS_MAX)
|
||||||
],
|
],
|
||||||
|
help_text=_('Number of front ports which may be mapped')
|
||||||
)
|
)
|
||||||
|
|
||||||
clone_fields = ('device', 'type', 'color', 'positions')
|
clone_fields = ('device', 'type', 'color', 'positions')
|
||||||
|
|
||||||
class Meta(ModularComponentModel.Meta):
|
class Meta(ModularComponentModel.Meta):
|
||||||
@@ -1224,13 +1173,13 @@ class RearPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
|
|||||||
|
|
||||||
# Check that positions count is greater than or equal to the number of associated FrontPorts
|
# Check that positions count is greater than or equal to the number of associated FrontPorts
|
||||||
if not self._state.adding:
|
if not self._state.adding:
|
||||||
mapping_count = self.mappings.count()
|
frontport_count = self.frontports.count()
|
||||||
if self.positions < mapping_count:
|
if self.positions < frontport_count:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
"positions": _(
|
"positions": _(
|
||||||
"The number of positions cannot be less than the number of mapped front ports "
|
"The number of positions cannot be less than the number of mapped front ports "
|
||||||
"({count})"
|
"({frontport_count})"
|
||||||
).format(count=mapping_count)
|
).format(frontport_count=frontport_count)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -1292,8 +1241,6 @@ class ModuleBay(ModularComponentModel, TrackingModelMixin, MPTTModel):
|
|||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if self.module:
|
if self.module:
|
||||||
self.parent = self.module.module_bay
|
self.parent = self.module.module_bay
|
||||||
else:
|
|
||||||
self.parent = None
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user