#!/bin/sh
# Swap rxg_sdan (module + ctl) to a build pulled straight from the builder,
# with no rXg involved.  Usage:  sdan-swap.sh [commit] [version]
#   SDAN_URL=<tarball url>  use that tarball instead (e.g. a tagged release)
#   FORCE_FLAT=1            accept a pre-1.20 tarball with no <release> level,
#                           only when you know it was built for this AP's release
#
# Snapshot -> swap -> replay.  rg_wss_client caches which tunnels and ports it
# believes exist and will not re-apply them, so a bare module reload leaves it
# describing a kernel that no longer matches.  Restoring the snapshot through
# the new ctl keeps the swap invisible to it.
#
# Reversible by design: any reboot or `ml restart` re-fetches whatever the rXg
# serves, so this cannot permanently strand an AP.  Management rides the
# upstream port, which sdan does not touch, so a failure costs client
# dataplane but never access.
set -u
COMMIT="${1:-5ac19e2}"
VERSION="${2:-1.18}"
BASE="https://build.rgnets.com/build/distfiles/rxg-sdan"
URL="${SDAN_URL:-$BASE/rxg_sdan-$VERSION-alpha-$COMMIT.tar.gz}"
KO=/root/rxg_sdan/rxg_sdan.ko
CTL=/usr/sbin/rxg_sdanctl
BK=/tmp/sdan-backup
W=/tmp/sdan-swap
KVER=$(uname -r)
# Same keys rg_wss_client uses: DISTRIB_TARGET (e.g. ipq60xx/generic) and the
# release reduced to major.minor ('25.12.3' -> 25.12, '23.05-SNAPSHOT' -> 23.05).
TARGET=$(. /etc/openwrt_release; echo "${DISTRIB_TARGET:-}")
REL=$(. /etc/openwrt_release; echo "${DISTRIB_RELEASE:-}" | sed -n 's/^\([0-9][0-9]*\.[0-9][0-9]*\).*/\1/p')

say() { echo "[sdan-swap] $*"; }

command -v rxg_sdanctl >/dev/null || { say "no rxg_sdanctl here"; exit 1; }

say "current: $(rxg_sdanctl version 2>&1 | head -1)"
say "target : $URL"
say "build  : $TARGET kernel $KVER release ${REL:-?}"

# --- snapshot -------------------------------------------------------------
ST=$(rxg_sdanctl -j status 2>/dev/null)
TUN=$(rxg_sdanctl -j tunnels 2>/dev/null)
UPSTREAM=$(echo "$ST" | sed -n 's/.*"upstream_dev":"\([^"]*\)".*/\1/p')
TIMEOUT=$(echo "$ST" | sed -n 's/.*"mac_timeout":\([0-9]*\).*/\1/p')
# Ports cannot be listed by the kernel, so derive them from the MAC table:
# the devs currently forwarding are exactly the ones worth re-registering.
PORTS=$(rxg_sdanctl -j macs 2>/dev/null | tr ',' '\n' \
        | sed -n 's/.*"dev":"\([^"]*\)".*/\1/p' | sort -u | tr '\n' ' ')
say "snapshot: upstream=$UPSTREAM timeout=$TIMEOUT ports='$PORTS'"
say "tunnels : $TUN"

# --- fetch ----------------------------------------------------------------
rm -rf "$W"; mkdir -p "$W" "$BK" || exit 1
cd "$W" || exit 1
say "fetching $URL"
wget -q -O pkg.tar.gz --timeout=90 "$URL" || { say "FETCH FAILED"; exit 1; }
tar xzf pkg.tar.gz || { say "EXTRACT FAILED"; exit 1; }

# Builds sit at <target>/<kernel>[/<release>]/. Match the target too: ipq50xx
# and ipq60xx both run 5.4.164. From 1.20 the release level matters as much,
# since 4.x (23.05) and 5.1 (25.12) share 5.4.164 with incompatible layouts,
# insmod cannot tell them apart, and the other tree's module hangs the AP.
# So never fall back to another release's build.
[ -n "$TARGET" ] || { say "DISTRIB_TARGET missing"; exit 1; }
if find . -name rxg_sdan.ko | grep -qE '/[0-9]+\.[0-9]+/rxg_sdan\.ko$'; then
    [ -n "$REL" ] || { say "DISTRIB_RELEASE has no major.minor; cannot pick a build"; exit 1; }
    NEWKO=$(find . -path "*/$TARGET/$KVER/$REL/rxg_sdan.ko" | head -1)
    NEWCTL=$(find . -path "*/$TARGET/$KVER/$REL/rxg_sdanctl" | head -1)
elif [ "${FORCE_FLAT:-}" = 1 ]; then
    # Pre-1.20 tarballs have no release level, so they cannot say which tree
    # they were built for; the caller has vouched for it.
    NEWKO=$(find . -path "*/$TARGET/$KVER/rxg_sdan.ko" | head -1)
    NEWCTL=$(find . -path "*/$TARGET/$KVER/rxg_sdanctl" | head -1)
else
    say "pre-1.20 tarball has no release level; rerun with FORCE_FLAT=1 only if it was built for $REL"
    exit 1
fi
if [ -z "$NEWKO" ] || [ -z "$NEWCTL" ]; then
    say "NO BUILD FOR $TARGET/$KVER/${REL:-?}. tarball has:"
    find . -name rxg_sdan.ko | sed 's|^\./||;s|/rxg_sdan.ko||' | sed 's/^/    /'
    exit 1
fi

# rmmod fails without unloading anything on 25.12's QSDK 5.4 kernels (empty
# modules.builtin.modinfo), so fall back to the delete_module syscall the way
# rg_wss_client does. Succeeds only once the module is gone.
unload_sdan() {
    grep -q '^rxg_sdan ' /proc/modules || return 0
    rmmod rxg_sdan 2>/dev/null
    grep -q '^rxg_sdan ' /proc/modules || return 0
    python3 -c 'import ctypes, os, sys; sys.exit(ctypes.CDLL(None, use_errno=True).delete_module(b"rxg_sdan", os.O_NONBLOCK) != 0)' 2>/dev/null
    ! grep -q '^rxg_sdan ' /proc/modules
}

# --- swap -----------------------------------------------------------------
cp "$KO" "$BK/rxg_sdan.ko.prev" 2>/dev/null
cp "$CTL" "$BK/rxg_sdanctl.prev" 2>/dev/null
say "swapping (backup in $BK)"
# Nothing has changed yet, so a module that will not unload is a clean stop.
unload_sdan || { say "UNLOAD FAILED - module still loaded (in use?); nothing changed"; exit 1; }
cp "$NEWKO" "$KO" || { say "INSTALL KO FAILED"; exit 1; }
cp "$NEWCTL" "$CTL" && chmod +x "$CTL" || { say "INSTALL CTL FAILED"; exit 1; }
if ! insmod "$KO"; then
    say "INSMOD FAILED - rolling back"
    cp "$BK/rxg_sdan.ko.prev" "$KO" 2>/dev/null
    cp "$BK/rxg_sdanctl.prev" "$CTL" 2>/dev/null; chmod +x "$CTL" 2>/dev/null
    insmod "$KO" 2>/dev/null && say "rolled back OK" || say "ROLLBACK FAILED"
    exit 1
fi

# --- replay ---------------------------------------------------------------
[ -n "$UPSTREAM" ] && rxg_sdanctl set-upstream "$UPSTREAM" >/dev/null && say "replay upstream=$UPSTREAM"
[ -n "$TIMEOUT" ]  && rxg_sdanctl timeout "$TIMEOUT"       >/dev/null && say "replay timeout=$TIMEOUT"
VLANS=""
echo "$TUN" | tr '}' '\n' | while read -r ent; do
    v=$(echo "$ent" | sed -n 's/.*"vlan":\([0-9]*\).*/\1/p')
    t=$(echo "$ent" | sed -n 's/.*"type":"\([^"]*\)".*/\1/p')
    r=$(echo "$ent" | sed -n 's/.*"remote":"\([^"]*\)".*/\1/p')
    [ -z "$v" ] && continue
    cmd="rxg_sdanctl add-tunnel $v ${t:-vlan}"
    [ -n "$r" ] && cmd="$cmd --remote $r"
    $cmd >/dev/null && echo "[sdan-swap] replay tunnel $v ${t:-vlan} ${r:+remote $r}"
done
V1=$(echo "$TUN" | sed -n 's/.*"vlan":\([0-9]*\).*/\1/p' | head -1)
if [ -n "$V1" ]; then
    for p in $PORTS; do
        out=$(rxg_sdanctl add-port "$p" "$V1" 2>&1)
        case "$out" in
            # rg_wss_client races us to re-register a VIF after the reload.
            # Already-present is the desired end state, not a failure.
            *"Object exists"*) say "replay port $p vlan $V1 (already present)" ;;
            *) [ -n "$out" ] && say "replay port $p vlan $V1" || say "replay port $p vlan $V1" ;;
        esac
    done
fi

# --- verify ---------------------------------------------------------------
NEW=$(rxg_sdanctl -j status 2>/dev/null)
say "now: $(rxg_sdanctl version 2>&1 | head -1)"
say "abi=$(echo "$NEW" | sed -n 's/.*"abi_version":\([0-9]*\).*/\1/p') tunnels=$(echo "$NEW" | sed -n 's/.*"tunnels":\([0-9]*\).*/\1/p') hooks=$(echo "$NEW" | sed -n 's/.*"ingress_hooks":\([0-9]*\).*/\1/p')"
echo "$NEW" | grep -q '"version_mismatch":false' \
  && say "DONE - ctl and module agree" \
  || { say "WARNING: ctl/module version mismatch"; exit 1; }
