mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-09 11:46:49 +00:00
A job stored its error and its result's warning as English text, much of it built with the values in it (volume paths, a disk, libvirt's error), so no catalog could translate it, and the job pages and panel showed English whatever the language. A job now stores them as messages: our English text, marked for translation, and its values; libvirt's own text is a message without values and is shown as it is. The jobs API and pages put them in the reader's language. OperationError keeps its text and values apart for the same reason, so a refusal raised while a job is submitted is shown translated too. Messages one after another still read as sentences. Errors stored before are kept as one message each, shown as they were.
140 lines
5.8 KiB
Python
140 lines
5.8 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 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']}")
|