145 lines
4.1 KiB
Python
145 lines
4.1 KiB
Python
"""Daily configuration backup to Gitea — executed after CEO approval."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import subprocess
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from app.db import execute, fetch_all, fetch_one
|
|
|
|
BACKUP_ROOT = Path(os.getenv("BACKUP_ROOT", "/data/backup-root"))
|
|
GITEA_REMOTE = os.getenv(
|
|
"GITEA_BACKUP_REMOTE",
|
|
"http://sysops:Foodlinkk%23SysOps2026@gitea:3001/aissa/foodlinkk-command-center.git",
|
|
)
|
|
GITEA_GIT_USER = os.getenv("GITEA_SYSOPS_USER", "sysops")
|
|
GITEA_GIT_EMAIL = os.getenv("GITEA_SYSOPS_EMAIL", "sysops@foodlinkk.local")
|
|
GITEA_GIT_NAME = os.getenv("GITEA_SYSOPS_NAME", "sysops")
|
|
GITEA_BRANCH = os.getenv("GITEA_BACKUP_BRANCH", "main")
|
|
|
|
BACKUP_PATHS = [
|
|
"docker-compose.yml",
|
|
".env.example",
|
|
"migrations",
|
|
"monitoring",
|
|
"cockpit/requirements.txt",
|
|
"tools-api/requirements.txt",
|
|
"deploy-all.sh",
|
|
"README.md",
|
|
]
|
|
|
|
|
|
def _iso_now() -> str:
|
|
return datetime.now(timezone.utc).isoformat()
|
|
|
|
|
|
def _run(cmd: list[str], cwd: Path | None = None, timeout: int = 120) -> dict[str, Any]:
|
|
try:
|
|
proc = subprocess.run( # noqa: S603
|
|
cmd,
|
|
cwd=str(cwd) if cwd else None,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=timeout,
|
|
check=False,
|
|
)
|
|
except subprocess.TimeoutExpired:
|
|
return {"ok": False, "error": "timeout", "cmd": cmd}
|
|
except FileNotFoundError as exc:
|
|
return {"ok": False, "error": str(exc), "cmd": cmd}
|
|
return {
|
|
"ok": proc.returncode == 0,
|
|
"code": proc.returncode,
|
|
"stdout": (proc.stdout or "").strip(),
|
|
"stderr": (proc.stderr or "").strip(),
|
|
"cmd": cmd,
|
|
}
|
|
|
|
|
|
def _table_exists(name: str) -> bool:
|
|
row = fetch_one(
|
|
"""
|
|
SELECT EXISTS(
|
|
SELECT 1 FROM information_schema.tables
|
|
WHERE table_schema = 'public' AND table_name = %s
|
|
) AS ok
|
|
""",
|
|
(name,),
|
|
)
|
|
return bool(row and row.get("ok"))
|
|
|
|
|
|
def _record_backup(
|
|
status: str,
|
|
message: str,
|
|
files_count: int = 0,
|
|
commit_ref: str | None = None,
|
|
approval_request_id: int | None = None,
|
|
payload: dict | None = None,
|
|
) -> dict[str, Any] | None:
|
|
if not _table_exists("config_backups"):
|
|
return None
|
|
row = fetch_one(
|
|
"""
|
|
INSERT INTO config_backups (approval_request_id, commit_ref, files_count, status, message, payload)
|
|
VALUES (%s, %s, %s, %s, %s, %s::jsonb)
|
|
RETURNING id, created_at
|
|
""",
|
|
(
|
|
approval_request_id,
|
|
commit_ref,
|
|
files_count,
|
|
status,
|
|
message,
|
|
json.dumps(payload or {}),
|
|
),
|
|
)
|
|
return dict(row) if row else None
|
|
|
|
|
|
def list_backups(limit: int = 20) -> list[dict[str, Any]]:
|
|
if not _table_exists("config_backups"):
|
|
return []
|
|
rows = fetch_all(
|
|
"SELECT * FROM config_backups ORDER BY created_at DESC LIMIT %s",
|
|
(max(1, min(limit, 100)),),
|
|
)
|
|
out = []
|
|
for r in rows:
|
|
item = dict(r)
|
|
if hasattr(item.get("created_at"), "isoformat"):
|
|
item["created_at"] = item["created_at"].isoformat()
|
|
out.append(item)
|
|
return out
|
|
|
|
|
|
def prepare_backup_manifest() -> dict[str, Any]:
|
|
root = BACKUP_ROOT
|
|
files: list[str] = []
|
|
if root.exists():
|
|
for rel in BACKUP_PATHS:
|
|
p = root / rel
|
|
if p.is_file():
|
|
files.append(rel)
|
|
elif p.is_dir():
|
|
for fp in p.rglob("*"):
|
|
if fp.is_file() and not any(x in str(fp) for x in [".git", "__pycache__", ".pyc"]):
|
|
files.append(str(fp.relative_to(root)))
|
|
return {
|
|
"root": str(root),
|
|
"root_exists": root.exists(),
|
|
"files": sorted(files)[:500],
|
|
"files_count": len(files),
|
|
"paths_configured": BACKUP_PATHS,
|
|
"generated_at": _iso_now(),
|
|
}
|
|
|
|
|
|
def run_gitea_backup(approval_request_id: int | None = None) -> dict[str, Any]:
|
|
from app.connectors.gitea_repo_sync import run_gitea_repo_sync
|
|
|
|
return run_gitea_repo_sync(reason="approved_backup", approval_request_id=approval_request_id)
|