From 470cc9860e93ec214ad6bc5746f0e71511320312 Mon Sep 17 00:00:00 2001 From: catborise Date: Sat, 7 Nov 2026 00:19:55 +0300 Subject: [PATCH] fix(api): a volume's size is GiB to create and bytes to read, each documented One serializer created volumes and read them: size was GiB in a request and bytes in an answer, the same field in the schema. A client that sent back a size it read asked for a volume 2**30 times as large. A new volume is CreateVolumeSerializer (size in GiB); a volume read is VolumeSerializer (allocation and size in bytes), and the schema says so, as for the pools' size and free space. --- storages/api/serializers.py | 24 +++++++++++++++++------- storages/api/viewsets.py | 6 +++--- webvirtcloud/tests/test_api_schema.py | 10 ++++++++++ 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/storages/api/serializers.py b/storages/api/serializers.py index e8e9f243..21b5a12b 100644 --- a/storages/api/serializers.py +++ b/storages/api/serializers.py @@ -7,7 +7,7 @@ class StoragesSerializer(serializers.Serializer): name = serializers.CharField(max_length=20) status = serializers.IntegerField() type = serializers.CharField(max_length=100) - size = serializers.IntegerField() + size = serializers.IntegerField(help_text="Capacity in bytes") volumes = serializers.IntegerField() @@ -15,8 +15,8 @@ class StorageSerializer(serializers.Serializer): """A storage pool's details.""" state = serializers.IntegerField() - size = serializers.IntegerField() - free = serializers.IntegerField() + size = serializers.IntegerField(help_text="Capacity in bytes") + free = serializers.IntegerField(help_text="Free space in bytes") status = serializers.CharField(max_length=128) path = serializers.CharField(max_length=128) type = serializers.CharField(max_length=128) @@ -26,10 +26,20 @@ class StorageSerializer(serializers.Serializer): class VolumeSerializer(serializers.Serializer): - # what the web form (storages.forms.CreateVolumeForm) takes + """A volume as the API reads it.""" + + name = serializers.CharField() + type = serializers.CharField() + allocation = serializers.IntegerField(help_text="Bytes the volume takes in its pool") + size = serializers.IntegerField(help_text="Capacity in bytes") + + +class CreateVolumeSerializer(serializers.Serializer): + """A new volume, as the web form (storages.forms.CreateVolumeForm) takes it: + its size in GiB, where a volume read back tells bytes.""" + name = serializers.RegexField(r"^[a-zA-Z0-9._-]+$", max_length=120) type = serializers.ChoiceField(choices=[("qcow2", "qcow2 (recommended)"), ("qcow", "qcow"), ("raw", "raw")], label="Format") - allocation = serializers.IntegerField(read_only=True) - size = serializers.IntegerField(min_value=1) # GiB - meta_prealloc = serializers.BooleanField(write_only=True, default=False) + size = serializers.IntegerField(min_value=1, help_text="Capacity in GiB") + meta_prealloc = serializers.BooleanField(default=False) diff --git a/storages/api/viewsets.py b/storages/api/viewsets.py index 9a96759d..91303aa2 100644 --- a/storages/api/viewsets.py +++ b/storages/api/viewsets.py @@ -15,7 +15,7 @@ from rest_framework.response import Response from vrtManager.storage import wvmStorage, wvmStorages from webvirtcloud.serializers import ErrorSerializer, StatusSerializer -from .serializers import StorageSerializer, StoragesSerializer, VolumeSerializer +from .serializers import CreateVolumeSerializer, StorageSerializer, StoragesSerializer, VolumeSerializer class StorageViewSet(viewsets.ViewSet): @@ -170,7 +170,7 @@ class VolumeViewSet(viewsets.ViewSet): return Response(serializer.data) - @extend_schema(responses={200: StatusSerializer, 400: dict, 409: ErrorSerializer}) + @extend_schema(request=CreateVolumeSerializer, responses={200: StatusSerializer, 400: dict, 409: ErrorSerializer}) def create(self, request, storage_pk=None, compute_pk=None): compute = get_object_or_404(Compute, pk=compute_pk) @@ -178,7 +178,7 @@ class VolumeViewSet(viewsets.ViewSet): compute.hostname, compute.login, compute.password, compute.type, storage_pk ) - serializer = VolumeSerializer(data=request.data) + serializer = CreateVolumeSerializer(data=request.data) if serializer.is_valid(): subject, action = f"Storage pool {storage_pk}", f"add volume {serializer.validated_data['name']}" state = conn.is_active() diff --git a/webvirtcloud/tests/test_api_schema.py b/webvirtcloud/tests/test_api_schema.py index 5b7e6a9e..8f2dccca 100644 --- a/webvirtcloud/tests/test_api_schema.py +++ b/webvirtcloud/tests/test_api_schema.py @@ -36,6 +36,16 @@ class APISchemaTestCase(TestCase): self.assertEqual(getattr(client, method)("/api/v1/nothere/").status_code, 404) self.assertEqual(client.get("/api/schema/").status_code, 200) + def test_a_volume_s_size_is_gib_to_create_and_bytes_to_read(self): + # one serializer had both: a size read back and sent again asked 2**30 times as much + schema = SchemaGenerator().get_schema(request=None, public=True) + create = schema["paths"]["/api/v1/computes/{compute_pk}/storages/{storage_pk}/volumes/"]["post"] + self.assertEqual(create["requestBody"]["content"]["application/json"]["schema"]["$ref"], + "#/components/schemas/CreateVolume") + components = schema["components"]["schemas"] + self.assertEqual(components["CreateVolume"]["properties"]["size"]["description"], "Capacity in GiB") + self.assertEqual(components["Volume"]["properties"]["size"]["description"], "Capacity in bytes") + def test_a_refusal_s_body_is_documented_with_its_409(self): schema = SchemaGenerator().get_schema(request=None, public=True) poweron = schema["paths"]["/api/v1/computes/{compute_pk}/instances/{id}/poweron/"]["post"]