diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..f186efa --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,47 @@ +# netcup-standby-guard (Mac LaunchDaemon) + +Zet de wuzapi-multiplexer op **netcup** automatisch op `STANDBY` zodra netcup weer +bereikbaar is, maar **alleen** als nextcloud actief is. Voorkomt dubbel-antwoorden in de +WhatsApp-groepen wanneer netcup terugkomt terwijl nextcloud (na handmatige flip) de +actieve node is. + +## Waarom / invariant + +- De multiplexer is de standby-guard (`src/multiplexer.js`): node zonder `STANDBY` + routeert webhooks door, node met `STANDBY=1` ACKt alleen (200). +- Bij een netcup-uitval kan de netcup-unit niet op STANDBY gezet worden → na de + handmatige flip van nextcloud naar ACTIEF zijn beide nodes bij netcup-comeback actief. + Deze guard flipt netcup dan binnen één poll-interval (60 s) naar STANDBY. +- **Invariant:** netcup wordt alleen naar STANDBY geflipt als nextcloud bevestigd ACTIEF + is (geen `STANDBY=1` in de nextcloud-unit). Staat nextcloud op STANDBY, dan is + netcup-actief een bewuste failover (nextcloud-onderhoud) → geen flip. + +## Installatie (Mac) + +```bash +sudo -A cp deploy/netcup-standby-guard.sh /usr/local/bin/netcup-standby-guard.sh +sudo -A chmod 755 /usr/local/bin/netcup-standby-guard.sh +sudo -A cp deploy/nl.eddydevink.netcup-standby-guard.plist /Library/LaunchDaemons/ +sudo -A launchctl bootstrap system /Library/LaunchDaemons/nl.eddydevink.netcup-standby-guard.plist +sudo -A launchctl kickstart -k system/nl.eddydevink.netcup-standby-guard # eerste run direct +``` + +- Draait als `eddy` (UserName in plist) → gebruikt de aliassen uit `~/.ssh/config` + (`netcup` met key `~/.ssh/netcup_agent_ed25519` zonder passphrase, en `nextcloud`). +- Notificatie via Gotify: token in `~/.netcup-guard.env` (`GOTIFY_TOKEN=...`, mode 600, + gitignored). Ontbreekt het bestand → alleen loggen. +- Log: `~/Library/Logs/netcup-standby-guard.log`; status: `~/.netcup-guard-state`. + +## Handmatige test + +```bash +/usr/local/bin/netcup-standby-guard.sh # run als eddy +tail -5 ~/Library/Logs/netcup-standby-guard.log +``` + +## Opruimen + +```bash +sudo -A launchctl bootout system/nl.eddydevink.netcup-standby-guard +sudo -A rm /Library/LaunchDaemons/nl.eddydevink.netcup-standby-guard.plist /usr/local/bin/netcup-standby-guard.sh +``` \ No newline at end of file diff --git a/deploy/netcup-standby-guard.sh b/deploy/netcup-standby-guard.sh new file mode 100755 index 0000000..ac9fe84 --- /dev/null +++ b/deploy/netcup-standby-guard.sh @@ -0,0 +1,109 @@ +#!/bin/bash +# netcup-standby-guard — wuzapi-multiplexer op netcup automatisch naar STANDBY flippen +# zodra netcup weer bereikbaar is, MAAR alleen als nextcloud actief is. +# +# Waarom: bij een netcup-uitval kan de netcup-unit niet op STANDBY gezet worden. Na de +# handmatige nextcloud-flip (zie vault "WhatsApp Failover netcup") zijn beide nodes +# actief zodra netcup terugkomt -> dubbel-antwoorden in de groepen. Deze guard detecteert +# netcup-comeback en zet de netcup-multiplexer binnen een poll-interval (60 s) op STANDBY. +# Invariant: nooit beide nodes tegelijk forwardend. Staat nextcloud op STANDBY, dan is +# netcup-actief een bewuste failover (nextcloud-onderhoud) -> geen flip. +# +# Draait als Mac LaunchDaemon nl.eddydevink.netcup-standby-guard (UserName=eddy). +# Installatie/werking: deploy/README.md + vault "WhatsApp Failover netcup" -> guard-sectie. +set -uo pipefail + +NETCUP="aiaiaiaiagent@netcup" +NEXTCLOUD="aiaiaiaiagent@nextcloud" +UNIT=/etc/systemd/system/wuzapi-multiplexer.service +STATEFILE="$HOME/.netcup-guard-state" +LOG="$HOME/Library/Logs/netcup-standby-guard.log" +LOCK=/tmp/netcup-standby-guard.lock + +log() { printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S %z')" "$*" >>"$LOG"; } +ssh_nc() { ssh -o ConnectTimeout=6 -o BatchMode=yes "$NETCUP" "$@"; } +ssh_nccl() { ssh -o ConnectTimeout=6 -o BatchMode=yes "$NEXTCLOUD" "$@"; } +state() { cat "$STATEFILE" 2>/dev/null || echo initial; } + +notify() { + local title="$1" msg="$2" + if [ ! -f "$HOME/.netcup-guard.env" ]; then + log "notificatie overgeslagen (geen ~/.netcup-guard.env): $title — $msg" + return + fi + # shellcheck source=/dev/null + . "$HOME/.netcup-guard.env" # GOTIFY_TOKEN= + if curl -s -m10 -X POST "https://gotify.eddydevink.nl/message?token=${GOTIFY_TOKEN:-}" \ + -H 'Content-Type: application/json' \ + -d "{\"title\":\"$title\",\"message\":\"$msg\",\"priority\":5}" >/dev/null 2>&1; then + log "notificatie verstuurd: $title" + else + log "notificatie MISLUKT (gotify niet bereikbaar?): $title" + fi +} + +# single-instance (StartInterval kan overlappen bij trage ssh-runs) +if ! mkdir "$LOCK" 2>/dev/null; then + # stale lock opruimen: proces weg + lock ouder dan 10 min + if [ -f "$LOCK/pid" ] && ! kill -0 "$(cat "$LOCK/pid" 2>/dev/null)" 2>/dev/null \ + && [ -n "$(find "$LOCK" -mmin +10 2>/dev/null)" ]; then + rm -rf "$LOCK" + mkdir "$LOCK" 2>/dev/null || exit 0 + else + exit 0 + fi +fi +echo $$ >"$LOCK/pid" +trap 'rm -rf "$LOCK"' EXIT + +# 1) netcup bereikbaar? +if ! ssh_nc 'true' >/dev/null 2>&1; then + [ "$(state)" = "down" ] || log "netcup onbereikbaar — wacht op comeback" + echo down >"$STATEFILE" + exit 0 +fi + +# 2) netcup-multiplexer al op STANDBY? +nc_st=$(ssh_nc "grep -c '^Environment=STANDBY=1' $UNIT 2>/dev/null || echo 0") || exit 0 +if [ "${nc_st:-0}" -ge 1 ]; then + [ "$(state)" = "flipped" ] || log "netcup-multiplexer staat al op STANDBY — niets te doen" + echo flipped >"$STATEFILE" + exit 0 +fi + +# 3) nextcloud moet actief zijn; anders is netcup-actief een bewuste failover +nccl_st=$(ssh_nccl "grep -c '^Environment=STANDBY=1' $UNIT 2>/dev/null || echo 0") +case "${nccl_st:-}" in + ''|*[!0-9]*) log "nextcloud-unit onleesbaar — netcup niet aanraken (kan bewuste failover zijn)"; echo unknown >"$STATEFILE"; exit 0;; +esac +if [ "$nccl_st" -ge 1 ]; then + [ "$(state)" = "deliberate" ] || log "nextcloud staat op STANDBY -> netcup actief is de bewuste richting; geen flip" + echo deliberate >"$STATEFILE" + exit 0 +fi + +# 4) FLIP: netcup zonder STANDBY terwijl nextcloud actief is +log "FLIP: netcup terug + nextcloud actief -> netcup-multiplexer op STANDBY zetten" +if ssh_nc "sudo -A sed -i '/^Environment=MERPLUS_WEBHOOK=.*/a Environment=STANDBY=1' $UNIT && sudo -A systemctl daemon-reload && sudo -A systemctl restart wuzapi-multiplexer"; then + sleep 2 + ok=1 + ssh_nc "grep -q '^Environment=STANDBY=1' $UNIT" || ok=0 + ssh_nc "systemctl is-active wuzapi-multiplexer | grep -q active" || ok=0 + # neutrale webhook-test: STANDBY-melding in het multiplexer-log = bewijs + ssh_nc "curl -s -X POST http://127.0.0.1:3007/webhook -H 'Content-Type: application/json' -d '{\"type\":\"Message\",\"event\":{\"Info\":{\"Chat\":\"999999999999999999@g.us\"}}}' >/dev/null" + sleep 1 + ssh_nc "sudo -A journalctl -u wuzapi-multiplexer -n 3 -o cat | grep -q 'STANDBY — webhook genegeerd'" || ok=0 + if [ "$ok" -eq 1 ]; then + log "FLIP OK — netcup-multiplexer STANDBY + active + webhook wordt genegeerd" + notify "✅ netcup is terug online" "wuzapi-multiplexer op netcup staat nu op STANDBY — nextcloud blijft de actieve WhatsApp-node." + else + log "FLIP VERIFICATIE MISLUKT — handmatig checken vereist!" + [ "$(state)" = "flipfail" ] || notify "⚠️ netcup-standby-guard: verificatie mislukt" "netcup is terug, maar de standby-flip kon niet geverifieerd worden. Log: $LOG" + fi + echo flipped >"$STATEFILE" +else + log "FLIP MISLUKT (ssh/sudo) — volgende poll probeert opnieuw" + [ "$(state)" = "flipfail" ] || notify "⚠️ netcup is terug, maar standby-flip faalde" "ssh/sudo naar netcup mislukte. Log: $LOG" + echo flipfail >"$STATEFILE" +fi +exit 0 \ No newline at end of file diff --git a/deploy/nl.eddydevink.netcup-standby-guard.plist b/deploy/nl.eddydevink.netcup-standby-guard.plist new file mode 100644 index 0000000..4577fd6 --- /dev/null +++ b/deploy/nl.eddydevink.netcup-standby-guard.plist @@ -0,0 +1,25 @@ + + + + + Label + nl.eddydevink.netcup-standby-guard + UserName + eddy + ProgramArguments + + /bin/bash + /usr/local/bin/netcup-standby-guard.sh + + StartInterval + 60 + RunAtLoad + + KeepAlive + + StandardOutPath + /Users/eddy/Library/Logs/netcup-standby-guard.log + StandardErrorPath + /Users/eddy/Library/Logs/netcup-standby-guard.log + + \ No newline at end of file