mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 11:16:47 +00:00
- Routers use path converters (use_regex_path=False): computes, instances and flavors take <int:...> ids, so non-numeric ids get 404 and drf-spectacular derives the parameter types itself. - Each method declares what it really returns: detail serializers, a shared StatusSerializer for status-only replies, no request body for actions. Instance properties used by the details serializer get return type hints. - The create-instance API drops the firmware field. Its only accepted value was "" (BIOS), which produced an empty, invalid enum, and the UEFI branch behind it was unreachable. Clients that still send it are unaffected; DRF ignores unknown fields. The schema now passes spectacular --validate --fail-on-warn; a test keeps it that way.
317 lines
12 KiB
Python
317 lines
12 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 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,
|
|
)
|
|
|
|
|
|
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()
|
|
instance_destroy(request, pk)
|
|
return Response({"status": "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()
|
|
poweron(request, pk)
|
|
return Response({"status": "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()
|
|
poweroff(request, pk)
|
|
return Response({"status": "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()
|
|
powercycle(request, pk)
|
|
return Response({"status": "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()
|
|
force_off(request, pk)
|
|
return Response({"status": "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()
|
|
suspend(request, pk)
|
|
return Response({"status": "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()
|
|
resume(request, pk)
|
|
return Response({"status": "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"]
|
|
xml_del = serializer.validated_data["xml_del"]
|
|
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,
|
|
xml_del,
|
|
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 = 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):
|
|
ret = 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"],
|
|
graphics=serializer.validated_data["graphics"],
|
|
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"],
|
|
)
|
|
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)
|