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>
This commit is contained in:
Giacomo Leidi 2025-05-17 17:09:54 +02:00 committed by Ludovic Courtès
parent 86022e994e
commit 1220d1a84e
No known key found for this signature in database
GPG key ID: 090B11993D9AEBB5
4 changed files with 242 additions and 64 deletions

View file

@ -466,6 +466,7 @@ Home Services
* GPG: GNU Privacy Guard. Setting up GPG and related tools. * GPG: GNU Privacy Guard. Setting up GPG and related tools.
* Desktop: Desktop Home Services. Services for graphical environments. * Desktop: Desktop Home Services. Services for graphical environments.
* Guix: Guix Home Services. Services for Guix. * Guix: Guix Home Services. Services for Guix.
* Backup: Backup Home Services. Services for backing up User's files.
* Fonts: Fonts Home Services. Services for managing User's fonts. * Fonts: Fonts Home Services. Services for managing User's fonts.
* Sound: Sound Home Services. Dealing with audio. * Sound: Sound Home Services. Dealing with audio.
* Mail: Mail Home Services. Services for managing mail. * Mail: Mail Home Services. Services for managing mail.
@ -44895,7 +44896,8 @@ The group used for running the current job.
@item @code{log-file} (type: maybe-string) @item @code{log-file} (type: maybe-string)
The file system path to the log file for this job. By default the file will The file system path to the log file for this job. By default the file will
have be @file{/var/log/restic-backup/@var{job-name}.log}, where @var{job-name} is the have be @file{/var/log/restic-backup/@var{job-name}.log}, where @var{job-name} is the
name defined in the @code{name} field. name defined in the @code{name} field. For Guix Home services it defaults to
@file{$XDG_STATE_HOME/shepherd/restic-backup/@var{job-name}.log}.
@item @code{max-duration} (type: maybe-number) @item @code{max-duration} (type: maybe-number)
The maximum duration in seconds that a job may last. Past The 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
evaluate to @code{calendar-event} records or to strings. Strings must contain evaluate to @code{calendar-event} records or to strings. Strings must contain
Vixie cron date lines. Vixie cron date lines.
@item @code{requirement} (default: @code{'()}) (type: list-of-symbols) @item @code{requirement} (type: maybe-list-of-symbols)
The list of Shepherd services that this backup job depends upon. The list of Shepherd services that this backup job depends upon. When unset it
defaults to @code{'()}, for Guix Home. Otherwise to
@code{'(user-processes file-systems)}.
@item @code{files} (default: @code{'()}) (type: list-of-lowerables) @item @code{files} (default: @code{'()}) (type: list-of-lowerables)
The list of files or directories to be backed up. It must be a list of The list of files or directories to be backed up. It must be a list of
@ -48824,6 +48828,7 @@ services)}.
* GPG: GNU Privacy Guard. Setting up GPG and related tools. * GPG: GNU Privacy Guard. Setting up GPG and related tools.
* Desktop: Desktop Home Services. Services for graphical environments. * Desktop: Desktop Home Services. Services for graphical environments.
* Guix: Guix Home Services. Services for Guix. * Guix: Guix Home Services. Services for Guix.
* Backup: Backup Home Services. Services for backing up User's files.
* Fonts: Fonts Home Services. Services for managing User's fonts. * Fonts: Fonts Home Services. Services for managing User's fonts.
* Sound: Sound Home Services. Dealing with audio. * Sound: Sound Home Services. Dealing with audio.
* Mail: Mail Home Services. Services for managing mail. * Mail: Mail Home Services. Services for managing mail.
@ -50414,6 +50419,77 @@ A typical extension for adding a channel might look like this:
@end lisp @end lisp
@end defvar @end defvar
@node Backup Home Services
@subsection Backup Services
The @code{(gnu home services backup)} module offers services for backing up
file system trees. For now, it provides the @code{home-restic-backup-service-type}.
With @code{home-restic-backup-service-type}, you can periodically back up
directories and files with @uref{https://restic.net/, Restic}, which
supports end-to-end encryption and deduplication. Consider the
following configuration:
@lisp
(use-modules (gnu home services backup) ;for 'restic-backup-job', 'home-restic-backup-service-type'
(gnu packages sync) ;for 'rclone'
@dots{})
(home-environment
(packages (list rclone ;for use by restic
@dots{}))
(services
(list
@dots{}
(simple-service 'backup-jobs
home-restic-backup-service-type
(list (restic-backup-job
(name "remote-ftp")
(repository "rclone:remote-ftp:backup/restic")
(password-file "/home/alice/.restic")
;; Every day at 23.
(schedule "0 23 * * *")
(files '("/home/alice/.restic"
"/home/alice/.config/rclone"
"/home/alice/Pictures"))))))))
@end lisp
In general it is preferrable to extend the @code{home-restic-backup-service-type},
as shown in the example above. This is because it takes care of wrapping everything
with @code{for-home}, which enables the @code{home-restic-backup-service-type} and
@code{restic-backup-service-type} to share the same codebase.
For a custom configuration, wrap your @code{restic-backup-configuration} in
@code{for-home}, as in this example:
@lisp
(use-modules (gnu services) ;for 'for-home'
(gnu services backup) ;for 'restic-backup-job' and 'restic-backup-configuration'
(gnu home services backup) ;for 'home-restic-backup-service-type'
(gnu packages sync) ;for 'rclone'
@dots{})
(home-environment
(packages (list rclone ;for use by restic
@dots{}))
(services
(list
@dots{}
(service home-restic-backup-service-type
(for-home
(restic-backup-configuration
(jobs (list @dots{}))))))))
@end lisp
You can refer to @pxref{Miscellaneous Services,
@code{restic-backup-service-type}} for details about
@code{restic-backup-configuration} and @code{restic-backup-job}.
The only difference is that the @code{home-restic-backup-service-type}
will ignore the @code{user} and @code{group} field of
@code{restic-backup-job}.
@node Fonts Home Services @node Fonts Home Services
@subsection Fonts Home Services @subsection Fonts Home Services

View file

@ -0,0 +1,38 @@
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2025 Giacomo Leidi <goodoldpaul@autistici.org>
;;;
;;; This file is part of GNU Guix.
;;;
;;; GNU Guix is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or (at
;;; your option) any later version.
;;;
;;; GNU Guix is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
(define-module (gnu home services backup)
#:use-module (gnu services)
#:use-module (gnu services backup)
#:use-module (gnu home services)
#:use-module (gnu home services shepherd)
#:export (home-restic-backup-service-type)
#:re-export (restic-backup-configuration
restic-backup-job))
(define home-restic-backup-service-type
(service-type
(inherit (system->home-service-type restic-backup-service-type))
(extend
(lambda (config jobs)
(for-home
(restic-backup-configuration
(inherit config)
(jobs (append (restic-backup-configuration-jobs config)
jobs))))))
(default-value (for-home (restic-backup-configuration)))))

View file

@ -103,6 +103,7 @@ GNU_SYSTEM_MODULES = \
%D%/home.scm \ %D%/home.scm \
%D%/home/services.scm \ %D%/home/services.scm \
%D%/home/services/admin.scm \ %D%/home/services/admin.scm \
%D%/home/services/backup.scm \
%D%/home/services/desktop.scm \ %D%/home/services/desktop.scm \
%D%/home/services/dict.scm \ %D%/home/services/dict.scm \
%D%/home/services/dotfiles.scm \ %D%/home/services/dotfiles.scm \

View file

@ -28,6 +28,7 @@
#:prefix license:) #:prefix license:)
#:use-module (guix modules) #:use-module (guix modules)
#:use-module (guix packages) #:use-module (guix packages)
#:use-module (guix records)
#:use-module (srfi srfi-1) #:use-module (srfi srfi-1)
#:export (restic-backup-job #:export (restic-backup-job
restic-backup-job? restic-backup-job?
@ -47,16 +48,21 @@
restic-backup-job-verbose? restic-backup-job-verbose?
restic-backup-job-extra-flags restic-backup-job-extra-flags
lower-restic-backup-job
restic-backup-configuration restic-backup-configuration
restic-backup-configuration? restic-backup-configuration?
restic-backup-configuration-fields
restic-backup-configuration-jobs restic-backup-configuration-jobs
restic-backup-job-program restic-backup-job-program
restic-backup-job->mcron-job restic-backup-job->shepherd-service
restic-guix restic-guix
restic-guix-wrapper-package restic-guix-wrapper-package
restic-backup-service-profile restic-backup-service-profile
restic-program
restic-job-log-file
restic-backup-job-command
restic-backup-job-modules
restic-backup-service-type)) restic-backup-service-type))
(define (gexp-or-string? value) (define (gexp-or-string? value)
@ -75,6 +81,8 @@
(define-maybe/no-serialization string) (define-maybe/no-serialization string)
(define-maybe/no-serialization number) (define-maybe/no-serialization number)
(define-maybe/no-serialization symbol)
(define-maybe/no-serialization list-of-symbols)
(define-configuration/no-serialization restic-backup-job (define-configuration/no-serialization restic-backup-job
(restic (restic
@ -90,7 +98,8 @@
(maybe-string) (maybe-string)
"The file system path to the log file for this job. By default the file will "The file system path to the log file for this job. By default the file will
have be @file{/var/log/restic-backup/@var{job-name}.log}, where @var{job-name} is the have be @file{/var/log/restic-backup/@var{job-name}.log}, where @var{job-name} is the
name defined in the @code{name} field.") name defined in the @code{name} field. For Guix Home services it defaults to
@file{$XDG_STATE_HOME/shepherd/restic-backup/@var{job-name}.log}.")
(max-duration (max-duration
(maybe-number) (maybe-number)
"The maximum duration in seconds that a job may last. Past "The maximum duration in seconds that a job may last. Past
@ -117,8 +126,10 @@ current job.")
evaluate to @code{calendar-event} records or to strings. Strings must contain evaluate to @code{calendar-event} records or to strings. Strings must contain
Vixie cron date lines.") Vixie cron date lines.")
(requirement (requirement
(list-of-symbols '()) (maybe-list-of-symbols)
"The list of Shepherd services that this backup job depends upon.") "The list of Shepherd services that this backup job depends upon. When unset it
defaults to @code{'()}, for Guix Home. Otherwise to
@code{'(user-processes file-systems)}.")
(files (files
(list-of-lowerables '()) (list-of-lowerables '())
"The list of files or directories to be backed up. It must be a list of "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.")
"A list of values that are lowered to strings. These will be passed as "A list of values that are lowered to strings. These will be passed as
command-line arguments to the current job @command{restic backup} invocation.")) command-line arguments to the current job @command{restic backup} invocation."))
(define list-of-restic-backup-jobs? ;; (for-home (restic-backup-configuration ...)) is not able to replace for-home? with #t,
(list-of restic-backup-job?)) ;; pk prints #f. Once for-home will be able to work with (gnu services configuration) the
;; record can be migrated back to define-configuration.
(define-record-type* <restic-backup-configuration>
restic-backup-configuration
make-restic-backup-configuration
restic-backup-configuration?
this-restic-backup-configuration
(define-configuration/no-serialization restic-backup-configuration (jobs restic-backup-configuration-jobs (default '())) ; list of restic-backup-job
(jobs (home-service? restic-backup-configuration-home-service?
(list-of-restic-backup-jobs '()) (default for-home?) (innate)))
"The list of backup jobs for the current system."))
(define (restic-backup-job-program config) (define (lower-restic-backup-job config)
(let ((restic (let ((restic
(file-append (restic-backup-job-restic config) "/bin/restic")) (file-append (restic-backup-job-restic config) "/bin/restic"))
(repository (repository
@ -150,22 +166,42 @@ command-line arguments to the current job @command{restic backup} invocation."))
(restic-backup-job-files config)) (restic-backup-job-files config))
(extra-flags (extra-flags
(restic-backup-job-extra-flags config)) (restic-backup-job-extra-flags config))
(verbose (verbose?
(if (restic-backup-job-verbose? config) (if (restic-backup-job-verbose? config)
'("--verbose") '("--verbose")
'()))) '())))
(program-file #~(list (list #$@files) #$restic #$repository #$password-file
"restic-backup-job.scm" (list #$@verbose?) (list #$@extra-flags))))
#~(begin
(use-modules (ice-9 popen)
(ice-9 rdelim))
(setenv "RESTIC_PASSWORD"
(with-input-from-file #$password-file read-line))
(execlp #$restic #$restic #$@verbose (define restic-program
"-r" #$repository #~(lambda (action action-args job-restic repository password-file verbose? extra-flags)
#$@extra-flags (use-modules (ice-9 format))
"backup" #$@files))))) ;; This can be extended later, i.e. to have a
;; centrally defined restic package.
;; See https://issues.guix.gnu.org/71639
(define restic job-restic)
(define command
`(,restic ,@verbose?
"-r" ,repository
,@extra-flags
,action ,@action-args))
(setenv "RESTIC_PASSWORD_FILE" password-file)
(when (> (length verbose?) 0)
(format #t "Running~{ ~a~}~%" command))
(apply execlp `(,restic ,@command))))
(define (restic-backup-job-program config)
(program-file
"restic-backup"
#~(let ((restic-exec
#$restic-program)
(job #$(lower-restic-backup-job config)))
(apply restic-exec `("backup" ,@job)))))
(define (restic-guix jobs) (define (restic-guix jobs)
(program-file (program-file
@ -207,55 +243,89 @@ command-line arguments to the current job @command{restic backup} invocation."))
(main (command-line))))) (main (command-line)))))
(define (restic-job-log-file job) (define* (restic-job-log-file job #:key (home-service? #f))
(let ((name (restic-backup-job-name job)) (let ((name (restic-backup-job-name job))
(log-file (restic-backup-job-log-file job))) (log-file (restic-backup-job-log-file job)))
(if (maybe-value-set? log-file) (if (maybe-value-set? log-file)
log-file log-file
(string-append "/var/log/restic-backup/" name ".log")))) (if home-service?
#~(begin
(use-modules (shepherd support))
(string-append %user-log-dir "/restic-backup/" #$name ".log"))
(string-append "/var/log/restic-backup/" name ".log")))))
(define (restic-backup-job->shepherd-service config) (define* (restic-backup-job-command name files #:key (home-service? #f))
(if home-service?
#~(list
"restic-guix" "backup" #$name)
;; We go through bash, instead of executing
;; restic-guix directly, because the login shell
;; gives us the correct user environment that some
;; backends require, such as rclone.
#~(list
(string-append #$bash-minimal "/bin/bash")
"-l" "-c"
(string-append "restic-guix backup " #$name))))
(define* (restic-job-requirement config #:key (home-service? #f))
(define maybe-requirement (restic-backup-job-requirement config))
(if (maybe-value-set? maybe-requirement)
maybe-requirement
(if home-service?
'()
'(user-processes file-systems))))
(define* (restic-backup-job-modules #:key (home-service? #f))
`((shepherd service timer)
,@(if home-service?
;;for %user-log-dir
'((shepherd support))
'())))
(define* (restic-backup-job->shepherd-service config #:key (home-service? #f))
(let ((schedule (restic-backup-job-schedule config)) (let ((schedule (restic-backup-job-schedule config))
(name (restic-backup-job-name config)) (name (restic-backup-job-name config))
(files (restic-backup-job-files config))
(user (restic-backup-job-user config)) (user (restic-backup-job-user config))
(group (restic-backup-job-group config)) (group (restic-backup-job-group config))
(max-duration (restic-backup-job-max-duration config)) (max-duration (restic-backup-job-max-duration config))
(wait-for-termination? (restic-backup-job-wait-for-termination? config)) (wait-for-termination? (restic-backup-job-wait-for-termination? config))
(log-file (restic-job-log-file config)) (log-file (restic-job-log-file
(requirement (restic-backup-job-requirement config))) config #:home-service? home-service?))
(requirement
(restic-job-requirement config #:home-service? home-service?)))
(shepherd-service (provision `(,(string->symbol name))) (shepherd-service (provision `(,(string->symbol name)))
(requirement (requirement requirement)
`(user-processes file-systems ,@requirement))
(documentation (documentation
"Run @code{restic} backed backups on a regular basis.") "Run restic backed backups on a regular basis.")
(modules '((shepherd service timer))) (modules (restic-backup-job-modules
#:home-service? home-service?))
(start (start
#~(make-timer-constructor #~(make-timer-constructor
(if (string? #$schedule) (if (string? #$schedule)
(cron-string->calendar-event #$schedule) (cron-string->calendar-event #$schedule)
#$schedule) #$schedule)
(command (command
(list #$(restic-backup-job-command
;; We go through bash, instead of executing name files #:home-service? home-service?)
;; restic-guix directly, because the login shell #$@(if home-service? '() (list #:user user))
;; gives us the correct user environment that some #$@(if home-service? '() (list #:group group))
;; backends require, such as rclone. #$@(if home-service? '()
(string-append #+bash-minimal "/bin/bash") (list
"-l" "-c" #:environment-variables
(string-append "restic-guix backup " #$name)) #~(list
#:user #$user (string-append
#:group #$group "HOME=" (passwd:dir (getpwnam #$user)))))))
#:environment-variables
(list
(string-append
"HOME=" (passwd:dir (getpwnam #$user)))))
#:log-file #$log-file #:log-file #$log-file
#:wait-for-termination? #$wait-for-termination? #:wait-for-termination? #$wait-for-termination?
#:max-duration #$(and (maybe-value-set? max-duration) #:max-duration #$(and (maybe-value-set? max-duration)
max-duration))) max-duration)))
(stop (stop
#~(make-timer-destructor)) #~(make-timer-destructor))
(actions (list shepherd-trigger-action))))) (actions (list (shepherd-action
(inherit shepherd-trigger-action)
(documentation "Manually trigger a backup,
without waiting for the scheduled time.")))))))
(define (restic-guix-wrapper-package jobs) (define (restic-guix-wrapper-package jobs)
(package (package
@ -283,26 +353,19 @@ without waiting for the scheduled job to run.")
(restic-guix-wrapper-package jobs)) (restic-guix-wrapper-package jobs))
'()))) '())))
(define (restic-backup-activation config)
#~(for-each
(lambda (log-file)
(mkdir-p (dirname log-file)))
(list #$@(map restic-job-log-file
(restic-backup-configuration-jobs config)))))
(define restic-backup-service-type (define restic-backup-service-type
(service-type (name 'restic-backup) (service-type (name 'restic-backup)
(extensions (extensions
(list (list
(service-extension activation-service-type
restic-backup-activation)
(service-extension profile-service-type (service-extension profile-service-type
restic-backup-service-profile) restic-backup-service-profile)
(service-extension shepherd-root-service-type (service-extension shepherd-root-service-type
(lambda (config) (match-record-lambda <restic-backup-configuration>
(map restic-backup-job->shepherd-service (jobs home-service?)
(restic-backup-configuration-jobs (map (lambda (job)
config)))))) (restic-backup-job->shepherd-service
job #:home-service? home-service?))
jobs)))))
(compose concatenate) (compose concatenate)
(extend (extend
(lambda (config jobs) (lambda (config jobs)