1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-10 12:16:49 +00:00
webvirtcloud/storages/jobs.py
catborise cad392f79c fix(jobs): VM create and volume clone check the user and the name again in the worker
Every other job re-checks its permission and state in precheck before any
effect; these two checked only when queued. A queued create of a user who
is no longer a superuser, or whose VM name a host took meanwhile, ran; so
did a volume clone whose name a volume made outside the panel took.
2026-11-05 16:16:47 +03:00

147 lines
6.2 KiB
Python

"""Jobs of the storages app (operations framework)."""
import contextlib
import logging
import os
from django.core.exceptions import PermissionDenied
from django.utils.translation import gettext_noop
from libvirt import libvirtError
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 _log(op, state, action):
compute, _sep, pool = op.resource_label.rpartition("/") # a pool name has no "/"
message = f"Storage pool {pool}: {action}"
return compute, "", "", message if state == Operation.SUCCEEDED else f"{message} ({state})"
def is_iso(name):
"""An ISO image, by its name: libvirt calls its format raw. It is uploaded, not cloned."""
return name.lower().endswith(".iso")
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 precheck(self, op, ctx):
if op.user is None or not op.user.is_active or not op.user.is_superuser:
raise PermissionDenied
# a volume made outside the panel since the job was queued may have taken the name
if op.intent["target"] in _storage(op.params)[1].get_volumes():
raise util.OperationError(gettext_noop("Name of volume already in use"))
def plan(self, user, params):
compute, stg = _storage(params)
volumes = stg.get_volumes()
if params["image"] not in volumes:
raise util.OperationError(gettext_noop("The pool has no volume named %(name)s"), name=params["image"])
if is_iso(params["image"]):
raise util.OperationError(gettext_noop("An ISO image is not cloned"))
target = stg.clone_name(params["name"], params["format"] or stg.get_volume_format_type(params["image"]))
if target in volumes:
raise util.OperationError(gettext_noop("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=utils.say(gettext_noop(
"The copy exists, but the job did not see it finish; check it, then resolve this job.")))
return Outcome(Operation.FAILED, error=utils.say(gettext_noop("The copy was not made")))
def log_entry(self, op, state):
return _log(op, state, f"clone volume {op.params['image']} as {op.intent['target']}")
@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 is_iso(name):
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)
with contextlib.suppress(libvirtError): # refused while a volume is cloned in the pool
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, utils.say(upload.IDLE))
def log_entry(self, op, state):
return _log(op, state, f"upload volume {op.params['name']}")