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)
- 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
View File

@ -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

View File

@ -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:

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
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