fix: full sshpass support for password-based transfer; pre-flight SSH check; clear error diagnostics
This commit is contained in:
@@ -381,9 +381,16 @@ def do_transfer_offer():
|
||||
from transfer.transfer import do_transfer
|
||||
do_transfer()
|
||||
else:
|
||||
# Пользователь отказался от ключа — очищаем старый ssh_key из state
|
||||
state.set_stage("TRANSFER", target_host=host, target_user=user, target_port=port_int, ssh_key=None)
|
||||
warn("SSH-ключ не настроен. Автоматическая передача невозможна (scp с BatchMode=yes не поддерживает пароль).")
|
||||
success(f"Архив оставлен в: {_ARCHIVE_DIR}")
|
||||
info("Перенесите вручную через rsync/scp с паролем, или запустите заново и сгенерируйте ключ.")
|
||||
state.set_stage("DONE")
|
||||
# Нет SSH-ключа — спрашиваем пароль и пробуем sshpass
|
||||
password = prompt(f"Введите пароль для {user}@{host} (или Enter для отмены)")
|
||||
if not password:
|
||||
state.set_error("ssh_key_setup", "", "Пароль не введён", suggestion="Запустите docker-migrate --resume или настройте SSH-ключ")
|
||||
raise RuntimeError("Пароль не введён. Установите SSH-ключ или введите пароль.")
|
||||
# Проверяем/устанавливаем sshpass
|
||||
from transfer.ssh import ensure_sshpass
|
||||
if not ensure_sshpass():
|
||||
state.set_error("ssh_key_setup", "", "sshpass не найден и не удалось установить", suggestion="Установите sshpass: apt-get install -y sshpass")
|
||||
raise RuntimeError("sshpass не найден. Установите: apt-get install -y 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()
|
||||
|
||||
@@ -147,6 +147,21 @@ def manual_copy_instructions(host, user, port, pubkey_path):
|
||||
print()
|
||||
|
||||
|
||||
def ensure_sshpass():
|
||||
"""Проверяет наличие sshpass, при необходимости ставит через apt."""
|
||||
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-подготовки.
|
||||
|
||||
@@ -19,6 +19,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 +38,31 @@ 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")
|
||||
# Создаём remote_dir через ssh перед scp/rsync
|
||||
ssh_base = f"ssh -p {port} -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new"
|
||||
if key_path and os.path.isfile(key_path):
|
||||
ssh_base += f" -i '{key_path}'"
|
||||
info(f"Используем SSH-ключ: {key_path}")
|
||||
elif password:
|
||||
if not exists("sshpass"):
|
||||
raise RuntimeError("sshpass не найден, но нужен для пароля. Установите: apt-get install -y sshpass")
|
||||
ssh_base = f"sshpass -p '{password}' {ssh_base}"
|
||||
info("Используем пароль через sshpass")
|
||||
else:
|
||||
ssh_base += " -o BatchMode=yes"
|
||||
info("Пробуем ключевую аутентификацию (BatchMode)")
|
||||
|
||||
# Проверим что target отвечает (mkdir)
|
||||
info("Проверяем доступ к target ...")
|
||||
r0 = run(f"{ssh_base} {user}@{host} 'mkdir -p {remote_dir}'", check=False, timeout=30)
|
||||
if r0.returncode != 0:
|
||||
cerror(f"SSH не удался: {r0.stderr.strip()[:200]}")
|
||||
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,33 +79,29 @@ 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
|
||||
if not (key_path and os.path.isfile(key_path)):
|
||||
warn("SSH-ключ не найден. scp с BatchMode=yes не может использовать пароль.")
|
||||
info("Варианты: 1) Сгенерируйте ключ через resume; 2) Используйте rsync; 3) Перенесите вручную.")
|
||||
state.set_error("TRANSFER", "", "SSH key missing for scp with BatchMode", suggestion="Запустите docker-migrate --resume и сгенерируйте временный SSH-ключ")
|
||||
raise RuntimeError("SSH-ключ отсутствует. Автоматический перенос невозможен без ключа.")
|
||||
scp_opts = f"-P {port} -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new -o BatchMode=yes"
|
||||
# scp: заглавная -P для порта
|
||||
scp_opts = f"-P {port} -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new"
|
||||
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)
|
||||
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:
|
||||
# rsync через ssh_base
|
||||
info("Копируем архив через rsync ...")
|
||||
r = run(f"rsync -avz --progress -e 'ssh {ssh_opts}' {archive_path} {user}@{host}:{remote_path}", check=False, timeout=60)
|
||||
rsync_ssh = ssh_base.replace("ssh ", "") # убираем 'ssh ' для rsync -e
|
||||
r = run(f"rsync -avz --progress -e 'ssh {rsync_ssh}' {archive_path} {user}@{host}:{remote_path}", check=False, timeout=60)
|
||||
|
||||
if r.returncode != 0:
|
||||
cerror(f"Передача не удалась: {r.stderr.strip()[:200]}")
|
||||
state.set_error(
|
||||
step="TRANSFER",
|
||||
stdout=r.stdout,
|
||||
@@ -97,33 +112,25 @@ def do_transfer():
|
||||
|
||||
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 = run(f"{ssh_base} {user}@{host} 'tar xzf {remote_path} -C {remote_dir}'", check=False, timeout=60)
|
||||
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}")
|
||||
warn(f"Не удалось распаковать: {r2.stderr.strip()[:200]}")
|
||||
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
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
else:
|
||||
success(f"Архив передан. Запустите на target: python3 core/main.py --mode=target --remote-dir={remote_dir}")
|
||||
|
||||
# Transfer выполнен — отмечаем выполненным, чтобы resume не повторял
|
||||
state.mark_completed("TRANSFER")
|
||||
state.set_stage("DONE")
|
||||
|
||||
Reference in New Issue
Block a user