diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
index 43ab47c9d..b0b8c02ad 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -13,7 +13,9 @@ body:
- type: dropdown
attributes:
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:
- Self-hosted
- NetBox Cloud
@@ -23,7 +25,7 @@ body:
attributes:
label: NetBox Version
description: What version of NetBox are you currently running?
- placeholder: v3.7.2
+ placeholder: v3.7.3
validations:
required: true
- type: dropdown
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml
index a198fd731..bc99999c0 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yaml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yaml
@@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
- placeholder: v3.7.2
+ placeholder: v3.7.3
validations:
required: true
- type: dropdown
diff --git a/README.md b/README.md
index f166919c4..a81e41854 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
-
+
diff --git a/base_requirements.txt b/base_requirements.txt
index 4ac2b044a..49d259349 100644
--- a/base_requirements.txt
+++ b/base_requirements.txt
@@ -105,7 +105,7 @@ mkdocs-material
mkdocstrings[python-legacy]
# 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
# Python bindings to the ammonia HTML sanitization library.
diff --git a/docs/configuration/remote-authentication.md b/docs/configuration/remote-authentication.md
index fb789bd98..e7fe56a09 100644
--- a/docs/configuration/remote-authentication.md
+++ b/docs/configuration/remote-authentication.md
@@ -67,7 +67,7 @@ When remote user authentication is in use, this is the name of the HTTP header w
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` )
---
diff --git a/docs/customization/custom-scripts.md b/docs/customization/custom-scripts.md
index 96423a94b..bdc3f9104 100644
--- a/docs/customization/custom-scripts.md
+++ b/docs/customization/custom-scripts.md
@@ -304,6 +304,7 @@ A particular object within NetBox. Each ObjectVar must specify a particular mode
* `model` - The model class
* `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 `` elements within the dropdown menu (optional; see below)
* `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:
@@ -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
Similar to `ObjectVar`, but allows for the selection of multiple objects.
diff --git a/docs/models/dcim/device.md b/docs/models/dcim/device.md
index c9f05cd93..8b38d7c89 100644
--- a/docs/models/dcim/device.md
+++ b/docs/models/dcim/device.md
@@ -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.
-### Device Role
+### Role
-The functional [role](./devicerole.md) assigned to this device.
+The functional [device role](./devicerole.md) assigned to this device.
### Device Type
diff --git a/docs/release-notes/version-3.7.md b/docs/release-notes/version-3.7.md
index 103b0664c..21e7489c3 100644
--- a/docs/release-notes/version-3.7.md
+++ b/docs/release-notes/version-3.7.md
@@ -1,6 +1,41 @@
# 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
---
diff --git a/docs/release-notes/version-4.0.md b/docs/release-notes/version-4.0.md
index 507b8dcdd..60b3115f0 100644
--- a/docs/release-notes/version-4.0.md
+++ b/docs/release-notes/version-4.0.md
@@ -5,6 +5,7 @@
### Breaking Changes
* 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
@@ -12,23 +13,68 @@
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
* [#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
* [#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
* [#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
* [#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
* [#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
* [#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()`)
* [#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
* [#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`
diff --git a/mkdocs.yml b/mkdocs.yml
index c5816f5f5..354c10608 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -292,6 +292,7 @@ nav:
- git Cheat Sheet: 'development/git-cheat-sheet.md'
- Release Notes:
- 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.6: 'release-notes/version-3.6.md'
- Version 3.5: 'release-notes/version-3.5.md'
diff --git a/netbox/circuits/api/nested_serializers.py b/netbox/circuits/api/nested_serializers.py
index c7aa583fa..36254dc8b 100644
--- a/netbox/circuits/api/nested_serializers.py
+++ b/netbox/circuits/api/nested_serializers.py
@@ -1,8 +1,8 @@
-from drf_spectacular.utils import extend_schema_field, extend_schema_serializer
-from drf_spectacular.types import OpenApiTypes
+from drf_spectacular.utils import extend_schema_serializer
from rest_framework import serializers
from circuits.models import *
+from netbox.api.fields import RelatedObjectCountField
from netbox.api.serializers import WritableNestedSerializer
__all__ = [
@@ -36,7 +36,7 @@ class NestedProviderNetworkSerializer(WritableNestedSerializer):
)
class NestedProviderSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
- circuit_count = serializers.IntegerField(read_only=True)
+ circuit_count = RelatedObjectCountField('circuits')
class Meta:
model = Provider
@@ -64,7 +64,7 @@ class NestedProviderAccountSerializer(WritableNestedSerializer):
)
class NestedCircuitTypeSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
- circuit_count = serializers.IntegerField(read_only=True)
+ circuit_count = RelatedObjectCountField('circuits')
class Meta:
model = CircuitType
diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py
index 5223de339..5e048218c 100644
--- a/netbox/circuits/api/serializers.py
+++ b/netbox/circuits/api/serializers.py
@@ -1,137 +1,3 @@
-from rest_framework import serializers
-
-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 .serializers_.providers import *
+from .serializers_.circuits 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',
- ]
diff --git a/netbox/circuits/api/serializers_/__init__.py b/netbox/circuits/api/serializers_/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/netbox/circuits/api/serializers_/circuits.py b/netbox/circuits/api/serializers_/circuits.py
new file mode 100644
index 000000000..b59c73f09
--- /dev/null
+++ b/netbox/circuits/api/serializers_/circuits.py
@@ -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')
diff --git a/netbox/circuits/api/serializers_/providers.py b/netbox/circuits/api/serializers_/providers.py
new file mode 100644
index 000000000..302c2da5a
--- /dev/null
+++ b/netbox/circuits/api/serializers_/providers.py
@@ -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')
diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py
index bd9431887..fffb59a57 100644
--- a/netbox/circuits/api/views.py
+++ b/netbox/circuits/api/views.py
@@ -4,7 +4,6 @@ from circuits import filtersets
from circuits.models import *
from dcim.api.views import PassThroughPortMixin
from netbox.api.viewsets import NetBoxModelViewSet
-from utilities.utils import count_related
from . import serializers
@@ -21,9 +20,7 @@ class CircuitsRootView(APIRootView):
#
class ProviderViewSet(NetBoxModelViewSet):
- queryset = Provider.objects.prefetch_related('asns', 'tags').annotate(
- circuit_count=count_related(Circuit, 'provider')
- )
+ queryset = Provider.objects.all()
serializer_class = serializers.ProviderSerializer
filterset_class = filtersets.ProviderFilterSet
@@ -33,9 +30,7 @@ class ProviderViewSet(NetBoxModelViewSet):
#
class CircuitTypeViewSet(NetBoxModelViewSet):
- queryset = CircuitType.objects.prefetch_related('tags').annotate(
- circuit_count=count_related(Circuit, 'type')
- )
+ queryset = CircuitType.objects.all()
serializer_class = serializers.CircuitTypeSerializer
filterset_class = filtersets.CircuitTypeFilterSet
@@ -45,9 +40,7 @@ class CircuitTypeViewSet(NetBoxModelViewSet):
#
class CircuitViewSet(NetBoxModelViewSet):
- queryset = Circuit.objects.prefetch_related(
- 'type', 'tenant', 'provider', 'provider_account', 'termination_a', 'termination_z'
- ).prefetch_related('tags')
+ queryset = Circuit.objects.all()
serializer_class = serializers.CircuitSerializer
filterset_class = filtersets.CircuitFilterSet
@@ -57,12 +50,9 @@ class CircuitViewSet(NetBoxModelViewSet):
#
class CircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
- queryset = CircuitTermination.objects.prefetch_related(
- 'circuit', 'site', 'provider_network', 'cable__terminations'
- )
+ queryset = CircuitTermination.objects.all()
serializer_class = serializers.CircuitTerminationSerializer
filterset_class = filtersets.CircuitTerminationFilterSet
- brief_prefetch_fields = ['circuit']
#
@@ -70,7 +60,7 @@ class CircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
#
class ProviderAccountViewSet(NetBoxModelViewSet):
- queryset = ProviderAccount.objects.prefetch_related('provider', 'tags')
+ queryset = ProviderAccount.objects.all()
serializer_class = serializers.ProviderAccountSerializer
filterset_class = filtersets.ProviderAccountFilterSet
@@ -80,6 +70,6 @@ class ProviderAccountViewSet(NetBoxModelViewSet):
#
class ProviderNetworkViewSet(NetBoxModelViewSet):
- queryset = ProviderNetwork.objects.prefetch_related('tags')
+ queryset = ProviderNetwork.objects.all()
serializer_class = serializers.ProviderNetworkSerializer
filterset_class = filtersets.ProviderNetworkFilterSet
diff --git a/netbox/circuits/apps.py b/netbox/circuits/apps.py
index 3acf3b98c..df6804303 100644
--- a/netbox/circuits/apps.py
+++ b/netbox/circuits/apps.py
@@ -6,4 +6,8 @@ class CircuitsConfig(AppConfig):
verbose_name = "Circuits"
def ready(self):
+ from netbox.models.features import register_models
from . import signals, search
+
+ # Register models
+ register_models(*self.get_models())
diff --git a/netbox/circuits/models/circuits.py b/netbox/circuits/models/circuits.py
index 4dc775364..7b65d52ad 100644
--- a/netbox/circuits/models/circuits.py
+++ b/netbox/circuits/models/circuits.py
@@ -234,9 +234,9 @@ class CircuitTermination(
# Must define either site *or* provider network
if self.site is None and self.provider_network is None:
- raise ValidationError("A circuit termination must attach to either a site or a provider network.")
+ raise ValidationError(_("A circuit termination must attach to either a site or a provider network."))
if self.site and self.provider_network:
- raise ValidationError("A circuit termination cannot attach to both a site and a provider network.")
+ raise ValidationError(_("A circuit termination cannot attach to both a site and a provider network."))
def to_objectchange(self, action):
objectchange = super().to_objectchange(action)
diff --git a/netbox/circuits/tests/test_api.py b/netbox/circuits/tests/test_api.py
index 1969441eb..c8ec08943 100644
--- a/netbox/circuits/tests/test_api.py
+++ b/netbox/circuits/tests/test_api.py
@@ -18,7 +18,7 @@ class AppTest(APITestCase):
class ProviderTest(APIViewTestCases.APIViewTestCase):
model = Provider
- brief_fields = ['circuit_count', 'display', 'id', 'name', 'slug', 'url']
+ brief_fields = ['circuit_count', 'description', 'display', 'id', 'name', 'slug', 'url']
bulk_update_data = {
'comments': 'New comments',
}
@@ -60,7 +60,7 @@ class ProviderTest(APIViewTestCases.APIViewTestCase):
class CircuitTypeTest(APIViewTestCases.APIViewTestCase):
model = CircuitType
- brief_fields = ['circuit_count', 'display', 'id', 'name', 'slug', 'url']
+ brief_fields = ['circuit_count', 'description', 'display', 'id', 'name', 'slug', 'url']
create_data = (
{
'name': 'Circuit Type 4',
@@ -92,7 +92,7 @@ class CircuitTypeTest(APIViewTestCases.APIViewTestCase):
class CircuitTest(APIViewTestCases.APIViewTestCase):
model = Circuit
- brief_fields = ['cid', 'display', 'id', 'url']
+ brief_fields = ['cid', 'description', 'display', 'id', 'url']
bulk_update_data = {
'status': 'planned',
}
@@ -149,7 +149,7 @@ class CircuitTest(APIViewTestCases.APIViewTestCase):
class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
model = CircuitTermination
- brief_fields = ['_occupied', 'cable', 'circuit', 'display', 'id', 'term_side', 'url']
+ brief_fields = ['_occupied', 'cable', 'circuit', 'description', 'display', 'id', 'term_side', 'url']
@classmethod
def setUpTestData(cls):
@@ -208,7 +208,7 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
class ProviderAccountTest(APIViewTestCases.APIViewTestCase):
model = ProviderAccount
- brief_fields = ['account', 'display', 'id', 'name', 'url']
+ brief_fields = ['account', 'description', 'display', 'id', 'name', 'url']
@classmethod
def setUpTestData(cls):
@@ -251,7 +251,7 @@ class ProviderAccountTest(APIViewTestCases.APIViewTestCase):
class ProviderNetworkTest(APIViewTestCases.APIViewTestCase):
model = ProviderNetwork
- brief_fields = ['display', 'id', 'name', 'url']
+ brief_fields = ['description', 'display', 'id', 'name', 'url']
@classmethod
def setUpTestData(cls):
diff --git a/netbox/core/api/nested_serializers.py b/netbox/core/api/nested_serializers.py
index d99738cbe..efb748ee0 100644
--- a/netbox/core/api/nested_serializers.py
+++ b/netbox/core/api/nested_serializers.py
@@ -4,7 +4,7 @@ from core.choices import JobStatusChoices
from core.models import *
from netbox.api.fields import ChoiceField
from netbox.api.serializers import WritableNestedSerializer
-from users.api.nested_serializers import NestedUserSerializer
+from users.api.serializers import UserSerializer
__all__ = (
'NestedDataFileSerializer',
@@ -32,7 +32,8 @@ class NestedDataFileSerializer(WritableNestedSerializer):
class NestedJobSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail')
status = ChoiceField(choices=JobStatusChoices)
- user = NestedUserSerializer(
+ user = UserSerializer(
+ nested=True,
read_only=True
)
diff --git a/netbox/core/api/schema.py b/netbox/core/api/schema.py
index b7e537c23..8eecfa8b9 100644
--- a/netbox/core/api/schema.py
+++ b/netbox/core/api/schema.py
@@ -8,6 +8,7 @@ from drf_spectacular.plumbing import (
build_basic_type, build_choice_field, build_media_type_object, build_object_type, get_doc,
)
from drf_spectacular.types import OpenApiTypes
+from rest_framework import serializers
from rest_framework.relations import ManyRelatedField
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
diff --git a/netbox/core/api/serializers.py b/netbox/core/api/serializers.py
index a16a06d62..8553bb91c 100644
--- a/netbox/core/api/serializers.py
+++ b/netbox/core/api/serializers.py
@@ -1,73 +1,3 @@
-from rest_framework import serializers
-
-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 .serializers_.data import *
+from .serializers_.jobs 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',
- ]
diff --git a/netbox/core/api/serializers_/__init__.py b/netbox/core/api/serializers_/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/netbox/core/api/serializers_/data.py b/netbox/core/api/serializers_/data.py
new file mode 100644
index 000000000..e60d8d95b
--- /dev/null
+++ b/netbox/core/api/serializers_/data.py
@@ -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')
diff --git a/netbox/core/api/serializers_/jobs.py b/netbox/core/api/serializers_/jobs.py
new file mode 100644
index 000000000..84afa3627
--- /dev/null
+++ b/netbox/core/api/serializers_/jobs.py
@@ -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')
diff --git a/netbox/core/api/views.py b/netbox/core/api/views.py
index 7bf2f87a6..3fddfd691 100644
--- a/netbox/core/api/views.py
+++ b/netbox/core/api/views.py
@@ -9,7 +9,6 @@ from rest_framework.viewsets import ReadOnlyModelViewSet
from core import filtersets
from core.models import *
from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet
-from utilities.utils import count_related
from . import serializers
@@ -22,9 +21,7 @@ class CoreRootView(APIRootView):
class DataSourceViewSet(NetBoxModelViewSet):
- queryset = DataSource.objects.annotate(
- file_count=count_related(DataFile, 'source')
- )
+ queryset = DataSource.objects.all()
serializer_class = serializers.DataSourceSerializer
filterset_class = filtersets.DataSourceFilterSet
@@ -44,7 +41,7 @@ class DataSourceViewSet(NetBoxModelViewSet):
class DataFileViewSet(NetBoxReadOnlyModelViewSet):
- queryset = DataFile.objects.defer('data').prefetch_related('source')
+ queryset = DataFile.objects.defer('data')
serializer_class = serializers.DataFileSerializer
filterset_class = filtersets.DataFileFilterSet
@@ -53,6 +50,6 @@ class JobViewSet(ReadOnlyModelViewSet):
"""
Retrieve a list of job results
"""
- queryset = Job.objects.prefetch_related('user')
+ queryset = Job.objects.all()
serializer_class = serializers.JobSerializer
filterset_class = filtersets.JobFilterSet
diff --git a/netbox/core/apps.py b/netbox/core/apps.py
index 2d999c57e..b1103469c 100644
--- a/netbox/core/apps.py
+++ b/netbox/core/apps.py
@@ -16,5 +16,9 @@ class CoreConfig(AppConfig):
name = "core"
def ready(self):
+ from core.api import schema # noqa
+ from netbox.models.features import register_models
from . import data_backends, search
- from core.api import schema # noqa: E402
+
+ # Register models
+ register_models(*self.get_models())
diff --git a/netbox/core/data_backends.py b/netbox/core/data_backends.py
index 9ff0b4d63..15891a6f5 100644
--- a/netbox/core/data_backends.py
+++ b/netbox/core/data_backends.py
@@ -102,7 +102,7 @@ class GitBackend(DataBackend):
try:
porcelain.clone(self.url, local_path.name, **clone_args)
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
diff --git a/netbox/core/forms/filtersets.py b/netbox/core/forms/filtersets.py
index 0c164ac29..bd74c0f14 100644
--- a/netbox/core/forms/filtersets.py
+++ b/netbox/core/forms/filtersets.py
@@ -68,7 +68,7 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
)
object_type = ContentTypeChoiceField(
label=_('Object Type'),
- queryset=ContentType.objects.with_feature('jobs'),
+ queryset=ObjectType.objects.with_feature('jobs'),
required=False,
)
status = forms.MultipleChoiceField(
diff --git a/netbox/core/forms/model_forms.py b/netbox/core/forms/model_forms.py
index 52fa3a608..e0c71fe48 100644
--- a/netbox/core/forms/model_forms.py
+++ b/netbox/core/forms/model_forms.py
@@ -103,9 +103,9 @@ class ManagedFileForm(SyncedDataMixin, NetBoxModelForm):
super().clean()
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'):
- 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
diff --git a/netbox/core/management/commands/nbshell.py b/netbox/core/management/commands/nbshell.py
index eeefe502b..b96870252 100644
--- a/netbox/core/management/commands/nbshell.py
+++ b/netbox/core/management/commands/nbshell.py
@@ -8,7 +8,7 @@ from django.conf import settings
from django.contrib.auth import get_user_model
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')
@@ -60,7 +60,7 @@ class Command(BaseCommand):
pass
# Additional objects to include
- namespace['ContentType'] = ContentType
+ namespace['ObjectType'] = ObjectType
namespace['User'] = get_user_model()
# Load convenience commands
diff --git a/netbox/core/migrations/0008_contenttype_proxy.py b/netbox/core/migrations/0008_contenttype_proxy.py
index ac11d906a..dee82a969 100644
--- a/netbox/core/migrations/0008_contenttype_proxy.py
+++ b/netbox/core/migrations/0008_contenttype_proxy.py
@@ -1,5 +1,3 @@
-# Generated by Django 4.2.6 on 2023-10-31 19:38
-
import core.models.contenttypes
from django.db import migrations
@@ -13,7 +11,7 @@ class Migration(migrations.Migration):
operations = [
migrations.CreateModel(
- name='ContentType',
+ name='ObjectType',
fields=[
],
options={
@@ -23,7 +21,7 @@ class Migration(migrations.Migration):
},
bases=('contenttypes.contenttype',),
managers=[
- ('objects', core.models.contenttypes.ContentTypeManager()),
+ ('objects', core.models.contenttypes.ObjectTypeManager()),
],
),
]
diff --git a/netbox/core/models/config.py b/netbox/core/models/config.py
index 6c8e41477..b2381ae40 100644
--- a/netbox/core/models/config.py
+++ b/netbox/core/models/config.py
@@ -44,7 +44,7 @@ class ConfigRevision(models.Model):
return gettext('Config revision #{id}').format(id=self.pk)
def __getattr__(self, item):
- if item in self.data:
+ if self.data and item in self.data:
return self.data[item]
return super().__getattribute__(item)
diff --git a/netbox/core/models/contenttypes.py b/netbox/core/models/contenttypes.py
index c98184c3d..b0301848f 100644
--- a/netbox/core/models/contenttypes.py
+++ b/netbox/core/models/contenttypes.py
@@ -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 netbox.registry import registry
__all__ = (
- 'ContentType',
- 'ContentTypeManager',
+ 'ObjectType',
+ 'ObjectTypeManager',
)
-class ContentTypeManager(ContentTypeManager_):
+class ObjectTypeManager(ContentTypeManager):
def public(self):
"""
@@ -40,11 +40,11 @@ class ContentTypeManager(ContentTypeManager_):
return self.get_queryset().filter(q)
-class ContentType(ContentType_):
+class ObjectType(ContentType):
"""
Wrap Django's native ContentType model to use our custom manager.
"""
- objects = ContentTypeManager()
+ objects = ObjectTypeManager()
class Meta:
proxy = True
diff --git a/netbox/core/models/data.py b/netbox/core/models/data.py
index 6597a4b4d..4ceb22ba9 100644
--- a/netbox/core/models/data.py
+++ b/netbox/core/models/data.py
@@ -177,7 +177,7 @@ class DataSource(JobsMixin, PrimaryModel):
Create/update/delete child DataFiles as necessary to synchronize with the remote source.
"""
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
pre_sync.send(sender=self.__class__, instance=self)
@@ -190,7 +190,7 @@ class DataSource(JobsMixin, PrimaryModel):
backend = self.get_backend()
except ModuleNotFoundError as e:
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:
diff --git a/netbox/core/models/jobs.py b/netbox/core/models/jobs.py
index 7cc62a15a..b9f0d0b91 100644
--- a/netbox/core/models/jobs.py
+++ b/netbox/core/models/jobs.py
@@ -11,7 +11,7 @@ from django.utils import timezone
from django.utils.translation import gettext as _
from core.choices import JobStatusChoices
-from core.models import ContentType
+from core.models import ObjectType
from core.signals import job_end, job_start
from extras.constants import EVENT_JOB_END, EVENT_JOB_START
from netbox.config import get_config
@@ -130,7 +130,7 @@ class Job(models.Model):
super().clean()
# 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(
_("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
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
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
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)
queue = django_rq.get_queue(rq_queue_name)
status = JobStatusChoices.STATUS_SCHEDULED if schedule_at else JobStatusChoices.STATUS_PENDING
diff --git a/netbox/core/tests/test_api.py b/netbox/core/tests/test_api.py
index cd25761f0..44db21bff 100644
--- a/netbox/core/tests/test_api.py
+++ b/netbox/core/tests/test_api.py
@@ -16,7 +16,7 @@ class AppTest(APITestCase):
class DataSourceTest(APIViewTestCases.APIViewTestCase):
model = DataSource
- brief_fields = ['display', 'id', 'name', 'url']
+ brief_fields = ['description', 'display', 'id', 'name', 'url']
bulk_update_data = {
'enabled': False,
'description': 'foo bar baz',
diff --git a/netbox/core/views.py b/netbox/core/views.py
index 5662b126e..0379900e1 100644
--- a/netbox/core/views.py
+++ b/netbox/core/views.py
@@ -184,7 +184,7 @@ class ConfigView(generic.ObjectView):
except ConfigRevision.DoesNotExist:
# Fall back to using the active config data if no record is found
return ConfigRevision(
- data=get_config()
+ data=get_config().defaults
)
diff --git a/netbox/dcim/api/nested_serializers.py b/netbox/dcim/api/nested_serializers.py
index c8440612d..1d9828ee3 100644
--- a/netbox/dcim/api/nested_serializers.py
+++ b/netbox/dcim/api/nested_serializers.py
@@ -2,11 +2,10 @@ from drf_spectacular.utils import extend_schema_serializer
from rest_framework import serializers
from dcim import models
-from netbox.api.serializers import BaseModelSerializer, WritableNestedSerializer
+from netbox.api.fields import RelatedObjectCountField
+from netbox.api.serializers import WritableNestedSerializer
__all__ = [
- 'ComponentNestedModuleSerializer',
- 'ModuleBayNestedModuleSerializer',
'NestedCableSerializer',
'NestedConsolePortSerializer',
'NestedConsolePortTemplateSerializer',
@@ -110,7 +109,7 @@ class NestedLocationSerializer(WritableNestedSerializer):
)
class NestedRackRoleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
- rack_count = serializers.IntegerField(read_only=True)
+ rack_count = RelatedObjectCountField('racks')
class Meta:
model = models.RackRole
@@ -122,7 +121,7 @@ class NestedRackRoleSerializer(WritableNestedSerializer):
)
class NestedRackSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
- device_count = serializers.IntegerField(read_only=True)
+ device_count = RelatedObjectCountField('devices')
class Meta:
model = models.Rack
@@ -150,7 +149,7 @@ class NestedRackReservationSerializer(WritableNestedSerializer):
)
class NestedManufacturerSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
- devicetype_count = serializers.IntegerField(read_only=True)
+ devicetype_count = RelatedObjectCountField('device_types')
class Meta:
model = models.Manufacturer
@@ -163,7 +162,7 @@ class NestedManufacturerSerializer(WritableNestedSerializer):
class NestedDeviceTypeSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
manufacturer = NestedManufacturerSerializer(read_only=True)
- device_count = serializers.IntegerField(read_only=True)
+ device_count = RelatedObjectCountField('instances')
class Meta:
model = models.DeviceType
@@ -173,7 +172,6 @@ class NestedDeviceTypeSerializer(WritableNestedSerializer):
class NestedModuleTypeSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
manufacturer = NestedManufacturerSerializer(read_only=True)
- # module_count = serializers.IntegerField(read_only=True)
class Meta:
model = models.ModuleType
@@ -274,8 +272,8 @@ class NestedInventoryItemTemplateSerializer(WritableNestedSerializer):
)
class NestedDeviceRoleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
- device_count = serializers.IntegerField(read_only=True)
- virtualmachine_count = serializers.IntegerField(read_only=True)
+ device_count = RelatedObjectCountField('devices')
+ virtualmachine_count = RelatedObjectCountField('virtual_machines')
class Meta:
model = models.DeviceRole
@@ -287,8 +285,8 @@ class NestedDeviceRoleSerializer(WritableNestedSerializer):
)
class NestedPlatformSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
- device_count = serializers.IntegerField(read_only=True)
- virtualmachine_count = serializers.IntegerField(read_only=True)
+ device_count = RelatedObjectCountField('devices')
+ virtualmachine_count = RelatedObjectCountField('virtual_machines')
class Meta:
model = models.Platform
@@ -311,26 +309,6 @@ class ModuleNestedModuleBaySerializer(WritableNestedSerializer):
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):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
device = NestedDeviceSerializer(read_only=True)
@@ -445,7 +423,7 @@ class NestedInventoryItemSerializer(WritableNestedSerializer):
)
class NestedInventoryItemRoleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail')
- inventoryitem_count = serializers.IntegerField(read_only=True)
+ inventoryitem_count = RelatedObjectCountField('inventory_items')
class Meta:
model = models.InventoryItemRole
@@ -490,7 +468,7 @@ class NestedVirtualChassisSerializer(WritableNestedSerializer):
)
class NestedPowerPanelSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
- powerfeed_count = serializers.IntegerField(read_only=True)
+ powerfeed_count = RelatedObjectCountField('powerfeeds')
class Meta:
model = models.PowerPanel
diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py
index 09933f2de..4f8bbac17 100644
--- a/netbox/dcim/api/serializers.py
+++ b/netbox/dcim/api/serializers.py
@@ -1,1253 +1,14 @@
-import decimal
-
-from django.contrib.contenttypes.models import ContentType
-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 timezone_field.rest_framework import TimeZoneSerializerField
-
-from dcim.choices import *
-from dcim.constants import *
-from dcim.models import *
-from extras.api.nested_serializers import NestedConfigTemplateSerializer
-from ipam.api.nested_serializers import (
- NestedASNSerializer, NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer,
-)
-from ipam.models import ASN, VLAN
-from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
-from netbox.api.serializers import (
- GenericObjectSerializer, NestedGroupModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer,
- WritableNestedSerializer,
-)
-from netbox.config import ConfigItem
-from netbox.constants import NESTED_SERIALIZER_PREFIX
-from tenancy.api.nested_serializers import NestedTenantSerializer
-from users.api.nested_serializers import NestedUserSerializer
-from utilities.api import get_serializer_for_model
-from virtualization.api.nested_serializers import NestedClusterSerializer
-from vpn.api.nested_serializers import NestedL2VPNTerminationSerializer
-from wireless.api.nested_serializers import NestedWirelessLANSerializer, NestedWirelessLinkSerializer
-from wireless.choices import *
-from wireless.models import WirelessLAN
+from .serializers_.cables import *
+from .serializers_.sites import *
+from .serializers_.racks import *
+from .serializers_.manufacturers import *
+from .serializers_.platforms import *
+from .serializers_.roles import *
+from .serializers_.devicetypes import *
+from .serializers_.devicetype_components import *
+from .serializers_.virtualchassis import *
+from .serializers_.devices import *
+from .serializers_.device_components import *
+from .serializers_.power import *
+from .serializers_.rackunits import *
from .nested_serializers import *
-
-
-class CabledObjectSerializer(serializers.ModelSerializer):
- cable = NestedCableSerializer(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], prefix=NESTED_SERIALIZER_PREFIX)
- context = {'request': self.context['request']}
- return serializer(obj.link_peers, context=context, many=True).data
-
- @extend_schema_field(serializers.BooleanField)
- def get__occupied(self, obj):
- return obj._occupied
-
-
-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], prefix=NESTED_SERIALIZER_PREFIX)
- context = {'request': self.context['request']}
- return serializer(endpoints, 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
-
-
-#
-# Regions/sites
-#
-
-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',
- ]
-
-
-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',
- ]
-
-
-class SiteSerializer(NetBoxModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
- status = ChoiceField(choices=SiteStatusChoices, required=False)
- region = NestedRegionSerializer(required=False, allow_null=True)
- group = NestedSiteGroupSerializer(required=False, allow_null=True)
- tenant = NestedTenantSerializer(required=False, allow_null=True)
- time_zone = TimeZoneSerializerField(required=False, allow_null=True)
- asns = SerializedPKRelatedField(
- queryset=ASN.objects.all(),
- serializer=NestedASNSerializer,
- required=False,
- many=True
- )
-
- # Related object counts
- circuit_count = serializers.IntegerField(read_only=True)
- device_count = serializers.IntegerField(read_only=True)
- prefix_count = serializers.IntegerField(read_only=True)
- rack_count = serializers.IntegerField(read_only=True)
- virtualmachine_count = serializers.IntegerField(read_only=True)
- vlan_count = serializers.IntegerField(read_only=True)
-
- 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',
- ]
-
-
-#
-# Racks
-#
-
-class LocationSerializer(NestedGroupModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail')
- site = NestedSiteSerializer()
- parent = NestedLocationSerializer(required=False, allow_null=True)
- status = ChoiceField(choices=LocationStatusChoices, required=False)
- tenant = NestedTenantSerializer(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',
- ]
-
-
-class RackRoleSerializer(NetBoxModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
- rack_count = serializers.IntegerField(read_only=True)
-
- class Meta:
- model = RackRole
- fields = [
- 'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created',
- 'last_updated', 'rack_count',
- ]
-
-
-class RackSerializer(NetBoxModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
- site = NestedSiteSerializer()
- location = NestedLocationSerializer(required=False, allow_null=True, default=None)
- tenant = NestedTenantSerializer(required=False, allow_null=True)
- status = ChoiceField(choices=RackStatusChoices, required=False)
- role = NestedRackRoleSerializer(required=False, allow_null=True)
- 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)
- device_count = serializers.IntegerField(read_only=True)
- powerfeed_count = serializers.IntegerField(read_only=True)
-
- 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',
- ]
-
-
-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 = NestedDeviceSerializer(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']
-
-
-class RackReservationSerializer(NetBoxModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail')
- rack = NestedRackSerializer()
- user = NestedUserSerializer()
- tenant = NestedTenantSerializer(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',
- ]
-
-
-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
- )
-
-
-#
-# Device/module types
-#
-
-class ManufacturerSerializer(NetBoxModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
- devicetype_count = serializers.IntegerField(read_only=True)
- inventoryitem_count = serializers.IntegerField(read_only=True)
- platform_count = serializers.IntegerField(read_only=True)
-
- class Meta:
- model = Manufacturer
- fields = [
- 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
- 'devicetype_count', 'inventoryitem_count', 'platform_count',
- ]
-
-
-class DeviceTypeSerializer(NetBoxModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
- manufacturer = NestedManufacturerSerializer()
- default_platform = NestedPlatformSerializer(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)
- device_count = serializers.IntegerField(read_only=True)
-
- # 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)
-
- 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',
- ]
-
-
-class ModuleTypeSerializer(NetBoxModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
- manufacturer = NestedManufacturerSerializer()
- 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',
- ]
-
-
-#
-# Component templates
-#
-
-class ConsolePortTemplateSerializer(ValidatedModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleporttemplate-detail')
- device_type = NestedDeviceTypeSerializer(
- required=False,
- allow_null=True,
- default=None
- )
- module_type = NestedModuleTypeSerializer(
- 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',
- ]
-
-
-class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverporttemplate-detail')
- device_type = NestedDeviceTypeSerializer(
- required=False,
- allow_null=True,
- default=None
- )
- module_type = NestedModuleTypeSerializer(
- 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',
- ]
-
-
-class PowerPortTemplateSerializer(ValidatedModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerporttemplate-detail')
- device_type = NestedDeviceTypeSerializer(
- required=False,
- allow_null=True,
- default=None
- )
- module_type = NestedModuleTypeSerializer(
- 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',
- ]
-
-
-class PowerOutletTemplateSerializer(ValidatedModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlettemplate-detail')
- device_type = NestedDeviceTypeSerializer(
- required=False,
- allow_null=True,
- default=None
- )
- module_type = NestedModuleTypeSerializer(
- required=False,
- allow_null=True,
- default=None
- )
- type = ChoiceField(
- choices=PowerOutletTypeChoices,
- allow_blank=True,
- required=False,
- allow_null=True
- )
- power_port = NestedPowerPortTemplateSerializer(
- 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',
- ]
-
-
-class InterfaceTemplateSerializer(ValidatedModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfacetemplate-detail')
- device_type = NestedDeviceTypeSerializer(
- required=False,
- allow_null=True,
- default=None
- )
- module_type = NestedModuleTypeSerializer(
- 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',
- ]
-
-
-class RearPortTemplateSerializer(ValidatedModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearporttemplate-detail')
- device_type = NestedDeviceTypeSerializer(
- required=False,
- allow_null=True,
- default=None
- )
- module_type = NestedModuleTypeSerializer(
- 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',
- ]
-
-
-class FrontPortTemplateSerializer(ValidatedModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontporttemplate-detail')
- device_type = NestedDeviceTypeSerializer(
- required=False,
- allow_null=True,
- default=None
- )
- module_type = NestedModuleTypeSerializer(
- required=False,
- allow_null=True,
- default=None
- )
- type = ChoiceField(choices=PortTypeChoices)
- rear_port = NestedRearPortTemplateSerializer()
-
- 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',
- ]
-
-
-class ModuleBayTemplateSerializer(ValidatedModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebaytemplate-detail')
- device_type = NestedDeviceTypeSerializer()
-
- class Meta:
- model = ModuleBayTemplate
- fields = [
- 'id', 'url', 'display', 'device_type', 'name', 'label', 'position', 'description', 'created',
- 'last_updated',
- ]
-
-
-class DeviceBayTemplateSerializer(ValidatedModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebaytemplate-detail')
- device_type = NestedDeviceTypeSerializer()
-
- class Meta:
- model = DeviceBayTemplate
- fields = ['id', 'url', 'display', 'device_type', 'name', 'label', 'description', 'created', 'last_updated']
-
-
-class InventoryItemTemplateSerializer(ValidatedModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemtemplate-detail')
- device_type = NestedDeviceTypeSerializer()
- parent = serializers.PrimaryKeyRelatedField(
- queryset=InventoryItemTemplate.objects.all(),
- allow_null=True,
- default=None
- )
- role = NestedInventoryItemRoleSerializer(required=False, allow_null=True)
- manufacturer = NestedManufacturerSerializer(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',
- ]
-
- @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, prefix=NESTED_SERIALIZER_PREFIX)
- context = {'request': self.context['request']}
- return serializer(obj.component, context=context).data
-
-
-#
-# Devices
-#
-
-class DeviceRoleSerializer(NetBoxModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
- config_template = NestedConfigTemplateSerializer(required=False, allow_null=True, default=None)
- device_count = serializers.IntegerField(read_only=True)
- virtualmachine_count = serializers.IntegerField(read_only=True)
-
- 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',
- ]
-
-
-class PlatformSerializer(NetBoxModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
- manufacturer = NestedManufacturerSerializer(required=False, allow_null=True)
- config_template = NestedConfigTemplateSerializer(required=False, allow_null=True, default=None)
- device_count = serializers.IntegerField(read_only=True)
- virtualmachine_count = serializers.IntegerField(read_only=True)
-
- class Meta:
- model = Platform
- fields = [
- 'id', 'url', 'display', 'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags',
- 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
- ]
-
-
-class DeviceSerializer(NetBoxModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
- device_type = NestedDeviceTypeSerializer()
- role = NestedDeviceRoleSerializer()
- device_role = NestedDeviceRoleSerializer(read_only=True, help_text='Deprecated in v3.6 in favor of `role`.')
- tenant = NestedTenantSerializer(required=False, allow_null=True, default=None)
- platform = NestedPlatformSerializer(required=False, allow_null=True)
- site = NestedSiteSerializer()
- location = NestedLocationSerializer(required=False, allow_null=True, default=None)
- rack = NestedRackSerializer(required=False, allow_null=True, 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 = NestedIPAddressSerializer(read_only=True)
- primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
- primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
- oob_ip = NestedIPAddressSerializer(required=False, allow_null=True)
- parent_device = serializers.SerializerMethodField()
- cluster = NestedClusterSerializer(required=False, allow_null=True)
- virtual_chassis = NestedVirtualChassisSerializer(required=False, allow_null=True, default=None)
- vc_position = serializers.IntegerField(allow_null=True, max_value=255, min_value=0, default=None)
- config_template = NestedConfigTemplateSerializer(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', 'device_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',
- ]
-
- @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
-
- def get_device_role(self, obj):
- return obj.role
-
-
-class DeviceWithConfigContextSerializer(DeviceSerializer):
- config_context = serializers.SerializerMethodField(read_only=True)
-
- class Meta(DeviceSerializer.Meta):
- fields = [
- 'id', 'url', 'display', 'name', 'device_type', 'role', 'device_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 = NestedDeviceSerializer()
- tenant = NestedTenantSerializer(required=False, allow_null=True, default=None)
- primary_ip = NestedIPAddressSerializer(read_only=True, allow_null=True)
- primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
- primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
- status = ChoiceField(choices=VirtualDeviceContextStatusChoices)
-
- # Related object counts
- interface_count = serializers.IntegerField(read_only=True)
-
- class Meta:
- model = VirtualDeviceContext
- fields = [
- 'id', 'url', 'display', 'name', 'device', 'identifier', 'tenant', 'primary_ip', 'primary_ip4',
- 'primary_ip6', 'status', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
- 'interface_count',
- ]
-
-
-class ModuleSerializer(NetBoxModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
- device = NestedDeviceSerializer()
- module_bay = NestedModuleBaySerializer()
- module_type = NestedModuleTypeSerializer()
- status = ChoiceField(choices=ModuleStatusChoices, required=False)
-
- class Meta:
- model = Module
- fields = [
- 'id', 'url', 'display', 'device', 'module_bay', 'module_type', 'status', 'serial', 'asset_tag',
- 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
- ]
-
-
-#
-# Device components
-#
-
-class ConsoleServerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
- device = NestedDeviceSerializer()
- module = ComponentNestedModuleSerializer(
- 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',
- ]
-
-
-class ConsolePortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
- device = NestedDeviceSerializer()
- module = ComponentNestedModuleSerializer(
- 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',
- ]
-
-
-class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
- device = NestedDeviceSerializer()
- module = ComponentNestedModuleSerializer(
- required=False,
- allow_null=True
- )
- type = ChoiceField(
- choices=PowerOutletTypeChoices,
- allow_blank=True,
- required=False,
- allow_null=True
- )
- power_port = NestedPowerPortSerializer(
- 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',
- ]
-
-
-class PowerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
- device = NestedDeviceSerializer()
- module = ComponentNestedModuleSerializer(
- 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',
- ]
-
-
-class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
- device = NestedDeviceSerializer()
- vdcs = SerializedPKRelatedField(
- queryset=VirtualDeviceContext.objects.all(),
- serializer=NestedVirtualDeviceContextSerializer,
- required=False,
- many=True
- )
- module = ComponentNestedModuleSerializer(
- required=False,
- allow_null=True
- )
- 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 = NestedVLANSerializer(required=False, allow_null=True)
- tagged_vlans = SerializedPKRelatedField(
- queryset=VLAN.objects.all(),
- serializer=NestedVLANSerializer,
- required=False,
- many=True
- )
- vrf = NestedVRFSerializer(required=False, allow_null=True)
- l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True, allow_null=True)
- wireless_link = NestedWirelessLinkSerializer(read_only=True, allow_null=True)
- wireless_lans = SerializedPKRelatedField(
- queryset=WirelessLAN.objects.all(),
- serializer=NestedWirelessLANSerializer,
- 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',
- ]
-
- def validate(self, data):
-
- # Validate many-to-many VLAN assignments
- 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, or "
- f"it must be global."
- })
-
- return super().validate(data)
-
-
-class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
- device = NestedDeviceSerializer()
- module = ComponentNestedModuleSerializer(
- 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',
- ]
-
-
-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 = NestedDeviceSerializer()
- module = ComponentNestedModuleSerializer(
- 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',
- ]
-
-
-class ModuleBaySerializer(NetBoxModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
- device = NestedDeviceSerializer()
- installed_module = ModuleBayNestedModuleSerializer(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',
- ]
-
-
-class DeviceBaySerializer(NetBoxModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
- device = NestedDeviceSerializer()
- installed_device = NestedDeviceSerializer(required=False, allow_null=True)
-
- class Meta:
- model = DeviceBay
- fields = [
- 'id', 'url', 'display', 'device', 'name', 'label', 'description', 'installed_device', 'tags',
- 'custom_fields', 'created', 'last_updated',
- ]
-
-
-class InventoryItemSerializer(NetBoxModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
- device = NestedDeviceSerializer()
- parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
- role = NestedInventoryItemRoleSerializer(required=False, allow_null=True)
- manufacturer = NestedManufacturerSerializer(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',
- ]
-
- @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, prefix=NESTED_SERIALIZER_PREFIX)
- context = {'request': self.context['request']}
- return serializer(obj.component, context=context).data
-
-
-#
-# Device component roles
-#
-
-class InventoryItemRoleSerializer(NetBoxModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail')
- inventoryitem_count = serializers.IntegerField(read_only=True)
-
- class Meta:
- model = InventoryItemRole
- fields = [
- 'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created',
- 'last_updated', 'inventoryitem_count',
- ]
-
-
-#
-# Cables
-#
-
-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 = NestedTenantSerializer(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',
- ]
-
-
-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, prefix=NESTED_SERIALIZER_PREFIX)
- context = {'request': self.context['request']}
- return serializer(obj.termination, 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], prefix=NESTED_SERIALIZER_PREFIX)
- context = {'request': self.context['request']}
- ret.append(serializer(nodes, context=context, many=True).data)
- return ret
-
-
-#
-# Virtual chassis
-#
-
-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',
- ]
-
-
-#
-# Power panels
-#
-
-class PowerPanelSerializer(NetBoxModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
- site = NestedSiteSerializer()
- location = NestedLocationSerializer(
- required=False,
- allow_null=True,
- default=None
- )
- powerfeed_count = serializers.IntegerField(read_only=True)
-
- class Meta:
- model = PowerPanel
- fields = [
- 'id', 'url', 'display', 'site', 'location', 'name', 'description', 'comments', 'tags', 'custom_fields',
- 'powerfeed_count', 'created', 'last_updated',
- ]
-
-
-class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
- power_panel = NestedPowerPanelSerializer()
- rack = NestedRackSerializer(
- 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 = NestedTenantSerializer(
- 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',
- ]
diff --git a/netbox/dcim/api/serializers_/__init__.py b/netbox/dcim/api/serializers_/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/netbox/dcim/api/serializers_/base.py b/netbox/dcim/api/serializers_/base.py
new file mode 100644
index 000000000..48f4967e3
--- /dev/null
+++ b/netbox/dcim/api/serializers_/base.py
@@ -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
diff --git a/netbox/dcim/api/serializers_/cables.py b/netbox/dcim/api/serializers_/cables.py
new file mode 100644
index 000000000..94a125d0c
--- /dev/null
+++ b/netbox/dcim/api/serializers_/cables.py
@@ -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
diff --git a/netbox/dcim/api/serializers_/device_components.py b/netbox/dcim/api/serializers_/device_components.py
new file mode 100644
index 000000000..87d142978
--- /dev/null
+++ b/netbox/dcim/api/serializers_/device_components.py
@@ -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
diff --git a/netbox/dcim/api/serializers_/devices.py b/netbox/dcim/api/serializers_/devices.py
new file mode 100644
index 000000000..303c35532
--- /dev/null
+++ b/netbox/dcim/api/serializers_/devices.py
@@ -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')
diff --git a/netbox/dcim/api/serializers_/devicetype_components.py b/netbox/dcim/api/serializers_/devicetype_components.py
new file mode 100644
index 000000000..259a5df27
--- /dev/null
+++ b/netbox/dcim/api/serializers_/devicetype_components.py
@@ -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
diff --git a/netbox/dcim/api/serializers_/devicetypes.py b/netbox/dcim/api/serializers_/devicetypes.py
new file mode 100644
index 000000000..2384f7b02
--- /dev/null
+++ b/netbox/dcim/api/serializers_/devicetypes.py
@@ -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')
diff --git a/netbox/dcim/api/serializers_/manufacturers.py b/netbox/dcim/api/serializers_/manufacturers.py
new file mode 100644
index 000000000..fd50fe97d
--- /dev/null
+++ b/netbox/dcim/api/serializers_/manufacturers.py
@@ -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')
diff --git a/netbox/dcim/api/serializers_/platforms.py b/netbox/dcim/api/serializers_/platforms.py
new file mode 100644
index 000000000..7365404eb
--- /dev/null
+++ b/netbox/dcim/api/serializers_/platforms.py
@@ -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')
diff --git a/netbox/dcim/api/serializers_/power.py b/netbox/dcim/api/serializers_/power.py
new file mode 100644
index 000000000..dddd54906
--- /dev/null
+++ b/netbox/dcim/api/serializers_/power.py
@@ -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')
diff --git a/netbox/dcim/api/serializers_/racks.py b/netbox/dcim/api/serializers_/racks.py
new file mode 100644
index 000000000..a6754cba0
--- /dev/null
+++ b/netbox/dcim/api/serializers_/racks.py
@@ -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
+ )
diff --git a/netbox/dcim/api/serializers_/rackunits.py b/netbox/dcim/api/serializers_/rackunits.py
new file mode 100644
index 000000000..1f5306718
--- /dev/null
+++ b/netbox/dcim/api/serializers_/rackunits.py
@@ -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']
diff --git a/netbox/dcim/api/serializers_/roles.py b/netbox/dcim/api/serializers_/roles.py
new file mode 100644
index 000000000..41f8f377d
--- /dev/null
+++ b/netbox/dcim/api/serializers_/roles.py
@@ -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')
diff --git a/netbox/dcim/api/serializers_/sites.py b/netbox/dcim/api/serializers_/sites.py
new file mode 100644
index 000000000..6fb3811ba
--- /dev/null
+++ b/netbox/dcim/api/serializers_/sites.py
@@ -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')
diff --git a/netbox/dcim/api/serializers_/virtualchassis.py b/netbox/dcim/api/serializers_/virtualchassis.py
new file mode 100644
index 000000000..570abfc7d
--- /dev/null
+++ b/netbox/dcim/api/serializers_/virtualchassis.py
@@ -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')
diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py
index cd5a297c9..668af28da 100644
--- a/netbox/dcim/api/views.py
+++ b/netbox/dcim/api/views.py
@@ -7,23 +7,18 @@ from rest_framework.response import Response
from rest_framework.routers import APIRootView
from rest_framework.viewsets import ViewSet
-from circuits.models import Circuit
from dcim import filtersets
from dcim.constants import CABLE_TRACE_SVG_DEFAULT_WIDTH
from dcim.models import *
from dcim.svg import CableTraceSVG
from extras.api.mixins import ConfigContextQuerySetMixin, RenderConfigMixin
-from ipam.models import Prefix, VLAN
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
from netbox.api.metadata import ContentTypeMetadata
from netbox.api.pagination import StripCountAnnotationsPaginator
from netbox.api.viewsets import NetBoxModelViewSet, MPTTLockedMixin
from netbox.api.viewsets.mixins import SequentialBulkCreatesMixin
-from netbox.constants import NESTED_SERIALIZER_PREFIX
from utilities.api import get_serializer_for_model
from utilities.query_functions import CollateAsChar
-from utilities.utils import count_related
-from virtualization.models import VirtualMachine
from . import serializers
from .exceptions import MissingFilterException
@@ -62,16 +57,16 @@ class PathEndpointMixin(object):
# Serialize path objects, iterating over each three-tuple in the path
for near_ends, cable, far_ends in obj.trace():
if near_ends:
- serializer_a = get_serializer_for_model(near_ends[0], prefix=NESTED_SERIALIZER_PREFIX)
- near_ends = serializer_a(near_ends, many=True, context={'request': request}).data
+ serializer_a = get_serializer_for_model(near_ends[0])
+ near_ends = serializer_a(near_ends, nested=True, many=True, context={'request': request}).data
else:
# Path is split; stop here
break
if cable:
cable = serializers.TracedCableSerializer(cable[0], context={'request': request}).data
if far_ends:
- serializer_b = get_serializer_for_model(far_ends[0], prefix=NESTED_SERIALIZER_PREFIX)
- far_ends = serializer_b(far_ends, many=True, context={'request': request}).data
+ serializer_b = get_serializer_for_model(far_ends[0])
+ far_ends = serializer_b(far_ends, nested=True, many=True, context={'request': request}).data
path.append((near_ends, cable, far_ends))
@@ -103,7 +98,7 @@ class RegionViewSet(MPTTLockedMixin, NetBoxModelViewSet):
'region',
'site_count',
cumulative=True
- ).prefetch_related('tags')
+ )
serializer_class = serializers.RegionSerializer
filterset_class = filtersets.RegionFilterSet
@@ -119,7 +114,7 @@ class SiteGroupViewSet(MPTTLockedMixin, NetBoxModelViewSet):
'group',
'site_count',
cumulative=True
- ).prefetch_related('tags')
+ )
serializer_class = serializers.SiteGroupSerializer
filterset_class = filtersets.SiteGroupFilterSet
@@ -129,16 +124,7 @@ class SiteGroupViewSet(MPTTLockedMixin, NetBoxModelViewSet):
#
class SiteViewSet(NetBoxModelViewSet):
- queryset = Site.objects.prefetch_related(
- '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')
- )
+ queryset = Site.objects.all()
serializer_class = serializers.SiteSerializer
filterset_class = filtersets.SiteFilterSet
@@ -160,7 +146,7 @@ class LocationViewSet(MPTTLockedMixin, NetBoxModelViewSet):
'location',
'rack_count',
cumulative=True
- ).prefetch_related('site', 'tags')
+ )
serializer_class = serializers.LocationSerializer
filterset_class = filtersets.LocationFilterSet
@@ -170,9 +156,7 @@ class LocationViewSet(MPTTLockedMixin, NetBoxModelViewSet):
#
class RackRoleViewSet(NetBoxModelViewSet):
- queryset = RackRole.objects.prefetch_related('tags').annotate(
- rack_count=count_related(Rack, 'role')
- )
+ queryset = RackRole.objects.all()
serializer_class = serializers.RackRoleSerializer
filterset_class = filtersets.RackRoleFilterSet
@@ -182,15 +166,16 @@ class RackRoleViewSet(NetBoxModelViewSet):
#
class RackViewSet(NetBoxModelViewSet):
- queryset = Rack.objects.prefetch_related(
- 'site', 'location', 'role', 'tenant', 'tags'
- ).annotate(
- device_count=count_related(Device, 'rack'),
- powerfeed_count=count_related(PowerFeed, 'rack')
- )
+ queryset = Rack.objects.all()
serializer_class = serializers.RackSerializer
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)
def elevation(self, request, pk=None):
"""
@@ -249,7 +234,7 @@ class RackViewSet(NetBoxModelViewSet):
#
class RackReservationViewSet(NetBoxModelViewSet):
- queryset = RackReservation.objects.prefetch_related('rack', 'user', 'tenant')
+ queryset = RackReservation.objects.all()
serializer_class = serializers.RackReservationSerializer
filterset_class = filtersets.RackReservationFilterSet
@@ -259,11 +244,7 @@ class RackReservationViewSet(NetBoxModelViewSet):
#
class ManufacturerViewSet(NetBoxModelViewSet):
- queryset = Manufacturer.objects.prefetch_related('tags').annotate(
- devicetype_count=count_related(DeviceType, 'manufacturer'),
- inventoryitem_count=count_related(InventoryItem, 'manufacturer'),
- platform_count=count_related(Platform, 'manufacturer')
- )
+ queryset = Manufacturer.objects.all()
serializer_class = serializers.ManufacturerSerializer
filterset_class = filtersets.ManufacturerFilterSet
@@ -273,21 +254,15 @@ class ManufacturerViewSet(NetBoxModelViewSet):
#
class DeviceTypeViewSet(NetBoxModelViewSet):
- queryset = DeviceType.objects.prefetch_related('manufacturer', 'default_platform', 'tags').annotate(
- device_count=count_related(Device, 'device_type')
- )
+ queryset = DeviceType.objects.all()
serializer_class = serializers.DeviceTypeSerializer
filterset_class = filtersets.DeviceTypeFilterSet
- brief_prefetch_fields = ['manufacturer']
class ModuleTypeViewSet(NetBoxModelViewSet):
- queryset = ModuleType.objects.prefetch_related('manufacturer', 'tags').annotate(
- # module_count=count_related(Module, 'module_type')
- )
+ queryset = ModuleType.objects.all()
serializer_class = serializers.ModuleTypeSerializer
filterset_class = filtersets.ModuleTypeFilterSet
- brief_prefetch_fields = ['manufacturer']
#
@@ -295,61 +270,61 @@ class ModuleTypeViewSet(NetBoxModelViewSet):
#
class ConsolePortTemplateViewSet(NetBoxModelViewSet):
- queryset = ConsolePortTemplate.objects.prefetch_related('device_type__manufacturer')
+ queryset = ConsolePortTemplate.objects.all()
serializer_class = serializers.ConsolePortTemplateSerializer
filterset_class = filtersets.ConsolePortTemplateFilterSet
class ConsoleServerPortTemplateViewSet(NetBoxModelViewSet):
- queryset = ConsoleServerPortTemplate.objects.prefetch_related('device_type__manufacturer')
+ queryset = ConsoleServerPortTemplate.objects.all()
serializer_class = serializers.ConsoleServerPortTemplateSerializer
filterset_class = filtersets.ConsoleServerPortTemplateFilterSet
class PowerPortTemplateViewSet(NetBoxModelViewSet):
- queryset = PowerPortTemplate.objects.prefetch_related('device_type__manufacturer')
+ queryset = PowerPortTemplate.objects.all()
serializer_class = serializers.PowerPortTemplateSerializer
filterset_class = filtersets.PowerPortTemplateFilterSet
class PowerOutletTemplateViewSet(NetBoxModelViewSet):
- queryset = PowerOutletTemplate.objects.prefetch_related('device_type__manufacturer')
+ queryset = PowerOutletTemplate.objects.all()
serializer_class = serializers.PowerOutletTemplateSerializer
filterset_class = filtersets.PowerOutletTemplateFilterSet
class InterfaceTemplateViewSet(NetBoxModelViewSet):
- queryset = InterfaceTemplate.objects.prefetch_related('device_type__manufacturer')
+ queryset = InterfaceTemplate.objects.all()
serializer_class = serializers.InterfaceTemplateSerializer
filterset_class = filtersets.InterfaceTemplateFilterSet
class FrontPortTemplateViewSet(NetBoxModelViewSet):
- queryset = FrontPortTemplate.objects.prefetch_related('device_type__manufacturer')
+ queryset = FrontPortTemplate.objects.all()
serializer_class = serializers.FrontPortTemplateSerializer
filterset_class = filtersets.FrontPortTemplateFilterSet
class RearPortTemplateViewSet(NetBoxModelViewSet):
- queryset = RearPortTemplate.objects.prefetch_related('device_type__manufacturer')
+ queryset = RearPortTemplate.objects.all()
serializer_class = serializers.RearPortTemplateSerializer
filterset_class = filtersets.RearPortTemplateFilterSet
class ModuleBayTemplateViewSet(NetBoxModelViewSet):
- queryset = ModuleBayTemplate.objects.prefetch_related('device_type__manufacturer')
+ queryset = ModuleBayTemplate.objects.all()
serializer_class = serializers.ModuleBayTemplateSerializer
filterset_class = filtersets.ModuleBayTemplateFilterSet
class DeviceBayTemplateViewSet(NetBoxModelViewSet):
- queryset = DeviceBayTemplate.objects.prefetch_related('device_type__manufacturer')
+ queryset = DeviceBayTemplate.objects.all()
serializer_class = serializers.DeviceBayTemplateSerializer
filterset_class = filtersets.DeviceBayTemplateFilterSet
class InventoryItemTemplateViewSet(MPTTLockedMixin, NetBoxModelViewSet):
- queryset = InventoryItemTemplate.objects.prefetch_related('device_type__manufacturer', 'role')
+ queryset = InventoryItemTemplate.objects.all()
serializer_class = serializers.InventoryItemTemplateSerializer
filterset_class = filtersets.InventoryItemTemplateFilterSet
@@ -359,10 +334,7 @@ class InventoryItemTemplateViewSet(MPTTLockedMixin, NetBoxModelViewSet):
#
class DeviceRoleViewSet(NetBoxModelViewSet):
- queryset = DeviceRole.objects.prefetch_related('config_template', 'tags').annotate(
- device_count=count_related(Device, 'role'),
- virtualmachine_count=count_related(VirtualMachine, 'role')
- )
+ queryset = DeviceRole.objects.all()
serializer_class = serializers.DeviceRoleSerializer
filterset_class = filtersets.DeviceRoleFilterSet
@@ -372,10 +344,7 @@ class DeviceRoleViewSet(NetBoxModelViewSet):
#
class PlatformViewSet(NetBoxModelViewSet):
- queryset = Platform.objects.prefetch_related('config_template', 'tags').annotate(
- device_count=count_related(Device, 'platform'),
- virtualmachine_count=count_related(VirtualMachine, 'platform')
- )
+ queryset = Platform.objects.all()
serializer_class = serializers.PlatformSerializer
filterset_class = filtersets.PlatformFilterSet
@@ -391,8 +360,7 @@ class DeviceViewSet(
NetBoxModelViewSet
):
queryset = Device.objects.prefetch_related(
- 'device_type__manufacturer', 'role', 'tenant', 'platform', 'site', 'location', 'rack', 'parent_bay',
- 'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'config_template', 'tags',
+ 'parent_bay', # Referenced by DeviceSerializer.get_parent_device()
)
filterset_class = filtersets.DeviceFilterSet
pagination_class = StripCountAnnotationsPaginator
@@ -407,31 +375,21 @@ class DeviceViewSet(
Else, return the DeviceWithConfigContextSerializer
"""
-
request = self.get_serializer_context()['request']
- if request.query_params.get('brief', False):
- return serializers.NestedDeviceSerializer
-
- elif 'config_context' in request.query_params.get('exclude', []):
+ if self.brief or 'config_context' in request.query_params.get('exclude', []):
return serializers.DeviceSerializer
return serializers.DeviceWithConfigContextSerializer
class VirtualDeviceContextViewSet(NetBoxModelViewSet):
- queryset = VirtualDeviceContext.objects.prefetch_related(
- 'device__device_type', 'device', 'tenant', 'tags',
- ).annotate(
- interface_count=count_related(Interface, 'vdcs'),
- )
+ queryset = VirtualDeviceContext.objects.all()
serializer_class = serializers.VirtualDeviceContextSerializer
filterset_class = filtersets.VirtualDeviceContextFilterSet
class ModuleViewSet(NetBoxModelViewSet):
- queryset = Module.objects.prefetch_related(
- 'device', 'module_bay', 'module_type__manufacturer', 'tags',
- )
+ queryset = Module.objects.all()
serializer_class = serializers.ModuleSerializer
filterset_class = filtersets.ModuleFilterSet
@@ -442,49 +400,45 @@ class ModuleViewSet(NetBoxModelViewSet):
class ConsolePortViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = ConsolePort.objects.prefetch_related(
- 'device', 'module__module_bay', '_path', 'cable__terminations', 'tags'
+ '_path', 'cable__terminations',
)
serializer_class = serializers.ConsolePortSerializer
filterset_class = filtersets.ConsolePortFilterSet
- brief_prefetch_fields = ['device']
class ConsoleServerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = ConsoleServerPort.objects.prefetch_related(
- 'device', 'module__module_bay', '_path', 'cable__terminations', 'tags'
+ '_path', 'cable__terminations',
)
serializer_class = serializers.ConsoleServerPortSerializer
filterset_class = filtersets.ConsoleServerPortFilterSet
- brief_prefetch_fields = ['device']
class PowerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = PowerPort.objects.prefetch_related(
- 'device', 'module__module_bay', '_path', 'cable__terminations', 'tags'
+ '_path', 'cable__terminations',
)
serializer_class = serializers.PowerPortSerializer
filterset_class = filtersets.PowerPortFilterSet
- brief_prefetch_fields = ['device']
class PowerOutletViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = PowerOutlet.objects.prefetch_related(
- 'device', 'module__module_bay', '_path', 'cable__terminations', 'tags'
+ '_path', 'cable__terminations',
)
serializer_class = serializers.PowerOutletSerializer
filterset_class = filtersets.PowerOutletFilterSet
- brief_prefetch_fields = ['device']
class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = Interface.objects.prefetch_related(
- 'device', 'module__module_bay', 'parent', 'bridge', 'lag', '_path', 'cable__terminations', 'wireless_lans',
- 'untagged_vlan', 'tagged_vlans', 'vrf', 'ip_addresses', 'fhrp_group_assignments', 'tags', 'l2vpn_terminations',
- 'vdcs',
+ '_path', 'cable__terminations',
+ 'l2vpn_terminations', # Referenced by InterfaceSerializer.l2vpn_termination
+ 'ip_addresses', # Referenced by Interface.count_ipaddresses()
+ 'fhrp_group_assignments', # Referenced by Interface.count_fhrp_groups()
)
serializer_class = serializers.InterfaceSerializer
filterset_class = filtersets.InterfaceFilterSet
- brief_prefetch_fields = ['device']
def get_bulk_destroy_queryset(self):
# Ensure child interfaces are deleted prior to their parents
@@ -493,41 +447,36 @@ class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
class FrontPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
queryset = FrontPort.objects.prefetch_related(
- 'device__device_type__manufacturer', 'module__module_bay', 'rear_port', 'cable__terminations', 'tags'
+ 'cable__terminations',
)
serializer_class = serializers.FrontPortSerializer
filterset_class = filtersets.FrontPortFilterSet
- brief_prefetch_fields = ['device']
class RearPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
queryset = RearPort.objects.prefetch_related(
- 'device__device_type__manufacturer', 'module__module_bay', 'cable__terminations', 'tags'
+ 'cable__terminations',
)
serializer_class = serializers.RearPortSerializer
filterset_class = filtersets.RearPortFilterSet
- brief_prefetch_fields = ['device']
class ModuleBayViewSet(NetBoxModelViewSet):
- queryset = ModuleBay.objects.prefetch_related('tags', 'installed_module')
+ queryset = ModuleBay.objects.all()
serializer_class = serializers.ModuleBaySerializer
filterset_class = filtersets.ModuleBayFilterSet
- brief_prefetch_fields = ['device']
class DeviceBayViewSet(NetBoxModelViewSet):
- queryset = DeviceBay.objects.prefetch_related('installed_device', 'tags')
+ queryset = DeviceBay.objects.all()
serializer_class = serializers.DeviceBaySerializer
filterset_class = filtersets.DeviceBayFilterSet
- brief_prefetch_fields = ['device']
class InventoryItemViewSet(MPTTLockedMixin, NetBoxModelViewSet):
- queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer', 'tags')
+ queryset = InventoryItem.objects.all()
serializer_class = serializers.InventoryItemSerializer
filterset_class = filtersets.InventoryItemFilterSet
- brief_prefetch_fields = ['device']
#
@@ -535,9 +484,7 @@ class InventoryItemViewSet(MPTTLockedMixin, NetBoxModelViewSet):
#
class InventoryItemRoleViewSet(NetBoxModelViewSet):
- queryset = InventoryItemRole.objects.prefetch_related('tags').annotate(
- inventoryitem_count=count_related(InventoryItem, 'role')
- )
+ queryset = InventoryItemRole.objects.all()
serializer_class = serializers.InventoryItemRoleSerializer
filterset_class = filtersets.InventoryItemRoleFilterSet
@@ -554,7 +501,7 @@ class CableViewSet(NetBoxModelViewSet):
class CableTerminationViewSet(NetBoxModelViewSet):
metadata_class = ContentTypeMetadata
- queryset = CableTermination.objects.prefetch_related('cable', 'termination')
+ queryset = CableTermination.objects.all()
serializer_class = serializers.CableTerminationSerializer
filterset_class = filtersets.CableTerminationFilterSet
@@ -564,10 +511,9 @@ class CableTerminationViewSet(NetBoxModelViewSet):
#
class VirtualChassisViewSet(NetBoxModelViewSet):
- queryset = VirtualChassis.objects.prefetch_related('tags')
+ queryset = VirtualChassis.objects.all()
serializer_class = serializers.VirtualChassisSerializer
filterset_class = filtersets.VirtualChassisFilterSet
- brief_prefetch_fields = ['master']
#
@@ -575,11 +521,7 @@ class VirtualChassisViewSet(NetBoxModelViewSet):
#
class PowerPanelViewSet(NetBoxModelViewSet):
- queryset = PowerPanel.objects.prefetch_related(
- 'site', 'location'
- ).annotate(
- powerfeed_count=count_related(PowerFeed, 'power_panel')
- )
+ queryset = PowerPanel.objects.all()
serializer_class = serializers.PowerPanelSerializer
filterset_class = filtersets.PowerPanelFilterSet
@@ -590,7 +532,7 @@ class PowerPanelViewSet(NetBoxModelViewSet):
class PowerFeedViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = PowerFeed.objects.prefetch_related(
- 'power_panel', 'rack', '_path', 'cable__terminations', 'tags'
+ '_path', 'cable__terminations',
)
serializer_class = serializers.PowerFeedSerializer
filterset_class = filtersets.PowerFeedFilterSet
diff --git a/netbox/dcim/apps.py b/netbox/dcim/apps.py
index 78ff0d4c1..4df66e367 100644
--- a/netbox/dcim/apps.py
+++ b/netbox/dcim/apps.py
@@ -8,9 +8,13 @@ class DCIMConfig(AppConfig):
verbose_name = "DCIM"
def ready(self):
+ from netbox.models.features import register_models
+ from utilities.counters import connect_counters
from . import signals, search
from .models import CableTermination, Device, DeviceType, VirtualChassis
- from utilities.counters import connect_counters
+
+ # Register models
+ register_models(*self.get_models())
# Register denormalized fields
denormalized.register(CableTermination, '_device', {
diff --git a/netbox/dcim/fields.py b/netbox/dcim/fields.py
index db1a28d39..535756bac 100644
--- a/netbox/dcim/fields.py
+++ b/netbox/dcim/fields.py
@@ -1,6 +1,7 @@
from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ValidationError
from django.db import models
+from django.utils.translation import gettext as _
from netaddr import AddrFormatError, EUI, eui64_unix_expanded, mac_unix_expanded
from .lookups import PathContains
@@ -41,7 +42,7 @@ class MACAddressField(models.Field):
try:
return EUI(value, version=48, dialect=mac_unix_expanded_uppercase)
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):
return 'macaddr'
@@ -67,7 +68,7 @@ class WWNField(models.Field):
try:
return EUI(value, version=64, dialect=eui64_unix_expanded_uppercase)
except AddrFormatError:
- raise ValidationError(f"Invalid WWN format: {value}")
+ raise ValidationError(_("Invalid WWN format: {value}").format(value=value))
def db_type(self, connection):
return 'macaddr8'
diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py
index 4810e9cab..6b1611694 100644
--- a/netbox/dcim/filtersets.py
+++ b/netbox/dcim/filtersets.py
@@ -2,6 +2,8 @@ import django_filters
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
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 extras.filtersets import LocalConfigContextFilterSet
@@ -818,6 +820,10 @@ class PlatformFilterSet(OrganizationalModelFilterSet):
to_field_name='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(
queryset=ConfigTemplate.objects.all(),
label=_('Config template (ID)'),
@@ -827,6 +833,14 @@ class PlatformFilterSet(OrganizationalModelFilterSet):
model = Platform
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(
NetBoxModelFilterSet,
diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py
index 68d8d4f89..79ecc8383 100644
--- a/netbox/dcim/forms/bulk_edit.py
+++ b/netbox/dcim/forms/bulk_edit.py
@@ -557,6 +557,9 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm):
label=_('Device type'),
queryset=DeviceType.objects.all(),
required=False,
+ context={
+ 'parent': 'manufacturer',
+ },
query_params={
'manufacturer_id': '$manufacturer'
}
@@ -640,6 +643,9 @@ class ModuleBulkEditForm(NetBoxModelBulkEditForm):
required=False,
query_params={
'manufacturer_id': '$manufacturer'
+ },
+ context={
+ 'parent': 'manufacturer',
}
)
status = forms.ChoiceField(
diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py
index f30ff91fa..47974096f 100644
--- a/netbox/dcim/forms/bulk_import.py
+++ b/netbox/dcim/forms/bulk_import.py
@@ -159,6 +159,14 @@ class LocationImportForm(NetBoxModelImportForm):
model = Location
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):
slug = SlugField()
@@ -870,7 +878,11 @@ class InterfaceImportForm(NetBoxModelImportForm):
def clean_vdcs(self):
for vdc in self.cleaned_data['vdcs']:
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']
@@ -996,7 +1008,7 @@ class DeviceBayImportForm(NetBoxModelImportForm):
device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD
).exclude(pk=device.pk)
else:
- self.fields['installed_device'].queryset = Interface.objects.none()
+ self.fields['installed_device'].queryset = Device.objects.none()
class InventoryItemImportForm(NetBoxModelImportForm):
@@ -1075,7 +1087,11 @@ class InventoryItemImportForm(NetBoxModelImportForm):
component = model.objects.get(device=device, name=component_name)
self.instance.component = component
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:
termination_object = model.objects.get(device=device, name=name)
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:
- 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])
return termination_object
diff --git a/netbox/dcim/forms/connections.py b/netbox/dcim/forms/connections.py
index 854c5ebed..67ef360fe 100644
--- a/netbox/dcim/forms/connections.py
+++ b/netbox/dcim/forms/connections.py
@@ -30,7 +30,9 @@ def get_cable_form(a_type, b_type):
attrs[f'{cable_end}_terminations'] = DynamicModelMultipleChoiceField(
queryset=term_cls.objects.all(),
label=term_cls._meta.verbose_name.title(),
- disabled_indicator='_occupied',
+ context={
+ 'disabled': '_occupied',
+ },
query_params={
'device_id': f'$termination_{cable_end}_device',
'kind': 'physical', # Exclude virtual interfaces
@@ -52,7 +54,9 @@ def get_cable_form(a_type, b_type):
attrs[f'{cable_end}_terminations'] = DynamicModelMultipleChoiceField(
queryset=term_cls.objects.all(),
label=_('Power Feed'),
- disabled_indicator='_occupied',
+ context={
+ 'disabled': '_occupied',
+ },
query_params={
'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(
queryset=term_cls.objects.all(),
label=_('Side'),
- disabled_indicator='_occupied',
+ context={
+ 'disabled': '_occupied',
+ },
query_params={
'circuit_id': f'$termination_{cable_end}_circuit',
}
diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py
index fdb5f1d5a..6c33ea8d6 100644
--- a/netbox/dcim/forms/model_forms.py
+++ b/netbox/dcim/forms/model_forms.py
@@ -291,7 +291,11 @@ class DeviceTypeForm(NetBoxModelForm):
default_platform = DynamicModelChoiceField(
label=_('Default platform'),
queryset=Platform.objects.all(),
- required=False
+ required=False,
+ selector=True,
+ query_params={
+ 'manufacturer_id': ['$manufacturer', 'null'],
+ }
)
slug = SlugField(
label=_('Slug'),
@@ -426,7 +430,7 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
widget=APISelect(
api_url='/api/dcim/racks/{{rack}}/elevation/',
attrs={
- 'disabled-indicator': 'device',
+ 'ts-disabled-field': 'device',
'data-dynamic-params': '[{"fieldName":"face","queryParam":"face"}]'
},
)
@@ -434,6 +438,9 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
device_type = DynamicModelChoiceField(
label=_('Device type'),
queryset=DeviceType.objects.all(),
+ context={
+ 'parent': 'manufacturer',
+ },
selector=True
)
role = DynamicModelChoiceField(
@@ -444,7 +451,10 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
label=_('Platform'),
queryset=Platform.objects.all(),
required=False,
- selector=True
+ selector=True,
+ query_params={
+ 'available_for_device_type': '$device_type',
+ }
)
cluster = DynamicModelChoiceField(
label=_('Cluster'),
@@ -461,6 +471,9 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
label=_('Virtual chassis'),
queryset=VirtualChassis.objects.all(),
required=False,
+ context={
+ 'parent': 'master',
+ },
selector=True
)
vc_position = forms.IntegerField(
@@ -568,6 +581,9 @@ class ModuleForm(ModuleCommonForm, NetBoxModelForm):
module_type = DynamicModelChoiceField(
label=_('Module type'),
queryset=ModuleType.objects.all(),
+ context={
+ 'parent': 'manufacturer',
+ },
selector=True
)
comments = CommentField()
@@ -774,7 +790,10 @@ class VCMemberSelectForm(forms.Form):
class ComponentTemplateForm(forms.ModelForm):
device_type = DynamicModelChoiceField(
label=_('Device type'),
- queryset=DeviceType.objects.all()
+ queryset=DeviceType.objects.all(),
+ context={
+ 'parent': 'manufacturer',
+ }
)
def __init__(self, *args, **kwargs):
@@ -789,12 +808,18 @@ class ModularComponentTemplateForm(ComponentTemplateForm):
device_type = DynamicModelChoiceField(
label=_('Device type'),
queryset=DeviceType.objects.all().all(),
- required=False
+ required=False,
+ context={
+ 'parent': 'manufacturer',
+ }
)
module_type = DynamicModelChoiceField(
label=_('Module type'),
queryset=ModuleType.objects.all(),
- required=False
+ required=False,
+ context={
+ 'parent': 'manufacturer',
+ }
)
def __init__(self, *args, **kwargs):
diff --git a/netbox/dcim/migrations/0002_squashed.py b/netbox/dcim/migrations/0002_squashed.py
index c7325210e..786167680 100644
--- a/netbox/dcim/migrations/0002_squashed.py
+++ b/netbox/dcim/migrations/0002_squashed.py
@@ -233,7 +233,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='powerfeed',
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(
model_name='powerfeed',
diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py
index 7ae755602..f8a61a794 100644
--- a/netbox/dcim/models/cables.py
+++ b/netbox/dcim/models/cables.py
@@ -9,7 +9,7 @@ from django.dispatch import Signal
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
-from core.models import ContentType
+from core.models import ObjectType
from dcim.choices import *
from dcim.constants import *
from dcim.fields import PathField
@@ -160,25 +160,26 @@ class Cable(PrimaryModel):
# Validate length and 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):
- 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:
# Check that all termination objects for either end are of the same type
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:]):
- 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
if self.a_terminations and self.b_terminations:
a_type = self.a_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):
- 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:
# can't directly use self.a_terminations here as possible they
# don't have pk yet
@@ -327,17 +328,24 @@ class CableTermination(ChangeLoggedModel):
existing_termination = qs.first()
if existing_termination is not None:
raise ValidationError(
- f"Duplicate termination found for {self.termination_type.app_label}.{self.termination_type.model} "
- f"{self.termination_id}: cable {existing_termination.cable.pk}"
+ _("Duplicate termination found for {app_label}.{model} {termination_id}: cable {cable_pk}".format(
+ 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)
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
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):
@@ -473,13 +481,13 @@ class CablePath(models.Model):
def origin_type(self):
if self.path:
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
def destination_type(self):
if self.is_complete:
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
def path_objects(self):
@@ -586,7 +594,7 @@ class CablePath(models.Model):
# Step 6: Determine the far-end terminations
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(
termination_type=termination_type,
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.
prefetched = {}
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)
if hasattr(model_class, 'device'):
queryset = queryset.prefetch_related('device')
@@ -766,7 +774,7 @@ class CablePath(models.Model):
"""
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 = []
for node in self._nodes:
diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py
index 88dddb312..5b2564b32 100644
--- a/netbox/dcim/models/device_components.py
+++ b/netbox/dcim/models/device_components.py
@@ -1133,13 +1133,13 @@ class DeviceBay(ComponentModel, TrackingModelMixin):
super().clean()
# 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(
device_type=self.device.device_type
))
# 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."))
# Check that the installed device is not already installed elsewhere
diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py
index b0edf49fe..c75757fa7 100644
--- a/netbox/dcim/models/devices.py
+++ b/netbox/dcim/models/devices.py
@@ -815,20 +815,6 @@ class Device(
def get_absolute_url(self):
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):
super().clean()
@@ -875,7 +861,7 @@ class Device(
if self.position and self.device_type.u_height == 0:
raise ValidationError({
'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)
})
diff --git a/netbox/dcim/models/power.py b/netbox/dcim/models/power.py
index 62578d6c4..826eaae9c 100644
--- a/netbox/dcim/models/power.py
+++ b/netbox/dcim/models/power.py
@@ -84,6 +84,7 @@ class PowerFeed(PrimaryModel, PathEndpoint, CabledObjectModel):
rack = models.ForeignKey(
to='Rack',
on_delete=models.PROTECT,
+ related_name='powerfeeds',
blank=True,
null=True
)
diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py
index 4c863e12a..d4c9641b6 100644
--- a/netbox/dcim/tables/devices.py
+++ b/netbox/dcim/tables/devices.py
@@ -359,6 +359,11 @@ class CableTerminationTable(NetBoxTable):
verbose_name=_('Mark Connected'),
)
+ def value_link_peer(self, value):
+ return ', '.join([
+ f"{termination.parent_object} > {termination}" for termination in value
+ ])
+
class PathEndpointTable(CableTerminationTable):
connection = columns.TemplateColumn(
diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py
index 835a72bc6..d3134656c 100644
--- a/netbox/dcim/tables/template_code.py
+++ b/netbox/dcim/tables/template_code.py
@@ -36,7 +36,7 @@ DEVICEBAY_STATUS = """
INTERFACE_IPADDRESSES = """
{% if value.count > 3 %}
- {{ value.count }}
+ {{ value.count }}
{% else %}
{% for ip in value.all %}
{% if ip.status != 'active' %}
diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py
index d012a840f..0a3931696 100644
--- a/netbox/dcim/tests/test_api.py
+++ b/netbox/dcim/tests/test_api.py
@@ -1,6 +1,7 @@
from django.contrib.auth import get_user_model
from django.test import override_settings
from django.urls import reverse
+from django.utils.translation import gettext as _
from rest_framework import status
from dcim.choices import *
@@ -45,7 +46,7 @@ class Mixins:
name='Peer Device'
)
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(
device=peer_device,
name='Peer Termination'
@@ -67,7 +68,7 @@ class Mixins:
class RegionTest(APIViewTestCases.APIViewTestCase):
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 = [
{
'name': 'Region 4',
@@ -96,7 +97,7 @@ class RegionTest(APIViewTestCases.APIViewTestCase):
class SiteGroupTest(APIViewTestCases.APIViewTestCase):
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 = [
{
'name': 'Site Group 4',
@@ -125,7 +126,7 @@ class SiteGroupTest(APIViewTestCases.APIViewTestCase):
class SiteTest(APIViewTestCases.APIViewTestCase):
model = Site
- brief_fields = ['display', 'id', 'name', 'slug', 'url']
+ brief_fields = ['description', 'display', 'id', 'name', 'slug', 'url']
bulk_update_data = {
'status': 'planned',
}
@@ -187,7 +188,7 @@ class SiteTest(APIViewTestCases.APIViewTestCase):
class LocationTest(APIViewTestCases.APIViewTestCase):
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 = {
'description': 'New description',
}
@@ -237,7 +238,7 @@ class LocationTest(APIViewTestCases.APIViewTestCase):
class RackRoleTest(APIViewTestCases.APIViewTestCase):
model = RackRole
- brief_fields = ['display', 'id', 'name', 'rack_count', 'slug', 'url']
+ brief_fields = ['description', 'display', 'id', 'name', 'rack_count', 'slug', 'url']
create_data = [
{
'name': 'Rack Role 4',
@@ -272,7 +273,7 @@ class RackRoleTest(APIViewTestCases.APIViewTestCase):
class RackTest(APIViewTestCases.APIViewTestCase):
model = Rack
- brief_fields = ['device_count', 'display', 'id', 'name', 'url']
+ brief_fields = ['description', 'device_count', 'display', 'id', 'name', 'url']
bulk_update_data = {
'status': 'planned',
}
@@ -360,7 +361,7 @@ class RackTest(APIViewTestCases.APIViewTestCase):
class RackReservationTest(APIViewTestCases.APIViewTestCase):
model = RackReservation
- brief_fields = ['display', 'id', 'units', 'url', 'user']
+ brief_fields = ['description', 'display', 'id', 'units', 'url', 'user']
bulk_update_data = {
'description': 'New description',
}
@@ -407,7 +408,7 @@ class RackReservationTest(APIViewTestCases.APIViewTestCase):
class ManufacturerTest(APIViewTestCases.APIViewTestCase):
model = Manufacturer
- brief_fields = ['devicetype_count', 'display', 'id', 'name', 'slug', 'url']
+ brief_fields = ['description', 'devicetype_count', 'display', 'id', 'name', 'slug', 'url']
create_data = [
{
'name': 'Manufacturer 4',
@@ -439,7 +440,7 @@ class ManufacturerTest(APIViewTestCases.APIViewTestCase):
class DeviceTypeTest(APIViewTestCases.APIViewTestCase):
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 = {
'part_number': 'ABC123',
}
@@ -484,7 +485,7 @@ class DeviceTypeTest(APIViewTestCases.APIViewTestCase):
class ModuleTypeTest(APIViewTestCases.APIViewTestCase):
model = ModuleType
- brief_fields = ['display', 'id', 'manufacturer', 'model', 'url']
+ brief_fields = ['description', 'display', 'id', 'manufacturer', 'model', 'url']
bulk_update_data = {
'part_number': 'ABC123',
}
@@ -523,7 +524,7 @@ class ModuleTypeTest(APIViewTestCases.APIViewTestCase):
class ConsolePortTemplateTest(APIViewTestCases.APIViewTestCase):
model = ConsolePortTemplate
- brief_fields = ['display', 'id', 'name', 'url']
+ brief_fields = ['description', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -567,7 +568,7 @@ class ConsolePortTemplateTest(APIViewTestCases.APIViewTestCase):
class ConsoleServerPortTemplateTest(APIViewTestCases.APIViewTestCase):
model = ConsoleServerPortTemplate
- brief_fields = ['display', 'id', 'name', 'url']
+ brief_fields = ['description', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -611,7 +612,7 @@ class ConsoleServerPortTemplateTest(APIViewTestCases.APIViewTestCase):
class PowerPortTemplateTest(APIViewTestCases.APIViewTestCase):
model = PowerPortTemplate
- brief_fields = ['display', 'id', 'name', 'url']
+ brief_fields = ['description', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -655,7 +656,7 @@ class PowerPortTemplateTest(APIViewTestCases.APIViewTestCase):
class PowerOutletTemplateTest(APIViewTestCases.APIViewTestCase):
model = PowerOutletTemplate
- brief_fields = ['display', 'id', 'name', 'url']
+ brief_fields = ['description', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -712,7 +713,7 @@ class PowerOutletTemplateTest(APIViewTestCases.APIViewTestCase):
class InterfaceTemplateTest(APIViewTestCases.APIViewTestCase):
model = InterfaceTemplate
- brief_fields = ['display', 'id', 'name', 'url']
+ brief_fields = ['description', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -760,7 +761,7 @@ class InterfaceTemplateTest(APIViewTestCases.APIViewTestCase):
class FrontPortTemplateTest(APIViewTestCases.APIViewTestCase):
model = FrontPortTemplate
- brief_fields = ['display', 'id', 'name', 'url']
+ brief_fields = ['description', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -849,7 +850,7 @@ class FrontPortTemplateTest(APIViewTestCases.APIViewTestCase):
class RearPortTemplateTest(APIViewTestCases.APIViewTestCase):
model = RearPortTemplate
- brief_fields = ['display', 'id', 'name', 'url']
+ brief_fields = ['description', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -897,7 +898,7 @@ class RearPortTemplateTest(APIViewTestCases.APIViewTestCase):
class ModuleBayTemplateTest(APIViewTestCases.APIViewTestCase):
model = ModuleBayTemplate
- brief_fields = ['display', 'id', 'name', 'url']
+ brief_fields = ['description', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -937,7 +938,7 @@ class ModuleBayTemplateTest(APIViewTestCases.APIViewTestCase):
class DeviceBayTemplateTest(APIViewTestCases.APIViewTestCase):
model = DeviceBayTemplate
- brief_fields = ['display', 'id', 'name', 'url']
+ brief_fields = ['description', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -977,7 +978,7 @@ class DeviceBayTemplateTest(APIViewTestCases.APIViewTestCase):
class InventoryItemTemplateTest(APIViewTestCases.APIViewTestCase):
model = InventoryItemTemplate
- brief_fields = ['_depth', 'display', 'id', 'name', 'url']
+ brief_fields = ['_depth', 'description', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -1028,7 +1029,7 @@ class InventoryItemTemplateTest(APIViewTestCases.APIViewTestCase):
class DeviceRoleTest(APIViewTestCases.APIViewTestCase):
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 = [
{
'name': 'Device Role 4',
@@ -1063,7 +1064,7 @@ class DeviceRoleTest(APIViewTestCases.APIViewTestCase):
class PlatformTest(APIViewTestCases.APIViewTestCase):
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 = [
{
'name': 'Platform 4',
@@ -1095,7 +1096,7 @@ class PlatformTest(APIViewTestCases.APIViewTestCase):
class DeviceTest(APIViewTestCases.APIViewTestCase):
model = Device
- brief_fields = ['display', 'id', 'name', 'url']
+ brief_fields = ['description', 'display', 'id', 'name', 'url']
bulk_update_data = {
'status': 'failed',
}
@@ -1285,7 +1286,7 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
class ModuleTest(APIViewTestCases.APIViewTestCase):
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 = {
'serial': '1234ABCD',
}
@@ -1349,7 +1350,7 @@ class ModuleTest(APIViewTestCases.APIViewTestCase):
class ConsolePortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
model = ConsolePort
- brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
+ brief_fields = ['_occupied', 'cable', 'description', 'device', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -1391,7 +1392,7 @@ class ConsolePortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCa
class ConsoleServerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
model = ConsoleServerPort
- brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
+ brief_fields = ['_occupied', 'cable', 'description', 'device', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -1433,7 +1434,7 @@ class ConsoleServerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIView
class PowerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
model = PowerPort
- brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
+ brief_fields = ['_occupied', 'cable', 'description', 'device', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -1472,7 +1473,7 @@ class PowerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
class PowerOutletTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
model = PowerOutlet
- brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
+ brief_fields = ['_occupied', 'cable', 'description', 'device', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -1520,7 +1521,7 @@ class PowerOutletTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCa
class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
model = Interface
- brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
+ brief_fields = ['_occupied', 'cable', 'description', 'device', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -1654,7 +1655,7 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
class FrontPortTest(APIViewTestCases.APIViewTestCase):
model = FrontPort
- brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
+ brief_fields = ['_occupied', 'cable', 'description', 'device', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -1712,7 +1713,7 @@ class FrontPortTest(APIViewTestCases.APIViewTestCase):
class RearPortTest(APIViewTestCases.APIViewTestCase):
model = RearPort
- brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
+ brief_fields = ['_occupied', 'cable', 'description', 'device', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -1754,7 +1755,7 @@ class RearPortTest(APIViewTestCases.APIViewTestCase):
class ModuleBayTest(APIViewTestCases.APIViewTestCase):
model = ModuleBay
- brief_fields = ['display', 'id', 'module', 'name', 'url']
+ brief_fields = ['description', 'display', 'id', 'installed_module', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -1793,7 +1794,7 @@ class ModuleBayTest(APIViewTestCases.APIViewTestCase):
class DeviceBayTest(APIViewTestCases.APIViewTestCase):
model = DeviceBay
- brief_fields = ['device', 'display', 'id', 'name', 'url']
+ brief_fields = ['description', 'device', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -1856,7 +1857,7 @@ class DeviceBayTest(APIViewTestCases.APIViewTestCase):
class InventoryItemTest(APIViewTestCases.APIViewTestCase):
model = InventoryItem
- brief_fields = ['_depth', 'device', 'display', 'id', 'name', 'url']
+ brief_fields = ['_depth', 'description', 'device', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -1916,7 +1917,7 @@ class InventoryItemTest(APIViewTestCases.APIViewTestCase):
class InventoryItemRoleTest(APIViewTestCases.APIViewTestCase):
model = InventoryItemRole
- brief_fields = ['display', 'id', 'inventoryitem_count', 'name', 'slug', 'url']
+ brief_fields = ['description', 'display', 'id', 'inventoryitem_count', 'name', 'slug', 'url']
create_data = [
{
'name': 'Inventory Item Role 4',
@@ -1951,7 +1952,7 @@ class InventoryItemRoleTest(APIViewTestCases.APIViewTestCase):
class CableTest(APIViewTestCases.APIViewTestCase):
model = Cable
- brief_fields = ['display', 'id', 'label', 'url']
+ brief_fields = ['description', 'display', 'id', 'label', 'url']
bulk_update_data = {
'length': 100,
'length_unit': 'm',
@@ -2074,7 +2075,7 @@ class ConnectedDeviceTest(APITestCase):
class VirtualChassisTest(APIViewTestCases.APIViewTestCase):
model = VirtualChassis
- brief_fields = ['display', 'id', 'master', 'member_count', 'name', 'url']
+ brief_fields = ['description', 'display', 'id', 'master', 'member_count', 'name', 'url']
@classmethod
def setUpTestData(cls):
@@ -2155,7 +2156,7 @@ class VirtualChassisTest(APIViewTestCases.APIViewTestCase):
class PowerPanelTest(APIViewTestCases.APIViewTestCase):
model = PowerPanel
- brief_fields = ['display', 'id', 'name', 'powerfeed_count', 'url']
+ brief_fields = ['description', 'display', 'id', 'name', 'powerfeed_count', 'url']
@classmethod
def setUpTestData(cls):
@@ -2204,7 +2205,7 @@ class PowerPanelTest(APIViewTestCases.APIViewTestCase):
class PowerFeedTest(APIViewTestCases.APIViewTestCase):
model = PowerFeed
- brief_fields = ['_occupied', 'cable', 'display', 'id', 'name', 'url']
+ brief_fields = ['_occupied', 'cable', 'description', 'display', 'id', 'name', 'url']
bulk_update_data = {
'status': 'planned',
}
@@ -2259,7 +2260,7 @@ class PowerFeedTest(APIViewTestCases.APIViewTestCase):
class VirtualDeviceContextTest(APIViewTestCases.APIViewTestCase):
model = VirtualDeviceContext
- brief_fields = ['device', 'display', 'id', 'identifier', 'name', 'url']
+ brief_fields = ['description', 'device', 'display', 'id', 'identifier', 'name', 'url']
bulk_update_data = {
'status': 'planned',
}
diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py
index a827939f7..49a71022e 100644
--- a/netbox/dcim/tests/test_cablepaths.py
+++ b/netbox/dcim/tests/test_cablepaths.py
@@ -2156,7 +2156,7 @@ class CablePathTestCase(TestCase):
device = Device.objects.create(
site=self.site,
device_type=self.device.device_type,
- device_role=self.device.device_role,
+ role=self.device.role,
name='Test mid-span Device'
)
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py
index 89d15a0ef..b255c283e 100644
--- a/netbox/dcim/tests/test_filtersets.py
+++ b/netbox/dcim/tests/test_filtersets.py
@@ -1787,6 +1787,7 @@ class PlatformTestCase(TestCase, ChangeLoggedFilterSetTests):
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 3', slug='platform-3', manufacturer=manufacturers[2], description='foobar3'),
+ Platform(name='Platform 4', slug='platform-4'),
)
Platform.objects.bulk_create(platforms)
@@ -1813,6 +1814,17 @@ class PlatformTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'manufacturer': [manufacturers[0].slug, manufacturers[1].slug]}
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):
queryset = Device.objects.all()
diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py
index d56bf0741..1a5cc8435 100644
--- a/netbox/dcim/tests/test_models.py
+++ b/netbox/dcim/tests/test_models.py
@@ -1,8 +1,8 @@
-from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.test import TestCase
from circuits.models import *
+from core.models import ObjectType
from dcim.choices import *
from dcim.models import *
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
cf1 = CustomField.objects.create(name='cf1', default='foo')
- cf1.content_types.set(
- ContentType.objects.filter(app_label='dcim', model__in=[
+ cf1.object_types.set(
+ ObjectType.objects.filter(app_label='dcim', model__in=[
'consoleport',
'consoleserverport',
'powerport',
@@ -533,30 +533,6 @@ class DeviceTestCase(TestCase):
device2.full_clean()
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):
diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py
index 53d7f3d34..e9e5a557b 100644
--- a/netbox/dcim/tests/test_views.py
+++ b/netbox/dcim/tests/test_views.py
@@ -3,7 +3,6 @@ from zoneinfo import ZoneInfo
import yaml
from django.contrib.auth import get_user_model
-from django.contrib.contenttypes.models import ContentType
from django.test import override_settings
from django.urls import reverse
from netaddr import EUI
@@ -2982,7 +2981,6 @@ class CableTestCase(
tags = create_tags('Alpha', 'Bravo', 'Charlie')
- interface_ct = ContentType.objects.get_for_model(Interface)
cls.form_data = {
# TODO: Revisit this limitation
# Changing terminations not supported when editing an existing Cable
diff --git a/netbox/extras/api/customfields.py b/netbox/extras/api/customfields.py
index ce3b388f4..81535a147 100644
--- a/netbox/extras/api/customfields.py
+++ b/netbox/extras/api/customfields.py
@@ -1,12 +1,12 @@
-from django.contrib.contenttypes.models import ContentType
-from drf_spectacular.utils import extend_schema_field
+from django.utils.translation import gettext as _
from drf_spectacular.types import OpenApiTypes
+from drf_spectacular.utils import extend_schema_field
from rest_framework.fields import Field
from rest_framework.serializers import ValidationError
+from core.models import ObjectType
from extras.choices import CustomFieldTypeChoices
from extras.models import CustomField
-from netbox.constants import NESTED_SERIALIZER_PREFIX
from utilities.api import get_serializer_for_model
@@ -24,8 +24,8 @@ class CustomFieldDefaultValues:
self.model = serializer_field.parent.Meta.model
# Retrieve the CustomFields for the parent model
- content_type = ContentType.objects.get_for_model(self.model)
- fields = CustomField.objects.filter(content_types=content_type)
+ object_type = ObjectType.objects.get_for_model(self.model)
+ fields = CustomField.objects.filter(object_types=object_type)
# Populate the default value for each CustomField
value = {}
@@ -46,8 +46,8 @@ class CustomFieldsDataField(Field):
Cache CustomFields assigned to this model to avoid redundant database queries
"""
if not hasattr(self, '_custom_fields'):
- content_type = ContentType.objects.get_for_model(self.parent.Meta.model)
- self._custom_fields = CustomField.objects.filter(content_types=content_type)
+ object_type = ObjectType.objects.get_for_model(self.parent.Meta.model)
+ self._custom_fields = CustomField.objects.filter(object_types=object_type)
return self._custom_fields
def to_representation(self, obj):
@@ -57,11 +57,11 @@ class CustomFieldsDataField(Field):
for cf in self._get_custom_fields():
value = cf.deserialize(obj.get(cf.name))
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)
- value = serializer(value, context=self.parent.context).data
+ serializer = get_serializer_for_model(cf.object_type.model_class())
+ value = serializer(value, nested=True, context=self.parent.context).data
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)
- value = serializer(value, many=True, context=self.parent.context).data
+ serializer = get_serializer_for_model(cf.object_type.model_class())
+ value = serializer(value, nested=True, many=True, context=self.parent.context).data
data[cf.name] = value
return data
@@ -79,16 +79,13 @@ class CustomFieldsDataField(Field):
CustomFieldTypeChoices.TYPE_OBJECT,
CustomFieldTypeChoices.TYPE_MULTIOBJECT
):
- serializer_class = get_serializer_for_model(
- model=cf.object_type.model_class(),
- prefix=NESTED_SERIALIZER_PREFIX
- )
+ serializer_class = get_serializer_for_model(cf.object_type.model_class())
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():
data[cf.name] = [obj['id'] for obj in serializer.data] if many else serializer.data['id']
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 self.parent.instance:
diff --git a/netbox/extras/api/mixins.py b/netbox/extras/api/mixins.py
index 1737ff9f8..aafdf32d4 100644
--- a/netbox/extras/api/mixins.py
+++ b/netbox/extras/api/mixins.py
@@ -5,7 +5,7 @@ from rest_framework.response import Response
from rest_framework.status import HTTP_400_BAD_REQUEST
from netbox.api.renderers import TextRenderer
-from .nested_serializers import NestedConfigTemplateSerializer
+from .serializers import ConfigTemplateSerializer
__all__ = (
'ConfigContextQuerySetMixin',
@@ -52,7 +52,7 @@ class ConfigTemplateRenderMixin:
if request.accepted_renderer.format == 'txt':
return Response(output)
- template_serializer = NestedConfigTemplateSerializer(configtemplate, context={'request': request})
+ template_serializer = ConfigTemplateSerializer(configtemplate, nested=True, context={'request': request})
return Response({
'configtemplate': template_serializer.data,
diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py
index e2f30e8b0..bd19b3184 100644
--- a/netbox/extras/api/serializers.py
+++ b/netbox/extras/api/serializers.py
@@ -1,671 +1,16 @@
-from django.contrib.auth import get_user_model
-from django.core.exceptions import ObjectDoesNotExist
-from drf_spectacular.types import OpenApiTypes
-from drf_spectacular.utils import extend_schema_field
-from rest_framework import serializers
-from rest_framework.fields import ListField
-
-from core.api.nested_serializers import NestedDataSourceSerializer, NestedDataFileSerializer, NestedJobSerializer
-from core.api.serializers import JobSerializer
-from core.models import ContentType
-from dcim.api.nested_serializers import (
- NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedLocationSerializer, NestedPlatformSerializer,
- NestedRegionSerializer, NestedSiteSerializer, NestedSiteGroupSerializer,
-)
-from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
-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 .serializers_.objecttypes import *
+from .serializers_.attachments import *
+from .serializers_.bookmarks import *
+from .serializers_.change_logging import *
+from .serializers_.customfields import *
+from .serializers_.customlinks import *
+from .serializers_.dashboard import *
+from .serializers_.events import *
+from .serializers_.exporttemplates import *
+from .serializers_.journaling import *
+from .serializers_.configcontexts import *
+from .serializers_.configtemplates import *
+from .serializers_.savedfilters import *
+from .serializers_.scripts import *
+from .serializers_.tags 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')
diff --git a/netbox/extras/api/serializers_/__init__.py b/netbox/extras/api/serializers_/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/netbox/extras/api/serializers_/attachments.py b/netbox/extras/api/serializers_/attachments.py
new file mode 100644
index 000000000..bcf3a24ec
--- /dev/null
+++ b/netbox/extras/api/serializers_/attachments.py
@@ -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
diff --git a/netbox/extras/api/serializers_/bookmarks.py b/netbox/extras/api/serializers_/bookmarks.py
new file mode 100644
index 000000000..7a2d4d6aa
--- /dev/null
+++ b/netbox/extras/api/serializers_/bookmarks.py
@@ -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
diff --git a/netbox/extras/api/serializers_/change_logging.py b/netbox/extras/api/serializers_/change_logging.py
new file mode 100644
index 000000000..32585637c
--- /dev/null
+++ b/netbox/extras/api/serializers_/change_logging.py
@@ -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
diff --git a/netbox/extras/api/serializers_/configcontexts.py b/netbox/extras/api/serializers_/configcontexts.py
new file mode 100644
index 000000000..e9688f254
--- /dev/null
+++ b/netbox/extras/api/serializers_/configcontexts.py
@@ -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')
diff --git a/netbox/extras/api/serializers_/configtemplates.py b/netbox/extras/api/serializers_/configtemplates.py
new file mode 100644
index 000000000..935214478
--- /dev/null
+++ b/netbox/extras/api/serializers_/configtemplates.py
@@ -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')
diff --git a/netbox/extras/api/serializers_/customfields.py b/netbox/extras/api/serializers_/customfields.py
new file mode 100644
index 000000000..efd6db063
--- /dev/null
+++ b/netbox/extras/api/serializers_/customfields.py
@@ -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'
diff --git a/netbox/extras/api/serializers_/customlinks.py b/netbox/extras/api/serializers_/customlinks.py
new file mode 100644
index 000000000..8635ea2a0
--- /dev/null
+++ b/netbox/extras/api/serializers_/customlinks.py
@@ -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')
diff --git a/netbox/extras/api/serializers_/dashboard.py b/netbox/extras/api/serializers_/dashboard.py
new file mode 100644
index 000000000..74d5daecf
--- /dev/null
+++ b/netbox/extras/api/serializers_/dashboard.py
@@ -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')
diff --git a/netbox/extras/api/serializers_/events.py b/netbox/extras/api/serializers_/events.py
new file mode 100644
index 000000000..4285b12e6
--- /dev/null
+++ b/netbox/extras/api/serializers_/events.py
@@ -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')
diff --git a/netbox/extras/api/serializers_/exporttemplates.py b/netbox/extras/api/serializers_/exporttemplates.py
new file mode 100644
index 000000000..43cc061a7
--- /dev/null
+++ b/netbox/extras/api/serializers_/exporttemplates.py
@@ -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')
diff --git a/netbox/extras/api/serializers_/journaling.py b/netbox/extras/api/serializers_/journaling.py
new file mode 100644
index 000000000..46ab0477b
--- /dev/null
+++ b/netbox/extras/api/serializers_/journaling.py
@@ -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
diff --git a/netbox/extras/api/serializers_/objecttypes.py b/netbox/extras/api/serializers_/objecttypes.py
new file mode 100644
index 000000000..8e4806652
--- /dev/null
+++ b/netbox/extras/api/serializers_/objecttypes.py
@@ -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']
diff --git a/netbox/extras/api/serializers_/savedfilters.py b/netbox/extras/api/serializers_/savedfilters.py
new file mode 100644
index 000000000..9e26f0c30
--- /dev/null
+++ b/netbox/extras/api/serializers_/savedfilters.py
@@ -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')
diff --git a/netbox/extras/api/serializers_/scripts.py b/netbox/extras/api/serializers_/scripts.py
new file mode 100644
index 000000000..b2a8ef29d
--- /dev/null
+++ b/netbox/extras/api/serializers_/scripts.py
@@ -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
diff --git a/netbox/extras/api/serializers_/tags.py b/netbox/extras/api/serializers_/tags.py
new file mode 100644
index 000000000..9d91ba5e1
--- /dev/null
+++ b/netbox/extras/api/serializers_/tags.py
@@ -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')
diff --git a/netbox/extras/api/urls.py b/netbox/extras/api/urls.py
index 68b4488bc..301cc1b0a 100644
--- a/netbox/extras/api/urls.py
+++ b/netbox/extras/api/urls.py
@@ -22,7 +22,7 @@ router.register('config-contexts', views.ConfigContextViewSet)
router.register('config-templates', views.ConfigTemplateViewSet)
router.register('scripts', views.ScriptViewSet, basename='script')
router.register('object-changes', views.ObjectChangeViewSet)
-router.register('content-types', views.ContentTypeViewSet)
+router.register('object-types', views.ObjectTypeViewSet)
app_name = 'extras-api'
urlpatterns = [
diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py
index 8f5841da1..3439f6f3f 100644
--- a/netbox/extras/api/views.py
+++ b/netbox/extras/api/views.py
@@ -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_rq.queues import get_connection
from rest_framework import status
@@ -9,21 +7,20 @@ from rest_framework.generics import RetrieveUpdateDestroyAPIView
from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response
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 core.choices import JobStatusChoices
-from core.models import Job
+from core.models import Job, ObjectType
from extras import filtersets
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.features import SyncedDataMixin
from netbox.api.metadata import ContentTypeMetadata
from netbox.api.renderers import TextRenderer
from netbox.api.viewsets import NetBoxModelViewSet
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 .mixins import ConfigTemplateRenderMixin
@@ -115,7 +112,7 @@ class CustomLinkViewSet(NetBoxModelViewSet):
class ExportTemplateViewSet(SyncedDataMixin, NetBoxModelViewSet):
metadata_class = ContentTypeMetadata
- queryset = ExportTemplate.objects.prefetch_related('data_source', 'data_file')
+ queryset = ExportTemplate.objects.all()
serializer_class = serializers.ExportTemplateSerializer
filterset_class = filtersets.ExportTemplateFilterSet
@@ -147,9 +144,7 @@ class BookmarkViewSet(NetBoxModelViewSet):
#
class TagViewSet(NetBoxModelViewSet):
- queryset = Tag.objects.annotate(
- tagged_items=count_related(TaggedItem, 'tag')
- )
+ queryset = Tag.objects.all()
serializer_class = serializers.TagSerializer
filterset_class = filtersets.TagFilterSet
@@ -181,10 +176,7 @@ class JournalEntryViewSet(NetBoxModelViewSet):
#
class ConfigContextViewSet(SyncedDataMixin, NetBoxModelViewSet):
- queryset = ConfigContext.objects.prefetch_related(
- 'regions', 'site_groups', 'sites', 'locations', 'roles', 'platforms', 'tenant_groups', 'tenants', 'data_source',
- 'data_file',
- )
+ queryset = ConfigContext.objects.all()
serializer_class = serializers.ConfigContextSerializer
filterset_class = filtersets.ConfigContextFilterSet
@@ -194,7 +186,7 @@ class ConfigContextViewSet(SyncedDataMixin, NetBoxModelViewSet):
#
class ConfigTemplateViewSet(SyncedDataMixin, ConfigTemplateRenderMixin, NetBoxModelViewSet):
- queryset = ConfigTemplate.objects.prefetch_related('data_source', 'data_file')
+ queryset = ConfigTemplate.objects.all()
serializer_class = serializers.ConfigTemplateSerializer
filterset_class = filtersets.ConfigTemplateFilterSet
@@ -214,66 +206,30 @@ class ConfigTemplateViewSet(SyncedDataMixin, ConfigTemplateRenderMixin, NetBoxMo
# Scripts
#
-class ScriptViewSet(ViewSet):
+class ScriptViewSet(ModelViewSet):
permission_classes = [IsAuthenticatedOrLoginNotRequired]
+ queryset = Script.objects.prefetch_related('jobs')
+ serializer_class = serializers.ScriptSerializer
+ filterset_class = filtersets.ScriptFilterSet
+
_ignore_model_permissions = True
- schema = None
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):
- module, script = self._get_script(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()
+ script = get_object_or_404(self.queryset, pk=pk)
serializer = serializers.ScriptDetailSerializer(script, context={'request': request})
return Response(serializer.data)
def post(self, request, pk):
"""
- Run a Script identified as ".