mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-28 03:16:25 -06:00
Merge branch 'feature' into 9583-add_column_specific_search_field_to_tables
This commit is contained in:
commit
77bfd620c3
6
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
6
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -13,7 +13,9 @@ body:
|
|||||||
- type: dropdown
|
- type: dropdown
|
||||||
attributes:
|
attributes:
|
||||||
label: Deployment Type
|
label: Deployment Type
|
||||||
description: How are you running NetBox?
|
description: >
|
||||||
|
How are you running NetBox? (For issues with the Docker image, please go to the
|
||||||
|
[netbox-docker](https://github.com/netbox-community/netbox-docker) repo.)
|
||||||
options:
|
options:
|
||||||
- Self-hosted
|
- Self-hosted
|
||||||
- NetBox Cloud
|
- NetBox Cloud
|
||||||
@ -23,7 +25,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox Version
|
label: NetBox Version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v3.7.2
|
placeholder: v3.7.3
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -14,7 +14,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox version
|
label: NetBox version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v3.7.2
|
placeholder: v3.7.3
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<a href="https://github.com/netbox-community/netbox/blob/master/LICENSE.txt"><img src="https://img.shields.io/badge/license-Apache_2.0-blue.svg" alt="License" /></a>
|
<a href="https://github.com/netbox-community/netbox/blob/master/LICENSE.txt"><img src="https://img.shields.io/badge/license-Apache_2.0-blue.svg" alt="License" /></a>
|
||||||
<a href="https://github.com/netbox-community/netbox/graphs/contributors"><img src="https://img.shields.io/github/contributors/netbox-community/netbox?color=blue" alt="Contributors" /></a>
|
<a href="https://github.com/netbox-community/netbox/graphs/contributors"><img src="https://img.shields.io/github/contributors/netbox-community/netbox?color=blue" alt="Contributors" /></a>
|
||||||
<a href="https://github.com/netbox-community/netbox/stargazers"><img src="https://img.shields.io/github/stars/netbox-community/netbox?style=flat" alt="GitHub stars" /></a>
|
<a href="https://github.com/netbox-community/netbox/stargazers"><img src="https://img.shields.io/github/stars/netbox-community/netbox?style=flat" alt="GitHub stars" /></a>
|
||||||
<a href="https://explore.transifex.com/netbox-community/netbox/"><img src="https://img.shields.io/badge/languages-6-blue" alt="Languages supported" /></a>
|
<a href="https://explore.transifex.com/netbox-community/netbox/"><img src="https://img.shields.io/badge/languages-7-blue" alt="Languages supported" /></a>
|
||||||
<a href="https://github.com/netbox-community/netbox/actions/workflows/ci.yml"><img src="https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=master" alt="CI status" /></a>
|
<a href="https://github.com/netbox-community/netbox/actions/workflows/ci.yml"><img src="https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=master" alt="CI status" /></a>
|
||||||
<p></p>
|
<p></p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -105,7 +105,7 @@ mkdocs-material
|
|||||||
mkdocstrings[python-legacy]
|
mkdocstrings[python-legacy]
|
||||||
|
|
||||||
# Library for manipulating IP prefixes and addresses
|
# Library for manipulating IP prefixes and addresses
|
||||||
# https://github.com/netaddr/netaddr/blob/master/CHANGELOG
|
# https://github.com/netaddr/netaddr/blob/master/CHANGELOG.rst
|
||||||
netaddr
|
netaddr
|
||||||
|
|
||||||
# Python bindings to the ammonia HTML sanitization library.
|
# Python bindings to the ammonia HTML sanitization library.
|
||||||
|
@ -67,7 +67,7 @@ When remote user authentication is in use, this is the name of the HTTP header w
|
|||||||
|
|
||||||
Default: `|` (Pipe)
|
Default: `|` (Pipe)
|
||||||
|
|
||||||
The Seperator upon which `REMOTE_AUTH_GROUP_HEADER` gets split into individual Groups. This needs to be coordinated with your authentication Proxy. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` )
|
The Separator upon which `REMOTE_AUTH_GROUP_HEADER` gets split into individual Groups. This needs to be coordinated with your authentication Proxy. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` )
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -304,6 +304,7 @@ A particular object within NetBox. Each ObjectVar must specify a particular mode
|
|||||||
|
|
||||||
* `model` - The model class
|
* `model` - The model class
|
||||||
* `query_params` - A dictionary of query parameters to use when retrieving available options (optional)
|
* `query_params` - A dictionary of query parameters to use when retrieving available options (optional)
|
||||||
|
* `context` - A custom dictionary mapping template context variables to fields, used when rendering `<option>` elements within the dropdown menu (optional; see below)
|
||||||
* `null_option` - A label representing a "null" or empty choice (optional)
|
* `null_option` - A label representing a "null" or empty choice (optional)
|
||||||
|
|
||||||
To limit the selections available within the list, additional query parameters can be passed as the `query_params` dictionary. For example, to show only devices with an "active" status:
|
To limit the selections available within the list, additional query parameters can be passed as the `query_params` dictionary. For example, to show only devices with an "active" status:
|
||||||
@ -331,6 +332,22 @@ site = ObjectVar(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Context Variables
|
||||||
|
|
||||||
|
Custom context variables can be passed to override the default attribute names or to display additional information, such as a parent object.
|
||||||
|
|
||||||
|
| Name | Default | Description |
|
||||||
|
|---------------|-----------------|------------------------------------------------------------------------------|
|
||||||
|
| `value` | `"id"` | The attribute which contains the option's value |
|
||||||
|
| `label` | `"display"` | The attribute used as the option's human-friendly label |
|
||||||
|
| `description` | `"description"` | The attribute to use as a description |
|
||||||
|
| `depth`[^1] | `"_depth"` | The attribute which indicates an object's depth within a recursive hierarchy |
|
||||||
|
| `disabled` | -- | The attribute which, if true, signifies that the option should be disabled |
|
||||||
|
| `parent` | -- | The attribute which represents the object's parent object |
|
||||||
|
| `count`[^1] | -- | The attribute which contains a numeric count of related objects |
|
||||||
|
|
||||||
|
[^1]: The value of this attribute must be a positive integer
|
||||||
|
|
||||||
### MultiObjectVar
|
### MultiObjectVar
|
||||||
|
|
||||||
Similar to `ObjectVar`, but allows for the selection of multiple objects.
|
Similar to `ObjectVar`, but allows for the selection of multiple objects.
|
||||||
|
@ -18,9 +18,9 @@ When a device has one or more interfaces with IP addresses assigned, a primary I
|
|||||||
|
|
||||||
The device's configured name. This field is optional; devices can be unnamed. However, if set, the name must be unique to the assigned site and tenant.
|
The device's configured name. This field is optional; devices can be unnamed. However, if set, the name must be unique to the assigned site and tenant.
|
||||||
|
|
||||||
### Device Role
|
### Role
|
||||||
|
|
||||||
The functional [role](./devicerole.md) assigned to this device.
|
The functional [device role](./devicerole.md) assigned to this device.
|
||||||
|
|
||||||
### Device Type
|
### Device Type
|
||||||
|
|
||||||
|
@ -1,6 +1,41 @@
|
|||||||
# NetBox v3.7
|
# NetBox v3.7
|
||||||
|
|
||||||
## v3.7.3 (FUTURE)
|
## v3.7.4 (FUTURE)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v3.7.3 (2024-02-21)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#14587](https://github.com/netbox-community/netbox/issues/14587) - Display a human-friendly name for the OpenID Connect remote auth backend
|
||||||
|
* [#14946](https://github.com/netbox-community/netbox/issues/14946) - Remove `associate_by_email()` from default social auth pipeline
|
||||||
|
* [#14966](https://github.com/netbox-community/netbox/issues/14966) - Add PostgreSQL index for object type & ID on CachedValue table to improve performance
|
||||||
|
* [#15177](https://github.com/netbox-community/netbox/issues/15177) - Add "last login" time to user display & REST API serializer
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#14058](https://github.com/netbox-community/netbox/issues/14058) - Limit platform options by manufacturer when editing a device or device type
|
||||||
|
* [#14064](https://github.com/netbox-community/netbox/issues/14064) - Resolving parent location should consider assigned site when bulk importing locations
|
||||||
|
* [#14079](https://github.com/netbox-community/netbox/issues/14079) - Ensure changes are logged on related objects when deleting an object referenced via a many-to-many relationship (e.g. tags)
|
||||||
|
* [#14405](https://github.com/netbox-community/netbox/issues/14405) - Clean up formatting of link peers in bulk CSV export of cable termination objects
|
||||||
|
* [#14689](https://github.com/netbox-community/netbox/issues/14689) - Preserve "empty" default values for JSON custom fields
|
||||||
|
* [#14952](https://github.com/netbox-community/netbox/issues/14952) - Update existing AutoSyncRecord when changing the data file of an auto-synced object
|
||||||
|
* [#15059](https://github.com/netbox-community/netbox/issues/15059) - Correct IP address count link in VM interfaces table
|
||||||
|
* [#15067](https://github.com/netbox-community/netbox/issues/15067) - Fix uncaught exception when attempting invalid device bay import
|
||||||
|
* [#15070](https://github.com/netbox-community/netbox/issues/15070) - Fix inclusion of `config_template` field on REST API serializer for virtual machines
|
||||||
|
* [#15084](https://github.com/netbox-community/netbox/issues/15084) - Fix "add export template" link under "export" button on object list views
|
||||||
|
* [#15090](https://github.com/netbox-community/netbox/issues/15090) - Ensure protection rules are evaluated prior to enqueueing events when deleting an object
|
||||||
|
* [#15091](https://github.com/netbox-community/netbox/issues/15091) - Fix designation of the active tab for assigned object when modifying an L2VPN termination
|
||||||
|
* [#15101](https://github.com/netbox-community/netbox/issues/15101) - Correct OpenAPI schema for rack elevation REST API endpoint
|
||||||
|
* [#15115](https://github.com/netbox-community/netbox/issues/15115) - Fix unhandled exception with invalid permission constraints
|
||||||
|
* [#15126](https://github.com/netbox-community/netbox/issues/15126) - `group` field should be optional when creating VPN tunnel via REST API
|
||||||
|
* [#15127](https://github.com/netbox-community/netbox/issues/15127) - Add missing group column to VPN tunnels table
|
||||||
|
* [#15133](https://github.com/netbox-community/netbox/issues/15133) - Fix FHRP group representation on assignments REST API endpoint using brief mode
|
||||||
|
* [#15174](https://github.com/netbox-community/netbox/issues/15174) - Warn that permission constraints are not supported for reports or scripts
|
||||||
|
* [#15184](https://github.com/netbox-community/netbox/issues/15184) - Correct REST API schema definition for `front_image` & `rear_image` on DeviceType
|
||||||
|
* [#15185](https://github.com/netbox-community/netbox/issues/15185) - Ensure error messages pertaining to related objects are displayed on the bulk import form
|
||||||
|
* [#15192](https://github.com/netbox-community/netbox/issues/15192) - Fix exception when viewing current config when no history is present
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
### Breaking Changes
|
### Breaking Changes
|
||||||
|
|
||||||
* The deprecated `device_role` & `device_role_id` filters for devices have been removed. (Use `role` and `role_id` instead.)
|
* The deprecated `device_role` & `device_role_id` filters for devices have been removed. (Use `role` and `role_id` instead.)
|
||||||
|
* The legacy reports functionality has been dropped. Reports will be automatically converted to custom scripts on upgrade.
|
||||||
|
|
||||||
### New Features
|
### New Features
|
||||||
|
|
||||||
@ -12,23 +13,68 @@
|
|||||||
|
|
||||||
The NetBox user interface has been completely refreshed and updated.
|
The NetBox user interface has been completely refreshed and updated.
|
||||||
|
|
||||||
|
#### Dynamic REST API Fields ([#15087](https://github.com/netbox-community/netbox/issues/15087))
|
||||||
|
|
||||||
|
The REST API now supports specifying which fields to include in the response data.
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
* [#12851](https://github.com/netbox-community/netbox/issues/12851) - Replace bleach HTML sanitization library with nh3
|
* [#12851](https://github.com/netbox-community/netbox/issues/12851) - Replace bleach HTML sanitization library with nh3
|
||||||
|
* [#13283](https://github.com/netbox-community/netbox/issues/13283) - Display additional context on API-backed dropdown fields
|
||||||
|
* [#14237](https://github.com/netbox-community/netbox/issues/14237) - Automatically clear dependent selection fields when modifying a parent selection
|
||||||
* [#14637](https://github.com/netbox-community/netbox/issues/14637) - Upgrade to Django 5.0
|
* [#14637](https://github.com/netbox-community/netbox/issues/14637) - Upgrade to Django 5.0
|
||||||
* [#14672](https://github.com/netbox-community/netbox/issues/14672) - Add support for Python 3.12
|
* [#14672](https://github.com/netbox-community/netbox/issues/14672) - Add support for Python 3.12
|
||||||
* [#14728](https://github.com/netbox-community/netbox/issues/14728) - The plugins list view has been moved from the legacy admin UI to the main NetBox UI
|
* [#14728](https://github.com/netbox-community/netbox/issues/14728) - The plugins list view has been moved from the legacy admin UI to the main NetBox UI
|
||||||
* [#14729](https://github.com/netbox-community/netbox/issues/14729) - All background task views have been moved from the legacy admin UI to the main NetBox UI
|
* [#14729](https://github.com/netbox-community/netbox/issues/14729) - All background task views have been moved from the legacy admin UI to the main NetBox UI
|
||||||
|
* [#14438](https://github.com/netbox-community/netbox/issues/14438) - Track individual custom scripts as database objects
|
||||||
|
* [#15131](https://github.com/netbox-community/netbox/issues/15131) - Automatically annotate related object counts on REST API querysets
|
||||||
|
* [#15238](https://github.com/netbox-community/netbox/issues/15238) - Include the `description` field in "brief" REST API serializations
|
||||||
|
|
||||||
### Other Changes
|
### Other Changes
|
||||||
|
|
||||||
* [#12325](https://github.com/netbox-community/netbox/issues/12325) - The Django admin UI is now disabled by default (set `DJANGO_ADMIN_ENABLED` to True to enable it)
|
* [#12325](https://github.com/netbox-community/netbox/issues/12325) - The Django admin UI is now disabled by default (set `DJANGO_ADMIN_ENABLED` to True to enable it)
|
||||||
* [#12795](https://github.com/netbox-community/netbox/issues/12795) - NetBox now uses a custom User model rather than the stock model provided by Django
|
* [#12510](https://github.com/netbox-community/netbox/issues/12510) - Dropped support for legacy reports
|
||||||
|
* [#12795](https://github.com/netbox-community/netbox/issues/12795) - NetBox now uses custom User and Group models rather than the stock models provided by Django
|
||||||
* [#13647](https://github.com/netbox-community/netbox/issues/13647) - Squash all database migrations prior to v3.7
|
* [#13647](https://github.com/netbox-community/netbox/issues/13647) - Squash all database migrations prior to v3.7
|
||||||
* [#14092](https://github.com/netbox-community/netbox/issues/14092) - Remove backward compatibility for importing plugin resources from `extras.plugins` (now `netbox.plugins`)
|
* [#14092](https://github.com/netbox-community/netbox/issues/14092) - Remove backward compatibility for importing plugin resources from `extras.plugins` (now `netbox.plugins`)
|
||||||
* [#14638](https://github.com/netbox-community/netbox/issues/14638) - Drop support for Python 3.8 and 3.9
|
* [#14638](https://github.com/netbox-community/netbox/issues/14638) - Drop support for Python 3.8 and 3.9
|
||||||
* [#14657](https://github.com/netbox-community/netbox/issues/14657) - Remove backward compatibility for old permissions mapping under `ActionsMixin`
|
* [#14657](https://github.com/netbox-community/netbox/issues/14657) - Remove backward compatibility for old permissions mapping under `ActionsMixin`
|
||||||
* [#14658](https://github.com/netbox-community/netbox/issues/14658) - Remove backward compatibility for importing `process_webhook()` (now `extras.webhooks.send_webhook()`)
|
* [#14658](https://github.com/netbox-community/netbox/issues/14658) - Remove backward compatibility for importing `process_webhook()` (now `extras.webhooks.send_webhook()`)
|
||||||
* [#14740](https://github.com/netbox-community/netbox/issues/14740) - Remove the obsolete `BootstrapMixin` form mixin class
|
* [#14740](https://github.com/netbox-community/netbox/issues/14740) - Remove the obsolete `BootstrapMixin` form mixin class
|
||||||
|
* [#15042](https://github.com/netbox-community/netbox/issues/15042) - Rearchitect the logic for registering models & model features
|
||||||
* [#15099](https://github.com/netbox-community/netbox/issues/15099) - Remove obsolete `device_role` and `device_role_id` filters for devices
|
* [#15099](https://github.com/netbox-community/netbox/issues/15099) - Remove obsolete `device_role` and `device_role_id` filters for devices
|
||||||
* [#15100](https://github.com/netbox-community/netbox/issues/15100) - Remove obsolete `NullableCharField` class
|
* [#15100](https://github.com/netbox-community/netbox/issues/15100) - Remove obsolete `NullableCharField` class
|
||||||
|
* [#15277](https://github.com/netbox-community/netbox/issues/15277) - Replace references to ContentType without ObjectType proxy model & standardize field names
|
||||||
|
* [#15292](https://github.com/netbox-community/netbox/issues/15292) - Remove obsolete `device_role` attribute from Device model (this field was renamed to `role` in v3.6)
|
||||||
|
|
||||||
|
### REST API Changes
|
||||||
|
|
||||||
|
* The `/api/extras/content-types/` endpoint has moved to `/api/extras/object-types/`
|
||||||
|
* dcim.Device
|
||||||
|
* The obsolete read-only attribute `device_role` has been removed (replaced by `role` in v3.6)
|
||||||
|
* extras.CustomField
|
||||||
|
* `content_types` has been renamed to `object_types`
|
||||||
|
* The `content_types` filter is now `object_type`
|
||||||
|
* The `content_type_id` filter is now `object_type_id`
|
||||||
|
* extras.CustomLink
|
||||||
|
* `content_types` has been renamed to `object_types`
|
||||||
|
* The `content_types` filter is now `object_type`
|
||||||
|
* The `content_type_id` filter is now `object_type_id`
|
||||||
|
* extras.EventRule
|
||||||
|
* `content_types` has been renamed to `object_types`
|
||||||
|
* The `content_types` filter is now `object_type`
|
||||||
|
* The `content_type_id` filter is now `object_type_id`
|
||||||
|
* extras.ExportTemplate
|
||||||
|
* `content_types` has been renamed to `object_types`
|
||||||
|
* The `content_types` filter is now `object_type`
|
||||||
|
* The `content_type_id` filter is now `object_type_id`
|
||||||
|
* extras.ImageAttachment
|
||||||
|
* `content_type` has been renamed to `object_type`
|
||||||
|
* The `content_type` filter is now `object_type`
|
||||||
|
* extras.SavedFilter
|
||||||
|
* `content_types` has been renamed to `object_types`
|
||||||
|
* The `content_types` filter is now `object_type`
|
||||||
|
* The `content_type_id` filter is now `object_type_id`
|
||||||
|
* tenancy.ContactAssignment
|
||||||
|
* `content_type` has been renamed to `object_type`
|
||||||
|
* The `content_type_id` filter is now `object_type_id`
|
||||||
|
@ -292,6 +292,7 @@ nav:
|
|||||||
- git Cheat Sheet: 'development/git-cheat-sheet.md'
|
- git Cheat Sheet: 'development/git-cheat-sheet.md'
|
||||||
- Release Notes:
|
- Release Notes:
|
||||||
- Summary: 'release-notes/index.md'
|
- Summary: 'release-notes/index.md'
|
||||||
|
- Version 4.0: 'release-notes/version-4.0.md'
|
||||||
- Version 3.7: 'release-notes/version-3.7.md'
|
- Version 3.7: 'release-notes/version-3.7.md'
|
||||||
- Version 3.6: 'release-notes/version-3.6.md'
|
- Version 3.6: 'release-notes/version-3.6.md'
|
||||||
- Version 3.5: 'release-notes/version-3.5.md'
|
- Version 3.5: 'release-notes/version-3.5.md'
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
from drf_spectacular.utils import extend_schema_field, extend_schema_serializer
|
from drf_spectacular.utils import extend_schema_serializer
|
||||||
from drf_spectacular.types import OpenApiTypes
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from circuits.models import *
|
from circuits.models import *
|
||||||
|
from netbox.api.fields import RelatedObjectCountField
|
||||||
from netbox.api.serializers import WritableNestedSerializer
|
from netbox.api.serializers import WritableNestedSerializer
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@ -36,7 +36,7 @@ class NestedProviderNetworkSerializer(WritableNestedSerializer):
|
|||||||
)
|
)
|
||||||
class NestedProviderSerializer(WritableNestedSerializer):
|
class NestedProviderSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
|
||||||
circuit_count = serializers.IntegerField(read_only=True)
|
circuit_count = RelatedObjectCountField('circuits')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Provider
|
model = Provider
|
||||||
@ -64,7 +64,7 @@ class NestedProviderAccountSerializer(WritableNestedSerializer):
|
|||||||
)
|
)
|
||||||
class NestedCircuitTypeSerializer(WritableNestedSerializer):
|
class NestedCircuitTypeSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
|
||||||
circuit_count = serializers.IntegerField(read_only=True)
|
circuit_count = RelatedObjectCountField('circuits')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitType
|
model = CircuitType
|
||||||
|
@ -1,137 +1,3 @@
|
|||||||
from rest_framework import serializers
|
from .serializers_.providers import *
|
||||||
|
from .serializers_.circuits import *
|
||||||
from circuits.choices import CircuitStatusChoices
|
|
||||||
from circuits.models import *
|
|
||||||
from dcim.api.nested_serializers import NestedSiteSerializer
|
|
||||||
from dcim.api.serializers import CabledObjectSerializer
|
|
||||||
from ipam.models import ASN
|
|
||||||
from ipam.api.nested_serializers import NestedASNSerializer
|
|
||||||
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
|
|
||||||
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
|
|
||||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
|
||||||
from .nested_serializers import *
|
from .nested_serializers import *
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Providers
|
|
||||||
#
|
|
||||||
|
|
||||||
class ProviderSerializer(NetBoxModelSerializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
|
|
||||||
accounts = SerializedPKRelatedField(
|
|
||||||
queryset=ProviderAccount.objects.all(),
|
|
||||||
serializer=NestedProviderAccountSerializer,
|
|
||||||
required=False,
|
|
||||||
many=True
|
|
||||||
)
|
|
||||||
asns = SerializedPKRelatedField(
|
|
||||||
queryset=ASN.objects.all(),
|
|
||||||
serializer=NestedASNSerializer,
|
|
||||||
required=False,
|
|
||||||
many=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Related object counts
|
|
||||||
circuit_count = serializers.IntegerField(read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Provider
|
|
||||||
fields = [
|
|
||||||
'id', 'url', 'display', 'name', 'slug', 'accounts', 'description', 'comments', 'asns', 'tags',
|
|
||||||
'custom_fields', 'created', 'last_updated', 'circuit_count',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Provider Accounts
|
|
||||||
#
|
|
||||||
|
|
||||||
class ProviderAccountSerializer(NetBoxModelSerializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provideraccount-detail')
|
|
||||||
provider = NestedProviderSerializer()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = ProviderAccount
|
|
||||||
fields = [
|
|
||||||
'id', 'url', 'display', 'provider', 'name', 'account', 'description', 'comments', 'tags', 'custom_fields',
|
|
||||||
'created', 'last_updated',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Provider networks
|
|
||||||
#
|
|
||||||
|
|
||||||
class ProviderNetworkSerializer(NetBoxModelSerializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail')
|
|
||||||
provider = NestedProviderSerializer()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = ProviderNetwork
|
|
||||||
fields = [
|
|
||||||
'id', 'url', 'display', 'provider', 'name', 'service_id', 'description', 'comments', 'tags',
|
|
||||||
'custom_fields', 'created', 'last_updated',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Circuits
|
|
||||||
#
|
|
||||||
|
|
||||||
class CircuitTypeSerializer(NetBoxModelSerializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
|
|
||||||
circuit_count = serializers.IntegerField(read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = CircuitType
|
|
||||||
fields = [
|
|
||||||
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
|
|
||||||
'circuit_count',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
|
||||||
site = NestedSiteSerializer(allow_null=True)
|
|
||||||
provider_network = NestedProviderNetworkSerializer(allow_null=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = CircuitTermination
|
|
||||||
fields = [
|
|
||||||
'id', 'url', 'display', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
|
|
||||||
'description',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class CircuitSerializer(NetBoxModelSerializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
|
|
||||||
provider = NestedProviderSerializer()
|
|
||||||
provider_account = NestedProviderAccountSerializer(required=False, allow_null=True)
|
|
||||||
status = ChoiceField(choices=CircuitStatusChoices, required=False)
|
|
||||||
type = NestedCircuitTypeSerializer()
|
|
||||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
|
||||||
termination_a = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
|
|
||||||
termination_z = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
|
|
||||||
|
|
||||||
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',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
|
||||||
circuit = NestedCircuitSerializer()
|
|
||||||
site = NestedSiteSerializer(required=False, allow_null=True)
|
|
||||||
provider_network = NestedProviderNetworkSerializer(required=False, allow_null=True)
|
|
||||||
|
|
||||||
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',
|
|
||||||
]
|
|
||||||
|
0
netbox/circuits/api/serializers_/__init__.py
Normal file
0
netbox/circuits/api/serializers_/__init__.py
Normal file
81
netbox/circuits/api/serializers_/circuits.py
Normal file
81
netbox/circuits/api/serializers_/circuits.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from circuits.choices import CircuitStatusChoices
|
||||||
|
from circuits.models import Circuit, CircuitTermination, CircuitType
|
||||||
|
from dcim.api.serializers_.cables import CabledObjectSerializer
|
||||||
|
from dcim.api.serializers_.sites import SiteSerializer
|
||||||
|
from netbox.api.fields import ChoiceField, RelatedObjectCountField
|
||||||
|
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
|
||||||
|
from tenancy.api.serializers_.tenants import TenantSerializer
|
||||||
|
|
||||||
|
from .providers import ProviderAccountSerializer, ProviderNetworkSerializer, ProviderSerializer
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'CircuitSerializer',
|
||||||
|
'CircuitTerminationSerializer',
|
||||||
|
'CircuitTypeSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitTypeSerializer(NetBoxModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
|
||||||
|
|
||||||
|
# Related object counts
|
||||||
|
circuit_count = RelatedObjectCountField('circuits')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CircuitType
|
||||||
|
fields = [
|
||||||
|
'id', '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',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
status = ChoiceField(choices=CircuitStatusChoices, required=False)
|
||||||
|
type = CircuitTypeSerializer(nested=True)
|
||||||
|
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||||
|
termination_a = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
|
||||||
|
termination_z = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
|
||||||
|
|
||||||
|
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',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', '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)
|
||||||
|
|
||||||
|
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',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'circuit', 'term_side', 'description', 'cable', '_occupied')
|
68
netbox/circuits/api/serializers_/providers.py
Normal file
68
netbox/circuits/api/serializers_/providers.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from circuits.models import Provider, ProviderAccount, ProviderNetwork
|
||||||
|
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 *
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'ProviderAccountSerializer',
|
||||||
|
'ProviderNetworkSerializer',
|
||||||
|
'ProviderSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProviderSerializer(NetBoxModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
|
||||||
|
accounts = SerializedPKRelatedField(
|
||||||
|
queryset=ProviderAccount.objects.all(),
|
||||||
|
serializer=NestedProviderAccountSerializer,
|
||||||
|
required=False,
|
||||||
|
many=True
|
||||||
|
)
|
||||||
|
asns = SerializedPKRelatedField(
|
||||||
|
queryset=ASN.objects.all(),
|
||||||
|
serializer=ASNSerializer,
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
many=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Related object counts
|
||||||
|
circuit_count = RelatedObjectCountField('circuits')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Provider
|
||||||
|
fields = [
|
||||||
|
'id', '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)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ProviderAccount
|
||||||
|
fields = [
|
||||||
|
'id', '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',
|
||||||
|
'custom_fields', 'created', 'last_updated',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
@ -4,7 +4,6 @@ from circuits import filtersets
|
|||||||
from circuits.models import *
|
from circuits.models import *
|
||||||
from dcim.api.views import PassThroughPortMixin
|
from dcim.api.views import PassThroughPortMixin
|
||||||
from netbox.api.viewsets import NetBoxModelViewSet
|
from netbox.api.viewsets import NetBoxModelViewSet
|
||||||
from utilities.utils import count_related
|
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
|
||||||
|
|
||||||
@ -21,9 +20,7 @@ class CircuitsRootView(APIRootView):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class ProviderViewSet(NetBoxModelViewSet):
|
class ProviderViewSet(NetBoxModelViewSet):
|
||||||
queryset = Provider.objects.prefetch_related('asns', 'tags').annotate(
|
queryset = Provider.objects.all()
|
||||||
circuit_count=count_related(Circuit, 'provider')
|
|
||||||
)
|
|
||||||
serializer_class = serializers.ProviderSerializer
|
serializer_class = serializers.ProviderSerializer
|
||||||
filterset_class = filtersets.ProviderFilterSet
|
filterset_class = filtersets.ProviderFilterSet
|
||||||
|
|
||||||
@ -33,9 +30,7 @@ class ProviderViewSet(NetBoxModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class CircuitTypeViewSet(NetBoxModelViewSet):
|
class CircuitTypeViewSet(NetBoxModelViewSet):
|
||||||
queryset = CircuitType.objects.prefetch_related('tags').annotate(
|
queryset = CircuitType.objects.all()
|
||||||
circuit_count=count_related(Circuit, 'type')
|
|
||||||
)
|
|
||||||
serializer_class = serializers.CircuitTypeSerializer
|
serializer_class = serializers.CircuitTypeSerializer
|
||||||
filterset_class = filtersets.CircuitTypeFilterSet
|
filterset_class = filtersets.CircuitTypeFilterSet
|
||||||
|
|
||||||
@ -45,9 +40,7 @@ class CircuitTypeViewSet(NetBoxModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class CircuitViewSet(NetBoxModelViewSet):
|
class CircuitViewSet(NetBoxModelViewSet):
|
||||||
queryset = Circuit.objects.prefetch_related(
|
queryset = Circuit.objects.all()
|
||||||
'type', 'tenant', 'provider', 'provider_account', 'termination_a', 'termination_z'
|
|
||||||
).prefetch_related('tags')
|
|
||||||
serializer_class = serializers.CircuitSerializer
|
serializer_class = serializers.CircuitSerializer
|
||||||
filterset_class = filtersets.CircuitFilterSet
|
filterset_class = filtersets.CircuitFilterSet
|
||||||
|
|
||||||
@ -57,12 +50,9 @@ class CircuitViewSet(NetBoxModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class CircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
class CircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
||||||
queryset = CircuitTermination.objects.prefetch_related(
|
queryset = CircuitTermination.objects.all()
|
||||||
'circuit', 'site', 'provider_network', 'cable__terminations'
|
|
||||||
)
|
|
||||||
serializer_class = serializers.CircuitTerminationSerializer
|
serializer_class = serializers.CircuitTerminationSerializer
|
||||||
filterset_class = filtersets.CircuitTerminationFilterSet
|
filterset_class = filtersets.CircuitTerminationFilterSet
|
||||||
brief_prefetch_fields = ['circuit']
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -70,7 +60,7 @@ class CircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class ProviderAccountViewSet(NetBoxModelViewSet):
|
class ProviderAccountViewSet(NetBoxModelViewSet):
|
||||||
queryset = ProviderAccount.objects.prefetch_related('provider', 'tags')
|
queryset = ProviderAccount.objects.all()
|
||||||
serializer_class = serializers.ProviderAccountSerializer
|
serializer_class = serializers.ProviderAccountSerializer
|
||||||
filterset_class = filtersets.ProviderAccountFilterSet
|
filterset_class = filtersets.ProviderAccountFilterSet
|
||||||
|
|
||||||
@ -80,6 +70,6 @@ class ProviderAccountViewSet(NetBoxModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class ProviderNetworkViewSet(NetBoxModelViewSet):
|
class ProviderNetworkViewSet(NetBoxModelViewSet):
|
||||||
queryset = ProviderNetwork.objects.prefetch_related('tags')
|
queryset = ProviderNetwork.objects.all()
|
||||||
serializer_class = serializers.ProviderNetworkSerializer
|
serializer_class = serializers.ProviderNetworkSerializer
|
||||||
filterset_class = filtersets.ProviderNetworkFilterSet
|
filterset_class = filtersets.ProviderNetworkFilterSet
|
||||||
|
@ -6,4 +6,8 @@ class CircuitsConfig(AppConfig):
|
|||||||
verbose_name = "Circuits"
|
verbose_name = "Circuits"
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
|
from netbox.models.features import register_models
|
||||||
from . import signals, search
|
from . import signals, search
|
||||||
|
|
||||||
|
# Register models
|
||||||
|
register_models(*self.get_models())
|
||||||
|
@ -234,9 +234,9 @@ class CircuitTermination(
|
|||||||
|
|
||||||
# Must define either site *or* provider network
|
# Must define either site *or* provider network
|
||||||
if self.site is None and self.provider_network is None:
|
if self.site is None and self.provider_network is None:
|
||||||
raise ValidationError("A circuit termination must attach to either a site or a provider network.")
|
raise ValidationError(_("A circuit termination must attach to either a site or a provider network."))
|
||||||
if self.site and self.provider_network:
|
if self.site and self.provider_network:
|
||||||
raise ValidationError("A circuit termination cannot attach to both a site and a provider network.")
|
raise ValidationError(_("A circuit termination cannot attach to both a site and a provider network."))
|
||||||
|
|
||||||
def to_objectchange(self, action):
|
def to_objectchange(self, action):
|
||||||
objectchange = super().to_objectchange(action)
|
objectchange = super().to_objectchange(action)
|
||||||
|
@ -18,7 +18,7 @@ class AppTest(APITestCase):
|
|||||||
|
|
||||||
class ProviderTest(APIViewTestCases.APIViewTestCase):
|
class ProviderTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = Provider
|
model = Provider
|
||||||
brief_fields = ['circuit_count', 'display', 'id', 'name', 'slug', 'url']
|
brief_fields = ['circuit_count', 'description', 'display', 'id', 'name', 'slug', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'comments': 'New comments',
|
'comments': 'New comments',
|
||||||
}
|
}
|
||||||
@ -60,7 +60,7 @@ class ProviderTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class CircuitTypeTest(APIViewTestCases.APIViewTestCase):
|
class CircuitTypeTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = CircuitType
|
model = CircuitType
|
||||||
brief_fields = ['circuit_count', 'display', 'id', 'name', 'slug', 'url']
|
brief_fields = ['circuit_count', 'description', 'display', 'id', 'name', 'slug', 'url']
|
||||||
create_data = (
|
create_data = (
|
||||||
{
|
{
|
||||||
'name': 'Circuit Type 4',
|
'name': 'Circuit Type 4',
|
||||||
@ -92,7 +92,7 @@ class CircuitTypeTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class CircuitTest(APIViewTestCases.APIViewTestCase):
|
class CircuitTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = Circuit
|
model = Circuit
|
||||||
brief_fields = ['cid', 'display', 'id', 'url']
|
brief_fields = ['cid', 'description', 'display', 'id', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'status': 'planned',
|
'status': 'planned',
|
||||||
}
|
}
|
||||||
@ -149,7 +149,7 @@ class CircuitTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
|
class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = CircuitTermination
|
model = CircuitTermination
|
||||||
brief_fields = ['_occupied', 'cable', 'circuit', 'display', 'id', 'term_side', 'url']
|
brief_fields = ['_occupied', 'cable', 'circuit', 'description', 'display', 'id', 'term_side', 'url']
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
@ -208,7 +208,7 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class ProviderAccountTest(APIViewTestCases.APIViewTestCase):
|
class ProviderAccountTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = ProviderAccount
|
model = ProviderAccount
|
||||||
brief_fields = ['account', 'display', 'id', 'name', 'url']
|
brief_fields = ['account', 'description', 'display', 'id', 'name', 'url']
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
@ -251,7 +251,7 @@ class ProviderAccountTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class ProviderNetworkTest(APIViewTestCases.APIViewTestCase):
|
class ProviderNetworkTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = ProviderNetwork
|
model = ProviderNetwork
|
||||||
brief_fields = ['display', 'id', 'name', 'url']
|
brief_fields = ['description', 'display', 'id', 'name', 'url']
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
|
@ -4,7 +4,7 @@ from core.choices import JobStatusChoices
|
|||||||
from core.models import *
|
from core.models import *
|
||||||
from netbox.api.fields import ChoiceField
|
from netbox.api.fields import ChoiceField
|
||||||
from netbox.api.serializers import WritableNestedSerializer
|
from netbox.api.serializers import WritableNestedSerializer
|
||||||
from users.api.nested_serializers import NestedUserSerializer
|
from users.api.serializers import UserSerializer
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'NestedDataFileSerializer',
|
'NestedDataFileSerializer',
|
||||||
@ -32,7 +32,8 @@ class NestedDataFileSerializer(WritableNestedSerializer):
|
|||||||
class NestedJobSerializer(serializers.ModelSerializer):
|
class NestedJobSerializer(serializers.ModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail')
|
||||||
status = ChoiceField(choices=JobStatusChoices)
|
status = ChoiceField(choices=JobStatusChoices)
|
||||||
user = NestedUserSerializer(
|
user = UserSerializer(
|
||||||
|
nested=True,
|
||||||
read_only=True
|
read_only=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ from drf_spectacular.plumbing import (
|
|||||||
build_basic_type, build_choice_field, build_media_type_object, build_object_type, get_doc,
|
build_basic_type, build_choice_field, build_media_type_object, build_object_type, get_doc,
|
||||||
)
|
)
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
|
from rest_framework import serializers
|
||||||
from rest_framework.relations import ManyRelatedField
|
from rest_framework.relations import ManyRelatedField
|
||||||
|
|
||||||
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
|
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
|
||||||
|
@ -1,73 +1,3 @@
|
|||||||
from rest_framework import serializers
|
from .serializers_.data import *
|
||||||
|
from .serializers_.jobs import *
|
||||||
from core.choices import *
|
|
||||||
from core.models import *
|
|
||||||
from netbox.api.fields import ChoiceField, ContentTypeField
|
|
||||||
from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer
|
|
||||||
from netbox.utils import get_data_backend_choices
|
|
||||||
from users.api.nested_serializers import NestedUserSerializer
|
|
||||||
from .nested_serializers import *
|
from .nested_serializers import *
|
||||||
|
|
||||||
__all__ = (
|
|
||||||
'DataFileSerializer',
|
|
||||||
'DataSourceSerializer',
|
|
||||||
'JobSerializer',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class DataSourceSerializer(NetBoxModelSerializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(
|
|
||||||
view_name='core-api:datasource-detail'
|
|
||||||
)
|
|
||||||
type = ChoiceField(
|
|
||||||
choices=get_data_backend_choices()
|
|
||||||
)
|
|
||||||
status = ChoiceField(
|
|
||||||
choices=DataSourceStatusChoices,
|
|
||||||
read_only=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Related object counts
|
|
||||||
file_count = serializers.IntegerField(
|
|
||||||
read_only=True
|
|
||||||
)
|
|
||||||
|
|
||||||
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',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class DataFileSerializer(NetBoxModelSerializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(
|
|
||||||
view_name='core-api:datafile-detail'
|
|
||||||
)
|
|
||||||
source = NestedDataSourceSerializer(
|
|
||||||
read_only=True
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = DataFile
|
|
||||||
fields = [
|
|
||||||
'id', 'url', 'display', 'source', 'path', 'last_updated', 'size', 'hash',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class JobSerializer(BaseModelSerializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail')
|
|
||||||
user = NestedUserSerializer(
|
|
||||||
read_only=True
|
|
||||||
)
|
|
||||||
status = ChoiceField(choices=JobStatusChoices, read_only=True)
|
|
||||||
object_type = ContentTypeField(
|
|
||||||
read_only=True
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Job
|
|
||||||
fields = [
|
|
||||||
'id', 'url', 'display', 'object_type', 'object_id', 'name', 'status', 'created', 'scheduled', 'interval',
|
|
||||||
'started', 'completed', 'user', 'data', 'error', 'job_id',
|
|
||||||
]
|
|
||||||
|
0
netbox/core/api/serializers_/__init__.py
Normal file
0
netbox/core/api/serializers_/__init__.py
Normal file
53
netbox/core/api/serializers_/data.py
Normal file
53
netbox/core/api/serializers_/data.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from core.choices import *
|
||||||
|
from core.models import DataFile, DataSource
|
||||||
|
from netbox.api.fields import ChoiceField, RelatedObjectCountField
|
||||||
|
from netbox.api.serializers import NetBoxModelSerializer
|
||||||
|
from netbox.utils import get_data_backend_choices
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'DataFileSerializer',
|
||||||
|
'DataSourceSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DataSourceSerializer(NetBoxModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(
|
||||||
|
view_name='core-api:datasource-detail'
|
||||||
|
)
|
||||||
|
type = ChoiceField(
|
||||||
|
choices=get_data_backend_choices()
|
||||||
|
)
|
||||||
|
status = ChoiceField(
|
||||||
|
choices=DataSourceStatusChoices,
|
||||||
|
read_only=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Related object counts
|
||||||
|
file_count = RelatedObjectCountField('datafiles')
|
||||||
|
|
||||||
|
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',
|
||||||
|
]
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = DataFile
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'source', 'path', 'last_updated', 'size', 'hash',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'path')
|
31
netbox/core/api/serializers_/jobs.py
Normal file
31
netbox/core/api/serializers_/jobs.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from core.choices import *
|
||||||
|
from core.models import Job
|
||||||
|
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||||
|
from netbox.api.serializers import BaseModelSerializer
|
||||||
|
from users.api.serializers_.users import UserSerializer
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'JobSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class JobSerializer(BaseModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail')
|
||||||
|
user = UserSerializer(
|
||||||
|
nested=True,
|
||||||
|
read_only=True
|
||||||
|
)
|
||||||
|
status = ChoiceField(choices=JobStatusChoices, read_only=True)
|
||||||
|
object_type = ContentTypeField(
|
||||||
|
read_only=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Job
|
||||||
|
fields = [
|
||||||
|
'id', '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')
|
@ -9,7 +9,6 @@ from rest_framework.viewsets import ReadOnlyModelViewSet
|
|||||||
from core import filtersets
|
from core import filtersets
|
||||||
from core.models import *
|
from core.models import *
|
||||||
from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet
|
from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet
|
||||||
from utilities.utils import count_related
|
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
|
||||||
|
|
||||||
@ -22,9 +21,7 @@ class CoreRootView(APIRootView):
|
|||||||
|
|
||||||
|
|
||||||
class DataSourceViewSet(NetBoxModelViewSet):
|
class DataSourceViewSet(NetBoxModelViewSet):
|
||||||
queryset = DataSource.objects.annotate(
|
queryset = DataSource.objects.all()
|
||||||
file_count=count_related(DataFile, 'source')
|
|
||||||
)
|
|
||||||
serializer_class = serializers.DataSourceSerializer
|
serializer_class = serializers.DataSourceSerializer
|
||||||
filterset_class = filtersets.DataSourceFilterSet
|
filterset_class = filtersets.DataSourceFilterSet
|
||||||
|
|
||||||
@ -44,7 +41,7 @@ class DataSourceViewSet(NetBoxModelViewSet):
|
|||||||
|
|
||||||
|
|
||||||
class DataFileViewSet(NetBoxReadOnlyModelViewSet):
|
class DataFileViewSet(NetBoxReadOnlyModelViewSet):
|
||||||
queryset = DataFile.objects.defer('data').prefetch_related('source')
|
queryset = DataFile.objects.defer('data')
|
||||||
serializer_class = serializers.DataFileSerializer
|
serializer_class = serializers.DataFileSerializer
|
||||||
filterset_class = filtersets.DataFileFilterSet
|
filterset_class = filtersets.DataFileFilterSet
|
||||||
|
|
||||||
@ -53,6 +50,6 @@ class JobViewSet(ReadOnlyModelViewSet):
|
|||||||
"""
|
"""
|
||||||
Retrieve a list of job results
|
Retrieve a list of job results
|
||||||
"""
|
"""
|
||||||
queryset = Job.objects.prefetch_related('user')
|
queryset = Job.objects.all()
|
||||||
serializer_class = serializers.JobSerializer
|
serializer_class = serializers.JobSerializer
|
||||||
filterset_class = filtersets.JobFilterSet
|
filterset_class = filtersets.JobFilterSet
|
||||||
|
@ -16,5 +16,9 @@ class CoreConfig(AppConfig):
|
|||||||
name = "core"
|
name = "core"
|
||||||
|
|
||||||
def ready(self):
|
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, search
|
||||||
from core.api import schema # noqa: E402
|
|
||||||
|
# Register models
|
||||||
|
register_models(*self.get_models())
|
||||||
|
@ -102,7 +102,7 @@ class GitBackend(DataBackend):
|
|||||||
try:
|
try:
|
||||||
porcelain.clone(self.url, local_path.name, **clone_args)
|
porcelain.clone(self.url, local_path.name, **clone_args)
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
raise SyncError(f"Fetching remote data failed ({type(e).__name__}): {e}")
|
raise SyncError(_("Fetching remote data failed ({name}): {error}").format(name=type(e).__name__, error=e))
|
||||||
|
|
||||||
yield local_path.name
|
yield local_path.name
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
)
|
)
|
||||||
object_type = ContentTypeChoiceField(
|
object_type = ContentTypeChoiceField(
|
||||||
label=_('Object Type'),
|
label=_('Object Type'),
|
||||||
queryset=ContentType.objects.with_feature('jobs'),
|
queryset=ObjectType.objects.with_feature('jobs'),
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
status = forms.MultipleChoiceField(
|
status = forms.MultipleChoiceField(
|
||||||
|
@ -103,9 +103,9 @@ class ManagedFileForm(SyncedDataMixin, NetBoxModelForm):
|
|||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
if self.cleaned_data.get('upload_file') and self.cleaned_data.get('data_file'):
|
if self.cleaned_data.get('upload_file') and self.cleaned_data.get('data_file'):
|
||||||
raise forms.ValidationError("Cannot upload a file and sync from an existing file")
|
raise forms.ValidationError(_("Cannot upload a file and sync from an existing file"))
|
||||||
if not self.cleaned_data.get('upload_file') and not self.cleaned_data.get('data_file'):
|
if not self.cleaned_data.get('upload_file') and not self.cleaned_data.get('data_file'):
|
||||||
raise forms.ValidationError("Must upload a file or select a data file to sync")
|
raise forms.ValidationError(_("Must upload a file or select a data file to sync"))
|
||||||
|
|
||||||
return self.cleaned_data
|
return self.cleaned_data
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ from django.conf import settings
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
from core.models import ContentType
|
from core.models import ObjectType
|
||||||
|
|
||||||
APPS = ('circuits', 'core', 'dcim', 'extras', 'ipam', 'tenancy', 'users', 'virtualization', 'vpn', 'wireless')
|
APPS = ('circuits', 'core', 'dcim', 'extras', 'ipam', 'tenancy', 'users', 'virtualization', 'vpn', 'wireless')
|
||||||
|
|
||||||
@ -60,7 +60,7 @@ class Command(BaseCommand):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
# Additional objects to include
|
# Additional objects to include
|
||||||
namespace['ContentType'] = ContentType
|
namespace['ObjectType'] = ObjectType
|
||||||
namespace['User'] = get_user_model()
|
namespace['User'] = get_user_model()
|
||||||
|
|
||||||
# Load convenience commands
|
# Load convenience commands
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
# Generated by Django 4.2.6 on 2023-10-31 19:38
|
|
||||||
|
|
||||||
import core.models.contenttypes
|
import core.models.contenttypes
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
|
||||||
@ -13,7 +11,7 @@ class Migration(migrations.Migration):
|
|||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='ContentType',
|
name='ObjectType',
|
||||||
fields=[
|
fields=[
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
@ -23,7 +21,7 @@ class Migration(migrations.Migration):
|
|||||||
},
|
},
|
||||||
bases=('contenttypes.contenttype',),
|
bases=('contenttypes.contenttype',),
|
||||||
managers=[
|
managers=[
|
||||||
('objects', core.models.contenttypes.ContentTypeManager()),
|
('objects', core.models.contenttypes.ObjectTypeManager()),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -44,7 +44,7 @@ class ConfigRevision(models.Model):
|
|||||||
return gettext('Config revision #{id}').format(id=self.pk)
|
return gettext('Config revision #{id}').format(id=self.pk)
|
||||||
|
|
||||||
def __getattr__(self, item):
|
def __getattr__(self, item):
|
||||||
if item in self.data:
|
if self.data and item in self.data:
|
||||||
return self.data[item]
|
return self.data[item]
|
||||||
return super().__getattribute__(item)
|
return super().__getattribute__(item)
|
||||||
|
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType as ContentType_, ContentTypeManager as ContentTypeManager_
|
from django.contrib.contenttypes.models import ContentType, ContentTypeManager
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ContentType',
|
'ObjectType',
|
||||||
'ContentTypeManager',
|
'ObjectTypeManager',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ContentTypeManager(ContentTypeManager_):
|
class ObjectTypeManager(ContentTypeManager):
|
||||||
|
|
||||||
def public(self):
|
def public(self):
|
||||||
"""
|
"""
|
||||||
@ -40,11 +40,11 @@ class ContentTypeManager(ContentTypeManager_):
|
|||||||
return self.get_queryset().filter(q)
|
return self.get_queryset().filter(q)
|
||||||
|
|
||||||
|
|
||||||
class ContentType(ContentType_):
|
class ObjectType(ContentType):
|
||||||
"""
|
"""
|
||||||
Wrap Django's native ContentType model to use our custom manager.
|
Wrap Django's native ContentType model to use our custom manager.
|
||||||
"""
|
"""
|
||||||
objects = ContentTypeManager()
|
objects = ObjectTypeManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
proxy = True
|
proxy = True
|
||||||
|
@ -177,7 +177,7 @@ class DataSource(JobsMixin, PrimaryModel):
|
|||||||
Create/update/delete child DataFiles as necessary to synchronize with the remote source.
|
Create/update/delete child DataFiles as necessary to synchronize with the remote source.
|
||||||
"""
|
"""
|
||||||
if self.status == DataSourceStatusChoices.SYNCING:
|
if self.status == DataSourceStatusChoices.SYNCING:
|
||||||
raise SyncError("Cannot initiate sync; syncing already in progress.")
|
raise SyncError(_("Cannot initiate sync; syncing already in progress."))
|
||||||
|
|
||||||
# Emit the pre_sync signal
|
# Emit the pre_sync signal
|
||||||
pre_sync.send(sender=self.__class__, instance=self)
|
pre_sync.send(sender=self.__class__, instance=self)
|
||||||
@ -190,7 +190,7 @@ class DataSource(JobsMixin, PrimaryModel):
|
|||||||
backend = self.get_backend()
|
backend = self.get_backend()
|
||||||
except ModuleNotFoundError as e:
|
except ModuleNotFoundError as e:
|
||||||
raise SyncError(
|
raise SyncError(
|
||||||
f"There was an error initializing the backend. A dependency needs to be installed: {e}"
|
_("There was an error initializing the backend. A dependency needs to be installed: ") + str(e)
|
||||||
)
|
)
|
||||||
with backend.fetch() as local_path:
|
with backend.fetch() as local_path:
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ from django.utils import timezone
|
|||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from core.choices import JobStatusChoices
|
from core.choices import JobStatusChoices
|
||||||
from core.models import ContentType
|
from core.models import ObjectType
|
||||||
from core.signals import job_end, job_start
|
from core.signals import job_end, job_start
|
||||||
from extras.constants import EVENT_JOB_END, EVENT_JOB_START
|
from extras.constants import EVENT_JOB_END, EVENT_JOB_START
|
||||||
from netbox.config import get_config
|
from netbox.config import get_config
|
||||||
@ -130,7 +130,7 @@ class Job(models.Model):
|
|||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
# Validate the assigned object type
|
# Validate the assigned object type
|
||||||
if self.object_type not in ContentType.objects.with_feature('jobs'):
|
if self.object_type not in ObjectType.objects.with_feature('jobs'):
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_("Jobs cannot be assigned to this object type ({type}).").format(type=self.object_type)
|
_("Jobs cannot be assigned to this object type ({type}).").format(type=self.object_type)
|
||||||
)
|
)
|
||||||
@ -181,7 +181,11 @@ class Job(models.Model):
|
|||||||
"""
|
"""
|
||||||
valid_statuses = JobStatusChoices.TERMINAL_STATE_CHOICES
|
valid_statuses = JobStatusChoices.TERMINAL_STATE_CHOICES
|
||||||
if status not in valid_statuses:
|
if status not in valid_statuses:
|
||||||
raise ValueError(f"Invalid status for job termination. Choices are: {', '.join(valid_statuses)}")
|
raise ValueError(
|
||||||
|
_("Invalid status for job termination. Choices are: {choices}").format(
|
||||||
|
choices=', '.join(valid_statuses)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Mark the job as completed
|
# Mark the job as completed
|
||||||
self.status = status
|
self.status = status
|
||||||
@ -206,7 +210,7 @@ class Job(models.Model):
|
|||||||
schedule_at: Schedule the job to be executed at the passed date and time
|
schedule_at: Schedule the job to be executed at the passed date and time
|
||||||
interval: Recurrence interval (in minutes)
|
interval: Recurrence interval (in minutes)
|
||||||
"""
|
"""
|
||||||
object_type = ContentType.objects.get_for_model(instance, for_concrete_model=False)
|
object_type = ObjectType.objects.get_for_model(instance, for_concrete_model=False)
|
||||||
rq_queue_name = get_queue_for_model(object_type.model)
|
rq_queue_name = get_queue_for_model(object_type.model)
|
||||||
queue = django_rq.get_queue(rq_queue_name)
|
queue = django_rq.get_queue(rq_queue_name)
|
||||||
status = JobStatusChoices.STATUS_SCHEDULED if schedule_at else JobStatusChoices.STATUS_PENDING
|
status = JobStatusChoices.STATUS_SCHEDULED if schedule_at else JobStatusChoices.STATUS_PENDING
|
||||||
|
@ -16,7 +16,7 @@ class AppTest(APITestCase):
|
|||||||
|
|
||||||
class DataSourceTest(APIViewTestCases.APIViewTestCase):
|
class DataSourceTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = DataSource
|
model = DataSource
|
||||||
brief_fields = ['display', 'id', 'name', 'url']
|
brief_fields = ['description', 'display', 'id', 'name', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'enabled': False,
|
'enabled': False,
|
||||||
'description': 'foo bar baz',
|
'description': 'foo bar baz',
|
||||||
|
@ -184,7 +184,7 @@ class ConfigView(generic.ObjectView):
|
|||||||
except ConfigRevision.DoesNotExist:
|
except ConfigRevision.DoesNotExist:
|
||||||
# Fall back to using the active config data if no record is found
|
# Fall back to using the active config data if no record is found
|
||||||
return ConfigRevision(
|
return ConfigRevision(
|
||||||
data=get_config()
|
data=get_config().defaults
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,11 +2,10 @@ from drf_spectacular.utils import extend_schema_serializer
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from dcim import models
|
from dcim import models
|
||||||
from netbox.api.serializers import BaseModelSerializer, WritableNestedSerializer
|
from netbox.api.fields import RelatedObjectCountField
|
||||||
|
from netbox.api.serializers import WritableNestedSerializer
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'ComponentNestedModuleSerializer',
|
|
||||||
'ModuleBayNestedModuleSerializer',
|
|
||||||
'NestedCableSerializer',
|
'NestedCableSerializer',
|
||||||
'NestedConsolePortSerializer',
|
'NestedConsolePortSerializer',
|
||||||
'NestedConsolePortTemplateSerializer',
|
'NestedConsolePortTemplateSerializer',
|
||||||
@ -110,7 +109,7 @@ class NestedLocationSerializer(WritableNestedSerializer):
|
|||||||
)
|
)
|
||||||
class NestedRackRoleSerializer(WritableNestedSerializer):
|
class NestedRackRoleSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
|
||||||
rack_count = serializers.IntegerField(read_only=True)
|
rack_count = RelatedObjectCountField('racks')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.RackRole
|
model = models.RackRole
|
||||||
@ -122,7 +121,7 @@ class NestedRackRoleSerializer(WritableNestedSerializer):
|
|||||||
)
|
)
|
||||||
class NestedRackSerializer(WritableNestedSerializer):
|
class NestedRackSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
|
||||||
device_count = serializers.IntegerField(read_only=True)
|
device_count = RelatedObjectCountField('devices')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Rack
|
model = models.Rack
|
||||||
@ -150,7 +149,7 @@ class NestedRackReservationSerializer(WritableNestedSerializer):
|
|||||||
)
|
)
|
||||||
class NestedManufacturerSerializer(WritableNestedSerializer):
|
class NestedManufacturerSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
|
||||||
devicetype_count = serializers.IntegerField(read_only=True)
|
devicetype_count = RelatedObjectCountField('device_types')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Manufacturer
|
model = models.Manufacturer
|
||||||
@ -163,7 +162,7 @@ class NestedManufacturerSerializer(WritableNestedSerializer):
|
|||||||
class NestedDeviceTypeSerializer(WritableNestedSerializer):
|
class NestedDeviceTypeSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
|
||||||
manufacturer = NestedManufacturerSerializer(read_only=True)
|
manufacturer = NestedManufacturerSerializer(read_only=True)
|
||||||
device_count = serializers.IntegerField(read_only=True)
|
device_count = RelatedObjectCountField('instances')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.DeviceType
|
model = models.DeviceType
|
||||||
@ -173,7 +172,6 @@ class NestedDeviceTypeSerializer(WritableNestedSerializer):
|
|||||||
class NestedModuleTypeSerializer(WritableNestedSerializer):
|
class NestedModuleTypeSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
|
||||||
manufacturer = NestedManufacturerSerializer(read_only=True)
|
manufacturer = NestedManufacturerSerializer(read_only=True)
|
||||||
# module_count = serializers.IntegerField(read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.ModuleType
|
model = models.ModuleType
|
||||||
@ -274,8 +272,8 @@ class NestedInventoryItemTemplateSerializer(WritableNestedSerializer):
|
|||||||
)
|
)
|
||||||
class NestedDeviceRoleSerializer(WritableNestedSerializer):
|
class NestedDeviceRoleSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
|
||||||
device_count = serializers.IntegerField(read_only=True)
|
device_count = RelatedObjectCountField('devices')
|
||||||
virtualmachine_count = serializers.IntegerField(read_only=True)
|
virtualmachine_count = RelatedObjectCountField('virtual_machines')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.DeviceRole
|
model = models.DeviceRole
|
||||||
@ -287,8 +285,8 @@ class NestedDeviceRoleSerializer(WritableNestedSerializer):
|
|||||||
)
|
)
|
||||||
class NestedPlatformSerializer(WritableNestedSerializer):
|
class NestedPlatformSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
|
||||||
device_count = serializers.IntegerField(read_only=True)
|
device_count = RelatedObjectCountField('devices')
|
||||||
virtualmachine_count = serializers.IntegerField(read_only=True)
|
virtualmachine_count = RelatedObjectCountField('virtual_machines')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Platform
|
model = models.Platform
|
||||||
@ -311,26 +309,6 @@ class ModuleNestedModuleBaySerializer(WritableNestedSerializer):
|
|||||||
fields = ['id', 'url', 'display', 'name']
|
fields = ['id', 'url', 'display', 'name']
|
||||||
|
|
||||||
|
|
||||||
class ModuleBayNestedModuleSerializer(WritableNestedSerializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = models.Module
|
|
||||||
fields = ['id', 'url', 'display', 'serial']
|
|
||||||
|
|
||||||
|
|
||||||
class ComponentNestedModuleSerializer(WritableNestedSerializer):
|
|
||||||
"""
|
|
||||||
Used by device component serializers.
|
|
||||||
"""
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
|
|
||||||
module_bay = ModuleNestedModuleBaySerializer(read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = models.Module
|
|
||||||
fields = ['id', 'url', 'display', 'device', 'module_bay']
|
|
||||||
|
|
||||||
|
|
||||||
class NestedModuleSerializer(WritableNestedSerializer):
|
class NestedModuleSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
|
||||||
device = NestedDeviceSerializer(read_only=True)
|
device = NestedDeviceSerializer(read_only=True)
|
||||||
@ -445,7 +423,7 @@ class NestedInventoryItemSerializer(WritableNestedSerializer):
|
|||||||
)
|
)
|
||||||
class NestedInventoryItemRoleSerializer(WritableNestedSerializer):
|
class NestedInventoryItemRoleSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail')
|
||||||
inventoryitem_count = serializers.IntegerField(read_only=True)
|
inventoryitem_count = RelatedObjectCountField('inventory_items')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.InventoryItemRole
|
model = models.InventoryItemRole
|
||||||
@ -490,7 +468,7 @@ class NestedVirtualChassisSerializer(WritableNestedSerializer):
|
|||||||
)
|
)
|
||||||
class NestedPowerPanelSerializer(WritableNestedSerializer):
|
class NestedPowerPanelSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
|
||||||
powerfeed_count = serializers.IntegerField(read_only=True)
|
powerfeed_count = RelatedObjectCountField('powerfeeds')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.PowerPanel
|
model = models.PowerPanel
|
||||||
|
File diff suppressed because it is too large
Load Diff
0
netbox/dcim/api/serializers_/__init__.py
Normal file
0
netbox/dcim/api/serializers_/__init__.py
Normal file
37
netbox/dcim/api/serializers_/base.py
Normal file
37
netbox/dcim/api/serializers_/base.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
from drf_spectacular.types import OpenApiTypes
|
||||||
|
from drf_spectacular.utils import extend_schema_field
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from utilities.api import get_serializer_for_model
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'ConnectedEndpointsSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectedEndpointsSerializer(serializers.ModelSerializer):
|
||||||
|
"""
|
||||||
|
Legacy serializer for pre-v3.3 connections
|
||||||
|
"""
|
||||||
|
connected_endpoints_type = serializers.SerializerMethodField(read_only=True)
|
||||||
|
connected_endpoints = serializers.SerializerMethodField(read_only=True)
|
||||||
|
connected_endpoints_reachable = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
|
@extend_schema_field(OpenApiTypes.STR)
|
||||||
|
def get_connected_endpoints_type(self, obj):
|
||||||
|
if endpoints := obj.connected_endpoints:
|
||||||
|
return f'{endpoints[0]._meta.app_label}.{endpoints[0]._meta.model_name}'
|
||||||
|
|
||||||
|
@extend_schema_field(serializers.ListField)
|
||||||
|
def get_connected_endpoints(self, obj):
|
||||||
|
"""
|
||||||
|
Return the appropriate serializer for the type of connected object.
|
||||||
|
"""
|
||||||
|
if endpoints := obj.connected_endpoints:
|
||||||
|
serializer = get_serializer_for_model(endpoints[0])
|
||||||
|
context = {'request': self.context['request']}
|
||||||
|
return serializer(endpoints, nested=True, many=True, context=context).data
|
||||||
|
|
||||||
|
@extend_schema_field(serializers.BooleanField)
|
||||||
|
def get_connected_endpoints_reachable(self, obj):
|
||||||
|
return obj._path and obj._path.is_complete and obj._path.is_active
|
126
netbox/dcim/api/serializers_/cables.py
Normal file
126
netbox/dcim/api/serializers_/cables.py
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from drf_spectacular.types import OpenApiTypes
|
||||||
|
from drf_spectacular.utils import extend_schema_field
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from dcim.choices import *
|
||||||
|
from dcim.constants import *
|
||||||
|
from dcim.models import Cable, CablePath, CableTermination
|
||||||
|
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||||
|
from netbox.api.serializers import GenericObjectSerializer, NetBoxModelSerializer
|
||||||
|
from tenancy.api.serializers_.tenants import TenantSerializer
|
||||||
|
from utilities.api import get_serializer_for_model
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'CablePathSerializer',
|
||||||
|
'CableSerializer',
|
||||||
|
'CableTerminationSerializer',
|
||||||
|
'CabledObjectSerializer',
|
||||||
|
'TracedCableSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CableSerializer(NetBoxModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
|
||||||
|
a_terminations = GenericObjectSerializer(many=True, required=False)
|
||||||
|
b_terminations = GenericObjectSerializer(many=True, required=False)
|
||||||
|
status = ChoiceField(choices=LinkStatusChoices, required=False)
|
||||||
|
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||||
|
length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False, allow_null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Cable
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'type', 'a_terminations', 'b_terminations', 'status', 'tenant', 'label', 'color',
|
||||||
|
'length', 'length_unit', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'label', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
class TracedCableSerializer(serializers.ModelSerializer):
|
||||||
|
"""
|
||||||
|
Used only while tracing a cable path.
|
||||||
|
"""
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Cable
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'description',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class CableTerminationSerializer(NetBoxModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cabletermination-detail')
|
||||||
|
termination_type = ContentTypeField(
|
||||||
|
queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS)
|
||||||
|
)
|
||||||
|
termination = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CableTermination
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'cable', 'cable_end', 'termination_type', 'termination_id', 'termination',
|
||||||
|
'created', 'last_updated',
|
||||||
|
]
|
||||||
|
|
||||||
|
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||||
|
def get_termination(self, obj):
|
||||||
|
serializer = get_serializer_for_model(obj.termination)
|
||||||
|
context = {'request': self.context['request']}
|
||||||
|
return serializer(obj.termination, nested=True, context=context).data
|
||||||
|
|
||||||
|
|
||||||
|
class CablePathSerializer(serializers.ModelSerializer):
|
||||||
|
path = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CablePath
|
||||||
|
fields = ['id', 'path', 'is_active', 'is_complete', 'is_split']
|
||||||
|
|
||||||
|
@extend_schema_field(serializers.ListField)
|
||||||
|
def get_path(self, obj):
|
||||||
|
ret = []
|
||||||
|
for nodes in obj.path_objects:
|
||||||
|
serializer = get_serializer_for_model(nodes[0])
|
||||||
|
context = {'request': self.context['request']}
|
||||||
|
ret.append(serializer(nodes, nested=True, many=True, context=context).data)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
class CabledObjectSerializer(serializers.ModelSerializer):
|
||||||
|
cable = CableSerializer(nested=True, read_only=True, allow_null=True)
|
||||||
|
cable_end = serializers.CharField(read_only=True)
|
||||||
|
link_peers_type = serializers.SerializerMethodField(read_only=True)
|
||||||
|
link_peers = serializers.SerializerMethodField(read_only=True)
|
||||||
|
_occupied = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
|
@extend_schema_field(OpenApiTypes.STR)
|
||||||
|
def get_link_peers_type(self, obj):
|
||||||
|
"""
|
||||||
|
Return the type of the peer link terminations, or None.
|
||||||
|
"""
|
||||||
|
if not obj.cable:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if obj.link_peers:
|
||||||
|
return f'{obj.link_peers[0]._meta.app_label}.{obj.link_peers[0]._meta.model_name}'
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@extend_schema_field(serializers.ListField)
|
||||||
|
def get_link_peers(self, obj):
|
||||||
|
"""
|
||||||
|
Return the appropriate serializer for the link termination model.
|
||||||
|
"""
|
||||||
|
if not obj.link_peers:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Return serialized peer termination objects
|
||||||
|
serializer = get_serializer_for_model(obj.link_peers[0])
|
||||||
|
context = {'request': self.context['request']}
|
||||||
|
return serializer(obj.link_peers, nested=True, many=True, context=context).data
|
||||||
|
|
||||||
|
@extend_schema_field(serializers.BooleanField)
|
||||||
|
def get__occupied(self, obj):
|
||||||
|
return obj._occupied
|
368
netbox/dcim/api/serializers_/device_components.py
Normal file
368
netbox/dcim/api/serializers_/device_components.py
Normal file
@ -0,0 +1,368 @@
|
|||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from drf_spectacular.utils import extend_schema_field
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from dcim.choices import *
|
||||||
|
from dcim.constants import *
|
||||||
|
from dcim.models import (
|
||||||
|
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, ModuleBay, PowerOutlet, PowerPort,
|
||||||
|
RearPort, VirtualDeviceContext,
|
||||||
|
)
|
||||||
|
from ipam.api.serializers_.vlans import VLANSerializer
|
||||||
|
from ipam.api.serializers_.vrfs import VRFSerializer
|
||||||
|
from ipam.models import VLAN
|
||||||
|
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
||||||
|
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
|
||||||
|
from utilities.api import get_serializer_for_model
|
||||||
|
from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer
|
||||||
|
from wireless.api.nested_serializers import NestedWirelessLinkSerializer
|
||||||
|
from wireless.api.serializers_.wirelesslans import WirelessLANSerializer
|
||||||
|
from wireless.choices import *
|
||||||
|
from wireless.models import WirelessLAN
|
||||||
|
from .base import ConnectedEndpointsSerializer
|
||||||
|
from .cables import CabledObjectSerializer
|
||||||
|
from .devices import DeviceSerializer, ModuleSerializer, VirtualDeviceContextSerializer
|
||||||
|
from .manufacturers import ManufacturerSerializer
|
||||||
|
from .roles import InventoryItemRoleSerializer
|
||||||
|
from ..nested_serializers import *
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'ConsolePortSerializer',
|
||||||
|
'ConsoleServerPortSerializer',
|
||||||
|
'DeviceBaySerializer',
|
||||||
|
'FrontPortSerializer',
|
||||||
|
'InterfaceSerializer',
|
||||||
|
'InventoryItemSerializer',
|
||||||
|
'ModuleBaySerializer',
|
||||||
|
'PowerOutletSerializer',
|
||||||
|
'PowerPortSerializer',
|
||||||
|
'RearPortSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ConsoleServerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
|
||||||
|
device = DeviceSerializer(nested=True)
|
||||||
|
module = ModuleSerializer(
|
||||||
|
nested=True,
|
||||||
|
fields=('id', 'url', 'display', 'device', 'module_bay'),
|
||||||
|
required=False,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
type = ChoiceField(
|
||||||
|
choices=ConsolePortTypeChoices,
|
||||||
|
allow_blank=True,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
speed = ChoiceField(
|
||||||
|
choices=ConsolePortSpeedChoices,
|
||||||
|
allow_null=True,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ConsoleServerPort
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description',
|
||||||
|
'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints',
|
||||||
|
'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
|
||||||
|
'last_updated', '_occupied',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
||||||
|
|
||||||
|
|
||||||
|
class ConsolePortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
|
||||||
|
device = DeviceSerializer(nested=True)
|
||||||
|
module = ModuleSerializer(
|
||||||
|
nested=True,
|
||||||
|
fields=('id', 'url', 'display', 'device', 'module_bay'),
|
||||||
|
required=False,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
type = ChoiceField(
|
||||||
|
choices=ConsolePortTypeChoices,
|
||||||
|
allow_blank=True,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
speed = ChoiceField(
|
||||||
|
choices=ConsolePortSpeedChoices,
|
||||||
|
allow_null=True,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ConsolePort
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description',
|
||||||
|
'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints',
|
||||||
|
'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
|
||||||
|
'last_updated', '_occupied',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
||||||
|
|
||||||
|
|
||||||
|
class PowerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
|
||||||
|
device = DeviceSerializer(nested=True)
|
||||||
|
module = ModuleSerializer(
|
||||||
|
nested=True,
|
||||||
|
fields=('id', 'url', 'display', 'device', 'module_bay'),
|
||||||
|
required=False,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
type = ChoiceField(
|
||||||
|
choices=PowerPortTypeChoices,
|
||||||
|
allow_blank=True,
|
||||||
|
required=False,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PowerPort
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw',
|
||||||
|
'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
|
||||||
|
'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields',
|
||||||
|
'created', 'last_updated', '_occupied',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
||||||
|
|
||||||
|
|
||||||
|
class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
|
||||||
|
device = DeviceSerializer(nested=True)
|
||||||
|
module = ModuleSerializer(
|
||||||
|
nested=True,
|
||||||
|
fields=('id', 'url', 'display', 'device', 'module_bay'),
|
||||||
|
required=False,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
type = ChoiceField(
|
||||||
|
choices=PowerOutletTypeChoices,
|
||||||
|
allow_blank=True,
|
||||||
|
required=False,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
power_port = PowerPortSerializer(
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
feed_leg = ChoiceField(
|
||||||
|
choices=PowerOutletFeedLegChoices,
|
||||||
|
allow_blank=True,
|
||||||
|
required=False,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PowerOutlet
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg',
|
||||||
|
'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
|
||||||
|
'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields',
|
||||||
|
'created', 'last_updated', '_occupied',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
||||||
|
|
||||||
|
|
||||||
|
class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
||||||
|
device = DeviceSerializer(nested=True)
|
||||||
|
vdcs = SerializedPKRelatedField(
|
||||||
|
queryset=VirtualDeviceContext.objects.all(),
|
||||||
|
serializer=VirtualDeviceContextSerializer,
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
many=True
|
||||||
|
)
|
||||||
|
module = ModuleSerializer(
|
||||||
|
nested=True,
|
||||||
|
fields=('id', 'url', 'display', 'device', 'module_bay'),
|
||||||
|
required=False,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
type = ChoiceField(choices=InterfaceTypeChoices)
|
||||||
|
parent = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||||
|
bridge = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||||
|
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||||
|
mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_blank=True)
|
||||||
|
duplex = ChoiceField(choices=InterfaceDuplexChoices, required=False, allow_blank=True, allow_null=True)
|
||||||
|
rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_blank=True)
|
||||||
|
rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False, allow_blank=True)
|
||||||
|
poe_mode = ChoiceField(choices=InterfacePoEModeChoices, required=False, allow_blank=True)
|
||||||
|
poe_type = ChoiceField(choices=InterfacePoETypeChoices, required=False, allow_blank=True)
|
||||||
|
untagged_vlan = VLANSerializer(nested=True, required=False, allow_null=True)
|
||||||
|
tagged_vlans = SerializedPKRelatedField(
|
||||||
|
queryset=VLAN.objects.all(),
|
||||||
|
serializer=VLANSerializer,
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
many=True
|
||||||
|
)
|
||||||
|
vrf = VRFSerializer(nested=True, required=False, allow_null=True)
|
||||||
|
l2vpn_termination = L2VPNTerminationSerializer(nested=True, read_only=True, allow_null=True)
|
||||||
|
wireless_link = NestedWirelessLinkSerializer(read_only=True, allow_null=True)
|
||||||
|
wireless_lans = SerializedPKRelatedField(
|
||||||
|
queryset=WirelessLAN.objects.all(),
|
||||||
|
serializer=WirelessLANSerializer,
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
many=True
|
||||||
|
)
|
||||||
|
count_ipaddresses = serializers.IntegerField(read_only=True)
|
||||||
|
count_fhrp_groups = serializers.IntegerField(read_only=True)
|
||||||
|
mac_address = serializers.CharField(
|
||||||
|
required=False,
|
||||||
|
default=None,
|
||||||
|
allow_blank=True,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
wwn = serializers.CharField(required=False, default=None, allow_blank=True, allow_null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Interface
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'device', 'vdcs', 'module', 'name', 'label', 'type', 'enabled', 'parent', 'bridge',
|
||||||
|
'lag', 'mtu', 'mac_address', 'speed', 'duplex', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role',
|
||||||
|
'rf_channel', 'poe_mode', 'poe_type', 'rf_channel_frequency', 'rf_channel_width', 'tx_power',
|
||||||
|
'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'cable_end', 'wireless_link', 'link_peers',
|
||||||
|
'link_peers_type', 'wireless_lans', 'vrf', 'l2vpn_termination', 'connected_endpoints',
|
||||||
|
'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
|
||||||
|
'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
|
||||||
|
# Validate many-to-many VLAN assignments
|
||||||
|
if not self.nested:
|
||||||
|
device = self.instance.device if self.instance else data.get('device')
|
||||||
|
for vlan in data.get('tagged_vlans', []):
|
||||||
|
if vlan.site not in [device.site, None]:
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'tagged_vlans': f"VLAN {vlan} must belong to the same site as the interface's parent device, "
|
||||||
|
f"or it must be global."
|
||||||
|
})
|
||||||
|
|
||||||
|
return super().validate(data)
|
||||||
|
|
||||||
|
|
||||||
|
class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
|
||||||
|
device = DeviceSerializer(nested=True)
|
||||||
|
module = ModuleSerializer(
|
||||||
|
nested=True,
|
||||||
|
fields=('id', 'url', 'display', 'device', 'module_bay'),
|
||||||
|
required=False,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
type = ChoiceField(choices=PortTypeChoices)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = RearPort
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'description',
|
||||||
|
'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'tags', 'custom_fields', 'created',
|
||||||
|
'last_updated', '_occupied',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
||||||
|
|
||||||
|
|
||||||
|
class FrontPortRearPortSerializer(WritableNestedSerializer):
|
||||||
|
"""
|
||||||
|
NestedRearPortSerializer but with parent device omitted (since front and rear ports must belong to same device)
|
||||||
|
"""
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = RearPort
|
||||||
|
fields = ['id', 'url', 'display', 'name', 'label', 'description']
|
||||||
|
|
||||||
|
|
||||||
|
class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
|
||||||
|
device = DeviceSerializer(nested=True)
|
||||||
|
module = ModuleSerializer(
|
||||||
|
nested=True,
|
||||||
|
fields=('id', 'url', 'display', 'device', 'module_bay'),
|
||||||
|
required=False,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
type = ChoiceField(choices=PortTypeChoices)
|
||||||
|
rear_port = FrontPortRearPortSerializer()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = FrontPort
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port',
|
||||||
|
'rear_port_position', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers',
|
||||||
|
'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleBaySerializer(NetBoxModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
|
||||||
|
device = DeviceSerializer(nested=True)
|
||||||
|
installed_module = ModuleSerializer(
|
||||||
|
nested=True,
|
||||||
|
fields=('id', 'url', 'display', 'serial', 'description'),
|
||||||
|
required=False,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ModuleBay
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'device', 'name', 'installed_module', 'label', 'position', 'description', 'tags',
|
||||||
|
'custom_fields', 'created', 'last_updated',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'installed_module', 'name', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceBaySerializer(NetBoxModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
|
||||||
|
device = DeviceSerializer(nested=True)
|
||||||
|
installed_device = DeviceSerializer(nested=True, required=False, allow_null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = DeviceBay
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'device', 'name', 'label', 'description', 'installed_device', 'tags',
|
||||||
|
'custom_fields', 'created', 'last_updated',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryItemSerializer(NetBoxModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
|
||||||
|
device = DeviceSerializer(nested=True)
|
||||||
|
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
|
||||||
|
role = InventoryItemRoleSerializer(nested=True, required=False, allow_null=True)
|
||||||
|
manufacturer = ManufacturerSerializer(nested=True, required=False, allow_null=True, default=None)
|
||||||
|
component_type = ContentTypeField(
|
||||||
|
queryset=ContentType.objects.filter(MODULAR_COMPONENT_MODELS),
|
||||||
|
required=False,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
component = serializers.SerializerMethodField(read_only=True)
|
||||||
|
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = InventoryItem
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial',
|
||||||
|
'asset_tag', 'discovered', 'description', 'component_type', 'component_id', 'component', 'tags',
|
||||||
|
'custom_fields', 'created', 'last_updated', '_depth',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', '_depth')
|
||||||
|
|
||||||
|
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||||
|
def get_component(self, obj):
|
||||||
|
if obj.component is None:
|
||||||
|
return None
|
||||||
|
serializer = get_serializer_for_model(obj.component)
|
||||||
|
context = {'request': self.context['request']}
|
||||||
|
return serializer(obj.component, nested=True, context=context).data
|
157
netbox/dcim/api/serializers_/devices.py
Normal file
157
netbox/dcim/api/serializers_/devices.py
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import decimal
|
||||||
|
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
from drf_spectacular.utils import extend_schema_field
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from dcim.choices import *
|
||||||
|
from dcim.models import Device, DeviceBay, Module, VirtualDeviceContext
|
||||||
|
from extras.api.serializers_.configtemplates import ConfigTemplateSerializer
|
||||||
|
from ipam.api.serializers_.ip import IPAddressSerializer
|
||||||
|
from netbox.api.fields import ChoiceField, RelatedObjectCountField
|
||||||
|
from netbox.api.serializers import NetBoxModelSerializer
|
||||||
|
from tenancy.api.serializers_.tenants import TenantSerializer
|
||||||
|
from virtualization.api.serializers_.clusters import ClusterSerializer
|
||||||
|
from .devicetypes import *
|
||||||
|
from .platforms import PlatformSerializer
|
||||||
|
from .racks import RackSerializer
|
||||||
|
from .roles import DeviceRoleSerializer
|
||||||
|
from .sites import LocationSerializer, SiteSerializer
|
||||||
|
from .virtualchassis import VirtualChassisSerializer
|
||||||
|
from ..nested_serializers import *
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'DeviceSerializer',
|
||||||
|
'DeviceWithConfigContextSerializer',
|
||||||
|
'ModuleSerializer',
|
||||||
|
'VirtualDeviceContextSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceSerializer(NetBoxModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
|
||||||
|
device_type = DeviceTypeSerializer(nested=True)
|
||||||
|
role = DeviceRoleSerializer(nested=True)
|
||||||
|
tenant = TenantSerializer(
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
platform = PlatformSerializer(nested=True, required=False, allow_null=True)
|
||||||
|
site = SiteSerializer(nested=True)
|
||||||
|
location = LocationSerializer(nested=True, required=False, allow_null=True, default=None)
|
||||||
|
rack = RackSerializer(nested=True, required=False, allow_null=True, default=None)
|
||||||
|
face = ChoiceField(choices=DeviceFaceChoices, allow_blank=True, default=lambda: '')
|
||||||
|
position = serializers.DecimalField(
|
||||||
|
max_digits=4,
|
||||||
|
decimal_places=1,
|
||||||
|
allow_null=True,
|
||||||
|
label=_('Position (U)'),
|
||||||
|
min_value=decimal.Decimal(0.5),
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
status = ChoiceField(choices=DeviceStatusChoices, required=False)
|
||||||
|
airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False)
|
||||||
|
primary_ip = IPAddressSerializer(nested=True, read_only=True)
|
||||||
|
primary_ip4 = IPAddressSerializer(nested=True, required=False, allow_null=True)
|
||||||
|
primary_ip6 = IPAddressSerializer(nested=True, required=False, allow_null=True)
|
||||||
|
oob_ip = IPAddressSerializer(nested=True, required=False, allow_null=True)
|
||||||
|
parent_device = serializers.SerializerMethodField()
|
||||||
|
cluster = ClusterSerializer(nested=True, required=False, allow_null=True)
|
||||||
|
virtual_chassis = VirtualChassisSerializer(nested=True, required=False, allow_null=True, default=None)
|
||||||
|
vc_position = serializers.IntegerField(allow_null=True, max_value=255, min_value=0, default=None)
|
||||||
|
config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None)
|
||||||
|
|
||||||
|
# Counter fields
|
||||||
|
console_port_count = serializers.IntegerField(read_only=True)
|
||||||
|
console_server_port_count = serializers.IntegerField(read_only=True)
|
||||||
|
power_port_count = serializers.IntegerField(read_only=True)
|
||||||
|
power_outlet_count = serializers.IntegerField(read_only=True)
|
||||||
|
interface_count = serializers.IntegerField(read_only=True)
|
||||||
|
front_port_count = serializers.IntegerField(read_only=True)
|
||||||
|
rear_port_count = serializers.IntegerField(read_only=True)
|
||||||
|
device_bay_count = serializers.IntegerField(read_only=True)
|
||||||
|
module_bay_count = serializers.IntegerField(read_only=True)
|
||||||
|
inventory_item_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Device
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'name', 'device_type', 'role', 'tenant', 'platform', 'serial', 'asset_tag', 'site',
|
||||||
|
'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', 'status', 'airflow',
|
||||||
|
'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', 'vc_position',
|
||||||
|
'vc_priority', 'description', 'comments', 'config_template', 'local_context_data', 'tags', 'custom_fields',
|
||||||
|
'created', 'last_updated', 'console_port_count', 'console_server_port_count', 'power_port_count',
|
||||||
|
'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count', 'device_bay_count',
|
||||||
|
'module_bay_count', 'inventory_item_count',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||||
|
|
||||||
|
@extend_schema_field(NestedDeviceSerializer)
|
||||||
|
def get_parent_device(self, obj):
|
||||||
|
try:
|
||||||
|
device_bay = obj.parent_bay
|
||||||
|
except DeviceBay.DoesNotExist:
|
||||||
|
return None
|
||||||
|
context = {'request': self.context['request']}
|
||||||
|
data = NestedDeviceSerializer(instance=device_bay.device, context=context).data
|
||||||
|
data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).data
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceWithConfigContextSerializer(DeviceSerializer):
|
||||||
|
config_context = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
|
class Meta(DeviceSerializer.Meta):
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'name', 'device_type', 'role', 'tenant', 'platform', 'serial', 'asset_tag', 'site',
|
||||||
|
'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', 'status', 'airflow',
|
||||||
|
'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', 'vc_position',
|
||||||
|
'vc_priority', 'description', 'comments', 'config_template', 'config_context', 'local_context_data', 'tags',
|
||||||
|
'custom_fields', 'created', 'last_updated', 'console_port_count', 'console_server_port_count',
|
||||||
|
'power_port_count', 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count',
|
||||||
|
'device_bay_count', 'module_bay_count', 'inventory_item_count',
|
||||||
|
]
|
||||||
|
|
||||||
|
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||||
|
def get_config_context(self, obj):
|
||||||
|
return obj.get_config_context()
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualDeviceContextSerializer(NetBoxModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualdevicecontext-detail')
|
||||||
|
device = DeviceSerializer(nested=True)
|
||||||
|
tenant = TenantSerializer(nested=True, required=False, allow_null=True, default=None)
|
||||||
|
primary_ip = IPAddressSerializer(nested=True, read_only=True, allow_null=True)
|
||||||
|
primary_ip4 = IPAddressSerializer(nested=True, required=False, allow_null=True)
|
||||||
|
primary_ip6 = IPAddressSerializer(nested=True, required=False, allow_null=True)
|
||||||
|
status = ChoiceField(choices=VirtualDeviceContextStatusChoices)
|
||||||
|
|
||||||
|
# Related object counts
|
||||||
|
interface_count = RelatedObjectCountField('interfaces')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VirtualDeviceContext
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'name', 'device', 'identifier', 'tenant', 'primary_ip', 'primary_ip4',
|
||||||
|
'primary_ip6', 'status', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
|
'interface_count',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'identifier', 'device', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleSerializer(NetBoxModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
|
||||||
|
device = DeviceSerializer(nested=True)
|
||||||
|
module_bay = NestedModuleBaySerializer()
|
||||||
|
module_type = ModuleTypeSerializer(nested=True)
|
||||||
|
status = ChoiceField(choices=ModuleStatusChoices, required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Module
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'device', 'module_bay', 'module_type', 'status', 'serial', 'asset_tag',
|
||||||
|
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'device', 'module_bay', 'module_type', 'description')
|
327
netbox/dcim/api/serializers_/devicetype_components.py
Normal file
327
netbox/dcim/api/serializers_/devicetype_components.py
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from drf_spectacular.utils import extend_schema_field
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from dcim.choices import *
|
||||||
|
from dcim.constants import *
|
||||||
|
from dcim.models import (
|
||||||
|
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate, InterfaceTemplate,
|
||||||
|
InventoryItemTemplate, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
|
||||||
|
)
|
||||||
|
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||||
|
from netbox.api.serializers import ValidatedModelSerializer
|
||||||
|
from utilities.api import get_serializer_for_model
|
||||||
|
from wireless.choices import *
|
||||||
|
from .devicetypes import DeviceTypeSerializer, ModuleTypeSerializer
|
||||||
|
from .manufacturers import ManufacturerSerializer
|
||||||
|
from .roles import InventoryItemRoleSerializer
|
||||||
|
from ..nested_serializers import *
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'ConsolePortTemplateSerializer',
|
||||||
|
'ConsoleServerPortTemplateSerializer',
|
||||||
|
'DeviceBayTemplateSerializer',
|
||||||
|
'FrontPortTemplateSerializer',
|
||||||
|
'InterfaceTemplateSerializer',
|
||||||
|
'InventoryItemTemplateSerializer',
|
||||||
|
'ModuleBayTemplateSerializer',
|
||||||
|
'PowerOutletTemplateSerializer',
|
||||||
|
'PowerPortTemplateSerializer',
|
||||||
|
'RearPortTemplateSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ConsolePortTemplateSerializer(ValidatedModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleporttemplate-detail')
|
||||||
|
device_type = DeviceTypeSerializer(
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
module_type = ModuleTypeSerializer(
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
type = ChoiceField(
|
||||||
|
choices=ConsolePortTypeChoices,
|
||||||
|
allow_blank=True,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ConsolePortTemplate
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'description', 'created',
|
||||||
|
'last_updated',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverporttemplate-detail')
|
||||||
|
device_type = DeviceTypeSerializer(
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
module_type = ModuleTypeSerializer(
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
type = ChoiceField(
|
||||||
|
choices=ConsolePortTypeChoices,
|
||||||
|
allow_blank=True,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ConsoleServerPortTemplate
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'description', 'created',
|
||||||
|
'last_updated',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
class PowerPortTemplateSerializer(ValidatedModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerporttemplate-detail')
|
||||||
|
device_type = DeviceTypeSerializer(
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
module_type = ModuleTypeSerializer(
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
type = ChoiceField(
|
||||||
|
choices=PowerPortTypeChoices,
|
||||||
|
allow_blank=True,
|
||||||
|
required=False,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PowerPortTemplate
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw',
|
||||||
|
'allocated_draw', 'description', 'created', 'last_updated',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlettemplate-detail')
|
||||||
|
device_type = DeviceTypeSerializer(
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
module_type = ModuleTypeSerializer(
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
type = ChoiceField(
|
||||||
|
choices=PowerOutletTypeChoices,
|
||||||
|
allow_blank=True,
|
||||||
|
required=False,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
power_port = PowerPortTemplateSerializer(
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
feed_leg = ChoiceField(
|
||||||
|
choices=PowerOutletFeedLegChoices,
|
||||||
|
allow_blank=True,
|
||||||
|
required=False,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PowerOutletTemplate
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg',
|
||||||
|
'description', 'created', 'last_updated',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
class InterfaceTemplateSerializer(ValidatedModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfacetemplate-detail')
|
||||||
|
device_type = DeviceTypeSerializer(
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
module_type = ModuleTypeSerializer(
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
type = ChoiceField(choices=InterfaceTypeChoices)
|
||||||
|
bridge = NestedInterfaceTemplateSerializer(
|
||||||
|
required=False,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
poe_mode = ChoiceField(
|
||||||
|
choices=InterfacePoEModeChoices,
|
||||||
|
required=False,
|
||||||
|
allow_blank=True,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
poe_type = ChoiceField(
|
||||||
|
choices=InterfacePoETypeChoices,
|
||||||
|
required=False,
|
||||||
|
allow_blank=True,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
rf_role = ChoiceField(
|
||||||
|
choices=WirelessRoleChoices,
|
||||||
|
required=False,
|
||||||
|
allow_blank=True,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = InterfaceTemplate
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only',
|
||||||
|
'description', 'bridge', 'poe_mode', 'poe_type', 'rf_role', 'created', 'last_updated',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
class RearPortTemplateSerializer(ValidatedModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearporttemplate-detail')
|
||||||
|
device_type = DeviceTypeSerializer(
|
||||||
|
required=False,
|
||||||
|
nested=True,
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
module_type = ModuleTypeSerializer(
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
type = ChoiceField(choices=PortTypeChoices)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = RearPortTemplate
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions',
|
||||||
|
'description', 'created', 'last_updated',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
class FrontPortTemplateSerializer(ValidatedModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontporttemplate-detail')
|
||||||
|
device_type = DeviceTypeSerializer(
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
module_type = ModuleTypeSerializer(
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
type = ChoiceField(choices=PortTypeChoices)
|
||||||
|
rear_port = RearPortTemplateSerializer(nested=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = FrontPortTemplate
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port',
|
||||||
|
'rear_port_position', 'description', 'created', 'last_updated',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleBayTemplateSerializer(ValidatedModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebaytemplate-detail')
|
||||||
|
device_type = DeviceTypeSerializer(
|
||||||
|
nested=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ModuleBayTemplate
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'device_type', 'name', 'label', 'position', 'description', 'created',
|
||||||
|
'last_updated',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceBayTemplateSerializer(ValidatedModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebaytemplate-detail')
|
||||||
|
device_type = DeviceTypeSerializer(
|
||||||
|
nested=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = DeviceBayTemplate
|
||||||
|
fields = ['id', 'url', 'display', 'device_type', 'name', 'label', 'description', 'created', 'last_updated']
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryItemTemplateSerializer(ValidatedModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemtemplate-detail')
|
||||||
|
device_type = DeviceTypeSerializer(
|
||||||
|
nested=True
|
||||||
|
)
|
||||||
|
parent = serializers.PrimaryKeyRelatedField(
|
||||||
|
queryset=InventoryItemTemplate.objects.all(),
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
role = InventoryItemRoleSerializer(nested=True, required=False, allow_null=True)
|
||||||
|
manufacturer = ManufacturerSerializer(
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
component_type = ContentTypeField(
|
||||||
|
queryset=ContentType.objects.filter(MODULAR_COMPONENT_TEMPLATE_MODELS),
|
||||||
|
required=False,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
component = serializers.SerializerMethodField(read_only=True)
|
||||||
|
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = InventoryItemTemplate
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id',
|
||||||
|
'description', 'component_type', 'component_id', 'component', 'created', 'last_updated', '_depth',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'description', '_depth')
|
||||||
|
|
||||||
|
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||||
|
def get_component(self, obj):
|
||||||
|
if obj.component is None:
|
||||||
|
return None
|
||||||
|
serializer = get_serializer_for_model(obj.component)
|
||||||
|
context = {'request': self.context['request']}
|
||||||
|
return serializer(obj.component, nested=True, context=context).data
|
74
netbox/dcim/api/serializers_/devicetypes.py
Normal file
74
netbox/dcim/api/serializers_/devicetypes.py
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
from django.utils.translation import gettext as _
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from dcim.choices import *
|
||||||
|
from dcim.models import DeviceType, ModuleType
|
||||||
|
from netbox.api.fields import ChoiceField, RelatedObjectCountField
|
||||||
|
from netbox.api.serializers import NetBoxModelSerializer
|
||||||
|
from .manufacturers import ManufacturerSerializer
|
||||||
|
from .platforms import PlatformSerializer
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'DeviceTypeSerializer',
|
||||||
|
'ModuleTypeSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceTypeSerializer(NetBoxModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
|
||||||
|
manufacturer = ManufacturerSerializer(nested=True)
|
||||||
|
default_platform = PlatformSerializer(nested=True, required=False, allow_null=True)
|
||||||
|
u_height = serializers.DecimalField(
|
||||||
|
max_digits=4,
|
||||||
|
decimal_places=1,
|
||||||
|
label=_('Position (U)'),
|
||||||
|
min_value=0,
|
||||||
|
default=1.0
|
||||||
|
)
|
||||||
|
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False, allow_null=True)
|
||||||
|
airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False, allow_null=True)
|
||||||
|
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
|
||||||
|
front_image = serializers.URLField(allow_null=True, required=False)
|
||||||
|
rear_image = serializers.URLField(allow_null=True, required=False)
|
||||||
|
|
||||||
|
# Counter fields
|
||||||
|
console_port_template_count = serializers.IntegerField(read_only=True)
|
||||||
|
console_server_port_template_count = serializers.IntegerField(read_only=True)
|
||||||
|
power_port_template_count = serializers.IntegerField(read_only=True)
|
||||||
|
power_outlet_template_count = serializers.IntegerField(read_only=True)
|
||||||
|
interface_template_count = serializers.IntegerField(read_only=True)
|
||||||
|
front_port_template_count = serializers.IntegerField(read_only=True)
|
||||||
|
rear_port_template_count = serializers.IntegerField(read_only=True)
|
||||||
|
device_bay_template_count = serializers.IntegerField(read_only=True)
|
||||||
|
module_bay_template_count = serializers.IntegerField(read_only=True)
|
||||||
|
inventory_item_template_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
|
# Related object counts
|
||||||
|
device_count = RelatedObjectCountField('instances')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = DeviceType
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height',
|
||||||
|
'exclude_from_utilization', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit',
|
||||||
|
'front_image', 'rear_image', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
|
'device_count', 'console_port_template_count', 'console_server_port_template_count',
|
||||||
|
'power_port_template_count', 'power_outlet_template_count', 'interface_template_count',
|
||||||
|
'front_port_template_count', 'rear_port_template_count', 'device_bay_template_count',
|
||||||
|
'module_bay_template_count', 'inventory_item_template_count',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'slug', 'description', 'device_count')
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleTypeSerializer(NetBoxModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
|
||||||
|
manufacturer = ManufacturerSerializer(nested=True)
|
||||||
|
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ModuleType
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'manufacturer', 'model', 'part_number', 'weight', 'weight_unit', 'description',
|
||||||
|
'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'description')
|
26
netbox/dcim/api/serializers_/manufacturers.py
Normal file
26
netbox/dcim/api/serializers_/manufacturers.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from dcim.models import Manufacturer
|
||||||
|
from netbox.api.fields import RelatedObjectCountField
|
||||||
|
from netbox.api.serializers import NetBoxModelSerializer
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'ManufacturerSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ManufacturerSerializer(NetBoxModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
|
||||||
|
|
||||||
|
# Related object counts
|
||||||
|
devicetype_count = RelatedObjectCountField('device_types')
|
||||||
|
inventoryitem_count = RelatedObjectCountField('inventory_items')
|
||||||
|
platform_count = RelatedObjectCountField('platforms')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Manufacturer
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
|
'devicetype_count', 'inventoryitem_count', 'platform_count',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'devicetype_count')
|
29
netbox/dcim/api/serializers_/platforms.py
Normal file
29
netbox/dcim/api/serializers_/platforms.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from dcim.models import Platform
|
||||||
|
from extras.api.serializers_.configtemplates import ConfigTemplateSerializer
|
||||||
|
from netbox.api.fields import RelatedObjectCountField
|
||||||
|
from netbox.api.serializers import NetBoxModelSerializer
|
||||||
|
from .manufacturers import ManufacturerSerializer
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'PlatformSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PlatformSerializer(NetBoxModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
|
||||||
|
manufacturer = ManufacturerSerializer(nested=True, required=False, allow_null=True)
|
||||||
|
config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None)
|
||||||
|
|
||||||
|
# Related object counts
|
||||||
|
device_count = RelatedObjectCountField('devices')
|
||||||
|
virtualmachine_count = RelatedObjectCountField('virtual_machines')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Platform
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags',
|
||||||
|
'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'device_count', 'virtualmachine_count')
|
80
netbox/dcim/api/serializers_/power.py
Normal file
80
netbox/dcim/api/serializers_/power.py
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from dcim.choices import *
|
||||||
|
from dcim.models import PowerFeed, PowerPanel
|
||||||
|
from netbox.api.fields import ChoiceField, RelatedObjectCountField
|
||||||
|
from netbox.api.serializers import NetBoxModelSerializer
|
||||||
|
from tenancy.api.serializers_.tenants import TenantSerializer
|
||||||
|
from .base import ConnectedEndpointsSerializer
|
||||||
|
from .cables import CabledObjectSerializer
|
||||||
|
from .racks import RackSerializer
|
||||||
|
from .sites import LocationSerializer, SiteSerializer
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'PowerFeedSerializer',
|
||||||
|
'PowerPanelSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PowerPanelSerializer(NetBoxModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
|
||||||
|
site = SiteSerializer(nested=True)
|
||||||
|
location = LocationSerializer(
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
|
||||||
|
# Related object counts
|
||||||
|
powerfeed_count = RelatedObjectCountField('powerfeeds')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PowerPanel
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'site', 'location', 'name', 'description', 'comments', 'tags', 'custom_fields',
|
||||||
|
'powerfeed_count', 'created', 'last_updated',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'description', 'powerfeed_count')
|
||||||
|
|
||||||
|
|
||||||
|
class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
|
||||||
|
power_panel = PowerPanelSerializer(nested=True)
|
||||||
|
rack = RackSerializer(
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
type = ChoiceField(
|
||||||
|
choices=PowerFeedTypeChoices,
|
||||||
|
default=lambda: PowerFeedTypeChoices.TYPE_PRIMARY,
|
||||||
|
)
|
||||||
|
status = ChoiceField(
|
||||||
|
choices=PowerFeedStatusChoices,
|
||||||
|
default=lambda: PowerFeedStatusChoices.STATUS_ACTIVE,
|
||||||
|
)
|
||||||
|
supply = ChoiceField(
|
||||||
|
choices=PowerFeedSupplyChoices,
|
||||||
|
default=lambda: PowerFeedSupplyChoices.SUPPLY_AC,
|
||||||
|
)
|
||||||
|
phase = ChoiceField(
|
||||||
|
choices=PowerFeedPhaseChoices,
|
||||||
|
default=lambda: PowerFeedPhaseChoices.PHASE_SINGLE,
|
||||||
|
)
|
||||||
|
tenant = TenantSerializer(
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PowerFeed
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage',
|
||||||
|
'amperage', 'max_utilization', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
|
||||||
|
'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'description',
|
||||||
|
'tenant', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'description', 'cable', '_occupied')
|
117
netbox/dcim/api/serializers_/racks.py
Normal file
117
netbox/dcim/api/serializers_/racks.py
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
from django.utils.translation import gettext as _
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from dcim.choices import *
|
||||||
|
from dcim.constants import *
|
||||||
|
from dcim.models import Rack, RackReservation, RackRole
|
||||||
|
from netbox.api.fields import ChoiceField, RelatedObjectCountField
|
||||||
|
from netbox.api.serializers import NetBoxModelSerializer
|
||||||
|
from netbox.config import ConfigItem
|
||||||
|
from tenancy.api.serializers_.tenants import TenantSerializer
|
||||||
|
from users.api.serializers_.users import UserSerializer
|
||||||
|
from .sites import LocationSerializer, SiteSerializer
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'RackElevationDetailFilterSerializer',
|
||||||
|
'RackReservationSerializer',
|
||||||
|
'RackRoleSerializer',
|
||||||
|
'RackSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RackRoleSerializer(NetBoxModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
|
||||||
|
|
||||||
|
# Related object counts
|
||||||
|
rack_count = RelatedObjectCountField('racks')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = RackRole
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created',
|
||||||
|
'last_updated', 'rack_count',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count')
|
||||||
|
|
||||||
|
|
||||||
|
class RackSerializer(NetBoxModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
|
||||||
|
site = SiteSerializer(nested=True)
|
||||||
|
location = LocationSerializer(nested=True, required=False, allow_null=True, default=None)
|
||||||
|
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||||
|
status = ChoiceField(choices=RackStatusChoices, required=False)
|
||||||
|
role = RackRoleSerializer(nested=True, required=False, allow_null=True)
|
||||||
|
type = ChoiceField(choices=RackTypeChoices, allow_blank=True, required=False, allow_null=True)
|
||||||
|
facility_id = serializers.CharField(max_length=50, allow_blank=True, allow_null=True, label=_('Facility ID'),
|
||||||
|
default=None)
|
||||||
|
width = ChoiceField(choices=RackWidthChoices, required=False)
|
||||||
|
outer_unit = ChoiceField(choices=RackDimensionUnitChoices, allow_blank=True, required=False, allow_null=True)
|
||||||
|
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
|
||||||
|
|
||||||
|
# Related object counts
|
||||||
|
device_count = RelatedObjectCountField('devices')
|
||||||
|
powerfeed_count = RelatedObjectCountField('powerfeeds')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Rack
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status', 'role', 'serial',
|
||||||
|
'asset_tag', 'type', 'width', 'u_height', 'starting_unit', 'weight', 'max_weight', 'weight_unit',
|
||||||
|
'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'description', 'comments',
|
||||||
|
'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'description', 'device_count')
|
||||||
|
|
||||||
|
|
||||||
|
class RackReservationSerializer(NetBoxModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail')
|
||||||
|
rack = RackSerializer(nested=True)
|
||||||
|
user = UserSerializer(nested=True)
|
||||||
|
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = RackReservation
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'rack', 'units', 'created', 'last_updated', 'user', 'tenant', 'description',
|
||||||
|
'comments', 'tags', 'custom_fields',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'user', 'description', 'units')
|
||||||
|
|
||||||
|
|
||||||
|
class RackElevationDetailFilterSerializer(serializers.Serializer):
|
||||||
|
q = serializers.CharField(
|
||||||
|
required=False,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
face = serializers.ChoiceField(
|
||||||
|
choices=DeviceFaceChoices,
|
||||||
|
default=DeviceFaceChoices.FACE_FRONT
|
||||||
|
)
|
||||||
|
render = serializers.ChoiceField(
|
||||||
|
choices=RackElevationDetailRenderChoices,
|
||||||
|
default=RackElevationDetailRenderChoices.RENDER_JSON
|
||||||
|
)
|
||||||
|
unit_width = serializers.IntegerField(
|
||||||
|
default=ConfigItem('RACK_ELEVATION_DEFAULT_UNIT_WIDTH')
|
||||||
|
)
|
||||||
|
unit_height = serializers.IntegerField(
|
||||||
|
default=ConfigItem('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT')
|
||||||
|
)
|
||||||
|
legend_width = serializers.IntegerField(
|
||||||
|
default=RACK_ELEVATION_DEFAULT_LEGEND_WIDTH
|
||||||
|
)
|
||||||
|
margin_width = serializers.IntegerField(
|
||||||
|
default=RACK_ELEVATION_DEFAULT_MARGIN_WIDTH
|
||||||
|
)
|
||||||
|
exclude = serializers.IntegerField(
|
||||||
|
required=False,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
expand_devices = serializers.BooleanField(
|
||||||
|
required=False,
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
include_images = serializers.BooleanField(
|
||||||
|
required=False,
|
||||||
|
default=True
|
||||||
|
)
|
31
netbox/dcim/api/serializers_/rackunits.py
Normal file
31
netbox/dcim/api/serializers_/rackunits.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
from drf_spectacular.types import OpenApiTypes
|
||||||
|
from drf_spectacular.utils import extend_schema_field
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from dcim.choices import *
|
||||||
|
from netbox.api.fields import ChoiceField
|
||||||
|
from .devices import DeviceSerializer
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'RackUnitSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RackUnitSerializer(serializers.Serializer):
|
||||||
|
"""
|
||||||
|
A rack unit is an abstraction formed by the set (rack, position, face); it does not exist as a row in the database.
|
||||||
|
"""
|
||||||
|
id = serializers.DecimalField(
|
||||||
|
max_digits=4,
|
||||||
|
decimal_places=1,
|
||||||
|
read_only=True
|
||||||
|
)
|
||||||
|
name = serializers.CharField(read_only=True)
|
||||||
|
face = ChoiceField(choices=DeviceFaceChoices, read_only=True)
|
||||||
|
device = DeviceSerializer(nested=True, read_only=True)
|
||||||
|
occupied = serializers.BooleanField(read_only=True)
|
||||||
|
display = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
|
@extend_schema_field(OpenApiTypes.STR)
|
||||||
|
def get_display(self, obj):
|
||||||
|
return obj['name']
|
43
netbox/dcim/api/serializers_/roles.py
Normal file
43
netbox/dcim/api/serializers_/roles.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from dcim.models import DeviceRole, InventoryItemRole
|
||||||
|
from extras.api.serializers_.configtemplates import ConfigTemplateSerializer
|
||||||
|
from netbox.api.fields import RelatedObjectCountField
|
||||||
|
from netbox.api.serializers import NetBoxModelSerializer
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'DeviceRoleSerializer',
|
||||||
|
'InventoryItemRoleSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceRoleSerializer(NetBoxModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
|
||||||
|
config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None)
|
||||||
|
|
||||||
|
# Related object counts
|
||||||
|
device_count = RelatedObjectCountField('devices')
|
||||||
|
virtualmachine_count = RelatedObjectCountField('virtual_machines')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = DeviceRole
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags',
|
||||||
|
'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'device_count', 'virtualmachine_count')
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryItemRoleSerializer(NetBoxModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail')
|
||||||
|
|
||||||
|
# Related object counts
|
||||||
|
inventoryitem_count = RelatedObjectCountField('inventory_items')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = InventoryItemRole
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created',
|
||||||
|
'last_updated', 'inventoryitem_count',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'inventoryitem_count')
|
98
netbox/dcim/api/serializers_/sites.py
Normal file
98
netbox/dcim/api/serializers_/sites.py
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
from timezone_field.rest_framework import TimeZoneSerializerField
|
||||||
|
|
||||||
|
from dcim.choices import *
|
||||||
|
from dcim.models import Location, Region, Site, SiteGroup
|
||||||
|
from ipam.api.serializers_.asns import ASNSerializer
|
||||||
|
from ipam.models import ASN
|
||||||
|
from netbox.api.fields import ChoiceField, RelatedObjectCountField, SerializedPKRelatedField
|
||||||
|
from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer
|
||||||
|
from tenancy.api.serializers_.tenants import TenantSerializer
|
||||||
|
from ..nested_serializers import *
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'LocationSerializer',
|
||||||
|
'RegionSerializer',
|
||||||
|
'SiteGroupSerializer',
|
||||||
|
'SiteSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RegionSerializer(NestedGroupModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
|
||||||
|
parent = NestedRegionSerializer(required=False, allow_null=True, default=None)
|
||||||
|
site_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Region
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created',
|
||||||
|
'last_updated', 'site_count', '_depth',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth')
|
||||||
|
|
||||||
|
|
||||||
|
class SiteGroupSerializer(NestedGroupModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail')
|
||||||
|
parent = NestedSiteGroupSerializer(required=False, allow_null=True, default=None)
|
||||||
|
site_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = SiteGroup
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created',
|
||||||
|
'last_updated', 'site_count', '_depth',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth')
|
||||||
|
|
||||||
|
|
||||||
|
class SiteSerializer(NetBoxModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
|
||||||
|
status = ChoiceField(choices=SiteStatusChoices, required=False)
|
||||||
|
region = RegionSerializer(nested=True, required=False, allow_null=True)
|
||||||
|
group = SiteGroupSerializer(nested=True, required=False, allow_null=True)
|
||||||
|
tenant = TenantSerializer(required=False, allow_null=True)
|
||||||
|
time_zone = TimeZoneSerializerField(required=False, allow_null=True)
|
||||||
|
asns = SerializedPKRelatedField(
|
||||||
|
queryset=ASN.objects.all(),
|
||||||
|
serializer=ASNSerializer,
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
many=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Related object counts
|
||||||
|
circuit_count = RelatedObjectCountField('circuit_terminations')
|
||||||
|
device_count = RelatedObjectCountField('devices')
|
||||||
|
prefix_count = RelatedObjectCountField('prefixes')
|
||||||
|
rack_count = RelatedObjectCountField('racks')
|
||||||
|
vlan_count = RelatedObjectCountField('vlans')
|
||||||
|
virtualmachine_count = RelatedObjectCountField('virtual_machines')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Site
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'time_zone',
|
||||||
|
'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'asns', 'tags',
|
||||||
|
'custom_fields', 'created', 'last_updated', 'circuit_count', 'device_count', 'prefix_count', 'rack_count',
|
||||||
|
'virtualmachine_count', 'vlan_count',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'description', 'slug')
|
||||||
|
|
||||||
|
|
||||||
|
class LocationSerializer(NestedGroupModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail')
|
||||||
|
site = SiteSerializer(nested=True)
|
||||||
|
parent = NestedLocationSerializer(required=False, allow_null=True)
|
||||||
|
status = ChoiceField(choices=LocationStatusChoices, required=False)
|
||||||
|
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||||
|
rack_count = serializers.IntegerField(read_only=True)
|
||||||
|
device_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Location
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'status', 'tenant', 'description', 'tags',
|
||||||
|
'custom_fields', 'created', 'last_updated', 'rack_count', 'device_count', '_depth',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count', '_depth')
|
25
netbox/dcim/api/serializers_/virtualchassis.py
Normal file
25
netbox/dcim/api/serializers_/virtualchassis.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from dcim.models import VirtualChassis
|
||||||
|
from netbox.api.serializers import NetBoxModelSerializer
|
||||||
|
from ..nested_serializers import *
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'VirtualChassisSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualChassisSerializer(NetBoxModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
|
||||||
|
master = NestedDeviceSerializer(required=False, allow_null=True, default=None)
|
||||||
|
|
||||||
|
# Counter fields
|
||||||
|
member_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VirtualChassis
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'name', 'domain', 'master', 'description', 'comments', 'tags', 'custom_fields',
|
||||||
|
'created', 'last_updated', 'member_count',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'master', 'description', 'member_count')
|
@ -7,23 +7,18 @@ from rest_framework.response import Response
|
|||||||
from rest_framework.routers import APIRootView
|
from rest_framework.routers import APIRootView
|
||||||
from rest_framework.viewsets import ViewSet
|
from rest_framework.viewsets import ViewSet
|
||||||
|
|
||||||
from circuits.models import Circuit
|
|
||||||
from dcim import filtersets
|
from dcim import filtersets
|
||||||
from dcim.constants import CABLE_TRACE_SVG_DEFAULT_WIDTH
|
from dcim.constants import CABLE_TRACE_SVG_DEFAULT_WIDTH
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from dcim.svg import CableTraceSVG
|
from dcim.svg import CableTraceSVG
|
||||||
from extras.api.mixins import ConfigContextQuerySetMixin, RenderConfigMixin
|
from extras.api.mixins import ConfigContextQuerySetMixin, RenderConfigMixin
|
||||||
from ipam.models import Prefix, VLAN
|
|
||||||
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
|
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
|
||||||
from netbox.api.metadata import ContentTypeMetadata
|
from netbox.api.metadata import ContentTypeMetadata
|
||||||
from netbox.api.pagination import StripCountAnnotationsPaginator
|
from netbox.api.pagination import StripCountAnnotationsPaginator
|
||||||
from netbox.api.viewsets import NetBoxModelViewSet, MPTTLockedMixin
|
from netbox.api.viewsets import NetBoxModelViewSet, MPTTLockedMixin
|
||||||
from netbox.api.viewsets.mixins import SequentialBulkCreatesMixin
|
from netbox.api.viewsets.mixins import SequentialBulkCreatesMixin
|
||||||
from netbox.constants import NESTED_SERIALIZER_PREFIX
|
|
||||||
from utilities.api import get_serializer_for_model
|
from utilities.api import get_serializer_for_model
|
||||||
from utilities.query_functions import CollateAsChar
|
from utilities.query_functions import CollateAsChar
|
||||||
from utilities.utils import count_related
|
|
||||||
from virtualization.models import VirtualMachine
|
|
||||||
from . import serializers
|
from . import serializers
|
||||||
from .exceptions import MissingFilterException
|
from .exceptions import MissingFilterException
|
||||||
|
|
||||||
@ -62,16 +57,16 @@ class PathEndpointMixin(object):
|
|||||||
# Serialize path objects, iterating over each three-tuple in the path
|
# Serialize path objects, iterating over each three-tuple in the path
|
||||||
for near_ends, cable, far_ends in obj.trace():
|
for near_ends, cable, far_ends in obj.trace():
|
||||||
if near_ends:
|
if near_ends:
|
||||||
serializer_a = get_serializer_for_model(near_ends[0], prefix=NESTED_SERIALIZER_PREFIX)
|
serializer_a = get_serializer_for_model(near_ends[0])
|
||||||
near_ends = serializer_a(near_ends, many=True, context={'request': request}).data
|
near_ends = serializer_a(near_ends, nested=True, many=True, context={'request': request}).data
|
||||||
else:
|
else:
|
||||||
# Path is split; stop here
|
# Path is split; stop here
|
||||||
break
|
break
|
||||||
if cable:
|
if cable:
|
||||||
cable = serializers.TracedCableSerializer(cable[0], context={'request': request}).data
|
cable = serializers.TracedCableSerializer(cable[0], context={'request': request}).data
|
||||||
if far_ends:
|
if far_ends:
|
||||||
serializer_b = get_serializer_for_model(far_ends[0], prefix=NESTED_SERIALIZER_PREFIX)
|
serializer_b = get_serializer_for_model(far_ends[0])
|
||||||
far_ends = serializer_b(far_ends, many=True, context={'request': request}).data
|
far_ends = serializer_b(far_ends, nested=True, many=True, context={'request': request}).data
|
||||||
|
|
||||||
path.append((near_ends, cable, far_ends))
|
path.append((near_ends, cable, far_ends))
|
||||||
|
|
||||||
@ -103,7 +98,7 @@ class RegionViewSet(MPTTLockedMixin, NetBoxModelViewSet):
|
|||||||
'region',
|
'region',
|
||||||
'site_count',
|
'site_count',
|
||||||
cumulative=True
|
cumulative=True
|
||||||
).prefetch_related('tags')
|
)
|
||||||
serializer_class = serializers.RegionSerializer
|
serializer_class = serializers.RegionSerializer
|
||||||
filterset_class = filtersets.RegionFilterSet
|
filterset_class = filtersets.RegionFilterSet
|
||||||
|
|
||||||
@ -119,7 +114,7 @@ class SiteGroupViewSet(MPTTLockedMixin, NetBoxModelViewSet):
|
|||||||
'group',
|
'group',
|
||||||
'site_count',
|
'site_count',
|
||||||
cumulative=True
|
cumulative=True
|
||||||
).prefetch_related('tags')
|
)
|
||||||
serializer_class = serializers.SiteGroupSerializer
|
serializer_class = serializers.SiteGroupSerializer
|
||||||
filterset_class = filtersets.SiteGroupFilterSet
|
filterset_class = filtersets.SiteGroupFilterSet
|
||||||
|
|
||||||
@ -129,16 +124,7 @@ class SiteGroupViewSet(MPTTLockedMixin, NetBoxModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class SiteViewSet(NetBoxModelViewSet):
|
class SiteViewSet(NetBoxModelViewSet):
|
||||||
queryset = Site.objects.prefetch_related(
|
queryset = Site.objects.all()
|
||||||
'region', 'tenant', 'asns', 'tags'
|
|
||||||
).annotate(
|
|
||||||
device_count=count_related(Device, 'site'),
|
|
||||||
rack_count=count_related(Rack, 'site'),
|
|
||||||
prefix_count=count_related(Prefix, 'site'),
|
|
||||||
vlan_count=count_related(VLAN, 'site'),
|
|
||||||
circuit_count=count_related(Circuit, 'terminations__site'),
|
|
||||||
virtualmachine_count=count_related(VirtualMachine, 'cluster__site')
|
|
||||||
)
|
|
||||||
serializer_class = serializers.SiteSerializer
|
serializer_class = serializers.SiteSerializer
|
||||||
filterset_class = filtersets.SiteFilterSet
|
filterset_class = filtersets.SiteFilterSet
|
||||||
|
|
||||||
@ -160,7 +146,7 @@ class LocationViewSet(MPTTLockedMixin, NetBoxModelViewSet):
|
|||||||
'location',
|
'location',
|
||||||
'rack_count',
|
'rack_count',
|
||||||
cumulative=True
|
cumulative=True
|
||||||
).prefetch_related('site', 'tags')
|
)
|
||||||
serializer_class = serializers.LocationSerializer
|
serializer_class = serializers.LocationSerializer
|
||||||
filterset_class = filtersets.LocationFilterSet
|
filterset_class = filtersets.LocationFilterSet
|
||||||
|
|
||||||
@ -170,9 +156,7 @@ class LocationViewSet(MPTTLockedMixin, NetBoxModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class RackRoleViewSet(NetBoxModelViewSet):
|
class RackRoleViewSet(NetBoxModelViewSet):
|
||||||
queryset = RackRole.objects.prefetch_related('tags').annotate(
|
queryset = RackRole.objects.all()
|
||||||
rack_count=count_related(Rack, 'role')
|
|
||||||
)
|
|
||||||
serializer_class = serializers.RackRoleSerializer
|
serializer_class = serializers.RackRoleSerializer
|
||||||
filterset_class = filtersets.RackRoleFilterSet
|
filterset_class = filtersets.RackRoleFilterSet
|
||||||
|
|
||||||
@ -182,15 +166,16 @@ class RackRoleViewSet(NetBoxModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class RackViewSet(NetBoxModelViewSet):
|
class RackViewSet(NetBoxModelViewSet):
|
||||||
queryset = Rack.objects.prefetch_related(
|
queryset = Rack.objects.all()
|
||||||
'site', 'location', 'role', 'tenant', 'tags'
|
|
||||||
).annotate(
|
|
||||||
device_count=count_related(Device, 'rack'),
|
|
||||||
powerfeed_count=count_related(PowerFeed, 'rack')
|
|
||||||
)
|
|
||||||
serializer_class = serializers.RackSerializer
|
serializer_class = serializers.RackSerializer
|
||||||
filterset_class = filtersets.RackFilterSet
|
filterset_class = filtersets.RackFilterSet
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
operation_id='dcim_racks_elevation_retrieve',
|
||||||
|
filters=False,
|
||||||
|
parameters=[serializers.RackElevationDetailFilterSerializer],
|
||||||
|
responses={200: serializers.RackUnitSerializer(many=True)}
|
||||||
|
)
|
||||||
@action(detail=True)
|
@action(detail=True)
|
||||||
def elevation(self, request, pk=None):
|
def elevation(self, request, pk=None):
|
||||||
"""
|
"""
|
||||||
@ -249,7 +234,7 @@ class RackViewSet(NetBoxModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class RackReservationViewSet(NetBoxModelViewSet):
|
class RackReservationViewSet(NetBoxModelViewSet):
|
||||||
queryset = RackReservation.objects.prefetch_related('rack', 'user', 'tenant')
|
queryset = RackReservation.objects.all()
|
||||||
serializer_class = serializers.RackReservationSerializer
|
serializer_class = serializers.RackReservationSerializer
|
||||||
filterset_class = filtersets.RackReservationFilterSet
|
filterset_class = filtersets.RackReservationFilterSet
|
||||||
|
|
||||||
@ -259,11 +244,7 @@ class RackReservationViewSet(NetBoxModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class ManufacturerViewSet(NetBoxModelViewSet):
|
class ManufacturerViewSet(NetBoxModelViewSet):
|
||||||
queryset = Manufacturer.objects.prefetch_related('tags').annotate(
|
queryset = Manufacturer.objects.all()
|
||||||
devicetype_count=count_related(DeviceType, 'manufacturer'),
|
|
||||||
inventoryitem_count=count_related(InventoryItem, 'manufacturer'),
|
|
||||||
platform_count=count_related(Platform, 'manufacturer')
|
|
||||||
)
|
|
||||||
serializer_class = serializers.ManufacturerSerializer
|
serializer_class = serializers.ManufacturerSerializer
|
||||||
filterset_class = filtersets.ManufacturerFilterSet
|
filterset_class = filtersets.ManufacturerFilterSet
|
||||||
|
|
||||||
@ -273,21 +254,15 @@ class ManufacturerViewSet(NetBoxModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class DeviceTypeViewSet(NetBoxModelViewSet):
|
class DeviceTypeViewSet(NetBoxModelViewSet):
|
||||||
queryset = DeviceType.objects.prefetch_related('manufacturer', 'default_platform', 'tags').annotate(
|
queryset = DeviceType.objects.all()
|
||||||
device_count=count_related(Device, 'device_type')
|
|
||||||
)
|
|
||||||
serializer_class = serializers.DeviceTypeSerializer
|
serializer_class = serializers.DeviceTypeSerializer
|
||||||
filterset_class = filtersets.DeviceTypeFilterSet
|
filterset_class = filtersets.DeviceTypeFilterSet
|
||||||
brief_prefetch_fields = ['manufacturer']
|
|
||||||
|
|
||||||
|
|
||||||
class ModuleTypeViewSet(NetBoxModelViewSet):
|
class ModuleTypeViewSet(NetBoxModelViewSet):
|
||||||
queryset = ModuleType.objects.prefetch_related('manufacturer', 'tags').annotate(
|
queryset = ModuleType.objects.all()
|
||||||
# module_count=count_related(Module, 'module_type')
|
|
||||||
)
|
|
||||||
serializer_class = serializers.ModuleTypeSerializer
|
serializer_class = serializers.ModuleTypeSerializer
|
||||||
filterset_class = filtersets.ModuleTypeFilterSet
|
filterset_class = filtersets.ModuleTypeFilterSet
|
||||||
brief_prefetch_fields = ['manufacturer']
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -295,61 +270,61 @@ class ModuleTypeViewSet(NetBoxModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class ConsolePortTemplateViewSet(NetBoxModelViewSet):
|
class ConsolePortTemplateViewSet(NetBoxModelViewSet):
|
||||||
queryset = ConsolePortTemplate.objects.prefetch_related('device_type__manufacturer')
|
queryset = ConsolePortTemplate.objects.all()
|
||||||
serializer_class = serializers.ConsolePortTemplateSerializer
|
serializer_class = serializers.ConsolePortTemplateSerializer
|
||||||
filterset_class = filtersets.ConsolePortTemplateFilterSet
|
filterset_class = filtersets.ConsolePortTemplateFilterSet
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortTemplateViewSet(NetBoxModelViewSet):
|
class ConsoleServerPortTemplateViewSet(NetBoxModelViewSet):
|
||||||
queryset = ConsoleServerPortTemplate.objects.prefetch_related('device_type__manufacturer')
|
queryset = ConsoleServerPortTemplate.objects.all()
|
||||||
serializer_class = serializers.ConsoleServerPortTemplateSerializer
|
serializer_class = serializers.ConsoleServerPortTemplateSerializer
|
||||||
filterset_class = filtersets.ConsoleServerPortTemplateFilterSet
|
filterset_class = filtersets.ConsoleServerPortTemplateFilterSet
|
||||||
|
|
||||||
|
|
||||||
class PowerPortTemplateViewSet(NetBoxModelViewSet):
|
class PowerPortTemplateViewSet(NetBoxModelViewSet):
|
||||||
queryset = PowerPortTemplate.objects.prefetch_related('device_type__manufacturer')
|
queryset = PowerPortTemplate.objects.all()
|
||||||
serializer_class = serializers.PowerPortTemplateSerializer
|
serializer_class = serializers.PowerPortTemplateSerializer
|
||||||
filterset_class = filtersets.PowerPortTemplateFilterSet
|
filterset_class = filtersets.PowerPortTemplateFilterSet
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletTemplateViewSet(NetBoxModelViewSet):
|
class PowerOutletTemplateViewSet(NetBoxModelViewSet):
|
||||||
queryset = PowerOutletTemplate.objects.prefetch_related('device_type__manufacturer')
|
queryset = PowerOutletTemplate.objects.all()
|
||||||
serializer_class = serializers.PowerOutletTemplateSerializer
|
serializer_class = serializers.PowerOutletTemplateSerializer
|
||||||
filterset_class = filtersets.PowerOutletTemplateFilterSet
|
filterset_class = filtersets.PowerOutletTemplateFilterSet
|
||||||
|
|
||||||
|
|
||||||
class InterfaceTemplateViewSet(NetBoxModelViewSet):
|
class InterfaceTemplateViewSet(NetBoxModelViewSet):
|
||||||
queryset = InterfaceTemplate.objects.prefetch_related('device_type__manufacturer')
|
queryset = InterfaceTemplate.objects.all()
|
||||||
serializer_class = serializers.InterfaceTemplateSerializer
|
serializer_class = serializers.InterfaceTemplateSerializer
|
||||||
filterset_class = filtersets.InterfaceTemplateFilterSet
|
filterset_class = filtersets.InterfaceTemplateFilterSet
|
||||||
|
|
||||||
|
|
||||||
class FrontPortTemplateViewSet(NetBoxModelViewSet):
|
class FrontPortTemplateViewSet(NetBoxModelViewSet):
|
||||||
queryset = FrontPortTemplate.objects.prefetch_related('device_type__manufacturer')
|
queryset = FrontPortTemplate.objects.all()
|
||||||
serializer_class = serializers.FrontPortTemplateSerializer
|
serializer_class = serializers.FrontPortTemplateSerializer
|
||||||
filterset_class = filtersets.FrontPortTemplateFilterSet
|
filterset_class = filtersets.FrontPortTemplateFilterSet
|
||||||
|
|
||||||
|
|
||||||
class RearPortTemplateViewSet(NetBoxModelViewSet):
|
class RearPortTemplateViewSet(NetBoxModelViewSet):
|
||||||
queryset = RearPortTemplate.objects.prefetch_related('device_type__manufacturer')
|
queryset = RearPortTemplate.objects.all()
|
||||||
serializer_class = serializers.RearPortTemplateSerializer
|
serializer_class = serializers.RearPortTemplateSerializer
|
||||||
filterset_class = filtersets.RearPortTemplateFilterSet
|
filterset_class = filtersets.RearPortTemplateFilterSet
|
||||||
|
|
||||||
|
|
||||||
class ModuleBayTemplateViewSet(NetBoxModelViewSet):
|
class ModuleBayTemplateViewSet(NetBoxModelViewSet):
|
||||||
queryset = ModuleBayTemplate.objects.prefetch_related('device_type__manufacturer')
|
queryset = ModuleBayTemplate.objects.all()
|
||||||
serializer_class = serializers.ModuleBayTemplateSerializer
|
serializer_class = serializers.ModuleBayTemplateSerializer
|
||||||
filterset_class = filtersets.ModuleBayTemplateFilterSet
|
filterset_class = filtersets.ModuleBayTemplateFilterSet
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayTemplateViewSet(NetBoxModelViewSet):
|
class DeviceBayTemplateViewSet(NetBoxModelViewSet):
|
||||||
queryset = DeviceBayTemplate.objects.prefetch_related('device_type__manufacturer')
|
queryset = DeviceBayTemplate.objects.all()
|
||||||
serializer_class = serializers.DeviceBayTemplateSerializer
|
serializer_class = serializers.DeviceBayTemplateSerializer
|
||||||
filterset_class = filtersets.DeviceBayTemplateFilterSet
|
filterset_class = filtersets.DeviceBayTemplateFilterSet
|
||||||
|
|
||||||
|
|
||||||
class InventoryItemTemplateViewSet(MPTTLockedMixin, NetBoxModelViewSet):
|
class InventoryItemTemplateViewSet(MPTTLockedMixin, NetBoxModelViewSet):
|
||||||
queryset = InventoryItemTemplate.objects.prefetch_related('device_type__manufacturer', 'role')
|
queryset = InventoryItemTemplate.objects.all()
|
||||||
serializer_class = serializers.InventoryItemTemplateSerializer
|
serializer_class = serializers.InventoryItemTemplateSerializer
|
||||||
filterset_class = filtersets.InventoryItemTemplateFilterSet
|
filterset_class = filtersets.InventoryItemTemplateFilterSet
|
||||||
|
|
||||||
@ -359,10 +334,7 @@ class InventoryItemTemplateViewSet(MPTTLockedMixin, NetBoxModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class DeviceRoleViewSet(NetBoxModelViewSet):
|
class DeviceRoleViewSet(NetBoxModelViewSet):
|
||||||
queryset = DeviceRole.objects.prefetch_related('config_template', 'tags').annotate(
|
queryset = DeviceRole.objects.all()
|
||||||
device_count=count_related(Device, 'role'),
|
|
||||||
virtualmachine_count=count_related(VirtualMachine, 'role')
|
|
||||||
)
|
|
||||||
serializer_class = serializers.DeviceRoleSerializer
|
serializer_class = serializers.DeviceRoleSerializer
|
||||||
filterset_class = filtersets.DeviceRoleFilterSet
|
filterset_class = filtersets.DeviceRoleFilterSet
|
||||||
|
|
||||||
@ -372,10 +344,7 @@ class DeviceRoleViewSet(NetBoxModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class PlatformViewSet(NetBoxModelViewSet):
|
class PlatformViewSet(NetBoxModelViewSet):
|
||||||
queryset = Platform.objects.prefetch_related('config_template', 'tags').annotate(
|
queryset = Platform.objects.all()
|
||||||
device_count=count_related(Device, 'platform'),
|
|
||||||
virtualmachine_count=count_related(VirtualMachine, 'platform')
|
|
||||||
)
|
|
||||||
serializer_class = serializers.PlatformSerializer
|
serializer_class = serializers.PlatformSerializer
|
||||||
filterset_class = filtersets.PlatformFilterSet
|
filterset_class = filtersets.PlatformFilterSet
|
||||||
|
|
||||||
@ -391,8 +360,7 @@ class DeviceViewSet(
|
|||||||
NetBoxModelViewSet
|
NetBoxModelViewSet
|
||||||
):
|
):
|
||||||
queryset = Device.objects.prefetch_related(
|
queryset = Device.objects.prefetch_related(
|
||||||
'device_type__manufacturer', 'role', 'tenant', 'platform', 'site', 'location', 'rack', 'parent_bay',
|
'parent_bay', # Referenced by DeviceSerializer.get_parent_device()
|
||||||
'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'config_template', 'tags',
|
|
||||||
)
|
)
|
||||||
filterset_class = filtersets.DeviceFilterSet
|
filterset_class = filtersets.DeviceFilterSet
|
||||||
pagination_class = StripCountAnnotationsPaginator
|
pagination_class = StripCountAnnotationsPaginator
|
||||||
@ -407,31 +375,21 @@ class DeviceViewSet(
|
|||||||
|
|
||||||
Else, return the DeviceWithConfigContextSerializer
|
Else, return the DeviceWithConfigContextSerializer
|
||||||
"""
|
"""
|
||||||
|
|
||||||
request = self.get_serializer_context()['request']
|
request = self.get_serializer_context()['request']
|
||||||
if request.query_params.get('brief', False):
|
if self.brief or 'config_context' in request.query_params.get('exclude', []):
|
||||||
return serializers.NestedDeviceSerializer
|
|
||||||
|
|
||||||
elif 'config_context' in request.query_params.get('exclude', []):
|
|
||||||
return serializers.DeviceSerializer
|
return serializers.DeviceSerializer
|
||||||
|
|
||||||
return serializers.DeviceWithConfigContextSerializer
|
return serializers.DeviceWithConfigContextSerializer
|
||||||
|
|
||||||
|
|
||||||
class VirtualDeviceContextViewSet(NetBoxModelViewSet):
|
class VirtualDeviceContextViewSet(NetBoxModelViewSet):
|
||||||
queryset = VirtualDeviceContext.objects.prefetch_related(
|
queryset = VirtualDeviceContext.objects.all()
|
||||||
'device__device_type', 'device', 'tenant', 'tags',
|
|
||||||
).annotate(
|
|
||||||
interface_count=count_related(Interface, 'vdcs'),
|
|
||||||
)
|
|
||||||
serializer_class = serializers.VirtualDeviceContextSerializer
|
serializer_class = serializers.VirtualDeviceContextSerializer
|
||||||
filterset_class = filtersets.VirtualDeviceContextFilterSet
|
filterset_class = filtersets.VirtualDeviceContextFilterSet
|
||||||
|
|
||||||
|
|
||||||
class ModuleViewSet(NetBoxModelViewSet):
|
class ModuleViewSet(NetBoxModelViewSet):
|
||||||
queryset = Module.objects.prefetch_related(
|
queryset = Module.objects.all()
|
||||||
'device', 'module_bay', 'module_type__manufacturer', 'tags',
|
|
||||||
)
|
|
||||||
serializer_class = serializers.ModuleSerializer
|
serializer_class = serializers.ModuleSerializer
|
||||||
filterset_class = filtersets.ModuleFilterSet
|
filterset_class = filtersets.ModuleFilterSet
|
||||||
|
|
||||||
@ -442,49 +400,45 @@ class ModuleViewSet(NetBoxModelViewSet):
|
|||||||
|
|
||||||
class ConsolePortViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
class ConsolePortViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||||
queryset = ConsolePort.objects.prefetch_related(
|
queryset = ConsolePort.objects.prefetch_related(
|
||||||
'device', 'module__module_bay', '_path', 'cable__terminations', 'tags'
|
'_path', 'cable__terminations',
|
||||||
)
|
)
|
||||||
serializer_class = serializers.ConsolePortSerializer
|
serializer_class = serializers.ConsolePortSerializer
|
||||||
filterset_class = filtersets.ConsolePortFilterSet
|
filterset_class = filtersets.ConsolePortFilterSet
|
||||||
brief_prefetch_fields = ['device']
|
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
class ConsoleServerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||||
queryset = ConsoleServerPort.objects.prefetch_related(
|
queryset = ConsoleServerPort.objects.prefetch_related(
|
||||||
'device', 'module__module_bay', '_path', 'cable__terminations', 'tags'
|
'_path', 'cable__terminations',
|
||||||
)
|
)
|
||||||
serializer_class = serializers.ConsoleServerPortSerializer
|
serializer_class = serializers.ConsoleServerPortSerializer
|
||||||
filterset_class = filtersets.ConsoleServerPortFilterSet
|
filterset_class = filtersets.ConsoleServerPortFilterSet
|
||||||
brief_prefetch_fields = ['device']
|
|
||||||
|
|
||||||
|
|
||||||
class PowerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
class PowerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||||
queryset = PowerPort.objects.prefetch_related(
|
queryset = PowerPort.objects.prefetch_related(
|
||||||
'device', 'module__module_bay', '_path', 'cable__terminations', 'tags'
|
'_path', 'cable__terminations',
|
||||||
)
|
)
|
||||||
serializer_class = serializers.PowerPortSerializer
|
serializer_class = serializers.PowerPortSerializer
|
||||||
filterset_class = filtersets.PowerPortFilterSet
|
filterset_class = filtersets.PowerPortFilterSet
|
||||||
brief_prefetch_fields = ['device']
|
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
class PowerOutletViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||||
queryset = PowerOutlet.objects.prefetch_related(
|
queryset = PowerOutlet.objects.prefetch_related(
|
||||||
'device', 'module__module_bay', '_path', 'cable__terminations', 'tags'
|
'_path', 'cable__terminations',
|
||||||
)
|
)
|
||||||
serializer_class = serializers.PowerOutletSerializer
|
serializer_class = serializers.PowerOutletSerializer
|
||||||
filterset_class = filtersets.PowerOutletFilterSet
|
filterset_class = filtersets.PowerOutletFilterSet
|
||||||
brief_prefetch_fields = ['device']
|
|
||||||
|
|
||||||
|
|
||||||
class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||||
queryset = Interface.objects.prefetch_related(
|
queryset = Interface.objects.prefetch_related(
|
||||||
'device', 'module__module_bay', 'parent', 'bridge', 'lag', '_path', 'cable__terminations', 'wireless_lans',
|
'_path', 'cable__terminations',
|
||||||
'untagged_vlan', 'tagged_vlans', 'vrf', 'ip_addresses', 'fhrp_group_assignments', 'tags', 'l2vpn_terminations',
|
'l2vpn_terminations', # Referenced by InterfaceSerializer.l2vpn_termination
|
||||||
'vdcs',
|
'ip_addresses', # Referenced by Interface.count_ipaddresses()
|
||||||
|
'fhrp_group_assignments', # Referenced by Interface.count_fhrp_groups()
|
||||||
)
|
)
|
||||||
serializer_class = serializers.InterfaceSerializer
|
serializer_class = serializers.InterfaceSerializer
|
||||||
filterset_class = filtersets.InterfaceFilterSet
|
filterset_class = filtersets.InterfaceFilterSet
|
||||||
brief_prefetch_fields = ['device']
|
|
||||||
|
|
||||||
def get_bulk_destroy_queryset(self):
|
def get_bulk_destroy_queryset(self):
|
||||||
# Ensure child interfaces are deleted prior to their parents
|
# Ensure child interfaces are deleted prior to their parents
|
||||||
@ -493,41 +447,36 @@ class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
|||||||
|
|
||||||
class FrontPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
class FrontPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
||||||
queryset = FrontPort.objects.prefetch_related(
|
queryset = FrontPort.objects.prefetch_related(
|
||||||
'device__device_type__manufacturer', 'module__module_bay', 'rear_port', 'cable__terminations', 'tags'
|
'cable__terminations',
|
||||||
)
|
)
|
||||||
serializer_class = serializers.FrontPortSerializer
|
serializer_class = serializers.FrontPortSerializer
|
||||||
filterset_class = filtersets.FrontPortFilterSet
|
filterset_class = filtersets.FrontPortFilterSet
|
||||||
brief_prefetch_fields = ['device']
|
|
||||||
|
|
||||||
|
|
||||||
class RearPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
class RearPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
||||||
queryset = RearPort.objects.prefetch_related(
|
queryset = RearPort.objects.prefetch_related(
|
||||||
'device__device_type__manufacturer', 'module__module_bay', 'cable__terminations', 'tags'
|
'cable__terminations',
|
||||||
)
|
)
|
||||||
serializer_class = serializers.RearPortSerializer
|
serializer_class = serializers.RearPortSerializer
|
||||||
filterset_class = filtersets.RearPortFilterSet
|
filterset_class = filtersets.RearPortFilterSet
|
||||||
brief_prefetch_fields = ['device']
|
|
||||||
|
|
||||||
|
|
||||||
class ModuleBayViewSet(NetBoxModelViewSet):
|
class ModuleBayViewSet(NetBoxModelViewSet):
|
||||||
queryset = ModuleBay.objects.prefetch_related('tags', 'installed_module')
|
queryset = ModuleBay.objects.all()
|
||||||
serializer_class = serializers.ModuleBaySerializer
|
serializer_class = serializers.ModuleBaySerializer
|
||||||
filterset_class = filtersets.ModuleBayFilterSet
|
filterset_class = filtersets.ModuleBayFilterSet
|
||||||
brief_prefetch_fields = ['device']
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayViewSet(NetBoxModelViewSet):
|
class DeviceBayViewSet(NetBoxModelViewSet):
|
||||||
queryset = DeviceBay.objects.prefetch_related('installed_device', 'tags')
|
queryset = DeviceBay.objects.all()
|
||||||
serializer_class = serializers.DeviceBaySerializer
|
serializer_class = serializers.DeviceBaySerializer
|
||||||
filterset_class = filtersets.DeviceBayFilterSet
|
filterset_class = filtersets.DeviceBayFilterSet
|
||||||
brief_prefetch_fields = ['device']
|
|
||||||
|
|
||||||
|
|
||||||
class InventoryItemViewSet(MPTTLockedMixin, NetBoxModelViewSet):
|
class InventoryItemViewSet(MPTTLockedMixin, NetBoxModelViewSet):
|
||||||
queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer', 'tags')
|
queryset = InventoryItem.objects.all()
|
||||||
serializer_class = serializers.InventoryItemSerializer
|
serializer_class = serializers.InventoryItemSerializer
|
||||||
filterset_class = filtersets.InventoryItemFilterSet
|
filterset_class = filtersets.InventoryItemFilterSet
|
||||||
brief_prefetch_fields = ['device']
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -535,9 +484,7 @@ class InventoryItemViewSet(MPTTLockedMixin, NetBoxModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class InventoryItemRoleViewSet(NetBoxModelViewSet):
|
class InventoryItemRoleViewSet(NetBoxModelViewSet):
|
||||||
queryset = InventoryItemRole.objects.prefetch_related('tags').annotate(
|
queryset = InventoryItemRole.objects.all()
|
||||||
inventoryitem_count=count_related(InventoryItem, 'role')
|
|
||||||
)
|
|
||||||
serializer_class = serializers.InventoryItemRoleSerializer
|
serializer_class = serializers.InventoryItemRoleSerializer
|
||||||
filterset_class = filtersets.InventoryItemRoleFilterSet
|
filterset_class = filtersets.InventoryItemRoleFilterSet
|
||||||
|
|
||||||
@ -554,7 +501,7 @@ class CableViewSet(NetBoxModelViewSet):
|
|||||||
|
|
||||||
class CableTerminationViewSet(NetBoxModelViewSet):
|
class CableTerminationViewSet(NetBoxModelViewSet):
|
||||||
metadata_class = ContentTypeMetadata
|
metadata_class = ContentTypeMetadata
|
||||||
queryset = CableTermination.objects.prefetch_related('cable', 'termination')
|
queryset = CableTermination.objects.all()
|
||||||
serializer_class = serializers.CableTerminationSerializer
|
serializer_class = serializers.CableTerminationSerializer
|
||||||
filterset_class = filtersets.CableTerminationFilterSet
|
filterset_class = filtersets.CableTerminationFilterSet
|
||||||
|
|
||||||
@ -564,10 +511,9 @@ class CableTerminationViewSet(NetBoxModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class VirtualChassisViewSet(NetBoxModelViewSet):
|
class VirtualChassisViewSet(NetBoxModelViewSet):
|
||||||
queryset = VirtualChassis.objects.prefetch_related('tags')
|
queryset = VirtualChassis.objects.all()
|
||||||
serializer_class = serializers.VirtualChassisSerializer
|
serializer_class = serializers.VirtualChassisSerializer
|
||||||
filterset_class = filtersets.VirtualChassisFilterSet
|
filterset_class = filtersets.VirtualChassisFilterSet
|
||||||
brief_prefetch_fields = ['master']
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -575,11 +521,7 @@ class VirtualChassisViewSet(NetBoxModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class PowerPanelViewSet(NetBoxModelViewSet):
|
class PowerPanelViewSet(NetBoxModelViewSet):
|
||||||
queryset = PowerPanel.objects.prefetch_related(
|
queryset = PowerPanel.objects.all()
|
||||||
'site', 'location'
|
|
||||||
).annotate(
|
|
||||||
powerfeed_count=count_related(PowerFeed, 'power_panel')
|
|
||||||
)
|
|
||||||
serializer_class = serializers.PowerPanelSerializer
|
serializer_class = serializers.PowerPanelSerializer
|
||||||
filterset_class = filtersets.PowerPanelFilterSet
|
filterset_class = filtersets.PowerPanelFilterSet
|
||||||
|
|
||||||
@ -590,7 +532,7 @@ class PowerPanelViewSet(NetBoxModelViewSet):
|
|||||||
|
|
||||||
class PowerFeedViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
class PowerFeedViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||||
queryset = PowerFeed.objects.prefetch_related(
|
queryset = PowerFeed.objects.prefetch_related(
|
||||||
'power_panel', 'rack', '_path', 'cable__terminations', 'tags'
|
'_path', 'cable__terminations',
|
||||||
)
|
)
|
||||||
serializer_class = serializers.PowerFeedSerializer
|
serializer_class = serializers.PowerFeedSerializer
|
||||||
filterset_class = filtersets.PowerFeedFilterSet
|
filterset_class = filtersets.PowerFeedFilterSet
|
||||||
|
@ -8,9 +8,13 @@ class DCIMConfig(AppConfig):
|
|||||||
verbose_name = "DCIM"
|
verbose_name = "DCIM"
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
|
from netbox.models.features import register_models
|
||||||
|
from utilities.counters import connect_counters
|
||||||
from . import signals, search
|
from . import signals, search
|
||||||
from .models import CableTermination, Device, DeviceType, VirtualChassis
|
from .models import CableTermination, Device, DeviceType, VirtualChassis
|
||||||
from utilities.counters import connect_counters
|
|
||||||
|
# Register models
|
||||||
|
register_models(*self.get_models())
|
||||||
|
|
||||||
# Register denormalized fields
|
# Register denormalized fields
|
||||||
denormalized.register(CableTermination, '_device', {
|
denormalized.register(CableTermination, '_device', {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from django.contrib.postgres.fields import ArrayField
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
from netaddr import AddrFormatError, EUI, eui64_unix_expanded, mac_unix_expanded
|
from netaddr import AddrFormatError, EUI, eui64_unix_expanded, mac_unix_expanded
|
||||||
|
|
||||||
from .lookups import PathContains
|
from .lookups import PathContains
|
||||||
@ -41,7 +42,7 @@ class MACAddressField(models.Field):
|
|||||||
try:
|
try:
|
||||||
return EUI(value, version=48, dialect=mac_unix_expanded_uppercase)
|
return EUI(value, version=48, dialect=mac_unix_expanded_uppercase)
|
||||||
except AddrFormatError:
|
except AddrFormatError:
|
||||||
raise ValidationError(f"Invalid MAC address format: {value}")
|
raise ValidationError(_("Invalid MAC address format: {value}").format(value=value))
|
||||||
|
|
||||||
def db_type(self, connection):
|
def db_type(self, connection):
|
||||||
return 'macaddr'
|
return 'macaddr'
|
||||||
@ -67,7 +68,7 @@ class WWNField(models.Field):
|
|||||||
try:
|
try:
|
||||||
return EUI(value, version=64, dialect=eui64_unix_expanded_uppercase)
|
return EUI(value, version=64, dialect=eui64_unix_expanded_uppercase)
|
||||||
except AddrFormatError:
|
except AddrFormatError:
|
||||||
raise ValidationError(f"Invalid WWN format: {value}")
|
raise ValidationError(_("Invalid WWN format: {value}").format(value=value))
|
||||||
|
|
||||||
def db_type(self, connection):
|
def db_type(self, connection):
|
||||||
return 'macaddr8'
|
return 'macaddr8'
|
||||||
|
@ -2,6 +2,8 @@ import django_filters
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
from drf_spectacular.types import OpenApiTypes
|
||||||
|
from drf_spectacular.utils import extend_schema_field
|
||||||
|
|
||||||
from circuits.models import CircuitTermination
|
from circuits.models import CircuitTermination
|
||||||
from extras.filtersets import LocalConfigContextFilterSet
|
from extras.filtersets import LocalConfigContextFilterSet
|
||||||
@ -818,6 +820,10 @@ class PlatformFilterSet(OrganizationalModelFilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label=_('Manufacturer (slug)'),
|
label=_('Manufacturer (slug)'),
|
||||||
)
|
)
|
||||||
|
available_for_device_type = django_filters.ModelChoiceFilter(
|
||||||
|
queryset=DeviceType.objects.all(),
|
||||||
|
method='get_for_device_type'
|
||||||
|
)
|
||||||
config_template_id = django_filters.ModelMultipleChoiceFilter(
|
config_template_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=ConfigTemplate.objects.all(),
|
queryset=ConfigTemplate.objects.all(),
|
||||||
label=_('Config template (ID)'),
|
label=_('Config template (ID)'),
|
||||||
@ -827,6 +833,14 @@ class PlatformFilterSet(OrganizationalModelFilterSet):
|
|||||||
model = Platform
|
model = Platform
|
||||||
fields = ['id', 'name', 'slug', 'description']
|
fields = ['id', 'name', 'slug', 'description']
|
||||||
|
|
||||||
|
@extend_schema_field(OpenApiTypes.STR)
|
||||||
|
def get_for_device_type(self, queryset, name, value):
|
||||||
|
"""
|
||||||
|
Return all Platforms available for a specific manufacturer based on device type and Platforms not assigned any
|
||||||
|
manufacturer
|
||||||
|
"""
|
||||||
|
return queryset.filter(Q(manufacturer=None) | Q(manufacturer__device_types=value))
|
||||||
|
|
||||||
|
|
||||||
class DeviceFilterSet(
|
class DeviceFilterSet(
|
||||||
NetBoxModelFilterSet,
|
NetBoxModelFilterSet,
|
||||||
|
@ -557,6 +557,9 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
label=_('Device type'),
|
label=_('Device type'),
|
||||||
queryset=DeviceType.objects.all(),
|
queryset=DeviceType.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
|
context={
|
||||||
|
'parent': 'manufacturer',
|
||||||
|
},
|
||||||
query_params={
|
query_params={
|
||||||
'manufacturer_id': '$manufacturer'
|
'manufacturer_id': '$manufacturer'
|
||||||
}
|
}
|
||||||
@ -640,6 +643,9 @@ class ModuleBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
required=False,
|
required=False,
|
||||||
query_params={
|
query_params={
|
||||||
'manufacturer_id': '$manufacturer'
|
'manufacturer_id': '$manufacturer'
|
||||||
|
},
|
||||||
|
context={
|
||||||
|
'parent': 'manufacturer',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
status = forms.ChoiceField(
|
status = forms.ChoiceField(
|
||||||
|
@ -159,6 +159,14 @@ class LocationImportForm(NetBoxModelImportForm):
|
|||||||
model = Location
|
model = Location
|
||||||
fields = ('site', 'parent', 'name', 'slug', 'status', 'tenant', 'description', 'tags')
|
fields = ('site', 'parent', 'name', 'slug', 'status', 'tenant', 'description', 'tags')
|
||||||
|
|
||||||
|
def __init__(self, data=None, *args, **kwargs):
|
||||||
|
super().__init__(data, *args, **kwargs)
|
||||||
|
|
||||||
|
if data:
|
||||||
|
# Limit location queryset by assigned site
|
||||||
|
params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
|
||||||
|
self.fields['parent'].queryset = self.fields['parent'].queryset.filter(**params)
|
||||||
|
|
||||||
|
|
||||||
class RackRoleImportForm(NetBoxModelImportForm):
|
class RackRoleImportForm(NetBoxModelImportForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
@ -870,7 +878,11 @@ class InterfaceImportForm(NetBoxModelImportForm):
|
|||||||
def clean_vdcs(self):
|
def clean_vdcs(self):
|
||||||
for vdc in self.cleaned_data['vdcs']:
|
for vdc in self.cleaned_data['vdcs']:
|
||||||
if vdc.device != self.cleaned_data['device']:
|
if vdc.device != self.cleaned_data['device']:
|
||||||
raise forms.ValidationError(f"VDC {vdc} is not assigned to device {self.cleaned_data['device']}")
|
raise forms.ValidationError(
|
||||||
|
_("VDC {vdc} is not assigned to device {device}").format(
|
||||||
|
vdc=vdc, device=self.cleaned_data['device']
|
||||||
|
)
|
||||||
|
)
|
||||||
return self.cleaned_data['vdcs']
|
return self.cleaned_data['vdcs']
|
||||||
|
|
||||||
|
|
||||||
@ -996,7 +1008,7 @@ class DeviceBayImportForm(NetBoxModelImportForm):
|
|||||||
device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD
|
device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD
|
||||||
).exclude(pk=device.pk)
|
).exclude(pk=device.pk)
|
||||||
else:
|
else:
|
||||||
self.fields['installed_device'].queryset = Interface.objects.none()
|
self.fields['installed_device'].queryset = Device.objects.none()
|
||||||
|
|
||||||
|
|
||||||
class InventoryItemImportForm(NetBoxModelImportForm):
|
class InventoryItemImportForm(NetBoxModelImportForm):
|
||||||
@ -1075,7 +1087,11 @@ class InventoryItemImportForm(NetBoxModelImportForm):
|
|||||||
component = model.objects.get(device=device, name=component_name)
|
component = model.objects.get(device=device, name=component_name)
|
||||||
self.instance.component = component
|
self.instance.component = component
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
raise forms.ValidationError(f"Component not found: {device} - {component_name}")
|
raise forms.ValidationError(
|
||||||
|
_("Component not found: {device} - {component_name}").format(
|
||||||
|
device=device, component_name=component_name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -1193,10 +1209,17 @@ class CableImportForm(NetBoxModelImportForm):
|
|||||||
else:
|
else:
|
||||||
termination_object = model.objects.get(device=device, name=name)
|
termination_object = model.objects.get(device=device, name=name)
|
||||||
if termination_object.cable is not None and termination_object.cable != self.instance:
|
if termination_object.cable is not None and termination_object.cable != self.instance:
|
||||||
raise forms.ValidationError(f"Side {side.upper()}: {device} {termination_object} is already connected")
|
raise forms.ValidationError(
|
||||||
|
_("Side {side_upper}: {device} {termination_object} is already connected").format(
|
||||||
|
side_upper=side.upper(), device=device, termination_object=termination_object
|
||||||
|
)
|
||||||
|
)
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
raise forms.ValidationError(f"{side.upper()} side termination not found: {device} {name}")
|
raise forms.ValidationError(
|
||||||
|
_("{side_upper} side termination not found: {device} {name}").format(
|
||||||
|
side_upper=side.upper(), device=device, name=name
|
||||||
|
)
|
||||||
|
)
|
||||||
setattr(self.instance, f'{side}_terminations', [termination_object])
|
setattr(self.instance, f'{side}_terminations', [termination_object])
|
||||||
return termination_object
|
return termination_object
|
||||||
|
|
||||||
|
@ -30,7 +30,9 @@ def get_cable_form(a_type, b_type):
|
|||||||
attrs[f'{cable_end}_terminations'] = DynamicModelMultipleChoiceField(
|
attrs[f'{cable_end}_terminations'] = DynamicModelMultipleChoiceField(
|
||||||
queryset=term_cls.objects.all(),
|
queryset=term_cls.objects.all(),
|
||||||
label=term_cls._meta.verbose_name.title(),
|
label=term_cls._meta.verbose_name.title(),
|
||||||
disabled_indicator='_occupied',
|
context={
|
||||||
|
'disabled': '_occupied',
|
||||||
|
},
|
||||||
query_params={
|
query_params={
|
||||||
'device_id': f'$termination_{cable_end}_device',
|
'device_id': f'$termination_{cable_end}_device',
|
||||||
'kind': 'physical', # Exclude virtual interfaces
|
'kind': 'physical', # Exclude virtual interfaces
|
||||||
@ -52,7 +54,9 @@ def get_cable_form(a_type, b_type):
|
|||||||
attrs[f'{cable_end}_terminations'] = DynamicModelMultipleChoiceField(
|
attrs[f'{cable_end}_terminations'] = DynamicModelMultipleChoiceField(
|
||||||
queryset=term_cls.objects.all(),
|
queryset=term_cls.objects.all(),
|
||||||
label=_('Power Feed'),
|
label=_('Power Feed'),
|
||||||
disabled_indicator='_occupied',
|
context={
|
||||||
|
'disabled': '_occupied',
|
||||||
|
},
|
||||||
query_params={
|
query_params={
|
||||||
'power_panel_id': f'$termination_{cable_end}_powerpanel',
|
'power_panel_id': f'$termination_{cable_end}_powerpanel',
|
||||||
}
|
}
|
||||||
@ -72,7 +76,9 @@ def get_cable_form(a_type, b_type):
|
|||||||
attrs[f'{cable_end}_terminations'] = DynamicModelMultipleChoiceField(
|
attrs[f'{cable_end}_terminations'] = DynamicModelMultipleChoiceField(
|
||||||
queryset=term_cls.objects.all(),
|
queryset=term_cls.objects.all(),
|
||||||
label=_('Side'),
|
label=_('Side'),
|
||||||
disabled_indicator='_occupied',
|
context={
|
||||||
|
'disabled': '_occupied',
|
||||||
|
},
|
||||||
query_params={
|
query_params={
|
||||||
'circuit_id': f'$termination_{cable_end}_circuit',
|
'circuit_id': f'$termination_{cable_end}_circuit',
|
||||||
}
|
}
|
||||||
|
@ -291,7 +291,11 @@ class DeviceTypeForm(NetBoxModelForm):
|
|||||||
default_platform = DynamicModelChoiceField(
|
default_platform = DynamicModelChoiceField(
|
||||||
label=_('Default platform'),
|
label=_('Default platform'),
|
||||||
queryset=Platform.objects.all(),
|
queryset=Platform.objects.all(),
|
||||||
required=False
|
required=False,
|
||||||
|
selector=True,
|
||||||
|
query_params={
|
||||||
|
'manufacturer_id': ['$manufacturer', 'null'],
|
||||||
|
}
|
||||||
)
|
)
|
||||||
slug = SlugField(
|
slug = SlugField(
|
||||||
label=_('Slug'),
|
label=_('Slug'),
|
||||||
@ -426,7 +430,7 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
|
|||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url='/api/dcim/racks/{{rack}}/elevation/',
|
api_url='/api/dcim/racks/{{rack}}/elevation/',
|
||||||
attrs={
|
attrs={
|
||||||
'disabled-indicator': 'device',
|
'ts-disabled-field': 'device',
|
||||||
'data-dynamic-params': '[{"fieldName":"face","queryParam":"face"}]'
|
'data-dynamic-params': '[{"fieldName":"face","queryParam":"face"}]'
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -434,6 +438,9 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
|
|||||||
device_type = DynamicModelChoiceField(
|
device_type = DynamicModelChoiceField(
|
||||||
label=_('Device type'),
|
label=_('Device type'),
|
||||||
queryset=DeviceType.objects.all(),
|
queryset=DeviceType.objects.all(),
|
||||||
|
context={
|
||||||
|
'parent': 'manufacturer',
|
||||||
|
},
|
||||||
selector=True
|
selector=True
|
||||||
)
|
)
|
||||||
role = DynamicModelChoiceField(
|
role = DynamicModelChoiceField(
|
||||||
@ -444,7 +451,10 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
|
|||||||
label=_('Platform'),
|
label=_('Platform'),
|
||||||
queryset=Platform.objects.all(),
|
queryset=Platform.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
selector=True
|
selector=True,
|
||||||
|
query_params={
|
||||||
|
'available_for_device_type': '$device_type',
|
||||||
|
}
|
||||||
)
|
)
|
||||||
cluster = DynamicModelChoiceField(
|
cluster = DynamicModelChoiceField(
|
||||||
label=_('Cluster'),
|
label=_('Cluster'),
|
||||||
@ -461,6 +471,9 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
|
|||||||
label=_('Virtual chassis'),
|
label=_('Virtual chassis'),
|
||||||
queryset=VirtualChassis.objects.all(),
|
queryset=VirtualChassis.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
|
context={
|
||||||
|
'parent': 'master',
|
||||||
|
},
|
||||||
selector=True
|
selector=True
|
||||||
)
|
)
|
||||||
vc_position = forms.IntegerField(
|
vc_position = forms.IntegerField(
|
||||||
@ -568,6 +581,9 @@ class ModuleForm(ModuleCommonForm, NetBoxModelForm):
|
|||||||
module_type = DynamicModelChoiceField(
|
module_type = DynamicModelChoiceField(
|
||||||
label=_('Module type'),
|
label=_('Module type'),
|
||||||
queryset=ModuleType.objects.all(),
|
queryset=ModuleType.objects.all(),
|
||||||
|
context={
|
||||||
|
'parent': 'manufacturer',
|
||||||
|
},
|
||||||
selector=True
|
selector=True
|
||||||
)
|
)
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
@ -774,7 +790,10 @@ class VCMemberSelectForm(forms.Form):
|
|||||||
class ComponentTemplateForm(forms.ModelForm):
|
class ComponentTemplateForm(forms.ModelForm):
|
||||||
device_type = DynamicModelChoiceField(
|
device_type = DynamicModelChoiceField(
|
||||||
label=_('Device type'),
|
label=_('Device type'),
|
||||||
queryset=DeviceType.objects.all()
|
queryset=DeviceType.objects.all(),
|
||||||
|
context={
|
||||||
|
'parent': 'manufacturer',
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -789,12 +808,18 @@ class ModularComponentTemplateForm(ComponentTemplateForm):
|
|||||||
device_type = DynamicModelChoiceField(
|
device_type = DynamicModelChoiceField(
|
||||||
label=_('Device type'),
|
label=_('Device type'),
|
||||||
queryset=DeviceType.objects.all().all(),
|
queryset=DeviceType.objects.all().all(),
|
||||||
required=False
|
required=False,
|
||||||
|
context={
|
||||||
|
'parent': 'manufacturer',
|
||||||
|
}
|
||||||
)
|
)
|
||||||
module_type = DynamicModelChoiceField(
|
module_type = DynamicModelChoiceField(
|
||||||
label=_('Module type'),
|
label=_('Module type'),
|
||||||
queryset=ModuleType.objects.all(),
|
queryset=ModuleType.objects.all(),
|
||||||
required=False
|
required=False,
|
||||||
|
context={
|
||||||
|
'parent': 'manufacturer',
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
@ -233,7 +233,7 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='powerfeed',
|
model_name='powerfeed',
|
||||||
name='rack',
|
name='rack',
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.rack'),
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='powerfeeds', to='dcim.rack'),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='powerfeed',
|
model_name='powerfeed',
|
||||||
|
@ -9,7 +9,7 @@ from django.dispatch import Signal
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from core.models import ContentType
|
from core.models import ObjectType
|
||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.fields import PathField
|
from dcim.fields import PathField
|
||||||
@ -160,25 +160,26 @@ class Cable(PrimaryModel):
|
|||||||
|
|
||||||
# Validate length and length_unit
|
# Validate length and length_unit
|
||||||
if self.length is not None and not self.length_unit:
|
if self.length is not None and not self.length_unit:
|
||||||
raise ValidationError("Must specify a unit when setting a cable length")
|
raise ValidationError(_("Must specify a unit when setting a cable length"))
|
||||||
|
|
||||||
if self.pk is None and (not self.a_terminations or not self.b_terminations):
|
if self.pk is None and (not self.a_terminations or not self.b_terminations):
|
||||||
raise ValidationError("Must define A and B terminations when creating a new cable.")
|
raise ValidationError(_("Must define A and B terminations when creating a new cable."))
|
||||||
|
|
||||||
if self._terminations_modified:
|
if self._terminations_modified:
|
||||||
|
|
||||||
# Check that all termination objects for either end are of the same type
|
# Check that all termination objects for either end are of the same type
|
||||||
for terms in (self.a_terminations, self.b_terminations):
|
for terms in (self.a_terminations, self.b_terminations):
|
||||||
if len(terms) > 1 and not all(isinstance(t, type(terms[0])) for t in terms[1:]):
|
if len(terms) > 1 and not all(isinstance(t, type(terms[0])) for t in terms[1:]):
|
||||||
raise ValidationError("Cannot connect different termination types to same end of cable.")
|
raise ValidationError(_("Cannot connect different termination types to same end of cable."))
|
||||||
|
|
||||||
# Check that termination types are compatible
|
# Check that termination types are compatible
|
||||||
if self.a_terminations and self.b_terminations:
|
if self.a_terminations and self.b_terminations:
|
||||||
a_type = self.a_terminations[0]._meta.model_name
|
a_type = self.a_terminations[0]._meta.model_name
|
||||||
b_type = self.b_terminations[0]._meta.model_name
|
b_type = self.b_terminations[0]._meta.model_name
|
||||||
if b_type not in COMPATIBLE_TERMINATION_TYPES.get(a_type):
|
if b_type not in COMPATIBLE_TERMINATION_TYPES.get(a_type):
|
||||||
raise ValidationError(f"Incompatible termination types: {a_type} and {b_type}")
|
raise ValidationError(
|
||||||
|
_("Incompatible termination types: {type_a} and {type_b}").format(type_a=a_type, type_b=b_type)
|
||||||
|
)
|
||||||
if a_type == b_type:
|
if a_type == b_type:
|
||||||
# can't directly use self.a_terminations here as possible they
|
# can't directly use self.a_terminations here as possible they
|
||||||
# don't have pk yet
|
# don't have pk yet
|
||||||
@ -327,17 +328,24 @@ class CableTermination(ChangeLoggedModel):
|
|||||||
existing_termination = qs.first()
|
existing_termination = qs.first()
|
||||||
if existing_termination is not None:
|
if existing_termination is not None:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
f"Duplicate termination found for {self.termination_type.app_label}.{self.termination_type.model} "
|
_("Duplicate termination found for {app_label}.{model} {termination_id}: cable {cable_pk}".format(
|
||||||
f"{self.termination_id}: cable {existing_termination.cable.pk}"
|
app_label=self.termination_type.app_label,
|
||||||
|
model=self.termination_type.model,
|
||||||
|
termination_id=self.termination_id,
|
||||||
|
cable_pk=existing_termination.cable.pk
|
||||||
|
))
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate interface type (if applicable)
|
# Validate interface type (if applicable)
|
||||||
if self.termination_type.model == 'interface' and self.termination.type in NONCONNECTABLE_IFACE_TYPES:
|
if self.termination_type.model == 'interface' and self.termination.type in NONCONNECTABLE_IFACE_TYPES:
|
||||||
raise ValidationError(f"Cables cannot be terminated to {self.termination.get_type_display()} interfaces")
|
raise ValidationError(
|
||||||
|
_("Cables cannot be terminated to {type_display} interfaces").format(
|
||||||
|
type_display=self.termination.get_type_display()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# A CircuitTermination attached to a ProviderNetwork cannot have a Cable
|
# A CircuitTermination attached to a ProviderNetwork cannot have a Cable
|
||||||
if self.termination_type.model == 'circuittermination' and self.termination.provider_network is not None:
|
if self.termination_type.model == 'circuittermination' and self.termination.provider_network is not None:
|
||||||
raise ValidationError("Circuit terminations attached to a provider network may not be cabled.")
|
raise ValidationError(_("Circuit terminations attached to a provider network may not be cabled."))
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
@ -473,13 +481,13 @@ class CablePath(models.Model):
|
|||||||
def origin_type(self):
|
def origin_type(self):
|
||||||
if self.path:
|
if self.path:
|
||||||
ct_id, _ = decompile_path_node(self.path[0][0])
|
ct_id, _ = decompile_path_node(self.path[0][0])
|
||||||
return ContentType.objects.get_for_id(ct_id)
|
return ObjectType.objects.get_for_id(ct_id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def destination_type(self):
|
def destination_type(self):
|
||||||
if self.is_complete:
|
if self.is_complete:
|
||||||
ct_id, _ = decompile_path_node(self.path[-1][0])
|
ct_id, _ = decompile_path_node(self.path[-1][0])
|
||||||
return ContentType.objects.get_for_id(ct_id)
|
return ObjectType.objects.get_for_id(ct_id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_objects(self):
|
def path_objects(self):
|
||||||
@ -586,7 +594,7 @@ class CablePath(models.Model):
|
|||||||
|
|
||||||
# Step 6: Determine the far-end terminations
|
# Step 6: Determine the far-end terminations
|
||||||
if isinstance(links[0], Cable):
|
if isinstance(links[0], Cable):
|
||||||
termination_type = ContentType.objects.get_for_model(terminations[0])
|
termination_type = ObjectType.objects.get_for_model(terminations[0])
|
||||||
local_cable_terminations = CableTermination.objects.filter(
|
local_cable_terminations = CableTermination.objects.filter(
|
||||||
termination_type=termination_type,
|
termination_type=termination_type,
|
||||||
termination_id__in=[t.pk for t in terminations]
|
termination_id__in=[t.pk for t in terminations]
|
||||||
@ -739,7 +747,7 @@ class CablePath(models.Model):
|
|||||||
# Prefetch path objects using one query per model type. Prefetch related devices where appropriate.
|
# Prefetch path objects using one query per model type. Prefetch related devices where appropriate.
|
||||||
prefetched = {}
|
prefetched = {}
|
||||||
for ct_id, object_ids in to_prefetch.items():
|
for ct_id, object_ids in to_prefetch.items():
|
||||||
model_class = ContentType.objects.get_for_id(ct_id).model_class()
|
model_class = ObjectType.objects.get_for_id(ct_id).model_class()
|
||||||
queryset = model_class.objects.filter(pk__in=object_ids)
|
queryset = model_class.objects.filter(pk__in=object_ids)
|
||||||
if hasattr(model_class, 'device'):
|
if hasattr(model_class, 'device'):
|
||||||
queryset = queryset.prefetch_related('device')
|
queryset = queryset.prefetch_related('device')
|
||||||
@ -766,7 +774,7 @@ class CablePath(models.Model):
|
|||||||
"""
|
"""
|
||||||
Return all Cable IDs within the path.
|
Return all Cable IDs within the path.
|
||||||
"""
|
"""
|
||||||
cable_ct = ContentType.objects.get_for_model(Cable).pk
|
cable_ct = ObjectType.objects.get_for_model(Cable).pk
|
||||||
cable_ids = []
|
cable_ids = []
|
||||||
|
|
||||||
for node in self._nodes:
|
for node in self._nodes:
|
||||||
|
@ -1133,13 +1133,13 @@ class DeviceBay(ComponentModel, TrackingModelMixin):
|
|||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
# Validate that the parent Device can have DeviceBays
|
# Validate that the parent Device can have DeviceBays
|
||||||
if not self.device.device_type.is_parent_device:
|
if hasattr(self, 'device') and not self.device.device_type.is_parent_device:
|
||||||
raise ValidationError(_("This type of device ({device_type}) does not support device bays.").format(
|
raise ValidationError(_("This type of device ({device_type}) does not support device bays.").format(
|
||||||
device_type=self.device.device_type
|
device_type=self.device.device_type
|
||||||
))
|
))
|
||||||
|
|
||||||
# Cannot install a device into itself, obviously
|
# Cannot install a device into itself, obviously
|
||||||
if self.device == self.installed_device:
|
if self.installed_device and getattr(self, 'device', None) == self.installed_device:
|
||||||
raise ValidationError(_("Cannot install a device into itself."))
|
raise ValidationError(_("Cannot install a device into itself."))
|
||||||
|
|
||||||
# Check that the installed device is not already installed elsewhere
|
# Check that the installed device is not already installed elsewhere
|
||||||
|
@ -815,20 +815,6 @@ class Device(
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('dcim:device', args=[self.pk])
|
return reverse('dcim:device', args=[self.pk])
|
||||||
|
|
||||||
@property
|
|
||||||
def device_role(self):
|
|
||||||
"""
|
|
||||||
For backwards compatibility with pre-v3.6 code expecting a device_role to be present on Device.
|
|
||||||
"""
|
|
||||||
return self.role
|
|
||||||
|
|
||||||
@device_role.setter
|
|
||||||
def device_role(self, value):
|
|
||||||
"""
|
|
||||||
For backwards compatibility with pre-v3.6 code expecting a device_role to be present on Device.
|
|
||||||
"""
|
|
||||||
self.role = value
|
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
@ -875,7 +861,7 @@ class Device(
|
|||||||
if self.position and self.device_type.u_height == 0:
|
if self.position and self.device_type.u_height == 0:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'position': _(
|
'position': _(
|
||||||
"A U0 device type ({device_type}) cannot be assigned to a rack position."
|
"A 0U device type ({device_type}) cannot be assigned to a rack position."
|
||||||
).format(device_type=self.device_type)
|
).format(device_type=self.device_type)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -84,6 +84,7 @@ class PowerFeed(PrimaryModel, PathEndpoint, CabledObjectModel):
|
|||||||
rack = models.ForeignKey(
|
rack = models.ForeignKey(
|
||||||
to='Rack',
|
to='Rack',
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
|
related_name='powerfeeds',
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
|
@ -359,6 +359,11 @@ class CableTerminationTable(NetBoxTable):
|
|||||||
verbose_name=_('Mark Connected'),
|
verbose_name=_('Mark Connected'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def value_link_peer(self, value):
|
||||||
|
return ', '.join([
|
||||||
|
f"{termination.parent_object} > {termination}" for termination in value
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
class PathEndpointTable(CableTerminationTable):
|
class PathEndpointTable(CableTerminationTable):
|
||||||
connection = columns.TemplateColumn(
|
connection = columns.TemplateColumn(
|
||||||
|
@ -36,7 +36,7 @@ DEVICEBAY_STATUS = """
|
|||||||
|
|
||||||
INTERFACE_IPADDRESSES = """
|
INTERFACE_IPADDRESSES = """
|
||||||
{% if value.count > 3 %}
|
{% if value.count > 3 %}
|
||||||
<a href="{% url 'ipam:ipaddress_list' %}?interface_id={{ record.pk }}">{{ value.count }}</a>
|
<a href="{% url 'ipam:ipaddress_list' %}?{{ record|meta:"model_name" }}_id={{ record.pk }}">{{ value.count }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% for ip in value.all %}
|
{% for ip in value.all %}
|
||||||
{% if ip.status != 'active' %}
|
{% if ip.status != 'active' %}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
@ -45,7 +46,7 @@ class Mixins:
|
|||||||
name='Peer Device'
|
name='Peer Device'
|
||||||
)
|
)
|
||||||
if self.peer_termination_type is None:
|
if self.peer_termination_type is None:
|
||||||
raise NotImplementedError("Test case must set peer_termination_type")
|
raise NotImplementedError(_("Test case must set peer_termination_type"))
|
||||||
peer_obj = self.peer_termination_type.objects.create(
|
peer_obj = self.peer_termination_type.objects.create(
|
||||||
device=peer_device,
|
device=peer_device,
|
||||||
name='Peer Termination'
|
name='Peer Termination'
|
||||||
@ -67,7 +68,7 @@ class Mixins:
|
|||||||
|
|
||||||
class RegionTest(APIViewTestCases.APIViewTestCase):
|
class RegionTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = Region
|
model = Region
|
||||||
brief_fields = ['_depth', 'display', 'id', 'name', 'site_count', 'slug', 'url']
|
brief_fields = ['_depth', 'description', 'display', 'id', 'name', 'site_count', 'slug', 'url']
|
||||||
create_data = [
|
create_data = [
|
||||||
{
|
{
|
||||||
'name': 'Region 4',
|
'name': 'Region 4',
|
||||||
@ -96,7 +97,7 @@ class RegionTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class SiteGroupTest(APIViewTestCases.APIViewTestCase):
|
class SiteGroupTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = SiteGroup
|
model = SiteGroup
|
||||||
brief_fields = ['_depth', 'display', 'id', 'name', 'site_count', 'slug', 'url']
|
brief_fields = ['_depth', 'description', 'display', 'id', 'name', 'site_count', 'slug', 'url']
|
||||||
create_data = [
|
create_data = [
|
||||||
{
|
{
|
||||||
'name': 'Site Group 4',
|
'name': 'Site Group 4',
|
||||||
@ -125,7 +126,7 @@ class SiteGroupTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class SiteTest(APIViewTestCases.APIViewTestCase):
|
class SiteTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = Site
|
model = Site
|
||||||
brief_fields = ['display', 'id', 'name', 'slug', 'url']
|
brief_fields = ['description', 'display', 'id', 'name', 'slug', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'status': 'planned',
|
'status': 'planned',
|
||||||
}
|
}
|
||||||
@ -187,7 +188,7 @@ class SiteTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class LocationTest(APIViewTestCases.APIViewTestCase):
|
class LocationTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = Location
|
model = Location
|
||||||
brief_fields = ['_depth', 'display', 'id', 'name', 'rack_count', 'slug', 'url']
|
brief_fields = ['_depth', 'description', 'display', 'id', 'name', 'rack_count', 'slug', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
}
|
}
|
||||||
@ -237,7 +238,7 @@ class LocationTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class RackRoleTest(APIViewTestCases.APIViewTestCase):
|
class RackRoleTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = RackRole
|
model = RackRole
|
||||||
brief_fields = ['display', 'id', 'name', 'rack_count', 'slug', 'url']
|
brief_fields = ['description', 'display', 'id', 'name', 'rack_count', 'slug', 'url']
|
||||||
create_data = [
|
create_data = [
|
||||||
{
|
{
|
||||||
'name': 'Rack Role 4',
|
'name': 'Rack Role 4',
|
||||||
@ -272,7 +273,7 @@ class RackRoleTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class RackTest(APIViewTestCases.APIViewTestCase):
|
class RackTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = Rack
|
model = Rack
|
||||||
brief_fields = ['device_count', 'display', 'id', 'name', 'url']
|
brief_fields = ['description', 'device_count', 'display', 'id', 'name', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'status': 'planned',
|
'status': 'planned',
|
||||||
}
|
}
|
||||||
@ -360,7 +361,7 @@ class RackTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class RackReservationTest(APIViewTestCases.APIViewTestCase):
|
class RackReservationTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = RackReservation
|
model = RackReservation
|
||||||
brief_fields = ['display', 'id', 'units', 'url', 'user']
|
brief_fields = ['description', 'display', 'id', 'units', 'url', 'user']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
}
|
}
|
||||||
@ -407,7 +408,7 @@ class RackReservationTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class ManufacturerTest(APIViewTestCases.APIViewTestCase):
|
class ManufacturerTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = Manufacturer
|
model = Manufacturer
|
||||||
brief_fields = ['devicetype_count', 'display', 'id', 'name', 'slug', 'url']
|
brief_fields = ['description', 'devicetype_count', 'display', 'id', 'name', 'slug', 'url']
|
||||||
create_data = [
|
create_data = [
|
||||||
{
|
{
|
||||||
'name': 'Manufacturer 4',
|
'name': 'Manufacturer 4',
|
||||||
@ -439,7 +440,7 @@ class ManufacturerTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class DeviceTypeTest(APIViewTestCases.APIViewTestCase):
|
class DeviceTypeTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
brief_fields = ['device_count', 'display', 'id', 'manufacturer', 'model', 'slug', 'url']
|
brief_fields = ['description', 'device_count', 'display', 'id', 'manufacturer', 'model', 'slug', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'part_number': 'ABC123',
|
'part_number': 'ABC123',
|
||||||
}
|
}
|
||||||
@ -484,7 +485,7 @@ class DeviceTypeTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class ModuleTypeTest(APIViewTestCases.APIViewTestCase):
|
class ModuleTypeTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = ModuleType
|
model = ModuleType
|
||||||
brief_fields = ['display', 'id', 'manufacturer', 'model', 'url']
|
brief_fields = ['description', 'display', 'id', 'manufacturer', 'model', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'part_number': 'ABC123',
|
'part_number': 'ABC123',
|
||||||
}
|
}
|
||||||
@ -523,7 +524,7 @@ class ModuleTypeTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class ConsolePortTemplateTest(APIViewTestCases.APIViewTestCase):
|
class ConsolePortTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = ConsolePortTemplate
|
model = ConsolePortTemplate
|
||||||
brief_fields = ['display', 'id', 'name', 'url']
|
brief_fields = ['description', 'display', 'id', 'name', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
}
|
}
|
||||||
@ -567,7 +568,7 @@ class ConsolePortTemplateTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class ConsoleServerPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
class ConsoleServerPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = ConsoleServerPortTemplate
|
model = ConsoleServerPortTemplate
|
||||||
brief_fields = ['display', 'id', 'name', 'url']
|
brief_fields = ['description', 'display', 'id', 'name', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
}
|
}
|
||||||
@ -611,7 +612,7 @@ class ConsoleServerPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class PowerPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
class PowerPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = PowerPortTemplate
|
model = PowerPortTemplate
|
||||||
brief_fields = ['display', 'id', 'name', 'url']
|
brief_fields = ['description', 'display', 'id', 'name', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
}
|
}
|
||||||
@ -655,7 +656,7 @@ class PowerPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class PowerOutletTemplateTest(APIViewTestCases.APIViewTestCase):
|
class PowerOutletTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = PowerOutletTemplate
|
model = PowerOutletTemplate
|
||||||
brief_fields = ['display', 'id', 'name', 'url']
|
brief_fields = ['description', 'display', 'id', 'name', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
}
|
}
|
||||||
@ -712,7 +713,7 @@ class PowerOutletTemplateTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class InterfaceTemplateTest(APIViewTestCases.APIViewTestCase):
|
class InterfaceTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = InterfaceTemplate
|
model = InterfaceTemplate
|
||||||
brief_fields = ['display', 'id', 'name', 'url']
|
brief_fields = ['description', 'display', 'id', 'name', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
}
|
}
|
||||||
@ -760,7 +761,7 @@ class InterfaceTemplateTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class FrontPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
class FrontPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = FrontPortTemplate
|
model = FrontPortTemplate
|
||||||
brief_fields = ['display', 'id', 'name', 'url']
|
brief_fields = ['description', 'display', 'id', 'name', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
}
|
}
|
||||||
@ -849,7 +850,7 @@ class FrontPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class RearPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
class RearPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = RearPortTemplate
|
model = RearPortTemplate
|
||||||
brief_fields = ['display', 'id', 'name', 'url']
|
brief_fields = ['description', 'display', 'id', 'name', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
}
|
}
|
||||||
@ -897,7 +898,7 @@ class RearPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class ModuleBayTemplateTest(APIViewTestCases.APIViewTestCase):
|
class ModuleBayTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = ModuleBayTemplate
|
model = ModuleBayTemplate
|
||||||
brief_fields = ['display', 'id', 'name', 'url']
|
brief_fields = ['description', 'display', 'id', 'name', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
}
|
}
|
||||||
@ -937,7 +938,7 @@ class ModuleBayTemplateTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class DeviceBayTemplateTest(APIViewTestCases.APIViewTestCase):
|
class DeviceBayTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = DeviceBayTemplate
|
model = DeviceBayTemplate
|
||||||
brief_fields = ['display', 'id', 'name', 'url']
|
brief_fields = ['description', 'display', 'id', 'name', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
}
|
}
|
||||||
@ -977,7 +978,7 @@ class DeviceBayTemplateTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class InventoryItemTemplateTest(APIViewTestCases.APIViewTestCase):
|
class InventoryItemTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = InventoryItemTemplate
|
model = InventoryItemTemplate
|
||||||
brief_fields = ['_depth', 'display', 'id', 'name', 'url']
|
brief_fields = ['_depth', 'description', 'display', 'id', 'name', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
}
|
}
|
||||||
@ -1028,7 +1029,7 @@ class InventoryItemTemplateTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class DeviceRoleTest(APIViewTestCases.APIViewTestCase):
|
class DeviceRoleTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = DeviceRole
|
model = DeviceRole
|
||||||
brief_fields = ['device_count', 'display', 'id', 'name', 'slug', 'url', 'virtualmachine_count']
|
brief_fields = ['description', 'device_count', 'display', 'id', 'name', 'slug', 'url', 'virtualmachine_count']
|
||||||
create_data = [
|
create_data = [
|
||||||
{
|
{
|
||||||
'name': 'Device Role 4',
|
'name': 'Device Role 4',
|
||||||
@ -1063,7 +1064,7 @@ class DeviceRoleTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class PlatformTest(APIViewTestCases.APIViewTestCase):
|
class PlatformTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = Platform
|
model = Platform
|
||||||
brief_fields = ['device_count', 'display', 'id', 'name', 'slug', 'url', 'virtualmachine_count']
|
brief_fields = ['description', 'device_count', 'display', 'id', 'name', 'slug', 'url', 'virtualmachine_count']
|
||||||
create_data = [
|
create_data = [
|
||||||
{
|
{
|
||||||
'name': 'Platform 4',
|
'name': 'Platform 4',
|
||||||
@ -1095,7 +1096,7 @@ class PlatformTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class DeviceTest(APIViewTestCases.APIViewTestCase):
|
class DeviceTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = Device
|
model = Device
|
||||||
brief_fields = ['display', 'id', 'name', 'url']
|
brief_fields = ['description', 'display', 'id', 'name', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'status': 'failed',
|
'status': 'failed',
|
||||||
}
|
}
|
||||||
@ -1285,7 +1286,7 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class ModuleTest(APIViewTestCases.APIViewTestCase):
|
class ModuleTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = Module
|
model = Module
|
||||||
brief_fields = ['device', 'display', 'id', 'module_bay', 'module_type', 'url']
|
brief_fields = ['description', 'device', 'display', 'id', 'module_bay', 'module_type', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'serial': '1234ABCD',
|
'serial': '1234ABCD',
|
||||||
}
|
}
|
||||||
@ -1349,7 +1350,7 @@ class ModuleTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class ConsolePortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
class ConsolePortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||||
model = ConsolePort
|
model = ConsolePort
|
||||||
brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
|
brief_fields = ['_occupied', 'cable', 'description', 'device', 'display', 'id', 'name', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
}
|
}
|
||||||
@ -1391,7 +1392,7 @@ class ConsolePortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCa
|
|||||||
|
|
||||||
class ConsoleServerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
class ConsoleServerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||||
model = ConsoleServerPort
|
model = ConsoleServerPort
|
||||||
brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
|
brief_fields = ['_occupied', 'cable', 'description', 'device', 'display', 'id', 'name', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
}
|
}
|
||||||
@ -1433,7 +1434,7 @@ class ConsoleServerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIView
|
|||||||
|
|
||||||
class PowerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
class PowerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||||
model = PowerPort
|
model = PowerPort
|
||||||
brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
|
brief_fields = ['_occupied', 'cable', 'description', 'device', 'display', 'id', 'name', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
}
|
}
|
||||||
@ -1472,7 +1473,7 @@ class PowerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
|
|||||||
|
|
||||||
class PowerOutletTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
class PowerOutletTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||||
model = PowerOutlet
|
model = PowerOutlet
|
||||||
brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
|
brief_fields = ['_occupied', 'cable', 'description', 'device', 'display', 'id', 'name', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
}
|
}
|
||||||
@ -1520,7 +1521,7 @@ class PowerOutletTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCa
|
|||||||
|
|
||||||
class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||||
model = Interface
|
model = Interface
|
||||||
brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
|
brief_fields = ['_occupied', 'cable', 'description', 'device', 'display', 'id', 'name', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
}
|
}
|
||||||
@ -1654,7 +1655,7 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
|
|||||||
|
|
||||||
class FrontPortTest(APIViewTestCases.APIViewTestCase):
|
class FrontPortTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = FrontPort
|
model = FrontPort
|
||||||
brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
|
brief_fields = ['_occupied', 'cable', 'description', 'device', 'display', 'id', 'name', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
}
|
}
|
||||||
@ -1712,7 +1713,7 @@ class FrontPortTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class RearPortTest(APIViewTestCases.APIViewTestCase):
|
class RearPortTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = RearPort
|
model = RearPort
|
||||||
brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
|
brief_fields = ['_occupied', 'cable', 'description', 'device', 'display', 'id', 'name', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
}
|
}
|
||||||
@ -1754,7 +1755,7 @@ class RearPortTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class ModuleBayTest(APIViewTestCases.APIViewTestCase):
|
class ModuleBayTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = ModuleBay
|
model = ModuleBay
|
||||||
brief_fields = ['display', 'id', 'module', 'name', 'url']
|
brief_fields = ['description', 'display', 'id', 'installed_module', 'name', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
}
|
}
|
||||||
@ -1793,7 +1794,7 @@ class ModuleBayTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class DeviceBayTest(APIViewTestCases.APIViewTestCase):
|
class DeviceBayTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = DeviceBay
|
model = DeviceBay
|
||||||
brief_fields = ['device', 'display', 'id', 'name', 'url']
|
brief_fields = ['description', 'device', 'display', 'id', 'name', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
}
|
}
|
||||||
@ -1856,7 +1857,7 @@ class DeviceBayTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class InventoryItemTest(APIViewTestCases.APIViewTestCase):
|
class InventoryItemTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
brief_fields = ['_depth', 'device', 'display', 'id', 'name', 'url']
|
brief_fields = ['_depth', 'description', 'device', 'display', 'id', 'name', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
}
|
}
|
||||||
@ -1916,7 +1917,7 @@ class InventoryItemTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class InventoryItemRoleTest(APIViewTestCases.APIViewTestCase):
|
class InventoryItemRoleTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = InventoryItemRole
|
model = InventoryItemRole
|
||||||
brief_fields = ['display', 'id', 'inventoryitem_count', 'name', 'slug', 'url']
|
brief_fields = ['description', 'display', 'id', 'inventoryitem_count', 'name', 'slug', 'url']
|
||||||
create_data = [
|
create_data = [
|
||||||
{
|
{
|
||||||
'name': 'Inventory Item Role 4',
|
'name': 'Inventory Item Role 4',
|
||||||
@ -1951,7 +1952,7 @@ class InventoryItemRoleTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class CableTest(APIViewTestCases.APIViewTestCase):
|
class CableTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = Cable
|
model = Cable
|
||||||
brief_fields = ['display', 'id', 'label', 'url']
|
brief_fields = ['description', 'display', 'id', 'label', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'length': 100,
|
'length': 100,
|
||||||
'length_unit': 'm',
|
'length_unit': 'm',
|
||||||
@ -2074,7 +2075,7 @@ class ConnectedDeviceTest(APITestCase):
|
|||||||
|
|
||||||
class VirtualChassisTest(APIViewTestCases.APIViewTestCase):
|
class VirtualChassisTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = VirtualChassis
|
model = VirtualChassis
|
||||||
brief_fields = ['display', 'id', 'master', 'member_count', 'name', 'url']
|
brief_fields = ['description', 'display', 'id', 'master', 'member_count', 'name', 'url']
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
@ -2155,7 +2156,7 @@ class VirtualChassisTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class PowerPanelTest(APIViewTestCases.APIViewTestCase):
|
class PowerPanelTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = PowerPanel
|
model = PowerPanel
|
||||||
brief_fields = ['display', 'id', 'name', 'powerfeed_count', 'url']
|
brief_fields = ['description', 'display', 'id', 'name', 'powerfeed_count', 'url']
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
@ -2204,7 +2205,7 @@ class PowerPanelTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class PowerFeedTest(APIViewTestCases.APIViewTestCase):
|
class PowerFeedTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = PowerFeed
|
model = PowerFeed
|
||||||
brief_fields = ['_occupied', 'cable', 'display', 'id', 'name', 'url']
|
brief_fields = ['_occupied', 'cable', 'description', 'display', 'id', 'name', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'status': 'planned',
|
'status': 'planned',
|
||||||
}
|
}
|
||||||
@ -2259,7 +2260,7 @@ class PowerFeedTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class VirtualDeviceContextTest(APIViewTestCases.APIViewTestCase):
|
class VirtualDeviceContextTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = VirtualDeviceContext
|
model = VirtualDeviceContext
|
||||||
brief_fields = ['device', 'display', 'id', 'identifier', 'name', 'url']
|
brief_fields = ['description', 'device', 'display', 'id', 'identifier', 'name', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'status': 'planned',
|
'status': 'planned',
|
||||||
}
|
}
|
||||||
|
@ -2156,7 +2156,7 @@ class CablePathTestCase(TestCase):
|
|||||||
device = Device.objects.create(
|
device = Device.objects.create(
|
||||||
site=self.site,
|
site=self.site,
|
||||||
device_type=self.device.device_type,
|
device_type=self.device.device_type,
|
||||||
device_role=self.device.device_role,
|
role=self.device.role,
|
||||||
name='Test mid-span Device'
|
name='Test mid-span Device'
|
||||||
)
|
)
|
||||||
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
||||||
|
@ -1787,6 +1787,7 @@ class PlatformTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Platform(name='Platform 1', slug='platform-1', manufacturer=manufacturers[0], description='foobar1'),
|
Platform(name='Platform 1', slug='platform-1', manufacturer=manufacturers[0], description='foobar1'),
|
||||||
Platform(name='Platform 2', slug='platform-2', manufacturer=manufacturers[1], description='foobar2'),
|
Platform(name='Platform 2', slug='platform-2', manufacturer=manufacturers[1], description='foobar2'),
|
||||||
Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturers[2], description='foobar3'),
|
Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturers[2], description='foobar3'),
|
||||||
|
Platform(name='Platform 4', slug='platform-4'),
|
||||||
)
|
)
|
||||||
Platform.objects.bulk_create(platforms)
|
Platform.objects.bulk_create(platforms)
|
||||||
|
|
||||||
@ -1813,6 +1814,17 @@ class PlatformTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'manufacturer': [manufacturers[0].slug, manufacturers[1].slug]}
|
params = {'manufacturer': [manufacturers[0].slug, manufacturers[1].slug]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_available_for_device_type(self):
|
||||||
|
manufacturers = Manufacturer.objects.all()[:2]
|
||||||
|
device_type = DeviceType.objects.create(
|
||||||
|
manufacturer=manufacturers[0],
|
||||||
|
model='Device Type 1',
|
||||||
|
slug='device-type-1',
|
||||||
|
u_height=1
|
||||||
|
)
|
||||||
|
params = {'available_for_device_type': device_type.pk}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = Device.objects.all()
|
queryset = Device.objects.all()
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from circuits.models import *
|
from circuits.models import *
|
||||||
|
from core.models import ObjectType
|
||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from extras.models import CustomField
|
from extras.models import CustomField
|
||||||
@ -293,8 +293,8 @@ class DeviceTestCase(TestCase):
|
|||||||
|
|
||||||
# Create a CustomField with a default value & assign it to all component models
|
# Create a CustomField with a default value & assign it to all component models
|
||||||
cf1 = CustomField.objects.create(name='cf1', default='foo')
|
cf1 = CustomField.objects.create(name='cf1', default='foo')
|
||||||
cf1.content_types.set(
|
cf1.object_types.set(
|
||||||
ContentType.objects.filter(app_label='dcim', model__in=[
|
ObjectType.objects.filter(app_label='dcim', model__in=[
|
||||||
'consoleport',
|
'consoleport',
|
||||||
'consoleserverport',
|
'consoleserverport',
|
||||||
'powerport',
|
'powerport',
|
||||||
@ -533,30 +533,6 @@ class DeviceTestCase(TestCase):
|
|||||||
device2.full_clean()
|
device2.full_clean()
|
||||||
device2.save()
|
device2.save()
|
||||||
|
|
||||||
def test_old_device_role_field(self):
|
|
||||||
"""
|
|
||||||
Ensure that the old device role field sets the value in the new role field.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Test getter method
|
|
||||||
device = Device(
|
|
||||||
site=Site.objects.first(),
|
|
||||||
device_type=DeviceType.objects.first(),
|
|
||||||
role=DeviceRole.objects.first(),
|
|
||||||
name='Test Device 1',
|
|
||||||
device_role=DeviceRole.objects.first()
|
|
||||||
)
|
|
||||||
device.full_clean()
|
|
||||||
device.save()
|
|
||||||
|
|
||||||
self.assertEqual(device.role, device.device_role)
|
|
||||||
|
|
||||||
# Test setter method
|
|
||||||
device.device_role = DeviceRole.objects.last()
|
|
||||||
device.full_clean()
|
|
||||||
device.save()
|
|
||||||
self.assertEqual(device.role, device.device_role)
|
|
||||||
|
|
||||||
|
|
||||||
class CableTestCase(TestCase):
|
class CableTestCase(TestCase):
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@ from zoneinfo import ZoneInfo
|
|||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from netaddr import EUI
|
from netaddr import EUI
|
||||||
@ -2982,7 +2981,6 @@ class CableTestCase(
|
|||||||
|
|
||||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
interface_ct = ContentType.objects.get_for_model(Interface)
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
# TODO: Revisit this limitation
|
# TODO: Revisit this limitation
|
||||||
# Changing terminations not supported when editing an existing Cable
|
# Changing terminations not supported when editing an existing Cable
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.utils.translation import gettext as _
|
||||||
from drf_spectacular.utils import extend_schema_field
|
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
|
from drf_spectacular.utils import extend_schema_field
|
||||||
from rest_framework.fields import Field
|
from rest_framework.fields import Field
|
||||||
from rest_framework.serializers import ValidationError
|
from rest_framework.serializers import ValidationError
|
||||||
|
|
||||||
|
from core.models import ObjectType
|
||||||
from extras.choices import CustomFieldTypeChoices
|
from extras.choices import CustomFieldTypeChoices
|
||||||
from extras.models import CustomField
|
from extras.models import CustomField
|
||||||
from netbox.constants import NESTED_SERIALIZER_PREFIX
|
|
||||||
from utilities.api import get_serializer_for_model
|
from utilities.api import get_serializer_for_model
|
||||||
|
|
||||||
|
|
||||||
@ -24,8 +24,8 @@ class CustomFieldDefaultValues:
|
|||||||
self.model = serializer_field.parent.Meta.model
|
self.model = serializer_field.parent.Meta.model
|
||||||
|
|
||||||
# Retrieve the CustomFields for the parent model
|
# Retrieve the CustomFields for the parent model
|
||||||
content_type = ContentType.objects.get_for_model(self.model)
|
object_type = ObjectType.objects.get_for_model(self.model)
|
||||||
fields = CustomField.objects.filter(content_types=content_type)
|
fields = CustomField.objects.filter(object_types=object_type)
|
||||||
|
|
||||||
# Populate the default value for each CustomField
|
# Populate the default value for each CustomField
|
||||||
value = {}
|
value = {}
|
||||||
@ -46,8 +46,8 @@ class CustomFieldsDataField(Field):
|
|||||||
Cache CustomFields assigned to this model to avoid redundant database queries
|
Cache CustomFields assigned to this model to avoid redundant database queries
|
||||||
"""
|
"""
|
||||||
if not hasattr(self, '_custom_fields'):
|
if not hasattr(self, '_custom_fields'):
|
||||||
content_type = ContentType.objects.get_for_model(self.parent.Meta.model)
|
object_type = ObjectType.objects.get_for_model(self.parent.Meta.model)
|
||||||
self._custom_fields = CustomField.objects.filter(content_types=content_type)
|
self._custom_fields = CustomField.objects.filter(object_types=object_type)
|
||||||
return self._custom_fields
|
return self._custom_fields
|
||||||
|
|
||||||
def to_representation(self, obj):
|
def to_representation(self, obj):
|
||||||
@ -57,11 +57,11 @@ class CustomFieldsDataField(Field):
|
|||||||
for cf in self._get_custom_fields():
|
for cf in self._get_custom_fields():
|
||||||
value = cf.deserialize(obj.get(cf.name))
|
value = cf.deserialize(obj.get(cf.name))
|
||||||
if value is not None and cf.type == CustomFieldTypeChoices.TYPE_OBJECT:
|
if value is not None and cf.type == CustomFieldTypeChoices.TYPE_OBJECT:
|
||||||
serializer = get_serializer_for_model(cf.object_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX)
|
serializer = get_serializer_for_model(cf.object_type.model_class())
|
||||||
value = serializer(value, context=self.parent.context).data
|
value = serializer(value, nested=True, context=self.parent.context).data
|
||||||
elif value is not None and cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
|
elif value is not None and cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
|
||||||
serializer = get_serializer_for_model(cf.object_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX)
|
serializer = get_serializer_for_model(cf.object_type.model_class())
|
||||||
value = serializer(value, many=True, context=self.parent.context).data
|
value = serializer(value, nested=True, many=True, context=self.parent.context).data
|
||||||
data[cf.name] = value
|
data[cf.name] = value
|
||||||
|
|
||||||
return data
|
return data
|
||||||
@ -79,16 +79,13 @@ class CustomFieldsDataField(Field):
|
|||||||
CustomFieldTypeChoices.TYPE_OBJECT,
|
CustomFieldTypeChoices.TYPE_OBJECT,
|
||||||
CustomFieldTypeChoices.TYPE_MULTIOBJECT
|
CustomFieldTypeChoices.TYPE_MULTIOBJECT
|
||||||
):
|
):
|
||||||
serializer_class = get_serializer_for_model(
|
serializer_class = get_serializer_for_model(cf.object_type.model_class())
|
||||||
model=cf.object_type.model_class(),
|
|
||||||
prefix=NESTED_SERIALIZER_PREFIX
|
|
||||||
)
|
|
||||||
many = cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT
|
many = cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT
|
||||||
serializer = serializer_class(data=data[cf.name], many=many, context=self.parent.context)
|
serializer = serializer_class(data=data[cf.name], nested=True, many=many, context=self.parent.context)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
data[cf.name] = [obj['id'] for obj in serializer.data] if many else serializer.data['id']
|
data[cf.name] = [obj['id'] for obj in serializer.data] if many else serializer.data['id']
|
||||||
else:
|
else:
|
||||||
raise ValidationError(f"Unknown related object(s): {data[cf.name]}")
|
raise ValidationError(_("Unknown related object(s): {name}").format(name=data[cf.name]))
|
||||||
|
|
||||||
# If updating an existing instance, start with existing custom_field_data
|
# If updating an existing instance, start with existing custom_field_data
|
||||||
if self.parent.instance:
|
if self.parent.instance:
|
||||||
|
@ -5,7 +5,7 @@ from rest_framework.response import Response
|
|||||||
from rest_framework.status import HTTP_400_BAD_REQUEST
|
from rest_framework.status import HTTP_400_BAD_REQUEST
|
||||||
|
|
||||||
from netbox.api.renderers import TextRenderer
|
from netbox.api.renderers import TextRenderer
|
||||||
from .nested_serializers import NestedConfigTemplateSerializer
|
from .serializers import ConfigTemplateSerializer
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ConfigContextQuerySetMixin',
|
'ConfigContextQuerySetMixin',
|
||||||
@ -52,7 +52,7 @@ class ConfigTemplateRenderMixin:
|
|||||||
if request.accepted_renderer.format == 'txt':
|
if request.accepted_renderer.format == 'txt':
|
||||||
return Response(output)
|
return Response(output)
|
||||||
|
|
||||||
template_serializer = NestedConfigTemplateSerializer(configtemplate, context={'request': request})
|
template_serializer = ConfigTemplateSerializer(configtemplate, nested=True, context={'request': request})
|
||||||
|
|
||||||
return Response({
|
return Response({
|
||||||
'configtemplate': template_serializer.data,
|
'configtemplate': template_serializer.data,
|
||||||
|
@ -1,671 +1,16 @@
|
|||||||
from django.contrib.auth import get_user_model
|
from .serializers_.objecttypes import *
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from .serializers_.attachments import *
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from .serializers_.bookmarks import *
|
||||||
from drf_spectacular.utils import extend_schema_field
|
from .serializers_.change_logging import *
|
||||||
from rest_framework import serializers
|
from .serializers_.customfields import *
|
||||||
from rest_framework.fields import ListField
|
from .serializers_.customlinks import *
|
||||||
|
from .serializers_.dashboard import *
|
||||||
from core.api.nested_serializers import NestedDataSourceSerializer, NestedDataFileSerializer, NestedJobSerializer
|
from .serializers_.events import *
|
||||||
from core.api.serializers import JobSerializer
|
from .serializers_.exporttemplates import *
|
||||||
from core.models import ContentType
|
from .serializers_.journaling import *
|
||||||
from dcim.api.nested_serializers import (
|
from .serializers_.configcontexts import *
|
||||||
NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedLocationSerializer, NestedPlatformSerializer,
|
from .serializers_.configtemplates import *
|
||||||
NestedRegionSerializer, NestedSiteSerializer, NestedSiteGroupSerializer,
|
from .serializers_.savedfilters import *
|
||||||
)
|
from .serializers_.scripts import *
|
||||||
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
|
from .serializers_.tags import *
|
||||||
from extras.choices import *
|
|
||||||
from extras.models import *
|
|
||||||
from netbox.api.exceptions import SerializerNotFound
|
|
||||||
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
|
||||||
from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer
|
|
||||||
from netbox.api.serializers.features import TaggableModelSerializer
|
|
||||||
from netbox.constants import NESTED_SERIALIZER_PREFIX
|
|
||||||
from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
|
|
||||||
from tenancy.models import Tenant, TenantGroup
|
|
||||||
from users.api.nested_serializers import NestedUserSerializer
|
|
||||||
from utilities.api import get_serializer_for_model
|
|
||||||
from virtualization.api.nested_serializers import (
|
|
||||||
NestedClusterGroupSerializer, NestedClusterSerializer, NestedClusterTypeSerializer,
|
|
||||||
)
|
|
||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
|
||||||
from .nested_serializers import *
|
from .nested_serializers import *
|
||||||
|
|
||||||
__all__ = (
|
|
||||||
'BookmarkSerializer',
|
|
||||||
'ConfigContextSerializer',
|
|
||||||
'ConfigTemplateSerializer',
|
|
||||||
'ContentTypeSerializer',
|
|
||||||
'CustomFieldChoiceSetSerializer',
|
|
||||||
'CustomFieldSerializer',
|
|
||||||
'CustomLinkSerializer',
|
|
||||||
'DashboardSerializer',
|
|
||||||
'EventRuleSerializer',
|
|
||||||
'ExportTemplateSerializer',
|
|
||||||
'ImageAttachmentSerializer',
|
|
||||||
'JournalEntrySerializer',
|
|
||||||
'ObjectChangeSerializer',
|
|
||||||
'ReportDetailSerializer',
|
|
||||||
'ReportSerializer',
|
|
||||||
'ReportInputSerializer',
|
|
||||||
'SavedFilterSerializer',
|
|
||||||
'ScriptDetailSerializer',
|
|
||||||
'ScriptInputSerializer',
|
|
||||||
'ScriptSerializer',
|
|
||||||
'TagSerializer',
|
|
||||||
'WebhookSerializer',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Event Rules
|
|
||||||
#
|
|
||||||
|
|
||||||
class EventRuleSerializer(NetBoxModelSerializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:eventrule-detail')
|
|
||||||
content_types = ContentTypeField(
|
|
||||||
queryset=ContentType.objects.with_feature('event_rules'),
|
|
||||||
many=True
|
|
||||||
)
|
|
||||||
action_type = ChoiceField(choices=EventRuleActionChoices)
|
|
||||||
action_object_type = ContentTypeField(
|
|
||||||
queryset=ContentType.objects.with_feature('event_rules'),
|
|
||||||
)
|
|
||||||
action_object = serializers.SerializerMethodField(read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = EventRule
|
|
||||||
fields = [
|
|
||||||
'id', 'url', 'display', 'content_types', 'name', 'type_create', 'type_update', 'type_delete',
|
|
||||||
'type_job_start', 'type_job_end', 'enabled', 'conditions', 'action_type', 'action_object_type',
|
|
||||||
'action_object_id', 'action_object', 'description', 'custom_fields', 'tags', 'created', 'last_updated',
|
|
||||||
]
|
|
||||||
|
|
||||||
@extend_schema_field(OpenApiTypes.OBJECT)
|
|
||||||
def get_action_object(self, instance):
|
|
||||||
context = {'request': self.context['request']}
|
|
||||||
# We need to manually instantiate the serializer for scripts
|
|
||||||
if instance.action_type == EventRuleActionChoices.SCRIPT:
|
|
||||||
script_name = instance.action_parameters['script_name']
|
|
||||||
script = instance.action_object.scripts[script_name]()
|
|
||||||
return NestedScriptSerializer(script, context=context).data
|
|
||||||
else:
|
|
||||||
serializer = get_serializer_for_model(
|
|
||||||
model=instance.action_object_type.model_class(),
|
|
||||||
prefix=NESTED_SERIALIZER_PREFIX
|
|
||||||
)
|
|
||||||
return serializer(instance.action_object, context=context).data
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Webhooks
|
|
||||||
#
|
|
||||||
|
|
||||||
class WebhookSerializer(NetBoxModelSerializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:webhook-detail')
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Webhook
|
|
||||||
fields = [
|
|
||||||
'id', 'url', 'display', 'name', 'description', 'payload_url', 'http_method', 'http_content_type',
|
|
||||||
'additional_headers', 'body_template', 'secret', 'ssl_verification', 'ca_file_path', 'custom_fields',
|
|
||||||
'tags', 'created', 'last_updated',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Custom fields
|
|
||||||
#
|
|
||||||
|
|
||||||
class CustomFieldSerializer(ValidatedModelSerializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail')
|
|
||||||
content_types = ContentTypeField(
|
|
||||||
queryset=ContentType.objects.with_feature('custom_fields'),
|
|
||||||
many=True
|
|
||||||
)
|
|
||||||
type = ChoiceField(choices=CustomFieldTypeChoices)
|
|
||||||
object_type = ContentTypeField(
|
|
||||||
queryset=ContentType.objects.all(),
|
|
||||||
required=False,
|
|
||||||
allow_null=True
|
|
||||||
)
|
|
||||||
filter_logic = ChoiceField(choices=CustomFieldFilterLogicChoices, required=False)
|
|
||||||
data_type = serializers.SerializerMethodField()
|
|
||||||
choice_set = NestedCustomFieldChoiceSetSerializer(
|
|
||||||
required=False,
|
|
||||||
allow_null=True
|
|
||||||
)
|
|
||||||
ui_visible = ChoiceField(choices=CustomFieldUIVisibleChoices, required=False)
|
|
||||||
ui_editable = ChoiceField(choices=CustomFieldUIEditableChoices, required=False)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = CustomField
|
|
||||||
fields = [
|
|
||||||
'id', 'url', 'display', 'content_types', 'type', 'object_type', 'data_type', 'name', 'label', 'group_name',
|
|
||||||
'description', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable',
|
|
||||||
'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set',
|
|
||||||
'created', 'last_updated',
|
|
||||||
]
|
|
||||||
|
|
||||||
def validate_type(self, value):
|
|
||||||
if self.instance and self.instance.type != value:
|
|
||||||
raise serializers.ValidationError('Changing the type of custom fields is not supported.')
|
|
||||||
|
|
||||||
return value
|
|
||||||
|
|
||||||
@extend_schema_field(OpenApiTypes.STR)
|
|
||||||
def get_data_type(self, obj):
|
|
||||||
types = CustomFieldTypeChoices
|
|
||||||
if obj.type == types.TYPE_INTEGER:
|
|
||||||
return 'integer'
|
|
||||||
if obj.type == types.TYPE_DECIMAL:
|
|
||||||
return 'decimal'
|
|
||||||
if obj.type == types.TYPE_BOOLEAN:
|
|
||||||
return 'boolean'
|
|
||||||
if obj.type in (types.TYPE_JSON, types.TYPE_OBJECT):
|
|
||||||
return 'object'
|
|
||||||
if obj.type in (types.TYPE_MULTISELECT, types.TYPE_MULTIOBJECT):
|
|
||||||
return 'array'
|
|
||||||
return 'string'
|
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldChoiceSetSerializer(ValidatedModelSerializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfieldchoiceset-detail')
|
|
||||||
base_choices = ChoiceField(
|
|
||||||
choices=CustomFieldChoiceSetBaseChoices,
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
extra_choices = serializers.ListField(
|
|
||||||
child=serializers.ListField(
|
|
||||||
min_length=2,
|
|
||||||
max_length=2
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = CustomFieldChoiceSet
|
|
||||||
fields = [
|
|
||||||
'id', 'url', 'display', 'name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically',
|
|
||||||
'choices_count', 'created', 'last_updated',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Custom links
|
|
||||||
#
|
|
||||||
|
|
||||||
class CustomLinkSerializer(ValidatedModelSerializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail')
|
|
||||||
content_types = ContentTypeField(
|
|
||||||
queryset=ContentType.objects.with_feature('custom_links'),
|
|
||||||
many=True
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = CustomLink
|
|
||||||
fields = [
|
|
||||||
'id', 'url', 'display', 'content_types', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name',
|
|
||||||
'button_class', 'new_window', 'created', 'last_updated',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Export templates
|
|
||||||
#
|
|
||||||
|
|
||||||
class ExportTemplateSerializer(ValidatedModelSerializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:exporttemplate-detail')
|
|
||||||
content_types = ContentTypeField(
|
|
||||||
queryset=ContentType.objects.with_feature('export_templates'),
|
|
||||||
many=True
|
|
||||||
)
|
|
||||||
data_source = NestedDataSourceSerializer(
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
data_file = NestedDataFileSerializer(
|
|
||||||
read_only=True
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = ExportTemplate
|
|
||||||
fields = [
|
|
||||||
'id', 'url', 'display', 'content_types', 'name', 'description', 'template_code', 'mime_type',
|
|
||||||
'file_extension', 'as_attachment', 'data_source', 'data_path', 'data_file', 'data_synced', 'created',
|
|
||||||
'last_updated',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Saved filters
|
|
||||||
#
|
|
||||||
|
|
||||||
class SavedFilterSerializer(ValidatedModelSerializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:savedfilter-detail')
|
|
||||||
content_types = ContentTypeField(
|
|
||||||
queryset=ContentType.objects.all(),
|
|
||||||
many=True
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = SavedFilter
|
|
||||||
fields = [
|
|
||||||
'id', 'url', 'display', 'content_types', 'name', 'slug', 'description', 'user', 'weight', 'enabled',
|
|
||||||
'shared', 'parameters', 'created', 'last_updated',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Bookmarks
|
|
||||||
#
|
|
||||||
|
|
||||||
class BookmarkSerializer(ValidatedModelSerializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:bookmark-detail')
|
|
||||||
object_type = ContentTypeField(
|
|
||||||
queryset=ContentType.objects.with_feature('bookmarks'),
|
|
||||||
)
|
|
||||||
object = serializers.SerializerMethodField(read_only=True)
|
|
||||||
user = NestedUserSerializer()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Bookmark
|
|
||||||
fields = [
|
|
||||||
'id', 'url', 'display', 'object_type', 'object_id', 'object', 'user', 'created',
|
|
||||||
]
|
|
||||||
|
|
||||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
|
||||||
def get_object(self, instance):
|
|
||||||
serializer = get_serializer_for_model(instance.object, prefix=NESTED_SERIALIZER_PREFIX)
|
|
||||||
return serializer(instance.object, context={'request': self.context['request']}).data
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Tags
|
|
||||||
#
|
|
||||||
|
|
||||||
class TagSerializer(ValidatedModelSerializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail')
|
|
||||||
object_types = ContentTypeField(
|
|
||||||
queryset=ContentType.objects.with_feature('tags'),
|
|
||||||
many=True,
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
tagged_items = serializers.IntegerField(read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Tag
|
|
||||||
fields = [
|
|
||||||
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'object_types', 'tagged_items', 'created',
|
|
||||||
'last_updated',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Image attachments
|
|
||||||
#
|
|
||||||
|
|
||||||
class ImageAttachmentSerializer(ValidatedModelSerializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail')
|
|
||||||
content_type = ContentTypeField(
|
|
||||||
queryset=ContentType.objects.all()
|
|
||||||
)
|
|
||||||
parent = serializers.SerializerMethodField(read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = ImageAttachment
|
|
||||||
fields = [
|
|
||||||
'id', 'url', 'display', 'content_type', 'object_id', 'parent', 'name', 'image', 'image_height',
|
|
||||||
'image_width', 'created', 'last_updated',
|
|
||||||
]
|
|
||||||
|
|
||||||
def validate(self, data):
|
|
||||||
|
|
||||||
# Validate that the parent object exists
|
|
||||||
try:
|
|
||||||
data['content_type'].get_object_for_this_type(id=data['object_id'])
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
raise serializers.ValidationError(
|
|
||||||
"Invalid parent object: {} ID {}".format(data['content_type'], data['object_id'])
|
|
||||||
)
|
|
||||||
|
|
||||||
# Enforce model validation
|
|
||||||
super().validate(data)
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
|
||||||
def get_parent(self, obj):
|
|
||||||
serializer = get_serializer_for_model(obj.parent, prefix=NESTED_SERIALIZER_PREFIX)
|
|
||||||
return serializer(obj.parent, context={'request': self.context['request']}).data
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Journal entries
|
|
||||||
#
|
|
||||||
|
|
||||||
class JournalEntrySerializer(NetBoxModelSerializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:journalentry-detail')
|
|
||||||
assigned_object_type = ContentTypeField(
|
|
||||||
queryset=ContentType.objects.all()
|
|
||||||
)
|
|
||||||
assigned_object = serializers.SerializerMethodField(read_only=True)
|
|
||||||
created_by = serializers.PrimaryKeyRelatedField(
|
|
||||||
allow_null=True,
|
|
||||||
queryset=get_user_model().objects.all(),
|
|
||||||
required=False,
|
|
||||||
default=serializers.CurrentUserDefault()
|
|
||||||
)
|
|
||||||
kind = ChoiceField(
|
|
||||||
choices=JournalEntryKindChoices,
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = JournalEntry
|
|
||||||
fields = [
|
|
||||||
'id', 'url', 'display', 'assigned_object_type', 'assigned_object_id', 'assigned_object', 'created',
|
|
||||||
'created_by', 'kind', 'comments', 'tags', 'custom_fields', 'last_updated',
|
|
||||||
]
|
|
||||||
|
|
||||||
def validate(self, data):
|
|
||||||
|
|
||||||
# Validate that the parent object exists
|
|
||||||
if 'assigned_object_type' in data and 'assigned_object_id' in data:
|
|
||||||
try:
|
|
||||||
data['assigned_object_type'].get_object_for_this_type(id=data['assigned_object_id'])
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
raise serializers.ValidationError(
|
|
||||||
f"Invalid assigned_object: {data['assigned_object_type']} ID {data['assigned_object_id']}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Enforce model validation
|
|
||||||
super().validate(data)
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
|
||||||
def get_assigned_object(self, instance):
|
|
||||||
serializer = get_serializer_for_model(instance.assigned_object_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX)
|
|
||||||
context = {'request': self.context['request']}
|
|
||||||
return serializer(instance.assigned_object, context=context).data
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Config contexts
|
|
||||||
#
|
|
||||||
|
|
||||||
class ConfigContextSerializer(ValidatedModelSerializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:configcontext-detail')
|
|
||||||
regions = SerializedPKRelatedField(
|
|
||||||
queryset=Region.objects.all(),
|
|
||||||
serializer=NestedRegionSerializer,
|
|
||||||
required=False,
|
|
||||||
many=True
|
|
||||||
)
|
|
||||||
site_groups = SerializedPKRelatedField(
|
|
||||||
queryset=SiteGroup.objects.all(),
|
|
||||||
serializer=NestedSiteGroupSerializer,
|
|
||||||
required=False,
|
|
||||||
many=True
|
|
||||||
)
|
|
||||||
sites = SerializedPKRelatedField(
|
|
||||||
queryset=Site.objects.all(),
|
|
||||||
serializer=NestedSiteSerializer,
|
|
||||||
required=False,
|
|
||||||
many=True
|
|
||||||
)
|
|
||||||
locations = SerializedPKRelatedField(
|
|
||||||
queryset=Location.objects.all(),
|
|
||||||
serializer=NestedLocationSerializer,
|
|
||||||
required=False,
|
|
||||||
many=True
|
|
||||||
)
|
|
||||||
device_types = SerializedPKRelatedField(
|
|
||||||
queryset=DeviceType.objects.all(),
|
|
||||||
serializer=NestedDeviceTypeSerializer,
|
|
||||||
required=False,
|
|
||||||
many=True
|
|
||||||
)
|
|
||||||
roles = SerializedPKRelatedField(
|
|
||||||
queryset=DeviceRole.objects.all(),
|
|
||||||
serializer=NestedDeviceRoleSerializer,
|
|
||||||
required=False,
|
|
||||||
many=True
|
|
||||||
)
|
|
||||||
platforms = SerializedPKRelatedField(
|
|
||||||
queryset=Platform.objects.all(),
|
|
||||||
serializer=NestedPlatformSerializer,
|
|
||||||
required=False,
|
|
||||||
many=True
|
|
||||||
)
|
|
||||||
cluster_types = SerializedPKRelatedField(
|
|
||||||
queryset=ClusterType.objects.all(),
|
|
||||||
serializer=NestedClusterTypeSerializer,
|
|
||||||
required=False,
|
|
||||||
many=True
|
|
||||||
)
|
|
||||||
cluster_groups = SerializedPKRelatedField(
|
|
||||||
queryset=ClusterGroup.objects.all(),
|
|
||||||
serializer=NestedClusterGroupSerializer,
|
|
||||||
required=False,
|
|
||||||
many=True
|
|
||||||
)
|
|
||||||
clusters = SerializedPKRelatedField(
|
|
||||||
queryset=Cluster.objects.all(),
|
|
||||||
serializer=NestedClusterSerializer,
|
|
||||||
required=False,
|
|
||||||
many=True
|
|
||||||
)
|
|
||||||
tenant_groups = SerializedPKRelatedField(
|
|
||||||
queryset=TenantGroup.objects.all(),
|
|
||||||
serializer=NestedTenantGroupSerializer,
|
|
||||||
required=False,
|
|
||||||
many=True
|
|
||||||
)
|
|
||||||
tenants = SerializedPKRelatedField(
|
|
||||||
queryset=Tenant.objects.all(),
|
|
||||||
serializer=NestedTenantSerializer,
|
|
||||||
required=False,
|
|
||||||
many=True
|
|
||||||
)
|
|
||||||
tags = serializers.SlugRelatedField(
|
|
||||||
queryset=Tag.objects.all(),
|
|
||||||
slug_field='slug',
|
|
||||||
required=False,
|
|
||||||
many=True
|
|
||||||
)
|
|
||||||
data_source = NestedDataSourceSerializer(
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
data_file = NestedDataFileSerializer(
|
|
||||||
read_only=True
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = ConfigContext
|
|
||||||
fields = [
|
|
||||||
'id', 'url', 'display', 'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites',
|
|
||||||
'locations', 'device_types', 'roles', 'platforms', 'cluster_types', 'cluster_groups', 'clusters',
|
|
||||||
'tenant_groups', 'tenants', 'tags', 'data_source', 'data_path', 'data_file', 'data_synced', 'data',
|
|
||||||
'created', 'last_updated',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Config templates
|
|
||||||
#
|
|
||||||
|
|
||||||
class ConfigTemplateSerializer(TaggableModelSerializer, ValidatedModelSerializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:configtemplate-detail')
|
|
||||||
data_source = NestedDataSourceSerializer(
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
data_file = NestedDataFileSerializer(
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = ConfigTemplate
|
|
||||||
fields = [
|
|
||||||
'id', 'url', 'display', 'name', 'description', 'environment_params', 'template_code', 'data_source',
|
|
||||||
'data_path', 'data_file', 'data_synced', 'tags', 'created', 'last_updated',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Reports
|
|
||||||
#
|
|
||||||
|
|
||||||
class ReportSerializer(serializers.Serializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(
|
|
||||||
view_name='extras-api:report-detail',
|
|
||||||
lookup_field='full_name',
|
|
||||||
lookup_url_kwarg='pk'
|
|
||||||
)
|
|
||||||
id = serializers.CharField(read_only=True, source="full_name")
|
|
||||||
module = serializers.CharField(max_length=255)
|
|
||||||
name = serializers.CharField(max_length=255)
|
|
||||||
description = serializers.CharField(max_length=255, required=False)
|
|
||||||
test_methods = serializers.ListField(child=serializers.CharField(max_length=255), read_only=True)
|
|
||||||
result = NestedJobSerializer()
|
|
||||||
display = serializers.SerializerMethodField(read_only=True)
|
|
||||||
|
|
||||||
@extend_schema_field(serializers.CharField())
|
|
||||||
def get_display(self, obj):
|
|
||||||
return f'{obj.name} ({obj.module})'
|
|
||||||
|
|
||||||
|
|
||||||
class ReportDetailSerializer(ReportSerializer):
|
|
||||||
result = JobSerializer()
|
|
||||||
|
|
||||||
|
|
||||||
class ReportInputSerializer(serializers.Serializer):
|
|
||||||
schedule_at = serializers.DateTimeField(required=False, allow_null=True)
|
|
||||||
interval = serializers.IntegerField(required=False, allow_null=True)
|
|
||||||
|
|
||||||
def validate_schedule_at(self, value):
|
|
||||||
if value and not self.context['report'].scheduling_enabled:
|
|
||||||
raise serializers.ValidationError("Scheduling is not enabled for this report.")
|
|
||||||
return value
|
|
||||||
|
|
||||||
def validate_interval(self, value):
|
|
||||||
if value and not self.context['report'].scheduling_enabled:
|
|
||||||
raise serializers.ValidationError("Scheduling is not enabled for this report.")
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Scripts
|
|
||||||
#
|
|
||||||
|
|
||||||
class ScriptSerializer(serializers.Serializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(
|
|
||||||
view_name='extras-api:script-detail',
|
|
||||||
lookup_field='full_name',
|
|
||||||
lookup_url_kwarg='pk'
|
|
||||||
)
|
|
||||||
id = serializers.CharField(read_only=True, source="full_name")
|
|
||||||
module = serializers.CharField(max_length=255)
|
|
||||||
name = serializers.CharField(read_only=True)
|
|
||||||
description = serializers.CharField(read_only=True)
|
|
||||||
vars = serializers.SerializerMethodField(read_only=True)
|
|
||||||
result = NestedJobSerializer()
|
|
||||||
display = serializers.SerializerMethodField(read_only=True)
|
|
||||||
|
|
||||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
|
||||||
def get_vars(self, instance):
|
|
||||||
return {
|
|
||||||
k: v.__class__.__name__ for k, v in instance._get_vars().items()
|
|
||||||
}
|
|
||||||
|
|
||||||
@extend_schema_field(serializers.CharField())
|
|
||||||
def get_display(self, obj):
|
|
||||||
return f'{obj.name} ({obj.module})'
|
|
||||||
|
|
||||||
|
|
||||||
class ScriptDetailSerializer(ScriptSerializer):
|
|
||||||
result = JobSerializer()
|
|
||||||
|
|
||||||
|
|
||||||
class ScriptInputSerializer(serializers.Serializer):
|
|
||||||
data = serializers.JSONField()
|
|
||||||
commit = serializers.BooleanField()
|
|
||||||
schedule_at = serializers.DateTimeField(required=False, allow_null=True)
|
|
||||||
interval = serializers.IntegerField(required=False, allow_null=True)
|
|
||||||
|
|
||||||
def validate_schedule_at(self, value):
|
|
||||||
if value and not self.context['script'].scheduling_enabled:
|
|
||||||
raise serializers.ValidationError("Scheduling is not enabled for this script.")
|
|
||||||
return value
|
|
||||||
|
|
||||||
def validate_interval(self, value):
|
|
||||||
if value and not self.context['script'].scheduling_enabled:
|
|
||||||
raise serializers.ValidationError("Scheduling is not enabled for this script.")
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Change logging
|
|
||||||
#
|
|
||||||
|
|
||||||
class ObjectChangeSerializer(BaseModelSerializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:objectchange-detail')
|
|
||||||
user = NestedUserSerializer(
|
|
||||||
read_only=True
|
|
||||||
)
|
|
||||||
action = ChoiceField(
|
|
||||||
choices=ObjectChangeActionChoices,
|
|
||||||
read_only=True
|
|
||||||
)
|
|
||||||
changed_object_type = ContentTypeField(
|
|
||||||
read_only=True
|
|
||||||
)
|
|
||||||
changed_object = serializers.SerializerMethodField(
|
|
||||||
read_only=True
|
|
||||||
)
|
|
||||||
|
|
||||||
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',
|
|
||||||
]
|
|
||||||
|
|
||||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
|
||||||
def get_changed_object(self, obj):
|
|
||||||
"""
|
|
||||||
Serialize a nested representation of the changed object.
|
|
||||||
"""
|
|
||||||
if obj.changed_object is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
serializer = get_serializer_for_model(obj.changed_object, prefix=NESTED_SERIALIZER_PREFIX)
|
|
||||||
except SerializerNotFound:
|
|
||||||
return obj.object_repr
|
|
||||||
context = {
|
|
||||||
'request': self.context['request']
|
|
||||||
}
|
|
||||||
data = serializer(obj.changed_object, context=context).data
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# ContentTypes
|
|
||||||
#
|
|
||||||
|
|
||||||
class ContentTypeSerializer(BaseModelSerializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:contenttype-detail')
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = ContentType
|
|
||||||
fields = ['id', 'url', 'display', 'app_label', 'model']
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# User dashboard
|
|
||||||
#
|
|
||||||
|
|
||||||
class DashboardSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = Dashboard
|
|
||||||
fields = ('layout', 'config')
|
|
||||||
|
0
netbox/extras/api/serializers_/__init__.py
Normal file
0
netbox/extras/api/serializers_/__init__.py
Normal file
50
netbox/extras/api/serializers_/attachments.py
Normal file
50
netbox/extras/api/serializers_/attachments.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from drf_spectacular.utils import extend_schema_field
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from core.models import ObjectType
|
||||||
|
from extras.models import ImageAttachment
|
||||||
|
from netbox.api.fields import ContentTypeField
|
||||||
|
from netbox.api.serializers import ValidatedModelSerializer
|
||||||
|
from utilities.api import get_serializer_for_model
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'ImageAttachmentSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ImageAttachmentSerializer(ValidatedModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail')
|
||||||
|
object_type = ContentTypeField(
|
||||||
|
queryset=ObjectType.objects.all()
|
||||||
|
)
|
||||||
|
parent = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ImageAttachment
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'object_type', 'object_id', 'parent', 'name', 'image', 'image_height',
|
||||||
|
'image_width', 'created', 'last_updated',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'image')
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
|
||||||
|
# Validate that the parent object exists
|
||||||
|
try:
|
||||||
|
data['object_type'].get_object_for_this_type(id=data['object_id'])
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
"Invalid parent object: {} ID {}".format(data['object_type'], data['object_id'])
|
||||||
|
)
|
||||||
|
|
||||||
|
# Enforce model validation
|
||||||
|
super().validate(data)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||||
|
def get_parent(self, obj):
|
||||||
|
serializer = get_serializer_for_model(obj.parent)
|
||||||
|
context = {'request': self.context['request']}
|
||||||
|
return serializer(obj.parent, nested=True, context=context).data
|
35
netbox/extras/api/serializers_/bookmarks.py
Normal file
35
netbox/extras/api/serializers_/bookmarks.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
from drf_spectacular.utils import extend_schema_field
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from core.models import ObjectType
|
||||||
|
from extras.models import Bookmark
|
||||||
|
from netbox.api.fields import ContentTypeField
|
||||||
|
from netbox.api.serializers import ValidatedModelSerializer
|
||||||
|
from users.api.serializers_.users import UserSerializer
|
||||||
|
from utilities.api import get_serializer_for_model
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'BookmarkSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BookmarkSerializer(ValidatedModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:bookmark-detail')
|
||||||
|
object_type = ContentTypeField(
|
||||||
|
queryset=ObjectType.objects.with_feature('bookmarks'),
|
||||||
|
)
|
||||||
|
object = serializers.SerializerMethodField(read_only=True)
|
||||||
|
user = UserSerializer(nested=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Bookmark
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'object_type', 'object_id', 'object', 'user', 'created',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'object_id', 'object_type')
|
||||||
|
|
||||||
|
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||||
|
def get_object(self, instance):
|
||||||
|
serializer = get_serializer_for_model(instance.object)
|
||||||
|
context = {'request': self.context['request']}
|
||||||
|
return serializer(instance.object, nested=True, context=context).data
|
55
netbox/extras/api/serializers_/change_logging.py
Normal file
55
netbox/extras/api/serializers_/change_logging.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
from drf_spectacular.utils import extend_schema_field
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from extras.choices import *
|
||||||
|
from extras.models import ObjectChange
|
||||||
|
from netbox.api.exceptions import SerializerNotFound
|
||||||
|
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||||
|
from netbox.api.serializers import BaseModelSerializer
|
||||||
|
from users.api.serializers_.users import UserSerializer
|
||||||
|
from utilities.api import get_serializer_for_model
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'ObjectChangeSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectChangeSerializer(BaseModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:objectchange-detail')
|
||||||
|
user = UserSerializer(
|
||||||
|
nested=True,
|
||||||
|
read_only=True
|
||||||
|
)
|
||||||
|
action = ChoiceField(
|
||||||
|
choices=ObjectChangeActionChoices,
|
||||||
|
read_only=True
|
||||||
|
)
|
||||||
|
changed_object_type = ContentTypeField(
|
||||||
|
read_only=True
|
||||||
|
)
|
||||||
|
changed_object = serializers.SerializerMethodField(
|
||||||
|
read_only=True
|
||||||
|
)
|
||||||
|
|
||||||
|
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',
|
||||||
|
]
|
||||||
|
|
||||||
|
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||||
|
def get_changed_object(self, obj):
|
||||||
|
"""
|
||||||
|
Serialize a nested representation of the changed object.
|
||||||
|
"""
|
||||||
|
if obj.changed_object is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
serializer = get_serializer_for_model(obj.changed_object)
|
||||||
|
except SerializerNotFound:
|
||||||
|
return obj.object_repr
|
||||||
|
data = serializer(obj.changed_object, nested=True, context={'request': self.context['request']}).data
|
||||||
|
|
||||||
|
return data
|
131
netbox/extras/api/serializers_/configcontexts.py
Normal file
131
netbox/extras/api/serializers_/configcontexts.py
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from core.api.serializers_.data import DataFileSerializer, DataSourceSerializer
|
||||||
|
from dcim.api.serializers_.devicetypes import DeviceTypeSerializer
|
||||||
|
from dcim.api.serializers_.platforms import PlatformSerializer
|
||||||
|
from dcim.api.serializers_.roles import DeviceRoleSerializer
|
||||||
|
from dcim.api.serializers_.sites import LocationSerializer, RegionSerializer, SiteSerializer, SiteGroupSerializer
|
||||||
|
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
|
||||||
|
from extras.models import ConfigContext, Tag
|
||||||
|
from netbox.api.fields import SerializedPKRelatedField
|
||||||
|
from netbox.api.serializers import ValidatedModelSerializer
|
||||||
|
from tenancy.api.serializers_.tenants import TenantSerializer, TenantGroupSerializer
|
||||||
|
from tenancy.models import Tenant, TenantGroup
|
||||||
|
from virtualization.api.serializers_.clusters import ClusterSerializer, ClusterGroupSerializer, ClusterTypeSerializer
|
||||||
|
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'ConfigContextSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigContextSerializer(ValidatedModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:configcontext-detail')
|
||||||
|
regions = SerializedPKRelatedField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
serializer=RegionSerializer,
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
many=True
|
||||||
|
)
|
||||||
|
site_groups = SerializedPKRelatedField(
|
||||||
|
queryset=SiteGroup.objects.all(),
|
||||||
|
serializer=SiteGroupSerializer,
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
many=True
|
||||||
|
)
|
||||||
|
sites = SerializedPKRelatedField(
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
serializer=SiteSerializer,
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
many=True
|
||||||
|
)
|
||||||
|
locations = SerializedPKRelatedField(
|
||||||
|
queryset=Location.objects.all(),
|
||||||
|
serializer=LocationSerializer,
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
many=True
|
||||||
|
)
|
||||||
|
device_types = SerializedPKRelatedField(
|
||||||
|
queryset=DeviceType.objects.all(),
|
||||||
|
serializer=DeviceTypeSerializer,
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
many=True
|
||||||
|
)
|
||||||
|
roles = SerializedPKRelatedField(
|
||||||
|
queryset=DeviceRole.objects.all(),
|
||||||
|
serializer=DeviceRoleSerializer,
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
many=True
|
||||||
|
)
|
||||||
|
platforms = SerializedPKRelatedField(
|
||||||
|
queryset=Platform.objects.all(),
|
||||||
|
serializer=PlatformSerializer,
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
many=True
|
||||||
|
)
|
||||||
|
cluster_types = SerializedPKRelatedField(
|
||||||
|
queryset=ClusterType.objects.all(),
|
||||||
|
serializer=ClusterTypeSerializer,
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
many=True
|
||||||
|
)
|
||||||
|
cluster_groups = SerializedPKRelatedField(
|
||||||
|
queryset=ClusterGroup.objects.all(),
|
||||||
|
serializer=ClusterGroupSerializer,
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
many=True
|
||||||
|
)
|
||||||
|
clusters = SerializedPKRelatedField(
|
||||||
|
queryset=Cluster.objects.all(),
|
||||||
|
serializer=ClusterSerializer,
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
many=True
|
||||||
|
)
|
||||||
|
tenant_groups = SerializedPKRelatedField(
|
||||||
|
queryset=TenantGroup.objects.all(),
|
||||||
|
serializer=TenantGroupSerializer,
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
many=True
|
||||||
|
)
|
||||||
|
tenants = SerializedPKRelatedField(
|
||||||
|
queryset=Tenant.objects.all(),
|
||||||
|
serializer=TenantSerializer,
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
many=True
|
||||||
|
)
|
||||||
|
tags = serializers.SlugRelatedField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
slug_field='slug',
|
||||||
|
required=False,
|
||||||
|
many=True
|
||||||
|
)
|
||||||
|
data_source = DataSourceSerializer(
|
||||||
|
nested=True,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
data_file = DataFileSerializer(
|
||||||
|
nested=True,
|
||||||
|
read_only=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ConfigContext
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites',
|
||||||
|
'locations', 'device_types', 'roles', 'platforms', 'cluster_types', 'cluster_groups', 'clusters',
|
||||||
|
'tenant_groups', 'tenants', 'tags', 'data_source', 'data_path', 'data_file', 'data_synced', 'data',
|
||||||
|
'created', 'last_updated',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
30
netbox/extras/api/serializers_/configtemplates.py
Normal file
30
netbox/extras/api/serializers_/configtemplates.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from core.api.serializers_.data import DataFileSerializer, DataSourceSerializer
|
||||||
|
from extras.models import ConfigTemplate
|
||||||
|
from netbox.api.serializers import ValidatedModelSerializer
|
||||||
|
from netbox.api.serializers.features import TaggableModelSerializer
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'ConfigTemplateSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigTemplateSerializer(TaggableModelSerializer, ValidatedModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:configtemplate-detail')
|
||||||
|
data_source = DataSourceSerializer(
|
||||||
|
nested=True,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
data_file = DataFileSerializer(
|
||||||
|
nested=True,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ConfigTemplate
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'name', 'description', 'environment_params', 'template_code', 'data_source',
|
||||||
|
'data_path', 'data_file', 'data_synced', 'tags', 'created', 'last_updated',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
91
netbox/extras/api/serializers_/customfields.py
Normal file
91
netbox/extras/api/serializers_/customfields.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
from django.utils.translation import gettext as _
|
||||||
|
from drf_spectacular.types import OpenApiTypes
|
||||||
|
from drf_spectacular.utils import extend_schema_field
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from core.models import ObjectType
|
||||||
|
from extras.choices import *
|
||||||
|
from extras.models import CustomField, CustomFieldChoiceSet
|
||||||
|
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||||
|
from netbox.api.serializers import ValidatedModelSerializer
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'CustomFieldChoiceSetSerializer',
|
||||||
|
'CustomFieldSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFieldChoiceSetSerializer(ValidatedModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfieldchoiceset-detail')
|
||||||
|
base_choices = ChoiceField(
|
||||||
|
choices=CustomFieldChoiceSetBaseChoices,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
extra_choices = serializers.ListField(
|
||||||
|
child=serializers.ListField(
|
||||||
|
min_length=2,
|
||||||
|
max_length=2
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CustomFieldChoiceSet
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically',
|
||||||
|
'choices_count', 'created', 'last_updated',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'description', 'choices_count')
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFieldSerializer(ValidatedModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail')
|
||||||
|
object_types = ContentTypeField(
|
||||||
|
queryset=ObjectType.objects.with_feature('custom_fields'),
|
||||||
|
many=True
|
||||||
|
)
|
||||||
|
type = ChoiceField(choices=CustomFieldTypeChoices)
|
||||||
|
object_type = ContentTypeField(
|
||||||
|
queryset=ObjectType.objects.all(),
|
||||||
|
required=False,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
filter_logic = ChoiceField(choices=CustomFieldFilterLogicChoices, required=False)
|
||||||
|
data_type = serializers.SerializerMethodField()
|
||||||
|
choice_set = CustomFieldChoiceSetSerializer(
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
ui_visible = ChoiceField(choices=CustomFieldUIVisibleChoices, required=False)
|
||||||
|
ui_editable = ChoiceField(choices=CustomFieldUIEditableChoices, required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CustomField
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'object_types', 'type', 'object_type', 'data_type', 'name', 'label', 'group_name',
|
||||||
|
'description', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable',
|
||||||
|
'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set',
|
||||||
|
'created', 'last_updated',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||||
|
|
||||||
|
def validate_type(self, value):
|
||||||
|
if self.instance and self.instance.type != value:
|
||||||
|
raise serializers.ValidationError(_('Changing the type of custom fields is not supported.'))
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
@extend_schema_field(OpenApiTypes.STR)
|
||||||
|
def get_data_type(self, obj):
|
||||||
|
types = CustomFieldTypeChoices
|
||||||
|
if obj.type == types.TYPE_INTEGER:
|
||||||
|
return 'integer'
|
||||||
|
if obj.type == types.TYPE_DECIMAL:
|
||||||
|
return 'decimal'
|
||||||
|
if obj.type == types.TYPE_BOOLEAN:
|
||||||
|
return 'boolean'
|
||||||
|
if obj.type in (types.TYPE_JSON, types.TYPE_OBJECT):
|
||||||
|
return 'object'
|
||||||
|
if obj.type in (types.TYPE_MULTISELECT, types.TYPE_MULTIOBJECT):
|
||||||
|
return 'array'
|
||||||
|
return 'string'
|
26
netbox/extras/api/serializers_/customlinks.py
Normal file
26
netbox/extras/api/serializers_/customlinks.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from core.models import ObjectType
|
||||||
|
from extras.models import CustomLink
|
||||||
|
from netbox.api.fields import ContentTypeField
|
||||||
|
from netbox.api.serializers import ValidatedModelSerializer
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'CustomLinkSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomLinkSerializer(ValidatedModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail')
|
||||||
|
object_types = ContentTypeField(
|
||||||
|
queryset=ObjectType.objects.with_feature('custom_links'),
|
||||||
|
many=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CustomLink
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'object_types', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name',
|
||||||
|
'button_class', 'new_window', 'created', 'last_updated',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name')
|
13
netbox/extras/api/serializers_/dashboard.py
Normal file
13
netbox/extras/api/serializers_/dashboard.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from extras.models import Dashboard
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'DashboardSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DashboardSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Dashboard
|
||||||
|
fields = ('layout', 'config')
|
71
netbox/extras/api/serializers_/events.py
Normal file
71
netbox/extras/api/serializers_/events.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
from drf_spectacular.types import OpenApiTypes
|
||||||
|
from drf_spectacular.utils import extend_schema_field
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from core.models import ObjectType
|
||||||
|
from extras.choices import *
|
||||||
|
from extras.models import EventRule, Webhook
|
||||||
|
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||||
|
from netbox.api.serializers import NetBoxModelSerializer
|
||||||
|
from utilities.api import get_serializer_for_model
|
||||||
|
from .scripts import ScriptSerializer
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'EventRuleSerializer',
|
||||||
|
'WebhookSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Event Rules
|
||||||
|
#
|
||||||
|
|
||||||
|
class EventRuleSerializer(NetBoxModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:eventrule-detail')
|
||||||
|
object_types = ContentTypeField(
|
||||||
|
queryset=ObjectType.objects.with_feature('event_rules'),
|
||||||
|
many=True
|
||||||
|
)
|
||||||
|
action_type = ChoiceField(choices=EventRuleActionChoices)
|
||||||
|
action_object_type = ContentTypeField(
|
||||||
|
queryset=ObjectType.objects.with_feature('event_rules'),
|
||||||
|
)
|
||||||
|
action_object = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = EventRule
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'object_types', 'name', 'type_create', 'type_update', 'type_delete',
|
||||||
|
'type_job_start', 'type_job_end', 'enabled', 'conditions', 'action_type', 'action_object_type',
|
||||||
|
'action_object_id', 'action_object', 'description', 'custom_fields', 'tags', 'created', 'last_updated',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||||
|
|
||||||
|
@extend_schema_field(OpenApiTypes.OBJECT)
|
||||||
|
def get_action_object(self, instance):
|
||||||
|
context = {'request': self.context['request']}
|
||||||
|
# We need to manually instantiate the serializer for scripts
|
||||||
|
if instance.action_type == EventRuleActionChoices.SCRIPT:
|
||||||
|
script = instance.action_object
|
||||||
|
instance = script.python_class() if script.python_class else None
|
||||||
|
return ScriptSerializer(instance, nested=True, context=context).data
|
||||||
|
else:
|
||||||
|
serializer = get_serializer_for_model(instance.action_object_type.model_class())
|
||||||
|
return serializer(instance.action_object, nested=True, context=context).data
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Webhooks
|
||||||
|
#
|
||||||
|
|
||||||
|
class WebhookSerializer(NetBoxModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:webhook-detail')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Webhook
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'name', 'description', 'payload_url', 'http_method', 'http_content_type',
|
||||||
|
'additional_headers', 'body_template', 'secret', 'ssl_verification', 'ca_file_path', 'custom_fields',
|
||||||
|
'tags', 'created', 'last_updated',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
36
netbox/extras/api/serializers_/exporttemplates.py
Normal file
36
netbox/extras/api/serializers_/exporttemplates.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from core.api.serializers_.data import DataFileSerializer, DataSourceSerializer
|
||||||
|
from core.models import ObjectType
|
||||||
|
from extras.models import ExportTemplate
|
||||||
|
from netbox.api.fields import ContentTypeField
|
||||||
|
from netbox.api.serializers import ValidatedModelSerializer
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'ExportTemplateSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ExportTemplateSerializer(ValidatedModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:exporttemplate-detail')
|
||||||
|
object_types = ContentTypeField(
|
||||||
|
queryset=ObjectType.objects.with_feature('export_templates'),
|
||||||
|
many=True
|
||||||
|
)
|
||||||
|
data_source = DataSourceSerializer(
|
||||||
|
nested=True,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
data_file = DataFileSerializer(
|
||||||
|
nested=True,
|
||||||
|
read_only=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ExportTemplate
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'object_types', 'name', 'description', 'template_code', 'mime_type',
|
||||||
|
'file_extension', 'as_attachment', 'data_source', 'data_path', 'data_file', 'data_synced', 'created',
|
||||||
|
'last_updated',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
63
netbox/extras/api/serializers_/journaling.py
Normal file
63
netbox/extras/api/serializers_/journaling.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from drf_spectacular.utils import extend_schema_field
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from core.models import ObjectType
|
||||||
|
from extras.choices import *
|
||||||
|
from extras.models import JournalEntry
|
||||||
|
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||||
|
from netbox.api.serializers import NetBoxModelSerializer
|
||||||
|
from utilities.api import get_serializer_for_model
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'JournalEntrySerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class JournalEntrySerializer(NetBoxModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:journalentry-detail')
|
||||||
|
assigned_object_type = ContentTypeField(
|
||||||
|
queryset=ObjectType.objects.all()
|
||||||
|
)
|
||||||
|
assigned_object = serializers.SerializerMethodField(read_only=True)
|
||||||
|
created_by = serializers.PrimaryKeyRelatedField(
|
||||||
|
allow_null=True,
|
||||||
|
queryset=get_user_model().objects.all(),
|
||||||
|
required=False,
|
||||||
|
default=serializers.CurrentUserDefault()
|
||||||
|
)
|
||||||
|
kind = ChoiceField(
|
||||||
|
choices=JournalEntryKindChoices,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = JournalEntry
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'assigned_object_type', 'assigned_object_id', 'assigned_object', 'created',
|
||||||
|
'created_by', 'kind', 'comments', 'tags', 'custom_fields', 'last_updated',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'created')
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
|
||||||
|
# Validate that the parent object exists
|
||||||
|
if 'assigned_object_type' in data and 'assigned_object_id' in data:
|
||||||
|
try:
|
||||||
|
data['assigned_object_type'].get_object_for_this_type(id=data['assigned_object_id'])
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
f"Invalid assigned_object: {data['assigned_object_type']} ID {data['assigned_object_id']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Enforce model validation
|
||||||
|
super().validate(data)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||||
|
def get_assigned_object(self, instance):
|
||||||
|
serializer = get_serializer_for_model(instance.assigned_object_type.model_class())
|
||||||
|
context = {'request': self.context['request']}
|
||||||
|
return serializer(instance.assigned_object, nested=True, context=context).data
|
16
netbox/extras/api/serializers_/objecttypes.py
Normal file
16
netbox/extras/api/serializers_/objecttypes.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from core.models import ObjectType
|
||||||
|
from netbox.api.serializers import BaseModelSerializer
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'ObjectTypeSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectTypeSerializer(BaseModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:objecttype-detail')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ObjectType
|
||||||
|
fields = ['id', 'url', 'display', 'app_label', 'model']
|
26
netbox/extras/api/serializers_/savedfilters.py
Normal file
26
netbox/extras/api/serializers_/savedfilters.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from core.models import ObjectType
|
||||||
|
from extras.models import SavedFilter
|
||||||
|
from netbox.api.fields import ContentTypeField
|
||||||
|
from netbox.api.serializers import ValidatedModelSerializer
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'SavedFilterSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SavedFilterSerializer(ValidatedModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:savedfilter-detail')
|
||||||
|
object_types = ContentTypeField(
|
||||||
|
queryset=ObjectType.objects.all(),
|
||||||
|
many=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = SavedFilter
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'object_types', 'name', 'slug', 'description', 'user', 'weight', 'enabled',
|
||||||
|
'shared', 'parameters', 'created', 'last_updated',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description')
|
77
netbox/extras/api/serializers_/scripts.py
Normal file
77
netbox/extras/api/serializers_/scripts.py
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
from django.utils.translation import gettext as _
|
||||||
|
from drf_spectacular.utils import extend_schema_field
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from core.api.serializers_.jobs import JobSerializer
|
||||||
|
from extras.models import Script
|
||||||
|
from netbox.api.serializers import ValidatedModelSerializer
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'ScriptDetailSerializer',
|
||||||
|
'ScriptInputSerializer',
|
||||||
|
'ScriptSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ScriptSerializer(ValidatedModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:script-detail')
|
||||||
|
description = serializers.SerializerMethodField(read_only=True)
|
||||||
|
vars = serializers.SerializerMethodField(read_only=True)
|
||||||
|
result = JobSerializer(nested=True, read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Script
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'module', 'name', 'description', 'vars', 'result', 'display', 'is_executable',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||||
|
|
||||||
|
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||||
|
def get_vars(self, obj):
|
||||||
|
if obj.python_class:
|
||||||
|
return {
|
||||||
|
k: v.__class__.__name__ for k, v in obj.python_class()._get_vars().items()
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@extend_schema_field(serializers.CharField())
|
||||||
|
def get_display(self, obj):
|
||||||
|
return f'{obj.name} ({obj.module})'
|
||||||
|
|
||||||
|
@extend_schema_field(serializers.CharField())
|
||||||
|
def get_description(self, obj):
|
||||||
|
if obj.python_class:
|
||||||
|
return obj.python_class().description
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class ScriptDetailSerializer(ScriptSerializer):
|
||||||
|
result = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
|
@extend_schema_field(JobSerializer())
|
||||||
|
def get_result(self, obj):
|
||||||
|
job = obj.jobs.all().order_by('-created').first()
|
||||||
|
context = {
|
||||||
|
'request': self.context['request']
|
||||||
|
}
|
||||||
|
data = JobSerializer(job, context=context).data
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class ScriptInputSerializer(serializers.Serializer):
|
||||||
|
data = serializers.JSONField()
|
||||||
|
commit = serializers.BooleanField()
|
||||||
|
schedule_at = serializers.DateTimeField(required=False, allow_null=True)
|
||||||
|
interval = serializers.IntegerField(required=False, allow_null=True)
|
||||||
|
|
||||||
|
def validate_schedule_at(self, value):
|
||||||
|
if value and not self.context['script'].scheduling_enabled:
|
||||||
|
raise serializers.ValidationError(_("Scheduling is not enabled for this script."))
|
||||||
|
return value
|
||||||
|
|
||||||
|
def validate_interval(self, value):
|
||||||
|
if value and not self.context['script'].scheduling_enabled:
|
||||||
|
raise serializers.ValidationError(_("Scheduling is not enabled for this script."))
|
||||||
|
return value
|
30
netbox/extras/api/serializers_/tags.py
Normal file
30
netbox/extras/api/serializers_/tags.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from core.models import ObjectType
|
||||||
|
from extras.models import Tag
|
||||||
|
from netbox.api.fields import ContentTypeField, RelatedObjectCountField
|
||||||
|
from netbox.api.serializers import ValidatedModelSerializer
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'TagSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TagSerializer(ValidatedModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail')
|
||||||
|
object_types = ContentTypeField(
|
||||||
|
queryset=ObjectType.objects.with_feature('tags'),
|
||||||
|
many=True,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Related object counts
|
||||||
|
tagged_items = RelatedObjectCountField('extras_taggeditem_items')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Tag
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'object_types', 'tagged_items', 'created',
|
||||||
|
'last_updated',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'color', 'description')
|
@ -22,7 +22,7 @@ router.register('config-contexts', views.ConfigContextViewSet)
|
|||||||
router.register('config-templates', views.ConfigTemplateViewSet)
|
router.register('config-templates', views.ConfigTemplateViewSet)
|
||||||
router.register('scripts', views.ScriptViewSet, basename='script')
|
router.register('scripts', views.ScriptViewSet, basename='script')
|
||||||
router.register('object-changes', views.ObjectChangeViewSet)
|
router.register('object-changes', views.ObjectChangeViewSet)
|
||||||
router.register('content-types', views.ContentTypeViewSet)
|
router.register('object-types', views.ObjectTypeViewSet)
|
||||||
|
|
||||||
app_name = 'extras-api'
|
app_name = 'extras-api'
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.http import Http404
|
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django_rq.queues import get_connection
|
from django_rq.queues import get_connection
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@ -9,21 +7,20 @@ from rest_framework.generics import RetrieveUpdateDestroyAPIView
|
|||||||
from rest_framework.renderers import JSONRenderer
|
from rest_framework.renderers import JSONRenderer
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.routers import APIRootView
|
from rest_framework.routers import APIRootView
|
||||||
from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
|
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
||||||
from rq import Worker
|
from rq import Worker
|
||||||
|
|
||||||
from core.choices import JobStatusChoices
|
from core.models import Job, ObjectType
|
||||||
from core.models import Job
|
|
||||||
from extras import filtersets
|
from extras import filtersets
|
||||||
from extras.models import *
|
from extras.models import *
|
||||||
from extras.scripts import get_module_and_script, run_script
|
from extras.scripts import run_script
|
||||||
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
|
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
|
||||||
from netbox.api.features import SyncedDataMixin
|
from netbox.api.features import SyncedDataMixin
|
||||||
from netbox.api.metadata import ContentTypeMetadata
|
from netbox.api.metadata import ContentTypeMetadata
|
||||||
from netbox.api.renderers import TextRenderer
|
from netbox.api.renderers import TextRenderer
|
||||||
from netbox.api.viewsets import NetBoxModelViewSet
|
from netbox.api.viewsets import NetBoxModelViewSet
|
||||||
from utilities.exceptions import RQWorkerNotRunningException
|
from utilities.exceptions import RQWorkerNotRunningException
|
||||||
from utilities.utils import copy_safe_request, count_related
|
from utilities.utils import copy_safe_request
|
||||||
from . import serializers
|
from . import serializers
|
||||||
from .mixins import ConfigTemplateRenderMixin
|
from .mixins import ConfigTemplateRenderMixin
|
||||||
|
|
||||||
@ -115,7 +112,7 @@ class CustomLinkViewSet(NetBoxModelViewSet):
|
|||||||
|
|
||||||
class ExportTemplateViewSet(SyncedDataMixin, NetBoxModelViewSet):
|
class ExportTemplateViewSet(SyncedDataMixin, NetBoxModelViewSet):
|
||||||
metadata_class = ContentTypeMetadata
|
metadata_class = ContentTypeMetadata
|
||||||
queryset = ExportTemplate.objects.prefetch_related('data_source', 'data_file')
|
queryset = ExportTemplate.objects.all()
|
||||||
serializer_class = serializers.ExportTemplateSerializer
|
serializer_class = serializers.ExportTemplateSerializer
|
||||||
filterset_class = filtersets.ExportTemplateFilterSet
|
filterset_class = filtersets.ExportTemplateFilterSet
|
||||||
|
|
||||||
@ -147,9 +144,7 @@ class BookmarkViewSet(NetBoxModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class TagViewSet(NetBoxModelViewSet):
|
class TagViewSet(NetBoxModelViewSet):
|
||||||
queryset = Tag.objects.annotate(
|
queryset = Tag.objects.all()
|
||||||
tagged_items=count_related(TaggedItem, 'tag')
|
|
||||||
)
|
|
||||||
serializer_class = serializers.TagSerializer
|
serializer_class = serializers.TagSerializer
|
||||||
filterset_class = filtersets.TagFilterSet
|
filterset_class = filtersets.TagFilterSet
|
||||||
|
|
||||||
@ -181,10 +176,7 @@ class JournalEntryViewSet(NetBoxModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class ConfigContextViewSet(SyncedDataMixin, NetBoxModelViewSet):
|
class ConfigContextViewSet(SyncedDataMixin, NetBoxModelViewSet):
|
||||||
queryset = ConfigContext.objects.prefetch_related(
|
queryset = ConfigContext.objects.all()
|
||||||
'regions', 'site_groups', 'sites', 'locations', 'roles', 'platforms', 'tenant_groups', 'tenants', 'data_source',
|
|
||||||
'data_file',
|
|
||||||
)
|
|
||||||
serializer_class = serializers.ConfigContextSerializer
|
serializer_class = serializers.ConfigContextSerializer
|
||||||
filterset_class = filtersets.ConfigContextFilterSet
|
filterset_class = filtersets.ConfigContextFilterSet
|
||||||
|
|
||||||
@ -194,7 +186,7 @@ class ConfigContextViewSet(SyncedDataMixin, NetBoxModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class ConfigTemplateViewSet(SyncedDataMixin, ConfigTemplateRenderMixin, NetBoxModelViewSet):
|
class ConfigTemplateViewSet(SyncedDataMixin, ConfigTemplateRenderMixin, NetBoxModelViewSet):
|
||||||
queryset = ConfigTemplate.objects.prefetch_related('data_source', 'data_file')
|
queryset = ConfigTemplate.objects.all()
|
||||||
serializer_class = serializers.ConfigTemplateSerializer
|
serializer_class = serializers.ConfigTemplateSerializer
|
||||||
filterset_class = filtersets.ConfigTemplateFilterSet
|
filterset_class = filtersets.ConfigTemplateFilterSet
|
||||||
|
|
||||||
@ -214,66 +206,30 @@ class ConfigTemplateViewSet(SyncedDataMixin, ConfigTemplateRenderMixin, NetBoxMo
|
|||||||
# Scripts
|
# Scripts
|
||||||
#
|
#
|
||||||
|
|
||||||
class ScriptViewSet(ViewSet):
|
class ScriptViewSet(ModelViewSet):
|
||||||
permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
||||||
|
queryset = Script.objects.prefetch_related('jobs')
|
||||||
|
serializer_class = serializers.ScriptSerializer
|
||||||
|
filterset_class = filtersets.ScriptFilterSet
|
||||||
|
|
||||||
_ignore_model_permissions = True
|
_ignore_model_permissions = True
|
||||||
schema = None
|
|
||||||
lookup_value_regex = '[^/]+' # Allow dots
|
lookup_value_regex = '[^/]+' # Allow dots
|
||||||
|
|
||||||
def _get_script(self, pk):
|
|
||||||
try:
|
|
||||||
module_name, script_name = pk.split('.', maxsplit=1)
|
|
||||||
except ValueError:
|
|
||||||
raise Http404
|
|
||||||
|
|
||||||
module, script = get_module_and_script(module_name, script_name)
|
|
||||||
if script is None:
|
|
||||||
raise Http404
|
|
||||||
|
|
||||||
return module, script
|
|
||||||
|
|
||||||
def list(self, request):
|
|
||||||
results = {
|
|
||||||
job.name: job
|
|
||||||
for job in Job.objects.filter(
|
|
||||||
object_type=ContentType.objects.get(app_label='extras', model='scriptmodule'),
|
|
||||||
status__in=JobStatusChoices.TERMINAL_STATE_CHOICES
|
|
||||||
).order_by('name', '-created').distinct('name').defer('data')
|
|
||||||
}
|
|
||||||
|
|
||||||
script_list = []
|
|
||||||
for script_module in ScriptModule.objects.restrict(request.user):
|
|
||||||
script_list.extend(script_module.scripts.values())
|
|
||||||
|
|
||||||
# Attach Job objects to each script (if any)
|
|
||||||
for script in script_list:
|
|
||||||
script.result = results.get(script.class_name, None)
|
|
||||||
|
|
||||||
serializer = serializers.ScriptSerializer(script_list, many=True, context={'request': request})
|
|
||||||
|
|
||||||
return Response({'count': len(script_list), 'results': serializer.data})
|
|
||||||
|
|
||||||
def retrieve(self, request, pk):
|
def retrieve(self, request, pk):
|
||||||
module, script = self._get_script(pk)
|
script = get_object_or_404(self.queryset, pk=pk)
|
||||||
object_type = ContentType.objects.get(app_label='extras', model='scriptmodule')
|
|
||||||
script.result = Job.objects.filter(
|
|
||||||
object_type=object_type,
|
|
||||||
name=script.class_name,
|
|
||||||
status__in=JobStatusChoices.TERMINAL_STATE_CHOICES
|
|
||||||
).first()
|
|
||||||
serializer = serializers.ScriptDetailSerializer(script, context={'request': request})
|
serializer = serializers.ScriptDetailSerializer(script, context={'request': request})
|
||||||
|
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
def post(self, request, pk):
|
def post(self, request, pk):
|
||||||
"""
|
"""
|
||||||
Run a Script identified as "<module>.<script>" and return the pending Job as the result
|
Run a Script identified by the id and return the pending Job as the result
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not request.user.has_perm('extras.run_script'):
|
if not request.user.has_perm('extras.run_script'):
|
||||||
raise PermissionDenied("This user does not have permission to run scripts.")
|
raise PermissionDenied("This user does not have permission to run scripts.")
|
||||||
|
|
||||||
module, script = self._get_script(pk)
|
script = get_object_or_404(self.queryset, pk=pk)
|
||||||
input_serializer = serializers.ScriptInputSerializer(
|
input_serializer = serializers.ScriptInputSerializer(
|
||||||
data=request.data,
|
data=request.data,
|
||||||
context={'script': script}
|
context={'script': script}
|
||||||
@ -286,13 +242,13 @@ class ScriptViewSet(ViewSet):
|
|||||||
if input_serializer.is_valid():
|
if input_serializer.is_valid():
|
||||||
script.result = Job.enqueue(
|
script.result = Job.enqueue(
|
||||||
run_script,
|
run_script,
|
||||||
instance=module,
|
instance=script.module,
|
||||||
name=script.class_name,
|
name=script.python_class.class_name,
|
||||||
user=request.user,
|
user=request.user,
|
||||||
data=input_serializer.data['data'],
|
data=input_serializer.data['data'],
|
||||||
request=copy_safe_request(request),
|
request=copy_safe_request(request),
|
||||||
commit=input_serializer.data['commit'],
|
commit=input_serializer.data['commit'],
|
||||||
job_timeout=script.job_timeout,
|
job_timeout=script.python_class.job_timeout,
|
||||||
schedule_at=input_serializer.validated_data.get('schedule_at'),
|
schedule_at=input_serializer.validated_data.get('schedule_at'),
|
||||||
interval=input_serializer.validated_data.get('interval')
|
interval=input_serializer.validated_data.get('interval')
|
||||||
)
|
)
|
||||||
@ -312,23 +268,23 @@ class ObjectChangeViewSet(ReadOnlyModelViewSet):
|
|||||||
Retrieve a list of recent changes.
|
Retrieve a list of recent changes.
|
||||||
"""
|
"""
|
||||||
metadata_class = ContentTypeMetadata
|
metadata_class = ContentTypeMetadata
|
||||||
queryset = ObjectChange.objects.valid_models().prefetch_related('user')
|
queryset = ObjectChange.objects.valid_models()
|
||||||
serializer_class = serializers.ObjectChangeSerializer
|
serializer_class = serializers.ObjectChangeSerializer
|
||||||
filterset_class = filtersets.ObjectChangeFilterSet
|
filterset_class = filtersets.ObjectChangeFilterSet
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# ContentTypes
|
# Object types
|
||||||
#
|
#
|
||||||
|
|
||||||
class ContentTypeViewSet(ReadOnlyModelViewSet):
|
class ObjectTypeViewSet(ReadOnlyModelViewSet):
|
||||||
"""
|
"""
|
||||||
Read-only list of ContentTypes. Limit results to ContentTypes pertinent to NetBox objects.
|
Read-only list of ObjectTypes.
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
||||||
queryset = ContentType.objects.order_by('app_label', 'model')
|
queryset = ObjectType.objects.order_by('app_label', 'model')
|
||||||
serializer_class = serializers.ContentTypeSerializer
|
serializer_class = serializers.ObjectTypeSerializer
|
||||||
filterset_class = filtersets.ContentTypeFilterSet
|
filterset_class = filtersets.ObjectTypeFilterSet
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -5,4 +5,8 @@ class ExtrasConfig(AppConfig):
|
|||||||
name = "extras"
|
name = "extras"
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
|
from netbox.models.features import register_models
|
||||||
from . import dashboard, lookups, search, signals
|
from . import dashboard, lookups, search, signals
|
||||||
|
|
||||||
|
# Register models
|
||||||
|
register_models(*self.get_models())
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import functools
|
import functools
|
||||||
import re
|
import re
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'Condition',
|
'Condition',
|
||||||
@ -50,11 +51,13 @@ class Condition:
|
|||||||
|
|
||||||
def __init__(self, attr, value, op=EQ, negate=False):
|
def __init__(self, attr, value, op=EQ, negate=False):
|
||||||
if op not in self.OPERATORS:
|
if op not in self.OPERATORS:
|
||||||
raise ValueError(f"Unknown operator: {op}. Must be one of: {', '.join(self.OPERATORS)}")
|
raise ValueError(_("Unknown operator: {op}. Must be one of: {operators}").format(
|
||||||
|
op=op, operators=', '.join(self.OPERATORS)
|
||||||
|
))
|
||||||
if type(value) not in self.TYPES:
|
if type(value) not in self.TYPES:
|
||||||
raise ValueError(f"Unsupported value type: {type(value)}")
|
raise ValueError(_("Unsupported value type: {value}").format(value=type(value)))
|
||||||
if op not in self.TYPES[type(value)]:
|
if op not in self.TYPES[type(value)]:
|
||||||
raise ValueError(f"Invalid type for {op} operation: {type(value)}")
|
raise ValueError(_("Invalid type for {op} operation: {value}").format(op=op, value=type(value)))
|
||||||
|
|
||||||
self.attr = attr
|
self.attr = attr
|
||||||
self.value = value
|
self.value = value
|
||||||
@ -131,14 +134,17 @@ class ConditionSet:
|
|||||||
"""
|
"""
|
||||||
def __init__(self, ruleset):
|
def __init__(self, ruleset):
|
||||||
if type(ruleset) is not dict:
|
if type(ruleset) is not dict:
|
||||||
raise ValueError(f"Ruleset must be a dictionary, not {type(ruleset)}.")
|
raise ValueError(_("Ruleset must be a dictionary, not {ruleset}.").format(ruleset=type(ruleset)))
|
||||||
if len(ruleset) != 1:
|
if len(ruleset) != 1:
|
||||||
raise ValueError(f"Ruleset must have exactly one logical operator (found {len(ruleset)})")
|
raise ValueError(_("Ruleset must have exactly one logical operator (found {ruleset})").format(
|
||||||
|
ruleset=len(ruleset)))
|
||||||
|
|
||||||
# Determine the logic type
|
# Determine the logic type
|
||||||
logic = list(ruleset.keys())[0]
|
logic = list(ruleset.keys())[0]
|
||||||
if type(logic) is not str or logic.lower() not in (AND, OR):
|
if type(logic) is not str or logic.lower() not in (AND, OR):
|
||||||
raise ValueError(f"Invalid logic type: {logic} (must be '{AND}' or '{OR}')")
|
raise ValueError(_("Invalid logic type: {logic} (must be '{op_and}' or '{op_or}')").format(
|
||||||
|
logic=logic, op_and=AND, op_or=OR
|
||||||
|
))
|
||||||
self.logic = logic.lower()
|
self.logic = logic.lower()
|
||||||
|
|
||||||
# Compile the set of Conditions
|
# Compile the set of Conditions
|
||||||
|
@ -2,6 +2,7 @@ import uuid
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
from extras.constants import DEFAULT_DASHBOARD
|
from extras.constants import DEFAULT_DASHBOARD
|
||||||
@ -32,7 +33,7 @@ def get_widget_class(name):
|
|||||||
try:
|
try:
|
||||||
return registry['widgets'][name]
|
return registry['widgets'][name]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise ValueError(f"Unregistered widget class: {name}")
|
raise ValueError(_("Unregistered widget class: {name}").format(name=name))
|
||||||
|
|
||||||
|
|
||||||
def get_dashboard(user):
|
def get_dashboard(user):
|
||||||
|
@ -12,7 +12,7 @@ from django.template.loader import render_to_string
|
|||||||
from django.urls import NoReverseMatch, resolve, reverse
|
from django.urls import NoReverseMatch, resolve, reverse
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from core.models import ContentType
|
from core.models import ObjectType
|
||||||
from extras.choices import BookmarkOrderingChoices
|
from extras.choices import BookmarkOrderingChoices
|
||||||
from utilities.choices import ButtonColorChoices
|
from utilities.choices import ButtonColorChoices
|
||||||
from utilities.permissions import get_permission_for_model
|
from utilities.permissions import get_permission_for_model
|
||||||
@ -34,14 +34,14 @@ __all__ = (
|
|||||||
def get_object_type_choices():
|
def get_object_type_choices():
|
||||||
return [
|
return [
|
||||||
(content_type_identifier(ct), content_type_name(ct))
|
(content_type_identifier(ct), content_type_name(ct))
|
||||||
for ct in ContentType.objects.public().order_by('app_label', 'model')
|
for ct in ObjectType.objects.public().order_by('app_label', 'model')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_bookmarks_object_type_choices():
|
def get_bookmarks_object_type_choices():
|
||||||
return [
|
return [
|
||||||
(content_type_identifier(ct), content_type_name(ct))
|
(content_type_identifier(ct), content_type_name(ct))
|
||||||
for ct in ContentType.objects.with_feature('bookmarks').order_by('app_label', 'model')
|
for ct in ObjectType.objects.with_feature('bookmarks').order_by('app_label', 'model')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -52,7 +52,7 @@ def get_models_from_content_types(content_types):
|
|||||||
models = []
|
models = []
|
||||||
for content_type_id in content_types:
|
for content_type_id in content_types:
|
||||||
app_label, model_name = content_type_id.split('.')
|
app_label, model_name = content_type_id.split('.')
|
||||||
content_type = ContentType.objects.get_by_natural_key(app_label, model_name)
|
content_type = ObjectType.objects.get_by_natural_key(app_label, model_name)
|
||||||
models.append(content_type.model_class())
|
models.append(content_type.model_class())
|
||||||
return models
|
return models
|
||||||
|
|
||||||
@ -111,7 +111,9 @@ class DashboardWidget:
|
|||||||
Params:
|
Params:
|
||||||
request: The current request
|
request: The current request
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError(f"{self.__class__} must define a render() method.")
|
raise NotImplementedError(_("{class_name} must define a render() method.").format(
|
||||||
|
class_name=self.__class__
|
||||||
|
))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
@ -177,7 +179,7 @@ class ObjectCountsWidget(DashboardWidget):
|
|||||||
try:
|
try:
|
||||||
dict(data)
|
dict(data)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
raise forms.ValidationError("Invalid format. Object filters must be passed as a dictionary.")
|
raise forms.ValidationError(_("Invalid format. Object filters must be passed as a dictionary."))
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def render(self, request):
|
def render(self, request):
|
||||||
@ -231,12 +233,12 @@ class ObjectListWidget(DashboardWidget):
|
|||||||
try:
|
try:
|
||||||
urlencode(data)
|
urlencode(data)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
raise forms.ValidationError("Invalid format. URL parameters must be passed as a dictionary.")
|
raise forms.ValidationError(_("Invalid format. URL parameters must be passed as a dictionary."))
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def render(self, request):
|
def render(self, request):
|
||||||
app_label, model_name = self.config['model'].split('.')
|
app_label, model_name = self.config['model'].split('.')
|
||||||
model = ContentType.objects.get_by_natural_key(app_label, model_name).model_class()
|
model = ObjectType.objects.get_by_natural_key(app_label, model_name).model_class()
|
||||||
viewname = get_viewname(model, action='list')
|
viewname = get_viewname(model, action='list')
|
||||||
|
|
||||||
# Evaluate user's permission. Note that this controls only whether the HTMX element is
|
# Evaluate user's permission. Note that this controls only whether the HTMX element is
|
||||||
@ -369,7 +371,7 @@ class BookmarksWidget(DashboardWidget):
|
|||||||
bookmarks = Bookmark.objects.filter(user=request.user).order_by(self.config['order_by'])
|
bookmarks = Bookmark.objects.filter(user=request.user).order_by(self.config['order_by'])
|
||||||
if object_types := self.config.get('object_types'):
|
if object_types := self.config.get('object_types'):
|
||||||
models = get_models_from_content_types(object_types)
|
models = get_models_from_content_types(object_types)
|
||||||
conent_types = ContentType.objects.get_for_models(*models).values()
|
conent_types = ObjectType.objects.get_for_models(*models).values()
|
||||||
bookmarks = bookmarks.filter(object_type__in=conent_types)
|
bookmarks = bookmarks.filter(object_type__in=conent_types)
|
||||||
if max_items := self.config.get('max_items'):
|
if max_items := self.config.get('max_items'):
|
||||||
bookmarks = bookmarks[:max_items]
|
bookmarks = bookmarks[:max_items]
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user