from __future__ import annotations import logging from datetime import timedelta from pathlib import Path from app.config import Config from app.extensions import db from app.models import ApplyLog, Profile, SystemState from app.models import utcnow from app.services import apt, backup, dns, docker_svc, github, pip_npm from app.services.host import run_on_host logger = logging.getLogger(__name__) APPLIERS = { "dns": dns.apply_dns, "apt": apt.apply_apt, "docker": docker_svc.apply_docker, "github": github.apply_github, "pip": pip_npm.apply_pip, "npm": pip_npm.apply_npm, } def ensure_initial_backup() -> SystemState: state = SystemState.query.first() if not state: state = SystemState() db.session.add(state) db.session.commit() if not state.initial_backup_done: path = backup.create_initial_backup() state.initial_backup_path = str(path) state.initial_backup_done = True db.session.commit() logger.info("Initial backup created at %s", path) return state def apply_profile(profile: Profile, rollback_minutes: int | None = None) -> ApplyLog: state = ensure_initial_backup() rollback_minutes = rollback_minutes or int(Config.ROLLBACK_MINUTES) previous_backup = backup.create_backup(f"before_apply_{profile.id}") apply_log = ApplyLog( profile_id=profile.id, status="running", backup_path=str(previous_backup), previous_backup_path=state.initial_backup_path, rollback_at=utcnow() + timedelta(minutes=rollback_minutes), ) db.session.add(apply_log) db.session.flush() details: dict = {"steps": [], "profile": profile.name} mirrors = profile.mirrors_by_category() order = ["dns", "apt", "docker", "github", "pip", "npm"] for category in order: mirror = mirrors.get(category) if not mirror: continue applier = APPLIERS.get(category) if not applier: continue try: result = applier(mirror) details["steps"].append({"category": category, "mirror": mirror.name, **result}) except Exception as exc: logger.exception("Apply failed for %s", category) details["steps"].append( {"category": category, "mirror": mirror.name, "success": False, "error": str(exc)} ) failed = [s for s in details["steps"] if not s.get("success", True)] if failed and apply_log.backup_path: rollback_result = backup.restore_backup(apply_log.backup_path) details["auto_rollback"] = rollback_result apply_log.set_details(details) apply_log.status = "failed" if failed else "pending_confirm" apply_log.finished_at = utcnow() state.current_profile_id = profile.id if not failed else state.current_profile_id state.pending_apply_log_id = apply_log.id if not failed else None db.session.commit() return apply_log def confirm_apply(apply_log_id: int) -> ApplyLog: apply_log = ApplyLog.query.get_or_404(apply_log_id) apply_log.confirmed = True apply_log.status = "confirmed" apply_log.rollback_at = None state = SystemState.query.first() if state: state.pending_apply_log_id = None db.session.commit() return apply_log def rollback_apply(apply_log_id: int) -> dict: apply_log = ApplyLog.query.get_or_404(apply_log_id) if not apply_log.backup_path: return {"success": False, "error": "مسیر backup یافت نشد"} result = backup.restore_backup(apply_log.backup_path) apply_log.status = "rolled_back" apply_log.finished_at = utcnow() state = SystemState.query.first() if state: state.pending_apply_log_id = None db.session.commit() return result def restore_initial() -> dict: state = SystemState.query.first() if not state or not state.initial_backup_path: return {"success": False, "error": "backup اولیه یافت نشد"} return backup.restore_backup(state.initial_backup_path) def get_system_status() -> dict: codename_result = run_on_host(["lsb_release", "-cs"]) version_result = run_on_host(["lsb_release", "-d"]) return { "ubuntu_codename": codename_result.stdout.strip() if codename_result.returncode == 0 else "unknown", "ubuntu_description": version_result.stdout.strip() if version_result.returncode == 0 else "unknown", "dns": dns.read_current_dns(), "apt": apt.read_current_apt(), "docker": docker_svc.read_current_docker(), "github": github.read_current_github(), "host_root": Config.HOST_ROOT or "(local mode)", }