mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-18 10:16:34 +00:00
Merge branch 'main' into feature
This commit is contained in:
commit
23d7515b41
@ -15,7 +15,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.4.1
|
||||
placeholder: v4.4.2
|
||||
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.4.1
|
||||
placeholder: v4.4.2
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
@ -25,9 +25,12 @@ body:
|
||||
- Getting started
|
||||
- Configuration
|
||||
- Customization
|
||||
- Best practices
|
||||
- Integrations/API
|
||||
- Plugins
|
||||
- Administration
|
||||
- Data model
|
||||
- Reference
|
||||
- Development
|
||||
- Other
|
||||
validations:
|
||||
|
||||
@ -30,7 +30,8 @@ django-htmx
|
||||
|
||||
# Modified Preorder Tree Traversal (recursive nesting of objects)
|
||||
# https://github.com/django-mptt/django-mptt/blob/main/CHANGELOG.rst
|
||||
django-mptt
|
||||
# v0.18.0 introduces errant migrations which need to be resolved
|
||||
django-mptt==0.17.0
|
||||
|
||||
# Context managers for PostgreSQL advisory locks
|
||||
# https://github.com/Xof/django-pglocks/blob/master/CHANGES.txt
|
||||
|
||||
@ -332,14 +332,14 @@
|
||||
"100base-t1",
|
||||
"1000base-bx10-d",
|
||||
"1000base-bx10-u",
|
||||
"1000base-cx",
|
||||
"1000base-cwdm",
|
||||
"1000base-cx",
|
||||
"1000base-dwdm",
|
||||
"1000base-ex",
|
||||
"1000base-sx",
|
||||
"1000base-lsx",
|
||||
"1000base-lx",
|
||||
"1000base-lx10",
|
||||
"1000base-sx",
|
||||
"1000base-t",
|
||||
"1000base-tx",
|
||||
"1000base-zx",
|
||||
@ -374,6 +374,7 @@
|
||||
"100gbase-cr2",
|
||||
"100gbase-cr4",
|
||||
"100gbase-cr10",
|
||||
"100gbase-cwdm4",
|
||||
"100gbase-dr",
|
||||
"100gbase-er4",
|
||||
"100gbase-fr1",
|
||||
@ -387,12 +388,12 @@
|
||||
"100gbase-zr",
|
||||
"200gbase-cr2",
|
||||
"200gbase-cr4",
|
||||
"200gbase-sr2",
|
||||
"200gbase-sr4",
|
||||
"200gbase-dr4",
|
||||
"200gbase-er4",
|
||||
"200gbase-fr4",
|
||||
"200gbase-lr4",
|
||||
"200gbase-sr2",
|
||||
"200gbase-sr4",
|
||||
"200gbase-vr2",
|
||||
"400gbase-cr4",
|
||||
"400gbase-dr4",
|
||||
@ -415,34 +416,34 @@
|
||||
"1000base-x-gbic",
|
||||
"1000base-x-sfp",
|
||||
"10gbase-x-sfpp",
|
||||
"10gbase-x-xfp",
|
||||
"10gbase-x-xenpak",
|
||||
"10gbase-x-xfp",
|
||||
"10gbase-x-x2",
|
||||
"25gbase-x-sfp28",
|
||||
"50gbase-x-sfp56",
|
||||
"40gbase-x-qsfpp",
|
||||
"50gbase-x-sfp28",
|
||||
"50gbase-x-sfp56",
|
||||
"100gbase-x-cfp",
|
||||
"100gbase-x-cfp2",
|
||||
"200gbase-x-cfp2",
|
||||
"400gbase-x-cfp2",
|
||||
"100gbase-x-cfp4",
|
||||
"100gbase-x-cxp",
|
||||
"100gbase-x-cpak",
|
||||
"100gbase-x-dsfp",
|
||||
"100gbase-x-sfpdd",
|
||||
"100gbase-x-qsfp28",
|
||||
"100gbase-x-qsfpdd",
|
||||
"100gbase-x-sfpdd",
|
||||
"200gbase-x-cfp2",
|
||||
"200gbase-x-qsfp56",
|
||||
"200gbase-x-qsfpdd",
|
||||
"400gbase-x-qsfp112",
|
||||
"400gbase-x-qsfpdd",
|
||||
"400gbase-x-cdfp",
|
||||
"400gbase-x-cfp2",
|
||||
"400gbase-x-cfp8",
|
||||
"400gbase-x-osfp",
|
||||
"400gbase-x-osfp-rhs",
|
||||
"400gbase-x-cdfp",
|
||||
"400gbase-x-cfp8",
|
||||
"800gbase-x-qsfpdd",
|
||||
"800gbase-x-osfp",
|
||||
"800gbase-x-qsfpdd",
|
||||
"1000base-kx",
|
||||
"2.5gbase-kx",
|
||||
"5gbase-kr",
|
||||
|
||||
File diff suppressed because one or more lines are too long
187
docs/best-practices/performance-handbook.md
Normal file
187
docs/best-practices/performance-handbook.md
Normal file
@ -0,0 +1,187 @@
|
||||
# Performance Handbook
|
||||
|
||||
The purpose of this handbook is to help users and administrators use NetBox efficiently. It contains assorted recommendations and best practices compiled over time, intending to serve a wide variety of use cases.
|
||||
|
||||
## Server Configuration
|
||||
|
||||
### WSGI Server Configuration
|
||||
|
||||
NetBox operates as a [Web Server Gateway Interface (WSGI)](https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface) application, which sits behind a frontend HTTP server such as nginx or Apache. The HTTP server handles low-level HTTP request processing and serving static assets, and forwards application-level requests to NetBox via WSGI.
|
||||
|
||||
A backend WSGI server (typically [Gunicorn](https://gunicorn.org/) or [uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/)) is responsible for running the NetBox application. This is accomplished by initializing a number of WSGI worker processes which accept WSGI requests relayed from the frontend HTTP server.
|
||||
|
||||
Tuning your WSGI server is crucial to realizing optimal performance from NetBox. Below are some recommended configuration parameters.
|
||||
|
||||
#### Provision Multiple Workers
|
||||
|
||||
General guidance is to set the number of worker processes to double the number of CPU cores available, plus one (`2 * CPUs + 1`).
|
||||
|
||||
#### Limit the Worker Lifetime
|
||||
|
||||
Set a maximum number of requests that a worker can service before being respawned. This helps protect against potential memory leaks.
|
||||
|
||||
#### Set a Request Timeout
|
||||
|
||||
Limit the time a worker may spend processing any request. This prevents a long-running request from tying up a worker beyond an acceptable threshold. We suggest a limit of 120 seconds as a reasonable safeguard.
|
||||
|
||||
#### Bind Using a Unix Socket
|
||||
|
||||
When running the HTTP frontend and WSGI server on the same machine, binding via a Unix socket (instead of a TCP socket) may yield slight performance gains.
|
||||
|
||||
### NetBox Configuration
|
||||
|
||||
NetBox ships with a reasonable default configuration for most environments, but administrators are encouraged to explore all the [available parameters](../configuration/index.md) to tune their installation. Some of the most notable parameters impacting performance are called out below.
|
||||
|
||||
#### Reduce the Maximum Page Size
|
||||
|
||||
NetBox paginates large result sets to reduce the overall response size. The [`MAX_PAGE_SIZE`](../configuration/miscellaneous.md#max_page_size) parameter specifies the maximum number of results per page that a client can request. This is set to 1,000 by default. Consider lowering this number if you find that API clients are frequently requesting very large result sets.
|
||||
|
||||
#### Limit GraphQL Aliases
|
||||
|
||||
By default, NetBox restricts a GraphQL query to 10 aliases. Consider reducing this number by setting [`GRAPHQL_MAX_ALIASES`](../configuration/graphql-api.md#graphql_max_aliases) to a lower value.
|
||||
|
||||
#### Designate Isolated Deployments
|
||||
|
||||
If your NetBox installation does not have Internet access, set [`ISOLATED_DEPLOYMENT`](../configuration/system.md#isolated_deployment) to True. This will prevent the application from attempting routine external requests.
|
||||
|
||||
#### Reduce Sentry Sampling
|
||||
|
||||
If [Sentry](https://sentry.io/) has been enabled for error reporting and analytics, consider lowering its sampling rate. This can be accomplished by modifying the values for `sample_rate` and `traces_sample_rate` under [`SENTRY_CONFIG`](../configuration/error-reporting.md#sentry_config).
|
||||
|
||||
#### Remove Unneeded Event Handlers
|
||||
|
||||
Check whether any custom event handlers have been added under [`EVENTS_PIPELINE`](../configuration/miscellaneous.md#events_pipeline). Remove any that are no longer needed.
|
||||
|
||||
### Background Task Workers
|
||||
|
||||
NetBox defers the execution of certain tasks to background workers via Redis queues serviced by one or more background workers. These workers operate asynchronously from the frontend WSGI workers, and process tasks in the order they are enqueued.
|
||||
|
||||
NetBox creates three default queues for background tasks: `high`, `default`, and `low`. Additional queues can be configured via the [`QUEUE_MAPPINGS`](../configuration/miscellaneous.md#queue_mappings) configuration parameter.
|
||||
|
||||
By default, a background worker (spawned via `manage.py rqworker`) will listen to all available queues. To improve responsiveness to high-priority background tasks, consider dedicating one or more workers to service the `high` queue only:
|
||||
|
||||
```
|
||||
$ ./manage.py rqworker high
|
||||
19:31:20 Worker 861be45b32214afc95c235beeb19c9fa: started with PID 2300029, version 2.6.0
|
||||
19:31:20 Worker 861be45b32214afc95c235beeb19c9fa: subscribing to channel rq:pubsub:861be45b32214afc95c235beeb19c9fa
|
||||
19:31:20 *** Listening on high...
|
||||
19:31:20 Worker 861be45b32214afc95c235beeb19c9fa: cleaning registries for queue: high
|
||||
19:31:20 Scheduler for high started with PID 2300096
|
||||
```
|
||||
|
||||
## API Clients
|
||||
|
||||
### REST API
|
||||
|
||||
NetBox's [REST API](../integrations/rest-api.md) is the primary means of integration with external systems, allowing full create, read, update, and delete (CRUD) operations. There are a few performance considerations to keep in mind when dealing with very large data sets.
|
||||
|
||||
#### Use "Brief" Mode for Simple Lists
|
||||
|
||||
In cases where you need to retrieve only a minimal representation of objects, append `?brief=True` to the URL. This instructs NetBox to omit all fields except the following:
|
||||
|
||||
* ID
|
||||
* URL
|
||||
* Display text
|
||||
* Name (or similar identifier)
|
||||
* Slug (if present)
|
||||
* Description
|
||||
* Counts of notable related objects (where applicable)
|
||||
|
||||
For example, a site fetched using brief mode returns only the following:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 2,
|
||||
"url": "https://netbox/api/dcim/sites/2/",
|
||||
"display": "DM-Akron",
|
||||
"name": "DM-Akron",
|
||||
"slug": "dm-akron",
|
||||
"description": ""
|
||||
}
|
||||
```
|
||||
|
||||
Omitting all other fields (especially those which fetch and return related objects) often results in much faster queries.
|
||||
|
||||
#### Declare Selected Fields
|
||||
|
||||
If you need more flexibility regarding the fields to be returned for an object type, you can specify a list of fields to include using the `fields` query parameter. For example, a request for `/api/dcim/sites/?fields=id,name,status,region` will return the following:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 2,
|
||||
"name": "DM-Akron",
|
||||
"status": {
|
||||
"value": "active",
|
||||
"label": "Active"
|
||||
},
|
||||
"region": {
|
||||
"id": 51,
|
||||
"url": "https://netbox/api/dcim/regions/51/",
|
||||
"display": "Ohio",
|
||||
"name": "Ohio",
|
||||
"slug": "us-oh",
|
||||
"description": "",
|
||||
"site_count": 0,
|
||||
"_depth": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Like brief mode, this approach can significantly reduce the response time of an API request by omitting unneeded data.
|
||||
|
||||
#### Employ Pagination
|
||||
|
||||
Like the user interface, the REST API employs pagination to limit the number of objects returned in a single response. If a page size is not specified by the request (i.e. by passing `?limit=10`), NetBox will use the default size defined by [`PAGINATE_COUNT`](../configuration/default-values.md#paginate_count). The default page size is 50.
|
||||
|
||||
For some requests, especially those using brief mode or a minimal selection of fields, it may be desirable to specify a higher page size, so that fewer requests are needed to retrieve all objects. Appending `?limit=0` to the request effectively seeks to disable pagination. (Note, however, that the requested page size cannot exceed the value of [`MAX_PAGE_SIZE`](../configuration/miscellaneous.md#max_page_size), which defaults to 1,000.)
|
||||
|
||||
Complex API requests, which pull in many related objects, generate a relatively high load on the application, and generally benefit from reduced page size. If you find that your API requests are taking an inordinate amount of time, try reducing the page size from the default value so that fewer objects need to be returned for each request.
|
||||
|
||||
### GraphQL API
|
||||
|
||||
NetBox's read-only [GraphQL API](../integrations/graphql-api.md) offers an alternative to its REST API, and provides a very flexible means of retrieving data. GraphQL enables the client to request any object from a single endpoint, specifying only the desired attributes and relations. Many users prefer this to the more rigid structure of the REST API, but it's important to understand the trade-offs of crafting complex queries.
|
||||
|
||||
#### Request Only the Necessary Fields
|
||||
|
||||
For optimal performance, craft your GraphQL queries to return only the fields needed by the client. This will reduce the overall query time, especially when omitting related objects.
|
||||
|
||||
#### Avoid Overly Complex Queries
|
||||
|
||||
The primary benefit of the GraphQL API is that it allows the client to offload to the server the work of stitching together various related objects, which would require the client to make multiple requests to different endpoints if using the REST API. However, this advantage does not come for free: The more information that is requested in a single query, the more work the server needs to do to fetch the raw data from the database and render it into a GraphQL response. Very complex queries can yield dozens or hundreds of SQL queries on the backend, which increase the time it takes to render a response.
|
||||
|
||||
While it can be tempting to pack as much data as possible into a single GraphQL query, realize that there is a balance to be struck between minimizing the number of queries needed and avoiding complexity in the interest of performance. For example, while it is possible to retrieve via a single GraphQL API request all the IP addresses and all attached cables for every device in a site, it is probably more efficient (often _much_ more efficient) to make two or three separate requests and correlate the data locally.
|
||||
|
||||
#### Use Filters
|
||||
|
||||
You can specify filters when making a GraphQL query to limit the set of objects returned. This works a bit differently from the REST API, as filters are declared inside the query statement rather than appended to the URL, but the concept is the same. For example, to return only active sites:
|
||||
|
||||
```graphql
|
||||
query {
|
||||
site_list(
|
||||
filters: {
|
||||
status: STATUS_ACTIVE
|
||||
}
|
||||
) {
|
||||
name
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This returns only sites with a status of "active" and avoid needing to parse through all the others. For further information about filters, see the [GraphQL API documentation](../integrations/graphql-api.md).
|
||||
|
||||
#### Employ Pagination
|
||||
|
||||
Like the REST API, the GraphQL API supports pagination. Queries which return a large number of objects should employ pagination to limit the size of each response.
|
||||
|
||||
```graphql
|
||||
{
|
||||
device_list(
|
||||
pagination: {limit: 100}
|
||||
) {
|
||||
id
|
||||
name
|
||||
serial
|
||||
status
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
This parameter controls the content and layout of user's default dashboard. Once the dashboard has been created, the user is free to customize it as they please by adding, removing, and reconfiguring widgets.
|
||||
|
||||
This parameter must specify an iterable of dictionaries, each representing a discrete dashboard widget and its configuration. The follow widget attributes are supported:
|
||||
This parameter must specify an iterable of dictionaries, each representing a discrete dashboard widget and its configuration. The following widget attributes are supported:
|
||||
|
||||
* `widget`: Dotted path to the Python class (required)
|
||||
* `width`: Default widget width (between 1 and 12, inclusive)
|
||||
@ -63,6 +63,8 @@ DEFAULT_USER_PREFERENCES = {
|
||||
|
||||
For a complete list of available preferences, log into NetBox and navigate to `/user/preferences/`. A period in a preference name indicates a level of nesting in the JSON data. The example above maps to `pagination.per_page`.
|
||||
|
||||
See also: [Clearing table preferences](../features/user-preferences.md#clearing-table-preferences) for resolving errors caused by saved table columns or ordering.
|
||||
|
||||
---
|
||||
|
||||
## PAGINATE_COUNT
|
||||
|
||||
@ -1,7 +1,32 @@
|
||||
# Error Reporting Settings
|
||||
|
||||
## SENTRY_CONFIG
|
||||
|
||||
A dictionary mapping keyword arguments to values, to be passed to `sentry_sdk.init()`. See the [Sentry Python SDK documentation](https://docs.sentry.io/platforms/python/) for more information on supported parameters.
|
||||
|
||||
The default configuration is shown below:
|
||||
|
||||
```python
|
||||
{
|
||||
"sample_rate": 1.0,
|
||||
"send_default_pii": False,
|
||||
"traces_sample_rate": 0,
|
||||
}
|
||||
```
|
||||
|
||||
Additionally, `http_proxy` and `https_proxy` are set to the HTTP and HTTPS proxies, respectively, configured for NetBox (if any).
|
||||
|
||||
## SENTRY_DSN
|
||||
|
||||
!!! warning "This parameter will be removed in NetBox v4.5."
|
||||
Set this using `SENTRY_CONFIG` instead:
|
||||
|
||||
```
|
||||
SENTRY_CONFIG = {
|
||||
"dsn": "https://examplePublicKey@o0.ingest.sentry.io/0",
|
||||
}
|
||||
```
|
||||
|
||||
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:
|
||||
@ -25,6 +50,15 @@ Set to `True` to enable automatic error reporting via [Sentry](https://sentry.io
|
||||
|
||||
## SENTRY_SAMPLE_RATE
|
||||
|
||||
!!! warning "This parameter will be removed in NetBox v4.5."
|
||||
Set this using `SENTRY_CONFIG` instead:
|
||||
|
||||
```
|
||||
SENTRY_CONFIG = {
|
||||
"sample_rate": 0.2,
|
||||
}
|
||||
```
|
||||
|
||||
Default: `1.0` (all)
|
||||
|
||||
The sampling rate for errors. Must be a value between 0 (disabled) and 1.0 (report on all errors).
|
||||
@ -33,6 +67,15 @@ The sampling rate for errors. Must be a value between 0 (disabled) and 1.0 (repo
|
||||
|
||||
## SENTRY_SEND_DEFAULT_PII
|
||||
|
||||
!!! warning "This parameter will be removed in NetBox v4.5."
|
||||
Set this using `SENTRY_CONFIG` instead:
|
||||
|
||||
```
|
||||
SENTRY_CONFIG = {
|
||||
"send_default_pii": True,
|
||||
}
|
||||
```
|
||||
|
||||
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,6 +103,15 @@ SENTRY_TAGS = {
|
||||
|
||||
## SENTRY_TRACES_SAMPLE_RATE
|
||||
|
||||
!!! warning "This parameter will be removed in NetBox v4.5."
|
||||
Set this using `SENTRY_CONFIG` instead:
|
||||
|
||||
```
|
||||
SENTRY_CONFIG = {
|
||||
"traces_sample_rate": 0.2,
|
||||
}
|
||||
```
|
||||
|
||||
Default: `0` (disabled)
|
||||
|
||||
The sampling rate for transactions. Must be a value between 0 (disabled) and 1.0 (report on all transactions).
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
The `users.UserConfig` model holds individual preferences for each user in the form of JSON data. This page serves as a manifest of all recognized user preferences in NetBox.
|
||||
|
||||
For end‑user guidance on resetting saved table layouts, see [Features > User Preferences](../features/user-preferences.md#clearing-table-preferences).
|
||||
|
||||
## Available Preferences
|
||||
|
||||
| Name | Description |
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
While NetBox strives to meet the needs of every network, the needs of users to cater to their own unique environments cannot be ignored. NetBox was built with this in mind, and can be customized in many ways to better suit your particular needs.
|
||||
|
||||
For end‑user personalization topics (bookmarks, table preferences, language, CSV delimiter, and more), see [Features > User Preferences](../features/user-preferences.md).
|
||||
|
||||
## Tags
|
||||
|
||||
Most objects in NetBox can be assigned user-created tags to aid with organization and filtering. Tag values are completely arbitrary: They may be used to store data in key-value pairs, or they may be employed simply as labels against which objects can be filtered. Each tag can also be assigned a color for quicker differentiation in the user interface.
|
||||
@ -18,10 +20,6 @@ The `tag` filter can be specified multiple times to match only objects which hav
|
||||
GET /api/dcim/devices/?tag=monitored&tag=deprecated
|
||||
```
|
||||
|
||||
## Bookmarks
|
||||
|
||||
Users can bookmark their most commonly visited objects for convenient access. Bookmarks are listed under a user's profile, and can be displayed with custom filtering and ordering on the user's personal dashboard.
|
||||
|
||||
## Custom Fields
|
||||
|
||||
While NetBox provides a rather extensive data model out of the box, the need may arise to store certain additional data associated with NetBox objects. For example, you might need to record the invoice ID alongside an installed device, or record an approving authority when creating a new IP prefix. NetBox administrators can create custom fields on built-in objects to meet these needs.
|
||||
@ -38,7 +36,7 @@ Custom links allow you to conveniently reference external resources related to N
|
||||
http://server.local/vms/?name={{ object.name }}
|
||||
```
|
||||
|
||||
Now, when viewing a virtual machine in NetBox, a user will see a handy button with the chosen title and link (complete with the name of the VM being viewed). Both the text and URL of custom links can be templatized in this manner, and custom links can be grouped together into dropdowns for more efficient display.
|
||||
Now, when viewing a virtual machine in NetBox, a user will see a handy button with the chosen title and link (complete with the name of the VM being viewed). Both the text and URL of custom links can be templatized in this manner, and custom links can be grouped together into dropdowns for a more efficient display.
|
||||
|
||||
To learn more about this feature, check out the [custom link documentation](../customization/custom-links.md).
|
||||
|
||||
|
||||
63
docs/features/user-preferences.md
Normal file
63
docs/features/user-preferences.md
Normal file
@ -0,0 +1,63 @@
|
||||
# User Preferences
|
||||
|
||||
NetBox stores per‑user options that control aspects of the web interface and data display. Preferences persist across sessions and can be managed under **User → Preferences**.
|
||||
|
||||
## Table configurations
|
||||
|
||||
When a list view is configured using **Configure**, NetBox records the selected columns and ordering as per‑user table preferences for that table. These preferences are applied automatically on subsequent visits.
|
||||
|
||||
### Clearing table preferences
|
||||
|
||||
Saved table preferences may need to be reset, for example, if a table fails to render or after an upgrade that changes available columns.
|
||||
|
||||
To clear saved preferences for one or more tables:
|
||||
|
||||
1. Click the username in the top‑right corner.
|
||||
2. Select **Preferences** from the dropdown.
|
||||
3. Scroll to the **Table Configurations** section.
|
||||
4. Select the tables to reset.
|
||||
5. Click **Submit** to clear the selected preferences.
|
||||
|
||||
After clearing preferences, reopen the list view and use **Configure** to set the desired columns and ordering.
|
||||
|
||||
!!! note
|
||||
Per‑user table preferences are distinct from **Table Configs**, which are named, reusable configurations managed under *Customization → Table Configs*. Clearing preferences does not delete any Table Configs. See [Table Configs](../models/extras/tableconfig.md) for details.
|
||||
|
||||
## Other preferences
|
||||
|
||||
### Language
|
||||
Selects the user interface language from installed translations (subject to system configuration).
|
||||
|
||||
### Page length
|
||||
Sets the default number of rows displayed on paginated tables.
|
||||
|
||||
### Paginator placement
|
||||
Controls where pagination controls are rendered relative to a table.
|
||||
|
||||
### HTMX navigation (experimental)
|
||||
Enables partial‑page navigation for supported views. Disable this preference if unexpected behavior is observed.
|
||||
|
||||
### Striped table rows
|
||||
Toggles alternating row backgrounds on tables.
|
||||
|
||||
### Data format (raw views)
|
||||
Sets the default format (JSON or YAML) when rendering raw data blocks.
|
||||
|
||||
### CSV delimiter
|
||||
Overrides the delimiter used when exporting CSV data.
|
||||
|
||||
## Bookmarks
|
||||
|
||||
Users can bookmark frequently visited objects for convenient access. Bookmarks appear under the user menu and can be displayed on the personal dashboard using the bookmarks' widget. See [Bookmark](../models/extras/bookmark.md) for model details.
|
||||
|
||||
## Notifications and subscriptions
|
||||
|
||||
Users may subscribe to objects to receive notifications when changes occur. Notifications are listed under the user menu and can be marked as read or deleted. See [Features > Notifications](notifications.md) and the data‑model references for [Subscription](../models/extras/subscription.md) and [Notification](../models/extras/notification.md).
|
||||
|
||||
## Admin defaults
|
||||
|
||||
Administrators can define defaults for new users via [`DEFAULT_USER_PREFERENCES`](../configuration/default-values.md#default_user_preferences). Users may override these values under their own preferences.
|
||||
|
||||
## See also
|
||||
|
||||
- [Development > User Preferences](../development/user-preferences.md) (manifest of recognized preference keys)
|
||||
@ -4,6 +4,9 @@ This object represents the saved configuration of an object table in NetBox. Tab
|
||||
|
||||
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.
|
||||
|
||||
!!! note
|
||||
Per‑user table preferences (columns and ordering remembered for an individual user) are distinct from Table Configs. If a list view fails to render due to outdated saved preferences, see [Clearing table preferences](../../features/user-preferences.md#clearing-table-preferences).
|
||||
|
||||
## Fields
|
||||
|
||||
### Name
|
||||
@ -20,7 +23,7 @@ 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.)
|
||||
The name of the specific table to which the table config pertains. (Some NetBox objects use multiple tables.)
|
||||
|
||||
### Weight
|
||||
|
||||
|
||||
@ -1,5 +1,34 @@
|
||||
# NetBox v4.4
|
||||
|
||||
## v4.4.2 (2025-09-30)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#17010](https://github.com/netbox-community/netbox/issues/17010) - Show admin navigation menu items only for staff & superusers
|
||||
* [#19590](https://github.com/netbox-community/netbox/issues/19590) - Add columns for device site & location to device component tables
|
||||
* [#19765](https://github.com/netbox-community/netbox/issues/19765) - Linkify assigned object types under saved filter view
|
||||
* [#20308](https://github.com/netbox-community/netbox/issues/20308) - Add a hotkey (`/`) for the global search field
|
||||
* [#20332](https://github.com/netbox-community/netbox/issues/20332) - Add a "none" option to object tag filters
|
||||
* [#20380](https://github.com/netbox-community/netbox/issues/20380) - Introduce the `SENTRY_CONFIG` configuration parameter
|
||||
* [#20412](https://github.com/netbox-community/netbox/issues/20412) - Linkify cluster type on virtual machine detail view
|
||||
* [#20438](https://github.com/netbox-community/netbox/issues/20438) - Add `facility` field to bulk edit forms for sites and locations
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#18878](https://github.com/netbox-community/netbox/issues/18878) - Automatically assign a designated primary MAC address upon creation of a new interface
|
||||
* [#20243](https://github.com/netbox-community/netbox/issues/20243) - Prevent scheduled system jobs from re-running multiple times
|
||||
* [#20253](https://github.com/netbox-community/netbox/issues/20253) - Fix support for filtering object contact assignments in GraphQL API
|
||||
* [#20365](https://github.com/netbox-community/netbox/issues/20365) - Address various inaccuracies in generated OpenAPI schema
|
||||
* [#20375](https://github.com/netbox-community/netbox/issues/20375) - Preserve filter parameters when performing bulk operations
|
||||
* [#20390](https://github.com/netbox-community/netbox/issues/20390) - Fix styling of page size selection dropdown
|
||||
* [#20392](https://github.com/netbox-community/netbox/issues/20392) - Clean up ordering of interface type options
|
||||
* [#20398](https://github.com/netbox-community/netbox/issues/20398) - Fix misleading error reporting for min/max custom field values
|
||||
* [#20419](https://github.com/netbox-community/netbox/issues/20419) - Correct action buttons for child object views
|
||||
* [#20425](https://github.com/netbox-community/netbox/issues/20425) - Fix Markdown preview functionality within "quick add" modal
|
||||
* [#20441](https://github.com/netbox-community/netbox/issues/20441) - Fix display of the "groups" column in contact assignments table
|
||||
|
||||
---
|
||||
|
||||
## v4.4.1 (2025-09-16)
|
||||
|
||||
### Enhancements
|
||||
|
||||
@ -86,6 +86,7 @@ nav:
|
||||
- Change Logging: 'features/change-logging.md'
|
||||
- Journaling: 'features/journaling.md'
|
||||
- Event Rules: 'features/event-rules.md'
|
||||
- User Preferences: 'features/user-preferences.md'
|
||||
- Notifications: 'features/notifications.md'
|
||||
- Background Jobs: 'features/background-jobs.md'
|
||||
- Auth & Permissions: 'features/authentication-permissions.md'
|
||||
@ -126,6 +127,7 @@ nav:
|
||||
- Custom Scripts: 'customization/custom-scripts.md'
|
||||
- Best Practices:
|
||||
- Modeling Pluggable Transceivers: 'best-practices/modeling-pluggable-transceivers.md'
|
||||
- Performance Handbook: 'best-practices/performance-handbook.md'
|
||||
- Integrations:
|
||||
- REST API: 'integrations/rest-api.md'
|
||||
- GraphQL API: 'integrations/graphql-api.md'
|
||||
|
||||
@ -13,7 +13,7 @@ class BackgroundTaskSerializer(serializers.Serializer):
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name='core-api:rqtask-detail',
|
||||
lookup_field='id',
|
||||
lookup_url_kwarg='pk'
|
||||
lookup_url_kwarg='id'
|
||||
)
|
||||
description = serializers.CharField()
|
||||
origin = serializers.CharField()
|
||||
|
||||
@ -5,7 +5,7 @@ from django_rq.queues import get_redis_connection
|
||||
from django_rq.settings import QUEUES_LIST
|
||||
from django_rq.utils import get_statistics
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from drf_spectacular.utils import OpenApiParameter, extend_schema
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
@ -117,29 +117,49 @@ class BaseRQViewSet(viewsets.ViewSet):
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
"""
|
||||
Return the serializer instance that should be used for validating and
|
||||
deserializing input, and for serializing output.
|
||||
deserializing input and for serializing output.
|
||||
"""
|
||||
serializer_class = self.get_serializer_class()
|
||||
kwargs['context'] = self.get_serializer_context()
|
||||
return serializer_class(*args, **kwargs)
|
||||
|
||||
def get_serializer_class(self):
|
||||
"""
|
||||
Return the class to use for the serializer.
|
||||
"""
|
||||
return self.serializer_class
|
||||
|
||||
def get_serializer_context(self):
|
||||
"""
|
||||
Extra context provided to the serializer class.
|
||||
"""
|
||||
return {
|
||||
'request': self.request,
|
||||
'format': self.format_kwarg,
|
||||
'view': self,
|
||||
}
|
||||
|
||||
|
||||
class BackgroundQueueViewSet(BaseRQViewSet):
|
||||
"""
|
||||
Retrieve a list of RQ Queues.
|
||||
Note: Queue names are not URL safe so not returning a detail view.
|
||||
Note: Queue names are not URL safe, so not returning a detail view.
|
||||
"""
|
||||
serializer_class = serializers.BackgroundQueueSerializer
|
||||
lookup_field = 'name'
|
||||
lookup_value_regex = r'[\w.@+-]+'
|
||||
|
||||
def get_view_name(self):
|
||||
return "Background Queues"
|
||||
return 'Background Queues'
|
||||
|
||||
def get_data(self):
|
||||
return get_statistics(run_maintenance_tasks=True)["queues"]
|
||||
return get_statistics(run_maintenance_tasks=True)['queues']
|
||||
|
||||
@extend_schema(responses={200: OpenApiTypes.OBJECT})
|
||||
@extend_schema(
|
||||
operation_id='core_background_queues_retrieve_by_name',
|
||||
parameters=[OpenApiParameter(name='name', type=OpenApiTypes.STR, location=OpenApiParameter.PATH)],
|
||||
responses={200: OpenApiTypes.OBJECT},
|
||||
)
|
||||
def retrieve(self, request, name):
|
||||
data = self.get_data()
|
||||
if not data:
|
||||
@ -161,12 +181,17 @@ class BackgroundWorkerViewSet(BaseRQViewSet):
|
||||
lookup_field = 'name'
|
||||
|
||||
def get_view_name(self):
|
||||
return "Background Workers"
|
||||
return 'Background Workers'
|
||||
|
||||
def get_data(self):
|
||||
config = QUEUES_LIST[0]
|
||||
return Worker.all(get_redis_connection(config['connection_config']))
|
||||
|
||||
@extend_schema(
|
||||
operation_id='core_background_workers_retrieve_by_name',
|
||||
parameters=[OpenApiParameter(name='name', type=OpenApiTypes.STR, location=OpenApiParameter.PATH)],
|
||||
responses={200: OpenApiTypes.OBJECT},
|
||||
)
|
||||
def retrieve(self, request, name):
|
||||
# all the RQ queues should use the same connection
|
||||
config = QUEUES_LIST[0]
|
||||
@ -184,9 +209,10 @@ class BackgroundTaskViewSet(BaseRQViewSet):
|
||||
Retrieve a list of RQ Tasks.
|
||||
"""
|
||||
serializer_class = serializers.BackgroundTaskSerializer
|
||||
lookup_field = 'id'
|
||||
|
||||
def get_view_name(self):
|
||||
return "Background Tasks"
|
||||
return 'Background Tasks'
|
||||
|
||||
def get_data(self):
|
||||
return get_rq_jobs()
|
||||
@ -199,45 +225,53 @@ class BackgroundTaskViewSet(BaseRQViewSet):
|
||||
|
||||
return task
|
||||
|
||||
@extend_schema(responses={200: OpenApiTypes.OBJECT})
|
||||
def retrieve(self, request, pk):
|
||||
@extend_schema(
|
||||
operation_id='core_background_tasks_retrieve_by_id',
|
||||
parameters=[OpenApiParameter(name='id', type=OpenApiTypes.STR, location=OpenApiParameter.PATH)],
|
||||
responses={200: OpenApiTypes.OBJECT},
|
||||
)
|
||||
def retrieve(self, request, id):
|
||||
"""
|
||||
Retrieve the details of the specified RQ Task.
|
||||
"""
|
||||
task = self.get_task_from_id(pk)
|
||||
task = self.get_task_from_id(id)
|
||||
serializer = self.serializer_class(task, context={'request': request})
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(methods=["POST"], detail=True)
|
||||
def delete(self, request, pk):
|
||||
@extend_schema(parameters=[OpenApiParameter(name='id', type=OpenApiTypes.STR, location=OpenApiParameter.PATH)])
|
||||
@action(methods=['POST'], detail=True)
|
||||
def delete(self, request, id):
|
||||
"""
|
||||
Delete the specified RQ Task.
|
||||
"""
|
||||
delete_rq_job(pk)
|
||||
delete_rq_job(id)
|
||||
return HttpResponse(status=200)
|
||||
|
||||
@action(methods=["POST"], detail=True)
|
||||
def requeue(self, request, pk):
|
||||
@extend_schema(parameters=[OpenApiParameter(name='id', type=OpenApiTypes.STR, location=OpenApiParameter.PATH)])
|
||||
@action(methods=['POST'], detail=True)
|
||||
def requeue(self, request, id):
|
||||
"""
|
||||
Requeues the specified RQ Task.
|
||||
"""
|
||||
requeue_rq_job(pk)
|
||||
requeue_rq_job(id)
|
||||
return HttpResponse(status=200)
|
||||
|
||||
@action(methods=["POST"], detail=True)
|
||||
def enqueue(self, request, pk):
|
||||
@extend_schema(parameters=[OpenApiParameter(name='id', type=OpenApiTypes.STR, location=OpenApiParameter.PATH)])
|
||||
@action(methods=['POST'], detail=True)
|
||||
def enqueue(self, request, id):
|
||||
"""
|
||||
Enqueues the specified RQ Task.
|
||||
"""
|
||||
enqueue_rq_job(pk)
|
||||
enqueue_rq_job(id)
|
||||
return HttpResponse(status=200)
|
||||
|
||||
@action(methods=["POST"], detail=True)
|
||||
def stop(self, request, pk):
|
||||
@extend_schema(parameters=[OpenApiParameter(name='id', type=OpenApiTypes.STR, location=OpenApiParameter.PATH)])
|
||||
@action(methods=['POST'], detail=True)
|
||||
def stop(self, request, id):
|
||||
"""
|
||||
Stops the specified RQ Task.
|
||||
"""
|
||||
stopped_jobs = stop_rq_job(pk)
|
||||
stopped_jobs = stop_rq_job(id)
|
||||
if len(stopped_jobs) == 1:
|
||||
return HttpResponse(status=200)
|
||||
else:
|
||||
|
||||
@ -1163,14 +1163,14 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
(
|
||||
(TYPE_1GE_BX10_D, '1000BASE-BX10-D (1GE BiDi Down)'),
|
||||
(TYPE_1GE_BX10_U, '1000BASE-BX10-U (1GE BiDi Up)'),
|
||||
(TYPE_1GE_CX, '1000BASE-CX (1GE DAC)'),
|
||||
(TYPE_1GE_CWDM, '1000BASE-CWDM (1GE)'),
|
||||
(TYPE_1GE_CX, '1000BASE-CX (1GE DAC)'),
|
||||
(TYPE_1GE_DWDM, '1000BASE-DWDM (1GE)'),
|
||||
(TYPE_1GE_EX, '1000BASE-EX (1GE)'),
|
||||
(TYPE_1GE_SX_FIXED, '1000BASE-SX (1GE)'),
|
||||
(TYPE_1GE_LSX, '1000BASE-LSX (1GE)'),
|
||||
(TYPE_1GE_LX_FIXED, '1000BASE-LX (1GE)'),
|
||||
(TYPE_1GE_LX10, '1000BASE-LX10/LH (1GE)'),
|
||||
(TYPE_1GE_SX_FIXED, '1000BASE-SX (1GE)'),
|
||||
(TYPE_1GE_FIXED, '1000BASE-T (1GE)'),
|
||||
(TYPE_1GE_TX_FIXED, '1000BASE-TX (1GE)'),
|
||||
(TYPE_1GE_ZX, '1000BASE-ZX (1GE)'),
|
||||
@ -1186,8 +1186,8 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
(
|
||||
_('10 Gbps Ethernet'),
|
||||
(
|
||||
(TYPE_10GE_BR_D, '10GBASE-DR-D (10GE BiDi Down)'),
|
||||
(TYPE_10GE_BR_U, '10GBASE-DR-U (10GE BiDi Up)'),
|
||||
(TYPE_10GE_BR_D, '10GBASE-BR-D (10GE BiDi Down)'),
|
||||
(TYPE_10GE_BR_U, '10GBASE-BR-U (10GE BiDi Up)'),
|
||||
(TYPE_10GE_CX4, '10GBASE-CX4 (10GE DAC)'),
|
||||
(TYPE_10GE_ER, '10GBASE-ER (10GE)'),
|
||||
(TYPE_10GE_LR, '10GBASE-LR (10GE)'),
|
||||
@ -1235,6 +1235,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
(TYPE_100GE_CR2, '100GBASE-CR2 (100GE DAC)'),
|
||||
(TYPE_100GE_CR4, '100GBASE-CR4 (100GE DAC)'),
|
||||
(TYPE_100GE_CR10, '100GBASE-CR10 (100GE DAC)'),
|
||||
(TYPE_100GE_CWDM4, '100GBASE-CWDM4 (100GE)'),
|
||||
(TYPE_100GE_DR, '100GBASE-DR (100GE)'),
|
||||
(TYPE_100GE_ER4, '100GBASE-ER4 (100GE)'),
|
||||
(TYPE_100GE_FR1, '100GBASE-FR1 (100GE)'),
|
||||
@ -1253,12 +1254,12 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
(
|
||||
(TYPE_200GE_CR2, '200GBASE-CR2 (200GE)'),
|
||||
(TYPE_200GE_CR4, '200GBASE-CR4 (200GE)'),
|
||||
(TYPE_200GE_SR2, '200GBASE-SR2 (200GE)'),
|
||||
(TYPE_200GE_SR4, '200GBASE-SR4 (200GE)'),
|
||||
(TYPE_200GE_DR4, '200GBASE-DR4 (200GE)'),
|
||||
(TYPE_200GE_ER4, '200GBASE-ER4 (200GE)'),
|
||||
(TYPE_200GE_FR4, '200GBASE-FR4 (200GE)'),
|
||||
(TYPE_200GE_LR4, '200GBASE-LR4 (200GE)'),
|
||||
(TYPE_200GE_SR2, '200GBASE-SR2 (200GE)'),
|
||||
(TYPE_200GE_SR4, '200GBASE-SR4 (200GE)'),
|
||||
(TYPE_200GE_VR2, '200GBASE-VR2 (200GE)'),
|
||||
)
|
||||
),
|
||||
@ -1296,34 +1297,34 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
(TYPE_1GE_GBIC, 'GBIC (1GE)'),
|
||||
(TYPE_1GE_SFP, 'SFP (1GE)'),
|
||||
(TYPE_10GE_SFP_PLUS, 'SFP+ (10GE)'),
|
||||
(TYPE_10GE_XFP, 'XFP (10GE)'),
|
||||
(TYPE_10GE_XENPAK, 'XENPAK (10GE)'),
|
||||
(TYPE_10GE_XFP, 'XFP (10GE)'),
|
||||
(TYPE_10GE_X2, 'X2 (10GE)'),
|
||||
(TYPE_25GE_SFP28, 'SFP28 (25GE)'),
|
||||
(TYPE_50GE_SFP56, 'SFP56 (50GE)'),
|
||||
(TYPE_40GE_QSFP_PLUS, 'QSFP+ (40GE)'),
|
||||
(TYPE_50GE_QSFP28, 'QSFP28 (50GE)'),
|
||||
(TYPE_50GE_SFP56, 'SFP56 (50GE)'),
|
||||
(TYPE_100GE_CFP, 'CFP (100GE)'),
|
||||
(TYPE_100GE_CFP2, 'CFP2 (100GE)'),
|
||||
(TYPE_200GE_CFP2, 'CFP2 (200GE)'),
|
||||
(TYPE_400GE_CFP2, 'CFP2 (400GE)'),
|
||||
(TYPE_100GE_CFP4, 'CFP4 (100GE)'),
|
||||
(TYPE_100GE_CXP, 'CXP (100GE)'),
|
||||
(TYPE_100GE_CPAK, 'Cisco CPAK (100GE)'),
|
||||
(TYPE_100GE_DSFP, 'DSFP (100GE)'),
|
||||
(TYPE_100GE_SFP_DD, 'SFP-DD (100GE)'),
|
||||
(TYPE_100GE_QSFP28, 'QSFP28 (100GE)'),
|
||||
(TYPE_100GE_QSFP_DD, 'QSFP-DD (100GE)'),
|
||||
(TYPE_100GE_SFP_DD, 'SFP-DD (100GE)'),
|
||||
(TYPE_200GE_CFP2, 'CFP2 (200GE)'),
|
||||
(TYPE_200GE_QSFP56, 'QSFP56 (200GE)'),
|
||||
(TYPE_200GE_QSFP_DD, 'QSFP-DD (200GE)'),
|
||||
(TYPE_400GE_QSFP112, 'QSFP112 (400GE)'),
|
||||
(TYPE_400GE_QSFP_DD, 'QSFP-DD (400GE)'),
|
||||
(TYPE_400GE_CDFP, 'CDFP (400GE)'),
|
||||
(TYPE_400GE_CFP2, 'CFP2 (400GE)'),
|
||||
(TYPE_400GE_CFP8, 'CPF8 (400GE)'),
|
||||
(TYPE_400GE_OSFP, 'OSFP (400GE)'),
|
||||
(TYPE_400GE_OSFP_RHS, 'OSFP-RHS (400GE)'),
|
||||
(TYPE_400GE_CDFP, 'CDFP (400GE)'),
|
||||
(TYPE_400GE_CFP8, 'CPF8 (400GE)'),
|
||||
(TYPE_800GE_QSFP_DD, 'QSFP-DD (800GE)'),
|
||||
(TYPE_800GE_OSFP, 'OSFP (800GE)'),
|
||||
(TYPE_800GE_QSFP_DD, 'QSFP-DD (800GE)'),
|
||||
)
|
||||
),
|
||||
(
|
||||
|
||||
@ -26,7 +26,7 @@ class eui64_unix_expanded_uppercase(eui64_unix_expanded):
|
||||
#
|
||||
|
||||
class MACAddressField(models.Field):
|
||||
description = "PostgreSQL MAC Address field"
|
||||
description = 'PostgreSQL MAC Address field'
|
||||
|
||||
def python_type(self):
|
||||
return EUI
|
||||
@ -34,6 +34,9 @@ class MACAddressField(models.Field):
|
||||
def from_db_value(self, value, expression, connection):
|
||||
return self.to_python(value)
|
||||
|
||||
def get_internal_type(self):
|
||||
return 'CharField'
|
||||
|
||||
def to_python(self, value):
|
||||
if value is None:
|
||||
return value
|
||||
@ -54,7 +57,7 @@ class MACAddressField(models.Field):
|
||||
|
||||
|
||||
class WWNField(models.Field):
|
||||
description = "World Wide Name field"
|
||||
description = 'World Wide Name field'
|
||||
|
||||
def python_type(self):
|
||||
return EUI
|
||||
@ -62,6 +65,9 @@ class WWNField(models.Field):
|
||||
def from_db_value(self, value, expression, connection):
|
||||
return self.to_python(value)
|
||||
|
||||
def get_internal_type(self):
|
||||
return 'CharField'
|
||||
|
||||
def to_python(self, value):
|
||||
if value is None:
|
||||
return value
|
||||
|
||||
@ -133,6 +133,11 @@ class SiteBulkEditForm(NetBoxModelBulkEditForm):
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False
|
||||
)
|
||||
facility = forms.CharField(
|
||||
label=_('Facility'),
|
||||
max_length=50,
|
||||
required=False
|
||||
)
|
||||
asns = DynamicModelMultipleChoiceField(
|
||||
queryset=ASN.objects.all(),
|
||||
label=_('ASNs'),
|
||||
@ -166,10 +171,10 @@ class SiteBulkEditForm(NetBoxModelBulkEditForm):
|
||||
|
||||
model = Site
|
||||
fieldsets = (
|
||||
FieldSet('status', 'region', 'group', 'tenant', 'asns', 'time_zone', 'description'),
|
||||
FieldSet('status', 'region', 'group', 'tenant', 'facility', 'asns', 'time_zone', 'description'),
|
||||
)
|
||||
nullable_fields = (
|
||||
'region', 'group', 'tenant', 'asns', 'time_zone', 'description', 'comments',
|
||||
'region', 'group', 'tenant', 'facility', 'asns', 'time_zone', 'description', 'comments',
|
||||
)
|
||||
|
||||
|
||||
@ -198,6 +203,11 @@ class LocationBulkEditForm(NetBoxModelBulkEditForm):
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False
|
||||
)
|
||||
facility = forms.CharField(
|
||||
label=_('Facility'),
|
||||
max_length=50,
|
||||
required=False
|
||||
)
|
||||
description = forms.CharField(
|
||||
label=_('Description'),
|
||||
max_length=200,
|
||||
@ -207,9 +217,9 @@ class LocationBulkEditForm(NetBoxModelBulkEditForm):
|
||||
|
||||
model = Location
|
||||
fieldsets = (
|
||||
FieldSet('site', 'parent', 'status', 'tenant', 'description'),
|
||||
FieldSet('site', 'parent', 'status', 'tenant', 'facility', 'description'),
|
||||
)
|
||||
nullable_fields = ('parent', 'tenant', 'description', 'comments')
|
||||
nullable_fields = ('parent', 'tenant', 'facility', 'description', 'comments')
|
||||
|
||||
|
||||
class RackRoleBulkEditForm(NetBoxModelBulkEditForm):
|
||||
|
||||
@ -632,10 +632,17 @@ class BaseInterface(models.Model):
|
||||
})
|
||||
|
||||
# Check that the primary MAC address (if any) is assigned to this interface
|
||||
if self.primary_mac_address and self.primary_mac_address.assigned_object != self:
|
||||
if (
|
||||
self.primary_mac_address and
|
||||
self.primary_mac_address.assigned_object is not None and
|
||||
self.primary_mac_address.assigned_object != self
|
||||
):
|
||||
raise ValidationError({
|
||||
'primary_mac_address': _("MAC address {mac_address} is not assigned to this interface.").format(
|
||||
mac_address=self.primary_mac_address
|
||||
'primary_mac_address': _(
|
||||
"MAC address {mac_address} is assigned to a different interface ({interface})."
|
||||
).format(
|
||||
mac_address=self.primary_mac_address,
|
||||
interface=self.primary_mac_address.assigned_object,
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ from django.db.models.signals import post_save, post_delete, pre_delete
|
||||
from django.dispatch import receiver
|
||||
|
||||
from dcim.choices import CableEndChoices, LinkStatusChoices
|
||||
from virtualization.models import VMInterface
|
||||
from .models import (
|
||||
Cable, CablePath, CableTermination, ConsolePort, ConsoleServerPort, Device, DeviceBay, FrontPort, Interface,
|
||||
InventoryItem, ModuleBay, PathEndpoint, PowerOutlet, PowerPanel, PowerPort, Rack, RearPort, Location,
|
||||
@ -170,3 +171,15 @@ def extend_rearport_cable_paths(instance, created, raw, **kwargs):
|
||||
rearport = instance.rear_port
|
||||
for cablepath in CablePath.objects.filter(_nodes__contains=rearport):
|
||||
cablepath.retrace()
|
||||
|
||||
|
||||
@receiver(post_save, sender=Interface)
|
||||
@receiver(post_save, sender=VMInterface)
|
||||
def update_mac_address_interface(instance, created, raw, **kwargs):
|
||||
"""
|
||||
When creating a new Interface or VMInterface, check whether a MACAddress has been designated as its primary. If so,
|
||||
assign the MACAddress to the interface.
|
||||
"""
|
||||
if created and not raw and instance.primary_mac_address:
|
||||
instance.primary_mac_address.assigned_object = instance
|
||||
instance.primary_mac_address.save()
|
||||
|
||||
@ -312,6 +312,16 @@ class DeviceComponentTable(NetBoxTable):
|
||||
verbose_name=_('Name'),
|
||||
linkify=True,
|
||||
)
|
||||
device_location = tables.Column(
|
||||
accessor=tables.A('device__location'),
|
||||
verbose_name=_('Device Location'),
|
||||
linkify=True,
|
||||
)
|
||||
device_site = tables.Column(
|
||||
accessor=tables.A('device__site'),
|
||||
verbose_name=_('Device Site'),
|
||||
linkify=True,
|
||||
)
|
||||
device_status = columns.ChoiceFieldColumn(
|
||||
accessor=tables.A('device__status'),
|
||||
verbose_name=_('Device Status'),
|
||||
|
||||
@ -26,6 +26,7 @@ class CustomFieldChoiceSetSerializer(ChangeLogMessageSerializer, ValidatedModelS
|
||||
max_length=2
|
||||
)
|
||||
)
|
||||
choices_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = CustomFieldChoiceSet
|
||||
|
||||
@ -26,6 +26,9 @@ class BaseIPField(models.Field):
|
||||
def from_db_value(self, value, expression, connection):
|
||||
return self.to_python(value)
|
||||
|
||||
def get_internal_type(self):
|
||||
return 'CharField'
|
||||
|
||||
def to_python(self, value):
|
||||
if not value:
|
||||
return value
|
||||
@ -57,7 +60,7 @@ class IPNetworkField(BaseIPField):
|
||||
"""
|
||||
IP prefix (network and mask)
|
||||
"""
|
||||
description = "PostgreSQL CIDR field"
|
||||
description = 'PostgreSQL CIDR field'
|
||||
default_validators = [validators.prefix_validator]
|
||||
|
||||
def db_type(self, connection):
|
||||
@ -83,7 +86,7 @@ class IPAddressField(BaseIPField):
|
||||
"""
|
||||
IP address (host address and mask)
|
||||
"""
|
||||
description = "PostgreSQL INET field"
|
||||
description = 'PostgreSQL INET field'
|
||||
|
||||
def db_type(self, connection):
|
||||
return 'inet'
|
||||
@ -110,7 +113,7 @@ IPAddressField.register_lookup(lookups.Inet)
|
||||
|
||||
|
||||
class ASNField(models.BigIntegerField):
|
||||
description = "32-bit ASN field"
|
||||
description = '32-bit ASN field'
|
||||
default_validators = [
|
||||
MinValueValidator(BGP_ASN_MIN),
|
||||
MaxValueValidator(BGP_ASN_MAX),
|
||||
|
||||
@ -354,13 +354,13 @@ class PrefixFilterSet(NetBoxModelFilterSet, ScopedFilterSet, TenancyFilterSet, C
|
||||
vlan_group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='vlan__group',
|
||||
queryset=VLANGroup.objects.all(),
|
||||
to_field_name="id",
|
||||
to_field_name='id',
|
||||
label=_('VLAN Group (ID)'),
|
||||
)
|
||||
vlan_group = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='vlan__group__slug',
|
||||
queryset=VLANGroup.objects.all(),
|
||||
to_field_name="slug",
|
||||
to_field_name='slug',
|
||||
label=_('VLAN Group (slug)'),
|
||||
)
|
||||
vlan_id = django_filters.ModelMultipleChoiceFilter(
|
||||
@ -695,12 +695,12 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFil
|
||||
return queryset.filter(q)
|
||||
|
||||
def parse_inet_addresses(self, value):
|
||||
'''
|
||||
"""
|
||||
Parse networks or IP addresses and cast to a format
|
||||
acceptable by the Postgres inet type.
|
||||
|
||||
Skips invalid values.
|
||||
'''
|
||||
"""
|
||||
parsed = []
|
||||
for addr in value:
|
||||
if netaddr.valid_ipv4(addr) or netaddr.valid_ipv6(addr):
|
||||
@ -718,7 +718,7 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFil
|
||||
# as argument. If they are all invalid,
|
||||
# we return an empty queryset
|
||||
value = self.parse_inet_addresses(value)
|
||||
if (len(value) == 0):
|
||||
if len(value) == 0:
|
||||
return queryset.none()
|
||||
|
||||
try:
|
||||
@ -1079,6 +1079,7 @@ class VLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
def get_for_virtualmachine(self, queryset, name, value):
|
||||
return queryset.get_for_virtualmachine(value)
|
||||
|
||||
@extend_schema_field(OpenApiTypes.INT)
|
||||
def filter_interface_id(self, queryset, name, value):
|
||||
if value is None:
|
||||
return queryset.none()
|
||||
@ -1087,6 +1088,7 @@ class VLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
Q(interfaces_as_untagged=value)
|
||||
).distinct()
|
||||
|
||||
@extend_schema_field(OpenApiTypes.INT)
|
||||
def filter_vminterface_id(self, queryset, name, value):
|
||||
if value is None:
|
||||
return queryset.none()
|
||||
|
||||
@ -4,6 +4,7 @@ from datetime import timedelta
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.utils.functional import classproperty
|
||||
from django.utils import timezone
|
||||
from django_pglocks import advisory_lock
|
||||
from rq.timeouts import JobTimeoutException
|
||||
|
||||
@ -113,7 +114,11 @@ class JobRunner(ABC):
|
||||
# If the executed job is a periodic job, schedule its next execution at the specified interval.
|
||||
finally:
|
||||
if job.interval:
|
||||
new_scheduled_time = (job.scheduled or job.started) + timedelta(minutes=job.interval)
|
||||
# Determine the new scheduled time. Cannot be earlier than one minute in the future.
|
||||
new_scheduled_time = max(
|
||||
(job.scheduled or job.started) + timedelta(minutes=job.interval),
|
||||
timezone.now() + timedelta(minutes=1)
|
||||
)
|
||||
if job.object and getattr(job.object, "python_class", None):
|
||||
kwargs["job_timeout"] = job.object.python_class.job_timeout
|
||||
cls.enqueue(
|
||||
|
||||
@ -412,7 +412,7 @@ ADMIN_MENU = Menu(
|
||||
MenuItem(
|
||||
link='users:user_list',
|
||||
link_text=_('Users'),
|
||||
auth_required=True,
|
||||
staff_only=True,
|
||||
permissions=['users.view_user'],
|
||||
buttons=(
|
||||
MenuItemButton(
|
||||
@ -432,7 +432,7 @@ ADMIN_MENU = Menu(
|
||||
MenuItem(
|
||||
link='users:group_list',
|
||||
link_text=_('Groups'),
|
||||
auth_required=True,
|
||||
staff_only=True,
|
||||
permissions=['users.view_group'],
|
||||
buttons=(
|
||||
MenuItemButton(
|
||||
@ -452,14 +452,14 @@ ADMIN_MENU = Menu(
|
||||
MenuItem(
|
||||
link='users:token_list',
|
||||
link_text=_('API Tokens'),
|
||||
auth_required=True,
|
||||
staff_only=True,
|
||||
permissions=['users.view_token'],
|
||||
buttons=get_model_buttons('users', 'token')
|
||||
),
|
||||
MenuItem(
|
||||
link='users:objectpermission_list',
|
||||
link_text=_('Permissions'),
|
||||
auth_required=True,
|
||||
staff_only=True,
|
||||
permissions=['users.view_objectpermission'],
|
||||
buttons=get_model_buttons('users', 'objectpermission', actions=['add'])
|
||||
),
|
||||
@ -471,23 +471,23 @@ ADMIN_MENU = Menu(
|
||||
MenuItem(
|
||||
link='core:system',
|
||||
link_text=_('System'),
|
||||
auth_required=True
|
||||
staff_only=True,
|
||||
),
|
||||
MenuItem(
|
||||
link='core:plugin_list',
|
||||
link_text=_('Plugins'),
|
||||
auth_required=True
|
||||
staff_only=True,
|
||||
),
|
||||
MenuItem(
|
||||
link='core:configrevision_list',
|
||||
link_text=_('Configuration History'),
|
||||
auth_required=True,
|
||||
permissions=['core.view_configrevision']
|
||||
staff_only=True,
|
||||
permissions=['core.view_configrevision'],
|
||||
),
|
||||
MenuItem(
|
||||
link='core:background_queue_list',
|
||||
link_text=_('Background Tasks'),
|
||||
auth_required=True
|
||||
staff_only=True,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -50,6 +50,14 @@ class ObjectAction:
|
||||
except NoReverseMatch:
|
||||
return
|
||||
|
||||
@classmethod
|
||||
def get_url_params(cls, context):
|
||||
request = context['request']
|
||||
params = request.GET.copy()
|
||||
if 'return_url' in context:
|
||||
params['return_url'] = context['return_url']
|
||||
return params
|
||||
|
||||
@classmethod
|
||||
def get_context(cls, context, obj):
|
||||
"""
|
||||
@ -63,6 +71,7 @@ class ObjectAction:
|
||||
'perms': context['perms'],
|
||||
'request': context['request'],
|
||||
'url': cls.get_url(obj),
|
||||
'url_params': cls.get_url_params(context),
|
||||
'label': cls.label,
|
||||
**cls.get_context(context, obj),
|
||||
**kwargs,
|
||||
|
||||
@ -174,11 +174,16 @@ SECURE_HSTS_INCLUDE_SUBDOMAINS = getattr(configuration, 'SECURE_HSTS_INCLUDE_SUB
|
||||
SECURE_HSTS_PRELOAD = getattr(configuration, 'SECURE_HSTS_PRELOAD', False)
|
||||
SECURE_HSTS_SECONDS = getattr(configuration, 'SECURE_HSTS_SECONDS', 0)
|
||||
SECURE_SSL_REDIRECT = getattr(configuration, 'SECURE_SSL_REDIRECT', False)
|
||||
SENTRY_CONFIG = getattr(configuration, 'SENTRY_CONFIG', {})
|
||||
# TODO: Remove in NetBox v4.5
|
||||
SENTRY_DSN = getattr(configuration, 'SENTRY_DSN', None)
|
||||
SENTRY_ENABLED = getattr(configuration, 'SENTRY_ENABLED', False)
|
||||
# TODO: Remove in NetBox v4.5
|
||||
SENTRY_SAMPLE_RATE = getattr(configuration, 'SENTRY_SAMPLE_RATE', 1.0)
|
||||
# TODO: Remove in NetBox v4.5
|
||||
SENTRY_SEND_DEFAULT_PII = getattr(configuration, 'SENTRY_SEND_DEFAULT_PII', False)
|
||||
SENTRY_TAGS = getattr(configuration, 'SENTRY_TAGS', {})
|
||||
# TODO: Remove in NetBox v4.5
|
||||
SENTRY_TRACES_SAMPLE_RATE = getattr(configuration, 'SENTRY_TRACES_SAMPLE_RATE', 0)
|
||||
SESSION_COOKIE_NAME = getattr(configuration, 'SESSION_COOKIE_NAME', 'sessionid')
|
||||
SESSION_COOKIE_PATH = CSRF_COOKIE_PATH
|
||||
@ -596,18 +601,29 @@ if SENTRY_ENABLED:
|
||||
import sentry_sdk
|
||||
except ModuleNotFoundError:
|
||||
raise ImproperlyConfigured("SENTRY_ENABLED is True but the sentry-sdk package is not installed.")
|
||||
if not SENTRY_DSN:
|
||||
raise ImproperlyConfigured("SENTRY_ENABLED is True but SENTRY_DSN has not been defined.")
|
||||
|
||||
# Construct default Sentry initialization parameters from legacy SENTRY_* config parameters
|
||||
sentry_config = {
|
||||
'dsn': SENTRY_DSN,
|
||||
'sample_rate': SENTRY_SAMPLE_RATE,
|
||||
'send_default_pii': SENTRY_SEND_DEFAULT_PII,
|
||||
'traces_sample_rate': SENTRY_TRACES_SAMPLE_RATE,
|
||||
# TODO: Support proxy routing
|
||||
'http_proxy': HTTP_PROXIES.get('http') if HTTP_PROXIES else None,
|
||||
'https_proxy': HTTP_PROXIES.get('https') if HTTP_PROXIES else None,
|
||||
}
|
||||
# Override/extend the default parameters with any provided via SENTRY_CONFIG
|
||||
sentry_config.update(SENTRY_CONFIG)
|
||||
# Check for a DSN
|
||||
if not sentry_config.get('dsn'):
|
||||
raise ImproperlyConfigured(
|
||||
"Sentry is enabled but a DSN has not been specified. Set one under the SENTRY_CONFIG parameter."
|
||||
)
|
||||
|
||||
# Initialize the SDK
|
||||
sentry_sdk.init(
|
||||
dsn=SENTRY_DSN,
|
||||
release=RELEASE.full_version,
|
||||
sample_rate=SENTRY_SAMPLE_RATE,
|
||||
traces_sample_rate=SENTRY_TRACES_SAMPLE_RATE,
|
||||
send_default_pii=SENTRY_SEND_DEFAULT_PII,
|
||||
# TODO: Support proxy routing
|
||||
http_proxy=HTTP_PROXIES.get('http') if HTTP_PROXIES else None,
|
||||
https_proxy=HTTP_PROXIES.get('https') if HTTP_PROXIES else None
|
||||
**sentry_config
|
||||
)
|
||||
# Assign any configured tags
|
||||
for k, v in SENTRY_TAGS.items():
|
||||
|
||||
@ -14,9 +14,7 @@ from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from core.signals import clear_events
|
||||
from netbox.object_actions import (
|
||||
BulkDelete, BulkEdit, BulkExport, BulkImport, CloneObject, DeleteObject, EditObject,
|
||||
)
|
||||
from netbox.object_actions import BulkDelete, BulkEdit, CloneObject, DeleteObject, EditObject
|
||||
from utilities.error_handlers import handle_protectederror
|
||||
from utilities.exceptions import AbortRequest, PermissionsViolation
|
||||
from utilities.forms import DeleteForm, restrict_form_fields
|
||||
@ -103,7 +101,7 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin):
|
||||
table = None
|
||||
filterset = None
|
||||
filterset_form = None
|
||||
actions = (BulkImport, BulkEdit, BulkExport, BulkDelete)
|
||||
actions = (CloneObject, EditObject, DeleteObject, BulkEdit, BulkDelete)
|
||||
template_name = 'generic/object_children.html'
|
||||
|
||||
def get_children(self, request, parent):
|
||||
@ -240,7 +238,8 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView):
|
||||
model = self.queryset.model
|
||||
|
||||
initial_data = normalize_querydict(request.GET)
|
||||
form = self.form(instance=obj, initial=initial_data)
|
||||
form_prefix = 'quickadd' if request.GET.get('_quickadd') else None
|
||||
form = self.form(instance=obj, initial=initial_data, prefix=form_prefix)
|
||||
restrict_form_fields(form, request.user)
|
||||
|
||||
context = {
|
||||
|
||||
8
netbox/project-static/dist/netbox.js
vendored
8
netbox/project-static/dist/netbox.js
vendored
File diff suppressed because one or more lines are too long
6
netbox/project-static/dist/netbox.js.map
vendored
6
netbox/project-static/dist/netbox.js.map
vendored
File diff suppressed because one or more lines are too long
@ -29,8 +29,8 @@
|
||||
"flatpickr": "4.6.13",
|
||||
"gridstack": "12.3.3",
|
||||
"htmx.org": "2.0.7",
|
||||
"query-string": "9.3.0",
|
||||
"sass": "1.92.1",
|
||||
"query-string": "9.3.1",
|
||||
"sass": "1.93.2",
|
||||
"tom-select": "2.4.3",
|
||||
"typeface-inter": "3.18.1",
|
||||
"typeface-roboto-mono": "1.1.13"
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import { isTruthy } from 'src/util';
|
||||
|
||||
/**
|
||||
* interface for htmx configRequest event
|
||||
*/
|
||||
@ -17,15 +15,6 @@ function initMarkdownPreview(markdownWidget: HTMLDivElement) {
|
||||
const textarea = markdownWidget.querySelector('textarea') as HTMLTextAreaElement;
|
||||
const preview = markdownWidget.querySelector('div.preview') as HTMLDivElement;
|
||||
|
||||
/**
|
||||
* Make sure the textarea has style attribute height
|
||||
* So that it can be copied over to preview div.
|
||||
*/
|
||||
if (!isTruthy(textarea.style.height)) {
|
||||
const { height } = textarea.getBoundingClientRect();
|
||||
textarea.style.height = `${height}px`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the value of the textarea to the body of the htmx request
|
||||
* and copy the height of text are to the preview div
|
||||
|
||||
@ -1,53 +1,23 @@
|
||||
import { getElements, scrollTo } from '../util';
|
||||
import { getElements } from '../util';
|
||||
|
||||
function handleFormSubmit(event: Event, form: HTMLFormElement): void {
|
||||
function handleFormSubmit(): void {
|
||||
// Automatically select all options in any <select> with the "select-all" class. This is useful for
|
||||
// multi-select fields that are used to add/remove choices.
|
||||
for (const element of getElements<HTMLOptionElement>('select.select-all option')) {
|
||||
element.selected = true;
|
||||
}
|
||||
|
||||
// Track the names of each invalid field.
|
||||
const invalids = new Set<string>();
|
||||
|
||||
for (const element of form.querySelectorAll<FormControls>('*[name]')) {
|
||||
if (!element.validity.valid) {
|
||||
invalids.add(element.name);
|
||||
// If the field is invalid, but doesn't contain the .is-invalid class, add it.
|
||||
if (!element.classList.contains('is-invalid')) {
|
||||
element.classList.add('is-invalid');
|
||||
}
|
||||
} else {
|
||||
// If the field is valid, but contains the .is-invalid class, remove it.
|
||||
if (element.classList.contains('is-invalid')) {
|
||||
element.classList.remove('is-invalid');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (invalids.size !== 0) {
|
||||
// If there are invalid fields, pick the first field and scroll to it.
|
||||
const firstInvalid = form.elements.namedItem(Array.from(invalids)[0]) as Element;
|
||||
scrollTo(firstInvalid);
|
||||
|
||||
// If the form has invalid fields, don't submit it.
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach an event listener to each form's submitter (button[type=submit]). When called, the
|
||||
* callback checks the validity of each form field and adds the appropriate Bootstrap CSS class
|
||||
* based on the field's validity.
|
||||
* Attach event listeners to each form's submit/reset buttons.
|
||||
*/
|
||||
export function initFormElements(): void {
|
||||
for (const form of getElements('form')) {
|
||||
// Find each of the form's submitters. Most object edit forms have a "Create" and
|
||||
// a "Create & Add", so we need to add a listener to both.
|
||||
// Find each of the form's submit buttons.
|
||||
const submitters = form.querySelectorAll<HTMLButtonElement>('button[type=submit]');
|
||||
for (const submitter of submitters) {
|
||||
// Add the event listener to each submitter.
|
||||
submitter.addEventListener('click', (event: Event) => handleFormSubmit(event, form));
|
||||
submitter.addEventListener('click', () => handleFormSubmit());
|
||||
}
|
||||
|
||||
// Initialize any reset buttons so that when clicked, the page is reloaded without query parameters.
|
||||
|
||||
@ -2990,10 +2990,10 @@ punycode@^2.1.0:
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
|
||||
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
|
||||
|
||||
query-string@9.3.0:
|
||||
version "9.3.0"
|
||||
resolved "https://registry.yarnpkg.com/query-string/-/query-string-9.3.0.tgz#f2d60d6b4442cb445f374b5ff749b937b2cccd03"
|
||||
integrity sha512-IQHOQ9aauHAApwAaUYifpEyLHv6fpVGVkMOnwPzcDScLjbLj8tLsILn6unSW79NafOw1llh8oK7Gd0VwmXBFmA==
|
||||
query-string@9.3.1:
|
||||
version "9.3.1"
|
||||
resolved "https://registry.yarnpkg.com/query-string/-/query-string-9.3.1.tgz#d0c93e6c7fb7c17bdf04aa09e382114580ede270"
|
||||
integrity sha512-5fBfMOcDi5SA9qj5jZhWAcTtDfKF5WFdd2uD9nVNlbxVv1baq65aALy6qofpNEGELHvisjjasxQp7BlM9gvMzw==
|
||||
dependencies:
|
||||
decode-uri-component "^0.4.1"
|
||||
filter-obj "^5.1.0"
|
||||
@ -3190,10 +3190,10 @@ safe-regex-test@^1.1.0:
|
||||
es-errors "^1.3.0"
|
||||
is-regex "^1.2.1"
|
||||
|
||||
sass@1.92.1:
|
||||
version "1.92.1"
|
||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.92.1.tgz#07fb1fec5647d7b712685d1090628bf52456fe86"
|
||||
integrity sha512-ffmsdbwqb3XeyR8jJR6KelIXARM9bFQe8A6Q3W4Klmwy5Ckd5gz7jgUNHo4UOqutU5Sk1DtKLbpDP0nLCg1xqQ==
|
||||
sass@1.93.2:
|
||||
version "1.93.2"
|
||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.93.2.tgz#e97d225d60f59a3b3dbb6d2ae3c1b955fd1f2cd1"
|
||||
integrity sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==
|
||||
dependencies:
|
||||
chokidar "^4.0.0"
|
||||
immutable "^5.0.2"
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
version: "4.4.1"
|
||||
version: "4.4.2"
|
||||
edition: "Community"
|
||||
published: "2025-09-16"
|
||||
published: "2025-09-30"
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
<button type="submit" name="_sync" {% formaction %}="{{ url }}{% if return_url %}?return_url={{ return_url }}{% endif %}" class="btn btn-primary">
|
||||
<button type="submit" name="_sync" {% formaction %}="{{ url }}{% if url_params %}?{{ url_params.urlencode }}{% endif %}" class="btn btn-primary">
|
||||
<i class="mdi mdi-sync" aria-hidden="true"></i> {{ label }}
|
||||
</button>
|
||||
|
||||
@ -6,63 +6,63 @@
|
||||
<ul class="dropdown-menu">
|
||||
{% if perms.dcim.add_consoleport %}
|
||||
<li>
|
||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_consoleport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_consoleport' %}{% if url_params %}?{{ url_params.urlencode }}{% endif %}" class="dropdown-item">
|
||||
{% trans "Console Ports" %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_consoleserverport %}
|
||||
<li>
|
||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_consoleserverport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item ">
|
||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_consoleserverport' %}{% if url_params %}?{{ url_params.urlencode }}{% endif %}" class="dropdown-item ">
|
||||
{% trans "Console Server Ports" %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_powerport %}
|
||||
<li>
|
||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_powerport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_powerport' %}{% if url_params %}?{{ url_params.urlencode }}{% endif %}" class="dropdown-item">
|
||||
{% trans "Power Ports" %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_poweroutlet %}
|
||||
<li>
|
||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_poweroutlet' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_poweroutlet' %}{% if url_params %}?{{ url_params.urlencode }}{% endif %}" class="dropdown-item">
|
||||
{% trans "Power Outlets" %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_interface %}
|
||||
<li>
|
||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_interface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_interface' %}{% if url_params %}?{{ url_params.urlencode }}{% endif %}" class="dropdown-item">
|
||||
{% trans "Interfaces" %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_rearport %}
|
||||
<li>
|
||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_rearport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_rearport' %}{% if url_params %}?{{ url_params.urlencode }}{% endif %}" class="dropdown-item">
|
||||
{% trans "Rear Ports" %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_devicebay %}
|
||||
<li>
|
||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_devicebay' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_devicebay' %}{% if url_params %}?{{ url_params.urlencode }}{% endif %}" class="dropdown-item">
|
||||
{% trans "Device Bays" %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_modulebay %}
|
||||
<li>
|
||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_modulebay' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_modulebay' %}{% if url_params %}?{{ url_params.urlencode }}{% endif %}" class="dropdown-item">
|
||||
{% trans "Module Bays" %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_inventoryitem %}
|
||||
<li>
|
||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_inventoryitem' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_inventoryitem' %}{% if url_params %}?{{ url_params.urlencode }}{% endif %}" class="dropdown-item">
|
||||
{% trans "Inventory Items" %}
|
||||
</button>
|
||||
</li>
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
<button type="submit" name="_disconnect" {% formaction %}="{{ url }}{% if return_url %}?return_url={{ return_url }}{% endif %}" class="btn btn-red">
|
||||
<button type="submit" name="_disconnect" {% formaction %}="{{ url }}{% if url_params %}?{{ url_params.urlencode }}{% endif %}" class="btn btn-red">
|
||||
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i> {{ label }}
|
||||
</button>
|
||||
|
||||
@ -65,7 +65,6 @@
|
||||
<div class="col-md-4">
|
||||
{{ form.length_unit }}
|
||||
</div>
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
{% render_field form.tags %}
|
||||
</div>
|
||||
|
||||
@ -37,20 +37,24 @@
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Assigned Models" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<div class="list-group list-group-flush" role="presentation">
|
||||
{% for object_type in object.object_types.all %}
|
||||
<tr>
|
||||
<td>{{ object_type }}</td>
|
||||
</tr>
|
||||
{% with object_type.model_class|validated_viewname:"list" as viewname %}
|
||||
{% if viewname %}
|
||||
<a href="{% url viewname %}?{{ object.url_params }}" class="list-group-item list-group-item-action">{{ object_type }}</a>
|
||||
{% else %}
|
||||
<div class="list-group-item list-group-item-action">{{ object_type }}</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-7">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Parameters" %}</h2>
|
||||
<div class="card-body">
|
||||
<div class="card-body p-0">
|
||||
<pre>{{ object.parameters|json }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -79,21 +79,23 @@
|
||||
{# /Showing #}
|
||||
|
||||
{# Pagination options #}
|
||||
<nav class="text-end" aria-label="{% trans "Pagination options" %}">
|
||||
<nav aria-label="{% trans "Pagination options" %}">
|
||||
{% if page %}
|
||||
<div class="dropdown">
|
||||
<div class="dropdown dropdown-menu-end">
|
||||
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||
{% trans "Per Page" %}
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<ul class="dropdown-menu">
|
||||
{% for n in page.paginator.get_page_lengths %}
|
||||
<li>
|
||||
{% if htmx %}
|
||||
<a href="#" hx-get="{{ table.htmx_url }}{% querystring request per_page=n %}" class="dropdown-item">{{ n }}</a>
|
||||
{% else %}
|
||||
<a href="{% querystring request per_page=n %}" class="dropdown-item">{{ n }}</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</nav>
|
||||
|
||||
@ -6,14 +6,14 @@
|
||||
<ul class="dropdown-menu">
|
||||
{% if perms.virtualization.add_vminterface %}
|
||||
<li>
|
||||
<button type="submit" {% formaction %}="{% url 'virtualization:virtualmachine_bulk_add_vminterface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||
<button type="submit" {% formaction %}="{% url 'virtualization:virtualmachine_bulk_add_vminterface' %}{% if url_params %}?{{ url_params.urlencode }}{% endif %}" class="dropdown-item">
|
||||
{% trans "Interfaces" %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.virtualization.add_virtualdisk %}
|
||||
<li>
|
||||
<button type="submit" {% formaction %}="{% url 'virtualization:virtualmachine_bulk_add_virtualdisk' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||
<button type="submit" {% formaction %}="{% url 'virtualization:virtualmachine_bulk_add_virtualdisk' %}{% if url_params %}?{{ url_params.urlencode }}{% endif %}" class="dropdown-item">
|
||||
{% trans "Virtual Disks" %}
|
||||
</button>
|
||||
</li>
|
||||
|
||||
@ -108,7 +108,9 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Cluster Type" %}</th>
|
||||
<td>{{ object.cluster.type }}</td>
|
||||
<td>
|
||||
{{ object.cluster.type|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Device" %}</th>
|
||||
|
||||
@ -9,7 +9,7 @@ from core.graphql.filter_mixins import BaseFilterMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from netbox.graphql.filter_lookups import TreeNodeFilter
|
||||
from .filters import ContactFilter, TenantFilter, TenantGroupFilter
|
||||
from .filters import ContactAssignmentFilter, TenantFilter, TenantGroupFilter
|
||||
|
||||
__all__ = (
|
||||
'ContactFilterMixin',
|
||||
@ -19,7 +19,7 @@ __all__ = (
|
||||
|
||||
@dataclass
|
||||
class ContactFilterMixin(BaseFilterMixin):
|
||||
contacts: Annotated['ContactFilter', strawberry.lazy('tenancy.graphql.filters')] | None = (
|
||||
contacts: Annotated['ContactAssignmentFilter', strawberry.lazy('tenancy.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@ -110,10 +110,10 @@ class ContactAssignmentTable(NetBoxTable):
|
||||
verbose_name=_('Role'),
|
||||
linkify=True
|
||||
)
|
||||
contact_group = tables.Column(
|
||||
accessor=Accessor('contact__group'),
|
||||
verbose_name=_('Group'),
|
||||
linkify=True
|
||||
contact_groups = columns.ManyToManyColumn(
|
||||
accessor=Accessor('contact__groups'),
|
||||
verbose_name=_('Groups'),
|
||||
linkify_item=('tenancy:contactgroup', {'pk': tables.A('pk')})
|
||||
)
|
||||
contact_title = tables.Column(
|
||||
accessor=Accessor('contact__title'),
|
||||
@ -152,7 +152,7 @@ class ContactAssignmentTable(NetBoxTable):
|
||||
model = ContactAssignment
|
||||
fields = (
|
||||
'pk', 'object_type', 'object', 'contact', 'role', 'priority', 'contact_title', 'contact_phone',
|
||||
'contact_email', 'contact_address', 'contact_link', 'contact_description', 'contact_group', 'tags',
|
||||
'contact_email', 'contact_address', 'contact_link', 'contact_description', 'contact_groups', 'tags',
|
||||
'actions'
|
||||
)
|
||||
default_columns = (
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,7 @@
|
||||
import json
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.db.models import Count
|
||||
from django.forms.fields import JSONField as _JSONField, InvalidJSONInput
|
||||
from django.templatetags.static import static
|
||||
@ -74,7 +75,8 @@ class TagFilterField(forms.MultipleChoiceField):
|
||||
count=Count('extras_taggeditem_items')
|
||||
).order_by('name')
|
||||
return [
|
||||
(str(tag.slug), '{} ({})'.format(tag.name, tag.count)) for tag in tags
|
||||
(settings.FILTERS_NULL_CHOICE_VALUE, settings.FILTERS_NULL_CHOICE_LABEL), # "None" option
|
||||
*[(str(tag.slug), f'{tag.name} ({tag.count})') for tag in tags]
|
||||
]
|
||||
|
||||
# Choices are fetched each time the form is initialized
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
<button type="submit" name="_delete" {% formaction %}="{{ url }}{% if return_url %}?return_url={{ return_url }}{% endif %}" class="btn btn-red">
|
||||
<button type="submit" name="_delete" {% formaction %}="{{ url }}{% if url_params %}?{{ url_params.urlencode }}{% endif %}" class="btn btn-red">
|
||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {{ label }}
|
||||
</button>
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
<button type="submit" name="_edit" {% formaction %}="{{ url }}{% if return_url %}?return_url={{ return_url }}{% endif %}" class="btn btn-yellow">
|
||||
<button type="submit" name="_edit" {% formaction %}="{{ url }}{% if url_params %}?{{ url_params.urlencode }}{% endif %}" class="btn btn-yellow">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {{ label }}
|
||||
</button>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{% if url %}
|
||||
<button type="submit" name="_rename" {% formaction %}="{{ url }}{% if return_url %}?return_url={{ return_url }}{% endif %}" class="btn btn-yellow">
|
||||
<button type="submit" name="_rename" {% formaction %}="{{ url }}{% if url_params %}?{{ url_params.urlencode }}{% endif %}" class="btn btn-yellow">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {{ label }}
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
@ -52,10 +52,6 @@
|
||||
<div class="form-text text-danger">
|
||||
{% for error in field.errors %}{{ error }}{% if not forloop.last %}<br />{% endif %}{% endfor %}
|
||||
</div>
|
||||
{% elif field.field.required %}
|
||||
<div class="invalid-feedback">
|
||||
{% trans "This field is required" %}.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Help text #}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
|
||||
[project]
|
||||
name = "netbox"
|
||||
version = "4.4.1"
|
||||
version = "4.4.2"
|
||||
requires-python = ">=3.10"
|
||||
description = "The premier source of truth powering network automation."
|
||||
readme = "README.md"
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
colorama==0.4.6
|
||||
Django==5.2.6
|
||||
django-cors-headers==4.8.0
|
||||
django-cors-headers==4.9.0
|
||||
django-debug-toolbar==5.2.0
|
||||
django-filter==25.1
|
||||
django-graphiql-debug-toolbar==0.2.0
|
||||
django-htmx==1.24.1
|
||||
django-htmx==1.26.0
|
||||
django-mptt==0.17.0
|
||||
django-pglocks==1.0.4
|
||||
django-prometheus==2.4.1
|
||||
django-redis==6.0.0
|
||||
django-rich==2.1.0
|
||||
django-rich==2.2.0
|
||||
django-rq==3.1
|
||||
django-storages==1.14.6
|
||||
django-tables2==2.7.5
|
||||
@ -24,13 +24,13 @@ Jinja2==3.1.6
|
||||
jsonschema==4.25.1
|
||||
Markdown==3.9
|
||||
mkdocs-material==9.6.20
|
||||
mkdocstrings==0.30.0
|
||||
mkdocstrings==0.30.1
|
||||
mkdocstrings-python==1.18.2
|
||||
netaddr==1.3.0
|
||||
nh3==0.3.0
|
||||
Pillow==11.3.0
|
||||
psycopg[c,pool]==3.2.10
|
||||
PyYAML==6.0.2
|
||||
PyYAML==6.0.3
|
||||
requests==2.32.5
|
||||
rq==2.6.0
|
||||
social-auth-app-django==5.5.1
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user