From 12818f178694adc3c1d6ef7daef72ab6bdba11c8 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 19 Sep 2025 13:54:51 -0400 Subject: [PATCH] Closes #20295: Make cable terminations REST API endpoint read-only (#20394) --- contrib/openapi.json | 394 +------------------------ netbox/dcim/api/serializers_/cables.py | 10 +- netbox/dcim/api/views.py | 4 +- netbox/dcim/tests/test_api.py | 27 ++ 4 files changed, 41 insertions(+), 394 deletions(-) diff --git a/contrib/openapi.json b/contrib/openapi.json index e241789e6..86cbd6f63 100644 --- a/contrib/openapi.json +++ b/contrib/openapi.json @@ -21698,191 +21698,6 @@ "description": "" } } - }, - "post": { - "operationId": "dcim_cable_terminations_create", - "description": "Post a list of cable termination objects.", - "tags": [ - "dcim" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CableTerminationRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/CableTerminationRequest" - } - } - }, - "required": true - }, - "security": [ - { - "cookieAuth": [] - }, - { - "tokenAuth": [] - } - ], - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CableTermination" - } - } - }, - "description": "" - } - } - }, - "put": { - "operationId": "dcim_cable_terminations_bulk_update", - "description": "Put a list of cable termination objects.", - "tags": [ - "dcim" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CableTerminationRequest" - } - } - }, - "multipart/form-data": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CableTerminationRequest" - } - } - } - }, - "required": true - }, - "security": [ - { - "cookieAuth": [] - }, - { - "tokenAuth": [] - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CableTermination" - } - } - } - }, - "description": "" - } - } - }, - "patch": { - "operationId": "dcim_cable_terminations_bulk_partial_update", - "description": "Patch a list of cable termination objects.", - "tags": [ - "dcim" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CableTerminationRequest" - } - } - }, - "multipart/form-data": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CableTerminationRequest" - } - } - } - }, - "required": true - }, - "security": [ - { - "cookieAuth": [] - }, - { - "tokenAuth": [] - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CableTermination" - } - } - } - }, - "description": "" - } - } - }, - "delete": { - "operationId": "dcim_cable_terminations_bulk_destroy", - "description": "Delete a list of cable termination objects.", - "tags": [ - "dcim" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CableTerminationRequest" - } - } - }, - "multipart/form-data": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CableTerminationRequest" - } - } - } - }, - "required": true - }, - "security": [ - { - "cookieAuth": [] - }, - { - "tokenAuth": [] - } - ], - "responses": { - "204": { - "description": "No response body" - } - } } }, "/api/dcim/cable-terminations/{id}/": { @@ -21923,142 +21738,6 @@ "description": "" } } - }, - "put": { - "operationId": "dcim_cable_terminations_update", - "description": "Put a cable termination object.", - "parameters": [ - { - "in": "path", - "name": "id", - "schema": { - "type": "integer" - }, - "description": "A unique integer value identifying this cable termination.", - "required": true - } - ], - "tags": [ - "dcim" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CableTerminationRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/CableTerminationRequest" - } - } - }, - "required": true - }, - "security": [ - { - "cookieAuth": [] - }, - { - "tokenAuth": [] - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CableTermination" - } - } - }, - "description": "" - } - } - }, - "patch": { - "operationId": "dcim_cable_terminations_partial_update", - "description": "Patch a cable termination object.", - "parameters": [ - { - "in": "path", - "name": "id", - "schema": { - "type": "integer" - }, - "description": "A unique integer value identifying this cable termination.", - "required": true - } - ], - "tags": [ - "dcim" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PatchedCableTerminationRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/PatchedCableTerminationRequest" - } - } - } - }, - "security": [ - { - "cookieAuth": [] - }, - { - "tokenAuth": [] - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CableTermination" - } - } - }, - "description": "" - } - } - }, - "delete": { - "operationId": "dcim_cable_terminations_destroy", - "description": "Delete a cable termination object.", - "parameters": [ - { - "in": "path", - "name": "id", - "schema": { - "type": "integer" - }, - "description": "A unique integer value identifying this cable termination.", - "required": true - } - ], - "tags": [ - "dcim" - ], - "security": [ - { - "cookieAuth": [] - }, - { - "tokenAuth": [] - } - ], - "responses": { - "204": { - "description": "No response body" - } - } } }, "/api/dcim/cables/": { @@ -204463,7 +204142,8 @@ "readOnly": true }, "cable": { - "type": "integer" + "type": "integer", + "readOnly": true }, "cable_end": { "enum": [ @@ -204473,16 +204153,16 @@ "type": "string", "description": "* `A` - A\n* `B` - B", "x-spec-enum-id": "1db84f9b93b261c8", + "readOnly": true, "title": "End" }, "termination_type": { - "type": "string" + "type": "string", + "readOnly": true }, "termination_id": { "type": "integer", - "maximum": 9223372036854775807, - "minimum": 0, - "format": "int64" + "readOnly": true }, "termination": { "nullable": true, @@ -204514,40 +204194,6 @@ "url" ] }, - "CableTerminationRequest": { - "type": "object", - "description": "Adds support for custom fields and tags.", - "properties": { - "cable": { - "type": "integer" - }, - "cable_end": { - "enum": [ - "A", - "B" - ], - "type": "string", - "description": "* `A` - A\n* `B` - B", - "x-spec-enum-id": "1db84f9b93b261c8", - "title": "End" - }, - "termination_type": { - "type": "string" - }, - "termination_id": { - "type": "integer", - "maximum": 9223372036854775807, - "minimum": 0, - "format": "int64" - } - }, - "required": [ - "cable", - "cable_end", - "termination_id", - "termination_type" - ] - }, "Circuit": { "type": "object", "description": "Adds support for custom fields and tags.", @@ -226099,34 +225745,6 @@ } } }, - "PatchedCableTerminationRequest": { - "type": "object", - "description": "Adds support for custom fields and tags.", - "properties": { - "cable": { - "type": "integer" - }, - "cable_end": { - "enum": [ - "A", - "B" - ], - "type": "string", - "description": "* `A` - A\n* `B` - B", - "x-spec-enum-id": "1db84f9b93b261c8", - "title": "End" - }, - "termination_type": { - "type": "string" - }, - "termination_id": { - "type": "integer", - "maximum": 9223372036854775807, - "minimum": 0, - "format": "int64" - } - } - }, "PatchedCircuitGroupRequest": { "type": "object", "description": "Adds support for custom fields and tags.", diff --git a/netbox/dcim/api/serializers_/cables.py b/netbox/dcim/api/serializers_/cables.py index 397e5cd16..bb9a12462 100644 --- a/netbox/dcim/api/serializers_/cables.py +++ b/netbox/dcim/api/serializers_/cables.py @@ -1,10 +1,8 @@ -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 BaseModelSerializer, GenericObjectSerializer, NetBoxModelSerializer @@ -51,9 +49,11 @@ class TracedCableSerializer(BaseModelSerializer): class CableTerminationSerializer(NetBoxModelSerializer): termination_type = ContentTypeField( - queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS) + read_only=True, + ) + termination = serializers.SerializerMethodField( + read_only=True, ) - termination = serializers.SerializerMethodField(read_only=True) class Meta: model = CableTermination @@ -61,6 +61,8 @@ class CableTerminationSerializer(NetBoxModelSerializer): 'id', 'url', 'display', 'cable', 'cable_end', 'termination_type', 'termination_id', 'termination', 'created', 'last_updated', ] + read_only_fields = fields + brief_fields = ('id', 'url', 'display', 'cable', 'cable_end', 'termination_type', 'termination_id') @extend_schema_field(serializers.JSONField(allow_null=True)) def get_termination(self, obj): diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index ffc0ca4d6..9ecaaa76a 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -16,7 +16,7 @@ from extras.api.mixins import ConfigContextQuerySetMixin, RenderConfigMixin 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 import NetBoxModelViewSet, MPTTLockedMixin, NetBoxReadOnlyModelViewSet from netbox.api.viewsets.mixins import SequentialBulkCreatesMixin from utilities.api import get_serializer_for_model from utilities.query_functions import CollateAsChar @@ -563,7 +563,7 @@ class CableViewSet(NetBoxModelViewSet): filterset_class = filtersets.CableFilterSet -class CableTerminationViewSet(NetBoxModelViewSet): +class CableTerminationViewSet(NetBoxReadOnlyModelViewSet): metadata_class = ContentTypeMetadata queryset = CableTermination.objects.all() serializer_class = serializers.CableTerminationSerializer diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 6a819a3c0..d0a385887 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -2376,6 +2376,33 @@ class CableTest(APIViewTestCases.APIViewTestCase): ] +class CableTerminationTest( + APIViewTestCases.GetObjectViewTestCase, + APIViewTestCases.ListObjectsViewTestCase, +): + model = CableTermination + brief_fields = ['cable', 'cable_end', 'display', 'id', 'termination_id', 'termination_type', 'url'] + + @classmethod + def setUpTestData(cls): + device1 = create_test_device('Device 1') + device2 = create_test_device('Device 2') + + interfaces = [] + for device in (device1, device2): + for i in range(0, 10): + interfaces.append(Interface(device=device, type=InterfaceTypeChoices.TYPE_1GE_FIXED, name=f'eth{i}')) + Interface.objects.bulk_create(interfaces) + + cables = ( + Cable(a_terminations=[interfaces[0]], b_terminations=[interfaces[10]], label='Cable 1'), + Cable(a_terminations=[interfaces[1]], b_terminations=[interfaces[11]], label='Cable 2'), + Cable(a_terminations=[interfaces[2]], b_terminations=[interfaces[12]], label='Cable 3'), + ) + for cable in cables: + cable.save() + + class ConnectedDeviceTest(APITestCase): @classmethod