#!/usr/bin/env bash
# scripts/geneve-encap-assert.sh
#
# GATE: OVN geneve underlay is HEALTHY and FAMILY-CONSISTENT across all chassis.
# Makes the phase-04 "geneve-over-v6 verified" gate EXECUTABLE (D-101 named it a
# verification gate; nothing asserted it -- ruled-not-built). Backed by the
# 2026-08-09 live root cause: docs/audit/geneve-over-v6-rootcause-20260808.md.
#
# TWO checks, both learned from measured failures on vr1-dc0:
# C1 FAMILY CONSISTENCY -- every chassis Encap.ip is the SAME address family.
# A split (containerized control v4 + metal compute v6) means cross-family
# tunnels never form (the 2026-08-08 root cause). Optional --expect-family
# v6|v4 also pins the family to the ruled one (parameterised, not hardcoded,
# so a legitimately v4-consistent cloud does not false-fail).
# C2 TUNNEL HEALTH -- every geneve tunnel interface has ofport >= 0. An ofport
# of -1 is an INVALID tunnel: the 2026-08-09 root cause was ovn-chassis 24.03
# emitting a BRACKETED v6 ovn-encap-ip ("[2602:...]") that OVS geneve rejects
# ("bad geneve 'remote_ip'"), leaving every v6 tunnel at ofport -1 while a
# family-only check (C1) still passes. C2 catches exactly that class.
#
# OFFLINE (testable): feed captured command output as files --
# --encap-ips FILE one chassis Encap.ip per line
# (live: ovn-sbctl --data=bare --no-heading --columns=ip list encap)
# --tunnel-ofports FILE one geneve-tunnel ofport per line, across ALL chassis
# (live: ovs-vsctl --data=bare --no-heading --columns=ofport find interface type=geneve)
# LIVE (integration): --live [MODEL] gathers both via juju on the DC rack (D-138).
# --expect-family v6|v4 optional: also assert the consistent family is this one.
#
# Usage:
# scripts/geneve-encap-assert.sh --encap-ips E.txt --tunnel-ofports T.txt [--expect-family v6]
# scripts/geneve-encap-assert.sh --live [MODEL] [--expect-family v6] # from the DC rack
# Exit: 0 all pass | 1 any FAIL | 2 usage/precondition. ASCII + LF.
set -uo pipefail
ENCAP_FILE=""; OFPORT_FILE=""; EXPECT_FAMILY=""; LIVE=0; MODEL="openstack"
FAIL=0
fail() { echo "FAIL: $*" >&2; FAIL=1; }
pass() { echo "PASS: $*"; }
die() { echo "USAGE: $*" >&2; exit 2; }
while [ $# -gt 0 ]; do
case "$1" in
--encap-ips) ENCAP_FILE="${2:-}"; shift 2 ;;
--tunnel-ofports) OFPORT_FILE="${2:-}"; shift 2 ;;
--expect-family) EXPECT_FAMILY="${2:-}"; shift 2 ;;
--live) LIVE=1; shift
# optional bare MODEL arg may follow
if [ $# -gt 0 ] && [ "${1#--}" = "$1" ]; then MODEL="$1"; shift; fi ;;
-h|--help) grep -E '^#' "$0" | sed 's/^# \{0,1\}//'; exit 2 ;;
*) die "unknown arg: $1" ;;
esac
done
[ -n "$EXPECT_FAMILY" ] && [ "$EXPECT_FAMILY" != v6 ] && [ "$EXPECT_FAMILY" != v4 ] \
&& die "--expect-family must be v6 or v4 (got '$EXPECT_FAMILY')"
# strip a surrounding [ ] and any quotes; return v4|v6|"" for an address string
fam_of() {
local a="$1"
a="${a//\"/}"; a="${a#[}"; a="${a%]}"
[ -z "$a" ] && { echo ""; return; }
case "$a" in *:*) echo v6 ;; *.*) echo v4 ;; *) echo "" ;; esac
}
if [ "$LIVE" -eq 1 ]; then
command -v juju >/dev/null 2>&1 || die "juju not found (run from the DC rack, D-138)"
ENCAP_FILE="$(mktemp)"; OFPORT_FILE="$(mktemp)"
trap 'rm -f "$ENCAP_FILE" "$OFPORT_FILE"' EXIT
juju exec -m "$MODEL" --unit ovn-central/leader -- \
sudo ovn-sbctl --data=bare --no-heading --columns=ip list encap </dev/null 2>/dev/null \
| tr -d '\r' | grep -vE '^\s*$' > "$ENCAP_FILE" || true
# ovn-chassis lives on nova-compute (subordinate) and octavia (ovn-chassis-octavia)
for app in nova-compute octavia; do
juju exec -m "$MODEL" --application "$app" -- \
sudo ovs-vsctl --data=bare --no-heading --columns=ofport find interface type=geneve \
</dev/null 2>/dev/null | tr -d '\r' | grep -E '^-?[0-9]+$' >> "$OFPORT_FILE" || true
done
fi
[ -n "$ENCAP_FILE" ] && [ -r "$ENCAP_FILE" ] || die "need --encap-ips FILE (or --live)"
[ -n "$OFPORT_FILE" ] && [ -r "$OFPORT_FILE" ] || die "need --tunnel-ofports FILE (or --live)"
# ---- C1: family consistency (+ optional expected family) --------------------
families=""; nencap=0
while IFS= read -r line; do
line="${line%%$'\r'}"; [ -z "${line// }" ] && continue
nencap=$((nencap + 1))
f="$(fam_of "$line")"
[ -z "$f" ] && { fail "C1: unparseable Encap.ip '$line'"; continue; }
case " $families " in *" $f "*) : ;; *) families="$families $f" ;; esac
done < "$ENCAP_FILE"
if [ "$nencap" -eq 0 ]; then
fail "C1: no chassis Encap.ip found -- cannot assert (refuse, not pass)"
else
nfam=$(echo $families | wc -w)
if [ "$nfam" -gt 1 ]; then
fail "C1: geneve encap SPLIT across families ($(echo $families)) over $nencap chassis -- cross-family tunnels cannot form"
else
pass "C1: all $nencap chassis Encap.ip are single-family ($(echo $families))"
if [ -n "$EXPECT_FAMILY" ] && [ "$(echo $families | tr -d ' ')" != "$EXPECT_FAMILY" ]; then
fail "C1: encap family is '$(echo $families | tr -d ' ')' but --expect-family=$EXPECT_FAMILY (ruled family)"
fi
fi
fi
# ---- C2: tunnel health (ofport >= 0 for every geneve tunnel) ----------------
ntun=0; bad=0
while IFS= read -r of; do
of="${of%%$'\r'}"; [ -z "${of// }" ] && continue
case "$of" in ''|*[!0-9-]*) fail "C2: unparseable ofport '$of'"; continue ;; esac
ntun=$((ntun + 1))
[ "$of" -lt 0 ] && bad=$((bad + 1))
done < "$OFPORT_FILE"
if [ "$ntun" -eq 0 ]; then
fail "C2: no geneve tunnel interfaces found -- cannot assert (refuse, not pass)"
elif [ "$bad" -gt 0 ]; then
fail "C2: $bad of $ntun geneve tunnels have ofport -1 (INVALID -- e.g. bracketed v6 ovn-encap-ip OVS rejects; see geneve-over-v6-rootcause-20260808.md)"
else
pass "C2: all $ntun geneve tunnel interfaces have ofport >= 0"
fi
if [ "$FAIL" -eq 0 ]; then echo "geneve-encap-assert: PASS"; exit 0; fi
echo "geneve-encap-assert: FAIL" >&2; exit 1