aboutsummaryrefslogtreecommitdiff
path: root/tw/services/restic.scm
blob: 18c501a914d6f75f9cf6d61778454ea3133537d3 (about) (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
(define-module (tw services restic)
  #:use-module (gnu)
  #:use-module (gnu home services mcron)
  #:use-module ((gnu packages admin)
                #:select (shadow))
  #:use-module (gnu packages backup)
  #:use-module (gnu services)
  #:use-module (gnu services configuration)
  #:use-module (gnu services mcron)
  #:use-module (gnu services shepherd)
  #:use-module (guix gexp)
  #:use-module (guix packages)
  #:use-module (srfi srfi-1)
  #:export (restic-server-service-type
            restic-server-configuration
            home-restic-backup-service-type
            restic-backup-repository))

(define-public %restic-user "restic")
(define-public %restic-group "restic")


;; Restic REST server

(define-maybe/no-serialization integer)
(define-maybe/no-serialization string)

;; TODO: implement --tls, --tls-cert and --tls-key, maybe using certbot-service-type?
;; TODO: implement --log
(define-configuration/no-serialization restic-server-configuration
  (repository-path (string "/var/lib/restic") "The directory containing
restic's repositories and @code{.htpasswd} file, unless otherwise configured
using @code{htpasswd-file}.")
  (restic-server (package restic-rest-server) "The restic REST server package to use.")
  (bind-address (string ":8000") "The listen address (including port) to bind to.")
  (htpasswd-file (maybe-string %unset-value) "Location of @code{.htpasswd}
file (default: @code{REPOSITORY-PATH/.htpasswd}).  Use @code{htpasswd} from
the @code{httpd} package to create and/or update this file.")
  (auth? (boolean #t) "Whether to authenticate users at all (using .htpasswd).")
  (verify-upload? (boolean #t) "Whether to verify the integrity of uploaded
data.  @emph{Do not disable} unless the restic server is to be run on a very
low-power device.")
  (append-only? (boolean #f) "Whether to run the restic server in append-only mode.")
  (max-repository-size (maybe-integer %unset-value) "Maximum repository size
in bytes, if any.")
  (private-repos-only? (boolean #f) "Whether to let users only access their
private restic repos.")
  (prometheus? (boolean #f) "Whether to serve Prometheus metrics.")
  (prometheus-auth? (boolean #t) "Whether to require authentication as the
@code{metrics} user to access the Prometheus /metrics endpoint."))

(define (restic-server-arguments config)
  "Turn CONFIG into a list of arguments to the restic-rest-server executable."
  `("--path" ,(restic-server-configuration-repository-path config)
    "--listen" ,(restic-server-configuration-bind-address config)
    ,@(let ((htpasswd-file (restic-server-configuration-htpasswd-file config)))
        (if (string? htpasswd-file) `("--htpasswd-file" ,htpasswd-file) '()))
    ,@(if (restic-server-configuration-auth? config) '() '("--no-auth"))
    ,@(if (restic-server-configuration-verify-upload? config) '() '("--no-verify-upload"))
    ,@(if (restic-server-configuration-append-only? config) '("--append-only") '())
    ,@(let ((max-size (restic-server-configuration-max-repository-size config)))
        (if (integer? max-size) `("--max-size" ,max-size) '()))
    ,@(if (restic-server-configuration-private-repos-only? config) '("--private-repos") '())
    ,@(if (restic-server-configuration-prometheus? config) '("--prometheus") '())
    ,@(if (restic-server-configuration-prometheus-auth? config) '() '("--prometheus-no-auth"))))

(define (restic-server-service config)
  "Create a `shepherd-service' for the restic REST server from CONFIG."
  (list (shepherd-service
         (provision '(restic-server))
         (requirement '(networking))
         (documentation "Run the Restic REST server to serve backup repositories via HTTP.")
         (start #~(make-forkexec-constructor
                   (list #$(file-append (restic-server-configuration-restic-server config)
                                        "/bin/restic-rest-server")
                         #$@(restic-server-arguments config))
                   #:user #$%restic-user #:group #$%restic-group))
         (stop #~(make-kill-destructor)))))

(define (restic-server-accounts config)
  "Create user accounts and groups for the restic REST server defined in CONFIG."
  (list (user-account
         (name %restic-user)
         (group %restic-group)
         (comment "Restic server user")
         (system? #t)
         (home-directory (restic-server-configuration-repository-path config))
         (shell (file-append shadow "/sbin/nologin")))
        (user-group
         (name %restic-group)
         (system? #t))))

(define restic-server-service-type
  (service-type
   (name 'restic-server)
   (extensions
    (list (service-extension shepherd-root-service-type restic-server-service)
          (service-extension account-service-type restic-server-accounts)))
   (description
    "Restic REST server, running as a service user instead of root.")))


;; Restic cleanup cronjobs

(define-configuration/no-serialization restic-cleanup-repository
  (repository-path string "The directory path of the restic repository to clean up.")
  (owner (string %restic-user) "The user that owns the REPOSITORY-PATH.")
  (group (string %restic-group) "The group that owns the REPOSITORY-PATH."))

(define-configuration/no-serialization restic-cleanup-configuration
  (restic (package restic) "The restic package to use."))

(define (restic-deletion-service config) #f)
(define (restic-prune-service config) #f)

(define-public restic-cleanup-service-type
  (service-type
   (name 'restic-cleanup)
   (extensions
    (list (service-extension mcron-service-type restic-deletion-service)
          (service-extension mcron-service-type restic-prune-service)))
   (description
    "")))


;; Restic scheduled backup jobs

(define (nonempty-list-of-strings? thing)
  (and (pair? thing) (list-of-strings? thing)))

(define-configuration/no-serialization restic-backup-repository
  (schedule gexp "An mcron schedule, specified as a gexp
(@pxref{G-Expressions}), to use for the backup job.  String, list or lambda
syntax is fine (@pxref{Syntax, mcron job specifications,, mcron,
GNU@tie{}mcron}).")
  (paths nonempty-list-of-strings "List of paths to back up.  At least one
must be given.  Leading @code{~/} are replaced with @code{$HOME}.")
  (url-command string "Run this command (inside @code{sh -c}) to obtain the
directory or URL of the repo to back up to.  This allows you to substitute
passwords in @code{rest:} URLs.")
  (password-command string "Run this command to obtain the repository password.")
  (tags (list-of-strings '()) "Optional tags to add to the snapshot.")
  (restic (package restic) "The restic package to use."))

(define (restic-backup-cronjobs repositories)
  (define (cronjob repo)
    #~(job #$(restic-backup-repository-schedule repo)
           #$(program-file
              "restic-backup-command"
              #~(begin
                  (use-modules (ice-9 popen)
                               (ice-9 textual-ports)
                               (srfi srfi-1))
                  (define (replace-home path)
                    (if (string-prefix? "~/" path)
                        (string-replace path (getenv "HOME") 0 1)
                        path))
                  (setenv "RESTIC_PASSWORD_COMMAND"
                          '#$(restic-backup-repository-password-command repo))
                  (let ((pipe (open-pipe '#$(restic-backup-repository-url-command repo) OPEN_READ)))
                    (setenv "RESTIC_REPOSITORY" (string-trim-right (get-string-all pipe) #\newline))
                    (close-pipe pipe))
                  (apply execl #$(file-append (restic-backup-repository-restic repo) "/bin/restic")
                         "restic" "backup" "--cleanup-cache"
                         #$@(append-map (lambda (tag) (list "--tag" tag))
                                        (restic-backup-repository-tags repo))
                         (map replace-home '#$(restic-backup-repository-paths repo)))))))
  (map cronjob repositories))

(define home-restic-backup-service-type
  (service-type
   (name 'restic-backup)
   (extensions
    (list (service-extension home-mcron-service-type restic-backup-cronjobs)))
   (compose concatenate)
   (extend append)
   (default-value '())
   (description "Back up local directories on a schedule.")))