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:
|
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()
|
||||||
|
|||||||
106
transfer/ssh.py
106
transfer/ssh.py
@@ -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")
|
|
||||||
|
|||||||
Reference in New Issue
Block a user