mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-10 02:19:37 -06:00
Merge pull request #6175 from netbox-community/feature
Prep for v2.11 release
This commit is contained in:
commit
428858dc75
@ -93,3 +93,7 @@ redis
|
||||
# SVG image rendering (used for rack elevations)
|
||||
# https://github.com/mozman/svgwrite
|
||||
svgwrite
|
||||
|
||||
# Tabular dataset library (for table-based exports)
|
||||
# https://github.com/jazzband/tablib
|
||||
tablib
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# Change Logging
|
||||
|
||||
Every time an object in NetBox is created, updated, or deleted, a serialized copy of that object is saved to the database, along with meta data including the current time and the user associated with the change. These records form a persistent record of changes both for each individual object as well as NetBox as a whole. The global change log can be viewed by navigating to Other > Change Log.
|
||||
Every time an object in NetBox is created, updated, or deleted, a serialized copy of that object taken both before and after the change is saved to the database, along with meta data including the current time and the user associated with the change. These records form a persistent record of changes both for each individual object as well as NetBox as a whole. The global change log can be viewed by navigating to Other > Change Log.
|
||||
|
||||
A serialized representation of the instance being modified is included in JSON format. This is similar to how objects are conveyed within the REST API, but does not include any nested representations. For instance, the `tenant` field of a site will record only the tenant's ID, not a representation of the tenant.
|
||||
|
||||
|
||||
@ -16,6 +16,7 @@ Custom fields must be created through the admin UI under Extras > Custom Fields.
|
||||
* Date: A date in ISO 8601 format (YYYY-MM-DD)
|
||||
* URL: This will be presented as a link in the web UI
|
||||
* Selection: A selection of one of several pre-defined custom choices
|
||||
* Multiple selection: A selection field which supports the assignment of multiple values
|
||||
|
||||
Each custom field must have a name; this should be a simple database-friendly string, e.g. `tps_report`. You may also assign a corresponding human-friendly label (e.g. "TPS report"); the label will be displayed on web forms. A weight is also required: Higher-weight fields will be ordered lower within a form. (The default weight is 100.) If a description is provided, it will appear beneath the field in a form.
|
||||
|
||||
@ -37,7 +38,7 @@ NetBox supports limited custom validation for custom field values. Following are
|
||||
|
||||
Each custom selection field must have at least two choices. These are specified as a comma-separated list. Choices appear in forms in the order they are listed. Note that choice values are saved exactly as they appear, so it's best to avoid superfluous punctuation or symbols where possible.
|
||||
|
||||
If a default value is specified for a selection field, it must exactly match one of the provided choices.
|
||||
If a default value is specified for a selection field, it must exactly match one of the provided choices. The value of a multiple selection field will always return a list, even if only one value is selected.
|
||||
|
||||
## Custom Fields in Templates
|
||||
|
||||
|
||||
@ -170,18 +170,13 @@ Similar to `ChoiceVar`, but allows for the selection of multiple choices.
|
||||
A particular object within NetBox. Each ObjectVar must specify a particular model, and allows the user to select one of the available instances. ObjectVar accepts several arguments, listed below.
|
||||
|
||||
* `model` - The model class
|
||||
* `display_field` - The name of the REST API object field to display in the selection list (default: `'name'`)
|
||||
* `display_field` - The name of the REST API object field to display in the selection list (default: `'display'`)
|
||||
* `query_params` - A dictionary of query parameters to use when retrieving available options (optional)
|
||||
* `null_option` - A label representing a "null" or empty choice (optional)
|
||||
|
||||
The `display_field` argument is useful when referencing a model which does not have a `name` field. For example, when displaying a list of device types, you would likely use the `model` field:
|
||||
|
||||
```python
|
||||
device_type = ObjectVar(
|
||||
model=DeviceType,
|
||||
display_field='model'
|
||||
)
|
||||
```
|
||||
!!! warning
|
||||
The `display_field` parameter is now deprecated, and will be removed in NetBox v2.12. All ObjectVar instances will
|
||||
instead use the new standard `display` field for all serializers (introduced in NetBox v2.11).
|
||||
|
||||
To limit the selections available within the list, additional query parameters can be passed as the `query_params` dictionary. For example, to show only devices with an "active" status:
|
||||
|
||||
|
||||
@ -2,7 +2,10 @@
|
||||
|
||||
NetBox allows users to define custom templates that can be used when exporting objects. To create an export template, navigate to Extras > Export Templates under the admin interface.
|
||||
|
||||
Each export template is associated with a certain type of object. For instance, if you create an export template for VLANs, your custom template will appear under the "Export" button on the VLANs list.
|
||||
Each export template is associated with a certain type of object. For instance, if you create an export template for VLANs, your custom template will appear under the "Export" button on the VLANs list. Each export template must have a name, and may optionally designate a specific export [MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) and/or file extension.
|
||||
|
||||
!!! note
|
||||
The name `table` is reserved for internal use.
|
||||
|
||||
Export templates must be written in [Jinja2](https://jinja.palletsprojects.com/).
|
||||
|
||||
@ -26,6 +29,8 @@ If you need to use the config context data in an export template, you'll should
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
The `as_attachment` attribute of an export template controls its behavior when rendered. If true, the rendered content will be returned to the user as a downloadable file. If false, it will be displayed within the browser. (This may be handy e.g. for generating HTML content.)
|
||||
|
||||
A MIME type and file extension can optionally be defined for each export template. The default MIME type is `text/plain`.
|
||||
|
||||
## Example
|
||||
|
||||
5
docs/additional-features/journaling.md
Normal file
5
docs/additional-features/journaling.md
Normal file
@ -0,0 +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.
|
||||
|
||||
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.
|
||||
@ -38,7 +38,8 @@ The following data is available as context for Jinja2 templates:
|
||||
* `timestamp` - The time at which the event occurred (in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format).
|
||||
* `username` - The name of the user account associated with the change.
|
||||
* `request_id` - The unique request ID. This may be used to correlate multiple changes associated with a single request.
|
||||
* `data` - A serialized representation of the object _after_ the change was made. This is typically equivalent to the model's representation in NetBox's REST API.
|
||||
* `data` - A detailed representation of the object in its current state. This is typically equivalent to the model's representation in NetBox's REST API.
|
||||
* `snapshots` - Minimal "snapshots" of the object state both before and after the change was made; provided ass a dictionary with keys named `prechange` and `postchange`. These are not as extensive as the fully serialized representation, but contain enough information to convey what has changed.
|
||||
|
||||
### Default Request Body
|
||||
|
||||
@ -47,7 +48,7 @@ If no body template is specified, the request body will be populated with a JSON
|
||||
```no-highlight
|
||||
{
|
||||
"event": "created",
|
||||
"timestamp": "2020-02-25 15:10:26.010582+00:00",
|
||||
"timestamp": "2021-03-09 17:55:33.968016+00:00",
|
||||
"model": "site",
|
||||
"username": "jstretch",
|
||||
"request_id": "fdbca812-3142-4783-b364-2e2bd5c16c6a",
|
||||
@ -62,6 +63,17 @@ If no body template is specified, the request body will be populated with a JSON
|
||||
},
|
||||
"region": null,
|
||||
...
|
||||
},
|
||||
"snapshots": {
|
||||
"prechange": null,
|
||||
"postchange": {
|
||||
"created": "2021-03-09",
|
||||
"last_updated": "2021-03-09T17:55:33.851Z",
|
||||
"name": "Site 1",
|
||||
"slug": "site-1",
|
||||
"status": "active",
|
||||
...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
# Circuits
|
||||
|
||||
{!docs/models/circuits/provider.md!}
|
||||
{!docs/models/circuits/providernetwork.md!}
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -8,6 +8,8 @@
|
||||
|
||||
## Device Components
|
||||
|
||||
Device components represent discrete objects within a device which are used to terminate cables, house child devices, or track resources.
|
||||
|
||||
{!docs/models/dcim/consoleport.md!}
|
||||
{!docs/models/dcim/consoleserverport.md!}
|
||||
{!docs/models/dcim/powerport.md!}
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
# Sites and Racks
|
||||
|
||||
{!docs/models/dcim/site.md!}
|
||||
{!docs/models/dcim/region.md!}
|
||||
{!docs/models/dcim/sitegroup.md!}
|
||||
{!docs/models/dcim/site.md!}
|
||||
{!docs/models/dcim/location.md!}
|
||||
|
||||
---
|
||||
|
||||
{!docs/models/dcim/rack.md!}
|
||||
{!docs/models/dcim/rackgroup.md!}
|
||||
{!docs/models/dcim/rackrole.md!}
|
||||
{!docs/models/dcim/rackreservation.md!}
|
||||
|
||||
98
docs/development/models.md
Normal file
98
docs/development/models.md
Normal file
@ -0,0 +1,98 @@
|
||||
# NetBox Models
|
||||
|
||||
## Model Types
|
||||
|
||||
A NetBox model represents a discrete object type such as a device or IP address. Each model is defined as a Python class and has its own SQL table. All NetBox data models can be categorized by type.
|
||||
|
||||
The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/) framework can be used to reference models within the database. A ContentType instance references a model by its `app_label` and `name`: For example, the Site model is referred to as `dcim.site`. The content type combined with an object's primary key form a globally unique identifier for the object (e.g. `dcim.site:123`).
|
||||
|
||||
### Features Matrix
|
||||
|
||||
* [Change logging](../additional-features/change-logging.md) - Changes to these objects are automatically recorded in the change log
|
||||
* [Webhooks](../additional-features/webhooks.md) - NetBox is capable of generating outgoing webhooks for these objects
|
||||
* [Custom fields](../additional-features/custom-fields.md) - These models support the addition of user-defined fields
|
||||
* [Export templates](../additional-features/export-templates.md) - Users can create custom export templates for these models
|
||||
* [Tagging](../models/extras/tag.md) - The models can be tagged with user-defined tags
|
||||
* [Journaling](../additional-features/journaling.md) - These models support persistent historical commentary
|
||||
* Nesting - These models can be nested recursively to create a hierarchy
|
||||
|
||||
| Type | Change Logging | Webhooks | Custom Fields | Export Templates | Tags | Journaling | Nesting |
|
||||
| ------------------ | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- |
|
||||
| Primary | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | |
|
||||
| Organizational | :material-check: | :material-check: | :material-check: | :material-check: | | | |
|
||||
| Nested Group | :material-check: | :material-check: | :material-check: | :material-check: | | | :material-check: |
|
||||
| Component | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | | |
|
||||
| Component Template | :material-check: | :material-check: | :material-check: | | | | |
|
||||
|
||||
## Models Index
|
||||
|
||||
### Primary Models
|
||||
|
||||
* [circuits.Circuit](../models/circuits/circuit.md)
|
||||
* [circuits.Provider](../models/circuits/provider.md)
|
||||
* [circuits.ProviderNetwork](../models/circuits/providernetwork.md)
|
||||
* [dcim.Cable](../models/dcim/cable.md)
|
||||
* [dcim.Device](../models/dcim/device.md)
|
||||
* [dcim.DeviceType](../models/dcim/devicetype.md)
|
||||
* [dcim.PowerFeed](../models/dcim/powerfeed.md)
|
||||
* [dcim.PowerPanel](../models/dcim/powerpanel.md)
|
||||
* [dcim.Rack](../models/dcim/rack.md)
|
||||
* [dcim.RackReservation](../models/dcim/rackreservation.md)
|
||||
* [dcim.Site](../models/dcim/site.md)
|
||||
* [dcim.VirtualChassis](../models/dcim/virtualchassis.md)
|
||||
* [ipam.Aggregate](../models/ipam/aggregate.md)
|
||||
* [ipam.IPAddress](../models/ipam/ipaddress.md)
|
||||
* [ipam.Prefix](../models/ipam/prefix.md)
|
||||
* [ipam.RouteTarget](../models/ipam/routetarget.md)
|
||||
* [ipam.Service](../models/ipam/service.md)
|
||||
* [ipam.VLAN](../models/ipam/vlan.md)
|
||||
* [ipam.VRF](../models/ipam/vrf.md)
|
||||
* [secrets.Secret](../models/secrets/secret.md)
|
||||
* [tenancy.Tenant](../models/tenancy/tenant.md)
|
||||
* [virtualization.Cluster](../models/virtualization/cluster.md)
|
||||
* [virtualization.VirtualMachine](../models/virtualization/virtualmachine.md)
|
||||
|
||||
### Organizational Models
|
||||
|
||||
* [circuits.CircuitType](../models/circuits/circuittype.md)
|
||||
* [dcim.DeviceRole](../models/dcim/devicerole.md)
|
||||
* [dcim.Manufacturer](../models/dcim/manufacturer.md)
|
||||
* [dcim.Platform](../models/dcim/platform.md)
|
||||
* [dcim.RackRole](../models/dcim/rackrole.md)
|
||||
* [ipam.RIR](../models/ipam/rir.md)
|
||||
* [ipam.Role](../models/ipam/role.md)
|
||||
* [ipam.VLANGroup](../models/ipam/vlangroup.md)
|
||||
* [secrets.SecretRole](../models/secrets/secretrole.md)
|
||||
* [virtualization.ClusterGroup](../models/virtualization/clustergroup.md)
|
||||
* [virtualization.ClusterType](../models/virtualization/clustertype.md)
|
||||
|
||||
### Nested Group Models
|
||||
|
||||
* [dcim.Location](../models/dcim/location.md) (formerly RackGroup)
|
||||
* [dcim.Region](../models/dcim/region.md)
|
||||
* [dcim.SiteGroup](../models/dcim/sitegroup.md)
|
||||
* [tenancy.TenantGroup](../models/tenancy/tenantgroup.md)
|
||||
|
||||
### Component Models
|
||||
|
||||
* [dcim.ConsolePort](../models/dcim/consoleport.md)
|
||||
* [dcim.ConsoleServerPort](../models/dcim/consoleserverport.md)
|
||||
* [dcim.DeviceBay](../models/dcim/devicebay.md)
|
||||
* [dcim.FrontPort](../models/dcim/frontport.md)
|
||||
* [dcim.Interface](../models/dcim/interface.md)
|
||||
* [dcim.InventoryItem](../models/dcim/inventoryitem.md)
|
||||
* [dcim.PowerOutlet](../models/dcim/poweroutlet.md)
|
||||
* [dcim.PowerPort](../models/dcim/powerport.md)
|
||||
* [dcim.RearPort](../models/dcim/rearport.md)
|
||||
* [virtualization.VMInterface](../models/virtualization/vminterface.md)
|
||||
|
||||
### Component Template Models
|
||||
|
||||
* [dcim.ConsolePortTemplate](../models/dcim/consoleporttemplate.md)
|
||||
* [dcim.ConsoleServerPortTemplate](../models/dcim/consoleserverporttemplate.md)
|
||||
* [dcim.DeviceBayTemplate](../models/dcim/devicebaytemplate.md)
|
||||
* [dcim.FrontPortTemplate](../models/dcim/frontporttemplate.md)
|
||||
* [dcim.InterfaceTemplate](../models/dcim/interfacetemplate.md)
|
||||
* [dcim.PowerOutletTemplate](../models/dcim/poweroutlettemplate.md)
|
||||
* [dcim.PowerPortTemplate](../models/dcim/powerporttemplate.md)
|
||||
* [dcim.RearPortTemplate](../models/dcim/rearporttemplate.md)
|
||||
@ -7,32 +7,31 @@ This section entails the installation and configuration of a local PostgreSQL da
|
||||
|
||||
## Installation
|
||||
|
||||
#### Ubuntu
|
||||
=== "Ubuntu"
|
||||
|
||||
Install the PostgreSQL server and client development libraries using `apt`.
|
||||
```no-highlight
|
||||
sudo apt update
|
||||
sudo apt install -y postgresql libpq-dev
|
||||
```
|
||||
|
||||
```no-highlight
|
||||
sudo apt update
|
||||
sudo apt install -y postgresql libpq-dev
|
||||
```
|
||||
=== "CentOS"
|
||||
|
||||
#### CentOS
|
||||
```no-highlight
|
||||
sudo yum install -y postgresql-server libpq-devel
|
||||
sudo postgresql-setup --initdb
|
||||
```
|
||||
|
||||
PostgreSQL 9.6 and later are available natively on CentOS 8.2. If using an earlier CentOS release, you may need to [install it from an RPM](https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/).
|
||||
!!! info
|
||||
PostgreSQL 9.6 and later are available natively on CentOS 8.2. If using an earlier CentOS release, you may need to [install it from an RPM](https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/).
|
||||
|
||||
```no-highlight
|
||||
sudo yum install -y postgresql-server libpq-devel
|
||||
sudo postgresql-setup --initdb
|
||||
```
|
||||
CentOS configures ident host-based authentication for PostgreSQL by default. Because NetBox will need to authenticate using a username and password, modify `/var/lib/pgsql/data/pg_hba.conf` to support MD5 authentication by changing `ident` to `md5` for the lines below:
|
||||
|
||||
CentOS configures ident host-based authentication for PostgreSQL by default. Because NetBox will need to authenticate using a username and password, modify `/var/lib/pgsql/data/pg_hba.conf` to support MD5 authentication by changing `ident` to `md5` for the lines below:
|
||||
```no-highlight
|
||||
host all all 127.0.0.1/32 md5
|
||||
host all all ::1/128 md5
|
||||
```
|
||||
|
||||
```no-highlight
|
||||
host all all 127.0.0.1/32 md5
|
||||
host all all ::1/128 md5
|
||||
```
|
||||
|
||||
Then, start the service and enable it to run at boot:
|
||||
Once PostgreSQL has been installed, start the service and enable it to run at boot:
|
||||
|
||||
```no-highlight
|
||||
sudo systemctl start postgresql
|
||||
|
||||
@ -7,19 +7,19 @@
|
||||
!!! note
|
||||
NetBox v2.9.0 and later require Redis v4.0 or higher. If your distribution does not offer a recent enough release, you will need to build Redis from source. Please see [the Redis installation documentation](https://github.com/redis/redis) for further details.
|
||||
|
||||
### Ubuntu
|
||||
=== "Ubuntu"
|
||||
|
||||
```no-highlight
|
||||
sudo apt install -y redis-server
|
||||
```
|
||||
```no-highlight
|
||||
sudo apt install -y redis-server
|
||||
```
|
||||
|
||||
### CentOS
|
||||
=== "CentOS"
|
||||
|
||||
```no-highlight
|
||||
sudo yum install -y redis
|
||||
sudo systemctl start redis
|
||||
sudo systemctl enable redis
|
||||
```
|
||||
```no-highlight
|
||||
sudo yum install -y redis
|
||||
sudo systemctl start redis
|
||||
sudo systemctl enable redis
|
||||
```
|
||||
|
||||
You may wish to modify the Redis configuration at `/etc/redis.conf` or `/etc/redis/redis.conf`, however in most cases the default configuration is sufficient.
|
||||
|
||||
|
||||
@ -9,17 +9,17 @@ Begin by installing all system packages required by NetBox and its dependencies.
|
||||
!!! note
|
||||
NetBox v2.8.0 and later require Python 3.6, 3.7, or 3.8.
|
||||
|
||||
### Ubuntu
|
||||
=== "Ubuntu"
|
||||
|
||||
```no-highlight
|
||||
sudo apt install -y python3 python3-pip python3-venv python3-dev build-essential libxml2-dev libxslt1-dev libffi-dev libpq-dev libssl-dev zlib1g-dev
|
||||
```
|
||||
```no-highlight
|
||||
sudo apt install -y python3 python3-pip python3-venv python3-dev build-essential libxml2-dev libxslt1-dev libffi-dev libpq-dev libssl-dev zlib1g-dev
|
||||
```
|
||||
|
||||
### CentOS
|
||||
=== "CentOS"
|
||||
|
||||
```no-highlight
|
||||
sudo yum install -y gcc python36 python36-devel python3-pip libxml2-devel libxslt-devel libffi-devel openssl-devel redhat-rpm-config
|
||||
```
|
||||
```no-highlight
|
||||
sudo yum install -y gcc python36 python36-devel python3-pip libxml2-devel libxslt-devel libffi-devel openssl-devel redhat-rpm-config
|
||||
```
|
||||
|
||||
Before continuing with either platform, update pip (Python's package management tool) to its latest release:
|
||||
|
||||
@ -57,17 +57,17 @@ sudo mkdir -p /opt/netbox/ && cd /opt/netbox/
|
||||
|
||||
If `git` is not already installed, install it:
|
||||
|
||||
#### Ubuntu
|
||||
=== "Ubuntu"
|
||||
|
||||
```no-highlight
|
||||
sudo apt install -y git
|
||||
```
|
||||
```no-highlight
|
||||
sudo apt install -y git
|
||||
```
|
||||
|
||||
#### CentOS
|
||||
=== "CentOS"
|
||||
|
||||
```no-highlight
|
||||
sudo yum install -y git
|
||||
```
|
||||
```no-highlight
|
||||
sudo yum install -y git
|
||||
```
|
||||
|
||||
Next, clone the **master** branch of the NetBox GitHub repository into the current directory. (This branch always holds the current stable release.)
|
||||
|
||||
@ -89,20 +89,20 @@ Checking connectivity... done.
|
||||
|
||||
Create a system user account named `netbox`. We'll configure the WSGI and HTTP services to run under this account. We'll also assign this user ownership of the media directory. This ensures that NetBox will be able to save uploaded files.
|
||||
|
||||
#### Ubuntu
|
||||
=== "Ubuntu"
|
||||
|
||||
```
|
||||
sudo adduser --system --group netbox
|
||||
sudo chown --recursive netbox /opt/netbox/netbox/media/
|
||||
```
|
||||
```
|
||||
sudo adduser --system --group netbox
|
||||
sudo chown --recursive netbox /opt/netbox/netbox/media/
|
||||
```
|
||||
|
||||
#### CentOS
|
||||
=== "CentOS"
|
||||
|
||||
```
|
||||
sudo groupadd --system netbox
|
||||
sudo adduser --system -g netbox netbox
|
||||
sudo chown --recursive netbox /opt/netbox/netbox/media/
|
||||
```
|
||||
```
|
||||
sudo groupadd --system netbox
|
||||
sudo adduser --system -g netbox netbox
|
||||
sudo chown --recursive netbox /opt/netbox/netbox/media/
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
@ -264,7 +264,7 @@ Quit the server with CONTROL-C.
|
||||
|
||||
Next, connect to the name or IP of the server (as defined in `ALLOWED_HOSTS`) on port 8000; for example, <http://127.0.0.1:8000/>. You should be greeted with the NetBox home page.
|
||||
|
||||
!!! warning
|
||||
!!! danger
|
||||
The development server is for development and testing purposes only. It is neither performant nor secure enough for production use. **Do not use it in production.**
|
||||
|
||||
!!! warning
|
||||
|
||||
@ -23,6 +23,9 @@ The video below demonstrates the installation of NetBox v2.10.3 on Ubuntu 20.04
|
||||
| PostgreSQL | 9.6 |
|
||||
| Redis | 4.0 |
|
||||
|
||||
!!! note
|
||||
Python 3.7 or later will be required in NetBox v2.12. Users are strongly encouraged to install NetBox using Python 3.7 or later for new deployments.
|
||||
|
||||
Below is a simplified overview of the NetBox application stack for reference:
|
||||
|
||||

|
||||
@ -30,6 +33,3 @@ Below is a simplified overview of the NetBox application stack for reference:
|
||||
## Upgrading
|
||||
|
||||
If you are upgrading from an existing installation, please consult the [upgrading guide](upgrading.md).
|
||||
|
||||
!!! note
|
||||
Beginning with v2.5.9, the official documentation calls for systemd to be used for managing the WSGI workers in place of supervisord. Please see the instructions for [migrating to systemd](migrating-to-systemd.md) if you are still using supervisord.
|
||||
|
||||
@ -2,9 +2,9 @@
|
||||
|
||||
The association of a circuit with a particular site and/or device is modeled separately as a circuit termination. A circuit may have up to two terminations, labeled A and Z. A single-termination circuit can be used when you don't know (or care) about the far end of a circuit (for example, an Internet access circuit which connects to a transit provider). A dual-termination circuit is useful for tracking circuits which connect two sites.
|
||||
|
||||
Each circuit termination is tied to a site, and may optionally be connected via a cable to a specific device interface or port within that site. Each termination must be assigned a port speed, and can optionally be assigned an upstream speed if it differs from the downstream speed (a common scenario with e.g. DOCSIS cable modems). Fields are also available to track cross-connect and patch panel details.
|
||||
Each circuit termination is attached to either a site or to a provider network. Site terminations may optionally be connected via a cable to a specific device interface or port within that site. Each termination must be assigned a port speed, and can optionally be assigned an upstream speed if it differs from the downstream speed (a common scenario with e.g. DOCSIS cable modems). Fields are also available to track cross-connect and patch panel details.
|
||||
|
||||
In adherence with NetBox's philosophy of closely modeling the real world, a circuit may terminate only to a physical interface. For example, circuits may not terminate to LAG interfaces, which are virtual in nature. In such cases, a separate physical circuit is associated with each LAG member interface and each needs to be modeled discretely.
|
||||
In adherence with NetBox's philosophy of closely modeling the real world, a circuit may be connected only to a physical interface. For example, circuits may not terminate to LAG interfaces, which are virtual in nature. In such cases, a separate physical circuit is associated with each LAG member interface and each needs to be modeled discretely.
|
||||
|
||||
!!! note
|
||||
A circuit in NetBox represents a physical link, and cannot have more than two endpoints. When modeling a multi-point topology, each leg of the topology must be defined as a discrete circuit, with one end terminating within the provider's infrastructure.
|
||||
A circuit in NetBox represents a physical link, and cannot have more than two endpoints. When modeling a multi-point topology, each leg of the topology must be defined as a discrete circuit, with one end terminating within the provider's infrastructure. The provider network model is ideal for representing these networks.
|
||||
|
||||
5
docs/models/circuits/providernetwork.md
Normal file
5
docs/models/circuits/providernetwork.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Provider Networks
|
||||
|
||||
This model can be used to represent the boundary of a provider network, the details of which are unknown or unimportant to the NetBox user. For example, it might represent a provider's regional MPLS network to which multiple circuits provide connectivity.
|
||||
|
||||
Each provider network must be assigned to a provider. A circuit may terminate to either a provider network or to a site.
|
||||
@ -8,7 +8,7 @@ A device is said to be full-depth if its installation on one rack face prevents
|
||||
|
||||
Each device must be instantiated from a pre-created device type, and its default components (console ports, power ports, interfaces, etc.) will be created automatically. (The device type associated with a device may be changed after its creation, however its components will not be updated retroactively.)
|
||||
|
||||
Each device must be assigned a site, device role, and operational status, and may optionally be assigned to a specific rack within a site. A platform, serial number, and asset tag may optionally be assigned to each device.
|
||||
Each device must be assigned a site, device role, and operational status, and may optionally be assigned to a specific location and/or rack within a site. A platform, serial number, and asset tag may optionally be assigned to each device.
|
||||
|
||||
Device names must be unique within a site, unless the device has been assigned to a tenant. Devices may also be unnamed.
|
||||
|
||||
|
||||
@ -2,11 +2,15 @@
|
||||
|
||||
Interfaces in NetBox represent network interfaces used to exchange data with connected devices. On modern networks, these are most commonly Ethernet, but other types are supported as well. Each interface must be assigned a type, and may optionally be assigned a MAC address, MTU, and IEEE 802.1Q mode (tagged or access). Each interface can also be enabled or disabled, and optionally designated as management-only (for out-of-band management).
|
||||
|
||||
Interfaces may be physical or virtual in nature, but only physical interfaces may be connected via cables. Cables can connect interfaces to pass-through ports, circuit terminations, or other interfaces.
|
||||
!!! note
|
||||
Although devices and virtual machines both can have interfaces, a separate model is used for each. Thus, device interfaces have some properties that are not present on virtual machine interfaces and vice versa.
|
||||
|
||||
### Interface Types
|
||||
|
||||
Interfaces may be physical or virtual in nature, but only physical interfaces may be connected via cables. Cables can connect interfaces to pass-through ports, circuit terminations, or other interfaces. Virtual interfaces, such as 802.1Q-tagged subinterfaces, may be assigned to physical parent interfaces.
|
||||
|
||||
Physical interfaces may be arranged into a link aggregation group (LAG) and associated with a parent LAG (virtual) interface. LAG interfaces can be recursively nested to model bonding of trunk groups. Like all virtual interfaces, LAG interfaces cannot be connected physically.
|
||||
|
||||
IP addresses can be assigned to interfaces. VLANs can also be assigned to each interface as either tagged or untagged. (An interface may have only one untagged VLAN.)
|
||||
### IP Address Assignment
|
||||
|
||||
!!! note
|
||||
Although devices and virtual machines both can have interfaces, a separate model is used for each. Thus, device interfaces have some properties that are not present on virtual machine interfaces and vice versa.
|
||||
IP addresses can be assigned to interfaces. VLANs can also be assigned to each interface as either tagged or untagged. (An interface may have only one untagged VLAN.)
|
||||
|
||||
5
docs/models/dcim/location.md
Normal file
5
docs/models/dcim/location.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Locations
|
||||
|
||||
Racks and devices can be grouped by location within a site. A location may represent a floor, room, cage, or similar organizational unit. Locations can be nested to form a hierarchy. For example, you may have floors within a site, and rooms within a floor.
|
||||
|
||||
The name and facility ID of each rack within a location must be unique. (Racks not assigned to the same location may have identical names and/or facility IDs.)
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
A power panel represents the origin point in NetBox for electrical power being disseminated by one or more power feeds. In a data center environment, one power panel often serves a group of racks, with an individual power feed extending to each rack, though this is not always the case. It is common to have two sets of panels and feeds arranged in parallel to provide redundant power to each rack.
|
||||
|
||||
Each power panel must be assigned to a site, and may optionally be assigned to a particular rack group.
|
||||
Each power panel must be assigned to a site, and may optionally be assigned to a particular location within that site.
|
||||
|
||||
!!! note
|
||||
NetBox does not model the mechanism by which power is delivered to a power panel. Power panels define the root level of the power distribution hierarchy in NetBox.
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# Racks
|
||||
|
||||
The rack model represents a physical two- or four-post equipment rack in which devices can be installed. Each rack must be assigned to a site, and may optionally be assigned to a rack group and/or tenant. Racks can also be organized by user-defined functional roles.
|
||||
The rack model represents a physical two- or four-post equipment rack in which devices can be installed. Each rack must be assigned to a site, and may optionally be assigned to a location and/or tenant. Racks can also be organized by user-defined functional roles.
|
||||
|
||||
Rack height is measured in *rack units* (U); racks are commonly between 42U and 48U tall, but NetBox allows you to define racks of arbitrary height. A toggle is provided to indicate whether rack units are in ascending (from the ground up) or descending order.
|
||||
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
# Rack Groups
|
||||
|
||||
Racks can be organized into groups, which can be nested into themselves similar to regions. As with sites, how you choose to designate rack groups will depend on the nature of your organization. For example, if each site represents a campus, each group might represent a building within a campus. If each site represents a building, each rack group might equate to a floor or room.
|
||||
|
||||
Each rack group must be assigned to a parent site, and rack groups may optionally be nested within a site to model a multi-level hierarchy. For example, you might have a tier of rooms beneath a tier of floors, all belonging to the same parent building (site).
|
||||
|
||||
The name and facility ID of each rack within a group must be unique. (Racks not assigned to the same rack group may have identical names and/or facility IDs.)
|
||||
3
docs/models/dcim/sitegroup.md
Normal file
3
docs/models/dcim/sitegroup.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Site Groups
|
||||
|
||||
Like regions, site groups can be used to organize sites. Whereas regions are intended to provide geographic organization, site groups can be used to classify sites by role or function. Also like regions, site groups can be nested to form a hierarchy. Sites which belong to a child group are also considered to be members of any of its parent groups.
|
||||
@ -3,11 +3,13 @@
|
||||
Sometimes it is desirable to associate additional data with a group of devices or virtual machines to aid in automated configuration. For example, you might want to associate a set of syslog servers for all devices within a particular region. Context data enables the association of extra user-defined data with devices and virtual machines grouped by one or more of the following assignments:
|
||||
|
||||
* Region
|
||||
* Site group
|
||||
* Site
|
||||
* Device type (devices only)
|
||||
* Role
|
||||
* Platform
|
||||
* Cluster group
|
||||
* Cluster
|
||||
* Cluster group (VMs only)
|
||||
* Cluster (VMs only)
|
||||
* Tenant group
|
||||
* Tenant
|
||||
* Tag
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# VLANs
|
||||
|
||||
A VLAN represents an isolated layer two domain, identified by a name and a numeric ID (1-4094) as defined in [IEEE 802.1Q](https://en.wikipedia.org/wiki/IEEE_802.1Q). Each VLAN may be assigned to a site, tenant, and/or VLAN group.
|
||||
A VLAN represents an isolated layer two domain, identified by a name and a numeric ID (1-4094) as defined in [IEEE 802.1Q](https://en.wikipedia.org/wiki/IEEE_802.1Q). VLANs are arranged into VLAN groups to define scope and to enforce uniqueness.
|
||||
|
||||
Each VLAN must be assigned one of the following operational statuses:
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# VLAN Groups
|
||||
|
||||
VLAN groups can be used to organize VLANs within NetBox. Each group may optionally be assigned to a specific site, but a group cannot belong to multiple sites.
|
||||
VLAN groups can be used to organize VLANs within NetBox. Each VLAN group can be scoped to a particular region, site group, site, location, rack, cluster group, or cluster. Member VLANs will be available for assignment to devices and/or virtual machines within the specified scope.
|
||||
|
||||
Groups can also be used to enforce uniqueness: Each VLAN within a group must have a unique ID and name. VLANs which are not assigned to a group may have overlapping names and IDs (including VLANs which belong to a common site). For example, you can create two VLANs with ID 123, but they cannot both be assigned to the same group.
|
||||
|
||||
@ -11,4 +11,6 @@ Like devices, each VM can be assigned a platform and/or functional role, and mus
|
||||
* Failed
|
||||
* Decommissioning
|
||||
|
||||
Additional fields are available for annotating the vCPU count, memory (GB), and disk (GB) allocated to each VM. Each VM may optionally be assigned to a tenant. Virtual machines may have virtual interfaces assigned to them, but do not support any physical component.
|
||||
Additional fields are available for annotating the vCPU count, memory (GB), and disk (GB) allocated to each VM. A VM may be allocated a partial vCPU count (e.g. 1.5 vCPU).
|
||||
|
||||
Each VM may optionally be assigned to a tenant. Virtual machines may have virtual interfaces assigned to them, but do not support any physical component.
|
||||
|
||||
@ -1 +1 @@
|
||||
version-2.10.md
|
||||
version-2.11.md
|
||||
202
docs/release-notes/version-2.11.md
Normal file
202
docs/release-notes/version-2.11.md
Normal file
@ -0,0 +1,202 @@
|
||||
# NetBox v2.11
|
||||
|
||||
## v2.11.0 (FUTURE)
|
||||
|
||||
### Enhancements (from Beta)
|
||||
|
||||
* [#5757](https://github.com/netbox-community/netbox/issues/5757) - Add unique identifier to every object view
|
||||
* [#5848](https://github.com/netbox-community/netbox/issues/5848) - Filter custom fields by content type in format `<app_label>.<model>`
|
||||
* [#6088](https://github.com/netbox-community/netbox/issues/6088) - Improved table configuration form
|
||||
* [#6097](https://github.com/netbox-community/netbox/issues/6097) - Redirect old slug-based object views
|
||||
* [#6109](https://github.com/netbox-community/netbox/issues/6109) - Add device counts to locations table
|
||||
* [#6121](https://github.com/netbox-community/netbox/issues/6121) - Extend parent interface assignment to VM interfaces
|
||||
* [#6125](https://github.com/netbox-community/netbox/issues/6125) - Add locations count to home page
|
||||
* [#6146](https://github.com/netbox-community/netbox/issues/6146) - Add bulk disconnect support for power feeds
|
||||
* [#6149](https://github.com/netbox-community/netbox/issues/6149) - Support image attachments for locations
|
||||
* [#6150](https://github.com/netbox-community/netbox/issues/6150) - Enable change logging for journal entries
|
||||
|
||||
### Bug Fixes (from Beta)
|
||||
|
||||
* [#5583](https://github.com/netbox-community/netbox/issues/5583) - Eliminate redundant change records when adding/removing tags
|
||||
* [#6100](https://github.com/netbox-community/netbox/issues/6100) - Fix VM interfaces table "add interfaces" link
|
||||
* [#6104](https://github.com/netbox-community/netbox/issues/6104) - Fix location column on racks table
|
||||
* [#6105](https://github.com/netbox-community/netbox/issues/6105) - Hide checkboxes for VMs under cluster VMs view
|
||||
* [#6106](https://github.com/netbox-community/netbox/issues/6106) - Allow assigning a virtual interface as the parent of an existing interface
|
||||
* [#6107](https://github.com/netbox-community/netbox/issues/6107) - Fix rack selection field on device form
|
||||
* [#6110](https://github.com/netbox-community/netbox/issues/6110) - Fix handling of TemplateColumn values for table export
|
||||
* [#6123](https://github.com/netbox-community/netbox/issues/6123) - Prevent device from being assigned to mismatched site and location
|
||||
* [#6124](https://github.com/netbox-community/netbox/issues/6124) - Location `parent` filter should return all child locations (not just those directly assigned)
|
||||
* [#6130](https://github.com/netbox-community/netbox/issues/6130) - Improve display of assigned models in custom fields list
|
||||
* [#6155](https://github.com/netbox-community/netbox/issues/6155) - Fix admin links for plugins, background tasks
|
||||
* [#6171](https://github.com/netbox-community/netbox/issues/6171) - Fix display of horizontally-scrolling object lists
|
||||
* [#6173](https://github.com/netbox-community/netbox/issues/6173) - Fix assigned device/VM count when bulk editing/deleting device roles
|
||||
|
||||
---
|
||||
|
||||
## v2.11-beta1 (2021-04-06)
|
||||
|
||||
**WARNING:** This is a beta release and is not suitable for production use. It is intended for development and evaluation purposes only. No upgrade path to the final v2.11 release will be provided from this beta, and users should assume that all data entered into the application will be lost.
|
||||
|
||||
**Note:** NetBox v2.11 is the last major release that will support Python 3.6. Beginning with NetBox v2.12, Python 3.7 or later will be required.
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* All objects now use numeric IDs in their UI view URLs instead of slugs. You may need to update external references to NetBox objects. (Note that this does _not_ affect the REST API.)
|
||||
* The UI now uses numeric IDs when filtering object lists. You may need to update external links to filtered object lists. (Note that the slug- and name-based filters will continue to work, however the filter selection fields within the UI will not be automatically populated.)
|
||||
* The RackGroup model has been renamed to Location (see [#4971](https://github.com/netbox-community/netbox/issues/4971)). Its REST API endpoint has changed from `/api/dcim/rack-groups/` to `/api/dcim/locations/`.
|
||||
* The foreign key field `group` on dcim.Rack has been renamed to `location`.
|
||||
* The foreign key field `site` on ipam.VLANGroup has been replaced with the `scope` generic foreign key (see [#5284](https://github.com/netbox-community/netbox/issues/5284)).
|
||||
* Custom script ObjectVars no longer support the `queryset` parameter: Use `model` instead (see [#5995](https://github.com/netbox-community/netbox/issues/5995)).
|
||||
|
||||
### New Features
|
||||
|
||||
#### Journaling Support ([#151](https://github.com/netbox-community/netbox/issues/151))
|
||||
|
||||
NetBox now supports journaling for all primary objects. The 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 some time, journal entries persist for the life of the associated object.
|
||||
|
||||
#### Parent Interface Assignments ([#1519](https://github.com/netbox-community/netbox/issues/1519))
|
||||
|
||||
Virtual device and VM interfaces can now be assigned to a "parent" interface by setting the `parent` field on the interface object. This is helpful for associating subinterfaces with their physical counterpart. For example, you might assign virtual interfaces Gi0/0.100 and Gi0/0.200 as children of the physical interface Gi0/0.
|
||||
|
||||
#### Pre- and Post-Change Snapshots in Webhooks ([#3451](https://github.com/netbox-community/netbox/issues/3451))
|
||||
|
||||
In conjunction with the newly improved change logging functionality ([#5913](https://github.com/netbox-community/netbox/issues/5913)), outgoing webhooks now include both pre- and post-change representations of the modified object. These are available in the rendering context as a dictionary named `snapshots` with keys `prechange` and `postchange`. For example, here are the abridged snapshots resulting from renaming a site and changing its status:
|
||||
|
||||
```json
|
||||
"snapshots": {
|
||||
"prechange": {
|
||||
"name": "Site 1",
|
||||
"slug": "site-1",
|
||||
"status": "active",
|
||||
...
|
||||
},
|
||||
"postchange": {
|
||||
"name": "Site 2",
|
||||
"slug": "site-2",
|
||||
"status": "planned",
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note: The pre-change snapshot for a newly created will always be null, as will the post-change snapshot for a deleted object.
|
||||
|
||||
#### Mark as Connected Without a Cable ([#3648](https://github.com/netbox-community/netbox/issues/3648))
|
||||
|
||||
Cable termination objects (circuit terminations, power feeds, and most device components) can now be marked as "connected" without actually attaching a cable. This helps simplify the process of modeling an infrastructure boundary where we don't necessarily know or care what is connected to an attachment point, but still need to reflect the termination as being occupied.
|
||||
|
||||
In addition to the new `mark_connected` boolean field, the REST API representation of these objects now also includes a read-only boolean field named `_occupied`. This conveniently returns true if either a cable is attached or `mark_connected` is true.
|
||||
|
||||
#### Allow Assigning Devices to Locations ([#4971](https://github.com/netbox-community/netbox/issues/4971))
|
||||
|
||||
Devices can now be assigned to locations (formerly known as rack groups) within a site without needing to be assigned to a particular rack. This is handy for assigning devices to rooms or floors within a building where racks are not used. The `location` foreign key field has been added to the Device model to support this.
|
||||
|
||||
#### Dynamic Object Exports ([#4999](https://github.com/netbox-community/netbox/issues/4999))
|
||||
|
||||
When exporting a list of objects in NetBox, users now have the option of selecting the "current view". This will render CSV output matching the current configuration of the table being viewed. For example, if you modify the sites list to display only the site name, tenant, and status, the rendered CSV will include only these columns, and they will appear in the order chosen.
|
||||
|
||||
The legacy static export behavior has been retained to ensure backward compatibility for dependent integrations. However, users are strongly encouraged to adapt custom export templates where needed as this functionality will be removed in v2.12.
|
||||
|
||||
#### Variable Scope Support for VLAN Groups ([#5284](https://github.com/netbox-community/netbox/issues/5284))
|
||||
|
||||
In previous releases, VLAN groups could be assigned only to a site. To afford more flexibility in conveying the true scope of an L2 domain, a VLAN group can now be assigned to a region, site group (new in v2.11), site, location, or rack. VLANs assigned to a group will be available only to devices and virtual machines which exist within its scope.
|
||||
|
||||
For example, a VLAN within a group assigned to a location will be available only to devices assigned to that location (or one of its child locations), or to a rack within that location.
|
||||
|
||||
#### New Site Group Model ([#5892](https://github.com/netbox-community/netbox/issues/5892))
|
||||
|
||||
This release introduces the new SiteGroup model, which can be used to organize sites similar to the existing Region model. Whereas regions are intended for geographically arranging sites into countries, states, and so on, the new site group model can be used to organize sites by functional role or other arbitrary classification. Using regions and site groups in conjunction provides two dimensions along which sites can be organized, offering greater flexibility to the user.
|
||||
|
||||
#### Improved Change Logging ([#5913](https://github.com/netbox-community/netbox/issues/5913))
|
||||
|
||||
The ObjectChange model (which is used to record the creation, modification, and deletion of NetBox objects) now explicitly records the pre-change and post-change state of each object, rather than only the post-change state. This was done to present a more clear depiction of each change being made, and to prevent the erroneous association of a previous unlogged change with its successor.
|
||||
|
||||
#### Provider Network Modeling ([#5986](https://github.com/netbox-community/netbox/issues/5986))
|
||||
|
||||
A new provider network model has been introduced to represent the boundary of a network that exists outside the scope of NetBox. Each instance of this model must be assigned to a provider, and circuits can now terminate to either provider networks or to sites. The use of this model will likely be extended by future releases to support overlay and virtual circuit modeling.
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#4833](https://github.com/netbox-community/netbox/issues/4833) - Allow assigning config contexts by device type
|
||||
* [#5344](https://github.com/netbox-community/netbox/issues/5344) - Add support for custom fields in tables
|
||||
* [#5370](https://github.com/netbox-community/netbox/issues/5370) - Extend custom field support to organizational models
|
||||
* [#5375](https://github.com/netbox-community/netbox/issues/5375) - Add `speed` attribute to console port models
|
||||
* [#5401](https://github.com/netbox-community/netbox/issues/5401) - Extend custom field support to device component models
|
||||
* [#5425](https://github.com/netbox-community/netbox/issues/5425) - Create separate tabs for VMs and devices under the cluster view
|
||||
* [#5451](https://github.com/netbox-community/netbox/issues/5451) - Add support for multiple-selection custom fields
|
||||
* [#5608](https://github.com/netbox-community/netbox/issues/5608) - Add REST API endpoint for custom links
|
||||
* [#5610](https://github.com/netbox-community/netbox/issues/5610) - Add REST API endpoint for webhooks
|
||||
* [#5830](https://github.com/netbox-community/netbox/issues/5830) - Add `as_attachment` to ExportTemplate to control download behavior
|
||||
* [#5891](https://github.com/netbox-community/netbox/issues/5891) - Add `display` field to all REST API serializers
|
||||
* [#5894](https://github.com/netbox-community/netbox/issues/5894) - Use primary keys when filtering object lists by related objects in the UI
|
||||
* [#5895](https://github.com/netbox-community/netbox/issues/5895) - Rename RackGroup to Location
|
||||
* [#5901](https://github.com/netbox-community/netbox/issues/5901) - Add `created` and `last_updated` fields to device component models
|
||||
* [#5971](https://github.com/netbox-community/netbox/issues/5971) - Add dedicated views for organizational models
|
||||
* [#5972](https://github.com/netbox-community/netbox/issues/5972) - Enable bulk editing for organizational models
|
||||
* [#5975](https://github.com/netbox-community/netbox/issues/5975) - Allow partial (decimal) vCPU allocations for virtual machines
|
||||
* [#6001](https://github.com/netbox-community/netbox/issues/6001) - Paginate component tables under device views
|
||||
* [#6038](https://github.com/netbox-community/netbox/issues/6038) - Include tagged objects list on tag view
|
||||
|
||||
### Other Changes
|
||||
|
||||
* [#1638](https://github.com/netbox-community/netbox/issues/1638) - Migrate all primary keys to 64-bit integers
|
||||
* [#5873](https://github.com/netbox-community/netbox/issues/5873) - Use numeric IDs in all object URLs
|
||||
* [#5938](https://github.com/netbox-community/netbox/issues/5938) - Deprecated support for Python 3.6
|
||||
* [#5990](https://github.com/netbox-community/netbox/issues/5990) - Deprecated `display_field` parameter for custom script ObjectVar and MultiObjectVar fields
|
||||
* [#5995](https://github.com/netbox-community/netbox/issues/5995) - Dropped backward compatibility for `queryset` parameter on ObjectVar and MultiObjectVar (use `model` instead)
|
||||
* [#6014](https://github.com/netbox-community/netbox/issues/6014) - Moved the virtual machine interfaces list to a separate view
|
||||
* [#6071](https://github.com/netbox-community/netbox/issues/6071) - Cable traces now traverse circuits
|
||||
|
||||
### REST API Changes
|
||||
|
||||
* All primary keys are now 64-bit integers
|
||||
* All model serializers now include a `display` field to be used for the presentation of an object to a human user
|
||||
* All device components
|
||||
* Added support for custom fields
|
||||
* Added `created` and `last_updated` fields to track object creation and modification
|
||||
* All device component templates
|
||||
* Added `created` and `last_updated` fields to track object creation and modification
|
||||
* All organizational models
|
||||
* Added support for custom fields
|
||||
* All cable termination models (cabled device components, power feeds, and circuit terminations)
|
||||
* Added `mark_connected` boolean field to force connection status
|
||||
* Added `_occupied` read-only boolean field as common attribute for determining whether an object is occupied
|
||||
* Renamed RackGroup to Location
|
||||
* The `/dcim/rack-groups/` endpoint is now `/dcim/locations/`
|
||||
* circuits.CircuitTermination
|
||||
* Added the `provider_network` field
|
||||
* Removed the `connected_endpoint`, `connected_endpoint_type`, and `connected_endpoint_reachable` fields
|
||||
* circuits.ProviderNetwork
|
||||
* Added the `/api/circuits/provider-networks/` endpoint
|
||||
* dcim.Device
|
||||
* Added the `location` field
|
||||
* dcim.Interface
|
||||
* Added the `parent` field
|
||||
* dcim.PowerPanel
|
||||
* Renamed `rack_group` field to `location`
|
||||
* dcim.Rack
|
||||
* Renamed `group` field to `location`
|
||||
* dcim.Site
|
||||
* Added the `group` foreign key field to SiteGroup
|
||||
* dcim.SiteGroup
|
||||
* Added the `/api/dcim/site-groups/` endpoint
|
||||
* extras.ConfigContext
|
||||
* Added the `site_groups` many-to-many field to track the assignment of ConfigContexts to SiteGroups
|
||||
* extras.CustomField
|
||||
* Added new custom field type: `multi-select`
|
||||
* extras.CustomLink
|
||||
* Added the `/api/extras/custom-links/` endpoint
|
||||
* extras.ExportTemplate
|
||||
* Added the `as_attachment` boolean field
|
||||
* extras.ObjectChange
|
||||
* Added the `prechange_data` field
|
||||
* Renamed `object_data` to `postchange_data`
|
||||
* extras.Webhook
|
||||
* Added the `/api/extras/webhooks/` endpoint
|
||||
* ipam.VLANGroup
|
||||
* Added the `scope_type`, `scope_id`, and `scope` fields (`scope` is a generic foreign key)
|
||||
* Dropped the `site` foreign key field
|
||||
* virtualization.VirtualMachine
|
||||
* `vcpus` has been changed from an integer to a decimal value
|
||||
* virtualization.VMInterface
|
||||
* Added the `parent` field
|
||||
@ -1,2 +1,2 @@
|
||||
mkdocs==1.1
|
||||
mkdocs-material
|
||||
git+https://github.com/cmacmackin/markdown-include.git
|
||||
|
||||
13
mkdocs.yml
13
mkdocs.yml
@ -5,14 +5,18 @@ python:
|
||||
install:
|
||||
- requirements: docs/requirements.txt
|
||||
theme:
|
||||
name: readthedocs
|
||||
navigation_depth: 3
|
||||
name: material
|
||||
extra_css:
|
||||
- extra.css
|
||||
markdown_extensions:
|
||||
- admonition:
|
||||
- admonition
|
||||
- markdown_include.include:
|
||||
headingOffset: 1
|
||||
- pymdownx.emoji:
|
||||
emoji_index: !!python/name:materialx.emoji.twemoji
|
||||
emoji_generator: !!python/name:materialx.emoji.to_svg
|
||||
- pymdownx.superfences
|
||||
- pymdownx.tabbed
|
||||
nav:
|
||||
- Introduction: 'index.md'
|
||||
- Installation:
|
||||
@ -49,6 +53,7 @@ nav:
|
||||
- Custom Links: 'additional-features/custom-links.md'
|
||||
- Custom Scripts: 'additional-features/custom-scripts.md'
|
||||
- Export Templates: 'additional-features/export-templates.md'
|
||||
- Journaling: 'additional-features/journaling.md'
|
||||
- NAPALM: 'additional-features/napalm.md'
|
||||
- Prometheus Metrics: 'additional-features/prometheus-metrics.md'
|
||||
- Reports: 'additional-features/reports.md'
|
||||
@ -70,11 +75,13 @@ nav:
|
||||
- Introduction: 'development/index.md'
|
||||
- Getting Started: 'development/getting-started.md'
|
||||
- Style Guide: 'development/style-guide.md'
|
||||
- Models: 'development/models.md'
|
||||
- Extending Models: 'development/extending-models.md'
|
||||
- Application Registry: 'development/application-registry.md'
|
||||
- User Preferences: 'development/user-preferences.md'
|
||||
- Release Checklist: 'development/release-checklist.md'
|
||||
- Release Notes:
|
||||
- Version 2.11: 'release-notes/version-2.11.md'
|
||||
- Version 2.10: 'release-notes/version-2.10.md'
|
||||
- Version 2.9: 'release-notes/version-2.9.md'
|
||||
- Version 2.8: 'release-notes/version-2.8.md'
|
||||
|
||||
@ -1,16 +1,29 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
from circuits.models import *
|
||||
from netbox.api import WritableNestedSerializer
|
||||
|
||||
__all__ = [
|
||||
'NestedCircuitSerializer',
|
||||
'NestedCircuitTerminationSerializer',
|
||||
'NestedCircuitTypeSerializer',
|
||||
'NestedProviderNetworkSerializer',
|
||||
'NestedProviderSerializer',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Provider networks
|
||||
#
|
||||
|
||||
class NestedProviderNetworkSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail')
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
#
|
||||
# Providers
|
||||
#
|
||||
@ -21,7 +34,7 @@ class NestedProviderSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ['id', 'url', 'name', 'slug', 'circuit_count']
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'circuit_count']
|
||||
|
||||
|
||||
#
|
||||
@ -34,7 +47,7 @@ class NestedCircuitTypeSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = ['id', 'url', 'name', 'slug', 'circuit_count']
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'circuit_count']
|
||||
|
||||
|
||||
class NestedCircuitSerializer(WritableNestedSerializer):
|
||||
@ -42,7 +55,7 @@ class NestedCircuitSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = ['id', 'url', 'cid']
|
||||
fields = ['id', 'url', 'display', 'cid']
|
||||
|
||||
|
||||
class NestedCircuitTerminationSerializer(WritableNestedSerializer):
|
||||
@ -51,4 +64,4 @@ class NestedCircuitTerminationSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = ['id', 'url', 'circuit', 'term_side', 'cable']
|
||||
fields = ['id', 'url', 'display', 'circuit', 'term_side', 'cable', '_occupied']
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from circuits.choices import CircuitStatusChoices
|
||||
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
|
||||
from dcim.api.nested_serializers import NestedCableSerializer, NestedInterfaceSerializer, NestedSiteSerializer
|
||||
from circuits.models import *
|
||||
from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
|
||||
from dcim.api.serializers import CableTerminationSerializer, ConnectedEndpointSerializer
|
||||
from extras.api.customfields import CustomFieldModelSerializer
|
||||
from extras.api.serializers import TaggedObjectSerializer
|
||||
from netbox.api import ChoiceField, ValidatedModelSerializer, WritableNestedSerializer
|
||||
from netbox.api import ChoiceField
|
||||
from netbox.api.serializers import (
|
||||
BaseModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer, WritableNestedSerializer
|
||||
)
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||
from .nested_serializers import *
|
||||
|
||||
@ -15,15 +16,31 @@ from .nested_serializers import *
|
||||
# Providers
|
||||
#
|
||||
|
||||
class ProviderSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class ProviderSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
|
||||
circuit_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = [
|
||||
'id', 'url', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'circuit_count',
|
||||
'id', 'url', 'display', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact',
|
||||
'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Provider networks
|
||||
#
|
||||
|
||||
class ProviderNetworkSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail')
|
||||
provider = NestedProviderSerializer()
|
||||
|
||||
class Meta:
|
||||
model = ProviderNetwork
|
||||
fields = [
|
||||
'id', 'url', 'display', 'provider', 'name', 'description', 'comments', 'tags', 'custom_fields', 'created',
|
||||
'last_updated',
|
||||
]
|
||||
|
||||
|
||||
@ -31,28 +48,31 @@ class ProviderSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
# Circuits
|
||||
#
|
||||
|
||||
class CircuitTypeSerializer(ValidatedModelSerializer):
|
||||
class CircuitTypeSerializer(OrganizationalModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
|
||||
circuit_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = ['id', 'url', 'name', 'slug', 'description', 'circuit_count']
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'description', 'custom_fields', 'created', 'last_updated',
|
||||
'circuit_count',
|
||||
]
|
||||
|
||||
|
||||
class CircuitCircuitTerminationSerializer(WritableNestedSerializer, ConnectedEndpointSerializer):
|
||||
class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
||||
site = NestedSiteSerializer()
|
||||
provider_network = NestedProviderNetworkSerializer()
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'id', 'url', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'connected_endpoint',
|
||||
'connected_endpoint_type', 'connected_endpoint_reachable',
|
||||
'id', 'url', 'display', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
|
||||
]
|
||||
|
||||
|
||||
class CircuitSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class CircuitSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
|
||||
provider = NestedProviderSerializer()
|
||||
status = ChoiceField(choices=CircuitStatusChoices, required=False)
|
||||
@ -64,21 +84,23 @@ class CircuitSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = [
|
||||
'id', 'url', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description',
|
||||
'termination_a', 'termination_z', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'id', 'url', 'display', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate',
|
||||
'description', 'termination_a', 'termination_z', 'comments', 'tags', 'custom_fields', 'created',
|
||||
'last_updated',
|
||||
]
|
||||
|
||||
|
||||
class CircuitTerminationSerializer(CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||
class CircuitTerminationSerializer(BaseModelSerializer, CableTerminationSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
||||
circuit = NestedCircuitSerializer()
|
||||
site = NestedSiteSerializer()
|
||||
site = NestedSiteSerializer(required=False)
|
||||
provider_network = NestedProviderNetworkSerializer(required=False)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'id', 'url', 'circuit', 'term_side', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
|
||||
'description', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
||||
'connected_endpoint_reachable'
|
||||
'id', 'url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed',
|
||||
'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type',
|
||||
'_occupied',
|
||||
]
|
||||
|
||||
@ -13,5 +13,8 @@ router.register('circuit-types', views.CircuitTypeViewSet)
|
||||
router.register('circuits', views.CircuitViewSet)
|
||||
router.register('circuit-terminations', views.CircuitTerminationViewSet)
|
||||
|
||||
# Provider networks
|
||||
router.register('provider-networks', views.ProviderNetworkViewSet)
|
||||
|
||||
app_name = 'circuits-api'
|
||||
urlpatterns = router.urls
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
from django.db.models import Prefetch
|
||||
from rest_framework.routers import APIRootView
|
||||
|
||||
from circuits import filters
|
||||
from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
|
||||
from circuits.models import *
|
||||
from dcim.api.views import PathEndpointMixin
|
||||
from extras.api.views import CustomFieldModelViewSet
|
||||
from netbox.api.views import ModelViewSet
|
||||
@ -34,7 +33,7 @@ class ProviderViewSet(CustomFieldModelViewSet):
|
||||
# Circuit Types
|
||||
#
|
||||
|
||||
class CircuitTypeViewSet(ModelViewSet):
|
||||
class CircuitTypeViewSet(CustomFieldModelViewSet):
|
||||
queryset = CircuitType.objects.annotate(
|
||||
circuit_count=count_related(Circuit, 'type')
|
||||
)
|
||||
@ -48,8 +47,7 @@ class CircuitTypeViewSet(ModelViewSet):
|
||||
|
||||
class CircuitViewSet(CustomFieldModelViewSet):
|
||||
queryset = Circuit.objects.prefetch_related(
|
||||
Prefetch('terminations', queryset=CircuitTermination.objects.prefetch_related('site')),
|
||||
'type', 'tenant', 'provider',
|
||||
'type', 'tenant', 'provider', 'termination_a', 'termination_z'
|
||||
).prefetch_related('tags')
|
||||
serializer_class = serializers.CircuitSerializer
|
||||
filterset_class = filters.CircuitFilterSet
|
||||
@ -61,8 +59,18 @@ class CircuitViewSet(CustomFieldModelViewSet):
|
||||
|
||||
class CircuitTerminationViewSet(PathEndpointMixin, ModelViewSet):
|
||||
queryset = CircuitTermination.objects.prefetch_related(
|
||||
'circuit', 'site', '_path__destination', 'cable'
|
||||
'circuit', 'site', 'provider_network', 'cable'
|
||||
)
|
||||
serializer_class = serializers.CircuitTerminationSerializer
|
||||
filterset_class = filters.CircuitTerminationFilterSet
|
||||
brief_prefetch_fields = ['circuit']
|
||||
|
||||
|
||||
#
|
||||
# Provider networks
|
||||
#
|
||||
|
||||
class ProviderNetworkViewSet(CustomFieldModelViewSet):
|
||||
queryset = ProviderNetwork.objects.prefetch_related('tags')
|
||||
serializer_class = serializers.ProviderNetworkSerializer
|
||||
filterset_class = filters.ProviderNetworkFilterSet
|
||||
|
||||
@ -2,19 +2,20 @@ import django_filters
|
||||
from django.db.models import Q
|
||||
|
||||
from dcim.filters import CableTerminationFilterSet, PathEndpointFilterSet
|
||||
from dcim.models import Region, Site
|
||||
from dcim.models import Region, Site, SiteGroup
|
||||
from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet
|
||||
from tenancy.filters import TenancyFilterSet
|
||||
from utilities.filters import (
|
||||
BaseFilterSet, NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter
|
||||
)
|
||||
from .choices import *
|
||||
from .models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
from .models import *
|
||||
|
||||
__all__ = (
|
||||
'CircuitFilterSet',
|
||||
'CircuitTerminationFilterSet',
|
||||
'CircuitTypeFilterSet',
|
||||
'ProviderNetworkFilterSet',
|
||||
'ProviderFilterSet',
|
||||
)
|
||||
|
||||
@ -37,6 +38,19 @@ class ProviderFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdated
|
||||
to_field_name='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)',
|
||||
)
|
||||
site_group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='circuits__terminations__site__group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Site group (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='circuits__terminations__site',
|
||||
queryset=Site.objects.all(),
|
||||
@ -66,6 +80,36 @@ class ProviderFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdated
|
||||
)
|
||||
|
||||
|
||||
class ProviderNetworkFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Provider.objects.all(),
|
||||
label='Provider (ID)',
|
||||
)
|
||||
provider = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider__slug',
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Provider (slug)',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = ProviderNetwork
|
||||
fields = ['id', 'name']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(description__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
).distinct()
|
||||
|
||||
|
||||
class CircuitTypeFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
|
||||
class Meta:
|
||||
@ -88,6 +132,11 @@ class CircuitFilterSet(BaseFilterSet, CustomFieldModelFilterSet, TenancyFilterSe
|
||||
to_field_name='slug',
|
||||
label='Provider (slug)',
|
||||
)
|
||||
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='terminations__provider_network',
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
label='ProviderNetwork (ID)',
|
||||
)
|
||||
type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=CircuitType.objects.all(),
|
||||
label='Circuit type (ID)',
|
||||
@ -102,17 +151,6 @@ class CircuitFilterSet(BaseFilterSet, CustomFieldModelFilterSet, TenancyFilterSe
|
||||
choices=CircuitStatusChoices,
|
||||
null_value=None
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='terminations__site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='terminations__site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='terminations__site__region',
|
||||
@ -126,6 +164,30 @@ class CircuitFilterSet(BaseFilterSet, CustomFieldModelFilterSet, TenancyFilterSe
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='terminations__site__group',
|
||||
lookup_expr='in',
|
||||
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)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='terminations__site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='terminations__site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
@ -145,7 +207,7 @@ class CircuitFilterSet(BaseFilterSet, CustomFieldModelFilterSet, TenancyFilterSe
|
||||
).distinct()
|
||||
|
||||
|
||||
class CircuitTerminationFilterSet(BaseFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
||||
class CircuitTerminationFilterSet(BaseFilterSet, CableTerminationFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@ -164,6 +226,10 @@ class CircuitTerminationFilterSet(BaseFilterSet, CableTerminationFilterSet, Path
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
label='ProviderNetwork (ID)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from dcim.models import Region, Site
|
||||
from dcim.models import Region, Site, SiteGroup
|
||||
from extras.forms import (
|
||||
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm,
|
||||
)
|
||||
@ -8,12 +9,12 @@ from extras.models import Tag
|
||||
from tenancy.forms import TenancyFilterForm, TenancyForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, CSVModelChoiceField, CSVModelForm, DatePicker,
|
||||
DynamicModelChoiceField, DynamicModelMultipleChoiceField, SmallTextarea, SlugField, StaticSelect2,
|
||||
StaticSelect2Multiple, TagFilterField,
|
||||
add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, CSVModelChoiceField, DatePicker,
|
||||
DynamicModelChoiceField, DynamicModelMultipleChoiceField, SelectSpeedWidget, SmallTextarea, SlugField,
|
||||
StaticSelect2, StaticSelect2Multiple, TagFilterField,
|
||||
)
|
||||
from .choices import CircuitStatusChoices
|
||||
from .models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
from .models import *
|
||||
|
||||
|
||||
#
|
||||
@ -33,6 +34,10 @@ class ProviderForm(BootstrapMixin, CustomFieldModelForm):
|
||||
fields = [
|
||||
'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags',
|
||||
]
|
||||
fieldsets = (
|
||||
('Provider', ('name', 'slug', 'asn', 'tags')),
|
||||
('Support Info', ('account', 'portal_url', 'noc_contact', 'admin_contact')),
|
||||
)
|
||||
widgets = {
|
||||
'noc_contact': SmallTextarea(
|
||||
attrs={'rows': 5}
|
||||
@ -101,24 +106,101 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = Provider
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label='Search'
|
||||
label=_('Search')
|
||||
)
|
||||
region = DynamicModelMultipleChoiceField(
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False
|
||||
required=False,
|
||||
label=_('Region')
|
||||
)
|
||||
site = DynamicModelMultipleChoiceField(
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
query_params={
|
||||
'region': '$region'
|
||||
}
|
||||
'region_id': '$region_id'
|
||||
},
|
||||
label=_('Site')
|
||||
)
|
||||
asn = forms.IntegerField(
|
||||
required=False,
|
||||
label='ASN'
|
||||
label=_('ASN')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
#
|
||||
# Provider networks
|
||||
#
|
||||
|
||||
class ProviderNetworkForm(BootstrapMixin, CustomFieldModelForm):
|
||||
provider = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all()
|
||||
)
|
||||
comments = CommentField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ProviderNetwork
|
||||
fields = [
|
||||
'provider', 'name', 'description', 'comments', 'tags',
|
||||
]
|
||||
fieldsets = (
|
||||
('Provider Network', ('provider', 'name', 'description', 'tags')),
|
||||
)
|
||||
|
||||
|
||||
class ProviderNetworkCSVForm(CustomFieldModelCSVForm):
|
||||
provider = CSVModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Assigned provider'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ProviderNetwork
|
||||
fields = [
|
||||
'provider', 'name', 'description', 'comments',
|
||||
]
|
||||
|
||||
|
||||
class ProviderNetworkBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
provider = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
required=False
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=100,
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = [
|
||||
'description', 'comments',
|
||||
]
|
||||
|
||||
|
||||
class ProviderNetworkFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = ProviderNetwork
|
||||
field_order = ['q', 'provider_id']
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label=_('Search')
|
||||
)
|
||||
provider_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
required=False,
|
||||
label=_('Provider')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@ -127,7 +209,7 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
# Circuit types
|
||||
#
|
||||
|
||||
class CircuitTypeForm(BootstrapMixin, forms.ModelForm):
|
||||
class CircuitTypeForm(BootstrapMixin, CustomFieldModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
@ -137,7 +219,21 @@ class CircuitTypeForm(BootstrapMixin, forms.ModelForm):
|
||||
]
|
||||
|
||||
|
||||
class CircuitTypeCSVForm(CSVModelForm):
|
||||
class CircuitTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=CircuitType.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['description']
|
||||
|
||||
|
||||
class CircuitTypeCSVForm(CustomFieldModelCSVForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
@ -171,6 +267,10 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
'cid', 'type', 'provider', 'status', 'install_date', 'commit_rate', 'description', 'tenant_group', 'tenant',
|
||||
'comments', 'tags',
|
||||
]
|
||||
fieldsets = (
|
||||
('Circuit', ('provider', 'cid', 'type', 'status', 'install_date', 'commit_rate', 'description', 'tags')),
|
||||
('Tenancy', ('tenant_group', 'tenant')),
|
||||
)
|
||||
help_texts = {
|
||||
'cid': "Unique circuit ID",
|
||||
'commit_rate': "Committed rate",
|
||||
@ -178,6 +278,7 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
widgets = {
|
||||
'status': StaticSelect2(),
|
||||
'install_date': DatePicker(),
|
||||
'commit_rate': SelectSpeedWidget(),
|
||||
}
|
||||
|
||||
|
||||
@ -256,44 +357,53 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
|
||||
class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
||||
model = Circuit
|
||||
field_order = [
|
||||
'q', 'type', 'provider', 'status', 'region', 'site', 'tenant_group', 'tenant', 'commit_rate',
|
||||
'q', 'type_id', 'provider_id', 'provider_network_id', 'status', 'region_id', 'site_id', 'tenant_group_id', 'tenant_id',
|
||||
'commit_rate',
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label='Search'
|
||||
label=_('Search')
|
||||
)
|
||||
type = DynamicModelMultipleChoiceField(
|
||||
type_id = DynamicModelMultipleChoiceField(
|
||||
queryset=CircuitType.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False
|
||||
required=False,
|
||||
label=_('Type')
|
||||
)
|
||||
provider = DynamicModelMultipleChoiceField(
|
||||
provider_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False
|
||||
required=False,
|
||||
label=_('Provider')
|
||||
)
|
||||
provider_network_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'provider_id': '$provider_id'
|
||||
},
|
||||
label=_('Provider network')
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
choices=CircuitStatusChoices,
|
||||
required=False,
|
||||
widget=StaticSelect2Multiple()
|
||||
)
|
||||
region = DynamicModelMultipleChoiceField(
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False
|
||||
required=False,
|
||||
label=_('Region')
|
||||
)
|
||||
site = DynamicModelMultipleChoiceField(
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
query_params={
|
||||
'region': '$region'
|
||||
}
|
||||
'region_id': '$region_id'
|
||||
},
|
||||
label=_('Site')
|
||||
)
|
||||
commit_rate = forms.IntegerField(
|
||||
required=False,
|
||||
min_value=0,
|
||||
label='Commit rate (Kbps)'
|
||||
label=_('Commit rate (Kbps)')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@ -310,17 +420,31 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
|
||||
'sites': '$site'
|
||||
}
|
||||
)
|
||||
site_group = DynamicModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'sites': '$site'
|
||||
}
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
query_params={
|
||||
'region_id': '$region'
|
||||
}
|
||||
'region_id': '$region',
|
||||
'group_id': '$site_group',
|
||||
},
|
||||
required=False
|
||||
)
|
||||
provider_network = DynamicModelChoiceField(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'term_side', 'region', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', 'description',
|
||||
'term_side', 'region', 'site_group', 'site', 'provider_network', 'mark_connected', 'port_speed',
|
||||
'upstream_speed', 'xconnect_id', 'pp_info', 'description',
|
||||
]
|
||||
help_texts = {
|
||||
'port_speed': "Physical circuit speed",
|
||||
@ -329,4 +453,11 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
|
||||
}
|
||||
widgets = {
|
||||
'term_side': forms.HiddenInput(),
|
||||
'port_speed': SelectSpeedWidget(),
|
||||
'upstream_speed': SelectSpeedWidget(),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['provider_network'].widget.add_query_param('provider_id', self.instance.circuit.provider_id)
|
||||
|
||||
47
netbox/circuits/migrations/0025_standardize_models.py
Normal file
47
netbox/circuits/migrations/0025_standardize_models.py
Normal file
@ -0,0 +1,47 @@
|
||||
import django.core.serializers.json
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0024_standardize_name_length'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='circuittype',
|
||||
name='custom_field_data',
|
||||
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuit',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuittermination',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuittype',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='provider',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
]
|
||||
16
netbox/circuits/migrations/0026_mark_connected.py
Normal file
16
netbox/circuits/migrations/0026_mark_connected.py
Normal file
@ -0,0 +1,16 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0025_standardize_models'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='mark_connected',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
65
netbox/circuits/migrations/0027_providernetwork.py
Normal file
65
netbox/circuits/migrations/0027_providernetwork.py
Normal file
@ -0,0 +1,65 @@
|
||||
import django.core.serializers.json
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0058_journalentry'),
|
||||
('circuits', '0026_mark_connected'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# Create the new ProviderNetwork model
|
||||
migrations.CreateModel(
|
||||
name='ProviderNetwork',
|
||||
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)),
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('description', models.CharField(blank=True, max_length=200)),
|
||||
('comments', models.TextField(blank=True)),
|
||||
('provider', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='networks', to='circuits.provider')),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('provider', 'name'),
|
||||
},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='providernetwork',
|
||||
constraint=models.UniqueConstraint(fields=('provider', 'name'), name='circuits_providernetwork_provider_name'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='providernetwork',
|
||||
unique_together={('provider', 'name')},
|
||||
),
|
||||
|
||||
# Add ProviderNetwork FK to CircuitTermination
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='provider_network',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuit_terminations', to='circuits.providernetwork'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuittermination',
|
||||
name='site',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuit_terminations', to='dcim.site'),
|
||||
),
|
||||
|
||||
# Add FKs to CircuitTermination on Circuit
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='termination_a',
|
||||
field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='circuits.circuittermination'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='termination_z',
|
||||
field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='circuits.circuittermination'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,37 @@
|
||||
import sys
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def cache_circuit_terminations(apps, schema_editor):
|
||||
Circuit = apps.get_model('circuits', 'Circuit')
|
||||
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
|
||||
|
||||
if 'test' not in sys.argv:
|
||||
print(f"\n Caching circuit terminations...", flush=True)
|
||||
|
||||
a_terminations = {
|
||||
ct.circuit_id: ct.pk for ct in CircuitTermination.objects.filter(term_side='A')
|
||||
}
|
||||
z_terminations = {
|
||||
ct.circuit_id: ct.pk for ct in CircuitTermination.objects.filter(term_side='Z')
|
||||
}
|
||||
for circuit in Circuit.objects.all():
|
||||
Circuit.objects.filter(pk=circuit.pk).update(
|
||||
termination_a_id=a_terminations.get(circuit.pk),
|
||||
termination_z_id=z_terminations.get(circuit.pk),
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0027_providernetwork'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
code=cache_circuit_terminations,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
]
|
||||
32
netbox/circuits/migrations/0029_circuit_tracing.py
Normal file
32
netbox/circuits/migrations/0029_circuit_tracing.py
Normal file
@ -0,0 +1,32 @@
|
||||
from django.db import migrations
|
||||
from django.db.models import Q
|
||||
|
||||
|
||||
def delete_obsolete_cablepaths(apps, schema_editor):
|
||||
"""
|
||||
Delete all CablePath instances which originate or terminate at a CircuitTermination.
|
||||
"""
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
|
||||
CablePath = apps.get_model('dcim', 'CablePath')
|
||||
|
||||
ct = ContentType.objects.get_for_model(CircuitTermination)
|
||||
CablePath.objects.filter(Q(origin_type=ct) | Q(destination_type=ct)).delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0028_cache_circuit_terminations'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='circuittermination',
|
||||
name='_path',
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=delete_obsolete_cablepaths,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
]
|
||||
@ -1,27 +1,27 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from dcim.fields import ASNField
|
||||
from dcim.models import CableTermination, PathEndpoint
|
||||
from extras.models import ChangeLoggedModel, CustomFieldModel, ObjectChange, TaggedItem
|
||||
from extras.models import ObjectChange
|
||||
from extras.utils import extras_features
|
||||
from netbox.models import BigIDModel, ChangeLoggedModel, OrganizationalModel, PrimaryModel
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.utils import serialize_object
|
||||
from .choices import *
|
||||
from .querysets import CircuitQuerySet
|
||||
|
||||
|
||||
__all__ = (
|
||||
'Circuit',
|
||||
'CircuitTermination',
|
||||
'CircuitType',
|
||||
'ProviderNetwork',
|
||||
'Provider',
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class Provider(ChangeLoggedModel, CustomFieldModel):
|
||||
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.
|
||||
@ -60,7 +60,6 @@ class Provider(ChangeLoggedModel, CustomFieldModel):
|
||||
comments = models.TextField(
|
||||
blank=True
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
@ -78,7 +77,7 @@ class Provider(ChangeLoggedModel, CustomFieldModel):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:provider', args=[self.slug])
|
||||
return reverse('circuits:provider', args=[self.pk])
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
@ -93,7 +92,65 @@ class Provider(ChangeLoggedModel, CustomFieldModel):
|
||||
)
|
||||
|
||||
|
||||
class CircuitType(ChangeLoggedModel):
|
||||
#
|
||||
# Provider networks
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class ProviderNetwork(PrimaryModel):
|
||||
"""
|
||||
This represents a provider network which exists outside of NetBox, the details of which are unknown or
|
||||
unimportant to the user.
|
||||
"""
|
||||
name = models.CharField(
|
||||
max_length=100
|
||||
)
|
||||
provider = models.ForeignKey(
|
||||
to='circuits.Provider',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='networks'
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
comments = models.TextField(
|
||||
blank=True
|
||||
)
|
||||
|
||||
csv_headers = [
|
||||
'provider', 'name', 'description', 'comments',
|
||||
]
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ('provider', 'name')
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('provider', 'name'),
|
||||
name='circuits_providernetwork_provider_name'
|
||||
),
|
||||
)
|
||||
unique_together = ('provider', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:providernetwork', args=[self.pk])
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.provider.name,
|
||||
self.name,
|
||||
self.description,
|
||||
self.comments,
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
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".
|
||||
@ -122,7 +179,7 @@ class CircuitType(ChangeLoggedModel):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return "{}?type={}".format(reverse('circuits:circuit_list'), self.slug)
|
||||
return reverse('circuits:circuittype', args=[self.pk])
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
@ -133,7 +190,7 @@ class CircuitType(ChangeLoggedModel):
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class Circuit(ChangeLoggedModel, CustomFieldModel):
|
||||
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
|
||||
@ -182,8 +239,25 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = CircuitQuerySet.as_manager()
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
# Cache associated CircuitTerminations
|
||||
termination_a = models.ForeignKey(
|
||||
to='circuits.CircuitTermination',
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='+',
|
||||
editable=False,
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
termination_z = models.ForeignKey(
|
||||
to='circuits.CircuitTermination',
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='+',
|
||||
editable=False,
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = [
|
||||
'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
|
||||
@ -218,22 +292,9 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
|
||||
def get_status_class(self):
|
||||
return CircuitStatusChoices.CSS_CLASSES.get(self.status)
|
||||
|
||||
def _get_termination(self, side):
|
||||
for ct in self.terminations.all():
|
||||
if ct.term_side == side:
|
||||
return ct
|
||||
return None
|
||||
|
||||
@property
|
||||
def termination_a(self):
|
||||
return self._get_termination('A')
|
||||
|
||||
@property
|
||||
def termination_z(self):
|
||||
return self._get_termination('Z')
|
||||
|
||||
|
||||
class CircuitTermination(PathEndpoint, CableTermination):
|
||||
@extras_features('webhooks')
|
||||
class CircuitTermination(ChangeLoggedModel, CableTermination):
|
||||
circuit = models.ForeignKey(
|
||||
to='circuits.Circuit',
|
||||
on_delete=models.CASCADE,
|
||||
@ -247,7 +308,16 @@ class CircuitTermination(PathEndpoint, CableTermination):
|
||||
site = models.ForeignKey(
|
||||
to='dcim.Site',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='circuit_terminations'
|
||||
related_name='circuit_terminations',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
provider_network = models.ForeignKey(
|
||||
to=ProviderNetwork,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='circuit_terminations',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
port_speed = models.PositiveIntegerField(
|
||||
verbose_name='Port speed (Kbps)',
|
||||
@ -282,26 +352,33 @@ class CircuitTermination(PathEndpoint, CableTermination):
|
||||
unique_together = ['circuit', 'term_side']
|
||||
|
||||
def __str__(self):
|
||||
return 'Side {}'.format(self.get_term_side_display())
|
||||
return f'Termination {self.term_side}: {self.site or self.provider_network}'
|
||||
|
||||
def get_absolute_url(self):
|
||||
if self.site:
|
||||
return self.site.get_absolute_url()
|
||||
return self.provider_network.get_absolute_url()
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Must define either site *or* provider network
|
||||
if self.site is None and self.provider_network is None:
|
||||
raise ValidationError("A circuit termination must attach to either a site or a provider network.")
|
||||
if self.site and self.provider_network:
|
||||
raise ValidationError("A circuit termination cannot attach to both a site and a provider network.")
|
||||
|
||||
def to_objectchange(self, action):
|
||||
# Annotate the parent Circuit
|
||||
try:
|
||||
related_object = self.circuit
|
||||
circuit = self.circuit
|
||||
except Circuit.DoesNotExist:
|
||||
# Parent circuit has been deleted
|
||||
related_object = None
|
||||
|
||||
return ObjectChange(
|
||||
changed_object=self,
|
||||
object_repr=str(self),
|
||||
action=action,
|
||||
related_object=related_object,
|
||||
object_data=serialize_object(self)
|
||||
)
|
||||
circuit = None
|
||||
return super().to_objectchange(action, related_object=circuit)
|
||||
|
||||
@property
|
||||
def parent(self):
|
||||
def parent_object(self):
|
||||
return self.circuit
|
||||
|
||||
def get_peer_termination(self):
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
from django.db.models import OuterRef, Subquery
|
||||
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
|
||||
|
||||
class CircuitQuerySet(RestrictedQuerySet):
|
||||
|
||||
def annotate_sites(self):
|
||||
"""
|
||||
Annotate the A and Z termination site names for ordering.
|
||||
"""
|
||||
from circuits.models import CircuitTermination
|
||||
_terminations = CircuitTermination.objects.filter(circuit=OuterRef('pk'))
|
||||
return self.annotate(
|
||||
a_side=Subquery(_terminations.filter(term_side='A').values('site__name')[:1]),
|
||||
z_side=Subquery(_terminations.filter(term_side='Z').values('site__name')[:1]),
|
||||
)
|
||||
@ -2,16 +2,28 @@ from django.db.models.signals import post_delete, post_save
|
||||
from django.dispatch import receiver
|
||||
from django.utils import timezone
|
||||
|
||||
from dcim.signals import rebuild_paths
|
||||
from .models import Circuit, CircuitTermination
|
||||
|
||||
|
||||
@receiver((post_save, post_delete), sender=CircuitTermination)
|
||||
@receiver(post_save, sender=CircuitTermination)
|
||||
def update_circuit(instance, **kwargs):
|
||||
"""
|
||||
When a CircuitTermination has been modified, update the last_updated time of its parent Circuit.
|
||||
When a CircuitTermination has been modified, update its parent Circuit.
|
||||
"""
|
||||
circuits = Circuit.objects.filter(pk=instance.circuit_id)
|
||||
time = timezone.now()
|
||||
for circuit in circuits:
|
||||
circuit.last_updated = time
|
||||
circuit.save()
|
||||
fields = {
|
||||
'last_updated': timezone.now(),
|
||||
f'termination_{instance.term_side.lower()}': instance.pk,
|
||||
}
|
||||
Circuit.objects.filter(pk=instance.circuit_id).update(**fields)
|
||||
|
||||
|
||||
@receiver((post_save, post_delete), sender=CircuitTermination)
|
||||
def rebuild_cablepaths(instance, raw=False, **kwargs):
|
||||
"""
|
||||
Rebuild any CablePaths which traverse the peer CircuitTermination.
|
||||
"""
|
||||
if not raw:
|
||||
peer_termination = instance.get_peer_termination()
|
||||
if peer_termination:
|
||||
rebuild_paths(peer_termination)
|
||||
|
||||
@ -1,9 +1,18 @@
|
||||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from tenancy.tables import COL_TENANT
|
||||
from tenancy.tables import TenantColumn
|
||||
from utilities.tables import BaseTable, ButtonsColumn, ChoiceFieldColumn, TagColumn, ToggleColumn
|
||||
from .models import Circuit, CircuitType, Provider
|
||||
from .models import *
|
||||
|
||||
|
||||
CIRCUITTERMINATION_LINK = """
|
||||
{% if value.site %}
|
||||
<a href="{{ value.site.get_absolute_url }}">{{ value.site }}</a>
|
||||
{% elif value.provider_network %}
|
||||
<a href="{{ value.provider_network.get_absolute_url }}">{{ value.provider_network }}</a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
|
||||
#
|
||||
@ -12,7 +21,9 @@ from .models import Circuit, CircuitType, Provider
|
||||
|
||||
class ProviderTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
circuit_count = tables.Column(
|
||||
accessor=Accessor('count_circuits'),
|
||||
verbose_name='Circuits'
|
||||
@ -29,17 +40,41 @@ class ProviderTable(BaseTable):
|
||||
default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count')
|
||||
|
||||
|
||||
#
|
||||
# Provider networks
|
||||
#
|
||||
|
||||
class ProviderNetworkTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
provider = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
tags = TagColumn(
|
||||
url_name='circuits:providernetwork_list'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ProviderNetwork
|
||||
fields = ('pk', 'name', 'provider', 'description', 'tags')
|
||||
default_columns = ('pk', 'name', 'provider', 'description')
|
||||
|
||||
|
||||
#
|
||||
# Circuit types
|
||||
#
|
||||
|
||||
class CircuitTypeTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
circuit_count = tables.Column(
|
||||
verbose_name='Circuits'
|
||||
)
|
||||
actions = ButtonsColumn(CircuitType, pk_field='slug')
|
||||
actions = ButtonsColumn(CircuitType)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = CircuitType
|
||||
@ -53,22 +88,22 @@ class CircuitTypeTable(BaseTable):
|
||||
|
||||
class CircuitTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
cid = tables.LinkColumn(
|
||||
cid = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='ID'
|
||||
)
|
||||
provider = tables.LinkColumn(
|
||||
viewname='circuits:provider',
|
||||
args=[Accessor('provider__slug')]
|
||||
provider = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
status = ChoiceFieldColumn()
|
||||
tenant = tables.TemplateColumn(
|
||||
template_code=COL_TENANT
|
||||
tenant = TenantColumn()
|
||||
termination_a = tables.TemplateColumn(
|
||||
template_code=CIRCUITTERMINATION_LINK,
|
||||
verbose_name='Side A'
|
||||
)
|
||||
a_side = tables.Column(
|
||||
verbose_name='A Side'
|
||||
)
|
||||
z_side = tables.Column(
|
||||
verbose_name='Z Side'
|
||||
termination_z = tables.TemplateColumn(
|
||||
template_code=CIRCUITTERMINATION_LINK,
|
||||
verbose_name='Side Z'
|
||||
)
|
||||
tags = TagColumn(
|
||||
url_name='circuits:circuit_list'
|
||||
@ -77,7 +112,9 @@ class CircuitTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Circuit
|
||||
fields = (
|
||||
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'a_side', 'z_side', 'install_date', 'commit_rate',
|
||||
'description', 'tags',
|
||||
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'install_date',
|
||||
'commit_rate', 'description', 'tags',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'description',
|
||||
)
|
||||
default_columns = ('pk', 'cid', 'provider', 'type', 'status', 'tenant', 'a_side', 'z_side', 'description')
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
from django.urls import reverse
|
||||
|
||||
from circuits.choices import *
|
||||
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
from circuits.models import *
|
||||
from dcim.models import Site
|
||||
from utilities.testing import APITestCase, APIViewTestCases
|
||||
|
||||
@ -17,7 +17,7 @@ class AppTest(APITestCase):
|
||||
|
||||
class ProviderTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Provider
|
||||
brief_fields = ['circuit_count', 'id', 'name', 'slug', 'url']
|
||||
brief_fields = ['circuit_count', 'display', 'id', 'name', 'slug', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'name': 'Provider 4',
|
||||
@ -49,7 +49,7 @@ class ProviderTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class CircuitTypeTest(APIViewTestCases.APIViewTestCase):
|
||||
model = CircuitType
|
||||
brief_fields = ['circuit_count', 'id', 'name', 'slug', 'url']
|
||||
brief_fields = ['circuit_count', 'display', 'id', 'name', 'slug', 'url']
|
||||
create_data = (
|
||||
{
|
||||
'name': 'Circuit Type 4',
|
||||
@ -81,7 +81,7 @@ class CircuitTypeTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class CircuitTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Circuit
|
||||
brief_fields = ['cid', 'id', 'url']
|
||||
brief_fields = ['cid', 'display', 'id', 'url']
|
||||
bulk_update_data = {
|
||||
'status': 'planned',
|
||||
}
|
||||
@ -129,7 +129,7 @@ class CircuitTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
|
||||
model = CircuitTermination
|
||||
brief_fields = ['cable', 'circuit', 'id', 'term_side', 'url']
|
||||
brief_fields = ['_occupied', 'cable', 'circuit', 'display', 'id', 'term_side', 'url']
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@ -178,3 +178,43 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
|
||||
cls.bulk_update_data = {
|
||||
'port_speed': 123456
|
||||
}
|
||||
|
||||
|
||||
class ProviderNetworkTest(APIViewTestCases.APIViewTestCase):
|
||||
model = ProviderNetwork
|
||||
brief_fields = ['display', 'id', 'name', 'url']
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
providers = (
|
||||
Provider(name='Provider 1', slug='provider-1'),
|
||||
Provider(name='Provider 2', slug='provider-2'),
|
||||
)
|
||||
Provider.objects.bulk_create(providers)
|
||||
|
||||
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)
|
||||
|
||||
cls.create_data = [
|
||||
{
|
||||
'name': 'Provider Network 4',
|
||||
'provider': providers[0].pk,
|
||||
},
|
||||
{
|
||||
'name': 'Provider Network 5',
|
||||
'provider': providers[0].pk,
|
||||
},
|
||||
{
|
||||
'name': 'Provider Network 6',
|
||||
'provider': providers[0].pk,
|
||||
},
|
||||
]
|
||||
|
||||
cls.bulk_update_data = {
|
||||
'provider': providers[1].pk,
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
@ -2,8 +2,8 @@ from django.test import TestCase
|
||||
|
||||
from circuits.choices import *
|
||||
from circuits.filters import *
|
||||
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
from dcim.models import Cable, Region, Site
|
||||
from circuits.models import *
|
||||
from dcim.models import Cable, Region, Site, SiteGroup
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
|
||||
|
||||
@ -27,13 +27,20 @@ class ProviderTestCase(TestCase):
|
||||
Region(name='Test Region 1', slug='test-region-1'),
|
||||
Region(name='Test Region 2', slug='test-region-2'),
|
||||
)
|
||||
# Can't use bulk_create for models with MPTT fields
|
||||
for r in regions:
|
||||
r.save()
|
||||
|
||||
site_groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for site_group in site_groups:
|
||||
site_group.save()
|
||||
|
||||
sites = (
|
||||
Site(name='Test Site 1', slug='test-site-1', region=regions[0]),
|
||||
Site(name='Test Site 2', slug='test-site-2', region=regions[1]),
|
||||
Site(name='Test Site 1', slug='test-site-1', region=regions[0], group=site_groups[0]),
|
||||
Site(name='Test Site 2', slug='test-site-2', region=regions[1], group=site_groups[1]),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
@ -74,13 +81,6 @@ class ProviderTestCase(TestCase):
|
||||
params = {'account': ['1234', '2345']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site': [sites[0].slug, sites[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_region(self):
|
||||
regions = Region.objects.all()[:2]
|
||||
params = {'region_id': [regions[0].pk, regions[1].pk]}
|
||||
@ -88,6 +88,20 @@ class ProviderTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site': [sites[0].slug, sites[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class CircuitTypeTestCase(TestCase):
|
||||
queryset = CircuitType.objects.all()
|
||||
@ -127,14 +141,21 @@ class CircuitTestCase(TestCase):
|
||||
Region(name='Test Region 2', slug='test-region-2'),
|
||||
Region(name='Test Region 3', slug='test-region-3'),
|
||||
)
|
||||
# Can't use bulk_create for models with MPTT fields
|
||||
for r in regions:
|
||||
r.save()
|
||||
|
||||
site_groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for site_group in site_groups:
|
||||
site_group.save()
|
||||
|
||||
sites = (
|
||||
Site(name='Test Site 1', slug='test-site-1', region=regions[0]),
|
||||
Site(name='Test Site 2', slug='test-site-2', region=regions[1]),
|
||||
Site(name='Test Site 3', slug='test-site-3', region=regions[2]),
|
||||
Site(name='Test Site 1', slug='test-site-1', region=regions[0], group=site_groups[0]),
|
||||
Site(name='Test Site 2', slug='test-site-2', region=regions[1], group=site_groups[1]),
|
||||
Site(name='Test Site 3', slug='test-site-3', region=regions[2], group=site_groups[2]),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
@ -165,6 +186,13 @@ class CircuitTestCase(TestCase):
|
||||
)
|
||||
Provider.objects.bulk_create(providers)
|
||||
|
||||
provider_networks = (
|
||||
ProviderNetwork(name='Provider Network 1', provider=providers[1]),
|
||||
ProviderNetwork(name='Provider Network 2', provider=providers[1]),
|
||||
ProviderNetwork(name='Provider Network 3', provider=providers[1]),
|
||||
)
|
||||
ProviderNetwork.objects.bulk_create(provider_networks)
|
||||
|
||||
circuits = (
|
||||
Circuit(provider=providers[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 1', install_date='2020-01-01', commit_rate=1000, status=CircuitStatusChoices.STATUS_ACTIVE),
|
||||
Circuit(provider=providers[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 2', install_date='2020-01-02', commit_rate=2000, status=CircuitStatusChoices.STATUS_ACTIVE),
|
||||
@ -179,6 +207,9 @@ class CircuitTestCase(TestCase):
|
||||
CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[1], site=sites[1], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[2], site=sites[2], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[3], provider_network=provider_networks[0], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[4], provider_network=provider_networks[1], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[5], provider_network=provider_networks[2], term_side='A'),
|
||||
))
|
||||
CircuitTermination.objects.bulk_create(circuit_terminations)
|
||||
|
||||
@ -205,6 +236,11 @@ class CircuitTestCase(TestCase):
|
||||
params = {'provider': [provider.slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_provider_network(self):
|
||||
provider_networks = ProviderNetwork.objects.all()[:2]
|
||||
params = {'provider_network_id': [provider_networks[0].pk, provider_networks[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_type(self):
|
||||
circuit_type = CircuitType.objects.first()
|
||||
params = {'type_id': [circuit_type.pk]}
|
||||
@ -223,6 +259,13 @@ class CircuitTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@ -253,14 +296,14 @@ class CircuitTerminationTestCase(TestCase):
|
||||
def setUpTestData(cls):
|
||||
|
||||
sites = (
|
||||
Site(name='Test Site 1', slug='test-site-1'),
|
||||
Site(name='Test Site 2', slug='test-site-2'),
|
||||
Site(name='Test Site 3', slug='test-site-3'),
|
||||
Site(name='Site 1', slug='site-1'),
|
||||
Site(name='Site 2', slug='site-2'),
|
||||
Site(name='Site 3', slug='site-3'),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
circuit_types = (
|
||||
CircuitType(name='Test Circuit Type 1', slug='test-circuit-type-1'),
|
||||
CircuitType(name='Circuit Type 1', slug='circuit-type-1'),
|
||||
)
|
||||
CircuitType.objects.bulk_create(circuit_types)
|
||||
|
||||
@ -269,10 +312,20 @@ class CircuitTerminationTestCase(TestCase):
|
||||
)
|
||||
Provider.objects.bulk_create(providers)
|
||||
|
||||
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)
|
||||
|
||||
circuits = (
|
||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Test Circuit 1'),
|
||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Test Circuit 2'),
|
||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Test Circuit 3'),
|
||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 1'),
|
||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 2'),
|
||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 3'),
|
||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 4'),
|
||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 5'),
|
||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 6'),
|
||||
)
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
@ -283,6 +336,9 @@ class CircuitTerminationTestCase(TestCase):
|
||||
CircuitTermination(circuit=circuits[1], site=sites[2], term_side='Z', port_speed=2000, upstream_speed=2000, xconnect_id='JKL'),
|
||||
CircuitTermination(circuit=circuits[2], site=sites[2], term_side='A', port_speed=3000, upstream_speed=3000, xconnect_id='MNO'),
|
||||
CircuitTermination(circuit=circuits[2], site=sites[0], term_side='Z', port_speed=3000, upstream_speed=3000, xconnect_id='PQR'),
|
||||
CircuitTermination(circuit=circuits[3], provider_network=provider_networks[0], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[4], provider_network=provider_networks[1], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[5], provider_network=provider_networks[2], term_side='A'),
|
||||
))
|
||||
CircuitTermination.objects.bulk_create(circuit_terminations)
|
||||
|
||||
@ -290,7 +346,7 @@ class CircuitTerminationTestCase(TestCase):
|
||||
|
||||
def test_term_side(self):
|
||||
params = {'term_side': 'A'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
|
||||
def test_port_speed(self):
|
||||
params = {'port_speed': ['1000', '2000']}
|
||||
@ -316,12 +372,48 @@ class CircuitTerminationTestCase(TestCase):
|
||||
params = {'site': [sites[0].slug, sites[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_provider_network(self):
|
||||
provider_networks = ProviderNetwork.objects.all()[:2]
|
||||
params = {'provider_network_id': [provider_networks[0].pk, provider_networks[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_cabled(self):
|
||||
params = {'cabled': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_connected(self):
|
||||
params = {'connected': True}
|
||||
|
||||
class ProviderNetworkTestCase(TestCase):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
filterset = ProviderNetworkFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
providers = (
|
||||
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)
|
||||
|
||||
provider_networks = (
|
||||
ProviderNetwork(name='Provider Network 1', provider=providers[0]),
|
||||
ProviderNetwork(name='Provider Network 2', provider=providers[1]),
|
||||
ProviderNetwork(name='Provider Network 3', provider=providers[2]),
|
||||
)
|
||||
ProviderNetwork.objects.bulk_create(provider_networks)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Provider Network 1', 'Provider Network 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_provider(self):
|
||||
providers = Provider.objects.all()[:2]
|
||||
params = {'provider_id': [providers[0].pk, providers[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'provider': [providers[0].slug, providers[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'connected': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
import datetime
|
||||
|
||||
from django.test import override_settings
|
||||
from django.urls import reverse
|
||||
|
||||
from circuits.choices import *
|
||||
from circuits.models import Circuit, CircuitType, Provider
|
||||
from utilities.testing import ViewTestCases
|
||||
from circuits.models import *
|
||||
from dcim.models import Cable, Interface, Site
|
||||
from utilities.testing import ViewTestCases, create_tags, create_test_device
|
||||
|
||||
|
||||
class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
@ -17,7 +21,7 @@ class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
Provider(name='Provider 3', slug='provider-3', asn=65003),
|
||||
])
|
||||
|
||||
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'name': 'Provider X',
|
||||
@ -73,6 +77,10 @@ class CircuitTypeTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
"Circuit Type 6,circuit-type-6",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'description': 'Foo',
|
||||
}
|
||||
|
||||
|
||||
class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = Circuit
|
||||
@ -98,7 +106,7 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
Circuit(cid='Circuit 3', provider=providers[0], type=circuittypes[0]),
|
||||
])
|
||||
|
||||
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'cid': 'Circuit X',
|
||||
@ -129,3 +137,99 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'description': 'New description',
|
||||
'comments': 'New comments',
|
||||
}
|
||||
|
||||
|
||||
class ProviderNetworkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = ProviderNetwork
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
providers = (
|
||||
Provider(name='Provider 1', slug='provider-1'),
|
||||
Provider(name='Provider 2', slug='provider-2'),
|
||||
)
|
||||
Provider.objects.bulk_create(providers)
|
||||
|
||||
ProviderNetwork.objects.bulk_create([
|
||||
ProviderNetwork(name='Provider Network 1', provider=providers[0]),
|
||||
ProviderNetwork(name='Provider Network 2', provider=providers[0]),
|
||||
ProviderNetwork(name='Provider Network 3', provider=providers[0]),
|
||||
])
|
||||
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'name': 'Provider Network X',
|
||||
'provider': providers[1].pk,
|
||||
'description': 'A new provider network',
|
||||
'comments': 'Longer description goes here',
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"name,provider,description",
|
||||
"Provider Network 4,Provider 1,Foo",
|
||||
"Provider Network 5,Provider 1,Bar",
|
||||
"Provider Network 6,Provider 1,Baz",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'provider': providers[1].pk,
|
||||
'description': 'New description',
|
||||
'comments': 'New comments',
|
||||
}
|
||||
|
||||
|
||||
class CircuitTerminationTestCase(
|
||||
ViewTestCases.EditObjectViewTestCase,
|
||||
ViewTestCases.DeleteObjectViewTestCase,
|
||||
):
|
||||
model = CircuitTermination
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1'),
|
||||
Site(name='Site 2', slug='site-2'),
|
||||
Site(name='Site 3', slug='site-3'),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
|
||||
|
||||
circuits = (
|
||||
Circuit(cid='Circuit 1', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 2', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 3', provider=provider, type=circuittype),
|
||||
)
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
circuit_terminations = (
|
||||
CircuitTermination(circuit=circuits[0], term_side='A', site=sites[0]),
|
||||
CircuitTermination(circuit=circuits[0], term_side='Z', site=sites[1]),
|
||||
CircuitTermination(circuit=circuits[1], term_side='A', site=sites[0]),
|
||||
CircuitTermination(circuit=circuits[1], term_side='Z', site=sites[1]),
|
||||
)
|
||||
CircuitTermination.objects.bulk_create(circuit_terminations)
|
||||
|
||||
cls.form_data = {
|
||||
'term_side': 'A',
|
||||
'site': sites[2].pk,
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||
def test_trace(self):
|
||||
device = create_test_device('Device 1')
|
||||
|
||||
circuittermination = CircuitTermination.objects.first()
|
||||
interface = Interface.objects.create(
|
||||
device=device,
|
||||
name='Interface 1'
|
||||
)
|
||||
Cable(termination_a=circuittermination, termination_b=interface).save()
|
||||
|
||||
response = self.client.get(reverse('circuits:circuittermination_trace', kwargs={'pk': circuittermination.pk}))
|
||||
self.assertHttpStatus(response, 200)
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
from django.urls import path
|
||||
|
||||
from dcim.views import CableCreateView, PathTraceView
|
||||
from extras.views import ObjectChangeLogView
|
||||
from extras.views import ObjectChangeLogView, ObjectJournalView
|
||||
from utilities.views import SlugRedirectView
|
||||
from . import views
|
||||
from .models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
from .models import *
|
||||
|
||||
app_name = 'circuits'
|
||||
urlpatterns = [
|
||||
@ -14,19 +15,35 @@ 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/<slug:slug>/', views.ProviderView.as_view(), name='provider'),
|
||||
path('providers/<slug:slug>/edit/', views.ProviderEditView.as_view(), name='provider_edit'),
|
||||
path('providers/<slug:slug>/delete/', views.ProviderDeleteView.as_view(), name='provider_delete'),
|
||||
path('providers/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}),
|
||||
path('providers/<int:pk>/', views.ProviderView.as_view(), name='provider'),
|
||||
path('providers/<slug:slug>/', SlugRedirectView.as_view(), kwargs={'model': 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}),
|
||||
|
||||
# Provider networks
|
||||
path('provider-networks/', views.ProviderNetworkListView.as_view(), name='providernetwork_list'),
|
||||
path('provider-networks/add/', views.ProviderNetworkEditView.as_view(), name='providernetwork_add'),
|
||||
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}),
|
||||
|
||||
# Circuit types
|
||||
path('circuit-types/', views.CircuitTypeListView.as_view(), name='circuittype_list'),
|
||||
path('circuit-types/add/', views.CircuitTypeEditView.as_view(), name='circuittype_add'),
|
||||
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/<slug:slug>/edit/', views.CircuitTypeEditView.as_view(), name='circuittype_edit'),
|
||||
path('circuit-types/<slug:slug>/delete/', views.CircuitTypeDeleteView.as_view(), name='circuittype_delete'),
|
||||
path('circuit-types/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}),
|
||||
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}),
|
||||
|
||||
# Circuits
|
||||
path('circuits/', views.CircuitListView.as_view(), name='circuit_list'),
|
||||
@ -38,6 +55,7 @@ urlpatterns = [
|
||||
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'),
|
||||
|
||||
# Circuit terminations
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
from django.contrib import messages
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django_tables2 import RequestConfig
|
||||
|
||||
from netbox.views import generic
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
||||
from utilities.tables import paginate_table
|
||||
from utilities.utils import count_related
|
||||
from . import filters, forms, tables
|
||||
from .choices import CircuitTerminationSideChoices
|
||||
from .models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
from .models import *
|
||||
|
||||
|
||||
#
|
||||
@ -33,16 +33,11 @@ class ProviderView(generic.ObjectView):
|
||||
provider=instance
|
||||
).prefetch_related(
|
||||
'type', 'tenant', 'terminations__site'
|
||||
).annotate_sites()
|
||||
)
|
||||
|
||||
circuits_table = tables.CircuitTable(circuits)
|
||||
circuits_table.columns.hide('provider')
|
||||
|
||||
paginate = {
|
||||
'paginator_class': EnhancedPaginator,
|
||||
'per_page': get_paginate_count(request)
|
||||
}
|
||||
RequestConfig(request, paginate).configure(circuits_table)
|
||||
paginate_table(circuits_table, request)
|
||||
|
||||
return {
|
||||
'circuits_table': circuits_table,
|
||||
@ -52,7 +47,6 @@ class ProviderView(generic.ObjectView):
|
||||
class ProviderEditView(generic.ObjectEditView):
|
||||
queryset = Provider.objects.all()
|
||||
model_form = forms.ProviderForm
|
||||
template_name = 'circuits/provider_edit.html'
|
||||
|
||||
|
||||
class ProviderDeleteView(generic.ObjectDeleteView):
|
||||
@ -82,6 +76,66 @@ class ProviderBulkDeleteView(generic.BulkDeleteView):
|
||||
table = tables.ProviderTable
|
||||
|
||||
|
||||
#
|
||||
# Provider networks
|
||||
#
|
||||
|
||||
class ProviderNetworkListView(generic.ObjectListView):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
filterset = filters.ProviderNetworkFilterSet
|
||||
filterset_form = forms.ProviderNetworkFilterForm
|
||||
table = tables.ProviderNetworkTable
|
||||
|
||||
|
||||
class ProviderNetworkView(generic.ObjectView):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
circuits = Circuit.objects.restrict(request.user, 'view').filter(
|
||||
Q(termination_a__provider_network=instance.pk) |
|
||||
Q(termination_z__provider_network=instance.pk)
|
||||
).prefetch_related(
|
||||
'type', 'tenant', 'terminations__site'
|
||||
)
|
||||
|
||||
circuits_table = tables.CircuitTable(circuits)
|
||||
circuits_table.columns.hide('termination_a')
|
||||
circuits_table.columns.hide('termination_z')
|
||||
paginate_table(circuits_table, request)
|
||||
|
||||
return {
|
||||
'circuits_table': circuits_table,
|
||||
}
|
||||
|
||||
|
||||
class ProviderNetworkEditView(generic.ObjectEditView):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
model_form = forms.ProviderNetworkForm
|
||||
|
||||
|
||||
class ProviderNetworkDeleteView(generic.ObjectDeleteView):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
|
||||
|
||||
class ProviderNetworkBulkImportView(generic.BulkImportView):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
model_form = forms.ProviderNetworkCSVForm
|
||||
table = tables.ProviderNetworkTable
|
||||
|
||||
|
||||
class ProviderNetworkBulkEditView(generic.BulkEditView):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
filterset = filters.ProviderNetworkFilterSet
|
||||
table = tables.ProviderNetworkTable
|
||||
form = forms.ProviderNetworkBulkEditForm
|
||||
|
||||
|
||||
class ProviderNetworkBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
filterset = filters.ProviderNetworkFilterSet
|
||||
table = tables.ProviderNetworkTable
|
||||
|
||||
|
||||
#
|
||||
# Circuit Types
|
||||
#
|
||||
@ -93,6 +147,23 @@ class CircuitTypeListView(generic.ObjectListView):
|
||||
table = tables.CircuitTypeTable
|
||||
|
||||
|
||||
class CircuitTypeView(generic.ObjectView):
|
||||
queryset = CircuitType.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
circuits = Circuit.objects.restrict(request.user, 'view').filter(
|
||||
type=instance
|
||||
)
|
||||
|
||||
circuits_table = tables.CircuitTable(circuits)
|
||||
circuits_table.columns.hide('type')
|
||||
paginate_table(circuits_table, request)
|
||||
|
||||
return {
|
||||
'circuits_table': circuits_table,
|
||||
}
|
||||
|
||||
|
||||
class CircuitTypeEditView(generic.ObjectEditView):
|
||||
queryset = CircuitType.objects.all()
|
||||
model_form = forms.CircuitTypeForm
|
||||
@ -108,6 +179,15 @@ class CircuitTypeBulkImportView(generic.BulkImportView):
|
||||
table = tables.CircuitTypeTable
|
||||
|
||||
|
||||
class CircuitTypeBulkEditView(generic.BulkEditView):
|
||||
queryset = CircuitType.objects.annotate(
|
||||
circuit_count=count_related(Circuit, 'type')
|
||||
)
|
||||
filterset = filters.CircuitTypeFilterSet
|
||||
table = tables.CircuitTypeTable
|
||||
form = forms.CircuitTypeBulkEditForm
|
||||
|
||||
|
||||
class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = CircuitType.objects.annotate(
|
||||
circuit_count=count_related(Circuit, 'type')
|
||||
@ -121,8 +201,8 @@ class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class CircuitListView(generic.ObjectListView):
|
||||
queryset = Circuit.objects.prefetch_related(
|
||||
'provider', 'type', 'tenant', 'terminations'
|
||||
).annotate_sites()
|
||||
'provider', 'type', 'tenant', 'termination_a', 'termination_z'
|
||||
)
|
||||
filterset = filters.CircuitFilterSet
|
||||
filterset_form = forms.CircuitFilterForm
|
||||
table = tables.CircuitTable
|
||||
@ -139,8 +219,6 @@ class CircuitView(generic.ObjectView):
|
||||
).filter(
|
||||
circuit=instance, term_side=CircuitTerminationSideChoices.SIDE_A
|
||||
).first()
|
||||
if termination_a and termination_a.connected_endpoint and hasattr(termination_a.connected_endpoint, 'ip_addresses'):
|
||||
termination_a.ip_addresses = termination_a.connected_endpoint.ip_addresses.restrict(request.user, 'view')
|
||||
|
||||
# Z-side termination
|
||||
termination_z = CircuitTermination.objects.restrict(request.user, 'view').prefetch_related(
|
||||
@ -148,8 +226,6 @@ class CircuitView(generic.ObjectView):
|
||||
).filter(
|
||||
circuit=instance, term_side=CircuitTerminationSideChoices.SIDE_Z
|
||||
).first()
|
||||
if termination_z and termination_z.connected_endpoint and hasattr(termination_z.connected_endpoint, 'ip_addresses'):
|
||||
termination_z.ip_addresses = termination_z.connected_endpoint.ip_addresses.restrict(request.user, 'view')
|
||||
|
||||
return {
|
||||
'termination_a': termination_a,
|
||||
@ -160,7 +236,6 @@ class CircuitView(generic.ObjectView):
|
||||
class CircuitEditView(generic.ObjectEditView):
|
||||
queryset = Circuit.objects.all()
|
||||
model_form = forms.CircuitForm
|
||||
template_name = 'circuits/circuit_edit.html'
|
||||
|
||||
|
||||
class CircuitDeleteView(generic.ObjectDeleteView):
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from dcim import models
|
||||
from netbox.api import WritableNestedSerializer
|
||||
from netbox.api.serializers import BaseModelSerializer, WritableNestedSerializer
|
||||
|
||||
__all__ = [
|
||||
'NestedCableSerializer',
|
||||
@ -27,7 +27,7 @@ __all__ = [
|
||||
'NestedPowerPanelSerializer',
|
||||
'NestedPowerPortSerializer',
|
||||
'NestedPowerPortTemplateSerializer',
|
||||
'NestedRackGroupSerializer',
|
||||
'NestedLocationSerializer',
|
||||
'NestedRackReservationSerializer',
|
||||
'NestedRackRoleSerializer',
|
||||
'NestedRackSerializer',
|
||||
@ -35,6 +35,7 @@ __all__ = [
|
||||
'NestedRearPortTemplateSerializer',
|
||||
'NestedRegionSerializer',
|
||||
'NestedSiteSerializer',
|
||||
'NestedSiteGroupSerializer',
|
||||
'NestedVirtualChassisSerializer',
|
||||
]
|
||||
|
||||
@ -50,7 +51,17 @@ class NestedRegionSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.Region
|
||||
fields = ['id', 'url', 'name', 'slug', 'site_count', '_depth']
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'site_count', '_depth']
|
||||
|
||||
|
||||
class NestedSiteGroupSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail')
|
||||
site_count = serializers.IntegerField(read_only=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.SiteGroup
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'site_count', '_depth']
|
||||
|
||||
|
||||
class NestedSiteSerializer(WritableNestedSerializer):
|
||||
@ -58,21 +69,21 @@ class NestedSiteSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.Site
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
fields = ['id', 'url', 'display', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Racks
|
||||
#
|
||||
|
||||
class NestedRackGroupSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackgroup-detail')
|
||||
class NestedLocationSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail')
|
||||
rack_count = serializers.IntegerField(read_only=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.RackGroup
|
||||
fields = ['id', 'url', 'name', 'slug', 'rack_count', '_depth']
|
||||
model = models.Location
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'rack_count', '_depth']
|
||||
|
||||
|
||||
class NestedRackRoleSerializer(WritableNestedSerializer):
|
||||
@ -81,7 +92,7 @@ class NestedRackRoleSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.RackRole
|
||||
fields = ['id', 'url', 'name', 'slug', 'rack_count']
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'rack_count']
|
||||
|
||||
|
||||
class NestedRackSerializer(WritableNestedSerializer):
|
||||
@ -90,7 +101,7 @@ class NestedRackSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.Rack
|
||||
fields = ['id', 'url', 'name', 'display_name', 'device_count']
|
||||
fields = ['id', 'url', 'display', 'name', 'display_name', 'device_count']
|
||||
|
||||
|
||||
class NestedRackReservationSerializer(WritableNestedSerializer):
|
||||
@ -99,7 +110,7 @@ class NestedRackReservationSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.RackReservation
|
||||
fields = ['id', 'url', 'user', 'units']
|
||||
fields = ['id', 'url', 'display', 'user', 'units']
|
||||
|
||||
def get_user(self, obj):
|
||||
return obj.user.username
|
||||
@ -115,7 +126,7 @@ class NestedManufacturerSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.Manufacturer
|
||||
fields = ['id', 'url', 'name', 'slug', 'devicetype_count']
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'devicetype_count']
|
||||
|
||||
|
||||
class NestedDeviceTypeSerializer(WritableNestedSerializer):
|
||||
@ -125,7 +136,7 @@ class NestedDeviceTypeSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.DeviceType
|
||||
fields = ['id', 'url', 'manufacturer', 'model', 'slug', 'display_name', 'device_count']
|
||||
fields = ['id', 'url', 'display', 'manufacturer', 'model', 'slug', 'display_name', 'device_count']
|
||||
|
||||
|
||||
class NestedConsolePortTemplateSerializer(WritableNestedSerializer):
|
||||
@ -133,7 +144,7 @@ class NestedConsolePortTemplateSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.ConsolePortTemplate
|
||||
fields = ['id', 'url', 'name']
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedConsoleServerPortTemplateSerializer(WritableNestedSerializer):
|
||||
@ -141,7 +152,7 @@ class NestedConsoleServerPortTemplateSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.ConsoleServerPortTemplate
|
||||
fields = ['id', 'url', 'name']
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedPowerPortTemplateSerializer(WritableNestedSerializer):
|
||||
@ -149,7 +160,7 @@ class NestedPowerPortTemplateSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.PowerPortTemplate
|
||||
fields = ['id', 'url', 'name']
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedPowerOutletTemplateSerializer(WritableNestedSerializer):
|
||||
@ -157,7 +168,7 @@ class NestedPowerOutletTemplateSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.PowerOutletTemplate
|
||||
fields = ['id', 'url', 'name']
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedInterfaceTemplateSerializer(WritableNestedSerializer):
|
||||
@ -165,7 +176,7 @@ class NestedInterfaceTemplateSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.InterfaceTemplate
|
||||
fields = ['id', 'url', 'name']
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedRearPortTemplateSerializer(WritableNestedSerializer):
|
||||
@ -173,7 +184,7 @@ class NestedRearPortTemplateSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.RearPortTemplate
|
||||
fields = ['id', 'url', 'name']
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedFrontPortTemplateSerializer(WritableNestedSerializer):
|
||||
@ -181,7 +192,7 @@ class NestedFrontPortTemplateSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.FrontPortTemplate
|
||||
fields = ['id', 'url', 'name']
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedDeviceBayTemplateSerializer(WritableNestedSerializer):
|
||||
@ -189,7 +200,7 @@ class NestedDeviceBayTemplateSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.DeviceBayTemplate
|
||||
fields = ['id', 'url', 'name']
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
#
|
||||
@ -203,7 +214,7 @@ class NestedDeviceRoleSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.DeviceRole
|
||||
fields = ['id', 'url', 'name', 'slug', 'device_count', 'virtualmachine_count']
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'device_count', 'virtualmachine_count']
|
||||
|
||||
|
||||
class NestedPlatformSerializer(WritableNestedSerializer):
|
||||
@ -213,7 +224,7 @@ class NestedPlatformSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.Platform
|
||||
fields = ['id', 'url', 'name', 'slug', 'device_count', 'virtualmachine_count']
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'device_count', 'virtualmachine_count']
|
||||
|
||||
|
||||
class NestedDeviceSerializer(WritableNestedSerializer):
|
||||
@ -221,7 +232,7 @@ class NestedDeviceSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.Device
|
||||
fields = ['id', 'url', 'name', 'display_name']
|
||||
fields = ['id', 'url', 'display', 'name', 'display_name']
|
||||
|
||||
|
||||
class NestedConsoleServerPortSerializer(WritableNestedSerializer):
|
||||
@ -230,7 +241,7 @@ class NestedConsoleServerPortSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.ConsoleServerPort
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
|
||||
|
||||
class NestedConsolePortSerializer(WritableNestedSerializer):
|
||||
@ -239,7 +250,7 @@ class NestedConsolePortSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.ConsolePort
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
|
||||
|
||||
class NestedPowerOutletSerializer(WritableNestedSerializer):
|
||||
@ -248,7 +259,7 @@ class NestedPowerOutletSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.PowerOutlet
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
|
||||
|
||||
class NestedPowerPortSerializer(WritableNestedSerializer):
|
||||
@ -257,7 +268,7 @@ class NestedPowerPortSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.PowerPort
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
|
||||
|
||||
class NestedInterfaceSerializer(WritableNestedSerializer):
|
||||
@ -266,7 +277,7 @@ class NestedInterfaceSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.Interface
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
|
||||
|
||||
class NestedRearPortSerializer(WritableNestedSerializer):
|
||||
@ -275,7 +286,7 @@ class NestedRearPortSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.RearPort
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
|
||||
|
||||
class NestedFrontPortSerializer(WritableNestedSerializer):
|
||||
@ -284,7 +295,7 @@ class NestedFrontPortSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.FrontPort
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
|
||||
|
||||
class NestedDeviceBaySerializer(WritableNestedSerializer):
|
||||
@ -293,7 +304,7 @@ class NestedDeviceBaySerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.DeviceBay
|
||||
fields = ['id', 'url', 'device', 'name']
|
||||
fields = ['id', 'url', 'display', 'device', 'name']
|
||||
|
||||
|
||||
class NestedInventoryItemSerializer(WritableNestedSerializer):
|
||||
@ -303,19 +314,19 @@ class NestedInventoryItemSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.InventoryItem
|
||||
fields = ['id', 'url', 'device', 'name', '_depth']
|
||||
fields = ['id', 'url', 'display', 'device', 'name', '_depth']
|
||||
|
||||
|
||||
#
|
||||
# Cables
|
||||
#
|
||||
|
||||
class NestedCableSerializer(serializers.ModelSerializer):
|
||||
class NestedCableSerializer(BaseModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.Cable
|
||||
fields = ['id', 'url', 'label']
|
||||
fields = ['id', 'url', 'display', 'label']
|
||||
|
||||
|
||||
#
|
||||
@ -342,7 +353,7 @@ class NestedPowerPanelSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.PowerPanel
|
||||
fields = ['id', 'url', 'name', 'powerfeed_count']
|
||||
fields = ['id', 'url', 'display', 'name', 'powerfeed_count']
|
||||
|
||||
|
||||
class NestedPowerFeedSerializer(WritableNestedSerializer):
|
||||
@ -350,4 +361,4 @@ class NestedPowerFeedSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.PowerFeed
|
||||
fields = ['id', 'url', 'name', 'cable']
|
||||
fields = ['id', 'url', 'display', 'name', 'cable', '_occupied']
|
||||
|
||||
@ -7,19 +7,12 @@ from timezone_field.rest_framework import TimeZoneSerializerField
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import (
|
||||
Cable, CablePath, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceType, DeviceRole, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
|
||||
Manufacturer, InventoryItem, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
|
||||
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
|
||||
VirtualChassis,
|
||||
)
|
||||
from extras.api.customfields import CustomFieldModelSerializer
|
||||
from extras.api.serializers import TaggedObjectSerializer
|
||||
from dcim.models import *
|
||||
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
|
||||
from ipam.models import VLAN
|
||||
from netbox.api import (
|
||||
ChoiceField, ContentTypeField, SerializedPKRelatedField, ValidatedModelSerializer,
|
||||
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
||||
from netbox.api.serializers import (
|
||||
NestedGroupModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer,
|
||||
WritableNestedSerializer,
|
||||
)
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||
@ -50,7 +43,7 @@ class CableTerminationSerializer(serializers.ModelSerializer):
|
||||
return None
|
||||
|
||||
|
||||
class ConnectedEndpointSerializer(ValidatedModelSerializer):
|
||||
class ConnectedEndpointSerializer(serializers.ModelSerializer):
|
||||
connected_endpoint_type = serializers.SerializerMethodField(read_only=True)
|
||||
connected_endpoint = serializers.SerializerMethodField(read_only=True)
|
||||
connected_endpoint_reachable = serializers.SerializerMethodField(read_only=True)
|
||||
@ -82,21 +75,37 @@ class ConnectedEndpointSerializer(ValidatedModelSerializer):
|
||||
# Regions/sites
|
||||
#
|
||||
|
||||
class RegionSerializer(serializers.ModelSerializer):
|
||||
class RegionSerializer(NestedGroupModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
|
||||
parent = NestedRegionSerializer(required=False, allow_null=True)
|
||||
site_count = serializers.IntegerField(read_only=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Region
|
||||
fields = ['id', 'url', 'name', 'slug', 'parent', 'description', 'site_count', '_depth']
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'custom_fields', 'created', 'last_updated',
|
||||
'site_count', '_depth',
|
||||
]
|
||||
|
||||
|
||||
class SiteSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class SiteGroupSerializer(NestedGroupModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail')
|
||||
parent = NestedRegionSerializer(required=False, allow_null=True)
|
||||
site_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = SiteGroup
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'custom_fields', 'created', 'last_updated',
|
||||
'site_count', '_depth',
|
||||
]
|
||||
|
||||
|
||||
class SiteSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
|
||||
status = ChoiceField(choices=SiteStatusChoices, required=False)
|
||||
region = NestedRegionSerializer(required=False, allow_null=True)
|
||||
group = NestedSiteGroupSerializer(required=False, allow_null=True)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
time_zone = TimeZoneSerializerField(required=False)
|
||||
circuit_count = serializers.IntegerField(read_only=True)
|
||||
@ -109,10 +118,10 @@ class SiteSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = [
|
||||
'id', 'url', 'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description',
|
||||
'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone',
|
||||
'contact_email', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count',
|
||||
'device_count', 'prefix_count', 'rack_count', 'virtualmachine_count', 'vlan_count',
|
||||
'id', 'url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'asn',
|
||||
'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name',
|
||||
'contact_phone', 'contact_email', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'circuit_count', 'device_count', 'prefix_count', 'rack_count', 'virtualmachine_count', 'vlan_count',
|
||||
]
|
||||
|
||||
|
||||
@ -120,31 +129,37 @@ class SiteSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
# Racks
|
||||
#
|
||||
|
||||
class RackGroupSerializer(ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackgroup-detail')
|
||||
class LocationSerializer(NestedGroupModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail')
|
||||
site = NestedSiteSerializer()
|
||||
parent = NestedRackGroupSerializer(required=False, allow_null=True)
|
||||
parent = NestedLocationSerializer(required=False, allow_null=True)
|
||||
rack_count = serializers.IntegerField(read_only=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
device_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = RackGroup
|
||||
fields = ['id', 'url', 'name', 'slug', 'site', 'parent', 'description', 'rack_count', '_depth']
|
||||
model = Location
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'description', 'custom_fields', 'created',
|
||||
'last_updated', 'rack_count', 'device_count', '_depth',
|
||||
]
|
||||
|
||||
|
||||
class RackRoleSerializer(ValidatedModelSerializer):
|
||||
class RackRoleSerializer(OrganizationalModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
|
||||
rack_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = RackRole
|
||||
fields = ['id', 'url', 'name', 'slug', 'color', 'description', 'rack_count']
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'custom_fields', 'created', 'last_updated',
|
||||
'rack_count',
|
||||
]
|
||||
|
||||
|
||||
class RackSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class RackSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
|
||||
site = NestedSiteSerializer()
|
||||
group = NestedRackGroupSerializer(required=False, allow_null=True, default=None)
|
||||
location = NestedLocationSerializer(required=False, allow_null=True, default=None)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
status = ChoiceField(choices=RackStatusChoices, required=False)
|
||||
role = NestedRackRoleSerializer(required=False, allow_null=True)
|
||||
@ -157,21 +172,22 @@ class RackSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = [
|
||||
'id', 'url', 'name', 'facility_id', 'display_name', 'site', 'group', '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',
|
||||
'id', 'url', 'display', 'name', 'facility_id', 'display_name', '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',
|
||||
]
|
||||
# Omit the UniqueTogetherValidator that would be automatically added to validate (group, facility_id). This
|
||||
# Omit the UniqueTogetherValidator that would be automatically added to validate (location, facility_id). This
|
||||
# prevents facility_id from being interpreted as a required field.
|
||||
validators = [
|
||||
UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('group', 'name'))
|
||||
UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('location', 'name'))
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
|
||||
# Validate uniqueness of (group, facility_id) since we omitted the automatically-created validator from Meta.
|
||||
# Validate uniqueness of (location, facility_id) since we omitted the automatically-created validator from Meta.
|
||||
if data.get('facility_id', None):
|
||||
validator = UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('group', 'facility_id'))
|
||||
validator = UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('location', 'facility_id'))
|
||||
validator(data, self)
|
||||
|
||||
# Enforce model validation
|
||||
@ -191,7 +207,7 @@ class RackUnitSerializer(serializers.Serializer):
|
||||
occupied = serializers.BooleanField(read_only=True)
|
||||
|
||||
|
||||
class RackReservationSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class RackReservationSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail')
|
||||
rack = NestedRackSerializer()
|
||||
user = NestedUserSerializer()
|
||||
@ -199,7 +215,10 @@ class RackReservationSerializer(TaggedObjectSerializer, CustomFieldModelSerializ
|
||||
|
||||
class Meta:
|
||||
model = RackReservation
|
||||
fields = ['id', 'url', 'rack', 'units', 'created', 'user', 'tenant', 'description', 'tags', 'custom_fields']
|
||||
fields = [
|
||||
'id', 'url', 'display', 'rack', 'units', 'created', 'user', 'tenant', 'description', 'tags',
|
||||
'custom_fields',
|
||||
]
|
||||
|
||||
|
||||
class RackElevationDetailFilterSerializer(serializers.Serializer):
|
||||
@ -242,7 +261,7 @@ class RackElevationDetailFilterSerializer(serializers.Serializer):
|
||||
# Device types
|
||||
#
|
||||
|
||||
class ManufacturerSerializer(ValidatedModelSerializer):
|
||||
class ManufacturerSerializer(OrganizationalModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
|
||||
devicetype_count = serializers.IntegerField(read_only=True)
|
||||
inventoryitem_count = serializers.IntegerField(read_only=True)
|
||||
@ -251,11 +270,12 @@ class ManufacturerSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = Manufacturer
|
||||
fields = [
|
||||
'id', 'url', 'name', 'slug', 'description', 'devicetype_count', 'inventoryitem_count', 'platform_count',
|
||||
'id', 'url', 'display', 'name', 'slug', 'description', 'custom_fields', 'created', 'last_updated',
|
||||
'devicetype_count', 'inventoryitem_count', 'platform_count',
|
||||
]
|
||||
|
||||
|
||||
class DeviceTypeSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class DeviceTypeSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
|
||||
manufacturer = NestedManufacturerSerializer()
|
||||
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False)
|
||||
@ -264,9 +284,9 @@ class DeviceTypeSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = [
|
||||
'id', 'url', 'manufacturer', 'model', 'slug', 'display_name', 'part_number', 'u_height', 'is_full_depth',
|
||||
'subdevice_role', 'front_image', 'rear_image', 'comments', 'tags', 'custom_fields', 'created',
|
||||
'last_updated', 'device_count',
|
||||
'id', 'url', 'display', 'manufacturer', 'model', 'slug', 'display_name', 'part_number', 'u_height',
|
||||
'is_full_depth', 'subdevice_role', 'front_image', 'rear_image', 'comments', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', 'device_count',
|
||||
]
|
||||
|
||||
|
||||
@ -281,7 +301,9 @@ class ConsolePortTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ConsolePortTemplate
|
||||
fields = ['id', 'url', 'device_type', 'name', 'label', 'type', 'description']
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'description', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
@ -295,7 +317,9 @@ class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = ['id', 'url', 'device_type', 'name', 'label', 'type', 'description']
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'description', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
class PowerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
@ -309,7 +333,10 @@ class PowerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = PowerPortTemplate
|
||||
fields = ['id', 'url', 'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description']
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw',
|
||||
'description', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
||||
@ -331,7 +358,10 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = PowerOutletTemplate
|
||||
fields = ['id', 'url', 'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description']
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
|
||||
'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
class InterfaceTemplateSerializer(ValidatedModelSerializer):
|
||||
@ -341,7 +371,10 @@ class InterfaceTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = InterfaceTemplate
|
||||
fields = ['id', 'url', 'device_type', 'name', 'label', 'type', 'mgmt_only', 'description']
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'mgmt_only', 'description', 'created',
|
||||
'last_updated',
|
||||
]
|
||||
|
||||
|
||||
class RearPortTemplateSerializer(ValidatedModelSerializer):
|
||||
@ -351,7 +384,10 @@ class RearPortTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = RearPortTemplate
|
||||
fields = ['id', 'url', 'device_type', 'name', 'label', 'type', 'positions', 'description']
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'positions', 'description', 'created',
|
||||
'last_updated',
|
||||
]
|
||||
|
||||
|
||||
class FrontPortTemplateSerializer(ValidatedModelSerializer):
|
||||
@ -362,7 +398,10 @@ class FrontPortTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = FrontPortTemplate
|
||||
fields = ['id', 'url', 'device_type', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description']
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'rear_port', 'rear_port_position',
|
||||
'description', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
class DeviceBayTemplateSerializer(ValidatedModelSerializer):
|
||||
@ -371,14 +410,14 @@ class DeviceBayTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = DeviceBayTemplate
|
||||
fields = ['id', 'url', 'device_type', 'name', 'label', 'description']
|
||||
fields = ['id', 'url', 'display', 'device_type', 'name', 'label', 'description', 'created', 'last_updated']
|
||||
|
||||
|
||||
#
|
||||
# Devices
|
||||
#
|
||||
|
||||
class DeviceRoleSerializer(ValidatedModelSerializer):
|
||||
class DeviceRoleSerializer(OrganizationalModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
|
||||
device_count = serializers.IntegerField(read_only=True)
|
||||
virtualmachine_count = serializers.IntegerField(read_only=True)
|
||||
@ -386,11 +425,12 @@ class DeviceRoleSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = DeviceRole
|
||||
fields = [
|
||||
'id', 'url', 'name', 'slug', 'color', 'vm_role', 'description', 'device_count', 'virtualmachine_count',
|
||||
'id', 'url', 'display', 'name', 'slug', 'color', 'vm_role', 'description', 'custom_fields', 'created',
|
||||
'last_updated', 'device_count', 'virtualmachine_count',
|
||||
]
|
||||
|
||||
|
||||
class PlatformSerializer(ValidatedModelSerializer):
|
||||
class PlatformSerializer(OrganizationalModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
|
||||
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True)
|
||||
device_count = serializers.IntegerField(read_only=True)
|
||||
@ -399,18 +439,19 @@ class PlatformSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = Platform
|
||||
fields = [
|
||||
'id', 'url', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'device_count',
|
||||
'virtualmachine_count',
|
||||
'id', 'url', 'display', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description',
|
||||
'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
|
||||
]
|
||||
|
||||
|
||||
class DeviceSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class DeviceSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
device_role = NestedDeviceRoleSerializer()
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
platform = NestedPlatformSerializer(required=False, allow_null=True)
|
||||
site = NestedSiteSerializer()
|
||||
location = NestedLocationSerializer(required=False, allow_null=True, default=None)
|
||||
rack = NestedRackSerializer(required=False, allow_null=True)
|
||||
face = ChoiceField(choices=DeviceFaceChoices, allow_blank=True, required=False)
|
||||
status = ChoiceField(choices=DeviceStatusChoices, required=False)
|
||||
@ -424,10 +465,10 @@ class DeviceSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = [
|
||||
'id', 'url', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial',
|
||||
'asset_tag', 'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4',
|
||||
'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'local_context_data',
|
||||
'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'id', 'url', 'display', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform',
|
||||
'serial', 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status',
|
||||
'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority',
|
||||
'comments', 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
validators = []
|
||||
|
||||
@ -460,10 +501,10 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
|
||||
|
||||
class Meta(DeviceSerializer.Meta):
|
||||
fields = [
|
||||
'id', 'url', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial',
|
||||
'asset_tag', 'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4',
|
||||
'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'local_context_data',
|
||||
'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
|
||||
'id', 'url', 'display', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform',
|
||||
'serial', 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status',
|
||||
'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority',
|
||||
'comments', 'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||
@ -475,7 +516,11 @@ class DeviceNAPALMSerializer(serializers.Serializer):
|
||||
method = serializers.DictField()
|
||||
|
||||
|
||||
class ConsoleServerPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||
#
|
||||
# Device components
|
||||
#
|
||||
|
||||
class ConsoleServerPortSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(
|
||||
@ -483,17 +528,23 @@ class ConsoleServerPortSerializer(TaggedObjectSerializer, CableTerminationSerial
|
||||
allow_blank=True,
|
||||
required=False
|
||||
)
|
||||
speed = ChoiceField(
|
||||
choices=ConsolePortSpeedChoices,
|
||||
allow_blank=True,
|
||||
required=False
|
||||
)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = [
|
||||
'id', 'url', 'device', 'name', 'label', 'type', 'description', 'cable', 'cable_peer', 'cable_peer_type',
|
||||
'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags',
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'speed', 'description', 'mark_connected',
|
||||
'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
||||
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
]
|
||||
|
||||
|
||||
class ConsolePortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||
class ConsolePortSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(
|
||||
@ -501,17 +552,23 @@ class ConsolePortSerializer(TaggedObjectSerializer, CableTerminationSerializer,
|
||||
allow_blank=True,
|
||||
required=False
|
||||
)
|
||||
speed = ChoiceField(
|
||||
choices=ConsolePortSpeedChoices,
|
||||
allow_blank=True,
|
||||
required=False
|
||||
)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = [
|
||||
'id', 'url', 'device', 'name', 'label', 'type', 'description', 'cable', 'cable_peer', 'cable_peer_type',
|
||||
'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags',
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'speed', 'description', 'mark_connected',
|
||||
'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
||||
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
]
|
||||
|
||||
|
||||
class PowerOutletSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||
class PowerOutletSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(
|
||||
@ -534,13 +591,13 @@ class PowerOutletSerializer(TaggedObjectSerializer, CableTerminationSerializer,
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = [
|
||||
'id', 'url', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'cable',
|
||||
'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
||||
'connected_endpoint_reachable', 'tags',
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
|
||||
'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
||||
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
]
|
||||
|
||||
|
||||
class PowerPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||
class PowerPortSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(
|
||||
@ -553,16 +610,17 @@ class PowerPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, Co
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = [
|
||||
'id', 'url', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'cable',
|
||||
'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
||||
'connected_endpoint_reachable', 'tags',
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
|
||||
'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
||||
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
]
|
||||
|
||||
|
||||
class InterfaceSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||
class InterfaceSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(choices=InterfaceTypeChoices)
|
||||
parent = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
|
||||
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
|
||||
@ -578,10 +636,11 @@ class InterfaceSerializer(TaggedObjectSerializer, CableTerminationSerializer, Co
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = [
|
||||
'id', 'url', 'device', 'name', 'label', 'type', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only',
|
||||
'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'cable', 'cable_peer', 'cable_peer_type',
|
||||
'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags',
|
||||
'count_ipaddresses',
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address',
|
||||
'mgmt_only', 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable',
|
||||
'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
||||
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses',
|
||||
'_occupied',
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
@ -598,7 +657,7 @@ class InterfaceSerializer(TaggedObjectSerializer, CableTerminationSerializer, Co
|
||||
return super().validate(data)
|
||||
|
||||
|
||||
class RearPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ValidatedModelSerializer):
|
||||
class RearPortSerializer(PrimaryModelSerializer, CableTerminationSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(choices=PortTypeChoices)
|
||||
@ -607,8 +666,8 @@ class RearPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, Val
|
||||
class Meta:
|
||||
model = RearPort
|
||||
fields = [
|
||||
'id', 'url', 'device', 'name', 'label', 'type', 'positions', 'description', 'cable', 'cable_peer',
|
||||
'cable_peer_type', 'tags',
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'positions', 'description', 'mark_connected',
|
||||
'cable', 'cable_peer', 'cable_peer_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
]
|
||||
|
||||
|
||||
@ -620,10 +679,10 @@ class FrontPortRearPortSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = RearPort
|
||||
fields = ['id', 'url', 'name', 'label']
|
||||
fields = ['id', 'url', 'display', 'name', 'label']
|
||||
|
||||
|
||||
class FrontPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ValidatedModelSerializer):
|
||||
class FrontPortSerializer(PrimaryModelSerializer, CableTerminationSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(choices=PortTypeChoices)
|
||||
@ -633,26 +692,30 @@ class FrontPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, Va
|
||||
class Meta:
|
||||
model = FrontPort
|
||||
fields = [
|
||||
'id', 'url', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable',
|
||||
'cable_peer', 'cable_peer_type', 'tags',
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description',
|
||||
'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'tags', 'custom_fields', 'created',
|
||||
'last_updated', '_occupied',
|
||||
]
|
||||
|
||||
|
||||
class DeviceBaySerializer(TaggedObjectSerializer, ValidatedModelSerializer):
|
||||
class DeviceBaySerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
installed_device = NestedDeviceSerializer(required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = DeviceBay
|
||||
fields = ['id', 'url', 'device', 'name', 'label', 'description', 'installed_device', 'tags']
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'description', 'installed_device', 'tags',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Inventory items
|
||||
#
|
||||
|
||||
class InventoryItemSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
|
||||
class InventoryItemSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
# Provide a default value to satisfy UniqueTogetherValidator
|
||||
@ -663,8 +726,8 @@ class InventoryItemSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = InventoryItem
|
||||
fields = [
|
||||
'id', 'url', 'device', 'parent', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag',
|
||||
'discovered', 'description', 'tags', '_depth',
|
||||
'id', 'url', 'display', 'device', 'parent', 'name', 'label', 'manufacturer', 'part_id', 'serial',
|
||||
'asset_tag', 'discovered', 'description', 'tags', 'custom_fields', 'created', 'last_updated', '_depth',
|
||||
]
|
||||
|
||||
|
||||
@ -672,7 +735,7 @@ class InventoryItemSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
|
||||
# Cables
|
||||
#
|
||||
|
||||
class CableSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class CableSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
|
||||
termination_a_type = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS)
|
||||
@ -688,7 +751,7 @@ class CableSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class Meta:
|
||||
model = Cable
|
||||
fields = [
|
||||
'id', 'url', 'termination_a_type', 'termination_a_id', 'termination_a', 'termination_b_type',
|
||||
'id', 'url', 'display', 'termination_a_type', 'termination_a_id', 'termination_a', 'termination_b_type',
|
||||
'termination_b_id', 'termination_b', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags',
|
||||
'custom_fields',
|
||||
]
|
||||
@ -802,24 +865,24 @@ class InterfaceConnectionSerializer(ValidatedModelSerializer):
|
||||
# Virtual chassis
|
||||
#
|
||||
|
||||
class VirtualChassisSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class VirtualChassisSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
|
||||
master = NestedDeviceSerializer(required=False)
|
||||
member_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = VirtualChassis
|
||||
fields = ['id', 'url', 'name', 'domain', 'master', 'tags', 'custom_fields', 'member_count']
|
||||
fields = ['id', 'url', 'display', 'name', 'domain', 'master', 'tags', 'custom_fields', 'member_count']
|
||||
|
||||
|
||||
#
|
||||
# Power panels
|
||||
#
|
||||
|
||||
class PowerPanelSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class PowerPanelSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
|
||||
site = NestedSiteSerializer()
|
||||
rack_group = NestedRackGroupSerializer(
|
||||
location = NestedLocationSerializer(
|
||||
required=False,
|
||||
allow_null=True,
|
||||
default=None
|
||||
@ -828,15 +891,10 @@ class PowerPanelSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = PowerPanel
|
||||
fields = ['id', 'url', 'site', 'rack_group', 'name', 'tags', 'custom_fields', 'powerfeed_count']
|
||||
fields = ['id', 'url', 'display', 'site', 'location', 'name', 'tags', 'custom_fields', 'powerfeed_count']
|
||||
|
||||
|
||||
class PowerFeedSerializer(
|
||||
TaggedObjectSerializer,
|
||||
CableTerminationSerializer,
|
||||
ConnectedEndpointSerializer,
|
||||
CustomFieldModelSerializer
|
||||
):
|
||||
class PowerFeedSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
|
||||
power_panel = NestedPowerPanelSerializer()
|
||||
rack = NestedRackSerializer(
|
||||
@ -865,8 +923,8 @@ class PowerFeedSerializer(
|
||||
class Meta:
|
||||
model = PowerFeed
|
||||
fields = [
|
||||
'id', 'url', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage',
|
||||
'max_utilization', 'comments', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint',
|
||||
'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created',
|
||||
'last_updated',
|
||||
'id', 'url', 'display', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage',
|
||||
'amperage', 'max_utilization', 'comments', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type',
|
||||
'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', '_occupied',
|
||||
]
|
||||
|
||||
@ -7,10 +7,11 @@ router.APIRootView = views.DCIMRootView
|
||||
|
||||
# Sites
|
||||
router.register('regions', views.RegionViewSet)
|
||||
router.register('site-groups', views.SiteGroupViewSet)
|
||||
router.register('sites', views.SiteViewSet)
|
||||
|
||||
# Racks
|
||||
router.register('rack-groups', views.RackGroupViewSet)
|
||||
router.register('locations', views.LocationViewSet)
|
||||
router.register('rack-roles', views.RackRoleViewSet)
|
||||
router.register('racks', views.RackViewSet)
|
||||
router.register('rack-reservations', views.RackReservationViewSet)
|
||||
|
||||
@ -17,13 +17,7 @@ from rest_framework.viewsets import GenericViewSet, ViewSet
|
||||
|
||||
from circuits.models import Circuit
|
||||
from dcim import filters
|
||||
from dcim.models import (
|
||||
Cable, CablePath, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
|
||||
Manufacturer, InventoryItem, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
|
||||
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
|
||||
VirtualChassis,
|
||||
)
|
||||
from dcim.models import *
|
||||
from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet
|
||||
from ipam.models import Prefix, VLAN
|
||||
from netbox.api.views import ModelViewSet
|
||||
@ -100,7 +94,7 @@ class PassThroughPortMixin(object):
|
||||
# Regions
|
||||
#
|
||||
|
||||
class RegionViewSet(ModelViewSet):
|
||||
class RegionViewSet(CustomFieldModelViewSet):
|
||||
queryset = Region.objects.add_related_count(
|
||||
Region.objects.all(),
|
||||
Site,
|
||||
@ -112,6 +106,22 @@ class RegionViewSet(ModelViewSet):
|
||||
filterset_class = filters.RegionFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Site groups
|
||||
#
|
||||
|
||||
class SiteGroupViewSet(CustomFieldModelViewSet):
|
||||
queryset = SiteGroup.objects.add_related_count(
|
||||
SiteGroup.objects.all(),
|
||||
Site,
|
||||
'group',
|
||||
'site_count',
|
||||
cumulative=True
|
||||
)
|
||||
serializer_class = serializers.SiteGroupSerializer
|
||||
filterset_class = filters.SiteGroupFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Sites
|
||||
#
|
||||
@ -132,26 +142,32 @@ class SiteViewSet(CustomFieldModelViewSet):
|
||||
|
||||
|
||||
#
|
||||
# Rack groups
|
||||
# Locations
|
||||
#
|
||||
|
||||
class RackGroupViewSet(ModelViewSet):
|
||||
queryset = RackGroup.objects.add_related_count(
|
||||
RackGroup.objects.all(),
|
||||
class LocationViewSet(CustomFieldModelViewSet):
|
||||
queryset = Location.objects.add_related_count(
|
||||
Location.objects.add_related_count(
|
||||
Location.objects.all(),
|
||||
Device,
|
||||
'location',
|
||||
'device_count',
|
||||
cumulative=True
|
||||
),
|
||||
Rack,
|
||||
'group',
|
||||
'location',
|
||||
'rack_count',
|
||||
cumulative=True
|
||||
).prefetch_related('site')
|
||||
serializer_class = serializers.RackGroupSerializer
|
||||
filterset_class = filters.RackGroupFilterSet
|
||||
serializer_class = serializers.LocationSerializer
|
||||
filterset_class = filters.LocationFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Rack roles
|
||||
#
|
||||
|
||||
class RackRoleViewSet(ModelViewSet):
|
||||
class RackRoleViewSet(CustomFieldModelViewSet):
|
||||
queryset = RackRole.objects.annotate(
|
||||
rack_count=count_related(Rack, 'role')
|
||||
)
|
||||
@ -165,7 +181,7 @@ class RackRoleViewSet(ModelViewSet):
|
||||
|
||||
class RackViewSet(CustomFieldModelViewSet):
|
||||
queryset = Rack.objects.prefetch_related(
|
||||
'site', 'group__site', 'role', 'tenant', 'tags'
|
||||
'site', 'location', 'role', 'tenant', 'tags'
|
||||
).annotate(
|
||||
device_count=count_related(Device, 'rack'),
|
||||
powerfeed_count=count_related(PowerFeed, 'rack')
|
||||
@ -239,7 +255,7 @@ class RackReservationViewSet(ModelViewSet):
|
||||
# Manufacturers
|
||||
#
|
||||
|
||||
class ManufacturerViewSet(ModelViewSet):
|
||||
class ManufacturerViewSet(CustomFieldModelViewSet):
|
||||
queryset = Manufacturer.objects.annotate(
|
||||
devicetype_count=count_related(DeviceType, 'manufacturer'),
|
||||
inventoryitem_count=count_related(InventoryItem, 'manufacturer'),
|
||||
@ -318,7 +334,7 @@ class DeviceBayTemplateViewSet(ModelViewSet):
|
||||
# Device roles
|
||||
#
|
||||
|
||||
class DeviceRoleViewSet(ModelViewSet):
|
||||
class DeviceRoleViewSet(CustomFieldModelViewSet):
|
||||
queryset = DeviceRole.objects.annotate(
|
||||
device_count=count_related(Device, 'device_role'),
|
||||
virtualmachine_count=count_related(VirtualMachine, 'role')
|
||||
@ -331,7 +347,7 @@ class DeviceRoleViewSet(ModelViewSet):
|
||||
# Platforms
|
||||
#
|
||||
|
||||
class PlatformViewSet(ModelViewSet):
|
||||
class PlatformViewSet(CustomFieldModelViewSet):
|
||||
queryset = Platform.objects.annotate(
|
||||
device_count=count_related(Device, 'platform'),
|
||||
virtualmachine_count=count_related(VirtualMachine, 'platform')
|
||||
@ -346,7 +362,7 @@ class PlatformViewSet(ModelViewSet):
|
||||
|
||||
class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
|
||||
queryset = Device.objects.prefetch_related(
|
||||
'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'rack', 'parent_bay',
|
||||
'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'location', 'rack', 'parent_bay',
|
||||
'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags',
|
||||
)
|
||||
filterset_class = filters.DeviceFilterSet
|
||||
@ -523,7 +539,7 @@ class PowerOutletViewSet(PathEndpointMixin, ModelViewSet):
|
||||
|
||||
class InterfaceViewSet(PathEndpointMixin, ModelViewSet):
|
||||
queryset = Interface.objects.prefetch_related(
|
||||
'device', '_path__destination', 'cable', '_cable_peer', 'ip_addresses', 'tags'
|
||||
'device', 'parent', 'lag', '_path__destination', 'cable', '_cable_peer', 'ip_addresses', 'tags'
|
||||
)
|
||||
serializer_class = serializers.InterfaceSerializer
|
||||
filterset_class = filters.InterfaceFilterSet
|
||||
@ -622,7 +638,7 @@ class VirtualChassisViewSet(ModelViewSet):
|
||||
|
||||
class PowerPanelViewSet(ModelViewSet):
|
||||
queryset = PowerPanel.objects.prefetch_related(
|
||||
'site', 'rack_group'
|
||||
'site', 'location'
|
||||
).annotate(
|
||||
powerfeed_count=count_related(PowerFeed, 'power_panel')
|
||||
)
|
||||
|
||||
@ -217,6 +217,29 @@ class ConsolePortTypeChoices(ChoiceSet):
|
||||
)
|
||||
|
||||
|
||||
class ConsolePortSpeedChoices(ChoiceSet):
|
||||
|
||||
SPEED_1200 = 1200
|
||||
SPEED_2400 = 2400
|
||||
SPEED_4800 = 4800
|
||||
SPEED_9600 = 9600
|
||||
SPEED_19200 = 19200
|
||||
SPEED_38400 = 38400
|
||||
SPEED_57600 = 57600
|
||||
SPEED_115200 = 115200
|
||||
|
||||
CHOICES = (
|
||||
(SPEED_1200, '1200 bps'),
|
||||
(SPEED_2400, '2400 bps'),
|
||||
(SPEED_4800, '4800 bps'),
|
||||
(SPEED_9600, '9600 bps'),
|
||||
(SPEED_19200, '19.2 kbps'),
|
||||
(SPEED_38400, '38.4 kbps'),
|
||||
(SPEED_57600, '57.6 kbps'),
|
||||
(SPEED_115200, '115.2 kbps'),
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# PowerPorts
|
||||
#
|
||||
|
||||
@ -12,13 +12,7 @@ from utilities.filters import (
|
||||
from virtualization.models import Cluster
|
||||
from .choices import *
|
||||
from .constants import *
|
||||
from .models import (
|
||||
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
|
||||
InventoryItem, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
|
||||
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
|
||||
VirtualChassis,
|
||||
)
|
||||
from .models import *
|
||||
|
||||
|
||||
__all__ = (
|
||||
@ -40,6 +34,7 @@ __all__ = (
|
||||
'InterfaceFilterSet',
|
||||
'InterfaceTemplateFilterSet',
|
||||
'InventoryItemFilterSet',
|
||||
'LocationFilterSet',
|
||||
'ManufacturerFilterSet',
|
||||
'PathEndpointFilterSet',
|
||||
'PlatformFilterSet',
|
||||
@ -51,13 +46,13 @@ __all__ = (
|
||||
'PowerPortFilterSet',
|
||||
'PowerPortTemplateFilterSet',
|
||||
'RackFilterSet',
|
||||
'RackGroupFilterSet',
|
||||
'RackReservationFilterSet',
|
||||
'RackRoleFilterSet',
|
||||
'RearPortFilterSet',
|
||||
'RearPortTemplateFilterSet',
|
||||
'RegionFilterSet',
|
||||
'SiteFilterSet',
|
||||
'SiteGroupFilterSet',
|
||||
'VirtualChassisFilterSet',
|
||||
)
|
||||
|
||||
@ -79,6 +74,23 @@ class RegionFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
|
||||
|
||||
class SiteGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
label='Parent site group (ID)',
|
||||
)
|
||||
parent = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='parent__slug',
|
||||
queryset=SiteGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Parent site group (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SiteGroup
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
|
||||
|
||||
class SiteFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@ -96,11 +108,22 @@ class SiteFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='region',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='group',
|
||||
lookup_expr='in',
|
||||
label='Group (ID)',
|
||||
)
|
||||
group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Group (slug)',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
@ -131,7 +154,7 @@ class SiteFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class RackGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
class LocationFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region',
|
||||
@ -145,6 +168,19 @@ class RackGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='site__group',
|
||||
lookup_expr='in',
|
||||
label='Site group (ID)',
|
||||
)
|
||||
site_group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='site__group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Site group (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
@ -155,19 +191,22 @@ class RackGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=RackGroup.objects.all(),
|
||||
label='Rack group (ID)',
|
||||
parent_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Location.objects.all(),
|
||||
field_name='parent',
|
||||
lookup_expr='in',
|
||||
label='Location (ID)',
|
||||
)
|
||||
parent = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='parent__slug',
|
||||
queryset=RackGroup.objects.all(),
|
||||
parent = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Location.objects.all(),
|
||||
field_name='parent',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Rack group (slug)',
|
||||
label='Location (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RackGroup
|
||||
model = Location
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
|
||||
|
||||
@ -196,6 +235,19 @@ class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='site__group',
|
||||
lookup_expr='in',
|
||||
label='Site group (ID)',
|
||||
)
|
||||
site_group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='site__group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Site group (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
@ -206,18 +258,18 @@ class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=RackGroup.objects.all(),
|
||||
field_name='group',
|
||||
location_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Location.objects.all(),
|
||||
field_name='location',
|
||||
lookup_expr='in',
|
||||
label='Rack group (ID)',
|
||||
label='Location (ID)',
|
||||
)
|
||||
group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=RackGroup.objects.all(),
|
||||
field_name='group',
|
||||
location = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Location.objects.all(),
|
||||
field_name='location',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Rack group (slug)',
|
||||
label='Location (slug)',
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=RackStatusChoices,
|
||||
@ -283,18 +335,18 @@ class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModel
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=RackGroup.objects.all(),
|
||||
field_name='rack__group',
|
||||
location_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Location.objects.all(),
|
||||
field_name='rack__location',
|
||||
lookup_expr='in',
|
||||
label='Rack group (ID)',
|
||||
label='Location (ID)',
|
||||
)
|
||||
group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=RackGroup.objects.all(),
|
||||
field_name='rack__group',
|
||||
location = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Location.objects.all(),
|
||||
field_name='rack__location',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Rack group (slug)',
|
||||
label='Location (slug)',
|
||||
)
|
||||
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=User.objects.all(),
|
||||
@ -581,6 +633,19 @@ class DeviceFilterSet(
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='site__group',
|
||||
lookup_expr='in',
|
||||
label='Site group (ID)',
|
||||
)
|
||||
site_group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='site__group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Site group (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
@ -591,11 +656,11 @@ class DeviceFilterSet(
|
||||
to_field_name='slug',
|
||||
label='Site name (slug)',
|
||||
)
|
||||
rack_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=RackGroup.objects.all(),
|
||||
field_name='rack__group',
|
||||
location_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Location.objects.all(),
|
||||
field_name='location',
|
||||
lookup_expr='in',
|
||||
label='Rack group (ID)',
|
||||
label='Location (ID)',
|
||||
)
|
||||
rack_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='rack',
|
||||
@ -719,7 +784,7 @@ class DeviceFilterSet(
|
||||
return queryset.exclude(devicebays__isnull=value)
|
||||
|
||||
|
||||
class DeviceComponentFilterSet(django_filters.FilterSet):
|
||||
class DeviceComponentFilterSet(CustomFieldModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@ -737,6 +802,19 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='device__site__group',
|
||||
lookup_expr='in',
|
||||
label='Site group (ID)',
|
||||
)
|
||||
site_group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='device__site__group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Site group (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device__site',
|
||||
queryset=Site.objects.all(),
|
||||
@ -864,6 +942,11 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati
|
||||
method='filter_kind',
|
||||
label='Kind of interface',
|
||||
)
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='parent',
|
||||
queryset=Interface.objects.all(),
|
||||
label='Parent interface (ID)',
|
||||
)
|
||||
lag_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='lag',
|
||||
queryset=Interface.objects.all(),
|
||||
@ -1066,6 +1149,19 @@ class VirtualChassisFilterSet(BaseFilterSet, CustomFieldModelFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='master__site__group',
|
||||
lookup_expr='in',
|
||||
label='Site group (ID)',
|
||||
)
|
||||
site_group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='master__site__group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Site group (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='master__site',
|
||||
queryset=Site.objects.all(),
|
||||
@ -1254,6 +1350,19 @@ class PowerPanelFilterSet(BaseFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='site__group',
|
||||
lookup_expr='in',
|
||||
label='Site group (ID)',
|
||||
)
|
||||
site_group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='site__group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Site group (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
@ -1264,11 +1373,11 @@ class PowerPanelFilterSet(BaseFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Site name (slug)',
|
||||
)
|
||||
rack_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=RackGroup.objects.all(),
|
||||
field_name='rack_group',
|
||||
location_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Location.objects.all(),
|
||||
field_name='location',
|
||||
lookup_expr='in',
|
||||
label='Rack group (ID)',
|
||||
label='Location (ID)',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
@ -1309,6 +1418,19 @@ class PowerFeedFilterSet(
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='power_panel__site__group',
|
||||
lookup_expr='in',
|
||||
label='Site group (ID)',
|
||||
)
|
||||
site_group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='power_panel__site__group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Site group (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='power_panel__site',
|
||||
queryset=Site.objects.all(),
|
||||
|
||||
1398
netbox/dcim/forms.py
1398
netbox/dcim/forms.py
File diff suppressed because it is too large
Load Diff
@ -2,12 +2,10 @@ from django.core.management.base import BaseCommand
|
||||
from django.core.management.color import no_style
|
||||
from django.db import connection
|
||||
|
||||
from circuits.models import CircuitTermination
|
||||
from dcim.models import CablePath, ConsolePort, ConsoleServerPort, Interface, PowerFeed, PowerOutlet, PowerPort
|
||||
from dcim.signals import create_cablepath
|
||||
|
||||
ENDPOINT_MODELS = (
|
||||
CircuitTermination,
|
||||
ConsolePort,
|
||||
ConsoleServerPort,
|
||||
Interface,
|
||||
|
||||
422
netbox/dcim/migrations/0123_standardize_models.py
Normal file
422
netbox/dcim/migrations/0123_standardize_models.py
Normal file
@ -0,0 +1,422 @@
|
||||
import django.core.serializers.json
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0122_standardize_name_length'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='consoleport',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleport',
|
||||
name='custom_field_data',
|
||||
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleport',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleporttemplate',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleporttemplate',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleserverport',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleserverport',
|
||||
name='custom_field_data',
|
||||
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleserverport',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleserverporttemplate',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleserverporttemplate',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicebay',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicebay',
|
||||
name='custom_field_data',
|
||||
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicebay',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicebaytemplate',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicebaytemplate',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicerole',
|
||||
name='custom_field_data',
|
||||
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='frontport',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='frontport',
|
||||
name='custom_field_data',
|
||||
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='frontport',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='frontporttemplate',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='frontporttemplate',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='custom_field_data',
|
||||
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interfacetemplate',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interfacetemplate',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventoryitem',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventoryitem',
|
||||
name='custom_field_data',
|
||||
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventoryitem',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='manufacturer',
|
||||
name='custom_field_data',
|
||||
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='platform',
|
||||
name='custom_field_data',
|
||||
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='poweroutlet',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='poweroutlet',
|
||||
name='custom_field_data',
|
||||
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='poweroutlet',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='poweroutlettemplate',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='poweroutlettemplate',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerport',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerport',
|
||||
name='custom_field_data',
|
||||
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerport',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerporttemplate',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerporttemplate',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rackgroup',
|
||||
name='custom_field_data',
|
||||
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rackrole',
|
||||
name='custom_field_data',
|
||||
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rearport',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rearport',
|
||||
name='custom_field_data',
|
||||
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rearport',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rearporttemplate',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rearporttemplate',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='region',
|
||||
name='custom_field_data',
|
||||
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cable',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cablepath',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consoleport',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consoleporttemplate',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consoleserverport',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consoleserverporttemplate',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicebay',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicebaytemplate',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicerole',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='frontport',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='frontporttemplate',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryitem',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='manufacturer',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='platform',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powerfeed',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='poweroutlet',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='poweroutlettemplate',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powerpanel',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powerport',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powerporttemplate',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rackgroup',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rackreservation',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rackrole',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rearport',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rearporttemplate',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='region',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='site',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='virtualchassis',
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
]
|
||||
51
netbox/dcim/migrations/0124_mark_connected.py
Normal file
51
netbox/dcim/migrations/0124_mark_connected.py
Normal file
@ -0,0 +1,51 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0123_standardize_models'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='consoleport',
|
||||
name='mark_connected',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleserverport',
|
||||
name='mark_connected',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='frontport',
|
||||
name='mark_connected',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='mark_connected',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerfeed',
|
||||
name='mark_connected',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='poweroutlet',
|
||||
name='mark_connected',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerport',
|
||||
name='mark_connected',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rearport',
|
||||
name='mark_connected',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
21
netbox/dcim/migrations/0125_console_port_speed.py
Normal file
21
netbox/dcim/migrations/0125_console_port_speed.py
Normal file
@ -0,0 +1,21 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0124_mark_connected'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='consoleport',
|
||||
name='speed',
|
||||
field=models.PositiveSmallIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleserverport',
|
||||
name='speed',
|
||||
field=models.PositiveSmallIntegerField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
39
netbox/dcim/migrations/0126_rename_rackgroup_location.py
Normal file
39
netbox/dcim/migrations/0126_rename_rackgroup_location.py
Normal file
@ -0,0 +1,39 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0125_console_port_speed'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name='RackGroup',
|
||||
new_name='Location',
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='rack',
|
||||
options={'ordering': ('site', 'location', '_name', 'pk')},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='location',
|
||||
name='site',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='locations', to='dcim.site'),
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='powerpanel',
|
||||
old_name='rack_group',
|
||||
new_name='location',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='rack',
|
||||
old_name='group',
|
||||
new_name='location',
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='rack',
|
||||
unique_together={('location', 'facility_id'), ('location', 'name')},
|
||||
),
|
||||
]
|
||||
17
netbox/dcim/migrations/0127_device_location.py
Normal file
17
netbox/dcim/migrations/0127_device_location.py
Normal file
@ -0,0 +1,17 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0126_rename_rackgroup_location'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='location',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.location'),
|
||||
),
|
||||
]
|
||||
24
netbox/dcim/migrations/0128_device_location_populate.py
Normal file
24
netbox/dcim/migrations/0128_device_location_populate.py
Normal file
@ -0,0 +1,24 @@
|
||||
from django.db import migrations
|
||||
from django.db.models import Subquery, OuterRef
|
||||
|
||||
|
||||
def populate_device_location(apps, schema_editor):
|
||||
Device = apps.get_model('dcim', 'Device')
|
||||
Device.objects.filter(rack__isnull=False).update(
|
||||
location_id=Subquery(
|
||||
Device.objects.filter(pk=OuterRef('pk')).values('rack__location_id')[:1]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0127_device_location'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
code=populate_device_location
|
||||
),
|
||||
]
|
||||
17
netbox/dcim/migrations/0129_interface_parent.py
Normal file
17
netbox/dcim/migrations/0129_interface_parent.py
Normal file
@ -0,0 +1,17 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0128_device_location_populate'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='parent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='child_interfaces', to='dcim.interface'),
|
||||
),
|
||||
]
|
||||
39
netbox/dcim/migrations/0130_sitegroup.py
Normal file
39
netbox/dcim/migrations/0130_sitegroup.py
Normal file
@ -0,0 +1,39 @@
|
||||
import django.core.serializers.json
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import mptt.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0129_interface_parent'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SiteGroup',
|
||||
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)),
|
||||
('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)),
|
||||
('lft', models.PositiveIntegerField(editable=False)),
|
||||
('rght', models.PositiveIntegerField(editable=False)),
|
||||
('tree_id', models.PositiveIntegerField(db_index=True, editable=False)),
|
||||
('level', models.PositiveIntegerField(editable=False)),
|
||||
('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='dcim.sitegroup')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='site',
|
||||
name='group',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sites', to='dcim.sitegroup'),
|
||||
),
|
||||
]
|
||||
@ -34,12 +34,13 @@ __all__ = (
|
||||
'PowerPort',
|
||||
'PowerPortTemplate',
|
||||
'Rack',
|
||||
'RackGroup',
|
||||
'Location',
|
||||
'RackReservation',
|
||||
'RackRole',
|
||||
'RearPort',
|
||||
'RearPortTemplate',
|
||||
'Region',
|
||||
'Site',
|
||||
'SiteGroup',
|
||||
'VirtualChassis',
|
||||
)
|
||||
|
||||
@ -6,14 +6,13 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import Sum
|
||||
from django.urls import reverse
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.fields import PathField
|
||||
from dcim.utils import decompile_path_node, object_to_path_node, path_node_to_object
|
||||
from extras.models import ChangeLoggedModel, CustomFieldModel, TaggedItem
|
||||
from extras.utils import extras_features
|
||||
from netbox.models import BigIDModel, PrimaryModel
|
||||
from utilities.fields import ColorField
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.utils import to_meters
|
||||
@ -32,7 +31,7 @@ __all__ = (
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class Cable(ChangeLoggedModel, CustomFieldModel):
|
||||
class Cable(PrimaryModel):
|
||||
"""
|
||||
A physical connection between two endpoints.
|
||||
"""
|
||||
@ -107,7 +106,6 @@ class Cable(ChangeLoggedModel, CustomFieldModel):
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
@ -244,6 +242,16 @@ class Cable(ChangeLoggedModel, CustomFieldModel):
|
||||
):
|
||||
raise ValidationError("A front port cannot be connected to it corresponding rear port")
|
||||
|
||||
# A CircuitTermination attached to a ProviderNetwork cannot have a Cable
|
||||
if isinstance(self.termination_a, CircuitTermination) and self.termination_a.provider_network is not None:
|
||||
raise ValidationError({
|
||||
'termination_a_id': "Circuit terminations attached to a provider network may not be cabled."
|
||||
})
|
||||
if isinstance(self.termination_b, CircuitTermination) and self.termination_b.provider_network is not None:
|
||||
raise ValidationError({
|
||||
'termination_b_id': "Circuit terminations attached to a provider network may not be cabled."
|
||||
})
|
||||
|
||||
# Check for an existing Cable connected to either termination object
|
||||
if self.termination_a.cable not in (None, self):
|
||||
raise ValidationError("{} already has a cable attached (#{})".format(
|
||||
@ -305,7 +313,7 @@ class Cable(ChangeLoggedModel, CustomFieldModel):
|
||||
return COMPATIBLE_TERMINATION_TYPES[self.termination_a._meta.model_name]
|
||||
|
||||
|
||||
class CablePath(models.Model):
|
||||
class CablePath(BigIDModel):
|
||||
"""
|
||||
A CablePath instance represents the physical path from an origin to a destination, including all intermediate
|
||||
elements in the path. Every instance must specify an `origin`, whereas `destination` may be null (for paths which do
|
||||
@ -386,6 +394,8 @@ class CablePath(models.Model):
|
||||
"""
|
||||
Create a new CablePath instance as traced from the given path origin.
|
||||
"""
|
||||
from circuits.models import CircuitTermination
|
||||
|
||||
if origin is None or origin.cable is None:
|
||||
return None
|
||||
|
||||
@ -433,6 +443,23 @@ class CablePath(models.Model):
|
||||
# No corresponding FrontPort found for the RearPort
|
||||
break
|
||||
|
||||
# Follow a CircuitTermination to its corresponding CircuitTermination (A to Z or vice versa)
|
||||
elif isinstance(peer_termination, CircuitTermination):
|
||||
path.append(object_to_path_node(peer_termination))
|
||||
# Get peer CircuitTermination
|
||||
node = peer_termination.get_peer_termination()
|
||||
if node:
|
||||
path.append(object_to_path_node(node))
|
||||
if node.provider_network:
|
||||
destination = node.provider_network
|
||||
break
|
||||
elif node.site and not node.cable:
|
||||
destination = node.site
|
||||
break
|
||||
else:
|
||||
# No peer CircuitTermination exists; halt the trace
|
||||
break
|
||||
|
||||
# Anything else marks the end of the path
|
||||
else:
|
||||
destination = peer_termination
|
||||
@ -478,15 +505,33 @@ class CablePath(models.Model):
|
||||
|
||||
return path
|
||||
|
||||
@property
|
||||
def last_node(self):
|
||||
"""
|
||||
Return either the destination or the last node within the path.
|
||||
"""
|
||||
return self.destination or path_node_to_object(self.path[-1])
|
||||
|
||||
def get_cable_ids(self):
|
||||
"""
|
||||
Return all Cable IDs within the path.
|
||||
"""
|
||||
cable_ct = ContentType.objects.get_for_model(Cable).pk
|
||||
cable_ids = []
|
||||
|
||||
for node in self.path:
|
||||
ct, id = decompile_path_node(node)
|
||||
if ct == cable_ct:
|
||||
cable_ids.append(id)
|
||||
|
||||
return cable_ids
|
||||
|
||||
def get_total_length(self):
|
||||
"""
|
||||
Return a tuple containing the sum of the length of each cable in the path
|
||||
and a flag indicating whether the length is definitive.
|
||||
"""
|
||||
cable_ids = [
|
||||
# Starting from the first element, every third element in the path should be a Cable
|
||||
decompile_path_node(self.path[i])[1] for i in range(0, len(self.path), 3)
|
||||
]
|
||||
cable_ids = self.get_cable_ids()
|
||||
cables = Cable.objects.filter(id__in=cable_ids, _abs_length__isnull=False)
|
||||
total_length = cables.aggregate(total=Sum('_abs_length'))['total']
|
||||
is_definitive = len(cables) == len(cable_ids)
|
||||
|
||||
@ -4,11 +4,11 @@ from django.db import models
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from extras.models import ObjectChange
|
||||
from extras.utils import extras_features
|
||||
from netbox.models import ChangeLoggedModel
|
||||
from utilities.fields import NaturalOrderingField
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.ordering import naturalize_interface
|
||||
from utilities.utils import serialize_object
|
||||
from .device_components import (
|
||||
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, PowerOutlet, PowerPort, RearPort,
|
||||
)
|
||||
@ -26,7 +26,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class ComponentTemplateModel(models.Model):
|
||||
class ComponentTemplateModel(ChangeLoggedModel):
|
||||
device_type = models.ForeignKey(
|
||||
to='dcim.DeviceType',
|
||||
on_delete=models.CASCADE,
|
||||
@ -73,15 +73,10 @@ class ComponentTemplateModel(models.Model):
|
||||
except ObjectDoesNotExist:
|
||||
# The parent DeviceType has already been deleted
|
||||
device_type = None
|
||||
return ObjectChange(
|
||||
changed_object=self,
|
||||
object_repr=str(self),
|
||||
action=action,
|
||||
related_object=device_type,
|
||||
object_data=serialize_object(self)
|
||||
)
|
||||
return super().to_objectchange(action, related_object=device_type)
|
||||
|
||||
|
||||
@extras_features('webhooks')
|
||||
class ConsolePortTemplate(ComponentTemplateModel):
|
||||
"""
|
||||
A template for a ConsolePort to be created for a new Device.
|
||||
@ -105,6 +100,7 @@ class ConsolePortTemplate(ComponentTemplateModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('webhooks')
|
||||
class ConsoleServerPortTemplate(ComponentTemplateModel):
|
||||
"""
|
||||
A template for a ConsoleServerPort to be created for a new Device.
|
||||
@ -128,6 +124,7 @@ class ConsoleServerPortTemplate(ComponentTemplateModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('webhooks')
|
||||
class PowerPortTemplate(ComponentTemplateModel):
|
||||
"""
|
||||
A template for a PowerPort to be created for a new Device.
|
||||
@ -174,6 +171,7 @@ class PowerPortTemplate(ComponentTemplateModel):
|
||||
})
|
||||
|
||||
|
||||
@extras_features('webhooks')
|
||||
class PowerOutletTemplate(ComponentTemplateModel):
|
||||
"""
|
||||
A template for a PowerOutlet to be created for a new Device.
|
||||
@ -225,6 +223,7 @@ class PowerOutletTemplate(ComponentTemplateModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('webhooks')
|
||||
class InterfaceTemplate(ComponentTemplateModel):
|
||||
"""
|
||||
A template for a physical data interface on a new Device.
|
||||
@ -259,6 +258,7 @@ class InterfaceTemplate(ComponentTemplateModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('webhooks')
|
||||
class FrontPortTemplate(ComponentTemplateModel):
|
||||
"""
|
||||
Template for a pass-through port on the front of a new Device.
|
||||
@ -319,6 +319,7 @@ class FrontPortTemplate(ComponentTemplateModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('webhooks')
|
||||
class RearPortTemplate(ComponentTemplateModel):
|
||||
"""
|
||||
Template for a pass-through port on the rear of a new Device.
|
||||
@ -349,6 +350,7 @@ class RearPortTemplate(ComponentTemplateModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('webhooks')
|
||||
class DeviceBayTemplate(ComponentTemplateModel):
|
||||
"""
|
||||
A template for a DeviceBay to be created for a new parent Device.
|
||||
|
||||
@ -6,19 +6,17 @@ from django.db import models
|
||||
from django.db.models import Sum
|
||||
from django.urls import reverse
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.fields import MACAddressField
|
||||
from extras.models import ObjectChange, TaggedItem
|
||||
from extras.utils import extras_features
|
||||
from netbox.models import PrimaryModel
|
||||
from utilities.fields import NaturalOrderingField
|
||||
from utilities.mptt import TreeManager
|
||||
from utilities.ordering import naturalize_interface
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.query_functions import CollateAsChar
|
||||
from utilities.utils import serialize_object
|
||||
|
||||
|
||||
__all__ = (
|
||||
@ -37,7 +35,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class ComponentModel(models.Model):
|
||||
class ComponentModel(PrimaryModel):
|
||||
"""
|
||||
An abstract model inherited by any model which has a parent Device.
|
||||
"""
|
||||
@ -81,17 +79,11 @@ class ComponentModel(models.Model):
|
||||
except ObjectDoesNotExist:
|
||||
# The parent Device has already been deleted
|
||||
device = None
|
||||
return ObjectChange(
|
||||
changed_object=self,
|
||||
object_repr=str(self),
|
||||
action=action,
|
||||
related_object=device,
|
||||
object_data=serialize_object(self)
|
||||
)
|
||||
return super().to_objectchange(action, related_object=device)
|
||||
|
||||
@property
|
||||
def parent(self):
|
||||
return getattr(self, 'device', None)
|
||||
def parent_object(self):
|
||||
return self.device
|
||||
|
||||
|
||||
class CableTermination(models.Model):
|
||||
@ -125,6 +117,10 @@ class CableTermination(models.Model):
|
||||
ct_field='_cable_peer_type',
|
||||
fk_field='_cable_peer_id'
|
||||
)
|
||||
mark_connected = models.BooleanField(
|
||||
default=False,
|
||||
help_text="Treat as if a cable is connected"
|
||||
)
|
||||
|
||||
# Generic relations to Cable. These ensure that an attached Cable is deleted if the terminated object is deleted.
|
||||
_cabled_as_a = GenericRelation(
|
||||
@ -141,14 +137,30 @@ class CableTermination(models.Model):
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
if self.mark_connected and self.cable_id:
|
||||
raise ValidationError({
|
||||
"mark_connected": "Cannot mark as connected with a cable attached."
|
||||
})
|
||||
|
||||
def get_cable_peer(self):
|
||||
return self._cable_peer
|
||||
|
||||
@property
|
||||
def _occupied(self):
|
||||
return bool(self.mark_connected or self.cable_id)
|
||||
|
||||
@property
|
||||
def parent_object(self):
|
||||
raise NotImplementedError("CableTermination models must implement parent_object()")
|
||||
|
||||
|
||||
class PathEndpoint(models.Model):
|
||||
"""
|
||||
An abstract model inherited by any CableTermination subclass which represents the end of a CablePath; specifically,
|
||||
these include ConsolePort, ConsoleServerPort, PowerPort, PowerOutlet, Interface, PowerFeed, and CircuitTermination.
|
||||
these include ConsolePort, ConsoleServerPort, PowerPort, PowerOutlet, Interface, and PowerFeed.
|
||||
|
||||
`_path` references the CablePath originating from this instance, if any. It is set or cleared by the receivers in
|
||||
dcim.signals in response to changes in the cable path, and complements the `origin` GenericForeignKey field on the
|
||||
@ -172,10 +184,11 @@ class PathEndpoint(models.Model):
|
||||
|
||||
# Construct the complete path
|
||||
path = [self, *self._path.get_path()]
|
||||
while (len(path) + 1) % 3:
|
||||
# Pad to ensure we have complete three-tuples (e.g. for paths that end at a RearPort)
|
||||
path.append(None)
|
||||
if self._path.destination:
|
||||
path.append(self._path.destination)
|
||||
while len(path) % 3:
|
||||
# Pad to ensure we have complete three-tuples (e.g. for paths that end at a RearPort)
|
||||
path.insert(-1, None)
|
||||
|
||||
# Return the path as a list of three-tuples (A termination, cable, B termination)
|
||||
return list(zip(*[iter(path)] * 3))
|
||||
@ -198,8 +211,8 @@ class PathEndpoint(models.Model):
|
||||
# Console ports
|
||||
#
|
||||
|
||||
@extras_features('export_templates', 'webhooks', 'custom_links')
|
||||
class ConsolePort(CableTermination, PathEndpoint, ComponentModel):
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class ConsolePort(ComponentModel, CableTermination, PathEndpoint):
|
||||
"""
|
||||
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
|
||||
"""
|
||||
@ -209,9 +222,14 @@ class ConsolePort(CableTermination, PathEndpoint, ComponentModel):
|
||||
blank=True,
|
||||
help_text='Physical port type'
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
speed = models.PositiveSmallIntegerField(
|
||||
choices=ConsolePortSpeedChoices,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text='Port speed in bits per second'
|
||||
)
|
||||
|
||||
csv_headers = ['device', 'name', 'label', 'type', 'description']
|
||||
csv_headers = ['device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ('device', '_name')
|
||||
@ -226,6 +244,8 @@ class ConsolePort(CableTermination, PathEndpoint, ComponentModel):
|
||||
self.name,
|
||||
self.label,
|
||||
self.type,
|
||||
self.speed,
|
||||
self.mark_connected,
|
||||
self.description,
|
||||
)
|
||||
|
||||
@ -234,8 +254,8 @@ class ConsolePort(CableTermination, PathEndpoint, ComponentModel):
|
||||
# Console server ports
|
||||
#
|
||||
|
||||
@extras_features('webhooks', 'custom_links')
|
||||
class ConsoleServerPort(CableTermination, PathEndpoint, ComponentModel):
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class ConsoleServerPort(ComponentModel, CableTermination, PathEndpoint):
|
||||
"""
|
||||
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
|
||||
"""
|
||||
@ -245,9 +265,14 @@ class ConsoleServerPort(CableTermination, PathEndpoint, ComponentModel):
|
||||
blank=True,
|
||||
help_text='Physical port type'
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
speed = models.PositiveSmallIntegerField(
|
||||
choices=ConsolePortSpeedChoices,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text='Port speed in bits per second'
|
||||
)
|
||||
|
||||
csv_headers = ['device', 'name', 'label', 'type', 'description']
|
||||
csv_headers = ['device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ('device', '_name')
|
||||
@ -262,6 +287,8 @@ class ConsoleServerPort(CableTermination, PathEndpoint, ComponentModel):
|
||||
self.name,
|
||||
self.label,
|
||||
self.type,
|
||||
self.speed,
|
||||
self.mark_connected,
|
||||
self.description,
|
||||
)
|
||||
|
||||
@ -270,8 +297,8 @@ class ConsoleServerPort(CableTermination, PathEndpoint, ComponentModel):
|
||||
# Power ports
|
||||
#
|
||||
|
||||
@extras_features('export_templates', 'webhooks', 'custom_links')
|
||||
class PowerPort(CableTermination, PathEndpoint, ComponentModel):
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class PowerPort(ComponentModel, CableTermination, PathEndpoint):
|
||||
"""
|
||||
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
|
||||
"""
|
||||
@ -293,9 +320,10 @@ class PowerPort(CableTermination, PathEndpoint, ComponentModel):
|
||||
validators=[MinValueValidator(1)],
|
||||
help_text="Allocated power draw (watts)"
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = ['device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description']
|
||||
csv_headers = [
|
||||
'device', 'name', 'label', 'type', 'mark_connected', 'maximum_draw', 'allocated_draw', 'description',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
ordering = ('device', '_name')
|
||||
@ -310,6 +338,7 @@ class PowerPort(CableTermination, PathEndpoint, ComponentModel):
|
||||
self.name,
|
||||
self.label,
|
||||
self.get_type_display(),
|
||||
self.mark_connected,
|
||||
self.maximum_draw,
|
||||
self.allocated_draw,
|
||||
self.description,
|
||||
@ -379,8 +408,8 @@ class PowerPort(CableTermination, PathEndpoint, ComponentModel):
|
||||
# Power outlets
|
||||
#
|
||||
|
||||
@extras_features('webhooks', 'custom_links')
|
||||
class PowerOutlet(CableTermination, PathEndpoint, ComponentModel):
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class PowerOutlet(ComponentModel, CableTermination, PathEndpoint):
|
||||
"""
|
||||
A physical power outlet (output) within a Device which provides power to a PowerPort.
|
||||
"""
|
||||
@ -403,9 +432,8 @@ class PowerOutlet(CableTermination, PathEndpoint, ComponentModel):
|
||||
blank=True,
|
||||
help_text="Phase (for three-phase feeds)"
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = ['device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description']
|
||||
csv_headers = ['device', 'name', 'label', 'type', 'mark_connected', 'power_port', 'feed_leg', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ('device', '_name')
|
||||
@ -420,6 +448,7 @@ class PowerOutlet(CableTermination, PathEndpoint, ComponentModel):
|
||||
self.name,
|
||||
self.label,
|
||||
self.get_type_display(),
|
||||
self.mark_connected,
|
||||
self.power_port.name if self.power_port else None,
|
||||
self.get_feed_leg_display(),
|
||||
self.description,
|
||||
@ -483,8 +512,8 @@ class BaseInterface(models.Model):
|
||||
return self.ip_addresses.count()
|
||||
|
||||
|
||||
@extras_features('export_templates', 'webhooks', 'custom_links')
|
||||
class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
|
||||
"""
|
||||
A network interface within a Device. A physical Interface can connect to exactly one other Interface.
|
||||
"""
|
||||
@ -495,6 +524,14 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
parent = models.ForeignKey(
|
||||
to='self',
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='child_interfaces',
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name='Parent interface'
|
||||
)
|
||||
lag = models.ForeignKey(
|
||||
to='self',
|
||||
on_delete=models.SET_NULL,
|
||||
@ -532,10 +569,10 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
|
||||
object_id_field='assigned_object_id',
|
||||
related_query_name='interface'
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = [
|
||||
'device', 'name', 'label', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only', 'description', 'mode',
|
||||
'device', 'name', 'label', 'parent', 'lag', 'type', 'enabled', 'mark_connected', 'mac_address', 'mtu',
|
||||
'mgmt_only', 'description', 'mode',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
@ -550,9 +587,11 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
|
||||
self.device.identifier if self.device else None,
|
||||
self.name,
|
||||
self.label,
|
||||
self.parent.name if self.parent else None,
|
||||
self.lag.name if self.lag else None,
|
||||
self.get_type_display(),
|
||||
self.enabled,
|
||||
self.mark_connected,
|
||||
self.mac_address,
|
||||
self.mtu,
|
||||
self.mgmt_only,
|
||||
@ -564,14 +603,38 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
|
||||
super().clean()
|
||||
|
||||
# Virtual interfaces cannot be connected
|
||||
if self.type in NONCONNECTABLE_IFACE_TYPES and (
|
||||
self.cable or getattr(self, 'circuit_termination', False)
|
||||
):
|
||||
if not self.is_connectable and self.cable:
|
||||
raise ValidationError({
|
||||
'type': "Virtual and wireless interfaces cannot be connected to another interface or circuit. "
|
||||
"Disconnect the interface or choose a suitable type."
|
||||
'type': f"{self.get_type_display()} interfaces cannot have a cable attached."
|
||||
})
|
||||
|
||||
# Non-connectable interfaces cannot be marked as connected
|
||||
if not self.is_connectable and self.mark_connected:
|
||||
raise ValidationError({
|
||||
'mark_connected': f"{self.get_type_display()} interfaces cannot be marked as connected."
|
||||
})
|
||||
|
||||
# An interface's parent must belong to the same device or virtual chassis
|
||||
if self.parent and self.parent.device != self.device:
|
||||
if self.device.virtual_chassis is None:
|
||||
raise ValidationError({
|
||||
'parent': f"The selected parent interface ({self.parent}) belongs to a different device "
|
||||
f"({self.parent.device})."
|
||||
})
|
||||
elif self.parent.device.virtual_chassis != self.parent.virtual_chassis:
|
||||
raise ValidationError({
|
||||
'parent': f"The selected parent interface ({self.parent}) belongs to {self.parent.device}, which "
|
||||
f"is not part of virtual chassis {self.device.virtual_chassis}."
|
||||
})
|
||||
|
||||
# An interface cannot be its own parent
|
||||
if self.pk and self.parent_id == self.pk:
|
||||
raise ValidationError({'parent': "An interface cannot be its own parent."})
|
||||
|
||||
# A physical interface cannot have a parent interface
|
||||
if self.type != InterfaceTypeChoices.TYPE_VIRTUAL and self.parent is not None:
|
||||
raise ValidationError({'parent': "Only virtual interfaces may be assigned to a parent interface."})
|
||||
|
||||
# An interface's LAG must belong to the same device or virtual chassis
|
||||
if self.lag and self.lag.device != self.device:
|
||||
if self.device.virtual_chassis is None:
|
||||
@ -593,16 +656,12 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
|
||||
raise ValidationError({'lag': "A LAG interface cannot be its own parent."})
|
||||
|
||||
# Validate untagged VLAN
|
||||
if self.untagged_vlan and self.untagged_vlan.site not in [self.parent.site, None]:
|
||||
if self.untagged_vlan and self.untagged_vlan.site not in [self.device.site, None]:
|
||||
raise ValidationError({
|
||||
'untagged_vlan': "The untagged VLAN ({}) must belong to the same site as the interface's parent "
|
||||
"device, or it must be global".format(self.untagged_vlan)
|
||||
})
|
||||
|
||||
@property
|
||||
def parent(self):
|
||||
return self.device
|
||||
|
||||
@property
|
||||
def is_connectable(self):
|
||||
return self.type not in NONCONNECTABLE_IFACE_TYPES
|
||||
@ -624,8 +683,8 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
|
||||
# Pass-through ports
|
||||
#
|
||||
|
||||
@extras_features('webhooks', 'custom_links')
|
||||
class FrontPort(CableTermination, ComponentModel):
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class FrontPort(ComponentModel, CableTermination):
|
||||
"""
|
||||
A pass-through port on the front of a Device.
|
||||
"""
|
||||
@ -645,9 +704,10 @@ class FrontPort(CableTermination, ComponentModel):
|
||||
MaxValueValidator(REARPORT_POSITIONS_MAX)
|
||||
]
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = ['device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description']
|
||||
csv_headers = [
|
||||
'device', 'name', 'label', 'type', 'mark_connected', 'rear_port', 'rear_port_position', 'description',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
ordering = ('device', '_name')
|
||||
@ -665,6 +725,7 @@ class FrontPort(CableTermination, ComponentModel):
|
||||
self.name,
|
||||
self.label,
|
||||
self.get_type_display(),
|
||||
self.mark_connected,
|
||||
self.rear_port.name,
|
||||
self.rear_port_position,
|
||||
self.description,
|
||||
@ -687,8 +748,8 @@ class FrontPort(CableTermination, ComponentModel):
|
||||
})
|
||||
|
||||
|
||||
@extras_features('webhooks', 'custom_links')
|
||||
class RearPort(CableTermination, ComponentModel):
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class RearPort(ComponentModel, CableTermination):
|
||||
"""
|
||||
A pass-through port on the rear of a Device.
|
||||
"""
|
||||
@ -703,9 +764,8 @@ class RearPort(CableTermination, ComponentModel):
|
||||
MaxValueValidator(REARPORT_POSITIONS_MAX)
|
||||
]
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = ['device', 'name', 'label', 'type', 'positions', 'description']
|
||||
csv_headers = ['device', 'name', 'label', 'type', 'mark_connected', 'positions', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ('device', '_name')
|
||||
@ -731,6 +791,7 @@ class RearPort(CableTermination, ComponentModel):
|
||||
self.name,
|
||||
self.label,
|
||||
self.get_type_display(),
|
||||
self.mark_connected,
|
||||
self.positions,
|
||||
self.description,
|
||||
)
|
||||
@ -740,7 +801,7 @@ class RearPort(CableTermination, ComponentModel):
|
||||
# Device bays
|
||||
#
|
||||
|
||||
@extras_features('webhooks', 'custom_links')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class DeviceBay(ComponentModel):
|
||||
"""
|
||||
An empty space within a Device which can house a child device
|
||||
@ -752,7 +813,6 @@ class DeviceBay(ComponentModel):
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = ['device', 'name', 'label', 'installed_device', 'description']
|
||||
|
||||
@ -800,7 +860,7 @@ class DeviceBay(ComponentModel):
|
||||
# Inventory items
|
||||
#
|
||||
|
||||
@extras_features('export_templates', 'webhooks', 'custom_links')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class InventoryItem(MPTTModel, ComponentModel):
|
||||
"""
|
||||
An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
|
||||
@ -845,8 +905,6 @@ class InventoryItem(MPTTModel, ComponentModel):
|
||||
help_text='This item was automatically discovered'
|
||||
)
|
||||
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = TreeManager()
|
||||
|
||||
csv_headers = [
|
||||
|
||||
@ -9,13 +9,13 @@ from django.db import models
|
||||
from django.db.models import F, ProtectedError
|
||||
from django.urls import reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from extras.models import ChangeLoggedModel, ConfigContextModel, CustomFieldModel, TaggedItem
|
||||
from extras.models import ConfigContextModel
|
||||
from extras.querysets import ConfigContextModelQuerySet
|
||||
from extras.utils import extras_features
|
||||
from netbox.models import OrganizationalModel, PrimaryModel
|
||||
from utilities.choices import ColorChoices
|
||||
from utilities.fields import ColorField, NaturalOrderingField
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
@ -36,8 +36,8 @@ __all__ = (
|
||||
# Device Types
|
||||
#
|
||||
|
||||
@extras_features('export_templates', 'webhooks')
|
||||
class Manufacturer(ChangeLoggedModel):
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
class Manufacturer(OrganizationalModel):
|
||||
"""
|
||||
A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
|
||||
"""
|
||||
@ -65,7 +65,7 @@ class Manufacturer(ChangeLoggedModel):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return "{}?manufacturer={}".format(reverse('dcim:devicetype_list'), self.slug)
|
||||
return reverse('dcim:manufacturer', args=[self.pk])
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
@ -76,7 +76,7 @@ class Manufacturer(ChangeLoggedModel):
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class DeviceType(ChangeLoggedModel, CustomFieldModel):
|
||||
class DeviceType(PrimaryModel):
|
||||
"""
|
||||
A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as
|
||||
well as high-level functional role(s).
|
||||
@ -135,7 +135,6 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
|
||||
comments = models.TextField(
|
||||
blank=True
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
@ -338,7 +337,8 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
|
||||
# Devices
|
||||
#
|
||||
|
||||
class DeviceRole(ChangeLoggedModel):
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
class DeviceRole(OrganizationalModel):
|
||||
"""
|
||||
Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a
|
||||
color to be used when displaying rack elevations. The vm_role field determines whether the role is applicable to
|
||||
@ -375,6 +375,9 @@ class DeviceRole(ChangeLoggedModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:devicerole', args=[self.pk])
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.name,
|
||||
@ -385,7 +388,8 @@ class DeviceRole(ChangeLoggedModel):
|
||||
)
|
||||
|
||||
|
||||
class Platform(ChangeLoggedModel):
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
class Platform(OrganizationalModel):
|
||||
"""
|
||||
Platform refers to the software or firmware running on a Device. For example, "Cisco IOS-XR" or "Juniper Junos".
|
||||
NetBox uses Platforms to determine how to interact with devices when pulling inventory data or other information by
|
||||
@ -435,7 +439,7 @@ class Platform(ChangeLoggedModel):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return "{}?platform={}".format(reverse('dcim:device_list'), self.slug)
|
||||
return reverse('dcim:platform', args=[self.pk])
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
@ -449,7 +453,7 @@ class Platform(ChangeLoggedModel):
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
class Device(PrimaryModel, ConfigContextModel):
|
||||
"""
|
||||
A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
|
||||
DeviceRole, and (optionally) a Platform. Device names are not required, however if one is set it must be unique.
|
||||
@ -514,6 +518,13 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
on_delete=models.PROTECT,
|
||||
related_name='devices'
|
||||
)
|
||||
location = models.ForeignKey(
|
||||
to='dcim.Location',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='devices',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
rack = models.ForeignKey(
|
||||
to='dcim.Rack',
|
||||
on_delete=models.PROTECT,
|
||||
@ -591,16 +602,15 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
object_id_field='assigned_object_id',
|
||||
related_query_name='device'
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = ConfigContextModelQuerySet.as_manager()
|
||||
|
||||
csv_headers = [
|
||||
'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
|
||||
'site', 'rack_group', 'rack_name', 'position', 'face', 'comments',
|
||||
'site', 'location', 'rack_name', 'position', 'face', 'comments',
|
||||
]
|
||||
clone_fields = [
|
||||
'device_type', 'device_role', 'tenant', 'platform', 'site', 'rack', 'status', 'cluster',
|
||||
'device_type', 'device_role', 'tenant', 'platform', 'site', 'location', 'rack', 'status', 'cluster',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
@ -637,11 +647,21 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Validate site/rack combination
|
||||
# Validate site/location/rack combination
|
||||
if self.rack and self.site != self.rack.site:
|
||||
raise ValidationError({
|
||||
'rack': f"Rack {self.rack} does not belong to site {self.site}.",
|
||||
})
|
||||
if self.location and self.site != self.location.site:
|
||||
raise ValidationError({
|
||||
'location': f"Location {self.location} does not belong to site {self.site}.",
|
||||
})
|
||||
if self.rack and self.location and self.rack.location != self.location:
|
||||
raise ValidationError({
|
||||
'rack': f"Rack {self.rack} does not belong to location {self.location}.",
|
||||
})
|
||||
elif self.rack:
|
||||
self.location = self.rack.location
|
||||
|
||||
if self.rack is None:
|
||||
if self.face:
|
||||
@ -796,7 +816,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
self.asset_tag,
|
||||
self.get_status_display(),
|
||||
self.site.name,
|
||||
self.rack.group.name if self.rack and self.rack.group else None,
|
||||
self.rack.location.name if self.rack and self.rack.location else None,
|
||||
self.rack.name if self.rack else None,
|
||||
self.position,
|
||||
self.get_face_display(),
|
||||
@ -882,7 +902,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class VirtualChassis(ChangeLoggedModel, CustomFieldModel):
|
||||
class VirtualChassis(PrimaryModel):
|
||||
"""
|
||||
A collection of Devices which operate with a shared control plane (e.g. a switch stack).
|
||||
"""
|
||||
@ -900,7 +920,6 @@ class VirtualChassis(ChangeLoggedModel, CustomFieldModel):
|
||||
max_length=30,
|
||||
blank=True
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
|
||||
@ -2,12 +2,11 @@ from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from extras.models import ChangeLoggedModel, CustomFieldModel, TaggedItem
|
||||
from extras.utils import extras_features
|
||||
from netbox.models import PrimaryModel
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.validators import ExclusionValidator
|
||||
from .device_components import CableTermination, PathEndpoint
|
||||
@ -23,7 +22,7 @@ __all__ = (
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class PowerPanel(ChangeLoggedModel, CustomFieldModel):
|
||||
class PowerPanel(PrimaryModel):
|
||||
"""
|
||||
A distribution point for electrical power; e.g. a data center RPP.
|
||||
"""
|
||||
@ -31,8 +30,8 @@ class PowerPanel(ChangeLoggedModel, CustomFieldModel):
|
||||
to='Site',
|
||||
on_delete=models.PROTECT
|
||||
)
|
||||
rack_group = models.ForeignKey(
|
||||
to='RackGroup',
|
||||
location = models.ForeignKey(
|
||||
to='dcim.Location',
|
||||
on_delete=models.PROTECT,
|
||||
blank=True,
|
||||
null=True
|
||||
@ -40,11 +39,10 @@ class PowerPanel(ChangeLoggedModel, CustomFieldModel):
|
||||
name = models.CharField(
|
||||
max_length=100
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['site', 'rack_group', 'name']
|
||||
csv_headers = ['site', 'location', 'name']
|
||||
|
||||
class Meta:
|
||||
ordering = ['site', 'name']
|
||||
@ -59,22 +57,22 @@ class PowerPanel(ChangeLoggedModel, CustomFieldModel):
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.site.name,
|
||||
self.rack_group.name if self.rack_group else None,
|
||||
self.location.name if self.location else None,
|
||||
self.name,
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# RackGroup must belong to assigned Site
|
||||
if self.rack_group and self.rack_group.site != self.site:
|
||||
raise ValidationError("Rack group {} ({}) is in a different site than {}".format(
|
||||
self.rack_group, self.rack_group.site, self.site
|
||||
))
|
||||
# Location must belong to assigned Site
|
||||
if self.location and self.location.site != self.site:
|
||||
raise ValidationError(
|
||||
f"Location {self.location} ({self.location.site}) is in a different site than {self.site}"
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class PowerFeed(ChangeLoggedModel, PathEndpoint, CableTermination, CustomFieldModel):
|
||||
class PowerFeed(PrimaryModel, PathEndpoint, CableTermination):
|
||||
"""
|
||||
An electrical circuit delivered from a PowerPanel.
|
||||
"""
|
||||
@ -132,17 +130,16 @@ class PowerFeed(ChangeLoggedModel, PathEndpoint, CableTermination, CustomFieldMo
|
||||
comments = models.TextField(
|
||||
blank=True
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = [
|
||||
'site', 'power_panel', 'rack_group', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage',
|
||||
'amperage', 'max_utilization', 'comments',
|
||||
'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase',
|
||||
'voltage', 'amperage', 'max_utilization', 'comments',
|
||||
]
|
||||
clone_fields = [
|
||||
'power_panel', 'rack', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization',
|
||||
'available_power',
|
||||
'power_panel', 'rack', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage',
|
||||
'max_utilization', 'available_power',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
@ -159,11 +156,12 @@ class PowerFeed(ChangeLoggedModel, PathEndpoint, CableTermination, CustomFieldMo
|
||||
return (
|
||||
self.power_panel.site.name,
|
||||
self.power_panel.name,
|
||||
self.rack.group.name if self.rack and self.rack.group else None,
|
||||
self.rack.location.name if self.rack and self.rack.location else None,
|
||||
self.rack.name if self.rack else None,
|
||||
self.name,
|
||||
self.get_status_display(),
|
||||
self.get_type_display(),
|
||||
self.mark_connected,
|
||||
self.get_supply_display(),
|
||||
self.get_phase_display(),
|
||||
self.voltage,
|
||||
@ -199,7 +197,7 @@ class PowerFeed(ChangeLoggedModel, PathEndpoint, CableTermination, CustomFieldMo
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def parent(self):
|
||||
def parent_object(self):
|
||||
return self.power_panel
|
||||
|
||||
def get_type_class(self):
|
||||
|
||||
@ -10,26 +10,22 @@ from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models import Count, Sum
|
||||
from django.urls import reverse
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.elevations import RackElevationSVG
|
||||
from extras.models import ChangeLoggedModel, CustomFieldModel, ObjectChange, TaggedItem
|
||||
from extras.utils import extras_features
|
||||
from netbox.models import OrganizationalModel, PrimaryModel
|
||||
from utilities.choices import ColorChoices
|
||||
from utilities.fields import ColorField, NaturalOrderingField
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.mptt import TreeManager
|
||||
from utilities.utils import array_to_string, serialize_object
|
||||
from utilities.utils import array_to_string
|
||||
from .device_components import PowerOutlet, PowerPort
|
||||
from .devices import Device
|
||||
from .power import PowerFeed
|
||||
|
||||
__all__ = (
|
||||
'Rack',
|
||||
'RackGroup',
|
||||
'RackReservation',
|
||||
'RackRole',
|
||||
)
|
||||
@ -39,90 +35,8 @@ __all__ = (
|
||||
# Racks
|
||||
#
|
||||
|
||||
@extras_features('export_templates')
|
||||
class RackGroup(MPTTModel, ChangeLoggedModel):
|
||||
"""
|
||||
Racks can be grouped as subsets within a Site. The scope of a group will depend on how Sites are defined. For
|
||||
example, if a Site spans a corporate campus, a RackGroup might be defined to represent each building within that
|
||||
campus. If a Site instead represents a single building, a RackGroup might represent a single room or floor.
|
||||
"""
|
||||
name = models.CharField(
|
||||
max_length=100
|
||||
)
|
||||
slug = models.SlugField(
|
||||
max_length=100
|
||||
)
|
||||
site = models.ForeignKey(
|
||||
to='dcim.Site',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='rack_groups'
|
||||
)
|
||||
parent = TreeForeignKey(
|
||||
to='self',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='children',
|
||||
blank=True,
|
||||
null=True,
|
||||
db_index=True
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = TreeManager()
|
||||
|
||||
csv_headers = ['site', 'parent', 'name', 'slug', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['site', 'name']
|
||||
unique_together = [
|
||||
['site', 'name'],
|
||||
['site', 'slug'],
|
||||
]
|
||||
|
||||
class MPTTMeta:
|
||||
order_insertion_by = ['name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk)
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.site,
|
||||
self.parent.name if self.parent else '',
|
||||
self.name,
|
||||
self.slug,
|
||||
self.description,
|
||||
)
|
||||
|
||||
def to_objectchange(self, action):
|
||||
# Remove MPTT-internal fields
|
||||
return ObjectChange(
|
||||
changed_object=self,
|
||||
object_repr=str(self),
|
||||
action=action,
|
||||
object_data=serialize_object(self, exclude=['level', 'lft', 'rght', 'tree_id'])
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# An MPTT model cannot be its own parent
|
||||
if self.pk and self.parent_id == self.pk:
|
||||
raise ValidationError({
|
||||
"parent": "Cannot assign self as parent."
|
||||
})
|
||||
|
||||
# Parent RackGroup (if any) must belong to the same Site
|
||||
if self.parent and self.parent.site != self.site:
|
||||
raise ValidationError(f"Parent rack group ({self.parent}) must belong to the same site ({self.site})")
|
||||
|
||||
|
||||
class RackRole(ChangeLoggedModel):
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
class RackRole(OrganizationalModel):
|
||||
"""
|
||||
Racks can be organized by functional role, similar to Devices.
|
||||
"""
|
||||
@ -153,7 +67,7 @@ class RackRole(ChangeLoggedModel):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return "{}?role={}".format(reverse('dcim:rack_list'), self.slug)
|
||||
return reverse('dcim:rackrole', args=[self.pk])
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
@ -165,10 +79,10 @@ class RackRole(ChangeLoggedModel):
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
class Rack(PrimaryModel):
|
||||
"""
|
||||
Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
|
||||
Each Rack is assigned to a Site and (optionally) a RackGroup.
|
||||
Each Rack is assigned to a Site and (optionally) a Location.
|
||||
"""
|
||||
name = models.CharField(
|
||||
max_length=100
|
||||
@ -190,13 +104,12 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
on_delete=models.PROTECT,
|
||||
related_name='racks'
|
||||
)
|
||||
group = models.ForeignKey(
|
||||
to='dcim.RackGroup',
|
||||
location = models.ForeignKey(
|
||||
to='dcim.Location',
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='racks',
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text='Assigned group'
|
||||
null=True
|
||||
)
|
||||
tenant = models.ForeignKey(
|
||||
to='tenancy.Tenant',
|
||||
@ -275,25 +188,24 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
images = GenericRelation(
|
||||
to='extras.ImageAttachment'
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = [
|
||||
'site', 'group', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag', 'width',
|
||||
'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag', 'width',
|
||||
'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments',
|
||||
]
|
||||
clone_fields = [
|
||||
'site', 'group', 'tenant', 'status', 'role', 'type', 'width', 'u_height', 'desc_units', 'outer_width',
|
||||
'site', 'location', 'tenant', 'status', 'role', 'type', 'width', 'u_height', 'desc_units', 'outer_width',
|
||||
'outer_depth', 'outer_unit',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
ordering = ('site', 'group', '_name', 'pk') # (site, group, name) may be non-unique
|
||||
ordering = ('site', 'location', '_name', 'pk') # (site, location, name) may be non-unique
|
||||
unique_together = (
|
||||
# Name and facility_id must be unique *only* within a RackGroup
|
||||
('group', 'name'),
|
||||
('group', 'facility_id'),
|
||||
# Name and facility_id must be unique *only* within a Location
|
||||
('location', 'name'),
|
||||
('location', 'facility_id'),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
@ -305,9 +217,9 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Validate group/site assignment
|
||||
if self.site and self.group and self.group.site != self.site:
|
||||
raise ValidationError(f"Assigned rack group must belong to parent site ({self.site}).")
|
||||
# Validate location/site assignment
|
||||
if self.site and self.location and self.location.site != self.site:
|
||||
raise ValidationError(f"Assigned location must belong to parent site ({self.site}).")
|
||||
|
||||
# Validate outer dimensions and unit
|
||||
if (self.outer_width is not None or self.outer_depth is not None) and not self.outer_unit:
|
||||
@ -330,17 +242,17 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
min_height
|
||||
)
|
||||
})
|
||||
# Validate that Rack was assigned a group of its same site, if applicable
|
||||
if self.group:
|
||||
if self.group.site != self.site:
|
||||
# Validate that Rack was assigned a Location of its same site, if applicable
|
||||
if self.location:
|
||||
if self.location.site != self.site:
|
||||
raise ValidationError({
|
||||
'group': "Rack group must be from the same site, {}.".format(self.site)
|
||||
'location': f"Location must be from the same site, {self.site}."
|
||||
})
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.site.name,
|
||||
self.group.name if self.group else None,
|
||||
self.location.name if self.location else None,
|
||||
self.name,
|
||||
self.facility_id,
|
||||
self.tenant.name if self.tenant else None,
|
||||
@ -556,7 +468,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class RackReservation(ChangeLoggedModel, CustomFieldModel):
|
||||
class RackReservation(PrimaryModel):
|
||||
"""
|
||||
One or more reserved units within a Rack.
|
||||
"""
|
||||
@ -582,11 +494,10 @@ class RackReservation(ChangeLoggedModel, CustomFieldModel):
|
||||
description = models.CharField(
|
||||
max_length=200
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['site', 'rack_group', 'rack', 'units', 'tenant', 'user', 'description']
|
||||
csv_headers = ['site', 'location', 'rack', 'units', 'tenant', 'user', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['created', 'pk']
|
||||
@ -627,7 +538,7 @@ class RackReservation(ChangeLoggedModel, CustomFieldModel):
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.rack.site.name,
|
||||
self.rack.group if self.rack.group else None,
|
||||
self.rack.location if self.rack.location else None,
|
||||
self.rack.name,
|
||||
','.join([str(u) for u in self.units]),
|
||||
self.tenant.name if self.tenant else None,
|
||||
|
||||
@ -1,24 +1,24 @@
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
from taggit.managers import TaggableManager
|
||||
from mptt.models import TreeForeignKey
|
||||
from timezone_field import TimeZoneField
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from django.core.exceptions import ValidationError
|
||||
from dcim.fields import ASNField
|
||||
from extras.models import ChangeLoggedModel, CustomFieldModel, ObjectChange, TaggedItem
|
||||
from extras.utils import extras_features
|
||||
from netbox.models import NestedGroupModel, PrimaryModel
|
||||
from utilities.fields import NaturalOrderingField
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.mptt import TreeManager
|
||||
from utilities.utils import serialize_object
|
||||
|
||||
__all__ = (
|
||||
'Location',
|
||||
'Region',
|
||||
'Site',
|
||||
'SiteGroup',
|
||||
)
|
||||
|
||||
|
||||
@ -26,10 +26,12 @@ __all__ = (
|
||||
# Regions
|
||||
#
|
||||
|
||||
@extras_features('export_templates', 'webhooks')
|
||||
class Region(MPTTModel, ChangeLoggedModel):
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
class Region(NestedGroupModel):
|
||||
"""
|
||||
Sites can be grouped within geographic Regions.
|
||||
A region represents a geographic collection of sites. For example, you might create regions representing countries,
|
||||
states, and/or cities. Regions are recursively nested into a hierarchy: all sites belonging to a child region are
|
||||
also considered to be members of its parent and ancestor region(s).
|
||||
"""
|
||||
parent = TreeForeignKey(
|
||||
to='self',
|
||||
@ -52,18 +54,10 @@ class Region(MPTTModel, ChangeLoggedModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = TreeManager()
|
||||
|
||||
csv_headers = ['name', 'slug', 'parent', 'description']
|
||||
|
||||
class MPTTMeta:
|
||||
order_insertion_by = ['name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return "{}?region={}".format(reverse('dcim:site_list'), self.slug)
|
||||
return reverse('dcim:region', args=[self.pk])
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
@ -79,23 +73,57 @@ class Region(MPTTModel, ChangeLoggedModel):
|
||||
Q(region__in=self.get_descendants())
|
||||
).count()
|
||||
|
||||
def to_objectchange(self, action):
|
||||
# Remove MPTT-internal fields
|
||||
return ObjectChange(
|
||||
changed_object=self,
|
||||
object_repr=str(self),
|
||||
action=action,
|
||||
object_data=serialize_object(self, exclude=['level', 'lft', 'rght', 'tree_id'])
|
||||
|
||||
#
|
||||
# Site groups
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
class SiteGroup(NestedGroupModel):
|
||||
"""
|
||||
A site group is an arbitrary grouping of sites. For example, you might have corporate sites and customer sites; and
|
||||
within corporate sites you might distinguish between offices and data centers. Like regions, site groups can be
|
||||
nested recursively to form a hierarchy.
|
||||
"""
|
||||
parent = TreeForeignKey(
|
||||
to='self',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='children',
|
||||
blank=True,
|
||||
null=True,
|
||||
db_index=True
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=100,
|
||||
unique=True
|
||||
)
|
||||
slug = models.SlugField(
|
||||
max_length=100,
|
||||
unique=True
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
csv_headers = ['name', 'slug', 'parent', 'description']
|
||||
|
||||
# An MPTT model cannot be its own parent
|
||||
if self.pk and self.parent_id == self.pk:
|
||||
raise ValidationError({
|
||||
"parent": "Cannot assign self as parent."
|
||||
})
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:sitegroup', args=[self.pk])
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
self.parent.name if self.parent else None,
|
||||
self.description,
|
||||
)
|
||||
|
||||
def get_site_count(self):
|
||||
return Site.objects.filter(
|
||||
Q(group=self) |
|
||||
Q(group__in=self.get_descendants())
|
||||
).count()
|
||||
|
||||
|
||||
#
|
||||
@ -103,7 +131,7 @@ class Region(MPTTModel, ChangeLoggedModel):
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class Site(ChangeLoggedModel, CustomFieldModel):
|
||||
class Site(PrimaryModel):
|
||||
"""
|
||||
A Site represents a geographic location within a network; typically a building or campus. The optional facility
|
||||
field can be used to include an external designation, such as a data center name (e.g. Equinix SV6).
|
||||
@ -133,6 +161,13 @@ class Site(ChangeLoggedModel, CustomFieldModel):
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
group = models.ForeignKey(
|
||||
to='dcim.SiteGroup',
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='sites',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
tenant = models.ForeignKey(
|
||||
to='tenancy.Tenant',
|
||||
on_delete=models.PROTECT,
|
||||
@ -198,16 +233,16 @@ class Site(ChangeLoggedModel, CustomFieldModel):
|
||||
images = GenericRelation(
|
||||
to='extras.ImageAttachment'
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = [
|
||||
'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address',
|
||||
'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', 'contact_email', 'comments',
|
||||
'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'asn', 'time_zone', 'description',
|
||||
'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone',
|
||||
'contact_email', 'comments',
|
||||
]
|
||||
clone_fields = [
|
||||
'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address',
|
||||
'status', 'region', 'group', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address',
|
||||
'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', 'contact_email',
|
||||
]
|
||||
|
||||
@ -218,7 +253,7 @@ class Site(ChangeLoggedModel, CustomFieldModel):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:site', args=[self.slug])
|
||||
return reverse('dcim:site', args=[self.pk])
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
@ -226,6 +261,7 @@ class Site(ChangeLoggedModel, CustomFieldModel):
|
||||
self.slug,
|
||||
self.get_status_display(),
|
||||
self.region.name if self.region else None,
|
||||
self.group.name if self.group else None,
|
||||
self.tenant.name if self.tenant else None,
|
||||
self.facility,
|
||||
self.asn,
|
||||
@ -243,3 +279,70 @@ class Site(ChangeLoggedModel, CustomFieldModel):
|
||||
|
||||
def get_status_class(self):
|
||||
return SiteStatusChoices.CSS_CLASSES.get(self.status)
|
||||
|
||||
|
||||
#
|
||||
# Locations
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
class Location(NestedGroupModel):
|
||||
"""
|
||||
A Location represents a subgroup of Racks and/or Devices within a Site. A Location may represent a building within a
|
||||
site, or a room within a building, for example.
|
||||
"""
|
||||
name = models.CharField(
|
||||
max_length=100
|
||||
)
|
||||
slug = models.SlugField(
|
||||
max_length=100
|
||||
)
|
||||
site = models.ForeignKey(
|
||||
to='dcim.Site',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='locations'
|
||||
)
|
||||
parent = TreeForeignKey(
|
||||
to='self',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='children',
|
||||
blank=True,
|
||||
null=True,
|
||||
db_index=True
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
images = GenericRelation(
|
||||
to='extras.ImageAttachment'
|
||||
)
|
||||
|
||||
csv_headers = ['site', 'parent', 'name', 'slug', 'description']
|
||||
clone_fields = ['site', 'parent', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['site', 'name']
|
||||
unique_together = [
|
||||
['site', 'name'],
|
||||
['site', 'slug'],
|
||||
]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:location', args=[self.pk])
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.site,
|
||||
self.parent.name if self.parent else '',
|
||||
self.name,
|
||||
self.slug,
|
||||
self.description,
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Parent Location (if any) must belong to the same Site
|
||||
if self.parent and self.parent.site != self.site:
|
||||
raise ValidationError(f"Parent location ({self.parent}) must belong to the same site ({self.site})")
|
||||
|
||||
@ -7,7 +7,7 @@ from django.db import transaction
|
||||
from django.dispatch import receiver
|
||||
|
||||
from .choices import CableStatusChoices
|
||||
from .models import Cable, CablePath, Device, PathEndpoint, PowerPanel, Rack, RackGroup, VirtualChassis
|
||||
from .models import Cable, CablePath, Device, PathEndpoint, PowerPanel, Rack, Location, VirtualChassis
|
||||
|
||||
|
||||
def create_cablepath(node):
|
||||
@ -37,36 +37,30 @@ def rebuild_paths(obj):
|
||||
|
||||
|
||||
#
|
||||
# Site/rack/device assignment
|
||||
# Location/rack/device assignment
|
||||
#
|
||||
|
||||
@receiver(post_save, sender=RackGroup)
|
||||
def handle_rackgroup_site_change(instance, created, **kwargs):
|
||||
@receiver(post_save, sender=Location)
|
||||
def handle_location_site_change(instance, created, **kwargs):
|
||||
"""
|
||||
Update child RackGroups and Racks if Site assignment has changed. We intentionally recurse through each child
|
||||
Update child objects if Site assignment has changed. We intentionally recurse through each child
|
||||
object instead of calling update() on the QuerySet to ensure the proper change records get created for each.
|
||||
"""
|
||||
if not created:
|
||||
for rackgroup in instance.get_children():
|
||||
rackgroup.site = instance.site
|
||||
rackgroup.save()
|
||||
for rack in Rack.objects.filter(group=instance).exclude(site=instance.site):
|
||||
rack.site = instance.site
|
||||
rack.save()
|
||||
for powerpanel in PowerPanel.objects.filter(rack_group=instance).exclude(site=instance.site):
|
||||
powerpanel.site = instance.site
|
||||
powerpanel.save()
|
||||
instance.get_descendants().update(site=instance.site)
|
||||
locations = instance.get_descendants(include_self=True).values_list('pk', flat=True)
|
||||
Rack.objects.filter(location__in=locations).update(site=instance.site)
|
||||
Device.objects.filter(location__in=locations).update(site=instance.site)
|
||||
PowerPanel.objects.filter(location__in=locations).update(site=instance.site)
|
||||
|
||||
|
||||
@receiver(post_save, sender=Rack)
|
||||
def handle_rack_site_change(instance, created, **kwargs):
|
||||
"""
|
||||
Update child Devices if Site assignment has changed.
|
||||
Update child Devices if Site or Location assignment has changed.
|
||||
"""
|
||||
if not created:
|
||||
for device in Device.objects.filter(rack=instance).exclude(site=instance.site):
|
||||
device.site = instance.site
|
||||
device.save()
|
||||
Device.objects.filter(rack=instance).update(site=instance.site, location=instance.location)
|
||||
|
||||
|
||||
#
|
||||
|
||||
@ -26,9 +26,10 @@ class CableTable(BaseTable):
|
||||
orderable=False,
|
||||
verbose_name='Side A'
|
||||
)
|
||||
termination_a = tables.LinkColumn(
|
||||
termination_a = tables.Column(
|
||||
accessor=Accessor('termination_a'),
|
||||
orderable=False,
|
||||
linkify=True,
|
||||
verbose_name='Termination A'
|
||||
)
|
||||
termination_b_parent = tables.TemplateColumn(
|
||||
@ -37,9 +38,10 @@ class CableTable(BaseTable):
|
||||
orderable=False,
|
||||
verbose_name='Side B'
|
||||
)
|
||||
termination_b = tables.LinkColumn(
|
||||
termination_b = tables.Column(
|
||||
accessor=Accessor('termination_b'),
|
||||
orderable=False,
|
||||
linkify=True,
|
||||
verbose_name='Termination B'
|
||||
)
|
||||
status = ChoiceFieldColumn()
|
||||
|
||||
@ -6,7 +6,7 @@ from dcim.models import (
|
||||
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, FrontPort, Interface, InventoryItem, Platform,
|
||||
PowerOutlet, PowerPort, RearPort, VirtualChassis,
|
||||
)
|
||||
from tenancy.tables import COL_TENANT
|
||||
from tenancy.tables import TenantColumn
|
||||
from utilities.tables import (
|
||||
BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn,
|
||||
TagColumn, ToggleColumn,
|
||||
@ -44,25 +44,36 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
def get_cabletermination_row_class(record):
|
||||
if record.mark_connected:
|
||||
return 'success'
|
||||
elif record.cable:
|
||||
return record.cable.get_status_class()
|
||||
return ''
|
||||
|
||||
|
||||
#
|
||||
# Device roles
|
||||
#
|
||||
|
||||
class DeviceRoleTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
device_count = LinkedCountColumn(
|
||||
viewname='dcim:device_list',
|
||||
url_params={'role': 'slug'},
|
||||
url_params={'role_id': 'pk'},
|
||||
verbose_name='Devices'
|
||||
)
|
||||
vm_count = LinkedCountColumn(
|
||||
viewname='virtualization:virtualmachine_list',
|
||||
url_params={'role': 'slug'},
|
||||
url_params={'role_id': 'pk'},
|
||||
verbose_name='VMs'
|
||||
)
|
||||
color = ColorColumn()
|
||||
vm_role = BooleanColumn()
|
||||
actions = ButtonsColumn(DeviceRole, pk_field='slug')
|
||||
actions = ButtonsColumn(DeviceRole)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = DeviceRole
|
||||
@ -76,17 +87,20 @@ class DeviceRoleTable(BaseTable):
|
||||
|
||||
class PlatformTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
device_count = LinkedCountColumn(
|
||||
viewname='dcim:device_list',
|
||||
url_params={'platform': 'slug'},
|
||||
url_params={'platform_id': 'pk'},
|
||||
verbose_name='Devices'
|
||||
)
|
||||
vm_count = LinkedCountColumn(
|
||||
viewname='virtualization:virtualmachine_list',
|
||||
url_params={'platform': 'slug'},
|
||||
url_params={'platform_id': 'pk'},
|
||||
verbose_name='VMs'
|
||||
)
|
||||
actions = ButtonsColumn(Platform, pk_field='slug')
|
||||
actions = ButtonsColumn(Platform)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Platform
|
||||
@ -110,23 +124,26 @@ class DeviceTable(BaseTable):
|
||||
template_code=DEVICE_LINK
|
||||
)
|
||||
status = ChoiceFieldColumn()
|
||||
tenant = tables.TemplateColumn(
|
||||
template_code=COL_TENANT
|
||||
)
|
||||
tenant = TenantColumn()
|
||||
site = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
location = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
rack = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
device_role = ColoredLabelColumn(
|
||||
verbose_name='Role'
|
||||
)
|
||||
device_type = tables.LinkColumn(
|
||||
viewname='dcim:devicetype',
|
||||
args=[Accessor('device_type__pk')],
|
||||
verbose_name='Type',
|
||||
text=lambda record: record.device_type.display_name
|
||||
manufacturer = tables.Column(
|
||||
accessor=Accessor('device_type__manufacturer'),
|
||||
linkify=True
|
||||
)
|
||||
device_type = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='Type'
|
||||
)
|
||||
if settings.PREFER_IPV4:
|
||||
primary_ip = tables.Column(
|
||||
@ -148,13 +165,11 @@ class DeviceTable(BaseTable):
|
||||
linkify=True,
|
||||
verbose_name='IPv6 Address'
|
||||
)
|
||||
cluster = tables.LinkColumn(
|
||||
viewname='virtualization:cluster',
|
||||
args=[Accessor('cluster__pk')]
|
||||
cluster = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
virtual_chassis = tables.LinkColumn(
|
||||
viewname='dcim:virtualchassis',
|
||||
args=[Accessor('virtual_chassis__pk')]
|
||||
virtual_chassis = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
vc_position = tables.Column(
|
||||
verbose_name='VC Position'
|
||||
@ -169,12 +184,13 @@ class DeviceTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Device
|
||||
fields = (
|
||||
'pk', 'name', 'status', 'tenant', 'device_role', 'device_type', 'platform', 'serial', 'asset_tag', 'site',
|
||||
'rack', 'position', 'face', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis',
|
||||
'vc_position', 'vc_priority', 'tags',
|
||||
'pk', 'name', 'status', 'tenant', 'device_role', 'manufacturer', 'device_type', 'platform', 'serial',
|
||||
'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'primary_ip', 'primary_ip4', 'primary_ip6',
|
||||
'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'tags',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type', 'primary_ip',
|
||||
'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type',
|
||||
'primary_ip',
|
||||
)
|
||||
|
||||
|
||||
@ -183,9 +199,7 @@ class DeviceImportTable(BaseTable):
|
||||
template_code=DEVICE_LINK
|
||||
)
|
||||
status = ChoiceFieldColumn()
|
||||
tenant = tables.TemplateColumn(
|
||||
template_code=COL_TENANT
|
||||
)
|
||||
tenant = TenantColumn()
|
||||
site = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
@ -221,6 +235,7 @@ class DeviceComponentTable(BaseTable):
|
||||
cable = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
mark_connected = BooleanColumn()
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
order_by = ('device', 'name')
|
||||
@ -241,11 +256,12 @@ class CableTerminationTable(BaseTable):
|
||||
orderable=False,
|
||||
verbose_name='Cable Peer'
|
||||
)
|
||||
mark_connected = BooleanColumn()
|
||||
|
||||
|
||||
class PathEndpointTable(CableTerminationTable):
|
||||
connection = tables.TemplateColumn(
|
||||
accessor='_path.destination',
|
||||
accessor='_path.last_node',
|
||||
template_code=CABLETERMINATION,
|
||||
verbose_name='Connection',
|
||||
orderable=False
|
||||
@ -253,6 +269,12 @@ class PathEndpointTable(CableTerminationTable):
|
||||
|
||||
|
||||
class ConsolePortTable(DeviceComponentTable, PathEndpointTable):
|
||||
device = tables.Column(
|
||||
linkify={
|
||||
'viewname': 'dcim:device_consoleports',
|
||||
'args': [Accessor('device_id')],
|
||||
}
|
||||
)
|
||||
tags = TagColumn(
|
||||
url_name='dcim:consoleport_list'
|
||||
)
|
||||
@ -260,10 +282,10 @@ class ConsolePortTable(DeviceComponentTable, PathEndpointTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = ConsolePort
|
||||
fields = (
|
||||
'pk', 'device', 'name', 'label', 'type', 'description', 'cable', 'cable_color', 'cable_peer', 'connection',
|
||||
'tags',
|
||||
'pk', 'device', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
|
||||
'cable_peer', 'connection', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'speed', 'description')
|
||||
|
||||
|
||||
class DeviceConsolePortTable(ConsolePortTable):
|
||||
@ -280,16 +302,22 @@ class DeviceConsolePortTable(ConsolePortTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = ConsolePort
|
||||
fields = (
|
||||
'pk', 'name', 'label', 'type', 'description', 'cable', 'cable_color', 'cable_peer', 'connection', 'tags',
|
||||
'actions'
|
||||
'pk', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
|
||||
'cable_peer', 'connection', 'tags', 'actions'
|
||||
)
|
||||
default_columns = ('pk', 'name', 'label', 'type', 'description', 'cable', 'connection', 'actions')
|
||||
default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection', 'actions')
|
||||
row_attrs = {
|
||||
'class': lambda record: record.cable.get_status_class() if record.cable else ''
|
||||
'class': get_cabletermination_row_class
|
||||
}
|
||||
|
||||
|
||||
class ConsoleServerPortTable(DeviceComponentTable, PathEndpointTable):
|
||||
device = tables.Column(
|
||||
linkify={
|
||||
'viewname': 'dcim:device_consoleserverports',
|
||||
'args': [Accessor('device_id')],
|
||||
}
|
||||
)
|
||||
tags = TagColumn(
|
||||
url_name='dcim:consoleserverport_list'
|
||||
)
|
||||
@ -297,10 +325,10 @@ class ConsoleServerPortTable(DeviceComponentTable, PathEndpointTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = ConsoleServerPort
|
||||
fields = (
|
||||
'pk', 'device', 'name', 'label', 'type', 'description', 'cable', 'cable_color', 'cable_peer', 'connection',
|
||||
'tags',
|
||||
'pk', 'device', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
|
||||
'cable_peer', 'connection', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'speed', 'description')
|
||||
|
||||
|
||||
class DeviceConsoleServerPortTable(ConsoleServerPortTable):
|
||||
@ -318,16 +346,22 @@ class DeviceConsoleServerPortTable(ConsoleServerPortTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = ConsoleServerPort
|
||||
fields = (
|
||||
'pk', 'name', 'label', 'type', 'description', 'cable', 'cable_color', 'cable_peer', 'connection', 'tags',
|
||||
'actions'
|
||||
'pk', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
|
||||
'cable_peer', 'connection', 'tags', 'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'label', 'type', 'description', 'cable', 'connection', 'actions')
|
||||
default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection', 'actions')
|
||||
row_attrs = {
|
||||
'class': lambda record: record.cable.get_status_class() if record.cable else ''
|
||||
'class': get_cabletermination_row_class
|
||||
}
|
||||
|
||||
|
||||
class PowerPortTable(DeviceComponentTable, PathEndpointTable):
|
||||
device = tables.Column(
|
||||
linkify={
|
||||
'viewname': 'dcim:device_powerports',
|
||||
'args': [Accessor('device_id')],
|
||||
}
|
||||
)
|
||||
tags = TagColumn(
|
||||
url_name='dcim:powerport_list'
|
||||
)
|
||||
@ -335,8 +369,8 @@ class PowerPortTable(DeviceComponentTable, PathEndpointTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = PowerPort
|
||||
fields = (
|
||||
'pk', 'device', 'name', 'label', 'type', 'description', 'maximum_draw', 'allocated_draw', 'cable',
|
||||
'cable_color', 'cable_peer', 'connection', 'tags',
|
||||
'pk', 'device', 'name', 'label', 'type', 'description', 'mark_connected', 'maximum_draw', 'allocated_draw',
|
||||
'cable', 'cable_color', 'cable_peer', 'connection', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description')
|
||||
|
||||
@ -356,19 +390,25 @@ class DevicePowerPortTable(PowerPortTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = PowerPort
|
||||
fields = (
|
||||
'pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'cable', 'cable_color',
|
||||
'cable_peer', 'connection', 'tags', 'actions',
|
||||
'pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'mark_connected', 'cable',
|
||||
'cable_color', 'cable_peer', 'connection', 'tags', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'cable', 'connection',
|
||||
'actions',
|
||||
)
|
||||
row_attrs = {
|
||||
'class': lambda record: record.cable.get_status_class() if record.cable else ''
|
||||
'class': get_cabletermination_row_class
|
||||
}
|
||||
|
||||
|
||||
class PowerOutletTable(DeviceComponentTable, PathEndpointTable):
|
||||
device = tables.Column(
|
||||
linkify={
|
||||
'viewname': 'dcim:device_poweroutlets',
|
||||
'args': [Accessor('device_id')],
|
||||
}
|
||||
)
|
||||
power_port = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
@ -379,8 +419,8 @@ class PowerOutletTable(DeviceComponentTable, PathEndpointTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = PowerOutlet
|
||||
fields = (
|
||||
'pk', 'device', 'name', 'label', 'type', 'description', 'power_port', 'feed_leg', 'cable', 'cable_color',
|
||||
'cable_peer', 'connection', 'tags',
|
||||
'pk', 'device', 'name', 'label', 'type', 'description', 'power_port', 'feed_leg', 'mark_connected', 'cable',
|
||||
'cable_color', 'cable_peer', 'connection', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description')
|
||||
|
||||
@ -399,14 +439,14 @@ class DevicePowerOutletTable(PowerOutletTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = PowerOutlet
|
||||
fields = (
|
||||
'pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'cable', 'cable_color',
|
||||
'cable_peer', 'connection', 'tags', 'actions',
|
||||
'pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'mark_connected', 'cable',
|
||||
'cable_color', 'cable_peer', 'connection', 'tags', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'cable', 'connection', 'actions',
|
||||
)
|
||||
row_attrs = {
|
||||
'class': lambda record: record.cable.get_status_class() if record.cable else ''
|
||||
'class': get_cabletermination_row_class
|
||||
}
|
||||
|
||||
|
||||
@ -426,6 +466,12 @@ class BaseInterfaceTable(BaseTable):
|
||||
|
||||
|
||||
class InterfaceTable(DeviceComponentTable, BaseInterfaceTable, PathEndpointTable):
|
||||
device = tables.Column(
|
||||
linkify={
|
||||
'viewname': 'dcim:device_interfaces',
|
||||
'args': [Accessor('device_id')],
|
||||
}
|
||||
)
|
||||
mgmt_only = BooleanColumn()
|
||||
tags = TagColumn(
|
||||
url_name='dcim:interface_list'
|
||||
@ -435,8 +481,8 @@ class InterfaceTable(DeviceComponentTable, BaseInterfaceTable, PathEndpointTable
|
||||
model = Interface
|
||||
fields = (
|
||||
'pk', 'device', 'name', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address',
|
||||
'description', 'cable', 'cable_color', 'cable_peer', 'connection', 'tags', 'ip_addresses', 'untagged_vlan',
|
||||
'tagged_vlans',
|
||||
'description', 'mark_connected', 'cable', 'cable_color', 'cable_peer', 'connection', 'tags', 'ip_addresses',
|
||||
'untagged_vlan', 'tagged_vlans',
|
||||
)
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'enabled', 'type', 'description')
|
||||
|
||||
@ -448,6 +494,10 @@ class DeviceInterfaceTable(InterfaceTable):
|
||||
'{% endif %}"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
|
||||
attrs={'td': {'class': 'text-nowrap'}}
|
||||
)
|
||||
parent = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='Parent'
|
||||
)
|
||||
lag = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='LAG'
|
||||
@ -461,21 +511,27 @@ class DeviceInterfaceTable(InterfaceTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = Interface
|
||||
fields = (
|
||||
'pk', 'name', 'label', 'enabled', 'type', 'lag', 'mgmt_only', 'mtu', 'mode', 'mac_address', 'description',
|
||||
'cable', 'cable_color', 'cable_peer', 'connection', 'tags', 'ip_addresses', 'untagged_vlan', 'tagged_vlans',
|
||||
'actions',
|
||||
'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mgmt_only', 'mtu', 'mode', 'mac_address',
|
||||
'description', 'mark_connected', 'cable', 'cable_color', 'cable_peer', 'connection', 'tags', 'ip_addresses',
|
||||
'untagged_vlan', 'tagged_vlans', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'label', 'enabled', 'type', 'lag', 'mtu', 'mode', 'description', 'ip_addresses', 'cable',
|
||||
'connection', 'actions',
|
||||
'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mtu', 'mode', 'description', 'ip_addresses',
|
||||
'cable', 'connection', 'actions',
|
||||
)
|
||||
row_attrs = {
|
||||
'class': lambda record: record.cable.get_status_class() if record.cable else '',
|
||||
'class': get_cabletermination_row_class,
|
||||
'data-name': lambda record: record.name,
|
||||
}
|
||||
|
||||
|
||||
class FrontPortTable(DeviceComponentTable, CableTerminationTable):
|
||||
device = tables.Column(
|
||||
linkify={
|
||||
'viewname': 'dcim:device_frontports',
|
||||
'args': [Accessor('device_id')],
|
||||
}
|
||||
)
|
||||
rear_port_position = tables.Column(
|
||||
verbose_name='Position'
|
||||
)
|
||||
@ -489,8 +545,8 @@ class FrontPortTable(DeviceComponentTable, CableTerminationTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = FrontPort
|
||||
fields = (
|
||||
'pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable',
|
||||
'cable_color', 'cable_peer', 'tags',
|
||||
'pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'mark_connected',
|
||||
'cable', 'cable_color', 'cable_peer', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description')
|
||||
|
||||
@ -510,19 +566,25 @@ class DeviceFrontPortTable(FrontPortTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = FrontPort
|
||||
fields = (
|
||||
'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'cable_color',
|
||||
'cable_peer', 'tags', 'actions',
|
||||
'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'mark_connected', 'cable',
|
||||
'cable_color', 'cable_peer', 'tags', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'cable_peer',
|
||||
'actions',
|
||||
)
|
||||
row_attrs = {
|
||||
'class': lambda record: record.cable.get_status_class() if record.cable else ''
|
||||
'class': get_cabletermination_row_class
|
||||
}
|
||||
|
||||
|
||||
class RearPortTable(DeviceComponentTable, CableTerminationTable):
|
||||
device = tables.Column(
|
||||
linkify={
|
||||
'viewname': 'dcim:device_rearports',
|
||||
'args': [Accessor('device_id')],
|
||||
}
|
||||
)
|
||||
tags = TagColumn(
|
||||
url_name='dcim:rearport_list'
|
||||
)
|
||||
@ -530,8 +592,8 @@ class RearPortTable(DeviceComponentTable, CableTerminationTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = RearPort
|
||||
fields = (
|
||||
'pk', 'device', 'name', 'label', 'type', 'positions', 'description', 'cable', 'cable_color', 'cable_peer',
|
||||
'tags',
|
||||
'pk', 'device', 'name', 'label', 'type', 'positions', 'description', 'mark_connected', 'cable',
|
||||
'cable_color', 'cable_peer', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
|
||||
|
||||
@ -551,18 +613,24 @@ class DeviceRearPortTable(RearPortTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = RearPort
|
||||
fields = (
|
||||
'pk', 'name', 'label', 'type', 'positions', 'description', 'cable', 'cable_color', 'cable_peer', 'tags',
|
||||
'actions',
|
||||
'pk', 'name', 'label', 'type', 'positions', 'description', 'mark_connected', 'cable', 'cable_color',
|
||||
'cable_peer', 'tags', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'label', 'type', 'positions', 'description', 'cable', 'cable_peer', 'actions',
|
||||
)
|
||||
row_attrs = {
|
||||
'class': lambda record: record.cable.get_status_class() if record.cable else ''
|
||||
'class': get_cabletermination_row_class
|
||||
}
|
||||
|
||||
|
||||
class DeviceBayTable(DeviceComponentTable):
|
||||
device = tables.Column(
|
||||
linkify={
|
||||
'viewname': 'dcim:device_devicebays',
|
||||
'args': [Accessor('device_id')],
|
||||
}
|
||||
)
|
||||
status = tables.TemplateColumn(
|
||||
template_code=DEVICEBAY_STATUS
|
||||
)
|
||||
@ -602,6 +670,12 @@ class DeviceDeviceBayTable(DeviceBayTable):
|
||||
|
||||
|
||||
class InventoryItemTable(DeviceComponentTable):
|
||||
device = tables.Column(
|
||||
linkify={
|
||||
'viewname': 'dcim:device_inventory',
|
||||
'args': [Accessor('device_id')],
|
||||
}
|
||||
)
|
||||
manufacturer = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
|
||||
@ -26,7 +26,9 @@ __all__ = (
|
||||
|
||||
class ManufacturerTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
devicetype_count = tables.Column(
|
||||
verbose_name='Device Types'
|
||||
)
|
||||
@ -37,7 +39,7 @@ class ManufacturerTable(BaseTable):
|
||||
verbose_name='Platforms'
|
||||
)
|
||||
slug = tables.Column()
|
||||
actions = ButtonsColumn(Manufacturer, pk_field='slug')
|
||||
actions = ButtonsColumn(Manufacturer)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Manufacturer
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from dcim.models import PowerFeed, PowerPanel
|
||||
from utilities.tables import BaseTable, ChoiceFieldColumn, LinkedCountColumn, TagColumn, ToggleColumn
|
||||
@ -17,10 +16,11 @@ __all__ = (
|
||||
|
||||
class PowerPanelTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
site = tables.LinkColumn(
|
||||
viewname='dcim:site',
|
||||
args=[Accessor('site__slug')]
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
site = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
powerfeed_count = LinkedCountColumn(
|
||||
viewname='dcim:powerfeed_list',
|
||||
@ -33,8 +33,8 @@ class PowerPanelTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerPanel
|
||||
fields = ('pk', 'name', 'site', 'rack_group', 'powerfeed_count', 'tags')
|
||||
default_columns = ('pk', 'name', 'site', 'rack_group', 'powerfeed_count')
|
||||
fields = ('pk', 'name', 'site', 'location', 'powerfeed_count', 'tags')
|
||||
default_columns = ('pk', 'name', 'site', 'location', 'powerfeed_count')
|
||||
|
||||
|
||||
#
|
||||
@ -45,7 +45,9 @@ class PowerPanelTable(BaseTable):
|
||||
# cannot traverse pass-through ports.
|
||||
class PowerFeedTable(CableTerminationTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
power_panel = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
@ -68,7 +70,8 @@ class PowerFeedTable(CableTerminationTable):
|
||||
model = PowerFeed
|
||||
fields = (
|
||||
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
|
||||
'max_utilization', 'cable', 'cable_color', 'cable_peer', 'connection', 'available_power', 'tags',
|
||||
'max_utilization', 'mark_connected', 'cable', 'cable_color', 'cable_peer', 'connection', 'available_power',
|
||||
'tags',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'cable',
|
||||
|
||||
@ -1,53 +1,21 @@
|
||||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from dcim.models import Rack, RackGroup, RackReservation, RackRole
|
||||
from tenancy.tables import COL_TENANT
|
||||
from dcim.models import Rack, RackReservation, RackRole
|
||||
from tenancy.tables import TenantColumn
|
||||
from utilities.tables import (
|
||||
BaseTable, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn, TagColumn,
|
||||
ToggleColumn,
|
||||
ToggleColumn, UtilizationColumn,
|
||||
)
|
||||
from .template_code import MPTT_LINK, RACKGROUP_ELEVATIONS, UTILIZATION_GRAPH
|
||||
|
||||
__all__ = (
|
||||
'RackTable',
|
||||
'RackDetailTable',
|
||||
'RackGroupTable',
|
||||
'RackReservationTable',
|
||||
'RackRoleTable',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Rack groups
|
||||
#
|
||||
|
||||
class RackGroupTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.TemplateColumn(
|
||||
template_code=MPTT_LINK,
|
||||
orderable=False,
|
||||
attrs={'td': {'class': 'text-nowrap'}}
|
||||
)
|
||||
site = tables.LinkColumn(
|
||||
viewname='dcim:site',
|
||||
args=[Accessor('site__slug')],
|
||||
verbose_name='Site'
|
||||
)
|
||||
rack_count = tables.Column(
|
||||
verbose_name='Racks'
|
||||
)
|
||||
actions = ButtonsColumn(
|
||||
model=RackGroup,
|
||||
prepend_template=RACKGROUP_ELEVATIONS
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RackGroup
|
||||
fields = ('pk', 'name', 'site', 'rack_count', 'description', 'slug', 'actions')
|
||||
default_columns = ('pk', 'name', 'site', 'rack_count', 'description', 'actions')
|
||||
|
||||
|
||||
#
|
||||
# Rack roles
|
||||
#
|
||||
@ -75,15 +43,13 @@ class RackTable(BaseTable):
|
||||
order_by=('_name',),
|
||||
linkify=True
|
||||
)
|
||||
group = tables.Column(
|
||||
location = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
site = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
tenant = tables.TemplateColumn(
|
||||
template_code=COL_TENANT
|
||||
)
|
||||
tenant = TenantColumn()
|
||||
status = ChoiceFieldColumn()
|
||||
role = ColoredLabelColumn()
|
||||
u_height = tables.TemplateColumn(
|
||||
@ -94,10 +60,10 @@ class RackTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Rack
|
||||
fields = (
|
||||
'pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'type',
|
||||
'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'type',
|
||||
'width', 'u_height',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'u_height')
|
||||
default_columns = ('pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'u_height')
|
||||
|
||||
|
||||
class RackDetailTable(RackTable):
|
||||
@ -106,13 +72,10 @@ class RackDetailTable(RackTable):
|
||||
url_params={'rack_id': 'pk'},
|
||||
verbose_name='Devices'
|
||||
)
|
||||
get_utilization = tables.TemplateColumn(
|
||||
template_code=UTILIZATION_GRAPH,
|
||||
orderable=False,
|
||||
get_utilization = UtilizationColumn(
|
||||
verbose_name='Space'
|
||||
)
|
||||
get_power_utilization = tables.TemplateColumn(
|
||||
template_code=UTILIZATION_GRAPH,
|
||||
get_power_utilization = UtilizationColumn(
|
||||
orderable=False,
|
||||
verbose_name='Power'
|
||||
)
|
||||
@ -122,11 +85,11 @@ class RackDetailTable(RackTable):
|
||||
|
||||
class Meta(RackTable.Meta):
|
||||
fields = (
|
||||
'pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'type',
|
||||
'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'type',
|
||||
'width', 'u_height', 'device_count', 'get_utilization', 'get_power_utilization', 'tags',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count',
|
||||
'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count',
|
||||
'get_utilization', 'get_power_utilization',
|
||||
)
|
||||
|
||||
@ -145,9 +108,7 @@ class RackReservationTable(BaseTable):
|
||||
accessor=Accessor('rack__site'),
|
||||
linkify=True
|
||||
)
|
||||
tenant = tables.TemplateColumn(
|
||||
template_code=COL_TENANT
|
||||
)
|
||||
tenant = TenantColumn()
|
||||
rack = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
|
||||
@ -1,13 +1,17 @@
|
||||
import django_tables2 as tables
|
||||
|
||||
from dcim.models import Region, Site
|
||||
from tenancy.tables import COL_TENANT
|
||||
from utilities.tables import BaseTable, ButtonsColumn, ChoiceFieldColumn, TagColumn, ToggleColumn
|
||||
from .template_code import MPTT_LINK
|
||||
from dcim.models import Location, Region, Site, SiteGroup
|
||||
from tenancy.tables import TenantColumn
|
||||
from utilities.tables import (
|
||||
BaseTable, ButtonsColumn, ChoiceFieldColumn, LinkedCountColumn, MPTTColumn, TagColumn, ToggleColumn,
|
||||
)
|
||||
from .template_code import LOCATION_ELEVATIONS
|
||||
|
||||
__all__ = (
|
||||
'LocationTable',
|
||||
'RegionTable',
|
||||
'SiteTable',
|
||||
'SiteGroupTable',
|
||||
)
|
||||
|
||||
|
||||
@ -17,12 +21,12 @@ __all__ = (
|
||||
|
||||
class RegionTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.TemplateColumn(
|
||||
template_code=MPTT_LINK,
|
||||
orderable=False,
|
||||
attrs={'td': {'class': 'text-nowrap'}}
|
||||
name = MPTTColumn(
|
||||
linkify=True
|
||||
)
|
||||
site_count = tables.Column(
|
||||
site_count = LinkedCountColumn(
|
||||
viewname='dcim:site_list',
|
||||
url_params={'region_id': 'pk'},
|
||||
verbose_name='Sites'
|
||||
)
|
||||
actions = ButtonsColumn(Region)
|
||||
@ -33,22 +37,45 @@ class RegionTable(BaseTable):
|
||||
default_columns = ('pk', 'name', 'site_count', 'description', 'actions')
|
||||
|
||||
|
||||
#
|
||||
# Site groups
|
||||
#
|
||||
|
||||
class SiteGroupTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = MPTTColumn(
|
||||
linkify=True
|
||||
)
|
||||
site_count = LinkedCountColumn(
|
||||
viewname='dcim:site_list',
|
||||
url_params={'group_id': 'pk'},
|
||||
verbose_name='Sites'
|
||||
)
|
||||
actions = ButtonsColumn(SiteGroup)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = SiteGroup
|
||||
fields = ('pk', 'name', 'slug', 'site_count', 'description', 'actions')
|
||||
default_columns = ('pk', 'name', 'site_count', 'description', 'actions')
|
||||
|
||||
|
||||
#
|
||||
# Sites
|
||||
#
|
||||
|
||||
class SiteTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn(
|
||||
order_by=('_name',)
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
status = ChoiceFieldColumn()
|
||||
region = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
tenant = tables.TemplateColumn(
|
||||
template_code=COL_TENANT
|
||||
group = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
tenant = TenantColumn()
|
||||
tags = TagColumn(
|
||||
url_name='dcim:site_list'
|
||||
)
|
||||
@ -56,8 +83,37 @@ class SiteTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Site
|
||||
fields = (
|
||||
'pk', 'name', 'slug', 'status', 'facility', 'region', 'tenant', 'asn', 'time_zone', 'description',
|
||||
'pk', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asn', 'time_zone', 'description',
|
||||
'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone',
|
||||
'contact_email', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'status', 'facility', 'region', 'tenant', 'asn', 'description')
|
||||
default_columns = ('pk', 'name', 'status', 'facility', 'region', 'group', 'tenant', 'asn', 'description')
|
||||
|
||||
|
||||
#
|
||||
# Locations
|
||||
#
|
||||
|
||||
class LocationTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = MPTTColumn(
|
||||
linkify=True
|
||||
)
|
||||
site = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
rack_count = tables.Column(
|
||||
verbose_name='Racks'
|
||||
)
|
||||
device_count = tables.Column(
|
||||
verbose_name='Devices'
|
||||
)
|
||||
actions = ButtonsColumn(
|
||||
model=Location,
|
||||
prepend_template=LOCATION_ELEVATIONS
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Location
|
||||
fields = ('pk', 'name', 'site', 'rack_count', 'device_count', 'description', 'slug', 'actions')
|
||||
default_columns = ('pk', 'name', 'site', 'rack_count', 'device_count', 'description', 'actions')
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
CABLETERMINATION = """
|
||||
{% if value %}
|
||||
<a href="{{ value.parent.get_absolute_url }}">{{ value.parent }}</a>
|
||||
{% if value.parent_object %}
|
||||
<a href="{{ value.parent_object.get_absolute_url }}">{{ value.parent_object }}</a>
|
||||
<i class="mdi mdi-chevron-right"></i>
|
||||
{% endif %}
|
||||
<a href="{{ value.get_absolute_url }}">{{ value }}</a>
|
||||
{% else %}
|
||||
—
|
||||
@ -56,13 +58,6 @@ INTERFACE_TAGGED_VLANS = """
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
MPTT_LINK = """
|
||||
{% for i in record.get_ancestors %}
|
||||
<i class="mdi mdi-circle-small"></i>
|
||||
{% endfor %}
|
||||
<a href="{{ record.get_absolute_url }}">{{ record.name }}</a>
|
||||
"""
|
||||
|
||||
POWERFEED_CABLE = """
|
||||
<a href="{{ value.get_absolute_url }}">{{ value }}</a>
|
||||
<a href="{% url 'dcim:powerfeed_trace' pk=record.pk %}" class="btn btn-primary btn-xs" title="Trace">
|
||||
@ -71,22 +66,17 @@ POWERFEED_CABLE = """
|
||||
"""
|
||||
|
||||
POWERFEED_CABLETERMINATION = """
|
||||
<a href="{{ value.parent.get_absolute_url }}">{{ value.parent }}</a>
|
||||
<a href="{{ value.parent_object.get_absolute_url }}">{{ value.parent_object }}</a>
|
||||
<i class="mdi mdi-chevron-right"></i>
|
||||
<a href="{{ value.get_absolute_url }}">{{ value }}</a>
|
||||
"""
|
||||
|
||||
RACKGROUP_ELEVATIONS = """
|
||||
<a href="{% url 'dcim:rack_elevation_list' %}?site={{ record.site.slug }}&group_id={{ record.pk }}" class="btn btn-xs btn-primary" title="View elevations">
|
||||
LOCATION_ELEVATIONS = """
|
||||
<a href="{% url 'dcim:rack_elevation_list' %}?site={{ record.site.slug }}&location_id={{ record.pk }}" class="btn btn-xs btn-primary" title="View elevations">
|
||||
<i class="mdi mdi-server"></i>
|
||||
</a>
|
||||
"""
|
||||
|
||||
UTILIZATION_GRAPH = """
|
||||
{% load helpers %}
|
||||
{% utilization_graph value %}
|
||||
"""
|
||||
|
||||
#
|
||||
# Device component buttons
|
||||
#
|
||||
@ -103,6 +93,7 @@ CONSOLEPORT_BUTTONS = """
|
||||
{% elif perms.dcim.add_cable %}
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||
{% if not record.mark_connected %}
|
||||
<span class="dropdown">
|
||||
<button type="button" class="btn btn-success btn-xs dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
|
||||
@ -113,6 +104,9 @@ CONSOLEPORT_BUTTONS = """
|
||||
<li><a href="{% url 'dcim:consoleport_connect' termination_a_id=record.pk termination_b_type='rear-port' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}">Rear Port</a></li>
|
||||
</ul>
|
||||
</span>
|
||||
{% else %}
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
@ -128,6 +122,7 @@ CONSOLESERVERPORT_BUTTONS = """
|
||||
{% elif perms.dcim.add_cable %}
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||
{% if not record.mark_connected %}
|
||||
<span class="dropdown">
|
||||
<button type="button" class="btn btn-success btn-xs dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
|
||||
@ -138,6 +133,9 @@ CONSOLESERVERPORT_BUTTONS = """
|
||||
<li><a href="{% url 'dcim:consoleserverport_connect' termination_a_id=record.pk termination_b_type='rear-port' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}">Rear Port</a></li>
|
||||
</ul>
|
||||
</span>
|
||||
{% else %}
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
@ -153,6 +151,7 @@ POWERPORT_BUTTONS = """
|
||||
{% elif perms.dcim.add_cable %}
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||
{% if not record.mark_connected %}
|
||||
<span class="dropdown">
|
||||
<button type="button" class="btn btn-success btn-xs dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
|
||||
@ -162,6 +161,9 @@ POWERPORT_BUTTONS = """
|
||||
<li><a href="{% url 'dcim:powerport_connect' termination_a_id=record.pk termination_b_type='power-feed' %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}">Power Feed</a></li>
|
||||
</ul>
|
||||
</span>
|
||||
{% else %}
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
@ -177,9 +179,13 @@ POWEROUTLET_BUTTONS = """
|
||||
{% elif perms.dcim.add_cable %}
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||
{% if not record.mark_connected %}
|
||||
<a href="{% url 'dcim:poweroutlet_connect' termination_a_id=record.pk termination_b_type='power-port' %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" title="Connect" class="btn btn-success btn-xs">
|
||||
<i class="mdi mdi-ethernet-cable" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
@ -200,6 +206,7 @@ INTERFACE_BUTTONS = """
|
||||
{% elif record.is_connectable and perms.dcim.add_cable %}
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||
{% if not record.mark_connected %}
|
||||
<span class="dropdown">
|
||||
<button type="button" class="btn btn-success btn-xs dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
|
||||
@ -211,6 +218,9 @@ INTERFACE_BUTTONS = """
|
||||
<li><a href="{% url 'dcim:interface_connect' termination_a_id=record.pk termination_b_type='circuit-termination' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Circuit Termination</a></li>
|
||||
</ul>
|
||||
</span>
|
||||
{% else %}
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
@ -226,6 +236,7 @@ FRONTPORT_BUTTONS = """
|
||||
{% elif perms.dcim.add_cable %}
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||
{% if not record.mark_connected %}
|
||||
<span class="dropdown">
|
||||
<button type="button" class="btn btn-success btn-xs dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
|
||||
@ -239,6 +250,9 @@ FRONTPORT_BUTTONS = """
|
||||
<li><a href="{% url 'dcim:frontport_connect' termination_a_id=record.pk termination_b_type='circuit-termination' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}">Circuit Termination</a></li>
|
||||
</ul>
|
||||
</span>
|
||||
{% else %}
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
@ -254,6 +268,7 @@ REARPORT_BUTTONS = """
|
||||
{% elif perms.dcim.add_cable %}
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||
{% if not record.mark_connected %}
|
||||
<span class="dropdown">
|
||||
<button type="button" class="btn btn-success btn-xs dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
|
||||
@ -265,6 +280,9 @@ REARPORT_BUTTONS = """
|
||||
<li><a href="{% url 'dcim:rearport_connect' termination_a_id=record.pk termination_b_type='circuit-termination' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}">Circuit Termination</a></li>
|
||||
</ul>
|
||||
</span>
|
||||
{% else %}
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
|
||||
@ -4,12 +4,7 @@ from rest_framework import status
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import (
|
||||
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, Manufacturer,
|
||||
InventoryItem, Platform, PowerFeed, PowerPort, PowerPortTemplate, PowerOutlet, PowerOutletTemplate, PowerPanel,
|
||||
Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
|
||||
)
|
||||
from dcim.models import *
|
||||
from ipam.models import VLAN
|
||||
from utilities.testing import APITestCase, APIViewTestCases
|
||||
from virtualization.models import Cluster, ClusterType
|
||||
@ -64,7 +59,7 @@ class Mixins:
|
||||
|
||||
class RegionTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Region
|
||||
brief_fields = ['_depth', 'id', 'name', 'site_count', 'slug', 'url']
|
||||
brief_fields = ['_depth', 'display', 'id', 'name', 'site_count', 'slug', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'name': 'Region 4',
|
||||
@ -91,9 +86,38 @@ class RegionTest(APIViewTestCases.APIViewTestCase):
|
||||
Region.objects.create(name='Region 3', slug='region-3')
|
||||
|
||||
|
||||
class SiteGroupTest(APIViewTestCases.APIViewTestCase):
|
||||
model = SiteGroup
|
||||
brief_fields = ['_depth', 'display', 'id', 'name', 'site_count', 'slug', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'name': 'Site Group 4',
|
||||
'slug': 'site-group-4',
|
||||
},
|
||||
{
|
||||
'name': 'Site Group 5',
|
||||
'slug': 'site-group-5',
|
||||
},
|
||||
{
|
||||
'name': 'Site Group 6',
|
||||
'slug': 'site-group-6',
|
||||
},
|
||||
]
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
SiteGroup.objects.create(name='Site Group 1', slug='site-group-1')
|
||||
SiteGroup.objects.create(name='Site Group 2', slug='site-group-2')
|
||||
SiteGroup.objects.create(name='Site Group 3', slug='site-group-3')
|
||||
|
||||
|
||||
class SiteTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Site
|
||||
brief_fields = ['id', 'name', 'slug', 'url']
|
||||
brief_fields = ['display', 'id', 'name', 'slug', 'url']
|
||||
bulk_update_data = {
|
||||
'status': 'planned',
|
||||
}
|
||||
@ -102,14 +126,19 @@ class SiteTest(APIViewTestCases.APIViewTestCase):
|
||||
def setUpTestData(cls):
|
||||
|
||||
regions = (
|
||||
Region.objects.create(name='Test Region 1', slug='test-region-1'),
|
||||
Region.objects.create(name='Test Region 2', slug='test-region-2'),
|
||||
Region.objects.create(name='Region 1', slug='region-1'),
|
||||
Region.objects.create(name='Region 2', slug='region-2'),
|
||||
)
|
||||
|
||||
groups = (
|
||||
SiteGroup.objects.create(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup.objects.create(name='Site Group 2', slug='site-group-2'),
|
||||
)
|
||||
|
||||
sites = (
|
||||
Site(region=regions[0], name='Site 1', slug='site-1'),
|
||||
Site(region=regions[0], name='Site 2', slug='site-2'),
|
||||
Site(region=regions[0], name='Site 3', slug='site-3'),
|
||||
Site(region=regions[0], group=groups[0], name='Site 1', slug='site-1'),
|
||||
Site(region=regions[0], group=groups[0], name='Site 2', slug='site-2'),
|
||||
Site(region=regions[0], group=groups[0], name='Site 3', slug='site-3'),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
@ -118,26 +147,29 @@ class SiteTest(APIViewTestCases.APIViewTestCase):
|
||||
'name': 'Site 4',
|
||||
'slug': 'site-4',
|
||||
'region': regions[1].pk,
|
||||
'group': groups[1].pk,
|
||||
'status': SiteStatusChoices.STATUS_ACTIVE,
|
||||
},
|
||||
{
|
||||
'name': 'Site 5',
|
||||
'slug': 'site-5',
|
||||
'region': regions[1].pk,
|
||||
'group': groups[1].pk,
|
||||
'status': SiteStatusChoices.STATUS_ACTIVE,
|
||||
},
|
||||
{
|
||||
'name': 'Site 6',
|
||||
'slug': 'site-6',
|
||||
'region': regions[1].pk,
|
||||
'group': groups[1].pk,
|
||||
'status': SiteStatusChoices.STATUS_ACTIVE,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class RackGroupTest(APIViewTestCases.APIViewTestCase):
|
||||
model = RackGroup
|
||||
brief_fields = ['_depth', 'id', 'name', 'rack_count', 'slug', 'url']
|
||||
class LocationTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Location
|
||||
brief_fields = ['_depth', 'display', 'id', 'name', 'rack_count', 'slug', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@ -151,40 +183,40 @@ class RackGroupTest(APIViewTestCases.APIViewTestCase):
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
parent_rack_groups = (
|
||||
RackGroup.objects.create(site=sites[0], name='Parent Rack Group 1', slug='parent-rack-group-1'),
|
||||
RackGroup.objects.create(site=sites[1], name='Parent Rack Group 2', slug='parent-rack-group-2'),
|
||||
parent_locations = (
|
||||
Location.objects.create(site=sites[0], name='Parent Location 1', slug='parent-location-1'),
|
||||
Location.objects.create(site=sites[1], name='Parent Location 2', slug='parent-location-2'),
|
||||
)
|
||||
|
||||
RackGroup.objects.create(site=sites[0], name='Rack Group 1', slug='rack-group-1', parent=parent_rack_groups[0])
|
||||
RackGroup.objects.create(site=sites[0], name='Rack Group 2', slug='rack-group-2', parent=parent_rack_groups[0])
|
||||
RackGroup.objects.create(site=sites[0], name='Rack Group 3', slug='rack-group-3', parent=parent_rack_groups[0])
|
||||
Location.objects.create(site=sites[0], name='Location 1', slug='location-1', parent=parent_locations[0])
|
||||
Location.objects.create(site=sites[0], name='Location 2', slug='location-2', parent=parent_locations[0])
|
||||
Location.objects.create(site=sites[0], name='Location 3', slug='location-3', parent=parent_locations[0])
|
||||
|
||||
cls.create_data = [
|
||||
{
|
||||
'name': 'Test Rack Group 4',
|
||||
'slug': 'test-rack-group-4',
|
||||
'name': 'Test Location 4',
|
||||
'slug': 'test-location-4',
|
||||
'site': sites[1].pk,
|
||||
'parent': parent_rack_groups[1].pk,
|
||||
'parent': parent_locations[1].pk,
|
||||
},
|
||||
{
|
||||
'name': 'Test Rack Group 5',
|
||||
'slug': 'test-rack-group-5',
|
||||
'name': 'Test Location 5',
|
||||
'slug': 'test-location-5',
|
||||
'site': sites[1].pk,
|
||||
'parent': parent_rack_groups[1].pk,
|
||||
'parent': parent_locations[1].pk,
|
||||
},
|
||||
{
|
||||
'name': 'Test Rack Group 6',
|
||||
'slug': 'test-rack-group-6',
|
||||
'name': 'Test Location 6',
|
||||
'slug': 'test-location-6',
|
||||
'site': sites[1].pk,
|
||||
'parent': parent_rack_groups[1].pk,
|
||||
'parent': parent_locations[1].pk,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class RackRoleTest(APIViewTestCases.APIViewTestCase):
|
||||
model = RackRole
|
||||
brief_fields = ['id', 'name', 'rack_count', 'slug', 'url']
|
||||
brief_fields = ['display', 'id', 'name', 'rack_count', 'slug', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'name': 'Rack Role 4',
|
||||
@ -219,7 +251,7 @@ class RackRoleTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class RackTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Rack
|
||||
brief_fields = ['device_count', 'display_name', 'id', 'name', 'url']
|
||||
brief_fields = ['device_count', 'display', 'display_name', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'status': 'planned',
|
||||
}
|
||||
@ -233,9 +265,9 @@ class RackTest(APIViewTestCases.APIViewTestCase):
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
rack_groups = (
|
||||
RackGroup.objects.create(site=sites[0], name='Rack Group 1', slug='rack-group-1'),
|
||||
RackGroup.objects.create(site=sites[1], name='Rack Group 2', slug='rack-group-2'),
|
||||
locations = (
|
||||
Location.objects.create(site=sites[0], name='Location 1', slug='location-1'),
|
||||
Location.objects.create(site=sites[1], name='Location 2', slug='location-2'),
|
||||
)
|
||||
|
||||
rack_roles = (
|
||||
@ -245,9 +277,9 @@ class RackTest(APIViewTestCases.APIViewTestCase):
|
||||
RackRole.objects.bulk_create(rack_roles)
|
||||
|
||||
racks = (
|
||||
Rack(site=sites[0], group=rack_groups[0], role=rack_roles[0], name='Rack 1'),
|
||||
Rack(site=sites[0], group=rack_groups[0], role=rack_roles[0], name='Rack 2'),
|
||||
Rack(site=sites[0], group=rack_groups[0], role=rack_roles[0], name='Rack 3'),
|
||||
Rack(site=sites[0], location=locations[0], role=rack_roles[0], name='Rack 1'),
|
||||
Rack(site=sites[0], location=locations[0], role=rack_roles[0], name='Rack 2'),
|
||||
Rack(site=sites[0], location=locations[0], role=rack_roles[0], name='Rack 3'),
|
||||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
@ -255,19 +287,19 @@ class RackTest(APIViewTestCases.APIViewTestCase):
|
||||
{
|
||||
'name': 'Test Rack 4',
|
||||
'site': sites[1].pk,
|
||||
'group': rack_groups[1].pk,
|
||||
'location': locations[1].pk,
|
||||
'role': rack_roles[1].pk,
|
||||
},
|
||||
{
|
||||
'name': 'Test Rack 5',
|
||||
'site': sites[1].pk,
|
||||
'group': rack_groups[1].pk,
|
||||
'location': locations[1].pk,
|
||||
'role': rack_roles[1].pk,
|
||||
},
|
||||
{
|
||||
'name': 'Test Rack 6',
|
||||
'site': sites[1].pk,
|
||||
'group': rack_groups[1].pk,
|
||||
'location': locations[1].pk,
|
||||
'role': rack_roles[1].pk,
|
||||
},
|
||||
]
|
||||
@ -307,7 +339,7 @@ class RackTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class RackReservationTest(APIViewTestCases.APIViewTestCase):
|
||||
model = RackReservation
|
||||
brief_fields = ['id', 'units', 'url', 'user']
|
||||
brief_fields = ['display', 'id', 'units', 'url', 'user']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@ -358,7 +390,7 @@ class RackReservationTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class ManufacturerTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Manufacturer
|
||||
brief_fields = ['devicetype_count', 'id', 'name', 'slug', 'url']
|
||||
brief_fields = ['devicetype_count', 'display', 'id', 'name', 'slug', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'name': 'Manufacturer 4',
|
||||
@ -390,7 +422,7 @@ class ManufacturerTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class DeviceTypeTest(APIViewTestCases.APIViewTestCase):
|
||||
model = DeviceType
|
||||
brief_fields = ['device_count', 'display_name', 'id', 'manufacturer', 'model', 'slug', 'url']
|
||||
brief_fields = ['device_count', 'display', 'display_name', 'id', 'manufacturer', 'model', 'slug', 'url']
|
||||
bulk_update_data = {
|
||||
'part_number': 'ABC123',
|
||||
}
|
||||
@ -432,7 +464,7 @@ class DeviceTypeTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class ConsolePortTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
model = ConsolePortTemplate
|
||||
brief_fields = ['id', 'name', 'url']
|
||||
brief_fields = ['display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@ -469,7 +501,7 @@ class ConsolePortTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class ConsoleServerPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
model = ConsoleServerPortTemplate
|
||||
brief_fields = ['id', 'name', 'url']
|
||||
brief_fields = ['display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@ -506,7 +538,7 @@ class ConsoleServerPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class PowerPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
model = PowerPortTemplate
|
||||
brief_fields = ['id', 'name', 'url']
|
||||
brief_fields = ['display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@ -543,7 +575,7 @@ class PowerPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class PowerOutletTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
model = PowerOutletTemplate
|
||||
brief_fields = ['id', 'name', 'url']
|
||||
brief_fields = ['display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@ -580,7 +612,7 @@ class PowerOutletTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class InterfaceTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
model = InterfaceTemplate
|
||||
brief_fields = ['id', 'name', 'url']
|
||||
brief_fields = ['display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@ -620,7 +652,7 @@ class InterfaceTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class FrontPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
model = FrontPortTemplate
|
||||
brief_fields = ['id', 'name', 'url']
|
||||
brief_fields = ['display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@ -691,7 +723,7 @@ class FrontPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class RearPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
model = RearPortTemplate
|
||||
brief_fields = ['id', 'name', 'url']
|
||||
brief_fields = ['display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@ -731,7 +763,7 @@ class RearPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class DeviceBayTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
model = DeviceBayTemplate
|
||||
brief_fields = ['id', 'name', 'url']
|
||||
brief_fields = ['display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@ -771,7 +803,7 @@ class DeviceBayTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class DeviceRoleTest(APIViewTestCases.APIViewTestCase):
|
||||
model = DeviceRole
|
||||
brief_fields = ['device_count', 'id', 'name', 'slug', 'url', 'virtualmachine_count']
|
||||
brief_fields = ['device_count', 'display', 'id', 'name', 'slug', 'url', 'virtualmachine_count']
|
||||
create_data = [
|
||||
{
|
||||
'name': 'Device Role 4',
|
||||
@ -806,7 +838,7 @@ class DeviceRoleTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class PlatformTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Platform
|
||||
brief_fields = ['device_count', 'id', 'name', 'slug', 'url', 'virtualmachine_count']
|
||||
brief_fields = ['device_count', 'display', 'id', 'name', 'slug', 'url', 'virtualmachine_count']
|
||||
create_data = [
|
||||
{
|
||||
'name': 'Platform 4',
|
||||
@ -838,7 +870,7 @@ class PlatformTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class DeviceTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Device
|
||||
brief_fields = ['display_name', 'id', 'name', 'url']
|
||||
brief_fields = ['display', 'display_name', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'status': 'failed',
|
||||
}
|
||||
@ -979,7 +1011,7 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class ConsolePortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||
model = ConsolePort
|
||||
brief_fields = ['cable', 'device', 'id', 'name', 'url']
|
||||
brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@ -1018,7 +1050,7 @@ class ConsolePortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCa
|
||||
|
||||
class ConsoleServerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||
model = ConsoleServerPort
|
||||
brief_fields = ['cable', 'device', 'id', 'name', 'url']
|
||||
brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@ -1057,7 +1089,7 @@ class ConsoleServerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIView
|
||||
|
||||
class PowerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||
model = PowerPort
|
||||
brief_fields = ['cable', 'device', 'id', 'name', 'url']
|
||||
brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@ -1096,7 +1128,7 @@ class PowerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
|
||||
|
||||
class PowerOutletTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||
model = PowerOutlet
|
||||
brief_fields = ['cable', 'device', 'id', 'name', 'url']
|
||||
brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@ -1135,7 +1167,7 @@ class PowerOutletTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCa
|
||||
|
||||
class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||
model = Interface
|
||||
brief_fields = ['cable', 'device', 'id', 'name', 'url']
|
||||
brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@ -1193,7 +1225,7 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
|
||||
|
||||
class FrontPortTest(APIViewTestCases.APIViewTestCase):
|
||||
model = FrontPort
|
||||
brief_fields = ['cable', 'device', 'id', 'name', 'url']
|
||||
brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@ -1251,7 +1283,7 @@ class FrontPortTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class RearPortTest(APIViewTestCases.APIViewTestCase):
|
||||
model = RearPort
|
||||
brief_fields = ['cable', 'device', 'id', 'name', 'url']
|
||||
brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@ -1293,7 +1325,7 @@ class RearPortTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class DeviceBayTest(APIViewTestCases.APIViewTestCase):
|
||||
model = DeviceBay
|
||||
brief_fields = ['device', 'id', 'name', 'url']
|
||||
brief_fields = ['device', 'display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@ -1356,7 +1388,7 @@ class DeviceBayTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class InventoryItemTest(APIViewTestCases.APIViewTestCase):
|
||||
model = InventoryItem
|
||||
brief_fields = ['_depth', 'device', 'id', 'name', 'url']
|
||||
brief_fields = ['_depth', 'device', 'display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@ -1394,7 +1426,7 @@ class InventoryItemTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class CableTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Cable
|
||||
brief_fields = ['id', 'label', 'url']
|
||||
brief_fields = ['display', 'id', 'label', 'url']
|
||||
bulk_update_data = {
|
||||
'length': 100,
|
||||
'length_unit': 'm',
|
||||
@ -1579,7 +1611,7 @@ class VirtualChassisTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class PowerPanelTest(APIViewTestCases.APIViewTestCase):
|
||||
model = PowerPanel
|
||||
brief_fields = ['id', 'name', 'powerfeed_count', 'url']
|
||||
brief_fields = ['display', 'id', 'name', 'powerfeed_count', 'url']
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@ -1588,17 +1620,17 @@ class PowerPanelTest(APIViewTestCases.APIViewTestCase):
|
||||
Site.objects.create(name='Site 2', slug='site-2'),
|
||||
)
|
||||
|
||||
rack_groups = (
|
||||
RackGroup.objects.create(name='Rack Group 1', slug='rack-group-1', site=sites[0]),
|
||||
RackGroup.objects.create(name='Rack Group 2', slug='rack-group-2', site=sites[0]),
|
||||
RackGroup.objects.create(name='Rack Group 3', slug='rack-group-3', site=sites[0]),
|
||||
RackGroup.objects.create(name='Rack Group 4', slug='rack-group-3', site=sites[1]),
|
||||
locations = (
|
||||
Location.objects.create(name='Location 1', slug='location-1', site=sites[0]),
|
||||
Location.objects.create(name='Location 2', slug='location-2', site=sites[0]),
|
||||
Location.objects.create(name='Location 3', slug='location-3', site=sites[0]),
|
||||
Location.objects.create(name='Location 4', slug='location-3', site=sites[1]),
|
||||
)
|
||||
|
||||
power_panels = (
|
||||
PowerPanel(site=sites[0], rack_group=rack_groups[0], name='Power Panel 1'),
|
||||
PowerPanel(site=sites[0], rack_group=rack_groups[1], name='Power Panel 2'),
|
||||
PowerPanel(site=sites[0], rack_group=rack_groups[2], name='Power Panel 3'),
|
||||
PowerPanel(site=sites[0], location=locations[0], name='Power Panel 1'),
|
||||
PowerPanel(site=sites[0], location=locations[1], name='Power Panel 2'),
|
||||
PowerPanel(site=sites[0], location=locations[2], name='Power Panel 3'),
|
||||
)
|
||||
PowerPanel.objects.bulk_create(power_panels)
|
||||
|
||||
@ -1606,29 +1638,29 @@ class PowerPanelTest(APIViewTestCases.APIViewTestCase):
|
||||
{
|
||||
'name': 'Power Panel 4',
|
||||
'site': sites[0].pk,
|
||||
'rack_group': rack_groups[0].pk,
|
||||
'location': locations[0].pk,
|
||||
},
|
||||
{
|
||||
'name': 'Power Panel 5',
|
||||
'site': sites[0].pk,
|
||||
'rack_group': rack_groups[1].pk,
|
||||
'location': locations[1].pk,
|
||||
},
|
||||
{
|
||||
'name': 'Power Panel 6',
|
||||
'site': sites[0].pk,
|
||||
'rack_group': rack_groups[2].pk,
|
||||
'location': locations[2].pk,
|
||||
},
|
||||
]
|
||||
|
||||
cls.bulk_update_data = {
|
||||
'site': sites[1].pk,
|
||||
'rack_group': rack_groups[3].pk
|
||||
'location': locations[3].pk
|
||||
}
|
||||
|
||||
|
||||
class PowerFeedTest(APIViewTestCases.APIViewTestCase):
|
||||
model = PowerFeed
|
||||
brief_fields = ['cable', 'id', 'name', 'url']
|
||||
brief_fields = ['_occupied', 'cable', 'display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'status': 'planned',
|
||||
}
|
||||
@ -1636,20 +1668,20 @@ class PowerFeedTest(APIViewTestCases.APIViewTestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
site = Site.objects.create(name='Site 1', slug='site-1')
|
||||
rackgroup = RackGroup.objects.create(site=site, name='Rack Group 1', slug='rack-group-1')
|
||||
location = Location.objects.create(site=site, name='Location 1', slug='location-1')
|
||||
rackrole = RackRole.objects.create(name='Rack Role 1', slug='rack-role-1', color='ff0000')
|
||||
|
||||
racks = (
|
||||
Rack(site=site, group=rackgroup, role=rackrole, name='Rack 1'),
|
||||
Rack(site=site, group=rackgroup, role=rackrole, name='Rack 2'),
|
||||
Rack(site=site, group=rackgroup, role=rackrole, name='Rack 3'),
|
||||
Rack(site=site, group=rackgroup, role=rackrole, name='Rack 4'),
|
||||
Rack(site=site, location=location, role=rackrole, name='Rack 1'),
|
||||
Rack(site=site, location=location, role=rackrole, name='Rack 2'),
|
||||
Rack(site=site, location=location, role=rackrole, name='Rack 3'),
|
||||
Rack(site=site, location=location, role=rackrole, name='Rack 4'),
|
||||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
power_panels = (
|
||||
PowerPanel(site=site, rack_group=rackgroup, name='Power Panel 1'),
|
||||
PowerPanel(site=site, rack_group=rackgroup, name='Power Panel 2'),
|
||||
PowerPanel(site=site, location=location, name='Power Panel 1'),
|
||||
PowerPanel(site=site, location=location, name='Power Panel 2'),
|
||||
)
|
||||
PowerPanel.objects.bulk_create(power_panels)
|
||||
|
||||
|
||||
@ -229,40 +229,6 @@ class CablePathTestCase(TestCase):
|
||||
# Check that all CablePaths have been deleted
|
||||
self.assertEqual(CablePath.objects.count(), 0)
|
||||
|
||||
def test_105_interface_to_circuittermination(self):
|
||||
"""
|
||||
[IF1] --C1-- [CT1A]
|
||||
"""
|
||||
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A')
|
||||
|
||||
# Create cable 1
|
||||
cable1 = Cable(termination_a=interface1, termination_b=circuittermination1)
|
||||
cable1.save()
|
||||
path1 = self.assertPathExists(
|
||||
origin=interface1,
|
||||
destination=circuittermination1,
|
||||
path=(cable1,),
|
||||
is_active=True
|
||||
)
|
||||
path2 = self.assertPathExists(
|
||||
origin=circuittermination1,
|
||||
destination=interface1,
|
||||
path=(cable1,),
|
||||
is_active=True
|
||||
)
|
||||
self.assertEqual(CablePath.objects.count(), 2)
|
||||
interface1.refresh_from_db()
|
||||
circuittermination1.refresh_from_db()
|
||||
self.assertPathIsSet(interface1, path1)
|
||||
self.assertPathIsSet(circuittermination1, path2)
|
||||
|
||||
# Delete cable 1
|
||||
cable1.delete()
|
||||
|
||||
# Check that all CablePaths have been deleted
|
||||
self.assertEqual(CablePath.objects.count(), 0)
|
||||
|
||||
def test_201_single_path_via_pass_through(self):
|
||||
"""
|
||||
[IF1] --C1-- [FP1] [RP1] --C2-- [IF2]
|
||||
@ -820,6 +786,294 @@ class CablePathTestCase(TestCase):
|
||||
)
|
||||
self.assertEqual(CablePath.objects.count(), 1)
|
||||
|
||||
def test_208_circuittermination(self):
|
||||
"""
|
||||
[IF1] --C1-- [CT1]
|
||||
"""
|
||||
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A')
|
||||
|
||||
# Create cable 1
|
||||
cable1 = Cable(termination_a=interface1, termination_b=circuittermination1)
|
||||
cable1.save()
|
||||
|
||||
# Check for incomplete path
|
||||
self.assertPathExists(
|
||||
origin=interface1,
|
||||
destination=None,
|
||||
path=(cable1, circuittermination1),
|
||||
is_active=False
|
||||
)
|
||||
self.assertEqual(CablePath.objects.count(), 1)
|
||||
|
||||
# Delete cable 1
|
||||
cable1.delete()
|
||||
self.assertEqual(CablePath.objects.count(), 0)
|
||||
interface1.refresh_from_db()
|
||||
self.assertPathIsNotSet(interface1)
|
||||
|
||||
def test_209_circuit_to_interface(self):
|
||||
"""
|
||||
[IF1] --C1-- [CT1] [CT2] --C2-- [IF2]
|
||||
"""
|
||||
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
||||
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A')
|
||||
|
||||
# Create cable 1
|
||||
cable1 = Cable(termination_a=interface1, termination_b=circuittermination1)
|
||||
cable1.save()
|
||||
|
||||
# Check for partial path from interface1
|
||||
self.assertPathExists(
|
||||
origin=interface1,
|
||||
destination=None,
|
||||
path=(cable1, circuittermination1),
|
||||
is_active=False
|
||||
)
|
||||
|
||||
# Create CT2
|
||||
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='Z')
|
||||
|
||||
# Check for partial path to site
|
||||
self.assertPathExists(
|
||||
origin=interface1,
|
||||
destination=self.site,
|
||||
path=(cable1, circuittermination1, circuittermination2),
|
||||
is_active=True
|
||||
)
|
||||
|
||||
# Create cable 2
|
||||
cable2 = Cable(termination_a=circuittermination2, termination_b=interface2)
|
||||
cable2.save()
|
||||
|
||||
# Check for complete path in each direction
|
||||
self.assertPathExists(
|
||||
origin=interface1,
|
||||
destination=interface2,
|
||||
path=(cable1, circuittermination1, circuittermination2, cable2),
|
||||
is_active=True
|
||||
)
|
||||
self.assertPathExists(
|
||||
origin=interface2,
|
||||
destination=interface1,
|
||||
path=(cable2, circuittermination2, circuittermination1, cable1),
|
||||
is_active=True
|
||||
)
|
||||
self.assertEqual(CablePath.objects.count(), 2)
|
||||
|
||||
# Delete cable 2
|
||||
cable2.delete()
|
||||
path1 = self.assertPathExists(
|
||||
origin=interface1,
|
||||
destination=self.site,
|
||||
path=(cable1, circuittermination1, circuittermination2),
|
||||
is_active=True
|
||||
)
|
||||
self.assertEqual(CablePath.objects.count(), 1)
|
||||
interface1.refresh_from_db()
|
||||
interface2.refresh_from_db()
|
||||
self.assertPathIsSet(interface1, path1)
|
||||
self.assertPathIsNotSet(interface2)
|
||||
|
||||
def test_210_circuit_to_site(self):
|
||||
"""
|
||||
[IF1] --C1-- [CT1] [CT2] --> [Site2]
|
||||
"""
|
||||
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
||||
site2 = Site.objects.create(name='Site 2', slug='site-2')
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A')
|
||||
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, site=site2, term_side='Z')
|
||||
|
||||
# Create cable 1
|
||||
cable1 = Cable(termination_a=interface1, termination_b=circuittermination1)
|
||||
cable1.save()
|
||||
self.assertPathExists(
|
||||
origin=interface1,
|
||||
destination=site2,
|
||||
path=(cable1, circuittermination1, circuittermination2),
|
||||
is_active=True
|
||||
)
|
||||
self.assertEqual(CablePath.objects.count(), 1)
|
||||
|
||||
# Delete cable 1
|
||||
cable1.delete()
|
||||
self.assertEqual(CablePath.objects.count(), 0)
|
||||
interface1.refresh_from_db()
|
||||
self.assertPathIsNotSet(interface1)
|
||||
|
||||
def test_211_circuit_to_providernetwork(self):
|
||||
"""
|
||||
[IF1] --C1-- [CT1] [CT2] --> [PN1]
|
||||
"""
|
||||
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
||||
providernetwork = ProviderNetwork.objects.create(name='Provider Network 1', provider=self.circuit.provider)
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A')
|
||||
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, provider_network=providernetwork, term_side='Z')
|
||||
|
||||
# Create cable 1
|
||||
cable1 = Cable(termination_a=interface1, termination_b=circuittermination1)
|
||||
cable1.save()
|
||||
self.assertPathExists(
|
||||
origin=interface1,
|
||||
destination=providernetwork,
|
||||
path=(cable1, circuittermination1, circuittermination2),
|
||||
is_active=True
|
||||
)
|
||||
self.assertEqual(CablePath.objects.count(), 1)
|
||||
|
||||
# Delete cable 1
|
||||
cable1.delete()
|
||||
self.assertEqual(CablePath.objects.count(), 0)
|
||||
interface1.refresh_from_db()
|
||||
self.assertPathIsNotSet(interface1)
|
||||
|
||||
def test_212_multiple_paths_via_circuit(self):
|
||||
"""
|
||||
[IF1] --C1-- [FP1:1] [RP1] --C3-- [CT1] [CT2] --C4-- [RP2] [FP2:1] --C5-- [IF3]
|
||||
[IF2] --C2-- [FP1:2] [FP2:2] --C6-- [IF4]
|
||||
"""
|
||||
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
||||
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
|
||||
interface3 = Interface.objects.create(device=self.device, name='Interface 3')
|
||||
interface4 = Interface.objects.create(device=self.device, name='Interface 4')
|
||||
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=4)
|
||||
rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=4)
|
||||
frontport1_1 = FrontPort.objects.create(
|
||||
device=self.device, name='Front Port 1:1', rear_port=rearport1, rear_port_position=1
|
||||
)
|
||||
frontport1_2 = FrontPort.objects.create(
|
||||
device=self.device, name='Front Port 1:2', rear_port=rearport1, rear_port_position=2
|
||||
)
|
||||
frontport2_1 = FrontPort.objects.create(
|
||||
device=self.device, name='Front Port 2:1', rear_port=rearport2, rear_port_position=1
|
||||
)
|
||||
frontport2_2 = FrontPort.objects.create(
|
||||
device=self.device, name='Front Port 2:2', rear_port=rearport2, rear_port_position=2
|
||||
)
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A')
|
||||
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='Z')
|
||||
|
||||
# Create cables
|
||||
cable1 = Cable(termination_a=interface1, termination_b=frontport1_1) # IF1 -> FP1:1
|
||||
cable1.save()
|
||||
cable2 = Cable(termination_a=interface2, termination_b=frontport1_2) # IF2 -> FP1:2
|
||||
cable2.save()
|
||||
cable3 = Cable(termination_a=rearport1, termination_b=circuittermination1) # RP1 -> CT1
|
||||
cable3.save()
|
||||
cable4 = Cable(termination_a=rearport2, termination_b=circuittermination2) # RP2 -> CT2
|
||||
cable4.save()
|
||||
cable5 = Cable(termination_a=interface3, termination_b=frontport2_1) # IF3 -> FP2:1
|
||||
cable5.save()
|
||||
cable6 = Cable(termination_a=interface4, termination_b=frontport2_2) # IF4 -> FP2:2
|
||||
cable6.save()
|
||||
self.assertPathExists(
|
||||
origin=interface1,
|
||||
destination=interface3,
|
||||
path=(
|
||||
cable1, frontport1_1, rearport1, cable3, circuittermination1, circuittermination2,
|
||||
cable4, rearport2, frontport2_1, cable5
|
||||
),
|
||||
is_active=True
|
||||
)
|
||||
self.assertPathExists(
|
||||
origin=interface2,
|
||||
destination=interface4,
|
||||
path=(
|
||||
cable2, frontport1_2, rearport1, cable3, circuittermination1, circuittermination2,
|
||||
cable4, rearport2, frontport2_2, cable6
|
||||
),
|
||||
is_active=True
|
||||
)
|
||||
self.assertPathExists(
|
||||
origin=interface3,
|
||||
destination=interface1,
|
||||
path=(
|
||||
cable5, frontport2_1, rearport2, cable4, circuittermination2, circuittermination1,
|
||||
cable3, rearport1, frontport1_1, cable1
|
||||
),
|
||||
is_active=True
|
||||
)
|
||||
self.assertPathExists(
|
||||
origin=interface4,
|
||||
destination=interface2,
|
||||
path=(
|
||||
cable6, frontport2_2, rearport2, cable4, circuittermination2, circuittermination1,
|
||||
cable3, rearport1, frontport1_2, cable2
|
||||
),
|
||||
is_active=True
|
||||
)
|
||||
self.assertEqual(CablePath.objects.count(), 4)
|
||||
|
||||
# Delete cables 3-4
|
||||
cable3.delete()
|
||||
cable4.delete()
|
||||
|
||||
# Check for four partial paths; one from each interface
|
||||
self.assertEqual(CablePath.objects.filter(destination_id__isnull=True).count(), 4)
|
||||
self.assertEqual(CablePath.objects.filter(destination_id__isnull=False).count(), 0)
|
||||
|
||||
def test_213_multiple_circuits_to_interface(self):
|
||||
"""
|
||||
[IF1] --C1-- [CT1] [CT2] --C2-- [CT3] [CT4] --C3-- [IF2]
|
||||
"""
|
||||
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
||||
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
|
||||
circuit2 = Circuit.objects.create(provider=self.circuit.provider, type=self.circuit.type, cid='Circuit 2')
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A')
|
||||
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='Z')
|
||||
circuittermination3 = CircuitTermination.objects.create(circuit=circuit2, site=self.site, term_side='A')
|
||||
circuittermination4 = CircuitTermination.objects.create(circuit=circuit2, site=self.site, term_side='Z')
|
||||
|
||||
# Create cables
|
||||
cable1 = Cable(termination_a=interface1, termination_b=circuittermination1)
|
||||
cable1.save()
|
||||
cable2 = Cable(termination_a=circuittermination2, termination_b=circuittermination3)
|
||||
cable2.save()
|
||||
cable3 = Cable(termination_a=circuittermination4, termination_b=interface2)
|
||||
cable3.save()
|
||||
|
||||
# Check for paths
|
||||
self.assertPathExists(
|
||||
origin=interface1,
|
||||
destination=interface2,
|
||||
path=(
|
||||
cable1, circuittermination1, circuittermination2, cable2, circuittermination3, circuittermination4,
|
||||
cable3
|
||||
),
|
||||
is_active=True
|
||||
)
|
||||
self.assertPathExists(
|
||||
origin=interface2,
|
||||
destination=interface1,
|
||||
path=(
|
||||
cable3, circuittermination4, circuittermination3, cable2, circuittermination2, circuittermination1,
|
||||
cable1
|
||||
),
|
||||
is_active=True
|
||||
)
|
||||
self.assertEqual(CablePath.objects.count(), 2)
|
||||
|
||||
# Delete cable 2
|
||||
cable2.delete()
|
||||
path1 = self.assertPathExists(
|
||||
origin=interface1,
|
||||
destination=self.site,
|
||||
path=(cable1, circuittermination1, circuittermination2),
|
||||
is_active=True
|
||||
)
|
||||
path2 = self.assertPathExists(
|
||||
origin=interface2,
|
||||
destination=self.site,
|
||||
path=(cable3, circuittermination4, circuittermination3),
|
||||
is_active=True
|
||||
)
|
||||
self.assertEqual(CablePath.objects.count(), 2)
|
||||
interface1.refresh_from_db()
|
||||
interface2.refresh_from_db()
|
||||
self.assertPathIsSet(interface1, path1)
|
||||
self.assertPathIsSet(interface2, path2)
|
||||
|
||||
def test_301_create_path_via_existing_cable(self):
|
||||
"""
|
||||
[IF1] --C1-- [FP1] [RP2] --C2-- [RP2] [FP2] --C3-- [IF2]
|
||||
|
||||
@ -3,13 +3,7 @@ from django.test import TestCase
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.filters import *
|
||||
from dcim.models import (
|
||||
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
|
||||
InventoryItem, Manufacturer, Platform, PowerFeed, PowerPanel, PowerPort, PowerPortTemplate, PowerOutlet,
|
||||
PowerOutletTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
|
||||
VirtualChassis,
|
||||
)
|
||||
from dcim.models import *
|
||||
from ipam.models import IPAddress
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from virtualization.models import Cluster, ClusterType
|
||||
@ -65,6 +59,56 @@ class RegionTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
|
||||
class SiteGroupTestCase(TestCase):
|
||||
queryset = SiteGroup.objects.all()
|
||||
filterset = SiteGroupFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
sitegroups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1', description='A'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2', description='B'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3', description='C'),
|
||||
)
|
||||
for sitegroup in sitegroups:
|
||||
sitegroup.save()
|
||||
|
||||
child_sitegroups = (
|
||||
SiteGroup(name='Site Group 1A', slug='site-group-1a', parent=sitegroups[0]),
|
||||
SiteGroup(name='Site Group 1B', slug='site-group-1b', parent=sitegroups[0]),
|
||||
SiteGroup(name='Site Group 2A', slug='site-group-2a', parent=sitegroups[1]),
|
||||
SiteGroup(name='Site Group 2B', slug='site-group-2b', parent=sitegroups[1]),
|
||||
SiteGroup(name='Site Group 3A', slug='site-group-3a', parent=sitegroups[2]),
|
||||
SiteGroup(name='Site Group 3B', slug='site-group-3b', parent=sitegroups[2]),
|
||||
)
|
||||
for sitegroup in child_sitegroups:
|
||||
sitegroup.save()
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Site Group 1', 'Site Group 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_slug(self):
|
||||
params = {'slug': ['site-group-1', 'site-group-2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['A', 'B']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_parent(self):
|
||||
parent_sitegroups = SiteGroup.objects.filter(parent__isnull=True)[:2]
|
||||
params = {'parent_id': [parent_sitegroups[0].pk, parent_sitegroups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'parent': [parent_sitegroups[0].slug, parent_sitegroups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
|
||||
class SiteTestCase(TestCase):
|
||||
queryset = Site.objects.all()
|
||||
filterset = SiteFilterSet
|
||||
@ -80,6 +124,14 @@ class SiteTestCase(TestCase):
|
||||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
|
||||
tenant_groups = (
|
||||
TenantGroup(name='Tenant group 1', slug='tenant-group-1'),
|
||||
TenantGroup(name='Tenant group 2', slug='tenant-group-2'),
|
||||
@ -96,9 +148,9 @@ class SiteTestCase(TestCase):
|
||||
Tenant.objects.bulk_create(tenants)
|
||||
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], tenant=tenants[0], status=SiteStatusChoices.STATUS_ACTIVE, facility='Facility 1', asn=65001, latitude=10, longitude=10, contact_name='Contact 1', contact_phone='123-555-0001', contact_email='contact1@example.com'),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], tenant=tenants[1], status=SiteStatusChoices.STATUS_PLANNED, facility='Facility 2', asn=65002, latitude=20, longitude=20, contact_name='Contact 2', contact_phone='123-555-0002', contact_email='contact2@example.com'),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], tenant=tenants[2], status=SiteStatusChoices.STATUS_RETIRED, facility='Facility 3', asn=65003, latitude=30, longitude=30, contact_name='Contact 3', contact_phone='123-555-0003', contact_email='contact3@example.com'),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0], tenant=tenants[0], status=SiteStatusChoices.STATUS_ACTIVE, facility='Facility 1', asn=65001, latitude=10, longitude=10, contact_name='Contact 1', contact_phone='123-555-0001', contact_email='contact1@example.com'),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1], tenant=tenants[1], status=SiteStatusChoices.STATUS_PLANNED, facility='Facility 2', asn=65002, latitude=20, longitude=20, contact_name='Contact 2', contact_phone='123-555-0002', contact_email='contact2@example.com'),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2], tenant=tenants[2], status=SiteStatusChoices.STATUS_RETIRED, facility='Facility 3', asn=65003, latitude=30, longitude=30, contact_name='Contact 3', contact_phone='123-555-0003', contact_email='contact3@example.com'),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
@ -153,6 +205,13 @@ class SiteTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site_group(self):
|
||||
groups = SiteGroup.objects.all()[:2]
|
||||
params = {'group_id': [groups[0].pk, groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'group': [groups[0].slug, groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_tenant(self):
|
||||
tenants = Tenant.objects.all()[:2]
|
||||
params = {'tenant_id': [tenants[0].pk, tenants[1].pk]}
|
||||
@ -168,9 +227,9 @@ class SiteTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class RackGroupTestCase(TestCase):
|
||||
queryset = RackGroup.objects.all()
|
||||
filterset = RackGroupFilterSet
|
||||
class LocationTestCase(TestCase):
|
||||
queryset = Location.objects.all()
|
||||
filterset = LocationFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@ -183,39 +242,47 @@ class RackGroupTestCase(TestCase):
|
||||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1', region=regions[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2]),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
parent_rack_groups = (
|
||||
RackGroup(name='Parent Rack Group 1', slug='parent-rack-group-1', site=sites[0]),
|
||||
RackGroup(name='Parent Rack Group 2', slug='parent-rack-group-2', site=sites[1]),
|
||||
RackGroup(name='Parent Rack Group 3', slug='parent-rack-group-3', site=sites[2]),
|
||||
parent_locations = (
|
||||
Location(name='Parent Location 1', slug='parent-location-1', site=sites[0]),
|
||||
Location(name='Parent Location 2', slug='parent-location-2', site=sites[1]),
|
||||
Location(name='Parent Location 3', slug='parent-location-3', site=sites[2]),
|
||||
)
|
||||
for rackgroup in parent_rack_groups:
|
||||
rackgroup.save()
|
||||
for location in parent_locations:
|
||||
location.save()
|
||||
|
||||
rack_groups = (
|
||||
RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0], parent=parent_rack_groups[0], description='A'),
|
||||
RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1], parent=parent_rack_groups[1], description='B'),
|
||||
RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2], parent=parent_rack_groups[2], description='C'),
|
||||
locations = (
|
||||
Location(name='Location 1', slug='location-1', site=sites[0], parent=parent_locations[0], description='A'),
|
||||
Location(name='Location 2', slug='location-2', site=sites[1], parent=parent_locations[1], description='B'),
|
||||
Location(name='Location 3', slug='location-3', site=sites[2], parent=parent_locations[2], description='C'),
|
||||
)
|
||||
for rackgroup in rack_groups:
|
||||
rackgroup.save()
|
||||
for location in locations:
|
||||
location.save()
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Rack Group 1', 'Rack Group 2']}
|
||||
params = {'name': ['Location 1', 'Location 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_slug(self):
|
||||
params = {'slug': ['rack-group-1', 'rack-group-2']}
|
||||
params = {'slug': ['location-1', 'location-2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
@ -229,6 +296,13 @@ class RackGroupTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@ -237,7 +311,7 @@ class RackGroupTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_parent(self):
|
||||
parent_groups = RackGroup.objects.filter(name__startswith='Parent')[:2]
|
||||
parent_groups = Location.objects.filter(name__startswith='Parent')[:2]
|
||||
params = {'parent_id': [parent_groups[0].pk, parent_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'parent': [parent_groups[0].slug, parent_groups[1].slug]}
|
||||
@ -290,20 +364,28 @@ class RackTestCase(TestCase):
|
||||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1', region=regions[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2]),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
rack_groups = (
|
||||
RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]),
|
||||
RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]),
|
||||
RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2]),
|
||||
locations = (
|
||||
Location(name='Location 1', slug='location-1', site=sites[0]),
|
||||
Location(name='Location 2', slug='location-2', site=sites[1]),
|
||||
Location(name='Location 3', slug='location-3', site=sites[2]),
|
||||
)
|
||||
for rackgroup in rack_groups:
|
||||
rackgroup.save()
|
||||
for location in locations:
|
||||
location.save()
|
||||
|
||||
rack_roles = (
|
||||
RackRole(name='Rack Role 1', slug='rack-role-1'),
|
||||
@ -328,9 +410,9 @@ class RackTestCase(TestCase):
|
||||
Tenant.objects.bulk_create(tenants)
|
||||
|
||||
racks = (
|
||||
Rack(name='Rack 1', facility_id='rack-1', site=sites[0], group=rack_groups[0], tenant=tenants[0], status=RackStatusChoices.STATUS_ACTIVE, role=rack_roles[0], serial='ABC', asset_tag='1001', type=RackTypeChoices.TYPE_2POST, width=RackWidthChoices.WIDTH_19IN, u_height=42, desc_units=False, outer_width=100, outer_depth=100, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER),
|
||||
Rack(name='Rack 2', facility_id='rack-2', site=sites[1], group=rack_groups[1], tenant=tenants[1], status=RackStatusChoices.STATUS_PLANNED, role=rack_roles[1], serial='DEF', asset_tag='1002', type=RackTypeChoices.TYPE_4POST, width=RackWidthChoices.WIDTH_21IN, u_height=43, desc_units=False, outer_width=200, outer_depth=200, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER),
|
||||
Rack(name='Rack 3', facility_id='rack-3', site=sites[2], group=rack_groups[2], tenant=tenants[2], status=RackStatusChoices.STATUS_RESERVED, role=rack_roles[2], serial='GHI', asset_tag='1003', type=RackTypeChoices.TYPE_CABINET, width=RackWidthChoices.WIDTH_23IN, u_height=44, desc_units=True, outer_width=300, outer_depth=300, outer_unit=RackDimensionUnitChoices.UNIT_INCH),
|
||||
Rack(name='Rack 1', facility_id='rack-1', site=sites[0], location=locations[0], tenant=tenants[0], status=RackStatusChoices.STATUS_ACTIVE, role=rack_roles[0], serial='ABC', asset_tag='1001', type=RackTypeChoices.TYPE_2POST, width=RackWidthChoices.WIDTH_19IN, u_height=42, desc_units=False, outer_width=100, outer_depth=100, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER),
|
||||
Rack(name='Rack 2', facility_id='rack-2', site=sites[1], location=locations[1], tenant=tenants[1], status=RackStatusChoices.STATUS_PLANNED, role=rack_roles[1], serial='DEF', asset_tag='1002', type=RackTypeChoices.TYPE_4POST, width=RackWidthChoices.WIDTH_21IN, u_height=43, desc_units=False, outer_width=200, outer_depth=200, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER),
|
||||
Rack(name='Rack 3', facility_id='rack-3', site=sites[2], location=locations[2], tenant=tenants[2], status=RackStatusChoices.STATUS_RESERVED, role=rack_roles[2], serial='GHI', asset_tag='1003', type=RackTypeChoices.TYPE_CABINET, width=RackWidthChoices.WIDTH_23IN, u_height=44, desc_units=True, outer_width=300, outer_depth=300, outer_unit=RackDimensionUnitChoices.UNIT_INCH),
|
||||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
@ -388,6 +470,13 @@ class RackTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@ -395,11 +484,11 @@ class RackTestCase(TestCase):
|
||||
params = {'site': [sites[0].slug, sites[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_group(self):
|
||||
groups = RackGroup.objects.all()[:2]
|
||||
params = {'group_id': [groups[0].pk, groups[1].pk]}
|
||||
def test_location(self):
|
||||
locations = Location.objects.all()[:2]
|
||||
params = {'location_id': [locations[0].pk, locations[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'group': [groups[0].slug, groups[1].slug]}
|
||||
params = {'location': [locations[0].slug, locations[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_status(self):
|
||||
@ -448,18 +537,18 @@ class RackReservationTestCase(TestCase):
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
rack_groups = (
|
||||
RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]),
|
||||
RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]),
|
||||
RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2]),
|
||||
locations = (
|
||||
Location(name='Location 1', slug='location-1', site=sites[0]),
|
||||
Location(name='Location 2', slug='location-2', site=sites[1]),
|
||||
Location(name='Location 3', slug='location-3', site=sites[2]),
|
||||
)
|
||||
for rackgroup in rack_groups:
|
||||
rackgroup.save()
|
||||
for location in locations:
|
||||
location.save()
|
||||
|
||||
racks = (
|
||||
Rack(name='Rack 1', site=sites[0], group=rack_groups[0]),
|
||||
Rack(name='Rack 2', site=sites[1], group=rack_groups[1]),
|
||||
Rack(name='Rack 3', site=sites[2], group=rack_groups[2]),
|
||||
Rack(name='Rack 1', site=sites[0], location=locations[0]),
|
||||
Rack(name='Rack 2', site=sites[1], location=locations[1]),
|
||||
Rack(name='Rack 3', site=sites[2], location=locations[2]),
|
||||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
@ -503,11 +592,11 @@ class RackReservationTestCase(TestCase):
|
||||
params = {'site': [sites[0].slug, sites[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_group(self):
|
||||
groups = RackGroup.objects.all()[:2]
|
||||
params = {'group_id': [groups[0].pk, groups[1].pk]}
|
||||
def test_location(self):
|
||||
locations = Location.objects.all()[:2]
|
||||
params = {'location_id': [locations[0].pk, locations[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'group': [groups[0].slug, groups[1].slug]}
|
||||
params = {'location': [locations[0].slug, locations[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_user(self):
|
||||
@ -1157,25 +1246,33 @@ class DeviceTestCase(TestCase):
|
||||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1', region=regions[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2]),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
rack_groups = (
|
||||
RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]),
|
||||
RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]),
|
||||
RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2]),
|
||||
locations = (
|
||||
Location(name='Location 1', slug='location-1', site=sites[0]),
|
||||
Location(name='Location 2', slug='location-2', site=sites[1]),
|
||||
Location(name='Location 3', slug='location-3', site=sites[2]),
|
||||
)
|
||||
for rackgroup in rack_groups:
|
||||
rackgroup.save()
|
||||
for location in locations:
|
||||
location.save()
|
||||
|
||||
racks = (
|
||||
Rack(name='Rack 1', site=sites[0], group=rack_groups[0]),
|
||||
Rack(name='Rack 2', site=sites[1], group=rack_groups[1]),
|
||||
Rack(name='Rack 3', site=sites[2], group=rack_groups[2]),
|
||||
Rack(name='Rack 1', site=sites[0], location=locations[0]),
|
||||
Rack(name='Rack 2', site=sites[1], location=locations[1]),
|
||||
Rack(name='Rack 3', site=sites[2], location=locations[2]),
|
||||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
@ -1203,9 +1300,9 @@ class DeviceTestCase(TestCase):
|
||||
Tenant.objects.bulk_create(tenants)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_types[0], device_role=device_roles[0], platform=platforms[0], tenant=tenants[0], serial='ABC', asset_tag='1001', site=sites[0], rack=racks[0], position=1, face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_ACTIVE, cluster=clusters[0], local_context_data={"foo": 123}),
|
||||
Device(name='Device 2', device_type=device_types[1], device_role=device_roles[1], platform=platforms[1], tenant=tenants[1], serial='DEF', asset_tag='1002', site=sites[1], rack=racks[1], position=2, face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_STAGED, cluster=clusters[1]),
|
||||
Device(name='Device 3', device_type=device_types[2], device_role=device_roles[2], platform=platforms[2], tenant=tenants[2], serial='GHI', asset_tag='1003', site=sites[2], rack=racks[2], position=3, face=DeviceFaceChoices.FACE_REAR, status=DeviceStatusChoices.STATUS_FAILED, cluster=clusters[2]),
|
||||
Device(name='Device 1', device_type=device_types[0], device_role=device_roles[0], platform=platforms[0], tenant=tenants[0], serial='ABC', asset_tag='1001', site=sites[0], location=locations[0], rack=racks[0], position=1, face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_ACTIVE, cluster=clusters[0], local_context_data={"foo": 123}),
|
||||
Device(name='Device 2', device_type=device_types[1], device_role=device_roles[1], platform=platforms[1], tenant=tenants[1], serial='DEF', asset_tag='1002', site=sites[1], location=locations[1], rack=racks[1], position=2, face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_STAGED, cluster=clusters[1]),
|
||||
Device(name='Device 3', device_type=device_types[2], device_role=device_roles[2], platform=platforms[2], tenant=tenants[2], serial='GHI', asset_tag='1003', site=sites[2], location=locations[2], rack=racks[2], position=3, face=DeviceFaceChoices.FACE_REAR, status=DeviceStatusChoices.STATUS_FAILED, cluster=clusters[2]),
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
@ -1320,6 +1417,13 @@ class DeviceTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@ -1327,9 +1431,9 @@ class DeviceTestCase(TestCase):
|
||||
params = {'site': [sites[0].slug, sites[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_rackgroup(self):
|
||||
rack_groups = RackGroup.objects.all()[:2]
|
||||
params = {'rack_group_id': [rack_groups[0].pk, rack_groups[1].pk]}
|
||||
def test_location(self):
|
||||
locations = Location.objects.all()[:2]
|
||||
params = {'location_id': [locations[0].pk, locations[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_rack(self):
|
||||
@ -1459,10 +1563,19 @@ class ConsolePortTestCase(TestCase):
|
||||
)
|
||||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
|
||||
sites = Site.objects.bulk_create((
|
||||
Site(name='Site 1', slug='site-1', region=regions[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2]),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||
Site(name='Site X', slug='site-x'),
|
||||
))
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
@ -1524,6 +1637,13 @@ class ConsolePortTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@ -1559,10 +1679,19 @@ class ConsoleServerPortTestCase(TestCase):
|
||||
)
|
||||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
|
||||
sites = Site.objects.bulk_create((
|
||||
Site(name='Site 1', slug='site-1', region=regions[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2]),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||
Site(name='Site X', slug='site-x'),
|
||||
))
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
@ -1624,6 +1753,13 @@ class ConsoleServerPortTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@ -1659,10 +1795,19 @@ class PowerPortTestCase(TestCase):
|
||||
)
|
||||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
|
||||
sites = Site.objects.bulk_create((
|
||||
Site(name='Site 1', slug='site-1', region=regions[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2]),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||
Site(name='Site X', slug='site-x'),
|
||||
))
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
@ -1732,6 +1877,13 @@ class PowerPortTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@ -1767,10 +1919,19 @@ class PowerOutletTestCase(TestCase):
|
||||
)
|
||||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
|
||||
sites = Site.objects.bulk_create((
|
||||
Site(name='Site 1', slug='site-1', region=regions[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2]),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||
Site(name='Site X', slug='site-x'),
|
||||
))
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
@ -1836,6 +1997,13 @@ class PowerOutletTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@ -1871,10 +2039,19 @@ class InterfaceTestCase(TestCase):
|
||||
)
|
||||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
|
||||
sites = Site.objects.bulk_create((
|
||||
Site(name='Site 1', slug='site-1', region=regions[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2]),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||
Site(name='Site X', slug='site-x'),
|
||||
))
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
@ -1946,6 +2123,34 @@ class InterfaceTestCase(TestCase):
|
||||
params = {'description': ['First', 'Second']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_parent(self):
|
||||
# Create child interfaces
|
||||
parent_interface = Interface.objects.first()
|
||||
child_interfaces = (
|
||||
Interface(device=parent_interface.device, name='Child 1', parent=parent_interface, type=InterfaceTypeChoices.TYPE_VIRTUAL),
|
||||
Interface(device=parent_interface.device, name='Child 2', parent=parent_interface, type=InterfaceTypeChoices.TYPE_VIRTUAL),
|
||||
Interface(device=parent_interface.device, name='Child 3', parent=parent_interface, type=InterfaceTypeChoices.TYPE_VIRTUAL),
|
||||
)
|
||||
Interface.objects.bulk_create(child_interfaces)
|
||||
|
||||
params = {'parent_id': [parent_interface.pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_lag(self):
|
||||
# Create LAG members
|
||||
device = Device.objects.first()
|
||||
lag_interface = Interface(device=device, name='LAG', type=InterfaceTypeChoices.TYPE_LAG)
|
||||
lag_interface.save()
|
||||
lag_members = (
|
||||
Interface(device=device, name='Member 1', lag=lag_interface, type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||
Interface(device=device, name='Member 2', lag=lag_interface, type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||
Interface(device=device, name='Member 3', lag=lag_interface, type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||
)
|
||||
Interface.objects.bulk_create(lag_members)
|
||||
|
||||
params = {'lag_id': [lag_interface.pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_region(self):
|
||||
regions = Region.objects.all()[:2]
|
||||
params = {'region_id': [regions[0].pk, regions[1].pk]}
|
||||
@ -1953,6 +2158,13 @@ class InterfaceTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@ -2002,10 +2214,19 @@ class FrontPortTestCase(TestCase):
|
||||
)
|
||||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
|
||||
sites = Site.objects.bulk_create((
|
||||
Site(name='Site 1', slug='site-1', region=regions[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2]),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||
Site(name='Site X', slug='site-x'),
|
||||
))
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
@ -2072,6 +2293,13 @@ class FrontPortTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@ -2107,10 +2335,19 @@ class RearPortTestCase(TestCase):
|
||||
)
|
||||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
|
||||
sites = Site.objects.bulk_create((
|
||||
Site(name='Site 1', slug='site-1', region=regions[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2]),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||
Site(name='Site X', slug='site-x'),
|
||||
))
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
@ -2171,6 +2408,13 @@ class RearPortTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@ -2206,10 +2450,19 @@ class DeviceBayTestCase(TestCase):
|
||||
)
|
||||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
|
||||
sites = Site.objects.bulk_create((
|
||||
Site(name='Site 1', slug='site-1', region=regions[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2]),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||
Site(name='Site X', slug='site-x'),
|
||||
))
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
@ -2253,6 +2506,13 @@ class DeviceBayTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@ -2293,10 +2553,18 @@ class InventoryItemTestCase(TestCase):
|
||||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1', region=regions[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2]),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
@ -2357,6 +2625,13 @@ class InventoryItemTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@ -2410,10 +2685,18 @@ class VirtualChassisTestCase(TestCase):
|
||||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1', region=regions[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2]),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
@ -2464,6 +2747,13 @@ class VirtualChassisTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@ -2611,25 +2901,33 @@ class PowerPanelTestCase(TestCase):
|
||||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1', region=regions[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2]),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
rack_groups = (
|
||||
RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]),
|
||||
RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]),
|
||||
RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2]),
|
||||
locations = (
|
||||
Location(name='Location 1', slug='location-1', site=sites[0]),
|
||||
Location(name='Location 2', slug='location-2', site=sites[1]),
|
||||
Location(name='Location 3', slug='location-3', site=sites[2]),
|
||||
)
|
||||
for rackgroup in rack_groups:
|
||||
rackgroup.save()
|
||||
for location in locations:
|
||||
location.save()
|
||||
|
||||
power_panels = (
|
||||
PowerPanel(name='Power Panel 1', site=sites[0], rack_group=rack_groups[0]),
|
||||
PowerPanel(name='Power Panel 2', site=sites[1], rack_group=rack_groups[1]),
|
||||
PowerPanel(name='Power Panel 3', site=sites[2], rack_group=rack_groups[2]),
|
||||
PowerPanel(name='Power Panel 1', site=sites[0], location=locations[0]),
|
||||
PowerPanel(name='Power Panel 2', site=sites[1], location=locations[1]),
|
||||
PowerPanel(name='Power Panel 3', site=sites[2], location=locations[2]),
|
||||
)
|
||||
PowerPanel.objects.bulk_create(power_panels)
|
||||
|
||||
@ -2648,6 +2946,13 @@ class PowerPanelTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@ -2655,9 +2960,9 @@ class PowerPanelTestCase(TestCase):
|
||||
params = {'site': [sites[0].slug, sites[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_rack_group(self):
|
||||
rack_groups = RackGroup.objects.all()[:2]
|
||||
params = {'rack_group_id': [rack_groups[0].pk, rack_groups[1].pk]}
|
||||
def test_location(self):
|
||||
locations = Location.objects.all()[:2]
|
||||
params = {'location_id': [locations[0].pk, locations[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
@ -2676,10 +2981,18 @@ class PowerFeedTestCase(TestCase):
|
||||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1', region=regions[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2]),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
@ -2759,6 +3072,13 @@ class PowerFeedTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
|
||||
@ -7,39 +7,66 @@ from dcim.models import *
|
||||
from tenancy.models import Tenant
|
||||
|
||||
|
||||
class RackGroupTestCase(TestCase):
|
||||
class LocationTestCase(TestCase):
|
||||
|
||||
def test_change_rackgroup_site(self):
|
||||
def test_change_location_site(self):
|
||||
"""
|
||||
Check that all child RackGroups and Racks get updated when a RackGroup is moved to a new Site. Topology:
|
||||
Check that all child Locations and Racks get updated when a Location is moved to a new Site. Topology:
|
||||
Site A
|
||||
- RackGroup A1
|
||||
- RackGroup A2
|
||||
- Location A1
|
||||
- Location A2
|
||||
- Rack 2
|
||||
- Device 2
|
||||
- Rack 1
|
||||
- Device 1
|
||||
"""
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
device_type = DeviceType.objects.create(
|
||||
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
|
||||
)
|
||||
device_role = DeviceRole.objects.create(
|
||||
name='Device Role 1', slug='device-role-1', color='ff0000'
|
||||
)
|
||||
|
||||
site_a = Site.objects.create(name='Site A', slug='site-a')
|
||||
site_b = Site.objects.create(name='Site B', slug='site-b')
|
||||
|
||||
rackgroup_a1 = RackGroup(site=site_a, name='RackGroup A1', slug='rackgroup-a1')
|
||||
rackgroup_a1.save()
|
||||
rackgroup_a2 = RackGroup(site=site_a, parent=rackgroup_a1, name='RackGroup A2', slug='rackgroup-a2')
|
||||
rackgroup_a2.save()
|
||||
location_a1 = Location(site=site_a, name='Location A1', slug='location-a1')
|
||||
location_a1.save()
|
||||
location_a2 = Location(site=site_a, parent=location_a1, name='Location A2', slug='location-a2')
|
||||
location_a2.save()
|
||||
|
||||
rack1 = Rack.objects.create(site=site_a, group=rackgroup_a1, name='Rack 1')
|
||||
rack2 = Rack.objects.create(site=site_a, group=rackgroup_a2, name='Rack 2')
|
||||
rack1 = Rack.objects.create(site=site_a, location=location_a1, name='Rack 1')
|
||||
rack2 = Rack.objects.create(site=site_a, location=location_a2, name='Rack 2')
|
||||
|
||||
powerpanel1 = PowerPanel.objects.create(site=site_a, rack_group=rackgroup_a1, name='Power Panel 1')
|
||||
device1 = Device.objects.create(
|
||||
site=site_a,
|
||||
location=location_a1,
|
||||
name='Device 1',
|
||||
device_type=device_type,
|
||||
device_role=device_role
|
||||
)
|
||||
device2 = Device.objects.create(
|
||||
site=site_a,
|
||||
location=location_a2,
|
||||
name='Device 2',
|
||||
device_type=device_type,
|
||||
device_role=device_role
|
||||
)
|
||||
|
||||
# Move RackGroup A1 to Site B
|
||||
rackgroup_a1.site = site_b
|
||||
rackgroup_a1.save()
|
||||
powerpanel1 = PowerPanel.objects.create(site=site_a, location=location_a1, name='Power Panel 1')
|
||||
|
||||
# Check that all objects within RackGroup A1 now belong to Site B
|
||||
self.assertEqual(RackGroup.objects.get(pk=rackgroup_a1.pk).site, site_b)
|
||||
self.assertEqual(RackGroup.objects.get(pk=rackgroup_a2.pk).site, site_b)
|
||||
# Move Location A1 to Site B
|
||||
location_a1.site = site_b
|
||||
location_a1.save()
|
||||
|
||||
# Check that all objects within Location A1 now belong to Site B
|
||||
self.assertEqual(Location.objects.get(pk=location_a1.pk).site, site_b)
|
||||
self.assertEqual(Location.objects.get(pk=location_a2.pk).site, site_b)
|
||||
self.assertEqual(Rack.objects.get(pk=rack1.pk).site, site_b)
|
||||
self.assertEqual(Rack.objects.get(pk=rack2.pk).site, site_b)
|
||||
self.assertEqual(Device.objects.get(pk=device1.pk).site, site_b)
|
||||
self.assertEqual(Device.objects.get(pk=device2.pk).site, site_b)
|
||||
self.assertEqual(PowerPanel.objects.get(pk=powerpanel1.pk).site, site_b)
|
||||
|
||||
|
||||
@ -55,12 +82,12 @@ class RackTestCase(TestCase):
|
||||
name='TestSite2',
|
||||
slug='test-site-2'
|
||||
)
|
||||
self.group1 = RackGroup.objects.create(
|
||||
self.location1 = Location.objects.create(
|
||||
name='TestGroup1',
|
||||
slug='test-group-1',
|
||||
site=self.site1
|
||||
)
|
||||
self.group2 = RackGroup.objects.create(
|
||||
self.location2 = Location.objects.create(
|
||||
name='TestGroup2',
|
||||
slug='test-group-2',
|
||||
site=self.site2
|
||||
@ -69,7 +96,7 @@ class RackTestCase(TestCase):
|
||||
name='TestRack1',
|
||||
facility_id='A101',
|
||||
site=self.site1,
|
||||
group=self.group1,
|
||||
location=self.location1,
|
||||
u_height=42
|
||||
)
|
||||
self.manufacturer = Manufacturer.objects.create(
|
||||
@ -134,19 +161,19 @@ class RackTestCase(TestCase):
|
||||
with self.assertRaises(ValidationError):
|
||||
rack1.clean()
|
||||
|
||||
def test_rack_group_site(self):
|
||||
def test_location_site(self):
|
||||
|
||||
rack_invalid_group = Rack(
|
||||
rack_invalid_location = Rack(
|
||||
name='TestRack2',
|
||||
facility_id='A102',
|
||||
site=self.site1,
|
||||
u_height=42,
|
||||
group=self.group2
|
||||
location=self.location2
|
||||
)
|
||||
rack_invalid_group.save()
|
||||
rack_invalid_location.save()
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
rack_invalid_group.clean()
|
||||
rack_invalid_location.clean()
|
||||
|
||||
def test_mount_single_device(self):
|
||||
|
||||
@ -452,10 +479,13 @@ class CableTestCase(TestCase):
|
||||
device=self.patch_pannel, name='FP4', type='8p8c', rear_port=self.rear_port4, rear_port_position=1
|
||||
)
|
||||
self.provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
provider_network = ProviderNetwork.objects.create(name='Provider Network 1', provider=self.provider)
|
||||
self.circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
|
||||
self.circuit = Circuit.objects.create(provider=self.provider, type=self.circuittype, cid='1')
|
||||
self.circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=site, term_side='A')
|
||||
self.circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, site=site, term_side='Z')
|
||||
self.circuit1 = Circuit.objects.create(provider=self.provider, type=self.circuittype, cid='1')
|
||||
self.circuit2 = Circuit.objects.create(provider=self.provider, type=self.circuittype, cid='2')
|
||||
self.circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit1, site=site, term_side='A')
|
||||
self.circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit1, site=site, term_side='Z')
|
||||
self.circuittermination3 = CircuitTermination.objects.create(circuit=self.circuit2, provider_network=provider_network, term_side='A')
|
||||
|
||||
def test_cable_creation(self):
|
||||
"""
|
||||
@ -525,6 +555,14 @@ class CableTestCase(TestCase):
|
||||
with self.assertRaises(ValidationError):
|
||||
cable.clean()
|
||||
|
||||
def test_cable_cannot_terminate_to_a_provider_network_circuittermination(self):
|
||||
"""
|
||||
Neither side of a cable can be terminated to a CircuitTermination which is attached to a ProviderNetwork
|
||||
"""
|
||||
cable = Cable(termination_a=self.interface3, termination_b=self.circuittermination3)
|
||||
with self.assertRaises(ValidationError):
|
||||
cable.clean()
|
||||
|
||||
def test_rearport_connections(self):
|
||||
"""
|
||||
Test various combinations of RearPort connections.
|
||||
|
||||
@ -12,20 +12,7 @@ from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import *
|
||||
from ipam.models import VLAN
|
||||
from utilities.testing import ViewTestCases
|
||||
|
||||
|
||||
def create_test_device(name):
|
||||
"""
|
||||
Convenience method for creating a Device (e.g. for component testing).
|
||||
"""
|
||||
site, _ = Site.objects.get_or_create(name='Site 1', slug='site-1')
|
||||
manufacturer, _ = Manufacturer.objects.get_or_create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
devicetype, _ = DeviceType.objects.get_or_create(model='Device Type 1', manufacturer=manufacturer)
|
||||
devicerole, _ = DeviceRole.objects.get_or_create(name='Device Role 1', slug='device-role-1')
|
||||
device = Device.objects.create(name=name, site=site, device_type=devicetype, device_role=devicerole)
|
||||
|
||||
return device
|
||||
from utilities.testing import ViewTestCases, create_tags, create_test_device
|
||||
|
||||
|
||||
class RegionTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
@ -57,6 +44,44 @@ class RegionTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
"Region 6,region-6,Sixth region",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
|
||||
class SiteGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
model = SiteGroup
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
# Create three SiteGroups
|
||||
sitegroups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for sitegroup in sitegroups:
|
||||
sitegroup.save()
|
||||
|
||||
cls.form_data = {
|
||||
'name': 'Site Group X',
|
||||
'slug': 'site-group-x',
|
||||
'parent': sitegroups[2].pk,
|
||||
'description': 'A new site group',
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"name,slug,description",
|
||||
"Site Group 4,site-group-4,Fourth site group",
|
||||
"Site Group 5,site-group-5,Fifth site group",
|
||||
"Site Group 6,site-group-6,Sixth site group",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
|
||||
class SiteTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = Site
|
||||
@ -71,19 +96,27 @@ class SiteTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
|
||||
Site.objects.bulk_create([
|
||||
Site(name='Site 1', slug='site-1', region=regions[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[0]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[0]),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[1]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[0], group=groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[0], group=groups[1]),
|
||||
])
|
||||
|
||||
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'name': 'Site X',
|
||||
'slug': 'site-x',
|
||||
'status': SiteStatusChoices.STATUS_PLANNED,
|
||||
'region': regions[1].pk,
|
||||
'group': groups[1].pk,
|
||||
'tenant': None,
|
||||
'facility': 'Facility X',
|
||||
'asn': 65001,
|
||||
@ -110,6 +143,7 @@ class SiteTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
cls.bulk_edit_data = {
|
||||
'status': SiteStatusChoices.STATUS_PLANNED,
|
||||
'region': regions[1].pk,
|
||||
'group': groups[1].pk,
|
||||
'tenant': None,
|
||||
'asn': 65009,
|
||||
'time_zone': pytz.timezone('US/Eastern'),
|
||||
@ -117,8 +151,8 @@ class SiteTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
|
||||
class RackGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
model = RackGroup
|
||||
class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
model = Location
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@ -126,28 +160,32 @@ class RackGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
site = Site(name='Site 1', slug='site-1')
|
||||
site.save()
|
||||
|
||||
rack_groups = (
|
||||
RackGroup(name='Rack Group 1', slug='rack-group-1', site=site),
|
||||
RackGroup(name='Rack Group 2', slug='rack-group-2', site=site),
|
||||
RackGroup(name='Rack Group 3', slug='rack-group-3', site=site),
|
||||
locations = (
|
||||
Location(name='Location 1', slug='location-1', site=site),
|
||||
Location(name='Location 2', slug='location-2', site=site),
|
||||
Location(name='Location 3', slug='location-3', site=site),
|
||||
)
|
||||
for rackgroup in rack_groups:
|
||||
rackgroup.save()
|
||||
for location in locations:
|
||||
location.save()
|
||||
|
||||
cls.form_data = {
|
||||
'name': 'Rack Group X',
|
||||
'slug': 'rack-group-x',
|
||||
'name': 'Location X',
|
||||
'slug': 'location-x',
|
||||
'site': site.pk,
|
||||
'description': 'A new rack group',
|
||||
'description': 'A new location',
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"site,name,slug,description",
|
||||
"Site 1,Rack Group 4,rack-group-4,Fourth rack group",
|
||||
"Site 1,Rack Group 5,rack-group-5,Fifth rack group",
|
||||
"Site 1,Rack Group 6,rack-group-6,Sixth rack group",
|
||||
"Site 1,Location 4,location-4,Fourth location",
|
||||
"Site 1,Location 5,location-5,Fifth location",
|
||||
"Site 1,Location 6,location-6,Sixth location",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
|
||||
class RackRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
model = RackRole
|
||||
@ -175,6 +213,11 @@ class RackRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
"Rack Role 6,rack-role-6,0000ff",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'color': '00ff00',
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
|
||||
class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = RackReservation
|
||||
@ -187,10 +230,10 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
|
||||
site = Site.objects.create(name='Site 1', slug='site-1')
|
||||
|
||||
rack_group = RackGroup(name='Rack Group 1', slug='rack-group-1', site=site)
|
||||
rack_group.save()
|
||||
location = Location(name='Location 1', slug='location-1', site=site)
|
||||
location.save()
|
||||
|
||||
rack = Rack(name='Rack 1', site=site, group=rack_group)
|
||||
rack = Rack(name='Rack 1', site=site, location=location)
|
||||
rack.save()
|
||||
|
||||
RackReservation.objects.bulk_create([
|
||||
@ -199,7 +242,7 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
RackReservation(rack=rack, user=user2, units=[7, 8, 9], description='Reservation 3'),
|
||||
])
|
||||
|
||||
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'rack': rack.pk,
|
||||
@ -211,10 +254,10 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
'site,rack_group,rack,units,description',
|
||||
'Site 1,Rack Group 1,Rack 1,"10,11,12",Reservation 1',
|
||||
'Site 1,Rack Group 1,Rack 1,"13,14,15",Reservation 2',
|
||||
'Site 1,Rack Group 1,Rack 1,"16,17,18",Reservation 3',
|
||||
'site,location,rack,units,description',
|
||||
'Site 1,Location 1,Rack 1,"10,11,12",Reservation 1',
|
||||
'Site 1,Location 1,Rack 1,"13,14,15",Reservation 2',
|
||||
'Site 1,Location 1,Rack 1,"16,17,18",Reservation 3',
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
@ -236,12 +279,12 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
rackgroups = (
|
||||
RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]),
|
||||
RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1])
|
||||
locations = (
|
||||
Location(name='Location 1', slug='location-1', site=sites[0]),
|
||||
Location(name='Location 2', slug='location-2', site=sites[1])
|
||||
)
|
||||
for rackgroup in rackgroups:
|
||||
rackgroup.save()
|
||||
for location in locations:
|
||||
location.save()
|
||||
|
||||
rackroles = (
|
||||
RackRole(name='Rack Role 1', slug='rack-role-1'),
|
||||
@ -255,13 +298,13 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
Rack(name='Rack 3', site=sites[0]),
|
||||
))
|
||||
|
||||
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'name': 'Rack X',
|
||||
'facility_id': 'Facility X',
|
||||
'site': sites[1].pk,
|
||||
'group': rackgroups[1].pk,
|
||||
'location': locations[1].pk,
|
||||
'tenant': None,
|
||||
'status': RackStatusChoices.STATUS_PLANNED,
|
||||
'role': rackroles[1].pk,
|
||||
@ -279,15 +322,15 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"site,group,name,width,u_height",
|
||||
"site,location,name,width,u_height",
|
||||
"Site 1,,Rack 4,19,42",
|
||||
"Site 1,Rack Group 1,Rack 5,19,42",
|
||||
"Site 2,Rack Group 2,Rack 6,19,42",
|
||||
"Site 1,Location 1,Rack 5,19,42",
|
||||
"Site 2,Location 2,Rack 6,19,42",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'site': sites[1].pk,
|
||||
'group': rackgroups[1].pk,
|
||||
'location': locations[1].pk,
|
||||
'tenant': None,
|
||||
'status': RackStatusChoices.STATUS_DEPRECATED,
|
||||
'role': rackroles[1].pk,
|
||||
@ -336,6 +379,10 @@ class ManufacturerTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
"Manufacturer 6,manufacturer-6,Sixth manufacturer",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
|
||||
# TODO: Change base class to PrimaryObjectViewTestCase
|
||||
# Blocked by absence of bulk import view for DeviceTypes
|
||||
@ -366,7 +413,7 @@ class DeviceTypeTestCase(
|
||||
DeviceType(model='Device Type 3', slug='device-type-3', manufacturer=manufacturers[0]),
|
||||
])
|
||||
|
||||
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'manufacturer': manufacturers[1].pk,
|
||||
@ -885,6 +932,11 @@ class DeviceRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
"Device Role 6,device-role-6,0000ff",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'color': '00ff00',
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
|
||||
class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
model = Platform
|
||||
@ -916,6 +968,11 @@ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
"Platform 6,platform-6,Sixth platform",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'napalm_driver': 'ios',
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
|
||||
class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = Device
|
||||
@ -929,11 +986,11 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
rack_group = RackGroup(site=sites[0], name='Rack Group 1', slug='rack-group-1')
|
||||
rack_group.save()
|
||||
location = Location(site=sites[0], name='Location 1', slug='location-1')
|
||||
location.save()
|
||||
|
||||
racks = (
|
||||
Rack(name='Rack 1', site=sites[0], group=rack_group),
|
||||
Rack(name='Rack 1', site=sites[0], location=location),
|
||||
Rack(name='Rack 2', site=sites[1]),
|
||||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
@ -964,7 +1021,7 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
Device(name='Device 3', site=sites[0], rack=racks[0], device_type=devicetypes[0], device_role=deviceroles[0], platform=platforms[0]),
|
||||
])
|
||||
|
||||
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'device_type': devicetypes[1].pk,
|
||||
@ -991,10 +1048,10 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"device_role,manufacturer,device_type,status,name,site,rack_group,rack,position,face",
|
||||
"Device Role 1,Manufacturer 1,Device Type 1,active,Device 4,Site 1,Rack Group 1,Rack 1,10,front",
|
||||
"Device Role 1,Manufacturer 1,Device Type 1,active,Device 5,Site 1,Rack Group 1,Rack 1,20,front",
|
||||
"Device Role 1,Manufacturer 1,Device Type 1,active,Device 6,Site 1,Rack Group 1,Rack 1,30,front",
|
||||
"device_role,manufacturer,device_type,status,name,site,location,rack,position,face",
|
||||
"Device Role 1,Manufacturer 1,Device Type 1,active,Device 4,Site 1,Location 1,Rack 1,10,front",
|
||||
"Device Role 1,Manufacturer 1,Device Type 1,active,Device 5,Site 1,Location 1,Rack 1,20,front",
|
||||
"Device Role 1,Manufacturer 1,Device Type 1,active,Device 6,Site 1,Location 1,Rack 1,30,front",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
@ -1144,7 +1201,7 @@ class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
ConsolePort(device=device, name='Console Port 3'),
|
||||
])
|
||||
|
||||
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'device': device.pk,
|
||||
@ -1176,6 +1233,18 @@ class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
"Device 1,Console Port 6",
|
||||
)
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||
def test_trace(self):
|
||||
consoleport = ConsolePort.objects.first()
|
||||
consoleserverport = ConsoleServerPort.objects.create(
|
||||
device=consoleport.device,
|
||||
name='Console Server Port 1'
|
||||
)
|
||||
Cable(termination_a=consoleport, termination_b=consoleserverport).save()
|
||||
|
||||
response = self.client.get(reverse('dcim:consoleport_trace', kwargs={'pk': consoleport.pk}))
|
||||
self.assertHttpStatus(response, 200)
|
||||
|
||||
|
||||
class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
model = ConsoleServerPort
|
||||
@ -1190,7 +1259,7 @@ class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
ConsoleServerPort(device=device, name='Console Server Port 3'),
|
||||
])
|
||||
|
||||
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'device': device.pk,
|
||||
@ -1220,6 +1289,18 @@ class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
"Device 1,Console Server Port 6",
|
||||
)
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||
def test_trace(self):
|
||||
consoleserverport = ConsoleServerPort.objects.first()
|
||||
consoleport = ConsolePort.objects.create(
|
||||
device=consoleserverport.device,
|
||||
name='Console Port 1'
|
||||
)
|
||||
Cable(termination_a=consoleserverport, termination_b=consoleport).save()
|
||||
|
||||
response = self.client.get(reverse('dcim:consoleserverport_trace', kwargs={'pk': consoleserverport.pk}))
|
||||
self.assertHttpStatus(response, 200)
|
||||
|
||||
|
||||
class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
model = PowerPort
|
||||
@ -1234,7 +1315,7 @@ class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
PowerPort(device=device, name='Power Port 3'),
|
||||
])
|
||||
|
||||
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'device': device.pk,
|
||||
@ -1270,6 +1351,18 @@ class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
"Device 1,Power Port 6",
|
||||
)
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||
def test_trace(self):
|
||||
powerport = PowerPort.objects.first()
|
||||
poweroutlet = PowerOutlet.objects.create(
|
||||
device=powerport.device,
|
||||
name='Power Outlet 1'
|
||||
)
|
||||
Cable(termination_a=powerport, termination_b=poweroutlet).save()
|
||||
|
||||
response = self.client.get(reverse('dcim:powerport_trace', kwargs={'pk': powerport.pk}))
|
||||
self.assertHttpStatus(response, 200)
|
||||
|
||||
|
||||
class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
model = PowerOutlet
|
||||
@ -1290,7 +1383,7 @@ class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
PowerOutlet(device=device, name='Power Outlet 3', power_port=powerports[0]),
|
||||
])
|
||||
|
||||
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'device': device.pk,
|
||||
@ -1326,6 +1419,15 @@ class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
"Device 1,Power Outlet 6",
|
||||
)
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||
def test_trace(self):
|
||||
poweroutlet = PowerOutlet.objects.first()
|
||||
powerport = PowerPort.objects.first()
|
||||
Cable(termination_a=poweroutlet, termination_b=powerport).save()
|
||||
|
||||
response = self.client.get(reverse('dcim:poweroutlet_trace', kwargs={'pk': poweroutlet.pk}))
|
||||
self.assertHttpStatus(response, 200)
|
||||
|
||||
|
||||
class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
model = Interface
|
||||
@ -1350,7 +1452,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
)
|
||||
VLAN.objects.bulk_create(vlans)
|
||||
|
||||
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'device': device.pk,
|
||||
@ -1405,6 +1507,14 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
"Device 1,Interface 6,1000base-t",
|
||||
)
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||
def test_trace(self):
|
||||
interface1, interface2 = Interface.objects.all()[:2]
|
||||
Cable(termination_a=interface1, termination_b=interface2).save()
|
||||
|
||||
response = self.client.get(reverse('dcim:interface_trace', kwargs={'pk': interface1.pk}))
|
||||
self.assertHttpStatus(response, 200)
|
||||
|
||||
|
||||
class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
model = FrontPort
|
||||
@ -1429,7 +1539,7 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
FrontPort(device=device, name='Front Port 3', rear_port=rearports[2]),
|
||||
])
|
||||
|
||||
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'device': device.pk,
|
||||
@ -1464,6 +1574,18 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
"Device 1,Front Port 6,8p8c,Rear Port 6,1",
|
||||
)
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||
def test_trace(self):
|
||||
frontport = FrontPort.objects.first()
|
||||
interface = Interface.objects.create(
|
||||
device=frontport.device,
|
||||
name='Interface 1'
|
||||
)
|
||||
Cable(termination_a=frontport, termination_b=interface).save()
|
||||
|
||||
response = self.client.get(reverse('dcim:frontport_trace', kwargs={'pk': frontport.pk}))
|
||||
self.assertHttpStatus(response, 200)
|
||||
|
||||
|
||||
class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
model = RearPort
|
||||
@ -1478,7 +1600,7 @@ class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
RearPort(device=device, name='Rear Port 3'),
|
||||
])
|
||||
|
||||
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'device': device.pk,
|
||||
@ -1510,6 +1632,18 @@ class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
"Device 1,Rear Port 6,8p8c,1",
|
||||
)
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||
def test_trace(self):
|
||||
rearport = RearPort.objects.first()
|
||||
interface = Interface.objects.create(
|
||||
device=rearport.device,
|
||||
name='Interface 1'
|
||||
)
|
||||
Cable(termination_a=rearport, termination_b=interface).save()
|
||||
|
||||
response = self.client.get(reverse('dcim:rearport_trace', kwargs={'pk': rearport.pk}))
|
||||
self.assertHttpStatus(response, 200)
|
||||
|
||||
|
||||
class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
model = DeviceBay
|
||||
@ -1527,7 +1661,7 @@ class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
DeviceBay(device=device, name='Device Bay 3'),
|
||||
])
|
||||
|
||||
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'device': device.pk,
|
||||
@ -1567,7 +1701,7 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
InventoryItem.objects.create(device=device, name='Inventory Item 2')
|
||||
InventoryItem.objects.create(device=device, name='Inventory Item 3')
|
||||
|
||||
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'device': device.pk,
|
||||
@ -1657,7 +1791,7 @@ class CableTestCase(
|
||||
Cable(termination_a=interfaces[1], termination_b=interfaces[4], type=CableTypeChoices.TYPE_CAT6).save()
|
||||
Cable(termination_a=interfaces[2], termination_b=interfaces[5], type=CableTypeChoices.TYPE_CAT6).save()
|
||||
|
||||
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
interface_ct = ContentType.objects.get_for_model(Interface)
|
||||
cls.form_data = {
|
||||
@ -1771,38 +1905,38 @@ class PowerPanelTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
rackgroups = (
|
||||
RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]),
|
||||
RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]),
|
||||
locations = (
|
||||
Location(name='Location 1', slug='location-1', site=sites[0]),
|
||||
Location(name='Location 2', slug='location-2', site=sites[1]),
|
||||
)
|
||||
for rackgroup in rackgroups:
|
||||
rackgroup.save()
|
||||
for location in locations:
|
||||
location.save()
|
||||
|
||||
PowerPanel.objects.bulk_create((
|
||||
PowerPanel(site=sites[0], rack_group=rackgroups[0], name='Power Panel 1'),
|
||||
PowerPanel(site=sites[0], rack_group=rackgroups[0], name='Power Panel 2'),
|
||||
PowerPanel(site=sites[0], rack_group=rackgroups[0], name='Power Panel 3'),
|
||||
PowerPanel(site=sites[0], location=locations[0], name='Power Panel 1'),
|
||||
PowerPanel(site=sites[0], location=locations[0], name='Power Panel 2'),
|
||||
PowerPanel(site=sites[0], location=locations[0], name='Power Panel 3'),
|
||||
))
|
||||
|
||||
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'site': sites[1].pk,
|
||||
'rack_group': rackgroups[1].pk,
|
||||
'location': locations[1].pk,
|
||||
'name': 'Power Panel X',
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"site,rack_group,name",
|
||||
"Site 1,Rack Group 1,Power Panel 4",
|
||||
"Site 1,Rack Group 1,Power Panel 5",
|
||||
"Site 1,Rack Group 1,Power Panel 6",
|
||||
"site,location,name",
|
||||
"Site 1,Location 1,Power Panel 4",
|
||||
"Site 1,Location 1,Power Panel 5",
|
||||
"Site 1,Location 1,Power Panel 6",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'site': sites[1].pk,
|
||||
'rack_group': rackgroups[1].pk,
|
||||
'location': locations[1].pk,
|
||||
}
|
||||
|
||||
|
||||
@ -1832,7 +1966,7 @@ class PowerFeedTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
PowerFeed(name='Power Feed 3', power_panel=powerpanels[0], rack=racks[0]),
|
||||
))
|
||||
|
||||
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'name': 'Power Feed X',
|
||||
@ -1868,3 +2002,26 @@ class PowerFeedTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'max_utilization': 50,
|
||||
'comments': 'New comments',
|
||||
}
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||
def test_trace(self):
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer', slug='manufacturer-1')
|
||||
device_type = DeviceType.objects.create(
|
||||
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
|
||||
)
|
||||
device_role = DeviceRole.objects.create(
|
||||
name='Device Role', slug='device-role-1'
|
||||
)
|
||||
device = Device.objects.create(
|
||||
site=Site.objects.first(), device_type=device_type, device_role=device_role
|
||||
)
|
||||
|
||||
powerfeed = PowerFeed.objects.first()
|
||||
powerport = PowerPort.objects.create(
|
||||
device=device,
|
||||
name='Power Port 1'
|
||||
)
|
||||
Cable(termination_a=powerfeed, termination_b=powerport).save()
|
||||
|
||||
response = self.client.get(reverse('dcim:powerfeed_trace', kwargs={'pk': powerfeed.pk}))
|
||||
self.assertHttpStatus(response, 200)
|
||||
|
||||
@ -1,13 +1,10 @@
|
||||
from django.urls import path
|
||||
|
||||
from extras.views import ObjectChangeLogView, ImageAttachmentEditView
|
||||
from extras.views import ImageAttachmentEditView, ObjectChangeLogView, ObjectJournalView
|
||||
from ipam.views import ServiceEditView
|
||||
from utilities.views import SlugRedirectView
|
||||
from . import views
|
||||
from .models import (
|
||||
Cable, ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, DeviceType, FrontPort, Interface,
|
||||
InventoryItem, Manufacturer, Platform, PowerFeed, PowerPanel, PowerPort, PowerOutlet, Rack, RackGroup,
|
||||
RackReservation, RackRole, RearPort, Region, Site, VirtualChassis,
|
||||
)
|
||||
from .models import *
|
||||
|
||||
app_name = 'dcim'
|
||||
urlpatterns = [
|
||||
@ -16,37 +13,57 @@ urlpatterns = [
|
||||
path('regions/', views.RegionListView.as_view(), name='region_list'),
|
||||
path('regions/add/', views.RegionEditView.as_view(), name='region_add'),
|
||||
path('regions/import/', views.RegionBulkImportView.as_view(), name='region_import'),
|
||||
path('regions/edit/', views.RegionBulkEditView.as_view(), name='region_bulk_edit'),
|
||||
path('regions/delete/', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'),
|
||||
path('regions/<int:pk>/', views.RegionView.as_view(), name='region'),
|
||||
path('regions/<int:pk>/edit/', views.RegionEditView.as_view(), name='region_edit'),
|
||||
path('regions/<int:pk>/delete/', views.RegionDeleteView.as_view(), name='region_delete'),
|
||||
path('regions/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='region_changelog', kwargs={'model': Region}),
|
||||
|
||||
# Site groups
|
||||
path('site-groups/', views.SiteGroupListView.as_view(), name='sitegroup_list'),
|
||||
path('site-groups/add/', views.SiteGroupEditView.as_view(), name='sitegroup_add'),
|
||||
path('site-groups/import/', views.SiteGroupBulkImportView.as_view(), name='sitegroup_import'),
|
||||
path('site-groups/edit/', views.SiteGroupBulkEditView.as_view(), name='sitegroup_bulk_edit'),
|
||||
path('site-groups/delete/', views.SiteGroupBulkDeleteView.as_view(), name='sitegroup_bulk_delete'),
|
||||
path('site-groups/<int:pk>/', views.SiteGroupView.as_view(), name='sitegroup'),
|
||||
path('site-groups/<int:pk>/edit/', views.SiteGroupEditView.as_view(), name='sitegroup_edit'),
|
||||
path('site-groups/<int:pk>/delete/', views.SiteGroupDeleteView.as_view(), name='sitegroup_delete'),
|
||||
path('site-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='sitegroup_changelog', kwargs={'model': SiteGroup}),
|
||||
|
||||
# Sites
|
||||
path('sites/', views.SiteListView.as_view(), name='site_list'),
|
||||
path('sites/add/', views.SiteEditView.as_view(), name='site_add'),
|
||||
path('sites/import/', views.SiteBulkImportView.as_view(), name='site_import'),
|
||||
path('sites/edit/', views.SiteBulkEditView.as_view(), name='site_bulk_edit'),
|
||||
path('sites/delete/', views.SiteBulkDeleteView.as_view(), name='site_bulk_delete'),
|
||||
path('sites/<slug:slug>/', views.SiteView.as_view(), name='site'),
|
||||
path('sites/<slug:slug>/edit/', views.SiteEditView.as_view(), name='site_edit'),
|
||||
path('sites/<slug:slug>/delete/', views.SiteDeleteView.as_view(), name='site_delete'),
|
||||
path('sites/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='site_changelog', kwargs={'model': Site}),
|
||||
path('sites/<int:pk>/', views.SiteView.as_view(), name='site'),
|
||||
path('sites/<slug:slug>/', SlugRedirectView.as_view(), kwargs={'model': Site}),
|
||||
path('sites/<int:pk>/edit/', views.SiteEditView.as_view(), name='site_edit'),
|
||||
path('sites/<int:pk>/delete/', views.SiteDeleteView.as_view(), name='site_delete'),
|
||||
path('sites/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='site_changelog', kwargs={'model': Site}),
|
||||
path('sites/<int:pk>/journal/', ObjectJournalView.as_view(), name='site_journal', kwargs={'model': Site}),
|
||||
path('sites/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='site_add_image', kwargs={'model': Site}),
|
||||
|
||||
# Rack groups
|
||||
path('rack-groups/', views.RackGroupListView.as_view(), name='rackgroup_list'),
|
||||
path('rack-groups/add/', views.RackGroupEditView.as_view(), name='rackgroup_add'),
|
||||
path('rack-groups/import/', views.RackGroupBulkImportView.as_view(), name='rackgroup_import'),
|
||||
path('rack-groups/delete/', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'),
|
||||
path('rack-groups/<int:pk>/edit/', views.RackGroupEditView.as_view(), name='rackgroup_edit'),
|
||||
path('rack-groups/<int:pk>/delete/', views.RackGroupDeleteView.as_view(), name='rackgroup_delete'),
|
||||
path('rack-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackgroup_changelog', kwargs={'model': RackGroup}),
|
||||
# Locations
|
||||
path('locations/', views.LocationListView.as_view(), name='location_list'),
|
||||
path('locations/add/', views.LocationEditView.as_view(), name='location_add'),
|
||||
path('locations/import/', views.LocationBulkImportView.as_view(), name='location_import'),
|
||||
path('locations/edit/', views.LocationBulkEditView.as_view(), name='location_bulk_edit'),
|
||||
path('locations/delete/', views.LocationBulkDeleteView.as_view(), name='location_bulk_delete'),
|
||||
path('locations/<int:pk>/', views.LocationView.as_view(), name='location'),
|
||||
path('locations/<int:pk>/edit/', views.LocationEditView.as_view(), name='location_edit'),
|
||||
path('locations/<int:pk>/delete/', views.LocationDeleteView.as_view(), name='location_delete'),
|
||||
path('locations/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='location_changelog', kwargs={'model': Location}),
|
||||
path('locations/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='location_add_image', kwargs={'model': Location}),
|
||||
|
||||
# Rack roles
|
||||
path('rack-roles/', views.RackRoleListView.as_view(), name='rackrole_list'),
|
||||
path('rack-roles/add/', views.RackRoleEditView.as_view(), name='rackrole_add'),
|
||||
path('rack-roles/import/', views.RackRoleBulkImportView.as_view(), name='rackrole_import'),
|
||||
path('rack-roles/edit/', views.RackRoleBulkEditView.as_view(), name='rackrole_bulk_edit'),
|
||||
path('rack-roles/delete/', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'),
|
||||
path('rack-roles/<int:pk>/', views.RackRoleView.as_view(), name='rackrole'),
|
||||
path('rack-roles/<int:pk>/edit/', views.RackRoleEditView.as_view(), name='rackrole_edit'),
|
||||
path('rack-roles/<int:pk>/delete/', views.RackRoleDeleteView.as_view(), name='rackrole_delete'),
|
||||
path('rack-roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackrole_changelog', kwargs={'model': RackRole}),
|
||||
@ -61,6 +78,7 @@ urlpatterns = [
|
||||
path('rack-reservations/<int:pk>/edit/', views.RackReservationEditView.as_view(), name='rackreservation_edit'),
|
||||
path('rack-reservations/<int:pk>/delete/', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'),
|
||||
path('rack-reservations/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackreservation_changelog', kwargs={'model': RackReservation}),
|
||||
path('rack-reservations/<int:pk>/journal/', ObjectJournalView.as_view(), name='rackreservation_journal', kwargs={'model': RackReservation}),
|
||||
|
||||
# Racks
|
||||
path('racks/', views.RackListView.as_view(), name='rack_list'),
|
||||
@ -73,16 +91,19 @@ urlpatterns = [
|
||||
path('racks/<int:pk>/edit/', views.RackEditView.as_view(), name='rack_edit'),
|
||||
path('racks/<int:pk>/delete/', views.RackDeleteView.as_view(), name='rack_delete'),
|
||||
path('racks/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rack_changelog', kwargs={'model': Rack}),
|
||||
path('racks/<int:pk>/journal/', ObjectJournalView.as_view(), name='rack_journal', kwargs={'model': Rack}),
|
||||
path('racks/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}),
|
||||
|
||||
# Manufacturers
|
||||
path('manufacturers/', views.ManufacturerListView.as_view(), name='manufacturer_list'),
|
||||
path('manufacturers/add/', views.ManufacturerEditView.as_view(), name='manufacturer_add'),
|
||||
path('manufacturers/import/', views.ManufacturerBulkImportView.as_view(), name='manufacturer_import'),
|
||||
path('manufacturers/edit/', views.ManufacturerBulkEditView.as_view(), name='manufacturer_bulk_edit'),
|
||||
path('manufacturers/delete/', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'),
|
||||
path('manufacturers/<slug:slug>/edit/', views.ManufacturerEditView.as_view(), name='manufacturer_edit'),
|
||||
path('manufacturers/<slug:slug>/delete/', views.ManufacturerDeleteView.as_view(), name='manufacturer_delete'),
|
||||
path('manufacturers/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='manufacturer_changelog', kwargs={'model': Manufacturer}),
|
||||
path('manufacturers/<int:pk>/', views.ManufacturerView.as_view(), name='manufacturer'),
|
||||
path('manufacturers/<int:pk>/edit/', views.ManufacturerEditView.as_view(), name='manufacturer_edit'),
|
||||
path('manufacturers/<int:pk>/delete/', views.ManufacturerDeleteView.as_view(), name='manufacturer_delete'),
|
||||
path('manufacturers/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='manufacturer_changelog', kwargs={'model': Manufacturer}),
|
||||
|
||||
# Device types
|
||||
path('device-types/', views.DeviceTypeListView.as_view(), name='devicetype_list'),
|
||||
@ -94,6 +115,7 @@ urlpatterns = [
|
||||
path('device-types/<int:pk>/edit/', views.DeviceTypeEditView.as_view(), name='devicetype_edit'),
|
||||
path('device-types/<int:pk>/delete/', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
|
||||
path('device-types/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}),
|
||||
path('device-types/<int:pk>/journal/', ObjectJournalView.as_view(), name='devicetype_journal', kwargs={'model': DeviceType}),
|
||||
|
||||
# Console port templates
|
||||
path('console-port-templates/add/', views.ConsolePortTemplateCreateView.as_view(), name='consoleporttemplate_add'),
|
||||
@ -163,19 +185,23 @@ urlpatterns = [
|
||||
path('device-roles/', views.DeviceRoleListView.as_view(), name='devicerole_list'),
|
||||
path('device-roles/add/', views.DeviceRoleEditView.as_view(), name='devicerole_add'),
|
||||
path('device-roles/import/', views.DeviceRoleBulkImportView.as_view(), name='devicerole_import'),
|
||||
path('device-roles/edit/', views.DeviceRoleBulkEditView.as_view(), name='devicerole_bulk_edit'),
|
||||
path('device-roles/delete/', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'),
|
||||
path('device-roles/<slug:slug>/edit/', views.DeviceRoleEditView.as_view(), name='devicerole_edit'),
|
||||
path('device-roles/<slug:slug>/delete/', views.DeviceRoleDeleteView.as_view(), name='devicerole_delete'),
|
||||
path('device-roles/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='devicerole_changelog', kwargs={'model': DeviceRole}),
|
||||
path('device-roles/<int:pk>/', views.DeviceRoleView.as_view(), name='devicerole'),
|
||||
path('device-roles/<int:pk>/edit/', views.DeviceRoleEditView.as_view(), name='devicerole_edit'),
|
||||
path('device-roles/<int:pk>/delete/', views.DeviceRoleDeleteView.as_view(), name='devicerole_delete'),
|
||||
path('device-roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='devicerole_changelog', kwargs={'model': DeviceRole}),
|
||||
|
||||
# Platforms
|
||||
path('platforms/', views.PlatformListView.as_view(), name='platform_list'),
|
||||
path('platforms/add/', views.PlatformEditView.as_view(), name='platform_add'),
|
||||
path('platforms/import/', views.PlatformBulkImportView.as_view(), name='platform_import'),
|
||||
path('platforms/edit/', views.PlatformBulkEditView.as_view(), name='platform_bulk_edit'),
|
||||
path('platforms/delete/', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'),
|
||||
path('platforms/<slug:slug>/edit/', views.PlatformEditView.as_view(), name='platform_edit'),
|
||||
path('platforms/<slug:slug>/delete/', views.PlatformDeleteView.as_view(), name='platform_delete'),
|
||||
path('platforms/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='platform_changelog', kwargs={'model': Platform}),
|
||||
path('platforms/<int:pk>/', views.PlatformView.as_view(), name='platform'),
|
||||
path('platforms/<int:pk>/edit/', views.PlatformEditView.as_view(), name='platform_edit'),
|
||||
path('platforms/<int:pk>/delete/', views.PlatformDeleteView.as_view(), name='platform_delete'),
|
||||
path('platforms/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='platform_changelog', kwargs={'model': Platform}),
|
||||
|
||||
# Devices
|
||||
path('devices/', views.DeviceListView.as_view(), name='device_list'),
|
||||
@ -198,6 +224,7 @@ urlpatterns = [
|
||||
path('devices/<int:pk>/inventory/', views.DeviceInventoryView.as_view(), name='device_inventory'),
|
||||
path('devices/<int:pk>/config-context/', views.DeviceConfigContextView.as_view(), name='device_configcontext'),
|
||||
path('devices/<int:pk>/changelog/', views.DeviceChangeLogView.as_view(), name='device_changelog', kwargs={'model': Device}),
|
||||
path('devices/<int:pk>/journal/', views.DeviceJournalView.as_view(), name='device_journal', kwargs={'model': Device}),
|
||||
path('devices/<int:pk>/status/', views.DeviceStatusView.as_view(), name='device_status'),
|
||||
path('devices/<int:pk>/lldp-neighbors/', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
|
||||
path('devices/<int:pk>/config/', views.DeviceConfigView.as_view(), name='device_config'),
|
||||
@ -353,6 +380,7 @@ urlpatterns = [
|
||||
path('cables/<int:pk>/edit/', views.CableEditView.as_view(), name='cable_edit'),
|
||||
path('cables/<int:pk>/delete/', views.CableDeleteView.as_view(), name='cable_delete'),
|
||||
path('cables/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='cable_changelog', kwargs={'model': Cable}),
|
||||
path('cables/<int:pk>/journal/', ObjectJournalView.as_view(), name='cable_journal', kwargs={'model': Cable}),
|
||||
|
||||
# Console/power/interface connections (read-only)
|
||||
path('console-connections/', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'),
|
||||
@ -369,6 +397,7 @@ urlpatterns = [
|
||||
path('virtual-chassis/<int:pk>/edit/', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'),
|
||||
path('virtual-chassis/<int:pk>/delete/', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'),
|
||||
path('virtual-chassis/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='virtualchassis_changelog', kwargs={'model': VirtualChassis}),
|
||||
path('virtual-chassis/<int:pk>/journal/', ObjectJournalView.as_view(), name='virtualchassis_journal', kwargs={'model': VirtualChassis}),
|
||||
path('virtual-chassis/<int:pk>/add-member/', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'),
|
||||
path('virtual-chassis-members/<int:pk>/delete/', views.VirtualChassisRemoveMemberView.as_view(), name='virtualchassis_remove_member'),
|
||||
|
||||
@ -382,18 +411,21 @@ urlpatterns = [
|
||||
path('power-panels/<int:pk>/edit/', views.PowerPanelEditView.as_view(), name='powerpanel_edit'),
|
||||
path('power-panels/<int:pk>/delete/', views.PowerPanelDeleteView.as_view(), name='powerpanel_delete'),
|
||||
path('power-panels/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='powerpanel_changelog', kwargs={'model': PowerPanel}),
|
||||
path('power-panels/<int:pk>/journal/', ObjectJournalView.as_view(), name='powerpanel_journal', kwargs={'model': PowerPanel}),
|
||||
|
||||
# Power feeds
|
||||
path('power-feeds/', views.PowerFeedListView.as_view(), name='powerfeed_list'),
|
||||
path('power-feeds/add/', views.PowerFeedEditView.as_view(), name='powerfeed_add'),
|
||||
path('power-feeds/import/', views.PowerFeedBulkImportView.as_view(), name='powerfeed_import'),
|
||||
path('power-feeds/edit/', views.PowerFeedBulkEditView.as_view(), name='powerfeed_bulk_edit'),
|
||||
path('power-feeds/disconnect/', views.PowerFeedBulkDisconnectView.as_view(), name='powerfeed_bulk_disconnect'),
|
||||
path('power-feeds/delete/', views.PowerFeedBulkDeleteView.as_view(), name='powerfeed_bulk_delete'),
|
||||
path('power-feeds/<int:pk>/', views.PowerFeedView.as_view(), name='powerfeed'),
|
||||
path('power-feeds/<int:pk>/edit/', views.PowerFeedEditView.as_view(), name='powerfeed_edit'),
|
||||
path('power-feeds/<int:pk>/delete/', views.PowerFeedDeleteView.as_view(), name='powerfeed_delete'),
|
||||
path('power-feeds/<int:pk>/trace/', views.PathTraceView.as_view(), name='powerfeed_trace', kwargs={'model': PowerFeed}),
|
||||
path('power-feeds/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='powerfeed_changelog', kwargs={'model': PowerFeed}),
|
||||
path('power-feeds/<int:pk>/journal/', ObjectJournalView.as_view(), name='powerfeed_journal', kwargs={'model': PowerFeed}),
|
||||
path('power-feeds/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='powerfeed_connect', kwargs={'termination_a_type': PowerFeed}),
|
||||
|
||||
]
|
||||
|
||||
@ -12,7 +12,7 @@ from django.utils.safestring import mark_safe
|
||||
from django.views.generic import View
|
||||
|
||||
from circuits.models import Circuit
|
||||
from extras.views import ObjectChangeLogView, ObjectConfigContextView
|
||||
from extras.views import ObjectChangeLogView, ObjectConfigContextView, ObjectJournalView
|
||||
from ipam.models import IPAddress, Prefix, Service, VLAN
|
||||
from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable
|
||||
from netbox.views import generic
|
||||
@ -20,6 +20,7 @@ from secrets.models import Secret
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
||||
from utilities.permissions import get_permission_for_model
|
||||
from utilities.tables import paginate_table
|
||||
from utilities.utils import csv_format, count_related
|
||||
from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin
|
||||
from virtualization.models import VirtualMachine
|
||||
@ -30,8 +31,8 @@ from .models import (
|
||||
Cable, CablePath, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
|
||||
InventoryItem, Manufacturer, PathEndpoint, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel,
|
||||
PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
|
||||
VirtualChassis,
|
||||
PowerPort, PowerPortTemplate, Rack, Location, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
|
||||
SiteGroup, VirtualChassis,
|
||||
)
|
||||
|
||||
|
||||
@ -111,6 +112,34 @@ class RegionListView(generic.ObjectListView):
|
||||
table = tables.RegionTable
|
||||
|
||||
|
||||
class RegionView(generic.ObjectView):
|
||||
queryset = Region.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
child_regions = Region.objects.add_related_count(
|
||||
Region.objects.all(),
|
||||
Site,
|
||||
'region',
|
||||
'site_count',
|
||||
cumulative=True
|
||||
).restrict(request.user, 'view').filter(
|
||||
parent__in=instance.get_descendants(include_self=True)
|
||||
)
|
||||
child_regions_table = tables.RegionTable(child_regions)
|
||||
|
||||
sites = Site.objects.restrict(request.user, 'view').filter(
|
||||
region=instance
|
||||
)
|
||||
sites_table = tables.SiteTable(sites)
|
||||
sites_table.columns.hide('region')
|
||||
paginate_table(sites_table, request)
|
||||
|
||||
return {
|
||||
'child_regions_table': child_regions_table,
|
||||
'sites_table': sites_table,
|
||||
}
|
||||
|
||||
|
||||
class RegionEditView(generic.ObjectEditView):
|
||||
queryset = Region.objects.all()
|
||||
model_form = forms.RegionForm
|
||||
@ -126,6 +155,19 @@ class RegionBulkImportView(generic.BulkImportView):
|
||||
table = tables.RegionTable
|
||||
|
||||
|
||||
class RegionBulkEditView(generic.BulkEditView):
|
||||
queryset = Region.objects.add_related_count(
|
||||
Region.objects.all(),
|
||||
Site,
|
||||
'region',
|
||||
'site_count',
|
||||
cumulative=True
|
||||
)
|
||||
filterset = filters.RegionFilterSet
|
||||
table = tables.RegionTable
|
||||
form = forms.RegionBulkEditForm
|
||||
|
||||
|
||||
class RegionBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Region.objects.add_related_count(
|
||||
Region.objects.all(),
|
||||
@ -138,6 +180,91 @@ class RegionBulkDeleteView(generic.BulkDeleteView):
|
||||
table = tables.RegionTable
|
||||
|
||||
|
||||
#
|
||||
# Site groups
|
||||
#
|
||||
|
||||
class SiteGroupListView(generic.ObjectListView):
|
||||
queryset = SiteGroup.objects.add_related_count(
|
||||
SiteGroup.objects.all(),
|
||||
Site,
|
||||
'group',
|
||||
'site_count',
|
||||
cumulative=True
|
||||
)
|
||||
filterset = filters.SiteGroupFilterSet
|
||||
filterset_form = forms.SiteGroupFilterForm
|
||||
table = tables.SiteGroupTable
|
||||
|
||||
|
||||
class SiteGroupView(generic.ObjectView):
|
||||
queryset = SiteGroup.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
child_groups = SiteGroup.objects.add_related_count(
|
||||
SiteGroup.objects.all(),
|
||||
Site,
|
||||
'group',
|
||||
'site_count',
|
||||
cumulative=True
|
||||
).restrict(request.user, 'view').filter(
|
||||
parent__in=instance.get_descendants(include_self=True)
|
||||
)
|
||||
child_groups_table = tables.SiteGroupTable(child_groups)
|
||||
|
||||
sites = Site.objects.restrict(request.user, 'view').filter(
|
||||
group=instance
|
||||
)
|
||||
sites_table = tables.SiteTable(sites)
|
||||
sites_table.columns.hide('group')
|
||||
paginate_table(sites_table, request)
|
||||
|
||||
return {
|
||||
'child_groups_table': child_groups_table,
|
||||
'sites_table': sites_table,
|
||||
}
|
||||
|
||||
|
||||
class SiteGroupEditView(generic.ObjectEditView):
|
||||
queryset = SiteGroup.objects.all()
|
||||
model_form = forms.SiteGroupForm
|
||||
|
||||
|
||||
class SiteGroupDeleteView(generic.ObjectDeleteView):
|
||||
queryset = SiteGroup.objects.all()
|
||||
|
||||
|
||||
class SiteGroupBulkImportView(generic.BulkImportView):
|
||||
queryset = SiteGroup.objects.all()
|
||||
model_form = forms.SiteGroupCSVForm
|
||||
table = tables.SiteGroupTable
|
||||
|
||||
|
||||
class SiteGroupBulkEditView(generic.BulkEditView):
|
||||
queryset = SiteGroup.objects.add_related_count(
|
||||
SiteGroup.objects.all(),
|
||||
Site,
|
||||
'group',
|
||||
'site_count',
|
||||
cumulative=True
|
||||
)
|
||||
filterset = filters.SiteGroupFilterSet
|
||||
table = tables.SiteGroupTable
|
||||
form = forms.SiteGroupBulkEditForm
|
||||
|
||||
|
||||
class SiteGroupBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = SiteGroup.objects.add_related_count(
|
||||
SiteGroup.objects.all(),
|
||||
Site,
|
||||
'group',
|
||||
'site_count',
|
||||
cumulative=True
|
||||
)
|
||||
filterset = filters.SiteGroupFilterSet
|
||||
table = tables.SiteGroupTable
|
||||
|
||||
|
||||
#
|
||||
# Sites
|
||||
#
|
||||
@ -161,24 +288,30 @@ class SiteView(generic.ObjectView):
|
||||
'circuit_count': Circuit.objects.restrict(request.user, 'view').filter(terminations__site=instance).count(),
|
||||
'vm_count': VirtualMachine.objects.restrict(request.user, 'view').filter(cluster__site=instance).count(),
|
||||
}
|
||||
rack_groups = RackGroup.objects.add_related_count(
|
||||
RackGroup.objects.all(),
|
||||
locations = Location.objects.add_related_count(
|
||||
Location.objects.all(),
|
||||
Rack,
|
||||
'group',
|
||||
'location',
|
||||
'rack_count',
|
||||
cumulative=True
|
||||
)
|
||||
locations = Location.objects.add_related_count(
|
||||
locations,
|
||||
Device,
|
||||
'location',
|
||||
'device_count',
|
||||
cumulative=True
|
||||
).restrict(request.user, 'view').filter(site=instance)
|
||||
|
||||
return {
|
||||
'stats': stats,
|
||||
'rack_groups': rack_groups,
|
||||
'locations': locations,
|
||||
}
|
||||
|
||||
|
||||
class SiteEditView(generic.ObjectEditView):
|
||||
queryset = Site.objects.all()
|
||||
model_form = forms.SiteForm
|
||||
template_name = 'dcim/site_edit.html'
|
||||
|
||||
|
||||
class SiteDeleteView(generic.ObjectDeleteView):
|
||||
@ -205,47 +338,83 @@ class SiteBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
|
||||
#
|
||||
# Rack groups
|
||||
# Locations
|
||||
#
|
||||
|
||||
class RackGroupListView(generic.ObjectListView):
|
||||
queryset = RackGroup.objects.add_related_count(
|
||||
RackGroup.objects.all(),
|
||||
class LocationListView(generic.ObjectListView):
|
||||
queryset = Location.objects.add_related_count(
|
||||
Location.objects.add_related_count(
|
||||
Location.objects.all(),
|
||||
Device,
|
||||
'location',
|
||||
'device_count',
|
||||
cumulative=True
|
||||
),
|
||||
Rack,
|
||||
'group',
|
||||
'location',
|
||||
'rack_count',
|
||||
cumulative=True
|
||||
)
|
||||
filterset = filters.RackGroupFilterSet
|
||||
filterset_form = forms.RackGroupFilterForm
|
||||
table = tables.RackGroupTable
|
||||
filterset = filters.LocationFilterSet
|
||||
filterset_form = forms.LocationFilterForm
|
||||
table = tables.LocationTable
|
||||
|
||||
|
||||
class RackGroupEditView(generic.ObjectEditView):
|
||||
queryset = RackGroup.objects.all()
|
||||
model_form = forms.RackGroupForm
|
||||
class LocationView(generic.ObjectView):
|
||||
queryset = Location.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
devices = Device.objects.restrict(request.user, 'view').filter(
|
||||
location=instance
|
||||
)
|
||||
|
||||
devices_table = tables.DeviceTable(devices)
|
||||
devices_table.columns.hide('location')
|
||||
paginate_table(devices_table, request)
|
||||
|
||||
return {
|
||||
'devices_table': devices_table,
|
||||
}
|
||||
|
||||
|
||||
class RackGroupDeleteView(generic.ObjectDeleteView):
|
||||
queryset = RackGroup.objects.all()
|
||||
class LocationEditView(generic.ObjectEditView):
|
||||
queryset = Location.objects.all()
|
||||
model_form = forms.LocationForm
|
||||
|
||||
|
||||
class RackGroupBulkImportView(generic.BulkImportView):
|
||||
queryset = RackGroup.objects.all()
|
||||
model_form = forms.RackGroupCSVForm
|
||||
table = tables.RackGroupTable
|
||||
class LocationDeleteView(generic.ObjectDeleteView):
|
||||
queryset = Location.objects.all()
|
||||
|
||||
|
||||
class RackGroupBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = RackGroup.objects.add_related_count(
|
||||
RackGroup.objects.all(),
|
||||
class LocationBulkImportView(generic.BulkImportView):
|
||||
queryset = Location.objects.all()
|
||||
model_form = forms.LocationCSVForm
|
||||
table = tables.LocationTable
|
||||
|
||||
|
||||
class LocationBulkEditView(generic.BulkEditView):
|
||||
queryset = Location.objects.add_related_count(
|
||||
Location.objects.all(),
|
||||
Rack,
|
||||
'group',
|
||||
'location',
|
||||
'rack_count',
|
||||
cumulative=True
|
||||
).prefetch_related('site')
|
||||
filterset = filters.RackGroupFilterSet
|
||||
table = tables.RackGroupTable
|
||||
filterset = filters.LocationFilterSet
|
||||
table = tables.LocationTable
|
||||
form = forms.LocationBulkEditForm
|
||||
|
||||
|
||||
class LocationBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Location.objects.add_related_count(
|
||||
Location.objects.all(),
|
||||
Rack,
|
||||
'location',
|
||||
'rack_count',
|
||||
cumulative=True
|
||||
).prefetch_related('site')
|
||||
filterset = filters.LocationFilterSet
|
||||
table = tables.LocationTable
|
||||
|
||||
|
||||
#
|
||||
@ -259,6 +428,23 @@ class RackRoleListView(generic.ObjectListView):
|
||||
table = tables.RackRoleTable
|
||||
|
||||
|
||||
class RackRoleView(generic.ObjectView):
|
||||
queryset = RackRole.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
racks = Rack.objects.restrict(request.user, 'view').filter(
|
||||
role=instance
|
||||
)
|
||||
|
||||
racks_table = tables.RackTable(racks)
|
||||
racks_table.columns.hide('role')
|
||||
paginate_table(racks_table, request)
|
||||
|
||||
return {
|
||||
'racks_table': racks_table,
|
||||
}
|
||||
|
||||
|
||||
class RackRoleEditView(generic.ObjectEditView):
|
||||
queryset = RackRole.objects.all()
|
||||
model_form = forms.RackRoleForm
|
||||
@ -274,6 +460,15 @@ class RackRoleBulkImportView(generic.BulkImportView):
|
||||
table = tables.RackRoleTable
|
||||
|
||||
|
||||
class RackRoleBulkEditView(generic.BulkEditView):
|
||||
queryset = RackRole.objects.annotate(
|
||||
rack_count=count_related(Rack, 'role')
|
||||
)
|
||||
filterset = filters.RackRoleFilterSet
|
||||
table = tables.RackRoleTable
|
||||
form = forms.RackRoleBulkEditForm
|
||||
|
||||
|
||||
class RackRoleBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = RackRole.objects.annotate(
|
||||
rack_count=count_related(Rack, 'role')
|
||||
@ -287,7 +482,7 @@ class RackRoleBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class RackListView(generic.ObjectListView):
|
||||
queryset = Rack.objects.prefetch_related(
|
||||
'site', 'group', 'tenant', 'role', 'devices__device_type'
|
||||
'site', 'location', 'tenant', 'role', 'devices__device_type'
|
||||
).annotate(
|
||||
device_count=count_related(Device, 'rack')
|
||||
)
|
||||
@ -339,7 +534,7 @@ class RackElevationListView(generic.ObjectListView):
|
||||
|
||||
|
||||
class RackView(generic.ObjectView):
|
||||
queryset = Rack.objects.prefetch_related('site__region', 'tenant__group', 'group', 'role')
|
||||
queryset = Rack.objects.prefetch_related('site__region', 'tenant__group', 'location', 'role')
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
# Get 0U devices located within the rack
|
||||
@ -351,10 +546,10 @@ class RackView(generic.ObjectView):
|
||||
|
||||
peer_racks = Rack.objects.restrict(request.user, 'view').filter(site=instance.site)
|
||||
|
||||
if instance.group:
|
||||
peer_racks = peer_racks.filter(group=instance.group)
|
||||
if instance.location:
|
||||
peer_racks = peer_racks.filter(location=instance.location)
|
||||
else:
|
||||
peer_racks = peer_racks.filter(group__isnull=True)
|
||||
peer_racks = peer_racks.filter(location__isnull=True)
|
||||
next_rack = peer_racks.filter(name__gt=instance.name).order_by('name').first()
|
||||
prev_rack = peer_racks.filter(name__lt=instance.name).order_by('-name').first()
|
||||
|
||||
@ -392,14 +587,14 @@ class RackBulkImportView(generic.BulkImportView):
|
||||
|
||||
|
||||
class RackBulkEditView(generic.BulkEditView):
|
||||
queryset = Rack.objects.prefetch_related('site', 'group', 'tenant', 'role')
|
||||
queryset = Rack.objects.prefetch_related('site', 'location', 'tenant', 'role')
|
||||
filterset = filters.RackFilterSet
|
||||
table = tables.RackTable
|
||||
form = forms.RackBulkEditForm
|
||||
|
||||
|
||||
class RackBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Rack.objects.prefetch_related('site', 'group', 'tenant', 'role')
|
||||
queryset = Rack.objects.prefetch_related('site', 'location', 'tenant', 'role')
|
||||
filterset = filters.RackFilterSet
|
||||
table = tables.RackTable
|
||||
|
||||
@ -422,7 +617,6 @@ class RackReservationView(generic.ObjectView):
|
||||
class RackReservationEditView(generic.ObjectEditView):
|
||||
queryset = RackReservation.objects.all()
|
||||
model_form = forms.RackReservationForm
|
||||
template_name = 'dcim/rackreservation_edit.html'
|
||||
|
||||
def alter_obj(self, obj, request, args, kwargs):
|
||||
if not obj.pk:
|
||||
@ -478,6 +672,23 @@ class ManufacturerListView(generic.ObjectListView):
|
||||
table = tables.ManufacturerTable
|
||||
|
||||
|
||||
class ManufacturerView(generic.ObjectView):
|
||||
queryset = Manufacturer.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
devicetypes = DeviceType.objects.restrict(request.user, 'view').filter(
|
||||
manufacturer=instance
|
||||
)
|
||||
|
||||
devicetypes_table = tables.DeviceTypeTable(devicetypes)
|
||||
devicetypes_table.columns.hide('manufacturer')
|
||||
paginate_table(devicetypes_table, request)
|
||||
|
||||
return {
|
||||
'devicetypes_table': devicetypes_table,
|
||||
}
|
||||
|
||||
|
||||
class ManufacturerEditView(generic.ObjectEditView):
|
||||
queryset = Manufacturer.objects.all()
|
||||
model_form = forms.ManufacturerForm
|
||||
@ -493,6 +704,15 @@ class ManufacturerBulkImportView(generic.BulkImportView):
|
||||
table = tables.ManufacturerTable
|
||||
|
||||
|
||||
class ManufacturerBulkEditView(generic.BulkEditView):
|
||||
queryset = Manufacturer.objects.annotate(
|
||||
devicetype_count=count_related(DeviceType, 'manufacturer')
|
||||
)
|
||||
filterset = filters.ManufacturerFilterSet
|
||||
table = tables.ManufacturerTable
|
||||
form = forms.ManufacturerBulkEditForm
|
||||
|
||||
|
||||
class ManufacturerBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Manufacturer.objects.annotate(
|
||||
devicetype_count=count_related(DeviceType, 'manufacturer')
|
||||
@ -578,7 +798,6 @@ class DeviceTypeView(generic.ObjectView):
|
||||
class DeviceTypeEditView(generic.ObjectEditView):
|
||||
queryset = DeviceType.objects.all()
|
||||
model_form = forms.DeviceTypeForm
|
||||
template_name = 'dcim/devicetype_edit.html'
|
||||
|
||||
|
||||
class DeviceTypeDeleteView(generic.ObjectDeleteView):
|
||||
@ -920,6 +1139,23 @@ class DeviceRoleListView(generic.ObjectListView):
|
||||
table = tables.DeviceRoleTable
|
||||
|
||||
|
||||
class DeviceRoleView(generic.ObjectView):
|
||||
queryset = DeviceRole.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
devices = Device.objects.restrict(request.user, 'view').filter(
|
||||
device_role=instance
|
||||
)
|
||||
|
||||
devices_table = tables.DeviceTable(devices)
|
||||
devices_table.columns.hide('device_role')
|
||||
paginate_table(devices_table, request)
|
||||
|
||||
return {
|
||||
'devices_table': devices_table,
|
||||
}
|
||||
|
||||
|
||||
class DeviceRoleEditView(generic.ObjectEditView):
|
||||
queryset = DeviceRole.objects.all()
|
||||
model_form = forms.DeviceRoleForm
|
||||
@ -935,8 +1171,21 @@ class DeviceRoleBulkImportView(generic.BulkImportView):
|
||||
table = tables.DeviceRoleTable
|
||||
|
||||
|
||||
class DeviceRoleBulkEditView(generic.BulkEditView):
|
||||
queryset = DeviceRole.objects.annotate(
|
||||
device_count=count_related(Device, 'device_role'),
|
||||
vm_count=count_related(VirtualMachine, 'role')
|
||||
)
|
||||
filterset = filters.DeviceRoleFilterSet
|
||||
table = tables.DeviceRoleTable
|
||||
form = forms.DeviceRoleBulkEditForm
|
||||
|
||||
|
||||
class DeviceRoleBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = DeviceRole.objects.all()
|
||||
queryset = DeviceRole.objects.annotate(
|
||||
device_count=count_related(Device, 'device_role'),
|
||||
vm_count=count_related(VirtualMachine, 'role')
|
||||
)
|
||||
table = tables.DeviceRoleTable
|
||||
|
||||
|
||||
@ -952,6 +1201,23 @@ class PlatformListView(generic.ObjectListView):
|
||||
table = tables.PlatformTable
|
||||
|
||||
|
||||
class PlatformView(generic.ObjectView):
|
||||
queryset = Platform.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
devices = Device.objects.restrict(request.user, 'view').filter(
|
||||
platform=instance
|
||||
)
|
||||
|
||||
devices_table = tables.DeviceTable(devices)
|
||||
devices_table.columns.hide('platform')
|
||||
paginate_table(devices_table, request)
|
||||
|
||||
return {
|
||||
'devices_table': devices_table,
|
||||
}
|
||||
|
||||
|
||||
class PlatformEditView(generic.ObjectEditView):
|
||||
queryset = Platform.objects.all()
|
||||
model_form = forms.PlatformForm
|
||||
@ -967,6 +1233,13 @@ class PlatformBulkImportView(generic.BulkImportView):
|
||||
table = tables.PlatformTable
|
||||
|
||||
|
||||
class PlatformBulkEditView(generic.BulkEditView):
|
||||
queryset = Platform.objects.all()
|
||||
filterset = filters.PlatformFilterSet
|
||||
table = tables.PlatformTable
|
||||
form = forms.PlatformBulkEditForm
|
||||
|
||||
|
||||
class PlatformBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Platform.objects.all()
|
||||
table = tables.PlatformTable
|
||||
@ -986,7 +1259,7 @@ class DeviceListView(generic.ObjectListView):
|
||||
|
||||
class DeviceView(generic.ObjectView):
|
||||
queryset = Device.objects.prefetch_related(
|
||||
'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform', 'primary_ip4', 'primary_ip6'
|
||||
'site__region', 'location', 'rack', 'tenant__group', 'device_role', 'platform', 'primary_ip4', 'primary_ip6'
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
@ -1037,6 +1310,7 @@ class DeviceConsolePortsView(generic.ObjectView):
|
||||
)
|
||||
if request.user.has_perm('dcim.change_consoleport') or request.user.has_perm('dcim.delete_consoleport'):
|
||||
consoleport_table.columns.show('pk')
|
||||
paginate_table(consoleport_table, request)
|
||||
|
||||
return {
|
||||
'consoleport_table': consoleport_table,
|
||||
@ -1062,6 +1336,7 @@ class DeviceConsoleServerPortsView(generic.ObjectView):
|
||||
if request.user.has_perm('dcim.change_consoleserverport') or \
|
||||
request.user.has_perm('dcim.delete_consoleserverport'):
|
||||
consoleserverport_table.columns.show('pk')
|
||||
paginate_table(consoleserverport_table, request)
|
||||
|
||||
return {
|
||||
'consoleserverport_table': consoleserverport_table,
|
||||
@ -1084,6 +1359,7 @@ class DevicePowerPortsView(generic.ObjectView):
|
||||
)
|
||||
if request.user.has_perm('dcim.change_powerport') or request.user.has_perm('dcim.delete_powerport'):
|
||||
powerport_table.columns.show('pk')
|
||||
paginate_table(powerport_table, request)
|
||||
|
||||
return {
|
||||
'powerport_table': powerport_table,
|
||||
@ -1106,6 +1382,7 @@ class DevicePowerOutletsView(generic.ObjectView):
|
||||
)
|
||||
if request.user.has_perm('dcim.change_poweroutlet') or request.user.has_perm('dcim.delete_poweroutlet'):
|
||||
poweroutlet_table.columns.show('pk')
|
||||
paginate_table(poweroutlet_table, request)
|
||||
|
||||
return {
|
||||
'poweroutlet_table': poweroutlet_table,
|
||||
@ -1130,6 +1407,7 @@ class DeviceInterfacesView(generic.ObjectView):
|
||||
)
|
||||
if request.user.has_perm('dcim.change_interface') or request.user.has_perm('dcim.delete_interface'):
|
||||
interface_table.columns.show('pk')
|
||||
paginate_table(interface_table, request)
|
||||
|
||||
return {
|
||||
'interface_table': interface_table,
|
||||
@ -1152,6 +1430,7 @@ class DeviceFrontPortsView(generic.ObjectView):
|
||||
)
|
||||
if request.user.has_perm('dcim.change_frontport') or request.user.has_perm('dcim.delete_frontport'):
|
||||
frontport_table.columns.show('pk')
|
||||
paginate_table(frontport_table, request)
|
||||
|
||||
return {
|
||||
'frontport_table': frontport_table,
|
||||
@ -1172,6 +1451,7 @@ class DeviceRearPortsView(generic.ObjectView):
|
||||
)
|
||||
if request.user.has_perm('dcim.change_rearport') or request.user.has_perm('dcim.delete_rearport'):
|
||||
rearport_table.columns.show('pk')
|
||||
paginate_table(rearport_table, request)
|
||||
|
||||
return {
|
||||
'rearport_table': rearport_table,
|
||||
@ -1194,6 +1474,7 @@ class DeviceDeviceBaysView(generic.ObjectView):
|
||||
)
|
||||
if request.user.has_perm('dcim.change_devicebay') or request.user.has_perm('dcim.delete_devicebay'):
|
||||
devicebay_table.columns.show('pk')
|
||||
paginate_table(devicebay_table, request)
|
||||
|
||||
return {
|
||||
'devicebay_table': devicebay_table,
|
||||
@ -1216,6 +1497,7 @@ class DeviceInventoryView(generic.ObjectView):
|
||||
)
|
||||
if request.user.has_perm('dcim.change_inventoryitem') or request.user.has_perm('dcim.delete_inventoryitem'):
|
||||
inventoryitem_table.columns.show('pk')
|
||||
paginate_table(inventoryitem_table, request)
|
||||
|
||||
return {
|
||||
'inventoryitem_table': inventoryitem_table,
|
||||
@ -1272,6 +1554,10 @@ class DeviceChangeLogView(ObjectChangeLogView):
|
||||
base_template = 'dcim/device/base.html'
|
||||
|
||||
|
||||
class DeviceJournalView(ObjectJournalView):
|
||||
base_template = 'dcim/device/base.html'
|
||||
|
||||
|
||||
class DeviceEditView(generic.ObjectEditView):
|
||||
queryset = Device.objects.all()
|
||||
model_form = forms.DeviceForm
|
||||
@ -1335,11 +1621,6 @@ class ConsolePortListView(generic.ObjectListView):
|
||||
class ConsolePortView(generic.ObjectView):
|
||||
queryset = ConsolePort.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
'breadcrumb_url': 'dcim:device_consoleports'
|
||||
}
|
||||
|
||||
|
||||
class ConsolePortCreateView(generic.ComponentCreateView):
|
||||
queryset = ConsolePort.objects.all()
|
||||
@ -1400,11 +1681,6 @@ class ConsoleServerPortListView(generic.ObjectListView):
|
||||
class ConsoleServerPortView(generic.ObjectView):
|
||||
queryset = ConsoleServerPort.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
'breadcrumb_url': 'dcim:device_consoleserverports'
|
||||
}
|
||||
|
||||
|
||||
class ConsoleServerPortCreateView(generic.ComponentCreateView):
|
||||
queryset = ConsoleServerPort.objects.all()
|
||||
@ -1465,11 +1741,6 @@ class PowerPortListView(generic.ObjectListView):
|
||||
class PowerPortView(generic.ObjectView):
|
||||
queryset = PowerPort.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
'breadcrumb_url': 'dcim:device_powerports'
|
||||
}
|
||||
|
||||
|
||||
class PowerPortCreateView(generic.ComponentCreateView):
|
||||
queryset = PowerPort.objects.all()
|
||||
@ -1530,11 +1801,6 @@ class PowerOutletListView(generic.ObjectListView):
|
||||
class PowerOutletView(generic.ObjectView):
|
||||
queryset = PowerOutlet.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
'breadcrumb_url': 'dcim:device_poweroutlets'
|
||||
}
|
||||
|
||||
|
||||
class PowerOutletCreateView(generic.ComponentCreateView):
|
||||
queryset = PowerOutlet.objects.all()
|
||||
@ -1602,6 +1868,14 @@ class InterfaceView(generic.ObjectView):
|
||||
orderable=False
|
||||
)
|
||||
|
||||
# Get child interfaces
|
||||
child_interfaces = Interface.objects.restrict(request.user, 'view').filter(parent=instance)
|
||||
child_interfaces_tables = tables.InterfaceTable(
|
||||
child_interfaces,
|
||||
orderable=False
|
||||
)
|
||||
child_interfaces_tables.columns.hide('device')
|
||||
|
||||
# Get assigned VLANs and annotate whether each is tagged or untagged
|
||||
vlans = []
|
||||
if instance.untagged_vlan is not None:
|
||||
@ -1618,8 +1892,8 @@ class InterfaceView(generic.ObjectView):
|
||||
|
||||
return {
|
||||
'ipaddress_table': ipaddress_table,
|
||||
'child_interfaces_table': child_interfaces_tables,
|
||||
'vlan_table': vlan_table,
|
||||
'breadcrumb_url': 'dcim:device_interfaces'
|
||||
}
|
||||
|
||||
|
||||
@ -1682,11 +1956,6 @@ class FrontPortListView(generic.ObjectListView):
|
||||
class FrontPortView(generic.ObjectView):
|
||||
queryset = FrontPort.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
'breadcrumb_url': 'dcim:device_frontports'
|
||||
}
|
||||
|
||||
|
||||
class FrontPortCreateView(generic.ComponentCreateView):
|
||||
queryset = FrontPort.objects.all()
|
||||
@ -1747,11 +2016,6 @@ class RearPortListView(generic.ObjectListView):
|
||||
class RearPortView(generic.ObjectView):
|
||||
queryset = RearPort.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
'breadcrumb_url': 'dcim:device_rearports'
|
||||
}
|
||||
|
||||
|
||||
class RearPortCreateView(generic.ComponentCreateView):
|
||||
queryset = RearPort.objects.all()
|
||||
@ -1812,11 +2076,6 @@ class DeviceBayListView(generic.ObjectListView):
|
||||
class DeviceBayView(generic.ObjectView):
|
||||
queryset = DeviceBay.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
'breadcrumb_url': 'dcim:device_devicebays'
|
||||
}
|
||||
|
||||
|
||||
class DeviceBayCreateView(generic.ComponentCreateView):
|
||||
queryset = DeviceBay.objects.all()
|
||||
@ -1938,11 +2197,6 @@ class InventoryItemListView(generic.ObjectListView):
|
||||
class InventoryItemView(generic.ObjectView):
|
||||
queryset = InventoryItem.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
'breadcrumb_url': 'dcim:device_inventory'
|
||||
}
|
||||
|
||||
|
||||
class InventoryItemEditView(generic.ObjectEditView):
|
||||
queryset = InventoryItem.objects.all()
|
||||
@ -2186,13 +2440,15 @@ class CableCreateView(generic.ObjectEditView):
|
||||
initial_data = {k: request.GET[k] for k in request.GET}
|
||||
|
||||
# Set initial site and rack based on side A termination (if not already set)
|
||||
termination_a_site = getattr(obj.termination_a.parent, 'site', None)
|
||||
termination_a_site = getattr(obj.termination_a.parent_object, 'site', None)
|
||||
if termination_a_site and 'termination_b_region' not in initial_data:
|
||||
initial_data['termination_b_region'] = termination_a_site.region
|
||||
if termination_a_site and 'termination_b_site_group' not in initial_data:
|
||||
initial_data['termination_b_site_group'] = termination_a_site.group
|
||||
if 'termination_b_site' not in initial_data:
|
||||
initial_data['termination_b_site'] = termination_a_site
|
||||
if 'termination_b_rack' not in initial_data:
|
||||
initial_data['termination_b_rack'] = getattr(obj.termination_a.parent, 'rack', None)
|
||||
initial_data['termination_b_rack'] = getattr(obj.termination_a.parent_object, 'rack', None)
|
||||
|
||||
form = self.model_form(instance=obj, initial=initial_data)
|
||||
|
||||
@ -2568,7 +2824,7 @@ class VirtualChassisBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class PowerPanelListView(generic.ObjectListView):
|
||||
queryset = PowerPanel.objects.prefetch_related(
|
||||
'site', 'rack_group'
|
||||
'site', 'location'
|
||||
).annotate(
|
||||
powerfeed_count=count_related(PowerFeed, 'power_panel')
|
||||
)
|
||||
@ -2578,7 +2834,7 @@ class PowerPanelListView(generic.ObjectListView):
|
||||
|
||||
|
||||
class PowerPanelView(generic.ObjectView):
|
||||
queryset = PowerPanel.objects.prefetch_related('site', 'rack_group')
|
||||
queryset = PowerPanel.objects.prefetch_related('site', 'location')
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
power_feeds = PowerFeed.objects.restrict(request.user).filter(power_panel=instance).prefetch_related('rack')
|
||||
@ -2586,6 +2842,8 @@ class PowerPanelView(generic.ObjectView):
|
||||
data=power_feeds,
|
||||
orderable=False
|
||||
)
|
||||
if request.user.has_perm('dcim.delete_cable'):
|
||||
powerfeed_table.columns.show('pk')
|
||||
powerfeed_table.exclude = ['power_panel']
|
||||
|
||||
return {
|
||||
@ -2596,7 +2854,6 @@ class PowerPanelView(generic.ObjectView):
|
||||
class PowerPanelEditView(generic.ObjectEditView):
|
||||
queryset = PowerPanel.objects.all()
|
||||
model_form = forms.PowerPanelForm
|
||||
template_name = 'dcim/powerpanel_edit.html'
|
||||
|
||||
|
||||
class PowerPanelDeleteView(generic.ObjectDeleteView):
|
||||
@ -2610,7 +2867,7 @@ class PowerPanelBulkImportView(generic.BulkImportView):
|
||||
|
||||
|
||||
class PowerPanelBulkEditView(generic.BulkEditView):
|
||||
queryset = PowerPanel.objects.prefetch_related('site', 'rack_group')
|
||||
queryset = PowerPanel.objects.prefetch_related('site', 'location')
|
||||
filterset = filters.PowerPanelFilterSet
|
||||
table = tables.PowerPanelTable
|
||||
form = forms.PowerPanelBulkEditForm
|
||||
@ -2618,7 +2875,7 @@ class PowerPanelBulkEditView(generic.BulkEditView):
|
||||
|
||||
class PowerPanelBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = PowerPanel.objects.prefetch_related(
|
||||
'site', 'rack_group'
|
||||
'site', 'location'
|
||||
).annotate(
|
||||
powerfeed_count=count_related(PowerFeed, 'power_panel')
|
||||
)
|
||||
@ -2644,7 +2901,6 @@ class PowerFeedView(generic.ObjectView):
|
||||
class PowerFeedEditView(generic.ObjectEditView):
|
||||
queryset = PowerFeed.objects.all()
|
||||
model_form = forms.PowerFeedForm
|
||||
template_name = 'dcim/powerfeed_edit.html'
|
||||
|
||||
|
||||
class PowerFeedDeleteView(generic.ObjectDeleteView):
|
||||
@ -2664,6 +2920,10 @@ class PowerFeedBulkEditView(generic.BulkEditView):
|
||||
form = forms.PowerFeedBulkEditForm
|
||||
|
||||
|
||||
class PowerFeedBulkDisconnectView(BulkDisconnectView):
|
||||
queryset = PowerFeed.objects.all()
|
||||
|
||||
|
||||
class PowerFeedBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack')
|
||||
filterset = filters.PowerFeedFilterSet
|
||||
|
||||
@ -1,16 +1,12 @@
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from utilities.forms import LaxURLField
|
||||
from utilities.forms import ContentTypeChoiceField, ContentTypeMultipleChoiceField, LaxURLField
|
||||
from utilities.utils import content_type_name
|
||||
from .models import CustomField, CustomLink, ExportTemplate, JobResult, Webhook
|
||||
|
||||
|
||||
def order_content_types(field):
|
||||
"""
|
||||
Order the list of available ContentTypes by application
|
||||
"""
|
||||
queryset = field.queryset.order_by('app_label', 'model')
|
||||
field.choices = [(ct.pk, '{} > {}'.format(ct.app_label, ct.name)) for ct in queryset]
|
||||
from .utils import FeatureQuery
|
||||
|
||||
|
||||
#
|
||||
@ -18,6 +14,10 @@ def order_content_types(field):
|
||||
#
|
||||
|
||||
class WebhookForm(forms.ModelForm):
|
||||
content_types = ContentTypeMultipleChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('webhooks')
|
||||
)
|
||||
payload_url = LaxURLField(
|
||||
label='URL'
|
||||
)
|
||||
@ -26,12 +26,6 @@ class WebhookForm(forms.ModelForm):
|
||||
model = Webhook
|
||||
exclude = ()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if 'content_types' in self.fields:
|
||||
order_content_types(self.fields['content_types'])
|
||||
|
||||
|
||||
@admin.register(Webhook)
|
||||
class WebhookAdmin(admin.ModelAdmin):
|
||||
@ -70,6 +64,10 @@ class WebhookAdmin(admin.ModelAdmin):
|
||||
#
|
||||
|
||||
class CustomFieldForm(forms.ModelForm):
|
||||
content_types = ContentTypeMultipleChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('custom_fields')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CustomField
|
||||
@ -84,11 +82,6 @@ class CustomFieldForm(forms.ModelForm):
|
||||
)
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
order_content_types(self.fields['content_types'])
|
||||
|
||||
|
||||
@admin.register(CustomField)
|
||||
class CustomFieldAdmin(admin.ModelAdmin):
|
||||
@ -119,7 +112,8 @@ class CustomFieldAdmin(admin.ModelAdmin):
|
||||
)
|
||||
|
||||
def models(self, obj):
|
||||
return ', '.join([ct.name for ct in obj.content_types.all()])
|
||||
ct_names = [content_type_name(ct) for ct in obj.content_types.all()]
|
||||
return mark_safe('<br/>'.join(ct_names))
|
||||
|
||||
|
||||
#
|
||||
@ -127,29 +121,26 @@ class CustomFieldAdmin(admin.ModelAdmin):
|
||||
#
|
||||
|
||||
class CustomLinkForm(forms.ModelForm):
|
||||
content_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('custom_links')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CustomLink
|
||||
exclude = []
|
||||
widgets = {
|
||||
'text': forms.Textarea,
|
||||
'url': forms.Textarea,
|
||||
'link_text': forms.Textarea,
|
||||
'link_url': forms.Textarea,
|
||||
}
|
||||
help_texts = {
|
||||
'weight': 'A numeric weight to influence the ordering of this link among its peers. Lower weights appear '
|
||||
'first in a list.',
|
||||
'text': 'Jinja2 template code for the link text. Reference the object as <code>{{ obj }}</code>. Links '
|
||||
'which render as empty text will not be displayed.',
|
||||
'url': 'Jinja2 template code for the link URL. Reference the object as <code>{{ obj }}</code>.',
|
||||
'link_text': 'Jinja2 template code for the link text. Reference the object as <code>{{ obj }}</code>. '
|
||||
'Links which render as empty text will not be displayed.',
|
||||
'link_url': 'Jinja2 template code for the link URL. Reference the object as <code>{{ obj }}</code>.',
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Format ContentType choices
|
||||
order_content_types(self.fields['content_type'])
|
||||
self.fields['content_type'].choices.insert(0, ('', '---------'))
|
||||
|
||||
|
||||
@admin.register(CustomLink)
|
||||
class CustomLinkAdmin(admin.ModelAdmin):
|
||||
@ -158,7 +149,7 @@ class CustomLinkAdmin(admin.ModelAdmin):
|
||||
'fields': ('content_type', 'name', 'group_name', 'weight', 'button_class', 'new_window')
|
||||
}),
|
||||
('Templates', {
|
||||
'fields': ('text', 'url'),
|
||||
'fields': ('link_text', 'link_url'),
|
||||
'classes': ('monospace',)
|
||||
})
|
||||
)
|
||||
@ -176,24 +167,21 @@ class CustomLinkAdmin(admin.ModelAdmin):
|
||||
#
|
||||
|
||||
class ExportTemplateForm(forms.ModelForm):
|
||||
content_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('custom_links')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ExportTemplate
|
||||
exclude = []
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Format ContentType choices
|
||||
order_content_types(self.fields['content_type'])
|
||||
self.fields['content_type'].choices.insert(0, ('', '---------'))
|
||||
|
||||
|
||||
@admin.register(ExportTemplate)
|
||||
class ExportTemplateAdmin(admin.ModelAdmin):
|
||||
fieldsets = (
|
||||
('Export Template', {
|
||||
'fields': ('content_type', 'name', 'description', 'mime_type', 'file_extension')
|
||||
'fields': ('content_type', 'name', 'description', 'mime_type', 'file_extension', 'as_attachment')
|
||||
}),
|
||||
('Content', {
|
||||
'fields': ('template_code',),
|
||||
@ -201,7 +189,7 @@ class ExportTemplateAdmin(admin.ModelAdmin):
|
||||
})
|
||||
)
|
||||
list_display = [
|
||||
'name', 'content_type', 'description', 'mime_type', 'file_extension',
|
||||
'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment',
|
||||
]
|
||||
list_filter = [
|
||||
'content_type',
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from rest_framework.fields import CreateOnlyDefault, Field
|
||||
from rest_framework.fields import Field
|
||||
|
||||
from extras.choices import *
|
||||
from extras.models import CustomField
|
||||
from netbox.api import ValidatedModelSerializer
|
||||
|
||||
|
||||
#
|
||||
@ -56,34 +54,3 @@ class CustomFieldsDataField(Field):
|
||||
data = {**self.parent.instance.custom_field_data, **data}
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class CustomFieldModelSerializer(ValidatedModelSerializer):
|
||||
"""
|
||||
Extends ModelSerializer to render any CustomFields and their values associated with an object.
|
||||
"""
|
||||
custom_fields = CustomFieldsDataField(
|
||||
source='custom_field_data',
|
||||
default=CreateOnlyDefault(CustomFieldDefaultValues())
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if self.instance is not None:
|
||||
|
||||
# Retrieve the set of CustomFields which apply to this type of object
|
||||
content_type = ContentType.objects.get_for_model(self.Meta.model)
|
||||
fields = CustomField.objects.filter(content_types=content_type)
|
||||
|
||||
# Populate CustomFieldValues for each instance from database
|
||||
if type(self.instance) in (list, tuple):
|
||||
for obj in self.instance:
|
||||
self._populate_custom_fields(obj, fields)
|
||||
else:
|
||||
self._populate_custom_fields(self.instance, fields)
|
||||
|
||||
def _populate_custom_fields(self, instance, custom_fields):
|
||||
instance.custom_fields = {}
|
||||
for field in custom_fields:
|
||||
instance.custom_fields[field.name] = instance.cf.get(field.name)
|
||||
|
||||
@ -2,24 +2,44 @@ from rest_framework import serializers
|
||||
|
||||
from extras import choices, models
|
||||
from netbox.api import ChoiceField, WritableNestedSerializer
|
||||
from netbox.api.serializers import NestedTagSerializer
|
||||
from users.api.nested_serializers import NestedUserSerializer
|
||||
|
||||
__all__ = [
|
||||
'NestedConfigContextSerializer',
|
||||
'NestedCustomFieldSerializer',
|
||||
'NestedCustomLinkSerializer',
|
||||
'NestedExportTemplateSerializer',
|
||||
'NestedImageAttachmentSerializer',
|
||||
'NestedJobResultSerializer',
|
||||
'NestedTagSerializer',
|
||||
'NestedJournalEntrySerializer',
|
||||
'NestedTagSerializer', # Defined in netbox.api.serializers
|
||||
'NestedWebhookSerializer',
|
||||
]
|
||||
|
||||
|
||||
class NestedWebhookSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:webhook-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.Webhook
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedCustomFieldSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.CustomField
|
||||
fields = ['id', 'url', 'name']
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedCustomLinkSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.CustomLink
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedConfigContextSerializer(WritableNestedSerializer):
|
||||
@ -27,7 +47,7 @@ class NestedConfigContextSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.ConfigContext
|
||||
fields = ['id', 'url', 'name']
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedExportTemplateSerializer(WritableNestedSerializer):
|
||||
@ -35,7 +55,7 @@ class NestedExportTemplateSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.ExportTemplate
|
||||
fields = ['id', 'url', 'name']
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedImageAttachmentSerializer(WritableNestedSerializer):
|
||||
@ -43,15 +63,15 @@ class NestedImageAttachmentSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.ImageAttachment
|
||||
fields = ['id', 'url', 'name', 'image']
|
||||
fields = ['id', 'url', 'display', 'name', 'image']
|
||||
|
||||
|
||||
class NestedTagSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail')
|
||||
class NestedJournalEntrySerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:journalentry-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.Tag
|
||||
fields = ['id', 'url', 'name', 'slug', 'color']
|
||||
model = models.JournalEntry
|
||||
fields = ['id', 'url', 'display', 'created']
|
||||
|
||||
|
||||
class NestedJobResultSerializer(serializers.ModelSerializer):
|
||||
|
||||
@ -4,17 +4,16 @@ from drf_yasg.utils import swagger_serializer_method
|
||||
from rest_framework import serializers
|
||||
|
||||
from dcim.api.nested_serializers import (
|
||||
NestedDeviceSerializer, NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedRackSerializer,
|
||||
NestedRegionSerializer, NestedSiteSerializer,
|
||||
NestedDeviceSerializer, NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedPlatformSerializer,
|
||||
NestedRackSerializer, NestedRegionSerializer, NestedSiteSerializer, NestedSiteGroupSerializer,
|
||||
)
|
||||
from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Platform, Rack, Region, Site, SiteGroup
|
||||
from extras.choices import *
|
||||
from extras.models import (
|
||||
ConfigContext, CustomField, ExportTemplate, ImageAttachment, ObjectChange, JobResult, Tag,
|
||||
)
|
||||
from extras.models import *
|
||||
from extras.utils import FeatureQuery
|
||||
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField, ValidatedModelSerializer
|
||||
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
||||
from netbox.api.exceptions import SerializerNotFound
|
||||
from netbox.api.serializers import BaseModelSerializer, ValidatedModelSerializer
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from users.api.nested_serializers import NestedUserSerializer
|
||||
@ -23,6 +22,46 @@ from virtualization.api.nested_serializers import NestedClusterGroupSerializer,
|
||||
from virtualization.models import Cluster, ClusterGroup
|
||||
from .nested_serializers import *
|
||||
|
||||
__all__ = (
|
||||
'ConfigContextSerializer',
|
||||
'ContentTypeSerializer',
|
||||
'CustomFieldSerializer',
|
||||
'CustomLinkSerializer',
|
||||
'ExportTemplateSerializer',
|
||||
'ImageAttachmentSerializer',
|
||||
'JobResultSerializer',
|
||||
'ObjectChangeSerializer',
|
||||
'ReportDetailSerializer',
|
||||
'ReportSerializer',
|
||||
'ScriptDetailSerializer',
|
||||
'ScriptInputSerializer',
|
||||
'ScriptLogMessageSerializer',
|
||||
'ScriptOutputSerializer',
|
||||
'ScriptSerializer',
|
||||
'TagSerializer',
|
||||
'WebhookSerializer',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Webhooks
|
||||
#
|
||||
|
||||
class WebhookSerializer(ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:webhook-detail')
|
||||
content_types = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(FeatureQuery('webhooks').get_query()),
|
||||
many=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Webhook
|
||||
fields = [
|
||||
'id', 'url', 'display', 'content_types', 'name', 'type_create', 'type_update', 'type_delete', 'payload_url',
|
||||
'enabled', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
|
||||
'ssl_verification', 'ca_file_path',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Custom fields
|
||||
@ -40,11 +79,29 @@ class CustomFieldSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = CustomField
|
||||
fields = [
|
||||
'id', 'url', 'content_types', 'type', 'name', 'label', 'description', 'required', 'filter_logic',
|
||||
'id', 'url', 'display', 'content_types', 'type', 'name', 'label', 'description', 'required', 'filter_logic',
|
||||
'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choices',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Custom links
|
||||
#
|
||||
|
||||
class CustomLinkSerializer(ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail')
|
||||
content_type = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(FeatureQuery('custom_links').get_query())
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CustomLink
|
||||
fields = [
|
||||
'id', 'url', 'display', 'content_type', 'name', 'link_text', 'link_url', 'weight', 'group_name',
|
||||
'button_class', 'new_window',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Export templates
|
||||
#
|
||||
@ -57,7 +114,10 @@ class ExportTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ExportTemplate
|
||||
fields = ['id', 'url', 'content_type', 'name', 'description', 'template_code', 'mime_type', 'file_extension']
|
||||
fields = [
|
||||
'id', 'url', 'display', 'content_type', 'name', 'description', 'template_code', 'mime_type',
|
||||
'file_extension', 'as_attachment',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
@ -70,39 +130,7 @@ class TagSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
fields = ['id', 'url', 'name', 'slug', 'color', 'description', 'tagged_items']
|
||||
|
||||
|
||||
class TaggedObjectSerializer(serializers.Serializer):
|
||||
tags = NestedTagSerializer(many=True, required=False)
|
||||
|
||||
def create(self, validated_data):
|
||||
tags = validated_data.pop('tags', None)
|
||||
instance = super().create(validated_data)
|
||||
|
||||
if tags is not None:
|
||||
return self._save_tags(instance, tags)
|
||||
return instance
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
tags = validated_data.pop('tags', None)
|
||||
|
||||
# Cache tags on instance for change logging
|
||||
instance._tags = tags or []
|
||||
|
||||
instance = super().update(instance, validated_data)
|
||||
|
||||
if tags is not None:
|
||||
return self._save_tags(instance, tags)
|
||||
return instance
|
||||
|
||||
def _save_tags(self, instance, tags):
|
||||
if tags:
|
||||
instance.tags.set(*[t.name for t in tags])
|
||||
else:
|
||||
instance.tags.clear()
|
||||
|
||||
return instance
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tagged_items']
|
||||
|
||||
|
||||
#
|
||||
@ -119,8 +147,8 @@ class ImageAttachmentSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = ImageAttachment
|
||||
fields = [
|
||||
'id', 'url', 'content_type', 'object_id', 'parent', 'name', 'image', 'image_height', 'image_width',
|
||||
'created',
|
||||
'id', 'url', 'display', 'content_type', 'object_id', 'parent', 'name', 'image', 'image_height',
|
||||
'image_width', 'created',
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
@ -154,6 +182,51 @@ class ImageAttachmentSerializer(ValidatedModelSerializer):
|
||||
return serializer(obj.parent, context={'request': self.context['request']}).data
|
||||
|
||||
|
||||
#
|
||||
# Journal entries
|
||||
#
|
||||
|
||||
class JournalEntrySerializer(ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:journalentry-detail')
|
||||
assigned_object_type = ContentTypeField(
|
||||
queryset=ContentType.objects.all()
|
||||
)
|
||||
assigned_object = serializers.SerializerMethodField(read_only=True)
|
||||
kind = ChoiceField(
|
||||
choices=JournalEntryKindChoices,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = JournalEntry
|
||||
fields = [
|
||||
'id', 'url', 'display', 'assigned_object_type', 'assigned_object_id', 'assigned_object', 'created',
|
||||
'created_by', 'kind', 'comments',
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
|
||||
# Validate that the parent object exists
|
||||
if 'assigned_object_type' in data and 'assigned_object_id' in data:
|
||||
try:
|
||||
data['assigned_object_type'].get_object_for_this_type(id=data['assigned_object_id'])
|
||||
except ObjectDoesNotExist:
|
||||
raise serializers.ValidationError(
|
||||
f"Invalid assigned_object: {data['assigned_object_type']} ID {data['assigned_object_id']}"
|
||||
)
|
||||
|
||||
# Enforce model validation
|
||||
super().validate(data)
|
||||
|
||||
return data
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||
def get_assigned_object(self, instance):
|
||||
serializer = get_serializer_for_model(instance.assigned_object_type.model_class(), prefix='Nested')
|
||||
context = {'request': self.context['request']}
|
||||
return serializer(instance.assigned_object, context=context).data
|
||||
|
||||
|
||||
#
|
||||
# Config contexts
|
||||
#
|
||||
@ -166,12 +239,24 @@ class ConfigContextSerializer(ValidatedModelSerializer):
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
site_groups = SerializedPKRelatedField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
serializer=NestedSiteGroupSerializer,
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
sites = SerializedPKRelatedField(
|
||||
queryset=Site.objects.all(),
|
||||
serializer=NestedSiteSerializer,
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
device_types = SerializedPKRelatedField(
|
||||
queryset=DeviceType.objects.all(),
|
||||
serializer=NestedDeviceTypeSerializer,
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
roles = SerializedPKRelatedField(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
serializer=NestedDeviceRoleSerializer,
|
||||
@ -218,8 +303,9 @@ class ConfigContextSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = ConfigContext
|
||||
fields = [
|
||||
'id', 'url', 'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms',
|
||||
'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', 'data', 'created', 'last_updated',
|
||||
'id', 'url', 'display', 'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites',
|
||||
'device_types', 'roles', 'platforms', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags',
|
||||
'data', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
@ -227,7 +313,7 @@ class ConfigContextSerializer(ValidatedModelSerializer):
|
||||
# Job Results
|
||||
#
|
||||
|
||||
class JobResultSerializer(serializers.ModelSerializer):
|
||||
class JobResultSerializer(BaseModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:jobresult-detail')
|
||||
user = NestedUserSerializer(
|
||||
read_only=True
|
||||
@ -240,7 +326,7 @@ class JobResultSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = JobResult
|
||||
fields = [
|
||||
'id', 'url', 'created', 'completed', 'name', 'obj_type', 'status', 'user', 'data', 'job_id',
|
||||
'id', 'url', 'display', 'created', 'completed', 'name', 'obj_type', 'status', 'user', 'data', 'job_id',
|
||||
]
|
||||
|
||||
|
||||
@ -318,7 +404,7 @@ class ScriptOutputSerializer(serializers.Serializer):
|
||||
# Change logging
|
||||
#
|
||||
|
||||
class ObjectChangeSerializer(serializers.ModelSerializer):
|
||||
class ObjectChangeSerializer(BaseModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:objectchange-detail')
|
||||
user = NestedUserSerializer(
|
||||
read_only=True
|
||||
@ -337,8 +423,8 @@ class ObjectChangeSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ObjectChange
|
||||
fields = [
|
||||
'id', 'url', 'time', 'user', 'user_name', 'request_id', 'action', 'changed_object_type',
|
||||
'changed_object_id', 'changed_object', 'object_data',
|
||||
'id', 'url', 'display', 'time', 'user', 'user_name', 'request_id', 'action', 'changed_object_type',
|
||||
'changed_object_id', 'changed_object', 'prechange_data', 'postchange_data',
|
||||
]
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||
@ -365,13 +451,13 @@ class ObjectChangeSerializer(serializers.ModelSerializer):
|
||||
# ContentTypes
|
||||
#
|
||||
|
||||
class ContentTypeSerializer(serializers.ModelSerializer):
|
||||
class ContentTypeSerializer(BaseModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:contenttype-detail')
|
||||
display_name = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = ContentType
|
||||
fields = ['id', 'url', 'app_label', 'model', 'display_name']
|
||||
fields = ['id', 'url', 'display', 'app_label', 'model', 'display_name']
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.CharField)
|
||||
def get_display_name(self, obj):
|
||||
|
||||
@ -5,9 +5,15 @@ from . import views
|
||||
router = OrderedDefaultRouter()
|
||||
router.APIRootView = views.ExtrasRootView
|
||||
|
||||
# Webhooks
|
||||
router.register('webhooks', views.WebhookViewSet)
|
||||
|
||||
# Custom fields
|
||||
router.register('custom-fields', views.CustomFieldViewSet)
|
||||
|
||||
# Custom links
|
||||
router.register('custom-links', views.CustomLinkViewSet)
|
||||
|
||||
# Export templates
|
||||
router.register('export-templates', views.ExportTemplateViewSet)
|
||||
|
||||
@ -17,6 +23,9 @@ router.register('tags', views.TagViewSet)
|
||||
# Image attachments
|
||||
router.register('image-attachments', views.ImageAttachmentViewSet)
|
||||
|
||||
# Journal entries
|
||||
router.register('journal-entries', views.JournalEntryViewSet)
|
||||
|
||||
# Config contexts
|
||||
router.register('config-contexts', views.ConfigContextViewSet)
|
||||
|
||||
|
||||
@ -11,9 +11,7 @@ from rq import Worker
|
||||
|
||||
from extras import filters
|
||||
from extras.choices import JobResultStatusChoices
|
||||
from extras.models import (
|
||||
ConfigContext, ExportTemplate, ImageAttachment, ObjectChange, JobResult, Tag, TaggedItem,
|
||||
)
|
||||
from extras.models import *
|
||||
from extras.models import CustomField
|
||||
from extras.reports import get_report, get_reports, run_report
|
||||
from extras.scripts import get_script, get_scripts, run_script
|
||||
@ -55,6 +53,17 @@ class ConfigContextQuerySetMixin:
|
||||
return queryset.annotate_config_context_data()
|
||||
|
||||
|
||||
#
|
||||
# Webhooks
|
||||
#
|
||||
|
||||
class WebhookViewSet(ModelViewSet):
|
||||
metadata_class = ContentTypeMetadata
|
||||
queryset = Webhook.objects.all()
|
||||
serializer_class = serializers.WebhookSerializer
|
||||
filterset_class = filters.WebhookFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Custom fields
|
||||
#
|
||||
@ -84,6 +93,17 @@ class CustomFieldModelViewSet(ModelViewSet):
|
||||
return context
|
||||
|
||||
|
||||
#
|
||||
# Custom links
|
||||
#
|
||||
|
||||
class CustomLinkViewSet(ModelViewSet):
|
||||
metadata_class = ContentTypeMetadata
|
||||
queryset = CustomLink.objects.all()
|
||||
serializer_class = serializers.CustomLinkSerializer
|
||||
filterset_class = filters.CustomLinkFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Export templates
|
||||
#
|
||||
@ -118,13 +138,24 @@ class ImageAttachmentViewSet(ModelViewSet):
|
||||
filterset_class = filters.ImageAttachmentFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Journal entries
|
||||
#
|
||||
|
||||
class JournalEntryViewSet(ModelViewSet):
|
||||
metadata_class = ContentTypeMetadata
|
||||
queryset = JournalEntry.objects.all()
|
||||
serializer_class = serializers.JournalEntrySerializer
|
||||
filterset_class = filters.JournalEntryFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Config contexts
|
||||
#
|
||||
|
||||
class ConfigContextViewSet(ModelViewSet):
|
||||
queryset = ConfigContext.objects.prefetch_related(
|
||||
'regions', 'sites', 'roles', 'platforms', 'tenant_groups', 'tenants',
|
||||
'regions', 'site_groups', 'sites', 'roles', 'platforms', 'tenant_groups', 'tenants',
|
||||
)
|
||||
serializer_class = serializers.ConfigContextSerializer
|
||||
filterset_class = filters.ConfigContextFilterSet
|
||||
|
||||
@ -13,6 +13,7 @@ class CustomFieldTypeChoices(ChoiceSet):
|
||||
TYPE_DATE = 'date'
|
||||
TYPE_URL = 'url'
|
||||
TYPE_SELECT = 'select'
|
||||
TYPE_MULTISELECT = 'multiselect'
|
||||
|
||||
CHOICES = (
|
||||
(TYPE_TEXT, 'Text'),
|
||||
@ -21,6 +22,7 @@ class CustomFieldTypeChoices(ChoiceSet):
|
||||
(TYPE_DATE, 'Date'),
|
||||
(TYPE_URL, 'URL'),
|
||||
(TYPE_SELECT, 'Selection'),
|
||||
(TYPE_MULTISELECT, 'Multiple selection'),
|
||||
)
|
||||
|
||||
|
||||
@ -85,6 +87,32 @@ class ObjectChangeActionChoices(ChoiceSet):
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# Jounral entries
|
||||
#
|
||||
|
||||
class JournalEntryKindChoices(ChoiceSet):
|
||||
|
||||
KIND_INFO = 'info'
|
||||
KIND_SUCCESS = 'success'
|
||||
KIND_WARNING = 'warning'
|
||||
KIND_DANGER = 'danger'
|
||||
|
||||
CHOICES = (
|
||||
(KIND_INFO, 'Info'),
|
||||
(KIND_SUCCESS, 'Success'),
|
||||
(KIND_WARNING, 'Warning'),
|
||||
(KIND_DANGER, 'Danger'),
|
||||
)
|
||||
|
||||
CSS_CLASSES = {
|
||||
KIND_INFO: 'default',
|
||||
KIND_SUCCESS: 'success',
|
||||
KIND_WARNING: 'warning',
|
||||
KIND_DANGER: 'danger',
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# Log Levels for Reports and Scripts
|
||||
#
|
||||
|
||||
@ -4,12 +4,12 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Q
|
||||
from django.forms import DateField, IntegerField, NullBooleanField
|
||||
|
||||
from dcim.models import DeviceRole, Platform, Region, Site
|
||||
from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.filters import BaseFilterSet, ContentTypeFilter
|
||||
from virtualization.models import Cluster, ClusterGroup
|
||||
from .choices import *
|
||||
from .models import ConfigContext, CustomField, ExportTemplate, ImageAttachment, JobResult, ObjectChange, Tag
|
||||
from .models import *
|
||||
|
||||
|
||||
__all__ = (
|
||||
@ -17,12 +17,15 @@ __all__ = (
|
||||
'ContentTypeFilterSet',
|
||||
'CreatedUpdatedFilterSet',
|
||||
'CustomFieldFilter',
|
||||
'CustomLinkFilterSet',
|
||||
'CustomFieldModelFilterSet',
|
||||
'ExportTemplateFilterSet',
|
||||
'ImageAttachmentFilterSet',
|
||||
'JournalEntryFilterSet',
|
||||
'LocalConfigContextFilterSet',
|
||||
'ObjectChangeFilterSet',
|
||||
'TagFilterSet',
|
||||
'WebhookFilterSet',
|
||||
)
|
||||
|
||||
EXACT_FILTER_TYPES = (
|
||||
@ -33,6 +36,20 @@ EXACT_FILTER_TYPES = (
|
||||
)
|
||||
|
||||
|
||||
class WebhookFilterSet(BaseFilterSet):
|
||||
content_types = ContentTypeFilter()
|
||||
http_method = django_filters.MultipleChoiceFilter(
|
||||
choices=WebhookHttpMethodChoices
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Webhook
|
||||
fields = [
|
||||
'id', 'content_types', 'name', 'type_create', 'type_update', 'type_delete', 'payload_url', 'enabled',
|
||||
'http_method', 'http_content_type', 'secret', 'ssl_verification', 'ca_file_path',
|
||||
]
|
||||
|
||||
|
||||
class CustomFieldFilter(django_filters.Filter):
|
||||
"""
|
||||
Filter objects by the presence of a CustomFieldValue. The filter's name is used as the CustomField name.
|
||||
@ -73,12 +90,20 @@ class CustomFieldModelFilterSet(django_filters.FilterSet):
|
||||
|
||||
|
||||
class CustomFieldFilterSet(django_filters.FilterSet):
|
||||
content_types = ContentTypeFilter()
|
||||
|
||||
class Meta:
|
||||
model = CustomField
|
||||
fields = ['id', 'content_types', 'name', 'required', 'filter_logic', 'weight']
|
||||
|
||||
|
||||
class CustomLinkFilterSet(BaseFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = CustomLink
|
||||
fields = ['id', 'content_type', 'name', 'link_text', 'link_url', 'weight', 'group_name', 'new_window']
|
||||
|
||||
|
||||
class ExportTemplateFilterSet(BaseFilterSet):
|
||||
|
||||
class Meta:
|
||||
@ -94,6 +119,37 @@ class ImageAttachmentFilterSet(BaseFilterSet):
|
||||
fields = ['id', 'content_type_id', 'object_id', 'name']
|
||||
|
||||
|
||||
class JournalEntryFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
created = django_filters.DateTimeFromToRangeFilter()
|
||||
assigned_object_type = ContentTypeFilter()
|
||||
created_by_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=User.objects.all(),
|
||||
label='User (ID)',
|
||||
)
|
||||
created_by = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='created_by__username',
|
||||
queryset=User.objects.all(),
|
||||
to_field_name='username',
|
||||
label='User (name)',
|
||||
)
|
||||
kind = django_filters.MultipleChoiceFilter(
|
||||
choices=JournalEntryKindChoices
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = JournalEntry
|
||||
fields = ['id', 'assigned_object_type_id', 'assigned_object_id', 'created', 'kind']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(comments__icontains=value)
|
||||
|
||||
|
||||
class TagFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@ -129,6 +185,17 @@ class ConfigContextFilterSet(BaseFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_group = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='site_groups__slug',
|
||||
queryset=SiteGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site group (slug)',
|
||||
)
|
||||
site_group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='site_groups',
|
||||
queryset=SiteGroup.objects.all(),
|
||||
label='Site group',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='sites',
|
||||
queryset=Site.objects.all(),
|
||||
@ -140,6 +207,11 @@ class ConfigContextFilterSet(BaseFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
device_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device_types',
|
||||
queryset=DeviceType.objects.all(),
|
||||
label='Device type',
|
||||
)
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='roles',
|
||||
queryset=DeviceRole.objects.all(),
|
||||
|
||||
@ -2,25 +2,51 @@ from django import forms
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from dcim.models import DeviceRole, Platform, Region, Site
|
||||
from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.forms import (
|
||||
add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect,
|
||||
ContentTypeSelect, CSVModelForm, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField,
|
||||
StaticSelect2, BOOLEAN_WITH_BLANK_CHOICES,
|
||||
CommentField, CSVModelForm, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect2,
|
||||
BOOLEAN_WITH_BLANK_CHOICES,
|
||||
)
|
||||
from virtualization.models import Cluster, ClusterGroup
|
||||
from .choices import *
|
||||
from .models import ConfigContext, CustomField, ImageAttachment, ObjectChange, Tag
|
||||
from .models import ConfigContext, CustomField, ImageAttachment, JournalEntry, ObjectChange, Tag
|
||||
|
||||
|
||||
#
|
||||
# Custom fields
|
||||
#
|
||||
|
||||
class CustomFieldModelForm(forms.ModelForm):
|
||||
class CustomFieldForm(forms.Form):
|
||||
"""
|
||||
Extend Form to include custom field support.
|
||||
"""
|
||||
model = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if self.model is None:
|
||||
raise NotImplementedError("CustomFieldForm must specify a model class.")
|
||||
self.custom_fields = []
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Append relevant custom fields to the form instance
|
||||
obj_type = ContentType.objects.get_for_model(self.model)
|
||||
for cf in CustomField.objects.filter(content_types=obj_type):
|
||||
field_name = 'cf_{}'.format(cf.name)
|
||||
self.fields[field_name] = cf.to_form_field()
|
||||
|
||||
# Annotate the field in the list of CustomField form fields
|
||||
self.custom_fields.append(field_name)
|
||||
|
||||
|
||||
class CustomFieldModelForm(forms.ModelForm):
|
||||
"""
|
||||
Extend ModelForm to include custom field support.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
self.obj_type = ContentType.objects.get_for_model(self._meta.model)
|
||||
@ -116,6 +142,9 @@ class TagForm(BootstrapMixin, forms.ModelForm):
|
||||
fields = [
|
||||
'name', 'slug', 'color', 'description'
|
||||
]
|
||||
fieldsets = (
|
||||
('Tag', ('name', 'slug', 'color', 'description')),
|
||||
)
|
||||
|
||||
|
||||
class TagCSVForm(CSVModelForm):
|
||||
@ -149,7 +178,7 @@ class TagFilterForm(BootstrapMixin, forms.Form):
|
||||
model = Tag
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label='Search'
|
||||
label=_('Search')
|
||||
)
|
||||
|
||||
|
||||
@ -181,10 +210,18 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
|
||||
queryset=Region.objects.all(),
|
||||
required=False
|
||||
)
|
||||
site_groups = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False
|
||||
)
|
||||
sites = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False
|
||||
)
|
||||
device_types = DynamicModelMultipleChoiceField(
|
||||
queryset=DeviceType.objects.all(),
|
||||
required=False
|
||||
)
|
||||
roles = DynamicModelMultipleChoiceField(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
required=False
|
||||
@ -220,8 +257,8 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = ConfigContext
|
||||
fields = (
|
||||
'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'cluster_groups',
|
||||
'clusters', 'tenant_groups', 'tenants', 'tags', 'data',
|
||||
'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites', 'roles', 'device_types',
|
||||
'platforms', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', 'data',
|
||||
)
|
||||
|
||||
|
||||
@ -250,54 +287,69 @@ class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
|
||||
|
||||
class ConfigContextFilterForm(BootstrapMixin, forms.Form):
|
||||
field_order = [
|
||||
'q', 'region_id', 'site_group_id', 'site_id', 'role_id', 'platform_id', 'cluster_group_id', 'cluster_id',
|
||||
'tenant_group_id', 'tenant_id',
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label='Search'
|
||||
label=_('Search')
|
||||
)
|
||||
region = DynamicModelMultipleChoiceField(
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False
|
||||
required=False,
|
||||
label=_('Regions')
|
||||
)
|
||||
site = DynamicModelMultipleChoiceField(
|
||||
site_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site groups')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False
|
||||
required=False,
|
||||
label=_('Sites')
|
||||
)
|
||||
role = DynamicModelMultipleChoiceField(
|
||||
device_type_id = DynamicModelMultipleChoiceField(
|
||||
queryset=DeviceType.objects.all(),
|
||||
required=False,
|
||||
label=_('Device types')
|
||||
)
|
||||
role_id = DynamicModelMultipleChoiceField(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False
|
||||
required=False,
|
||||
label=_('Roles')
|
||||
)
|
||||
platform = DynamicModelMultipleChoiceField(
|
||||
platform_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Platform.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False
|
||||
required=False,
|
||||
label=_('Platforms')
|
||||
)
|
||||
cluster_group = DynamicModelMultipleChoiceField(
|
||||
cluster_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ClusterGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False
|
||||
required=False,
|
||||
label=_('Cluster groups')
|
||||
)
|
||||
cluster_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Cluster.objects.all(),
|
||||
required=False,
|
||||
label='Cluster'
|
||||
label=_('Clusters')
|
||||
)
|
||||
tenant_group = DynamicModelMultipleChoiceField(
|
||||
tenant_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=TenantGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False
|
||||
required=False,
|
||||
label=_('Tenant groups')
|
||||
)
|
||||
tenant = DynamicModelMultipleChoiceField(
|
||||
tenant_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False
|
||||
required=False,
|
||||
label=_('Tenant')
|
||||
)
|
||||
tag = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False
|
||||
required=False,
|
||||
label=_('Tags')
|
||||
)
|
||||
|
||||
|
||||
@ -308,7 +360,7 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
|
||||
class LocalConfigContextFilterForm(forms.Form):
|
||||
local_context_data = forms.NullBooleanField(
|
||||
required=False,
|
||||
label='Has local config context data',
|
||||
label=_('Has local config context data'),
|
||||
widget=StaticSelect2(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
@ -328,6 +380,79 @@ class ImageAttachmentForm(BootstrapMixin, forms.ModelForm):
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Journal entries
|
||||
#
|
||||
|
||||
class JournalEntryForm(BootstrapMixin, forms.ModelForm):
|
||||
comments = CommentField()
|
||||
|
||||
class Meta:
|
||||
model = JournalEntry
|
||||
fields = ['assigned_object_type', 'assigned_object_id', 'kind', 'comments']
|
||||
widgets = {
|
||||
'assigned_object_type': forms.HiddenInput,
|
||||
'assigned_object_id': forms.HiddenInput,
|
||||
}
|
||||
|
||||
|
||||
class JournalEntryBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=JournalEntry.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
kind = forms.ChoiceField(
|
||||
choices=JournalEntryKindChoices,
|
||||
required=False
|
||||
)
|
||||
comments = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.Textarea()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = []
|
||||
|
||||
|
||||
class JournalEntryFilterForm(BootstrapMixin, forms.Form):
|
||||
model = JournalEntry
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label=_('Search')
|
||||
)
|
||||
created_after = forms.DateTimeField(
|
||||
required=False,
|
||||
label=_('After'),
|
||||
widget=DateTimePicker()
|
||||
)
|
||||
created_before = forms.DateTimeField(
|
||||
required=False,
|
||||
label=_('Before'),
|
||||
widget=DateTimePicker()
|
||||
)
|
||||
created_by_id = DynamicModelMultipleChoiceField(
|
||||
queryset=User.objects.all(),
|
||||
required=False,
|
||||
label=_('User'),
|
||||
widget=APISelectMultiple(
|
||||
api_url='/api/users/users/',
|
||||
)
|
||||
)
|
||||
assigned_object_type_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
required=False,
|
||||
label=_('Object Type'),
|
||||
widget=APISelectMultiple(
|
||||
api_url='/api/extras/content-types/',
|
||||
)
|
||||
)
|
||||
kind = forms.ChoiceField(
|
||||
choices=add_blank_choice(JournalEntryKindChoices),
|
||||
required=False,
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Change logging
|
||||
#
|
||||
@ -336,16 +461,16 @@ class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
|
||||
model = ObjectChange
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label='Search'
|
||||
label=_('Search')
|
||||
)
|
||||
time_after = forms.DateTimeField(
|
||||
label='After',
|
||||
required=False,
|
||||
label=_('After'),
|
||||
widget=DateTimePicker()
|
||||
)
|
||||
time_before = forms.DateTimeField(
|
||||
label='Before',
|
||||
required=False,
|
||||
label=_('Before'),
|
||||
widget=DateTimePicker()
|
||||
)
|
||||
action = forms.ChoiceField(
|
||||
@ -356,8 +481,7 @@ class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
|
||||
user_id = DynamicModelMultipleChoiceField(
|
||||
queryset=User.objects.all(),
|
||||
required=False,
|
||||
display_field='username',
|
||||
label='User',
|
||||
label=_('User'),
|
||||
widget=APISelectMultiple(
|
||||
api_url='/api/users/users/',
|
||||
)
|
||||
@ -365,8 +489,7 @@ class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
|
||||
changed_object_type_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
required=False,
|
||||
display_field='display_name',
|
||||
label='Object Type',
|
||||
label=_('Object Type'),
|
||||
widget=APISelectMultiple(
|
||||
api_url='/api/extras/content-types/',
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user