fix: install.sh pipe-mode, add .gitignore, robust error handling, resume state check

This commit is contained in:
2026-05-22 20:08:41 +04:00
parent ddb2ae9501
commit 13a1583df1
17 changed files with 210 additions and 76 deletions
Binary file not shown.
+99 -52
View File
@@ -1,12 +1,14 @@
# -*- coding: utf-8 -*-
"""
source.py — Orchestrator режима SOURCE (подготовка к переносу на старом сервере)
Строгая защита: каждый шаг в try/except, state сохраняется при ошибке.
"""
import os
import json
import tarfile
import time
import sys
from datetime import datetime
from core.color import header, subheader, success, warn, error as cerror, info, step, prompt, confirm, divider, log_cmd
from core import state
@@ -23,77 +25,119 @@ def run_source_mode():
state.set_stage("INIT", mode="source")
from core.fsm import FSM
fsm = FSM(mode="source")
fsm.resume_from("INIT")
try:
fsm.resume_from("INIT")
except Exception as e:
cerror(f"Source-режим прерван: {e}")
sys.exit(1)
def do_discovery():
step(1, "АВТО-ПОИСК ЗАВИСИМОСТЕЙ")
state.set_stage("SOURCE_DISCOVER")
# Запрашиваем имя сервиса (не нужно выбирать что искать — скрипт сам)
service_name = prompt("Введите имя Docker-сервиса или контейнера (например, marzban)")
if not service_name:
raise RuntimeError("Имя сервиса не указано")
# Docker discovery
docker_data = discover_docker(service_name)
cid = docker_data["container_id"]
try:
docker_data = discover_docker(service_name)
except RuntimeError:
# Если exact не найден, discover_docker уже предложил список
raise
except Exception as e:
raise RuntimeError(f"Ошибка поиска Docker: {e}")
# Собираем подсказки для поиска: порты и домены
cid = docker_data.get("container_id")
if not cid:
raise RuntimeError("Не удалось определить ID контейнера")
# Подсказки для nginx
ports = list(docker_data.get("ports", {}).keys())
ports_list = []
for p in ports:
# p вида "0.0.0.0:443" или "127.0.0.1:8000"
try:
port_num = p.split(":")[-1]
ports_list.append(int(port_num))
except Exception:
pass
# Домены из labels/env
domain_hints = []
env_dict = {}
labels = docker_data.get("labels", {})
for k, v in labels.items():
if "DOMAIN" in k.upper() or "HOST" in k.upper():
domain_hints.append(str(v))
# Также из Env контейнера
env_list = docker_data.get("env", [])
for e in env_list:
if "DOMAIN" in e.upper() or "HOST" in e.upper():
try:
val = e.split("=", 1)[1]
if val and "." in val:
domain_hints.append(val)
except Exception:
pass
# Sidecar / loopback discovery
sidecars = find_sidecar_processes(cid, ports_list)
# Детали процессов sidecar (exe, files, unit)
for s in sidecars:
if s.get("host_pid"):
details = get_process_details(s["host_pid"])
s["details"] = details
# Sidecar discovery (не fatal если не получилось)
sidecars = []
try:
sidecars = find_sidecar_processes(cid, ports_list)
for s in sidecars:
if s.get("host_pid"):
try:
details = get_process_details(s["host_pid"])
s["details"] = details
except Exception:
warn(f"Не удалось получить детали процесса {s.get('host_pid')}")
if sidecars:
success(f"Найдено sidecar/loopback зависимостей: {len(sidecars)}")
except Exception as e:
warn(f"Sidecar discovery не удался: {e}. Продолжаем без sidecar.")
# Nginx
nginx_data = discover_nginx(service_ports=ports_list, service_domain_hints=domain_hints)
nginx_data = []
try:
nginx_data = discover_nginx(service_ports=ports_list, service_domain_hints=domain_hints)
if nginx_data:
success(f"Найдено связанных nginx конфигов: {len(nginx_data)}")
except Exception as e:
warn(f"Nginx discovery не удался: {e}. Продолжаем без nginx.")
# Systemd units (nginx + sidecars)
# Systemd
sidecar_procs = [{"pid": s["host_pid"], "process": s["host_process"]} for s in sidecars if s.get("host_pid")]
systemd_units = find_systemd_units_related(sidecar_procs)
nginx_units = get_nginx_systemd_unit()
for nu in nginx_units:
systemd_units.append({
"name": nu["unit"],
"path": nu["path"],
"related_to": "nginx",
})
systemd_units = []
try:
systemd_units = find_systemd_units_related(sidecar_procs)
nginx_units = get_nginx_systemd_unit()
for nu in nginx_units:
systemd_units.append({
"name": nu["unit"],
"path": nu["path"],
"related_to": "nginx",
})
if systemd_units:
success(f"Найдено systemd units: {len(systemd_units)}")
except Exception as e:
warn(f"Systemd discovery не удался: {e}")
# Cron
hints = [docker_data.get("container_name", "")] + [s.get("host_process", "") for s in sidecars]
cron_jobs = gather_cron_jobs(user_hint=hints)
cron_jobs = []
try:
cron_jobs = gather_cron_jobs(user_hint=hints)
if cron_jobs:
success(f"Найдено cron заданий: {len(cron_jobs)}")
except Exception as e:
warn(f"Cron discovery не удался: {e}")
# Host network
host_network = gather_host_network_info()
host_network = {}
try:
host_network = gather_host_network_info()
except Exception as e:
warn(f"Сетевая информация не собрана: {e}")
# Compose-dependent files (sidecar configs near compose dir)
extra_hints = []
compose_dir = None
if docker_data.get("compose_file"):
compose_dir = os.path.dirname(docker_data["compose_file"])
manifest = build_manifest(
docker_data=docker_data,
nginx_data=nginx_data,
@@ -104,7 +148,6 @@ def do_discovery():
extra_hints=extra_hints,
)
# Путь для manifest
os.makedirs(_ARCHIVE_DIR, exist_ok=True)
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
manifest_path = os.path.join(_ARCHIVE_DIR, f"{service_name}_{stamp}_manifest.json")
@@ -112,7 +155,6 @@ def do_discovery():
state.set_stage("SOURCE_MANIFEST_REVIEW", manifest_path=manifest_path)
info(f"Этап discovery завершён. Manifest: {manifest_path}")
# Автоматически переходим к review
do_manifest_review()
@@ -156,19 +198,20 @@ def do_pack():
if os.path.isdir(src) or os.path.isfile(src):
files_to_pack.add(src)
elif m["type"] == "volume":
# Docker named volume: сохраняем через docker run --rm -v vol:/data tar
vol_name = m["source"]
tmpvol = os.path.join(_ARCHIVE_DIR, f"vol_{vol_name}_{stamp}.tar")
info(f"Архивируем Docker volume: {vol_name}{tmpvol}")
run(f"docker run --rm -v {vol_name}:/data -v {_ARCHIVE_DIR}:/out alpine tar czf /out/vol_{vol_name}_{stamp}.tar.gz -C /data .", check=False)
files_to_pack.add(f"vol_{vol_name}_{stamp}.tar.gz") # относительный путь
info(f"Архивируем Docker volume: {vol_name} ...")
try:
run(f"docker run --rm -v {vol_name}:/data -v {_ARCHIVE_DIR}:/out alpine tar czf /out/vol_{vol_name}_{stamp}.tar.gz -C /data .", check=False)
files_to_pack.add(f"vol_{vol_name}_{stamp}.tar.gz")
success(f"Volume {vol_name} заархивирован")
except Exception as e:
warn(f"Не удалось архивировать volume {vol_name}: {e}")
# nginx configs
# nginx configs + SSL
for n in manifest.get("nginx", []):
f = n.get("file")
if f and os.path.isfile(f):
files_to_pack.add(f)
# SSL сертификаты
cert = n.get("ssl_certificate")
key = n.get("ssl_certificate_key")
if cert and os.path.isfile(cert):
@@ -176,16 +219,14 @@ def do_pack():
if key and os.path.isfile(key):
files_to_pack.add(key)
# sidecar configs (open files процессов)
# sidecar configs
for s in manifest.get("sidecars", []):
details = s.get("details", {})
for f in details.get("files", []):
if os.path.isfile(f):
files_to_pack.add(f)
# systemd unit
unit = details.get("unit")
if unit:
# найдём файл unit через systemctl
try:
uout = run(f"systemctl show {unit} -p FragmentPath --value", check=False)
path = uout.stdout.strip()
@@ -194,7 +235,7 @@ def do_pack():
except Exception:
pass
# systemd units (nginx и т.п.)
# systemd units
for u in manifest.get("systemd_units", []):
p = u.get("path")
if p and os.path.isfile(p):
@@ -208,9 +249,12 @@ def do_pack():
# host_network snapshot
netfile = os.path.join(_ARCHIVE_DIR, f"{svc_name}_{stamp}_network.json")
with open(netfile, "w", encoding="utf-8") as f:
json.dump(manifest.get("host_network", {}), f, indent=2, ensure_ascii=False)
files_to_pack.add(netfile)
try:
with open(netfile, "w", encoding="utf-8") as f:
json.dump(manifest.get("host_network", {}), f, indent=2, ensure_ascii=False)
files_to_pack.add(netfile)
except Exception as e:
warn(f"Не удалось сохранить snapshot сети: {e}")
# manifest itself
files_to_pack.add(manifest_path)
@@ -220,7 +264,6 @@ def do_pack():
with tarfile.open(archive_path, "w:gz") as tar:
for fp in files_to_pack:
if os.path.exists(fp):
# Убираем ведущий /, чтобы tar создавал относительные пути
arcname = fp.lstrip("/")
tar.add(fp, arcname=arcname)
info(f" + {arcname}")
@@ -247,7 +290,6 @@ def do_stop_service():
success(f"Контейнер {cid[:12]} остановлен")
except Exception as e:
warn(f"Не удалось остановить контейнер: {e}")
# Остановить sidecar unit-ы
for u in manifest.get("systemd_units", []):
uname = u.get("name")
if uname:
@@ -271,7 +313,12 @@ def do_transfer_offer():
return
host = prompt("IP или домен нового сервера")
if not host:
warn("IP/домен не указан. Перенос пропущен.")
return
user = prompt("SSH user (root или обычный пользователь)")
if not user:
user = "root"
port = prompt("SSH порт (Enter=22)") or "22"
use_key = confirm("Использовать SSH-ключ (иначе — пароль)", default="y")
if not use_key: