remove sshkey .env only

This commit is contained in:
duffyduck 2026-03-04 23:35:38 +01:00
parent c0e6f96498
commit f072320ab9
4 changed files with 92 additions and 301 deletions

View File

@ -12,7 +12,7 @@ Migriert ein komplettes Proxmox-Cluster (inkl. Ceph) von einem Netzwerk in ein a
- Ceph-Support (Public Network, Cluster Network, MON-Adressen) - Ceph-Support (Public Network, Cluster Network, MON-Adressen)
- Funktioniert auch bei **gebrochenem Quorum** (z.B. wenn ein Node bereits manuell geändert wurde) - Funktioniert auch bei **gebrochenem Quorum** (z.B. wenn ein Node bereits manuell geändert wurde)
- **Rescue-Netzwerk** — temporäres Emergency-Netz wenn sich Nodes nicht mehr erreichen können - **Rescue-Netzwerk** — temporäres Emergency-Netz wenn sich Nodes nicht mehr erreichen können
- **Passwort-Auth via `.env`**SSH-Passwort aus `.env`-Datei, funktioniert auch nach pve-cluster Stop - **Passwort-Auth via `.env`**unabhängig von `/etc/pve`, funktioniert immer
- Automatische Backups aller Konfigurationen vor der Migration - Automatische Backups aller Konfigurationen vor der Migration
- Dry-Run-Modus zum gefahrlosen Testen - Dry-Run-Modus zum gefahrlosen Testen
- Verifikation nach der Migration - Verifikation nach der Migration
@ -21,8 +21,8 @@ Migriert ein komplettes Proxmox-Cluster (inkl. Ceph) von einem Netzwerk in ein a
- Python 3.9+ (auf Proxmox standardmäßig vorhanden) - Python 3.9+ (auf Proxmox standardmäßig vorhanden)
- Root-Zugriff auf dem Node, auf dem das Tool läuft - Root-Zugriff auf dem Node, auf dem das Tool läuft
- SSH-Zugriff zu allen anderen Cluster-Nodes (Key-basiert oder Passwort via `.env`) - `sshpass` — wird beim ersten Start automatisch installiert falls nicht vorhanden
- `sshpass` (nur bei Passwort-Auth): `apt install sshpass` - Root-Passwort der Proxmox-Nodes (alle Nodes müssen das gleiche Passwort haben)
- Keine externen Python-Pakete nötig (nur stdlib) - Keine externen Python-Pakete nötig (nur stdlib)
## Installation ## Installation
@ -34,48 +34,24 @@ scp -r proxmox-cluster-network-changer/ root@pve1:/root/
# Oder direkt klonen # Oder direkt klonen
cd /root cd /root
git clone <repo-url> proxmox-cluster-network-changer git clone <repo-url> proxmox-cluster-network-changer
# .env erstellen
cd proxmox-cluster-network-changer
cp .env.example .env
nano .env # SSH_PASSWORD=dein-root-passwort eintragen
``` ```
## SSH-Authentifizierung ## SSH-Authentifizierung
### Empfohlen: Passwort via `.env` (sshpass) Das Tool nutzt `sshpass` mit dem Root-Passwort aus der `.env`-Datei. Key-basierte Auth funktioniert bei Proxmox nicht zuverlässig, weil `/etc/pve/priv/authorized_keys` verschwindet wenn `pve-cluster` gestoppt wird.
Die einfachste und robusteste Methode. Funktioniert immer — auch nachdem `pve-cluster` gestoppt wird und `/etc/pve/priv/authorized_keys` verschwindet.
```bash ```bash
# .env-Datei erstellen # .env-Datei erstellen
cp .env.example .env echo 'SSH_PASSWORD=dein-root-passwort' > .env
# Passwort eintragen
nano .env
```
`.env`-Datei:
```
SSH_PASSWORD=dein-root-passwort
```
Voraussetzung:
```bash
apt install sshpass
``` ```
> **Hinweis:** Alle Nodes müssen das gleiche Root-Passwort haben (bei Proxmox-Clustern üblich). > **Hinweis:** Alle Nodes müssen das gleiche Root-Passwort haben (bei Proxmox-Clustern üblich).
### Alternative: Key-basiert
Ohne `.env`-Datei wird automatisch Key-basierte Authentifizierung verwendet. Bei dieser Methode werden vor dem pve-cluster-Stop die SSH-Keys gesichert und die sshd-Konfiguration temporär angepasst.
```bash
# Ohne .env -> Key-basiert
python3 main.py
# Oder mit explizitem Key
python3 main.py --ssh-key /root/.ssh/id_rsa
```
> **Achtung:** Key-basierte Auth kann Probleme verursachen wenn `/etc/pve` unmounted wird und der sshd nur dort nach Keys sucht.
## Verwendung ## Verwendung
### Aktuelle Konfiguration anzeigen (Discovery) ### Aktuelle Konfiguration anzeigen (Discovery)
@ -112,7 +88,7 @@ Das Tool führt interaktiv durch den Prozess:
Proxmox Cluster Network Changer Proxmox Cluster Network Changer
============================================================ ============================================================
[SSH] Passwort-Authentifizierung aktiv (via .env) [SSH] Passwort-Authentifizierung aktiv (via sshpass)
=== Phase 1: Discovery === === Phase 1: Discovery ===
@ -161,19 +137,16 @@ Wenn Ceph Public/Cluster auf separaten NICs liegen, werden die IPs einzeln pro N
| `--discover` | Nur aktuelle Config anzeigen | | `--discover` | Nur aktuelle Config anzeigen |
| `--rescue` | Rescue-Modus: Emergency-Netzwerk einrichten | | `--rescue` | Rescue-Modus: Emergency-Netzwerk einrichten |
| `--rescue-commands SUBNET` | Nur Rescue-Befehle ausgeben (z.B. `10.99.99.0/24`) | | `--rescue-commands SUBNET` | Nur Rescue-Befehle ausgeben (z.B. `10.99.99.0/24`) |
| `--ssh-key PFAD` | Pfad zum SSH-Key (Standard: Default-Key) |
| `--ssh-port PORT` | SSH-Port (Standard: 22) | | `--ssh-port PORT` | SSH-Port (Standard: 22) |
| `--env-file PFAD` | Pfad zur .env-Datei (Standard: `.env`) | | `--env-file PFAD` | Pfad zur .env-Datei (Standard: `.env`) |
### Umgebungsvariablen / `.env` ### `.env`-Datei
| Variable | Beschreibung | | Variable | Beschreibung |
|---|---| |---|---|
| `SSH_PASSWORD` | Root-Passwort für SSH (aktiviert sshpass-Modus) | | `SSH_PASSWORD` | **Pflicht.** Root-Passwort für SSH (alle Nodes gleich) |
| `SSH_USER` | SSH-Benutzer (Standard: `root`) | | `SSH_USER` | SSH-Benutzer (Standard: `root`) |
Variablen können in der `.env`-Datei oder als Umgebungsvariablen gesetzt werden.
## Was wird geändert? ## Was wird geändert?
| Datei | Wo | Was | | Datei | Wo | Was |
@ -185,28 +158,18 @@ Variablen können in der `.env`-Datei oder als Umgebungsvariablen gesetzt werden
## Migrationsablauf (Phase 4) ## Migrationsablauf (Phase 4)
### Mit Passwort-Auth (empfohlen, 7 Schritte)
1. Neue Konfigurationen werden auf alle Nodes verteilt (Staging) 1. Neue Konfigurationen werden auf alle Nodes verteilt (Staging)
2. Corosync wird auf allen Nodes gestoppt 2. Corosync wird auf allen Nodes gestoppt
3. pve-cluster (pmxcfs) wird gestoppt → `/etc/pve` unmounted 3. pve-cluster (pmxcfs) wird gestoppt → `/etc/pve` unmounted
4. Corosync-Config wird direkt geschrieben (`/etc/corosync/corosync.conf`) 4. Corosync-Config wird direkt geschrieben (`/etc/corosync/corosync.conf`)
5. `/etc/hosts` wird aktualisiert 5. `/etc/hosts` wird aktualisiert
6. `/etc/network/interfaces` wird aktualisiert + `ifreload -a` (alle Bridges) 6. `/etc/network/interfaces` wird aktualisiert + Netzwerk-Reload:
7. Services starten, Quorum abwarten, Ceph aktualisieren - Remote-Nodes zuerst (fire-and-forget via `nohup`)
- Lokaler Node zuletzt
- Verifikation der neuen IPs mit Retry
7. Services starten (pve-cluster, corosync), Quorum abwarten, Ceph aktualisieren
> SSH funktioniert durchgehend, weil Passwort-Auth unabhängig von `/etc/pve` ist. > SSH funktioniert durchgehend via `sshpass` — unabhängig von `/etc/pve`.
### Mit Key-Auth (Fallback, 8 Schritte)
1. Neue Konfigurationen werden auf alle Nodes verteilt (Staging)
2. SSH-Keys sichern (`/etc/pve/priv/authorized_keys` → `~/.ssh/authorized_keys`) + sshd anpassen
3. Corosync wird auf allen Nodes gestoppt
4. pve-cluster (pmxcfs) wird gestoppt → `/etc/pve` unmounted
5. Corosync-Config wird direkt geschrieben (`/etc/corosync/corosync.conf`)
6. `/etc/hosts` wird aktualisiert
7. `/etc/network/interfaces` wird aktualisiert + `ifreload -a` (alle Bridges)
8. Services starten, Quorum abwarten, Ceph aktualisieren, SSH-Keys wiederherstellen
## Rescue-Netzwerk (Emergency Mode) ## Rescue-Netzwerk (Emergency Mode)
@ -268,7 +231,7 @@ Ablauf:
4. Du führst die Befehle auf den anderen Nodes per Konsole aus 4. Du führst die Befehle auf den anderen Nodes per Konsole aus
5. Das Tool testet die Verbindung und liest die Configs 5. Das Tool testet die Verbindung und liest die Configs
6. Danach läuft die normale Migration 6. Danach läuft die normale Migration
7. Am Ende werden die Emergency-IPs automatisch entfernt 7. Rescue-IPs werden durch `ifreload -a` automatisch entfernt
### Wann brauche ich das? ### Wann brauche ich das?
@ -325,26 +288,26 @@ systemctl restart corosync
## Hinweise ## Hinweise
- Das Tool muss als **root** ausgeführt werden - Das Tool muss als **root** ausgeführt werden
- Bei Passwort-Auth müssen alle Nodes das gleiche Root-Passwort haben - Alle Nodes müssen das gleiche Root-Passwort haben
- Bei Key-Auth müssen SSH-Keys vorher eingerichtet sein (bei Proxmox-Clustern standardmäßig der Fall)
- VMs/CTs werden **nicht** automatisch migriert oder gestoppt — das Netzwerk wird im laufenden Betrieb geändert - VMs/CTs werden **nicht** automatisch migriert oder gestoppt — das Netzwerk wird im laufenden Betrieb geändert
- Nach der Migration sollten VM-Netzwerke (Bridges in VM-Configs) geprüft werden, falls diese sich auf spezifische IPs beziehen - Nach der Migration sollten VM-Netzwerke (Bridges in VM-Configs) geprüft werden, falls diese sich auf spezifische IPs beziehen
- Die Emergency-IPs (`ip addr add`) sind temporär und überleben keinen Reboot — sie werden nur zur SSH-Kommunikation während der Migration genutzt - Die Emergency-IPs (`ip addr add`) sind temporär und werden durch `ifreload -a` automatisch entfernt
- Bridges werden automatisch erkannt — keine manuelle Angabe nötig. Alle betroffenen Bridges (Management, Ceph Public, Ceph Cluster) werden per `ifreload -a` aktualisiert - Bridges werden automatisch erkannt — keine manuelle Angabe nötig
- Beim Netzwerk-Reload werden Remote-Nodes zuerst umgestellt (fire-and-forget), der lokale Node zuletzt — so schneidet sich das Tool nicht selbst die Verbindung ab
- Getestet mit Proxmox VE 7.x und 8.x - Getestet mit Proxmox VE 7.x und 8.x
## Projektstruktur ## Projektstruktur
``` ```
proxmox-cluster-network-changer/ proxmox-cluster-network-changer/
├── main.py # Entry-Point, CLI mit allen Optionen ├── main.py # Entry-Point, CLI, .env-Loader
├── discovery.py # Phase 1: Cluster-Config lesen & parsen ├── discovery.py # Phase 1: Cluster-Config lesen & parsen
├── planner.py # Phase 2: IP-Mapping, neue Configs generieren ├── planner.py # Phase 2: IP-Mapping, neue Configs generieren
├── backup.py # Phase 3: Backup aller Configs ├── backup.py # Phase 3: Backup aller Configs
├── migrator.py # Phase 4: Migration durchführen ├── migrator.py # Phase 4: Migration durchführen (7 Schritte)
├── verifier.py # Phase 5: Post-Migration Checks ├── verifier.py # Phase 5: Post-Migration Checks
├── rescue.py # Rescue-Netzwerk (Emergency Mode) ├── rescue.py # Rescue-Netzwerk (Emergency Mode)
├── ssh_manager.py # SSH-Verbindungen (Key + Passwort via sshpass) ├── ssh_manager.py # SSH via sshpass (Passwort-Auth)
├── config_parser.py # Parser für Corosync/Ceph/Network Configs ├── config_parser.py # Parser für Corosync/Ceph/Network Configs
├── models.py # Dataclasses (NodeInfo, CorosyncConfig, etc.) ├── models.py # Dataclasses (NodeInfo, CorosyncConfig, etc.)
├── .env.example # Vorlage für SSH-Credentials ├── .env.example # Vorlage für SSH-Credentials
@ -356,7 +319,7 @@ proxmox-cluster-network-changer/
### Szenario 1: Normaler Umzug (alles funktioniert noch) ### Szenario 1: Normaler Umzug (alles funktioniert noch)
```bash ```bash
cp .env.example .env && nano .env # Passwort eintragen echo 'SSH_PASSWORD=dein-passwort' > .env
python3 main.py --dry-run # Erst testen python3 main.py --dry-run # Erst testen
python3 main.py # Dann ausführen python3 main.py # Dann ausführen
``` ```

62
main.py
View File

@ -9,6 +9,7 @@ Kann auch mit gebrochenem Quorum umgehen (z.B. wenn ein Node bereits
manuell geändert wurde). manuell geändert wurde).
Muss als root auf einem Proxmox-Node ausgeführt werden. Muss als root auf einem Proxmox-Node ausgeführt werden.
Benötigt SSH_PASSWORD in .env und sshpass.
Verwendung: Verwendung:
python3 main.py # Interaktiver Modus python3 main.py # Interaktiver Modus
@ -68,6 +69,28 @@ def check_prerequisites():
sys.exit(0) sys.exit(0)
def ensure_sshpass():
"""Ensure sshpass is installed, offer to install if missing."""
if shutil.which("sshpass"):
return True
print("\n [SSH] sshpass nicht gefunden — wird für SSH-Authentifizierung benötigt.")
answer = input(" sshpass jetzt installieren? (apt install sshpass) [J/n]: ").strip().lower()
if answer not in ('n', 'nein', 'no'):
rc = subprocess.run(
["apt", "install", "-y", "sshpass"],
capture_output=True, text=True,
).returncode
if rc == 0:
print(" sshpass installiert.")
return True
else:
print(" FEHLER: Installation fehlgeschlagen!")
print(" Bitte manuell installieren: apt install sshpass")
return False
return False
def main(): def main():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Proxmox Cluster Network Changer - " description="Proxmox Cluster Network Changer - "
@ -81,10 +104,6 @@ def main():
"--discover", action="store_true", "--discover", action="store_true",
help="Nur Discovery durchführen, keine Migration" help="Nur Discovery durchführen, keine Migration"
) )
parser.add_argument(
"--ssh-key", type=str, default=None,
help="Pfad zum SSH-Key (Standard: Default SSH-Key)"
)
parser.add_argument( parser.add_argument(
"--ssh-port", type=int, default=22, "--ssh-port", type=int, default=22,
help="SSH-Port (Standard: 22)" help="SSH-Port (Standard: 22)"
@ -116,37 +135,28 @@ def main():
ssh_password = env.get('SSH_PASSWORD') or os.environ.get('SSH_PASSWORD') ssh_password = env.get('SSH_PASSWORD') or os.environ.get('SSH_PASSWORD')
ssh_user = env.get('SSH_USER') or os.environ.get('SSH_USER') or 'root' ssh_user = env.get('SSH_USER') or os.environ.get('SSH_USER') or 'root'
# Ensure sshpass is available when password auth is requested if not ssh_password:
if ssh_password and not shutil.which("sshpass"): print("\n FEHLER: SSH_PASSWORD nicht gesetzt!")
print("\n [SSH] sshpass nicht gefunden — wird für Passwort-Auth benötigt.") print(" Erstelle eine .env-Datei mit dem Root-Passwort:")
answer = input(" sshpass jetzt installieren? (apt install sshpass) [J/n]: ").strip().lower() print(" cp .env.example .env")
if answer not in ('n', 'nein', 'no'): print(" echo 'SSH_PASSWORD=dein-passwort' > .env")
rc = subprocess.run( print()
["apt", "install", "-y", "sshpass"], print(" Oder als Umgebungsvariable:")
capture_output=True, text=True, print(" SSH_PASSWORD=dein-passwort python3 main.py")
).returncode sys.exit(1)
if rc == 0:
print(" sshpass installiert.") # Ensure sshpass is installed
else: if not ensure_sshpass():
print(" FEHLER: Installation fehlgeschlagen!")
print(" Bitte manuell installieren: apt install sshpass")
sys.exit(1) sys.exit(1)
else:
print(" Ohne sshpass ist Passwort-Auth nicht möglich.")
ssh_password = None
# Initialize SSH manager # Initialize SSH manager
ssh = SSHManager( ssh = SSHManager(
ssh_user=ssh_user, ssh_user=ssh_user,
ssh_key=args.ssh_key,
ssh_port=args.ssh_port, ssh_port=args.ssh_port,
ssh_password=ssh_password, ssh_password=ssh_password,
) )
if ssh.uses_password: print(f"\n [SSH] Passwort-Authentifizierung aktiv (via sshpass)")
print(f"\n [SSH] Passwort-Authentifizierung aktiv (via .env)")
else:
print(f"\n [SSH] Key-basierte Authentifizierung")
rescue = RescueNetwork(ssh) rescue = RescueNetwork(ssh)
# Quick mode: just print rescue commands and exit # Quick mode: just print rescue commands and exit

View File

@ -31,58 +31,38 @@ class Migrator:
print(" FEHLER: Keine Nodes erreichbar!") print(" FEHLER: Keine Nodes erreichbar!")
return False return False
uses_password = self.ssh.uses_password
step = 1
total = 7 if uses_password else 8
# Step 1: Write new configs to all nodes (but don't activate yet) # Step 1: Write new configs to all nodes (but don't activate yet)
print(f"[{step}/{total}] Neue Konfigurationen verteilen...") print("[1/7] Neue Konfigurationen verteilen...")
if not self._distribute_configs(plan, configs, dry_run): if not self._distribute_configs(plan, configs, dry_run):
return False return False
step += 1
# Step 2: Preserve SSH keys (only needed for key-based auth) # Step 2: Stop Corosync on all nodes
if not uses_password: print("\n[2/7] Corosync stoppen auf allen Nodes...")
print(f"\n[{step}/{total}] SSH-Keys sichern (werden nach pve-cluster stop benötigt)...")
if not self._preserve_ssh_keys(reachable_nodes, dry_run):
return False
step += 1
else:
if not dry_run:
print(f"\n SSH-Key-Sicherung übersprungen (Passwort-Auth aktiv)")
# Stop Corosync on all nodes
print(f"\n[{step}/{total}] Corosync stoppen auf allen Nodes...")
if not self._stop_corosync(reachable_nodes, dry_run): if not self._stop_corosync(reachable_nodes, dry_run):
return False return False
step += 1
# Stop pve-cluster (pmxcfs) to release corosync.conf # Step 3: Stop pve-cluster (pmxcfs) to release corosync.conf
print(f"\n[{step}/{total}] pve-cluster stoppen...") print("\n[3/7] pve-cluster stoppen...")
if not self._stop_pve_cluster(reachable_nodes, dry_run): if not self._stop_pve_cluster(reachable_nodes, dry_run):
return False return False
step += 1
# Write corosync config directly # Step 4: Write corosync config directly
print(f"\n[{step}/{total}] Corosync-Konfiguration aktualisieren...") print("\n[4/7] Corosync-Konfiguration aktualisieren...")
if not self._update_corosync(reachable_nodes, configs, dry_run): if not self._update_corosync(reachable_nodes, configs, dry_run):
return False return False
step += 1
# Update /etc/hosts on all nodes # Step 5: Update /etc/hosts on all nodes
print(f"\n[{step}/{total}] /etc/hosts aktualisieren...") print("\n[5/7] /etc/hosts aktualisieren...")
if not self._update_hosts(plan, configs, dry_run): if not self._update_hosts(plan, configs, dry_run):
return False return False
step += 1
# Update network interfaces and restart networking # Step 6: Update network interfaces and restart networking
print(f"\n[{step}/{total}] Netzwerk-Interfaces aktualisieren und Netzwerk neu starten...") print("\n[6/7] Netzwerk-Interfaces aktualisieren und Netzwerk neu starten...")
if not self._update_network(plan, configs, dry_run): if not self._update_network(plan, configs, dry_run):
return False return False
step += 1
# Start services back up # Step 7: Start services back up
print(f"\n[{step}/{total}] Services starten...") print("\n[7/7] Services starten...")
if not self._start_services(plan, configs, dry_run): if not self._start_services(plan, configs, dry_run):
return False return False
@ -177,118 +157,6 @@ class Migrator:
return True return True
def _preserve_ssh_keys(self, nodes: list, dry_run: bool) -> bool:
"""Ensure SSH keeps working after pve-cluster stop.
When pve-cluster (pmxcfs) is stopped, /etc/pve gets unmounted and
the cluster SSH keys in /etc/pve/priv/authorized_keys disappear.
This breaks SSH between nodes.
Fix: Copy PVE keys to ~/.ssh/authorized_keys AND ensure sshd is
configured to actually check that file (Proxmox may only check /etc/pve/).
"""
for node in nodes:
if dry_run:
print(f" [{node.name}] Würde SSH-Keys sichern")
continue
# Step 1: Copy PVE keys to ~/.ssh/authorized_keys
copy_cmd = (
"mkdir -p /root/.ssh && "
"cp /root/.ssh/authorized_keys /root/.ssh/authorized_keys.pre_migration 2>/dev/null; "
"if [ -f /etc/pve/priv/authorized_keys ]; then "
" cat /etc/pve/priv/authorized_keys >> /root/.ssh/authorized_keys && "
" sort -u /root/.ssh/authorized_keys > /root/.ssh/authorized_keys.tmp && "
" mv /root/.ssh/authorized_keys.tmp /root/.ssh/authorized_keys && "
" chmod 600 /root/.ssh/authorized_keys && "
" echo keys_copied; "
"else "
" echo no_pve_keys; "
"fi"
)
rc, stdout, err = self.ssh.run_on_node(
node.ssh_host, copy_cmd, node.is_local
)
if "keys_copied" in stdout:
print(f" [{node.name}] PVE-Keys nach ~/.ssh/authorized_keys kopiert")
elif "no_pve_keys" in stdout:
print(f" [{node.name}] Keine PVE-Keys gefunden (übersprungen)")
else:
print(f" [{node.name}] WARNUNG Key-Kopie: rc={rc} {err}")
# Step 2: Ensure sshd checks ~/.ssh/authorized_keys
# Proxmox sshd_config may only list /etc/pve/priv/authorized_keys,
# or use AuthorizedKeysCommand pointing to /etc/pve/priv/.
# We need to ensure .ssh/authorized_keys is checked as fallback.
sshd_cmd = (
"cp /etc/ssh/sshd_config /etc/ssh/sshd_config.pre_migration && "
"NEED_RELOAD=0 && "
# Handle AuthorizedKeysFile
"if grep -q '^AuthorizedKeysFile' /etc/ssh/sshd_config; then "
" if ! grep '^AuthorizedKeysFile' /etc/ssh/sshd_config | grep -q '.ssh/authorized_keys'; then "
" sed -i '/^AuthorizedKeysFile/s|$| .ssh/authorized_keys|' /etc/ssh/sshd_config && "
" NEED_RELOAD=1; "
" fi; "
"else "
# No AuthorizedKeysFile line = uses default (.ssh/authorized_keys), which is fine.
# But if AuthorizedKeysCommand is active, it might override. Add explicit line.
" if grep -q '^AuthorizedKeysCommand ' /etc/ssh/sshd_config; then "
" echo 'AuthorizedKeysFile .ssh/authorized_keys' >> /etc/ssh/sshd_config && "
" NEED_RELOAD=1; "
" fi; "
"fi && "
# Temporarily disable AuthorizedKeysCommand if it points to /etc/pve
"if grep '^AuthorizedKeysCommand ' /etc/ssh/sshd_config | grep -q '/etc/pve'; then "
" sed -i 's|^AuthorizedKeysCommand |#AuthorizedKeysCommand_DISABLED |' /etc/ssh/sshd_config && "
" NEED_RELOAD=1; "
"fi && "
"if [ $NEED_RELOAD -eq 1 ]; then "
" systemctl reload sshd && echo sshd_modified; "
"else "
" echo sshd_already_ok; "
"fi"
)
rc2, stdout2, err2 = self.ssh.run_on_node(
node.ssh_host, sshd_cmd, node.is_local
)
if "sshd_modified" in stdout2:
print(f" [{node.name}] sshd_config angepasst (.ssh/authorized_keys hinzugefügt)")
elif "sshd_already_ok" in stdout2:
print(f" [{node.name}] sshd_config OK")
else:
print(f" [{node.name}] WARNUNG sshd: {err2}")
# Step 3: Verify SSH will still work after pve-cluster stop
# Test that ~/.ssh/authorized_keys is readable on all remote nodes
print(" [Verifikation] Prüfe ob SSH-Keys korrekt gesichert sind...")
for node in nodes:
if node.is_local:
continue
rc, stdout, _ = self.ssh.run_on_node(
node.ssh_host,
"wc -l /root/.ssh/authorized_keys 2>/dev/null || echo 0",
False,
)
key_count = stdout.strip().split()[0] if stdout.strip() else "0"
print(f" [{node.name}] authorized_keys: {key_count} Zeilen")
return True
def _restore_ssh_keys(self, nodes: list):
"""Restore original ~/.ssh/authorized_keys and sshd_config after migration."""
for node in nodes:
new_host = node.new_ip if not node.is_local else node.ssh_host
cmd = (
"if [ -f /root/.ssh/authorized_keys.pre_migration ]; then "
" mv /root/.ssh/authorized_keys.pre_migration /root/.ssh/authorized_keys; "
"fi; "
"if [ -f /etc/ssh/sshd_config.pre_migration ]; then "
" mv /etc/ssh/sshd_config.pre_migration /etc/ssh/sshd_config && "
" systemctl reload sshd 2>/dev/null; "
"fi"
)
self.ssh.run_on_node(new_host, cmd, node.is_local)
def _stop_corosync(self, nodes: list, dry_run: bool) -> bool: def _stop_corosync(self, nodes: list, dry_run: bool) -> bool:
"""Stop corosync on all nodes.""" """Stop corosync on all nodes."""
for node in nodes: for node in nodes:
@ -519,11 +387,6 @@ class Migrator:
if configs.get('ceph'): if configs.get('ceph'):
self._update_ceph(plan, configs) self._update_ceph(plan, configs)
# Restore original SSH keys (only needed for key-based auth)
if not self.ssh.uses_password:
print("\n SSH-Keys wiederherstellen...")
self._restore_ssh_keys(plan.nodes)
# Cleanup staging directories # Cleanup staging directories
print("\n Staging-Verzeichnisse aufräumen...") print("\n Staging-Verzeichnisse aufräumen...")
for node in plan.nodes: for node in plan.nodes:

View File

@ -1,79 +1,40 @@
"""SSH connection manager for remote Proxmox nodes.""" """SSH connection manager for remote Proxmox nodes.
Uses sshpass + password from .env for authentication.
This is required because Proxmox stores SSH keys in /etc/pve/priv/authorized_keys,
which disappears when pve-cluster is stopped during migration.
"""
import subprocess import subprocess
from typing import Optional from typing import Optional
class SSHManager: class SSHManager:
"""Manages SSH connections to Proxmox nodes using system ssh. """Manages SSH connections to Proxmox nodes using sshpass + system ssh."""
Supports two authentication modes: def __init__(self, ssh_user: str = "root", ssh_port: int = 22,
- Key-based (default): Uses ssh keys (may break when pve-cluster stops) ssh_password: Optional[str] = None):
- Password-based: Uses sshpass + password from .env (always works)
"""
def __init__(self, ssh_user: str = "root", ssh_key: Optional[str] = None,
ssh_port: int = 22, ssh_password: Optional[str] = None):
self.ssh_user = ssh_user self.ssh_user = ssh_user
self.ssh_key = ssh_key
self.ssh_port = ssh_port self.ssh_port = ssh_port
self.ssh_password = ssh_password self.ssh_password = ssh_password
def _build_ssh_cmd(self, host: str, command: str) -> list[str]: def _build_ssh_cmd(self, host: str, command: str) -> list[str]:
"""Build the ssh command list.""" """Build the ssh command list."""
cmd = [] cmd = ["sshpass", "-p", self.ssh_password]
if self.ssh_password:
cmd.extend(["sshpass", "-p", self.ssh_password])
cmd.extend([ cmd.extend([
"ssh", "ssh",
"-o", "StrictHostKeyChecking=no", "-o", "StrictHostKeyChecking=no",
"-o", "ConnectTimeout=10", "-o", "ConnectTimeout=10",
"-o", "BatchMode=no",
"-o", "PubkeyAuthentication=no",
"-p", str(self.ssh_port), "-p", str(self.ssh_port),
]) ])
if self.ssh_password:
# Disable BatchMode for password auth, enable keyboard-interactive
cmd.extend(["-o", "BatchMode=no"])
cmd.extend(["-o", "PubkeyAuthentication=no"])
else:
cmd.extend(["-o", "BatchMode=yes"])
if self.ssh_key:
cmd.extend(["-i", self.ssh_key])
cmd.append(f"{self.ssh_user}@{host}") cmd.append(f"{self.ssh_user}@{host}")
cmd.append(command) cmd.append(command)
return cmd return cmd
def _build_scp_cmd(self, host: str, local_path: str,
remote_path: str) -> list[str]:
"""Build an scp command list."""
cmd = []
if self.ssh_password:
cmd.extend(["sshpass", "-p", self.ssh_password])
cmd.extend([
"scp",
"-o", "StrictHostKeyChecking=no",
"-o", "ConnectTimeout=10",
"-P", str(self.ssh_port),
])
if self.ssh_password:
cmd.extend(["-o", "BatchMode=no"])
cmd.extend(["-o", "PubkeyAuthentication=no"])
else:
cmd.extend(["-o", "BatchMode=yes"])
if self.ssh_key:
cmd.extend(["-i", self.ssh_key])
cmd.extend([local_path, f"{self.ssh_user}@{host}:{remote_path}"])
return cmd
def execute(self, host: str, command: str, timeout: int = 30) -> tuple[int, str, str]: def execute(self, host: str, command: str, timeout: int = 30) -> tuple[int, str, str]:
"""Execute a command on a remote host via SSH. """Execute a command on a remote host via SSH.
@ -108,7 +69,6 @@ class SSHManager:
Returns: (success, message) Returns: (success, message)
""" """
# Use heredoc via ssh to write file
cmd = self._build_ssh_cmd( cmd = self._build_ssh_cmd(
host, f"cat > {path} << 'PROXMOX_NET_EOF'\n{content}\nPROXMOX_NET_EOF" host, f"cat > {path} << 'PROXMOX_NET_EOF'\n{content}\nPROXMOX_NET_EOF"
) )
@ -185,8 +145,3 @@ class SSHManager:
if is_local: if is_local:
return self.write_local_file(path, content) return self.write_local_file(path, content)
return self.write_file(host, path, content) return self.write_file(host, path, content)
@property
def uses_password(self) -> bool:
"""Whether password-based auth is active."""
return self.ssh_password is not None