Files
mirror/app/services/backup.py
T
mohammadian7 63d0699cdd Fix Docker apply crash and add safe profile test flow
Remove invalid _mirror_manager from daemon.json that prevented Docker from starting. Add 2-minute test apply with auto-rollback, final apply confirmation, emergency-restore CLI/UI, and post-restart Docker health checks.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 20:20:10 +03:30

200 lines
6.6 KiB
Python

from __future__ import annotations
import json
import logging
import shutil
from datetime import datetime
from pathlib import Path
from app.config import Config
from app.services.docker_svc import sanitize_daemon_json_file
from app.services.host import host_path, run_on_host
logger = logging.getLogger(__name__)
BACKUP_TARGETS = {
"resolved.conf": "etc/systemd/resolved.conf",
"resolved.conf.d": "etc/systemd/resolved.conf.d",
"sources.list": "etc/apt/sources.list",
"sources.list.d": "etc/apt/sources.list.d",
"daemon.json": "etc/docker/daemon.json",
"gitconfig_system": "etc/gitconfig",
}
def _backup_dir(name: str) -> Path:
path = Config.BACKUP_DIR / name
path.mkdir(parents=True, exist_ok=True)
return path
def _copy_if_exists(src: Path, dst: Path) -> None:
if not src.exists():
return
if src.is_dir():
if dst.exists():
shutil.rmtree(dst)
shutil.copytree(src, dst)
else:
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, dst)
def _restore_file(relative: str, backup_root: Path) -> None:
key = relative.replace("/", "_").replace(".", "_")
for name, rel in BACKUP_TARGETS.items():
if rel == relative or rel.endswith(relative):
key = name
break
src = backup_root / key
dst = host_path(relative)
if not src.exists():
if dst.exists() and key in ("daemon.json", "gitconfig_system"):
dst.unlink(missing_ok=True)
return
if src.is_dir():
if dst.exists():
shutil.rmtree(dst)
shutil.copytree(src, dst)
else:
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, dst)
def create_backup(label: str = "manual") -> Path:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_root = _backup_dir(f"{timestamp}_{label}")
manifest: dict = {"label": label, "timestamp": timestamp, "files": {}}
for key, relative in BACKUP_TARGETS.items():
src = host_path(relative)
dst = backup_root / key
_copy_if_exists(src, dst)
manifest["files"][key] = {
"relative": relative,
"existed": src.exists(),
}
gitconfig_global = host_path("root/.gitconfig")
if not gitconfig_global.exists():
gitconfig_global = Path.home() / ".gitconfig"
if gitconfig_global.exists():
shutil.copy2(gitconfig_global, backup_root / "gitconfig_global")
manifest["files"]["gitconfig_global"] = {"path": str(gitconfig_global), "existed": True}
pip_conf = host_path("etc/pip.conf")
if pip_conf.exists():
shutil.copy2(pip_conf, backup_root / "pip.conf")
manifest["files"]["pip.conf"] = {"relative": "etc/pip.conf", "existed": True}
npmrc = host_path("root/.npmrc")
if npmrc.exists():
shutil.copy2(npmrc, backup_root / "npmrc")
manifest["files"]["npmrc"] = {"relative": "root/.npmrc", "existed": True}
mirror_dropin = host_path("etc/systemd/resolved.conf.d/mirror-manager.conf")
if mirror_dropin.exists():
shutil.copy2(mirror_dropin, backup_root / "mirror_manager_resolved.conf")
with open(backup_root / "manifest.json", "w", encoding="utf-8") as f:
json.dump(manifest, f, ensure_ascii=False, indent=2)
logger.info("Backup created at %s", backup_root)
return backup_root
def restore_backup(backup_root: Path | str) -> dict:
backup_root = Path(backup_root)
if not backup_root.exists():
return {"success": False, "error": "مسیر backup یافت نشد"}
results: list[str] = []
for key, relative in BACKUP_TARGETS.items():
src = backup_root / key
if src.exists():
dst = host_path(relative)
_copy_if_exists(src, dst)
results.append(f"بازگردانی {relative}")
gitconfig_backup = backup_root / "gitconfig_global"
if gitconfig_backup.exists():
for target in [host_path("root/.gitconfig"), host_path("home/.gitconfig")]:
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(gitconfig_backup, target)
results.append("بازگردانی gitconfig global")
pip_backup = backup_root / "pip.conf"
if pip_backup.exists():
dst = host_path("etc/pip.conf")
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(pip_backup, dst)
results.append("بازگردانی pip.conf")
npmrc_backup = backup_root / "npmrc"
if npmrc_backup.exists():
dst = host_path("root/.npmrc")
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(npmrc_backup, dst)
results.append("بازگردانی .npmrc")
dropin = host_path("etc/systemd/resolved.conf.d/mirror-manager.conf")
mirror_backup = backup_root / "mirror_manager_resolved.conf"
if mirror_backup.exists():
dropin.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(mirror_backup, dropin)
elif dropin.exists():
dropin.unlink()
daemon_path = host_path("etc/docker/daemon.json")
if sanitize_daemon_json_file(daemon_path):
results.append("پاکسازی کلیدهای نامعتبر daemon.json")
run_on_host(["systemctl", "restart", "systemd-resolved"])
docker_result = run_on_host(["systemctl", "restart", "docker"], timeout=120)
if docker_result.returncode != 0:
return {
"success": False,
"error": (docker_result.stderr or "خطا در restart docker")[:500],
"restored": results,
}
return {"success": True, "restored": results}
def get_latest_apply_backup() -> Path | None:
"""Return the most recent before_apply backup directory, if any."""
if not Config.BACKUP_DIR.exists():
return None
candidates = sorted(
(
p
for p in Config.BACKUP_DIR.iterdir()
if p.is_dir() and "before_apply" in p.name
),
reverse=True,
)
return candidates[0] if candidates else None
def create_initial_backup() -> Path:
return create_backup("initial")
def list_backups() -> list[dict]:
backups = []
if not Config.BACKUP_DIR.exists():
return backups
for path in sorted(Config.BACKUP_DIR.iterdir(), reverse=True):
if path.is_dir():
manifest_path = path / "manifest.json"
label = path.name
if manifest_path.exists():
with open(manifest_path, encoding="utf-8") as f:
manifest = json.load(f)
label = manifest.get("label", path.name)
backups.append({"path": str(path), "name": path.name, "label": label})
return backups