from __future__ import annotations import os import posixpath import stat import tarfile import time import socket from pathlib import Path import paramiko HOST = "2.26.123.84" USER = "root" PASSWORD = os.environ["VPS_PASSWORD"] LOCAL_ROOT = Path(__file__).resolve().parent ARCHIVE = LOCAL_ROOT / "iaai_deploy.tar.gz" REMOTE_DIR = "/root/iaai-parser" REMOTE_ARCHIVE = "/root/iaai_deploy.tar.gz" EXCLUDE_DIRS = { ".git", ".venv", "__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache", "iaai_scraper.egg-info", } EXCLUDE_FILES = {"iaai_deploy.tar.gz", "deploy_vps.py"} def _include(path: Path) -> bool: rel = path.relative_to(LOCAL_ROOT) parts = set(rel.parts) if parts & EXCLUDE_DIRS: return False if path.name in EXCLUDE_FILES: return False if path.suffix in {".pyc", ".pyo"}: return False return True def make_archive() -> None: if ARCHIVE.exists(): ARCHIVE.unlink() with tarfile.open(ARCHIVE, "w:gz") as tf: for path in LOCAL_ROOT.rglob("*"): if not _include(path): continue tf.add(path, arcname=str(path.relative_to(LOCAL_ROOT))) print(f"ARCHIVE_READY {ARCHIVE} {ARCHIVE.stat().st_size} bytes", flush=True) def connect() -> paramiko.SSHClient: last_exc: BaseException | None = None for attempt in range(1, 6): client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) try: print(f"SSH_CONNECT attempt={attempt}", flush=True) client.connect( HOST, username=USER, password=PASSWORD, look_for_keys=False, allow_agent=False, timeout=45, banner_timeout=60, auth_timeout=45, ) return client except (paramiko.SSHException, socket.timeout, OSError) as exc: last_exc = exc client.close() print(f"SSH_CONNECT_RETRY attempt={attempt} error={type(exc).__name__}: {exc}", flush=True) time.sleep(3 * attempt) raise SystemExit(f"SSH_CONNECT_FAILED: {last_exc}") def run(client: paramiko.SSHClient, cmd: str, timeout: int | None = None) -> None: print(f"REMOTE_RUN {cmd}", flush=True) stdin, stdout, stderr = client.exec_command(cmd, timeout=timeout) exit_code = stdout.channel.recv_exit_status() out = stdout.read().decode("utf-8", "replace") err = stderr.read().decode("utf-8", "replace") if out: print(out, end="", flush=True) if err: print(err, end="", flush=True) if exit_code != 0: raise SystemExit(f"REMOTE_FAILED code={exit_code}: {cmd}") def upload_file(sftp: paramiko.SFTPClient, local: Path, remote: str) -> None: total = local.stat().st_size last = 0.0 def cb(done: int, _total: int) -> None: nonlocal last now = time.time() if now - last >= 2 or done == total: print(f"UPLOAD {done}/{total}", flush=True) last = now sftp.put(str(local), remote, callback=cb) def main() -> None: make_archive() client = connect() try: run(client, "echo VPS_OK && hostname && docker --version && docker compose version") sftp = client.open_sftp() try: upload_file(sftp, ARCHIVE, REMOTE_ARCHIVE) finally: sftp.close() run( client, f"mkdir -p {REMOTE_DIR} && cd {REMOTE_DIR} && docker compose down || true && " f"find {REMOTE_DIR} -mindepth 1 -maxdepth 1 ! -name '.env' -exec rm -rf {{}} + && " f"tar -xzf {REMOTE_ARCHIVE} -C {REMOTE_DIR} && rm -f {REMOTE_ARCHIVE}", timeout=300, ) run(client, f"cd {REMOTE_DIR} && docker compose up -d --build", timeout=1800) run(client, f"cd {REMOTE_DIR} && docker compose ps", timeout=120) run(client, "docker logs --since 2m iaai-parser-worker-1 2>&1 | tail -n 120 || docker compose -f /root/iaai-parser/docker-compose.yml logs --since 2m worker | tail -n 120", timeout=120) finally: client.close() if __name__ == "__main__": main()