migrating machines in case extern network fails

Zabrolak

Member
Sep 25, 2022
4
0
6
Hello,

let's assume a 3 node cluster with ceph, node1/node2/node3 being let's say 10.0.0.1/2/3 respectively on external interfaces, and 10.10.10.1/2/3 on internal ones
(ceph+migration on 10.10.10.0/24, 'production' on 10.0.0.0/24).

let's assume node1 external network (10.0.0.1) goes dark for whatever reason -> Web UI connection fails, node is green (because internal is ok).

VMs on it are not accessible in production, of course.

I can log in to node1 from both node2/3 using ssh 10.10.10.1, I can issue
'qm migrate vmid node2 --online' or whatever similar -> the result is a failed migration due to not reachable external address (ssh tunnel to 10.0.0.2 cannot be created).

Is it my error in the cluster config? (cluster info shows link0 in 10.10.10.0/24 and link1 on another link, omitted for simplicity)

What would be the correct way to migrate VMs out of a node with failed external interface?

(I went with qm shutdown... followed by moving .conf files in /etc/pve/ to another nodes and starting, but I feel that's not THE RIGHT WAY)


Thanks

M
 
'qm migrate vmid node2 --online' or whatever similar -> the result is a failed migration due to not reachable external address (ssh tunnel to 10.0.0.2 cannot be created).

Did you test this and got the error above?
I assume since the migration network on a different network it should work, assuming you still have corosync quorum on the other nodes.
 
I think if you add
Code:
migration: type=secure,network=10.10.10.0/24
To your /etc/pve/datacenter.cfg file, it will them migrate on your 10.10.10.XX network
the node stayed green because HA health is derived from corosync link state, not from whether any given bridge uplink has carrier. There is no readiness probe concept in Proxmox that I am aware of. A VM can be completely unreachable while Proxmox reports the node as healthy. If you want it to migrate when the external link goes down, I think you need to script it. Something like this. Fair warning, I am not very good at coding, these were written with the help of AI, so check the code for yourself. And change the IPs as appropriate.

Code:
#!/bin/bash
#
# pve-uplink-watchdog.sh
#
# Watches this node's production (external) network path. If it fails while the
# internal cluster network stays healthy, puts the node into HA maintenance mode
# so HA-managed guests drain to a peer. Reverses automatically on recovery.
#
# REQUIRES: migration network pinned to the internal CIDR in
#           /etc/pve/datacenter.cfg, e.g.
#             migration: type=secure,network=10.10.10.0/24
#           Without that, the drain fails for the same reason manual migration does.
#
# Install:  /usr/local/sbin/pve-uplink-watchdog.sh  (chmod 750, root:root)
#

set -uo pipefail

### ------------------------- configuration -------------------------------- ###

# External targets that prove production reachability. Use the gateway plus at
# least one peer node external address. Any single success means we are UP.
EXTERNAL_TARGETS=("10.0.0.254" "10.0.0.2" "10.0.0.3")

# Peer addresses on the INTERNAL network. Used to sanity check whether this is a
# local fault or a site-wide outage.
INTERNAL_PEERS=("10.10.10.2" "10.10.10.3")

# Address on the internal network each peer should be able to reach externally.
# Usually the production gateway.
PEER_EXTERNAL_PROBE="10.0.0.254"

# Consecutive failed cycles before draining. At 10s interval, 6 = ~60s.
FAIL_THRESHOLD=6

# Consecutive successful cycles before undoing maintenance. Deliberately higher
# than FAIL_THRESHOLD so a flapping link does not ping-pong guests.
RECOVER_THRESHOLD=18

INTERVAL=10
PING_COUNT=2
PING_TIMEOUT=2
SSH_TIMEOUT=5

STATE_DIR="/run/pve-uplink-watchdog"
STATE_FILE="${STATE_DIR}/state"

# Set to 1 for a dry run. Logs the decision, changes nothing.
DRY_RUN=0

### ------------------------------------------------------------------------ ###

NODE="$(hostname -s)"
mkdir -p "$STATE_DIR"

log() { logger -t pve-uplink-watchdog -p "daemon.${1}" -- "$2"; }

# --- health probes ---------------------------------------------------------

external_up() {
    local t
    for t in "${EXTERNAL_TARGETS[@]}"; do
        if ping -c "$PING_COUNT" -W "$PING_TIMEOUT" -q "$t" &>/dev/null; then
            return 0
        fi
    done
    return 1
}

internal_up() {
    local p
    for p in "${INTERNAL_PEERS[@]}"; do
        if ping -c "$PING_COUNT" -W "$PING_TIMEOUT" -q "$p" &>/dev/null; then
            return 0
        fi
    done
    return 1
}

# Guard against a site-wide outage. Ask a peer, over the internal link, whether
# IT can still reach the production gateway. If no peer can, the fault is
# upstream and draining would only stampede guests onto equally blind nodes.
peer_confirms_local_fault() {
    local p
    for p in "${INTERNAL_PEERS[@]}"; do
        if ssh -o BatchMode=yes \
               -o StrictHostKeyChecking=accept-new \
               -o ConnectTimeout="$SSH_TIMEOUT" \
               "root@${p}" \
               "ping -c 2 -W 2 -q ${PEER_EXTERNAL_PROBE}" &>/dev/null; then
            log info "peer ${p} still reaches ${PEER_EXTERNAL_PROBE}; fault is local to ${NODE}"
            return 0
        fi
    done
    log warning "no peer can reach ${PEER_EXTERNAL_PROBE}; treating as site-wide outage, not draining"
    return 1
}

quorate() {
    pvecm status 2>/dev/null | grep -qi "Quorate:.*Yes"
}

# --- actions ---------------------------------------------------------------

enter_maintenance() {
    if [[ "$DRY_RUN" -eq 1 ]]; then
        log notice "DRY RUN: would enable node-maintenance on ${NODE}"
        return 0
    fi
    if ha-manager crm-command node-maintenance enable "$NODE"; then
        log notice "node-maintenance ENABLED on ${NODE}; HA guests draining"
        echo "maintenance" > "$STATE_FILE"
        return 0
    fi
    log err "failed to enable node-maintenance on ${NODE}"
    return 1
}

exit_maintenance() {
    if [[ "$DRY_RUN" -eq 1 ]]; then
        log notice "DRY RUN: would disable node-maintenance on ${NODE}"
        return 0
    fi
    if ha-manager crm-command node-maintenance disable "$NODE"; then
        log notice "node-maintenance DISABLED on ${NODE}; uplink recovered"
        echo "normal" > "$STATE_FILE"
        return 0
    fi
    log err "failed to disable node-maintenance on ${NODE}"
    return 1
}

# --- main loop -------------------------------------------------------------

[[ -f "$STATE_FILE" ]] || echo "normal" > "$STATE_FILE"

fail_count=0
ok_count=0

log info "started on ${NODE} (dry_run=${DRY_RUN}, threshold=${FAIL_THRESHOLD})"

while true; do
    state="$(cat "$STATE_FILE" 2>/dev/null || echo normal)"

    if external_up; then
        fail_count=0
        ok_count=$((ok_count + 1))

        if [[ "$state" == "maintenance" && "$ok_count" -ge "$RECOVER_THRESHOLD" ]]; then
            exit_maintenance && ok_count=0
        fi
    else
        ok_count=0
        fail_count=$((fail_count + 1))
        log warning "external path down (${fail_count}/${FAIL_THRESHOLD})"

        if [[ "$state" != "maintenance" && "$fail_count" -ge "$FAIL_THRESHOLD" ]]; then

            if ! internal_up; then
                log err "internal network also down; HA/fencing owns this, standing down"
                fail_count=0

            elif ! quorate; then
                log err "node not quorate; /etc/pve read-only, cannot drain"
                fail_count=0

            elif peer_confirms_local_fault; then
                enter_maintenance
                fail_count=0

            else
                fail_count=0
            fi
        fi
    fi

    sleep "$INTERVAL"
done

Here's the "pve-uplink-watchdog.service" file

Code:
[Unit]
Description=Proxmox external uplink watchdog (drains HA guests on production path loss)
After=network-online.target pve-cluster.service corosync.service
Wants=network-online.target
Requires=pve-cluster.service

[Service]
Type=simple
ExecStart=/usr/local/sbin/pve-uplink-watchdog.sh
Restart=always
RestartSec=15
User=root

# Give the cluster stack time to settle before probing after a reboot.
ExecStartPre=/bin/sleep 60

[Install]
WantedBy=multi-user.target