mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 03:56:53 -06:00
commit
def3ccfaee
3
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
3
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.3.10
|
||||
placeholder: v3.4.0
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
@ -25,6 +25,7 @@ body:
|
||||
- "3.8"
|
||||
- "3.9"
|
||||
- "3.10"
|
||||
- "3.11"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.3.10
|
||||
placeholder: v3.4.0
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -9,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:
|
||||
|
@ -4,7 +4,7 @@ bleach
|
||||
|
||||
# The Python web framework on which NetBox is built
|
||||
# https://github.com/django/django
|
||||
Django<4.1
|
||||
Django<4.2
|
||||
|
||||
# Django middleware which permits cross-domain API requests
|
||||
# https://github.com/OttoYiu/django-cors-headers
|
||||
@ -68,7 +68,7 @@ drf-yasg[validation]
|
||||
|
||||
# Django wrapper for Graphene (GraphQL support)
|
||||
# https://github.com/graphql-python/graphene-django
|
||||
graphene_django<3.0
|
||||
graphene_django
|
||||
|
||||
# WSGI HTTP server
|
||||
# https://gunicorn.org/
|
||||
|
@ -58,9 +58,11 @@ The following model fields support configurable choices:
|
||||
* `circuits.Circuit.status`
|
||||
* `dcim.Device.status`
|
||||
* `dcim.Location.status`
|
||||
* `dcim.Module.status`
|
||||
* `dcim.PowerFeed.status`
|
||||
* `dcim.Rack.status`
|
||||
* `dcim.Site.status`
|
||||
* `dcim.VirtualDeviceContext.status`
|
||||
* `extras.JournalEntry.kind`
|
||||
* `ipam.IPAddress.status`
|
||||
* `ipam.IPRange.status`
|
||||
@ -68,6 +70,7 @@ The following model fields support configurable choices:
|
||||
* `ipam.VLAN.status`
|
||||
* `virtualization.Cluster.status`
|
||||
* `virtualization.VirtualMachine.status`
|
||||
* `wireless.WirelessLAN.status`
|
||||
|
||||
The following colors are supported:
|
||||
|
||||
|
@ -141,6 +141,22 @@ When determining the primary IP address for a device, IPv6 is preferred over IPv
|
||||
|
||||
---
|
||||
|
||||
## QUEUE_MAPPINGS
|
||||
|
||||
Allows changing which queues are used internally for background tasks.
|
||||
|
||||
```python
|
||||
QUEUE_MAPPINGS = {
|
||||
'webhook': 'low',
|
||||
'report': 'high',
|
||||
'script': 'high',
|
||||
}
|
||||
```
|
||||
|
||||
If no queue is defined the queue named `default` will be used.
|
||||
|
||||
---
|
||||
|
||||
## RELEASE_CHECK_URL
|
||||
|
||||
Default: None (disabled)
|
||||
|
@ -25,7 +25,7 @@ ALLOWED_HOSTS = ['*']
|
||||
|
||||
## DATABASE
|
||||
|
||||
NetBox requires access to a PostgreSQL 10 or later database service to store data. This service can run locally on the NetBox server or on a remote system. The following parameters must be defined within the `DATABASE` dictionary:
|
||||
NetBox requires access to a PostgreSQL 11 or later database service to store data. This service can run locally on the NetBox server or on a remote system. The following parameters must be defined within the `DATABASE` dictionary:
|
||||
|
||||
* `NAME` - Database name
|
||||
* `USER` - PostgreSQL username
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -60,7 +60,7 @@ Create the HTML template for the object view. (The other views each typically em
|
||||
|
||||
## 10. Add the model to the navigation menu
|
||||
|
||||
Add the relevant navigation menu items in `netbox/netbox/navigation_menu.py`.
|
||||
Add the relevant navigation menu items in `netbox/netbox/navigation/menu.py`.
|
||||
|
||||
## 11. REST API components
|
||||
|
||||
|
@ -56,11 +56,15 @@ If the new field should be filterable, add it to the `FilterSet` for the model.
|
||||
|
||||
If the new field will be included in the object list view, add a column to the model's table. For simple fields, adding the field name to `Meta.fields` will be sufficient. More complex fields may require declaring a custom column. Also add the field name to `default_columns` if the column should be present in the table by default.
|
||||
|
||||
## 8. Update the UI templates
|
||||
## 8. Update the SearchIndex
|
||||
|
||||
Where applicable, add the new field to the model's SearchIndex for inclusion in global search.
|
||||
|
||||
## 9. Update the UI templates
|
||||
|
||||
Edit the object's view template to display the new field. There may also be a custom add/edit form template that needs to be updated.
|
||||
|
||||
## 9. Create/extend test cases
|
||||
## 10. Create/extend test cases
|
||||
|
||||
Create or extend the relevant test cases to verify that the new field and any accompanying validation logic perform as expected. This is especially important for relational fields. NetBox incorporates various test suites, including:
|
||||
|
||||
@ -72,6 +76,6 @@ Create or extend the relevant test cases to verify that the new field and any ac
|
||||
|
||||
Be diligent to ensure all of the relevant test suites are adapted or extended as necessary to test any new functionality.
|
||||
|
||||
## 10. Update the model's documentation
|
||||
## 11. Update the model's documentation
|
||||
|
||||
Each model has a dedicated page in the documentation, at `models/<app>/<model>.md`. Update this file to include any relevant information about the new field.
|
||||
|
@ -40,6 +40,7 @@ The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/
|
||||
* [dcim.RackReservation](../models/dcim/rackreservation.md)
|
||||
* [dcim.Site](../models/dcim/site.md)
|
||||
* [dcim.VirtualChassis](../models/dcim/virtualchassis.md)
|
||||
* [dcim.VirtualDeviceContext](../models/dcim/virtualdevicecontext.md)
|
||||
* [ipam.Aggregate](../models/ipam/aggregate.md)
|
||||
* [ipam.ASN](../models/ipam/asn.md)
|
||||
* [ipam.FHRPGroup](../models/ipam/fhrpgroup.md)
|
||||
|
37
docs/development/search.md
Normal file
37
docs/development/search.md
Normal 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 |
|
@ -71,13 +71,13 @@ To learn more about this feature, check out the [export template documentation](
|
||||
|
||||
NetBox administrators can install custom Python scripts, known as _reports_, which run within NetBox and can be executed and analyzed within the NetBox UI. Reports are a great way to evaluate NetBox objects against a set of arbitrary rules. For example, you could write a report to check that every router has a loopback interface with an IP address assigned, or that every site has a minimum set of VLANs defined.
|
||||
|
||||
When a report runs, its logs messages pertaining to the operations being performed, and will ultimately result in either a pass or fail. Reports can be executed via the UI, REST API, or CLI (as a management command).
|
||||
When a report runs, its logs messages pertaining to the operations being performed, and will ultimately result in either a pass or fail. Reports can be executed via the UI, REST API, or CLI (as a management command). They can be run immediately or scheduled to run at a future time.
|
||||
|
||||
To learn more about this feature, check out the [documentation for reports](../customization/reports.md).
|
||||
|
||||
## Custom Scripts
|
||||
|
||||
Custom scripts are similar to reports, but more powerful. A custom script can prompt the user for input via a form (or API data), and is built to do much more than just reporting. Custom scripts are generally used to automate tasks, such as the population of new objects in NetBox, or exchanging data with external systems.
|
||||
Custom scripts are similar to reports, but more powerful. A custom script can prompt the user for input via a form (or API data), and is built to do much more than just reporting. Custom scripts are generally used to automate tasks, such as the population of new objects in NetBox, or exchanging data with external systems. As with reports, they can be run via the UI, REST API, or CLI, and be scheduled to execute at a future time.
|
||||
|
||||
The complete Python environment is available to a custom script, including all of NetBox's internal mechanisms: There are no artificial restrictions on what a script can do. As such, custom scripting is considered an advanced feature and requires sufficient familiarity with Python and NetBox's data model.
|
||||
|
||||
|
@ -65,6 +65,10 @@ Each device can have an operational status, functional role, and software platfo
|
||||
|
||||
Sometimes it is necessary to model a set of physical devices as sharing a single management plane. Perhaps the most common example of such a scenario is stackable switches. These can be modeled as virtual chassis in NetBox, with one device acting as the chassis master and the rest as members. All components of member devices will appear on the master.
|
||||
|
||||
### Virtual Device Contexts
|
||||
|
||||
A virtual device context (VDC) is a logical partition within a device. Each VDC operates autonomously but shares a common pool of resources. Each interface can be assigned to one or more VDCs on its device.
|
||||
|
||||
## Module Types & Modules
|
||||
|
||||
Much like device types and devices, module types can instantiate discrete modules, which are hardware components installed within devices. Modules often have their own child components, which become available to the parent device. For example, when modeling a chassis-based switch with multiple line cards in NetBox, the chassis would be created (from a device type) as a device, and each of its line cards would be instantiated from a module type as a module installed in one of the device's module bays.
|
||||
|
@ -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.
|
||||
|
27
docs/features/search.md
Normal file
27
docs/features/search.md
Normal file
@ -0,0 +1,27 @@
|
||||
# Search
|
||||
|
||||
## Global Search
|
||||
|
||||
NetBox includes a powerful global search engine, providing a single convenient interface to search across its complex data model. Relevant fields on each model are indexed according to their precedence, so that the most relevant results are returned first. When objects are created or modified, the search index is updated immediately, ensuring real-time accuracy.
|
||||
|
||||
When entering a search query, the user can choose a specific lookup type: exact match, partial match, etc. When a partial match is found, the matching portion of the applicable field value is included with each result so that the user can easily determine its relevance.
|
||||
|
||||
Custom fields defined by NetBox administrators are also included in search results if configured with a search weight. Additionally, NetBox plugins can register their own custom models for inclusion alongside core models.
|
||||
|
||||
## Saved Filters
|
||||
|
||||
Each type of object in NetBox is accompanied by an extensive set of filters, each tied to a specific attribute, which enable the creation of complex queries. Often you'll find that certain queries are used routinely to apply some set of prescribed conditions to a query. Once a set of filters has been applied, NetBox offers the option to save it for future use.
|
||||
|
||||
For example, suppose you often need to locate all planned devices of a certain type within a region. The applicable filters can be applied and then saved as custom named filter for reuse, such that
|
||||
|
||||
```
|
||||
?status=planned&device_type_id=78®ion_id=12
|
||||
```
|
||||
|
||||
becomes
|
||||
|
||||
```
|
||||
?filter=my-custom-filter
|
||||
```
|
||||
|
||||
These saved filters can be used both within the UI and for API queries.
|
@ -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
|
||||
|
@ -2,8 +2,8 @@
|
||||
|
||||
This section entails the installation and configuration of a local PostgreSQL database. If you already have a PostgreSQL database service in place, skip to [the next section](2-redis.md).
|
||||
|
||||
!!! warning "PostgreSQL 10 or later required"
|
||||
NetBox requires PostgreSQL 10 or later. Please note that MySQL and other relational databases are **not** supported.
|
||||
!!! warning "PostgreSQL 11 or later required"
|
||||
NetBox requires PostgreSQL 11 or later. Please note that MySQL and other relational databases are **not** supported.
|
||||
|
||||
## Installation
|
||||
|
||||
@ -35,7 +35,7 @@ sudo systemctl start postgresql
|
||||
sudo systemctl enable postgresql
|
||||
```
|
||||
|
||||
Before continuing, verify that you have installed PostgreSQL 10 or later:
|
||||
Before continuing, verify that you have installed PostgreSQL 11 or later:
|
||||
|
||||
```no-highlight
|
||||
psql -V
|
||||
|
@ -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"
|
||||
|
||||
|
@ -18,7 +18,7 @@ The following sections detail how to set up a new instance of NetBox:
|
||||
| Dependency | Minimum Version |
|
||||
|------------|-----------------|
|
||||
| Python | 3.8 |
|
||||
| PostgreSQL | 10 |
|
||||
| PostgreSQL | 11 |
|
||||
| Redis | 4.0 |
|
||||
|
||||
Below is a simplified overview of the NetBox application stack for reference:
|
||||
|
@ -20,7 +20,7 @@ NetBox v3.0 and later require the following:
|
||||
| Dependency | Minimum Version |
|
||||
|------------|-----------------|
|
||||
| Python | 3.8 |
|
||||
| PostgreSQL | 10 |
|
||||
| PostgreSQL | 11 |
|
||||
| Redis | 4.0 |
|
||||
|
||||
## 3. Install the Latest Release
|
||||
@ -28,16 +28,15 @@ NetBox v3.0 and later require the following:
|
||||
As with the initial installation, you can upgrade NetBox by either downloading the latest release package or by cloning the `master` branch of the git repository.
|
||||
|
||||
!!! warning
|
||||
Use the same method as you used to install Netbox originally
|
||||
Use the same method as you used to install NetBox originally
|
||||
|
||||
If you are not sure how Netbox was installed originally, check with this
|
||||
command:
|
||||
If you are not sure how NetBox was installed originally, check with this command:
|
||||
|
||||
```
|
||||
ls -ld /opt/netbox /opt/netbox/.git
|
||||
```
|
||||
|
||||
If Netbox was installed from a release package, then `/opt/netbox` will be a
|
||||
If NetBox was installed from a release package, then `/opt/netbox` will be a
|
||||
symlink pointing to the current version, and `/opt/netbox/.git` will not
|
||||
exist. If it was installed from git, then `/opt/netbox` and
|
||||
`/opt/netbox/.git` will both exist as normal directories.
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -74,6 +74,6 @@ NetBox is built on the [Django](https://djangoproject.com/) Python framework and
|
||||
| HTTP service | nginx or Apache |
|
||||
| WSGI service | gunicorn or uWSGI |
|
||||
| Application | Django/Python |
|
||||
| Database | PostgreSQL 10+ |
|
||||
| Database | PostgreSQL 11+ |
|
||||
| Task queuing | Redis/django-rq |
|
||||
| Live device access | NAPALM (optional) |
|
||||
|
@ -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.
|
||||
|
@ -18,6 +18,13 @@ The [module bay](./modulebay.md) into which the module is installed.
|
||||
|
||||
The [module type](./moduletype.md) which represents the physical make & model of hardware. By default, module components will be instantiated automatically from the module type when creating a new module.
|
||||
|
||||
### Status
|
||||
|
||||
The module's operational status.
|
||||
|
||||
!!! tip
|
||||
Additional statuses may be defined by setting `Module.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
|
||||
|
||||
### Serial Number
|
||||
|
||||
The unique physical serial number assigned to this module by its manufacturer.
|
||||
|
@ -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).
|
||||
|
@ -65,6 +65,18 @@ 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).
|
||||
|
||||
### Maximum Weight
|
||||
|
||||
The maximum total weight capacity for all installed devices, inclusive of the rack itself.
|
||||
|
||||
### 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.)
|
||||
If selected, the rack's elevation will display unit 1 at the top of the rack. (Most racks use ascending numbering, with unit 1 assigned to the bottommost position.)
|
||||
|
@ -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
|
||||
|
||||
|
33
docs/models/dcim/virtualdevicecontext.md
Normal file
33
docs/models/dcim/virtualdevicecontext.md
Normal file
@ -0,0 +1,33 @@
|
||||
# Virtual Device Context
|
||||
|
||||
A virtual device context (VDC) represents a logical partition within a physical device, to which interfaces from the parent device can be allocated. Each VDC effectively provides an isolated control plane, but relies on shared resources of the parent device. A VDC is somewhat similar to a virtual machine in that it effects isolation between various components, but stops short of delivering a fully virtualized environment.
|
||||
|
||||
Each VDC must be assigned to a device upon creation, after which interfaces belonging to that device can be assigned to one or more of its VDCs. A VDC can have any number of interfaces assigned to it, and an interface can belong to any number of VDCs.
|
||||
|
||||
!!! info "A VDC by Any Other Name"
|
||||
Network vendors use differing names for this concept. Cisco uses the term VDC, whereas Juniper refers to it as a _Virtual Routing Instance_, and Fortinet uses _Virtual Domain_, for instance. While there may be some nuance among the vendors' unique implementations, the general concept remains the same for each.
|
||||
|
||||
## Fields
|
||||
|
||||
### Device
|
||||
|
||||
The device to which this VDC belongs.
|
||||
|
||||
### Name
|
||||
|
||||
The VDC's configured name. Must be unique to the assigned device.
|
||||
|
||||
### Status
|
||||
|
||||
The operational status of the VDC.
|
||||
|
||||
### Identifier
|
||||
|
||||
A vendor-prescribed unique identifier for the VDC (optional). Must be unique to the assigned device if defined.
|
||||
|
||||
### Primary IPv4 & IPv6 Addresses
|
||||
|
||||
Each VDC may designate one primary IPv4 address and/or one primary IPv6 address for management purposes.
|
||||
|
||||
!!! tip
|
||||
NetBox will prefer IPv6 addresses over IPv4 addresses by default. This can be changed by setting the `PREFER_IPV4` configuration parameter.
|
13
docs/models/extras/branch.md
Normal file
13
docs/models/extras/branch.md
Normal file
@ -0,0 +1,13 @@
|
||||
# Branches
|
||||
|
||||
A branch is a collection of related [staged changes](./stagedchange.md) that have been prepared for merging into the active database. A branch can be merged by executing its `commit()` method. Deleting a branch will delete all its related changes.
|
||||
|
||||
## Fields
|
||||
|
||||
### Name
|
||||
|
||||
The branch's name.
|
||||
|
||||
### User
|
||||
|
||||
The user to which the branch belongs (optional).
|
47
docs/models/extras/savedfilter.md
Normal file
47
docs/models/extras/savedfilter.md
Normal file
@ -0,0 +1,47 @@
|
||||
# Saved Filters
|
||||
|
||||
When filtering lists of objects in NetBox, users can save applied filters for future use. This is handy for complex filter strategies involving multiple discrete filters. For example, you might want to find all planned devices within a region that have a specific platform. Once you've applied the desired filters to the object list, simply create a saved filter with name and optional description. This filter can then be applied directly for future queries via both the UI and REST API.
|
||||
|
||||
## Fields
|
||||
|
||||
### Name
|
||||
|
||||
The filter's human-friendly name.
|
||||
|
||||
### Slug
|
||||
|
||||
The unique identifier by which this filter will be referenced during application (e.g. `?filter=my-slug`).
|
||||
|
||||
### User
|
||||
|
||||
The user to which this filter belongs. The current user will be assigned automatically when creating saved filters via the UI, and cannot be changed.
|
||||
|
||||
### Weight
|
||||
|
||||
A numeric weight used to override alphabetic ordering of filters by name. Saved filters with a lower weight will be listed before those with a higher weight.
|
||||
|
||||
### Enabled
|
||||
|
||||
Determines whether this filter can be used. Disabled filters will not appear as options in the UI, however they will be included in API results.
|
||||
|
||||
### Shared
|
||||
|
||||
Determines whether this filter is intended for use by all users or only its owner. Note that disabling this field does **not** hide the filter from other users; it is merely excluded from the list of available filters in UI object list views.
|
||||
|
||||
### Parameters
|
||||
|
||||
The query parameters to apply when the filter is active. These must be specified as JSON data. For example, the URL query string
|
||||
|
||||
```
|
||||
?status=active®ion_id=51&tag=alpha&tag=bravo
|
||||
```
|
||||
|
||||
is represented in JSON as
|
||||
|
||||
```json
|
||||
{
|
||||
'tag': ['alpha', 'bravo'],
|
||||
'status': 'active',
|
||||
'region_id': 51
|
||||
}
|
||||
```
|
26
docs/models/extras/stagedchange.md
Normal file
26
docs/models/extras/stagedchange.md
Normal file
@ -0,0 +1,26 @@
|
||||
# Staged Changes
|
||||
|
||||
A staged change represents the creation of a new object or the modification or deletion of an existing object to be performed at some future point. Each change must be assigned to a [branch](./branch.md).
|
||||
|
||||
Changes can be applied individually via the `apply()` method, however it is recommended to apply changes in bulk using the parent branch's `commit()` method.
|
||||
|
||||
## Fields
|
||||
|
||||
!!! warning
|
||||
Staged changes are not typically created or manipulated directly, but rather effected through the use of the [`checkout()`](../../plugins/development/staged-changes.md) context manager.
|
||||
|
||||
### Branch
|
||||
|
||||
The [branch](./branch.md) to which this change belongs.
|
||||
|
||||
### Action
|
||||
|
||||
The type of action this change represents: `create`, `update`, or `delete`.
|
||||
|
||||
### Object
|
||||
|
||||
A generic foreign key referencing the existing object to which this change applies.
|
||||
|
||||
### Data
|
||||
|
||||
JSON representation of the changes being made to the object (not applicable for deletions).
|
@ -19,6 +19,10 @@ The wire protocol employed by cooperating servers to maintain the virtual [IP ad
|
||||
|
||||
The group's numeric identifier.
|
||||
|
||||
### Name
|
||||
|
||||
An optional name for the FHRP group.
|
||||
|
||||
### Authentication Type
|
||||
|
||||
The type of authentication employed by group nodes, if any.
|
||||
|
@ -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.
|
||||
|
@ -4,11 +4,11 @@
|
||||
|
||||
NetBox provides several base form classes for use by plugins.
|
||||
|
||||
| Form Class | Purpose |
|
||||
|---------------------------|--------------------------------------|
|
||||
| `NetBoxModelForm` | Create/edit individual objects |
|
||||
| `NetBoxModelCSVForm` | Bulk import objects from CSV data |
|
||||
| `NetBoxModelBulkEditForm` | Edit multiple objects simultaneously |
|
||||
| Form Class | Purpose |
|
||||
|----------------------------|--------------------------------------|
|
||||
| `NetBoxModelForm` | Create/edit individual objects |
|
||||
| `NetBoxModelImportForm` | Bulk import objects from CSV data |
|
||||
| `NetBoxModelBulkEditForm` | Edit multiple objects simultaneously |
|
||||
| `NetBoxModelFilterSetForm` | Filter objects within a list view |
|
||||
|
||||
### `NetBoxModelForm`
|
||||
@ -45,19 +45,20 @@ class MyModelForm(NetBoxModelForm):
|
||||
!!! tip "Comment fields"
|
||||
If your form has a `comments` field, there's no need to list it; this will always appear last on the page.
|
||||
|
||||
### `NetBoxModelCSVForm`
|
||||
### `NetBoxModelImportForm`
|
||||
|
||||
This form facilitates the bulk import of new objects from CSV data. As with model forms, you'll need to declare a `Meta` subclass specifying the associated `model` and `fields`. NetBox also provides several form fields suitable for import various types of CSV data, listed below.
|
||||
This form facilitates the bulk import of new objects from CSV, JSON, or YAML data. As with model forms, you'll need to declare a `Meta` subclass specifying the associated `model` and `fields`. NetBox also provides several form fields suitable for import various types of CSV data, listed below.
|
||||
|
||||
**Example**
|
||||
|
||||
```python
|
||||
from dcim.models import Site
|
||||
from netbox.forms import NetBoxModelCSVForm
|
||||
from netbox.forms import NetBoxModelImportForm
|
||||
from utilities.forms import CSVModelChoiceField
|
||||
from .models import MyModel
|
||||
|
||||
class MyModelCSVForm(NetBoxModelCSVForm):
|
||||
|
||||
class MyModelImportForm(NetBoxModelImportForm):
|
||||
site = CSVModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='name',
|
||||
@ -69,6 +70,9 @@ class MyModelCSVForm(NetBoxModelCSVForm):
|
||||
fields = ('name', 'status', 'site', 'comments')
|
||||
```
|
||||
|
||||
!!! note "Previously NetBoxModelCSVForm"
|
||||
This form class was previously named `NetBoxModelCSVForm`. It was renamed in NetBox v3.4 to convey support for JSON and YAML formats in addition to CSV. The `NetBoxModelCSVForm` class has been retained for backward compatibility and functions exactly the same as `NetBoxModelImportForm`. However, plugin authors should be aware that this backward compatability will be removed in NetBox v3.5.
|
||||
|
||||
### `NetBoxModelBulkEditForm`
|
||||
|
||||
This form facilitates editing multiple objects in bulk. Unlike a model form, this form does not have a child `Meta` class, and must explicitly define each field. All fields in a bulk edit form are generally declared with `required=False`.
|
||||
@ -84,11 +88,12 @@ This form facilitates editing multiple objects in bulk. Unlike a model form, thi
|
||||
```python
|
||||
from django import forms
|
||||
from dcim.models import Site
|
||||
from netbox.forms import NetBoxModelCSVForm
|
||||
from netbox.forms import NetBoxModelImportForm
|
||||
from utilities.forms import CommentField, DynamicModelChoiceField
|
||||
from .models import MyModel, MyModelStatusChoices
|
||||
|
||||
class MyModelEditForm(NetBoxModelCSVForm):
|
||||
|
||||
class MyModelEditForm(NetBoxModelImportForm):
|
||||
name = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
|
@ -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`) |
|
||||
@ -113,13 +117,21 @@ 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 in `settings.PLUGINS_CONFIG`, mapped by plugin name. For example:
|
||||
Plugin configuration parameters can be accessed using the `get_plugin_config()` function. For example:
|
||||
|
||||
```python
|
||||
from django.conf import settings
|
||||
settings.PLUGINS_CONFIG['myplugin']['verbose_name']
|
||||
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:
|
||||
|
@ -49,23 +49,11 @@ class MyModel(NetBoxModel):
|
||||
...
|
||||
```
|
||||
|
||||
### The `clone()` Method
|
||||
### NetBoxModel Properties
|
||||
|
||||
!!! info
|
||||
This method was introduced in NetBox v3.3.
|
||||
#### `docs_url`
|
||||
|
||||
The `NetBoxModel` class includes a `clone()` method to be used for gathering attributes which can be used to create a "cloned" instance. This is used primarily for form initialization, e.g. when using the "clone" button in the NetBox UI. By default, this method will replicate any fields listed in the model's `clone_fields` list, if defined.
|
||||
|
||||
Plugin models can leverage this method by defining `clone_fields` as a list of field names to be replicated, or override this method to replace or extend its content:
|
||||
|
||||
```python
|
||||
class MyModel(NetBoxModel):
|
||||
|
||||
def clone(self):
|
||||
attrs = super().clone()
|
||||
attrs['extra-value'] = 123
|
||||
return attrs
|
||||
```
|
||||
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
|
||||
|
||||
@ -116,6 +104,8 @@ For more information about database migrations, see the [Django documentation](h
|
||||
|
||||
::: netbox.models.features.ChangeLoggingMixin
|
||||
|
||||
::: netbox.models.features.CloningMixin
|
||||
|
||||
::: netbox.models.features.CustomLinksMixin
|
||||
|
||||
::: netbox.models.features.CustomFieldsMixin
|
||||
|
@ -1,25 +1,67 @@
|
||||
# Navigation
|
||||
|
||||
## Menus
|
||||
|
||||
!!! note
|
||||
This feature was introduced in NetBox v3.4.
|
||||
|
||||
A plugin can register its own submenu as part of NetBox's navigation menu. This is done by defining a variable named `menu` in `navigation.py`, pointing to an instance of the `PluginMenu` class. Each menu must define a label and grouped menu items (discussed below), and may optionally specify an icon. An example is shown below.
|
||||
|
||||
```python title="navigation.py"
|
||||
from extras.plugins import PluginMenu
|
||||
|
||||
menu = PluginMenu(
|
||||
label='My Plugin',
|
||||
groups=(
|
||||
('Foo', (item1, item2, item3)),
|
||||
('Bar', (item4, item5)),
|
||||
),
|
||||
icon_class='mdi mdi-router'
|
||||
)
|
||||
```
|
||||
|
||||
Note that each group is a two-tuple containing a label and an iterable of menu items. The group's label serves as the section header within the submenu. A group label is required even if you have only one group of items.
|
||||
|
||||
!!! tip
|
||||
The path to the menu class can be modified by setting `menu` in the PluginConfig instance.
|
||||
|
||||
A `PluginMenu` has the following attributes:
|
||||
|
||||
| Attribute | Required | Description |
|
||||
|--------------|----------|---------------------------------------------------|
|
||||
| `label` | Yes | The text displayed as the menu heading |
|
||||
| `groups` | Yes | An iterable of named groups containing menu items |
|
||||
| `icon_class` | - | The CSS name of the icon to use for the heading |
|
||||
|
||||
!!! tip
|
||||
Supported icons can be found at [Material Design Icons](https://materialdesignicons.com/)
|
||||
|
||||
### The Default Menu
|
||||
|
||||
If your plugin has only a small number of menu items, it may be desirable to use NetBox's shared "Plugins" menu rather than creating your own. To do this, simply declare `menu_items` as a list of `PluginMenuItems` in `navigation.py`. The listed items will appear under a heading bearing the name of your plugin in the "Plugins" submenu.
|
||||
|
||||
```python title="navigation.py"
|
||||
menu_items = (item1, item2, item3)
|
||||
```
|
||||
|
||||
!!! tip
|
||||
The path to the menu items list can be modified by setting `menu_items` in the PluginConfig instance.
|
||||
|
||||
## Menu Items
|
||||
|
||||
To make its views easily accessible to users, a plugin can inject items in NetBox's navigation menu under the "Plugins" header. Menu items are added by defining a list of PluginMenuItem instances. By default, this should be a variable named `menu_items` in the file `navigation.py`. An example is shown below.
|
||||
Each menu item represents a link and (optionally) a set of buttons comprising one entry in NetBox's navigation menu. Menu items are defined as PluginMenuItem instances. An example is shown below.
|
||||
|
||||
!!! tip
|
||||
The path to declared menu items can be modified by setting `menu_items` in the PluginConfig instance.
|
||||
|
||||
```python
|
||||
```python filename="navigation.py"
|
||||
from extras.plugins import PluginMenuButton, PluginMenuItem
|
||||
from utilities.choices import ButtonColorChoices
|
||||
|
||||
menu_items = (
|
||||
PluginMenuItem(
|
||||
link='plugins:netbox_animal_sounds:random_animal',
|
||||
link_text='Random sound',
|
||||
buttons=(
|
||||
PluginMenuButton('home', 'Button A', 'fa fa-info', ButtonColorChoices.BLUE),
|
||||
PluginMenuButton('home', 'Button B', 'fa fa-warning', ButtonColorChoices.GREEN),
|
||||
)
|
||||
),
|
||||
item1 = PluginMenuItem(
|
||||
link='plugins:myplugin:myview',
|
||||
link_text='Some text',
|
||||
buttons=(
|
||||
PluginMenuButton('home', 'Button A', 'fa fa-info', ButtonColorChoices.BLUE),
|
||||
PluginMenuButton('home', 'Button B', 'fa fa-warning', ButtonColorChoices.GREEN),
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
@ -34,17 +76,19 @@ A `PluginMenuItem` has the following attributes:
|
||||
|
||||
## Menu Buttons
|
||||
|
||||
Each menu item can include a set of buttons. These can be handy for providing shortcuts related to the menu item. For instance, most items in NetBox's navigation menu include buttons to create and import new objects.
|
||||
|
||||
A `PluginMenuButton` has the following attributes:
|
||||
|
||||
| Attribute | Required | Description |
|
||||
|---------------|----------|--------------------------------------------------------------------|
|
||||
| `link` | Yes | Name of the URL path to which this button links |
|
||||
| `title` | Yes | The tooltip text (displayed when the mouse hovers over the button) |
|
||||
| `icon_class` | Yes | Button icon CSS class* |
|
||||
| `icon_class` | Yes | Button icon CSS class |
|
||||
| `color` | - | One of the choices provided by `ButtonColorChoices` |
|
||||
| `permissions` | - | A list of permissions required to display this button |
|
||||
|
||||
*NetBox supports [Material Design Icons](https://materialdesignicons.com/).
|
||||
Any buttons associated within a menu item will be shown only if the user has permission to view the link, regardless of what permissions are set on the buttons.
|
||||
|
||||
!!! note
|
||||
Any buttons associated within a menu item will be shown only if the user has permission to view the link, regardless of what permissions are set on the buttons.
|
||||
!!! tip
|
||||
Supported icons can be found at [Material Design Icons](https://materialdesignicons.com/)
|
||||
|
31
docs/plugins/development/search.md
Normal file
31
docs/plugins/development/search.md
Normal file
@ -0,0 +1,31 @@
|
||||
# Search
|
||||
|
||||
!!! note
|
||||
This feature was introduced in NetBox v3.4.
|
||||
|
||||
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
|
42
docs/plugins/development/staged-changes.md
Normal file
42
docs/plugins/development/staged-changes.md
Normal file
@ -0,0 +1,42 @@
|
||||
# Staged Changes
|
||||
|
||||
!!! danger "Experimental Feature"
|
||||
This feature is still under active development and considered experimental in nature. Its use in production is strongly discouraged at this time.
|
||||
|
||||
!!! note
|
||||
This feature was introduced in NetBox v3.4.
|
||||
|
||||
NetBox provides a programmatic API to stage the creation, modification, and deletion of objects without actually committing those changes to the active database. This can be useful for performing a "dry run" of bulk operations, or preparing a set of changes for administrative approval, for example.
|
||||
|
||||
To begin staging changes, first create a [branch](../../models/extras/branch.md):
|
||||
|
||||
```python
|
||||
from extras.models import Branch
|
||||
|
||||
branch1 = Branch.objects.create(name='branch1')
|
||||
```
|
||||
|
||||
Then, activate the branch using the `checkout()` context manager and begin making your changes. This initiates a new database transaction.
|
||||
|
||||
```python
|
||||
from extras.models import Branch
|
||||
from netbox.staging import checkout
|
||||
|
||||
branch1 = Branch.objects.get(name='branch1')
|
||||
with checkout(branch1):
|
||||
Site.objects.create(name='New Site', slug='new-site')
|
||||
# ...
|
||||
```
|
||||
|
||||
Upon exiting the context, the database transaction is automatically rolled back and your changes recorded as [staged changes](../../models/extras/stagedchange.md). Re-entering a branch will trigger a new database transaction and automatically apply any staged changes associated with the branch.
|
||||
|
||||
To apply the changes within a branch, call the branch's `commit()` method:
|
||||
|
||||
```python
|
||||
from extras.models import Branch
|
||||
|
||||
branch1 = Branch.objects.get(name='branch1')
|
||||
branch1.commit()
|
||||
```
|
||||
|
||||
Committing a branch is an all-or-none operation: Any exceptions will revert the entire set of changes. After successfully committing a branch, all its associated StagedChange objects are automatically deleted (however the branch itself will remain and can be reused).
|
@ -82,23 +82,25 @@ 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
|
||||
options:
|
||||
members:
|
||||
- get_object
|
||||
- get_template_name
|
||||
|
||||
::: netbox.views.generic.ObjectEditView
|
||||
options:
|
||||
members:
|
||||
- get_object
|
||||
- alter_object
|
||||
|
||||
::: netbox.views.generic.ObjectDeleteView
|
||||
options:
|
||||
members:
|
||||
- get_object
|
||||
members: false
|
||||
|
||||
::: netbox.views.generic.ObjectChildrenView
|
||||
options:
|
||||
@ -111,6 +113,10 @@ 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
|
||||
options:
|
||||
@ -121,7 +127,8 @@ Below are the class definitions for NetBox's multi-object views. These views han
|
||||
|
||||
::: netbox.views.generic.BulkImportView
|
||||
options:
|
||||
members: false
|
||||
members:
|
||||
- save_object
|
||||
|
||||
::: netbox.views.generic.BulkEditView
|
||||
options:
|
||||
@ -148,18 +155,51 @@ These views are provided to enable or enhance certain NetBox model features, suc
|
||||
|
||||
## Extending Core Views
|
||||
|
||||
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:
|
||||
### Additional Tabs
|
||||
|
||||
* `left_page()` - Inject content on the left side of the page
|
||||
* `right_page()` - Inject content on the right side of the page
|
||||
* `full_width_page()` - Inject content across the entire bottom of the page
|
||||
* `buttons()` - Add buttons to the top of the page
|
||||
!!! note
|
||||
This feature was introduced in NetBox v3.4.
|
||||
|
||||
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, name='myview', 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 core NetBox views. This is accomplished by subclassing `PluginTemplateExtension`, designating a particular NetBox model, and defining the desired method(s) to render custom content. Five methods are available:
|
||||
|
||||
| Method | View | Description |
|
||||
|---------------------|-------------|-----------------------------------------------------|
|
||||
| `left_page()` | Object view | Inject content on the left side of the page |
|
||||
| `right_page()` | Object view | Inject content on the right side of the page |
|
||||
| `full_width_page()` | Object view | Inject content across the entire bottom of the page |
|
||||
| `buttons()` | Object view | Add buttons to the top of the page |
|
||||
| `list_buttons()` | List view | Add buttons to the top of the page |
|
||||
|
||||
Additionally, a `render()` method is available for convenience. This method accepts the name of a template to render, and any additional context data you want to pass. Its use is optional, however.
|
||||
|
||||
When a PluginTemplateExtension is instantiated, context data is assigned to `self.context`. Available data include:
|
||||
|
||||
* `object` - The object being viewed
|
||||
* `object` - The object being viewed (object views only)
|
||||
* `model` - The model of the list view (list views only)
|
||||
* `request` - The current request
|
||||
* `settings` - Global NetBox settings
|
||||
* `config` - Plugin-specific configuration parameters
|
||||
|
225
docs/release-notes/version-3.4.md
Normal file
225
docs/release-notes/version-3.4.md
Normal file
@ -0,0 +1,225 @@
|
||||
# NetBox v3.4
|
||||
|
||||
## v3.4.0 (2022-12-14)
|
||||
|
||||
!!! warning "PostgreSQL 11 Required"
|
||||
NetBox v3.4 requires PostgreSQL 11 or later.
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* Device and virtual machine names are no longer case-sensitive. Attempting to create e.g. "device1" and "DEVICE1" within the same site will raise a validation error.
|
||||
* The `asn`, `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 ASN and contact models introduced in NetBox v3.1 prior to upgrading.
|
||||
* The `content_type` fields on the CustomLink and ExportTemplate models have been renamed to `content_types` and now support the assignment of multiple content types per object.
|
||||
* Within the Python API, 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, reference the object's `custom_field_data` attribute instead.
|
||||
* The `NetBoxModelCSVForm` class has been renamed to `NetBoxModelImportForm`. Backward compatability with the previous name has been retained for this release, but will be dropped in NetBox v3.5.
|
||||
|
||||
### 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. This new implementation provides a much faster, more intelligent search capability. Results are returned in order of precedence regardless of object type, and matching field values are highlighted in the results. Additionally, custom field values are now included in global search results (where enabled). Plugins can also register their own models with the new global search engine.
|
||||
|
||||
#### Virtual Device Contexts ([#7854](https://github.com/netbox-community/netbox/issues/7854))
|
||||
|
||||
A new model representing virtual device contexts (VDCs) has been added. VDCs are logical partitions of resources within a device that can be managed independently. A VDC is created within a device and may have device interfaces assigned to it. An interface can be allocated to any number of VDCs on its device.
|
||||
|
||||
#### Saved Filters ([#9623](https://github.com/netbox-community/netbox/issues/9623))
|
||||
|
||||
Object lists can be filtered by a variety of different fields and characteristics. Applied filters can now be saved for reuse. For example, the query string
|
||||
|
||||
```
|
||||
?status=active®ion_id=12&tenant=acme
|
||||
```
|
||||
|
||||
can be saved and applied to future queries as
|
||||
|
||||
```
|
||||
?filter=my-custom-filter
|
||||
```
|
||||
|
||||
Saved filters can be kept private, or shared among NetBox users. They can be applied to both UI and REST API searches.
|
||||
|
||||
#### JSON/YAML Bulk Imports ([#4347](https://github.com/netbox-community/netbox/issues/4347))
|
||||
|
||||
NetBox's bulk import feature, which was previously limited to CSV-formatted data for most types of objects, has been extended to accept data formatted in JSON or YAML as well. This enables users to directly import objects from a variety of sources without needing to first convert data to CSV. NetBox will attempt to automatically determine the format of import data if not specified by the user.
|
||||
|
||||
#### Update Existing Objects via Bulk Import ([#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.
|
||||
|
||||
#### Scheduled Reports & Scripts ([#8366](https://github.com/netbox-community/netbox/issues/8366))
|
||||
|
||||
Reports and custom scripts can now be scheduled for execution at a desired future time. Background scheduling is handled entirely by the existing RQ workers; there is no need to configure additional tasks to support scheduled jobs. When creating a scheduled job, the user may optionally specify an interval at which the job will run repeatedly (e.g. every 24 hours).
|
||||
|
||||
#### API for Staged Changes ([#10851](https://github.com/netbox-community/netbox/issues/10851))
|
||||
|
||||
This release introduces a new programmatic API that enables plugins and custom scripts to prepare changes in NetBox without actually committing them to the active database. To stage changes, create and activate a branch using the `checkout()` context manager. Any changes made within this context will be captured, recorded, and rolled back for future use. Once ready, a branch can be applied to the active database by calling `merge()`.
|
||||
|
||||
!!! danger "Experimental Feature"
|
||||
This feature is still under active development and considered experimental in nature. Its use in production is strongly discouraged at this time.
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#815](https://github.com/netbox-community/netbox/issues/815) - Enable specifying terminations when bulk importing circuits
|
||||
* [#6003](https://github.com/netbox-community/netbox/issues/6003) - Enable the inclusion of custom field values in global search
|
||||
* [#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
|
||||
* [#10348](https://github.com/netbox-community/netbox/issues/10348) - Add decimal custom field type
|
||||
* [#10371](https://github.com/netbox-community/netbox/issues/10371) - Add `status` field for modules
|
||||
* [#10545](https://github.com/netbox-community/netbox/issues/10545) - Standardize the use of `description` and `comments` fields on all primary models
|
||||
* [#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
|
||||
* [#10675](https://github.com/netbox-community/netbox/issues/10675) - Add `max_weight` field to track maximum load capacity for racks
|
||||
* [#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
|
||||
* [#10945](https://github.com/netbox-community/netbox/issues/10945) - Enable recurring execution of scheduled reports & scripts
|
||||
* [#11022](https://github.com/netbox-community/netbox/issues/11022) - Introduce `QUEUE_MAPPINGS` configuration parameter to allow customization of background task prioritization
|
||||
|
||||
### Bug Fixes (from v3.4-beta1)
|
||||
|
||||
* [#10946](https://github.com/netbox-community/netbox/issues/10946) - Fix AttributeError exception when viewing a device with a primary IP and no platform assigned
|
||||
* [#10948](https://github.com/netbox-community/netbox/issues/10948) - Linkify primary IPs for VDCs
|
||||
* [#10950](https://github.com/netbox-community/netbox/issues/10950) - Fix validation of VDC primary IPs
|
||||
* [#10957](https://github.com/netbox-community/netbox/issues/10957) - Add missing VDCs column to interface tables
|
||||
* [#10973](https://github.com/netbox-community/netbox/issues/10973) - Fix device links in VDC table
|
||||
* [#10980](https://github.com/netbox-community/netbox/issues/10980) - Fix view tabs for plugin objects
|
||||
* [#10982](https://github.com/netbox-community/netbox/issues/10982) - Catch `NoReverseMatch` exception when rendering tabs with no registered URL
|
||||
* [#10984](https://github.com/netbox-community/netbox/issues/10984) - Fix navigation menu expansion for plugin menus comprising multiple words
|
||||
* [#11000](https://github.com/netbox-community/netbox/issues/11000) - Improve validation of YAML-formatted import data
|
||||
* [#11046](https://github.com/netbox-community/netbox/issues/11046) - Fix exception when caching very large field values for search
|
||||
* [#11154](https://github.com/netbox-community/netbox/issues/11154) - Index VM interface MAC address and MTU for global search
|
||||
* [#11171](https://github.com/netbox-community/netbox/issues/11171) - Fix querying of related objects under GraphQL API
|
||||
|
||||
### Plugins API
|
||||
|
||||
* [#4751](https://github.com/netbox-community/netbox/issues/4751) - Enable embedding custom content on core list views via `list_buttons()` method
|
||||
* [#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) - Enable plugins to register top-level navigation menus using PluginMenu
|
||||
* [#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) - Enable plugins to install and register other Django apps via `django_apps` attribute
|
||||
* [#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
|
||||
* [#10052](https://github.com/netbox-community/netbox/issues/10052) - The `cf` attribute on objects now returns deserialized custom field data
|
||||
* [#10358](https://github.com/netbox-community/netbox/issues/10358) - Raise minimum required PostgreSQL version from 10 to 11
|
||||
* [#10694](https://github.com/netbox-community/netbox/issues/10694) - Emit the `post_save` signal when creating device components in bulk
|
||||
* [#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 unused custom `import_object()` function
|
||||
* [#10781](https://github.com/netbox-community/netbox/issues/10781) - Add support for Python v3.11
|
||||
* [#10816](https://github.com/netbox-community/netbox/issues/10816) - Pass the current request as context 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
|
||||
|
||||
* Added the `/api/dcim/virtual-device-contexts/` endpoint
|
||||
* 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 `description`, `weight`, and `weight_unit` fields
|
||||
* dcim.Module
|
||||
* Added a `description` field
|
||||
* dcim.Interface
|
||||
* Added the `vdcs` field
|
||||
* dcim.Module
|
||||
* Added a required `status` field
|
||||
* dcim.ModuleType
|
||||
* Added `description`, `weight`, and `weight_unit` fields
|
||||
* dcim.PowerFeed
|
||||
* Added a `description` field
|
||||
* dcim.PowerPanel
|
||||
* Added `description` and `comments` fields
|
||||
* dcim.Rack
|
||||
* Added `description`, `mounting_depth`, `weight`, `max_weight`, and `weight_unit` fields
|
||||
* dcim.RackReservation
|
||||
* Added a `comments` field
|
||||
* dcim.VirtualChassis
|
||||
* Added `description` and `comments` fields
|
||||
* extras.CustomField
|
||||
* Added a `search_weight` field
|
||||
* extras.CustomLink
|
||||
* Renamed `content_type` field to `content_types`
|
||||
* extras.ExportTemplate
|
||||
* Renamed `content_type` field to `content_types`
|
||||
* extras.JobResult
|
||||
* Added `interval`, `scheduled`, and `started` fields
|
||||
* ipam.Aggregate
|
||||
* Added a `comments` field
|
||||
* ipam.ASN
|
||||
* Added a `comments` field
|
||||
* ipam.FHRPGroup
|
||||
* Added `name` and `comments` fields
|
||||
* 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
|
@ -72,6 +72,7 @@ nav:
|
||||
- Virtualization: 'features/virtualization.md'
|
||||
- Tenancy: 'features/tenancy.md'
|
||||
- Contacts: 'features/contacts.md'
|
||||
- Search: 'features/search.md'
|
||||
- Context Data: 'features/context-data.md'
|
||||
- Change Logging: 'features/change-logging.md'
|
||||
- Journaling: 'features/journaling.md'
|
||||
@ -128,9 +129,11 @@ nav:
|
||||
- Tables: 'plugins/development/tables.md'
|
||||
- Forms: 'plugins/development/forms.md'
|
||||
- Filters & Filter Sets: 'plugins/development/filtersets.md'
|
||||
- Search: 'plugins/development/search.md'
|
||||
- REST API: 'plugins/development/rest-api.md'
|
||||
- GraphQL API: 'plugins/development/graphql-api.md'
|
||||
- Background Tasks: 'plugins/development/background-tasks.md'
|
||||
- Staged Changes: 'plugins/development/staged-changes.md'
|
||||
- Exceptions: 'plugins/development/exceptions.md'
|
||||
- Administration:
|
||||
- Authentication:
|
||||
@ -189,13 +192,17 @@ nav:
|
||||
- Site: 'models/dcim/site.md'
|
||||
- SiteGroup: 'models/dcim/sitegroup.md'
|
||||
- VirtualChassis: 'models/dcim/virtualchassis.md'
|
||||
- VirtualDeviceContext: 'models/dcim/virtualdevicecontext.md'
|
||||
- Extras:
|
||||
- Branch: 'models/extras/branch.md'
|
||||
- ConfigContext: 'models/extras/configcontext.md'
|
||||
- CustomField: 'models/extras/customfield.md'
|
||||
- CustomLink: 'models/extras/customlink.md'
|
||||
- ExportTemplate: 'models/extras/exporttemplate.md'
|
||||
- ImageAttachment: 'models/extras/imageattachment.md'
|
||||
- JournalEntry: 'models/extras/journalentry.md'
|
||||
- SavedFilter: 'models/extras/savedfilter.md'
|
||||
- StagedChange: 'models/extras/stagedchange.md'
|
||||
- Tag: 'models/extras/tag.md'
|
||||
- Webhook: 'models/extras/webhook.md'
|
||||
- IPAM:
|
||||
@ -244,6 +251,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'
|
||||
@ -251,6 +259,7 @@ nav:
|
||||
- git Cheat Sheet: 'development/git-cheat-sheet.md'
|
||||
- Release Notes:
|
||||
- Summary: 'release-notes/index.md'
|
||||
- Version 3.4: 'release-notes/version-3.4.md'
|
||||
- Version 3.3: 'release-notes/version-3.3.md'
|
||||
- Version 3.2: 'release-notes/version-3.2.md'
|
||||
- Version 3.1: 'release-notes/version-3.1.md'
|
||||
|
@ -31,8 +31,8 @@ class ProviderSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact',
|
||||
'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',
|
||||
]
|
||||
|
||||
|
||||
|
@ -6,4 +6,4 @@ class CircuitsConfig(AppConfig):
|
||||
verbose_name = "Circuits"
|
||||
|
||||
def ready(self):
|
||||
import circuits.signals
|
||||
from . import signals, search
|
||||
|
@ -1,5 +1,6 @@
|
||||
import django_filters
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from dcim.filtersets import CabledObjectFilterSet
|
||||
from dcim.models import Region, Site, SiteGroup
|
||||
@ -24,48 +25,48 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
|
||||
queryset=Region.objects.all(),
|
||||
field_name='circuits__terminations__site__region',
|
||||
lookup_expr='in',
|
||||
label='Region (ID)',
|
||||
label=_('Region (ID)'),
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='circuits__terminations__site__region',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
label=_('Region (slug)'),
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='circuits__terminations__site__group',
|
||||
lookup_expr='in',
|
||||
label='Site group (ID)',
|
||||
label=_('Site group (ID)'),
|
||||
)
|
||||
site_group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='circuits__terminations__site__group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Site group (slug)',
|
||||
label=_('Site group (slug)'),
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='circuits__terminations__site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site',
|
||||
label=_('Site'),
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='circuits__terminations__site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
label=_('Site (slug)'),
|
||||
)
|
||||
asn_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='asns',
|
||||
queryset=ASN.objects.all(),
|
||||
label='ASN (ID)',
|
||||
label=_('ASN (ID)'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ['id', 'name', 'slug', 'asn', 'account']
|
||||
fields = ['id', 'name', 'slug', 'account']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@ -73,8 +74,6 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(account__icontains=value) |
|
||||
Q(noc_contact__icontains=value) |
|
||||
Q(admin_contact__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
|
||||
@ -82,13 +81,13 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
|
||||
class ProviderNetworkFilterSet(NetBoxModelFilterSet):
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Provider.objects.all(),
|
||||
label='Provider (ID)',
|
||||
label=_('Provider (ID)'),
|
||||
)
|
||||
provider = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider__slug',
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Provider (slug)',
|
||||
label=_('Provider (slug)'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@ -116,28 +115,28 @@ class CircuitTypeFilterSet(OrganizationalModelFilterSet):
|
||||
class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Provider.objects.all(),
|
||||
label='Provider (ID)',
|
||||
label=_('Provider (ID)'),
|
||||
)
|
||||
provider = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider__slug',
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Provider (slug)',
|
||||
label=_('Provider (slug)'),
|
||||
)
|
||||
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='terminations__provider_network',
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
label='ProviderNetwork (ID)',
|
||||
label=_('ProviderNetwork (ID)'),
|
||||
)
|
||||
type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=CircuitType.objects.all(),
|
||||
label='Circuit type (ID)',
|
||||
label=_('Circuit type (ID)'),
|
||||
)
|
||||
type = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='type__slug',
|
||||
queryset=CircuitType.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Circuit type (slug)',
|
||||
label=_('Circuit type (slug)'),
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=CircuitStatusChoices,
|
||||
@ -147,38 +146,38 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
|
||||
queryset=Region.objects.all(),
|
||||
field_name='terminations__site__region',
|
||||
lookup_expr='in',
|
||||
label='Region (ID)',
|
||||
label=_('Region (ID)'),
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='terminations__site__region',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
label=_('Region (slug)'),
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='terminations__site__group',
|
||||
lookup_expr='in',
|
||||
label='Site group (ID)',
|
||||
label=_('Site group (ID)'),
|
||||
)
|
||||
site_group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='terminations__site__group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Site group (slug)',
|
||||
label=_('Site group (slug)'),
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='terminations__site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
label=_('Site (ID)'),
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='terminations__site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
label=_('Site (slug)'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@ -201,25 +200,25 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
|
||||
class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
label=_('Search'),
|
||||
)
|
||||
circuit_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Circuit.objects.all(),
|
||||
label='Circuit',
|
||||
label=_('Circuit'),
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
label=_('Site (ID)'),
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
label=_('Site (slug)'),
|
||||
)
|
||||
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
label='ProviderNetwork (ID)',
|
||||
label=_('ProviderNetwork (ID)'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
@ -1,4 +1,4 @@
|
||||
from .bulk_edit import *
|
||||
from .bulk_import import *
|
||||
from .filtersets import *
|
||||
from .models import *
|
||||
from .model_forms import *
|
||||
|
@ -20,10 +20,6 @@ __all__ = (
|
||||
|
||||
|
||||
class ProviderBulkEditForm(NetBoxModelBulkEditForm):
|
||||
asn = forms.IntegerField(
|
||||
required=False,
|
||||
label='ASN (legacy)'
|
||||
)
|
||||
asns = DynamicModelMultipleChoiceField(
|
||||
queryset=ASN.objects.all(),
|
||||
label=_('ASNs'),
|
||||
@ -32,33 +28,23 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
|
||||
account = forms.CharField(
|
||||
max_length=30,
|
||||
required=False,
|
||||
label='Account number'
|
||||
label=_('Account number')
|
||||
)
|
||||
portal_url = forms.URLField(
|
||||
required=False,
|
||||
label='Portal'
|
||||
)
|
||||
noc_contact = forms.CharField(
|
||||
required=False,
|
||||
widget=SmallTextarea,
|
||||
label='NOC contact'
|
||||
)
|
||||
admin_contact = forms.CharField(
|
||||
required=False,
|
||||
widget=SmallTextarea,
|
||||
label='Admin contact'
|
||||
description = forms.CharField(
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label='Comments'
|
||||
label=_('Comments')
|
||||
)
|
||||
|
||||
model = Provider
|
||||
fieldsets = (
|
||||
(None, ('asn', 'asns', 'account', 'portal_url', 'noc_contact', 'admin_contact')),
|
||||
(None, ('asns', 'account', )),
|
||||
)
|
||||
nullable_fields = (
|
||||
'asn', 'asns', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
|
||||
'asns', 'account', 'description', 'comments',
|
||||
)
|
||||
|
||||
|
||||
@ -70,7 +56,7 @@ class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm):
|
||||
service_id = forms.CharField(
|
||||
max_length=100,
|
||||
required=False,
|
||||
label='Service ID'
|
||||
label=_('Service ID')
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=200,
|
||||
@ -78,7 +64,7 @@ class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm):
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label='Comments'
|
||||
label=_('Comments')
|
||||
)
|
||||
|
||||
model = ProviderNetwork
|
||||
@ -132,7 +118,7 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
|
||||
)
|
||||
commit_rate = forms.IntegerField(
|
||||
required=False,
|
||||
label='Commit rate (Kbps)'
|
||||
label=_('Commit rate (Kbps)')
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=100,
|
||||
@ -140,7 +126,7 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label='Comments'
|
||||
label=_('Comments')
|
||||
)
|
||||
|
||||
model = Circuit
|
||||
|
@ -1,77 +1,102 @@
|
||||
from django import forms
|
||||
|
||||
from circuits.choices import CircuitStatusChoices
|
||||
from circuits.models import *
|
||||
from netbox.forms import NetBoxModelCSVForm
|
||||
from dcim.models import Site
|
||||
from django.utils.translation import gettext as _
|
||||
from netbox.forms import NetBoxModelImportForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import CSVChoiceField, CSVModelChoiceField, SlugField
|
||||
from utilities.forms import BootstrapMixin, CSVChoiceField, CSVModelChoiceField, SlugField
|
||||
|
||||
__all__ = (
|
||||
'CircuitCSVForm',
|
||||
'CircuitTypeCSVForm',
|
||||
'ProviderCSVForm',
|
||||
'ProviderNetworkCSVForm',
|
||||
'CircuitImportForm',
|
||||
'CircuitTerminationImportForm',
|
||||
'CircuitTypeImportForm',
|
||||
'ProviderImportForm',
|
||||
'ProviderNetworkImportForm',
|
||||
)
|
||||
|
||||
|
||||
class ProviderCSVForm(NetBoxModelCSVForm):
|
||||
class ProviderImportForm(NetBoxModelImportForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = (
|
||||
'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
|
||||
'name', 'slug', 'account', 'description', 'comments', 'tags',
|
||||
)
|
||||
|
||||
|
||||
class ProviderNetworkCSVForm(NetBoxModelCSVForm):
|
||||
class ProviderNetworkImportForm(NetBoxModelImportForm):
|
||||
provider = CSVModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Assigned provider'
|
||||
help_text=_('Assigned provider')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ProviderNetwork
|
||||
fields = [
|
||||
'provider', 'name', 'service_id', 'description', 'comments',
|
||||
'provider', 'name', 'service_id', 'description', 'comments', 'tags'
|
||||
]
|
||||
|
||||
|
||||
class CircuitTypeCSVForm(NetBoxModelCSVForm):
|
||||
class CircuitTypeImportForm(NetBoxModelImportForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = ('name', 'slug', 'description')
|
||||
fields = ('name', 'slug', 'description', 'tags')
|
||||
help_texts = {
|
||||
'name': 'Name of circuit type',
|
||||
'name': _('Name of circuit type'),
|
||||
}
|
||||
|
||||
|
||||
class CircuitCSVForm(NetBoxModelCSVForm):
|
||||
class CircuitImportForm(NetBoxModelImportForm):
|
||||
provider = CSVModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Assigned provider'
|
||||
help_text=_('Assigned provider')
|
||||
)
|
||||
type = CSVModelChoiceField(
|
||||
queryset=CircuitType.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Type of circuit'
|
||||
help_text=_('Type of circuit')
|
||||
)
|
||||
status = CSVChoiceField(
|
||||
choices=CircuitStatusChoices,
|
||||
help_text='Operational status'
|
||||
help_text=_('Operational status')
|
||||
)
|
||||
tenant = CSVModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Assigned tenant'
|
||||
help_text=_('Assigned tenant')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = [
|
||||
'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate',
|
||||
'description', 'comments',
|
||||
'description', 'comments', 'tags'
|
||||
]
|
||||
|
||||
|
||||
class CircuitTerminationImportForm(BootstrapMixin, forms.ModelForm):
|
||||
site = CSVModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False
|
||||
)
|
||||
provider_network = CSVModelChoiceField(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
|
||||
'pp_info', 'description',
|
||||
]
|
||||
|
@ -20,7 +20,7 @@ __all__ = (
|
||||
class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
model = Provider
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
(None, ('q', 'filter_id', '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_id', '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_id', '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')),
|
||||
|
@ -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,29 +29,17 @@ class ProviderForm(NetBoxModelForm):
|
||||
comments = CommentField()
|
||||
|
||||
fieldsets = (
|
||||
('Provider', ('name', 'slug', 'asn', 'asns', 'tags')),
|
||||
('Support Info', ('account', 'portal_url', 'noc_contact', 'admin_contact')),
|
||||
('Provider', ('name', 'slug', 'asns', 'description', 'tags')),
|
||||
('Support Info', ('account',)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = [
|
||||
'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'asns', 'comments', 'tags',
|
||||
'name', 'slug', 'account', 'asns', 'description', 'comments', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'noc_contact': SmallTextarea(
|
||||
attrs={'rows': 5}
|
||||
),
|
||||
'admin_contact': SmallTextarea(
|
||||
attrs={'rows': 5}
|
||||
),
|
||||
}
|
||||
help_texts = {
|
||||
'name': "Full name of the provider",
|
||||
'asn': "BGP autonomous system number (if applicable)",
|
||||
'portal_url': "URL of the provider's customer support portal",
|
||||
'noc_contact': "NOC email address and phone number",
|
||||
'admin_contact': "Administrative contact email address and phone number",
|
||||
'name': _("Full name of the provider"),
|
||||
}
|
||||
|
||||
|
||||
@ -111,8 +98,8 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
|
||||
'tenant_group', 'tenant', 'comments', 'tags',
|
||||
]
|
||||
help_texts = {
|
||||
'cid': "Unique circuit ID",
|
||||
'commit_rate': "Committed rate",
|
||||
'cid': _("Unique circuit ID"),
|
||||
'commit_rate': _("Committed rate"),
|
||||
}
|
||||
widgets = {
|
||||
'status': StaticSelect(),
|
||||
@ -182,9 +169,9 @@ class CircuitTerminationForm(NetBoxModelForm):
|
||||
'description', 'tags',
|
||||
]
|
||||
help_texts = {
|
||||
'port_speed': "Physical circuit speed",
|
||||
'xconnect_id': "ID of the local cross-connect",
|
||||
'pp_info': "Patch panel ID and port number(s)"
|
||||
'port_speed': _("Physical circuit speed"),
|
||||
'xconnect_id': _("ID of the local cross-connect"),
|
||||
'pp_info': _("Patch panel ID and port number(s)")
|
||||
}
|
||||
widgets = {
|
||||
'term_side': StaticSelect(),
|
@ -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
|
||||
|
@ -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)),
|
||||
|
@ -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',
|
||||
|
39
netbox/circuits/migrations/0039_unique_constraints.py
Normal file
39
netbox/circuits/migrations/0039_unique_constraints.py
Normal file
@ -0,0 +1,39 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0038_cabling_cleanup'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveConstraint(
|
||||
model_name='providernetwork',
|
||||
name='circuits_providernetwork_provider_name',
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='circuit',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='circuittermination',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='providernetwork',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='circuit',
|
||||
constraint=models.UniqueConstraint(fields=('provider', 'cid'), name='circuits_circuit_unique_provider_cid'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='circuittermination',
|
||||
constraint=models.UniqueConstraint(fields=('circuit', 'term_side'), name='circuits_circuittermination_unique_circuit_term_side'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='providernetwork',
|
||||
constraint=models.UniqueConstraint(fields=('provider', 'name'), name='circuits_providernetwork_unique_provider_name'),
|
||||
),
|
||||
]
|
@ -0,0 +1,59 @@
|
||||
import os
|
||||
|
||||
from django.db import migrations
|
||||
from django.db.utils import DataError
|
||||
|
||||
|
||||
def check_legacy_data(apps, schema_editor):
|
||||
"""
|
||||
Abort the migration if any legacy provider fields still contain data.
|
||||
"""
|
||||
Provider = apps.get_model('circuits', 'Provider')
|
||||
|
||||
provider_count = Provider.objects.exclude(asn__isnull=True).count()
|
||||
if provider_count and 'NETBOX_DELETE_LEGACY_DATA' not in os.environ:
|
||||
raise DataError(
|
||||
f"Unable to proceed with deleting asn field from Provider model: Found {provider_count} "
|
||||
f"providers with legacy ASN data. Please ensure all legacy provider ASN data has been "
|
||||
f"migrated to ASN objects before proceeding. Or, set the NETBOX_DELETE_LEGACY_DATA "
|
||||
f"environment variable to bypass this safeguard and delete all legacy provider ASN data."
|
||||
)
|
||||
|
||||
provider_count = Provider.objects.exclude(admin_contact='', noc_contact='', portal_url='').count()
|
||||
if provider_count and 'NETBOX_DELETE_LEGACY_DATA' not in os.environ:
|
||||
raise DataError(
|
||||
f"Unable to proceed with deleting contact fields from Provider model: Found {provider_count} "
|
||||
f"providers with legacy contact data. Please ensure all legacy provider contact data has been "
|
||||
f"migrated to contact objects before proceeding. Or, set the NETBOX_DELETE_LEGACY_DATA "
|
||||
f"environment variable to bypass this safeguard and delete all legacy provider contact data."
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0039_unique_constraints'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
code=check_legacy_data,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='provider',
|
||||
name='admin_contact',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='provider',
|
||||
name='asn',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='provider',
|
||||
name='noc_contact',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='provider',
|
||||
name='portal_url',
|
||||
),
|
||||
]
|
@ -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),
|
||||
),
|
||||
]
|
@ -3,11 +3,12 @@ from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
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 +24,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 +74,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(
|
||||
@ -129,18 +104,23 @@ class Circuit(NetBoxModel):
|
||||
clone_fields = (
|
||||
'provider', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate', 'description',
|
||||
)
|
||||
prerequisite_models = (
|
||||
'circuits.CircuitType',
|
||||
'circuits.Provider',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['provider', 'cid']
|
||||
unique_together = ['provider', 'cid']
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('provider', 'cid'),
|
||||
name='%(app_label)s_%(class)s_unique_provider_cid'
|
||||
),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.cid
|
||||
|
||||
@classmethod
|
||||
def get_prerequisite_models(cls):
|
||||
return [apps.get_model('circuits.Provider'), CircuitType]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:circuit', args=[self.pk])
|
||||
|
||||
@ -189,7 +169,7 @@ class CircuitTermination(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name='Upstream speed (Kbps)',
|
||||
help_text='Upstream speed, if different from port speed'
|
||||
help_text=_('Upstream speed, if different from port speed')
|
||||
)
|
||||
xconnect_id = models.CharField(
|
||||
max_length=50,
|
||||
@ -208,7 +188,12 @@ class CircuitTermination(
|
||||
|
||||
class Meta:
|
||||
ordering = ['circuit', 'term_side']
|
||||
unique_together = ['circuit', 'term_side']
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('circuit', 'term_side'),
|
||||
name='%(app_label)s_%(class)s_unique_circuit_term_side'
|
||||
),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f'Termination {self.term_side}: {self.site or self.provider_network}'
|
||||
|
@ -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.
|
||||
@ -24,12 +23,6 @@ class Provider(NetBoxModel):
|
||||
max_length=100,
|
||||
unique=True
|
||||
)
|
||||
asn = ASNField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name='ASN',
|
||||
help_text='32-bit autonomous system number'
|
||||
)
|
||||
asns = models.ManyToManyField(
|
||||
to='ipam.ASN',
|
||||
related_name='providers',
|
||||
@ -40,21 +33,6 @@ class Provider(NetBoxModel):
|
||||
blank=True,
|
||||
verbose_name='Account number'
|
||||
)
|
||||
portal_url = models.URLField(
|
||||
blank=True,
|
||||
verbose_name='Portal URL'
|
||||
)
|
||||
noc_contact = models.TextField(
|
||||
blank=True,
|
||||
verbose_name='NOC contact'
|
||||
)
|
||||
admin_contact = models.TextField(
|
||||
blank=True,
|
||||
verbose_name='Admin contact'
|
||||
)
|
||||
comments = models.TextField(
|
||||
blank=True
|
||||
)
|
||||
|
||||
# Generic relations
|
||||
contacts = GenericRelation(
|
||||
@ -62,7 +40,7 @@ class Provider(NetBoxModel):
|
||||
)
|
||||
|
||||
clone_fields = (
|
||||
'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact',
|
||||
'account',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@ -75,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.
|
||||
@ -93,23 +71,15 @@ 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')
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('provider', 'name'),
|
||||
name='circuits_providernetwork_provider_name'
|
||||
name='%(app_label)s_%(class)s_unique_provider_name'
|
||||
),
|
||||
)
|
||||
unique_together = ('provider', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
56
netbox/circuits/search.py
Normal file
56
netbox/circuits/search.py
Normal file
@ -0,0 +1,56 @@
|
||||
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),
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
|
||||
|
||||
@register_search
|
||||
class ProviderNetworkIndex(SearchIndex):
|
||||
model = models.ProviderNetwork
|
||||
fields = (
|
||||
('name', 100),
|
||||
('service_id', 200),
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
@ -39,10 +39,10 @@ class ProviderTable(ContactsColumnMixin, NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = Provider
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'asn', 'asns', 'account', 'portal_url', 'noc_contact', 'admin_contact', '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', 'asn', 'account', 'circuit_count')
|
||||
default_columns = ('pk', 'name', 'account', 'circuit_count')
|
||||
|
||||
|
||||
class ProviderNetworkTable(NetBoxTable):
|
||||
|
@ -20,7 +20,7 @@ class ProviderTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Provider
|
||||
brief_fields = ['circuit_count', 'display', 'id', 'name', 'slug', 'url']
|
||||
bulk_update_data = {
|
||||
'asn': 1234,
|
||||
'account': '1234',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
@ -25,11 +25,11 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
ASN.objects.bulk_create(asns)
|
||||
|
||||
providers = (
|
||||
Provider(name='Provider 1', slug='provider-1', asn=65001, account='1234'),
|
||||
Provider(name='Provider 2', slug='provider-2', asn=65002, account='2345'),
|
||||
Provider(name='Provider 3', slug='provider-3', asn=65003, account='3456'),
|
||||
Provider(name='Provider 4', slug='provider-4', asn=65004, account='4567'),
|
||||
Provider(name='Provider 5', slug='provider-5', asn=65005, account='5678'),
|
||||
Provider(name='Provider 1', slug='provider-1', account='1234'),
|
||||
Provider(name='Provider 2', slug='provider-2', account='2345'),
|
||||
Provider(name='Provider 3', slug='provider-3', account='3456'),
|
||||
Provider(name='Provider 4', slug='provider-4', account='4567'),
|
||||
Provider(name='Provider 5', slug='provider-5', account='5678'),
|
||||
)
|
||||
Provider.objects.bulk_create(providers)
|
||||
providers[0].asns.set([asns[0]])
|
||||
@ -82,10 +82,6 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'slug': ['provider-1', 'provider-2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_asn(self): # Legacy field
|
||||
params = {'asn': ['65001', '65002']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_asn_id(self): # ASN object assignment
|
||||
asns = ASN.objects.all()[:2]
|
||||
params = {'asn_id': [asns[0].pk, asns[1].pk]}
|
||||
|
@ -23,9 +23,9 @@ class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
ASN.objects.bulk_create(asns)
|
||||
|
||||
providers = (
|
||||
Provider(name='Provider 1', slug='provider-1', asn=65001),
|
||||
Provider(name='Provider 2', slug='provider-2', asn=65002),
|
||||
Provider(name='Provider 3', slug='provider-3', asn=65003),
|
||||
Provider(name='Provider 1', slug='provider-1'),
|
||||
Provider(name='Provider 2', slug='provider-2'),
|
||||
Provider(name='Provider 3', slug='provider-3'),
|
||||
)
|
||||
Provider.objects.bulk_create(providers)
|
||||
providers[0].asns.set([asns[0], asns[1]])
|
||||
@ -37,12 +37,8 @@ class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
cls.form_data = {
|
||||
'name': 'Provider X',
|
||||
'slug': 'provider-x',
|
||||
'asn': 65123,
|
||||
'asns': [asns[6].pk, asns[7].pk],
|
||||
'account': '1234',
|
||||
'portal_url': 'http://example.com/portal',
|
||||
'noc_contact': 'noc@example.com',
|
||||
'admin_contact': 'admin@example.com',
|
||||
'comments': 'Another provider',
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
@ -54,12 +50,15 @@ 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 = {
|
||||
'asn': 65009,
|
||||
'account': '5678',
|
||||
'portal_url': 'http://example.com/portal2',
|
||||
'noc_contact': 'noc2@example.com',
|
||||
'admin_contact': 'admin2@example.com',
|
||||
'comments': 'New comments',
|
||||
}
|
||||
|
||||
@ -70,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')
|
||||
|
||||
@ -92,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',
|
||||
}
|
||||
@ -100,12 +108,19 @@ class CircuitTypeTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = Circuit
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.add_permissions(
|
||||
'circuits.add_circuittermination',
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
providers = (
|
||||
Provider(name='Provider 1', slug='provider-1', asn=65001),
|
||||
Provider(name='Provider 2', slug='provider-2', asn=65002),
|
||||
Provider(name='Provider 1', slug='provider-1'),
|
||||
Provider(name='Provider 2', slug='provider-2'),
|
||||
)
|
||||
Provider.objects.bulk_create(providers)
|
||||
|
||||
@ -115,11 +130,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')
|
||||
|
||||
@ -144,6 +161,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,
|
||||
@ -167,11 +191,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')
|
||||
|
||||
@ -190,6 +216,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',
|
||||
|
@ -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'))),
|
||||
|
||||
]
|
||||
|
@ -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,18 +44,20 @@ 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()
|
||||
|
||||
|
||||
class ProviderBulkImportView(generic.BulkImportView):
|
||||
queryset = Provider.objects.all()
|
||||
model_form = forms.ProviderCSVForm
|
||||
model_form = forms.ProviderImportForm
|
||||
table = tables.ProviderTable
|
||||
|
||||
|
||||
@ -84,6 +89,7 @@ class ProviderNetworkListView(generic.ObjectListView):
|
||||
table = tables.ProviderNetworkTable
|
||||
|
||||
|
||||
@register_model_view(ProviderNetwork)
|
||||
class ProviderNetworkView(generic.ObjectView):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
|
||||
@ -103,18 +109,20 @@ 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()
|
||||
|
||||
|
||||
class ProviderNetworkBulkImportView(generic.BulkImportView):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
model_form = forms.ProviderNetworkCSVForm
|
||||
model_form = forms.ProviderNetworkImportForm
|
||||
table = tables.ProviderNetworkTable
|
||||
|
||||
|
||||
@ -144,6 +152,7 @@ class CircuitTypeListView(generic.ObjectListView):
|
||||
table = tables.CircuitTypeTable
|
||||
|
||||
|
||||
@register_model_view(CircuitType)
|
||||
class CircuitTypeView(generic.ObjectView):
|
||||
queryset = CircuitType.objects.all()
|
||||
|
||||
@ -157,18 +166,20 @@ 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()
|
||||
|
||||
|
||||
class CircuitTypeBulkImportView(generic.BulkImportView):
|
||||
queryset = CircuitType.objects.all()
|
||||
model_form = forms.CircuitTypeCSVForm
|
||||
model_form = forms.CircuitTypeImportForm
|
||||
table = tables.CircuitTypeTable
|
||||
|
||||
|
||||
@ -202,23 +213,36 @@ 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()
|
||||
|
||||
|
||||
class CircuitBulkImportView(generic.BulkImportView):
|
||||
queryset = Circuit.objects.all()
|
||||
model_form = forms.CircuitCSVForm
|
||||
model_form = forms.CircuitImportForm
|
||||
table = tables.CircuitTable
|
||||
additional_permissions = [
|
||||
'circuits.add_circuittermination',
|
||||
]
|
||||
related_object_forms = {
|
||||
'terminations': forms.CircuitTerminationImportForm,
|
||||
}
|
||||
|
||||
def prep_related_object_data(self, parent, data):
|
||||
data.update({'circuit': parent})
|
||||
return data
|
||||
|
||||
|
||||
class CircuitBulkEditView(generic.BulkEditView):
|
||||
@ -318,11 +342,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)
|
||||
|
@ -45,6 +45,7 @@ __all__ = [
|
||||
'NestedSiteSerializer',
|
||||
'NestedSiteGroupSerializer',
|
||||
'NestedVirtualChassisSerializer',
|
||||
'NestedVirtualDeviceContextSerializer',
|
||||
]
|
||||
|
||||
|
||||
@ -466,3 +467,12 @@ class NestedPowerFeedSerializer(WritableNestedSerializer):
|
||||
class Meta:
|
||||
model = models.PowerFeed
|
||||
fields = ['id', 'url', 'display', 'name', 'cable', '_occupied']
|
||||
|
||||
|
||||
class NestedVirtualDeviceContextSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualdevicecontext-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
|
||||
class Meta:
|
||||
model = models.VirtualDeviceContext
|
||||
fields = ['id', 'url', 'display', 'name', 'identifier', 'device']
|
||||
|
@ -1,6 +1,7 @@
|
||||
import decimal
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import gettext as _
|
||||
from drf_yasg.utils import swagger_serializer_method
|
||||
from rest_framework import serializers
|
||||
from timezone_field.rest_framework import TimeZoneSerializerField
|
||||
@ -197,10 +198,11 @@ class RackSerializer(NetBoxModelSerializer):
|
||||
status = ChoiceField(choices=RackStatusChoices, required=False)
|
||||
role = NestedRackRoleSerializer(required=False, allow_null=True)
|
||||
type = ChoiceField(choices=RackTypeChoices, allow_blank=True, required=False)
|
||||
facility_id = serializers.CharField(max_length=50, allow_blank=True, allow_null=True, label='Facility ID',
|
||||
facility_id = serializers.CharField(max_length=50, allow_blank=True, allow_null=True, label=_('Facility ID'),
|
||||
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 +210,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', 'max_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 +244,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',
|
||||
]
|
||||
|
||||
|
||||
@ -309,33 +312,34 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
|
||||
u_height = serializers.DecimalField(
|
||||
max_digits=4,
|
||||
decimal_places=1,
|
||||
label='Position (U)',
|
||||
label=_('Position (U)'),
|
||||
min_value=0,
|
||||
default=1.0
|
||||
)
|
||||
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',
|
||||
]
|
||||
|
||||
|
||||
@ -633,7 +637,7 @@ class DeviceSerializer(NetBoxModelSerializer):
|
||||
max_digits=4,
|
||||
decimal_places=1,
|
||||
allow_null=True,
|
||||
label='Position (U)',
|
||||
label=_('Position (U)'),
|
||||
min_value=decimal.Decimal(0.5),
|
||||
default=None
|
||||
)
|
||||
@ -652,8 +656,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)
|
||||
@ -668,17 +672,37 @@ class DeviceSerializer(NetBoxModelSerializer):
|
||||
return data
|
||||
|
||||
|
||||
class VirtualDeviceContextSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True, default=None)
|
||||
primary_ip = NestedIPAddressSerializer(read_only=True)
|
||||
primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||
primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||
|
||||
# Related object counts
|
||||
interface_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = VirtualDeviceContext
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'device', 'identifier', 'tenant', 'primary_ip', 'primary_ip4',
|
||||
'primary_ip6', 'status', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'interface_count',
|
||||
]
|
||||
|
||||
|
||||
class ModuleSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
module_bay = NestedModuleBaySerializer()
|
||||
module_type = NestedModuleTypeSerializer()
|
||||
status = ChoiceField(choices=ModuleStatusChoices, required=False)
|
||||
|
||||
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', 'status', 'serial', 'asset_tag',
|
||||
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
@ -820,6 +844,12 @@ class PowerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
|
||||
class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
vdcs = SerializedPKRelatedField(
|
||||
queryset=VirtualDeviceContext.objects.all(),
|
||||
serializer=NestedVirtualDeviceContextSerializer,
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
module = ComponentNestedModuleSerializer(
|
||||
required=False,
|
||||
allow_null=True
|
||||
@ -856,13 +886,13 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', 'lag',
|
||||
'mtu', 'mac_address', 'speed', 'duplex', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel',
|
||||
'poe_mode', 'poe_type', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan',
|
||||
'tagged_vlans', 'mark_connected', 'cable', 'cable_end', 'wireless_link', 'link_peers', 'link_peers_type',
|
||||
'wireless_lans', 'vrf', 'l2vpn_termination', 'connected_endpoints', 'connected_endpoints_type',
|
||||
'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses',
|
||||
'count_fhrp_groups', '_occupied',
|
||||
'id', 'url', 'display', 'device', 'vdcs', 'module', 'name', 'label', 'type', 'enabled', 'parent', 'bridge',
|
||||
'lag', 'mtu', 'mac_address', 'speed', 'duplex', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role',
|
||||
'rf_channel', 'poe_mode', 'poe_type', 'rf_channel_frequency', 'rf_channel_width', 'tx_power',
|
||||
'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'cable_end', 'wireless_link', 'link_peers',
|
||||
'link_peers_type', 'wireless_lans', 'vrf', 'l2vpn_termination', 'connected_endpoints',
|
||||
'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
|
||||
'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
@ -1016,7 +1046,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 +1112,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 +1134,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 +1168,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',
|
||||
]
|
||||
|
@ -37,6 +37,7 @@ router.register('inventory-item-templates', views.InventoryItemTemplateViewSet)
|
||||
router.register('device-roles', views.DeviceRoleViewSet)
|
||||
router.register('platforms', views.PlatformViewSet)
|
||||
router.register('devices', views.DeviceViewSet)
|
||||
router.register('virtual-device-contexts', views.VirtualDeviceContextViewSet)
|
||||
router.register('modules', views.ModuleViewSet)
|
||||
|
||||
# Device components
|
||||
|
@ -538,6 +538,16 @@ class DeviceViewSet(ConfigContextQuerySetMixin, NetBoxModelViewSet):
|
||||
return Response(response)
|
||||
|
||||
|
||||
class VirtualDeviceContextViewSet(NetBoxModelViewSet):
|
||||
queryset = VirtualDeviceContext.objects.prefetch_related(
|
||||
'device__device_type', 'device', 'tenant', 'tags',
|
||||
).annotate(
|
||||
interface_count=count_related(Interface, 'vdcs'),
|
||||
)
|
||||
serializer_class = serializers.VirtualDeviceContextSerializer
|
||||
filterset_class = filtersets.VirtualDeviceContextFilterSet
|
||||
|
||||
|
||||
class ModuleViewSet(NetBoxModelViewSet):
|
||||
queryset = Module.objects.prefetch_related(
|
||||
'device', 'module_bay', 'module_type__manufacturer', 'tags',
|
||||
|
@ -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
|
||||
|
@ -198,6 +198,30 @@ class DeviceAirflowChoices(ChoiceSet):
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Modules
|
||||
#
|
||||
|
||||
class ModuleStatusChoices(ChoiceSet):
|
||||
key = 'Module.status'
|
||||
|
||||
STATUS_OFFLINE = 'offline'
|
||||
STATUS_ACTIVE = 'active'
|
||||
STATUS_PLANNED = 'planned'
|
||||
STATUS_STAGED = 'staged'
|
||||
STATUS_FAILED = 'failed'
|
||||
STATUS_DECOMMISSIONING = 'decommissioning'
|
||||
|
||||
CHOICES = [
|
||||
(STATUS_OFFLINE, 'Offline', 'gray'),
|
||||
(STATUS_ACTIVE, 'Active', 'green'),
|
||||
(STATUS_PLANNED, 'Planned', 'cyan'),
|
||||
(STATUS_STAGED, 'Staged', 'blue'),
|
||||
(STATUS_FAILED, 'Failed', 'red'),
|
||||
(STATUS_DECOMMISSIONING, 'Decommissioning', 'yellow'),
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# ConsolePorts
|
||||
#
|
||||
@ -1343,6 +1367,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
|
||||
#
|
||||
@ -1410,3 +1452,20 @@ class PowerFeedPhaseChoices(ChoiceSet):
|
||||
(PHASE_SINGLE, 'Single phase'),
|
||||
(PHASE_3PHASE, 'Three-phase'),
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# VDC
|
||||
#
|
||||
class VirtualDeviceContextStatusChoices(ChoiceSet):
|
||||
key = 'VirtualDeviceContext.status'
|
||||
|
||||
STATUS_ACTIVE = 'active'
|
||||
STATUS_PLANNED = 'planned'
|
||||
STATUS_OFFLINE = 'offline'
|
||||
|
||||
CHOICES = [
|
||||
(STATUS_ACTIVE, 'Active', 'green'),
|
||||
(STATUS_PLANNED, 'Planned', 'cyan'),
|
||||
(STATUS_OFFLINE, 'Offline', 'red'),
|
||||
]
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,4 @@
|
||||
from .models import *
|
||||
from .model_forms import *
|
||||
from .filtersets import *
|
||||
from .object_create import *
|
||||
from .object_import import *
|
||||
|
@ -1,6 +1,7 @@
|
||||
from django import forms
|
||||
|
||||
from dcim.models import *
|
||||
from django.utils.translation import gettext as _
|
||||
from extras.forms import CustomFieldsMixin
|
||||
from extras.models import Tag
|
||||
from utilities.forms import BootstrapMixin, DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model
|
||||
@ -105,9 +106,9 @@ class ModuleBayBulkCreateForm(DeviceBulkAddComponentForm):
|
||||
field_order = ('name', 'label', 'position_pattern', 'description', 'tags')
|
||||
replication_fields = ('name', 'label', 'position')
|
||||
position_pattern = ExpandableNameField(
|
||||
label='Position',
|
||||
label=_('Position'),
|
||||
required=False,
|
||||
help_text='Alphanumeric ranges are supported. (Must match the number of names being created.)'
|
||||
help_text=_('Alphanumeric ranges are supported. (Must match the number of names being created.)')
|
||||
)
|
||||
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils.translation import gettext as _
|
||||
from timezone_field import TimeZoneFormField
|
||||
|
||||
from dcim.choices import *
|
||||
@ -54,6 +54,7 @@ __all__ = (
|
||||
'SiteBulkEditForm',
|
||||
'SiteGroupBulkEditForm',
|
||||
'VirtualChassisBulkEditForm',
|
||||
'VirtualDeviceContextBulkEditForm'
|
||||
)
|
||||
|
||||
|
||||
@ -125,24 +126,28 @@ class SiteBulkEditForm(NetBoxModelBulkEditForm):
|
||||
)
|
||||
contact_email = forms.EmailField(
|
||||
required=False,
|
||||
label='Contact E-mail'
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=100,
|
||||
required=False
|
||||
label=_('Contact E-mail')
|
||||
)
|
||||
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',
|
||||
)
|
||||
|
||||
|
||||
@ -243,7 +248,7 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
|
||||
serial = forms.CharField(
|
||||
max_length=50,
|
||||
required=False,
|
||||
label='Serial Number'
|
||||
label=_('Serial Number')
|
||||
)
|
||||
asset_tag = forms.CharField(
|
||||
max_length=50,
|
||||
@ -261,12 +266,12 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
|
||||
)
|
||||
u_height = forms.IntegerField(
|
||||
required=False,
|
||||
label='Height (U)'
|
||||
label=_('Height (U)')
|
||||
)
|
||||
desc_units = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect,
|
||||
label='Descending units'
|
||||
label=_('Descending units')
|
||||
)
|
||||
outer_width = forms.IntegerField(
|
||||
required=False,
|
||||
@ -281,6 +286,28 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
mounting_depth = forms.IntegerField(
|
||||
required=False,
|
||||
min_value=1
|
||||
)
|
||||
weight = forms.DecimalField(
|
||||
min_value=0,
|
||||
required=False
|
||||
)
|
||||
max_weight = forms.IntegerField(
|
||||
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 +315,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', 'max_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',
|
||||
'max_weight', 'weight_unit', 'description', 'comments',
|
||||
)
|
||||
|
||||
|
||||
@ -310,14 +341,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):
|
||||
@ -348,19 +384,38 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm):
|
||||
is_full_depth = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect(),
|
||||
label='Is full depth'
|
||||
label=_('Is full depth')
|
||||
)
|
||||
airflow = forms.ChoiceField(
|
||||
choices=add_blank_choice(DeviceAirflowChoices),
|
||||
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 +426,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):
|
||||
@ -386,7 +460,7 @@ class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm):
|
||||
vm_role = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect,
|
||||
label='VM role'
|
||||
label=_('VM role')
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=200,
|
||||
@ -470,17 +544,25 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm):
|
||||
serial = forms.CharField(
|
||||
max_length=50,
|
||||
required=False,
|
||||
label='Serial Number'
|
||||
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',
|
||||
)
|
||||
|
||||
|
||||
@ -496,17 +578,31 @@ class ModuleBulkEditForm(NetBoxModelBulkEditForm):
|
||||
'manufacturer_id': '$manufacturer'
|
||||
}
|
||||
)
|
||||
status = forms.ChoiceField(
|
||||
choices=add_blank_choice(ModuleStatusChoices),
|
||||
required=False,
|
||||
initial='',
|
||||
widget=StaticSelect()
|
||||
)
|
||||
serial = forms.CharField(
|
||||
max_length=50,
|
||||
required=False,
|
||||
label='Serial Number'
|
||||
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', 'status', 'serial', 'description')),
|
||||
)
|
||||
nullable_fields = ('serial',)
|
||||
nullable_fields = ('serial', 'description', 'comments')
|
||||
|
||||
|
||||
class CableBulkEditForm(NetBoxModelBulkEditForm):
|
||||
@ -543,39 +639,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 +709,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,17 +771,21 @@ class PowerFeedBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label='Comments'
|
||||
label=_('Comments')
|
||||
)
|
||||
|
||||
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')
|
||||
|
||||
|
||||
#
|
||||
@ -735,12 +848,12 @@ class PowerPortTemplateBulkEditForm(BulkEditForm):
|
||||
maximum_draw = forms.IntegerField(
|
||||
min_value=1,
|
||||
required=False,
|
||||
help_text="Maximum power draw (watts)"
|
||||
help_text=_("Maximum power draw (watts)")
|
||||
)
|
||||
allocated_draw = forms.IntegerField(
|
||||
min_value=1,
|
||||
required=False,
|
||||
help_text="Allocated power draw (watts)"
|
||||
help_text=_("Allocated power draw (watts)")
|
||||
)
|
||||
description = forms.CharField(
|
||||
required=False
|
||||
@ -813,7 +926,7 @@ class InterfaceTemplateBulkEditForm(BulkEditForm):
|
||||
mgmt_only = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect,
|
||||
label='Management only'
|
||||
label=_('Management only')
|
||||
)
|
||||
description = forms.CharField(
|
||||
required=False
|
||||
@ -823,14 +936,14 @@ class InterfaceTemplateBulkEditForm(BulkEditForm):
|
||||
required=False,
|
||||
initial='',
|
||||
widget=StaticSelect(),
|
||||
label='PoE mode'
|
||||
label=_('PoE mode')
|
||||
)
|
||||
poe_type = forms.ChoiceField(
|
||||
choices=add_blank_choice(InterfacePoETypeChoices),
|
||||
required=False,
|
||||
initial='',
|
||||
widget=StaticSelect(),
|
||||
label='PoE type'
|
||||
label=_('PoE type')
|
||||
)
|
||||
|
||||
nullable_fields = ('label', 'description', 'poe_mode', 'poe_type')
|
||||
@ -1071,31 +1184,31 @@ class InterfaceBulkEditForm(
|
||||
query_params={
|
||||
'type': 'lag',
|
||||
},
|
||||
label='LAG'
|
||||
label=_('LAG')
|
||||
)
|
||||
speed = forms.IntegerField(
|
||||
required=False,
|
||||
widget=SelectSpeedWidget(),
|
||||
label='Speed'
|
||||
label=_('Speed')
|
||||
)
|
||||
mgmt_only = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect,
|
||||
label='Management only'
|
||||
label=_('Management only')
|
||||
)
|
||||
poe_mode = forms.ChoiceField(
|
||||
choices=add_blank_choice(InterfacePoEModeChoices),
|
||||
required=False,
|
||||
initial='',
|
||||
widget=StaticSelect(),
|
||||
label='PoE mode'
|
||||
label=_('PoE mode')
|
||||
)
|
||||
poe_type = forms.ChoiceField(
|
||||
choices=add_blank_choice(InterfacePoETypeChoices),
|
||||
required=False,
|
||||
initial='',
|
||||
widget=StaticSelect(),
|
||||
label='PoE type'
|
||||
label=_('PoE type')
|
||||
)
|
||||
mark_connected = forms.NullBooleanField(
|
||||
required=False,
|
||||
@ -1110,7 +1223,7 @@ class InterfaceBulkEditForm(
|
||||
vlan_group = DynamicModelChoiceField(
|
||||
queryset=VLANGroup.objects.all(),
|
||||
required=False,
|
||||
label='VLAN group'
|
||||
label=_('VLAN group')
|
||||
)
|
||||
untagged_vlan = DynamicModelChoiceField(
|
||||
queryset=VLAN.objects.all(),
|
||||
@ -1118,7 +1231,7 @@ class InterfaceBulkEditForm(
|
||||
query_params={
|
||||
'group_id': '$vlan_group',
|
||||
},
|
||||
label='Untagged VLAN'
|
||||
label=_('Untagged VLAN')
|
||||
)
|
||||
tagged_vlans = DynamicModelMultipleChoiceField(
|
||||
queryset=VLAN.objects.all(),
|
||||
@ -1126,12 +1239,12 @@ class InterfaceBulkEditForm(
|
||||
query_params={
|
||||
'group_id': '$vlan_group',
|
||||
},
|
||||
label='Tagged VLANs'
|
||||
label=_('Tagged VLANs')
|
||||
)
|
||||
vrf = DynamicModelChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
label='VRF'
|
||||
label=_('VRF')
|
||||
)
|
||||
|
||||
model = Interface
|
||||
@ -1296,3 +1409,24 @@ class InventoryItemRoleBulkEditForm(NetBoxModelBulkEditForm):
|
||||
(None, ('color', 'description')),
|
||||
)
|
||||
nullable_fields = ('color', 'description')
|
||||
|
||||
|
||||
class VirtualDeviceContextBulkEditForm(NetBoxModelBulkEditForm):
|
||||
device = DynamicModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False
|
||||
)
|
||||
status = forms.ChoiceField(
|
||||
required=False,
|
||||
choices=add_blank_choice(VirtualDeviceContextStatusChoices),
|
||||
widget=StaticSelect()
|
||||
)
|
||||
tenant = DynamicModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False
|
||||
)
|
||||
model = VirtualDeviceContext
|
||||
fieldsets = (
|
||||
(None, ('device', 'status', 'tenant')),
|
||||
)
|
||||
nullable_fields = ('device', 'tenant', )
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,5 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
@ -13,13 +14,13 @@ class InterfaceCommonForm(forms.Form):
|
||||
mac_address = forms.CharField(
|
||||
empty_value=None,
|
||||
required=False,
|
||||
label='MAC address'
|
||||
label=_('MAC address')
|
||||
)
|
||||
mtu = forms.IntegerField(
|
||||
required=False,
|
||||
min_value=INTERFACE_MTU_MIN,
|
||||
max_value=INTERFACE_MTU_MAX,
|
||||
label='MTU'
|
||||
label=_('MTU')
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
@ -51,14 +52,15 @@ class InterfaceCommonForm(forms.Form):
|
||||
|
||||
|
||||
class ModuleCommonForm(forms.Form):
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
replicate_components = self.cleaned_data.get("replicate_components")
|
||||
adopt_components = self.cleaned_data.get("adopt_components")
|
||||
device = self.cleaned_data['device']
|
||||
module_type = self.cleaned_data['module_type']
|
||||
module_bay = self.cleaned_data['module_bay']
|
||||
device = self.cleaned_data.get('device')
|
||||
module_type = self.cleaned_data.get('module_type')
|
||||
module_bay = self.cleaned_data.get('module_bay')
|
||||
|
||||
if adopt_components:
|
||||
self.instance._adopt_components = True
|
||||
|
@ -1,9 +1,10 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
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):
|
||||
@ -16,7 +17,7 @@ def get_cable_form(a_type, b_type):
|
||||
|
||||
attrs[f'termination_{cable_end}_region'] = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
label='Region',
|
||||
label=_('Region'),
|
||||
required=False,
|
||||
initial_params={
|
||||
'sites': f'$termination_{cable_end}_site'
|
||||
@ -24,7 +25,7 @@ def get_cable_form(a_type, b_type):
|
||||
)
|
||||
attrs[f'termination_{cable_end}_sitegroup'] = DynamicModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
label='Site group',
|
||||
label=_('Site group'),
|
||||
required=False,
|
||||
initial_params={
|
||||
'sites': f'$termination_{cable_end}_site'
|
||||
@ -32,7 +33,7 @@ def get_cable_form(a_type, b_type):
|
||||
)
|
||||
attrs[f'termination_{cable_end}_site'] = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site',
|
||||
label=_('Site'),
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': f'$termination_{cable_end}_region',
|
||||
@ -41,7 +42,7 @@ def get_cable_form(a_type, b_type):
|
||||
)
|
||||
attrs[f'termination_{cable_end}_location'] = DynamicModelChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
label='Location',
|
||||
label=_('Location'),
|
||||
required=False,
|
||||
null_option='None',
|
||||
query_params={
|
||||
@ -54,7 +55,7 @@ def get_cable_form(a_type, b_type):
|
||||
|
||||
attrs[f'termination_{cable_end}_rack'] = DynamicModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
label='Rack',
|
||||
label=_('Rack'),
|
||||
required=False,
|
||||
null_option='None',
|
||||
initial_params={
|
||||
@ -67,7 +68,7 @@ def get_cable_form(a_type, b_type):
|
||||
)
|
||||
attrs[f'termination_{cable_end}_device'] = DynamicModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
label='Device',
|
||||
label=_('Device'),
|
||||
required=False,
|
||||
initial_params={
|
||||
f'{term_cls._meta.model_name}s__in': f'${cable_end}_terminations'
|
||||
@ -93,7 +94,7 @@ def get_cable_form(a_type, b_type):
|
||||
|
||||
attrs[f'termination_{cable_end}_powerpanel'] = DynamicModelChoiceField(
|
||||
queryset=PowerPanel.objects.all(),
|
||||
label='Power Panel',
|
||||
label=_('Power Panel'),
|
||||
required=False,
|
||||
initial_params={
|
||||
'powerfeeds__in': f'${cable_end}_terminations'
|
||||
@ -105,7 +106,7 @@ def get_cable_form(a_type, b_type):
|
||||
)
|
||||
attrs[f'{cable_end}_terminations'] = DynamicModelMultipleChoiceField(
|
||||
queryset=term_cls.objects.all(),
|
||||
label='Power Feed',
|
||||
label=_('Power Feed'),
|
||||
disabled_indicator='_occupied',
|
||||
query_params={
|
||||
'power_panel_id': f'$termination_{cable_end}_powerpanel',
|
||||
@ -117,7 +118,7 @@ def get_cable_form(a_type, b_type):
|
||||
|
||||
attrs[f'termination_{cable_end}_provider'] = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
label='Provider',
|
||||
label=_('Provider'),
|
||||
initial_params={
|
||||
'circuits': f'$termination_{cable_end}_circuit'
|
||||
},
|
||||
@ -125,7 +126,7 @@ def get_cable_form(a_type, b_type):
|
||||
)
|
||||
attrs[f'termination_{cable_end}_circuit'] = DynamicModelChoiceField(
|
||||
queryset=Circuit.objects.all(),
|
||||
label='Circuit',
|
||||
label=_('Circuit'),
|
||||
initial_params={
|
||||
'terminations__in': f'${cable_end}_terminations'
|
||||
},
|
||||
@ -136,7 +137,7 @@ def get_cable_form(a_type, b_type):
|
||||
)
|
||||
attrs[f'{cable_end}_terminations'] = DynamicModelMultipleChoiceField(
|
||||
queryset=term_cls.objects.all(),
|
||||
label='Side',
|
||||
label=_('Side'),
|
||||
disabled_indicator='_occupied',
|
||||
query_params={
|
||||
'circuit_id': f'$termination_{cable_end}_circuit',
|
||||
|
@ -50,6 +50,7 @@ __all__ = (
|
||||
'SiteFilterForm',
|
||||
'SiteGroupFilterForm',
|
||||
'VirtualChassisFilterForm',
|
||||
'VirtualDeviceContextFilterForm'
|
||||
)
|
||||
|
||||
|
||||
@ -116,7 +117,7 @@ class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
|
||||
class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
model = Region
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag', 'parent_id')),
|
||||
(None, ('q', 'filter_id', 'tag', 'parent_id')),
|
||||
('Contacts', ('contact', 'contact_role', 'contact_group'))
|
||||
)
|
||||
parent_id = DynamicModelMultipleChoiceField(
|
||||
@ -130,7 +131,7 @@ class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
model = SiteGroup
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag', 'parent_id')),
|
||||
(None, ('q', 'filter_id', 'tag', 'parent_id')),
|
||||
('Contacts', ('contact', 'contact_role', 'contact_group'))
|
||||
)
|
||||
parent_id = DynamicModelMultipleChoiceField(
|
||||
@ -144,7 +145,7 @@ class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
model = Site
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
(None, ('q', 'filter_id', 'tag')),
|
||||
('Attributes', ('status', 'region_id', 'group_id', 'asn_id')),
|
||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||
('Contacts', ('contact', 'contact_role', 'contact_group')),
|
||||
@ -174,7 +175,7 @@ class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
|
||||
class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
model = Location
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
(None, ('q', 'filter_id', '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 +223,13 @@ class RackRoleFilterForm(NetBoxModelFilterSetForm):
|
||||
class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
model = Rack
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
(None, ('q', 'filter_id', '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', 'max_weight', 'weight_unit')),
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
@ -281,6 +283,18 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
|
||||
required=False
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
weight = forms.DecimalField(
|
||||
required=False,
|
||||
min_value=1
|
||||
)
|
||||
max_weight = forms.IntegerField(
|
||||
required=False,
|
||||
min_value=1
|
||||
)
|
||||
weight_unit = forms.ChoiceField(
|
||||
choices=add_blank_choice(WeightUnitChoices),
|
||||
required=False
|
||||
)
|
||||
|
||||
|
||||
class RackElevationFilterForm(RackFilterForm):
|
||||
@ -298,7 +312,7 @@ class RackElevationFilterForm(RackFilterForm):
|
||||
class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
model = RackReservation
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
(None, ('q', 'filter_id', 'tag')),
|
||||
('User', ('user_id',)),
|
||||
('Rack', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
|
||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||
@ -354,7 +368,7 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
class ManufacturerFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
model = Manufacturer
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
(None, ('q', 'filter_id', 'tag')),
|
||||
('Contacts', ('contact', 'contact_role', 'contact_group'))
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
@ -363,13 +377,14 @@ class ManufacturerFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
|
||||
model = DeviceType
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
(None, ('q', 'filter_id', '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 +480,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_id', '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 +552,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 +584,7 @@ class DeviceFilterForm(
|
||||
):
|
||||
model = Device
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
(None, ('q', 'filter_id', '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')),
|
||||
@ -704,11 +734,41 @@ class DeviceFilterForm(
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class VirtualDeviceContextFilterForm(
|
||||
TenancyFilterForm,
|
||||
NetBoxModelFilterSetForm
|
||||
):
|
||||
model = VirtualDeviceContext
|
||||
fieldsets = (
|
||||
(None, ('q', 'filter_id', 'tag')),
|
||||
('Attributes', ('device', 'status', 'has_primary_ip')),
|
||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||
)
|
||||
device = DynamicModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
label=_('Device'),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
status = MultipleChoiceField(
|
||||
required=False,
|
||||
choices=add_blank_choice(VirtualDeviceContextStatusChoices)
|
||||
)
|
||||
has_primary_ip = forms.NullBooleanField(
|
||||
required=False,
|
||||
label='Has a primary IP',
|
||||
widget=StaticSelect(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
model = Module
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Hardware', ('manufacturer_id', 'module_type_id', 'serial', 'asset_tag')),
|
||||
(None, ('q', 'filter_id', 'tag')),
|
||||
('Hardware', ('manufacturer_id', 'module_type_id', 'status', 'serial', 'asset_tag')),
|
||||
)
|
||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
@ -725,6 +785,10 @@ class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxMo
|
||||
label=_('Type'),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
status = MultipleChoiceField(
|
||||
choices=ModuleStatusChoices,
|
||||
required=False
|
||||
)
|
||||
serial = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
@ -737,7 +801,7 @@ class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxMo
|
||||
class VirtualChassisFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
model = VirtualChassis
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
(None, ('q', 'filter_id', 'tag')),
|
||||
('Location', ('region_id', 'site_group_id', 'site_id')),
|
||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||
)
|
||||
@ -766,7 +830,7 @@ class VirtualChassisFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
model = Cable
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
(None, ('q', 'filter_id', '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 +902,7 @@ class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
model = PowerPanel
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
(None, ('q', 'filter_id', 'tag')),
|
||||
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')),
|
||||
('Contacts', ('contact', 'contact_role', 'contact_group')),
|
||||
)
|
||||
@ -876,7 +940,7 @@ class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
class PowerFeedFilterForm(NetBoxModelFilterSetForm):
|
||||
model = PowerFeed
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
(None, ('q', 'filter_id', '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 +1042,7 @@ class PathEndpointFilterForm(CabledFilterForm):
|
||||
class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
model = ConsolePort
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
(None, ('q', 'filter_id', '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 +1061,7 @@ class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
model = ConsoleServerPort
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
(None, ('q', 'filter_id', '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 +1080,7 @@ class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterF
|
||||
class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
model = PowerPort
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
(None, ('q', 'filter_id', '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 +1095,7 @@ class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
model = PowerOutlet
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
(None, ('q', 'filter_id', '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,14 +1110,23 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
model = Interface
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
(None, ('q', 'filter_id', 'tag')),
|
||||
('Attributes', ('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only')),
|
||||
('Addressing', ('vrf_id', 'mac_address', 'wwn')),
|
||||
('PoE', ('poe_mode', 'poe_type')),
|
||||
('Wireless', ('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id',
|
||||
'device_id', 'vdc_id')),
|
||||
('Connection', ('cabled', 'connected', 'occupied')),
|
||||
)
|
||||
vdc_id = DynamicModelMultipleChoiceField(
|
||||
queryset=VirtualDeviceContext.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'device_id': '$device_id',
|
||||
},
|
||||
label=_('Virtual Device Context')
|
||||
)
|
||||
kind = MultipleChoiceField(
|
||||
choices=InterfaceKindChoices,
|
||||
required=False
|
||||
@ -1135,7 +1208,7 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
|
||||
class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
(None, ('q', 'filter_id', '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 +1227,7 @@ class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
||||
class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
||||
model = RearPort
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
(None, ('q', 'filter_id', '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 +1245,7 @@ class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
||||
class ModuleBayFilterForm(DeviceComponentFilterForm):
|
||||
model = ModuleBay
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
(None, ('q', 'filter_id', 'tag')),
|
||||
('Attributes', ('name', 'label', 'position')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
||||
)
|
||||
@ -1185,7 +1258,7 @@ class ModuleBayFilterForm(DeviceComponentFilterForm):
|
||||
class DeviceBayFilterForm(DeviceComponentFilterForm):
|
||||
model = DeviceBay
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
(None, ('q', 'filter_id', 'tag')),
|
||||
('Attributes', ('name', 'label')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
||||
)
|
||||
@ -1195,7 +1268,7 @@ class DeviceBayFilterForm(DeviceComponentFilterForm):
|
||||
class InventoryItemFilterForm(DeviceComponentFilterForm):
|
||||
model = InventoryItem
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
(None, ('q', 'filter_id', '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')),
|
||||
)
|
||||
|
@ -1,7 +1,7 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import gettext as _
|
||||
from timezone_field import TimeZoneFormField
|
||||
|
||||
from dcim.choices import *
|
||||
@ -62,6 +62,7 @@ __all__ = (
|
||||
'SiteGroupForm',
|
||||
'VCMemberSelectForm',
|
||||
'VirtualChassisForm',
|
||||
'VirtualDeviceContextForm'
|
||||
)
|
||||
|
||||
INTERFACE_MODE_HELP_TEXT = """
|
||||
@ -162,14 +163,14 @@ class SiteForm(TenancyForm, NetBoxModelForm):
|
||||
'time_zone': StaticSelect(),
|
||||
}
|
||||
help_texts = {
|
||||
'name': "Full name of the site",
|
||||
'facility': "Data center provider and facility (e.g. Equinix NY7)",
|
||||
'time_zone': "Local time zone",
|
||||
'description': "Short description (will appear in sites list)",
|
||||
'physical_address': "Physical location of the building (e.g. for GPS)",
|
||||
'shipping_address': "If different from the physical address",
|
||||
'latitude': "Latitude in decimal format (xx.yyyyyy)",
|
||||
'longitude': "Longitude in decimal format (xx.yyyyyy)"
|
||||
'name': _("Full name of the site"),
|
||||
'facility': _("Data center provider and facility (e.g. Equinix NY7)"),
|
||||
'time_zone': _("Local time zone"),
|
||||
'description': _("Short description (will appear in sites list)"),
|
||||
'physical_address': _("Physical location of the building (e.g. for GPS)"),
|
||||
'shipping_address': _("If different from the physical address"),
|
||||
'latitude': _("Latitude in decimal format (xx.yyyyyy)"),
|
||||
'longitude': _("Longitude in decimal format (xx.yyyyyy)")
|
||||
}
|
||||
|
||||
|
||||
@ -278,19 +279,20 @@ 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', 'max_weight', 'weight_unit', 'description', 'comments', 'tags',
|
||||
]
|
||||
help_texts = {
|
||||
'site': "The site at which the rack exists",
|
||||
'name': "Organizational rack name",
|
||||
'facility_id': "The unique rack ID assigned by the facility",
|
||||
'u_height': "Height in rack units",
|
||||
'site': _("The site at which the rack exists"),
|
||||
'name': _("Organizational rack name"),
|
||||
'facility_id': _("The unique rack ID assigned by the facility"),
|
||||
'u_height': _("Height in rack units"),
|
||||
}
|
||||
widgets = {
|
||||
'status': StaticSelect(),
|
||||
'type': StaticSelect(),
|
||||
'width': StaticSelect(),
|
||||
'outer_unit': StaticSelect(),
|
||||
'weight_unit': StaticSelect(),
|
||||
}
|
||||
|
||||
|
||||
@ -333,7 +335,7 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
|
||||
)
|
||||
units = NumericArrayField(
|
||||
base_field=forms.IntegerField(),
|
||||
help_text="Comma-separated list of numeric unit IDs. A range may be specified using a hyphen."
|
||||
help_text=_("Comma-separated list of numeric unit IDs. A range may be specified using a hyphen.")
|
||||
)
|
||||
user = forms.ModelChoiceField(
|
||||
queryset=User.objects.order_by(
|
||||
@ -341,6 +343,7 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
|
||||
),
|
||||
widget=StaticSelect()
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
fieldsets = (
|
||||
('Reservation', ('region', 'site_group', 'site', 'location', 'rack', 'units', 'user', 'description', 'tags')),
|
||||
@ -351,7 +354,7 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
|
||||
model = RackReservation
|
||||
fields = [
|
||||
'region', 'site_group', 'site', 'location', 'rack', 'units', 'user', 'tenant_group', 'tenant',
|
||||
'description', 'tags',
|
||||
'description', 'comments', 'tags',
|
||||
]
|
||||
|
||||
|
||||
@ -381,11 +384,9 @@ class DeviceTypeForm(NetBoxModelForm):
|
||||
comments = CommentField()
|
||||
|
||||
fieldsets = (
|
||||
('Device Type', (
|
||||
'manufacturer', 'model', 'slug', 'part_number', 'tags',
|
||||
)),
|
||||
('Device Type', ('manufacturer', 'model', 'slug', 'description', 'tags')),
|
||||
('Chassis', (
|
||||
'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
|
||||
'u_height', 'is_full_depth', 'part_number', 'subdevice_role', 'airflow', 'weight', 'weight_unit',
|
||||
)),
|
||||
('Images', ('front_image', 'rear_image')),
|
||||
)
|
||||
@ -394,7 +395,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(),
|
||||
@ -404,7 +405,8 @@ class DeviceTypeForm(NetBoxModelForm):
|
||||
}),
|
||||
'rear_image': ClearableFileInput(attrs={
|
||||
'accept': DEVICETYPE_IMAGE_FORMATS
|
||||
})
|
||||
}),
|
||||
'weight_unit': StaticSelect(),
|
||||
}
|
||||
|
||||
|
||||
@ -415,17 +417,20 @@ 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()
|
||||
@ -511,7 +516,7 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
|
||||
)
|
||||
position = forms.DecimalField(
|
||||
required=False,
|
||||
help_text="The lowest-numbered unit occupied by the device",
|
||||
help_text=_("The lowest-numbered unit occupied by the device"),
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/racks/{{rack}}/elevation/',
|
||||
attrs={
|
||||
@ -569,13 +574,13 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
|
||||
)
|
||||
vc_position = forms.IntegerField(
|
||||
required=False,
|
||||
label='Position',
|
||||
help_text="The position in the virtual chassis this device is identified by"
|
||||
label=_('Position'),
|
||||
help_text=_("The position in the virtual chassis this device is identified by")
|
||||
)
|
||||
vc_priority = forms.IntegerField(
|
||||
required=False,
|
||||
label='Priority',
|
||||
help_text="The priority of the device in the virtual chassis"
|
||||
label=_('Priority'),
|
||||
help_text=_("The priority of the device in the virtual chassis")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@ -584,13 +589,13 @@ 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",
|
||||
'serial': "Chassis serial number",
|
||||
'local_context_data': "Local config context data overwrites all source contexts in the final rendered "
|
||||
"config context",
|
||||
'device_role': _("The function this device serves"),
|
||||
'serial': _("Chassis serial number"),
|
||||
'local_context_data': _("Local config context data overwrites all source contexts in the final rendered "
|
||||
"config context"),
|
||||
}
|
||||
widgets = {
|
||||
'face': StaticSelect(),
|
||||
@ -687,18 +692,18 @@ class ModuleForm(ModuleCommonForm, NetBoxModelForm):
|
||||
replicate_components = forms.BooleanField(
|
||||
required=False,
|
||||
initial=True,
|
||||
help_text="Automatically populate components associated with this module type"
|
||||
help_text=_("Automatically populate components associated with this module type")
|
||||
)
|
||||
|
||||
adopt_components = forms.BooleanField(
|
||||
required=False,
|
||||
initial=False,
|
||||
help_text="Adopt already existing components"
|
||||
help_text=_("Adopt already existing components")
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
('Module', (
|
||||
'device', 'module_bay', 'manufacturer', 'module_type', 'tags',
|
||||
'device', 'module_bay', 'manufacturer', 'module_type', 'status', 'description', 'tags',
|
||||
)),
|
||||
('Hardware', (
|
||||
'serial', 'asset_tag', 'replicate_components', 'adopt_components',
|
||||
@ -708,8 +713,8 @@ class ModuleForm(ModuleCommonForm, NetBoxModelForm):
|
||||
class Meta:
|
||||
model = Module
|
||||
fields = [
|
||||
'device', 'module_bay', 'manufacturer', 'module_type', 'serial', 'asset_tag', 'tags',
|
||||
'replicate_components', 'adopt_components', 'comments',
|
||||
'device', 'module_bay', 'manufacturer', 'module_type', 'status', 'serial', 'asset_tag', 'tags',
|
||||
'replicate_components', 'adopt_components', 'description', 'comments',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -724,11 +729,13 @@ class ModuleForm(ModuleCommonForm, 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,
|
||||
@ -771,15 +778,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',
|
||||
]
|
||||
|
||||
|
||||
@ -837,15 +845,16 @@ class PowerFeedForm(NetBoxModelForm):
|
||||
|
||||
fieldsets = (
|
||||
('Power Panel', ('region', 'site', 'power_panel')),
|
||||
('Power Feed', ('location', 'rack', 'name', 'status', 'type', 'mark_connected', 'tags')),
|
||||
('Power Feed', ('location', 'rack', 'name', 'status', 'type', 'description', 'mark_connected', 'tags')),
|
||||
('Characteristics', ('supply', 'voltage', 'amperage', 'phase', 'max_utilization')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerFeed
|
||||
fields = [
|
||||
'region', 'site_group', 'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply',
|
||||
'phase', 'voltage', 'amperage', 'max_utilization', 'comments', 'tags',
|
||||
'region', 'site_group', 'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type',
|
||||
'mark_connected', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', 'description', 'comments',
|
||||
'tags',
|
||||
]
|
||||
widgets = {
|
||||
'status': StaticSelect(),
|
||||
@ -864,11 +873,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(),
|
||||
@ -1316,10 +1326,18 @@ class PowerOutletForm(ModularDeviceComponentForm):
|
||||
|
||||
|
||||
class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
||||
vdcs = DynamicModelMultipleChoiceField(
|
||||
queryset=VirtualDeviceContext.objects.all(),
|
||||
required=False,
|
||||
label='Virtual Device Contexts',
|
||||
query_params={
|
||||
'device_id': '$device',
|
||||
}
|
||||
)
|
||||
parent = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
label='Parent interface',
|
||||
label=_('Parent interface'),
|
||||
query_params={
|
||||
'device_id': '$device',
|
||||
}
|
||||
@ -1327,7 +1345,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
||||
bridge = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
label='Bridged interface',
|
||||
label=_('Bridged interface'),
|
||||
query_params={
|
||||
'device_id': '$device',
|
||||
}
|
||||
@ -1335,7 +1353,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
||||
lag = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
label='LAG interface',
|
||||
label=_('LAG interface'),
|
||||
query_params={
|
||||
'device_id': '$device',
|
||||
'type': 'lag',
|
||||
@ -1344,12 +1362,12 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
||||
wireless_lan_group = DynamicModelChoiceField(
|
||||
queryset=WirelessLANGroup.objects.all(),
|
||||
required=False,
|
||||
label='Wireless LAN group'
|
||||
label=_('Wireless LAN group')
|
||||
)
|
||||
wireless_lans = DynamicModelMultipleChoiceField(
|
||||
queryset=WirelessLAN.objects.all(),
|
||||
required=False,
|
||||
label='Wireless LANs',
|
||||
label=_('Wireless LANs'),
|
||||
query_params={
|
||||
'group_id': '$wireless_lan_group',
|
||||
}
|
||||
@ -1357,12 +1375,12 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
||||
vlan_group = DynamicModelChoiceField(
|
||||
queryset=VLANGroup.objects.all(),
|
||||
required=False,
|
||||
label='VLAN group'
|
||||
label=_('VLAN group')
|
||||
)
|
||||
untagged_vlan = DynamicModelChoiceField(
|
||||
queryset=VLAN.objects.all(),
|
||||
required=False,
|
||||
label='Untagged VLAN',
|
||||
label=_('Untagged VLAN'),
|
||||
query_params={
|
||||
'group_id': '$vlan_group',
|
||||
'available_on_device': '$device',
|
||||
@ -1371,7 +1389,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
||||
tagged_vlans = DynamicModelMultipleChoiceField(
|
||||
queryset=VLAN.objects.all(),
|
||||
required=False,
|
||||
label='Tagged VLANs',
|
||||
label=_('Tagged VLANs'),
|
||||
query_params={
|
||||
'group_id': '$vlan_group',
|
||||
'available_on_device': '$device',
|
||||
@ -1380,19 +1398,18 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
||||
vrf = DynamicModelChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
label='VRF'
|
||||
label=_('VRF')
|
||||
)
|
||||
|
||||
wwn = forms.CharField(
|
||||
empty_value=None,
|
||||
required=False,
|
||||
label='WWN'
|
||||
label=_('WWN')
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
('Interface', ('device', 'module', 'name', 'label', 'type', 'speed', 'duplex', 'description', 'tags')),
|
||||
('Addressing', ('vrf', 'mac_address', 'wwn')),
|
||||
('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
|
||||
('Operation', ('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
|
||||
('Related Interfaces', ('parent', 'bridge', 'lag')),
|
||||
('PoE', ('poe_mode', 'poe_type')),
|
||||
('802.1Q Switching', ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
|
||||
@ -1404,7 +1421,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = [
|
||||
'device', 'module', 'name', 'label', 'type', 'speed', 'duplex', 'enabled', 'parent', 'bridge', 'lag',
|
||||
'device', 'module', 'vdcs', 'name', 'label', 'type', 'speed', 'duplex', 'enabled', 'parent', 'bridge', 'lag',
|
||||
'mac_address', 'wwn', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'poe_mode', 'poe_type', 'mode',
|
||||
'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'wireless_lans',
|
||||
'untagged_vlan', 'tagged_vlans', 'vrf', 'tags',
|
||||
@ -1424,8 +1441,8 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
||||
}
|
||||
help_texts = {
|
||||
'mode': INTERFACE_MODE_HELP_TEXT,
|
||||
'rf_channel_frequency': "Populated by selected channel (if set)",
|
||||
'rf_channel_width': "Populated by selected channel (if set)",
|
||||
'rf_channel_frequency': _("Populated by selected channel (if set)"),
|
||||
'rf_channel_width': _("Populated by selected channel (if set)"),
|
||||
}
|
||||
|
||||
|
||||
@ -1499,8 +1516,8 @@ class DeviceBayForm(DeviceComponentForm):
|
||||
class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
|
||||
installed_device = forms.ModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
label='Child Device',
|
||||
help_text="Child devices must first be created and assigned to the site/rack of the parent device.",
|
||||
label=_('Child Device'),
|
||||
help_text=_("Child devices must first be created and assigned to the site/rack of the parent device."),
|
||||
widget=StaticSelect(),
|
||||
)
|
||||
|
||||
@ -1581,3 +1598,90 @@ class InventoryItemRoleForm(NetBoxModelForm):
|
||||
fields = [
|
||||
'name', 'slug', 'color', 'description', 'tags',
|
||||
]
|
||||
|
||||
|
||||
class VirtualDeviceContextForm(TenancyForm, NetBoxModelForm):
|
||||
region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'sites': '$site'
|
||||
}
|
||||
)
|
||||
site_group = DynamicModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'sites': '$site'
|
||||
}
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$region',
|
||||
'group_id': '$site_group',
|
||||
}
|
||||
)
|
||||
location = DynamicModelChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'site_id': '$site'
|
||||
},
|
||||
initial_params={
|
||||
'racks': '$rack'
|
||||
}
|
||||
)
|
||||
rack = DynamicModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'site_id': '$site',
|
||||
'location_id': '$location',
|
||||
}
|
||||
)
|
||||
device = DynamicModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
query_params={
|
||||
'site_id': '$site',
|
||||
'location_id': '$location',
|
||||
'rack_id': '$rack',
|
||||
}
|
||||
)
|
||||
primary_ip4 = DynamicModelChoiceField(
|
||||
queryset=IPAddress.objects.all(),
|
||||
label='Primary IPv4',
|
||||
required=False,
|
||||
query_params={
|
||||
'device_id': '$device',
|
||||
'family': '4',
|
||||
}
|
||||
)
|
||||
primary_ip6 = DynamicModelChoiceField(
|
||||
queryset=IPAddress.objects.all(),
|
||||
label='Primary IPv6',
|
||||
required=False,
|
||||
query_params={
|
||||
'device_id': '$device',
|
||||
'family': '6',
|
||||
}
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
('Assigned Device', ('region', 'site_group', 'site', 'location', 'rack', 'device')),
|
||||
('Virtual Device Context', ('name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tags')),
|
||||
('Tenancy', ('tenant_group', 'tenant'))
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VirtualDeviceContext
|
||||
fields = [
|
||||
'region', 'site_group', 'site', 'location', 'rack', 'device', 'name', 'status', 'identifier',
|
||||
'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant', 'comments', 'tags'
|
||||
]
|
||||
widgets = {
|
||||
'status': StaticSelect(),
|
||||
'primary_ip4': StaticSelect(),
|
||||
'primary_ip6': StaticSelect(),
|
||||
}
|
@ -1,9 +1,10 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
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',
|
||||
@ -39,7 +40,7 @@ class ComponentCreateForm(forms.Form):
|
||||
name = ExpandableNameField()
|
||||
label = ExpandableNameField(
|
||||
required=False,
|
||||
help_text='Alphanumeric ranges are supported. (Must match the number of objects being created.)'
|
||||
help_text=_('Alphanumeric ranges are supported. (Must match the number of objects being created.)')
|
||||
)
|
||||
|
||||
# Identify the fields which support replication (i.e. ExpandableNameFields). This is referenced by
|
||||
@ -97,8 +98,8 @@ class InterfaceTemplateCreateForm(ComponentCreateForm, model_forms.InterfaceTemp
|
||||
class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemplateForm):
|
||||
rear_port = forms.MultipleChoiceField(
|
||||
choices=[],
|
||||
label='Rear ports',
|
||||
help_text='Select one rear port assignment for each front port being created.',
|
||||
label=_('Rear ports'),
|
||||
help_text=_('Select one rear port assignment for each front port being created.'),
|
||||
)
|
||||
|
||||
# Override fieldsets from FrontPortTemplateForm to omit rear_port_position
|
||||
@ -166,9 +167,9 @@ class DeviceBayTemplateCreateForm(ComponentCreateForm, model_forms.DeviceBayTemp
|
||||
|
||||
class ModuleBayTemplateCreateForm(ComponentCreateForm, model_forms.ModuleBayTemplateForm):
|
||||
position = ExpandableNameField(
|
||||
label='Position',
|
||||
label=_('Position'),
|
||||
required=False,
|
||||
help_text='Alphanumeric ranges are supported. (Must match the number of objects being created.)'
|
||||
help_text=_('Alphanumeric ranges are supported. (Must match the number of objects being created.)')
|
||||
)
|
||||
replication_fields = ('name', 'label', 'position')
|
||||
|
||||
@ -226,8 +227,8 @@ class InterfaceCreateForm(ComponentCreateForm, model_forms.InterfaceForm):
|
||||
class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
|
||||
rear_port = forms.MultipleChoiceField(
|
||||
choices=[],
|
||||
label='Rear ports',
|
||||
help_text='Select one rear port assignment for each front port being created.',
|
||||
label=_('Rear ports'),
|
||||
help_text=_('Select one rear port assignment for each front port being created.'),
|
||||
)
|
||||
|
||||
# Override fieldsets from FrontPortForm to omit rear_port_position
|
||||
@ -290,9 +291,9 @@ class DeviceBayCreateForm(ComponentCreateForm, model_forms.DeviceBayForm):
|
||||
|
||||
class ModuleBayCreateForm(ComponentCreateForm, model_forms.ModuleBayForm):
|
||||
position = ExpandableNameField(
|
||||
label='Position',
|
||||
label=_('Position'),
|
||||
required=False,
|
||||
help_text='Alphanumeric ranges are supported. (Must match the number of objects being created.)'
|
||||
help_text=_('Alphanumeric ranges are supported. (Must match the number of objects being created.)')
|
||||
)
|
||||
replication_fields = ('name', 'label', 'position')
|
||||
|
||||
@ -352,7 +353,7 @@ class VirtualChassisCreateForm(NetBoxModelForm):
|
||||
initial_position = forms.IntegerField(
|
||||
initial=1,
|
||||
required=False,
|
||||
help_text='Position of the first member device. Increases by one for each additional member.'
|
||||
help_text=_('Position of the first member device. Increases by one for each additional member.')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
@ -1,4 +1,5 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from dcim.choices import InterfacePoEModeChoices, InterfacePoETypeChoices, InterfaceTypeChoices, PortTypeChoices
|
||||
from dcim.models import *
|
||||
@ -8,43 +9,16 @@ __all__ = (
|
||||
'ConsolePortTemplateImportForm',
|
||||
'ConsoleServerPortTemplateImportForm',
|
||||
'DeviceBayTemplateImportForm',
|
||||
'DeviceTypeImportForm',
|
||||
'FrontPortTemplateImportForm',
|
||||
'InterfaceTemplateImportForm',
|
||||
'InventoryItemTemplateImportForm',
|
||||
'ModuleBayTemplateImportForm',
|
||||
'ModuleTypeImportForm',
|
||||
'PowerOutletTemplateImportForm',
|
||||
'PowerPortTemplateImportForm',
|
||||
'RearPortTemplateImportForm',
|
||||
)
|
||||
|
||||
|
||||
class DeviceTypeImportForm(BootstrapMixin, forms.ModelForm):
|
||||
manufacturer = forms.ModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
to_field_name='name'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = [
|
||||
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
|
||||
'comments',
|
||||
]
|
||||
|
||||
|
||||
class ModuleTypeImportForm(BootstrapMixin, forms.ModelForm):
|
||||
manufacturer = forms.ModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
to_field_name='name'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ModuleType
|
||||
fields = ['manufacturer', 'model', 'part_number', 'comments']
|
||||
|
||||
|
||||
#
|
||||
# Component template import forms
|
||||
#
|
||||
@ -115,12 +89,12 @@ class InterfaceTemplateImportForm(ComponentTemplateImportForm):
|
||||
poe_mode = forms.ChoiceField(
|
||||
choices=InterfacePoEModeChoices,
|
||||
required=False,
|
||||
label='PoE mode'
|
||||
label=_('PoE mode')
|
||||
)
|
||||
poe_type = forms.ChoiceField(
|
||||
choices=InterfacePoETypeChoices,
|
||||
required=False,
|
||||
label='PoE type'
|
||||
label=_('PoE type')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
169
netbox/dcim/graphql/gfk_mixins.py
Normal file
169
netbox/dcim/graphql/gfk_mixins.py
Normal 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
|
@ -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
|
||||
|
@ -2,6 +2,7 @@ import graphene
|
||||
|
||||
from netbox.graphql.fields import ObjectField, ObjectListField
|
||||
from .types import *
|
||||
from .types import VirtualDeviceContextType
|
||||
|
||||
|
||||
class DCIMQuery(graphene.ObjectType):
|
||||
@ -121,3 +122,6 @@ class DCIMQuery(graphene.ObjectType):
|
||||
|
||||
virtual_chassis = ObjectField(VirtualChassisType)
|
||||
virtual_chassis_list = ObjectListField(VirtualChassisType)
|
||||
|
||||
virtual_device_context = ObjectField(VirtualDeviceContextType)
|
||||
virtual_device_context_list = ObjectListField(VirtualDeviceContextType)
|
||||
|
@ -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
|
||||
@ -480,3 +500,11 @@ class VirtualChassisType(NetBoxObjectType):
|
||||
model = models.VirtualChassis
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.VirtualChassisFilterSet
|
||||
|
||||
|
||||
class VirtualDeviceContextType(NetBoxObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.VirtualDeviceContext
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.VirtualDeviceContextFilterSet
|
||||
|
@ -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)),
|
||||
|
@ -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)),
|
||||
|
@ -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(
|
||||
|
332
netbox/dcim/migrations/0162_unique_constraints.py
Normal file
332
netbox/dcim/migrations/0162_unique_constraints.py
Normal file
@ -0,0 +1,332 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.functions.text
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0161_cabling_cleanup'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveConstraint(
|
||||
model_name='cabletermination',
|
||||
name='dcim_cable_termination_unique_termination',
|
||||
),
|
||||
migrations.RemoveConstraint(
|
||||
model_name='location',
|
||||
name='dcim_location_name',
|
||||
),
|
||||
migrations.RemoveConstraint(
|
||||
model_name='location',
|
||||
name='dcim_location_slug',
|
||||
),
|
||||
migrations.RemoveConstraint(
|
||||
model_name='region',
|
||||
name='dcim_region_name',
|
||||
),
|
||||
migrations.RemoveConstraint(
|
||||
model_name='region',
|
||||
name='dcim_region_slug',
|
||||
),
|
||||
migrations.RemoveConstraint(
|
||||
model_name='sitegroup',
|
||||
name='dcim_sitegroup_name',
|
||||
),
|
||||
migrations.RemoveConstraint(
|
||||
model_name='sitegroup',
|
||||
name='dcim_sitegroup_slug',
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='consoleport',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='consoleporttemplate',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='consoleserverport',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='consoleserverporttemplate',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='device',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='devicebay',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='devicebaytemplate',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='devicetype',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='frontport',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='frontporttemplate',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='interface',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='interfacetemplate',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='inventoryitem',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='inventoryitemtemplate',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='modulebay',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='modulebaytemplate',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='moduletype',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='powerfeed',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='poweroutlet',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='poweroutlettemplate',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='powerpanel',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='powerport',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='powerporttemplate',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='rack',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='rearport',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='rearporttemplate',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='cabletermination',
|
||||
constraint=models.UniqueConstraint(fields=('termination_type', 'termination_id'), name='dcim_cabletermination_unique_termination'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='consoleport',
|
||||
constraint=models.UniqueConstraint(fields=('device', 'name'), name='dcim_consoleport_unique_device_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='consoleporttemplate',
|
||||
constraint=models.UniqueConstraint(fields=('device_type', 'name'), name='dcim_consoleporttemplate_unique_device_type_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='consoleporttemplate',
|
||||
constraint=models.UniqueConstraint(fields=('module_type', 'name'), name='dcim_consoleporttemplate_unique_module_type_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='consoleserverport',
|
||||
constraint=models.UniqueConstraint(fields=('device', 'name'), name='dcim_consoleserverport_unique_device_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='consoleserverporttemplate',
|
||||
constraint=models.UniqueConstraint(fields=('device_type', 'name'), name='dcim_consoleserverporttemplate_unique_device_type_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='consoleserverporttemplate',
|
||||
constraint=models.UniqueConstraint(fields=('module_type', 'name'), name='dcim_consoleserverporttemplate_unique_module_type_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='device',
|
||||
constraint=models.UniqueConstraint(django.db.models.functions.text.Lower('name'), models.F('site'), models.F('tenant'), name='dcim_device_unique_name_site_tenant'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='device',
|
||||
constraint=models.UniqueConstraint(django.db.models.functions.text.Lower('name'), models.F('site'), condition=models.Q(('tenant__isnull', True)), name='dcim_device_unique_name_site', violation_error_message='Device name must be unique per site.'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='device',
|
||||
constraint=models.UniqueConstraint(fields=('rack', 'position', 'face'), name='dcim_device_unique_rack_position_face'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='device',
|
||||
constraint=models.UniqueConstraint(fields=('virtual_chassis', 'vc_position'), name='dcim_device_unique_virtual_chassis_vc_position'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='devicebay',
|
||||
constraint=models.UniqueConstraint(fields=('device', 'name'), name='dcim_devicebay_unique_device_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='devicebaytemplate',
|
||||
constraint=models.UniqueConstraint(fields=('device_type', 'name'), name='dcim_devicebaytemplate_unique_device_type_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='devicetype',
|
||||
constraint=models.UniqueConstraint(fields=('manufacturer', 'model'), name='dcim_devicetype_unique_manufacturer_model'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='devicetype',
|
||||
constraint=models.UniqueConstraint(fields=('manufacturer', 'slug'), name='dcim_devicetype_unique_manufacturer_slug'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='frontport',
|
||||
constraint=models.UniqueConstraint(fields=('device', 'name'), name='dcim_frontport_unique_device_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='frontport',
|
||||
constraint=models.UniqueConstraint(fields=('rear_port', 'rear_port_position'), name='dcim_frontport_unique_rear_port_position'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='frontporttemplate',
|
||||
constraint=models.UniqueConstraint(fields=('device_type', 'name'), name='dcim_frontporttemplate_unique_device_type_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='frontporttemplate',
|
||||
constraint=models.UniqueConstraint(fields=('module_type', 'name'), name='dcim_frontporttemplate_unique_module_type_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='frontporttemplate',
|
||||
constraint=models.UniqueConstraint(fields=('rear_port', 'rear_port_position'), name='dcim_frontporttemplate_unique_rear_port_position'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='interface',
|
||||
constraint=models.UniqueConstraint(fields=('device', 'name'), name='dcim_interface_unique_device_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='interfacetemplate',
|
||||
constraint=models.UniqueConstraint(fields=('device_type', 'name'), name='dcim_interfacetemplate_unique_device_type_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='interfacetemplate',
|
||||
constraint=models.UniqueConstraint(fields=('module_type', 'name'), name='dcim_interfacetemplate_unique_module_type_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='inventoryitem',
|
||||
constraint=models.UniqueConstraint(fields=('device', 'parent', 'name'), name='dcim_inventoryitem_unique_device_parent_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='inventoryitemtemplate',
|
||||
constraint=models.UniqueConstraint(fields=('device_type', 'parent', 'name'), name='dcim_inventoryitemtemplate_unique_device_type_parent_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='location',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('parent__isnull', True)), fields=('site', 'name'), name='dcim_location_name', violation_error_message='A location with this name already exists within the specified site.'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='location',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('parent__isnull', True)), fields=('site', 'slug'), name='dcim_location_slug', violation_error_message='A location with this slug already exists within the specified site.'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='modulebay',
|
||||
constraint=models.UniqueConstraint(fields=('device', 'name'), name='dcim_modulebay_unique_device_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='modulebaytemplate',
|
||||
constraint=models.UniqueConstraint(fields=('device_type', 'name'), name='dcim_modulebaytemplate_unique_device_type_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='moduletype',
|
||||
constraint=models.UniqueConstraint(fields=('manufacturer', 'model'), name='dcim_moduletype_unique_manufacturer_model'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='powerfeed',
|
||||
constraint=models.UniqueConstraint(fields=('power_panel', 'name'), name='dcim_powerfeed_unique_power_panel_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='poweroutlet',
|
||||
constraint=models.UniqueConstraint(fields=('device', 'name'), name='dcim_poweroutlet_unique_device_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='poweroutlettemplate',
|
||||
constraint=models.UniqueConstraint(fields=('device_type', 'name'), name='dcim_poweroutlettemplate_unique_device_type_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='poweroutlettemplate',
|
||||
constraint=models.UniqueConstraint(fields=('module_type', 'name'), name='dcim_poweroutlettemplate_unique_module_type_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='powerpanel',
|
||||
constraint=models.UniqueConstraint(fields=('site', 'name'), name='dcim_powerpanel_unique_site_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='powerport',
|
||||
constraint=models.UniqueConstraint(fields=('device', 'name'), name='dcim_powerport_unique_device_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='powerporttemplate',
|
||||
constraint=models.UniqueConstraint(fields=('device_type', 'name'), name='dcim_powerporttemplate_unique_device_type_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='powerporttemplate',
|
||||
constraint=models.UniqueConstraint(fields=('module_type', 'name'), name='dcim_powerporttemplate_unique_module_type_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='rack',
|
||||
constraint=models.UniqueConstraint(fields=('location', 'name'), name='dcim_rack_unique_location_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='rack',
|
||||
constraint=models.UniqueConstraint(fields=('location', 'facility_id'), name='dcim_rack_unique_location_facility_id'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='rearport',
|
||||
constraint=models.UniqueConstraint(fields=('device', 'name'), name='dcim_rearport_unique_device_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='rearporttemplate',
|
||||
constraint=models.UniqueConstraint(fields=('device_type', 'name'), name='dcim_rearporttemplate_unique_device_type_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='rearporttemplate',
|
||||
constraint=models.UniqueConstraint(fields=('module_type', 'name'), name='dcim_rearporttemplate_unique_module_type_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='region',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('parent__isnull', True)), fields=('name',), name='dcim_region_name', violation_error_message='A top-level region with this name already exists.'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='region',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('parent__isnull', True)), fields=('slug',), name='dcim_region_slug', violation_error_message='A top-level region with this slug already exists.'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='sitegroup',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('parent__isnull', True)), fields=('name',), name='dcim_sitegroup_name', violation_error_message='A top-level site group with this name already exists.'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='sitegroup',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('parent__isnull', True)), fields=('slug',), name='dcim_sitegroup_slug', violation_error_message='A top-level site group with this slug already exists.'),
|
||||
),
|
||||
]
|
72
netbox/dcim/migrations/0163_weight_fields.py
Normal file
72
netbox/dcim/migrations/0163_weight_fields.py
Normal file
@ -0,0 +1,72 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0162_unique_constraints'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
||||
# Device types
|
||||
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='devicetype',
|
||||
name='_abs_weight',
|
||||
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||
),
|
||||
|
||||
# Module types
|
||||
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='moduletype',
|
||||
name='_abs_weight',
|
||||
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||
),
|
||||
|
||||
# Racks
|
||||
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='max_weight',
|
||||
field=models.PositiveIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
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='_abs_max_weight',
|
||||
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||
),
|
||||
]
|
18
netbox/dcim/migrations/0164_rack_mounting_depth.py
Normal file
18
netbox/dcim/migrations/0164_rack_mounting_depth.py
Normal 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_weight_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='mounting_depth',
|
||||
field=models.PositiveSmallIntegerField(blank=True, null=True),
|
||||
),
|
||||
]
|
@ -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),
|
||||
),
|
||||
]
|
54
netbox/dcim/migrations/0166_virtualdevicecontext.py
Normal file
54
netbox/dcim/migrations/0166_virtualdevicecontext.py
Normal file
@ -0,0 +1,54 @@
|
||||
# Generated by Django 4.1.2 on 2022-11-10 16:56
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
import utilities.json
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0063_standardize_description_comments'),
|
||||
('extras', '0082_savedfilter'),
|
||||
('tenancy', '0009_standardize_description_comments'),
|
||||
('dcim', '0165_standardize_description_comments'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='VirtualDeviceContext',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateTimeField(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=utilities.json.CustomFieldJSONEncoder)),
|
||||
('description', models.CharField(blank=True, max_length=200)),
|
||||
('name', models.CharField(max_length=64)),
|
||||
('status', models.CharField(max_length=50)),
|
||||
('identifier', models.PositiveSmallIntegerField(blank=True, null=True)),
|
||||
('comments', models.TextField(blank=True)),
|
||||
('device', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vdcs', to='dcim.device')),
|
||||
('primary_ip4', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='ipam.ipaddress')),
|
||||
('primary_ip6', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='ipam.ipaddress')),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
('tenant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vdcs', to='tenancy.tenant')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='vdcs',
|
||||
field=models.ManyToManyField(related_name='interfaces', to='dcim.virtualdevicecontext'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='virtualdevicecontext',
|
||||
constraint=models.UniqueConstraint(fields=('device', 'identifier'), name='dcim_virtualdevicecontext_device_identifier'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='virtualdevicecontext',
|
||||
constraint=models.UniqueConstraint(fields=('device', 'name'), name='dcim_virtualdevicecontext_device_name'),
|
||||
),
|
||||
]
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user