| 1 | |
| 2 | |
| 3 | |
| 4 | |
| 5 | |
| 6 | |
| 7 | |
| 8 | |
| 9 | |
| 10 | |
| 11 | |
| 12 | |
| 13 | |
| 14 | |
| 15 | |
| 16 | |
| 17 | |
| 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
|
| 100 | have be @file{/var/log/restic-backup/@var{job-name}.log}, where @var{job-name} is the
|
| 101 | name 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;
|
| 110 | otherwise, perform it strictly on every occurrence of event, at the risk of
|
| 111 | having 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
|
| 121 | will be used to set the @code{RESTIC_PASSWORD} environment variable for the
|
| 122 | current job.")
|
| 123 | (schedule
|
| 124 | (gexp-or-string)
|
| 125 | "A string or a gexp representing the frequency of the backup. Gexp must
|
| 126 | evaluate to @code{calendar-event} records or to strings. Strings must contain
|
| 127 | Vixie 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
|
| 131 | defaults 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
|
| 136 | values 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
|
| 143 | command-line arguments to the current job @command{restic backup} invocation."))
|
| 144 |
|
| 145 | |
| 146 | |
| 147 | |
| 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 '())) |
| 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 | |
| 180 | |
| 181 | |
| 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 | |
| 262 | |
| 263 | |
| 264 | |
| 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 | |
| 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,
|
| 328 | without 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
|
| 343 | by the @code{restic-backup-service-type}. It allows for easily interacting
|
| 344 | with Guix configured backup jobs, for example for manually triggering a backup
|
| 345 | without 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
|
| 379 | with restic.")))
|