fix: install.sh pipe-mode, add .gitignore, robust error handling, resume state check
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.egg-info/
|
||||||
|
.migrate-state.json
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+28
-2
@@ -97,14 +97,37 @@ def pretty_dict(data, indent=0):
|
|||||||
print(f"{prefix}{bold(k)}: {v}")
|
print(f"{prefix}{bold(k)}: {v}")
|
||||||
|
|
||||||
|
|
||||||
def prompt(text):
|
def prompt(text, default=None):
|
||||||
|
"""Интерактивный ввод с обработкой EOF/pipe"""
|
||||||
|
try:
|
||||||
return input(f"{yellow('❯')} {text} ").strip()
|
return input(f"{yellow('❯')} {text} ").strip()
|
||||||
|
except EOFError:
|
||||||
|
if default is not None:
|
||||||
|
print(f" (pipe detected, используем default={default})")
|
||||||
|
return default
|
||||||
|
print(f"{red('✗')} Невозможно читать ввод (stdin закрыт). Перезапустите вне pipe.")
|
||||||
|
sys.exit(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print(f"\n{yellow('⚠')} Прервано пользователем (Ctrl+C)")
|
||||||
|
sys.exit(130)
|
||||||
|
|
||||||
|
|
||||||
def confirm(text, default="y"):
|
def confirm(text, default="y"):
|
||||||
|
"""Да/нет с обработкой EOF. В pipe — возвращает default."""
|
||||||
yn = "Y/n" if default.lower() == "y" else "y/N"
|
yn = "Y/n" if default.lower() == "y" else "y/N"
|
||||||
while True:
|
while True:
|
||||||
|
try:
|
||||||
r = input(f"{yellow('❯')} {text} [{yn}] ").strip().lower()
|
r = input(f"{yellow('❯')} {text} [{yn}] ").strip().lower()
|
||||||
|
except EOFError:
|
||||||
|
if default.lower() == "y":
|
||||||
|
print(f" (pipe detected, используем default={default})")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f" (pipe detected, используем default={default})")
|
||||||
|
return False
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print(f"\n{yellow('⚠')} Прервано пользователем (Ctrl+C)")
|
||||||
|
sys.exit(130)
|
||||||
if not r:
|
if not r:
|
||||||
r = default
|
r = default
|
||||||
if r in ("y", "yes", "д", "да"):
|
if r in ("y", "yes", "д", "да"):
|
||||||
@@ -126,11 +149,14 @@ def banner():
|
|||||||
"""))
|
"""))
|
||||||
|
|
||||||
|
|
||||||
def menu():
|
def menu(has_resume=False):
|
||||||
print(f"\n{bold('Выберите режим:')}")
|
print(f"\n{bold('Выберите режим:')}")
|
||||||
print(f" {cyan('1')} {white('Подготовка к переносу (Source)')} — {gray('На текущем сервере')}")
|
print(f" {cyan('1')} {white('Подготовка к переносу (Source)')} — {gray('На текущем сервере')}")
|
||||||
print(f" {cyan('2')} {white('Восстановление (Target)')} — {gray('На новом сервере')}")
|
print(f" {cyan('2')} {white('Восстановление (Target)')} — {gray('На новом сервере')}")
|
||||||
|
if has_resume:
|
||||||
print(f" {cyan('3')} {white('Продолжить (Resume)')} — {gray('После исправления ошибки')}")
|
print(f" {cyan('3')} {white('Продолжить (Resume)')} — {gray('После исправления ошибки')}")
|
||||||
|
else:
|
||||||
|
print(f" {gray('3')} {gray('Продолжить (Resume)')} — {gray('(нет сохранённого состояния)')}")
|
||||||
print(f" {cyan('4')} {white('Статус и логи (Status/Logs)')} — {gray('Посмотреть состояние')}")
|
print(f" {cyan('4')} {white('Статус и логи (Status/Logs)')} — {gray('Посмотреть состояние')}")
|
||||||
print(f" {cyan('0')} {white('Выход')}")
|
print(f" {cyan('0')} {white('Выход')}")
|
||||||
print()
|
print()
|
||||||
|
|||||||
+36
-5
@@ -7,12 +7,11 @@ import sys
|
|||||||
import os
|
import os
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
# Добавляем корень проекта в PATH
|
|
||||||
_PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
_PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
if _PROJECT_ROOT not in sys.path:
|
if _PROJECT_ROOT not in sys.path:
|
||||||
sys.path.insert(0, _PROJECT_ROOT)
|
sys.path.insert(0, _PROJECT_ROOT)
|
||||||
|
|
||||||
from core.color import banner, menu, prompt, info, error, warn
|
from core.color import banner, menu, prompt, info, error as cerror, warn
|
||||||
from core import state
|
from core import state
|
||||||
|
|
||||||
|
|
||||||
@@ -55,16 +54,42 @@ def main():
|
|||||||
# Интерактивный режим
|
# Интерактивный режим
|
||||||
banner()
|
banner()
|
||||||
while True:
|
while True:
|
||||||
menu()
|
st = state.load_state()
|
||||||
choice = prompt("Ваш выбор").strip()
|
has_resume = bool(st.get("mode") and st.get("stage") and st.get("stage") != "INIT")
|
||||||
|
menu(has_resume=has_resume)
|
||||||
|
try:
|
||||||
|
choice = prompt("Ваш выбор", default="0").strip()
|
||||||
|
except SystemExit:
|
||||||
|
break
|
||||||
if choice == "1":
|
if choice == "1":
|
||||||
from source.source import run_source_mode
|
from source.source import run_source_mode
|
||||||
|
try:
|
||||||
run_source_mode()
|
run_source_mode()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
warn("Прервано")
|
||||||
|
except Exception as e:
|
||||||
|
cerror(f"Ошибка: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
break
|
||||||
elif choice == "2":
|
elif choice == "2":
|
||||||
from target.target import run_target_mode
|
from target.target import run_target_mode
|
||||||
|
try:
|
||||||
run_target_mode()
|
run_target_mode()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
warn("Прервано")
|
||||||
|
except Exception as e:
|
||||||
|
cerror(f"Ошибка: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
break
|
||||||
elif choice == "3":
|
elif choice == "3":
|
||||||
|
if not has_resume:
|
||||||
|
warn("Нет сохранённого состояния для продолжения")
|
||||||
|
continue
|
||||||
|
try:
|
||||||
_do_resume()
|
_do_resume()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
warn("Прервано")
|
||||||
|
break
|
||||||
elif choice == "4":
|
elif choice == "4":
|
||||||
_do_status_logs()
|
_do_status_logs()
|
||||||
elif choice == "0":
|
elif choice == "0":
|
||||||
@@ -83,7 +108,10 @@ def _do_resume():
|
|||||||
info("Нет сохранённого состояния для продолжения")
|
info("Нет сохранённого состояния для продолжения")
|
||||||
return
|
return
|
||||||
fsm = FSM(mode=mode)
|
fsm = FSM(mode=mode)
|
||||||
|
try:
|
||||||
fsm.resume_from(stage)
|
fsm.resume_from(stage)
|
||||||
|
except Exception as e:
|
||||||
|
cerror(f"Ошибка при resume: {e}")
|
||||||
|
|
||||||
|
|
||||||
def _do_status_logs():
|
def _do_status_logs():
|
||||||
@@ -93,7 +121,10 @@ def _do_status_logs():
|
|||||||
print(" 1 Показать статус")
|
print(" 1 Показать статус")
|
||||||
print(" 2 Показать лог последней ошибки")
|
print(" 2 Показать лог последней ошибки")
|
||||||
print(" 0 Назад")
|
print(" 0 Назад")
|
||||||
c = prompt("Выбор").strip()
|
try:
|
||||||
|
c = prompt("Выбор", default="0").strip()
|
||||||
|
except SystemExit:
|
||||||
|
break
|
||||||
if c == "1":
|
if c == "1":
|
||||||
stmod.show_status()
|
stmod.show_status()
|
||||||
elif c == "2":
|
elif c == "2":
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
+27
-5
@@ -1,13 +1,21 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# install.sh — Docker Migrate Tool Installer
|
# install.sh — Docker Migrate Tool Installer
|
||||||
# Запуск: wget -qO- https://your-gitea/raw/main/install.sh | bash
|
# Запуск: wget -qO- https://giteas.stitch505.su/Stitch505/docker-migrate/raw/main/install.sh | bash
|
||||||
# curl -fsSL https://your-gitea/raw/main/install.sh | bash
|
# Потом запускать: docker-migrate
|
||||||
|
#
|
||||||
|
# Чтобы сразу запустить после установки (не через pipe):
|
||||||
|
# curl -fsSL https://.../install.sh -o install.sh && bash install.sh --run
|
||||||
|
#
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
REPO_URL="https://giteas.stitch505.su/Stitch505/docker-migrate"
|
REPO_URL="https://giteas.stitch505.su/Stitch505/docker-migrate"
|
||||||
BRANCH="main"
|
BRANCH="main"
|
||||||
INSTALL_DIR="/opt/docker-migrate"
|
INSTALL_DIR="/opt/docker-migrate"
|
||||||
|
RUN_IMMEDIATELY=false
|
||||||
|
|
||||||
|
if [ "$1" = "--run" ]; then
|
||||||
|
RUN_IMMEDIATELY=true
|
||||||
|
fi
|
||||||
|
|
||||||
log() { echo "[migrate-install] $*"; }
|
log() { echo "[migrate-install] $*"; }
|
||||||
|
|
||||||
@@ -43,7 +51,7 @@ fi
|
|||||||
|
|
||||||
# Установка
|
# Установка
|
||||||
if [ -d "$INSTALL_DIR" ]; then
|
if [ -d "$INSTALL_DIR" ]; then
|
||||||
log "Директория $INSTALL_DIR уже существует. Удаляем ..."
|
log "Директория $INSTALL_DIR уже существует. Перезаписываем ..."
|
||||||
rm -rf "$INSTALL_DIR"
|
rm -rf "$INSTALL_DIR"
|
||||||
fi
|
fi
|
||||||
mkdir -p "$INSTALL_DIR"
|
mkdir -p "$INSTALL_DIR"
|
||||||
@@ -57,7 +65,6 @@ case "$METHOD" in
|
|||||||
log "Скачиваем через curl ..."
|
log "Скачиваем через curl ..."
|
||||||
tmpzip=$(mktemp /tmp/migrate-XXXXX.zip)
|
tmpzip=$(mktemp /tmp/migrate-XXXXX.zip)
|
||||||
curl -fsSL "${REPO_URL}/archive/refs/heads/${BRANCH}.zip" -o "$tmpzip" || {
|
curl -fsSL "${REPO_URL}/archive/refs/heads/${BRANCH}.zip" -o "$tmpzip" || {
|
||||||
# Fallback для gitea raw zip
|
|
||||||
curl -fsSL "${REPO_URL}/archive/${BRANCH}.zip" -o "$tmpzip"
|
curl -fsSL "${REPO_URL}/archive/${BRANCH}.zip" -o "$tmpzip"
|
||||||
}
|
}
|
||||||
python3 -c "import zipfile; zipfile.ZipFile('$tmpzip').extractall('/tmp')"
|
python3 -c "import zipfile; zipfile.ZipFile('$tmpzip').extractall('/tmp')"
|
||||||
@@ -81,6 +88,21 @@ log "Установлено в $INSTALL_DIR"
|
|||||||
# Создаём symlink в /usr/local/bin
|
# Создаём symlink в /usr/local/bin
|
||||||
ln -sf "$INSTALL_DIR/migrate" /usr/local/bin/docker-migrate 2>/dev/null || true
|
ln -sf "$INSTALL_DIR/migrate" /usr/local/bin/docker-migrate 2>/dev/null || true
|
||||||
|
|
||||||
|
if [ "$RUN_IMMEDIATELY" = true ]; then
|
||||||
log "Запускаем docker-migrate ..."
|
log "Запускаем docker-migrate ..."
|
||||||
cd "$INSTALL_DIR"
|
cd "$INSTALL_DIR"
|
||||||
exec python3 migrate "$@"
|
exec python3 migrate "$@"
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Установка завершена!"
|
||||||
|
echo ""
|
||||||
|
echo "Запустите интерактивный режим:"
|
||||||
|
echo " docker-migrate"
|
||||||
|
echo ""
|
||||||
|
echo "Или напрямую:"
|
||||||
|
echo " docker-migrate --mode=source # подготовка к переносу"
|
||||||
|
echo " docker-migrate --mode=target # восстановление на новом сервере"
|
||||||
|
echo " docker-migrate --resume # продолжить после ошибки"
|
||||||
|
echo "=========================================="
|
||||||
|
fi
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
+77
-30
@@ -1,12 +1,14 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
source.py — Orchestrator режима SOURCE (подготовка к переносу на старом сервере)
|
source.py — Orchestrator режима SOURCE (подготовка к переносу на старом сервере)
|
||||||
|
Строгая защита: каждый шаг в try/except, state сохраняется при ошибке.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import tarfile
|
import tarfile
|
||||||
import time
|
import time
|
||||||
|
import sys
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from core.color import header, subheader, success, warn, error as cerror, info, step, prompt, confirm, divider, log_cmd
|
from core.color import header, subheader, success, warn, error as cerror, info, step, prompt, confirm, divider, log_cmd
|
||||||
from core import state
|
from core import state
|
||||||
@@ -23,55 +25,88 @@ def run_source_mode():
|
|||||||
state.set_stage("INIT", mode="source")
|
state.set_stage("INIT", mode="source")
|
||||||
from core.fsm import FSM
|
from core.fsm import FSM
|
||||||
fsm = FSM(mode="source")
|
fsm = FSM(mode="source")
|
||||||
|
try:
|
||||||
fsm.resume_from("INIT")
|
fsm.resume_from("INIT")
|
||||||
|
except Exception as e:
|
||||||
|
cerror(f"Source-режим прерван: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def do_discovery():
|
def do_discovery():
|
||||||
step(1, "АВТО-ПОИСК ЗАВИСИМОСТЕЙ")
|
step(1, "АВТО-ПОИСК ЗАВИСИМОСТЕЙ")
|
||||||
state.set_stage("SOURCE_DISCOVER")
|
state.set_stage("SOURCE_DISCOVER")
|
||||||
|
|
||||||
# Запрашиваем имя сервиса (не нужно выбирать что искать — скрипт сам)
|
|
||||||
service_name = prompt("Введите имя Docker-сервиса или контейнера (например, marzban)")
|
service_name = prompt("Введите имя Docker-сервиса или контейнера (например, marzban)")
|
||||||
if not service_name:
|
if not service_name:
|
||||||
raise RuntimeError("Имя сервиса не указано")
|
raise RuntimeError("Имя сервиса не указано")
|
||||||
|
|
||||||
# Docker discovery
|
try:
|
||||||
docker_data = discover_docker(service_name)
|
docker_data = discover_docker(service_name)
|
||||||
cid = docker_data["container_id"]
|
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(docker_data.get("ports", {}).keys())
|
||||||
ports_list = []
|
ports_list = []
|
||||||
for p in ports:
|
for p in ports:
|
||||||
# p вида "0.0.0.0:443" или "127.0.0.1:8000"
|
|
||||||
try:
|
try:
|
||||||
port_num = p.split(":")[-1]
|
port_num = p.split(":")[-1]
|
||||||
ports_list.append(int(port_num))
|
ports_list.append(int(port_num))
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Домены из labels/env
|
|
||||||
domain_hints = []
|
domain_hints = []
|
||||||
env_dict = {}
|
|
||||||
labels = docker_data.get("labels", {})
|
labels = docker_data.get("labels", {})
|
||||||
for k, v in labels.items():
|
for k, v in labels.items():
|
||||||
if "DOMAIN" in k.upper() or "HOST" in k.upper():
|
if "DOMAIN" in k.upper() or "HOST" in k.upper():
|
||||||
domain_hints.append(str(v))
|
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
|
# Sidecar discovery (не fatal если не получилось)
|
||||||
|
sidecars = []
|
||||||
|
try:
|
||||||
sidecars = find_sidecar_processes(cid, ports_list)
|
sidecars = find_sidecar_processes(cid, ports_list)
|
||||||
|
|
||||||
# Детали процессов sidecar (exe, files, unit)
|
|
||||||
for s in sidecars:
|
for s in sidecars:
|
||||||
if s.get("host_pid"):
|
if s.get("host_pid"):
|
||||||
|
try:
|
||||||
details = get_process_details(s["host_pid"])
|
details = get_process_details(s["host_pid"])
|
||||||
s["details"] = details
|
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
|
||||||
|
nginx_data = []
|
||||||
|
try:
|
||||||
nginx_data = discover_nginx(service_ports=ports_list, service_domain_hints=domain_hints)
|
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")]
|
sidecar_procs = [{"pid": s["host_pid"], "process": s["host_process"]} for s in sidecars if s.get("host_pid")]
|
||||||
|
systemd_units = []
|
||||||
|
try:
|
||||||
systemd_units = find_systemd_units_related(sidecar_procs)
|
systemd_units = find_systemd_units_related(sidecar_procs)
|
||||||
nginx_units = get_nginx_systemd_unit()
|
nginx_units = get_nginx_systemd_unit()
|
||||||
for nu in nginx_units:
|
for nu in nginx_units:
|
||||||
@@ -80,20 +115,29 @@ def do_discovery():
|
|||||||
"path": nu["path"],
|
"path": nu["path"],
|
||||||
"related_to": "nginx",
|
"related_to": "nginx",
|
||||||
})
|
})
|
||||||
|
if systemd_units:
|
||||||
|
success(f"Найдено systemd units: {len(systemd_units)}")
|
||||||
|
except Exception as e:
|
||||||
|
warn(f"Systemd discovery не удался: {e}")
|
||||||
|
|
||||||
# Cron
|
# Cron
|
||||||
hints = [docker_data.get("container_name", "")] + [s.get("host_process", "") for s in sidecars]
|
hints = [docker_data.get("container_name", "")] + [s.get("host_process", "") for s in sidecars]
|
||||||
|
cron_jobs = []
|
||||||
|
try:
|
||||||
cron_jobs = gather_cron_jobs(user_hint=hints)
|
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
|
||||||
|
host_network = {}
|
||||||
|
try:
|
||||||
host_network = gather_host_network_info()
|
host_network = gather_host_network_info()
|
||||||
|
except Exception as e:
|
||||||
|
warn(f"Сетевая информация не собрана: {e}")
|
||||||
|
|
||||||
# Compose-dependent files (sidecar configs near compose dir)
|
|
||||||
extra_hints = []
|
extra_hints = []
|
||||||
compose_dir = None
|
|
||||||
if docker_data.get("compose_file"):
|
|
||||||
compose_dir = os.path.dirname(docker_data["compose_file"])
|
|
||||||
|
|
||||||
manifest = build_manifest(
|
manifest = build_manifest(
|
||||||
docker_data=docker_data,
|
docker_data=docker_data,
|
||||||
nginx_data=nginx_data,
|
nginx_data=nginx_data,
|
||||||
@@ -104,7 +148,6 @@ def do_discovery():
|
|||||||
extra_hints=extra_hints,
|
extra_hints=extra_hints,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Путь для manifest
|
|
||||||
os.makedirs(_ARCHIVE_DIR, exist_ok=True)
|
os.makedirs(_ARCHIVE_DIR, exist_ok=True)
|
||||||
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
manifest_path = os.path.join(_ARCHIVE_DIR, f"{service_name}_{stamp}_manifest.json")
|
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)
|
state.set_stage("SOURCE_MANIFEST_REVIEW", manifest_path=manifest_path)
|
||||||
info(f"Этап discovery завершён. Manifest: {manifest_path}")
|
info(f"Этап discovery завершён. Manifest: {manifest_path}")
|
||||||
|
|
||||||
# Автоматически переходим к review
|
|
||||||
do_manifest_review()
|
do_manifest_review()
|
||||||
|
|
||||||
|
|
||||||
@@ -156,19 +198,20 @@ def do_pack():
|
|||||||
if os.path.isdir(src) or os.path.isfile(src):
|
if os.path.isdir(src) or os.path.isfile(src):
|
||||||
files_to_pack.add(src)
|
files_to_pack.add(src)
|
||||||
elif m["type"] == "volume":
|
elif m["type"] == "volume":
|
||||||
# Docker named volume: сохраняем через docker run --rm -v vol:/data tar
|
|
||||||
vol_name = m["source"]
|
vol_name = m["source"]
|
||||||
tmpvol = os.path.join(_ARCHIVE_DIR, f"vol_{vol_name}_{stamp}.tar")
|
info(f"Архивируем Docker volume: {vol_name} ...")
|
||||||
info(f"Архивируем Docker volume: {vol_name} → {tmpvol}")
|
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)
|
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") # относительный путь
|
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", []):
|
for n in manifest.get("nginx", []):
|
||||||
f = n.get("file")
|
f = n.get("file")
|
||||||
if f and os.path.isfile(f):
|
if f and os.path.isfile(f):
|
||||||
files_to_pack.add(f)
|
files_to_pack.add(f)
|
||||||
# SSL сертификаты
|
|
||||||
cert = n.get("ssl_certificate")
|
cert = n.get("ssl_certificate")
|
||||||
key = n.get("ssl_certificate_key")
|
key = n.get("ssl_certificate_key")
|
||||||
if cert and os.path.isfile(cert):
|
if cert and os.path.isfile(cert):
|
||||||
@@ -176,16 +219,14 @@ def do_pack():
|
|||||||
if key and os.path.isfile(key):
|
if key and os.path.isfile(key):
|
||||||
files_to_pack.add(key)
|
files_to_pack.add(key)
|
||||||
|
|
||||||
# sidecar configs (open files процессов)
|
# sidecar configs
|
||||||
for s in manifest.get("sidecars", []):
|
for s in manifest.get("sidecars", []):
|
||||||
details = s.get("details", {})
|
details = s.get("details", {})
|
||||||
for f in details.get("files", []):
|
for f in details.get("files", []):
|
||||||
if os.path.isfile(f):
|
if os.path.isfile(f):
|
||||||
files_to_pack.add(f)
|
files_to_pack.add(f)
|
||||||
# systemd unit
|
|
||||||
unit = details.get("unit")
|
unit = details.get("unit")
|
||||||
if unit:
|
if unit:
|
||||||
# найдём файл unit через systemctl
|
|
||||||
try:
|
try:
|
||||||
uout = run(f"systemctl show {unit} -p FragmentPath --value", check=False)
|
uout = run(f"systemctl show {unit} -p FragmentPath --value", check=False)
|
||||||
path = uout.stdout.strip()
|
path = uout.stdout.strip()
|
||||||
@@ -194,7 +235,7 @@ def do_pack():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# systemd units (nginx и т.п.)
|
# systemd units
|
||||||
for u in manifest.get("systemd_units", []):
|
for u in manifest.get("systemd_units", []):
|
||||||
p = u.get("path")
|
p = u.get("path")
|
||||||
if p and os.path.isfile(p):
|
if p and os.path.isfile(p):
|
||||||
@@ -208,9 +249,12 @@ def do_pack():
|
|||||||
|
|
||||||
# host_network snapshot
|
# host_network snapshot
|
||||||
netfile = os.path.join(_ARCHIVE_DIR, f"{svc_name}_{stamp}_network.json")
|
netfile = os.path.join(_ARCHIVE_DIR, f"{svc_name}_{stamp}_network.json")
|
||||||
|
try:
|
||||||
with open(netfile, "w", encoding="utf-8") as f:
|
with open(netfile, "w", encoding="utf-8") as f:
|
||||||
json.dump(manifest.get("host_network", {}), f, indent=2, ensure_ascii=False)
|
json.dump(manifest.get("host_network", {}), f, indent=2, ensure_ascii=False)
|
||||||
files_to_pack.add(netfile)
|
files_to_pack.add(netfile)
|
||||||
|
except Exception as e:
|
||||||
|
warn(f"Не удалось сохранить snapshot сети: {e}")
|
||||||
|
|
||||||
# manifest itself
|
# manifest itself
|
||||||
files_to_pack.add(manifest_path)
|
files_to_pack.add(manifest_path)
|
||||||
@@ -220,7 +264,6 @@ def do_pack():
|
|||||||
with tarfile.open(archive_path, "w:gz") as tar:
|
with tarfile.open(archive_path, "w:gz") as tar:
|
||||||
for fp in files_to_pack:
|
for fp in files_to_pack:
|
||||||
if os.path.exists(fp):
|
if os.path.exists(fp):
|
||||||
# Убираем ведущий /, чтобы tar создавал относительные пути
|
|
||||||
arcname = fp.lstrip("/")
|
arcname = fp.lstrip("/")
|
||||||
tar.add(fp, arcname=arcname)
|
tar.add(fp, arcname=arcname)
|
||||||
info(f" + {arcname}")
|
info(f" + {arcname}")
|
||||||
@@ -247,7 +290,6 @@ def do_stop_service():
|
|||||||
success(f"Контейнер {cid[:12]} остановлен")
|
success(f"Контейнер {cid[:12]} остановлен")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
warn(f"Не удалось остановить контейнер: {e}")
|
warn(f"Не удалось остановить контейнер: {e}")
|
||||||
# Остановить sidecar unit-ы
|
|
||||||
for u in manifest.get("systemd_units", []):
|
for u in manifest.get("systemd_units", []):
|
||||||
uname = u.get("name")
|
uname = u.get("name")
|
||||||
if uname:
|
if uname:
|
||||||
@@ -271,7 +313,12 @@ def do_transfer_offer():
|
|||||||
return
|
return
|
||||||
|
|
||||||
host = prompt("IP или домен нового сервера")
|
host = prompt("IP или домен нового сервера")
|
||||||
|
if not host:
|
||||||
|
warn("IP/домен не указан. Перенос пропущен.")
|
||||||
|
return
|
||||||
user = prompt("SSH user (root или обычный пользователь)")
|
user = prompt("SSH user (root или обычный пользователь)")
|
||||||
|
if not user:
|
||||||
|
user = "root"
|
||||||
port = prompt("SSH порт (Enter=22)") or "22"
|
port = prompt("SSH порт (Enter=22)") or "22"
|
||||||
use_key = confirm("Использовать SSH-ключ (иначе — пароль)", default="y")
|
use_key = confirm("Использовать SSH-ключ (иначе — пароль)", default="y")
|
||||||
if not use_key:
|
if not use_key:
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user