mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-09 19:56:48 +00:00
The upload session lives in a job instead of flocks and .meta/.done files; the job panel shows and cancels it; an upload idle for an hour is ended and its partial file deleted; the page no longer reloads after an upload.
118 lines
4.9 KiB
Python
118 lines
4.9 KiB
Python
"""Jobs of the storages app (operations framework)."""
|
|
|
|
import logging
|
|
import os
|
|
|
|
from django.core.exceptions import PermissionDenied
|
|
from django.utils.translation import gettext_noop
|
|
from vrtManager import util
|
|
from vrtManager.storage import wvmStorage
|
|
|
|
from computes.models import Compute
|
|
from operations import claims, utils
|
|
from operations.jobs import Kind, Outcome, Plan, register
|
|
from operations.models import Claim, Operation
|
|
|
|
from storages import upload
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _storage(params):
|
|
c = Compute.objects.get(pk=params["compute"])
|
|
return c, wvmStorage(c.hostname, c.login, c.password, c.type, params["pool"])
|
|
|
|
|
|
@register
|
|
class CloneVolume(Kind):
|
|
name = "volume.clone"
|
|
label = gettext_noop("Clone volume")
|
|
time_limit = 12 * 3600 # a copy cannot be stopped: over time is only noted
|
|
|
|
def normalize(self, raw):
|
|
fmt = str(raw.get("format") or "") if raw.get("convert") else ""
|
|
return {"compute": int(raw["compute"]), "pool": str(raw["pool"]), "image": str(raw["image"]),
|
|
"name": str(raw["name"]), "format": fmt, "prealloc": fmt == "qcow2" and bool(raw.get("meta_prealloc"))}
|
|
|
|
def validate(self, user, params):
|
|
if not user.is_superuser:
|
|
raise PermissionDenied
|
|
|
|
def plan(self, user, params):
|
|
compute, stg = _storage(params)
|
|
volumes = stg.get_volumes()
|
|
if params["image"] not in volumes:
|
|
raise util.OperationError(f"The pool has no volume named {params['image']}")
|
|
target = stg.clone_name(params["name"], params["format"] or stg.get_volume_format_type(params["image"]))
|
|
if target in volumes:
|
|
raise util.OperationError("Name of volume already in use")
|
|
pool = claims.pool_key(compute.pk, params["pool"])
|
|
return Plan(claims={claims.compute_key(compute.pk): Claim.SHARED, pool: Claim.SHARED,
|
|
claims.volume_key(compute.pk, params["pool"], target): Claim.EXCLUSIVE},
|
|
resource=pool, label=f"{compute.name}/{params['pool']}", intent={"target": target})
|
|
|
|
def run(self, op, ctx):
|
|
_compute, stg = _storage(op.params)
|
|
ctx.record_intent()
|
|
ctx.set_phase(gettext_noop("Copying the volume"))
|
|
created = stg.clone_volume(op.params["image"], op.params["name"], op.params["format"] or None,
|
|
op.params["prealloc"])
|
|
ctx.record_intent(created=created)
|
|
return {}
|
|
|
|
def reconcile(self, op):
|
|
if op.intent.get("created"):
|
|
return Outcome(Operation.SUCCEEDED)
|
|
_compute, stg = _storage(op.params) # unreachable: recovery waits and retries
|
|
if op.intent["target"] in stg.get_volumes():
|
|
return Outcome(Operation.UNCERTAIN, error="The copy exists, but the job did not see it finish; "
|
|
"check it, then resolve this job.")
|
|
return Outcome(Operation.FAILED, error="The copy was not made")
|
|
|
|
|
|
@register
|
|
class UploadVolume(Kind):
|
|
"""An ISO upload: its chunk requests run it (storages.upload)."""
|
|
|
|
name = "volume.upload"
|
|
label = gettext_noop("Upload ISO image")
|
|
cancellable = True
|
|
progress_bytes = True
|
|
runs_in_request = True
|
|
|
|
def normalize(self, raw):
|
|
return {"compute": int(raw["compute"]), "pool": str(raw["pool"]),
|
|
"name": os.path.basename(str(raw["name"])).strip(), "size": int(raw["size"]),
|
|
"chunks": int(raw["chunks"])}
|
|
|
|
def validate(self, user, params):
|
|
if not user.is_superuser:
|
|
raise PermissionDenied
|
|
name = params["name"]
|
|
if not name or name in (".", "..") or "\x00" in name:
|
|
raise ValueError("Invalid file name")
|
|
if len(name) <= 4 or not name.lower().endswith(".iso"):
|
|
raise ValueError("Only .iso files can be uploaded")
|
|
if params["size"] <= 0:
|
|
raise ValueError("Invalid file size")
|
|
if params["chunks"] < 1:
|
|
raise ValueError("Invalid chunk index or total chunks")
|
|
|
|
def plan(self, user, params):
|
|
compute, stg = _storage(params)
|
|
stg.refresh()
|
|
if params["name"] in stg.get_volumes():
|
|
raise FileExistsError(f"The pool has a volume named {params['name']}")
|
|
pool = claims.pool_key(compute.pk, params["pool"])
|
|
return Plan(claims={claims.compute_key(compute.pk): Claim.SHARED, pool: Claim.SHARED,
|
|
claims.volume_key(compute.pk, params["pool"], params["name"]): Claim.EXCLUSIVE},
|
|
resource=pool, label=f"{compute.name}/{params['pool']}", intent={"chunk": 0, "created": False})
|
|
|
|
def cancel(self, op):
|
|
try:
|
|
upload.end(op, Operation.CANCELLED)
|
|
except Exception as err: # the next chunk or the expiry ends it
|
|
logger.error("cancelling upload %s failed: %s", op.pk, utils.safe_text(err))
|
|
|
|
def expire(self, op):
|
|
upload.end(op, Operation.FAILED, upload.IDLE)
|