diff options
| -rw-r--r-- | yellowsquid/services/btrfs.scm | 295 | 
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.")))  | 
