Einführung

In virtuellen Umgebungen mit Proxmox VE ist Cloud-Init ein mächtiges Werkzeug zur automatisierten Konfiguration von virtuellen Maschinen. Eine häufige Anforderung ist die Einrichtung benutzerdefinierter Netzwerkrouten für VMs. In diesem Blogbeitrag zeige ich, wie Sie statische Routen zu Ihrer Cloud-Init-Konfiguration hinzufügen können – sowohl manuell als auch mit einem praktischen Skript, das die Arbeit für Sie erledigt.

Das Problem

Bei der Arbeit mit Proxmox möchte man oft, dass virtuelle Maschinen beim Start bestimmte Netzwerkrouten einrichten. Dies kann besonders nützlich sein für:

  • VPN-Verbindungen
  • Zugriff auf isolierte Netzwerksegmente
  • Multi-Site-Umgebungen
  • Komplexe Netzwerktopologien

Die Standardkonfiguration von Cloud-Init im Proxmox Web-Interface bietet jedoch keine direkte Möglichkeit, statische Routen hinzuzufügen.

Die Lösung

Die Lösung besteht darin, eine benutzerdefinierte Cloud-Init-Netzwerkkonfiguration zu erstellen und diese auf die VM anzuwenden. Dazu gibt es zwei Hauptansätze:

  1. Manuelle Konfiguration: Erstellung einer benutzerdefinierten YAML-Datei
  2. Automatisierte Konfiguration: Verwenden eines Skripts, das die bestehende Konfiguration anpasst

In diesem Beitrag konzentriere ich mich auf die zweite Methode: Ein Bash-Skript, das automatisch statische Routen zu einer bestehenden Cloud-Init-VM hinzufügt.

Das Skript

Hier ist das vollständige Skript, das eine statische Route zu einer Cloud-Init-VM hinzufügt:

#!/bin/bash

# Script to add static routes to Cloud-Init configuration for a Proxmox VM
# Usage: ./add_static_route.sh VMID NETWORK GATEWAY [DESCRIPTION]
# Example: ./add_static_route.sh 100 192.168.10.0/24 10.0.0.1 "Route to internal network"

set -e

# Check if required parameters are provided
if [ $# -lt 3 ]; then
    echo "Usage: $0 VMID NETWORK GATEWAY [DESCRIPTION]"
    echo "Example: $0 100 192.168.10.0/24 10.0.0.1 \"Route to internal network\""
    exit 1
fi

VMID=$1
NETWORK=$2
GATEWAY=$3
DESCRIPTION=${4:-"Custom static route"}

# Verify that the VM exists
if ! qm status $VMID &>/dev/null; then
    echo "Error: VM with ID $VMID does not exist"
    exit 1
fi

# Check if the VM has cloud-init configured (on any IDE/SCSI device)
if ! qm config $VMID | grep -E "(ide|scsi)[0-9].*cloudinit" > /dev/null; then
    echo "Error: VM $VMID does not have Cloud-Init configured (no cloudinit drive found)"
    exit 1
fi

# Create a temporary directory for our work
TEMP_DIR=$(mktemp -d)
trap 'rm -rf "$TEMP_DIR"' EXIT

# Get current network cloud-init config
echo "Dumping current Cloud-Init network configuration..."
if ! qm cloudinit dump $VMID network > "$TEMP_DIR/network_original.yaml" 2>/dev/null; then
    echo "Info: No existing network configuration found, creating new one"
    # Create a basic network config structure if none exists
    cat > "$TEMP_DIR/network_original.yaml" << EOF
version: 2
ethernets:
  eth0:
    dhcp4: true
EOF
fi

# Make a backup of the original configuration
cp "$TEMP_DIR/network_original.yaml" "$TEMP_DIR/network_original.yaml.bak"

# Create a modified network configuration
echo "Creating modified configuration with static route..."
cp "$TEMP_DIR/network_original.yaml" "$TEMP_DIR/network_modified.yaml"

# Determine network configuration version
VERSION=$(grep -m 1 "version:" "$TEMP_DIR/network_original.yaml" | awk '{print $2}')
echo "Detected network configuration version: $VERSION"

if [ "$VERSION" = "1" ]; then
    # For version 1, we need to carefully modify the config to maintain proper indentation
    # First, let's create a new file with the correct structure
    cp "$TEMP_DIR/network_modified.yaml" "$TEMP_DIR/network_modified.yaml.tmp"
    
    # Find the physical section with eth0
    ETH0_SECTION=$(grep -n "name: eth0" "$TEMP_DIR/network_modified.yaml.tmp" | cut -d: -f1)
    if [ -z "$ETH0_SECTION" ]; then
        echo "Error: Could not find eth0 section"
        exit 1
    fi
    
    # Create a new file with the corrected format
    echo "version: 1" > "$TEMP_DIR/network_modified.yaml"
    echo "config:" >> "$TEMP_DIR/network_modified.yaml"
    
    # Process the file line by line with correct indentation
    FOUND_ETH0=false
    ADDED_ROUTES=false
    SUBNETS_SECTION=false
    
    while IFS= read -r line; do
        # Skip the version and config lines as we've already added them
        if [[ "$line" == "version: 1" || "$line" == "config:" ]]; then
            continue
        fi
        
        # Check if we're in the eth0 section
        if [[ "$line" == *"name: eth0"* ]]; then
            FOUND_ETH0=true
        fi
        
        # Check if we're in the subnets section 
        if [[ "$FOUND_ETH0" == true && "$line" == *"subnets:"* ]]; then
            SUBNETS_SECTION=true
        fi
        
        # Add the routes after the last subnet entry
        if [[ "$SUBNETS_SECTION" == true && "$line" == *"gateway:"* && "$ADDED_ROUTES" == false ]]; then
            echo "$line" >> "$TEMP_DIR/network_modified.yaml"
            echo "        routes:" >> "$TEMP_DIR/network_modified.yaml"
            echo "          - network: $NETWORK" >> "$TEMP_DIR/network_modified.yaml"
            echo "            gateway: $GATEWAY" >> "$TEMP_DIR/network_modified.yaml"
            ADDED_ROUTES=true
            continue
        fi
        
        # Add the line to the new file
        echo "$line" >> "$TEMP_DIR/network_modified.yaml"
        
    done < "$TEMP_DIR/network_modified.yaml.tmp"
    
    # If we didn't find a place to add routes, report an error
    if [[ "$ADDED_ROUTES" == false ]]; then
        echo "Error: Could not find an appropriate place to add routes"
        echo "Please manually modify the cloud-init configuration"
        exit 1
    fi
elif [ "$VERSION" = "2" ]; then
    # Check if the eth0 interface exists
    if ! grep -q "eth0:" "$TEMP_DIR/network_modified.yaml"; then
        echo "Error: Version 2 network configuration doesn't contain eth0 section"
        cat "$TEMP_DIR/network_original.yaml"
        exit 1
    fi
    
    # Check if routes section already exists
    if grep -q "routes:" "$TEMP_DIR/network_modified.yaml"; then
        # Add route to existing routes section
        sed -i "/routes:/a\\        - to: $NETWORK\\n          via: $GATEWAY\\n          # $DESCRIPTION" "$TEMP_DIR/network_modified.yaml"
    else
        # Add routes section after eth0
        sed -i "/eth0:/a\\      routes:\\n        - to: $NETWORK\\n          via: $GATEWAY\\n          # $DESCRIPTION" "$TEMP_DIR/network_modified.yaml"
    fi
else
    echo "Error: Unsupported network configuration version: $VERSION"
    cat "$TEMP_DIR/network_original.yaml"
    exit 1
fi

# Save the modified configuration to a snippets directory
SNIPPET_NAME="vm-${VMID}-custom-network-$(date +%s).yaml"
SNIPPETS_PATH="/var/lib/vz/snippets"

# Ensure snippets directory exists
if [ ! -d "$SNIPPETS_PATH" ]; then
    echo "Creating snippets directory at $SNIPPETS_PATH"
    mkdir -p "$SNIPPETS_PATH"
fi

# Create backup of original config
BACKUP_NAME="vm-${VMID}-network-backup-$(date +%s).yaml"
cp "$TEMP_DIR/network_original.yaml" "$SNIPPETS_PATH/$BACKUP_NAME"
echo "Original configuration backed up to $SNIPPETS_PATH/$BACKUP_NAME"

# Show differences between original and modified configuration
echo -e "\nConfiguration changes:"
diff -u "$TEMP_DIR/network_original.yaml" "$TEMP_DIR/network_modified.yaml" || true

# Ask for confirmation before applying changes
echo -e "\nReady to apply the new network configuration to VM $VMID."
echo "Do you want to continue? (y/N)"
read -r confirm
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
    echo "Operation cancelled by user."
    exit 0
fi

# Copy the modified configuration to snippets
cp "$TEMP_DIR/network_modified.yaml" "$SNIPPETS_PATH/$SNIPPET_NAME"
echo "Saved modified network configuration to $SNIPPETS_PATH/$SNIPPET_NAME"

# Set the custom network configuration for the VM
echo "Applying custom network configuration to VM $VMID..."
qm set $VMID --cicustom "network=local:snippets/$SNIPPET_NAME"

# Show the final configuration
echo -e "\nFinal Cloud-Init network configuration for VM $VMID:"
qm cloudinit dump $VMID network

echo -e "\nStatic route added successfully to VM $VMID"
echo "Network: $NETWORK"
echo "Gateway: $GATEWAY"
echo "Description: $DESCRIPTION"
echo -e "\nCustom configuration stored in: $SNIPPETS_PATH/$SNIPPET_NAME"
echo -e "Backup stored in: $SNIPPETS_PATH/$BACKUP_NAME"
echo -e "\nTo restore the original configuration if needed, run:"
echo "qm set $VMID --cicustom \"network=local:snippets/$BACKUP_NAME\""

Wie das Skript funktioniert

Das Skript führt folgende Schritte aus:

  1. Parameter prüfen: Überprüft, ob die notwendigen Parameter (VM-ID, Netzwerk, Gateway) angegeben wurden
  2. VM validieren: Stellt sicher, dass die VM existiert und mit Cloud-Init konfiguriert ist
  3. Konfiguration exportieren: Ruft die aktuelle Cloud-Init Netzwerkkonfiguration ab
  4. Konfigurationsformat erkennen: Unterscheidet zwischen Version 1 und Version 2 Konfigurationen
  5. Route hinzufügen: Fügt die statische Route im richtigen Format hinzu
  6. Backup erstellen: Sichert die ursprüngliche Konfiguration
  7. Änderungen anzeigen: Zeigt die Unterschiede zur Überprüfung an
  8. Bestätigung einholen: Fragt nach Bestätigung, bevor Änderungen angewendet werden
  9. Konfiguration anwenden: Wendet die neue Konfiguration auf die VM an

Unterschiedliche Cloud-Init Versionen

Ein wichtiger Aspekt ist die Unterstützung verschiedener Cloud-Init Konfigurationsformate:

Version 1 (Legacy-Format)

Das ältere Format verwendet diese Struktur für Routen:

version: 1
config:
  - type: physical
    name: eth0
    subnets:
      - type: static
        address: '10.0.0.2'
        netmask: '255.255.255.0'
        gateway: '10.0.0.1'
        routes:
          - network: '192.168.0.0/24'
            gateway: '10.0.0.254'

Version 2 (Netplan-Format)

Das neuere Format verwendet diese Struktur:

version: 2
ethernets:
  eth0:
    addresses: ['10.0.0.2/24']
    gateway4: '10.0.0.1'
    routes:
      - to: '192.168.0.0/24'
        via: '10.0.0.254'

Das Skript erkennt automatisch das verwendete Format und passt die Konfiguration entsprechend an.

Verwendung des Skripts

Um das Skript zu verwenden, speichern Sie es in einer Datei (z.B. add_static_route.sh), machen Sie es ausführbar und führen Sie es mit den erforderlichen Parametern aus:

chmod +x add_static_route.sh
./add_static_route.sh 100 192.168.10.0/24 10.0.0.1 "Route zu internem Netzwerk"

Die Parameter sind:

  1. VMID: Die ID der virtuellen Maschine
  2. NETWORK: Das Zielnetzwerk im CIDR-Format
  3. GATEWAY: Die Gateway-IP-Adresse
  4. DESCRIPTION (optional): Eine Beschreibung der Route

Sicherheitsfeatures

Das Skript enthält mehrere Sicherheitsfeatures, um Probleme zu vermeiden:

  1. Automatisches Backup: Die ursprüngliche Konfiguration wird gesichert
  2. Differenzanzeige: Änderungen werden vor dem Anwenden angezeigt
  3. Bestätigungsabfrage: Bestätigung wird vor dem Anwenden eingeholt
  4. Wiederherstellungsanleitung: Befehl zur Wiederherstellung wird angezeigt

Anwendungsfälle

Dieses Skript ist besonders nützlich für:

  1. Automatisierte Bereitstellung: Hinzufügen von Routen zu neu erstellten VMs
  2. Batch-Updates: Aktualisieren mehrerer VMs mit denselben Routingregeln
  3. VPN-Konfiguration: Einrichten von Routen zu entfernten Netzwerken über VPN
  4. Netzwerksegmentierung: Zugriff auf isolierte Netzwerksegmente

Erweiterungsmöglichkeiten

Das Skript kann auf verschiedene Weise erweitert werden:

  1. Mehrere Routen: Unterstützung für mehrere Routen in einem Durchlauf
  2. Template-Integration: Automatisches Anwenden auf alle aus einem Template erstellten VMs
  3. Metriken und Prioritäten: Hinzufügen von Routing-Metriken für fortgeschrittene Szenarien
  4. DNS-Konfiguration: Gleichzeitige Anpassung der DNS-Einstellungen

Fazit

Die Möglichkeit, statische Routen über Cloud-Init zu konfigurieren, ist ein mächtiges Feature für komplexere Netzwerkumgebungen in Proxmox. Mit dem vorgestellten Skript wird dieser Prozess automatisiert und fehlerresistent.

Dieses Werkzeug spart nicht nur Zeit bei der Konfiguration, sondern stellt auch sicher, dass VMs nach Neustarts oder Migration konsistent mit den richtigen Netzwerkrouten konfiguriert werden.

Ich hoffe, dieser Beitrag und das Skript sind für eure Proxmox-Umgebungen hilfreich. Viel Spaß beim Experimentieren und Optimieren eurer Cloud-Init-Konfigurationen!