From 3efe44b50bed4c5972c8050c08a80022cf1aa27a Mon Sep 17 00:00:00 2001 From: Timo Makinen Date: Tue, 17 Dec 2024 22:32:25 +0000 Subject: [PATCH] rsync_backup: Initial version of role --- roles/rsync_backup/files/backup-daily.sh | 150 +++++++++++++++++++++++ roles/rsync_backup/meta/main.yml | 4 + roles/rsync_backup/tasks/main.yml | 52 ++++++++ 3 files changed, 206 insertions(+) create mode 100755 roles/rsync_backup/files/backup-daily.sh create mode 100644 roles/rsync_backup/meta/main.yml create mode 100644 roles/rsync_backup/tasks/main.yml diff --git a/roles/rsync_backup/files/backup-daily.sh b/roles/rsync_backup/files/backup-daily.sh new file mode 100755 index 0000000..4840732 --- /dev/null +++ b/roles/rsync_backup/files/backup-daily.sh @@ -0,0 +1,150 @@ +#!/bin/sh + +set -eu + +umask 077 + +ROTATED=30 + +CONFDIR="/etc/rsync-backup" +DESTDIR="/srv/backup" +LOGDIR="/var/log/rsync-backup" +RUNDIR="/var/run/rsync-backup" + +find_rotated() { + # sort dailys from oldest to newest, daily.7 daily.6 daily.5 ... + find "$1" -mindepth 1 -maxdepth 1 -type d -name "daily.*" | sort -V -r +} + +rotate_dirs() { + for host in "$@"; do + # rotate dailys starting from oldest + if [ ! -d "${DESTDIR}/${host}" ]; then + continue + fi + find_rotated "${DESTDIR}/${host}" | while read -r dir; do + ext="${dir##*.}" + next="${dir%.*}.$((ext+1))" + mv "$dir" "$next" + done + done + # compress logs over 1 day old + find "$LOGDIR" -type f -name '*.log' -mtime +1 -execdir gzip -f {} ';' +} + +prune_dirs() { + for host in "$@"; do + # remove oldest dailys + find_rotated "${DESTDIR}/${host}" | while read -r dir ; do + num="$(basename "$dir" | sed -e 's/^daily.//')" + if [ "$num" -gt $ROTATED ]; then + rm -rf "$dir" + fi + done + done + # remove logs over ROTATED*2 days old + find "$LOGDIR" -type f -name '*.log.gz' -mtime +$((ROTATED*2)) -delete +} + +rsync_pull() { + dirs="" + opts="" + host="$1" + conf="${CONFDIR}/${host}.conf" + if [ -s "$conf" ] && [ -x "$conf" ]; then + # shellcheck source=/dev/null + . "$conf" || return + else + echo "skipped: ${1}" 1>&2 + return + fi + + lockdir="${RUNDIR}/${host}.lock" + mkdir -m 0755 "$lockdir" || return + + if [ "$host" = "$(hostname)" ]; then + # skip ssh for localhost + set -- $dirs + else + set -- $(for d in $dirs; do echo "${host}:${d}" ; done) + fi + + base="${DESTDIR}/${host}" + if [ ! -d "$base" ]; then + mkdir -m 0700 "$base" || return + fi + dest="${base}/daily.0" + last="${base}/daily.1" + if [ ! -d "$dest" ]; then + mkdir -m 0700 "$dest" || return + fi + if [ -d "$last" ]; then + # hardlink unchanged files to previous daily + opts="--ignore-existing --link-dest=${last}" + fi + + logfile="${LOGDIR}/${host}.$(date +%Y%m%d-%H%M%S).log" + if ! /usr/local/bin/rsync \ + -e "ssh -o BatchMode=yes -i ${CONFDIR}/id_ed25519" \ + -Raqxz --no-devices $opts \ + --log-file="$logfile" \ + "$@" "$dest" + then + echo "rsync log: ${logfile}" 1>&2 + fi + rmdir "$lockdir" +} + +if [ ! -d "$DESTDIR" ]; then + echo "ERROR: ${DESTDIR} does not exist" 1>&2 + exit 1 +fi + +if [ ! -d "$LOGDIR" ]; then + echo "ERROR: ${LOGDIR} does not exist" 1>&2 + exit 1 +fi + +if [ ! -d "$RUNDIR" ]; then + mkdir -m 0755 "$RUNDIR" +fi + +ALL=false +PRUNE=false +ROTATE=false +while getopts "apr" OPT; do + case "$OPT" in + a) + ALL=true + ;; + p) + PRUNE=true + ;; + r) + ROTATE=true + ;; + *) + echo "Usage: $(basename "$0") [-apr] [host ...]" 1>&2 + exit 1 + ;; + esac +done +shift $((OPTIND-1)) + +mkdir -m 0755 "${RUNDIR}/daily.lock" +trap 'rmdir "${RUNDIR}/daily.lock"' EXIT + +if [ $ALL ]; then + for conf in "${CONFDIR}"/*.conf ; do + host="$(basename "$conf" ".conf")" + set -- "$host" "$@" + done +fi + +$ROTATE && rotate_dirs "$@" + +for host in "$@" ; do + rsync_pull "$host" +done + +$PRUNE && prune_dirs "$@" diff --git a/roles/rsync_backup/meta/main.yml b/roles/rsync_backup/meta/main.yml new file mode 100644 index 0000000..a6cb84e --- /dev/null +++ b/roles/rsync_backup/meta/main.yml @@ -0,0 +1,4 @@ +--- +dependencies: + - {role: backup_base} + - {role: ssh_known_hosts} diff --git a/roles/rsync_backup/tasks/main.yml b/roles/rsync_backup/tasks/main.yml new file mode 100644 index 0000000..7562bb0 --- /dev/null +++ b/roles/rsync_backup/tasks/main.yml @@ -0,0 +1,52 @@ +--- +- name: Copy backup script + ansible.builtin.copy: + dest: /usr/local/sbin/backup-daily + src: backup-daily.sh + mode: "0755" + owner: root + group: "{{ ansible_wheel }}" + +- name: Create config directory + ansible.builtin.file: + path: /etc/rsync-backup + state: directory + mode: "0755" + owner: root + group: "{{ ansible_wheel }}" + +- name: Create logdir + ansible.builtin.file: + path: /var/log/rsync-backup + state: directory + mode: "0700" + owner: root + group: "{{ ansible_wheel }}" + +- name: Create ssh keys + ansible.builtin.command: + argv: + - ssh-keygen + - -t + - ed25519 + - -C + - "root@{{ inventory_hostname }}" + - -N + - "" + - -f + - /etc/rsync-backup/id_ed25519 + creates: /etc/rsync-backup/id_ed25519 + +- name: Fetch ssh public key + ansible.builtin.fetch: + src: /etc/rsync-backup/id_ed25519.pub + dest: ../files/ssh/rsync-backup.pub + flat: true + +- name: Install cron job + ansible.builtin.cron: + name: daily rsync backup + job: /usr/local/sbin/backup-daily -a -p -r + hour: "00" + minute: "30" +