diff --git a/computes/api/serializers.py b/computes/api/serializers.py index 2d2867e9..d260ee81 100644 --- a/computes/api/serializers.py +++ b/computes/api/serializers.py @@ -20,6 +20,10 @@ class ComputeSerializer(serializers.ModelSerializer): ) type = serializers.ChoiceField(choices=conn_types) + def validate_password(self, value): + # as on the web form, an empty password on an edit keeps the stored one + return value or (self.instance.password if self.instance else value) + class Meta: model = Compute fields = ["id", "name", "hostname", "login", "password", "type", "details", "migration_address"] diff --git a/computes/api/viewsets.py b/computes/api/viewsets.py index 8b86960d..f7d86be6 100644 --- a/computes/api/viewsets.py +++ b/computes/api/viewsets.py @@ -1,5 +1,6 @@ from admin.permissions import IsSuperUser from computes.models import Compute +from django.shortcuts import get_object_or_404 from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema from logs.audit import audit, changed_fields, snapshot @@ -50,7 +51,7 @@ class ComputeArchitecturesView(viewsets.ViewSet): """ Return a list of supported host architectures. """ - compute = Compute.objects.get(pk=compute_pk) + compute = get_object_or_404(Compute, pk=compute_pk) conn = wvmCreate( compute.hostname, compute.login, @@ -61,7 +62,7 @@ class ComputeArchitecturesView(viewsets.ViewSet): @extend_schema(responses={200: {"type": "array", "items": {"type": "string"}}}) def retrieve(self, request, compute_pk=None, pk=None): - compute = Compute.objects.get(pk=compute_pk) + compute = get_object_or_404(Compute, pk=compute_pk) conn = wvmCreate( compute.hostname, compute.login, @@ -70,19 +71,3 @@ class ComputeArchitecturesView(viewsets.ViewSet): ) return Response(conn.get_machine_types(pk)) - -class ComputeMachinesView(viewsets.ViewSet): - permission_classes = [IsSuperUser] - - def list(self, request, compute_pk=None, archs_pk=None): - """ - Return a list of supported host architectures. - """ - compute = Compute.objects.get(pk=compute_pk) - conn = wvmCreate( - compute.hostname, - compute.login, - compute.password, - compute.type, - ) - return Response(conn.get_machine_types(archs_pk)) diff --git a/computes/tests/test_compute_password.py b/computes/tests/test_compute_password.py index a6ac2614..aee972eb 100644 --- a/computes/tests/test_compute_password.py +++ b/computes/tests/test_compute_password.py @@ -31,3 +31,12 @@ class ComputePasswordTests(TestCase): compute = Compute.objects.create(name="cp-long", hostname="192.0.2.7", login="admin", password="", type=1) self.assertContains(self.client.get(reverse("compute_update", args=[compute.id])), "Edit Compute") self.assertEqual(self.edit(compute, "p" * 40), "p" * 40) + + def test_the_api_keeps_the_password_when_it_is_left_empty(self): + compute = Compute.objects.create(name="cp-api", hostname="192.0.2.8", login="admin", password="secret", type=1) + for password in ("", None): + with self.subTest(password=password): + self.client.patch(f"/api/v1/computes/{compute.id}/", {"password": password}, content_type="application/json") + compute.refresh_from_db() + self.assertEqual(compute.password, "secret") + self.assertEqual(self.client.get("/api/v1/computes/999999/archs/").status_code, 404) diff --git a/storages/api/serializers.py b/storages/api/serializers.py index 25846168..fdb52295 100644 --- a/storages/api/serializers.py +++ b/storages/api/serializers.py @@ -18,8 +18,11 @@ class StorageSerializer(serializers.ModelSerializer): class VolumeSerializer(serializers.ModelSerializer): + # what the web form (storages.forms.CreateVolumeForm) takes + name = serializers.RegexField(r"^[a-zA-Z0-9._-]+$", max_length=120) + size = serializers.IntegerField(min_value=1) # GiB allocation = serializers.ReadOnlyField() - meta_prealloc = serializers.BooleanField(write_only=True) + meta_prealloc = serializers.BooleanField(write_only=True, default=False) class Meta: model = Volume fields = ['name', 'type', 'allocation', 'size', 'meta_prealloc'] diff --git a/storages/api/viewsets.py b/storages/api/viewsets.py index 177ee761..a691402b 100644 --- a/storages/api/viewsets.py +++ b/storages/api/viewsets.py @@ -185,7 +185,7 @@ class VolumeViewSet(viewsets.ViewSet): serializer.validated_data["name"], serializer.validated_data["size"], serializer.validated_data["type"], - serializer.validated_data["meta_prealloc"], + serializer.validated_data["meta_prealloc"] and serializer.validated_data["type"] == "qcow2", int(app_settings.INSTANCE_VOLUME_DEFAULT_OWNER_UID), int(app_settings.INSTANCE_VOLUME_DEFAULT_OWNER_GID), ) diff --git a/storages/tests/test_audit_rows.py b/storages/tests/test_audit_rows.py index 99a2b4ef..3ae784d9 100644 --- a/storages/tests/test_audit_rows.py +++ b/storages/tests/test_audit_rows.py @@ -204,6 +204,18 @@ class PoolAndVolumeRowTests(TransactionTestCase): ("Storage pool default: delete volume disk1.qcow2", True), ]) + def test_the_api_takes_the_volumes_the_web_form_takes(self): + for name, size in (("x/etc/passwdy", 1), ("disk3", 0)): + with self.subTest(name=name, size=size): + response = self.client.post(self.api + "volumes/", {"name": name, "type": "qcow2", "size": size}, + content_type="application/json") + self.assertEqual(response.status_code, 400) + self.api_pool.create_volume.assert_not_called() + # metadata preallocation is a qcow2 option, as on the web form + self.client.post(self.api + "volumes/", {"name": "disk3", "type": "raw", "size": 1, "meta_prealloc": True}, + content_type="application/json") + self.assertFalse(self.api_pool.create_volume.call_args.args[3]) + def test_an_inactive_pool_refuses_api_volume_changes(self): self.api_pool.is_active.return_value = False response = self.client.post(self.api + "volumes/", {"name": "disk2", "type": "qcow2", "size": 1, diff --git a/vrtManager/storage.py b/vrtManager/storage.py index 4c607fda..03285258 100644 --- a/vrtManager/storage.py +++ b/vrtManager/storage.py @@ -332,7 +332,7 @@ class wvmStorage(wvmConnect): alloc = 0 xml = f""" - {name} + {util.xml_escape(name)} {size} {alloc}