spb/maclustr-dispatch
Public
1"""CLI `mld` — MacLustr Dispatch (tourne sur la passerelle M1M32)."""2import argparse3import json4import sys5from . import config, deploy, manifest, nodes, picker, registry678def cmd_discover(a):9 nodes.discover()101112def cmd_scan(a):13 nodes.scan(aliases=a.nodes or None)141516def cmd_nodes(a):17 res = nodes.load_scan() or nodes.scan(verbose=False)18 nodes.print_scan(res)192021def cmd_apps(a):22 ms = manifest.load_all()23 r = registry.load()["apps"]24 print("%-18s %-8s %-6s %-30s %-8s %s" % ("app", "nœud", "port", "domaine", "épingle", "runtimes"))25 for name, m in ms.items():26 cur = r.get(name, {}).get("node") or "—"27 print("%-18s %-8s %-6s %-30s %-8s %s" % (name, cur, m.get("port") or "", m.get("domain") or "", m["placement"].get("pin") or "", ",".join(m["requires"]["runtimes"])))282930def cmd_show(a):31 print(json.dumps(manifest.load(a.app), indent=2, ensure_ascii=False))323334def cmd_pick(a):35 m = manifest.load(a.app) if a.app else {"app": "(ad hoc)", "requires": {"runtimes": a.runtime or ["pm2"], "ram_gb": a.ram or 1, "ports": []}, "ram_mb_observed": int((a.ram or 1) * 1024), "size_mb": 0, "placement": {"pin": None, "prefer": None, "avoid": [], "reason": ""}}36 scan = nodes.load_scan() if not a.rescan else nodes.scan(verbose=False)37 rows = picker.rank(m, scan, exclude=a.exclude or ())38 if a.json:39 print(json.dumps(rows, indent=2))40 return41 print("classement pour %s :" % m["app"])42 for r in rows:43 if r["ok"]:44 print(" %-8s score %.3f" % (r["node"], r["score"]))45 elif scan.get(r["node"], {}).get("online") and scan[r["node"]].get("role") == "worker":46 print(" %-8s — %s" % (r["node"], r["why"]))474849def cmd_plan(a):50 ms = manifest.load_all()51 if a.apps:52 ms = {k: v for k, v in ms.items() if k in a.apps}53 scan = nodes.scan(verbose=False) if a.rescan else (nodes.load_scan() or nodes.scan(verbose=False))54 rows = picker.plan(ms, scan, exclude=a.exclude or ())55 if a.json:56 print(json.dumps(rows, indent=2, ensure_ascii=False))57 return58 print("%-19s %-8s %-8s %-6s %7s %s" % ("app", "actuel", "proposé", "score", "taille", "note"))59 counts = {}60 moving_mb = 061 for r in rows:62 counts[r["node"]] = counts.get(r["node"], 0) + 163 mv = r["node"] and r["current"] and r["node"] != r["current"]64 if mv:65 moving_mb += r["size_mb"]66 note = ("épinglé — " + r["reason"]) if r["pin"] else (("alt: " + ", ".join("%s %.2f" % x for x in r["alternatives"])) if r["alternatives"] else "")67 if not r["node"]:68 note = "AUCUN NŒUD : " + " ; ".join("%s (%s)" % b for b in r["blockers"])69 flag = "→" if mv else ("=" if r["node"] else "!")70 print("%-19s %-8s %-8s %-6s %6dM %s %s" % (r["app"], r["current"] or "—", r["node"] or "—", ("%.3f" % r["score"]) if r["score"] is not None else "", r["size_mb"], flag, note))71 print("\nrépartition : " + ", ".join("%s×%s" % (k, v) for k, v in sorted(counts.items(), key=lambda x: -x[1]) if k))72 print("données à déplacer : %.1f Go (→ = migration, = = redéploiement en place)" % (moving_mb / 1024.0))737475def cmd_fetch(a):76 deploy.fetch(a.app, a.from_node, final=a.final)777879def cmd_deploy(a):80 if a.node:81 node = a.node82 else:83 scan = nodes.load_scan() or nodes.scan(verbose=False)84 rows = picker.rank(manifest.load(a.app), scan)85 if not rows or not rows[0]["ok"]:86 raise SystemExit("aucun nœud éligible : " + "; ".join("%s: %s" % (r["node"], r["why"]) for r in rows[:6]))87 node = rows[0]["node"]88 print("nœud choisi : %s (score %.3f)" % (node, rows[0]["score"]))89 try:90 deploy.deploy(a.app, node, reinstall=a.reinstall, skip_sync=a.skip_sync)91 except deploy.DeployError as e:92 raise SystemExit("ÉCHEC : %s" % e)939495def cmd_move(a):96 to = a.to97 if not to:98 scan = nodes.load_scan() or nodes.scan(verbose=False)99 rows = picker.rank(manifest.load(a.app), scan, exclude=[registry.node_of(a.app)] if a.away else ())100 if not rows or not rows[0]["ok"]:101 raise SystemExit("aucun nœud éligible : " + "; ".join("%s: %s" % (r["node"], r["why"]) for r in rows[:6]))102 to = rows[0]["node"]103 print("nœud choisi : %s (score %.3f)" % (to, rows[0]["score"]))104 try:105 deploy.move(a.app, to, from_alias=a.from_node, keep_source=a.keep_source, reinstall=a.reinstall)106 except deploy.DeployError as e:107 raise SystemExit("ÉCHEC : %s" % e)108109110def cmd_stop(a):111 deploy.stop(a.app, a.node or registry.node_of(a.app))112 registry.set_app(a.app, status="stopped")113114115def cmd_start(a):116 node = a.node or registry.node_of(a.app)117 deploy.start(a.app, node)118 ok, det = deploy.health(a.app, node)119 registry.set_app(a.app, node=node, status="online" if ok else "unhealthy", health=det)120 print("santé :", det)121122123def cmd_restart(a):124 cmd_stop(a)125 cmd_start(a)126127128def cmd_retire(a):129 deploy.retire(a.app, a.node or registry.node_of(a.app), remove_dir=not a.keep_dir)130 if not a.keep_registry:131 registry.remove_app(a.app, note=a.note or "")132133134def cmd_status(a):135 if a.live:136 for row in deploy.live_status(a.app):137 print("%-18s %-8s %-30s %-3s %s" % row)138 else:139 print("%-18s %-8s %-6s %-30s %-10s %s" % ("app", "nœud", "port", "domaine", "état", "déployée"))140 for row in deploy.status(a.app):141 print("%-18s %-8s %-6s %-30s %-10s %s" % tuple("" if x is None else x for x in row))142143144def cmd_health(a):145 ok, det = deploy.health(a.app, a.node or registry.node_of(a.app))146 print("OK" if ok else "KO", det)147 sys.exit(0 if ok else 1)148149150def cmd_registry(a):151 print(json.dumps(registry.load(), indent=2, ensure_ascii=False))152153154def cmd_import(a):155 from . import importer156 for p in importer.import_node(a.node, apps_filter=a.apps or None):157 print("brouillon :", p)158159160def cmd_prepare(a):161 from . import prepare162 prepare.prepare(a.node, runtimes=a.runtime or None, ka_helpers=a.ka_helpers)163164165def cmd_bootstrap(a):166 from . import prepare167 prepare.bootstrap(a.node)168169170def cmd_logs(a):171 node = a.node or registry.node_of(a.app)172 from . import ssh173 ip = nodes.ip_of(node)174 names = manifest.pm2_names(manifest.load(a.app))175 rc, out, err = ssh.run(ip, "pm2 logs %s --nostream --lines %d 2>/dev/null" % (" ".join(names), a.lines), timeout=60)176 print(out)177178179def main(argv=None):180 p = argparse.ArgumentParser(prog="mld", description="MacLustr Dispatch — déploie les apps depuis la passerelle %s vers le nœud le plus efficient." % config.GATEWAY)181 sp = p.add_subparsers(dest="cmd", required=True)182 s = sp.add_parser("discover", help="balaye le LAN, met en cache les IP des nœuds"); s.set_defaults(f=cmd_discover)183 s = sp.add_parser("scan", help="sonde ressources + runtimes de chaque nœud"); s.add_argument("nodes", nargs="*"); s.set_defaults(f=cmd_scan)184 s = sp.add_parser("nodes", help="dernier scan"); s.set_defaults(f=cmd_nodes)185 s = sp.add_parser("apps", help="manifestes + emplacement courant"); s.set_defaults(f=cmd_apps)186 s = sp.add_parser("show", help="affiche un manifeste"); s.add_argument("app"); s.set_defaults(f=cmd_show)187 s = sp.add_parser("pick", help="classe les nœuds pour une app (ou un besoin ad hoc)"); s.add_argument("app", nargs="?"); s.add_argument("--ram", type=float); s.add_argument("--runtime", action="append"); s.add_argument("--exclude", action="append"); s.add_argument("--rescan", action="store_true"); s.add_argument("--json", action="store_true"); s.set_defaults(f=cmd_pick)188 s = sp.add_parser("plan", help="plan de placement pour toutes les apps (ou une liste)"); s.add_argument("apps", nargs="*"); s.add_argument("--exclude", action="append"); s.add_argument("--rescan", action="store_true"); s.add_argument("--json", action="store_true"); s.set_defaults(f=cmd_plan)189 s = sp.add_parser("fetch", help="copie l'app d'un nœud vers le staging"); s.add_argument("app"); s.add_argument("--from", dest="from_node", required=True); s.add_argument("--final", action="store_true"); s.set_defaults(f=cmd_fetch)190 s = sp.add_parser("deploy", help="staging -> nœud (choisi automatiquement sans --node)"); s.add_argument("app"); s.add_argument("--node"); s.add_argument("--reinstall", action="store_true"); s.add_argument("--skip-sync", action="store_true"); s.set_defaults(f=cmd_deploy)191 s = sp.add_parser("move", help="migre une app (fetch, stop, deploy, retire)"); s.add_argument("app"); s.add_argument("--to"); s.add_argument("--from", dest="from_node"); s.add_argument("--away", action="store_true", help="exclut le nœud actuel"); s.add_argument("--keep-source", action="store_true"); s.add_argument("--reinstall", action="store_true"); s.set_defaults(f=cmd_move)192 s = sp.add_parser("stop"); s.add_argument("app"); s.add_argument("--node"); s.set_defaults(f=cmd_stop)193 s = sp.add_parser("start"); s.add_argument("app"); s.add_argument("--node"); s.set_defaults(f=cmd_start)194 s = sp.add_parser("restart"); s.add_argument("app"); s.add_argument("--node"); s.set_defaults(f=cmd_restart)195 s = sp.add_parser("retire", help="arrête + efface la copie d'un nœud"); s.add_argument("app"); s.add_argument("--node"); s.add_argument("--keep-dir", action="store_true"); s.add_argument("--keep-registry", action="store_true"); s.add_argument("--note"); s.set_defaults(f=cmd_retire)196 s = sp.add_parser("status"); s.add_argument("app", nargs="?"); s.add_argument("--live", action="store_true"); s.set_defaults(f=cmd_status)197 s = sp.add_parser("health"); s.add_argument("app"); s.add_argument("--node"); s.set_defaults(f=cmd_health)198 s = sp.add_parser("logs"); s.add_argument("app"); s.add_argument("--node"); s.add_argument("--lines", type=int, default=40); s.set_defaults(f=cmd_logs)199 s = sp.add_parser("registry", help="registre JSON complet"); s.set_defaults(f=cmd_registry)200 s = sp.add_parser("import", help="brouillons de manifestes depuis les PM2 d'un nœud"); s.add_argument("node"); s.add_argument("apps", nargs="*"); s.set_defaults(f=cmd_import)201 s = sp.add_parser("prepare", help="installe runtimes/ngrok/pm2-startup sur un nœud"); s.add_argument("node"); s.add_argument("--runtime", action="append"); s.add_argument("--ka-helpers", action="store_true"); s.set_defaults(f=cmd_prepare)202 s = sp.add_parser("bootstrap", help="Xcode CLT + Homebrew sur un nœud vierge (long)"); s.add_argument("node"); s.set_defaults(f=cmd_bootstrap)203 a = p.parse_args(argv)204 a.f(a)205206207if __name__ == "__main__":208 main()209