summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGiacomo Leidi <goodoldpaul@autistici.org>2025-05-17 17:09:54 +0200
committerLudovic Courtès <ludo@gnu.org>2025-05-18 22:55:53 +0200
commit1220d1a84eaaa8a60a457fa378cd986ec2269c08 (patch)
tree83fd23a403c792646b7afc0a2a9f380cd1b986c5
parent86022e994e5fcb3918f2d3d2f6f89b24c5562910 (diff)
home: Add home-restic-backup service.
* gnu/services/backup.scm: Drop mcron obsolete export. (restic-backup-job-program): Generalize to restic-program. (lower-restic-backup-job): New procedure implementing a standard way to lower restic-backup-job records into lists. (restic-program): Implement general way to run restic commands, for example to initialize repositories. (restic-backup-configuration): Reimplement with (guix records). (restic-backup-job-{logfile,command,requirement,modules}): Add new procedures and add support for Guix Home environments. (restic-backup-job->shepherd-service): Add support for Guix Home environments. (restic-backup-service-activation): Drop procedure as now the Shepherd takes care of creating timers log file directories. (restic-backup-service-type): Drop profile and activation services extensions. * gnu/home/services/backup.scm: New file. * gnu/local.mk: Add this. * doc/guix.texi: Document this. Change-Id: Ied1c0a5756b715fba176a0e42ea154246089e6be Signed-off-by: Ludovic Courtès <ludo@gnu.org>
-rw-r--r--doc/guix.texi82
-rw-r--r--gnu/home/services/backup.scm38
-rw-r--r--gnu/local.mk1
-rw-r--r--gnu/services/backup.scm187
4 files changed, 243 insertions, 65 deletions
diff --git a/doc/guix.texi b/doc/guix.texi
index 8e73685a216..98a9ed573aa 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -466,6 +466,7 @@ Home Services
466* GPG: GNU Privacy Guard. Setting up GPG and related tools. 466* GPG: GNU Privacy Guard. Setting up GPG and related tools.
467* Desktop: Desktop Home Services. Services for graphical environments. 467* Desktop: Desktop Home Services. Services for graphical environments.
468* Guix: Guix Home Services. Services for Guix. 468* Guix: Guix Home Services. Services for Guix.
469* Backup: Backup Home Services. Services for backing up User's files.
469* Fonts: Fonts Home Services. Services for managing User's fonts. 470* Fonts: Fonts Home Services. Services for managing User's fonts.
470* Sound: Sound Home Services. Dealing with audio. 471* Sound: Sound Home Services. Dealing with audio.
471* Mail: Mail Home Services. Services for managing mail. 472* Mail: Mail Home Services. Services for managing mail.
@@ -44895,7 +44896,8 @@ The group used for running the current job.
44895@item @code{log-file} (type: maybe-string) 44896@item @code{log-file} (type: maybe-string)
44896The file system path to the log file for this job. By default the file will 44897The file system path to the log file for this job. By default the file will
44897have be @file{/var/log/restic-backup/@var{job-name}.log}, where @var{job-name} is the 44898have be @file{/var/log/restic-backup/@var{job-name}.log}, where @var{job-name} is the
44898name defined in the @code{name} field. 44899name defined in the @code{name} field. For Guix Home services it defaults to
44900@file{$XDG_STATE_HOME/shepherd/restic-backup/@var{job-name}.log}.
44899 44901
44900@item @code{max-duration} (type: maybe-number) 44902@item @code{max-duration} (type: maybe-number)
44901The maximum duration in seconds that a job may last. Past 44903The maximum duration in seconds that a job may last. Past
@@ -44922,8 +44924,10 @@ A string or a gexp representing the frequency of the backup. Gexp must
44922evaluate to @code{calendar-event} records or to strings. Strings must contain 44924evaluate to @code{calendar-event} records or to strings. Strings must contain
44923Vixie cron date lines. 44925Vixie cron date lines.
44924 44926
44925@item @code{requirement} (default: @code{'()}) (type: list-of-symbols) 44927@item @code{requirement} (type: maybe-list-of-symbols)
44926The list of Shepherd services that this backup job depends upon. 44928The list of Shepherd services that this backup job depends upon. When unset it
44929defaults to @code{'()}, for Guix Home. Otherwise to
44930@code{'(user-processes file-systems)}.
44927 44931
44928@item @code{files} (default: @code{'()}) (type: list-of-lowerables) 44932@item @code{files} (default: @code{'()}) (type: list-of-lowerables)
44929The list of files or directories to be backed up. It must be a list of 44933The list of files or directories to be backed up. It must be a list of
@@ -48824,6 +48828,7 @@ services)}.
48824* GPG: GNU Privacy Guard. Setting up GPG and related tools. 48828* GPG: GNU Privacy Guard. Setting up GPG and related tools.
48825* Desktop: Desktop Home Services. Services for graphical environments. 48829* Desktop: Desktop Home Services. Services for graphical environments.
48826* Guix: Guix Home Services. Services for Guix. 48830* Guix: Guix Home Services. Services for Guix.
48831* Backup: Backup Home Services. Services for backing up User's files.
48827* Fonts: Fonts Home Services. Services for managing User's fonts. 48832* Fonts: Fonts Home Services. Services for managing User's fonts.
48828* Sound: Sound Home Services. Dealing with audio. 48833* Sound: Sound Home Services. Dealing with audio.
48829* Mail: Mail Home Services. Services for managing mail. 48834* Mail: Mail Home Services. Services for managing mail.
@@ -50414,6 +50419,77 @@ A typical extension for adding a channel might look like this:
50414@end lisp 50419@end lisp
50415@end defvar 50420@end defvar
50416 50421
50422@node Backup Home Services
50423@subsection Backup Services
50424
50425The @code{(gnu home services backup)} module offers services for backing up
50426file system trees. For now, it provides the @code{home-restic-backup-service-type}.
50427
50428With @code{home-restic-backup-service-type}, you can periodically back up
50429directories and files with @uref{https://restic.net/, Restic}, which
50430supports end-to-end encryption and deduplication. Consider the
50431following configuration:
50432
50433@lisp
50434(use-modules (gnu home services backup) ;for 'restic-backup-job', 'home-restic-backup-service-type'
50435 (gnu packages sync) ;for 'rclone'
50436 @dots{})
50437
50438(home-environment
50439
50440 (packages (list rclone ;for use by restic
50441 @dots{}))
50442 (services
50443 (list
50444 @dots{}
50445 (simple-service 'backup-jobs
50446 home-restic-backup-service-type
50447 (list (restic-backup-job
50448 (name "remote-ftp")
50449 (repository "rclone:remote-ftp:backup/restic")
50450 (password-file "/home/alice/.restic")
50451 ;; Every day at 23.
50452 (schedule "0 23 * * *")
50453 (files '("/home/alice/.restic"
50454 "/home/alice/.config/rclone"
50455 "/home/alice/Pictures"))))))))
50456@end lisp
50457
50458In general it is preferrable to extend the @code{home-restic-backup-service-type},
50459as shown in the example above. This is because it takes care of wrapping everything
50460with @code{for-home}, which enables the @code{home-restic-backup-service-type} and
50461@code{restic-backup-service-type} to share the same codebase.
50462
50463For a custom configuration, wrap your @code{restic-backup-configuration} in
50464@code{for-home}, as in this example:
50465
50466@lisp
50467(use-modules (gnu services) ;for 'for-home'
50468 (gnu services backup) ;for 'restic-backup-job' and 'restic-backup-configuration'
50469 (gnu home services backup) ;for 'home-restic-backup-service-type'
50470 (gnu packages sync) ;for 'rclone'
50471 @dots{})
50472
50473(home-environment
50474
50475 (packages (list rclone ;for use by restic
50476 @dots{}))
50477 (services
50478 (list
50479 @dots{}
50480 (service home-restic-backup-service-type
50481 (for-home
50482 (restic-backup-configuration
50483 (jobs (list @dots{}))))))))
50484@end lisp
50485
50486You can refer to @pxref{Miscellaneous Services,
50487@code{restic-backup-service-type}} for details about
50488@code{restic-backup-configuration} and @code{restic-backup-job}.
50489The only difference is that the @code{home-restic-backup-service-type}
50490will ignore the @code{user} and @code{group} field of
50491@code{restic-backup-job}.
50492
50417@node Fonts Home Services 50493@node Fonts Home Services
50418@subsection Fonts Home Services 50494@subsection Fonts Home Services
50419 50495
diff --git a/gnu/home/services/backup.scm b/gnu/home/services/backup.scm
new file mode 100644
index 00000000000..ac977f835b6
--- /dev/null
+++ b/gnu/home/services/backup.scm
@@ -0,0 +1,38 @@
1;;; GNU Guix --- Functional package management for GNU
2;;; Copyright © 2025 Giacomo Leidi <goodoldpaul@autistici.org>
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 home services backup)
20 #:use-module (gnu services)
21 #:use-module (gnu services backup)
22 #:use-module (gnu home services)
23 #:use-module (gnu home services shepherd)
24 #:export (home-restic-backup-service-type)
25 #:re-export (restic-backup-configuration
26 restic-backup-job))
27
28(define home-restic-backup-service-type
29 (service-type
30 (inherit (system->home-service-type restic-backup-service-type))
31 (extend
32 (lambda (config jobs)
33 (for-home
34 (restic-backup-configuration
35 (inherit config)
36 (jobs (append (restic-backup-configuration-jobs config)
37 jobs))))))
38 (default-value (for-home (restic-backup-configuration)))))
diff --git a/gnu/local.mk b/gnu/local.mk
index dfafe8b8953..7311c33f0f6 100644
--- a/gnu/local.mk
+++ b/gnu/local.mk
@@ -103,6 +103,7 @@ GNU_SYSTEM_MODULES = \
103 %D%/home.scm \ 103 %D%/home.scm \
104 %D%/home/services.scm \ 104 %D%/home/services.scm \
105 %D%/home/services/admin.scm \ 105 %D%/home/services/admin.scm \
106 %D%/home/services/backup.scm \
106 %D%/home/services/desktop.scm \ 107 %D%/home/services/desktop.scm \
107 %D%/home/services/dict.scm \ 108 %D%/home/services/dict.scm \
108 %D%/home/services/dotfiles.scm \ 109 %D%/home/services/dotfiles.scm \
diff --git a/gnu/services/backup.scm b/gnu/services/backup.scm
index 6e066bd3d66..8fdf9ce902f 100644
--- a/gnu/services/backup.scm
+++ b/gnu/services/backup.scm
@@ -28,6 +28,7 @@
28 #:prefix license:) 28 #:prefix license:)
29 #:use-module (guix modules) 29 #:use-module (guix modules)
30 #:use-module (guix packages) 30 #:use-module (guix packages)
31 #:use-module (guix records)
31 #:use-module (srfi srfi-1) 32 #:use-module (srfi srfi-1)
32 #:export (restic-backup-job 33 #:export (restic-backup-job
33 restic-backup-job? 34 restic-backup-job?
@@ -47,16 +48,21 @@
47 restic-backup-job-verbose? 48 restic-backup-job-verbose?
48 restic-backup-job-extra-flags 49 restic-backup-job-extra-flags
49 50
51 lower-restic-backup-job
52
50 restic-backup-configuration 53 restic-backup-configuration
51 restic-backup-configuration? 54 restic-backup-configuration?
52 restic-backup-configuration-fields
53 restic-backup-configuration-jobs 55 restic-backup-configuration-jobs
54 56
55 restic-backup-job-program 57 restic-backup-job-program
56 restic-backup-job->mcron-job 58 restic-backup-job->shepherd-service
57 restic-guix 59 restic-guix
58 restic-guix-wrapper-package 60 restic-guix-wrapper-package
59 restic-backup-service-profile 61 restic-backup-service-profile
62 restic-program
63 restic-job-log-file
64 restic-backup-job-command
65 restic-backup-job-modules
60 restic-backup-service-type)) 66 restic-backup-service-type))
61 67
62(define (gexp-or-string? value) 68(define (gexp-or-string? value)
@@ -75,6 +81,8 @@
75 81
76(define-maybe/no-serialization string) 82(define-maybe/no-serialization string)
77(define-maybe/no-serialization number) 83(define-maybe/no-serialization number)
84(define-maybe/no-serialization symbol)
85(define-maybe/no-serialization list-of-symbols)
78 86
79(define-configuration/no-serialization restic-backup-job 87(define-configuration/no-serialization restic-backup-job
80 (restic 88 (restic
@@ -90,7 +98,8 @@
90 (maybe-string) 98 (maybe-string)
91 "The file system path to the log file for this job. By default the file will 99 "The file system path to the log file for this job. By default the file will
92have be @file{/var/log/restic-backup/@var{job-name}.log}, where @var{job-name} is the 100have be @file{/var/log/restic-backup/@var{job-name}.log}, where @var{job-name} is the
93name defined in the @code{name} field.") 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}.")
94 (max-duration 103 (max-duration
95 (maybe-number) 104 (maybe-number)
96 "The maximum duration in seconds that a job may last. Past 105 "The maximum duration in seconds that a job may last. Past
@@ -117,8 +126,10 @@ current job.")
117evaluate to @code{calendar-event} records or to strings. Strings must contain 126evaluate to @code{calendar-event} records or to strings. Strings must contain
118Vixie cron date lines.") 127Vixie cron date lines.")
119 (requirement 128 (requirement
120 (list-of-symbols '()) 129 (maybe-list-of-symbols)
121 "The list of Shepherd services that this backup job depends upon.") 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)}.")
122 (files 133 (files
123 (list-of-lowerables '()) 134 (list-of-lowerables '())
124 "The list of files or directories to be backed up. It must be a list of 135 "The list of files or directories to be backed up. It must be a list of
@@ -131,15 +142,20 @@ values that can be lowered to strings.")
131 "A list of values that are lowered to strings. These will be passed as 142 "A list of values that are lowered to strings. These will be passed as
132command-line arguments to the current job @command{restic backup} invocation.")) 143command-line arguments to the current job @command{restic backup} invocation."))
133 144
134(define list-of-restic-backup-jobs? 145;; (for-home (restic-backup-configuration ...)) is not able to replace for-home? with #t,
135 (list-of restic-backup-job?)) 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
136 153
137(define-configuration/no-serialization restic-backup-configuration 154 (jobs restic-backup-configuration-jobs (default '())) ; list of restic-backup-job
138 (jobs 155 (home-service? restic-backup-configuration-home-service?
139 (list-of-restic-backup-jobs '()) 156 (default for-home?) (innate)))
140 "The list of backup jobs for the current system."))
141 157
142(define (restic-backup-job-program config) 158(define (lower-restic-backup-job config)
143 (let ((restic 159 (let ((restic
144 (file-append (restic-backup-job-restic config) "/bin/restic")) 160 (file-append (restic-backup-job-restic config) "/bin/restic"))
145 (repository 161 (repository
@@ -150,22 +166,42 @@ command-line arguments to the current job @command{restic backup} invocation."))
150 (restic-backup-job-files config)) 166 (restic-backup-job-files config))
151 (extra-flags 167 (extra-flags
152 (restic-backup-job-extra-flags config)) 168 (restic-backup-job-extra-flags config))
153 (verbose 169 (verbose?
154 (if (restic-backup-job-verbose? config) 170 (if (restic-backup-job-verbose? config)
155 '("--verbose") 171 '("--verbose")
156 '()))) 172 '())))
157 (program-file 173 #~(list (list #$@files) #$restic #$repository #$password-file
158 "restic-backup-job.scm" 174 (list #$@verbose?) (list #$@extra-flags))))
159 #~(begin 175
160 (use-modules (ice-9 popen) 176(define restic-program
161 (ice-9 rdelim)) 177 #~(lambda (action action-args job-restic repository password-file verbose? extra-flags)
162 (setenv "RESTIC_PASSWORD" 178 (use-modules (ice-9 format))
163 (with-input-from-file #$password-file read-line)) 179 ;; This can be extended later, i.e. to have a
164 180 ;; centrally defined restic package.
165 (execlp #$restic #$restic #$@verbose 181 ;; See https://issues.guix.gnu.org/71639
166 "-r" #$repository 182 (define restic job-restic)
167 #$@extra-flags 183
168 "backup" #$@files))))) 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)))))
169 205
170(define (restic-guix jobs) 206(define (restic-guix jobs)
171 (program-file 207 (program-file
@@ -207,55 +243,89 @@ command-line arguments to the current job @command{restic backup} invocation."))
207 243
208 (main (command-line))))) 244 (main (command-line)))))
209 245
210(define (restic-job-log-file job) 246(define* (restic-job-log-file job #:key (home-service? #f))
211 (let ((name (restic-backup-job-name job)) 247 (let ((name (restic-backup-job-name job))
212 (log-file (restic-backup-job-log-file job))) 248 (log-file (restic-backup-job-log-file job)))
213 (if (maybe-value-set? log-file) 249 (if (maybe-value-set? log-file)
214 log-file 250 log-file
215 (string-append "/var/log/restic-backup/" name ".log")))) 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))))
216 269
217(define (restic-backup-job->shepherd-service config) 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))
218 (let ((schedule (restic-backup-job-schedule config)) 286 (let ((schedule (restic-backup-job-schedule config))
219 (name (restic-backup-job-name config)) 287 (name (restic-backup-job-name config))
288 (files (restic-backup-job-files config))
220 (user (restic-backup-job-user config)) 289 (user (restic-backup-job-user config))
221 (group (restic-backup-job-group config)) 290 (group (restic-backup-job-group config))
222 (max-duration (restic-backup-job-max-duration config)) 291 (max-duration (restic-backup-job-max-duration config))
223 (wait-for-termination? (restic-backup-job-wait-for-termination? config)) 292 (wait-for-termination? (restic-backup-job-wait-for-termination? config))
224 (log-file (restic-job-log-file config)) 293 (log-file (restic-job-log-file
225 (requirement (restic-backup-job-requirement config))) 294 config #:home-service? home-service?))
295 (requirement
296 (restic-job-requirement config #:home-service? home-service?)))
226 (shepherd-service (provision `(,(string->symbol name))) 297 (shepherd-service (provision `(,(string->symbol name)))
227 (requirement 298 (requirement requirement)
228 `(user-processes file-systems ,@requirement))
229 (documentation 299 (documentation
230 "Run @code{restic} backed backups on a regular basis.") 300 "Run restic backed backups on a regular basis.")
231 (modules '((shepherd service timer))) 301 (modules (restic-backup-job-modules
302 #:home-service? home-service?))
232 (start 303 (start
233 #~(make-timer-constructor 304 #~(make-timer-constructor
234 (if (string? #$schedule) 305 (if (string? #$schedule)
235 (cron-string->calendar-event #$schedule) 306 (cron-string->calendar-event #$schedule)
236 #$schedule) 307 #$schedule)
237 (command 308 (command
238 (list 309 #$(restic-backup-job-command
239 ;; We go through bash, instead of executing 310 name files #:home-service? home-service?)
240 ;; restic-guix directly, because the login shell 311 #$@(if home-service? '() (list #:user user))
241 ;; gives us the correct user environment that some 312 #$@(if home-service? '() (list #:group group))
242 ;; backends require, such as rclone. 313 #$@(if home-service? '()
243 (string-append #+bash-minimal "/bin/bash") 314 (list
244 "-l" "-c" 315 #:environment-variables
245 (string-append "restic-guix backup " #$name)) 316 #~(list
246 #:user #$user 317 (string-append
247 #:group #$group 318 "HOME=" (passwd:dir (getpwnam #$user)))))))
248 #:environment-variables
249 (list
250 (string-append
251 "HOME=" (passwd:dir (getpwnam #$user)))))
252 #:log-file #$log-file 319 #:log-file #$log-file
253 #:wait-for-termination? #$wait-for-termination? 320 #:wait-for-termination? #$wait-for-termination?
254 #:max-duration #$(and (maybe-value-set? max-duration) 321 #:max-duration #$(and (maybe-value-set? max-duration)
255 max-duration))) 322 max-duration)))
256 (stop 323 (stop
257 #~(make-timer-destructor)) 324 #~(make-timer-destructor))
258 (actions (list shepherd-trigger-action))))) 325 (actions (list (shepherd-action
326 (inherit shepherd-trigger-action)
327 (documentation "Manually trigger a backup,
328without waiting for the scheduled time.")))))))
259 329
260(define (restic-guix-wrapper-package jobs) 330(define (restic-guix-wrapper-package jobs)
261 (package 331 (package
@@ -283,26 +353,19 @@ without waiting for the scheduled job to run.")
283 (restic-guix-wrapper-package jobs)) 353 (restic-guix-wrapper-package jobs))
284 '()))) 354 '())))
285 355
286(define (restic-backup-activation config)
287 #~(for-each
288 (lambda (log-file)
289 (mkdir-p (dirname log-file)))
290 (list #$@(map restic-job-log-file
291 (restic-backup-configuration-jobs config)))))
292
293(define restic-backup-service-type 356(define restic-backup-service-type
294 (service-type (name 'restic-backup) 357 (service-type (name 'restic-backup)
295 (extensions 358 (extensions
296 (list 359 (list
297 (service-extension activation-service-type
298 restic-backup-activation)
299 (service-extension profile-service-type 360 (service-extension profile-service-type
300 restic-backup-service-profile) 361 restic-backup-service-profile)
301 (service-extension shepherd-root-service-type 362 (service-extension shepherd-root-service-type
302 (lambda (config) 363 (match-record-lambda <restic-backup-configuration>
303 (map restic-backup-job->shepherd-service 364 (jobs home-service?)
304 (restic-backup-configuration-jobs 365 (map (lambda (job)
305 config)))))) 366 (restic-backup-job->shepherd-service
367 job #:home-service? home-service?))
368 jobs)))))
306 (compose concatenate) 369 (compose concatenate)
307 (extend 370 (extend
308 (lambda (config jobs) 371 (lambda (config jobs)