Files
bf-infra/routeros/import-core-rsc.py
Christoph Emsenhuber 6f8ba8c8b5 sshpass -p repo-weit weg + vier TP15-Cutover-Skripte archiviert
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>
2026-08-31 08:34:08 +02:00

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}")