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>
This commit is contained in:
+72
-9
@@ -2,11 +2,10 @@ 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 ApplyLog, Profile, Setting, 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
|
||||
@@ -22,6 +21,9 @@ APPLIERS = {
|
||||
"npm": pip_npm.apply_npm,
|
||||
}
|
||||
|
||||
APPLY_MODE_TEST = "test"
|
||||
APPLY_MODE_FINAL = "final"
|
||||
|
||||
|
||||
def ensure_initial_backup() -> SystemState:
|
||||
state = SystemState.query.first()
|
||||
@@ -40,9 +42,22 @@ def ensure_initial_backup() -> SystemState:
|
||||
return state
|
||||
|
||||
|
||||
def apply_profile(profile: Profile, rollback_minutes: int | None = None) -> ApplyLog:
|
||||
def _rollback_minutes_for_mode(mode: str, rollback_minutes: int | None) -> int | None:
|
||||
if mode == APPLY_MODE_FINAL:
|
||||
return None
|
||||
if rollback_minutes is not None:
|
||||
return rollback_minutes
|
||||
default = Setting.get("test_rollback_minutes", str(Config.TEST_ROLLBACK_MINUTES))
|
||||
return int(default or Config.TEST_ROLLBACK_MINUTES)
|
||||
|
||||
|
||||
def apply_profile(
|
||||
profile: Profile,
|
||||
mode: str = APPLY_MODE_TEST,
|
||||
rollback_minutes: int | None = None,
|
||||
) -> ApplyLog:
|
||||
state = ensure_initial_backup()
|
||||
rollback_minutes = rollback_minutes or int(Config.ROLLBACK_MINUTES)
|
||||
rollback_minutes = _rollback_minutes_for_mode(mode, rollback_minutes)
|
||||
|
||||
previous_backup = backup.create_backup(f"before_apply_{profile.id}")
|
||||
apply_log = ApplyLog(
|
||||
@@ -50,12 +65,17 @@ def apply_profile(profile: Profile, rollback_minutes: int | None = None) -> Appl
|
||||
status="running",
|
||||
backup_path=str(previous_backup),
|
||||
previous_backup_path=state.initial_backup_path,
|
||||
rollback_at=utcnow() + timedelta(minutes=rollback_minutes),
|
||||
rollback_at=utcnow() + timedelta(minutes=rollback_minutes) if rollback_minutes else None,
|
||||
)
|
||||
db.session.add(apply_log)
|
||||
db.session.flush()
|
||||
|
||||
details: dict = {"steps": [], "profile": profile.name}
|
||||
details: dict = {
|
||||
"steps": [],
|
||||
"profile": profile.name,
|
||||
"mode": mode,
|
||||
"rollback_minutes": rollback_minutes,
|
||||
}
|
||||
mirrors = profile.mirrors_by_category()
|
||||
order = ["dns", "apt", "docker", "github", "pip", "npm"]
|
||||
|
||||
@@ -69,11 +89,14 @@ def apply_profile(profile: Profile, rollback_minutes: int | None = None) -> Appl
|
||||
try:
|
||||
result = applier(mirror)
|
||||
details["steps"].append({"category": category, "mirror": mirror.name, **result})
|
||||
if not result.get("success", True):
|
||||
break
|
||||
except Exception as exc:
|
||||
logger.exception("Apply failed for %s", category)
|
||||
details["steps"].append(
|
||||
{"category": category, "mirror": mirror.name, "success": False, "error": str(exc)}
|
||||
)
|
||||
break
|
||||
|
||||
failed = [s for s in details["steps"] if not s.get("success", True)]
|
||||
if failed and apply_log.backup_path:
|
||||
@@ -81,11 +104,22 @@ def apply_profile(profile: Profile, rollback_minutes: int | None = None) -> Appl
|
||||
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
|
||||
if failed:
|
||||
apply_log.status = "failed"
|
||||
apply_log.rollback_at = None
|
||||
elif mode == APPLY_MODE_FINAL:
|
||||
apply_log.confirmed = True
|
||||
apply_log.status = "confirmed"
|
||||
apply_log.rollback_at = None
|
||||
state.current_profile_id = profile.id
|
||||
state.pending_apply_log_id = None
|
||||
else:
|
||||
apply_log.status = "pending_confirm"
|
||||
state.current_profile_id = profile.id
|
||||
state.pending_apply_log_id = apply_log.id
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return apply_log
|
||||
@@ -97,9 +131,15 @@ def confirm_apply(apply_log_id: int) -> ApplyLog:
|
||||
apply_log.status = "confirmed"
|
||||
apply_log.rollback_at = None
|
||||
|
||||
details = apply_log.get_details()
|
||||
details["confirmed_from"] = APPLY_MODE_TEST
|
||||
apply_log.set_details(details)
|
||||
|
||||
state = SystemState.query.first()
|
||||
if state:
|
||||
state.pending_apply_log_id = None
|
||||
if apply_log.profile_id:
|
||||
state.current_profile_id = apply_log.profile_id
|
||||
db.session.commit()
|
||||
return apply_log
|
||||
|
||||
@@ -112,6 +152,7 @@ def rollback_apply(apply_log_id: int) -> dict:
|
||||
result = backup.restore_backup(apply_log.backup_path)
|
||||
apply_log.status = "rolled_back"
|
||||
apply_log.finished_at = utcnow()
|
||||
apply_log.rollback_at = None
|
||||
|
||||
state = SystemState.query.first()
|
||||
if state:
|
||||
@@ -120,6 +161,28 @@ def rollback_apply(apply_log_id: int) -> dict:
|
||||
return result
|
||||
|
||||
|
||||
def emergency_restore_last_apply() -> dict:
|
||||
"""One-shot restore of the most recent pre-apply backup."""
|
||||
path = backup.get_latest_apply_backup()
|
||||
if not path:
|
||||
return {"success": False, "error": "backup قبل از apply یافت نشد"}
|
||||
result = backup.restore_backup(path)
|
||||
if result.get("success"):
|
||||
result["backup_path"] = str(path)
|
||||
|
||||
state = SystemState.query.first()
|
||||
pending_id = state.pending_apply_log_id if state else None
|
||||
if pending_id:
|
||||
log = ApplyLog.query.get(pending_id)
|
||||
if log and log.status == "pending_confirm":
|
||||
log.status = "rolled_back"
|
||||
log.finished_at = utcnow()
|
||||
log.rollback_at = None
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user