SPB Git forge
29commits 1branches 0releases
684.0 KBsize
maindefault branch
2 days agolast push
Python 95.8% Shell 4.2%
9.6 KB · 156 lines python
Raw Blame History
1"""`mld prepare <nœud>` : installe les runtimes manquants (Homebrew), active pm2 au démarrage, pose le marqueur, raccorde le2nœud au MacLustr Tunnel (wg1 → BHS64) et applique `mld harden` (résilience coupure/MAJ)."""3import os4import shlex5from . import config, nodes, ssh678def _sudo_pw(alias=None):9    pwf = config.sudo_pw_file(alias)10    if os.path.exists(pwf):11        return open(pwf).read().strip()12    return None131415def prepare(alias, runtimes=None, ka_helpers=False, tunnel_peer=True, harden=True):16    ip = nodes.ip_of(alias)17    if not ip:18        raise SystemExit("nœud %s injoignable" % alias)19    info = nodes.probe(alias, ip)20    have = info.get("runtimes", {})21    want = runtimes or ["node", "pnpm", "pm2", "python@3.14", "python@3.13", "uv", "git"]22    print("prepare %s (%s) — présents : %s" % (alias, ip, ", ".join(k for k, v in have.items() if v)))23    if not have.get("brew"):24        print("  ✗ Homebrew absent : bootstrap requis (Xcode CLT). Lancer d'abord :  mld bootstrap %s" % alias)25        return False26    missing = [r for r in want if not have.get(r)]27    script = ""28    for r in missing:29        if r in config.BREW_FORMULAE:30            fallback = " || npm install -g pm2 >/dev/null 2>&1" if r == "pm2" else ""31            script += "echo '  brew install %s'; brew install -q %s >/dev/null 2>&1%s || echo '   ! échec %s'\n" % (r, config.BREW_FORMULAE[r], fallback, r)32        elif r in config.BREW_CASKS:33            script += "echo '  brew install --cask %s'; brew install -q --cask %s >/dev/null 2>&1 || echo '   ! échec %s'\n" % (r, config.BREW_CASKS[r], r)34    # marqueur35    script += "[ -f ~/.maclustr-node ] || echo %s > ~/.maclustr-node\n" % alias36    # pm2 startup (launchd) — nécessite sudo37    pw = _sudo_pw(alias)38    # pm2 startup doit tourner sous sudo (écrit le plist + launchctl load) ; l'ancien `${cmd#sudo }` le lançait sans sudo → « à faire manuellement »39    if pw:40        # `pm2 ls` d'abord (crée ~/.pm2 sous l'utilisateur), puis chown après le startup sous sudo : sinon root laisse des fichiers dans ~/.pm2 → EACCES pm2.log41        script += ("mkdir -p ~/Library/LaunchAgents; pm2 ls >/dev/null 2>&1; if ! ls ~/Library/LaunchAgents 2>/dev/null | grep -qi pm2; then "42                   "if echo %s | sudo -S -p '' env PATH=\"$PATH\" pm2 startup launchd -u %s --hp %s >/dev/null 2>&1; then echo '  pm2 startup: ok'; else echo '  pm2 startup: ÉCHEC'; fi; fi\n"43                   "echo %s | sudo -S -p '' chown -R %s ~/.pm2 2>/dev/null\n"44                   "launchctl list 2>/dev/null | grep -qi pm2 || launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/pm2.*.plist >/dev/null 2>&1; "45                   "launchctl list 2>/dev/null | grep -qi pm2 && echo '  pm2 launchd: chargé' || echo '  pm2 launchd: NON chargé'\n"46                   # nœud sans session graphique (loué, M2U64…) : le domaine gui n'existe pas au boot → LaunchDaemon système avec UserName47                   "if %s || ! launchctl print gui/$(id -u) >/dev/null 2>&1; then P=$(ls ~/Library/LaunchAgents/pm2.*.plist 2>/dev/null | head -1); "48                   "if [ -n \"$P\" ]; then L=$(/usr/libexec/PlistBuddy -c 'Print :Label' \"$P\"); D=/Library/LaunchDaemons/$(basename \"$P\"); "49                   "echo %s | sudo -S -p '' sh -c \"cp '$P' '$D' && /usr/libexec/PlistBuddy -c 'Delete :UserName' '$D' 2>/dev/null; /usr/libexec/PlistBuddy -c 'Add :UserName string %s' '$D' && chown root:wheel '$D' && chmod 644 '$D' && launchctl bootout system/$L 2>/dev/null; launchctl bootstrap system '$D' 2>/dev/null; launchctl print system/$L >/dev/null 2>&1 && echo '  pm2 LaunchDaemon (pas de session graphique) : chargé' || echo '  pm2 LaunchDaemon : NON chargé'\"; fi; fi\n") % (50            shlex.quote(pw), config.user_of(alias), config.home_of(alias), shlex.quote(pw), config.user_of(alias),51            # nœuds loués (REMOTE_NODES) : LaunchDaemon systématique, la session graphique n'est pas garantie au boot52            "true" if alias in config.REMOTE_NODES else "false", shlex.quote(pw), config.user_of(alias))53    else:54        script += "echo '  pm2 startup: mot de passe sudo absent (%s)'\n" % config.sudo_pw_file(alias)55    rc, out, err = ssh.run(ip, script, timeout=1800)56    if ka_helpers:57        out += ka_helpers_install(alias, ip)58    print(out)59    if err.strip():60        print("  stderr:", err.strip()[-300:])61    # MacLustr Tunnel : raccorde le nœud au hub BHS64 (wg1) s'il ne l'est pas — nécessaire pour exposer un site62    if tunnel_peer:63        from . import tunnel64        try:65            tunnel.ensure_peer(alias, log=print)66        except tunnel.TunnelError as e:67            print("  tunnel : %s" % e)68    # résilience (auto-login, pas de veille, redémarrage après coupure, MAJ macOS/Tailscale off)69    if harden:70        from . import harden as _harden71        _harden.harden(alias)72    info = nodes.probe(alias, ip)73    still = [r for r in want if not info.get("runtimes", {}).get(r)]74    print("  résultat : %s" % ("tout est là" if not still else "manquent encore " + ", ".join(still)))75    return not still767778def bootstrap(alias):79    """Installe Xcode CLT + Homebrew sans GUI. Active un sudo NOPASSWD temporaire (retiré à la fin). Long (plusieurs Go)."""80    ip = nodes.ip_of(alias)81    if not ip:82        raise SystemExit("nœud %s injoignable" % alias)83    pw = _sudo_pw(alias)84    if not pw:85        raise SystemExit("mot de passe sudo requis dans %s" % config.sudo_pw_file(alias))86    script = r'''87export LC_ALL=C88echo %s | sudo -S -p '' sh -c 'echo "%s ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/mld-bootstrap && chmod 440 /etc/sudoers.d/mld-bootstrap'89trap 'sudo rm -f /etc/sudoers.d/mld-bootstrap' EXIT90if ! xcode-select -p >/dev/null 2>&1; then91  echo '  installation Command Line Tools...'92  touch /tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress93  PROD=$(softwareupdate -l 2>&1 | grep -oE 'Label: Command Line Tools for Xcode[^,]*' | sed 's/^Label: //' | sort -V | tail -1)94  echo "  produit: ${PROD:-introuvable}"95  if [ -n "$PROD" ]; then sudo softwareupdate -i "$PROD" --verbose 2>&1 | tail -3; fi96  rm -f /tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress97  sudo xcode-select --switch /Library/Developer/CommandLineTools 2>/dev/null || true98fi99if [ ! -x /opt/homebrew/bin/brew ]; then100  echo '  installation Homebrew...'101  NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 2>&1 | tail -4102  grep -q 'opt/homebrew/bin/brew shellenv' ~/.zprofile 2>/dev/null || echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile103fi104echo "  CLT: $(xcode-select -p 2>/dev/null || echo absent)  brew: $(/opt/homebrew/bin/brew --version 2>/dev/null | head -1 || echo absent)"105''' % (shlex.quote(pw), config.user_of(alias))106    rc, out, err = ssh.run(ip, script, timeout=5400)107    print(out)108    if err.strip():109        print("  stderr:", err.strip()[-400:])110    return rc == 0111112113def gateway_mdns():114    import subprocess115    name = subprocess.run(["scutil", "--get", "LocalHostName"], capture_output=True, text=True).stdout.strip()116    return (name + ".local") if name else nodes.load_lan()["ips"].get(config.GATEWAY, "")117118119def ka_helpers_install(alias, ip):120    """Pousseur Ka (launchd zsh → git push vers spbgit) + alias SSH `gitsrv` → passerelle + clé autorisée sur la passerelle."""121    assets = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "assets")122    out = ["  ka-helpers sur %s :" % alias]123    home = config.home_of(alias)124    ssh.write_remote_file(ip, "%s/ka-pousseur.sh" % home, open(os.path.join(assets, "ka-pousseur.sh")).read(), mode="755")125    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")126    mdns = gateway_mdns()127    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)128    script = r"""129mkdir -p ~/.ssh ~/ka-guardian-spool; touch ~/.ka-pousseur-repos; chmod 700 ~/.ssh130[ -f ~/.ssh/ka_guardian_ed25519 ] || ssh-keygen -q -t ed25519 -N '' -C "ka-guardian@$(cat ~/.maclustr-node 2>/dev/null || hostname -s)" -f ~/.ssh/ka_guardian_ed25519131# alias gitsrv en TÊTE du config (avant Host *) — remplace un bloc existant132python3 - <<'PYEOF'133import os,re134p=os.path.expanduser('~/.ssh/config'); s=open(p).read() if os.path.exists(p) else ''135s=re.sub(r'(?ms)^Host gitsrv\n(?:[ \t]+.*\n?)*\n?', '', s)136open(p,'w').write(%r + s); os.chmod(p, 0o600); print('  gitsrv -> %s')137PYEOF138cat ~/.ssh/ka_guardian_ed25519.pub139launchctl bootout gui/$(id -u)/com.ka.pousseur >/dev/null 2>&1; launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.ka.pousseur.plist && echo '  pousseur launchd chargé'140""" % (block, mdns)141    rc, o, e = ssh.run(ip, script, timeout=120)142    pub = [l for l in o.splitlines() if l.startswith("ssh-ed25519")]143    out += ["  " + l for l in o.splitlines() if not l.startswith("ssh-ed25519")]144    if pub:145        ak = os.path.expanduser("~/.ssh/authorized_keys")146        cur = open(ak).read() if os.path.exists(ak) else ""147        if pub[0].split()[1] not in cur:148            with open(ak, "a") as f:149                f.write(pub[0].strip() + "\n")150            out.append("  clé %s autorisée sur la passerelle" % (pub[0].split()[-1] if len(pub[0].split()) > 2 else alias))151        else:152            out.append("  clé déjà autorisée sur la passerelle")153    rc, o, e = ssh.run(ip, "ssh -o BatchMode=yes -o ConnectTimeout=6 gitsrv 'echo ok' 2>&1 | tail -1", timeout=30)154    out.append("  test ssh gitsrv : %s" % (o.strip() or e.strip()[-80:]))155    return "\n".join(out) + "\n"156