Subject: Re: [bug#32141] [PATCH] services: Add ddclient service.

From: Oleg Pykhalov <[email-masked]>
Date: Wed, 01 Aug 2018 20:27:38 +0300

Hello Ludovic,

I applied all your suggestions and updated the documentation.  The patch
is attached below.  I run a ddclient service from this patch currently.

[email-masked] (Ludovic Courtès) writes:

[…]

>> Also, the generated ‘ddclient.conf’ which contains secrets is stored in
>> the store.  I probably should change the ‘ddclient-activation’ procedure
>>
>> (copy-file #$(plain-file "ddclient.conf" config-str) file)
>>
>> to a procedure which writes ‘config-str’ to the file without storing it
>> somewhere else.  WDYT?
>
> The problem would be the same: the activation script would contain
> ‘config-str’, and it would live in the store.
>
> In short we must not manipulate secrets in anything that goes through
> the store.  The only thing I can suggest is to leave it up to the
> user to create a file containing the secret in an out-of-band fashion;
> /etc is a good place for such things.
>
> For example, they could create /etc/ddclient-secrets and then we would
> somehow arrange to get that file read.
>
> To do that there are two possibilities that come to mind:
>
>   1. If the config file syntax has an “include” directive, just include
>      /etc/ddclient-secrets unconditionally in the generated config file.
>
>   2. Write an activation snippet that concatenates the generated config
>      file with /etc/ddclient-secrets and stores that as
>      /etc/ddclient.conf (or something like that.)
>
> Thoughts?

Could we use ‘/etc/ddclient’ directory for secrets file, because
ddclient program use this directory by default?

--8<---------------cut here---------------start------------->8---
The following example will configure the service.

By default, the @code{secret-file} in @code{ddclient-configuration} is
pointing to @file{/etc/ddclient/secrets.conf} file, which will be appended to
@file{/etc/ddclient/ddclient.conf} and should be created in advance.  See
samples inside @file{/share/ddclient} directory of @code{ddclient} package.

@example
(service ddclient-service-type)
@end example
--8<---------------cut here---------------end--------------->8---

From 3f47ae60ecb2e8780c451e93976b5c83135d8420 Mon Sep 17 00:00:00 2001
From: Oleg Pykhalov <[email-masked]>
Date: Fri, 13 Jul 2018 11:49:13 +0300
Subject: [PATCH] services: Add ddclient service.

* gnu/services/dns.scm (ddclient-configuration, ddclient-service-type): New
variables.
(uglify-field-name, serialize-field, serialize-boolean, serialize-integer,
serialize-string, serialize-list, serialize-extra-options,
ddclient-activation, ddclient-shepherd-service,
generate-ddclient-documentation): New procedures.
* doc/guix.texi (DNS Services): Document it.
---
 doc/guix.texi        | 103 +++++++++++++++++++++++++++
 gnu/services/dns.scm | 166 ++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 268 insertions(+), 1 deletion(-)

diff --git a/doc/guix.texi b/doc/guix.texi
index 080b091b3..e08bfe059 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -17211,6 +17211,109 @@ When false, disable negative caching.
 @end table
 @end deftp
 
+@subsubheading ddclient Service
+
+@cindex ddclient
+The ddclient service described below runs the ddclient daemon, which takes
+care of automatically updating DNS entries for service providers such as
+@uref{https://dyn.com/dns/, Dyn}.
+
+The following example will configure the service.
+
+By default, the @code{secret-file} in @code{ddclient-configuration} is
+pointing to @file{/etc/ddclient/secrets.conf} file, which will be appended to
+@file{/etc/ddclient/ddclient.conf} and should be created in advance.  See
+samples inside @file{/share/ddclient} directory of @code{ddclient} package.
+
+@example
+(service ddclient-service-type)
+@end example
+
+
+@c %start of fragment
+
+Available @code{ddclient-configuration} fields are:
+
+@deftypevr {@code{ddclient-configuration} parameter} package ddclient
+The ddclient package.
+
+@end deftypevr
+
+@deftypevr {@code{ddclient-configuration} parameter} integer daemon
+The period after which ddclient will retry to check IP and domain name.
+
+Defaults to @samp{300}.
+
+@end deftypevr
+
+@deftypevr {@code{ddclient-configuration} parameter} boolean syslog
+Use syslog for the output.
+
+Defaults to @samp{#t}.
+
+@end deftypevr
+
+@deftypevr {@code{ddclient-configuration} parameter} string mail
+Mail to user.
+
+Defaults to @samp{"root"}.
+
+@end deftypevr
+
+@deftypevr {@code{ddclient-configuration} parameter} string mail-failure
+Mail failed update to user.
+
+Defaults to @samp{"root"}.
+
+@end deftypevr
+
+@deftypevr {@code{ddclient-configuration} parameter} string pid
+The ddclient PID file.
+
+Defaults to @samp{"/var/run/ddclient/ddclient.pid"}.
+
+@end deftypevr
+
+@deftypevr {@code{ddclient-configuration} parameter} boolean ssl
+Enable SSL support.
+
+Defaults to @samp{#t}.
+
+@end deftypevr
+
+@deftypevr {@code{ddclient-configuration} parameter} string user
+Specifies the user name or ID that is used when running ddclient
+program.
+
+Defaults to @samp{"ddclient"}.
+
+@end deftypevr
+
+@deftypevr {@code{ddclient-configuration} parameter} string group
+Group of the user who will run the ddclient program.
+
+Defaults to @samp{"ddclient"}.
+
+@end deftypevr
+
+@deftypevr {@code{ddclient-configuration} parameter} string secret-file
+Secret file which will be appended to ddclient.conf file.
+
+Defaults to @samp{"/etc/ddclient/secrets.conf"}.
+
+@end deftypevr
+
+@deftypevr {@code{ddclient-configuration} parameter} list extra-options
+Extra options will be appended to ddclient configuration file.
+
+Defaults to @samp{()}.
+
+@end deftypevr
+
+
+@c %end of fragment
+
+
 @node VPN Services
 @subsubsection VPN Services
 @cindex VPN (virtual private network)
diff --git a/gnu/services/dns.scm b/gnu/services/dns.scm
index 2c57a36b8..7ac61dfca 100644
--- a/gnu/services/dns.scm
+++ b/gnu/services/dns.scm
@@ -1,5 +1,6 @@
 ;;; GNU Guix --- Functional package management for GNU
 ;;; Copyright © 2017 Julien Lepiller <[email-masked]>
+;;; Copyright © 2018 Oleg Pykhalov <[email-masked]>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -45,7 +46,10 @@
             zone-entry
 
             dnsmasq-service-type
-            dnsmasq-configuration))
+            dnsmasq-configuration
+
+            ddclient-service-type
+            ddclient-configuration))
 
 ;;;
 ;;; Knot DNS.
@@ -670,3 +674,163 @@
                              (compose list dnsmasq-shepherd-service))))
    (default-value (dnsmasq-configuration))
    (description "Run the dnsmasq DNS server.")))
+
+
+;;;
+;;; ddclient
+;;;
+
+(define (uglify-field-name field-name)
+  (string-delete #\? (symbol->string field-name)))
+
+(define (serialize-field field-name val)
+  (format #t "~a=~a\n" (uglify-field-name field-name) val))
+
+(define (serialize-boolean field-name val)
+  (serialize-field field-name (if val "yes" "no")))
+
+(define (serialize-integer field-name val)
+  (serialize-field field-name (number->string val)))
+
+(define (serialize-string field-name val)
+  (if (and (string? val) (string=? val ""))
+      ""
+      (serialize-field field-name val)))
+
+(define (serialize-list field-name val)
+  (if (null? val) "" (serialize-field field-name (string-join val))))
+
+(define (serialize-extra-options extra-options)
+  (string-join extra-options "\n" 'suffix))
+
+(define-configuration ddclient-configuration
+  (ddclient
+   (package ddclient)
+   "The ddclient package.")
+  (daemon
+   (integer 300)
+   "The period after which ddclient will retry to check IP and domain name.")
+  (syslog
+   (boolean #t)
+   "Use syslog for the output.")
+  (mail
+   (string "root")
+   "Mail to user.")
+  (mail-failure
+   (string "root")
+   "Mail failed update to user.")
+  (pid
+   (string "/var/run/ddclient/ddclient.pid")
+   "The ddclient PID file.")
+  (ssl
+   (boolean #t)
+   "Enable SSL support.")
+  (user
+   (string "ddclient")
+   "Specifies the user name or ID that is used when running ddclient
+program.")
+  (group
+   (string "ddclient")
+   "Group of the user who will run the ddclient program.")
+  (secret-file
+   (string "/etc/ddclient/secrets.conf")
+   "Secret file which will be appended to ddclient.conf file.")
+  (extra-options
+   (list '())
+   "Extra options will be appended to ddclient configuration file."))
+
+(define (ddclient-account config)
+  "Return the user accounts and user groups for CONFIG."
+  (let ((ddclient-user (ddclient-configuration-user config))
+        (ddclient-group (ddclient-configuration-group config)))
+    (list (user-group
+           (name ddclient-group)
+           (system? #t))
+          (user-account
+           (name ddclient-user)
+           (system? #t)
+           (group ddclient-group)
+           (comment "ddclientd privilege separation user")
+           (home-directory (string-append "/var/run/" ddclient-user))))))
+
+(define (ddclient-activation config)
+  "Return the activation GEXP for CONFIG."
+  (with-imported-modules '((guix build utils)
+                           (ice-9 rdelim))
+    #~(begin
+        (use-modules (guix build utils)
+                     (ice-9 rdelim))
+        (let ((ddclient-user
+               #$(passwd:uid (getpw (ddclient-configuration-user config))))
+              (ddclient-group
+               #$(passwd:gid (getpw (ddclient-configuration-group config))))
+              (ddclient-secret-file
+               #$(ddclient-configuration-secret-file config)))
+          ;; 'ddclient' complains about ddclient.conf file permissions, which
+          ;; rules out /gnu/store.  Thus we copy the ddclient.conf to /etc.
+          (for-each (lambda (dir)
+                      (mkdir-p dir)
+                      (chmod dir #o700)
+                      (chown dir ddclient-user ddclient-group))
+                    '("/var/cache/ddclient" "/var/run/ddclient"
+                      "/etc/ddclient"))
+          (with-output-to-file "/etc/ddclient/ddclient.conf"
+            (lambda ()
+              (display
+               (string-append
+                "# Generated by 'ddclient-service'.\n\n"
+                #$(with-output-to-string
+                    (lambda ()
+                      (serialize-configuration config
+                                               ddclient-configuration-fields)))
+                (if (string-null? ddclient-secret-file)
+                    ""
+                    (format #f "\n\n# Appended from '~a'.\n\n~a"
+                            ddclient-secret-file
+                            (with-input-from-file ddclient-secret-file
+                              read-string)))))))
+          (chmod "/etc/ddclient/ddclient.conf" #o600)
+          (chown "/etc/ddclient/ddclient.conf"
+                 ddclient-user ddclient-group)))))
+
+(define (ddclient-shepherd-service config)
+  "Return a <shepherd-service> for ddclient with CONFIG."
+  (let ((ddclient (ddclient-configuration-ddclient config))
+        (ddclient-pid (ddclient-configuration-pid config))
+        (ddclient-user (ddclient-configuration-user config))
+        (ddclient-group (ddclient-configuration-group config)))
+    (list (shepherd-service
+           (provision '(ddclient))
+           (documentation "Run ddclient daemon.")
+           (start #~(make-forkexec-constructor
+                     (list #$(file-append ddclient "/bin/ddclient")
+                           "-foreground"
+                           "-file" "/etc/ddclient/ddclient.conf")
+                     #:pid-file #$ddclient-pid
+                     #:environment-variables
+                     (list "SSL_CERT_DIR=/run/current-system/profile\
+/etc/ssl/certs"
+                           "SSL_CERT_FILE=/run/current-system/profile\
+/etc/ssl/certs/ca-certificates.crt")
+                     #:user #$ddclient-user
+                     #:group #$ddclient-group))
+           (stop #~(make-kill-destructor))))))
+
+(define ddclient-service-type
+  (service-type
+   (name 'ddclient)
+   (extensions
+    (list (service-extension account-service-type
+                             ddclient-account)
+          (service-extension shepherd-root-service-type
+                             ddclient-shepherd-service)
+          (service-extension activation-service-type
+                             ddclient-activation)))
+   (default-value (ddclient-configuration))
+   (description "Configure address updating utility for dynamic DNS services,
+ddclient.")))
+
+(define (generate-ddclient-documentation)
+  (generate-documentation
+   `((ddclient-configuration ,ddclient-configuration-fields))
+   'ddclient-configuration))
-- 
2.18.0