mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 19:26:48 +00:00
- libvirt errors all answered 400. A VM, pool or volume the host does not
have now answers 404, a volume in use 409 (vrtManager.util.InUse) and a
host that cannot be reached 503, which a client may retry.
- A template's power on or cycle and a stopped pool answered
{"status": ...} with 409; they answer {"detail": ...}, as every other
refusal. The schema documents these 409s (ErrorSerializer).
- The volume list and a volume of a stopped pool answered 200 with an
empty list or a made-up volume; they answer 409.
- An API URL that names nothing answered the HTML error page; it answers
a JSON 404 after the API's authentication.
- A session that must change its password was redirected to the HTML
form; an API request of it gets the JSON 403 a token gets.
268 lines
11 KiB
Python
268 lines
11 KiB
Python
from admin.permissions import IsSuperUser
|
|
from computes import utils
|
|
from computes.models import Compute
|
|
from django.contrib.messages import get_messages
|
|
from django.http import Http404
|
|
from django.shortcuts import get_object_or_404
|
|
from django.urls import reverse
|
|
from django.utils.translation import gettext as _
|
|
from instances.jobs import MIGRATE_OPTIONS
|
|
from instances.models import Flavor, Instance
|
|
from instances.views import (
|
|
force_off,
|
|
get_instance,
|
|
powercycle,
|
|
poweroff,
|
|
poweron,
|
|
resume,
|
|
suspend,
|
|
)
|
|
from drf_spectacular.utils import extend_schema
|
|
from logs.audit import audit, changed_fields, snapshot
|
|
from operations import jobs
|
|
from operations.api.serializers import OperationSerializer
|
|
from rest_framework import permissions, status, viewsets
|
|
from rest_framework.decorators import action
|
|
from rest_framework.response import Response
|
|
from webvirtcloud.serializers import ActionStatusSerializer, ErrorSerializer
|
|
|
|
from .serializers import (
|
|
CreateInstanceSerializer,
|
|
DestroySerializer,
|
|
FlavorSerializer,
|
|
InstanceDetailsSerializer,
|
|
InstanceSerializer,
|
|
MigrateSerializer,
|
|
)
|
|
|
|
|
|
def instance_action_response(action, request, pk, message):
|
|
response = action(request, pk)
|
|
if response.status_code >= 400:
|
|
return response
|
|
# the page's view reports a partial result as a message for the page it
|
|
# redirects to: the API answers with them
|
|
return Response({"status": message, "messages": [str(m) for m in get_messages(request)]})
|
|
|
|
|
|
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(request=DestroySerializer, responses={202: OperationSerializer, 400: dict, 409: ErrorSerializer})
|
|
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()
|
|
serializer = DestroySerializer(data=request.data)
|
|
if not serializer.is_valid():
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
data = dict(serializer.validated_data)
|
|
key = data.pop("submission_key")
|
|
op = jobs.submit("instance.delete", request.user, {"instance": queryset.pk, **data}, key)
|
|
return Response(OperationSerializer(op, context={"request": request}).data, status=status.HTTP_202_ACCEPTED,
|
|
headers={"Location": reverse("operation-detail", args=[op.pk])})
|
|
|
|
@extend_schema(request=None, responses={200: ActionStatusSerializer, 409: ErrorSerializer})
|
|
@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()
|
|
if queryset.is_template:
|
|
return Response({"detail": _("Templates cannot be started.")}, status=status.HTTP_409_CONFLICT)
|
|
return instance_action_response(poweron, request, pk, "poweron command send")
|
|
|
|
@extend_schema(request=None, responses={200: ActionStatusSerializer, 409: ErrorSerializer})
|
|
@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={200: ActionStatusSerializer, 409: ErrorSerializer})
|
|
@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()
|
|
if queryset.is_template:
|
|
return Response({"detail": _("Templates cannot be started.")}, status=status.HTTP_409_CONFLICT)
|
|
return instance_action_response(powercycle, request, pk, "powercycle command send")
|
|
|
|
@extend_schema(request=None, responses={200: ActionStatusSerializer, 409: ErrorSerializer})
|
|
@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={200: ActionStatusSerializer, 409: ErrorSerializer})
|
|
@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={200: ActionStatusSerializer, 409: ErrorSerializer})
|
|
@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={202: OperationSerializer, 400: dict, 409: ErrorSerializer})
|
|
def create(self, request):
|
|
serializer = MigrateSerializer(data=request.data)
|
|
if not serializer.is_valid():
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
data = serializer.validated_data
|
|
raw = {"instance": data["instance"].pk, "target": data["target_compute"].pk, "disk_pools": data["disk_pools"],
|
|
**{name: data.get(name, False) for name in MIGRATE_OPTIONS}}
|
|
op = jobs.submit("instance.migrate", request.user, raw, data["submission_key"])
|
|
return Response(OperationSerializer(op, context={"request": request}).data, status=status.HTTP_202_ACCEPTED,
|
|
headers={"Location": reverse("operation-detail", args=[op.pk])})
|
|
|
|
|
|
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()]
|
|
|
|
def perform_create(self, serializer):
|
|
with audit(self.request, None, f"Flavor {serializer.validated_data['label']}", "add"):
|
|
serializer.save()
|
|
|
|
def perform_update(self, serializer):
|
|
flavor = serializer.instance
|
|
subject, before = f"Flavor {flavor.label}", snapshot(flavor)
|
|
with audit(self.request, None, subject, "change") as entry:
|
|
serializer.save()
|
|
fields = changed_fields(before, snapshot(flavor))
|
|
entry.action = "change " + ", ".join(fields)
|
|
entry.skip = not fields
|
|
|
|
def perform_destroy(self, instance):
|
|
with audit(self.request, None, f"Flavor {instance.label}", "delete"):
|
|
instance.delete()
|
|
|
|
|
|
class CreateInstanceViewSet(viewsets.ViewSet):
|
|
"""
|
|
A viewset for creating instances.
|
|
"""
|
|
|
|
permission_classes = [IsSuperUser]
|
|
serializer_class = CreateInstanceSerializer
|
|
queryset = ""
|
|
|
|
@extend_schema(responses={202: OperationSerializer, 400: dict, 409: ErrorSerializer})
|
|
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 not serializer.is_valid():
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
data = dict(serializer.validated_data)
|
|
key = data.pop("submission_key")
|
|
op = jobs.submit("instance.create", request.user, {**data, "compute": compute_pk, "arch": arch,
|
|
"machine": machine}, key)
|
|
return Response(OperationSerializer(op, context={"request": request}).data, status=status.HTTP_202_ACCEPTED,
|
|
headers={"Location": reverse("operation-detail", args=[op.pk])})
|