SPB Git

spb/toit-ka Public

Toit-Ka — louer ou acheter un toit au Québec, un seul endroit (fusion Lou-Ka × Immo-Ka) — www.toit-ka.com

Python 40.2% TypeScript 39% CSS 20.2% HTML 0.7%
3.6 KB · 88 lines python
Raw Blame History
1# Author: Simon-Pierre Boucher2# Contact: contact@spboucher.ai3# Project: Toit-Ka4# -----------------------------------------------------------------------------5# replicate.py : réplication LECTURE SEULE des BD de prod vers ce nœud.6#7#   Lou-Ka  tourne sur M3U96b (~/apps/lou-ka/data/louka.db)8#   Immo-Ka tourne sur M4M64a (~/apps/immo-ka/data/immoka.db)9#   Toit-Ka tourne sur M3U96a et TIRE des snapshots cohérents via le LAN :10#     1. ssh <node> "sqlite3 <db> '.backup /tmp/<nom>-snap.db'"  (snapshot sûr11#        même en cours d'écriture — jamais un rsync direct d'une DB WAL vivante)12#     2. rsync du snapshot vers data/replicas/<nom>.db (échange atomique)13#14#   Config .env (absente en dev -> replicate ne fait rien, l'ETL lit les BD15#   locales pointées par TOITKA_LOUKA_DB / TOITKA_IMMOKA_DB) :16#     TOITKA_REPLICA_SOURCES=louka=192.168.2.82:apps/lou-ka/data/louka.db,immoka=192.168.2.83:apps/immo-ka/data/immoka.db17#     TOITKA_REPLICA_USER=simon-pierreboucher18# -----------------------------------------------------------------------------19from __future__ import annotations2021import os22import subprocess23import time24from pathlib import Path2526from .db import DATA2728REPLICAS = DATA / "replicas"29SSH_OPTS = ["-o", "BatchMode=yes", "-o", "ConnectTimeout=10",30            "-o", "StrictHostKeyChecking=accept-new"]313233def _sources() -> list[tuple[str, str, str]]:34    """[(nom, hôte, chemin distant relatif au $HOME), …] depuis l'environnement."""35    raw = os.environ.get("TOITKA_REPLICA_SOURCES", "").strip()36    out = []37    for part in filter(None, (p.strip() for p in raw.split(","))):38        name, _, rest = part.partition("=")39        host, _, path = rest.partition(":")40        if name and host and path:41            out.append((name.strip(), host.strip(), path.strip()))42    return out434445def _pull(name: str, host: str, remote_db: str, user: str) -> bool:46    t0 = time.time()47    snap = f"/tmp/toitka-{name}-snap.db"48    dest = REPLICAS / f"{name}.db"49    tmp = REPLICAS / f"{name}.db.part"50    # 1. snapshot cohérent côté source (sqlite .backup gère le WAL en cours)51    r = subprocess.run(52        ["ssh", *SSH_OPTS, f"{user}@{host}",53         f"sqlite3 ~/{remote_db} \".backup {snap}\""],54        capture_output=True, text=True, timeout=600)55    if r.returncode != 0:56        print(f"[replicate] {name} : snapshot ÉCHOUÉ — {r.stderr.strip()[:300]}")57        return False58    # 2. transfert (rsync incrémental) puis échange atomique59    r = subprocess.run(60        ["rsync", "-a", "--partial", "-e", "ssh " + " ".join(SSH_OPTS),61         f"{user}@{host}:{snap}", str(tmp)],62        capture_output=True, text=True, timeout=1800)63    if r.returncode != 0 or not tmp.exists():64        print(f"[replicate] {name} : rsync ÉCHOUÉ — {r.stderr.strip()[:300]}")65        tmp.unlink(missing_ok=True)66        return False67    os.replace(tmp, dest)68    subprocess.run(["ssh", *SSH_OPTS, f"{user}@{host}", f"rm -f {snap}"],69                   capture_output=True, timeout=60)70    mb = dest.stat().st_size / 1e671    print(f"[replicate] {name} : {mb:.0f} Mo en {time.time() - t0:.1f} s")72    return True737475def run() -> None:76    sources = _sources()77    if not sources:78        return                      # dev : rien à répliquer, l'ETL lit en local79    REPLICAS.mkdir(parents=True, exist_ok=True)80    user = os.environ.get("TOITKA_REPLICA_USER", os.environ.get("USER", ""))81    for name, host, path in sources:82        try:83            _pull(name, host, path, user)84        except subprocess.TimeoutExpired:85            print(f"[replicate] {name} : délai dépassé")86        except Exception as e:87            print(f"[replicate] {name} : {type(e).__name__}: {e}")88