spb/maclustr-dispatch
Public
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). Les brouillons vont dans4~/dispatch/apps/_drafts/<app>.json ; on les relit, on ajuste, puis on les déplace dans ~/dispatch/apps/.5"""6import json7import os8import re9from . import config, manifest, nodes, ssh1011SUFFIXES = ("-web", "-sync", "-ngrok", "-api", "-tunnel", "-server", "-etl", "-scheduler", "-ingest", "-market",12 "-worker", "-proxy", "-crawler", "-enrichment", "-sitemaps", "-tun-search", "-tun-embed")13ENV_DROP_PREFIX = ("PM2_", "npm_", "__", "TERM", "SSH_", "LC_", "HOMEBREW", "ITERM", "XPC_", "COLOR", "LS_", "SHELL", "PWD", "OLDPWD",14 "PATH", "HOME", "USER", "LOGNAME", "LANG", "TMPDIR", "SECURITYSESSIONID", "COMMAND_MODE", "unique_id", "FPATH",15 "CLAUDE", "OTEL", "DISPLAY", "MallocNanoZone", "ZSH", "EDITOR", "PAGER", "LESS", "NoDefaultCurrentDirectoryInExePath",16 "COREPACK", "AI_AGENT", "OSLogRateLimit", "GIT_", "ANTHROPIC_MODEL", "DISABLE_", "MANPATH", "INFOPATH", "STARSHIP",17 "TERMINFO", "WINDOWID", "NVM", "P9K", "TMUX", "SSH", "_")181920def app_of(name):21 for s in SUFFIXES:22 if name.endswith(s):23 return name[: -len(s)]24 return name252627def clean_env(env, name):28 out = {}29 for k, v in (env or {}).items():30 if k == name or k.startswith(ENV_DROP_PREFIX) or not isinstance(v, str):31 continue32 out[k] = v33 return out343536def import_node(alias, apps_filter=None):37 ip = nodes.ip_of(alias)38 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)39 raw, _, mem = out.partition("__MEM__")40 L = json.loads(raw.strip() or "[]")41 mems = json.loads(mem.strip() or "{}")42 groups = {}43 for p in L:44 e = p.get("pm2_env", {})45 app = app_of(p["name"])46 if apps_filter and app not in apps_filter:47 continue48 g = groups.setdefault(app, {"app": app, "processes": [], "ngrok": None, "cwd": None, "ram": 0})49 g["ram"] += int(mems.get(p["name"], 0)) // (1024 * 1024)50 args = e.get("args") or []51 script = e.get("pm_exec_path") or ""52 if "ngrok" in script or (args and "ngrok" in " ".join(map(str, args))):53 blob = " ".join([script] + [str(a) for a in args])54 mo = re.search(r"--(?:url|domain)=(?:https?://)?([^\s]+)", blob)55 port = re.search(r"\s(\d{2,5})(?:\s|$)", blob)56 g["ngrok"] = {"name": p["name"], "url": mo.group(1) if mo else None, "port": int(port.group(1)) if port else None}57 continue58 g["processes"].append({59 "name": p["name"], "manager": "pm2", "script": script, "args": args,60 "interpreter": e.get("exec_interpreter") if e.get("exec_interpreter") not in (None, "none", "N/A") else None,61 "cwd": e.get("pm_cwd"), "env": clean_env(e.get("env"), p["name"]), "cron_restart": e.get("cron_restart"),62 "autorestart": e.get("autorestart", True), "max_memory_restart": e.get("max_memory_restart"),63 })64 g["cwd"] = g["cwd"] or e.get("pm_cwd")65 drafts = os.path.join(config.APPS_DIR, "_drafts")66 os.makedirs(drafts, exist_ok=True)67 written = []68 for app, g in groups.items():69 cwd = g["cwd"] or ""70 m = json.loads(json.dumps(manifest.DEFAULTS))71 mo = re.match(r"(/Users/[^/]+/(?:apps/)?[^/]+|/opt/[^/]+)", cwd)72 d = mo.group(1) if mo else cwd73 m.update({"app": app, "label": app, "dir": d.replace(config.home_of(alias), "~"), "processes": g["processes"], "ngrok": g["ngrok"],74 "domain": (g["ngrok"] or {}).get("url"), "port": (g["ngrok"] or {}).get("port"), "ram_mb_observed": max(g["ram"], 128)})75 m["requires"]["ports"] = [m["port"]] if m["port"] else []76 m["placement"]["reason"] = "importé de %s" % alias77 manifest.detect_requirements(m)78 p = os.path.join(drafts, app + ".json")79 json.dump(m, open(p, "w"), indent=2, ensure_ascii=False)80 written.append(p)81 return written82