feat: netcup-standby-guard — flipt netcup-multiplexer naar STANDBY bij comeback (Mac LaunchDaemon)
This commit is contained in:
parent
50919910c9
commit
909c78469b
3 changed files with 181 additions and 0 deletions
47
deploy/README.md
Normal file
47
deploy/README.md
Normal file
|
|
@ -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
|
||||||
|
```
|
||||||
109
deploy/netcup-standby-guard.sh
Executable file
109
deploy/netcup-standby-guard.sh
Executable file
|
|
@ -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
|
||||||
25
deploy/nl.eddydevink.netcup-standby-guard.plist
Normal file
25
deploy/nl.eddydevink.netcup-standby-guard.plist
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>nl.eddydevink.netcup-standby-guard</string>
|
||||||
|
<key>UserName</key>
|
||||||
|
<string>eddy</string>
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>/bin/bash</string>
|
||||||
|
<string>/usr/local/bin/netcup-standby-guard.sh</string>
|
||||||
|
</array>
|
||||||
|
<key>StartInterval</key>
|
||||||
|
<integer>60</integer>
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<true/>
|
||||||
|
<key>KeepAlive</key>
|
||||||
|
<false/>
|
||||||
|
<key>StandardOutPath</key>
|
||||||
|
<string>/Users/eddy/Library/Logs/netcup-standby-guard.log</string>
|
||||||
|
<key>StandardErrorPath</key>
|
||||||
|
<string>/Users/eddy/Library/Logs/netcup-standby-guard.log</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
Loading…
Reference in a new issue