diff --git a/roles/restic/README.md b/roles/restic/README.md new file mode 100644 index 0000000000000000000000000000000000000000..aa2d74a407f5cc9075b4625714645e893760b26d --- /dev/null +++ b/roles/restic/README.md @@ -0,0 +1,35 @@ +# Restic role + +*Compatibility: Debian Buster* + +## Configuration example +``` +restic_credentials: + envvars: | + export OS_AUTH_URL=someurl + export OS_REGION_NAME=someregion + export OS_USERNAME=someusername + export OS_PASSWORD=somepassword + export OS_TENANT_NAME=sometenant + export MYSQLPASSWORD=sometenant + restic_password: 'somepassword' + +restic: + backups: + - name: 'swift-matomo' + repository: 'swift:restic_matomo-backup:/piwik.killiankemps.fr' + cron: + minute: '0' + hour: '3' + stdin: + - database: 'mysqldump -u piwik -p$MYSQLPASSWORD 127.0.0.1' + filename: 'matomo.sql' + - name: 'local-mastodon' + repository: '/srv/backups/restic_mastodon-backup' + cron: + minute: '0' + hour: '2' + folders: + - '/home/mastodon/live/.env.production' + - '/home/mastodon/live/public/system' +``` diff --git a/roles/restic/defaults/main.yml b/roles/restic/defaults/main.yml new file mode 100644 index 0000000000000000000000000000000000000000..43338c8dcceebe0db0a40591cd8db5174d40b830 --- /dev/null +++ b/roles/restic/defaults/main.yml @@ -0,0 +1,7 @@ +--- +restic_architecture: "{{ 'arm64' if ansible_architecture == 'aarch64' else 'amd64' }}" + +restic_credentials: [] + +restic: + backups: [] diff --git a/roles/restic/tasks/main.yml b/roles/restic/tasks/main.yml new file mode 100644 index 0000000000000000000000000000000000000000..990dd02044a6d294303658019dd71648251ebef9 --- /dev/null +++ b/roles/restic/tasks/main.yml @@ -0,0 +1,86 @@ +--- + +- name: Install bzip2 + apt: + name: bzip2 + state: latest + register: apt_info +- name: Display stdout of apt + debug: msg={{ apt_info.stdout.split('\n')[:-1] }} + when: apt_info is changed + +- name: Get checksum for restic + set_fact: + restic_checksum: "{{ item.split(' ')[0] }}" + with_items: + - "{{ lookup('url', 'https://github.com/restic/restic/releases/download/v' + restic_version + '/SHA256SUMS', wantlist=True) | list }}" + when: "('restic_' + restic_version + '_linux_{{ restic_architecture }}.bz2') in item" + +- name: Download restic archive + get_url: + url: "https://github.com/restic/restic/releases/download/v{{ restic_version }}/restic_{{ restic_version }}_linux_{{ restic_architecture }}.bz2" + dest: "/tmp/restic_{{ restic_version }}_linux_{{ restic_architecture }}.bz2" + checksum: "sha256:{{ restic_checksum }}" + +- name: Decompress restic archive + shell: "bzip2 -dc /tmp/restic_{{ restic_version }}_linux_{{ restic_architecture }}.bz2 > /opt/restic_{{ restic_version }}_linux_{{ restic_architecture }}" + args: + creates: "/opt/restic_{{ restic_version }}_linux_{{ restic_architecture }}" + +- name: Ensure permissions are correct + file: + path: "/opt/restic_{{ restic_version }}_linux_{{ restic_architecture }}" + mode: '0755' + owner: 'root' + group: 'root' + +- name: Create symbolic link to the correct version + file: + src: "/opt/restic_{{ restic_version }}_linux_{{ restic_architecture }}" + path: '/usr/local/bin/restic' + state: link + force: True + +- name: Copy Restic secrets + template: + src: restic-keys.j2 + dest: "/root/.restic-keys-{{ item.name }}" + owner: root + group: root + mode: 0640 + loop: "{{ restic_credentials }}" + +- name: Create scripts folder + file: + path: "/root/scripts" + state: directory + mode: 0775 + owner: root + group: root + +- name: Create local backups folders + file: + path: "{{ item.repository }}" + state: directory + mode: 0775 + owner: root + group: root + loop: "{{ restic.backups }}" + when: "item.repository.startswith('/')" + +- name: Copy backup scripts + template: + src: backup_script.j2 + dest: "/root/scripts/restic_backup_{{ item.name }}.sh" + owner: root + group: root + mode: 0644 + loop: "{{ restic.backups }}" + +- name: Set cron for each backup script + cron: + name: "Backup {{ item.name }}" + minute: "{{ item.cron.minute }}" + hour: "{{ item.cron.hour }}" + job: "ionice -c2 -n7 nice -n19 bash /root/scripts/restic_backup_{{ item.name }}.sh 2>&1 | tee -a /var/log/cron_restic_backup_{{ item.name }}.log" + loop: "{{ restic.backups }}" diff --git a/roles/restic/templates/backup_script.j2 b/roles/restic/templates/backup_script.j2 new file mode 100644 index 0000000000000000000000000000000000000000..8c8e0398c0b614a9ae69e7ed78b23cf5a34d3379 --- /dev/null +++ b/roles/restic/templates/backup_script.j2 @@ -0,0 +1,65 @@ +#!/bin/bash +set -e +set -u +set -o pipefail + +RESTIC='/usr/local/bin/restic' +source $HOME/.restic-keys-{{ item.name }} +export RESTIC_REPOSITORY="{{ item.repository }}" + +run_cmd_with_backoff_retry_without_exit() { + MAX_TRY=10 + try=0 + while ! "$@" + do + if [[ ${try} -ge ${MAX_TRY} ]] + then + printf "All snapshots reading attempts have failed!\n" + return 1 + fi + ((try++)) + printf "Reading snapshots failed trying again in 10 seconds [%s/%s]\n" "${try}" "${MAX_TRY}" + sleep 10 + done +} + +run_cmd_with_backoff_retry() { + MAX_TRY=10 + try=0 + while ! "$@" + do + if [[ ${try} -ge ${MAX_TRY} ]] + then + printf "All backup attempts have failed!\n" + exit 1 + fi + ((try++)) + printf "Backup failed trying again in 10 seconds [%s/%s]\n" "${try}" "${MAX_TRY}" + sleep 10 + done +} + +echo -e "\n`date` - Checking repository is initialized...\n" +run_cmd_with_backoff_retry_without_exit $RESTIC snapshots || run_cmd_with_backoff_retry $RESTIC init + +echo -e "\n`date` - Starting backup...\n" + +{% if item.folders is defined %} + {% for folder in item.folders %} + run_cmd_with_backoff_retry $RESTIC backup {% if item.tags is defined %}--tag {{ item.tags|join(" --tag ") }}{% endif %} {{ folder }} + {% endfor %} +{% endif %} + +{% if item.stdin is defined %} + {% for input in item.stdin %} + run_cmd_with_backoff_retry {{ input.database }} | $RESTIC backup {% if item.tags is defined %}--tag {{ item.tags|join(" --tag ") }}{% endif %} --stdin --stdin-filename {{ input.filename }} + {% endfor %} +{% endif %} + +{% if item.keep is defined %} +echo -e "\n`date` - Running forget and prune...\n" + +$RESTIC forget --prune {% if item.tags is defined %}--tag {{ item.tags|join(" --tag ") }}{% endif %} --keep-daily {{ item.keep.daily|default(7) }} --keep-weekly {{ item.keep.weekly|default(4) }} --keep-monthly {{ item.keep.monthly|default(12) }} --keep-yearly {{ item.keep.yearly|default(3) }} --keep-tag to-keep +{% endif %} + +echo -e "\n`date` - Backup finished.\n" diff --git a/roles/restic/templates/restic-keys.j2 b/roles/restic/templates/restic-keys.j2 new file mode 100644 index 0000000000000000000000000000000000000000..30197f1126825703cb3f1930dbfe73c9082761a1 --- /dev/null +++ b/roles/restic/templates/restic-keys.j2 @@ -0,0 +1,5 @@ +export RESTIC_PASSWORD={{ item.restic_password }} + +{% if item.envvars is defined %} +{{ item.envvars }} +{% endif %} diff --git a/roles/restic/vars/main.yml b/roles/restic/vars/main.yml new file mode 100644 index 0000000000000000000000000000000000000000..ead7ba6b2ec1f3297701c68e3e6374bb8ba2aae5 --- /dev/null +++ b/roles/restic/vars/main.yml @@ -0,0 +1 @@ +restic_version: '0.12.1'