Compare commits
11
Commits
7d45d1f8c0
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79b91f157e | ||
|
|
c8bef6b41a | ||
|
|
80bb196d24 | ||
|
|
bfd6302448 | ||
|
|
21bd747329 | ||
|
|
0cd968a16d | ||
|
|
3ee5104302 | ||
|
|
bd16e339c0 | ||
|
|
6d6418c059 | ||
|
|
4d6c7ef506 | ||
|
|
bfa3136131 |
@@ -112,6 +112,19 @@ def prompt(text, default=None):
|
|||||||
sys.exit(130)
|
sys.exit(130)
|
||||||
|
|
||||||
|
|
||||||
|
def prompt_password(text):
|
||||||
|
"""Скрытый ввод пароля/passphrase. В pipe — обычный prompt с предупреждением."""
|
||||||
|
import getpass
|
||||||
|
try:
|
||||||
|
return getpass.getpass(f"{yellow('❯')} {text} ")
|
||||||
|
except EOFError:
|
||||||
|
print(f"{yellow('⚠')} Ввод пароля в pipe не поддерживается скрытым режимом. Используем обычный ввод.")
|
||||||
|
return prompt(text)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print(f"\n{yellow('⚠')} Прервано пользователем (Ctrl+C)")
|
||||||
|
sys.exit(130)
|
||||||
|
|
||||||
|
|
||||||
def confirm(text, default="y"):
|
def confirm(text, default="y"):
|
||||||
"""Да/нет с обработкой EOF. В pipe — возвращает default."""
|
"""Да/нет с обработкой EOF. В pipe — возвращает default."""
|
||||||
yn = "Y/n" if default.lower() == "y" else "y/N"
|
yn = "Y/n" if default.lower() == "y" else "y/N"
|
||||||
|
|||||||
+10
-12
@@ -10,7 +10,7 @@ import tarfile
|
|||||||
import time
|
import time
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from core.color import header, subheader, success, warn, error as cerror, info, step, prompt, confirm, divider, log_cmd
|
from core.color import header, subheader, success, warn, error as cerror, info, step, prompt, prompt_password, confirm, divider, log_cmd
|
||||||
from core import state
|
from core import state
|
||||||
from core.runner import run
|
from core.runner import run
|
||||||
from discover.docker import discover_docker, get_container_pid
|
from discover.docker import discover_docker, get_container_pid
|
||||||
@@ -378,19 +378,17 @@ 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-ключа — запрашиваем пароль от НОВОГО сервера
|
||||||
password = prompt(f"Введите пароль для {user}@{host} (или Enter для отмены)")
|
password = prompt_password(f"Пароль от нового сервера {user}@{host} (Enter для отмены)")
|
||||||
if not password:
|
if not password:
|
||||||
state.set_error("ssh_key_setup", "", "Пароль не введён", suggestion="Запустите docker-migrate --resume или настройте SSH-ключ")
|
state.set_error("ssh_key_setup", "", "Пароль не введён", suggestion="Запустите docker-migrate --resume или настройте приватный SSH-ключ на новом сервере")
|
||||||
raise RuntimeError("Пароль не введён. Установите SSH-ключ или введите пароль.")
|
raise RuntimeError("Пароль от нового сервера не введён.")
|
||||||
# Проверяем/устанавливаем sshpass
|
|
||||||
from transfer.ssh import ensure_sshpass
|
from transfer.ssh import ensure_sshpass
|
||||||
if not ensure_sshpass():
|
if not ensure_sshpass():
|
||||||
state.set_error("ssh_key_setup", "", "sshpass не найден и не удалось установить", suggestion="Установите sshpass: apt-get install -y sshpass")
|
state.set_error("ssh_key_setup", "", "sshpass не найден", suggestion="apt-get install -y sshpass")
|
||||||
raise RuntimeError("sshpass не найден. Установите: apt-get install -y sshpass")
|
raise RuntimeError("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()
|
||||||
|
|||||||
+27
-7
@@ -145,14 +145,34 @@ def do_restore():
|
|||||||
|
|
||||||
# Определяем директорию с архивом
|
# Определяем директорию с архивом
|
||||||
remote_dir = st.get("target_remote_dir", "/tmp/docker-migrate-incoming")
|
remote_dir = st.get("target_remote_dir", "/tmp/docker-migrate-incoming")
|
||||||
if not os.path.isdir(remote_dir):
|
|
||||||
remote_dir = prompt("Укажите папку с распакованным архивом (manifest + файлы)")
|
# Если директория пуста — спрашиваем путь к tar.gz
|
||||||
|
if not os.path.isdir(remote_dir) or not os.listdir(remote_dir):
|
||||||
|
archive_prompt = prompt("Архив не найден в стандартной папке. Укажите путь к .tar.gz архиву (или Enter для указания папки)")
|
||||||
|
if archive_prompt and os.path.isfile(archive_prompt):
|
||||||
|
# Распаковываем tar.gz во временную папку
|
||||||
|
extract_dir = os.path.join(remote_dir, "extracted")
|
||||||
|
run(f"{sudo} mkdir -p {extract_dir}", check=False)
|
||||||
|
run(f"{sudo} tar xzf '{archive_prompt}' -C {extract_dir}", check=False)
|
||||||
|
remote_dir = extract_dir
|
||||||
|
info(f"Архив распакован в: {remote_dir}")
|
||||||
|
elif archive_prompt and os.path.isdir(archive_prompt):
|
||||||
|
remote_dir = archive_prompt
|
||||||
|
else:
|
||||||
|
remote_dir = prompt("Укажите папку с распакованным архивом (manifest + файлы)") or "/tmp/docker-migrate-incoming"
|
||||||
|
if not os.path.isdir(remote_dir):
|
||||||
|
raise RuntimeError(f"Папка не найдена: {remote_dir}")
|
||||||
|
|
||||||
manifest_file = None
|
manifest_file = None
|
||||||
for f in os.listdir(remote_dir):
|
if os.path.isdir(remote_dir):
|
||||||
if f.endswith("_manifest.json"):
|
# Рекурсивный поиск manifest.json (может быть в подпапках после tar xzf)
|
||||||
manifest_file = os.path.join(remote_dir, f)
|
for root, _, files in os.walk(remote_dir):
|
||||||
break
|
for f in files:
|
||||||
|
if f.endswith("_manifest.json"):
|
||||||
|
manifest_file = os.path.join(root, f)
|
||||||
|
break
|
||||||
|
if manifest_file:
|
||||||
|
break
|
||||||
if not manifest_file:
|
if not manifest_file:
|
||||||
raise RuntimeError("manifest.json не найден в папке архива")
|
raise RuntimeError("manifest.json не найден в папке архива")
|
||||||
|
|
||||||
|
|||||||
+73
-153
@@ -1,95 +1,31 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
ssh.py — SSH-диагностика: поиск ключей, проверка подключения, генерация временных ключей
|
ssh.py — SSH-подготовка для переноса на новый сервер.
|
||||||
|
НЕ ищем ключи на старом сервере — спрашиваем пользователя как подключиться к новому.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import glob
|
|
||||||
import re
|
|
||||||
from core.color import info, warn, success, error as cerror, prompt, confirm
|
from core.color import info, warn, success, error as cerror, prompt, confirm
|
||||||
from core.runner import run, exists
|
from core.runner import run, exists
|
||||||
from core import state
|
|
||||||
|
|
||||||
_SSH_DIR = os.path.expanduser("~/.ssh")
|
|
||||||
|
|
||||||
|
|
||||||
def list_private_keys():
|
def check_ssh_keyless(host, user, port, key_path, timeout=10):
|
||||||
"""Ищет приватные SSH-ключи в ~/.ssh/"""
|
"""Проверяет подключение к новому серверу с данным ключом."""
|
||||||
if not os.path.isdir(_SSH_DIR):
|
|
||||||
return []
|
|
||||||
keys = []
|
|
||||||
# Стандартные паттерны приватных ключей (не .pub)
|
|
||||||
for pattern in ("id_rsa", "id_ed25519", "id_ecdsa", "id_dsa"):
|
|
||||||
for f in glob.glob(os.path.join(_SSH_DIR, pattern)):
|
|
||||||
# Убедимся что это не .pub файл
|
|
||||||
if not f.endswith(".pub") and os.path.isfile(f):
|
|
||||||
# Проверим что есть соответствующий .pub
|
|
||||||
pub = f + ".pub"
|
|
||||||
has_pub = os.path.isfile(pub)
|
|
||||||
keys.append({
|
|
||||||
"private": f,
|
|
||||||
"public": pub if has_pub else None,
|
|
||||||
"type": os.path.basename(f).replace("id_", ""),
|
|
||||||
"has_pub": has_pub,
|
|
||||||
})
|
|
||||||
# Ищем и другие файлы без расширения (которые содержат PRIVATE KEY)
|
|
||||||
for f in glob.glob(os.path.join(_SSH_DIR, "*")):
|
|
||||||
if os.path.isfile(f) and not f.endswith(".pub") and not f.endswith(".config"):
|
|
||||||
try:
|
|
||||||
with open(f, "r", encoding="utf-8", errors="ignore") as fh:
|
|
||||||
first = fh.read(100)
|
|
||||||
if "PRIVATE KEY" in first or "OPENSSH" in first:
|
|
||||||
pub = f + ".pub"
|
|
||||||
has_pub = os.path.isfile(pub)
|
|
||||||
if f not in [k["private"] for k in keys]:
|
|
||||||
keys.append({
|
|
||||||
"private": f,
|
|
||||||
"public": pub if has_pub else None,
|
|
||||||
"type": "unknown",
|
|
||||||
"has_pub": has_pub,
|
|
||||||
})
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return keys
|
|
||||||
|
|
||||||
|
|
||||||
def check_ssh_connectivity(host, user, port, key_path=None, timeout=10):
|
|
||||||
"""Проверяет SSH-соединение. Возвращает (ok, stdout, stderr)."""
|
|
||||||
if not exists("ssh"):
|
if not exists("ssh"):
|
||||||
return (False, "", "ssh не найден")
|
return (False, "ssh не найден")
|
||||||
|
|
||||||
identity = f"-i '{key_path}'" if key_path and os.path.isfile(key_path) else ""
|
identity = f"-i '{key_path}'" if key_path and os.path.isfile(key_path) else ""
|
||||||
cmd = f"ssh {identity} -p {port} -o ConnectTimeout={timeout} -o BatchMode=yes -o StrictHostKeyChecking=accept-new {user}@{host} 'echo migrate-ok'"
|
cmd = f"ssh {identity} -p {port} -o ConnectTimeout={timeout} -o BatchMode=yes -o StrictHostKeyChecking=accept-new {user}@{host} 'echo migrate-ok'"
|
||||||
r = run(cmd, check=False, capture=True, timeout=30)
|
r = run(cmd, check=False, capture=True, timeout=30)
|
||||||
if r.returncode == 0 and "migrate-ok" in r.stdout:
|
if r.returncode == 0 and "migrate-ok" in r.stdout:
|
||||||
return (True, r.stdout, r.stderr)
|
return (True, "OK")
|
||||||
return (False, r.stdout, r.stderr)
|
err = (r.stderr or "").strip()
|
||||||
|
if "passphrase" in err.lower():
|
||||||
|
return (False, "PASSPHRASE")
|
||||||
def test_keys_against_target(host, user, port, keys):
|
return (False, err[:120])
|
||||||
"""Перебирает ключи и проверяет какой работает с target."""
|
|
||||||
if not keys:
|
|
||||||
return None
|
|
||||||
info(f"Проверяем SSH-подключение к {user}@{host}:{port} с найденными ключами ...")
|
|
||||||
working = None
|
|
||||||
for k in keys:
|
|
||||||
label = os.path.basename(k["private"])
|
|
||||||
ok, out, err = check_ssh_connectivity(host, user, port, key_path=k["private"])
|
|
||||||
if ok:
|
|
||||||
success(f" ✓ Ключ '{label}' работает!")
|
|
||||||
working = k
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
# Проверим, не passphrase ли
|
|
||||||
if "passphrase" in err.lower() or "password" in err.lower():
|
|
||||||
warn(f" ! Ключ '{label}' требует passphrase (зашифрован). Пропускаем (нужен ssh-agent).")
|
|
||||||
else:
|
|
||||||
info(f" ✗ Ключ '{label}' не подходит ({err.strip()[:60]}).")
|
|
||||||
return working
|
|
||||||
|
|
||||||
|
|
||||||
def generate_temp_keypair(host_label):
|
def generate_temp_keypair(host_label):
|
||||||
"""Генерирует временную пару ed25519 в /tmp/ для миграции."""
|
"""Генерирует временную пару ed25519 в /tmp/."""
|
||||||
temp_dir = "/tmp/docker-migrate-ssh-keys"
|
temp_dir = "/tmp/docker-migrate-ssh-keys"
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
priv = os.path.join(temp_dir, f"migrate_{host_label}")
|
priv = os.path.join(temp_dir, f"migrate_{host_label}")
|
||||||
@@ -97,64 +33,32 @@ def generate_temp_keypair(host_label):
|
|||||||
info("Генерируем временную SSH-пару ed25519 ...")
|
info("Генерируем временную SSH-пару ed25519 ...")
|
||||||
run(f"ssh-keygen -t ed25519 -C 'docker-migrate-temp' -f '{priv}' -N ''", check=False)
|
run(f"ssh-keygen -t ed25519 -C 'docker-migrate-temp' -f '{priv}' -N ''", check=False)
|
||||||
if os.path.isfile(priv) and os.path.isfile(pub):
|
if os.path.isfile(priv) and os.path.isfile(pub):
|
||||||
success(f"Временная пара создана: {priv}")
|
|
||||||
# Устанавливаем безопасные права
|
|
||||||
os.chmod(priv, 0o600)
|
os.chmod(priv, 0o600)
|
||||||
|
success(f"Временная пара создана: {priv}")
|
||||||
return {"private": priv, "public": pub, "type": "ed25519", "temp": True}
|
return {"private": priv, "public": pub, "type": "ed25519", "temp": True}
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def ssh_copy_id(host, user, port, pubkey_path):
|
def ssh_copy_id(host, user, port, pubkey_path):
|
||||||
"""Копирует публичный ключ на target через ssh-copy-id или вручную."""
|
"""ssh-copy-id на новый сервер. Интерактивно (пароль нового сервера)."""
|
||||||
if exists("ssh-copy-id"):
|
if not exists("ssh-copy-id"):
|
||||||
info("Копируем публичный ключ через ssh-copy-id ...")
|
return False
|
||||||
# Добавляем SSH опции чтобы избежать зависания
|
warn("⚠ Сейчас запустится ssh-copy-id — введите ПАРОЛЬ от НОВОГО сервера ↓")
|
||||||
r = run(
|
r = run(
|
||||||
f"ssh-copy-id -p {port} -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new -i '{pubkey_path}' {user}@{host}",
|
f"ssh-copy-id -p {port} -o ConnectTimeout=30 -o StrictHostKeyChecking=accept-new -i '{pubkey_path}' {user}@{host}",
|
||||||
check=False,
|
check=False, capture=False, timeout=120
|
||||||
timeout=60
|
)
|
||||||
)
|
return r.returncode == 0
|
||||||
if r.returncode == 0:
|
|
||||||
success("Ключ добавлен на target через ssh-copy-id")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
warn(f"ssh-copy-id не удался: {r.stderr.strip()[:120]}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def manual_copy_instructions(host, user, port, pubkey_path):
|
|
||||||
"""Выводит инструкции по ручному добавлению ключа."""
|
|
||||||
print()
|
|
||||||
cerror("SSH-подключение к target не удалось ни с одним ключом.")
|
|
||||||
print()
|
|
||||||
info("Варианты решения:")
|
|
||||||
print(f" {yellow('1')} Добавьте публичный ключ на target вручную:")
|
|
||||||
try:
|
|
||||||
with open(pubkey_path, "r") as f:
|
|
||||||
pubkey_content = f.read().strip()
|
|
||||||
except Exception:
|
|
||||||
pubkey_content = "(не удалось прочитать)"
|
|
||||||
print(f" На {user}@{host} выполните:")
|
|
||||||
print(f" mkdir -p ~/.ssh && chmod 700 ~/.ssh")
|
|
||||||
print(f" echo '{pubkey_content}' >> ~/.ssh/authorized_keys")
|
|
||||||
print(f" chmod 600 ~/.ssh/authorized_keys")
|
|
||||||
print()
|
|
||||||
print(f" {yellow('2')} Или скопируйте через ssh-copy-id (если есть парольный доступ):")
|
|
||||||
print(f" ssh-copy-id -p {port} -i {pubkey_path} {user}@{host}")
|
|
||||||
print()
|
|
||||||
print(f" {yellow('3')} После добавления ключа запустите миграцию заново:")
|
|
||||||
print(f" docker-migrate --resume")
|
|
||||||
print()
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_sshpass():
|
def ensure_sshpass():
|
||||||
"""Проверяет наличие sshpass, при необходимости ставит через apt."""
|
"""Устанавливает sshpass если нужно."""
|
||||||
if exists("sshpass"):
|
if exists("sshpass"):
|
||||||
return True
|
return True
|
||||||
info("sshpass не найден. Устанавливаем ...")
|
info("sshpass не найден. Устанавливаем ...")
|
||||||
r1 = run("apt-get update", check=False, timeout=60)
|
r1 = run("apt-get update", check=False, timeout=60)
|
||||||
if r1.returncode != 0:
|
if r1.returncode != 0:
|
||||||
warn(f"apt-get update не удался: {r1.stderr.strip()[:120]}")
|
warn(f"apt-get update: {r1.stderr.strip()[:120]}")
|
||||||
r = run("apt-get install -y sshpass", check=False, timeout=120)
|
r = run("apt-get install -y sshpass", check=False, timeout=120)
|
||||||
if r.returncode != 0:
|
if r.returncode != 0:
|
||||||
warn(f"Не удалось установить sshpass: {r.stderr.strip()[:120]}")
|
warn(f"Не удалось установить sshpass: {r.stderr.strip()[:120]}")
|
||||||
@@ -162,43 +66,59 @@ def ensure_sshpass():
|
|||||||
return exists("sshpass")
|
return exists("sshpass")
|
||||||
|
|
||||||
|
|
||||||
|
# ...
|
||||||
|
|
||||||
|
|
||||||
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. Найти существующие ключи
|
info("")
|
||||||
keys = list_private_keys()
|
info("Как подключиться к новому серверу?")
|
||||||
if keys:
|
print(" 1) У меня есть приватный SSH-ключ от нового сервера (файл .pem/.key/.ppk)")
|
||||||
info(f"Найдено SSH-ключей: {len(keys)}")
|
print(" 2) У меня есть логин и пароль от нового сервера")
|
||||||
for k in keys:
|
print(" 0) Отмена")
|
||||||
label = os.path.basename(k["private"])
|
choice = prompt("Ваш выбор", default="1").strip()
|
||||||
status = "(есть .pub)" if k["has_pub"] else "(❗ нет .pub!)"
|
|
||||||
print(f" - {label} {status}")
|
|
||||||
|
|
||||||
# 2. Проверить подключение с существующими ключами
|
if choice == "1":
|
||||||
working_key = test_keys_against_target(host, user, port, keys)
|
# Пользователь даёт путь к ключу нового сервера
|
||||||
if working_key:
|
key_path = prompt("Путь к приватному SSH-ключу (например, /root/server.pem)").strip()
|
||||||
return working_key["private"]
|
if not key_path or not os.path.isfile(key_path):
|
||||||
|
raise RuntimeError(f"Файл не найден: {key_path}")
|
||||||
# 3. Если не удалось — спросить о генерации временного ключа
|
ok, why = check_ssh_keyless(host, user, port, key_path)
|
||||||
if not confirm("SSH-подключение не удалось. Сгенерировать временную пару ed25519", default="y"):
|
|
||||||
warn("Продолжаем без ключевой пары. Передача через пароль (или выберите 'нет' и перенесите архив вручную).")
|
|
||||||
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:
|
if ok:
|
||||||
return temp_key["private"]
|
success("Приватный SSH-ключ подходит для нового сервера!")
|
||||||
|
return key_path
|
||||||
|
if why == "PASSPHRASE":
|
||||||
|
warn(f"Приватный SSH-ключ защищён passphrase. Разблокируйте: ssh-add {key_path}")
|
||||||
|
raise RuntimeError("Приватный SSH-ключ зашифрован. Разблокируйте через ssh-add и перезапустите.")
|
||||||
|
raise RuntimeError(f"Приватный SSH-ключ не подходит: {why}")
|
||||||
|
|
||||||
# 5. Fallback на инструкции
|
if choice == "2":
|
||||||
manual_copy_instructions(host, user, port, temp_key["public"])
|
# Пароль нового сервера
|
||||||
raise RuntimeError("SSH-подключение не настроено. Выполните инструкции выше и запустите: docker-migrate --resume")
|
info("")
|
||||||
|
info("Выберите способ:")
|
||||||
|
print(" 1) Ввести пароль от нового сервера — scp через sshpass")
|
||||||
|
print(" 2) Сгенерировать временный SSH-ключ + ssh-copy-id (новый сервер спросит пароль)")
|
||||||
|
sub = prompt("Ваш выбор", default="1").strip()
|
||||||
|
|
||||||
|
if sub == "2":
|
||||||
|
temp = generate_temp_keypair(host.replace(".", "_"))
|
||||||
|
if not temp:
|
||||||
|
raise RuntimeError("Не удалось сгенерировать SSH-ключ")
|
||||||
|
if ssh_copy_id(host, user, port, temp["public"]):
|
||||||
|
ok, _ = check_ssh_keyless(host, user, port, temp["private"])
|
||||||
|
if ok:
|
||||||
|
success("Временный SSH-ключ добавлен на новый сервер!")
|
||||||
|
return temp["private"]
|
||||||
|
# ssh-copy-id не сработал
|
||||||
|
warn("ssh-copy-id не удался. Варианты:")
|
||||||
|
print(f" 1) Вручную на новом сервере: mkdir -p ~/.ssh; echo '$(cat {temp['public']})' >> ~/.ssh/authorized_keys")
|
||||||
|
print(f" 2) Затем: docker-migrate --resume")
|
||||||
|
raise RuntimeError("ssh-copy-id не удался.")
|
||||||
|
|
||||||
|
# sub == "1" или любое другое — возвращаем None, caller запросит пароль
|
||||||
|
return None
|
||||||
|
|
||||||
|
raise RuntimeError("Перенос отменён пользователем.")
|
||||||
|
|||||||
+76
-42
@@ -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,62 @@ 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-команду (capture=True, для скриптов)
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Интерактивный SSH (capture=False, для target-mode и другого интерактивного ввода)
|
||||||
|
def ssh_cmd_interactive(remote_cmd, timeout=300):
|
||||||
|
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, capture=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 +115,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 +146,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. Распаковываем и запускаем (интерактивно, т.к. target-mode спрашивает у пользователя)
|
||||||
|
ssh_cmd_interactive(f"mkdir -p /opt/docker-migrate-tool && tar xzf {tool_remote} -C /opt/docker-migrate-tool")
|
||||||
|
ssh_cmd_interactive(f"cd /opt/docker-migrate-tool && python3 core/main.py --mode=target --remote-dir={remote_dir}", timeout=600)
|
||||||
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")
|
||||||
|
|||||||
Reference in New Issue
Block a user