1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-11 12:46:49 +00:00

fix(instances): disk edits keep their XML; changes lock per VM

Disk edits:
- edit_disk built a new <disk> from the form and replaced the old one,
  losing what the form does not show (e.g. encryption and virtio
  driver options); a bus change detached and re-attached the disk.
  The disk of the persistent definition is now edited in place and the
  domain defined once; a bus change picks a free device name and drops
  the old address and virtio-only options. Pending persistent changes of
  a running VM are kept.
- Clone omits empty disk devices, refuses a disk without a supported
  source and destination, and checks the name before allocating storage.

Locks:
- Every VM change held its compute's exclusive lock, so one slow
  operation blocked all VMs on that compute. A VM change now holds its
  own VM lock plus a shared compute lock; reconciliation, migration and
  cloning hold the compute exclusively (two clones of different VMs
  must not pass the name check together).
- Quota-checked changes of one user (CPU, memory and disk resize,
  clone) run one at a time under a per-user lock, also across computes,
  so concurrent requests cannot all pass the check against the old
  allocation. It is the outermost lock; superusers, who have no quota,
  skip it.
- A busy lock answers 409 to the API and a message in the web UI. API
  power and destroy actions return the view's error instead of success.
This commit is contained in:
catborise 2026-10-03 19:11:44 +03:00
parent 158d4db6c4
commit 51dedb7b8f
12 changed files with 510 additions and 269 deletions

View file

@ -33,6 +33,13 @@ from .serializers import (
)
def instance_action_response(action, request, pk, message):
response = action(request, pk)
if response.status_code >= 400:
return response
return Response({"status": message})
class InstancesViewSet(viewsets.ViewSet):
"""
A simple ViewSet for listing or retrieving ALL/Compute Instances.
@ -117,8 +124,7 @@ class InstanceViewSet(viewsets.ViewSet):
queryset = get_instance(request.user, pk, perm_type="delete")
if compute_pk is not None and str(queryset.compute_id) != str(compute_pk):
raise Http404()
instance_destroy(request, pk)
return Response({"status": "Instance is destroyed"})
return instance_action_response(instance_destroy, request, pk, "Instance is destroyed")
@extend_schema(request=None, responses=StatusSerializer)
@action(detail=True, methods=["post"])
@ -126,8 +132,7 @@ class InstanceViewSet(viewsets.ViewSet):
queryset = get_instance(request.user, pk, perm_type="power")
if compute_pk is not None and str(queryset.compute_id) != str(compute_pk):
raise Http404()
poweron(request, pk)
return Response({"status": "poweron command send"})
return instance_action_response(poweron, request, pk, "poweron command send")
@extend_schema(request=None, responses=StatusSerializer)
@action(detail=True, methods=["post"])
@ -135,8 +140,7 @@ class InstanceViewSet(viewsets.ViewSet):
queryset = get_instance(request.user, pk, perm_type="power")
if compute_pk is not None and str(queryset.compute_id) != str(compute_pk):
raise Http404()
poweroff(request, pk)
return Response({"status": "poweroff command send"})
return instance_action_response(poweroff, request, pk, "poweroff command send")
@extend_schema(request=None, responses=StatusSerializer)
@action(detail=True, methods=["post"])
@ -144,8 +148,7 @@ class InstanceViewSet(viewsets.ViewSet):
queryset = get_instance(request.user, pk, perm_type="power")
if compute_pk is not None and str(queryset.compute_id) != str(compute_pk):
raise Http404()
powercycle(request, pk)
return Response({"status": "powercycle command send"})
return instance_action_response(powercycle, request, pk, "powercycle command send")
@extend_schema(request=None, responses=StatusSerializer)
@action(detail=True, methods=["post"])
@ -153,8 +156,7 @@ class InstanceViewSet(viewsets.ViewSet):
queryset = get_instance(request.user, pk, perm_type="power")
if compute_pk is not None and str(queryset.compute_id) != str(compute_pk):
raise Http404()
force_off(request, pk)
return Response({"status": "force off command send"})
return instance_action_response(force_off, request, pk, "force off command send")
@extend_schema(request=None, responses=StatusSerializer)
@action(detail=True, methods=["post"])
@ -162,8 +164,7 @@ class InstanceViewSet(viewsets.ViewSet):
queryset = get_instance(request.user, pk)
if compute_pk is not None and str(queryset.compute_id) != str(compute_pk):
raise Http404()
suspend(request, pk)
return Response({"status": "suspend command send"})
return instance_action_response(suspend, request, pk, "suspend command send")
@extend_schema(request=None, responses=StatusSerializer)
@action(detail=True, methods=["post"])
@ -171,8 +172,7 @@ class InstanceViewSet(viewsets.ViewSet):
queryset = get_instance(request.user, pk)
if compute_pk is not None and str(queryset.compute_id) != str(compute_pk):
raise Http404()
resume(request, pk)
return Response({"status": "resume command send"})
return instance_action_response(resume, request, pk, "resume command send")
class MigrateViewSet(viewsets.ViewSet):