* gnu/services/certbot.scm (certbot-renewal-one-shot): New procedure. (certbot-service-type)[extensions]: Add it to shepherd-root extension. (certbot-command): Make connection errors return a different exit code. (certbot-activation): Remove message with certificate renewal instructions.
Change-Id: I614ac6214a753dba0396e2385a75926c8355caa1 --- gnu/services/certbot.scm | 77 +++++++++++++++++++++++++++++++++------- 1 file changed, 65 insertions(+), 12 deletions(-) diff --git a/gnu/services/certbot.scm b/gnu/services/certbot.scm index 490b9e8d6d..d6354c86d3 100644 --- a/gnu/services/certbot.scm +++ b/gnu/services/certbot.scm @@ -183,15 +183,37 @@ (define certbot-command (program-file "certbot-command" #~(begin - (use-modules (ice-9 match)) - (let ((code 0)) + (use-modules (ice-9 match) + (ice-9 textual-ports)) + + (define (file-contains? file string) + (string-contains (call-with-input-file file + get-string-all) + string)) + + (define (connection-error?) + (file-contains? "/var/log/letsencrypt/letsencrypt.log" + "Failed to establish a new connection")) + + (let ((script-code 0)) (for-each (match-lambda ((name . command) (begin (format #t "Acquiring or renewing certificate: ~a~%" name) - (set! code (or (apply system* command) code))))) - '#$commands) code))))))) + (unless (zero? (status:exit-val (apply system* command))) + ;; Certbot errors are always exit code 1, but we'd like + ;; to separate connection errors from other error types. + (if (connection-error?) + ;; If we have a connection error, then bail early + ;; with exit code 2. We don't expect this to + ;; resolve within the timespan of this script. + (exit 2) + ;; If we have any other type of error, then continue + ;; but exit with a failing status code in the end. + (set! script-code 1)))))) + '#$commands) + (exit script-code)))))))) (define (certbot-renewal-jobs config) (list @@ -200,6 +222,40 @@ (define (certbot-renewal-jobs config) #~(job '(next-minute-from (next-hour '(0 12)) (list (random 60))) #$(certbot-command config)))) +(define (certbot-renewal-one-shot config) + (list + ;; Renew certificates when the system first starts. This is a one-shot + ;; service, because the mcron configuration will take care of running this + ;; periodically. This is most useful the very first time the system starts, + ;; to overwrite our self-signed certificates as soon as possible without + ;; user intervention. + (shepherd-service + (provision '(renew-certbot-certificates)) + (requirement '(nginx)) + (one-shot? #t) + (start #~(lambda _ + ;; This needs the network, but there's no reliable way to know + ;; if the network is up other than trying. If we fail due to a + ;; connection error we retry a number of times in the hope that + ;; the network comes up soon. + (let loop ((attempt 0)) + (let ((code (status:exit-val + (system* #$(certbot-command config))))) + (cond + ((and (= code 2) ; Exit code 2 means connection error + (< attempt 12)) ; 12 * 10 seconds = 2 minutes + (sleep 10) + (loop (1+ attempt))) + ((zero? code) + ;; Success! + #t) + (else + ;; Failure. + #f)))))) + (auto-start? #t) + (documentation "Call certbot to renew certificates.") + (actions (list (shepherd-configuration-action (certbot-command config))))))) + (define (generate-certificate-gexp certbot-cert-directory rsa-key-size) (match-lambda (($ <certificate-configuration> name (primary-domain other-domains ...) @@ -243,9 +299,7 @@ (define (generate-certificate-gexp certbot-cert-directory rsa-key-size) (define (certbot-activation config) (let* ((certbot-directory "/var/lib/certbot") - (certbot-cert-directory "/etc/letsencrypt/live") - (script (in-vicinity certbot-directory "renew-certificates")) - (message (format #f (G_ "~a may need to be run~%") script))) + (certbot-cert-directory "/etc/letsencrypt/live")) (match config (($ <certbot-configuration> package webroot certificates email server rsa-key-size default-location) @@ -261,10 +315,7 @@ (define (certbot-activation config) (map (generate-certificate-gexp certbot-cert-directory rsa-key-size) (filter certificate-configuration-start-self-signed? - certificates))) - - (copy-file #$(certbot-command config) #$script) - (display #$message))))))) + certificates))))))))) (define certbot-nginx-server-configurations (match-lambda @@ -297,7 +348,9 @@ (define certbot-service-type (service-extension activation-service-type certbot-activation) (service-extension mcron-service-type - certbot-renewal-jobs))) + certbot-renewal-jobs) + (service-extension shepherd-root-service-type + certbot-renewal-one-shot))) (compose concatenate) (extend (lambda (config additional-certificates) (certbot-configuration -- 2.41.0