feat: add ledger backup safety workflow
confirm dcy-002 and add REQ/BAT evidence add ledger backup/status metadata workflow document restore as runbook/script-only guarded operation add synthetic DB backup/restore smoke tests
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Restore/smoke helper for Jarca Ledger backups.
|
||||
|
||||
This script is intentionally not exposed as an MCP tool. It restores a backup file
|
||||
into an explicitly provided target DB path, then runs SQLite integrity and Jarca
|
||||
ledger health/status checks. Use a synthetic fixture or repo-external validation
|
||||
DB first. Do not point --target at the live operating DB unless the user has
|
||||
explicitly approved the restore.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from jarca_ledger_mcp import service
|
||||
|
||||
APPROVAL_TEXT = "RESTORE_APPROVED"
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Restore a ledger backup into a target DB and run smoke checks.")
|
||||
parser.add_argument("--backup", required=True, help="Backup sqlite3 file to restore from")
|
||||
parser.add_argument("--target", required=True, help="Target sqlite3 path to create/replace")
|
||||
parser.add_argument(
|
||||
"--approval-token",
|
||||
default="",
|
||||
help=f"Required token for overwrite/live restore. Must be {APPROVAL_TEXT!r}.",
|
||||
)
|
||||
parser.add_argument("--allow-overwrite", action="store_true", help="Allow replacing an existing target DB")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
backup = Path(args.backup).expanduser().resolve()
|
||||
target = Path(args.target).expanduser().resolve()
|
||||
if not backup.exists():
|
||||
print(json.dumps({"success": False, "error_code": "not_found", "error": f"backup not found: {backup}"}, ensure_ascii=False))
|
||||
return 1
|
||||
if target.exists() and not args.allow_overwrite:
|
||||
print(json.dumps({"success": False, "error_code": "target_exists", "error": f"target exists: {target}"}, ensure_ascii=False))
|
||||
return 2
|
||||
if target.exists() and args.approval_token != APPROVAL_TEXT:
|
||||
print(json.dumps({"success": False, "error_code": "approval_required", "error": f"overwrite requires --approval-token {APPROVAL_TEXT}"}, ensure_ascii=False))
|
||||
return 3
|
||||
|
||||
before = service.get_ledger_backup_status(str(backup))
|
||||
if not before.get("success") or before.get("backup", {}).get("integrity_check") != "ok":
|
||||
print(json.dumps({"success": False, "error_code": "invalid_backup", "backup_status": before}, ensure_ascii=False))
|
||||
return 4
|
||||
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(backup, target)
|
||||
health = service.health_check(str(target))
|
||||
status = service.get_ledger_db_status(str(target))
|
||||
result = {
|
||||
"success": bool(health.get("success") and status.get("success") and status.get("integrity_check") == "ok"),
|
||||
"backup": before.get("backup", {}),
|
||||
"target": status,
|
||||
"health_check": health,
|
||||
"restore_policy": service.BACKUP_RESTORE_POLICY,
|
||||
}
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
return 0 if result["success"] else 5
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user