Compare commits

..
11 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
5 changed files with 199 additions and 214 deletions
+13
View File
@@ -112,6 +112,19 @@ def prompt(text, default=None):
sys.exit(130) 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"): def confirm(text, default="y"):
"""Да/нет с обработкой EOF. В pipe — возвращает default.""" """Да/нет с обработкой EOF. В pipe — возвращает default."""
yn = "Y/n" if default.lower() == "y" else "y/N" yn = "Y/n" if default.lower() == "y" else "y/N"
+10 -12
View File
@@ -10,7 +10,7 @@ import tarfile
import time import time
import sys 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, prompt_password, confirm, divider, log_cmd
from core import state from core import state
from core.runner import run from core.runner import run
from discover.docker import discover_docker, get_container_pid from discover.docker import discover_docker, get_container_pid
@@ -378,19 +378,17 @@ def do_transfer_offer():
if key_path: if key_path:
state.set_stage("TRANSFER", target_host=host, target_user=user, target_port=port_int, ssh_key=key_path) state.set_stage("TRANSFER", target_host=host, target_user=user, target_port=port_int, ssh_key=key_path)
from transfer.transfer import do_transfer
do_transfer()
else: else:
# Нет SSH-ключа — спрашиваем пароль и пробуем sshpass # Нет SSH-ключа — запрашиваем пароль от НОВОГО сервера
password = prompt(f"Введите пароль для {user}@{host} (или Enter для отмены)") password = prompt_password(f"Пароль от нового сервера {user}@{host} (Enter для отмены)")
if not password: if not password:
state.set_error("ssh_key_setup", "", "Пароль не введён", suggestion="Запустите docker-migrate --resume или настройте SSH-ключ") state.set_error("ssh_key_setup", "", "Пароль не введён", suggestion="Запустите docker-migrate --resume или настройте приватный SSH-ключ на новом сервере")
raise RuntimeError("Пароль не введён. Установите SSH-ключ или введите пароль.") raise RuntimeError("Пароль от нового сервера не введён.")
# Проверяем/устанавливаем sshpass
from transfer.ssh import ensure_sshpass from transfer.ssh import ensure_sshpass
if not ensure_sshpass(): if not ensure_sshpass():
state.set_error("ssh_key_setup", "", "sshpass не найден и не удалось установить", suggestion="Установите sshpass: apt-get install -y sshpass") state.set_error("ssh_key_setup", "", "sshpass не найден", suggestion="apt-get install -y sshpass")
raise RuntimeError("sshpass не найден. Установите: 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) 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() from transfer.transfer import do_transfer
do_transfer()
+27 -7
View File
@@ -145,14 +145,34 @@ def do_restore():
# Определяем директорию с архивом # Определяем директорию с архивом
remote_dir = st.get("target_remote_dir", "/tmp/docker-migrate-incoming") 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 manifest_file = None
for f in os.listdir(remote_dir): if os.path.isdir(remote_dir):
if f.endswith("_manifest.json"): # Рекурсивный поиск manifest.json (может быть в подпапках после tar xzf)
manifest_file = os.path.join(remote_dir, f) for root, _, files in os.walk(remote_dir):
break 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: if not manifest_file:
raise RuntimeError("manifest.json не найден в папке архива") raise RuntimeError("manifest.json не найден в папке архива")
+73 -153
View File
@@ -1,95 +1,31 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
ssh.py — SSH-диагностика: поиск ключей, проверка подключения, генерация временных ключей ssh.py — SSH-подготовка для переноса на новый сервер.
НЕ ищем ключи на старом сервере — спрашиваем пользователя как подключиться к новому.
""" """
import os import os
import glob
import re
from core.color import info, warn, success, error as cerror, prompt, confirm from core.color import info, warn, success, error as cerror, prompt, confirm
from core.runner import run, exists from core.runner import run, exists
from core import state
_SSH_DIR = os.path.expanduser("~/.ssh")
def list_private_keys(): def check_ssh_keyless(host, user, port, key_path, timeout=10):
"""Ищет приватные 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)."""
if not exists("ssh"): 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 "" 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'" 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) r = run(cmd, check=False, capture=True, timeout=30)
if r.returncode == 0 and "migrate-ok" in r.stdout: if r.returncode == 0 and "migrate-ok" in r.stdout:
return (True, r.stdout, r.stderr) return (True, "OK")
return (False, r.stdout, r.stderr) err = (r.stderr or "").strip()
if "passphrase" in err.lower():
return (False, "PASSPHRASE")
def test_keys_against_target(host, user, port, keys): return (False, err[:120])
"""Перебирает ключи и проверяет какой работает с 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
def generate_temp_keypair(host_label): def generate_temp_keypair(host_label):
"""Генерирует временную пару ed25519 в /tmp/ для миграции.""" """Генерирует временную пару ed25519 в /tmp/."""
temp_dir = "/tmp/docker-migrate-ssh-keys" temp_dir = "/tmp/docker-migrate-ssh-keys"
os.makedirs(temp_dir, exist_ok=True) os.makedirs(temp_dir, exist_ok=True)
priv = os.path.join(temp_dir, f"migrate_{host_label}") priv = os.path.join(temp_dir, f"migrate_{host_label}")
@@ -97,64 +33,32 @@ def generate_temp_keypair(host_label):
info("Генерируем временную SSH-пару ed25519 ...") info("Генерируем временную SSH-пару ed25519 ...")
run(f"ssh-keygen -t ed25519 -C 'docker-migrate-temp' -f '{priv}' -N ''", check=False) 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): if os.path.isfile(priv) and os.path.isfile(pub):
success(f"Временная пара создана: {priv}")
# Устанавливаем безопасные права
os.chmod(priv, 0o600) os.chmod(priv, 0o600)
success(f"Временная пара создана: {priv}")
return {"private": priv, "public": pub, "type": "ed25519", "temp": True} return {"private": priv, "public": pub, "type": "ed25519", "temp": True}
return None return None
def ssh_copy_id(host, user, port, pubkey_path): def ssh_copy_id(host, user, port, pubkey_path):
"""Копирует публичный ключ на target через ssh-copy-id или вручную.""" """ssh-copy-id на новый сервер. Интерактивно (пароль нового сервера)."""
if exists("ssh-copy-id"): if not exists("ssh-copy-id"):
info("Копируем публичный ключ через ssh-copy-id ...") return False
# Добавляем SSH опции чтобы избежать зависания warn("⚠ Сейчас запустится ssh-copy-id — введите ПАРОЛЬ от НОВОГО сервера ↓")
r = run( r = run(
f"ssh-copy-id -p {port} -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new -i '{pubkey_path}' {user}@{host}", f"ssh-copy-id -p {port} -o ConnectTimeout=30 -o StrictHostKeyChecking=accept-new -i '{pubkey_path}' {user}@{host}",
check=False, check=False, capture=False, timeout=120
timeout=60 )
) return r.returncode == 0
if r.returncode == 0:
success("Ключ добавлен на target через ssh-copy-id")
return True
else:
warn(f"ssh-copy-id не удался: {r.stderr.strip()[:120]}")
return False
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(): def ensure_sshpass():
"""Проверяет наличие sshpass, при необходимости ставит через apt.""" """Устанавливает sshpass если нужно."""
if exists("sshpass"): if exists("sshpass"):
return True return True
info("sshpass не найден. Устанавливаем ...") info("sshpass не найден. Устанавливаем ...")
r1 = run("apt-get update", check=False, timeout=60) r1 = run("apt-get update", check=False, timeout=60)
if r1.returncode != 0: if r1.returncode != 0:
warn(f"apt-get update не удался: {r1.stderr.strip()[:120]}") warn(f"apt-get update: {r1.stderr.strip()[:120]}")
r = run("apt-get install -y sshpass", check=False, timeout=120) r = run("apt-get install -y sshpass", check=False, timeout=120)
if r.returncode != 0: if r.returncode != 0:
warn(f"Не удалось установить sshpass: {r.stderr.strip()[:120]}") warn(f"Не удалось установить sshpass: {r.stderr.strip()[:120]}")
@@ -162,43 +66,59 @@ def ensure_sshpass():
return exists("sshpass") return exists("sshpass")
# ...
def pick_or_setup_ssh_key(host, user, port): def pick_or_setup_ssh_key(host, user, port):
""" """
Главная функция SSH-подготовки. Возвращает путь к приватному SSH-ключу для нового сервера.
Возвращает путь к приватному ключу для работы с target. Если None - значит нужен пароль нового сервера (sshpass).
Если None — значит надо продолжить через пароль (или пользователь отказался).
""" """
# 1. Найти существующие ключи info("")
keys = list_private_keys() info("Как подключиться к новому серверу?")
if keys: print(" 1) У меня есть приватный SSH-ключ от нового сервера (файл .pem/.key/.ppk)")
info(f"Найдено SSH-ключей: {len(keys)}") print(" 2) У меня есть логин и пароль от нового сервера")
for k in keys: print(" 0) Отмена")
label = os.path.basename(k["private"]) choice = prompt("Ваш выбор", default="1").strip()
status = "(есть .pub)" if k["has_pub"] else "(❗ нет .pub!)"
print(f" - {label} {status}")
# 2. Проверить подключение с существующими ключами if choice == "1":
working_key = test_keys_against_target(host, user, port, keys) # Пользователь даёт путь к ключу нового сервера
if working_key: key_path = prompt("Путь к приватному SSH-ключу (например, /root/server.pem)").strip()
return working_key["private"] if not key_path or not os.path.isfile(key_path):
raise RuntimeError(f"Файл не найден: {key_path}")
# 3. Если не удалось — спросить о генерации временного ключа ok, why = check_ssh_keyless(host, user, port, key_path)
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 ok: 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 на инструкции if choice == "2":
manual_copy_instructions(host, user, port, temp_key["public"]) # Пароль нового сервера
raise RuntimeError("SSH-подключение не настроено. Выполните инструкции выше и запустите: docker-migrate --resume") 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("Перенос отменён пользователем.")
+76 -42
View File
@@ -1,11 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
transfer.py — Адаптивный перенос архива (scp/rsync/fallback) transfer.py — Адаптивный перенос архива (scp/rsync) + удалённый запуск target-mode.
""" """
import os import os
import json from core.color import info, success, warn, error as cerror, confirm, step
from core.color import info, success, warn, error as cerror, prompt, confirm, step
from core import state from core import state
from core.runner import run, exists from core.runner import run, exists
@@ -42,25 +41,62 @@ def do_transfer():
remote_dir = "/tmp/docker-migrate-incoming" remote_dir = "/tmp/docker-migrate-incoming"
remote_path = os.path.join(remote_dir, os.path.basename(archive_path)) remote_path = os.path.join(remote_dir, os.path.basename(archive_path))
# Создаём remote_dir через ssh перед scp/rsync # --- Готовим SSH/SCP опции один раз ---
ssh_base = f"ssh -p {port} -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new" 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): if key_path and os.path.isfile(key_path):
ssh_base += f" -i '{key_path}'" ssh_opts_common += f" -i '{key_path}'"
info(f"Используем SSH-ключ: {key_path}") scp_opts_common += f" -i '{key_path}'"
info(f"Используем приватный SSH-ключ: {key_path}")
elif password: elif password:
if not exists("sshpass"): # sshpass будет добавлен при вызове
raise RuntimeError("sshpass не найден, но нужен для пароля. Установите: apt-get install -y sshpass")
ssh_base = f"sshpass -p '{password}' {ssh_base}"
info("Используем пароль через sshpass") info("Используем пароль через sshpass")
else: else:
ssh_base += " -o BatchMode=yes" ssh_opts_common += " -o BatchMode=yes"
scp_opts_common += " -o BatchMode=yes"
info("Пробуем ключевую аутентификацию (BatchMode)") 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) # Проверим что target отвечает (mkdir)
info("Проверяем доступ к target ...") info("Проверяем доступ к target ...")
r0 = run(f"{ssh_base} {user}@{host} 'mkdir -p {remote_dir}'", check=False, timeout=30) r0 = ssh_cmd(f"mkdir -p {remote_dir}")
if r0.returncode != 0: if r0.returncode != 0:
cerror(f"SSH не удался: {r0.stderr.strip()[:200]}") err = (r0.stderr or "").strip()[:200]
cerror(f"SSH не удался: {err}")
state.set_error("TRANSFER", r0.stdout, r0.stderr, suggestion="Проверьте пароль/ключ и доступ SSH. После исправления: docker-migrate --resume") state.set_error("TRANSFER", r0.stdout, r0.stderr, suggestion="Проверьте пароль/ключ и доступ SSH. После исправления: docker-migrate --resume")
raise RuntimeError(f"SSH-доступ не работает. Код: {r0.returncode}") raise RuntimeError(f"SSH-доступ не работает. Код: {r0.returncode}")
@@ -79,44 +115,28 @@ def do_transfer():
info(f"Выбран метод переноса: {method}") info(f"Выбран метод переноса: {method}")
# Перенос архива
if method == "scp": if method == "scp":
# scp: заглавная -P для порта info("Копируем архив через scp ...")
scp_opts = f"-P {port} -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new" r = scp_cmd(archive_path, remote_path)
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)
elif password:
# sshpass оборачивает всю команду scp
info("Копируем архив через scp + sshpass ...")
r = run(f"sshpass -p '{password}' scp {scp_opts} {archive_path} {user}@{host}:{remote_path}", check=False, timeout=60)
else:
scp_opts += " -o BatchMode=yes"
info("Копируем архив через scp ...")
r = run(f"scp {scp_opts} {archive_path} {user}@{host}:{remote_path}", check=False, timeout=60)
else: else:
# rsync через ssh_base
info("Копируем архив через rsync ...") info("Копируем архив через rsync ...")
rsync_ssh = ssh_base.replace("ssh ", "") # убираем 'ssh ' для rsync -e r = rsync_cmd(archive_path, remote_path)
r = run(f"rsync -avz --progress -e 'ssh {rsync_ssh}' {archive_path} {user}@{host}:{remote_path}", check=False, timeout=60)
if r.returncode != 0: if r.returncode != 0:
cerror(f"Передача не удалась: {r.stderr.strip()[:200]}") err = (r.stderr or "").strip()[:200]
state.set_error( cerror(f"Передача не удалась: {err}")
step="TRANSFER", state.set_error(step="TRANSFER", stdout=r.stdout, stderr=r.stderr, suggestion=f"Проверьте SSH-доступ к {user}@{host}:{port}. После исправления: docker-migrate --resume")
stdout=r.stdout,
stderr=r.stderr,
suggestion=f"Проверьте SSH-доступ к {user}@{host}:{port}. После исправления: docker-migrate --resume"
)
raise RuntimeError(f"Передача файла не удалась. Код возврата: {r.returncode}") raise RuntimeError(f"Передача файла не удалась. Код возврата: {r.returncode}")
success(f"Архив передан на {host}:{remote_path}") success(f"Архив передан на {host}:{remote_path}")
# Распаковка на target # Распаковка на target
info("Распаковываем архив на target ...") info("Распаковываем архив на target ...")
r2 = run(f"{ssh_base} {user}@{host} 'tar xzf {remote_path} -C {remote_dir}'", check=False, timeout=60) r2 = ssh_cmd(f"tar xzf {remote_path} -C {remote_dir}")
if r2.returncode != 0: if r2.returncode != 0:
warn(f"Не удалось распаковать: {r2.stderr.strip()[:200]}") err = (r2.stderr or "").strip()[:200]
warn(f"Не удалось распаковать: {err}")
state.set_error("TRANSFER_UNPACK", r2.stdout, r2.stderr, suggestion="Проверьте tar на target") state.set_error("TRANSFER_UNPACK", r2.stdout, r2.stderr, suggestion="Проверьте tar на target")
raise RuntimeError("Распаковка не удалась") raise RuntimeError("Распаковка не удалась")
@@ -126,11 +146,25 @@ def do_transfer():
# Предлагаем запустить target-режим удалённо # Предлагаем запустить target-режим удалённо
if confirm("Сразу запустить восстановление на новом сервере", default="y"): if confirm("Сразу запустить восстановление на новом сервере", default="y"):
info("Запускаем target-mode удалённо ...") info("Запускаем target-mode удалённо ...")
# 1. Упаковываем скрипт локально
project_local = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) project_local = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
run(f"{ssh_base} {user}@{host} 'mkdir -p /opt/docker-migrate-tool && tar czf - -C {project_local} . | tar xzf - -C /opt/docker-migrate-tool'", check=False, timeout=60) local_tool_tar = "/tmp/docker-migrate-tool.tar.gz"
run(f"{ssh_base} {user}@{host} 'cd /opt/docker-migrate-tool && python3 core/main.py --mode=target --remote-dir={remote_dir}'", check=False, timeout=300) 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: else:
success(f"Архив передан. Запустите на target: python3 core/main.py --mode=target --remote-dir={remote_dir}") success(f"Архив передан. Запустите на новом сервере: python3 core/main.py --mode=target --remote-dir={remote_dir}")
state.mark_completed("TRANSFER") state.mark_completed("TRANSFER")
state.set_stage("DONE") state.set_stage("DONE")