Add MQTT backup reporting and trigger listener
This commit is contained in:
1 parent
a1ce3b19bc
commit
d73e00db1c
9 files changed
+197
-1
No files matched your search
@@ -14,6 +14,15 @@ START_RETRIES=1
|
||||
LOCK_FILE="/run/lock/zorruno-docker-backup.lock"
|
||||
ZSTD_EXTRA_OPTS="-T0 -12"
|
||||
|
||||
# Optional retained MQTT status and command listener settings.
|
||||
MQTT_REPORT_ENABLED=0
|
||||
MQTT_HOST="beaver"
|
||||
MQTT_PORT=1883
|
||||
MQTT_USERNAME="dockerbackup"
|
||||
MQTT_PASSWORD_FILE="/etc/zorruno-docker-backup/mqtt-password"
|
||||
MQTT_STATUS_TOPIC="viewroad-status/dockerbackup/beaver"
|
||||
MQTT_COMMAND_TOPIC="viewroad-commands/dockerbackup/beaver/run"
|
||||
|
||||
# Rclone destination becomes <remote>/beaver/.
|
||||
RCLONE_ENABLED=0
|
||||
RCLONE_REMOTE="gdrive:dockerbackups"
|
||||
|
||||
@@ -135,6 +135,48 @@ sudo stages/90-prune.sh --dry-run
|
||||
`--force` runs a transfer method even when its `*_ENABLED` value is false. It
|
||||
still requires the method's destination to be configured and available.
|
||||
|
||||
## MQTT reporting and Home Assistant trigger
|
||||
|
||||
Enable retained run results in the host's `.env`:
|
||||
|
||||
```bash
|
||||
MQTT_REPORT_ENABLED=1
|
||||
MQTT_HOST="beaver"
|
||||
MQTT_PORT=1883
|
||||
MQTT_USERNAME="dockerbackup"
|
||||
MQTT_PASSWORD_FILE="/etc/zorruno-docker-backup/mqtt-password"
|
||||
MQTT_STATUS_TOPIC="viewroad-status/dockerbackup/panda"
|
||||
MQTT_COMMAND_TOPIC="viewroad-commands/dockerbackup/panda/run"
|
||||
```
|
||||
|
||||
The retained status JSON includes overall status, timestamps, duration, exit
|
||||
code, container totals, successful backups, dry-run counts, and error messages.
|
||||
Final reporting runs from an exit trap, so failures in any earlier stage are
|
||||
reported when MQTT remains available.
|
||||
|
||||
Install the optional root-run command listener with:
|
||||
|
||||
```bash
|
||||
cp systemd/zorruno-docker-backup-mqtt.service /etc/systemd/system/
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now zorruno-docker-backup-mqtt.service
|
||||
```
|
||||
|
||||
It accepts only JSON containing the exact action `run` on the configured
|
||||
command topic:
|
||||
|
||||
```json
|
||||
{"action":"run"}
|
||||
```
|
||||
|
||||
Publish commands without MQTT retain. The listener also ignores retained
|
||||
messages, preventing an old command from starting another backup after a
|
||||
listener or broker restart.
|
||||
|
||||
Restrict publish access to the command topic with broker ACLs. The listener
|
||||
runs as root because the backup stage controls Docker containers. The existing
|
||||
pipeline lock prevents concurrent runs.
|
||||
|
||||
## Adding another backup method
|
||||
|
||||
Add an executable script named `methods/NN-name.sh`, for example
|
||||
|
||||
@@ -16,6 +16,32 @@ while [[ $# -gt 0 ]]; do
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
BACKUP_RUN_ID="$(run_id)"
|
||||
BACKUP_STARTED_EPOCH="$(date +%s)"
|
||||
BACKUP_RUN_LOG="$(mktemp "${TMPDIR:-/tmp}/zorruno-docker-backup.XXXXXX.log")"
|
||||
export BACKUP_RUN_ID BACKUP_STARTED_EPOCH BACKUP_RUN_LOG
|
||||
|
||||
finish_run() {
|
||||
local exit_code=$? finished status
|
||||
trap - EXIT
|
||||
set +e
|
||||
finished="$(date +%s)"
|
||||
if (( exit_code == 0 )); then
|
||||
if (( DRY_RUN )); then status="dry_run_success"; else status="success"; fi
|
||||
else
|
||||
if (( DRY_RUN )); then status="dry_run_failed"; else status="failed"; fi
|
||||
fi
|
||||
if is_true "$MQTT_REPORT_ENABLED"; then
|
||||
"$SCRIPT_DIR/stages/99-mqtt-report.sh" "$status" "$exit_code" \
|
||||
"$BACKUP_STARTED_EPOCH" "$finished" "$BACKUP_RUN_ID" "$BACKUP_RUN_LOG" \
|
||||
|| log ERROR "Unable to publish final MQTT result"
|
||||
fi
|
||||
rm -f -- "$BACKUP_RUN_LOG"
|
||||
exit "$exit_code"
|
||||
}
|
||||
trap finish_run EXIT
|
||||
|
||||
require_command flock
|
||||
mkdir -p "$(dirname "$LOCK_FILE")"
|
||||
exec 9>"$LOCK_FILE"
|
||||
@@ -24,6 +50,11 @@ stage_args=()
|
||||
(( DRY_RUN )) && stage_args+=(--dry-run)
|
||||
[[ -n "$CONTAINERS_OVERRIDE" ]] && stage_args+=(--containers "$CONTAINERS_OVERRIDE")
|
||||
log START "Backup pipeline for $BACKUP_HOSTNAME"
|
||||
if is_true "$MQTT_REPORT_ENABLED"; then
|
||||
"$SCRIPT_DIR/stages/99-mqtt-report.sh" running 0 "$BACKUP_STARTED_EPOCH" \
|
||||
"$BACKUP_STARTED_EPOCH" "$BACKUP_RUN_ID" "$BACKUP_RUN_LOG" \
|
||||
|| log WARN "Unable to publish MQTT running state; backup will continue"
|
||||
fi
|
||||
(( SKIP_CONTAINERS )) || "$SCRIPT_DIR/stages/10-containers.sh" "${stage_args[@]}"
|
||||
method_args=(); (( DRY_RUN )) && method_args+=(--dry-run)
|
||||
for method in $BACKUP_METHODS; do
|
||||
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
# Listen for an authenticated JSON command and run the backup pipeline.
|
||||
set -Eeuo pipefail
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)"
|
||||
. "$SCRIPT_DIR/lib/common.sh"
|
||||
. "$SCRIPT_DIR/lib/mqtt.sh"
|
||||
|
||||
require_command jq
|
||||
require_command mosquitto_sub
|
||||
[[ -n "$MQTT_HOST" ]] || die "MQTT_HOST is not configured"
|
||||
mqtt_common_args
|
||||
log MQTT "Listening on $MQTT_COMMAND_TOPIC"
|
||||
|
||||
# Never replay a retained command after this listener or the MQTT broker restarts.
|
||||
# Home Assistant should also publish run commands without the retain flag.
|
||||
mosquitto_sub "${MQTT_ARGS[@]}" -R -t "$MQTT_COMMAND_TOPIC" | while IFS= read -r payload; do
|
||||
action="$(jq -er '.action // empty' <<<"$payload" 2>/dev/null || true)"
|
||||
if [[ "$action" != "run" ]]; then
|
||||
log WARN "Ignored invalid MQTT command"
|
||||
continue
|
||||
fi
|
||||
log MQTT "Accepted backup run command"
|
||||
"$SCRIPT_DIR/backup.sh" || log ERROR "MQTT-triggered backup failed"
|
||||
done
|
||||
+13
-1
@@ -16,6 +16,13 @@ WAIT_INTERVAL=3
|
||||
START_RETRIES=1
|
||||
LOCK_FILE="/run/lock/zorruno-docker-backup.lock"
|
||||
ZSTD_EXTRA_OPTS="-T0 -12"
|
||||
MQTT_REPORT_ENABLED=0
|
||||
MQTT_HOST=""
|
||||
MQTT_PORT=1883
|
||||
MQTT_USERNAME=""
|
||||
MQTT_PASSWORD_FILE=""
|
||||
MQTT_STATUS_TOPIC="viewroad-status/dockerbackup/$BACKUP_HOSTNAME"
|
||||
MQTT_COMMAND_TOPIC="viewroad-commands/dockerbackup/$BACKUP_HOSTNAME/run"
|
||||
|
||||
RCLONE_ENABLED=0
|
||||
RCLONE_REMOTE=""
|
||||
@@ -39,7 +46,12 @@ if [[ -n "${SOURCE_DIR:-}" && "$BACKUP_ROOT" == "/dockerbackups" ]]; then
|
||||
fi
|
||||
|
||||
timestamp() { date '+%F %T'; }
|
||||
log() { printf '%s [%s] %s\n' "$(timestamp)" "$1" "$2" >&2; }
|
||||
log() {
|
||||
local line
|
||||
line="$(timestamp) [$1] $2"
|
||||
printf '%s\n' "$line" >&2
|
||||
[[ -n "${BACKUP_RUN_LOG:-}" ]] && printf '%s\n' "$line" >>"$BACKUP_RUN_LOG"
|
||||
}
|
||||
die() { log ERROR "$1"; exit "${2:-1}"; }
|
||||
|
||||
is_true() {
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
mqtt_common_args() {
|
||||
MQTT_ARGS=(-h "$MQTT_HOST" -p "$MQTT_PORT")
|
||||
[[ -n "$MQTT_USERNAME" ]] && MQTT_ARGS+=(-u "$MQTT_USERNAME")
|
||||
if [[ -n "$MQTT_PASSWORD_FILE" ]]; then
|
||||
[[ -r "$MQTT_PASSWORD_FILE" ]] || die "MQTT password file is not readable: $MQTT_PASSWORD_FILE"
|
||||
MQTT_ARGS+=(-P "$(<"$MQTT_PASSWORD_FILE")")
|
||||
fi
|
||||
}
|
||||
|
||||
mqtt_publish() {
|
||||
local topic="$1" payload="$2" retain="${3:-0}"
|
||||
[[ -n "$MQTT_HOST" ]] || die "MQTT_HOST is not configured"
|
||||
require_command mosquitto_pub
|
||||
mqtt_common_args
|
||||
(( retain )) && MQTT_ARGS+=(-r)
|
||||
mosquitto_pub "${MQTT_ARGS[@]}" -t "$topic" -m "$payload"
|
||||
}
|
||||
@@ -100,6 +100,7 @@ backup_project() {
|
||||
compose up -d || true
|
||||
return 1
|
||||
fi
|
||||
log ARCHIVE "Created $output"
|
||||
if is_true "$PULL_IMAGES"; then
|
||||
compose pull || log WARN "Image pull failed for $project; starting with available images"
|
||||
fi
|
||||
|
||||
Executable
+43
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env bash
|
||||
# Publish retained backup run state for Home Assistant.
|
||||
set -Eeuo pipefail
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
. "$SCRIPT_DIR/../lib/common.sh"
|
||||
. "$SCRIPT_DIR/../lib/mqtt.sh"
|
||||
|
||||
STATUS="${1:?Usage: ${0##*/} STATUS EXIT_CODE STARTED_EPOCH FINISHED_EPOCH RUN_ID LOG_FILE}"
|
||||
EXIT_CODE="${2:-0}"
|
||||
STARTED_EPOCH="${3:-$(date +%s)}"
|
||||
FINISHED_EPOCH="${4:-$(date +%s)}"
|
||||
RUN_ID="${5:-unknown}"
|
||||
LOG_FILE="${6:-/dev/null}"
|
||||
|
||||
require_command jq
|
||||
total=0; archived=0; dry_run=0; error_count=0
|
||||
if [[ -r "$LOG_FILE" ]]; then
|
||||
total="$(grep -c '\[INFO\] Project:' "$LOG_FILE" || true)"
|
||||
archived="$(grep -c '\[ARCHIVE\] Created ' "$LOG_FILE" || true)"
|
||||
dry_run="$(grep -c '\[DRY\] Would stop, archive' "$LOG_FILE" || true)"
|
||||
error_count="$(grep -c '\[ERROR\]' "$LOG_FILE" || true)"
|
||||
errors_json="$(sed -n 's/^.*\[ERROR\] //p' "$LOG_FILE" | jq -Rsc 'split("\n") | map(select(length > 0))')"
|
||||
else
|
||||
errors_json='[]'
|
||||
fi
|
||||
if (( EXIT_CODE != 0 && error_count == 0 )); then
|
||||
errors_json="$(jq -cn --arg message "Pipeline exited with status $EXIT_CODE" '[ $message ]')"
|
||||
error_count=1
|
||||
fi
|
||||
|
||||
payload="$(jq -cn \
|
||||
--arg host "$BACKUP_HOSTNAME" --arg status "$STATUS" --arg run_id "$RUN_ID" \
|
||||
--argjson exit_code "$EXIT_CODE" --argjson started "$STARTED_EPOCH" \
|
||||
--argjson finished "$FINISHED_EPOCH" --argjson duration "$((FINISHED_EPOCH - STARTED_EPOCH))" \
|
||||
--argjson total "$total" --argjson archived "$archived" --argjson dry_run "$dry_run" \
|
||||
--argjson error_count "$error_count" --argjson errors "$errors_json" \
|
||||
'{host:$host,status:$status,run_id:$run_id,exit_code:$exit_code,
|
||||
started_at:($started|todateiso8601),finished_at:($finished|todateiso8601),
|
||||
duration_seconds:$duration,containers_total:$total,containers_backed_up:$archived,
|
||||
containers_dry_run:$dry_run,error_count:$error_count,errors:$errors}')"
|
||||
|
||||
mqtt_publish "$MQTT_STATUS_TOPIC" "$payload" 1
|
||||
log MQTT "Published $STATUS result to $MQTT_STATUS_TOPIC"
|
||||
@@ -0,0 +1,15 @@
|
||||
[Unit]
|
||||
Description=Zorruno Docker backup MQTT command listener
|
||||
After=network-online.target docker.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/backupdockerscripts
|
||||
ExecStart=/backupdockerscripts/bin/mqtt-trigger-listener.sh
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Reference in new issue
Block a user