mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 19:26:48 +00:00
fix(api): host API checks what the web forms check
- An API volume takes the name and size the web form takes. Its name went into the volume XML unchecked, so a name could add elements such as a backing store pointing at another VM's disk; the name is also escaped there now. A size of 0 and metadata preallocation on a non-qcow2 volume are refused or dropped, as on the web form. - An API compute update with an empty or null password keeps the stored one, as the web form does; it emptied it and cut the host off. - The architectures endpoint answers 404 for an unknown compute instead of 500; an unrouted copy of it is removed.
This commit is contained in:
parent
c69c65728b
commit
726f330dd0
7 changed files with 34 additions and 21 deletions
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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</name><backingStore><path>/etc/passwd</path></backingStore><name>y", 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,
|
||||
|
|
|
|||
|
|
@ -332,7 +332,7 @@ class wvmStorage(wvmConnect):
|
|||
alloc = 0
|
||||
xml = f"""
|
||||
<volume>
|
||||
<name>{name}</name>
|
||||
<name>{util.xml_escape(name)}</name>
|
||||
<capacity>{size}</capacity>
|
||||
<allocation>{alloc}</allocation>
|
||||
<target>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue