Merge pull request #18308 from netbox-community/feature

Prep for v4.2.0 release
This commit is contained in:
Jeremy Stretch 2025-01-06 14:02:29 -05:00 committed by GitHub
commit ab0a1f0bbc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
456 changed files with 17527 additions and 5252 deletions

View File

@ -28,7 +28,7 @@ jobs:
strategy:
matrix:
python-version: ['3.10', '3.11', '3.12']
node-version: ['18.x']
node-version: ['20.x']
services:
redis:
image: redis

View File

@ -1,6 +1,6 @@
# The Python web framework on which NetBox is built
# https://docs.djangoproject.com/en/stable/releases/
Django<5.1
Django<5.2
# Django middleware which permits cross-domain API requests
# https://github.com/adamchainz/django-cors-headers/blob/main/CHANGELOG.rst

View File

@ -96,14 +96,6 @@ The maximum size (in bytes) of an incoming HTTP request (i.e. `GET` or `POST` da
---
## DJANGO_ADMIN_ENABLED
Default: False
Setting this to True installs the `django.contrib.admin` app and enables the [Django admin UI](https://docs.djangoproject.com/en/5.0/ref/contrib/admin/). This may be necessary to support older plugins which do not integrate with the native NetBox interface.
---
## ENFORCE_GLOBAL_UNIQUE
!!! tip "Dynamic Configuration Parameter"
@ -114,6 +106,16 @@ By default, NetBox will prevent the creation of duplicate prefixes and IP addres
---
## EVENTS_PIPELINE
!!! info "This parameter was introduced in NetBox v4.2."
Default: `['extras.events.process_event_queue',]`
NetBox will call dotted paths to the functions listed here for events (create, update, delete) on models as well as when custom EventRules are fired.
---
## FILE_UPLOAD_MAX_MEMORY_SIZE
Default: `2621440` (2.5 MB)

View File

@ -89,8 +89,6 @@ addresses (and [`DEBUG`](./development.md#debug) is true).
## ISOLATED_DEPLOYMENT
!!! info "This feature was introduced in NetBox v4.1."
Default: False
Set this configuration parameter to True for NetBox deployments which do not have Internet access. This will disable miscellaneous functionality which depends on access to the Internet.

View File

@ -8,7 +8,7 @@ Each model should define, at a minimum:
* A `Meta` class specifying a deterministic ordering (if ordered by fields other than the primary ID)
* A `__str__()` method returning a user-friendly string representation of the instance
* A `get_absolute_url()` method returning an instance's direct URL (using `reverse()`)
* A `get_absolute_url()` method if necessary; a standard version of the method is defined in the `NetBoxFeatureSet` base class, but you will need to provide your own (returning an instance's direct URL using `reverse()`) if not subclassing that base class
## 2. Define field choices
@ -78,6 +78,8 @@ Create the following for each model:
Create a GraphQL object type for the model in `graphql/types.py` by subclassing the appropriate class from `netbox.graphql.types`.
**Note:** GraphQL unit tests may fail citing null values on a non-nullable field if related objects are prefetched. You may need to fix this by setting the type annotation to be `= strawberry_django.field(select_related=["policy"])` or similar.
Also extend the schema class defined in `graphql/schema.py` with the individual object and object list fields per the established convention.
## 14. Add tests

View File

@ -1,7 +1,5 @@
# Notifications
!!! info "This feature was introduced in NetBox v4.1."
NetBox includes a system for generating user notifications, which can be marked as read or deleted by individual users. There are two built-in mechanisms for generating a notification:
* A user can subscribe to an object. When that object is modified, a notification is created to inform the user of the change.

View File

@ -36,6 +36,12 @@ The operational status of the circuit. By default, the following statuses are av
!!! tip "Custom circuit statuses"
Additional circuit statuses may be defined by setting `Circuit.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
### Distance
!!! info "This field was introduced in NetBox v4.2."
The distance between the circuit's two endpoints, including a unit designation (e.g. 100 meters or 25 feet).
### Description
A brief description of the circuit.

View File

@ -1,7 +1,5 @@
# Circuit Groups
!!! info "This feature was introduced in NetBox v4.1."
[Circuits](./circuit.md) can be arranged into administrative groups for organization. The assignment of a circuit to a group is optional.
## Fields

View File

@ -8,9 +8,9 @@ Circuits can be assigned to [circuit groups](./circuitgroup.md) for correlation
The [circuit group](./circuitgroup.md) being assigned.
### Circuit
### Member
The [circuit](./circuit.md) that is being assigned to the group.
The [circuit](./circuit.md) or [virtual circuit](./virtualcircuit.md) assigned to the group.
### Priority

View File

@ -21,13 +21,11 @@ Designates the termination as forming either the A or Z end of the circuit.
If selected, the circuit termination will be considered "connected" even if no cable has been connected to it in NetBox.
### Site
### Termination
The [site](../dcim/site.md) with which this circuit termination is associated. Once created, a cable can be connected between the circuit termination and a device interface (or similar component).
!!! info "This field replaced the `site` and `provider_network` fields in NetBox v4.2."
### Provider Network
Circuits which do not connect to a site modeled by NetBox can instead be terminated to a [provider network](./providernetwork.md) representing an unknown network operated by a [provider](./provider.md).
The [region](../dcim/region.md), [site group](../dcim/sitegroup.md), [site](../dcim/site.md), [location](../dcim/location.md) or [provider network](./providernetwork.md) with which this circuit termination is associated. Once created, a cable can be connected between the circuit termination and a device interface (or similar component).
### Port Speed

View File

@ -0,0 +1,39 @@
# Virtual Circuits
!!! info "This feature was introduced in NetBox v4.2."
A virtual circuit can connect two or more interfaces atop a set of decoupled physical connections. For example, it's very common to form a virtual connection between two virtual interfaces, each of which is bound to a physical interface on its respective device and physically connected to a [provider network](./providernetwork.md) via an independent [physical circuit](./circuit.md).
## Fields
### Provider Network
The [provider network](./providernetwork.md) across which the virtual circuit is formed.
### Provider Account
The [provider account](./provideraccount.md) with which the virtual circuit is associated (if any).
### Circuit ID
The unique identifier assigned to the virtual circuit by its [provider](./provider.md).
### Type
The assigned [virtual circuit type](./virtualcircuittype.md).
### Status
The operational status of the virtual circuit. By default, the following statuses are available:
| Name |
|----------------|
| Planned |
| Provisioning |
| Active |
| Offline |
| Deprovisioning |
| Decommissioned |
!!! tip "Custom circuit statuses"
Additional circuit statuses may be defined by setting `Circuit.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.

View File

@ -0,0 +1,23 @@
# Virtual Circuit Terminations
!!! info "This feature was introduced in NetBox v4.2."
This model represents the connection of a virtual [interface](../dcim/interface.md) to a [virtual circuit](./virtualcircuit.md).
## Fields
### Virtual Circuit
The [virtual circuit](./virtualcircuit.md) to which the interface is connected.
### Interface
The [interface](../dcim/interface.md) connected to the virtual circuit.
### Role
The functional role of the termination. This depends on the virtual circuit's topology, which is typically either peer-to-peer or hub-and-spoke (multipoint). Valid choices include:
* Peer
* Hub
* Spoke

View File

@ -0,0 +1,13 @@
# Virtual Circuit Types
Like physical [circuits](./circuit.md), [virtual circuits](./virtualcircuit.md) are classified by functional type. These types are completely customizable, and can help categorize circuits by function or technology.
## Fields
### Name
A unique human-friendly name.
### Slug
A unique URL-friendly identifier. (This value can be used for filtering.)

View File

@ -45,9 +45,12 @@ The operation duplex (full, half, or auto).
The [virtual routing and forwarding](../ipam/vrf.md) instance to which this interface is assigned.
### MAC Address
### Primary MAC Address
The 48-bit MAC address (for Ethernet interfaces).
The [MAC address](./macaddress.md) assigned to this interface which is designated as its primary.
!!! note "Changed in NetBox v4.2"
The MAC address of an interface (formerly a concrete database field) is available as a property, `mac_address`, which reflects the value of the primary linked [MAC address](./macaddress.md) object.
### WWN
@ -109,6 +112,7 @@ For switched Ethernet interfaces, this identifies the 802.1Q encapsulation strat
* **Access:** All traffic is assigned to a single VLAN, with no tagging.
* **Tagged:** One untagged "native" VLAN is allowed, as well as any number of tagged VLANs.
* **Tagged (all):** Implies that all VLANs are carried by the interface. One untagged VLAN may be designated.
* **Q-in-Q:** Q-in-Q (IEEE 802.1ad) encapsulation is performed using the assigned SVLAN.
This field must be left blank for routed interfaces which do employ 802.1Q encapsulation.
@ -120,6 +124,12 @@ The "native" (untagged) VLAN for the interface. Valid only when one of the above
The tagged VLANs which are configured to be carried by this interface. Valid only for the "tagged" 802.1Q mode above.
### Q-in-Q SVLAN
!!! info "This field was introduced in NetBox v4.2."
The assigned service VLAN (for Q-in-Q/802.1ad interfaces).
### Wireless Role
Indicates the configured role for wireless interfaces (access point or station).
@ -142,3 +152,9 @@ The configured channel width of a wireless interface, in MHz. This is typically
### Wireless LANs
The [wireless LANs](../wireless/wirelesslan.md) for which this interface carries traffic. (Valid for wireless interfaces only.)
### VLAN Translation Policy
!!! info "This field was introduced in NetBox v4.2."
The [VLAN translation policy](../ipam/vlantranslationpolicy.md) that applies to this interface (optional).

View File

@ -25,6 +25,12 @@ The inventory item's name. If the inventory item is assigned to a parent item, i
An alternative physical label identifying the inventory item.
### Status
!!! info "This field was introduced in NetBox v4.2."
The inventory item's operational status.
### Role
The functional [role](./inventoryitemrole.md) assigned to this inventory item.

View File

@ -0,0 +1,13 @@
# MAC Addresses
!!! info "This feature was introduced in NetBox v4.2."
A MAC address object in NetBox comprises a single Ethernet link layer address, and represents a MAC address as reported by or assigned to a network interface. MAC addresses can be assigned to [device](../dcim/device.md) and [virtual machine](../virtualization/virtualmachine.md) interfaces. A MAC address can be specified as the primary MAC address for a given device or VM interface.
Most interfaces have only a single MAC address, hard-coded at the factory. However, on some devices (particularly virtual interfaces) it is possible to assign additional MAC addresses or change existing ones. For this reason NetBox allows multiple MACAddress objects to be assigned to a single interface.
## Fields
### MAC Address
The 48-bit MAC address, in colon-hexadecimal notation (e.g. `aa:bb:cc:11:22:33`).

View File

@ -16,8 +16,6 @@ The device to which this module bay belongs.
### Module
!!! info "This feature was introduced in NetBox v4.1."
The module to which this bay belongs (optional).
### Name

View File

@ -42,6 +42,4 @@ The numeric weight of the module, including a unit designation (e.g. 3 kilograms
### Airflow
!!! info "The `airflow` field was introduced in NetBox v4.1."
The direction in which air circulates through the device chassis for cooling.

View File

@ -29,6 +29,12 @@ An alternative physical label identifying the power outlet.
The type of power outlet.
### Color
!!! info "This field was introduced in NetBox v4.2."
The power outlet's color (optional).
### Power Port
When modeling a device which redistributes power from an upstream supply, such as a power distribution unit (PDU), each power outlet should be mapped to the respective [power port](./powerport.md) on the device which supplies power. For example, a 24-outlet PDU may two power ports, each distributing power to 12 of its outlets.

View File

@ -1,7 +1,5 @@
# Rack Types
!!! info "This feature was introduced in NetBox v4.1."
A rack type defines the physical characteristics of a particular model of [rack](./rack.md).
## Fields

View File

@ -1,5 +1,8 @@
# Branches
!!! danger "Deprecated Feature"
This feature has been deprecated in NetBox v4.2 and will be removed in a future release. Please consider using the [netbox-branching plugin](https://github.com/netboxlabs/netbox-branching), which provides much more robust functionality.
A branch is a collection of related [staged changes](./stagedchange.md) that have been prepared for merging into the active database. A branch can be merged by executing its `commit()` method. Deleting a branch will delete all its related changes.
## Fields

View File

@ -44,8 +44,6 @@ For object and multiple-object fields only. Designates the type of NetBox object
### Related Object Filter
!!! info "This field was introduced in NetBox v4.1."
For object and multi-object custom fields, a filter may be defined to limit the available objects when populating a field value. This filter maps object attributes to values. For example, `{"status": "active"}` will include only objects with a status of "active."
!!! warning

View File

@ -1,5 +1,8 @@
# Staged Changes
!!! danger "Deprecated Feature"
This feature has been deprecated in NetBox v4.2 and will be removed in a future release. Please consider using the [netbox-branching plugin](https://github.com/netboxlabs/netbox-branching), which provides much more robust functionality.
A staged change represents the creation of a new object or the modification or deletion of an existing object to be performed at some future point. Each change must be assigned to a [branch](./branch.md).
Changes can be applied individually via the `apply()` method, however it is recommended to apply changes in bulk using the parent branch's `commit()` method.

View File

@ -34,9 +34,11 @@ Designates whether the prefix should be treated as a pool. If selected, the firs
If selected, this prefix will report 100% utilization regardless of how many child objects have been defined within it.
### Site
### Scope
The [site](../dcim/site.md) to which this prefix is assigned (optional).
!!! info "This field replaced the `site` field in NetBox v4.2."
The [region](../dcim/region.md), [site](../dcim/site.md), [site group](../dcim/sitegroup.md) or [location](../dcim/location.md) to which the prefix is assigned (optional).
### VLAN

View File

@ -26,3 +26,15 @@ The user-defined functional [role](./role.md) assigned to the VLAN.
### VLAN Group or Site
The [VLAN group](./vlangroup.md) or [site](../dcim/site.md) to which the VLAN is assigned.
### Q-in-Q Role
!!! info "This field was introduced in NetBox v4.2."
For VLANs which comprise a Q-in-Q/IEEE 802.1ad topology, this field indicates whether the VLAN is treated as a service or customer VLAN.
### Q-in-Q Service VLAN
!!! info "This field was introduced in NetBox v4.2."
The designated parent service VLAN for a Q-in-Q customer VLAN. This may be set only for Q-in-Q custom VLANs.

View File

@ -16,8 +16,6 @@ A unique URL-friendly identifier. (This value can be used for filtering.)
### VLAN ID Ranges
!!! info "This field replaced the legacy `min_vid` and `max_vid` fields in NetBox v4.1."
The set of VLAN IDs which are encompassed by the group. By default, this will be the entire range of valid IEEE 802.1Q VLAN IDs (1 to 4094, inclusive). VLANs created within a group must have a VID that falls within one of these ranges. Ranges may not overlap.
### Scope

View File

@ -0,0 +1,28 @@
# VLAN Translation Policies
!!! info "This feature was introduced in NetBox v4.2."
VLAN translation is a feature that consists of VLAN translation policies and [VLAN translation rules](./vlantranslationrule.md). Many rules can belong to a policy, and each rule defines a mapping of a local to remote VLAN ID (VID). A policy can then be assigned to an [Interface](../dcim/interface.md) or [VMInterface](../virtualization/vminterface.md), and all VLAN translation rules associated with that policy will be visible in the interface details.
There are uniqueness constraints on `(policy, local_vid)` and on `(policy, remote_vid)` in the `VLANTranslationRule` model. Thus, you cannot have multiple rules linked to the same policy that have the same local VID or the same remote VID. A set of policies and rules might look like this:
Policy 1:
- Rule: 100 -> 200
- Rule: 101 -> 201
Policy 2:
- Rule: 100 -> 300
- Rule: 101 -> 301
However this is not allowed:
Policy 3:
- Rule: 100 -> 200
- Rule: 100 -> 300
## Fields
### Name
A unique human-friendly name.

View File

@ -0,0 +1,21 @@
# VLAN Translation Rules
!!! info "This feature was introduced in NetBox v4.2."
A VLAN translation rule represents a one-to-one mapping of a local VLAN ID (VID) to a remote VID. Many rules can belong to a single policy.
See [VLAN translation policies](./vlantranslationpolicy.md) for an overview of the VLAN Translation feature.
## Fields
### Policy
The [VLAN Translation Policy](./vlantranslationpolicy.md) to which this rule belongs.
### Local VID
VLAN ID (1-4094) in the local network which is to be translated to a remote VID.
### Remote VID
VLAN ID (1-4094) in the remote network to which the local VID will be translated.

View File

@ -23,6 +23,8 @@ The cluster's operational status.
!!! tip
Additional statuses may be defined by setting `Cluster.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
### Site
### Scope
The [site](../dcim/site.md) with which the cluster is associated.
!!! info "This field replaced the `site` field in NetBox v4.2."
The [region](../dcim/region.md), [site](../dcim/site.md), [site group](../dcim/sitegroup.md) or [location](../dcim/location.md) with which this cluster is associated.

View File

@ -57,6 +57,4 @@ The amount of disk storage provisioned, in megabytes.
### Serial Number
!!! info "This field was introduced in NetBox v4.1."
Optional serial number assigned to this virtual machine. Unlike devices, uniqueness is not enforced for virtual machine serial numbers.

View File

@ -27,9 +27,12 @@ An interface on the same VM with which this interface is bridged.
If not selected, this interface will be treated as disabled/inoperative.
### MAC Address
### Primary MAC Address
The 48-bit MAC address (for Ethernet interfaces).
The [MAC address](../dcim/macaddress.md) assigned to this interface which is designated as its primary.
!!! note "Changed in NetBox v4.2"
The MAC address of an interface (formerly a concrete database field) is available as a property, `mac_address`, which reflects the value of the primary linked [MAC address](../dcim/macaddress.md) object.
### MTU
@ -42,6 +45,7 @@ For switched Ethernet interfaces, this identifies the 802.1Q encapsulation strat
* **Access:** All traffic is assigned to a single VLAN, with no tagging.
* **Tagged:** One untagged "native" VLAN is allowed, as well as any number of tagged VLANs.
* **Tagged (all):** Implies that all VLANs are carried by the interface. One untagged VLAN may be designated.
* **Q-in-Q:** Q-in-Q (IEEE 802.1ad) encapsulation is performed using the assigned SVLAN.
This field must be left blank for routed interfaces which do employ 802.1Q encapsulation.
@ -53,6 +57,18 @@ The "native" (untagged) VLAN for the interface. Valid only when one of the above
The tagged VLANs which are configured to be carried by this interface. Valid only for the "tagged" 802.1Q mode above.
### Q-in-Q SVLAN
!!! info "This field was introduced in NetBox v4.2."
The assigned service VLAN (for Q-in-Q/802.1ad interfaces).
### VRF
The [virtual routing and forwarding](../ipam/vrf.md) instance to which this interface is assigned.
### VLAN Translation Policy
!!! info "This field was introduced in NetBox v4.2."
The [VLAN translation policy](../ipam/vlantranslationpolicy.md) that applies to this interface (optional).

View File

@ -43,3 +43,9 @@ The security cipher used to apply wireless authentication. Options include:
### Pre-Shared Key
The security key configured on each client to grant access to the secured wireless LAN. This applies only to certain authentication types.
### Scope
!!! info "This field was introduced in NetBox v4.2."
The [region](../dcim/region.md), [site](../dcim/site.md), [site group](../dcim/sitegroup.md) or [location](../dcim/location.md) with which this wireless LAN is associated.

View File

@ -22,8 +22,6 @@ The service set identifier (SSID) for the wireless link (optional).
### Distance
!!! info "This field was introduced in NetBox v4.1."
The distance between the link's two endpoints, including a unit designation (e.g. 100 meters or 25 feet).
### Authentication Type

View File

@ -1,7 +1,5 @@
# Background Jobs
!!! info "This feature was introduced in NetBox v4.1."
NetBox plugins can defer certain operations by enqueuing [background jobs](../../features/background-jobs.md), which are executed asynchronously by background workers. This is helpful for decoupling long-running processes from the user-facing request-response cycle.
For example, your plugin might need to fetch data from a remote system. Depending on the amount of data and the responsiveness of the remote server, this could take a few minutes. Deferring this task to a queued job ensures that it can be completed in the background, without interrupting the user. The data it fetches can be made available once the job has completed.
@ -29,6 +27,9 @@ class MyTestJob(JobRunner):
You can schedule the background job from within your code (e.g. from a model's `save()` method or a view) by calling `MyTestJob.enqueue()`. This method passes through all arguments to `Job.enqueue()`. However, no `name` argument must be passed, as the background job name will be used instead.
!!! tip
A set of predefined intervals is available at `core.choices.JobIntervalChoices` for convenience.
### Attributes
`JobRunner` attributes are defined under a class named `Meta` within the job. These are optional, but encouraged.
@ -46,26 +47,57 @@ As described above, jobs can be scheduled for immediate execution or at any late
#### Example
```python title="models.py"
from django.db import models
from core.choices import JobIntervalChoices
from netbox.models import NetBoxModel
from .jobs import MyTestJob
class MyModel(NetBoxModel):
foo = models.CharField()
def save(self, *args, **kwargs):
MyTestJob.enqueue_once(instance=self, interval=JobIntervalChoices.INTERVAL_HOURLY)
return super().save(*args, **kwargs)
def sync(self):
MyTestJob.enqueue(instance=self)
```
### System Jobs
!!! info "This feature was introduced in NetBox v4.2."
Some plugins may implement background jobs that are decoupled from the request/response cycle. Typical use cases would be housekeeping tasks or synchronization jobs. These can be registered as _system jobs_ using the `system_job()` decorator. The job interval must be passed as an integer (in minutes) when registering a system job. System jobs are scheduled automatically when the RQ worker (`manage.py rqworker`) is run.
#### Example
```python title="jobs.py"
from netbox.jobs import JobRunner
from core.choices import JobIntervalChoices
from netbox.jobs import JobRunner, system_job
from .models import MyModel
# Specify a predefined choice or an integer indicating
# the number of minutes between job executions
@system_job(interval=JobIntervalChoices.INTERVAL_HOURLY)
class MyHousekeepingJob(JobRunner):
class Meta:
name = "Housekeeping"
name = "My Housekeeping Job"
def run(self, *args, **kwargs):
# your logic goes here
MyModel.objects.filter(foo='bar').delete()
```
```python title="__init__.py"
from netbox.plugins import PluginConfig
!!! note
Ensure that any system jobs are imported on initialization. Otherwise, they won't be registered. This can be achieved by extending the PluginConfig's `ready()` method. For example:
class MyPluginConfig(PluginConfig):
```python
def ready(self):
super().ready()
from .jobs import MyHousekeepingJob
MyHousekeepingJob.setup(interval=60)
```
```
## Task queues

View File

@ -18,6 +18,6 @@ backends = [MyDataBackend]
```
!!! tip
The path to the list of search indexes can be modified by setting `data_backends` in the PluginConfig instance.
The path to the list of data backends can be modified by setting `data_backends` in the PluginConfig instance.
::: netbox.data_backends.DataBackend

View File

@ -1,7 +1,5 @@
# Event Types
!!! info "This feature was introduced in NetBox v4.1."
Plugins can register their own custom event types for use with NetBox [event rules](../../models/extras/eventrule.md). This is accomplished by calling the `register()` method on an instance of the `EventType` class. This can be done anywhere within the plugin. An example is provided below.
```python

View File

@ -98,28 +98,29 @@ NetBox looks for the `config` variable within a plugin's `__init__.py` to load i
### PluginConfig Attributes
| Name | Description |
|-----------------------|--------------------------------------------------------------------------------------------------------------------------|
| `name` | Raw plugin name; same as the plugin's source directory |
| `verbose_name` | Human-friendly name for the plugin |
| `version` | Current release ([semantic versioning](https://semver.org/) is encouraged) |
| `description` | Brief description of the plugin's purpose |
| `author` | Name of plugin's author |
| `author_email` | Author's public email address |
| `base_url` | Base path to use for plugin URLs (optional). If not specified, the project's `name` will be used. |
| `required_settings` | A list of any configuration parameters that **must** be defined by the user |
| `default_settings` | A dictionary of configuration parameters and their default values |
| `django_apps` | A list of additional Django apps to load alongside the plugin |
| `min_version` | Minimum version of NetBox with which the plugin is compatible |
| `max_version` | Maximum version of NetBox with which the plugin is compatible |
| `middleware` | A list of middleware classes to append after NetBox's build-in middleware |
| `queues` | A list of custom background task queues to create |
| `search_extensions` | The dotted path to the list of search index classes (default: `search.indexes`) |
| `data_backends` | The dotted path to the list of data source backend classes (default: `data_backends.backends`) |
| `template_extensions` | The dotted path to the list of template extension classes (default: `template_content.template_extensions`) |
| `menu_items` | The dotted path to the list of menu items provided by the plugin (default: `navigation.menu_items`) |
| `graphql_schema` | The dotted path to the plugin's GraphQL schema class, if any (default: `graphql.schema`) |
| `user_preferences` | The dotted path to the dictionary mapping of user preferences defined by the plugin (default: `preferences.preferences`) |
| Name | Description |
|-----------------------|------------------------------------------------------------------------------------------------------------------------------------|
| `name` | Raw plugin name; same as the plugin's source directory |
| `verbose_name` | Human-friendly name for the plugin |
| `version` | Current release ([semantic versioning](https://semver.org/) is encouraged) |
| `description` | Brief description of the plugin's purpose |
| `author` | Name of plugin's author |
| `author_email` | Author's public email address |
| `base_url` | Base path to use for plugin URLs (optional). If not specified, the project's `name` will be used. |
| `required_settings` | A list of any configuration parameters that **must** be defined by the user |
| `default_settings` | A dictionary of configuration parameters and their default values |
| `django_apps` | A list of additional Django apps to load alongside the plugin |
| `min_version` | Minimum version of NetBox with which the plugin is compatible |
| `max_version` | Maximum version of NetBox with which the plugin is compatible |
| `middleware` | A list of middleware classes to append after NetBox's build-in middleware |
| `queues` | A list of custom background task queues to create |
| `events_pipeline` | A list of handlers to add to [`EVENTS_PIPELINE`](../../configuration/miscellaneous.md#events_pipeline), identified by dotted paths |
| `search_extensions` | The dotted path to the list of search index classes (default: `search.indexes`) |
| `data_backends` | The dotted path to the list of data source backend classes (default: `data_backends.backends`) |
| `template_extensions` | The dotted path to the list of template extension classes (default: `template_content.template_extensions`) |
| `menu_items` | The dotted path to the list of menu items provided by the plugin (default: `navigation.menu_items`) |
| `graphql_schema` | The dotted path to the plugin's GraphQL schema class, if any (default: `graphql.schema`) |
| `user_preferences` | The dotted path to the dictionary mapping of user preferences defined by the plugin (default: `preferences.preferences`) |
All required settings must be configured by the user. If a configuration parameter is listed in both `required_settings` and `default_settings`, the default setting will be ignored.

View File

@ -1,7 +1,7 @@
# Staged Changes
!!! danger "Experimental Feature"
This feature is still under active development and considered experimental in nature. Its use in production is strongly discouraged at this time.
!!! danger "Deprecated Feature"
This feature has been deprecated in NetBox v4.2 and will be removed in a future release. Please consider using the [netbox-branching plugin](https://github.com/netboxlabs/netbox-branching), which provides much more robust functionality.
NetBox provides a programmatic API to stage the creation, modification, and deletion of objects without actually committing those changes to the active database. This can be useful for performing a "dry run" of bulk operations, or preparing a set of changes for administrative approval, for example.

View File

@ -185,6 +185,9 @@ class MyView(generic.ObjectView):
)
```
!!! note "Changed in NetBox v4.2"
The `register_model_view()` function was extended in NetBox v4.2 to support registration of list views by passing `detail=False`.
::: utilities.views.register_model_view
::: utilities.views.ViewTab
@ -203,8 +206,6 @@ Plugins can inject custom content into certain areas of core NetBox views. This
| `right_page()` | Object view | Inject content on the right side of the page |
| `full_width_page()` | Object view | Inject content across the entire bottom of the page |
!!! info "The `navbar()` and `alerts()` methods were introduced in NetBox v4.1."
Additionally, a `render()` method is available for convenience. This method accepts the name of a template to render, and any additional context data you want to pass. Its use is optional, however.
To control where the custom content is injected, plugin authors can specify an iterable of models by overriding the `models` attribute on the subclass. Extensions which do not specify a set of models will be invoked on every view, where supported.

View File

@ -10,6 +10,14 @@ Minor releases are published in April, August, and December of each calendar yea
This page contains a history of all major and minor releases since NetBox v2.0. For more detail on a specific patch release, please see the release notes page for that specific minor release.
#### [Version 4.2](./version-4.2.md) (January 2025)
* Assign Multiple MAC Addresses per Interface ([#4867](https://github.com/netbox-community/netbox/issues/4867))
* Quick Add UI Widget ([#5858](https://github.com/netbox-community/netbox/issues/5858))
* VLAN Translation ([#7336](https://github.com/netbox-community/netbox/issues/7336))
* Virtual Circuits ([#13086](https://github.com/netbox-community/netbox/issues/13086))
* Q-in-Q Encapsulation ([#13428](https://github.com/netbox-community/netbox/issues/13428))
#### [Version 4.1](./version-4.1.md) (September 2024)
* Circuit Groups ([#7025](https://github.com/netbox-community/netbox/issues/7025))

View File

@ -0,0 +1,126 @@
# NetBox v4.2
## v4.2-beta1 (2024-12-02)
!!! danger "Not for Production Use"
This is a beta release of NetBox intended for testing and evaluation. **Do not use this software in production.** Also be aware that no upgrade path is provided to future releases.
### Breaking Changes
* Support for the Django admin UI has been completely removed. (The Django admin UI was disabled by default in NetBox v4.0.)
* NetBox has adopted collation-based natural ordering for many models. This may alter the order in which some objects are listed by default.
* Automatic redirects from pre-v4.1 UI views for virtual disks have been removed.
* The `site` and `provider_network` foreign key fields on `circuits.CircuitTermination` have been replaced by the `termination` generic foreign key.
* The `site` foreign key field on `ipam.Prefix` has been replaced by the `scope` generic foreign key.
* The `site` foreign key field on `virtualization.Cluster` has been replaced by the `scope` generic foreign key.
* The `circuit` foreign key field on `circuits.CircuitGroupAssignment` has been replaced by the `member` generic foreign key.
* Obsolete nested REST API serializers have been removed. These were deprecated in NetBox v4.1 under [#17143](https://github.com/netbox-community/netbox/issues/17143).
### New Features
#### Assign Multiple MAC Addresses per Interface ([#4867](https://github.com/netbox-community/netbox/issues/4867))
MAC addresses are now managed as independent objects, rather than attributes on device and VM interfaces. NetBox now supports the assignment of multiple MAC addresses per interface, and allows a primary MAC address to be designated for each.
#### Quick Add UI Widget ([#5858](https://github.com/netbox-community/netbox/issues/5858))
A new UI widget has been introduced to enable conveniently creating new related objects while creating or editing an object. For instance, it is now possible to create and assign a new device role when creating or editing a device from within the device form.
#### VLAN Translation ([#7336](https://github.com/netbox-community/netbox/issues/7336))
User can now define policies which track the translation of VLAN IDs on IEEE 802.1Q-encapsulated interfaces. Translation policies can be reused across multiple interfaces.
#### Virtual Circuits ([#13086](https://github.com/netbox-community/netbox/issues/13086))
New models have been introduced to support the documentation of virtual circuits as an extension to the physical circuit modeling already supported. This enables users to accurately reflect point-to-point or multipoint virtual circuits atop infrastructure comprising physical circuits and cables.
#### Q-in-Q Encapsulation ([#13428](https://github.com/netbox-community/netbox/issues/13428))
NetBox now supports the designation of customer VLANs (CVLANs) and service VLANs (SVLANs) to support IEEE 802.1ad/Q-in-Q encapsulation. Each interface can now have it mode designated "Q-in-Q" and be assigned an SVLAN.
### Enhancements
* [#6414](https://github.com/netbox-community/netbox/issues/6414) - Prefixes can now be scoped by region, site group, site, or location
* [#7699](https://github.com/netbox-community/netbox/issues/7699) - Virtualization clusters can now be scoped by region, site group, site, or location
* [#9604](https://github.com/netbox-community/netbox/issues/9604) - The scope of a circuit termination now include a region, site group, site, location, or provider network
* [#10711](https://github.com/netbox-community/netbox/issues/10711) - Wireless LANs can now be scoped by region, site group, site, or location
* [#11279](https://github.com/netbox-community/netbox/issues/11279) - Improved the use of natural ordering for various models throughout the application
* [#12596](https://github.com/netbox-community/netbox/issues/12596) - Extended the virtualization clusters REST API endpoint to report on allocated VM resources
* [#16547](https://github.com/netbox-community/netbox/issues/16547) - Add a geographic distance field for circuits
* [#16783](https://github.com/netbox-community/netbox/issues/16783) - Add an operational status field for inventory items
* [#17195](https://github.com/netbox-community/netbox/issues/17195) - Add a color field for power outlets
### Plugins
* [#15093](https://github.com/netbox-community/netbox/issues/15093) - Introduced the `events_pipeline` configuration parameter, which allows plugins to hook into NetBox event processing
* [#16546](https://github.com/netbox-community/netbox/issues/16546) - NetBoxModel now provides a default `get_absolute_url()` method
* [#16971](https://github.com/netbox-community/netbox/issues/16971) - Plugins can now easily register system jobs to perform background tasks
* [#17029](https://github.com/netbox-community/netbox/issues/17029) - Registering a `PluginTemplateExtension` subclass for a single model has been deprecated (replace `model` with `models`)
* [#18023](https://github.com/netbox-community/netbox/issues/18023) - Extend `register_model_view()` to handle list views
### Other Changes
* [#16136](https://github.com/netbox-community/netbox/issues/16136) - Removed support for the Django admin UI
* [#17165](https://github.com/netbox-community/netbox/issues/17165) - All obsolete nested REST API serializers have been removed
* [#17472](https://github.com/netbox-community/netbox/issues/17472) - The legacy staged changes API has been deprecated, and will be removed in Netbox v4.3
* [#17476](https://github.com/netbox-community/netbox/issues/17476) - Upgrade to Django 5.1
* [#17752](https://github.com/netbox-community/netbox/issues/17752) - Bulk object import URL paths have been renamed from `*_import` to `*_bulk_import`
* [#17761](https://github.com/netbox-community/netbox/issues/17761) - Optional choice fields now store empty values as null (rather than empty strings) in the database
* [#18093](https://github.com/netbox-community/netbox/issues/18093) - Redirects for pre-v4.1 virtual disk UI views have been removed
### REST API Changes
* Added the following endpoints:
* `/api/circuits/virtual-circuits/`
* `/api/circuits/virtual-circuit-terminations/`
* `/api/dcim/mac-addresses/`
* `/api/ipam/vlan-translation-policies/`
* `/api/ipam/vlan-translation-rules/`
* circuits.Circuit
* Added the optional `distance` and `distance_unit` fields
* circuits.CircuitGroupAssignment
* Replaced the `circuit` field with `member_type` and `member_id` to support virtual circuit assignment
* circuits.CircuitTermination
* Removed the `site` & `provider_network` fields
* Added the `termination_type` & `termination_id` fields to facilitate termination assignment
* Added the read-only `termination` field
* dcim.Interface
* The `mac_address` field is now read-only
* Added the `primary_mac_address` relation to dcim.MACAddress
* Added the read-only `mac_addresses` list
* Added the `qinq_svlan` relation to ipam.VLAN
* Added the `vlan_translation_policy` relation to ipam.VLANTranslationPolicy
* Added `mode` choice "Q-in-Q"
* dcim.InventoryItem
* Added the optional `status` choice field
* dcim.Location
* Added the read-only `prefix_count` field
* dcim.PowerOutlet
* Added the optional `color` field
* dcim.Region
* Added the read-only `prefix_count` field
* dcim.SiteGroup
* Added the read-only `prefix_count` field
* ipam.Prefix
* Removed the `site` field
* Added the `scope_type` & `scope_id` fields to facilitate scope assignment
* Added the read-only `scope` field
* ipam.VLAN
* Added the optional `qinq_role` selection field
* Added the `qinq_svlan` recursive relation
* virtualization.Cluster
* Removed the `site` field
* Added the `scope_type` & `scope_id` fields to facilitate scope assignment
* Added the read-only `scope` field
* virtualization.Cluster
* Added the read-only fields `allocated_vcpus`, `allocated_memory`, and `allocated_disk`
* virtualization.VMInterface
* The `mac_address` field is now read-only
* Added the `primary_mac_address` relation to dcim.MACAddress
* Added the read-only `mac_addresses` list
* Added the `qinq_svlan` relation to ipam.VLAN
* Added the `vlan_translation_policy` relation to ipam.VLANTranslationPolicy
* Added `mode` choice "Q-in-Q"
* wireless.WirelessLAN
* Added the `scope_type` & `scope_id` fields to support scope assignment
* Added the read-only `scope` field

View File

@ -174,6 +174,8 @@ nav:
- Provider: 'models/circuits/provider.md'
- Provider Account: 'models/circuits/provideraccount.md'
- Provider Network: 'models/circuits/providernetwork.md'
- Virtual Circuit: 'models/circuits/virtualcircuit.md'
- Virtual Circuit Termination: 'models/circuits/virtualcircuittermination.md'
- Core:
- DataFile: 'models/core/datafile.md'
- DataSource: 'models/core/datasource.md'
@ -197,6 +199,7 @@ nav:
- InventoryItemRole: 'models/dcim/inventoryitemrole.md'
- InventoryItemTemplate: 'models/dcim/inventoryitemtemplate.md'
- Location: 'models/dcim/location.md'
- MACAddress: 'models/dcim/macaddress.md'
- Manufacturer: 'models/dcim/manufacturer.md'
- Module: 'models/dcim/module.md'
- ModuleBay: 'models/dcim/modulebay.md'
@ -255,6 +258,8 @@ nav:
- ServiceTemplate: 'models/ipam/servicetemplate.md'
- VLAN: 'models/ipam/vlan.md'
- VLANGroup: 'models/ipam/vlangroup.md'
- VLANTranslationPolicy: 'models/ipam/vlantranslationpolicy.md'
- VLANTranslationRule: 'models/ipam/vlantranslationrule.md'
- VRF: 'models/ipam/vrf.md'
- Tenancy:
- Contact: 'models/tenancy/contact.md'
@ -306,6 +311,7 @@ nav:
- git Cheat Sheet: 'development/git-cheat-sheet.md'
- Release Notes:
- Summary: 'release-notes/index.md'
- Version 4.2: 'release-notes/version-4.2.md'
- Version 4.1: 'release-notes/version-4.1.md'
- Version 4.0: 'release-notes/version-4.0.md'
- Version 3.7: 'release-notes/version-3.7.md'

View File

@ -1,79 +0,0 @@
import warnings
from drf_spectacular.utils import extend_schema_serializer
from circuits.models import *
from netbox.api.fields import RelatedObjectCountField
from netbox.api.serializers import WritableNestedSerializer
from .serializers_.nested import NestedProviderAccountSerializer
__all__ = [
'NestedCircuitSerializer',
'NestedCircuitTerminationSerializer',
'NestedCircuitTypeSerializer',
'NestedProviderNetworkSerializer',
'NestedProviderSerializer',
'NestedProviderAccountSerializer',
]
# TODO: Remove in v4.2
warnings.warn(
"Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
DeprecationWarning
)
#
# Provider networks
#
class NestedProviderNetworkSerializer(WritableNestedSerializer):
class Meta:
model = ProviderNetwork
fields = ['id', 'url', 'display_url', 'display', 'name']
#
# Providers
#
@extend_schema_serializer(
exclude_fields=('circuit_count',),
)
class NestedProviderSerializer(WritableNestedSerializer):
circuit_count = RelatedObjectCountField('circuits')
class Meta:
model = Provider
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'circuit_count']
#
# Circuits
#
@extend_schema_serializer(
exclude_fields=('circuit_count',),
)
class NestedCircuitTypeSerializer(WritableNestedSerializer):
circuit_count = RelatedObjectCountField('circuits')
class Meta:
model = CircuitType
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'circuit_count']
class NestedCircuitSerializer(WritableNestedSerializer):
class Meta:
model = Circuit
fields = ['id', 'url', 'display_url', 'display', 'cid']
class NestedCircuitTerminationSerializer(WritableNestedSerializer):
circuit = NestedCircuitSerializer()
class Meta:
model = CircuitTermination
fields = ['id', 'url', 'display_url', 'display', 'circuit', 'term_side', 'cable', '_occupied']

View File

@ -1,11 +1,20 @@
from circuits.choices import CircuitPriorityChoices, CircuitStatusChoices
from circuits.models import Circuit, CircuitGroup, CircuitGroupAssignment, CircuitTermination, CircuitType
from dcim.api.serializers_.cables import CabledObjectSerializer
from dcim.api.serializers_.sites import SiteSerializer
from netbox.api.fields import ChoiceField, RelatedObjectCountField
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
from tenancy.api.serializers_.tenants import TenantSerializer
from django.contrib.contenttypes.models import ContentType
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from circuits.choices import CircuitPriorityChoices, CircuitStatusChoices, VirtualCircuitTerminationRoleChoices
from circuits.constants import CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS, CIRCUIT_TERMINATION_TERMINATION_TYPES
from circuits.models import (
Circuit, CircuitGroup, CircuitGroupAssignment, CircuitTermination, CircuitType, VirtualCircuit,
VirtualCircuitTermination, VirtualCircuitType,
)
from dcim.api.serializers_.device_components import InterfaceSerializer
from dcim.api.serializers_.cables import CabledObjectSerializer
from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
from netbox.choices import DistanceUnitChoices
from tenancy.api.serializers_.tenants import TenantSerializer
from utilities.api import get_serializer_for_model
from .providers import ProviderAccountSerializer, ProviderNetworkSerializer, ProviderSerializer
__all__ = (
@ -14,6 +23,9 @@ __all__ = (
'CircuitGroupSerializer',
'CircuitTerminationSerializer',
'CircuitTypeSerializer',
'VirtualCircuitSerializer',
'VirtualCircuitTerminationSerializer',
'VirtualCircuitTypeSerializer',
)
@ -32,16 +44,32 @@ class CircuitTypeSerializer(NetBoxModelSerializer):
class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
site = SiteSerializer(nested=True, allow_null=True)
provider_network = ProviderNetworkSerializer(nested=True, allow_null=True)
termination_type = ContentTypeField(
queryset=ContentType.objects.filter(
model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES
),
allow_null=True,
required=False,
default=None
)
termination_id = serializers.IntegerField(allow_null=True, required=False, default=None)
termination = serializers.SerializerMethodField(read_only=True)
class Meta:
model = CircuitTermination
fields = [
'id', 'url', 'display_url', 'display', 'site', 'provider_network', 'port_speed', 'upstream_speed',
'xconnect_id', 'description',
'id', 'url', 'display_url', 'display', 'termination_type', 'termination_id', 'termination', 'port_speed',
'upstream_speed', 'xconnect_id', 'description',
]
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_termination(self, obj):
if obj.termination_id is None:
return None
serializer = get_serializer_for_model(obj.termination)
context = {'request': self.context['request']}
return serializer(obj.termination, nested=True, context=context).data
class CircuitGroupSerializer(NetBoxModelSerializer):
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
@ -76,6 +104,7 @@ class CircuitSerializer(NetBoxModelSerializer):
provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True, default=None)
status = ChoiceField(choices=CircuitStatusChoices, required=False)
type = CircuitTypeSerializer(nested=True)
distance_unit = ChoiceField(choices=DistanceUnitChoices, allow_blank=True, required=False, allow_null=True)
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
termination_a = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
termination_z = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
@ -85,33 +114,107 @@ class CircuitSerializer(NetBoxModelSerializer):
model = Circuit
fields = [
'id', 'url', 'display_url', 'display', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant',
'install_date', 'termination_date', 'commit_rate', 'description', 'termination_a', 'termination_z',
'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'assignments',
'install_date', 'termination_date', 'commit_rate', 'description', 'distance', 'distance_unit',
'termination_a', 'termination_z', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
'assignments',
]
brief_fields = ('id', 'url', 'display', 'provider', 'cid', 'description')
class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer):
circuit = CircuitSerializer(nested=True)
site = SiteSerializer(nested=True, required=False, allow_null=True)
provider_network = ProviderNetworkSerializer(nested=True, required=False, allow_null=True)
termination_type = ContentTypeField(
queryset=ContentType.objects.filter(
model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES
),
allow_null=True,
required=False,
default=None
)
termination_id = serializers.IntegerField(allow_null=True, required=False, default=None)
termination = serializers.SerializerMethodField(read_only=True)
class Meta:
model = CircuitTermination
fields = [
'id', 'url', 'display_url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed',
'upstream_speed', 'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'cable_end',
'link_peers', 'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
'id', 'url', 'display_url', 'display', 'circuit', 'term_side', 'termination_type', 'termination_id',
'termination', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', 'description', 'mark_connected',
'cable', 'cable_end', 'link_peers', 'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated',
'_occupied',
]
brief_fields = ('id', 'url', 'display', 'circuit', 'term_side', 'description', 'cable', '_occupied')
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_termination(self, obj):
if obj.termination_id is None:
return None
serializer = get_serializer_for_model(obj.termination)
context = {'request': self.context['request']}
return serializer(obj.termination, nested=True, context=context).data
class CircuitGroupAssignmentSerializer(CircuitGroupAssignmentSerializer_):
circuit = CircuitSerializer(nested=True)
member_type = ContentTypeField(
queryset=ContentType.objects.filter(CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS)
)
member = serializers.SerializerMethodField(read_only=True)
class Meta:
model = CircuitGroupAssignment
fields = [
'id', 'url', 'display_url', 'display', 'group', 'circuit', 'priority', 'tags', 'created', 'last_updated',
'id', 'url', 'display_url', 'display', 'group', 'member_type', 'member_id', 'member', 'priority', 'tags',
'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'group', 'circuit', 'priority')
brief_fields = ('id', 'url', 'display', 'group', 'member_type', 'member_id', 'member', 'priority')
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_member(self, obj):
if obj.member_id is None:
return None
serializer = get_serializer_for_model(obj.member)
context = {'request': self.context['request']}
return serializer(obj.member, nested=True, context=context).data
class VirtualCircuitTypeSerializer(NetBoxModelSerializer):
# Related object counts
virtual_circuit_count = RelatedObjectCountField('virtual_circuits')
class Meta:
model = VirtualCircuitType
fields = [
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields',
'created', 'last_updated', 'virtual_circuit_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'virtual_circuit_count')
class VirtualCircuitSerializer(NetBoxModelSerializer):
provider_network = ProviderNetworkSerializer(nested=True)
provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True, default=None)
type = VirtualCircuitTypeSerializer(nested=True)
status = ChoiceField(choices=CircuitStatusChoices, required=False)
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
class Meta:
model = VirtualCircuit
fields = [
'id', 'url', 'display_url', 'display', 'cid', 'provider_network', 'provider_account', 'type', 'status',
'tenant', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'provider_network', 'cid', 'description')
class VirtualCircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer):
virtual_circuit = VirtualCircuitSerializer(nested=True)
role = ChoiceField(choices=VirtualCircuitTerminationRoleChoices, required=False)
interface = InterfaceSerializer(nested=True)
class Meta:
model = VirtualCircuitTermination
fields = [
'id', 'url', 'display_url', 'display', 'virtual_circuit', 'role', 'interface', 'description', 'tags',
'custom_fields', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'virtual_circuit', 'role', 'interface', 'description')

View File

@ -17,5 +17,10 @@ router.register('circuit-terminations', views.CircuitTerminationViewSet)
router.register('circuit-groups', views.CircuitGroupViewSet)
router.register('circuit-group-assignments', views.CircuitGroupAssignmentViewSet)
# Virtual circuits
router.register('virtual-circuits', views.VirtualCircuitViewSet)
router.register('virtual-circuit-types', views.VirtualCircuitTypeViewSet)
router.register('virtual-circuit-terminations', views.VirtualCircuitTerminationViewSet)
app_name = 'circuits-api'
urlpatterns = router.urls

View File

@ -93,3 +93,33 @@ class ProviderNetworkViewSet(NetBoxModelViewSet):
queryset = ProviderNetwork.objects.all()
serializer_class = serializers.ProviderNetworkSerializer
filterset_class = filtersets.ProviderNetworkFilterSet
#
# Virtual circuit types
#
class VirtualCircuitTypeViewSet(NetBoxModelViewSet):
queryset = VirtualCircuitType.objects.all()
serializer_class = serializers.VirtualCircuitTypeSerializer
filterset_class = filtersets.VirtualCircuitTypeFilterSet
#
# Virtual circuits
#
class VirtualCircuitViewSet(NetBoxModelViewSet):
queryset = VirtualCircuit.objects.all()
serializer_class = serializers.VirtualCircuitSerializer
filterset_class = filtersets.VirtualCircuitFilterSet
#
# Virtual circuit terminations
#
class VirtualCircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
queryset = VirtualCircuitTermination.objects.all()
serializer_class = serializers.VirtualCircuitTerminationSerializer
filterset_class = filtersets.VirtualCircuitTerminationFilterSet

View File

@ -92,3 +92,19 @@ class CircuitPriorityChoices(ChoiceSet):
(PRIORITY_TERTIARY, _('Tertiary')),
(PRIORITY_INACTIVE, _('Inactive')),
]
#
# Virtual circuits
#
class VirtualCircuitTerminationRoleChoices(ChoiceSet):
ROLE_PEER = 'peer'
ROLE_HUB = 'hub'
ROLE_SPOKE = 'spoke'
CHOICES = [
(ROLE_PEER, _('Peer'), 'green'),
(ROLE_HUB, _('Hub'), 'blue'),
(ROLE_SPOKE, _('Spoke'), 'orange'),
]

View File

@ -0,0 +1,12 @@
from django.db.models import Q
# models values for ContentTypes which may be CircuitTermination termination types
CIRCUIT_TERMINATION_TERMINATION_TYPES = (
'region', 'sitegroup', 'site', 'location', 'providernetwork',
)
CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS = Q(
app_label='circuits',
model__in=['circuit', 'virtualcircuit']
)

View File

@ -1,13 +1,16 @@
import django_filters
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.utils.translation import gettext as _
from dcim.filtersets import CabledObjectFilterSet
from dcim.models import Region, Site, SiteGroup
from dcim.models import Interface, Location, Region, Site, SiteGroup
from ipam.models import ASN
from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
from utilities.filters import TreeNodeMultipleChoiceFilter
from utilities.filters import (
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, TreeNodeMultipleChoiceFilter,
)
from .choices import *
from .models import *
@ -20,43 +23,46 @@ __all__ = (
'ProviderNetworkFilterSet',
'ProviderAccountFilterSet',
'ProviderFilterSet',
'VirtualCircuitFilterSet',
'VirtualCircuitTerminationFilterSet',
'VirtualCircuitTypeFilterSet',
)
class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='circuits__terminations__site__region',
field_name='circuits__terminations___region',
lookup_expr='in',
label=_('Region (ID)'),
)
region = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='circuits__terminations__site__region',
field_name='circuits__terminations___region',
lookup_expr='in',
to_field_name='slug',
label=_('Region (slug)'),
)
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='circuits__terminations__site__group',
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',
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',
field_name='circuits__terminations___site',
queryset=Site.objects.all(),
label=_('Site'),
)
site = django_filters.ModelMultipleChoiceFilter(
field_name='circuits__terminations__site__slug',
field_name='circuits__terminations___site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label=_('Site (slug)'),
@ -173,7 +179,7 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
label=_('Provider account (account)'),
)
provider_network_id = django_filters.ModelMultipleChoiceFilter(
field_name='terminations__provider_network',
field_name='terminations___provider_network',
queryset=ProviderNetwork.objects.all(),
label=_('Provider network (ID)'),
)
@ -193,37 +199,37 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
)
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='terminations__site__region',
field_name='terminations___region',
lookup_expr='in',
label=_('Region (ID)'),
)
region = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='terminations__site__region',
field_name='terminations___region',
lookup_expr='in',
to_field_name='slug',
label=_('Region (slug)'),
)
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='terminations__site__group',
field_name='terminations___site_group',
lookup_expr='in',
label=_('Site group (ID)'),
)
site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='terminations__site__group',
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',
field_name='terminations___site',
queryset=Site.objects.all(),
label=_('Site (ID)'),
)
site = django_filters.ModelMultipleChoiceFilter(
field_name='terminations__site__slug',
field_name='terminations___site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label=_('Site (slug)'),
@ -239,7 +245,9 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
class Meta:
model = Circuit
fields = ('id', 'cid', 'description', 'install_date', 'termination_date', 'commit_rate')
fields = (
'id', 'cid', 'description', 'install_date', 'termination_date', 'commit_rate', 'distance', 'distance_unit',
)
def search(self, queryset, name, value):
if not value.strip():
@ -263,18 +271,60 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
queryset=Circuit.objects.all(),
label=_('Circuit'),
)
termination_type = ContentTypeFilter()
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='_region',
lookup_expr='in',
label=_('Region (ID)'),
)
region = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='_region',
lookup_expr='in',
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(),
field_name='_site',
label=_('Site (ID)'),
)
site = django_filters.ModelMultipleChoiceFilter(
field_name='site__slug',
field_name='_site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label=_('Site (slug)'),
)
location_id = TreeNodeMultipleChoiceFilter(
queryset=Location.objects.all(),
field_name='_location',
lookup_expr='in',
label=_('Location (ID)'),
)
location = TreeNodeMultipleChoiceFilter(
queryset=Location.objects.all(),
field_name='_location',
lookup_expr='in',
to_field_name='slug',
label=_('Location (slug)'),
)
provider_network_id = django_filters.ModelMultipleChoiceFilter(
queryset=ProviderNetwork.objects.all(),
field_name='_provider_network',
label=_('ProviderNetwork (ID)'),
)
provider_id = django_filters.ModelMultipleChoiceFilter(
@ -292,8 +342,8 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
class Meta:
model = CircuitTermination
fields = (
'id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description', 'mark_connected',
'pp_info', 'cable_end',
'id', 'termination_id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description',
'mark_connected', 'pp_info', 'cable_end',
)
def search(self, queryset, name, value):
@ -319,26 +369,36 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
method='search',
label=_('Search'),
)
provider_id = django_filters.ModelMultipleChoiceFilter(
field_name='circuit__provider',
queryset=Provider.objects.all(),
label=_('Provider (ID)'),
member_type = ContentTypeFilter()
circuit = MultiValueCharFilter(
method='filter_circuit',
field_name='cid',
label=_('Circuit (CID)'),
)
provider = django_filters.ModelMultipleChoiceFilter(
field_name='circuit__provider__slug',
queryset=Provider.objects.all(),
to_field_name='slug',
label=_('Provider (slug)'),
)
circuit_id = django_filters.ModelMultipleChoiceFilter(
queryset=Circuit.objects.all(),
circuit_id = MultiValueNumberFilter(
method='filter_circuit',
field_name='pk',
label=_('Circuit (ID)'),
)
circuit = django_filters.ModelMultipleChoiceFilter(
field_name='circuit__cid',
queryset=Circuit.objects.all(),
to_field_name='cid',
label=_('Circuit (CID)'),
virtual_circuit = MultiValueCharFilter(
method='filter_virtual_circuit',
field_name='cid',
label=_('Virtual circuit (CID)'),
)
virtual_circuit_id = MultiValueNumberFilter(
method='filter_virtual_circuit',
field_name='pk',
label=_('Virtual circuit (ID)'),
)
provider = MultiValueCharFilter(
method='filter_provider',
field_name='slug',
label=_('Provider (name)'),
)
provider_id = MultiValueNumberFilter(
method='filter_provider',
field_name='pk',
label=_('Provider (ID)'),
)
group_id = django_filters.ModelMultipleChoiceFilter(
queryset=CircuitGroup.objects.all(),
@ -353,12 +413,173 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
class Meta:
model = CircuitGroupAssignment
fields = ('id', 'priority')
fields = ('id', 'member_id', 'priority')
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(circuit__cid__icontains=value) |
Q(member__cid__icontains=value) |
Q(group__name__icontains=value)
)
def filter_circuit(self, queryset, name, value):
circuits = Circuit.objects.filter(**{f'{name}__in': value})
if not circuits.exists():
return queryset.none()
return queryset.filter(
Q(
member_type=ContentType.objects.get_for_model(Circuit),
member_id__in=circuits
)
)
def filter_virtual_circuit(self, queryset, name, value):
virtual_circuits = VirtualCircuit.objects.filter(**{f'{name}__in': value})
if not virtual_circuits.exists():
return queryset.none()
return queryset.filter(
Q(
member_type=ContentType.objects.get_for_model(VirtualCircuit),
member_id__in=virtual_circuits
)
)
def filter_provider(self, queryset, name, value):
providers = Provider.objects.filter(**{f'{name}__in': value})
if not providers.exists():
return queryset.none()
circuits = Circuit.objects.filter(provider__in=providers)
virtual_circuits = VirtualCircuit.objects.filter(provider_network__provider__in=providers)
return queryset.filter(
Q(
member_type=ContentType.objects.get_for_model(Circuit),
member_id__in=circuits
) |
Q(
member_type=ContentType.objects.get_for_model(VirtualCircuit),
member_id__in=virtual_circuits
)
)
class VirtualCircuitTypeFilterSet(OrganizationalModelFilterSet):
class Meta:
model = VirtualCircuitType
fields = ('id', 'name', 'slug', 'color', 'description')
class VirtualCircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
provider_id = django_filters.ModelMultipleChoiceFilter(
field_name='provider_network__provider',
queryset=Provider.objects.all(),
label=_('Provider (ID)'),
)
provider = django_filters.ModelMultipleChoiceFilter(
field_name='provider_network__provider__slug',
queryset=Provider.objects.all(),
to_field_name='slug',
label=_('Provider (slug)'),
)
provider_account_id = django_filters.ModelMultipleChoiceFilter(
field_name='provider_account',
queryset=ProviderAccount.objects.all(),
label=_('Provider account (ID)'),
)
provider_account = django_filters.ModelMultipleChoiceFilter(
field_name='provider_account__account',
queryset=Provider.objects.all(),
to_field_name='account',
label=_('Provider account (account)'),
)
provider_network_id = django_filters.ModelMultipleChoiceFilter(
queryset=ProviderNetwork.objects.all(),
label=_('Provider network (ID)'),
)
type_id = django_filters.ModelMultipleChoiceFilter(
queryset=VirtualCircuitType.objects.all(),
label=_('Virtual circuit type (ID)'),
)
type = django_filters.ModelMultipleChoiceFilter(
field_name='type__slug',
queryset=VirtualCircuitType.objects.all(),
to_field_name='slug',
label=_('Virtual circuit type (slug)'),
)
status = django_filters.MultipleChoiceFilter(
choices=CircuitStatusChoices,
null_value=None
)
class Meta:
model = VirtualCircuit
fields = ('id', 'cid', 'description')
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(cid__icontains=value) |
Q(description__icontains=value) |
Q(comments__icontains=value)
).distinct()
class VirtualCircuitTerminationFilterSet(NetBoxModelFilterSet):
q = django_filters.CharFilter(
method='search',
label=_('Search'),
)
virtual_circuit_id = django_filters.ModelMultipleChoiceFilter(
queryset=VirtualCircuit.objects.all(),
label=_('Virtual circuit'),
)
role = django_filters.MultipleChoiceFilter(
choices=VirtualCircuitTerminationRoleChoices,
null_value=None
)
provider_id = django_filters.ModelMultipleChoiceFilter(
field_name='virtual_circuit__provider_network__provider',
queryset=Provider.objects.all(),
label=_('Provider (ID)'),
)
provider = django_filters.ModelMultipleChoiceFilter(
field_name='virtual_circuit__provider_network__provider__slug',
queryset=Provider.objects.all(),
to_field_name='slug',
label=_('Provider (slug)'),
)
provider_account_id = django_filters.ModelMultipleChoiceFilter(
field_name='virtual_circuit__provider_account',
queryset=ProviderAccount.objects.all(),
label=_('Provider account (ID)'),
)
provider_account = django_filters.ModelMultipleChoiceFilter(
field_name='virtual_circuit__provider_account__account',
queryset=ProviderAccount.objects.all(),
to_field_name='account',
label=_('Provider account (account)'),
)
provider_network_id = django_filters.ModelMultipleChoiceFilter(
queryset=ProviderNetwork.objects.all(),
field_name='virtual_circuit__provider_network',
label=_('Provider network (ID)'),
)
interface_id = django_filters.ModelMultipleChoiceFilter(
queryset=Interface.objects.all(),
field_name='interface',
label=_('Interface (ID)'),
)
class Meta:
model = VirtualCircuitTermination
fields = ('id', 'interface_id', 'description')
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(virtual_circuit__cid__icontains=value) |
Q(description__icontains=value)
).distinct()

View File

@ -1,16 +1,25 @@
from django import forms
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import gettext_lazy as _
from circuits.choices import CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices
from circuits.choices import (
CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices, VirtualCircuitTerminationRoleChoices,
)
from circuits.constants import CIRCUIT_TERMINATION_TERMINATION_TYPES
from circuits.models import *
from dcim.models import Site
from ipam.models import ASN
from netbox.choices import DistanceUnitChoices
from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant
from utilities.forms import add_blank_choice
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
from utilities.forms.rendering import FieldSet, TabbedGroups
from utilities.forms.widgets import BulkEditNullBooleanSelect, DatePicker, NumberWithOptions
from utilities.forms import add_blank_choice, get_field_value
from utilities.forms.fields import (
ColorField, CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
)
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import BulkEditNullBooleanSelect, DatePicker, HTMXSelect, NumberWithOptions
from utilities.templatetags.builtins.filters import bettertitle
__all__ = (
'CircuitBulkEditForm',
@ -21,6 +30,9 @@ __all__ = (
'ProviderBulkEditForm',
'ProviderAccountBulkEditForm',
'ProviderNetworkBulkEditForm',
'VirtualCircuitBulkEditForm',
'VirtualCircuitTerminationBulkEditForm',
'VirtualCircuitTypeBulkEditForm',
)
@ -160,6 +172,17 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
options=CircuitCommitRateChoices
)
)
distance = forms.DecimalField(
label=_('Distance'),
min_value=0,
required=False
)
distance_unit = forms.ChoiceField(
label=_('Distance unit'),
choices=add_blank_choice(DistanceUnitChoices),
required=False,
initial=''
)
description = forms.CharField(
label=_('Description'),
max_length=100,
@ -171,6 +194,7 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
fieldsets = (
FieldSet('provider', 'type', 'status', 'description', name=_('Circuit')),
FieldSet('provider_account', 'install_date', 'termination_date', 'commit_rate', name=_('Service Parameters')),
FieldSet('distance', 'distance_unit', name=_('Attributes')),
FieldSet('tenant', name=_('Tenancy')),
)
nullable_fields = (
@ -184,15 +208,18 @@ class CircuitTerminationBulkEditForm(NetBoxModelBulkEditForm):
max_length=200,
required=False
)
site = DynamicModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(),
required=False
termination_type = ContentTypeChoiceField(
queryset=ContentType.objects.filter(model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES),
widget=HTMXSelect(method='post', attrs={'hx-select': '#form_fields'}),
required=False,
label=_('Termination type')
)
provider_network = DynamicModelChoiceField(
label=_('Provider Network'),
queryset=ProviderNetwork.objects.all(),
required=False
termination = DynamicModelChoiceField(
label=_('Termination'),
queryset=Site.objects.none(), # Initial queryset
required=False,
disabled=True,
selector=True
)
port_speed = forms.IntegerField(
required=False,
@ -212,15 +239,26 @@ class CircuitTerminationBulkEditForm(NetBoxModelBulkEditForm):
fieldsets = (
FieldSet(
'description',
TabbedGroups(
FieldSet('site', name=_('Site')),
FieldSet('provider_network', name=_('Provider Network')),
),
'termination_type', 'termination',
'mark_connected', name=_('Circuit Termination')
),
FieldSet('port_speed', 'upstream_speed', name=_('Termination Details')),
)
nullable_fields = ('description')
nullable_fields = ('description', 'termination')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if termination_type_id := get_field_value(self, 'termination_type'):
try:
termination_type = ContentType.objects.get(pk=termination_type_id)
model = termination_type.model_class()
self.fields['termination'].queryset = model.objects.all()
self.fields['termination'].widget.attrs['selector'] = model._meta.label_lower
self.fields['termination'].disabled = False
self.fields['termination'].label = _(bettertitle(model._meta.verbose_name))
except ObjectDoesNotExist:
pass
class CircuitGroupBulkEditForm(NetBoxModelBulkEditForm):
@ -242,7 +280,7 @@ class CircuitGroupBulkEditForm(NetBoxModelBulkEditForm):
class CircuitGroupAssignmentBulkEditForm(NetBoxModelBulkEditForm):
circuit = DynamicModelChoiceField(
member = DynamicModelChoiceField(
label=_('Circuit'),
queryset=Circuit.objects.all(),
required=False
@ -255,6 +293,88 @@ class CircuitGroupAssignmentBulkEditForm(NetBoxModelBulkEditForm):
model = CircuitGroupAssignment
fieldsets = (
FieldSet('circuit', 'priority'),
FieldSet('member', 'priority'),
)
nullable_fields = ('priority',)
class VirtualCircuitTypeBulkEditForm(NetBoxModelBulkEditForm):
color = ColorField(
label=_('Color'),
required=False
)
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
model = VirtualCircuitType
fieldsets = (
FieldSet('color', 'description'),
)
nullable_fields = ('color', 'description')
class VirtualCircuitBulkEditForm(NetBoxModelBulkEditForm):
provider_network = DynamicModelChoiceField(
label=_('Provider network'),
queryset=ProviderNetwork.objects.all(),
required=False
)
provider_account = DynamicModelChoiceField(
label=_('Provider account'),
queryset=ProviderAccount.objects.all(),
required=False
)
type = DynamicModelChoiceField(
label=_('Type'),
queryset=VirtualCircuitType.objects.all(),
required=False
)
status = forms.ChoiceField(
label=_('Status'),
choices=add_blank_choice(CircuitStatusChoices),
required=False,
initial=''
)
tenant = DynamicModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(),
required=False
)
description = forms.CharField(
label=_('Description'),
max_length=100,
required=False
)
comments = CommentField()
model = VirtualCircuit
fieldsets = (
FieldSet('provider_network', 'provider_account', 'status', 'description', name=_('Virtual circuit')),
FieldSet('tenant', name=_('Tenancy')),
)
nullable_fields = (
'provider_account', 'tenant', 'description', 'comments',
)
class VirtualCircuitTerminationBulkEditForm(NetBoxModelBulkEditForm):
role = forms.ChoiceField(
label=_('Role'),
choices=add_blank_choice(VirtualCircuitTerminationRoleChoices),
required=False,
initial=''
)
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
model = VirtualCircuitTermination
fieldsets = (
FieldSet('role', 'description'),
)
nullable_fields = ('description',)

View File

@ -1,12 +1,15 @@
from django import forms
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _
from circuits.choices import *
from circuits.constants import *
from circuits.models import *
from dcim.models import Site
from dcim.models import Interface
from netbox.choices import DistanceUnitChoices
from netbox.forms import NetBoxModelImportForm
from tenancy.models import Tenant
from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, SlugField
from utilities.forms.fields import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, SlugField
__all__ = (
'CircuitImportForm',
@ -18,6 +21,10 @@ __all__ = (
'ProviderImportForm',
'ProviderAccountImportForm',
'ProviderNetworkImportForm',
'VirtualCircuitImportForm',
'VirtualCircuitTerminationImportForm',
'VirtualCircuitTerminationImportRelatedForm',
'VirtualCircuitTypeImportForm',
)
@ -94,6 +101,12 @@ class CircuitImportForm(NetBoxModelImportForm):
choices=CircuitStatusChoices,
help_text=_('Operational status')
)
distance_unit = CSVChoiceField(
label=_('Distance unit'),
choices=DistanceUnitChoices,
required=False,
help_text=_('Distance unit')
)
tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(),
@ -106,7 +119,7 @@ class CircuitImportForm(NetBoxModelImportForm):
model = Circuit
fields = [
'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date', 'termination_date',
'commit_rate', 'description', 'comments', 'tags'
'commit_rate', 'distance', 'distance_unit', 'description', 'comments', 'tags'
]
@ -120,17 +133,10 @@ class BaseCircuitTerminationImportForm(forms.ModelForm):
label=_('Termination'),
choices=CircuitTerminationSideChoices,
)
site = CSVModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(),
to_field_name='name',
required=False
)
provider_network = CSVModelChoiceField(
label=_('Provider network'),
queryset=ProviderNetwork.objects.all(),
to_field_name='name',
required=False
termination_type = CSVContentTypeField(
queryset=ContentType.objects.filter(model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES),
required=False,
label=_('Termination type (app & model)')
)
@ -138,9 +144,12 @@ class CircuitTerminationImportRelatedForm(BaseCircuitTerminationImportForm):
class Meta:
model = CircuitTermination
fields = [
'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
'circuit', 'term_side', 'termination_type', 'termination_id', 'port_speed', 'upstream_speed', 'xconnect_id',
'pp_info', 'description'
]
labels = {
'termination_id': _('Termination ID'),
}
class CircuitTerminationImportForm(NetBoxModelImportForm, BaseCircuitTerminationImportForm):
@ -148,9 +157,12 @@ class CircuitTerminationImportForm(NetBoxModelImportForm, BaseCircuitTermination
class Meta:
model = CircuitTermination
fields = [
'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
'circuit', 'term_side', 'termination_type', 'termination_id', 'port_speed', 'upstream_speed', 'xconnect_id',
'pp_info', 'description', 'tags'
]
labels = {
'termination_id': _('Termination ID'),
}
class CircuitGroupImportForm(NetBoxModelImportForm):
@ -168,7 +180,101 @@ class CircuitGroupImportForm(NetBoxModelImportForm):
class CircuitGroupAssignmentImportForm(NetBoxModelImportForm):
member_type = CSVContentTypeField(
queryset=ContentType.objects.filter(CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS),
label=_('Circuit type (app & model)')
)
priority = CSVChoiceField(
label=_('Priority'),
choices=CircuitPriorityChoices,
required=False
)
class Meta:
model = CircuitGroupAssignment
fields = ('circuit', 'group', 'priority')
fields = ('member_type', 'member_id', 'group', 'priority')
class VirtualCircuitTypeImportForm(NetBoxModelImportForm):
slug = SlugField()
class Meta:
model = VirtualCircuitType
fields = ('name', 'slug', 'color', 'description', 'tags')
class VirtualCircuitImportForm(NetBoxModelImportForm):
provider_network = CSVModelChoiceField(
label=_('Provider network'),
queryset=ProviderNetwork.objects.all(),
to_field_name='name',
help_text=_('The network to which this virtual circuit belongs')
)
provider_account = CSVModelChoiceField(
label=_('Provider account'),
queryset=ProviderAccount.objects.all(),
to_field_name='account',
help_text=_('Assigned provider account (if any)'),
required=False
)
type = CSVModelChoiceField(
label=_('Type'),
queryset=VirtualCircuitType.objects.all(),
to_field_name='name',
help_text=_('Type of virtual circuit')
)
status = CSVChoiceField(
label=_('Status'),
choices=CircuitStatusChoices,
help_text=_('Operational status')
)
tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(),
required=False,
to_field_name='name',
help_text=_('Assigned tenant')
)
class Meta:
model = VirtualCircuit
fields = [
'cid', 'provider_network', 'provider_account', 'type', 'status', 'tenant', 'description', 'comments',
'tags',
]
class BaseVirtualCircuitTerminationImportForm(forms.ModelForm):
virtual_circuit = CSVModelChoiceField(
label=_('Virtual circuit'),
queryset=VirtualCircuit.objects.all(),
to_field_name='cid',
)
role = CSVChoiceField(
label=_('Role'),
choices=VirtualCircuitTerminationRoleChoices,
help_text=_('Operational role')
)
interface = CSVModelChoiceField(
label=_('Interface'),
queryset=Interface.objects.all(),
to_field_name='pk',
)
class VirtualCircuitTerminationImportRelatedForm(BaseVirtualCircuitTerminationImportForm):
class Meta:
model = VirtualCircuitTermination
fields = [
'virtual_circuit', 'role', 'interface', 'description',
]
class VirtualCircuitTerminationImportForm(NetBoxModelImportForm, BaseVirtualCircuitTerminationImportForm):
class Meta:
model = VirtualCircuitTermination
fields = [
'virtual_circuit', 'role', 'interface', 'description', 'tags',
]

View File

@ -1,12 +1,17 @@
from django import forms
from django.utils.translation import gettext as _
from circuits.choices import CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices, CircuitTerminationSideChoices
from circuits.choices import (
CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices, CircuitTerminationSideChoices,
VirtualCircuitTerminationRoleChoices,
)
from circuits.models import *
from dcim.models import Region, Site, SiteGroup
from dcim.models import Location, Region, Site, SiteGroup
from ipam.models import ASN
from netbox.choices import DistanceUnitChoices
from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import TenancyFilterForm, ContactModelFilterForm
from utilities.forms import add_blank_choice
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import DatePicker, NumberWithOptions
@ -20,6 +25,9 @@ __all__ = (
'ProviderFilterForm',
'ProviderAccountFilterForm',
'ProviderNetworkFilterForm',
'VirtualCircuitFilterForm',
'VirtualCircuitTerminationFilterForm',
'VirtualCircuitTypeFilterForm',
)
@ -114,7 +122,10 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
FieldSet('type_id', 'status', 'install_date', 'termination_date', 'commit_rate', name=_('Attributes')),
FieldSet(
'type_id', 'status', 'install_date', 'termination_date', 'commit_rate', 'distance', 'distance_unit',
name=_('Attributes')
),
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
@ -188,6 +199,15 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
options=CircuitCommitRateChoices
)
)
distance = forms.DecimalField(
label=_('Distance'),
required=False,
)
distance_unit = forms.ChoiceField(
label=_('Distance unit'),
choices=add_blank_choice(DistanceUnitChoices),
required=False
)
tag = TagFilterField(model)
@ -196,18 +216,29 @@ class CircuitTerminationFilterForm(NetBoxModelFilterSetForm):
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('circuit_id', 'term_side', name=_('Circuit')),
FieldSet('provider_id', 'provider_network_id', name=_('Provider')),
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
FieldSet('provider_id', name=_('Provider')),
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Termination')),
)
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
required=False,
query_params={
'region_id': '$region_id',
'site_group_id': '$site_group_id',
},
label=_('Site')
)
location_id = DynamicModelMultipleChoiceField(
queryset=Location.objects.all(),
required=False,
label=_('Location')
)
circuit_id = DynamicModelMultipleChoiceField(
queryset=Circuit.objects.all(),
required=False,
@ -247,14 +278,14 @@ class CircuitGroupAssignmentFilterForm(NetBoxModelFilterSetForm):
model = CircuitGroupAssignment
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('provider_id', 'circuit_id', 'group_id', 'priority', name=_('Assignment')),
FieldSet('provider_id', 'member_id', 'group_id', 'priority', name=_('Assignment')),
)
provider_id = DynamicModelMultipleChoiceField(
queryset=Provider.objects.all(),
required=False,
label=_('Provider')
)
circuit_id = DynamicModelMultipleChoiceField(
member_id = DynamicModelMultipleChoiceField(
queryset=Circuit.objects.all(),
required=False,
label=_('Circuit')
@ -270,3 +301,93 @@ class CircuitGroupAssignmentFilterForm(NetBoxModelFilterSetForm):
required=False
)
tag = TagFilterField(model)
class VirtualCircuitTypeFilterForm(NetBoxModelFilterSetForm):
model = VirtualCircuitType
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('color', name=_('Attributes')),
)
tag = TagFilterField(model)
color = ColorField(
label=_('Color'),
required=False
)
class VirtualCircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
model = VirtualCircuit
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
FieldSet('type', 'status', name=_('Attributes')),
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
)
selector_fields = ('filter_id', 'q', 'provider_id', 'provider_network_id')
provider_id = DynamicModelMultipleChoiceField(
queryset=Provider.objects.all(),
required=False,
label=_('Provider')
)
provider_account_id = DynamicModelMultipleChoiceField(
queryset=ProviderAccount.objects.all(),
required=False,
query_params={
'provider_id': '$provider_id'
},
label=_('Provider account')
)
provider_network_id = DynamicModelMultipleChoiceField(
queryset=ProviderNetwork.objects.all(),
required=False,
query_params={
'provider_id': '$provider_id'
},
label=_('Provider network')
)
type_id = DynamicModelMultipleChoiceField(
queryset=VirtualCircuitType.objects.all(),
required=False,
label=_('Type')
)
status = forms.MultipleChoiceField(
label=_('Status'),
choices=CircuitStatusChoices,
required=False
)
tag = TagFilterField(model)
class VirtualCircuitTerminationFilterForm(NetBoxModelFilterSetForm):
model = VirtualCircuitTermination
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('virtual_circuit_id', 'role', name=_('Virtual circuit')),
FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
)
virtual_circuit_id = DynamicModelMultipleChoiceField(
queryset=VirtualCircuit.objects.all(),
required=False,
label=_('Virtual circuit')
)
role = forms.MultipleChoiceField(
label=_('Role'),
choices=VirtualCircuitTerminationRoleChoices,
required=False
)
provider_network_id = DynamicModelMultipleChoiceField(
queryset=ProviderNetwork.objects.all(),
required=False,
query_params={
'provider_id': '$provider_id'
},
label=_('Provider network')
)
provider_id = DynamicModelMultipleChoiceField(
queryset=Provider.objects.all(),
required=False,
label=_('Provider')
)
tag = TagFilterField(model)

View File

@ -1,14 +1,24 @@
from django import forms
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import gettext_lazy as _
from circuits.choices import CircuitCommitRateChoices, CircuitTerminationPortSpeedChoices
from circuits.choices import (
CircuitCommitRateChoices, CircuitTerminationPortSpeedChoices, VirtualCircuitTerminationRoleChoices,
)
from circuits.constants import *
from circuits.models import *
from dcim.models import Site
from dcim.models import Interface, Site
from ipam.models import ASN
from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm
from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
from utilities.forms.rendering import FieldSet, TabbedGroups
from utilities.forms.widgets import DatePicker, NumberWithOptions
from utilities.forms import get_field_value
from utilities.forms.fields import (
CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField,
)
from utilities.forms.rendering import FieldSet, InlineFields
from utilities.forms.widgets import DatePicker, HTMXSelect, NumberWithOptions
from utilities.templatetags.builtins.filters import bettertitle
__all__ = (
'CircuitForm',
@ -19,6 +29,9 @@ __all__ = (
'ProviderForm',
'ProviderAccountForm',
'ProviderNetworkForm',
'VirtualCircuitForm',
'VirtualCircuitTerminationForm',
'VirtualCircuitTypeForm',
)
@ -45,7 +58,9 @@ class ProviderForm(NetBoxModelForm):
class ProviderAccountForm(NetBoxModelForm):
provider = DynamicModelChoiceField(
label=_('Provider'),
queryset=Provider.objects.all()
queryset=Provider.objects.all(),
selector=True,
quick_add=True
)
comments = CommentField()
@ -59,7 +74,9 @@ class ProviderAccountForm(NetBoxModelForm):
class ProviderNetworkForm(NetBoxModelForm):
provider = DynamicModelChoiceField(
label=_('Provider'),
queryset=Provider.objects.all()
queryset=Provider.objects.all(),
selector=True,
quick_add=True
)
comments = CommentField()
@ -92,7 +109,8 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
provider = DynamicModelChoiceField(
label=_('Provider'),
queryset=Provider.objects.all(),
selector=True
selector=True,
quick_add=True
)
provider_account = DynamicModelChoiceField(
label=_('Provider account'),
@ -103,12 +121,23 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
}
)
type = DynamicModelChoiceField(
queryset=CircuitType.objects.all()
queryset=CircuitType.objects.all(),
quick_add=True
)
comments = CommentField()
fieldsets = (
FieldSet('provider', 'provider_account', 'cid', 'type', 'status', 'description', 'tags', name=_('Circuit')),
FieldSet(
'provider',
'provider_account',
'cid',
'type',
'status',
InlineFields('distance', 'distance_unit', label=_('Distance')),
'description',
'tags',
name=_('Circuit')
),
FieldSet('install_date', 'termination_date', 'commit_rate', name=_('Service Parameters')),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
)
@ -117,7 +146,7 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
model = Circuit
fields = [
'cid', 'type', 'provider', 'provider_account', 'status', 'install_date', 'termination_date', 'commit_rate',
'description', 'tenant_group', 'tenant', 'comments', 'tags',
'distance', 'distance_unit', 'description', 'tenant_group', 'tenant', 'comments', 'tags',
]
widgets = {
'install_date': DatePicker(),
@ -134,26 +163,24 @@ class CircuitTerminationForm(NetBoxModelForm):
queryset=Circuit.objects.all(),
selector=True
)
site = DynamicModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(),
termination_type = ContentTypeChoiceField(
queryset=ContentType.objects.filter(model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES),
widget=HTMXSelect(),
required=False,
selector=True
label=_('Termination type')
)
provider_network = DynamicModelChoiceField(
label=_('Provider network'),
queryset=ProviderNetwork.objects.all(),
termination = DynamicModelChoiceField(
label=_('Termination'),
queryset=Site.objects.none(), # Initial queryset
required=False,
disabled=True,
selector=True
)
fieldsets = (
FieldSet(
'circuit', 'term_side', 'description', 'tags',
TabbedGroups(
FieldSet('site', name=_('Site')),
FieldSet('provider_network', name=_('Provider Network')),
),
'termination_type', 'termination',
'mark_connected', name=_('Circuit Termination')
),
FieldSet('port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', name=_('Termination Details')),
@ -162,7 +189,7 @@ class CircuitTerminationForm(NetBoxModelForm):
class Meta:
model = CircuitTermination
fields = [
'circuit', 'term_side', 'site', 'provider_network', 'mark_connected', 'port_speed', 'upstream_speed',
'circuit', 'term_side', 'termination_type', 'mark_connected', 'port_speed', 'upstream_speed',
'xconnect_id', 'pp_info', 'description', 'tags',
]
widgets = {
@ -174,6 +201,36 @@ class CircuitTerminationForm(NetBoxModelForm):
),
}
def __init__(self, *args, **kwargs):
instance = kwargs.get('instance')
initial = kwargs.get('initial', {})
if instance is not None and instance.termination:
initial['termination'] = instance.termination
kwargs['initial'] = initial
super().__init__(*args, **kwargs)
if termination_type_id := get_field_value(self, 'termination_type'):
try:
termination_type = ContentType.objects.get(pk=termination_type_id)
model = termination_type.model_class()
self.fields['termination'].queryset = model.objects.all()
self.fields['termination'].widget.attrs['selector'] = model._meta.label_lower
self.fields['termination'].disabled = False
self.fields['termination'].label = _(bettertitle(model._meta.verbose_name))
except ObjectDoesNotExist:
pass
if self.instance and termination_type_id != self.instance.termination_type_id:
self.initial['termination'] = None
def clean(self):
super().clean()
# Assign the selected termination (if any)
self.instance.termination = self.cleaned_data.get('termination')
class CircuitGroupForm(TenancyForm, NetBoxModelForm):
slug = SlugField()
@ -195,14 +252,137 @@ class CircuitGroupAssignmentForm(NetBoxModelForm):
label=_('Group'),
queryset=CircuitGroup.objects.all(),
)
circuit = DynamicModelChoiceField(
member_type = ContentTypeChoiceField(
queryset=ContentType.objects.filter(CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS),
widget=HTMXSelect(),
required=False,
label=_('Circuit type')
)
member = DynamicModelChoiceField(
label=_('Circuit'),
queryset=Circuit.objects.all(),
queryset=Circuit.objects.none(), # Initial queryset
required=False,
disabled=True,
selector=True
)
fieldsets = (
FieldSet('group', 'member_type', 'member', 'priority', 'tags', name=_('Group Assignment')),
)
class Meta:
model = CircuitGroupAssignment
fields = [
'group', 'circuit', 'priority', 'tags',
'group', 'member_type', 'priority', 'tags',
]
def __init__(self, *args, **kwargs):
instance = kwargs.get('instance')
initial = kwargs.get('initial', {})
if instance is not None and instance.member:
initial['member'] = instance.member
kwargs['initial'] = initial
super().__init__(*args, **kwargs)
if member_type_id := get_field_value(self, 'member_type'):
try:
model = ContentType.objects.get(pk=member_type_id).model_class()
self.fields['member'].queryset = model.objects.all()
self.fields['member'].widget.attrs['selector'] = model._meta.label_lower
self.fields['member'].disabled = False
self.fields['member'].label = _(bettertitle(model._meta.verbose_name))
except ObjectDoesNotExist:
pass
if self.instance.pk and member_type_id != self.instance.member_type_id:
self.initial['member'] = None
def clean(self):
super().clean()
# Assign the selected circuit (if any)
self.instance.member = self.cleaned_data.get('member')
class VirtualCircuitTypeForm(NetBoxModelForm):
slug = SlugField()
fieldsets = (
FieldSet('name', 'slug', 'color', 'description', 'tags'),
)
class Meta:
model = VirtualCircuitType
fields = [
'name', 'slug', 'color', 'description', 'tags',
]
class VirtualCircuitForm(TenancyForm, NetBoxModelForm):
provider_network = DynamicModelChoiceField(
label=_('Provider network'),
queryset=ProviderNetwork.objects.all(),
selector=True
)
provider_account = DynamicModelChoiceField(
label=_('Provider account'),
queryset=ProviderAccount.objects.all(),
required=False
)
type = DynamicModelChoiceField(
queryset=VirtualCircuitType.objects.all(),
quick_add=True
)
comments = CommentField()
fieldsets = (
FieldSet(
'provider_network', 'provider_account', 'cid', 'type', 'status', 'description', 'tags',
name=_('Virtual circuit'),
),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
)
class Meta:
model = VirtualCircuit
fields = [
'cid', 'provider_network', 'provider_account', 'type', 'status', 'description', 'tenant_group', 'tenant',
'comments', 'tags',
]
class VirtualCircuitTerminationForm(NetBoxModelForm):
virtual_circuit = DynamicModelChoiceField(
label=_('Virtual circuit'),
queryset=VirtualCircuit.objects.all(),
selector=True
)
role = forms.ChoiceField(
choices=VirtualCircuitTerminationRoleChoices,
widget=HTMXSelect(),
label=_('Role')
)
interface = DynamicModelChoiceField(
label=_('Interface'),
queryset=Interface.objects.all(),
selector=True,
query_params={
'kind': 'virtual',
'virtual_circuit_termination_id': 'null',
},
context={
'parent': 'device',
}
)
fieldsets = (
FieldSet('virtual_circuit', 'role', 'interface', 'description', 'tags'),
)
class Meta:
model = VirtualCircuitTermination
fields = [
'virtual_circuit', 'role', 'interface', 'description', 'tags',
]

View File

@ -4,14 +4,17 @@ from circuits import filtersets, models
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
__all__ = (
'CircuitTerminationFilter',
'CircuitFilter',
'CircuitGroupAssignmentFilter',
'CircuitGroupFilter',
'CircuitTerminationFilter',
'CircuitTypeFilter',
'ProviderFilter',
'ProviderAccountFilter',
'ProviderNetworkFilter',
'VirtualCircuitFilter',
'VirtualCircuitTerminationFilter',
'VirtualCircuitTypeFilter',
)
@ -61,3 +64,21 @@ class ProviderAccountFilter(BaseFilterMixin):
@autotype_decorator(filtersets.ProviderNetworkFilterSet)
class ProviderNetworkFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.VirtualCircuitType, lookups=True)
@autotype_decorator(filtersets.VirtualCircuitTypeFilterSet)
class VirtualCircuitTypeFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.VirtualCircuit, lookups=True)
@autotype_decorator(filtersets.VirtualCircuitFilterSet)
class VirtualCircuitFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.VirtualCircuitTermination, lookups=True)
@autotype_decorator(filtersets.VirtualCircuitTerminationFilterSet)
class VirtualCircuitTerminationFilter(BaseFilterMixin):
pass

View File

@ -31,3 +31,12 @@ class CircuitsQuery:
provider_network: ProviderNetworkType = strawberry_django.field()
provider_network_list: List[ProviderNetworkType] = strawberry_django.field()
virtual_circuit: VirtualCircuitType = strawberry_django.field()
virtual_circuit_list: List[VirtualCircuitType] = strawberry_django.field()
virtual_circuit_termination: VirtualCircuitTerminationType = strawberry_django.field()
virtual_circuit_termination_list: List[VirtualCircuitTerminationType] = strawberry_django.field()
virtual_circuit_type: VirtualCircuitTypeType = strawberry_django.field()
virtual_circuit_type_list: List[VirtualCircuitTypeType] = strawberry_django.field()

View File

@ -1,4 +1,4 @@
from typing import Annotated, List
from typing import Annotated, List, Union
import strawberry
import strawberry_django
@ -19,6 +19,9 @@ __all__ = (
'ProviderType',
'ProviderAccountType',
'ProviderNetworkType',
'VirtualCircuitTerminationType',
'VirtualCircuitType',
'VirtualCircuitTypeType',
)
@ -59,13 +62,21 @@ class ProviderNetworkType(NetBoxObjectType):
@strawberry_django.type(
models.CircuitTermination,
fields='__all__',
exclude=('termination_type', 'termination_id', '_location', '_region', '_site', '_site_group', '_provider_network'),
filters=CircuitTerminationFilter
)
class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, ObjectType):
circuit: Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]
provider_network: Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')] | None
site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')] | None
@strawberry_django.field
def termination(self) -> Annotated[Union[
Annotated["LocationType", strawberry.lazy('dcim.graphql.types')],
Annotated["RegionType", strawberry.lazy('dcim.graphql.types')],
Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')],
Annotated["SiteType", strawberry.lazy('dcim.graphql.types')],
Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')],
], strawberry.union("CircuitTerminationTerminationType")] | None:
return self.termination
@strawberry_django.type(
@ -106,9 +117,58 @@ class CircuitGroupType(OrganizationalObjectType):
@strawberry_django.type(
models.CircuitGroupAssignment,
fields='__all__',
exclude=('member_type', 'member_id'),
filters=CircuitGroupAssignmentFilter
)
class CircuitGroupAssignmentType(TagsMixin, BaseObjectType):
group: Annotated["CircuitGroupType", strawberry.lazy('circuits.graphql.types')]
circuit: Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]
@strawberry_django.field
def member(self) -> Annotated[Union[
Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')],
Annotated["VirtualCircuitType", strawberry.lazy('circuits.graphql.types')],
], strawberry.union("CircuitGroupAssignmentMemberType")] | None:
return self.member
@strawberry_django.type(
models.VirtualCircuitType,
fields='__all__',
filters=VirtualCircuitTypeFilter
)
class VirtualCircuitTypeType(OrganizationalObjectType):
color: str
virtual_circuits: List[Annotated["VirtualCircuitType", strawberry.lazy('circuits.graphql.types')]]
@strawberry_django.type(
models.VirtualCircuitTermination,
fields='__all__',
filters=VirtualCircuitTerminationFilter
)
class VirtualCircuitTerminationType(CustomFieldsMixin, TagsMixin, ObjectType):
virtual_circuit: Annotated[
"VirtualCircuitType",
strawberry.lazy('circuits.graphql.types')
] = strawberry_django.field(select_related=["virtual_circuit"])
interface: Annotated[
"InterfaceType",
strawberry.lazy('dcim.graphql.types')
] = strawberry_django.field(select_related=["interface"])
@strawberry_django.type(
models.VirtualCircuit,
fields='__all__',
filters=VirtualCircuitFilter
)
class VirtualCircuitType(NetBoxObjectType):
provider_network: ProviderNetworkType = strawberry_django.field(select_related=["provider_network"])
provider_account: ProviderAccountType | None
type: Annotated["VirtualCircuitTypeType", strawberry.lazy('circuits.graphql.types')] = strawberry_django.field(
select_related=["type"]
)
tenant: TenantType | None
terminations: List[VirtualCircuitTerminationType]

View File

@ -5,11 +5,9 @@ import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
dependencies = []
replaces = [
('circuits', '0001_initial'),
@ -98,7 +96,12 @@ class Migration(migrations.Migration):
('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')),
(
'provider',
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, related_name='networks', to='circuits.provider'
),
),
],
options={
'ordering': ('provider', 'name'),

View File

@ -4,7 +4,6 @@ import taggit.managers
class Migration(migrations.Migration):
dependencies = [
('dcim', '0001_initial'),
('contenttypes', '0002_remove_content_type_name'),
@ -58,32 +57,56 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='circuittermination',
name='_cable_peer_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='contenttypes.contenttype'),
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='+',
to='contenttypes.contenttype',
),
),
migrations.AddField(
model_name='circuittermination',
name='cable',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.cable'),
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.cable'
),
),
migrations.AddField(
model_name='circuittermination',
name='circuit',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='terminations', to='circuits.circuit'),
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, related_name='terminations', to='circuits.circuit'
),
),
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'),
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name='circuit_terminations',
to='circuits.providernetwork',
),
),
migrations.AddField(
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'),
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name='circuit_terminations',
to='dcim.site',
),
),
migrations.AddField(
model_name='circuit',
name='provider',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.provider'),
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.provider'
),
),
migrations.AddField(
model_name='circuit',
@ -93,26 +116,50 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='circuit',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='tenancy.tenant'),
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name='circuits',
to='tenancy.tenant',
),
),
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'),
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'),
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='type',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.circuittype'),
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.circuittype'
),
),
migrations.AddConstraint(
model_name='providernetwork',
constraint=models.UniqueConstraint(fields=('provider', 'name'), name='circuits_providernetwork_provider_name'),
constraint=models.UniqueConstraint(
fields=('provider', 'name'), name='circuits_providernetwork_provider_name'
),
),
migrations.AlterUniqueTogether(
name='providernetwork',

View File

@ -5,7 +5,6 @@ import utilities.json
class Migration(migrations.Migration):
replaces = [
('circuits', '0003_extend_tag_support'),
('circuits', '0004_rename_cable_peer'),
@ -14,7 +13,7 @@ class Migration(migrations.Migration):
('circuits', '0034_created_datetimefield'),
('circuits', '0035_provider_asns'),
('circuits', '0036_circuit_termination_date_tags_custom_fields'),
('circuits', '0037_new_cabling_models')
('circuits', '0037_new_cabling_models'),
]
dependencies = [

View File

@ -6,13 +6,12 @@ import utilities.json
class Migration(migrations.Migration):
replaces = [
('circuits', '0038_cabling_cleanup'),
('circuits', '0039_unique_constraints'),
('circuits', '0040_provider_remove_deprecated_fields'),
('circuits', '0041_standardize_description_comments'),
('circuits', '0042_provideraccount')
('circuits', '0042_provideraccount'),
]
dependencies = [
@ -51,11 +50,15 @@ class Migration(migrations.Migration):
),
migrations.AddConstraint(
model_name='circuittermination',
constraint=models.UniqueConstraint(fields=('circuit', 'term_side'), name='circuits_circuittermination_unique_circuit_term_side'),
constraint=models.UniqueConstraint(
fields=('circuit', 'term_side'), name='circuits_circuittermination_unique_circuit_term_side'
),
),
migrations.AddConstraint(
model_name='providernetwork',
constraint=models.UniqueConstraint(fields=('provider', 'name'), name='circuits_providernetwork_unique_provider_name'),
constraint=models.UniqueConstraint(
fields=('provider', 'name'), name='circuits_providernetwork_unique_provider_name'
),
),
migrations.RemoveField(
model_name='provider',
@ -84,12 +87,20 @@ class Migration(migrations.Migration):
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
(
'custom_field_data',
models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
),
('description', models.CharField(blank=True, max_length=200)),
('comments', models.TextField(blank=True)),
('account', models.CharField(max_length=100)),
('name', models.CharField(blank=True, max_length=100)),
('provider', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='accounts', to='circuits.provider')),
(
'provider',
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, related_name='accounts', to='circuits.provider'
),
),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
@ -98,11 +109,17 @@ class Migration(migrations.Migration):
),
migrations.AddConstraint(
model_name='provideraccount',
constraint=models.UniqueConstraint(condition=models.Q(('name', ''), _negated=True), fields=('provider', 'name'), name='circuits_provideraccount_unique_provider_name'),
constraint=models.UniqueConstraint(
condition=models.Q(('name', ''), _negated=True),
fields=('provider', 'name'),
name='circuits_provideraccount_unique_provider_name',
),
),
migrations.AddConstraint(
model_name='provideraccount',
constraint=models.UniqueConstraint(fields=('provider', 'account'), name='circuits_provideraccount_unique_provider_account'),
constraint=models.UniqueConstraint(
fields=('provider', 'account'), name='circuits_provideraccount_unique_provider_account'
),
),
migrations.RemoveField(
model_name='provider',
@ -111,7 +128,13 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='circuit',
name='provider_account',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.provideraccount'),
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name='circuits',
to='circuits.provideraccount',
),
preserve_default=False,
),
migrations.AlterModelOptions(
@ -120,6 +143,8 @@ class Migration(migrations.Migration):
),
migrations.AddConstraint(
model_name='circuit',
constraint=models.UniqueConstraint(fields=('provider_account', 'cid'), name='circuits_circuit_unique_provideraccount_cid'),
constraint=models.UniqueConstraint(
fields=('provider_account', 'cid'), name='circuits_circuit_unique_provideraccount_cid'
),
),
]

View File

@ -5,7 +5,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0043_circuittype_color'),
('extras', '0119_notifications'),

View File

@ -0,0 +1,27 @@
# Generated by Django 5.0.9 on 2024-09-26 22:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0044_circuit_groups'),
]
operations = [
migrations.AddField(
model_name='circuit',
name='_abs_distance',
field=models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True),
),
migrations.AddField(
model_name='circuit',
name='distance',
field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True),
),
migrations.AddField(
model_name='circuit',
name='distance_unit',
field=models.CharField(blank=True, max_length=50),
),
]

View File

@ -0,0 +1,39 @@
from django.db import migrations, models
def set_null_values(apps, schema_editor):
"""
Replace empty strings with null values.
"""
Circuit = apps.get_model('circuits', 'Circuit')
CircuitGroupAssignment = apps.get_model('circuits', 'CircuitGroupAssignment')
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
Circuit.objects.filter(distance_unit='').update(distance_unit=None)
CircuitGroupAssignment.objects.filter(priority='').update(priority=None)
CircuitTermination.objects.filter(cable_end='').update(cable_end=None)
class Migration(migrations.Migration):
dependencies = [
('circuits', '0045_circuit_distance'),
]
operations = [
migrations.AlterField(
model_name='circuit',
name='distance_unit',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='circuitgroupassignment',
name='priority',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='circuittermination',
name='cable_end',
field=models.CharField(blank=True, max_length=1, null=True),
),
migrations.RunPython(code=set_null_values, reverse_code=migrations.RunPython.noop),
]

View File

@ -0,0 +1,53 @@
import django.db.models.deletion
from django.db import migrations, models
def copy_site_assignments(apps, schema_editor):
"""
Copy site ForeignKey values to the Termination GFK.
"""
ContentType = apps.get_model('contenttypes', 'ContentType')
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
Site = apps.get_model('dcim', 'Site')
CircuitTermination.objects.filter(site__isnull=False).update(
termination_type=ContentType.objects.get_for_model(Site), termination_id=models.F('site_id')
)
ProviderNetwork = apps.get_model('circuits', 'ProviderNetwork')
CircuitTermination.objects.filter(provider_network__isnull=False).update(
termination_type=ContentType.objects.get_for_model(ProviderNetwork),
termination_id=models.F('provider_network_id'),
)
class Migration(migrations.Migration):
dependencies = [
('circuits', '0046_charfield_null_choices'),
('contenttypes', '0002_remove_content_type_name'),
('dcim', '0193_poweroutlet_color'),
]
operations = [
migrations.AddField(
model_name='circuittermination',
name='termination_id',
field=models.PositiveBigIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='circuittermination',
name='termination_type',
field=models.ForeignKey(
blank=True,
limit_choices_to=models.Q(
('model__in', ('region', 'sitegroup', 'site', 'location', 'providernetwork'))
),
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name='+',
to='contenttypes.contenttype',
),
),
# Copy over existing site assignments
migrations.RunPython(code=copy_site_assignments, reverse_code=migrations.RunPython.noop),
]

View File

@ -0,0 +1,84 @@
# Generated by Django 5.0.9 on 2024-10-21 17:34
import django.db.models.deletion
from django.db import migrations, models
def populate_denormalized_fields(apps, schema_editor):
"""
Copy site ForeignKey values to the Termination GFK.
"""
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
terminations = CircuitTermination.objects.filter(site__isnull=False).prefetch_related('site')
for termination in terminations:
termination._region_id = termination.site.region_id
termination._site_group_id = termination.site.group_id
termination._site_id = termination.site_id
# Note: Location cannot be set prior to migration
CircuitTermination.objects.bulk_update(terminations, ['_region', '_site_group', '_site'])
class Migration(migrations.Migration):
dependencies = [
('circuits', '0047_circuittermination__termination'),
]
operations = [
migrations.AddField(
model_name='circuittermination',
name='_location',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='circuit_terminations',
to='dcim.location',
),
),
migrations.AddField(
model_name='circuittermination',
name='_region',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='circuit_terminations',
to='dcim.region',
),
),
migrations.AddField(
model_name='circuittermination',
name='_site',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='circuit_terminations',
to='dcim.site',
),
),
migrations.AddField(
model_name='circuittermination',
name='_site_group',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='circuit_terminations',
to='dcim.sitegroup',
),
),
# Populate denormalized FK values
migrations.RunPython(code=populate_denormalized_fields, reverse_code=migrations.RunPython.noop),
# Delete the site ForeignKey
migrations.RemoveField(
model_name='circuittermination',
name='site',
),
migrations.RenameField(
model_name='circuittermination',
old_name='provider_network',
new_name='_provider_network',
),
]

View File

@ -0,0 +1,21 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0048_circuitterminations_cached_relations'),
('dcim', '0197_natural_sort_collation'),
]
operations = [
migrations.AlterField(
model_name='provider',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
),
migrations.AlterField(
model_name='providernetwork',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=100),
),
]

View File

@ -0,0 +1,147 @@
import django.db.models.deletion
import taggit.managers
from django.db import migrations, models
import utilities.fields
import utilities.json
class Migration(migrations.Migration):
dependencies = [
('circuits', '0049_natural_ordering'),
('dcim', '0196_qinq_svlan'),
('extras', '0122_charfield_null_choices'),
('tenancy', '0016_charfield_null_choices'),
]
operations = [
migrations.CreateModel(
name='VirtualCircuitType',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('custom_field_data', models.JSONField(
blank=True,
default=dict,
encoder=utilities.json.CustomFieldJSONEncoder
)),
('name', models.CharField(max_length=100, unique=True)),
('slug', models.SlugField(max_length=100, unique=True)),
('description', models.CharField(blank=True, max_length=200)),
('color', utilities.fields.ColorField(blank=True, max_length=6)),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
'verbose_name': 'virtual circuit type',
'verbose_name_plural': 'virtual circuit types',
'ordering': ('name',),
},
),
migrations.CreateModel(
name='VirtualCircuit',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
(
'custom_field_data',
models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
),
('description', models.CharField(blank=True, max_length=200)),
('comments', models.TextField(blank=True)),
('cid', models.CharField(max_length=100)),
('status', models.CharField(default='active', max_length=50)),
(
'provider_account',
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name='virtual_circuits',
to='circuits.provideraccount',
),
),
(
'provider_network',
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name='virtual_circuits',
to='circuits.providernetwork',
),
),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
(
'type',
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name='virtual_circuits',
to='circuits.virtualcircuittype'
)
),
(
'tenant',
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name='virtual_circuits',
to='tenancy.tenant',
),
),
],
options={
'verbose_name': 'circuit',
'verbose_name_plural': 'circuits',
'ordering': ['provider_network', 'provider_account', 'cid'],
},
),
migrations.CreateModel(
name='VirtualCircuitTermination',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
(
'custom_field_data',
models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
),
('role', models.CharField(default='peer', max_length=50)),
('description', models.CharField(blank=True, max_length=200)),
(
'interface',
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name='virtual_circuit_termination',
to='dcim.interface',
),
),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
(
'virtual_circuit',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='terminations',
to='circuits.virtualcircuit',
),
),
],
options={
'verbose_name': 'virtual circuit termination',
'verbose_name_plural': 'virtual circuit terminations',
'ordering': ['virtual_circuit', 'role', 'pk'],
},
),
migrations.AddConstraint(
model_name='virtualcircuit',
constraint=models.UniqueConstraint(
fields=('provider_network', 'cid'), name='circuits_virtualcircuit_unique_provider_network_cid'
),
),
migrations.AddConstraint(
model_name='virtualcircuit',
constraint=models.UniqueConstraint(
fields=('provider_account', 'cid'), name='circuits_virtualcircuit_unique_provideraccount_cid'
),
),
]

View File

@ -0,0 +1,85 @@
import django.db.models.deletion
from django.db import migrations, models
def set_member_type(apps, schema_editor):
"""
Set member_type on any existing CircuitGroupAssignments to the content type for Circuit.
"""
ContentType = apps.get_model('contenttypes', 'ContentType')
Circuit = apps.get_model('circuits', 'Circuit')
CircuitGroupAssignment = apps.get_model('circuits', 'CircuitGroupAssignment')
CircuitGroupAssignment.objects.update(
member_type=ContentType.objects.get_for_model(Circuit)
)
class Migration(migrations.Migration):
dependencies = [
('circuits', '0050_virtual_circuits'),
('contenttypes', '0002_remove_content_type_name'),
('extras', '0122_charfield_null_choices'),
]
operations = [
migrations.RemoveConstraint(
model_name='circuitgroupassignment',
name='circuits_circuitgroupassignment_unique_circuit_group',
),
migrations.AlterModelOptions(
name='circuitgroupassignment',
options={'ordering': ('group', 'member_type', 'member_id', 'priority', 'pk')},
),
# Change member_id to an integer field for the member GFK
migrations.RenameField(
model_name='circuitgroupassignment',
old_name='circuit',
new_name='member_id',
),
migrations.AlterField(
model_name='circuitgroupassignment',
name='member_id',
field=models.PositiveBigIntegerField(),
),
# Add content type pointer for the member GFK
migrations.AddField(
model_name='circuitgroupassignment',
name='member_type',
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
limit_choices_to=models.Q(('app_label', 'circuits'), ('model__in', ['circuit', 'virtualcircuit'])),
related_name='+',
to='contenttypes.contenttype',
blank=True,
null=True
),
preserve_default=False,
),
# Populate member_type for any existing assignments
migrations.RunPython(code=set_member_type, reverse_code=migrations.RunPython.noop),
# Disallow null values for member_type
migrations.AlterField(
model_name='circuitgroupassignment',
name='member_type',
field=models.ForeignKey(
limit_choices_to=models.Q(('app_label', 'circuits'), ('model__in', ['circuit', 'virtualcircuit'])),
on_delete=django.db.models.deletion.PROTECT,
related_name='+',
to='contenttypes.contenttype'
),
),
migrations.AddConstraint(
model_name='circuitgroupassignment',
constraint=models.UniqueConstraint(
fields=('member_type', 'member_id', 'group'),
name='circuits_circuitgroupassignment_unique_member_group'
),
),
]

View File

@ -1,2 +1,3 @@
from .circuits import *
from .providers import *
from .virtual_circuits import *

View File

@ -0,0 +1,23 @@
from django.utils.translation import gettext_lazy as _
from netbox.models import OrganizationalModel
from utilities.fields import ColorField
__all__ = (
'BaseCircuitType',
)
class BaseCircuitType(OrganizationalModel):
"""
Abstract base model to represent a type of physical or virtual circuit.
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".
"""
color = ColorField(
verbose_name=_('color'),
blank=True
)
class Meta:
abstract = True

View File

@ -1,13 +1,19 @@
from django.apps import apps
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from circuits.choices import *
from circuits.constants import *
from dcim.models import CabledObjectModel
from netbox.models import ChangeLoggedModel, OrganizationalModel, PrimaryModel
from netbox.models.features import ContactsMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, ImageAttachmentsMixin, TagsMixin
from utilities.fields import ColorField
from netbox.models.mixins import DistanceMixin
from netbox.models.features import (
ContactsMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, ImageAttachmentsMixin, TagsMixin,
)
from .base import BaseCircuitType
__all__ = (
'Circuit',
@ -18,26 +24,18 @@ __all__ = (
)
class CircuitType(OrganizationalModel):
class CircuitType(BaseCircuitType):
"""
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".
"""
color = ColorField(
verbose_name=_('color'),
blank=True
)
def get_absolute_url(self):
return reverse('circuits:circuittype', args=[self.pk])
class Meta:
ordering = ('name',)
verbose_name = _('circuit type')
verbose_name_plural = _('circuit types')
class Circuit(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
class Circuit(ContactsMixin, ImageAttachmentsMixin, DistanceMixin, 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, and may optionally be assigned to a particular
@ -61,7 +59,7 @@ class Circuit(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
null=True
)
type = models.ForeignKey(
to='CircuitType',
to='circuits.CircuitType',
on_delete=models.PROTECT,
related_name='circuits'
)
@ -113,6 +111,13 @@ class Circuit(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
null=True
)
group_assignments = GenericRelation(
to='circuits.CircuitGroupAssignment',
content_type_field='member_type',
object_id_field='member_id',
related_query_name='circuit'
)
clone_fields = (
'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate',
'description',
@ -140,9 +145,6 @@ class Circuit(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
def __str__(self):
return self.cid
def get_absolute_url(self):
return reverse('circuits:circuit', args=[self.pk])
def get_status_color(self):
return CircuitStatusChoices.colors.get(self.status)
@ -173,21 +175,26 @@ class CircuitGroup(OrganizationalModel):
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('circuits:circuitgroup', args=[self.pk])
class CircuitGroupAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):
"""
Assignment of a Circuit to a CircuitGroup with an optional priority.
Assignment of a physical or virtual circuit to a CircuitGroup with an optional priority.
"""
circuit = models.ForeignKey(
Circuit,
on_delete=models.CASCADE,
related_name='assignments'
member_type = models.ForeignKey(
to='contenttypes.ContentType',
limit_choices_to=CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS,
on_delete=models.PROTECT,
related_name='+'
)
member_id = models.PositiveBigIntegerField(
verbose_name=_('member ID')
)
member = GenericForeignKey(
ct_field='member_type',
fk_field='member_id'
)
group = models.ForeignKey(
CircuitGroup,
to='circuits.CircuitGroup',
on_delete=models.CASCADE,
related_name='assignments'
)
@ -195,19 +202,19 @@ class CircuitGroupAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin,
verbose_name=_('priority'),
max_length=50,
choices=CircuitPriorityChoices,
blank=True
blank=True,
null=True
)
prerequisite_models = (
'circuits.Circuit',
'circuits.CircuitGroup',
)
class Meta:
ordering = ('group', 'circuit', 'priority', 'pk')
ordering = ('group', 'member_type', 'member_id', 'priority', 'pk')
constraints = (
models.UniqueConstraint(
fields=('circuit', 'group'),
name='%(app_label)s_%(class)s_unique_circuit_group'
fields=('member_type', 'member_id', 'group'),
name='%(app_label)s_%(class)s_unique_member_group'
),
)
verbose_name = _('Circuit group assignment')
@ -237,22 +244,24 @@ class CircuitTermination(
term_side = models.CharField(
max_length=1,
choices=CircuitTerminationSideChoices,
verbose_name=_('termination')
verbose_name=_('termination side')
)
site = models.ForeignKey(
to='dcim.Site',
termination_type = models.ForeignKey(
to='contenttypes.ContentType',
on_delete=models.PROTECT,
related_name='circuit_terminations',
limit_choices_to=Q(model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES),
related_name='+',
blank=True,
null=True
)
provider_network = models.ForeignKey(
to='circuits.ProviderNetwork',
on_delete=models.PROTECT,
related_name='circuit_terminations',
termination_id = models.PositiveBigIntegerField(
blank=True,
null=True
)
termination = GenericForeignKey(
ct_field='termination_type',
fk_field='termination_id'
)
port_speed = models.PositiveIntegerField(
verbose_name=_('port speed (Kbps)'),
blank=True,
@ -283,6 +292,43 @@ class CircuitTermination(
blank=True
)
# Cached associations to enable efficient filtering
_provider_network = models.ForeignKey(
to='circuits.ProviderNetwork',
on_delete=models.PROTECT,
related_name='circuit_terminations',
blank=True,
null=True
)
_location = models.ForeignKey(
to='dcim.Location',
on_delete=models.CASCADE,
related_name='circuit_terminations',
blank=True,
null=True
)
_site = models.ForeignKey(
to='dcim.Site',
on_delete=models.CASCADE,
related_name='circuit_terminations',
blank=True,
null=True
)
_region = models.ForeignKey(
to='dcim.Region',
on_delete=models.CASCADE,
related_name='circuit_terminations',
blank=True,
null=True
)
_site_group = models.ForeignKey(
to='dcim.SiteGroup',
on_delete=models.CASCADE,
related_name='circuit_terminations',
blank=True,
null=True
)
class Meta:
ordering = ['circuit', 'term_side']
constraints = (
@ -304,10 +350,35 @@ class CircuitTermination(
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."))
if self.termination is None:
raise ValidationError(_("A circuit termination must attach to termination."))
def save(self, *args, **kwargs):
# Cache objects associated with the terminating object (for filtering)
self.cache_related_objects()
super().save(*args, **kwargs)
def cache_related_objects(self):
self._provider_network = self._region = self._site_group = self._site = self._location = None
if self.termination_type:
termination_type = self.termination_type.model_class()
if termination_type == apps.get_model('dcim', 'region'):
self._region = self.termination
elif termination_type == apps.get_model('dcim', 'sitegroup'):
self._site_group = self.termination
elif termination_type == apps.get_model('dcim', 'site'):
self._region = self.termination.region
self._site_group = self.termination.group
self._site = self.termination
elif termination_type == apps.get_model('dcim', 'location'):
self._region = self.termination.site.region
self._site_group = self.termination.site.group
self._site = self.termination.site
self._location = self.termination
elif termination_type == apps.get_model('circuits', 'providernetwork'):
self._provider_network = self.termination
cache_related_objects.alters_data = True
def to_objectchange(self, action):
objectchange = super().to_objectchange(action)
@ -321,7 +392,7 @@ class CircuitTermination(
def get_peer_termination(self):
peer_side = 'Z' if self.term_side == 'A' else 'A'
try:
return CircuitTermination.objects.prefetch_related('site').get(
return CircuitTermination.objects.prefetch_related('termination').get(
circuit=self.circuit,
term_side=peer_side
)

View File

@ -1,6 +1,5 @@
from django.db import models
from django.db.models import Q
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from netbox.models import PrimaryModel
@ -22,7 +21,8 @@ class Provider(ContactsMixin, PrimaryModel):
verbose_name=_('name'),
max_length=100,
unique=True,
help_text=_('Full name of the provider')
help_text=_('Full name of the provider'),
db_collation="natural_sort"
)
slug = models.SlugField(
verbose_name=_('slug'),
@ -45,9 +45,6 @@ class Provider(ContactsMixin, PrimaryModel):
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('circuits:provider', args=[self.pk])
class ProviderAccount(ContactsMixin, PrimaryModel):
"""
@ -91,9 +88,6 @@ class ProviderAccount(ContactsMixin, PrimaryModel):
return f'{self.account} ({self.name})'
return f'{self.account}'
def get_absolute_url(self):
return reverse('circuits:provideraccount', args=[self.pk])
class ProviderNetwork(PrimaryModel):
"""
@ -102,7 +96,8 @@ class ProviderNetwork(PrimaryModel):
"""
name = models.CharField(
verbose_name=_('name'),
max_length=100
max_length=100,
db_collation="natural_sort"
)
provider = models.ForeignKey(
to='circuits.Provider',
@ -128,6 +123,3 @@ class ProviderNetwork(PrimaryModel):
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('circuits:providernetwork', args=[self.pk])

View File

@ -0,0 +1,191 @@
from functools import cached_property
from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from circuits.choices import *
from netbox.models import ChangeLoggedModel, PrimaryModel
from netbox.models.features import CustomFieldsMixin, CustomLinksMixin, TagsMixin
from .base import BaseCircuitType
__all__ = (
'VirtualCircuit',
'VirtualCircuitTermination',
'VirtualCircuitType',
)
class VirtualCircuitType(BaseCircuitType):
"""
Like physical circuits, virtual circuits can be organized by their functional role. For example, a user might wish
to categorize virtual circuits by their technological nature or by product name.
"""
class Meta:
ordering = ('name',)
verbose_name = _('virtual circuit type')
verbose_name_plural = _('virtual circuit types')
class VirtualCircuit(PrimaryModel):
"""
A virtual connection between two or more endpoints, delivered across one or more physical circuits.
"""
cid = models.CharField(
max_length=100,
verbose_name=_('circuit ID'),
help_text=_('Unique circuit ID')
)
provider_network = models.ForeignKey(
to='circuits.ProviderNetwork',
on_delete=models.PROTECT,
related_name='virtual_circuits'
)
provider_account = models.ForeignKey(
to='circuits.ProviderAccount',
on_delete=models.PROTECT,
related_name='virtual_circuits',
blank=True,
null=True
)
type = models.ForeignKey(
to='circuits.VirtualCircuitType',
on_delete=models.PROTECT,
related_name='virtual_circuits'
)
status = models.CharField(
verbose_name=_('status'),
max_length=50,
choices=CircuitStatusChoices,
default=CircuitStatusChoices.STATUS_ACTIVE
)
tenant = models.ForeignKey(
to='tenancy.Tenant',
on_delete=models.PROTECT,
related_name='virtual_circuits',
blank=True,
null=True
)
group_assignments = GenericRelation(
to='circuits.CircuitGroupAssignment',
content_type_field='member_type',
object_id_field='member_id',
related_query_name='virtual_circuit'
)
clone_fields = (
'provider_network', 'provider_account', 'status', 'tenant', 'description',
)
prerequisite_models = (
'circuits.ProviderNetwork',
'circuits.VirtualCircuitType',
)
class Meta:
ordering = ['provider_network', 'provider_account', 'cid']
constraints = (
models.UniqueConstraint(
fields=('provider_network', 'cid'),
name='%(app_label)s_%(class)s_unique_provider_network_cid'
),
models.UniqueConstraint(
fields=('provider_account', 'cid'),
name='%(app_label)s_%(class)s_unique_provideraccount_cid'
),
)
verbose_name = _('virtual circuit')
verbose_name_plural = _('virtual circuits')
def __str__(self):
return self.cid
def get_status_color(self):
return CircuitStatusChoices.colors.get(self.status)
def clean(self):
super().clean()
if self.provider_account and self.provider_network.provider != self.provider_account.provider:
raise ValidationError({
'provider_account': "The assigned account must belong to the provider of the assigned network."
})
@property
def provider(self):
return self.provider_network.provider
class VirtualCircuitTermination(
CustomFieldsMixin,
CustomLinksMixin,
TagsMixin,
ChangeLoggedModel
):
virtual_circuit = models.ForeignKey(
to='circuits.VirtualCircuit',
on_delete=models.CASCADE,
related_name='terminations'
)
role = models.CharField(
verbose_name=_('role'),
max_length=50,
choices=VirtualCircuitTerminationRoleChoices,
default=VirtualCircuitTerminationRoleChoices.ROLE_PEER
)
interface = models.OneToOneField(
to='dcim.Interface',
on_delete=models.CASCADE,
related_name='virtual_circuit_termination'
)
description = models.CharField(
verbose_name=_('description'),
max_length=200,
blank=True
)
class Meta:
ordering = ['virtual_circuit', 'role', 'pk']
verbose_name = _('virtual circuit termination')
verbose_name_plural = _('virtual circuit terminations')
def __str__(self):
return f'{self.virtual_circuit}: {self.get_role_display()} termination'
def get_absolute_url(self):
return reverse('circuits:virtualcircuittermination', args=[self.pk])
def get_role_color(self):
return VirtualCircuitTerminationRoleChoices.colors.get(self.role)
def to_objectchange(self, action):
objectchange = super().to_objectchange(action)
objectchange.related_object = self.virtual_circuit
return objectchange
@property
def parent_object(self):
return self.virtual_circuit
@cached_property
def peer_terminations(self):
if self.role == VirtualCircuitTerminationRoleChoices.ROLE_PEER:
return self.virtual_circuit.terminations.exclude(pk=self.pk).filter(
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER
)
if self.role == VirtualCircuitTerminationRoleChoices.ROLE_HUB:
return self.virtual_circuit.terminations.filter(
role=VirtualCircuitTerminationRoleChoices.ROLE_SPOKE
)
if self.role == VirtualCircuitTerminationRoleChoices.ROLE_SPOKE:
return self.virtual_circuit.terminations.filter(
role=VirtualCircuitTerminationRoleChoices.ROLE_HUB
)
def clean(self):
super().clean()
if self.interface and not self.interface.is_virtual:
raise ValidationError("Virtual circuits may be terminated only to virtual interfaces.")

View File

@ -80,3 +80,34 @@ class ProviderNetworkIndex(SearchIndex):
('comments', 5000),
)
display_attrs = ('provider', 'service_id', 'description')
@register_search
class VirtualCircuitIndex(SearchIndex):
model = models.VirtualCircuit
fields = (
('cid', 100),
('description', 500),
('comments', 5000),
)
display_attrs = ('provider', 'provider_network', 'provider_account', 'status', 'tenant', 'description')
@register_search
class VirtualCircuitTerminationIndex(SearchIndex):
model = models.VirtualCircuitTermination
fields = (
('description', 500),
)
display_attrs = ('virtual_circuit', 'role', 'description')
@register_search
class VirtualCircuitTypeIndex(SearchIndex):
model = models.VirtualCircuitType
fields = (
('name', 100),
('slug', 110),
('description', 500),
)
display_attrs = ('description',)

View File

@ -1,3 +1,4 @@
from .circuits import *
from .columns import *
from .providers import *
from .virtual_circuits import *

View File

@ -18,10 +18,8 @@ __all__ = (
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>
{% if value.termination %}
<a href="{{ value.termination.get_absolute_url }}">{{ value.termination }}</a>
{% endif %}
"""
@ -44,9 +42,10 @@ class CircuitTypeTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = CircuitType
fields = (
'pk', 'id', 'name', 'circuit_count', 'color', 'description', 'slug', 'tags', 'created', 'last_updated', 'actions',
'pk', 'id', 'name', 'circuit_count', 'color', 'description', 'slug', 'tags', 'created', 'last_updated',
'actions',
)
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug')
default_columns = ('pk', 'name', 'circuit_count', 'color', 'description')
class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
@ -62,13 +61,17 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
linkify=True,
verbose_name=_('Account')
)
type = tables.Column(
verbose_name=_('Type'),
linkify=True
)
status = columns.ChoiceFieldColumn()
termination_a = tables.TemplateColumn(
termination_a = columns.TemplateColumn(
template_code=CIRCUITTERMINATION_LINK,
orderable=False,
verbose_name=_('Side A')
)
termination_z = tables.TemplateColumn(
termination_z = columns.TemplateColumn(
template_code=CIRCUITTERMINATION_LINK,
orderable=False,
verbose_name=_('Side Z')
@ -76,6 +79,7 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
commit_rate = CommitRateColumn(
verbose_name=_('Commit Rate')
)
distance = columns.DistanceColumn()
comments = columns.MarkdownColumn(
verbose_name=_('Comments')
)
@ -109,22 +113,54 @@ class CircuitTerminationTable(NetBoxTable):
linkify=True,
accessor='circuit.provider'
)
term_side = tables.Column(
verbose_name=_('Side')
)
termination_type = columns.ContentTypeColumn(
verbose_name=_('Termination Type'),
)
termination = tables.Column(
verbose_name=_('Termination Point'),
linkify=True
)
# Termination types
site = tables.Column(
verbose_name=_('Site'),
linkify=True
linkify=True,
accessor='_site'
)
site_group = tables.Column(
verbose_name=_('Site Group'),
linkify=True,
accessor='_sitegroup'
)
region = tables.Column(
verbose_name=_('Region'),
linkify=True,
accessor='_region'
)
location = tables.Column(
verbose_name=_('Location'),
linkify=True,
accessor='_location'
)
provider_network = tables.Column(
verbose_name=_('Provider Network'),
linkify=True
linkify=True,
accessor='_provider_network'
)
class Meta(NetBoxTable.Meta):
model = CircuitTermination
fields = (
'pk', 'id', 'circuit', 'provider', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed',
'xconnect_id', 'pp_info', 'description', 'created', 'last_updated', 'actions',
'pk', 'id', 'circuit', 'provider', 'term_side', 'termination_type', 'termination', 'site_group', 'region',
'site', 'location', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
'description', 'created', 'last_updated', 'actions',
)
default_columns = (
'pk', 'id', 'circuit', 'provider', 'term_side', 'termination_type', 'termination', 'description',
)
default_columns = ('pk', 'id', 'circuit', 'provider', 'term_side', 'description')
class CircuitGroupTable(NetBoxTable):
@ -156,11 +192,14 @@ class CircuitGroupAssignmentTable(NetBoxTable):
linkify=True
)
provider = tables.Column(
accessor='circuit__provider',
accessor='member__provider',
verbose_name=_('Provider'),
linkify=True
)
circuit = tables.Column(
member_type = columns.ContentTypeColumn(
verbose_name=_('Type')
)
member = tables.Column(
verbose_name=_('Circuit'),
linkify=True
)
@ -174,6 +213,7 @@ class CircuitGroupAssignmentTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = CircuitGroupAssignment
fields = (
'pk', 'id', 'group', 'provider', 'circuit', 'priority', 'created', 'last_updated', 'actions', 'tags',
'pk', 'id', 'group', 'provider', 'member_type', 'member', 'priority', 'created', 'last_updated', 'actions',
'tags',
)
default_columns = ('pk', 'group', 'provider', 'circuit', 'priority')
default_columns = ('pk', 'group', 'provider', 'member_type', 'member', 'priority')

View File

@ -0,0 +1,124 @@
import django_tables2 as tables
from django.utils.translation import gettext_lazy as _
from circuits.models import *
from netbox.tables import NetBoxTable, columns
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
__all__ = (
'VirtualCircuitTable',
'VirtualCircuitTerminationTable',
'VirtualCircuitTypeTable',
)
class VirtualCircuitTypeTable(NetBoxTable):
name = tables.Column(
linkify=True,
verbose_name=_('Name'),
)
color = columns.ColorColumn()
tags = columns.TagColumn(
url_name='circuits:virtualcircuittype_list'
)
virtual_circuit_count = columns.LinkedCountColumn(
viewname='circuits:virtualcircuit_list',
url_params={'type_id': 'pk'},
verbose_name=_('Circuits')
)
class Meta(NetBoxTable.Meta):
model = VirtualCircuitType
fields = (
'pk', 'id', 'name', 'virtual_circuit_count', 'color', 'description', 'slug', 'tags', 'created',
'last_updated', 'actions',
)
default_columns = ('pk', 'name', 'virtual_circuit_count', 'color', 'description')
class VirtualCircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
cid = tables.Column(
linkify=True,
verbose_name=_('Circuit ID')
)
provider = tables.Column(
accessor=tables.A('provider_network__provider'),
verbose_name=_('Provider'),
linkify=True
)
provider_network = tables.Column(
linkify=True,
verbose_name=_('Provider network')
)
provider_account = tables.Column(
linkify=True,
verbose_name=_('Account')
)
type = tables.Column(
verbose_name=_('Type'),
linkify=True
)
status = columns.ChoiceFieldColumn()
termination_count = columns.LinkedCountColumn(
viewname='circuits:virtualcircuittermination_list',
url_params={'virtual_circuit_id': 'pk'},
verbose_name=_('Terminations')
)
comments = columns.MarkdownColumn(
verbose_name=_('Comments')
)
tags = columns.TagColumn(
url_name='circuits:virtualcircuit_list'
)
class Meta(NetBoxTable.Meta):
model = VirtualCircuit
fields = (
'pk', 'id', 'cid', 'provider', 'provider_account', 'provider_network', 'type', 'status', 'tenant',
'tenant_group', 'description', 'comments', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'cid', 'provider', 'provider_account', 'provider_network', 'type', 'status', 'tenant',
'termination_count', 'description',
)
class VirtualCircuitTerminationTable(NetBoxTable):
virtual_circuit = tables.Column(
verbose_name=_('Virtual circuit'),
linkify=True
)
provider = tables.Column(
accessor=tables.A('virtual_circuit__provider_network__provider'),
verbose_name=_('Provider'),
linkify=True
)
provider_network = tables.Column(
accessor=tables.A('virtual_circuit__provider_network'),
linkify=True,
verbose_name=_('Provider network')
)
provider_account = tables.Column(
linkify=True,
verbose_name=_('Account')
)
role = columns.ChoiceFieldColumn()
device = tables.Column(
accessor=tables.A('interface__device'),
linkify=True,
verbose_name=_('Device')
)
interface = tables.Column(
verbose_name=_('Interface'),
linkify=True
)
class Meta(NetBoxTable.Meta):
model = VirtualCircuitTermination
fields = (
'pk', 'id', 'virtual_circuit', 'provider', 'provider_network', 'provider_account', 'role', 'interfaces',
'description', 'created', 'last_updated', 'actions',
)
default_columns = (
'pk', 'id', 'virtual_circuit', 'role', 'device', 'interface', 'description',
)

View File

@ -2,7 +2,8 @@ from django.urls import reverse
from circuits.choices import *
from circuits.models import *
from dcim.models import Site
from dcim.choices import InterfaceTypeChoices
from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer, Site
from ipam.models import ASN, RIR
from utilities.testing import APITestCase, APIViewTestCases
@ -120,9 +121,15 @@ class CircuitTest(APIViewTestCases.APIViewTestCase):
CircuitType.objects.bulk_create(circuit_types)
circuits = (
Circuit(cid='Circuit 1', provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0]),
Circuit(cid='Circuit 2', provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0]),
Circuit(cid='Circuit 3', provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0]),
Circuit(
cid='Circuit 1', provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0]
),
Circuit(
cid='Circuit 2', provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0]
),
Circuit(
cid='Circuit 3', provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0]
),
)
Circuit.objects.bulk_create(circuits)
@ -181,10 +188,10 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
Circuit.objects.bulk_create(circuits)
circuit_terminations = (
CircuitTermination(circuit=circuits[0], term_side=SIDE_A, site=sites[0]),
CircuitTermination(circuit=circuits[0], term_side=SIDE_Z, provider_network=provider_networks[0]),
CircuitTermination(circuit=circuits[1], term_side=SIDE_A, site=sites[1]),
CircuitTermination(circuit=circuits[1], term_side=SIDE_Z, provider_network=provider_networks[1]),
CircuitTermination(circuit=circuits[0], term_side=SIDE_A, termination=sites[0]),
CircuitTermination(circuit=circuits[0], term_side=SIDE_Z, termination=provider_networks[0]),
CircuitTermination(circuit=circuits[1], term_side=SIDE_A, termination=sites[1]),
CircuitTermination(circuit=circuits[1], term_side=SIDE_Z, termination=provider_networks[1]),
)
CircuitTermination.objects.bulk_create(circuit_terminations)
@ -192,13 +199,15 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
{
'circuit': circuits[2].pk,
'term_side': SIDE_A,
'site': sites[0].pk,
'termination_type': 'dcim.site',
'termination_id': sites[0].pk,
'port_speed': 200000,
},
{
'circuit': circuits[2].pk,
'term_side': SIDE_Z,
'provider_network': provider_networks[0].pk,
'termination_type': 'circuits.providernetwork',
'termination_id': provider_networks[0].pk,
'port_speed': 200000,
},
]
@ -286,7 +295,7 @@ class ProviderAccountTest(APIViewTestCases.APIViewTestCase):
class CircuitGroupAssignmentTest(APIViewTestCases.APIViewTestCase):
model = CircuitGroupAssignment
brief_fields = ['circuit', 'display', 'group', 'id', 'priority', 'url']
brief_fields = ['display', 'group', 'id', 'member', 'member_id', 'member_type', 'priority', 'url']
bulk_update_data = {
'priority': CircuitPriorityChoices.PRIORITY_INACTIVE,
}
@ -321,17 +330,17 @@ class CircuitGroupAssignmentTest(APIViewTestCases.APIViewTestCase):
assignments = (
CircuitGroupAssignment(
group=circuit_groups[0],
circuit=circuits[0],
member=circuits[0],
priority=CircuitPriorityChoices.PRIORITY_PRIMARY
),
CircuitGroupAssignment(
group=circuit_groups[1],
circuit=circuits[1],
member=circuits[1],
priority=CircuitPriorityChoices.PRIORITY_SECONDARY
),
CircuitGroupAssignment(
group=circuit_groups[2],
circuit=circuits[2],
member=circuits[2],
priority=CircuitPriorityChoices.PRIORITY_TERTIARY
),
)
@ -340,17 +349,20 @@ class CircuitGroupAssignmentTest(APIViewTestCases.APIViewTestCase):
cls.create_data = [
{
'group': circuit_groups[3].pk,
'circuit': circuits[3].pk,
'member_type': 'circuits.circuit',
'member_id': circuits[3].pk,
'priority': CircuitPriorityChoices.PRIORITY_PRIMARY,
},
{
'group': circuit_groups[4].pk,
'circuit': circuits[4].pk,
'member_type': 'circuits.circuit',
'member_id': circuits[4].pk,
'priority': CircuitPriorityChoices.PRIORITY_SECONDARY,
},
{
'group': circuit_groups[5].pk,
'circuit': circuits[5].pk,
'member_type': 'circuits.circuit',
'member_id': circuits[5].pk,
'priority': CircuitPriorityChoices.PRIORITY_TERTIARY,
},
]
@ -395,3 +407,290 @@ class ProviderNetworkTest(APIViewTestCases.APIViewTestCase):
'provider': providers[1].pk,
'description': 'New description',
}
class VirtualCircuitTypeTest(APIViewTestCases.APIViewTestCase):
model = VirtualCircuitType
brief_fields = ['description', 'display', 'id', 'name', 'slug', 'url', 'virtual_circuit_count']
create_data = (
{
'name': 'Virtual Circuit Type 4',
'slug': 'virtual-circuit-type-4',
},
{
'name': 'Virtual Circuit Type 5',
'slug': 'virtual-circuit-type-5',
},
{
'name': 'Virtual Circuit Type 6',
'slug': 'virtual-circuit-type-6',
},
)
bulk_update_data = {
'description': 'New description',
}
@classmethod
def setUpTestData(cls):
virtual_circuit_types = (
VirtualCircuitType(name='Virtual Circuit Type 1', slug='virtual-circuit-type-1'),
VirtualCircuitType(name='Virtual Circuit Type 2', slug='virtual-circuit-type-2'),
VirtualCircuitType(name='Virtual Circuit Type 3', slug='virtual-circuit-type-3'),
)
VirtualCircuitType.objects.bulk_create(virtual_circuit_types)
class VirtualCircuitTest(APIViewTestCases.APIViewTestCase):
model = VirtualCircuit
brief_fields = ['cid', 'description', 'display', 'id', 'provider_network', 'url']
bulk_update_data = {
'status': 'planned',
}
@classmethod
def setUpTestData(cls):
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
provider_network = ProviderNetwork.objects.create(provider=provider, name='Provider Network 1')
provider_account = ProviderAccount.objects.create(provider=provider, account='Provider Account 1')
virtual_circuit_type = VirtualCircuitType.objects.create(
name='Virtual Circuit Type 1',
slug='virtual-circuit-type-1'
)
virtual_circuits = (
VirtualCircuit(
provider_network=provider_network,
provider_account=provider_account,
type=virtual_circuit_type,
cid='Virtual Circuit 1'
),
VirtualCircuit(
provider_network=provider_network,
provider_account=provider_account,
type=virtual_circuit_type,
cid='Virtual Circuit 2'
),
VirtualCircuit(
provider_network=provider_network,
provider_account=provider_account,
type=virtual_circuit_type,
cid='Virtual Circuit 3'
),
)
VirtualCircuit.objects.bulk_create(virtual_circuits)
cls.create_data = [
{
'cid': 'Virtual Circuit 4',
'provider_network': provider_network.pk,
'provider_account': provider_account.pk,
'type': virtual_circuit_type.pk,
'status': CircuitStatusChoices.STATUS_PLANNED,
},
{
'cid': 'Virtual Circuit 5',
'provider_network': provider_network.pk,
'provider_account': provider_account.pk,
'type': virtual_circuit_type.pk,
'status': CircuitStatusChoices.STATUS_PLANNED,
},
{
'cid': 'Virtual Circuit 6',
'provider_network': provider_network.pk,
'provider_account': provider_account.pk,
'type': virtual_circuit_type.pk,
'status': CircuitStatusChoices.STATUS_PLANNED,
},
]
class VirtualCircuitTerminationTest(APIViewTestCases.APIViewTestCase):
model = VirtualCircuitTermination
brief_fields = ['description', 'display', 'id', 'interface', 'role', 'url', 'virtual_circuit']
bulk_update_data = {
'description': 'New description',
}
@classmethod
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1')
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
site = Site.objects.create(name='Site 1', slug='site-1')
devices = (
Device(site=site, name='hub', device_type=device_type, role=device_role),
Device(site=site, name='spoke1', device_type=device_type, role=device_role),
Device(site=site, name='spoke2', device_type=device_type, role=device_role),
Device(site=site, name='spoke3', device_type=device_type, role=device_role),
)
Device.objects.bulk_create(devices)
physical_interfaces = (
Interface(device=devices[0], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
Interface(device=devices[1], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
Interface(device=devices[2], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
Interface(device=devices[3], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
)
Interface.objects.bulk_create(physical_interfaces)
virtual_interfaces = (
# Point-to-point VCs
Interface(
device=devices[0],
name='eth0.1',
parent=physical_interfaces[0],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[0],
name='eth0.2',
parent=physical_interfaces[0],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[0],
name='eth0.3',
parent=physical_interfaces[0],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[1],
name='eth0.1',
parent=physical_interfaces[1],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[2],
name='eth0.1',
parent=physical_interfaces[2],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[3],
name='eth0.1',
parent=physical_interfaces[3],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
# Hub and spoke VCs
Interface(
device=devices[0],
name='eth0.9',
parent=physical_interfaces[0],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[1],
name='eth0.9',
parent=physical_interfaces[0],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[2],
name='eth0.9',
parent=physical_interfaces[0],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[3],
name='eth0.9',
parent=physical_interfaces[0],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
)
Interface.objects.bulk_create(virtual_interfaces)
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
provider_network = ProviderNetwork.objects.create(provider=provider, name='Provider Network 1')
provider_account = ProviderAccount.objects.create(provider=provider, account='Provider Account 1')
virtual_circuit_type = VirtualCircuitType.objects.create(
name='Virtual Circuit Type 1',
slug='virtual-circuit-type-1'
)
virtual_circuits = (
VirtualCircuit(
provider_network=provider_network,
provider_account=provider_account,
cid='Virtual Circuit 1',
type=virtual_circuit_type
),
VirtualCircuit(
provider_network=provider_network,
provider_account=provider_account,
cid='Virtual Circuit 2',
type=virtual_circuit_type
),
VirtualCircuit(
provider_network=provider_network,
provider_account=provider_account,
cid='Virtual Circuit 3',
type=virtual_circuit_type
),
VirtualCircuit(
provider_network=provider_network,
provider_account=provider_account,
cid='Virtual Circuit 4',
type=virtual_circuit_type
),
)
VirtualCircuit.objects.bulk_create(virtual_circuits)
virtual_circuit_terminations = (
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[0],
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
interface=virtual_interfaces[0]
),
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[0],
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
interface=virtual_interfaces[3]
),
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[1],
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
interface=virtual_interfaces[1]
),
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[1],
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
interface=virtual_interfaces[4]
),
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[2],
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
interface=virtual_interfaces[2]
),
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[2],
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
interface=virtual_interfaces[5]
),
)
VirtualCircuitTermination.objects.bulk_create(virtual_circuit_terminations)
cls.create_data = [
{
'virtual_circuit': virtual_circuits[3].pk,
'role': VirtualCircuitTerminationRoleChoices.ROLE_HUB,
'interface': virtual_interfaces[6].pk
},
{
'virtual_circuit': virtual_circuits[3].pk,
'role': VirtualCircuitTerminationRoleChoices.ROLE_SPOKE,
'interface': virtual_interfaces[7].pk
},
{
'virtual_circuit': virtual_circuits[3].pk,
'role': VirtualCircuitTerminationRoleChoices.ROLE_SPOKE,
'interface': virtual_interfaces[8].pk
},
{
'virtual_circuit': virtual_circuits[3].pk,
'role': VirtualCircuitTerminationRoleChoices.ROLE_SPOKE,
'interface': virtual_interfaces[9].pk
},
]

View File

@ -3,8 +3,10 @@ from django.test import TestCase
from circuits.choices import *
from circuits.filtersets import *
from circuits.models import *
from dcim.models import Cable, Region, Site, SiteGroup
from dcim.choices import InterfaceTypeChoices
from dcim.models import Cable, Device, DeviceRole, DeviceType, Interface, Manufacturer, Region, Site, SiteGroup
from ipam.models import ASN, RIR
from netbox.choices import DistanceUnitChoices
from tenancy.models import Tenant, TenantGroup
from utilities.testing import ChangeLoggedFilterSetTests
@ -69,10 +71,12 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
)
Circuit.objects.bulk_create(circuits)
CircuitTermination.objects.bulk_create((
CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A'),
CircuitTermination(circuit=circuits[1], site=sites[0], term_side='A'),
))
circuit_terminations = (
CircuitTermination(circuit=circuits[0], termination=sites[0], term_side='A'),
CircuitTermination(circuit=circuits[1], termination=sites[0], term_side='A'),
)
for ct in circuit_terminations:
ct.save()
def test_q(self):
params = {'q': 'foobar1'}
@ -222,24 +226,93 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
ProviderNetwork.objects.bulk_create(provider_networks)
circuits = (
Circuit(provider=providers[0], provider_account=provider_accounts[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 1', install_date='2020-01-01', termination_date='2021-01-01', commit_rate=1000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar1'),
Circuit(provider=providers[0], provider_account=provider_accounts[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 2', install_date='2020-01-02', termination_date='2021-01-02', commit_rate=2000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar2'),
Circuit(provider=providers[0], provider_account=provider_accounts[1], tenant=tenants[1], type=circuit_types[0], cid='Test Circuit 3', install_date='2020-01-03', termination_date='2021-01-03', commit_rate=3000, status=CircuitStatusChoices.STATUS_PLANNED),
Circuit(provider=providers[1], provider_account=provider_accounts[1], tenant=tenants[1], type=circuit_types[1], cid='Test Circuit 4', install_date='2020-01-04', termination_date='2021-01-04', commit_rate=4000, status=CircuitStatusChoices.STATUS_PLANNED),
Circuit(provider=providers[1], provider_account=provider_accounts[2], tenant=tenants[2], type=circuit_types[1], cid='Test Circuit 5', install_date='2020-01-05', termination_date='2021-01-05', commit_rate=5000, status=CircuitStatusChoices.STATUS_OFFLINE),
Circuit(provider=providers[1], provider_account=provider_accounts[2], tenant=tenants[2], type=circuit_types[1], cid='Test Circuit 6', install_date='2020-01-06', termination_date='2021-01-06', commit_rate=6000, status=CircuitStatusChoices.STATUS_OFFLINE),
Circuit(
provider=providers[0],
provider_account=provider_accounts[0],
tenant=tenants[0],
type=circuit_types[0],
cid='Test Circuit 1',
install_date='2020-01-01',
termination_date='2021-01-01',
commit_rate=1000,
status=CircuitStatusChoices.STATUS_ACTIVE,
description='foobar1',
distance=10,
distance_unit=DistanceUnitChoices.UNIT_FOOT,
),
Circuit(
provider=providers[0],
provider_account=provider_accounts[0],
tenant=tenants[0],
type=circuit_types[0],
cid='Test Circuit 2',
install_date='2020-01-02',
termination_date='2021-01-02',
commit_rate=2000,
status=CircuitStatusChoices.STATUS_ACTIVE,
description='foobar2',
distance=20,
distance_unit=DistanceUnitChoices.UNIT_METER,
),
Circuit(
provider=providers[0],
provider_account=provider_accounts[1],
tenant=tenants[1],
type=circuit_types[0],
cid='Test Circuit 3',
install_date='2020-01-03',
termination_date='2021-01-03',
commit_rate=3000,
status=CircuitStatusChoices.STATUS_PLANNED,
distance=30,
distance_unit=DistanceUnitChoices.UNIT_METER,
),
Circuit(
provider=providers[1],
provider_account=provider_accounts[1],
tenant=tenants[1],
type=circuit_types[1],
cid='Test Circuit 4',
install_date='2020-01-04',
termination_date='2021-01-04',
commit_rate=4000,
status=CircuitStatusChoices.STATUS_PLANNED,
),
Circuit(
provider=providers[1],
provider_account=provider_accounts[2],
tenant=tenants[2],
type=circuit_types[1],
cid='Test Circuit 5',
install_date='2020-01-05',
termination_date='2021-01-05',
commit_rate=5000,
status=CircuitStatusChoices.STATUS_OFFLINE,
),
Circuit(
provider=providers[1],
provider_account=provider_accounts[2],
tenant=tenants[2],
type=circuit_types[1],
cid='Test Circuit 6',
install_date='2020-01-06',
termination_date='2021-01-06',
commit_rate=6000,
status=CircuitStatusChoices.STATUS_OFFLINE,
),
)
Circuit.objects.bulk_create(circuits)
circuit_terminations = ((
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(circuit=circuits[0], termination=sites[0], term_side='A'),
CircuitTermination(circuit=circuits[1], termination=sites[1], term_side='A'),
CircuitTermination(circuit=circuits[2], termination=sites[2], term_side='A'),
CircuitTermination(circuit=circuits[3], termination=provider_networks[0], term_side='A'),
CircuitTermination(circuit=circuits[4], termination=provider_networks[1], term_side='A'),
CircuitTermination(circuit=circuits[5], termination=provider_networks[2], term_side='A'),
))
CircuitTermination.objects.bulk_create(circuit_terminations)
for ct in circuit_terminations:
ct.save()
def test_q(self):
params = {'q': 'foobar1'}
@ -289,6 +362,14 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'status': [CircuitStatusChoices.STATUS_ACTIVE, CircuitStatusChoices.STATUS_PLANNED]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_distance(self):
params = {'distance': [10, 20]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_distance_unit(self):
params = {'distance_unit': DistanceUnitChoices.UNIT_FOOT}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_description(self):
params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@ -374,19 +455,66 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
)
Circuit.objects.bulk_create(circuits)
circuit_terminations = ((
CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A', port_speed=1000, upstream_speed=1000, xconnect_id='ABC', description='foobar1'),
CircuitTermination(circuit=circuits[0], site=sites[1], term_side='Z', port_speed=1000, upstream_speed=1000, xconnect_id='DEF', description='foobar2'),
CircuitTermination(circuit=circuits[1], site=sites[1], term_side='A', port_speed=2000, upstream_speed=2000, xconnect_id='GHI'),
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(circuit=circuits[6], provider_network=provider_networks[0], term_side='A', mark_connected=True),
))
CircuitTermination.objects.bulk_create(circuit_terminations)
circuit_terminations = (
CircuitTermination(
circuit=circuits[0],
termination=sites[0],
term_side='A',
port_speed=1000,
upstream_speed=1000,
xconnect_id='ABC',
description='foobar1',
),
CircuitTermination(
circuit=circuits[0],
termination=sites[1],
term_side='Z',
port_speed=1000,
upstream_speed=1000,
xconnect_id='DEF',
description='foobar2',
),
CircuitTermination(
circuit=circuits[1],
termination=sites[1],
term_side='A',
port_speed=2000,
upstream_speed=2000,
xconnect_id='GHI',
),
CircuitTermination(
circuit=circuits[1],
termination=sites[2],
term_side='Z',
port_speed=2000,
upstream_speed=2000,
xconnect_id='JKL',
),
CircuitTermination(
circuit=circuits[2],
termination=sites[2],
term_side='A',
port_speed=3000,
upstream_speed=3000,
xconnect_id='MNO',
),
CircuitTermination(
circuit=circuits[2],
termination=sites[0],
term_side='Z',
port_speed=3000,
upstream_speed=3000,
xconnect_id='PQR',
),
CircuitTermination(circuit=circuits[3], termination=provider_networks[0], term_side='A'),
CircuitTermination(circuit=circuits[4], termination=provider_networks[1], term_side='A'),
CircuitTermination(circuit=circuits[5], termination=provider_networks[2], term_side='A'),
CircuitTermination(
circuit=circuits[6], termination=provider_networks[0], term_side='A', mark_connected=True
),
)
for ct in circuit_terminations:
ct.save()
Cable(a_terminations=[circuit_terminations[0]], b_terminations=[circuit_terminations[1]]).save()
@ -520,7 +648,6 @@ class CircuitGroupAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests):
CircuitGroup(name='Circuit Group 1', slug='circuit-group-1'),
CircuitGroup(name='Circuit Group 2', slug='circuit-group-2'),
CircuitGroup(name='Circuit Group 3', slug='circuit-group-3'),
CircuitGroup(name='Circuit Group 4', slug='circuit-group-4'),
)
CircuitGroup.objects.bulk_create(circuit_groups)
@ -528,43 +655,86 @@ class CircuitGroupAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests):
Provider(name='Provider 1', slug='provider-1'),
Provider(name='Provider 2', slug='provider-2'),
Provider(name='Provider 3', slug='provider-3'),
Provider(name='Provider 4', slug='provider-4'),
))
circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
circuit_type = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
circuits = (
Circuit(cid='Circuit 1', provider=providers[0], type=circuittype),
Circuit(cid='Circuit 2', provider=providers[1], type=circuittype),
Circuit(cid='Circuit 3', provider=providers[2], type=circuittype),
Circuit(cid='Circuit 4', provider=providers[3], type=circuittype),
Circuit(cid='Circuit 1', provider=providers[0], type=circuit_type),
Circuit(cid='Circuit 2', provider=providers[1], type=circuit_type),
Circuit(cid='Circuit 3', provider=providers[2], type=circuit_type),
)
Circuit.objects.bulk_create(circuits)
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)
virtual_circuit_type = VirtualCircuitType.objects.create(
name='Virtual Circuit Type 1',
slug='virtual-circuit-type-1'
)
virtual_circuits = (
VirtualCircuit(
provider_network=provider_networks[0],
cid='Virtual Circuit 1',
type=virtual_circuit_type
),
VirtualCircuit(
provider_network=provider_networks[1],
cid='Virtual Circuit 2',
type=virtual_circuit_type
),
VirtualCircuit(
provider_network=provider_networks[2],
cid='Virtual Circuit 3',
type=virtual_circuit_type
),
)
VirtualCircuit.objects.bulk_create(virtual_circuits)
assignments = (
CircuitGroupAssignment(
group=circuit_groups[0],
circuit=circuits[0],
member=circuits[0],
priority=CircuitPriorityChoices.PRIORITY_PRIMARY
),
CircuitGroupAssignment(
group=circuit_groups[1],
circuit=circuits[1],
member=circuits[1],
priority=CircuitPriorityChoices.PRIORITY_SECONDARY
),
CircuitGroupAssignment(
group=circuit_groups[2],
circuit=circuits[2],
member=circuits[2],
priority=CircuitPriorityChoices.PRIORITY_TERTIARY
),
CircuitGroupAssignment(
group=circuit_groups[0],
member=virtual_circuits[0],
priority=CircuitPriorityChoices.PRIORITY_PRIMARY
),
CircuitGroupAssignment(
group=circuit_groups[1],
member=virtual_circuits[1],
priority=CircuitPriorityChoices.PRIORITY_SECONDARY
),
CircuitGroupAssignment(
group=circuit_groups[2],
member=virtual_circuits[2],
priority=CircuitPriorityChoices.PRIORITY_TERTIARY
),
)
CircuitGroupAssignment.objects.bulk_create(assignments)
def test_group_id(self):
groups = CircuitGroup.objects.filter(name__in=['Circuit Group 1', 'Circuit Group 2'])
def test_group(self):
groups = CircuitGroup.objects.all()[:2]
params = {'group_id': [groups[0].pk, groups[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'group': [groups[0].slug, groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_circuit(self):
circuits = Circuit.objects.all()[:2]
@ -573,12 +743,19 @@ class CircuitGroupAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'circuit': [circuits[0].cid, circuits[1].cid]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_virtual_circuit(self):
virtual_circuits = VirtualCircuit.objects.all()[:2]
params = {'virtual_circuit_id': [virtual_circuits[0].pk, virtual_circuits[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'virtual_circuit': [virtual_circuits[0].cid, virtual_circuits[1].cid]}
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)
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'provider': [providers[0].slug, providers[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
class ProviderNetworkTestCase(TestCase, ChangeLoggedFilterSetTests):
@ -665,3 +842,347 @@ class ProviderAccountTestCase(TestCase, ChangeLoggedFilterSetTests):
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)
class VirtualCircuitTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = VirtualCircuitType.objects.all()
filterset = VirtualCircuitTypeFilterSet
@classmethod
def setUpTestData(cls):
VirtualCircuitType.objects.bulk_create((
VirtualCircuitType(name='Virtual Circuit Type 1', slug='virtual-circuit-type-1', description='foobar1'),
VirtualCircuitType(name='Virtual Circuit Type 2', slug='virtual-circuit-type-2', description='foobar2'),
VirtualCircuitType(name='Virtual Circuit Type 3', slug='virtual-circuit-type-3'),
))
def test_q(self):
params = {'q': 'foobar1'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_name(self):
params = {'name': ['Virtual Circuit Type 1']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_slug(self):
params = {'slug': ['virtual-circuit-type-1']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_description(self):
params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class VirtualCircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = VirtualCircuit.objects.all()
filterset = VirtualCircuitFilterSet
@classmethod
def setUpTestData(cls):
tenant_groups = (
TenantGroup(name='Tenant group 1', slug='tenant-group-1'),
TenantGroup(name='Tenant group 2', slug='tenant-group-2'),
TenantGroup(name='Tenant group 3', slug='tenant-group-3'),
)
for tenantgroup in tenant_groups:
tenantgroup.save()
tenants = (
Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]),
Tenant(name='Tenant 2', slug='tenant-2', group=tenant_groups[1]),
Tenant(name='Tenant 3', slug='tenant-3', group=tenant_groups[2]),
)
Tenant.objects.bulk_create(tenants)
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_accounts = (
ProviderAccount(name='Provider Account 1', provider=providers[0], account='A'),
ProviderAccount(name='Provider Account 2', provider=providers[1], account='B'),
ProviderAccount(name='Provider Account 3', provider=providers[2], account='C'),
)
ProviderAccount.objects.bulk_create(provider_accounts)
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)
virtual_circuit_types = (
VirtualCircuitType(name='Virtual Circuit Type 1', slug='virtual-circuit-type-1'),
VirtualCircuitType(name='Virtual Circuit Type 2', slug='virtual-circuit-type-2'),
VirtualCircuitType(name='Virtual Circuit Type 3', slug='virtual-circuit-type-3'),
)
VirtualCircuitType.objects.bulk_create(virtual_circuit_types)
virutal_circuits = (
VirtualCircuit(
provider_network=provider_networks[0],
provider_account=provider_accounts[0],
tenant=tenants[0],
cid='Virtual Circuit 1',
type=virtual_circuit_types[0],
status=CircuitStatusChoices.STATUS_PLANNED,
description='virtualcircuit1',
),
VirtualCircuit(
provider_network=provider_networks[1],
provider_account=provider_accounts[1],
tenant=tenants[1],
cid='Virtual Circuit 2',
type=virtual_circuit_types[1],
status=CircuitStatusChoices.STATUS_ACTIVE,
description='virtualcircuit2',
),
VirtualCircuit(
provider_network=provider_networks[2],
provider_account=provider_accounts[2],
tenant=tenants[2],
cid='Virtual Circuit 3',
type=virtual_circuit_types[2],
status=CircuitStatusChoices.STATUS_DEPROVISIONING,
description='virtualcircuit3',
),
)
VirtualCircuit.objects.bulk_create(virutal_circuits)
def test_q(self):
params = {'q': 'virtualcircuit1'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_cid(self):
params = {'cid': ['Virtual Circuit 1', 'Virtual Circuit 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)
def test_provider_account(self):
provider_accounts = ProviderAccount.objects.all()[:2]
params = {'provider_account_id': [provider_accounts[0].pk, provider_accounts[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
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):
virtual_circuit_types = VirtualCircuitType.objects.all()[:2]
params = {'type_id': [virtual_circuit_types[0].pk, virtual_circuit_types[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'type': [virtual_circuit_types[0].slug, virtual_circuit_types[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_status(self):
params = {'status': [CircuitStatusChoices.STATUS_ACTIVE, CircuitStatusChoices.STATUS_PLANNED]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_description(self):
params = {'description': ['virtualcircuit1', 'virtualcircuit2']}
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]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'tenant': [tenants[0].slug, tenants[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_tenant_group(self):
tenant_groups = TenantGroup.objects.all()[:2]
params = {'tenant_group_id': [tenant_groups[0].pk, tenant_groups[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class VirtualCircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = VirtualCircuitTermination.objects.all()
filterset = VirtualCircuitTerminationFilterSet
@classmethod
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1')
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
site = Site.objects.create(name='Site 1', slug='site-1')
devices = (
Device(site=site, name='Device 1', device_type=device_type, role=device_role),
Device(site=site, name='Device 2', device_type=device_type, role=device_role),
Device(site=site, name='Device 3', device_type=device_type, role=device_role),
)
Device.objects.bulk_create(devices)
virtual_interfaces = (
# Device 1
Interface(
device=devices[0],
name='eth0.1',
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[0],
name='eth0.2',
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
# Device 2
Interface(
device=devices[1],
name='eth0.1',
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[1],
name='eth0.2',
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
# Device 3
Interface(
device=devices[2],
name='eth0.1',
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[2],
name='eth0.2',
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
)
Interface.objects.bulk_create(virtual_interfaces)
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(provider=providers[0], name='Provider Network 1'),
ProviderNetwork(provider=providers[1], name='Provider Network 2'),
ProviderNetwork(provider=providers[2], name='Provider Network 3'),
)
ProviderNetwork.objects.bulk_create(provider_networks)
provider_accounts = (
ProviderAccount(provider=providers[0], account='Provider Account 1'),
ProviderAccount(provider=providers[1], account='Provider Account 2'),
ProviderAccount(provider=providers[2], account='Provider Account 3'),
)
ProviderAccount.objects.bulk_create(provider_accounts)
virtual_circuit_type = VirtualCircuitType.objects.create(
name='Virtual Circuit Type 1',
slug='virtual-circuit-type-1'
)
virtual_circuits = (
VirtualCircuit(
provider_network=provider_networks[0],
provider_account=provider_accounts[0],
cid='Virtual Circuit 1',
type=virtual_circuit_type
),
VirtualCircuit(
provider_network=provider_networks[1],
provider_account=provider_accounts[1],
cid='Virtual Circuit 2',
type=virtual_circuit_type
),
VirtualCircuit(
provider_network=provider_networks[2],
provider_account=provider_accounts[2],
cid='Virtual Circuit 3',
type=virtual_circuit_type
),
)
VirtualCircuit.objects.bulk_create(virtual_circuits)
virtual_circuit_terminations = (
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[0],
role=VirtualCircuitTerminationRoleChoices.ROLE_HUB,
interface=virtual_interfaces[0],
description='termination1'
),
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[0],
role=VirtualCircuitTerminationRoleChoices.ROLE_SPOKE,
interface=virtual_interfaces[3],
description='termination2'
),
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[1],
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
interface=virtual_interfaces[1],
description='termination3'
),
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[1],
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
interface=virtual_interfaces[4],
description='termination4'
),
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[2],
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
interface=virtual_interfaces[2],
description='termination5'
),
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[2],
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
interface=virtual_interfaces[5],
description='termination6'
),
)
VirtualCircuitTermination.objects.bulk_create(virtual_circuit_terminations)
def test_q(self):
params = {'q': 'termination1'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_description(self):
params = {'description': ['termination1', 'termination2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_virtual_circuit_id(self):
virtual_circuits = VirtualCircuit.objects.filter()[:2]
params = {'virtual_circuit_id': [virtual_circuits[0].pk, virtual_circuits[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
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(), 4)
params = {'provider': [providers[0].slug, providers[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(), 4)
def test_provider_account(self):
provider_accounts = ProviderAccount.objects.all()[:2]
params = {'provider_account_id': [provider_accounts[0].pk, provider_accounts[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'provider_account': [provider_accounts[0].account, provider_accounts[1].account]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_interface(self):
interfaces = Interface.objects.all()[:2]
params = {'interface_id': [interfaces[0].pk, interfaces[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

View File

@ -1,12 +1,14 @@
import datetime
from django.contrib.contenttypes.models import ContentType
from django.test import override_settings
from django.urls import reverse
from circuits.choices import *
from circuits.models import *
from core.models import ObjectType
from dcim.models import Cable, Interface, Site
from dcim.choices import InterfaceTypeChoices
from dcim.models import Cable, Device, DeviceRole, DeviceType, Interface, Manufacturer, Site
from ipam.models import ASN, RIR
from netbox.choices import ImportFormatChoices
from users.models import ObjectPermission
@ -139,9 +141,15 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
CircuitType.objects.bulk_create(circuittypes)
circuits = (
Circuit(cid='Circuit 1', provider=providers[0], provider_account=provider_accounts[0], type=circuittypes[0]),
Circuit(cid='Circuit 2', provider=providers[0], provider_account=provider_accounts[0], type=circuittypes[0]),
Circuit(cid='Circuit 3', provider=providers[0], provider_account=provider_accounts[0], type=circuittypes[0]),
Circuit(
cid='Circuit 1', provider=providers[0], provider_account=provider_accounts[0], type=circuittypes[0]
),
Circuit(
cid='Circuit 2', provider=providers[0], provider_account=provider_accounts[0], type=circuittypes[0]
),
Circuit(
cid='Circuit 3', provider=providers[0], provider_account=provider_accounts[0], type=circuittypes[0]
),
)
Circuit.objects.bulk_create(circuits)
@ -190,27 +198,31 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[])
def test_bulk_import_objects_with_terminations(self):
json_data = """
site = Site.objects.first()
json_data = f"""
[
{
{{
"cid": "Circuit 7",
"provider": "Provider 1",
"type": "Circuit Type 1",
"status": "active",
"description": "Testing Import",
"terminations": [
{
{{
"term_side": "A",
"site": "Site 1"
},
{
"termination_type": "dcim.site",
"termination_id": "{site.pk}"
}},
{{
"term_side": "Z",
"site": "Site 1"
}
"termination_type": "dcim.site",
"termination_id": "{site.pk}"
}}
]
}
}}
]
"""
initial_count = self._get_queryset().count()
data = {
'data': json_data,
@ -227,10 +239,10 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
# Try GET with model-level permission
self.assertHttpStatus(self.client.get(self._get_url('import')), 200)
self.assertHttpStatus(self.client.get(self._get_url('bulk_import')), 200)
# Test POST with permission
self.assertHttpStatus(self.client.post(self._get_url('import'), data), 302)
self.assertHttpStatus(self.client.post(self._get_url('bulk_import'), data), 302)
self.assertEqual(self._get_queryset().count(), initial_count + 1)
@ -359,24 +371,27 @@ class CircuitTerminationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
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(circuit=circuits[0], term_side='A', termination=sites[0]),
CircuitTermination(circuit=circuits[0], term_side='Z', termination=sites[1]),
CircuitTermination(circuit=circuits[1], term_side='A', termination=sites[0]),
CircuitTermination(circuit=circuits[1], term_side='Z', termination=sites[1]),
)
CircuitTermination.objects.bulk_create(circuit_terminations)
for ct in circuit_terminations:
ct.save()
cls.form_data = {
'circuit': circuits[2].pk,
'term_side': 'A',
'site': sites[2].pk,
'termination_type': ContentType.objects.get_for_model(Site).pk,
'termination': sites[2].pk,
'description': 'New description',
}
site = sites[0].pk
cls.csv_data = (
"circuit,term_side,site,description",
"Circuit 3,A,Site 1,Foo",
"Circuit 3,Z,Site 1,Bar",
"circuit,term_side,termination_type,termination_id,description",
f"Circuit 3,A,dcim.site,{site},Foo",
f"Circuit 3,Z,dcim.site,{site},Bar",
)
cls.csv_update_data = (
@ -453,6 +468,7 @@ class CircuitGroupAssignmentTestCase(
ViewTestCases.DeleteObjectViewTestCase,
ViewTestCases.ListObjectsViewTestCase,
ViewTestCases.BulkEditObjectsViewTestCase,
ViewTestCases.BulkImportObjectsViewTestCase,
ViewTestCases.BulkDeleteObjectsViewTestCase
):
model = CircuitGroupAssignment
@ -482,17 +498,17 @@ class CircuitGroupAssignmentTestCase(
assignments = (
CircuitGroupAssignment(
group=circuit_groups[0],
circuit=circuits[0],
member=circuits[0],
priority=CircuitPriorityChoices.PRIORITY_PRIMARY
),
CircuitGroupAssignment(
group=circuit_groups[1],
circuit=circuits[1],
member=circuits[1],
priority=CircuitPriorityChoices.PRIORITY_SECONDARY
),
CircuitGroupAssignment(
group=circuit_groups[2],
circuit=circuits[2],
member=circuits[2],
priority=CircuitPriorityChoices.PRIORITY_TERTIARY
),
)
@ -502,11 +518,420 @@ class CircuitGroupAssignmentTestCase(
cls.form_data = {
'group': circuit_groups[3].pk,
'circuit': circuits[3].pk,
'member_type': ContentType.objects.get_for_model(Circuit).pk,
'member': circuits[3].pk,
'priority': CircuitPriorityChoices.PRIORITY_INACTIVE,
'tags': [t.pk for t in tags],
}
cls.csv_data = (
"member_type,member_id,group,priority",
f"circuits.circuit,{circuits[0].pk},{circuit_groups[3].pk},primary",
f"circuits.circuit,{circuits[1].pk},{circuit_groups[3].pk},secondary",
f"circuits.circuit,{circuits[2].pk},{circuit_groups[3].pk},tertiary",
)
cls.csv_update_data = (
"id,priority",
f"{assignments[0].pk},inactive",
f"{assignments[1].pk},inactive",
f"{assignments[2].pk},inactive",
)
cls.bulk_edit_data = {
'priority': CircuitPriorityChoices.PRIORITY_INACTIVE,
}
class VirtualCircuitTypeTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
model = VirtualCircuitType
@classmethod
def setUpTestData(cls):
virtual_circuit_types = (
VirtualCircuitType(name='Virtual Circuit Type 1', slug='circuit-type-1'),
VirtualCircuitType(name='Virtual Circuit Type 2', slug='circuit-type-2'),
VirtualCircuitType(name='Virtual Circuit Type 3', slug='circuit-type-3'),
)
VirtualCircuitType.objects.bulk_create(virtual_circuit_types)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'name': 'Virtual Circuit Type X',
'slug': 'virtual-circuit-type-x',
'description': 'A new virtual circuit type',
'tags': [t.pk for t in tags],
}
cls.csv_data = (
"name,slug",
"Virtual Circuit Type 4,circuit-type-4",
"Virtual Circuit Type 5,circuit-type-5",
"Virtual Circuit Type 6,circuit-type-6",
)
cls.csv_update_data = (
"id,name,description",
f"{virtual_circuit_types[0].pk},Virtual Circuit Type 7,New description7",
f"{virtual_circuit_types[1].pk},Virtual Circuit Type 8,New description8",
f"{virtual_circuit_types[2].pk},Virtual Circuit Type 9,New description9",
)
cls.bulk_edit_data = {
'description': 'Foo',
}
class VirtualCircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = VirtualCircuit
def setUp(self):
super().setUp()
self.add_permissions(
'circuits.add_virtualcircuittermination',
)
@classmethod
def setUpTestData(cls):
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
provider_networks = (
ProviderNetwork(provider=provider, name='Provider Network 1'),
ProviderNetwork(provider=provider, name='Provider Network 2'),
)
ProviderNetwork.objects.bulk_create(provider_networks)
provider_accounts = (
ProviderAccount(provider=provider, account='Provider Account 1'),
ProviderAccount(provider=provider, account='Provider Account 2'),
)
ProviderAccount.objects.bulk_create(provider_accounts)
virtual_circuit_types = (
VirtualCircuitType(name='Virtual Circuit Type 1', slug='virtual-circuit-type-1'),
VirtualCircuitType(name='Virtual Circuit Type 2', slug='virtual-circuit-type-2'),
)
VirtualCircuitType.objects.bulk_create(virtual_circuit_types)
virtual_circuits = (
VirtualCircuit(
provider_network=provider_networks[0],
provider_account=provider_accounts[0],
cid='Virtual Circuit 1',
type=virtual_circuit_types[0]
),
VirtualCircuit(
provider_network=provider_networks[0],
provider_account=provider_accounts[0],
cid='Virtual Circuit 2',
type=virtual_circuit_types[0]
),
VirtualCircuit(
provider_network=provider_networks[0],
provider_account=provider_accounts[0],
cid='Virtual Circuit 3',
type=virtual_circuit_types[0]
),
)
VirtualCircuit.objects.bulk_create(virtual_circuits)
device = create_test_device('Device 1')
interfaces = (
Interface(device=device, name='Interface 1', type=InterfaceTypeChoices.TYPE_VIRTUAL),
Interface(device=device, name='Interface 2', type=InterfaceTypeChoices.TYPE_VIRTUAL),
Interface(device=device, name='Interface 3', type=InterfaceTypeChoices.TYPE_VIRTUAL),
)
Interface.objects.bulk_create(interfaces)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'cid': 'Virtual Circuit X',
'provider_network': provider_networks[1].pk,
'provider_account': provider_accounts[1].pk,
'type': virtual_circuit_types[1].pk,
'status': CircuitStatusChoices.STATUS_PLANNED,
'description': 'A new virtual circuit',
'comments': 'Some comments',
'tags': [t.pk for t in tags],
}
cls.csv_data = (
"cid,provider_network,provider_account,type,status",
(
f"Virtual Circuit 4,Provider Network 1,Provider Account 1,{virtual_circuit_types[0].name},"
f"{CircuitStatusChoices.STATUS_PLANNED}"
),
(
f"Virtual Circuit 5,Provider Network 1,Provider Account 1,{virtual_circuit_types[0].name},"
f"{CircuitStatusChoices.STATUS_PLANNED}"
),
(
f"Virtual Circuit 6,Provider Network 1,Provider Account 1,{virtual_circuit_types[0].name},"
f"{CircuitStatusChoices.STATUS_PLANNED}"
),
)
cls.csv_update_data = (
"id,cid,description,type,status",
(
f"{virtual_circuits[0].pk},Virtual Circuit A,New description,{virtual_circuit_types[1].name},"
f"{CircuitStatusChoices.STATUS_DECOMMISSIONED}"
),
(
f"{virtual_circuits[1].pk},Virtual Circuit B,New description,{virtual_circuit_types[1].name},"
f"{CircuitStatusChoices.STATUS_DECOMMISSIONED}"
),
(
f"{virtual_circuits[2].pk},Virtual Circuit C,New description,{virtual_circuit_types[1].name},"
f"{CircuitStatusChoices.STATUS_DECOMMISSIONED}"
),
)
cls.bulk_edit_data = {
'provider_network': provider_networks[1].pk,
'provider_account': provider_accounts[1].pk,
'type': virtual_circuit_types[1].pk,
'status': CircuitStatusChoices.STATUS_DECOMMISSIONED,
'description': 'New description',
'comments': 'New comments',
}
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[])
def test_bulk_import_objects_with_terminations(self):
interfaces = Interface.objects.filter(type=InterfaceTypeChoices.TYPE_VIRTUAL)
json_data = f"""
[
{{
"cid": "Virtual Circuit 7",
"provider_network": "Provider Network 1",
"type": "Virtual Circuit Type 1",
"status": "active",
"terminations": [
{{
"role": "hub",
"interface": {interfaces[0].pk}
}},
{{
"role": "spoke",
"interface": {interfaces[1].pk}
}},
{{
"role": "spoke",
"interface": {interfaces[2].pk}
}}
]
}}
]
"""
initial_count = self._get_queryset().count()
data = {
'data': json_data,
'format': ImportFormatChoices.JSON,
}
# Assign model-level permission
obj_perm = ObjectPermission(
name='Test permission',
actions=['add']
)
obj_perm.save()
obj_perm.users.add(self.user)
obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
# Try GET with model-level permission
self.assertHttpStatus(self.client.get(self._get_url('bulk_import')), 200)
# Test POST with permission
self.assertHttpStatus(self.client.post(self._get_url('bulk_import'), data), 302)
self.assertEqual(self._get_queryset().count(), initial_count + 1)
class VirtualCircuitTerminationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = VirtualCircuitTermination
@classmethod
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1')
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
site = Site.objects.create(name='Site 1', slug='site-1')
devices = (
Device(site=site, name='hub', device_type=device_type, role=device_role),
Device(site=site, name='spoke1', device_type=device_type, role=device_role),
Device(site=site, name='spoke2', device_type=device_type, role=device_role),
Device(site=site, name='spoke3', device_type=device_type, role=device_role),
)
Device.objects.bulk_create(devices)
physical_interfaces = (
Interface(device=devices[0], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
Interface(device=devices[1], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
Interface(device=devices[2], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
Interface(device=devices[3], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
)
Interface.objects.bulk_create(physical_interfaces)
virtual_interfaces = (
# Point-to-point VCs
Interface(
device=devices[0],
name='eth0.1',
parent=physical_interfaces[0],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[0],
name='eth0.2',
parent=physical_interfaces[0],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[0],
name='eth0.3',
parent=physical_interfaces[0],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[1],
name='eth0.1',
parent=physical_interfaces[1],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[2],
name='eth0.1',
parent=physical_interfaces[2],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[3],
name='eth0.1',
parent=physical_interfaces[3],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
# Hub and spoke VCs
Interface(
device=devices[0],
name='eth0.9',
parent=physical_interfaces[0],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[1],
name='eth0.9',
parent=physical_interfaces[0],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[2],
name='eth0.9',
parent=physical_interfaces[0],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[3],
name='eth0.9',
parent=physical_interfaces[0],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
)
Interface.objects.bulk_create(virtual_interfaces)
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
provider_network = ProviderNetwork.objects.create(provider=provider, name='Provider Network 1')
provider_account = ProviderAccount.objects.create(provider=provider, account='Provider Account 1')
virtual_circuit_type = VirtualCircuitType.objects.create(
name='Virtual Circuit Type 1',
slug='virtual-circuit-type-1'
)
virtual_circuits = (
VirtualCircuit(
provider_network=provider_network,
provider_account=provider_account,
cid='Virtual Circuit 1',
type=virtual_circuit_type
),
VirtualCircuit(
provider_network=provider_network,
provider_account=provider_account,
cid='Virtual Circuit 2',
type=virtual_circuit_type
),
VirtualCircuit(
provider_network=provider_network,
provider_account=provider_account,
cid='Virtual Circuit 3',
type=virtual_circuit_type
),
VirtualCircuit(
provider_network=provider_network,
provider_account=provider_account,
cid='Virtual Circuit 4',
type=virtual_circuit_type
),
)
VirtualCircuit.objects.bulk_create(virtual_circuits)
virtual_circuit_terminations = (
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[0],
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
interface=virtual_interfaces[0]
),
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[0],
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
interface=virtual_interfaces[3]
),
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[1],
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
interface=virtual_interfaces[1]
),
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[1],
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
interface=virtual_interfaces[4]
),
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[2],
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
interface=virtual_interfaces[2]
),
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[2],
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
interface=virtual_interfaces[5]
),
)
VirtualCircuitTermination.objects.bulk_create(virtual_circuit_terminations)
cls.form_data = {
'virtual_circuit': virtual_circuits[3].pk,
'role': VirtualCircuitTerminationRoleChoices.ROLE_HUB,
'interface': virtual_interfaces[6].pk
}
cls.csv_data = (
"virtual_circuit,role,interface,description",
f"Virtual Circuit 4,{VirtualCircuitTerminationRoleChoices.ROLE_HUB},{virtual_interfaces[6].pk},Hub",
f"Virtual Circuit 4,{VirtualCircuitTerminationRoleChoices.ROLE_SPOKE},{virtual_interfaces[7].pk},Spoke 1",
f"Virtual Circuit 4,{VirtualCircuitTerminationRoleChoices.ROLE_SPOKE},{virtual_interfaces[8].pk},Spoke 2",
f"Virtual Circuit 4,{VirtualCircuitTerminationRoleChoices.ROLE_SPOKE},{virtual_interfaces[9].pk},Spoke 3",
)
cls.csv_update_data = (
"id,role,description",
f"{virtual_circuit_terminations[0].pk},{VirtualCircuitTerminationRoleChoices.ROLE_SPOKE},New description",
f"{virtual_circuit_terminations[1].pk},{VirtualCircuitTerminationRoleChoices.ROLE_SPOKE},New description",
f"{virtual_circuit_terminations[2].pk},{VirtualCircuitTerminationRoleChoices.ROLE_SPOKE},New description",
)
cls.bulk_edit_data = {
'description': 'New description',
}

View File

@ -5,69 +5,71 @@ from . import views
app_name = 'circuits'
urlpatterns = [
# Providers
path('providers/', views.ProviderListView.as_view(), name='provider_list'),
path('providers/add/', views.ProviderEditView.as_view(), name='provider_add'),
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/', include(get_model_urls('circuits', 'provider', detail=False))),
path('providers/<int:pk>/', include(get_model_urls('circuits', 'provider'))),
# Provider accounts
path('provider-accounts/', views.ProviderAccountListView.as_view(), name='provideraccount_list'),
path('provider-accounts/add/', views.ProviderAccountEditView.as_view(), name='provideraccount_add'),
path('provider-accounts/import/', views.ProviderAccountBulkImportView.as_view(), name='provideraccount_import'),
path('provider-accounts/edit/', views.ProviderAccountBulkEditView.as_view(), name='provideraccount_bulk_edit'),
path('provider-accounts/delete/', views.ProviderAccountBulkDeleteView.as_view(), name='provideraccount_bulk_delete'),
path('provider-accounts/', include(get_model_urls('circuits', 'provideraccount', detail=False))),
path('provider-accounts/<int:pk>/', include(get_model_urls('circuits', 'provideraccount'))),
# 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/', include(get_model_urls('circuits', 'providernetwork', detail=False))),
path('provider-networks/<int:pk>/', include(get_model_urls('circuits', '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/', include(get_model_urls('circuits', 'circuittype', detail=False))),
path('circuit-types/<int:pk>/', include(get_model_urls('circuits', 'circuittype'))),
# Circuits
path('circuits/', views.CircuitListView.as_view(), name='circuit_list'),
path('circuits/add/', views.CircuitEditView.as_view(), name='circuit_add'),
path('circuits/import/', views.CircuitBulkImportView.as_view(), name='circuit_import'),
path('circuits/edit/', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'),
path('circuits/delete/', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'),
path('circuits/<int:pk>/terminations/swap/', views.CircuitSwapTerminations.as_view(), name='circuit_terminations_swap'),
path('circuits/', include(get_model_urls('circuits', 'circuit', detail=False))),
path(
'circuits/<int:pk>/terminations/swap/',
views.CircuitSwapTerminations.as_view(),
name='circuit_terminations_swap'
),
path('circuits/<int:pk>/', include(get_model_urls('circuits', 'circuit'))),
# Circuit terminations
path('circuit-terminations/', views.CircuitTerminationListView.as_view(), name='circuittermination_list'),
path('circuit-terminations/add/', views.CircuitTerminationEditView.as_view(), name='circuittermination_add'),
path('circuit-terminations/import/', views.CircuitTerminationBulkImportView.as_view(), name='circuittermination_import'),
path('circuit-terminations/edit/', views.CircuitTerminationBulkEditView.as_view(), name='circuittermination_bulk_edit'),
path('circuit-terminations/delete/', views.CircuitTerminationBulkDeleteView.as_view(), name='circuittermination_bulk_delete'),
path('circuit-terminations/', include(get_model_urls('circuits', 'circuittermination', detail=False))),
path('circuit-terminations/<int:pk>/', include(get_model_urls('circuits', 'circuittermination'))),
# Circuit Groups
path('circuit-groups/', views.CircuitGroupListView.as_view(), name='circuitgroup_list'),
path('circuit-groups/add/', views.CircuitGroupEditView.as_view(), name='circuitgroup_add'),
path('circuit-groups/import/', views.CircuitGroupBulkImportView.as_view(), name='circuitgroup_import'),
path('circuit-groups/edit/', views.CircuitGroupBulkEditView.as_view(), name='circuitgroup_bulk_edit'),
path('circuit-groups/delete/', views.CircuitGroupBulkDeleteView.as_view(), name='circuitgroup_bulk_delete'),
path('circuit-groups/', include(get_model_urls('circuits', 'circuitgroup', detail=False))),
path('circuit-groups/<int:pk>/', include(get_model_urls('circuits', 'circuitgroup'))),
# Circuit Group Assignments
path('circuit-group-assignments/', views.CircuitGroupAssignmentListView.as_view(), name='circuitgroupassignment_list'),
path('circuit-group-assignments/add/', views.CircuitGroupAssignmentEditView.as_view(), name='circuitgroupassignment_add'),
path('circuit-group-assignments/import/', views.CircuitGroupAssignmentBulkImportView.as_view(), name='circuitgroupassignment_import'),
path('circuit-group-assignments/edit/', views.CircuitGroupAssignmentBulkEditView.as_view(), name='circuitgroupassignment_bulk_edit'),
path('circuit-group-assignments/delete/', views.CircuitGroupAssignmentBulkDeleteView.as_view(), name='circuitgroupassignment_bulk_delete'),
path('circuit-group-assignments/', include(get_model_urls('circuits', 'circuitgroupassignment', detail=False))),
path('circuit-group-assignments/<int:pk>/', include(get_model_urls('circuits', 'circuitgroupassignment'))),
# Virtual circuits
path('virtual-circuits/', views.VirtualCircuitListView.as_view(), name='virtualcircuit_list'),
path('virtual-circuits/add/', views.VirtualCircuitEditView.as_view(), name='virtualcircuit_add'),
path('virtual-circuits/import/', views.VirtualCircuitBulkImportView.as_view(), name='virtualcircuit_bulk_import'),
path('virtual-circuits/edit/', views.VirtualCircuitBulkEditView.as_view(), name='virtualcircuit_bulk_edit'),
path('virtual-circuits/delete/', views.VirtualCircuitBulkDeleteView.as_view(), name='virtualcircuit_bulk_delete'),
path('virtual-circuits/<int:pk>/', include(get_model_urls('circuits', 'virtualcircuit'))),
path('virtual-circuit-types/', include(get_model_urls('circuits', 'virtualcircuittype', detail=False))),
path('virtual-circuit-types/<int:pk>/', include(get_model_urls('circuits', 'virtualcircuittype'))),
# Virtual circuit terminations
path(
'virtual-circuit-terminations/',
views.VirtualCircuitTerminationListView.as_view(),
name='virtualcircuittermination_list',
),
path(
'virtual-circuit-terminations/add/',
views.VirtualCircuitTerminationEditView.as_view(),
name='virtualcircuittermination_add',
),
path(
'virtual-circuit-terminations/import/',
views.VirtualCircuitTerminationBulkImportView.as_view(),
name='virtualcircuittermination_bulk_import',
),
path(
'virtual-circuit-terminations/edit/',
views.VirtualCircuitTerminationBulkEditView.as_view(),
name='virtualcircuittermination_bulk_edit',
),
path(
'virtual-circuit-terminations/delete/',
views.VirtualCircuitTerminationBulkDeleteView.as_view(),
name='virtualcircuittermination_bulk_delete',
),
path('virtual-circuit-terminations/<int:pk>/', include(get_model_urls('circuits', 'virtualcircuittermination'))),
]

View File

@ -17,6 +17,7 @@ from .models import *
# Providers
#
@register_model_view(Provider, 'list', path='', detail=False)
class ProviderListView(generic.ObjectListView):
queryset = Provider.objects.annotate(
count_circuits=count_related(Circuit, 'provider')
@ -36,6 +37,7 @@ class ProviderView(GetRelatedModelsMixin, generic.ObjectView):
}
@register_model_view(Provider, 'add', detail=False)
@register_model_view(Provider, 'edit')
class ProviderEditView(generic.ObjectEditView):
queryset = Provider.objects.all()
@ -47,11 +49,13 @@ class ProviderDeleteView(generic.ObjectDeleteView):
queryset = Provider.objects.all()
@register_model_view(Provider, 'bulk_import', detail=False)
class ProviderBulkImportView(generic.BulkImportView):
queryset = Provider.objects.all()
model_form = forms.ProviderImportForm
@register_model_view(Provider, 'bulk_edit', path='edit', detail=False)
class ProviderBulkEditView(generic.BulkEditView):
queryset = Provider.objects.annotate(
count_circuits=count_related(Circuit, 'provider')
@ -61,6 +65,7 @@ class ProviderBulkEditView(generic.BulkEditView):
form = forms.ProviderBulkEditForm
@register_model_view(Provider, 'bulk_delete', path='delete', detail=False)
class ProviderBulkDeleteView(generic.BulkDeleteView):
queryset = Provider.objects.annotate(
count_circuits=count_related(Circuit, 'provider')
@ -78,6 +83,7 @@ class ProviderContactsView(ObjectContactsView):
# ProviderAccounts
#
@register_model_view(ProviderAccount, 'list', path='', detail=False)
class ProviderAccountListView(generic.ObjectListView):
queryset = ProviderAccount.objects.annotate(
count_circuits=count_related(Circuit, 'provider_account')
@ -97,6 +103,7 @@ class ProviderAccountView(GetRelatedModelsMixin, generic.ObjectView):
}
@register_model_view(ProviderAccount, 'add', detail=False)
@register_model_view(ProviderAccount, 'edit')
class ProviderAccountEditView(generic.ObjectEditView):
queryset = ProviderAccount.objects.all()
@ -108,12 +115,14 @@ class ProviderAccountDeleteView(generic.ObjectDeleteView):
queryset = ProviderAccount.objects.all()
@register_model_view(ProviderAccount, 'bulk_import', detail=False)
class ProviderAccountBulkImportView(generic.BulkImportView):
queryset = ProviderAccount.objects.all()
model_form = forms.ProviderAccountImportForm
table = tables.ProviderAccountTable
@register_model_view(ProviderAccount, 'bulk_edit', path='edit', detail=False)
class ProviderAccountBulkEditView(generic.BulkEditView):
queryset = ProviderAccount.objects.annotate(
count_circuits=count_related(Circuit, 'provider_account')
@ -123,6 +132,7 @@ class ProviderAccountBulkEditView(generic.BulkEditView):
form = forms.ProviderAccountBulkEditForm
@register_model_view(ProviderAccount, 'bulk_delete', path='delete', detail=False)
class ProviderAccountBulkDeleteView(generic.BulkDeleteView):
queryset = ProviderAccount.objects.annotate(
count_circuits=count_related(Circuit, 'provider_account')
@ -140,6 +150,7 @@ class ProviderAccountContactsView(ObjectContactsView):
# Provider networks
#
@register_model_view(ProviderNetwork, 'list', path='', detail=False)
class ProviderNetworkListView(generic.ObjectListView):
queryset = ProviderNetwork.objects.all()
filterset = filtersets.ProviderNetworkFilterSet
@ -158,7 +169,7 @@ class ProviderNetworkView(GetRelatedModelsMixin, generic.ObjectView):
instance,
extra=(
(
Circuit.objects.restrict(request.user, 'view').filter(terminations__provider_network=instance),
Circuit.objects.restrict(request.user, 'view').filter(terminations___provider_network=instance),
'provider_network_id',
),
),
@ -166,6 +177,7 @@ class ProviderNetworkView(GetRelatedModelsMixin, generic.ObjectView):
}
@register_model_view(ProviderNetwork, 'add', detail=False)
@register_model_view(ProviderNetwork, 'edit')
class ProviderNetworkEditView(generic.ObjectEditView):
queryset = ProviderNetwork.objects.all()
@ -177,11 +189,13 @@ class ProviderNetworkDeleteView(generic.ObjectDeleteView):
queryset = ProviderNetwork.objects.all()
@register_model_view(ProviderNetwork, 'bulk_import', detail=False)
class ProviderNetworkBulkImportView(generic.BulkImportView):
queryset = ProviderNetwork.objects.all()
model_form = forms.ProviderNetworkImportForm
@register_model_view(ProviderNetwork, 'bulk_edit', path='edit', detail=False)
class ProviderNetworkBulkEditView(generic.BulkEditView):
queryset = ProviderNetwork.objects.all()
filterset = filtersets.ProviderNetworkFilterSet
@ -189,6 +203,7 @@ class ProviderNetworkBulkEditView(generic.BulkEditView):
form = forms.ProviderNetworkBulkEditForm
@register_model_view(ProviderNetwork, 'bulk_delete', path='delete', detail=False)
class ProviderNetworkBulkDeleteView(generic.BulkDeleteView):
queryset = ProviderNetwork.objects.all()
filterset = filtersets.ProviderNetworkFilterSet
@ -199,6 +214,7 @@ class ProviderNetworkBulkDeleteView(generic.BulkDeleteView):
# Circuit Types
#
@register_model_view(CircuitType, 'list', path='', detail=False)
class CircuitTypeListView(generic.ObjectListView):
queryset = CircuitType.objects.annotate(
circuit_count=count_related(Circuit, 'type')
@ -218,6 +234,7 @@ class CircuitTypeView(GetRelatedModelsMixin, generic.ObjectView):
}
@register_model_view(CircuitType, 'add', detail=False)
@register_model_view(CircuitType, 'edit')
class CircuitTypeEditView(generic.ObjectEditView):
queryset = CircuitType.objects.all()
@ -229,11 +246,13 @@ class CircuitTypeDeleteView(generic.ObjectDeleteView):
queryset = CircuitType.objects.all()
@register_model_view(CircuitType, 'bulk_import', detail=False)
class CircuitTypeBulkImportView(generic.BulkImportView):
queryset = CircuitType.objects.all()
model_form = forms.CircuitTypeImportForm
@register_model_view(CircuitType, 'bulk_edit', path='edit', detail=False)
class CircuitTypeBulkEditView(generic.BulkEditView):
queryset = CircuitType.objects.annotate(
circuit_count=count_related(Circuit, 'type')
@ -243,6 +262,7 @@ class CircuitTypeBulkEditView(generic.BulkEditView):
form = forms.CircuitTypeBulkEditForm
@register_model_view(CircuitType, 'bulk_delete', path='delete', detail=False)
class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
queryset = CircuitType.objects.annotate(
circuit_count=count_related(Circuit, 'type')
@ -255,10 +275,10 @@ class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
# Circuits
#
@register_model_view(Circuit, 'list', path='', detail=False)
class CircuitListView(generic.ObjectListView):
queryset = Circuit.objects.prefetch_related(
'tenant__group', 'termination_a__site', 'termination_z__site',
'termination_a__provider_network', 'termination_z__provider_network',
'tenant__group', 'termination_a__termination', 'termination_z__termination',
)
filterset = filtersets.CircuitFilterSet
filterset_form = forms.CircuitFilterForm
@ -270,6 +290,7 @@ class CircuitView(generic.ObjectView):
queryset = Circuit.objects.all()
@register_model_view(Circuit, 'add', detail=False)
@register_model_view(Circuit, 'edit')
class CircuitEditView(generic.ObjectEditView):
queryset = Circuit.objects.all()
@ -281,6 +302,7 @@ class CircuitDeleteView(generic.ObjectDeleteView):
queryset = Circuit.objects.all()
@register_model_view(Circuit, 'bulk_import', detail=False)
class CircuitBulkImportView(generic.BulkImportView):
queryset = Circuit.objects.all()
model_form = forms.CircuitImportForm
@ -296,20 +318,20 @@ class CircuitBulkImportView(generic.BulkImportView):
return data
@register_model_view(Circuit, 'bulk_edit', path='edit', detail=False)
class CircuitBulkEditView(generic.BulkEditView):
queryset = Circuit.objects.prefetch_related(
'termination_a__site', 'termination_z__site',
'termination_a__provider_network', 'termination_z__provider_network',
'tenant__group', 'termination_a__termination', 'termination_z__termination',
)
filterset = filtersets.CircuitFilterSet
table = tables.CircuitTable
form = forms.CircuitBulkEditForm
@register_model_view(Circuit, 'bulk_delete', path='delete', detail=False)
class CircuitBulkDeleteView(generic.BulkDeleteView):
queryset = Circuit.objects.prefetch_related(
'termination_a__site', 'termination_z__site',
'termination_a__provider_network', 'termination_z__provider_network',
'tenant__group', 'termination_a__termination', 'termination_z__termination',
)
filterset = filtersets.CircuitFilterSet
table = tables.CircuitTable
@ -400,6 +422,7 @@ class CircuitContactsView(ObjectContactsView):
# Circuit terminations
#
@register_model_view(CircuitTermination, 'list', path='', detail=False)
class CircuitTerminationListView(generic.ObjectListView):
queryset = CircuitTermination.objects.all()
filterset = filtersets.CircuitTerminationFilterSet
@ -412,6 +435,7 @@ class CircuitTerminationView(generic.ObjectView):
queryset = CircuitTermination.objects.all()
@register_model_view(CircuitTermination, 'add', detail=False)
@register_model_view(CircuitTermination, 'edit')
class CircuitTerminationEditView(generic.ObjectEditView):
queryset = CircuitTermination.objects.all()
@ -423,11 +447,13 @@ class CircuitTerminationDeleteView(generic.ObjectDeleteView):
queryset = CircuitTermination.objects.all()
@register_model_view(CircuitTermination, 'bulk_import', detail=False)
class CircuitTerminationBulkImportView(generic.BulkImportView):
queryset = CircuitTermination.objects.all()
model_form = forms.CircuitTerminationImportForm
@register_model_view(CircuitTermination, 'bulk_edit', path='edit', detail=False)
class CircuitTerminationBulkEditView(generic.BulkEditView):
queryset = CircuitTermination.objects.all()
filterset = filtersets.CircuitTerminationFilterSet
@ -435,6 +461,7 @@ class CircuitTerminationBulkEditView(generic.BulkEditView):
form = forms.CircuitTerminationBulkEditForm
@register_model_view(CircuitTermination, 'bulk_delete', path='delete', detail=False)
class CircuitTerminationBulkDeleteView(generic.BulkDeleteView):
queryset = CircuitTermination.objects.all()
filterset = filtersets.CircuitTerminationFilterSet
@ -449,6 +476,7 @@ register_model_view(CircuitTermination, 'trace', kwargs={'model': CircuitTermina
# Circuit Groups
#
@register_model_view(CircuitGroup, 'list', path='', detail=False)
class CircuitGroupListView(generic.ObjectListView):
queryset = CircuitGroup.objects.annotate(
circuit_group_assignment_count=count_related(CircuitGroupAssignment, 'group')
@ -468,6 +496,7 @@ class CircuitGroupView(GetRelatedModelsMixin, generic.ObjectView):
}
@register_model_view(CircuitGroup, 'add', detail=False)
@register_model_view(CircuitGroup, 'edit')
class CircuitGroupEditView(generic.ObjectEditView):
queryset = CircuitGroup.objects.all()
@ -479,11 +508,13 @@ class CircuitGroupDeleteView(generic.ObjectDeleteView):
queryset = CircuitGroup.objects.all()
@register_model_view(CircuitGroup, 'bulk_import', detail=False)
class CircuitGroupBulkImportView(generic.BulkImportView):
queryset = CircuitGroup.objects.all()
model_form = forms.CircuitGroupImportForm
@register_model_view(CircuitGroup, 'bulk_edit', path='edit', detail=False)
class CircuitGroupBulkEditView(generic.BulkEditView):
queryset = CircuitGroup.objects.all()
filterset = filtersets.CircuitGroupFilterSet
@ -491,6 +522,7 @@ class CircuitGroupBulkEditView(generic.BulkEditView):
form = forms.CircuitGroupBulkEditForm
@register_model_view(CircuitGroup, 'bulk_delete', path='delete', detail=False)
class CircuitGroupBulkDeleteView(generic.BulkDeleteView):
queryset = CircuitGroup.objects.all()
filterset = filtersets.CircuitGroupFilterSet
@ -501,6 +533,7 @@ class CircuitGroupBulkDeleteView(generic.BulkDeleteView):
# Circuit Groups
#
@register_model_view(CircuitGroupAssignment, 'list', path='', detail=False)
class CircuitGroupAssignmentListView(generic.ObjectListView):
queryset = CircuitGroupAssignment.objects.all()
filterset = filtersets.CircuitGroupAssignmentFilterSet
@ -513,6 +546,7 @@ class CircuitGroupAssignmentView(generic.ObjectView):
queryset = CircuitGroupAssignment.objects.all()
@register_model_view(CircuitGroupAssignment, 'add', detail=False)
@register_model_view(CircuitGroupAssignment, 'edit')
class CircuitGroupAssignmentEditView(generic.ObjectEditView):
queryset = CircuitGroupAssignment.objects.all()
@ -524,11 +558,13 @@ class CircuitGroupAssignmentDeleteView(generic.ObjectDeleteView):
queryset = CircuitGroupAssignment.objects.all()
@register_model_view(CircuitGroupAssignment, 'bulk_import', detail=False)
class CircuitGroupAssignmentBulkImportView(generic.BulkImportView):
queryset = CircuitGroupAssignment.objects.all()
model_form = forms.CircuitGroupAssignmentImportForm
@register_model_view(CircuitGroupAssignment, 'bulk_edit', path='edit', detail=False)
class CircuitGroupAssignmentBulkEditView(generic.BulkEditView):
queryset = CircuitGroupAssignment.objects.all()
filterset = filtersets.CircuitGroupAssignmentFilterSet
@ -536,7 +572,175 @@ class CircuitGroupAssignmentBulkEditView(generic.BulkEditView):
form = forms.CircuitGroupAssignmentBulkEditForm
@register_model_view(CircuitGroupAssignment, 'bulk_delete', path='delete', detail=False)
class CircuitGroupAssignmentBulkDeleteView(generic.BulkDeleteView):
queryset = CircuitGroupAssignment.objects.all()
filterset = filtersets.CircuitGroupAssignmentFilterSet
table = tables.CircuitGroupAssignmentTable
#
# Virtual circuit Types
#
@register_model_view(VirtualCircuitType, 'list', path='', detail=False)
class VirtualCircuitTypeListView(generic.ObjectListView):
queryset = VirtualCircuitType.objects.annotate(
virtual_circuit_count=count_related(VirtualCircuit, 'type')
)
filterset = filtersets.VirtualCircuitTypeFilterSet
filterset_form = forms.VirtualCircuitTypeFilterForm
table = tables.VirtualCircuitTypeTable
@register_model_view(VirtualCircuitType)
class VirtualCircuitTypeView(GetRelatedModelsMixin, generic.ObjectView):
queryset = VirtualCircuitType.objects.all()
def get_extra_context(self, request, instance):
return {
'related_models': self.get_related_models(request, instance),
}
@register_model_view(VirtualCircuitType, 'add', detail=False)
@register_model_view(VirtualCircuitType, 'edit')
class VirtualCircuitTypeEditView(generic.ObjectEditView):
queryset = VirtualCircuitType.objects.all()
form = forms.VirtualCircuitTypeForm
@register_model_view(VirtualCircuitType, 'delete')
class VirtualCircuitTypeDeleteView(generic.ObjectDeleteView):
queryset = VirtualCircuitType.objects.all()
@register_model_view(VirtualCircuitType, 'bulk_import', detail=False)
class VirtualCircuitTypeBulkImportView(generic.BulkImportView):
queryset = VirtualCircuitType.objects.all()
model_form = forms.VirtualCircuitTypeImportForm
@register_model_view(VirtualCircuitType, 'bulk_edit', path='edit', detail=False)
class VirtualCircuitTypeBulkEditView(generic.BulkEditView):
queryset = VirtualCircuitType.objects.annotate(
circuit_count=count_related(Circuit, 'type')
)
filterset = filtersets.VirtualCircuitTypeFilterSet
table = tables.VirtualCircuitTypeTable
form = forms.VirtualCircuitTypeBulkEditForm
@register_model_view(VirtualCircuitType, 'bulk_delete', path='delete', detail=False)
class VirtualCircuitTypeBulkDeleteView(generic.BulkDeleteView):
queryset = VirtualCircuitType.objects.annotate(
circuit_count=count_related(Circuit, 'type')
)
filterset = filtersets.VirtualCircuitTypeFilterSet
table = tables.VirtualCircuitTypeTable
#
# Virtual circuits
#
class VirtualCircuitListView(generic.ObjectListView):
queryset = VirtualCircuit.objects.annotate(
termination_count=count_related(VirtualCircuitTermination, 'virtual_circuit')
)
filterset = filtersets.VirtualCircuitFilterSet
filterset_form = forms.VirtualCircuitFilterForm
table = tables.VirtualCircuitTable
@register_model_view(VirtualCircuit)
class VirtualCircuitView(generic.ObjectView):
queryset = VirtualCircuit.objects.all()
@register_model_view(VirtualCircuit, 'edit')
class VirtualCircuitEditView(generic.ObjectEditView):
queryset = VirtualCircuit.objects.all()
form = forms.VirtualCircuitForm
@register_model_view(VirtualCircuit, 'delete')
class VirtualCircuitDeleteView(generic.ObjectDeleteView):
queryset = VirtualCircuit.objects.all()
class VirtualCircuitBulkImportView(generic.BulkImportView):
queryset = VirtualCircuit.objects.all()
model_form = forms.VirtualCircuitImportForm
additional_permissions = [
'circuits.add_virtualcircuittermination',
]
related_object_forms = {
'terminations': forms.VirtualCircuitTerminationImportRelatedForm,
}
def prep_related_object_data(self, parent, data):
data.update({'virtual_circuit': parent})
return data
class VirtualCircuitBulkEditView(generic.BulkEditView):
queryset = VirtualCircuit.objects.annotate(
termination_count=count_related(VirtualCircuitTermination, 'virtual_circuit')
)
filterset = filtersets.VirtualCircuitFilterSet
table = tables.VirtualCircuitTable
form = forms.VirtualCircuitBulkEditForm
class VirtualCircuitBulkDeleteView(generic.BulkDeleteView):
queryset = VirtualCircuit.objects.annotate(
termination_count=count_related(VirtualCircuitTermination, 'virtual_circuit')
)
filterset = filtersets.VirtualCircuitFilterSet
table = tables.VirtualCircuitTable
#
# Virtual circuit terminations
#
class VirtualCircuitTerminationListView(generic.ObjectListView):
queryset = VirtualCircuitTermination.objects.all()
filterset = filtersets.VirtualCircuitTerminationFilterSet
filterset_form = forms.VirtualCircuitTerminationFilterForm
table = tables.VirtualCircuitTerminationTable
@register_model_view(VirtualCircuitTermination)
class VirtualCircuitTerminationView(generic.ObjectView):
queryset = VirtualCircuitTermination.objects.all()
@register_model_view(VirtualCircuitTermination, 'edit')
class VirtualCircuitTerminationEditView(generic.ObjectEditView):
queryset = VirtualCircuitTermination.objects.all()
form = forms.VirtualCircuitTerminationForm
@register_model_view(VirtualCircuitTermination, 'delete')
class VirtualCircuitTerminationDeleteView(generic.ObjectDeleteView):
queryset = VirtualCircuitTermination.objects.all()
class VirtualCircuitTerminationBulkImportView(generic.BulkImportView):
queryset = VirtualCircuitTermination.objects.all()
model_form = forms.VirtualCircuitTerminationImportForm
class VirtualCircuitTerminationBulkEditView(generic.BulkEditView):
queryset = VirtualCircuitTermination.objects.all()
filterset = filtersets.VirtualCircuitTerminationFilterSet
table = tables.VirtualCircuitTerminationTable
form = forms.VirtualCircuitTerminationBulkEditForm
class VirtualCircuitTerminationBulkDeleteView(generic.BulkDeleteView):
queryset = VirtualCircuitTermination.objects.all()
filterset = filtersets.VirtualCircuitTerminationFilterSet
table = tables.VirtualCircuitTerminationTable

View File

@ -1,47 +0,0 @@
import warnings
from rest_framework import serializers
from core.choices import JobStatusChoices
from core.models import *
from netbox.api.fields import ChoiceField
from netbox.api.serializers import WritableNestedSerializer
from users.api.serializers import UserSerializer
__all__ = (
'NestedDataFileSerializer',
'NestedDataSourceSerializer',
'NestedJobSerializer',
)
# TODO: Remove in v4.2
warnings.warn(
"Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
DeprecationWarning
)
class NestedDataSourceSerializer(WritableNestedSerializer):
class Meta:
model = DataSource
fields = ['id', 'url', 'display_url', 'display', 'name']
class NestedDataFileSerializer(WritableNestedSerializer):
class Meta:
model = DataFile
fields = ['id', 'url', 'display_url', 'display', 'path']
class NestedJobSerializer(serializers.ModelSerializer):
status = ChoiceField(choices=JobStatusChoices)
user = UserSerializer(
nested=True,
read_only=True
)
class Meta:
model = Job
fields = ['url', 'display_url', 'created', 'completed', 'user', 'status']

View File

@ -35,7 +35,10 @@ class ChoiceFieldFix(OpenApiSerializerFieldExtension):
elif direction == "response":
value = build_cf
label = {**build_basic_type(OpenApiTypes.STR), "enum": list(OrderedDict.fromkeys(self.target.choices.values()))}
label = {
**build_basic_type(OpenApiTypes.STR),
"enum": list(OrderedDict.fromkeys(self.target.choices.values()))
}
return build_object_type(
properties={
@ -155,6 +158,9 @@ class NetBoxAutoSchema(AutoSchema):
fields = {} if hasattr(serializer, 'child') else serializer.fields
remove_fields = []
# If you get a failure here for "AttributeError: 'cached_property' object has no attribute 'items'"
# it is probably because you are using a viewsets.ViewSet for the API View and are defining a
# serializer_class. You will also need to define a get_serializer() method like for GenericAPIView.
for child_name, child in fields.items():
# read_only fields don't need to be in writable (write only) serializers
if 'read_only' in dir(child) and child.read_only:

View File

@ -1,3 +1,4 @@
from .serializers_.change_logging import *
from .serializers_.data import *
from .serializers_.jobs import *
from .serializers_.tasks import *

View File

@ -22,7 +22,7 @@ class JobSerializer(BaseModelSerializer):
class Meta:
model = Job
fields = [
'id', 'url', 'display_url', 'display', 'object_type', 'object_id', 'name', 'status', 'created', 'scheduled', 'interval',
'started', 'completed', 'user', 'data', 'error', 'job_id',
'id', 'url', 'display_url', 'display', 'object_type', 'object_id', 'name', 'status', 'created', 'scheduled',
'interval', 'started', 'completed', 'user', 'data', 'error', 'job_id',
]
brief_fields = ('url', 'created', 'completed', 'user', 'status')

View File

@ -0,0 +1,87 @@
from rest_framework import serializers
from rest_framework.reverse import reverse
__all__ = (
'BackgroundTaskSerializer',
'BackgroundQueueSerializer',
'BackgroundWorkerSerializer',
)
class BackgroundTaskSerializer(serializers.Serializer):
id = serializers.CharField()
url = serializers.HyperlinkedIdentityField(
view_name='core-api:rqtask-detail',
lookup_field='id',
lookup_url_kwarg='pk'
)
description = serializers.CharField()
origin = serializers.CharField()
func_name = serializers.CharField()
args = serializers.ListField(child=serializers.CharField())
kwargs = serializers.DictField()
result = serializers.CharField()
timeout = serializers.IntegerField()
result_ttl = serializers.IntegerField()
created_at = serializers.DateTimeField()
enqueued_at = serializers.DateTimeField()
started_at = serializers.DateTimeField()
ended_at = serializers.DateTimeField()
worker_name = serializers.CharField()
position = serializers.SerializerMethodField()
status = serializers.SerializerMethodField()
meta = serializers.DictField()
last_heartbeat = serializers.CharField()
is_finished = serializers.BooleanField()
is_queued = serializers.BooleanField()
is_failed = serializers.BooleanField()
is_started = serializers.BooleanField()
is_deferred = serializers.BooleanField()
is_canceled = serializers.BooleanField()
is_scheduled = serializers.BooleanField()
is_stopped = serializers.BooleanField()
def get_position(self, obj) -> int:
return obj.get_position()
def get_status(self, obj) -> str:
return obj.get_status()
class BackgroundQueueSerializer(serializers.Serializer):
name = serializers.CharField()
url = serializers.SerializerMethodField()
jobs = serializers.IntegerField()
oldest_job_timestamp = serializers.CharField()
index = serializers.IntegerField()
scheduler_pid = serializers.CharField()
workers = serializers.IntegerField()
finished_jobs = serializers.IntegerField()
started_jobs = serializers.IntegerField()
deferred_jobs = serializers.IntegerField()
failed_jobs = serializers.IntegerField()
scheduled_jobs = serializers.IntegerField()
def get_url(self, obj):
return reverse('core-api:rqqueue-detail', args=[obj['name']], request=self.context.get("request"))
class BackgroundWorkerSerializer(serializers.Serializer):
name = serializers.CharField()
url = serializers.HyperlinkedIdentityField(
view_name='core-api:rqworker-detail',
lookup_field='name'
)
state = serializers.SerializerMethodField()
birth_date = serializers.DateTimeField()
queue_names = serializers.ListField(
child=serializers.CharField()
)
pid = serializers.CharField()
successful_job_count = serializers.IntegerField()
failed_job_count = serializers.IntegerField()
total_working_time = serializers.IntegerField()
def get_state(self, obj):
return obj.get_state()

View File

@ -1,6 +1,7 @@
from netbox.api.routers import NetBoxRouter
from . import views
app_name = 'core-api'
router = NetBoxRouter()
router.APIRootView = views.CoreRootView
@ -9,6 +10,8 @@ router.register('data-sources', views.DataSourceViewSet)
router.register('data-files', views.DataFileViewSet)
router.register('jobs', views.JobViewSet)
router.register('object-changes', views.ObjectChangeViewSet)
router.register('background-queues', views.BackgroundQueueViewSet, basename='rqqueue')
router.register('background-workers', views.BackgroundWorkerViewSet, basename='rqworker')
router.register('background-tasks', views.BackgroundTaskViewSet, basename='rqtask')
app_name = 'core-api'
urlpatterns = router.urls

View File

@ -1,5 +1,8 @@
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response
@ -10,8 +13,17 @@ from core import filtersets
from core.choices import DataSourceStatusChoices
from core.jobs import SyncDataSourceJob
from core.models import *
from core.utils import delete_rq_job, enqueue_rq_job, get_rq_jobs, requeue_rq_job, stop_rq_job
from django_rq.queues import get_redis_connection
from django_rq.utils import get_statistics
from django_rq.settings import QUEUES_LIST
from netbox.api.metadata import ContentTypeMetadata
from netbox.api.pagination import LimitOffsetListPagination
from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet
from rest_framework import viewsets
from rest_framework.permissions import IsAdminUser
from rq.job import Job as RQ_Job
from rq.worker import Worker
from . import serializers
@ -71,3 +83,152 @@ class ObjectChangeViewSet(ReadOnlyModelViewSet):
queryset = ObjectChange.objects.valid_models()
serializer_class = serializers.ObjectChangeSerializer
filterset_class = filtersets.ObjectChangeFilterSet
class BaseRQViewSet(viewsets.ViewSet):
"""
Base class for RQ view sets. Provides a list() method. Subclasses must implement get_data().
"""
permission_classes = [IsAdminUser]
serializer_class = None
def get_data(self):
raise NotImplementedError()
@extend_schema(responses={200: OpenApiTypes.OBJECT})
def list(self, request):
data = self.get_data()
paginator = LimitOffsetListPagination()
data = paginator.paginate_list(data, request)
serializer = self.serializer_class(data, many=True, context={'request': request})
return paginator.get_paginated_response(serializer.data)
def get_serializer(self, *args, **kwargs):
"""
Return the serializer instance that should be used for validating and
deserializing input, and for serializing output.
"""
serializer_class = self.get_serializer_class()
kwargs['context'] = self.get_serializer_context()
return serializer_class(*args, **kwargs)
class BackgroundQueueViewSet(BaseRQViewSet):
"""
Retrieve a list of RQ Queues.
Note: Queue names are not URL safe so not returning a detail view.
"""
serializer_class = serializers.BackgroundQueueSerializer
lookup_field = 'name'
lookup_value_regex = r'[\w.@+-]+'
def get_view_name(self):
return "Background Queues"
def get_data(self):
return get_statistics(run_maintenance_tasks=True)["queues"]
@extend_schema(responses={200: OpenApiTypes.OBJECT})
def retrieve(self, request, name):
data = self.get_data()
if not data:
raise Http404
for queue in data:
if queue['name'] == name:
serializer = self.serializer_class(queue, context={'request': request})
return Response(serializer.data)
raise Http404
class BackgroundWorkerViewSet(BaseRQViewSet):
"""
Retrieve a list of RQ Workers.
"""
serializer_class = serializers.BackgroundWorkerSerializer
lookup_field = 'name'
def get_view_name(self):
return "Background Workers"
def get_data(self):
config = QUEUES_LIST[0]
return Worker.all(get_redis_connection(config['connection_config']))
def retrieve(self, request, name):
# all the RQ queues should use the same connection
config = QUEUES_LIST[0]
workers = Worker.all(get_redis_connection(config['connection_config']))
worker = next((item for item in workers if item.name == name), None)
if not worker:
raise Http404
serializer = serializers.BackgroundWorkerSerializer(worker, context={'request': request})
return Response(serializer.data)
class BackgroundTaskViewSet(BaseRQViewSet):
"""
Retrieve a list of RQ Tasks.
"""
serializer_class = serializers.BackgroundTaskSerializer
def get_view_name(self):
return "Background Tasks"
def get_data(self):
return get_rq_jobs()
def get_task_from_id(self, task_id):
config = QUEUES_LIST[0]
task = RQ_Job.fetch(task_id, connection=get_redis_connection(config['connection_config']))
if not task:
raise Http404
return task
@extend_schema(responses={200: OpenApiTypes.OBJECT})
def retrieve(self, request, pk):
"""
Retrieve the details of the specified RQ Task.
"""
task = self.get_task_from_id(pk)
serializer = self.serializer_class(task, context={'request': request})
return Response(serializer.data)
@action(methods=["POST"], detail=True)
def delete(self, request, pk):
"""
Delete the specified RQ Task.
"""
delete_rq_job(pk)
return HttpResponse(status=200)
@action(methods=["POST"], detail=True)
def requeue(self, request, pk):
"""
Requeues the specified RQ Task.
"""
requeue_rq_job(pk)
return HttpResponse(status=200)
@action(methods=["POST"], detail=True)
def enqueue(self, request, pk):
"""
Enqueues the specified RQ Task.
"""
enqueue_rq_job(pk)
return HttpResponse(status=200)
@action(methods=["POST"], detail=True)
def stop(self, request, pk):
"""
Stops the specified RQ Task.
"""
stopped_jobs = stop_rq_job(pk)
if len(stopped_jobs) == 1:
return HttpResponse(status=200)
else:
return HttpResponse(status=204)

View File

@ -72,6 +72,20 @@ class JobStatusChoices(ChoiceSet):
)
class JobIntervalChoices(ChoiceSet):
INTERVAL_MINUTELY = 1
INTERVAL_HOURLY = 60
INTERVAL_DAILY = 60 * 24
INTERVAL_WEEKLY = 60 * 24 * 7
CHOICES = (
(INTERVAL_MINUTELY, _('Minutely')),
(INTERVAL_HOURLY, _('Hourly')),
(INTERVAL_DAILY, _('Daily')),
(INTERVAL_WEEKLY, _('Weekly')),
)
#
# ObjectChanges
#

View File

@ -2,6 +2,8 @@ import logging
from django_rq.management.commands.rqworker import Command as _Command
from netbox.registry import registry
DEFAULT_QUEUES = ('high', 'default', 'low')
@ -14,6 +16,15 @@ class Command(_Command):
of only the 'default' queue).
"""
def handle(self, *args, **options):
# Setup system jobs.
for job, kwargs in registry['system_jobs'].items():
try:
interval = kwargs['interval']
except KeyError:
raise TypeError("System job must specify an interval (in minutes).")
logger.debug(f"Scheduling system job {job.name} (interval={interval})")
job.enqueue_once(**kwargs)
# Run the worker with scheduler functionality
options['with_scheduler'] = True

View File

@ -8,13 +8,12 @@ import utilities.json
class Migration(migrations.Migration):
replaces = [
('core', '0001_initial'),
('core', '0002_managedfile'),
('core', '0003_job'),
('core', '0004_replicate_jobresults'),
('core', '0005_job_created_auto_now')
('core', '0005_job_created_auto_now'),
]
dependencies = [
@ -30,7 +29,10 @@ class Migration(migrations.Migration):
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
(
'custom_field_data',
models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
),
('description', models.CharField(blank=True, max_length=200)),
('comments', models.TextField(blank=True)),
('name', models.CharField(max_length=100, unique=True)),
@ -55,9 +57,28 @@ class Migration(migrations.Migration):
('last_updated', models.DateTimeField(editable=False)),
('path', models.CharField(editable=False, max_length=1000)),
('size', models.PositiveIntegerField(editable=False)),
('hash', models.CharField(editable=False, max_length=64, validators=[django.core.validators.RegexValidator(message='Length must be 64 hexadecimal characters.', regex='^[0-9a-f]{64}$')])),
(
'hash',
models.CharField(
editable=False,
max_length=64,
validators=[
django.core.validators.RegexValidator(
message='Length must be 64 hexadecimal characters.', regex='^[0-9a-f]{64}$'
)
],
),
),
('data', models.BinaryField()),
('source', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='datafiles', to='core.datasource')),
(
'source',
models.ForeignKey(
editable=False,
on_delete=django.db.models.deletion.CASCADE,
related_name='datafiles',
to='core.datasource',
),
),
],
options={
'ordering': ('source', 'path'),
@ -76,8 +97,18 @@ class Migration(migrations.Migration):
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('object_id', models.PositiveBigIntegerField()),
('datafile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='core.datafile')),
('object_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='contenttypes.contenttype')),
(
'datafile',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, related_name='+', to='core.datafile'
),
),
(
'object_type',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, related_name='+', to='contenttypes.contenttype'
),
),
],
options={
'indexes': [models.Index(fields=['object_type', 'object_id'], name='core_autosy_object__c17bac_idx')],
@ -97,8 +128,26 @@ class Migration(migrations.Migration):
('last_updated', models.DateTimeField(blank=True, editable=False, null=True)),
('file_root', models.CharField(max_length=1000)),
('file_path', models.FilePathField(editable=False)),
('data_file', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='core.datafile')),
('data_source', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='core.datasource')),
(
'data_file',
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='+',
to='core.datafile',
),
),
(
'data_source',
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name='+',
to='core.datasource',
),
),
('auto_sync_enabled', models.BooleanField(default=False)),
],
options={
@ -108,7 +157,9 @@ class Migration(migrations.Migration):
),
migrations.AddConstraint(
model_name='managedfile',
constraint=models.UniqueConstraint(fields=('file_root', 'file_path'), name='core_managedfile_unique_root_path'),
constraint=models.UniqueConstraint(
fields=('file_root', 'file_path'), name='core_managedfile_unique_root_path'
),
),
migrations.CreateModel(
name='Job',
@ -118,14 +169,33 @@ class Migration(migrations.Migration):
('name', models.CharField(max_length=200)),
('created', models.DateTimeField()),
('scheduled', models.DateTimeField(blank=True, null=True)),
('interval', models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)])),
(
'interval',
models.PositiveIntegerField(
blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)]
),
),
('started', models.DateTimeField(blank=True, null=True)),
('completed', models.DateTimeField(blank=True, null=True)),
('status', models.CharField(default='pending', max_length=30)),
('data', models.JSONField(blank=True, null=True)),
('job_id', models.UUIDField(unique=True)),
('object_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='jobs', to='contenttypes.contenttype')),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
(
'object_type',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, related_name='jobs', to='contenttypes.contenttype'
),
),
(
'user',
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='+',
to=settings.AUTH_USER_MODEL,
),
),
],
options={
'ordering': ['-created'],

View File

@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0005_job_created_auto_now'),
]

View File

@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0006_datasource_type_remove_choices'),
]

View File

@ -3,7 +3,6 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('core', '0007_job_add_error_field'),
@ -12,8 +11,7 @@ class Migration(migrations.Migration):
operations = [
migrations.CreateModel(
name='ObjectType',
fields=[
],
fields=[],
options={
'proxy': True,
'indexes': [],

View File

@ -2,7 +2,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0008_contenttype_proxy'),
]

View File

@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0009_configrevision'),
]

View File

@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('core', '0010_gfk_indexes'),
@ -27,15 +26,49 @@ class Migration(migrations.Migration):
('object_repr', models.CharField(editable=False, max_length=200)),
('prechange_data', models.JSONField(blank=True, editable=False, null=True)),
('postchange_data', models.JSONField(blank=True, editable=False, null=True)),
('changed_object_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype')),
('related_object_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype')),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='changes', to=settings.AUTH_USER_MODEL)),
(
'changed_object_type',
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name='+',
to='contenttypes.contenttype',
),
),
(
'related_object_type',
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name='+',
to='contenttypes.contenttype',
),
),
(
'user',
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='changes',
to=settings.AUTH_USER_MODEL,
),
),
],
options={
'verbose_name': 'object change',
'verbose_name_plural': 'object changes',
'ordering': ['-time'],
'indexes': [models.Index(fields=['changed_object_type', 'changed_object_id'], name='core_object_changed_c227ce_idx'), models.Index(fields=['related_object_type', 'related_object_id'], name='core_object_related_3375d6_idx')],
'indexes': [
models.Index(
fields=['changed_object_type', 'changed_object_id'],
name='core_object_changed_c227ce_idx',
),
models.Index(
fields=['related_object_type', 'related_object_id'],
name='core_object_related_3375d6_idx',
),
],
},
),
],

View File

@ -3,7 +3,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('core', '0011_move_objectchange'),
@ -18,7 +17,7 @@ class Migration(migrations.Migration):
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='jobs',
to='contenttypes.contenttype'
to='contenttypes.contenttype',
),
),
]

View File

@ -84,9 +84,6 @@ class DataSource(JobsMixin, PrimaryModel):
def __str__(self):
return f'{self.name}'
def get_absolute_url(self):
return reverse('core:datasource', args=[self.pk])
@property
def docs_url(self):
return f'{settings.STATIC_URL}docs/models/{self._meta.app_label}/{self._meta.model_name}/'

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