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:
2026-05-22 23:41:09 +04:00
parent bfa3136131
commit 4d6c7ef506
2 changed files with 87 additions and 26 deletions

View File

@@ -378,8 +378,6 @@ 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-ключа — спрашиваем пароль и пробуем sshpass
password = prompt(f"Введите пароль для {user}@{host} (или Enter для отмены)") 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") state.set_error("ssh_key_setup", "", "sshpass не найден и не удалось установить", suggestion="Установите sshpass: apt-get install -y sshpass")
raise RuntimeError("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) 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()

View File

@@ -162,11 +162,47 @@ def ensure_sshpass():
return exists("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): def pick_or_setup_ssh_key(host, user, port):
""" """
Главная функция SSH-подготовки. Главная функция SSH-подготовки. Возвращает путь к приватному ключу.
Возвращает путь к приватному ключу для работы с target. Если None — значит используем пароль (sshpass).
Если None — значит надо продолжить через пароль (или пользователь отказался).
""" """
# 1. Найти существующие ключи # 1. Найти существующие ключи
keys = list_private_keys() 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!)" status = "(есть .pub)" if k["has_pub"] else "(❗ нет .pub!)"
print(f" - {label} {status}") print(f" - {label} {status}")
# 2. Проверить подключение с существующими ключами # 2. Проверить ключи БЕЗ passphrase
working_key = test_keys_against_target(host, user, port, keys) working_key = test_keys_against_target(host, user, port, keys)
if working_key: if working_key:
return working_key["private"] return working_key["private"]
# 3. Если не удалось — спросить о генерации временного ключа # 3. Проверить ключи С passphrase (если пользователь знает)
if not confirm("SSH-подключение не удалось. Сгенерировать временную пару ed25519", default="y"): for k in keys:
warn("Продолжаем без ключевой пары. Передача через пароль (или выберите 'нет' и перенесите архив вручную).") 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 return None
temp_key = generate_temp_keypair(host.replace(".", "_")) else:
if not temp_key: raise RuntimeError("Перенос отменён пользователем.")
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")