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}