63d0699cdd
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>
107 lines
3.5 KiB
Python
107 lines
3.5 KiB
Python
from __future__ import annotations
|
|
|
|
|
|
|
|
import json
|
|
|
|
import logging
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
from app.models import Mirror
|
|
|
|
from app.services.host import host_path, run_on_host
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
DAEMON_JSON = "etc/docker/daemon.json"
|
|
|
|
INVALID_DAEMON_KEYS = ("_mirror_manager",)
|
|
|
|
|
|
|
|
|
|
|
|
def sanitize_daemon_config(config: dict) -> dict:
|
|
|
|
"""Remove keys that Docker daemon does not accept."""
|
|
|
|
for key in INVALID_DAEMON_KEYS:
|
|
|
|
config.pop(key, None)
|
|
|
|
return config
|
|
|
|
|
|
|
|
|
|
|
|
def sanitize_daemon_json_file(path: Path | None = None) -> bool:
|
|
|
|
"""Strip invalid keys from daemon.json on disk. Returns True if file was modified."""
|
|
|
|
path = path or host_path(DAEMON_JSON)
|
|
|
|
if not path.exists():
|
|
|
|
return False
|
|
|
|
try:
|
|
|
|
config = json.loads(path.read_text(encoding="utf-8"))
|
|
|
|
except json.JSONDecodeError:
|
|
|
|
return False
|
|
|
|
cleaned = sanitize_daemon_config(dict(config))
|
|
|
|
if cleaned == config:
|
|
|
|
return False
|
|
|
|
path.write_text(json.dumps(cleaned, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def verify_docker_running() -> dict:
|
|
|
|
active = run_on_host(["systemctl", "is-active", "docker"], timeout=30)
|
|
|
|
if active.returncode != 0:
|
|
|
|
return {
|
|
|
|
"success": False,
|
|
|
|
"error": "Docker بعد از restart بالا نیامد — تنظیمات daemon.json را بررسی کنید",
|
|
|
|
}
|
|
|
|
info = run_on_host(["docker", "info"], timeout=30)
|
|
|
|
if info.returncode != 0:
|
|
|
|
return {
|
|
|
|
"success": False,
|
|
|
|
"error": (info.stderr or info.stdout or "docker info ناموفق")[:500],
|
|
|
|
}
|
|
|
|
return {"success": True}
|
|
|
|
|
|
|
|
|
|
|
|
def read_current_docker() -> dict:
|
|
|
|
path = host_path(DAEMON_JSON)
|
|
|
|
if not path.exists():
|
|
|
|
return {"exists": False, "config": {}}
|
|
|
|
try:
|
|
|
|
config = json.loads(path.read_text(encoding="utf-8"))
|
|
|
|
has_invalid = any(key in config for key in INVALID_DAEMON_KEYS)
|
|
|
|
return {
|
|
|
|
"exists": True,
|
|
|
|
"config": config,
|
|
|
|
"has_invalid_keys": has_invalid,
|
|
|
|
}
|
|
|
|
except json.JSONDecodeError:
|
|
|
|
return {"exists": True, "config": {}, "parse_error": True}
|
|
|
|
|
|
|
|
|
|
|
|
def apply_docker(mirror: Mirror) -> dict:
|
|
|
|
if not mirror.url:
|
|
|
|
return {"success": False, "error": "URL رجیstry داکر تعریف نشده"}
|
|
|
|
|
|
|
|
path = host_path(DAEMON_JSON)
|
|
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
|
|
config: dict = {}
|
|
|
|
if path.exists():
|
|
|
|
try:
|
|
|
|
config = json.loads(path.read_text(encoding="utf-8"))
|
|
|
|
except json.JSONDecodeError:
|
|
|
|
config = {}
|
|
|
|
|
|
|
|
config = sanitize_daemon_config(config)
|
|
|
|
config["registry-mirrors"] = [mirror.url.rstrip("/")]
|
|
|
|
|
|
|
|
path.write_text(json.dumps(config, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
|
|
|
|
|
|
result = run_on_host(["systemctl", "restart", "docker"], timeout=120)
|
|
|
|
if result.returncode != 0:
|
|
|
|
return {
|
|
|
|
"success": False,
|
|
|
|
"error": (result.stderr or result.stdout or "خطا در restart docker")[:500],
|
|
|
|
}
|
|
|
|
|
|
|
|
health = verify_docker_running()
|
|
|
|
if not health.get("success"):
|
|
|
|
return health
|
|
|
|
|
|
|
|
return {
|
|
|
|
"success": True,
|
|
|
|
"message": f"Docker registry mirror اعمال شد: {mirror.name}",
|
|
|
|
"url": mirror.url,
|
|
|
|
"warning": "سرویس Docker restart شد — containerها موقتاً قطع میشوند",
|
|
|
|
}
|
|
|
|
|