fix: transfer.py — helper functions for ssh/scp/rsync, local tar then remote unpack for tool script, consistent error handling

This commit is contained in:
2026-05-23 00:11:04 +04:00
parent bfd6302448
commit 80bb196d24

View File

@@ -1,11 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
transfer.py — Адаптивный перенос архива (scp/rsync/fallback) transfer.py — Адаптивный перенос архива (scp/rsync) + удалённый запуск target-mode.
""" """
import os import os
import json from core.color import info, success, warn, error as cerror, confirm, step
from core.color import info, success, warn, error as cerror, prompt, confirm, step
from core import state from core import state
from core.runner import run, exists from core.runner import run, exists
@@ -42,25 +41,54 @@ def do_transfer():
remote_dir = "/tmp/docker-migrate-incoming" remote_dir = "/tmp/docker-migrate-incoming"
remote_path = os.path.join(remote_dir, os.path.basename(archive_path)) remote_path = os.path.join(remote_dir, os.path.basename(archive_path))
# Создаём remote_dir через ssh перед scp/rsync # --- Готовим SSH/SCP опции один раз ---
ssh_base = f"ssh -p {port} -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new" ssh_opts_common = f"-p {port} -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new"
scp_opts_common = f"-P {port} -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new"
if key_path and os.path.isfile(key_path): if key_path and os.path.isfile(key_path):
ssh_base += f" -i '{key_path}'" ssh_opts_common += f" -i '{key_path}'"
info(f"Используем SSH-ключ: {key_path}") scp_opts_common += f" -i '{key_path}'"
info(f"Используем приватный SSH-ключ: {key_path}")
elif password: elif password:
if not exists("sshpass"): # sshpass будет добавлен при вызове
raise RuntimeError("sshpass не найден, но нужен для пароля. Установите: apt-get install -y sshpass")
ssh_base = f"sshpass -p '{password}' {ssh_base}"
info("Используем пароль через sshpass") info("Используем пароль через sshpass")
else: else:
ssh_base += " -o BatchMode=yes" ssh_opts_common += " -o BatchMode=yes"
scp_opts_common += " -o BatchMode=yes"
info("Пробуем ключевую аутентификацию (BatchMode)") info("Пробуем ключевую аутентификацию (BatchMode)")
# Вспомогательная функция: выполнить SSH-команду
def ssh_cmd(remote_cmd, timeout=30):
if password:
full = f"sshpass -p '{password}' ssh {ssh_opts_common} {user}@{host} '{remote_cmd}'"
else:
full = f"ssh {ssh_opts_common} {user}@{host} '{remote_cmd}'"
return run(full, check=False, timeout=timeout)
# Вспомогательная функция: выполнить scp
def scp_cmd(src, dst, timeout=60):
if password:
full = f"sshpass -p '{password}' scp {scp_opts_common} {src} {user}@{host}:{dst}"
else:
full = f"scp {scp_opts_common} {src} {user}@{host}:{dst}"
return run(full, check=False, timeout=timeout)
# Вспомогательная функция: rsync
def rsync_cmd(src, dst, timeout=60):
# rsync -e 'ssh ...' (без sshpass в -e, sshpass оборачивает всю команду)
ssh_part = ssh_opts_common
if password:
full = f"sshpass -p '{password}' rsync -avz --progress -e 'ssh {ssh_part}' {src} {user}@{host}:{dst}"
else:
full = f"rsync -avz --progress -e 'ssh {ssh_part}' {src} {user}@{host}:{dst}"
return run(full, check=False, timeout=timeout)
# Проверим что target отвечает (mkdir) # Проверим что target отвечает (mkdir)
info("Проверяем доступ к target ...") info("Проверяем доступ к target ...")
r0 = run(f"{ssh_base} {user}@{host} 'mkdir -p {remote_dir}'", check=False, timeout=30) r0 = ssh_cmd(f"mkdir -p {remote_dir}")
if r0.returncode != 0: if r0.returncode != 0:
cerror(f"SSH не удался: {r0.stderr.strip()[:200]}") err = (r0.stderr or "").strip()[:200]
cerror(f"SSH не удался: {err}")
state.set_error("TRANSFER", r0.stdout, r0.stderr, suggestion="Проверьте пароль/ключ и доступ SSH. После исправления: docker-migrate --resume") state.set_error("TRANSFER", r0.stdout, r0.stderr, suggestion="Проверьте пароль/ключ и доступ SSH. После исправления: docker-migrate --resume")
raise RuntimeError(f"SSH-доступ не работает. Код: {r0.returncode}") raise RuntimeError(f"SSH-доступ не работает. Код: {r0.returncode}")
@@ -79,44 +107,28 @@ def do_transfer():
info(f"Выбран метод переноса: {method}") info(f"Выбран метод переноса: {method}")
# Перенос архива
if method == "scp": if method == "scp":
# scp: заглавная -P для порта info("Копируем архив через scp ...")
scp_opts = f"-P {port} -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new" r = scp_cmd(archive_path, remote_path)
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)
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: else:
# rsync через ssh_base
info("Копируем архив через rsync ...") info("Копируем архив через rsync ...")
rsync_ssh = ssh_base.replace("ssh ", "") # убираем 'ssh ' для rsync -e r = rsync_cmd(archive_path, remote_path)
r = run(f"rsync -avz --progress -e 'ssh {rsync_ssh}' {archive_path} {user}@{host}:{remote_path}", check=False, timeout=60)
if r.returncode != 0: if r.returncode != 0:
cerror(f"Передача не удалась: {r.stderr.strip()[:200]}") err = (r.stderr or "").strip()[:200]
state.set_error( cerror(f"Передача не удалась: {err}")
step="TRANSFER", state.set_error(step="TRANSFER", stdout=r.stdout, stderr=r.stderr, suggestion=f"Проверьте SSH-доступ к {user}@{host}:{port}. После исправления: docker-migrate --resume")
stdout=r.stdout,
stderr=r.stderr,
suggestion=f"Проверьте SSH-доступ к {user}@{host}:{port}. После исправления: docker-migrate --resume"
)
raise RuntimeError(f"Передача файла не удалась. Код возврата: {r.returncode}") raise RuntimeError(f"Передача файла не удалась. Код возврата: {r.returncode}")
success(f"Архив передан на {host}:{remote_path}") success(f"Архив передан на {host}:{remote_path}")
# Распаковка на target # Распаковка на target
info("Распаковываем архив на target ...") info("Распаковываем архив на target ...")
r2 = run(f"{ssh_base} {user}@{host} 'tar xzf {remote_path} -C {remote_dir}'", check=False, timeout=60) r2 = ssh_cmd(f"tar xzf {remote_path} -C {remote_dir}")
if r2.returncode != 0: if r2.returncode != 0:
warn(f"Не удалось распаковать: {r2.stderr.strip()[:200]}") err = (r2.stderr or "").strip()[:200]
warn(f"Не удалось распаковать: {err}")
state.set_error("TRANSFER_UNPACK", r2.stdout, r2.stderr, suggestion="Проверьте tar на target") state.set_error("TRANSFER_UNPACK", r2.stdout, r2.stderr, suggestion="Проверьте tar на target")
raise RuntimeError("Распаковка не удалась") raise RuntimeError("Распаковка не удалась")
@@ -126,11 +138,25 @@ def do_transfer():
# Предлагаем запустить target-режим удалённо # Предлагаем запустить target-режим удалённо
if confirm("Сразу запустить восстановление на новом сервере", default="y"): if confirm("Сразу запустить восстановление на новом сервере", default="y"):
info("Запускаем target-mode удалённо ...") info("Запускаем target-mode удалённо ...")
# 1. Упаковываем скрипт локально
project_local = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) project_local = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
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) local_tool_tar = "/tmp/docker-migrate-tool.tar.gz"
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) run(f"tar czf {local_tool_tar} -C {project_local} .", check=False, timeout=30)
# 2. Передаём на новый сервер
tool_remote = "/tmp/docker-migrate-tool.tar.gz"
if method == "scp":
r3 = scp_cmd(local_tool_tar, tool_remote)
else:
r3 = rsync_cmd(local_tool_tar, tool_remote)
if r3.returncode != 0:
warn("Не удалось передать скрипт на новый сервер.")
success(f"Архив передан. Запустите на новом сервере: python3 core/main.py --mode=target --remote-dir={remote_dir}")
else:
# 3. Распаковываем и запускаем
ssh_cmd(f"mkdir -p /opt/docker-migrate-tool && tar xzf {tool_remote} -C /opt/docker-migrate-tool")
ssh_cmd(f"cd /opt/docker-migrate-tool && python3 core/main.py --mode=target --remote-dir={remote_dir}", timeout=300)
else: else:
success(f"Архив передан. Запустите на target: python3 core/main.py --mode=target --remote-dir={remote_dir}") success(f"Архив передан. Запустите на новом сервере: python3 core/main.py --mode=target --remote-dir={remote_dir}")
state.mark_completed("TRANSFER") state.mark_completed("TRANSFER")
state.set_stage("DONE") state.set_stage("DONE")