SPB Git forge
29commits 1branches 0releases
684.0 KBsize
maindefault branch
2 days agolast push
Python 95.8% Shell 4.2%
3.6 KB · 80 lines python
Raw Blame History
1"""SSH / rsync vers les nœuds via le LAN 192.168.2.x (le SSH inter-nœuds Tailscale est bloqué par ACL)."""2import os3import shlex4import subprocess5from . import config67SSH_OPTS = [8    "-o", "BatchMode=yes", "-o", "ConnectTimeout=8", "-o", "StrictHostKeyChecking=no",9    "-o", "UserKnownHostsFile=/dev/null", "-o", "LogLevel=ERROR", "-o", "ServerAliveInterval=15",10    "-i", config.SSH_KEY,11]12# PATH complet sur le nœud (les shells non interactifs n'ont pas Homebrew)13REMOTE_PATH = "export PATH=/opt/homebrew/bin:/opt/homebrew/sbin:$HOME/bin:$HOME/.local/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin; "141516def target(ip):17    return "%s@%s" % (config.user_of(ip), ip)181920def run(ip, cmd, timeout=120, stdin=None, path=True):21    """Exécute `cmd` (bash) sur le nœud. Retourne (rc, stdout, stderr)."""22    full = (REMOTE_PATH if path else "") + cmd23    # le script est d'abord écrit dans un fichier temporaire puis exécuté avec stdin=/dev/null :24    # avec `bash -s`, toute commande lisant stdin (brew, sudo, npm…) avalerait la suite du script.25    wrapper = "f=$(mktemp /tmp/mld.XXXXXX) && cat > \"$f\" && bash \"$f\" </dev/null; rc=$?; rm -f \"$f\"; exit $rc"26    try:27        p = subprocess.run(["ssh"] + SSH_OPTS + [target(ip), wrapper], input=full if stdin is None else stdin,28                           capture_output=True, text=True, timeout=timeout)29        return p.returncode, p.stdout, p.stderr30    except subprocess.TimeoutExpired:31        return 124, "", "timeout after %ss" % timeout323334def run_script(ip, script, timeout=300):35    """Envoie un script complet sur stdin de `bash -s`."""36    return run(ip, "", timeout=timeout, stdin=REMOTE_PATH + "\n" + script)373839def ok(ip, cmd, timeout=60):40    return run(ip, cmd, timeout)[0] == 0414243def rsync_push(local_dir, ip, remote_dir, excludes=(), delete=True, timeout=7200, extra=()):44    """Passerelle -> nœud. remote_dir peut contenir ~ (résolu côté nœud)."""45    args = ["rsync", "-a", "--stats", "-e", "ssh " + " ".join(shlex.quote(o) for o in SSH_OPTS)]46    if delete:47        args.append("--delete")48    for e in excludes:49        args += ["--exclude", e]50    args += list(extra)51    args += ["--rsync-path", "mkdir -p %s && rsync" % shlex.quote(remote_dir), local_dir.rstrip("/") + "/", "%s:%s/" % (target(ip), remote_dir)]52    p = subprocess.run(args, capture_output=True, text=True, timeout=timeout)53    return p.returncode, p.stdout, p.stderr545556def rsync_pull(ip, remote_dir, local_dir, excludes=(), delete=True, timeout=7200):57    """Nœud -> passerelle (staging)."""58    os.makedirs(local_dir, exist_ok=True)59    args = ["rsync", "-a", "--stats", "-e", "ssh " + " ".join(shlex.quote(o) for o in SSH_OPTS)]60    if delete:61        args.append("--delete")62    for e in excludes:63        args += ["--exclude", e]64    args += ["%s:%s/" % (target(ip), remote_dir), local_dir.rstrip("/") + "/"]65    p = subprocess.run(args, capture_output=True, text=True, timeout=timeout)66    return p.returncode, p.stdout, p.stderr676869def scp_to(ip, local_file, remote_path):70    p = subprocess.run(["scp"] + SSH_OPTS + [local_file, "%s:%s" % (target(ip), remote_path)], capture_output=True, text=True)71    return p.returncode == 0727374def write_remote_file(ip, remote_path, content, mode="644"):75    """Écrit un fichier sur le nœud via heredoc (contenu texte)."""76    q = shlex.quote(remote_path)77    # "$(dirname …)" entre guillemets : sinon un chemin avec espace (Library/Application Support) est éclaté en deux mkdir78    script = "mkdir -p \"$(dirname %s)\" && cat > %s <<'__MLD_EOF__'\n%s\n__MLD_EOF__\nchmod %s %s" % (q, q, content, mode, q)79    return run(ip, script, timeout=60)80