From 82ddb25c01c69c7727f2f76fb7e95019c3ead838 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Wed, 4 Mar 2026 22:27:31 +0100 Subject: [PATCH] update ssh keys authorization file --- config_parser.py | 8 +++-- migrator.py | 84 +++++++++++++++++++++++++++++++++++++++++------- planner.py | 35 +++++++++++++++++--- 3 files changed, 109 insertions(+), 18 deletions(-) diff --git a/config_parser.py b/config_parser.py index d332227..c82b326 100644 --- a/config_parser.py +++ b/config_parser.py @@ -91,11 +91,15 @@ def parse_ceph_conf(content: str) -> CephConfig: if m: config.cluster_network = m.group(1) - # Extract mon_host + # Extract mon_host (can be comma-separated, space-separated, or both) m = re.search(r'mon.host\s*=\s*(.+)', content) if m: hosts_str = m.group(1).strip() - config.mon_hosts = [h.strip() for h in hosts_str.split(',') if h.strip()] + # Split by comma or whitespace + config.mon_hosts = [ + h.strip() for h in re.split(r'[,\s]+', hosts_str) + if h.strip() + ] # Extract [mon.X] sections mon_sections = re.findall( diff --git a/migrator.py b/migrator.py index 8cf472e..a4d7b73 100644 --- a/migrator.py +++ b/migrator.py @@ -36,33 +36,39 @@ class Migrator: if not self._distribute_configs(plan, configs, dry_run): return False - # Step 2: Stop Corosync on all nodes - print("\n[2/7] Corosync stoppen auf allen Nodes...") + # 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)...") + if not self._preserve_ssh_keys(reachable_nodes, dry_run): + return False + + # Step 3: Stop Corosync on all nodes + print("\n[3/8] Corosync stoppen auf allen Nodes...") if not self._stop_corosync(reachable_nodes, dry_run): return False - # Step 3: Stop pve-cluster (pmxcfs) to release corosync.conf - print("\n[3/7] pve-cluster stoppen...") + # Step 4: Stop pve-cluster (pmxcfs) to release corosync.conf + print("\n[4/8] pve-cluster stoppen...") if not self._stop_pve_cluster(reachable_nodes, dry_run): return False - # Step 4: Write corosync config directly - print("\n[4/7] Corosync-Konfiguration aktualisieren...") + # Step 5: Write corosync config directly + print("\n[5/8] Corosync-Konfiguration aktualisieren...") if not self._update_corosync(reachable_nodes, configs, dry_run): return False - # Step 5: Update /etc/hosts on all nodes - print("\n[5/7] /etc/hosts aktualisieren...") + # Step 6: Update /etc/hosts on all nodes + print("\n[6/8] /etc/hosts aktualisieren...") if not self._update_hosts(plan, configs, dry_run): return False - # Step 6: Update network interfaces and restart networking - print("\n[6/7] Netzwerk-Interfaces aktualisieren und Netzwerk neu starten...") + # Step 7: Update network interfaces and restart networking + print("\n[7/8] Netzwerk-Interfaces aktualisieren und Netzwerk neu starten...") if not self._update_network(plan, configs, dry_run): return False - # Step 7: Start services back up - print("\n[7/7] Services starten...") + # Step 8: Start services back up + print("\n[8/8] Services starten...") if not self._start_services(plan, configs, dry_run): return False @@ -157,6 +163,56 @@ class Migrator: return True + def _preserve_ssh_keys(self, nodes: list, dry_run: bool) -> bool: + """Copy /etc/pve/priv/authorized_keys to ~/.ssh/ on all nodes. + + When pve-cluster (pmxcfs) is stopped, /etc/pve gets unmounted and + the cluster SSH keys disappear. This breaks SSH between nodes. + We temporarily copy them to ~/.ssh/authorized_keys so SSH keeps working. + """ + for node in nodes: + if dry_run: + print(f" [{node.name}] Würde SSH-Keys sichern") + continue + + # Append pve keys to ~/.ssh/authorized_keys (avoid duplicates) + cmd = ( + "if [ -f /etc/pve/priv/authorized_keys ]; then " + " mkdir -p /root/.ssh && " + " cp /root/.ssh/authorized_keys /root/.ssh/authorized_keys.pre_migration 2>/dev/null; " + " cat /etc/pve/priv/authorized_keys >> /root/.ssh/authorized_keys && " + " sort -u -o /root/.ssh/authorized_keys /root/.ssh/authorized_keys && " + " chmod 600 /root/.ssh/authorized_keys && " + " echo ok; " + "else " + " echo no_pve_keys; " + "fi" + ) + rc, stdout, err = self.ssh.run_on_node( + node.ssh_host, cmd, node.is_local + ) + if rc == 0 and "ok" in stdout: + print(f" [{node.name}] SSH-Keys gesichert") + elif "no_pve_keys" in stdout: + print(f" [{node.name}] Keine PVE-Keys gefunden (übersprungen)") + else: + print(f" [{node.name}] WARNUNG SSH-Keys: {err}") + return True + + def _restore_ssh_keys(self, nodes: list): + """Restore original ~/.ssh/authorized_keys 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 && " + " echo restored; " + "else " + " echo no_backup; " + "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: @@ -337,6 +393,10 @@ class Migrator: if configs.get('ceph'): self._update_ceph(plan, configs) + # Restore original SSH keys (pve-cluster manages them again now) + 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: diff --git a/planner.py b/planner.py index e87ef10..b98f965 100644 --- a/planner.py +++ b/planner.py @@ -34,12 +34,39 @@ class Planner: # Detect old network from first node if nodes: old_ip = ipaddress.ip_address(nodes[0].current_ip) - for iface in nodes[0].interfaces: - if iface.address == str(old_ip): - plan.old_network = f"{ipaddress.ip_network(f'{iface.address}/{iface.cidr}', strict=False)}" - plan.bridge_name = iface.name + # Try to find matching interface + for node in nodes: + for iface in node.interfaces: + if iface.address == str(old_ip) or ( + iface.address and iface.cidr and + ipaddress.ip_address(iface.address) in + ipaddress.ip_network(f'{iface.address}/{iface.cidr}', strict=False) and + old_ip in ipaddress.ip_network(f'{iface.address}/{iface.cidr}', strict=False) + ): + plan.old_network = str(ipaddress.ip_network( + f'{iface.address}/{iface.cidr}', strict=False + )) + plan.bridge_name = iface.name + break + if plan.old_network: break + # Fallback: try to guess from corosync IPs + if not plan.old_network: + # Find common network from all corosync node IPs + for cidr_guess in [24, 16, 8]: + net = ipaddress.ip_network( + f'{nodes[0].current_ip}/{cidr_guess}', strict=False + ) + if all(ipaddress.ip_address(n.current_ip) in net for n in nodes): + plan.old_network = str(net) + break + + if plan.old_network: + print(f" Erkanntes altes Netzwerk: {plan.old_network}") + else: + print(" [!] Altes Netzwerk konnte nicht erkannt werden") + # Generate IP mapping suggestions print("\n[IP-Mapping]") print("Für jeden Node wird eine neue IP benötigt.\n")