about summary refs log tree commit diff
path: root/bundles/restic-backup
diff options
context:
space:
mode:
authorRobert Günzler <r@gnzler.io>2025-09-07 17:32:13 +0200
committerRobert Günzler <r@gnzler.io>2025-09-07 17:42:42 +0200
commit38be6c13f76e893cf47636257071b440dec0c5b2 (patch)
treef7ded3235a7e3362cbd1ed3d91523968fe0faf90 /bundles/restic-backup
initial commit
Signed-off-by: Robert Günzler <r@gnzler.io>
Diffstat (limited to 'bundles/restic-backup')
-rw-r--r--bundles/restic-backup/files/cron_job5
-rw-r--r--bundles/restic-backup/files/excludes3
-rw-r--r--bundles/restic-backup/files/includes3
-rw-r--r--bundles/restic-backup/files/restic.conf4
-rw-r--r--bundles/restic-backup/files/resticbackup36
-rw-r--r--bundles/restic-backup/files/resticrestore29
-rw-r--r--bundles/restic-backup/files/resticw21
-rw-r--r--bundles/restic-backup/items.py62
-rw-r--r--bundles/restic-backup/metadata.py7
9 files changed, 170 insertions, 0 deletions
diff --git a/bundles/restic-backup/files/cron_job b/bundles/restic-backup/files/cron_job
new file mode 100644
index 0000000..6fcea06
--- /dev/null
+++ b/bundles/restic-backup/files/cron_job
@@ -0,0 +1,5 @@
+#!/bin/sh
+set -eu
+/usr/bin/resticbackup >/dev/null 2>&1
+tgnotify '✨*${node.name}/resticbackup*\nsuccess!' >/dev/null 2>&1
+exit 0
diff --git a/bundles/restic-backup/files/excludes b/bundles/restic-backup/files/excludes
new file mode 100644
index 0000000..8a02379
--- /dev/null
+++ b/bundles/restic-backup/files/excludes
@@ -0,0 +1,3 @@
+% for path in paths:
+${path}
+% endfor
diff --git a/bundles/restic-backup/files/includes b/bundles/restic-backup/files/includes
new file mode 100644
index 0000000..8a02379
--- /dev/null
+++ b/bundles/restic-backup/files/includes
@@ -0,0 +1,3 @@
+% for path in paths:
+${path}
+% endfor
diff --git a/bundles/restic-backup/files/restic.conf b/bundles/restic-backup/files/restic.conf
new file mode 100644
index 0000000..3807fd7
--- /dev/null
+++ b/bundles/restic-backup/files/restic.conf
@@ -0,0 +1,4 @@
+RESTIC_REPOSITORY=b2:${repo.vault.cmd(f"rbw get {node.name}/b2/backupbucket")}:/
+RESTIC_PASSWORD=${repo.vault.password_for(f"b2_bucket_{node.name}_pw")}
+B2_ACCOUNT_ID=${repo.vault.cmd(f"rbw get {node.name}/b2/keyid")}
+B2_ACCOUNT_KEY=${repo.vault.cmd(f"rbw get {node.name}/b2/appkey")}
diff --git a/bundles/restic-backup/files/resticbackup b/bundles/restic-backup/files/resticbackup
new file mode 100644
index 0000000..af61d7e
--- /dev/null
+++ b/bundles/restic-backup/files/resticbackup
@@ -0,0 +1,36 @@
+#!/bin/sh
+set -eu
+
+backup_includes=/etc/restic/includes
+backup_excludes=/etc/restic/excludes
+backup_tag=foobar
+backup_keep=1m
+
+main() {
+	# remove stale locks
+	resticw unlock
+
+	# run backup
+	resticw backup \
+		--one-file-system \
+		--files-from "${backup_includes}" \
+		--exclude-file "${backup_excludes}" \
+		--tag "${backup_tag}"
+
+	# prune backups older than $backup_keep duration
+	resticw forget \
+		--keep-within "${backup_keep}" \
+		--prune \
+		--group-by "paths,tags" \
+		--tag "${backup_tag}"
+
+	# TODO: restic check
+	# TODO: dump stats to file in metrics format to /run/prom/restic
+}
+
+exit_hook() {
+	resticw unlock
+}
+trap exit_hook INT EXIT
+
+main
diff --git a/bundles/restic-backup/files/resticrestore b/bundles/restic-backup/files/resticrestore
new file mode 100644
index 0000000..68a10f1
--- /dev/null
+++ b/bundles/restic-backup/files/resticrestore
@@ -0,0 +1,29 @@
+#!/bin/sh
+set -eu
+
+backup_tag=foobar
+backup_restore_snapshot=latest
+
+main() {
+	# remove stale locks
+	resticw unlock
+
+	# run restore
+	resticw restore \
+		--verify \
+		--tag "${backup_tag}" \
+		--include "${1:-*}" \
+		--target / \
+		"${backup_restore_snapshot}"
+
+	# TODO: restic check
+	# TODO: dump stats to file in metrics format to /run/prom/restic
+}
+
+exit_hook() {
+	resticw unlock
+}
+trap exit_hook INT EXIT
+
+[ "$1" = "-h" ] && printf 'usage: resticrestore [TARGET]\n\n' && exit 0
+main "$1"
diff --git a/bundles/restic-backup/files/resticw b/bundles/restic-backup/files/resticw
new file mode 100644
index 0000000..e3a3e87
--- /dev/null
+++ b/bundles/restic-backup/files/resticw
@@ -0,0 +1,21 @@
+#!/bin/sh
+set -eu
+
+# import environment from config
+# can overwrite above defaults
+set -a
+. /etc/restic/restic.conf
+set +a
+
+test -n "${RESTIC_REPOSITORY+1}"
+test -n "${RESTIC_PASSWORD+1}"
+
+case "${RESTIC_REPOSITORY}" in
+b2*)
+	test -n "${B2_ACCOUNT_ID+1}"
+	test -n "${B2_ACCOUNT_KEY+1}"
+	;;
+
+esac
+
+exec restic $@
diff --git a/bundles/restic-backup/items.py b/bundles/restic-backup/items.py
new file mode 100644
index 0000000..9ffc565
--- /dev/null
+++ b/bundles/restic-backup/items.py
@@ -0,0 +1,62 @@
+if node.os != "alpine":
+    raise BundleError(f"{node.name}: OS {node.os} is not supported.")
+
+files = {
+    "/usr/bin/resticw": {
+        "source": "resticw",
+        "content_type": "text",
+        "mode": "0755",
+    },
+    "/usr/bin/resticbackup": {
+        "source": "resticbackup",
+        "content_type": "text",
+        "mode": "0755",
+    },
+    "/usr/bin/resticrestore": {
+        "source": "resticrestore",
+        "content_type": "text",
+        "mode": "0755",
+    },
+    "/etc/restic/restic.conf": {
+        "source": "restic.conf",
+        "content_type": "mako",
+        "mode": "0600",
+    },
+    "/etc/restic/includes": {
+        "source": "includes",
+        "content_type": "mako",
+        "context": {
+            "paths": sorted(node.metadata.get("backup/includes", {})),
+        },
+    },
+    "/etc/restic/excludes": {
+        "source": "includes",
+        "content_type": "mako",
+        "context": {
+            "paths": sorted(node.metadata.get("backup/excludes", {})),
+        },
+    },
+    "/etc/periodic/weekly/restic-backup": {
+        "source": "cron_job",
+        "content_type": "mako",
+        "mode": "0755",
+        "needs": {
+            "bundle:tgnotify",
+        }
+    },
+}
+
+actions = {
+    f"initialize_restic_repository": {
+        "command": "resticw init",
+        "unless": "resticw cat config",
+        "needs": {
+            "file:/usr/bin/resticw",
+            "file:/etc/restic/restic.conf",
+        },
+        "triggered": True,
+        "triggered_by": {
+            "file:/etc/restic/restic.conf",
+        },
+    }
+}
diff --git a/bundles/restic-backup/metadata.py b/bundles/restic-backup/metadata.py
new file mode 100644
index 0000000..583be1f
--- /dev/null
+++ b/bundles/restic-backup/metadata.py
@@ -0,0 +1,7 @@
+defaults = {
+    "apk": {
+        "packages": {
+            "restic": {},
+        },
+    },
+}