mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-09 11:46:49 +00:00
libvirt deletes any volume it is asked to. The storage page and the API deleted a VM's disk, the base image of another volume or the file a snapshot was taken on without a check, breaking the VM or its snapshots. wvmStorage.del_volume now refuses a volume a domain on the compute uses as a disk or a backing file of one, a file of a domain's snapshots, or the backing file of another volume. The check is the one destroy already used, moved to wvmConnect and extended to snapshot files, so destroy also keeps the files of another VM's snapshots. The storage page shows the refusal; the API answers 400 with it, as for other refusals. Deleting a disk from a VM refuses after the detach when the VM's own snapshots need the volume, and says the volume was kept. Network disks count by their source name, pool volume disks by their volume's path. A volume whose metadata cannot be read stops the check instead of counting as unused; only one removed meanwhile is skipped. The check and the delete hold the compute lock, so no VM operation attaches the volume in between; a busy compute is a message on the page and 409 in the API.
348 lines
13 KiB
Python
348 lines
13 KiB
Python
import os
|
|
import re
|
|
|
|
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 vrtManager.connection import CONN_TCP, CONN_TLS
|
|
from vrtManager.util import OperationError
|
|
from vrtManager.storage import wvmStorage, wvmStorages
|
|
|
|
from storages.forms import AddStgPool, CloneImage, CreateVolumeForm
|
|
from storages.upload import handle_uploaded_file
|
|
|
|
|
|
def _handle_iso_upload(request, conn, path, compute, pool):
|
|
file_chunk = request.FILES.get("file")
|
|
if not file_chunk:
|
|
return JsonResponse({"error": _("No file chunk was submitted.")}, status=400)
|
|
|
|
file_name = request.POST.get("file_name", "")
|
|
clean_file_name = os.path.basename(file_name).strip()
|
|
if not clean_file_name or clean_file_name in (".", "..") or "\x00" in clean_file_name:
|
|
return JsonResponse({"error": _("Invalid file name.")}, status=400)
|
|
file_name = clean_file_name
|
|
|
|
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 total_chunks < 1 or chunk_index < 0 or chunk_index >= total_chunks:
|
|
return JsonResponse({"error": _("Invalid chunk index or total chunks.")}, status=400)
|
|
|
|
is_last_chunk = chunk_index == total_chunks - 1
|
|
|
|
file_size = None
|
|
if conn.conn in (CONN_TCP, CONN_TLS):
|
|
try:
|
|
file_size = int(request.POST.get("file_size", ""))
|
|
except (ValueError, TypeError):
|
|
return JsonResponse({"error": _("Invalid file size.")}, status=400)
|
|
if file_size <= 0:
|
|
return JsonResponse({"error": _("Invalid file size.")}, status=400)
|
|
|
|
upload_id = request.POST.get("upload_id")
|
|
if not upload_id or not isinstance(upload_id, str):
|
|
return JsonResponse({"error": _("Missing upload_id.")}, status=400)
|
|
upload_id = upload_id.strip()
|
|
if not re.match(r"^[a-zA-Z0-9_\-]+$", upload_id) or len(upload_id) > 64:
|
|
return JsonResponse({"error": _("Invalid upload_id format.")}, status=400)
|
|
|
|
try:
|
|
handle_uploaded_file(
|
|
conn,
|
|
path,
|
|
file_name,
|
|
file_chunk,
|
|
is_last_chunk,
|
|
chunk_index=chunk_index,
|
|
total_chunks=total_chunks,
|
|
upload_id=upload_id,
|
|
user_id=request.user.id,
|
|
compute_id=compute.id,
|
|
pool=pool,
|
|
file_size=file_size,
|
|
)
|
|
|
|
if is_last_chunk:
|
|
success_msg = _("ISO: %(file)s has been uploaded successfully.") % {"file": file_name}
|
|
messages.success(request, success_msg)
|
|
return JsonResponse({"success": True, "message": success_msg, "reload": True})
|
|
else:
|
|
return JsonResponse({"success": True, "message": "Chunk received."})
|
|
except FileExistsError:
|
|
return JsonResponse({"error": _("ISO image already exists")}, status=400)
|
|
except TimeoutError:
|
|
return JsonResponse({"error": _("Upload session is busy. Please retry.")}, status=429)
|
|
except ValueError as e:
|
|
return JsonResponse({"error": str(e)}, status=400)
|
|
except Exception as e:
|
|
error_msg = str(e)
|
|
messages.error(request, error_msg)
|
|
return JsonResponse({"error": error_msg}, status=500)
|
|
|
|
|
|
@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, conn.get_target_path(), 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 TimeoutError:
|
|
messages.error(request, _("A VM operation on this host is running. Please retry."))
|
|
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():
|
|
data = form.cleaned_data
|
|
img_name = data["name"]
|
|
meta_prealloc = 0
|
|
if img_name in conn.update_volumes():
|
|
msg = _("Name of volume already in use")
|
|
messages.error(request, msg)
|
|
if data["convert"]:
|
|
format = data["format"]
|
|
if data["meta_prealloc"] and data["format"] == "qcow2":
|
|
meta_prealloc = True
|
|
else:
|
|
format = None
|
|
try:
|
|
name = conn.clone_volume(
|
|
data["image"], data["name"], format, meta_prealloc
|
|
)
|
|
messages.success(
|
|
request,
|
|
_("%(image)s image cloned as %(name)s successfully")
|
|
% {"image": data["image"], "name": name},
|
|
)
|
|
return HttpResponseRedirect(request.get_full_path())
|
|
except libvirtError as lib_err:
|
|
messages.error(request, lib_err)
|
|
else:
|
|
for msg_err in form.errors.values():
|
|
messages.error(request, msg_err.as_text())
|
|
|
|
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)
|