2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -26,7 +26,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox Version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.0.11
|
||||
placeholder: v4.1.0
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.0.11
|
||||
placeholder: v4.1.0
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
17
docs/configuration/graphql-api.md
Normal file
@ -0,0 +1,17 @@
|
||||
# GraphQL API Parameters
|
||||
|
||||
## GRAPHQL_ENABLED
|
||||
|
||||
!!! tip "Dynamic Configuration Parameter"
|
||||
|
||||
Default: True
|
||||
|
||||
Setting this to False will disable the GraphQL API.
|
||||
|
||||
---
|
||||
|
||||
## GRAPHQL_MAX_ALIASES
|
||||
|
||||
Default: 10
|
||||
|
||||
The maximum number of queries that a GraphQL API request may contain.
|
@ -25,7 +25,7 @@ Some configuration parameters are primarily controlled via NetBox's admin interf
|
||||
* [`CUSTOM_VALIDATORS`](./data-validation.md#custom_validators)
|
||||
* [`DEFAULT_USER_PREFERENCES`](./default-values.md#default_user_preferences)
|
||||
* [`ENFORCE_GLOBAL_UNIQUE`](./miscellaneous.md#enforce_global_unique)
|
||||
* [`GRAPHQL_ENABLED`](./miscellaneous.md#graphql_enabled)
|
||||
* [`GRAPHQL_ENABLED`](./graphql-api.md#graphql_enabled)
|
||||
* [`JOB_RETENTION`](./miscellaneous.md#job_retention)
|
||||
* [`MAINTENANCE_MODE`](./miscellaneous.md#maintenance_mode)
|
||||
* [`MAPS_URL`](./miscellaneous.md#maps_url)
|
||||
|
@ -122,16 +122,6 @@ The maximum amount (in bytes) of uploaded data that will be held in memory befor
|
||||
|
||||
---
|
||||
|
||||
## GRAPHQL_ENABLED
|
||||
|
||||
!!! tip "Dynamic Configuration Parameter"
|
||||
|
||||
Default: True
|
||||
|
||||
Setting this to False will disable the GraphQL API.
|
||||
|
||||
---
|
||||
|
||||
## JOB_RETENTION
|
||||
|
||||
!!! tip "Dynamic Configuration Parameter"
|
||||
|
@ -20,19 +20,29 @@ A list of permitted URL schemes referenced when rendering links within NetBox. N
|
||||
|
||||
## AUTH_PASSWORD_VALIDATORS
|
||||
|
||||
This parameter acts as a pass-through for configuring Django's built-in password validators for local user accounts. If configured, these will be applied whenever a user's password is updated to ensure that it meets minimum criteria such as length or complexity. An example is provided below. For more detail on the available options, please see [the Django documentation](https://docs.djangoproject.com/en/stable/topics/auth/passwords/#password-validation).
|
||||
This parameter acts as a pass-through for configuring Django's built-in password validators for local user accounts. These rules are applied whenever a user's password is created or updated to ensure that it meets minimum criteria such as length or complexity. The default configuration is shown below.
|
||||
|
||||
```python
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
'OPTIONS': {
|
||||
'min_length': 10,
|
||||
}
|
||||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||
"OPTIONS": {
|
||||
"min_length": 12,
|
||||
},
|
||||
},
|
||||
{
|
||||
"NAME": "utilities.password_validation.AlphanumericPasswordValidator",
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
The default configuration enforces the follow criteria:
|
||||
|
||||
* A password must be at least 12 characters in length.
|
||||
* A password must have at least one uppercase letter, one lowercase letter, and one numeric digit.
|
||||
|
||||
Although it is not recommended, the default validation rules can be disabled by setting `AUTH_PASSWORD_VALIDATORS = []` in the configuration file. For more detail on customizing password validation, please see [the Django documentation](https://docs.djangoproject.com/en/stable/topics/auth/passwords/#password-validation).
|
||||
|
||||
---
|
||||
|
||||
## CORS_ORIGIN_ALLOW_ALL
|
||||
|
@ -87,6 +87,19 @@ 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.
|
||||
|
||||
!!! note
|
||||
If Internet access is available via a proxy, set [`HTTP_PROXIES`](#http_proxies) instead.
|
||||
|
||||
---
|
||||
|
||||
## JINJA2_FILTERS
|
||||
|
||||
Default: `{}`
|
||||
@ -143,7 +156,7 @@ LOGGING = {
|
||||
|
||||
## MEDIA_ROOT
|
||||
|
||||
Default: $INSTALL_ROOT/netbox/media/
|
||||
Default: `$INSTALL_ROOT/netbox/media/`
|
||||
|
||||
The file path to the location where media files (such as image attachments) are stored. By default, this is the `netbox/media/` directory within the base NetBox installation path.
|
||||
|
||||
|
@ -74,6 +74,8 @@ If a default value is specified for a selection field, it must exactly match one
|
||||
|
||||
An object or multi-object custom field can be used to refer to a particular NetBox object or objects as the "value" for a custom field. These custom fields must define an `object_type`, which determines the type of object to which custom field instances point.
|
||||
|
||||
By default, an object choice field will make all objects of that type available for selection in the drop-down. The list choices can be filtered to show only objects with certain values by providing a `query_params` dict in the Related Object Filter field, as a JSON value. More information about `query_params` can be found [here](./custom-scripts.md#objectvar).
|
||||
|
||||
## Custom Fields in Templates
|
||||
|
||||
Several features within NetBox, such as export templates and webhooks, utilize Jinja2 templating. For convenience, objects which support custom field assignment expose custom field data through the `cf` property. This is a bit cleaner than accessing custom field data through the actual field (`custom_field_data`).
|
||||
|
@ -86,8 +86,6 @@ CUSTOM_VALIDATORS = {
|
||||
|
||||
#### Referencing Related Object Attributes
|
||||
|
||||
!!! info "This feature was introduced in NetBox v4.0."
|
||||
|
||||
The attributes of a related object can be referenced by specifying a dotted path. For example, to reference the name of a region to which a site is assigned, use `region.name`:
|
||||
|
||||
```python
|
||||
@ -104,8 +102,6 @@ CUSTOM_VALIDATORS = {
|
||||
|
||||
#### Validating Request Parameters
|
||||
|
||||
!!! info "This feature was introduced in NetBox v4.0."
|
||||
|
||||
In addition to validating object attributes, custom validators can also match against parameters of the current request (where available). For example, the following rule will permit only the user named "admin" to modify an object:
|
||||
|
||||
```json
|
||||
|
@ -71,7 +71,6 @@ Add the relevant navigation menu items in `netbox/netbox/navigation/menu.py`.
|
||||
Create the following for each model:
|
||||
|
||||
* Detailed (full) model serializer in `api/serializers.py`
|
||||
* Nested serializer in `api/nested_serializers.py`
|
||||
* API view in `api/views.py`
|
||||
* Endpoint route in `api/urls.py`
|
||||
|
||||
|
@ -50,7 +50,7 @@ If you're adding a relational field (e.g. `ForeignKey`) and intend to include th
|
||||
|
||||
## 5. Update API serializer
|
||||
|
||||
Extend the model's API serializer in `<app>.api.serializers` to include the new field. In most cases, it will not be necessary to also extend the nested serializer, which produces a minimal representation of the model.
|
||||
Extend the model's API serializer in `<app>.api.serializers` to include the new field.
|
||||
|
||||
## 6. Add fields to forms
|
||||
|
||||
|
@ -18,7 +18,7 @@ Depending on its classification, each NetBox model may support various features
|
||||
| [Custom links](../customization/custom-links.md) | `CustomLinksMixin` | `custom_links` | These models support the assignment of custom links |
|
||||
| [Custom validation](../customization/custom-validation.md) | `CustomValidationMixin` | - | Supports the enforcement of custom validation rules |
|
||||
| [Export templates](../customization/export-templates.md) | `ExportTemplatesMixin` | `export_templates` | Users can create custom export templates for these models |
|
||||
| [Job results](../features/background-jobs.md) | `JobsMixin` | `jobs` | Users can create custom export templates for these models |
|
||||
| [Job results](../features/background-jobs.md) | `JobsMixin` | `jobs` | Background jobs can be scheduled for these models |
|
||||
| [Journaling](../features/journaling.md) | `JournalingMixin` | `journaling` | These models support persistent historical commentary |
|
||||
| [Synchronized data](../integrations/synchronized-data.md) | `SyncedDataMixin` | `synced_data` | Certain model data can be automatically synchronized from a remote data source |
|
||||
| [Tagging](../models/extras/tag.md) | `TagsMixin` | `tags` | The models can be tagged with user-defined tags |
|
||||
@ -34,7 +34,9 @@ These are considered the "core" application models which are used to model netwo
|
||||
* [circuits.Provider](../models/circuits/provider.md)
|
||||
* [circuits.ProviderAccount](../models/circuits/provideraccount.md)
|
||||
* [circuits.ProviderNetwork](../models/circuits/providernetwork.md)
|
||||
* [core.DataFile](../models/core/datafile.md)
|
||||
* [core.DataSource](../models/core/datasource.md)
|
||||
* [core.Job](../models/core/job.md)
|
||||
* [dcim.Cable](../models/dcim/cable.md)
|
||||
* [dcim.Device](../models/dcim/device.md)
|
||||
* [dcim.DeviceType](../models/dcim/devicetype.md)
|
||||
@ -44,12 +46,14 @@ These are considered the "core" application models which are used to model netwo
|
||||
* [dcim.PowerPanel](../models/dcim/powerpanel.md)
|
||||
* [dcim.Rack](../models/dcim/rack.md)
|
||||
* [dcim.RackReservation](../models/dcim/rackreservation.md)
|
||||
* [dcim.RackType](../models/dcim/racktype.md)
|
||||
* [dcim.Site](../models/dcim/site.md)
|
||||
* [dcim.VirtualChassis](../models/dcim/virtualchassis.md)
|
||||
* [dcim.VirtualDeviceContext](../models/dcim/virtualdevicecontext.md)
|
||||
* [ipam.Aggregate](../models/ipam/aggregate.md)
|
||||
* [ipam.ASN](../models/ipam/asn.md)
|
||||
* [ipam.FHRPGroup](../models/ipam/fhrpgroup.md)
|
||||
* [ipam.FHRPGroupAssignment](../models/ipam/fhrpgroupassignment.md)
|
||||
* [ipam.IPAddress](../models/ipam/ipaddress.md)
|
||||
* [ipam.IPRange](../models/ipam/iprange.md)
|
||||
* [ipam.Prefix](../models/ipam/prefix.md)
|
||||
@ -76,6 +80,7 @@ These are considered the "core" application models which are used to model netwo
|
||||
|
||||
Organization models are used to organize and classify primary models.
|
||||
|
||||
* [circuits.CircuitGroup](../models/circuits/circuitgroup.md)
|
||||
* [circuits.CircuitType](../models/circuits/circuittype.md)
|
||||
* [dcim.DeviceRole](../models/dcim/devicerole.md)
|
||||
* [dcim.Manufacturer](../models/dcim/manufacturer.md)
|
||||
@ -88,6 +93,7 @@ Organization models are used to organize and classify primary models.
|
||||
* [tenancy.ContactRole](../models/tenancy/contactrole.md)
|
||||
* [virtualization.ClusterGroup](../models/virtualization/clustergroup.md)
|
||||
* [virtualization.ClusterType](../models/virtualization/clustertype.md)
|
||||
* [vpn.TunnelGroup](../models/vpn/tunnelgroup.md)
|
||||
|
||||
### Nested Group Models
|
||||
|
||||
@ -131,3 +137,10 @@ These function as templates to effect the replication of device and virtual mach
|
||||
* [dcim.PowerOutletTemplate](../models/dcim/poweroutlettemplate.md)
|
||||
* [dcim.PowerPortTemplate](../models/dcim/powerporttemplate.md)
|
||||
* [dcim.RearPortTemplate](../models/dcim/rearporttemplate.md)
|
||||
|
||||
### Connection Models
|
||||
|
||||
Connection models are used to model the connections, or connection endpoints between models.
|
||||
|
||||
* [circuits.CircuitTermination](../models/circuits/circuittermination.md)
|
||||
* [vpn.TunnelTermination](../models/vpn/tunneltermination.md)
|
||||
|
@ -2,9 +2,9 @@
|
||||
|
||||
This documentation describes the process of packaging and publishing a new NetBox release. There are three types of release:
|
||||
|
||||
* Major release (e.g. v2.11 to v3.0)
|
||||
* Minor release (e.g. v3.2 to v3.3)
|
||||
* Patch release (e.g. v3.3.0 to v3.3.1)
|
||||
* Major release (e.g. v3.7.8 to v4.0.0)
|
||||
* Minor release (e.g. v4.0.10 to v4.1.0)
|
||||
* Patch release (e.g. v4.1.0 to v4.1.1)
|
||||
|
||||
While major releases generally introduce some very substantial change to the application, they are typically treated the same as minor version increments for the purpose of release packaging.
|
||||
|
||||
@ -90,7 +90,7 @@ Updated language translations should be pulled from [Transifex](https://app.tran
|
||||
|
||||
### Update Version and Changelog
|
||||
|
||||
* Update the `VERSION` constant in `settings.py` to the new release version.
|
||||
* Update the version and published date in `release.yaml` with the current version & date. Add a designation (e.g.g `beta1`) if applicable.
|
||||
* Update the example version numbers in the feature request and bug report templates under `.github/ISSUE_TEMPLATES/`.
|
||||
* Replace the "FUTURE" placeholder in the release notes with the current date.
|
||||
|
||||
@ -117,16 +117,6 @@ Create a [new release](https://github.com/netbox-community/netbox/releases/new)
|
||||
|
||||
Once created, the release will become available for users to install.
|
||||
|
||||
### Update the Development Version
|
||||
|
||||
On the `develop` branch, update `VERSION` in `settings.py` to point to the next release. For example, if you just released v3.3.1, set:
|
||||
|
||||
```
|
||||
VERSION = 'v3.3.2-dev'
|
||||
```
|
||||
|
||||
Commit this change with the comment "PRVB" (for _post-release version bump_) and push the commit upstream.
|
||||
|
||||
### Update the Public Documentation
|
||||
|
||||
After a release has been published, the public NetBox documentation needs to be updated. This is accomplished by running two actions on the [netboxlabs-docs](https://github.com/netboxlabs/netboxlabs-docs) repository.
|
||||
|
@ -1,9 +1,10 @@
|
||||
# Event Rules
|
||||
|
||||
NetBox includes the ability to execute certain functions in response to internal object changes. These include:
|
||||
NetBox includes the ability to automatically perform certain functions in response to internal events. These include:
|
||||
|
||||
* [Scripts](../customization/custom-scripts.md) execution
|
||||
* [Webhooks](../integrations/webhooks.md) execution
|
||||
* Executing a [custom script](../customization/custom-scripts.md)
|
||||
* Sending a [webhook](../integrations/webhooks.md)
|
||||
* Generating [user notifications](../features/notifications.md)
|
||||
|
||||
For example, suppose you want to automatically configure a monitoring system to start monitoring a device when its operational status is changed to active, and remove it from monitoring for any other status. You can create a webhook in NetBox for the device model and craft its content and destination URL to effect the desired change on the receiving system. You can then associate an event rule with this webhook and the webhook will be sent automatically by NetBox whenever the configured constraints are met.
|
||||
|
||||
|
@ -56,6 +56,10 @@ A site typically represents a building within a region and/or site group. Each s
|
||||
|
||||
A location can be any logical subdivision within a building, such as a floor or room. Like regions and site groups, locations can be nested into a self-recursive hierarchy for maximum flexibility. And like sites, each location has an operational status assigned to it.
|
||||
|
||||
## Rack Types
|
||||
|
||||
A rack type represents a unique specification of a rack which exists in the real world. Each rack type can be setup with weight, height, and unit ordering. New racks of this type can then be created in NetBox, and any associated specifications will be automatically replicated from the device type.
|
||||
|
||||
## Racks
|
||||
|
||||
Finally, NetBox models each equipment rack as a discrete object within a site and location. These are physical objects into which devices are installed. Each rack can be assigned an operational status, type, facility ID, and other attributes related to inventory tracking. Each rack also must define a height (in rack units) and width, and may optionally specify its physical dimensions.
|
||||
|
10
docs/features/notifications.md
Normal file
@ -0,0 +1,10 @@
|
||||
# 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.
|
||||
* An [event rule](./event-rules.md) can be defined to automatically generate a notification for one or more users in response to specific system events.
|
||||
|
||||
Additionally, NetBox plugins can generate notifications for their own purposes.
|
@ -112,4 +112,4 @@ Authorization: Token $TOKEN
|
||||
|
||||
## Disabling the GraphQL API
|
||||
|
||||
If not needed, the GraphQL API can be disabled by setting the [`GRAPHQL_ENABLED`](../configuration/miscellaneous.md#graphql_enabled) configuration parameter to False and restarting NetBox.
|
||||
If not needed, the GraphQL API can be disabled by setting the [`GRAPHQL_ENABLED`](../configuration/graphql-api.md#graphql_enabled) configuration parameter to False and restarting NetBox.
|
||||
|
@ -101,7 +101,7 @@ See the [filtering documentation](../reference/filtering.md) for more details on
|
||||
|
||||
## Serialization
|
||||
|
||||
The REST API employs two types of serializers to represent model data: base serializers and nested serializers. The base serializer is used to present the complete view of a model. This includes all database table fields which comprise the model, and may include additional metadata. A base serializer includes relationships to parent objects, but **does not** include child objects. For example, the `VLANSerializer` includes a nested representation its parent VLANGroup (if any), but does not include any assigned Prefixes.
|
||||
The REST API generally represents objects in one of two ways: complete or brief. The base serializer is used to present the complete view of an object. This includes all database table fields which comprise the model, and may include additional metadata. A base serializer includes relationships to parent objects, but **does not** include child objects. For example, the `VLANSerializer` includes a nested representation its parent VLANGroup (if any), but does not include any assigned Prefixes. Serializers employ a minimal "brief" representation of related objects, which includes only the attributes prudent for identifying the object.
|
||||
|
||||
```json
|
||||
{
|
||||
@ -139,7 +139,7 @@ The REST API employs two types of serializers to represent model data: base seri
|
||||
|
||||
### Related Objects
|
||||
|
||||
Related objects (e.g. `ForeignKey` fields) are represented using nested serializers. A nested serializer provides a minimal representation of an object, including only its direct URL and enough information to display the object to a user. When performing write API actions (`POST`, `PUT`, and `PATCH`), related objects may be specified by either numeric ID (primary key), or by a set of attributes sufficiently unique to return the desired object.
|
||||
Related objects (e.g. `ForeignKey` fields) are included using nested brief representations. This is a minimal representation of an object, including only its direct URL and enough information to display the object to a user. When performing write API actions (`POST`, `PUT`, and `PATCH`), related objects may be specified by either numeric ID (primary key), or by a set of attributes sufficiently unique to return the desired object.
|
||||
|
||||
For example, when creating a new device, its rack can be specified by NetBox ID (PK):
|
||||
|
||||
@ -151,7 +151,7 @@ For example, when creating a new device, its rack can be specified by NetBox ID
|
||||
}
|
||||
```
|
||||
|
||||
Or by a set of nested attributes which uniquely identify the rack:
|
||||
Or by a set of attributes which uniquely identify the rack:
|
||||
|
||||
```json
|
||||
{
|
||||
|
Before Width: | Height: | Size: 193 KiB After Width: | Height: | Size: 403 KiB |
Before Width: | Height: | Size: 422 KiB After Width: | Height: | Size: 548 KiB |
Before Width: | Height: | Size: 433 KiB After Width: | Height: | Size: 481 KiB |
Before Width: | Height: | Size: 510 KiB After Width: | Height: | Size: 562 KiB |
Before Width: | Height: | Size: 341 KiB After Width: | Height: | Size: 372 KiB |
15
docs/models/circuits/circuitgroup.md
Normal file
@ -0,0 +1,15 @@
|
||||
# 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
|
||||
|
||||
### Name
|
||||
|
||||
A unique human-friendly name.
|
||||
|
||||
### Slug
|
||||
|
||||
A unique URL-friendly identifier. (This value can be used for filtering.)
|
25
docs/models/circuits/circuitgroupassignment.md
Normal file
@ -0,0 +1,25 @@
|
||||
# Circuit Group Assignments
|
||||
|
||||
Circuits can be assigned to [circuit groups](./circuitgroup.md) for correlation purposes. For instance, three circuits, each belonging to a different provider, may each be assigned to the same circuit group. Each assignment may optionally include a priority designation.
|
||||
|
||||
## Fields
|
||||
|
||||
### Group
|
||||
|
||||
The [circuit group](./circuitgroup.md) being assigned.
|
||||
|
||||
### Circuit
|
||||
|
||||
The [circuit](./circuit.md) that is being assigned to the group.
|
||||
|
||||
### Priority
|
||||
|
||||
The circuit's operation priority relative to its peers within the group. The assignment of a priority is optional. Choices include:
|
||||
|
||||
* Primary
|
||||
* Secondary
|
||||
* Tertiary
|
||||
* Inactive
|
||||
|
||||
!!! tip
|
||||
Additional priority choices may be defined by setting `CircuitGroupAssignment.priority` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
|
@ -14,6 +14,12 @@ Module bays represent a space or slot within a device in which a field-replaceab
|
||||
|
||||
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
|
||||
|
||||
The module bay's name. Must be unique to the parent device.
|
||||
|
@ -39,3 +39,9 @@ An alternative part number to uniquely identify the module type.
|
||||
### Weight
|
||||
|
||||
The numeric weight of the module, including a unit designation (e.g. 3 kilograms or 1 pound).
|
||||
|
||||
### Airflow
|
||||
|
||||
!!! info "The `airflow` field was introduced in NetBox v4.1."
|
||||
|
||||
The direction in which air circulates through the device chassis for cooling.
|
||||
|
@ -20,6 +20,10 @@ The [location](./location.md) within a site where the rack has been installed (o
|
||||
|
||||
The rack's name or identifier. Must be unique to the rack's location, if assigned.
|
||||
|
||||
### Rack Type
|
||||
|
||||
The [physical type](./racktype.md) of this rack. The rack type defines physical attributes such as height and weight.
|
||||
|
||||
### Status
|
||||
|
||||
Operational status.
|
||||
@ -43,44 +47,5 @@ The unique physical serial number assigned to this rack.
|
||||
|
||||
A unique, locally-administered label used to identify hardware resources.
|
||||
|
||||
### Type
|
||||
|
||||
A rack can be designated as one of the following types:
|
||||
|
||||
* 2-post frame
|
||||
* 4-post frame
|
||||
* 4-post cabinet
|
||||
* Wall-mounted frame
|
||||
* Wall-mounted cabinet
|
||||
|
||||
### Width
|
||||
|
||||
The canonical distance between the two vertical rails on a face. (This is typically 19 inches, however other standard widths exist.)
|
||||
|
||||
### Height
|
||||
|
||||
The height of the rack, measured in units.
|
||||
|
||||
### Starting Unit
|
||||
|
||||
The number of the numerically lowest unit in the rack. This value defaults to one, but may be higher in certain situations. For example, you may want to model only a select range of units within a shared physical rack (e.g. U13 through U24).
|
||||
|
||||
### Outer Dimensions
|
||||
|
||||
The external width and depth of the rack can be tracked to aid in floorplan calculations. These measurements must be designated in either millimeters or inches.
|
||||
|
||||
### Mounting Depth
|
||||
|
||||
The maximum depth of a mounted device that the rack can accommodate, in millimeters. For four-post frames or cabinets, this is the horizontal distance between the front and rear vertical rails. (Note that this measurement does _not_ include space between the rails and the cabinet doors.)
|
||||
|
||||
### Weight
|
||||
|
||||
The numeric weight of the rack, including a unit designation (e.g. 10 kilograms or 20 pounds).
|
||||
|
||||
### Maximum Weight
|
||||
|
||||
The maximum total weight capacity for all installed devices, inclusive of the rack itself.
|
||||
|
||||
### Descending Units
|
||||
|
||||
If selected, the rack's elevation will display unit 1 at the top of the rack. (Most racks use ascending numbering, with unit 1 assigned to the bottommost position.)
|
||||
!!! note
|
||||
Some additional fields pertaining to physical attributes such as height and weight can also be defined on each rack, but should generally be defined instead on the [rack type](./racktype.md).
|
||||
|
61
docs/models/dcim/racktype.md
Normal file
@ -0,0 +1,61 @@
|
||||
# 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
|
||||
|
||||
### Manufacturer
|
||||
|
||||
The [manufacturer](./manufacturer.md) which produces this type of rack.
|
||||
|
||||
### Model
|
||||
|
||||
The model number assigned to this rack type by its manufacturer. Must be unique to the manufacturer.
|
||||
|
||||
### Slug
|
||||
|
||||
A unique URL-friendly representation of the model identifier. (This value can be used for filtering.)
|
||||
|
||||
### Form Factor
|
||||
|
||||
A rack can be designated as one of the following form factors:
|
||||
|
||||
* 2-post frame
|
||||
* 4-post frame
|
||||
* 4-post cabinet
|
||||
* Wall-mounted frame
|
||||
* Wall-mounted cabinet
|
||||
|
||||
### Width
|
||||
|
||||
The canonical distance between the two vertical rails on a face. (This is typically 19 inches, however other standard widths exist.)
|
||||
|
||||
### Height
|
||||
|
||||
The height of the rack, measured in units.
|
||||
|
||||
### Starting Unit
|
||||
|
||||
The number of the numerically lowest unit in the rack. This value defaults to one, but may be higher in certain situations. For example, you may want to model only a select range of units within a shared physical rack (e.g. U13 through U24).
|
||||
|
||||
### Outer Dimensions
|
||||
|
||||
The external width and depth of the rack can be tracked to aid in floorplan calculations. These measurements must be designated in either millimeters or inches.
|
||||
|
||||
### Mounting Depth
|
||||
|
||||
The maximum depth of a mounted device that the rack can accommodate, in millimeters. For four-post frames or cabinets, this is the horizontal distance between the front and rear vertical rails. (Note that this measurement does _not_ include space between the rails and the cabinet doors.)
|
||||
|
||||
### Weight
|
||||
|
||||
The numeric weight of the rack, including a unit designation (e.g. 10 kilograms or 20 pounds).
|
||||
|
||||
### Maximum Weight
|
||||
|
||||
The maximum total weight capacity for all installed devices, inclusive of the rack itself.
|
||||
|
||||
### Descending Units
|
||||
|
||||
If selected, the rack's elevation will display unit 1 at the top of the rack. (Most racks use ascending numbering, with unit 1 assigned to the bottommost position.)
|
@ -42,13 +42,26 @@ The type of data this field holds. This must be one of the following:
|
||||
|
||||
For object and multiple-object fields only. Designates the type of NetBox object being referenced.
|
||||
|
||||
### 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
|
||||
This setting is employed for convenience only, and should not be relied upon to enforce data integrity.
|
||||
|
||||
### Weight
|
||||
|
||||
A numeric weight used to override alphabetic ordering of fields by name. Custom fields with a lower weight will be listed before those with a higher weight. (Note that weight applies within the context of a custom field group, if defined.)
|
||||
|
||||
### Required
|
||||
|
||||
If checked, this custom field must be populated with a valid value for the object to pass validation.
|
||||
If enabled, this custom field must be populated with a valid value for the object to pass validation.
|
||||
|
||||
### Unique
|
||||
|
||||
If enabled, each object must have a unique value set for this custom field (per object type).
|
||||
|
||||
### Description
|
||||
|
||||
|
@ -18,17 +18,22 @@ The type(s) of object in NetBox that will trigger the rule.
|
||||
|
||||
If not selected, the event rule will not be processed.
|
||||
|
||||
### Events
|
||||
### Events Types
|
||||
|
||||
The events which will trigger the rule. At least one event type must be selected.
|
||||
The event types which will trigger the rule. At least one event type must be selected.
|
||||
|
||||
| Name | Description |
|
||||
|------------|--------------------------------------|
|
||||
| Creations | A new object has been created |
|
||||
| Updates | An existing object has been modified |
|
||||
| Deletions | An object has been deleted |
|
||||
| Job starts | A job for an object starts |
|
||||
| Job ends | A job for an object terminates |
|
||||
|----------------|---------------------------------------------|
|
||||
| Object created | A new object has been created |
|
||||
| Object updated | An existing object has been modified |
|
||||
| Object deleted | An object has been deleted |
|
||||
| Job started | A background job is initiated |
|
||||
| Job completed | A background job completes successfully |
|
||||
| Job failed | A background job fails |
|
||||
| Job errored | A background job is aborted due to an error |
|
||||
|
||||
!!! tip "Custom Event Types"
|
||||
The above list includes only built-in event types. NetBox plugins can also register their own custom event types.
|
||||
|
||||
### Conditions
|
||||
|
||||
|
17
docs/models/extras/notification.md
Normal file
@ -0,0 +1,17 @@
|
||||
# Notification
|
||||
|
||||
A notification alerts a user that a specific action has taken place in NetBox, such as an object being modified or a background job completing. A notification may be generated via a user's [subscription](./subscription.md) to a particular object, or by an event rule targeting a [notification group](./notificationgroup.md) of which the user is a member.
|
||||
|
||||
## Fields
|
||||
|
||||
### User
|
||||
|
||||
The recipient of the notification.
|
||||
|
||||
### Object
|
||||
|
||||
The object to which the notification relates.
|
||||
|
||||
### Event Type
|
||||
|
||||
The type of event indicated by the notification.
|
17
docs/models/extras/notificationgroup.md
Normal file
@ -0,0 +1,17 @@
|
||||
# Notification Group
|
||||
|
||||
A set of NetBox users and/or groups of users identified as recipients for certain [notifications](./notification.md).
|
||||
|
||||
## Fields
|
||||
|
||||
### Name
|
||||
|
||||
The name of the notification group.
|
||||
|
||||
### Users
|
||||
|
||||
One or more users directly designated as members of the notification group.
|
||||
|
||||
### Groups
|
||||
|
||||
All users of any selected groups are considered as members of the notification group.
|
15
docs/models/extras/subscription.md
Normal file
@ -0,0 +1,15 @@
|
||||
# Subscription
|
||||
|
||||
A record indicating that a user is to be notified of any changes to a particular NetBox object. A notification maps exactly one user to exactly one object.
|
||||
|
||||
When an object to which a user is subscribed changes, a [notification](./notification.md) is generated for the user.
|
||||
|
||||
## Fields
|
||||
|
||||
### User
|
||||
|
||||
The subscribed user.
|
||||
|
||||
### Object
|
||||
|
||||
The object to which the user is subscribed.
|
@ -14,9 +14,11 @@ A unique human-friendly name.
|
||||
|
||||
A unique URL-friendly identifier. (This value can be used for filtering.)
|
||||
|
||||
### Minimum & Maximum VLAN IDs
|
||||
### VLAN ID Ranges
|
||||
|
||||
A minimum and maximum child VLAN ID must be set for each group. (These default to 1 and 4094 respectively.) VLANs created within a group must have a VID that falls between these values (inclusive).
|
||||
!!! 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
|
||||
|
||||
|
@ -10,4 +10,4 @@ A human-friendly name that is unique to the assigned virtual machine.
|
||||
|
||||
### Size
|
||||
|
||||
The allocated disk size, in gigabytes.
|
||||
The allocated disk size, in megabytes.
|
||||
|
@ -50,4 +50,13 @@ The amount of running memory provisioned, in megabytes.
|
||||
|
||||
### Disk
|
||||
|
||||
The amount of disk storage provisioned, in gigabytes.
|
||||
The amount of disk storage provisioned, in megabytes.
|
||||
|
||||
!!! warning
|
||||
This field may be directly modified only on virtual machines which do not define discrete [virtual disks](./virtualdisk.md). Otherwise, it will report the sum of all attached disks.
|
||||
|
||||
### 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.
|
||||
|
@ -20,6 +20,12 @@ The operational status of the link. Options include:
|
||||
|
||||
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
|
||||
|
||||
The type of wireless authentication in use. Options include:
|
||||
|
Before Width: | Height: | Size: 3.8 KiB |
@ -1,21 +1,24 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1100 320">
|
||||
<g fill="#9cc8f8" stroke="#9cc8f8">
|
||||
<circle cx="37" cy="284" r="23"/>
|
||||
<circle cx="101" cy="37" r="23"/>
|
||||
<circle cx="101" cy="220" r="23"/>
|
||||
<circle cx="284" cy="220" r="23"/>
|
||||
<rect x="93" y="37" width="16" height="180"/>
|
||||
<rect x="101" y="212" width="180" height="16"/>
|
||||
<rect x="93" y="212" width="16" height="90" transform="rotate(45 101 220)"/>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1299.6 366">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #00857d;
|
||||
}
|
||||
|
||||
.cls-1, .cls-2 {
|
||||
stroke-width: 0px;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #001423;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="Layer_1-2" data-name="Layer 1">
|
||||
<g>
|
||||
<path class="cls-2" d="M337.27,228.59c-12.35,0-22.88,7.8-26.94,18.74h-174.71c-2.9-7.83-9.12-14.04-16.95-16.95V55.67c10.94-4.06,18.74-14.59,18.74-26.94,0-15.87-12.86-28.73-28.73-28.73s-28.73,12.86-28.73,28.73c0,12.35,7.8,22.88,18.74,26.94v174.71c-10.94,4.06-18.74,14.59-18.74,26.94,0,4.28.94,8.33,2.62,11.98l-41.85,41.85c-3.65-1.68-7.7-2.62-11.98-2.62-15.87,0-28.73,12.86-28.73,28.73s12.86,28.73,28.73,28.73,28.73-12.86,28.73-28.73c0-4.28-.94-8.33-2.62-11.98l41.85-41.85c3.65,1.68,7.7,2.62,11.98,2.62,12.35,0,22.88-7.8,26.94-18.74h174.71c4.06,10.94,14.59,18.74,26.94,18.74,15.87,0,28.73-12.86,28.73-28.73s-12.86-28.73-28.73-28.73Z"/>
|
||||
<path class="cls-1" d="M366,28.73c0,15.87-12.86,28.73-28.73,28.73-4.28,0-8.33-.94-11.98-2.62l-41.85,41.85c1.68,3.65,2.62,7.7,2.62,11.98,0,12.35-7.8,22.88-18.74,26.94v174.71c10.94,4.06,18.74,14.59,18.74,26.94,0,15.87-12.86,28.73-28.73,28.73s-28.73-12.86-28.73-28.73c0-12.35,7.8-22.88,18.74-26.94v-174.71c-7.83-2.9-14.04-9.12-16.95-16.95H55.67c-4.06,10.94-14.59,18.74-26.94,18.74-15.87,0-28.73-12.86-28.73-28.73s12.86-28.73,28.73-28.73c12.35,0,22.88,7.8,26.94,18.74h174.71c4.06-10.94,14.59-18.74,26.94-18.74,4.28,0,8.33.94,11.98,2.62l41.85-41.85c-1.68-3.65-2.62-7.7-2.62-11.98,0-15.87,12.86-28.73,28.73-28.73s28.73,12.86,28.73,28.73ZM579.76,136.45c-4.63-4.38-10.18-7.68-16.24-9.66-6.09-2.07-12.48-3.11-18.91-3.08-9.75-.17-19.37,2.17-27.95,6.78-2.68,1.56-5.23,3.35-7.61,5.34v-9.04h-34.53v134.64h34.53v-69.06c-.08-5.7.68-11.38,2.26-16.86,1.26-4.03,3.36-7.74,6.17-10.89,2.41-2.69,5.44-4.74,8.84-5.96,3.71-1.26,7.6-1.89,11.51-1.85,2.99,0,5.97.41,8.84,1.23,2.62.91,5,2.38,6.99,4.32,2.11,2.28,3.78,4.93,4.93,7.81,1.32,4.12,1.95,8.42,1.85,12.74v78.52h34.53v-85.1c.22-7.94-1.18-15.84-4.11-23.23-2.37-6.33-6.16-12.03-11.1-16.65ZM744.41,169.34c2.28,8.16,3.46,16.6,3.49,25.08v13.77h-98.46c.38,2.33,1.22,4.57,2.47,6.58,1.83,3.77,4.51,7.08,7.81,9.66,3.42,2.8,7.32,4.96,11.51,6.37,4.42,1.57,9.08,2.33,13.77,2.26,5.63.24,11.21-1.19,16.03-4.11,5.19-3.31,9.78-7.48,13.57-12.33l3.49-4.11,26.31,20.14-3.29,4.52c-14.18,18.09-34.12,27.34-59.2,27.34-9.78.09-19.49-1.72-28.57-5.34-8.34-3.34-15.84-8.46-21.99-15.01-6.02-6.49-10.7-14.1-13.77-22.4-3.18-8.83-4.78-18.16-4.73-27.54-.02-9.49,1.72-18.9,5.14-27.75,3.36-8.35,8.32-15.96,14.59-22.4,6.24-6.44,13.72-11.54,21.99-15.01,8.74-3.58,18.1-5.4,27.54-5.34,11.92,0,21.99,2.06,30.42,6.37,7.92,3.9,14.87,9.52,20.35,16.44,5.36,6.74,9.28,14.5,11.51,22.82ZM711.31,178.39c-.43-2.36-.98-4.69-1.64-6.99-1.14-3.45-3.04-6.61-5.55-9.25-2.45-2.78-5.56-4.9-9.04-6.17-8.68-3.42-18.36-3.27-26.93.41-3.87,1.69-7.37,4.13-10.28,7.19-2.81,2.83-5.05,6.18-6.58,9.87-.73,1.58-1.28,3.23-1.64,4.93h61.66ZM827.24,230.8c-2.56.57-5.18.84-7.81.82-2.41.12-4.82-.37-6.99-1.44-1.42-1.08-2.55-2.49-3.29-4.11-.93-2.36-1.42-4.87-1.44-7.4-.21-3.29-.41-6.58-.41-9.87v-50.57h33.71v-31.45h-33.71v-34.53h-34.53v34.53h-21.79v31.45h21.79v58.79c-.04,5.15.24,10.3.82,15.42.38,5.56,1.99,10.97,4.73,15.83,3.21,5.18,7.85,9.32,13.36,11.92,5.76,2.88,13.36,4.32,23.43,4.32,3.71-.04,7.42-.31,11.1-.82,4.47-.56,8.79-1.95,12.74-4.11l2.88-1.44v-34.33l-8.43,4.93c-1.93,1.02-4.01,1.72-6.17,2.06ZM997.03,166.46c3.16,8.91,4.76,18.3,4.73,27.75.04,9.32-1.56,18.57-4.73,27.34-3.07,8.3-7.75,15.92-13.77,22.4-6.1,6.56-13.53,11.74-21.79,15.21-8.94,3.62-18.51,5.44-28.16,5.34-9.17-.04-18.22-2.07-26.52-5.96-4.12-1.71-7.93-4.07-11.31-6.99v9.87h-34.53V53.41h34.53v83.04c3.23-2.59,6.75-4.8,10.48-6.58,8.54-4.07,17.88-6.18,27.34-6.17,9.65-.09,19.22,1.72,28.16,5.34,8.18,3.52,15.58,8.62,21.79,15.01,5.91,6.58,10.57,14.17,13.77,22.4ZM963.11,178.8c-1.41-4.39-3.8-8.39-6.99-11.72-3.07-3.26-6.78-5.85-10.89-7.61-9.47-3.57-19.92-3.57-29.39,0-4.12,1.76-7.83,4.35-10.89,7.61-3.12,3.37-5.5,7.37-6.99,11.72-1.71,4.96-2.55,10.17-2.47,15.42-.05,5.24.78,10.45,2.47,15.42,1.54,4.27,3.91,8.18,6.99,11.51,3.01,3.32,6.74,5.92,10.89,7.61,9.42,3.83,19.97,3.83,29.39,0,4.16-1.68,7.88-4.28,10.89-7.61,3.15-3.28,5.54-7.21,6.99-11.51,1.68-4.96,2.52-10.18,2.47-15.42.07-5.24-.77-10.46-2.47-15.42ZM1136.6,244.16c-28.24,27.15-72.89,27.15-101.13,0-13.17-13.29-20.56-31.24-20.55-49.95-.1-28.4,16.95-54.05,43.17-64.95,17.9-7.4,38.01-7.4,55.91,0,26.14,11,43.15,36.59,43.17,64.95,0,18.71-7.38,36.66-20.55,49.95ZM1118.51,178.8c-1.42-4.34-3.73-8.33-6.78-11.72-3.1-3.22-6.8-5.8-10.89-7.61-9.55-3.56-20.05-3.56-29.6,0-4.09,1.81-7.79,4.39-10.89,7.61-3.05,3.39-5.36,7.38-6.78,11.72-1.88,4.92-2.79,10.15-2.67,15.42-.08,5.26.82,10.49,2.67,15.42,1.47,4.25,3.77,8.17,6.78,11.51,3.05,3.28,6.77,5.87,10.89,7.61,9.49,3.84,20.11,3.84,29.6,0,4.13-1.74,7.84-4.33,10.89-7.61,3.01-3.34,5.32-7.26,6.78-11.51,1.75-4.95,2.66-10.16,2.67-15.42,0-5.25-.9-10.47-2.67-15.42ZM1291.58,126.79h-42.34l-26.52,39.47-26.93-39.47h-44.4l48.1,63.1-54.27,71.53h42.96l33.5-47.69,33.71,47.69h44.19l-54.27-71.53,46.25-63.1Z"/>
|
||||
</g>
|
||||
<g fill="#1685fc" stroke="#1685fc">
|
||||
<circle cx="284" cy="37" r="23"/>
|
||||
<circle cx="37" cy="101" r="23"/>
|
||||
<circle cx="220" cy="101" r="23"/>
|
||||
<circle cx="220" cy="284" r="23"/>
|
||||
<rect x="37" y="93" width="180" height="16"/>
|
||||
<rect x="212" y="101" width="16" height="180"/>
|
||||
<rect x="212" y="93" width="16" height="90" transform="rotate(225 220 101)"/>
|
||||
<path transform="translate(380, 8)" d="M13.60 200L13.60 104L36.40 104L36.40 119.40L36.80 119.40Q40.20 112.20 47.20 106.90Q54.20 101.60 66.20 101.60L66.20 101.60Q75.80 101.60 82.50 104.80Q89.20 108 93.40 113.20Q97.60 118.40 99.40 125.20Q101.20 132 101.20 139.40L101.20 139.40L101.20 200L77.20 200L77.20 151.40Q77.20 147.40 76.80 142.50Q76.40 137.60 74.70 133.30Q73 129 69.40 126.10Q65.80 123.20 59.60 123.20L59.60 123.20Q53.60 123.20 49.50 125.20Q45.40 127.20 42.70 130.60Q40 134 38.80 138.40Q37.60 142.80 37.60 147.60L37.60 147.60L37.60 200L13.60 200ZM224.80 160.40L151.60 160.40Q152.80 171.20 160 177.20Q167.20 183.20 177.40 183.20L177.40 183.20Q186.40 183.20 192.50 179.50Q198.60 175.80 203.20 170.20L203.20 170.20L220.40 183.20Q212 193.60 201.60 198Q191.20 202.40 179.80 202.40L179.80 202.40Q169 202.40 159.40 198.80Q149.80 195.20 142.80 188.60Q135.80 182 131.70 172.70Q127.60 163.40 127.60 152L127.60 152Q127.60 140.60 131.70 131.30Q135.80 122 142.80 115.40Q149.80 108.80 159.40 105.20Q169 101.60 179.80 101.60L179.80 101.60Q189.80 101.60 198.10 105.10Q206.40 108.60 212.30 115.20Q218.20 121.80 221.50 131.50Q224.80 141.20 224.80 153.80L224.80 153.80L224.80 160.40ZM151.60 142.40L200.80 142.40Q200.60 131.80 194.20 125.70Q187.80 119.60 176.40 119.60L176.40 119.60Q165.60 119.60 159.30 125.80Q153 132 151.60 142.40L151.60 142.40ZM259.80 124.40L240.00 124.40L240.00 104L259.80 104L259.80 76.20L283.80 76.20L283.80 104L310.20 104L310.20 124.40L283.80 124.40L283.80 166.40Q283.80 173.60 286.50 177.80Q289.20 182 297.20 182L297.20 182Q300.40 182 304.20 181.30Q308 180.60 310.20 179L310.20 179L310.20 199.20Q306.40 201 300.90 201.70Q295.40 202.40 291.20 202.40L291.20 202.40Q281.60 202.40 275.50 200.30Q269.40 198.20 265.90 193.90Q262.40 189.60 261.10 183.20Q259.80 176.80 259.80 168.40L259.80 168.40L259.80 124.40ZM333.20 200L333.20 48.80L357.20 48.80L357.20 116.20L357.80 116.20Q359.60 113.80 362.40 111.30Q365.20 108.80 369.20 106.60Q373.20 104.40 378.40 103Q383.60 101.60 390.40 101.60L390.40 101.60Q400.60 101.60 409.20 105.50Q417.80 109.40 423.90 116.20Q430 123 433.40 132.20Q436.80 141.40 436.80 152L436.80 152Q436.80 162.60 433.60 171.80Q430.40 181 424.20 187.80Q418 194.60 409.20 198.50Q400.40 202.40 389.40 202.40L389.40 202.40Q379.20 202.40 370.40 198.40Q361.60 194.40 356.40 185.60L356.40 185.60L356 185.60L356 200L333.20 200ZM412.80 152L412.80 152Q412.80 146.40 410.90 141.20Q409 136 405.30 132Q401.60 128 396.40 125.60Q391.20 123.20 384.60 123.20L384.60 123.20Q378 123.20 372.80 125.60Q367.60 128 363.90 132Q360.20 136 358.30 141.20Q356.40 146.40 356.40 152L356.40 152Q356.40 157.60 358.30 162.80Q360.20 168 363.90 172Q367.60 176 372.80 178.40Q378 180.80 384.60 180.80L384.60 180.80Q391.20 180.80 396.40 178.40Q401.60 176 405.30 172Q409 168 410.90 162.80Q412.80 157.60 412.80 152ZM458.40 152L458.40 152Q458.40 140.60 462.50 131.30Q466.60 122 473.60 115.40Q480.60 108.80 490.20 105.20Q499.80 101.60 510.60 101.60L510.60 101.60Q521.40 101.60 531 105.20Q540.60 108.80 547.60 115.40Q554.60 122 558.70 131.30Q562.80 140.60 562.80 152L562.80 152Q562.80 163.40 558.70 172.70Q554.60 182 547.60 188.60Q540.60 195.20 531 198.80Q521.40 202.40 510.60 202.40L510.60 202.40Q499.80 202.40 490.20 198.80Q480.60 195.20 473.60 188.60Q466.60 182 462.50 172.70Q458.40 163.40 458.40 152ZM482.40 152L482.40 152Q482.40 157.60 484.30 162.80Q486.20 168 489.90 172Q493.60 176 498.80 178.40Q504 180.80 510.60 180.80L510.60 180.80Q517.20 180.80 522.40 178.40Q527.60 176 531.30 172Q535 168 536.90 162.80Q538.80 157.60 538.80 152L538.80 152Q538.80 146.40 536.90 141.20Q535 136 531.30 132Q527.60 128 522.40 125.60Q517.20 123.20 510.60 123.20L510.60 123.20Q504 123.20 498.80 125.60Q493.60 128 489.90 132Q486.20 136 484.30 141.20Q482.40 146.40 482.40 152ZM575.40 200L614 148.40L580.80 104L610 104L629.20 132.80L650 104L677.40 104L644.60 148.40L683.20 200L654 200L629 165.60L603.80 200L575.40 200Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 5.0 KiB |
99
docs/plugins/development/background-jobs.md
Normal file
@ -0,0 +1,99 @@
|
||||
# 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.
|
||||
|
||||
## Job Runners
|
||||
|
||||
A background job implements a basic [Job](../../models/core/job.md) executor for all kinds of tasks. It has logic implemented to handle the management of the associated job object, rescheduling of periodic jobs in the given interval and error handling. Adding custom jobs is done by subclassing NetBox's `JobRunner` class.
|
||||
|
||||
::: netbox.jobs.JobRunner
|
||||
|
||||
#### Example
|
||||
|
||||
```python title="jobs.py"
|
||||
from netbox.jobs import JobRunner
|
||||
|
||||
|
||||
class MyTestJob(JobRunner):
|
||||
class Meta:
|
||||
name = "My Test Job"
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
obj = self.job.object
|
||||
# your logic goes here
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
### Attributes
|
||||
|
||||
`JobRunner` attributes are defined under a class named `Meta` within the job. These are optional, but encouraged.
|
||||
|
||||
#### `name`
|
||||
|
||||
This is the human-friendly names of your background job. If omitted, the class name will be used.
|
||||
|
||||
### Scheduled Jobs
|
||||
|
||||
As described above, jobs can be scheduled for immediate execution or at any later time using the `enqueue()` method. However, for management purposes, the `enqueue_once()` method allows a job to be scheduled exactly once avoiding duplicates. If a job is already scheduled for a particular instance, a second one won't be scheduled, respecting thread safety. An example use case would be to schedule a periodic task that is bound to an instance in general, but not to any event of that instance (such as updates). The parameters of the `enqueue_once()` method are identical to those of `enqueue()`.
|
||||
|
||||
!!! tip
|
||||
It is not forbidden to `enqueue()` additional jobs while an interval schedule is active. An example use of this would be to schedule a periodic daily synchronization, but also trigger additional synchronizations on demand when the user presses a button.
|
||||
|
||||
#### Example
|
||||
|
||||
```python title="jobs.py"
|
||||
from netbox.jobs import JobRunner
|
||||
|
||||
|
||||
class MyHousekeepingJob(JobRunner):
|
||||
class Meta:
|
||||
name = "Housekeeping"
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
# your logic goes here
|
||||
```
|
||||
|
||||
```python title="__init__.py"
|
||||
from netbox.plugins import PluginConfig
|
||||
|
||||
class MyPluginConfig(PluginConfig):
|
||||
def ready(self):
|
||||
from .jobs import MyHousekeepingJob
|
||||
MyHousekeepingJob.setup(interval=60)
|
||||
```
|
||||
|
||||
## Task queues
|
||||
|
||||
Three task queues of differing priority are defined by default:
|
||||
|
||||
* High
|
||||
* Default
|
||||
* Low
|
||||
|
||||
Any tasks in the "high" queue are completed before the default queue is checked, and any tasks in the default queue are completed before those in the "low" queue.
|
||||
|
||||
Plugins can also add custom queues for their own needs by setting the `queues` attribute under the PluginConfig class. An example is included below:
|
||||
|
||||
```python
|
||||
class MyPluginConfig(PluginConfig):
|
||||
name = 'myplugin'
|
||||
...
|
||||
queues = [
|
||||
'foo',
|
||||
'bar',
|
||||
]
|
||||
```
|
||||
|
||||
The `PluginConfig` above creates two custom queues with the following names `my_plugin.foo` and `my_plugin.bar`. (The plugin's name is prepended to each queue to avoid conflicts between plugins.)
|
||||
|
||||
!!! warning "Configuring the RQ worker process"
|
||||
By default, NetBox's RQ worker process only services the high, default, and low queues. Plugins which introduce custom queues should advise users to either reconfigure the default worker, or run a dedicated worker specifying the necessary queues. For example:
|
||||
|
||||
```
|
||||
python manage.py rqworker my_plugin.foo my_plugin.bar
|
||||
```
|
@ -1,30 +0,0 @@
|
||||
# Background Tasks
|
||||
|
||||
NetBox supports the queuing of tasks that need to be performed in the background, decoupled from the request-response cycle, using the [Python RQ](https://python-rq.org/) library. Three task queues of differing priority are defined by default:
|
||||
|
||||
* High
|
||||
* Default
|
||||
* Low
|
||||
|
||||
Any tasks in the "high" queue are completed before the default queue is checked, and any tasks in the default queue are completed before those in the "low" queue.
|
||||
|
||||
Plugins can also add custom queues for their own needs by setting the `queues` attribute under the PluginConfig class. An example is included below:
|
||||
|
||||
```python
|
||||
class MyPluginConfig(PluginConfig):
|
||||
name = 'myplugin'
|
||||
...
|
||||
queues = [
|
||||
'foo',
|
||||
'bar',
|
||||
]
|
||||
```
|
||||
|
||||
The PluginConfig above creates two custom queues with the following names `my_plugin.foo` and `my_plugin.bar`. (The plugin's name is prepended to each queue to avoid conflicts between plugins.)
|
||||
|
||||
!!! warning "Configuring the RQ worker process"
|
||||
By default, NetBox's RQ worker process only services the high, default, and low queues. Plugins which introduce custom queues should advise users to either reconfigure the default worker, or run a dedicated worker specifying the necessary queues. For example:
|
||||
|
||||
```
|
||||
python manage.py rqworker my_plugin.foo my_plugin.bar
|
||||
```
|
18
docs/plugins/development/event-types.md
Normal file
@ -0,0 +1,18 @@
|
||||
# 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
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from netbox.events import EventType, EVENT_TYPE_KIND_SUCCESS
|
||||
|
||||
EventType(
|
||||
name='ticket_opened',
|
||||
text=_('Ticket opened'),
|
||||
kind=EVENT_TYPE_KIND_SUCCESS
|
||||
).register()
|
||||
```
|
||||
|
||||
::: netbox.events.EventType
|
@ -47,6 +47,7 @@ project-name/
|
||||
- __init__.py
|
||||
- filtersets.py
|
||||
- graphql.py
|
||||
- jobs.py
|
||||
- models.py
|
||||
- middleware.py
|
||||
- navigation.py
|
||||
|
@ -130,6 +130,8 @@ For more information about database migrations, see the [Django documentation](h
|
||||
|
||||
::: netbox.models.features.ExportTemplatesMixin
|
||||
|
||||
::: netbox.models.features.JobsMixin
|
||||
|
||||
::: netbox.models.features.JournalingMixin
|
||||
|
||||
::: netbox.models.features.TagsMixin
|
||||
|
@ -25,9 +25,11 @@ project-name/
|
||||
|
||||
Serializers are responsible for converting Python objects to JSON data suitable for conveying to consumers, and vice versa. NetBox provides the `NetBoxModelSerializer` class for use by plugins to handle the assignment of tags and custom field data. (These features can also be included ad hoc via the `CustomFieldModelSerializer` and `TaggableModelSerializer` classes.)
|
||||
|
||||
The default nested representation of an object is defined by the `brief_fields` attributes under the serializer's `Meta` class. (Older versions of NetBox required the definition of a separate nested serializer.)
|
||||
|
||||
#### Example
|
||||
|
||||
To create a serializer for a plugin model, subclass `NetBoxModelSerializer` in `api/serializers.py`. Specify the model class and the fields to include within the serializer's `Meta` class. It is generally advisable to include a `url` attribute on each serializer. This will render the direct link to access the object being rendered.
|
||||
To create a serializer for a plugin model, subclass `NetBoxModelSerializer` in `api/serializers.py`. Specify the model class and the fields to include within the serializer's `Meta` class.
|
||||
|
||||
```python
|
||||
# api/serializers.py
|
||||
@ -36,40 +38,12 @@ from netbox.api.serializers import NetBoxModelSerializer
|
||||
from my_plugin.models import MyModel
|
||||
|
||||
class MyModelSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name='plugins-api:myplugin-api:mymodel-detail'
|
||||
)
|
||||
foo = SiteSerializer(nested=True, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = MyModel
|
||||
fields = ('id', 'foo', 'bar', 'baz')
|
||||
```
|
||||
|
||||
### Nested Serializers
|
||||
|
||||
There are two cases where it is generally desirable to show only a minimal representation of an object:
|
||||
|
||||
1. When displaying an object related to the one being viewed (for example, the region to which a site is assigned)
|
||||
2. Listing many objects using "brief" mode
|
||||
|
||||
To accommodate these, it is recommended to create nested serializers accompanying the "full" serializer for each model. NetBox provides the `WritableNestedSerializer` class for just this purpose. This class accepts a primary key value on write, but displays an object representation for read requests. It also includes a read-only `display` attribute which conveys the string representation of the object.
|
||||
|
||||
#### Example
|
||||
|
||||
```python
|
||||
# api/serializers.py
|
||||
from rest_framework import serializers
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
from my_plugin.models import MyModel
|
||||
|
||||
class NestedMyModelSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name='plugins-api:myplugin-api:mymodel-detail'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = MyModel
|
||||
fields = ('id', 'display', 'foo')
|
||||
brief_fields = ('id', 'url', 'display', 'bar')
|
||||
```
|
||||
|
||||
## Viewsets
|
||||
|
@ -191,19 +191,25 @@ class MyView(generic.ObjectView):
|
||||
|
||||
### Extra Template Content
|
||||
|
||||
Plugins can inject custom content into certain areas of core NetBox views. This is accomplished by subclassing `PluginTemplateExtension`, designating a particular NetBox model, and defining the desired method(s) to render custom content. Five methods are available:
|
||||
Plugins can inject custom content into certain areas of core NetBox views. This is accomplished by subclassing `PluginTemplateExtension`, optionally designating one or more particular NetBox models, and defining the desired method(s) to render custom content. Five methods are available:
|
||||
|
||||
| Method | View | Description |
|
||||
|---------------------|-------------|-----------------------------------------------------|
|
||||
| `navbar()` | All | Inject content inside the top navigation bar |
|
||||
| `list_buttons()` | List view | Add buttons to the top of the page |
|
||||
| `buttons()` | Object view | Add buttons to the top of the page |
|
||||
| `alerts()` | Object view | Inject content at the top of the page |
|
||||
| `left_page()` | Object view | Inject content on the left side of the page |
|
||||
| `right_page()` | Object view | Inject content on the right side of the page |
|
||||
| `full_width_page()` | Object view | Inject content across the entire bottom of the page |
|
||||
| `buttons()` | Object view | Add buttons to the top of the page |
|
||||
| `list_buttons()` | List view | Add buttons to the top of the page |
|
||||
|
||||
!!! 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.
|
||||
|
||||
When a PluginTemplateExtension is instantiated, context data is assigned to `self.context`. Available data include:
|
||||
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.
|
||||
|
||||
When a PluginTemplateExtension is instantiated, context data is assigned to `self.context`. Available data includes:
|
||||
|
||||
* `object` - The object being viewed (object views only)
|
||||
* `model` - The model of the list view (list views only)
|
||||
@ -220,7 +226,7 @@ from netbox.plugins import PluginTemplateExtension
|
||||
from .models import Animal
|
||||
|
||||
class SiteAnimalCount(PluginTemplateExtension):
|
||||
model = 'dcim.site'
|
||||
models = ['dcim.site']
|
||||
|
||||
def right_page(self):
|
||||
return self.render('netbox_animal_sounds/inc/animal_count.html', extra_context={
|
||||
|
126
docs/release-notes/version-4.1.md
Normal file
@ -0,0 +1,126 @@
|
||||
# NetBox v4.1
|
||||
|
||||
## v4.1.0 (2024-09-03)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* Several filters deprecated in v4.0 have been removed (see [#15410](https://github.com/netbox-community/netbox/issues/15410)).
|
||||
* The unit size for `VirtualMachine.disk` and `VirtualDisk.size` has been changed from 1 gigabyte to 1 megabyte. Existing values will be adjusted automatically during the upgrade process.
|
||||
* The `min_vid` and `max_vid` fields on the VLAN group model have been replaced with `vid_ranges`, an array of starting and ending VLAN ID pairs.
|
||||
* The five individual event type fields on the EventRule model have been replaced by a single `event_types` array field, which lists applicable event types by name.
|
||||
* All UI views & API endpoints associated with change records have been moved from `/extras` to `/core`.
|
||||
* The `validate()` method on CustomValidator subclasses now **must** accept the request argument (deprecated in v4.0 by [#14279](https://github.com/netbox-community/netbox/issues/14279/)).
|
||||
|
||||
### New Features
|
||||
|
||||
#### Circuit Groups ([#7025](https://github.com/netbox-community/netbox/issues/7025))
|
||||
|
||||
Circuits can now be assigned to groups for administrative purposes. Each circuit may be assigned to multiple groups, and each assignment may optionally indicate a priority (primary, secondary, or tertiary).
|
||||
|
||||
#### VLAN Group ID Ranges ([#9627](https://github.com/netbox-community/netbox/issues/9627))
|
||||
|
||||
The VLAN group model has been enhanced to support multiple VLAN ID (VID) ranges, whereas previously it could track only a single beginning and ending VID pair. VID ranges are stored as an array of beginning and ending (inclusive) integer pairs, e.g. `1-100,1000-1999`.
|
||||
|
||||
#### Nested Device Modules ([#10500](https://github.com/netbox-community/netbox/issues/10500))
|
||||
|
||||
Module bays can now be added to modules to effect a hierarchical arrangement of submodules within a device. A module installed within a device's module bay may itself have module bays into which child modules may be installed.
|
||||
|
||||
#### Rack Types ([#12826](https://github.com/netbox-community/netbox/issues/12826))
|
||||
|
||||
A new rack type model has been introduced, which functions similarly to device types. Users can now define a common make and model of equipment rack, the attributes of which are automatically populated when creating a new rack of that type. Backward compatibility for racks with individually defined characteristics is fully retained.
|
||||
|
||||
#### Plugins Catalog Integration ([#14731](https://github.com/netbox-community/netbox/issues/14731))
|
||||
|
||||
The NetBox UI now integrates directly with the canonical [plugins catalog](https://netboxlabs.com/netbox-plugins/) hosted by [NetBox Labs](https://netboxlabs.com). Users can now explore available plugins and check for newer releases natively within the NetBox user interface.
|
||||
|
||||
#### User Notifications ([#15621](https://github.com/netbox-community/netbox/issues/15621))
|
||||
|
||||
NetBox now includes a user notification system. Users can subscribe to individual objects and be alerted to changes within the web interface. Additionally, event rules can be created to trigger notifications for specific users and/or groups. Plugins can also employ this notification system for their own purposes.
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#7537](https://github.com/netbox-community/netbox/issues/7537) - Add a serial number field for virtual machines
|
||||
* [#8198](https://github.com/netbox-community/netbox/issues/8198) - Enable uniqueness enforcement for custom field values
|
||||
* [#8984](https://github.com/netbox-community/netbox/issues/8984) - Enable filtering of custom script output by log level
|
||||
* [#11969](https://github.com/netbox-community/netbox/issues/11969) - Support for tracking airflow on racks and module types
|
||||
* [#14656](https://github.com/netbox-community/netbox/issues/14656) - Dynamically render the custom field edit form depending on the selected field type
|
||||
* [#15106](https://github.com/netbox-community/netbox/issues/15106) - Add `distance` and `distance_unit` fields for wireless links
|
||||
* [#15156](https://github.com/netbox-community/netbox/issues/15156) - Add `display_url` field to all REST API serializers, which links to the corresponding UI view for an object
|
||||
* [#16574](https://github.com/netbox-community/netbox/issues/16574) - Add `last_synced` time to REST API serializer for data sources
|
||||
* [#16580](https://github.com/netbox-community/netbox/issues/16580) - Enable plugin views to enforce `LOGIN_REQUIRED` selectively (remove `AUTH_EXEMPT_PATHS`)
|
||||
* [#16782](https://github.com/netbox-community/netbox/issues/16782) - Enable filtering of selection choices for object and multi-object custom fields
|
||||
* [#16907](https://github.com/netbox-community/netbox/issues/16907) - Update user interface styling
|
||||
* [#17051](https://github.com/netbox-community/netbox/issues/17051) - Introduce `ISOLATED_DEPLOYMENT` config parameter for denoting Internet isolation
|
||||
* [#17221](https://github.com/netbox-community/netbox/issues/17221) - `ObjectEditView` now supports HTMX-based object editing
|
||||
* [#17288](https://github.com/netbox-community/netbox/issues/17288) - Introduce a configurable limit on the number of aliases within a GraphQL API request
|
||||
* [#17289](https://github.com/netbox-community/netbox/issues/17289) - Enforce a standard policy for local passwords by default
|
||||
* [#17318](https://github.com/netbox-community/netbox/issues/17318) - Include the assigned provider in nested API representation of circuits
|
||||
|
||||
### Bug Fixes (From Beta1)
|
||||
|
||||
* [#17086](https://github.com/netbox-community/netbox/issues/17086) - Fix exception when viewing a job with no related object
|
||||
* [#17097](https://github.com/netbox-community/netbox/issues/17097) - Record static object representation when calling `NotificationGroup.notify()`
|
||||
* [#17098](https://github.com/netbox-community/netbox/issues/17098) - Prevent automatic deletion of related notifications when deleting an object
|
||||
* [#17159](https://github.com/netbox-community/netbox/issues/17159) - Correct file paths in plugin installation instructions
|
||||
* [#17163](https://github.com/netbox-community/netbox/issues/17163) - Fix filtering of related services under IP address view
|
||||
* [#17169](https://github.com/netbox-community/netbox/issues/17169) - Avoid duplicating catalog listings for installed plugins
|
||||
* [#17301](https://github.com/netbox-community/netbox/issues/17301) - Correct styling of the edit & delete buttons for custom script modules
|
||||
* [#17302](https://github.com/netbox-community/netbox/issues/17302) - Fix log level filtering support for custom script messages
|
||||
* [#17306](https://github.com/netbox-community/netbox/issues/17306) - Correct rounding of reported VLAN group utilization
|
||||
|
||||
### Plugins
|
||||
|
||||
* [#15692](https://github.com/netbox-community/netbox/issues/15692) - Introduce improved plugin support for background jobs
|
||||
* [#16359](https://github.com/netbox-community/netbox/issues/16359) - Enable plugins to embed content in the top navigation bar
|
||||
* [#16726](https://github.com/netbox-community/netbox/issues/16726) - Extend `PluginTemplateExtension` to enable registering multiple models
|
||||
* [#16776](https://github.com/netbox-community/netbox/issues/16776) - Add an `alerts()` method to `PluginTemplateExtension` for embedding important information on object views
|
||||
* [#16886](https://github.com/netbox-community/netbox/issues/16886) - Introduce a mechanism for plugins to register custom event types (for use with user notifications)
|
||||
|
||||
### Other Changes
|
||||
|
||||
* [#14692](https://github.com/netbox-community/netbox/issues/14692) - Change the atomic unit for virtual disks from 1GB to 1MB
|
||||
* [#14861](https://github.com/netbox-community/netbox/issues/14861) - The URL path for UI views concerning virtual disks has been standardized to `/virtualization/virtual-disks/`
|
||||
* [#15410](https://github.com/netbox-community/netbox/issues/15410) - Remove various deprecated query filters
|
||||
* [#15908](https://github.com/netbox-community/netbox/issues/15908) - Indicate product edition in release data
|
||||
* [#16388](https://github.com/netbox-community/netbox/issues/16388) - Move all change logging resources from `extras` to `core`
|
||||
* [#16884](https://github.com/netbox-community/netbox/issues/16884) - Remove the ID column from the default table configuration for changelog records
|
||||
* [#16988](https://github.com/netbox-community/netbox/issues/16988) - Relocate rack items in navigation menu
|
||||
* [#17143](https://github.com/netbox-community/netbox/issues/17143) - The use of legacy "nested" serializer classes has been deprecated
|
||||
|
||||
### REST API Changes
|
||||
|
||||
* The `/api/extras/object-changes/` endpoint has moved to `/api/core/object-changes/`.
|
||||
* Most object representations now include a read-only `display_url` field, which links to the object's corresponding UI view.
|
||||
* Added the following endpoints:
|
||||
* `/api/circuits/circuit-groups/`
|
||||
* `/api/circuits/circuit-group-assignments/`
|
||||
* `/api/dcim/rack-types/`
|
||||
* `/api/extras/notification-groups/`
|
||||
* `/api/extras/notifications/`
|
||||
* `/api/extras/subscriptions/`
|
||||
* circuits.Circuit
|
||||
* Added the `assignments` field, which lists all group assignments
|
||||
* core.DataSource
|
||||
* Added the read-only `last_synced` field
|
||||
* dcim.ModuleBay
|
||||
* Added the optional `module` foreign key field
|
||||
* dcim.ModuleBayTemplate
|
||||
* Added the optional `module_type` foreign key field
|
||||
* dcim.ModuleType
|
||||
* Added the optional `airflow` choice field
|
||||
* dcim.Rack
|
||||
* Added the optional `rack_type` foreign key field
|
||||
* Added the optional `airflow` choice field
|
||||
* extras.CustomField
|
||||
* Added the `related_object_filter` JSON field for object and multi-object custom fields
|
||||
* Added the `validation_unique` boolean field
|
||||
* extras.EventRule
|
||||
* Removed the `type_create`, `type_update`, `type_delete`, `type_job_start`, and `type_job_end` boolean fields
|
||||
* Added the `event_types` array field
|
||||
* ipam.VLANGroup
|
||||
* Removed the `min_vid` and `max_vid` fields
|
||||
* Added the `vid_ranges` field, an array of starting & ending VLAN IDs
|
||||
* virtualization.VirtualMachine
|
||||
* Added the optional `serial` field
|
||||
* wireless.WirelessLink
|
||||
* Added the optional `distance` and `distance_unit` fields
|
12
mkdocs.yml
@ -86,6 +86,7 @@ nav:
|
||||
- Change Logging: 'features/change-logging.md'
|
||||
- Journaling: 'features/journaling.md'
|
||||
- Event Rules: 'features/event-rules.md'
|
||||
- Notifications: 'features/notifications.md'
|
||||
- Background Jobs: 'features/background-jobs.md'
|
||||
- Auth & Permissions: 'features/authentication-permissions.md'
|
||||
- API & Integration: 'features/api-integration.md'
|
||||
@ -108,6 +109,7 @@ nav:
|
||||
- Required Parameters: 'configuration/required-parameters.md'
|
||||
- System: 'configuration/system.md'
|
||||
- Security: 'configuration/security.md'
|
||||
- GraphQL API: 'configuration/graphql-api.md'
|
||||
- Remote Authentication: 'configuration/remote-authentication.md'
|
||||
- Data & Validation: 'configuration/data-validation.md'
|
||||
- Default Values: 'configuration/default-values.md'
|
||||
@ -142,10 +144,11 @@ nav:
|
||||
- Forms: 'plugins/development/forms.md'
|
||||
- Filters & Filter Sets: 'plugins/development/filtersets.md'
|
||||
- Search: 'plugins/development/search.md'
|
||||
- Event Types: 'plugins/development/event-types.md'
|
||||
- Data Backends: 'plugins/development/data-backends.md'
|
||||
- REST API: 'plugins/development/rest-api.md'
|
||||
- GraphQL API: 'plugins/development/graphql-api.md'
|
||||
- Background Tasks: 'plugins/development/background-tasks.md'
|
||||
- Background Jobs: 'plugins/development/background-jobs.md'
|
||||
- Dashboard Widgets: 'plugins/development/dashboard-widgets.md'
|
||||
- Staged Changes: 'plugins/development/staged-changes.md'
|
||||
- Exceptions: 'plugins/development/exceptions.md'
|
||||
@ -163,6 +166,8 @@ nav:
|
||||
- Data Model:
|
||||
- Circuits:
|
||||
- Circuit: 'models/circuits/circuit.md'
|
||||
- CircuitGroup: 'models/circuits/circuitgroup.md'
|
||||
- CircuitGroupAssignment: 'models/circuits/circuitgroupassignment.md'
|
||||
- Circuit Termination: 'models/circuits/circuittermination.md'
|
||||
- Circuit Type: 'models/circuits/circuittype.md'
|
||||
- Provider: 'models/circuits/provider.md'
|
||||
@ -206,6 +211,7 @@ nav:
|
||||
- Rack: 'models/dcim/rack.md'
|
||||
- RackReservation: 'models/dcim/rackreservation.md'
|
||||
- RackRole: 'models/dcim/rackrole.md'
|
||||
- RackType: 'models/dcim/racktype.md'
|
||||
- RearPort: 'models/dcim/rearport.md'
|
||||
- RearPortTemplate: 'models/dcim/rearporttemplate.md'
|
||||
- Region: 'models/dcim/region.md'
|
||||
@ -225,8 +231,11 @@ nav:
|
||||
- ExportTemplate: 'models/extras/exporttemplate.md'
|
||||
- ImageAttachment: 'models/extras/imageattachment.md'
|
||||
- JournalEntry: 'models/extras/journalentry.md'
|
||||
- Notification: 'models/extras/notification.md'
|
||||
- NotificationGroup: 'models/extras/notificationgroup.md'
|
||||
- SavedFilter: 'models/extras/savedfilter.md'
|
||||
- StagedChange: 'models/extras/stagedchange.md'
|
||||
- Subscription: 'models/extras/subscription.md'
|
||||
- Tag: 'models/extras/tag.md'
|
||||
- Webhook: 'models/extras/webhook.md'
|
||||
- IPAM:
|
||||
@ -296,6 +305,7 @@ nav:
|
||||
- git Cheat Sheet: 'development/git-cheat-sheet.md'
|
||||
- Release Notes:
|
||||
- Summary: 'release-notes/index.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'
|
||||
- Version 3.6: 'release-notes/version-3.6.md'
|
||||
|
@ -9,6 +9,8 @@ urlpatterns = [
|
||||
# Account views
|
||||
path('profile/', views.ProfileView.as_view(), name='profile'),
|
||||
path('bookmarks/', views.BookmarkListView.as_view(), name='bookmarks'),
|
||||
path('notifications/', views.NotificationListView.as_view(), name='notifications'),
|
||||
path('subscriptions/', views.SubscriptionListView.as_view(), name='subscriptions'),
|
||||
path('preferences/', views.UserConfigView.as_view(), name='preferences'),
|
||||
path('password/', views.ChangePasswordView.as_view(), name='change_password'),
|
||||
path('api-tokens/', views.UserTokenListView.as_view(), name='usertoken_list'),
|
||||
|
@ -19,8 +19,10 @@ from django.views.generic import View
|
||||
from social_core.backends.utils import load_backends
|
||||
|
||||
from account.models import UserToken
|
||||
from extras.models import Bookmark, ObjectChange
|
||||
from extras.tables import BookmarkTable, ObjectChangeTable
|
||||
from core.models import ObjectChange
|
||||
from core.tables import ObjectChangeTable
|
||||
from extras.models import Bookmark
|
||||
from extras.tables import BookmarkTable, NotificationTable, SubscriptionTable
|
||||
from netbox.authentication import get_auth_backend_display, get_saml_idps
|
||||
from netbox.config import get_config
|
||||
from netbox.views import generic
|
||||
@ -275,6 +277,36 @@ class BookmarkListView(LoginRequiredMixin, generic.ObjectListView):
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# Notifications & subscriptions
|
||||
#
|
||||
|
||||
class NotificationListView(LoginRequiredMixin, generic.ObjectListView):
|
||||
table = NotificationTable
|
||||
template_name = 'account/notifications.html'
|
||||
|
||||
def get_queryset(self, request):
|
||||
return request.user.notifications.all()
|
||||
|
||||
def get_extra_context(self, request):
|
||||
return {
|
||||
'active_tab': 'notifications',
|
||||
}
|
||||
|
||||
|
||||
class SubscriptionListView(LoginRequiredMixin, generic.ObjectListView):
|
||||
table = SubscriptionTable
|
||||
template_name = 'account/subscriptions.html'
|
||||
|
||||
def get_queryset(self, request):
|
||||
return request.user.subscriptions.all()
|
||||
|
||||
def get_extra_context(self, request):
|
||||
return {
|
||||
'active_tab': 'subscriptions',
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# User views for token management
|
||||
#
|
||||
|
@ -1,9 +1,11 @@
|
||||
import warnings
|
||||
|
||||
from drf_spectacular.utils import extend_schema_serializer
|
||||
from rest_framework import serializers
|
||||
|
||||
from circuits.models import *
|
||||
from netbox.api.fields import RelatedObjectCountField
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
from .serializers_.nested import NestedProviderAccountSerializer
|
||||
|
||||
__all__ = [
|
||||
'NestedCircuitSerializer',
|
||||
@ -14,17 +16,22 @@ __all__ = [
|
||||
'NestedProviderAccountSerializer',
|
||||
]
|
||||
|
||||
# TODO: Remove in v4.2
|
||||
warnings.warn(
|
||||
f"Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
|
||||
DeprecationWarning
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Provider networks
|
||||
#
|
||||
|
||||
class NestedProviderNetworkSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail')
|
||||
|
||||
class Meta:
|
||||
model = ProviderNetwork
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name']
|
||||
|
||||
|
||||
#
|
||||
@ -35,24 +42,11 @@ class NestedProviderNetworkSerializer(WritableNestedSerializer):
|
||||
exclude_fields=('circuit_count',),
|
||||
)
|
||||
class NestedProviderSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
|
||||
circuit_count = RelatedObjectCountField('circuits')
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'circuit_count']
|
||||
|
||||
|
||||
#
|
||||
# Provider Accounts
|
||||
#
|
||||
|
||||
class NestedProviderAccountSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provideraccount-detail')
|
||||
|
||||
class Meta:
|
||||
model = ProviderAccount
|
||||
fields = ['id', 'url', 'display', 'name', 'account']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'circuit_count']
|
||||
|
||||
|
||||
#
|
||||
@ -63,26 +57,23 @@ class NestedProviderAccountSerializer(WritableNestedSerializer):
|
||||
exclude_fields=('circuit_count',),
|
||||
)
|
||||
class NestedCircuitTypeSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
|
||||
circuit_count = RelatedObjectCountField('circuits')
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'circuit_count']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'circuit_count']
|
||||
|
||||
|
||||
class NestedCircuitSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = ['id', 'url', 'display', 'cid']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'cid']
|
||||
|
||||
|
||||
class NestedCircuitTerminationSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
||||
circuit = NestedCircuitSerializer()
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = ['id', 'url', 'display', 'circuit', 'term_side', 'cable', '_occupied']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'circuit', 'term_side', 'cable', '_occupied']
|
||||
|
@ -1,3 +1,2 @@
|
||||
from .serializers_.providers import *
|
||||
from .serializers_.circuits import *
|
||||
from .nested_serializers import *
|
||||
|
@ -1,7 +1,5 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from circuits.choices import CircuitStatusChoices
|
||||
from circuits.models import Circuit, CircuitTermination, CircuitType
|
||||
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
|
||||
@ -12,13 +10,14 @@ from .providers import ProviderAccountSerializer, ProviderNetworkSerializer, Pro
|
||||
|
||||
__all__ = (
|
||||
'CircuitSerializer',
|
||||
'CircuitGroupAssignmentSerializer',
|
||||
'CircuitGroupSerializer',
|
||||
'CircuitTerminationSerializer',
|
||||
'CircuitTypeSerializer',
|
||||
)
|
||||
|
||||
|
||||
class CircuitTypeSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
|
||||
|
||||
# Related object counts
|
||||
circuit_count = RelatedObjectCountField('circuits')
|
||||
@ -26,27 +25,53 @@ class CircuitTypeSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created',
|
||||
'last_updated', 'circuit_count',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', 'circuit_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count')
|
||||
|
||||
|
||||
class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
||||
site = SiteSerializer(nested=True, allow_null=True)
|
||||
provider_network = ProviderNetworkSerializer(nested=True, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'id', 'url', 'display', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
|
||||
'description',
|
||||
'id', 'url', 'display_url', 'display', 'site', 'provider_network', 'port_speed', 'upstream_speed',
|
||||
'xconnect_id', 'description',
|
||||
]
|
||||
|
||||
|
||||
class CircuitGroupSerializer(NetBoxModelSerializer):
|
||||
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||
circuit_count = RelatedObjectCountField('assignments')
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroup
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'tenant',
|
||||
'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count'
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name')
|
||||
|
||||
|
||||
class CircuitGroupAssignmentSerializer_(NetBoxModelSerializer):
|
||||
"""
|
||||
Base serializer for group assignments under CircuitSerializer.
|
||||
"""
|
||||
group = CircuitGroupSerializer(nested=True)
|
||||
priority = ChoiceField(choices=CircuitPriorityChoices, allow_blank=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroupAssignment
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'group', 'priority', 'tags', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'group', 'priority')
|
||||
|
||||
|
||||
class CircuitSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
|
||||
provider = ProviderSerializer(nested=True)
|
||||
provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True, default=None)
|
||||
status = ChoiceField(choices=CircuitStatusChoices, required=False)
|
||||
@ -54,19 +79,19 @@ class CircuitSerializer(NetBoxModelSerializer):
|
||||
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)
|
||||
assignments = CircuitGroupAssignmentSerializer_(nested=True, many=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = [
|
||||
'id', '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',
|
||||
'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',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'cid', 'description')
|
||||
brief_fields = ('id', 'url', 'display', 'provider', 'cid', 'description')
|
||||
|
||||
|
||||
class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
||||
circuit = CircuitSerializer(nested=True)
|
||||
site = SiteSerializer(nested=True, required=False, allow_null=True)
|
||||
provider_network = ProviderNetworkSerializer(nested=True, required=False, allow_null=True)
|
||||
@ -74,8 +99,19 @@ class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'id', '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', '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',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'circuit', 'term_side', 'description', 'cable', '_occupied')
|
||||
|
||||
|
||||
class CircuitGroupAssignmentSerializer(CircuitGroupAssignmentSerializer_):
|
||||
circuit = CircuitSerializer(nested=True)
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroupAssignment
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'group', 'circuit', 'priority', 'tags', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'group', 'circuit', 'priority')
|
||||
|
13
netbox/circuits/api/serializers_/nested.py
Normal file
@ -0,0 +1,13 @@
|
||||
from circuits.models import ProviderAccount
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
|
||||
__all__ = (
|
||||
'NestedProviderAccountSerializer',
|
||||
)
|
||||
|
||||
|
||||
class NestedProviderAccountSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ProviderAccount
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'account']
|
@ -5,7 +5,7 @@ from ipam.api.serializers_.asns import ASNSerializer
|
||||
from ipam.models import ASN
|
||||
from netbox.api.fields import RelatedObjectCountField, SerializedPKRelatedField
|
||||
from netbox.api.serializers import NetBoxModelSerializer
|
||||
from ..nested_serializers import *
|
||||
from .nested import NestedProviderAccountSerializer
|
||||
|
||||
__all__ = (
|
||||
'ProviderAccountSerializer',
|
||||
@ -15,7 +15,6 @@ __all__ = (
|
||||
|
||||
|
||||
class ProviderSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
|
||||
accounts = SerializedPKRelatedField(
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
serializer=NestedProviderAccountSerializer,
|
||||
@ -36,34 +35,32 @@ class ProviderSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'accounts', 'description', 'comments', 'asns', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'circuit_count',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'accounts', 'description', 'comments',
|
||||
'asns', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count')
|
||||
|
||||
|
||||
class ProviderAccountSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provideraccount-detail')
|
||||
provider = ProviderSerializer(nested=True)
|
||||
name = serializers.CharField(allow_blank=True, max_length=100, required=False, default='')
|
||||
|
||||
class Meta:
|
||||
model = ProviderAccount
|
||||
fields = [
|
||||
'id', 'url', 'display', 'provider', 'name', 'account', 'description', 'comments', 'tags', 'custom_fields',
|
||||
'created', 'last_updated',
|
||||
'id', 'url', 'display_url', 'display', 'provider', 'name', 'account', 'description', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'account', 'description')
|
||||
|
||||
|
||||
class ProviderNetworkSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail')
|
||||
provider = ProviderSerializer(nested=True)
|
||||
|
||||
class Meta:
|
||||
model = ProviderNetwork
|
||||
fields = [
|
||||
'id', 'url', 'display', 'provider', 'name', 'service_id', 'description', 'comments', 'tags',
|
||||
'id', 'url', 'display_url', 'display', 'provider', 'name', 'service_id', 'description', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||
|
@ -14,6 +14,8 @@ router.register('provider-networks', views.ProviderNetworkViewSet)
|
||||
router.register('circuit-types', views.CircuitTypeViewSet)
|
||||
router.register('circuits', views.CircuitViewSet)
|
||||
router.register('circuit-terminations', views.CircuitTerminationViewSet)
|
||||
router.register('circuit-groups', views.CircuitGroupViewSet)
|
||||
router.register('circuit-group-assignments', views.CircuitGroupAssignmentViewSet)
|
||||
|
||||
app_name = 'circuits-api'
|
||||
urlpatterns = router.urls
|
||||
|
@ -55,6 +55,26 @@ class CircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
||||
filterset_class = filtersets.CircuitTerminationFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Circuit Groups
|
||||
#
|
||||
|
||||
class CircuitGroupViewSet(NetBoxModelViewSet):
|
||||
queryset = CircuitGroup.objects.all()
|
||||
serializer_class = serializers.CircuitGroupSerializer
|
||||
filterset_class = filtersets.CircuitGroupFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Circuit Group Assignments
|
||||
#
|
||||
|
||||
class CircuitGroupAssignmentViewSet(NetBoxModelViewSet):
|
||||
queryset = CircuitGroupAssignment.objects.all()
|
||||
serializer_class = serializers.CircuitGroupAssignmentSerializer
|
||||
filterset_class = filtersets.CircuitGroupAssignmentFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Provider accounts
|
||||
#
|
||||
|
@ -76,3 +76,19 @@ class CircuitTerminationPortSpeedChoices(ChoiceSet):
|
||||
(1544, 'T1 (1.544 Mbps)'),
|
||||
(2048, 'E1 (2.048 Mbps)'),
|
||||
]
|
||||
|
||||
|
||||
class CircuitPriorityChoices(ChoiceSet):
|
||||
key = 'CircuitGroupAssignment.priority'
|
||||
|
||||
PRIORITY_PRIMARY = 'primary'
|
||||
PRIORITY_SECONDARY = 'secondary'
|
||||
PRIORITY_TERTIARY = 'tertiary'
|
||||
PRIORITY_INACTIVE = 'inactive'
|
||||
|
||||
CHOICES = [
|
||||
(PRIORITY_PRIMARY, _('Primary')),
|
||||
(PRIORITY_SECONDARY, _('Secondary')),
|
||||
(PRIORITY_TERTIARY, _('Tertiary')),
|
||||
(PRIORITY_INACTIVE, _('Inactive')),
|
||||
]
|
||||
|
@ -13,6 +13,8 @@ from .models import *
|
||||
|
||||
__all__ = (
|
||||
'CircuitFilterSet',
|
||||
'CircuitGroupAssignmentFilterSet',
|
||||
'CircuitGroupFilterSet',
|
||||
'CircuitTerminationFilterSet',
|
||||
'CircuitTypeFilterSet',
|
||||
'ProviderNetworkFilterSet',
|
||||
@ -303,3 +305,60 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
|
||||
Q(pp_info__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
).distinct()
|
||||
|
||||
|
||||
class CircuitGroupFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroup
|
||||
fields = ('id', 'name', 'slug', 'description')
|
||||
|
||||
|
||||
class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label=_('Search'),
|
||||
)
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='circuit__provider',
|
||||
queryset=Provider.objects.all(),
|
||||
label=_('Provider (ID)'),
|
||||
)
|
||||
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(),
|
||||
label=_('Circuit (ID)'),
|
||||
)
|
||||
circuit = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='circuit__cid',
|
||||
queryset=Circuit.objects.all(),
|
||||
to_field_name='cid',
|
||||
label=_('Circuit (CID)'),
|
||||
)
|
||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=CircuitGroup.objects.all(),
|
||||
label=_('Circuit group (ID)'),
|
||||
)
|
||||
group = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='group__slug',
|
||||
queryset=CircuitGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
label=_('Circuit group (slug)'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroupAssignment
|
||||
fields = ('id', 'priority')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(circuit__cid__icontains=value) |
|
||||
Q(group__name__icontains=value)
|
||||
)
|
||||
|
@ -1,7 +1,7 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from circuits.choices import CircuitCommitRateChoices, CircuitStatusChoices
|
||||
from circuits.choices import CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices
|
||||
from circuits.models import *
|
||||
from dcim.models import Site
|
||||
from ipam.models import ASN
|
||||
@ -14,6 +14,8 @@ from utilities.forms.widgets import BulkEditNullBooleanSelect, DatePicker, Numbe
|
||||
|
||||
__all__ = (
|
||||
'CircuitBulkEditForm',
|
||||
'CircuitGroupAssignmentBulkEditForm',
|
||||
'CircuitGroupBulkEditForm',
|
||||
'CircuitTerminationBulkEditForm',
|
||||
'CircuitTypeBulkEditForm',
|
||||
'ProviderBulkEditForm',
|
||||
@ -219,3 +221,40 @@ class CircuitTerminationBulkEditForm(NetBoxModelBulkEditForm):
|
||||
FieldSet('port_speed', 'upstream_speed', name=_('Termination Details')),
|
||||
)
|
||||
nullable_fields = ('description')
|
||||
|
||||
|
||||
class CircuitGroupBulkEditForm(NetBoxModelBulkEditForm):
|
||||
description = forms.CharField(
|
||||
label=_('Description'),
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
tenant = DynamicModelChoiceField(
|
||||
label=_('Tenant'),
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
model = CircuitGroup
|
||||
nullable_fields = (
|
||||
'description', 'tenant',
|
||||
)
|
||||
|
||||
|
||||
class CircuitGroupAssignmentBulkEditForm(NetBoxModelBulkEditForm):
|
||||
circuit = DynamicModelChoiceField(
|
||||
label=_('Circuit'),
|
||||
queryset=Circuit.objects.all(),
|
||||
required=False
|
||||
)
|
||||
priority = forms.ChoiceField(
|
||||
label=_('Priority'),
|
||||
choices=add_blank_choice(CircuitPriorityChoices),
|
||||
required=False
|
||||
)
|
||||
|
||||
model = CircuitGroupAssignment
|
||||
fieldsets = (
|
||||
FieldSet('circuit', 'priority'),
|
||||
)
|
||||
nullable_fields = ('priority',)
|
||||
|
@ -11,6 +11,8 @@ from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, SlugFiel
|
||||
|
||||
__all__ = (
|
||||
'CircuitImportForm',
|
||||
'CircuitGroupAssignmentImportForm',
|
||||
'CircuitGroupImportForm',
|
||||
'CircuitTerminationImportForm',
|
||||
'CircuitTerminationImportRelatedForm',
|
||||
'CircuitTypeImportForm',
|
||||
@ -150,3 +152,24 @@ class CircuitTerminationImportForm(NetBoxModelImportForm, BaseCircuitTermination
|
||||
'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
|
||||
'pp_info', 'description', 'tags'
|
||||
]
|
||||
|
||||
|
||||
class CircuitGroupImportForm(NetBoxModelImportForm):
|
||||
tenant = CSVModelChoiceField(
|
||||
label=_('Tenant'),
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text=_('Assigned tenant')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroup
|
||||
fields = ('name', 'slug', 'description', 'tenant', 'tags')
|
||||
|
||||
|
||||
class CircuitGroupAssignmentImportForm(NetBoxModelImportForm):
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroupAssignment
|
||||
fields = ('circuit', 'group', 'priority')
|
||||
|
@ -1,7 +1,7 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from circuits.choices import CircuitCommitRateChoices, CircuitStatusChoices, CircuitTerminationSideChoices
|
||||
from circuits.choices import CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices, CircuitTerminationSideChoices
|
||||
from circuits.models import *
|
||||
from dcim.models import Region, Site, SiteGroup
|
||||
from ipam.models import ASN
|
||||
@ -13,6 +13,8 @@ from utilities.forms.widgets import DatePicker, NumberWithOptions
|
||||
|
||||
__all__ = (
|
||||
'CircuitFilterForm',
|
||||
'CircuitGroupAssignmentFilterForm',
|
||||
'CircuitGroupFilterForm',
|
||||
'CircuitTerminationFilterForm',
|
||||
'CircuitTypeFilterForm',
|
||||
'ProviderFilterForm',
|
||||
@ -230,3 +232,41 @@ class CircuitTerminationFilterForm(NetBoxModelFilterSetForm):
|
||||
label=_('Provider')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class CircuitGroupFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
model = CircuitGroup
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class CircuitGroupAssignmentFilterForm(NetBoxModelFilterSetForm):
|
||||
model = CircuitGroupAssignment
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('provider_id', 'circuit_id', 'group_id', 'priority', name=_('Assignment')),
|
||||
)
|
||||
provider_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
required=False,
|
||||
label=_('Provider')
|
||||
)
|
||||
circuit_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Circuit.objects.all(),
|
||||
required=False,
|
||||
label=_('Circuit')
|
||||
)
|
||||
group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=CircuitGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Group')
|
||||
)
|
||||
priority = forms.MultipleChoiceField(
|
||||
label=_('Priority'),
|
||||
choices=CircuitPriorityChoices,
|
||||
required=False
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
@ -12,6 +12,8 @@ from utilities.forms.widgets import DatePicker, NumberWithOptions
|
||||
|
||||
__all__ = (
|
||||
'CircuitForm',
|
||||
'CircuitGroupAssignmentForm',
|
||||
'CircuitGroupForm',
|
||||
'CircuitTerminationForm',
|
||||
'CircuitTypeForm',
|
||||
'ProviderForm',
|
||||
@ -171,3 +173,36 @@ class CircuitTerminationForm(NetBoxModelForm):
|
||||
options=CircuitTerminationPortSpeedChoices
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class CircuitGroupForm(TenancyForm, NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('name', 'slug', 'description', 'tags', name=_('Circuit Group')),
|
||||
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroup
|
||||
fields = [
|
||||
'name', 'slug', 'description', 'tenant_group', 'tenant', 'tags',
|
||||
]
|
||||
|
||||
|
||||
class CircuitGroupAssignmentForm(NetBoxModelForm):
|
||||
group = DynamicModelChoiceField(
|
||||
label=_('Group'),
|
||||
queryset=CircuitGroup.objects.all(),
|
||||
)
|
||||
circuit = DynamicModelChoiceField(
|
||||
label=_('Circuit'),
|
||||
queryset=Circuit.objects.all(),
|
||||
selector=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroupAssignment
|
||||
fields = [
|
||||
'group', 'circuit', 'priority', 'tags',
|
||||
]
|
||||
|
@ -7,6 +7,8 @@ from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
|
||||
__all__ = (
|
||||
'CircuitTerminationFilter',
|
||||
'CircuitFilter',
|
||||
'CircuitGroupAssignmentFilter',
|
||||
'CircuitGroupFilter',
|
||||
'CircuitTypeFilter',
|
||||
'ProviderFilter',
|
||||
'ProviderAccountFilter',
|
||||
@ -32,6 +34,18 @@ class CircuitTypeFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.CircuitGroup, lookups=True)
|
||||
@autotype_decorator(filtersets.CircuitGroupFilterSet)
|
||||
class CircuitGroupFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.CircuitGroupAssignment, lookups=True)
|
||||
@autotype_decorator(filtersets.CircuitGroupAssignmentFilterSet)
|
||||
class CircuitGroupAssignmentFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Provider, lookups=True)
|
||||
@autotype_decorator(filtersets.ProviderFilterSet)
|
||||
class ProviderFilter(BaseFilterMixin):
|
||||
|
@ -17,6 +17,12 @@ class CircuitsQuery:
|
||||
circuit_type: CircuitTypeType = strawberry_django.field()
|
||||
circuit_type_list: List[CircuitTypeType] = strawberry_django.field()
|
||||
|
||||
circuit_group: CircuitGroupType = strawberry_django.field()
|
||||
circuit_group_list: List[CircuitGroupType] = strawberry_django.field()
|
||||
|
||||
circuit_group_assignment: CircuitGroupAssignmentType = strawberry_django.field()
|
||||
circuit_group_assignment_list: List[CircuitGroupAssignmentType] = strawberry_django.field()
|
||||
|
||||
provider: ProviderType = strawberry_django.field()
|
||||
provider_list: List[ProviderType] = strawberry_django.field()
|
||||
|
||||
|
@ -6,13 +6,15 @@ import strawberry_django
|
||||
from circuits import models
|
||||
from dcim.graphql.mixins import CabledObjectMixin
|
||||
from extras.graphql.mixins import ContactsMixin, CustomFieldsMixin, TagsMixin
|
||||
from netbox.graphql.types import NetBoxObjectType, ObjectType, OrganizationalObjectType
|
||||
from netbox.graphql.types import BaseObjectType, NetBoxObjectType, ObjectType, OrganizationalObjectType
|
||||
from tenancy.graphql.types import TenantType
|
||||
from .filters import *
|
||||
|
||||
__all__ = (
|
||||
'CircuitTerminationType',
|
||||
'CircuitType',
|
||||
'CircuitGroupAssignmentType',
|
||||
'CircuitGroupType',
|
||||
'CircuitTypeType',
|
||||
'ProviderType',
|
||||
'ProviderAccountType',
|
||||
@ -91,3 +93,22 @@ class CircuitType(NetBoxObjectType, ContactsMixin):
|
||||
tenant: TenantType | None
|
||||
|
||||
terminations: List[CircuitTerminationType]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.CircuitGroup,
|
||||
fields='__all__',
|
||||
filters=CircuitGroupFilter
|
||||
)
|
||||
class CircuitGroupType(OrganizationalObjectType):
|
||||
tenant: TenantType | None
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.CircuitGroupAssignment,
|
||||
fields='__all__',
|
||||
filters=CircuitGroupAssignmentFilter
|
||||
)
|
||||
class CircuitGroupAssignmentType(TagsMixin, BaseObjectType):
|
||||
group: Annotated["CircuitGroupType", strawberry.lazy('circuits.graphql.types')]
|
||||
circuit: Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]
|
||||
|
88
netbox/circuits/migrations/0044_circuit_groups.py
Normal file
@ -0,0 +1,88 @@
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
import utilities.json
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0043_circuittype_color'),
|
||||
('extras', '0119_notifications'),
|
||||
('tenancy', '0015_contactassignment_rename_content_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CircuitGroup',
|
||||
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)),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
(
|
||||
'tenant',
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='circuit_groups',
|
||||
to='tenancy.tenant',
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Circuit group',
|
||||
'verbose_name_plural': 'Circuit group',
|
||||
'ordering': ('name',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CircuitGroupAssignment',
|
||||
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),
|
||||
),
|
||||
('priority', models.CharField(blank=True, max_length=50)),
|
||||
(
|
||||
'circuit',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='assignments',
|
||||
to='circuits.circuit',
|
||||
),
|
||||
),
|
||||
(
|
||||
'group',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='assignments',
|
||||
to='circuits.circuitgroup',
|
||||
),
|
||||
),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Circuit group assignment',
|
||||
'verbose_name_plural': 'Circuit group assignments',
|
||||
'ordering': ('group', 'circuit', 'priority', 'pk'),
|
||||
},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='circuitgroupassignment',
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=('circuit', 'group'), name='circuits_circuitgroupassignment_unique_circuit_group'
|
||||
),
|
||||
),
|
||||
]
|
@ -6,11 +6,13 @@ from django.utils.translation import gettext_lazy as _
|
||||
from circuits.choices import *
|
||||
from dcim.models import CabledObjectModel
|
||||
from netbox.models import ChangeLoggedModel, OrganizationalModel, PrimaryModel
|
||||
from netbox.models.features import ContactsMixin, CustomFieldsMixin, CustomLinksMixin, ImageAttachmentsMixin, TagsMixin
|
||||
from netbox.models.features import ContactsMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, ImageAttachmentsMixin, TagsMixin
|
||||
from utilities.fields import ColorField
|
||||
|
||||
__all__ = (
|
||||
'Circuit',
|
||||
'CircuitGroup',
|
||||
'CircuitGroupAssignment',
|
||||
'CircuitTermination',
|
||||
'CircuitType',
|
||||
)
|
||||
@ -151,6 +153,75 @@ class Circuit(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
|
||||
raise ValidationError({'provider_account': "The assigned account must belong to the assigned provider."})
|
||||
|
||||
|
||||
class CircuitGroup(OrganizationalModel):
|
||||
"""
|
||||
An administrative grouping of Circuits.
|
||||
"""
|
||||
tenant = models.ForeignKey(
|
||||
to='tenancy.Tenant',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='circuit_groups',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ('name',)
|
||||
verbose_name = _('circuit group')
|
||||
verbose_name_plural = _('circuit groups')
|
||||
|
||||
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.
|
||||
"""
|
||||
circuit = models.ForeignKey(
|
||||
Circuit,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='assignments'
|
||||
)
|
||||
group = models.ForeignKey(
|
||||
CircuitGroup,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='assignments'
|
||||
)
|
||||
priority = models.CharField(
|
||||
verbose_name=_('priority'),
|
||||
max_length=50,
|
||||
choices=CircuitPriorityChoices,
|
||||
blank=True
|
||||
)
|
||||
prerequisite_models = (
|
||||
'circuits.Circuit',
|
||||
'circuits.CircuitGroup',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ('group', 'circuit', 'priority', 'pk')
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('circuit', 'group'),
|
||||
name='%(app_label)s_%(class)s_unique_circuit_group'
|
||||
),
|
||||
)
|
||||
verbose_name = _('Circuit group assignment')
|
||||
verbose_name_plural = _('Circuit group assignments')
|
||||
|
||||
def __str__(self):
|
||||
if self.priority:
|
||||
return f"{self.group} ({self.get_priority_display()})"
|
||||
return str(self.group)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:circuitgroupassignment', args=[self.pk])
|
||||
|
||||
|
||||
class CircuitTermination(
|
||||
CustomFieldsMixin,
|
||||
CustomLinksMixin,
|
||||
|
@ -13,6 +13,17 @@ class CircuitIndex(SearchIndex):
|
||||
display_attrs = ('provider', 'provider_account', 'type', 'status', 'tenant', 'description')
|
||||
|
||||
|
||||
@register_search
|
||||
class CircuitGroupIndex(SearchIndex):
|
||||
model = models.CircuitGroup
|
||||
fields = (
|
||||
('name', 100),
|
||||
('slug', 110),
|
||||
('description', 500),
|
||||
)
|
||||
display_attrs = ('description',)
|
||||
|
||||
|
||||
@register_search
|
||||
class CircuitTerminationIndex(SearchIndex):
|
||||
model = models.CircuitTermination
|
||||
|
@ -9,6 +9,8 @@ from netbox.tables import NetBoxTable, columns
|
||||
from .columns import CommitRateColumn
|
||||
|
||||
__all__ = (
|
||||
'CircuitGroupAssignmentTable',
|
||||
'CircuitGroupTable',
|
||||
'CircuitTable',
|
||||
'CircuitTerminationTable',
|
||||
'CircuitTypeTable',
|
||||
@ -75,18 +77,22 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
verbose_name=_('Commit Rate')
|
||||
)
|
||||
comments = columns.MarkdownColumn(
|
||||
verbose_name=_('Comments'),
|
||||
verbose_name=_('Comments')
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='circuits:circuit_list'
|
||||
)
|
||||
assignments = columns.ManyToManyColumn(
|
||||
verbose_name=_('Assignments'),
|
||||
linkify_item=True
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = Circuit
|
||||
fields = (
|
||||
'pk', 'id', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'tenant_group',
|
||||
'termination_a', 'termination_z', 'install_date', 'termination_date', 'commit_rate', 'description',
|
||||
'comments', 'contacts', 'tags', 'created', 'last_updated',
|
||||
'comments', 'contacts', 'tags', 'created', 'last_updated', 'assignments',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'description',
|
||||
@ -119,3 +125,55 @@ class CircuitTerminationTable(NetBoxTable):
|
||||
'xconnect_id', 'pp_info', 'description', 'created', 'last_updated', 'actions',
|
||||
)
|
||||
default_columns = ('pk', 'id', 'circuit', 'provider', 'term_side', 'description')
|
||||
|
||||
|
||||
class CircuitGroupTable(NetBoxTable):
|
||||
name = tables.Column(
|
||||
verbose_name=_('Name'),
|
||||
linkify=True
|
||||
)
|
||||
circuit_group_assignment_count = columns.LinkedCountColumn(
|
||||
viewname='circuits:circuitgroupassignment_list',
|
||||
url_params={'group_id': 'pk'},
|
||||
verbose_name=_('Circuits')
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='circuits:circuitgroup_list'
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = CircuitGroup
|
||||
fields = (
|
||||
'pk', 'name', 'description', 'circuit_group_assignment_count', 'tags',
|
||||
'created', 'last_updated', 'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'description', 'circuit_group_assignment_count')
|
||||
|
||||
|
||||
class CircuitGroupAssignmentTable(NetBoxTable):
|
||||
group = tables.Column(
|
||||
verbose_name=_('Group'),
|
||||
linkify=True
|
||||
)
|
||||
provider = tables.Column(
|
||||
accessor='circuit__provider',
|
||||
verbose_name=_('Provider'),
|
||||
linkify=True
|
||||
)
|
||||
circuit = tables.Column(
|
||||
verbose_name=_('Circuit'),
|
||||
linkify=True
|
||||
)
|
||||
priority = tables.Column(
|
||||
verbose_name=_('Priority'),
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='circuits:circuitgroupassignment_list'
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = CircuitGroupAssignment
|
||||
fields = (
|
||||
'pk', 'id', 'group', 'provider', 'circuit', 'priority', 'created', 'last_updated', 'actions', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'group', 'provider', 'circuit', 'priority')
|
||||
|
@ -92,7 +92,7 @@ class CircuitTypeTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class CircuitTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Circuit
|
||||
brief_fields = ['cid', 'description', 'display', 'id', 'url']
|
||||
brief_fields = ['cid', 'description', 'display', 'id', 'provider', 'url']
|
||||
bulk_update_data = {
|
||||
'status': 'planned',
|
||||
}
|
||||
@ -208,10 +208,42 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
|
||||
}
|
||||
|
||||
|
||||
class CircuitGroupTest(APIViewTestCases.APIViewTestCase):
|
||||
model = CircuitGroup
|
||||
brief_fields = ['display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
circuit_groups = (
|
||||
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.objects.bulk_create(circuit_groups)
|
||||
|
||||
cls.create_data = [
|
||||
{
|
||||
'name': 'Circuit Group 4',
|
||||
'slug': 'circuit-group-4',
|
||||
},
|
||||
{
|
||||
'name': 'Circuit Group 5',
|
||||
'slug': 'circuit-group-5',
|
||||
},
|
||||
{
|
||||
'name': 'Circuit Group 6',
|
||||
'slug': 'circuit-group-6',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class ProviderAccountTest(APIViewTestCases.APIViewTestCase):
|
||||
model = ProviderAccount
|
||||
brief_fields = ['account', 'description', 'display', 'id', 'name', 'url']
|
||||
user_permissions = ('circuits.view_provider', )
|
||||
user_permissions = ('circuits.view_provider',)
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@ -252,6 +284,78 @@ class ProviderAccountTest(APIViewTestCases.APIViewTestCase):
|
||||
}
|
||||
|
||||
|
||||
class CircuitGroupAssignmentTest(APIViewTestCases.APIViewTestCase):
|
||||
model = CircuitGroupAssignment
|
||||
brief_fields = ['circuit', 'display', 'group', 'id', 'priority', 'url']
|
||||
bulk_update_data = {
|
||||
'priority': CircuitPriorityChoices.PRIORITY_INACTIVE,
|
||||
}
|
||||
user_permissions = ('circuits.view_circuit', 'circuits.view_circuitgroup')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
circuit_groups = (
|
||||
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(name='Circuit Group 5', slug='circuit-group-5'),
|
||||
CircuitGroup(name='Circuit Group 6', slug='circuit-group-6'),
|
||||
)
|
||||
CircuitGroup.objects.bulk_create(circuit_groups)
|
||||
|
||||
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
|
||||
|
||||
circuits = (
|
||||
Circuit(cid='Circuit 1', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 2', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 3', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 4', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 5', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 6', provider=provider, type=circuittype),
|
||||
)
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
assignments = (
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[0],
|
||||
circuit=circuits[0],
|
||||
priority=CircuitPriorityChoices.PRIORITY_PRIMARY
|
||||
),
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[1],
|
||||
circuit=circuits[1],
|
||||
priority=CircuitPriorityChoices.PRIORITY_SECONDARY
|
||||
),
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[2],
|
||||
circuit=circuits[2],
|
||||
priority=CircuitPriorityChoices.PRIORITY_TERTIARY
|
||||
),
|
||||
)
|
||||
CircuitGroupAssignment.objects.bulk_create(assignments)
|
||||
|
||||
cls.create_data = [
|
||||
{
|
||||
'group': circuit_groups[3].pk,
|
||||
'circuit': circuits[3].pk,
|
||||
'priority': CircuitPriorityChoices.PRIORITY_PRIMARY,
|
||||
},
|
||||
{
|
||||
'group': circuit_groups[4].pk,
|
||||
'circuit': circuits[4].pk,
|
||||
'priority': CircuitPriorityChoices.PRIORITY_SECONDARY,
|
||||
},
|
||||
{
|
||||
'group': circuit_groups[5].pk,
|
||||
'circuit': circuits[5].pk,
|
||||
'priority': CircuitPriorityChoices.PRIORITY_TERTIARY,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class ProviderNetworkTest(APIViewTestCases.APIViewTestCase):
|
||||
model = ProviderNetwork
|
||||
brief_fields = ['description', 'display', 'id', 'name', 'url']
|
||||
|
@ -451,6 +451,136 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 7)
|
||||
|
||||
|
||||
class CircuitGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = CircuitGroup.objects.all()
|
||||
filterset = CircuitGroupFilterSet
|
||||
|
||||
@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)
|
||||
|
||||
CircuitGroup.objects.bulk_create((
|
||||
CircuitGroup(name='Circuit Group 1', slug='circuit-group-1', description='foobar1', tenant=tenants[0]),
|
||||
CircuitGroup(name='Circuit Group 2', slug='circuit-group-2', description='foobar2', tenant=tenants[1]),
|
||||
CircuitGroup(name='Circuit Group 3', slug='circuit-group-3', tenant=tenants[1]),
|
||||
))
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Circuit Group 1']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_slug(self):
|
||||
params = {'slug': ['circuit-group-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)
|
||||
|
||||
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(), 3)
|
||||
params = {'tenant': [tenants[0].slug, tenants[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
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(), 3)
|
||||
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
|
||||
class CircuitGroupAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = CircuitGroupAssignment.objects.all()
|
||||
filterset = CircuitGroupAssignmentFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
circuit_groups = (
|
||||
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)
|
||||
|
||||
providers = Provider.objects.bulk_create((
|
||||
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')
|
||||
|
||||
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.objects.bulk_create(circuits)
|
||||
|
||||
assignments = (
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[0],
|
||||
circuit=circuits[0],
|
||||
priority=CircuitPriorityChoices.PRIORITY_PRIMARY
|
||||
),
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[1],
|
||||
circuit=circuits[1],
|
||||
priority=CircuitPriorityChoices.PRIORITY_SECONDARY
|
||||
),
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[2],
|
||||
circuit=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'])
|
||||
params = {'group_id': [groups[0].pk, groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'group': [groups[0].slug, groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_circuit(self):
|
||||
circuits = Circuit.objects.all()[:2]
|
||||
params = {'circuit_id': [circuits[0].pk, circuits[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'circuit': [circuits[0].cid, 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)
|
||||
params = {'provider': [providers[0].slug, providers[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class ProviderNetworkTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
filterset = ProviderNetworkFilterSet
|
||||
|
@ -404,3 +404,109 @@ class CircuitTerminationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
|
||||
response = self.client.get(reverse('circuits:circuittermination_trace', kwargs={'pk': circuittermination.pk}))
|
||||
self.assertHttpStatus(response, 200)
|
||||
|
||||
|
||||
class CircuitGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
model = CircuitGroup
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
circuit_groups = (
|
||||
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.objects.bulk_create(circuit_groups)
|
||||
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'name': 'Circuit Group X',
|
||||
'slug': 'circuit-group-x',
|
||||
'description': 'A new Circuit Group',
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"name,slug",
|
||||
"Circuit Group 4,circuit-group-4",
|
||||
"Circuit Group 5,circuit-group-5",
|
||||
"Circuit Group 6,circuit-group-6",
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
"id,name,description",
|
||||
f"{circuit_groups[0].pk},Circuit Group 7,New description7",
|
||||
f"{circuit_groups[1].pk},Circuit Group 8,New description8",
|
||||
f"{circuit_groups[2].pk},Circuit Group 9,New description9",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'description': 'Foo',
|
||||
}
|
||||
|
||||
|
||||
class CircuitGroupAssignmentTestCase(
|
||||
ViewTestCases.CreateObjectViewTestCase,
|
||||
ViewTestCases.EditObjectViewTestCase,
|
||||
ViewTestCases.DeleteObjectViewTestCase,
|
||||
ViewTestCases.ListObjectsViewTestCase,
|
||||
ViewTestCases.BulkEditObjectsViewTestCase,
|
||||
ViewTestCases.BulkDeleteObjectsViewTestCase
|
||||
):
|
||||
model = CircuitGroupAssignment
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
circuit_groups = (
|
||||
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)
|
||||
|
||||
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
|
||||
|
||||
circuits = (
|
||||
Circuit(cid='Circuit 1', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 2', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 3', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 4', provider=provider, type=circuittype),
|
||||
)
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
assignments = (
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[0],
|
||||
circuit=circuits[0],
|
||||
priority=CircuitPriorityChoices.PRIORITY_PRIMARY
|
||||
),
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[1],
|
||||
circuit=circuits[1],
|
||||
priority=CircuitPriorityChoices.PRIORITY_SECONDARY
|
||||
),
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[2],
|
||||
circuit=circuits[2],
|
||||
priority=CircuitPriorityChoices.PRIORITY_TERTIARY
|
||||
),
|
||||
)
|
||||
CircuitGroupAssignment.objects.bulk_create(assignments)
|
||||
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'group': circuit_groups[3].pk,
|
||||
'circuit': circuits[3].pk,
|
||||
'priority': CircuitPriorityChoices.PRIORITY_INACTIVE,
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'priority': CircuitPriorityChoices.PRIORITY_INACTIVE,
|
||||
}
|
||||
|
@ -55,4 +55,19 @@ urlpatterns = [
|
||||
path('circuit-terminations/delete/', views.CircuitTerminationBulkDeleteView.as_view(), name='circuittermination_bulk_delete'),
|
||||
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/<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/<int:pk>/', include(get_model_urls('circuits', 'circuitgroupassignment'))),
|
||||
]
|
||||
|
@ -443,3 +443,100 @@ class CircuitTerminationBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
# Trace view
|
||||
register_model_view(CircuitTermination, 'trace', kwargs={'model': CircuitTermination})(PathTraceView)
|
||||
|
||||
|
||||
#
|
||||
# Circuit Groups
|
||||
#
|
||||
|
||||
class CircuitGroupListView(generic.ObjectListView):
|
||||
queryset = CircuitGroup.objects.annotate(
|
||||
circuit_group_assignment_count=count_related(CircuitGroupAssignment, 'group')
|
||||
)
|
||||
filterset = filtersets.CircuitGroupFilterSet
|
||||
filterset_form = forms.CircuitGroupFilterForm
|
||||
table = tables.CircuitGroupTable
|
||||
|
||||
|
||||
@register_model_view(CircuitGroup)
|
||||
class CircuitGroupView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = CircuitGroup.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
'related_models': self.get_related_models(request, instance),
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(CircuitGroup, 'edit')
|
||||
class CircuitGroupEditView(generic.ObjectEditView):
|
||||
queryset = CircuitGroup.objects.all()
|
||||
form = forms.CircuitGroupForm
|
||||
|
||||
|
||||
@register_model_view(CircuitGroup, 'delete')
|
||||
class CircuitGroupDeleteView(generic.ObjectDeleteView):
|
||||
queryset = CircuitGroup.objects.all()
|
||||
|
||||
|
||||
class CircuitGroupBulkImportView(generic.BulkImportView):
|
||||
queryset = CircuitGroup.objects.all()
|
||||
model_form = forms.CircuitGroupImportForm
|
||||
|
||||
|
||||
class CircuitGroupBulkEditView(generic.BulkEditView):
|
||||
queryset = CircuitGroup.objects.all()
|
||||
filterset = filtersets.CircuitGroupFilterSet
|
||||
table = tables.CircuitGroupTable
|
||||
form = forms.CircuitGroupBulkEditForm
|
||||
|
||||
|
||||
class CircuitGroupBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = CircuitGroup.objects.all()
|
||||
filterset = filtersets.CircuitGroupFilterSet
|
||||
table = tables.CircuitGroupTable
|
||||
|
||||
|
||||
#
|
||||
# Circuit Groups
|
||||
#
|
||||
|
||||
class CircuitGroupAssignmentListView(generic.ObjectListView):
|
||||
queryset = CircuitGroupAssignment.objects.all()
|
||||
filterset = filtersets.CircuitGroupAssignmentFilterSet
|
||||
filterset_form = forms.CircuitGroupAssignmentFilterForm
|
||||
table = tables.CircuitGroupAssignmentTable
|
||||
|
||||
|
||||
@register_model_view(CircuitGroupAssignment)
|
||||
class CircuitGroupAssignmentView(generic.ObjectView):
|
||||
queryset = CircuitGroupAssignment.objects.all()
|
||||
|
||||
|
||||
@register_model_view(CircuitGroupAssignment, 'edit')
|
||||
class CircuitGroupAssignmentEditView(generic.ObjectEditView):
|
||||
queryset = CircuitGroupAssignment.objects.all()
|
||||
form = forms.CircuitGroupAssignmentForm
|
||||
|
||||
|
||||
@register_model_view(CircuitGroupAssignment, 'delete')
|
||||
class CircuitGroupAssignmentDeleteView(generic.ObjectDeleteView):
|
||||
queryset = CircuitGroupAssignment.objects.all()
|
||||
|
||||
|
||||
class CircuitGroupAssignmentBulkImportView(generic.BulkImportView):
|
||||
queryset = CircuitGroupAssignment.objects.all()
|
||||
model_form = forms.CircuitGroupAssignmentImportForm
|
||||
|
||||
|
||||
class CircuitGroupAssignmentBulkEditView(generic.BulkEditView):
|
||||
queryset = CircuitGroupAssignment.objects.all()
|
||||
filterset = filtersets.CircuitGroupAssignmentFilterSet
|
||||
table = tables.CircuitGroupAssignmentTable
|
||||
form = forms.CircuitGroupAssignmentBulkEditForm
|
||||
|
||||
|
||||
class CircuitGroupAssignmentBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = CircuitGroupAssignment.objects.all()
|
||||
filterset = filtersets.CircuitGroupAssignmentFilterSet
|
||||
table = tables.CircuitGroupAssignmentTable
|
||||
|
@ -1,3 +1,5 @@
|
||||
import warnings
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from core.choices import JobStatusChoices
|
||||
@ -12,25 +14,28 @@ __all__ = (
|
||||
'NestedJobSerializer',
|
||||
)
|
||||
|
||||
# TODO: Remove in v4.2
|
||||
warnings.warn(
|
||||
f"Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
|
||||
DeprecationWarning
|
||||
)
|
||||
|
||||
|
||||
class NestedDataSourceSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='core-api:datasource-detail')
|
||||
|
||||
class Meta:
|
||||
model = DataSource
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedDataFileSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='core-api:datafile-detail')
|
||||
|
||||
class Meta:
|
||||
model = DataFile
|
||||
fields = ['id', 'url', 'display', 'path']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'path']
|
||||
|
||||
|
||||
class NestedJobSerializer(serializers.ModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail')
|
||||
status = ChoiceField(choices=JobStatusChoices)
|
||||
user = UserSerializer(
|
||||
nested=True,
|
||||
@ -39,4 +44,4 @@ class NestedJobSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Job
|
||||
fields = ['url', 'created', 'completed', 'user', 'status']
|
||||
fields = ['url', 'display_url', 'created', 'completed', 'user', 'status']
|
||||
|
@ -1,3 +1,3 @@
|
||||
from .serializers_.change_logging import *
|
||||
from .serializers_.data import *
|
||||
from .serializers_.jobs import *
|
||||
from .nested_serializers import *
|
||||
|
@ -1,8 +1,8 @@
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import serializers
|
||||
|
||||
from extras.choices import *
|
||||
from extras.models import ObjectChange
|
||||
from core.choices import *
|
||||
from core.models import ObjectChange
|
||||
from netbox.api.exceptions import SerializerNotFound
|
||||
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||
from netbox.api.serializers import BaseModelSerializer
|
||||
@ -15,7 +15,6 @@ __all__ = (
|
||||
|
||||
|
||||
class ObjectChangeSerializer(BaseModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:objectchange-detail')
|
||||
user = UserSerializer(
|
||||
nested=True,
|
||||
read_only=True
|
||||
@ -44,8 +43,8 @@ class ObjectChangeSerializer(BaseModelSerializer):
|
||||
class Meta:
|
||||
model = ObjectChange
|
||||
fields = [
|
||||
'id', 'url', 'display', 'time', 'user', 'user_name', 'request_id', 'action', 'changed_object_type',
|
||||
'changed_object_id', 'changed_object', 'prechange_data', 'postchange_data',
|
||||
'id', 'url', 'display_url', 'display', 'time', 'user', 'user_name', 'request_id', 'action',
|
||||
'changed_object_type', 'changed_object_id', 'changed_object', 'prechange_data', 'postchange_data',
|
||||
]
|
||||
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
@ -1,5 +1,3 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from core.choices import *
|
||||
from core.models import DataFile, DataSource
|
||||
from netbox.api.fields import ChoiceField, RelatedObjectCountField
|
||||
@ -13,9 +11,6 @@ __all__ = (
|
||||
|
||||
|
||||
class DataSourceSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name='core-api:datasource-detail'
|
||||
)
|
||||
type = ChoiceField(
|
||||
choices=get_data_backend_choices()
|
||||
)
|
||||
@ -30,16 +25,14 @@ class DataSourceSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = DataSource
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'type', 'source_url', 'enabled', 'status', 'description', 'comments',
|
||||
'parameters', 'ignore_rules', 'custom_fields', 'created', 'last_updated', 'file_count',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'type', 'source_url', 'enabled', 'status', 'description',
|
||||
'parameters', 'ignore_rules', 'comments', 'custom_fields', 'created', 'last_updated', 'last_synced',
|
||||
'file_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||
|
||||
|
||||
class DataFileSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name='core-api:datafile-detail'
|
||||
)
|
||||
source = DataSourceSerializer(
|
||||
nested=True,
|
||||
read_only=True
|
||||
@ -48,6 +41,6 @@ class DataFileSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = DataFile
|
||||
fields = [
|
||||
'id', 'url', 'display', 'source', 'path', 'last_updated', 'size', 'hash',
|
||||
'id', 'url', 'display_url', 'display', 'source', 'path', 'last_updated', 'size', 'hash',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'path')
|
||||
|
@ -12,7 +12,6 @@ __all__ = (
|
||||
|
||||
|
||||
class JobSerializer(BaseModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail')
|
||||
user = UserSerializer(
|
||||
nested=True,
|
||||
read_only=True
|
||||
@ -25,7 +24,7 @@ class JobSerializer(BaseModelSerializer):
|
||||
class Meta:
|
||||
model = Job
|
||||
fields = [
|
||||
'id', 'url', 'display', 'object_type', 'object_id', 'name', 'status', 'created', 'scheduled', 'interval',
|
||||
'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')
|
||||
|
@ -5,12 +5,10 @@ from . import views
|
||||
router = NetBoxRouter()
|
||||
router.APIRootView = views.CoreRootView
|
||||
|
||||
# Data sources
|
||||
router.register('data-sources', views.DataSourceViewSet)
|
||||
router.register('data-files', views.DataFileViewSet)
|
||||
|
||||
# Jobs
|
||||
router.register('jobs', views.JobViewSet)
|
||||
router.register('object-changes', views.ObjectChangeViewSet)
|
||||
|
||||
app_name = 'core-api'
|
||||
urlpatterns = router.urls
|
||||
|
@ -7,7 +7,10 @@ from rest_framework.routers import APIRootView
|
||||
from rest_framework.viewsets import ReadOnlyModelViewSet
|
||||
|
||||
from core import filtersets
|
||||
from core.choices import DataSourceStatusChoices
|
||||
from core.jobs import SyncDataSourceJob
|
||||
from core.models import *
|
||||
from netbox.api.metadata import ContentTypeMetadata
|
||||
from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet
|
||||
from . import serializers
|
||||
|
||||
@ -35,7 +38,11 @@ class DataSourceViewSet(NetBoxModelViewSet):
|
||||
if not request.user.has_perm('core.sync_datasource', obj=datasource):
|
||||
raise PermissionDenied(_("This user does not have permission to synchronize this data source."))
|
||||
|
||||
datasource.enqueue_sync_job(request)
|
||||
# Enqueue the sync job & update the DataSource's status
|
||||
SyncDataSourceJob.enqueue(instance=datasource, user=request.user)
|
||||
datasource.status = DataSourceStatusChoices.QUEUED
|
||||
DataSource.objects.filter(pk=datasource.pk).update(status=datasource.status)
|
||||
|
||||
serializer = serializers.DataSourceSerializer(datasource, context={'request': request})
|
||||
|
||||
return Response(serializer.data)
|
||||
@ -54,3 +61,13 @@ class JobViewSet(ReadOnlyModelViewSet):
|
||||
queryset = Job.objects.all()
|
||||
serializer_class = serializers.JobSerializer
|
||||
filterset_class = filtersets.JobFilterSet
|
||||
|
||||
|
||||
class ObjectChangeViewSet(ReadOnlyModelViewSet):
|
||||
"""
|
||||
Retrieve a list of recent changes.
|
||||
"""
|
||||
metadata_class = ContentTypeMetadata
|
||||
queryset = ObjectChange.objects.valid_models()
|
||||
serializer_class = serializers.ObjectChangeSerializer
|
||||
filterset_class = filtersets.ObjectChangeFilterSet
|
||||
|
@ -18,7 +18,7 @@ class CoreConfig(AppConfig):
|
||||
def ready(self):
|
||||
from core.api import schema # noqa
|
||||
from netbox.models.features import register_models
|
||||
from . import data_backends, search
|
||||
from . import data_backends, events, search
|
||||
|
||||
# Register models
|
||||
register_models(*self.get_models())
|
||||
|
@ -59,8 +59,31 @@ class JobStatusChoices(ChoiceSet):
|
||||
(STATUS_FAILED, _('Failed'), 'red'),
|
||||
)
|
||||
|
||||
ENQUEUED_STATE_CHOICES = (
|
||||
STATUS_PENDING,
|
||||
STATUS_SCHEDULED,
|
||||
STATUS_RUNNING,
|
||||
)
|
||||
|
||||
TERMINAL_STATE_CHOICES = (
|
||||
STATUS_COMPLETED,
|
||||
STATUS_ERRORED,
|
||||
STATUS_FAILED,
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# ObjectChanges
|
||||
#
|
||||
|
||||
class ObjectChangeActionChoices(ChoiceSet):
|
||||
|
||||
ACTION_CREATE = 'create'
|
||||
ACTION_UPDATE = 'update'
|
||||
ACTION_DELETE = 'delete'
|
||||
|
||||
CHOICES = (
|
||||
(ACTION_CREATE, _('Created'), 'green'),
|
||||
(ACTION_UPDATE, _('Updated'), 'blue'),
|
||||
(ACTION_DELETE, _('Deleted'), 'red'),
|
||||
)
|
||||
|
33
netbox/core/events.py
Normal file
@ -0,0 +1,33 @@
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from netbox.events import EventType, EVENT_TYPE_KIND_DANGER, EVENT_TYPE_KIND_SUCCESS, EVENT_TYPE_KIND_WARNING
|
||||
|
||||
__all__ = (
|
||||
'JOB_COMPLETED',
|
||||
'JOB_ERRORED',
|
||||
'JOB_FAILED',
|
||||
'JOB_STARTED',
|
||||
'OBJECT_CREATED',
|
||||
'OBJECT_DELETED',
|
||||
'OBJECT_UPDATED',
|
||||
)
|
||||
|
||||
# Object events
|
||||
OBJECT_CREATED = 'object_created'
|
||||
OBJECT_UPDATED = 'object_updated'
|
||||
OBJECT_DELETED = 'object_deleted'
|
||||
|
||||
# Job events
|
||||
JOB_STARTED = 'job_started'
|
||||
JOB_COMPLETED = 'job_completed'
|
||||
JOB_FAILED = 'job_failed'
|
||||
JOB_ERRORED = 'job_errored'
|
||||
|
||||
# Register core events
|
||||
EventType(OBJECT_CREATED, _('Object created')).register()
|
||||
EventType(OBJECT_UPDATED, _('Object updated')).register()
|
||||
EventType(OBJECT_DELETED, _('Object deleted'), destructive=True).register()
|
||||
EventType(JOB_STARTED, _('Job started')).register()
|
||||
EventType(JOB_COMPLETED, _('Job completed'), kind=EVENT_TYPE_KIND_SUCCESS).register()
|
||||
EventType(JOB_FAILED, _('Job failed'), kind=EVENT_TYPE_KIND_WARNING).register()
|
||||
EventType(JOB_ERRORED, _('Job errored'), kind=EVENT_TYPE_KIND_DANGER).register()
|
@ -1,3 +1,4 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
@ -5,6 +6,8 @@ import django_filters
|
||||
|
||||
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet
|
||||
from netbox.utils import get_data_backend_choices
|
||||
from users.models import User
|
||||
from utilities.filters import ContentTypeFilter
|
||||
from .choices import *
|
||||
from .models import *
|
||||
|
||||
@ -13,6 +16,7 @@ __all__ = (
|
||||
'DataFileFilterSet',
|
||||
'DataSourceFilterSet',
|
||||
'JobFilterSet',
|
||||
'ObjectChangeFilterSet',
|
||||
)
|
||||
|
||||
|
||||
@ -126,6 +130,43 @@ class JobFilterSet(BaseFilterSet):
|
||||
)
|
||||
|
||||
|
||||
class ObjectChangeFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label=_('Search'),
|
||||
)
|
||||
time = django_filters.DateTimeFromToRangeFilter()
|
||||
changed_object_type = ContentTypeFilter()
|
||||
changed_object_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ContentType.objects.all()
|
||||
)
|
||||
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=User.objects.all(),
|
||||
label=_('User (ID)'),
|
||||
)
|
||||
user = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='user__username',
|
||||
queryset=User.objects.all(),
|
||||
to_field_name='username',
|
||||
label=_('User name'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ObjectChange
|
||||
fields = (
|
||||
'id', 'user', 'user_name', 'request_id', 'action', 'changed_object_type_id', 'changed_object_id',
|
||||
'related_object_type', 'related_object_id', 'object_repr',
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(user_name__icontains=value) |
|
||||
Q(object_repr__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class ConfigRevisionFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
|
@ -1,5 +1,4 @@
|
||||
from django import forms
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.choices import *
|
||||
@ -7,8 +6,11 @@ from core.models import *
|
||||
from netbox.forms import NetBoxModelFilterSetForm
|
||||
from netbox.forms.mixins import SavedFiltersMixin
|
||||
from netbox.utils import get_data_backend_choices
|
||||
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm
|
||||
from utilities.forms.fields import ContentTypeChoiceField, DynamicModelMultipleChoiceField
|
||||
from users.models import User
|
||||
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
|
||||
from utilities.forms.fields import (
|
||||
ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField,
|
||||
)
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.widgets import DateTimePicker
|
||||
|
||||
@ -17,6 +19,7 @@ __all__ = (
|
||||
'DataFileFilterForm',
|
||||
'DataSourceFilterForm',
|
||||
'JobFilterForm',
|
||||
'ObjectChangeFilterForm',
|
||||
)
|
||||
|
||||
|
||||
@ -118,12 +121,46 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
|
||||
widget=DateTimePicker()
|
||||
)
|
||||
user = DynamicModelMultipleChoiceField(
|
||||
queryset=get_user_model().objects.all(),
|
||||
queryset=User.objects.all(),
|
||||
required=False,
|
||||
label=_('User')
|
||||
)
|
||||
|
||||
|
||||
class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm):
|
||||
model = ObjectChange
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id'),
|
||||
FieldSet('time_before', 'time_after', name=_('Time')),
|
||||
FieldSet('action', 'user_id', 'changed_object_type_id', name=_('Attributes')),
|
||||
)
|
||||
time_after = forms.DateTimeField(
|
||||
required=False,
|
||||
label=_('After'),
|
||||
widget=DateTimePicker()
|
||||
)
|
||||
time_before = forms.DateTimeField(
|
||||
required=False,
|
||||
label=_('Before'),
|
||||
widget=DateTimePicker()
|
||||
)
|
||||
action = forms.ChoiceField(
|
||||
label=_('Action'),
|
||||
choices=add_blank_choice(ObjectChangeActionChoices),
|
||||
required=False
|
||||
)
|
||||
user_id = DynamicModelMultipleChoiceField(
|
||||
queryset=User.objects.all(),
|
||||
required=False,
|
||||
label=_('User')
|
||||
)
|
||||
changed_object_type_id = ContentTypeMultipleChoiceField(
|
||||
queryset=ObjectType.objects.with_feature('change_logging'),
|
||||
required=False,
|
||||
label=_('Object Type'),
|
||||
)
|
||||
|
||||
|
||||
class ConfigRevisionFilterForm(SavedFiltersMixin, FilterForm):
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id'),
|
||||
|
@ -6,6 +6,7 @@ from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
|
||||
__all__ = (
|
||||
'DataFileFilter',
|
||||
'DataSourceFilter',
|
||||
'ObjectChangeFilter',
|
||||
)
|
||||
|
||||
|
||||
@ -19,3 +20,9 @@ class DataFileFilter(BaseFilterMixin):
|
||||
@autotype_decorator(filtersets.DataSourceFilterSet)
|
||||
class DataSourceFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ObjectChange, lookups=True)
|
||||
@autotype_decorator(filtersets.ObjectChangeFilterSet)
|
||||
class ObjectChangeFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
24
netbox/core/graphql/mixins.py
Normal file
@ -0,0 +1,24 @@
|
||||
from typing import Annotated, List
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from core.models import ObjectChange
|
||||
|
||||
__all__ = (
|
||||
'ChangelogMixin',
|
||||
)
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class ChangelogMixin:
|
||||
|
||||
@strawberry_django.field
|
||||
def changelog(self, info) -> List[Annotated["ObjectChangeType", strawberry.lazy('.types')]]:
|
||||
content_type = ContentType.objects.get_for_model(self)
|
||||
object_changes = ObjectChange.objects.filter(
|
||||
changed_object_type=content_type,
|
||||
changed_object_id=self.pk
|
||||
)
|
||||
return object_changes.restrict(info.context.request.user, 'view')
|
@ -10,6 +10,7 @@ from .filters import *
|
||||
__all__ = (
|
||||
'DataFileType',
|
||||
'DataSourceType',
|
||||
'ObjectChangeType',
|
||||
)
|
||||
|
||||
|
||||
@ -30,3 +31,12 @@ class DataFileType(BaseObjectType):
|
||||
class DataSourceType(NetBoxObjectType):
|
||||
|
||||
datafiles: List[Annotated["DataFileType", strawberry.lazy('core.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.ObjectChange,
|
||||
fields='__all__',
|
||||
filters=ObjectChangeFilter
|
||||
)
|
||||
class ObjectChangeType(BaseObjectType):
|
||||
pass
|
||||
|
@ -1,33 +1,33 @@
|
||||
import logging
|
||||
|
||||
from netbox.jobs import JobRunner
|
||||
from netbox.search.backends import search_backend
|
||||
from .choices import *
|
||||
from .choices import DataSourceStatusChoices
|
||||
from .exceptions import SyncError
|
||||
from .models import DataSource
|
||||
from rq.timeouts import JobTimeoutException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def sync_datasource(job, *args, **kwargs):
|
||||
class SyncDataSourceJob(JobRunner):
|
||||
"""
|
||||
Call sync() on a DataSource.
|
||||
"""
|
||||
datasource = DataSource.objects.get(pk=job.object_id)
|
||||
|
||||
class Meta:
|
||||
name = 'Synchronization'
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
datasource = DataSource.objects.get(pk=self.job.object_id)
|
||||
|
||||
try:
|
||||
job.start()
|
||||
datasource.sync()
|
||||
|
||||
# Update the search cache for DataFiles belonging to this source
|
||||
search_backend.cache(datasource.datafiles.iterator())
|
||||
|
||||
job.terminate()
|
||||
|
||||
except Exception as e:
|
||||
job.terminate(status=JobStatusChoices.STATUS_ERRORED, error=repr(e))
|
||||
DataSource.objects.filter(pk=datasource.pk).update(status=DataSourceStatusChoices.FAILED)
|
||||
if type(e) in (SyncError, JobTimeoutException):
|
||||
if type(e) is SyncError:
|
||||
logging.error(e)
|
||||
else:
|
||||
raise e
|
||||
|
@ -5,10 +5,10 @@ import sys
|
||||
from django import get_version
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from core.models import ObjectType
|
||||
from users.models import User
|
||||
|
||||
APPS = ('circuits', 'core', 'dcim', 'extras', 'ipam', 'tenancy', 'users', 'virtualization', 'vpn', 'wireless')
|
||||
|
||||
@ -18,7 +18,7 @@ BANNER_TEXT = """### NetBox interactive shell ({node})
|
||||
node=platform.node(),
|
||||
python=platform.python_version(),
|
||||
django=get_version(),
|
||||
netbox=settings.VERSION
|
||||
netbox=settings.RELEASE.name
|
||||
)
|
||||
|
||||
|
||||
@ -61,7 +61,7 @@ class Command(BaseCommand):
|
||||
|
||||
# Additional objects to include
|
||||
namespace['ObjectType'] = ObjectType
|
||||
namespace['User'] = get_user_model()
|
||||
namespace['User'] = User
|
||||
|
||||
# Load convenience commands
|
||||
namespace.update({
|
||||
|
45
netbox/core/migrations/0011_move_objectchange.py
Normal file
@ -0,0 +1,45 @@
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('core', '0010_gfk_indexes'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.SeparateDatabaseAndState(
|
||||
state_operations=[
|
||||
migrations.CreateModel(
|
||||
name='ObjectChange',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('time', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||
('user_name', models.CharField(editable=False, max_length=150)),
|
||||
('request_id', models.UUIDField(db_index=True, editable=False)),
|
||||
('action', models.CharField(max_length=50)),
|
||||
('changed_object_id', models.PositiveBigIntegerField()),
|
||||
('related_object_id', models.PositiveBigIntegerField(blank=True, null=True)),
|
||||
('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)),
|
||||
],
|
||||
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')],
|
||||
},
|
||||
),
|
||||
],
|
||||
# Table has been renamed from 'extras' app
|
||||
database_operations=[],
|
||||
),
|
||||
]
|
24
netbox/core/migrations/0012_job_object_type_optional.py
Normal file
@ -0,0 +1,24 @@
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('core', '0011_move_objectchange'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='job',
|
||||
name='object_type',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='jobs',
|
||||
to='contenttypes.contenttype'
|
||||
),
|
||||
),
|
||||
]
|
@ -1,5 +1,6 @@
|
||||
from .config import *
|
||||
from .contenttypes import *
|
||||
from .change_logging import *
|
||||
from .config import *
|
||||
from .data import *
|
||||
from .files import *
|
||||
from .jobs import *
|
||||
|
@ -8,11 +8,11 @@ from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from mptt.models import MPTTModel
|
||||
|
||||
from core.models import ObjectType
|
||||
from extras.choices import *
|
||||
from core.choices import ObjectChangeActionChoices
|
||||
from core.querysets import ObjectChangeQuerySet
|
||||
from netbox.models.features import ChangeLoggingMixin
|
||||
from utilities.data import shallow_compare_dict
|
||||
from ..querysets import ObjectChangeQuerySet
|
||||
from .contenttypes import ObjectType
|
||||
|
||||
__all__ = (
|
||||
'ObjectChange',
|
||||
@ -136,7 +136,7 @@ class ObjectChange(models.Model):
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('extras:objectchange', args=[self.pk])
|
||||
return reverse('core:objectchange', args=[self.pk])
|
||||
|
||||
def get_action_color(self):
|
||||
return ObjectChangeActionChoices.colors.get(self.action)
|
@ -1,10 +1,10 @@
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import yaml
|
||||
from fnmatch import fnmatchcase
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import yaml
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.core.exceptions import ValidationError
|
||||
@ -12,7 +12,6 @@ from django.core.validators import RegexValidator
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.module_loading import import_string
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from netbox.constants import CENSOR_TOKEN, CENSOR_TOKEN_CHANGED
|
||||
@ -22,8 +21,6 @@ from netbox.registry import registry
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from ..choices import *
|
||||
from ..exceptions import SyncError
|
||||
from ..signals import post_sync, pre_sync
|
||||
from .jobs import Job
|
||||
|
||||
__all__ = (
|
||||
'AutoSyncRecord',
|
||||
@ -153,21 +150,6 @@ class DataSource(JobsMixin, PrimaryModel):
|
||||
|
||||
return objectchange
|
||||
|
||||
def enqueue_sync_job(self, request):
|
||||
"""
|
||||
Enqueue a background job to synchronize the DataSource by calling sync().
|
||||
"""
|
||||
# Set the status to "syncing"
|
||||
self.status = DataSourceStatusChoices.QUEUED
|
||||
DataSource.objects.filter(pk=self.pk).update(status=self.status)
|
||||
|
||||
# Enqueue a sync job
|
||||
return Job.enqueue(
|
||||
import_string('core.jobs.sync_datasource'),
|
||||
instance=self,
|
||||
user=request.user
|
||||
)
|
||||
|
||||
def get_backend(self):
|
||||
backend_params = self.parameters or {}
|
||||
return self.backend_class(self.source_url, **backend_params)
|
||||
@ -176,6 +158,8 @@ class DataSource(JobsMixin, PrimaryModel):
|
||||
"""
|
||||
Create/update/delete child DataFiles as necessary to synchronize with the remote source.
|
||||
"""
|
||||
from core.signals import post_sync, pre_sync
|
||||
|
||||
if self.status == DataSourceStatusChoices.SYNCING:
|
||||
raise SyncError(_("Cannot initiate sync; syncing already in progress."))
|
||||
|
||||
|
@ -13,7 +13,6 @@ from django.utils.translation import gettext as _
|
||||
from core.choices import JobStatusChoices
|
||||
from core.models import ObjectType
|
||||
from core.signals import job_end, job_start
|
||||
from extras.constants import EVENT_JOB_END, EVENT_JOB_START
|
||||
from netbox.config import get_config
|
||||
from netbox.constants import RQ_QUEUE_DEFAULT
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
@ -32,6 +31,8 @@ class Job(models.Model):
|
||||
to='contenttypes.ContentType',
|
||||
related_name='jobs',
|
||||
on_delete=models.CASCADE,
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
object_id = models.PositiveBigIntegerField(
|
||||
blank=True,
|
||||
@ -198,25 +199,34 @@ class Job(models.Model):
|
||||
job_end.send(self)
|
||||
|
||||
@classmethod
|
||||
def enqueue(cls, func, instance, name='', user=None, schedule_at=None, interval=None, **kwargs):
|
||||
def enqueue(cls, func, instance=None, name='', user=None, schedule_at=None, interval=None, immediate=False, **kwargs):
|
||||
"""
|
||||
Create a Job instance and enqueue a job using the given callable
|
||||
|
||||
Args:
|
||||
func: The callable object to be enqueued for execution
|
||||
instance: The NetBox object to which this job pertains
|
||||
instance: The NetBox object to which this job pertains (optional)
|
||||
name: Name for the job (optional)
|
||||
user: The user responsible for running the job
|
||||
schedule_at: Schedule the job to be executed at the passed date and time
|
||||
interval: Recurrence interval (in minutes)
|
||||
immediate: Run the job immediately without scheduling it in the background. Should be used for interactive
|
||||
management commands only.
|
||||
"""
|
||||
if schedule_at and immediate:
|
||||
raise ValueError(_("enqueue() cannot be called with values for both schedule_at and immediate."))
|
||||
|
||||
if instance:
|
||||
object_type = ObjectType.objects.get_for_model(instance, for_concrete_model=False)
|
||||
rq_queue_name = get_queue_for_model(object_type.model)
|
||||
object_id = instance.pk
|
||||
else:
|
||||
object_type = object_id = None
|
||||
rq_queue_name = get_queue_for_model(object_type.model if object_type else None)
|
||||
queue = django_rq.get_queue(rq_queue_name)
|
||||
status = JobStatusChoices.STATUS_SCHEDULED if schedule_at else JobStatusChoices.STATUS_PENDING
|
||||
job = Job.objects.create(
|
||||
object_type=object_type,
|
||||
object_id=instance.pk,
|
||||
object_id=object_id,
|
||||
name=name,
|
||||
status=status,
|
||||
scheduled=schedule_at,
|
||||
@ -225,8 +235,16 @@ class Job(models.Model):
|
||||
job_id=uuid.uuid4()
|
||||
)
|
||||
|
||||
if schedule_at:
|
||||
# Run the job immediately, rather than enqueuing it as a background task. Note that this is a synchronous
|
||||
# (blocking) operation, and execution will pause until the job completes.
|
||||
if immediate:
|
||||
func(job_id=str(job.job_id), job=job, **kwargs)
|
||||
|
||||
# Schedule the job to run at a specific date & time.
|
||||
elif schedule_at:
|
||||
queue.enqueue_at(schedule_at, func, job_id=str(job.job_id), job=job, **kwargs)
|
||||
|
||||
# Schedule the job to run asynchronously at this first available opportunity.
|
||||
else:
|
||||
queue.enqueue(func, job_id=str(job.job_id), job=job, **kwargs)
|
||||
|
||||
|
216
netbox/core/plugins.py
Normal file
@ -0,0 +1,216 @@
|
||||
import datetime
|
||||
import importlib
|
||||
import importlib.util
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
|
||||
from netbox.plugins import PluginConfig
|
||||
from utilities.datetime import datetime_from_timestamp
|
||||
|
||||
USER_AGENT_STRING = f'NetBox/{settings.RELEASE.version} {settings.RELEASE.edition}'
|
||||
CACHE_KEY_CATALOG_FEED = 'plugins-catalog-feed'
|
||||
|
||||
|
||||
@dataclass
|
||||
class PluginAuthor:
|
||||
"""
|
||||
Identifying information for the author of a plugin.
|
||||
"""
|
||||
name: str
|
||||
org_id: str = ''
|
||||
url: str = ''
|
||||
|
||||
|
||||
@dataclass
|
||||
class PluginVersion:
|
||||
"""
|
||||
Details for a specific versioned release of a plugin.
|
||||
"""
|
||||
date: datetime.datetime = None
|
||||
version: str = ''
|
||||
netbox_min_version: str = ''
|
||||
netbox_max_version: str = ''
|
||||
has_model: bool = False
|
||||
is_certified: bool = False
|
||||
is_feature: bool = False
|
||||
is_integration: bool = False
|
||||
is_netboxlabs_supported: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class Plugin:
|
||||
"""
|
||||
The representation of a NetBox plugin in the catalog API.
|
||||
"""
|
||||
id: str = ''
|
||||
status: str = ''
|
||||
title_short: str = ''
|
||||
title_long: str = ''
|
||||
tag_line: str = ''
|
||||
description_short: str = ''
|
||||
slug: str = ''
|
||||
author: Optional[PluginAuthor] = None
|
||||
created_at: datetime.datetime = None
|
||||
updated_at: datetime.datetime = None
|
||||
license_type: str = ''
|
||||
homepage_url: str = ''
|
||||
package_name_pypi: str = ''
|
||||
config_name: str = ''
|
||||
is_certified: bool = False
|
||||
release_latest: PluginVersion = field(default_factory=PluginVersion)
|
||||
release_recent_history: list[PluginVersion] = field(default_factory=list)
|
||||
is_local: bool = False # extra field for locally installed plugins
|
||||
is_installed: bool = False
|
||||
installed_version: str = ''
|
||||
|
||||
|
||||
def get_local_plugins(plugins=None):
|
||||
"""
|
||||
Return a dictionary of all locally-installed plugins, mapped by name.
|
||||
"""
|
||||
plugins = plugins or {}
|
||||
local_plugins = {}
|
||||
|
||||
# Gather all locally-installed plugins
|
||||
for plugin_name in settings.PLUGINS:
|
||||
plugin = importlib.import_module(plugin_name)
|
||||
plugin_config: PluginConfig = plugin.config
|
||||
|
||||
local_plugins[plugin_config.name] = Plugin(
|
||||
config_name=plugin_config.name,
|
||||
title_short=plugin_config.verbose_name,
|
||||
title_long=plugin_config.verbose_name,
|
||||
tag_line=plugin_config.description,
|
||||
description_short=plugin_config.description,
|
||||
is_local=True,
|
||||
is_installed=True,
|
||||
installed_version=plugin_config.version,
|
||||
)
|
||||
|
||||
# Update catalog entries for local plugins, or add them to the list if not listed
|
||||
for k, v in local_plugins.items():
|
||||
if k in plugins:
|
||||
plugins[k].is_local = True
|
||||
plugins[k].is_installed = True
|
||||
plugins[k].installed_version = v.installed_version
|
||||
else:
|
||||
plugins[k] = v
|
||||
|
||||
return plugins
|
||||
|
||||
|
||||
def get_catalog_plugins():
|
||||
"""
|
||||
Return a dictionary of all entries in the plugins catalog, mapped by name.
|
||||
"""
|
||||
session = requests.Session()
|
||||
|
||||
# Disable catalog fetching for isolated deployments
|
||||
if settings.ISOLATED_DEPLOYMENT:
|
||||
return {}
|
||||
|
||||
def get_pages():
|
||||
# TODO: pagination is currently broken in API
|
||||
payload = {'page': '1', 'per_page': '50'}
|
||||
first_page = session.get(
|
||||
settings.PLUGIN_CATALOG_URL,
|
||||
headers={'User-Agent': USER_AGENT_STRING},
|
||||
proxies=settings.HTTP_PROXIES,
|
||||
timeout=3,
|
||||
params=payload
|
||||
).json()
|
||||
yield first_page
|
||||
num_pages = first_page['metadata']['pagination']['last_page']
|
||||
|
||||
for page in range(2, num_pages + 1):
|
||||
payload['page'] = page
|
||||
next_page = session.get(
|
||||
settings.PLUGIN_CATALOG_URL,
|
||||
headers={'User-Agent': USER_AGENT_STRING},
|
||||
proxies=settings.HTTP_PROXIES,
|
||||
timeout=3,
|
||||
params=payload
|
||||
).json()
|
||||
yield next_page
|
||||
|
||||
def make_plugin_dict():
|
||||
plugins = {}
|
||||
|
||||
for page in get_pages():
|
||||
for data in page['data']:
|
||||
|
||||
# Populate releases
|
||||
releases = []
|
||||
for version in data['release_recent_history']:
|
||||
releases.append(
|
||||
PluginVersion(
|
||||
date=datetime_from_timestamp(version['date']),
|
||||
version=version['version'],
|
||||
netbox_min_version=version['netbox_min_version'],
|
||||
netbox_max_version=version['netbox_max_version'],
|
||||
has_model=version['has_model'],
|
||||
is_certified=version['is_certified'],
|
||||
is_feature=version['is_feature'],
|
||||
is_integration=version['is_integration'],
|
||||
is_netboxlabs_supported=version['is_netboxlabs_supported'],
|
||||
)
|
||||
)
|
||||
releases = sorted(releases, key=lambda x: x.date, reverse=True)
|
||||
latest_release = PluginVersion(
|
||||
date=datetime_from_timestamp(data['release_latest']['date']),
|
||||
version=data['release_latest']['version'],
|
||||
netbox_min_version=data['release_latest']['netbox_min_version'],
|
||||
netbox_max_version=data['release_latest']['netbox_max_version'],
|
||||
has_model=data['release_latest']['has_model'],
|
||||
is_certified=data['release_latest']['is_certified'],
|
||||
is_feature=data['release_latest']['is_feature'],
|
||||
is_integration=data['release_latest']['is_integration'],
|
||||
is_netboxlabs_supported=data['release_latest']['is_netboxlabs_supported'],
|
||||
)
|
||||
|
||||
# Populate author (if any)
|
||||
if data['author']:
|
||||
author = PluginAuthor(
|
||||
name=data['author']['name'],
|
||||
org_id=data['author']['org_id'],
|
||||
url=data['author']['url'],
|
||||
)
|
||||
else:
|
||||
author = None
|
||||
|
||||
# Populate plugin data
|
||||
plugins[data['config_name']] = Plugin(
|
||||
id=data['id'],
|
||||
status=data['status'],
|
||||
title_short=data['title_short'],
|
||||
title_long=data['title_long'],
|
||||
tag_line=data['tag_line'],
|
||||
description_short=data['description_short'],
|
||||
slug=data['slug'],
|
||||
author=author,
|
||||
created_at=datetime_from_timestamp(data['created_at']),
|
||||
updated_at=datetime_from_timestamp(data['updated_at']),
|
||||
license_type=data['license_type'],
|
||||
homepage_url=data['homepage_url'],
|
||||
package_name_pypi=data['package_name_pypi'],
|
||||
config_name=data['config_name'],
|
||||
is_certified=data['is_certified'],
|
||||
release_latest=latest_release,
|
||||
release_recent_history=releases,
|
||||
)
|
||||
|
||||
return plugins
|
||||
|
||||
catalog_plugins = cache.get(CACHE_KEY_CATALOG_FEED, default={})
|
||||
if not catalog_plugins:
|
||||
try:
|
||||
catalog_plugins = make_plugin_dict()
|
||||
cache.set(CACHE_KEY_CATALOG_FEED, catalog_plugins, 3600)
|
||||
except requests.exceptions.RequestException:
|
||||
pass
|
||||
|
||||
return catalog_plugins
|