1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 19:26:48 +00:00
webvirtcloud/instances/api/viewsets.py
catborise 25b3671c33 fix(instances): a migrated VM's source definition is always removed
Migration left the VM defined on the source host unless "Delete
original" was ticked (VIR_MIGRATE_UNDEFINE_SOURCE). With the shared
storage migration needs, that definition points at the disks the
destination VM is running on: the VM showed on both hosts and could be
started (or autostarted) a second time against the same disks; only a
configured lock manager stops that.

moveto now always passes VIR_MIGRATE_UNDEFINE_SOURCE with
VIR_MIGRATE_PERSIST_DEST. The option is gone from the form, the view,
the API (MigrateSerializer.xml_del) and the clone auto-migration. It
removes the definition, not the disk files.
2026-10-06 13:58:59 +03:00

331 lines
13 KiB
Python

from admin.permissions import IsSuperUser
from appsettings.settings import app_settings
from computes import utils
from computes.models import Compute
from django.http import Http404
from django.shortcuts import get_object_or_404
from instances.models import Flavor, Instance
from instances.utils import migrate_instance
from instances.views import destroy as instance_destroy
from instances.views import (
force_off,
get_instance,
powercycle,
poweroff,
poweron,
resume,
suspend,
)
from drf_spectacular.utils import extend_schema
from libvirt import libvirtError
from rest_framework import permissions, status, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from vrtManager import util
from vrtManager.create import wvmCreate
from webvirtcloud.serializers import StatusSerializer
from .serializers import (
CreateInstanceSerializer,
FlavorSerializer,
InstanceDetailsSerializer,
InstanceSerializer,
MigrateSerializer,
)
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.
"""
permission_classes = [permissions.IsAuthenticated]
serializer_class = InstanceSerializer
lookup_value_converter = "int"
def list(self, request):
if request.user.is_superuser or request.user.has_perm(
"instances.view_instances"
):
queryset = Instance.objects.all().prefetch_related("userinstance_set")
else:
queryset = Instance.objects.filter(
userinstance__user=request.user
).prefetch_related("userinstance_set")
serializer = InstanceSerializer(
queryset, many=True, context={"request": request}
)
return Response(serializer.data)
def retrieve(self, request, pk=None, compute_pk=None):
queryset = get_instance(request.user, pk)
serializer = InstanceSerializer(queryset, context={"request": request})
return Response(serializer.data)
class InstanceViewSet(viewsets.ViewSet):
"""
A simple ViewSet for listing or retrieving Compute Instances.
"""
permission_classes = [permissions.IsAuthenticated]
serializer_class = InstanceSerializer
lookup_value_converter = "int"
def list(self, request, compute_pk=None):
compute = get_object_or_404(Compute, pk=compute_pk)
is_admin = request.user.is_superuser or request.user.has_perm(
"instances.view_instances"
)
if not is_admin:
if not Instance.objects.filter(
compute=compute, userinstance__user=request.user
).exists():
return Response([])
utils.refresh_instance_database(compute)
if is_admin:
queryset = Instance.objects.filter(compute=compute).prefetch_related(
"userinstance_set"
)
else:
queryset = Instance.objects.filter(
compute=compute, userinstance__user=request.user
).prefetch_related("userinstance_set")
serializer = InstanceSerializer(
queryset, many=True, context={"request": request}
)
return Response(serializer.data)
@extend_schema(responses=InstanceDetailsSerializer)
def retrieve(self, request, pk=None, compute_pk=None):
queryset = get_instance(request.user, pk)
if compute_pk is not None and str(queryset.compute_id) != str(compute_pk):
raise Http404()
serializer = InstanceDetailsSerializer(queryset, context={"request": request})
return Response(serializer.data)
@extend_schema(responses=StatusSerializer)
def destroy(self, request, pk=None, compute_pk=None):
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()
return instance_action_response(instance_destroy, request, pk, "Instance is destroyed")
@extend_schema(request=None, responses=StatusSerializer)
@action(detail=True, methods=["post"])
def poweron(self, request, pk=None, compute_pk=None):
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()
return instance_action_response(poweron, request, pk, "poweron command send")
@extend_schema(request=None, responses=StatusSerializer)
@action(detail=True, methods=["post"])
def poweroff(self, request, pk=None, compute_pk=None):
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()
return instance_action_response(poweroff, request, pk, "poweroff command send")
@extend_schema(request=None, responses=StatusSerializer)
@action(detail=True, methods=["post"])
def powercycle(self, request, pk=None, compute_pk=None):
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()
return instance_action_response(powercycle, request, pk, "powercycle command send")
@extend_schema(request=None, responses=StatusSerializer)
@action(detail=True, methods=["post"])
def forceoff(self, request, pk=None, compute_pk=None):
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()
return instance_action_response(force_off, request, pk, "force off command send")
@extend_schema(request=None, responses=StatusSerializer)
@action(detail=True, methods=["post"])
def suspend(self, request, pk=None, compute_pk=None):
queryset = get_instance(request.user, pk)
if compute_pk is not None and str(queryset.compute_id) != str(compute_pk):
raise Http404()
return instance_action_response(suspend, request, pk, "suspend command send")
@extend_schema(request=None, responses=StatusSerializer)
@action(detail=True, methods=["post"])
def resume(self, request, pk=None, compute_pk=None):
queryset = get_instance(request.user, pk)
if compute_pk is not None and str(queryset.compute_id) != str(compute_pk):
raise Http404()
return instance_action_response(resume, request, pk, "resume command send")
class MigrateViewSet(viewsets.ViewSet):
"""
A viewset for migrating instances.
"""
permission_classes = [IsSuperUser]
serializer_class = MigrateSerializer
queryset = ""
@extend_schema(responses=StatusSerializer)
def create(self, request):
serializer = MigrateSerializer(data=request.data)
if serializer.is_valid():
instance = serializer.validated_data["instance"]
target_host = serializer.validated_data["target_compute"]
live = serializer.validated_data["live"]
unsafe = serializer.validated_data["unsafe"]
offline = serializer.validated_data["offline"]
autoconverge = serializer.validated_data["autoconverge"]
postcopy = serializer.validated_data["postcopy"]
compress = serializer.validated_data["compress"]
migrate_instance(
target_host,
instance,
request.user,
live,
unsafe,
offline,
autoconverge,
compress,
postcopy,
)
return Response({"status": "instance migrate is started"})
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class FlavorViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows flavor to be viewed or edited.
"""
queryset = Flavor.objects.all().order_by("id")
lookup_value_converter = "int"
serializer_class = FlavorSerializer
def get_permissions(self):
if self.action in ["list", "retrieve"]:
return [permissions.IsAuthenticated()]
return [IsSuperUser()]
class CreateInstanceViewSet(viewsets.ViewSet):
"""
A viewset for creating instances.
"""
permission_classes = [IsSuperUser]
serializer_class = CreateInstanceSerializer
queryset = ""
@extend_schema(responses=StatusSerializer)
def create(self, request, compute_pk=None, arch=None, machine=None):
serializer = CreateInstanceSerializer(
data=request.data,
context={"compute_pk": compute_pk, "arch": arch, "machine": machine},
)
if serializer.is_valid():
volume_list = []
default_bus = app_settings.INSTANCE_VOLUME_DEFAULT_BUS
default_io = app_settings.INSTANCE_VOLUME_DEFAULT_IO
default_discard = app_settings.INSTANCE_VOLUME_DEFAULT_DISCARD
default_zeroes = app_settings.INSTANCE_VOLUME_DEFAULT_DETECT_ZEROES
default_scsi_disk_model = (
app_settings.INSTANCE_VOLUME_DEFAULT_SCSI_CONTROLLER
)
default_disk_format = app_settings.INSTANCE_VOLUME_DEFAULT_FORMAT
default_disk_owner_uid = int(app_settings.INSTANCE_VOLUME_DEFAULT_OWNER_UID)
default_disk_owner_gid = int(app_settings.INSTANCE_VOLUME_DEFAULT_OWNER_GID)
compute = Compute.objects.get(pk=compute_pk)
conn = wvmCreate(
compute.hostname,
compute.login,
compute.password,
compute.type,
)
path = None # the volume this request allocated
try:
path = conn.create_volume(
serializer.validated_data["storage"],
serializer.validated_data["name"],
serializer.validated_data["hdd_size"],
default_disk_format,
serializer.validated_data["meta_prealloc"],
default_disk_owner_uid,
default_disk_owner_gid,
)
volume = {}
volume["device"] = "disk"
volume["path"] = path
volume["type"] = conn.get_volume_format_type(path)
volume["cache_mode"] = serializer.validated_data["cache_mode"]
volume["bus"] = default_bus
if volume["bus"] == "scsi":
volume["scsi_model"] = default_scsi_disk_model
volume["discard_mode"] = default_discard
volume["detect_zeroes_mode"] = default_zeroes
volume["io_mode"] = default_io
volume_list.append(volume)
with utils.libvirt_compute_lock(compute):
conn.create_instance(
name=serializer.validated_data["name"],
memory=serializer.validated_data["memory"],
vcpu=serializer.validated_data["vcpu"],
vcpu_mode=serializer.validated_data["vcpu_mode"],
uuid=util.randomUUID(),
arch=arch,
machine=machine,
firmware={},
volumes=volume_list,
networks=serializer.validated_data["networks"],
nwfilter=serializer.validated_data["nwfilter"],
virtio=serializer.validated_data["virtio"],
listener_addr=serializer.validated_data["listener_addr"],
video=serializer.validated_data["video"],
console_pass=serializer.validated_data["console_pass"],
mac=serializer.validated_data["mac"],
qemu_ga=serializer.validated_data["qemu_ga"],
)
except Exception as err:
# A failed definition must not leave the new disk behind
if path:
try:
conn.delete_volume(path)
except libvirtError:
return Response(
{"status": f"{err}; remove {path} by hand"}, status=status.HTTP_400_BAD_REQUEST
)
if not isinstance(err, libvirtError):
raise
return Response({"status": str(err)}, status=status.HTTP_400_BAD_REQUEST)
# After the definition: the VM uses the volume, so no rollback here.
with utils.libvirt_compute_lock(compute):
utils.refresh_instance_database(compute)
msg = f"Instance {serializer.validated_data['name']} is created"
return Response({"status": msg})
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)