Compare commits

..

No commits in common. "main" and "v4.3.0-beta1" have entirely different histories.

411 changed files with 50142 additions and 60023 deletions

View File

@ -15,7 +15,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v4.3.4
placeholder: v4.2.7
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.3.4
placeholder: v4.2.7
validations:
required: true
- type: dropdown

View File

@ -16,7 +16,7 @@ jobs:
if: github.repository == 'netbox-community/netbox'
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
- uses: dessant/lock-threads@v5
with:
issue-inactive-days: 90
pr-inactive-days: 30

View File

@ -48,7 +48,7 @@ jobs:
run: python netbox/manage.py makemessages -l ${{ env.LOCALE }}
- name: Commit changes
uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9.1.4
uses: EndBug/add-and-commit@v9
with:
add: 'netbox/translations/'
default_author: github_actions

View File

@ -8,7 +8,7 @@
</h3>
<h3>
:jigsaw: <a href="#jigsaw-creating-plugins">Create a plugin</a> &middot;
:briefcase: <a href="#briefcase-looking-for-a-job">Work with us!</a> &middot;
:rescue_worker_helmet: <a href="#rescue_worker_helmet-become-a-maintainer">Become a maintainer</a> &middot;
:heart: <a href="#heart-other-ways-to-contribute">Other ideas</a>
</h3>
</div>
@ -109,9 +109,21 @@ Do you have an idea for something you'd like to build in NetBox, but might not b
Check out our [plugin development tutorial](https://github.com/netbox-community/netbox-plugin-tutorial) to get started!
## :briefcase: Looking for a Job?
## :rescue_worker_helmet: Become a Maintainer
At [NetBox Labs](https://netboxlabs.com/), we're always looking for highly skilled and motivated people to join our team. While NetBox is a core part of our product lineup, we have an ever-expanding suite of solutions serving the network automation space. Check out our [current openings](https://netboxlabs.com/careers/) to see if you might be a fit!
We're always looking for motivated individuals to join the maintainers team and help drive NetBox's long-term development. Some of our most sought-after skills include:
* Python development with a strong focus on the [Django](https://www.djangoproject.com/) framework
* Expertise working with PostgreSQL databases
* Javascript & TypeScript proficiency
* A knack for web application design (HTML & CSS)
* Familiarity with git and software development best practices
* Excellent attention to detail
* Working experience in the field of network operations & engineering
We generally ask that maintainers dedicate around four hours of work to the project each week on average, which includes both hands-on development and project management tasks such as issue triage. Maintainers are also encouraged (but not required) to attend our bi-weekly Zoom call to catch up on recent items.
Interested? You can contact our lead maintainer, Jeremy Stretch, at jeremy@netbox.dev or on the [NetDev Community Slack](https://netdev.chat/). We'd love to have you on the team!
## :heart: Other Ways to Contribute

View File

@ -6,9 +6,9 @@
<a href="https://github.com/netbox-community/netbox/graphs/contributors"><img src="https://img.shields.io/github/contributors/netbox-community/netbox?color=blue" alt="Contributors" /></a>
<a href="https://github.com/netbox-community/netbox/stargazers"><img src="https://img.shields.io/github/stars/netbox-community/netbox?style=flat" alt="GitHub stars" /></a>
<a href="https://explore.transifex.com/netbox-community/netbox/"><img src="https://img.shields.io/badge/languages-15-blue" alt="Languages supported" /></a>
<a href="https://github.com/netbox-community/netbox/actions/workflows/ci.yml"><img src="https://github.com/netbox-community/netbox/actions/workflows/ci.yml/badge.svg" alt="CI status" /></a>
<a href="https://github.com/netbox-community/netbox/actions/workflows/ci.yml"><img src="https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=main" alt="CI status" /></a>
<p>
<strong><a href="https://netboxlabs.com/community/">NetBox Community</a></strong> |
<strong><a href="https://github.com/netbox-community/netbox/">NetBox Community</a></strong> |
<strong><a href="https://netboxlabs.com/netbox-cloud/">NetBox Cloud</a></strong> |
<strong><a href="https://netboxlabs.com/netbox-enterprise/">NetBox Enterprise</a></strong>
</p>

View File

@ -14,12 +14,6 @@ Administrators are encouraged to adhere to industry best practices concerning th
* Prohibit access to your database from clients other than the NetBox application
* Keep your deployment updated to the most recent stable release
## Compliance Reporting
Please note that security compliance reports (e.g. SOC 2) are provided by NetBox Labs only to customers using NetBox Cloud or NetBox Enterprise. They are not available to users of self-hosted NetBox Community Edition.
If you would like to consider upgrading to NetBox Cloud or Enterprise, please contact `sales@netboxlabs.com`.
## Reporting a Suspected Vulnerability
If you believe you've uncovered a security vulnerability and wish to report it confidentially, you may do so by emailing `security@netboxlabs.com`. Please ensure that your report meets all the following conditions:

View File

@ -14,10 +14,6 @@ django-debug-toolbar
# https://github.com/carltongibson/django-filter/blob/main/CHANGES.rst
django-filter
# Django Debug Toolbar extension for GraphiQL
# https://github.com/flavors/django-graphiql-debug-toolbar/blob/main/CHANGES.rst
django-graphiql-debug-toolbar
# HTMX utilities for Django
# https://django-htmx.readthedocs.io/en/latest/changelog.html
django-htmx
@ -112,7 +108,6 @@ nh3
# Fork of PIL (Python Imaging Library) for image processing
# https://github.com/python-pillow/Pillow/releases
# https://pillow.readthedocs.io/en/stable/releasenotes/
Pillow
# PostgreSQL database adapter for Python
@ -131,22 +126,21 @@ requests
# https://github.com/rq/rq/blob/master/CHANGES.md
rq
# Django app for social-auth-core
# https://github.com/python-social-auth/social-app-django/blob/master/CHANGELOG.md
social-auth-app-django
# Social authentication framework
# https://github.com/python-social-auth/social-core/blob/master/CHANGELOG.md
social-auth-core
# Django app for social-auth-core
# https://github.com/python-social-auth/social-app-django/blob/master/CHANGELOG.md
social-auth-app-django
# Strawberry GraphQL
# https://github.com/strawberry-graphql/strawberry/blob/main/CHANGELOG.md
strawberry-graphql
# Strawberry GraphQL Django extension
# https://github.com/strawberry-graphql/strawberry-django/releases
# See #19771
strawberry-graphql-django==0.60.0
strawberry-graphql-django
# SVG image rendering (used for rack elevations)
# https://github.com/mozman/svgwrite/blob/master/NEWS.rst

View File

@ -329,7 +329,6 @@
"100base-tx",
"100base-t1",
"1000base-t",
"1000base-sx",
"1000base-lx",
"1000base-tx",
"2.5gbase-t",

View File

@ -69,7 +69,7 @@ For a complete list of available preferences, log into NetBox and navigate to `/
!!! tip "Dynamic Configuration Parameter"
Default: `50`
Default: 50
The default maximum number of objects to display per page within each list of objects.
@ -79,7 +79,7 @@ The default maximum number of objects to display per page within each list of ob
!!! tip "Dynamic Configuration Parameter"
Default: `15`
Default: 15
The default value for the `amperage` field when creating new power feeds.
@ -89,7 +89,7 @@ The default value for the `amperage` field when creating new power feeds.
!!! tip "Dynamic Configuration Parameter"
Default: `80`
Default: 80
The default value (percentage) for the `max_utilization` field when creating new power feeds.
@ -99,7 +99,7 @@ The default value (percentage) for the `max_utilization` field when creating new
!!! tip "Dynamic Configuration Parameter"
Default: `120`
Default: 120
The default value for the `voltage` field when creating new power feeds.
@ -109,7 +109,7 @@ The default value for the `voltage` field when creating new power feeds.
!!! tip "Dynamic Configuration Parameter"
Default: `22`
Default: 22
Default height (in pixels) of a unit within a rack elevation. For best results, this should be approximately one tenth of `RACK_ELEVATION_DEFAULT_UNIT_WIDTH`.
@ -119,6 +119,6 @@ Default height (in pixels) of a unit within a rack elevation. For best results,
!!! tip "Dynamic Configuration Parameter"
Default: `220`
Default: 220
Default width (in pixels) of a unit within a rack elevation.

View File

@ -2,7 +2,7 @@
## DEBUG
Default: `False`
Default: False
This setting enables debugging. Debugging should be enabled only during development or troubleshooting. Note that only
clients which access NetBox from a recognized [internal IP address](./system.md#internal_ips) will see debugging tools in the user
@ -16,6 +16,6 @@ interface.
## DEVELOPER
Default: `False`
Default: False
This parameter serves as a safeguard to prevent some potentially dangerous behavior, such as generating new database schema migrations. Additionally, enabling this setting disables the debug warning banner in the UI. Set this to `True` **only** if you are actively developing the NetBox code base.

View File

@ -2,9 +2,9 @@
## SENTRY_DSN
Default: `None`
Default: None
Defines a Sentry data source name (DSN) for automated error reporting. `SENTRY_ENABLED` must be `True` for this parameter to take effect. For example:
Defines a Sentry data source name (DSN) for automated error reporting. `SENTRY_ENABLED` must be True for this parameter to take effect. For example:
```
SENTRY_DSN = "https://examplePublicKey@o0.ingest.sentry.io/0"
@ -14,9 +14,9 @@ SENTRY_DSN = "https://examplePublicKey@o0.ingest.sentry.io/0"
## SENTRY_ENABLED
Default: `False`
Default: False
Set to `True` to enable automatic error reporting via [Sentry](https://sentry.io/).
Set to True to enable automatic error reporting via [Sentry](https://sentry.io/).
!!! note
The `sentry-sdk` Python package is required to enable Sentry integration.
@ -25,7 +25,7 @@ Set to `True` to enable automatic error reporting via [Sentry](https://sentry.io
## SENTRY_SAMPLE_RATE
Default: `1.0` (all)
Default: 1.0 (all)
The sampling rate for errors. Must be a value between 0 (disabled) and 1.0 (report on all errors).
@ -33,7 +33,7 @@ The sampling rate for errors. Must be a value between 0 (disabled) and 1.0 (repo
## SENTRY_SEND_DEFAULT_PII
Default: `False`
Default: False
Maps to the Sentry SDK's [`send_default_pii`](https://docs.sentry.io/platforms/python/configuration/options/#send-default-pii) parameter. If enabled, certain personally identifiable information (PII) is added.
@ -60,7 +60,7 @@ SENTRY_TAGS = {
## SENTRY_TRACES_SAMPLE_RATE
Default: `0` (disabled)
Default: 0 (disabled)
The sampling rate for transactions. Must be a value between 0 (disabled) and 1.0 (report on all transactions).

View File

@ -4,14 +4,14 @@
!!! tip "Dynamic Configuration Parameter"
Default: `True`
Default: True
Setting this to `False` will disable the GraphQL API.
Setting this to False will disable the GraphQL API.
---
## GRAPHQL_MAX_ALIASES
Default: `10`
Default: 10
The maximum number of queries that a GraphQL API request may contain.

View File

@ -55,9 +55,9 @@ Sets content for the top banner in the user interface.
## CENSUS_REPORTING_ENABLED
Default: `True`
Default: True
Enables anonymous census reporting. To opt out of census reporting, set this to `False`.
Enables anonymous census reporting. To opt out of census reporting, set this to False.
This data enables the project maintainers to estimate how many NetBox deployments exist and track the adoption of new versions over time. Census reporting effects a single HTTP request each time a worker starts. The only data reported by this function are the NetBox version, Python version, and a pseudorandom unique identifier.
@ -67,7 +67,7 @@ This data enables the project maintainers to estimate how many NetBox deployment
!!! tip "Dynamic Configuration Parameter"
Default: `90`
Default: 90
The number of days to retain logged changes (object creations, updates, and deletions). Set this to `0` to retain
changes in the database indefinitely.
@ -79,7 +79,7 @@ changes in the database indefinitely.
## CHANGELOG_SKIP_EMPTY_CHANGES
Default: `True`
Default: True
If enabled, a change log record will not be created when an object is updated without any changes to its existing field values.
@ -100,9 +100,9 @@ The maximum size (in bytes) of an incoming HTTP request (i.e. `GET` or `POST` da
!!! tip "Dynamic Configuration Parameter"
Default: `True`
Default: True
By default, NetBox will prevent the creation of duplicate prefixes and IP addresses in the global table (that is, those which are not assigned to any VRF). This validation can be disabled by setting `ENFORCE_GLOBAL_UNIQUE` to `False`.
By default, NetBox will prevent the creation of duplicate prefixes and IP addresses in the global table (that is, those which are not assigned to any VRF). This validation can be disabled by setting `ENFORCE_GLOBAL_UNIQUE` to False.
---
@ -128,7 +128,7 @@ The maximum amount (in bytes) of uploaded data that will be held in memory befor
!!! tip "Dynamic Configuration Parameter"
Default: `90`
Default: 90
The number of days to retain job results (scripts and reports). Set this to `0` to retain job results in the database indefinitely.
@ -141,9 +141,9 @@ The number of days to retain job results (scripts and reports). Set this to `0`
!!! tip "Dynamic Configuration Parameter"
Default: `False`
Default: False
Setting this to `True` will display a "maintenance mode" banner at the top of every page. Additionally, NetBox will no longer update a user's "last active" time upon login. This is to allow new logins when the database is in a read-only state. Recording of login times will resume when maintenance mode is disabled.
Setting this to True will display a "maintenance mode" banner at the top of every page. Additionally, NetBox will no longer update a user's "last active" time upon login. This is to allow new logins when the database is in a read-only state. Recording of login times will resume when maintenance mode is disabled.
---
@ -161,7 +161,7 @@ This specifies the URL to use when presenting a map of a physical location by st
!!! tip "Dynamic Configuration Parameter"
Default: `1000`
Default: 1000
A web user or API consumer can request an arbitrary number of objects by appending the "limit" parameter to the URL (e.g. `?limit=1000`). This parameter defines the maximum acceptable limit. Setting this to `0` or `None` will allow a client to retrieve _all_ matching objects at once with no limit by specifying `?limit=0`.
@ -169,7 +169,7 @@ A web user or API consumer can request an arbitrary number of objects by appendi
## METRICS_ENABLED
Default: `False`
Default: False
Toggle the availability Prometheus-compatible metrics at `/metrics`. See the [Prometheus Metrics](../integrations/prometheus-metrics.md) documentation for more details.
@ -179,9 +179,9 @@ Toggle the availability Prometheus-compatible metrics at `/metrics`. See the [Pr
!!! tip "Dynamic Configuration Parameter"
Default: `False`
Default: False
When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to `True` to prefer IPv4 instead.
When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to prefer IPv4 instead.
---
@ -203,7 +203,7 @@ If no queue is defined the queue named `default` will be used.
## RELEASE_CHECK_URL
Default: `None` (disabled)
Default: None (disabled)
This parameter defines the URL of the repository that will be checked for new NetBox releases. When a new release is detected, a message will be displayed to administrative users on the home page. This can be set to the official repository (`'https://api.github.com/repos/netbox-community/netbox/releases'`) or a custom fork. Set this to `None` to disable automatic update checks.

View File

@ -2,7 +2,7 @@
## PLUGINS
Default: `[]`
Default: Empty
A list of installed [NetBox plugins](../plugins/index.md) to enable. Plugins will not take effect unless they are listed here.
@ -13,7 +13,7 @@ A list of installed [NetBox plugins](../plugins/index.md) to enable. Plugins wil
## PLUGINS_CONFIG
Default: `[]`
Default: Empty
This parameter holds configuration settings for individual NetBox plugins. It is defined as a dictionary, with each key using the name of an installed plugin. The specific parameters supported are unique to each plugin: Reference the plugin's documentation to determine the supported parameters. An example configuration is shown below:
@ -35,7 +35,7 @@ Note that a plugin must be listed in `PLUGINS` for its configuration to take eff
## PLUGINS_CATALOG_CONFIG
Default: `{}` (Empty)
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.

View File

@ -1,6 +1,6 @@
# Remote Authentication Settings
The configuration parameters listed here control remote authentication for NetBox. Note that `REMOTE_AUTH_ENABLED` must be `True` in order for these settings to take effect.
The configuration parameters listed here control remote authentication for NetBox. Note that `REMOTE_AUTH_ENABLED` must be true in order for these settings to take effect.
---
@ -8,7 +8,7 @@ The configuration parameters listed here control remote authentication for NetBo
Default: `False`
If `True`, NetBox will automatically create groups specified in the `REMOTE_AUTH_GROUP_HEADER` header if they don't already exist. (Requires `REMOTE_AUTH_ENABLED`.)
If true, NetBox will automatically create groups specified in the `REMOTE_AUTH_GROUP_HEADER` header if they don't already exist. (Requires `REMOTE_AUTH_ENABLED`.)
---
@ -16,7 +16,7 @@ If `True`, NetBox will automatically create groups specified in the `REMOTE_AUTH
Default: `False`
If `True`, NetBox will automatically create local accounts for users authenticated via a remote service. (Requires `REMOTE_AUTH_ENABLED`.)
If true, NetBox will automatically create local accounts for users authenticated via a remote service. (Requires `REMOTE_AUTH_ENABLED`.)
---
@ -43,7 +43,7 @@ The list of groups to assign a new user account when created using remote authen
Default: `{}` (Empty dictionary)
A mapping of permissions to assign a new user account when created using remote authentication. Each key in the dictionary should be set to a dictionary of the attributes to be applied to the permission, or `None` to allow all objects. (Requires `REMOTE_AUTH_ENABLED` as `True` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` as `False`.)
A mapping of permissions to assign a new user account when created using remote authentication. Each key in the dictionary should be set to a dictionary of the attributes to be applied to the permission, or `None` to allow all objects. (Requires `REMOTE_AUTH_ENABLED` as True and `REMOTE_AUTH_GROUP_SYNC_ENABLED` as False.)
---

View File

@ -2,12 +2,12 @@
## ALLOWED_HOSTS
This is a list of valid fully-qualified domain names (FQDNs) and/or IP addresses that can be used to reach the NetBox service. Usually this is the same as the hostname for the NetBox server, but can also be different; for example, when using a reverse proxy serving the NetBox website under a different FQDN than the hostname of the NetBox server. To help guard against [HTTP Host header attacks](https://docs.djangoproject.com/en/stable/topics/security/#host-headers-virtual-hosting), NetBox will not permit access to the server via any other hostnames (or IPs).
This is a list of valid fully-qualified domain names (FQDNs) and/or IP addresses that can be used to reach the NetBox service. Usually this is the same as the hostname for the NetBox server, but can also be different; for example, when using a reverse proxy serving the NetBox website under a different FQDN than the hostname of the NetBox server. To help guard against [HTTP Host header attacks](https://docs.djangoproject.com/en/3.0/topics/security/#host-headers-virtual-hosting), NetBox will not permit access to the server via any other hostnames (or IPs).
!!! note
This parameter must always be defined as a list or tuple, even if only a single value is provided.
The value of this option is also used to set `CSRF_TRUSTED_ORIGINS`, which restricts POST requests to the same set of hosts (more about this [here](https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-CSRF_TRUSTED_ORIGINS)). Keep in mind that NetBox, by default, sets `USE_X_FORWARDED_HOST` to `True`, which means that if you're using a reverse proxy, it's the FQDN used to reach that reverse proxy which needs to be in this list (more about this [here](https://docs.djangoproject.com/en/stable/ref/settings/#allowed-hosts)).
The value of this option is also used to set `CSRF_TRUSTED_ORIGINS`, which restricts POST requests to the same set of hosts (more about this [here](https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-CSRF_TRUSTED_ORIGINS)). Keep in mind that NetBox, by default, sets `USE_X_FORWARDED_HOST` to true, which means that if you're using a reverse proxy, it's the FQDN used to reach that reverse proxy which needs to be in this list (more about this [here](https://docs.djangoproject.com/en/stable/ref/settings/#allowed-hosts)).
Example:

View File

@ -2,10 +2,10 @@
## ALLOW_TOKEN_RETRIEVAL
Default: `False`
Default: False
!!! note
The default value of this parameter changed from `True` to `False` in NetBox v4.3.0.
The default value of this parameter changed from true to false in NetBox v4.3.0.
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.
@ -50,9 +50,9 @@ Although it is not recommended, the default validation rules can be disabled by
## CORS_ORIGIN_ALLOW_ALL
Default: `False`
Default: False
If `True`, cross-origin resource sharing (CORS) requests will be accepted from all origins. If False, a whitelist will be used (see below).
If True, cross-origin resource sharing (CORS) requests will be accepted from all origins. If False, a whitelist will be used (see below).
---
@ -62,7 +62,7 @@ If `True`, cross-origin resource sharing (CORS) requests will be accepted from a
These settings specify a list of origins that are authorized to make cross-site API requests. Use
`CORS_ORIGIN_WHITELIST` to define a list of exact hostnames, or `CORS_ORIGIN_REGEX_WHITELIST` to define a set of regular
expressions. (These settings have no effect if `CORS_ORIGIN_ALLOW_ALL` is `True`.) For example:
expressions. (These settings have no effect if `CORS_ORIGIN_ALLOW_ALL` is True.) For example:
```python
CORS_ORIGIN_WHITELIST = [
@ -82,9 +82,9 @@ The name of the cookie to use for the cross-site request forgery (CSRF) authenti
## CSRF_COOKIE_SECURE
Default: `False`
Default: False
If `True`, the cookie employed for cross-site request forgery (CSRF) protection will be marked as secure, meaning that it can only be sent across an HTTPS connection.
If true, the cookie employed for cross-site request forgery (CSRF) protection will be marked as secure, meaning that it can only be sent across an HTTPS connection.
---
@ -92,7 +92,7 @@ If `True`, the cookie employed for cross-site request forgery (CSRF) protection
Default: `[]`
Defines a list of trusted origins for unsafe (e.g. `POST`) requests. This is a pass-through to Django's [`CSRF_TRUSTED_ORIGINS`](https://docs.djangoproject.com/en/stable/ref/settings/#csrf-trusted-origins) setting. Note that each host listed must specify a scheme (e.g. `http://` or `https://).
Defines a list of trusted origins for unsafe (e.g. `POST`) requests. This is a pass-through to Django's [`CSRF_TRUSTED_ORIGINS`](https://docs.djangoproject.com/en/4.0/ref/settings/#std:setting-CSRF_TRUSTED_ORIGINS) setting. Note that each host listed must specify a scheme (e.g. `http://` or `https://).
```python
CSRF_TRUSTED_ORIGINS = (
@ -135,7 +135,7 @@ DEFAULT_PERMISSIONS = {
## EXEMPT_VIEW_PERMISSIONS
Default: `[]` (Empty list)
Default: Empty list
A list of NetBox models to exempt from the enforcement of view permissions. Models listed here will be viewable by all users, both authenticated and anonymous.
@ -162,9 +162,9 @@ EXEMPT_VIEW_PERMISSIONS = ['*']
## LOGIN_PERSISTENCE
Default: `False`
Default: False
If `True`, the lifetime of a user's authentication session will be automatically reset upon each valid request. For example, if [`LOGIN_TIMEOUT`](#login_timeout) is configured to 14 days (the default), and a user whose session is due to expire in five days makes a NetBox request (with a valid session cookie), the session's lifetime will be reset to 14 days.
If true, the lifetime of a user's authentication session will be automatically reset upon each valid request. For example, if [`LOGIN_TIMEOUT`](#login_timeout) is configured to 14 days (the default), and a user whose session is due to expire in five days makes a NetBox request (with a valid session cookie), the session's lifetime will be reset to 14 days.
Note that enabling this setting causes NetBox to update a user's session in the database (or file, as configured per [`SESSION_FILE_PATH`](#session_file_path)) with each request, which may introduce significant overhead in very active environments. It also permits an active user to remain authenticated to NetBox indefinitely.
@ -172,7 +172,7 @@ Note that enabling this setting causes NetBox to update a user's session in the
## LOGIN_REQUIRED
Default: `True`
Default: True
When enabled, only authenticated users are permitted to access any part of NetBox. Disabling this will allow unauthenticated users to access most areas of NetBox (but not make any changes).
@ -183,7 +183,7 @@ When enabled, only authenticated users are permitted to access any part of NetBo
## LOGIN_TIMEOUT
Default: `1209600` seconds (14 days)
Default: 1209600 seconds (14 days)
The lifetime (in seconds) of the authentication cookie issued to a NetBox user upon login.
@ -191,7 +191,7 @@ The lifetime (in seconds) of the authentication cookie issued to a NetBox user u
## LOGIN_FORM_HIDDEN
Default: `False`
Default: False
Option to hide the login form when only SSO authentication is in use.
@ -210,23 +210,23 @@ The view name or URL to which a user is redirected after logging out.
## SECURE_HSTS_INCLUDE_SUBDOMAINS
Default: `False`
Default: False
If `True`, the `includeSubDomains` directive will be included in the HTTP Strict Transport Security (HSTS) header. This directive instructs the browser to apply the HSTS policy to all subdomains of the current domain.
If true, the `includeSubDomains` directive will be included in the HTTP Strict Transport Security (HSTS) header. This directive instructs the browser to apply the HSTS policy to all subdomains of the current domain.
---
## SECURE_HSTS_PRELOAD
Default: `False`
Default: False
If `True`, the `preload` directive will be included in the HTTP Strict Transport Security (HSTS) header. This directive instructs the browser to preload the site in HTTPS. Browsers that use the HSTS preload list will force the site to be accessed via HTTPS even if the user types HTTP in the address bar.
If true, the `preload` directive will be included in the HTTP Strict Transport Security (HSTS) header. This directive instructs the browser to preload the site in HTTPS. Browsers that use the HSTS preload list will force the site to be accessed via HTTPS even if the user types HTTP in the address bar.
---
## SECURE_HSTS_SECONDS
Default: `0`
Default: 0
If set to a non-zero integer value, the SecurityMiddleware sets the HTTP Strict Transport Security (HSTS) header on all responses that do not already have it. This will instruct the browser that the website must be accessed via HTTPS, blocking any HTTP request.
@ -234,9 +234,9 @@ If set to a non-zero integer value, the SecurityMiddleware sets the HTTP Strict
## SECURE_SSL_REDIRECT
Default: `False`
Default: False
If `True`, all non-HTTPS requests will be automatically redirected to use HTTPS.
If true, all non-HTTPS requests will be automatically redirected to use HTTPS.
!!! warning
Ensure that your frontend HTTP daemon has been configured to forward the HTTP scheme correctly before enabling this option. An incorrectly configured frontend may result in a looping redirect.
@ -253,14 +253,14 @@ The name used for the session cookie. See the [Django documentation](https://doc
## SESSION_COOKIE_SECURE
Default: `False`
Default: False
If `True`, the cookie employed for session authentication will be marked as secure, meaning that it can only be sent across an HTTPS connection.
If true, the cookie employed for session authentication will be marked as secure, meaning that it can only be sent across an HTTPS connection.
---
## SESSION_FILE_PATH
Default: `None`
Default: None
HTTP session data is used to track authenticated users when they access NetBox. By default, NetBox stores session data in its PostgreSQL database. However, this inhibits authentication to a standby instance of NetBox without write access to the database. Alternatively, a local file path may be specified here and NetBox will store session data as files instead of using the database. Note that the NetBox system user must have read and write permissions to this path.

View File

@ -2,7 +2,7 @@
## BASE_PATH
Default: `None`
Default: None
The base URL path to use when accessing NetBox. Do not include the scheme or domain name. For example, if installed at https://example.com/netbox/, set:
@ -74,7 +74,7 @@ Email is sent from NetBox only for critical events or if configured for [logging
## HTTP_PROXIES
Default: `None`
Default: Empty
A dictionary of HTTP proxies to use for outbound requests originating from NetBox (e.g. when sending webhook requests). Proxies should be specified by schema (HTTP and HTTPS) as per the [Python requests library documentation](https://requests.readthedocs.io/en/latest/user/advanced/#proxies). For example:
@ -95,15 +95,15 @@ Default: `('127.0.0.1', '::1')`
A list of IP addresses recognized as internal to the system, used to control the display of debugging output. For
example, the debugging toolbar will be viewable only when a client is accessing NetBox from one of the listed IP
addresses (and [`DEBUG`](./development.md#debug) is `True`).
addresses (and [`DEBUG`](./development.md#debug) is true).
---
## ISOLATED_DEPLOYMENT
Default: `False`
Default: False
Set this configuration parameter to `True` for NetBox deployments which do not have Internet access. This will disable miscellaneous functionality which depends on access to the Internet.
Set this configuration parameter to True for NetBox deployments which do not have Internet access. This will disable miscellaneous functionality which depends on access to the Internet.
!!! note
If Internet access is available via a proxy, set [`HTTP_PROXIES`](#http_proxies) instead.
@ -114,7 +114,7 @@ Set this configuration parameter to `True` for NetBox deployments which do not h
Default: `{}`
A dictionary of custom Jinja2 filters with the key being the filter name and the value being a callable. For more information see the [Jinja2 documentation](https://jinja.palletsprojects.com/en/3.1.x/api/#custom-filters). For example:
A dictionary of custom jinja2 filters with the key being the filter name and the value being a callable. For more information see the [Jinja2 documentation](https://jinja.palletsprojects.com/en/3.1.x/api/#custom-filters). For example:
```python
def uppercase(x):
@ -158,7 +158,6 @@ LOGGING = {
* `netbox.<app>.<model>` - Generic form for model-specific log messages
* `netbox.auth.*` - Authentication events
* `netbox.api.views.*` - Views which handle business logic for the REST API
* `netbox.event_rules` - Event rules
* `netbox.reports.*` - Report execution (`module.name`)
* `netbox.scripts.*` - Custom script execution (`module.name`)
* `netbox.views.*` - Views which handle business logic for the web UI
@ -254,7 +253,7 @@ The specific configuration settings for each storage backend can be found in the
## TIME_ZONE
Default: `"UTC"`
Default: UTC
The time zone NetBox will use when dealing with dates and times. It is recommended to use UTC time unless you have a specific need to use a local time zone. Please see the [list of available time zones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones).
@ -262,6 +261,6 @@ The time zone NetBox will use when dealing with dates and times. It is recommend
## TRANSLATION_ENABLED
Default: `True`
Default: True
Enables language translation for the user interface. (This parameter maps to Django's [USE_I18N](https://docs.djangoproject.com/en/stable/ref/settings/#std-setting-USE_I18N) setting.)

View File

@ -147,7 +147,7 @@ For UI development you will need to review the [Web UI Development Guide](web-ui
## Populating Demo Data
Once you have your development environment up and running, it might be helpful to populate some "dummy" data to make interacting with the UI and APIs more convenient. Check out the [netbox-demo-data](https://github.com/netbox-community/netbox-demo-data) repo on GitHub, which houses a collection of sample data that can be easily imported to any new NetBox deployment. This sample data is used to populate the [public demo instance](https://demo.netbox.dev).
Once you have your development environment up and running, it might be helpful to populate some "dummy" data to make interacting with the UI and APIs more convenient. Check out the [netbox-demo-data](https://github.com/netbox-community/netbox-demo-data) repo on GitHub, which houses a collection of sample data that can be easily imported to any new NetBox deployment. (This sample data is used to populate the public demo instance at <https://demo.netbox.dev>.)
The demo data is provided in JSON format and loaded into an empty database using Django's `loaddata` management command. Consult the demo data repo's `README` file for complete instructions on populating the data.

View File

@ -53,8 +53,6 @@ If a new Django release is adopted or other major dependencies (Python, PostgreS
* 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.
* Update the minimum PostgreSQL version in the programming error template (`netbox/templates/exceptions/programming_error.html`).
* Update the minimum and supported Python versions in the project metadata file (`pyproject.toml`)
### Manually Perform a New Install
@ -152,7 +150,7 @@ This will automatically update the schema file at `contrib/generated_schema.json
Updated language translations should be pulled from [Transifex](https://app.transifex.com/netbox-community/netbox/dashboard/) and re-compiled for each new release. First, retrieve any updated translation files using the Transifex CLI client:
```no-highlight
tx pull --force
tx pull
```
Then, compile these portable (`.po`) files for use in the application:
@ -166,8 +164,7 @@ Then, compile these portable (`.po`) files for use in the application:
### Update Version and Changelog
* Update the version number and published date in `netbox/release.yaml`. Add or remove the designation (e.g. `beta1`) if applicable.
* Copy the version number from `release.yaml` to `pyproject.toml` in the project root.
* Update the version number and date in `netbox/release.yaml`. Add or remove the designation (e.g. `beta1`) if applicable.
* Update the example version numbers in the feature request and bug report templates under `.github/ISSUE_TEMPLATES/`.
* 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.
@ -193,3 +190,15 @@ Create a [new release](https://github.com/netbox-community/netbox/releases/new)
* **Description:** Copy from the pull request body, then promote the `###` headers to `##` ones
Once created, the release will become available for users to install.
### Update the Public Documentation
After a release has been published, the public NetBox documentation needs to be updated. This is accomplished by running two actions on the [netboxlabs-docs](https://github.com/netboxlabs/netboxlabs-docs) repository.
First, run the `build-site` action, by navigating to Actions > build-site > Run workflow. This process compiles the documentation along with an overlay for integration with the documentation portal at <https://netboxlabs.com/docs>. The job should take about two minutes.
Once the documentation files have been compiled, they must be published by running the `deploy-kinsta` action. Select the desired deployment environment (staging or production) and specify `latest` as the deploy tag.
Clear the CDN cache from the [Kinsta](https://my.kinsta.com/) portal. Navigate to _Sites_ / _NetBox Labs_ / _Live_, select _Cache_ in the left-nav, click the _Clear Cache_ button, and confirm the clear operation.
Finally, verify that the documentation at <https://netboxlabs.com/docs/netbox/en/stable/> has been updated.

View File

@ -33,10 +33,10 @@ To download translated strings automatically, you'll need to:
Once you have the client set up, run the following command from the project root (e.g. `/opt/netbox/`):
```no-highlight
TX_TOKEN=$TOKEN tx pull --force
TX_TOKEN=$TOKEN tx pull
```
This will download all portable (`.po`) translation files from Transifex, updating them locally as needed. (The `--force` argument instructs the client to disregard the timestamps of local translation files.)
This will download all portable (`.po`) translation files from Transifex, updating them locally as needed.
Once retrieved, the updated strings need to be compiled into new `.mo` files so they can be used by the application. Run Django's [`compilemessages`](https://docs.djangoproject.com/en/stable/ref/django-admin/#django-admin-compilemessages) management command to compile them:

View File

@ -2,9 +2,9 @@
NetBox includes the ability to execute certain functions as background tasks. These include:
* [Report](../customization/reports.md) execution
* [Custom script](../customization/custom-scripts.md) execution
* Synchronization of [remote data sources](../integrations/synchronized-data.md)
* Housekeeping tasks
Additionally, NetBox plugins can enqueue their own background tasks. This is accomplished using the [Job model](../models/core/job.md). Background tasks are executed by the `rqworker` process(es).

View File

@ -9,7 +9,7 @@ NetBox is the leading solution for modeling and documenting modern networks. By
## :material-server-network: Built for Networks
Unlike general-purpose configuration management databases (CMDBs), NetBox has curated a data model which caters specifically to the needs of network engineers and operators. It delivers a wide assortment of object types carefully crafted to best serve the needs of infrastructure design and documentation. These cover all facets of network technology, from IP address managements to cabling to overlays and more:
Unlike general-purpose CMDBs, NetBox has curated a data model which caters specifically to the needs of network engineers and operators. It delivers a wide assortment of object types carefully crafted to best serve the needs of infrastructure design and documentation. These cover all facets of network technology, from IP address managements to cabling to overlays and more:
* Hierarchical regions, sites, and locations
* Racks, devices, and device components

View File

@ -7,11 +7,33 @@ This section entails the installation and configuration of a local PostgreSQL da
## Installation
=== "Ubuntu"
```no-highlight
sudo apt update
sudo apt install -y postgresql
```
=== "CentOS"
```no-highlight
sudo yum install -y postgresql-server
sudo postgresql-setup --initdb
```
CentOS configures ident host-based authentication for PostgreSQL by default. Because NetBox will need to authenticate using a username and password, modify `/var/lib/pgsql/data/pg_hba.conf` to support MD5 authentication by changing `ident` to `md5` for the lines below:
```no-highlight
host all all 127.0.0.1/32 md5
host all all ::1/128 md5
```
Once PostgreSQL has been installed, start the service and enable it to run at boot:
```no-highlight
sudo systemctl enable --now postgresql
```
Before continuing, verify that you have installed PostgreSQL 14 or later:
```no-highlight

View File

@ -4,10 +4,19 @@
[Redis](https://redis.io/) is an in-memory key-value store which NetBox employs for caching and queuing. This section entails the installation and configuration of a local Redis instance. If you already have a Redis service in place, skip to [the next section](3-netbox.md).
=== "Ubuntu"
```no-highlight
sudo apt install -y redis-server
```
=== "CentOS"
```no-highlight
sudo yum install -y redis
sudo systemctl enable --now redis
```
Before continuing, verify that your installed version of Redis is at least v4.0:
```no-highlight

View File

@ -9,10 +9,16 @@ Begin by installing all system packages required by NetBox and its dependencies.
!!! warning "Python 3.10 or later required"
NetBox supports Python 3.10, 3.11, and 3.12.
=== "Ubuntu"
```no-highlight
sudo apt install -y python3 python3-pip python3-venv python3-dev \
build-essential libxml2-dev libxslt1-dev libffi-dev libpq-dev \
libssl-dev zlib1g-dev
sudo apt install -y python3 python3-pip python3-venv python3-dev build-essential libxml2-dev libxslt1-dev libffi-dev libpq-dev libssl-dev zlib1g-dev
```
=== "CentOS"
```no-highlight
sudo yum install -y gcc libxml2-devel libxslt-devel libffi-devel libpq-devel openssl-devel redhat-rpm-config
```
Before continuing, check that your installed Python version is at least 3.10:
@ -49,10 +55,18 @@ cd /opt/netbox/
If `git` is not already installed, install it:
=== "Ubuntu"
```no-highlight
sudo apt install -y git
```
=== "CentOS"
```no-highlight
sudo yum install -y git
```
Next, clone the git repository:
```no-highlight
@ -83,6 +97,8 @@ Using this installation method enables easy upgrades in the future by simply che
Create a system user account named `netbox`. We'll configure the WSGI and HTTP services to run under this account. We'll also assign this user ownership of the media directory. This ensures that NetBox will be able to save uploaded files.
=== "Ubuntu"
```
sudo adduser --system --group netbox
sudo chown --recursive netbox /opt/netbox/netbox/media/
@ -90,6 +106,16 @@ sudo chown --recursive netbox /opt/netbox/netbox/reports/
sudo chown --recursive netbox /opt/netbox/netbox/scripts/
```
=== "CentOS"
```
sudo groupadd --system netbox
sudo adduser --system -g netbox netbox
sudo chown --recursive netbox /opt/netbox/netbox/media/
sudo chown --recursive netbox /opt/netbox/netbox/reports/
sudo chown --recursive netbox /opt/netbox/netbox/scripts/
```
## Configuration
Move into the NetBox configuration directory and make a copy of `configuration_example.py` named `configuration.py`. This file will hold all of your local configuration parameters.
@ -108,7 +134,7 @@ Open `configuration.py` with your preferred editor to begin configuring NetBox.
### ALLOWED_HOSTS
This is a list of the valid hostnames and IP addresses by which this server can be reached. You must specify at least one name or IP address. (Note that this does not restrict the locations from which NetBox may be accessed: It is merely for [HTTP host header validation](https://docs.djangoproject.com/en/stable/topics/security/#host-headers-virtual-hosting).)
This is a list of the valid hostnames and IP addresses by which this server can be reached. You must specify at least one name or IP address. (Note that this does not restrict the locations from which NetBox may be accessed: It is merely for [HTTP host header validation](https://docs.djangoproject.com/en/3.0/topics/security/#host-headers-virtual-hosting).)
```python
ALLOWED_HOSTS = ['netbox.example.com', '192.0.2.123']
@ -224,7 +250,7 @@ Once NetBox has been configured, we're ready to proceed with the actual installa
* Create a Python virtual environment
* Installs all required Python packages
* Run database schema migrations (skip with `--readonly`)
* Run database schema migrations
* Builds the documentation locally (for offline use)
* Aggregate static resource files on disk
@ -244,9 +270,6 @@ sudo PYTHON=/usr/bin/python3.10 /opt/netbox/upgrade.sh
!!! note
Upon completion, the upgrade script may warn that no existing virtual environment was detected. As this is a new installation, this warning can be safely ignored.
!!! note
To run the script on a node connected to a database in read-only mode, include the `--readonly` parameter. This will skip the application of any database migrations.
## Create a Super User
NetBox does not come with any predefined user accounts. You'll need to create a super user (administrative account) to be able to log into NetBox. First, enter the Python virtual environment created by the upgrade script:

View File

@ -28,7 +28,7 @@ NetBox ships with a default configuration file for uWSGI. To use it, copy `/opt/
sudo cp /opt/netbox/contrib/uwsgi.ini /opt/netbox/uwsgi.ini
```
While the provided configuration should suffice for most initial installations, you may wish to edit this file to change the bound IP address and/or port number, or to make performance-related adjustments. See [the uWSGI documentation](https://uwsgi-docs-additions.readthedocs.io/en/latest/Options.html) for the available configuration parameters and take a minute to review the [Things to know](https://uwsgi-docs.readthedocs.io/en/latest/ThingsToKnow.html) page. Django also provides [additional documentation](https://docs.djangoproject.com/en/stable/howto/deployment/wsgi/uwsgi/) on configuring uWSGI with a Django app.
While the provided configuration should suffice for most initial installations, you may wish to edit this file to change the bound IP address and/or port number, or to make performance-related adjustments. See [the uWSGI documentation](https://uwsgi-docs-additions.readthedocs.io/en/latest/Options.html) for the available configuration parameters and take a minute to review the [Things to know](https://uwsgi-docs.readthedocs.io/en/latest/ThingsToKnow.html) page. Django also provides [additional documentation](https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/uwsgi/) on configuring uWSGI with a Django app.
## systemd Setup

View File

@ -6,10 +6,18 @@ This guide explains how to implement LDAP authentication using an external serve
### Install System Packages
On Ubuntu:
```no-highlight
sudo apt install -y libldap2-dev libsasl2-dev libssl-dev
```
On CentOS:
```no-highlight
sudo yum install -y openldap-devel python3-devel
```
### Install django-auth-ldap
Activate the Python virtual environment and install the `django-auth-ldap` package using pip:

View File

@ -1,18 +1,11 @@
# Installation
<div class="grid cards" markdown>
!!! info "NetBox Cloud"
The instructions below are for installing NetBox as a standalone, self-hosted application. For a Cloud-delivered solution, check out [NetBox Cloud](https://netboxlabs.com/netbox-cloud/) by NetBox Labs.
- :material-clock-fast:{ .lg .middle } __Eager to Get Started?__
The installation instructions provided here have been tested to work on Ubuntu 22.04 and CentOS 8.3. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
---
Check out the [NetBox Cloud Free Plan](https://netboxlabs.com/free-netbox-cloud/)! Skip the installation process and grab your own NetBox Cloud instance, preconfigured and ready to go in minutes. Completely free!
[:octicons-arrow-right-24: Sign Up](https://signup.netboxlabs.com/)
</div>
The installation instructions provided here have been tested to work on Ubuntu 22.04. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
<iframe width="560" height="315" src="https://www.youtube.com/embed/_y5JRiW_PLM" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
The following sections detail how to set up a new instance of NetBox:

View File

@ -122,21 +122,17 @@ sudo cp /opt/netbox-$OLDVER/gunicorn.py /opt/netbox/
### Option B: Check Out a Git Release
This guide assumes that NetBox is installed in `/opt/netbox`. First, determine the latest release either by visiting our [releases page](https://github.com/netbox-community/netbox/releases) or by running the following command:
This guide assumes that NetBox is installed at `/opt/netbox`. First, determine the latest release either by visiting our [releases page](https://github.com/netbox-community/netbox/releases) or by running the following `git` commands:
```
git ls-remote --tags https://github.com/netbox-community/netbox.git \
| grep -o 'refs/tags/v[0-9]*\.[0-9]*\.[0-9]*$' \
| tail -n 1 \
| sed 's|refs/tags/||'
sudo git fetch --tags
git describe --tags $(git rev-list --tags --max-count=1)
```
Check out the desired release by specifying its tag. For example:
Check out the desired release by specifying its tag:
```
cd /opt/netbox && \
sudo git fetch --tags && \
sudo git checkout v4.2.7
sudo git checkout v4.2.0
```
## 4. Run the Upgrade Script
@ -154,9 +150,6 @@ sudo ./upgrade.sh
sudo PYTHON=/usr/bin/python3.10 ./upgrade.sh
```
!!! note
To run the script on a node connected to a database in read-only mode, include the `--readonly` parameter. This will skip the application of any database migrations.
This script performs the following actions:
* Destroys and rebuilds the Python virtual environment

View File

@ -217,34 +217,26 @@ If we wanted to assign this IP address to a virtual machine interface instead, w
### Brief Format
Most API endpoints support an optional "brief" format, which returns only a minimal representation of each object in the response. This is useful when you need only a list of available objects without any related data, such as when populating a drop-down list in a form. As an example, the default (complete) format of a prefix looks like this:
Most API endpoints support an optional "brief" format, which returns only a minimal representation of each object in the response. This is useful when you need only a list of available objects without any related data, such as when populating a drop-down list in a form. As an example, the default (complete) format of an IP address looks like this:
```no-highlight
GET /api/ipam/prefixes/13980/
```
GET /api/ipam/prefixes/13980/
```json
{
"id": 13980,
"url": "http://netbox/api/ipam/prefixes/13980/",
"display_url": "http://netbox/api/ipam/prefixes/13980/",
"display": "192.0.2.0/24",
"family": {
"value": 4,
"label": "IPv4"
},
"prefix": "192.0.2.0/24",
"vrf": null,
"scope_type": "dcim.site",
"scope_id": 3,
"scope": {
"site": {
"id": 3,
"url": "http://netbox/api/dcim/sites/3/",
"display": "Site 23A",
"url": "http://netbox/api/dcim/sites/17/",
"name": "Site 23A",
"slug": "site-23a",
"description": ""
"slug": "site-23a"
},
"vrf": null,
"tenant": null,
"vlan": null,
"status": {
@ -258,36 +250,24 @@ GET /api/ipam/prefixes/13980/
"slug": "staging"
},
"is_pool": false,
"mark_utilized": false,
"description": "Example prefix",
"comments": "",
"tags": [],
"custom_fields": {},
"created": "2025-03-01T20:01:23.458302Z",
"last_updated": "2025-03-01T20:02:46.173540Z",
"children": 0,
"_depth": 0
"created": "2018-12-10",
"last_updated": "2019-03-01T20:02:46.173540Z"
}
```
The brief format is much more terse:
```no-highlight
GET /api/ipam/prefixes/13980/?brief=1
```
GET /api/ipam/prefixes/13980/?brief=1
```json
{
"id": 13980,
"url": "http://netbox/api/ipam/prefixes/13980/",
"display": "192.0.2.0/24",
"family": {
"value": 4,
"label": "IPv4"
},
"prefix": "192.0.2.0/24",
"description": "Example prefix",
"_depth": 0
"family": 4,
"prefix": "10.40.3.0/24"
}
```
@ -420,31 +400,25 @@ curl -s -X POST \
-H "Authorization: Token $TOKEN" \
-H "Content-Type: application/json" \
http://netbox/api/ipam/prefixes/ \
--data '{"prefix": "192.0.2.0/24", "scope_type": "dcim.site", "scope_id": 6}' | jq '.'
--data '{"prefix": "192.0.2.0/24", "site": 6}' | jq '.'
```
```json
{
"id": 18691,
"url": "http://netbox/api/ipam/prefixes/18691/",
"display_url": "http://netbox/api/ipam/prefixes/18691/",
"display": "192.0.2.0/24",
"family": {
"value": 4,
"label": "IPv4"
},
"prefix": "192.0.2.0/24",
"vrf": null,
"scope_type": "dcim.site",
"scope_id": 6,
"scope": {
"site": {
"id": 6,
"url": "http://netbox/api/dcim/sites/6/",
"display": "US-East 4",
"name": "US-East 4",
"slug": "us-east-4",
"description": ""
"slug": "us-east-4"
},
"vrf": null,
"tenant": null,
"vlan": null,
"status": {
@ -453,15 +427,11 @@ http://netbox/api/ipam/prefixes/ \
},
"role": null,
"is_pool": false,
"mark_utilized": false,
"description": "",
"comments": "",
"tags": [],
"custom_fields": {},
"created": "2025-04-29T15:44:47.597092Z",
"last_updated": "2025-04-29T15:44:47.597092Z",
"children": 0,
"_depth": 0
"created": "2020-08-04",
"last_updated": "2020-08-04T20:08:39.007125Z"
}
```
@ -520,24 +490,18 @@ http://netbox/api/ipam/prefixes/18691/ \
{
"id": 18691,
"url": "http://netbox/api/ipam/prefixes/18691/",
"display_url": "http://netbox/api/ipam/prefixes/18691/",
"display": "192.0.2.0/24",
"family": {
"value": 4,
"label": "IPv4"
},
"prefix": "192.0.2.0/24",
"vrf": null,
"scope_type": "dcim.site",
"scope_id": 6,
"scope": {
"site": {
"id": 6,
"url": "http://netbox/api/dcim/sites/6/",
"display": "US-East 4",
"name": "US-East 4",
"slug": "us-east-4",
"description": ""
"slug": "us-east-4"
},
"vrf": null,
"tenant": null,
"vlan": null,
"status": {
@ -546,15 +510,11 @@ http://netbox/api/ipam/prefixes/18691/ \
},
"role": null,
"is_pool": false,
"mark_utilized": false,
"description": "",
"comments": "",
"tags": [],
"custom_fields": {},
"created": "2025-04-29T15:44:47.597092Z",
"last_updated": "2025-04-29T15:49:40.689109Z",
"children": 0,
"_depth": 0
"created": "2020-08-04",
"last_updated": "2020-08-04T20:14:55.709430Z"
}
```
@ -608,23 +568,6 @@ http://netbox/api/dcim/sites/ \
!!! note
The bulk deletion of objects is an all-or-none operation, meaning that if NetBox fails to delete any of the specified objects (e.g. due a dependency by a related object), the entire operation will be aborted and none of the objects will be deleted.
## Uploading Files
As JSON does not support the inclusion of binary data, files cannot be uploaded using JSON-formatted API requests. Instead, we can use form data encoding to attach a local file.
For example, we can upload an image attachment using the `curl` command shown below. Note that the `@` signifies a local file on disk to be uploaded.
```no-highlight
curl -X POST \
-H "Authorization: Token $TOKEN" \
-H "Accept: application/json; indent=4" \
-F "object_type=dcim.site" \
-F "object_id=2" \
-F "name=attachment1.png" \
-F "image=@local_file.png" \
http://netbox/api/extras/image-attachments/
```
## Authentication
The NetBox REST API primarily employs token-based authentication. For convenience, cookie-based authentication can also be used when navigating the browsable API.
@ -710,7 +653,6 @@ Note that we are _not_ passing an existing REST API token with this request. If
{
"id": 6,
"url": "https://netbox/api/users/tokens/6/",
"display_url": "https://netbox/api/users/tokens/6/",
"display": "**********************************3c9cb9",
"user": {
"id": 2,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -15,6 +15,7 @@ A background job implements a basic [Job](../../models/core/job.md) executor for
```python title="jobs.py"
from netbox.jobs import JobRunner
class MyTestJob(JobRunner):
class Meta:
name = "My Test Job"
@ -24,8 +25,6 @@ class MyTestJob(JobRunner):
# your logic goes here
```
Completed jobs will have their status updated to "completed" by default, or "errored" if an unhandled exception was raised by the `run()` method. To intentionally mark a job as failed, raise the `core.exceptions.JobFailed` exception. (Note that "failed" differs from "errored" in that a failure may be expected under certain conditions, whereas an error is not.)
You can schedule the background job from within your code (e.g. from a model's `save()` method or a view) by calling `MyTestJob.enqueue()`. This method passes through all arguments to `Job.enqueue()`. However, no `name` argument must be passed, as the background job name will be used instead.
!!! tip

View File

@ -22,7 +22,7 @@ from netbox.plugins import PluginConfig
### ContentType renamed to ObjectType
NetBox's proxy model for Django's [ContentType model](https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/#the-contenttype-model) has been renamed to ObjectType for clarity. In general, plugins should use the ObjectType proxy when referencing content types, as it includes several custom manager methods. The one exception to this is when defining [generic foreign keys](https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/#generic-relations): The ForeignKey field used for a GFK should point to Django's native ContentType.
NetBox's proxy model for Django's [ContentType model](https://docs.djangoproject.com/en/5.0/ref/contrib/contenttypes/#the-contenttype-model) has been renamed to ObjectType for clarity. In general, plugins should use the ObjectType proxy when referencing content types, as it includes several custom manager methods. The one exception to this is when defining [generic foreign keys](https://docs.djangoproject.com/en/5.0/ref/contrib/contenttypes/#generic-relations): The ForeignKey field used for a GFK should point to Django's native ContentType.
Additionally, plugin maintainers are strongly encouraged to adopt the "object type" terminology for field and filter names wherever feasible to be consistent with NetBox core (however this is not required for compatibility).

View File

@ -1,6 +1,6 @@
# Views
## Writing Basic Views
## Writing Views
If your plugin will provide its own page or pages within the NetBox web UI, you'll need to define views. A view is a piece of business logic which performs an action and/or renders a page when a request is made to a particular URL. HTML content is rendered using a [template](./templates.md). Views are typically defined in `views.py`, and URL patterns in `urls.py`.
@ -47,13 +47,9 @@ A URL pattern has three components:
This makes our view accessible at the URL `/plugins/animal-sounds/random/`. (Remember, our `AnimalSoundsConfig` class sets our plugin's base URL to `animal-sounds`.) Viewing this URL should show the base NetBox template with our custom content inside it.
## NetBox Model Views
NetBox provides several generic view classes and additional helper functions, to simplify the implementation of plugin logic. These are recommended to be used whenever possible to keep the maintenance overhead of plugins low.
### View Classes
Generic view classes (documented below) facilitate common operations, such as creating, viewing, modifying, and deleting objects. Plugins can subclass these views for their own use.
NetBox provides several generic view classes (documented below) to facilitate common operations, such as creating, viewing, modifying, and deleting objects. Plugins can subclass these views for their own use.
| View Class | Description |
|----------------------|--------------------------------------------------------|
@ -69,51 +65,18 @@ Generic view classes (documented below) facilitate common operations, such as cr
!!! warning
Please note that only the classes which appear in this documentation are currently supported. Although other classes may be present within the `views.generic` module, they are not yet supported for use by plugins.
### URL registration
The NetBox URL registration process has two parts:
1. View classes can be decorated with `@register_model_view()`. This registers a new URL for the model.
2. All of a model's URLs can be included in `urls.py` using the `get_model_urls()` function. This call is usually required twice: once to import general views for the model and again to import model detail views tied to the object's primary key.
::: utilities.views.register_model_view
!!! note "Changed in NetBox v4.2"
In NetBox v4.2, the `register_model_view()` function was extended to support the registration of list views by passing `detail=False`.
::: utilities.urls.get_model_urls
!!! note "Changed in NetBox v4.2"
In NetBox v4.2, the `get_model_urls()` function was extended to support retrieving registered general model views (e.g. for listing objects) by passing `detail=False`.
### Example Usage
#### Example Usage
```python
# views.py
from netbox.views.generic import ObjectEditView
from utilities.views import register_model_view
from .models import Thing
@register_model_view(Thing, name='add', detail=False)
@register_model_view(Thing, name='edit')
class ThingEditView(ObjectEditView):
queryset = Thing.objects.all()
template_name = 'myplugin/thing.html'
...
```
```python
# urls.py
from django.urls import include, path
from utilities.urls import get_model_urls
urlpatterns = [
path('thing/', include(get_model_urls('myplugin', 'thing', detail=False))),
path('thing/<int:pk>/', include(get_model_urls('myplugin', 'thing'))),
...
]
```
## Object Views
Below are the class definitions for NetBox's object views. These views handle CRUD actions for individual objects. The view, add/edit, and delete views each inherit from `BaseObjectView`, which is not intended to be used directly.
@ -180,9 +143,6 @@ Below are the class definitions for NetBox's multi-object views. These views han
These views are provided to enable or enhance certain NetBox model features, such as change logging or journaling. These typically do not need to be subclassed: They can be used directly e.g. in a URL path.
!!! note
These feature views are automatically registered for all models that implement the respective feature. There is usually no need to override them. However, if that's the case, the URL must be registered manually in `urls.py` instead of using the `register_model_view()` function or decorator.
::: netbox.views.generic.ObjectChangeLogView
options:
members:
@ -197,7 +157,7 @@ These views are provided to enable or enhance certain NetBox model features, suc
### Additional Tabs
Plugins can "attach" a custom view to a NetBox model by registering it with `register_model_view()`. To include a tab for this view within the NetBox UI, declare a TabView instance named `tab`, and add it to the template context dict:
Plugins can "attach" a custom view to a core NetBox model by registering it with `register_model_view()`. To include a tab for this view within the NetBox UI, declare a TabView instance named `tab`, and add it to the template context dict:
```python
from dcim.models import Site
@ -225,6 +185,11 @@ class MyView(generic.ObjectView):
)
```
!!! note "Changed in NetBox v4.2"
The `register_model_view()` function was extended in NetBox v4.2 to support registration of list views by passing `detail=False`.
::: utilities.views.register_model_view
::: utilities.views.ViewTab
### Extra Template Content

View File

@ -86,69 +86,3 @@ netbox=> DELETE FROM django_migrations WHERE app='pluginname';
!!! warning
Exercise extreme caution when altering Django system tables. Users are strongly encouraged to perform a backup of their database immediately before taking these actions.
## Clean Up Content Types and Permissions
After removing a plugin and its database tables, you may find that object type references (`ContentTypes`) created by the plugin still appear in the permissions management section (e.g., when editing permissions in the NetBox UI).
This happens because the `django_content_type` table retains entries for the models that the plugin registered with Django.
!!! warning
Please use caution when removing `ContentTypes`. It is strongly recommended to **back up your database** before making these changes.
**Identify Stale Content Types:**
Open the Django shell to inspect lingering `ContentType` entries related to the removed plugin.
Typically, the Content Type's `app_label` matches the plugins name.
```no-highlight
$ cd /opt/netbox/
$ source /opt/netbox/venv/bin/activate
(venv) $ python3 netbox/manage.py nbshell
```
Then, in the shell:
```no-highlight
from django.contrib.contenttypes.models import ContentType
# Replace 'pluginname' with your plugin's actual name
stale_types = ContentType.objects.filter(app_label="pluginname")
for ct in stale_types:
print(ct)
### ^^^ These will be removed, make sure its ok
```
!!! warning
Review the output carefully and confirm that each listed Content Type is related to the plugin you removed.
**Remove Stale Content Types and Related Permissions:**
Next, check for any permissions associated with these Content Types:
```no-highlight
from django.contrib.auth.models import Permission
for ct in stale_types:
perms = Permission.objects.filter(content_type=ct)
print(list(perms))
```
If there are related Permissions, you can remove them safely:
```no-highlight
for ct in stale_types:
Permission.objects.filter(content_type=ct).delete()
```
After removing any related permissions, delete the Content Type entries:
```no-highlight
stale_types.delete()
```
**Restart NetBox:**
After making these changes, restart the NetBox service to ensure all changes are reflected.
```no-highlight
sudo systemctl restart netbox
```

View File

@ -1,72 +1,5 @@
# NetBox v4.2
## v4.2.9 (2025-04-30)
### Enhancements
* [#17151](https://github.com/netbox-community/netbox/issues/17151) - Display circuit type with background color in circuits list
* [#17319](https://github.com/netbox-community/netbox/issues/17319) - Improve layout of component template edit forms
* [#17405](https://github.com/netbox-community/netbox/issues/17405) - Display plugin icons in plugins list
* [#18215](https://github.com/netbox-community/netbox/issues/18215) - Link to script results list from script history
* [#18334](https://github.com/netbox-community/netbox/issues/18334) - Add region, site group, site, location, and rack filters for modules
* [#18982](https://github.com/netbox-community/netbox/issues/18982) - Reference rack as related object in changelog records for rack reservations
* [#18989](https://github.com/netbox-community/netbox/issues/18989) - List virtual circuits under provider view
* [#19110](https://github.com/netbox-community/netbox/issues/19110) - Enable filtering devices and virtual machines by primary IP address
* [#19358](https://github.com/netbox-community/netbox/issues/19358) - Move release info from footer to the navigation menu
### Bug Fixes
* [#15739](https://github.com/netbox-community/netbox/issues/15739) - Account for parallel cables when calculating total path length
* [#15971](https://github.com/netbox-community/netbox/issues/15971) - Preserve "none" selection in filter form fields
* [#16238](https://github.com/netbox-community/netbox/issues/16238) - Fix styling for white, gray, and black custom link buttons
* [#17613](https://github.com/netbox-community/netbox/issues/17613) - Fix layout of object view content on mobile
* [#17676](https://github.com/netbox-community/netbox/issues/17676) - Fix support for module bay creation when bulk importing module types
* [#18706](https://github.com/netbox-community/netbox/issues/18706) - Fix validation for VLANs assigned to both a group and a site
* [#18717](https://github.com/netbox-community/netbox/issues/18717) - Ensure change logs populated for many-to-one changes
* [#19117](https://github.com/netbox-community/netbox/issues/19117) - Avoid `AttributeError` exception when bulk import objects which have a multi-object custom field with a default value
* [#19204](https://github.com/netbox-community/netbox/issues/19204) - Improve JSON serialization support for data returned by a custom script
* [#19217](https://github.com/netbox-community/netbox/issues/19217) - Ensure static assets for the debug toolbar are installed even if `DEBUG` is false
* [#19228](https://github.com/netbox-community/netbox/issues/19228) - Fix ordering of custom scripts to avoid `NoReverseMatch` exception
* [#19229](https://github.com/netbox-community/netbox/issues/19229) - Fix `ValueError` exception when attempting to nullify interface mode when a VLAN is assigned
* [#19275](https://github.com/netbox-community/netbox/issues/19275) - `type` field should not be required when bulk editing interfaces
* [#19279](https://github.com/netbox-community/netbox/issues/19279) - `status` field should not be required when bulk editing inventory items
* [#19281](https://github.com/netbox-community/netbox/issues/19281) - Fix form validation failure when attempting to create a service from a service template
* [#19320](https://github.com/netbox-community/netbox/issues/19320) - Include Q-in-Q VLAN (if any) in VM interface details
* [#19322](https://github.com/netbox-community/netbox/issues/19322) - Correct URL paths for bulk import views
* [#19346](https://github.com/netbox-community/netbox/issues/19346) - Ensure all redirect URLs are validated before use
---
## v4.2.8 (2025-04-22)
### Enhancements
* [#17136](https://github.com/netbox-community/netbox/issues/17136) - Introduce the `--readonly` flag on upgrade script
* [#17908](https://github.com/netbox-community/netbox/issues/17908) - Add trace buttons to terminations under cable view
* [#18879](https://github.com/netbox-community/netbox/issues/18879) - Enable filtering prefixes by group of assigned VLAN
* [#18976](https://github.com/netbox-community/netbox/issues/18976) - Include FHRP group name on interface lists
* [#18978](https://github.com/netbox-community/netbox/issues/18978) - Add 802.1Q mode to interface filter form
* [#19038](https://github.com/netbox-community/netbox/issues/19038) - Show count of related VLAN groups under cluster view
* [#19040](https://github.com/netbox-community/netbox/issues/19040) - Add "copy to clipboard" button for rendered config
* [#19056](https://github.com/netbox-community/netbox/issues/19056) - Enable filtering devices by location slug
* [#19196](https://github.com/netbox-community/netbox/issues/19196) - Add filtering by VLAN translation policy to interface filter forms
### Bug Fixes
* [#18500](https://github.com/netbox-community/netbox/issues/18500) - `prepare_cloned_fields()` should validate cloning support on model
* [#18669](https://github.com/netbox-community/netbox/issues/18669) - Ensure default custom field values are respected when creating objects via the REST API
* [#18881](https://github.com/netbox-community/netbox/issues/18881) - Include missing related object counts under certain views
* [#18955](https://github.com/netbox-community/netbox/issues/18955) - Omit "clear" button on required choice fields
* [#18959](https://github.com/netbox-community/netbox/issues/18959) - Preserve ordering of terminations in cable traces
* [#18961](https://github.com/netbox-community/netbox/issues/18961) - Virtual chassis form should exclude members of other VCs when adding members
* [#19166](https://github.com/netbox-community/netbox/issues/19166) - Fix custom field choices bulk import support for `base_choices`
* [#19189](https://github.com/netbox-community/netbox/issues/19189) - The `load_yaml()` convenience method on BaseScript should use SafeLoader
* [#19195](https://github.com/netbox-community/netbox/issues/19195) - Language cookie should respect `SESSION_COOKIE_SECURE` value
* [#19230](https://github.com/netbox-community/netbox/issues/19230) - Allow label reuse when creating multiple components from a pattern
* [#19268](https://github.com/netbox-community/netbox/issues/19268) - Restore editing conflict protection for several object forms
---
## v4.2.7 (2025-04-10)
### Enhancements

View File

@ -1,112 +1,4 @@
# NetBox v4.3
## v4.3.4 (2025-07-15)
### Enhancements
* [#18811](https://github.com/netbox-community/netbox/issues/18811) - Match expanded form IPv6 addresses in global search
* [#19550](https://github.com/netbox-community/netbox/issues/19550) - Enable lazy loading for rack elevations
* [#19571](https://github.com/netbox-community/netbox/issues/19571) - Add a default module type profile for expansion cards
* [#19793](https://github.com/netbox-community/netbox/issues/19793) - Support custom dynamic navigation menu links
* [#19828](https://github.com/netbox-community/netbox/issues/19828) - Expose L2VPN termination in interface GraphQL response
### Bug Fixes
* [#19413](https://github.com/netbox-community/netbox/issues/19413) - Custom fields should be grouped in filter forms
* [#19633](https://github.com/netbox-community/netbox/issues/19633) - Introduce InvalidCondition exception and log all evaluations of invalid event rule conditions
* [#19800](https://github.com/netbox-community/netbox/issues/19800) - Module type bulk import should support profile assignment
* [#19806](https://github.com/netbox-community/netbox/issues/19806) - Introduce JobFailed exception to allow marking background jobs as failed
* [#19827](https://github.com/netbox-community/netbox/issues/19827) - Enforce uniqueness for device role names & slugs
* [#19839](https://github.com/netbox-community/netbox/issues/19839) - Enable export of parent assignment for recursively nested objects
* [#19876](https://github.com/netbox-community/netbox/issues/19876) - Remove Markdown rendering from CustomFieldChoiceSet description field
---
## v4.3.3 (2025-06-26)
### Enhancements
* [#17183](https://github.com/netbox-community/netbox/issues/17183) - Enable associating tags with object types during bulk import
* [#17719](https://github.com/netbox-community/netbox/issues/17719) - Introduce a user preference for table row striping
* [#19492](https://github.com/netbox-community/netbox/issues/19492) - Add a UI button to download the output of an executed custom script
* [#19499](https://github.com/netbox-community/netbox/issues/19499) - Support qualifying interfaces by parent device when bulk importing wireless links
### Bug Fixes
* [#19529](https://github.com/netbox-community/netbox/issues/19529) - Fix support for running custom scripts via the `runscript` management command
* [#19555](https://github.com/netbox-community/netbox/issues/19555) - Fix support for `schedule_at` when invoking a custom script via the REST API
* [#19617](https://github.com/netbox-community/netbox/issues/19617) - Ensure consistent styling of "connect" buttons in UI
* [#19640](https://github.com/netbox-community/netbox/issues/19640) - Restore ability to filter FHRP group assignments by device/VM in GraphQL API
* [#19644](https://github.com/netbox-community/netbox/issues/19644) - Atomic transactions should always employ database routing
* [#19659](https://github.com/netbox-community/netbox/issues/19659) - Populate initial device/VM selection for "add a service" button
* [#19665](https://github.com/netbox-community/netbox/issues/19665) - Correct field reference in wireless link model validation
* [#19667](https://github.com/netbox-community/netbox/issues/19667) - Fix `TypeError` exception when creating a new module profile type with no schema
* [#19673](https://github.com/netbox-community/netbox/issues/19673) - Ignore custom field references when compiling table prefetches
* [#19677](https://github.com/netbox-community/netbox/issues/19677) - Fix exception when passing null value to `present_in_vrf` filter
* [#19680](https://github.com/netbox-community/netbox/issues/19680) - Correct chronological ordering of change records resulting from device deletions
* [#19687](https://github.com/netbox-community/netbox/issues/19687) - Cellular interface types should be considered non-connectable
* [#19702](https://github.com/netbox-community/netbox/issues/19702) - Fix `DoesNotExist` exception when deleting a notification group with an associated event rule
* [#19745](https://github.com/netbox-community/netbox/issues/19745) - Fix bulk import of services with IP addresses assigned to FHRP groups
---
## v4.3.2 (2025-06-05)
### Enhancements
* [#19200](https://github.com/netbox-community/netbox/issues/19200) - Display assigned virtual chassis (if any) on device view
* [#19461](https://github.com/netbox-community/netbox/issues/19461) - Add color backgrounds for virtual circuit types
* [#19605](https://github.com/netbox-community/netbox/issues/19605) - Enable filtering IP addresses by family in GraphQL API
* [#19627](https://github.com/netbox-community/netbox/issues/19627) - Introduce object change migrators
### Bug Fixes
* [#19415](https://github.com/netbox-community/netbox/issues/19415) - Increase maximum supported distance for circuits and wireless links
* [#19475](https://github.com/netbox-community/netbox/issues/19475) - VLANs belonging to the same location as a VM's cluster should be eligible for assignment to interfaces on that VM
* [#19486](https://github.com/netbox-community/netbox/issues/19486) - Fix connection card rendering for console server ports
* [#19487](https://github.com/netbox-community/netbox/issues/19487) - Fix `FieldError` exception when ordering circuit or tunnel terminations by the terminating object
* [#19490](https://github.com/netbox-community/netbox/issues/19490) - Fix inclusion support for config templates populated via a data source
* [#19496](https://github.com/netbox-community/netbox/issues/19496) - Fix `AttributeError` exception when rendering a config template with no output
* [#19510](https://github.com/netbox-community/netbox/issues/19510) - Restore GraphQL API filtering for assigned IP addresses
* [#19520](https://github.com/netbox-community/netbox/issues/19520) - Restore ability to alter prefix scope via the REST API
* [#19587](https://github.com/netbox-community/netbox/issues/19587) - The `occupied` filter should include interfaces terminating a wireless link
* [#19599](https://github.com/netbox-community/netbox/issues/19599) - Fix `AttributeError` exception when sorting change history under user view
* [#19610](https://github.com/netbox-community/netbox/issues/19610) - Fix `FieldError` exception when sorting tunnel terminations by tenant
* [#19623](https://github.com/netbox-community/netbox/issues/19623) - Display description under provider account view
---
## v4.3.1 (2025-05-13)
### Enhancements
* [#17073](https://github.com/netbox-community/netbox/issues/17073) - Enable global search for tags
* [#18419](https://github.com/netbox-community/netbox/issues/18419) - Enable specifying a queue name when calling `Job.enqueue()`
* [#19416](https://github.com/netbox-community/netbox/issues/19416) - Add the 1000BASE-SX interface type
* [#19434](https://github.com/netbox-community/netbox/issues/19434) - Add pre-populated interface speed choices for 2.5 and 5 Gbps
### Bug Fixes
* [#17107](https://github.com/netbox-community/netbox/issues/17107) - Fix cosmetic issue in cable traces ending at a provider network
* [#19309](https://github.com/netbox-community/netbox/issues/19309) - Improve REST API query performance for prefixes and IP addresses
* [#19361](https://github.com/netbox-community/netbox/issues/19361) - Fix incorrect GraphQL object types
* [#19375](https://github.com/netbox-community/netbox/issues/19375) - Fix table configuration after applying a saved table config
* [#19376](https://github.com/netbox-community/netbox/issues/19376) - Fix `FieldDoesNotExist` exception when global search results include a contact
* [#19380](https://github.com/netbox-community/netbox/issues/19380) - Fix column selections for child object tables
* [#19381](https://github.com/netbox-community/netbox/issues/19381) - Fix syncing of custom scripts from a remote data source
* [#19396](https://github.com/netbox-community/netbox/issues/19396) - Enable nullifying VLAN `qinq_role` via the REST API
* [#19397](https://github.com/netbox-community/netbox/issues/19397) - Correct enum type for IPRangeFilter in GraphQL API
* [#19432](https://github.com/netbox-community/netbox/issues/19432) - Update minimum required PostgreSQL version referenced by server error page
* [#19440](https://github.com/netbox-community/netbox/issues/19440) - Ensure data migrations use the correct database connection
* [#19444](https://github.com/netbox-community/netbox/issues/19444) - Fix change logging for contact group assignments
* [#19463](https://github.com/netbox-community/netbox/issues/19463) - Hide button dropdown for tables which do not support saved configs
* [#19464](https://github.com/netbox-community/netbox/issues/19464) - Fix bulk editing of inventory items from device view
* [#19465](https://github.com/netbox-community/netbox/issues/19465) - Fix ability to clear assigned prefix scope in UI
* [#19472](https://github.com/netbox-community/netbox/issues/19472) - Fix device column rendering in virtual device contexts table
---
## v4.3.0 (2025-05-01)
## v4.3.0-beta1 (2025-04-14)
### Breaking Changes
@ -115,7 +7,6 @@
* 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.
* `django-storages` is now a required dependency. (It will be installed automatically on upgrade.)
* PluginTemplateExtension no longer supports registration via the singular `model` attribute (use `models` instead).
* The legacy staged changes functionality has been removed.
@ -167,7 +58,6 @@ User can now declare one or more proxy routers via the `PROXY_ROUTERS` configura
* [#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
* [#18896](https://github.com/netbox-community/netbox/issues/18896) - Enable the use of remote storage for custom scripts
### Plugins
@ -184,7 +74,7 @@ User can now declare one or more proxy routers via the `PROXY_ROUTERS` configura
* [#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.2
* [#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

View File

@ -49,7 +49,6 @@ markdown_extensions:
- admonition
- attr_list
- footnotes
- md_in_html
- pymdownx.emoji:
emoji_index: !!python/name:material.extensions.emoji.twemoji
emoji_generator: !!python/name:material.extensions.emoji.to_svg

View File

@ -12,7 +12,7 @@ from django.shortcuts import get_object_or_404, redirect
from django.shortcuts import render, resolve_url
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.http import urlencode
from django.utils.http import url_has_allowed_host_and_scheme, urlencode
from django.utils.translation import gettext_lazy as _
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import View
@ -28,8 +28,6 @@ from netbox.config import get_config
from netbox.views import generic
from users import forms, tables
from users.models import UserConfig
from utilities.request import safe_for_redirect
from utilities.string import remove_linebreaks
from utilities.views import register_model_view
@ -127,18 +125,12 @@ class LoginView(View):
# Set the user's preferred language (if any)
if language := request.user.config.get('locale.language'):
response.set_cookie(
key=settings.LANGUAGE_COOKIE_NAME,
value=language,
max_age=request.session.get_expiry_age(),
secure=settings.SESSION_COOKIE_SECURE,
)
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language, max_age=request.session.get_expiry_age())
return response
else:
username = form['username'].value()
logger.debug(f"Login form validation failed for username: {remove_linebreaks(username)}")
logger.debug(f"Login form validation failed for username: {form['username'].value()}")
return render(request, self.template_name, {
'form': form,
@ -149,11 +141,11 @@ class LoginView(View):
data = request.POST if request.method == "POST" else request.GET
redirect_url = data.get('next', settings.LOGIN_REDIRECT_URL)
if redirect_url and safe_for_redirect(redirect_url):
logger.debug(f"Redirecting user to {remove_linebreaks(redirect_url)}")
if redirect_url and url_has_allowed_host_and_scheme(redirect_url, allowed_hosts=None):
logger.debug(f"Redirecting user to {redirect_url}")
else:
if redirect_url:
logger.warning(f"Ignoring unsafe 'next' URL passed to login form: {remove_linebreaks(redirect_url)}")
logger.warning(f"Ignoring unsafe 'next' URL passed to login form: {redirect_url}")
redirect_url = reverse('home')
return HttpResponseRedirect(redirect_url)
@ -191,10 +183,12 @@ class ProfileView(LoginRequiredMixin, View):
def get(self, request):
# Compile changelog table
changelog = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(user=request.user)[:20]
changelog = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(
user=request.user
).prefetch_related(
'changed_object_type'
)[:20]
changelog_table = ObjectChangeTable(changelog)
changelog_table.orderable = False
changelog_table.configure(request)
return render(request, self.template_name, {
'changelog_table': changelog_table,
@ -226,12 +220,7 @@ class UserConfigView(LoginRequiredMixin, View):
# Set/clear language cookie
if language := form.cleaned_data['locale.language']:
response.set_cookie(
key=settings.LANGUAGE_COOKIE_NAME,
value=language,
max_age=request.session.get_expiry_age(),
secure=settings.SESSION_COOKIE_SECURE,
)
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language, max_age=request.session.get_expiry_age())
else:
response.delete_cookie(settings.LANGUAGE_COOKIE_NAME)

View File

@ -16,7 +16,6 @@ from utilities.forms import get_field_value
from utilities.forms.fields import (
CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField,
)
from utilities.forms.mixins import DistanceValidationMixin
from utilities.forms.rendering import FieldSet, InlineFields
from utilities.forms.widgets import DatePicker, HTMXSelect, NumberWithOptions
from utilities.templatetags.builtins.filters import bettertitle
@ -106,7 +105,7 @@ class CircuitTypeForm(NetBoxModelForm):
]
class CircuitForm(DistanceValidationMixin, TenancyForm, NetBoxModelForm):
class CircuitForm(TenancyForm, NetBoxModelForm):
provider = DynamicModelChoiceField(
label=_('Provider'),
queryset=Provider.objects.all(),

View File

@ -4,13 +4,17 @@ from circuits.choices import *
__all__ = (
'CircuitStatusEnum',
'CircuitCommitRateEnum',
'CircuitTerminationSideEnum',
'CircuitTerminationPortSpeedEnum',
'CircuitPriorityEnum',
'VirtualCircuitTerminationRoleEnum',
)
CircuitPriorityEnum = strawberry.enum(CircuitPriorityChoices.as_enum(prefix='priority'))
CircuitStatusEnum = strawberry.enum(CircuitStatusChoices.as_enum('status'))
CircuitTerminationSideEnum = strawberry.enum(CircuitTerminationSideChoices.as_enum(prefix='side'))
VirtualCircuitTerminationRoleEnum = strawberry.enum(VirtualCircuitTerminationRoleChoices.as_enum(prefix='role'))
CircuitCommitRateEnum = strawberry.enum(CircuitCommitRateChoices.as_enum())
CircuitPriorityEnum = strawberry.enum(CircuitPriorityChoices.as_enum())
CircuitStatusEnum = strawberry.enum(CircuitStatusChoices.as_enum())
CircuitTerminationSideEnum = strawberry.enum(CircuitTerminationSideChoices.as_enum())
CircuitTerminationPortSpeedEnum = strawberry.enum(CircuitTerminationPortSpeedChoices.as_enum())
VirtualCircuitTerminationRoleEnum = strawberry.enum(VirtualCircuitTerminationRoleChoices.as_enum())

View File

@ -21,7 +21,7 @@ from .filter_mixins import BaseCircuitTypeFilterMixin
if TYPE_CHECKING:
from core.graphql.filters import ContentTypeFilter
from dcim.graphql.filters import InterfaceFilter, LocationFilter, RegionFilter, SiteFilter, SiteGroupFilter
from dcim.graphql.filters import InterfaceFilter
from ipam.graphql.filters import ASNFilter
from netbox.graphql.filter_lookups import IntegerLookup
from .enums import *
@ -41,7 +41,7 @@ __all__ = (
)
@strawberry_django.filter_type(models.CircuitTermination, lookups=True)
@strawberry_django.filter(models.CircuitTermination, lookups=True)
class CircuitTerminationFilter(
BaseObjectTypeFilterMixin,
CustomFieldsFilterMixin,
@ -69,25 +69,8 @@ class CircuitTerminationFilter(
pp_info: FilterLookup[str] | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field()
# Cached relations
_provider_network: Annotated['ProviderNetworkFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field(name='provider_network')
)
_location: Annotated['LocationFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field(name='location')
)
_region: Annotated['RegionFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field(name='region')
)
_site_group: Annotated['SiteGroupFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field(name='site_group')
)
_site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field(name='site')
)
@strawberry_django.filter_type(models.Circuit, lookups=True)
@strawberry_django.filter(models.Circuit, lookups=True)
class CircuitFilter(
ContactFilterMixin,
ImageAttachmentFilterMixin,
@ -116,22 +99,19 @@ class CircuitFilter(
commit_rate: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
terminations: Annotated['CircuitTerminationFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter_type(models.CircuitType, lookups=True)
@strawberry_django.filter(models.CircuitType, lookups=True)
class CircuitTypeFilter(BaseCircuitTypeFilterMixin):
pass
@strawberry_django.filter_type(models.CircuitGroup, lookups=True)
@strawberry_django.filter(models.CircuitGroup, lookups=True)
class CircuitGroupFilter(TenancyFilterMixin, OrganizationalModelFilterMixin):
pass
@strawberry_django.filter_type(models.CircuitGroupAssignment, lookups=True)
@strawberry_django.filter(models.CircuitGroupAssignment, lookups=True)
class CircuitGroupAssignmentFilter(
BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin
):
@ -148,17 +128,14 @@ class CircuitGroupAssignmentFilter(
)
@strawberry_django.filter_type(models.Provider, lookups=True)
@strawberry_django.filter(models.Provider, lookups=True)
class ProviderFilter(ContactFilterMixin, PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field()
slug: FilterLookup[str] | None = strawberry_django.filter_field()
asns: Annotated['ASNFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
circuits: Annotated['CircuitFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter_type(models.ProviderAccount, lookups=True)
@strawberry_django.filter(models.ProviderAccount, lookups=True)
class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilterMixin):
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field()
@ -168,7 +145,7 @@ class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.ProviderNetwork, lookups=True)
@strawberry_django.filter(models.ProviderNetwork, lookups=True)
class ProviderNetworkFilter(PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field()
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
@ -178,12 +155,12 @@ class ProviderNetworkFilter(PrimaryModelFilterMixin):
service_id: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.VirtualCircuitType, lookups=True)
@strawberry_django.filter(models.VirtualCircuitType, lookups=True)
class VirtualCircuitTypeFilter(BaseCircuitTypeFilterMixin):
pass
@strawberry_django.filter_type(models.VirtualCircuit, lookups=True)
@strawberry_django.filter(models.VirtualCircuit, lookups=True)
class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
cid: FilterLookup[str] | None = strawberry_django.filter_field()
provider_network: Annotated['ProviderNetworkFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
@ -206,7 +183,7 @@ class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
)
@strawberry_django.filter_type(models.VirtualCircuitTermination, lookups=True)
@strawberry_django.filter(models.VirtualCircuitTermination, lookups=True)
class VirtualCircuitTerminationFilter(
BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin
):

View File

@ -8,11 +8,10 @@ def set_null_values(apps, schema_editor):
Circuit = apps.get_model('circuits', 'Circuit')
CircuitGroupAssignment = apps.get_model('circuits', 'CircuitGroupAssignment')
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
db_alias = schema_editor.connection.alias
Circuit.objects.using(db_alias).filter(distance_unit='').update(distance_unit=None)
CircuitGroupAssignment.objects.using(db_alias).filter(priority='').update(priority=None)
CircuitTermination.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
Circuit.objects.filter(distance_unit='').update(distance_unit=None)
CircuitGroupAssignment.objects.filter(priority='').update(priority=None)
CircuitTermination.objects.filter(cable_end='').update(cable_end=None)
class Migration(migrations.Migration):

View File

@ -1,5 +1,4 @@
import django.db.models.deletion
from django.contrib.contenttypes.models import ContentType
from django.db import migrations, models
@ -9,15 +8,14 @@ def copy_site_assignments(apps, schema_editor):
"""
ContentType = apps.get_model('contenttypes', 'ContentType')
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
ProviderNetwork = apps.get_model('circuits', 'ProviderNetwork')
Site = apps.get_model('dcim', 'Site')
db_alias = schema_editor.connection.alias
CircuitTermination.objects.using(db_alias).filter(site__isnull=False).update(
CircuitTermination.objects.filter(site__isnull=False).update(
termination_type=ContentType.objects.get_for_model(Site), termination_id=models.F('site_id')
)
CircuitTermination.objects.using(db_alias).filter(provider_network__isnull=False).update(
ProviderNetwork = apps.get_model('circuits', 'ProviderNetwork')
CircuitTermination.objects.filter(provider_network__isnull=False).update(
termination_type=ContentType.objects.get_for_model(ProviderNetwork),
termination_id=models.F('provider_network_id'),
)
@ -50,26 +48,3 @@ class Migration(migrations.Migration):
# Copy over existing site assignments
migrations.RunPython(code=copy_site_assignments, reverse_code=migrations.RunPython.noop),
]
def oc_circuittermination_termination(objectchange, reverting):
site_ct = ContentType.objects.get_by_natural_key('dcim', 'site').pk
provider_network_ct = ContentType.objects.get_by_natural_key('circuits', 'providernetwork').pk
for data in (objectchange.prechange_data, objectchange.postchange_data):
if data is None:
continue
if site_id := data.get('site'):
data.update({
'termination_type': site_ct,
'termination_id': site_id,
})
elif provider_network_id := data.get('provider_network'):
data.update({
'termination_type': provider_network_ct,
'termination_id': provider_network_id,
})
objectchange_migrators = {
'circuits.circuittermination': oc_circuittermination_termination,
}

View File

@ -7,20 +7,15 @@ def populate_denormalized_fields(apps, schema_editor):
Copy site ForeignKey values to the Termination GFK.
"""
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
db_alias = schema_editor.connection.alias
terminations = CircuitTermination.objects.using(db_alias).filter(site__isnull=False).prefetch_related('site')
terminations = CircuitTermination.objects.filter(site__isnull=False).prefetch_related('site')
for termination in terminations:
termination._region_id = termination.site.region_id
termination._site_group_id = termination.site.group_id
termination._site_id = termination.site_id
# Note: Location cannot be set prior to migration
CircuitTermination.objects.using(db_alias).bulk_update(
terminations,
['_region', '_site_group', '_site'],
batch_size=100
)
CircuitTermination.objects.bulk_update(terminations, ['_region', '_site_group', '_site'], batch_size=100)
class Migration(migrations.Migration):
@ -86,15 +81,3 @@ class Migration(migrations.Migration):
new_name='_provider_network',
),
]
def oc_circuittermination_remove_fields(objectchange, reverting):
for data in (objectchange.prechange_data, objectchange.postchange_data):
if data is not None:
data.pop('site', None)
data.pop('provider_network', None)
objectchange_migrators = {
'circuits.circuittermination': oc_circuittermination_remove_fields,
}

View File

@ -1,5 +1,4 @@
import django.db.models.deletion
from django.contrib.contenttypes.models import ContentType
from django.db import migrations, models
@ -10,9 +9,8 @@ def set_member_type(apps, schema_editor):
ContentType = apps.get_model('contenttypes', 'ContentType')
Circuit = apps.get_model('circuits', 'Circuit')
CircuitGroupAssignment = apps.get_model('circuits', 'CircuitGroupAssignment')
db_alias = schema_editor.connection.alias
CircuitGroupAssignment.objects.using(db_alias).update(
CircuitGroupAssignment.objects.update(
member_type=ContentType.objects.get_for_model(Circuit)
)
@ -83,21 +81,3 @@ class Migration(migrations.Migration):
),
),
]
def oc_circuitgroupassignment_member(objectchange, reverting):
circuit_ct = ContentType.objects.get_by_natural_key('circuits', 'circuit').pk
for data in (objectchange.prechange_data, objectchange.postchange_data):
if data is None:
continue
if circuit_id := data.get('circuit'):
data.update({
'member_type': circuit_ct,
'member_id': circuit_id,
})
data.pop('circuit', None)
objectchange_migrators = {
'circuits.circuitgroupassignment': oc_circuitgroupassignment_member,
}

View File

@ -1,16 +0,0 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0051_virtualcircuit_group_assignment'),
]
operations = [
migrations.AlterField(
model_name='circuit',
name='_abs_distance',
field=models.DecimalField(blank=True, decimal_places=4, max_digits=13, null=True),
),
]

View File

@ -61,8 +61,9 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
linkify=True,
verbose_name=_('Account')
)
type = columns.ColoredLabelColumn(
type = tables.Column(
verbose_name=_('Type'),
linkify=True
)
status = columns.ChoiceFieldColumn()
termination_a = columns.TemplateColumn(
@ -120,8 +121,7 @@ class CircuitTerminationTable(NetBoxTable):
)
termination = tables.Column(
verbose_name=_('Termination Point'),
linkify=True,
orderable=False,
linkify=True
)
# Termination types
@ -133,7 +133,7 @@ class CircuitTerminationTable(NetBoxTable):
site_group = tables.Column(
verbose_name=_('Site Group'),
linkify=True,
accessor='_site_group'
accessor='_sitegroup'
)
region = tables.Column(
verbose_name=_('Region'),

View File

@ -54,8 +54,9 @@ class VirtualCircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable)
linkify=True,
verbose_name=_('Account')
)
type = columns.ColoredLabelColumn(
type = tables.Column(
verbose_name=_('Type'),
linkify=True
)
status = columns.ChoiceFieldColumn()
termination_count = columns.LinkedCountColumn(

View File

@ -1,23 +0,0 @@
from django.test import RequestFactory, tag, TestCase
from circuits.models import CircuitTermination
from circuits.tables import CircuitTerminationTable
@tag('regression')
class CircuitTerminationTableTest(TestCase):
def test_every_orderable_field_does_not_throw_exception(self):
terminations = CircuitTermination.objects.all()
disallowed = {'actions', }
orderable_columns = [
column.name for column in CircuitTerminationTable(terminations).columns
if column.orderable and column.name not in disallowed
]
fake_request = RequestFactory().get("/")
for col in orderable_columns:
for dir in ('-', ''):
table = CircuitTerminationTable(terminations)
table.order_by = f'{dir}{col}'
table.as_html(fake_request)

View File

@ -1,5 +1,5 @@
from django.contrib import messages
from django.db import router, transaction
from django.db import transaction
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.translation import gettext_lazy as _
@ -35,19 +35,7 @@ class ProviderView(GetRelatedModelsMixin, generic.ObjectView):
def get_extra_context(self, request, instance):
return {
'related_models': self.get_related_models(
request,
instance,
omit=(),
extra=(
(
VirtualCircuit.objects.restrict(request.user, 'view').filter(
provider_network__provider=instance
),
'provider_id',
),
),
),
'related_models': self.get_related_models(request, instance),
}
@ -63,7 +51,7 @@ class ProviderDeleteView(generic.ObjectDeleteView):
queryset = Provider.objects.all()
@register_model_view(Provider, 'bulk_import', path='import', detail=False)
@register_model_view(Provider, 'bulk_import', detail=False)
class ProviderBulkImportView(generic.BulkImportView):
queryset = Provider.objects.all()
model_form = forms.ProviderImportForm
@ -124,7 +112,7 @@ class ProviderAccountDeleteView(generic.ObjectDeleteView):
queryset = ProviderAccount.objects.all()
@register_model_view(ProviderAccount, 'bulk_import', path='import', detail=False)
@register_model_view(ProviderAccount, 'bulk_import', detail=False)
class ProviderAccountBulkImportView(generic.BulkImportView):
queryset = ProviderAccount.objects.all()
model_form = forms.ProviderAccountImportForm
@ -171,16 +159,11 @@ class ProviderNetworkView(GetRelatedModelsMixin, generic.ObjectView):
'related_models': self.get_related_models(
request,
instance,
omit=(CircuitTermination,),
extra=(
(
Circuit.objects.restrict(request.user, 'view').filter(terminations___provider_network=instance),
'provider_network_id',
),
(
CircuitTermination.objects.restrict(request.user, 'view').filter(_provider_network=instance),
'provider_network_id',
),
),
),
}
@ -198,7 +181,7 @@ class ProviderNetworkDeleteView(generic.ObjectDeleteView):
queryset = ProviderNetwork.objects.all()
@register_model_view(ProviderNetwork, 'bulk_import', path='import', detail=False)
@register_model_view(ProviderNetwork, 'bulk_import', detail=False)
class ProviderNetworkBulkImportView(generic.BulkImportView):
queryset = ProviderNetwork.objects.all()
model_form = forms.ProviderNetworkImportForm
@ -255,7 +238,7 @@ class CircuitTypeDeleteView(generic.ObjectDeleteView):
queryset = CircuitType.objects.all()
@register_model_view(CircuitType, 'bulk_import', path='import', detail=False)
@register_model_view(CircuitType, 'bulk_import', detail=False)
class CircuitTypeBulkImportView(generic.BulkImportView):
queryset = CircuitType.objects.all()
model_form = forms.CircuitTypeImportForm
@ -311,7 +294,7 @@ class CircuitDeleteView(generic.ObjectDeleteView):
queryset = Circuit.objects.all()
@register_model_view(Circuit, 'bulk_import', path='import', detail=False)
@register_model_view(Circuit, 'bulk_import', detail=False)
class CircuitBulkImportView(generic.BulkImportView):
queryset = Circuit.objects.all()
model_form = forms.CircuitImportForm
@ -384,7 +367,7 @@ class CircuitSwapTerminations(generic.ObjectEditView):
if termination_a and termination_z:
# Use a placeholder to avoid an IntegrityError on the (circuit, term_side) unique constraint
with transaction.atomic(using=router.db_for_write(CircuitTermination)):
with transaction.atomic():
termination_a.term_side = '_'
termination_a.save()
termination_z.term_side = 'A'
@ -451,7 +434,7 @@ class CircuitTerminationDeleteView(generic.ObjectDeleteView):
queryset = CircuitTermination.objects.all()
@register_model_view(CircuitTermination, 'bulk_import', path='import', detail=False)
@register_model_view(CircuitTermination, 'bulk_import', detail=False)
class CircuitTerminationBulkImportView(generic.BulkImportView):
queryset = CircuitTermination.objects.all()
model_form = forms.CircuitTerminationImportForm
@ -512,7 +495,7 @@ class CircuitGroupDeleteView(generic.ObjectDeleteView):
queryset = CircuitGroup.objects.all()
@register_model_view(CircuitGroup, 'bulk_import', path='import', detail=False)
@register_model_view(CircuitGroup, 'bulk_import', detail=False)
class CircuitGroupBulkImportView(generic.BulkImportView):
queryset = CircuitGroup.objects.all()
model_form = forms.CircuitGroupImportForm
@ -562,7 +545,7 @@ class CircuitGroupAssignmentDeleteView(generic.ObjectDeleteView):
queryset = CircuitGroupAssignment.objects.all()
@register_model_view(CircuitGroupAssignment, 'bulk_import', path='import', detail=False)
@register_model_view(CircuitGroupAssignment, 'bulk_import', detail=False)
class CircuitGroupAssignmentBulkImportView(generic.BulkImportView):
queryset = CircuitGroupAssignment.objects.all()
model_form = forms.CircuitGroupAssignmentImportForm
@ -619,7 +602,7 @@ class VirtualCircuitTypeDeleteView(generic.ObjectDeleteView):
queryset = VirtualCircuitType.objects.all()
@register_model_view(VirtualCircuitType, 'bulk_import', path='import', detail=False)
@register_model_view(VirtualCircuitType, 'bulk_import', detail=False)
class VirtualCircuitTypeBulkImportView(generic.BulkImportView):
queryset = VirtualCircuitType.objects.all()
model_form = forms.VirtualCircuitTypeImportForm

View File

@ -1,19 +1,9 @@
from django.core.exceptions import ImproperlyConfigured
__all__ = (
'IncompatiblePluginError',
'JobFailed',
'SyncError',
)
class IncompatiblePluginError(ImproperlyConfigured):
pass
class JobFailed(Exception):
pass
class SyncError(Exception):
pass
class IncompatiblePluginError(ImproperlyConfigured):
pass

View File

@ -23,7 +23,7 @@ __all__ = (
)
@strawberry_django.filter_type(models.DataFile, lookups=True)
@strawberry_django.filter(models.DataFile, lookups=True)
class DataFileFilter(BaseFilterMixin):
id: ID | None = strawberry_django.filter_field()
created: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()
@ -39,7 +39,7 @@ class DataFileFilter(BaseFilterMixin):
hash: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.DataSource, lookups=True)
@strawberry_django.filter(models.DataSource, lookups=True)
class DataSourceFilter(PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field()
type: FilterLookup[str] | None = strawberry_django.filter_field()
@ -56,7 +56,7 @@ class DataSourceFilter(PrimaryModelFilterMixin):
)
@strawberry_django.filter_type(models.ObjectChange, lookups=True)
@strawberry_django.filter(models.ObjectChange, lookups=True)
class ObjectChangeFilter(BaseFilterMixin):
id: ID | None = strawberry_django.filter_field()
time: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()
@ -82,7 +82,7 @@ class ObjectChangeFilter(BaseFilterMixin):
)
@strawberry_django.filter_type(DjangoContentType, lookups=True)
@strawberry_django.filter(DjangoContentType, lookups=True)
class ContentTypeFilter(BaseFilterMixin):
id: ID | None = strawberry_django.filter_field()
app_label: FilterLookup[str] | None = strawberry_django.filter_field()

View File

@ -1,10 +1,12 @@
# Generated by Django 5.1.6 on 2025-02-26 19:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0013_job_data_encoder'),
('core', '0012_job_object_type_optional'),
]
operations = [

View File

@ -1,17 +0,0 @@
import django.core.serializers.json
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0012_job_object_type_optional'),
]
operations = [
migrations.AlterField(
model_name='job',
name='data',
field=models.JSONField(blank=True, encoder=django.core.serializers.json.DjangoJSONEncoder, null=True),
),
]

View File

@ -1,10 +1,12 @@
# Generated by Django 5.2b1 on 2025-04-03 18:32
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0014_datasource_sync_interval'),
('core', '0013_datasource_sync_interval'),
]
operations = [

View File

@ -88,11 +88,19 @@ class ManagedFile(SyncedDataMixin, models.Model):
def sync_data(self):
if self.data_file:
self.file_path = os.path.basename(self.data_path)
self._write_to_disk(self.full_path, overwrite=True)
def _write_to_disk(self, path, overwrite=False):
"""
Write the object's data to disk at the specified path
"""
# Check whether file already exists
storage = self.storage
if storage.exists(path) and not overwrite:
raise FileExistsError()
with storage.open(self.full_path, 'wb+') as new_file:
new_file.write(self.data_file.data)
with storage.open(path, 'wb+') as new_file:
new_file.write(self.data)
@cached_property
def storage(self):

View File

@ -5,7 +5,6 @@ import django_rq
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.core.exceptions import ValidationError
from django.core.serializers.json import DjangoJSONEncoder
from django.core.validators import MinValueValidator
from django.db import models, transaction
from django.urls import reverse
@ -91,9 +90,8 @@ class Job(models.Model):
)
data = models.JSONField(
verbose_name=_('data'),
encoder=DjangoJSONEncoder,
null=True,
blank=True,
blank=True
)
error = models.TextField(
verbose_name=_('error'),
@ -187,14 +185,15 @@ class Job(models.Model):
"""
Mark the job as completed, optionally specifying a particular termination status.
"""
if status not in JobStatusChoices.TERMINAL_STATE_CHOICES:
valid_statuses = JobStatusChoices.TERMINAL_STATE_CHOICES
if status not in valid_statuses:
raise ValueError(
_("Invalid status for job termination. Choices are: {choices}").format(
choices=', '.join(JobStatusChoices.TERMINAL_STATE_CHOICES)
choices=', '.join(valid_statuses)
)
)
# Set the job's status and completion time
# Mark the job as completed
self.status = status
if error:
self.error = error
@ -214,7 +213,6 @@ class Job(models.Model):
schedule_at=None,
interval=None,
immediate=False,
queue_name=None,
**kwargs
):
"""
@ -238,7 +236,7 @@ class Job(models.Model):
object_id = instance.pk
else:
object_type = object_id = None
rq_queue_name = queue_name if queue_name else get_queue_for_model(object_type.model if object_type else None)
rq_queue_name = get_queue_for_model(object_type.model if object_type else None)
queue = django_rq.get_queue(rq_queue_name)
status = JobStatusChoices.STATUS_SCHEDULED if schedule_at else JobStatusChoices.STATUS_PENDING
job = Job(

View File

@ -49,7 +49,6 @@ class Plugin:
The representation of a NetBox plugin in the catalog API.
"""
id: str = ''
icon_url: str = ''
status: str = ''
title_short: str = ''
title_long: str = ''
@ -211,7 +210,6 @@ def get_catalog_plugins():
# Populate plugin data
plugins[data['config_name']] = Plugin(
id=data['id'],
icon_url=data['icon'],
status=data['status'],
title_short=data['title_short'],
title_long=data['title_long'],

View File

@ -2,7 +2,7 @@ import logging
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db.models.fields.reverse_related import ManyToManyRel, ManyToOneRel
from django.db.models.fields.reverse_related import ManyToManyRel
from django.db.models.signals import m2m_changed, post_save, pre_delete
from django.dispatch import receiver, Signal
from django.utils.translation import gettext_lazy as _
@ -145,10 +145,8 @@ def handle_deleted_object(sender, instance, **kwargs):
# instance being deleted, and explicitly call .remove() on the remote M2M field to delete
# the association. This triggers an m2m_changed signal with the `post_remove` action type
# for the forward direction of the relationship, ensuring that the change is recorded.
# Similarly, for many-to-one relationships, we set the value on the related object to None
# and save it to trigger a change record on that object.
for relation in instance._meta.related_objects:
if type(relation) not in [ManyToManyRel, ManyToOneRel]:
if type(relation) is not ManyToManyRel:
continue
related_model = relation.related_model
related_field_name = relation.remote_field.name
@ -158,17 +156,7 @@ def handle_deleted_object(sender, instance, **kwargs):
continue
for obj in related_model.objects.filter(**{related_field_name: instance.pk}):
obj.snapshot() # Ensure the change record includes the "before" state
if type(relation) is ManyToManyRel:
getattr(obj, related_field_name).remove(instance)
elif type(relation) is ManyToOneRel and relation.field.null is True:
setattr(obj, related_field_name, None)
# make sure the object hasn't been deleted - in case of
# deletion chaining of related objects
try:
obj.refresh_from_db()
except DoesNotExist:
continue
obj.save()
# Enqueue the object for event processing
queue = events_queue.get()

View File

@ -12,12 +12,6 @@ __all__ = (
)
PLUGIN_NAME_TEMPLATE = """
<img class="plugin-icon" src="{{ record.icon_url }}">
<a href="{% url 'core:plugin' record.config_name %}">{{ record.title_long }}</a>
"""
class PluginVersionTable(BaseTable):
version = tables.Column(
verbose_name=_('Version')
@ -48,9 +42,8 @@ class PluginVersionTable(BaseTable):
class CatalogPluginTable(BaseTable):
title_long = columns.TemplateColumn(
template_code=PLUGIN_NAME_TEMPLATE,
verbose_name=_('Name')
title_long = tables.Column(
verbose_name=_('Name'),
)
author = tables.Column(
accessor=tables.A('author__name'),

View File

@ -9,7 +9,6 @@ from rq.registry import FailedJobRegistry, StartedJobRegistry
from users.models import Token, User
from utilities.testing import APITestCase, APIViewTestCases, TestCase
from utilities.testing.utils import disable_logging
from ..models import *
@ -190,7 +189,6 @@ class BackgroundTaskTestCase(TestCase):
# Enqueue & run a job that will fail
job = queue.enqueue(self.dummy_job_failing)
worker = get_worker('default')
with disable_logging():
worker.work(burst=True)
self.assertTrue(job.is_failed)
@ -233,7 +231,6 @@ class BackgroundTaskTestCase(TestCase):
self.assertEqual(job.get_status(), JobStatus.STARTED)
response = self.client.post(reverse('core-api:rqtask-stop', args=[job.id]), **self.header)
self.assertEqual(response.status_code, 200)
with disable_logging():
worker.monitor_work_horse(job, queue) # Sets the job as Failed and removes from Started
started_job_registry = StartedJobRegistry(queue.name, connection=queue.connection)
self.assertEqual(len(started_job_registry), 0)

View File

@ -6,13 +6,12 @@ from rest_framework import status
from core.choices import ObjectChangeActionChoices
from core.models import ObjectChange, ObjectType
from dcim.choices import SiteStatusChoices
from dcim.models import Site, CableTermination, Device, DeviceType, DeviceRole, Interface, Cable
from dcim.models import Site
from extras.choices import *
from extras.models import CustomField, CustomFieldChoiceSet, Tag
from utilities.testing import APITestCase
from utilities.testing.utils import create_tags, post_data
from utilities.testing.views import ModelViewTestCase
from dcim.models import Manufacturer
class ChangeLogViewTest(ModelViewTestCase):
@ -271,81 +270,6 @@ class ChangeLogViewTest(ModelViewTestCase):
# Check that no ObjectChange records have been created
self.assertEqual(ObjectChange.objects.count(), 0)
def test_ordering_genericrelation(self):
# Create required objects first
manufacturer = Manufacturer.objects.create(name='Manufacturer 1')
device_type = DeviceType.objects.create(
manufacturer=manufacturer,
model='Model 1',
slug='model-1'
)
device_role = DeviceRole.objects.create(
name='Role 1',
slug='role-1'
)
site = Site.objects.create(
name='Site 1',
slug='site-1'
)
# Create two devices
device1 = Device.objects.create(
name='Device 1',
device_type=device_type,
role=device_role,
site=site
)
device2 = Device.objects.create(
name='Device 2',
device_type=device_type,
role=device_role,
site=site
)
# Create interfaces on both devices
interface1 = Interface.objects.create(
device=device1,
name='eth0',
type='1000base-t'
)
interface2 = Interface.objects.create(
device=device2,
name='eth0',
type='1000base-t'
)
# Create a cable between the interfaces
_ = Cable.objects.create(
a_terminations=[interface1],
b_terminations=[interface2],
status='connected'
)
# Delete device1
request = {
'path': reverse('dcim:device_delete', kwargs={'pk': device1.pk}),
'data': post_data({'confirm': True}),
}
self.add_permissions(
'dcim.delete_device',
'dcim.delete_interface',
'dcim.delete_cable',
'dcim.delete_cabletermination'
)
response = self.client.post(**request)
self.assertHttpStatus(response, 302)
# Get the ObjectChange records for delete actions ordered by time
changes = ObjectChange.objects.filter(
action=ObjectChangeActionChoices.ACTION_DELETE
).order_by('time')[:3]
# Verify the order of deletion
self.assertEqual(len(changes), 3)
self.assertEqual(changes[0].changed_object_type, ContentType.objects.get_for_model(CableTermination))
self.assertEqual(changes[1].changed_object_type, ContentType.objects.get_for_model(Interface))
self.assertEqual(changes[2].changed_object_type, ContentType.objects.get_for_model(Device))
class ChangeLogAPITest(APITestCase):

View File

@ -14,7 +14,7 @@ from core.choices import ObjectChangeActionChoices
from core.models import *
from dcim.models import Site
from users.models import User
from utilities.testing import TestCase, ViewTestCases, create_tags, disable_logging
from utilities.testing import TestCase, ViewTestCases, create_tags
class DataSourceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
@ -271,7 +271,6 @@ class BackgroundTaskTestCase(TestCase):
# Enqueue & run a job that will fail
job = queue.enqueue(self.dummy_job_failing)
worker = get_worker('default')
with disable_logging():
worker.work(burst=True)
self.assertTrue(job.is_failed)
@ -318,7 +317,6 @@ class BackgroundTaskTestCase(TestCase):
self.assertEqual(len(started_job_registry), 1)
response = self.client.get(reverse('core:background_task_stop', args=[job.id]))
self.assertEqual(response.status_code, 302)
with disable_logging():
worker.monitor_work_horse(job, queue) # Sets the job as Failed and removes from Started
self.assertEqual(len(started_job_registry), 0)

View File

@ -103,7 +103,7 @@ class DataSourceDeleteView(generic.ObjectDeleteView):
queryset = DataSource.objects.all()
@register_model_view(DataSource, 'bulk_import', path='import', detail=False)
@register_model_view(DataSource, 'bulk_import', detail=False)
class DataSourceBulkImportView(generic.BulkImportView):
queryset = DataSource.objects.all()
model_form = forms.DataSourceImportForm
@ -223,7 +223,6 @@ class ObjectChangeView(generic.ObjectView):
data=related_changes[:50],
orderable=False
)
related_changes_table.configure(request)
objectchanges = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(
changed_object_type=instance.changed_object_type,

View File

@ -461,7 +461,6 @@ class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
Interface.objects.select_related("device", "cable"),
],
),
'virtual_circuit_termination',
'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

@ -874,7 +874,6 @@ class InterfaceTypeChoices(ChoiceSet):
TYPE_100ME_T1 = '100base-t1'
TYPE_100ME_SFP = '100base-x-sfp'
TYPE_1GE_FIXED = '1000base-t'
TYPE_1GE_SX_FIXED = '1000base-sx'
TYPE_1GE_LX_FIXED = '1000base-lx'
TYPE_1GE_TX_FIXED = '1000base-tx'
TYPE_1GE_GBIC = '1000base-x-gbic'
@ -1039,7 +1038,6 @@ class InterfaceTypeChoices(ChoiceSet):
(TYPE_100ME_FIXED, '100BASE-TX (10/100ME)'),
(TYPE_100ME_T1, '100BASE-T1 (10/100ME Single Pair)'),
(TYPE_1GE_FIXED, '1000BASE-T (1GE)'),
(TYPE_1GE_SX_FIXED, '1000BASE-SX (1GE)'),
(TYPE_1GE_LX_FIXED, '1000BASE-LX (1GE)'),
(TYPE_1GE_TX_FIXED, '1000BASE-TX (1GE)'),
(TYPE_2GE_FIXED, '2.5GBASE-T (2.5GE)'),
@ -1240,8 +1238,6 @@ class InterfaceSpeedChoices(ChoiceSet):
(10000, '10 Mbps'),
(100000, '100 Mbps'),
(1000000, '1 Gbps'),
(2500000, '2.5 Gbps'),
(5000000, '5 Gbps'),
(10000000, '10 Gbps'),
(25000000, '25 Gbps'),
(40000000, '40 Gbps'),

View File

@ -53,11 +53,6 @@ WIRELESS_IFACE_TYPES = [
InterfaceTypeChoices.TYPE_802151,
InterfaceTypeChoices.TYPE_802154,
InterfaceTypeChoices.TYPE_OTHER_WIRELESS,
InterfaceTypeChoices.TYPE_GSM,
InterfaceTypeChoices.TYPE_CDMA,
InterfaceTypeChoices.TYPE_LTE,
InterfaceTypeChoices.TYPE_4G,
InterfaceTypeChoices.TYPE_5G,
]
NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES

View File

@ -1110,13 +1110,6 @@ class DeviceFilterSet(
lookup_expr='in',
label=_('Location (ID)'),
)
location = TreeNodeMultipleChoiceFilter(
queryset=Location.objects.all(),
field_name='location',
lookup_expr='in',
to_field_name='slug',
label=_('Location (slug)'),
)
rack_id = django_filters.ModelMultipleChoiceFilter(
field_name='rack',
queryset=Rack.objects.all(),
@ -1390,75 +1383,10 @@ class ModuleFilterSet(NetBoxModelFilterSet):
lookup_expr='in',
label=_('Module bay (ID)'),
)
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='device__site__region',
lookup_expr='in',
label=_('Region (ID)'),
)
region = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='device__site__region',
lookup_expr='in',
to_field_name='slug',
label=_('Region (slug)'),
)
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='device__site__group',
lookup_expr='in',
label=_('Site group (ID)'),
)
site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='device__site__group',
lookup_expr='in',
to_field_name='slug',
label=_('Site group (slug)'),
)
site_id = django_filters.ModelMultipleChoiceFilter(
field_name='device__site',
queryset=Site.objects.all(),
label=_('Site (ID)'),
)
site = django_filters.ModelMultipleChoiceFilter(
field_name='device__site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label=_('Site name (slug)'),
)
location_id = django_filters.ModelMultipleChoiceFilter(
field_name='device__location',
queryset=Location.objects.all(),
label=_('Location (ID)'),
)
location = django_filters.ModelMultipleChoiceFilter(
field_name='device__location__slug',
queryset=Location.objects.all(),
to_field_name='slug',
label=_('Location (slug)'),
)
rack_id = django_filters.ModelMultipleChoiceFilter(
field_name='device__rack',
queryset=Rack.objects.all(),
label=_('Rack (ID)'),
)
rack = django_filters.ModelMultipleChoiceFilter(
field_name='device__rack__name',
queryset=Rack.objects.all(),
to_field_name='name',
label=_('Rack (name)'),
)
device_id = django_filters.ModelMultipleChoiceFilter(
queryset=Device.objects.all(),
label=_('Device (ID)'),
)
device = django_filters.ModelMultipleChoiceFilter(
field_name='device__name',
queryset=Device.objects.all(),
to_field_name='name',
label=_('Device (name)'),
)
status = django_filters.MultipleChoiceFilter(
choices=ModuleStatusChoices,
null_value=None
@ -1811,10 +1739,6 @@ class MACAddressFilterSet(NetBoxModelFilterSet):
class CommonInterfaceFilterSet(django_filters.FilterSet):
mode = django_filters.MultipleChoiceFilter(
choices=InterfaceModeChoices,
label=_('802.1Q Mode')
)
vlan_id = django_filters.CharFilter(
method='filter_vlan_id',
label=_('Assigned VLAN')
@ -2012,21 +1936,6 @@ class InterfaceFilterSet(
'wireless': queryset.filter(type__in=WIRELESS_IFACE_TYPES),
}.get(value, queryset.none())
# Override the method on CabledObjectFilterSet to also check for wireless links
def filter_occupied(self, queryset, name, value):
if value:
return queryset.filter(
Q(cable__isnull=False) |
Q(wireless_link__isnull=False) |
Q(mark_connected=True)
)
else:
return queryset.filter(
cable__isnull=True,
wireless_link__isnull=True,
mark_connected=False
)
class FrontPortFilterSet(
ModularDeviceComponentFilterSet,

View File

@ -121,11 +121,11 @@ class DeviceBayBulkCreateForm(DeviceBulkAddComponentForm):
class InventoryItemBulkCreateForm(
form_from_model(InventoryItem, ['status', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered']),
form_from_model(InventoryItem, ['role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered']),
DeviceBulkAddComponentForm
):
model = InventoryItem
field_order = (
'name', 'label', 'status', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
'description', 'tags',
)

View File

@ -1779,13 +1779,6 @@ class InventoryItemBulkEditForm(
)
nullable_fields = ('label', 'role', 'manufacturer', 'part_id', 'description')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Remove parent device passed as context to avoid conflicts with the actual device field
# on this form (see bug #19464)
self.initial.pop('device', None)
#
# Device component roles

View File

@ -470,8 +470,8 @@ class ModuleTypeImportForm(NetBoxModelImportForm):
class Meta:
model = ModuleType
fields = [
'manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit', 'profile',
'comments', 'tags'
'manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit', 'comments',
'tags',
]

View File

@ -41,6 +41,7 @@ class InterfaceCommonForm(forms.Form):
def clean(self):
super().clean()
parent_field = 'device' if 'device' in self.cleaned_data else 'virtual_machine'
if 'tagged_vlans' in self.fields.keys():
tagged_vlans = self.cleaned_data.get('tagged_vlans') if self.is_bound else \
@ -60,12 +61,6 @@ class InterfaceCommonForm(forms.Form):
"or they must be global"
).format(vlans=', '.join(invalid_vlans))
})
# Validate mode change
if self.instance.pk and (self.instance.mode != self.cleaned_data['mode']):
if 'untagged_vlan' not in self.cleaned_data and self.instance.untagged_vlan is not None:
self.instance.untagged_vlan = None
if 'tagged_vlans' not in self.cleaned_data and self.instance.tagged_vlans is not None:
self.instance.tagged_vlans.clear()
class ModuleCommonForm(forms.Form):

View File

@ -6,7 +6,7 @@ from dcim.constants import *
from dcim.models import *
from extras.forms import LocalConfigContextFilterForm
from extras.models import ConfigTemplate
from ipam.models import ASN, VRF, VLANTranslationPolicy
from ipam.models import ASN, VRF
from netbox.choices import *
from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
@ -959,56 +959,8 @@ class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxMo
model = Module
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'device_id', name=_('Location')),
FieldSet('manufacturer_id', 'module_type_id', 'status', 'serial', 'asset_tag', name=_('Hardware')),
)
device_id = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
required=False,
query_params={
'site_id': '$site_id',
'location_id': '$location_id',
'rack_id': '$rack_id',
},
label=_('Device')
)
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
required=False,
query_params={
'region_id': '$region_id',
'group_id': '$site_group_id',
},
label=_('Site')
)
location_id = DynamicModelMultipleChoiceField(
queryset=Location.objects.all(),
required=False,
query_params={
'site_id': '$site_id',
},
label=_('Location')
)
rack_id = DynamicModelMultipleChoiceField(
queryset=Rack.objects.all(),
required=False,
label=_('Rack'),
null_option='None',
query_params={
'site_id': '$site_id',
'location_id': '$location_id',
}
)
manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(),
required=False,
@ -1404,7 +1356,6 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
FieldSet('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only', name=_('Attributes')),
FieldSet('vrf_id', 'l2vpn_id', 'mac_address', 'wwn', name=_('Addressing')),
FieldSet('poe_mode', 'poe_type', name=_('PoE')),
FieldSet('mode', 'vlan_translation_policy_id', name=_('802.1Q Switching')),
FieldSet('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power', name=_('Wireless')),
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
FieldSet(
@ -1476,16 +1427,6 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
required=False,
label=_('PoE type')
)
mode = forms.MultipleChoiceField(
choices=InterfaceModeChoices,
required=False,
label=_('802.1Q mode')
)
vlan_translation_policy_id = DynamicModelMultipleChoiceField(
queryset=VLANTranslationPolicy.objects.all(),
required=False,
label=_('VLAN Translation Policy')
)
rf_role = forms.MultipleChoiceField(
choices=WirelessRoleChoices,
required=False,

View File

@ -66,10 +66,6 @@ class ScopedForm(forms.Form):
if self.instance and scope_type_id != self.instance.scope_type_id:
self.initial['scope'] = None
else:
# Clear the initial scope value if scope_type is not set
self.initial['scope'] = None
class ScopedBulkEditForm(forms.Form):
scope_type = ContentTypeChoiceField(

View File

@ -993,7 +993,7 @@ class ComponentTemplateForm(forms.ModelForm):
class ModularComponentTemplateForm(ComponentTemplateForm):
device_type = DynamicModelChoiceField(
label=_('Device type'),
queryset=DeviceType.objects.all(),
queryset=DeviceType.objects.all().all(),
required=False,
context={
'parent': 'manufacturer',
@ -1008,16 +1008,6 @@ class ModularComponentTemplateForm(ComponentTemplateForm):
}
)
fieldsets = (
FieldSet(
TabbedGroups(
FieldSet('device_type', name=_('Device Type')),
FieldSet('module_type', name=_('Module Type')),
),
'name', 'label', 'type', 'description'
),
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -1034,6 +1024,10 @@ class ModularComponentTemplateForm(ComponentTemplateForm):
class ConsolePortTemplateForm(ModularComponentTemplateForm):
fieldsets = (
FieldSet('device_type', 'module_type', 'name', 'label', 'type', 'description'),
)
class Meta:
model = ConsolePortTemplate
fields = [
@ -1042,6 +1036,10 @@ class ConsolePortTemplateForm(ModularComponentTemplateForm):
class ConsoleServerPortTemplateForm(ModularComponentTemplateForm):
fieldsets = (
FieldSet('device_type', 'module_type', 'name', 'label', 'type', 'description'),
)
class Meta:
model = ConsoleServerPortTemplate
fields = [
@ -1052,11 +1050,7 @@ class ConsoleServerPortTemplateForm(ModularComponentTemplateForm):
class PowerPortTemplateForm(ModularComponentTemplateForm):
fieldsets = (
FieldSet(
TabbedGroups(
FieldSet('device_type', name=_('Device Type')),
FieldSet('module_type', name=_('Module Type')),
),
'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
),
)
@ -1078,13 +1072,7 @@ class PowerOutletTemplateForm(ModularComponentTemplateForm):
)
fieldsets = (
FieldSet(
TabbedGroups(
FieldSet('device_type', name=_('Device Type')),
FieldSet('module_type', name=_('Module Type')),
),
'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
),
FieldSet('device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description'),
)
class Meta:
@ -1107,11 +1095,7 @@ class InterfaceTemplateForm(ModularComponentTemplateForm):
fieldsets = (
FieldSet(
TabbedGroups(
FieldSet('device_type', name=_('Device Type')),
FieldSet('module_type', name=_('Module Type')),
),
'name', 'label', 'type', 'enabled', 'mgmt_only', 'description', 'bridge',
'device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'description', 'bridge',
),
FieldSet('poe_mode', 'poe_type', name=_('PoE')),
FieldSet('rf_role', name=_('Wireless')),
@ -1138,11 +1122,8 @@ class FrontPortTemplateForm(ModularComponentTemplateForm):
fieldsets = (
FieldSet(
TabbedGroups(
FieldSet('device_type', name=_('Device Type')),
FieldSet('module_type', name=_('Module Type')),
),
'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
'description',
),
)
@ -1156,13 +1137,7 @@ class FrontPortTemplateForm(ModularComponentTemplateForm):
class RearPortTemplateForm(ModularComponentTemplateForm):
fieldsets = (
FieldSet(
TabbedGroups(
FieldSet('device_type', name=_('Device Type')),
FieldSet('module_type', name=_('Module Type')),
),
'name', 'label', 'type', 'color', 'positions', 'description',
),
FieldSet('device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description'),
)
class Meta:
@ -1174,13 +1149,7 @@ class RearPortTemplateForm(ModularComponentTemplateForm):
class ModuleBayTemplateForm(ModularComponentTemplateForm):
fieldsets = (
FieldSet(
TabbedGroups(
FieldSet('device_type', name=_('Device Type')),
FieldSet('module_type', name=_('Module Type')),
),
'name', 'label', 'position', 'description',
),
FieldSet('device_type', 'module_type', 'name', 'label', 'position', 'description'),
)
class Meta:

View File

@ -4,7 +4,7 @@ from django.utils.translation import gettext_lazy as _
from dcim.models import *
from netbox.forms import NetBoxModelForm
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField
from utilities.forms.rendering import FieldSet, TabbedGroups
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import APISelect
from . import model_forms
@ -55,18 +55,14 @@ class ComponentCreateForm(forms.Form):
def clean(self):
super().clean()
# Validate that all replication fields generate an equal number of values (or a single value)
# Validate that all replication fields generate an equal number of values
if not (patterns := self.cleaned_data.get(self.replication_fields[0])):
return
pattern_count = len(patterns)
for field_name in self.replication_fields:
value_count = len(self.cleaned_data[field_name])
if self.cleaned_data[field_name]:
if value_count == 1:
# If the field resolves to a single value (because no pattern was used), multiply it by the number
# of expected values. This allows us to reuse the same label when creating multiple components.
self.cleaned_data[field_name] = self.cleaned_data[field_name] * pattern_count
elif value_count != pattern_count:
if self.cleaned_data[field_name] and value_count != pattern_count:
raise forms.ValidationError({
field_name: _(
"The provided pattern specifies {value_count} values, but {pattern_count} are expected."
@ -118,13 +114,7 @@ class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemp
# Override fieldsets from FrontPortTemplateForm to omit rear_port_position
fieldsets = (
FieldSet(
TabbedGroups(
FieldSet('device_type', name=_('Device Type')),
FieldSet('module_type', name=_('Module Type')),
),
'name', 'label', 'type', 'color', 'rear_port', 'description',
),
FieldSet('device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'description'),
)
class Meta(model_forms.FrontPortTemplateForm.Meta):
@ -414,7 +404,6 @@ class VirtualChassisCreateForm(NetBoxModelForm):
queryset=Device.objects.all(),
required=False,
query_params={
'virtual_chassis_id': 'null',
'site_id': '$site',
'rack_id': '$rack',
}

View File

@ -159,7 +159,7 @@ class ModuleBayTemplateImportForm(forms.ModelForm):
class Meta:
model = ModuleBayTemplate
fields = [
'device_type', 'module_type', 'name', 'label', 'position', 'description',
'device_type', 'name', 'label', 'position', 'description',
]

View File

@ -15,6 +15,7 @@ __all__ = (
'InterfaceModeEnum',
'InterfacePoEModeEnum',
'InterfacePoETypeEnum',
'InterfaceSpeedEnum',
'InterfaceTypeEnum',
'InventoryItemStatusEnum',
'LinkStatusEnum',
@ -39,37 +40,38 @@ __all__ = (
'VirtualDeviceContextStatusEnum',
)
CableEndEnum = strawberry.enum(CableEndChoices.as_enum(prefix='side'))
CableLengthUnitEnum = strawberry.enum(CableLengthUnitChoices.as_enum(prefix='unit'))
CableTypeEnum = strawberry.enum(CableTypeChoices.as_enum(prefix='type'))
ConsolePortSpeedEnum = strawberry.enum(ConsolePortSpeedChoices.as_enum(prefix='speed'))
ConsolePortTypeEnum = strawberry.enum(ConsolePortTypeChoices.as_enum(prefix='type'))
DeviceAirflowEnum = strawberry.enum(DeviceAirflowChoices.as_enum(prefix='airflow'))
DeviceFaceEnum = strawberry.enum(DeviceFaceChoices.as_enum(prefix='face'))
DeviceStatusEnum = strawberry.enum(DeviceStatusChoices.as_enum(prefix='status'))
InterfaceDuplexEnum = strawberry.enum(InterfaceDuplexChoices.as_enum(prefix='duplex'))
InterfaceModeEnum = strawberry.enum(InterfaceModeChoices.as_enum(prefix='mode'))
InterfacePoEModeEnum = strawberry.enum(InterfacePoEModeChoices.as_enum(prefix='mode'))
CableEndEnum = strawberry.enum(CableEndChoices.as_enum())
CableLengthUnitEnum = strawberry.enum(CableLengthUnitChoices.as_enum())
CableTypeEnum = strawberry.enum(CableTypeChoices.as_enum())
ConsolePortSpeedEnum = strawberry.enum(ConsolePortSpeedChoices.as_enum())
ConsolePortTypeEnum = strawberry.enum(ConsolePortTypeChoices.as_enum())
DeviceAirflowEnum = strawberry.enum(DeviceAirflowChoices.as_enum())
DeviceFaceEnum = strawberry.enum(DeviceFaceChoices.as_enum())
DeviceStatusEnum = strawberry.enum(DeviceStatusChoices.as_enum())
InterfaceDuplexEnum = strawberry.enum(InterfaceDuplexChoices.as_enum())
InterfaceModeEnum = strawberry.enum(InterfaceModeChoices.as_enum())
InterfacePoEModeEnum = strawberry.enum(InterfacePoEModeChoices.as_enum())
InterfacePoETypeEnum = strawberry.enum(InterfacePoETypeChoices.as_enum())
InterfaceTypeEnum = strawberry.enum(InterfaceTypeChoices.as_enum(prefix='type'))
InventoryItemStatusEnum = strawberry.enum(InventoryItemStatusChoices.as_enum(prefix='status'))
LinkStatusEnum = strawberry.enum(LinkStatusChoices.as_enum(prefix='status'))
LocationStatusEnum = strawberry.enum(LocationStatusChoices.as_enum(prefix='status'))
InterfaceSpeedEnum = strawberry.enum(InterfaceSpeedChoices.as_enum())
InterfaceTypeEnum = strawberry.enum(InterfaceTypeChoices.as_enum())
InventoryItemStatusEnum = strawberry.enum(InventoryItemStatusChoices.as_enum())
LinkStatusEnum = strawberry.enum(LinkStatusChoices.as_enum())
LocationStatusEnum = strawberry.enum(LocationStatusChoices.as_enum())
ModuleAirflowEnum = strawberry.enum(ModuleAirflowChoices.as_enum())
ModuleStatusEnum = strawberry.enum(ModuleStatusChoices.as_enum(prefix='status'))
PortTypeEnum = strawberry.enum(PortTypeChoices.as_enum(prefix='type'))
PowerFeedPhaseEnum = strawberry.enum(PowerFeedPhaseChoices.as_enum(prefix='phase'))
PowerFeedStatusEnum = strawberry.enum(PowerFeedStatusChoices.as_enum(prefix='status'))
PowerFeedSupplyEnum = strawberry.enum(PowerFeedSupplyChoices.as_enum(prefix='supply'))
PowerFeedTypeEnum = strawberry.enum(PowerFeedTypeChoices.as_enum(prefix='type'))
PowerOutletFeedLegEnum = strawberry.enum(PowerOutletFeedLegChoices.as_enum(prefix='feed_leg'))
PowerOutletTypeEnum = strawberry.enum(PowerOutletTypeChoices.as_enum(prefix='type'))
PowerPortTypeEnum = strawberry.enum(PowerPortTypeChoices.as_enum(prefix='type'))
ModuleStatusEnum = strawberry.enum(ModuleStatusChoices.as_enum())
PortTypeEnum = strawberry.enum(PortTypeChoices.as_enum())
PowerFeedPhaseEnum = strawberry.enum(PowerFeedPhaseChoices.as_enum())
PowerFeedStatusEnum = strawberry.enum(PowerFeedStatusChoices.as_enum())
PowerFeedSupplyEnum = strawberry.enum(PowerFeedSupplyChoices.as_enum())
PowerFeedTypeEnum = strawberry.enum(PowerFeedTypeChoices.as_enum())
PowerOutletFeedLegEnum = strawberry.enum(PowerOutletFeedLegChoices.as_enum())
PowerOutletTypeEnum = strawberry.enum(PowerOutletTypeChoices.as_enum())
PowerPortTypeEnum = strawberry.enum(PowerPortTypeChoices.as_enum())
RackAirflowEnum = strawberry.enum(RackAirflowChoices.as_enum())
RackDimensionUnitEnum = strawberry.enum(RackDimensionUnitChoices.as_enum(prefix='unit'))
RackFormFactorEnum = strawberry.enum(RackFormFactorChoices.as_enum(prefix='type'))
RackStatusEnum = strawberry.enum(RackStatusChoices.as_enum(prefix='status'))
RackWidthEnum = strawberry.enum(RackWidthChoices.as_enum(prefix='width'))
SiteStatusEnum = strawberry.enum(SiteStatusChoices.as_enum(prefix='status'))
SubdeviceRoleEnum = strawberry.enum(SubdeviceRoleChoices.as_enum(prefix='role'))
VirtualDeviceContextStatusEnum = strawberry.enum(VirtualDeviceContextStatusChoices.as_enum(prefix='status'))
RackDimensionUnitEnum = strawberry.enum(RackDimensionUnitChoices.as_enum())
RackFormFactorEnum = strawberry.enum(RackFormFactorChoices.as_enum())
RackStatusEnum = strawberry.enum(RackStatusChoices.as_enum())
RackWidthEnum = strawberry.enum(RackWidthChoices.as_enum())
SiteStatusEnum = strawberry.enum(SiteStatusChoices.as_enum())
SubdeviceRoleEnum = strawberry.enum(SubdeviceRoleChoices.as_enum())
VirtualDeviceContextStatusEnum = strawberry.enum(VirtualDeviceContextStatusChoices.as_enum())

View File

@ -97,6 +97,10 @@ class InterfaceBaseFilterMixin(BaseFilterMixin):
strawberry_django.filter_field()
)
mode: InterfaceModeEnum | None = strawberry_django.filter_field()
parent: Annotated['InterfaceFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
parent_id: ID | None = strawberry_django.filter_field()
bridge: Annotated['InterfaceFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@ -131,9 +135,6 @@ class RackBaseFilterMixin(WeightFilterMixin, PrimaryModelFilterMixin):
outer_width: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
outer_height: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
outer_depth: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)

View File

@ -90,7 +90,7 @@ __all__ = (
)
@strawberry_django.filter_type(models.Cable, lookups=True)
@strawberry_django.filter(models.Cable, lookups=True)
class CableFilter(PrimaryModelFilterMixin, TenancyFilterMixin):
type: Annotated['CableTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
status: Annotated['LinkStatusEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
@ -107,7 +107,7 @@ class CableFilter(PrimaryModelFilterMixin, TenancyFilterMixin):
)
@strawberry_django.filter_type(models.CableTermination, lookups=True)
@strawberry_django.filter(models.CableTermination, lookups=True)
class CableTerminationFilter(ChangeLogFilterMixin):
cable: Annotated['CableFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
cable_id: ID | None = strawberry_django.filter_field()
@ -120,7 +120,7 @@ class CableTerminationFilter(ChangeLogFilterMixin):
termination_id: ID | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.ConsolePort, lookups=True)
@strawberry_django.filter(models.ConsolePort, lookups=True)
class ConsolePortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
@ -130,14 +130,14 @@ class ConsolePortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilte
)
@strawberry_django.filter_type(models.ConsolePortTemplate, lookups=True)
@strawberry_django.filter(models.ConsolePortTemplate, lookups=True)
class ConsolePortTemplateFilter(ModularComponentTemplateFilterMixin):
type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter_type(models.ConsoleServerPort, lookups=True)
@strawberry_django.filter(models.ConsoleServerPort, lookups=True)
class ConsoleServerPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
@ -147,14 +147,14 @@ class ConsoleServerPortFilter(ModularComponentModelFilterMixin, CabledObjectMode
)
@strawberry_django.filter_type(models.ConsoleServerPortTemplate, lookups=True)
@strawberry_django.filter(models.ConsoleServerPortTemplate, lookups=True)
class ConsoleServerPortTemplateFilter(ModularComponentTemplateFilterMixin):
type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter_type(models.Device, lookups=True)
@strawberry_django.filter(models.Device, lookups=True)
class DeviceFilter(
ContactFilterMixin,
TenancyFilterMixin,
@ -229,31 +229,31 @@ class DeviceFilter(
longitude: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
console_ports: Annotated['ConsolePortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
console_server_ports: Annotated['ConsoleServerPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
power_outlets: Annotated['PowerOutletFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
power_ports: Annotated['PowerPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
interfaces: Annotated['InterfaceFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
front_ports: Annotated['FrontPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
consoleports: Annotated['ConsolePortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
rear_ports: Annotated['RearPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
consoleserverports: Annotated['ConsoleServerPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
device_bays: Annotated['DeviceBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
poweroutlets: Annotated['PowerOutletFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
module_bays: Annotated['ModuleBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
powerports: Annotated['PowerPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
devicebays: Annotated['DeviceBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
frontports: Annotated['FrontPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
rearports: Annotated['RearPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
modulebays: Annotated['ModuleBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
modules: Annotated['ModuleFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
@ -271,7 +271,7 @@ class DeviceFilter(
inventory_item_count: FilterLookup[int] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.DeviceBay, lookups=True)
@strawberry_django.filter(models.DeviceBay, lookups=True)
class DeviceBayFilter(ComponentModelFilterMixin):
installed_device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
@ -279,12 +279,12 @@ class DeviceBayFilter(ComponentModelFilterMixin):
installed_device_id: ID | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.DeviceBayTemplate, lookups=True)
@strawberry_django.filter(models.DeviceBayTemplate, lookups=True)
class DeviceBayTemplateFilter(ComponentTemplateFilterMixin):
pass
@strawberry_django.filter_type(models.InventoryItemTemplate, lookups=True)
@strawberry_django.filter(models.InventoryItemTemplate, lookups=True)
class InventoryItemTemplateFilter(ComponentTemplateFilterMixin):
parent: Annotated['InventoryItemTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
@ -304,13 +304,13 @@ class InventoryItemTemplateFilter(ComponentTemplateFilterMixin):
part_id: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.DeviceRole, lookups=True)
@strawberry_django.filter(models.DeviceRole, lookups=True)
class DeviceRoleFilter(OrganizationalModelFilterMixin, RenderConfigFilterMixin):
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
vm_role: FilterLookup[bool] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.DeviceType, lookups=True)
@strawberry_django.filter(models.DeviceType, lookups=True)
class DeviceTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, WeightFilterMixin):
manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
@ -340,36 +340,6 @@ class DeviceTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, Weig
rear_image: Annotated['ImageAttachmentFilter', strawberry.lazy('extras.graphql.filters')] | None = (
strawberry_django.filter_field()
)
console_port_templates: (
Annotated['ConsolePortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
) = strawberry_django.filter_field()
console_server_port_templates: (
Annotated['ConsoleServerPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
) = strawberry_django.filter_field()
power_port_templates: (
Annotated['PowerPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
) = strawberry_django.filter_field()
power_outlet_templates: (
Annotated['PowerOutletTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
) = strawberry_django.filter_field()
interface_templates: (
Annotated['InterfaceTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
) = strawberry_django.filter_field()
front_port_templates: (
Annotated['FrontPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
) = strawberry_django.filter_field()
rear_port_templates: (
Annotated['RearPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
) = strawberry_django.filter_field()
device_bay_templates: (
Annotated['DeviceBayTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
) = strawberry_django.filter_field()
module_bay_templates: (
Annotated['ModuleBayTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
) = strawberry_django.filter_field()
inventory_item_templates: (
Annotated['InventoryItemTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
) = strawberry_django.filter_field()
console_port_template_count: FilterLookup[int] | None = strawberry_django.filter_field()
console_server_port_template_count: FilterLookup[int] | None = strawberry_django.filter_field()
power_port_template_count: FilterLookup[int] | None = strawberry_django.filter_field()
@ -382,7 +352,7 @@ class DeviceTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, Weig
inventory_item_template_count: FilterLookup[int] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.FrontPort, lookups=True)
@strawberry_django.filter(models.FrontPort, lookups=True)
class FrontPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
@ -395,7 +365,7 @@ class FrontPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterM
)
@strawberry_django.filter_type(models.FrontPortTemplate, lookups=True)
@strawberry_django.filter(models.FrontPortTemplate, lookups=True)
class FrontPortTemplateFilter(ModularComponentTemplateFilterMixin):
type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
@ -408,7 +378,7 @@ class FrontPortTemplateFilter(ModularComponentTemplateFilterMixin):
)
@strawberry_django.filter_type(models.MACAddress, lookups=True)
@strawberry_django.filter(models.MACAddress, lookups=True)
class MACAddressFilter(PrimaryModelFilterMixin):
mac_address: FilterLookup[str] | None = strawberry_django.filter_field()
assigned_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
@ -417,7 +387,7 @@ class MACAddressFilter(PrimaryModelFilterMixin):
assigned_object_id: ID | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.Interface, lookups=True)
@strawberry_django.filter(models.Interface, lookups=True)
class InterfaceFilter(ModularComponentModelFilterMixin, InterfaceBaseFilterMixin, CabledObjectModelFilterMixin):
vcdcs: Annotated['VirtualDeviceContextFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
@ -435,10 +405,6 @@ class InterfaceFilter(ModularComponentModelFilterMixin, InterfaceBaseFilterMixin
strawberry_django.filter_field()
)
wwn: FilterLookup[str] | None = strawberry_django.filter_field()
parent: Annotated['InterfaceFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
parent_id: ID | None = strawberry_django.filter_field()
rf_role: Annotated['WirelessRoleEnum', strawberry.lazy('wireless.graphql.enums')] | None = (
strawberry_django.filter_field()
)
@ -486,7 +452,7 @@ class InterfaceFilter(ModularComponentModelFilterMixin, InterfaceBaseFilterMixin
)
@strawberry_django.filter_type(models.InterfaceTemplate, lookups=True)
@strawberry_django.filter(models.InterfaceTemplate, lookups=True)
class InterfaceTemplateFilter(ModularComponentTemplateFilterMixin):
type: Annotated['InterfaceTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
@ -508,7 +474,7 @@ class InterfaceTemplateFilter(ModularComponentTemplateFilterMixin):
)
@strawberry_django.filter_type(models.InventoryItem, lookups=True)
@strawberry_django.filter(models.InventoryItem, lookups=True)
class InventoryItemFilter(ComponentModelFilterMixin):
parent: Annotated['InventoryItemFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
@ -535,12 +501,12 @@ class InventoryItemFilter(ComponentModelFilterMixin):
discovered: FilterLookup[bool] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.InventoryItemRole, lookups=True)
@strawberry_django.filter(models.InventoryItemRole, lookups=True)
class InventoryItemRoleFilter(OrganizationalModelFilterMixin):
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.Location, lookups=True)
@strawberry_django.filter(models.Location, lookups=True)
class LocationFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, NestedGroupModelFilterMixin):
site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
site_id: ID | None = strawberry_django.filter_field()
@ -556,12 +522,12 @@ class LocationFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilt
)
@strawberry_django.filter_type(models.Manufacturer, lookups=True)
@strawberry_django.filter(models.Manufacturer, lookups=True)
class ManufacturerFilter(ContactFilterMixin, OrganizationalModelFilterMixin):
pass
@strawberry_django.filter_type(models.Module, lookups=True)
@strawberry_django.filter(models.Module, lookups=True)
class ModuleFilter(PrimaryModelFilterMixin, ConfigContextFilterMixin):
device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
device_id: ID | None = strawberry_django.filter_field()
@ -578,39 +544,9 @@ class ModuleFilter(PrimaryModelFilterMixin, ConfigContextFilterMixin):
)
serial: FilterLookup[str] | None = strawberry_django.filter_field()
asset_tag: FilterLookup[str] | None = strawberry_django.filter_field()
console_ports: Annotated['ConsolePortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
console_server_ports: Annotated['ConsoleServerPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
power_outlets: Annotated['PowerOutletFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
power_ports: Annotated['PowerPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
interfaces: Annotated['InterfaceFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
front_ports: Annotated['FrontPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
rear_ports: Annotated['RearPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
device_bays: Annotated['DeviceBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
module_bays: Annotated['ModuleBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
modules: Annotated['ModuleFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter_type(models.ModuleBay, lookups=True)
@strawberry_django.filter(models.ModuleBay, lookups=True)
class ModuleBayFilter(ModularComponentModelFilterMixin):
parent: Annotated['ModuleBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
@ -619,64 +555,30 @@ class ModuleBayFilter(ModularComponentModelFilterMixin):
position: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.ModuleBayTemplate, lookups=True)
@strawberry_django.filter(models.ModuleBayTemplate, lookups=True)
class ModuleBayTemplateFilter(ModularComponentTemplateFilterMixin):
position: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.ModuleTypeProfile, lookups=True)
@strawberry_django.filter(models.ModuleTypeProfile, lookups=True)
class ModuleTypeProfileFilter(PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.ModuleType, lookups=True)
@strawberry_django.filter(models.ModuleType, lookups=True)
class ModuleTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, WeightFilterMixin):
manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
manufacturer_id: ID | None = strawberry_django.filter_field()
profile: Annotated['ModuleTypeProfileFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
profile_id: ID | None = strawberry_django.filter_field()
model: FilterLookup[str] | None = strawberry_django.filter_field()
part_number: FilterLookup[str] | None = strawberry_django.filter_field()
airflow: Annotated['ModuleAirflowEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
)
console_port_templates: (
Annotated['ConsolePortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
) = strawberry_django.filter_field()
console_server_port_templates: (
Annotated['ConsoleServerPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
) = strawberry_django.filter_field()
power_port_templates: (
Annotated['PowerPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
) = strawberry_django.filter_field()
power_outlet_templates: (
Annotated['PowerOutletTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
) = strawberry_django.filter_field()
interface_templates: (
Annotated['InterfaceTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
) = strawberry_django.filter_field()
front_port_templates: (
Annotated['FrontPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
) = strawberry_django.filter_field()
rear_port_templates: (
Annotated['RearPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
) = strawberry_django.filter_field()
device_bay_templates: (
Annotated['DeviceBayTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
) = strawberry_django.filter_field()
module_bay_templates: (
Annotated['ModuleBayTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
) = strawberry_django.filter_field()
inventory_item_templates: (
Annotated['InventoryItemTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
) = strawberry_django.filter_field()
@strawberry_django.filter_type(models.Platform, lookups=True)
@strawberry_django.filter(models.Platform, lookups=True)
class PlatformFilter(OrganizationalModelFilterMixin):
manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
@ -688,7 +590,7 @@ class PlatformFilter(OrganizationalModelFilterMixin):
config_template_id: ID | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.PowerFeed, lookups=True)
@strawberry_django.filter(models.PowerFeed, lookups=True)
class PowerFeedFilter(CabledObjectModelFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
power_panel: Annotated['PowerPanelFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
@ -723,7 +625,7 @@ class PowerFeedFilter(CabledObjectModelFilterMixin, TenancyFilterMixin, PrimaryM
)
@strawberry_django.filter_type(models.PowerOutlet, lookups=True)
@strawberry_django.filter(models.PowerOutlet, lookups=True)
class PowerOutletFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
type: Annotated['PowerOutletTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
@ -738,7 +640,7 @@ class PowerOutletFilter(ModularComponentModelFilterMixin, CabledObjectModelFilte
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.PowerOutletTemplate, lookups=True)
@strawberry_django.filter(models.PowerOutletTemplate, lookups=True)
class PowerOutletTemplateFilter(ModularComponentModelFilterMixin):
type: Annotated['PowerOutletTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
@ -752,7 +654,7 @@ class PowerOutletTemplateFilter(ModularComponentModelFilterMixin):
)
@strawberry_django.filter_type(models.PowerPanel, lookups=True)
@strawberry_django.filter(models.PowerPanel, lookups=True)
class PowerPanelFilter(ContactFilterMixin, ImageAttachmentFilterMixin, PrimaryModelFilterMixin):
site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
site_id: ID | None = strawberry_django.filter_field()
@ -765,7 +667,7 @@ class PowerPanelFilter(ContactFilterMixin, ImageAttachmentFilterMixin, PrimaryMo
name: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.PowerPort, lookups=True)
@strawberry_django.filter(models.PowerPort, lookups=True)
class PowerPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
type: Annotated['PowerPortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
@ -778,7 +680,7 @@ class PowerPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterM
)
@strawberry_django.filter_type(models.PowerPortTemplate, lookups=True)
@strawberry_django.filter(models.PowerPortTemplate, lookups=True)
class PowerPortTemplateFilter(ModularComponentTemplateFilterMixin):
type: Annotated['PowerPortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
@ -791,7 +693,7 @@ class PowerPortTemplateFilter(ModularComponentTemplateFilterMixin):
)
@strawberry_django.filter_type(models.RackType, lookups=True)
@strawberry_django.filter(models.RackType, lookups=True)
class RackTypeFilter(RackBaseFilterMixin):
form_factor: Annotated['RackFormFactorEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
@ -804,7 +706,7 @@ class RackTypeFilter(RackBaseFilterMixin):
slug: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.Rack, lookups=True)
@strawberry_django.filter(models.Rack, lookups=True)
class RackFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, RackBaseFilterMixin):
form_factor: Annotated['RackFormFactorEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
@ -836,7 +738,7 @@ class RackFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMi
)
@strawberry_django.filter_type(models.RackReservation, lookups=True)
@strawberry_django.filter(models.RackReservation, lookups=True)
class RackReservationFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
rack: Annotated['RackFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
rack_id: ID | None = strawberry_django.filter_field()
@ -848,12 +750,12 @@ class RackReservationFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
description: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.RackRole, lookups=True)
@strawberry_django.filter(models.RackRole, lookups=True)
class RackRoleFilter(OrganizationalModelFilterMixin):
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.RearPort, lookups=True)
@strawberry_django.filter(models.RearPort, lookups=True)
class RearPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
@ -862,7 +764,7 @@ class RearPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMi
)
@strawberry_django.filter_type(models.RearPortTemplate, lookups=True)
@strawberry_django.filter(models.RearPortTemplate, lookups=True)
class RearPortTemplateFilter(ModularComponentTemplateFilterMixin):
type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
@ -871,7 +773,7 @@ class RearPortTemplateFilter(ModularComponentTemplateFilterMixin):
)
@strawberry_django.filter_type(models.Region, lookups=True)
@strawberry_django.filter(models.Region, lookups=True)
class RegionFilter(ContactFilterMixin, NestedGroupModelFilterMixin):
prefixes: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field()
@ -881,7 +783,7 @@ class RegionFilter(ContactFilterMixin, NestedGroupModelFilterMixin):
)
@strawberry_django.filter_type(models.Site, lookups=True)
@strawberry_django.filter(models.Site, lookups=True)
class SiteFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field()
slug: FilterLookup[str] | None = strawberry_django.filter_field()
@ -915,7 +817,7 @@ class SiteFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMi
)
@strawberry_django.filter_type(models.SiteGroup, lookups=True)
@strawberry_django.filter(models.SiteGroup, lookups=True)
class SiteGroupFilter(ContactFilterMixin, NestedGroupModelFilterMixin):
prefixes: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field()
@ -925,19 +827,16 @@ class SiteGroupFilter(ContactFilterMixin, NestedGroupModelFilterMixin):
)
@strawberry_django.filter_type(models.VirtualChassis, lookups=True)
@strawberry_django.filter(models.VirtualChassis, lookups=True)
class VirtualChassisFilter(PrimaryModelFilterMixin):
master: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
master_id: ID | None = strawberry_django.filter_field()
name: FilterLookup[str] | None = strawberry_django.filter_field()
domain: FilterLookup[str] | None = strawberry_django.filter_field()
members: (
Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None
) = strawberry_django.filter_field()
member_count: FilterLookup[int] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.VirtualDeviceContext, lookups=True)
@strawberry_django.filter(models.VirtualDeviceContext, lookups=True)
class VirtualDeviceContextFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
device_id: ID | None = strawberry_django.filter_field()
@ -957,6 +856,3 @@ class VirtualDeviceContextFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
)
primary_ip6_id: ID | None = strawberry_django.filter_field()
comments: FilterLookup[str] | None = strawberry_django.filter_field()
interfaces: (
Annotated['InterfaceFilter', strawberry.lazy('dcim.graphql.filters')] | None
) = strawberry_django.filter_field()

View File

@ -33,7 +33,6 @@ if TYPE_CHECKING:
from tenancy.graphql.types import TenantType
from users.graphql.types import UserType
from virtualization.graphql.types import ClusterType, VMInterfaceType, VirtualMachineType
from vpn.graphql.types import L2VPNTerminationType
from wireless.graphql.types import WirelessLANType, WirelessLinkType
__all__ = (
@ -441,7 +440,6 @@ class InterfaceType(IPAddressesMixin, ModularComponentType, CabledObjectMixin, P
primary_mac_address: Annotated["MACAddressType", strawberry.lazy('dcim.graphql.types')] | None
qinq_svlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None
vlan_translation_policy: Annotated["VLANTranslationPolicyType", strawberry.lazy('ipam.graphql.types')] | None
l2vpn_termination: Annotated["L2VPNTerminationType", strawberry.lazy('vpn.graphql.types')] | None
vdcs: List[Annotated["VirtualDeviceContextType", strawberry.lazy('dcim.graphql.types')]]
tagged_vlans: List[Annotated["VLANType", strawberry.lazy('ipam.graphql.types')]]
@ -543,10 +541,10 @@ class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, Organi
class ManufacturerType(OrganizationalObjectType, ContactsMixin):
platforms: List[Annotated["PlatformType", strawberry.lazy('dcim.graphql.types')]]
device_types: List[Annotated["DeviceTypeType", strawberry.lazy('dcim.graphql.types')]]
device_types: List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]
inventory_item_templates: List[Annotated["InventoryItemTemplateType", strawberry.lazy('dcim.graphql.types')]]
inventory_items: List[Annotated["InventoryItemType", strawberry.lazy('dcim.graphql.types')]]
module_types: List[Annotated["ModuleTypeType", strawberry.lazy('dcim.graphql.types')]]
module_types: List[Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
@ -619,11 +617,11 @@ class ModuleTypeType(NetBoxObjectType):
frontporttemplates: List[Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]]
consoleserverporttemplates: List[Annotated["ConsoleServerPortTemplateType", strawberry.lazy('dcim.graphql.types')]]
interfacetemplates: List[Annotated["InterfaceTemplateType", strawberry.lazy('dcim.graphql.types')]]
powerporttemplates: List[Annotated["PowerPortTemplateType", strawberry.lazy('dcim.graphql.types')]]
powerporttemplates: List[Annotated["PowerOutletTemplateType", strawberry.lazy('dcim.graphql.types')]]
poweroutlettemplates: List[Annotated["PowerOutletTemplateType", strawberry.lazy('dcim.graphql.types')]]
rearporttemplates: List[Annotated["RearPortTemplateType", strawberry.lazy('dcim.graphql.types')]]
instances: List[Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')]]
consoleporttemplates: List[Annotated["ConsolePortTemplateType", strawberry.lazy('dcim.graphql.types')]]
instances: List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]
consoleporttemplates: List[Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(

View File

@ -100,16 +100,3 @@ class Migration(migrations.Migration):
),
),
]
def oc_rename_type(objectchange, reverting):
for data in (objectchange.prechange_data, objectchange.postchange_data):
if data is None:
continue
if 'type' in data:
data['form_factor'] = data.pop('type')
objectchange_migrators = {
'dcim.rack': oc_rename_type,
}

View File

@ -26,50 +26,49 @@ def set_null_values(apps, schema_editor):
RackType = apps.get_model('dcim', 'RackType')
RearPort = apps.get_model('dcim', 'RearPort')
Site = apps.get_model('dcim', 'Site')
db_alias = schema_editor.connection.alias
Cable.objects.using(db_alias).filter(length_unit='').update(length_unit=None)
Cable.objects.using(db_alias).filter(type='').update(type=None)
ConsolePort.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
ConsolePort.objects.using(db_alias).filter(type='').update(type=None)
ConsolePortTemplate.objects.using(db_alias).filter(type='').update(type=None)
ConsoleServerPort.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
ConsoleServerPort.objects.using(db_alias).filter(type='').update(type=None)
ConsoleServerPortTemplate.objects.using(db_alias).filter(type='').update(type=None)
Device.objects.using(db_alias).filter(airflow='').update(airflow=None)
Device.objects.using(db_alias).filter(face='').update(face=None)
DeviceType.objects.using(db_alias).filter(airflow='').update(airflow=None)
DeviceType.objects.using(db_alias).filter(subdevice_role='').update(subdevice_role=None)
DeviceType.objects.using(db_alias).filter(weight_unit='').update(weight_unit=None)
FrontPort.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
Interface.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
Interface.objects.using(db_alias).filter(mode='').update(mode=None)
Interface.objects.using(db_alias).filter(poe_mode='').update(poe_mode=None)
Interface.objects.using(db_alias).filter(poe_type='').update(poe_type=None)
Interface.objects.using(db_alias).filter(rf_channel='').update(rf_channel=None)
Interface.objects.using(db_alias).filter(rf_role='').update(rf_role=None)
InterfaceTemplate.objects.using(db_alias).filter(poe_mode='').update(poe_mode=None)
InterfaceTemplate.objects.using(db_alias).filter(poe_type='').update(poe_type=None)
InterfaceTemplate.objects.using(db_alias).filter(rf_role='').update(rf_role=None)
ModuleType.objects.using(db_alias).filter(airflow='').update(airflow=None)
ModuleType.objects.using(db_alias).filter(weight_unit='').update(weight_unit=None)
PowerFeed.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
PowerOutlet.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
PowerOutlet.objects.using(db_alias).filter(feed_leg='').update(feed_leg=None)
PowerOutlet.objects.using(db_alias).filter(type='').update(type=None)
PowerOutletTemplate.objects.using(db_alias).filter(feed_leg='').update(feed_leg=None)
PowerOutletTemplate.objects.using(db_alias).filter(type='').update(type=None)
PowerPort.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
PowerPort.objects.using(db_alias).filter(type='').update(type=None)
PowerPortTemplate.objects.using(db_alias).filter(type='').update(type=None)
Rack.objects.using(db_alias).filter(airflow='').update(airflow=None)
Rack.objects.using(db_alias).filter(form_factor='').update(form_factor=None)
Rack.objects.using(db_alias).filter(outer_unit='').update(outer_unit=None)
Rack.objects.using(db_alias).filter(weight_unit='').update(weight_unit=None)
RackType.objects.using(db_alias).filter(outer_unit='').update(outer_unit=None)
RackType.objects.using(db_alias).filter(weight_unit='').update(weight_unit=None)
RearPort.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
Site.objects.using(db_alias).filter(time_zone='').update(time_zone=None)
Cable.objects.filter(length_unit='').update(length_unit=None)
Cable.objects.filter(type='').update(type=None)
ConsolePort.objects.filter(cable_end='').update(cable_end=None)
ConsolePort.objects.filter(type='').update(type=None)
ConsolePortTemplate.objects.filter(type='').update(type=None)
ConsoleServerPort.objects.filter(cable_end='').update(cable_end=None)
ConsoleServerPort.objects.filter(type='').update(type=None)
ConsoleServerPortTemplate.objects.filter(type='').update(type=None)
Device.objects.filter(airflow='').update(airflow=None)
Device.objects.filter(face='').update(face=None)
DeviceType.objects.filter(airflow='').update(airflow=None)
DeviceType.objects.filter(subdevice_role='').update(subdevice_role=None)
DeviceType.objects.filter(weight_unit='').update(weight_unit=None)
FrontPort.objects.filter(cable_end='').update(cable_end=None)
Interface.objects.filter(cable_end='').update(cable_end=None)
Interface.objects.filter(mode='').update(mode=None)
Interface.objects.filter(poe_mode='').update(poe_mode=None)
Interface.objects.filter(poe_type='').update(poe_type=None)
Interface.objects.filter(rf_channel='').update(rf_channel=None)
Interface.objects.filter(rf_role='').update(rf_role=None)
InterfaceTemplate.objects.filter(poe_mode='').update(poe_mode=None)
InterfaceTemplate.objects.filter(poe_type='').update(poe_type=None)
InterfaceTemplate.objects.filter(rf_role='').update(rf_role=None)
ModuleType.objects.filter(airflow='').update(airflow=None)
ModuleType.objects.filter(weight_unit='').update(weight_unit=None)
PowerFeed.objects.filter(cable_end='').update(cable_end=None)
PowerOutlet.objects.filter(cable_end='').update(cable_end=None)
PowerOutlet.objects.filter(feed_leg='').update(feed_leg=None)
PowerOutlet.objects.filter(type='').update(type=None)
PowerOutletTemplate.objects.filter(feed_leg='').update(feed_leg=None)
PowerOutletTemplate.objects.filter(type='').update(type=None)
PowerPort.objects.filter(cable_end='').update(cable_end=None)
PowerPort.objects.filter(type='').update(type=None)
PowerPortTemplate.objects.filter(type='').update(type=None)
Rack.objects.filter(airflow='').update(airflow=None)
Rack.objects.filter(form_factor='').update(form_factor=None)
Rack.objects.filter(outer_unit='').update(outer_unit=None)
Rack.objects.filter(weight_unit='').update(weight_unit=None)
RackType.objects.filter(outer_unit='').update(outer_unit=None)
RackType.objects.filter(weight_unit='').update(weight_unit=None)
RearPort.objects.filter(cable_end='').update(cable_end=None)
Site.objects.filter(time_zone='').update(time_zone=None)
class Migration(migrations.Migration):

View File

@ -1,6 +1,4 @@
import django.db.models.deletion
from django.apps import apps
from django.contrib.contenttypes.models import ContentType
from django.db import migrations, models
@ -8,26 +6,19 @@ def populate_mac_addresses(apps, schema_editor):
ContentType = apps.get_model('contenttypes', 'ContentType')
Interface = apps.get_model('dcim', 'Interface')
MACAddress = apps.get_model('dcim', 'MACAddress')
db_alias = schema_editor.connection.alias
interface_ct = ContentType.objects.get_for_model(Interface)
mac_addresses = [
MACAddress(
mac_address=interface.mac_address,
assigned_object_type=interface_ct,
assigned_object_id=interface.pk
mac_address=interface.mac_address, assigned_object_type=interface_ct, assigned_object_id=interface.pk
)
for interface in Interface.objects.using(db_alias).filter(mac_address__isnull=False)
for interface in Interface.objects.filter(mac_address__isnull=False)
]
MACAddress.objects.using(db_alias).bulk_create(mac_addresses, batch_size=100)
MACAddress.objects.bulk_create(mac_addresses, batch_size=100)
# TODO: Optimize interface updates
for mac_address in mac_addresses:
Interface.objects.using(db_alias).filter(
pk=mac_address.assigned_object_id
).update(
primary_mac_address=mac_address
)
Interface.objects.filter(pk=mac_address.assigned_object_id).update(primary_mac_address=mac_address)
class Migration(migrations.Migration):
@ -53,43 +44,3 @@ class Migration(migrations.Migration):
name='mac_address',
),
]
# See peer migrator in virtualization.0048_populate_mac_addresses before making changes
def oc_interface_primary_mac_address(objectchange, reverting):
MACAddress = apps.get_model('dcim', 'MACAddress')
interface_ct = ContentType.objects.get_by_natural_key('dcim', 'interface')
# Swap data order if the change is being reverted
if not reverting:
before, after = objectchange.prechange_data, objectchange.postchange_data
else:
before, after = objectchange.postchange_data, objectchange.prechange_data
if after.get('mac_address') != before.get('mac_address'):
# Create & assign the new MACAddress (if any)
if after.get('mac_address'):
mac = MACAddress.objects.create(
mac_address=after['mac_address'],
assigned_object_type=interface_ct,
assigned_object_id=objectchange.changed_object_id,
)
after['primary_mac_address'] = mac.pk
else:
after['primary_mac_address'] = None
# Delete the old MACAddress (if any)
if before.get('mac_address'):
MACAddress.objects.filter(
mac_address=before['mac_address'],
assigned_object_type=interface_ct,
assigned_object_id=objectchange.changed_object_id,
).delete()
before['primary_mac_address'] = None
before.pop('mac_address', None)
after.pop('mac_address', None)
objectchange_migrators = {
'dcim.interface': oc_interface_primary_mac_address,
}

View File

@ -11,16 +11,13 @@ def load_initial_data(apps, schema_editor):
Load initial ModuleTypeProfile objects from file.
"""
ModuleTypeProfile = apps.get_model('dcim', 'ModuleTypeProfile')
db_alias = schema_editor.connection.alias
initial_profiles = (
'cpu',
'fan',
'gpu',
'hard_disk',
'memory',
'power_supply',
'expansion_card'
'power_supply'
)
for name in initial_profiles:
@ -28,7 +25,7 @@ def load_initial_data(apps, schema_editor):
with file_path.open('r') as f:
data = json.load(f)
try:
ModuleTypeProfile.objects.using(db_alias).create(**data)
ModuleTypeProfile.objects.create(**data)
except Exception as e:
print(f"Error loading data from {file_path}")
raise e

View File

@ -1,44 +0,0 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0207_remove_redundant_indexes'),
('extras', '0129_fix_script_paths'),
]
operations = [
migrations.AddConstraint(
model_name='devicerole',
constraint=models.UniqueConstraint(
fields=('parent', 'name'),
name='dcim_devicerole_parent_name'
),
),
migrations.AddConstraint(
model_name='devicerole',
constraint=models.UniqueConstraint(
condition=models.Q(('parent__isnull', True)),
fields=('name',),
name='dcim_devicerole_name',
violation_error_message='A top-level device role with this name already exists.'
),
),
migrations.AddConstraint(
model_name='devicerole',
constraint=models.UniqueConstraint(
fields=('parent', 'slug'),
name='dcim_devicerole_parent_slug'
),
),
migrations.AddConstraint(
model_name='devicerole',
constraint=models.UniqueConstraint(
condition=models.Q(('parent__isnull', True)),
fields=('slug',),
name='dcim_devicerole_slug',
violation_error_message='A top-level device role with this slug already exists.'
),
),
]

View File

@ -1,15 +0,0 @@
{
"name": "Expansion card",
"schema": {
"properties": {
"connector_type": {
"type": "string",
"description": "Connector type e.g. PCIe x4"
},
"bandwidth": {
"type": "integer",
"description": "Total Bandwidth for this module"
}
}
}
}

View File

@ -3,6 +3,7 @@ import itertools
from django.contrib.contenttypes.fields import GenericForeignKey
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import Sum
from django.dispatch import Signal
from django.utils.translation import gettext_lazy as _
@ -773,28 +774,9 @@ class CablePath(models.Model):
Return a tuple containing the sum of the length of each cable in the path
and a flag indicating whether the length is definitive.
"""
cable_ct = ObjectType.objects.get_for_model(Cable).pk
# Pre-cache cable lengths by ID
cable_ids = self.get_cable_ids()
cables = {
cable['pk']: cable['_abs_length']
for cable in Cable.objects.filter(id__in=cable_ids, _abs_length__isnull=False).values('pk', '_abs_length')
}
# Iterate through each set of nodes in the path. For cables, add the length of the longest cable to the total
# length of the path.
total_length = 0
for node_set in self.path:
hop_length = 0
for node in node_set:
ct, pk = decompile_path_node(node)
if ct != cable_ct:
break # Not a cable
if pk in cables and cables[pk] > hop_length:
hop_length = cables[pk]
total_length += hop_length
cables = Cable.objects.filter(id__in=cable_ids, _abs_length__isnull=False)
total_length = cables.aggregate(total=Sum('_abs_length'))['total']
is_definitive = len(cables) == len(cable_ids)
return total_length, is_definitive

View File

@ -398,28 +398,6 @@ class DeviceRole(NestedGroupModel):
class Meta:
ordering = ('name',)
constraints = (
models.UniqueConstraint(
fields=('parent', 'name'),
name='%(app_label)s_%(class)s_parent_name'
),
models.UniqueConstraint(
fields=('name',),
name='%(app_label)s_%(class)s_name',
condition=Q(parent__isnull=True),
violation_error_message=_("A top-level device role with this name already exists.")
),
models.UniqueConstraint(
fields=('parent', 'slug'),
name='%(app_label)s_%(class)s_parent_slug'
),
models.UniqueConstraint(
fields=('slug',),
name='%(app_label)s_%(class)s_slug',
condition=Q(parent__isnull=True),
violation_error_message=_("A top-level device role with this slug already exists.")
),
)
verbose_name = _('device role')
verbose_name_plural = _('device roles')

View File

@ -85,7 +85,7 @@ class CachedScopeMixin(models.Model):
abstract = True
def clean(self):
if self.scope_type and not (self.scope or self.scope_id):
if self.scope_type and not self.scope:
scope_type = self.scope_type.model_class()
raise ValidationError({
'scope': _(

View File

@ -144,7 +144,7 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
super().clean()
# Validate any attributes against the assigned profile's schema
if self.profile and self.profile.schema:
if self.profile:
try:
jsonschema.validate(self.attribute_data, schema=self.profile.schema)
except JSONValidationError as e:

View File

@ -732,8 +732,3 @@ class RackReservation(PrimaryModel):
@property
def unit_list(self):
return array_to_string(self.units)
def to_objectchange(self, action):
objectchange = super().to_objectchange(action)
objectchange.related_object = self.rack
return objectchange

View File

@ -225,7 +225,8 @@ class CableTraceSVG:
"""
nodes_height = 0
nodes = []
for i, term in enumerate(terminations):
# Sort them by name to make renders more readable
for i, term in enumerate(sorted(terminations, key=lambda x: str(x))):
node = Node(
position=(offset_x + i * width, self.cursor),
width=width,
@ -329,9 +330,11 @@ class CableTraceSVG:
# Draw attachment (line)
start = (OFFSET + self.center, OFFSET + self.cursor)
end = (start[0], start[1] + CABLE_HEIGHT)
height = PADDING * 2 + LINE_HEIGHT + PADDING * 2
end = (start[0], start[1] + height)
line = Line(start=start, end=end, class_='attachment')
group.add(line)
self.cursor += PADDING * 4
return group
@ -356,10 +359,10 @@ class CableTraceSVG:
# Else: No need to draw parent objects (parent objects are drawn in last "round" as the far-end!)
near_terminations = self.draw_terminations(near_ends, parent_object_nodes)
self.cursor += CABLE_HEIGHT
# Connector (a Cable or WirelessLink)
if links and far_ends:
self.cursor += CABLE_HEIGHT
obj_list = {end.parent_object for end in far_ends}
parent_object_nodes, far_terminations = self.draw_far_objects(obj_list, far_ends)
@ -447,7 +450,6 @@ class CableTraceSVG:
# Attachment
attachment = self.draw_attachment()
self.connectors.append(attachment)
self.cursor += CABLE_HEIGHT
# Object
parent_object_nodes = self.draw_parent_objects(far_ends)

View File

@ -63,10 +63,6 @@ class DeviceRoleTable(NetBoxTable):
verbose_name=_('Name'),
linkify=True
)
parent = tables.Column(
verbose_name=_('Parent'),
linkify=True,
)
device_count = columns.LinkedCountColumn(
viewname='dcim:device_list',
url_params={'role_id': 'pk'},
@ -92,8 +88,8 @@ class DeviceRoleTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = models.DeviceRole
fields = (
'pk', 'id', 'name', 'parent', 'device_count', 'vm_count', 'color', 'vm_role', 'config_template',
'description', 'slug', 'tags', 'actions', 'created', 'last_updated',
'pk', 'id', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'config_template', 'description',
'slug', 'tags', 'actions', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description')
@ -1095,9 +1091,10 @@ class VirtualDeviceContextTable(TenancyColumnsMixin, NetBoxTable):
verbose_name=_('Name'),
linkify=True
)
device = tables.Column(
device = tables.TemplateColumn(
verbose_name=_('Device'),
order_by=('device___name',),
template_code=DEVICE_LINK,
linkify=True
)
status = columns.ChoiceFieldColumn(

View File

@ -24,10 +24,6 @@ class RegionTable(ContactsColumnMixin, NetBoxTable):
verbose_name=_('Name'),
linkify=True
)
parent = tables.Column(
verbose_name=_('Parent'),
linkify=True,
)
site_count = columns.LinkedCountColumn(
viewname='dcim:site_list',
url_params={'region_id': 'pk'},
@ -43,7 +39,7 @@ class RegionTable(ContactsColumnMixin, NetBoxTable):
class Meta(NetBoxTable.Meta):
model = Region
fields = (
'pk', 'id', 'name', 'parent', 'slug', 'site_count', 'description', 'comments', 'contacts', 'tags',
'pk', 'id', 'name', 'slug', 'site_count', 'description', 'comments', 'contacts', 'tags',
'created', 'last_updated', 'actions',
)
default_columns = ('pk', 'name', 'site_count', 'description')
@ -58,10 +54,6 @@ class SiteGroupTable(ContactsColumnMixin, NetBoxTable):
verbose_name=_('Name'),
linkify=True
)
parent = tables.Column(
verbose_name=_('Parent'),
linkify=True,
)
site_count = columns.LinkedCountColumn(
viewname='dcim:site_list',
url_params={'group_id': 'pk'},
@ -77,7 +69,7 @@ class SiteGroupTable(ContactsColumnMixin, NetBoxTable):
class Meta(NetBoxTable.Meta):
model = SiteGroup
fields = (
'pk', 'id', 'name', 'parent', 'slug', 'site_count', 'description', 'comments', 'contacts', 'tags',
'pk', 'id', 'name', 'slug', 'site_count', 'description', 'comments', 'contacts', 'tags',
'created', 'last_updated', 'actions',
)
default_columns = ('pk', 'name', 'site_count', 'description')
@ -143,10 +135,6 @@ class LocationTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
verbose_name=_('Name'),
linkify=True
)
parent = tables.Column(
verbose_name=_('Parent'),
linkify=True,
)
site = tables.Column(
verbose_name=_('Site'),
linkify=True
@ -182,8 +170,8 @@ class LocationTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
class Meta(NetBoxTable.Meta):
model = Location
fields = (
'pk', 'id', 'name', 'parent', 'site', 'status', 'facility', 'tenant', 'tenant_group', 'rack_count',
'device_count', 'description', 'slug', 'comments', 'contacts', 'tags', 'actions', 'created', 'last_updated',
'pk', 'id', 'name', 'site', 'status', 'facility', 'tenant', 'tenant_group', 'rack_count', 'device_count',
'description', 'slug', 'comments', 'contacts', 'tags', 'actions', 'created', 'last_updated',
'vlangroup_count',
)
default_columns = (

View File

@ -64,7 +64,7 @@ INTERFACE_IPADDRESSES = """
INTERFACE_FHRPGROUPS = """
{% for assignment in value.all %}
<a href="{{ assignment.group.get_absolute_url }}">{{ assignment.group }}</a>
<a href="{{ assignment.group.get_absolute_url }}">{{ assignment.group.get_protocol_display }}: {{ assignment.group.group_id }}</a>
{% endfor %}
"""

View File

@ -14,7 +14,7 @@ from ipam.models import ASN, RIR, VLAN, VRF
from netbox.api.serializers import GenericObjectSerializer
from tenancy.models import Tenant
from users.models import User
from utilities.testing import APITestCase, APIViewTestCases, create_test_device, disable_logging
from utilities.testing import APITestCase, APIViewTestCases, create_test_device
from virtualization.models import Cluster, ClusterType
from wireless.choices import WirelessChannelChoices
from wireless.models import WirelessLAN
@ -1858,7 +1858,6 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
# Attempt to delete only the parent interface
url = self._get_detail_url(interface1)
with disable_logging():
self.client.delete(url, **self.header)
self.assertEqual(device.interfaces.count(), 4) # Parent was not deleted

Some files were not shown because too many files have changed in this diff Show More