From 9eae00f2a76dc1bf62c70080805ff2e1522c1a7a Mon Sep 17 00:00:00 2001 From: Timo Wilken Date: Tue, 10 Oct 2023 00:24:00 +0200 Subject: Store Nextcloud backups in restic repo Replace the Nextcloud backup shell script with a Guile program that writes to a restic repository instead. --- tw/services/files/nextcloud-backup | 68 -------------------- tw/services/nextcloud.scm | 124 ++++++++++++++++++++++++++++++++----- 2 files changed, 109 insertions(+), 83 deletions(-) delete mode 100755 tw/services/files/nextcloud-backup (limited to 'tw/services') diff --git a/tw/services/files/nextcloud-backup b/tw/services/files/nextcloud-backup deleted file mode 100755 index 4c533758..00000000 --- a/tw/services/files/nextcloud-backup +++ /dev/null @@ -1,68 +0,0 @@ -#!/bin/sh -e -# Nextcloud backup script, to run nightly. -# Documentation on backups: -# https://docs.nextcloud.com/server/latest/admin_manual/maintenance/backup.html -# https://docs.nextcloud.com/server/latest/admin_manual/maintenance/restore.html -# https://git.mdns.eu/nextcloud/passwords/-/wikis/Administrators/Backups - -. /etc/default/nextcloud-backup -: "${DATABASE_PASSWORD:?You must pass the MySQL database password as DATABASE_PASSWORD}" - -php_ini=$1 -backup_dir=/var/backups/nextcloud/$(date -u '+%Y-%m-%d') -nextcloud_dir=/var/www/nextcloud -nextcloud_data_partition=/var/data # mountpoint of the partition containing Nextcloud data dir -nextcloud_data_path=nextcloud # relative to $nextcloud_data_partition -snapshot=$nextcloud_data_partition/tmp-nextcloud-backup - -nc_maintenance () { - # Enable (--on) or disable (--off) Nextcloud's maintenance mode. - sudo -nu httpd php ${php_ini:+-c "$php_ini"} "$nextcloud_dir/occ" maintenance:mode "$@" -} - -# If there is a previous backup, compare against it later (so we don't have to -# transfer every file). -last_backup_dir=$(ls -1d "$(dirname "$backup_dir")"/????-??-?? | LC_ALL=C sort | tail -1) -[ -d "$last_backup_dir" ] || unset last_backup_dir - -# Don't overwrite existing backups. mkdir will fail if $backup_dir exists. -mkdir -m 750 "$backup_dir" - -# Always turn off maintenance mode and clean up the temporary snapshot on exit, -# whether or not the backup succeeded. -cleanup () { - nc_maintenance --off || : - if [ -d "$snapshot" ]; then - btrfs subvolume delete -c "$snapshot" || : - fi -} -trap cleanup EXIT HUP INT TERM # can't trap KILL - -# Turn Nextcloud off temporarily so the data doesn't change during the backup. -nc_maintenance --on - -# Backup the database. This can only be done offline. -mysqldump --single-transaction --default-character-set=utf8mb4 \ - -u nextcloud -p"$DATABASE_PASSWORD" nextcloud > "$backup_dir/nextcloud.sql" - -# These shouldn't be copied while Nextcloud is online. -rsync -AUXHavx ${last_backup_dir+--link-dest="$last_backup_dir"} \ - "$nextcloud_dir/config" "$nextcloud_dir/themes" "$backup_dir" - -# Make sure everything is synced to disk so it's in our snapshot. -btrfs filesystem sync "$nextcloud_data_partition/$nextcloud_data_path" -btrfs subvolume snapshot -r "$nextcloud_data_partition" "$snapshot" - -# At this point, the data directory is in the snapshot, so Nextcloud can be -# turned on again. -nc_maintenance --off - -# --link-dest is brittle (it only hardlinks to the old file if no metadata has -# changed). Reflinks would be better, but rsync doesn't seem to support them. -# We don't need files under preview/, as those are thumbnails from the Previews -# "app" and can be regenerated using `php -f occ preview:pre-generate`. -rsync -AUXHavx --exclude='appdata_*/preview' --exclude='appdata_*/passwords/*Cache' \ - ${last_backup_dir+--link-dest="$last_backup_dir/data"} \ - "$snapshot/$nextcloud_data_path/" "$backup_dir/data" -# Make sure everything is written out to the backup disk before we exit. -btrfs filesystem sync "$backup_dir" diff --git a/tw/services/nextcloud.scm b/tw/services/nextcloud.scm index 6ede7005..e7952b49 100644 --- a/tw/services/nextcloud.scm +++ b/tw/services/nextcloud.scm @@ -1,6 +1,9 @@ (define-module (tw services nextcloud) #:use-module (gnu) + #:use-module (gnu packages backup) #:use-module (gnu packages certs) + #:use-module (gnu packages databases) + #:use-module (gnu packages linux) #:use-module (gnu packages php) #:use-module (gnu services certbot) #:use-module (gnu services mcron) @@ -39,6 +42,107 @@ opcache.save_comments=1 opcache.revalidate_freq=120 ")))))))) +(define nextcloud-backup-program + (program-file "nextcloud-backup-command" + #~(begin + (use-modules (srfi srfi-1) + (srfi srfi-26) + (ice-9 popen) + (ice-9 receive) + (ice-9 textual-ports)) + + (define nextcloud-dir "/var/www/nextcloud") + (define nextcloud-data-partition "/var/data") ; mountpoint of the partition containing Nextcloud data dir + (define nextcloud-data-path "nextcloud") ; relative to `nextcloud-data-partition' + (define snapshot (string-append nextcloud-data-partition "/tmp-nextcloud-backup")) + (define btrfs #$(file-append btrfs-progs "/bin/btrfs")) + (define restic #$(file-append restic "/bin/restic")) + (setenv "RESTIC_REPOSITORY" "/var/backups/nextcloud") + (setenv "RESTIC_PASSWORD_FILE" "/etc/restic/lud-nextcloud") + + (define (nc-maintenance enable?) + (let ((child-pid (primitive-fork))) + (if (zero? child-pid) + (begin ; this is the child + (setgid (group:gid (getgr "httpd"))) ; while still root + (setuid (passwd:uid (getpw "httpd"))) + (execl #$(file-append php "/bin/php") + "php" "-c" #$%nextcloud-php.ini + (string-append nextcloud-dir "/occ") + "maintenance:mode" + (if enable? "--on" "--off"))) + (zero? (status:exit-val (cdr (waitpid child-pid))))))) + + (define* (cleanup #:optional (recv-signal #f) #:key (rethrow #f)) + (nc-maintenance #f) + (when (and (file-exists? snapshot) + (file-is-directory? snapshot)) + (system* btrfs "subvolume" "delete" "-c" snapshot)) + (cond + (recv-signal (exit (- recv-signal))) + (rethrow (raise-exception rethrow)))) + + (define (run-pipeline . commands) + (receive (from to pids) (pipeline commands) + (close to) + (do ((char (read-char from) (read-char from))) + ((eof-object? char)) + (display char)) + (close from) + (let ((failing-index + (list-index (compose not zero? status:exit-val cdr waitpid) + (reverse pids)))) + (when failing-index + (apply error "Command exited with error status" + (list-ref commands failing-index)))))) + + (define (read-file name) + (string-trim-right (call-with-input-file name get-string-all) #\newline)) + + (define (main) + (unless (nc-maintenance #t) + (error "Could not enter maintenance mode")) + + ;; Backup the database. This can only be done offline. + (run-pipeline + ;; `mysql-configuration' uses mariadb by default, so match it here. + (list #$(file-append mariadb "/bin/mysqldump") + "--single-transaction" "--quick" "--default-character-set=utf8mb4" + (string-append "-p" (read-file "/etc/default/nextcloud-database-password")) + "-u" "nextcloud" "nextcloud") + (list restic "backup" "--no-cache" "--stdin" "--stdin-filename=nextcloud.sql")) + + ;; These shouldn't be copied while Nextcloud is online. They're also + ;; not in the data folder, so they won't be in the snapshot below. + (run-pipeline + (list restic "backup" "--no-cache" + (string-append nextcloud-dir "/config") + (string-append nextcloud-dir "/themes"))) + + ;; Make sure everything is synced to disk so it's in our snapshot. + (run-pipeline + (list btrfs "filesystem" "sync" + (string-append nextcloud-data-partition "/" nextcloud-data-path))) + (run-pipeline + (list btrfs "subvolume" "snapshot" "-r" nextcloud-data-partition snapshot)) + + ;; At this point, the data directory is in the snapshot, so Nextcloud + ;; can be turned on again. + (nc-maintenance #f) + + ;; We don't need files under preview/, as those are thumbnails from + ;; the Previews "app" and can be regenerated using `php -f occ + ;; preview:pre-generate`. + (run-pipeline + (list restic "backup" "--no-cache" + "--exclude=appdata_*/preview" + "--exclude=appdata_*/passwords/*Cache" + (string-append snapshot "/" nextcloud-data-path)))) + + (for-each (cut sigaction <> cleanup) (list SIGHUP SIGINT SIGTERM)) + (with-exception-handler (cut cleanup #:rethrow <>) main) + (cleanup)))) + (define-public %nextcloud-services (list (simple-service 'nextcloud-https-server httpd-service-type ;; The certbot service redirects everything on port 80 to @@ -106,21 +210,11 @@ Header always set Strict-Transport-Security \"max-age=15552000\" (list #~(job "*/5 * * * *" #$(program-file "nextcloud-cron-command" #~(begin - ;; `setgid' first while we're still root - (setgid (group:gid (getgr "httpd"))) - (setuid (passwd:uid (getpw "httpd"))) - (chdir "/var/www/nextcloud") ;; Nextcloud News needs this to fetch HTTPS feeds. (setenv "SSL_CERT_DIR" #$(file-append nss-certs "/etc/ssl/certs")) (execl #$(file-append php "/bin/php") "php" - "-c" #$%nextcloud-php.ini "cron.php")))) - - ;; Nextcloud backups - ;; Requires: sudo, php, btrfs, mysqldump, rsync - (let ((backup-script (local-file "files/nextcloud-backup" #:recursive? #t))) - #~(job "0 6 * * *" - (lambda () - ;; Pass through the php.ini file that allows us to - ;; use Nextcloud's occ script. - (execl #$backup-script "nextcloud-backup" #$%nextcloud-php.ini)) - (string-append #$backup-script " " #$%nextcloud-php.ini))))))) + "-c" #$%nextcloud-php.ini "cron.php"))) + #:user "httpd") + + ;; TODO: try `with-mail-out' from `(mcron redirect)'? + #~(job "0 6 * * *" #$nextcloud-backup-program))))) -- cgit v1.2.3