#!/usr/bin/env bash # {{ ansible_managed }} # Backup script for {{ item.src|default('stdin') }} # Use this file to create a Backup and prune existing data with one execution. pid="/var/run/restic_backup_{{ item.name | regex_replace('\'', '\'\\\'\'') }}.pid" trap "rm -f $pid" SIGSEGV trap "rm -f $pid" SIGINT if [ -e $pid ]; then echo "Another version of this restic backup script is already running!" {% if item.mail_on_error is defined and item.mail_on_error == true %} mail -s "starting restic backup failed on {{ ansible_hostname }}" {{ item.mail_address }} <<< "Another restic backup process is already running. We canceled starting a new restic backup script running at {{ ansible_hostname }} at $(date -u '+%Y-%m-%d %H:%M:%S'). {%- if item.src is defined -%} {{ ' ' }}We tried to backup '{{ item.src }}'. {%- endif -%} {{ ' ' }}Please repair the restic-{{ item.name | replace(' ', '') }} job." {% endif %} exit # pid file exists, another instance is running, so now we politely exit else echo $$ > $pid # pid file doesn't exit, create one and go on fi # your normal workflow here... {% if item.disable_logging is defined and item.disable_logging %} {% set backup_result_log, backup_output_log = "/dev/null", "/dev/null" %} {% set forget_result_log, forget_output_log = "/dev/null", "/dev/null" %} {% else %} {% if (item.log_to_journald is defined and item.log_to_journald) %} {% set backup_result_log, backup_output_log = "| systemd-cat -t " + item.name, "2>&1 | systemd-cat -t " + item.name %} {% set forget_result_log, forget_output_log = "| systemd-cat -t " + item.name, "2>&1 | systemd-cat -t " + item.name %} {% else %} {% set backup_result_log, backup_output_log = ">> " + restic_log_dir + "/" + item.name + "-backup-result.log", " 2>&1 | tee " + restic_log_dir + "/" + item.name + "-backup-output.log" %} {% set forget_result_log, forget_output_log = ">> " + restic_log_dir + "/" + item.name + "-forget-result.log", " 2>&1 | tee " + restic_log_dir + "/" + item.name + "-forget-output.log" %} {% endif %} {% endif %} {% if restic__cache_config | bool -%} export XDG_CACHE_HOME={{ restic__cache_dir }} {% endif %} {% if restic__limit_cpu_usage | bool -%} # Maximum number of CPUs used by restic is limited to {{ restic__max_cpus }} export GOMAXPROCS={{ restic__max_cpus }} {% endif %} export RESTIC_REPOSITORY={{ restic_repos[item.repo].location }} export RESTIC_PASSWORD='{{ restic_repos[item.repo].password | regex_replace('\'', '\'\\\'\'') }}' BACKUP_NAME={{ item.name }} {% if restic_repos[item.repo].aws_access_key is defined %} export AWS_ACCESS_KEY_ID={{ restic_repos[item.repo].aws_access_key }} {% endif %} {% if restic_repos[item.repo].aws_secret_access_key is defined %} export AWS_SECRET_ACCESS_KEY='{{ restic_repos[item.repo].aws_secret_access_key | regex_replace('\'', '\'\\\'\'') }}' {% endif %} {% if restic_repos[item.repo].aws_default_region is defined %} export AWS_DEFAULT_REGION={{ restic_repos[item.repo].aws_default_region }} {% endif %} {% if restic_repos[item.repo].azure_account_name is defined %} export AZURE_ACCOUNT_NAME={{ restic_repos[item.repo].azure_account_name }} {% endif %} {% if restic_repos[item.repo].azure_account_key is defined %} export AZURE_ACCOUNT_KEY={{ restic_repos[item.repo].azure_account_key }} {% endif %} {% if restic_repos[item.repo].azure_account_sas is defined %} export AZURE_ACCOUNT_SAS='{{ restic_repos[item.repo].azure_account_sas }}' {% endif %} {% if restic_repos[item.repo].azure_endpoint_suffix is defined %} export AZURE_ENDPOINT_SUFFIX={{ restic_repos[item.repo].azure_endpoint_suffix }} {% endif %} {% if restic_repos[item.repo].b2_account_id is defined %} export B2_ACCOUNT_ID={{ restic_repos[item.repo].b2_account_id }} {% endif %} {% if restic_repos[item.repo].b2_account_key is defined %} export B2_ACCOUNT_KEY={{ restic_repos[item.repo].b2_account_key }} {% endif %} {% if item.src is defined and item.src is string %} BACKUP_SOURCE={{ item.src }} {% endif %} {% if item.src is defined and item.src.__class__.__name__ =='list' %} BACKUP_SOURCE='{{ item.src| join(' ') }}' {% endif %} {% if item.lvm is defined %} # Set up functions for LVM. function mount_opt_map { mount_type="$1" case "$mount_type" in xfs) echo "noatime,nouuid" ;; ext4) echo "noatime" ;; *) echo "noatime" esac } function prepare_vol { local path="$1" [ -d "$path" ] || path="$(dirname "$path")" if [ "$path" == '/' ] ; then mkdir -p /rootfs; newpath='/rootfs'; else newpath="$path"; fi { local source="$(findmnt -J -T ${path} | jq -r '.filesystems[0].source')" local target="$(findmnt -J -T ${path} | jq -r '.filesystems[0].target')" subdir=${path##$target} echo "Creating snapshot ..." lvcreate -y -L "${size:-10G}" -s -n "${source}_snap" "${source}" local tmpdir="$(mktemp -d)" local fs="$(lsblk -J --fs "$source" | jq -r '.blockdevices[0]|.fstype')" echo "Identified fstype: $fs; using opts $(mount_opt_map "$fs") ..." mount -t "$fs" \ -o "$(mount_opt_map "$fs")" \ --make-private \ "${source}_snap" "${tmpdir}" mount --bind --make-private "${tmpdir}/${subdir}" "${newpath}" } } function cleanup_vol { local path="$1" [ -d "$path" ] || path="$(dirname "$path")" if [ "$path" == '/' ] ; then newpath='/rootfs'; else newpath="$path"; fi { local source="$(findmnt -v -J -T "${newpath}" | jq -r '.filesystems[]|.source' | grep '_snap$')" if ! grep -q '_snap$' <<< $source; then echo "Snapshot for ${path} could not be found (found: ${source}). Exiting!" && return 1; fi echo "Cleaning up mount ..." umount "${newpath}" echo "Cleaning up snapshot ..." umount "${source}" lvremove -y "${source}"; } } {% endif %} set -uxo pipefail {# Define Tags #} {% macro tags(tags) -%} {% if tags is defined and (tags|length>0) %}{% for tag in tags %} --tag {{ tag }}{% endfor %}{% endif %} {%- endmacro %} {# Define Keeped Tags #} {% macro keep_tags(tags) -%} {% if tags is defined and (tags|length>0) %}{% for tag in tags %} --keep-tag {{ tag }}{% endfor %}{% endif %} {%- endmacro %} {# Define Hostname #} {% macro hostname(h) -%} {% if h is defined %} --hostname {{ h }}{% endif %} {%- endmacro %} {# Define stdin filename #} {% macro stdin_filename(n) -%} {% if n is defined %} --stdin-filename {{ n }}{% endif %} {%- endmacro %} {# Define paths #} {% macro paths(repo) -%} {% if repo.src is defined and repo.src != None and (repo.src is not string) %}{%for path in repo.src %} --path {{ path }}{% endfor %}{%elif repo.src is string %}--path {{repo.src}} {% else %} --path {{ repo.stdin_filename }}{% endif %} {%- endmacro %} {# Define retention pattern #} {% macro retention_pattern(repo) -%} {% if repo.keep_last is defined and repo.keep_last != None %}--keep-last {{ item.keep_last }}{% endif %} \ {% if repo.keep_hourly is defined and repo.keep_hourly != None %}--keep-hourly {{ item.keep_hourly }}{% endif %} \ {% if repo.keep_daily is defined and repo.keep_daily != None %}--keep-daily {{ item.keep_daily }}{% endif %} \ {% if repo.keep_weekly is defined and repo.keep_weekly != None %}--keep-weekly {{ item.keep_weekly }}{% endif %} \ {% if repo.keep_monthly is defined and repo.keep_monthly != None %}--keep-monthly {{ item.keep_monthly }}{% endif %} \ {% if repo.keep_yearly is defined and repo.keep_yearly != None %}--keep-yearly {{ item.keep_yearly }}{% endif %} \ {% if repo.keep_within is defined and repo.keep_within != None %}--keep-within {{ item.keep_within }}{% endif %} \ {% if repo.keep_tag is defined and (repo.keep_tag|length>0) %}{{ keep_tags(repo.keep_tag) }}{% endif %} {%- endmacro %} {% macro exclude(exclude) %} {% if exclude.exclude_caches is defined and exclude.exclude_caches == true %} --exclude-caches \ {% endif %} {% if exclude.exclude is defined %} {% for path in exclude.exclude %} --exclude {{ path }} \ {% endfor %} {% endif %} {% if exclude.iexclude is defined %} {% for path in exclude.iexclude %} --iexclude {{ path }} \ {% endfor %} {% endif %} {% if exclude.exclude_file is defined %} {% for path in exclude.exclude_file %} --exclude-file {{ path }} \ {% endfor %} {% endif %} {% if exclude.exclude_if_present is defined %} {% for path in exclude.exclude_if_present %} --exclude-if-present {{ path }} \ {% endfor %} {% endif %} {% endmacro %} {# Define backup commands #} if [[ -z ${CRON+x} ]]; then MODE_TAG="--tag manual" else MODE_TAG="--tag cron" fi {% if item.stdin is defined and item.stdin == true %} {{ item.stdin_cmd }} | {{ restic_install_path }}/restic backup \ --stdin $MODE_TAG \ {{ tags(item.tags) }} \ {{ stdin_filename(item.stdin_filename) }} \ {% if item.exclude is defined %}{{ exclude(item.exclude) }}{% endif %} \ $@ \ {% else %} { {% if item.lvm is defined %}prepare_vol $BACKUP_SOURCE &&{% endif %} {{ restic_install_path }}/restic backup {% if item.lvm is defined and item.src == '/' %}/rootfs{% endif %}$BACKUP_SOURCE $MODE_TAG \ {{ tags(item.tags) }} \ {% if item.exclude is defined %}{{ exclude(item.exclude) }}{% endif %} \ $@ } \ {% endif %} {{ backup_output_log }} case $? in 0) echo "$(date -u '+%Y-%m-%d %H:%M:%S') OK" {{ backup_result_log }} ;; 3) echo "$(date -u '+%Y-%m-%d %H:%M:%S') WARNING" {{ backup_result_log }} ;; *) echo "$(date -u '+%Y-%m-%d %H:%M:%S') ERROR" {{ backup_result_log }} {% if item.mail_on_error is defined and item.mail_on_error == true %} mail -s "restic backup failed on {{ ansible_hostname }}" {{ item.mail_address }} <<< "Something went wrong while running restic backup script running at {{ ansible_hostname }} at $(date -u '+%Y-%m-%d %H:%M:%S'). {%- if item.src is defined -%} {{ ' ' }}We tried to backup '{{ item.src }}'. {%- endif -%} {{ ' ' }}Please repair the restic-{{ item.name | replace(' ', '') }} job." {% endif %} esac {% if item.lvm is defined %} cleanup_vol $BACKUP_SOURCE {% endif %} {# Define stdin forget commands #} {{ restic_install_path }}/restic forget {{ paths(item) }} {{ retention_pattern(item) }} {% if item.prune is defined and item.prune == true %}--prune{% endif %} {{ forget_output_log }} if [[ $? -eq 0 ]] then echo "$(date -u '+%Y-%m-%d %H:%M:%S') OK" {{ forget_result_log }} {% if item.monitoring_call is defined %} {{ item.monitoring_call }} {% endif %} else echo "$(date -u '+%Y-%m-%d %H:%M:%S') ERROR" {{ forget_result_log }} {% if item.mail_on_error is defined and item.mail_on_error == true %} mail -s "restic backup failed on {{ ansible_hostname }}" {{ item.mail_address }} <<< "Something went wrong while running restic backup script running at {{ ansible_hostname }} at $(date -u '+%Y-%m-%d %H:%M:%S'). {%- if item.src is defined -%} {{ ' ' }}We tried to backup '{{ item.src }}'. {%- endif -%} {{ ' ' }}Please repair the restic-{{ item.name | replace(' ', '') }} job." {% endif %} fi rm -f $pid # remove pid file just before exiting exit