From 19162e2af3cba48f56e69e003e7526db7ba2e27f Mon Sep 17 00:00:00 2001 From: Marty Oehme <contact@martyoeh.me> Date: Fri, 28 Feb 2025 21:40:02 +0100 Subject: [PATCH] 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. --- roles/backup/defaults/main.yaml | 10 +++++ roles/backup/files/snapstic | 53 +++++++++++++++++++++++++++ roles/backup/tasks/main.yaml | 8 +++- roles/backup/tasks/restic.yaml | 52 ++++++++++++++++++++++++++ roles/backup/templates/restic.conf.j2 | 9 +++++ roles/backup/vars/main.yaml | 0 6 files changed, 131 insertions(+), 1 deletion(-) create mode 100755 roles/backup/files/snapstic create mode 100644 roles/backup/tasks/restic.yaml create mode 100644 roles/backup/templates/restic.conf.j2 delete mode 100644 roles/backup/vars/main.yaml diff --git a/roles/backup/defaults/main.yaml b/roles/backup/defaults/main.yaml index 85242b0..c7c6052 100644 --- a/roles/backup/defaults/main.yaml +++ b/roles/backup/defaults/main.yaml @@ -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 diff --git a/roles/backup/files/snapstic b/roles/backup/files/snapstic new file mode 100755 index 0000000..a09a148 --- /dev/null +++ b/roles/backup/files/snapstic @@ -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" diff --git a/roles/backup/tasks/main.yaml b/roles/backup/tasks/main.yaml index a7039cf..af75d10 100644 --- a/roles/backup/tasks/main.yaml +++ b/roles/backup/tasks/main.yaml @@ -2,4 +2,10 @@ import_tasks: snapper.yaml tags: - btrfs - - snapshots + - snapper + +- name: Set up restic backups + import_tasks: restic.yaml + tags: + - btrfs + - restic diff --git a/roles/backup/tasks/restic.yaml b/roles/backup/tasks/restic.yaml new file mode 100644 index 0000000..20cbad4 --- /dev/null +++ b/roles/backup/tasks/restic.yaml @@ -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 diff --git a/roles/backup/templates/restic.conf.j2 b/roles/backup/templates/restic.conf.j2 new file mode 100644 index 0000000..b337c77 --- /dev/null +++ b/roles/backup/templates/restic.conf.j2 @@ -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 }} diff --git a/roles/backup/vars/main.yaml b/roles/backup/vars/main.yaml deleted file mode 100644 index e69de29..0000000