spb/maclustr-dispatch
Public
Python 95.8%
Shell 4.2%
1"""`mld import <nœud>` : brouillons de manifestes à partir des processus PM2 / launchd vivants d'un nœud.23Regroupe par préfixe de nom (lou-ka-web, lou-ka-sync, lou-ka-ngrok → lou-ka ; un vieux tunnel ngrok devient une route4MacLustr Tunnel `tunnel.domain`). Les brouillons vont dans5~/dispatch/apps/_drafts/<app>.json ; on les relit, on ajuste, puis on les déplace dans ~/dispatch/apps/.6"""7import json8import os9import re10from . import config, manifest, nodes, ssh1112SUFFIXES = ("-web", "-sync", "-ngrok", "-api", "-tunnel", "-server", "-etl", "-scheduler", "-ingest", "-market",13 "-worker", "-proxy", "-crawler", "-enrichment", "-sitemaps", "-tun-search", "-tun-embed")14ENV_DROP_PREFIX = ("PM2_", "npm_", "__", "TERM", "SSH_", "LC_", "HOMEBREW", "ITERM", "XPC_", "COLOR", "LS_", "SHELL", "PWD", "OLDPWD",15 "PATH", "HOME", "USER", "LOGNAME", "LANG", "TMPDIR", "SECURITYSESSIONID", "COMMAND_MODE", "unique_id", "FPATH",16 "CLAUDE", "OTEL", "DISPLAY", "MallocNanoZone", "ZSH", "EDITOR", "PAGER", "LESS", "NoDefaultCurrentDirectoryInExePath",17 "COREPACK", "AI_AGENT", "OSLogRateLimit", "GIT_", "ANTHROPIC_MODEL", "DISABLE_", "MANPATH", "INFOPATH", "STARSHIP",18 "TERMINFO", "WINDOWID", "NVM", "P9K", "TMUX", "SSH", "_")192021def app_of(name):22 for s in SUFFIXES:23 if name.endswith(s):24 return name[: -len(s)]25 return name262728def clean_env(env, name):29 out = {}30 for k, v in (env or {}).items():31 if k == name or k.startswith(ENV_DROP_PREFIX) or not isinstance(v, str):32 continue33 out[k] = v34 return out353637def import_node(alias, apps_filter=None):38 ip = nodes.ip_of(alias)39 rc, out, err = ssh.run(ip, "pm2 jlist 2>/dev/null; echo; echo __MEM__; pm2 jlist 2>/dev/null | python3 -c \"import json,sys; print(json.dumps({p['name']: p.get('monit',{}).get('memory',0) for p in json.load(sys.stdin)}))\"", timeout=60)40 raw, _, mem = out.partition("__MEM__")41 L = json.loads(raw.strip() or "[]")42 mems = json.loads(mem.strip() or "{}")43 groups = {}44 for p in L:45 e = p.get("pm2_env", {})46 app = app_of(p["name"])47 if apps_filter and app not in apps_filter:48 continue49 g = groups.setdefault(app, {"app": app, "processes": [], "ngrok": None, "cwd": None, "ram": 0})50 g["ram"] += int(mems.get(p["name"], 0)) // (1024 * 1024)51 args = e.get("args") or []52 script = e.get("pm_exec_path") or ""53 if "ngrok" in script or (args and "ngrok" in " ".join(map(str, args))):54 blob = " ".join([script] + [str(a) for a in args])55 mo = re.search(r"--(?:url|domain)=(?:https?://)?([^\s]+)", blob)56 port = re.search(r"\s(\d{2,5})(?:\s|$)", blob)57 g["ngrok"] = {"name": p["name"], "url": mo.group(1) if mo else None, "port": int(port.group(1)) if port else None}58 continue59 g["processes"].append({60 "name": p["name"], "manager": "pm2", "script": script, "args": args,61 "interpreter": e.get("exec_interpreter") if e.get("exec_interpreter") not in (None, "none", "N/A") else None,62 "cwd": e.get("pm_cwd"), "env": clean_env(e.get("env"), p["name"]), "cron_restart": e.get("cron_restart"),63 "autorestart": e.get("autorestart", True), "max_memory_restart": e.get("max_memory_restart"),64 })65 g["cwd"] = g["cwd"] or e.get("pm_cwd")66 drafts = os.path.join(config.APPS_DIR, "_drafts")67 os.makedirs(drafts, exist_ok=True)68 written = []69 for app, g in groups.items():70 cwd = g["cwd"] or ""71 m = json.loads(json.dumps(manifest.DEFAULTS))72 mo = re.match(r"(/Users/[^/]+/(?:apps/)?[^/]+|/opt/[^/]+)", cwd)73 d = mo.group(1) if mo else cwd74 # un tunnel ngrok encore vivant sur le nœud devient une route MacLustr Tunnel (le processus ngrok sera supprimé au 1er start)75 ng = g["ngrok"] or {}76 m.update({"app": app, "label": app, "dir": d.replace(config.home_of(alias), "~"), "processes": g["processes"], "ngrok": None,77 "tunnel": ({"domain": ng.get("url"), "gateway": config.TUNNEL_DEFAULT} if ng.get("url") else None),78 "legacy_pm2": [ng["name"]] if ng.get("name") else [],79 "domain": ng.get("url"), "port": ng.get("port"), "ram_mb_observed": max(g["ram"], 128)})80 m["requires"]["ports"] = [m["port"]] if m["port"] else []81 m["placement"]["reason"] = "importé de %s" % alias82 manifest.detect_requirements(m)83 p = os.path.join(drafts, app + ".json")84 json.dump(m, open(p, "w"), indent=2, ensure_ascii=False)85 written.append(p)86 return written87