Skip to content

Commit

Permalink
serialized state api
Browse files Browse the repository at this point in the history
  • Loading branch information
amyasnikov committed Jan 2, 2024
1 parent 5bbbc64 commit 75c8f9f
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 38 deletions.
31 changes: 31 additions & 0 deletions validity/api/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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&param=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)
48 changes: 32 additions & 16 deletions validity/api/serializers.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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")
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
7 changes: 4 additions & 3 deletions validity/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@
path("reports/<int:pk>/devices/", views.DeviceReportView.as_view(), name="report_devices"),
] + router.urls

app_name = "validity"


dcim_urls.append(
path("devices/<int:pk>/serialized_config/", views.SerializedConfigView.as_view(), name="serialized_config")
path("devices/<int:pk>/serialized_state/", views.SerializedStateView.as_view(), name="serialized_state")
)


app_name = "validity"
30 changes: 11 additions & 19 deletions validity/api/views.py
Original file line number Diff line number Diff line change
@@ -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"]

Expand Down Expand Up @@ -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):
Expand All @@ -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)

0 comments on commit 75c8f9f

Please sign in to comment.