"""Backup/restore for a single site: ZFS snapshot for files, mariadb-dump for DB. Two decoupled streams (see docs/06-backup-and-dr.md). Offsite `zfs send` / rsync are scheduled separately (Phase 5); this module covers the on-host operations the CLI drives. """ from __future__ import annotations from datetime import datetime from pathlib import Path from . import database, naming, zfs from .config import Config, Site from .runner import Runner def db_backup_dir(cfg: Config, site: Site) -> str: return f"/{cfg.pool}/platform/db-backups/{site.customer}/{site.site}" def dump_database(runner: Runner, cfg: Config, site: Site) -> str | None: if not site.database: return None db = naming.database_name(site.customer, site.site) ts = datetime.now().strftime("%Y%m%d-%H%M%S") out_dir = db_backup_dir(cfg, site) dest = f"{out_dir}/{db}-{ts}.sql" runner.mkdir(out_dir) pw = "" if runner.dry_run else database.root_password(cfg) sql = runner.run( ["docker", "exec", cfg.mariadb_container, "mariadb-dump", "--single-transaction", "--databases", db, "-uroot", f"-p{pw}"], capture=True, secret=True, ) if not runner.dry_run: runner.write_file(dest, sql) else: runner._echo(f"DRY write {dest}") return dest def backup_site(runner: Runner, cfg: Config, site: Site) -> dict: snap = zfs.snapshot(runner, zfs.web_dataset(cfg, site)) dump = dump_database(runner, cfg, site) return {"snapshot": snap, "dump": dump} def restore_files(runner: Runner, cfg: Config, site: Site, snapshot: str) -> None: ds = zfs.web_dataset(cfg, site) name = snapshot if "@" in snapshot else f"{ds}@{snapshot}" # -r discards snapshots newer than the target; acceptable for a deliberate restore. runner.run(["zfs", "rollback", "-r", name]) def restore_database(runner: Runner, cfg: Config, site: Site, dump_path: str) -> None: if not site.database: return pw = "" if runner.dry_run else database.root_password(cfg) data = "" if runner.dry_run else Path(dump_path).read_text(encoding="utf-8") runner.run( ["docker", "exec", "-i", cfg.mariadb_container, "mariadb", "-uroot", f"-p{pw}"], input=data, secret=True, )