summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChloe Brown <chloe.brown.00@outlook.com>2022-01-04 10:40:18 +0000
committerChloe Brown <chloe.brown.00@outlook.com>2022-01-04 10:40:18 +0000
commitec260670d6b8ce0c5da9ff1aed2a76d5ab7a5496 (patch)
tree3ef1383ee3c76c3dc2c2301e3b168f47fcdf07eb
parentde32392b1c2977537f7f240367806f400ff2831b (diff)
services: btrfs: add snapshot services
-rw-r--r--yellowsquid/services/btrfs.scm295
1 files changed, 295 insertions, 0 deletions
diff --git a/yellowsquid/services/btrfs.scm b/yellowsquid/services/btrfs.scm
new file mode 100644
index 0000000..afa38d8
--- /dev/null
+++ b/yellowsquid/services/btrfs.scm
@@ -0,0 +1,295 @@
+(define-module (yellowsquid services btrfs)
+ #:use-module (gnu home services)
+ #:use-module (gnu home services mcron)
+ #:use-module (gnu packages linux)
+ #:use-module (gnu services)
+ #:use-module (gnu services configuration)
+ #:use-module (gnu services mcron)
+ #:use-module (guix gexp)
+ #:use-module (guix modules)
+ #:use-module (guix packages)
+ #:use-module (guix records)
+ #:use-module (ice-9 match)
+ #:use-module (srfi srfi-1)
+ #:export (<snapshot-location>
+ snapshot-location
+ make-snapshot-location
+ snapshot-location?
+ snapshot-location-name
+ snapshot-location-path
+
+ <snapshot-frequency>
+ snapshot-frequency
+ make-snapshot-frequency
+ snapshot-frequency?
+ snapshot-frequency-name
+ snapshot-frequency-min-period
+ snapshot-frequency-max-keep
+
+ list-of-locations?
+ list-of-frequencies?
+
+ <btrfs-snapshot-configuration>
+ btrfs-snapshot-configuration
+ make-btrfs-snapshot-configuration
+ btrfs-snapshot-configuration?
+ btrfs-snapshot-configuration-btrfs-progs
+ btrfs-snapshot-configuration-destination
+ btrfs-snapshot-configuration-frequencies
+ btrfs-snapshot-configuration-locations
+ btrfs-snapshot-configuration-ts-format
+
+ btrfs-snapshot-service-type
+ home-btrfs-snapshot-service-type))
+
+(define-record-type* <snapshot-location>
+ snapshot-location make-snapshot-location
+ snapshot-location?
+ (name snapshot-location-name)
+ (path snapshot-location-path))
+
+(define-record-type* <snapshot-frequency>
+ snapshot-frequency make-snapshot-frequency
+ snapshot-frequency?
+ (name snapshot-frequency-name)
+ ;; in seconds
+ (min-period snapshot-frequency-min-period)
+ (max-keep snapshot-frequency-max-keep))
+
+(define list-of-locations?
+ (list-of snapshot-location?))
+
+(define list-of-frequencies?
+ (list-of snapshot-frequency?))
+
+(define-configuration/no-serialization btrfs-snapshot-configuration
+ (btrfs-progs
+ (package btrfs-progs)
+ "The btrfs package to use.")
+ (destination
+ (string "/backup")
+ "Root directory where backups are stored.")
+ (frequencies
+ (list-of-frequencies '())
+ "List of backup frequencies.")
+ (locations
+ (list-of-locations '())
+ "List of locations to backup.")
+ (ts-format
+ (string "%FT%T")
+ "File timestamp format."))
+
+(define (frobnicate destination freq loc)
+ (string-append (if (string-suffix? "/" destination)
+ destination
+ (string-append destination "/"))
+ (symbol->string (snapshot-location-name loc))
+ "/"
+ (symbol->string (snapshot-frequency-name freq))
+ "/"))
+
+(define (btrfs-snapshot-one-job destination btrfs ts-format freq loc)
+ (define dest (frobnicate destination freq loc))
+
+ (define name->path+date
+ #~(lambda (name)
+ "Converts the NAME of a snapshot into the path to the snapshot and the
+date it was created."
+ (let ((strptimed
+ (false-if-exception (strptime #$ts-format name))))
+ (and strptimed
+ (list (string-append #$dest name)
+ (car (mktime (car strptimed) "GMT")))))))
+
+ (define name->date
+ #~(lambda (name)
+ "Converts the NAME of a snapshot into the date it was created."
+ (let ((path+date (#$name->path+date name)))
+ (and path+date (cadr path+date)))))
+
+ (define next-create
+ #~(lambda (date)
+ "Returns the next time after DATE that a new snapshot can be created."
+ (+ date #$(snapshot-frequency-min-period freq))))
+
+ (define first-delete
+ #~(lambda (date)
+ "Returns the first time after DATE that this snapshot can be deleted."
+ (+ date #$(snapshot-frequency-max-keep freq))))
+
+ (define make-time
+ #~(lambda (date)
+ "Returns DATE as a string."
+ (strftime #$ts-format (gmtime date))))
+
+ (list
+ #~(job (lambda (now)
+ (use-modules (ice-9 ftw))
+ (let* ((scanned (or (scandir #$dest) '()))
+ (dates (filter-map
+ #$name->date
+ scanned)))
+ (if (null? dates)
+ (next-minute-from now)
+ (max (if (< (abs (- now (current-time))) 5)
+ (next-minute-from (1+ now))
+ (let ((time (localtime now)))
+ (set-tm:year time (+ 1000 (tm:year time)))
+ (car (mktime time))))
+ (apply max (map #$next-create dates))))))
+ #$(program-file
+ (string-append "backup-create-"
+ (symbol->string (snapshot-location-name loc))
+ "-"
+ (symbol->string (snapshot-frequency-name freq))
+ ".scm")
+ (with-imported-modules
+ (source-module-closure '((guix build utils)))
+ #~(begin
+ (use-modules (guix build utils)
+ (ice-9 ftw)
+ (ice-9 match)
+ (srfi srfi-1))
+
+ (let* ((now (current-time))
+ (scanned (or (scandir #$dest)
+ (begin (mkdir-p #$dest) '())))
+ (dates (filter-map #$name->date scanned))
+ (skip? (any (lambda (date)
+ (and (< now (#$next-create date))
+ date))
+ dates)))
+ (if skip?
+ (format
+ #t
+ "Skipping ~a backup of ~a as last success was at ~a.\n"
+ #$(symbol->string (snapshot-frequency-name freq))
+ #$(symbol->string (snapshot-location-name loc))
+ (#$make-time skip?))
+ (invoke
+ #$btrfs
+ "subvolume"
+ "snapshot"
+ "-r"
+ #$(snapshot-location-path loc)
+ (string-append
+ #$dest
+ (#$make-time (current-time)))))))))
+ #$(format #f
+ "Create a ~a backup of ~a."
+ (symbol->string (snapshot-frequency-name freq))
+ (symbol->string (snapshot-location-name loc))))
+ #~(job (lambda (now)
+ (use-modules (ice-9 ftw))
+ (let* ((scanned (or (scandir #$dest) '()))
+ (dates (filter-map
+ #$name->date
+ scanned)))
+ (if (null? dates)
+ (next-minute-from now)
+ (max (if (< (abs (- now (current-time))) 5)
+ (next-minute-from (1+ now))
+ (let ((time (localtime now)))
+ (set-tm:year time (+ 1000 (tm:year time)))
+ (car (mktime time))))
+ (apply min (map #$first-delete dates))))))
+ #$(program-file
+ (string-append "backup-delete-"
+ (symbol->string (snapshot-location-name loc))
+ "-"
+ (symbol->string (snapshot-frequency-name freq))
+ ".scm")
+ (with-imported-modules
+ (source-module-closure '((guix build utils)))
+ #~(begin
+ (use-modules (guix build utils)
+ (ice-9 ftw)
+ (ice-9 match)
+ (srfi srfi-1))
+
+ (let* ((now (current-time))
+ (scanned (or (scandir #$dest)
+ (begin (mkdir-p #$dest) '())))
+ (path-dates (filter-map #$name->path+date scanned))
+ (del (filter-map
+ (match-lambda
+ ((path date)
+ (and (> now (#$first-delete date))
+ path)))
+ path-dates)))
+ (for-each
+ (lambda (path)
+ (invoke #$btrfs
+ "property"
+ "set"
+ path
+ "ro"
+ "false"))
+ del)
+ (unless (null? del)
+ (apply invoke
+ #$btrfs
+ (cons* "subvolume" "delete" "-c" del)))))))
+ #$(format #f
+ "Delete a ~a backup of ~a."
+ (symbol->string (snapshot-frequency-name freq))
+ (symbol->string (snapshot-location-name loc))))))
+
+(define (btrfs-snapshot-jobs config)
+ "Returns a list of mcron jobs to manage local backups."
+ (define (product xs ys)
+ (match xs
+ (() '())
+ ((x xs ...) (append (map (lambda (y) (list x y)) ys) (product xs ys)))))
+
+ (match-record config <btrfs-snapshot-configuration>
+ (btrfs-progs destination frequencies locations ts-format)
+ (append-map
+ (match-lambda
+ ((freq loc)
+ (btrfs-snapshot-one-job
+ destination
+ (file-append btrfs-progs "/bin/btrfs")
+ ts-format
+ freq
+ loc)))
+ (product frequencies locations))))
+
+(define (btrfs-snapshot-activation config)
+ "Returns a list of mcron jobs to manage local backups."
+ (match-record config <btrfs-snapshot-configuration>
+ (btrfs-progs destination frequencies locations ts-format)
+ #~(begin
+ #@(append-map
+ (lambda (loc)
+ (map
+ (lambda (freq)
+ #~(mkdir-p #$(frobnicate destination loc freq)))
+ frequencies))
+ locations))))
+
+(define home-btrfs-snapshot-service-type
+ (service-type (name 'home-btrfs-snapshot-service)
+ (extensions
+ (list (service-extension
+ home-mcron-service-type
+ btrfs-snapshot-jobs)
+ (service-extension
+ home-activation-service-type
+ btrfs-snapshot-activation)))
+ (default-value (btrfs-snapshot-configuration))
+ (description "Create jobs to keep a rolling set of backups for
+some btrfs subvolumes.")))
+
+(define btrfs-snapshot-service-type
+ (service-type (name 'btrfs-snapshot-service)
+ (extensions
+ (list (service-extension
+ mcron-service-type
+ btrfs-snapshot-jobs)
+ (service-extension
+ activation-service-type
+ btrfs-snapshot-activation)))
+ (default-value (btrfs-snapshot-configuration))
+ (description "Create jobs to keep a rolling set of backups for
+some btrfs subvolumes.")))