server/broker-cutover.sh: passwd/acl to dynamic security, with rollback

One reviewed step instead of a hand-typed sequence on the host: back up
the config, convert the passwd file into the plugin's store with every
hash intact, rewrite mosquitto.conf, restart, and prove the server and
the health probe reconnect. ROLLBACK=1 restores the previous config.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
This commit is contained in:
2026-09-19 11:56:38 +05:30
parent 4c750cb2ac
commit c93fbff31f

59
server/broker-cutover.sh Executable file
View File

@@ -0,0 +1,59 @@
#!/usr/bin/env bash
# Move a running broker from passwd/acl files to the dynamic-security plugin,
# keeping every existing login and password. Run AFTER deploy.sh has put a
# server on the host that has `broker-init`.
#
# server/broker-cutover.sh do it
# ROLLBACK=1 server/broker-cutover.sh put the previous config back
#
# What it does: backs up mosquitto/config, converts passwd → the plugin's store
# inside the broker's data volume (same hashes, so no shop PC re-claims),
# rewrites mosquitto.conf, restarts the broker, and proves the server and the
# health probe reconnect. Every step before the restart is reversible by not
# doing the restart; the rollback restores the backed-up config and restarts.
set -euo pipefail
HOST=${HOST:-root@66.116.226.161}
KEY=${KEY:-$HOME/.ssh/behavision_deploy}
DIR=/root/behavision
SSH=(ssh -i "$KEY" -o BatchMode=yes -o ConnectTimeout=10 "$HOST")
step() { printf '\n\033[1m%s\033[0m\n' "$*"; }
if [ -n "${ROLLBACK:-}" ]; then
step "Rolling back to the passwd/acl configuration"
"${SSH[@]}" "cd $DIR && latest=\$(ls -d mosquitto/config.bak-* | tail -1) && cp \$latest/mosquitto.conf mosquitto/config/mosquitto.conf && docker compose restart mosquitto && sleep 3 && docker logs --tail 5 behavision-mqtt"
exit 0
fi
step "1. Back up the broker configuration"
"${SSH[@]}" "cd $DIR && cp -a mosquitto/config mosquitto/config.bak-\$(date +%Y%m%d-%H%M%S) && ls -d mosquitto/config.bak-* | tail -1"
step "2. Convert passwd into the plugin's store (hashes unchanged)"
# The data volume belongs to the broker's user (1883); the init runs as root to
# write there and then hands the file over. Refuses if a store already exists.
"${SSH[@]}" "cd $DIR && docker run --rm --user root \
-v $DIR/mosquitto/config:/m:ro -v behavision_mosquitto-data:/d \
--entrypoint /usr/local/bin/behavision-server behavision-backend:latest \
broker-init -passwd /m/passwd -out /d/dynamic-security.json \
&& docker run --rm --user root -v behavision_mosquitto-data:/d alpine:3.20 sh -c 'chown 1883:1883 /d/dynamic-security.json && chmod 600 /d/dynamic-security.json && ls -la /d/dynamic-security.json'"
step "3. Rewrite mosquitto.conf for the plugin"
"${SSH[@]}" "cd $DIR && python3 - <<'PY'
import re
p = 'mosquitto/config/mosquitto.conf'
s = open(p).read()
s = re.sub(r'^per_listener_settings\s+true\s*$', 'per_listener_settings false', s, flags=re.M)
s = re.sub(r'^(password_file|acl_file)\s+.*\n', '', s, flags=re.M)
if 'mosquitto_dynamic_security' not in s:
s = s.rstrip('\n') + '\n\n# Logins and topic permissions live in the dynamic-security plugin now.\n# The server creates a shop\'s login over the control topic; nothing is\n# edited by hand and nothing is reloaded.\nplugin /usr/lib/mosquitto_dynamic_security.so\nplugin_opt_config_file /mosquitto/data/dynamic-security.json\n'
open(p, 'w').write(s)
print(open(p).read())
PY"
step "4. Restart the broker"
"${SSH[@]}" "cd $DIR && docker compose restart mosquitto && sleep 4 && docker logs --tail 8 behavision-mqtt 2>&1 | grep -i 'error\|plugin\|running\|connected' | tail -6"
step "5. Prove the server and the health probe are back"
"${SSH[@]}" "cd $DIR && sleep 6 && docker logs --since 30s behavision-backend 2>&1 | grep -i 'broker\|subscribed' | tail -3; docker inspect behavision-mqtt --format 'health: {{.State.Health.Status}}' 2>/dev/null || true; docker logs --since 40s behavision-mqtt 2>&1 | grep -i 'not authori\|denied' | head -3 || true"
echo
echo "If step 5 shows 'subscribed to bv/#' and no 'not authorised', the cutover is done."
echo "Anything wrong: ROLLBACK=1 server/broker-cutover.sh"