Merge branch 'feature' into 4347-json-import

This commit is contained in:
jeremystretch 2022-11-09 10:27:46 -05:00
commit d9306d827f
412 changed files with 10629 additions and 6521 deletions

View File

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

View File

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

View File

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

View File

@ -1,13 +1,14 @@
<!--
Thank you for your interest in contributing to NetBox! Please note that
our contribution policy requires that a feature request or bug report be
approved and assigned prior to filing a pull request. This helps avoid
wasting time and effort on something that we might not be able to accept.
approved and assigned prior to opening a pull request. This helps avoid
waste time and effort on a proposed change that we might not be able to
accept.
IF YOUR PULL REQUEST DOES NOT REFERENCE AN ISSUE WHICH HAS BEEN ASSIGNED
TO YOU, IT WE BE CLOSED AUTOMATICALLY.
TO YOU, IT WILL BE CLOSED AUTOMATICALLY.
Specify your assigned issue number on the line below.
Please specify your assigned issue number on the line below.
-->
### Fixes: #1234

View File

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

View File

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

View File

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

View File

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

View File

@ -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/' %}
<meta name="robots" content="noindex">
{% endif %}
{% endblock %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.)
<!-- TODO: Screenshot -->
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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/<app_label>/<model_name>/`. 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.)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,4 +6,4 @@ class CircuitsConfig(AppConfig):
verbose_name = "Circuits"
def ready(self):
import circuits.signals
from . import signals, search

View File

@ -1,4 +1,4 @@
from .bulk_edit import *
from .bulk_import import *
from .filtersets import *
from .models import *
from .model_forms import *

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

55
netbox/circuits/search.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -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/<int:pk>/', views.ProviderView.as_view(), name='provider'),
path('providers/<int:pk>/edit/', views.ProviderEditView.as_view(), name='provider_edit'),
path('providers/<int:pk>/delete/', views.ProviderDeleteView.as_view(), name='provider_delete'),
path('providers/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}),
path('providers/<int:pk>/journal/', ObjectJournalView.as_view(), name='provider_journal', kwargs={'model': Provider}),
path('providers/<int:pk>/', 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/<int:pk>/', views.ProviderNetworkView.as_view(), name='providernetwork'),
path('provider-networks/<int:pk>/edit/', views.ProviderNetworkEditView.as_view(), name='providernetwork_edit'),
path('provider-networks/<int:pk>/delete/', views.ProviderNetworkDeleteView.as_view(), name='providernetwork_delete'),
path('provider-networks/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='providernetwork_changelog', kwargs={'model': ProviderNetwork}),
path('provider-networks/<int:pk>/journal/', ObjectJournalView.as_view(), name='providernetwork_journal', kwargs={'model': ProviderNetwork}),
path('provider-networks/<int:pk>/', 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/<int:pk>/', views.CircuitTypeView.as_view(), name='circuittype'),
path('circuit-types/<int:pk>/edit/', views.CircuitTypeEditView.as_view(), name='circuittype_edit'),
path('circuit-types/<int:pk>/delete/', views.CircuitTypeDeleteView.as_view(), name='circuittype_delete'),
path('circuit-types/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}),
path('circuit-types/<int:pk>/', 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/<int:pk>/', views.CircuitView.as_view(), name='circuit'),
path('circuits/<int:pk>/edit/', views.CircuitEditView.as_view(), name='circuit_edit'),
path('circuits/<int:pk>/delete/', views.CircuitDeleteView.as_view(), name='circuit_delete'),
path('circuits/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='circuit_changelog', kwargs={'model': Circuit}),
path('circuits/<int:pk>/journal/', ObjectJournalView.as_view(), name='circuit_journal', kwargs={'model': Circuit}),
path('circuits/<int:pk>/terminations/swap/', views.CircuitSwapTerminations.as_view(), name='circuit_terminations_swap'),
path('circuits/<int:pk>/', include(get_model_urls('circuits', 'circuit'))),
# Circuit terminations
path('circuit-terminations/add/', views.CircuitTerminationEditView.as_view(), name='circuittermination_add'),
path('circuit-terminations/<int:pk>/edit/', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'),
path('circuit-terminations/<int:pk>/delete/', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'),
path('circuit-terminations/<int:pk>/trace/', PathTraceView.as_view(), name='circuittermination_trace', kwargs={'model': CircuitTermination}),
path('circuit-terminations/<int:pk>/', include(get_model_urls('circuits', 'circuittermination'))),
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
from .models import *
from .model_forms import *
from .filtersets import *
from .object_create import *
from .object_import import *

View File

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

View File

@ -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. <code>00ff00</code>)'),
}
@ -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. <code>00ff00</code>)'),
}
@ -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. <code>00ff00</code>)'),
@ -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):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

293
netbox/dcim/search.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = """
<a href="{% url 'dcim:device' pk=record.pk %}">
{{ record.name|default:'<span class="badge bg-info">Unnamed device</span>' }}

View File

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

View File

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

View File

@ -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/<int:pk>/', views.RegionView.as_view(), name='region'),
path('regions/<int:pk>/edit/', views.RegionEditView.as_view(), name='region_edit'),
path('regions/<int:pk>/delete/', views.RegionDeleteView.as_view(), name='region_delete'),
path('regions/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='region_changelog', kwargs={'model': Region}),
path('regions/<int:pk>/', 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/<int:pk>/', views.SiteGroupView.as_view(), name='sitegroup'),
path('site-groups/<int:pk>/edit/', views.SiteGroupEditView.as_view(), name='sitegroup_edit'),
path('site-groups/<int:pk>/delete/', views.SiteGroupDeleteView.as_view(), name='sitegroup_delete'),
path('site-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='sitegroup_changelog', kwargs={'model': SiteGroup}),
path('site-groups/<int:pk>/', 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/<int:pk>/', views.SiteView.as_view(), name='site'),
path('sites/<int:pk>/edit/', views.SiteEditView.as_view(), name='site_edit'),
path('sites/<int:pk>/delete/', views.SiteDeleteView.as_view(), name='site_delete'),
path('sites/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='site_changelog', kwargs={'model': Site}),
path('sites/<int:pk>/journal/', ObjectJournalView.as_view(), name='site_journal', kwargs={'model': Site}),
path('sites/<int:pk>/', 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/<int:pk>/', views.LocationView.as_view(), name='location'),
path('locations/<int:pk>/edit/', views.LocationEditView.as_view(), name='location_edit'),
path('locations/<int:pk>/delete/', views.LocationDeleteView.as_view(), name='location_delete'),
path('locations/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='location_changelog', kwargs={'model': Location}),
path('locations/<int:pk>/', 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/<int:pk>/', views.RackRoleView.as_view(), name='rackrole'),
path('rack-roles/<int:pk>/edit/', views.RackRoleEditView.as_view(), name='rackrole_edit'),
path('rack-roles/<int:pk>/delete/', views.RackRoleDeleteView.as_view(), name='rackrole_delete'),
path('rack-roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackrole_changelog', kwargs={'model': RackRole}),
path('rack-roles/<int:pk>/', 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/<int:pk>/', views.RackReservationView.as_view(), name='rackreservation'),
path('rack-reservations/<int:pk>/edit/', views.RackReservationEditView.as_view(), name='rackreservation_edit'),
path('rack-reservations/<int:pk>/delete/', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'),
path('rack-reservations/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackreservation_changelog', kwargs={'model': RackReservation}),
path('rack-reservations/<int:pk>/journal/', ObjectJournalView.as_view(), name='rackreservation_journal', kwargs={'model': RackReservation}),
path('rack-reservations/<int:pk>/', 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/<int:pk>/', views.RackView.as_view(), name='rack'),
path('racks/<int:pk>/edit/', views.RackEditView.as_view(), name='rack_edit'),
path('racks/<int:pk>/delete/', views.RackDeleteView.as_view(), name='rack_delete'),
path('racks/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rack_changelog', kwargs={'model': Rack}),
path('racks/<int:pk>/journal/', ObjectJournalView.as_view(), name='rack_journal', kwargs={'model': Rack}),
path('racks/<int:pk>/', 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/<int:pk>/', views.ManufacturerView.as_view(), name='manufacturer'),
path('manufacturers/<int:pk>/edit/', views.ManufacturerEditView.as_view(), name='manufacturer_edit'),
path('manufacturers/<int:pk>/delete/', views.ManufacturerDeleteView.as_view(), name='manufacturer_delete'),
path('manufacturers/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='manufacturer_changelog', kwargs={'model': Manufacturer}),
path('manufacturers/<int:pk>/', 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/<int:pk>/', views.DeviceTypeView.as_view(), name='devicetype'),
path('device-types/<int:pk>/console-ports/', views.DeviceTypeConsolePortsView.as_view(), name='devicetype_consoleports'),
path('device-types/<int:pk>/console-server-ports/', views.DeviceTypeConsoleServerPortsView.as_view(), name='devicetype_consoleserverports'),
path('device-types/<int:pk>/power-ports/', views.DeviceTypePowerPortsView.as_view(), name='devicetype_powerports'),
path('device-types/<int:pk>/power-outlets/', views.DeviceTypePowerOutletsView.as_view(), name='devicetype_poweroutlets'),
path('device-types/<int:pk>/interfaces/', views.DeviceTypeInterfacesView.as_view(), name='devicetype_interfaces'),
path('device-types/<int:pk>/front-ports/', views.DeviceTypeFrontPortsView.as_view(), name='devicetype_frontports'),
path('device-types/<int:pk>/rear-ports/', views.DeviceTypeRearPortsView.as_view(), name='devicetype_rearports'),
path('device-types/<int:pk>/module-bays/', views.DeviceTypeModuleBaysView.as_view(), name='devicetype_modulebays'),
path('device-types/<int:pk>/device-bays/', views.DeviceTypeDeviceBaysView.as_view(), name='devicetype_devicebays'),
path('device-types/<int:pk>/inventory-items/', views.DeviceTypeInventoryItemsView.as_view(), name='devicetype_inventoryitems'),
path('device-types/<int:pk>/edit/', views.DeviceTypeEditView.as_view(), name='devicetype_edit'),
path('device-types/<int:pk>/delete/', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
path('device-types/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}),
path('device-types/<int:pk>/journal/', ObjectJournalView.as_view(), name='devicetype_journal', kwargs={'model': DeviceType}),
path('device-types/<int:pk>/', 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/<int:pk>/', views.ModuleTypeView.as_view(), name='moduletype'),
path('module-types/<int:pk>/console-ports/', views.ModuleTypeConsolePortsView.as_view(), name='moduletype_consoleports'),
path('module-types/<int:pk>/console-server-ports/', views.ModuleTypeConsoleServerPortsView.as_view(), name='moduletype_consoleserverports'),
path('module-types/<int:pk>/power-ports/', views.ModuleTypePowerPortsView.as_view(), name='moduletype_powerports'),
path('module-types/<int:pk>/power-outlets/', views.ModuleTypePowerOutletsView.as_view(), name='moduletype_poweroutlets'),
path('module-types/<int:pk>/interfaces/', views.ModuleTypeInterfacesView.as_view(), name='moduletype_interfaces'),
path('module-types/<int:pk>/front-ports/', views.ModuleTypeFrontPortsView.as_view(), name='moduletype_frontports'),
path('module-types/<int:pk>/rear-ports/', views.ModuleTypeRearPortsView.as_view(), name='moduletype_rearports'),
path('module-types/<int:pk>/edit/', views.ModuleTypeEditView.as_view(), name='moduletype_edit'),
path('module-types/<int:pk>/delete/', views.ModuleTypeDeleteView.as_view(), name='moduletype_delete'),
path('module-types/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='moduletype_changelog', kwargs={'model': ModuleType}),
path('module-types/<int:pk>/journal/', ObjectJournalView.as_view(), name='moduletype_journal', kwargs={'model': ModuleType}),
path('module-types/<int:pk>/', 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/<int:pk>/edit/', views.ConsolePortTemplateEditView.as_view(), name='consoleporttemplate_edit'),
path('console-port-templates/<int:pk>/delete/', views.ConsolePortTemplateDeleteView.as_view(), name='consoleporttemplate_delete'),
path('console-port-templates/<int:pk>/', 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/<int:pk>/edit/', views.ConsoleServerPortTemplateEditView.as_view(), name='consoleserverporttemplate_edit'),
path('console-server-port-templates/<int:pk>/delete/', views.ConsoleServerPortTemplateDeleteView.as_view(), name='consoleserverporttemplate_delete'),
path('console-server-port-templates/<int:pk>/', 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/<int:pk>/edit/', views.PowerPortTemplateEditView.as_view(), name='powerporttemplate_edit'),
path('power-port-templates/<int:pk>/delete/', views.PowerPortTemplateDeleteView.as_view(), name='powerporttemplate_delete'),
path('power-port-templates/<int:pk>/', 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/<int:pk>/edit/', views.PowerOutletTemplateEditView.as_view(), name='poweroutlettemplate_edit'),
path('power-outlet-templates/<int:pk>/delete/', views.PowerOutletTemplateDeleteView.as_view(), name='poweroutlettemplate_delete'),
path('power-outlet-templates/<int:pk>/', 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/<int:pk>/edit/', views.InterfaceTemplateEditView.as_view(), name='interfacetemplate_edit'),
path('interface-templates/<int:pk>/delete/', views.InterfaceTemplateDeleteView.as_view(), name='interfacetemplate_delete'),
path('interface-templates/<int:pk>/', 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/<int:pk>/edit/', views.FrontPortTemplateEditView.as_view(), name='frontporttemplate_edit'),
path('front-port-templates/<int:pk>/delete/', views.FrontPortTemplateDeleteView.as_view(), name='frontporttemplate_delete'),
path('front-port-templates/<int:pk>/', 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/<int:pk>/edit/', views.RearPortTemplateEditView.as_view(), name='rearporttemplate_edit'),
path('rear-port-templates/<int:pk>/delete/', views.RearPortTemplateDeleteView.as_view(), name='rearporttemplate_delete'),
path('rear-port-templates/<int:pk>/', 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/<int:pk>/edit/', views.DeviceBayTemplateEditView.as_view(), name='devicebaytemplate_edit'),
path('device-bay-templates/<int:pk>/delete/', views.DeviceBayTemplateDeleteView.as_view(), name='devicebaytemplate_delete'),
path('device-bay-templates/<int:pk>/', 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/<int:pk>/edit/', views.ModuleBayTemplateEditView.as_view(), name='modulebaytemplate_edit'),
path('module-bay-templates/<int:pk>/delete/', views.ModuleBayTemplateDeleteView.as_view(), name='modulebaytemplate_delete'),
path('module-bay-templates/<int:pk>/', 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/<int:pk>/edit/', views.InventoryItemTemplateEditView.as_view(), name='inventoryitemtemplate_edit'),
path('inventory-item-templates/<int:pk>/delete/', views.InventoryItemTemplateDeleteView.as_view(), name='inventoryitemtemplate_delete'),
path('inventory-item-templates/<int:pk>/', 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/<int:pk>/', views.DeviceRoleView.as_view(), name='devicerole'),
path('device-roles/<int:pk>/edit/', views.DeviceRoleEditView.as_view(), name='devicerole_edit'),
path('device-roles/<int:pk>/delete/', views.DeviceRoleDeleteView.as_view(), name='devicerole_delete'),
path('device-roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='devicerole_changelog', kwargs={'model': DeviceRole}),
path('device-roles/<int:pk>/', 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/<int:pk>/', views.PlatformView.as_view(), name='platform'),
path('platforms/<int:pk>/edit/', views.PlatformEditView.as_view(), name='platform_edit'),
path('platforms/<int:pk>/delete/', views.PlatformDeleteView.as_view(), name='platform_delete'),
path('platforms/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='platform_changelog', kwargs={'model': Platform}),
path('platforms/<int:pk>/', 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/<int:pk>/', views.DeviceView.as_view(), name='device'),
path('devices/<int:pk>/edit/', views.DeviceEditView.as_view(), name='device_edit'),
path('devices/<int:pk>/delete/', views.DeviceDeleteView.as_view(), name='device_delete'),
path('devices/<int:pk>/console-ports/', views.DeviceConsolePortsView.as_view(), name='device_consoleports'),
path('devices/<int:pk>/console-server-ports/', views.DeviceConsoleServerPortsView.as_view(), name='device_consoleserverports'),
path('devices/<int:pk>/power-ports/', views.DevicePowerPortsView.as_view(), name='device_powerports'),
path('devices/<int:pk>/power-outlets/', views.DevicePowerOutletsView.as_view(), name='device_poweroutlets'),
path('devices/<int:pk>/interfaces/', views.DeviceInterfacesView.as_view(), name='device_interfaces'),
path('devices/<int:pk>/front-ports/', views.DeviceFrontPortsView.as_view(), name='device_frontports'),
path('devices/<int:pk>/rear-ports/', views.DeviceRearPortsView.as_view(), name='device_rearports'),
path('devices/<int:pk>/module-bays/', views.DeviceModuleBaysView.as_view(), name='device_modulebays'),
path('devices/<int:pk>/device-bays/', views.DeviceDeviceBaysView.as_view(), name='device_devicebays'),
path('devices/<int:pk>/inventory/', views.DeviceInventoryView.as_view(), name='device_inventory'),
path('devices/<int:pk>/config-context/', views.DeviceConfigContextView.as_view(), name='device_configcontext'),
path('devices/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='device_changelog', kwargs={'model': Device}),
path('devices/<int:pk>/journal/', ObjectJournalView.as_view(), name='device_journal', kwargs={'model': Device}),
path('devices/<int:pk>/status/', views.DeviceStatusView.as_view(), name='device_status'),
path('devices/<int:pk>/lldp-neighbors/', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
path('devices/<int:pk>/config/', views.DeviceConfigView.as_view(), name='device_config'),
path('devices/<int:pk>/', 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/<int:pk>/', views.ModuleView.as_view(), name='module'),
path('modules/<int:pk>/edit/', views.ModuleEditView.as_view(), name='module_edit'),
path('modules/<int:pk>/delete/', views.ModuleDeleteView.as_view(), name='module_delete'),
path('modules/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='module_changelog', kwargs={'model': Module}),
path('modules/<int:pk>/journal/', ObjectJournalView.as_view(), name='module_journal', kwargs={'model': Module}),
path('modules/<int:pk>/', 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/<int:pk>/', views.ConsolePortView.as_view(), name='consoleport'),
path('console-ports/<int:pk>/edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'),
path('console-ports/<int:pk>/delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
path('console-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='consoleport_changelog', kwargs={'model': ConsolePort}),
path('console-ports/<int:pk>/trace/', views.PathTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}),
path('console-ports/<int:pk>/', 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/<int:pk>/', views.ConsoleServerPortView.as_view(), name='consoleserverport'),
path('console-server-ports/<int:pk>/edit/', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'),
path('console-server-ports/<int:pk>/delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'),
path('console-server-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='consoleserverport_changelog', kwargs={'model': ConsoleServerPort}),
path('console-server-ports/<int:pk>/trace/', views.PathTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}),
path('console-server-ports/<int:pk>/', 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/<int:pk>/', views.PowerPortView.as_view(), name='powerport'),
path('power-ports/<int:pk>/edit/', views.PowerPortEditView.as_view(), name='powerport_edit'),
path('power-ports/<int:pk>/delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
path('power-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='powerport_changelog', kwargs={'model': PowerPort}),
path('power-ports/<int:pk>/trace/', views.PathTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}),
path('power-ports/<int:pk>/', 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/<int:pk>/', views.PowerOutletView.as_view(), name='poweroutlet'),
path('power-outlets/<int:pk>/edit/', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'),
path('power-outlets/<int:pk>/delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'),
path('power-outlets/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='poweroutlet_changelog', kwargs={'model': PowerOutlet}),
path('power-outlets/<int:pk>/trace/', views.PathTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}),
path('power-outlets/<int:pk>/', 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/<int:pk>/', views.InterfaceView.as_view(), name='interface'),
path('interfaces/<int:pk>/edit/', views.InterfaceEditView.as_view(), name='interface_edit'),
path('interfaces/<int:pk>/delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'),
path('interfaces/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}),
path('interfaces/<int:pk>/trace/', views.PathTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}),
path('interfaces/<int:pk>/', 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/<int:pk>/', views.FrontPortView.as_view(), name='frontport'),
path('front-ports/<int:pk>/edit/', views.FrontPortEditView.as_view(), name='frontport_edit'),
path('front-ports/<int:pk>/delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'),
path('front-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='frontport_changelog', kwargs={'model': FrontPort}),
path('front-ports/<int:pk>/trace/', views.PathTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}),
path('front-ports/<int:pk>/', 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/<int:pk>/', views.RearPortView.as_view(), name='rearport'),
path('rear-ports/<int:pk>/edit/', views.RearPortEditView.as_view(), name='rearport_edit'),
path('rear-ports/<int:pk>/delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'),
path('rear-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rearport_changelog', kwargs={'model': RearPort}),
path('rear-ports/<int:pk>/trace/', views.PathTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}),
path('rear-ports/<int:pk>/', 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/<int:pk>/', views.ModuleBayView.as_view(), name='modulebay'),
path('module-bays/<int:pk>/edit/', views.ModuleBayEditView.as_view(), name='modulebay_edit'),
path('module-bays/<int:pk>/delete/', views.ModuleBayDeleteView.as_view(), name='modulebay_delete'),
path('module-bays/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='modulebay_changelog', kwargs={'model': ModuleBay}),
path('module-bays/<int:pk>/', 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/<int:pk>/', views.DeviceBayView.as_view(), name='devicebay'),
path('device-bays/<int:pk>/edit/', views.DeviceBayEditView.as_view(), name='devicebay_edit'),
path('device-bays/<int:pk>/delete/', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'),
path('device-bays/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='devicebay_changelog', kwargs={'model': DeviceBay}),
path('device-bays/<int:pk>/populate/', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'),
path('device-bays/<int:pk>/depopulate/', views.DeviceBayDepopulateView.as_view(), name='devicebay_depopulate'),
path('device-bays/<int:pk>/', 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/<int:pk>/', views.InventoryItemView.as_view(), name='inventoryitem'),
path('inventory-items/<int:pk>/edit/', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'),
path('inventory-items/<int:pk>/delete/', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'),
path('inventory-items/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='inventoryitem_changelog', kwargs={'model': InventoryItem}),
path('inventory-items/<int:pk>/', 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/<int:pk>/', views.InventoryItemRoleView.as_view(), name='inventoryitemrole'),
path('inventory-item-roles/<int:pk>/edit/', views.InventoryItemRoleEditView.as_view(), name='inventoryitemrole_edit'),
path('inventory-item-roles/<int:pk>/delete/', views.InventoryItemRoleDeleteView.as_view(), name='inventoryitemrole_delete'),
path('inventory-item-roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='inventoryitemrole_changelog', kwargs={'model': InventoryItemRole}),
path('inventory-item-roles/<int:pk>/', 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/<int:pk>/', views.CableView.as_view(), name='cable'),
path('cables/<int:pk>/edit/', views.CableEditView.as_view(), name='cable_edit'),
path('cables/<int:pk>/delete/', views.CableDeleteView.as_view(), name='cable_delete'),
path('cables/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='cable_changelog', kwargs={'model': Cable}),
path('cables/<int:pk>/journal/', ObjectJournalView.as_view(), name='cable_journal', kwargs={'model': Cable}),
path('cables/<int:pk>/', 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/<int:pk>/', views.VirtualChassisView.as_view(), name='virtualchassis'),
path('virtual-chassis/<int:pk>/edit/', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'),
path('virtual-chassis/<int:pk>/delete/', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'),
path('virtual-chassis/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='virtualchassis_changelog', kwargs={'model': VirtualChassis}),
path('virtual-chassis/<int:pk>/journal/', ObjectJournalView.as_view(), name='virtualchassis_journal', kwargs={'model': VirtualChassis}),
path('virtual-chassis/<int:pk>/add-member/', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'),
path('virtual-chassis/<int:pk>/', include(get_model_urls('dcim', 'virtualchassis'))),
path('virtual-chassis-members/<int:pk>/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/<int:pk>/', views.PowerPanelView.as_view(), name='powerpanel'),
path('power-panels/<int:pk>/edit/', views.PowerPanelEditView.as_view(), name='powerpanel_edit'),
path('power-panels/<int:pk>/delete/', views.PowerPanelDeleteView.as_view(), name='powerpanel_delete'),
path('power-panels/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='powerpanel_changelog', kwargs={'model': PowerPanel}),
path('power-panels/<int:pk>/journal/', ObjectJournalView.as_view(), name='powerpanel_journal', kwargs={'model': PowerPanel}),
path('power-panels/<int:pk>/', 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/<int:pk>/', views.PowerFeedView.as_view(), name='powerfeed'),
path('power-feeds/<int:pk>/edit/', views.PowerFeedEditView.as_view(), name='powerfeed_edit'),
path('power-feeds/<int:pk>/delete/', views.PowerFeedDeleteView.as_view(), name='powerfeed_delete'),
path('power-feeds/<int:pk>/trace/', views.PathTraceView.as_view(), name='powerfeed_trace', kwargs={'model': PowerFeed}),
path('power-feeds/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='powerfeed_changelog', kwargs={'model': PowerFeed}),
path('power-feeds/<int:pk>/journal/', ObjectJournalView.as_view(), name='powerfeed_journal', kwargs={'model': PowerFeed}),
path('power-feeds/<int:pk>/', include(get_model_urls('dcim', 'powerfeed'))),
]

File diff suppressed because it is too large Load Diff

View File

@ -131,24 +131,3 @@ class ConfigRevisionAdmin(admin.ModelAdmin):
})
return TemplateResponse(request, 'admin/extras/configrevision/restore.html', context)
#
# Reports & scripts
#
@admin.register(JobResult)
class JobResultAdmin(admin.ModelAdmin):
list_display = [
'obj_type', 'name', 'created', 'completed', 'user', 'status',
]
fields = [
'obj_type', 'name', 'created', 'completed', 'user', 'status', 'data', 'job_id'
]
list_filter = [
'status',
]
readonly_fields = fields
def has_add_permission(self, request):
return False

View File

@ -13,6 +13,7 @@ __all__ = [
'NestedImageAttachmentSerializer',
'NestedJobResultSerializer',
'NestedJournalEntrySerializer',
'NestedSavedFilterSerializer',
'NestedTagSerializer', # Defined in netbox.api.serializers
'NestedWebhookSerializer',
]
@ -58,6 +59,14 @@ class NestedExportTemplateSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display', 'name']
class NestedSavedFilterSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:savedfilter-detail')
class Meta:
model = models.SavedFilter
fields = ['id', 'url', 'display', 'name']
class NestedImageAttachmentSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail')

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