feat(backup): Add restic backup

Restic backup creates a snapper snapshot of the root system which it
then chroots into and starts a restic backup to a (wasabi) S3 bucket to.

Intended to roughly follow this
<https://codeberg.org/silmaril/my-restic-solution> solution to achieve
restic backup of the _newest_ snapshot of my live root system.
This commit is contained in:
Marty Oehme 2025-02-28 21:40:02 +01:00
parent 2400bbf1af
commit 19162e2af3
Signed by: Marty
GPG key ID: 4E535BC19C61886E
6 changed files with 131 additions and 1 deletions
roles/backup

View file

@ -1,2 +1,12 @@
---
# user_name: # required for snapper templates
restic_repository:
restic_password:
restic_s3_id:
restic_s3_key:
restic_daily: 3
restic_weekly: 2
restic_monthly: 6
restic_yearly: 1

53
roles/backup/files/snapstic Executable file
View file

@ -0,0 +1,53 @@
#!/usr/bin/env bash
set -euo pipefail
shopt -s expand_aliases
# Restic snapshot backup script
msg() { # $1=message string $2=unit
printf "[%s] %s\n" "${2:-snapstic}" "$1"
}
config="${SNAPSTIC_RESTIC_CONFIG:-root}"
restic_conf="/etc/restic/${config}/restic.conf"
snapper_snapshot_dir="${SNAPSTIC_SNAPSHOT_DIR:-/.snapshots}"
# snapper_config="${SNAPSTIC_SNAPPER_CONFIG}" # TODO: call snapper with associated conf if exists?
source "$restic_conf"
trap cleanup 1 2 3 6
cleanup() {
msg "unmount restic configuration"
umount "${snap_dir}/.snapshots"
umount "${snap_dir}/tmp"
exit
}
msg "create snapper snapshot"
snap_num=$(snapper create --cleanup-algorithm number --description 'restic' --print-number)
snap_dir="${snapper_snapshot_dir}/${snap_num}/snapshot"
if [[ "$#" -eq 0 ]]; then
msg "no backup options selected"
fi
msg "mount restic configuration to ephemeral dir"
mount --bind "/etc/restic/${config}" "${snap_dir}/.snapshots"
msg "mount restic cache dir"
mount --bind "/tmp" "${snap_dir}/tmp"
if [[ $* == *"backup"* ]]; then
msg "backup in chrooted $snap_dir"
chroot "$snap_dir" /bin/bash -c '. /.snapshots/restic.conf && AWS_ACCESS_KEY_ID="$AWS_ACCESS_KEY_ID" AWS_SECRET_ACCESS_KEY="$AWS_SECRET_ACCESS_KEY" RESTIC_REPOSITORY=$RESTIC_REPOSITORY RESTIC_PASSWORD="$RESTIC_PASSWORD" /usr/bin/restic backup --tag "snapstic" --files-from="/.snapshots/include" --exclude-file="/.snapshots/exclude" --exclude "/.snapshots" --exclude-caches --no-cache'
fi
if [[ $* == *"forget"* ]]; then
msg "forget snapshots in $snap_dir"
chroot "$snap_dir" /bin/bash -c '. /.snapshots/restic.conf && DAILY=$DAILY WEEKLY=$WEEKLY MONTHLY=$MONTHLY YEARLY=$YEARLY AWS_ACCESS_KEY_ID="$AWS_ACCESS_KEY_ID" AWS_SECRET_ACCESS_KEY="$AWS_SECRET_ACCESS_KEY" RESTIC_REPOSITORY=$RESTIC_REPOSITORY RESTIC_PASSWORD="$RESTIC_PASSWORD" /usr/bin/restic forget --tag "snapstic" --group-by "paths,tags" --keep-daily="${DAILY:-0}" --keep-weekly="${WEEKLY:-0}" --keep-monthly="${MONTHLY:-0}" --keep-yearly="${YEARLY:-0}" --no-cache'
fi
if [[ $* == *"stats"* ]]; then
msg "display repo stats"
chroot "$snap_dir" /bin/bash -c '. /.snapshots/restic.conf && AWS_ACCESS_KEY_ID="$AWS_ACCESS_KEY_ID" AWS_SECRET_ACCESS_KEY="$AWS_SECRET_ACCESS_KEY" RESTIC_REPOSITORY=$RESTIC_REPOSITORY RESTIC_PASSWORD="$RESTIC_PASSWORD" /usr/bin/restic stats --no-cache'
fi
msg "done"

View file

@ -2,4 +2,10 @@
import_tasks: snapper.yaml
tags:
- btrfs
- snapshots
- snapper
- name: Set up restic backups
import_tasks: restic.yaml
tags:
- btrfs
- restic

View file

@ -0,0 +1,52 @@
- name: Install restic
community.general.xbps:
name:
- restic
state: present
tags: packages
- name: Ensure restic configuration directory exists
ansible.builtin.file:
dest: "/etc/restic/root"
state: directory
- name: Create restic root backup configuration
ansible.builtin.template:
src: restic.conf.j2
dest: "/etc/restic/root/restic.conf"
mode: 0600
force: true # ensure contents are always exact
- name: Ensure files and exclude files exist # Only change if necessary
ansible.builtin.file:
dest: "/etc/restic/root/{{ item }}"
state: touch
modification_time: preserve
access_time: preserve
loop:
- include
- exclude
- name: Create include files # TODO: Rename file to include?
ansible.builtin.copy:
content: "/"
dest: "/etc/restic/root/include"
force: true # ensure contents are always exact
- name: Install snapstic script
ansible.builtin.copy:
src: snapstic
dest: "/usr/bin/snapstic"
mode: 0744
- name: Ensure restic configuration directory exists
ansible.builtin.file:
dest: "/etc/cron.weekly"
state: directory
- name: Add snapstic to weekly cronjobs
ansible.builtin.copy:
content: "#!/bin/sh\n\nsnapstic backup"
dest: "/etc/cron.weekly/snapstic"
mode: 0755
force: true # ensure contents are always exact

View file

@ -0,0 +1,9 @@
RESTIC_REPOSITORY='{{ restic_repository }}'
AWS_ACCESS_KEY_ID='{{ restic_s3_id }}'
AWS_SECRET_ACCESS_KEY='{{ restic_s3_key }}'
RESTIC_PASSWORD='{{ restic_password }}'
RESTIC_CACHE_DIR=/var/tmp
DAILY={{ restic_daily }}
WEEKLY={{ restic_weekly }}
MONTHLY={{ restic_monthly }}
YEARLY={{ restic_yearly }}