fester/backend/storage/config.py

93 lines
2.4 KiB
Python

"""
Storage configuration loader.
Reads from /etc/fester/storage.json by default, or from
FESTER_STORAGE_CONFIG env var. Falls back to sensible defaults
if the file doesn't exist or isn't readable.
"""
import json
import os
from pathlib import Path
DEFAULT_CONFIG_PATH = Path(
os.environ.get("FESTER_STORAGE_CONFIG", "/etc/fester/storage.json")
)
DEFAULT_CONFIG = {
"tmpfs": True,
"btrfs": False,
"qcow2_freeze": False,
"btrfs_path": "/var/lib/fester/cas",
"tmpfs_path": "/dev/shm/fester-build",
"qcow2_path": "/var/lib/fester/snapshots",
}
def _writable_path(p: Path) -> bool:
"""Check if a path is writable (or can be created)."""
try:
p.mkdir(parents=True, exist_ok=True)
# touch a test file
test = p / ".fester_write_test"
test.write_text("")
test.unlink()
return True
except (OSError, PermissionError):
return False
def _resolve_paths(cfg: dict) -> dict:
"""If a configured path isn't writable, fall back to user/tmp paths."""
fallbacks = {
"btrfs_path": [
Path.home() / ".fester" / "cas",
Path("/tmp/fester/cas"),
],
"tmpfs_path": [
Path.home() / ".fester" / "tmpfs",
Path("/tmp/fester/tmpfs"),
],
"qcow2_path": [
Path.home() / ".fester" / "snapshots",
Path("/tmp/fester/snapshots"),
],
}
for key, alts in fallbacks.items():
p = Path(cfg[key])
if not _writable_path(p):
for alt in alts:
if _writable_path(alt):
cfg[key] = str(alt)
break
return cfg
def load_storage_config() -> dict:
"""Load storage config with graceful fallbacks."""
cfg = DEFAULT_CONFIG.copy()
if DEFAULT_CONFIG_PATH.exists():
try:
with open(DEFAULT_CONFIG_PATH) as f:
user_cfg = json.load(f)
cfg.update(user_cfg)
except (json.JSONDecodeError, OSError):
pass
cfg = _resolve_paths(cfg)
return cfg
def save_storage_config(cfg: dict) -> bool:
"""Persist config to disk. Returns True on success."""
try:
DEFAULT_CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
with open(DEFAULT_CONFIG_PATH, "w") as f:
json.dump(cfg, f, indent=2)
return True
except (OSError, PermissionError):
return False