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>
This commit is contained in:
62
routeros/archiv-tp15-cutover/README.md
Normal file
62
routeros/archiv-tp15-cutover/README.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# 📦 ARCHIV — TP15/TP21.1 Core-Cutover (2026-08-06 … 2026-08-08)
|
||||
|
||||
> **Dieses Verzeichnis ist Historie. Die Skripte sind ERLEDIGT und werden nicht mehr ausgeführt.**
|
||||
> Sie haben den Umzug des L3-Routers von **ap1** auf **`crs326-core`** begleitet — Backup davor,
|
||||
> Staged-Config einspielen, Restore als Rückweg. Der Cutover ist am **2026-08-08** durchgezogen;
|
||||
> seither ist `crs326-core` der produktive L3-Router und ap1 ein reiner CAP.
|
||||
> Archiviert am **2026-08-31** auf Christophs Entscheid.
|
||||
> Projektkonvention ist *fortschreiben statt löschen* — dieselbe wie bei [`switch/`](../../switch/README.md).
|
||||
|
||||
## 🔴 Warum sie nicht liegen bleiben durften, wo sie lagen
|
||||
|
||||
Alle vier zeigten auf **Adressen, die es nicht mehr gibt** — und einer davon ist nicht harmlos
|
||||
kaputt, sondern **falsch**. Live geprüft am 2026-08-31:
|
||||
|
||||
| Datei | Ziel-IP im Code | Realität |
|
||||
|---|---|---|
|
||||
| `apply-core-cutover-l3.py` | `10.40.41.211` | ❌ tot — das VLAN-Schema `10.40.41.x` ist seit **2026-08-27** abgeschafft |
|
||||
| `backup-core.py` | `10.40.41.211` | ❌ tot, dito |
|
||||
| `restore-core.py` | `192.168.88.1` | ❌ tot — Werks-Temp-Pfad; dazu ein hartkodierter Backup-Dateiname vom 2026-08-06 |
|
||||
| `backup-ap1.py` | `10.40.10.1` | 🔴 **antwortet — ist aber der CORE** (VLAN10-SVI, tcp/22 offen). ap1 ist seit dem Cutover `10.40.99.11`. |
|
||||
|
||||
🔑 **Die Merkregel aus `backup-ap1.py` ist die wichtigste Zeile in dieser Datei:**
|
||||
das Skript wäre **nicht** gescheitert. Es hätte den **Core** gesichert und die Datei
|
||||
`ap1-pre-core-cutover-*.backup` genannt. Wer so ein Backup Monate später zurückspielt, spielt eine
|
||||
**Core-Config auf einen AP**. Ein Skript, das still das falsche Gerät trifft, ist gefährlicher als
|
||||
eines, das gar nicht läuft — deshalb liegt es hier und nicht mehr neben den benutzbaren Werkzeugen.
|
||||
|
||||
## Was hier **nicht** mehr gilt
|
||||
|
||||
| Datei | Status |
|
||||
|---|---|
|
||||
| `apply-core-cutover-l3.py` | ❌ nicht ausführen — tote IP; liest zudem `../tp15-core-cutover-intended.rsc`, das **gitignored** ist und durch den Verzeichniswechsel ohnehin nicht mehr gefunden würde |
|
||||
| `backup-core.py` | ❌ nicht ausführen — tote IP; schriebe seine Ausgabe jetzt in **dieses** Archivverzeichnis (`OUT_DIR = Path(__file__).parent`) |
|
||||
| `restore-core.py` | ❌ nicht ausführen — tote IP + Dateiname von 2026-08-06 |
|
||||
| `backup-ap1.py` | 🔴 **auf keinen Fall ausführen** — s. oben, es trifft das falsche Gerät |
|
||||
|
||||
⚠️ **Die Pfad-Nebenwirkungen des Verschiebens sind absichtlich nicht repariert.** Sie zu reparieren
|
||||
hieße, die Skripte wieder lauffähig zu machen — das ist genau *nicht* die Absicht.
|
||||
|
||||
## ✅ Was an ihnen trotzdem noch gepflegt wurde
|
||||
|
||||
Am **2026-08-31** sind sie — vor dem Archivieren — von `sshpass -p <PW>` auf **`SSHPASS`-Env +
|
||||
`sshpass -e`** umgestellt worden, zusammen mit `nautobot/gc-transaktion.py` und
|
||||
`../import-core-rsc.py`. Grund: `-p` schreibt das Passwort in `argv`, wo es in `ps` und jedem
|
||||
argv-Trace steht — so ist am **2026-08-22** das Core-Passwort verbrannt.
|
||||
**Auch Archiv-Code wird kopiert.** Wer hier ein Muster abschreibt, soll das richtige abschreiben.
|
||||
|
||||
In `apply-core-cutover-l3.py` wurde dabei ein latenter Bug mitgefixt: `run_silent` griff auf ein
|
||||
`pw` zu, das in der Funktion gar nicht existierte — **jeder Fehlerfall endete in einem `NameError`
|
||||
statt in der gedachten Meldung.**
|
||||
|
||||
## Wo die Wahrheit heute steht
|
||||
|
||||
- **Ist-Stand + TP-Steuerung:** [`docs/teilprojekte.md`](../../docs/teilprojekte.md)
|
||||
- **Config-Weg für Netzgeräte:** ausschließlich **Nautobot Golden Config** (TP21/TP22) —
|
||||
`nautobot/gc-configplan.py`, Teständerungen als Transaktion über `nautobot/gc-transaktion.py`
|
||||
- **Backup eines Geräts heute:** `/system backup save` direkt am Gerät (so ist am 2026-08-31
|
||||
`pre-tp25-funde-ftoverds.backup` auf dem Core entstanden) — **kein** Skript nötig
|
||||
- **Aktuelle Adressen:** `crs326-core` `10.40.99.1` · `crs326-branch` `10.40.99.2` ·
|
||||
ap1 `10.40.99.11` · ap2 `10.40.99.12` · ap3 `10.40.99.13`
|
||||
- **Interaktiv/Debug:** MikroMCP · **noch benutzbar geblieben:** [`../import-core-rsc.py`](../import-core-rsc.py)
|
||||
(generisch, aber ebenfalls mit totem Default-Host — `--host` mitgeben)
|
||||
93
routeros/archiv-tp15-cutover/apply-core-cutover-l3.py
Normal file
93
routeros/archiv-tp15-cutover/apply-core-cutover-l3.py
Normal file
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Lade die staged L3-Cutover-Config auf crs326-core (noch NICHT enablen).
|
||||
|
||||
Creds aus ~/.claude.json. Alle operativen Objekte in der rsc sind disabled=yes,
|
||||
also bleibt der aktive Datenpfad unberührt.
|
||||
|
||||
🔴 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.
|
||||
|
||||
⚠️ CORE_IP 10.40.41.211 ist TOT (VLAN41 abgeschafft 2026-08-27); der Core ist heute
|
||||
10.40.99.1. Und die gelesene RSC ist gitignored -- dieses Skript ist ein TP15-Artefakt
|
||||
vom Cutover 2026-08-08. Bewusst nicht stillschweigend geaendert, s. Handoff 2026-08-31.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
CORE_IP = "10.40.41.211"
|
||||
RSC = pathlib.Path(__file__).with_name("tp15-core-cutover-intended.rsc")
|
||||
CLAUDE_JSON = pathlib.Path.home() / ".claude.json"
|
||||
|
||||
|
||||
def load_core_creds():
|
||||
cfg = json.loads(CLAUDE_JSON.read_text())
|
||||
env = cfg.get("mcpServers", {}).get("mikromcp", {}).get("env", {})
|
||||
user = env.get("CRS326_CORE_USER") or env.get("ROUTER_USER")
|
||||
pw = env.get("CRS326_CORE_PASS") or env.get("ROUTER_PASS")
|
||||
if not user or not pw:
|
||||
raise SystemExit("CRS326_CORE_USER/PASS nicht in ~/.claude.json mikromcp.env gefunden")
|
||||
return user, pw
|
||||
|
||||
|
||||
# Das Passwort geht ausschliesslich ueber die Umgebung an sshpass -- nie als Argument.
|
||||
def ssh_env(pw):
|
||||
return dict(os.environ, SSHPASS=pw)
|
||||
|
||||
|
||||
def ssh_base(user):
|
||||
return [
|
||||
"sshpass", "-e",
|
||||
"ssh",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "UserKnownHostsFile=/dev/null",
|
||||
"-o", "PubkeyAuthentication=no",
|
||||
f"{user}@{CORE_IP}",
|
||||
]
|
||||
|
||||
|
||||
# 🔴 pw ist Parameter, nicht Global: die alte Fassung griff hier auf ein `pw` zu, das
|
||||
# in dieser Funktion gar nicht existierte -- jeder Fehlerfall endete in einem
|
||||
# NameError statt in der gedachten Meldung. Mitgefixt 2026-08-31.
|
||||
def run_silent(base, cmd, pw):
|
||||
r = subprocess.run(base + [cmd], capture_output=True, text=True, env=ssh_env(pw))
|
||||
if r.returncode != 0:
|
||||
out = (r.stdout + r.stderr).replace(pw, "***")
|
||||
raise SystemExit(f"Befehl fehlgeschlagen (rc={r.returncode}):\n{out}")
|
||||
return r
|
||||
|
||||
|
||||
def main():
|
||||
if not RSC.exists():
|
||||
raise SystemExit(f"{RSC} fehlt")
|
||||
user, pw = load_core_creds()
|
||||
base = ssh_base(user)
|
||||
print(f"crs326-core L3-Cutover-Config laden ({RSC.name}) ...")
|
||||
|
||||
# 1) Datei auf Router kopieren
|
||||
r = subprocess.run(
|
||||
["sshpass", "-e",
|
||||
"scp",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "UserKnownHostsFile=/dev/null",
|
||||
"-o", "PubkeyAuthentication=no",
|
||||
str(RSC),
|
||||
f"{user}@{CORE_IP}:{RSC.name}"],
|
||||
capture_output=True, text=True, env=ssh_env(pw),
|
||||
)
|
||||
if r.returncode != 0:
|
||||
raise SystemExit(f"SCP fehlgeschlagen: {r.stderr.replace(pw, '***')}")
|
||||
|
||||
# 2) Import auslösen
|
||||
run_silent(base, f'/import file-name="{RSC.name}"', pw)
|
||||
|
||||
print(" Import abgeschlossen (staged / disabled).")
|
||||
print(" Nächster Schritt: L2-Portplan + Cutover-Enable.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
108
routeros/archiv-tp15-cutover/backup-ap1.py
Normal file
108
routeros/archiv-tp15-cutover/backup-ap1.py
Normal file
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Erstelle ein Binary-Backup + einen Text-Export von ap1 vor dem TP15-Cutover.
|
||||
|
||||
Creds aus ~/.claude.json (mikromcp-env). Backup-Dateien landen unter routeros/.
|
||||
|
||||
🔴 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.
|
||||
|
||||
🔴 AP1_IP 10.40.10.1 ist NICHT ap1 -- das ist die VLAN10-SVI des CORE (live geprueft
|
||||
2026-08-31: tcp/22 offen). ap1 ist seit dem Core-Cutover 10.40.99.11. Dieses Skript
|
||||
wuerde also den CORE sichern und die Datei "ap1-pre-core-cutover-*.backup" nennen --
|
||||
es scheitert nicht, es benennt falsch. VOR DEM NAECHSTEN LAUF die IP korrigieren.
|
||||
Bewusst nicht stillschweigend geaendert, s. Handoff 2026-08-31.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
AP1_IP = "10.40.10.1"
|
||||
CLAUDE_JSON = pathlib.Path.home() / ".claude.json"
|
||||
OUT_DIR = pathlib.Path(__file__).resolve().parent
|
||||
|
||||
|
||||
def load_ap1_creds():
|
||||
cfg = json.loads(CLAUDE_JSON.read_text())
|
||||
env = cfg.get("mcpServers", {}).get("mikromcp", {}).get("env", {})
|
||||
user = env.get("ROUTER_AP1_USER") or env.get("ROUTER_USER")
|
||||
pw = env.get("ROUTER_AP1_PASS") or env.get("ROUTER_PASS")
|
||||
if not user or not pw:
|
||||
raise SystemExit("ROUTER_AP1_USER/PASS nicht in ~/.claude.json mikromcp.env gefunden")
|
||||
return user, pw
|
||||
|
||||
|
||||
# Das Passwort geht ausschliesslich ueber die Umgebung an sshpass -- nie als Argument.
|
||||
def ssh_env(pw):
|
||||
return dict(os.environ, SSHPASS=pw)
|
||||
|
||||
|
||||
def ssh_cmd(user, remote_command):
|
||||
return [
|
||||
"sshpass", "-e",
|
||||
"ssh",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "UserKnownHostsFile=/dev/null",
|
||||
"-o", "PubkeyAuthentication=no",
|
||||
f"{user}@{AP1_IP}",
|
||||
remote_command,
|
||||
]
|
||||
|
||||
|
||||
def scp_cmd(user, remote_file, local_file):
|
||||
return [
|
||||
"sshpass", "-e",
|
||||
"scp",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "UserKnownHostsFile=/dev/null",
|
||||
"-o", "PubkeyAuthentication=no",
|
||||
f"{user}@{AP1_IP}:{remote_file}",
|
||||
str(local_file),
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
user, pw = load_ap1_creds()
|
||||
ts = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
|
||||
backup_name = f"ap1-pre-core-cutover-{ts}"
|
||||
|
||||
print(f"ap1 Backup via SSH {user}@{AP1_IP}")
|
||||
|
||||
# 1) Binary backup + Text-Export auf ap1 erzeugen
|
||||
create_cmd = ssh_cmd(
|
||||
user,
|
||||
f'/system backup save name="{backup_name}" dont-encrypt=yes; '
|
||||
f'/export file="{backup_name}"'
|
||||
)
|
||||
print(f" Erzeuge Backup/Export auf ap1 ...")
|
||||
subprocess.run(create_cmd, check=True, env=ssh_env(pw))
|
||||
|
||||
# 2) Herunterladen
|
||||
local_backup = OUT_DIR / f"{backup_name}.backup"
|
||||
local_export = OUT_DIR / f"{backup_name}.rsc"
|
||||
|
||||
print(f" Lade {backup_name}.backup herunter ...")
|
||||
subprocess.run(scp_cmd(user, f"{backup_name}.backup", local_backup), check=True, env=ssh_env(pw))
|
||||
|
||||
print(f" Lade {backup_name}.rsc herunter ...")
|
||||
subprocess.run(scp_cmd(user, f"{backup_name}.rsc", local_export), check=True, env=ssh_env(pw))
|
||||
|
||||
# 3) Verifizieren
|
||||
backup_size = local_backup.stat().st_size
|
||||
export_size = local_export.stat().st_size
|
||||
print(f" OK: {local_backup.name} ({backup_size} Bytes)")
|
||||
print(f" OK: {local_export.name} ({export_size} Bytes)")
|
||||
|
||||
if backup_size < 1000:
|
||||
print(" WARN: Backup-Datei auffällig klein!")
|
||||
sys.exit(1)
|
||||
|
||||
print("\nNächster Schritt: Cutover-Config auf crs326-core laden.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
100
routeros/archiv-tp15-cutover/backup-core.py
Normal file
100
routeros/archiv-tp15-cutover/backup-core.py
Normal file
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Erstelle ein Binary-Backup + einen Text-Export von crs326-core vor dem TP15-Cutover.
|
||||
|
||||
Creds aus ~/.claude.json (mikromcp-env, CRS326_CORE_*). Backup-Dateien landen unter routeros/.
|
||||
|
||||
🔴 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.
|
||||
|
||||
⚠️ CORE_IP 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 pathlib
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
CORE_IP = "10.40.41.211" # VLAN1-Temp; alternativ 10.40.99.11 sobald 99T auf Uplink
|
||||
CLAUDE_JSON = pathlib.Path.home() / ".claude.json"
|
||||
OUT_DIR = pathlib.Path(__file__).resolve().parent
|
||||
|
||||
|
||||
def load_core_creds():
|
||||
cfg = json.loads(CLAUDE_JSON.read_text())
|
||||
env = cfg.get("mcpServers", {}).get("mikromcp", {}).get("env", {})
|
||||
user = env.get("CRS326_CORE_USER") or env.get("ROUTER_USER")
|
||||
pw = env.get("CRS326_CORE_PASS") or env.get("ROUTER_PASS")
|
||||
if not user or not pw:
|
||||
raise SystemExit("CRS326_CORE_USER/PASS nicht in ~/.claude.json mikromcp.env gefunden")
|
||||
return user, pw
|
||||
|
||||
|
||||
# Das Passwort geht ausschliesslich ueber die Umgebung an sshpass -- nie als Argument.
|
||||
def ssh_env(pw):
|
||||
return dict(os.environ, SSHPASS=pw)
|
||||
|
||||
|
||||
def ssh_cmd(user, remote_command):
|
||||
return [
|
||||
"sshpass", "-e",
|
||||
"ssh",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "UserKnownHostsFile=/dev/null",
|
||||
"-o", "PubkeyAuthentication=no",
|
||||
f"{user}@{CORE_IP}",
|
||||
remote_command,
|
||||
]
|
||||
|
||||
|
||||
def scp_cmd(user, remote_file, local_file):
|
||||
return [
|
||||
"sshpass", "-e",
|
||||
"scp",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "UserKnownHostsFile=/dev/null",
|
||||
"-o", "PubkeyAuthentication=no",
|
||||
f"{user}@{CORE_IP}:{remote_file}",
|
||||
str(local_file),
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
user, pw = load_core_creds()
|
||||
ts = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
|
||||
backup_name = f"crs326-core-pre-cutover-{ts}"
|
||||
|
||||
print(f"crs326-core Backup via SSH {user}@{CORE_IP}")
|
||||
|
||||
create_cmd = ssh_cmd(
|
||||
user,
|
||||
f'/system backup save name="{backup_name}" dont-encrypt=yes; '
|
||||
f'/export file="{backup_name}"'
|
||||
)
|
||||
print(f" Erzeuge Backup/Export auf crs326-core ...")
|
||||
subprocess.run(create_cmd, check=True, env=ssh_env(pw))
|
||||
|
||||
local_backup = OUT_DIR / f"{backup_name}.backup"
|
||||
local_export = OUT_DIR / f"{backup_name}.rsc"
|
||||
|
||||
print(f" Lade {backup_name}.backup herunter ...")
|
||||
subprocess.run(scp_cmd(user, f"{backup_name}.backup", local_backup), check=True, env=ssh_env(pw))
|
||||
|
||||
print(f" Lade {backup_name}.rsc herunter ...")
|
||||
subprocess.run(scp_cmd(user, f"{backup_name}.rsc", local_export), check=True, env=ssh_env(pw))
|
||||
|
||||
backup_size = local_backup.stat().st_size
|
||||
export_size = local_export.stat().st_size
|
||||
print(f" OK: {local_backup.name} ({backup_size} Bytes)")
|
||||
print(f" OK: {local_export.name} ({export_size} Bytes)")
|
||||
|
||||
if backup_size < 1000:
|
||||
print(" WARN: Backup-Datei auffällig klein!")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
94
routeros/archiv-tp15-cutover/restore-core.py
Normal file
94
routeros/archiv-tp15-cutover/restore-core.py
Normal file
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Stelle das crs326-core Backup vom Cutover-Stand wieder her.
|
||||
|
||||
Creds aus ~/.claude.json (mikromcp-env, CRS326_CORE_*).
|
||||
Backup-Datei wird per SCP hochgeladen und /system backup load ausgefuehrt.
|
||||
|
||||
🔴 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.
|
||||
|
||||
⚠️ CORE_IP 192.168.88.1 ist der Werks-Temp-Pfad und antwortet nicht; der Core ist heute
|
||||
10.40.99.1. Der hartkodierte BACKUP-Dateiname stammt vom 2026-08-06.
|
||||
Bewusst nicht stillschweigend geaendert, s. Handoff 2026-08-31.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
CORE_IP = "192.168.88.1"
|
||||
BACKUP = "crs326-core-pre-cutover-20260806-073712.backup"
|
||||
CLAUDE_JSON = pathlib.Path.home() / ".claude.json"
|
||||
OUT_DIR = pathlib.Path(__file__).resolve().parent
|
||||
|
||||
|
||||
def load_core_creds():
|
||||
cfg = json.loads(CLAUDE_JSON.read_text())
|
||||
env = cfg.get("mcpServers", {}).get("mikromcp", {}).get("env", {})
|
||||
user = env.get("CRS326_CORE_USER") or env.get("ROUTER_USER")
|
||||
pw = env.get("CRS326_CORE_PASS") or env.get("ROUTER_PASS")
|
||||
if not user or not pw:
|
||||
raise SystemExit("CRS326_CORE_USER/PASS nicht in ~/.claude.json mikromcp.env gefunden")
|
||||
return user, pw
|
||||
|
||||
|
||||
# Das Passwort geht ausschliesslich ueber die Umgebung an sshpass -- nie als Argument.
|
||||
def ssh_env(pw):
|
||||
return dict(os.environ, SSHPASS=pw)
|
||||
|
||||
|
||||
def ssh_cmd(user, remote_command):
|
||||
return [
|
||||
"sshpass", "-e",
|
||||
"ssh",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "UserKnownHostsFile=/dev/null",
|
||||
"-o", "PubkeyAuthentication=no",
|
||||
f"{user}@{CORE_IP}",
|
||||
remote_command,
|
||||
]
|
||||
|
||||
|
||||
def scp_cmd(user, local_file, remote_file):
|
||||
return [
|
||||
"sshpass", "-e",
|
||||
"scp",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "UserKnownHostsFile=/dev/null",
|
||||
"-o", "PubkeyAuthentication=no",
|
||||
str(local_file),
|
||||
f"{user}@{CORE_IP}:{remote_file}",
|
||||
]
|
||||
|
||||
|
||||
def run_silent(cmd, pw):
|
||||
r = subprocess.run(cmd, capture_output=True, text=True, env=ssh_env(pw))
|
||||
if r.returncode != 0:
|
||||
out = (r.stdout + r.stderr).replace(pw, "***")
|
||||
raise SystemExit(f"Befehl fehlgeschlagen (rc={r.returncode}):\n{out}")
|
||||
return r
|
||||
|
||||
|
||||
def main():
|
||||
local_backup = OUT_DIR / BACKUP
|
||||
if not local_backup.exists():
|
||||
raise SystemExit(f"{local_backup} fehlt")
|
||||
|
||||
user, pw = load_core_creds()
|
||||
|
||||
print(f"crs326-core Restore via SSH {user}@{CORE_IP}")
|
||||
print(f" Lade {BACKUP} hoch ...")
|
||||
run_silent(scp_cmd(user, local_backup, BACKUP), pw)
|
||||
|
||||
print(f" Spiele Backup zurueck ...")
|
||||
run_silent(ssh_cmd(user, f'/system backup load name="{BACKUP}" password=""'), pw)
|
||||
|
||||
print(" Backup geladen, Router rebootet.")
|
||||
print(" Warte auf Wiederverbindung an 10.40.41.211 ...")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user