#!/usr/bin/env python3 """Recover compose conflict and finish accounting deploy on production.""" from __future__ import annotations import base64 import sys from pathlib import Path import paramiko ROOT = Path(__file__).resolve().parents[1] APP_HOST = "192.168.10.162" APP_USER = "morteza" APP_PASS = "Moli@5404" NGX_HOST = "192.168.10.156" NGX_USER = "torbatyaruser" NGX_PASS = "Moli@5404" REMOTE_DIR = "/home/morteza/torbatyar" def connect(host: str, user: str, password: str) -> paramiko.SSHClient: c = paramiko.SSHClient() c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) c.connect(host, username=user, password=password, timeout=30, allow_agent=False, look_for_keys=False) return c def run(client: paramiko.SSHClient, cmd: str, *, sudo: bool = False, password: str = "", timeout: int = 600) -> tuple[int, str]: cmd = cmd.replace("\r\n", "\n").replace("\r", "\n").strip() + "\n" b64 = base64.b64encode(cmd.encode()).decode() if sudo: full = f"echo {password!r} | sudo -S -p '' bash -c 'echo {b64} | base64 -d | bash'" else: full = f"bash -lc 'echo {b64} | base64 -d | bash'" print(f"\n>>> {'[sudo] ' if sudo else ''}{cmd[:240].replace(chr(10), ' | ')}...") _stdin, stdout, stderr = client.exec_command(full, timeout=timeout, get_pty=True) out = stdout.read().decode("utf-8", errors="replace") err = stderr.read().decode("utf-8", errors="replace") code = stdout.channel.recv_exit_status() text = out + (("\n" + err) if err.strip() else "") printable = text[-6000:] if len(text) > 6000 else text try: print(printable) except UnicodeEncodeError: print(printable.encode("ascii", "replace").decode("ascii")) return code, text def put_text_sudo(client: paramiko.SSHClient, password: str, remote: str, content: str) -> None: b64 = base64.b64encode(content.encode()).decode() cmd = f"mkdir -p $(dirname {remote!r}) && echo {b64} | base64 -d > {remote!r} && chmod 644 {remote!r}" code, _ = run(client, cmd, sudo=True, password=password, timeout=60) if code != 0: raise RuntimeError(f"failed writing {remote}") def main() -> int: app = connect(APP_HOST, APP_USER, APP_PASS) try: # Clean docker name conflicts / stuck removals run( app, f""" set -e cd {REMOTE_DIR} docker ps -a --format '{{{{.ID}}}} {{{{.Names}}}}' || true # remove orphan/conflict core containers by name pattern for c in $(docker ps -aq --filter name=superapp_core_service); do docker rm -f "$c" || true done for c in $(docker ps -aq --filter name=61dc7452ee05); do docker rm -f "$c" || true done docker container prune -f || true """, sudo=True, password=APP_PASS, timeout=180, ) # Ensure DB + env keys + start services code, _ = run( app, f""" set -e cd {REMOTE_DIR} # upsert accounting env if missing grep -q '^NEXT_PUBLIC_ACCOUNTING_API_URL=' .env || echo 'NEXT_PUBLIC_ACCOUNTING_API_URL=https://accounting.torbatyar.ir' >> .env grep -q '^ACCOUNTING_DATABASE_URL=' .env || echo 'ACCOUNTING_DATABASE_URL=postgresql+asyncpg://superapp:superapp_password@postgres:5432/accounting_db' >> .env grep -q '^ACCOUNTING_DATABASE_URL_SYNC=' .env || echo 'ACCOUNTING_DATABASE_URL_SYNC=postgresql+psycopg://superapp:superapp_password@postgres:5432/accounting_db' >> .env grep -q '^KEYCLOAK_SERVER_URL=' .env || echo 'KEYCLOAK_SERVER_URL=http://keycloak:8080' >> .env # force public accounting URL sed -i 's|^NEXT_PUBLIC_ACCOUNTING_API_URL=.*|NEXT_PUBLIC_ACCOUNTING_API_URL=https://accounting.torbatyar.ir|' .env sed -i 's|^KEYCLOAK_SERVER_URL=.*|KEYCLOAK_SERVER_URL=http://keycloak:8080|' .env docker compose up -d postgres sleep 3 docker compose exec -T postgres psql -U superapp -d postgres -c "SELECT 1 FROM pg_database WHERE datname='accounting_db';" | grep -q 1 \\ || docker compose exec -T postgres psql -U superapp -d postgres -c "CREATE DATABASE accounting_db;" docker compose up -d --build --force-recreate accounting-service frontend sleep 5 docker compose ps docker compose logs --tail=60 accounting-service curl -sf http://127.0.0.1:8002/health echo curl -sf http://127.0.0.1:3000/accounting >/dev/null && echo FRONTEND_ACCOUNTING_OK || echo FRONTEND_ACCOUNTING_CHECK """, sudo=True, password=APP_PASS, timeout=1800, ) if code != 0: raise RuntimeError("app recover failed") finally: app.close() ngx = connect(NGX_HOST, NGX_USER, NGX_PASS) try: conf = (ROOT / "infrastructure" / "nginx" / "torbatyar.ir.conf").read_text(encoding="utf-8") put_text_sudo(ngx, NGX_PASS, "/etc/nginx/conf.d/torbatyar.ir.conf", conf) code, _ = run( ngx, r""" set -e mkdir -p /var/www/html certbot certonly --webroot -w /var/www/html \ -d torbatyar.ir -d www.torbatyar.ir \ -d api.torbatyar.ir -d identity.torbatyar.ir -d auth.torbatyar.ir -d accounting.torbatyar.ir \ --non-interactive --agree-tos --email support@torbatyar.ir --expand || true nginx -t systemctl reload nginx echo NGINX_RELOADED curl -skI https://accounting.torbatyar.ir/health | head -n 5 || true curl -sf https://accounting.torbatyar.ir/health || curl -sk https://accounting.torbatyar.ir/health || true """, sudo=True, password=NGX_PASS, timeout=300, ) if code != 0: raise RuntimeError("nginx step failed") finally: ngx.close() print("\n=== RECOVER DEPLOY FINISHED ===") return 0 if __name__ == "__main__": try: raise SystemExit(main()) except Exception as exc: print(f"FATAL: {exc}", file=sys.stderr) raise