diff --git a/source/source.py b/source/source.py index 94a829c..6ea2348 100644 --- a/source/source.py +++ b/source/source.py @@ -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() diff --git a/transfer/ssh.py b/transfer/ssh.py index 92041da..b9decbc 100644 --- a/transfer/ssh.py +++ b/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("Перенос отменён пользователем.")