.env un dsshpass

This commit is contained in:
duffyduck 2026-03-04 23:20:37 +01:00
parent 899265faca
commit 1083fbb375
5 changed files with 251 additions and 39 deletions

10
.env.example Normal file
View File

@ -0,0 +1,10 @@
# SSH-Zugangsdaten für die Cluster-Nodes
# Kopiere diese Datei nach .env und passe die Werte an:
# cp .env.example .env
# Root-Passwort der Proxmox-Nodes (alle Nodes müssen das gleiche Passwort haben)
# Wird mit sshpass verwendet — funktioniert auch nach pve-cluster Stop
SSH_PASSWORD=dein-root-passwort
# SSH-Benutzer (Standard: root)
#SSH_USER=root

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)
- 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
- SSH-Key-Sicherung vor pve-cluster-Stop (verhindert SSH-Abbrüche)
- **Passwort-Auth via `.env`** — SSH-Passwort aus `.env`-Datei, funktioniert auch nach pve-cluster Stop
- Automatische Backups aller Konfigurationen vor der Migration
- Dry-Run-Modus zum gefahrlosen Testen
- Verifikation nach der Migration
@ -21,7 +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 (Key-basiert) zu allen anderen Cluster-Nodes (oder Konsolenzugriff für Rescue-Modus)
- SSH-Zugriff zu allen anderen Cluster-Nodes (Key-basiert oder Passwort via `.env`)
- `sshpass` (nur bei Passwort-Auth): `apt install sshpass`
- Keine externen Python-Pakete nötig (nur stdlib)
## Installation
@ -35,6 +36,46 @@ cd /root
git clone <repo-url> proxmox-cluster-network-changer
```
## 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.
```bash
# .env-Datei erstellen
cp .env.example .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).
### 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)
@ -67,6 +108,12 @@ python3 main.py
Das Tool führt interaktiv durch den Prozess:
```
============================================================
Proxmox Cluster Network Changer
============================================================
[SSH] Passwort-Authentifizierung aktiv (via .env)
=== Phase 1: Discovery ===
[Corosync]
@ -116,6 +163,16 @@ Wenn Ceph Public/Cluster auf separaten NICs liegen, werden die IPs einzeln pro N
| `--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`
| Variable | Beschreibung |
|---|---|
| `SSH_PASSWORD` | Root-Passwort für SSH (aktiviert sshpass-Modus) |
| `SSH_USER` | SSH-Benutzer (Standard: `root`) |
Variablen können in der `.env`-Datei oder als Umgebungsvariablen gesetzt werden.
## Was wird geändert?
@ -128,14 +185,28 @@ Wenn Ceph Public/Cluster auf separaten NICs liegen, werden die IPs einzeln pro N
## Migrationsablauf (Phase 4)
### Mit Passwort-Auth (empfohlen, 7 Schritte)
1. Neue Konfigurationen werden auf alle Nodes verteilt (Staging)
2. SSH-Keys sichern (`/etc/pve/priv/authorized_keys` → `~/.ssh/authorized_keys`)
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
> 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 aufräumen
8. Services starten, Quorum abwarten, Ceph aktualisieren, SSH-Keys wiederherstellen
## Rescue-Netzwerk (Emergency Mode)
@ -254,12 +325,12 @@ systemctl restart corosync
## Hinweise
- Das Tool muss als **root** ausgeführt werden
- SSH-Keys müssen **vorher** zwischen den Nodes eingerichtet sein (bei Proxmox-Clustern standardmäßig der Fall)
- 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)
- 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
- SSH-Keys werden vor dem pve-cluster-Stop gesichert und danach wiederhergestellt, damit SSH während der Migration nicht abbricht
- Getestet mit Proxmox VE 7.x und 8.x
## Projektstruktur
@ -270,12 +341,13 @@ proxmox-cluster-network-changer/
├── 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 (8 Schritte)
├── migrator.py # Phase 4: Migration durchführen
├── verifier.py # Phase 5: Post-Migration Checks
├── rescue.py # Rescue-Netzwerk (Emergency Mode)
├── ssh_manager.py # SSH-Verbindungen (lokal + remote)
├── ssh_manager.py # SSH-Verbindungen (Key + Passwort via sshpass)
├── config_parser.py # Parser für Corosync/Ceph/Network Configs
├── models.py # Dataclasses (NodeInfo, CorosyncConfig, etc.)
├── .env.example # Vorlage für SSH-Credentials
└── requirements.txt # Keine externen Dependencies
```
@ -284,6 +356,7 @@ 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
```

64
main.py
View File

@ -18,6 +18,8 @@ Verwendung:
import argparse
import os
import shutil
import subprocess
import sys
from ssh_manager import SSHManager
@ -29,6 +31,28 @@ from verifier import Verifier
from rescue import RescueNetwork
def load_env(env_file: str = ".env") -> dict[str, str]:
"""Load variables from a .env file (simple key=value parser)."""
env = {}
if not os.path.exists(env_file):
return env
with open(env_file, 'r') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
if '=' not in line:
continue
key, _, value = line.partition('=')
key = key.strip()
# Strip surrounding quotes
value = value.strip()
if len(value) >= 2 and value[0] in ('"', "'") and value[-1] == value[0]:
value = value[1:-1]
env[key] = value
return env
def check_prerequisites():
"""Check that we're running as root on a Proxmox node."""
if os.geteuid() != 0:
@ -75,6 +99,10 @@ def main():
help="Nur Rescue-Befehle ausgeben ohne Migration "
"(z.B. --rescue-commands 10.99.99.0/24)"
)
parser.add_argument(
"--env-file", type=str, default=".env",
help="Pfad zur .env-Datei mit SSH_PASSWORD (Standard: .env)"
)
args = parser.parse_args()
print("=" * 60)
@ -83,8 +111,42 @@ def main():
check_prerequisites()
# Load .env file
env = load_env(args.env_file)
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
# Initialize SSH manager
ssh = SSHManager(ssh_key=args.ssh_key, ssh_port=args.ssh_port)
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")
rescue = RescueNetwork(ssh)
# Quick mode: just print rescue commands and exit

View File

@ -31,44 +31,58 @@ 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("[1/8] Neue Konfigurationen verteilen...")
print(f"[{step}/{total}] Neue Konfigurationen verteilen...")
if not self._distribute_configs(plan, configs, dry_run):
return False
step += 1
# Step 2: Preserve SSH keys before stopping pve-cluster
# /etc/pve/priv/authorized_keys gets unmounted when pve-cluster stops!
print("\n[2/8] SSH-Keys sichern (werden nach pve-cluster stop benötigt)...")
# 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)")
# Step 3: Stop Corosync on all nodes
print("\n[3/8] Corosync stoppen auf allen Nodes...")
# Stop Corosync on all nodes
print(f"\n[{step}/{total}] Corosync stoppen auf allen Nodes...")
if not self._stop_corosync(reachable_nodes, dry_run):
return False
step += 1
# Step 4: Stop pve-cluster (pmxcfs) to release corosync.conf
print("\n[4/8] pve-cluster stoppen...")
# Stop pve-cluster (pmxcfs) to release corosync.conf
print(f"\n[{step}/{total}] pve-cluster stoppen...")
if not self._stop_pve_cluster(reachable_nodes, dry_run):
return False
step += 1
# Step 5: Write corosync config directly
print("\n[5/8] Corosync-Konfiguration aktualisieren...")
# Write corosync config directly
print(f"\n[{step}/{total}] Corosync-Konfiguration aktualisieren...")
if not self._update_corosync(reachable_nodes, configs, dry_run):
return False
step += 1
# Step 6: Update /etc/hosts on all nodes
print("\n[6/8] /etc/hosts aktualisieren...")
# Update /etc/hosts on all nodes
print(f"\n[{step}/{total}] /etc/hosts aktualisieren...")
if not self._update_hosts(plan, configs, dry_run):
return False
step += 1
# Step 7: Update network interfaces and restart networking
print("\n[7/8] Netzwerk-Interfaces aktualisieren und Netzwerk neu starten...")
# Update network interfaces and restart networking
print(f"\n[{step}/{total}] Netzwerk-Interfaces aktualisieren und Netzwerk neu starten...")
if not self._update_network(plan, configs, dry_run):
return False
step += 1
# Step 8: Start services back up
print("\n[8/8] Services starten...")
# Start services back up
print(f"\n[{step}/{total}] Services starten...")
if not self._start_services(plan, configs, dry_run):
return False
@ -456,7 +470,8 @@ class Migrator:
if configs.get('ceph'):
self._update_ceph(plan, configs)
# Restore original SSH keys (pve-cluster manages them again now)
# 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)

View File

@ -5,29 +5,75 @@ from typing import Optional
class SSHManager:
"""Manages SSH connections to Proxmox nodes using system ssh."""
"""Manages SSH connections to Proxmox nodes using 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_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 = [
cmd = []
if self.ssh_password:
cmd.extend(["sshpass", "-p", self.ssh_password])
cmd.extend([
"ssh",
"-o", "StrictHostKeyChecking=no",
"-o", "ConnectTimeout=10",
"-o", "BatchMode=yes",
"-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.
@ -63,8 +109,9 @@ class SSHManager:
Returns: (success, message)
"""
# Use heredoc via ssh to write file
escaped = content.replace("'", "'\\''")
cmd = self._build_ssh_cmd(host, f"cat > {path} << 'PROXMOX_NET_EOF'\n{content}\nPROXMOX_NET_EOF")
cmd = self._build_ssh_cmd(
host, f"cat > {path} << 'PROXMOX_NET_EOF'\n{content}\nPROXMOX_NET_EOF"
)
try:
result = subprocess.run(
cmd,
@ -138,3 +185,8 @@ 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