125 lines
3.8 KiB
Python
125 lines
3.8 KiB
Python
"""
|
|
Storage router — unified entry point for workspace preparation,
|
|
btrfs CAS, tmpfs acceleration, and qcow2 freeze.
|
|
|
|
Used by:
|
|
- BuildRunner (to prepare per-build workspace)
|
|
- /api/storage/* endpoints (status, config, freeze)
|
|
"""
|
|
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Any, Dict, Optional
|
|
|
|
from backend.storage.config import load_storage_config, save_storage_config
|
|
from backend.storage.tmpfs import get_tmpfs_workspace
|
|
from backend.storage.btrfs_cas import (
|
|
cas_path, cas_store, cas_exists, cas_retrieve, is_btrfs, reflink_snapshot,
|
|
)
|
|
from backend.storage.qcow2_freeze import (
|
|
freeze_to_qcow2, qcow2_info, list_snapshots,
|
|
)
|
|
|
|
|
|
def prepare_workspace(project_name: str) -> Dict[str, Any]:
|
|
"""Prepare a workspace for a build. Always returns a tmpfs path,
|
|
optionally also a btrfs CAS path."""
|
|
cfg = load_storage_config()
|
|
|
|
workspace = {
|
|
"tmpfs": None,
|
|
"cas": None,
|
|
"freeze": None,
|
|
"config": cfg,
|
|
}
|
|
|
|
# Always: tmpfs workspace
|
|
try:
|
|
workspace["tmpfs"] = get_tmpfs_workspace(cfg, project_name)
|
|
except Exception:
|
|
workspace["tmpfs"] = None
|
|
|
|
# Optional: btrfs CAS path
|
|
if cfg.get("btrfs"):
|
|
try:
|
|
workspace["cas"] = cas_path(cfg, project_name)
|
|
except Exception:
|
|
workspace["cas"] = None
|
|
|
|
return workspace
|
|
|
|
|
|
def maybe_freeze_workspace(workdir, project_name) -> Optional[str]:
|
|
"""If qcow2_freeze is enabled, snapshot the workdir into a qcow2 image.
|
|
Returns the path to the .qcow2 file, or None if freezing is disabled
|
|
or failed."""
|
|
cfg = load_storage_config()
|
|
|
|
if not cfg.get("qcow2_freeze"):
|
|
return None
|
|
|
|
output_dir = Path(cfg.get("qcow2_path", "/var/lib/fester/snapshots"))
|
|
output = output_dir / f"{project_name}-{int(time.time())}.qcow2"
|
|
|
|
return freeze_to_qcow2(workdir, str(output))
|
|
|
|
|
|
def storage_status() -> Dict[str, Any]:
|
|
"""Return a snapshot of the current storage configuration + capabilities."""
|
|
cfg = load_storage_config()
|
|
|
|
# Check which capabilities are actually available
|
|
import shutil as _sh
|
|
have_qemu_img = _sh.which("qemu-img") is not None
|
|
have_qemu_nbd = _sh.which("qemu-nbd") is not None
|
|
have_rsync = _sh.which("rsync") is not None
|
|
|
|
# Check if tmpfs path is on tmpfs
|
|
tmpfs_path = Path(cfg.get("tmpfs_path", "/dev/shm/fester-build"))
|
|
on_tmpfs = False
|
|
if tmpfs_path.exists():
|
|
try:
|
|
import subprocess
|
|
out = subprocess.check_output(
|
|
["stat", "-f", "-c", "%T", str(tmpfs_path)],
|
|
stderr=subprocess.DEVNULL,
|
|
).decode().strip()
|
|
on_tmpfs = out == "tmpfs"
|
|
except Exception:
|
|
pass
|
|
|
|
# Check if btrfs path is on btrfs
|
|
btrfs_path = Path(cfg.get("btrfs_path", "/var/lib/fester/cas"))
|
|
on_btrfs = is_btrfs(btrfs_path) if btrfs_path.exists() else False
|
|
|
|
return {
|
|
"config": cfg,
|
|
"capabilities": {
|
|
"tmpfs": {
|
|
"enabled": cfg.get("tmpfs", False),
|
|
"path": str(tmpfs_path),
|
|
"on_tmpfs": on_tmpfs,
|
|
},
|
|
"btrfs_cas": {
|
|
"enabled": cfg.get("btrfs", False),
|
|
"path": str(btrfs_path),
|
|
"on_btrfs": on_btrfs,
|
|
},
|
|
"qcow2_freeze": {
|
|
"enabled": cfg.get("qcow2_freeze", False),
|
|
"path": cfg.get("qcow2_path", "/var/lib/fester/snapshots"),
|
|
"qemu_img_available": have_qemu_img,
|
|
"qemu_nbd_available": have_qemu_nbd,
|
|
"rsync_available": have_rsync,
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
def update_storage_config(updates: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Merge updates into the storage config and persist."""
|
|
cfg = load_storage_config()
|
|
cfg.update(updates)
|
|
saved = save_storage_config(cfg)
|
|
return {"saved": saved, "config": cfg}
|