remove sshkey .env only
This commit is contained in:
parent
c0e6f96498
commit
f072320ab9
97
README.md
97
README.md
|
|
@ -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)
|
||||
- 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
|
||||
- **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
|
||||
- Dry-Run-Modus zum gefahrlosen Testen
|
||||
- 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)
|
||||
- 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` (nur bei Passwort-Auth): `apt install sshpass`
|
||||
- `sshpass` — wird beim ersten Start automatisch installiert falls nicht vorhanden
|
||||
- Root-Passwort der Proxmox-Nodes (alle Nodes müssen das gleiche Passwort haben)
|
||||
- Keine externen Python-Pakete nötig (nur stdlib)
|
||||
|
||||
## Installation
|
||||
|
|
@ -34,48 +34,24 @@ scp -r proxmox-cluster-network-changer/ root@pve1:/root/
|
|||
# Oder direkt klonen
|
||||
cd /root
|
||||
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
|
||||
|
||||
### Empfohlen: Passwort via `.env` (sshpass)
|
||||
|
||||
Die einfachste und robusteste Methode. Funktioniert immer — auch nachdem `pve-cluster` gestoppt wird und `/etc/pve/priv/authorized_keys` verschwindet.
|
||||
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.
|
||||
|
||||
```bash
|
||||
# .env-Datei erstellen
|
||||
cp .env.example .env
|
||||
|
||||
# Passwort eintragen
|
||||
nano .env
|
||||
```
|
||||
|
||||
`.env`-Datei:
|
||||
```
|
||||
SSH_PASSWORD=dein-root-passwort
|
||||
```
|
||||
|
||||
Voraussetzung:
|
||||
```bash
|
||||
apt install sshpass
|
||||
echo 'SSH_PASSWORD=dein-root-passwort' > .env
|
||||
```
|
||||
|
||||
> **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
|
||||
|
||||
### Aktuelle Konfiguration anzeigen (Discovery)
|
||||
|
|
@ -112,7 +88,7 @@ Das Tool führt interaktiv durch den Prozess:
|
|||
Proxmox Cluster Network Changer
|
||||
============================================================
|
||||
|
||||
[SSH] Passwort-Authentifizierung aktiv (via .env)
|
||||
[SSH] Passwort-Authentifizierung aktiv (via sshpass)
|
||||
|
||||
=== 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 |
|
||||
| `--rescue` | Rescue-Modus: Emergency-Netzwerk einrichten |
|
||||
| `--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) |
|
||||
| `--env-file PFAD` | Pfad zur .env-Datei (Standard: `.env`) |
|
||||
|
||||
### Umgebungsvariablen / `.env`
|
||||
### `.env`-Datei
|
||||
|
||||
| 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`) |
|
||||
|
||||
Variablen können in der `.env`-Datei oder als Umgebungsvariablen gesetzt werden.
|
||||
|
||||
## Was wird geändert?
|
||||
|
||||
| Datei | Wo | Was |
|
||||
|
|
@ -185,28 +158,18 @@ Variablen können in der `.env`-Datei oder als Umgebungsvariablen gesetzt werden
|
|||
|
||||
## Migrationsablauf (Phase 4)
|
||||
|
||||
### Mit Passwort-Auth (empfohlen, 7 Schritte)
|
||||
|
||||
1. Neue Konfigurationen werden auf alle Nodes verteilt (Staging)
|
||||
2. Corosync wird auf allen Nodes gestoppt
|
||||
3. pve-cluster (pmxcfs) wird gestoppt → `/etc/pve` unmounted
|
||||
4. Corosync-Config wird direkt geschrieben (`/etc/corosync/corosync.conf`)
|
||||
5. `/etc/hosts` wird aktualisiert
|
||||
6. `/etc/network/interfaces` wird aktualisiert + `ifreload -a` (alle Bridges)
|
||||
7. Services starten, Quorum abwarten, Ceph aktualisieren
|
||||
6. `/etc/network/interfaces` wird aktualisiert + Netzwerk-Reload:
|
||||
- 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.
|
||||
|
||||
### 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
|
||||
> SSH funktioniert durchgehend via `sshpass` — unabhängig von `/etc/pve`.
|
||||
|
||||
## Rescue-Netzwerk (Emergency Mode)
|
||||
|
||||
|
|
@ -268,7 +231,7 @@ Ablauf:
|
|||
4. Du führst die Befehle auf den anderen Nodes per Konsole aus
|
||||
5. Das Tool testet die Verbindung und liest die Configs
|
||||
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?
|
||||
|
||||
|
|
@ -325,26 +288,26 @@ systemctl restart corosync
|
|||
## Hinweise
|
||||
|
||||
- Das Tool muss als **root** ausgeführt werden
|
||||
- Bei Passwort-Auth müssen alle Nodes das gleiche Root-Passwort haben
|
||||
- Bei Key-Auth müssen SSH-Keys vorher eingerichtet sein (bei Proxmox-Clustern standardmäßig der Fall)
|
||||
- Alle Nodes müssen das gleiche Root-Passwort haben
|
||||
- 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
|
||||
- Die Emergency-IPs (`ip addr add`) sind temporär und überleben keinen Reboot — sie werden nur zur SSH-Kommunikation während der Migration genutzt
|
||||
- Bridges werden automatisch erkannt — keine manuelle Angabe nötig. Alle betroffenen Bridges (Management, Ceph Public, Ceph Cluster) werden per `ifreload -a` aktualisiert
|
||||
- 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
|
||||
- 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
|
||||
|
||||
## Projektstruktur
|
||||
|
||||
```
|
||||
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
|
||||
├── planner.py # Phase 2: IP-Mapping, neue Configs generieren
|
||||
├── 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
|
||||
├── 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
|
||||
├── models.py # Dataclasses (NodeInfo, CorosyncConfig, etc.)
|
||||
├── .env.example # Vorlage für SSH-Credentials
|
||||
|
|
@ -356,9 +319,9 @@ proxmox-cluster-network-changer/
|
|||
### Szenario 1: Normaler Umzug (alles funktioniert noch)
|
||||
|
||||
```bash
|
||||
cp .env.example .env && nano .env # Passwort eintragen
|
||||
python3 main.py --dry-run # Erst testen
|
||||
python3 main.py # Dann ausführen
|
||||
echo 'SSH_PASSWORD=dein-passwort' > .env
|
||||
python3 main.py --dry-run # Erst testen
|
||||
python3 main.py # Dann ausführen
|
||||
```
|
||||
|
||||
### Szenario 2: Ein Node wurde bereits manuell geändert
|
||||
|
|
|
|||
64
main.py
64
main.py
|
|
@ -9,6 +9,7 @@ Kann auch mit gebrochenem Quorum umgehen (z.B. wenn ein Node bereits
|
|||
manuell geändert wurde).
|
||||
|
||||
Muss als root auf einem Proxmox-Node ausgeführt werden.
|
||||
Benötigt SSH_PASSWORD in .env und sshpass.
|
||||
|
||||
Verwendung:
|
||||
python3 main.py # Interaktiver Modus
|
||||
|
|
@ -68,6 +69,28 @@ def check_prerequisites():
|
|||
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():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Proxmox Cluster Network Changer - "
|
||||
|
|
@ -81,10 +104,6 @@ def main():
|
|||
"--discover", action="store_true",
|
||||
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(
|
||||
"--ssh-port", type=int, default=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_user = env.get('SSH_USER') or os.environ.get('SSH_USER') or 'root'
|
||||
|
||||
# Ensure sshpass is available when password auth is requested
|
||||
if ssh_password and not shutil.which("sshpass"):
|
||||
print("\n [SSH] sshpass nicht gefunden — wird für Passwort-Auth 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.")
|
||||
else:
|
||||
print(" FEHLER: Installation fehlgeschlagen!")
|
||||
print(" Bitte manuell installieren: apt install sshpass")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(" Ohne sshpass ist Passwort-Auth nicht möglich.")
|
||||
ssh_password = None
|
||||
if not ssh_password:
|
||||
print("\n FEHLER: SSH_PASSWORD nicht gesetzt!")
|
||||
print(" Erstelle eine .env-Datei mit dem Root-Passwort:")
|
||||
print(" cp .env.example .env")
|
||||
print(" echo 'SSH_PASSWORD=dein-passwort' > .env")
|
||||
print()
|
||||
print(" Oder als Umgebungsvariable:")
|
||||
print(" SSH_PASSWORD=dein-passwort python3 main.py")
|
||||
sys.exit(1)
|
||||
|
||||
# Ensure sshpass is installed
|
||||
if not ensure_sshpass():
|
||||
sys.exit(1)
|
||||
|
||||
# Initialize SSH manager
|
||||
ssh = SSHManager(
|
||||
ssh_user=ssh_user,
|
||||
ssh_key=args.ssh_key,
|
||||
ssh_port=args.ssh_port,
|
||||
ssh_password=ssh_password,
|
||||
)
|
||||
|
||||
if ssh.uses_password:
|
||||
print(f"\n [SSH] Passwort-Authentifizierung aktiv (via .env)")
|
||||
else:
|
||||
print(f"\n [SSH] Key-basierte Authentifizierung")
|
||||
print(f"\n [SSH] Passwort-Authentifizierung aktiv (via sshpass)")
|
||||
rescue = RescueNetwork(ssh)
|
||||
|
||||
# Quick mode: just print rescue commands and exit
|
||||
|
|
|
|||
163
migrator.py
163
migrator.py
|
|
@ -31,58 +31,38 @@ class Migrator:
|
|||
print(" FEHLER: Keine Nodes erreichbar!")
|
||||
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)
|
||||
print(f"[{step}/{total}] Neue Konfigurationen verteilen...")
|
||||
print("[1/7] Neue Konfigurationen verteilen...")
|
||||
if not self._distribute_configs(plan, configs, dry_run):
|
||||
return False
|
||||
step += 1
|
||||
|
||||
# Step 2: Preserve SSH keys (only needed for key-based auth)
|
||||
if not uses_password:
|
||||
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...")
|
||||
# Step 2: Stop Corosync on all nodes
|
||||
print("\n[2/7] Corosync stoppen auf allen Nodes...")
|
||||
if not self._stop_corosync(reachable_nodes, dry_run):
|
||||
return False
|
||||
step += 1
|
||||
|
||||
# Stop pve-cluster (pmxcfs) to release corosync.conf
|
||||
print(f"\n[{step}/{total}] pve-cluster stoppen...")
|
||||
# Step 3: Stop pve-cluster (pmxcfs) to release corosync.conf
|
||||
print("\n[3/7] pve-cluster stoppen...")
|
||||
if not self._stop_pve_cluster(reachable_nodes, dry_run):
|
||||
return False
|
||||
step += 1
|
||||
|
||||
# Write corosync config directly
|
||||
print(f"\n[{step}/{total}] Corosync-Konfiguration aktualisieren...")
|
||||
# Step 4: Write corosync config directly
|
||||
print("\n[4/7] Corosync-Konfiguration aktualisieren...")
|
||||
if not self._update_corosync(reachable_nodes, configs, dry_run):
|
||||
return False
|
||||
step += 1
|
||||
|
||||
# Update /etc/hosts on all nodes
|
||||
print(f"\n[{step}/{total}] /etc/hosts aktualisieren...")
|
||||
# Step 5: Update /etc/hosts on all nodes
|
||||
print("\n[5/7] /etc/hosts aktualisieren...")
|
||||
if not self._update_hosts(plan, configs, dry_run):
|
||||
return False
|
||||
step += 1
|
||||
|
||||
# Update network interfaces and restart networking
|
||||
print(f"\n[{step}/{total}] Netzwerk-Interfaces aktualisieren und Netzwerk neu starten...")
|
||||
# Step 6: Update network interfaces and restart networking
|
||||
print("\n[6/7] Netzwerk-Interfaces aktualisieren und Netzwerk neu starten...")
|
||||
if not self._update_network(plan, configs, dry_run):
|
||||
return False
|
||||
step += 1
|
||||
|
||||
# Start services back up
|
||||
print(f"\n[{step}/{total}] Services starten...")
|
||||
# Step 7: Start services back up
|
||||
print("\n[7/7] Services starten...")
|
||||
if not self._start_services(plan, configs, dry_run):
|
||||
return False
|
||||
|
||||
|
|
@ -177,118 +157,6 @@ class Migrator:
|
|||
|
||||
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:
|
||||
"""Stop corosync on all nodes."""
|
||||
for node in nodes:
|
||||
|
|
@ -519,11 +387,6 @@ class Migrator:
|
|||
if configs.get('ceph'):
|
||||
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
|
||||
print("\n Staging-Verzeichnisse aufräumen...")
|
||||
for node in plan.nodes:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
from typing import Optional
|
||||
|
||||
|
||||
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:
|
||||
- Key-based (default): Uses ssh keys (may break when pve-cluster stops)
|
||||
- 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):
|
||||
def __init__(self, ssh_user: str = "root", ssh_port: int = 22,
|
||||
ssh_password: Optional[str] = None):
|
||||
self.ssh_user = ssh_user
|
||||
self.ssh_key = ssh_key
|
||||
self.ssh_port = ssh_port
|
||||
self.ssh_password = ssh_password
|
||||
|
||||
def _build_ssh_cmd(self, host: str, command: str) -> list[str]:
|
||||
"""Build the ssh command list."""
|
||||
cmd = []
|
||||
|
||||
if self.ssh_password:
|
||||
cmd.extend(["sshpass", "-p", self.ssh_password])
|
||||
cmd = ["sshpass", "-p", self.ssh_password]
|
||||
|
||||
cmd.extend([
|
||||
"ssh",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "ConnectTimeout=10",
|
||||
"-o", "BatchMode=no",
|
||||
"-o", "PubkeyAuthentication=no",
|
||||
"-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(command)
|
||||
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]:
|
||||
"""Execute a command on a remote host via SSH.
|
||||
|
||||
|
|
@ -108,7 +69,6 @@ class SSHManager:
|
|||
|
||||
Returns: (success, message)
|
||||
"""
|
||||
# Use heredoc via ssh to write file
|
||||
cmd = self._build_ssh_cmd(
|
||||
host, f"cat > {path} << 'PROXMOX_NET_EOF'\n{content}\nPROXMOX_NET_EOF"
|
||||
)
|
||||
|
|
@ -185,8 +145,3 @@ class SSHManager:
|
|||
if is_local:
|
||||
return self.write_local_file(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
|
||||
|
|
|
|||
Loading…
Reference in New Issue