mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-29 03:46:25 -06:00
Merge branch 'feature' of https://github.com/netbox-community/netbox into 15924-fix-api-interface-patch-tagged-all-mode
This commit is contained in:
commit
8dd611986a
@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.0.11
|
||||
placeholder: v4.1.4
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
@ -24,6 +24,21 @@ body:
|
||||
- Data model extension
|
||||
- New functionality
|
||||
- Change to existing functionality
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Triage priority
|
||||
description: >
|
||||
Issue triage may be prioritized in some cases. Select whichever of the following
|
||||
conditions applies, if any.
|
||||
options:
|
||||
- I volunteer to perform this work (if approved)
|
||||
- I'm a NetBox Labs customer
|
||||
- This is a very minor change
|
||||
- N/A
|
||||
default: 3
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
@ -22,11 +22,25 @@ body:
|
||||
- Self-hosted
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Triage priority
|
||||
description: >
|
||||
Issue triage may be prioritized in some cases. Select whichever of the following
|
||||
conditions applies, if any.
|
||||
options:
|
||||
- I volunteer to perform this work (if approved)
|
||||
- I'm a NetBox Labs customer
|
||||
- This is preventing me from using NetBox
|
||||
- N/A
|
||||
default: 3
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: NetBox Version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.0.11
|
||||
placeholder: v4.1.4
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.10', '3.11', '3.12']
|
||||
node-version: ['18.x']
|
||||
node-version: ['20.x']
|
||||
services:
|
||||
redis:
|
||||
image: redis
|
||||
@ -73,7 +73,7 @@ jobs:
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install pycodestyle coverage tblib
|
||||
pip install ruff coverage tblib
|
||||
|
||||
- name: Build documentation
|
||||
run: mkdocs build
|
||||
@ -85,7 +85,7 @@ jobs:
|
||||
run: python netbox/manage.py makemigrations --check
|
||||
|
||||
- name: Check PEP8 compliance
|
||||
run: pycodestyle --ignore=W504,E501 --exclude=node_modules netbox/
|
||||
run: ruff check netbox/
|
||||
|
||||
- name: Check UI ESLint, TypeScript, and Prettier Compliance
|
||||
run: yarn --cwd netbox/project-static validate
|
||||
|
5
.github/workflows/close-stale-issues.yml
vendored
5
.github/workflows/close-stale-issues.yml
vendored
@ -18,7 +18,7 @@ jobs:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
# General parameters
|
||||
operations-per-run: 100
|
||||
operations-per-run: 200
|
||||
remove-stale-when-updated: false
|
||||
|
||||
# Issue parameters
|
||||
@ -43,8 +43,9 @@ jobs:
|
||||
# Pull request parameters
|
||||
close-pr-message: >
|
||||
This PR has been automatically closed due to lack of activity.
|
||||
days-before-pr-stale: 15
|
||||
days-before-pr-stale: 30
|
||||
days-before-pr-close: 15
|
||||
exempt-pr-labels: 'status: blocked'
|
||||
stale-pr-label: 'pending closure'
|
||||
stale-pr-message: >
|
||||
This PR has been automatically marked as stale because it has not had
|
||||
|
44
.pre-commit-config.yaml
Normal file
44
.pre-commit-config.yaml
Normal file
@ -0,0 +1,44 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.6.9
|
||||
hooks:
|
||||
- id: ruff
|
||||
name: "Ruff linter"
|
||||
args: [ netbox/ ]
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: django-check
|
||||
name: "Django system check"
|
||||
description: "Run Django's internal check for common problems"
|
||||
entry: python netbox/manage.py check
|
||||
language: system
|
||||
pass_filenames: false
|
||||
types: [python]
|
||||
- id: django-makemigrations
|
||||
name: "Django migrations check"
|
||||
description: "Check for any missing Django migrations"
|
||||
entry: python netbox/manage.py makemigrations --check
|
||||
language: system
|
||||
pass_filenames: false
|
||||
types: [python]
|
||||
- id: mkdocs-build
|
||||
name: "Build documentation"
|
||||
description: "Build the documentation with mkdocs"
|
||||
files: 'docs/'
|
||||
entry: mkdocs build
|
||||
language: system
|
||||
pass_filenames: false
|
||||
- id: yarn-validate
|
||||
name: "Yarn validate"
|
||||
description: "Check UI ESLint, TypeScript, and Prettier compliance"
|
||||
files: 'netbox/project-static/'
|
||||
entry: yarn --cwd netbox/project-static validate
|
||||
language: system
|
||||
pass_filenames: false
|
||||
- id: verify-bundles
|
||||
name: "Verify static asset bundles"
|
||||
description: "Ensure that any modified static assets have been compiled"
|
||||
files: 'netbox/project-static/'
|
||||
entry: scripts/verify-bundles.sh
|
||||
language: system
|
||||
pass_filenames: false
|
11
README.md
11
README.md
@ -7,7 +7,11 @@
|
||||
<a href="https://github.com/netbox-community/netbox/stargazers"><img src="https://img.shields.io/github/stars/netbox-community/netbox?style=flat" alt="GitHub stars" /></a>
|
||||
<a href="https://explore.transifex.com/netbox-community/netbox/"><img src="https://img.shields.io/badge/languages-15-blue" alt="Languages supported" /></a>
|
||||
<a href="https://github.com/netbox-community/netbox/actions/workflows/ci.yml"><img src="https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=master" alt="CI status" /></a>
|
||||
<p></p>
|
||||
<p>
|
||||
<strong><a href="https://github.com/netbox-community/netbox/">NetBox Community</a></strong> |
|
||||
<strong><a href="https://netboxlabs.com/netbox-cloud/">NetBox Cloud</a></strong> |
|
||||
<strong><a href="https://netboxlabs.com/netbox-enterprise/">NetBox Enterprise</a></strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
NetBox exists to empower network engineers. Since its release in 2016, it has become the go-to solution for modeling and documenting network infrastructure for thousands of organizations worldwide. As a successor to legacy IPAM and DCIM applications, NetBox provides a cohesive, extensive, and accessible data model for all things networked. By providing a single robust user interface and programmable APIs for everything from cable maps to device configurations, NetBox serves as the central source of truth for the modern network.
|
||||
@ -81,11 +85,6 @@ NetBox automatically logs the creation, modification, and deletion of all manage
|
||||
* The [official documentation](https://docs.netbox.dev) offers a comprehensive introduction.
|
||||
* Check out [our wiki](https://github.com/netbox-community/netbox/wiki/Community-Contributions) for even more projects to get the most out of NetBox!
|
||||
|
||||
<p align="center">
|
||||
<a href="https://netboxlabs.com/netbox-cloud/"><img src="docs/media/misc/netbox_cloud.png" alt="NetBox Cloud" /></a><br />
|
||||
Looking for a managed solution? Check out <strong><a href="https://netboxlabs.com/netbox-cloud/">NetBox Cloud</a></strong> or <strong><a href="https://netboxlabs.com/netbox-enterprise/">NetBox Enterprise</a></strong>!
|
||||
</p>
|
||||
|
||||
## Get Involved
|
||||
|
||||
* Follow [@NetBoxOfficial](https://twitter.com/NetBoxOfficial) on Twitter!
|
||||
|
@ -84,10 +84,6 @@ Jinja2
|
||||
# https://python-markdown.github.io/changelog/
|
||||
Markdown
|
||||
|
||||
# File inclusion plugin for Python-Markdown
|
||||
# https://github.com/cmacmackin/markdown-include
|
||||
markdown-include
|
||||
|
||||
# MkDocs Material theme (for documentation build)
|
||||
# https://squidfunk.github.io/mkdocs-material/changelog/
|
||||
mkdocs-material
|
||||
|
@ -20,7 +20,7 @@
|
||||
Alias /static /opt/netbox/netbox/static
|
||||
|
||||
<Directory /opt/netbox/netbox/static>
|
||||
Options Indexes FollowSymLinks MultiViews
|
||||
Options FollowSymLinks MultiViews
|
||||
AllowOverride None
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
@ -12,6 +12,9 @@
|
||||
"left-to-right",
|
||||
"right-to-left",
|
||||
"side-to-rear",
|
||||
"rear-to-side",
|
||||
"bottom-to-top",
|
||||
"top-to-bottom",
|
||||
"passive",
|
||||
"mixed"
|
||||
]
|
||||
@ -149,6 +152,7 @@
|
||||
"nema-l15-60p",
|
||||
"nema-l21-20p",
|
||||
"nema-l21-30p",
|
||||
"nema-l22-20p",
|
||||
"nema-l22-30p",
|
||||
"cs6361c",
|
||||
"cs6365c",
|
||||
@ -262,6 +266,7 @@
|
||||
"nema-l15-60r",
|
||||
"nema-l21-20r",
|
||||
"nema-l21-30r",
|
||||
"nema-l22-20r",
|
||||
"nema-l22-30r",
|
||||
"CS6360C",
|
||||
"CS6364C",
|
||||
@ -288,6 +293,7 @@
|
||||
"molex-micro-fit-2x2",
|
||||
"molex-micro-fit-2x4",
|
||||
"dc-terminal",
|
||||
"eaton-c39",
|
||||
"hdot-cx",
|
||||
"saf-d-grid",
|
||||
"neutrik-powercon-20a",
|
||||
@ -328,6 +334,7 @@
|
||||
"5gbase-t",
|
||||
"10gbase-t",
|
||||
"10gbase-cx4",
|
||||
"100base-x-sfp",
|
||||
"1000base-x-gbic",
|
||||
"1000base-x-sfp",
|
||||
"10gbase-x-sfpp",
|
||||
@ -379,6 +386,7 @@
|
||||
"ieee802.11ay",
|
||||
"ieee802.11be",
|
||||
"ieee802.15.1",
|
||||
"ieee802.15.4",
|
||||
"other-wireless",
|
||||
"gsm",
|
||||
"cdma",
|
||||
@ -518,6 +526,14 @@
|
||||
"urm-p4",
|
||||
"urm-p8",
|
||||
"splice",
|
||||
"usb-a",
|
||||
"usb-b",
|
||||
"usb-c",
|
||||
"usb-mini-a",
|
||||
"usb-mini-b",
|
||||
"usb-micro-a",
|
||||
"usb-micro-b",
|
||||
"usb-micro-ab",
|
||||
"other"
|
||||
]
|
||||
}
|
||||
@ -575,6 +591,14 @@
|
||||
"urm-p4",
|
||||
"urm-p8",
|
||||
"splice",
|
||||
"usb-a",
|
||||
"usb-b",
|
||||
"usb-c",
|
||||
"usb-mini-a",
|
||||
"usb-mini-b",
|
||||
"usb-micro-a",
|
||||
"usb-micro-b",
|
||||
"usb-micro-ab",
|
||||
"other"
|
||||
]
|
||||
}
|
||||
|
18
docs/_theme/partials/copyright.html
vendored
Normal file
18
docs/_theme/partials/copyright.html
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
<div class="md-copyright">
|
||||
{% if config.copyright %}
|
||||
<div class="md-copyright__highlight">
|
||||
{{ config.copyright }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not config.extra.generator == false %}
|
||||
Made with
|
||||
<a href="https://squidfunk.github.io/mkdocs-material/" target="_blank" rel="noopener">
|
||||
Material for MkDocs
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if not config.extra.build_public %}
|
||||
<div class="md-copyright">
|
||||
ℹ️ Documentation is being served locally
|
||||
</div>
|
||||
{% endif %}
|
@ -1,8 +1,8 @@
|
||||
# Microsoft Azure AD
|
||||
# Microsoft Entra ID
|
||||
|
||||
This guide explains how to configure single sign-on (SSO) support for NetBox using [Microsoft Azure Active Directory (AD)](https://azure.microsoft.com/en-us/services/active-directory/) as an authentication backend.
|
||||
This guide explains how to configure single sign-on (SSO) support for NetBox using [Microsoft Entra ID](https://www.microsoft.com/en-us/security/business/identity-access/microsoft-entra-id) as an authentication backend.
|
||||
|
||||
## Azure AD Configuration
|
||||
## Entra ID Configuration
|
||||
|
||||
### 1. Create a test user (optional)
|
||||
|
||||
@ -16,7 +16,7 @@ Under the Azure Active Directory dashboard, navigate to **Add > App registration
|
||||
|
||||
Enter a name for the registration (e.g. "NetBox") and ensure that the "single tenant" option is selected.
|
||||
|
||||
Under "Redirect URI", select "Web" for the platform and enter the path to your NetBox installation, ending with `/oauth/complete/azuread-oauth2/`. Note that this URI **must** begin with `https://` unless you are referencing localhost (for development purposes).
|
||||
Under "Redirect URI", select "Web" for the platform and enter the path to your NetBox installation, ending with `/oauth/complete/entraid-oauth2/`. Note that this URI **must** begin with `https://` unless you are referencing localhost (for development purposes).
|
||||
|
||||

|
||||
|
@ -96,14 +96,6 @@ The maximum size (in bytes) of an incoming HTTP request (i.e. `GET` or `POST` da
|
||||
|
||||
---
|
||||
|
||||
## DJANGO_ADMIN_ENABLED
|
||||
|
||||
Default: False
|
||||
|
||||
Setting this to True installs the `django.contrib.admin` app and enables the [Django admin UI](https://docs.djangoproject.com/en/5.0/ref/contrib/admin/). This may be necessary to support older plugins which do not integrate with the native NetBox interface.
|
||||
|
||||
---
|
||||
|
||||
## ENFORCE_GLOBAL_UNIQUE
|
||||
|
||||
!!! tip "Dynamic Configuration Parameter"
|
||||
|
@ -17,6 +17,9 @@ They can also be used as a mechanism for validating the integrity of data within
|
||||
|
||||
Custom scripts are Python code which exists outside the NetBox code base, so they can be updated and changed without interfering with the core NetBox installation. And because they're completely custom, there is no inherent limitation on what a script can accomplish.
|
||||
|
||||
!!! danger "Only install trusted scripts"
|
||||
Custom scripts have unrestricted access to change anything in the databse and are inherently unsafe and should only be installed and run from trusted sources. You should also review and set permissions for who can run scripts if the script can modify any data.
|
||||
|
||||
## Writing Custom Scripts
|
||||
|
||||
All custom scripts must inherit from the `extras.scripts.Script` base class. This class provides the functionality necessary to generate forms and log activity.
|
||||
|
@ -62,22 +62,7 @@ $issue-$description
|
||||
|
||||
The description should be just two or three words to imply the focus of the work being performed. For example, bug #1234 to fix a TypeError exception when creating a device might be named `1234-device-typerror`. This ensures that branches are always follow some logical ordering (e.g. when running `git branch -a`) and helps other developers quickly identify the purpose of each.
|
||||
|
||||
### 3. Enable Pre-Commit Hooks
|
||||
|
||||
NetBox ships with a [git pre-commit hook](https://githooks.com/) script that automatically checks for style compliance and missing database migrations prior to committing changes. This helps avoid erroneous commits that result in CI test failures. You are encouraged to enable it by creating a link to `scripts/git-hooks/pre-commit`:
|
||||
|
||||
```no-highlight
|
||||
cd .git/hooks/
|
||||
ln -s ../../scripts/git-hooks/pre-commit
|
||||
```
|
||||
For the pre-commit hooks to work, you will also need to install the pycodestyle package:
|
||||
|
||||
```no-highlight
|
||||
python -m pip install pycodestyle
|
||||
```
|
||||
...and set up the yarn packages as shown in the [Web UI Development Guide](web-ui.md)
|
||||
|
||||
### 4. Create a Python Virtual Environment
|
||||
### 3. Create a Python Virtual Environment
|
||||
|
||||
A [virtual environment](https://docs.python.org/3/tutorial/venv.html) (or "venv" for short) is like a container for a set of Python packages. These allow you to build environments suited to specific projects without interfering with system packages or other projects. When installed per the documentation, NetBox uses a virtual environment in production.
|
||||
|
||||
@ -101,7 +86,7 @@ source ~/.venv/netbox/bin/activate
|
||||
|
||||
Notice that the console prompt changes to indicate the active environment. This updates the necessary system environment variables to ensure that any Python scripts are run within the virtual environment.
|
||||
|
||||
### 5. Install Required Packages
|
||||
### 4. Install Required Packages
|
||||
|
||||
With the virtual environment activated, install the project's required Python packages using the `pip` module. Required packages are defined in `requirements.txt`. Each line in this file specifies the name and specific version of a required package.
|
||||
|
||||
@ -109,6 +94,26 @@ With the virtual environment activated, install the project's required Python pa
|
||||
python -m pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 5. Install Pre-Commit
|
||||
|
||||
NetBox uses [`pre-commit`](https://pre-commit.com/) to automatically validate code when commiting new changes. This includes the following operations:
|
||||
|
||||
* Run the `ruff` Python linter
|
||||
* Run Django's internal system check
|
||||
* Check for missing database migrations
|
||||
* Validate any changes to the documentation with `mkdocs`
|
||||
* Validate Typescript & Sass styling with `yarn`
|
||||
* Ensure that any modified static front end assets have been recompiled
|
||||
|
||||
Enable `pre-commit` with the following commands _prior_ to commiting any changes:
|
||||
|
||||
```no-highlight
|
||||
python -m pip install ruff pre-commit
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
You may also need to set up the yarn packages as shown in the [Web UI Development Guide](web-ui.md).
|
||||
|
||||
### 6. Configure NetBox
|
||||
|
||||
Within the `netbox/netbox/` directory, copy `configuration_example.py` to `configuration.py` and update the following parameters:
|
||||
|
@ -39,6 +39,10 @@ mkdocs serve
|
||||
|
||||
Follow these instructions to perform a new installation of NetBox in a temporary environment. This process must not be automated: The goal of this step is to catch any errors or omissions in the documentation, and ensure that it is kept up-to-date for each release. Make any necessary changes to the documentation before proceeding with the release.
|
||||
|
||||
### Test Upgrade Paths
|
||||
|
||||
Upgrading from a previous version typically involves database migrations, which must work without errors. Supported upgrade paths include from one minor version to another within the same major version (i.e. 4.0 to 4.1), as well as from the latest patch version of the previous minor version (i.e. 3.7 to 4.0 or to 4.1). Prior to release, test all these supported paths by loading demo data from the source version and performing a `./manage.py migrate`.
|
||||
|
||||
### Merge the Release Branch
|
||||
|
||||
Submit a pull request to merge the `feature` branch into the `develop` branch in preparation for its release. Once it has been merged, continue with the section for patch releases below.
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Style Guide
|
||||
|
||||
NetBox generally follows the [Django style guide](https://docs.djangoproject.com/en/stable/internals/contributing/writing-code/coding-style/), which is itself based on [PEP 8](https://www.python.org/dev/peps/pep-0008/). [Pycodestyle](https://github.com/pycqa/pycodestyle) is used to validate code formatting, ignoring certain violations.
|
||||
NetBox generally follows the [Django style guide](https://docs.djangoproject.com/en/stable/internals/contributing/writing-code/coding-style/), which is itself based on [PEP 8](https://www.python.org/dev/peps/pep-0008/). [ruff](https://docs.astral.sh/ruff/) is used for linting (with certain [exceptions](#linter-exceptions)).
|
||||
|
||||
## Code
|
||||
|
||||
@ -20,32 +20,32 @@ NetBox generally follows the [Django style guide](https://docs.djangoproject.com
|
||||
|
||||
* Nested API serializers generate minimal representations of an object. These are stored separately from the primary serializers to avoid circular dependencies. Always import nested serializers from other apps directly. For example, from within the DCIM app you would write `from ipam.api.nested_serializers import NestedIPAddressSerializer`.
|
||||
|
||||
### PEP 8 Exceptions
|
||||
### Linting
|
||||
|
||||
NetBox ignores certain PEP8 assertions. These are listed below.
|
||||
The [ruff](https://docs.astral.sh/ruff/) linter is used to enforce code style. A [pre-commit hook](./getting-started.md#3-enable-pre-commit-hooks) which runs this automatically is included with NetBox. To invoke `ruff` manually, run:
|
||||
|
||||
#### Wildcard Imports
|
||||
```
|
||||
ruff check netbox/
|
||||
```
|
||||
|
||||
#### Linter Exceptions
|
||||
|
||||
The following rules are ignored when linting.
|
||||
|
||||
##### [E501](https://docs.astral.sh/ruff/rules/line-too-long/): Line too long
|
||||
|
||||
NetBox does not enforce a hard restriction on line length, although a maximum length of 120 characters is strongly encouraged for Python code where possible. The maximum length does not apply to HTML templates or to automatically generated code (e.g. database migrations).
|
||||
|
||||
##### [F403](https://docs.astral.sh/ruff/rules/undefined-local-with-import-star/): Undefined local with import star
|
||||
|
||||
Wildcard imports (for example, `from .constants import *`) are acceptable under any of the following conditions:
|
||||
|
||||
* The library being import contains only constant declarations (e.g. `constants.py`)
|
||||
* The library being imported explicitly defines `__all__`
|
||||
|
||||
#### Maximum Line Length (E501)
|
||||
##### [F405](https://docs.astral.sh/ruff/rules/undefined-local-with-import-star-usage/): Undefined local with import star usage
|
||||
|
||||
NetBox does not restrict lines to a maximum length of 79 characters. We use a maximum line length of 120 characters, however this is not enforced by CI. The maximum length does not apply to HTML templates or to automatically generated code (e.g. database migrations).
|
||||
|
||||
#### Line Breaks Following Binary Operators (W504)
|
||||
|
||||
Line breaks are permitted following binary operators.
|
||||
|
||||
### Enforcing Code Style
|
||||
|
||||
The [`pycodestyle`](https://pypi.org/project/pycodestyle/) utility (formerly `pep8`) is used by the CI process to enforce code style. A [pre-commit hook](./getting-started.md#3-enable-pre-commit-hooks) which runs this automatically is included with NetBox. To invoke `pycodestyle` manually, run:
|
||||
|
||||
```
|
||||
pycodestyle --ignore=W504,E501 netbox/
|
||||
```
|
||||
The justification for ignoring this rule is the same as F403 above.
|
||||
|
||||
### Introducing New Dependencies
|
||||
|
||||
|
@ -41,7 +41,7 @@ NetBox integrates with the open source [python-social-auth](https://github.com/p
|
||||
* Google
|
||||
* Hashicorp Vault
|
||||
* Keycloak
|
||||
* Microsoft Azure AD
|
||||
* Microsoft Entra ID
|
||||
* Microsoft Graph
|
||||
* Okta
|
||||
* OIDC
|
||||
|
@ -13,6 +13,9 @@ To enable remote data synchronization, the NetBox administrator first designates
|
||||
!!! info
|
||||
Data backends which connect to external sources typically require the installation of one or more supporting Python libraries. The Git backend requires the [`dulwich`](https://www.dulwich.io/) package, and the S3 backend requires the [`boto3`](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html) package. These must be installed within NetBox's environment to enable these backends.
|
||||
|
||||
!!! info
|
||||
If you are configuring Git and have `HTTP_PROXIES` configured to use the SOCKS protocol, you will also need to install the [`python_socks`](https://pypi.org/project/python-socks/) Python library.
|
||||
|
||||
Each type of remote source has its own configuration parameters. For instance, a git source will ask the user to specify a branch and authentication credentials. Once the source has been created, a synchronization job is run to automatically replicate remote files in the local database.
|
||||
|
||||
The following NetBox models can be associated with replicated data files:
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 6.8 KiB |
Binary file not shown.
Before Width: | Height: | Size: 3.8 KiB |
@ -36,6 +36,12 @@ The operational status of the circuit. By default, the following statuses are av
|
||||
!!! tip "Custom circuit statuses"
|
||||
Additional circuit statuses may be defined by setting `Circuit.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
|
||||
|
||||
### Distance
|
||||
|
||||
!!! info "This field was introduced in NetBox v4.2."
|
||||
|
||||
The distance between the circuit's two endpoints, including a unit designation (e.g. 100 meters or 25 feet).
|
||||
|
||||
### Description
|
||||
|
||||
A brief description of the circuit.
|
||||
|
@ -44,3 +44,7 @@ The serial number assigned by the manufacturer.
|
||||
### Asset Tag
|
||||
|
||||
A unique, locally-administered label used to identify hardware resources.
|
||||
|
||||
### Status
|
||||
|
||||
The inventory item's operational status.
|
||||
|
@ -29,6 +29,10 @@ An alternative physical label identifying the power outlet.
|
||||
|
||||
The type of power outlet.
|
||||
|
||||
### Color
|
||||
|
||||
The power outlet's color (optional).
|
||||
|
||||
### Power Port
|
||||
|
||||
When modeling a device which redistributes power from an upstream supply, such as a power distribution unit (PDU), each power outlet should be mapped to the respective [power port](./powerport.md) on the device which supplies power. For example, a 24-outlet PDU may two power ports, each distributing power to 12 of its outlets.
|
||||
|
@ -1,5 +1,8 @@
|
||||
# Branches
|
||||
|
||||
!!! danger "Deprecated Feature"
|
||||
This feature has been deprecated in NetBox v4.2 and will be removed in a future release. Please consider using the [netbox-branching plugin](https://github.com/netboxlabs/netbox-branching), which provides much more robust functionality.
|
||||
|
||||
A branch is a collection of related [staged changes](./stagedchange.md) that have been prepared for merging into the active database. A branch can be merged by executing its `commit()` method. Deleting a branch will delete all its related changes.
|
||||
|
||||
## Fields
|
||||
|
@ -1,5 +1,8 @@
|
||||
# Staged Changes
|
||||
|
||||
!!! danger "Deprecated Feature"
|
||||
This feature has been deprecated in NetBox v4.2 and will be removed in a future release. Please consider using the [netbox-branching plugin](https://github.com/netboxlabs/netbox-branching), which provides much more robust functionality.
|
||||
|
||||
A staged change represents the creation of a new object or the modification or deletion of an existing object to be performed at some future point. Each change must be assigned to a [branch](./branch.md).
|
||||
|
||||
Changes can be applied individually via the `apply()` method, however it is recommended to apply changes in bulk using the parent branch's `commit()` method.
|
||||
|
@ -1,6 +1,6 @@
|
||||
# ASNs
|
||||
|
||||
An Autonomous System Number (ASN) is a numeric identifier used in the BGP protocol to identify which [autonomous system](https://en.wikipedia.org/wiki/Autonomous_system_%28Internet%29) a particular prefix is originating and transiting through. NetBox support both 32- and 64- ASNs.
|
||||
An Autonomous System Number (ASN) is a numeric identifier used in the Border Gateway Protocol (BGP) to identify which [autonomous system](https://en.wikipedia.org/wiki/Autonomous_system_%28Internet%29) a particular prefix is originating from or transiting through. NetBox supports both 16- and 32-bit ASNs.
|
||||
|
||||
ASNs must be globally unique within NetBox, and may be allocated from within a [defined range](./asnrange.md). Each ASN may be assigned to multiple [sites](../dcim/site.md).
|
||||
|
||||
@ -8,7 +8,7 @@ ASNs must be globally unique within NetBox, and may be allocated from within a [
|
||||
|
||||
### AS Number
|
||||
|
||||
The 32- or 64-bit AS number.
|
||||
The 16- or 32-bit AS number.
|
||||
|
||||
### RIR
|
||||
|
||||
|
@ -28,6 +28,7 @@ The technology employed in forming and operating the L2VPN. Choices include:
|
||||
* VXLAN-EVPN
|
||||
* MPLS-EVPN
|
||||
* PBB-EVPN
|
||||
* EVPN-VPWS
|
||||
|
||||
!!! note
|
||||
Designating the type as VPWS, EPL, EP-LAN, EP-TREE will limit the L2VPN instance to two terminations.
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Staged Changes
|
||||
|
||||
!!! danger "Experimental Feature"
|
||||
This feature is still under active development and considered experimental in nature. Its use in production is strongly discouraged at this time.
|
||||
!!! danger "Deprecated Feature"
|
||||
This feature has been deprecated in NetBox v4.2 and will be removed in a future release. Please consider using the [netbox-branching plugin](https://github.com/netboxlabs/netbox-branching), which provides much more robust functionality.
|
||||
|
||||
NetBox provides a programmatic API to stage the creation, modification, and deletion of objects without actually committing those changes to the active database. This can be useful for performing a "dry run" of bulk operations, or preparing a set of changes for administrative approval, for example.
|
||||
|
||||
|
@ -1,6 +1,108 @@
|
||||
# NetBox v4.1
|
||||
|
||||
## v4.1.0 (FUTURE)
|
||||
## v4.1.4 (2024-10-15)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#11671](https://github.com/netbox-community/netbox/issues/11671) - Display device's rack position in cable traces
|
||||
* [#15829](https://github.com/netbox-community/netbox/issues/15829) - Rename Microsoft Azure AD SSO backend to Microsoft Entra ID
|
||||
* [#16009](https://github.com/netbox-community/netbox/issues/16009) - Float form & bulk operation buttons within UI
|
||||
* [#17079](https://github.com/netbox-community/netbox/issues/17079) - Introduce additional choices for device airflow direction
|
||||
* [#17216](https://github.com/netbox-community/netbox/issues/17216) - Add EVPN-VPWS L2VPN type
|
||||
* [#17655](https://github.com/netbox-community/netbox/issues/17655) - Limit the display of tagged VLANs within interface tables
|
||||
* [#17669](https://github.com/netbox-community/netbox/issues/17669) - Enable filtering VLANs by assigned device or VM interface
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#16024](https://github.com/netbox-community/netbox/issues/16024) - Fix AND/OR filtering in GraphQL API for selection fields
|
||||
* [#17400](https://github.com/netbox-community/netbox/issues/17400) - Fix cable tracing across split paths
|
||||
* [#17562](https://github.com/netbox-community/netbox/issues/17562) - Fix GraphQL API query support for custom field choices
|
||||
* [#17566](https://github.com/netbox-community/netbox/issues/17566) - Fix AttributeError exception resulting from background jobs with no associated object type
|
||||
* [#17614](https://github.com/netbox-community/netbox/issues/17614) - Disallow removal of a master device from its virtual chassis
|
||||
* [#17636](https://github.com/netbox-community/netbox/issues/17636) - Fix filtering of related objects when adding a power port, rear port, or inventory item template to a device type
|
||||
* [#17644](https://github.com/netbox-community/netbox/issues/17644) - Correct sizing of logo & SSO icons on login page
|
||||
* [#17648](https://github.com/netbox-community/netbox/issues/17648) - Fix AttributeError exception when attempting to delete a background job under certain conditions
|
||||
* [#17663](https://github.com/netbox-community/netbox/issues/17663) - Fix extended lookups for choice field filters
|
||||
* [#17671](https://github.com/netbox-community/netbox/issues/17671) - Fix the display of rack types in global search results
|
||||
* [#17713](https://github.com/netbox-community/netbox/issues/17713) - Fix UnboundLocalError exception when attempting to sync data source in parallel
|
||||
|
||||
---
|
||||
|
||||
## v4.1.3 (2024-10-02)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#17639](https://github.com/netbox-community/netbox/issues/17639) - Add SOCKS support to proxy settings for Git remote data sources
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#17558](https://github.com/netbox-community/netbox/issues/17558) - Raise validation error when attempting to remove a custom field choice in use
|
||||
|
||||
---
|
||||
|
||||
## v4.1.2 (2024-09-26)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#14201](https://github.com/netbox-community/netbox/issues/14201) - Enable global search for AS numbers using "AS" prefix
|
||||
* [#15408](https://github.com/netbox-community/netbox/issues/15408) - Enable bulk import of primary IPv4 & IPv6 addresses for virtual device contexts (VDCs)
|
||||
* [#16781](https://github.com/netbox-community/netbox/issues/16781) - Add 100Base-X SFP interface type
|
||||
* [#17255](https://github.com/netbox-community/netbox/issues/17255) - Include return URL when creating new IP address from prefix IPs list
|
||||
* [#17471](https://github.com/netbox-community/netbox/issues/17471) - Add Eaton C39 power outlet type
|
||||
* [#17482](https://github.com/netbox-community/netbox/issues/17482) - Do not preload Branch & StagedChange models in `nbshell`
|
||||
* [#17550](https://github.com/netbox-community/netbox/issues/17550) - Add IEEE 802.15.4 wireless interface type
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#16837](https://github.com/netbox-community/netbox/issues/16837) - Fix filtering of cables with no type assigned
|
||||
* [#17083](https://github.com/netbox-community/netbox/issues/17083) - Trim clickable area of form field labels
|
||||
* [#17126](https://github.com/netbox-community/netbox/issues/17126) - Show total device weight in both imperial & metric units
|
||||
* [#17360](https://github.com/netbox-community/netbox/issues/17360) - Fix AttributeError under child object views when experimental HTMX navigation is enabled
|
||||
* [#17406](https://github.com/netbox-community/netbox/issues/17406) - Fix the cleanup of stale custom field data after removing a plugin
|
||||
* [#17419](https://github.com/netbox-community/netbox/issues/17419) - Rebuild MPTT for module bays on upgrade to v4.1
|
||||
* [#17492](https://github.com/netbox-community/netbox/issues/17492) - Fix URL resolution in `NetBoxModelSerializer` for plugin models
|
||||
* [#17497](https://github.com/netbox-community/netbox/issues/17497) - Fix uncaught FieldError exception when referencing an invalid field on a related object during bulk import
|
||||
* [#17498](https://github.com/netbox-community/netbox/issues/17498) - Fix MultipleObjectsReturned exception when importing a device type without uniquely specifying a manufacturer
|
||||
* [#17501](https://github.com/netbox-community/netbox/issues/17501) - Fix reporting of last run time & status for custom scripts under UI
|
||||
* [#17511](https://github.com/netbox-community/netbox/issues/17511) - Restore consistent font support for non-Latin characters
|
||||
* [#17517](https://github.com/netbox-community/netbox/issues/17517) - Fix cable termination selection after switching termination type
|
||||
* [#17521](https://github.com/netbox-community/netbox/issues/17521) - Correct text color in notification pop-ups under dark mode
|
||||
* [#17522](https://github.com/netbox-community/netbox/issues/17522) - Fix language translation of form field labels under user preferences
|
||||
* [#17537](https://github.com/netbox-community/netbox/issues/17537) - Fix global search support for ASN range names
|
||||
* [#17555](https://github.com/netbox-community/netbox/issues/17555) - Fix toggling disconnected interfaces under device view
|
||||
* [#17601](https://github.com/netbox-community/netbox/issues/17601) - Record change to terminating object when disconnecting a cable
|
||||
* [#17605](https://github.com/netbox-community/netbox/issues/17605) - Fix calculation of aggregate VM disk space under cluster view
|
||||
* [#17611](https://github.com/netbox-community/netbox/issues/17611) - Correct custom field minimum value validation error message
|
||||
|
||||
---
|
||||
|
||||
## v4.1.1 (2024-09-12)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#16926](https://github.com/netbox-community/netbox/issues/16926) - Add USB front & rear port types
|
||||
* [#17347](https://github.com/netbox-community/netbox/issues/17347) - Add NEMA L22-20 power port & outlet types
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#17066](https://github.com/netbox-community/netbox/issues/17066) - Fix OpenAPI schema definition for custom scripts REST API endpoint
|
||||
* [#17332](https://github.com/netbox-community/netbox/issues/17332) - Restore pagination for object list dashboard widgets
|
||||
* [#17333](https://github.com/netbox-community/netbox/issues/17333) - Avoid prefetching all jobs when retrieving custom scripts via the REST API
|
||||
* [#17353](https://github.com/netbox-community/netbox/issues/17353) - Fix styling of map buttons under site and device views
|
||||
* [#17354](https://github.com/netbox-community/netbox/issues/17354) - Prevent object & multi-object custom fields from breaking bulk import forms
|
||||
* [#17362](https://github.com/netbox-community/netbox/issues/17362) - Remove duplicate prefixes & IP addresses returned by the `present_in_vrf` query filter
|
||||
* [#17364](https://github.com/netbox-community/netbox/issues/17364) - Fix rendering of Markdown tables inside object list dashboard widgets
|
||||
* [#17387](https://github.com/netbox-community/netbox/issues/17387) - Fix display of the changelog tab for users with sufficient permission
|
||||
* [#17410](https://github.com/netbox-community/netbox/issues/17410) - Enable debug toolbar middleware for `strawberry-django` only when `DEBUG` is true
|
||||
* [#17414](https://github.com/netbox-community/netbox/issues/17414) - Fix support for declaring individual VLAN IDs within a VLAN group
|
||||
* [#17431](https://github.com/netbox-community/netbox/issues/17431) - Fix database migration error when upgrading to v4.1 from v3.7 or earlier
|
||||
* [#17437](https://github.com/netbox-community/netbox/issues/17437) - Fix exception when specifying a bridge relationship on an interface template
|
||||
* [#17444](https://github.com/netbox-community/netbox/issues/17444) - Custom script fails to execute when triggered by an event rule
|
||||
* [#17457](https://github.com/netbox-community/netbox/issues/17457) - GraphQL `service_list` filter should not require a port number
|
||||
|
||||
---
|
||||
|
||||
## v4.1.0 (2024-09-03)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
|
@ -156,7 +156,7 @@ nav:
|
||||
- Administration:
|
||||
- Authentication:
|
||||
- Overview: 'administration/authentication/overview.md'
|
||||
- Microsoft Azure AD: 'administration/authentication/microsoft-azure-ad.md'
|
||||
- Microsoft Entra ID: 'administration/authentication/microsoft-entra-id.md'
|
||||
- Okta: 'administration/authentication/okta.md'
|
||||
- Permissions: 'administration/permissions.md'
|
||||
- Error Reporting: 'administration/error-reporting.md'
|
||||
|
@ -18,7 +18,7 @@ __all__ = [
|
||||
|
||||
# TODO: Remove in v4.2
|
||||
warnings.warn(
|
||||
f"Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
|
||||
"Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
|
||||
DeprecationWarning
|
||||
)
|
||||
|
||||
|
@ -4,6 +4,7 @@ from dcim.api.serializers_.cables import CabledObjectSerializer
|
||||
from dcim.api.serializers_.sites import SiteSerializer
|
||||
from netbox.api.fields import ChoiceField, RelatedObjectCountField
|
||||
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
|
||||
from netbox.choices import DistanceUnitChoices
|
||||
from tenancy.api.serializers_.tenants import TenantSerializer
|
||||
|
||||
from .providers import ProviderAccountSerializer, ProviderNetworkSerializer, ProviderSerializer
|
||||
@ -80,13 +81,14 @@ class CircuitSerializer(NetBoxModelSerializer):
|
||||
termination_a = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
|
||||
termination_z = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
|
||||
assignments = CircuitGroupAssignmentSerializer_(nested=True, many=True, required=False)
|
||||
distance_unit = ChoiceField(choices=DistanceUnitChoices, allow_blank=True, required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant',
|
||||
'install_date', 'termination_date', 'commit_rate', 'description', 'termination_a', 'termination_z',
|
||||
'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'assignments',
|
||||
'distance', 'distance_unit', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'assignments',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'provider', 'cid', 'description')
|
||||
|
||||
|
@ -7,7 +7,7 @@ class CircuitsConfig(AppConfig):
|
||||
|
||||
def ready(self):
|
||||
from netbox.models.features import register_models
|
||||
from . import signals, search
|
||||
from . import signals, search # noqa: F401
|
||||
|
||||
# Register models
|
||||
register_models(*self.get_models())
|
||||
|
@ -239,7 +239,7 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = ('id', 'cid', 'description', 'install_date', 'termination_date', 'commit_rate')
|
||||
fields = ('id', 'cid', 'description', 'install_date', 'termination_date', 'commit_rate', 'distance', 'distance_unit')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
|
@ -5,6 +5,7 @@ from circuits.choices import CircuitCommitRateChoices, CircuitPriorityChoices, C
|
||||
from circuits.models import *
|
||||
from dcim.models import Site
|
||||
from ipam.models import ASN
|
||||
from netbox.choices import DistanceUnitChoices
|
||||
from netbox.forms import NetBoxModelBulkEditForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import add_blank_choice
|
||||
@ -160,6 +161,17 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
|
||||
options=CircuitCommitRateChoices
|
||||
)
|
||||
)
|
||||
distance = forms.DecimalField(
|
||||
label=_('Distance'),
|
||||
min_value=0,
|
||||
required=False
|
||||
)
|
||||
distance_unit = forms.ChoiceField(
|
||||
label=_('Distance unit'),
|
||||
choices=add_blank_choice(DistanceUnitChoices),
|
||||
required=False,
|
||||
initial=''
|
||||
)
|
||||
description = forms.CharField(
|
||||
label=_('Description'),
|
||||
max_length=100,
|
||||
@ -171,6 +183,7 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
|
||||
fieldsets = (
|
||||
FieldSet('provider', 'type', 'status', 'description', name=_('Circuit')),
|
||||
FieldSet('provider_account', 'install_date', 'termination_date', 'commit_rate', name=_('Service Parameters')),
|
||||
FieldSet('distance', 'distance_unit', name=_('Attributes')),
|
||||
FieldSet('tenant', name=_('Tenancy')),
|
||||
)
|
||||
nullable_fields = (
|
||||
|
@ -1,10 +1,10 @@
|
||||
from django import forms
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from circuits.choices import *
|
||||
from circuits.models import *
|
||||
from dcim.models import Site
|
||||
from netbox.choices import DistanceUnitChoices
|
||||
from netbox.forms import NetBoxModelImportForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, SlugField
|
||||
@ -95,6 +95,12 @@ class CircuitImportForm(NetBoxModelImportForm):
|
||||
choices=CircuitStatusChoices,
|
||||
help_text=_('Operational status')
|
||||
)
|
||||
distance_unit = CSVChoiceField(
|
||||
label=_('Distance unit'),
|
||||
choices=DistanceUnitChoices,
|
||||
required=False,
|
||||
help_text=_('Distance unit')
|
||||
)
|
||||
tenant = CSVModelChoiceField(
|
||||
label=_('Tenant'),
|
||||
queryset=Tenant.objects.all(),
|
||||
@ -107,7 +113,7 @@ class CircuitImportForm(NetBoxModelImportForm):
|
||||
model = Circuit
|
||||
fields = [
|
||||
'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date', 'termination_date',
|
||||
'commit_rate', 'description', 'comments', 'tags'
|
||||
'commit_rate', 'distance', 'distance_unit', 'description', 'comments', 'tags'
|
||||
]
|
||||
|
||||
|
||||
|
@ -5,8 +5,10 @@ from circuits.choices import CircuitCommitRateChoices, CircuitPriorityChoices, C
|
||||
from circuits.models import *
|
||||
from dcim.models import Region, Site, SiteGroup
|
||||
from ipam.models import ASN
|
||||
from netbox.choices import DistanceUnitChoices
|
||||
from netbox.forms import NetBoxModelFilterSetForm
|
||||
from tenancy.forms import TenancyFilterForm, ContactModelFilterForm
|
||||
from utilities.forms import add_blank_choice
|
||||
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.widgets import DatePicker, NumberWithOptions
|
||||
@ -114,7 +116,7 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
|
||||
FieldSet('type_id', 'status', 'install_date', 'termination_date', 'commit_rate', name=_('Attributes')),
|
||||
FieldSet('type_id', 'status', 'install_date', 'termination_date', 'commit_rate', 'distance', 'distance_unit', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
@ -188,6 +190,15 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
|
||||
options=CircuitCommitRateChoices
|
||||
)
|
||||
)
|
||||
distance = forms.DecimalField(
|
||||
label=_('Distance'),
|
||||
required=False,
|
||||
)
|
||||
distance_unit = forms.ChoiceField(
|
||||
label=_('Distance unit'),
|
||||
choices=add_blank_choice(DistanceUnitChoices),
|
||||
required=False
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
|
@ -7,7 +7,7 @@ from ipam.models import ASN
|
||||
from netbox.forms import NetBoxModelForm
|
||||
from tenancy.forms import TenancyForm
|
||||
from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
|
||||
from utilities.forms.rendering import FieldSet, TabbedGroups
|
||||
from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups
|
||||
from utilities.forms.widgets import DatePicker, NumberWithOptions
|
||||
|
||||
__all__ = (
|
||||
@ -108,7 +108,17 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
|
||||
comments = CommentField()
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('provider', 'provider_account', 'cid', 'type', 'status', 'description', 'tags', name=_('Circuit')),
|
||||
FieldSet(
|
||||
'provider',
|
||||
'provider_account',
|
||||
'cid',
|
||||
'type',
|
||||
'status',
|
||||
InlineFields('distance', 'distance_unit', label=_('Distance')),
|
||||
'description',
|
||||
'tags',
|
||||
name=_('Circuit')
|
||||
),
|
||||
FieldSet('install_date', 'termination_date', 'commit_rate', name=_('Service Parameters')),
|
||||
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
|
||||
)
|
||||
@ -117,7 +127,7 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
|
||||
model = Circuit
|
||||
fields = [
|
||||
'cid', 'type', 'provider', 'provider_account', 'status', 'install_date', 'termination_date', 'commit_rate',
|
||||
'description', 'tenant_group', 'tenant', 'comments', 'tags',
|
||||
'distance', 'distance_unit', 'description', 'tenant_group', 'tenant', 'comments', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'install_date': DatePicker(),
|
||||
|
@ -1,7 +1,6 @@
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from circuits import filtersets, models
|
||||
|
||||
from circuits import filtersets, models
|
||||
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
|
||||
|
||||
__all__ = (
|
||||
|
28
netbox/circuits/migrations/0045_circuit_distance.py
Normal file
28
netbox/circuits/migrations/0045_circuit_distance.py
Normal file
@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.0.9 on 2024-09-26 22:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0044_circuit_groups'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='_abs_distance',
|
||||
field=models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='distance',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='distance_unit',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
]
|
@ -6,6 +6,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from circuits.choices import *
|
||||
from dcim.models import CabledObjectModel
|
||||
from netbox.models import ChangeLoggedModel, OrganizationalModel, PrimaryModel
|
||||
from netbox.models.mixins import DistanceMixin
|
||||
from netbox.models.features import ContactsMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, ImageAttachmentsMixin, TagsMixin
|
||||
from utilities.fields import ColorField
|
||||
|
||||
@ -28,16 +29,13 @@ class CircuitType(OrganizationalModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:circuittype', args=[self.pk])
|
||||
|
||||
class Meta:
|
||||
ordering = ('name',)
|
||||
verbose_name = _('circuit type')
|
||||
verbose_name_plural = _('circuit types')
|
||||
|
||||
|
||||
class Circuit(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
|
||||
class Circuit(ContactsMixin, ImageAttachmentsMixin, DistanceMixin, PrimaryModel):
|
||||
"""
|
||||
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
|
||||
circuits. Each circuit is also assigned a CircuitType and a Site, and may optionally be assigned to a particular
|
||||
@ -140,9 +138,6 @@ class Circuit(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
|
||||
def __str__(self):
|
||||
return self.cid
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:circuit', args=[self.pk])
|
||||
|
||||
def get_status_color(self):
|
||||
return CircuitStatusChoices.colors.get(self.status)
|
||||
|
||||
@ -173,9 +168,6 @@ class CircuitGroup(OrganizationalModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:circuitgroup', args=[self.pk])
|
||||
|
||||
|
||||
class CircuitGroupAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):
|
||||
"""
|
||||
|
@ -1,6 +1,5 @@
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.models import PrimaryModel
|
||||
@ -45,9 +44,6 @@ class Provider(ContactsMixin, PrimaryModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:provider', args=[self.pk])
|
||||
|
||||
|
||||
class ProviderAccount(ContactsMixin, PrimaryModel):
|
||||
"""
|
||||
@ -91,9 +87,6 @@ class ProviderAccount(ContactsMixin, PrimaryModel):
|
||||
return f'{self.account} ({self.name})'
|
||||
return f'{self.account}'
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:provideraccount', args=[self.pk])
|
||||
|
||||
|
||||
class ProviderNetwork(PrimaryModel):
|
||||
"""
|
||||
@ -128,6 +121,3 @@ class ProviderNetwork(PrimaryModel):
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:providernetwork', args=[self.pk])
|
||||
|
@ -76,6 +76,7 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
commit_rate = CommitRateColumn(
|
||||
verbose_name=_('Commit Rate')
|
||||
)
|
||||
distance = columns.DistanceColumn()
|
||||
comments = columns.MarkdownColumn(
|
||||
verbose_name=_('Comments')
|
||||
)
|
||||
|
@ -5,6 +5,7 @@ from circuits.filtersets import *
|
||||
from circuits.models import *
|
||||
from dcim.models import Cable, Region, Site, SiteGroup
|
||||
from ipam.models import ASN, RIR
|
||||
from netbox.choices import DistanceUnitChoices
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.testing import ChangeLoggedFilterSetTests
|
||||
|
||||
@ -222,9 +223,9 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
ProviderNetwork.objects.bulk_create(provider_networks)
|
||||
|
||||
circuits = (
|
||||
Circuit(provider=providers[0], provider_account=provider_accounts[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 1', install_date='2020-01-01', termination_date='2021-01-01', commit_rate=1000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar1'),
|
||||
Circuit(provider=providers[0], provider_account=provider_accounts[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 2', install_date='2020-01-02', termination_date='2021-01-02', commit_rate=2000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar2'),
|
||||
Circuit(provider=providers[0], provider_account=provider_accounts[1], tenant=tenants[1], type=circuit_types[0], cid='Test Circuit 3', install_date='2020-01-03', termination_date='2021-01-03', commit_rate=3000, status=CircuitStatusChoices.STATUS_PLANNED),
|
||||
Circuit(provider=providers[0], provider_account=provider_accounts[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 1', install_date='2020-01-01', termination_date='2021-01-01', commit_rate=1000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar1', distance=10, distance_unit=DistanceUnitChoices.UNIT_FOOT),
|
||||
Circuit(provider=providers[0], provider_account=provider_accounts[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 2', install_date='2020-01-02', termination_date='2021-01-02', commit_rate=2000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar2', distance=20, distance_unit=DistanceUnitChoices.UNIT_METER),
|
||||
Circuit(provider=providers[0], provider_account=provider_accounts[1], tenant=tenants[1], type=circuit_types[0], cid='Test Circuit 3', install_date='2020-01-03', termination_date='2021-01-03', commit_rate=3000, status=CircuitStatusChoices.STATUS_PLANNED, distance=30, distance_unit=DistanceUnitChoices.UNIT_METER),
|
||||
Circuit(provider=providers[1], provider_account=provider_accounts[1], tenant=tenants[1], type=circuit_types[1], cid='Test Circuit 4', install_date='2020-01-04', termination_date='2021-01-04', commit_rate=4000, status=CircuitStatusChoices.STATUS_PLANNED),
|
||||
Circuit(provider=providers[1], provider_account=provider_accounts[2], tenant=tenants[2], type=circuit_types[1], cid='Test Circuit 5', install_date='2020-01-05', termination_date='2021-01-05', commit_rate=5000, status=CircuitStatusChoices.STATUS_OFFLINE),
|
||||
Circuit(provider=providers[1], provider_account=provider_accounts[2], tenant=tenants[2], type=circuit_types[1], cid='Test Circuit 6', install_date='2020-01-06', termination_date='2021-01-06', commit_rate=6000, status=CircuitStatusChoices.STATUS_OFFLINE),
|
||||
@ -289,6 +290,14 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'status': [CircuitStatusChoices.STATUS_ACTIVE, CircuitStatusChoices.STATUS_PLANNED]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_distance(self):
|
||||
params = {'distance': [10, 20]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_distance_unit(self):
|
||||
params = {'distance_unit': DistanceUnitChoices.UNIT_FOOT}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
@ -171,7 +171,7 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
f"id,cid,description,status",
|
||||
"id,cid,description,status",
|
||||
f"{circuits[0].pk},Circuit 7,New description7,{CircuitStatusChoices.STATUS_DECOMMISSIONED}",
|
||||
f"{circuits[1].pk},Circuit 8,New description8,{CircuitStatusChoices.STATUS_DECOMMISSIONED}",
|
||||
f"{circuits[2].pk},Circuit 9,New description9,{CircuitStatusChoices.STATUS_DECOMMISSIONED}",
|
||||
|
@ -16,7 +16,7 @@ __all__ = (
|
||||
|
||||
# TODO: Remove in v4.2
|
||||
warnings.warn(
|
||||
f"Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
|
||||
"Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
|
||||
DeprecationWarning
|
||||
)
|
||||
|
||||
|
@ -8,10 +8,8 @@ from drf_spectacular.plumbing import (
|
||||
build_basic_type, build_choice_field, build_media_type_object, build_object_type, get_doc,
|
||||
)
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from rest_framework import serializers
|
||||
from rest_framework.relations import ManyRelatedField
|
||||
|
||||
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
|
||||
from netbox.api.fields import ChoiceField
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
|
||||
# see netbox.api.routers.NetBoxRouter
|
||||
|
@ -1,5 +1,3 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from core.choices import *
|
||||
from core.models import Job
|
||||
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||
|
@ -16,9 +16,9 @@ class CoreConfig(AppConfig):
|
||||
name = "core"
|
||||
|
||||
def ready(self):
|
||||
from core.api import schema # noqa
|
||||
from core.api import schema # noqa: F401
|
||||
from netbox.models.features import register_models
|
||||
from . import data_backends, events, search
|
||||
from . import data_backends, events, search # noqa: F401
|
||||
|
||||
# Register models
|
||||
register_models(*self.get_models())
|
||||
|
@ -8,10 +8,13 @@ from urllib.parse import urlparse
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from netbox.data_backends import DataBackend
|
||||
from netbox.utils import register_data_backend
|
||||
from utilities.constants import HTTP_PROXY_SUPPORTED_SCHEMAS, HTTP_PROXY_SUPPORTED_SOCK_SCHEMAS
|
||||
from utilities.socks import ProxyPoolManager
|
||||
from .exceptions import SyncError
|
||||
|
||||
__all__ = (
|
||||
@ -31,7 +34,7 @@ class LocalBackend(DataBackend):
|
||||
|
||||
@contextmanager
|
||||
def fetch(self):
|
||||
logger.debug(f"Data source type is local; skipping fetch")
|
||||
logger.debug("Data source type is local; skipping fetch")
|
||||
local_path = urlparse(self.url).path # Strip file:// scheme
|
||||
|
||||
yield local_path
|
||||
@ -67,11 +70,18 @@ class GitBackend(DataBackend):
|
||||
|
||||
# Initialize backend config
|
||||
config = ConfigDict()
|
||||
self.use_socks = False
|
||||
|
||||
# Apply HTTP proxy (if configured)
|
||||
if settings.HTTP_PROXIES and self.url_scheme in ('http', 'https'):
|
||||
if proxy := settings.HTTP_PROXIES.get(self.url_scheme):
|
||||
config.set("http", "proxy", proxy)
|
||||
if settings.HTTP_PROXIES:
|
||||
if proxy := settings.HTTP_PROXIES.get(self.url_scheme, None):
|
||||
if urlparse(proxy).scheme not in HTTP_PROXY_SUPPORTED_SCHEMAS:
|
||||
raise ImproperlyConfigured(f"Unsupported Git DataSource proxy scheme: {urlparse(proxy).scheme}")
|
||||
|
||||
if self.url_scheme in ('http', 'https'):
|
||||
config.set("http", "proxy", proxy)
|
||||
if urlparse(proxy).scheme in HTTP_PROXY_SUPPORTED_SOCK_SCHEMAS:
|
||||
self.use_socks = True
|
||||
|
||||
return config
|
||||
|
||||
@ -87,6 +97,10 @@ class GitBackend(DataBackend):
|
||||
"errstream": porcelain.NoneStream(),
|
||||
}
|
||||
|
||||
# check if using socks for proxy - if so need to use custom pool_manager
|
||||
if self.use_socks:
|
||||
clone_args['pool_manager'] = ProxyPoolManager(settings.HTTP_PROXIES.get(self.url_scheme))
|
||||
|
||||
if self.url_scheme in ('http', 'https'):
|
||||
if self.params.get('username'):
|
||||
clone_args.update(
|
||||
|
@ -15,7 +15,7 @@ __all__ = (
|
||||
class ChangelogMixin:
|
||||
|
||||
@strawberry_django.field
|
||||
def changelog(self, info) -> List[Annotated["ObjectChangeType", strawberry.lazy('.types')]]:
|
||||
def changelog(self, info) -> List[Annotated["ObjectChangeType", strawberry.lazy('.types')]]: # noqa: F821
|
||||
content_type = ContentType.objects.get_for_model(self)
|
||||
object_changes = ObjectChange.objects.filter(
|
||||
changed_object_type=content_type,
|
||||
|
@ -11,6 +11,10 @@ from core.models import ObjectType
|
||||
from users.models import User
|
||||
|
||||
APPS = ('circuits', 'core', 'dcim', 'extras', 'ipam', 'tenancy', 'users', 'virtualization', 'vpn', 'wireless')
|
||||
EXCLUDE_MODELS = (
|
||||
'extras.branch',
|
||||
'extras.stagedchange',
|
||||
)
|
||||
|
||||
BANNER_TEXT = """### NetBox interactive shell ({node})
|
||||
### Python {python} | Django {django} | NetBox {netbox}
|
||||
@ -44,12 +48,16 @@ class Command(BaseCommand):
|
||||
|
||||
# Gather Django models and constants from each app
|
||||
for app in APPS:
|
||||
self.django_models[app] = []
|
||||
models = []
|
||||
|
||||
# Load models from each app
|
||||
for model in apps.get_app_config(app).get_models():
|
||||
namespace[model.__name__] = model
|
||||
self.django_models[app].append(model.__name__)
|
||||
app_label = model._meta.app_label
|
||||
model_name = model._meta.model_name
|
||||
if f'{app_label}.{model_name}' not in EXCLUDE_MODELS:
|
||||
namespace[model.__name__] = model
|
||||
models.append(model.__name__)
|
||||
self.django_models[app] = sorted(models)
|
||||
|
||||
# Constants
|
||||
try:
|
||||
|
@ -26,7 +26,7 @@ class Command(BaseCommand):
|
||||
if invalid_names := set(options['name']) - found_names:
|
||||
raise CommandError(f"Invalid data source names: {', '.join(invalid_names)}")
|
||||
else:
|
||||
raise CommandError(f"Must specify at least one data source, or set --all.")
|
||||
raise CommandError("Must specify at least one data source, or set --all.")
|
||||
|
||||
if len(options['name']) > 1:
|
||||
self.stdout.write(f"Syncing {len(datasources)} data sources.")
|
||||
@ -43,4 +43,4 @@ class Command(BaseCommand):
|
||||
raise e
|
||||
|
||||
if len(options['name']) > 1:
|
||||
self.stdout.write(f"Finished.")
|
||||
self.stdout.write("Finished.")
|
||||
|
@ -84,9 +84,6 @@ class DataSource(JobsMixin, PrimaryModel):
|
||||
def __str__(self):
|
||||
return f'{self.name}'
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('core:datasource', args=[self.pk])
|
||||
|
||||
@property
|
||||
def docs_url(self):
|
||||
return f'{settings.STATIC_URL}docs/models/{self._meta.app_label}/{self._meta.model_name}/'
|
||||
@ -125,7 +122,7 @@ class DataSource(JobsMixin, PrimaryModel):
|
||||
# Ensure URL scheme matches selected type
|
||||
if self.backend_class.is_local and self.url_scheme not in ('file', ''):
|
||||
raise ValidationError({
|
||||
'source_url': f"URLs for local sources must start with file:// (or specify no scheme)"
|
||||
'source_url': "URLs for local sources must start with file:// (or specify no scheme)"
|
||||
})
|
||||
|
||||
def to_objectchange(self, action):
|
||||
@ -201,7 +198,7 @@ class DataSource(JobsMixin, PrimaryModel):
|
||||
logger.debug(f"Updated {updated_count} files")
|
||||
|
||||
# Bulk delete deleted files
|
||||
deleted_count, _ = DataFile.objects.filter(pk__in=deleted_file_ids).delete()
|
||||
deleted_count, __ = DataFile.objects.filter(pk__in=deleted_file_ids).delete()
|
||||
logger.debug(f"Deleted {deleted_count} files")
|
||||
|
||||
# Walk the local replication to find new files
|
||||
|
@ -13,8 +13,6 @@ from django.utils.translation import gettext as _
|
||||
from core.choices import JobStatusChoices
|
||||
from core.models import ObjectType
|
||||
from core.signals import job_end, job_start
|
||||
from netbox.config import get_config
|
||||
from netbox.constants import RQ_QUEUE_DEFAULT
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.rqworker import get_queue_for_model
|
||||
|
||||
@ -118,10 +116,11 @@ class Job(models.Model):
|
||||
|
||||
def get_absolute_url(self):
|
||||
# TODO: Employ dynamic registration
|
||||
if self.object_type.model == 'reportmodule':
|
||||
return reverse(f'extras:report_result', kwargs={'job_pk': self.pk})
|
||||
if self.object_type.model == 'scriptmodule':
|
||||
return reverse(f'extras:script_result', kwargs={'job_pk': self.pk})
|
||||
if self.object_type:
|
||||
if self.object_type.model == 'reportmodule':
|
||||
return reverse('extras:report_result', kwargs={'job_pk': self.pk})
|
||||
elif self.object_type.model == 'scriptmodule':
|
||||
return reverse('extras:script_result', kwargs={'job_pk': self.pk})
|
||||
return reverse('core:job', args=[self.pk])
|
||||
|
||||
def get_status_color(self):
|
||||
@ -154,7 +153,7 @@ class Job(models.Model):
|
||||
def delete(self, *args, **kwargs):
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
rq_queue_name = get_config().QUEUE_MAPPINGS.get(self.object_type.model, RQ_QUEUE_DEFAULT)
|
||||
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)
|
||||
job = queue.fetch_job(str(self.job_id))
|
||||
|
||||
|
@ -56,7 +56,7 @@ __all__ = [
|
||||
|
||||
# TODO: Remove in v4.2
|
||||
warnings.warn(
|
||||
f"Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
|
||||
"Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
|
||||
DeprecationWarning
|
||||
)
|
||||
|
||||
|
@ -156,7 +156,7 @@ class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, Conne
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'power_port',
|
||||
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'power_port',
|
||||
'feed_leg', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
|
||||
'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', '_occupied',
|
||||
@ -364,11 +364,12 @@ class InventoryItemSerializer(NetBoxModelSerializer):
|
||||
)
|
||||
component = serializers.SerializerMethodField(read_only=True, allow_null=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
status = ChoiceField(choices=InventoryItemStatusChoices, required=False)
|
||||
|
||||
class Meta:
|
||||
model = InventoryItem
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'device', 'parent', 'name', 'label', 'role', 'manufacturer',
|
||||
'id', 'url', 'display_url', 'display', 'device', 'parent', 'name', 'label', 'status', 'role', 'manufacturer',
|
||||
'part_id', 'serial', 'asset_tag', 'discovered', 'description', 'component_type', 'component_id',
|
||||
'component', 'tags', 'custom_fields', 'created', 'last_updated', '_depth',
|
||||
]
|
||||
|
@ -7,6 +7,7 @@ from dcim.choices import *
|
||||
from dcim.models import DeviceType, ModuleType
|
||||
from netbox.api.fields import ChoiceField, RelatedObjectCountField
|
||||
from netbox.api.serializers import NetBoxModelSerializer
|
||||
from netbox.choices import *
|
||||
from .manufacturers import ManufacturerSerializer
|
||||
from .platforms import PlatformSerializer
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from dcim.models import Manufacturer
|
||||
from netbox.api.fields import RelatedObjectCountField
|
||||
from netbox.api.serializers import NetBoxModelSerializer
|
||||
|
@ -72,7 +72,7 @@ class NestedInterfaceTemplateSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.InterfaceTemplate
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name']
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedDeviceBaySerializer(WritableNestedSerializer):
|
||||
|
@ -1,5 +1,3 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from dcim.models import Platform
|
||||
from extras.api.serializers_.configtemplates import ConfigTemplateSerializer
|
||||
from netbox.api.fields import RelatedObjectCountField
|
||||
|
@ -1,5 +1,3 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.models import PowerFeed, PowerPanel
|
||||
from netbox.api.fields import ChoiceField, RelatedObjectCountField
|
||||
|
@ -6,6 +6,7 @@ from dcim.constants import *
|
||||
from dcim.models import Rack, RackReservation, RackRole, RackType
|
||||
from netbox.api.fields import ChoiceField, RelatedObjectCountField
|
||||
from netbox.api.serializers import NetBoxModelSerializer
|
||||
from netbox.choices import *
|
||||
from netbox.config import ConfigItem
|
||||
from tenancy.api.serializers_.tenants import TenantSerializer
|
||||
from users.api.serializers_.users import UserSerializer
|
||||
|
@ -1,5 +1,3 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from dcim.models import DeviceRole, InventoryItemRole
|
||||
from extras.api.serializers_.configtemplates import ConfigTemplateSerializer
|
||||
from netbox.api.fields import RelatedObjectCountField
|
||||
|
@ -10,7 +10,7 @@ class DCIMConfig(AppConfig):
|
||||
def ready(self):
|
||||
from netbox.models.features import register_models
|
||||
from utilities.counters import connect_counters
|
||||
from . import signals, search
|
||||
from . import signals, search # noqa: F401
|
||||
from .models import CableTermination, Device, DeviceType, VirtualChassis
|
||||
|
||||
# Register models
|
||||
|
@ -197,6 +197,9 @@ class DeviceAirflowChoices(ChoiceSet):
|
||||
AIRFLOW_LEFT_TO_RIGHT = 'left-to-right'
|
||||
AIRFLOW_RIGHT_TO_LEFT = 'right-to-left'
|
||||
AIRFLOW_SIDE_TO_REAR = 'side-to-rear'
|
||||
AIRFLOW_REAR_TO_SIDE = 'rear-to-side'
|
||||
AIRFLOW_BOTTOM_TO_TOP = 'bottom-to-top'
|
||||
AIRFLOW_TOP_TO_BOTTOM = 'top-to-bottom'
|
||||
AIRFLOW_PASSIVE = 'passive'
|
||||
AIRFLOW_MIXED = 'mixed'
|
||||
|
||||
@ -206,6 +209,9 @@ class DeviceAirflowChoices(ChoiceSet):
|
||||
(AIRFLOW_LEFT_TO_RIGHT, _('Left to right')),
|
||||
(AIRFLOW_RIGHT_TO_LEFT, _('Right to left')),
|
||||
(AIRFLOW_SIDE_TO_REAR, _('Side to rear')),
|
||||
(AIRFLOW_REAR_TO_SIDE, _('Rear to side')),
|
||||
(AIRFLOW_BOTTOM_TO_TOP, _('Bottom to top')),
|
||||
(AIRFLOW_TOP_TO_BOTTOM, _('Top to bottom')),
|
||||
(AIRFLOW_PASSIVE, _('Passive')),
|
||||
(AIRFLOW_MIXED, _('Mixed')),
|
||||
)
|
||||
@ -396,6 +402,7 @@ class PowerPortTypeChoices(ChoiceSet):
|
||||
TYPE_NEMA_L1560P = 'nema-l15-60p'
|
||||
TYPE_NEMA_L2120P = 'nema-l21-20p'
|
||||
TYPE_NEMA_L2130P = 'nema-l21-30p'
|
||||
TYPE_NEMA_L2220P = 'nema-l22-20p'
|
||||
TYPE_NEMA_L2230P = 'nema-l22-30p'
|
||||
# California style
|
||||
TYPE_CS6361C = 'cs6361c'
|
||||
@ -517,6 +524,7 @@ class PowerPortTypeChoices(ChoiceSet):
|
||||
(TYPE_NEMA_L1560P, 'NEMA L15-60P'),
|
||||
(TYPE_NEMA_L2120P, 'NEMA L21-20P'),
|
||||
(TYPE_NEMA_L2130P, 'NEMA L21-30P'),
|
||||
(TYPE_NEMA_L2220P, 'NEMA L22-20P'),
|
||||
(TYPE_NEMA_L2230P, 'NEMA L22-30P'),
|
||||
)),
|
||||
(_('California Style'), (
|
||||
@ -649,6 +657,7 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
TYPE_NEMA_L1560R = 'nema-l15-60r'
|
||||
TYPE_NEMA_L2120R = 'nema-l21-20r'
|
||||
TYPE_NEMA_L2130R = 'nema-l21-30r'
|
||||
TYPE_NEMA_L2220R = 'nema-l22-20r'
|
||||
TYPE_NEMA_L2230R = 'nema-l22-30r'
|
||||
# California style
|
||||
TYPE_CS6360C = 'CS6360C'
|
||||
@ -681,6 +690,7 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
# Direct current (DC)
|
||||
TYPE_DC = 'dc-terminal'
|
||||
# Proprietary
|
||||
TYPE_EATON_C39 = 'eaton-c39'
|
||||
TYPE_HDOT_CX = 'hdot-cx'
|
||||
TYPE_SAF_D_GRID = 'saf-d-grid'
|
||||
TYPE_NEUTRIK_POWERCON_20A = 'neutrik-powercon-20a'
|
||||
@ -763,6 +773,7 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
(TYPE_NEMA_L1560R, 'NEMA L15-60R'),
|
||||
(TYPE_NEMA_L2120R, 'NEMA L21-20R'),
|
||||
(TYPE_NEMA_L2130R, 'NEMA L21-30R'),
|
||||
(TYPE_NEMA_L2220R, 'NEMA L22-20R'),
|
||||
(TYPE_NEMA_L2230R, 'NEMA L22-30R'),
|
||||
)),
|
||||
(_('California Style'), (
|
||||
@ -801,6 +812,7 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
(TYPE_DC, 'DC Terminal'),
|
||||
)),
|
||||
(_('Proprietary'), (
|
||||
(TYPE_EATON_C39, 'Eaton C39'),
|
||||
(TYPE_HDOT_CX, 'HDOT Cx'),
|
||||
(TYPE_SAF_D_GRID, 'Saf-D-Grid'),
|
||||
(TYPE_NEUTRIK_POWERCON_20A, 'Neutrik powerCON (20A)'),
|
||||
@ -857,6 +869,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
TYPE_100ME_LFX = '100base-lfx'
|
||||
TYPE_100ME_FIXED = '100base-tx'
|
||||
TYPE_100ME_T1 = '100base-t1'
|
||||
TYPE_100ME_SFP = '100base-x-sfp'
|
||||
TYPE_1GE_FIXED = '1000base-t'
|
||||
TYPE_1GE_TX_FIXED = '1000base-tx'
|
||||
TYPE_1GE_GBIC = '1000base-x-gbic'
|
||||
@ -918,6 +931,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
TYPE_80211AY = 'ieee802.11ay'
|
||||
TYPE_80211BE = 'ieee802.11be'
|
||||
TYPE_802151 = 'ieee802.15.1'
|
||||
TYPE_802154 = 'ieee802.15.4'
|
||||
TYPE_OTHER_WIRELESS = 'other-wireless'
|
||||
|
||||
# Cellular
|
||||
@ -1029,6 +1043,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
(
|
||||
_('Ethernet (modular)'),
|
||||
(
|
||||
(TYPE_100ME_SFP, 'SFP (100ME)'),
|
||||
(TYPE_1GE_GBIC, 'GBIC (1GE)'),
|
||||
(TYPE_1GE_SFP, 'SFP (1GE)'),
|
||||
(TYPE_10GE_SFP_PLUS, 'SFP+ (10GE)'),
|
||||
@ -1090,6 +1105,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
(TYPE_80211AY, 'IEEE 802.11ay'),
|
||||
(TYPE_80211BE, 'IEEE 802.11be'),
|
||||
(TYPE_802151, 'IEEE 802.15.1 (Bluetooth)'),
|
||||
(TYPE_802154, 'IEEE 802.15.4 (LR-WPAN)'),
|
||||
(TYPE_OTHER_WIRELESS, 'Other (Wireless)'),
|
||||
)
|
||||
),
|
||||
@ -1347,6 +1363,14 @@ class PortTypeChoices(ChoiceSet):
|
||||
TYPE_URM_P2 = 'urm-p2'
|
||||
TYPE_URM_P4 = 'urm-p4'
|
||||
TYPE_URM_P8 = 'urm-p8'
|
||||
TYPE_USB_A = 'usb-a'
|
||||
TYPE_USB_B = 'usb-b'
|
||||
TYPE_USB_C = 'usb-c'
|
||||
TYPE_USB_MINI_A = 'usb-mini-a'
|
||||
TYPE_USB_MINI_B = 'usb-mini-b'
|
||||
TYPE_USB_MICRO_A = 'usb-micro-a'
|
||||
TYPE_USB_MICRO_B = 'usb-micro-b'
|
||||
TYPE_USB_MICRO_AB = 'usb-micro-ab'
|
||||
TYPE_OTHER = 'other'
|
||||
|
||||
CHOICES = (
|
||||
@ -1406,6 +1430,19 @@ class PortTypeChoices(ChoiceSet):
|
||||
(TYPE_SPLICE, 'Splice'),
|
||||
),
|
||||
),
|
||||
(
|
||||
_('USB'),
|
||||
(
|
||||
(TYPE_USB_A, 'USB Type A'),
|
||||
(TYPE_USB_B, 'USB Type B'),
|
||||
(TYPE_USB_C, 'USB Type C'),
|
||||
(TYPE_USB_MINI_A, 'USB Mini A'),
|
||||
(TYPE_USB_MINI_B, 'USB Mini B'),
|
||||
(TYPE_USB_MICRO_A, 'USB Micro A'),
|
||||
(TYPE_USB_MICRO_B, 'USB Micro B'),
|
||||
(TYPE_USB_MICRO_AB, 'USB Micro AB'),
|
||||
),
|
||||
),
|
||||
(
|
||||
_('Other'),
|
||||
(
|
||||
@ -1444,6 +1481,7 @@ class CableTypeChoices(ChoiceSet):
|
||||
TYPE_SMF_OS2 = 'smf-os2'
|
||||
TYPE_AOC = 'aoc'
|
||||
TYPE_POWER = 'power'
|
||||
TYPE_USB = 'usb'
|
||||
|
||||
CHOICES = (
|
||||
(
|
||||
@ -1476,6 +1514,7 @@ class CableTypeChoices(ChoiceSet):
|
||||
(TYPE_AOC, 'Active Optical Cabling (AOC)'),
|
||||
),
|
||||
),
|
||||
(TYPE_USB, _('USB')),
|
||||
(TYPE_POWER, _('Power')),
|
||||
)
|
||||
|
||||
@ -1515,24 +1554,6 @@ class CableLengthUnitChoices(ChoiceSet):
|
||||
)
|
||||
|
||||
|
||||
class WeightUnitChoices(ChoiceSet):
|
||||
|
||||
# Metric
|
||||
UNIT_KILOGRAM = 'kg'
|
||||
UNIT_GRAM = 'g'
|
||||
|
||||
# Imperial
|
||||
UNIT_POUND = 'lb'
|
||||
UNIT_OUNCE = 'oz'
|
||||
|
||||
CHOICES = (
|
||||
(UNIT_KILOGRAM, _('Kilograms')),
|
||||
(UNIT_GRAM, _('Grams')),
|
||||
(UNIT_POUND, _('Pounds')),
|
||||
(UNIT_OUNCE, _('Ounces')),
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# CableTerminations
|
||||
#
|
||||
@ -1617,3 +1638,27 @@ class VirtualDeviceContextStatusChoices(ChoiceSet):
|
||||
(STATUS_PLANNED, _('Planned'), 'cyan'),
|
||||
(STATUS_OFFLINE, _('Offline'), 'red'),
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# InventoryItem
|
||||
#
|
||||
|
||||
class InventoryItemStatusChoices(ChoiceSet):
|
||||
key = 'InventoryItem.status'
|
||||
|
||||
STATUS_OFFLINE = 'offline'
|
||||
STATUS_ACTIVE = 'active'
|
||||
STATUS_PLANNED = 'planned'
|
||||
STATUS_STAGED = 'staged'
|
||||
STATUS_FAILED = 'failed'
|
||||
STATUS_DECOMMISSIONING = 'decommissioning'
|
||||
|
||||
CHOICES = [
|
||||
(STATUS_OFFLINE, _('Offline'), 'gray'),
|
||||
(STATUS_ACTIVE, _('Active'), 'green'),
|
||||
(STATUS_PLANNED, _('Planned'), 'cyan'),
|
||||
(STATUS_STAGED, _('Staged'), 'blue'),
|
||||
(STATUS_FAILED, _('Failed'), 'red'),
|
||||
(STATUS_DECOMMISSIONING, _('Decommissioning'), 'yellow'),
|
||||
]
|
||||
|
@ -51,6 +51,7 @@ WIRELESS_IFACE_TYPES = [
|
||||
InterfaceTypeChoices.TYPE_80211AY,
|
||||
InterfaceTypeChoices.TYPE_80211BE,
|
||||
InterfaceTypeChoices.TYPE_802151,
|
||||
InterfaceTypeChoices.TYPE_802154,
|
||||
InterfaceTypeChoices.TYPE_OTHER_WIRELESS,
|
||||
]
|
||||
|
||||
|
@ -271,7 +271,7 @@ class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalM
|
||||
|
||||
class Meta:
|
||||
model = Location
|
||||
fields = ('id', 'name', 'slug', 'status', 'facility', 'description')
|
||||
fields = ('id', 'name', 'slug', 'facility', 'description')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@ -1594,7 +1594,7 @@ class PowerOutletFilterSet(
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = (
|
||||
'id', 'name', 'label', 'feed_leg', 'description', 'mark_connected', 'cable_end',
|
||||
'id', 'name', 'label', 'feed_leg', 'description', 'color', 'mark_connected', 'cable_end',
|
||||
)
|
||||
|
||||
|
||||
@ -1860,10 +1860,14 @@ class InventoryItemFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet):
|
||||
serial = MultiValueCharFilter(
|
||||
lookup_expr='iexact'
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=InventoryItemStatusChoices,
|
||||
null_value=None
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InventoryItem
|
||||
fields = ('id', 'name', 'label', 'part_id', 'asset_tag', 'description', 'discovered')
|
||||
fields = ('id', 'name', 'label', 'part_id', 'asset_tag', 'status', 'description', 'discovered')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
|
@ -69,7 +69,7 @@ class PowerPortBulkCreateForm(
|
||||
|
||||
|
||||
class PowerOutletBulkCreateForm(
|
||||
form_from_model(PowerOutlet, ['type', 'feed_leg', 'mark_connected']),
|
||||
form_from_model(PowerOutlet, ['type', 'color', 'feed_leg', 'mark_connected']),
|
||||
DeviceBulkAddComponentForm
|
||||
):
|
||||
model = PowerOutlet
|
||||
|
@ -8,6 +8,7 @@ from dcim.constants import *
|
||||
from dcim.models import *
|
||||
from extras.models import ConfigTemplate
|
||||
from ipam.models import ASN, VLAN, VLANGroup, VRF
|
||||
from netbox.choices import *
|
||||
from netbox.forms import NetBoxModelBulkEditForm
|
||||
from tenancy.models import Tenant
|
||||
from users.models import User
|
||||
@ -1361,7 +1362,7 @@ class PowerPortBulkEditForm(
|
||||
|
||||
class PowerOutletBulkEditForm(
|
||||
ComponentBulkEditForm,
|
||||
form_from_model(PowerOutlet, ['label', 'type', 'feed_leg', 'power_port', 'mark_connected', 'description'])
|
||||
form_from_model(PowerOutlet, ['label', 'type', 'color', 'feed_leg', 'power_port', 'mark_connected', 'description'])
|
||||
):
|
||||
mark_connected = forms.NullBooleanField(
|
||||
label=_('Mark connected'),
|
||||
@ -1371,7 +1372,7 @@ class PowerOutletBulkEditForm(
|
||||
|
||||
model = PowerOutlet
|
||||
fieldsets = (
|
||||
FieldSet('module', 'type', 'label', 'description', 'mark_connected'),
|
||||
FieldSet('module', 'type', 'label', 'description', 'mark_connected', 'color'),
|
||||
FieldSet('feed_leg', 'power_port', name=_('Power')),
|
||||
)
|
||||
nullable_fields = ('module', 'label', 'type', 'feed_leg', 'power_port', 'description')
|
||||
@ -1661,10 +1662,16 @@ class InventoryItemBulkEditForm(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False
|
||||
)
|
||||
status = forms.ChoiceField(
|
||||
label=_('Status'),
|
||||
choices=add_blank_choice(InventoryItemStatusChoices),
|
||||
required=False,
|
||||
initial=''
|
||||
)
|
||||
|
||||
model = InventoryItem
|
||||
fieldsets = (
|
||||
FieldSet('device', 'label', 'role', 'manufacturer', 'part_id', 'description'),
|
||||
FieldSet('device', 'label', 'role', 'manufacturer', 'part_id', 'status', 'description'),
|
||||
)
|
||||
nullable_fields = ('label', 'role', 'manufacturer', 'part_id', 'description')
|
||||
|
||||
|
@ -9,7 +9,8 @@ from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import *
|
||||
from extras.models import ConfigTemplate
|
||||
from ipam.models import VRF
|
||||
from ipam.models import VRF, IPAddress
|
||||
from netbox.choices import *
|
||||
from netbox.forms import NetBoxModelImportForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms.fields import (
|
||||
@ -367,13 +368,13 @@ class ManufacturerImportForm(NetBoxModelImportForm):
|
||||
|
||||
|
||||
class DeviceTypeImportForm(NetBoxModelImportForm):
|
||||
manufacturer = forms.ModelChoiceField(
|
||||
manufacturer = CSVModelChoiceField(
|
||||
label=_('Manufacturer'),
|
||||
queryset=Manufacturer.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text=_('The manufacturer which produces this device type')
|
||||
)
|
||||
default_platform = forms.ModelChoiceField(
|
||||
default_platform = CSVModelChoiceField(
|
||||
label=_('Default platform'),
|
||||
queryset=Platform.objects.all(),
|
||||
to_field_name='name',
|
||||
@ -798,7 +799,7 @@ class PowerOutletImportForm(NetBoxModelImportForm):
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = ('device', 'name', 'label', 'type', 'mark_connected', 'power_port', 'feed_leg', 'description', 'tags')
|
||||
fields = ('device', 'name', 'label', 'type', 'color', 'mark_connected', 'power_port', 'feed_leg', 'description', 'tags')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -1103,11 +1104,16 @@ class InventoryItemImportForm(NetBoxModelImportForm):
|
||||
required=False,
|
||||
help_text=_('Component Name')
|
||||
)
|
||||
status = CSVChoiceField(
|
||||
label=_('Status'),
|
||||
choices=InventoryItemStatusChoices,
|
||||
help_text=_('Operational status')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InventoryItem
|
||||
fields = (
|
||||
'device', 'name', 'label', 'role', 'manufacturer', 'parent', 'part_id', 'serial', 'asset_tag', 'discovered',
|
||||
'device', 'name', 'label', 'status', 'role', 'manufacturer', 'parent', 'part_id', 'serial', 'asset_tag', 'discovered',
|
||||
'description', 'tags', 'component_type', 'component_name',
|
||||
)
|
||||
|
||||
@ -1435,9 +1441,33 @@ class VirtualDeviceContextImportForm(NetBoxModelImportForm):
|
||||
label=_('Status'),
|
||||
choices=VirtualDeviceContextStatusChoices,
|
||||
)
|
||||
primary_ip4 = CSVModelChoiceField(
|
||||
label=_('Primary IPv4'),
|
||||
queryset=IPAddress.objects.all(),
|
||||
required=False,
|
||||
to_field_name='address',
|
||||
help_text=_('IPv4 address with mask, e.g. 1.2.3.4/24')
|
||||
)
|
||||
primary_ip6 = CSVModelChoiceField(
|
||||
label=_('Primary IPv6'),
|
||||
queryset=IPAddress.objects.all(),
|
||||
required=False,
|
||||
to_field_name='address',
|
||||
help_text=_('IPv6 address with prefix length, e.g. 2001:db8::1/64')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
fields = [
|
||||
'name', 'device', 'status', 'tenant', 'identifier', 'comments',
|
||||
'name', 'device', 'status', 'tenant', 'identifier', 'comments', 'primary_ip4', 'primary_ip6',
|
||||
]
|
||||
model = VirtualDeviceContext
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
super().__init__(data, *args, **kwargs)
|
||||
|
||||
if data:
|
||||
|
||||
# Limit primary_ip4/ip6 querysets by assigned device
|
||||
params = {f"interface__device__{self.fields['device'].to_field_name}": data.get('device')}
|
||||
self.fields['primary_ip4'].queryset = self.fields['primary_ip4'].queryset.filter(**params)
|
||||
self.fields['primary_ip6'].queryset = self.fields['primary_ip6'].queryset.filter(**params)
|
||||
|
@ -4,7 +4,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from circuits.models import Circuit, CircuitTermination
|
||||
from dcim.models import *
|
||||
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
||||
from utilities.forms.fields import DynamicModelMultipleChoiceField
|
||||
from .model_forms import CableForm
|
||||
|
||||
|
||||
|
@ -7,6 +7,7 @@ from dcim.models import *
|
||||
from extras.forms import LocalConfigContextFilterForm
|
||||
from extras.models import ConfigTemplate
|
||||
from ipam.models import ASN, VRF
|
||||
from netbox.choices import *
|
||||
from netbox.forms import NetBoxModelFilterSetForm
|
||||
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
|
||||
from users.models import User
|
||||
@ -35,7 +36,6 @@ __all__ = (
|
||||
'LocationFilterForm',
|
||||
'ManufacturerFilterForm',
|
||||
'ModuleFilterForm',
|
||||
'ModuleFilterForm',
|
||||
'ModuleBayFilterForm',
|
||||
'ModuleTypeFilterForm',
|
||||
'PlatformFilterForm',
|
||||
@ -1304,7 +1304,7 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
model = PowerOutlet
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', 'label', 'type', name=_('Attributes')),
|
||||
FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
@ -1318,6 +1318,10 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
required=False
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
color = ColorField(
|
||||
label=_('Color'),
|
||||
required=False
|
||||
)
|
||||
|
||||
|
||||
class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
@ -1553,6 +1557,11 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
label=_('Status'),
|
||||
choices=InventoryItemStatusChoices,
|
||||
required=False
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
|
@ -954,7 +954,7 @@ class PowerOutletTemplateForm(ModularComponentTemplateForm):
|
||||
queryset=PowerPortTemplate.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'devicetype_id': '$device_type',
|
||||
'device_type_id': '$device_type',
|
||||
}
|
||||
)
|
||||
|
||||
@ -975,8 +975,8 @@ class InterfaceTemplateForm(ModularComponentTemplateForm):
|
||||
queryset=InterfaceTemplate.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'devicetype_id': '$device_type',
|
||||
'moduletype_id': '$module_type',
|
||||
'device_type_id': '$device_type',
|
||||
'module_type_id': '$module_type',
|
||||
}
|
||||
)
|
||||
|
||||
@ -1001,8 +1001,8 @@ class FrontPortTemplateForm(ModularComponentTemplateForm):
|
||||
queryset=RearPortTemplate.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'devicetype_id': '$device_type',
|
||||
'moduletype_id': '$module_type',
|
||||
'device_type_id': '$device_type',
|
||||
'module_type_id': '$module_type',
|
||||
}
|
||||
)
|
||||
|
||||
@ -1063,7 +1063,7 @@ class InventoryItemTemplateForm(ComponentTemplateForm):
|
||||
queryset=InventoryItemTemplate.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'devicetype_id': '$device_type'
|
||||
'device_type_id': '$device_type'
|
||||
}
|
||||
)
|
||||
role = DynamicModelChoiceField(
|
||||
@ -1285,7 +1285,7 @@ class PowerOutletForm(ModularDeviceComponentForm):
|
||||
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg', 'mark_connected', 'description',
|
||||
'device', 'module', 'name', 'label', 'type', 'color', 'power_port', 'feed_leg', 'mark_connected', 'description',
|
||||
'tags',
|
||||
),
|
||||
)
|
||||
@ -1293,7 +1293,7 @@ class PowerOutletForm(ModularDeviceComponentForm):
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = [
|
||||
'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg', 'mark_connected', 'description',
|
||||
'device', 'module', 'name', 'label', 'type', 'color', 'power_port', 'feed_leg', 'mark_connected', 'description',
|
||||
'tags',
|
||||
]
|
||||
|
||||
@ -1351,7 +1351,8 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
||||
vlan_group = DynamicModelChoiceField(
|
||||
queryset=VLANGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('VLAN group')
|
||||
label=_('VLAN group'),
|
||||
help_text=_("Filter VLANs available for assignment by group.")
|
||||
)
|
||||
untagged_vlan = DynamicModelChoiceField(
|
||||
queryset=VLAN.objects.all(),
|
||||
@ -1575,7 +1576,7 @@ class InventoryItemForm(DeviceComponentForm):
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('device', 'parent', 'name', 'label', 'role', 'description', 'tags', name=_('Inventory Item')),
|
||||
FieldSet('device', 'parent', 'name', 'label', 'status', 'role', 'description', 'tags', name=_('Inventory Item')),
|
||||
FieldSet('manufacturer', 'part_id', 'serial', 'asset_tag', name=_('Hardware')),
|
||||
FieldSet(
|
||||
TabbedGroups(
|
||||
@ -1595,7 +1596,7 @@ class InventoryItemForm(DeviceComponentForm):
|
||||
model = InventoryItem
|
||||
fields = [
|
||||
'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
|
||||
'description', 'tags',
|
||||
'status', 'description', 'tags',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
@ -261,8 +261,8 @@ class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
|
||||
# TODO: Clean up the application of HTMXSelect attributes
|
||||
attrs={
|
||||
'hx-get': '.',
|
||||
'hx-include': f'#form_fields',
|
||||
'hx-target': f'#form_fields',
|
||||
'hx-include': '#form_fields',
|
||||
'hx-target': '#form_fields',
|
||||
}
|
||||
)
|
||||
)
|
||||
|
@ -1,7 +1,6 @@
|
||||
from typing import Annotated, List, Union
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
__all__ = (
|
||||
'CabledObjectMixin',
|
||||
@ -11,18 +10,18 @@ __all__ = (
|
||||
|
||||
@strawberry.type
|
||||
class CabledObjectMixin:
|
||||
cable: Annotated["CableType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
cable: Annotated["CableType", strawberry.lazy('dcim.graphql.types')] | None # noqa: F821
|
||||
|
||||
link_peers: List[Annotated[Union[
|
||||
Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')],
|
||||
Annotated["ConsolePortType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["ConsoleServerPortType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["PowerFeedType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["PowerOutletType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')], # noqa: F821
|
||||
Annotated["ConsolePortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
Annotated["ConsoleServerPortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
Annotated["PowerFeedType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
Annotated["PowerOutletType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
], strawberry.union("LinkPeerType")]]
|
||||
|
||||
|
||||
@ -30,14 +29,14 @@ class CabledObjectMixin:
|
||||
class PathEndpointMixin:
|
||||
|
||||
connected_endpoints: List[Annotated[Union[
|
||||
Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')],
|
||||
Annotated["ConsolePortType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["ConsoleServerPortType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["PowerFeedType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["PowerOutletType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')],
|
||||
Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')], # noqa: F821
|
||||
Annotated["ConsolePortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
Annotated["ConsoleServerPortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
Annotated["PowerFeedType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
Annotated["PowerOutletType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')], # noqa: F821
|
||||
Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
], strawberry.union("ConnectedEndpointType")]]
|
||||
|
@ -568,6 +568,7 @@ class PowerFeedType(NetBoxObjectType, CabledObjectMixin, PathEndpointMixin):
|
||||
)
|
||||
class PowerOutletType(ModularComponentType, CabledObjectMixin, PathEndpointMixin):
|
||||
power_port: Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
color: str
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
|
@ -60,7 +60,7 @@ class Command(BaseCommand):
|
||||
self.stdout.write((self.style.SUCCESS(f' Deleted {deleted_count} paths')))
|
||||
|
||||
# Reinitialize the model's PK sequence
|
||||
self.stdout.write(f'Resetting database sequence for CablePath model')
|
||||
self.stdout.write('Resetting database sequence for CablePath model')
|
||||
sequence_sql = connection.ops.sequence_reset_sql(no_style(), [CablePath])
|
||||
with connection.cursor() as cursor:
|
||||
for sql in sequence_sql:
|
||||
|
26
netbox/dcim/migrations/0191_module_bay_rebuild.py
Normal file
26
netbox/dcim/migrations/0191_module_bay_rebuild.py
Normal file
@ -0,0 +1,26 @@
|
||||
from django.db import migrations
|
||||
import mptt
|
||||
import mptt.managers
|
||||
|
||||
|
||||
def rebuild_mptt(apps, schema_editor):
|
||||
manager = mptt.managers.TreeManager()
|
||||
ModuleBay = apps.get_model('dcim', 'ModuleBay')
|
||||
manager.model = ModuleBay
|
||||
mptt.register(ModuleBay)
|
||||
manager.contribute_to_class(ModuleBay, 'objects')
|
||||
manager.rebuild()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0190_nested_modules'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
code=rebuild_mptt,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
]
|
18
netbox/dcim/migrations/0192_inventoryitem_status.py
Normal file
18
netbox/dcim/migrations/0192_inventoryitem_status.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.9 on 2024-09-26 20:19
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0191_module_bay_rebuild'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='inventoryitem',
|
||||
name='status',
|
||||
field=models.CharField(default='active', max_length=50),
|
||||
),
|
||||
]
|
19
netbox/dcim/migrations/0193_poweroutlet_color.py
Normal file
19
netbox/dcim/migrations/0193_poweroutlet_color.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.0.9 on 2024-09-26 19:31
|
||||
|
||||
import utilities.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0192_inventoryitem_status'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='poweroutlet',
|
||||
name='color',
|
||||
field=utilities.fields.ColorField(blank=True, max_length=6),
|
||||
),
|
||||
]
|
@ -6,7 +6,6 @@ from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import Sum
|
||||
from django.dispatch import Signal
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.models import ObjectType
|
||||
@ -116,9 +115,6 @@ class Cable(PrimaryModel):
|
||||
pk = self.pk or self._pk
|
||||
return self.label or f'#{pk}'
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:cable', args=[self.pk])
|
||||
|
||||
@property
|
||||
def a_terminations(self):
|
||||
if hasattr(self, '_a_terminations'):
|
||||
@ -164,7 +160,7 @@ class Cable(PrimaryModel):
|
||||
if self.length is not None and not self.length_unit:
|
||||
raise ValidationError(_("Must specify a unit when setting a cable length"))
|
||||
|
||||
if self.pk is None and (not self.a_terminations or not self.b_terminations):
|
||||
if self._state.adding and (not self.a_terminations or not self.b_terminations):
|
||||
raise ValidationError(_("Must define A and B terminations when creating a new cable."))
|
||||
|
||||
if self._terminations_modified:
|
||||
@ -366,11 +362,11 @@ class CableTermination(ChangeLoggedModel):
|
||||
def delete(self, *args, **kwargs):
|
||||
|
||||
# Delete the cable association on the terminating object
|
||||
termination_model = self.termination._meta.model
|
||||
termination_model.objects.filter(pk=self.termination_id).update(
|
||||
cable=None,
|
||||
cable_end=''
|
||||
)
|
||||
termination = self.termination._meta.model.objects.get(pk=self.termination_id)
|
||||
termination.snapshot()
|
||||
termination.cable = None
|
||||
termination.cable_end = ''
|
||||
termination.save()
|
||||
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
@ -666,6 +662,14 @@ class CablePath(models.Model):
|
||||
rear_port_id=remote_terminations[0].pk,
|
||||
rear_port_position__in=position_stack.pop()
|
||||
)
|
||||
# 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:
|
||||
# No position indicated: path has split, so we stop at the RearPorts
|
||||
is_split = True
|
||||
|
@ -98,7 +98,7 @@ class ComponentTemplateModel(ChangeLoggedModel, TrackingModelMixin):
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
if self.pk is not None and self._original_device_type != self.device_type_id:
|
||||
if not self._state.adding and self._original_device_type != self.device_type_id:
|
||||
raise ValidationError({
|
||||
"device_type": _("Component templates cannot be moved to a different device type.")
|
||||
})
|
||||
@ -160,7 +160,6 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
|
||||
|
||||
def _get_module_tree(self, module):
|
||||
modules = []
|
||||
all_module_bays = module.device.modulebays.all().select_related('module')
|
||||
while module:
|
||||
modules.append(module)
|
||||
if module.module_bay:
|
||||
|
@ -4,8 +4,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelatio
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models import F, Sum
|
||||
from django.urls import reverse
|
||||
from django.db.models import Sum
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
|
||||
@ -22,7 +21,6 @@ from utilities.tracking import TrackingModelMixin
|
||||
from wireless.choices import *
|
||||
from wireless.utils import get_channel_attr
|
||||
|
||||
|
||||
__all__ = (
|
||||
'BaseInterface',
|
||||
'CabledObjectModel',
|
||||
@ -301,9 +299,6 @@ class ConsolePort(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracki
|
||||
verbose_name = _('console port')
|
||||
verbose_name_plural = _('console ports')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:consoleport', kwargs={'pk': self.pk})
|
||||
|
||||
|
||||
class ConsoleServerPort(ModularComponentModel, CabledObjectModel, PathEndpoint, TrackingModelMixin):
|
||||
"""
|
||||
@ -330,9 +325,6 @@ class ConsoleServerPort(ModularComponentModel, CabledObjectModel, PathEndpoint,
|
||||
verbose_name = _('console server port')
|
||||
verbose_name_plural = _('console server ports')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:consoleserverport', kwargs={'pk': self.pk})
|
||||
|
||||
|
||||
#
|
||||
# Power components
|
||||
@ -370,9 +362,6 @@ class PowerPort(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracking
|
||||
verbose_name = _('power port')
|
||||
verbose_name_plural = _('power ports')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:powerport', kwargs={'pk': self.pk})
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
@ -481,6 +470,10 @@ class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracki
|
||||
blank=True,
|
||||
help_text=_('Phase (for three-phase feeds)')
|
||||
)
|
||||
color = ColorField(
|
||||
verbose_name=_('color'),
|
||||
blank=True
|
||||
)
|
||||
|
||||
clone_fields = ('device', 'module', 'type', 'power_port', 'feed_leg')
|
||||
|
||||
@ -488,9 +481,6 @@ class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracki
|
||||
verbose_name = _('power outlet')
|
||||
verbose_name_plural = _('power outlets')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:poweroutlet', kwargs={'pk': self.pk})
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
@ -561,7 +551,7 @@ class BaseInterface(models.Model):
|
||||
self.untagged_vlan = None
|
||||
|
||||
# Only "tagged" interfaces may have tagged VLANs assigned. ("tagged all" implies all VLANs are assigned.)
|
||||
if self.pk and self.mode != InterfaceModeChoices.MODE_TAGGED:
|
||||
if not self._state.adding and self.mode != InterfaceModeChoices.MODE_TAGGED:
|
||||
self.tagged_vlans.clear()
|
||||
|
||||
return super().save(*args, **kwargs)
|
||||
@ -746,9 +736,6 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
|
||||
verbose_name = _('interface')
|
||||
verbose_name_plural = _('interfaces')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:interface', kwargs={'pk': self.pk})
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
@ -1009,9 +996,6 @@ class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
|
||||
verbose_name = _('front port')
|
||||
verbose_name_plural = _('front ports')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:frontport', kwargs={'pk': self.pk})
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
@ -1067,14 +1051,11 @@ class RearPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
|
||||
verbose_name = _('rear port')
|
||||
verbose_name_plural = _('rear ports')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:rearport', kwargs={'pk': self.pk})
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Check that positions count is greater than or equal to the number of associated FrontPorts
|
||||
if self.pk:
|
||||
if not self._state.adding:
|
||||
frontport_count = self.frontports.count()
|
||||
if self.positions < frontport_count:
|
||||
raise ValidationError({
|
||||
@ -1126,9 +1107,6 @@ class ModuleBay(ModularComponentModel, TrackingModelMixin, MPTTModel):
|
||||
class MPTTMeta:
|
||||
order_insertion_by = ('module',)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:modulebay', kwargs={'pk': self.pk})
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
@ -1167,9 +1145,6 @@ class DeviceBay(ComponentModel, TrackingModelMixin):
|
||||
verbose_name = _('device bay')
|
||||
verbose_name_plural = _('device bays')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:devicebay', kwargs={'pk': self.pk})
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
@ -1213,9 +1188,6 @@ class InventoryItemRole(OrganizationalModel):
|
||||
verbose_name = _('inventory item role')
|
||||
verbose_name_plural = _('inventory item roles')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:inventoryitemrole', args=[self.pk])
|
||||
|
||||
|
||||
class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
|
||||
"""
|
||||
@ -1246,6 +1218,12 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
|
||||
ct_field='component_type',
|
||||
fk_field='component_id'
|
||||
)
|
||||
status = models.CharField(
|
||||
verbose_name=_('status'),
|
||||
max_length=50,
|
||||
choices=InventoryItemStatusChoices,
|
||||
default=InventoryItemStatusChoices.STATUS_ACTIVE
|
||||
)
|
||||
role = models.ForeignKey(
|
||||
to='dcim.InventoryItemRole',
|
||||
on_delete=models.PROTECT,
|
||||
@ -1287,7 +1265,7 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
|
||||
|
||||
objects = TreeManager()
|
||||
|
||||
clone_fields = ('device', 'parent', 'role', 'manufacturer', 'part_id',)
|
||||
clone_fields = ('device', 'parent', 'role', 'manufacturer', 'status', 'part_id')
|
||||
|
||||
class Meta:
|
||||
ordering = ('device__id', 'parent__id', '_name')
|
||||
@ -1303,9 +1281,6 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
|
||||
verbose_name = _('inventory item')
|
||||
verbose_name_plural = _('inventory items')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:inventoryitem', kwargs={'pk': self.pk})
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
@ -1316,7 +1291,7 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
|
||||
})
|
||||
|
||||
# Validation for moving InventoryItems
|
||||
if self.pk:
|
||||
if not self._state.adding:
|
||||
# Cannot move an InventoryItem to another device if it has a parent
|
||||
if self.parent and self.parent.device != self.device:
|
||||
raise ValidationError({
|
||||
@ -1336,3 +1311,6 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
|
||||
raise ValidationError({
|
||||
"device": _("Cannot assign inventory item to component on another device")
|
||||
})
|
||||
|
||||
def get_status_color(self):
|
||||
return InventoryItemStatusChoices.colors.get(self.status)
|
||||
|
@ -21,11 +21,12 @@ from extras.querysets import ConfigContextModelQuerySet
|
||||
from netbox.choices import ColorChoices
|
||||
from netbox.config import ConfigItem
|
||||
from netbox.models import OrganizationalModel, PrimaryModel
|
||||
from netbox.models.mixins import WeightMixin
|
||||
from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
|
||||
from utilities.fields import ColorField, CounterCacheField, NaturalOrderingField
|
||||
from utilities.tracking import TrackingModelMixin
|
||||
from .device_components import *
|
||||
from .mixins import RenderConfigMixin, WeightMixin
|
||||
from .mixins import RenderConfigMixin
|
||||
|
||||
|
||||
__all__ = (
|
||||
@ -54,9 +55,6 @@ class Manufacturer(ContactsMixin, OrganizationalModel):
|
||||
verbose_name = _('manufacturer')
|
||||
verbose_name_plural = _('manufacturers')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:manufacturer', args=[self.pk])
|
||||
|
||||
|
||||
class DeviceType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
|
||||
"""
|
||||
@ -217,9 +215,6 @@ class DeviceType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
|
||||
self._original_front_image = self.__dict__.get('front_image')
|
||||
self._original_rear_image = self.__dict__.get('rear_image')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:devicetype', args=[self.pk])
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
return f"{self.manufacturer} {self.model}"
|
||||
@ -293,7 +288,7 @@ class DeviceType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
|
||||
# If editing an existing DeviceType to have a larger u_height, first validate that *all* instances of it have
|
||||
# room to expand within their racks. This validation will impose a very high performance penalty when there are
|
||||
# many instances to check, but increasing the u_height of a DeviceType should be a very rare occurrence.
|
||||
if self.pk and self.u_height > self._original_u_height:
|
||||
if not self._state.adding and self.u_height > self._original_u_height:
|
||||
for d in Device.objects.filter(device_type=self, position__isnull=False):
|
||||
face_required = None if self.is_full_depth else d.face
|
||||
u_available = d.rack.get_available_units(
|
||||
@ -310,7 +305,7 @@ class DeviceType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
|
||||
})
|
||||
|
||||
# If modifying the height of an existing DeviceType to 0U, check for any instances assigned to a rack position.
|
||||
elif self.pk and self._original_u_height > 0 and self.u_height == 0:
|
||||
elif not self._state.adding and self._original_u_height > 0 and self.u_height == 0:
|
||||
racked_instance_count = Device.objects.filter(
|
||||
device_type=self,
|
||||
position__isnull=False
|
||||
@ -414,9 +409,6 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
|
||||
def __str__(self):
|
||||
return self.model
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:moduletype', args=[self.pk])
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
return f"{self.manufacturer} {self.model}"
|
||||
@ -497,9 +489,6 @@ class DeviceRole(OrganizationalModel):
|
||||
verbose_name = _('device role')
|
||||
verbose_name_plural = _('device roles')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:devicerole', args=[self.pk])
|
||||
|
||||
|
||||
class Platform(OrganizationalModel):
|
||||
"""
|
||||
@ -527,9 +516,6 @@ class Platform(OrganizationalModel):
|
||||
verbose_name = _('platform')
|
||||
verbose_name_plural = _('platforms')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:platform', args=[self.pk])
|
||||
|
||||
|
||||
def update_interface_bridges(device, interface_templates, module=None):
|
||||
"""
|
||||
@ -823,9 +809,6 @@ class Device(
|
||||
return f'{self.device_type.manufacturer} {self.device_type.model} ({self.pk})'
|
||||
return super().__str__()
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:device', args=[self.pk])
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
@ -983,6 +966,13 @@ class Device(
|
||||
'vc_position': _("A device assigned to a virtual chassis must have its position defined.")
|
||||
})
|
||||
|
||||
if hasattr(self, 'vc_master_for') and self.vc_master_for and self.vc_master_for != self.virtual_chassis:
|
||||
raise ValidationError({
|
||||
'virtual_chassis': _('Device cannot be removed from virtual chassis {virtual_chassis} because it is currently designated as its master.').format(
|
||||
virtual_chassis=self.vc_master_for
|
||||
)
|
||||
})
|
||||
|
||||
def _instantiate_components(self, queryset, bulk_create=True):
|
||||
"""
|
||||
Instantiate components for the device from the specified component templates.
|
||||
@ -1192,9 +1182,6 @@ class Module(PrimaryModel, ConfigContextModel):
|
||||
def __str__(self):
|
||||
return f'{self.module_bay.name}: {self.module_type} ({self.pk})'
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:module', args=[self.pk])
|
||||
|
||||
def get_status_color(self):
|
||||
return ModuleStatusChoices.colors.get(self.status)
|
||||
|
||||
@ -1343,15 +1330,12 @@ class VirtualChassis(PrimaryModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:virtualchassis', kwargs={'pk': self.pk})
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Verify that the selected master device has been assigned to this VirtualChassis. (Skip when creating a new
|
||||
# VirtualChassis.)
|
||||
if self.pk and self.master and self.master not in self.members.all():
|
||||
if not self._state.adding and self.master and self.master not in self.members.all():
|
||||
raise ValidationError({
|
||||
'master': _("The selected master ({master}) is not assigned to this virtual chassis.").format(
|
||||
master=self.master
|
||||
@ -1445,9 +1429,6 @@ class VirtualDeviceContext(PrimaryModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:virtualdevicecontext', kwargs={'pk': self.pk})
|
||||
|
||||
def get_status_color(self):
|
||||
return VirtualDeviceContextStatusChoices.colors.get(self.status)
|
||||
|
||||
|
@ -1,56 +1,10 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from dcim.choices import *
|
||||
from utilities.conversion import to_grams
|
||||
|
||||
__all__ = (
|
||||
'RenderConfigMixin',
|
||||
'WeightMixin',
|
||||
)
|
||||
|
||||
|
||||
class WeightMixin(models.Model):
|
||||
weight = models.DecimalField(
|
||||
verbose_name=_('weight'),
|
||||
max_digits=8,
|
||||
decimal_places=2,
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
weight_unit = models.CharField(
|
||||
verbose_name=_('weight unit'),
|
||||
max_length=50,
|
||||
choices=WeightUnitChoices,
|
||||
blank=True,
|
||||
)
|
||||
# Stores the normalized weight (in grams) for database ordering
|
||||
_abs_weight = models.PositiveBigIntegerField(
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
# Store the given weight (if any) in grams for use in database ordering
|
||||
if self.weight and self.weight_unit:
|
||||
self._abs_weight = to_grams(self.weight, self.weight_unit)
|
||||
else:
|
||||
self._abs_weight = None
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Validate weight and weight_unit
|
||||
if self.weight and not self.weight_unit:
|
||||
raise ValidationError(_("Must specify a unit when setting a weight"))
|
||||
|
||||
|
||||
class RenderConfigMixin(models.Model):
|
||||
config_template = models.ForeignKey(
|
||||
to='extras.ConfigTemplate',
|
||||
|
@ -1,7 +1,6 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim.choices import *
|
||||
@ -58,9 +57,6 @@ class PowerPanel(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:powerpanel', args=[self.pk])
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
@ -167,9 +163,6 @@ class PowerFeed(PrimaryModel, PathEndpoint, CabledObjectModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:powerfeed', args=[self.pk])
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
|
@ -8,7 +8,6 @@ from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models import Count
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim.choices import *
|
||||
@ -16,13 +15,13 @@ from dcim.constants import *
|
||||
from dcim.svg import RackElevationSVG
|
||||
from netbox.choices import ColorChoices
|
||||
from netbox.models import OrganizationalModel, PrimaryModel
|
||||
from netbox.models.mixins import WeightMixin
|
||||
from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
|
||||
from utilities.conversion import to_grams
|
||||
from utilities.data import array_to_string, drange
|
||||
from utilities.fields import ColorField, NaturalOrderingField
|
||||
from .device_components import PowerPort
|
||||
from .devices import Device, Module
|
||||
from .mixins import WeightMixin
|
||||
from .power import PowerFeed
|
||||
|
||||
__all__ = (
|
||||
@ -165,9 +164,6 @@ class RackType(RackBase):
|
||||
def __str__(self):
|
||||
return self.model
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:racktype', args=[self.pk])
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
return f"{self.manufacturer} {self.model}"
|
||||
@ -230,9 +226,6 @@ class RackRole(OrganizationalModel):
|
||||
verbose_name = _('rack role')
|
||||
verbose_name_plural = _('rack roles')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:rackrole', args=[self.pk])
|
||||
|
||||
|
||||
class Rack(ContactsMixin, ImageAttachmentsMixin, RackBase):
|
||||
"""
|
||||
@ -364,9 +357,6 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, RackBase):
|
||||
return f'{self.name} ({self.facility_id})'
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:rack', args=[self.pk])
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
@ -382,7 +372,7 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, RackBase):
|
||||
if self.max_weight and not self.weight_unit:
|
||||
raise ValidationError(_("Must specify a unit when setting a maximum weight"))
|
||||
|
||||
if self.pk:
|
||||
if not self._state.adding:
|
||||
mounted_devices = Device.objects.filter(rack=self).exclude(position__isnull=True).order_by('position')
|
||||
|
||||
# Validate that Rack is tall enough to house the highest mounted Device
|
||||
@ -468,7 +458,7 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, RackBase):
|
||||
}
|
||||
|
||||
# Add devices to rack units list
|
||||
if self.pk:
|
||||
if not self._state.adding:
|
||||
|
||||
# Retrieve all devices installed within the rack
|
||||
devices = Device.objects.prefetch_related(
|
||||
@ -699,9 +689,6 @@ class RackReservation(PrimaryModel):
|
||||
def __str__(self):
|
||||
return "Reservation for rack {}".format(self.rack)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:rackreservation', args=[self.pk])
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from timezone_field import TimeZoneField
|
||||
|
||||
@ -62,9 +61,6 @@ class Region(ContactsMixin, NestedGroupModel):
|
||||
verbose_name = _('region')
|
||||
verbose_name_plural = _('regions')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:region', args=[self.pk])
|
||||
|
||||
def get_site_count(self):
|
||||
return Site.objects.filter(
|
||||
Q(region=self) |
|
||||
@ -115,9 +111,6 @@ class SiteGroup(ContactsMixin, NestedGroupModel):
|
||||
verbose_name = _('site group')
|
||||
verbose_name_plural = _('site groups')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:sitegroup', args=[self.pk])
|
||||
|
||||
def get_site_count(self):
|
||||
return Site.objects.filter(
|
||||
Q(group=self) |
|
||||
@ -241,9 +234,6 @@ class Site(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:site', args=[self.pk])
|
||||
|
||||
def get_status_color(self):
|
||||
return SiteStatusChoices.colors.get(self.status)
|
||||
|
||||
@ -322,9 +312,6 @@ class Location(ContactsMixin, ImageAttachmentsMixin, NestedGroupModel):
|
||||
verbose_name = _('location')
|
||||
verbose_name_plural = _('locations')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:location', args=[self.pk])
|
||||
|
||||
def get_status_color(self):
|
||||
return LocationStatusChoices.colors.get(self.status)
|
||||
|
||||
|
@ -250,7 +250,7 @@ class RackTypeIndex(SearchIndex):
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('type', 'description')
|
||||
display_attrs = ('model', 'description')
|
||||
|
||||
|
||||
@register_search
|
||||
|
@ -162,6 +162,9 @@ class CableTraceSVG:
|
||||
location_label += f' / {instance.location}'
|
||||
if instance.rack:
|
||||
location_label += f' / {instance.rack}'
|
||||
if instance.position:
|
||||
location_label += f' / {instance.get_face_display()}'
|
||||
location_label += f' / U{instance.position}'
|
||||
labels.append(location_label)
|
||||
elif instance._meta.model_name == 'circuit':
|
||||
labels[0] = f'Circuit {instance}'
|
||||
|
@ -512,6 +512,7 @@ class PowerOutletTable(ModularDeviceComponentTable, PathEndpointTable):
|
||||
verbose_name=_('Power Port'),
|
||||
linkify=True
|
||||
)
|
||||
color = columns.ColorColumn()
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:poweroutlet_list'
|
||||
)
|
||||
@ -520,10 +521,10 @@ class PowerOutletTable(ModularDeviceComponentTable, PathEndpointTable):
|
||||
model = models.PowerOutlet
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'description', 'power_port',
|
||||
'feed_leg', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'inventory_items',
|
||||
'color', 'feed_leg', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'inventory_items',
|
||||
'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'type', 'power_port', 'feed_leg', 'description')
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'type', 'color', 'power_port', 'feed_leg', 'description')
|
||||
|
||||
|
||||
class DevicePowerOutletTable(PowerOutletTable):
|
||||
@ -540,11 +541,11 @@ class DevicePowerOutletTable(PowerOutletTable):
|
||||
class Meta(CableTerminationTable.Meta, DeviceComponentTable.Meta):
|
||||
model = models.PowerOutlet
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'power_port', 'feed_leg', 'description',
|
||||
'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions',
|
||||
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'color', 'power_port', 'feed_leg',
|
||||
'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'cable', 'connection',
|
||||
'pk', 'name', 'label', 'type', 'color', 'power_port', 'feed_leg', 'description', 'cable', 'connection',
|
||||
)
|
||||
|
||||
|
||||
@ -588,6 +589,9 @@ class BaseInterfaceTable(NetBoxTable):
|
||||
def value_ip_addresses(self, value):
|
||||
return ",".join([str(obj.address) for obj in value.all()])
|
||||
|
||||
def value_tagged_vlans(self, value):
|
||||
return ",".join([str(obj) for obj in value.all()])
|
||||
|
||||
|
||||
class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpointTable):
|
||||
device = tables.Column(
|
||||
@ -684,7 +688,8 @@ class DeviceInterfaceTable(InterfaceTable):
|
||||
'data-virtual': lambda record: "true" if record.is_virtual else "false",
|
||||
'data-mark-connected': lambda record: "true" if record.mark_connected else "false",
|
||||
'data-cable-status': lambda record: record.cable.status if record.cable else "",
|
||||
'data-type': lambda record: record.type
|
||||
'data-type': lambda record: record.type,
|
||||
'data-connected': lambda record: "connected" if record.mark_connected or record.cable else "disconnected"
|
||||
}
|
||||
|
||||
|
||||
@ -945,6 +950,9 @@ class InventoryItemTable(DeviceComponentTable):
|
||||
verbose_name=_('Discovered'),
|
||||
false_mark=None
|
||||
)
|
||||
status = columns.ChoiceFieldColumn(
|
||||
verbose_name=_('Status'),
|
||||
)
|
||||
parent = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name=_('Parent'),
|
||||
@ -957,11 +965,11 @@ class InventoryItemTable(DeviceComponentTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = models.InventoryItem
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'device', 'parent', 'component', 'label', 'role', 'manufacturer', 'part_id', 'serial',
|
||||
'pk', 'id', 'name', 'device', 'parent', 'component', 'label', 'status', 'role', 'manufacturer', 'part_id', 'serial',
|
||||
'asset_tag', 'description', 'discovered', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'device', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
|
||||
'pk', 'name', 'device', 'label', 'status', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
|
||||
)
|
||||
|
||||
|
||||
@ -977,11 +985,11 @@ class DeviceInventoryItemTable(InventoryItemTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = models.InventoryItem
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component',
|
||||
'pk', 'id', 'name', 'label', 'status', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component',
|
||||
'description', 'discovered', 'tags', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component',
|
||||
'pk', 'name', 'label', 'status', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component',
|
||||
)
|
||||
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
import django_tables2 as tables
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim import models
|
||||
from netbox.tables import NetBoxTable, columns
|
||||
|
@ -56,9 +56,13 @@ INTERFACE_FHRPGROUPS = """
|
||||
|
||||
INTERFACE_TAGGED_VLANS = """
|
||||
{% if record.mode == 'tagged' %}
|
||||
{% if value.count > 3 %}
|
||||
<a href="{% url 'ipam:vlan_list' %}?{{ record|meta:"model_name" }}_id={{ record.pk }}">{{ value.count }} VLANs</a>
|
||||
{% else %}
|
||||
{% for vlan in value.all %}
|
||||
<a href="{{ vlan.get_absolute_url }}">{{ vlan }}</a><br />
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% elif record.mode == 'tagged-all' %}
|
||||
All
|
||||
{% endif %}
|
||||
|
@ -2163,12 +2163,12 @@ class ConnectedDeviceTest(APITestCase):
|
||||
def test_get_connected_device(self):
|
||||
url = reverse('dcim-api:connected-device-list')
|
||||
|
||||
url_params = f'?peer_device=TestDevice1&peer_interface=eth0'
|
||||
url_params = '?peer_device=TestDevice1&peer_interface=eth0'
|
||||
response = self.client.get(url + url_params, **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['name'], 'TestDevice2')
|
||||
|
||||
url_params = f'?peer_device=TestDevice1&peer_interface=eth1'
|
||||
url_params = '?peer_device=TestDevice1&peer_interface=eth1'
|
||||
response = self.client.get(url + url_params, **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
@ -2060,6 +2060,49 @@ class CablePathTestCase(TestCase):
|
||||
# Test SVG generation
|
||||
CableTraceSVG(interface1).render()
|
||||
|
||||
def test_222_single_path_via_multiple_singleposition_rear_ports(self):
|
||||
"""
|
||||
[IF1] --C1-- [FP1] [RP1] --C2-- [IF2]
|
||||
[FP2] [RP2]
|
||||
"""
|
||||
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
||||
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
|
||||
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1)
|
||||
rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=1)
|
||||
frontport1 = FrontPort.objects.create(
|
||||
device=self.device, name='Front Port 1', rear_port=rearport1, rear_port_position=1
|
||||
)
|
||||
frontport2 = FrontPort.objects.create(
|
||||
device=self.device, name='Front Port 2', rear_port=rearport2, rear_port_position=1
|
||||
)
|
||||
|
||||
cable1 = Cable(
|
||||
a_terminations=[interface1],
|
||||
b_terminations=[frontport1, frontport2]
|
||||
)
|
||||
cable1.save()
|
||||
self.assertEqual(CablePath.objects.count(), 1)
|
||||
|
||||
cable2 = Cable(
|
||||
a_terminations=[rearport1, rearport2],
|
||||
b_terminations=[interface2]
|
||||
)
|
||||
cable2.save()
|
||||
self.assertEqual(CablePath.objects.count(), 2)
|
||||
|
||||
self.assertPathExists(
|
||||
(interface1, cable1, (frontport1, frontport2), (rearport1, rearport2), cable2, interface2),
|
||||
is_complete=True
|
||||
)
|
||||
self.assertPathExists(
|
||||
(interface2, cable2, (rearport1, rearport2), (frontport1, frontport2), cable1, interface1),
|
||||
is_complete=True
|
||||
)
|
||||
|
||||
# Test SVG generation both directions
|
||||
CableTraceSVG(interface1).render()
|
||||
CableTraceSVG(interface2).render()
|
||||
|
||||
def test_301_create_path_via_existing_cable(self):
|
||||
"""
|
||||
[IF1] --C1-- [FP1] [RP1] --C2-- [RP2] [FP2] --C3-- [IF2]
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user