projects/guix·blob java-team · refs

379 lines · 14.3 KiB

1;;; GNU Guix --- Functional package management for GNU
2;;; Copyright © 2024, 2025 Giacomo Leidi <[email protected]>
3;;;
4;;; This file is part of GNU Guix.
5;;;
6;;; GNU Guix is free software; you can redistribute it and/or modify it
7;;; under the terms of the GNU General Public License as published by
8;;; the Free Software Foundation; either version 3 of the License, or (at
9;;; your option) any later version.
10;;;
11;;; GNU Guix is distributed in the hope that it will be useful, but
12;;; WITHOUT ANY WARRANTY; without even the implied warranty of
13;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14;;; GNU General Public License for more details.
15;;;
16;;; You should have received a copy of the GNU General Public License
17;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
18
19(define-module (gnu services backup)
20 #:use-module (gnu packages backup)
21 #:use-module (gnu packages bash)
22 #:use-module (gnu services)
23 #:use-module (gnu services configuration)
24 #:use-module (gnu services shepherd)
25 #:use-module (guix build-system copy)
26 #:use-module (guix gexp)
27 #:use-module ((guix licenses)
28 #:prefix license:)
29 #:use-module (guix modules)
30 #:use-module (guix packages)
31 #:use-module (guix records)
32 #:use-module (srfi srfi-1)
33 #:export (restic-backup-job
34 restic-backup-job?
35 restic-backup-job-fields
36 restic-backup-job-restic
37 restic-backup-job-user
38 restic-backup-job-group
39 restic-backup-job-log-file
40 restic-backup-job-max-duration
41 restic-backup-job-wait-for-termination?
42 restic-backup-job-name
43 restic-backup-job-repository
44 restic-backup-job-password-file
45 restic-backup-job-schedule
46 restic-backup-job-files
47 restic-backup-job-requirement
48 restic-backup-job-verbose?
49 restic-backup-job-extra-flags
50
51 lower-restic-backup-job
52
53 restic-backup-configuration
54 restic-backup-configuration?
55 restic-backup-configuration-jobs
56
57 restic-backup-job-program
58 restic-backup-job->shepherd-service
59 restic-guix
60 restic-guix-wrapper-package
61 restic-backup-service-profile
62 restic-program
63 restic-job-log-file
64 restic-backup-job-command
65 restic-backup-job-modules
66 restic-backup-service-type))
67
68(define (gexp-or-string? value)
69 (or (gexp? value)
70 (string? value)))
71
72(define (lowerable? value)
73 (or (file-like? value)
74 (gexp-or-string? value)))
75
76(define list-of-lowerables?
77 (list-of lowerable?))
78
79(define list-of-symbols?
80 (list-of symbol?))
81
82(define-maybe/no-serialization string)
83(define-maybe/no-serialization number)
84(define-maybe/no-serialization symbol)
85(define-maybe/no-serialization list-of-symbols)
86
87(define-configuration/no-serialization restic-backup-job
88 (restic
89 (package restic)
90 "The restic package to be used for the current job.")
91 (user
92 (string "root")
93 "The user used for running the current job.")
94 (group
95 (string "root")
96 "The group used for running the current job.")
97 (log-file
98 (maybe-string)
99 "The file system path to the log file for this job. By default the file will
100have be @file{/var/log/restic-backup/@var{job-name}.log}, where @var{job-name} is the
101name defined in the @code{name} field. For Guix Home services it defaults to
102@file{$XDG_STATE_HOME/shepherd/restic-backup/@var{job-name}.log}.")
103 (max-duration
104 (maybe-number)
105 "The maximum duration in seconds that a job may last. Past
106@code{max-duration} seconds, the job is forcefully terminated.")
107 (wait-for-termination?
108 (boolean #f)
109 "Wait until the job has finished before considering executing it again;
110otherwise, perform it strictly on every occurrence of event, at the risk of
111having multiple instances running concurrently.")
112 (name
113 (string)
114 "A string denoting a name for this job.")
115 (repository
116 (string)
117 "The restic repository target of this job.")
118 (password-file
119 (string)
120 "Name of the password file, readable by the configured @code{user}, that
121will be used to set the @code{RESTIC_PASSWORD} environment variable for the
122current job.")
123 (schedule
124 (gexp-or-string)
125 "A string or a gexp representing the frequency of the backup. Gexp must
126evaluate to @code{calendar-event} records or to strings. Strings must contain
127Vixie cron date lines.")
128 (requirement
129 (maybe-list-of-symbols)
130 "The list of Shepherd services that this backup job depends upon. When unset it
131defaults to @code{'()}, for Guix Home. Otherwise to
132@code{'(user-processes file-systems)}.")
133 (files
134 (list-of-lowerables '())
135 "The list of files or directories to be backed up. It must be a list of
136values that can be lowered to strings.")
137 (verbose?
138 (boolean #f)
139 "Whether to enable verbose output for the current backup job.")
140 (extra-flags
141 (list-of-lowerables '())
142 "A list of values that are lowered to strings. These will be passed as
143command-line arguments to the current job @command{restic backup} invocation."))
144
145;; (for-home (restic-backup-configuration ...)) is not able to replace for-home? with #t,
146;; pk prints #f. Once for-home will be able to work with (gnu services configuration) the
147;; record can be migrated back to define-configuration.
148(define-record-type* <restic-backup-configuration>
149 restic-backup-configuration
150 make-restic-backup-configuration
151 restic-backup-configuration?
152 this-restic-backup-configuration
153
154 (jobs restic-backup-configuration-jobs (default '())) ; list of restic-backup-job
155 (home-service? restic-backup-configuration-home-service?
156 (default for-home?) (innate)))
157
158(define (lower-restic-backup-job config)
159 (let ((restic
160 (file-append (restic-backup-job-restic config) "/bin/restic"))
161 (repository
162 (restic-backup-job-repository config))
163 (password-file
164 (restic-backup-job-password-file config))
165 (files
166 (restic-backup-job-files config))
167 (extra-flags
168 (restic-backup-job-extra-flags config))
169 (verbose?
170 (if (restic-backup-job-verbose? config)
171 '("--verbose")
172 '())))
173 #~(list (list #$@files) #$restic #$repository #$password-file
174 (list #$@verbose?) (list #$@extra-flags))))
175
176(define restic-program
177 #~(lambda (action action-args job-restic repository password-file verbose? extra-flags)
178 (use-modules (ice-9 format))
179 ;; This can be extended later, i.e. to have a
180 ;; centrally defined restic package.
181 ;; See https://issues.guix.gnu.org/71639
182 (define restic job-restic)
183
184 (define command
185 `(,restic ,@verbose?
186 "-r" ,repository
187 ,@extra-flags
188 ,action ,@action-args))
189
190 (setenv "RESTIC_PASSWORD_FILE" password-file)
191
192 (when (> (length verbose?) 0)
193 (format #t "Running~{ ~a~}~%" command))
194
195 (apply execlp `(,restic ,@command))))
196
197(define (restic-backup-job-program config)
198 (program-file
199 "restic-backup"
200 #~(let ((restic-exec
201 #$restic-program)
202 (job #$(lower-restic-backup-job config)))
203
204 (apply restic-exec `("backup" ,@job)))))
205
206(define (restic-guix jobs)
207 (program-file
208 "restic-guix"
209 #~(begin
210 (use-modules (ice-9 match)
211 (srfi srfi-1))
212
213 (define names '#$(map restic-backup-job-name jobs))
214 (define programs '#$(map restic-backup-job-program jobs))
215
216 (define (get-program name)
217 (define idx
218 (list-index (lambda (n) (string=? n name)) names))
219 (unless idx
220 (error (string-append "Unknown job name " name "\n\n"
221 "Possible job names are: "
222 (string-join names " "))))
223 (list-ref programs idx))
224
225 (define (backup args)
226 (define name (third args))
227 (define program (get-program name))
228 (execlp program program))
229
230 (define (validate-args args)
231 (when (not (>= (length args) 3))
232 (error (string-append "Usage: " (basename (car args))
233 " backup NAME"))))
234
235 (define (main args)
236 (validate-args args)
237 (define action (second args))
238 (match action
239 ("backup"
240 (backup args))
241 (_
242 (error (string-append "Unknown action: " action)))))
243
244 (main (command-line)))))
245
246(define* (restic-job-log-file job #:key (home-service? #f))
247 (let ((name (restic-backup-job-name job))
248 (log-file (restic-backup-job-log-file job)))
249 (if (maybe-value-set? log-file)
250 log-file
251 (if home-service?
252 #~(begin
253 (use-modules (shepherd support))
254 (string-append %user-log-dir "/restic-backup/" #$name ".log"))
255 (string-append "/var/log/restic-backup/" name ".log")))))
256
257(define* (restic-backup-job-command name files #:key (home-service? #f))
258 (if home-service?
259 #~(list
260 "restic-guix" "backup" #$name)
261 ;; We go through bash, instead of executing
262 ;; restic-guix directly, because the login shell
263 ;; gives us the correct user environment that some
264 ;; backends require, such as rclone.
265 #~(list
266 (string-append #$bash-minimal "/bin/bash")
267 "-l" "-c"
268 (string-append "restic-guix backup " #$name))))
269
270(define* (restic-job-requirement config #:key (home-service? #f))
271 (define maybe-requirement (restic-backup-job-requirement config))
272 (if (maybe-value-set? maybe-requirement)
273 maybe-requirement
274 (if home-service?
275 '()
276 '(user-processes file-systems))))
277
278(define* (restic-backup-job-modules #:key (home-service? #f))
279 `((shepherd service timer)
280 ,@(if home-service?
281 ;;for %user-log-dir
282 '((shepherd support))
283 '())))
284
285(define* (restic-backup-job->shepherd-service config #:key (home-service? #f))
286 (let ((schedule (restic-backup-job-schedule config))
287 (name (restic-backup-job-name config))
288 (files (restic-backup-job-files config))
289 (user (restic-backup-job-user config))
290 (group (restic-backup-job-group config))
291 (max-duration (restic-backup-job-max-duration config))
292 (wait-for-termination? (restic-backup-job-wait-for-termination? config))
293 (log-file (restic-job-log-file
294 config #:home-service? home-service?))
295 (requirement
296 (restic-job-requirement config #:home-service? home-service?)))
297 (shepherd-service (provision `(,(string->symbol name)))
298 (requirement requirement)
299 (documentation
300 "Run restic backed backups on a regular basis.")
301 (modules (restic-backup-job-modules
302 #:home-service? home-service?))
303 (start
304 #~(make-timer-constructor
305 (if (string? #$schedule)
306 (cron-string->calendar-event #$schedule)
307 #$schedule)
308 (command
309 #$(restic-backup-job-command
310 name files #:home-service? home-service?)
311 #$@(if home-service? '() (list #:user user))
312 #$@(if home-service? '() (list #:group group))
313 #$@(if home-service? '()
314 (list
315 #:environment-variables
316 #~(list
317 (string-append
318 "HOME=" (passwd:dir (getpwnam #$user)))))))
319 #:log-file #$log-file
320 #:wait-for-termination? #$wait-for-termination?
321 #:max-duration #$(and (maybe-value-set? max-duration)
322 max-duration)))
323 (stop
324 #~(make-timer-destructor))
325 (actions (list (shepherd-action
326 (inherit shepherd-trigger-action)
327 (documentation "Manually trigger a backup,
328without waiting for the scheduled time.")))))))
329
330(define (restic-guix-wrapper-package jobs)
331 (package
332 (name "restic-backup-service-wrapper")
333 (version "0.0.0")
334 (source (restic-guix jobs))
335 (build-system copy-build-system)
336 (arguments
337 (list #:install-plan #~'(("./" "/bin"))))
338 (home-page "https://restic.net")
339 (synopsis
340 "Easily interact from the CLI with Guix configured backups")
341 (description
342 "This package provides a simple wrapper around @code{restic}, handled
343by the @code{restic-backup-service-type}. It allows for easily interacting
344with Guix configured backup jobs, for example for manually triggering a backup
345without waiting for the scheduled job to run.")
346 (license license:gpl3+)))
347
348(define restic-backup-service-profile
349 (lambda (config)
350 (define jobs (restic-backup-configuration-jobs config))
351 (if (> (length jobs) 0)
352 (list
353 (restic-guix-wrapper-package jobs))
354 '())))
355
356(define restic-backup-service-type
357 (service-type (name 'restic-backup)
358 (extensions
359 (list
360 (service-extension profile-service-type
361 restic-backup-service-profile)
362 (service-extension shepherd-root-service-type
363 (match-record-lambda <restic-backup-configuration>
364 (jobs home-service?)
365 (map (lambda (job)
366 (restic-backup-job->shepherd-service
367 job #:home-service? home-service?))
368 jobs)))))
369 (compose concatenate)
370 (extend
371 (lambda (config jobs)
372 (restic-backup-configuration
373 (inherit config)
374 (jobs (append (restic-backup-configuration-jobs config)
375 jobs)))))
376 (default-value (restic-backup-configuration))
377 (description
378 "This service configures Shepherd timers for running backups
379with restic.")))