From 1aba2885dc6aef02661848ba4f75d4ba821550c4 Mon Sep 17 00:00:00 2001
From: Adar Nimrod <nimrod@shore.co.il>
Date: Sat, 23 Apr 2022 23:34:16 +0300
Subject: [PATCH] Debian server role: Docker volume backup.

Script, Systemd service and timer to backup Docker volumes marked as
such.
---
 Ansible/roles/debian_server/files/dvb         | 77 +++++++++++++++++++
 Ansible/roles/debian_server/files/dvb.service |  9 +++
 Ansible/roles/debian_server/files/dvb.timer   | 10 +++
 Ansible/roles/debian_server/tasks/docker.yml  | 32 ++++++++
 4 files changed, 128 insertions(+)
 create mode 100755 Ansible/roles/debian_server/files/dvb
 create mode 100644 Ansible/roles/debian_server/files/dvb.service
 create mode 100644 Ansible/roles/debian_server/files/dvb.timer

diff --git a/Ansible/roles/debian_server/files/dvb b/Ansible/roles/debian_server/files/dvb
new file mode 100755
index 0000000..00d0ed5
--- /dev/null
+++ b/Ansible/roles/debian_server/files/dvb
@@ -0,0 +1,77 @@
+#!/bin/sh
+set -eu
+
+# Cleanup of snapshots, added as trap so it's always executed even in case of a
+# failure or ctrl+c.
+cleanup () {
+    btrfs subvolume delete "$snapshot"
+}
+
+usage () {
+    echo "Usage: $(basename "$0") DESTINATION" >&2
+    exit 1
+}
+
+get_mountpoint () {
+    df --output=target "$1" | tail +2
+}
+
+get_snapshot () {
+    name="$1"
+    mountpoint="$(docker volume inspect --format '{{ .Mountpoint }}' "$name" )"
+    snapshot="$(dirname "$mountpoint")/snapshot)"
+    echo "$snapshot"
+}
+
+if [ "$#" -ne 1 ]
+then
+    usage
+elif [ ! -d "$1" ]
+then
+    echo "Destinaton $1 not found or is not a directory." >&2
+    exit 1
+elif [ "$(id -u)" -ne 0 ]
+then
+    echo "This program must run as root." >&2
+    exit 1
+elif ! command -v btrfs >/dev/null
+then
+    echo 'btrfs-progs must be installed.' >&2
+    exit 1
+elif ! docker info --format '{{ .ServerVersion }}' >/dev/null
+then
+    echo 'Docker must be installed and running.' >&2
+    exit 1
+elif ! df --type=btrfs /var/lib/docker/volumes >/dev/null
+then
+    echo "Docker volumes are not on a btrfs filesystem." >&2
+    exit 1
+elif ! df --type=btrfs "$1" >/dev/null
+then
+    echo "Destination $1 is not on a btrfs filesystem." >&2
+    exit 1
+elif [ "$(df --output=target /var/lib/docker/volumes)" != "$(df --output=target "$1")" ]
+then
+    echo "The destination $1 is not on the same btrfs volume as the Docker volumes." >&2
+    exit 1
+elif [ -z "$(docker volume ls --filter label=snapshot=true --format '{{ .Name }}')" ]
+then
+    echo "No Docker volumes marked for backup, exiting." >&2
+    exit 1
+fi
+
+dest="$1"
+root_volume="$(get_mountpoint /var/lib/docker/volumes)"
+snapshot="$(mktemp --dry-run "--tmpdir=$root_volume")"
+trap 'cleanup' INT QUIT EXIT TERM
+btrfs subvolume snapshot "$root_volume" "$snapshot"
+
+for name in $(docker volume ls --filter label=snapshot=true --format '{{ .Name }}')
+do
+    echo "Backing up $name."
+    mountpoint="$(docker volume inspect --format '{{ .Mountpoint }}' "$name")"
+    src="$snapshot/${mountpoint#$root_volume/}"
+    # shellcheck disable=SC2115
+    [ ! -e "$dest/$name" ] || rm -rf "$dest/$name"
+    cp --archive --force --reflink=always "$src" "$dest/$name"
+done
diff --git a/Ansible/roles/debian_server/files/dvb.service b/Ansible/roles/debian_server/files/dvb.service
new file mode 100644
index 0000000..37617a3
--- /dev/null
+++ b/Ansible/roles/debian_server/files/dvb.service
@@ -0,0 +1,9 @@
+# vim: filetype=systemd
+[Unit]
+Description=Docker volume backup (dvb)
+ConditionACPower=true
+After=local-fs.target
+
+[Service]
+Type=exec
+ExecStart=dvb /var/backups/docker-volumes
diff --git a/Ansible/roles/debian_server/files/dvb.timer b/Ansible/roles/debian_server/files/dvb.timer
new file mode 100644
index 0000000..5606bdd
--- /dev/null
+++ b/Ansible/roles/debian_server/files/dvb.timer
@@ -0,0 +1,10 @@
+# vim: filetype=systemd
+[Unit]
+Description=Docker volume backup (dvb)
+
+[Timer]
+OnCalendar=weekly
+RandomizedDelaySec=21600
+
+[Install]
+WantedBy=multi-user.target
diff --git a/Ansible/roles/debian_server/tasks/docker.yml b/Ansible/roles/debian_server/tasks/docker.yml
index f5d5e8f..9c15907 100644
--- a/Ansible/roles/debian_server/tasks/docker.yml
+++ b/Ansible/roles/debian_server/tasks/docker.yml
@@ -55,3 +55,35 @@
     direction: in
     interface: docker0
     rule: allow
+
+- name: Create the Docker volume backup destination directory
+  ansible.builtin.file:
+    group: backup
+    mode: 0o0750
+    owner: root
+    path: /var/backups/docker-volumes
+    state: directory
+
+- name: Copy the Docker volume backup script
+  ansible.builtin.copy:
+    dest: /usr/local/bin/
+    mode: preserve
+    src: dvb
+
+- name: Copy the Docker volume backup service and timer
+  loop:
+    - service
+    - timer
+  ansible.builtin.copy:
+    dest: /etc/systemd/system/dvb.{{ item }}
+    mode: 0o0644
+    src: dvb.{{ item }}
+  notify:
+    - Systemd daemon reload
+
+- name: Enable the Docker volume backup timer
+  ansible.builtin.systemd:
+    enabled: true
+    name: dvb.timer
+  notify:
+    - Systemd daemon reload
-- 
GitLab