remove sshkey .env only
This commit is contained in:
parent
c0e6f96498
commit
f072320ab9
93
README.md
93
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)
|
- 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
62
main.py
|
|
@ -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
|
||||||
|
|
|
||||||
163
migrator.py
163
migrator.py
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue