SPB Git forge
29commits 1branches 0releases
684.0 KBsize
maindefault branch
2 days agolast push
Python 95.8% Shell 4.2%
5.3 KB · 159 lines python
Raw Blame History
1"""Registre des déploiements (source de vérité) : ~/dispatch/registry.json.23Diffusion (2026-09-04) : chaque sauvegarde POUSSE une copie du registre aux4abonnés déclarés dans ~/dispatch/subscribers.json, pour que les consommateurs5(console admin-ka, gardiens ka2/ka4/ka6…) connaissent TOUJOURS l'emplacement6réel des apps sans rien coder en dur. Un abonné est un dict :78    {"app": "admin-ka", "path": "data/registry.json"}        # path relatif au dir de l'app9    {"app": "ka2", "path": "~/ka-guardian-spool/registry.json"}10    {"node": "M4M36", "path": "~/x/registry.json"}           # ou un nœud fixe1112« app » est résolu dans le registre lui-même (nœud + dir) : l'abonnement suit13les migrations. Le push est best-effort (les consommateurs re-tirent aussi14périodiquement avec `ssh gitsrv cat ~/dispatch/registry.json`).15"""16import json17import os18import shlex19import sys20import time21from . import config2223SUBSCRIBERS = os.path.join(config.STATE, "subscribers.json")242526def load():27    if os.path.exists(config.REGISTRY):28        return json.load(open(config.REGISTRY))29    return {"updated": None, "gateway": config.GATEWAY, "apps": {}, "history": []}303132def save(r, push=True):33    r["updated"] = time.strftime("%Y-%m-%dT%H:%M:%S")34    r["gateway"] = config.GATEWAY35    tmp = config.REGISTRY + ".tmp"36    json.dump(r, open(tmp, "w"), indent=2, ensure_ascii=False)37    os.replace(tmp, config.REGISTRY)38    if push:39        try:40            push_to_subscribers(r)41        except Exception as e:  # jamais bloquant pour un déploiement42            print("  registre : diffusion aux abonnés échouée (%s)" % e, file=sys.stderr)434445def node_of(app):46    return load()["apps"].get(app, {}).get("node")474849def apps_on(alias):50    return [a for a, v in load()["apps"].items() if v.get("node") == alias]515253def set_app(app, **fields):54    r = load()55    cur = r["apps"].get(app, {})56    cur.update(fields)57    cur["updated"] = time.strftime("%Y-%m-%dT%H:%M:%S")58    r["apps"][app] = cur59    save(r)606162def update_many(changes, push=True):63    """Met à jour plusieurs apps ({app: {champs}}) en une seule sauvegarde/diffusion (status --live, heal)."""64    if not changes:65        return66    r = load()67    now = time.strftime("%Y-%m-%dT%H:%M:%S")68    for app, fields in changes.items():69        cur = r["apps"].get(app, {})70        cur.update(fields)71        cur["updated"] = now72        r["apps"][app] = cur73    save(r, push=push)747576def remove_app(app, note=""):77    r = load()78    if app in r["apps"]:79        r["history"].append({"ts": time.strftime("%Y-%m-%dT%H:%M:%S"), "app": app, "event": "removed", "was": r["apps"][app], "note": note})80        del r["apps"][app]81    save(r)828384def log(app, event, **kw):85    r = load()86    r["history"].append(dict(ts=time.strftime("%Y-%m-%dT%H:%M:%S"), app=app, event=event, **kw))87    r["history"] = r["history"][-500:]88    save(r)899091# ----------------------------------------------------------------- abonnés ---9293def load_subscribers():94    if os.path.exists(SUBSCRIBERS):95        return json.load(open(SUBSCRIBERS))96    return []979899def save_subscribers(subs):100    tmp = SUBSCRIBERS + ".tmp"101    json.dump(subs, open(tmp, "w"), indent=2, ensure_ascii=False)102    os.replace(tmp, SUBSCRIBERS)103104105def _abs(p, ip=None):106    return p.replace("~", config.home_of(ip), 1) if p.startswith("~") else p107108109def resolve_subscriber(sub, r):110    """→ (label, node, ip, chemin absolu) ou lève ValueError."""111    from . import nodes112    apps = r.get("apps", {})113    if sub.get("app"):114        entry = apps.get(sub["app"])115        if not entry or not entry.get("node"):116            raise ValueError("app %s absente du registre" % sub["app"])117        node = entry["node"]118        # IP live d abord : l IP figee au deploiement devient fausse quand le DHCP renumerote le LAN (2026-09-21)119        ip = nodes.ip_of(node, rediscover=False) or entry.get("ip")120        base = entry.get("dir") or "~"121        label = "%s@%s" % (sub["app"], node)122    else:123        node = sub["node"]124        ip = nodes.ip_of(node, rediscover=False)125        base = "~"126        label = node127    if not ip:128        raise ValueError("IP inconnue pour %s" % node)129    path = sub["path"]130    if not (path.startswith("/") or path.startswith("~")):131        path = base.rstrip("/") + "/" + path132    return label, node, ip, _abs(path, ip)133134135def push_to_subscribers(r=None, verbose=True):136    """Copie atomique du registre chez chaque abonné (ssh LAN, 12 s max chacun)."""137    from . import ssh138    r = r or load()139    subs = load_subscribers()140    results = []141    payload = json.dumps(r, indent=2, ensure_ascii=False)142    for sub in subs:143        try:144            label, node, ip, path = resolve_subscriber(sub, r)145        except ValueError as e:146            results.append((sub, False, str(e)))147            if verbose:148                print("  registre → %s : ignoré (%s)" % (sub.get("app") or sub.get("node"), e))149            continue150        q = shlex.quote(path)151        script = ("mkdir -p $(dirname %s) && cat > %s.tmp <<'__MLD_REG__'\n%s\n__MLD_REG__\n"152                  "mv -f %s.tmp %s && echo ok" % (q, q, payload, q, q))153        rc, out, err = ssh.run(ip, script, timeout=12, path=False)154        ok = rc == 0 and "ok" in out155        results.append((sub, ok, (err or out).strip()[:120]))156        if verbose:157            print("  registre → %s:%s %s" % (label, path, "ok" if ok else "ÉCHEC " + (err or out).strip()[:80]))158    return results159