#!/bin/bash # OVN Compute Node Setup for Spinifex VPC Networking # # This script bootstraps a compute node for OVN-based VPC networking: # 1. Installs OVN/OVS packages (if not present) # 2. Enables required services (openvswitch-switch, ovn-controller) # 3. Creates br-int with secure fail-mode # 4. Configures OVS external_ids for OVN chassis identity # 5. Applies sysctl tuning for overlay networking # # Usage: # ./scripts/setup-ovn.sh [options] # # Options: # --management Also start OVN central services (NB DB, SB DB, ovn-northd) # --external-bridge Create br-external for public subnet WAN uplink # --external-iface WAN NIC to add to br-external (default: eth1) # --dhcp Obtain gateway IP via DHCP on the external bridge interface # --ovn-remote OVN SB DB address (default: tcp:127.0.0.1:6642) # --encap-ip Geneve tunnel endpoint IP (default: auto-detect) # --chassis-id OVN chassis identifier (default: hostname) # # Examples: # # Single-node development (management + compute on same host): # ./scripts/setup-ovn.sh --management # # # With external bridge (macvlan on WAN NIC — SSH-safe): # ./scripts/setup-ovn.sh --management --external-bridge --external-iface=eth0 # # # Compute node joining an existing cluster: # ./scripts/setup-ovn.sh --ovn-remote=tcp:10.0.0.1:6642 --encap-ip=10.0.0.2 # # # Multi-node dev cluster (node1 = management): # Node 1: ./scripts/setup-ovn.sh --management --encap-ip=127.0.0.1 # Node 2: ./scripts/setup-ovn.sh --ovn-remote=tcp:127.0.0.1:6642 --encap-ip=127.0.0.2 # Node 3: ./scripts/setup-ovn.sh --ovn-remote=tcp:127.0.0.1:6642 --encap-ip=127.0.0.3 set -e # Defaults MANAGEMENT=false EXTERNAL_BRIDGE=false EXTERNAL_DHCP=false EXTERNAL_IFACE="eth1" OVN_REMOTE="tcp:127.0.0.1:6642" ENCAP_IP="" CHASSIS_ID="" # Parse arguments for arg in "$@"; do case "$arg" in --management) MANAGEMENT=true ;; --external-bridge) EXTERNAL_BRIDGE=true ;; --dhcp) EXTERNAL_DHCP=true ;; --external-iface=*) EXTERNAL_IFACE="${arg#*=}" ;; --ovn-remote=*) OVN_REMOTE="${arg#*=}" ;; --encap-ip=*) ENCAP_IP="${arg#*=}" ;; --chassis-id=*) CHASSIS_ID="${arg#*=}" ;; --help|-h) head -36 "$0" | tail -34 exit 0 ;; *) echo "Unknown option: $arg" exit 1 ;; esac done # Auto-detect encap IP if not specified if [ -z "$ENCAP_IP" ]; then ENCAP_IP=$(ip -4 route get 8.8.8.8 2>/dev/null | awk '/src/{print $7}' | head -1) if [ -z "$ENCAP_IP" ]; then ENCAP_IP="127.0.0.1" fi echo "Auto-detected encap IP: $ENCAP_IP" fi # Auto-detect chassis ID if not specified if [ -z "$CHASSIS_ID" ]; then CHASSIS_ID="chassis-$(hostname -s)" echo "Auto-detected chassis ID: $CHASSIS_ID" fi echo "=== Spinifex OVN Compute Node Setup ===" echo " Management node: $MANAGEMENT" echo " External bridge: $EXTERNAL_BRIDGE" if [ "$EXTERNAL_BRIDGE" = true ]; then echo " External iface: $EXTERNAL_IFACE" fi echo " OVN Remote (SB): $OVN_REMOTE" echo " Encap IP: $ENCAP_IP" echo " Chassis ID: $CHASSIS_ID" echo "" # --- Step 1: Install packages --- echo "Step 1: Checking OVN/OVS packages..." install_packages() { local missing=() for pkg in openvswitch-switch ovn-host; do if ! dpkg -s "$pkg" >/dev/null 2>&1; then missing+=("$pkg") fi done if [ "$MANAGEMENT" = true ]; then if ! dpkg -s ovn-central >/dev/null 2>&1; then missing+=("ovn-central") fi fi if [ ${#missing[@]} -gt 0 ]; then echo " Installing: ${missing[*]}" sudo apt-get update -qq sudo apt-get install -y -qq "${missing[@]}" else echo " All packages installed" fi } install_packages # --- Step 2: Enable services --- echo "" echo "Step 2: Enabling services..." sudo systemctl enable openvswitch-switch sudo systemctl start openvswitch-switch echo " openvswitch-switch: started" if [ "$MANAGEMENT" = true ]; then sudo systemctl enable ovn-central sudo systemctl start ovn-central echo " ovn-central: started (NB DB + SB DB + ovn-northd)" # Wait for OVN NB DB socket to become available for i in $(seq 1 15); do if sudo ovn-nbctl --timeout=2 get-connection >/dev/null 2>&1; then break fi echo " Waiting for OVN NB DB... ($i/15)" sleep 1 done # Allow remote connections to NB and SB databases sudo ovn-nbctl set-connection ptcp:6641 sudo ovn-sbctl set-connection ptcp:6642 echo " OVN NB DB listening on tcp:6641" echo " OVN SB DB listening on tcp:6642" fi # --- Step 3: Create and configure br-int --- echo "" echo "Step 3: Configuring br-int..." sudo ovs-vsctl --may-exist add-br br-int sudo ovs-vsctl set Bridge br-int fail-mode=secure sudo ovs-vsctl set Bridge br-int other-config:disable-in-band=true sudo ip link set br-int up echo " br-int: created, fail-mode=secure, up" # --- Step 3b: Create br-external for WAN uplink (optional) --- if [ "$EXTERNAL_BRIDGE" = true ]; then echo "" echo "Step 3b: Configuring br-external for public subnet WAN uplink..." # Verify the WAN NIC exists if ! ip link show "$EXTERNAL_IFACE" >/dev/null 2>&1; then echo " ERROR: interface $EXTERNAL_IFACE does not exist" echo " Available interfaces:" ip -o link show | awk -F': ' '{print " " $2}' exit 1 fi # Create br-external sudo ovs-vsctl --may-exist add-br br-external sudo ip link set br-external up # macvlan strategy: create a macvlan sub-interface in bridge mode off the # WAN NIC. The host keeps its IP on the parent NIC — SSH-safe. OVN localnet # traffic flows through the macvlan to the physical wire. MACVLAN_NAME="spx-ext-${EXTERNAL_IFACE}" if ip link show "$MACVLAN_NAME" >/dev/null 2>&1; then echo " macvlan $MACVLAN_NAME already exists" else sudo ip link add "$MACVLAN_NAME" link "$EXTERNAL_IFACE" type macvlan mode bridge echo " created macvlan: $MACVLAN_NAME (bridge mode) on $EXTERNAL_IFACE" fi sudo ip link set "$MACVLAN_NAME" up sudo ovs-vsctl --may-exist add-port br-external "$MACVLAN_NAME" echo " br-external: created with macvlan port $MACVLAN_NAME" echo " NOTE: host keeps its IP on $EXTERNAL_IFACE (no migration)" echo " QUIRK: host cannot reach VMs at their public IPs (macvlan isolation)" # --- DHCP: obtain gateway IP for OVN SNAT --- if [ "$EXTERNAL_DHCP" = true ]; then echo "" echo "Step 3c: Obtaining external gateway IP via DHCP..." # DHCP on the macvlan interface (it has L2 access to the WAN) DHCP_IFACE="spx-ext-${EXTERNAL_IFACE}" # Run DHCP client to get a lease if command -v dhcpcd >/dev/null 2>&1; then sudo dhcpcd --waitip=4 --timeout 15 "$DHCP_IFACE" 2>/dev/null || true elif command -v dhclient >/dev/null 2>&1; then sudo dhclient -1 -timeout 15 "$DHCP_IFACE" 2>/dev/null || true else echo " WARNING: no DHCP client found (dhcpcd or dhclient)" echo " Install dhcpcd-base or isc-dhcp-client, or set gateway_ip manually" fi # Read the obtained IP DHCP_IP=$(ip -4 addr show dev "$DHCP_IFACE" 2>/dev/null | awk '/inet /{print $2}' | head -1 | cut -d/ -f1) if [ -n "$DHCP_IP" ]; then echo " DHCP obtained: $DHCP_IP on $DHCP_IFACE" # Write the gateway IP to the spinifex config so vpcd can use it CONFIG_DIR="${CONFIG_DIR:-$HOME/spinifex/config}" CONFIG_FILE="$CONFIG_DIR/spinifex.toml" if [ -f "$CONFIG_FILE" ]; then # Update gateway_ip in the config if grep -q "gateway_ip" "$CONFIG_FILE"; then sed -i "s/gateway_ip.*/gateway_ip = \"$DHCP_IP\"/" "$CONFIG_FILE" else # Insert gateway_ip after the gateway line in the pool section sed -i "/^gateway *=.*/a gateway_ip = \"$DHCP_IP\"" "$CONFIG_FILE" fi echo " Updated $CONFIG_FILE with gateway_ip = $DHCP_IP" else echo " WARNING: $CONFIG_FILE not found — set gateway_ip manually" fi else echo " WARNING: DHCP failed to obtain IP on $DHCP_IFACE" echo " VMs will not have external connectivity until gateway_ip is configured" fi fi fi # --- Step 4: Configure OVN external_ids --- echo "" echo "Step 4: Setting OVS external_ids for OVN..." BRIDGE_MAPPINGS="external:br-external" if [ "$EXTERNAL_BRIDGE" = true ]; then sudo ovs-vsctl set Open_vSwitch . \ external_ids:system-id="$CHASSIS_ID" \ external_ids:ovn-remote="$OVN_REMOTE" \ external_ids:ovn-encap-ip="$ENCAP_IP" \ external_ids:ovn-encap-type="geneve" \ external_ids:ovn-bridge-mappings="$BRIDGE_MAPPINGS" echo " ovn-bridge-mappings: $BRIDGE_MAPPINGS" else sudo ovs-vsctl set Open_vSwitch . \ external_ids:system-id="$CHASSIS_ID" \ external_ids:ovn-remote="$OVN_REMOTE" \ external_ids:ovn-encap-ip="$ENCAP_IP" \ external_ids:ovn-encap-type="geneve" fi echo " system-id: $CHASSIS_ID" echo " ovn-remote: $OVN_REMOTE" echo " ovn-encap-ip: $ENCAP_IP" echo " ovn-encap-type: geneve" # --- Step 5: Start ovn-controller --- echo "" echo "Step 5: Starting ovn-controller..." sudo systemctl start ovn-controller echo " ovn-controller: started" # --- Step 6: Sysctl tuning --- echo "" echo "Step 6: Applying sysctl for overlay networking..." sudo tee /etc/sysctl.d/99-spinifex-vpc.conf >/dev/null <<'SYSCTL' # Spinifex VPC networking: enable IP forwarding and disable rp_filter # for Geneve overlay traffic on OVS bridges. net.ipv4.ip_forward=1 net.ipv4.conf.all.rp_filter=0 net.ipv4.conf.default.rp_filter=0 SYSCTL sudo sysctl --system -q echo " ip_forward=1, rp_filter=0" # --- Step 6b: Ensure data NIC routing for Geneve tunnels --- echo "" echo "Step 6b: Configuring data NIC routing for Geneve tunnels..." # When management and data NICs share the same subnet (e.g. both on 10.1.0.0/16), # the kernel may route Geneve tunnel traffic through the management NIC with the # wrong source IP. This causes remote OVS nodes to drop incoming tunnel packets # because the source IP doesn't match the configured tunnel remote_ip. # Fix: lower the route metric on the data NIC so it's preferred. DATA_IFACE=$(ip -o -4 addr show | awk -v ip="$ENCAP_IP" '$0 ~ ip"/" {print $2}') if [ -n "$DATA_IFACE" ]; then SUBNET=$(ip -o -4 route show dev "$DATA_IFACE" proto kernel scope link | awk '{print $1}' | head -1) if [ -n "$SUBNET" ]; then sudo ip route replace "$SUBNET" dev "$DATA_IFACE" src "$ENCAP_IP" metric 50 echo " data route: $SUBNET via $DATA_IFACE src $ENCAP_IP (metric 50)" else echo " skipped: no kernel route found for $DATA_IFACE" fi else echo " skipped: could not find interface for $ENCAP_IP" fi # --- Step 7: Verify Geneve kernel support --- echo "" echo "Step 7: Verifying Geneve kernel module..." if sudo modprobe geneve 2>/dev/null; then echo " geneve module: loaded" else echo " WARNING: geneve module not available (tunnels may not work)" fi # --- Step 8: Grant non-root access to OVS/OVN --- echo "" echo "Step 8: Configuring non-root access..." # Open OVS DB socket so non-root processes can use ovs-vsctl OVS_SOCK="/var/run/openvswitch/db.sock" if [ -S "$OVS_SOCK" ]; then sudo chmod 0666 "$OVS_SOCK" echo " OVS DB socket: opened ($OVS_SOCK)" fi # Open OVN runtime directory and ctl sockets for ovs-appctl access if [ -d "/var/run/ovn" ]; then sudo chmod 0755 /var/run/ovn sudo chmod 0666 /var/run/ovn/*.ctl 2>/dev/null || true echo " OVN ctl sockets: opened (/var/run/ovn/)" fi if [ -d "/var/run/openvswitch" ]; then sudo chmod 0666 /var/run/openvswitch/*.ctl 2>/dev/null || true fi # Create persistent systemd override so permissions survive OVS restarts OVERRIDE_DIR="/etc/systemd/system/openvswitch-switch.service.d" if [ ! -f "$OVERRIDE_DIR/spinifex-perms.conf" ]; then sudo mkdir -p "$OVERRIDE_DIR" sudo tee "$OVERRIDE_DIR/spinifex-perms.conf" >/dev/null <<'OVERRIDE' [Service] ExecStartPost=/bin/chmod 0666 /var/run/openvswitch/db.sock OVERRIDE sudo systemctl daemon-reload echo " systemd override: created (db.sock permissions persist across restarts)" else echo " systemd override: already exists" fi # Create sudoers rule for network commands that always need root # (ip tuntap, ip link set — NET_ADMIN operations) SUDOERS_FILE="/etc/sudoers.d/spinifex-network" if [ ! -f "$SUDOERS_FILE" ]; then CURRENT_USER=$(whoami) sudo tee "$SUDOERS_FILE" >/dev/null </dev/null 2>&1; then sudo rm -f /var/log/ovn/*.log.log* echo " cleaned up stale .log.log files" fi LOGROTATE_FILE="/etc/logrotate.d/ovn-spinifex" sudo tee "$LOGROTATE_FILE" >/dev/null <<'LOGROTATE' /var/log/ovn/ovn-controller.log /var/log/ovn/ovn-northd.log /var/log/ovn/ovsdb-server-nb.log /var/log/ovn/ovsdb-server-sb.log { daily rotate 3 maxsize 100M compress missingok notifempty copytruncate } LOGROTATE echo " logrotate config: $LOGROTATE_FILE (daily, 100M max, 3 rotations, explicit filenames)" # --- Step 10: Disable auto-start on boot --- # start-dev.sh / stop-dev.sh manage the OVN lifecycle. Without spinifex services # running, ovn-controller spins in a tight reconnect loop burning CPU. echo "" echo "Step 10: Disabling OVN auto-start on boot (start-dev.sh will manage lifecycle)..." sudo systemctl disable openvswitch-switch 2>/dev/null || true sudo systemctl disable ovn-controller 2>/dev/null || true echo " openvswitch-switch: disabled on boot" echo " ovn-controller: disabled on boot" # --- Step 11: Health check --- echo "" echo "Step 11: Verifying setup..." OK=true # Check br-int if sudo ovs-vsctl br-exists br-int; then echo " br-int: OK" else echo " br-int: FAILED" OK=false fi # Check br-external (only if --external-bridge was used) if [ "$EXTERNAL_BRIDGE" = true ]; then if sudo ovs-vsctl br-exists br-external; then echo " br-external: OK" MACVLAN_NAME="spx-ext-${EXTERNAL_IFACE}" if ip link show "$MACVLAN_NAME" >/dev/null 2>&1; then echo " macvlan: OK ($MACVLAN_NAME)" else echo " macvlan: FAILED ($MACVLAN_NAME not found)" OK=false fi else echo " br-external: FAILED" OK=false fi fi # Check ovn-controller if sudo ovs-appctl -t ovn-controller version >/dev/null 2>&1 || systemctl is-active --quiet ovn-controller 2>/dev/null; then echo " ovn-controller: OK" else echo " ovn-controller: FAILED (may still be starting)" OK=false fi # Check chassis registration (may take a moment) if [ "$MANAGEMENT" = true ]; then sleep 2 CHASSIS_COUNT=$(sudo ovn-sbctl show 2>/dev/null | grep -c "Chassis" || true) echo " chassis count: $CHASSIS_COUNT" fi echo "" if [ "$OK" = true ]; then echo "=== OVN compute node setup complete ===" else echo "=== Setup completed with warnings (check above) ===" fi