mirror of
https://github.com/netbox-community/netbox.git
synced 2026-04-11 11:47:08 +02:00
Compare commits
5 Commits
v4.3.0-bet
...
11507-show
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64fca01cdb | ||
|
|
d699c69abf | ||
|
|
97d426d205 | ||
|
|
e32d2ca637 | ||
|
|
1777d4228e |
@@ -15,7 +15,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.2.7
|
||||
placeholder: v4.2.3
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
@@ -27,7 +27,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox Version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.2.7
|
||||
placeholder: v4.2.3
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
@@ -12,7 +12,6 @@ permissions:
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
if: github.repository == 'netbox-community/netbox'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
|
||||
1
.github/workflows/close-stale-issues.yml
vendored
1
.github/workflows/close-stale-issues.yml
vendored
@@ -13,7 +13,6 @@ permissions:
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
if: github.repository == 'netbox-community/netbox'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
|
||||
1
.github/workflows/lock-threads.yml
vendored
1
.github/workflows/lock-threads.yml
vendored
@@ -13,7 +13,6 @@ permissions:
|
||||
|
||||
jobs:
|
||||
lock:
|
||||
if: github.repository == 'netbox-community/netbox'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v5
|
||||
|
||||
@@ -13,7 +13,6 @@ env:
|
||||
|
||||
jobs:
|
||||
makemessages:
|
||||
if: github.repository == 'netbox-community/netbox'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NETBOX_CONFIGURATION: netbox.configuration_testing
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[main]
|
||||
host = https://app.transifex.com
|
||||
|
||||
[o:netbox-community:p:netbox:r:034999968a7366ba27a8bdf1ab63bf42]
|
||||
[o:netbox-community:p:netbox:r:9cbf4fcf95b3d92e4ebbf1a5e5d1caee]
|
||||
file_filter = netbox/translations/<lang>/LC_MESSAGES/django.po
|
||||
source_file = netbox/translations/en/LC_MESSAGES/django.po
|
||||
type = PO
|
||||
|
||||
@@ -82,10 +82,6 @@ gunicorn
|
||||
# https://jinja.palletsprojects.com/changes/
|
||||
Jinja2
|
||||
|
||||
# JSON schema validation
|
||||
# https://github.com/python-jsonschema/jsonschema/blob/main/CHANGELOG.rst
|
||||
jsonschema
|
||||
|
||||
# Simple markup language for rendering HTML
|
||||
# https://python-markdown.github.io/changelog/
|
||||
Markdown
|
||||
@@ -96,7 +92,8 @@ mkdocs-material
|
||||
|
||||
# Introspection for embedded code
|
||||
# https://github.com/mkdocstrings/mkdocstrings/blob/main/CHANGELOG.md
|
||||
mkdocstrings[python]
|
||||
# See #18568
|
||||
mkdocstrings[python-legacy]==0.27.0
|
||||
|
||||
# Library for manipulating IP prefixes and addresses
|
||||
# https://github.com/netaddr/netaddr/blob/master/CHANGELOG.rst
|
||||
@@ -140,7 +137,8 @@ strawberry-graphql
|
||||
|
||||
# Strawberry GraphQL Django extension
|
||||
# https://github.com/strawberry-graphql/strawberry-django/releases
|
||||
strawberry-graphql-django
|
||||
# Pinned to v0.52.0 for suspected upstream bug; see #18329
|
||||
strawberry-graphql-django==0.52.0
|
||||
|
||||
# SVG image rendering (used for rack elevations)
|
||||
# https://github.com/mozman/svgwrite/blob/master/NEWS.rst
|
||||
|
||||
@@ -427,7 +427,6 @@
|
||||
"e3",
|
||||
"xdsl",
|
||||
"docsis",
|
||||
"moca",
|
||||
"bpon",
|
||||
"epon",
|
||||
"10g-epon",
|
||||
@@ -501,9 +500,6 @@
|
||||
"n",
|
||||
"mrj21",
|
||||
"fc",
|
||||
"fc-pc",
|
||||
"fc-upc",
|
||||
"fc-apc",
|
||||
"lc",
|
||||
"lc-pc",
|
||||
"lc-upc",
|
||||
@@ -569,9 +565,6 @@
|
||||
"n",
|
||||
"mrj21",
|
||||
"fc",
|
||||
"fc-pc",
|
||||
"fc-upc",
|
||||
"fc-apc",
|
||||
"lc",
|
||||
"lc-pc",
|
||||
"lc-upc",
|
||||
|
||||
@@ -54,7 +54,6 @@ Icons](https://github.com/google/material-design-icons) icon's name; or be
|
||||
`None` for no icon.
|
||||
|
||||
For instance, the OIDC backend may be customized with
|
||||
|
||||
```python
|
||||
SOCIAL_AUTH_BACKEND_ATTRS = {
|
||||
'oidc': ("My awesome SSO", "login"),
|
||||
|
||||
@@ -233,15 +233,3 @@ This parameter controls how frequently a failed job is retried, up to the maximu
|
||||
Default: `0` (retries disabled)
|
||||
|
||||
The maximum number of times a background task will be retried before being marked as failed.
|
||||
|
||||
## DISK_BASE_UNIT
|
||||
|
||||
Default: `1000`
|
||||
|
||||
The base unit for disk sizes. Set this to `1024` to use binary prefixes (MiB, GiB, etc.) instead of decimal prefixes (MB, GB, etc.).
|
||||
|
||||
## RAM_BASE_UNIT
|
||||
|
||||
Default: `1000`
|
||||
|
||||
The base unit for RAM sizes. Set this to `1024` to use binary prefixes (MiB, GiB, etc.) instead of decimal prefixes (MB, GB, etc.).
|
||||
|
||||
@@ -33,21 +33,3 @@ Note that a plugin must be listed in `PLUGINS` for its configuration to take eff
|
||||
|
||||
---
|
||||
|
||||
## PLUGINS_CATALOG_CONFIG
|
||||
|
||||
Default: Empty
|
||||
|
||||
This parameter controls how individual plugins are displayed in the plugins catalog under Admin > System > Plugins. Adding a plugin to the `hidden` list will omit that plugin from the catalog. Adding a plugin to the `static` list will display the plugin, but not link to the plugin details or upgrade instructions.
|
||||
|
||||
An example configuration is shown below:
|
||||
|
||||
```python
|
||||
PLUGINS_CATALOG_CONFIG = {
|
||||
'hidden': [
|
||||
'plugin1',
|
||||
],
|
||||
'static': [
|
||||
'plugin2',
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
|
||||
## ALLOW_TOKEN_RETRIEVAL
|
||||
|
||||
Default: False
|
||||
|
||||
!!! note
|
||||
The default value of this parameter changed from true to false in NetBox v4.3.0.
|
||||
Default: True
|
||||
|
||||
If disabled, the values of API tokens will not be displayed after each token's initial creation. A user **must** record the value of a token prior to its creation, or it will be lost. Note that this affects _all_ users, regardless of assigned permissions.
|
||||
|
||||
|
||||
@@ -14,8 +14,6 @@ BASE_PATH = 'netbox/'
|
||||
|
||||
## DATABASE_ROUTERS
|
||||
|
||||
!!! info "This parameter was introduced in NetBox v4.3."
|
||||
|
||||
Default: `[]` (empty list)
|
||||
|
||||
An iterable of [database routers](https://docs.djangoproject.com/en/stable/topics/db/multi-db/) to use for automatically selecting the appropriate database(s) for a query. This is useful only when [multiple databases](./required-parameters.md#databases) have been configured.
|
||||
@@ -174,8 +172,6 @@ The file path to the location where media files (such as image attachments) are
|
||||
|
||||
## PROXY_ROUTERS
|
||||
|
||||
!!! info "This parameter was introduced in NetBox v4.3."
|
||||
|
||||
Default: `["utilities.proxy.DefaultProxyRouter"]`
|
||||
|
||||
A list of Python classes responsible for determining which proxy server(s) to use for outbound HTTP requests. Each item in the list can be the class itself or the dotted path to the class.
|
||||
|
||||
@@ -310,7 +310,6 @@ A particular object within NetBox. Each ObjectVar must specify a particular mode
|
||||
* `query_params` - A dictionary of query parameters to use when retrieving available options (optional)
|
||||
* `context` - A custom dictionary mapping template context variables to fields, used when rendering `<option>` elements within the dropdown menu (optional; see below)
|
||||
* `null_option` - A label representing a "null" or empty choice (optional)
|
||||
* `selector` - A boolean that, when True, includes an advanced object selection widget to assist the user in identifying the desired object (optional; False by default)
|
||||
|
||||
To limit the selections available within the list, additional query parameters can be passed as the `query_params` dictionary. For example, to show only devices with an "active" status:
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ Height: {{ rack.u_height }}U
|
||||
To access custom fields of an object within a template, use the `cf` attribute. For example, `{{ obj.cf.color }}` will return the value (if any) for a custom field named `color` on `obj`.
|
||||
|
||||
If you need to use the config context data in an export template, you'll should use the function `get_config_context` to get all the config context data. For example:
|
||||
|
||||
```
|
||||
{% for server in queryset %}
|
||||
{% set data = server.get_config_context() %}
|
||||
|
||||
@@ -6,7 +6,7 @@ Below is a list of tasks to consider when adding a new field to a core model.
|
||||
|
||||
Add the field to the model, taking care to address any of the following conditions.
|
||||
|
||||
* When adding a GenericForeignKey field, you may need add an index under `Meta` for its two concrete fields. (This is required only for non-unique GFK relationships, as the unique constraint introduces its own index.) For example:
|
||||
* When adding a GenericForeignKey field, also add an index under `Meta` for its two concrete fields. For example:
|
||||
|
||||
```python
|
||||
class Meta:
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
# Release Checklist
|
||||
|
||||
This documentation describes the process of packaging and publishing a new NetBox release. There are three types of releases:
|
||||
This documentation describes the process of packaging and publishing a new NetBox release. There are three types of release:
|
||||
|
||||
* Major release (e.g. v3.7.8 to v4.0.0)
|
||||
* Minor release (e.g. v4.0.10 to v4.1.0)
|
||||
* Patch release (e.g. v4.1.0 to v4.1.1)
|
||||
|
||||
While major releases generally introduce some very substantial changes to the application, they are typically treated the same as minor version increments for the purpose of release packaging.
|
||||
|
||||
For patch releases (e.g. upgrading from v4.2.2 to v4.2.3), begin at the [patch releases](#patch-releases) heading below. For minor or major releases, complete the entire checklist.
|
||||
While major releases generally introduce some very substantial change to the application, they are typically treated the same as minor version increments for the purpose of release packaging.
|
||||
|
||||
## Minor Version Releases
|
||||
|
||||
@@ -31,29 +29,6 @@ Close the [release milestone](https://github.com/netbox-community/netbox/milesto
|
||||
|
||||
Check that a link to the release notes for the new version is present in the navigation menu (defined in `mkdocs.yml`), and that a summary of all major new features has been added to `docs/index.md`.
|
||||
|
||||
### Update the Dependency Requirements Matrix
|
||||
|
||||
For every minor release, update the dependency requirements matrix in `docs/installation/upgrading.md` ("All versions") to reflect the supported versions of Python, PostgreSQL, and Redis:
|
||||
|
||||
1. Add a new row with the supported dependency versions.
|
||||
2. Include a documentation link using the release tag format: `https://github.com/netbox-community/netbox/blob/v4.2.0/docs/installation/index.md`
|
||||
3. Bold any version changes for clarity.
|
||||
|
||||
**Example Update:**
|
||||
|
||||
```markdown
|
||||
| NetBox Version | Python min | Python max | PostgreSQL min | Redis min | Documentation |
|
||||
|:--------------:|:----------:|:----------:|:--------------:|:---------:|:-------------------------------------------------------------------------------------------------:|
|
||||
| 4.2 | 3.10 | 3.12 | **13** | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v4.2.0/docs/installation/index.md) |
|
||||
```
|
||||
|
||||
### Update System Requirements
|
||||
|
||||
If a new Django release is adopted or other major dependencies (Python, PostgreSQL, Redis) change:
|
||||
|
||||
* Update the installation guide (`docs/installation/index.md`) with the new minimum versions.
|
||||
* Update the upgrade guide (`docs/installation/upgrading.md`) for the current version accordingly.
|
||||
|
||||
### Manually Perform a New Install
|
||||
|
||||
Start the documentation server and navigate to the current version of the installation docs:
|
||||
@@ -62,25 +37,15 @@ Start the documentation server and navigate to the current version of the instal
|
||||
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.
|
||||
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.
|
||||
Test the following supported upgrade paths:
|
||||
|
||||
- From one minor version to another within the same major version (e.g. 4.0 to 4.1).
|
||||
- From the latest patch version of the previous minor version (e.g. 3.7 to 4.0 or 4.1).
|
||||
|
||||
Prior to release, test all these supported paths by loading demo data from the source version and performing:
|
||||
|
||||
```no-highlight
|
||||
./manage.py migrate
|
||||
```
|
||||
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 `feature` Branch
|
||||
|
||||
Submit a pull request to merge the `feature` branch into the `main` branch in preparation for its release. Once it has been merged, continue with the section for the patch releases below.
|
||||
Submit a pull request to merge the `feature` branch into the `main` branch in preparation for its release. Once it has been merged, continue with the section for patch releases below.
|
||||
|
||||
### Rebuild Demo Data (After Release)
|
||||
|
||||
@@ -92,7 +57,7 @@ After the release of a new minor version, generate a new demo data snapshot comp
|
||||
|
||||
### Create a Release Branch
|
||||
|
||||
Begin by creating a new branch (based on `main`) to effect the release. This will comprise the changes listed below.
|
||||
Begin by creating a new branch (based off of `main`) to effect the release. This will comprise the changes listed below.
|
||||
|
||||
```
|
||||
git checkout main
|
||||
@@ -120,20 +85,7 @@ In cases where upgrading a dependency to its most recent release is breaking, it
|
||||
|
||||
### Update UI Dependencies
|
||||
|
||||
Check whether any UI dependencies (JavaScript packages, fonts, etc.) need to be updated by running `yarn outdated` from within the `project-static/` directory. [Upgrade these dependencies](./web-ui.md#updating-dependencies) as necessary, then run `yarn bundle` to generate the necessary files for distribution:
|
||||
|
||||
```
|
||||
$ yarn bundle
|
||||
yarn run v1.22.19
|
||||
$ node bundle.js
|
||||
✅ Bundled source file 'styles/external.scss' to 'netbox-external.css'
|
||||
✅ Bundled source file 'styles/netbox.scss' to 'netbox.css'
|
||||
✅ Bundled source file 'styles/svg/rack_elevation.scss' to 'rack_elevation.css'
|
||||
✅ Bundled source file 'styles/svg/cable_trace.scss' to 'cable_trace.css'
|
||||
✅ Bundled source file 'index.ts' to 'netbox.js'
|
||||
✅ Copied graphiql files
|
||||
Done in 1.00s.
|
||||
```
|
||||
Check whether any UI dependencies (JavaScript packages, fonts, etc.) need to be updated by running `yarn outdated` from within the `project-static/` directory. [Upgrade these dependencies](./web-ui.md#updating-dependencies) as necessary, then run `yarn bundle` to generate the necessary files for distribution.
|
||||
|
||||
### Rebuild the Device Type Definition Schema
|
||||
|
||||
@@ -164,12 +116,9 @@ Then, compile these portable (`.po`) files for use in the application:
|
||||
|
||||
### Update Version and Changelog
|
||||
|
||||
* Update the version number and date in `netbox/release.yaml`. Add or remove the designation (e.g. `beta1`) if applicable.
|
||||
* Update the version and published date in `release.yaml` with the current version & date. Add a designation (e.g.g `beta1`) if applicable.
|
||||
* Update the example version numbers in the feature request and bug report templates under `.github/ISSUE_TEMPLATES/`.
|
||||
* Add a section for this release at the top of the changelog page for the minor version (e.g. `docs/release-notes/version-4.2.md`) listing all relevant changes made in this release.
|
||||
|
||||
!!! tip
|
||||
Put yourself in the shoes of the user when recording change notes. Focus on the effect that each change has for the end user, rather than the specific bits of code that were modified in a PR. Ensure that each message conveys meaning absent context of the initial feature request or bug report. Remember to include keywords or phrases (such as exception names) that can be easily searched.
|
||||
* Replace the "FUTURE" placeholder in the release notes with the current date.
|
||||
|
||||
### Submit a Pull Request
|
||||
|
||||
@@ -177,9 +126,6 @@ Commit the above changes and submit a pull request titled **"Release vX.Y.Z"** t
|
||||
|
||||
Once CI has completed and a colleague has reviewed the PR, merge it. This effects a new release in the `main` branch.
|
||||
|
||||
!!! warning
|
||||
To ensure a streamlined review process, the pull request for a release **must** be limited to the changes outlined in this document. A release PR must never include functional changes to the application: Any unrelated "cleanup" needs to be captured in a separate PR prior to the release being shipped.
|
||||
|
||||
### Create a New Release
|
||||
|
||||
Create a [new release](https://github.com/netbox-community/netbox/releases/new) on GitHub with the following parameters.
|
||||
|
||||
@@ -22,7 +22,7 @@ NetBox generally follows the [Django style guide](https://docs.djangoproject.com
|
||||
|
||||
### Linting
|
||||
|
||||
The [ruff](https://docs.astral.sh/ruff/) linter is used to enforce code style, and is run automatically by [pre-commit](./getting-started.md#5-install-pre-commit). To invoke `ruff` manually, run:
|
||||
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:
|
||||
|
||||
```
|
||||
ruff check netbox/
|
||||
|
||||
@@ -30,7 +30,7 @@ To download translated strings automatically, you'll need to:
|
||||
1. Install the [Transifex CLI client](https://github.com/transifex/cli)
|
||||
2. Generate a [Transifex API token](https://app.transifex.com/user/settings/api/)
|
||||
|
||||
Once you have the client set up, run the following command from the project root (e.g. `/opt/netbox/`):
|
||||
Once you have the client set up, run the following command:
|
||||
|
||||
```no-highlight
|
||||
TX_TOKEN=$TOKEN tx pull
|
||||
@@ -46,9 +46,6 @@ Once retrieved, the updated strings need to be compiled into new `.mo` files so
|
||||
|
||||
Once any new `.mo` files have been generated, they need to be committed and pushed back up to GitHub. (Again, this is typically done as part of publishing a new NetBox release.)
|
||||
|
||||
!!! tip
|
||||
Run `git status` to check that both `*.mo` & `*.po` files have been updated as expected.
|
||||
|
||||
## Proposing New Languages
|
||||
|
||||
If you'd like to add support for a new language to NetBox, the first step is to [submit a GitHub issue](https://github.com/netbox-community/netbox/issues/new?assignees=&labels=type%3A+translation&projects=&template=translation.yaml) to capture the proposal. While we'd like to add as many languages as possible, we do need to limit the rate at which new languages are added. New languages will be selected according to community interest and the number of volunteers who sign up as translators.
|
||||
|
||||
@@ -23,45 +23,6 @@ NetBox requires the following dependencies:
|
||||
| PostgreSQL | 14+ |
|
||||
| Redis | 4.0+ |
|
||||
|
||||
### Version History
|
||||
|
||||
| NetBox Version | Python min | Python max | PostgreSQL min | Redis min | Documentation |
|
||||
|:--------------:|:----------:|:----------:|:--------------:|:---------:|:-------------------------------------------------------------------------------------------------:|
|
||||
| 4.3 | 3.10 | 3.12 | 14 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v4.3.0/docs/installation/index.md) |
|
||||
| 4.2 | 3.10 | 3.12 | 13 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v4.2.0/docs/installation/index.md) |
|
||||
| 4.1 | 3.10 | 3.12 | 12 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v4.1.0/docs/installation/index.md) |
|
||||
| 4.0 | 3.10 | 3.12 | 12 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v4.0.0/docs/installation/index.md) |
|
||||
| 3.7 | 3.8 | 3.11 | 12 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.7.0/docs/installation/index.md) |
|
||||
| 3.6 | 3.8 | 3.11 | 12 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.6.0/docs/installation/index.md) |
|
||||
| 3.5 | 3.8 | 3.10 | 11 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.5.0/docs/installation/index.md) |
|
||||
| 3.4 | 3.8 | 3.10 | 11 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.4.0/docs/installation/index.md) |
|
||||
| 3.3 | 3.8 | 3.10 | 10 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.3.0/docs/installation/index.md) |
|
||||
| 3.2 | 3.8 | 3.10 | 10 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.2.0/docs/installation/index.md) |
|
||||
| 3.1 | 3.7 | 3.9 | 10 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.1.0/docs/installation/index.md) |
|
||||
| 3.0 | 3.7 | 3.9 | 9.6 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.0.0/docs/installation/index.md) |
|
||||
| 2.11 | 3.6 | 3.9 | 9.6 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v2.11.0/docs/installation/index.md) |
|
||||
| 2.10 | 3.6 | 3.8 | 9.6 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v2.10.0/docs/installation/index.md) |
|
||||
| 2.9 | 3.6 | 3.8 | 9.5 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v2.9.0/docs/installation/index.md) |
|
||||
| 2.8 | 3.6 | 3.8 | 9.5 | 3.4 | [Link](https://github.com/netbox-community/netbox/blob/v2.8.0/docs/installation/index.md) |
|
||||
| 2.7 | 3.5 | 3.7 | 9.4 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.7.0/docs/installation/index.md) |
|
||||
| 2.6 | 3.5 | 3.7 | 9.4 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.6.0/docs/installation/index.md) |
|
||||
| 2.5 | 3.5 | 3.7 | 9.4 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.5.0/docs/installation/index.md) |
|
||||
| 2.4 | 3.4 | 3.7 | 9.4 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.4.0/docs/installation/index.md) |
|
||||
| 2.3 | 2.7 | 3.6 | 9.4 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.3.0/docs/installation/postgresql.md) |
|
||||
| 2.2 | 2.7 | 3.6 | 9.4 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.2.0/docs/installation/postgresql.md) |
|
||||
| 2.1 | 2.7 | 3.6 | 9.3 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.1.0/docs/installation/postgresql.md) |
|
||||
| 2.0 | 2.7 | 3.6 | 9.3 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.0.0/docs/installation/postgresql.md) |
|
||||
| 1.9 | 2.7 | 3.5 | 9.2 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.9.0-r1/docs/installation/postgresql.md) |
|
||||
| 1.8 | 2.7 | 3.5 | 9.2 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.8.0/docs/installation/postgresql.md) |
|
||||
| 1.7 | 2.7 | 3.5 | 9.2 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.7.0/docs/installation/postgresql.md) |
|
||||
| 1.6 | 2.7 | 3.5 | 9.2 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.6.0/docs/installation/postgresql.md) |
|
||||
| 1.5 | 2.7 | 3.5 | 9.2 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.5.0/docs/installation/postgresql.md) |
|
||||
| 1.4 | 2.7 | 3.5 | 9.1 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.4.0/docs/installation/postgresql.md) |
|
||||
| 1.3 | 2.7 | 3.5 | 9.1 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.3.0/docs/installation/postgresql.md) |
|
||||
| 1.2 | 2.7 | 3.5 | 9.1 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.2.0/docs/installation/postgresql.md) |
|
||||
| 1.1 | 2.7 | 3.5 | 9.1 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.1.0/docs/getting-started.md) |
|
||||
| 1.0 | 2.7 | 3.5 | 9.1 | - | [Link](https://github.com/netbox-community/netbox/blob/1.0.0/docs/getting-started.md) |
|
||||
|
||||
## 3. Install the Latest Release
|
||||
|
||||
As with the initial installation, you can upgrade NetBox by either downloading the latest release package or by checking out the latest production release from the git repository.
|
||||
|
||||
@@ -127,22 +127,10 @@ Certain queries can return multiple types of objects, for example cable terminat
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
The field "class_type" is an easy way to distinguish what type of object it is when viewing the returned data, or when filtering. It contains the class name, for example "CircuitTermination" or "ConsoleServerPort".
|
||||
|
||||
## Pagination
|
||||
|
||||
Queries can be paginated by specifying pagination in the query and supplying an offset and optionaly a limit in the query. If no limit is given, a default of 100 is used. Queries are not paginated unless requested in the query. An example paginated query is shown below:
|
||||
|
||||
```
|
||||
query {
|
||||
device_list(pagination: { offset: 0, limit: 20 }) {
|
||||
id
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
NetBox's GraphQL API uses the same API authentication tokens as its REST API. Authentication tokens are included with requests by attaching an `Authorization` HTTP header in the following form:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## What is a REST API?
|
||||
|
||||
REST stands for [representational state transfer](https://en.wikipedia.org/wiki/REST). It's a particular type of API which employs HTTP requests and [JavaScript Object Notation (JSON)](https://www.json.org/) to facilitate create, retrieve, update, and delete (CRUD) operations on objects within an application. Each type of operation is associated with a particular HTTP verb:
|
||||
REST stands for [representational state transfer](https://en.wikipedia.org/wiki/Representational_state_transfer). It's a particular type of API which employs HTTP requests and [JavaScript Object Notation (JSON)](https://www.json.org/) to facilitate create, retrieve, update, and delete (CRUD) operations on objects within an application. Each type of operation is associated with a particular HTTP verb:
|
||||
|
||||
* `GET`: Retrieve an object or list of objects
|
||||
* `POST`: Create an object
|
||||
|
||||
@@ -4,12 +4,6 @@ Devices can be organized by functional roles, which are fully customizable by th
|
||||
|
||||
## Fields
|
||||
|
||||
### Parent
|
||||
|
||||
!!! info "This field was introduced in NetBox v4.3."
|
||||
|
||||
The parent role of which this role is a child (optional).
|
||||
|
||||
### Name
|
||||
|
||||
A unique human-friendly name.
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
# Inventory Items
|
||||
|
||||
!!! warning "Deprecation Warning"
|
||||
Beginning in NetBox v4.3, the use of inventory items has been deprecated. They are planned for removal in a future NetBox release. Users are strongly encouraged to begin using [modules](./module.md) and [module types](./moduletype.md) in place of inventory items. Modules provide enhanced functionality and can be configured with user-defined attributes.
|
||||
|
||||
Inventory items represent hardware components installed within a device, such as a power supply or CPU or line card. They are intended to be used primarily for inventory purposes.
|
||||
|
||||
Inventory items are hierarchical in nature, such that any individual item may be designated as the parent for other items. For example, an inventory item might be created to represent a line card which houses several SFP optics, each of which exists as a child item within the device. An inventory item may also be associated with a specific component within the same device. For example, you may wish to associate a transceiver with an interface.
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
# Inventory Item Roles
|
||||
|
||||
!!! warning "Deprecation Warning"
|
||||
Beginning in NetBox v4.3, the use of inventory items has been deprecated. They are planned for removal in a future NetBox release. Users are strongly encouraged to begin using [modules](./module.md) and [module types](./moduletype.md) in place of inventory items. Modules provide enhanced functionality and can be configured with user-defined attributes.
|
||||
|
||||
Inventory items can be organized by functional roles, which are fully customizable by the user. For example, you might create roles for power supplies, fans, interface optics, etc.
|
||||
|
||||
## Fields
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# Inventory Item Templates
|
||||
|
||||
!!! warning "Deprecation Warning"
|
||||
Beginning in NetBox v4.3, the use of inventory items has been deprecated. They are planned for removal in a future NetBox release. Users are strongly encouraged to begin using [modules](./module.md) and [module types](./moduletype.md) in place of inventory items. Modules provide enhanced functionality and can be configured with user-defined attributes.
|
||||
|
||||
A template for an inventory item that will be automatically created when instantiating a new device. All attributes of this object will be copied to the new inventory item, including the associations with a parent item and assigned component, if any. See the [inventory item](./inventoryitem.md) documentation for more detail.
|
||||
|
||||
@@ -43,11 +43,3 @@ The numeric weight of the module, including a unit designation (e.g. 3 kilograms
|
||||
### Airflow
|
||||
|
||||
The direction in which air circulates through the device chassis for cooling.
|
||||
|
||||
### Profile
|
||||
|
||||
The assigned [profile](./moduletypeprofile.md) for the type of module. Profiles can be used to classify module types by function (e.g. power supply, hard disk, etc.), and they support the addition of user-configurable attributes on module types. The assignment of a module type to a profile is optional.
|
||||
|
||||
### Attributes
|
||||
|
||||
Depending on the module type's assigned [profile](./moduletypeprofile.md) (if any), one or more user-defined attributes may be available to configure.
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
# Module Type Profiles
|
||||
|
||||
!!! info "This model was introduced in NetBox v4.3."
|
||||
|
||||
Each [module type](./moduletype.md) may optionally be assigned a profile according to its classification. A profile can extend module types with user-configured attributes. For example, you might want to specify the input current and voltage of a power supply, or the clock speed and number of cores for a processor.
|
||||
|
||||
Module type attributes are managed via the configuration of a [JSON schema](https://json-schema.org/) on the profile. For example, the following schema introduces three module type attributes, two of which are designated as required attributes.
|
||||
|
||||
```json
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"title": "Disk type",
|
||||
"enum": ["HD", "SSD", "NVME"],
|
||||
"default": "HD"
|
||||
},
|
||||
"capacity": {
|
||||
"type": "integer",
|
||||
"title": "Capacity (GB)",
|
||||
"description": "Gross disk size"
|
||||
},
|
||||
"speed": {
|
||||
"type": "integer",
|
||||
"title": "Speed (RPM)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type", "capacity"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The assignment of module types to a profile is optional. The designation of a schema for a profile is also optional: A profile can be used simply as a mechanism for classifying module types if the addition of custom attributes is not needed.
|
||||
|
||||
## Fields
|
||||
|
||||
### Schema
|
||||
|
||||
This field holds the [JSON schema](https://json-schema.org/) for the profile. The configured JSON schema must be valid (or the field must be null).
|
||||
@@ -40,9 +40,7 @@ The number of the numerically lowest unit in the rack. This value defaults to on
|
||||
|
||||
### Outer Dimensions
|
||||
|
||||
The external width, height and depth of the rack can be tracked to aid in floorplan calculations. These measurements must be designated in either millimeters or inches.
|
||||
|
||||
!!! info "The `outer_height` field was introduced in NetBox v4.3."
|
||||
The external width and depth of the rack can be tracked to aid in floorplan calculations. These measurements must be designated in either millimeters or inches.
|
||||
|
||||
### Mounting Depth
|
||||
|
||||
|
||||
@@ -12,6 +12,10 @@ See the [configuration rendering documentation](../../features/configuration-ren
|
||||
|
||||
A unique human-friendly name.
|
||||
|
||||
### Weight
|
||||
|
||||
A numeric value which influences the order in which context data is merged. Contexts with a lower weight are merged before those with a higher weight.
|
||||
|
||||
### Data File
|
||||
|
||||
Template code may optionally be sourced from a remote [data file](../core/datafile.md), which is synchronized from a remote data source. When designating a data file, there is no need to specify template code: It will be populated automatically from the data file.
|
||||
@@ -23,27 +27,3 @@ Jinja2 template code, if being defined locally rather than replicated from a dat
|
||||
### Environment Parameters
|
||||
|
||||
A dictionary of any additional parameters to pass when instantiating the [Jinja2 environment](https://jinja.palletsprojects.com/en/3.1.x/api/#jinja2.Environment). Jinja2 supports various optional parameters which can be used to modify its default behavior.
|
||||
|
||||
### MIME Type
|
||||
|
||||
!!! info "This field was introduced in NetBox v4.3."
|
||||
|
||||
The MIME type to indicate in the response when rendering the configuration template (optional). Defaults to `text/plain`.
|
||||
|
||||
### File Name
|
||||
|
||||
!!! info "This field was introduced in NetBox v4.3."
|
||||
|
||||
The file name to give to the rendered export file (optional).
|
||||
|
||||
### File Extension
|
||||
|
||||
!!! info "This field was introduced in NetBox v4.3."
|
||||
|
||||
The file extension to append to the file name in the response (optional).
|
||||
|
||||
### As Attachment
|
||||
|
||||
!!! info "This field was introduced in NetBox v4.3."
|
||||
|
||||
If selected, the rendered content will be returned as a file attachment, rather than displayed directly in-browser (where supported).
|
||||
@@ -20,20 +20,10 @@ Template code may optionally be sourced from a remote [data file](../core/datafi
|
||||
|
||||
Jinja2 template code for rendering the exported data.
|
||||
|
||||
### Environment Parameters
|
||||
|
||||
!!! info "This field was introduced in NetBox v4.3."
|
||||
|
||||
A dictionary of any additional parameters to pass when instantiating the [Jinja2 environment](https://jinja.palletsprojects.com/en/3.1.x/api/#jinja2.Environment). Jinja2 supports various optional parameters which can be used to modify its default behavior.
|
||||
|
||||
### MIME Type
|
||||
|
||||
The MIME type to indicate in the response when rendering the export template (optional). Defaults to `text/plain`.
|
||||
|
||||
### File Name
|
||||
|
||||
The file name to give to the rendered export file (optional).
|
||||
|
||||
### File Extension
|
||||
|
||||
The file extension to append to the file name in the response (optional).
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
# Table Configs
|
||||
|
||||
This object represents the saved configuration of an object table in NetBox. Table configs can be crafted, saved, and shared among users to apply specific views within object lists. Each table config can specify which table columns to display, the order in which to display them, and which columns are used for sorting.
|
||||
|
||||
For example, you might wish to create a table config for the devices list to assist in inventory tasks. This view might show the device name, location, serial number, and asset tag, but omit operational details like IP addresses. Once applied, this table config can be saved for reuse in future audits.
|
||||
|
||||
## Fields
|
||||
|
||||
### Name
|
||||
|
||||
A human-friendly name for the table config.
|
||||
|
||||
### User
|
||||
|
||||
The user to which this filter belongs. The current user will be assigned automatically when saving a table config via the UI, and cannot be changed.
|
||||
|
||||
### Object Type
|
||||
|
||||
The type of NetBox object to which the table config pertains.
|
||||
|
||||
### Table
|
||||
|
||||
The name of the specific table to which the table config pertains. (Some NetBox object use multiple tables.)
|
||||
|
||||
### Weight
|
||||
|
||||
A numeric weight used to influence the order in which table configs are listed. Table configs with a lower weight will be listed before those with a higher weight. Table configs having the same weight will be ordered alphabetically.
|
||||
|
||||
### Enabled
|
||||
|
||||
Determines whether this table config can be used. Disabled table configs will not appear as options in the UI, however they will be included in API results.
|
||||
|
||||
### Shared
|
||||
|
||||
Determines whether this table config is intended for use by all users or only its owner. Note that deselecting this option does **not** hide the table config from other users; it is merely excluded from the list of available table configs in UI object list views.
|
||||
|
||||
### Ordering
|
||||
|
||||
A list of column names by which the table is to be ordered. If left blank, the table's default ordering will be used.
|
||||
|
||||
### Columns
|
||||
|
||||
A list of columns to be displayed in the table. The table will render these columns in the order they appear in the list. At least one column must be selected.
|
||||
@@ -16,12 +16,6 @@ A unique URL-friendly identifier. (This value will be used for filtering.) This
|
||||
|
||||
The color to use when displaying the tag in the NetBox UI.
|
||||
|
||||
### Weight
|
||||
|
||||
A numeric weight employed to influence the ordering of tags. Tags with a lower weight will be listed before those with higher weights. Values must be within the range **0** to **32767**.
|
||||
|
||||
!!! info "This field was introduced in NetBox v4.3."
|
||||
|
||||
### Object Types
|
||||
|
||||
The assignment of a tag may be limited to a prescribed set of objects. For example, it may be desirable to limit the application of a specific tag to only devices and virtual machines.
|
||||
|
||||
@@ -2,12 +2,6 @@
|
||||
|
||||
This model represents an arbitrary range of individual IPv4 or IPv6 addresses, inclusive of its starting and ending addresses. For instance, the range 192.0.2.10 to 192.0.2.20 has eleven members. (The total member count is available as the `size` property on an IPRange instance.) Like [prefixes](./prefix.md) and [IP addresses](./ipaddress.md), each IP range may optionally be assigned to a [VRF](./vrf.md).
|
||||
|
||||
Each IP range can be marked as populated, which instructs NetBox to treat the range as though every IP address within it has been created (even though these individual IP addresses don't actually exist in the database). This can be helpful in scenarios where the management of a subset of IP addresses has been deferred to an external system of record, such as a DHCP server. NetBox will prohibit the creation of individual IP addresses within a range that has been marked as populated.
|
||||
|
||||
An IP range can also be marked as utilized. This will cause its utilization to always be reported as 100% when viewing the range or when calculating the utilization of a parent prefix. (If not enabled, a range's utilization is calculated based on the number of IP addresses which have been created within it.)
|
||||
|
||||
Typically, IP ranges marked as populated should also be marked as utilized, although there may be scenarios where this is undesirable (e.g. when reclaiming old IP space). An IP range which has been marked as populated but _not_ marked as utilized will always report a utilization of 0%, as it cannot contain child IP addresses.
|
||||
|
||||
## Fields
|
||||
|
||||
### VRF
|
||||
@@ -35,12 +29,6 @@ The IP range's operational status. Note that the status of a range does _not_ ha
|
||||
!!! tip
|
||||
Additional statuses may be defined by setting `IPRange.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
|
||||
|
||||
### Mark Populated
|
||||
|
||||
!!! note "This field was added in NetBox v4.3."
|
||||
|
||||
If enabled, NetBox will treat this IP range as being fully populated when calculating available IP space. It will also prevent the creation of IP addresses which fall within the declared range (and assigned VRF, if any).
|
||||
|
||||
### Mark Utilized
|
||||
|
||||
If enabled, the IP range will be considered 100% utilized regardless of how many IP addresses are defined within it. This is useful for documenting DHCP ranges, for example.
|
||||
|
||||
@@ -6,15 +6,6 @@ To aid in the efficient creation of services, users may opt to first create a [s
|
||||
|
||||
## Fields
|
||||
|
||||
### Parent
|
||||
|
||||
The parent object to which the service is assigned. This must be one of [Device](../dcim/device.md),
|
||||
[VirtualMachine](../virtualization/virtualmachine.md), or [FHRP Group](./fhrpgroup.md).
|
||||
|
||||
!!! note "Changed in NetBox v4.3"
|
||||
|
||||
Previously, `parent` was a property that pointed to either a Device or Virtual Machine. With the capability to assign services to FHRP groups, this is a unified in a concrete field.
|
||||
|
||||
### Name
|
||||
|
||||
A service or protocol name.
|
||||
|
||||
@@ -4,11 +4,9 @@ A contact represents an individual or group that has been associated with an obj
|
||||
|
||||
## Fields
|
||||
|
||||
### Groups
|
||||
### Group
|
||||
|
||||
The [contact groups](./contactgroup.md) to which this contact is assigned (if any).
|
||||
|
||||
!!! info "This field was renamed from `group` to `groups` in NetBox v4.3, and now supports the assignment of a contact to more than one group."
|
||||
The [contact group](./contactgroup.md) to which this contact is assigned (if any).
|
||||
|
||||
### Name
|
||||
|
||||
|
||||
@@ -61,11 +61,6 @@ class MyModelViewSet(...):
|
||||
|
||||
The `TagFilter` class is available for all models which support tag assignment (those which inherit from `NetBoxModel` or `TagsMixin`). This filter subclasses django-filter's `ModelMultipleChoiceFilter` to work with NetBox's `TaggedItem` class.
|
||||
|
||||
This class filters `tags` using the `slug` field. For example:
|
||||
|
||||
`GET /api/dcim/sites/?tag=alpha&tag=bravo`
|
||||
|
||||
|
||||
```python
|
||||
from django_filters import FilterSet
|
||||
from extras.filters import TagFilter
|
||||
@@ -73,19 +68,3 @@ from extras.filters import TagFilter
|
||||
class MyModelFilterSet(FilterSet):
|
||||
tag = TagFilter()
|
||||
```
|
||||
|
||||
### TagIDFilter
|
||||
|
||||
The `TagIDFilter` class is available for all models which support tag assignment (those which inherit from `NetBoxModel` or `TagsMixin`). This filter subclasses django-filter's `ModelMultipleChoiceFilter` to work with NetBox's `TaggedItem` class.
|
||||
|
||||
This class filters `tags` using the `id` field. For example:
|
||||
|
||||
`GET /api/dcim/sites/?tag_id=100&tag_id=200`
|
||||
|
||||
```python
|
||||
from django_filters import FilterSet
|
||||
from extras.filters import TagIDFilter
|
||||
|
||||
class MyModelFilterSet(FilterSet):
|
||||
tag_id = TagIDFilter()
|
||||
```
|
||||
|
||||
@@ -205,7 +205,6 @@ To ease development, it is recommended to go ahead and install the plugin at thi
|
||||
```no-highlight
|
||||
$ pip install -e .
|
||||
```
|
||||
|
||||
More information on editable builds can be found at [Editable installs for pyproject.toml ](https://peps.python.org/pep-0660/).
|
||||
|
||||
## Configure NetBox
|
||||
|
||||
@@ -117,10 +117,6 @@ For more information about database migrations, see the [Django documentation](h
|
||||
|
||||
::: netbox.models.features.CloningMixin
|
||||
|
||||
::: netbox.models.features.ContactsMixin
|
||||
|
||||
!!! info "Plugin support for ContactsMixin was introduced in NetBox v4.3."
|
||||
|
||||
::: netbox.models.features.CustomLinksMixin
|
||||
|
||||
::: netbox.models.features.CustomFieldsMixin
|
||||
@@ -129,6 +125,9 @@ For more information about database migrations, see the [Django documentation](h
|
||||
|
||||
::: netbox.models.features.EventRulesMixin
|
||||
|
||||
!!! note
|
||||
`EventRulesMixin` was renamed from `WebhooksMixin` in NetBox v3.7.
|
||||
|
||||
::: netbox.models.features.ExportTemplatesMixin
|
||||
|
||||
::: netbox.models.features.JobsMixin
|
||||
|
||||
@@ -198,7 +198,6 @@ Plugins can inject custom content into certain areas of core NetBox views. This
|
||||
|
||||
| Method | View | Description |
|
||||
|---------------------|-------------|-----------------------------------------------------|
|
||||
| `head()` | All | Custom HTML `<head>` block includes |
|
||||
| `navbar()` | All | Inject content inside the top navigation bar |
|
||||
| `list_buttons()` | List view | Add buttons to the top of the page |
|
||||
| `buttons()` | Object view | Add buttons to the top of the page |
|
||||
|
||||
@@ -10,15 +10,6 @@ Minor releases are published in April, August, and December of each calendar yea
|
||||
|
||||
This page contains a history of all major and minor releases since NetBox v2.0. For more detail on a specific patch release, please see the release notes page for that specific minor release.
|
||||
|
||||
#### [Version 4.3](./version-4.3.md) (May 2025)
|
||||
|
||||
* Module Type Profiles & Custom Attributes ([#19002](https://github.com/netbox-community/netbox/issues/19002))
|
||||
* Reusable Table Configurations ([#14591](https://github.com/netbox-community/netbox/issues/14591))
|
||||
* Option to Treat IP Ranges as Fully Populated ([#9763](https://github.com/netbox-community/netbox/issues/9763))
|
||||
* Hierarchical Device Roles ([#18245](https://github.com/netbox-community/netbox/issues/18245))
|
||||
* Periodic Synchronization of Data Sources ([#18287](https://github.com/netbox-community/netbox/issues/18287))
|
||||
* Proxy Routing ([#18627](https://github.com/netbox-community/netbox/issues/18627))
|
||||
|
||||
#### [Version 4.2](./version-4.2.md) (January 2025)
|
||||
|
||||
* Assign Multiple MAC Addresses per Interface ([#4867](https://github.com/netbox-community/netbox/issues/4867))
|
||||
|
||||
@@ -150,5 +150,5 @@ The [NAPALM automation](https://github.com/napalm-automation/napalm) library pro
|
||||
* Modified the interface serializer to include three discrete fields relating to connections: `is_connected` (boolean), `interface_connection`, and `circuit_termination`
|
||||
* Added two new fields to the inventory item serializer: `asset_tag` and `description`
|
||||
* Added "wireless" to interface type filter (in addition to physical, virtual, and LAG)
|
||||
* Added a new endpoint at /api/ipam/prefixes/<pk\>/available-ips/ to retrieve or create available IPs within a prefix
|
||||
* Added a new endpoint at /api/ipam/prefixes/<pk>/available-ips/ to retrieve or create available IPs within a prefix
|
||||
* Extended `parent_device` on DeviceSerializer to include the `url` and `display_name` of the parent Device, and the `url` of the DeviceBay
|
||||
|
||||
@@ -1,126 +1,5 @@
|
||||
# NetBox v4.2
|
||||
|
||||
## v4.2.7 (2025-04-10)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#16144](https://github.com/netbox-community/netbox/issues/16144) - Add support for plugin models to GetReturnURLMixin
|
||||
* [#18138](https://github.com/netbox-community/netbox/issues/18138) - Enable filtering of ObjectVar and MultiObjectVar input selections for custom fields
|
||||
* [#18656](https://github.com/netbox-community/netbox/issues/18656) - Enable FHRP group assignment when bulk importing IP addresses
|
||||
* [#18980](https://github.com/netbox-community/netbox/issues/18980) - Optimize bulk updates of custom field values when custom fields are added/removed
|
||||
* [#19018](https://github.com/netbox-community/netbox/issues/19018) - Add MoCA interface type
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#18553](https://github.com/netbox-community/netbox/issues/18553) - Avoid clearing site of assigned virtual machines when editing a cluster
|
||||
* [#18738](https://github.com/netbox-community/netbox/issues/18738) - Respect declared ordering of custom scripts within a module
|
||||
* [#18895](https://github.com/netbox-community/netbox/issues/18895) - Fix GraphQL support for interfaces which terminate virtual circuits
|
||||
* [#18904](https://github.com/netbox-community/netbox/issues/18904) - Add missing tags column to config contexts table
|
||||
* [#18964](https://github.com/netbox-community/netbox/issues/18964) - Fix "select all" behavior on object lists
|
||||
* [#18965](https://github.com/netbox-community/netbox/issues/18965) - "Run script" button should respect default commit toggle for custom scripts
|
||||
* [#18991](https://github.com/netbox-community/netbox/issues/18991) - Fix cable path tracing for pass-through ports in REST API
|
||||
* [#18999](https://github.com/netbox-community/netbox/issues/18999) - Fix filtering of inventory items with no manufacturer in GraphQL API
|
||||
* [#19021](https://github.com/netbox-community/netbox/issues/19021) - Preserve JSONField stylign when `help_text` is passed
|
||||
* [#19023](https://github.com/netbox-community/netbox/issues/19023) - `get_field_value()` should honor null values on bound form fields
|
||||
* [#19030](https://github.com/netbox-community/netbox/issues/19030) - Prevent pagination buttons from overlapping bulk action buttons on object lists
|
||||
* [#19041](https://github.com/netbox-community/netbox/issues/19041) - Fix `IndexError` exception when creating multiple front ports with a label
|
||||
* [#19092](https://github.com/netbox-community/netbox/issues/19092) - Fix clearing of scope field when bulk editing prefixes
|
||||
* [#19122](https://github.com/netbox-community/netbox/issues/19122) - Fix styling of server error page
|
||||
|
||||
---
|
||||
|
||||
## v4.2.6 (2025-03-21)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#17503](https://github.com/netbox-community/netbox/issues/17503) - Add rack title above rack on rack detail view
|
||||
* [#17686](https://github.com/netbox-community/netbox/issues/17686) - Add config option for disk space divisor
|
||||
* [#18579](https://github.com/netbox-community/netbox/issues/18579) - Update filtersets and filter forms to include contact filters where missing
|
||||
* [#18744](https://github.com/netbox-community/netbox/issues/18744) - Ensure contact link in tables is hyperlinked
|
||||
* [#18816](https://github.com/netbox-community/netbox/issues/18816) - Add FC/UPC, FC/APC and FC/PC port types
|
||||
* [#18880](https://github.com/netbox-community/netbox/issues/18880) - Delay enqueuing background tasks until DB transaction is committed to avoid race condition
|
||||
* [#18939](https://github.com/netbox-community/netbox/issues/18939) - Support site group search for ASNs
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#18409](https://github.com/netbox-community/netbox/issues/18409) - Eliminate N+1 issue by adding generic prefetch operation to Interface API endpoint
|
||||
* [#18557](https://github.com/netbox-community/netbox/issues/18557) - Update JSONField to enclose bare string values in quotes
|
||||
* [#18582](https://github.com/netbox-community/netbox/issues/18582) - Fix prefix bulk import with associated VLAN and conflicting VLAN IDs
|
||||
* [#18742](https://github.com/netbox-community/netbox/issues/18742) - Ensure location list and detail views show related VLAN group information
|
||||
* [#18782](https://github.com/netbox-community/netbox/issues/18782) - Ensure misconfigured object list widgets on the dashboard now degrade gracefully
|
||||
* [#18833](https://github.com/netbox-community/netbox/issues/18833) - Fix inventory item bulk edit to ensure that component name and type are both validated Ensure
|
||||
* [#18838](https://github.com/netbox-community/netbox/issues/18838) - Ensure that local context data correctly rejects falsy values
|
||||
* [#18845](https://github.com/netbox-community/netbox/issues/18845) - Restore default sort behavior of name column on devices list view
|
||||
* [#18863](https://github.com/netbox-community/netbox/issues/18863) - Exempt MPTT-based models from ordering fix introduced in #18279
|
||||
* [#18869](https://github.com/netbox-community/netbox/issues/18869) - Ensure numeric conversion helper always return a clean decimal value
|
||||
* [#18872](https://github.com/netbox-community/netbox/issues/18872) - Ensure that `kind` is a required field when making journal entries
|
||||
* [#18884](https://github.com/netbox-community/netbox/issues/18884) - Ensure tag deserialization is handled correctly
|
||||
* [#18887](https://github.com/netbox-community/netbox/issues/18887) - Allow VM interface objects to be set on prefix object-type custom field
|
||||
* [#18926](https://github.com/netbox-community/netbox/issues/18926) - Fix icon displayed for GitHub authentication on login page
|
||||
* [#18928](https://github.com/netbox-community/netbox/issues/18928) - Support cascading deletions when cleaning up expired changelog records
|
||||
* [#18933](https://github.com/netbox-community/netbox/issues/18933) - Allow filtering VLAN groups by associated site groups
|
||||
* [#18944](https://github.com/netbox-community/netbox/issues/18944) - Ensure clearing "Widget type" field when adding widgets to dashboard does not cause a "ValueError: Unregistered widget class" error
|
||||
* [#18949](https://github.com/netbox-community/netbox/issues/18949) - Add missing contacts property to GraphQL types where the associated model has a connection to a contact
|
||||
|
||||
---
|
||||
|
||||
## v4.2.5 (2025-03-06)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#17357](https://github.com/netbox-community/netbox/issues/17357) - Use VirtualChassis name as fallback for unnamed devices
|
||||
* [#17542](https://github.com/netbox-community/netbox/issues/17542) - Add contact assignments to VPN tunnels
|
||||
* [#17944](https://github.com/netbox-community/netbox/issues/17944) - Allow script inputs to be filtered on ObjectVar and MultiObjectVar selections
|
||||
* [#18024](https://github.com/netbox-community/netbox/issues/18024) - Add permalink URL pattern to match a custom script by module and class name
|
||||
* [#18141](https://github.com/netbox-community/netbox/issues/18141) - Support "Quick Add" for plugins
|
||||
* [#18403](https://github.com/netbox-community/netbox/issues/18403) - Improve performance of job list views
|
||||
* [#18693](https://github.com/netbox-community/netbox/issues/18693) - Support setting VLAN translation on bulk edit of interfaces
|
||||
* [#18772](https://github.com/netbox-community/netbox/issues/18772) - Add "type" filter for virtual circuits
|
||||
* [#18774](https://github.com/netbox-community/netbox/issues/18774) - Add tooltip preview of tag descriptions when hovering over tags
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#15016](https://github.com/netbox-community/netbox/issues/15016) - Prevent AssertionError when adding multiple devices "mid-span" in a cable trace
|
||||
* [#15924](https://github.com/netbox-community/netbox/issues/15924) - Prevent setting tagged VLANs on interfaces with mode: tagged-all
|
||||
* [#17488](https://github.com/netbox-community/netbox/issues/17488) - Ensure VLANGroup.vid_ranges shows up in API results
|
||||
* [#17709](https://github.com/netbox-community/netbox/issues/17709) - Allow primary key for nested models in OpenAPI request schemas
|
||||
* [#17796](https://github.com/netbox-community/netbox/issues/17796) - Fix IndexError on "Create & Add Another" operation on custom field choices
|
||||
* [#18605](https://github.com/netbox-community/netbox/issues/18605) - Limit VLAN selection dropdown to choices appropriate to site
|
||||
* [#18722](https://github.com/netbox-community/netbox/issues/18722) - Improve UI feedback on failed script execution
|
||||
* [#18729](https://github.com/netbox-community/netbox/issues/18729) - Fix unpredictable ordering on querysets with annotations/groupings
|
||||
* [#18753](https://github.com/netbox-community/netbox/issues/18753) - Prevent webhooks from being triggered on a script dry-run
|
||||
* [#18758](https://github.com/netbox-community/netbox/issues/18758) - Fix FieldError when sorting by account count field in providers list
|
||||
* [#18768](https://github.com/netbox-community/netbox/issues/18768) - Fix removing a secondary MAC address from an interface
|
||||
|
||||
---
|
||||
|
||||
## v4.2.4 (2025-02-21)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#17309](https://github.com/netbox-community/netbox/issues/17309) - Omit empty counts in related object tables
|
||||
* [#18277](https://github.com/netbox-community/netbox/issues/18277) - Improve multi-table inheritance in serialization of change-logged models
|
||||
* [#18286](https://github.com/netbox-community/netbox/issues/18286) - Add more job duration choices
|
||||
* [#18357](https://github.com/netbox-community/netbox/issues/18357) - Display author name in plugin list for locally installed plugins
|
||||
* [#18408](https://github.com/netbox-community/netbox/issues/18408) - Add Paused status for virtual machines
|
||||
* [#18584](https://github.com/netbox-community/netbox/issues/18584) - Add rack type column to manufacturer list
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#17436](https://github.com/netbox-community/netbox/issues/17436) - Fix {module} replacement in module bays
|
||||
* [#18013](https://github.com/netbox-community/netbox/issues/18013) - Limit object type to selected object in change log filter
|
||||
* [#18241](https://github.com/netbox-community/netbox/issues/18241) - Default logging level of custom scripts changed to INFO
|
||||
* [#18247](https://github.com/netbox-community/netbox/issues/18247) - Fix visibility of disabled cable paths in dark mode
|
||||
* [#18480](https://github.com/netbox-community/netbox/issues/18480) - Clean data passed to script in runscript command
|
||||
* [#18555](https://github.com/netbox-community/netbox/issues/18555) - Add default get_absolute_url method to plugin models
|
||||
* [#18585](https://github.com/netbox-community/netbox/issues/18585) - Fix filtering circuits by location
|
||||
* [#18593](https://github.com/netbox-community/netbox/issues/18593) - Fix "Create & Add Another" IP Address workflow
|
||||
* [#18594](https://github.com/netbox-community/netbox/issues/18594) - Enable sorting by ASN count on site and provider lists
|
||||
* [#18619](https://github.com/netbox-community/netbox/issues/18619) - Ensure shift-click selection selects only visible list items
|
||||
* [#18674](https://github.com/netbox-community/netbox/issues/18674) - Preserve form values when selecting speed on circuit termination
|
||||
|
||||
---
|
||||
|
||||
## v4.2.3 (2025-02-04)
|
||||
|
||||
### Enhancements
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
## v4.3.0-beta1 (2025-04-14)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* The GraphQL API Now uses an advanced syntax for filtering, to enable e.g. logical AND/OR filtering and custom field lookups.
|
||||
* PostgreSQL 13 is no longer supported. NetBox v4.3 requires PostgreSQL 14.0 or later.
|
||||
* The `ALLOW_TOKEN_RETRIEVAL` configuration parameter now defaults to False.
|
||||
* The `device` and `virtual_machine` foreign keys on the Service model have been replaced with a generic `parent` relationship to support the assignment of services to FHRP groups as well.
|
||||
* The `group` foreign key on the Contact model has been replaced with a many-to-many `groups` field.
|
||||
* PluginTemplateExtension no longer supports registration via the singular `model` attribute (use `models` instead).
|
||||
* The legacy staged changes functionality has been removed.
|
||||
|
||||
### New Features
|
||||
|
||||
#### Module Type Profiles & Custom Attributes ([#19002](https://github.com/netbox-community/netbox/issues/19002))
|
||||
|
||||
The new [module type profile](../models/dcim/moduletypeprofile.md) model enables users to declare custom profiles for module types, with the ability to define custom attributes for each profile according to its functional role. For example, a CPU module type might declare architecture and clock speed attributes; a hard disk profile might declare attributes for type and speed.
|
||||
|
||||
Attributes can be declared on each profile using [JSON schema](https://json-schema.org/), which allows for attributes to be declared as strings (text), integers, decimals, booleans, or choice fields. Profile attributes render as individual form fields when modifying a module type. Several profiles have been included by default to serve as examples, however these may be modified or removed.
|
||||
|
||||
#### Reusable Table Configurations ([#14591](https://github.com/netbox-community/netbox/issues/14591))
|
||||
|
||||
After modifying the displayed columns and/or ordering for a specific object table in the user interface, users now have the option to save that configuration so that it can be reused in the future. Similar to saved filters, table configs can be shared with other users to easily replicate table layouts crafted to serve specific use cases.
|
||||
|
||||
#### Option to Treat IP Ranges as Fully Populated ([#9763](https://github.com/netbox-community/netbox/issues/9763))
|
||||
|
||||
A new `mark_populated` boolean field has been added to the IPRange model. If set to true, NetBox will consider the IP range to be fully populated, and will not permit the creation of individual IP addresses within the range. For example, you might defer the management of an IP range to an external DHCP server, and wish for NetBox to treat the range as a opaque monolithic block for planning and allocation purposes.
|
||||
|
||||
#### Hierarchical Device Roles ([#18245](https://github.com/netbox-community/netbox/issues/18245))
|
||||
|
||||
Device roles can now be arranged hierarchically, with one role optionally serving as a parent to one or more child roles. For example, you might wish to create a generic "Server" role for devices with "Application Server" and "Database Server" roles beneath it. A device could then be assigned to any of these three roles.
|
||||
|
||||
#### Periodic Synchronization of Data Sources ([#18287](https://github.com/netbox-community/netbox/issues/18287))
|
||||
|
||||
Data sources can now be configured to synchronize automatically at a specified interval, as indicated by the new `sync_interval` field. No additional system configuration is necessary to support this functionality; background jobs will be scheduled automatically by the RQ worker process.
|
||||
|
||||
#### Proxy Routing ([#18627](https://github.com/netbox-community/netbox/issues/18627))
|
||||
|
||||
User can now declare one or more proxy routers via the `PROXY_ROUTERS` configuration parameter to control the use of specific proxy servers for various outbound connections. For example, it is now possible to configure NetBox to use different proxies based on the type of outbound traffic or its destination.
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#7598](https://github.com/netbox-community/netbox/issues/7598) - Adopt advanced query filtering in GraphQL API to support filtering by custom fields
|
||||
* [#8423](https://github.com/netbox-community/netbox/issues/8423) - Enable assigning services to FHRP groups
|
||||
* [#15842](https://github.com/netbox-community/netbox/issues/15842) - Introduce the `LOGIN_FORM_HIDDEN` configuration parameter
|
||||
* [#16224](https://github.com/netbox-community/netbox/issues/16224) - Implement pagination support for the GraphQL API
|
||||
* [#17170](https://github.com/netbox-community/netbox/issues/17170) - Enable the assignment of a contact to multiple contact groups
|
||||
* [#17443](https://github.com/netbox-community/netbox/issues/17443) - Add a `file_name` field to the export template model
|
||||
* [#17602](https://github.com/netbox-community/netbox/issues/17602) - Add a `comments` field to all nested group models (Region, SiteGroup, Location, ContactGroup, TenantGroup, and WirelessLANGroup)
|
||||
* [#17608](https://github.com/netbox-community/netbox/issues/17608) - Add a `status` field to the L2VPN model
|
||||
* [#17653](https://github.com/netbox-community/netbox/issues/17653) - Enable declaring Jinja environment parameters on export templates (similar to config templates)
|
||||
* [#17793](https://github.com/netbox-community/netbox/issues/17793) - Introduce a REST API endpoint for tagged objects (`/api/extras/tagged-objects/`)
|
||||
* [#17841](https://github.com/netbox-community/netbox/issues/17841) - Add a `weight` field to the Tag model to influence ordering
|
||||
* [#18296](https://github.com/netbox-community/netbox/issues/18296) - Add a `tenant` field to the VLAN group model
|
||||
* [#18352](https://github.com/netbox-community/netbox/issues/18352) - Add a `status` field to the power outlet model
|
||||
* [#18417](https://github.com/netbox-community/netbox/issues/18417) - Add an `outer_height` field to the rack & rack type models
|
||||
* [#18535](https://github.com/netbox-community/netbox/issues/18535) - The presence of incompatible plugins will no longer prevent NetBox from starting
|
||||
* [#18780](https://github.com/netbox-community/netbox/issues/18780) - Introduce `DATABASES` and `DATABASE_ROUTERS` configuration parameters to enable defining connections to external databases (e.g. for plugins)
|
||||
* [#18783](https://github.com/netbox-community/netbox/issues/18783) - Enable filtering all applicable models by tag ID
|
||||
* [#18785](https://github.com/netbox-community/netbox/issues/18785) - Enable custom choices for rack, device, and module airflow
|
||||
|
||||
### Plugins
|
||||
|
||||
* [#16630](https://github.com/netbox-community/netbox/issues/16630) - Plugins can now inject content within the HTML `<head>` block via the new `plugin_head()` method on PluginTemplateExtension
|
||||
* [#17424](https://github.com/netbox-community/netbox/issues/17424) - Extend ViewTab with a `visible` argument to control tab rendering
|
||||
* [#17857](https://github.com/netbox-community/netbox/issues/17857) - Added a `release_track` attribute to PluginConfig
|
||||
* [#18305](https://github.com/netbox-community/netbox/issues/18305) - Introduce plugin support for ContactsMixin
|
||||
* [#19073](https://github.com/netbox-community/netbox/issues/19073) - Allow installed plugins to be omitted from the plugins list
|
||||
|
||||
### Other Changes
|
||||
|
||||
* [#18071](https://github.com/netbox-community/netbox/issues/18071) - Removed legacy staged changed functionality in favor of the [netbox-branching](https://github.com/netboxlabs/netbox-branching) plugin
|
||||
* [#18072](https://github.com/netbox-community/netbox/issues/18072) - Drop support for the singular `model` attribute on PluginTemplateExtension (use `models` instead)
|
||||
* [#18191](https://github.com/netbox-community/netbox/issues/18191) - Remove redundant PostgreSQL indexes
|
||||
* [#18236](https://github.com/netbox-community/netbox/issues/18236) - Upgrade the HTMX library to v2.0
|
||||
* [#18540](https://github.com/netbox-community/netbox/issues/18540) - Operational plugins are now recorded in the application registry
|
||||
* [#18623](https://github.com/netbox-community/netbox/issues/18623) - Upgrade the Tabler CSS theme to v1.0
|
||||
* [#18743](https://github.com/netbox-community/netbox/issues/18743) - Upgrade Django to v5.2
|
||||
* [#18751](https://github.com/netbox-community/netbox/issues/18751) - Change the default value for `ALLOW_TOKEN_RETRIEVAL` to False
|
||||
* [#18808](https://github.com/netbox-community/netbox/issues/18808) - Squashed migration dependencies have been altered to rectify an issue with Django's `sqlmigrate` management command
|
||||
* [#18820](https://github.com/netbox-community/netbox/issues/18820) - PostgreSQL 13 is no longer supported
|
||||
* [#19004](https://github.com/netbox-community/netbox/issues/19004) - The use of inventory items has been deprecated in favor of modules. Inventory items and roles may be removed in a future NetBox release.
|
||||
|
||||
### REST API Changes
|
||||
|
||||
* Added the following endpoints:
|
||||
* `/api/extras/table-configs/`
|
||||
* `/api/extras/tagged-objects/`
|
||||
* `/api/dcim/module-type-profiles/`
|
||||
* core.DataSource
|
||||
* Added the optional `sync_interval` field
|
||||
* dcim.DeviceRole
|
||||
* Added the optional `parent` recursive foreign key field to effect hierarchical ordering
|
||||
* Added a `comments` field
|
||||
* dcim.Location
|
||||
* Added a `comments` field
|
||||
* dcim.ModuleType
|
||||
* Added the optional `profile` foreign key to the new ModuleTypeProfile model
|
||||
* dcim.PowerOutlet
|
||||
* Added a `status` field
|
||||
* dcim.Rack
|
||||
* Added the optional `outer_height` field
|
||||
* dcim.RackType
|
||||
* Added the optional `outer_height` field
|
||||
* dcim.Region
|
||||
* Added a `comments` field
|
||||
* dcim.SiteGroup
|
||||
* Added a `comments` field
|
||||
* extras.ConfigTemplate
|
||||
* Added optional fields `mime_type`, `file_name`, `file_extension` and `as_attachment`
|
||||
* extras.ExportTemplate
|
||||
* Added optional fields `file_name` and `environment_params` (JSON)
|
||||
* extras.Tag
|
||||
* Added a `weight` field
|
||||
* ipam.IPRange
|
||||
* Added a `mark_populaed` boolean field
|
||||
* ipam.L2VPN
|
||||
* Added a `status` field
|
||||
* ipam.Service
|
||||
* Removed the `device` and `virtual_machine` foreign key fields
|
||||
* Added the `parent_object_type`, `parent_object_id`, and (read-only) `parent` fields
|
||||
* ipam.VLANGroup
|
||||
* Added the optional `tenant` foreign key field
|
||||
* tenancy.Contact
|
||||
* Removed the `group` foreign key field
|
||||
* Added the `groups` many-to-many field
|
||||
* tenancy.ContactGroup
|
||||
* Added a `comments` field
|
||||
* tenancy.TenantGroup
|
||||
* Added a `comments` field
|
||||
* wireless.WirelessLANGroup
|
||||
* Added a `comments` field
|
||||
@@ -59,8 +59,6 @@ markdown_extensions:
|
||||
format: !!python/name:pymdownx.superfences.fence_code_format
|
||||
- pymdownx.tabbed:
|
||||
alternate_style: true
|
||||
not_in_nav: |
|
||||
/index.md
|
||||
nav:
|
||||
- Introduction: 'introduction.md'
|
||||
- Features:
|
||||
@@ -202,7 +200,6 @@ nav:
|
||||
- ModuleBay: 'models/dcim/modulebay.md'
|
||||
- ModuleBayTemplate: 'models/dcim/modulebaytemplate.md'
|
||||
- ModuleType: 'models/dcim/moduletype.md'
|
||||
- ModuleTypeProfile: 'models/dcim/moduletypeprofile.md'
|
||||
- Platform: 'models/dcim/platform.md'
|
||||
- PowerFeed: 'models/dcim/powerfeed.md'
|
||||
- PowerOutlet: 'models/dcim/poweroutlet.md'
|
||||
@@ -236,7 +233,6 @@ nav:
|
||||
- NotificationGroup: 'models/extras/notificationgroup.md'
|
||||
- SavedFilter: 'models/extras/savedfilter.md'
|
||||
- Subscription: 'models/extras/subscription.md'
|
||||
- TableConfig: 'models/extras/tableconfig.md'
|
||||
- Tag: 'models/extras/tag.md'
|
||||
- Webhook: 'models/extras/webhook.md'
|
||||
- IPAM:
|
||||
@@ -308,7 +304,6 @@ nav:
|
||||
- git Cheat Sheet: 'development/git-cheat-sheet.md'
|
||||
- Release Notes:
|
||||
- Summary: 'release-notes/index.md'
|
||||
- Version 4.3: 'release-notes/version-4.3.md'
|
||||
- Version 4.2: 'release-notes/version-4.2.md'
|
||||
- Version 4.1: 'release-notes/version-4.1.md'
|
||||
- Version 4.0: 'release-notes/version-4.0.md'
|
||||
|
||||
@@ -95,7 +95,7 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
|
||||
)
|
||||
|
||||
|
||||
class ProviderAccountFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
|
||||
class ProviderAccountFilterSet(NetBoxModelFilterSet):
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Provider.objects.all(),
|
||||
label=_('Provider (ID)'),
|
||||
@@ -234,11 +234,6 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
|
||||
to_field_name='slug',
|
||||
label=_('Site (slug)'),
|
||||
)
|
||||
location_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='terminations___location',
|
||||
label=_('Location (ID)'),
|
||||
queryset=Location.objects.all(),
|
||||
)
|
||||
termination_a_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=CircuitTermination.objects.all(),
|
||||
label=_('Termination A (ID)'),
|
||||
|
||||
@@ -66,12 +66,11 @@ class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class ProviderAccountFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
class ProviderAccountFilterForm(NetBoxModelFilterSetForm):
|
||||
model = ProviderAccount
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('provider_id', 'account', name=_('Attributes')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
)
|
||||
provider_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
@@ -127,7 +126,7 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
|
||||
'type_id', 'status', 'install_date', 'termination_date', 'commit_rate', 'distance', 'distance_unit',
|
||||
name=_('Attributes')
|
||||
),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')),
|
||||
FieldSet('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')),
|
||||
)
|
||||
@@ -182,11 +181,6 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
|
||||
},
|
||||
label=_('Site')
|
||||
)
|
||||
location_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
required=False,
|
||||
label=_('Location')
|
||||
)
|
||||
install_date = forms.DateField(
|
||||
label=_('Install date'),
|
||||
required=False,
|
||||
@@ -328,7 +322,7 @@ class VirtualCircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBox
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
|
||||
FieldSet('type_id', 'status', name=_('Attributes')),
|
||||
FieldSet('type', 'status', name=_('Attributes')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'provider_id', 'provider_network_id')
|
||||
|
||||
@@ -32,8 +32,7 @@ __all__ = (
|
||||
@strawberry_django.type(
|
||||
models.Provider,
|
||||
fields='__all__',
|
||||
filters=ProviderFilter,
|
||||
pagination=True
|
||||
filters=ProviderFilter
|
||||
)
|
||||
class ProviderType(NetBoxObjectType, ContactsMixin):
|
||||
|
||||
@@ -46,10 +45,9 @@ class ProviderType(NetBoxObjectType, ContactsMixin):
|
||||
@strawberry_django.type(
|
||||
models.ProviderAccount,
|
||||
fields='__all__',
|
||||
filters=ProviderAccountFilter,
|
||||
pagination=True
|
||||
filters=ProviderAccountFilter
|
||||
)
|
||||
class ProviderAccountType(ContactsMixin, NetBoxObjectType):
|
||||
class ProviderAccountType(NetBoxObjectType):
|
||||
provider: Annotated["ProviderType", strawberry.lazy('circuits.graphql.types')]
|
||||
|
||||
circuits: List[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]
|
||||
@@ -58,8 +56,7 @@ class ProviderAccountType(ContactsMixin, NetBoxObjectType):
|
||||
@strawberry_django.type(
|
||||
models.ProviderNetwork,
|
||||
fields='__all__',
|
||||
filters=ProviderNetworkFilter,
|
||||
pagination=True
|
||||
filters=ProviderNetworkFilter
|
||||
)
|
||||
class ProviderNetworkType(NetBoxObjectType):
|
||||
provider: Annotated["ProviderType", strawberry.lazy('circuits.graphql.types')]
|
||||
@@ -70,8 +67,7 @@ class ProviderNetworkType(NetBoxObjectType):
|
||||
@strawberry_django.type(
|
||||
models.CircuitTermination,
|
||||
exclude=['termination_type', 'termination_id', '_location', '_region', '_site', '_site_group', '_provider_network'],
|
||||
filters=CircuitTerminationFilter,
|
||||
pagination=True
|
||||
filters=CircuitTerminationFilter
|
||||
)
|
||||
class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, ObjectType):
|
||||
circuit: Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]
|
||||
@@ -90,8 +86,7 @@ class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, Ob
|
||||
@strawberry_django.type(
|
||||
models.CircuitType,
|
||||
fields='__all__',
|
||||
filters=CircuitTypeFilter,
|
||||
pagination=True
|
||||
filters=CircuitTypeFilter
|
||||
)
|
||||
class CircuitTypeType(OrganizationalObjectType):
|
||||
color: str
|
||||
@@ -102,8 +97,7 @@ class CircuitTypeType(OrganizationalObjectType):
|
||||
@strawberry_django.type(
|
||||
models.Circuit,
|
||||
fields='__all__',
|
||||
filters=CircuitFilter,
|
||||
pagination=True
|
||||
filters=CircuitFilter
|
||||
)
|
||||
class CircuitType(NetBoxObjectType, ContactsMixin):
|
||||
provider: ProviderType
|
||||
@@ -119,8 +113,7 @@ class CircuitType(NetBoxObjectType, ContactsMixin):
|
||||
@strawberry_django.type(
|
||||
models.CircuitGroup,
|
||||
fields='__all__',
|
||||
filters=CircuitGroupFilter,
|
||||
pagination=True
|
||||
filters=CircuitGroupFilter
|
||||
)
|
||||
class CircuitGroupType(OrganizationalObjectType):
|
||||
tenant: TenantType | None
|
||||
@@ -129,8 +122,7 @@ class CircuitGroupType(OrganizationalObjectType):
|
||||
@strawberry_django.type(
|
||||
models.CircuitGroupAssignment,
|
||||
exclude=['member_type', 'member_id'],
|
||||
filters=CircuitGroupAssignmentFilter,
|
||||
pagination=True
|
||||
filters=CircuitGroupAssignmentFilter
|
||||
)
|
||||
class CircuitGroupAssignmentType(TagsMixin, BaseObjectType):
|
||||
group: Annotated["CircuitGroupType", strawberry.lazy('circuits.graphql.types')]
|
||||
@@ -146,8 +138,7 @@ class CircuitGroupAssignmentType(TagsMixin, BaseObjectType):
|
||||
@strawberry_django.type(
|
||||
models.VirtualCircuitType,
|
||||
fields='__all__',
|
||||
filters=VirtualCircuitTypeFilter,
|
||||
pagination=True
|
||||
filters=VirtualCircuitTypeFilter
|
||||
)
|
||||
class VirtualCircuitTypeType(OrganizationalObjectType):
|
||||
color: str
|
||||
@@ -158,8 +149,7 @@ class VirtualCircuitTypeType(OrganizationalObjectType):
|
||||
@strawberry_django.type(
|
||||
models.VirtualCircuitTermination,
|
||||
fields='__all__',
|
||||
filters=VirtualCircuitTerminationFilter,
|
||||
pagination=True
|
||||
filters=VirtualCircuitTerminationFilter
|
||||
)
|
||||
class VirtualCircuitTerminationType(CustomFieldsMixin, TagsMixin, ObjectType):
|
||||
virtual_circuit: Annotated[
|
||||
@@ -175,8 +165,7 @@ class VirtualCircuitTerminationType(CustomFieldsMixin, TagsMixin, ObjectType):
|
||||
@strawberry_django.type(
|
||||
models.VirtualCircuit,
|
||||
fields='__all__',
|
||||
filters=VirtualCircuitFilter,
|
||||
pagination=True
|
||||
filters=VirtualCircuitFilter
|
||||
)
|
||||
class VirtualCircuitType(NetBoxObjectType):
|
||||
provider_network: ProviderNetworkType = strawberry_django.field(select_related=["provider_network"])
|
||||
|
||||
@@ -39,6 +39,9 @@ class Migration(migrations.Migration):
|
||||
name='termination_type',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
limit_choices_to=models.Q(
|
||||
('model__in', ('region', 'sitegroup', 'site', 'location', 'providernetwork'))
|
||||
),
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='+',
|
||||
|
||||
@@ -51,6 +51,7 @@ class Migration(migrations.Migration):
|
||||
name='member_type',
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
limit_choices_to=models.Q(('app_label', 'circuits'), ('model__in', ['circuit', 'virtualcircuit'])),
|
||||
related_name='+',
|
||||
to='contenttypes.contenttype',
|
||||
blank=True,
|
||||
@@ -67,6 +68,7 @@ class Migration(migrations.Migration):
|
||||
model_name='circuitgroupassignment',
|
||||
name='member_type',
|
||||
field=models.ForeignKey(
|
||||
limit_choices_to=models.Q(('app_label', 'circuits'), ('model__in', ['circuit', 'virtualcircuit'])),
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='+',
|
||||
to='contenttypes.contenttype'
|
||||
|
||||
@@ -182,6 +182,7 @@ class CircuitGroupAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin,
|
||||
"""
|
||||
member_type = models.ForeignKey(
|
||||
to='contenttypes.ContentType',
|
||||
limit_choices_to=CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='+'
|
||||
)
|
||||
@@ -248,6 +249,7 @@ class CircuitTermination(
|
||||
termination_type = models.ForeignKey(
|
||||
to='contenttypes.ContentType',
|
||||
on_delete=models.PROTECT,
|
||||
limit_choices_to=Q(model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES),
|
||||
related_name='+',
|
||||
blank=True,
|
||||
null=True
|
||||
@@ -347,8 +349,9 @@ class CircuitTermination(
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Must define either site *or* provider network
|
||||
if self.termination is None:
|
||||
raise ValidationError(_("A circuit termination must attach to a terminating object."))
|
||||
raise ValidationError(_("A circuit termination must attach to termination."))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Cache objects associated with the terminating object (for filtering)
|
||||
|
||||
@@ -111,7 +111,7 @@ class CircuitTerminationTable(NetBoxTable):
|
||||
provider = tables.Column(
|
||||
verbose_name=_('Provider'),
|
||||
linkify=True,
|
||||
accessor='circuit__provider'
|
||||
accessor='circuit.provider'
|
||||
)
|
||||
term_side = tables.Column(
|
||||
verbose_name=_('Side')
|
||||
|
||||
@@ -23,6 +23,7 @@ class ProviderTable(ContactsColumnMixin, NetBoxTable):
|
||||
verbose_name=_('Accounts')
|
||||
)
|
||||
account_count = columns.LinkedCountColumn(
|
||||
accessor=tables.A('accounts__count'),
|
||||
viewname='circuits:provideraccount_list',
|
||||
url_params={'provider_id': 'pk'},
|
||||
verbose_name=_('Account Count')
|
||||
@@ -32,6 +33,7 @@ class ProviderTable(ContactsColumnMixin, NetBoxTable):
|
||||
verbose_name=_('ASNs')
|
||||
)
|
||||
asn_count = columns.LinkedCountColumn(
|
||||
accessor=tables.A('asns__count'),
|
||||
viewname='ipam:asn_list',
|
||||
url_params={'provider_id': 'pk'},
|
||||
verbose_name=_('ASN Count')
|
||||
|
||||
@@ -3,10 +3,8 @@ from django.test import TestCase
|
||||
from circuits.choices import *
|
||||
from circuits.filtersets import *
|
||||
from circuits.models import *
|
||||
from dcim.choices import InterfaceTypeChoices, LocationStatusChoices
|
||||
from dcim.models import (
|
||||
Cable, Device, DeviceRole, DeviceType, Interface, Location, Manufacturer, Region, Site, SiteGroup
|
||||
)
|
||||
from dcim.choices import InterfaceTypeChoices
|
||||
from dcim.models import Cable, Device, DeviceRole, DeviceType, Interface, Manufacturer, Region, Site, SiteGroup
|
||||
from ipam.models import ASN, RIR
|
||||
from netbox.choices import DistanceUnitChoices
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
@@ -227,17 +225,6 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
)
|
||||
ProviderNetwork.objects.bulk_create(provider_networks)
|
||||
|
||||
locations = (
|
||||
Location.objects.create(
|
||||
site=sites[0], name='Test Location 1', slug='test-location-1',
|
||||
status=LocationStatusChoices.STATUS_ACTIVE,
|
||||
),
|
||||
Location.objects.create(
|
||||
site=sites[1], name='Test Location 2', slug='test-location-2',
|
||||
status=LocationStatusChoices.STATUS_ACTIVE,
|
||||
),
|
||||
)
|
||||
|
||||
circuits = (
|
||||
Circuit(
|
||||
provider=providers[0],
|
||||
@@ -318,9 +305,7 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
|
||||
circuit_terminations = ((
|
||||
CircuitTermination(circuit=circuits[0], termination=sites[0], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[0], termination=locations[0], term_side='Z'),
|
||||
CircuitTermination(circuit=circuits[1], termination=sites[1], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[1], termination=locations[1], term_side='Z'),
|
||||
CircuitTermination(circuit=circuits[2], termination=sites[2], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[3], termination=provider_networks[0], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[4], termination=provider_networks[1], term_side='A'),
|
||||
@@ -410,11 +395,6 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'site': [sites[0].slug, sites[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_location(self):
|
||||
location_ids = Location.objects.values_list('id', flat=True)[:2]
|
||||
params = {'location_id': location_ids}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_tenant(self):
|
||||
tenants = Tenant.objects.all()[:2]
|
||||
params = {'tenant_id': [tenants[0].pk, tenants[1].pk]}
|
||||
|
||||
@@ -4,8 +4,8 @@ from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim.views import PathTraceView
|
||||
from ipam.models import ASN
|
||||
from netbox.views import generic
|
||||
from tenancy.views import ObjectContactsView
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.query import count_related
|
||||
from utilities.views import GetRelatedModelsMixin, register_model_view
|
||||
@@ -20,9 +20,7 @@ from .models import *
|
||||
@register_model_view(Provider, 'list', path='', detail=False)
|
||||
class ProviderListView(generic.ObjectListView):
|
||||
queryset = Provider.objects.annotate(
|
||||
count_circuits=count_related(Circuit, 'provider'),
|
||||
asn_count=count_related(ASN, 'providers'),
|
||||
account_count=count_related(ProviderAccount, 'provider'),
|
||||
count_circuits=count_related(Circuit, 'provider')
|
||||
)
|
||||
filterset = filtersets.ProviderFilterSet
|
||||
filterset_form = forms.ProviderFilterForm
|
||||
@@ -76,6 +74,11 @@ class ProviderBulkDeleteView(generic.BulkDeleteView):
|
||||
table = tables.ProviderTable
|
||||
|
||||
|
||||
@register_model_view(Provider, 'contacts')
|
||||
class ProviderContactsView(ObjectContactsView):
|
||||
queryset = Provider.objects.all()
|
||||
|
||||
|
||||
#
|
||||
# ProviderAccounts
|
||||
#
|
||||
@@ -138,6 +141,11 @@ class ProviderAccountBulkDeleteView(generic.BulkDeleteView):
|
||||
table = tables.ProviderAccountTable
|
||||
|
||||
|
||||
@register_model_view(ProviderAccount, 'contacts')
|
||||
class ProviderAccountContactsView(ObjectContactsView):
|
||||
queryset = ProviderAccount.objects.all()
|
||||
|
||||
|
||||
#
|
||||
# Provider networks
|
||||
#
|
||||
@@ -405,6 +413,11 @@ class CircuitSwapTerminations(generic.ObjectEditView):
|
||||
})
|
||||
|
||||
|
||||
@register_model_view(Circuit, 'contacts')
|
||||
class CircuitContactsView(ObjectContactsView):
|
||||
queryset = Circuit.objects.all()
|
||||
|
||||
|
||||
#
|
||||
# Circuit terminations
|
||||
#
|
||||
|
||||
@@ -2,13 +2,12 @@ import re
|
||||
import typing
|
||||
from collections import OrderedDict
|
||||
|
||||
from drf_spectacular.extensions import OpenApiSerializerFieldExtension, OpenApiSerializerExtension, _SchemaType
|
||||
from drf_spectacular.extensions import OpenApiSerializerFieldExtension
|
||||
from drf_spectacular.openapi import AutoSchema
|
||||
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 drf_spectacular.utils import Direction
|
||||
|
||||
from netbox.api.fields import ChoiceField
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
@@ -278,40 +277,3 @@ class FixSerializedPKRelatedField(OpenApiSerializerFieldExtension):
|
||||
return component.ref if component else None
|
||||
else:
|
||||
return build_basic_type(OpenApiTypes.INT)
|
||||
|
||||
|
||||
class FixIntegerRangeSerializerSchema(OpenApiSerializerExtension):
|
||||
target_class = 'netbox.api.fields.IntegerRangeSerializer'
|
||||
|
||||
def map_serializer(self, auto_schema: 'AutoSchema', direction: Direction) -> _SchemaType:
|
||||
return {
|
||||
'type': 'array',
|
||||
'items': {
|
||||
'type': 'array',
|
||||
'items': {
|
||||
'type': 'integer',
|
||||
},
|
||||
'minItems': 2,
|
||||
'maxItems': 2,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# Nested models can be passed by ID in requests
|
||||
# The logic for this is handled in `BaseModelSerializer.to_internal_value`
|
||||
class FixWritableNestedSerializerAllowPK(OpenApiSerializerFieldExtension):
|
||||
target_class = 'netbox.api.serializers.BaseModelSerializer'
|
||||
match_subclasses = True
|
||||
|
||||
def map_serializer_field(self, auto_schema, direction):
|
||||
schema = auto_schema._map_serializer_field(self.target, direction, bypass_extensions=True)
|
||||
if schema is None:
|
||||
return schema
|
||||
if direction == 'request' and self.target.nested:
|
||||
return {
|
||||
'oneOf': [
|
||||
build_basic_type(OpenApiTypes.INT),
|
||||
schema,
|
||||
]
|
||||
}
|
||||
return schema
|
||||
|
||||
@@ -3,10 +3,7 @@ from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.db import models
|
||||
from django.db.migrations.operations import AlterModelOptions
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from core.events import *
|
||||
from netbox.events import EventType, EVENT_TYPE_KIND_DANGER, EVENT_TYPE_KIND_SUCCESS, EVENT_TYPE_KIND_WARNING
|
||||
from utilities.migration import custom_deconstruct
|
||||
|
||||
# Ignore verbose_name & verbose_name_plural Meta options when calculating model migrations
|
||||
@@ -22,7 +19,6 @@ class CoreConfig(AppConfig):
|
||||
|
||||
def ready(self):
|
||||
from core.api import schema # noqa: F401
|
||||
from core.checks import check_duplicate_indexes # noqa: F401
|
||||
from netbox.models.features import register_models
|
||||
from . import data_backends, events, search # noqa: F401
|
||||
from netbox import context_managers # noqa: F401
|
||||
@@ -30,15 +26,6 @@ class CoreConfig(AppConfig):
|
||||
# Register models
|
||||
register_models(*self.get_models())
|
||||
|
||||
# Register core events
|
||||
EventType(OBJECT_CREATED, _('Object created')).register()
|
||||
EventType(OBJECT_UPDATED, _('Object updated')).register()
|
||||
EventType(OBJECT_DELETED, _('Object deleted'), destructive=True).register()
|
||||
EventType(JOB_STARTED, _('Job started')).register()
|
||||
EventType(JOB_COMPLETED, _('Job completed'), kind=EVENT_TYPE_KIND_SUCCESS).register()
|
||||
EventType(JOB_FAILED, _('Job failed'), kind=EVENT_TYPE_KIND_WARNING).register()
|
||||
EventType(JOB_ERRORED, _('Job errored'), kind=EVENT_TYPE_KIND_DANGER).register()
|
||||
|
||||
# Clear Redis cache on startup in development mode
|
||||
if settings.DEBUG:
|
||||
try:
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
from django.core.checks import Error, register, Tags
|
||||
from django.db.models import Index, UniqueConstraint
|
||||
from django.apps import apps
|
||||
|
||||
__all__ = (
|
||||
'check_duplicate_indexes',
|
||||
)
|
||||
|
||||
|
||||
@register(Tags.models)
|
||||
def check_duplicate_indexes(app_configs, **kwargs):
|
||||
"""
|
||||
Check for an index which is redundant to a declared unique constraint.
|
||||
"""
|
||||
errors = []
|
||||
|
||||
for model in apps.get_models():
|
||||
if not (meta := getattr(model, "_meta", None)):
|
||||
continue
|
||||
|
||||
index_fields = {
|
||||
tuple(index.fields) for index in getattr(meta, 'indexes', [])
|
||||
if isinstance(index, Index)
|
||||
}
|
||||
constraint_fields = {
|
||||
tuple(constraint.fields) for constraint in getattr(meta, 'constraints', [])
|
||||
if isinstance(constraint, UniqueConstraint)
|
||||
}
|
||||
|
||||
# Find overlapping definitions
|
||||
if duplicated := index_fields & constraint_fields:
|
||||
for fields in duplicated:
|
||||
errors.append(
|
||||
Error(
|
||||
f"Model '{model.__name__}' defines the same field set {fields} in both `Meta.indexes` and "
|
||||
f"`Meta.constraints`.",
|
||||
obj=model,
|
||||
)
|
||||
)
|
||||
|
||||
return errors
|
||||
@@ -81,10 +81,8 @@ class JobIntervalChoices(ChoiceSet):
|
||||
CHOICES = (
|
||||
(INTERVAL_MINUTELY, _('Minutely')),
|
||||
(INTERVAL_HOURLY, _('Hourly')),
|
||||
(INTERVAL_HOURLY * 12, _('12 hours')),
|
||||
(INTERVAL_DAILY, _('Daily')),
|
||||
(INTERVAL_WEEKLY, _('Weekly')),
|
||||
(INTERVAL_DAILY * 30, _('30 days')),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from netbox.events import EventType, EVENT_TYPE_KIND_DANGER, EVENT_TYPE_KIND_SUCCESS, EVENT_TYPE_KIND_WARNING
|
||||
|
||||
__all__ = (
|
||||
'JOB_COMPLETED',
|
||||
'JOB_ERRORED',
|
||||
@@ -18,3 +22,12 @@ JOB_STARTED = 'job_started'
|
||||
JOB_COMPLETED = 'job_completed'
|
||||
JOB_FAILED = 'job_failed'
|
||||
JOB_ERRORED = 'job_errored'
|
||||
|
||||
# Register core events
|
||||
EventType(OBJECT_CREATED, _('Object created')).register()
|
||||
EventType(OBJECT_UPDATED, _('Object updated')).register()
|
||||
EventType(OBJECT_DELETED, _('Object deleted'), destructive=True).register()
|
||||
EventType(JOB_STARTED, _('Job started')).register()
|
||||
EventType(JOB_COMPLETED, _('Job completed'), kind=EVENT_TYPE_KIND_SUCCESS).register()
|
||||
EventType(JOB_FAILED, _('Job failed'), kind=EVENT_TYPE_KIND_WARNING).register()
|
||||
EventType(JOB_ERRORED, _('Job errored'), kind=EVENT_TYPE_KIND_DANGER).register()
|
||||
|
||||
@@ -67,7 +67,6 @@ class DataFileFilterForm(NetBoxModelFilterSetForm):
|
||||
|
||||
|
||||
class JobFilterForm(SavedFiltersMixin, FilterForm):
|
||||
model = Job
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id'),
|
||||
FieldSet('object_type', 'status', name=_('Attributes')),
|
||||
@@ -168,7 +167,6 @@ class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm):
|
||||
|
||||
|
||||
class ConfigRevisionFilterForm(SavedFiltersMixin, FilterForm):
|
||||
model = ConfigRevision
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id'),
|
||||
)
|
||||
|
||||
@@ -19,8 +19,7 @@ __all__ = (
|
||||
@strawberry_django.type(
|
||||
models.DataFile,
|
||||
exclude=['data',],
|
||||
filters=DataFileFilter,
|
||||
pagination=True
|
||||
filters=DataFileFilter
|
||||
)
|
||||
class DataFileType(BaseObjectType):
|
||||
source: Annotated["DataSourceType", strawberry.lazy('core.graphql.types')]
|
||||
@@ -29,8 +28,7 @@ class DataFileType(BaseObjectType):
|
||||
@strawberry_django.type(
|
||||
models.DataSource,
|
||||
fields='__all__',
|
||||
filters=DataSourceFilter,
|
||||
pagination=True
|
||||
filters=DataSourceFilter
|
||||
)
|
||||
class DataSourceType(NetBoxObjectType):
|
||||
|
||||
@@ -40,17 +38,12 @@ class DataSourceType(NetBoxObjectType):
|
||||
@strawberry_django.type(
|
||||
models.ObjectChange,
|
||||
fields='__all__',
|
||||
filters=ObjectChangeFilter,
|
||||
pagination=True
|
||||
filters=ObjectChangeFilter
|
||||
)
|
||||
class ObjectChangeType(BaseObjectType):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
DjangoContentType,
|
||||
fields='__all__',
|
||||
pagination=True
|
||||
)
|
||||
@strawberry_django.type(DjangoContentType, fields='__all__')
|
||||
class ContentType:
|
||||
pass
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# Generated by Django 5.2b1 on 2025-04-03 18:32
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0013_datasource_sync_interval'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveIndex(
|
||||
model_name='autosyncrecord',
|
||||
name='core_autosy_object__c17bac_idx',
|
||||
),
|
||||
migrations.RemoveIndex(
|
||||
model_name='datafile',
|
||||
name='core_datafile_source_path',
|
||||
),
|
||||
migrations.RemoveIndex(
|
||||
model_name='managedfile',
|
||||
name='core_managedfile_root_path',
|
||||
),
|
||||
]
|
||||
@@ -310,6 +310,9 @@ class DataFile(models.Model):
|
||||
name='%(app_label)s_%(class)s_unique_source_path'
|
||||
),
|
||||
)
|
||||
indexes = [
|
||||
models.Index(fields=('source', 'path'), name='core_datafile_source_path'),
|
||||
]
|
||||
verbose_name = _('data file')
|
||||
verbose_name_plural = _('data files')
|
||||
|
||||
@@ -384,5 +387,8 @@ class AutoSyncRecord(models.Model):
|
||||
name='%(app_label)s_%(class)s_object'
|
||||
),
|
||||
)
|
||||
indexes = (
|
||||
models.Index(fields=('object_type', 'object_id')),
|
||||
)
|
||||
verbose_name = _('auto sync record')
|
||||
verbose_name_plural = _('auto sync records')
|
||||
|
||||
@@ -58,6 +58,9 @@ class ManagedFile(SyncedDataMixin, models.Model):
|
||||
name='%(app_label)s_%(class)s_unique_root_path'
|
||||
),
|
||||
)
|
||||
indexes = [
|
||||
models.Index(fields=('file_root', 'file_path'), name='core_managedfile_root_path'),
|
||||
]
|
||||
verbose_name = _('managed file')
|
||||
verbose_name_plural = _('managed files')
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import uuid
|
||||
from functools import partial
|
||||
|
||||
import django_rq
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.db import models, transaction
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
@@ -259,12 +258,10 @@ class Job(models.Model):
|
||||
|
||||
# Schedule the job to run at a specific date & time.
|
||||
elif schedule_at:
|
||||
callback = partial(queue.enqueue_at, schedule_at, func, job_id=str(job.job_id), job=job, **kwargs)
|
||||
transaction.on_commit(callback)
|
||||
queue.enqueue_at(schedule_at, func, job_id=str(job.job_id), job=job, **kwargs)
|
||||
|
||||
# Schedule the job to run asynchronously at this first available opportunity.
|
||||
else:
|
||||
callback = partial(queue.enqueue, func, job_id=str(job.job_id), job=job, **kwargs)
|
||||
transaction.on_commit(callback)
|
||||
queue.enqueue(func, job_id=str(job.job_id), job=job, **kwargs)
|
||||
|
||||
return job
|
||||
|
||||
@@ -87,13 +87,6 @@ def get_local_plugins(plugins=None):
|
||||
if plugin_config.release_track:
|
||||
installed_version = f'{installed_version}-{plugin_config.release_track}'
|
||||
|
||||
if plugin_config.author:
|
||||
author = PluginAuthor(
|
||||
name=plugin_config.author,
|
||||
)
|
||||
else:
|
||||
author = None
|
||||
|
||||
local_plugins[plugin_config.name] = Plugin(
|
||||
config_name=plugin_config.name,
|
||||
title_short=plugin_config.verbose_name,
|
||||
@@ -105,7 +98,6 @@ def get_local_plugins(plugins=None):
|
||||
installed_version=installed_version,
|
||||
netbox_min_version=plugin_config.min_version,
|
||||
netbox_max_version=plugin_config.max_version,
|
||||
author=author,
|
||||
)
|
||||
|
||||
# Update catalog entries for local plugins, or add them to the list if not listed
|
||||
@@ -117,13 +109,6 @@ def get_local_plugins(plugins=None):
|
||||
else:
|
||||
plugins[k] = v
|
||||
|
||||
# Update plugin table config for hidden and static plugins
|
||||
hidden = settings.PLUGINS_CATALOG_CONFIG.get('hidden', [])
|
||||
static = settings.PLUGINS_CATALOG_CONFIG.get('static', [])
|
||||
for k, v in plugins.items():
|
||||
v.hidden = k in hidden
|
||||
v.static = k in static
|
||||
|
||||
return plugins
|
||||
|
||||
|
||||
|
||||
@@ -14,10 +14,10 @@ __all__ = (
|
||||
class DataSourceTable(NetBoxTable):
|
||||
name = tables.Column(
|
||||
verbose_name=_('Name'),
|
||||
linkify=True,
|
||||
linkify=True
|
||||
)
|
||||
type = BackendTypeColumn(
|
||||
verbose_name=_('Type'),
|
||||
verbose_name=_('Type')
|
||||
)
|
||||
status = columns.ChoiceFieldColumn(
|
||||
verbose_name=_('Status'),
|
||||
@@ -28,21 +28,18 @@ class DataSourceTable(NetBoxTable):
|
||||
sync_interval = columns.ChoiceFieldColumn(
|
||||
verbose_name=_('Sync interval'),
|
||||
)
|
||||
last_synced = tables.DateTimeColumn(
|
||||
verbose_name=_('Last Synced'),
|
||||
tags = columns.TagColumn(
|
||||
url_name='core:datasource_list'
|
||||
)
|
||||
file_count = tables.Column(
|
||||
verbose_name=_('Files'),
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='core:datasource_list',
|
||||
verbose_name='Files'
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = DataSource
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'type', 'status', 'enabled', 'source_url', 'description', 'sync_interval', 'comments',
|
||||
'parameters', 'last_synced', 'created', 'last_updated', 'file_count',
|
||||
'parameters', 'created', 'last_updated', 'file_count',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'type', 'status', 'enabled', 'description', 'sync_interval', 'file_count')
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import django_tables2 as tables
|
||||
from django.urls import reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.tables import BaseTable, columns
|
||||
@@ -43,7 +41,8 @@ class PluginVersionTable(BaseTable):
|
||||
|
||||
class CatalogPluginTable(BaseTable):
|
||||
title_long = tables.Column(
|
||||
verbose_name=_('Name'),
|
||||
linkify=('core:plugin', [tables.A('config_name')]),
|
||||
verbose_name=_('Name')
|
||||
)
|
||||
author = tables.Column(
|
||||
accessor=tables.A('author__name'),
|
||||
@@ -87,9 +86,3 @@ class CatalogPluginTable(BaseTable):
|
||||
# List installed plugins first, then certified plugins, then
|
||||
# everything else (with each tranche ordered alphabetically)
|
||||
order_by = ('-is_installed', '-is_certified', 'name')
|
||||
|
||||
def render_title_long(self, value, record):
|
||||
if record.static:
|
||||
return value
|
||||
url = reverse('core:plugin', args=[record.config_name])
|
||||
return mark_safe(f"<a href='{url}'>{value}</a>")
|
||||
|
||||
@@ -166,7 +166,7 @@ class DataFileBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
@register_model_view(Job, 'list', path='', detail=False)
|
||||
class JobListView(generic.ObjectListView):
|
||||
queryset = Job.objects.defer('data')
|
||||
queryset = Job.objects.all()
|
||||
filterset = filtersets.JobFilterSet
|
||||
filterset_form = forms.JobFilterForm
|
||||
table = tables.JobTable
|
||||
@@ -183,12 +183,12 @@ class JobView(generic.ObjectView):
|
||||
|
||||
@register_model_view(Job, 'delete')
|
||||
class JobDeleteView(generic.ObjectDeleteView):
|
||||
queryset = Job.objects.defer('data')
|
||||
queryset = Job.objects.all()
|
||||
|
||||
|
||||
@register_model_view(Job, 'bulk_delete', path='delete', detail=False)
|
||||
class JobBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Job.objects.defer('data')
|
||||
queryset = Job.objects.all()
|
||||
filterset = filtersets.JobFilterSet
|
||||
table = tables.JobTable
|
||||
|
||||
@@ -613,8 +613,6 @@ class PluginListView(BasePluginView):
|
||||
if q:
|
||||
plugins = [obj for obj in plugins if q.casefold() in obj.title_short.casefold()]
|
||||
|
||||
plugins = [plugin for plugin in plugins if not plugin.hidden]
|
||||
|
||||
table = CatalogPluginTable(plugins, user=request.user)
|
||||
table.configure(request)
|
||||
|
||||
|
||||
@@ -152,7 +152,6 @@ class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, Conne
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
status = ChoiceField(choices=PowerOutletStatusChoices, required=False)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
|
||||
@@ -4,8 +4,8 @@ from django.utils.translation import gettext as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.models import DeviceType, ModuleType, ModuleTypeProfile
|
||||
from netbox.api.fields import AttributesField, ChoiceField, RelatedObjectCountField
|
||||
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
|
||||
@@ -13,7 +13,6 @@ from .platforms import PlatformSerializer
|
||||
|
||||
__all__ = (
|
||||
'DeviceTypeSerializer',
|
||||
'ModuleTypeProfileSerializer',
|
||||
'ModuleTypeSerializer',
|
||||
)
|
||||
|
||||
@@ -63,23 +62,7 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
|
||||
brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'slug', 'description', 'device_count')
|
||||
|
||||
|
||||
class ModuleTypeProfileSerializer(NetBoxModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ModuleTypeProfile
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'description', 'schema', 'comments', 'tags', 'custom_fields',
|
||||
'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||
|
||||
|
||||
class ModuleTypeSerializer(NetBoxModelSerializer):
|
||||
profile = ModuleTypeProfileSerializer(
|
||||
nested=True,
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
manufacturer = ManufacturerSerializer(
|
||||
nested=True
|
||||
)
|
||||
@@ -95,17 +78,12 @@ class ModuleTypeSerializer(NetBoxModelSerializer):
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
attributes = AttributesField(
|
||||
source='attribute_data',
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ModuleType
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'profile', 'manufacturer', 'model', 'part_number', 'airflow',
|
||||
'weight', 'weight_unit', 'description', 'attributes', 'comments', 'tags', 'custom_fields', 'created',
|
||||
'last_updated',
|
||||
'id', 'url', 'display_url', 'display', 'manufacturer', 'model', 'part_number', 'airflow',
|
||||
'weight', 'weight_unit', 'description', 'comments', 'tags', 'custom_fields',
|
||||
'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'profile', 'manufacturer', 'model', 'description')
|
||||
brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'description')
|
||||
|
||||
@@ -52,13 +52,6 @@ class NestedLocationSerializer(WritableNestedSerializer):
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'rack_count', '_depth']
|
||||
|
||||
|
||||
class NestedDeviceRoleSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.DeviceRole
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedDeviceSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -70,8 +70,8 @@ class RackTypeSerializer(RackBaseSerializer):
|
||||
model = RackType
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'manufacturer', 'model', 'slug', 'description', 'form_factor',
|
||||
'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_height', 'outer_depth',
|
||||
'outer_unit', 'weight', 'max_weight', 'weight_unit', 'mounting_depth', 'description', 'comments', 'tags',
|
||||
'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'weight',
|
||||
'max_weight', 'weight_unit', 'mounting_depth', 'description', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'slug', 'description')
|
||||
@@ -129,9 +129,9 @@ class RackSerializer(RackBaseSerializer):
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status',
|
||||
'role', 'serial', 'asset_tag', 'rack_type', 'form_factor', 'width', 'u_height', 'starting_unit', 'weight',
|
||||
'max_weight', 'weight_unit', 'desc_units', 'outer_width', 'outer_height', 'outer_depth', 'outer_unit',
|
||||
'mounting_depth', 'airflow', 'description', 'comments', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', 'device_count', 'powerfeed_count',
|
||||
'max_weight', 'weight_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth',
|
||||
'airflow', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count',
|
||||
'powerfeed_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description', 'device_count')
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from dcim.models import DeviceRole, InventoryItemRole
|
||||
from extras.api.serializers_.configtemplates import ConfigTemplateSerializer
|
||||
from netbox.api.fields import RelatedObjectCountField
|
||||
from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer
|
||||
from .nested import NestedDeviceRoleSerializer
|
||||
from netbox.api.serializers import NetBoxModelSerializer
|
||||
|
||||
__all__ = (
|
||||
'DeviceRoleSerializer',
|
||||
@@ -10,8 +9,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class DeviceRoleSerializer(NestedGroupModelSerializer):
|
||||
parent = NestedDeviceRoleSerializer(required=False, allow_null=True, default=None)
|
||||
class DeviceRoleSerializer(NetBoxModelSerializer):
|
||||
config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None)
|
||||
|
||||
# Related object counts
|
||||
@@ -21,13 +19,10 @@ class DeviceRoleSerializer(NestedGroupModelSerializer):
|
||||
class Meta:
|
||||
model = DeviceRole
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'vm_role', 'config_template', 'parent',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'vm_role', 'config_template',
|
||||
'description', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
|
||||
'comments', '_depth',
|
||||
]
|
||||
brief_fields = (
|
||||
'id', 'url', 'display', 'name', 'slug', 'description', 'device_count', 'virtualmachine_count', '_depth'
|
||||
)
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'device_count', 'virtualmachine_count')
|
||||
|
||||
|
||||
class InventoryItemRoleSerializer(NetBoxModelSerializer):
|
||||
|
||||
@@ -27,7 +27,7 @@ class RegionSerializer(NestedGroupModelSerializer):
|
||||
model = Region
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', 'site_count', 'prefix_count', 'comments', '_depth',
|
||||
'created', 'last_updated', 'site_count', 'prefix_count', '_depth',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth')
|
||||
|
||||
@@ -41,7 +41,7 @@ class SiteGroupSerializer(NestedGroupModelSerializer):
|
||||
model = SiteGroup
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', 'site_count', 'prefix_count', 'comments', '_depth',
|
||||
'created', 'last_updated', 'site_count', 'prefix_count', '_depth',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth')
|
||||
|
||||
@@ -93,6 +93,6 @@ class LocationSerializer(NestedGroupModelSerializer):
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'site', 'parent', 'status', 'tenant', 'facility',
|
||||
'description', 'tags', 'custom_fields', 'created', 'last_updated', 'rack_count', 'device_count',
|
||||
'prefix_count', 'comments', '_depth',
|
||||
'prefix_count', '_depth',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count', '_depth')
|
||||
|
||||
@@ -21,7 +21,6 @@ router.register('rack-reservations', views.RackReservationViewSet)
|
||||
router.register('manufacturers', views.ManufacturerViewSet)
|
||||
router.register('device-types', views.DeviceTypeViewSet)
|
||||
router.register('module-types', views.ModuleTypeViewSet)
|
||||
router.register('module-type-profiles', views.ModuleTypeProfileViewSet)
|
||||
|
||||
# Device type components
|
||||
router.register('console-port-templates', views.ConsolePortTemplateViewSet)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from django.contrib.contenttypes.prefetch import GenericPrefetch
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
@@ -270,12 +269,6 @@ class DeviceTypeViewSet(NetBoxModelViewSet):
|
||||
filterset_class = filtersets.DeviceTypeFilterSet
|
||||
|
||||
|
||||
class ModuleTypeProfileViewSet(NetBoxModelViewSet):
|
||||
queryset = ModuleTypeProfile.objects.all()
|
||||
serializer_class = serializers.ModuleTypeProfileSerializer
|
||||
filterset_class = filtersets.ModuleTypeProfileFilterSet
|
||||
|
||||
|
||||
class ModuleTypeViewSet(NetBoxModelViewSet):
|
||||
queryset = ModuleType.objects.all()
|
||||
serializer_class = serializers.ModuleTypeSerializer
|
||||
@@ -449,18 +442,7 @@ class PowerOutletViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||
|
||||
class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||
queryset = Interface.objects.prefetch_related(
|
||||
GenericPrefetch(
|
||||
"cable__terminations__termination",
|
||||
[
|
||||
Interface.objects.select_related("device", "cable"),
|
||||
],
|
||||
),
|
||||
GenericPrefetch(
|
||||
"_path__path_objects",
|
||||
[
|
||||
Interface.objects.select_related("device", "cable"),
|
||||
],
|
||||
),
|
||||
'_path', 'cable__terminations',
|
||||
'l2vpn_terminations', # Referenced by InterfaceSerializer.l2vpn_termination
|
||||
'ip_addresses', # Referenced by Interface.count_ipaddresses()
|
||||
'fhrp_group_assignments', # Referenced by Interface.count_fhrp_groups()
|
||||
|
||||
@@ -128,15 +128,14 @@ class RackElevationDetailRenderChoices(ChoiceSet):
|
||||
|
||||
|
||||
class RackAirflowChoices(ChoiceSet):
|
||||
key = 'Rack.airflow'
|
||||
|
||||
FRONT_TO_REAR = 'front-to-rear'
|
||||
REAR_TO_FRONT = 'rear-to-front'
|
||||
|
||||
CHOICES = [
|
||||
CHOICES = (
|
||||
(FRONT_TO_REAR, _('Front to rear')),
|
||||
(REAR_TO_FRONT, _('Rear to front')),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@@ -192,7 +191,6 @@ class DeviceStatusChoices(ChoiceSet):
|
||||
|
||||
|
||||
class DeviceAirflowChoices(ChoiceSet):
|
||||
key = 'Device.airflow'
|
||||
|
||||
AIRFLOW_FRONT_TO_REAR = 'front-to-rear'
|
||||
AIRFLOW_REAR_TO_FRONT = 'rear-to-front'
|
||||
@@ -205,7 +203,7 @@ class DeviceAirflowChoices(ChoiceSet):
|
||||
AIRFLOW_PASSIVE = 'passive'
|
||||
AIRFLOW_MIXED = 'mixed'
|
||||
|
||||
CHOICES = [
|
||||
CHOICES = (
|
||||
(AIRFLOW_FRONT_TO_REAR, _('Front to rear')),
|
||||
(AIRFLOW_REAR_TO_FRONT, _('Rear to front')),
|
||||
(AIRFLOW_LEFT_TO_RIGHT, _('Left to right')),
|
||||
@@ -216,7 +214,7 @@ class DeviceAirflowChoices(ChoiceSet):
|
||||
(AIRFLOW_TOP_TO_BOTTOM, _('Top to bottom')),
|
||||
(AIRFLOW_PASSIVE, _('Passive')),
|
||||
(AIRFLOW_MIXED, _('Mixed')),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@@ -244,7 +242,6 @@ class ModuleStatusChoices(ChoiceSet):
|
||||
|
||||
|
||||
class ModuleAirflowChoices(ChoiceSet):
|
||||
key = 'Module.airflow'
|
||||
|
||||
FRONT_TO_REAR = 'front-to-rear'
|
||||
REAR_TO_FRONT = 'rear-to-front'
|
||||
@@ -253,14 +250,14 @@ class ModuleAirflowChoices(ChoiceSet):
|
||||
SIDE_TO_REAR = 'side-to-rear'
|
||||
PASSIVE = 'passive'
|
||||
|
||||
CHOICES = [
|
||||
CHOICES = (
|
||||
(FRONT_TO_REAR, _('Front to rear')),
|
||||
(REAR_TO_FRONT, _('Rear to front')),
|
||||
(LEFT_TO_RIGHT, _('Left to right')),
|
||||
(RIGHT_TO_LEFT, _('Right to left')),
|
||||
(SIDE_TO_REAR, _('Side to rear')),
|
||||
(PASSIVE, _('Passive')),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@@ -989,7 +986,6 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
|
||||
# Coaxial
|
||||
TYPE_DOCSIS = 'docsis'
|
||||
TYPE_MOCA = 'moca'
|
||||
|
||||
# PON
|
||||
TYPE_BPON = 'bpon'
|
||||
@@ -1186,7 +1182,6 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
_('Coaxial'),
|
||||
(
|
||||
(TYPE_DOCSIS, 'DOCSIS'),
|
||||
(TYPE_MOCA, 'MoCA'),
|
||||
)
|
||||
),
|
||||
(
|
||||
@@ -1350,9 +1345,6 @@ class PortTypeChoices(ChoiceSet):
|
||||
TYPE_SC_UPC = 'sc-upc'
|
||||
TYPE_SC_APC = 'sc-apc'
|
||||
TYPE_FC = 'fc'
|
||||
TYPE_FC_PC = 'fc-pc'
|
||||
TYPE_FC_UPC = 'fc-upc'
|
||||
TYPE_FC_APC = 'fc-apc'
|
||||
TYPE_LC = 'lc'
|
||||
TYPE_LC_PC = 'lc-pc'
|
||||
TYPE_LC_UPC = 'lc-upc'
|
||||
@@ -1413,9 +1405,6 @@ class PortTypeChoices(ChoiceSet):
|
||||
_('Fiber Optic'),
|
||||
(
|
||||
(TYPE_FC, 'FC'),
|
||||
(TYPE_FC_PC, 'FC/PC'),
|
||||
(TYPE_FC_UPC, 'FC/UPC'),
|
||||
(TYPE_FC_APC, 'FC/APC'),
|
||||
(TYPE_LC, 'LC'),
|
||||
(TYPE_LC_PC, 'LC/PC'),
|
||||
(TYPE_LC_UPC, 'LC/UPC'),
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
class UnsupportedCablePath(Exception):
|
||||
pass
|
||||
@@ -11,8 +11,7 @@ from ipam.filtersets import PrimaryIPFilterSet
|
||||
from ipam.models import ASN, IPAddress, VLANTranslationPolicy, VRF
|
||||
from netbox.choices import ColorChoices
|
||||
from netbox.filtersets import (
|
||||
AttributeFiltersMixin, BaseFilterSet, ChangeLoggedModelFilterSet, NestedGroupModelFilterSet, NetBoxModelFilterSet,
|
||||
OrganizationalModelFilterSet,
|
||||
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet,
|
||||
)
|
||||
from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
|
||||
from tenancy.models import *
|
||||
@@ -59,7 +58,6 @@ __all__ = (
|
||||
'ModuleBayTemplateFilterSet',
|
||||
'ModuleFilterSet',
|
||||
'ModuleTypeFilterSet',
|
||||
'ModuleTypeProfileFilterSet',
|
||||
'PathEndpointFilterSet',
|
||||
'PlatformFilterSet',
|
||||
'PowerConnectionFilterSet',
|
||||
@@ -83,7 +81,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class RegionFilterSet(NestedGroupModelFilterSet, ContactModelFilterSet):
|
||||
class RegionFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
label=_('Parent region (ID)'),
|
||||
@@ -113,7 +111,7 @@ class RegionFilterSet(NestedGroupModelFilterSet, ContactModelFilterSet):
|
||||
fields = ('id', 'name', 'slug', 'description')
|
||||
|
||||
|
||||
class SiteGroupFilterSet(NestedGroupModelFilterSet, ContactModelFilterSet):
|
||||
class SiteGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
label=_('Parent site group (ID)'),
|
||||
@@ -207,7 +205,7 @@ class SiteFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe
|
||||
return queryset.filter(qs_filter).distinct()
|
||||
|
||||
|
||||
class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, NestedGroupModelFilterSet):
|
||||
class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalModelFilterSet):
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region',
|
||||
@@ -277,13 +275,13 @@ class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, NestedGroupMode
|
||||
fields = ('id', 'name', 'slug', 'facility', 'description')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
# extended in order to include querying on Location.facility
|
||||
queryset = super().search(queryset, name, value)
|
||||
|
||||
if value.strip():
|
||||
queryset = queryset | queryset.model.objects.filter(facility__icontains=value)
|
||||
|
||||
return queryset
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(facility__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class RackRoleFilterSet(OrganizationalModelFilterSet):
|
||||
@@ -314,8 +312,8 @@ class RackTypeFilterSet(NetBoxModelFilterSet):
|
||||
class Meta:
|
||||
model = RackType
|
||||
fields = (
|
||||
'id', 'model', 'slug', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_height',
|
||||
'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description',
|
||||
'id', 'model', 'slug', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth',
|
||||
'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description',
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
@@ -427,8 +425,8 @@ class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe
|
||||
model = Rack
|
||||
fields = (
|
||||
'id', 'name', 'facility_id', 'asset_tag', 'u_height', 'starting_unit', 'desc_units', 'outer_width',
|
||||
'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'airflow', 'weight', 'max_weight',
|
||||
'weight_unit', 'description',
|
||||
'outer_depth', 'outer_unit', 'mounting_depth', 'airflow', 'weight', 'max_weight', 'weight_unit',
|
||||
'description',
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
@@ -675,33 +673,7 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
|
||||
return queryset.exclude(inventoryitemtemplates__isnull=value)
|
||||
|
||||
|
||||
class ModuleTypeProfileFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ModuleTypeProfile
|
||||
fields = ('id', 'name', 'description')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class ModuleTypeFilterSet(AttributeFiltersMixin, NetBoxModelFilterSet):
|
||||
profile_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ModuleTypeProfile.objects.all(),
|
||||
label=_('Profile (ID)'),
|
||||
)
|
||||
profile = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='profile__name',
|
||||
queryset=ModuleTypeProfile.objects.all(),
|
||||
to_field_name='name',
|
||||
label=_('Profile (name)'),
|
||||
)
|
||||
class ModuleTypeFilterSet(NetBoxModelFilterSet):
|
||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
label=_('Manufacturer (ID)'),
|
||||
@@ -949,29 +921,6 @@ class DeviceRoleFilterSet(OrganizationalModelFilterSet):
|
||||
queryset=ConfigTemplate.objects.all(),
|
||||
label=_('Config template (ID)'),
|
||||
)
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
label=_('Parent device role (ID)'),
|
||||
)
|
||||
parent = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='parent__slug',
|
||||
queryset=DeviceRole.objects.all(),
|
||||
to_field_name='slug',
|
||||
label=_('Parent device role (slug)'),
|
||||
)
|
||||
ancestor_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
field_name='parent',
|
||||
lookup_expr='in',
|
||||
label=_('Parent device role (ID)'),
|
||||
)
|
||||
ancestor = TreeNodeMultipleChoiceFilter(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
field_name='parent',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label=_('Parent device role (slug)'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = DeviceRole
|
||||
@@ -1040,16 +989,14 @@ class DeviceFilterSet(
|
||||
queryset=DeviceType.objects.all(),
|
||||
label=_('Device type (ID)'),
|
||||
)
|
||||
role_id = TreeNodeMultipleChoiceFilter(
|
||||
field_name='role',
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='role_id',
|
||||
queryset=DeviceRole.objects.all(),
|
||||
lookup_expr='in',
|
||||
label=_('Role (ID)'),
|
||||
)
|
||||
role = TreeNodeMultipleChoiceFilter(
|
||||
role = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='role__slug',
|
||||
queryset=DeviceRole.objects.all(),
|
||||
field_name='role',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label=_('Role (slug)'),
|
||||
)
|
||||
@@ -1246,7 +1193,6 @@ class DeviceFilterSet(
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(virtual_chassis__name__icontains=value) |
|
||||
Q(serial__icontains=value.strip()) |
|
||||
Q(inventoryitems__serial__icontains=value.strip()) |
|
||||
Q(asset_tag__icontains=value.strip()) |
|
||||
|
||||
@@ -14,9 +14,7 @@ from netbox.forms import NetBoxModelBulkEditForm
|
||||
from tenancy.models import Tenant
|
||||
from users.models import User
|
||||
from utilities.forms import BulkEditForm, add_blank_choice, form_from_model
|
||||
from utilities.forms.fields import (
|
||||
ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField,
|
||||
)
|
||||
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
||||
from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups
|
||||
from utilities.forms.widgets import BulkEditNullBooleanSelect, NumberWithOptions
|
||||
from virtualization.models import Cluster
|
||||
@@ -48,7 +46,6 @@ __all__ = (
|
||||
'ModuleBayBulkEditForm',
|
||||
'ModuleBayTemplateBulkEditForm',
|
||||
'ModuleTypeBulkEditForm',
|
||||
'ModuleTypeProfileBulkEditForm',
|
||||
'PlatformBulkEditForm',
|
||||
'PowerFeedBulkEditForm',
|
||||
'PowerOutletBulkEditForm',
|
||||
@@ -81,13 +78,12 @@ class RegionBulkEditForm(NetBoxModelBulkEditForm):
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
model = Region
|
||||
fieldsets = (
|
||||
FieldSet('parent', 'description'),
|
||||
)
|
||||
nullable_fields = ('parent', 'description', 'comments')
|
||||
nullable_fields = ('parent', 'description')
|
||||
|
||||
|
||||
class SiteGroupBulkEditForm(NetBoxModelBulkEditForm):
|
||||
@@ -101,13 +97,12 @@ class SiteGroupBulkEditForm(NetBoxModelBulkEditForm):
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
model = SiteGroup
|
||||
fieldsets = (
|
||||
FieldSet('parent', 'description'),
|
||||
)
|
||||
nullable_fields = ('parent', 'description', 'comments')
|
||||
nullable_fields = ('parent', 'description')
|
||||
|
||||
|
||||
class SiteBulkEditForm(NetBoxModelBulkEditForm):
|
||||
@@ -202,13 +197,12 @@ class LocationBulkEditForm(NetBoxModelBulkEditForm):
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
model = Location
|
||||
fieldsets = (
|
||||
FieldSet('site', 'parent', 'status', 'tenant', 'description'),
|
||||
)
|
||||
nullable_fields = ('parent', 'tenant', 'description', 'comments')
|
||||
nullable_fields = ('parent', 'tenant', 'description')
|
||||
|
||||
|
||||
class RackRoleBulkEditForm(NetBoxModelBulkEditForm):
|
||||
@@ -263,11 +257,6 @@ class RackTypeBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False,
|
||||
min_value=1
|
||||
)
|
||||
outer_height = forms.IntegerField(
|
||||
label=_('Outer height'),
|
||||
required=False,
|
||||
min_value=1
|
||||
)
|
||||
outer_depth = forms.IntegerField(
|
||||
label=_('Outer depth'),
|
||||
required=False,
|
||||
@@ -310,7 +299,7 @@ class RackTypeBulkEditForm(NetBoxModelBulkEditForm):
|
||||
fieldsets = (
|
||||
FieldSet('manufacturer', 'description', 'form_factor', 'width', 'u_height', name=_('Rack Type')),
|
||||
FieldSet(
|
||||
InlineFields('outer_width', 'outer_height', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')),
|
||||
InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')),
|
||||
InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
|
||||
'mounting_depth',
|
||||
name=_('Dimensions')
|
||||
@@ -318,7 +307,7 @@ class RackTypeBulkEditForm(NetBoxModelBulkEditForm):
|
||||
FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
|
||||
)
|
||||
nullable_fields = (
|
||||
'outer_width', 'outer_height', 'outer_depth', 'outer_unit', 'weight',
|
||||
'outer_width', 'outer_depth', 'outer_unit', 'weight',
|
||||
'max_weight', 'weight_unit', 'description', 'comments',
|
||||
)
|
||||
|
||||
@@ -412,11 +401,6 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False,
|
||||
min_value=1
|
||||
)
|
||||
outer_height = forms.IntegerField(
|
||||
label=_('Outer height'),
|
||||
required=False,
|
||||
min_value=1
|
||||
)
|
||||
outer_depth = forms.IntegerField(
|
||||
label=_('Outer depth'),
|
||||
required=False,
|
||||
@@ -464,13 +448,15 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
|
||||
fieldsets = (
|
||||
FieldSet('status', 'role', 'tenant', 'serial', 'asset_tag', 'rack_type', 'description', name=_('Rack')),
|
||||
FieldSet('region', 'site_group', 'site', 'location', name=_('Location')),
|
||||
FieldSet('outer_width', 'outer_height', 'outer_depth', 'outer_unit', name=_('Outer Dimensions')),
|
||||
FieldSet('form_factor', 'width', 'u_height', 'desc_units', 'airflow', 'mounting_depth', name=_('Hardware')),
|
||||
FieldSet(
|
||||
'form_factor', 'width', 'u_height', 'desc_units', 'airflow', 'outer_width', 'outer_depth', 'outer_unit',
|
||||
'mounting_depth', name=_('Hardware')
|
||||
),
|
||||
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
|
||||
)
|
||||
nullable_fields = (
|
||||
'location', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_height', 'outer_depth',
|
||||
'outer_unit', 'weight', 'max_weight', 'weight_unit', 'description', 'comments',
|
||||
'location', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_depth', 'outer_unit', 'weight',
|
||||
'max_weight', 'weight_unit', 'description', 'comments',
|
||||
)
|
||||
|
||||
|
||||
@@ -577,31 +563,7 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm):
|
||||
nullable_fields = ('part_number', 'airflow', 'weight', 'weight_unit', 'description', 'comments')
|
||||
|
||||
|
||||
class ModuleTypeProfileBulkEditForm(NetBoxModelBulkEditForm):
|
||||
schema = JSONField(
|
||||
label=_('Schema'),
|
||||
required=False
|
||||
)
|
||||
description = forms.CharField(
|
||||
label=_('Description'),
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
model = ModuleTypeProfile
|
||||
fieldsets = (
|
||||
FieldSet('name', 'description', 'schema', name=_('Profile')),
|
||||
)
|
||||
nullable_fields = ('description', 'comments')
|
||||
|
||||
|
||||
class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm):
|
||||
profile = DynamicModelChoiceField(
|
||||
label=_('Profile'),
|
||||
queryset=ModuleTypeProfile.objects.all(),
|
||||
required=False
|
||||
)
|
||||
manufacturer = DynamicModelChoiceField(
|
||||
label=_('Manufacturer'),
|
||||
queryset=Manufacturer.objects.all(),
|
||||
@@ -636,22 +598,17 @@ class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm):
|
||||
|
||||
model = ModuleType
|
||||
fieldsets = (
|
||||
FieldSet('profile', 'manufacturer', 'part_number', 'description', name=_('Module Type')),
|
||||
FieldSet('manufacturer', 'part_number', 'description', name=_('Module Type')),
|
||||
FieldSet(
|
||||
'airflow',
|
||||
InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
|
||||
name=_('Chassis')
|
||||
),
|
||||
)
|
||||
nullable_fields = ('part_number', 'weight', 'weight_unit', 'profile', 'description', 'comments')
|
||||
nullable_fields = ('part_number', 'weight', 'weight_unit', 'description', 'comments')
|
||||
|
||||
|
||||
class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm):
|
||||
parent = DynamicModelChoiceField(
|
||||
label=_('Parent'),
|
||||
queryset=DeviceRole.objects.all(),
|
||||
required=False,
|
||||
)
|
||||
color = ColorField(
|
||||
label=_('Color'),
|
||||
required=False
|
||||
@@ -671,13 +628,12 @@ class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm):
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
model = DeviceRole
|
||||
fieldsets = (
|
||||
FieldSet('parent', 'color', 'vm_role', 'config_template', 'description'),
|
||||
FieldSet('color', 'vm_role', 'config_template', 'description'),
|
||||
)
|
||||
nullable_fields = ('parent', 'color', 'config_template', 'description', 'comments')
|
||||
nullable_fields = ('color', 'config_template', 'description')
|
||||
|
||||
|
||||
class PlatformBulkEditForm(NetBoxModelBulkEditForm):
|
||||
@@ -1458,7 +1414,7 @@ class InterfaceBulkEditForm(
|
||||
form_from_model(Interface, [
|
||||
'label', 'type', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'wwn', 'mtu', 'mgmt_only', 'mark_connected',
|
||||
'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power',
|
||||
'wireless_lans', 'vlan_translation_policy'
|
||||
'wireless_lans'
|
||||
])
|
||||
):
|
||||
enabled = forms.NullBooleanField(
|
||||
@@ -1611,9 +1567,7 @@ class InterfaceBulkEditForm(
|
||||
FieldSet('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected', name=_('Operation')),
|
||||
FieldSet('poe_mode', 'poe_type', name=_('PoE')),
|
||||
FieldSet('parent', 'bridge', 'lag', name=_('Related Interfaces')),
|
||||
FieldSet(
|
||||
'mode', 'vlan_group', 'untagged_vlan', 'qinq_svlan', 'vlan_translation_policy', name=_('802.1Q Switching')
|
||||
),
|
||||
FieldSet('mode', 'vlan_group', 'untagged_vlan', 'qinq_svlan', name=_('802.1Q Switching')),
|
||||
FieldSet(
|
||||
TabbedGroups(
|
||||
FieldSet('tagged_vlans', name=_('Assignment')),
|
||||
@@ -1628,7 +1582,7 @@ class InterfaceBulkEditForm(
|
||||
nullable_fields = (
|
||||
'module', 'label', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'wwn', 'vdcs', 'mtu', 'description',
|
||||
'poe_mode', 'poe_type', 'mode', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power',
|
||||
'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vrf', 'wireless_lans', 'vlan_translation_policy',
|
||||
'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vrf', 'wireless_lans'
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@@ -39,7 +39,6 @@ __all__ = (
|
||||
'ModuleImportForm',
|
||||
'ModuleBayImportForm',
|
||||
'ModuleTypeImportForm',
|
||||
'ModuleTypeProfileImportForm',
|
||||
'PlatformImportForm',
|
||||
'PowerFeedImportForm',
|
||||
'PowerOutletImportForm',
|
||||
@@ -69,7 +68,7 @@ class RegionImportForm(NetBoxModelImportForm):
|
||||
|
||||
class Meta:
|
||||
model = Region
|
||||
fields = ('name', 'slug', 'parent', 'description', 'tags', 'comments')
|
||||
fields = ('name', 'slug', 'parent', 'description', 'tags')
|
||||
|
||||
|
||||
class SiteGroupImportForm(NetBoxModelImportForm):
|
||||
@@ -83,7 +82,7 @@ class SiteGroupImportForm(NetBoxModelImportForm):
|
||||
|
||||
class Meta:
|
||||
model = SiteGroup
|
||||
fields = ('name', 'slug', 'parent', 'description', 'comments', 'tags')
|
||||
fields = ('name', 'slug', 'parent', 'description')
|
||||
|
||||
|
||||
class SiteImportForm(NetBoxModelImportForm):
|
||||
@@ -161,10 +160,7 @@ class LocationImportForm(NetBoxModelImportForm):
|
||||
|
||||
class Meta:
|
||||
model = Location
|
||||
fields = (
|
||||
'site', 'parent', 'name', 'slug', 'status', 'tenant', 'facility', 'description',
|
||||
'tags', 'comments',
|
||||
)
|
||||
fields = ('site', 'parent', 'name', 'slug', 'status', 'tenant', 'facility', 'description', 'tags')
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
super().__init__(data, *args, **kwargs)
|
||||
@@ -223,7 +219,7 @@ class RackTypeImportForm(NetBoxModelImportForm):
|
||||
model = RackType
|
||||
fields = (
|
||||
'manufacturer', 'model', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units',
|
||||
'outer_width', 'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight',
|
||||
'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight',
|
||||
'weight_unit', 'description', 'comments', 'tags',
|
||||
)
|
||||
|
||||
@@ -308,7 +304,7 @@ class RackImportForm(NetBoxModelImportForm):
|
||||
model = Rack
|
||||
fields = (
|
||||
'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'rack_type', 'form_factor', 'serial',
|
||||
'asset_tag', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_height', 'outer_depth', 'outer_unit',
|
||||
'asset_tag', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
|
||||
'mounting_depth', 'airflow', 'weight', 'max_weight', 'weight_unit', 'description', 'comments', 'tags',
|
||||
)
|
||||
|
||||
@@ -428,22 +424,7 @@ class DeviceTypeImportForm(NetBoxModelImportForm):
|
||||
]
|
||||
|
||||
|
||||
class ModuleTypeProfileImportForm(NetBoxModelImportForm):
|
||||
|
||||
class Meta:
|
||||
model = ModuleTypeProfile
|
||||
fields = [
|
||||
'name', 'description', 'schema', 'comments', 'tags',
|
||||
]
|
||||
|
||||
|
||||
class ModuleTypeImportForm(NetBoxModelImportForm):
|
||||
profile = forms.ModelChoiceField(
|
||||
label=_('Profile'),
|
||||
queryset=ModuleTypeProfile.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False
|
||||
)
|
||||
manufacturer = forms.ModelChoiceField(
|
||||
label=_('Manufacturer'),
|
||||
queryset=Manufacturer.objects.all(),
|
||||
@@ -476,16 +457,6 @@ class ModuleTypeImportForm(NetBoxModelImportForm):
|
||||
|
||||
|
||||
class DeviceRoleImportForm(NetBoxModelImportForm):
|
||||
parent = CSVModelChoiceField(
|
||||
label=_('Parent'),
|
||||
queryset=DeviceRole.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text=_('Parent Device Role'),
|
||||
error_messages={
|
||||
'invalid_choice': _('Device role not found.'),
|
||||
}
|
||||
)
|
||||
config_template = CSVModelChoiceField(
|
||||
label=_('Config template'),
|
||||
queryset=ConfigTemplate.objects.all(),
|
||||
@@ -497,9 +468,7 @@ class DeviceRoleImportForm(NetBoxModelImportForm):
|
||||
|
||||
class Meta:
|
||||
model = DeviceRole
|
||||
fields = (
|
||||
'name', 'slug', 'parent', 'color', 'vm_role', 'config_template', 'description', 'comments', 'tags'
|
||||
)
|
||||
fields = ('name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags')
|
||||
|
||||
|
||||
class PlatformImportForm(NetBoxModelImportForm):
|
||||
@@ -1192,45 +1161,27 @@ class InventoryItemImportForm(NetBoxModelImportForm):
|
||||
else:
|
||||
self.fields['parent'].queryset = InventoryItem.objects.none()
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
cleaned_data = self.cleaned_data
|
||||
component_type = cleaned_data.get('component_type')
|
||||
component_name = cleaned_data.get('component_name')
|
||||
def clean_component_name(self):
|
||||
content_type = self.cleaned_data.get('component_type')
|
||||
component_name = self.cleaned_data.get('component_name')
|
||||
device = self.cleaned_data.get("device")
|
||||
|
||||
if component_type:
|
||||
if device is None:
|
||||
cleaned_data.pop('component_type', None)
|
||||
if component_name is None:
|
||||
cleaned_data.pop('component_type', None)
|
||||
raise forms.ValidationError(
|
||||
_("Component name must be specified when component type is specified")
|
||||
if not device and hasattr(self, 'instance') and hasattr(self.instance, 'device'):
|
||||
device = self.instance.device
|
||||
|
||||
if not all([device, content_type, component_name]):
|
||||
return None
|
||||
|
||||
model = content_type.model_class()
|
||||
try:
|
||||
component = model.objects.get(device=device, name=component_name)
|
||||
self.instance.component = component
|
||||
except ObjectDoesNotExist:
|
||||
raise forms.ValidationError(
|
||||
_("Component not found: {device} - {component_name}").format(
|
||||
device=device, component_name=component_name
|
||||
)
|
||||
if all([device, component_name]):
|
||||
try:
|
||||
model = component_type.model_class()
|
||||
self.instance.component = model.objects.get(device=device, name=component_name)
|
||||
except ObjectDoesNotExist:
|
||||
cleaned_data.pop('component_type', None)
|
||||
cleaned_data.pop('component_name', None)
|
||||
raise forms.ValidationError(
|
||||
_("Component not found: {device} - {component_name}").format(
|
||||
device=device, component_name=component_name
|
||||
)
|
||||
)
|
||||
else:
|
||||
cleaned_data.pop('component_type', None)
|
||||
if not component_name:
|
||||
raise forms.ValidationError(
|
||||
_("Component name must be specified when component type is specified")
|
||||
)
|
||||
else:
|
||||
if component_name:
|
||||
raise forms.ValidationError(
|
||||
_("Component type must be specified when component name is specified")
|
||||
)
|
||||
return cleaned_data
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -39,7 +39,6 @@ __all__ = (
|
||||
'ModuleFilterForm',
|
||||
'ModuleBayFilterForm',
|
||||
'ModuleTypeFilterForm',
|
||||
'ModuleTypeProfileFilterForm',
|
||||
'PlatformFilterForm',
|
||||
'PowerConnectionFilterForm',
|
||||
'PowerFeedFilterForm',
|
||||
@@ -304,7 +303,7 @@ class RackTypeFilterForm(RackBaseFilterForm):
|
||||
model = RackType
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('manufacturer_id', 'form_factor', 'width', 'u_height', name=_('Rack Type')),
|
||||
FieldSet('form_factor', 'width', 'u_height', name=_('Rack Type')),
|
||||
FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
|
||||
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
|
||||
)
|
||||
@@ -603,19 +602,11 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
|
||||
)
|
||||
|
||||
|
||||
class ModuleTypeProfileFilterForm(NetBoxModelFilterSetForm):
|
||||
model = ModuleTypeProfile
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q')
|
||||
|
||||
|
||||
class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
|
||||
model = ModuleType
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('profile_id', 'manufacturer_id', 'part_number', 'airflow', name=_('Hardware')),
|
||||
FieldSet('manufacturer_id', 'part_number', 'airflow', name=_('Hardware')),
|
||||
FieldSet(
|
||||
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
|
||||
'pass_through_ports', name=_('Components')
|
||||
@@ -623,11 +614,6 @@ class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
|
||||
FieldSet('weight', 'weight_unit', name=_('Weight')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'manufacturer_id')
|
||||
profile_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ModuleTypeProfile.objects.all(),
|
||||
required=False,
|
||||
label=_('Profile')
|
||||
)
|
||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False,
|
||||
@@ -703,11 +689,6 @@ class DeviceRoleFilterForm(NetBoxModelFilterSetForm):
|
||||
required=False,
|
||||
label=_('Config template')
|
||||
)
|
||||
parent_id = DynamicModelMultipleChoiceField(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
required=False,
|
||||
label=_('Parent')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.validators import EMPTY_VALUES
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from timezone_field import TimeZoneFormField
|
||||
|
||||
@@ -19,7 +18,6 @@ from utilities.forms.fields import (
|
||||
)
|
||||
from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups
|
||||
from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK
|
||||
from utilities.jsonschema import JSONSchemaProperty
|
||||
from virtualization.models import Cluster, VMInterface
|
||||
from wireless.models import WirelessLAN, WirelessLANGroup
|
||||
from .common import InterfaceCommonForm, ModuleCommonForm
|
||||
@@ -50,7 +48,6 @@ __all__ = (
|
||||
'ModuleBayForm',
|
||||
'ModuleBayTemplateForm',
|
||||
'ModuleTypeForm',
|
||||
'ModuleTypeProfileForm',
|
||||
'PlatformForm',
|
||||
'PopulateDeviceBayForm',
|
||||
'PowerFeedForm',
|
||||
@@ -81,7 +78,6 @@ class RegionForm(NetBoxModelForm):
|
||||
required=False
|
||||
)
|
||||
slug = SlugField()
|
||||
comments = CommentField()
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('parent', 'name', 'slug', 'description', 'tags'),
|
||||
@@ -90,7 +86,7 @@ class RegionForm(NetBoxModelForm):
|
||||
class Meta:
|
||||
model = Region
|
||||
fields = (
|
||||
'parent', 'name', 'slug', 'description', 'tags', 'comments',
|
||||
'parent', 'name', 'slug', 'description', 'tags',
|
||||
)
|
||||
|
||||
|
||||
@@ -101,7 +97,6 @@ class SiteGroupForm(NetBoxModelForm):
|
||||
required=False
|
||||
)
|
||||
slug = SlugField()
|
||||
comments = CommentField()
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('parent', 'name', 'slug', 'description', 'tags'),
|
||||
@@ -110,7 +105,7 @@ class SiteGroupForm(NetBoxModelForm):
|
||||
class Meta:
|
||||
model = SiteGroup
|
||||
fields = (
|
||||
'parent', 'name', 'slug', 'description', 'comments', 'tags',
|
||||
'parent', 'name', 'slug', 'description', 'tags',
|
||||
)
|
||||
|
||||
|
||||
@@ -184,7 +179,6 @@ class LocationForm(TenancyForm, NetBoxModelForm):
|
||||
}
|
||||
)
|
||||
slug = SlugField()
|
||||
comments = CommentField()
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('site', 'parent', 'name', 'slug', 'status', 'facility', 'description', 'tags', name=_('Location')),
|
||||
@@ -194,8 +188,7 @@ class LocationForm(TenancyForm, NetBoxModelForm):
|
||||
class Meta:
|
||||
model = Location
|
||||
fields = (
|
||||
'site', 'parent', 'name', 'slug', 'status', 'description', 'tenant_group', 'tenant',
|
||||
'facility', 'tags', 'comments',
|
||||
'site', 'parent', 'name', 'slug', 'status', 'description', 'tenant_group', 'tenant', 'facility', 'tags',
|
||||
)
|
||||
|
||||
|
||||
@@ -229,7 +222,7 @@ class RackTypeForm(NetBoxModelForm):
|
||||
FieldSet('manufacturer', 'model', 'slug', 'description', 'form_factor', 'tags', name=_('Rack Type')),
|
||||
FieldSet(
|
||||
'width', 'u_height',
|
||||
InlineFields('outer_width', 'outer_height', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')),
|
||||
InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')),
|
||||
InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
|
||||
'mounting_depth', name=_('Dimensions')
|
||||
),
|
||||
@@ -240,8 +233,8 @@ class RackTypeForm(NetBoxModelForm):
|
||||
model = RackType
|
||||
fields = [
|
||||
'manufacturer', 'model', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units',
|
||||
'outer_width', 'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight',
|
||||
'weight_unit', 'description', 'comments', 'tags',
|
||||
'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit',
|
||||
'description', 'comments', 'tags',
|
||||
]
|
||||
|
||||
|
||||
@@ -286,8 +279,8 @@ class RackForm(TenancyForm, NetBoxModelForm):
|
||||
fields = [
|
||||
'site', 'location', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial',
|
||||
'asset_tag', 'rack_type', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width',
|
||||
'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'airflow', 'weight', 'max_weight',
|
||||
'weight_unit', 'description', 'comments', 'tags',
|
||||
'outer_depth', 'outer_unit', 'mounting_depth', 'airflow', 'weight', 'max_weight', 'weight_unit',
|
||||
'description', 'comments', 'tags',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -309,8 +302,7 @@ class RackForm(TenancyForm, NetBoxModelForm):
|
||||
*self.fieldsets,
|
||||
FieldSet(
|
||||
'form_factor', 'width', 'starting_unit', 'u_height',
|
||||
InlineFields('outer_width', 'outer_height', 'outer_depth', 'outer_unit',
|
||||
label=_('Outer Dimensions')),
|
||||
InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')),
|
||||
InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
|
||||
'mounting_depth', 'desc_units', name=_('Dimensions')
|
||||
),
|
||||
@@ -407,104 +399,25 @@ class DeviceTypeForm(NetBoxModelForm):
|
||||
}
|
||||
|
||||
|
||||
class ModuleTypeProfileForm(NetBoxModelForm):
|
||||
schema = JSONField(
|
||||
label=_('Schema'),
|
||||
required=False,
|
||||
help_text=_("Enter a valid JSON schema to define supported attributes.")
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('name', 'description', 'schema', 'tags', name=_('Profile')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ModuleTypeProfile
|
||||
fields = [
|
||||
'name', 'description', 'schema', 'comments', 'tags',
|
||||
]
|
||||
|
||||
|
||||
class ModuleTypeForm(NetBoxModelForm):
|
||||
profile = forms.ModelChoiceField(
|
||||
queryset=ModuleTypeProfile.objects.all(),
|
||||
label=_('Profile'),
|
||||
required=False,
|
||||
widget=HTMXSelect()
|
||||
)
|
||||
manufacturer = DynamicModelChoiceField(
|
||||
label=_('Manufacturer'),
|
||||
queryset=Manufacturer.objects.all()
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
@property
|
||||
def fieldsets(self):
|
||||
return [
|
||||
FieldSet('manufacturer', 'model', 'part_number', 'description', 'tags', name=_('Module Type')),
|
||||
FieldSet('airflow', 'weight', 'weight_unit', name=_('Hardware')),
|
||||
FieldSet('profile', *self.attr_fields, name=_('Profile & Attributes'))
|
||||
]
|
||||
fieldsets = (
|
||||
FieldSet('manufacturer', 'model', 'part_number', 'description', 'tags', name=_('Module Type')),
|
||||
FieldSet('airflow', 'weight', 'weight_unit', name=_('Chassis'))
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ModuleType
|
||||
fields = [
|
||||
'profile', 'manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit',
|
||||
'manufacturer', 'model', 'part_number', 'airflow', 'weight', 'weight_unit', 'description',
|
||||
'comments', 'tags',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Track profile-specific attribute fields
|
||||
self.attr_fields = []
|
||||
|
||||
# Retrieve assigned ModuleTypeProfile, if any
|
||||
if not (profile_id := get_field_value(self, 'profile')):
|
||||
return
|
||||
if not (profile := ModuleTypeProfile.objects.filter(pk=profile_id).first()):
|
||||
return
|
||||
|
||||
# Extend form with fields for profile attributes
|
||||
for attr, form_field in self._get_attr_form_fields(profile).items():
|
||||
field_name = f'attr_{attr}'
|
||||
self.attr_fields.append(field_name)
|
||||
self.fields[field_name] = form_field
|
||||
if self.instance.attribute_data:
|
||||
self.fields[field_name].initial = self.instance.attribute_data.get(attr)
|
||||
|
||||
@staticmethod
|
||||
def _get_attr_form_fields(profile):
|
||||
"""
|
||||
Return a dictionary mapping of attribute names to form fields, suitable for extending
|
||||
the form per the selected ModuleTypeProfile.
|
||||
"""
|
||||
if not profile.schema:
|
||||
return {}
|
||||
|
||||
properties = profile.schema.get('properties', {})
|
||||
required_fields = profile.schema.get('required', [])
|
||||
|
||||
attr_fields = {}
|
||||
for name, options in properties.items():
|
||||
prop = JSONSchemaProperty(**options)
|
||||
attr_fields[name] = prop.to_form_field(name, required=name in required_fields)
|
||||
|
||||
return dict(sorted(attr_fields.items()))
|
||||
|
||||
def _post_clean(self):
|
||||
|
||||
# Compile attribute data from the individual form fields
|
||||
if self.cleaned_data.get('profile'):
|
||||
self.instance.attribute_data = {
|
||||
name[5:]: self.cleaned_data[name] # Remove the attr_ prefix
|
||||
for name in self.attr_fields
|
||||
if self.cleaned_data.get(name) not in EMPTY_VALUES
|
||||
}
|
||||
|
||||
return super()._post_clean()
|
||||
|
||||
|
||||
class DeviceRoleForm(NetBoxModelForm):
|
||||
config_template = DynamicModelChoiceField(
|
||||
@@ -513,24 +426,17 @@ class DeviceRoleForm(NetBoxModelForm):
|
||||
required=False
|
||||
)
|
||||
slug = SlugField()
|
||||
parent = DynamicModelChoiceField(
|
||||
label=_('Parent'),
|
||||
queryset=DeviceRole.objects.all(),
|
||||
required=False,
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
'name', 'slug', 'parent', 'color', 'vm_role', 'config_template', 'description',
|
||||
'tags', name=_('Device Role')
|
||||
'name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags', name=_('Device Role')
|
||||
),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = DeviceRole
|
||||
fields = [
|
||||
'name', 'slug', 'parent', 'color', 'vm_role', 'config_template', 'description', 'comments', 'tags',
|
||||
'name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -153,7 +153,6 @@ class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemp
|
||||
self.fields['rear_port'].choices = choices
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Check that the number of FrontPortTemplates to be created matches the selected number of RearPortTemplate
|
||||
# positions
|
||||
@@ -303,7 +302,6 @@ class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
|
||||
self.fields['rear_port'].choices = choices
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Check that the number of FrontPorts to be created matches the selected number of RearPort positions
|
||||
frontport_count = len(self.cleaned_data['name'])
|
||||
|
||||
@@ -68,7 +68,6 @@ __all__ = (
|
||||
'ModuleBayFilter',
|
||||
'ModuleBayTemplateFilter',
|
||||
'ModuleTypeFilter',
|
||||
'ModuleTypeProfileFilter',
|
||||
'PlatformFilter',
|
||||
'PowerFeedFilter',
|
||||
'PowerOutletFilter',
|
||||
@@ -560,11 +559,6 @@ class ModuleBayTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
position: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ModuleTypeProfile, lookups=True)
|
||||
class ModuleTypeProfileFilter(PrimaryModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ModuleType, lookups=True)
|
||||
class ModuleTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, WeightFilterMixin):
|
||||
manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
|
||||
@@ -30,7 +30,6 @@ class PathEndpointMixin:
|
||||
|
||||
connected_endpoints: List[Annotated[Union[
|
||||
Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')], # noqa: F821
|
||||
Annotated["VirtualCircuitTerminationType", 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
|
||||
|
||||
@@ -77,9 +77,6 @@ class DCIMQuery:
|
||||
module_bay_template: ModuleBayTemplateType = strawberry_django.field()
|
||||
module_bay_template_list: List[ModuleBayTemplateType] = strawberry_django.field()
|
||||
|
||||
module_type_profile: ModuleTypeProfileType = strawberry_django.field()
|
||||
module_type_profile_list: List[ModuleTypeProfileType] = strawberry_django.field()
|
||||
|
||||
module_type: ModuleTypeType = strawberry_django.field()
|
||||
module_type_list: List[ModuleTypeType] = strawberry_django.field()
|
||||
|
||||
|
||||
@@ -61,7 +61,6 @@ __all__ = (
|
||||
'ModuleType',
|
||||
'ModuleBayType',
|
||||
'ModuleBayTemplateType',
|
||||
'ModuleTypeProfileType',
|
||||
'ModuleTypeType',
|
||||
'PlatformType',
|
||||
'PowerFeedType',
|
||||
@@ -134,8 +133,7 @@ class ModularComponentTemplateType(ComponentTemplateType):
|
||||
@strawberry_django.type(
|
||||
models.CableTermination,
|
||||
exclude=['termination_type', 'termination_id', '_device', '_rack', '_location', '_site'],
|
||||
filters=CableTerminationFilter,
|
||||
pagination=True
|
||||
filters=CableTerminationFilter
|
||||
)
|
||||
class CableTerminationType(NetBoxObjectType):
|
||||
cable: Annotated["CableType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
@@ -155,8 +153,7 @@ class CableTerminationType(NetBoxObjectType):
|
||||
@strawberry_django.type(
|
||||
models.Cable,
|
||||
fields='__all__',
|
||||
filters=CableFilter,
|
||||
pagination=True
|
||||
filters=CableFilter
|
||||
)
|
||||
class CableType(NetBoxObjectType):
|
||||
color: str
|
||||
@@ -192,8 +189,7 @@ class CableType(NetBoxObjectType):
|
||||
@strawberry_django.type(
|
||||
models.ConsolePort,
|
||||
exclude=['_path'],
|
||||
filters=ConsolePortFilter,
|
||||
pagination=True
|
||||
filters=ConsolePortFilter
|
||||
)
|
||||
class ConsolePortType(ModularComponentType, CabledObjectMixin, PathEndpointMixin):
|
||||
pass
|
||||
@@ -202,8 +198,7 @@ class ConsolePortType(ModularComponentType, CabledObjectMixin, PathEndpointMixin
|
||||
@strawberry_django.type(
|
||||
models.ConsolePortTemplate,
|
||||
fields='__all__',
|
||||
filters=ConsolePortTemplateFilter,
|
||||
pagination=True
|
||||
filters=ConsolePortTemplateFilter
|
||||
)
|
||||
class ConsolePortTemplateType(ModularComponentTemplateType):
|
||||
pass
|
||||
@@ -212,8 +207,7 @@ class ConsolePortTemplateType(ModularComponentTemplateType):
|
||||
@strawberry_django.type(
|
||||
models.ConsoleServerPort,
|
||||
exclude=['_path'],
|
||||
filters=ConsoleServerPortFilter,
|
||||
pagination=True
|
||||
filters=ConsoleServerPortFilter
|
||||
)
|
||||
class ConsoleServerPortType(ModularComponentType, CabledObjectMixin, PathEndpointMixin):
|
||||
pass
|
||||
@@ -222,8 +216,7 @@ class ConsoleServerPortType(ModularComponentType, CabledObjectMixin, PathEndpoin
|
||||
@strawberry_django.type(
|
||||
models.ConsoleServerPortTemplate,
|
||||
fields='__all__',
|
||||
filters=ConsoleServerPortTemplateFilter,
|
||||
pagination=True
|
||||
filters=ConsoleServerPortTemplateFilter
|
||||
)
|
||||
class ConsoleServerPortTemplateType(ModularComponentTemplateType):
|
||||
pass
|
||||
@@ -232,8 +225,7 @@ class ConsoleServerPortTemplateType(ModularComponentTemplateType):
|
||||
@strawberry_django.type(
|
||||
models.Device,
|
||||
fields='__all__',
|
||||
filters=DeviceFilter,
|
||||
pagination=True
|
||||
filters=DeviceFilter
|
||||
)
|
||||
class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
|
||||
console_port_count: BigInt
|
||||
@@ -288,8 +280,7 @@ class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, NetBo
|
||||
@strawberry_django.type(
|
||||
models.DeviceBay,
|
||||
fields='__all__',
|
||||
filters=DeviceBayFilter,
|
||||
pagination=True
|
||||
filters=DeviceBayFilter
|
||||
)
|
||||
class DeviceBayType(ComponentType):
|
||||
installed_device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
@@ -298,8 +289,7 @@ class DeviceBayType(ComponentType):
|
||||
@strawberry_django.type(
|
||||
models.DeviceBayTemplate,
|
||||
fields='__all__',
|
||||
filters=DeviceBayTemplateFilter,
|
||||
pagination=True
|
||||
filters=DeviceBayTemplateFilter
|
||||
)
|
||||
class DeviceBayTemplateType(ComponentTemplateType):
|
||||
pass
|
||||
@@ -308,8 +298,7 @@ class DeviceBayTemplateType(ComponentTemplateType):
|
||||
@strawberry_django.type(
|
||||
models.InventoryItemTemplate,
|
||||
exclude=['component_type', 'component_id', 'parent'],
|
||||
filters=InventoryItemTemplateFilter,
|
||||
pagination=True
|
||||
filters=InventoryItemTemplateFilter
|
||||
)
|
||||
class InventoryItemTemplateType(ComponentTemplateType):
|
||||
role: Annotated["InventoryItemRoleType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
@@ -335,12 +324,9 @@ class InventoryItemTemplateType(ComponentTemplateType):
|
||||
@strawberry_django.type(
|
||||
models.DeviceRole,
|
||||
fields='__all__',
|
||||
filters=DeviceRoleFilter,
|
||||
pagination=True
|
||||
filters=DeviceRoleFilter
|
||||
)
|
||||
class DeviceRoleType(OrganizationalObjectType):
|
||||
parent: Annotated['DeviceRoleType', strawberry.lazy('dcim.graphql.types')] | None
|
||||
children: List[Annotated['DeviceRoleType', strawberry.lazy('dcim.graphql.types')]]
|
||||
color: str
|
||||
config_template: Annotated["ConfigTemplateType", strawberry.lazy('extras.graphql.types')] | None
|
||||
|
||||
@@ -351,8 +337,7 @@ class DeviceRoleType(OrganizationalObjectType):
|
||||
@strawberry_django.type(
|
||||
models.DeviceType,
|
||||
fields='__all__',
|
||||
filters=DeviceTypeFilter,
|
||||
pagination=True
|
||||
filters=DeviceTypeFilter
|
||||
)
|
||||
class DeviceTypeType(NetBoxObjectType):
|
||||
console_port_template_count: BigInt
|
||||
@@ -386,8 +371,7 @@ class DeviceTypeType(NetBoxObjectType):
|
||||
@strawberry_django.type(
|
||||
models.FrontPort,
|
||||
fields='__all__',
|
||||
filters=FrontPortFilter,
|
||||
pagination=True
|
||||
filters=FrontPortFilter
|
||||
)
|
||||
class FrontPortType(ModularComponentType, CabledObjectMixin):
|
||||
color: str
|
||||
@@ -397,8 +381,7 @@ class FrontPortType(ModularComponentType, CabledObjectMixin):
|
||||
@strawberry_django.type(
|
||||
models.FrontPortTemplate,
|
||||
fields='__all__',
|
||||
filters=FrontPortTemplateFilter,
|
||||
pagination=True
|
||||
filters=FrontPortTemplateFilter
|
||||
)
|
||||
class FrontPortTemplateType(ModularComponentTemplateType):
|
||||
color: str
|
||||
@@ -408,8 +391,7 @@ class FrontPortTemplateType(ModularComponentTemplateType):
|
||||
@strawberry_django.type(
|
||||
models.MACAddress,
|
||||
exclude=['assigned_object_type', 'assigned_object_id'],
|
||||
filters=MACAddressFilter,
|
||||
pagination=True
|
||||
filters=MACAddressFilter
|
||||
)
|
||||
class MACAddressType(NetBoxObjectType):
|
||||
mac_address: str
|
||||
@@ -425,8 +407,7 @@ class MACAddressType(NetBoxObjectType):
|
||||
@strawberry_django.type(
|
||||
models.Interface,
|
||||
exclude=['_path'],
|
||||
filters=InterfaceFilter,
|
||||
pagination=True
|
||||
filters=InterfaceFilter
|
||||
)
|
||||
class InterfaceType(IPAddressesMixin, ModularComponentType, CabledObjectMixin, PathEndpointMixin):
|
||||
_name: str
|
||||
@@ -453,8 +434,7 @@ class InterfaceType(IPAddressesMixin, ModularComponentType, CabledObjectMixin, P
|
||||
@strawberry_django.type(
|
||||
models.InterfaceTemplate,
|
||||
fields='__all__',
|
||||
filters=InterfaceTemplateFilter,
|
||||
pagination=True
|
||||
filters=InterfaceTemplateFilter
|
||||
)
|
||||
class InterfaceTemplateType(ModularComponentTemplateType):
|
||||
_name: str
|
||||
@@ -466,12 +446,11 @@ class InterfaceTemplateType(ModularComponentTemplateType):
|
||||
@strawberry_django.type(
|
||||
models.InventoryItem,
|
||||
exclude=['component_type', 'component_id', 'parent'],
|
||||
filters=InventoryItemFilter,
|
||||
pagination=True
|
||||
filters=InventoryItemFilter
|
||||
)
|
||||
class InventoryItemType(ComponentType):
|
||||
role: Annotated["InventoryItemRoleType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')]
|
||||
|
||||
child_items: List[Annotated["InventoryItemType", strawberry.lazy('dcim.graphql.types')]]
|
||||
|
||||
@@ -493,8 +472,7 @@ class InventoryItemType(ComponentType):
|
||||
@strawberry_django.type(
|
||||
models.InventoryItemRole,
|
||||
fields='__all__',
|
||||
filters=InventoryItemRoleFilter,
|
||||
pagination=True
|
||||
filters=InventoryItemRoleFilter
|
||||
)
|
||||
class InventoryItemRoleType(OrganizationalObjectType):
|
||||
color: str
|
||||
@@ -507,8 +485,7 @@ class InventoryItemRoleType(OrganizationalObjectType):
|
||||
models.Location,
|
||||
# fields='__all__',
|
||||
exclude=['parent'], # bug - temp
|
||||
filters=LocationFilter,
|
||||
pagination=True
|
||||
filters=LocationFilter
|
||||
)
|
||||
class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, OrganizationalObjectType):
|
||||
site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]
|
||||
@@ -535,8 +512,7 @@ class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, Organi
|
||||
@strawberry_django.type(
|
||||
models.Manufacturer,
|
||||
fields='__all__',
|
||||
filters=ManufacturerFilter,
|
||||
pagination=True
|
||||
filters=ManufacturerFilter
|
||||
)
|
||||
class ManufacturerType(OrganizationalObjectType, ContactsMixin):
|
||||
|
||||
@@ -550,8 +526,7 @@ class ManufacturerType(OrganizationalObjectType, ContactsMixin):
|
||||
@strawberry_django.type(
|
||||
models.Module,
|
||||
fields='__all__',
|
||||
filters=ModuleFilter,
|
||||
pagination=True
|
||||
filters=ModuleFilter
|
||||
)
|
||||
class ModuleType(NetBoxObjectType):
|
||||
device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]
|
||||
@@ -571,8 +546,7 @@ class ModuleType(NetBoxObjectType):
|
||||
models.ModuleBay,
|
||||
# fields='__all__',
|
||||
exclude=['parent'],
|
||||
filters=ModuleBayFilter,
|
||||
pagination=True
|
||||
filters=ModuleBayFilter
|
||||
)
|
||||
class ModuleBayType(ModularComponentType):
|
||||
|
||||
@@ -587,31 +561,18 @@ class ModuleBayType(ModularComponentType):
|
||||
@strawberry_django.type(
|
||||
models.ModuleBayTemplate,
|
||||
fields='__all__',
|
||||
filters=ModuleBayTemplateFilter,
|
||||
pagination=True
|
||||
filters=ModuleBayTemplateFilter
|
||||
)
|
||||
class ModuleBayTemplateType(ModularComponentTemplateType):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.ModuleTypeProfile,
|
||||
fields='__all__',
|
||||
filters=ModuleTypeProfileFilter,
|
||||
pagination=True
|
||||
)
|
||||
class ModuleTypeProfileType(NetBoxObjectType):
|
||||
module_types: List[Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.ModuleType,
|
||||
fields='__all__',
|
||||
filters=ModuleTypeFilter,
|
||||
pagination=True
|
||||
filters=ModuleTypeFilter
|
||||
)
|
||||
class ModuleTypeType(NetBoxObjectType):
|
||||
profile: Annotated["ModuleTypeProfileType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')]
|
||||
|
||||
frontporttemplates: List[Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]]
|
||||
@@ -627,8 +588,7 @@ class ModuleTypeType(NetBoxObjectType):
|
||||
@strawberry_django.type(
|
||||
models.Platform,
|
||||
fields='__all__',
|
||||
filters=PlatformFilter,
|
||||
pagination=True
|
||||
filters=PlatformFilter
|
||||
)
|
||||
class PlatformType(OrganizationalObjectType):
|
||||
manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
@@ -641,8 +601,7 @@ class PlatformType(OrganizationalObjectType):
|
||||
@strawberry_django.type(
|
||||
models.PowerFeed,
|
||||
exclude=['_path'],
|
||||
filters=PowerFeedFilter,
|
||||
pagination=True
|
||||
filters=PowerFeedFilter
|
||||
)
|
||||
class PowerFeedType(NetBoxObjectType, CabledObjectMixin, PathEndpointMixin):
|
||||
power_panel: Annotated["PowerPanelType", strawberry.lazy('dcim.graphql.types')]
|
||||
@@ -653,8 +612,7 @@ class PowerFeedType(NetBoxObjectType, CabledObjectMixin, PathEndpointMixin):
|
||||
@strawberry_django.type(
|
||||
models.PowerOutlet,
|
||||
exclude=['_path'],
|
||||
filters=PowerOutletFilter,
|
||||
pagination=True
|
||||
filters=PowerOutletFilter
|
||||
)
|
||||
class PowerOutletType(ModularComponentType, CabledObjectMixin, PathEndpointMixin):
|
||||
power_port: Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
@@ -664,8 +622,7 @@ class PowerOutletType(ModularComponentType, CabledObjectMixin, PathEndpointMixin
|
||||
@strawberry_django.type(
|
||||
models.PowerOutletTemplate,
|
||||
fields='__all__',
|
||||
filters=PowerOutletTemplateFilter,
|
||||
pagination=True
|
||||
filters=PowerOutletTemplateFilter
|
||||
)
|
||||
class PowerOutletTemplateType(ModularComponentTemplateType):
|
||||
power_port: Annotated["PowerPortTemplateType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
@@ -674,8 +631,7 @@ class PowerOutletTemplateType(ModularComponentTemplateType):
|
||||
@strawberry_django.type(
|
||||
models.PowerPanel,
|
||||
fields='__all__',
|
||||
filters=PowerPanelFilter,
|
||||
pagination=True
|
||||
filters=PowerPanelFilter
|
||||
)
|
||||
class PowerPanelType(NetBoxObjectType, ContactsMixin):
|
||||
site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]
|
||||
@@ -687,8 +643,7 @@ class PowerPanelType(NetBoxObjectType, ContactsMixin):
|
||||
@strawberry_django.type(
|
||||
models.PowerPort,
|
||||
exclude=['_path'],
|
||||
filters=PowerPortFilter,
|
||||
pagination=True
|
||||
filters=PowerPortFilter
|
||||
)
|
||||
class PowerPortType(ModularComponentType, CabledObjectMixin, PathEndpointMixin):
|
||||
|
||||
@@ -698,8 +653,7 @@ class PowerPortType(ModularComponentType, CabledObjectMixin, PathEndpointMixin):
|
||||
@strawberry_django.type(
|
||||
models.PowerPortTemplate,
|
||||
fields='__all__',
|
||||
filters=PowerPortTemplateFilter,
|
||||
pagination=True
|
||||
filters=PowerPortTemplateFilter
|
||||
)
|
||||
class PowerPortTemplateType(ModularComponentTemplateType):
|
||||
poweroutlet_templates: List[Annotated["PowerOutletTemplateType", strawberry.lazy('dcim.graphql.types')]]
|
||||
@@ -708,8 +662,7 @@ class PowerPortTemplateType(ModularComponentTemplateType):
|
||||
@strawberry_django.type(
|
||||
models.RackType,
|
||||
fields='__all__',
|
||||
filters=RackTypeFilter,
|
||||
pagination=True
|
||||
filters=RackTypeFilter
|
||||
)
|
||||
class RackTypeType(NetBoxObjectType):
|
||||
manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')]
|
||||
@@ -718,8 +671,7 @@ class RackTypeType(NetBoxObjectType):
|
||||
@strawberry_django.type(
|
||||
models.Rack,
|
||||
fields='__all__',
|
||||
filters=RackFilter,
|
||||
pagination=True
|
||||
filters=RackFilter
|
||||
)
|
||||
class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
|
||||
site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]
|
||||
@@ -737,8 +689,7 @@ class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObje
|
||||
@strawberry_django.type(
|
||||
models.RackReservation,
|
||||
fields='__all__',
|
||||
filters=RackReservationFilter,
|
||||
pagination=True
|
||||
filters=RackReservationFilter
|
||||
)
|
||||
class RackReservationType(NetBoxObjectType):
|
||||
units: List[int]
|
||||
@@ -750,8 +701,7 @@ class RackReservationType(NetBoxObjectType):
|
||||
@strawberry_django.type(
|
||||
models.RackRole,
|
||||
fields='__all__',
|
||||
filters=RackRoleFilter,
|
||||
pagination=True
|
||||
filters=RackRoleFilter
|
||||
)
|
||||
class RackRoleType(OrganizationalObjectType):
|
||||
color: str
|
||||
@@ -762,8 +712,7 @@ class RackRoleType(OrganizationalObjectType):
|
||||
@strawberry_django.type(
|
||||
models.RearPort,
|
||||
fields='__all__',
|
||||
filters=RearPortFilter,
|
||||
pagination=True
|
||||
filters=RearPortFilter
|
||||
)
|
||||
class RearPortType(ModularComponentType, CabledObjectMixin):
|
||||
color: str
|
||||
@@ -774,8 +723,7 @@ class RearPortType(ModularComponentType, CabledObjectMixin):
|
||||
@strawberry_django.type(
|
||||
models.RearPortTemplate,
|
||||
fields='__all__',
|
||||
filters=RearPortTemplateFilter,
|
||||
pagination=True
|
||||
filters=RearPortTemplateFilter
|
||||
)
|
||||
class RearPortTemplateType(ModularComponentTemplateType):
|
||||
color: str
|
||||
@@ -786,8 +734,7 @@ class RearPortTemplateType(ModularComponentTemplateType):
|
||||
@strawberry_django.type(
|
||||
models.Region,
|
||||
exclude=['parent'],
|
||||
filters=RegionFilter,
|
||||
pagination=True
|
||||
filters=RegionFilter
|
||||
)
|
||||
class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
|
||||
|
||||
@@ -812,8 +759,7 @@ class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
|
||||
@strawberry_django.type(
|
||||
models.Site,
|
||||
fields='__all__',
|
||||
filters=SiteFilter,
|
||||
pagination=True
|
||||
filters=SiteFilter
|
||||
)
|
||||
class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
|
||||
time_zone: str | None
|
||||
@@ -847,8 +793,7 @@ class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObje
|
||||
@strawberry_django.type(
|
||||
models.SiteGroup,
|
||||
exclude=['parent'], # bug - temp
|
||||
filters=SiteGroupFilter,
|
||||
pagination=True
|
||||
filters=SiteGroupFilter
|
||||
)
|
||||
class SiteGroupType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
|
||||
|
||||
@@ -873,8 +818,7 @@ class SiteGroupType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
|
||||
@strawberry_django.type(
|
||||
models.VirtualChassis,
|
||||
fields='__all__',
|
||||
filters=VirtualChassisFilter,
|
||||
pagination=True
|
||||
filters=VirtualChassisFilter
|
||||
)
|
||||
class VirtualChassisType(NetBoxObjectType):
|
||||
member_count: BigInt
|
||||
@@ -886,8 +830,7 @@ class VirtualChassisType(NetBoxObjectType):
|
||||
@strawberry_django.type(
|
||||
models.VirtualDeviceContext,
|
||||
fields='__all__',
|
||||
filters=VirtualDeviceContextFilter,
|
||||
pagination=True
|
||||
filters=VirtualDeviceContextFilter
|
||||
)
|
||||
class VirtualDeviceContextType(NetBoxObjectType):
|
||||
device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
|
||||
@@ -505,6 +505,28 @@ class Migration(migrations.Migration):
|
||||
model_name='cable',
|
||||
name='termination_a_type',
|
||||
field=models.ForeignKey(
|
||||
limit_choices_to=models.Q(
|
||||
models.Q(
|
||||
models.Q(('app_label', 'circuits'), ('model__in', ('circuittermination',))),
|
||||
models.Q(
|
||||
('app_label', 'dcim'),
|
||||
(
|
||||
'model__in',
|
||||
(
|
||||
'consoleport',
|
||||
'consoleserverport',
|
||||
'frontport',
|
||||
'interface',
|
||||
'powerfeed',
|
||||
'poweroutlet',
|
||||
'powerport',
|
||||
'rearport',
|
||||
),
|
||||
),
|
||||
),
|
||||
_connector='OR',
|
||||
)
|
||||
),
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='+',
|
||||
to='contenttypes.contenttype',
|
||||
@@ -514,6 +536,28 @@ class Migration(migrations.Migration):
|
||||
model_name='cable',
|
||||
name='termination_b_type',
|
||||
field=models.ForeignKey(
|
||||
limit_choices_to=models.Q(
|
||||
models.Q(
|
||||
models.Q(('app_label', 'circuits'), ('model__in', ('circuittermination',))),
|
||||
models.Q(
|
||||
('app_label', 'dcim'),
|
||||
(
|
||||
'model__in',
|
||||
(
|
||||
'consoleport',
|
||||
'consoleserverport',
|
||||
'frontport',
|
||||
'interface',
|
||||
'powerfeed',
|
||||
'poweroutlet',
|
||||
'powerport',
|
||||
'rearport',
|
||||
),
|
||||
),
|
||||
),
|
||||
_connector='OR',
|
||||
)
|
||||
),
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='+',
|
||||
to='contenttypes.contenttype',
|
||||
|
||||
@@ -866,6 +866,21 @@ class Migration(migrations.Migration):
|
||||
name='component_type',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
limit_choices_to=models.Q(
|
||||
('app_label', 'dcim'),
|
||||
(
|
||||
'model__in',
|
||||
(
|
||||
'consoleport',
|
||||
'consoleserverport',
|
||||
'frontport',
|
||||
'interface',
|
||||
'poweroutlet',
|
||||
'powerport',
|
||||
'rearport',
|
||||
),
|
||||
),
|
||||
),
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='+',
|
||||
@@ -1223,6 +1238,21 @@ class Migration(migrations.Migration):
|
||||
'component_type',
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
limit_choices_to=models.Q(
|
||||
('app_label', 'dcim'),
|
||||
(
|
||||
'model__in',
|
||||
(
|
||||
'consoleporttemplate',
|
||||
'consoleserverporttemplate',
|
||||
'frontporttemplate',
|
||||
'interfacetemplate',
|
||||
'poweroutlettemplate',
|
||||
'powerporttemplate',
|
||||
'rearporttemplate',
|
||||
),
|
||||
),
|
||||
),
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='+',
|
||||
@@ -1448,6 +1478,28 @@ class Migration(migrations.Migration):
|
||||
(
|
||||
'termination_type',
|
||||
models.ForeignKey(
|
||||
limit_choices_to=models.Q(
|
||||
models.Q(
|
||||
models.Q(('app_label', 'circuits'), ('model__in', ('circuittermination',))),
|
||||
models.Q(
|
||||
('app_label', 'dcim'),
|
||||
(
|
||||
'model__in',
|
||||
(
|
||||
'consoleport',
|
||||
'consoleserverport',
|
||||
'frontport',
|
||||
'interface',
|
||||
'powerfeed',
|
||||
'poweroutlet',
|
||||
'powerport',
|
||||
'rearport',
|
||||
),
|
||||
),
|
||||
),
|
||||
_connector='OR',
|
||||
)
|
||||
),
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='+',
|
||||
to='contenttypes.contenttype',
|
||||
|
||||
@@ -31,6 +31,13 @@ class Migration(migrations.Migration):
|
||||
'assigned_object_type',
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
limit_choices_to=models.Q(
|
||||
models.Q(
|
||||
models.Q(('app_label', 'dcim'), ('model', 'interface')),
|
||||
models.Q(('app_label', 'virtualization'), ('model', 'vminterface')),
|
||||
_connector='OR',
|
||||
)
|
||||
),
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='+',
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0201_add_power_outlet_status'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='location',
|
||||
name='comments',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='region',
|
||||
name='comments',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitegroup',
|
||||
name='comments',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
]
|
||||
@@ -1,23 +0,0 @@
|
||||
# Generated by Django 5.2b1 on 2025-03-18 15:15
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0202_location_comments_region_comments_sitegroup_comments'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='outer_height',
|
||||
field=models.PositiveSmallIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='racktype',
|
||||
name='outer_height',
|
||||
field=models.PositiveSmallIntegerField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,65 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-03-25 18:06
|
||||
|
||||
import django.db.models.manager
|
||||
import mptt.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0203_add_rack_outer_height'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='devicerole',
|
||||
name='level',
|
||||
field=models.PositiveIntegerField(default=0, editable=False),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicerole',
|
||||
name='lft',
|
||||
field=models.PositiveIntegerField(default=0, editable=False),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicerole',
|
||||
name='rght',
|
||||
field=models.PositiveIntegerField(default=0, editable=False),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicerole',
|
||||
name='tree_id',
|
||||
field=models.PositiveIntegerField(db_index=True, default=0, editable=False),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicerole',
|
||||
name='parent',
|
||||
field=mptt.fields.TreeForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='children',
|
||||
to='dcim.devicerole',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicerole',
|
||||
name='comments',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicerole',
|
||||
name='name',
|
||||
field=models.CharField(max_length=100),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicerole',
|
||||
name='slug',
|
||||
field=models.SlugField(max_length=100),
|
||||
),
|
||||
]
|
||||
@@ -1,22 +0,0 @@
|
||||
from django.db import migrations
|
||||
import mptt
|
||||
import mptt.managers
|
||||
|
||||
|
||||
def rebuild_mptt(apps, schema_editor):
|
||||
manager = mptt.managers.TreeManager()
|
||||
DeviceRole = apps.get_model('dcim', 'DeviceRole')
|
||||
manager.model = DeviceRole
|
||||
mptt.register(DeviceRole)
|
||||
manager.contribute_to_class(DeviceRole, 'objects')
|
||||
manager.rebuild()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('dcim', '0203_device_role_nested'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(code=rebuild_mptt, reverse_code=migrations.RunPython.noop),
|
||||
]
|
||||
@@ -1,57 +0,0 @@
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
from django.db import migrations, models
|
||||
|
||||
import utilities.json
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('dcim', '0204_device_role_rebuild'),
|
||||
('extras', '0126_exporttemplate_file_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ModuleTypeProfile',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
(
|
||||
'custom_field_data',
|
||||
models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
|
||||
),
|
||||
('description', models.CharField(blank=True, max_length=200)),
|
||||
('comments', models.TextField(blank=True)),
|
||||
('name', models.CharField(max_length=100, unique=True)),
|
||||
('schema', models.JSONField(blank=True, null=True)),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'module type profile',
|
||||
'verbose_name_plural': 'module type profiles',
|
||||
'ordering': ('name',),
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='moduletype',
|
||||
name='attribute_data',
|
||||
field=models.JSONField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='moduletype',
|
||||
name='profile',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='module_types',
|
||||
to='dcim.moduletypeprofile',
|
||||
),
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='moduletype',
|
||||
options={'ordering': ('profile', 'manufacturer', 'model')},
|
||||
),
|
||||
]
|
||||
@@ -1,42 +0,0 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
DATA_FILES_PATH = Path(__file__).parent / 'initial_data' / 'module_type_profiles'
|
||||
|
||||
|
||||
def load_initial_data(apps, schema_editor):
|
||||
"""
|
||||
Load initial ModuleTypeProfile objects from file.
|
||||
"""
|
||||
ModuleTypeProfile = apps.get_model('dcim', 'ModuleTypeProfile')
|
||||
initial_profiles = (
|
||||
'cpu',
|
||||
'fan',
|
||||
'gpu',
|
||||
'hard_disk',
|
||||
'memory',
|
||||
'power_supply'
|
||||
)
|
||||
|
||||
for name in initial_profiles:
|
||||
file_path = DATA_FILES_PATH / f'{name}.json'
|
||||
with file_path.open('r') as f:
|
||||
data = json.load(f)
|
||||
try:
|
||||
ModuleTypeProfile.objects.create(**data)
|
||||
except Exception as e:
|
||||
print(f"Error loading data from {file_path}")
|
||||
raise e
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0205_moduletypeprofile'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(load_initial_data),
|
||||
]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user