Compare commits

...

13 Commits

Author SHA1 Message Date
Stitch505
79b91f157e fix: recursive search for manifest.json after tar extraction (files may be nested) 2026-05-23 00:20:36 +04:00
Stitch505
c8bef6b41a fix: target-mode now runs with capture=False (interactive), so user sees prompts and can respond 2026-05-23 00:16:54 +04:00
Stitch505
80bb196d24 fix: transfer.py — helper functions for ssh/scp/rsync, local tar then remote unpack for tool script, consistent error handling 2026-05-23 00:11:04 +04:00
Stitch505
bfd6302448 fix: restore missing ensure_sshpass() function in ssh.py 2026-05-23 00:06:34 +04:00
Stitch505
21bd747329 refactor: consistent terminology - 'новый сервер' and 'приватный SSH-ключ' instead of 'target' and generic 'ключ' 2026-05-23 00:02:53 +04:00
Stitch505
0cd968a16d fix: target-first SSH flow — ask user for THEIR target key/password, don't scan source keys 2026-05-22 23:59:37 +04:00
Stitch505
3ee5104302 refactor: clear SSH auth flow — passphrase is local only, target password is separate; interactive ssh-copy-id with visible prompt; 90 lines removed, logic simplified 2026-05-22 23:53:15 +04:00
Stitch505
bd16e339c0 fix: ssh-copy-id interactive (capture=False) so password prompt visible; encrypted keys just warned, not source-server passphrase confusion; clear auth choice menu for target 2026-05-22 23:48:58 +04:00
Stitch505
6d6418c059 feat: secure password/passphrase input via getpass; interactive SSH auth menu (key/passphrase/password/temp-key); fix missing do_transfer call 2026-05-22 23:42:52 +04:00
Stitch505
4d6c7ef506 feat: interactive SSH auth choice (key with passphrase, password via sshpass, or temp ed25519); fix missing do_transfer call for password path 2026-05-22 23:41:09 +04:00
Stitch505
bfa3136131 fix: target mode supports manual tar.gz path input + auto-extract when archive not transferred yet 2026-05-22 23:35:32 +04:00
Stitch505
7d45d1f8c0 fix: full sshpass support for password-based transfer; pre-flight SSH check; clear error diagnostics 2026-05-22 23:28:27 +04:00
Stitch505
4a1e7923de fix: clear stale ssh_key from state when user declines keygen; add guard for missing key in do_transfer 2026-05-22 23:23:02 +04:00
5 changed files with 231 additions and 208 deletions

View File

@@ -112,6 +112,19 @@ def prompt(text, default=None):
sys.exit(130)
def prompt_password(text):
"""Скрытый ввод пароля/passphrase. В pipe — обычный prompt с предупреждением."""
import getpass
try:
return getpass.getpass(f"{yellow('')} {text} ")
except EOFError:
print(f"{yellow('')} Ввод пароля в pipe не поддерживается скрытым режимом. Используем обычный ввод.")
return prompt(text)
except KeyboardInterrupt:
print(f"\n{yellow('')} Прервано пользователем (Ctrl+C)")
sys.exit(130)
def confirm(text, default="y"):
"""Да/нет с обработкой EOF. В pipe — возвращает default."""
yn = "Y/n" if default.lower() == "y" else "y/N"

View File

@@ -10,7 +10,7 @@ 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.color import header, subheader, success, warn, error as cerror, info, step, prompt, prompt_password, confirm, divider, log_cmd
from core import state
from core.runner import run
from discover.docker import discover_docker, get_container_pid
@@ -342,7 +342,7 @@ def do_transfer_offer():
host, user, port_int = resume_host, resume_user or "root", int(resume_port)
else:
# Сбрасываем, чтобы запросить заново
state.set_stage("SOURCE_STOP", target_host=None, target_user=None, target_port=None)
state.set_stage("SOURCE_STOP", target_host=None, target_user=None, target_port=None, ssh_key=None)
host = user = None
else:
host = user = None
@@ -379,7 +379,16 @@ def do_transfer_offer():
if key_path:
state.set_stage("TRANSFER", target_host=host, target_user=user, target_port=port_int, ssh_key=key_path)
else:
state.set_stage("TRANSFER", target_host=host, target_user=user, target_port=port_int)
# Нет SSH-ключа — запрашиваем пароль от НОВОГО сервера
password = prompt_password(f"Пароль от нового сервера {user}@{host} (Enter для отмены)")
if not password:
state.set_error("ssh_key_setup", "", "Пароль не введён", suggestion="Запустите docker-migrate --resume или настройте приватный SSH-ключ на новом сервере")
raise RuntimeError("Пароль от нового сервера не введён.")
from transfer.ssh import ensure_sshpass
if not ensure_sshpass():
state.set_error("ssh_key_setup", "", "sshpass не найден", suggestion="apt-get install -y sshpass")
raise RuntimeError("sshpass не установлен.")
state.set_stage("TRANSFER", target_host=host, target_user=user, target_port=port_int, ssh_key=None, ssh_password=password)
from transfer.transfer import do_transfer
do_transfer()

View File

@@ -145,14 +145,34 @@ def do_restore():
# Определяем директорию с архивом
remote_dir = st.get("target_remote_dir", "/tmp/docker-migrate-incoming")
if not os.path.isdir(remote_dir):
remote_dir = prompt("Укажите папку с распакованным архивом (manifest + файлы)")
# Если директория пуста — спрашиваем путь к tar.gz
if not os.path.isdir(remote_dir) or not os.listdir(remote_dir):
archive_prompt = prompt("Архив не найден в стандартной папке. Укажите путь к .tar.gz архиву (или Enter для указания папки)")
if archive_prompt and os.path.isfile(archive_prompt):
# Распаковываем tar.gz во временную папку
extract_dir = os.path.join(remote_dir, "extracted")
run(f"{sudo} mkdir -p {extract_dir}", check=False)
run(f"{sudo} tar xzf '{archive_prompt}' -C {extract_dir}", check=False)
remote_dir = extract_dir
info(f"Архив распакован в: {remote_dir}")
elif archive_prompt and os.path.isdir(archive_prompt):
remote_dir = archive_prompt
else:
remote_dir = prompt("Укажите папку с распакованным архивом (manifest + файлы)") or "/tmp/docker-migrate-incoming"
if not os.path.isdir(remote_dir):
raise RuntimeError(f"Папка не найдена: {remote_dir}")
manifest_file = None
for f in os.listdir(remote_dir):
if f.endswith("_manifest.json"):
manifest_file = os.path.join(remote_dir, f)
break
if os.path.isdir(remote_dir):
# Рекурсивный поиск manifest.json (может быть в подпапках после tar xzf)
for root, _, files in os.walk(remote_dir):
for f in files:
if f.endswith("_manifest.json"):
manifest_file = os.path.join(root, f)
break
if manifest_file:
break
if not manifest_file:
raise RuntimeError("manifest.json не найден в папке архива")

View File

@@ -1,95 +1,31 @@
# -*- coding: utf-8 -*-
"""
ssh.py — SSH-диагностика: поиск ключей, проверка подключения, генерация временных ключей
ssh.py — SSH-подготовка для переноса на новый сервер.
НЕ ищем ключи на старом сервере — спрашиваем пользователя как подключиться к новому.
"""
import os
import glob
import re
from core.color import info, warn, success, error as cerror, prompt, confirm
from core.runner import run, exists
from core import state
_SSH_DIR = os.path.expanduser("~/.ssh")
def list_private_keys():
"""Ищет приватные SSH-ключи в ~/.ssh/"""
if not os.path.isdir(_SSH_DIR):
return []
keys = []
# Стандартные паттерны приватных ключей (не .pub)
for pattern in ("id_rsa", "id_ed25519", "id_ecdsa", "id_dsa"):
for f in glob.glob(os.path.join(_SSH_DIR, pattern)):
# Убедимся что это не .pub файл
if not f.endswith(".pub") and os.path.isfile(f):
# Проверим что есть соответствующий .pub
pub = f + ".pub"
has_pub = os.path.isfile(pub)
keys.append({
"private": f,
"public": pub if has_pub else None,
"type": os.path.basename(f).replace("id_", ""),
"has_pub": has_pub,
})
# Ищем и другие файлы без расширения (которые содержат PRIVATE KEY)
for f in glob.glob(os.path.join(_SSH_DIR, "*")):
if os.path.isfile(f) and not f.endswith(".pub") and not f.endswith(".config"):
try:
with open(f, "r", encoding="utf-8", errors="ignore") as fh:
first = fh.read(100)
if "PRIVATE KEY" in first or "OPENSSH" in first:
pub = f + ".pub"
has_pub = os.path.isfile(pub)
if f not in [k["private"] for k in keys]:
keys.append({
"private": f,
"public": pub if has_pub else None,
"type": "unknown",
"has_pub": has_pub,
})
except Exception:
pass
return keys
def check_ssh_connectivity(host, user, port, key_path=None, timeout=10):
"""Проверяет SSH-соединение. Возвращает (ok, stdout, stderr)."""
def check_ssh_keyless(host, user, port, key_path, timeout=10):
"""Проверяет подключение к новому серверу с данным ключом."""
if not exists("ssh"):
return (False, "", "ssh не найден")
return (False, "ssh не найден")
identity = f"-i '{key_path}'" if key_path and os.path.isfile(key_path) else ""
cmd = f"ssh {identity} -p {port} -o ConnectTimeout={timeout} -o BatchMode=yes -o StrictHostKeyChecking=accept-new {user}@{host} 'echo migrate-ok'"
r = run(cmd, check=False, capture=True, timeout=30)
if r.returncode == 0 and "migrate-ok" in r.stdout:
return (True, r.stdout, r.stderr)
return (False, r.stdout, r.stderr)
def test_keys_against_target(host, user, port, keys):
"""Перебирает ключи и проверяет какой работает с target."""
if not keys:
return None
info(f"Проверяем SSH-подключение к {user}@{host}:{port} с найденными ключами ...")
working = None
for k in keys:
label = os.path.basename(k["private"])
ok, out, err = check_ssh_connectivity(host, user, port, key_path=k["private"])
if ok:
success(f" ✓ Ключ '{label}' работает!")
working = k
break
else:
# Проверим, не passphrase ли
if "passphrase" in err.lower() or "password" in err.lower():
warn(f" ! Ключ '{label}' требует passphrase (зашифрован). Пропускаем (нужен ssh-agent).")
else:
info(f" ✗ Ключ '{label}' не подходит ({err.strip()[:60]}).")
return working
return (True, "OK")
err = (r.stderr or "").strip()
if "passphrase" in err.lower():
return (False, "PASSPHRASE")
return (False, err[:120])
def generate_temp_keypair(host_label):
"""Генерирует временную пару ed25519 в /tmp/ для миграции."""
"""Генерирует временную пару ed25519 в /tmp/."""
temp_dir = "/tmp/docker-migrate-ssh-keys"
os.makedirs(temp_dir, exist_ok=True)
priv = os.path.join(temp_dir, f"migrate_{host_label}")
@@ -97,93 +33,92 @@ def generate_temp_keypair(host_label):
info("Генерируем временную SSH-пару ed25519 ...")
run(f"ssh-keygen -t ed25519 -C 'docker-migrate-temp' -f '{priv}' -N ''", check=False)
if os.path.isfile(priv) and os.path.isfile(pub):
success(f"Временная пара создана: {priv}")
# Устанавливаем безопасные права
os.chmod(priv, 0o600)
success(f"Временная пара создана: {priv}")
return {"private": priv, "public": pub, "type": "ed25519", "temp": True}
return None
def ssh_copy_id(host, user, port, pubkey_path):
"""Копирует публичный ключ на target через ssh-copy-id или вручную."""
if exists("ssh-copy-id"):
info("Копируем публичный ключ через ssh-copy-id ...")
# Добавляем SSH опции чтобы избежать зависания
r = run(
f"ssh-copy-id -p {port} -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new -i '{pubkey_path}' {user}@{host}",
check=False,
timeout=60
)
if r.returncode == 0:
success("Ключ добавлен на target через ssh-copy-id")
return True
else:
warn(f"ssh-copy-id не удался: {r.stderr.strip()[:120]}")
return False
"""ssh-copy-id на новый сервер. Интерактивно (пароль нового сервера)."""
if not exists("ssh-copy-id"):
return False
warn("⚠ Сейчас запустится ssh-copy-id — введите ПАРОЛЬ от НОВОГО сервера ↓")
r = run(
f"ssh-copy-id -p {port} -o ConnectTimeout=30 -o StrictHostKeyChecking=accept-new -i '{pubkey_path}' {user}@{host}",
check=False, capture=False, timeout=120
)
return r.returncode == 0
def manual_copy_instructions(host, user, port, pubkey_path):
"""Выводит инструкции по ручному добавлению ключа."""
print()
cerror("SSH-подключение к target не удалось ни с одним ключом.")
print()
info("Варианты решения:")
print(f" {yellow('1')} Добавьте публичный ключ на target вручную:")
try:
with open(pubkey_path, "r") as f:
pubkey_content = f.read().strip()
except Exception:
pubkey_content = "(не удалось прочитать)"
print(f" На {user}@{host} выполните:")
print(f" mkdir -p ~/.ssh && chmod 700 ~/.ssh")
print(f" echo '{pubkey_content}' >> ~/.ssh/authorized_keys")
print(f" chmod 600 ~/.ssh/authorized_keys")
print()
print(f" {yellow('2')} Или скопируйте через ssh-copy-id (если есть парольный доступ):")
print(f" ssh-copy-id -p {port} -i {pubkey_path} {user}@{host}")
print()
print(f" {yellow('3')} После добавления ключа запустите миграцию заново:")
print(f" docker-migrate --resume")
print()
def ensure_sshpass():
"""Устанавливает sshpass если нужно."""
if exists("sshpass"):
return True
info("sshpass не найден. Устанавливаем ...")
r1 = run("apt-get update", check=False, timeout=60)
if r1.returncode != 0:
warn(f"apt-get update: {r1.stderr.strip()[:120]}")
r = run("apt-get install -y sshpass", check=False, timeout=120)
if r.returncode != 0:
warn(f"Не удалось установить sshpass: {r.stderr.strip()[:120]}")
return False
return exists("sshpass")
# ...
def pick_or_setup_ssh_key(host, user, port):
"""
Главная функция SSH-подготовки.
Возвращает путь к приватному ключу для работы с target.
Если None — значит надо продолжить через пароль (или пользователь отказался).
Возвращает путь к приватному SSH-ключу для нового сервера.
Если None - значит нужен пароль нового сервера (sshpass).
"""
# 1. Найти существующие ключи
keys = list_private_keys()
if keys:
info(f"Найдено SSH-ключей: {len(keys)}")
for k in keys:
label = os.path.basename(k["private"])
status = "(есть .pub)" if k["has_pub"] else "(❗ нет .pub!)"
print(f" - {label} {status}")
info("")
info("Как подключиться к новому серверу?")
print(" 1) У меня есть приватный SSH-ключ от нового сервера (файл .pem/.key/.ppk)")
print(" 2) У меня есть логин и пароль от нового сервера")
print(" 0) Отмена")
choice = prompt("Ваш выбор", default="1").strip()
# 2. Проверить подключение с существующими ключами
working_key = test_keys_against_target(host, user, port, keys)
if working_key:
return working_key["private"]
# 3. Если не удалось — спросить о генерации временного ключа
if not confirm("SSH-подключение не удалось. Сгенерировать временную пару ed25519", default="y"):
warn("Продолжаем без ключевой пары. Передача через пароль (или выберите 'нет' и перенесите архив вручную).")
return None
temp_key = generate_temp_keypair(host.replace(".", "_"))
if not temp_key:
cerror("Не удалось сгенерировать ключ.")
return None
# 4. Попробовать ssh-copy-id с временным ключом
if ssh_copy_id(host, user, port, temp_key["public"]):
# Проверим ещё раз
ok, _, _ = check_ssh_connectivity(host, user, port, key_path=temp_key["private"])
if choice == "1":
# Пользователь даёт путь к ключу нового сервера
key_path = prompt("Путь к приватному SSH-ключу (например, /root/server.pem)").strip()
if not key_path or not os.path.isfile(key_path):
raise RuntimeError(f"Файл не найден: {key_path}")
ok, why = check_ssh_keyless(host, user, port, key_path)
if ok:
return temp_key["private"]
success("Приватный SSH-ключ подходит для нового сервера!")
return key_path
if why == "PASSPHRASE":
warn(f"Приватный SSH-ключ защищён passphrase. Разблокируйте: ssh-add {key_path}")
raise RuntimeError("Приватный SSH-ключ зашифрован. Разблокируйте через ssh-add и перезапустите.")
raise RuntimeError(f"Приватный SSH-ключ не подходит: {why}")
# 5. Fallback на инструкции
manual_copy_instructions(host, user, port, temp_key["public"])
raise RuntimeError("SSH-подключение не настроено. Выполните инструкции выше и запустите: docker-migrate --resume")
if choice == "2":
# Пароль нового сервера
info("")
info("Выберите способ:")
print(" 1) Ввести пароль от нового сервера — scp через sshpass")
print(" 2) Сгенерировать временный SSH-ключ + ssh-copy-id (новый сервер спросит пароль)")
sub = prompt("Ваш выбор", default="1").strip()
if sub == "2":
temp = generate_temp_keypair(host.replace(".", "_"))
if not temp:
raise RuntimeError("Не удалось сгенерировать SSH-ключ")
if ssh_copy_id(host, user, port, temp["public"]):
ok, _ = check_ssh_keyless(host, user, port, temp["private"])
if ok:
success("Временный SSH-ключ добавлен на новый сервер!")
return temp["private"]
# ssh-copy-id не сработал
warn("ssh-copy-id не удался. Варианты:")
print(f" 1) Вручную на новом сервере: mkdir -p ~/.ssh; echo '$(cat {temp['public']})' >> ~/.ssh/authorized_keys")
print(f" 2) Затем: docker-migrate --resume")
raise RuntimeError("ssh-copy-id не удался.")
# sub == "1" или любое другое — возвращаем None, caller запросит пароль
return None
raise RuntimeError("Перенос отменён пользователем.")

View File

@@ -1,11 +1,10 @@
# -*- coding: utf-8 -*-
"""
transfer.py — Адаптивный перенос архива (scp/rsync/fallback)
transfer.py — Адаптивный перенос архива (scp/rsync) + удалённый запуск target-mode.
"""
import os
import json
from core.color import info, success, warn, error as cerror, prompt, confirm, step
from core.color import info, success, warn, error as cerror, confirm, step
from core import state
from core.runner import run, exists
@@ -19,6 +18,8 @@ def do_transfer():
host = st.get("target_host")
user = st.get("target_user")
port = st.get("target_port", 22)
key_path = st.get("ssh_key")
password = st.get("ssh_password")
if not host or host.lower() in ("none", "", "localhost"):
warn("Target host не указан. Перенос невозможен.")
@@ -36,14 +37,68 @@ def do_transfer():
size_mb = size / 1024 / 1024
info(f"Размер архива: {size_mb:.2f} MB")
# Определяем путь назначения
# Путь назначения
remote_dir = "/tmp/docker-migrate-incoming"
remote_path = os.path.join(remote_dir, os.path.basename(archive_path))
# Копируем bootstrap (migrate + core/) чтобы target мог запуститься
# Найдём корень проекта
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
tar_script = os.path.join(project_root, "tar_project.sh")
# --- Готовим SSH/SCP опции один раз ---
ssh_opts_common = f"-p {port} -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new"
scp_opts_common = f"-P {port} -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new"
if key_path and os.path.isfile(key_path):
ssh_opts_common += f" -i '{key_path}'"
scp_opts_common += f" -i '{key_path}'"
info(f"Используем приватный SSH-ключ: {key_path}")
elif password:
# sshpass будет добавлен при вызове
info("Используем пароль через sshpass")
else:
ssh_opts_common += " -o BatchMode=yes"
scp_opts_common += " -o BatchMode=yes"
info("Пробуем ключевую аутентификацию (BatchMode)")
# Вспомогательная функция: выполнить SSH-команду (capture=True, для скриптов)
def ssh_cmd(remote_cmd, timeout=30):
if password:
full = f"sshpass -p '{password}' ssh {ssh_opts_common} {user}@{host} '{remote_cmd}'"
else:
full = f"ssh {ssh_opts_common} {user}@{host} '{remote_cmd}'"
return run(full, check=False, timeout=timeout)
# Интерактивный SSH (capture=False, для target-mode и другого интерактивного ввода)
def ssh_cmd_interactive(remote_cmd, timeout=300):
if password:
full = f"sshpass -p '{password}' ssh {ssh_opts_common} {user}@{host} '{remote_cmd}'"
else:
full = f"ssh {ssh_opts_common} {user}@{host} '{remote_cmd}'"
return run(full, check=False, capture=False, timeout=timeout)
# Вспомогательная функция: выполнить scp
def scp_cmd(src, dst, timeout=60):
if password:
full = f"sshpass -p '{password}' scp {scp_opts_common} {src} {user}@{host}:{dst}"
else:
full = f"scp {scp_opts_common} {src} {user}@{host}:{dst}"
return run(full, check=False, timeout=timeout)
# Вспомогательная функция: rsync
def rsync_cmd(src, dst, timeout=60):
# rsync -e 'ssh ...' (без sshpass в -e, sshpass оборачивает всю команду)
ssh_part = ssh_opts_common
if password:
full = f"sshpass -p '{password}' rsync -avz --progress -e 'ssh {ssh_part}' {src} {user}@{host}:{dst}"
else:
full = f"rsync -avz --progress -e 'ssh {ssh_part}' {src} {user}@{host}:{dst}"
return run(full, check=False, timeout=timeout)
# Проверим что target отвечает (mkdir)
info("Проверяем доступ к target ...")
r0 = ssh_cmd(f"mkdir -p {remote_dir}")
if r0.returncode != 0:
err = (r0.stderr or "").strip()[:200]
cerror(f"SSH не удался: {err}")
state.set_error("TRANSFER", r0.stdout, r0.stderr, suggestion="Проверьте пароль/ключ и доступ SSH. После исправления: docker-migrate --resume")
raise RuntimeError(f"SSH-доступ не работает. Код: {r0.returncode}")
# Выбираем метод
if size_mb < 50 and exists("scp"):
@@ -60,65 +115,56 @@ def do_transfer():
info(f"Выбран метод переноса: {method}")
key_path = st.get("ssh_key")
# Предварительная проверка уже сделана в pick_or_setup_ssh_key, повторно не делаем
# Базовые SSH-опции (ssh/rsh/rsync)
ssh_opts = f"-p {port}"
if key_path and os.path.isfile(key_path):
ssh_opts += f" -i '{key_path}'"
info(f"Используем SSH-ключ: {key_path}")
# Перенос архива
if method == "scp":
# scp использует заглавную -P для порта (строчная -p = preserve timestamps)
# Добавляем SSH-опции чтобы избежать зависания и silent fail
scp_opts = f"-P {port} -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new -o BatchMode=yes"
if key_path and os.path.isfile(key_path):
scp_opts += f" -i '{key_path}'"
info("Копируем архив через scp ...")
r = run(f"scp {scp_opts} {archive_path} {user}@{host}:{remote_path}", check=False, timeout=60)
r = scp_cmd(archive_path, remote_path)
else:
info("Копируем архив через rsync ...")
r = run(f"rsync -avz --progress -e 'ssh {ssh_opts}' {archive_path} {user}@{host}:{remote_path}", check=False, timeout=60)
r = rsync_cmd(archive_path, remote_path)
if r.returncode != 0:
state.set_error(
step="TRANSFER",
stdout=r.stdout,
stderr=r.stderr,
suggestion=f"Проверьте SSH-доступ к {user}@{host}:{port}. После исправления: docker-migrate --resume"
)
err = (r.stderr or "").strip()[:200]
cerror(f"Передача не удалась: {err}")
state.set_error(step="TRANSFER", stdout=r.stdout, stderr=r.stderr, suggestion=f"Проверьте SSH-доступ к {user}@{host}:{port}. После исправления: docker-migrate --resume")
raise RuntimeError(f"Передача файла не удалась. Код возврата: {r.returncode}")
success(f"Архив передан на {host}:{remote_path}")
# Распаковка на target во временную директорию
# Распаковка на target
info("Распаковываем архив на target ...")
r2 = run(f"ssh {ssh_opts} {user}@{host} 'mkdir -p {remote_dir} && tar xzf {remote_path} -C {remote_dir}'", check=False)
r2 = ssh_cmd(f"tar xzf {remote_path} -C {remote_dir}")
if r2.returncode != 0:
warn(f"Не удалось распаковать архив на target: {r2.stderr}")
state.set_error(
step="TRANSFER_UNPACK",
stdout=r2.stdout,
stderr=r2.stderr,
suggestion=f"Проверьте доступ SSH и tar на {host}. После исправления: docker-migrate --resume"
)
raise RuntimeError(f"Распаковка на target не удалась: {r2.stderr}")
err = (r2.stderr or "").strip()[:200]
warn(f"Не удалось распаковать: {err}")
state.set_error("TRANSFER_UNPACK", r2.stdout, r2.stderr, suggestion="Проверьте tar на target")
raise RuntimeError("Распаковка не удалась")
# Сохраняем remote_dir в state для target
state.set_stage("TRANSFER", target_remote_dir=remote_dir)
# Предлагаем сразу запустить target-режим удалённо
if confirm("Сразу запустить восстановление на новом сервере (remote target mode)", default="y"):
# Предлагаем запустить target-режим удалённо
if confirm("Сразу запустить восстановление на новом сервере", default="y"):
info("Запускаем target-mode удалённо ...")
# Передаём скрипт на target
# 1. Упаковываем скрипт локально
project_local = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
run(f"scp {ssh_opts} -r {project_local} {user}@{host}:/opt/docker-migrate-tool", check=False)
# Запуск target как remote python
run(f"ssh {ssh_opts} {user}@{host} 'cd /opt/docker-migrate-tool && python3 core/main.py --mode=target --remote-dir={remote_dir}'", check=False)
local_tool_tar = "/tmp/docker-migrate-tool.tar.gz"
run(f"tar czf {local_tool_tar} -C {project_local} .", check=False, timeout=30)
# 2. Передаём на новый сервер
tool_remote = "/tmp/docker-migrate-tool.tar.gz"
if method == "scp":
r3 = scp_cmd(local_tool_tar, tool_remote)
else:
r3 = rsync_cmd(local_tool_tar, tool_remote)
if r3.returncode != 0:
warn("Не удалось передать скрипт на новый сервер.")
success(f"Архив передан. Запустите на новом сервере: python3 core/main.py --mode=target --remote-dir={remote_dir}")
else:
# 3. Распаковываем и запускаем (интерактивно, т.к. target-mode спрашивает у пользователя)
ssh_cmd_interactive(f"mkdir -p /opt/docker-migrate-tool && tar xzf {tool_remote} -C /opt/docker-migrate-tool")
ssh_cmd_interactive(f"cd /opt/docker-migrate-tool && python3 core/main.py --mode=target --remote-dir={remote_dir}", timeout=600)
else:
success(f"Архив передан. Запустите на target: python3 core/main.py --mode=target --remote-dir={remote_dir}")
# Transfer выполнен — отмечаем выполненным, чтобы resume не повторял
success(f"Архив передан. Запустите на новом сервере: python3 core/main.py --mode=target --remote-dir={remote_dir}")
state.mark_completed("TRANSFER")
state.set_stage("DONE")