TorbatYar/scripts/deploy_accounting_recover.py
Mortezakoohjani 12c8615615 Ship enterprise Accounting FE/API with CRUD parity and production wiring.
Adds accounting-service PATCH/archive, fiscal helpers, COA templates and setup status, plus SuperApp Accounting UI (DS, scoreboard, masters, vouchers, ledger, ops modules) with session refresh and HTTPS public API URLs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 15:26:43 +03:30

156 lines
5.8 KiB
Python

#!/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