nœuds hors LAN (REMOTE_NODES) : M1M64 Mac Studio M1 Max loué, user simon, IP publique ; user_of/home_of/sudo_pw_file par nœud ; discover direct ; réservé (--node)
8 changed files +90 −39
modified
README.md
+7 −0
@@ -75,6 +75,13 @@ Personne ne code plus d'emplacement en dur : les consommateurs lisent le registr | ||
| 75 | 75 | 2. `echo <alias> > ~/.maclustr-node` sur le nœud ; ajouter l'alias dans `mld/config.py` (`NODES`). |
| 76 | 76 | 3. `mld discover` → `mld bootstrap <alias>` (si Mac vierge) → `mld prepare <alias>`. |
| 77 | 77 | |
| 78 | +Nœud **hors LAN** (IP publique, autre utilisateur — ex. M1M64, Mac Studio loué, 2026-09-08) : le déclarer aussi | |
| 79 | +dans `REMOTE_NODES` (`host`, `user`, `sudo_pw_file`) ; `discover` s'y connecte directement au lieu de balayer | |
| 80 | +192.168.2.x, `ssh.target`/`home_of`/`sudo_pw_file` suivent l'utilisateur du nœud. Ces nœuds sont `RESERVED` | |
| 81 | +(pas de LAN vers les apps sœurs, pas de gardiens Ka) : déploiement seulement avec `--node`. Si le sshd du | |
| 82 | +fournisseur a `AllowUsers` (ex. `/etc/ssh/sshd_config.d/100-rental.conf`), ajouter un fichier | |
| 83 | +`000-maclustr.conf` avec `AllowUsers <user>` (les directives se cumulent). | |
| 84 | + | |
| 78 | 85 | ## Fichiers |
| 79 | 86 | |
| 80 | 87 | - `mld/config.py` inventaire statique, rôles, chemins, détection des runtimes |
modified
mld/config.py
+34 −1
@@ -44,10 +44,43 @@ NODES = { | ||
| 44 | 44 | "m2m8a": {"cores": 8, "ram_gb": 8, "model": "Mac mini"}, |
| 45 | 45 | "m2m8b": {"cores": 8, "ram_gb": 8, "model": "Mac mini"}, |
| 46 | 46 | "m4md": {"cores": 10, "ram_gb": 16, "model": "Mac mini"}, # ajouté 2026-09-04 (Mac16,10, M4, 100.78.227.79) |
| 47 | + "M1M64": {"cores": 10, "ram_gb": 64, "model": "Mac Studio"}, # ajouté 2026-09-08 (Mac13,1, M1 Max, loué, hors LAN — voir REMOTE_NODES) | |
| 47 | 48 | } |
| 48 | 49 | |
| 50 | +# Nœuds HORS LAN : IP publique fixe + autre utilisateur + autre mot de passe sudo. Ils ne sont pas trouvés par le | |
| 51 | +# balayage 192.168.2.x : `discover` s'y connecte directement et vérifie le marqueur ~/.maclustr-node. | |
| 52 | +REMOTE_NODES = { | |
| 53 | + "M1M64": {"host": "45.74.241.221", "user": "simon", "sudo_pw_file": os.path.join(STATE, ".sudo-M1M64")}, | |
| 54 | +} | |
| 55 | + | |
| 56 | + | |
| 57 | +def _remote(key): | |
| 58 | + for alias, r in REMOTE_NODES.items(): | |
| 59 | + if key in (alias, r["host"]): | |
| 60 | + return r | |
| 61 | + return None | |
| 62 | + | |
| 63 | + | |
| 64 | +def user_of(key=None): | |
| 65 | + """Utilisateur SSH d'un nœud (alias ou IP) — simon-pierreboucher partout sauf REMOTE_NODES.""" | |
| 66 | + r = _remote(key) if key else None | |
| 67 | + return r["user"] if r else USER | |
| 68 | + | |
| 69 | + | |
| 70 | +def home_of(key=None): | |
| 71 | + return "/Users/%s" % user_of(key) | |
| 72 | + | |
| 73 | + | |
| 74 | +def sudo_pw_file(key=None): | |
| 75 | + r = _remote(key) if key else None | |
| 76 | + return r["sudo_pw_file"] if r else SUDO_PW_FILE | |
| 77 | + | |
| 78 | + | |
| 49 | 79 | # Nœuds réservés : inéligibles au placement automatique (sauf app épinglée dessus). |
| 50 | −RESERVED = {"M3U96b": "réservé à hfmarketdata (lac FirstRate Data) depuis le 2026-09-04"} | |
| 80 | +RESERVED = { | |
| 81 | + "M3U96b": "réservé à hfmarketdata (lac FirstRate Data) depuis le 2026-09-04", | |
| 82 | + "M1M64": "nœud distant hors LAN (loué, user simon) : pas de LAN 192.168.2.x ni de gardiens Ka → placement seulement avec --node", | |
| 83 | +} | |
| 51 | 84 | |
| 52 | 85 | # Rôles : gateway = jamais d'app ; worker = éligible. Un nœud absent du scan est « offline ». |
| 53 | 86 | ROLES = {alias: ("gateway" if alias == GATEWAY else "worker") for alias in NODES} |
modified
mld/deploy.py
+22 −20
@@ -14,11 +14,12 @@ class DeployError(Exception): | ||
| 14 | 14 | |
| 15 | 15 | def _ctx(alias, ip): |
| 16 | 16 | lan = nodes.load_lan()["ips"] |
| 17 | − return {"HOME": "/Users/%s" % config.USER, "NODE": alias, "LAN_IP": ip, "ips": lan, "USER": config.USER} | |
| 17 | + return {"HOME": config.home_of(ip), "NODE": alias, "LAN_IP": ip, "ips": lan, "USER": config.user_of(ip)} | |
| 18 | 18 | |
| 19 | 19 | |
| 20 | −def _abs(p): | |
| 21 | − return p.replace("~", "/Users/%s" % config.USER, 1) if p.startswith("~") else p | |
| 20 | +def _abs(p, ip=None): | |
| 21 | + """~ → home du nœud (dépend de l'utilisateur : simon-pierreboucher, ou celui d'un REMOTE_NODE).""" | |
| 22 | + return p.replace("~", config.home_of(ip), 1) if p.startswith("~") else p | |
| 22 | 23 | |
| 23 | 24 | |
| 24 | 25 | def _log(app, msg): |
@@ -39,10 +40,11 @@ def _ensure_dir(ip, d): | ||
| 39 | 40 | """Crée le répertoire cible ; sous /opt (racine root) passe par sudo -S avec le mot de passe de ~/dispatch/.sudo.""" |
| 40 | 41 | if ssh.ok(ip, "mkdir -p %s 2>/dev/null && test -w %s" % (shlex.quote(d), shlex.quote(d))): |
| 41 | 42 | return |
| 42 | − pw = open(config.SUDO_PW_FILE).read().strip() if os.path.exists(config.SUDO_PW_FILE) else None | |
| 43 | + pwf = config.sudo_pw_file(ip) | |
| 44 | + pw = open(pwf).read().strip() if os.path.exists(pwf) else None | |
| 43 | 45 | if not pw: |
| 44 | − raise DeployError("impossible de créer %s (sudo requis, %s absent)" % (d, config.SUDO_PW_FILE)) | |
| 45 | − rc, out, err = ssh.run(ip, "echo %s | sudo -S -p '' mkdir -p %s && echo %s | sudo -S -p '' chown %s %s" % (shlex.quote(pw), shlex.quote(d), shlex.quote(pw), config.USER, shlex.quote(d))) | |
| 46 | + raise DeployError("impossible de créer %s (sudo requis, %s absent)" % (d, pwf)) | |
| 47 | + rc, out, err = ssh.run(ip, "echo %s | sudo -S -p '' mkdir -p %s && echo %s | sudo -S -p '' chown %s %s" % (shlex.quote(pw), shlex.quote(d), shlex.quote(pw), config.user_of(ip), shlex.quote(d))) | |
| 46 | 48 | if rc != 0: |
| 47 | 49 | raise DeployError("création de %s impossible : %s" % (d, err.strip()[-200:])) |
| 48 | 50 | |
@@ -57,24 +59,24 @@ def fetch(app, from_alias, final=False): | ||
| 57 | 59 | ip = _ip(from_alias) |
| 58 | 60 | _log(app, "fetch %s depuis %s (%s)%s" % (m["dir"], from_alias, ip, " [final]" if final else "")) |
| 59 | 61 | excl = SYNC_BASE_EXCLUDES + list(m.get("sync_excludes") or []) |
| 60 | − rc, out, err = ssh.rsync_pull(ip, _abs(m["dir"]), os.path.join(stage_dir(app), "dir"), excludes=excl) | |
| 62 | + rc, out, err = ssh.rsync_pull(ip, _abs(m["dir"], ip), os.path.join(stage_dir(app), "dir"), excludes=excl) | |
| 61 | 63 | if rc == 23 and not final: |
| 62 | 64 | _log(app, " avertissement pré-copie à chaud (fichiers en cours d'écriture, repris au delta final) : %s" % err.strip().splitlines()[0][:120]) |
| 63 | 65 | elif rc not in (0, 24): |
| 64 | 66 | raise DeployError("rsync fetch a échoué (%s): %s" % (rc, err.strip()[-300:])) |
| 65 | 67 | for extra in m.get("extra_paths") or []: |
| 66 | 68 | dest = os.path.join(stage_dir(app), "extra", extra.strip("~/").replace("/", "__")) |
| 67 | − rc2, out2, err2 = ssh.run(ip, "test -e %s" % shlex.quote(_abs(extra))) | |
| 69 | + rc2, out2, err2 = ssh.run(ip, "test -e %s" % shlex.quote(_abs(extra, ip))) | |
| 68 | 70 | if rc2 != 0: |
| 69 | 71 | _log(app, " extra absent sur la source, ignoré : %s" % extra) |
| 70 | 72 | continue |
| 71 | − isdir = ssh.ok(ip, "test -d %s" % shlex.quote(_abs(extra))) | |
| 73 | + isdir = ssh.ok(ip, "test -d %s" % shlex.quote(_abs(extra, ip))) | |
| 72 | 74 | if isdir: |
| 73 | − rc2, out2, err2 = ssh.rsync_pull(ip, _abs(extra), dest, delete=True) | |
| 75 | + rc2, out2, err2 = ssh.rsync_pull(ip, _abs(extra, ip), dest, delete=True) | |
| 74 | 76 | else: |
| 75 | 77 | os.makedirs(dest, exist_ok=True) |
| 76 | 78 | import subprocess |
| 77 | − subprocess.run(["scp", "-p"] + ssh.SSH_OPTS + ["%s:%s" % (ssh.target(ip), _abs(extra)), os.path.join(dest, os.path.basename(extra))], capture_output=True) | |
| 79 | + subprocess.run(["scp", "-p"] + ssh.SSH_OPTS + ["%s:%s" % (ssh.target(ip), _abs(extra, ip)), os.path.join(dest, os.path.basename(extra))], capture_output=True) | |
| 78 | 80 | json.dump({"app": app, "from": from_alias, "ip": ip, "ts": time.strftime("%Y-%m-%dT%H:%M:%S"), "final": final}, |
| 79 | 81 | open(os.path.join(stage_dir(app), "fetch.json"), "w"), indent=2) |
| 80 | 82 | stat = [l for l in out.splitlines() if "Total transferred file size" in l or "Number of regular files transferred" in l or "Number of files transferred" in l or "Total bytes sent" in l] |
@@ -112,7 +114,7 @@ def stop(app, alias, keep_dir=True): | ||
| 112 | 114 | def _install_hooks(m, ip, ctx): |
| 113 | 115 | """Commandes post-sync : réécritures .env, hooks, inscription pousseur.""" |
| 114 | 116 | script = "" |
| 115 | − d = _abs(m["dir"]) | |
| 117 | + d = _abs(m["dir"], ip) | |
| 116 | 118 | for fname, kv in (m.get("env_overrides") or {}).items(): |
| 117 | 119 | target = os.path.join(d, fname) |
| 118 | 120 | kv = manifest.render(kv, ctx) |
@@ -140,7 +142,7 @@ def deploy(app, alias, reinstall=False, skip_sync=False): | ||
| 140 | 142 | if missing: |
| 141 | 143 | raise DeployError("runtimes manquants sur %s : %s (→ `mld prepare %s`)" % (alias, ", ".join(missing), alias)) |
| 142 | 144 | # 2. sync |
| 143 | − d = _abs(m["dir"]) | |
| 145 | + d = _abs(m["dir"], ip) | |
| 144 | 146 | if not skip_sync: |
| 145 | 147 | _ensure_dir(ip, d) |
| 146 | 148 | excl = SYNC_BASE_EXCLUDES + list(m.get("sync_excludes") or []) |
@@ -154,11 +156,11 @@ def deploy(app, alias, reinstall=False, skip_sync=False): | ||
| 154 | 156 | if not os.path.exists(src): |
| 155 | 157 | continue |
| 156 | 158 | if os.path.isdir(src) and not os.path.exists(os.path.join(src, os.path.basename(extra))): |
| 157 | − ssh.rsync_push(src, ip, _abs(extra), delete=False) | |
| 159 | + ssh.rsync_push(src, ip, _abs(extra, ip), delete=False) | |
| 158 | 160 | else: |
| 159 | 161 | import subprocess |
| 160 | − ssh.run(ip, "mkdir -p %s" % shlex.quote(os.path.dirname(_abs(extra)))) | |
| 161 | − subprocess.run(["scp", "-p"] + ssh.SSH_OPTS + [os.path.join(src, os.path.basename(extra)), "%s:%s" % (ssh.target(ip), _abs(extra))], capture_output=True) | |
| 162 | + ssh.run(ip, "mkdir -p %s" % shlex.quote(os.path.dirname(_abs(extra, ip)))) | |
| 163 | + subprocess.run(["scp", "-p"] + ssh.SSH_OPTS + [os.path.join(src, os.path.basename(extra)), "%s:%s" % (ssh.target(ip), _abs(extra, ip))], capture_output=True) | |
| 162 | 164 | _log(app, " extra: %s" % extra) |
| 163 | 165 | # 3. install (optionnel) + hooks |
| 164 | 166 | script = "cd %s || exit 3\n" % shlex.quote(d) |
@@ -197,7 +199,7 @@ def start(app, alias, m=None, ctx=None, ip=None): | ||
| 197 | 199 | ip = ip or _ip(alias) |
| 198 | 200 | ctx = ctx or _ctx(alias, ip) |
| 199 | 201 | _log(app, "start sur %s" % alias) |
| 200 | − run_dir = _abs(config.RUN_DIR) | |
| 202 | + run_dir = _abs(config.RUN_DIR, ip) | |
| 201 | 203 | eco = render.ecosystem(m, ctx) |
| 202 | 204 | eco_path = "%s/%s.config.cjs" % (run_dir, app) |
| 203 | 205 | ssh.write_remote_file(ip, eco_path, eco, mode="600") |
@@ -212,7 +214,7 @@ def start(app, alias, m=None, ctx=None, ip=None): | ||
| 212 | 214 | ssh.write_remote_file(ip, ppath, pl, mode="644") |
| 213 | 215 | script += "launchctl bootout gui/$(id -u)/%s >/dev/null 2>&1; launchctl bootstrap gui/$(id -u) %s && echo ' launchd: %s chargé' || echo ' launchd: %s ÉCHEC'\n" % (item["label"], shlex.quote(ppath), item["label"], item["label"]) |
| 214 | 216 | for hook in (m.get("hooks") or {}).get("post_start") or []: |
| 215 | − script += "cd %s && { %s ; }\n" % (shlex.quote(_abs(m["dir"])), manifest.render(hook, ctx)) | |
| 217 | + script += "cd %s && { %s ; }\n" % (shlex.quote(_abs(m["dir"], ip)), manifest.render(hook, ctx)) | |
| 216 | 218 | rc, out, err = ssh.run(ip, script, timeout=300) |
| 217 | 219 | for l in out.splitlines(): |
| 218 | 220 | _log(app, l) |
@@ -262,8 +264,8 @@ def retire(app, alias, remove_dir=True): | ||
| 262 | 264 | ip = _ip(alias) |
| 263 | 265 | stop(app, alias) |
| 264 | 266 | if remove_dir: |
| 265 | − d = _abs(m["dir"]) | |
| 266 | − rc, out, err = ssh.run(ip, "rm -rf %s && echo ' rm -rf %s'; rm -f %s/%s.config.cjs; grep -v -x %s ~/.ka-pousseur-repos > ~/.ka-pousseur-repos.tmp 2>/dev/null && mv ~/.ka-pousseur-repos.tmp ~/.ka-pousseur-repos; true" % (shlex.quote(d), d, _abs(config.RUN_DIR), app, shlex.quote(m["dir"])), timeout=600) | |
| 267 | + d = _abs(m["dir"], ip) | |
| 268 | + rc, out, err = ssh.run(ip, "rm -rf %s && echo ' rm -rf %s'; rm -f %s/%s.config.cjs; grep -v -x %s ~/.ka-pousseur-repos > ~/.ka-pousseur-repos.tmp 2>/dev/null && mv ~/.ka-pousseur-repos.tmp ~/.ka-pousseur-repos; true" % (shlex.quote(d), d, _abs(config.RUN_DIR, ip), app, shlex.quote(m["dir"])), timeout=600) | |
| 267 | 269 | for l in out.splitlines(): |
| 268 | 270 | _log(app, l) |
| 269 | 271 | registry.log(app, "retired", node=alias) |
modified
mld/importer.py
+1 −1
@@ -70,7 +70,7 @@ def import_node(alias, apps_filter=None): | ||
| 70 | 70 | m = json.loads(json.dumps(manifest.DEFAULTS)) |
| 71 | 71 | mo = re.match(r"(/Users/[^/]+/(?:apps/)?[^/]+|/opt/[^/]+)", cwd) |
| 72 | 72 | d = mo.group(1) if mo else cwd |
| 73 | − m.update({"app": app, "label": app, "dir": d.replace("/Users/%s" % config.USER, "~"), "processes": g["processes"], "ngrok": g["ngrok"], | |
| 73 | + m.update({"app": app, "label": app, "dir": d.replace(config.home_of(alias), "~"), "processes": g["processes"], "ngrok": g["ngrok"], | |
| 74 | 74 | "domain": (g["ngrok"] or {}).get("url"), "port": (g["ngrok"] or {}).get("port"), "ram_mb_observed": max(g["ram"], 128)}) |
| 75 | 75 | m["requires"]["ports"] = [m["port"]] if m["port"] else [] |
| 76 | 76 | m["placement"]["reason"] = "importé de %s" % alias |
modified
mld/nodes.py
+6 −1
@@ -47,7 +47,7 @@ def _identify(ip): | ||
| 47 | 47 | |
| 48 | 48 | |
| 49 | 49 | def discover(rng=range(1, 255), verbose=True): |
| 50 | − """Balaye 192.168.2.0/24 en SSH (clé passerelle) et lit le marqueur ~/.maclustr-node.""" | |
| 50 | + """Balaye 192.168.2.0/24 en SSH (clé passerelle) et lit le marqueur ~/.maclustr-node ; ajoute les REMOTE_NODES joignables.""" | |
| 51 | 51 | ips = [config.LAN_PREFIX + str(i) for i in rng] |
| 52 | 52 | found = {} |
| 53 | 53 | with ThreadPoolExecutor(max_workers=48) as ex: |
@@ -59,6 +59,11 @@ def discover(rng=range(1, 255), verbose=True): | ||
| 59 | 59 | alias = a |
| 60 | 60 | break |
| 61 | 61 | found[alias] = ip |
| 62 | + # nœuds hors LAN : connexion directe à l'IP publique, le marqueur doit confirmer l'alias | |
| 63 | + for alias, r in config.REMOTE_NODES.items(): | |
| 64 | + _, seen = _identify(r["host"]) | |
| 65 | + if seen and seen.lower() == alias.lower(): | |
| 66 | + found[alias] = r["host"] | |
| 62 | 67 | lan = load_lan() |
| 63 | 68 | lan["ips"] = found |
| 64 | 69 | save_lan(lan) |
modified
mld/prepare.py
+16 −12
@@ -4,9 +4,10 @@ import shlex | ||
| 4 | 4 | from . import config, nodes, ssh |
| 5 | 5 | |
| 6 | 6 | |
| 7 | −def _sudo_pw(): | |
| 8 | − if os.path.exists(config.SUDO_PW_FILE): | |
| 9 | − return open(config.SUDO_PW_FILE).read().strip() | |
| 7 | +def _sudo_pw(alias=None): | |
| 8 | + pwf = config.sudo_pw_file(alias) | |
| 9 | + if os.path.exists(pwf): | |
| 10 | + return open(pwf).read().strip() | |
| 10 | 11 | return None |
| 11 | 12 | |
| 12 | 13 | |
@@ -32,14 +33,14 @@ def prepare(alias, runtimes=None, ka_helpers=False): | ||
| 32 | 33 | # config ngrok (authtoken) depuis la passerelle |
| 33 | 34 | if "ngrok" in missing or not have.get("ngrok"): |
| 34 | 35 | if os.path.exists(config.NGROK_CFG): |
| 35 | − rc0, _, _ = ssh.write_remote_file(ip, "/Users/%s/Library/Application Support/ngrok/ngrok.yml" % config.USER, open(config.NGROK_CFG).read(), mode="600") | |
| 36 | + rc0, _, _ = ssh.write_remote_file(ip, "%s/Library/Application Support/ngrok/ngrok.yml" % config.home_of(alias), open(config.NGROK_CFG).read(), mode="600") | |
| 36 | 37 | script += "echo ' ngrok.yml %s'\n" % ("copié" if rc0 == 0 else "ÉCHEC de copie") |
| 37 | 38 | # marqueur |
| 38 | 39 | script += "[ -f ~/.maclustr-node ] || echo %s > ~/.maclustr-node\n" % alias |
| 39 | 40 | # pm2 startup (launchd) — nécessite sudo |
| 40 | − pw = _sudo_pw() | |
| 41 | − script += "if ! ls ~/Library/LaunchAgents 2>/dev/null | grep -qi pm2; then cmd=$(pm2 startup launchd -u %s --hp /Users/%s 2>/dev/null | grep '^sudo' | head -1); if [ -n \"$cmd\" ]; then %s echo \" pm2 startup: $( ${cmd#sudo } >/dev/null 2>&1 && echo ok || echo 'à faire manuellement')\"; fi; fi\n" % ( | |
| 42 | − config.USER, config.USER, ("echo %s | sudo -S -v 2>/dev/null;" % shlex.quote(pw)) if pw else "") | |
| 41 | + pw = _sudo_pw(alias) | |
| 42 | + script += "if ! ls ~/Library/LaunchAgents 2>/dev/null | grep -qi pm2; then cmd=$(pm2 startup launchd -u %s --hp %s 2>/dev/null | grep '^sudo' | head -1); if [ -n \"$cmd\" ]; then %s echo \" pm2 startup: $( ${cmd#sudo } >/dev/null 2>&1 && echo ok || echo 'à faire manuellement')\"; fi; fi\n" % ( | |
| 43 | + config.user_of(alias), config.home_of(alias), ("echo %s | sudo -S -v 2>/dev/null;" % shlex.quote(pw)) if pw else "") | |
| 43 | 44 | rc, out, err = ssh.run(ip, script, timeout=1800) |
| 44 | 45 | if ka_helpers: |
| 45 | 46 | out += ka_helpers_install(alias, ip) |
@@ -55,9 +56,11 @@ def prepare(alias, runtimes=None, ka_helpers=False): | ||
| 55 | 56 | def bootstrap(alias): |
| 56 | 57 | """Installe Xcode CLT + Homebrew sans GUI. Active un sudo NOPASSWD temporaire (retiré à la fin). Long (plusieurs Go).""" |
| 57 | 58 | ip = nodes.ip_of(alias) |
| 58 | − pw = _sudo_pw() | |
| 59 | + if not ip: | |
| 60 | + raise SystemExit("nœud %s injoignable" % alias) | |
| 61 | + pw = _sudo_pw(alias) | |
| 59 | 62 | if not pw: |
| 60 | − raise SystemExit("mot de passe sudo requis dans %s" % config.SUDO_PW_FILE) | |
| 63 | + raise SystemExit("mot de passe sudo requis dans %s" % config.sudo_pw_file(alias)) | |
| 61 | 64 | script = r''' |
| 62 | 65 | export LC_ALL=C |
| 63 | 66 | echo %s | sudo -S -p '' sh -c 'echo "%s ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/mld-bootstrap && chmod 440 /etc/sudoers.d/mld-bootstrap' |
@@ -77,7 +80,7 @@ if [ ! -x /opt/homebrew/bin/brew ]; then | ||
| 77 | 80 | grep -q 'opt/homebrew/bin/brew shellenv' ~/.zprofile 2>/dev/null || echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile |
| 78 | 81 | fi |
| 79 | 82 | echo " CLT: $(xcode-select -p 2>/dev/null || echo absent) brew: $(/opt/homebrew/bin/brew --version 2>/dev/null | head -1 || echo absent)" |
| 80 | −''' % (shlex.quote(pw), config.USER) | |
| 83 | +''' % (shlex.quote(pw), config.user_of(alias)) | |
| 81 | 84 | rc, out, err = ssh.run(ip, script, timeout=5400) |
| 82 | 85 | print(out) |
| 83 | 86 | if err.strip(): |
@@ -95,8 +98,9 @@ def ka_helpers_install(alias, ip): | ||
| 95 | 98 | """Pousseur Ka (launchd zsh → git push vers spbgit) + alias SSH `gitsrv` → passerelle + clé autorisée sur la passerelle.""" |
| 96 | 99 | assets = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "assets") |
| 97 | 100 | out = [" ka-helpers sur %s :" % alias] |
| 98 | − ssh.write_remote_file(ip, "/Users/%s/ka-pousseur.sh" % config.USER, open(os.path.join(assets, "ka-pousseur.sh")).read(), mode="755") | |
| 99 | − ssh.write_remote_file(ip, "/Users/%s/Library/LaunchAgents/com.ka.pousseur.plist" % config.USER, open(os.path.join(assets, "com.ka.pousseur.plist")).read(), mode="644") | |
| 101 | + home = config.home_of(alias) | |
| 102 | + ssh.write_remote_file(ip, "%s/ka-pousseur.sh" % home, open(os.path.join(assets, "ka-pousseur.sh")).read(), mode="755") | |
| 103 | + ssh.write_remote_file(ip, "%s/Library/LaunchAgents/com.ka.pousseur.plist" % home, open(os.path.join(assets, "com.ka.pousseur.plist")).read(), mode="644") | |
| 100 | 104 | mdns = gateway_mdns() |
| 101 | 105 | block = "Host gitsrv\n HostName %s\n User %s\n IdentityFile ~/.ssh/ka_guardian_ed25519\n IdentitiesOnly yes\n ControlMaster no\n ControlPath none\n StrictHostKeyChecking no\n UserKnownHostsFile /dev/null\n LogLevel ERROR\n\n" % (mdns, config.USER) |
| 102 | 106 | script = r""" |
modified
mld/registry.py
+3 −3
@@ -88,8 +88,8 @@ def save_subscribers(subs): | ||
| 88 | 88 | os.replace(tmp, SUBSCRIBERS) |
| 89 | 89 | |
| 90 | 90 | |
| 91 | −def _abs(p): | |
| 92 | − return p.replace("~", "/Users/%s" % config.USER, 1) if p.startswith("~") else p | |
| 91 | +def _abs(p, ip=None): | |
| 92 | + return p.replace("~", config.home_of(ip), 1) if p.startswith("~") else p | |
| 93 | 93 | |
| 94 | 94 | |
| 95 | 95 | def resolve_subscriber(sub, r): |
@@ -114,7 +114,7 @@ def resolve_subscriber(sub, r): | ||
| 114 | 114 | path = sub["path"] |
| 115 | 115 | if not (path.startswith("/") or path.startswith("~")): |
| 116 | 116 | path = base.rstrip("/") + "/" + path |
| 117 | − return label, node, ip, _abs(path) | |
| 117 | + return label, node, ip, _abs(path, ip) | |
| 118 | 118 | |
| 119 | 119 | |
| 120 | 120 | def push_to_subscribers(r=None, verbose=True): |
modified
mld/ssh.py
+1 −1
@@ -14,7 +14,7 @@ REMOTE_PATH = "export PATH=/opt/homebrew/bin:/opt/homebrew/sbin:$HOME/bin:$HOME/ | ||
| 14 | 14 | |
| 15 | 15 | |
| 16 | 16 | def target(ip): |
| 17 | − return "%s@%s" % (config.USER, ip) | |
| 17 | + return "%s@%s" % (config.user_of(ip), ip) | |
| 18 | 18 | |
| 19 | 19 | |
| 20 | 20 | def run(ip, cmd, timeout=120, stdin=None, path=True): |
| 21 | 21 | |