""" 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}