diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
index 11b7e9aff..3e3d2ec3a 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
- placeholder: v3.3.4
+ placeholder: v3.3.7
validations:
required: true
- type: dropdown
@@ -25,6 +25,7 @@ body:
- "3.8"
- "3.9"
- "3.10"
+ - "3.11"
validations:
required: true
- type: textarea
diff --git a/.github/ISSUE_TEMPLATE/documentation_change.yaml b/.github/ISSUE_TEMPLATE/documentation_change.yaml
index 0f87115fc..cb097d579 100644
--- a/.github/ISSUE_TEMPLATE/documentation_change.yaml
+++ b/.github/ISSUE_TEMPLATE/documentation_change.yaml
@@ -19,11 +19,15 @@ body:
label: Area
description: To what section of the documentation does this change primarily pertain?
options:
- - Installation instructions
- - Configuration parameters
- - Functionality/features
- - REST API
- - Administration/development
+ - Features
+ - Installation/upgrade
+ - Getting started
+ - Configuration
+ - Customization
+ - Integrations/API
+ - Plugins
+ - Administration
+ - Development
- Other
validations:
required: true
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml
index bc00a3921..5f0a17aa7 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yaml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yaml
@@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
- placeholder: v3.3.4
+ placeholder: v3.3.7
validations:
required: true
- type: dropdown
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 33134cb45..0bbbe90c7 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,13 +1,14 @@
### Fixes: #1234
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 67f5028cd..1d9692194 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,5 +1,7 @@
name: CI
on: [push, pull_request]
+permissions:
+ contents: read
jobs:
build:
runs-on: ubuntu-latest
@@ -7,7 +9,7 @@ jobs:
NETBOX_CONFIGURATION: netbox.configuration_testing
strategy:
matrix:
- python-version: ['3.8', '3.9', '3.10']
+ python-version: ['3.8', '3.9', '3.10', '3.11']
node-version: ['14.x']
services:
redis:
diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml
index 9df4bc441..6019cef5d 100644
--- a/.github/workflows/lock.yml
+++ b/.github/workflows/lock.yml
@@ -4,6 +4,11 @@ name: 'Lock threads'
on:
schedule:
- cron: '0 3 * * *'
+ workflow_dispatch:
+
+permissions:
+ issues: write
+ pull-requests: write
jobs:
lock:
@@ -11,7 +16,6 @@ jobs:
steps:
- uses: dessant/lock-threads@v3
with:
- github-token: ${{ github.token }}
issue-inactive-days: 90
pr-inactive-days: 30
issue-lock-reason: 'resolved'
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index 57666417a..ab259af2a 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -1,14 +1,21 @@
# close-stale-issues (https://github.com/marketplace/actions/close-stale-issues)
name: 'Close stale issues/PRs'
+
on:
schedule:
- cron: '0 4 * * *'
+ workflow_dispatch:
+
+permissions:
+ issues: write
+ pull-requests: write
jobs:
stale:
+
runs-on: ubuntu-latest
steps:
- - uses: actions/stale@v5
+ - uses: actions/stale@v6
with:
close-issue-message: >
This issue has been automatically closed due to lack of activity. In an
diff --git a/base_requirements.txt b/base_requirements.txt
index 22106587d..4032bc26e 100644
--- a/base_requirements.txt
+++ b/base_requirements.txt
@@ -68,7 +68,7 @@ drf-yasg[validation]
# Django wrapper for Graphene (GraphQL support)
# https://github.com/graphql-python/graphene-django
-graphene_django
+graphene_django<3.0
# WSGI HTTP server
# https://gunicorn.org/
@@ -80,7 +80,8 @@ Jinja2
# Simple markup language for rendering HTML
# https://github.com/Python-Markdown/markdown
-Markdown
+# mkdocs currently requires Markdown v3.3
+Markdown<3.4
# File inclusion plugin for Python-Markdown
# https://github.com/cmacmackin/markdown-include
diff --git a/docs/_theme/main.html b/docs/_theme/main.html
index 4dfc4e14e..3ff44b9cb 100644
--- a/docs/_theme/main.html
+++ b/docs/_theme/main.html
@@ -2,8 +2,8 @@
{% block site_meta %}
{{ super() }}
- {# Disable search indexing unless we're building for ReadTheDocs #}
- {% if not config.extra.readthedocs %}
+ {# Disable search indexing unless we're building for ReadTheDocs (see #10496) #}
+ {% if page.canonical_url != 'https://docs.netbox.dev/' %}
{% endif %}
{% endblock %}
diff --git a/docs/configuration/security.md b/docs/configuration/security.md
index 6aa363b1a..b8c2b1e11 100644
--- a/docs/configuration/security.md
+++ b/docs/configuration/security.md
@@ -1,5 +1,13 @@
# Security & Authentication Parameters
+## ALLOW_TOKEN_RETRIEVAL
+
+Default: True
+
+If disabled, the values of API tokens will not be displayed after each token's initial creation. A user **must** record the value of a token immediately upon its creation, or it will be lost. Note that this affects _all_ users, regardless of assigned permissions.
+
+---
+
## ALLOWED_URL_SCHEMES
!!! tip "Dynamic Configuration Parameter"
diff --git a/docs/configuration/system.md b/docs/configuration/system.md
index 93f8fa902..3756b6a83 100644
--- a/docs/configuration/system.md
+++ b/docs/configuration/system.md
@@ -157,6 +157,14 @@ The file path to the location where [custom scripts](../customization/custom-scr
---
+## SEARCH_BACKEND
+
+Default: `'netbox.search.backends.CachedValueSearchBackend'`
+
+The dotted path to the desired search backend class. `CachedValueSearchBackend` is currently the only search backend provided in NetBox, however this setting can be used to enable a custom backend.
+
+---
+
## STORAGE_BACKEND
Default: None (local storage)
diff --git a/docs/customization/custom-fields.md b/docs/customization/custom-fields.md
index c443fa9f6..81aaa5247 100644
--- a/docs/customization/custom-fields.md
+++ b/docs/customization/custom-fields.md
@@ -13,6 +13,7 @@ Custom fields may be created by navigating to Customization > Custom Fields. Net
* Text: Free-form text (intended for single-line use)
* Long text: Free-form of any length; supports Markdown rendering
* Integer: A whole number (positive or negative)
+* Decimal: A fixed-precision decimal number (4 decimal places)
* Boolean: True or false
* Date: A date in ISO 8601 format (YYYY-MM-DD)
* URL: This will be presented as a link in the web UI
diff --git a/docs/customization/custom-scripts.md b/docs/customization/custom-scripts.md
index e5d5a1ef5..456bcf472 100644
--- a/docs/customization/custom-scripts.md
+++ b/docs/customization/custom-scripts.md
@@ -267,7 +267,7 @@ An IPv4 or IPv6 network with a mask. Returns a `netaddr.IPNetwork` object. Two a
### Via the Web UI
-Custom scripts can be run via the web UI by navigating to the script, completing any required form data, and clicking the "run script" button.
+Custom scripts can be run via the web UI by navigating to the script, completing any required form data, and clicking the "run script" button. It is possible to schedule a script to be executed at specified time in the future. A scheduled script can be canceled by deleting the associated job result object.
### Via the API
@@ -282,6 +282,8 @@ http://netbox/api/extras/scripts/example.MyReport/ \
--data '{"data": {"foo": "somevalue", "bar": 123}, "commit": true}'
```
+Optionally `schedule_at` can be passed in the form data with a datetime string to schedule a script at the specified date and time.
+
### Via the CLI
Scripts can be run on the CLI by invoking the management command:
diff --git a/docs/customization/reports.md b/docs/customization/reports.md
index 150c32f40..470868ea0 100644
--- a/docs/customization/reports.md
+++ b/docs/customization/reports.md
@@ -136,7 +136,7 @@ Once you have created a report, it will appear in the reports list. Initially, r
### Via the Web UI
-Reports can be run via the web UI by navigating to the report and clicking the "run report" button at top right. Once a report has been run, its associated results will be included in the report view.
+Reports can be run via the web UI by navigating to the report and clicking the "run report" button at top right. Once a report has been run, its associated results will be included in the report view. It is possible to schedule a report to be executed at specified time in the future. A scheduled report can be canceled by deleting the associated job result object.
### Via the API
@@ -152,6 +152,8 @@ Our example report above would be called as:
POST /api/extras/reports/devices.DeviceConnectionsReport/run/
```
+Optionally `schedule_at` can be passed in the form data with a datetime string to schedule a script at the specified date and time.
+
### Via the CLI
Reports can be run on the CLI by invoking the management command:
diff --git a/docs/development/search.md b/docs/development/search.md
new file mode 100644
index 000000000..02bcaa898
--- /dev/null
+++ b/docs/development/search.md
@@ -0,0 +1,37 @@
+# Search
+
+NetBox v3.4 introduced a new global search mechanism, which employs the `extras.CachedValue` model to store discrete field values from many models in a single table.
+
+## SearchIndex
+
+To enable search support for a model, declare and register a subclass of `netbox.search.SearchIndex` for it. Typically, this will be done within an app's `search.py` module.
+
+```python
+from netbox.search import SearchIndex, register_search
+
+@register_search
+class MyModelIndex(SearchIndex):
+ model = MyModel
+ fields = (
+ ('name', 100),
+ ('description', 500),
+ ('comments', 5000),
+ )
+```
+
+A SearchIndex subclass defines both its model and a list of two-tuples specifying which model fields to be indexed and the weight (precedence) associated with each. Guidance on weight assignment for fields is provided below.
+
+### Field Weight Guidance
+
+| Weight | Field Role | Examples |
+|--------|--------------------------------------------------|----------------------------------------------------|
+| 50 | Unique serialized attribute | Device.asset_tag |
+| 60 | Unique serialized attribute (per related object) | Device.serial |
+| 100 | Primary human identifier | Device.name, Circuit.cid, Cable.label |
+| 110 | Slug | Site.slug |
+| 200 | Secondary identifier | Provider.account, DeviceType.part_number |
+| 300 | Highly unique descriptive attribute | CircuitTermination.xconnect_id, IPAddress.dns_name |
+| 500 | Description | Site.description |
+| 1000 | Custom field default | - |
+| 2000 | Other discrete attribute | CircuitTermination.port_speed |
+| 5000 | Comment field | Site.comments |
diff --git a/docs/features/journaling.md b/docs/features/journaling.md
index ce126bf27..8aebdb446 100644
--- a/docs/features/journaling.md
+++ b/docs/features/journaling.md
@@ -1,5 +1,5 @@
# Journaling
-All primary objects in NetBox support journaling. A journal is a collection of human-generated notes and comments about an object maintained for historical context. It supplements NetBox's change log to provide additional information about why changes have been made or to convey events which occur outside NetBox. Unlike the change log, in which records typically expire after a configurable period of time, journal entries persist for the life of their associated object.
+All primary and organizational models in NetBox support journaling. A journal is a collection of human-generated notes and comments about an object maintained for historical context. It supplements NetBox's change log to provide additional information about why changes have been made or to convey events which occur outside NetBox. Unlike the change log, in which records typically expire after a configurable period of time, journal entries persist for the life of their associated object.
Each journal entry has a selectable kind (info, success, warning, or danger) and a user-populated `comments` field. Each entry automatically records the date, time, and associated user upon being created.
diff --git a/docs/getting-started/populating-data.md b/docs/getting-started/populating-data.md
index bb0e8e17f..9a2386d71 100644
--- a/docs/getting-started/populating-data.md
+++ b/docs/getting-started/populating-data.md
@@ -20,12 +20,14 @@ To create a new object in NetBox, find the object type in the navigation menu an
## Bulk Import (CSV/YAML)
-NetBox supports the bulk import of new objects using CSV-formatted data. This method can be ideal for importing spreadsheet data, which is very easy to convert to CSV data. CSV data can be imported either as raw text using the form field, or by uploading a properly formatted CSV file.
+NetBox supports the bulk import of new objects, and updating of existing objects using CSV-formatted data. This method can be ideal for importing spreadsheet data, which is very easy to convert to CSV data. CSV data can be imported either as raw text using the form field, or by uploading a properly formatted CSV file.
When viewing the CSV import form for an object type, you'll notice that the headers for the required columns have been pre-populated. Each form has a table beneath it titled "CSV Field Options," which lists _all_ supported columns for your reference. (Generally, these map to the fields you see in the corresponding creation form for individual objects.)
+If an "id" field is added the data will be used to update existing records instead of importing new objects.
+
Note that some models (namely device types and module types) do not support CSV import. Instead, they accept YAML-formatted data to facilitate the import of both the parent object as well as child components.
## Scripting
diff --git a/docs/installation/3-netbox.md b/docs/installation/3-netbox.md
index f42e28deb..353b0ddab 100644
--- a/docs/installation/3-netbox.md
+++ b/docs/installation/3-netbox.md
@@ -7,7 +7,7 @@ This section of the documentation discusses installing and configuring the NetBo
Begin by installing all system packages required by NetBox and its dependencies.
!!! warning "Python 3.8 or later required"
- NetBox requires Python 3.8, 3.9, or 3.10.
+ NetBox requires Python 3.8, 3.9, 3.10 or 3.11.
=== "Ubuntu"
diff --git a/docs/installation/6-ldap.md b/docs/installation/6-ldap.md
index 163ace70d..ffba6889b 100644
--- a/docs/installation/6-ldap.md
+++ b/docs/installation/6-ldap.md
@@ -46,7 +46,7 @@ Next, create a file in the same directory as `configuration.py` (typically `/opt
### General Server Configuration
!!! info
- When using Windows Server 2012 you may need to specify a port on `AUTH_LDAP_SERVER_URI`. Use `3269` for secure, or `3268` for non-secure.
+ When using Active Directory you may need to specify a port on `AUTH_LDAP_SERVER_URI` to authenticate users from all domains in the forest. Use `3269` for secure, or `3268` for non-secure access to the GC (Global Catalog).
```python
import ldap
@@ -67,6 +67,16 @@ AUTH_LDAP_BIND_PASSWORD = "demo"
# Note that this is a NetBox-specific setting which sets:
# ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
LDAP_IGNORE_CERT_ERRORS = True
+
+# Include this setting if you want to validate the LDAP server certificates against a CA certificate directory on your server
+# Note that this is a NetBox-specific setting which sets:
+# ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, LDAP_CA_CERT_DIR)
+LDAP_CA_CERT_DIR = '/etc/ssl/certs'
+
+# Include this setting if you want to validate the LDAP server certificates against your own CA.
+# Note that this is a NetBox-specific setting which sets:
+# ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, LDAP_CA_CERT_FILE)
+LDAP_CA_CERT_FILE = '/path/to/example-CA.crt'
```
STARTTLS can be configured by setting `AUTH_LDAP_START_TLS = True` and using the `ldap://` URI scheme.
diff --git a/docs/integrations/graphql-api.md b/docs/integrations/graphql-api.md
index 4fc6d2dd8..a078bb82a 100644
--- a/docs/integrations/graphql-api.md
+++ b/docs/integrations/graphql-api.md
@@ -47,7 +47,7 @@ NetBox provides both a singular and plural query field for each object type:
For example, query `device(id:123)` to fetch a specific device (identified by its unique ID), and query `device_list` (with an optional set of filters) to fetch all devices.
-For more detail on constructing GraphQL queries, see the [Graphene documentation](https://docs.graphene-python.org/en/latest/).
+For more detail on constructing GraphQL queries, see the [Graphene documentation](https://docs.graphene-python.org/en/latest/) as well as the [GraphQL queries documentation](https://graphql.org/learn/queries/).
## Filtering
@@ -56,6 +56,47 @@ The GraphQL API employs the same filtering logic as the UI and REST API. Filters
```
{"query": "query {site_list(region:\"north-carolina\", status:\"active\") {name}}"}
```
+In addition, filtering can be done on list of related objects as shown in the following query:
+
+```
+{
+ device_list {
+ id
+ name
+ interfaces(enabled: true) {
+ name
+ }
+ }
+}
+```
+
+## Multiple Return Types
+
+Certain queries can return multiple types of objects, for example cable terminations can return circuit terminations, console ports and many others. These can be queried using [inline fragments](https://graphql.org/learn/schema/#union-types) as shown below:
+
+```
+{
+ cable_list {
+ id
+ a_terminations {
+ ... on CircuitTerminationType {
+ id
+ class_type
+ }
+ ... on ConsolePortType {
+ id
+ class_type
+ }
+ ... on ConsoleServerPortType {
+ id
+ class_type
+ }
+ }
+ }
+}
+
+```
+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".
## Authentication
diff --git a/docs/integrations/rest-api.md b/docs/integrations/rest-api.md
index 3a5aed055..6f54a8cb0 100644
--- a/docs/integrations/rest-api.md
+++ b/docs/integrations/rest-api.md
@@ -579,6 +579,9 @@ By default, a token can be used to perform all actions via the API that a user w
Additionally, a token can be set to expire at a specific time. This can be useful if an external client needs to be granted temporary access to NetBox.
+!!! warning "Restricting Token Retrieval"
+ The ability to retrieve the key value of a previously-created API token can be restricted by disabling the [`ALLOW_TOKEN_RETRIEVAL`](../configuration/security.md#allow_token_retrieval) configuration parameter.
+
#### Client IP Restriction
!!! note
diff --git a/docs/models/dcim/devicetype.md b/docs/models/dcim/devicetype.md
index 050f93244..6dc4aa13e 100644
--- a/docs/models/dcim/devicetype.md
+++ b/docs/models/dcim/devicetype.md
@@ -41,6 +41,10 @@ Indicates whether this is a parent type (capable of housing child devices), a ch
The default direction in which airflow circulates within the device chassis. This may be configured differently for instantiated devices (e.g. because of different fan modules).
+### Weight
+
+The numeric weight of the device, including a unit designation (e.g. 10 kilograms or 20 pounds).
+
### Front & Rear Images
Users can upload illustrations of the device's front and rear panels. If present, these will be used to render the device in [rack](./rack.md) elevation diagrams.
diff --git a/docs/models/dcim/moduletype.md b/docs/models/dcim/moduletype.md
index b8ec0ac6e..3122d2e00 100644
--- a/docs/models/dcim/moduletype.md
+++ b/docs/models/dcim/moduletype.md
@@ -35,3 +35,7 @@ The model number assigned to this module type by its manufacturer. Must be uniqu
### Part Number
An alternative part number to uniquely identify the module type.
+
+### Weight
+
+The numeric weight of the module, including a unit designation (e.g. 3 kilograms or 1 pound).
diff --git a/docs/models/dcim/rack.md b/docs/models/dcim/rack.md
index 57e7bec98..505160d3e 100644
--- a/docs/models/dcim/rack.md
+++ b/docs/models/dcim/rack.md
@@ -65,6 +65,14 @@ The height of the rack, measured in units.
The external width and depth of the rack can be tracked to aid in floorplan calculations. These measurements must be designated in either millimeters or inches.
+### Mounting Depth
+
+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.)
+
+### Weight
+
+The numeric weight of the rack, including a unit designation (e.g. 10 kilograms or 20 pounds).
+
### Descending Units
If selected, the rack's elevation will display unit 1 at the top of the rack. (Most racks use asceneding numbering, with unit 1 assigned to the bottommost position.)
diff --git a/docs/models/dcim/site.md b/docs/models/dcim/site.md
index c74c209e1..2e35ab11f 100644
--- a/docs/models/dcim/site.md
+++ b/docs/models/dcim/site.md
@@ -33,7 +33,7 @@ Each site can have multiple [AS numbers](../ipam/asn.md) assigned to it.
### Time Zone
-The site's local time zone. (Time zones are provided by the [pytz](https://pypi.org/project/pytz/) package.)
+The site's local time zone. (Time zones are provided by the [zoneinfo](https://docs.python.org/3/library/zoneinfo.html) library.)
### Physical Address
diff --git a/docs/models/wireless/wirelesslan.md b/docs/models/wireless/wirelesslan.md
index 5bb3dbd65..0f50fa75f 100644
--- a/docs/models/wireless/wirelesslan.md
+++ b/docs/models/wireless/wirelesslan.md
@@ -12,6 +12,13 @@ The service set identifier (SSID) for the wireless network.
The [wireless LAN group](./wirelesslangroup.md) to which this wireless LAN is assigned (if any).
+### Status
+
+The operational status of the wireless network.
+
+!!! tip
+ Additional statuses may be defined by setting `WirelessLAN.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
+
### VLAN
Each wireless LAN can optionally be mapped to a [VLAN](../ipam/vlan.md), to model a bridge between wired and wireless segments.
diff --git a/docs/plugins/development/forms.md b/docs/plugins/development/forms.md
index e1eefa7a5..dee0d3796 100644
--- a/docs/plugins/development/forms.md
+++ b/docs/plugins/development/forms.md
@@ -144,73 +144,73 @@ class MyModelFilterForm(NetBoxModelFilterSetForm):
In addition to the [form fields provided by Django](https://docs.djangoproject.com/en/stable/ref/forms/fields/), NetBox provides several field classes for use within forms to handle specific types of data. These can be imported from `utilities.forms.fields` and are documented below.
::: utilities.forms.ColorField
- selection:
+ options:
members: false
::: utilities.forms.CommentField
- selection:
+ options:
members: false
::: utilities.forms.JSONField
- selection:
+ options:
members: false
::: utilities.forms.MACAddressField
- selection:
+ options:
members: false
::: utilities.forms.SlugField
- selection:
+ options:
members: false
## Choice Fields
::: utilities.forms.ChoiceField
- selection:
+ options:
members: false
::: utilities.forms.MultipleChoiceField
- selection:
+ options:
members: false
## Dynamic Object Fields
::: utilities.forms.DynamicModelChoiceField
- selection:
+ options:
members: false
::: utilities.forms.DynamicModelMultipleChoiceField
- selection:
+ options:
members: false
## Content Type Fields
::: utilities.forms.ContentTypeChoiceField
- selection:
+ options:
members: false
::: utilities.forms.ContentTypeMultipleChoiceField
- selection:
+ options:
members: false
## CSV Import Fields
::: utilities.forms.CSVChoiceField
- selection:
+ options:
members: false
::: utilities.forms.CSVMultipleChoiceField
- selection:
+ options:
members: false
::: utilities.forms.CSVModelChoiceField
- selection:
+ options:
members: false
::: utilities.forms.CSVContentTypeField
- selection:
+ options:
members: false
::: utilities.forms.CSVMultipleContentTypeField
- selection:
+ options:
members: false
diff --git a/docs/plugins/development/graphql-api.md b/docs/plugins/development/graphql-api.md
index 0dadf021f..f802e8025 100644
--- a/docs/plugins/development/graphql-api.md
+++ b/docs/plugins/development/graphql-api.md
@@ -32,11 +32,11 @@ schema = MyQuery
NetBox provides two object type classes for use by plugins.
::: netbox.graphql.types.BaseObjectType
- selection:
+ options:
members: false
::: netbox.graphql.types.NetBoxObjectType
- selection:
+ options:
members: false
## GraphQL Fields
@@ -44,9 +44,9 @@ NetBox provides two object type classes for use by plugins.
NetBox provides two field classes for use by plugins.
::: netbox.graphql.fields.ObjectField
- selection:
+ options:
members: false
::: netbox.graphql.fields.ObjectListField
- selection:
+ options:
members: false
diff --git a/docs/plugins/development/index.md b/docs/plugins/development/index.md
index 98db9e0bb..dcbad9d8d 100644
--- a/docs/plugins/development/index.md
+++ b/docs/plugins/development/index.md
@@ -14,6 +14,7 @@ Plugins can do a lot, including:
* Provide their own "pages" (views) in the web user interface
* Inject template content and navigation links
* Extend NetBox's REST and GraphQL APIs
+* Load additional Django apps
* Add custom request/response middleware
However, keep in mind that each piece of functionality is entirely optional. For example, if your plugin merely adds a piece of middleware or an API endpoint for existing data, there's no need to define any new models.
@@ -82,6 +83,7 @@ class FooBarConfig(PluginConfig):
default_settings = {
'baz': True
}
+ django_apps = ["foo", "bar", "baz"]
config = FooBarConfig
```
@@ -101,10 +103,12 @@ NetBox looks for the `config` variable within a plugin's `__init__.py` to load i
| `base_url` | Base path to use for plugin URLs (optional). If not specified, the project's `name` will be used. |
| `required_settings` | A list of any configuration parameters that **must** be defined by the user |
| `default_settings` | A dictionary of configuration parameters and their default values |
+| `django_apps` | A list of additional Django apps to load alongside the plugin |
| `min_version` | Minimum version of NetBox with which the plugin is compatible |
| `max_version` | Maximum version of NetBox with which the plugin is compatible |
| `middleware` | A list of middleware classes to append after NetBox's build-in middleware |
| `queues` | A list of custom background task queues to create |
+| `search_extensions` | The dotted path to the list of search index classes (default: `search.indexes`) |
| `template_extensions` | The dotted path to the list of template extension classes (default: `template_content.template_extensions`) |
| `menu_items` | The dotted path to the list of menu items provided by the plugin (default: `navigation.menu_items`) |
| `graphql_schema` | The dotted path to the plugin's GraphQL schema class, if any (default: `graphql.schema`) |
@@ -112,6 +116,22 @@ NetBox looks for the `config` variable within a plugin's `__init__.py` to load i
All required settings must be configured by the user. If a configuration parameter is listed in both `required_settings` and `default_settings`, the default setting will be ignored.
+!!! tip "Accessing Config Parameters"
+ Plugin configuration parameters can be accessed using the `get_plugin_config()` function. For example:
+
+ ```python
+ from extras.plugins import get_plugin_config
+ get_plugin_config('my_plugin', 'verbose_name')
+ ```
+
+#### Important Notes About `django_apps`
+
+Loading additional apps may cause more harm than good and could make identifying problems within NetBox itself more difficult. The `django_apps` attribute is intended only for advanced use cases that require a deeper Django integration.
+
+Apps from this list are inserted *before* the plugin's `PluginConfig` in the order defined. Adding the plugin's `PluginConfig` module to this list changes this behavior and allows for apps to be loaded *after* the plugin.
+
+Any additional apps must be installed within the same Python environment as NetBox or `ImproperlyConfigured` exceptions will be raised when loading the plugin.
+
## Create setup.py
`setup.py` is the [setup script](https://docs.python.org/3.8/distutils/setupscript.html) used to package and install our plugin once it's finished. The primary function of this script is to call the setuptools library's `setup()` function to create a Python distribution package. We can pass a number of keyword arguments to control the package creation as well as to provide metadata about the plugin. An example `setup.py` is below:
diff --git a/docs/plugins/development/models.md b/docs/plugins/development/models.md
index 16f5dd0df..b3bcb292a 100644
--- a/docs/plugins/development/models.md
+++ b/docs/plugins/development/models.md
@@ -49,6 +49,12 @@ class MyModel(NetBoxModel):
...
```
+### NetBoxModel Properties
+
+#### `docs_url`
+
+This attribute specifies the URL at which the documentation for this model can be reached. By default, it will return `/static/docs/models///`. Plugin models can override this to return a custom URL. For example, you might direct the user to your plugin's documentation hosted on [ReadTheDocs](https://readthedocs.org/).
+
### Enabling Features Individually
If you prefer instead to enable only a subset of these features for a plugin model, NetBox provides a discrete "mix-in" class for each feature. You can subclass each of these individually when defining your model. (Your model will also need to inherit from Django's built-in `Model` class.)
diff --git a/docs/plugins/development/search.md b/docs/plugins/development/search.md
new file mode 100644
index 000000000..e3b861f00
--- /dev/null
+++ b/docs/plugins/development/search.md
@@ -0,0 +1,28 @@
+# Search
+
+Plugins can define and register their own models to extend NetBox's core search functionality. Typically, a plugin will include a file named `search.py`, which holds all search indexes for its models (see the example below).
+
+```python
+# search.py
+from netbox.search import SearchIndex
+from .models import MyModel
+
+class MyModelIndex(SearchIndex):
+ model = MyModel
+ fields = (
+ ('name', 100),
+ ('description', 500),
+ ('comments', 5000),
+ )
+```
+
+To register one or more indexes with NetBox, define a list named `indexes` at the end of this file:
+
+```python
+indexes = [MyModelIndex]
+```
+
+!!! tip
+ The path to the list of search indexes can be modified by setting `search_indexes` in the PluginConfig instance.
+
+::: netbox.search.SearchIndex
diff --git a/docs/plugins/development/tables.md b/docs/plugins/development/tables.md
index 6dccb4ee2..f846139f0 100644
--- a/docs/plugins/development/tables.md
+++ b/docs/plugins/development/tables.md
@@ -52,38 +52,38 @@ This will automatically apply any user-specific preferences for the table. (If u
The table column classes listed below are supported for use in plugins. These classes can be imported from `netbox.tables.columns`.
::: netbox.tables.BooleanColumn
- selection:
+ options:
members: false
::: netbox.tables.ChoiceFieldColumn
- selection:
+ options:
members: false
::: netbox.tables.ColorColumn
- selection:
+ options:
members: false
::: netbox.tables.ColoredLabelColumn
- selection:
+ options:
members: false
::: netbox.tables.ContentTypeColumn
- selection:
+ options:
members: false
::: netbox.tables.ContentTypesColumn
- selection:
+ options:
members: false
::: netbox.tables.MarkdownColumn
- selection:
+ options:
members: false
::: netbox.tables.TagColumn
- selection:
+ options:
members: false
::: netbox.tables.TemplateColumn
- selection:
+ options:
members:
- __init__
diff --git a/docs/plugins/development/views.md b/docs/plugins/development/views.md
index cabcd7045..45d7064cf 100644
--- a/docs/plugins/development/views.md
+++ b/docs/plugins/development/views.md
@@ -82,26 +82,28 @@ class ThingEditView(ObjectEditView):
Below are the class definitions for NetBox's object views. These views handle CRUD actions for individual objects. The view, add/edit, and delete views each inherit from `BaseObjectView`, which is not intended to be used directly.
::: netbox.views.generic.base.BaseObjectView
+ options:
+ members:
+ - get_queryset
+ - get_object
+ - get_extra_context
::: netbox.views.generic.ObjectView
- selection:
+ options:
members:
- - get_object
- get_template_name
::: netbox.views.generic.ObjectEditView
- selection:
+ options:
members:
- - get_object
- alter_object
::: netbox.views.generic.ObjectDeleteView
- selection:
- members:
- - get_object
+ options:
+ members: false
::: netbox.views.generic.ObjectChildrenView
- selection:
+ options:
members:
- get_children
- prep_table_data
@@ -111,24 +113,28 @@ Below are the class definitions for NetBox's object views. These views handle CR
Below are the class definitions for NetBox's multi-object views. These views handle simultaneous actions for sets objects. The list, import, edit, and delete views each inherit from `BaseMultiObjectView`, which is not intended to be used directly.
::: netbox.views.generic.base.BaseMultiObjectView
+ options:
+ members:
+ - get_queryset
+ - get_extra_context
::: netbox.views.generic.ObjectListView
- selection:
+ options:
members:
- get_table
- export_table
- export_template
::: netbox.views.generic.BulkImportView
- selection:
+ options:
members: false
::: netbox.views.generic.BulkEditView
- selection:
+ options:
members: false
::: netbox.views.generic.BulkDeleteView
- selection:
+ options:
members:
- get_form
@@ -137,17 +143,43 @@ Below are the class definitions for NetBox's multi-object views. These views han
These views are provided to enable or enhance certain NetBox model features, such as change logging or journaling. These typically do not need to be subclassed: They can be used directly e.g. in a URL path.
::: netbox.views.generic.ObjectChangeLogView
- selection:
+ options:
members:
- get_form
::: netbox.views.generic.ObjectJournalView
- selection:
+ options:
members:
- get_form
## Extending Core Views
+### Additional Tabs
+
+Plugins can "attach" a custom view to a core NetBox model by registering it with `register_model_view()`. To include a tab for this view within the NetBox UI, declare a TabView instance named `tab`:
+
+```python
+from dcim.models import Site
+from myplugin.models import Stuff
+from netbox.views import generic
+from utilities.views import ViewTab, register_model_view
+
+@register_model_view(Site, 'mview', path='some-other-stuff')
+class MyView(generic.ObjectView):
+ ...
+ tab = ViewTab(
+ label='Other Stuff',
+ badge=lambda obj: Stuff.objects.filter(site=obj).count(),
+ permission='myplugin.view_stuff'
+ )
+```
+
+::: utilities.views.register_model_view
+
+::: utilities.views.ViewTab
+
+### Extra Template Content
+
Plugins can inject custom content into certain areas of the detail views of applicable models. This is accomplished by subclassing `PluginTemplateExtension`, designating a particular NetBox model, and defining the desired methods to render custom content. Four methods are available:
* `left_page()` - Inject content on the left side of the page
diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md
index 2955e17d5..a693ec1e0 100644
--- a/docs/release-notes/version-3.3.md
+++ b/docs/release-notes/version-3.3.md
@@ -1,18 +1,83 @@
# NetBox v3.3
-## v3.3.5 (FUTURE)
+## v3.3.8 (FUTURE)
+
+---
+
+## v3.3.7 (2022-11-01)
+
+### Bug Fixes
+
+* [#10282](https://github.com/netbox-community/netbox/issues/10282) - Enforce advisory locks when allocating available IP addresses to prevent race conditions
+* [#10770](https://github.com/netbox-community/netbox/issues/10282) - Fix social authentication for new users
+* [#10791](https://github.com/netbox-community/netbox/issues/10791) - Permit nullifying VLAN group `scope_type` via REST API
+* [#10803](https://github.com/netbox-community/netbox/issues/10803) - Fix exception when ordering contacts by number of assignments
+* [#10809](https://github.com/netbox-community/netbox/issues/10809) - Permit nullifying site `time_zone` via REST API
+
+---
+
+## v3.3.6 (2022-10-26)
+
+### Enhancements
+
+* [#9584](https://github.com/netbox-community/netbox/issues/9584) - Enable filtering devices by device type slug
+* [#9722](https://github.com/netbox-community/netbox/issues/9722) - Add LDAP configuration parameters to specify certificates
+* [#10580](https://github.com/netbox-community/netbox/issues/10580) - Link "assigned" checkbox in IP address table to assigned interface
+* [#10639](https://github.com/netbox-community/netbox/issues/10639) - Set cookie paths according to configured `BASE_PATH`
+* [#10685](https://github.com/netbox-community/netbox/issues/10685) - Position A/Z termination cards above the fold under circuit view
+
+### Bug Fixes
+
+* [#9669](https://github.com/netbox-community/netbox/issues/9669) - Strip colons from usernames when using remote authentication
+* [#10575](https://github.com/netbox-community/netbox/issues/10575) - Include OIDC dependencies for python-social-auth
+* [#10584](https://github.com/netbox-community/netbox/issues/10584) - Fix service clone link
+* [#10610](https://github.com/netbox-community/netbox/issues/10610) - Allow assignment of VC member to LAG on non-master peer
+* [#10643](https://github.com/netbox-community/netbox/issues/10643) - Ensure consistent display of custom fields for all model forms
+* [#10646](https://github.com/netbox-community/netbox/issues/10646) - Fix filtering of power feed by power panel when connecting a cable
+* [#10655](https://github.com/netbox-community/netbox/issues/10655) - Correct display of assigned contacts in object tables
+* [#10666](https://github.com/netbox-community/netbox/issues/10666) - Re-evaluate disabled LDAP user when processing API requests
+* [#10682](https://github.com/netbox-community/netbox/issues/10682) - Correct home view links to connection lists
+* [#10712](https://github.com/netbox-community/netbox/issues/10712) - Fix ModuleNotFoundError exception when generating API schema under Python 3.9+
+* [#10716](https://github.com/netbox-community/netbox/issues/10716) - Add left/right page plugin content embeds for tag view
+* [#10719](https://github.com/netbox-community/netbox/issues/10719) - Prevent user without sufficient permission from creating an IP address via FHRP group creation
+* [#10723](https://github.com/netbox-community/netbox/issues/10723) - Distinguish between inside/outside NAT assignments for device/VM primary IPs
+* [#10745](https://github.com/netbox-community/netbox/issues/10745) - Correct display of status field in clusters list
+* [#10746](https://github.com/netbox-community/netbox/issues/10746) - Add missing status attribute to cluster view
+
+---
+
+## v3.3.5 (2022-10-05)
+
+### Enhancements
+
+* [#8424](https://github.com/netbox-community/netbox/issues/8424) - Include rack elevation under device view
+* [#10352](https://github.com/netbox-community/netbox/issues/10352) - Omit extraneous URL query attributes during search
+* [#10465](https://github.com/netbox-community/netbox/issues/10465) - Improve formatting of device heights and rack positions
### Bug Fixes
* [#9497](https://github.com/netbox-community/netbox/issues/9497) - Adjust non-racked device filter on site and location detailed view
+* [#10408](https://github.com/netbox-community/netbox/issues/10408) - Fix validation when attempting to add redundant contact assignments
+* [#10423](https://github.com/netbox-community/netbox/issues/10423) - Enforce object type validation when creating journal entries
* [#10435](https://github.com/netbox-community/netbox/issues/10435) - Fix exception when filtering VLANs by virtual machine with no cluster assigned
* [#10439](https://github.com/netbox-community/netbox/issues/10439) - Fix form widget styling for DeviceType airflow field
+* [#10445](https://github.com/netbox-community/netbox/issues/10445) - Avoid rounding virtual machine memory values
+* [#10460](https://github.com/netbox-community/netbox/issues/10460) - Restore missing connection details for device components
+* [#10461](https://github.com/netbox-community/netbox/issues/10461) - Enable filtering by read-only custom fields in the UI
+* [#10470](https://github.com/netbox-community/netbox/issues/10470) - Omit read-only custom fields from CSV import forms
+* [#10480](https://github.com/netbox-community/netbox/issues/10480) - Cable trace SVG links should not force a new window
+* [#10491](https://github.com/netbox-community/netbox/issues/10491) - Clarify representation of blocking contact assignments during contact deletion
+* [#10513](https://github.com/netbox-community/netbox/issues/10513) - Disable the reassignment of a module to a new device
+* [#10517](https://github.com/netbox-community/netbox/issues/10517) - Automatically inherit site assignment from cluster when creating a virtual machine
+* [#10559](https://github.com/netbox-community/netbox/issues/10559) - Permit the pinning of a VM to a particular device within a cluster which has no site assignment
+* [#10562](https://github.com/netbox-community/netbox/issues/10562) - Correct URL for contacts table tags column
---
## v3.3.4 (2022-09-16)
### Bug Fixes
+
* [#10383](https://github.com/netbox-community/netbox/issues/10383) - Fix assignment of component templates to module types via web UI
* [#10387](https://github.com/netbox-community/netbox/issues/10387) - Fix `MultiValueDictKeyError` exception when editing a device interface
diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md
index 24e5a0ea9..ce3edc07c 100644
--- a/docs/release-notes/version-3.4.md
+++ b/docs/release-notes/version-3.4.md
@@ -8,32 +8,156 @@
* Device and virtual machine names are no longer case-sensitive. Attempting to create e.g. "device1" and "DEVICE1" will raise a validation error.
* The `asn` field has been removed from the provider model. Please replicate any provider ASN assignments to the ASN model introduced in NetBox v3.1 prior to upgrading.
* The `noc_contact`, `admin_contact`, and `portal_url` fields have been removed from the provider model. Please replicate any data remaining in these fields to the contact model introduced in NetBox v3.1 prior to upgrading.
+* The `content_type` field on the CustomLink and ExportTemplate models have been renamed to `content_types` and now supports the assignment of multiple content types.
+* The `cf` property on an object with custom fields now returns deserialized values. For example, a custom field referencing an object will return the object instance rather than its numeric ID. To access the raw serialized values, use `custom_field_data` instead.
### New Features
+#### New Global Search ([#10560](https://github.com/netbox-community/netbox/issues/10560))
+
+NetBox's global search functionality has been completely overhauled and replaced by a new cache-based lookup.
+
+#### CSV-Based Bulk Updates ([#7961](https://github.com/netbox-community/netbox/issues/7961))
+
+NetBox's CSV-based bulk import functionality has been extended to support also modifying existing objects. When an `id` column is present in the import form, it will be used to infer the object to be modified, rather than a new object being created. All fields (columns) are optional when modifying existing objects.
+
#### Top-Level Plugin Navigation Menus ([#9071](https://github.com/netbox-community/netbox/issues/9071))
A new `PluginMenu` class has been introduced, which enables a plugin to inject a top-level menu in NetBox's navigation menu. This menu can have one or more groups of menu items, just like core items. Backward compatibility with the existing `menu_items` has been maintained.
### Enhancements
+* [#7376](https://github.com/netbox-community/netbox/issues/7376) - Enable the assignment of tags during CSV import
+* [#8245](https://github.com/netbox-community/netbox/issues/8245) - Enable GraphQL filtering of related objects
+* [#8274](https://github.com/netbox-community/netbox/issues/8274) - Enable associating a custom link with multiple object types
+* [#8485](https://github.com/netbox-community/netbox/issues/8485) - Enable journaling for all organizational models
+* [#8853](https://github.com/netbox-community/netbox/issues/8853) - Introduce the `ALLOW_TOKEN_RETRIEVAL` config parameter to restrict the display of API tokens
* [#9249](https://github.com/netbox-community/netbox/issues/9249) - Device and virtual machine names are no longer case-sensitive
+* [#9478](https://github.com/netbox-community/netbox/issues/9478) - Add `link_peers` field to GraphQL types for cabled objects
+* [#9654](https://github.com/netbox-community/netbox/issues/9654) - Add `weight` field to racks, device types, and module types
+* [#9817](https://github.com/netbox-community/netbox/issues/9817) - Add `assigned_object` field to GraphQL type for IP addresses and L2VPN terminations
+* [#9832](https://github.com/netbox-community/netbox/issues/9832) - Add `mounting_depth` field to rack model
* [#9892](https://github.com/netbox-community/netbox/issues/9892) - Add optional `name` field for FHRP groups
+* [#10052](https://github.com/netbox-community/netbox/issues/10052) - The `cf` attribute now returns deserialized custom field data
+* [#10348](https://github.com/netbox-community/netbox/issues/10348) - Add decimal custom field type
+* [#10556](https://github.com/netbox-community/netbox/issues/10556) - Include a `display` field in all GraphQL object types
+* [#10595](https://github.com/netbox-community/netbox/issues/10595) - Add GraphQL relationships for additional generic foreign key fields
+* [#10698](https://github.com/netbox-community/netbox/issues/10698) - Omit app label from content type in table columns
+* [#10710](https://github.com/netbox-community/netbox/issues/10710) - Add `status` field to WirelessLAN
+* [#10761](https://github.com/netbox-community/netbox/issues/10761) - Enable associating an export template with multiple object types
+* [#10781](https://github.com/netbox-community/netbox/issues/10781) - Add support for Python v3.11
### Plugins API
+* [#8927](https://github.com/netbox-community/netbox/issues/8927) - Enable inclusion of plugin models in global search via `SearchIndex`
* [#9071](https://github.com/netbox-community/netbox/issues/9071) - Introduce `PluginMenu` for top-level plugin navigation menus
+* [#9072](https://github.com/netbox-community/netbox/issues/9072) - Enable registration of tabbed plugin views for core NetBox models
+* [#9880](https://github.com/netbox-community/netbox/issues/9880) - Introduce `django_apps` plugin configuration parameter
+* [#9887](https://github.com/netbox-community/netbox/issues/9887) - Inspect `docs_url` property to determine link to model documentation
* [#10314](https://github.com/netbox-community/netbox/issues/10314) - Move `clone()` method from NetBoxModel to CloningMixin
+* [#10543](https://github.com/netbox-community/netbox/issues/10543) - Introduce `get_plugin_config()` utility function
+* [#10739](https://github.com/netbox-community/netbox/issues/10739) - Introduce `get_queryset()` method on generic views
### Other Changes
* [#9045](https://github.com/netbox-community/netbox/issues/9045) - Remove legacy ASN field from provider model
* [#9046](https://github.com/netbox-community/netbox/issues/9046) - Remove legacy contact fields from provider model
* [#10358](https://github.com/netbox-community/netbox/issues/10358) - Raise minimum required PostgreSQL version from 10 to 11
+* [#10697](https://github.com/netbox-community/netbox/issues/10697) - Move application registry into core app
+* [#10699](https://github.com/netbox-community/netbox/issues/10699) - Remove custom `import_object()` function
+* [#10816](https://github.com/netbox-community/netbox/issues/10816) - Pass the current request when instantiating a FilterSet within UI views
+* [#10820](https://github.com/netbox-community/netbox/issues/10820) - Switch timezone library from pytz to zoneinfo
+* [#10821](https://github.com/netbox-community/netbox/issues/10821) - Enable data localization
### REST API Changes
* circuits.provider
* Removed the `asn`, `noc_contact`, `admin_contact`, and `portal_url` fields
+ * Added a `description` field
+* dcim.Cable
+ * Added `description` and `comments` fields
+* dcim.Device
+ * Added a `description` field
+* dcim.DeviceType
+ * Added a `description` field
+ * Added optional `weight` and `weight_unit` fields
+* dcim.Module
+ * Added a `description` field
+* dcim.ModuleType
+ * Added a `description` field
+ * Added optional `weight` and `weight_unit` fields
+* dcim.PowerFeed
+ * Added a `description` field
+* dcim.PowerPanel
+ * Added `description` and `comments` fields
+* dcim.Rack
+ * Added a `description` field
+ * Added optional `weight` and `weight_unit` fields
+* dcim.RackReservation
+ * Added a `comments` field
+* dcim.VirtualChassis
+ * Added `description` and `comments` fields
+* extras.CustomLink
+ * Renamed `content_type` field to `content_types`
+* extras.ExportTemplate
+ * Renamed `content_type` field to `content_types`
+* ipam.Aggregate
+ * Added a `comments` field
+* ipam.ASN
+ * Added a `comments` field
* ipam.FHRPGroup
+ * Added a `comments` field
* Added optional `name` field
+* ipam.IPAddress
+ * Added a `comments` field
+* ipam.IPRange
+ * Added a `comments` field
+* ipam.L2VPN
+ * Added a `comments` field
+* ipam.Prefix
+ * Added a `comments` field
+* ipam.RouteTarget
+ * Added a `comments` field
+* ipam.Service
+ * Added a `comments` field
+* ipam.ServiceTemplate
+ * Added a `comments` field
+* ipam.VLAN
+ * Added a `comments` field
+* ipam.VRF
+ * Added a `comments` field
+* tenancy.Contact
+ * Added a `description` field
+* virtualization.Cluster
+ * Added a `description` field
+* virtualization.VirtualMachine
+ * Added a `description` field
+* wireless.WirelessLAN
+ * Added a required `status` choice field
+ * Added a `comments` field
+* wireless.WirelessLink
+ * Added a `comments` field
+
+### GraphQL API Changes
+
+* All object types now include a `display` field
+* All cabled object types now include a `link_peers` field
+* Add a `contacts` relationship for all relevant models
+* dcim.Cable
+ * Add A/B terminations fields
+* dcim.CableTermination
+ * Add `termination` field
+* dcim.InventoryItem
+ * Add `component` field
+* dcim.InventoryItemTemplate
+ * Add `component` field
+* dcim.Rack
+ * Add `mounting_depth` field
+* ipam.FHRPGroupAssignment
+ * Add `interface` field
+* ipam.IPAddress
+ * Add `assigned_object` field
+* ipam.L2VPNTermination
+ * Add `assigned_object` field
+* ipam.VLANGroupType
+ * Add `scope` field
diff --git a/mkdocs.yml b/mkdocs.yml
index 8f6e2930a..011d4414f 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -30,7 +30,7 @@ plugins:
- os.chdir('netbox/')
- os.environ.setdefault("DJANGO_SETTINGS_MODULE", "netbox.settings")
- django.setup()
- rendering:
+ options:
heading_level: 3
members_order: source
show_root_heading: true
@@ -38,7 +38,6 @@ plugins:
show_root_toc_entry: false
show_source: false
extra:
- readthedocs: !ENV READTHEDOCS
social:
- icon: fontawesome/brands/github
link: https://github.com/netbox-community/netbox
@@ -133,6 +132,7 @@ nav:
- GraphQL API: 'plugins/development/graphql-api.md'
- Background Tasks: 'plugins/development/background-tasks.md'
- Exceptions: 'plugins/development/exceptions.md'
+ - Search: 'plugins/development/search.md'
- Administration:
- Authentication:
- Overview: 'administration/authentication/overview.md'
@@ -245,6 +245,7 @@ nav:
- Adding Models: 'development/adding-models.md'
- Extending Models: 'development/extending-models.md'
- Signals: 'development/signals.md'
+ - Search: 'development/search.md'
- Application Registry: 'development/application-registry.md'
- User Preferences: 'development/user-preferences.md'
- Web UI: 'development/web-ui.md'
diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py
index 4a8e2bd28..2bcb0895a 100644
--- a/netbox/circuits/api/serializers.py
+++ b/netbox/circuits/api/serializers.py
@@ -31,8 +31,8 @@ class ProviderSerializer(NetBoxModelSerializer):
class Meta:
model = Provider
fields = [
- 'id', 'url', 'display', 'name', 'slug', 'account',
- 'comments', 'asns', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count',
+ 'id', 'url', 'display', 'name', 'slug', 'account', 'description', 'comments', 'asns', 'tags',
+ 'custom_fields', 'created', 'last_updated', 'circuit_count',
]
diff --git a/netbox/circuits/apps.py b/netbox/circuits/apps.py
index bc0b7d87d..3acf3b98c 100644
--- a/netbox/circuits/apps.py
+++ b/netbox/circuits/apps.py
@@ -6,4 +6,4 @@ class CircuitsConfig(AppConfig):
verbose_name = "Circuits"
def ready(self):
- import circuits.signals
+ from . import signals, search
diff --git a/netbox/circuits/forms/__init__.py b/netbox/circuits/forms/__init__.py
index 5c23f833a..1499f98b2 100644
--- a/netbox/circuits/forms/__init__.py
+++ b/netbox/circuits/forms/__init__.py
@@ -1,4 +1,4 @@
from .bulk_edit import *
from .bulk_import import *
from .filtersets import *
-from .models import *
+from .model_forms import *
diff --git a/netbox/circuits/forms/bulk_edit.py b/netbox/circuits/forms/bulk_edit.py
index 12975b5d6..6e9ae516c 100644
--- a/netbox/circuits/forms/bulk_edit.py
+++ b/netbox/circuits/forms/bulk_edit.py
@@ -30,6 +30,10 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
required=False,
label='Account number'
)
+ description = forms.CharField(
+ max_length=200,
+ required=False
+ )
comments = CommentField(
widget=SmallTextarea,
label='Comments'
@@ -40,7 +44,7 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
(None, ('asns', 'account', )),
)
nullable_fields = (
- 'asns', 'account', 'comments',
+ 'asns', 'account', 'description', 'comments',
)
diff --git a/netbox/circuits/forms/bulk_import.py b/netbox/circuits/forms/bulk_import.py
index 77ebb3de9..4976e2d9b 100644
--- a/netbox/circuits/forms/bulk_import.py
+++ b/netbox/circuits/forms/bulk_import.py
@@ -18,7 +18,7 @@ class ProviderCSVForm(NetBoxModelCSVForm):
class Meta:
model = Provider
fields = (
- 'name', 'slug', 'account', 'comments',
+ 'name', 'slug', 'account', 'description', 'comments', 'tags',
)
@@ -32,7 +32,7 @@ class ProviderNetworkCSVForm(NetBoxModelCSVForm):
class Meta:
model = ProviderNetwork
fields = [
- 'provider', 'name', 'service_id', 'description', 'comments',
+ 'provider', 'name', 'service_id', 'description', 'comments', 'tags'
]
@@ -41,7 +41,7 @@ class CircuitTypeCSVForm(NetBoxModelCSVForm):
class Meta:
model = CircuitType
- fields = ('name', 'slug', 'description')
+ fields = ('name', 'slug', 'description', 'tags')
help_texts = {
'name': 'Name of circuit type',
}
@@ -73,5 +73,5 @@ class CircuitCSVForm(NetBoxModelCSVForm):
model = Circuit
fields = [
'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate',
- 'description', 'comments',
+ 'description', 'comments', 'tags'
]
diff --git a/netbox/circuits/forms/filtersets.py b/netbox/circuits/forms/filtersets.py
index 29410ffdf..9ad825299 100644
--- a/netbox/circuits/forms/filtersets.py
+++ b/netbox/circuits/forms/filtersets.py
@@ -20,7 +20,7 @@ __all__ = (
class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Provider
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id')),
('ASN', ('asn',)),
('Contacts', ('contact', 'contact_role', 'contact_group')),
@@ -59,7 +59,7 @@ class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
class ProviderNetworkFilterForm(NetBoxModelFilterSetForm):
model = ProviderNetwork
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Attributes', ('provider_id', 'service_id')),
)
provider_id = DynamicModelMultipleChoiceField(
@@ -82,7 +82,7 @@ class CircuitTypeFilterForm(NetBoxModelFilterSetForm):
class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Circuit
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Provider', ('provider_id', 'provider_network_id')),
('Attributes', ('type_id', 'status', 'install_date', 'termination_date', 'commit_rate')),
('Location', ('region_id', 'site_group_id', 'site_id')),
diff --git a/netbox/circuits/forms/models.py b/netbox/circuits/forms/model_forms.py
similarity index 91%
rename from netbox/circuits/forms/models.py
rename to netbox/circuits/forms/model_forms.py
index 17c2e7480..ab1b6bca2 100644
--- a/netbox/circuits/forms/models.py
+++ b/netbox/circuits/forms/model_forms.py
@@ -1,4 +1,3 @@
-from django import forms
from django.utils.translation import gettext as _
from circuits.models import *
@@ -7,8 +6,8 @@ from ipam.models import ASN
from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm
from utilities.forms import (
- BootstrapMixin, CommentField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
- SelectSpeedWidget, SmallTextarea, SlugField, StaticSelect,
+ CommentField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SelectSpeedWidget, SlugField,
+ StaticSelect,
)
__all__ = (
@@ -30,14 +29,14 @@ class ProviderForm(NetBoxModelForm):
comments = CommentField()
fieldsets = (
- ('Provider', ('name', 'slug', 'asns', 'tags')),
+ ('Provider', ('name', 'slug', 'asns', 'description', 'tags')),
('Support Info', ('account',)),
)
class Meta:
model = Provider
fields = [
- 'name', 'slug', 'account', 'asns', 'comments', 'tags',
+ 'name', 'slug', 'account', 'asns', 'description', 'comments', 'tags',
]
help_texts = {
'name': "Full name of the provider",
@@ -64,6 +63,12 @@ class ProviderNetworkForm(NetBoxModelForm):
class CircuitTypeForm(NetBoxModelForm):
slug = SlugField()
+ fieldsets = (
+ ('Circuit Type', (
+ 'name', 'slug', 'description', 'tags',
+ )),
+ )
+
class Meta:
model = CircuitType
fields = [
diff --git a/netbox/circuits/graphql/types.py b/netbox/circuits/graphql/types.py
index e96fe98a5..5582de798 100644
--- a/netbox/circuits/graphql/types.py
+++ b/netbox/circuits/graphql/types.py
@@ -1,6 +1,8 @@
+import graphene
+
from circuits import filtersets, models
from dcim.graphql.mixins import CabledObjectMixin
-from extras.graphql.mixins import CustomFieldsMixin, TagsMixin
+from extras.graphql.mixins import CustomFieldsMixin, TagsMixin, ContactsMixin
from netbox.graphql.types import ObjectType, OrganizationalObjectType, NetBoxObjectType
__all__ = (
@@ -20,8 +22,7 @@ class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, Ob
filterset_class = filtersets.CircuitTerminationFilterSet
-class CircuitType(NetBoxObjectType):
-
+class CircuitType(NetBoxObjectType, ContactsMixin):
class Meta:
model = models.Circuit
fields = '__all__'
@@ -36,7 +37,7 @@ class CircuitTypeType(OrganizationalObjectType):
filterset_class = filtersets.CircuitTypeFilterSet
-class ProviderType(NetBoxObjectType):
+class ProviderType(NetBoxObjectType, ContactsMixin):
class Meta:
model = models.Provider
diff --git a/netbox/circuits/migrations/0001_squashed.py b/netbox/circuits/migrations/0001_squashed.py
index 851f40a22..656eb35a1 100644
--- a/netbox/circuits/migrations/0001_squashed.py
+++ b/netbox/circuits/migrations/0001_squashed.py
@@ -1,5 +1,5 @@
import dcim.fields
-import django.core.serializers.json
+from utilities.json import CustomFieldJSONEncoder
from django.db import migrations, models
import django.db.models.deletion
@@ -21,7 +21,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('cid', models.CharField(max_length=100)),
('status', models.CharField(default='active', max_length=50)),
@@ -58,14 +58,14 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, unique=True)),
('slug', models.SlugField(max_length=100, unique=True)),
('description', models.CharField(blank=True, max_length=200)),
],
options={
- 'ordering': ['name'],
+ 'ordering': ('name',),
},
),
migrations.CreateModel(
@@ -73,7 +73,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, unique=True)),
('slug', models.SlugField(max_length=100, unique=True)),
@@ -93,7 +93,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100)),
('description', models.CharField(blank=True, max_length=200)),
diff --git a/netbox/circuits/migrations/0036_circuit_termination_date_tags_custom_fields.py b/netbox/circuits/migrations/0036_circuit_termination_date_tags_custom_fields.py
index c686bf042..96b2a9d97 100644
--- a/netbox/circuits/migrations/0036_circuit_termination_date_tags_custom_fields.py
+++ b/netbox/circuits/migrations/0036_circuit_termination_date_tags_custom_fields.py
@@ -1,4 +1,4 @@
-import django.core.serializers.json
+from utilities.json import CustomFieldJSONEncoder
from django.db import migrations, models
import taggit.managers
@@ -18,7 +18,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='circuittermination',
name='custom_field_data',
- field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
+ field=models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder),
),
migrations.AddField(
model_name='circuittermination',
diff --git a/netbox/circuits/migrations/0041_standardize_description_comments.py b/netbox/circuits/migrations/0041_standardize_description_comments.py
new file mode 100644
index 000000000..49cdefcba
--- /dev/null
+++ b/netbox/circuits/migrations/0041_standardize_description_comments.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.2 on 2022-11-03 18:24
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('circuits', '0040_provider_remove_deprecated_fields'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='provider',
+ name='description',
+ field=models.CharField(blank=True, max_length=200),
+ ),
+ ]
diff --git a/netbox/circuits/models/circuits.py b/netbox/circuits/models/circuits.py
index ea74eeb40..9d302bb8e 100644
--- a/netbox/circuits/models/circuits.py
+++ b/netbox/circuits/models/circuits.py
@@ -7,7 +7,7 @@ from django.urls import reverse
from circuits.choices import *
from dcim.models import CabledObjectModel
from netbox.models import (
- ChangeLoggedModel, CustomFieldsMixin, CustomLinksMixin, OrganizationalModel, NetBoxModel, TagsMixin,
+ ChangeLoggedModel, CustomFieldsMixin, CustomLinksMixin, OrganizationalModel, PrimaryModel, TagsMixin,
)
from netbox.models.features import WebhooksMixin
@@ -23,30 +23,11 @@ class CircuitType(OrganizationalModel):
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
"Long Haul," "Metro," or "Out-of-Band".
"""
- name = models.CharField(
- max_length=100,
- unique=True
- )
- slug = models.SlugField(
- max_length=100,
- unique=True
- )
- description = models.CharField(
- max_length=200,
- blank=True,
- )
-
- class Meta:
- ordering = ['name']
-
- def __str__(self):
- return self.name
-
def get_absolute_url(self):
return reverse('circuits:circuittype', args=[self.pk])
-class Circuit(NetBoxModel):
+class Circuit(PrimaryModel):
"""
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
circuits. Each circuit is also assigned a CircuitType and a Site. Circuit port speed and commit rate are measured
@@ -92,13 +73,6 @@ class Circuit(NetBoxModel):
blank=True,
null=True,
verbose_name='Commit rate (Kbps)')
- description = models.CharField(
- max_length=200,
- blank=True
- )
- comments = models.TextField(
- blank=True
- )
# Generic relations
contacts = GenericRelation(
diff --git a/netbox/circuits/models/providers.py b/netbox/circuits/models/providers.py
index bd63ff0c6..18a81dcef 100644
--- a/netbox/circuits/models/providers.py
+++ b/netbox/circuits/models/providers.py
@@ -2,8 +2,7 @@ from django.contrib.contenttypes.fields import GenericRelation
from django.db import models
from django.urls import reverse
-from dcim.fields import ASNField
-from netbox.models import NetBoxModel
+from netbox.models import PrimaryModel
__all__ = (
'ProviderNetwork',
@@ -11,7 +10,7 @@ __all__ = (
)
-class Provider(NetBoxModel):
+class Provider(PrimaryModel):
"""
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
stores information pertinent to the user's relationship with the Provider.
@@ -34,9 +33,6 @@ class Provider(NetBoxModel):
blank=True,
verbose_name='Account number'
)
- comments = models.TextField(
- blank=True
- )
# Generic relations
contacts = GenericRelation(
@@ -57,7 +53,7 @@ class Provider(NetBoxModel):
return reverse('circuits:provider', args=[self.pk])
-class ProviderNetwork(NetBoxModel):
+class ProviderNetwork(PrimaryModel):
"""
This represents a provider network which exists outside of NetBox, the details of which are unknown or
unimportant to the user.
@@ -75,13 +71,6 @@ class ProviderNetwork(NetBoxModel):
blank=True,
verbose_name='Service ID'
)
- description = models.CharField(
- max_length=200,
- blank=True
- )
- comments = models.TextField(
- blank=True
- )
class Meta:
ordering = ('provider', 'name')
diff --git a/netbox/circuits/search.py b/netbox/circuits/search.py
new file mode 100644
index 000000000..673f6308f
--- /dev/null
+++ b/netbox/circuits/search.py
@@ -0,0 +1,55 @@
+from netbox.search import SearchIndex, register_search
+from . import models
+
+
+@register_search
+class CircuitIndex(SearchIndex):
+ model = models.Circuit
+ fields = (
+ ('cid', 100),
+ ('description', 500),
+ ('comments', 5000),
+ )
+
+
+@register_search
+class CircuitTerminationIndex(SearchIndex):
+ model = models.CircuitTermination
+ fields = (
+ ('xconnect_id', 300),
+ ('pp_info', 300),
+ ('description', 500),
+ ('port_speed', 2000),
+ ('upstream_speed', 2000),
+ )
+
+
+@register_search
+class CircuitTypeIndex(SearchIndex):
+ model = models.CircuitType
+ fields = (
+ ('name', 100),
+ ('slug', 110),
+ ('description', 500),
+ )
+
+
+@register_search
+class ProviderIndex(SearchIndex):
+ model = models.Provider
+ fields = (
+ ('name', 100),
+ ('account', 200),
+ ('comments', 5000),
+ )
+
+
+@register_search
+class ProviderNetworkIndex(SearchIndex):
+ model = models.ProviderNetwork
+ fields = (
+ ('name', 100),
+ ('service_id', 200),
+ ('description', 500),
+ ('comments', 5000),
+ )
diff --git a/netbox/circuits/tables/circuits.py b/netbox/circuits/tables/circuits.py
index f9ab7e190..477f9c1ab 100644
--- a/netbox/circuits/tables/circuits.py
+++ b/netbox/circuits/tables/circuits.py
@@ -1,8 +1,9 @@
import django_tables2 as tables
-
from circuits.models import *
+from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
+
from netbox.tables import NetBoxTable, columns
-from tenancy.tables import TenancyColumnsMixin
+
from .columns import CommitRateColumn
__all__ = (
@@ -39,7 +40,7 @@ class CircuitTypeTable(NetBoxTable):
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug')
-class CircuitTable(TenancyColumnsMixin, NetBoxTable):
+class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
cid = tables.Column(
linkify=True,
verbose_name='Circuit ID'
@@ -58,9 +59,6 @@ class CircuitTable(TenancyColumnsMixin, NetBoxTable):
)
commit_rate = CommitRateColumn()
comments = columns.MarkdownColumn()
- contacts = columns.ManyToManyColumn(
- linkify_item=True
- )
tags = columns.TagColumn(
url_name='circuits:circuit_list'
)
diff --git a/netbox/circuits/tables/providers.py b/netbox/circuits/tables/providers.py
index 3e2fd1193..9de8d25b2 100644
--- a/netbox/circuits/tables/providers.py
+++ b/netbox/circuits/tables/providers.py
@@ -1,7 +1,8 @@
import django_tables2 as tables
-from django_tables2.utils import Accessor
-
from circuits.models import *
+from django_tables2.utils import Accessor
+from tenancy.tables import ContactsColumnMixin
+
from netbox.tables import NetBoxTable, columns
__all__ = (
@@ -10,7 +11,7 @@ __all__ = (
)
-class ProviderTable(NetBoxTable):
+class ProviderTable(ContactsColumnMixin, NetBoxTable):
name = tables.Column(
linkify=True
)
@@ -31,9 +32,6 @@ class ProviderTable(NetBoxTable):
verbose_name='Circuits'
)
comments = columns.MarkdownColumn()
- contacts = columns.ManyToManyColumn(
- linkify_item=True
- )
tags = columns.TagColumn(
url_name='circuits:provider_list'
)
@@ -41,8 +39,8 @@ class ProviderTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = Provider
fields = (
- 'pk', 'id', 'name', 'asns', 'account', 'asn_count',
- 'circuit_count', 'comments', 'contacts', 'tags', 'created', 'last_updated',
+ 'pk', 'id', 'name', 'asns', 'account', 'asn_count', 'circuit_count', 'description', 'comments', 'contacts',
+ 'tags', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'account', 'circuit_count')
diff --git a/netbox/circuits/tests/test_views.py b/netbox/circuits/tests/test_views.py
index 9644c0b02..54d001c8d 100644
--- a/netbox/circuits/tests/test_views.py
+++ b/netbox/circuits/tests/test_views.py
@@ -50,6 +50,13 @@ class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase):
"Provider 6,provider-6",
)
+ cls.csv_update_data = (
+ "id,name,comments",
+ f"{providers[0].pk},Provider 7,New comment7",
+ f"{providers[1].pk},Provider 8,New comment8",
+ f"{providers[2].pk},Provider 9,New comment9",
+ )
+
cls.bulk_edit_data = {
'account': '5678',
'comments': 'New comments',
@@ -62,11 +69,13 @@ class CircuitTypeTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
@classmethod
def setUpTestData(cls):
- CircuitType.objects.bulk_create([
+ circuit_types = (
CircuitType(name='Circuit Type 1', slug='circuit-type-1'),
CircuitType(name='Circuit Type 2', slug='circuit-type-2'),
CircuitType(name='Circuit Type 3', slug='circuit-type-3'),
- ])
+ )
+
+ CircuitType.objects.bulk_create(circuit_types)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
@@ -84,6 +93,13 @@ class CircuitTypeTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
"Circuit Type 6,circuit-type-6",
)
+ cls.csv_update_data = (
+ "id,name,description",
+ f"{circuit_types[0].pk},Circuit Type 7,New description7",
+ f"{circuit_types[1].pk},Circuit Type 8,New description8",
+ f"{circuit_types[2].pk},Circuit Type 9,New description9",
+ )
+
cls.bulk_edit_data = {
'description': 'Foo',
}
@@ -107,11 +123,13 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
)
CircuitType.objects.bulk_create(circuittypes)
- Circuit.objects.bulk_create([
+ circuits = (
Circuit(cid='Circuit 1', provider=providers[0], type=circuittypes[0]),
Circuit(cid='Circuit 2', provider=providers[0], type=circuittypes[0]),
Circuit(cid='Circuit 3', provider=providers[0], type=circuittypes[0]),
- ])
+ )
+
+ Circuit.objects.bulk_create(circuits)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
@@ -136,6 +154,13 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
"Circuit 6,Provider 1,Circuit Type 1,active",
)
+ cls.csv_update_data = (
+ f"id,cid,description,status",
+ f"{circuits[0].pk},Circuit 7,New description7,{CircuitStatusChoices.STATUS_DECOMMISSIONED}",
+ f"{circuits[1].pk},Circuit 8,New description8,{CircuitStatusChoices.STATUS_DECOMMISSIONED}",
+ f"{circuits[2].pk},Circuit 9,New description9,{CircuitStatusChoices.STATUS_DECOMMISSIONED}",
+ )
+
cls.bulk_edit_data = {
'provider': providers[1].pk,
'type': circuittypes[1].pk,
@@ -159,11 +184,13 @@ class ProviderNetworkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
)
Provider.objects.bulk_create(providers)
- ProviderNetwork.objects.bulk_create([
+ provider_networks = (
ProviderNetwork(name='Provider Network 1', provider=providers[0]),
ProviderNetwork(name='Provider Network 2', provider=providers[0]),
ProviderNetwork(name='Provider Network 3', provider=providers[0]),
- ])
+ )
+
+ ProviderNetwork.objects.bulk_create(provider_networks)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
@@ -182,6 +209,13 @@ class ProviderNetworkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
"Provider Network 6,Provider 1,Baz",
)
+ cls.csv_update_data = (
+ "id,name,description",
+ f"{provider_networks[0].pk},Provider Network 7,New description7",
+ f"{provider_networks[1].pk},Provider Network 8,New description8",
+ f"{provider_networks[2].pk},Provider Network 9,New description9",
+ )
+
cls.bulk_edit_data = {
'provider': providers[1].pk,
'description': 'New description',
diff --git a/netbox/circuits/urls.py b/netbox/circuits/urls.py
index 5b15b29ac..d8c5ea276 100644
--- a/netbox/circuits/urls.py
+++ b/netbox/circuits/urls.py
@@ -1,9 +1,7 @@
-from django.urls import path
+from django.urls import include, path
-from dcim.views import PathTraceView
-from netbox.views.generic import ObjectChangeLogView, ObjectJournalView
+from utilities.urls import get_model_urls
from . import views
-from .models import *
app_name = 'circuits'
urlpatterns = [
@@ -14,11 +12,7 @@ urlpatterns = [
path('providers/import/', views.ProviderBulkImportView.as_view(), name='provider_import'),
path('providers/edit/', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'),
path('providers/delete/', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'),
- path('providers//', views.ProviderView.as_view(), name='provider'),
- path('providers//edit/', views.ProviderEditView.as_view(), name='provider_edit'),
- path('providers//delete/', views.ProviderDeleteView.as_view(), name='provider_delete'),
- path('providers//changelog/', ObjectChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}),
- path('providers//journal/', ObjectJournalView.as_view(), name='provider_journal', kwargs={'model': Provider}),
+ path('providers//', include(get_model_urls('circuits', 'provider'))),
# Provider networks
path('provider-networks/', views.ProviderNetworkListView.as_view(), name='providernetwork_list'),
@@ -26,11 +20,7 @@ urlpatterns = [
path('provider-networks/import/', views.ProviderNetworkBulkImportView.as_view(), name='providernetwork_import'),
path('provider-networks/edit/', views.ProviderNetworkBulkEditView.as_view(), name='providernetwork_bulk_edit'),
path('provider-networks/delete/', views.ProviderNetworkBulkDeleteView.as_view(), name='providernetwork_bulk_delete'),
- path('provider-networks//', views.ProviderNetworkView.as_view(), name='providernetwork'),
- path('provider-networks//edit/', views.ProviderNetworkEditView.as_view(), name='providernetwork_edit'),
- path('provider-networks//delete/', views.ProviderNetworkDeleteView.as_view(), name='providernetwork_delete'),
- path('provider-networks//changelog/', ObjectChangeLogView.as_view(), name='providernetwork_changelog', kwargs={'model': ProviderNetwork}),
- path('provider-networks//journal/', ObjectJournalView.as_view(), name='providernetwork_journal', kwargs={'model': ProviderNetwork}),
+ path('provider-networks//', include(get_model_urls('circuits', 'providernetwork'))),
# Circuit types
path('circuit-types/', views.CircuitTypeListView.as_view(), name='circuittype_list'),
@@ -38,10 +28,7 @@ urlpatterns = [
path('circuit-types/import/', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'),
path('circuit-types/edit/', views.CircuitTypeBulkEditView.as_view(), name='circuittype_bulk_edit'),
path('circuit-types/delete/', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'),
- path('circuit-types//', views.CircuitTypeView.as_view(), name='circuittype'),
- path('circuit-types//edit/', views.CircuitTypeEditView.as_view(), name='circuittype_edit'),
- path('circuit-types//delete/', views.CircuitTypeDeleteView.as_view(), name='circuittype_delete'),
- path('circuit-types//changelog/', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}),
+ path('circuit-types//', include(get_model_urls('circuits', 'circuittype'))),
# Circuits
path('circuits/', views.CircuitListView.as_view(), name='circuit_list'),
@@ -49,17 +36,11 @@ urlpatterns = [
path('circuits/import/', views.CircuitBulkImportView.as_view(), name='circuit_import'),
path('circuits/edit/', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'),
path('circuits/delete/', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'),
- path('circuits//', views.CircuitView.as_view(), name='circuit'),
- path('circuits//edit/', views.CircuitEditView.as_view(), name='circuit_edit'),
- path('circuits//delete/', views.CircuitDeleteView.as_view(), name='circuit_delete'),
- path('circuits//changelog/', ObjectChangeLogView.as_view(), name='circuit_changelog', kwargs={'model': Circuit}),
- path('circuits//journal/', ObjectJournalView.as_view(), name='circuit_journal', kwargs={'model': Circuit}),
path('circuits//terminations/swap/', views.CircuitSwapTerminations.as_view(), name='circuit_terminations_swap'),
+ path('circuits//', include(get_model_urls('circuits', 'circuit'))),
# Circuit terminations
path('circuit-terminations/add/', views.CircuitTerminationEditView.as_view(), name='circuittermination_add'),
- path('circuit-terminations//edit/', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'),
- path('circuit-terminations//delete/', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'),
- path('circuit-terminations//trace/', PathTraceView.as_view(), name='circuittermination_trace', kwargs={'model': CircuitTermination}),
+ path('circuit-terminations//', include(get_model_urls('circuits', 'circuittermination'))),
]
diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py
index 423bd67d6..dc809666b 100644
--- a/netbox/circuits/views.py
+++ b/netbox/circuits/views.py
@@ -3,9 +3,11 @@ from django.db import transaction
from django.db.models import Q
from django.shortcuts import get_object_or_404, redirect, render
+from dcim.views import PathTraceView
from netbox.views import generic
from utilities.forms import ConfirmationForm
from utilities.utils import count_related
+from utilities.views import register_model_view
from . import filtersets, forms, tables
from .models import *
@@ -23,6 +25,7 @@ class ProviderListView(generic.ObjectListView):
table = tables.ProviderTable
+@register_model_view(Provider)
class ProviderView(generic.ObjectView):
queryset = Provider.objects.all()
@@ -41,11 +44,13 @@ class ProviderView(generic.ObjectView):
}
+@register_model_view(Provider, 'edit')
class ProviderEditView(generic.ObjectEditView):
queryset = Provider.objects.all()
form = forms.ProviderForm
+@register_model_view(Provider, 'delete')
class ProviderDeleteView(generic.ObjectDeleteView):
queryset = Provider.objects.all()
@@ -84,6 +89,7 @@ class ProviderNetworkListView(generic.ObjectListView):
table = tables.ProviderNetworkTable
+@register_model_view(ProviderNetwork)
class ProviderNetworkView(generic.ObjectView):
queryset = ProviderNetwork.objects.all()
@@ -103,11 +109,13 @@ class ProviderNetworkView(generic.ObjectView):
}
+@register_model_view(ProviderNetwork, 'edit')
class ProviderNetworkEditView(generic.ObjectEditView):
queryset = ProviderNetwork.objects.all()
form = forms.ProviderNetworkForm
+@register_model_view(ProviderNetwork, 'delete')
class ProviderNetworkDeleteView(generic.ObjectDeleteView):
queryset = ProviderNetwork.objects.all()
@@ -144,6 +152,7 @@ class CircuitTypeListView(generic.ObjectListView):
table = tables.CircuitTypeTable
+@register_model_view(CircuitType)
class CircuitTypeView(generic.ObjectView):
queryset = CircuitType.objects.all()
@@ -157,11 +166,13 @@ class CircuitTypeView(generic.ObjectView):
}
+@register_model_view(CircuitType, 'edit')
class CircuitTypeEditView(generic.ObjectEditView):
queryset = CircuitType.objects.all()
form = forms.CircuitTypeForm
+@register_model_view(CircuitType, 'delete')
class CircuitTypeDeleteView(generic.ObjectDeleteView):
queryset = CircuitType.objects.all()
@@ -202,15 +213,18 @@ class CircuitListView(generic.ObjectListView):
table = tables.CircuitTable
+@register_model_view(Circuit)
class CircuitView(generic.ObjectView):
queryset = Circuit.objects.all()
+@register_model_view(Circuit, 'edit')
class CircuitEditView(generic.ObjectEditView):
queryset = Circuit.objects.all()
form = forms.CircuitForm
+@register_model_view(Circuit, 'delete')
class CircuitDeleteView(generic.ObjectDeleteView):
queryset = Circuit.objects.all()
@@ -318,11 +332,17 @@ class CircuitSwapTerminations(generic.ObjectEditView):
# Circuit terminations
#
+@register_model_view(CircuitTermination, 'edit')
class CircuitTerminationEditView(generic.ObjectEditView):
queryset = CircuitTermination.objects.all()
form = forms.CircuitTerminationForm
template_name = 'circuits/circuittermination_edit.html'
+@register_model_view(CircuitTermination, 'delete')
class CircuitTerminationDeleteView(generic.ObjectDeleteView):
queryset = CircuitTermination.objects.all()
+
+
+# Trace view
+register_model_view(CircuitTermination, 'trace', kwargs={'model': CircuitTermination})(PathTraceView)
diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py
index 897ee4ca3..9317d7c51 100644
--- a/netbox/dcim/api/serializers.py
+++ b/netbox/dcim/api/serializers.py
@@ -130,7 +130,7 @@ class SiteSerializer(NetBoxModelSerializer):
region = NestedRegionSerializer(required=False, allow_null=True)
group = NestedSiteGroupSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True)
- time_zone = TimeZoneSerializerField(required=False)
+ time_zone = TimeZoneSerializerField(required=False, allow_null=True)
asns = SerializedPKRelatedField(
queryset=ASN.objects.all(),
serializer=NestedASNSerializer,
@@ -201,6 +201,7 @@ class RackSerializer(NetBoxModelSerializer):
default=None)
width = ChoiceField(choices=RackWidthChoices, required=False)
outer_unit = ChoiceField(choices=RackDimensionUnitChoices, allow_blank=True, required=False)
+ weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False)
device_count = serializers.IntegerField(read_only=True)
powerfeed_count = serializers.IntegerField(read_only=True)
@@ -208,8 +209,9 @@ class RackSerializer(NetBoxModelSerializer):
model = Rack
fields = [
'id', 'url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status', 'role', 'serial',
- 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
- 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count',
+ 'asset_tag', 'type', 'width', 'u_height', 'weight', 'weight_unit', 'desc_units', 'outer_width',
+ 'outer_depth', 'outer_unit', 'mounting_depth', 'description', 'comments', 'tags', 'custom_fields',
+ 'created', 'last_updated', 'device_count', 'powerfeed_count',
]
@@ -241,8 +243,8 @@ class RackReservationSerializer(NetBoxModelSerializer):
class Meta:
model = RackReservation
fields = [
- 'id', 'url', 'display', 'rack', 'units', 'created', 'last_updated', 'user', 'tenant', 'description', 'tags',
- 'custom_fields',
+ 'id', 'url', 'display', 'rack', 'units', 'created', 'last_updated', 'user', 'tenant', 'description',
+ 'comments', 'tags', 'custom_fields',
]
@@ -315,27 +317,28 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
)
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False)
airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False)
+ weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False)
device_count = serializers.IntegerField(read_only=True)
class Meta:
model = DeviceType
fields = [
'id', 'url', 'display', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
- 'subdevice_role', 'airflow', 'front_image', 'rear_image', 'comments', 'tags', 'custom_fields', 'created',
- 'last_updated', 'device_count',
+ 'subdevice_role', 'airflow', 'weight', 'weight_unit', 'front_image', 'rear_image', 'description',
+ 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count',
]
class ModuleTypeSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
manufacturer = NestedManufacturerSerializer()
- # module_count = serializers.IntegerField(read_only=True)
+ weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False)
class Meta:
model = ModuleType
fields = [
- 'id', 'url', 'display', 'manufacturer', 'model', 'part_number', 'comments', 'tags', 'custom_fields',
- 'created', 'last_updated',
+ 'id', 'url', 'display', 'manufacturer', 'model', 'part_number', 'weight', 'weight_unit', 'description',
+ 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
]
@@ -652,8 +655,8 @@ class DeviceSerializer(NetBoxModelSerializer):
fields = [
'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'airflow', 'primary_ip',
- 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments',
- 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated',
+ 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'description',
+ 'comments', 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated',
]
@swagger_serializer_method(serializer_or_field=NestedDeviceSerializer)
@@ -677,8 +680,8 @@ class ModuleSerializer(NetBoxModelSerializer):
class Meta:
model = Module
fields = [
- 'id', 'url', 'display', 'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'comments', 'tags',
- 'custom_fields', 'created', 'last_updated',
+ 'id', 'url', 'display', 'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'description',
+ 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
]
@@ -1016,7 +1019,7 @@ class CableSerializer(NetBoxModelSerializer):
model = Cable
fields = [
'id', 'url', 'display', 'type', 'a_terminations', 'b_terminations', 'status', 'tenant', 'label', 'color',
- 'length', 'length_unit', 'tags', 'custom_fields', 'created', 'last_updated',
+ 'length', 'length_unit', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
]
@@ -1082,8 +1085,8 @@ class VirtualChassisSerializer(NetBoxModelSerializer):
class Meta:
model = VirtualChassis
fields = [
- 'id', 'url', 'display', 'name', 'domain', 'master', 'tags', 'custom_fields', 'member_count',
- 'created', 'last_updated',
+ 'id', 'url', 'display', 'name', 'domain', 'master', 'description', 'comments', 'tags', 'custom_fields',
+ 'member_count', 'created', 'last_updated',
]
@@ -1104,8 +1107,8 @@ class PowerPanelSerializer(NetBoxModelSerializer):
class Meta:
model = PowerPanel
fields = [
- 'id', 'url', 'display', 'site', 'location', 'name', 'tags', 'custom_fields', 'powerfeed_count',
- 'created', 'last_updated',
+ 'id', 'url', 'display', 'site', 'location', 'name', 'description', 'comments', 'tags', 'custom_fields',
+ 'powerfeed_count', 'created', 'last_updated',
]
@@ -1138,7 +1141,7 @@ class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
model = PowerFeed
fields = [
'id', 'url', 'display', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage',
- 'amperage', 'max_utilization', 'comments', 'mark_connected', 'cable', 'cable_end', 'link_peers',
- 'link_peers_type', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable',
- 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
+ 'amperage', 'max_utilization', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
+ 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'description',
+ 'comments', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
]
diff --git a/netbox/dcim/apps.py b/netbox/dcim/apps.py
index 4be2df659..bfb09e601 100644
--- a/netbox/dcim/apps.py
+++ b/netbox/dcim/apps.py
@@ -8,7 +8,7 @@ class DCIMConfig(AppConfig):
verbose_name = "DCIM"
def ready(self):
- import dcim.signals
+ from . import signals, search
from .models import CableTermination
# Register denormalized fields
diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py
index 7d35a40f9..8466d4861 100644
--- a/netbox/dcim/choices.py
+++ b/netbox/dcim/choices.py
@@ -1314,6 +1314,24 @@ class CableLengthUnitChoices(ChoiceSet):
)
+class WeightUnitChoices(ChoiceSet):
+
+ # Metric
+ UNIT_KILOGRAM = 'kg'
+ UNIT_GRAM = 'g'
+
+ # Imperial
+ UNIT_POUND = 'lb'
+ UNIT_OUNCE = 'oz'
+
+ CHOICES = (
+ (UNIT_KILOGRAM, 'Kilograms'),
+ (UNIT_GRAM, 'Grams'),
+ (UNIT_POUND, 'Pounds'),
+ (UNIT_OUNCE, 'Ounces'),
+ )
+
+
#
# CableTerminations
#
diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py
index 3a66e6c30..78afd816c 100644
--- a/netbox/dcim/filtersets.py
+++ b/netbox/dcim/filtersets.py
@@ -320,7 +320,7 @@ class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe
model = Rack
fields = [
'id', 'name', 'facility_id', 'asset_tag', 'u_height', 'desc_units', 'outer_width', 'outer_depth',
- 'outer_unit',
+ 'outer_unit', 'mounting_depth', 'weight', 'weight_unit'
]
def search(self, queryset, name, value):
@@ -482,7 +482,7 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
class Meta:
model = DeviceType
fields = [
- 'id', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
+ 'id', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit'
]
def search(self, queryset, name, value):
@@ -576,7 +576,7 @@ class ModuleTypeFilterSet(NetBoxModelFilterSet):
class Meta:
model = ModuleType
- fields = ['id', 'model', 'part_number']
+ fields = ['id', 'model', 'part_number', 'weight', 'weight_unit']
def search(self, queryset, name, value):
if not value.strip():
@@ -800,6 +800,12 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter
to_field_name='slug',
label='Manufacturer (slug)',
)
+ device_type = django_filters.ModelMultipleChoiceFilter(
+ field_name='device_type__slug',
+ queryset=DeviceType.objects.all(),
+ to_field_name='slug',
+ label='Device type (slug)',
+ )
device_type_id = django_filters.ModelMultipleChoiceFilter(
queryset=DeviceType.objects.all(),
label='Device type (ID)',
@@ -1360,7 +1366,7 @@ class InterfaceFilterSet(
try:
devices = Device.objects.filter(pk__in=id_list)
for device in devices:
- vc_interface_ids += device.vc_interfaces().values_list('id', flat=True)
+ vc_interface_ids += device.vc_interfaces(if_master=False).values_list('id', flat=True)
return queryset.filter(pk__in=vc_interface_ids)
except Device.DoesNotExist:
return queryset.none()
diff --git a/netbox/dcim/forms/__init__.py b/netbox/dcim/forms/__init__.py
index 22f0b1204..7510a979f 100644
--- a/netbox/dcim/forms/__init__.py
+++ b/netbox/dcim/forms/__init__.py
@@ -1,4 +1,4 @@
-from .models import *
+from .model_forms import *
from .filtersets import *
from .object_create import *
from .object_import import *
diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py
index 396f7e59b..1e58dd2f7 100644
--- a/netbox/dcim/forms/bulk_edit.py
+++ b/netbox/dcim/forms/bulk_edit.py
@@ -127,22 +127,26 @@ class SiteBulkEditForm(NetBoxModelBulkEditForm):
required=False,
label='Contact E-mail'
)
- description = forms.CharField(
- max_length=100,
- required=False
- )
time_zone = TimeZoneFormField(
choices=add_blank_choice(TimeZoneFormField().choices),
required=False,
widget=StaticSelect()
)
+ description = forms.CharField(
+ max_length=200,
+ required=False
+ )
+ comments = CommentField(
+ widget=SmallTextarea,
+ label='Comments'
+ )
model = Site
fieldsets = (
(None, ('status', 'region', 'group', 'tenant', 'asns', 'time_zone', 'description')),
)
nullable_fields = (
- 'region', 'group', 'tenant', 'asns', 'description', 'time_zone',
+ 'region', 'group', 'tenant', 'asns', 'time_zone', 'description', 'comments',
)
@@ -281,6 +285,24 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
required=False,
widget=StaticSelect()
)
+ mounting_depth = forms.IntegerField(
+ required=False,
+ min_value=1
+ )
+ weight = forms.DecimalField(
+ min_value=0,
+ required=False
+ )
+ weight_unit = forms.ChoiceField(
+ choices=add_blank_choice(WeightUnitChoices),
+ required=False,
+ initial='',
+ widget=StaticSelect()
+ )
+ description = forms.CharField(
+ max_length=200,
+ required=False
+ )
comments = CommentField(
widget=SmallTextarea,
label='Comments'
@@ -288,12 +310,16 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
model = Rack
fieldsets = (
- ('Rack', ('status', 'role', 'tenant', 'serial', 'asset_tag')),
+ ('Rack', ('status', 'role', 'tenant', 'serial', 'asset_tag', 'description')),
('Location', ('region', 'site_group', 'site', 'location')),
- ('Hardware', ('type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit')),
+ ('Hardware', (
+ 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth',
+ )),
+ ('Weight', ('weight', 'weight_unit')),
)
nullable_fields = (
- 'location', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_depth', 'outer_unit', 'comments',
+ 'location', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_depth', 'outer_unit', 'weight',
+ 'weight_unit', 'description', 'comments',
)
@@ -310,14 +336,19 @@ class RackReservationBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
description = forms.CharField(
- max_length=100,
+ max_length=200,
required=False
)
+ comments = CommentField(
+ widget=SmallTextarea,
+ label='Comments'
+ )
model = RackReservation
fieldsets = (
(None, ('user', 'tenant', 'description')),
)
+ nullable_fields = ('comments',)
class ManufacturerBulkEditForm(NetBoxModelBulkEditForm):
@@ -355,12 +386,31 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm):
required=False,
widget=StaticSelect()
)
+ weight = forms.DecimalField(
+ min_value=0,
+ required=False
+ )
+ weight_unit = forms.ChoiceField(
+ choices=add_blank_choice(WeightUnitChoices),
+ required=False,
+ initial='',
+ widget=StaticSelect()
+ )
+ description = forms.CharField(
+ max_length=200,
+ required=False
+ )
+ comments = CommentField(
+ widget=SmallTextarea,
+ label='Comments'
+ )
model = DeviceType
fieldsets = (
- (None, ('manufacturer', 'part_number', 'u_height', 'is_full_depth', 'airflow')),
+ ('Device Type', ('manufacturer', 'part_number', 'u_height', 'is_full_depth', 'airflow', 'description')),
+ ('Weight', ('weight', 'weight_unit')),
)
- nullable_fields = ('part_number', 'airflow')
+ nullable_fields = ('part_number', 'airflow', 'weight', 'weight_unit', 'description', 'comments')
class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm):
@@ -371,12 +421,31 @@ class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm):
part_number = forms.CharField(
required=False
)
+ weight = forms.DecimalField(
+ min_value=0,
+ required=False
+ )
+ weight_unit = forms.ChoiceField(
+ choices=add_blank_choice(WeightUnitChoices),
+ required=False,
+ initial='',
+ widget=StaticSelect()
+ )
+ description = forms.CharField(
+ max_length=200,
+ required=False
+ )
+ comments = CommentField(
+ widget=SmallTextarea,
+ label='Comments'
+ )
model = ModuleType
fieldsets = (
- (None, ('manufacturer', 'part_number')),
+ ('Module Type', ('manufacturer', 'part_number', 'description')),
+ ('Weight', ('weight', 'weight_unit')),
)
- nullable_fields = ('part_number',)
+ nullable_fields = ('part_number', 'weight', 'weight_unit', 'description', 'comments')
class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm):
@@ -472,15 +541,23 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm):
required=False,
label='Serial Number'
)
+ description = forms.CharField(
+ max_length=200,
+ required=False
+ )
+ comments = CommentField(
+ widget=SmallTextarea,
+ label='Comments'
+ )
model = Device
fieldsets = (
- ('Device', ('device_role', 'status', 'tenant', 'platform')),
+ ('Device', ('device_role', 'status', 'tenant', 'platform', 'description')),
('Location', ('site', 'location')),
('Hardware', ('manufacturer', 'device_type', 'airflow', 'serial')),
)
nullable_fields = (
- 'location', 'tenant', 'platform', 'serial', 'airflow',
+ 'location', 'tenant', 'platform', 'serial', 'airflow', 'description', 'comments',
)
@@ -501,12 +578,20 @@ class ModuleBulkEditForm(NetBoxModelBulkEditForm):
required=False,
label='Serial Number'
)
+ description = forms.CharField(
+ max_length=200,
+ required=False
+ )
+ comments = CommentField(
+ widget=SmallTextarea,
+ label='Comments'
+ )
model = Module
fieldsets = (
- (None, ('manufacturer', 'module_type', 'serial')),
+ (None, ('manufacturer', 'module_type', 'serial', 'description')),
)
- nullable_fields = ('serial',)
+ nullable_fields = ('serial', 'description', 'comments')
class CableBulkEditForm(NetBoxModelBulkEditForm):
@@ -543,39 +628,44 @@ class CableBulkEditForm(NetBoxModelBulkEditForm):
initial='',
widget=StaticSelect()
)
+ description = forms.CharField(
+ max_length=200,
+ required=False
+ )
+ comments = CommentField(
+ widget=SmallTextarea,
+ label='Comments'
+ )
model = Cable
fieldsets = (
- (None, ('type', 'status', 'tenant', 'label')),
+ (None, ('type', 'status', 'tenant', 'label', 'description')),
('Attributes', ('color', 'length', 'length_unit')),
)
nullable_fields = (
- 'type', 'status', 'tenant', 'label', 'color', 'length',
+ 'type', 'status', 'tenant', 'label', 'color', 'length', 'description', 'comments',
)
- def clean(self):
- super().clean()
-
- # Validate length/unit
- length = self.cleaned_data.get('length')
- length_unit = self.cleaned_data.get('length_unit')
- if length and not length_unit:
- raise forms.ValidationError({
- 'length_unit': "Must specify a unit when setting length"
- })
-
class VirtualChassisBulkEditForm(NetBoxModelBulkEditForm):
domain = forms.CharField(
max_length=30,
required=False
)
+ description = forms.CharField(
+ max_length=200,
+ required=False
+ )
+ comments = CommentField(
+ widget=SmallTextarea,
+ label='Comments'
+ )
model = VirtualChassis
fieldsets = (
- (None, ('domain',)),
+ (None, ('domain', 'description')),
)
- nullable_fields = ('domain',)
+ nullable_fields = ('domain', 'description', 'comments')
class PowerPanelBulkEditForm(NetBoxModelBulkEditForm):
@@ -608,12 +698,20 @@ class PowerPanelBulkEditForm(NetBoxModelBulkEditForm):
'site_id': '$site'
}
)
+ description = forms.CharField(
+ max_length=200,
+ required=False
+ )
+ comments = CommentField(
+ widget=SmallTextarea,
+ label='Comments'
+ )
model = PowerPanel
fieldsets = (
- (None, ('region', 'site_group', 'site', 'location')),
+ (None, ('region', 'site_group', 'site', 'location', 'description')),
)
- nullable_fields = ('location',)
+ nullable_fields = ('location', 'description', 'comments')
class PowerFeedBulkEditForm(NetBoxModelBulkEditForm):
@@ -662,6 +760,10 @@ class PowerFeedBulkEditForm(NetBoxModelBulkEditForm):
required=False,
widget=BulkEditNullBooleanSelect
)
+ description = forms.CharField(
+ max_length=200,
+ required=False
+ )
comments = CommentField(
widget=SmallTextarea,
label='Comments'
@@ -669,10 +771,10 @@ class PowerFeedBulkEditForm(NetBoxModelBulkEditForm):
model = PowerFeed
fieldsets = (
- (None, ('power_panel', 'rack', 'status', 'type', 'mark_connected')),
+ (None, ('power_panel', 'rack', 'status', 'type', 'mark_connected', 'description')),
('Power', ('supply', 'phase', 'voltage', 'amperage', 'max_utilization'))
)
- nullable_fields = ('location', 'comments')
+ nullable_fields = ('location', 'description', 'comments')
#
diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py
index f0fd9bf86..2b77ef5a9 100644
--- a/netbox/dcim/forms/bulk_import.py
+++ b/netbox/dcim/forms/bulk_import.py
@@ -56,7 +56,7 @@ class RegionCSVForm(NetBoxModelCSVForm):
class Meta:
model = Region
- fields = ('name', 'slug', 'parent', 'description')
+ fields = ('name', 'slug', 'parent', 'description', 'tags')
class SiteGroupCSVForm(NetBoxModelCSVForm):
@@ -100,7 +100,7 @@ class SiteCSVForm(NetBoxModelCSVForm):
model = Site
fields = (
'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'time_zone', 'description',
- 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments',
+ 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'tags'
)
help_texts = {
'time_zone': mark_safe(
@@ -137,7 +137,7 @@ class LocationCSVForm(NetBoxModelCSVForm):
class Meta:
model = Location
- fields = ('site', 'parent', 'name', 'slug', 'status', 'tenant', 'description')
+ fields = ('site', 'parent', 'name', 'slug', 'status', 'tenant', 'description', 'tags')
class RackRoleCSVForm(NetBoxModelCSVForm):
@@ -145,7 +145,7 @@ class RackRoleCSVForm(NetBoxModelCSVForm):
class Meta:
model = RackRole
- fields = ('name', 'slug', 'color', 'description')
+ fields = ('name', 'slug', 'color', 'description', 'tags')
help_texts = {
'color': mark_safe('RGB color in hexadecimal (e.g. 00ff00
)'),
}
@@ -196,7 +196,8 @@ class RackCSVForm(NetBoxModelCSVForm):
model = Rack
fields = (
'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag',
- 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments',
+ 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth',
+ 'description', 'comments', 'tags',
)
def __init__(self, data=None, *args, **kwargs):
@@ -240,7 +241,7 @@ class RackReservationCSVForm(NetBoxModelCSVForm):
class Meta:
model = RackReservation
- fields = ('site', 'location', 'rack', 'units', 'tenant', 'description')
+ fields = ('site', 'location', 'rack', 'units', 'tenant', 'description', 'comments', 'tags')
def __init__(self, data=None, *args, **kwargs):
super().__init__(data, *args, **kwargs)
@@ -263,7 +264,7 @@ class ManufacturerCSVForm(NetBoxModelCSVForm):
class Meta:
model = Manufacturer
- fields = ('name', 'slug', 'description')
+ fields = ('name', 'slug', 'description', 'tags')
class DeviceRoleCSVForm(NetBoxModelCSVForm):
@@ -271,7 +272,7 @@ class DeviceRoleCSVForm(NetBoxModelCSVForm):
class Meta:
model = DeviceRole
- fields = ('name', 'slug', 'color', 'vm_role', 'description')
+ fields = ('name', 'slug', 'color', 'vm_role', 'description', 'tags')
help_texts = {
'color': mark_safe('RGB color in hexadecimal (e.g. 00ff00
)'),
}
@@ -288,7 +289,7 @@ class PlatformCSVForm(NetBoxModelCSVForm):
class Meta:
model = Platform
- fields = ('name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description')
+ fields = ('name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'tags')
class BaseDeviceCSVForm(NetBoxModelCSVForm):
@@ -387,7 +388,7 @@ class DeviceCSVForm(BaseDeviceCSVForm):
fields = [
'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
'site', 'location', 'rack', 'position', 'face', 'airflow', 'virtual_chassis', 'vc_position', 'vc_priority',
- 'cluster', 'comments',
+ 'cluster', 'description', 'comments', 'tags',
]
def __init__(self, data=None, *args, **kwargs):
@@ -424,7 +425,7 @@ class ModuleCSVForm(NetBoxModelCSVForm):
class Meta:
model = Module
fields = (
- 'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'comments',
+ 'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'description', 'comments', 'tags',
)
def __init__(self, data=None, *args, **kwargs):
@@ -451,7 +452,7 @@ class ChildDeviceCSVForm(BaseDeviceCSVForm):
class Meta(BaseDeviceCSVForm.Meta):
fields = [
'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
- 'parent', 'device_bay', 'virtual_chassis', 'vc_position', 'vc_priority', 'cluster', 'comments',
+ 'parent', 'device_bay', 'virtual_chassis', 'vc_position', 'vc_priority', 'cluster', 'comments', 'tags'
]
def __init__(self, data=None, *args, **kwargs):
@@ -502,7 +503,7 @@ class ConsolePortCSVForm(NetBoxModelCSVForm):
class Meta:
model = ConsolePort
- fields = ('device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description')
+ fields = ('device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags')
class ConsoleServerPortCSVForm(NetBoxModelCSVForm):
@@ -525,7 +526,7 @@ class ConsoleServerPortCSVForm(NetBoxModelCSVForm):
class Meta:
model = ConsoleServerPort
- fields = ('device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description')
+ fields = ('device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags')
class PowerPortCSVForm(NetBoxModelCSVForm):
@@ -542,7 +543,7 @@ class PowerPortCSVForm(NetBoxModelCSVForm):
class Meta:
model = PowerPort
fields = (
- 'device', 'name', 'label', 'type', 'mark_connected', 'maximum_draw', 'allocated_draw', 'description',
+ 'device', 'name', 'label', 'type', 'mark_connected', 'maximum_draw', 'allocated_draw', 'description', 'tags'
)
@@ -570,13 +571,13 @@ class PowerOutletCSVForm(NetBoxModelCSVForm):
class Meta:
model = PowerOutlet
- fields = ('device', 'name', 'label', 'type', 'mark_connected', 'power_port', 'feed_leg', 'description')
+ fields = ('device', 'name', 'label', 'type', 'mark_connected', 'power_port', 'feed_leg', 'description', 'tags')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit PowerPort choices to those belonging to this device (or VC master)
- if self.is_bound:
+ if self.is_bound and 'device' in self.data:
try:
device = self.fields['device'].to_python(self.data['device'])
except forms.ValidationError:
@@ -658,7 +659,7 @@ class InterfaceCSVForm(NetBoxModelCSVForm):
fields = (
'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'speed', 'duplex', 'enabled',
'mark_connected', 'mac_address', 'wwn', 'mtu', 'mgmt_only', 'description', 'poe_mode', 'poe_type', 'mode',
- 'vrf', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power',
+ 'vrf', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'tags'
)
def __init__(self, data=None, *args, **kwargs):
@@ -701,7 +702,7 @@ class FrontPortCSVForm(NetBoxModelCSVForm):
model = FrontPort
fields = (
'device', 'name', 'label', 'type', 'color', 'mark_connected', 'rear_port', 'rear_port_position',
- 'description',
+ 'description', 'tags'
)
help_texts = {
'rear_port_position': 'Mapped position on corresponding rear port',
@@ -711,7 +712,7 @@ class FrontPortCSVForm(NetBoxModelCSVForm):
super().__init__(*args, **kwargs)
# Limit RearPort choices to those belonging to this device (or VC master)
- if self.is_bound:
+ if self.is_bound and 'device' in self.data:
try:
device = self.fields['device'].to_python(self.data['device'])
except forms.ValidationError:
@@ -742,7 +743,7 @@ class RearPortCSVForm(NetBoxModelCSVForm):
class Meta:
model = RearPort
- fields = ('device', 'name', 'label', 'type', 'color', 'mark_connected', 'positions', 'description')
+ fields = ('device', 'name', 'label', 'type', 'color', 'mark_connected', 'positions', 'description', 'tags')
help_texts = {
'positions': 'Number of front ports which may be mapped'
}
@@ -756,7 +757,7 @@ class ModuleBayCSVForm(NetBoxModelCSVForm):
class Meta:
model = ModuleBay
- fields = ('device', 'name', 'label', 'position', 'description')
+ fields = ('device', 'name', 'label', 'position', 'description', 'tags')
class DeviceBayCSVForm(NetBoxModelCSVForm):
@@ -776,13 +777,13 @@ class DeviceBayCSVForm(NetBoxModelCSVForm):
class Meta:
model = DeviceBay
- fields = ('device', 'name', 'label', 'installed_device', 'description')
+ fields = ('device', 'name', 'label', 'installed_device', 'description', 'tags')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit installed device choices to devices of the correct type and location
- if self.is_bound:
+ if self.is_bound and 'device' in self.data:
try:
device = self.fields['device'].to_python(self.data['device'])
except forms.ValidationError:
@@ -831,7 +832,7 @@ class InventoryItemCSVForm(NetBoxModelCSVForm):
model = InventoryItem
fields = (
'device', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
- 'description',
+ 'description', 'tags'
)
def __init__(self, *args, **kwargs):
@@ -927,7 +928,7 @@ class CableCSVForm(NetBoxModelCSVForm):
model = Cable
fields = [
'side_a_device', 'side_a_type', 'side_a_name', 'side_b_device', 'side_b_type', 'side_b_name', 'type',
- 'status', 'tenant', 'label', 'color', 'length', 'length_unit',
+ 'status', 'tenant', 'label', 'color', 'length', 'length_unit', 'description', 'comments', 'tags',
]
help_texts = {
'color': mark_safe('RGB color in hexadecimal (e.g. 00ff00
)'),
@@ -984,7 +985,7 @@ class VirtualChassisCSVForm(NetBoxModelCSVForm):
class Meta:
model = VirtualChassis
- fields = ('name', 'domain', 'master')
+ fields = ('name', 'domain', 'master', 'description', 'comments', 'tags')
#
@@ -1005,7 +1006,7 @@ class PowerPanelCSVForm(NetBoxModelCSVForm):
class Meta:
model = PowerPanel
- fields = ('site', 'location', 'name')
+ fields = ('site', 'location', 'name', 'description', 'comments', 'tags')
def __init__(self, data=None, *args, **kwargs):
super().__init__(data, *args, **kwargs)
@@ -1061,7 +1062,7 @@ class PowerFeedCSVForm(NetBoxModelCSVForm):
model = PowerFeed
fields = (
'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase',
- 'voltage', 'amperage', 'max_utilization', 'comments',
+ 'voltage', 'amperage', 'max_utilization', 'description', 'comments', 'tags',
)
def __init__(self, data=None, *args, **kwargs):
diff --git a/netbox/dcim/forms/connections.py b/netbox/dcim/forms/connections.py
index cc5cf362f..537a89bad 100644
--- a/netbox/dcim/forms/connections.py
+++ b/netbox/dcim/forms/connections.py
@@ -3,7 +3,7 @@ from django import forms
from circuits.models import Circuit, CircuitTermination, Provider
from dcim.models import *
from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField
-from .models import CableForm
+from .model_forms import CableForm
def get_cable_form(a_type, b_type):
@@ -108,7 +108,7 @@ def get_cable_form(a_type, b_type):
label='Power Feed',
disabled_indicator='_occupied',
query_params={
- 'powerpanel_id': f'$termination_{cable_end}_powerpanel',
+ 'power_panel_id': f'$termination_{cable_end}_powerpanel',
}
)
diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py
index 96b0d1319..905a898df 100644
--- a/netbox/dcim/forms/filtersets.py
+++ b/netbox/dcim/forms/filtersets.py
@@ -116,7 +116,7 @@ class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Region
fieldsets = (
- (None, ('q', 'tag', 'parent_id')),
+ (None, ('q', 'filter', 'tag', 'parent_id')),
('Contacts', ('contact', 'contact_role', 'contact_group'))
)
parent_id = DynamicModelMultipleChoiceField(
@@ -130,7 +130,7 @@ class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = SiteGroup
fieldsets = (
- (None, ('q', 'tag', 'parent_id')),
+ (None, ('q', 'filter', 'tag', 'parent_id')),
('Contacts', ('contact', 'contact_role', 'contact_group'))
)
parent_id = DynamicModelMultipleChoiceField(
@@ -144,7 +144,7 @@ class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Site
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Attributes', ('status', 'region_id', 'group_id', 'asn_id')),
('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role', 'contact_group')),
@@ -174,7 +174,7 @@ class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Location
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Attributes', ('region_id', 'site_group_id', 'site_id', 'parent_id', 'status')),
('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role', 'contact_group')),
@@ -222,12 +222,13 @@ class RackRoleFilterForm(NetBoxModelFilterSetForm):
class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Rack
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')),
('Function', ('status', 'role_id')),
('Hardware', ('type', 'width', 'serial', 'asset_tag')),
('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role', 'contact_group')),
+ ('Weight', ('weight', 'weight_unit')),
)
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
@@ -281,6 +282,13 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
required=False
)
tag = TagFilterField(model)
+ weight = forms.DecimalField(
+ required=False
+ )
+ weight_unit = forms.ChoiceField(
+ choices=add_blank_choice(WeightUnitChoices),
+ required=False
+ )
class RackElevationFilterForm(RackFilterForm):
@@ -298,7 +306,7 @@ class RackElevationFilterForm(RackFilterForm):
class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = RackReservation
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('User', ('user_id',)),
('Rack', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
('Tenant', ('tenant_group_id', 'tenant_id')),
@@ -354,7 +362,7 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class ManufacturerFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Manufacturer
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Contacts', ('contact', 'contact_role', 'contact_group'))
)
tag = TagFilterField(model)
@@ -363,13 +371,14 @@ class ManufacturerFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
model = DeviceType
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Hardware', ('manufacturer_id', 'part_number', 'subdevice_role', 'airflow')),
('Images', ('has_front_image', 'has_rear_image')),
('Components', (
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
'pass_through_ports', 'device_bays', 'module_bays', 'inventory_items',
)),
+ ('Weight', ('weight', 'weight_unit')),
)
manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(),
@@ -465,17 +474,25 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
)
)
tag = TagFilterField(model)
+ weight = forms.DecimalField(
+ required=False
+ )
+ weight_unit = forms.ChoiceField(
+ choices=add_blank_choice(WeightUnitChoices),
+ required=False
+ )
class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
model = ModuleType
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Hardware', ('manufacturer_id', 'part_number')),
('Components', (
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
'pass_through_ports',
)),
+ ('Weight', ('weight', 'weight_unit')),
)
manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(),
@@ -529,6 +546,13 @@ class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
)
)
tag = TagFilterField(model)
+ weight = forms.DecimalField(
+ required=False
+ )
+ weight_unit = forms.ChoiceField(
+ choices=add_blank_choice(WeightUnitChoices),
+ required=False
+ )
class DeviceRoleFilterForm(NetBoxModelFilterSetForm):
@@ -554,7 +578,7 @@ class DeviceFilterForm(
):
model = Device
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
('Operation', ('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address')),
('Hardware', ('manufacturer_id', 'device_type_id', 'platform_id')),
@@ -707,7 +731,7 @@ class DeviceFilterForm(
class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm):
model = Module
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Hardware', ('manufacturer_id', 'module_type_id', 'serial', 'asset_tag')),
)
manufacturer_id = DynamicModelMultipleChoiceField(
@@ -737,7 +761,7 @@ class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxMo
class VirtualChassisFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = VirtualChassis
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id')),
('Tenant', ('tenant_group_id', 'tenant_id')),
)
@@ -766,7 +790,7 @@ class VirtualChassisFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = Cable
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Location', ('site_id', 'location_id', 'rack_id', 'device_id')),
('Attributes', ('type', 'status', 'color', 'length', 'length_unit')),
('Tenant', ('tenant_group_id', 'tenant_id')),
@@ -838,7 +862,7 @@ class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = PowerPanel
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')),
('Contacts', ('contact', 'contact_role', 'contact_group')),
)
@@ -876,7 +900,7 @@ class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
class PowerFeedFilterForm(NetBoxModelFilterSetForm):
model = PowerFeed
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id', 'power_panel_id', 'rack_id')),
('Attributes', ('status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization')),
)
@@ -978,7 +1002,7 @@ class PathEndpointFilterForm(CabledFilterForm):
class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = ConsolePort
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Attributes', ('name', 'label', 'type', 'speed')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
('Connection', ('cabled', 'connected', 'occupied')),
@@ -997,7 +1021,7 @@ class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = ConsoleServerPort
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Attributes', ('name', 'label', 'type', 'speed')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
('Connection', ('cabled', 'connected', 'occupied')),
@@ -1016,7 +1040,7 @@ class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterF
class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = PowerPort
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Attributes', ('name', 'label', 'type')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
('Connection', ('cabled', 'connected', 'occupied')),
@@ -1031,7 +1055,7 @@ class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = PowerOutlet
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Attributes', ('name', 'label', 'type')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
('Connection', ('cabled', 'connected', 'occupied')),
@@ -1046,7 +1070,7 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = Interface
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Attributes', ('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only')),
('Addressing', ('vrf_id', 'mac_address', 'wwn')),
('PoE', ('poe_mode', 'poe_type')),
@@ -1135,7 +1159,7 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Attributes', ('name', 'label', 'type', 'color')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
('Cable', ('cabled', 'occupied')),
@@ -1154,7 +1178,7 @@ class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
model = RearPort
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Attributes', ('name', 'label', 'type', 'color')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
('Cable', ('cabled', 'occupied')),
@@ -1172,7 +1196,7 @@ class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
class ModuleBayFilterForm(DeviceComponentFilterForm):
model = ModuleBay
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Attributes', ('name', 'label', 'position')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
)
@@ -1185,7 +1209,7 @@ class ModuleBayFilterForm(DeviceComponentFilterForm):
class DeviceBayFilterForm(DeviceComponentFilterForm):
model = DeviceBay
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Attributes', ('name', 'label')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
)
@@ -1195,7 +1219,7 @@ class DeviceBayFilterForm(DeviceComponentFilterForm):
class InventoryItemFilterForm(DeviceComponentFilterForm):
model = InventoryItem
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Attributes', ('name', 'label', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
)
diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/model_forms.py
similarity index 95%
rename from netbox/dcim/forms/models.py
rename to netbox/dcim/forms/model_forms.py
index b33023ece..539c48709 100644
--- a/netbox/dcim/forms/models.py
+++ b/netbox/dcim/forms/model_forms.py
@@ -78,6 +78,12 @@ class RegionForm(NetBoxModelForm):
)
slug = SlugField()
+ fieldsets = (
+ ('Region', (
+ 'parent', 'name', 'slug', 'description', 'tags',
+ )),
+ )
+
class Meta:
model = Region
fields = (
@@ -92,6 +98,12 @@ class SiteGroupForm(NetBoxModelForm):
)
slug = SlugField()
+ fieldsets = (
+ ('Site Group', (
+ 'parent', 'name', 'slug', 'description', 'tags',
+ )),
+ )
+
class Meta:
model = SiteGroup
fields = (
@@ -213,6 +225,12 @@ class LocationForm(TenancyForm, NetBoxModelForm):
class RackRoleForm(NetBoxModelForm):
slug = SlugField()
+ fieldsets = (
+ ('Rack Role', (
+ 'name', 'slug', 'color', 'description', 'tags',
+ )),
+ )
+
class Meta:
model = RackRole
fields = [
@@ -260,7 +278,7 @@ class RackForm(TenancyForm, NetBoxModelForm):
fields = [
'region', 'site_group', 'site', 'location', 'name', 'facility_id', 'tenant_group', 'tenant', 'status',
'role', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth',
- 'outer_unit', 'comments', 'tags',
+ 'outer_unit', 'mounting_depth', 'weight', 'weight_unit', 'description', 'comments', 'tags',
]
help_texts = {
'site': "The site at which the rack exists",
@@ -273,6 +291,7 @@ class RackForm(TenancyForm, NetBoxModelForm):
'type': StaticSelect(),
'width': StaticSelect(),
'outer_unit': StaticSelect(),
+ 'weight_unit': StaticSelect(),
}
@@ -323,6 +342,7 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
),
widget=StaticSelect()
)
+ comments = CommentField()
fieldsets = (
('Reservation', ('region', 'site_group', 'site', 'location', 'rack', 'units', 'user', 'description', 'tags')),
@@ -333,13 +353,19 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
model = RackReservation
fields = [
'region', 'site_group', 'site', 'location', 'rack', 'units', 'user', 'tenant_group', 'tenant',
- 'description', 'tags',
+ 'description', 'comments', 'tags',
]
class ManufacturerForm(NetBoxModelForm):
slug = SlugField()
+ fieldsets = (
+ ('Manufacturer', (
+ 'name', 'slug', 'description', 'tags',
+ )),
+ )
+
class Meta:
model = Manufacturer
fields = [
@@ -358,11 +384,12 @@ class DeviceTypeForm(NetBoxModelForm):
fieldsets = (
('Device Type', (
- 'manufacturer', 'model', 'slug', 'part_number', 'tags',
+ 'manufacturer', 'model', 'slug', 'description', 'tags',
)),
('Chassis', (
- 'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
+ 'u_height', 'is_full_depth', 'part_number', 'subdevice_role', 'airflow',
)),
+ ('Attributes', ('weight', 'weight_unit')),
('Images', ('front_image', 'rear_image')),
)
@@ -370,7 +397,7 @@ class DeviceTypeForm(NetBoxModelForm):
model = DeviceType
fields = [
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
- 'front_image', 'rear_image', 'comments', 'tags',
+ 'weight', 'weight_unit', 'front_image', 'rear_image', 'description', 'comments', 'tags',
]
widgets = {
'airflow': StaticSelect(),
@@ -380,7 +407,8 @@ class DeviceTypeForm(NetBoxModelForm):
}),
'rear_image': ClearableFileInput(attrs={
'accept': DEVICETYPE_IMAGE_FORMATS
- })
+ }),
+ 'weight_unit': StaticSelect(),
}
@@ -391,21 +419,30 @@ class ModuleTypeForm(NetBoxModelForm):
comments = CommentField()
fieldsets = (
- ('Module Type', (
- 'manufacturer', 'model', 'part_number', 'tags',
- )),
+ ('Module Type', ('manufacturer', 'model', 'part_number', 'description', 'tags')),
+ ('Weight', ('weight', 'weight_unit'))
)
class Meta:
model = ModuleType
fields = [
- 'manufacturer', 'model', 'part_number', 'comments', 'tags',
+ 'manufacturer', 'model', 'part_number', 'weight', 'weight_unit', 'description', 'comments', 'tags',
]
+ widgets = {
+ 'weight_unit': StaticSelect(),
+ }
+
class DeviceRoleForm(NetBoxModelForm):
slug = SlugField()
+ fieldsets = (
+ ('Device Role', (
+ 'name', 'slug', 'color', 'vm_role', 'description', 'tags',
+ )),
+ )
+
class Meta:
model = DeviceRole
fields = [
@@ -422,6 +459,13 @@ class PlatformForm(NetBoxModelForm):
max_length=64
)
+ fieldsets = (
+ ('Platform', (
+ 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'tags',
+
+ )),
+ )
+
class Meta:
model = Platform
fields = [
@@ -547,7 +591,7 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'rack',
'location', 'position', 'face', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6',
'cluster_group', 'cluster', 'tenant_group', 'tenant', 'virtual_chassis', 'vc_position', 'vc_priority',
- 'comments', 'tags', 'local_context_data'
+ 'description', 'comments', 'tags', 'local_context_data'
]
help_texts = {
'device_role': "The function this device serves",
@@ -661,7 +705,7 @@ class ModuleForm(NetBoxModelForm):
fieldsets = (
('Module', (
- 'device', 'module_bay', 'manufacturer', 'module_type', 'tags',
+ 'device', 'module_bay', 'manufacturer', 'module_type', 'description', 'tags',
)),
('Hardware', (
'serial', 'asset_tag', 'replicate_components', 'adopt_components',
@@ -672,13 +716,14 @@ class ModuleForm(NetBoxModelForm):
model = Module
fields = [
'device', 'module_bay', 'manufacturer', 'module_type', 'serial', 'asset_tag', 'tags',
- 'replicate_components', 'adopt_components', 'comments',
+ 'replicate_components', 'adopt_components', 'description', 'comments',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance.pk:
+ self.fields['device'].disabled = True
self.fields['replicate_components'].initial = False
self.fields['replicate_components'].disabled = True
self.fields['adopt_components'].initial = False
@@ -748,11 +793,13 @@ class ModuleForm(NetBoxModelForm):
class CableForm(TenancyForm, NetBoxModelForm):
+ comments = CommentField()
class Meta:
model = Cable
fields = [
- 'type', 'status', 'tenant_group', 'tenant', 'label', 'color', 'length', 'length_unit', 'tags',
+ 'type', 'status', 'tenant_group', 'tenant', 'label', 'color', 'length', 'length_unit', 'description',
+ 'comments', 'tags',
]
widgets = {
'status': StaticSelect,
@@ -795,15 +842,16 @@ class PowerPanelForm(NetBoxModelForm):
'site_id': '$site'
}
)
+ comments = CommentField()
fieldsets = (
- ('Power Panel', ('region', 'site_group', 'site', 'location', 'name', 'tags')),
+ ('Power Panel', ('region', 'site_group', 'site', 'location', 'name', 'description', 'tags')),
)
class Meta:
model = PowerPanel
fields = [
- 'region', 'site_group', 'site', 'location', 'name', 'tags',
+ 'region', 'site_group', 'site', 'location', 'name', 'description', 'comments', 'tags',
]
@@ -849,7 +897,7 @@ class PowerFeedForm(NetBoxModelForm):
comments = CommentField()
fieldsets = (
- ('Power Panel', ('region', 'site', 'power_panel')),
+ ('Power Panel', ('region', 'site', 'power_panel', 'description')),
('Power Feed', ('rack', 'name', 'status', 'type', 'mark_connected', 'tags')),
('Characteristics', ('supply', 'voltage', 'amperage', 'phase', 'max_utilization')),
)
@@ -858,7 +906,7 @@ class PowerFeedForm(NetBoxModelForm):
model = PowerFeed
fields = [
'region', 'site_group', 'site', 'power_panel', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply',
- 'phase', 'voltage', 'amperage', 'max_utilization', 'comments', 'tags',
+ 'phase', 'voltage', 'amperage', 'max_utilization', 'description', 'comments', 'tags',
]
widgets = {
'status': StaticSelect(),
@@ -877,11 +925,12 @@ class VirtualChassisForm(NetBoxModelForm):
queryset=Device.objects.all(),
required=False,
)
+ comments = CommentField()
class Meta:
model = VirtualChassis
fields = [
- 'name', 'domain', 'master', 'tags',
+ 'name', 'domain', 'master', 'description', 'comments', 'tags',
]
widgets = {
'master': SelectWithPK(),
@@ -1576,6 +1625,12 @@ class InventoryItemForm(DeviceComponentForm):
class InventoryItemRoleForm(NetBoxModelForm):
slug = SlugField()
+ fieldsets = (
+ ('Inventory Item Role', (
+ 'name', 'slug', 'color', 'description', 'tags',
+ )),
+ )
+
class Meta:
model = InventoryItemRole
fields = [
diff --git a/netbox/dcim/forms/object_create.py b/netbox/dcim/forms/object_create.py
index a03597db1..afdaa4fcc 100644
--- a/netbox/dcim/forms/object_create.py
+++ b/netbox/dcim/forms/object_create.py
@@ -3,7 +3,7 @@ from django import forms
from dcim.models import *
from netbox.forms import NetBoxModelForm
from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField
-from . import models as model_forms
+from . import model_forms
__all__ = (
'ComponentCreateForm',
diff --git a/netbox/dcim/forms/object_import.py b/netbox/dcim/forms/object_import.py
index 023aba8f1..82ee093dd 100644
--- a/netbox/dcim/forms/object_import.py
+++ b/netbox/dcim/forms/object_import.py
@@ -30,7 +30,7 @@ class DeviceTypeImportForm(BootstrapMixin, forms.ModelForm):
model = DeviceType
fields = [
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
- 'comments',
+ 'description', 'comments',
]
@@ -42,7 +42,7 @@ class ModuleTypeImportForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = ModuleType
- fields = ['manufacturer', 'model', 'part_number', 'comments']
+ fields = ['manufacturer', 'model', 'part_number', 'description', 'comments']
#
diff --git a/netbox/dcim/graphql/gfk_mixins.py b/netbox/dcim/graphql/gfk_mixins.py
new file mode 100644
index 000000000..f3b8b696b
--- /dev/null
+++ b/netbox/dcim/graphql/gfk_mixins.py
@@ -0,0 +1,169 @@
+import graphene
+from circuits.graphql.types import CircuitTerminationType
+from circuits.models import CircuitTermination
+from dcim.graphql.types import (
+ ConsolePortTemplateType,
+ ConsolePortType,
+ ConsoleServerPortTemplateType,
+ ConsoleServerPortType,
+ FrontPortTemplateType,
+ FrontPortType,
+ InterfaceTemplateType,
+ InterfaceType,
+ PowerFeedType,
+ PowerOutletTemplateType,
+ PowerOutletType,
+ PowerPortTemplateType,
+ PowerPortType,
+ RearPortTemplateType,
+ RearPortType,
+)
+from dcim.models import (
+ ConsolePort,
+ ConsolePortTemplate,
+ ConsoleServerPort,
+ ConsoleServerPortTemplate,
+ FrontPort,
+ FrontPortTemplate,
+ Interface,
+ InterfaceTemplate,
+ PowerFeed,
+ PowerOutlet,
+ PowerOutletTemplate,
+ PowerPort,
+ PowerPortTemplate,
+ RearPort,
+ RearPortTemplate,
+)
+
+
+class LinkPeerType(graphene.Union):
+ class Meta:
+ types = (
+ CircuitTerminationType,
+ ConsolePortType,
+ ConsoleServerPortType,
+ FrontPortType,
+ InterfaceType,
+ PowerFeedType,
+ PowerOutletType,
+ PowerPortType,
+ RearPortType,
+ )
+
+ @classmethod
+ def resolve_type(cls, instance, info):
+ if type(instance) == CircuitTermination:
+ return CircuitTerminationType
+ if type(instance) == ConsolePortType:
+ return ConsolePortType
+ if type(instance) == ConsoleServerPort:
+ return ConsoleServerPortType
+ if type(instance) == FrontPort:
+ return FrontPortType
+ if type(instance) == Interface:
+ return InterfaceType
+ if type(instance) == PowerFeed:
+ return PowerFeedType
+ if type(instance) == PowerOutlet:
+ return PowerOutletType
+ if type(instance) == PowerPort:
+ return PowerPortType
+ if type(instance) == RearPort:
+ return RearPortType
+
+
+class CableTerminationTerminationType(graphene.Union):
+ class Meta:
+ types = (
+ CircuitTerminationType,
+ ConsolePortType,
+ ConsoleServerPortType,
+ FrontPortType,
+ InterfaceType,
+ PowerFeedType,
+ PowerOutletType,
+ PowerPortType,
+ RearPortType,
+ )
+
+ @classmethod
+ def resolve_type(cls, instance, info):
+ if type(instance) == CircuitTermination:
+ return CircuitTerminationType
+ if type(instance) == ConsolePortType:
+ return ConsolePortType
+ if type(instance) == ConsoleServerPort:
+ return ConsoleServerPortType
+ if type(instance) == FrontPort:
+ return FrontPortType
+ if type(instance) == Interface:
+ return InterfaceType
+ if type(instance) == PowerFeed:
+ return PowerFeedType
+ if type(instance) == PowerOutlet:
+ return PowerOutletType
+ if type(instance) == PowerPort:
+ return PowerPortType
+ if type(instance) == RearPort:
+ return RearPortType
+
+
+class InventoryItemTemplateComponentType(graphene.Union):
+ class Meta:
+ types = (
+ ConsolePortTemplateType,
+ ConsoleServerPortTemplateType,
+ FrontPortTemplateType,
+ InterfaceTemplateType,
+ PowerOutletTemplateType,
+ PowerPortTemplateType,
+ RearPortTemplateType,
+ )
+
+ @classmethod
+ def resolve_type(cls, instance, info):
+ if type(instance) == ConsolePortTemplate:
+ return ConsolePortTemplateType
+ if type(instance) == ConsoleServerPortTemplate:
+ return ConsoleServerPortTemplateType
+ if type(instance) == FrontPortTemplate:
+ return FrontPortTemplateType
+ if type(instance) == InterfaceTemplate:
+ return InterfaceTemplateType
+ if type(instance) == PowerOutletTemplate:
+ return PowerOutletTemplateType
+ if type(instance) == PowerPortTemplate:
+ return PowerPortTemplateType
+ if type(instance) == RearPortTemplate:
+ return RearPortTemplateType
+
+
+class InventoryItemComponentType(graphene.Union):
+ class Meta:
+ types = (
+ ConsolePortType,
+ ConsoleServerPortType,
+ FrontPortType,
+ InterfaceType,
+ PowerOutletType,
+ PowerPortType,
+ RearPortType,
+ )
+
+ @classmethod
+ def resolve_type(cls, instance, info):
+ if type(instance) == ConsolePort:
+ return ConsolePortType
+ if type(instance) == ConsoleServerPort:
+ return ConsoleServerPortType
+ if type(instance) == FrontPort:
+ return FrontPortType
+ if type(instance) == Interface:
+ return InterfaceType
+ if type(instance) == PowerOutlet:
+ return PowerOutletType
+ if type(instance) == PowerPort:
+ return PowerPortType
+ if type(instance) == RearPort:
+ return RearPortType
diff --git a/netbox/dcim/graphql/mixins.py b/netbox/dcim/graphql/mixins.py
index d8488aa5f..133d6259f 100644
--- a/netbox/dcim/graphql/mixins.py
+++ b/netbox/dcim/graphql/mixins.py
@@ -1,5 +1,12 @@
+import graphene
+
+
class CabledObjectMixin:
+ link_peers = graphene.List('dcim.graphql.gfk_mixins.LinkPeerType')
def resolve_cable_end(self, info):
# Handle empty values
return self.cable_end or None
+
+ def resolve_link_peers(self, info):
+ return self.link_peers
diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py
index 52a98278a..bb414ed00 100644
--- a/netbox/dcim/graphql/types.py
+++ b/netbox/dcim/graphql/types.py
@@ -2,7 +2,7 @@ import graphene
from dcim import filtersets, models
from extras.graphql.mixins import (
- ChangelogMixin, ConfigContextMixin, CustomFieldsMixin, ImageAttachmentsMixin, TagsMixin,
+ ChangelogMixin, ConfigContextMixin, ContactsMixin, CustomFieldsMixin, ImageAttachmentsMixin, TagsMixin,
)
from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin
from netbox.graphql.scalars import BigInt
@@ -87,6 +87,8 @@ class ComponentTemplateObjectType(
#
class CableType(NetBoxObjectType):
+ a_terminations = graphene.List('dcim.graphql.gfk_mixins.CableTerminationTerminationType')
+ b_terminations = graphene.List('dcim.graphql.gfk_mixins.CableTerminationTerminationType')
class Meta:
model = models.Cable
@@ -99,12 +101,19 @@ class CableType(NetBoxObjectType):
def resolve_length_unit(self, info):
return self.length_unit or None
+ def resolve_a_terminations(self, info):
+ return self.a_terminations
+
+ def resolve_b_terminations(self, info):
+ return self.b_terminations
+
class CableTerminationType(NetBoxObjectType):
+ termination = graphene.Field('dcim.graphql.gfk_mixins.CableTerminationTerminationType')
class Meta:
model = models.CableTermination
- fields = '__all__'
+ exclude = ('termination_type', 'termination_id')
filterset_class = filtersets.CableTerminationFilterSet
@@ -152,7 +161,7 @@ class ConsoleServerPortTemplateType(ComponentTemplateObjectType):
return self.type or None
-class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, NetBoxObjectType):
+class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
class Meta:
model = models.Device
@@ -183,10 +192,11 @@ class DeviceBayTemplateType(ComponentTemplateObjectType):
class InventoryItemTemplateType(ComponentTemplateObjectType):
+ component = graphene.Field('dcim.graphql.gfk_mixins.InventoryItemTemplateComponentType')
class Meta:
model = models.InventoryItemTemplate
- fields = '__all__'
+ exclude = ('component_type', 'component_id')
filterset_class = filtersets.InventoryItemTemplateFilterSet
@@ -211,6 +221,9 @@ class DeviceTypeType(NetBoxObjectType):
def resolve_airflow(self, info):
return self.airflow or None
+ def resolve_weight_unit(self, info):
+ return self.weight_unit or None
+
class FrontPortType(ComponentObjectType, CabledObjectMixin):
@@ -266,10 +279,11 @@ class InterfaceTemplateType(ComponentTemplateObjectType):
class InventoryItemType(ComponentObjectType):
+ component = graphene.Field('dcim.graphql.gfk_mixins.InventoryItemComponentType')
class Meta:
model = models.InventoryItem
- fields = '__all__'
+ exclude = ('component_type', 'component_id')
filterset_class = filtersets.InventoryItemFilterSet
@@ -281,7 +295,7 @@ class InventoryItemRoleType(OrganizationalObjectType):
filterset_class = filtersets.InventoryItemRoleFilterSet
-class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, OrganizationalObjectType):
+class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, OrganizationalObjectType):
class Meta:
model = models.Location
@@ -289,7 +303,7 @@ class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, OrganizationalObjectT
filterset_class = filtersets.LocationFilterSet
-class ManufacturerType(OrganizationalObjectType):
+class ManufacturerType(OrganizationalObjectType, ContactsMixin):
class Meta:
model = models.Manufacturer
@@ -328,6 +342,9 @@ class ModuleTypeType(NetBoxObjectType):
fields = '__all__'
filterset_class = filtersets.ModuleTypeFilterSet
+ def resolve_weight_unit(self, info):
+ return self.weight_unit or None
+
class PlatformType(OrganizationalObjectType):
@@ -373,7 +390,7 @@ class PowerOutletTemplateType(ComponentTemplateObjectType):
return self.type or None
-class PowerPanelType(NetBoxObjectType):
+class PowerPanelType(NetBoxObjectType, ContactsMixin):
class Meta:
model = models.PowerPanel
@@ -403,7 +420,7 @@ class PowerPortTemplateType(ComponentTemplateObjectType):
return self.type or None
-class RackType(VLANGroupsMixin, ImageAttachmentsMixin, NetBoxObjectType):
+class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
class Meta:
model = models.Rack
@@ -416,6 +433,9 @@ class RackType(VLANGroupsMixin, ImageAttachmentsMixin, NetBoxObjectType):
def resolve_outer_unit(self, info):
return self.outer_unit or None
+ def resolve_weight_unit(self, info):
+ return self.weight_unit or None
+
class RackReservationType(NetBoxObjectType):
@@ -449,7 +469,7 @@ class RearPortTemplateType(ComponentTemplateObjectType):
filterset_class = filtersets.RearPortTemplateFilterSet
-class RegionType(VLANGroupsMixin, OrganizationalObjectType):
+class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
class Meta:
model = models.Region
@@ -457,7 +477,7 @@ class RegionType(VLANGroupsMixin, OrganizationalObjectType):
filterset_class = filtersets.RegionFilterSet
-class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, NetBoxObjectType):
+class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
asn = graphene.Field(BigInt)
class Meta:
@@ -466,7 +486,7 @@ class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, NetBoxObjectType):
filterset_class = filtersets.SiteFilterSet
-class SiteGroupType(VLANGroupsMixin, OrganizationalObjectType):
+class SiteGroupType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
class Meta:
model = models.SiteGroup
diff --git a/netbox/dcim/migrations/0001_squashed.py b/netbox/dcim/migrations/0001_squashed.py
index 374d3bf45..3d7156e17 100644
--- a/netbox/dcim/migrations/0001_squashed.py
+++ b/netbox/dcim/migrations/0001_squashed.py
@@ -1,6 +1,6 @@
import dcim.fields
import django.contrib.postgres.fields
-import django.core.serializers.json
+from utilities.json import CustomFieldJSONEncoder
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
@@ -28,7 +28,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('termination_a_id', models.PositiveIntegerField()),
('termination_b_id', models.PositiveIntegerField()),
@@ -60,7 +60,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=64)),
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
@@ -96,7 +96,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=64)),
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
@@ -132,7 +132,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('local_context_data', models.JSONField(blank=True, null=True)),
('name', models.CharField(blank=True, max_length=64, null=True)),
@@ -155,7 +155,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=64)),
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
@@ -186,7 +186,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, unique=True)),
('slug', models.SlugField(max_length=100, unique=True)),
@@ -195,7 +195,7 @@ class Migration(migrations.Migration):
('description', models.CharField(blank=True, max_length=200)),
],
options={
- 'ordering': ['name'],
+ 'ordering': ('name',),
},
),
migrations.CreateModel(
@@ -203,7 +203,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('model', models.CharField(max_length=100)),
('slug', models.SlugField(max_length=100)),
@@ -224,7 +224,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=64)),
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
@@ -261,7 +261,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=64)),
('label', models.CharField(blank=True, max_length=64)),
@@ -302,7 +302,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=64)),
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
@@ -326,7 +326,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100)),
('slug', models.SlugField(max_length=100)),
@@ -345,14 +345,14 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, unique=True)),
('slug', models.SlugField(max_length=100, unique=True)),
('description', models.CharField(blank=True, max_length=200)),
],
options={
- 'ordering': ['name'],
+ 'ordering': ('name',),
},
),
migrations.CreateModel(
@@ -360,7 +360,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, unique=True)),
('slug', models.SlugField(max_length=100, unique=True)),
@@ -369,7 +369,7 @@ class Migration(migrations.Migration):
('description', models.CharField(blank=True, max_length=200)),
],
options={
- 'ordering': ['name'],
+ 'ordering': ('name',),
},
),
migrations.CreateModel(
@@ -377,7 +377,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('_cable_peer_id', models.PositiveIntegerField(blank=True, null=True)),
('mark_connected', models.BooleanField(default=False)),
@@ -401,7 +401,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=64)),
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
@@ -438,7 +438,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100)),
],
@@ -451,7 +451,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=64)),
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
@@ -490,7 +490,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100)),
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
@@ -516,7 +516,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('units', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveSmallIntegerField(), size=None)),
('description', models.CharField(max_length=200)),
@@ -530,7 +530,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, unique=True)),
('slug', models.SlugField(max_length=100, unique=True)),
@@ -538,7 +538,7 @@ class Migration(migrations.Migration):
('description', models.CharField(blank=True, max_length=200)),
],
options={
- 'ordering': ['name'],
+ 'ordering': ('name',),
},
),
migrations.CreateModel(
@@ -546,7 +546,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=64)),
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
@@ -583,7 +583,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, unique=True)),
('slug', models.SlugField(max_length=100, unique=True)),
@@ -602,7 +602,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, unique=True)),
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
@@ -630,7 +630,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, unique=True)),
('slug', models.SlugField(max_length=100, unique=True)),
@@ -649,7 +649,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=64)),
('domain', models.CharField(blank=True, max_length=30)),
diff --git a/netbox/dcim/migrations/0146_modules.py b/netbox/dcim/migrations/0146_modules.py
index 11324fc58..821cf6119 100644
--- a/netbox/dcim/migrations/0146_modules.py
+++ b/netbox/dcim/migrations/0146_modules.py
@@ -1,4 +1,4 @@
-import django.core.serializers.json
+from utilities.json import CustomFieldJSONEncoder
from django.db import migrations, models
import django.db.models.deletion
import taggit.managers
@@ -107,7 +107,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('model', models.CharField(max_length=100)),
('part_number', models.CharField(blank=True, max_length=50)),
@@ -125,7 +125,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=64)),
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
@@ -145,7 +145,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('local_context_data', models.JSONField(blank=True, null=True)),
('serial', models.CharField(blank=True, max_length=50)),
diff --git a/netbox/dcim/migrations/0147_inventoryitemrole.py b/netbox/dcim/migrations/0147_inventoryitemrole.py
index f5e1f23f5..4b6c27450 100644
--- a/netbox/dcim/migrations/0147_inventoryitemrole.py
+++ b/netbox/dcim/migrations/0147_inventoryitemrole.py
@@ -1,4 +1,4 @@
-import django.core.serializers.json
+from utilities.json import CustomFieldJSONEncoder
from django.db import migrations, models
import django.db.models.deletion
import taggit.managers
@@ -18,7 +18,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, unique=True)),
('slug', models.SlugField(max_length=100, unique=True)),
@@ -27,7 +27,7 @@ class Migration(migrations.Migration):
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
- 'ordering': ['name'],
+ 'ordering': ('name',),
},
),
migrations.AddField(
diff --git a/netbox/dcim/migrations/0163_rack_devicetype_moduletype_weights.py b/netbox/dcim/migrations/0163_rack_devicetype_moduletype_weights.py
new file mode 100644
index 000000000..09bef5736
--- /dev/null
+++ b/netbox/dcim/migrations/0163_rack_devicetype_moduletype_weights.py
@@ -0,0 +1,58 @@
+# Generated by Django 4.0.7 on 2022-09-23 01:01
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dcim', '0162_unique_constraints'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='devicetype',
+ name='_abs_weight',
+ field=models.PositiveBigIntegerField(blank=True, null=True),
+ ),
+ migrations.AddField(
+ model_name='devicetype',
+ name='weight',
+ field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True),
+ ),
+ migrations.AddField(
+ model_name='devicetype',
+ name='weight_unit',
+ field=models.CharField(blank=True, max_length=50),
+ ),
+ migrations.AddField(
+ model_name='moduletype',
+ name='_abs_weight',
+ field=models.PositiveBigIntegerField(blank=True, null=True),
+ ),
+ migrations.AddField(
+ model_name='moduletype',
+ name='weight',
+ field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True),
+ ),
+ migrations.AddField(
+ model_name='moduletype',
+ name='weight_unit',
+ field=models.CharField(blank=True, max_length=50),
+ ),
+ migrations.AddField(
+ model_name='rack',
+ name='_abs_weight',
+ field=models.PositiveBigIntegerField(blank=True, null=True),
+ ),
+ migrations.AddField(
+ model_name='rack',
+ name='weight',
+ field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True),
+ ),
+ migrations.AddField(
+ model_name='rack',
+ name='weight_unit',
+ field=models.CharField(blank=True, max_length=50),
+ ),
+ ]
diff --git a/netbox/dcim/migrations/0164_rack_mounting_depth.py b/netbox/dcim/migrations/0164_rack_mounting_depth.py
new file mode 100644
index 000000000..5bd087beb
--- /dev/null
+++ b/netbox/dcim/migrations/0164_rack_mounting_depth.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.1 on 2022-10-27 14:01
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dcim', '0163_rack_devicetype_moduletype_weights'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='rack',
+ name='mounting_depth',
+ field=models.PositiveSmallIntegerField(blank=True, null=True),
+ ),
+ ]
diff --git a/netbox/dcim/migrations/0165_standardize_description_comments.py b/netbox/dcim/migrations/0165_standardize_description_comments.py
new file mode 100644
index 000000000..f17f1d321
--- /dev/null
+++ b/netbox/dcim/migrations/0165_standardize_description_comments.py
@@ -0,0 +1,78 @@
+# Generated by Django 4.1.2 on 2022-11-03 18:24
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dcim', '0164_rack_mounting_depth'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='cable',
+ name='comments',
+ field=models.TextField(blank=True),
+ ),
+ migrations.AddField(
+ model_name='cable',
+ name='description',
+ field=models.CharField(blank=True, max_length=200),
+ ),
+ migrations.AddField(
+ model_name='device',
+ name='description',
+ field=models.CharField(blank=True, max_length=200),
+ ),
+ migrations.AddField(
+ model_name='devicetype',
+ name='description',
+ field=models.CharField(blank=True, max_length=200),
+ ),
+ migrations.AddField(
+ model_name='module',
+ name='description',
+ field=models.CharField(blank=True, max_length=200),
+ ),
+ migrations.AddField(
+ model_name='moduletype',
+ name='description',
+ field=models.CharField(blank=True, max_length=200),
+ ),
+ migrations.AddField(
+ model_name='powerfeed',
+ name='description',
+ field=models.CharField(blank=True, max_length=200),
+ ),
+ migrations.AddField(
+ model_name='powerpanel',
+ name='comments',
+ field=models.TextField(blank=True),
+ ),
+ migrations.AddField(
+ model_name='powerpanel',
+ name='description',
+ field=models.CharField(blank=True, max_length=200),
+ ),
+ migrations.AddField(
+ model_name='rack',
+ name='description',
+ field=models.CharField(blank=True, max_length=200),
+ ),
+ migrations.AddField(
+ model_name='rackreservation',
+ name='comments',
+ field=models.TextField(blank=True),
+ ),
+ migrations.AddField(
+ model_name='virtualchassis',
+ name='comments',
+ field=models.TextField(blank=True),
+ ),
+ migrations.AddField(
+ model_name='virtualchassis',
+ name='description',
+ field=models.CharField(blank=True, max_length=200),
+ ),
+ ]
diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py
index fad3e8bd6..c51b59f94 100644
--- a/netbox/dcim/models/cables.py
+++ b/netbox/dcim/models/cables.py
@@ -12,8 +12,8 @@ from django.urls import reverse
from dcim.choices import *
from dcim.constants import *
from dcim.fields import PathField
-from dcim.utils import decompile_path_node, object_to_path_node, path_node_to_object
-from netbox.models import NetBoxModel
+from dcim.utils import decompile_path_node, object_to_path_node
+from netbox.models import PrimaryModel
from utilities.fields import ColorField
from utilities.querysets import RestrictedQuerySet
from utilities.utils import to_meters
@@ -34,7 +34,7 @@ trace_paths = Signal()
# Cables
#
-class Cable(NetBoxModel):
+class Cable(PrimaryModel):
"""
A physical connection between two endpoints.
"""
diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py
index 59d63ef7b..8855107b3 100644
--- a/netbox/dcim/models/device_components.py
+++ b/netbox/dcim/models/device_components.py
@@ -1025,27 +1025,9 @@ class InventoryItemRole(OrganizationalModel):
"""
Inventory items may optionally be assigned a functional role.
"""
- name = models.CharField(
- max_length=100,
- unique=True
- )
- slug = models.SlugField(
- max_length=100,
- unique=True
- )
color = ColorField(
default=ColorChoices.COLOR_GREY
)
- description = models.CharField(
- max_length=200,
- blank=True,
- )
-
- class Meta:
- ordering = ['name']
-
- def __str__(self):
- return self.name
def get_absolute_url(self):
return reverse('dcim:inventoryitemrole', args=[self.pk])
diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py
index d0d9001ad..78282f893 100644
--- a/netbox/dcim/models/devices.py
+++ b/netbox/dcim/models/devices.py
@@ -1,7 +1,8 @@
import decimal
-
import yaml
+from functools import cached_property
+
from django.apps import apps
from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import ValidationError
@@ -17,10 +18,11 @@ from dcim.constants import *
from extras.models import ConfigContextModel
from extras.querysets import ConfigContextModelQuerySet
from netbox.config import ConfigItem
-from netbox.models import OrganizationalModel, NetBoxModel
+from netbox.models import OrganizationalModel, PrimaryModel
from utilities.choices import ColorChoices
from utilities.fields import ColorField, NaturalOrderingField
from .device_components import *
+from .mixins import WeightMixin
__all__ = (
@@ -43,35 +45,16 @@ class Manufacturer(OrganizationalModel):
"""
A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
"""
- name = models.CharField(
- max_length=100,
- unique=True
- )
- slug = models.SlugField(
- max_length=100,
- unique=True
- )
- description = models.CharField(
- max_length=200,
- blank=True
- )
-
# Generic relations
contacts = GenericRelation(
to='tenancy.ContactAssignment'
)
- class Meta:
- ordering = ['name']
-
- def __str__(self):
- return self.name
-
def get_absolute_url(self):
return reverse('dcim:manufacturer', args=[self.pk])
-class DeviceType(NetBoxModel):
+class DeviceType(PrimaryModel, WeightMixin):
"""
A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as
well as high-level functional role(s).
@@ -134,12 +117,9 @@ class DeviceType(NetBoxModel):
upload_to='devicetype-images',
blank=True
)
- comments = models.TextField(
- blank=True
- )
clone_fields = (
- 'manufacturer', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
+ 'manufacturer', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit',
)
class Meta:
@@ -315,7 +295,7 @@ class DeviceType(NetBoxModel):
return self.subdevice_role == SubdeviceRoleChoices.ROLE_CHILD
-class ModuleType(NetBoxModel):
+class ModuleType(PrimaryModel, WeightMixin):
"""
A ModuleType represents a hardware element that can be installed within a device and which houses additional
components; for example, a line card within a chassis-based switch such as the Cisco Catalyst 6500. Like a
@@ -335,16 +315,13 @@ class ModuleType(NetBoxModel):
blank=True,
help_text='Discrete part number (optional)'
)
- comments = models.TextField(
- blank=True
- )
# Generic relations
images = GenericRelation(
to='extras.ImageAttachment'
)
- clone_fields = ('manufacturer',)
+ clone_fields = ('manufacturer', 'weight', 'weight_unit',)
class Meta:
ordering = ('manufacturer', 'model')
@@ -416,14 +393,6 @@ class DeviceRole(OrganizationalModel):
color to be used when displaying rack elevations. The vm_role field determines whether the role is applicable to
virtual machines as well.
"""
- name = models.CharField(
- max_length=100,
- unique=True
- )
- slug = models.SlugField(
- max_length=100,
- unique=True
- )
color = ColorField(
default=ColorChoices.COLOR_GREY
)
@@ -432,16 +401,6 @@ class DeviceRole(OrganizationalModel):
verbose_name='VM Role',
help_text='Virtual machines may be assigned to this role'
)
- description = models.CharField(
- max_length=200,
- blank=True,
- )
-
- class Meta:
- ordering = ['name']
-
- def __str__(self):
- return self.name
def get_absolute_url(self):
return reverse('dcim:devicerole', args=[self.pk])
@@ -453,14 +412,6 @@ class Platform(OrganizationalModel):
NetBox uses Platforms to determine how to interact with devices when pulling inventory data or other information by
specifying a NAPALM driver.
"""
- name = models.CharField(
- max_length=100,
- unique=True
- )
- slug = models.SlugField(
- max_length=100,
- unique=True
- )
manufacturer = models.ForeignKey(
to='dcim.Manufacturer',
on_delete=models.PROTECT,
@@ -481,22 +432,12 @@ class Platform(OrganizationalModel):
verbose_name='NAPALM arguments',
help_text='Additional arguments to pass when initiating the NAPALM driver (JSON format)'
)
- description = models.CharField(
- max_length=200,
- blank=True
- )
-
- class Meta:
- ordering = ['name']
-
- def __str__(self):
- return self.name
def get_absolute_url(self):
return reverse('dcim:platform', args=[self.pk])
-class Device(NetBoxModel, ConfigContextModel):
+class Device(PrimaryModel, ConfigContextModel):
"""
A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
DeviceRole, and (optionally) a Platform. Device names are not required, however if one is set it must be unique.
@@ -640,9 +581,6 @@ class Device(NetBoxModel, ConfigContextModel):
null=True,
validators=[MaxValueValidator(255)]
)
- comments = models.TextField(
- blank=True
- )
# Generic relations
contacts = GenericRelation(
@@ -946,8 +884,20 @@ class Device(NetBoxModel, ConfigContextModel):
def get_status_color(self):
return DeviceStatusChoices.colors.get(self.status)
+ @cached_property
+ def total_weight(self):
+ total_weight = sum(
+ module.module_type._abs_weight
+ for module in Module.objects.filter(device=self)
+ .exclude(module_type___abs_weight__isnull=True)
+ .prefetch_related('module_type')
+ )
+ if self.device_type._abs_weight:
+ total_weight += self.device_type._abs_weight
+ return round(total_weight / 1000, 2)
-class Module(NetBoxModel, ConfigContextModel):
+
+class Module(PrimaryModel, ConfigContextModel):
"""
A Module represents a field-installable component within a Device which may itself hold multiple device components
(for example, a line card within a chassis switch). Modules are instantiated from ModuleTypes.
@@ -980,9 +930,6 @@ class Module(NetBoxModel, ConfigContextModel):
verbose_name='Asset tag',
help_text='A unique tag used to identify this device'
)
- comments = models.TextField(
- blank=True
- )
clone_fields = ('device', 'module_type')
@@ -995,6 +942,14 @@ class Module(NetBoxModel, ConfigContextModel):
def get_absolute_url(self):
return reverse('dcim:module', args=[self.pk])
+ def clean(self):
+ super().clean()
+
+ if self.module_bay.device != self.device:
+ raise ValidationError(
+ f"Module must be installed within a module bay belonging to the assigned device ({self.device})."
+ )
+
def save(self, *args, **kwargs):
is_new = self.pk is None
@@ -1052,7 +1007,7 @@ class Module(NetBoxModel, ConfigContextModel):
# Virtual chassis
#
-class VirtualChassis(NetBoxModel):
+class VirtualChassis(PrimaryModel):
"""
A collection of Devices which operate with a shared control plane (e.g. a switch stack).
"""
diff --git a/netbox/dcim/models/mixins.py b/netbox/dcim/models/mixins.py
new file mode 100644
index 000000000..b5449332b
--- /dev/null
+++ b/netbox/dcim/models/mixins.py
@@ -0,0 +1,45 @@
+from django.core.exceptions import ValidationError
+from django.db import models
+from dcim.choices import *
+from utilities.utils import to_grams
+
+
+class WeightMixin(models.Model):
+ weight = models.DecimalField(
+ max_digits=8,
+ decimal_places=2,
+ blank=True,
+ null=True
+ )
+ weight_unit = models.CharField(
+ max_length=50,
+ choices=WeightUnitChoices,
+ blank=True,
+ )
+ # Stores the normalized weight (in grams) for database ordering
+ _abs_weight = models.PositiveBigIntegerField(
+ blank=True,
+ null=True
+ )
+
+ class Meta:
+ abstract = True
+
+ def save(self, *args, **kwargs):
+
+ # Store the given weight (if any) in grams for use in database ordering
+ if self.weight and self.weight_unit:
+ self._abs_weight = to_grams(self.weight, self.weight_unit)
+ else:
+ self._abs_weight = None
+
+ super().save(*args, **kwargs)
+
+ def clean(self):
+ super().clean()
+
+ # Validate weight and weight_unit
+ if self.weight is not None and not self.weight_unit:
+ raise ValidationError("Must specify a unit when setting a weight")
+ elif self.weight is None:
+ self.weight_unit = ''
diff --git a/netbox/dcim/models/power.py b/netbox/dcim/models/power.py
index 39f0f37ef..e79cf4c44 100644
--- a/netbox/dcim/models/power.py
+++ b/netbox/dcim/models/power.py
@@ -6,9 +6,8 @@ from django.db import models
from django.urls import reverse
from dcim.choices import *
-from dcim.constants import *
from netbox.config import ConfigItem
-from netbox.models import NetBoxModel
+from netbox.models import PrimaryModel
from utilities.validators import ExclusionValidator
from .device_components import CabledObjectModel, PathEndpoint
@@ -22,7 +21,7 @@ __all__ = (
# Power
#
-class PowerPanel(NetBoxModel):
+class PowerPanel(PrimaryModel):
"""
A distribution point for electrical power; e.g. a data center RPP.
"""
@@ -77,7 +76,7 @@ class PowerPanel(NetBoxModel):
)
-class PowerFeed(NetBoxModel, PathEndpoint, CabledObjectModel):
+class PowerFeed(PrimaryModel, PathEndpoint, CabledObjectModel):
"""
An electrical circuit delivered from a PowerPanel.
"""
@@ -132,9 +131,6 @@ class PowerFeed(NetBoxModel, PathEndpoint, CabledObjectModel):
default=0,
editable=False
)
- comments = models.TextField(
- blank=True
- )
clone_fields = (
'power_panel', 'rack', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage',
diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py
index 10550e906..e37fc8dc3 100644
--- a/netbox/dcim/models/racks.py
+++ b/netbox/dcim/models/racks.py
@@ -1,4 +1,5 @@
import decimal
+from functools import cached_property
from django.apps import apps
from django.contrib.auth.models import User
@@ -13,12 +14,13 @@ from django.urls import reverse
from dcim.choices import *
from dcim.constants import *
from dcim.svg import RackElevationSVG
-from netbox.models import OrganizationalModel, NetBoxModel
+from netbox.models import OrganizationalModel, PrimaryModel
from utilities.choices import ColorChoices
from utilities.fields import ColorField, NaturalOrderingField
from utilities.utils import array_to_string, drange
from .device_components import PowerPort
-from .devices import Device
+from .devices import Device, Module
+from .mixins import WeightMixin
from .power import PowerFeed
__all__ = (
@@ -36,33 +38,15 @@ class RackRole(OrganizationalModel):
"""
Racks can be organized by functional role, similar to Devices.
"""
- name = models.CharField(
- max_length=100,
- unique=True
- )
- slug = models.SlugField(
- max_length=100,
- unique=True
- )
color = ColorField(
default=ColorChoices.COLOR_GREY
)
- description = models.CharField(
- max_length=200,
- blank=True,
- )
-
- class Meta:
- ordering = ['name']
-
- def __str__(self):
- return self.name
def get_absolute_url(self):
return reverse('dcim:rackrole', args=[self.pk])
-class Rack(NetBoxModel):
+class Rack(PrimaryModel, WeightMixin):
"""
Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
Each Rack is assigned to a Site and (optionally) a Location.
@@ -165,8 +149,13 @@ class Rack(NetBoxModel):
choices=RackDimensionUnitChoices,
blank=True,
)
- comments = models.TextField(
- blank=True
+ mounting_depth = models.PositiveSmallIntegerField(
+ blank=True,
+ null=True,
+ help_text=(
+ 'Maximum depth of a mounted device, in millimeters. For four-post racks, this is the '
+ 'distance between the front and rear rails.'
+ )
)
# Generic relations
@@ -185,7 +174,7 @@ class Rack(NetBoxModel):
clone_fields = (
'site', 'location', 'tenant', 'status', 'role', 'type', 'width', 'u_height', 'desc_units', 'outer_width',
- 'outer_depth', 'outer_unit',
+ 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'weight_unit',
)
class Meta:
@@ -454,8 +443,24 @@ class Rack(NetBoxModel):
return int(allocated_draw / available_power_total * 100)
+ @cached_property
+ def total_weight(self):
+ total_weight = sum(
+ device.device_type._abs_weight
+ for device in self.devices.exclude(device_type___abs_weight__isnull=True).prefetch_related('device_type')
+ )
+ total_weight += sum(
+ module.module_type._abs_weight
+ for module in Module.objects.filter(device__rack=self)
+ .exclude(module_type___abs_weight__isnull=True)
+ .prefetch_related('module_type')
+ )
+ if self._abs_weight:
+ total_weight += self._abs_weight
+ return round(total_weight / 1000, 2)
-class RackReservation(NetBoxModel):
+
+class RackReservation(PrimaryModel):
"""
One or more reserved units within a Rack.
"""
diff --git a/netbox/dcim/models/sites.py b/netbox/dcim/models/sites.py
index 9ddadace2..c760119fb 100644
--- a/netbox/dcim/models/sites.py
+++ b/netbox/dcim/models/sites.py
@@ -2,12 +2,11 @@ from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
-from mptt.models import TreeForeignKey
from timezone_field import TimeZoneField
from dcim.choices import *
from dcim.constants import *
-from netbox.models import NestedGroupModel, NetBoxModel
+from netbox.models import NestedGroupModel, PrimaryModel
from utilities.fields import NaturalOrderingField
__all__ = (
@@ -28,25 +27,6 @@ class Region(NestedGroupModel):
states, and/or cities. Regions are recursively nested into a hierarchy: all sites belonging to a child region are
also considered to be members of its parent and ancestor region(s).
"""
- parent = TreeForeignKey(
- to='self',
- on_delete=models.CASCADE,
- related_name='children',
- blank=True,
- null=True,
- db_index=True
- )
- name = models.CharField(
- max_length=100
- )
- slug = models.SlugField(
- max_length=100
- )
- description = models.CharField(
- max_length=200,
- blank=True
- )
-
# Generic relations
vlan_groups = GenericRelation(
to='ipam.VLANGroup',
@@ -102,25 +82,6 @@ class SiteGroup(NestedGroupModel):
within corporate sites you might distinguish between offices and data centers. Like regions, site groups can be
nested recursively to form a hierarchy.
"""
- parent = TreeForeignKey(
- to='self',
- on_delete=models.CASCADE,
- related_name='children',
- blank=True,
- null=True,
- db_index=True
- )
- name = models.CharField(
- max_length=100
- )
- slug = models.SlugField(
- max_length=100
- )
- description = models.CharField(
- max_length=200,
- blank=True
- )
-
# Generic relations
vlan_groups = GenericRelation(
to='ipam.VLANGroup',
@@ -170,7 +131,7 @@ class SiteGroup(NestedGroupModel):
# Sites
#
-class Site(NetBoxModel):
+class Site(PrimaryModel):
"""
A Site represents a geographic location within a network; typically a building or campus. The optional facility
field can be used to include an external designation, such as a data center name (e.g. Equinix SV6).
@@ -227,10 +188,6 @@ class Site(NetBoxModel):
time_zone = TimeZoneField(
blank=True
)
- description = models.CharField(
- max_length=200,
- blank=True
- )
physical_address = models.CharField(
max_length=200,
blank=True
@@ -253,9 +210,6 @@ class Site(NetBoxModel):
null=True,
help_text='GPS coordinate (longitude)'
)
- comments = models.TextField(
- blank=True
- )
# Generic relations
vlan_groups = GenericRelation(
@@ -298,25 +252,11 @@ class Location(NestedGroupModel):
A Location represents a subgroup of Racks and/or Devices within a Site. A Location may represent a building within a
site, or a room within a building, for example.
"""
- name = models.CharField(
- max_length=100
- )
- slug = models.SlugField(
- max_length=100
- )
site = models.ForeignKey(
to='dcim.Site',
on_delete=models.CASCADE,
related_name='locations'
)
- parent = TreeForeignKey(
- to='self',
- on_delete=models.CASCADE,
- related_name='children',
- blank=True,
- null=True,
- db_index=True
- )
status = models.CharField(
max_length=50,
choices=LocationStatusChoices,
@@ -329,10 +269,6 @@ class Location(NestedGroupModel):
blank=True,
null=True
)
- description = models.CharField(
- max_length=200,
- blank=True
- )
# Generic relations
vlan_groups = GenericRelation(
diff --git a/netbox/dcim/search.py b/netbox/dcim/search.py
new file mode 100644
index 000000000..d34a78888
--- /dev/null
+++ b/netbox/dcim/search.py
@@ -0,0 +1,293 @@
+from netbox.search import SearchIndex, register_search
+from . import models
+
+
+@register_search
+class CableIndex(SearchIndex):
+ model = models.Cable
+ fields = (
+ ('label', 100),
+ )
+
+
+@register_search
+class ConsolePortIndex(SearchIndex):
+ model = models.ConsolePort
+ fields = (
+ ('name', 100),
+ ('label', 200),
+ ('description', 500),
+ ('speed', 2000),
+ )
+
+
+@register_search
+class ConsoleServerPortIndex(SearchIndex):
+ model = models.ConsoleServerPort
+ fields = (
+ ('name', 100),
+ ('label', 200),
+ ('description', 500),
+ ('speed', 2000),
+ )
+
+
+@register_search
+class DeviceIndex(SearchIndex):
+ model = models.Device
+ fields = (
+ ('asset_tag', 50),
+ ('serial', 60),
+ ('name', 100),
+ ('comments', 5000),
+ )
+
+
+@register_search
+class DeviceBayIndex(SearchIndex):
+ model = models.DeviceBay
+ fields = (
+ ('name', 100),
+ ('label', 200),
+ ('description', 500),
+ )
+
+
+@register_search
+class DeviceRoleIndex(SearchIndex):
+ model = models.DeviceRole
+ fields = (
+ ('name', 100),
+ ('slug', 110),
+ ('description', 500),
+ )
+
+
+@register_search
+class DeviceTypeIndex(SearchIndex):
+ model = models.DeviceType
+ fields = (
+ ('model', 100),
+ ('part_number', 200),
+ ('comments', 5000),
+ )
+
+
+@register_search
+class FrontPortIndex(SearchIndex):
+ model = models.FrontPort
+ fields = (
+ ('name', 100),
+ ('label', 200),
+ ('description', 500),
+ )
+
+
+@register_search
+class InterfaceIndex(SearchIndex):
+ model = models.Interface
+ fields = (
+ ('name', 100),
+ ('label', 200),
+ ('mac_address', 300),
+ ('wwn', 300),
+ ('description', 500),
+ ('mtu', 2000),
+ ('speed', 2000),
+ )
+
+
+@register_search
+class InventoryItemIndex(SearchIndex):
+ model = models.InventoryItem
+ fields = (
+ ('asset_tag', 50),
+ ('serial', 60),
+ ('name', 100),
+ ('label', 200),
+ ('description', 500),
+ ('part_id', 2000),
+ )
+
+
+@register_search
+class LocationIndex(SearchIndex):
+ model = models.Location
+ fields = (
+ ('name', 100),
+ ('slug', 110),
+ ('description', 500),
+ )
+
+
+@register_search
+class ManufacturerIndex(SearchIndex):
+ model = models.Manufacturer
+ fields = (
+ ('name', 100),
+ ('slug', 110),
+ ('description', 500),
+ )
+
+
+@register_search
+class ModuleIndex(SearchIndex):
+ model = models.Module
+ fields = (
+ ('asset_tag', 50),
+ ('serial', 60),
+ ('comments', 5000),
+ )
+
+
+@register_search
+class ModuleBayIndex(SearchIndex):
+ model = models.ModuleBay
+ fields = (
+ ('name', 100),
+ ('label', 200),
+ ('description', 500),
+ )
+
+
+@register_search
+class ModuleTypeIndex(SearchIndex):
+ model = models.ModuleType
+ fields = (
+ ('model', 100),
+ ('part_number', 200),
+ ('comments', 5000),
+ )
+
+
+@register_search
+class PlatformIndex(SearchIndex):
+ model = models.Platform
+ fields = (
+ ('name', 100),
+ ('slug', 110),
+ ('napalm_driver', 300),
+ ('description', 500),
+ )
+
+
+@register_search
+class PowerFeedIndex(SearchIndex):
+ model = models.PowerFeed
+ fields = (
+ ('name', 100),
+ ('comments', 5000),
+ )
+
+
+@register_search
+class PowerOutletIndex(SearchIndex):
+ model = models.PowerOutlet
+ fields = (
+ ('name', 100),
+ ('label', 200),
+ ('description', 500),
+ )
+
+
+@register_search
+class PowerPanelIndex(SearchIndex):
+ model = models.PowerPanel
+ fields = (
+ ('name', 100),
+ )
+
+
+@register_search
+class PowerPortIndex(SearchIndex):
+ model = models.PowerPort
+ fields = (
+ ('name', 100),
+ ('label', 200),
+ ('description', 500),
+ ('maximum_draw', 2000),
+ ('allocated_draw', 2000),
+ )
+
+
+@register_search
+class RackIndex(SearchIndex):
+ model = models.Rack
+ fields = (
+ ('asset_tag', 50),
+ ('serial', 60),
+ ('name', 100),
+ ('facility_id', 200),
+ ('comments', 5000),
+ )
+
+
+@register_search
+class RackReservationIndex(SearchIndex):
+ model = models.RackReservation
+ fields = (
+ ('description', 500),
+ )
+
+
+@register_search
+class RackRoleIndex(SearchIndex):
+ model = models.RackRole
+ fields = (
+ ('name', 100),
+ ('slug', 110),
+ ('description', 500),
+ )
+
+
+@register_search
+class RearPortIndex(SearchIndex):
+ model = models.RearPort
+ fields = (
+ ('name', 100),
+ ('label', 200),
+ ('description', 500),
+ )
+
+
+@register_search
+class RegionIndex(SearchIndex):
+ model = models.Region
+ fields = (
+ ('name', 100),
+ ('slug', 110),
+ ('description', 500)
+ )
+
+
+@register_search
+class SiteIndex(SearchIndex):
+ model = models.Site
+ fields = (
+ ('name', 100),
+ ('facility', 100),
+ ('slug', 110),
+ ('description', 500),
+ ('physical_address', 2000),
+ ('shipping_address', 2000),
+ ('comments', 5000),
+ )
+
+
+@register_search
+class SiteGroupIndex(SearchIndex):
+ model = models.SiteGroup
+ fields = (
+ ('name', 100),
+ ('slug', 110),
+ ('description', 500)
+ )
+
+
+@register_search
+class VirtualChassisIndex(SearchIndex):
+ model = models.VirtualChassis
+ fields = (
+ ('name', 100),
+ ('domain', 300)
+ )
diff --git a/netbox/dcim/svg/cables.py b/netbox/dcim/svg/cables.py
index 3872bc4fe..9a847acc9 100644
--- a/netbox/dcim/svg/cables.py
+++ b/netbox/dcim/svg/cables.py
@@ -35,7 +35,7 @@ class Node(Hyperlink):
"""
def __init__(self, position, width, url, color, labels, radius=10, **extra):
- super(Node, self).__init__(href=url, target='_blank', **extra)
+ super(Node, self).__init__(href=url, target='_parent', **extra)
x, y = position
diff --git a/netbox/dcim/svg/racks.py b/netbox/dcim/svg/racks.py
index 573fc966c..6c57e6023 100644
--- a/netbox/dcim/svg/racks.py
+++ b/netbox/dcim/svg/racks.py
@@ -9,6 +9,7 @@ from svgwrite.text import Text
from django.conf import settings
from django.core.exceptions import FieldError
from django.db.models import Q
+from django.template.defaultfilters import floatformat
from django.urls import reverse
from django.utils.http import urlencode
@@ -41,7 +42,7 @@ def get_device_description(device):
device.device_role,
device.device_type.manufacturer.name,
device.device_type.model,
- device.device_type.u_height,
+ floatformat(device.device_type.u_height),
device.asset_tag or '',
device.serial or ''
)
diff --git a/netbox/dcim/tables/cables.py b/netbox/dcim/tables/cables.py
index e5410e42a..6e9d49719 100644
--- a/netbox/dcim/tables/cables.py
+++ b/netbox/dcim/tables/cables.py
@@ -111,6 +111,7 @@ class CableTable(TenancyColumnsMixin, NetBoxTable):
order_by=('_abs_length', 'length_unit')
)
color = columns.ColorColumn()
+ comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='dcim:cable_list'
)
@@ -120,7 +121,7 @@ class CableTable(TenancyColumnsMixin, NetBoxTable):
fields = (
'pk', 'id', 'label', 'a_terminations', 'b_terminations', 'device_a', 'device_b', 'rack_a', 'rack_b',
'location_a', 'location_b', 'site_a', 'site_b', 'status', 'type', 'tenant', 'tenant_group', 'color',
- 'length', 'tags', 'created', 'last_updated',
+ 'length', 'description', 'comments', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'id', 'label', 'a_terminations', 'b_terminations', 'status', 'type',
diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py
index 142c7ef67..45a210080 100644
--- a/netbox/dcim/tables/devices.py
+++ b/netbox/dcim/tables/devices.py
@@ -1,12 +1,10 @@
import django_tables2 as tables
+from dcim import models
from django_tables2.utils import Accessor
+from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
-from dcim.models import (
- ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, FrontPort, Interface, InventoryItem,
- InventoryItemRole, ModuleBay, Platform, PowerOutlet, PowerPort, RearPort, VirtualChassis,
-)
from netbox.tables import NetBoxTable, columns
-from tenancy.tables import TenancyColumnsMixin
+
from .template_code import *
__all__ = (
@@ -92,7 +90,7 @@ class DeviceRoleTable(NetBoxTable):
)
class Meta(NetBoxTable.Meta):
- model = DeviceRole
+ model = models.DeviceRole
fields = (
'pk', 'id', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'tags',
'actions', 'created', 'last_updated',
@@ -123,7 +121,7 @@ class PlatformTable(NetBoxTable):
)
class Meta(NetBoxTable.Meta):
- model = Platform
+ model = models.Platform
fields = (
'pk', 'id', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'napalm_args',
'description', 'tags', 'actions', 'created', 'last_updated',
@@ -137,7 +135,7 @@ class PlatformTable(NetBoxTable):
# Devices
#
-class DeviceTable(TenancyColumnsMixin, NetBoxTable):
+class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
name = tables.TemplateColumn(
order_by=('_name',),
template_code=DEVICE_LINK
@@ -201,20 +199,17 @@ class DeviceTable(TenancyColumnsMixin, NetBoxTable):
verbose_name='VC Priority'
)
comments = columns.MarkdownColumn()
- contacts = columns.ManyToManyColumn(
- linkify_item=True
- )
tags = columns.TagColumn(
url_name='dcim:device_list'
)
class Meta(NetBoxTable.Meta):
- model = Device
+ model = models.Device
fields = (
'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'device_role', 'manufacturer', 'device_type',
'platform', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'location', 'rack', 'position', 'face',
'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position',
- 'vc_priority', 'comments', 'contacts', 'tags', 'created', 'last_updated',
+ 'vc_priority', 'description', 'comments', 'contacts', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type',
@@ -241,7 +236,7 @@ class DeviceImportTable(TenancyColumnsMixin, NetBoxTable):
)
class Meta(NetBoxTable.Meta):
- model = Device
+ model = models.Device
fields = ('id', 'name', 'status', 'tenant', 'tenant_group', 'site', 'rack', 'position', 'device_role', 'device_type')
empty_text = False
@@ -315,7 +310,7 @@ class ConsolePortTable(ModularDeviceComponentTable, PathEndpointTable):
)
class Meta(DeviceComponentTable.Meta):
- model = ConsolePort
+ model = models.ConsolePort
fields = (
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'speed', 'description',
'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'created', 'last_updated',
@@ -334,7 +329,7 @@ class DeviceConsolePortTable(ConsolePortTable):
)
class Meta(DeviceComponentTable.Meta):
- model = ConsolePort
+ model = models.ConsolePort
fields = (
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'speed', 'description', 'mark_connected',
'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions'
@@ -357,7 +352,7 @@ class ConsoleServerPortTable(ModularDeviceComponentTable, PathEndpointTable):
)
class Meta(DeviceComponentTable.Meta):
- model = ConsoleServerPort
+ model = models.ConsoleServerPort
fields = (
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'speed', 'description',
'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'created', 'last_updated',
@@ -377,7 +372,7 @@ class DeviceConsoleServerPortTable(ConsoleServerPortTable):
)
class Meta(DeviceComponentTable.Meta):
- model = ConsoleServerPort
+ model = models.ConsoleServerPort
fields = (
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'speed', 'description', 'mark_connected',
'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions',
@@ -400,7 +395,7 @@ class PowerPortTable(ModularDeviceComponentTable, PathEndpointTable):
)
class Meta(DeviceComponentTable.Meta):
- model = PowerPort
+ model = models.PowerPort
fields = (
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'description', 'mark_connected',
'maximum_draw', 'allocated_draw', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'created',
@@ -421,7 +416,7 @@ class DevicePowerPortTable(PowerPortTable):
)
class Meta(DeviceComponentTable.Meta):
- model = PowerPort
+ model = models.PowerPort
fields = (
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'maximum_draw', 'allocated_draw',
'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions',
@@ -449,7 +444,7 @@ class PowerOutletTable(ModularDeviceComponentTable, PathEndpointTable):
)
class Meta(DeviceComponentTable.Meta):
- model = PowerOutlet
+ model = models.PowerOutlet
fields = (
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'description', 'power_port',
'feed_leg', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'created',
@@ -469,7 +464,7 @@ class DevicePowerOutletTable(PowerOutletTable):
)
class Meta(DeviceComponentTable.Meta):
- model = PowerOutlet
+ model = models.PowerOutlet
fields = (
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'power_port', 'feed_leg', 'description',
'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions',
@@ -533,7 +528,7 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
)
class Meta(DeviceComponentTable.Meta):
- model = Interface
+ model = models.Interface
fields = (
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'enabled', 'type', 'mgmt_only', 'mtu',
'speed', 'duplex', 'mode', 'mac_address', 'wwn', 'poe_mode', 'poe_type', 'rf_role', 'rf_channel',
@@ -567,7 +562,7 @@ class DeviceInterfaceTable(InterfaceTable):
)
class Meta(DeviceComponentTable.Meta):
- model = Interface
+ model = models.Interface
fields = (
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'enabled', 'type', 'parent', 'bridge', 'lag',
'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_frequency',
@@ -606,7 +601,7 @@ class FrontPortTable(ModularDeviceComponentTable, CableTerminationTable):
)
class Meta(DeviceComponentTable.Meta):
- model = FrontPort
+ model = models.FrontPort
fields = (
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'rear_port',
'rear_port_position', 'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags',
@@ -629,7 +624,7 @@ class DeviceFrontPortTable(FrontPortTable):
)
class Meta(DeviceComponentTable.Meta):
- model = FrontPort
+ model = models.FrontPort
fields = (
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'rear_port', 'rear_port_position',
'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags', 'actions',
@@ -655,7 +650,7 @@ class RearPortTable(ModularDeviceComponentTable, CableTerminationTable):
)
class Meta(DeviceComponentTable.Meta):
- model = RearPort
+ model = models.RearPort
fields = (
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'description',
'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags', 'created', 'last_updated',
@@ -675,7 +670,7 @@ class DeviceRearPortTable(RearPortTable):
)
class Meta(DeviceComponentTable.Meta):
- model = RearPort
+ model = models.RearPort
fields = (
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'positions', 'description', 'mark_connected',
'cable', 'cable_color', 'link_peer', 'tags', 'actions',
@@ -716,7 +711,7 @@ class DeviceBayTable(DeviceComponentTable):
)
class Meta(DeviceComponentTable.Meta):
- model = DeviceBay
+ model = models.DeviceBay
fields = (
'pk', 'id', 'name', 'device', 'label', 'status', 'device_role', 'device_type', 'installed_device', 'description', 'tags',
'created', 'last_updated',
@@ -737,7 +732,7 @@ class DeviceDeviceBayTable(DeviceBayTable):
)
class Meta(DeviceComponentTable.Meta):
- model = DeviceBay
+ model = models.DeviceBay
fields = (
'pk', 'id', 'name', 'label', 'status', 'installed_device', 'description', 'tags', 'actions',
)
@@ -766,7 +761,7 @@ class ModuleBayTable(DeviceComponentTable):
)
class Meta(DeviceComponentTable.Meta):
- model = ModuleBay
+ model = models.ModuleBay
fields = (
'pk', 'id', 'name', 'device', 'label', 'position', 'installed_module', 'module_serial', 'module_asset_tag',
'description', 'tags',
@@ -780,7 +775,7 @@ class DeviceModuleBayTable(ModuleBayTable):
)
class Meta(DeviceComponentTable.Meta):
- model = ModuleBay
+ model = models.ModuleBay
fields = (
'pk', 'id', 'name', 'label', 'position', 'installed_module', 'module_serial', 'module_asset_tag',
'description', 'tags', 'actions',
@@ -810,7 +805,7 @@ class InventoryItemTable(DeviceComponentTable):
cable = None # Override DeviceComponentTable
class Meta(NetBoxTable.Meta):
- model = InventoryItem
+ model = models.InventoryItem
fields = (
'pk', 'id', 'name', 'device', 'component', 'label', 'role', 'manufacturer', 'part_id', 'serial',
'asset_tag', 'description', 'discovered', 'tags', 'created', 'last_updated',
@@ -829,7 +824,7 @@ class DeviceInventoryItemTable(InventoryItemTable):
)
class Meta(NetBoxTable.Meta):
- model = InventoryItem
+ model = models.InventoryItem
fields = (
'pk', 'id', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component',
'description', 'discovered', 'tags', 'actions',
@@ -854,7 +849,7 @@ class InventoryItemRoleTable(NetBoxTable):
)
class Meta(NetBoxTable.Meta):
- model = InventoryItemRole
+ model = models.InventoryItemRole
fields = (
'pk', 'id', 'name', 'inventoryitem_count', 'color', 'description', 'slug', 'tags', 'actions',
)
@@ -877,11 +872,15 @@ class VirtualChassisTable(NetBoxTable):
url_params={'virtual_chassis_id': 'pk'},
verbose_name='Members'
)
+ comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='dcim:virtualchassis_list'
)
class Meta(NetBoxTable.Meta):
- model = VirtualChassis
- fields = ('pk', 'id', 'name', 'domain', 'master', 'member_count', 'tags', 'created', 'last_updated',)
+ model = models.VirtualChassis
+ fields = (
+ 'pk', 'id', 'name', 'domain', 'master', 'member_count', 'description', 'comments', 'tags', 'created',
+ 'last_updated',
+ )
default_columns = ('pk', 'name', 'domain', 'master', 'member_count')
diff --git a/netbox/dcim/tables/devicetypes.py b/netbox/dcim/tables/devicetypes.py
index 3ed4d8c08..a52d41b70 100644
--- a/netbox/dcim/tables/devicetypes.py
+++ b/netbox/dcim/tables/devicetypes.py
@@ -1,11 +1,9 @@
import django_tables2 as tables
-from dcim.models import (
- ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, DeviceType, FrontPortTemplate, InterfaceTemplate,
- InventoryItemTemplate, Manufacturer, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
-)
+from dcim import models
from netbox.tables import NetBoxTable, columns
-from .template_code import MODULAR_COMPONENT_TEMPLATE_BUTTONS
+from tenancy.tables import ContactsColumnMixin
+from .template_code import MODULAR_COMPONENT_TEMPLATE_BUTTONS, DEVICE_WEIGHT
__all__ = (
'ConsolePortTemplateTable',
@@ -27,7 +25,7 @@ __all__ = (
# Manufacturers
#
-class ManufacturerTable(NetBoxTable):
+class ManufacturerTable(ContactsColumnMixin, NetBoxTable):
name = tables.Column(
linkify=True
)
@@ -43,15 +41,12 @@ class ManufacturerTable(NetBoxTable):
verbose_name='Platforms'
)
slug = tables.Column()
- contacts = columns.ManyToManyColumn(
- linkify_item=True
- )
tags = columns.TagColumn(
url_name='dcim:manufacturer_list'
)
class Meta(NetBoxTable.Meta):
- model = Manufacturer
+ model = models.Manufacturer
fields = (
'pk', 'id', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug',
'contacts', 'actions', 'created', 'last_updated',
@@ -85,12 +80,19 @@ class DeviceTypeTable(NetBoxTable):
tags = columns.TagColumn(
url_name='dcim:devicetype_list'
)
+ u_height = columns.TemplateColumn(
+ template_code='{{ value|floatformat }}'
+ )
+ weight = columns.TemplateColumn(
+ template_code=DEVICE_WEIGHT,
+ order_by=('_abs_weight', 'weight_unit')
+ )
class Meta(NetBoxTable.Meta):
- model = DeviceType
+ model = models.DeviceType
fields = (
'pk', 'id', 'model', 'manufacturer', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
- 'airflow', 'comments', 'instance_count', 'tags', 'created', 'last_updated',
+ 'airflow', 'weight', 'description', 'comments', 'instance_count', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'instance_count',
@@ -120,7 +122,7 @@ class ConsolePortTemplateTable(ComponentTemplateTable):
)
class Meta(ComponentTemplateTable.Meta):
- model = ConsolePortTemplate
+ model = models.ConsolePortTemplate
fields = ('pk', 'name', 'label', 'type', 'description', 'actions')
empty_text = "None"
@@ -132,7 +134,7 @@ class ConsoleServerPortTemplateTable(ComponentTemplateTable):
)
class Meta(ComponentTemplateTable.Meta):
- model = ConsoleServerPortTemplate
+ model = models.ConsoleServerPortTemplate
fields = ('pk', 'name', 'label', 'type', 'description', 'actions')
empty_text = "None"
@@ -144,7 +146,7 @@ class PowerPortTemplateTable(ComponentTemplateTable):
)
class Meta(ComponentTemplateTable.Meta):
- model = PowerPortTemplate
+ model = models.PowerPortTemplate
fields = ('pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'actions')
empty_text = "None"
@@ -156,7 +158,7 @@ class PowerOutletTemplateTable(ComponentTemplateTable):
)
class Meta(ComponentTemplateTable.Meta):
- model = PowerOutletTemplate
+ model = models.PowerOutletTemplate
fields = ('pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'actions')
empty_text = "None"
@@ -171,7 +173,7 @@ class InterfaceTemplateTable(ComponentTemplateTable):
)
class Meta(ComponentTemplateTable.Meta):
- model = InterfaceTemplate
+ model = models.InterfaceTemplate
fields = ('pk', 'name', 'label', 'mgmt_only', 'type', 'description', 'poe_mode', 'poe_type', 'actions')
empty_text = "None"
@@ -187,7 +189,7 @@ class FrontPortTemplateTable(ComponentTemplateTable):
)
class Meta(ComponentTemplateTable.Meta):
- model = FrontPortTemplate
+ model = models.FrontPortTemplate
fields = ('pk', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description', 'actions')
empty_text = "None"
@@ -200,7 +202,7 @@ class RearPortTemplateTable(ComponentTemplateTable):
)
class Meta(ComponentTemplateTable.Meta):
- model = RearPortTemplate
+ model = models.RearPortTemplate
fields = ('pk', 'name', 'label', 'type', 'color', 'positions', 'description', 'actions')
empty_text = "None"
@@ -211,7 +213,7 @@ class ModuleBayTemplateTable(ComponentTemplateTable):
)
class Meta(ComponentTemplateTable.Meta):
- model = ModuleBayTemplate
+ model = models.ModuleBayTemplate
fields = ('pk', 'name', 'label', 'position', 'description', 'actions')
empty_text = "None"
@@ -222,7 +224,7 @@ class DeviceBayTemplateTable(ComponentTemplateTable):
)
class Meta(ComponentTemplateTable.Meta):
- model = DeviceBayTemplate
+ model = models.DeviceBayTemplate
fields = ('pk', 'name', 'label', 'description', 'actions')
empty_text = "None"
@@ -242,7 +244,7 @@ class InventoryItemTemplateTable(ComponentTemplateTable):
)
class Meta(ComponentTemplateTable.Meta):
- model = InventoryItemTemplate
+ model = models.InventoryItemTemplate
fields = (
'pk', 'name', 'label', 'parent', 'role', 'manufacturer', 'part_id', 'component', 'description', 'actions',
)
diff --git a/netbox/dcim/tables/modules.py b/netbox/dcim/tables/modules.py
index e40d7bd80..9df26eb73 100644
--- a/netbox/dcim/tables/modules.py
+++ b/netbox/dcim/tables/modules.py
@@ -2,6 +2,7 @@ import django_tables2 as tables
from dcim.models import Module, ModuleType
from netbox.tables import NetBoxTable, columns
+from .template_code import DEVICE_WEIGHT
__all__ = (
'ModuleTable',
@@ -26,11 +27,15 @@ class ModuleTypeTable(NetBoxTable):
tags = columns.TagColumn(
url_name='dcim:moduletype_list'
)
+ weight = columns.TemplateColumn(
+ template_code=DEVICE_WEIGHT,
+ order_by=('_abs_weight', 'weight_unit')
+ )
class Meta(NetBoxTable.Meta):
model = ModuleType
fields = (
- 'pk', 'id', 'model', 'manufacturer', 'part_number', 'comments', 'tags',
+ 'pk', 'id', 'model', 'manufacturer', 'part_number', 'weight', 'description', 'comments', 'tags',
)
default_columns = (
'pk', 'model', 'manufacturer', 'part_number',
@@ -59,8 +64,8 @@ class ModuleTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = Module
fields = (
- 'pk', 'id', 'device', 'module_bay', 'manufacturer', 'module_type', 'serial', 'asset_tag', 'comments',
- 'tags',
+ 'pk', 'id', 'device', 'module_bay', 'manufacturer', 'module_type', 'serial', 'asset_tag', 'description',
+ 'comments', 'tags',
)
default_columns = (
'pk', 'id', 'device', 'module_bay', 'manufacturer', 'module_type', 'serial', 'asset_tag',
diff --git a/netbox/dcim/tables/power.py b/netbox/dcim/tables/power.py
index 6696d516a..feff29e12 100644
--- a/netbox/dcim/tables/power.py
+++ b/netbox/dcim/tables/power.py
@@ -1,7 +1,9 @@
import django_tables2 as tables
-
from dcim.models import PowerFeed, PowerPanel
+from tenancy.tables import ContactsColumnMixin
+
from netbox.tables import NetBoxTable, columns
+
from .devices import CableTerminationTable
__all__ = (
@@ -14,7 +16,7 @@ __all__ = (
# Power panels
#
-class PowerPanelTable(NetBoxTable):
+class PowerPanelTable(ContactsColumnMixin, NetBoxTable):
name = tables.Column(
linkify=True
)
@@ -29,9 +31,7 @@ class PowerPanelTable(NetBoxTable):
url_params={'power_panel_id': 'pk'},
verbose_name='Feeds'
)
- contacts = columns.ManyToManyColumn(
- linkify_item=True
- )
+ comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='dcim:powerpanel_list'
)
@@ -39,7 +39,8 @@ class PowerPanelTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = PowerPanel
fields = (
- 'pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'contacts', 'tags', 'created', 'last_updated',
+ 'pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'contacts', 'description', 'comments', 'tags',
+ 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'site', 'location', 'powerfeed_count')
@@ -78,7 +79,7 @@ class PowerFeedTable(CableTerminationTable):
fields = (
'pk', 'id', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
'max_utilization', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'available_power',
- 'comments', 'tags', 'created', 'last_updated',
+ 'description', 'comments', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'cable',
diff --git a/netbox/dcim/tables/racks.py b/netbox/dcim/tables/racks.py
index 39553bac0..b360002d2 100644
--- a/netbox/dcim/tables/racks.py
+++ b/netbox/dcim/tables/racks.py
@@ -3,7 +3,8 @@ from django_tables2.utils import Accessor
from dcim.models import Rack, RackReservation, RackRole
from netbox.tables import NetBoxTable, columns
-from tenancy.tables import TenancyColumnsMixin
+from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
+from .template_code import DEVICE_WEIGHT
__all__ = (
'RackTable',
@@ -37,7 +38,7 @@ class RackRoleTable(NetBoxTable):
# Racks
#
-class RackTable(TenancyColumnsMixin, NetBoxTable):
+class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
name = tables.Column(
order_by=('_name',),
linkify=True
@@ -68,9 +69,6 @@ class RackTable(TenancyColumnsMixin, NetBoxTable):
orderable=False,
verbose_name='Power'
)
- contacts = columns.ManyToManyColumn(
- linkify_item=True
- )
tags = columns.TagColumn(
url_name='dcim:rack_list'
)
@@ -82,13 +80,18 @@ class RackTable(TenancyColumnsMixin, NetBoxTable):
template_code="{{ record.outer_depth }} {{ record.outer_unit }}",
verbose_name='Outer Depth'
)
+ weight = columns.TemplateColumn(
+ template_code=DEVICE_WEIGHT,
+ order_by=('_abs_weight', 'weight_unit')
+ )
class Meta(NetBoxTable.Meta):
model = Rack
fields = (
- 'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'tenant_group', 'role', 'serial', 'asset_tag',
- 'type', 'width', 'outer_width', 'outer_depth', 'u_height', 'comments', 'device_count', 'get_utilization',
- 'get_power_utilization', 'contacts', 'tags', 'created', 'last_updated',
+ 'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'tenant_group', 'role', 'serial',
+ 'asset_tag', 'type', 'u_height', 'width', 'outer_width', 'outer_depth', 'mounting_depth', 'weight',
+ 'comments', 'device_count', 'get_utilization', 'get_power_utilization', 'description', 'contacts', 'tags',
+ 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count',
@@ -120,6 +123,7 @@ class RackReservationTable(TenancyColumnsMixin, NetBoxTable):
orderable=False,
verbose_name='Units'
)
+ comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='dcim:rackreservation_list'
)
@@ -127,7 +131,7 @@ class RackReservationTable(TenancyColumnsMixin, NetBoxTable):
class Meta(NetBoxTable.Meta):
model = RackReservation
fields = (
- 'pk', 'id', 'reservation', 'site', 'location', 'rack', 'unit_list', 'user', 'created', 'tenant', 'tenant_group', 'description', 'tags',
- 'actions', 'created', 'last_updated',
+ 'pk', 'id', 'reservation', 'site', 'location', 'rack', 'unit_list', 'user', 'created', 'tenant',
+ 'tenant_group', 'description', 'comments', 'tags', 'actions', 'created', 'last_updated',
)
default_columns = ('pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'description')
diff --git a/netbox/dcim/tables/sites.py b/netbox/dcim/tables/sites.py
index 5dc2aa611..f013025f7 100644
--- a/netbox/dcim/tables/sites.py
+++ b/netbox/dcim/tables/sites.py
@@ -1,8 +1,9 @@
import django_tables2 as tables
-
from dcim.models import Location, Region, Site, SiteGroup
+from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
+
from netbox.tables import NetBoxTable, columns
-from tenancy.tables import TenancyColumnsMixin
+
from .template_code import LOCATION_BUTTONS
__all__ = (
@@ -17,7 +18,7 @@ __all__ = (
# Regions
#
-class RegionTable(NetBoxTable):
+class RegionTable(ContactsColumnMixin, NetBoxTable):
name = columns.MPTTColumn(
linkify=True
)
@@ -26,9 +27,6 @@ class RegionTable(NetBoxTable):
url_params={'region_id': 'pk'},
verbose_name='Sites'
)
- contacts = columns.ManyToManyColumn(
- linkify_item=True
- )
tags = columns.TagColumn(
url_name='dcim:region_list'
)
@@ -46,7 +44,7 @@ class RegionTable(NetBoxTable):
# Site groups
#
-class SiteGroupTable(NetBoxTable):
+class SiteGroupTable(ContactsColumnMixin, NetBoxTable):
name = columns.MPTTColumn(
linkify=True
)
@@ -55,9 +53,6 @@ class SiteGroupTable(NetBoxTable):
url_params={'group_id': 'pk'},
verbose_name='Sites'
)
- contacts = columns.ManyToManyColumn(
- linkify_item=True
- )
tags = columns.TagColumn(
url_name='dcim:sitegroup_list'
)
@@ -75,7 +70,7 @@ class SiteGroupTable(NetBoxTable):
# Sites
#
-class SiteTable(TenancyColumnsMixin, NetBoxTable):
+class SiteTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
name = tables.Column(
linkify=True
)
@@ -97,9 +92,6 @@ class SiteTable(TenancyColumnsMixin, NetBoxTable):
verbose_name='ASN Count'
)
comments = columns.MarkdownColumn()
- contacts = columns.ManyToManyColumn(
- linkify_item=True
- )
tags = columns.TagColumn(
url_name='dcim:site_list'
)
@@ -118,7 +110,7 @@ class SiteTable(TenancyColumnsMixin, NetBoxTable):
# Locations
#
-class LocationTable(TenancyColumnsMixin, NetBoxTable):
+class LocationTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
name = columns.MPTTColumn(
linkify=True
)
@@ -136,9 +128,6 @@ class LocationTable(TenancyColumnsMixin, NetBoxTable):
url_params={'location_id': 'pk'},
verbose_name='Devices'
)
- contacts = columns.ManyToManyColumn(
- linkify_item=True
- )
tags = columns.TagColumn(
url_name='dcim:location_list'
)
diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py
index dfc77b854..9b8fb8fd6 100644
--- a/netbox/dcim/tables/template_code.py
+++ b/netbox/dcim/tables/template_code.py
@@ -15,6 +15,11 @@ CABLE_LENGTH = """
{% if record.length %}{{ record.length|simplify_decimal }} {{ record.length_unit }}{% endif %}
"""
+DEVICE_WEIGHT = """
+{% load helpers %}
+{% if record.weight %}{{ record.weight|simplify_decimal }} {{ record.weight_unit }}{% endif %}
+"""
+
DEVICE_LINK = """
{{ record.name|default:'Unnamed device' }}
diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py
index 7a745721b..92298bd73 100644
--- a/netbox/dcim/tests/test_filtersets.py
+++ b/netbox/dcim/tests/test_filtersets.py
@@ -409,9 +409,9 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
Tenant.objects.bulk_create(tenants)
racks = (
- Rack(name='Rack 1', facility_id='rack-1', site=sites[0], location=locations[0], tenant=tenants[0], status=RackStatusChoices.STATUS_ACTIVE, role=rack_roles[0], serial='ABC', asset_tag='1001', type=RackTypeChoices.TYPE_2POST, width=RackWidthChoices.WIDTH_19IN, u_height=42, desc_units=False, outer_width=100, outer_depth=100, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER),
- Rack(name='Rack 2', facility_id='rack-2', site=sites[1], location=locations[1], tenant=tenants[1], status=RackStatusChoices.STATUS_PLANNED, role=rack_roles[1], serial='DEF', asset_tag='1002', type=RackTypeChoices.TYPE_4POST, width=RackWidthChoices.WIDTH_21IN, u_height=43, desc_units=False, outer_width=200, outer_depth=200, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER),
- Rack(name='Rack 3', facility_id='rack-3', site=sites[2], location=locations[2], tenant=tenants[2], status=RackStatusChoices.STATUS_RESERVED, role=rack_roles[2], serial='GHI', asset_tag='1003', type=RackTypeChoices.TYPE_CABINET, width=RackWidthChoices.WIDTH_23IN, u_height=44, desc_units=True, outer_width=300, outer_depth=300, outer_unit=RackDimensionUnitChoices.UNIT_INCH),
+ Rack(name='Rack 1', facility_id='rack-1', site=sites[0], location=locations[0], tenant=tenants[0], status=RackStatusChoices.STATUS_ACTIVE, role=rack_roles[0], serial='ABC', asset_tag='1001', type=RackTypeChoices.TYPE_2POST, width=RackWidthChoices.WIDTH_19IN, u_height=42, desc_units=False, outer_width=100, outer_depth=100, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER, weight=10, weight_unit=WeightUnitChoices.UNIT_POUND),
+ Rack(name='Rack 2', facility_id='rack-2', site=sites[1], location=locations[1], tenant=tenants[1], status=RackStatusChoices.STATUS_PLANNED, role=rack_roles[1], serial='DEF', asset_tag='1002', type=RackTypeChoices.TYPE_4POST, width=RackWidthChoices.WIDTH_21IN, u_height=43, desc_units=False, outer_width=200, outer_depth=200, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER, weight=20, weight_unit=WeightUnitChoices.UNIT_POUND),
+ Rack(name='Rack 3', facility_id='rack-3', site=sites[2], location=locations[2], tenant=tenants[2], status=RackStatusChoices.STATUS_RESERVED, role=rack_roles[2], serial='GHI', asset_tag='1003', type=RackTypeChoices.TYPE_CABINET, width=RackWidthChoices.WIDTH_23IN, u_height=44, desc_units=True, outer_width=300, outer_depth=300, outer_unit=RackDimensionUnitChoices.UNIT_INCH, weight=30, weight_unit=WeightUnitChoices.UNIT_KILOGRAM),
)
Rack.objects.bulk_create(racks)
@@ -517,6 +517,14 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ def test_weight(self):
+ params = {'weight': [10, 20]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_weight_unit(self):
+ params = {'weight_unit': WeightUnitChoices.UNIT_POUND}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = RackReservation.objects.all()
@@ -688,9 +696,9 @@ class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
Manufacturer.objects.bulk_create(manufacturers)
device_types = (
- DeviceType(manufacturer=manufacturers[0], model='Model 1', slug='model-1', part_number='Part Number 1', u_height=1, is_full_depth=True, front_image='front.png', rear_image='rear.png'),
- DeviceType(manufacturer=manufacturers[1], model='Model 2', slug='model-2', part_number='Part Number 2', u_height=2, is_full_depth=True, subdevice_role=SubdeviceRoleChoices.ROLE_PARENT, airflow=DeviceAirflowChoices.AIRFLOW_FRONT_TO_REAR),
- DeviceType(manufacturer=manufacturers[2], model='Model 3', slug='model-3', part_number='Part Number 3', u_height=3, is_full_depth=False, subdevice_role=SubdeviceRoleChoices.ROLE_CHILD, airflow=DeviceAirflowChoices.AIRFLOW_REAR_TO_FRONT),
+ DeviceType(manufacturer=manufacturers[0], model='Model 1', slug='model-1', part_number='Part Number 1', u_height=1, is_full_depth=True, front_image='front.png', rear_image='rear.png', weight=10, weight_unit=WeightUnitChoices.UNIT_POUND),
+ DeviceType(manufacturer=manufacturers[1], model='Model 2', slug='model-2', part_number='Part Number 2', u_height=2, is_full_depth=True, subdevice_role=SubdeviceRoleChoices.ROLE_PARENT, airflow=DeviceAirflowChoices.AIRFLOW_FRONT_TO_REAR, weight=20, weight_unit=WeightUnitChoices.UNIT_POUND),
+ DeviceType(manufacturer=manufacturers[2], model='Model 3', slug='model-3', part_number='Part Number 3', u_height=3, is_full_depth=False, subdevice_role=SubdeviceRoleChoices.ROLE_CHILD, airflow=DeviceAirflowChoices.AIRFLOW_REAR_TO_FRONT, weight=30, weight_unit=WeightUnitChoices.UNIT_KILOGRAM),
)
DeviceType.objects.bulk_create(device_types)
@@ -839,6 +847,14 @@ class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'inventory_items': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ def test_weight(self):
+ params = {'weight': [10, 20]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_weight_unit(self):
+ params = {'weight_unit': WeightUnitChoices.UNIT_POUND}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = ModuleType.objects.all()
@@ -855,9 +871,9 @@ class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
Manufacturer.objects.bulk_create(manufacturers)
module_types = (
- ModuleType(manufacturer=manufacturers[0], model='Model 1', part_number='Part Number 1'),
- ModuleType(manufacturer=manufacturers[1], model='Model 2', part_number='Part Number 2'),
- ModuleType(manufacturer=manufacturers[2], model='Model 3', part_number='Part Number 3'),
+ ModuleType(manufacturer=manufacturers[0], model='Model 1', part_number='Part Number 1', weight=10, weight_unit=WeightUnitChoices.UNIT_POUND),
+ ModuleType(manufacturer=manufacturers[1], model='Model 2', part_number='Part Number 2', weight=20, weight_unit=WeightUnitChoices.UNIT_POUND),
+ ModuleType(manufacturer=manufacturers[2], model='Model 3', part_number='Part Number 3', weight=30, weight_unit=WeightUnitChoices.UNIT_KILOGRAM),
)
ModuleType.objects.bulk_create(module_types)
@@ -943,6 +959,14 @@ class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'pass_through_ports': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+ def test_weight(self):
+ params = {'weight': [10, 20]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_weight_unit(self):
+ params = {'weight_unit': WeightUnitChoices.UNIT_POUND}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
class ConsolePortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = ConsolePortTemplate.objects.all()
@@ -1646,6 +1670,8 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
device_types = DeviceType.objects.all()[:2]
params = {'device_type_id': [device_types[0].pk, device_types[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ params = {'device_type': [device_types[0].slug, device_types[1].slug]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_devicerole(self):
device_roles = DeviceRole.objects.all()[:2]
diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py
index 50b36e36d..d563dcfd6 100644
--- a/netbox/dcim/tests/test_views.py
+++ b/netbox/dcim/tests/test_views.py
@@ -1,6 +1,10 @@
from decimal import Decimal
+try:
+ from zoneinfo import ZoneInfo
+except ImportError:
+ # Python 3.8
+ from backports.zoneinfo import ZoneInfo
-import pytz
import yaml
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
@@ -12,7 +16,6 @@ from dcim.choices import *
from dcim.constants import *
from dcim.models import *
from ipam.models import ASN, RIR, VLAN, VRF
-from netbox.api.serializers import GenericObjectSerializer
from tenancy.models import Tenant
from utilities.testing import ViewTestCases, create_tags, create_test_device, post_data
from wireless.models import WirelessLAN
@@ -50,6 +53,13 @@ class RegionTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
"Region 6,region-6,Sixth region",
)
+ cls.csv_update_data = (
+ "id,name,description",
+ f"{regions[0].pk},Region 7,Fourth region7",
+ f"{regions[1].pk},Region 8,Fifth region8",
+ f"{regions[2].pk},Region 0,Sixth region9",
+ )
+
cls.bulk_edit_data = {
'description': 'New description',
}
@@ -87,6 +97,13 @@ class SiteGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
"Site Group 6,site-group-6,Sixth site group",
)
+ cls.csv_update_data = (
+ "id,name,description",
+ f"{sitegroups[0].pk},Site Group 7,Fourth site group7",
+ f"{sitegroups[1].pk},Site Group 8,Fifth site group8",
+ f"{sitegroups[2].pk},Site Group 0,Sixth site group9",
+ )
+
cls.bulk_edit_data = {
'description': 'New description',
}
@@ -139,7 +156,7 @@ class SiteTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'tenant': None,
'facility': 'Facility X',
'asns': [asns[6].pk, asns[7].pk],
- 'time_zone': pytz.UTC,
+ 'time_zone': ZoneInfo('UTC'),
'description': 'Site description',
'physical_address': '742 Evergreen Terrace, Springfield, USA',
'shipping_address': '742 Evergreen Terrace, Springfield, USA',
@@ -156,12 +173,19 @@ class SiteTestCase(ViewTestCases.PrimaryObjectViewTestCase):
"Site 6,site-6,staging",
)
+ cls.csv_update_data = (
+ "id,name,status",
+ f"{sites[0].pk},Site 7,staging",
+ f"{sites[1].pk},Site 8,planned",
+ f"{sites[2].pk},Site 9,active",
+ )
+
cls.bulk_edit_data = {
'status': SiteStatusChoices.STATUS_PLANNED,
'region': regions[1].pk,
'group': groups[1].pk,
'tenant': None,
- 'time_zone': pytz.timezone('US/Eastern'),
+ 'time_zone': ZoneInfo('US/Eastern'),
'description': 'New description',
}
@@ -202,6 +226,13 @@ class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
"Site 1,Tenant 1,Location 6,location-6,planned,Sixth location",
)
+ cls.csv_update_data = (
+ "id,name,description",
+ f"{locations[0].pk},Location 7,Fourth location7",
+ f"{locations[1].pk},Location 8,Fifth location8",
+ f"{locations[2].pk},Location 0,Sixth location9",
+ )
+
cls.bulk_edit_data = {
'description': 'New description',
}
@@ -213,11 +244,12 @@ class RackRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
@classmethod
def setUpTestData(cls):
- RackRole.objects.bulk_create([
+ rack_roles = (
RackRole(name='Rack Role 1', slug='rack-role-1'),
RackRole(name='Rack Role 2', slug='rack-role-2'),
RackRole(name='Rack Role 3', slug='rack-role-3'),
- ])
+ )
+ RackRole.objects.bulk_create(rack_roles)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
@@ -236,6 +268,13 @@ class RackRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
"Rack Role 6,rack-role-6,0000ff",
)
+ cls.csv_update_data = (
+ "id,name,description",
+ f"{rack_roles[0].pk},Rack Role 7,New description7",
+ f"{rack_roles[1].pk},Rack Role 8,New description8",
+ f"{rack_roles[2].pk},Rack Role 9,New description9",
+ )
+
cls.bulk_edit_data = {
'color': '00ff00',
'description': 'New description',
@@ -259,11 +298,12 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
rack = Rack(name='Rack 1', site=site, location=location)
rack.save()
- RackReservation.objects.bulk_create([
+ rack_reservations = (
RackReservation(rack=rack, user=user2, units=[1, 2, 3], description='Reservation 1'),
RackReservation(rack=rack, user=user2, units=[4, 5, 6], description='Reservation 2'),
RackReservation(rack=rack, user=user2, units=[7, 8, 9], description='Reservation 3'),
- ])
+ )
+ RackReservation.objects.bulk_create(rack_reservations)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
@@ -283,6 +323,13 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'Site 1,Location 1,Rack 1,"16,17,18",Reservation 3',
)
+ cls.csv_update_data = (
+ 'id,description',
+ f'{rack_reservations[0].pk},New description1',
+ f'{rack_reservations[1].pk},New description2',
+ f'{rack_reservations[2].pk},New description3',
+ )
+
cls.bulk_edit_data = {
'user': user3.pk,
'tenant': None,
@@ -315,11 +362,12 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
)
RackRole.objects.bulk_create(rackroles)
- Rack.objects.bulk_create((
+ racks = (
Rack(name='Rack 1', site=sites[0]),
Rack(name='Rack 2', site=sites[0]),
Rack(name='Rack 3', site=sites[0]),
- ))
+ )
+ Rack.objects.bulk_create(racks)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
@@ -351,6 +399,13 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
"Site 2,Location 2,Rack 6,active,19,42",
)
+ cls.csv_update_data = (
+ "id,name,status",
+ f"{racks[0].pk},Rack 7,{RackStatusChoices.STATUS_DEPRECATED}",
+ f"{racks[1].pk},Rack 8,{RackStatusChoices.STATUS_DEPRECATED}",
+ f"{racks[2].pk},Rack 9,{RackStatusChoices.STATUS_DEPRECATED}",
+ )
+
cls.bulk_edit_data = {
'site': sites[1].pk,
'location': locations[1].pk,
@@ -383,11 +438,12 @@ class ManufacturerTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
@classmethod
def setUpTestData(cls):
- Manufacturer.objects.bulk_create([
+ manufacturers = (
Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
Manufacturer(name='Manufacturer 3', slug='manufacturer-3'),
- ])
+ )
+ Manufacturer.objects.bulk_create(manufacturers)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
@@ -405,6 +461,13 @@ class ManufacturerTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
"Manufacturer 6,manufacturer-6,Sixth manufacturer",
)
+ cls.csv_update_data = (
+ "id,name,description",
+ f"{manufacturers[0].pk},Manufacturer 7,Fourth manufacturer7",
+ f"{manufacturers[1].pk},Manufacturer 8,Fifth manufacturer8",
+ f"{manufacturers[2].pk},Manufacturer 9,Sixth manufacturer9",
+ )
+
cls.bulk_edit_data = {
'description': 'New description',
}
@@ -1444,11 +1507,12 @@ class DeviceRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
@classmethod
def setUpTestData(cls):
- DeviceRole.objects.bulk_create([
+ device_roles = (
DeviceRole(name='Device Role 1', slug='device-role-1'),
DeviceRole(name='Device Role 2', slug='device-role-2'),
DeviceRole(name='Device Role 3', slug='device-role-3'),
- ])
+ )
+ DeviceRole.objects.bulk_create(device_roles)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
@@ -1468,6 +1532,13 @@ class DeviceRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
"Device Role 6,device-role-6,0000ff",
)
+ cls.csv_update_data = (
+ "id,name,description",
+ f"{device_roles[0].pk},Device Role 7,New description7",
+ f"{device_roles[1].pk},Device Role 8,New description8",
+ f"{device_roles[2].pk},Device Role 9,New description9",
+ )
+
cls.bulk_edit_data = {
'color': '00ff00',
'description': 'New description',
@@ -1482,11 +1553,12 @@ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
- Platform.objects.bulk_create([
+ platforms = (
Platform(name='Platform 1', slug='platform-1', manufacturer=manufacturer),
Platform(name='Platform 2', slug='platform-2', manufacturer=manufacturer),
Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturer),
- ])
+ )
+ Platform.objects.bulk_create(platforms)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
@@ -1507,6 +1579,13 @@ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
"Platform 6,platform-6,Sixth platform",
)
+ cls.csv_update_data = (
+ "id,name,description",
+ f"{platforms[0].pk},Platform 7,Fourth platform7",
+ f"{platforms[1].pk},Platform 8,Fifth platform8",
+ f"{platforms[2].pk},Platform 9,Sixth platform9",
+ )
+
cls.bulk_edit_data = {
'napalm_driver': 'ios',
'description': 'New description',
@@ -1554,11 +1633,12 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
)
Platform.objects.bulk_create(platforms)
- Device.objects.bulk_create([
+ devices = (
Device(name='Device 1', site=sites[0], rack=racks[0], device_type=devicetypes[0], device_role=deviceroles[0], platform=platforms[0]),
Device(name='Device 2', site=sites[0], rack=racks[0], device_type=devicetypes[0], device_role=deviceroles[0], platform=platforms[0]),
Device(name='Device 3', site=sites[0], rack=racks[0], device_type=devicetypes[0], device_role=deviceroles[0], platform=platforms[0]),
- ])
+ )
+ Device.objects.bulk_create(devices)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
@@ -1595,6 +1675,13 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
"Device Role 1,Manufacturer 1,Device Type 1,active,Device 6,Site 1,Location 1,Rack 1,30,front,Virtual Chassis 1,3,30",
)
+ cls.csv_update_data = (
+ "id,status",
+ f"{devices[0].pk},{DeviceStatusChoices.STATUS_DECOMMISSIONING}",
+ f"{devices[1].pk},{DeviceStatusChoices.STATUS_DECOMMISSIONING}",
+ f"{devices[2].pk},{DeviceStatusChoices.STATUS_DECOMMISSIONING}",
+ )
+
cls.bulk_edit_data = {
'device_type': devicetypes[1].pk,
'device_role': deviceroles[1].pk,
@@ -1778,10 +1865,12 @@ class ModuleTestCase(
ModuleBay(device=devices[0], name='Module Bay 2'),
ModuleBay(device=devices[0], name='Module Bay 3'),
ModuleBay(device=devices[0], name='Module Bay 4'),
+ ModuleBay(device=devices[0], name='Module Bay 5'),
ModuleBay(device=devices[1], name='Module Bay 1'),
ModuleBay(device=devices[1], name='Module Bay 2'),
ModuleBay(device=devices[1], name='Module Bay 3'),
ModuleBay(device=devices[1], name='Module Bay 4'),
+ ModuleBay(device=devices[1], name='Module Bay 5'),
)
ModuleBay.objects.bulk_create(module_bays)
@@ -1795,7 +1884,7 @@ class ModuleTestCase(
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
- 'device': devices[1].pk,
+ 'device': devices[0].pk,
'module_bay': module_bays[3].pk,
'module_type': module_types[0].pk,
'serial': 'A',
@@ -1813,6 +1902,13 @@ class ModuleTestCase(
"Device 2,Module Bay 3,Module Type 3,C,C",
)
+ cls.csv_update_data = (
+ "id,serial",
+ f"{modules[0].pk},Serial 2",
+ f"{modules[1].pk},Serial 3",
+ f"{modules[2].pk},Serial 1",
+ )
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_module_component_replication(self):
self.add_permissions('dcim.add_module')
@@ -1867,7 +1963,6 @@ class ModuleTestCase(
self.assertIsNone(interface.module)
# Create a module with adopted components
- form_data['module_bay'] = ModuleBay.objects.filter(device=device).first()
form_data['module_type'] = module_type
form_data['replicate_components'] = False
form_data['adopt_components'] = True
@@ -1893,11 +1988,12 @@ class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
def setUpTestData(cls):
device = create_test_device('Device 1')
- ConsolePort.objects.bulk_create([
+ console_ports = (
ConsolePort(device=device, name='Console Port 1'),
ConsolePort(device=device, name='Console Port 2'),
ConsolePort(device=device, name='Console Port 3'),
- ])
+ )
+ ConsolePort.objects.bulk_create(console_ports)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
@@ -1931,6 +2027,13 @@ class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
"Device 1,Console Port 6",
)
+ cls.csv_update_data = (
+ "id,name,description",
+ f"{console_ports[0].pk},Console Port 7,New description7",
+ f"{console_ports[1].pk},Console Port 8,New description8",
+ f"{console_ports[2].pk},Console Port 9,New description9",
+ )
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_trace(self):
consoleport = ConsolePort.objects.first()
@@ -1952,11 +2055,12 @@ class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
def setUpTestData(cls):
device = create_test_device('Device 1')
- ConsoleServerPort.objects.bulk_create([
+ console_server_ports = (
ConsoleServerPort(device=device, name='Console Server Port 1'),
ConsoleServerPort(device=device, name='Console Server Port 2'),
ConsoleServerPort(device=device, name='Console Server Port 3'),
- ])
+ )
+ ConsoleServerPort.objects.bulk_create(console_server_ports)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
@@ -1988,6 +2092,13 @@ class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
"Device 1,Console Server Port 6",
)
+ cls.csv_update_data = (
+ "id,name,description",
+ f"{console_server_ports[0].pk},Console Server Port 7,New description 7",
+ f"{console_server_ports[1].pk},Console Server Port 8,New description 8",
+ f"{console_server_ports[2].pk},Console Server Port 9,New description 9",
+ )
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_trace(self):
consoleserverport = ConsoleServerPort.objects.first()
@@ -2009,11 +2120,12 @@ class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
def setUpTestData(cls):
device = create_test_device('Device 1')
- PowerPort.objects.bulk_create([
+ power_ports = (
PowerPort(device=device, name='Power Port 1'),
PowerPort(device=device, name='Power Port 2'),
PowerPort(device=device, name='Power Port 3'),
- ])
+ )
+ PowerPort.objects.bulk_create(power_ports)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
@@ -2051,6 +2163,13 @@ class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
"Device 1,Power Port 6",
)
+ cls.csv_update_data = (
+ "id,name,description",
+ f"{power_ports[0].pk},Power Port 7,New description7",
+ f"{power_ports[1].pk},Power Port 8,New description8",
+ f"{power_ports[2].pk},Power Port 9,New description9",
+ )
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_trace(self):
powerport = PowerPort.objects.first()
@@ -2078,11 +2197,12 @@ class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
)
PowerPort.objects.bulk_create(powerports)
- PowerOutlet.objects.bulk_create([
+ power_outlets = (
PowerOutlet(device=device, name='Power Outlet 1', power_port=powerports[0]),
PowerOutlet(device=device, name='Power Outlet 2', power_port=powerports[0]),
PowerOutlet(device=device, name='Power Outlet 3', power_port=powerports[0]),
- ])
+ )
+ PowerOutlet.objects.bulk_create(power_outlets)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
@@ -2120,6 +2240,13 @@ class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
"Device 1,Power Outlet 6",
)
+ cls.csv_update_data = (
+ "id,name,description",
+ f"{power_outlets[0].pk},Power Outlet 7,New description7",
+ f"{power_outlets[1].pk},Power Outlet 8,New description8",
+ f"{power_outlets[2].pk},Power Outlet 9,New description9",
+ )
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_trace(self):
poweroutlet = PowerOutlet.objects.first()
@@ -2246,6 +2373,13 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
f"Device 1,Interface 6,1000base-t,{vrfs[0].pk},pse,type1-ieee802.3af",
)
+ cls.csv_update_data = (
+ "id,name,description",
+ f"{interfaces[0].pk},Interface 7,New description7",
+ f"{interfaces[1].pk},Interface 8,New description8",
+ f"{interfaces[2].pk},Interface 9,New description9",
+ )
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_trace(self):
interface1, interface2 = Interface.objects.all()[:2]
@@ -2273,11 +2407,12 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
)
RearPort.objects.bulk_create(rearports)
- FrontPort.objects.bulk_create([
+ front_ports = (
FrontPort(device=device, name='Front Port 1', rear_port=rearports[0]),
FrontPort(device=device, name='Front Port 2', rear_port=rearports[1]),
FrontPort(device=device, name='Front Port 3', rear_port=rearports[2]),
- ])
+ )
+ FrontPort.objects.bulk_create(front_ports)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
@@ -2312,6 +2447,13 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
"Device 1,Front Port 6,8p8c,Rear Port 6,1",
)
+ cls.csv_update_data = (
+ "id,name,description",
+ f"{front_ports[0].pk},Front Port 7,New description7",
+ f"{front_ports[1].pk},Front Port 8,New description8",
+ f"{front_ports[2].pk},Front Port 9,New description9",
+ )
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_trace(self):
frontport = FrontPort.objects.first()
@@ -2333,11 +2475,12 @@ class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
def setUpTestData(cls):
device = create_test_device('Device 1')
- RearPort.objects.bulk_create([
+ rear_ports = (
RearPort(device=device, name='Rear Port 1'),
RearPort(device=device, name='Rear Port 2'),
RearPort(device=device, name='Rear Port 3'),
- ])
+ )
+ RearPort.objects.bulk_create(rear_ports)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
@@ -2371,6 +2514,13 @@ class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
"Device 1,Rear Port 6,8p8c,1",
)
+ cls.csv_update_data = (
+ "id,name,description",
+ f"{rear_ports[0].pk},Rear Port 7,New description7",
+ f"{rear_ports[1].pk},Rear Port 8,New description8",
+ f"{rear_ports[2].pk},Rear Port 9,New description9",
+ )
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_trace(self):
rearport = RearPort.objects.first()
@@ -2392,11 +2542,12 @@ class ModuleBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
def setUpTestData(cls):
device = create_test_device('Device 1')
- ModuleBay.objects.bulk_create([
+ module_bays = (
ModuleBay(device=device, name='Module Bay 1'),
ModuleBay(device=device, name='Module Bay 2'),
ModuleBay(device=device, name='Module Bay 3'),
- ])
+ )
+ ModuleBay.objects.bulk_create(module_bays)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
@@ -2425,6 +2576,13 @@ class ModuleBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
"Device 1,Module Bay 6",
)
+ cls.csv_update_data = (
+ "id,name,description",
+ f"{module_bays[0].pk},Module Bay 7,New description7",
+ f"{module_bays[1].pk},Module Bay 8,New description8",
+ f"{module_bays[2].pk},Module Bay 9,New description9",
+ )
+
class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
model = DeviceBay
@@ -2437,11 +2595,12 @@ class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
# Update the DeviceType subdevice role to allow adding DeviceBays
DeviceType.objects.update(subdevice_role=SubdeviceRoleChoices.ROLE_PARENT)
- DeviceBay.objects.bulk_create([
+ device_bays = (
DeviceBay(device=device, name='Device Bay 1'),
DeviceBay(device=device, name='Device Bay 2'),
DeviceBay(device=device, name='Device Bay 3'),
- ])
+ )
+ DeviceBay.objects.bulk_create(device_bays)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
@@ -2470,6 +2629,13 @@ class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
"Device 1,Device Bay 6",
)
+ cls.csv_update_data = (
+ "id,name,description",
+ f"{device_bays[0].pk},Device Bay 7,New description7",
+ f"{device_bays[1].pk},Device Bay 8,New description8",
+ f"{device_bays[2].pk},Device Bay 9,New description9",
+ )
+
class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
model = InventoryItem
@@ -2486,9 +2652,9 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
)
InventoryItemRole.objects.bulk_create(roles)
- InventoryItem.objects.create(device=device, name='Inventory Item 1', role=roles[0], manufacturer=manufacturer)
- InventoryItem.objects.create(device=device, name='Inventory Item 2', role=roles[0], manufacturer=manufacturer)
- InventoryItem.objects.create(device=device, name='Inventory Item 3', role=roles[0], manufacturer=manufacturer)
+ inventory_item1 = InventoryItem.objects.create(device=device, name='Inventory Item 1', role=roles[0], manufacturer=manufacturer)
+ inventory_item2 = InventoryItem.objects.create(device=device, name='Inventory Item 2', role=roles[0], manufacturer=manufacturer)
+ inventory_item3 = InventoryItem.objects.create(device=device, name='Inventory Item 3', role=roles[0], manufacturer=manufacturer)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
@@ -2532,6 +2698,13 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
"Device 1,Inventory Item 6,Inventory Item 3",
)
+ cls.csv_update_data = (
+ "id,name,description",
+ f"{inventory_item1.pk},Inventory Item 7,New description7",
+ f"{inventory_item2.pk},Inventory Item 8,New description8",
+ f"{inventory_item3.pk},Inventory Item 9,New description9",
+ )
+
class InventoryItemRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
model = InventoryItemRole
@@ -2539,11 +2712,12 @@ class InventoryItemRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
@classmethod
def setUpTestData(cls):
- InventoryItemRole.objects.bulk_create([
+ inventory_item_roles = (
InventoryItemRole(name='Inventory Item Role 1', slug='inventory-item-role-1'),
InventoryItemRole(name='Inventory Item Role 2', slug='inventory-item-role-2'),
InventoryItemRole(name='Inventory Item Role 3', slug='inventory-item-role-3'),
- ])
+ )
+ InventoryItemRole.objects.bulk_create(inventory_item_roles)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
@@ -2562,6 +2736,13 @@ class InventoryItemRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
"Inventory Item Role 6,inventory-item-role-6,0000ff",
)
+ cls.csv_update_data = (
+ "id,name,description",
+ f"{inventory_item_roles[0].pk},Inventory Item Role 7,New description7",
+ f"{inventory_item_roles[1].pk},Inventory Item Role 8,New description8",
+ f"{inventory_item_roles[2].pk},Inventory Item Role 9,New description9",
+ )
+
cls.bulk_edit_data = {
'color': '00ff00',
'description': 'New description',
@@ -2614,9 +2795,12 @@ class CableTestCase(
)
Interface.objects.bulk_create(interfaces)
- Cable(a_terminations=[interfaces[0]], b_terminations=[interfaces[3]], type=CableTypeChoices.TYPE_CAT6).save()
- Cable(a_terminations=[interfaces[1]], b_terminations=[interfaces[4]], type=CableTypeChoices.TYPE_CAT6).save()
- Cable(a_terminations=[interfaces[2]], b_terminations=[interfaces[5]], type=CableTypeChoices.TYPE_CAT6).save()
+ cable1 = Cable(a_terminations=[interfaces[0]], b_terminations=[interfaces[3]], type=CableTypeChoices.TYPE_CAT6)
+ cable1.save()
+ cable2 = Cable(a_terminations=[interfaces[1]], b_terminations=[interfaces[4]], type=CableTypeChoices.TYPE_CAT6)
+ cable2.save()
+ cable3 = Cable(a_terminations=[interfaces[2]], b_terminations=[interfaces[5]], type=CableTypeChoices.TYPE_CAT6)
+ cable3.save()
tags = create_tags('Alpha', 'Bravo', 'Charlie')
@@ -2642,6 +2826,13 @@ class CableTestCase(
"Device 3,dcim.interface,Interface 3,Device 4,dcim.interface,Interface 3",
)
+ cls.csv_update_data = (
+ "id,label,color",
+ f"{cable1.pk},New label7,00ff00",
+ f"{cable2.pk},New label8,00ff00",
+ f"{cable3.pk},New label9,00ff00",
+ )
+
cls.bulk_edit_data = {
'type': CableTypeChoices.TYPE_CAT5E,
'status': LinkStatusChoices.STATUS_CONNECTED,
@@ -2725,6 +2916,13 @@ class VirtualChassisTestCase(ViewTestCases.PrimaryObjectViewTestCase):
"VC6,Domain 6,Device 12",
)
+ cls.csv_update_data = (
+ "id,name,domain",
+ f"{vc1.pk},VC7,Domain 7",
+ f"{vc2.pk},VC8,Domain 8",
+ f"{vc3.pk},VC9,Domain 9",
+ )
+
cls.bulk_edit_data = {
'domain': 'domain-x',
}
@@ -2749,11 +2947,12 @@ class PowerPanelTestCase(ViewTestCases.PrimaryObjectViewTestCase):
for location in locations:
location.save()
- PowerPanel.objects.bulk_create((
+ power_panels = (
PowerPanel(site=sites[0], location=locations[0], name='Power Panel 1'),
PowerPanel(site=sites[0], location=locations[0], name='Power Panel 2'),
PowerPanel(site=sites[0], location=locations[0], name='Power Panel 3'),
- ))
+ )
+ PowerPanel.objects.bulk_create(power_panels)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
@@ -2771,6 +2970,13 @@ class PowerPanelTestCase(ViewTestCases.PrimaryObjectViewTestCase):
"Site 1,Location 1,Power Panel 6",
)
+ cls.csv_update_data = (
+ "id,name",
+ f"{power_panels[0].pk},Power Panel 7",
+ f"{power_panels[1].pk},Power Panel 8",
+ f"{power_panels[2].pk},Power Panel 9",
+ )
+
cls.bulk_edit_data = {
'site': sites[1].pk,
'location': locations[1].pk,
@@ -2797,11 +3003,12 @@ class PowerFeedTestCase(ViewTestCases.PrimaryObjectViewTestCase):
)
Rack.objects.bulk_create(racks)
- PowerFeed.objects.bulk_create((
+ power_feeds = (
PowerFeed(name='Power Feed 1', power_panel=powerpanels[0], rack=racks[0]),
PowerFeed(name='Power Feed 2', power_panel=powerpanels[0], rack=racks[0]),
PowerFeed(name='Power Feed 3', power_panel=powerpanels[0], rack=racks[0]),
- ))
+ )
+ PowerFeed.objects.bulk_create(power_feeds)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
@@ -2827,6 +3034,13 @@ class PowerFeedTestCase(ViewTestCases.PrimaryObjectViewTestCase):
"Site 1,Power Panel 1,Power Feed 6,active,primary,ac,single-phase,120,20,80",
)
+ cls.csv_update_data = (
+ "id,name,status",
+ f"{power_feeds[0].pk},Power Feed 7,{PowerFeedStatusChoices.STATUS_PLANNED}",
+ f"{power_feeds[1].pk},Power Feed 8,{PowerFeedStatusChoices.STATUS_PLANNED}",
+ f"{power_feeds[2].pk},Power Feed 9,{PowerFeedStatusChoices.STATUS_PLANNED}",
+ )
+
cls.bulk_edit_data = {
'power_panel': powerpanels[1].pk,
'rack': racks[1].pk,
diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py
index c11a92a99..ecd2d46c5 100644
--- a/netbox/dcim/urls.py
+++ b/netbox/dcim/urls.py
@@ -1,8 +1,7 @@
-from django.urls import path
+from django.urls import include, path
-from netbox.views.generic import ObjectChangeLogView, ObjectJournalView
+from utilities.urls import get_model_urls
from . import views
-from .models import *
app_name = 'dcim'
urlpatterns = [
@@ -13,10 +12,7 @@ urlpatterns = [
path('regions/import/', views.RegionBulkImportView.as_view(), name='region_import'),
path('regions/edit/', views.RegionBulkEditView.as_view(), name='region_bulk_edit'),
path('regions/delete/', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'),
- path('regions//', views.RegionView.as_view(), name='region'),
- path('regions//edit/', views.RegionEditView.as_view(), name='region_edit'),
- path('regions//delete/', views.RegionDeleteView.as_view(), name='region_delete'),
- path('regions//changelog/', ObjectChangeLogView.as_view(), name='region_changelog', kwargs={'model': Region}),
+ path('regions//', include(get_model_urls('dcim', 'region'))),
# Site groups
path('site-groups/', views.SiteGroupListView.as_view(), name='sitegroup_list'),
@@ -24,10 +20,7 @@ urlpatterns = [
path('site-groups/import/', views.SiteGroupBulkImportView.as_view(), name='sitegroup_import'),
path('site-groups/edit/', views.SiteGroupBulkEditView.as_view(), name='sitegroup_bulk_edit'),
path('site-groups/delete/', views.SiteGroupBulkDeleteView.as_view(), name='sitegroup_bulk_delete'),
- path('site-groups//', views.SiteGroupView.as_view(), name='sitegroup'),
- path('site-groups//edit/', views.SiteGroupEditView.as_view(), name='sitegroup_edit'),
- path('site-groups//delete/', views.SiteGroupDeleteView.as_view(), name='sitegroup_delete'),
- path('site-groups//changelog/', ObjectChangeLogView.as_view(), name='sitegroup_changelog', kwargs={'model': SiteGroup}),
+ path('site-groups//', include(get_model_urls('dcim', 'sitegroup'))),
# Sites
path('sites/', views.SiteListView.as_view(), name='site_list'),
@@ -35,11 +28,7 @@ urlpatterns = [
path('sites/import/', views.SiteBulkImportView.as_view(), name='site_import'),
path('sites/edit/', views.SiteBulkEditView.as_view(), name='site_bulk_edit'),
path('sites/delete/', views.SiteBulkDeleteView.as_view(), name='site_bulk_delete'),
- path('sites//', views.SiteView.as_view(), name='site'),
- path('sites//edit/', views.SiteEditView.as_view(), name='site_edit'),
- path('sites//delete/', views.SiteDeleteView.as_view(), name='site_delete'),
- path('sites//changelog/', ObjectChangeLogView.as_view(), name='site_changelog', kwargs={'model': Site}),
- path('sites//journal/', ObjectJournalView.as_view(), name='site_journal', kwargs={'model': Site}),
+ path('sites//', include(get_model_urls('dcim', 'site'))),
# Locations
path('locations/', views.LocationListView.as_view(), name='location_list'),
@@ -47,10 +36,7 @@ urlpatterns = [
path('locations/import/', views.LocationBulkImportView.as_view(), name='location_import'),
path('locations/edit/', views.LocationBulkEditView.as_view(), name='location_bulk_edit'),
path('locations/delete/', views.LocationBulkDeleteView.as_view(), name='location_bulk_delete'),
- path('locations//', views.LocationView.as_view(), name='location'),
- path('locations//edit/', views.LocationEditView.as_view(), name='location_edit'),
- path('locations//delete/', views.LocationDeleteView.as_view(), name='location_delete'),
- path('locations//changelog/', ObjectChangeLogView.as_view(), name='location_changelog', kwargs={'model': Location}),
+ path('locations//', include(get_model_urls('dcim', 'location'))),
# Rack roles
path('rack-roles/', views.RackRoleListView.as_view(), name='rackrole_list'),
@@ -58,10 +44,7 @@ urlpatterns = [
path('rack-roles/import/', views.RackRoleBulkImportView.as_view(), name='rackrole_import'),
path('rack-roles/edit/', views.RackRoleBulkEditView.as_view(), name='rackrole_bulk_edit'),
path('rack-roles/delete/', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'),
- path('rack-roles//', views.RackRoleView.as_view(), name='rackrole'),
- path('rack-roles//edit/', views.RackRoleEditView.as_view(), name='rackrole_edit'),
- path('rack-roles//delete/', views.RackRoleDeleteView.as_view(), name='rackrole_delete'),
- path('rack-roles//changelog/', ObjectChangeLogView.as_view(), name='rackrole_changelog', kwargs={'model': RackRole}),
+ path('rack-roles//', include(get_model_urls('dcim', 'rackrole'))),
# Rack reservations
path('rack-reservations/', views.RackReservationListView.as_view(), name='rackreservation_list'),
@@ -69,11 +52,7 @@ urlpatterns = [
path('rack-reservations/import/', views.RackReservationImportView.as_view(), name='rackreservation_import'),
path('rack-reservations/edit/', views.RackReservationBulkEditView.as_view(), name='rackreservation_bulk_edit'),
path('rack-reservations/delete/', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'),
- path('rack-reservations//', views.RackReservationView.as_view(), name='rackreservation'),
- path('rack-reservations//edit/', views.RackReservationEditView.as_view(), name='rackreservation_edit'),
- path('rack-reservations//delete/', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'),
- path('rack-reservations//changelog/', ObjectChangeLogView.as_view(), name='rackreservation_changelog', kwargs={'model': RackReservation}),
- path('rack-reservations//journal/', ObjectJournalView.as_view(), name='rackreservation_journal', kwargs={'model': RackReservation}),
+ path('rack-reservations//', include(get_model_urls('dcim', 'rackreservation'))),
# Racks
path('racks/', views.RackListView.as_view(), name='rack_list'),
@@ -82,11 +61,7 @@ urlpatterns = [
path('racks/import/', views.RackBulkImportView.as_view(), name='rack_import'),
path('racks/edit/', views.RackBulkEditView.as_view(), name='rack_bulk_edit'),
path('racks/delete/', views.RackBulkDeleteView.as_view(), name='rack_bulk_delete'),
- path('racks//', views.RackView.as_view(), name='rack'),
- path('racks//edit/', views.RackEditView.as_view(), name='rack_edit'),
- path('racks//delete/', views.RackDeleteView.as_view(), name='rack_delete'),
- path('racks//changelog/', ObjectChangeLogView.as_view(), name='rack_changelog', kwargs={'model': Rack}),
- path('racks//journal/', ObjectJournalView.as_view(), name='rack_journal', kwargs={'model': Rack}),
+ path('racks//', include(get_model_urls('dcim', 'rack'))),
# Manufacturers
path('manufacturers/', views.ManufacturerListView.as_view(), name='manufacturer_list'),
@@ -94,10 +69,7 @@ urlpatterns = [
path('manufacturers/import/', views.ManufacturerBulkImportView.as_view(), name='manufacturer_import'),
path('manufacturers/edit/', views.ManufacturerBulkEditView.as_view(), name='manufacturer_bulk_edit'),
path('manufacturers/delete/', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'),
- path('manufacturers//', views.ManufacturerView.as_view(), name='manufacturer'),
- path('manufacturers//edit/', views.ManufacturerEditView.as_view(), name='manufacturer_edit'),
- path('manufacturers//delete/', views.ManufacturerDeleteView.as_view(), name='manufacturer_delete'),
- path('manufacturers//changelog/', ObjectChangeLogView.as_view(), name='manufacturer_changelog', kwargs={'model': Manufacturer}),
+ path('manufacturers//', include(get_model_urls('dcim', 'manufacturer'))),
# Device types
path('device-types/', views.DeviceTypeListView.as_view(), name='devicetype_list'),
@@ -105,21 +77,7 @@ urlpatterns = [
path('device-types/import/', views.DeviceTypeImportView.as_view(), name='devicetype_import'),
path('device-types/edit/', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'),
path('device-types/delete/', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
- path('device-types//', views.DeviceTypeView.as_view(), name='devicetype'),
- path('device-types//console-ports/', views.DeviceTypeConsolePortsView.as_view(), name='devicetype_consoleports'),
- path('device-types//console-server-ports/', views.DeviceTypeConsoleServerPortsView.as_view(), name='devicetype_consoleserverports'),
- path('device-types//power-ports/', views.DeviceTypePowerPortsView.as_view(), name='devicetype_powerports'),
- path('device-types//power-outlets/', views.DeviceTypePowerOutletsView.as_view(), name='devicetype_poweroutlets'),
- path('device-types//interfaces/', views.DeviceTypeInterfacesView.as_view(), name='devicetype_interfaces'),
- path('device-types//front-ports/', views.DeviceTypeFrontPortsView.as_view(), name='devicetype_frontports'),
- path('device-types//rear-ports/', views.DeviceTypeRearPortsView.as_view(), name='devicetype_rearports'),
- path('device-types//module-bays/', views.DeviceTypeModuleBaysView.as_view(), name='devicetype_modulebays'),
- path('device-types//device-bays/', views.DeviceTypeDeviceBaysView.as_view(), name='devicetype_devicebays'),
- path('device-types//inventory-items/', views.DeviceTypeInventoryItemsView.as_view(), name='devicetype_inventoryitems'),
- path('device-types//edit/', views.DeviceTypeEditView.as_view(), name='devicetype_edit'),
- path('device-types//delete/', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
- path('device-types//changelog/', ObjectChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}),
- path('device-types//journal/', ObjectJournalView.as_view(), name='devicetype_journal', kwargs={'model': DeviceType}),
+ path('device-types//', include(get_model_urls('dcim', 'devicetype'))),
# Module types
path('module-types/', views.ModuleTypeListView.as_view(), name='moduletype_list'),
@@ -127,98 +85,77 @@ urlpatterns = [
path('module-types/import/', views.ModuleTypeImportView.as_view(), name='moduletype_import'),
path('module-types/edit/', views.ModuleTypeBulkEditView.as_view(), name='moduletype_bulk_edit'),
path('module-types/delete/', views.ModuleTypeBulkDeleteView.as_view(), name='moduletype_bulk_delete'),
- path('module-types//', views.ModuleTypeView.as_view(), name='moduletype'),
- path('module-types//console-ports/', views.ModuleTypeConsolePortsView.as_view(), name='moduletype_consoleports'),
- path('module-types//console-server-ports/', views.ModuleTypeConsoleServerPortsView.as_view(), name='moduletype_consoleserverports'),
- path('module-types//power-ports/', views.ModuleTypePowerPortsView.as_view(), name='moduletype_powerports'),
- path('module-types//power-outlets/', views.ModuleTypePowerOutletsView.as_view(), name='moduletype_poweroutlets'),
- path('module-types//interfaces/', views.ModuleTypeInterfacesView.as_view(), name='moduletype_interfaces'),
- path('module-types//front-ports/', views.ModuleTypeFrontPortsView.as_view(), name='moduletype_frontports'),
- path('module-types//rear-ports/', views.ModuleTypeRearPortsView.as_view(), name='moduletype_rearports'),
- path('module-types//edit/', views.ModuleTypeEditView.as_view(), name='moduletype_edit'),
- path('module-types//delete/', views.ModuleTypeDeleteView.as_view(), name='moduletype_delete'),
- path('module-types//changelog/', ObjectChangeLogView.as_view(), name='moduletype_changelog', kwargs={'model': ModuleType}),
- path('module-types//journal/', ObjectJournalView.as_view(), name='moduletype_journal', kwargs={'model': ModuleType}),
+ path('module-types//', include(get_model_urls('dcim', 'moduletype'))),
# Console port templates
path('console-port-templates/add/', views.ConsolePortTemplateCreateView.as_view(), name='consoleporttemplate_add'),
path('console-port-templates/edit/', views.ConsolePortTemplateBulkEditView.as_view(), name='consoleporttemplate_bulk_edit'),
path('console-port-templates/rename/', views.ConsolePortTemplateBulkRenameView.as_view(), name='consoleporttemplate_bulk_rename'),
path('console-port-templates/delete/', views.ConsolePortTemplateBulkDeleteView.as_view(), name='consoleporttemplate_bulk_delete'),
- path('console-port-templates//edit/', views.ConsolePortTemplateEditView.as_view(), name='consoleporttemplate_edit'),
- path('console-port-templates//delete/', views.ConsolePortTemplateDeleteView.as_view(), name='consoleporttemplate_delete'),
+ path('console-port-templates//', include(get_model_urls('dcim', 'consoleporttemplate'))),
# Console server port templates
path('console-server-port-templates/add/', views.ConsoleServerPortTemplateCreateView.as_view(), name='consoleserverporttemplate_add'),
path('console-server-port-templates/edit/', views.ConsoleServerPortTemplateBulkEditView.as_view(), name='consoleserverporttemplate_bulk_edit'),
path('console-server-port-templates/rename/', views.ConsoleServerPortTemplateBulkRenameView.as_view(), name='consoleserverporttemplate_bulk_rename'),
path('console-server-port-templates/delete/', views.ConsoleServerPortTemplateBulkDeleteView.as_view(), name='consoleserverporttemplate_bulk_delete'),
- path('console-server-port-templates//edit/', views.ConsoleServerPortTemplateEditView.as_view(), name='consoleserverporttemplate_edit'),
- path('console-server-port-templates//delete/', views.ConsoleServerPortTemplateDeleteView.as_view(), name='consoleserverporttemplate_delete'),
+ path('console-server-port-templates//', include(get_model_urls('dcim', 'consoleserverporttemplate'))),
# Power port templates
path('power-port-templates/add/', views.PowerPortTemplateCreateView.as_view(), name='powerporttemplate_add'),
path('power-port-templates/edit/', views.PowerPortTemplateBulkEditView.as_view(), name='powerporttemplate_bulk_edit'),
path('power-port-templates/rename/', views.PowerPortTemplateBulkRenameView.as_view(), name='powerporttemplate_bulk_rename'),
path('power-port-templates/delete/', views.PowerPortTemplateBulkDeleteView.as_view(), name='powerporttemplate_bulk_delete'),
- path('power-port-templates//edit/', views.PowerPortTemplateEditView.as_view(), name='powerporttemplate_edit'),
- path('power-port-templates//delete/', views.PowerPortTemplateDeleteView.as_view(), name='powerporttemplate_delete'),
+ path('power-port-templates//', include(get_model_urls('dcim', 'powerporttemplate'))),
# Power outlet templates
path('power-outlet-templates/add/', views.PowerOutletTemplateCreateView.as_view(), name='poweroutlettemplate_add'),
path('power-outlet-templates/edit/', views.PowerOutletTemplateBulkEditView.as_view(), name='poweroutlettemplate_bulk_edit'),
path('power-outlet-templates/rename/', views.PowerOutletTemplateBulkRenameView.as_view(), name='poweroutlettemplate_bulk_rename'),
path('power-outlet-templates/delete/', views.PowerOutletTemplateBulkDeleteView.as_view(), name='poweroutlettemplate_bulk_delete'),
- path('power-outlet-templates//edit/', views.PowerOutletTemplateEditView.as_view(), name='poweroutlettemplate_edit'),
- path('power-outlet-templates//delete/', views.PowerOutletTemplateDeleteView.as_view(), name='poweroutlettemplate_delete'),
+ path('power-outlet-templates//', include(get_model_urls('dcim', 'poweroutlettemplate'))),
# Interface templates
path('interface-templates/add/', views.InterfaceTemplateCreateView.as_view(), name='interfacetemplate_add'),
path('interface-templates/edit/', views.InterfaceTemplateBulkEditView.as_view(), name='interfacetemplate_bulk_edit'),
path('interface-templates/rename/', views.InterfaceTemplateBulkRenameView.as_view(), name='interfacetemplate_bulk_rename'),
path('interface-templates/delete/', views.InterfaceTemplateBulkDeleteView.as_view(), name='interfacetemplate_bulk_delete'),
- path('interface-templates//edit/', views.InterfaceTemplateEditView.as_view(), name='interfacetemplate_edit'),
- path('interface-templates//delete/', views.InterfaceTemplateDeleteView.as_view(), name='interfacetemplate_delete'),
+ path('interface-templates//', include(get_model_urls('dcim', 'interfacetemplate'))),
# Front port templates
path('front-port-templates/add/', views.FrontPortTemplateCreateView.as_view(), name='frontporttemplate_add'),
path('front-port-templates/edit/', views.FrontPortTemplateBulkEditView.as_view(), name='frontporttemplate_bulk_edit'),
path('front-port-templates/rename/', views.FrontPortTemplateBulkRenameView.as_view(), name='frontporttemplate_bulk_rename'),
path('front-port-templates/delete/', views.FrontPortTemplateBulkDeleteView.as_view(), name='frontporttemplate_bulk_delete'),
- path('front-port-templates//edit/', views.FrontPortTemplateEditView.as_view(), name='frontporttemplate_edit'),
- path('front-port-templates//delete/', views.FrontPortTemplateDeleteView.as_view(), name='frontporttemplate_delete'),
+ path('front-port-templates//', include(get_model_urls('dcim', 'frontporttemplate'))),
# Rear port templates
path('rear-port-templates/add/', views.RearPortTemplateCreateView.as_view(), name='rearporttemplate_add'),
path('rear-port-templates/edit/', views.RearPortTemplateBulkEditView.as_view(), name='rearporttemplate_bulk_edit'),
path('rear-port-templates/rename/', views.RearPortTemplateBulkRenameView.as_view(), name='rearporttemplate_bulk_rename'),
path('rear-port-templates/delete/', views.RearPortTemplateBulkDeleteView.as_view(), name='rearporttemplate_bulk_delete'),
- path('rear-port-templates//edit/', views.RearPortTemplateEditView.as_view(), name='rearporttemplate_edit'),
- path('rear-port-templates//delete/', views.RearPortTemplateDeleteView.as_view(), name='rearporttemplate_delete'),
+ path('rear-port-templates//', include(get_model_urls('dcim', 'rearporttemplate'))),
# Device bay templates
path('device-bay-templates/add/', views.DeviceBayTemplateCreateView.as_view(), name='devicebaytemplate_add'),
path('device-bay-templates/edit/', views.DeviceBayTemplateBulkEditView.as_view(), name='devicebaytemplate_bulk_edit'),
path('device-bay-templates/rename/', views.DeviceBayTemplateBulkRenameView.as_view(), name='devicebaytemplate_bulk_rename'),
path('device-bay-templates/delete/', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicebaytemplate_bulk_delete'),
- path('device-bay-templates//edit/', views.DeviceBayTemplateEditView.as_view(), name='devicebaytemplate_edit'),
- path('device-bay-templates//delete/', views.DeviceBayTemplateDeleteView.as_view(), name='devicebaytemplate_delete'),
+ path('device-bay-templates//', include(get_model_urls('dcim', 'devicebaytemplate'))),
# Module bay templates
path('module-bay-templates/add/', views.ModuleBayTemplateCreateView.as_view(), name='modulebaytemplate_add'),
path('module-bay-templates/edit/', views.ModuleBayTemplateBulkEditView.as_view(), name='modulebaytemplate_bulk_edit'),
path('module-bay-templates/rename/', views.ModuleBayTemplateBulkRenameView.as_view(), name='modulebaytemplate_bulk_rename'),
path('module-bay-templates/delete/', views.ModuleBayTemplateBulkDeleteView.as_view(), name='modulebaytemplate_bulk_delete'),
- path('module-bay-templates//edit/', views.ModuleBayTemplateEditView.as_view(), name='modulebaytemplate_edit'),
- path('module-bay-templates//delete/', views.ModuleBayTemplateDeleteView.as_view(), name='modulebaytemplate_delete'),
+ path('module-bay-templates//', include(get_model_urls('dcim', 'modulebaytemplate'))),
# Inventory item templates
path('inventory-item-templates/add/', views.InventoryItemTemplateCreateView.as_view(), name='inventoryitemtemplate_add'),
path('inventory-item-templates/edit/', views.InventoryItemTemplateBulkEditView.as_view(), name='inventoryitemtemplate_bulk_edit'),
path('inventory-item-templates/rename/', views.InventoryItemTemplateBulkRenameView.as_view(), name='inventoryitemtemplate_bulk_rename'),
path('inventory-item-templates/delete/', views.InventoryItemTemplateBulkDeleteView.as_view(), name='inventoryitemtemplate_bulk_delete'),
- path('inventory-item-templates//edit/', views.InventoryItemTemplateEditView.as_view(), name='inventoryitemtemplate_edit'),
- path('inventory-item-templates//delete/', views.InventoryItemTemplateDeleteView.as_view(), name='inventoryitemtemplate_delete'),
+ path('inventory-item-templates//', include(get_model_urls('dcim', 'inventoryitemtemplate'))),
# Device roles
path('device-roles/', views.DeviceRoleListView.as_view(), name='devicerole_list'),
@@ -226,10 +163,7 @@ urlpatterns = [
path('device-roles/import/', views.DeviceRoleBulkImportView.as_view(), name='devicerole_import'),
path('device-roles/edit/', views.DeviceRoleBulkEditView.as_view(), name='devicerole_bulk_edit'),
path('device-roles/delete/', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'),
- path('device-roles//', views.DeviceRoleView.as_view(), name='devicerole'),
- path('device-roles//edit/', views.DeviceRoleEditView.as_view(), name='devicerole_edit'),
- path('device-roles//delete/', views.DeviceRoleDeleteView.as_view(), name='devicerole_delete'),
- path('device-roles//changelog/', ObjectChangeLogView.as_view(), name='devicerole_changelog', kwargs={'model': DeviceRole}),
+ path('device-roles//', include(get_model_urls('dcim', 'devicerole'))),
# Platforms
path('platforms/', views.PlatformListView.as_view(), name='platform_list'),
@@ -237,10 +171,7 @@ urlpatterns = [
path('platforms/import/', views.PlatformBulkImportView.as_view(), name='platform_import'),
path('platforms/edit/', views.PlatformBulkEditView.as_view(), name='platform_bulk_edit'),
path('platforms/delete/', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'),
- path('platforms//', views.PlatformView.as_view(), name='platform'),
- path('platforms//edit/', views.PlatformEditView.as_view(), name='platform_edit'),
- path('platforms//delete/', views.PlatformDeleteView.as_view(), name='platform_delete'),
- path('platforms//changelog/', ObjectChangeLogView.as_view(), name='platform_changelog', kwargs={'model': Platform}),
+ path('platforms//', include(get_model_urls('dcim', 'platform'))),
# Devices
path('devices/', views.DeviceListView.as_view(), name='device_list'),
@@ -250,25 +181,7 @@ urlpatterns = [
path('devices/edit/', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'),
path('devices/rename/', views.DeviceBulkRenameView.as_view(), name='device_bulk_rename'),
path('devices/delete/', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'),
- path('devices//', views.DeviceView.as_view(), name='device'),
- path('devices//edit/', views.DeviceEditView.as_view(), name='device_edit'),
- path('devices//delete/', views.DeviceDeleteView.as_view(), name='device_delete'),
- path('devices//console-ports/', views.DeviceConsolePortsView.as_view(), name='device_consoleports'),
- path('devices//console-server-ports/', views.DeviceConsoleServerPortsView.as_view(), name='device_consoleserverports'),
- path('devices//power-ports/', views.DevicePowerPortsView.as_view(), name='device_powerports'),
- path('devices//power-outlets/', views.DevicePowerOutletsView.as_view(), name='device_poweroutlets'),
- path('devices//interfaces/', views.DeviceInterfacesView.as_view(), name='device_interfaces'),
- path('devices//front-ports/', views.DeviceFrontPortsView.as_view(), name='device_frontports'),
- path('devices//rear-ports/', views.DeviceRearPortsView.as_view(), name='device_rearports'),
- path('devices//module-bays/', views.DeviceModuleBaysView.as_view(), name='device_modulebays'),
- path('devices//device-bays/', views.DeviceDeviceBaysView.as_view(), name='device_devicebays'),
- path('devices//inventory/', views.DeviceInventoryView.as_view(), name='device_inventory'),
- path('devices//config-context/', views.DeviceConfigContextView.as_view(), name='device_configcontext'),
- path('devices//changelog/', ObjectChangeLogView.as_view(), name='device_changelog', kwargs={'model': Device}),
- path('devices//journal/', ObjectJournalView.as_view(), name='device_journal', kwargs={'model': Device}),
- path('devices//status/', views.DeviceStatusView.as_view(), name='device_status'),
- path('devices//lldp-neighbors/', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
- path('devices//config/', views.DeviceConfigView.as_view(), name='device_config'),
+ path('devices//', include(get_model_urls('dcim', 'device'))),
# Modules
path('modules/', views.ModuleListView.as_view(), name='module_list'),
@@ -276,11 +189,7 @@ urlpatterns = [
path('modules/import/', views.ModuleBulkImportView.as_view(), name='module_import'),
path('modules/edit/', views.ModuleBulkEditView.as_view(), name='module_bulk_edit'),
path('modules/delete/', views.ModuleBulkDeleteView.as_view(), name='module_bulk_delete'),
- path('modules//', views.ModuleView.as_view(), name='module'),
- path('modules//edit/', views.ModuleEditView.as_view(), name='module_edit'),
- path('modules//delete/', views.ModuleDeleteView.as_view(), name='module_delete'),
- path('modules//changelog/', ObjectChangeLogView.as_view(), name='module_changelog', kwargs={'model': Module}),
- path('modules//journal/', ObjectJournalView.as_view(), name='module_journal', kwargs={'model': Module}),
+ path('modules//', include(get_model_urls('dcim', 'module'))),
# Console ports
path('console-ports/', views.ConsolePortListView.as_view(), name='consoleport_list'),
@@ -290,11 +199,7 @@ urlpatterns = [
path('console-ports/rename/', views.ConsolePortBulkRenameView.as_view(), name='consoleport_bulk_rename'),
path('console-ports/disconnect/', views.ConsolePortBulkDisconnectView.as_view(), name='consoleport_bulk_disconnect'),
path('console-ports/delete/', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
- path('console-ports//', views.ConsolePortView.as_view(), name='consoleport'),
- path('console-ports//edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'),
- path('console-ports//delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
- path('console-ports//changelog/', ObjectChangeLogView.as_view(), name='consoleport_changelog', kwargs={'model': ConsolePort}),
- path('console-ports//trace/', views.PathTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}),
+ path('console-ports//', include(get_model_urls('dcim', 'consoleport'))),
path('devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'),
# Console server ports
@@ -305,11 +210,7 @@ urlpatterns = [
path('console-server-ports/rename/', views.ConsoleServerPortBulkRenameView.as_view(), name='consoleserverport_bulk_rename'),
path('console-server-ports/disconnect/', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'),
path('console-server-ports/delete/', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'),
- path('console-server-ports//', views.ConsoleServerPortView.as_view(), name='consoleserverport'),
- path('console-server-ports//edit/', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'),
- path('console-server-ports//delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'),
- path('console-server-ports//changelog/', ObjectChangeLogView.as_view(), name='consoleserverport_changelog', kwargs={'model': ConsoleServerPort}),
- path('console-server-ports//trace/', views.PathTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}),
+ path('console-server-ports//', include(get_model_urls('dcim', 'consoleserverport'))),
path('devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'),
# Power ports
@@ -320,11 +221,7 @@ urlpatterns = [
path('power-ports/rename/', views.PowerPortBulkRenameView.as_view(), name='powerport_bulk_rename'),
path('power-ports/disconnect/', views.PowerPortBulkDisconnectView.as_view(), name='powerport_bulk_disconnect'),
path('power-ports/delete/', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
- path('power-ports//', views.PowerPortView.as_view(), name='powerport'),
- path('power-ports//edit/', views.PowerPortEditView.as_view(), name='powerport_edit'),
- path('power-ports//delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
- path('power-ports//changelog/', ObjectChangeLogView.as_view(), name='powerport_changelog', kwargs={'model': PowerPort}),
- path('power-ports//trace/', views.PathTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}),
+ path('power-ports//', include(get_model_urls('dcim', 'powerport'))),
path('devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
# Power outlets
@@ -335,11 +232,7 @@ urlpatterns = [
path('power-outlets/rename/', views.PowerOutletBulkRenameView.as_view(), name='poweroutlet_bulk_rename'),
path('power-outlets/disconnect/', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'),
path('power-outlets/delete/', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'),
- path('power-outlets//', views.PowerOutletView.as_view(), name='poweroutlet'),
- path('power-outlets//edit/', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'),
- path('power-outlets//delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'),
- path('power-outlets//changelog/', ObjectChangeLogView.as_view(), name='poweroutlet_changelog', kwargs={'model': PowerOutlet}),
- path('power-outlets//trace/', views.PathTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}),
+ path('power-outlets//', include(get_model_urls('dcim', 'poweroutlet'))),
path('devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'),
# Interfaces
@@ -350,11 +243,7 @@ urlpatterns = [
path('interfaces/rename/', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'),
path('interfaces/disconnect/', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'),
path('interfaces/delete/', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
- path('interfaces//', views.InterfaceView.as_view(), name='interface'),
- path('interfaces//edit/', views.InterfaceEditView.as_view(), name='interface_edit'),
- path('interfaces//delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'),
- path('interfaces//changelog/', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}),
- path('interfaces//trace/', views.PathTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}),
+ path('interfaces//', include(get_model_urls('dcim', 'interface'))),
path('devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
# Front ports
@@ -365,11 +254,7 @@ urlpatterns = [
path('front-ports/rename/', views.FrontPortBulkRenameView.as_view(), name='frontport_bulk_rename'),
path('front-ports/disconnect/', views.FrontPortBulkDisconnectView.as_view(), name='frontport_bulk_disconnect'),
path('front-ports/delete/', views.FrontPortBulkDeleteView.as_view(), name='frontport_bulk_delete'),
- path('front-ports//', views.FrontPortView.as_view(), name='frontport'),
- path('front-ports//edit/', views.FrontPortEditView.as_view(), name='frontport_edit'),
- path('front-ports//delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'),
- path('front-ports//changelog/', ObjectChangeLogView.as_view(), name='frontport_changelog', kwargs={'model': FrontPort}),
- path('front-ports//trace/', views.PathTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}),
+ path('front-ports//', include(get_model_urls('dcim', 'frontport'))),
# path('devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'),
# Rear ports
@@ -380,11 +265,7 @@ urlpatterns = [
path('rear-ports/rename/', views.RearPortBulkRenameView.as_view(), name='rearport_bulk_rename'),
path('rear-ports/disconnect/', views.RearPortBulkDisconnectView.as_view(), name='rearport_bulk_disconnect'),
path('rear-ports/delete/', views.RearPortBulkDeleteView.as_view(), name='rearport_bulk_delete'),
- path('rear-ports//', views.RearPortView.as_view(), name='rearport'),
- path('rear-ports//edit/', views.RearPortEditView.as_view(), name='rearport_edit'),
- path('rear-ports//delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'),
- path('rear-ports//changelog/', ObjectChangeLogView.as_view(), name='rearport_changelog', kwargs={'model': RearPort}),
- path('rear-ports//trace/', views.PathTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}),
+ path('rear-ports//', include(get_model_urls('dcim', 'rearport'))),
path('devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'),
# Module bays
@@ -394,10 +275,7 @@ urlpatterns = [
path('module-bays/edit/', views.ModuleBayBulkEditView.as_view(), name='modulebay_bulk_edit'),
path('module-bays/rename/', views.ModuleBayBulkRenameView.as_view(), name='modulebay_bulk_rename'),
path('module-bays/delete/', views.ModuleBayBulkDeleteView.as_view(), name='modulebay_bulk_delete'),
- path('module-bays//', views.ModuleBayView.as_view(), name='modulebay'),
- path('module-bays//edit/', views.ModuleBayEditView.as_view(), name='modulebay_edit'),
- path('module-bays//delete/', views.ModuleBayDeleteView.as_view(), name='modulebay_delete'),
- path('module-bays//changelog/', ObjectChangeLogView.as_view(), name='modulebay_changelog', kwargs={'model': ModuleBay}),
+ path('module-bays//', include(get_model_urls('dcim', 'modulebay'))),
path('devices/module-bays/add/', views.DeviceBulkAddModuleBayView.as_view(), name='device_bulk_add_modulebay'),
# Device bays
@@ -407,12 +285,7 @@ urlpatterns = [
path('device-bays/edit/', views.DeviceBayBulkEditView.as_view(), name='devicebay_bulk_edit'),
path('device-bays/rename/', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'),
path('device-bays/delete/', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'),
- path('device-bays//', views.DeviceBayView.as_view(), name='devicebay'),
- path('device-bays//edit/', views.DeviceBayEditView.as_view(), name='devicebay_edit'),
- path('device-bays//delete/', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'),
- path('device-bays//changelog/', ObjectChangeLogView.as_view(), name='devicebay_changelog', kwargs={'model': DeviceBay}),
- path('device-bays//populate/', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'),
- path('device-bays//depopulate/', views.DeviceBayDepopulateView.as_view(), name='devicebay_depopulate'),
+ path('device-bays//', include(get_model_urls('dcim', 'devicebay'))),
path('devices/device-bays/add/', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'),
# Inventory items
@@ -422,22 +295,16 @@ urlpatterns = [
path('inventory-items/edit/', views.InventoryItemBulkEditView.as_view(), name='inventoryitem_bulk_edit'),
path('inventory-items/rename/', views.InventoryItemBulkRenameView.as_view(), name='inventoryitem_bulk_rename'),
path('inventory-items/delete/', views.InventoryItemBulkDeleteView.as_view(), name='inventoryitem_bulk_delete'),
- path('inventory-items//', views.InventoryItemView.as_view(), name='inventoryitem'),
- path('inventory-items//edit/', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'),
- path('inventory-items//delete/', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'),
- path('inventory-items//changelog/', ObjectChangeLogView.as_view(), name='inventoryitem_changelog', kwargs={'model': InventoryItem}),
+ path('inventory-items//', include(get_model_urls('dcim', 'inventoryitem'))),
path('devices/inventory-items/add/', views.DeviceBulkAddInventoryItemView.as_view(), name='device_bulk_add_inventoryitem'),
- # Device roles
+ # Inventory item roles
path('inventory-item-roles/', views.InventoryItemRoleListView.as_view(), name='inventoryitemrole_list'),
path('inventory-item-roles/add/', views.InventoryItemRoleEditView.as_view(), name='inventoryitemrole_add'),
path('inventory-item-roles/import/', views.InventoryItemRoleBulkImportView.as_view(), name='inventoryitemrole_import'),
path('inventory-item-roles/edit/', views.InventoryItemRoleBulkEditView.as_view(), name='inventoryitemrole_bulk_edit'),
path('inventory-item-roles/delete/', views.InventoryItemRoleBulkDeleteView.as_view(), name='inventoryitemrole_bulk_delete'),
- path('inventory-item-roles//', views.InventoryItemRoleView.as_view(), name='inventoryitemrole'),
- path('inventory-item-roles//edit/', views.InventoryItemRoleEditView.as_view(), name='inventoryitemrole_edit'),
- path('inventory-item-roles//delete/', views.InventoryItemRoleDeleteView.as_view(), name='inventoryitemrole_delete'),
- path('inventory-item-roles//changelog/', ObjectChangeLogView.as_view(), name='inventoryitemrole_changelog', kwargs={'model': InventoryItemRole}),
+ path('inventory-item-roles//', include(get_model_urls('dcim', 'inventoryitemrole'))),
# Cables
path('cables/', views.CableListView.as_view(), name='cable_list'),
@@ -445,11 +312,7 @@ urlpatterns = [
path('cables/import/', views.CableBulkImportView.as_view(), name='cable_import'),
path('cables/edit/', views.CableBulkEditView.as_view(), name='cable_bulk_edit'),
path('cables/delete/', views.CableBulkDeleteView.as_view(), name='cable_bulk_delete'),
- path('cables//', views.CableView.as_view(), name='cable'),
- path('cables//edit/', views.CableEditView.as_view(), name='cable_edit'),
- path('cables//delete/', views.CableDeleteView.as_view(), name='cable_delete'),
- path('cables//changelog/', ObjectChangeLogView.as_view(), name='cable_changelog', kwargs={'model': Cable}),
- path('cables//journal/', ObjectJournalView.as_view(), name='cable_journal', kwargs={'model': Cable}),
+ path('cables//', include(get_model_urls('dcim', 'cable'))),
# Console/power/interface connections (read-only)
path('console-connections/', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'),
@@ -462,12 +325,7 @@ urlpatterns = [
path('virtual-chassis/import/', views.VirtualChassisBulkImportView.as_view(), name='virtualchassis_import'),
path('virtual-chassis/edit/', views.VirtualChassisBulkEditView.as_view(), name='virtualchassis_bulk_edit'),
path('virtual-chassis/delete/', views.VirtualChassisBulkDeleteView.as_view(), name='virtualchassis_bulk_delete'),
- path('virtual-chassis//', views.VirtualChassisView.as_view(), name='virtualchassis'),
- path('virtual-chassis//edit/', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'),
- path('virtual-chassis//delete/', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'),
- path('virtual-chassis//changelog/', ObjectChangeLogView.as_view(), name='virtualchassis_changelog', kwargs={'model': VirtualChassis}),
- path('virtual-chassis//journal/', ObjectJournalView.as_view(), name='virtualchassis_journal', kwargs={'model': VirtualChassis}),
- path('virtual-chassis//add-member/', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'),
+ path('virtual-chassis//', include(get_model_urls('dcim', 'virtualchassis'))),
path('virtual-chassis-members//delete/', views.VirtualChassisRemoveMemberView.as_view(), name='virtualchassis_remove_member'),
# Power panels
@@ -476,11 +334,7 @@ urlpatterns = [
path('power-panels/import/', views.PowerPanelBulkImportView.as_view(), name='powerpanel_import'),
path('power-panels/edit/', views.PowerPanelBulkEditView.as_view(), name='powerpanel_bulk_edit'),
path('power-panels/delete/', views.PowerPanelBulkDeleteView.as_view(), name='powerpanel_bulk_delete'),
- path('power-panels//', views.PowerPanelView.as_view(), name='powerpanel'),
- path('power-panels//edit/', views.PowerPanelEditView.as_view(), name='powerpanel_edit'),
- path('power-panels//delete/', views.PowerPanelDeleteView.as_view(), name='powerpanel_delete'),
- path('power-panels//changelog/', ObjectChangeLogView.as_view(), name='powerpanel_changelog', kwargs={'model': PowerPanel}),
- path('power-panels//journal/', ObjectJournalView.as_view(), name='powerpanel_journal', kwargs={'model': PowerPanel}),
+ path('power-panels//', include(get_model_urls('dcim', 'powerpanel'))),
# Power feeds
path('power-feeds/', views.PowerFeedListView.as_view(), name='powerfeed_list'),
@@ -489,11 +343,6 @@ urlpatterns = [
path('power-feeds/edit/', views.PowerFeedBulkEditView.as_view(), name='powerfeed_bulk_edit'),
path('power-feeds/disconnect/', views.PowerFeedBulkDisconnectView.as_view(), name='powerfeed_bulk_disconnect'),
path('power-feeds/delete/', views.PowerFeedBulkDeleteView.as_view(), name='powerfeed_bulk_delete'),
- path('power-feeds//', views.PowerFeedView.as_view(), name='powerfeed'),
- path('power-feeds/