mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-16 12:12:53 -06:00
Merge branch 'feature' of https://github.com/netbox-community/netbox into feature-ip-prefix-link
# Conflicts: # netbox/ipam/forms/bulk_import.py
This commit is contained in:
commit
c211b624d0
@ -15,7 +15,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.2.3
|
||||
placeholder: v4.2.9
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
2
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
@ -27,7 +27,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox Version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.2.3
|
||||
placeholder: v4.2.9
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
@ -12,6 +12,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
if: github.repository == 'netbox-community/netbox'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
|
1
.github/workflows/close-stale-issues.yml
vendored
1
.github/workflows/close-stale-issues.yml
vendored
@ -13,6 +13,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
if: github.repository == 'netbox-community/netbox'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
|
3
.github/workflows/lock-threads.yml
vendored
3
.github/workflows/lock-threads.yml
vendored
@ -13,9 +13,10 @@ permissions:
|
||||
|
||||
jobs:
|
||||
lock:
|
||||
if: github.repository == 'netbox-community/netbox'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v5
|
||||
- uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
|
||||
with:
|
||||
issue-inactive-days: 90
|
||||
pr-inactive-days: 30
|
||||
|
@ -13,6 +13,7 @@ env:
|
||||
|
||||
jobs:
|
||||
makemessages:
|
||||
if: github.repository == 'netbox-community/netbox'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NETBOX_CONFIGURATION: netbox.configuration_testing
|
||||
@ -47,7 +48,7 @@ jobs:
|
||||
run: python netbox/manage.py makemessages -l ${{ env.LOCALE }}
|
||||
|
||||
- name: Commit changes
|
||||
uses: EndBug/add-and-commit@v9
|
||||
uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9.1.4
|
||||
with:
|
||||
add: 'netbox/translations/'
|
||||
default_author: github_actions
|
||||
|
@ -1,7 +1,7 @@
|
||||
[main]
|
||||
host = https://app.transifex.com
|
||||
|
||||
[o:netbox-community:p:netbox:r:9cbf4fcf95b3d92e4ebbf1a5e5d1caee]
|
||||
[o:netbox-community:p:netbox:r:034999968a7366ba27a8bdf1ab63bf42]
|
||||
file_filter = netbox/translations/<lang>/LC_MESSAGES/django.po
|
||||
source_file = netbox/translations/en/LC_MESSAGES/django.po
|
||||
type = PO
|
||||
|
@ -96,8 +96,7 @@ mkdocs-material
|
||||
|
||||
# Introspection for embedded code
|
||||
# https://github.com/mkdocstrings/mkdocstrings/blob/main/CHANGELOG.md
|
||||
# See #18568
|
||||
mkdocstrings[python-legacy]==0.27.0
|
||||
mkdocstrings[python]
|
||||
|
||||
# Library for manipulating IP prefixes and addresses
|
||||
# https://github.com/netaddr/netaddr/blob/master/CHANGELOG.rst
|
||||
@ -141,8 +140,7 @@ strawberry-graphql
|
||||
|
||||
# Strawberry GraphQL Django extension
|
||||
# https://github.com/strawberry-graphql/strawberry-django/releases
|
||||
# Pinned to v0.52.0 for suspected upstream bug; see #18329
|
||||
strawberry-graphql-django==0.52.0
|
||||
strawberry-graphql-django
|
||||
|
||||
# SVG image rendering (used for rack elevations)
|
||||
# https://github.com/mozman/svgwrite/blob/master/NEWS.rst
|
||||
|
@ -427,6 +427,7 @@
|
||||
"e3",
|
||||
"xdsl",
|
||||
"docsis",
|
||||
"moca",
|
||||
"bpon",
|
||||
"epon",
|
||||
"10g-epon",
|
||||
@ -500,6 +501,9 @@
|
||||
"n",
|
||||
"mrj21",
|
||||
"fc",
|
||||
"fc-pc",
|
||||
"fc-upc",
|
||||
"fc-apc",
|
||||
"lc",
|
||||
"lc-pc",
|
||||
"lc-upc",
|
||||
@ -565,6 +569,9 @@
|
||||
"n",
|
||||
"mrj21",
|
||||
"fc",
|
||||
"fc-pc",
|
||||
"fc-upc",
|
||||
"fc-apc",
|
||||
"lc",
|
||||
"lc-pc",
|
||||
"lc-upc",
|
||||
|
@ -54,6 +54,7 @@ Icons](https://github.com/google/material-design-icons) icon's name; or be
|
||||
`None` for no icon.
|
||||
|
||||
For instance, the OIDC backend may be customized with
|
||||
|
||||
```python
|
||||
SOCIAL_AUTH_BACKEND_ATTRS = {
|
||||
'oidc': ("My awesome SSO", "login"),
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
## 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:
|
||||
|
||||
@ -14,7 +14,7 @@ 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/).
|
||||
|
||||
@ -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).
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
!!! tip "Dynamic Configuration Parameter"
|
||||
|
||||
Default: True
|
||||
Default: `True`
|
||||
|
||||
Setting this to False will disable the GraphQL API.
|
||||
|
||||
@ -12,6 +12,6 @@ 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.
|
||||
|
@ -55,7 +55,7 @@ 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.
|
||||
|
||||
@ -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,7 +100,7 @@ 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.
|
||||
|
||||
@ -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,7 +141,7 @@ 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.
|
||||
|
||||
@ -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,7 +179,7 @@ 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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -233,3 +233,15 @@ This parameter controls how frequently a failed job is retried, up to the maximu
|
||||
Default: `0` (retries disabled)
|
||||
|
||||
The maximum number of times a background task will be retried before being marked as failed.
|
||||
|
||||
## DISK_BASE_UNIT
|
||||
|
||||
Default: `1000`
|
||||
|
||||
The base unit for disk sizes. Set this to `1024` to use binary prefixes (MiB, GiB, etc.) instead of decimal prefixes (MB, GB, etc.).
|
||||
|
||||
## RAM_BASE_UNIT
|
||||
|
||||
Default: `1000`
|
||||
|
||||
The base unit for RAM sizes. Set this to `1024` to use binary prefixes (MiB, GiB, etc.) instead of decimal prefixes (MB, GB, etc.).
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
## PLUGINS
|
||||
|
||||
Default: Empty
|
||||
Default: `[]`
|
||||
|
||||
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: Empty
|
||||
Default: `[]`
|
||||
|
||||
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:
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
## ALLOW_TOKEN_RETRIEVAL
|
||||
|
||||
Default: False
|
||||
Default: `False`
|
||||
|
||||
!!! note
|
||||
The default value of this parameter changed from true to false in NetBox v4.3.0.
|
||||
@ -50,7 +50,7 @@ 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).
|
||||
|
||||
@ -82,7 +82,7 @@ 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.
|
||||
|
||||
@ -162,7 +162,7 @@ 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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -210,7 +210,7 @@ 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.
|
||||
|
||||
@ -218,7 +218,7 @@ If true, the `includeSubDomains` directive will be included in the HTTP Strict T
|
||||
|
||||
## 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.
|
||||
|
||||
@ -226,7 +226,7 @@ If true, the `preload` directive will be included in the HTTP Strict Transport S
|
||||
|
||||
## 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,7 +234,7 @@ 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.
|
||||
|
||||
@ -253,7 +253,7 @@ 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.
|
||||
|
||||
@ -261,6 +261,6 @@ If true, the cookie employed for session authentication will be marked as secure
|
||||
|
||||
## 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.
|
||||
|
@ -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:
|
||||
|
||||
@ -14,6 +14,8 @@ BASE_PATH = 'netbox/'
|
||||
|
||||
## DATABASE_ROUTERS
|
||||
|
||||
!!! info "This parameter was introduced in NetBox v4.3."
|
||||
|
||||
Default: `[]` (empty list)
|
||||
|
||||
An iterable of [database routers](https://docs.djangoproject.com/en/stable/topics/db/multi-db/) to use for automatically selecting the appropriate database(s) for a query. This is useful only when [multiple databases](./required-parameters.md#databases) have been configured.
|
||||
@ -72,7 +74,7 @@ Email is sent from NetBox only for critical events or if configured for [logging
|
||||
|
||||
## HTTP_PROXIES
|
||||
|
||||
Default: Empty
|
||||
Default: `None`
|
||||
|
||||
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:
|
||||
|
||||
@ -99,7 +101,7 @@ 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.
|
||||
|
||||
@ -172,6 +174,8 @@ The file path to the location where media files (such as image attachments) are
|
||||
|
||||
## PROXY_ROUTERS
|
||||
|
||||
!!! info "This parameter was introduced in NetBox v4.3."
|
||||
|
||||
Default: `["utilities.proxy.DefaultProxyRouter"]`
|
||||
|
||||
A list of Python classes responsible for determining which proxy server(s) to use for outbound HTTP requests. Each item in the list can be the class itself or the dotted path to the class.
|
||||
@ -249,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).
|
||||
|
||||
@ -257,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.)
|
||||
|
@ -310,6 +310,7 @@ A particular object within NetBox. Each ObjectVar must specify a particular mode
|
||||
* `query_params` - A dictionary of query parameters to use when retrieving available options (optional)
|
||||
* `context` - A custom dictionary mapping template context variables to fields, used when rendering `<option>` elements within the dropdown menu (optional; see below)
|
||||
* `null_option` - A label representing a "null" or empty choice (optional)
|
||||
* `selector` - A boolean that, when True, includes an advanced object selection widget to assist the user in identifying the desired object (optional; False by default)
|
||||
|
||||
To limit the selections available within the list, additional query parameters can be passed as the `query_params` dictionary. For example, to show only devices with an "active" status:
|
||||
|
||||
|
@ -25,6 +25,7 @@ Height: {{ rack.u_height }}U
|
||||
To access custom fields of an object within a template, use the `cf` attribute. For example, `{{ obj.cf.color }}` will return the value (if any) for a custom field named `color` on `obj`.
|
||||
|
||||
If you need to use the config context data in an export template, you'll should use the function `get_config_context` to get all the config context data. For example:
|
||||
|
||||
```
|
||||
{% for server in queryset %}
|
||||
{% set data = server.get_config_context() %}
|
||||
|
@ -1,12 +1,14 @@
|
||||
# Release Checklist
|
||||
|
||||
This documentation describes the process of packaging and publishing a new NetBox release. There are three types of release:
|
||||
This documentation describes the process of packaging and publishing a new NetBox release. There are three types of releases:
|
||||
|
||||
* Major release (e.g. v3.7.8 to v4.0.0)
|
||||
* Minor release (e.g. v4.0.10 to v4.1.0)
|
||||
* Patch release (e.g. v4.1.0 to v4.1.1)
|
||||
|
||||
While major releases generally introduce some very substantial change to the application, they are typically treated the same as minor version increments for the purpose of release packaging.
|
||||
While major releases generally introduce some very substantial changes to the application, they are typically treated the same as minor version increments for the purpose of release packaging.
|
||||
|
||||
For patch releases (e.g. upgrading from v4.2.2 to v4.2.3), begin at the [patch releases](#patch-releases) heading below. For minor or major releases, complete the entire checklist.
|
||||
|
||||
## Minor Version Releases
|
||||
|
||||
@ -29,6 +31,29 @@ Close the [release milestone](https://github.com/netbox-community/netbox/milesto
|
||||
|
||||
Check that a link to the release notes for the new version is present in the navigation menu (defined in `mkdocs.yml`), and that a summary of all major new features has been added to `docs/index.md`.
|
||||
|
||||
### Update the Dependency Requirements Matrix
|
||||
|
||||
For every minor release, update the dependency requirements matrix in `docs/installation/upgrading.md` ("All versions") to reflect the supported versions of Python, PostgreSQL, and Redis:
|
||||
|
||||
1. Add a new row with the supported dependency versions.
|
||||
2. Include a documentation link using the release tag format: `https://github.com/netbox-community/netbox/blob/v4.2.0/docs/installation/index.md`
|
||||
3. Bold any version changes for clarity.
|
||||
|
||||
**Example Update:**
|
||||
|
||||
```markdown
|
||||
| NetBox Version | Python min | Python max | PostgreSQL min | Redis min | Documentation |
|
||||
|:--------------:|:----------:|:----------:|:--------------:|:---------:|:-------------------------------------------------------------------------------------------------:|
|
||||
| 4.2 | 3.10 | 3.12 | **13** | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v4.2.0/docs/installation/index.md) |
|
||||
```
|
||||
|
||||
### Update System Requirements
|
||||
|
||||
If a new Django release is adopted or other major dependencies (Python, PostgreSQL, Redis) change:
|
||||
|
||||
* Update the installation guide (`docs/installation/index.md`) with the new minimum versions.
|
||||
* Update the upgrade guide (`docs/installation/upgrading.md`) for the current version accordingly.
|
||||
|
||||
### Manually Perform a New Install
|
||||
|
||||
Start the documentation server and navigate to the current version of the installation docs:
|
||||
@ -37,15 +62,25 @@ Start the documentation server and navigate to the current version of the instal
|
||||
mkdocs serve
|
||||
```
|
||||
|
||||
Follow these instructions to perform a new installation of NetBox in a temporary environment. This process must not be automated: The goal of this step is to catch any errors or omissions in the documentation, and ensure that it is kept up-to-date for each release. Make any necessary changes to the documentation before proceeding with the release.
|
||||
Follow these instructions to perform a new installation of NetBox in a temporary environment. This process must not be automated: The goal of this step is to catch any errors or omissions in the documentation and ensure that it is kept up to date for each release. Make any necessary changes to the documentation before proceeding with the release.
|
||||
|
||||
### Test Upgrade Paths
|
||||
|
||||
Upgrading from a previous version typically involves database migrations, which must work without errors. Supported upgrade paths include from one minor version to another within the same major version (i.e. 4.0 to 4.1), as well as from the latest patch version of the previous minor version (i.e. 3.7 to 4.0 or to 4.1). Prior to release, test all these supported paths by loading demo data from the source version and performing a `./manage.py migrate`.
|
||||
Upgrading from a previous version typically involves database migrations, which must work without errors.
|
||||
Test the following supported upgrade paths:
|
||||
|
||||
- From one minor version to another within the same major version (e.g. 4.0 to 4.1).
|
||||
- From the latest patch version of the previous minor version (e.g. 3.7 to 4.0 or 4.1).
|
||||
|
||||
Prior to release, test all these supported paths by loading demo data from the source version and performing:
|
||||
|
||||
```no-highlight
|
||||
./manage.py migrate
|
||||
```
|
||||
|
||||
### Merge the `feature` Branch
|
||||
|
||||
Submit a pull request to merge the `feature` branch into the `main` branch in preparation for its release. Once it has been merged, continue with the section for patch releases below.
|
||||
Submit a pull request to merge the `feature` branch into the `main` branch in preparation for its release. Once it has been merged, continue with the section for the patch releases below.
|
||||
|
||||
### Rebuild Demo Data (After Release)
|
||||
|
||||
@ -57,7 +92,7 @@ After the release of a new minor version, generate a new demo data snapshot comp
|
||||
|
||||
### Create a Release Branch
|
||||
|
||||
Begin by creating a new branch (based off of `main`) to effect the release. This will comprise the changes listed below.
|
||||
Begin by creating a new branch (based on `main`) to effect the release. This will comprise the changes listed below.
|
||||
|
||||
```
|
||||
git checkout main
|
||||
@ -85,7 +120,20 @@ In cases where upgrading a dependency to its most recent release is breaking, it
|
||||
|
||||
### Update UI Dependencies
|
||||
|
||||
Check whether any UI dependencies (JavaScript packages, fonts, etc.) need to be updated by running `yarn outdated` from within the `project-static/` directory. [Upgrade these dependencies](./web-ui.md#updating-dependencies) as necessary, then run `yarn bundle` to generate the necessary files for distribution.
|
||||
Check whether any UI dependencies (JavaScript packages, fonts, etc.) need to be updated by running `yarn outdated` from within the `project-static/` directory. [Upgrade these dependencies](./web-ui.md#updating-dependencies) as necessary, then run `yarn bundle` to generate the necessary files for distribution:
|
||||
|
||||
```
|
||||
$ yarn bundle
|
||||
yarn run v1.22.19
|
||||
$ node bundle.js
|
||||
✅ Bundled source file 'styles/external.scss' to 'netbox-external.css'
|
||||
✅ Bundled source file 'styles/netbox.scss' to 'netbox.css'
|
||||
✅ Bundled source file 'styles/svg/rack_elevation.scss' to 'rack_elevation.css'
|
||||
✅ Bundled source file 'styles/svg/cable_trace.scss' to 'cable_trace.css'
|
||||
✅ Bundled source file 'index.ts' to 'netbox.js'
|
||||
✅ Copied graphiql files
|
||||
Done in 1.00s.
|
||||
```
|
||||
|
||||
### Rebuild the Device Type Definition Schema
|
||||
|
||||
@ -102,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
|
||||
tx pull --force
|
||||
```
|
||||
|
||||
Then, compile these portable (`.po`) files for use in the application:
|
||||
@ -116,9 +164,12 @@ Then, compile these portable (`.po`) files for use in the application:
|
||||
|
||||
### Update Version and Changelog
|
||||
|
||||
* Update the version and published date in `release.yaml` with the current version & date. Add a designation (e.g.g `beta1`) if applicable.
|
||||
* Update the 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/`.
|
||||
* Replace the "FUTURE" placeholder in the release notes with the current date.
|
||||
* Add a section for this release at the top of the changelog page for the minor version (e.g. `docs/release-notes/version-4.2.md`) listing all relevant changes made in this release.
|
||||
|
||||
!!! tip
|
||||
Put yourself in the shoes of the user when recording change notes. Focus on the effect that each change has for the end user, rather than the specific bits of code that were modified in a PR. Ensure that each message conveys meaning absent context of the initial feature request or bug report. Remember to include keywords or phrases (such as exception names) that can be easily searched.
|
||||
|
||||
### Submit a Pull Request
|
||||
|
||||
@ -126,6 +177,9 @@ Commit the above changes and submit a pull request titled **"Release vX.Y.Z"** t
|
||||
|
||||
Once CI has completed and a colleague has reviewed the PR, merge it. This effects a new release in the `main` branch.
|
||||
|
||||
!!! warning
|
||||
To ensure a streamlined review process, the pull request for a release **must** be limited to the changes outlined in this document. A release PR must never include functional changes to the application: Any unrelated "cleanup" needs to be captured in a separate PR prior to the release being shipped.
|
||||
|
||||
### Create a New Release
|
||||
|
||||
Create a [new release](https://github.com/netbox-community/netbox/releases/new) on GitHub with the following parameters.
|
||||
|
@ -22,7 +22,7 @@ NetBox generally follows the [Django style guide](https://docs.djangoproject.com
|
||||
|
||||
### Linting
|
||||
|
||||
The [ruff](https://docs.astral.sh/ruff/) linter is used to enforce code style. A [pre-commit hook](./getting-started.md#3-enable-pre-commit-hooks) which runs this automatically is included with NetBox. To invoke `ruff` manually, run:
|
||||
The [ruff](https://docs.astral.sh/ruff/) linter is used to enforce code style, and is run automatically by [pre-commit](./getting-started.md#5-install-pre-commit). To invoke `ruff` manually, run:
|
||||
|
||||
```
|
||||
ruff check netbox/
|
||||
|
@ -30,13 +30,13 @@ To download translated strings automatically, you'll need to:
|
||||
1. Install the [Transifex CLI client](https://github.com/transifex/cli)
|
||||
2. Generate a [Transifex API token](https://app.transifex.com/user/settings/api/)
|
||||
|
||||
Once you have the client set up, run the following command:
|
||||
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
|
||||
TX_TOKEN=$TOKEN tx pull --force
|
||||
```
|
||||
|
||||
This will download all portable (`.po`) translation files from Transifex, updating them locally as needed.
|
||||
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.)
|
||||
|
||||
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:
|
||||
|
||||
@ -46,6 +46,9 @@ Once retrieved, the updated strings need to be compiled into new `.mo` files so
|
||||
|
||||
Once any new `.mo` files have been generated, they need to be committed and pushed back up to GitHub. (Again, this is typically done as part of publishing a new NetBox release.)
|
||||
|
||||
!!! tip
|
||||
Run `git status` to check that both `*.mo` & `*.po` files have been updated as expected.
|
||||
|
||||
## Proposing New Languages
|
||||
|
||||
If you'd like to add support for a new language to NetBox, the first step is to [submit a GitHub issue](https://github.com/netbox-community/netbox/issues/new?assignees=&labels=type%3A+translation&projects=&template=translation.yaml) to capture the proposal. While we'd like to add as many languages as possible, we do need to limit the rate at which new languages are added. New languages will be selected according to community interest and the number of volunteers who sign up as translators.
|
||||
|
@ -7,32 +7,10 @@ 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
|
||||
```
|
||||
```no-highlight
|
||||
sudo apt update
|
||||
sudo apt install -y postgresql
|
||||
```
|
||||
|
||||
Before continuing, verify that you have installed PostgreSQL 14 or later:
|
||||
|
||||
|
@ -4,18 +4,9 @@
|
||||
|
||||
[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
|
||||
```
|
||||
```no-highlight
|
||||
sudo apt install -y redis-server
|
||||
```
|
||||
|
||||
Before continuing, verify that your installed version of Redis is at least v4.0:
|
||||
|
||||
|
@ -9,17 +9,11 @@ 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
|
||||
```
|
||||
|
||||
=== "CentOS"
|
||||
|
||||
```no-highlight
|
||||
sudo yum install -y gcc libxml2-devel libxslt-devel libffi-devel libpq-devel openssl-devel redhat-rpm-config
|
||||
```
|
||||
```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
|
||||
```
|
||||
|
||||
Before continuing, check that your installed Python version is at least 3.10:
|
||||
|
||||
@ -55,17 +49,9 @@ 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
|
||||
```
|
||||
```no-highlight
|
||||
sudo apt install -y git
|
||||
```
|
||||
|
||||
Next, clone the git repository:
|
||||
|
||||
@ -97,24 +83,12 @@ 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/
|
||||
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/
|
||||
```
|
||||
```
|
||||
sudo adduser --system --group 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
|
||||
|
||||
@ -250,7 +224,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
|
||||
* Run database schema migrations (skip with `--readonly`)
|
||||
* Builds the documentation locally (for offline use)
|
||||
* Aggregate static resource files on disk
|
||||
|
||||
@ -270,6 +244,9 @@ 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:
|
||||
|
@ -6,18 +6,10 @@ 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:
|
||||
|
@ -1,11 +1,18 @@
|
||||
# Installation
|
||||
|
||||
!!! 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.
|
||||
<div class="grid cards" markdown>
|
||||
|
||||
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.
|
||||
- :material-clock-fast:{ .lg .middle } __Eager to Get Started?__
|
||||
|
||||
<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>
|
||||
---
|
||||
|
||||
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.
|
||||
|
||||
The following sections detail how to set up a new instance of NetBox:
|
||||
|
||||
|
@ -23,6 +23,45 @@ NetBox requires the following dependencies:
|
||||
| PostgreSQL | 14+ |
|
||||
| Redis | 4.0+ |
|
||||
|
||||
### Version History
|
||||
|
||||
| NetBox Version | Python min | Python max | PostgreSQL min | Redis min | Documentation |
|
||||
|:--------------:|:----------:|:----------:|:--------------:|:---------:|:-------------------------------------------------------------------------------------------------:|
|
||||
| 4.3 | 3.10 | 3.12 | 14 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v4.3.0/docs/installation/index.md) |
|
||||
| 4.2 | 3.10 | 3.12 | 13 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v4.2.0/docs/installation/index.md) |
|
||||
| 4.1 | 3.10 | 3.12 | 12 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v4.1.0/docs/installation/index.md) |
|
||||
| 4.0 | 3.10 | 3.12 | 12 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v4.0.0/docs/installation/index.md) |
|
||||
| 3.7 | 3.8 | 3.11 | 12 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.7.0/docs/installation/index.md) |
|
||||
| 3.6 | 3.8 | 3.11 | 12 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.6.0/docs/installation/index.md) |
|
||||
| 3.5 | 3.8 | 3.10 | 11 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.5.0/docs/installation/index.md) |
|
||||
| 3.4 | 3.8 | 3.10 | 11 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.4.0/docs/installation/index.md) |
|
||||
| 3.3 | 3.8 | 3.10 | 10 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.3.0/docs/installation/index.md) |
|
||||
| 3.2 | 3.8 | 3.10 | 10 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.2.0/docs/installation/index.md) |
|
||||
| 3.1 | 3.7 | 3.9 | 10 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.1.0/docs/installation/index.md) |
|
||||
| 3.0 | 3.7 | 3.9 | 9.6 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.0.0/docs/installation/index.md) |
|
||||
| 2.11 | 3.6 | 3.9 | 9.6 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v2.11.0/docs/installation/index.md) |
|
||||
| 2.10 | 3.6 | 3.8 | 9.6 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v2.10.0/docs/installation/index.md) |
|
||||
| 2.9 | 3.6 | 3.8 | 9.5 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v2.9.0/docs/installation/index.md) |
|
||||
| 2.8 | 3.6 | 3.8 | 9.5 | 3.4 | [Link](https://github.com/netbox-community/netbox/blob/v2.8.0/docs/installation/index.md) |
|
||||
| 2.7 | 3.5 | 3.7 | 9.4 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.7.0/docs/installation/index.md) |
|
||||
| 2.6 | 3.5 | 3.7 | 9.4 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.6.0/docs/installation/index.md) |
|
||||
| 2.5 | 3.5 | 3.7 | 9.4 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.5.0/docs/installation/index.md) |
|
||||
| 2.4 | 3.4 | 3.7 | 9.4 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.4.0/docs/installation/index.md) |
|
||||
| 2.3 | 2.7 | 3.6 | 9.4 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.3.0/docs/installation/postgresql.md) |
|
||||
| 2.2 | 2.7 | 3.6 | 9.4 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.2.0/docs/installation/postgresql.md) |
|
||||
| 2.1 | 2.7 | 3.6 | 9.3 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.1.0/docs/installation/postgresql.md) |
|
||||
| 2.0 | 2.7 | 3.6 | 9.3 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.0.0/docs/installation/postgresql.md) |
|
||||
| 1.9 | 2.7 | 3.5 | 9.2 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.9.0-r1/docs/installation/postgresql.md) |
|
||||
| 1.8 | 2.7 | 3.5 | 9.2 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.8.0/docs/installation/postgresql.md) |
|
||||
| 1.7 | 2.7 | 3.5 | 9.2 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.7.0/docs/installation/postgresql.md) |
|
||||
| 1.6 | 2.7 | 3.5 | 9.2 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.6.0/docs/installation/postgresql.md) |
|
||||
| 1.5 | 2.7 | 3.5 | 9.2 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.5.0/docs/installation/postgresql.md) |
|
||||
| 1.4 | 2.7 | 3.5 | 9.1 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.4.0/docs/installation/postgresql.md) |
|
||||
| 1.3 | 2.7 | 3.5 | 9.1 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.3.0/docs/installation/postgresql.md) |
|
||||
| 1.2 | 2.7 | 3.5 | 9.1 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.2.0/docs/installation/postgresql.md) |
|
||||
| 1.1 | 2.7 | 3.5 | 9.1 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.1.0/docs/getting-started.md) |
|
||||
| 1.0 | 2.7 | 3.5 | 9.1 | - | [Link](https://github.com/netbox-community/netbox/blob/1.0.0/docs/getting-started.md) |
|
||||
|
||||
## 3. Install the Latest Release
|
||||
|
||||
As with the initial installation, you can upgrade NetBox by either downloading the latest release package or by checking out the latest production release from the git repository.
|
||||
@ -83,17 +122,19 @@ sudo cp /opt/netbox-$OLDVER/gunicorn.py /opt/netbox/
|
||||
|
||||
### Option B: Check Out a Git Release
|
||||
|
||||
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:
|
||||
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 command:
|
||||
|
||||
```
|
||||
sudo git fetch --tags
|
||||
git describe --tags $(git rev-list --tags --max-count=1)
|
||||
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/||'
|
||||
```
|
||||
|
||||
Check out the desired release by specifying its tag:
|
||||
Check out the desired release by specifying its tag. For example:
|
||||
|
||||
```
|
||||
sudo git checkout v4.2.0
|
||||
sudo git checkout v4.2.7
|
||||
```
|
||||
|
||||
## 4. Run the Upgrade Script
|
||||
@ -111,6 +152,9 @@ 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
|
||||
|
@ -127,8 +127,8 @@ Certain queries can return multiple types of objects, for example cable terminat
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
The field "class_type" is an easy way to distinguish what type of object it is when viewing the returned data, or when filtering. It contains the class name, for example "CircuitTermination" or "ConsoleServerPort".
|
||||
|
||||
## Pagination
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
## What is a REST API?
|
||||
|
||||
REST stands for [representational state transfer](https://en.wikipedia.org/wiki/Representational_state_transfer). It's a particular type of API which employs HTTP requests and [JavaScript Object Notation (JSON)](https://www.json.org/) to facilitate create, retrieve, update, and delete (CRUD) operations on objects within an application. Each type of operation is associated with a particular HTTP verb:
|
||||
REST stands for [representational state transfer](https://en.wikipedia.org/wiki/REST). It's a particular type of API which employs HTTP requests and [JavaScript Object Notation (JSON)](https://www.json.org/) to facilitate create, retrieve, update, and delete (CRUD) operations on objects within an application. Each type of operation is associated with a particular HTTP verb:
|
||||
|
||||
* `GET`: Retrieve an object or list of objects
|
||||
* `POST`: Create an object
|
||||
@ -217,26 +217,34 @@ 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 an IP address 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 a prefix looks like this:
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
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",
|
||||
"site": {
|
||||
"id": 3,
|
||||
"url": "http://netbox/api/dcim/sites/17/",
|
||||
"name": "Site 23A",
|
||||
"slug": "site-23a"
|
||||
},
|
||||
"vrf": null,
|
||||
"scope_type": "dcim.site",
|
||||
"scope_id": 3,
|
||||
"scope": {
|
||||
"id": 3,
|
||||
"url": "http://netbox/api/dcim/sites/3/",
|
||||
"display": "Site 23A",
|
||||
"name": "Site 23A",
|
||||
"slug": "site-23a",
|
||||
"description": ""
|
||||
},
|
||||
"tenant": null,
|
||||
"vlan": null,
|
||||
"status": {
|
||||
@ -250,24 +258,36 @@ GET /api/ipam/prefixes/13980/
|
||||
"slug": "staging"
|
||||
},
|
||||
"is_pool": false,
|
||||
"mark_utilized": false,
|
||||
"description": "Example prefix",
|
||||
"comments": "",
|
||||
"tags": [],
|
||||
"custom_fields": {},
|
||||
"created": "2018-12-10",
|
||||
"last_updated": "2019-03-01T20:02:46.173540Z"
|
||||
"created": "2025-03-01T20:01:23.458302Z",
|
||||
"last_updated": "2025-03-01T20:02:46.173540Z",
|
||||
"children": 0,
|
||||
"_depth": 0
|
||||
}
|
||||
```
|
||||
|
||||
The brief format is much more terse:
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
GET /api/ipam/prefixes/13980/?brief=1
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 13980,
|
||||
"url": "http://netbox/api/ipam/prefixes/13980/",
|
||||
"family": 4,
|
||||
"prefix": "10.40.3.0/24"
|
||||
"display": "192.0.2.0/24",
|
||||
"family": {
|
||||
"value": 4,
|
||||
"label": "IPv4"
|
||||
},
|
||||
"prefix": "192.0.2.0/24",
|
||||
"description": "Example prefix",
|
||||
"_depth": 0
|
||||
}
|
||||
```
|
||||
|
||||
@ -400,25 +420,31 @@ 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", "site": 6}' | jq '.'
|
||||
--data '{"prefix": "192.0.2.0/24", "scope_type": "dcim.site", "scope_id": 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",
|
||||
"site": {
|
||||
"vrf": null,
|
||||
"scope_type": "dcim.site",
|
||||
"scope_id": 6,
|
||||
"scope": {
|
||||
"id": 6,
|
||||
"url": "http://netbox/api/dcim/sites/6/",
|
||||
"display": "US-East 4",
|
||||
"name": "US-East 4",
|
||||
"slug": "us-east-4"
|
||||
"slug": "us-east-4",
|
||||
"description": ""
|
||||
},
|
||||
"vrf": null,
|
||||
"tenant": null,
|
||||
"vlan": null,
|
||||
"status": {
|
||||
@ -427,11 +453,15 @@ http://netbox/api/ipam/prefixes/ \
|
||||
},
|
||||
"role": null,
|
||||
"is_pool": false,
|
||||
"mark_utilized": false,
|
||||
"description": "",
|
||||
"comments": "",
|
||||
"tags": [],
|
||||
"custom_fields": {},
|
||||
"created": "2020-08-04",
|
||||
"last_updated": "2020-08-04T20:08:39.007125Z"
|
||||
"created": "2025-04-29T15:44:47.597092Z",
|
||||
"last_updated": "2025-04-29T15:44:47.597092Z",
|
||||
"children": 0,
|
||||
"_depth": 0
|
||||
}
|
||||
```
|
||||
|
||||
@ -490,18 +520,24 @@ 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",
|
||||
"site": {
|
||||
"vrf": null,
|
||||
"scope_type": "dcim.site",
|
||||
"scope_id": 6,
|
||||
"scope": {
|
||||
"id": 6,
|
||||
"url": "http://netbox/api/dcim/sites/6/",
|
||||
"display": "US-East 4",
|
||||
"name": "US-East 4",
|
||||
"slug": "us-east-4"
|
||||
"slug": "us-east-4",
|
||||
"description": ""
|
||||
},
|
||||
"vrf": null,
|
||||
"tenant": null,
|
||||
"vlan": null,
|
||||
"status": {
|
||||
@ -510,11 +546,15 @@ http://netbox/api/ipam/prefixes/18691/ \
|
||||
},
|
||||
"role": null,
|
||||
"is_pool": false,
|
||||
"mark_utilized": false,
|
||||
"description": "",
|
||||
"comments": "",
|
||||
"tags": [],
|
||||
"custom_fields": {},
|
||||
"created": "2020-08-04",
|
||||
"last_updated": "2020-08-04T20:14:55.709430Z"
|
||||
"created": "2025-04-29T15:44:47.597092Z",
|
||||
"last_updated": "2025-04-29T15:49:40.689109Z",
|
||||
"children": 0,
|
||||
"_depth": 0
|
||||
}
|
||||
```
|
||||
|
||||
@ -568,6 +608,23 @@ 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.
|
||||
@ -653,6 +710,7 @@ 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,
|
||||
|
@ -6,6 +6,8 @@ Devices can be organized by functional roles, which are fully customizable by th
|
||||
|
||||
### Parent
|
||||
|
||||
!!! info "This field was introduced in NetBox v4.3."
|
||||
|
||||
The parent role of which this role is a child (optional).
|
||||
|
||||
### Name
|
||||
|
@ -42,6 +42,8 @@ The number of the numerically lowest unit in the rack. This value defaults to on
|
||||
|
||||
The external width, height and depth of the rack can be tracked to aid in floorplan calculations. These measurements must be designated in either millimeters or inches.
|
||||
|
||||
!!! info "The `outer_height` field was introduced in NetBox v4.3."
|
||||
|
||||
### Mounting Depth
|
||||
|
||||
The maximum depth of a mounted device that the rack can accommodate, in millimeters. For four-post frames or cabinets, this is the horizontal distance between the front and rear vertical rails. (Note that this measurement does _not_ include space between the rails and the cabinet doors.)
|
||||
|
43
docs/models/extras/tableconfig.md
Normal file
43
docs/models/extras/tableconfig.md
Normal file
@ -0,0 +1,43 @@
|
||||
# Table Configs
|
||||
|
||||
This object represents the saved configuration of an object table in NetBox. Table configs can be crafted, saved, and shared among users to apply specific views within object lists. Each table config can specify which table columns to display, the order in which to display them, and which columns are used for sorting.
|
||||
|
||||
For example, you might wish to create a table config for the devices list to assist in inventory tasks. This view might show the device name, location, serial number, and asset tag, but omit operational details like IP addresses. Once applied, this table config can be saved for reuse in future audits.
|
||||
|
||||
## Fields
|
||||
|
||||
### Name
|
||||
|
||||
A human-friendly name for the table config.
|
||||
|
||||
### User
|
||||
|
||||
The user to which this filter belongs. The current user will be assigned automatically when saving a table config via the UI, and cannot be changed.
|
||||
|
||||
### Object Type
|
||||
|
||||
The type of NetBox object to which the table config pertains.
|
||||
|
||||
### Table
|
||||
|
||||
The name of the specific table to which the table config pertains. (Some NetBox object use multiple tables.)
|
||||
|
||||
### Weight
|
||||
|
||||
A numeric weight used to influence the order in which table configs are listed. Table configs with a lower weight will be listed before those with a higher weight. Table configs having the same weight will be ordered alphabetically.
|
||||
|
||||
### Enabled
|
||||
|
||||
Determines whether this table config can be used. Disabled table configs will not appear as options in the UI, however they will be included in API results.
|
||||
|
||||
### Shared
|
||||
|
||||
Determines whether this table config is intended for use by all users or only its owner. Note that deselecting this option does **not** hide the table config from other users; it is merely excluded from the list of available table configs in UI object list views.
|
||||
|
||||
### Ordering
|
||||
|
||||
A list of column names by which the table is to be ordered. If left blank, the table's default ordering will be used.
|
||||
|
||||
### Columns
|
||||
|
||||
A list of columns to be displayed in the table. The table will render these columns in the order they appear in the list. At least one column must be selected.
|
@ -6,6 +6,15 @@ To aid in the efficient creation of services, users may opt to first create a [s
|
||||
|
||||
## Fields
|
||||
|
||||
### Parent
|
||||
|
||||
The parent object to which the service is assigned. This must be one of [Device](../dcim/device.md),
|
||||
[VirtualMachine](../virtualization/virtualmachine.md), or [FHRP Group](./fhrpgroup.md).
|
||||
|
||||
!!! note "Changed in NetBox v4.3"
|
||||
|
||||
Previously, `parent` was a property that pointed to either a Device or Virtual Machine. With the capability to assign services to FHRP groups, this is a unified in a concrete field.
|
||||
|
||||
### Name
|
||||
|
||||
A service or protocol name.
|
||||
|
@ -61,6 +61,11 @@ class MyModelViewSet(...):
|
||||
|
||||
The `TagFilter` class is available for all models which support tag assignment (those which inherit from `NetBoxModel` or `TagsMixin`). This filter subclasses django-filter's `ModelMultipleChoiceFilter` to work with NetBox's `TaggedItem` class.
|
||||
|
||||
This class filters `tags` using the `slug` field. For example:
|
||||
|
||||
`GET /api/dcim/sites/?tag=alpha&tag=bravo`
|
||||
|
||||
|
||||
```python
|
||||
from django_filters import FilterSet
|
||||
from extras.filters import TagFilter
|
||||
@ -68,3 +73,19 @@ from extras.filters import TagFilter
|
||||
class MyModelFilterSet(FilterSet):
|
||||
tag = TagFilter()
|
||||
```
|
||||
|
||||
### TagIDFilter
|
||||
|
||||
The `TagIDFilter` class is available for all models which support tag assignment (those which inherit from `NetBoxModel` or `TagsMixin`). This filter subclasses django-filter's `ModelMultipleChoiceFilter` to work with NetBox's `TaggedItem` class.
|
||||
|
||||
This class filters `tags` using the `id` field. For example:
|
||||
|
||||
`GET /api/dcim/sites/?tag_id=100&tag_id=200`
|
||||
|
||||
```python
|
||||
from django_filters import FilterSet
|
||||
from extras.filters import TagIDFilter
|
||||
|
||||
class MyModelFilterSet(FilterSet):
|
||||
tag_id = TagIDFilter()
|
||||
```
|
||||
|
@ -205,6 +205,7 @@ To ease development, it is recommended to go ahead and install the plugin at thi
|
||||
```no-highlight
|
||||
$ pip install -e .
|
||||
```
|
||||
|
||||
More information on editable builds can be found at [Editable installs for pyproject.toml ](https://peps.python.org/pep-0660/).
|
||||
|
||||
## Configure NetBox
|
||||
|
@ -119,6 +119,8 @@ For more information about database migrations, see the [Django documentation](h
|
||||
|
||||
::: netbox.models.features.ContactsMixin
|
||||
|
||||
!!! info "Plugin support for ContactsMixin was introduced in NetBox v4.3."
|
||||
|
||||
::: netbox.models.features.CustomLinksMixin
|
||||
|
||||
::: netbox.models.features.CustomFieldsMixin
|
||||
@ -127,9 +129,6 @@ For more information about database migrations, see the [Django documentation](h
|
||||
|
||||
::: netbox.models.features.EventRulesMixin
|
||||
|
||||
!!! note
|
||||
`EventRulesMixin` was renamed from `WebhooksMixin` in NetBox v3.7.
|
||||
|
||||
::: netbox.models.features.ExportTemplatesMixin
|
||||
|
||||
::: netbox.models.features.JobsMixin
|
||||
|
@ -10,6 +10,15 @@ Minor releases are published in April, August, and December of each calendar yea
|
||||
|
||||
This page contains a history of all major and minor releases since NetBox v2.0. For more detail on a specific patch release, please see the release notes page for that specific minor release.
|
||||
|
||||
#### [Version 4.3](./version-4.3.md) (May 2025)
|
||||
|
||||
* Module Type Profiles & Custom Attributes ([#19002](https://github.com/netbox-community/netbox/issues/19002))
|
||||
* Reusable Table Configurations ([#14591](https://github.com/netbox-community/netbox/issues/14591))
|
||||
* Option to Treat IP Ranges as Fully Populated ([#9763](https://github.com/netbox-community/netbox/issues/9763))
|
||||
* Hierarchical Device Roles ([#18245](https://github.com/netbox-community/netbox/issues/18245))
|
||||
* Periodic Synchronization of Data Sources ([#18287](https://github.com/netbox-community/netbox/issues/18287))
|
||||
* Proxy Routing ([#18627](https://github.com/netbox-community/netbox/issues/18627))
|
||||
|
||||
#### [Version 4.2](./version-4.2.md) (January 2025)
|
||||
|
||||
* Assign Multiple MAC Addresses per Interface ([#4867](https://github.com/netbox-community/netbox/issues/4867))
|
||||
|
@ -150,5 +150,5 @@ The [NAPALM automation](https://github.com/napalm-automation/napalm) library pro
|
||||
* Modified the interface serializer to include three discrete fields relating to connections: `is_connected` (boolean), `interface_connection`, and `circuit_termination`
|
||||
* Added two new fields to the inventory item serializer: `asset_tag` and `description`
|
||||
* Added "wireless" to interface type filter (in addition to physical, virtual, and LAG)
|
||||
* Added a new endpoint at /api/ipam/prefixes/<pk>/available-ips/ to retrieve or create available IPs within a prefix
|
||||
* Added a new endpoint at /api/ipam/prefixes/<pk\>/available-ips/ to retrieve or create available IPs within a prefix
|
||||
* Extended `parent_device` on DeviceSerializer to include the `url` and `display_name` of the parent Device, and the `url` of the DeviceBay
|
||||
|
@ -1,5 +1,193 @@
|
||||
# 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
|
||||
|
||||
* [#16144](https://github.com/netbox-community/netbox/issues/16144) - Add support for plugin models to GetReturnURLMixin
|
||||
* [#18138](https://github.com/netbox-community/netbox/issues/18138) - Enable filtering of ObjectVar and MultiObjectVar input selections for custom fields
|
||||
* [#18656](https://github.com/netbox-community/netbox/issues/18656) - Enable FHRP group assignment when bulk importing IP addresses
|
||||
* [#18980](https://github.com/netbox-community/netbox/issues/18980) - Optimize bulk updates of custom field values when custom fields are added/removed
|
||||
* [#19018](https://github.com/netbox-community/netbox/issues/19018) - Add MoCA interface type
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#18553](https://github.com/netbox-community/netbox/issues/18553) - Avoid clearing site of assigned virtual machines when editing a cluster
|
||||
* [#18738](https://github.com/netbox-community/netbox/issues/18738) - Respect declared ordering of custom scripts within a module
|
||||
* [#18895](https://github.com/netbox-community/netbox/issues/18895) - Fix GraphQL support for interfaces which terminate virtual circuits
|
||||
* [#18904](https://github.com/netbox-community/netbox/issues/18904) - Add missing tags column to config contexts table
|
||||
* [#18964](https://github.com/netbox-community/netbox/issues/18964) - Fix "select all" behavior on object lists
|
||||
* [#18965](https://github.com/netbox-community/netbox/issues/18965) - "Run script" button should respect default commit toggle for custom scripts
|
||||
* [#18991](https://github.com/netbox-community/netbox/issues/18991) - Fix cable path tracing for pass-through ports in REST API
|
||||
* [#18999](https://github.com/netbox-community/netbox/issues/18999) - Fix filtering of inventory items with no manufacturer in GraphQL API
|
||||
* [#19021](https://github.com/netbox-community/netbox/issues/19021) - Preserve JSONField stylign when `help_text` is passed
|
||||
* [#19023](https://github.com/netbox-community/netbox/issues/19023) - `get_field_value()` should honor null values on bound form fields
|
||||
* [#19030](https://github.com/netbox-community/netbox/issues/19030) - Prevent pagination buttons from overlapping bulk action buttons on object lists
|
||||
* [#19041](https://github.com/netbox-community/netbox/issues/19041) - Fix `IndexError` exception when creating multiple front ports with a label
|
||||
* [#19092](https://github.com/netbox-community/netbox/issues/19092) - Fix clearing of scope field when bulk editing prefixes
|
||||
* [#19122](https://github.com/netbox-community/netbox/issues/19122) - Fix styling of server error page
|
||||
|
||||
---
|
||||
|
||||
## v4.2.6 (2025-03-21)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#17503](https://github.com/netbox-community/netbox/issues/17503) - Add rack title above rack on rack detail view
|
||||
* [#17686](https://github.com/netbox-community/netbox/issues/17686) - Add config option for disk space divisor
|
||||
* [#18579](https://github.com/netbox-community/netbox/issues/18579) - Update filtersets and filter forms to include contact filters where missing
|
||||
* [#18744](https://github.com/netbox-community/netbox/issues/18744) - Ensure contact link in tables is hyperlinked
|
||||
* [#18816](https://github.com/netbox-community/netbox/issues/18816) - Add FC/UPC, FC/APC and FC/PC port types
|
||||
* [#18880](https://github.com/netbox-community/netbox/issues/18880) - Delay enqueuing background tasks until DB transaction is committed to avoid race condition
|
||||
* [#18939](https://github.com/netbox-community/netbox/issues/18939) - Support site group search for ASNs
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#18409](https://github.com/netbox-community/netbox/issues/18409) - Eliminate N+1 issue by adding generic prefetch operation to Interface API endpoint
|
||||
* [#18557](https://github.com/netbox-community/netbox/issues/18557) - Update JSONField to enclose bare string values in quotes
|
||||
* [#18582](https://github.com/netbox-community/netbox/issues/18582) - Fix prefix bulk import with associated VLAN and conflicting VLAN IDs
|
||||
* [#18742](https://github.com/netbox-community/netbox/issues/18742) - Ensure location list and detail views show related VLAN group information
|
||||
* [#18782](https://github.com/netbox-community/netbox/issues/18782) - Ensure misconfigured object list widgets on the dashboard now degrade gracefully
|
||||
* [#18833](https://github.com/netbox-community/netbox/issues/18833) - Fix inventory item bulk edit to ensure that component name and type are both validated Ensure
|
||||
* [#18838](https://github.com/netbox-community/netbox/issues/18838) - Ensure that local context data correctly rejects falsy values
|
||||
* [#18845](https://github.com/netbox-community/netbox/issues/18845) - Restore default sort behavior of name column on devices list view
|
||||
* [#18863](https://github.com/netbox-community/netbox/issues/18863) - Exempt MPTT-based models from ordering fix introduced in #18279
|
||||
* [#18869](https://github.com/netbox-community/netbox/issues/18869) - Ensure numeric conversion helper always return a clean decimal value
|
||||
* [#18872](https://github.com/netbox-community/netbox/issues/18872) - Ensure that `kind` is a required field when making journal entries
|
||||
* [#18884](https://github.com/netbox-community/netbox/issues/18884) - Ensure tag deserialization is handled correctly
|
||||
* [#18887](https://github.com/netbox-community/netbox/issues/18887) - Allow VM interface objects to be set on prefix object-type custom field
|
||||
* [#18926](https://github.com/netbox-community/netbox/issues/18926) - Fix icon displayed for GitHub authentication on login page
|
||||
* [#18928](https://github.com/netbox-community/netbox/issues/18928) - Support cascading deletions when cleaning up expired changelog records
|
||||
* [#18933](https://github.com/netbox-community/netbox/issues/18933) - Allow filtering VLAN groups by associated site groups
|
||||
* [#18944](https://github.com/netbox-community/netbox/issues/18944) - Ensure clearing "Widget type" field when adding widgets to dashboard does not cause a "ValueError: Unregistered widget class" error
|
||||
* [#18949](https://github.com/netbox-community/netbox/issues/18949) - Add missing contacts property to GraphQL types where the associated model has a connection to a contact
|
||||
|
||||
---
|
||||
|
||||
## v4.2.5 (2025-03-06)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#17357](https://github.com/netbox-community/netbox/issues/17357) - Use VirtualChassis name as fallback for unnamed devices
|
||||
* [#17542](https://github.com/netbox-community/netbox/issues/17542) - Add contact assignments to VPN tunnels
|
||||
* [#17944](https://github.com/netbox-community/netbox/issues/17944) - Allow script inputs to be filtered on ObjectVar and MultiObjectVar selections
|
||||
* [#18024](https://github.com/netbox-community/netbox/issues/18024) - Add permalink URL pattern to match a custom script by module and class name
|
||||
* [#18141](https://github.com/netbox-community/netbox/issues/18141) - Support "Quick Add" for plugins
|
||||
* [#18403](https://github.com/netbox-community/netbox/issues/18403) - Improve performance of job list views
|
||||
* [#18693](https://github.com/netbox-community/netbox/issues/18693) - Support setting VLAN translation on bulk edit of interfaces
|
||||
* [#18772](https://github.com/netbox-community/netbox/issues/18772) - Add "type" filter for virtual circuits
|
||||
* [#18774](https://github.com/netbox-community/netbox/issues/18774) - Add tooltip preview of tag descriptions when hovering over tags
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#15016](https://github.com/netbox-community/netbox/issues/15016) - Prevent AssertionError when adding multiple devices "mid-span" in a cable trace
|
||||
* [#15924](https://github.com/netbox-community/netbox/issues/15924) - Prevent setting tagged VLANs on interfaces with mode: tagged-all
|
||||
* [#17488](https://github.com/netbox-community/netbox/issues/17488) - Ensure VLANGroup.vid_ranges shows up in API results
|
||||
* [#17709](https://github.com/netbox-community/netbox/issues/17709) - Allow primary key for nested models in OpenAPI request schemas
|
||||
* [#17796](https://github.com/netbox-community/netbox/issues/17796) - Fix IndexError on "Create & Add Another" operation on custom field choices
|
||||
* [#18605](https://github.com/netbox-community/netbox/issues/18605) - Limit VLAN selection dropdown to choices appropriate to site
|
||||
* [#18722](https://github.com/netbox-community/netbox/issues/18722) - Improve UI feedback on failed script execution
|
||||
* [#18729](https://github.com/netbox-community/netbox/issues/18729) - Fix unpredictable ordering on querysets with annotations/groupings
|
||||
* [#18753](https://github.com/netbox-community/netbox/issues/18753) - Prevent webhooks from being triggered on a script dry-run
|
||||
* [#18758](https://github.com/netbox-community/netbox/issues/18758) - Fix FieldError when sorting by account count field in providers list
|
||||
* [#18768](https://github.com/netbox-community/netbox/issues/18768) - Fix removing a secondary MAC address from an interface
|
||||
|
||||
---
|
||||
|
||||
## v4.2.4 (2025-02-21)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#17309](https://github.com/netbox-community/netbox/issues/17309) - Omit empty counts in related object tables
|
||||
* [#18277](https://github.com/netbox-community/netbox/issues/18277) - Improve multi-table inheritance in serialization of change-logged models
|
||||
* [#18286](https://github.com/netbox-community/netbox/issues/18286) - Add more job duration choices
|
||||
* [#18357](https://github.com/netbox-community/netbox/issues/18357) - Display author name in plugin list for locally installed plugins
|
||||
* [#18408](https://github.com/netbox-community/netbox/issues/18408) - Add Paused status for virtual machines
|
||||
* [#18584](https://github.com/netbox-community/netbox/issues/18584) - Add rack type column to manufacturer list
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#17436](https://github.com/netbox-community/netbox/issues/17436) - Fix {module} replacement in module bays
|
||||
* [#18013](https://github.com/netbox-community/netbox/issues/18013) - Limit object type to selected object in change log filter
|
||||
* [#18241](https://github.com/netbox-community/netbox/issues/18241) - Default logging level of custom scripts changed to INFO
|
||||
* [#18247](https://github.com/netbox-community/netbox/issues/18247) - Fix visibility of disabled cable paths in dark mode
|
||||
* [#18480](https://github.com/netbox-community/netbox/issues/18480) - Clean data passed to script in runscript command
|
||||
* [#18555](https://github.com/netbox-community/netbox/issues/18555) - Add default get_absolute_url method to plugin models
|
||||
* [#18585](https://github.com/netbox-community/netbox/issues/18585) - Fix filtering circuits by location
|
||||
* [#18593](https://github.com/netbox-community/netbox/issues/18593) - Fix "Create & Add Another" IP Address workflow
|
||||
* [#18594](https://github.com/netbox-community/netbox/issues/18594) - Enable sorting by ASN count on site and provider lists
|
||||
* [#18619](https://github.com/netbox-community/netbox/issues/18619) - Ensure shift-click selection selects only visible list items
|
||||
* [#18674](https://github.com/netbox-community/netbox/issues/18674) - Preserve form values when selecting speed on circuit termination
|
||||
|
||||
---
|
||||
|
||||
## v4.2.3 (2025-02-04)
|
||||
|
||||
### Enhancements
|
||||
|
141
docs/release-notes/version-4.3.md
Normal file
141
docs/release-notes/version-4.3.md
Normal file
@ -0,0 +1,141 @@
|
||||
## v4.3.0-beta2 (2025-04-23)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* The GraphQL API Now uses an advanced syntax for filtering, to enable e.g. logical AND/OR filtering and custom field lookups.
|
||||
* PostgreSQL 13 is no longer supported. NetBox v4.3 requires PostgreSQL 14.0 or later.
|
||||
* The `ALLOW_TOKEN_RETRIEVAL` configuration parameter now defaults to False.
|
||||
* The `device` and `virtual_machine` foreign keys on the Service model have been replaced with a generic `parent` relationship to support the assignment of services to FHRP groups as well.
|
||||
* The `group` foreign key on the Contact model has been replaced with a many-to-many `groups` field.
|
||||
* PluginTemplateExtension no longer supports registration via the singular `model` attribute (use `models` instead).
|
||||
* The legacy staged changes functionality has been removed.
|
||||
|
||||
### New Features
|
||||
|
||||
#### Module Type Profiles & Custom Attributes ([#19002](https://github.com/netbox-community/netbox/issues/19002))
|
||||
|
||||
The new [module type profile](../models/dcim/moduletypeprofile.md) model enables users to declare custom profiles for module types, with the ability to define custom attributes for each profile according to its functional role. For example, a CPU module type might declare architecture and clock speed attributes; a hard disk profile might declare attributes for type and speed.
|
||||
|
||||
Attributes can be declared on each profile using [JSON schema](https://json-schema.org/), which allows for attributes to be declared as strings (text), integers, decimals, booleans, or choice fields. Profile attributes render as individual form fields when modifying a module type. Several profiles have been included by default to serve as examples, however these may be modified or removed.
|
||||
|
||||
#### Reusable Table Configurations ([#14591](https://github.com/netbox-community/netbox/issues/14591))
|
||||
|
||||
After modifying the displayed columns and/or ordering for a specific object table in the user interface, users now have the option to save that configuration so that it can be reused in the future. Similar to saved filters, table configs can be shared with other users to easily replicate table layouts crafted to serve specific use cases.
|
||||
|
||||
#### Option to Treat IP Ranges as Fully Populated ([#9763](https://github.com/netbox-community/netbox/issues/9763))
|
||||
|
||||
A new `mark_populated` boolean field has been added to the IPRange model. If set to true, NetBox will consider the IP range to be fully populated, and will not permit the creation of individual IP addresses within the range. For example, you might defer the management of an IP range to an external DHCP server, and wish for NetBox to treat the range as a opaque monolithic block for planning and allocation purposes.
|
||||
|
||||
#### Hierarchical Device Roles ([#18245](https://github.com/netbox-community/netbox/issues/18245))
|
||||
|
||||
Device roles can now be arranged hierarchically, with one role optionally serving as a parent to one or more child roles. For example, you might wish to create a generic "Server" role for devices with "Application Server" and "Database Server" roles beneath it. A device could then be assigned to any of these three roles.
|
||||
|
||||
#### Periodic Synchronization of Data Sources ([#18287](https://github.com/netbox-community/netbox/issues/18287))
|
||||
|
||||
Data sources can now be configured to synchronize automatically at a specified interval, as indicated by the new `sync_interval` field. No additional system configuration is necessary to support this functionality; background jobs will be scheduled automatically by the RQ worker process.
|
||||
|
||||
#### Proxy Routing ([#18627](https://github.com/netbox-community/netbox/issues/18627))
|
||||
|
||||
User can now declare one or more proxy routers via the `PROXY_ROUTERS` configuration parameter to control the use of specific proxy servers for various outbound connections. For example, it is now possible to configure NetBox to use different proxies based on the type of outbound traffic or its destination.
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#7598](https://github.com/netbox-community/netbox/issues/7598) - Adopt advanced query filtering in GraphQL API to support filtering by custom fields
|
||||
* [#8423](https://github.com/netbox-community/netbox/issues/8423) - Enable assigning services to FHRP groups
|
||||
* [#15842](https://github.com/netbox-community/netbox/issues/15842) - Introduce the `LOGIN_FORM_HIDDEN` configuration parameter
|
||||
* [#16224](https://github.com/netbox-community/netbox/issues/16224) - Implement pagination support for the GraphQL API
|
||||
* [#17170](https://github.com/netbox-community/netbox/issues/17170) - Enable the assignment of a contact to multiple contact groups
|
||||
* [#17443](https://github.com/netbox-community/netbox/issues/17443) - Add a `file_name` field to the export template model
|
||||
* [#17602](https://github.com/netbox-community/netbox/issues/17602) - Add a `comments` field to all nested group models (Region, SiteGroup, Location, ContactGroup, TenantGroup, and WirelessLANGroup)
|
||||
* [#17608](https://github.com/netbox-community/netbox/issues/17608) - Add a `status` field to the L2VPN model
|
||||
* [#17653](https://github.com/netbox-community/netbox/issues/17653) - Enable declaring Jinja environment parameters on export templates (similar to config templates)
|
||||
* [#17793](https://github.com/netbox-community/netbox/issues/17793) - Introduce a REST API endpoint for tagged objects (`/api/extras/tagged-objects/`)
|
||||
* [#17841](https://github.com/netbox-community/netbox/issues/17841) - Add a `weight` field to the Tag model to influence ordering
|
||||
* [#18296](https://github.com/netbox-community/netbox/issues/18296) - Add a `tenant` field to the VLAN group model
|
||||
* [#18352](https://github.com/netbox-community/netbox/issues/18352) - Add a `status` field to the power outlet model
|
||||
* [#18417](https://github.com/netbox-community/netbox/issues/18417) - Add an `outer_height` field to the rack & rack type models
|
||||
* [#18535](https://github.com/netbox-community/netbox/issues/18535) - The presence of incompatible plugins will no longer prevent NetBox from starting
|
||||
* [#18780](https://github.com/netbox-community/netbox/issues/18780) - Introduce `DATABASES` and `DATABASE_ROUTERS` configuration parameters to enable defining connections to external databases (e.g. for plugins)
|
||||
* [#18783](https://github.com/netbox-community/netbox/issues/18783) - Enable filtering all applicable models by tag ID
|
||||
* [#18785](https://github.com/netbox-community/netbox/issues/18785) - Enable custom choices for rack, device, and module airflow
|
||||
|
||||
### Plugins
|
||||
|
||||
* [#16630](https://github.com/netbox-community/netbox/issues/16630) - Plugins can now inject content within the HTML `<head>` block via the new `plugin_head()` method on PluginTemplateExtension
|
||||
* [#17424](https://github.com/netbox-community/netbox/issues/17424) - Extend ViewTab with a `visible` argument to control tab rendering
|
||||
* [#17857](https://github.com/netbox-community/netbox/issues/17857) - Added a `release_track` attribute to PluginConfig
|
||||
* [#18305](https://github.com/netbox-community/netbox/issues/18305) - Introduce plugin support for ContactsMixin
|
||||
* [#19073](https://github.com/netbox-community/netbox/issues/19073) - Allow installed plugins to be omitted from the plugins list
|
||||
|
||||
### Bug Fixes (From beta1)
|
||||
|
||||
* [#19213](https://github.com/netbox-community/netbox/issues/19213) - Fix rendering of dropdown selection form fields
|
||||
* [#19224](https://github.com/netbox-community/netbox/issues/19224) - Fix GraphQL API support for custom field choices
|
||||
* [#19225](https://github.com/netbox-community/netbox/issues/19225) - Restore missing GraphQL API filters
|
||||
* [#19263](https://github.com/netbox-community/netbox/issues/19263) - Render action buttons only if the record model matches the table model
|
||||
* [#19264](https://github.com/netbox-community/netbox/issues/19264) - Support table configs on child object list views
|
||||
* [#19266](https://github.com/netbox-community/netbox/issues/19266) - Fix copy-to-clipboard button for IP addresses
|
||||
|
||||
### Other Changes
|
||||
|
||||
* [#18071](https://github.com/netbox-community/netbox/issues/18071) - Removed legacy staged changed functionality in favor of the [netbox-branching](https://github.com/netboxlabs/netbox-branching) plugin
|
||||
* [#18072](https://github.com/netbox-community/netbox/issues/18072) - Drop support for the singular `model` attribute on PluginTemplateExtension (use `models` instead)
|
||||
* [#18191](https://github.com/netbox-community/netbox/issues/18191) - Remove redundant PostgreSQL indexes
|
||||
* [#18236](https://github.com/netbox-community/netbox/issues/18236) - Upgrade the HTMX library to v2.0
|
||||
* [#18540](https://github.com/netbox-community/netbox/issues/18540) - Operational plugins are now recorded in the application registry
|
||||
* [#18623](https://github.com/netbox-community/netbox/issues/18623) - Upgrade the Tabler CSS theme to v1.0
|
||||
* [#18743](https://github.com/netbox-community/netbox/issues/18743) - Upgrade Django to v5.2
|
||||
* [#18751](https://github.com/netbox-community/netbox/issues/18751) - Change the default value for `ALLOW_TOKEN_RETRIEVAL` to False
|
||||
* [#18808](https://github.com/netbox-community/netbox/issues/18808) - Squashed migration dependencies have been altered to rectify an issue with Django's `sqlmigrate` management command
|
||||
* [#18820](https://github.com/netbox-community/netbox/issues/18820) - PostgreSQL 13 is no longer supported
|
||||
* [#19004](https://github.com/netbox-community/netbox/issues/19004) - The use of inventory items has been deprecated in favor of modules. Inventory items and roles may be removed in a future NetBox release.
|
||||
|
||||
### REST API Changes
|
||||
|
||||
* Added the following endpoints:
|
||||
* `/api/extras/table-configs/`
|
||||
* `/api/extras/tagged-objects/`
|
||||
* `/api/dcim/module-type-profiles/`
|
||||
* core.DataSource
|
||||
* Added the optional `sync_interval` field
|
||||
* dcim.DeviceRole
|
||||
* Added the optional `parent` recursive foreign key field to effect hierarchical ordering
|
||||
* Added a `comments` field
|
||||
* dcim.Location
|
||||
* Added a `comments` field
|
||||
* dcim.ModuleType
|
||||
* Added the optional `profile` foreign key to the new ModuleTypeProfile model
|
||||
* dcim.PowerOutlet
|
||||
* Added a `status` field
|
||||
* dcim.Rack
|
||||
* Added the optional `outer_height` field
|
||||
* dcim.RackType
|
||||
* Added the optional `outer_height` field
|
||||
* dcim.Region
|
||||
* Added a `comments` field
|
||||
* dcim.SiteGroup
|
||||
* Added a `comments` field
|
||||
* extras.ConfigTemplate
|
||||
* Added optional fields `mime_type`, `file_name`, `file_extension` and `as_attachment`
|
||||
* extras.ExportTemplate
|
||||
* Added optional fields `file_name` and `environment_params` (JSON)
|
||||
* extras.Tag
|
||||
* Added a `weight` field
|
||||
* ipam.IPRange
|
||||
* Added a `mark_populaed` boolean field
|
||||
* ipam.L2VPN
|
||||
* Added a `status` field
|
||||
* ipam.Service
|
||||
* Removed the `device` and `virtual_machine` foreign key fields
|
||||
* Added the `parent_object_type`, `parent_object_id`, and (read-only) `parent` fields
|
||||
* ipam.VLANGroup
|
||||
* Added the optional `tenant` foreign key field
|
||||
* tenancy.Contact
|
||||
* Removed the `group` foreign key field
|
||||
* Added the `groups` many-to-many field
|
||||
* tenancy.ContactGroup
|
||||
* Added a `comments` field
|
||||
* tenancy.TenantGroup
|
||||
* Added a `comments` field
|
||||
* wireless.WirelessLANGroup
|
||||
* Added a `comments` field
|
@ -49,6 +49,7 @@ 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
|
||||
@ -59,6 +60,8 @@ markdown_extensions:
|
||||
format: !!python/name:pymdownx.superfences.fence_code_format
|
||||
- pymdownx.tabbed:
|
||||
alternate_style: true
|
||||
not_in_nav: |
|
||||
/index.md
|
||||
nav:
|
||||
- Introduction: 'introduction.md'
|
||||
- Features:
|
||||
@ -200,6 +203,7 @@ nav:
|
||||
- ModuleBay: 'models/dcim/modulebay.md'
|
||||
- ModuleBayTemplate: 'models/dcim/modulebaytemplate.md'
|
||||
- ModuleType: 'models/dcim/moduletype.md'
|
||||
- ModuleTypeProfile: 'models/dcim/moduletypeprofile.md'
|
||||
- Platform: 'models/dcim/platform.md'
|
||||
- PowerFeed: 'models/dcim/powerfeed.md'
|
||||
- PowerOutlet: 'models/dcim/poweroutlet.md'
|
||||
@ -233,6 +237,7 @@ nav:
|
||||
- NotificationGroup: 'models/extras/notificationgroup.md'
|
||||
- SavedFilter: 'models/extras/savedfilter.md'
|
||||
- Subscription: 'models/extras/subscription.md'
|
||||
- TableConfig: 'models/extras/tableconfig.md'
|
||||
- Tag: 'models/extras/tag.md'
|
||||
- Webhook: 'models/extras/webhook.md'
|
||||
- IPAM:
|
||||
@ -304,6 +309,7 @@ nav:
|
||||
- git Cheat Sheet: 'development/git-cheat-sheet.md'
|
||||
- Release Notes:
|
||||
- Summary: 'release-notes/index.md'
|
||||
- Version 4.3: 'release-notes/version-4.3.md'
|
||||
- Version 4.2: 'release-notes/version-4.2.md'
|
||||
- Version 4.1: 'release-notes/version-4.1.md'
|
||||
- Version 4.0: 'release-notes/version-4.0.md'
|
||||
|
@ -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 url_has_allowed_host_and_scheme, urlencode
|
||||
from django.utils.http import 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,6 +28,8 @@ 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
|
||||
|
||||
|
||||
@ -125,12 +127,18 @@ class LoginView(View):
|
||||
|
||||
# Set the user's preferred language (if any)
|
||||
if language := request.user.config.get('locale.language'):
|
||||
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language, max_age=request.session.get_expiry_age())
|
||||
response.set_cookie(
|
||||
key=settings.LANGUAGE_COOKIE_NAME,
|
||||
value=language,
|
||||
max_age=request.session.get_expiry_age(),
|
||||
secure=settings.SESSION_COOKIE_SECURE,
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
else:
|
||||
logger.debug(f"Login form validation failed for username: {form['username'].value()}")
|
||||
username = form['username'].value()
|
||||
logger.debug(f"Login form validation failed for username: {remove_linebreaks(username)}")
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'form': form,
|
||||
@ -141,11 +149,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 url_has_allowed_host_and_scheme(redirect_url, allowed_hosts=None):
|
||||
logger.debug(f"Redirecting user to {redirect_url}")
|
||||
if redirect_url and safe_for_redirect(redirect_url):
|
||||
logger.debug(f"Redirecting user to {remove_linebreaks(redirect_url)}")
|
||||
else:
|
||||
if redirect_url:
|
||||
logger.warning(f"Ignoring unsafe 'next' URL passed to login form: {redirect_url}")
|
||||
logger.warning(f"Ignoring unsafe 'next' URL passed to login form: {remove_linebreaks(redirect_url)}")
|
||||
redirect_url = reverse('home')
|
||||
|
||||
return HttpResponseRedirect(redirect_url)
|
||||
@ -220,7 +228,12 @@ class UserConfigView(LoginRequiredMixin, View):
|
||||
|
||||
# Set/clear language cookie
|
||||
if language := form.cleaned_data['locale.language']:
|
||||
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language, max_age=request.session.get_expiry_age())
|
||||
response.set_cookie(
|
||||
key=settings.LANGUAGE_COOKIE_NAME,
|
||||
value=language,
|
||||
max_age=request.session.get_expiry_age(),
|
||||
secure=settings.SESSION_COOKIE_SECURE,
|
||||
)
|
||||
else:
|
||||
response.delete_cookie(settings.LANGUAGE_COOKIE_NAME)
|
||||
|
||||
|
@ -95,7 +95,7 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
|
||||
)
|
||||
|
||||
|
||||
class ProviderAccountFilterSet(NetBoxModelFilterSet):
|
||||
class ProviderAccountFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Provider.objects.all(),
|
||||
label=_('Provider (ID)'),
|
||||
@ -234,6 +234,11 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
|
||||
to_field_name='slug',
|
||||
label=_('Site (slug)'),
|
||||
)
|
||||
location_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='terminations___location',
|
||||
label=_('Location (ID)'),
|
||||
queryset=Location.objects.all(),
|
||||
)
|
||||
termination_a_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=CircuitTermination.objects.all(),
|
||||
label=_('Termination A (ID)'),
|
||||
|
@ -66,11 +66,12 @@ class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class ProviderAccountFilterForm(NetBoxModelFilterSetForm):
|
||||
class ProviderAccountFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
model = ProviderAccount
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('provider_id', 'account', name=_('Attributes')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
)
|
||||
provider_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
@ -126,7 +127,7 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
|
||||
'type_id', 'status', 'install_date', 'termination_date', 'commit_rate', 'distance', 'distance_unit',
|
||||
name=_('Attributes')
|
||||
),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
)
|
||||
@ -181,6 +182,11 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
|
||||
},
|
||||
label=_('Site')
|
||||
)
|
||||
location_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
required=False,
|
||||
label=_('Location')
|
||||
)
|
||||
install_date = forms.DateField(
|
||||
label=_('Install date'),
|
||||
required=False,
|
||||
@ -322,7 +328,7 @@ class VirtualCircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBox
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
|
||||
FieldSet('type', 'status', name=_('Attributes')),
|
||||
FieldSet('type_id', 'status', name=_('Attributes')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'provider_id', 'provider_network_id')
|
||||
|
@ -4,17 +4,13 @@ from circuits.choices import *
|
||||
|
||||
__all__ = (
|
||||
'CircuitStatusEnum',
|
||||
'CircuitCommitRateEnum',
|
||||
'CircuitTerminationSideEnum',
|
||||
'CircuitTerminationPortSpeedEnum',
|
||||
'CircuitPriorityEnum',
|
||||
'VirtualCircuitTerminationRoleEnum',
|
||||
)
|
||||
|
||||
|
||||
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())
|
||||
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'))
|
||||
|
@ -21,7 +21,7 @@ from .filter_mixins import BaseCircuitTypeFilterMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.graphql.filters import ContentTypeFilter
|
||||
from dcim.graphql.filters import InterfaceFilter
|
||||
from dcim.graphql.filters import InterfaceFilter, LocationFilter, RegionFilter, SiteFilter, SiteGroupFilter
|
||||
from ipam.graphql.filters import ASNFilter
|
||||
from netbox.graphql.filter_lookups import IntegerLookup
|
||||
from .enums import *
|
||||
@ -69,6 +69,23 @@ 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(models.Circuit, lookups=True)
|
||||
class CircuitFilter(
|
||||
@ -99,6 +116,9 @@ 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(models.CircuitType, lookups=True)
|
||||
@ -133,6 +153,9 @@ 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(models.ProviderAccount, lookups=True)
|
||||
|
@ -49,7 +49,7 @@ class ProviderType(NetBoxObjectType, ContactsMixin):
|
||||
filters=ProviderAccountFilter,
|
||||
pagination=True
|
||||
)
|
||||
class ProviderAccountType(NetBoxObjectType):
|
||||
class ProviderAccountType(ContactsMixin, NetBoxObjectType):
|
||||
provider: Annotated["ProviderType", strawberry.lazy('circuits.graphql.types')]
|
||||
|
||||
circuits: List[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]
|
||||
|
@ -347,9 +347,8 @@ class CircuitTermination(
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Must define either site *or* provider network
|
||||
if self.termination is None:
|
||||
raise ValidationError(_("A circuit termination must attach to termination."))
|
||||
raise ValidationError(_("A circuit termination must attach to a terminating object."))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Cache objects associated with the terminating object (for filtering)
|
||||
|
@ -61,9 +61,8 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
linkify=True,
|
||||
verbose_name=_('Account')
|
||||
)
|
||||
type = tables.Column(
|
||||
type = columns.ColoredLabelColumn(
|
||||
verbose_name=_('Type'),
|
||||
linkify=True
|
||||
)
|
||||
status = columns.ChoiceFieldColumn()
|
||||
termination_a = columns.TemplateColumn(
|
||||
@ -111,7 +110,7 @@ class CircuitTerminationTable(NetBoxTable):
|
||||
provider = tables.Column(
|
||||
verbose_name=_('Provider'),
|
||||
linkify=True,
|
||||
accessor='circuit.provider'
|
||||
accessor='circuit__provider'
|
||||
)
|
||||
term_side = tables.Column(
|
||||
verbose_name=_('Side')
|
||||
|
@ -23,7 +23,6 @@ class ProviderTable(ContactsColumnMixin, NetBoxTable):
|
||||
verbose_name=_('Accounts')
|
||||
)
|
||||
account_count = columns.LinkedCountColumn(
|
||||
accessor=tables.A('accounts__count'),
|
||||
viewname='circuits:provideraccount_list',
|
||||
url_params={'provider_id': 'pk'},
|
||||
verbose_name=_('Account Count')
|
||||
@ -33,7 +32,6 @@ class ProviderTable(ContactsColumnMixin, NetBoxTable):
|
||||
verbose_name=_('ASNs')
|
||||
)
|
||||
asn_count = columns.LinkedCountColumn(
|
||||
accessor=tables.A('asns__count'),
|
||||
viewname='ipam:asn_list',
|
||||
url_params={'provider_id': 'pk'},
|
||||
verbose_name=_('ASN Count')
|
||||
|
@ -3,8 +3,10 @@ from django.test import TestCase
|
||||
from circuits.choices import *
|
||||
from circuits.filtersets import *
|
||||
from circuits.models import *
|
||||
from dcim.choices import InterfaceTypeChoices
|
||||
from dcim.models import Cable, Device, DeviceRole, DeviceType, Interface, Manufacturer, Region, Site, SiteGroup
|
||||
from dcim.choices import InterfaceTypeChoices, LocationStatusChoices
|
||||
from dcim.models import (
|
||||
Cable, Device, DeviceRole, DeviceType, Interface, Location, Manufacturer, Region, Site, SiteGroup
|
||||
)
|
||||
from ipam.models import ASN, RIR
|
||||
from netbox.choices import DistanceUnitChoices
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
@ -225,6 +227,17 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
)
|
||||
ProviderNetwork.objects.bulk_create(provider_networks)
|
||||
|
||||
locations = (
|
||||
Location.objects.create(
|
||||
site=sites[0], name='Test Location 1', slug='test-location-1',
|
||||
status=LocationStatusChoices.STATUS_ACTIVE,
|
||||
),
|
||||
Location.objects.create(
|
||||
site=sites[1], name='Test Location 2', slug='test-location-2',
|
||||
status=LocationStatusChoices.STATUS_ACTIVE,
|
||||
),
|
||||
)
|
||||
|
||||
circuits = (
|
||||
Circuit(
|
||||
provider=providers[0],
|
||||
@ -305,7 +318,9 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
|
||||
circuit_terminations = ((
|
||||
CircuitTermination(circuit=circuits[0], termination=sites[0], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[0], termination=locations[0], term_side='Z'),
|
||||
CircuitTermination(circuit=circuits[1], termination=sites[1], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[1], termination=locations[1], term_side='Z'),
|
||||
CircuitTermination(circuit=circuits[2], termination=sites[2], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[3], termination=provider_networks[0], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[4], termination=provider_networks[1], term_side='A'),
|
||||
@ -395,6 +410,11 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'site': [sites[0].slug, sites[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_location(self):
|
||||
location_ids = Location.objects.values_list('id', flat=True)[:2]
|
||||
params = {'location_id': location_ids}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_tenant(self):
|
||||
tenants = Tenant.objects.all()[:2]
|
||||
params = {'tenant_id': [tenants[0].pk, tenants[1].pk]}
|
||||
|
@ -4,6 +4,7 @@ from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim.views import PathTraceView
|
||||
from ipam.models import ASN
|
||||
from netbox.views import generic
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.query import count_related
|
||||
@ -19,7 +20,9 @@ from .models import *
|
||||
@register_model_view(Provider, 'list', path='', detail=False)
|
||||
class ProviderListView(generic.ObjectListView):
|
||||
queryset = Provider.objects.annotate(
|
||||
count_circuits=count_related(Circuit, 'provider')
|
||||
count_circuits=count_related(Circuit, 'provider'),
|
||||
asn_count=count_related(ASN, 'providers'),
|
||||
account_count=count_related(ProviderAccount, 'provider'),
|
||||
)
|
||||
filterset = filtersets.ProviderFilterSet
|
||||
filterset_form = forms.ProviderFilterForm
|
||||
@ -32,7 +35,19 @@ class ProviderView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
'related_models': self.get_related_models(request, instance),
|
||||
'related_models': self.get_related_models(
|
||||
request,
|
||||
instance,
|
||||
omit=(),
|
||||
extra=(
|
||||
(
|
||||
VirtualCircuit.objects.restrict(request.user, 'view').filter(
|
||||
provider_network__provider=instance
|
||||
),
|
||||
'provider_id',
|
||||
),
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@ -48,7 +63,7 @@ class ProviderDeleteView(generic.ObjectDeleteView):
|
||||
queryset = Provider.objects.all()
|
||||
|
||||
|
||||
@register_model_view(Provider, 'bulk_import', detail=False)
|
||||
@register_model_view(Provider, 'bulk_import', path='import', detail=False)
|
||||
class ProviderBulkImportView(generic.BulkImportView):
|
||||
queryset = Provider.objects.all()
|
||||
model_form = forms.ProviderImportForm
|
||||
@ -109,7 +124,7 @@ class ProviderAccountDeleteView(generic.ObjectDeleteView):
|
||||
queryset = ProviderAccount.objects.all()
|
||||
|
||||
|
||||
@register_model_view(ProviderAccount, 'bulk_import', detail=False)
|
||||
@register_model_view(ProviderAccount, 'bulk_import', path='import', detail=False)
|
||||
class ProviderAccountBulkImportView(generic.BulkImportView):
|
||||
queryset = ProviderAccount.objects.all()
|
||||
model_form = forms.ProviderAccountImportForm
|
||||
@ -156,11 +171,16 @@ 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',
|
||||
),
|
||||
),
|
||||
),
|
||||
}
|
||||
@ -178,7 +198,7 @@ class ProviderNetworkDeleteView(generic.ObjectDeleteView):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
|
||||
|
||||
@register_model_view(ProviderNetwork, 'bulk_import', detail=False)
|
||||
@register_model_view(ProviderNetwork, 'bulk_import', path='import', detail=False)
|
||||
class ProviderNetworkBulkImportView(generic.BulkImportView):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
model_form = forms.ProviderNetworkImportForm
|
||||
@ -235,7 +255,7 @@ class CircuitTypeDeleteView(generic.ObjectDeleteView):
|
||||
queryset = CircuitType.objects.all()
|
||||
|
||||
|
||||
@register_model_view(CircuitType, 'bulk_import', detail=False)
|
||||
@register_model_view(CircuitType, 'bulk_import', path='import', detail=False)
|
||||
class CircuitTypeBulkImportView(generic.BulkImportView):
|
||||
queryset = CircuitType.objects.all()
|
||||
model_form = forms.CircuitTypeImportForm
|
||||
@ -291,7 +311,7 @@ class CircuitDeleteView(generic.ObjectDeleteView):
|
||||
queryset = Circuit.objects.all()
|
||||
|
||||
|
||||
@register_model_view(Circuit, 'bulk_import', detail=False)
|
||||
@register_model_view(Circuit, 'bulk_import', path='import', detail=False)
|
||||
class CircuitBulkImportView(generic.BulkImportView):
|
||||
queryset = Circuit.objects.all()
|
||||
model_form = forms.CircuitImportForm
|
||||
@ -431,7 +451,7 @@ class CircuitTerminationDeleteView(generic.ObjectDeleteView):
|
||||
queryset = CircuitTermination.objects.all()
|
||||
|
||||
|
||||
@register_model_view(CircuitTermination, 'bulk_import', detail=False)
|
||||
@register_model_view(CircuitTermination, 'bulk_import', path='import', detail=False)
|
||||
class CircuitTerminationBulkImportView(generic.BulkImportView):
|
||||
queryset = CircuitTermination.objects.all()
|
||||
model_form = forms.CircuitTerminationImportForm
|
||||
@ -492,7 +512,7 @@ class CircuitGroupDeleteView(generic.ObjectDeleteView):
|
||||
queryset = CircuitGroup.objects.all()
|
||||
|
||||
|
||||
@register_model_view(CircuitGroup, 'bulk_import', detail=False)
|
||||
@register_model_view(CircuitGroup, 'bulk_import', path='import', detail=False)
|
||||
class CircuitGroupBulkImportView(generic.BulkImportView):
|
||||
queryset = CircuitGroup.objects.all()
|
||||
model_form = forms.CircuitGroupImportForm
|
||||
@ -542,7 +562,7 @@ class CircuitGroupAssignmentDeleteView(generic.ObjectDeleteView):
|
||||
queryset = CircuitGroupAssignment.objects.all()
|
||||
|
||||
|
||||
@register_model_view(CircuitGroupAssignment, 'bulk_import', detail=False)
|
||||
@register_model_view(CircuitGroupAssignment, 'bulk_import', path='import', detail=False)
|
||||
class CircuitGroupAssignmentBulkImportView(generic.BulkImportView):
|
||||
queryset = CircuitGroupAssignment.objects.all()
|
||||
model_form = forms.CircuitGroupAssignmentImportForm
|
||||
@ -599,7 +619,7 @@ class VirtualCircuitTypeDeleteView(generic.ObjectDeleteView):
|
||||
queryset = VirtualCircuitType.objects.all()
|
||||
|
||||
|
||||
@register_model_view(VirtualCircuitType, 'bulk_import', detail=False)
|
||||
@register_model_view(VirtualCircuitType, 'bulk_import', path='import', detail=False)
|
||||
class VirtualCircuitTypeBulkImportView(generic.BulkImportView):
|
||||
queryset = VirtualCircuitType.objects.all()
|
||||
model_form = forms.VirtualCircuitTypeImportForm
|
||||
|
@ -2,12 +2,13 @@ import re
|
||||
import typing
|
||||
from collections import OrderedDict
|
||||
|
||||
from drf_spectacular.extensions import OpenApiSerializerFieldExtension
|
||||
from drf_spectacular.extensions import OpenApiSerializerFieldExtension, OpenApiSerializerExtension, _SchemaType
|
||||
from drf_spectacular.openapi import AutoSchema
|
||||
from drf_spectacular.plumbing import (
|
||||
build_basic_type, build_choice_field, build_media_type_object, build_object_type, get_doc,
|
||||
)
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import Direction
|
||||
|
||||
from netbox.api.fields import ChoiceField
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
@ -277,3 +278,40 @@ class FixSerializedPKRelatedField(OpenApiSerializerFieldExtension):
|
||||
return component.ref if component else None
|
||||
else:
|
||||
return build_basic_type(OpenApiTypes.INT)
|
||||
|
||||
|
||||
class FixIntegerRangeSerializerSchema(OpenApiSerializerExtension):
|
||||
target_class = 'netbox.api.fields.IntegerRangeSerializer'
|
||||
|
||||
def map_serializer(self, auto_schema: 'AutoSchema', direction: Direction) -> _SchemaType:
|
||||
return {
|
||||
'type': 'array',
|
||||
'items': {
|
||||
'type': 'array',
|
||||
'items': {
|
||||
'type': 'integer',
|
||||
},
|
||||
'minItems': 2,
|
||||
'maxItems': 2,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# Nested models can be passed by ID in requests
|
||||
# The logic for this is handled in `BaseModelSerializer.to_internal_value`
|
||||
class FixWritableNestedSerializerAllowPK(OpenApiSerializerFieldExtension):
|
||||
target_class = 'netbox.api.serializers.BaseModelSerializer'
|
||||
match_subclasses = True
|
||||
|
||||
def map_serializer_field(self, auto_schema, direction):
|
||||
schema = auto_schema._map_serializer_field(self.target, direction, bypass_extensions=True)
|
||||
if schema is None:
|
||||
return schema
|
||||
if direction == 'request' and self.target.nested:
|
||||
return {
|
||||
'oneOf': [
|
||||
build_basic_type(OpenApiTypes.INT),
|
||||
schema,
|
||||
]
|
||||
}
|
||||
return schema
|
||||
|
@ -3,7 +3,10 @@ from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.db import models
|
||||
from django.db.migrations.operations import AlterModelOptions
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from core.events import *
|
||||
from netbox.events import EventType, EVENT_TYPE_KIND_DANGER, EVENT_TYPE_KIND_SUCCESS, EVENT_TYPE_KIND_WARNING
|
||||
from utilities.migration import custom_deconstruct
|
||||
|
||||
# Ignore verbose_name & verbose_name_plural Meta options when calculating model migrations
|
||||
@ -27,6 +30,15 @@ class CoreConfig(AppConfig):
|
||||
# Register models
|
||||
register_models(*self.get_models())
|
||||
|
||||
# Register core events
|
||||
EventType(OBJECT_CREATED, _('Object created')).register()
|
||||
EventType(OBJECT_UPDATED, _('Object updated')).register()
|
||||
EventType(OBJECT_DELETED, _('Object deleted'), destructive=True).register()
|
||||
EventType(JOB_STARTED, _('Job started')).register()
|
||||
EventType(JOB_COMPLETED, _('Job completed'), kind=EVENT_TYPE_KIND_SUCCESS).register()
|
||||
EventType(JOB_FAILED, _('Job failed'), kind=EVENT_TYPE_KIND_WARNING).register()
|
||||
EventType(JOB_ERRORED, _('Job errored'), kind=EVENT_TYPE_KIND_DANGER).register()
|
||||
|
||||
# Clear Redis cache on startup in development mode
|
||||
if settings.DEBUG:
|
||||
try:
|
||||
|
@ -81,8 +81,10 @@ class JobIntervalChoices(ChoiceSet):
|
||||
CHOICES = (
|
||||
(INTERVAL_MINUTELY, _('Minutely')),
|
||||
(INTERVAL_HOURLY, _('Hourly')),
|
||||
(INTERVAL_HOURLY * 12, _('12 hours')),
|
||||
(INTERVAL_DAILY, _('Daily')),
|
||||
(INTERVAL_WEEKLY, _('Weekly')),
|
||||
(INTERVAL_DAILY * 30, _('30 days')),
|
||||
)
|
||||
|
||||
|
||||
|
@ -1,7 +1,3 @@
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from netbox.events import EventType, EVENT_TYPE_KIND_DANGER, EVENT_TYPE_KIND_SUCCESS, EVENT_TYPE_KIND_WARNING
|
||||
|
||||
__all__ = (
|
||||
'JOB_COMPLETED',
|
||||
'JOB_ERRORED',
|
||||
@ -22,12 +18,3 @@ JOB_STARTED = 'job_started'
|
||||
JOB_COMPLETED = 'job_completed'
|
||||
JOB_FAILED = 'job_failed'
|
||||
JOB_ERRORED = 'job_errored'
|
||||
|
||||
# Register core events
|
||||
EventType(OBJECT_CREATED, _('Object created')).register()
|
||||
EventType(OBJECT_UPDATED, _('Object updated')).register()
|
||||
EventType(OBJECT_DELETED, _('Object deleted'), destructive=True).register()
|
||||
EventType(JOB_STARTED, _('Job started')).register()
|
||||
EventType(JOB_COMPLETED, _('Job completed'), kind=EVENT_TYPE_KIND_SUCCESS).register()
|
||||
EventType(JOB_FAILED, _('Job failed'), kind=EVENT_TYPE_KIND_WARNING).register()
|
||||
EventType(JOB_ERRORED, _('Job errored'), kind=EVENT_TYPE_KIND_DANGER).register()
|
||||
|
@ -67,6 +67,7 @@ class DataFileFilterForm(NetBoxModelFilterSetForm):
|
||||
|
||||
|
||||
class JobFilterForm(SavedFiltersMixin, FilterForm):
|
||||
model = Job
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id'),
|
||||
FieldSet('object_type', 'status', name=_('Attributes')),
|
||||
@ -167,6 +168,7 @@ class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm):
|
||||
|
||||
|
||||
class ConfigRevisionFilterForm(SavedFiltersMixin, FilterForm):
|
||||
model = ConfigRevision
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id'),
|
||||
)
|
||||
|
17
netbox/core/migrations/0013_job_data_encoder.py
Normal file
17
netbox/core/migrations/0013_job_data_encoder.py
Normal file
@ -0,0 +1,17 @@
|
||||
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),
|
||||
),
|
||||
]
|
@ -1,12 +1,10 @@
|
||||
# Generated by Django 5.1.6 on 2025-02-26 19:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0012_job_object_type_optional'),
|
||||
('core', '0013_job_data_encoder'),
|
||||
]
|
||||
|
||||
operations = [
|
@ -1,12 +1,10 @@
|
||||
# Generated by Django 5.2b1 on 2025-04-03 18:32
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0013_datasource_sync_interval'),
|
||||
('core', '0014_datasource_sync_interval'),
|
||||
]
|
||||
|
||||
operations = [
|
@ -1,11 +1,13 @@
|
||||
import uuid
|
||||
from functools import partial
|
||||
|
||||
import django_rq
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.db import models
|
||||
from django.db import models, transaction
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
@ -89,8 +91,9 @@ class Job(models.Model):
|
||||
)
|
||||
data = models.JSONField(
|
||||
verbose_name=_('data'),
|
||||
encoder=DjangoJSONEncoder,
|
||||
null=True,
|
||||
blank=True
|
||||
blank=True,
|
||||
)
|
||||
error = models.TextField(
|
||||
verbose_name=_('error'),
|
||||
@ -258,10 +261,12 @@ class Job(models.Model):
|
||||
|
||||
# Schedule the job to run at a specific date & time.
|
||||
elif schedule_at:
|
||||
queue.enqueue_at(schedule_at, func, job_id=str(job.job_id), job=job, **kwargs)
|
||||
callback = partial(queue.enqueue_at, schedule_at, func, job_id=str(job.job_id), job=job, **kwargs)
|
||||
transaction.on_commit(callback)
|
||||
|
||||
# Schedule the job to run asynchronously at this first available opportunity.
|
||||
else:
|
||||
queue.enqueue(func, job_id=str(job.job_id), job=job, **kwargs)
|
||||
callback = partial(queue.enqueue, func, job_id=str(job.job_id), job=job, **kwargs)
|
||||
transaction.on_commit(callback)
|
||||
|
||||
return job
|
||||
|
@ -49,6 +49,7 @@ 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 = ''
|
||||
@ -87,6 +88,13 @@ def get_local_plugins(plugins=None):
|
||||
if plugin_config.release_track:
|
||||
installed_version = f'{installed_version}-{plugin_config.release_track}'
|
||||
|
||||
if plugin_config.author:
|
||||
author = PluginAuthor(
|
||||
name=plugin_config.author,
|
||||
)
|
||||
else:
|
||||
author = None
|
||||
|
||||
local_plugins[plugin_config.name] = Plugin(
|
||||
config_name=plugin_config.name,
|
||||
title_short=plugin_config.verbose_name,
|
||||
@ -98,6 +106,7 @@ def get_local_plugins(plugins=None):
|
||||
installed_version=installed_version,
|
||||
netbox_min_version=plugin_config.min_version,
|
||||
netbox_max_version=plugin_config.max_version,
|
||||
author=author,
|
||||
)
|
||||
|
||||
# Update catalog entries for local plugins, or add them to the list if not listed
|
||||
@ -202,6 +211,7 @@ 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'],
|
||||
|
@ -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
|
||||
from django.db.models.fields.reverse_related import ManyToManyRel, ManyToOneRel
|
||||
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,8 +145,10 @@ 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) is not ManyToManyRel:
|
||||
if type(relation) not in [ManyToManyRel, ManyToOneRel]:
|
||||
continue
|
||||
related_model = relation.related_model
|
||||
related_field_name = relation.remote_field.name
|
||||
@ -156,7 +158,11 @@ 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
|
||||
getattr(obj, related_field_name).remove(instance)
|
||||
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)
|
||||
obj.save()
|
||||
|
||||
# Enqueue the object for event processing
|
||||
queue = events_queue.get()
|
||||
|
@ -14,10 +14,10 @@ __all__ = (
|
||||
class DataSourceTable(NetBoxTable):
|
||||
name = tables.Column(
|
||||
verbose_name=_('Name'),
|
||||
linkify=True
|
||||
linkify=True,
|
||||
)
|
||||
type = BackendTypeColumn(
|
||||
verbose_name=_('Type')
|
||||
verbose_name=_('Type'),
|
||||
)
|
||||
status = columns.ChoiceFieldColumn(
|
||||
verbose_name=_('Status'),
|
||||
@ -28,18 +28,21 @@ class DataSourceTable(NetBoxTable):
|
||||
sync_interval = columns.ChoiceFieldColumn(
|
||||
verbose_name=_('Sync interval'),
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='core:datasource_list'
|
||||
last_synced = tables.DateTimeColumn(
|
||||
verbose_name=_('Last Synced'),
|
||||
)
|
||||
file_count = tables.Column(
|
||||
verbose_name='Files'
|
||||
verbose_name=_('Files'),
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='core:datasource_list',
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = DataSource
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'type', 'status', 'enabled', 'source_url', 'description', 'sync_interval', 'comments',
|
||||
'parameters', 'created', 'last_updated', 'file_count',
|
||||
'parameters', 'last_synced', 'created', 'last_updated', 'file_count',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'type', 'status', 'enabled', 'description', 'sync_interval', 'file_count')
|
||||
|
||||
|
@ -12,6 +12,12 @@ __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')
|
||||
@ -42,8 +48,9 @@ class PluginVersionTable(BaseTable):
|
||||
|
||||
|
||||
class CatalogPluginTable(BaseTable):
|
||||
title_long = tables.Column(
|
||||
verbose_name=_('Name'),
|
||||
title_long = columns.TemplateColumn(
|
||||
template_code=PLUGIN_NAME_TEMPLATE,
|
||||
verbose_name=_('Name')
|
||||
)
|
||||
author = tables.Column(
|
||||
accessor=tables.A('author__name'),
|
||||
|
@ -103,7 +103,7 @@ class DataSourceDeleteView(generic.ObjectDeleteView):
|
||||
queryset = DataSource.objects.all()
|
||||
|
||||
|
||||
@register_model_view(DataSource, 'bulk_import', detail=False)
|
||||
@register_model_view(DataSource, 'bulk_import', path='import', detail=False)
|
||||
class DataSourceBulkImportView(generic.BulkImportView):
|
||||
queryset = DataSource.objects.all()
|
||||
model_form = forms.DataSourceImportForm
|
||||
@ -166,7 +166,7 @@ class DataFileBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
@register_model_view(Job, 'list', path='', detail=False)
|
||||
class JobListView(generic.ObjectListView):
|
||||
queryset = Job.objects.all()
|
||||
queryset = Job.objects.defer('data')
|
||||
filterset = filtersets.JobFilterSet
|
||||
filterset_form = forms.JobFilterForm
|
||||
table = tables.JobTable
|
||||
@ -183,12 +183,12 @@ class JobView(generic.ObjectView):
|
||||
|
||||
@register_model_view(Job, 'delete')
|
||||
class JobDeleteView(generic.ObjectDeleteView):
|
||||
queryset = Job.objects.all()
|
||||
queryset = Job.objects.defer('data')
|
||||
|
||||
|
||||
@register_model_view(Job, 'bulk_delete', path='delete', detail=False)
|
||||
class JobBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Job.objects.all()
|
||||
queryset = Job.objects.defer('data')
|
||||
filterset = filtersets.JobFilterSet
|
||||
table = tables.JobTable
|
||||
|
||||
|
@ -152,6 +152,7 @@ class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, Conne
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
status = ChoiceField(choices=PowerOutletStatusChoices, required=False)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
|
@ -1,3 +1,4 @@
|
||||
from django.contrib.contenttypes.prefetch import GenericPrefetch
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
@ -448,7 +449,18 @@ class PowerOutletViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||
|
||||
class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||
queryset = Interface.objects.prefetch_related(
|
||||
'_path', 'cable__terminations',
|
||||
GenericPrefetch(
|
||||
"cable__terminations__termination",
|
||||
[
|
||||
Interface.objects.select_related("device", "cable"),
|
||||
],
|
||||
),
|
||||
GenericPrefetch(
|
||||
"_path__path_objects",
|
||||
[
|
||||
Interface.objects.select_related("device", "cable"),
|
||||
],
|
||||
),
|
||||
'l2vpn_terminations', # Referenced by InterfaceSerializer.l2vpn_termination
|
||||
'ip_addresses', # Referenced by Interface.count_ipaddresses()
|
||||
'fhrp_group_assignments', # Referenced by Interface.count_fhrp_groups()
|
||||
|
@ -989,6 +989,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
|
||||
# Coaxial
|
||||
TYPE_DOCSIS = 'docsis'
|
||||
TYPE_MOCA = 'moca'
|
||||
|
||||
# PON
|
||||
TYPE_BPON = 'bpon'
|
||||
@ -1185,6 +1186,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
_('Coaxial'),
|
||||
(
|
||||
(TYPE_DOCSIS, 'DOCSIS'),
|
||||
(TYPE_MOCA, 'MoCA'),
|
||||
)
|
||||
),
|
||||
(
|
||||
@ -1348,6 +1350,9 @@ class PortTypeChoices(ChoiceSet):
|
||||
TYPE_SC_UPC = 'sc-upc'
|
||||
TYPE_SC_APC = 'sc-apc'
|
||||
TYPE_FC = 'fc'
|
||||
TYPE_FC_PC = 'fc-pc'
|
||||
TYPE_FC_UPC = 'fc-upc'
|
||||
TYPE_FC_APC = 'fc-apc'
|
||||
TYPE_LC = 'lc'
|
||||
TYPE_LC_PC = 'lc-pc'
|
||||
TYPE_LC_UPC = 'lc-upc'
|
||||
@ -1408,6 +1413,9 @@ class PortTypeChoices(ChoiceSet):
|
||||
_('Fiber Optic'),
|
||||
(
|
||||
(TYPE_FC, 'FC'),
|
||||
(TYPE_FC_PC, 'FC/PC'),
|
||||
(TYPE_FC_UPC, 'FC/UPC'),
|
||||
(TYPE_FC_APC, 'FC/APC'),
|
||||
(TYPE_LC, 'LC'),
|
||||
(TYPE_LC_PC, 'LC/PC'),
|
||||
(TYPE_LC_UPC, 'LC/UPC'),
|
||||
|
2
netbox/dcim/exceptions.py
Normal file
2
netbox/dcim/exceptions.py
Normal file
@ -0,0 +1,2 @@
|
||||
class UnsupportedCablePath(Exception):
|
||||
pass
|
@ -1110,6 +1110,13 @@ 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(),
|
||||
@ -1246,6 +1253,7 @@ class DeviceFilterSet(
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(virtual_chassis__name__icontains=value) |
|
||||
Q(serial__icontains=value.strip()) |
|
||||
Q(inventoryitems__serial__icontains=value.strip()) |
|
||||
Q(asset_tag__icontains=value.strip()) |
|
||||
@ -1382,10 +1390,75 @@ 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
|
||||
@ -1738,6 +1811,10 @@ 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')
|
||||
|
@ -121,11 +121,11 @@ class DeviceBayBulkCreateForm(DeviceBulkAddComponentForm):
|
||||
|
||||
|
||||
class InventoryItemBulkCreateForm(
|
||||
form_from_model(InventoryItem, ['role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered']),
|
||||
form_from_model(InventoryItem, ['status', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered']),
|
||||
DeviceBulkAddComponentForm
|
||||
):
|
||||
model = InventoryItem
|
||||
field_order = (
|
||||
'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
|
||||
'name', 'label', 'status', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
|
||||
'description', 'tags',
|
||||
)
|
||||
|
@ -1458,7 +1458,7 @@ class InterfaceBulkEditForm(
|
||||
form_from_model(Interface, [
|
||||
'label', 'type', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'wwn', 'mtu', 'mgmt_only', 'mark_connected',
|
||||
'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power',
|
||||
'wireless_lans'
|
||||
'wireless_lans', 'vlan_translation_policy'
|
||||
])
|
||||
):
|
||||
enabled = forms.NullBooleanField(
|
||||
@ -1611,7 +1611,9 @@ class InterfaceBulkEditForm(
|
||||
FieldSet('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected', name=_('Operation')),
|
||||
FieldSet('poe_mode', 'poe_type', name=_('PoE')),
|
||||
FieldSet('parent', 'bridge', 'lag', name=_('Related Interfaces')),
|
||||
FieldSet('mode', 'vlan_group', 'untagged_vlan', 'qinq_svlan', name=_('802.1Q Switching')),
|
||||
FieldSet(
|
||||
'mode', 'vlan_group', 'untagged_vlan', 'qinq_svlan', 'vlan_translation_policy', name=_('802.1Q Switching')
|
||||
),
|
||||
FieldSet(
|
||||
TabbedGroups(
|
||||
FieldSet('tagged_vlans', name=_('Assignment')),
|
||||
@ -1626,7 +1628,7 @@ class InterfaceBulkEditForm(
|
||||
nullable_fields = (
|
||||
'module', 'label', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'wwn', 'vdcs', 'mtu', 'description',
|
||||
'poe_mode', 'poe_type', 'mode', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power',
|
||||
'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vrf', 'wireless_lans'
|
||||
'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vrf', 'wireless_lans', 'vlan_translation_policy',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
@ -1192,27 +1192,45 @@ class InventoryItemImportForm(NetBoxModelImportForm):
|
||||
else:
|
||||
self.fields['parent'].queryset = InventoryItem.objects.none()
|
||||
|
||||
def clean_component_name(self):
|
||||
content_type = self.cleaned_data.get('component_type')
|
||||
component_name = self.cleaned_data.get('component_name')
|
||||
def clean(self):
|
||||
super().clean()
|
||||
cleaned_data = self.cleaned_data
|
||||
component_type = cleaned_data.get('component_type')
|
||||
component_name = cleaned_data.get('component_name')
|
||||
device = self.cleaned_data.get("device")
|
||||
|
||||
if not device and hasattr(self, 'instance') and hasattr(self.instance, 'device'):
|
||||
device = self.instance.device
|
||||
|
||||
if not all([device, content_type, component_name]):
|
||||
return None
|
||||
|
||||
model = content_type.model_class()
|
||||
try:
|
||||
component = model.objects.get(device=device, name=component_name)
|
||||
self.instance.component = component
|
||||
except ObjectDoesNotExist:
|
||||
raise forms.ValidationError(
|
||||
_("Component not found: {device} - {component_name}").format(
|
||||
device=device, component_name=component_name
|
||||
if component_type:
|
||||
if device is None:
|
||||
cleaned_data.pop('component_type', None)
|
||||
if component_name is None:
|
||||
cleaned_data.pop('component_type', None)
|
||||
raise forms.ValidationError(
|
||||
_("Component name must be specified when component type is specified")
|
||||
)
|
||||
)
|
||||
if all([device, component_name]):
|
||||
try:
|
||||
model = component_type.model_class()
|
||||
self.instance.component = model.objects.get(device=device, name=component_name)
|
||||
except ObjectDoesNotExist:
|
||||
cleaned_data.pop('component_type', None)
|
||||
cleaned_data.pop('component_name', None)
|
||||
raise forms.ValidationError(
|
||||
_("Component not found: {device} - {component_name}").format(
|
||||
device=device, component_name=component_name
|
||||
)
|
||||
)
|
||||
else:
|
||||
cleaned_data.pop('component_type', None)
|
||||
if not component_name:
|
||||
raise forms.ValidationError(
|
||||
_("Component name must be specified when component type is specified")
|
||||
)
|
||||
else:
|
||||
if component_name:
|
||||
raise forms.ValidationError(
|
||||
_("Component type must be specified when component name is specified")
|
||||
)
|
||||
return cleaned_data
|
||||
|
||||
|
||||
#
|
||||
|
@ -41,7 +41,6 @@ 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 \
|
||||
@ -61,6 +60,12 @@ 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):
|
||||
|
@ -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
|
||||
from ipam.models import ASN, VRF, VLANTranslationPolicy
|
||||
from netbox.choices import *
|
||||
from netbox.forms import NetBoxModelFilterSetForm
|
||||
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
|
||||
@ -304,7 +304,7 @@ class RackTypeFilterForm(RackBaseFilterForm):
|
||||
model = RackType
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('form_factor', 'width', 'u_height', name=_('Rack Type')),
|
||||
FieldSet('manufacturer_id', 'form_factor', 'width', 'u_height', name=_('Rack Type')),
|
||||
FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
|
||||
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
|
||||
)
|
||||
@ -959,8 +959,56 @@ 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,
|
||||
@ -1356,6 +1404,7 @@ 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(
|
||||
@ -1427,6 +1476,16 @@ 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,
|
||||
|
@ -993,7 +993,7 @@ class ComponentTemplateForm(forms.ModelForm):
|
||||
class ModularComponentTemplateForm(ComponentTemplateForm):
|
||||
device_type = DynamicModelChoiceField(
|
||||
label=_('Device type'),
|
||||
queryset=DeviceType.objects.all().all(),
|
||||
queryset=DeviceType.objects.all(),
|
||||
required=False,
|
||||
context={
|
||||
'parent': 'manufacturer',
|
||||
@ -1008,6 +1008,16 @@ 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)
|
||||
|
||||
@ -1024,10 +1034,6 @@ class ModularComponentTemplateForm(ComponentTemplateForm):
|
||||
|
||||
|
||||
class ConsolePortTemplateForm(ModularComponentTemplateForm):
|
||||
fieldsets = (
|
||||
FieldSet('device_type', 'module_type', 'name', 'label', 'type', 'description'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConsolePortTemplate
|
||||
fields = [
|
||||
@ -1036,10 +1042,6 @@ class ConsolePortTemplateForm(ModularComponentTemplateForm):
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateForm(ModularComponentTemplateForm):
|
||||
fieldsets = (
|
||||
FieldSet('device_type', 'module_type', 'name', 'label', 'type', 'description'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = [
|
||||
@ -1050,7 +1052,11 @@ class ConsoleServerPortTemplateForm(ModularComponentTemplateForm):
|
||||
class PowerPortTemplateForm(ModularComponentTemplateForm):
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
|
||||
TabbedGroups(
|
||||
FieldSet('device_type', name=_('Device Type')),
|
||||
FieldSet('module_type', name=_('Module Type')),
|
||||
),
|
||||
'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
|
||||
),
|
||||
)
|
||||
|
||||
@ -1072,7 +1078,13 @@ class PowerOutletTemplateForm(ModularComponentTemplateForm):
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description'),
|
||||
FieldSet(
|
||||
TabbedGroups(
|
||||
FieldSet('device_type', name=_('Device Type')),
|
||||
FieldSet('module_type', name=_('Module Type')),
|
||||
),
|
||||
'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
|
||||
),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@ -1095,7 +1107,11 @@ class InterfaceTemplateForm(ModularComponentTemplateForm):
|
||||
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
'device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'description', 'bridge',
|
||||
TabbedGroups(
|
||||
FieldSet('device_type', name=_('Device Type')),
|
||||
FieldSet('module_type', name=_('Module Type')),
|
||||
),
|
||||
'name', 'label', 'type', 'enabled', 'mgmt_only', 'description', 'bridge',
|
||||
),
|
||||
FieldSet('poe_mode', 'poe_type', name=_('PoE')),
|
||||
FieldSet('rf_role', name=_('Wireless')),
|
||||
@ -1122,8 +1138,11 @@ class FrontPortTemplateForm(ModularComponentTemplateForm):
|
||||
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
|
||||
'description',
|
||||
TabbedGroups(
|
||||
FieldSet('device_type', name=_('Device Type')),
|
||||
FieldSet('module_type', name=_('Module Type')),
|
||||
),
|
||||
'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
|
||||
),
|
||||
)
|
||||
|
||||
@ -1137,7 +1156,13 @@ class FrontPortTemplateForm(ModularComponentTemplateForm):
|
||||
|
||||
class RearPortTemplateForm(ModularComponentTemplateForm):
|
||||
fieldsets = (
|
||||
FieldSet('device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description'),
|
||||
FieldSet(
|
||||
TabbedGroups(
|
||||
FieldSet('device_type', name=_('Device Type')),
|
||||
FieldSet('module_type', name=_('Module Type')),
|
||||
),
|
||||
'name', 'label', 'type', 'color', 'positions', 'description',
|
||||
),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@ -1149,7 +1174,13 @@ class RearPortTemplateForm(ModularComponentTemplateForm):
|
||||
|
||||
class ModuleBayTemplateForm(ModularComponentTemplateForm):
|
||||
fieldsets = (
|
||||
FieldSet('device_type', 'module_type', 'name', 'label', 'position', 'description'),
|
||||
FieldSet(
|
||||
TabbedGroups(
|
||||
FieldSet('device_type', name=_('Device Type')),
|
||||
FieldSet('module_type', name=_('Module Type')),
|
||||
),
|
||||
'name', 'label', 'position', 'description',
|
||||
),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
@ -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
|
||||
from utilities.forms.rendering import FieldSet, TabbedGroups
|
||||
from utilities.forms.widgets import APISelect
|
||||
from . import model_forms
|
||||
|
||||
@ -55,19 +55,23 @@ class ComponentCreateForm(forms.Form):
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Validate that all replication fields generate an equal number of values
|
||||
# Validate that all replication fields generate an equal number of values (or a single value)
|
||||
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] and value_count != pattern_count:
|
||||
raise forms.ValidationError({
|
||||
field_name: _(
|
||||
"The provided pattern specifies {value_count} values, but {pattern_count} are expected."
|
||||
).format(value_count=value_count, pattern_count=pattern_count)
|
||||
}, code='label_pattern_mismatch')
|
||||
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:
|
||||
raise forms.ValidationError({
|
||||
field_name: _(
|
||||
"The provided pattern specifies {value_count} values, but {pattern_count} are expected."
|
||||
).format(value_count=value_count, pattern_count=pattern_count)
|
||||
}, code='label_pattern_mismatch')
|
||||
|
||||
|
||||
#
|
||||
@ -114,7 +118,13 @@ class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemp
|
||||
|
||||
# Override fieldsets from FrontPortTemplateForm to omit rear_port_position
|
||||
fieldsets = (
|
||||
FieldSet('device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'description'),
|
||||
FieldSet(
|
||||
TabbedGroups(
|
||||
FieldSet('device_type', name=_('Device Type')),
|
||||
FieldSet('module_type', name=_('Module Type')),
|
||||
),
|
||||
'name', 'label', 'type', 'color', 'rear_port', 'description',
|
||||
),
|
||||
)
|
||||
|
||||
class Meta(model_forms.FrontPortTemplateForm.Meta):
|
||||
@ -153,6 +163,7 @@ class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemp
|
||||
self.fields['rear_port'].choices = choices
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Check that the number of FrontPortTemplates to be created matches the selected number of RearPortTemplate
|
||||
# positions
|
||||
@ -302,6 +313,7 @@ class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
|
||||
self.fields['rear_port'].choices = choices
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Check that the number of FrontPorts to be created matches the selected number of RearPort positions
|
||||
frontport_count = len(self.cleaned_data['name'])
|
||||
@ -402,6 +414,7 @@ class VirtualChassisCreateForm(NetBoxModelForm):
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'virtual_chassis_id': 'null',
|
||||
'site_id': '$site',
|
||||
'rack_id': '$rack',
|
||||
}
|
||||
|
@ -159,7 +159,7 @@ class ModuleBayTemplateImportForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = ModuleBayTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'label', 'position', 'description',
|
||||
'device_type', 'module_type', 'name', 'label', 'position', 'description',
|
||||
]
|
||||
|
||||
|
||||
|
@ -15,7 +15,6 @@ __all__ = (
|
||||
'InterfaceModeEnum',
|
||||
'InterfacePoEModeEnum',
|
||||
'InterfacePoETypeEnum',
|
||||
'InterfaceSpeedEnum',
|
||||
'InterfaceTypeEnum',
|
||||
'InventoryItemStatusEnum',
|
||||
'LinkStatusEnum',
|
||||
@ -40,38 +39,37 @@ __all__ = (
|
||||
'VirtualDeviceContextStatusEnum',
|
||||
)
|
||||
|
||||
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())
|
||||
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'))
|
||||
InterfacePoETypeEnum = strawberry.enum(InterfacePoETypeChoices.as_enum())
|
||||
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())
|
||||
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'))
|
||||
ModuleAirflowEnum = strawberry.enum(ModuleAirflowChoices.as_enum())
|
||||
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())
|
||||
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'))
|
||||
RackAirflowEnum = strawberry.enum(RackAirflowChoices.as_enum())
|
||||
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())
|
||||
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'))
|
||||
|
@ -97,10 +97,6 @@ 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()
|
||||
)
|
||||
@ -135,6 +131,9 @@ 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()
|
||||
)
|
||||
|
@ -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()
|
||||
)
|
||||
consoleports: Annotated['ConsolePortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
front_ports: Annotated['FrontPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
consoleserverports: Annotated['ConsoleServerPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
rear_ports: Annotated['RearPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
poweroutlets: Annotated['PowerOutletFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
device_bays: Annotated['DeviceBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
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 = (
|
||||
module_bays: Annotated['ModuleBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
modules: Annotated['ModuleFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
@ -340,6 +340,36 @@ 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()
|
||||
@ -405,6 +435,10 @@ 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()
|
||||
)
|
||||
@ -544,6 +578,36 @@ 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(models.ModuleBay, lookups=True)
|
||||
@ -571,11 +635,45 @@ class ModuleTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, Weig
|
||||
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(models.Platform, lookups=True)
|
||||
@ -833,6 +931,9 @@ class VirtualChassisFilter(PrimaryModelFilterMixin):
|
||||
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()
|
||||
|
||||
|
||||
@ -856,3 +957,6 @@ 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()
|
||||
|
@ -30,6 +30,7 @@ class PathEndpointMixin:
|
||||
|
||||
connected_endpoints: List[Annotated[Union[
|
||||
Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')], # noqa: F821
|
||||
Annotated["VirtualCircuitTerminationType", strawberry.lazy('circuits.graphql.types')], # noqa: F821
|
||||
Annotated["ConsolePortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
Annotated["ConsoleServerPortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
|
@ -471,7 +471,7 @@ class InterfaceTemplateType(ModularComponentTemplateType):
|
||||
)
|
||||
class InventoryItemType(ComponentType):
|
||||
role: Annotated["InventoryItemRoleType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')]
|
||||
manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
|
||||
child_items: List[Annotated["InventoryItemType", strawberry.lazy('dcim.graphql.types')]]
|
||||
|
||||
|
@ -8,7 +8,7 @@ import utilities.json
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('dcim', '0204_device_role_rebuild'),
|
||||
('extras', '0125_exporttemplate_file_name'),
|
||||
('extras', '0126_exporttemplate_file_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
@ -1,10 +1,8 @@
|
||||
import itertools
|
||||
from collections import defaultdict
|
||||
|
||||
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 _
|
||||
|
||||
@ -15,7 +13,8 @@ from dcim.fields import PathField
|
||||
from dcim.utils import decompile_path_node, object_to_path_node
|
||||
from netbox.models import ChangeLoggedModel, PrimaryModel
|
||||
from utilities.conversion import to_meters
|
||||
from utilities.fields import ColorField
|
||||
from utilities.exceptions import AbortRequest
|
||||
from utilities.fields import ColorField, GenericArrayForeignKey
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from wireless.models import WirelessLink
|
||||
from .device_components import FrontPort, RearPort, PathEndpoint
|
||||
@ -26,6 +25,7 @@ __all__ = (
|
||||
'CableTermination',
|
||||
)
|
||||
|
||||
from ..exceptions import UnsupportedCablePath
|
||||
|
||||
trace_paths = Signal()
|
||||
|
||||
@ -236,8 +236,10 @@ class Cable(PrimaryModel):
|
||||
for termination in self.b_terminations:
|
||||
if not termination.pk or termination not in b_terminations:
|
||||
CableTermination(cable=self, cable_end='B', termination=termination).save()
|
||||
|
||||
trace_paths.send(Cable, instance=self, created=_created)
|
||||
try:
|
||||
trace_paths.send(Cable, instance=self, created=_created)
|
||||
except UnsupportedCablePath as e:
|
||||
raise AbortRequest(e)
|
||||
|
||||
def get_status_color(self):
|
||||
return LinkStatusChoices.colors.get(self.status)
|
||||
@ -486,13 +488,16 @@ class CablePath(models.Model):
|
||||
return ObjectType.objects.get_for_id(ct_id)
|
||||
|
||||
@property
|
||||
def path_objects(self):
|
||||
"""
|
||||
Cache and return the complete path as lists of objects, derived from their annotation within the path.
|
||||
"""
|
||||
if not hasattr(self, '_path_objects'):
|
||||
self._path_objects = self._get_path()
|
||||
return self._path_objects
|
||||
def _path_decompiled(self):
|
||||
res = []
|
||||
for step in self.path:
|
||||
nodes = []
|
||||
for node in step:
|
||||
nodes.append(decompile_path_node(node))
|
||||
res.append(nodes)
|
||||
return res
|
||||
|
||||
path_objects = GenericArrayForeignKey("_path_decompiled")
|
||||
|
||||
@property
|
||||
def origins(self):
|
||||
@ -527,8 +532,8 @@ class CablePath(models.Model):
|
||||
return None
|
||||
|
||||
# Ensure all originating terminations are attached to the same link
|
||||
if len(terminations) > 1:
|
||||
assert all(t.link == terminations[0].link for t in terminations[1:])
|
||||
if len(terminations) > 1 and not all(t.link == terminations[0].link for t in terminations[1:]):
|
||||
raise UnsupportedCablePath(_("All originating terminations must be attached to the same link"))
|
||||
|
||||
path = []
|
||||
position_stack = []
|
||||
@ -539,12 +544,13 @@ class CablePath(models.Model):
|
||||
while terminations:
|
||||
|
||||
# Terminations must all be of the same type
|
||||
assert all(isinstance(t, type(terminations[0])) for t in terminations[1:])
|
||||
if not all(isinstance(t, type(terminations[0])) for t in terminations[1:]):
|
||||
raise UnsupportedCablePath(_("All mid-span terminations must have the same termination type"))
|
||||
|
||||
# All mid-span terminations must all be attached to the same device
|
||||
if not isinstance(terminations[0], PathEndpoint):
|
||||
assert all(isinstance(t, type(terminations[0])) for t in terminations[1:])
|
||||
assert all(t.parent_object == terminations[0].parent_object for t in terminations[1:])
|
||||
if (not isinstance(terminations[0], PathEndpoint) and not
|
||||
all(t.parent_object == terminations[0].parent_object for t in terminations[1:])):
|
||||
raise UnsupportedCablePath(_("All mid-span terminations must have the same parent object"))
|
||||
|
||||
# Check for a split path (e.g. rear port fanning out to multiple front ports with
|
||||
# different cables attached)
|
||||
@ -567,8 +573,10 @@ class CablePath(models.Model):
|
||||
return None
|
||||
# Otherwise, halt the trace if no link exists
|
||||
break
|
||||
assert all(type(link) in (Cable, WirelessLink) for link in links)
|
||||
assert all(isinstance(link, type(links[0])) for link in links)
|
||||
if not all(type(link) in (Cable, WirelessLink) for link in links):
|
||||
raise UnsupportedCablePath(_("All links must be cable or wireless"))
|
||||
if not all(isinstance(link, type(links[0])) for link in links):
|
||||
raise UnsupportedCablePath(_("All links must match first link type"))
|
||||
|
||||
# Step 3: Record asymmetric paths as split
|
||||
not_connected_terminations = [termination.link for termination in terminations if termination.link is None]
|
||||
@ -649,14 +657,18 @@ class CablePath(models.Model):
|
||||
positions = position_stack.pop()
|
||||
|
||||
# Ensure we have a number of positions equal to the amount of remote terminations
|
||||
assert len(remote_terminations) == len(positions)
|
||||
if len(remote_terminations) != len(positions):
|
||||
raise UnsupportedCablePath(
|
||||
_("All positions counts within the path on opposite ends of links must match")
|
||||
)
|
||||
|
||||
# Get our front ports
|
||||
q_filter = Q()
|
||||
for rt in remote_terminations:
|
||||
position = positions.pop()
|
||||
q_filter |= Q(rear_port_id=rt.pk, rear_port_position=position)
|
||||
assert q_filter is not Q()
|
||||
if q_filter is Q():
|
||||
raise UnsupportedCablePath(_("Remote termination position filter is missing"))
|
||||
front_ports = FrontPort.objects.filter(q_filter)
|
||||
# Obtain the individual front ports based on the termination and position
|
||||
elif position_stack:
|
||||
@ -742,42 +754,6 @@ class CablePath(models.Model):
|
||||
self.delete()
|
||||
retrace.alters_data = True
|
||||
|
||||
def _get_path(self):
|
||||
"""
|
||||
Return the path as a list of prefetched objects.
|
||||
"""
|
||||
# Compile a list of IDs to prefetch for each type of model in the path
|
||||
to_prefetch = defaultdict(list)
|
||||
for node in self._nodes:
|
||||
ct_id, object_id = decompile_path_node(node)
|
||||
to_prefetch[ct_id].append(object_id)
|
||||
|
||||
# Prefetch path objects using one query per model type. Prefetch related devices where appropriate.
|
||||
prefetched = {}
|
||||
for ct_id, object_ids in to_prefetch.items():
|
||||
model_class = ObjectType.objects.get_for_id(ct_id).model_class()
|
||||
queryset = model_class.objects.filter(pk__in=object_ids)
|
||||
if hasattr(model_class, 'device'):
|
||||
queryset = queryset.prefetch_related('device')
|
||||
prefetched[ct_id] = {
|
||||
obj.id: obj for obj in queryset
|
||||
}
|
||||
|
||||
# Replicate the path using the prefetched objects.
|
||||
path = []
|
||||
for step in self.path:
|
||||
nodes = []
|
||||
for node in step:
|
||||
ct_id, object_id = decompile_path_node(node)
|
||||
try:
|
||||
nodes.append(prefetched[ct_id][object_id])
|
||||
except KeyError:
|
||||
# Ignore stale (deleted) object IDs
|
||||
pass
|
||||
path.append(nodes)
|
||||
|
||||
return path
|
||||
|
||||
def get_cable_ids(self):
|
||||
"""
|
||||
Return all Cable IDs within the path.
|
||||
@ -797,9 +773,28 @@ 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.objects.filter(id__in=cable_ids, _abs_length__isnull=False)
|
||||
total_length = cables.aggregate(total=Sum('_abs_length'))['total']
|
||||
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
|
||||
|
||||
is_definitive = len(cables) == len(cable_ids)
|
||||
|
||||
return total_length, is_definitive
|
||||
|
@ -184,8 +184,11 @@ class CabledObjectModel(models.Model):
|
||||
@cached_property
|
||||
def link_peers(self):
|
||||
if self.cable:
|
||||
peers = self.cable.terminations.exclude(cable_end=self.cable_end).prefetch_related('termination')
|
||||
return [peer.termination for peer in peers]
|
||||
return [
|
||||
peer.termination
|
||||
for peer in self.cable.terminations.all()
|
||||
if peer.cable_end != self.cable_end
|
||||
]
|
||||
return []
|
||||
|
||||
@property
|
||||
|
@ -3,7 +3,7 @@ import yaml
|
||||
|
||||
from functools import cached_property
|
||||
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.storage import default_storage
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
@ -609,6 +609,12 @@ class Device(
|
||||
null=True,
|
||||
help_text=_("GPS coordinate in decimal format (xx.yyyyyy)")
|
||||
)
|
||||
services = GenericRelation(
|
||||
to='ipam.Service',
|
||||
content_type_field='parent_object_type',
|
||||
object_id_field='parent_object_id',
|
||||
related_query_name='device',
|
||||
)
|
||||
|
||||
# Counter fields
|
||||
console_port_count = CounterCacheField(
|
||||
@ -690,14 +696,10 @@ class Device(
|
||||
verbose_name_plural = _('devices')
|
||||
|
||||
def __str__(self):
|
||||
if self.name and self.asset_tag:
|
||||
return f'{self.name} ({self.asset_tag})'
|
||||
elif self.name:
|
||||
return self.name
|
||||
elif self.virtual_chassis and self.asset_tag:
|
||||
return f'{self.virtual_chassis.name}:{self.vc_position} ({self.asset_tag})'
|
||||
elif self.virtual_chassis:
|
||||
return f'{self.virtual_chassis.name}:{self.vc_position} ({self.pk})'
|
||||
if self.label and self.asset_tag:
|
||||
return f'{self.label} ({self.asset_tag})'
|
||||
elif self.label:
|
||||
return self.label
|
||||
elif self.device_type and self.asset_tag:
|
||||
return f'{self.device_type.manufacturer} {self.device_type.model} ({self.asset_tag})'
|
||||
elif self.device_type:
|
||||
@ -961,14 +963,22 @@ class Device(
|
||||
device.location = self.location
|
||||
device.save()
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
"""
|
||||
Return the device name if set; otherwise return a generated name if available.
|
||||
"""
|
||||
if self.name:
|
||||
return self.name
|
||||
if self.virtual_chassis:
|
||||
return f'{self.virtual_chassis.name}:{self.vc_position}'
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
"""
|
||||
Return the device name if set; otherwise return the Device's primary key as {pk}
|
||||
"""
|
||||
if self.name is not None:
|
||||
return self.name
|
||||
return '{{{}}}'.format(self.pk)
|
||||
return self.label or '{{{}}}'.format(self.pk)
|
||||
|
||||
@property
|
||||
def primary_ip(self):
|
||||
@ -1268,7 +1278,10 @@ class MACAddress(PrimaryModel):
|
||||
ct = ObjectType.objects.get_for_id(self._original_assigned_object_type_id)
|
||||
original_assigned_object = ct.get_object_for_this_type(pk=self._original_assigned_object_id)
|
||||
|
||||
if original_assigned_object.primary_mac_address:
|
||||
if (
|
||||
original_assigned_object.primary_mac_address
|
||||
and original_assigned_object.primary_mac_address.pk == self.pk
|
||||
):
|
||||
if not assigned_object:
|
||||
raise ValidationError(
|
||||
_("Cannot unassign MAC Address while it is designated as the primary MAC for an object")
|
||||
|
@ -7,6 +7,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from jsonschema.exceptions import ValidationError as JSONValidationError
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import MODULE_TOKEN
|
||||
from dcim.utils import update_interface_bridges
|
||||
from extras.models import ConfigContextModel, CustomField
|
||||
from netbox.models import PrimaryModel
|
||||
@ -341,6 +342,7 @@ class Module(PrimaryModel, ConfigContextModel):
|
||||
else:
|
||||
# ModuleBays must be saved individually for MPTT
|
||||
for instance in create_instances:
|
||||
instance.name = instance.name.replace(MODULE_TOKEN, str(self.module_bay.position))
|
||||
instance.save()
|
||||
|
||||
update_fields = ['module']
|
||||
|
@ -732,3 +732,8 @@ 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
|
||||
|
@ -44,6 +44,7 @@ class DeviceIndex(SearchIndex):
|
||||
('asset_tag', 50),
|
||||
('serial', 60),
|
||||
('name', 100),
|
||||
('virtual_chassis', 200),
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
|
@ -225,8 +225,7 @@ class CableTraceSVG:
|
||||
"""
|
||||
nodes_height = 0
|
||||
nodes = []
|
||||
# Sort them by name to make renders more readable
|
||||
for i, term in enumerate(sorted(terminations, key=lambda x: str(x))):
|
||||
for i, term in enumerate(terminations):
|
||||
node = Node(
|
||||
position=(offset_x + i * width, self.cursor),
|
||||
width=width,
|
||||
|
@ -30,10 +30,8 @@ STROKE_RESERVED = '#4d4dff'
|
||||
|
||||
|
||||
def get_device_name(device):
|
||||
if device.virtual_chassis:
|
||||
name = f'{device.virtual_chassis.name}:{device.vc_position}'
|
||||
elif device.name:
|
||||
name = device.name
|
||||
if device.label:
|
||||
name = device.label
|
||||
else:
|
||||
name = str(device.device_type)
|
||||
if device.devicebay_count:
|
||||
|
@ -144,7 +144,7 @@ class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
name = tables.TemplateColumn(
|
||||
verbose_name=_('Name'),
|
||||
template_code=DEVICE_LINK,
|
||||
linkify=True
|
||||
linkify=True,
|
||||
)
|
||||
status = columns.ChoiceFieldColumn(
|
||||
verbose_name=_('Status'),
|
||||
@ -678,7 +678,7 @@ class InterfaceTable(BaseInterfaceTable, ModularDeviceComponentTable, PathEndpoi
|
||||
'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description',
|
||||
'mark_connected', 'cable', 'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection',
|
||||
'tags', 'vdcs', 'vrf', 'l2vpn', 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans',
|
||||
'qinq_svlan', 'inventory_items', 'created', 'last_updated',
|
||||
'qinq_svlan', 'inventory_items', 'created', 'last_updated', 'vlan_translation_policy'
|
||||
)
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
|
||||
|
||||
|
@ -31,6 +31,11 @@ class ManufacturerTable(ContactsColumnMixin, NetBoxTable):
|
||||
verbose_name=_('Name'),
|
||||
linkify=True
|
||||
)
|
||||
racktype_count = columns.LinkedCountColumn(
|
||||
viewname='dcim:racktype_list',
|
||||
url_params={'manufacturer_id': 'pk'},
|
||||
verbose_name=_('Rack Types')
|
||||
)
|
||||
devicetype_count = columns.LinkedCountColumn(
|
||||
viewname='dcim:devicetype_list',
|
||||
url_params={'manufacturer_id': 'pk'},
|
||||
@ -58,12 +63,12 @@ class ManufacturerTable(ContactsColumnMixin, NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = models.Manufacturer
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'devicetype_count', 'moduletype_count', 'inventoryitem_count', 'platform_count',
|
||||
'description', 'slug', 'tags', 'contacts', 'actions', 'created', 'last_updated',
|
||||
'pk', 'id', 'name', 'racktype_count', 'devicetype_count', 'moduletype_count', 'inventoryitem_count',
|
||||
'platform_count', 'description', 'slug', 'tags', 'contacts', 'actions', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'devicetype_count', 'moduletype_count', 'inventoryitem_count', 'platform_count',
|
||||
'description', 'slug',
|
||||
'pk', 'name', 'racktype_count', 'devicetype_count', 'moduletype_count', 'inventoryitem_count',
|
||||
'platform_count', 'description', 'slug',
|
||||
)
|
||||
|
||||
|
||||
|
@ -100,7 +100,6 @@ class SiteTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
verbose_name=_('ASNs')
|
||||
)
|
||||
asn_count = columns.LinkedCountColumn(
|
||||
accessor=tables.A('asns__count'),
|
||||
viewname='ipam:asn_list',
|
||||
url_params={'site_id': 'pk'},
|
||||
verbose_name=_('ASN Count')
|
||||
@ -153,6 +152,11 @@ class LocationTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
url_params={'location_id': 'pk'},
|
||||
verbose_name=_('Devices')
|
||||
)
|
||||
vlangroup_count = columns.LinkedCountColumn(
|
||||
viewname='ipam:vlangroup_list',
|
||||
url_params={'location': 'pk'},
|
||||
verbose_name=_('VLAN Groups')
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:location_list'
|
||||
)
|
||||
@ -168,7 +172,9 @@ class LocationTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
fields = (
|
||||
'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 = (
|
||||
'pk', 'name', 'site', 'status', 'facility', 'tenant', 'rack_count', 'device_count', 'description'
|
||||
'pk', 'name', 'site', 'status', 'facility', 'tenant', 'rack_count', 'device_count', 'vlangroup_count',
|
||||
'description'
|
||||
)
|
||||
|
@ -35,7 +35,7 @@ WEIGHT = """
|
||||
"""
|
||||
|
||||
DEVICE_LINK = """
|
||||
{{ value|default:'<span class="badge text-bg-info">Unnamed device</span>' }}
|
||||
{{ record.label|default:'<span class="badge text-bg-info">Unnamed device</span>' }}
|
||||
"""
|
||||
|
||||
DEVICEBAY_STATUS = """
|
||||
@ -64,7 +64,7 @@ INTERFACE_IPADDRESSES = """
|
||||
|
||||
INTERFACE_FHRPGROUPS = """
|
||||
{% for assignment in value.all %}
|
||||
<a href="{{ assignment.group.get_absolute_url }}">{{ assignment.group.get_protocol_display }}: {{ assignment.group.group_id }}</a>
|
||||
<a href="{{ assignment.group.get_absolute_url }}">{{ assignment.group }}</a>
|
||||
{% endfor %}
|
||||
"""
|
||||
|
||||
@ -164,8 +164,8 @@ CONSOLEPORT_BUTTONS = """
|
||||
</span>
|
||||
{% endif %}
|
||||
{% elif perms.dcim.add_cable %}
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-secondary btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-secondary btn-sm disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||
<span class="dropdown">
|
||||
<button type="button" class="btn btn-success btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
|
||||
@ -177,7 +177,7 @@ CONSOLEPORT_BUTTONS = """
|
||||
</ul>
|
||||
</span>
|
||||
{% else %}
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-secondary btn-sm disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
@ -214,8 +214,8 @@ CONSOLESERVERPORT_BUTTONS = """
|
||||
</span>
|
||||
{% endif %}
|
||||
{% elif perms.dcim.add_cable %}
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-secondary btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-secondary btn-sm disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||
<span class="dropdown">
|
||||
<button type="button" class="btn btn-success btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
|
||||
@ -227,7 +227,7 @@ CONSOLESERVERPORT_BUTTONS = """
|
||||
</ul>
|
||||
</span>
|
||||
{% else %}
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-secondary btn-sm disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
@ -264,8 +264,8 @@ POWERPORT_BUTTONS = """
|
||||
</span>
|
||||
{% endif %}
|
||||
{% elif perms.dcim.add_cable %}
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-secondary btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-secondary btn-sm disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||
<span class="dropdown">
|
||||
<button type="button" class="btn btn-success btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
|
||||
@ -276,7 +276,7 @@ POWERPORT_BUTTONS = """
|
||||
</ul>
|
||||
</span>
|
||||
{% else %}
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-secondary btn-sm disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
@ -313,14 +313,14 @@ POWEROUTLET_BUTTONS = """
|
||||
</span>
|
||||
{% endif %}
|
||||
{% elif perms.dcim.add_cable %}
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-secondary btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-secondary btn-sm disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||
{% if not record.mark_connected %}
|
||||
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.poweroutlet&a_terminations={{ record.pk }}&b_terminations_type=dcim.powerport&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" title="Connect" class="btn btn-success btn-sm">
|
||||
<i class="mdi mdi-ethernet-cable" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-secondary btn-sm disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
"""
|
||||
@ -407,8 +407,8 @@ INTERFACE_BUTTONS = """
|
||||
</a>
|
||||
{% endif %}
|
||||
{% elif record.is_wired and perms.dcim.add_cable %}
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-secondary btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-secondary btn-sm disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||
{% if not record.mark_connected %}
|
||||
<span class="dropdown">
|
||||
<button type="button" class="btn btn-success btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Connect cable">
|
||||
@ -422,7 +422,7 @@ INTERFACE_BUTTONS = """
|
||||
</ul>
|
||||
</span>
|
||||
{% else %}
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-secondary btn-sm disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
{% elif record.is_wireless and perms.wireless.add_wirelesslink %}
|
||||
<a href="{% url 'wireless:wirelesslink_add' %}?site_a={{ record.device.site.pk }}&location_a={{ record.device.location.pk }}&device_a={{ record.device_id }}&interface_a={{ record.pk }}&site_b={{ record.device.site.pk }}&location_b={{ record.device.location.pk }}" class="btn btn-success btn-sm">
|
||||
@ -464,8 +464,8 @@ FRONTPORT_BUTTONS = """
|
||||
</span>
|
||||
{% endif %}
|
||||
{% elif perms.dcim.add_cable %}
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-secondary btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-secondary btn-sm disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||
{% if not record.mark_connected %}
|
||||
<span class="dropdown">
|
||||
<button type="button" class="btn btn-success btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
@ -481,7 +481,7 @@ FRONTPORT_BUTTONS = """
|
||||
</ul>
|
||||
</span>
|
||||
{% else %}
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-secondary btn-sm disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
"""
|
||||
@ -519,8 +519,8 @@ REARPORT_BUTTONS = """
|
||||
</span>
|
||||
{% endif %}
|
||||
{% elif perms.dcim.add_cable %}
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-secondary btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-secondary btn-sm disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||
{% if not record.mark_connected %}
|
||||
<span class="dropdown">
|
||||
<button type="button" class="btn btn-success btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
@ -536,7 +536,7 @@ REARPORT_BUTTONS = """
|
||||
</ul>
|
||||
</span>
|
||||
{% else %}
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-secondary btn-sm disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
"""
|
||||
|
@ -1,6 +1,6 @@
|
||||
import json
|
||||
|
||||
from django.test import override_settings
|
||||
from django.test import override_settings, tag
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
from rest_framework import status
|
||||
@ -1979,6 +1979,27 @@ class FrontPortTest(APIViewTestCases.APIViewTestCase):
|
||||
},
|
||||
]
|
||||
|
||||
@tag('regression') # Issue #18991
|
||||
def test_front_port_paths(self):
|
||||
device = Device.objects.first()
|
||||
rear_port = RearPort.objects.create(
|
||||
device=device, name='Rear Port 10', type=PortTypeChoices.TYPE_8P8C
|
||||
)
|
||||
interface1 = Interface.objects.create(device=device, name='Interface 1')
|
||||
front_port = FrontPort.objects.create(
|
||||
device=device,
|
||||
name='Rear Port 10',
|
||||
type=PortTypeChoices.TYPE_8P8C,
|
||||
rear_port=rear_port,
|
||||
)
|
||||
Cable.objects.create(a_terminations=[interface1], b_terminations=[front_port])
|
||||
|
||||
self.add_permissions(f'dcim.view_{self.model._meta.model_name}')
|
||||
url = reverse(f'dcim-api:{self.model._meta.model_name}-paths', kwargs={'pk': front_port.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
|
||||
|
||||
class RearPortTest(APIViewTestCases.APIViewTestCase):
|
||||
model = RearPort
|
||||
@ -2022,6 +2043,23 @@ class RearPortTest(APIViewTestCases.APIViewTestCase):
|
||||
},
|
||||
]
|
||||
|
||||
@tag('regression') # Issue #18991
|
||||
def test_rear_port_paths(self):
|
||||
device = Device.objects.first()
|
||||
interface1 = Interface.objects.create(device=device, name='Interface 1')
|
||||
rear_port = RearPort.objects.create(
|
||||
device=device,
|
||||
name='Rear Port 10',
|
||||
type=PortTypeChoices.TYPE_8P8C,
|
||||
)
|
||||
Cable.objects.create(a_terminations=[interface1], b_terminations=[rear_port])
|
||||
|
||||
self.add_permissions(f'dcim.view_{self.model._meta.model_name}')
|
||||
url = reverse(f'dcim-api:{self.model._meta.model_name}-paths', kwargs={'pk': rear_port.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
|
||||
|
||||
class ModuleBayTest(APIViewTestCases.APIViewTestCase):
|
||||
model = ModuleBay
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user