diff --git a/RUNBOOK.md b/RUNBOOK.md index 2976eb4..74ae321 100644 --- a/RUNBOOK.md +++ b/RUNBOOK.md @@ -47,6 +47,37 @@ Run once, by hand, on the server: # create a remote named "scaleway", type S3, matching your Scaleway # Object Storage credentials and region ``` +7. Set up email notifications (optional — skip this if you don't want mail): + Install a mail transport agent (choose one): + ```bash + # Option A: mailutils (Debian/Ubuntu) — includes /usr/bin/mail + apt install -y mailutils + + # Option B: msmtp + mailutils (lightweight, no full MTA) + apt install -y msmtp msmtp-mta mailutils + ``` + Configure the recipient address. Either set `MAIL_TO` in the environment + (`/etc/environment` or the cron file itself) or edit `borg-backup.sh`: + ``` + MAIL_TO="${MAIL_TO:-you@example.com}" + ``` + If using `msmtp`, configure `/etc/msmtprc`: + ``` + # /etc/msmtprc — example for Gmail SMTP + defaults + auth on + tls on + tls_trust_file /etc/ssl/certs/ca-certificates.crt + + account default + host smtp.gmail.com + port 587 + from backup-sender@gmail.com + user backup-sender@gmail.com + password your-app-password + ``` + The script auto-detects which command is available (`mail` → `sendmail` → `msmtp`) + and falls back silently if none is present — notifications are never fatal. ## 2. Deploying the Scripts @@ -62,9 +93,9 @@ chmod +x /opt/backup-agent/dump_db.sh ## 3. Scheduling Add a cron entry to run the backup daily, off-peak: - ``` # /etc/cron.d/borg-backup +MAIL_TO=you@example.com 30 2 * * * root /opt/backup-agent/borg-backup.sh >> /var/log/borg/cron.log 2>&1 ``` diff --git a/borg-backup.sh b/borg-backup.sh index aa8021c..68e1564 100755 --- a/borg-backup.sh +++ b/borg-backup.sh @@ -58,6 +58,10 @@ REQUIRED_CMDS=(borg rclone docker timeout flock date find) # ================================================================= +# Email notification (set to your address to receive backup summary). +# Leave empty to skip notifications. The script auto-detects mail/sendmail/msmtp. +MAIL_TO="${MAIL_TO:-}" + SELF="$(readlink -f "$0")" mkdir -p "$LOGDIR" LOGFILE="${BORG_BACKUP_LOGFILE:-$LOGDIR/backup-$(date +%Y-%m-%d-%H%M%S).log}" @@ -89,6 +93,68 @@ run_cmd() { "$@" } +# Detect available mail transport. Returns the command name or empty string. +find_mail_cmd() { + for cmd in mail sendmail msmtp; do + command -v "$cmd" >/dev/null 2>&1 && { echo "$cmd"; return 0; } + done + return 1 +} + +# Send an email notification about the backup result. +# Called from cleanup() so the log file is complete. +# Non-fatal — mail failures do not affect the backup exit code. +send_notification() { + local exit_code=$1 status started ended + [[ -n "$MAIL_TO" ]] || return 0 + + local mail_cmd + mail_cmd="$(find_mail_cmd)" || { log "Notification skipped: no mail command found"; return 0; } + + if [ "$exit_code" -eq 0 ]; then + status="SUCCESS" + else + status="FAILED (exit $exit_code)" + fi + + started="$(head -1 "$LOGFILE" 2>/dev/null | sed -n 's/^\[\(.*\)\].*/\1/p' || echo "?")" + ended="$(date '+%F %T')" + + # Build message body with headers and log tail. + { + echo "Subject: Backup $status - $(hostname -s)" + echo "To: $MAIL_TO" + echo "" + echo "===== Backup Summary =====" + echo "Host: $(hostname -f)" + echo "Archive: $ARCHIVE" + echo "Status: $status" + echo "Started: $started" + echo "Ended: $ended" + echo "Log: $LOGFILE" + echo "" + echo "----- Last 80 lines of backup log -----" + tail -80 "$LOGFILE" 2>/dev/null || echo "(log unavailable)" + } | case "$mail_cmd" in + mail) + mail -s "Backup $status - $(hostname -s)" "$MAIL_TO" + ;; + sendmail) + sendmail "$MAIL_TO" + ;; + msmtp) + msmtp "$MAIL_TO" + ;; + esac + + local rc=$? + if [ "$rc" -eq 0 ]; then + log "Notification sent to $MAIL_TO (via $mail_cmd)" + else + log "WARNING: notification via $mail_cmd failed (exit $rc)" + fi +} + container_running() { [[ "$(docker inspect -f '{{.State.Running}}' "$1" 2>/dev/null || echo false)" == "true" ]] } @@ -143,6 +209,7 @@ cleanup() { log "=== Backup FAILED (exit $exit_code) ===" fi log "Full log: $LOGFILE" + send_notification "$exit_code" } # Returns 0 if the repo is encrypted, 1 if not.