diff --git a/source/source.py b/source/source.py index 1041675..94a829c 100644 --- a/source/source.py +++ b/source/source.py @@ -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() diff --git a/transfer/ssh.py b/transfer/ssh.py index 3341d4c..92041da 100644 --- a/transfer/ssh.py +++ b/transfer/ssh.py @@ -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-подготовки. diff --git a/transfer/transfer.py b/transfer/transfer.py index f1e8eb2..9ffced7 100644 --- a/transfer/transfer.py +++ b/transfer/transfer.py @@ -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")