feat: interactive SSH auth choice (key with passphrase, password via sshpass, or temp ed25519); fix missing do_transfer call for password path
This commit is contained in:
@@ -378,8 +378,6 @@ 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)
|
||||
from transfer.transfer import do_transfer
|
||||
do_transfer()
|
||||
else:
|
||||
# Нет SSH-ключа — спрашиваем пароль и пробуем sshpass
|
||||
password = prompt(f"Введите пароль для {user}@{host} (или Enter для отмены)")
|
||||
@@ -392,5 +390,6 @@ def do_transfer_offer():
|
||||
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()
|
||||
|
||||
from transfer.transfer import do_transfer
|
||||
do_transfer()
|
||||
|
||||
106
transfer/ssh.py
106
transfer/ssh.py
@@ -162,11 +162,47 @@ def ensure_sshpass():
|
||||
return exists("sshpass")
|
||||
|
||||
|
||||
def try_key_with_passphrase(key_path, passphrase):
|
||||
"""Проверяет, работает ли ключ с введённым passphrase через ssh-agent или expect."""
|
||||
# Способ 1: ssh-agent + ssh-add
|
||||
from core.runner import run
|
||||
import subprocess
|
||||
try:
|
||||
# Запускаем ssh-agent и добавляем ключ
|
||||
agent_out = subprocess.run(["ssh-agent", "-s"], capture_output=True, text=True)
|
||||
if agent_out.returncode != 0:
|
||||
return False
|
||||
# Парсим переменные окружения из ssh-agent -s
|
||||
env_lines = agent_out.stdout.strip().split(";")
|
||||
agent_env = {}
|
||||
for ln in env_lines:
|
||||
if "=" in ln and "SSH_" in ln:
|
||||
k, v = ln.strip().split("=", 1)
|
||||
agent_env[k] = v
|
||||
|
||||
# ssh-add с passphrase через PIPE
|
||||
add_proc = subprocess.Popen(
|
||||
["ssh-add", key_path],
|
||||
env={**os.environ, **agent_env},
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
stdout, stderr = add_proc.communicate(input=passphrase + "\n", timeout=10)
|
||||
if add_proc.returncode == 0:
|
||||
return True
|
||||
# Если не сработало — убиваем ssh-agent
|
||||
subprocess.run(["ssh-agent", "-k"], env={**os.environ, **agent_env}, capture_output=True)
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def pick_or_setup_ssh_key(host, user, port):
|
||||
"""
|
||||
Главная функция SSH-подготовки.
|
||||
Возвращает путь к приватному ключу для работы с target.
|
||||
Если None — значит надо продолжить через пароль (или пользователь отказался).
|
||||
Главная функция SSH-подготовки. Возвращает путь к приватному ключу.
|
||||
Если None — значит используем пароль (sshpass).
|
||||
"""
|
||||
# 1. Найти существующие ключи
|
||||
keys = list_private_keys()
|
||||
@@ -177,28 +213,54 @@ def pick_or_setup_ssh_key(host, user, port):
|
||||
status = "(есть .pub)" if k["has_pub"] else "(❗ нет .pub!)"
|
||||
print(f" - {label} {status}")
|
||||
|
||||
# 2. Проверить подключение с существующими ключами
|
||||
# 2. Проверить ключи БЕЗ passphrase
|
||||
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("Продолжаем без ключевой пары. Передача через пароль (или выберите 'нет' и перенесите архив вручную).")
|
||||
# 3. Проверить ключи С passphrase (если пользователь знает)
|
||||
for k in keys:
|
||||
label = os.path.basename(k["private"])
|
||||
# Проверим, не passphrase ли мешает
|
||||
ok, _, err = check_ssh_connectivity(host, user, port, key_path=k["private"])
|
||||
if not ok and ("passphrase" in err.lower() or "password" in err.lower()):
|
||||
if confirm(f"Ключ '{label}' зашифрован. Ввести passphrase и попробовать", default="y"):
|
||||
pw = prompt(f"Passphrase для {label} (ввод скрыт)")
|
||||
if pw and try_key_with_passphrase(k["private"], pw):
|
||||
# После ssh-add ключ должен работать
|
||||
ok2, _, _ = check_ssh_connectivity(host, user, port, key_path=k["private"])
|
||||
if ok2:
|
||||
success(f"Ключ '{label}' разблокирован и работает!")
|
||||
return k["private"]
|
||||
else:
|
||||
warn(f"Passphrase введён, но ключ всё равно не подходит для {host}")
|
||||
|
||||
# 4. Ничего не работает — выбор метода
|
||||
print()
|
||||
info("Выберите способ подключения к target:")
|
||||
print(" 1 Пароль (sshpass) — ввести пароль прямо сейчас")
|
||||
print(" 2 Сгенерировать временный ed25519 ключ — скрипт сам сделает ssh-copy-id")
|
||||
print(" 0 Отмена — передать архив вручную")
|
||||
choice = prompt("Ваш выбор", default="1").strip()
|
||||
|
||||
if choice == "2":
|
||||
# Генерация временного ключа
|
||||
temp_key = generate_temp_keypair(host.replace(".", "_"))
|
||||
if not temp_key:
|
||||
cerror("Не удалось сгенерировать ключ.")
|
||||
return None
|
||||
if ssh_copy_id(host, user, port, temp_key["public"]):
|
||||
ok, _, _ = check_ssh_connectivity(host, user, port, key_path=temp_key["private"])
|
||||
if ok:
|
||||
success("Временный ключ настроен и работает!")
|
||||
return temp_key["private"]
|
||||
# ssh-copy-id не сработал — показываем инструкции
|
||||
manual_copy_instructions(host, user, port, temp_key["public"])
|
||||
raise RuntimeError("ssh-copy-id не удался. Настройте ключ вручную и запустите: docker-migrate --resume")
|
||||
|
||||
elif choice == "1":
|
||||
# Пароль — caller запросит его позже
|
||||
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:
|
||||
return temp_key["private"]
|
||||
|
||||
# 5. Fallback на инструкции
|
||||
manual_copy_instructions(host, user, port, temp_key["public"])
|
||||
raise RuntimeError("SSH-подключение не настроено. Выполните инструкции выше и запустите: docker-migrate --resume")
|
||||
else:
|
||||
raise RuntimeError("Перенос отменён пользователем.")
|
||||
|
||||
Reference in New Issue
Block a user