sshpass -p schreibt das Passwort in argv, wo es in ps und jedem argv-Trace steht -- die Klasse, an der am 2026-08-22 das Core-Passwort verbrannt ist. Nach gc-transaktion.py (voriger Commit) jetzt auch die fuenf unter routeros/: SSHPASS-Env + sshpass -e. Nachweis, gemessen statt gelesen: - alle 13 subprocess.run mit sshpass tragen env= (per AST geprueft; ohne env waere SSHPASS leer und alles still kaputt) - Signaturen passen zu den Aufrufen (AST-Arity; py_compile findet das NICHT) - echter Lauf des gepatchten Codes gegen den lebenden Core: backup-core.py importiert, IP zur Laufzeit auf 10.40.99.1 -> "name: crs326-core", rc=0 - Passwort in argv? nein. In ps waehrend der Verbindung? nein, 40 Stichproben - Negativkontrolle: derselbe Test mit einem Dummy-Wert ueber die alte -p-Form -> 36 Treffer. Die Probe ist also nicht blind, das "nein" hat Aussagekraft Mitgefixt, weil es im umgebauten Code lag: apply-core-cutover-l3.py griff in run_silent auf ein pw zu, das dort gar nicht existierte -- jeder Fehlerfall endete in einem NameError statt in der gedachten Meldung. Fuer TP30 aendert das NICHTS: der Weg ist zu, der Wert bleibt verbrannt. Posten #1 (MikroTik-admin) bleibt faellig. Im TP so hingeschrieben, damit es niemand als erledigt liest. --- Archivierung (Christoph-Entscheid) --- Beim Fixen fiel auf, dass alle vier Cutover-Skripte auf tote Adressen zeigten: apply-core-cutover-l3.py 10.40.41.211 tot (VLAN41 seit 2026-08-27 weg) backup-core.py 10.40.41.211 tot restore-core.py 192.168.88.1 tot (Werks-Temp-Pfad) backup-ap1.py 10.40.10.1 ANTWORTET -- ist aber der CORE backup-ap1.py war der gefaehrliche: er waere nicht gescheitert, er haette den Core gesichert und die Datei ap1-pre-core-cutover-*.backup genannt. Wer das spaeter zurueckspielt, spielt Core-Config auf einen AP. Entscheid war archivieren, nicht reparieren -- der Cutover ist am 2026-08-08 durch, die Skripte haben ihre Aufgabe erfuellt. Sie liegen jetzt unter routeros/archiv-tp15-cutover/ mit README nach dem Muster von switch/. Die Pfad-Nebenwirkungen des Verschiebens sind ABSICHTLICH nicht repariert (backup-core.py schriebe jetzt ins Archivverzeichnis, apply-core-cutover-l3.py faende seine .rsc nicht mehr) -- sie zu reparieren hiesse, die Skripte wieder lauffaehig zu machen, und das ist gerade nicht die Absicht. Der sshpass-Fix wurde trotzdem VOR dem Verschieben gemacht: Archiv-Code wird abgeschrieben, also soll dort das richtige Muster stehen. import-core-rsc.py bleibt, wo es ist -- als einziges generisch nuetzlich. Sein Default-Host ist ebenfalls tot (--host mitgeben), und in --host steckt ein zweiter latenter Bug: sys.argv[1] wird als RSC-Pfad gelesen, bevor auf --host geprueft wird. NICHT gefixt, eigene Runde. Zeiger gesetzt in: CLAUDE.md (sshpass-Gotcha), teilprojekte.md (TP30), teilprojekte-archiv.md (TP15), Handoff 2026-08-31. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
108 lines
3.4 KiB
Python
108 lines
3.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Kopiert ein beliebiges .rsc auf crs326-core und importiert es via SSH.
|
|
|
|
Usage: python3 import-core-rsc.py <pfad/zur/datei.rsc> [--host 10.40.41.211]
|
|
|
|
Creds kommen aus ~/.claude.json (mcpServers.mikromcp.env) envPrefix CRS326_CORE_.
|
|
|
|
🔴 PASSWORT NIE UEBER argv. `sshpass -p <PW>` schreibt es in die Kommandozeile, wo es
|
|
in `ps` und jedem argv-Trace steht -- genau so ist am 2026-08-22 das Core-Passwort in
|
|
ein Sitzungstranskript geraten und musste rotiert werden. `sshpass -e` liest es aus der
|
|
Umgebung des Kindprozesses. Umgestellt 2026-08-31.
|
|
|
|
⚠️ Der Default-HOST 10.40.41.211 ist TOT (VLAN41 abgeschafft 2026-08-27).
|
|
Der Core ist heute 10.40.99.1. Bewusst nicht stillschweigend geaendert -- s. Handoff 2026-08-31.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
CLAUDe_JSON = Path.home() / ".claude.json"
|
|
RSC_PATH = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else None
|
|
HOST = sys.argv[2] if len(sys.argv) > 2 and sys.argv[1] == "--host" else None
|
|
|
|
|
|
def usage():
|
|
print(__doc__, file=sys.stderr)
|
|
sys.exit(2)
|
|
|
|
|
|
if RSC_PATH is None:
|
|
usage()
|
|
|
|
if not RSC_PATH.exists():
|
|
sys.exit(f"Datei nicht gefunden: {RSC_PATH}")
|
|
|
|
if HOST is None:
|
|
HOST = "10.40.41.211"
|
|
|
|
# Creds aus Claude-Settings laden
|
|
env = json.loads(CLAUDe_JSON.read_text())["mcpServers"]["mikromcp"]["env"]
|
|
get = lambda key, default=None: env.get(key, default)
|
|
|
|
user = get("CRS326_CORE_USER", "admin")
|
|
pw = get("CRS326_CORE_PASS")
|
|
if not pw:
|
|
sys.exit("CRS326_CORE_PASS fehlt in ~/.claude.json")
|
|
|
|
# Das Passwort geht ausschliesslich hierueber an sshpass -- nie als Argument.
|
|
SSH_ENV = dict(os.environ, SSHPASS=pw)
|
|
|
|
# Remote-Dateiname: alphanumerisch + Unterstrich/Punkt/Minus
|
|
remote_name = re.sub(r"[^A-Za-z0-9_.-]", "_", RSC_PATH.name)
|
|
|
|
|
|
def ssh_cmd(remote_command: str) -> list:
|
|
return [
|
|
"sshpass", "-e",
|
|
"ssh",
|
|
"-o", "StrictHostKeyChecking=no",
|
|
"-o", "UserKnownHostsFile=/dev/null",
|
|
"-o", "PubkeyAuthentication=no",
|
|
"-o", "LogLevel=ERROR",
|
|
f"{user}@{HOST}",
|
|
remote_command,
|
|
]
|
|
|
|
|
|
def run_silent(cmd: list):
|
|
r = subprocess.run(cmd, capture_output=True, text=True, env=SSH_ENV)
|
|
if r.returncode != 0:
|
|
out = (r.stdout + r.stderr).replace(pw, "***")
|
|
raise SystemExit(f"SSH-Befehl fehlgeschlagen (rc={r.returncode}):\n{out}")
|
|
return r
|
|
|
|
|
|
# 1) Eventuell vorhandene alte Remote-Datei entfernen
|
|
run_silent(ssh_cmd(f"/file/remove [find name=\"{remote_name}\"]; :delay 500ms"))
|
|
|
|
# 2) SCP
|
|
scp = [
|
|
"sshpass", "-e",
|
|
"scp",
|
|
"-o", "StrictHostKeyChecking=no",
|
|
"-o", "UserKnownHostsFile=/dev/null",
|
|
"-o", "PubkeyAuthentication=no",
|
|
"-o", "LogLevel=ERROR",
|
|
str(RSC_PATH),
|
|
f"{user}@{HOST}:/{remote_name}",
|
|
]
|
|
r = subprocess.run(scp, capture_output=True, text=True, env=SSH_ENV)
|
|
if r.returncode != 0:
|
|
out = (r.stdout + r.stderr).replace(pw, "***")
|
|
raise SystemExit(f"SCP fehlgeschlagen (rc={r.returncode}):\n{out}")
|
|
|
|
# 3) Import — mit verbose, damit wir die Zeile sehen, wenn ein Fehler auftritt
|
|
import_cmd = f"/import file-name={remote_name} verbose=yes"
|
|
res = subprocess.run(ssh_cmd(import_cmd), capture_output=True, text=True, env=SSH_ENV)
|
|
out = (res.stdout + res.stderr).replace(pw, "***")
|
|
if res.returncode != 0:
|
|
raise SystemExit(f"Import fehlgeschlagen (rc={res.returncode}):\n{out}")
|
|
|
|
print(out)
|
|
print(f"OK: {RSC_PATH.name} importiert auf {HOST} als /{remote_name}")
|