1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-09 03:36:48 +00:00
webvirtcloud/storages/views.py
catborise ef1faf57c8 fix(storages): an unexpected ISO upload failure is logged
The upload view sent the error of an unexpected failure to the browser
but left nothing in the server log, so whoever runs the server had no
trace of it. It is logged with its traceback now; the superuser still
sees the message.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 21:43:56 +03:00

300 lines
11 KiB
Python

import logging
from admin.decorators import superuser_only
from appsettings.settings import app_settings
from computes.models import Compute
from computes.utils import libvirt_compute_lock
from django.contrib import messages
from django.http import HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from libvirt import libvirtError
from operations import claims, jobs
from operations.utils import busy_message
from vrtManager.util import OperationError
from vrtManager.storage import wvmStorage, wvmStorages
from storages.forms import AddStgPool, CloneImage, CreateVolumeForm
from storages.upload import write_chunk
def _handle_iso_upload(request, conn, compute, pool):
file_chunk = request.FILES.get("file")
if not file_chunk:
return JsonResponse({"error": _("No file chunk was submitted.")}, status=400)
try:
chunk_index = int(request.POST.get("chunk_index", 0))
total_chunks = int(request.POST.get("total_chunks", 1))
except (ValueError, TypeError):
return JsonResponse({"error": _("Invalid chunk parameters.")}, status=400)
if chunk_index < 0 or chunk_index >= total_chunks:
return JsonResponse({"error": _("Invalid chunk index or total chunks.")}, status=400)
try:
file_size = int(request.POST.get("file_size", ""))
except (ValueError, TypeError):
return JsonResponse({"error": _("Invalid file size.")}, status=400)
try:
op = write_chunk(request.user, conn, compute, pool, request.POST.get("upload_id", ""),
{"name": request.POST.get("file_name", ""), "size": file_size, "chunks": total_chunks},
chunk_index, file_chunk, request=request)
except claims.Busy as busy:
if busy.blocker is not None: # another job holds the pool or the name
return JsonResponse({"error": busy_message(busy, request.user)}, status=400)
return JsonResponse({"error": _("Upload session is busy. Please retry.")}, status=429)
except FileExistsError:
return JsonResponse({"error": _("ISO image already exists")}, status=400)
except (ValueError, jobs.SubmissionConflict, OperationError) as e:
return JsonResponse({"error": str(e)}, status=400)
except Exception as e:
logging.getLogger(__name__).exception("ISO upload chunk failed")
return JsonResponse({"error": str(e)}, status=500)
return JsonResponse({"success": True, "job": str(op.pk)})
@superuser_only
def storages(request, compute_id):
"""
:param request:
:param compute_id:
:return:
"""
compute = get_object_or_404(Compute, pk=compute_id)
errors = False
try:
conn = wvmStorages(
compute.hostname, compute.login, compute.password, compute.type
)
storages = conn.get_storages_info()
secrets = conn.get_secrets()
if request.method == "POST":
if "create" in request.POST:
form = AddStgPool(request.POST)
if form.is_valid():
data = form.cleaned_data
if data["name"] in storages:
msg = _("Pool name already use")
messages.error(request, msg)
errors = True
if data["stg_type"] == "rbd":
if not data["secret"]:
msg = _("You need create secret for pool")
messages.error(request, msg)
errors = True
if (
not data["ceph_pool"]
and not data["ceph_host"]
and not data["ceph_user"]
):
msg = _("You need input all fields for creating ceph pool")
messages.error(request, msg)
errors = True
if not errors:
if data["stg_type"] == "rbd":
conn.create_storage_ceph(
data["stg_type"],
data["name"],
data["ceph_pool"],
data["ceph_host"],
data["ceph_user"],
data["secret"],
)
elif data["stg_type"] == "netfs":
conn.create_storage_netfs(
data["stg_type"],
data["name"],
data["netfs_host"],
data["source"],
data["source_format"],
data["target"],
)
else:
conn.create_storage(
data["stg_type"],
data["name"],
data["source"],
data["target"],
)
return HttpResponseRedirect(
reverse("storage", args=[compute_id, data["name"]])
)
else:
for msg_err in form.errors.values():
messages.error(request, msg_err.as_text())
conn.close()
except libvirtError as lib_err:
messages.error(request, lib_err)
return render(request, "storages.html", locals())
@superuser_only
def storage(request, compute_id, pool):
"""
:param request:
:param compute_id:
:param pool:
:return:
"""
compute = get_object_or_404(Compute, pk=compute_id)
meta_prealloc = False
form = CreateVolumeForm()
conn = wvmStorage(
compute.hostname, compute.login, compute.password, compute.type, pool
)
if request.method == "POST" and "iso_upload" in request.POST:
try:
return _handle_iso_upload(request, conn, compute, pool)
finally:
conn.close()
storages = conn.get_storages()
state = conn.is_active()
try:
size, free = conn.get_size()
used = size - free
if state:
percent = (used * 100) // size
else:
percent = 0
except libvirtError:
size, free, used, percent = 0, 0, 0, 0
status = conn.get_status()
path = conn.get_target_path()
type = conn.get_type()
autostart = conn.get_autostart()
if state:
conn.refresh()
volumes = conn.update_volumes()
else:
volumes = None
if request.method == "POST":
if "start" in request.POST:
conn.start()
return HttpResponseRedirect(request.get_full_path())
if "stop" in request.POST:
conn.stop()
return HttpResponseRedirect(request.get_full_path())
if "delete" in request.POST:
conn.delete()
return HttpResponseRedirect(reverse("storages", args=[compute_id]))
if "set_autostart" in request.POST:
conn.set_autostart(1)
return HttpResponseRedirect(request.get_full_path())
if "unset_autostart" in request.POST:
conn.set_autostart(0)
return HttpResponseRedirect(request.get_full_path())
if "del_volume" in request.POST:
volname = request.POST.get("volname", "")
try:
# no VM operation attaches it between the check and the delete
with libvirt_compute_lock(compute):
conn.del_volume(volname)
except OperationError as err:
messages.error(request, str(err))
except claims.Busy as busy:
messages.error(request, busy_message(busy, request.user))
else:
messages.success(
request, _("Volume: %(vol)s is deleted.") % {"vol": volname}
)
return redirect(reverse("storage", args=[compute.id, pool]))
# return HttpResponseRedirect(request.get_full_path())
if "cln_volume" in request.POST:
form = CloneImage(request.POST)
if form.is_valid():
try:
op = jobs.submit("volume.clone", request.user, {"compute": compute.id, "pool": pool,
**form.cleaned_data}, request.POST.get("submission_key", ""))
except claims.Busy as busy:
messages.error(request, busy_message(busy, request.user))
except (jobs.SubmissionConflict, libvirtError) as err:
messages.error(request, str(err))
else:
messages.info(request, _("The volume is being cloned; follow it in the job panel."))
jobs.remember_job(request, op)
else:
for msg_err in form.errors.values():
messages.error(request, msg_err.as_text())
return redirect(reverse("storage", args=[compute.id, pool]))
job_resource = claims.pool_key(compute.id, pool)
conn.close()
return render(request, "storage.html", locals())
@superuser_only
def create_volume(request, compute_id, pool):
"""
:param request:
:param compute_id: compute id
:param pool: pool name
:return:
"""
compute = get_object_or_404(Compute, pk=compute_id)
meta_prealloc = False
conn = wvmStorage(
compute.hostname, compute.login, compute.password, compute.type, pool
)
form = CreateVolumeForm(request.POST or None)
if form.is_valid():
data = form.cleaned_data
if data["meta_prealloc"] and data["format"] == "qcow2":
meta_prealloc = True
disk_owner_uid = int(app_settings.INSTANCE_VOLUME_DEFAULT_OWNER_UID)
disk_owner_gid = int(app_settings.INSTANCE_VOLUME_DEFAULT_OWNER_GID)
name = conn.create_volume(
data["name"],
data["size"],
data["format"],
meta_prealloc,
disk_owner_uid,
disk_owner_gid,
)
messages.success(
request, _("Image file %(name)s is created successfully") % {"name": name}
)
else:
for msg_err in form.errors.values():
messages.error(request, msg_err.as_text())
return redirect(reverse("storage", args=[compute.id, pool]))
def get_volumes(request, compute_id, pool):
"""
:param request:
:param compute_id: compute id
:param pool: pool name
:return: volumes list of pool
"""
data = {}
compute = get_object_or_404(Compute, pk=compute_id)
try:
conn = wvmStorage(
compute.hostname, compute.login, compute.password, compute.type, pool
)
conn.refresh()
data["vols"] = sorted(conn.get_volumes())
except libvirtError as err:
# do not answer 200 with an empty list when the listing failed: the
# caller cannot tell "no volumes" from "could not read the pool"
return JsonResponse({"error": str(err)}, status=400)
return JsonResponse(data)