From 75c8f9f804bd081965e2a86137891d7e09458a83 Mon Sep 17 00:00:00 2001 From: Anton M Date: Tue, 2 Jan 2024 19:48:42 +0400 Subject: [PATCH] serialized state api --- validity/api/helpers.py | 31 ++++++++++++++++++++++++ validity/api/serializers.py | 48 ++++++++++++++++++++++++------------- validity/api/urls.py | 7 +++--- validity/api/views.py | 30 +++++++++-------------- 4 files changed, 78 insertions(+), 38 deletions(-) diff --git a/validity/api/helpers.py b/validity/api/helpers.py index aa60013..bc7da36 100644 --- a/validity/api/helpers.py +++ b/validity/api/helpers.py @@ -32,3 +32,34 @@ def to_representation(self, value): def to_internal_value(self, data): return EncryptedDict(super().to_internal_value(data)) + + +class ListQPMixin: + """ + Serializer Mixin. Allows to get list query params in 2 forms: + 1. ?param=v1¶m=v2 + 2. ?param=v1,v2 + """ + + def get_list_param(self, param: str) -> list[str] | None: + if "request" not in self.context or param not in self.context['request'].query_params: + return None + param_value = self.context["request"].query_params.getlist(param) + if len(param_value) == 1: + return param_value[0].split(',') + return param_value + + +class FieldsMixin(ListQPMixin): + """ + Serializer Mixin. Allows to include specific fields only + """ + + query_param = "fields" + + def to_representation(self, instance): + if query_fields := self.get_list_param(self.query_param): + self.fields = { + field_name: field for field_name, field in self.fields.items() if field_name in set(query_fields) + } + return super().to_representation(instance) diff --git a/validity/api/serializers.py b/validity/api/serializers.py index 66777fb..0ccc3fc 100644 --- a/validity/api/serializers.py +++ b/validity/api/serializers.py @@ -1,5 +1,4 @@ -from urllib.parse import urljoin - +from rest_framework.fields import empty from core.api.nested_serializers import NestedDataFileSerializer, NestedDataSourceSerializer from dcim.api.nested_serializers import ( NestedDeviceSerializer, @@ -20,8 +19,8 @@ from tenancy.models import Tenant from validity import models -from .helpers import EncryptedDictField, nested_factory - +from .helpers import EncryptedDictField, FieldsMixin, nested_factory, ListQPMixin +from rest_framework.fields import empty class ComplianceSelectorSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name="plugins-api:validity-api:complianceselector-detail") @@ -260,18 +259,6 @@ def run_validation(self, data=...): NestedNameSetSerializer = nested_factory(NameSetSerializer, ("id", "url", "display", "name")) -class SerializedConfigSerializer(serializers.Serializer): - serializer = NestedSerializerSerializer(read_only=True, source="device.serializer") - data_source = NestedDataSourceSerializer(read_only=True, source="device.data_source") - data_file = NestedDataFileSerializer(read_only=True, source="device.data_file") - local_copy_last_updated = serializers.DateTimeField(allow_null=True, source="last_modified") - config_web_link = serializers.SerializerMethodField() - serialized_config = serializers.JSONField(source="serialized") - - def get_config_web_link(self, obj): - return urljoin(obj.device.data_source.web_url, obj.device.config_path) - - class DeviceReportSerializer(NestedDeviceSerializer): compliance_passed = serializers.BooleanField() results_passed = serializers.IntegerField() @@ -348,3 +335,32 @@ def validate(self, data): NestedPollerSerializer = nested_factory(PollerSerializer, ("id", "url", "display", "name")) + + +class SerializedStateItemSerializer(FieldsMixin, serializers.Serializer): + name = serializers.CharField(read_only=True) + serializer = NestedSerializerSerializer(read_only=True) + data_source = NestedDataSourceSerializer(read_only=True, source="data_file.source") + data_file = NestedDataFileSerializer(read_only=True) + command = NestedCommandSerializer(read_only=True) + last_updated = serializers.DateTimeField(allow_null=True, source="data_file.last_updated", read_only=True) + error = serializers.CharField(read_only=True) + value = serializers.SerializerMethodField(source="serialized", method_name="get_serialized") + + def get_serialized(self, state_item): + if state_item.error is not None: + return None + return state_item.serialized + + +class SerializedStateSerializer(ListQPMixin, serializers.Serializer): + count = serializers.SerializerMethodField() + results = SerializedStateItemSerializer(many=True, read_only=True, source='*') + + def get_count(self, state): + return len(state) + + def to_representation(self, instance): + if name_filter := self.get_list_param('name'): + instance = [item for item in instance if item.name in set(name_filter)] + return super().to_representation(instance) diff --git a/validity/api/urls.py b/validity/api/urls.py index e9dca9b..d6195d2 100644 --- a/validity/api/urls.py +++ b/validity/api/urls.py @@ -21,9 +21,10 @@ path("reports//devices/", views.DeviceReportView.as_view(), name="report_devices"), ] + router.urls -app_name = "validity" - dcim_urls.append( - path("devices//serialized_config/", views.SerializedConfigView.as_view(), name="serialized_config") + path("devices//serialized_state/", views.SerializedStateView.as_view(), name="serialized_state") ) + + +app_name = "validity" diff --git a/validity/api/views.py b/validity/api/views.py index f2c3c64..5afd123 100644 --- a/validity/api/views.py +++ b/validity/api/views.py @@ -1,23 +1,15 @@ -from http import HTTPStatus - +from drf_spectacular.utils import OpenApiParameter, extend_schema from netbox.api.viewsets import NetBoxModelViewSet from rest_framework.exceptions import NotFound from rest_framework.generics import ListAPIView from rest_framework.response import Response from rest_framework.views import APIView -from validity import config, filtersets, models +from validity import filtersets, models from validity.choices import SeverityChoices -from validity.compliance.exceptions import SerializationError from . import serializers -if config.netbox_version < "3.5.0": - from drf_yasg.utils import swagger_auto_schema as extend_schema -else: - from drf_spectacular.utils import extend_schema - - class ReadOnlyNetboxViewSet(NetBoxModelViewSet): http_method_names = ["get", "head", "options", "trace"] @@ -96,7 +88,7 @@ def get_queryset(self): ) -class SerializedConfigView(APIView): +class SerializedStateView(APIView): queryset = models.VDevice.objects.prefetch_datasource().prefetch_serializer().prefetch_poller() def get_object(self, pk): @@ -106,14 +98,14 @@ def get_object(self, pk): raise NotFound @extend_schema( - responses={200: serializers.SerializedConfigSerializer()}, operation_id="dcim_devices_serialized_config" + responses={200: serializers.SerializedStateSerializer()}, + operation_id="dcim_devices_serialized_state", + parameters=[ + OpenApiParameter(name="fields", type=str, many=True), + OpenApiParameter(name="name", type=str, many=True), + ], ) def get(self, request, pk): device = self.get_object(pk) - try: - serializer = serializers.SerializedConfigSerializer(device.device_config, context={"request": request}) - return Response(serializer.data) - except SerializationError as e: - return Response( - data={"detail": "Unable to fetch serialized config", "error": str(e)}, status=HTTPStatus.BAD_REQUEST - ) + serializer = serializers.SerializedStateSerializer(device.state.values(), context={"request": request}) + return Response(serializer.data)