fix: install.sh pipe-mode, add .gitignore, robust error handling, resume state check
This commit is contained in:
Binary file not shown.
+99
-52
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user