#!/bin/sh
# rg_wss_client 1.0.38.1 hotfix installer for an rXg (FreeBSD). Run as root.
#
# 1.0.38.1 = the 1.0.38 field build (07beb38ec8) + the switch-VLAN fixes from
# !5631 (merged as 1.0.42) only: on WF-186h, per-client VLANs and wired-port
# isolation VLANs are written straight to switch hardware and re-asserted after
# every config apply, so a config push no longer leaves units / LAN ports dark,
# and the client no longer reloads the switch at startup when VLAN mode is
# already on.
#
# USAGE (from the csh root shell -- run it with sh):
#   fetch -o /tmp/el-hotfix-1.0.38.1.sh https://pre-alpha.henryhaller.com/el-hotfix-1.0.38.1.sh
#   sh /tmp/el-hotfix-1.0.38.1.sh            # check: download + verify, changes nothing
#   sh /tmp/el-hotfix-1.0.38.1.sh install    # back up + install the files, no restarts
#   sh /tmp/el-hotfix-1.0.38.1.sh restart    # reload the Rails processes that read the version
#   sh /tmp/el-hotfix-1.0.38.1.sh apply      # install + restart (single-node rXg only)
#   sh /tmp/el-hotfix-1.0.38.1.sh rollback   # restore the pre-hotfix files, no restarts
#
# CLUSTER: every node serves its own copy of the client and announces its own
# version. Run `install` on EVERY node first, then `restart` on every node. An
# AP that restarts while the nodes disagree can fetch the old client from a node
# that has not been updated, and then refuses to retry the same target version
# until it reboots (/tmp/rg_wss_client_upgrade_guard on the AP). Roll back the
# same way: `rollback` on every node, then `restart` on every node.
#
# install:
#   1. downloads the 1.0.38.1 client tarball + version.py and verifies sha256
#   2. refuses if 1.0.38.1 is already installed
#   3. backs up the served tarball(s), version.py and the client source dir to
#      $BACKUP_ROOT/<timestamp>; the first backup is also linked as
#      $BACKUP_ROOT/pre-hotfix, which is what rollback restores
#   4. installs the tarball (app/lib + public copies), overlays the source dir,
#      installs version.py
# restart reloads AnyCableRpc, OpenWifiWssProcessor, DelayedJob,
# RailsJobScheduler and RailsCluster (RailsCluster briefly interrupts the web
# UI), then checks that AnyCable RPC is listening again. It does NOT restart
# AnyCableGo or touch any AP, and no config is pushed. APs then move to the new
# version by themselves: the rXg's version check (every 15 min) pushes state to
# APs on another version, 50 APs/min, and each AP restarts its client after a
# short per-AP delay. The controller's 'Start WSS Client Update' action runs that
# check immediately.
set -u
MODE=${1:-check}
VERSION=1.0.38.1
BASE=https://pre-alpha.henryhaller.com/el-hotfix-1.0.38.1
TAR_SHA=760956646e8fddc27633362331fe2763d9f72eb62f425d0a4e9fe6454e9d6679
VER_SHA=0701902022fb8aba7b761773ec06772d3871036b25a31933d188e12904b84d5c

CONSOLE=/space/rxg/console
AP_DIR=$CONSOLE/app/lib/openwifi/ap
T_APP=$AP_DIR/rg_wss_client.tar.gz
T_PUB=$CONSOLE/public/rg_wss_client.tar.gz
T_PUBTAR=$CONSOLE/public/rg_wss_client.tar
VER_PY=$AP_DIR/rg_wss_client/version.py
WORK=/tmp/rg_wss_client-$VERSION
BACKUP_ROOT=/space/rxg/backup-rg_wss_client
PRE_HOTFIX=$BACKUP_ROOT/pre-hotfix
RPC_LOG=$CONSOLE/log/anycable.log
RPC_WAIT_S=90
APPS="AnyCableRpc OpenWifiWssProcessor DelayedJob RailsJobScheduler RailsCluster"

die() { echo "ERROR: $*" >&2; exit 1; }
say() { echo "== $*"; }
sha() { if command -v sha256 >/dev/null 2>&1; then sha256 -q "$1"; else sha256sum "$1" | cut -d' ' -f1; fi; }
served_version() { [ -f "$1" ] && tar -xzOf "$1" rg_wss_client/version.py 2>/dev/null | sed -n 's/^VERSION = "\(.*\)"/\1/p'; }
pyver() { [ -f "$1" ] && sed -n 's/^VERSION = "\(.*\)"/\1/p' "$1"; }
get() { if command -v fetch >/dev/null 2>&1; then fetch -q -o "$2" "$1"; else curl -fsS -o "$2" "$1"; fi; }
log_lines() { if [ -f "$RPC_LOG" ]; then wc -l < "$RPC_LOG" | tr -d ' '; else echo 0; fi; }

installed() {
    [ "$(served_version "$T_APP")" = "$VERSION" ] && [ "$(sha "$T_APP")" = "$TAR_SHA" ] && [ "$(pyver "$VER_PY")" = "$VERSION" ]
}

download() {
    rm -rf "$WORK" && mkdir -p "$WORK" || die "cannot create $WORK"
    get "$BASE/rg_wss_client-$VERSION.tar.gz" "$WORK/rg_wss_client.tar.gz" || die "download of the tarball failed"
    get "$BASE/version-$VERSION.py" "$WORK/version.py" || die "download of version.py failed"
    [ "$(sha "$WORK/rg_wss_client.tar.gz")" = "$TAR_SHA" ] || die "tarball sha256 mismatch"
    [ "$(sha "$WORK/version.py")" = "$VER_SHA" ] || die "version.py sha256 mismatch"
    [ "$(served_version "$WORK/rg_wss_client.tar.gz")" = "$VERSION" ] || die "tarball does not contain VERSION $VERSION"
    [ "$(pyver "$WORK/version.py")" = "$VERSION" ] || die "version.py is not $VERSION"
    say "downloaded and verified $VERSION (sha256 OK)"
}

install_files() {
    installed && die "$VERSION is already installed on this node -- nothing to do (run 'restart' if the processes were not reloaded)"
    download

    TS=$(date +%Y%m%d-%H%M%S)
    B=$BACKUP_ROOT/$TS
    mkdir -p "$BACKUP_ROOT" && mkdir "$B" || die "cannot create $B (it may already exist -- wait a second and retry)"
    cp -p "$T_APP" "$B/app-rg_wss_client.tar.gz" || die "backup of $T_APP failed"
    [ -f "$T_PUB" ] && cp -p "$T_PUB" "$B/public-rg_wss_client.tar.gz"
    [ -f "$T_PUBTAR" ] && cp -p "$T_PUBTAR" "$B/public-rg_wss_client.tar"
    tar -czf "$B/rg_wss_client-dir.tgz" -C "$AP_DIR" rg_wss_client || die "backup of the source dir failed"
    say "backed up to $B"
    if [ -e "$PRE_HOTFIX" ]; then
        say "keeping $PRE_HOTFIX -> $(readlink "$PRE_HOTFIX") as the rollback target"
    elif [ "$(served_version "$T_APP")" = "$VERSION" ] || [ "$(pyver "$VER_PY")" = "$VERSION" ]; then
        say "WARNING: these files are already partly $VERSION, so this backup is not marked pre-hotfix"
    else
        ln -s "$B" "$PRE_HOTFIX" || die "cannot create $PRE_HOTFIX"
        say "rollback target: $PRE_HOTFIX -> $B"
    fi

    install -m 644 "$WORK/rg_wss_client.tar.gz" "$T_APP" || die "install of $T_APP failed"
    if [ -f "$T_PUB" ]; then install -m 644 "$WORK/rg_wss_client.tar.gz" "$T_PUB"; fi
    if [ -f "$T_PUBTAR" ]; then gunzip -c "$WORK/rg_wss_client.tar.gz" > "$T_PUBTAR.new" && mv "$T_PUBTAR.new" "$T_PUBTAR"; fi
    tar -xzf "$WORK/rg_wss_client.tar.gz" -C "$AP_DIR" || die "overlay of the source dir failed"
    install -m 644 "$WORK/version.py" "$VER_PY" || die "install of version.py failed"
    say "installed:  app tarball=$(served_version "$T_APP")  public tarball=$(served_version "$T_PUB")  version.py=$(pyver "$VER_PY")"
}

# No AP can connect while AnyCable RPC is down.
check_rpc() {
    start=$1
    waited=0
    while [ "$waited" -lt "$RPC_WAIT_S" ]; do
        if tail -n +"$((start + 1))" "$RPC_LOG" 2>/dev/null | grep -q 'RPC server is listening'; then
            say "AnyCable RPC is listening again"
            return 0
        fi
        sleep 5
        waited=$((waited + 5))
    done
    echo "WARNING: AnyCable RPC has not come back up within ${RPC_WAIT_S}s. Contact RG Nets."
    return 1
}

restart_apps() {
    rpc_start=$(log_lines)
    for a in $APPS; do
        say "rrapp $a"
        rrapp "$a" || echo "   (rrapp $a returned non-zero)"
        sleep 3
    done
    check_rpc "$rpc_start"
}

[ "$(id -u)" = 0 ] || die "run as root"
[ -d "$AP_DIR" ] || die "$AP_DIR not found -- is this an rXg?"

say "currently served:  app tarball=$(served_version "$T_APP")  public tarball=$(served_version "$T_PUB")  version.py=$(pyver "$VER_PY")"
if [ -e "$PRE_HOTFIX" ]; then say "rollback target: $PRE_HOTFIX -> $(readlink "$PRE_HOTFIX")"; fi

case "$MODE" in
check)
    if installed; then
        say "$VERSION is already installed on this node"
    else
        download
        say "check only. On a cluster: 'sh $0 install' on every node, then 'sh $0 restart' on every node."
        say "On a single rXg: 'sh $0 apply'."
    fi
    ;;
install)
    install_files
    say "done. Run 'sh $0 install' on every other node, then 'sh $0 restart' on every node."
    ;;
restart)
    restart_apps
    say "done. APs move to $(pyver "$VER_PY") on their own within ~15 min (or now via the"
    say "openwifi_wss controller's 'Start WSS Client Update' action)."
    ;;
apply)
    install_files
    restart_apps
    say "done. APs move to $VERSION on their own within ~15 min (or now via the"
    say "openwifi_wss controller's 'Start WSS Client Update' action)."
    say "rollback: 'sh $0 rollback', then 'sh $0 restart'"
    ;;
rollback)
    B=$(readlink "$PRE_HOTFIX" 2>/dev/null) || true
    [ -n "$B" ] && [ -d "$B" ] || die "no pre-hotfix backup found at $PRE_HOTFIX"
    say "restoring from $B"
    install -m 644 "$B/app-rg_wss_client.tar.gz" "$T_APP" || die "restore of $T_APP failed"
    [ -f "$B/public-rg_wss_client.tar.gz" ] && install -m 644 "$B/public-rg_wss_client.tar.gz" "$T_PUB"
    [ -f "$B/public-rg_wss_client.tar" ] && install -m 644 "$B/public-rg_wss_client.tar" "$T_PUBTAR"
    rm -rf "$AP_DIR/rg_wss_client" && tar -xzf "$B/rg_wss_client-dir.tgz" -C "$AP_DIR" || die "restore of the source dir failed"
    say "restored:  app tarball=$(served_version "$T_APP")  version.py=$(pyver "$VER_PY")"
    say "done. Run 'sh $0 rollback' on every other node, then 'sh $0 restart' on every node."
    ;;
*)
    die "usage: sh $0 [check|install|restart|apply|rollback]"
    ;;
esac
