#!/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
