mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 19:26:48 +00:00
Opening /computes/<id>/storage/<pool>/ for a pool the host does not have (or a host that does not answer) raised libvirt's error out of the view: a 500. Now the pools list shows the reason, as the network page does.
269 lines
12 KiB
Python
269 lines
12 KiB
Python
import contextlib
|
|
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 logs.audit import audit, failed
|
|
|
|
from vrtManager.util import OperationError
|
|
from vrtManager.storage import wvmStorage, wvmStorages
|
|
|
|
from storages.forms import AddStgPool, CloneImage, CreateVolumeForm
|
|
from storages.jobs import is_iso
|
|
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, conn, compute, pool, request.POST.get("upload_id", ""),
|
|
{"name": request.POST.get("file_name", ""), "size": file_size, "chunks": total_chunks},
|
|
chunk_index, file_chunk)
|
|
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)
|
|
form = AddStgPool()
|
|
# the new pool dialog's type: its radio cards tell the ISO pool from another directory
|
|
pool_kind = request.POST.get("pool_kind", "dir")
|
|
|
|
try:
|
|
conn = wvmStorages(
|
|
compute.hostname, compute.login, compute.password, compute.type
|
|
)
|
|
storages = conn.get_storages_info()
|
|
iso_pool = next((pool for pool in storages if pool["name"] == "iso"), None)
|
|
# the secrets an RBD pool can use, by the name they were given
|
|
ceph_secrets = [secret for secret in conn.get_secrets_info() if secret["usage_type"] == "ceph"]
|
|
|
|
if request.method == "POST" and "create" in request.POST:
|
|
form = AddStgPool(request.POST, pools=storages, ceph_secrets=[s["uuid"] for s in ceph_secrets])
|
|
if form.is_valid():
|
|
data = form.cleaned_data
|
|
try:
|
|
with audit(request, compute, f"Storage pool {data['name']}", "add"):
|
|
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"])
|
|
except libvirtError as err: # shown in the dialog, which opens again
|
|
form.add_error(None, str(err))
|
|
else:
|
|
return HttpResponseRedirect(reverse("storage", args=[compute_id, data["name"]]))
|
|
elif form.has_error("name", "taken"):
|
|
failed(request, compute, f"Storage pool {form.data['name'].strip()}", "add", "name already in use")
|
|
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)
|
|
form = CreateVolumeForm()
|
|
|
|
try:
|
|
conn = wvmStorage(compute.hostname, compute.login, compute.password, compute.type, pool)
|
|
except libvirtError as lib_err: # no such pool on the host, or the host does not answer
|
|
messages.error(request, lib_err)
|
|
return HttpResponseRedirect(reverse("storages", args=[compute_id]))
|
|
|
|
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()
|
|
|
|
if request.method == "POST":
|
|
subject = f"Storage pool {pool}"
|
|
if "start" in request.POST:
|
|
with audit(request, compute, subject, "start"):
|
|
conn.start()
|
|
return HttpResponseRedirect(request.get_full_path())
|
|
if "stop" in request.POST:
|
|
try:
|
|
with audit(request, compute, subject, "stop"):
|
|
conn.stop()
|
|
except OperationError as err: # a running VM's disk is in it
|
|
messages.error(request, str(err))
|
|
return HttpResponseRedirect(request.get_full_path())
|
|
if "delete" in request.POST:
|
|
try:
|
|
with audit(request, compute, subject, "delete"):
|
|
conn.delete()
|
|
except OperationError as err: # a VM names it for a disk
|
|
messages.error(request, str(err))
|
|
return HttpResponseRedirect(request.get_full_path())
|
|
return HttpResponseRedirect(reverse("storages", args=[compute_id]))
|
|
if "set_autostart" in request.POST:
|
|
with audit(request, compute, subject, "autostart on"):
|
|
conn.set_autostart(1)
|
|
return HttpResponseRedirect(request.get_full_path())
|
|
if "unset_autostart" in request.POST:
|
|
with audit(request, compute, subject, "autostart off"):
|
|
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 audit(request, compute, subject, f"delete volume {volname}"), 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 "create_volume" in request.POST:
|
|
form = CreateVolumeForm(request.POST)
|
|
if form.is_valid():
|
|
data = form.cleaned_data
|
|
with audit(request, compute, subject, f"add volume {data['name']}") as entry:
|
|
name = conn.create_volume(
|
|
data["name"],
|
|
data["size"],
|
|
data["format"],
|
|
data["meta_prealloc"] and data["format"] == "qcow2",
|
|
int(app_settings.INSTANCE_VOLUME_DEFAULT_OWNER_UID),
|
|
int(app_settings.INSTANCE_VOLUME_DEFAULT_OWNER_GID),
|
|
)
|
|
entry.action = f"add volume {name}"
|
|
messages.success(request, _("Image file %(name)s is created successfully") % {"name": name})
|
|
return redirect(reverse("storage", args=[compute.id, pool]))
|
|
# refused: the page shows the dialog again, with what was entered and why;
|
|
# a pool stopped meanwhile has no dialog, so the reasons go to the page
|
|
if not state:
|
|
for msg_err in form.errors.values():
|
|
messages.error(request, msg_err.as_text())
|
|
if "cln_volume" in request.POST:
|
|
form = CloneImage(request.POST)
|
|
if form.is_valid():
|
|
data = form.cleaned_data
|
|
try:
|
|
with audit(request, compute, f"Storage pool {pool}",
|
|
f"clone volume {data['image']} as {data['name']}") as entry:
|
|
op = jobs.submit("volume.clone", request.user, {"compute": compute.id, "pool": pool, **data},
|
|
request.POST.get("submission_key", ""))
|
|
entry.skip = True # the job writes its row when it ends
|
|
except claims.Busy as busy:
|
|
messages.error(request, busy_message(busy, request.user))
|
|
except (jobs.SubmissionConflict, OperationError, libvirtError) as err:
|
|
messages.error(request, str(err))
|
|
else:
|
|
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]))
|
|
|
|
# read only for the page: every action above redirects, but a refused new volume
|
|
# first: it refreshes the pool, and libvirt measures a pool's space only then
|
|
volumes = conn.update_volumes() if state else None
|
|
for volume in volumes or []:
|
|
volume["iso"] = is_iso(volume["name"])
|
|
try:
|
|
size, free = conn.get_size()
|
|
status = conn.get_status()
|
|
used = size - free
|
|
if state and size: # an iSCSI pool without LUNs has no size
|
|
percent = (used * 100) // size
|
|
else:
|
|
percent = 0
|
|
except libvirtError: # both read the pool's info: the page says it could not be read
|
|
size = free = used = percent = status = None
|
|
|
|
path = conn.get_target_path() or conn.get_source_name() # an RBD pool: its Ceph pool
|
|
type = conn.get_type()
|
|
autostart = conn.get_autostart()
|
|
job_resource = claims.pool_key(compute.id, pool)
|
|
conn.close()
|
|
|
|
return render(request, "storage.html", locals())
|
|
|
|
|
|
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
|
|
)
|
|
with contextlib.suppress(libvirtError): # refused while a volume is cloned in the 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)
|