Compare commits

..

5 Commits

Author SHA1 Message Date
Daniel Sheppard
64fca01cdb Final test fix 2025-04-05 20:11:27 -05:00
Daniel Sheppard
d699c69abf Fix test 2025-04-03 11:11:30 -05:00
Daniel Sheppard
97d426d205 Correct test failure and add check for related_model 2025-04-01 08:16:33 -05:00
Daniel Sheppard
e32d2ca637 Annotate queryset instead of using a model property 2025-03-27 14:36:09 -05:00
Daniel Sheppard
1777d4228e Work on 11507
* Add `rir` property method to model
* Add `aggregate` property method to model
* Add `rir` attribute to serializer
* Add `aggregate` attribute to serializer
* Add test for both rir and aggregate fields on API
2025-03-17 21:52:08 -05:00
405 changed files with 53849 additions and 68297 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -12,7 +12,6 @@ permissions:
jobs:
stale:
if: github.repository == 'netbox-community/netbox'
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9

View File

@@ -13,7 +13,6 @@ permissions:
jobs:
stale:
if: github.repository == 'netbox-community/netbox'
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9

View File

@@ -13,7 +13,6 @@ permissions:
jobs:
lock:
if: github.repository == 'netbox-community/netbox'
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v5

View File

@@ -13,7 +13,6 @@ env:
jobs:
makemessages:
if: github.repository == 'netbox-community/netbox'
runs-on: ubuntu-latest
env:
NETBOX_CONFIGURATION: netbox.configuration_testing

View File

@@ -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

View File

@@ -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

View File

@@ -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",

View File

@@ -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"),

View File

@@ -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.).

View File

@@ -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',
],
}
```

View File

@@ -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.

View File

@@ -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.

View File

@@ -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:

View File

@@ -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() %}

View File

@@ -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:

View File

@@ -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.

View File

@@ -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/

View File

@@ -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.

View File

@@ -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.

View File

@@ -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:

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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).

View File

@@ -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

View File

@@ -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).

View File

@@ -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).

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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()
```

View File

@@ -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

View File

@@ -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

View File

@@ -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 |

View File

@@ -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))

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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'

View File

@@ -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)'),

View File

@@ -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')

View File

@@ -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"])

View File

@@ -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='+',

View File

@@ -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'

View File

@@ -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)

View File

@@ -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')

View File

@@ -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')

View File

@@ -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]}

View File

@@ -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
#

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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')),
)

View File

@@ -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()

View File

@@ -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'),
)

View File

@@ -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

View File

@@ -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',
),
]

View File

@@ -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')

View File

@@ -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')

View File

@@ -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

View File

@@ -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

View File

@@ -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')

View File

@@ -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>")

View File

@@ -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)

View File

@@ -152,7 +152,6 @@ class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, Conne
required=False,
allow_null=True
)
status = ChoiceField(choices=PowerOutletStatusChoices, required=False)
class Meta:
model = PowerOutlet

View File

@@ -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')

View File

@@ -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:

View File

@@ -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')

View File

@@ -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):

View File

@@ -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')

View File

@@ -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)

View File

@@ -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()

View File

@@ -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'),

View File

@@ -1,2 +0,0 @@
class UnsupportedCablePath(Exception):
pass

View File

@@ -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()) |

View File

@@ -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):

View File

@@ -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
)
#

View File

@@ -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)

View File

@@ -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',
]

View File

@@ -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'])

View File

@@ -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 = (

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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',

View File

@@ -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',

View File

@@ -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='+',

View File

@@ -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),
),
]

View File

@@ -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),
),
]

View File

@@ -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),
),
]

View File

@@ -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),
]

View File

@@ -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')},
),
]

View File

@@ -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