[PATCH 0/1] (Help needed!) machine: Implement 'digital-ocean-environment-type'.

  • Done
  • quality assurance status badge
Details
2 participants
  • Ludovic Courtès
  • Jakob L. Kreuze
Owner
unassigned
Submitted by
Jakob L. Kreuze
Severity
normal
J
J
Jakob L. Kreuze wrote on 19 Aug 2019 18:41
(address . guix-patches@gnu.org)
87ftlxf6q3.fsf@sdf.lonestar.org
Hi all,

I've spent the past couple of days attempting to add rudimentary support
to 'guix deploy' for some more complicated use-cases. I think I've made
some decent progress, but I've reached a point where I'm having an issue
that's beyond my abilities.

'deploy-digital-ocean' gets to a point where there's a droplet running a
"bootstrap" configuration of the Guix System, but I can't keep an open
SSH channel for sending over the operating-system configuration
specified for the deployment.
sending 3 store items (0 MiB) to '167.71.253.223'...
;;; [2019/08/19 12:21:33.409456, 0] write_to_channel_port: [GSSH ERROR] Remote channel is closed: #<input-output: channel (open) d3b2e0>
Backtrace:
In ice-9/eval.scm:
619:8 19 (_ #(#(#<directory (guile-user) e17140>)))
In guix/ui.scm:
1692:12 18 (run-guix-command _ . _)
In guix/store.scm:
623:10 17 (call-with-store _)
In srfi/srfi-1.scm:
640:9 16 (for-each #<procedure 48d21c0 at guix/scripts/deploy.s…> …)
In guix/scripts/deploy.scm:
96:20 15 (_ _)
In ice-9/boot-9.scm:
829:9 14 (catch _ _ #<procedure 48d4980 at guix/scripts/deploy.…> …)
In guix/store.scm:
1803:24 13 (run-with-store #<store-connection 256.99 43d6420> _ # _ …)
In unknown file:
12 (_ #<procedure 48fe260 at ice-9/eval.scm:330:13 ()> #<…> …)
11 (_ #<procedure 4975a20 at ice-9/eval.scm:330:13 ()> #<…> …)
10 (_ #<procedure 4975840 at ice-9/eval.scm:330:13 ()> #<…> …)
In guix/monads.scm:
482:9 9 (_ _)
In unknown file:
8 (_ #<procedure 4975660 at ice-9/eval.scm:330:13 ()> #<…> …)
In guix/remote.scm:
134:10 7 (_ _)
In guix/store.scm:
1696:38 6 (_ #<store-connection 256.99 3606720>)
In guix/ssh.scm:
358:4 5 (send-files #<store-connection 256.99 3606720> _ _ # _ # …)
In guix/store.scm:
1568:12 4 (export-paths #<store-connection 256.99 3606720> _ #<i…> …)
1548:22 3 (export-path #<store-connection 256.99 3606720> _ #<in…> …)
697:13 2 (process-stderr _ _)
660:10 1 (dump-port #<input-output: socket 15> #<input-output: …> …)
In unknown file:
0 (put-bytevector #<input-output: channel (open) d3b2e0> # …)

ERROR: In procedure put-bytevector:
Throw to key `guile-ssh-error' with args `("write_to_channel_port" "Remote channel is closed" #<input-output: channel (open) d3b2e0> #f)'.
I can connect to the droplet over SSH, but trying to manually deploy to
the droplet with 'managed-host-environment-type' fails with the same
error. I am still able to deploy to my various Guix QEMU guests using
'managed-host-environment-type' without fail -- this seems to be
specific to Digital Ocean droplets running this configuration.
(use-modules (gnu)) (use-service-modules networking ssh) (operating-system (host-name "gnu-bootstrap") (timezone "Etc/UTC") (bootloader (bootloader-configuration (bootloader grub-bootloader) (target "/dev/vda") (terminal-outputs '(console)))) (file-systems (cons (file-system (mount-point "/") (device "/dev/vda1") (type "ext4")) %base-file-systems)) (services (append (list (static-networking-service "eth0" "~a" #:netmask "~a" #:gateway "~a" #:name-servers '("84.200.69.80" "84.200.70.40")) (service openssh-service-type (openssh-configuration (permit-root-login 'without-password)))) %base-services)))
I suspect there may an issue with the configuration of the bootstrap
system's SSH daemon, but the logs are devoid of anything particularly
telling. If anyone is willing to offer up their knowledge of SSH to
suggest what could be going wrong, I would appreciate it greatly.

Thank you,
Jakob

Jakob L. Kreuze (1):
machine: Implement 'digital-ocean-environment-type'.

doc/guix.texi | 21 +-
gnu/local.mk | 1 +
gnu/machine/digital-ocean.scm | 409 ++++++++++++++++++++++++++++++++++
3 files changed, 428 insertions(+), 3 deletions(-)
create mode 100644 gnu/machine/digital-ocean.scm

--
2.22.0
-----BEGIN PGP SIGNATURE-----

iQIzBAEBCAAdFiEEa1VJLOiXAjQ2BGSm9Qb9Fp2P2VoFAl1a0TcACgkQ9Qb9Fp2P
2Vqh3w/+Lg+d2EBJNXk1PxvjxOfodzr2unuUTPsnVgLxvFrVG3zG+FGXzIKAHm5j
E5pcsagV/URrSFcwQtfAAcvfXUG9j9EiNlAOEXSr6UkWu9Ue4DbOx6QqkIrxrhy5
I6KhKoVu9FqmOGfmWAp/kPYcOd9g6Nf/A0A0kLcYQFPCLC8rHEcVPbZB4f8BKj0a
6RMPtD/oK/uN6lDzISLc6H7USSsL2xUcQDHGUw0JC32Rtd1PQVE2eXD8dsD5XTxr
wd10H0tJzviOe2G7pcWxaCTG8potNv87R8EznErw6hYnNFVPXxBmOQCYTXjYdOqa
D4jOgY2HYkzW37qNq+SgYJWL8Zv8j6zwexgIEtizbyEKrVMbj/+2iTkq3iR91yzb
8fDcdo2ijayaLKVE59q7NW28PIizBq87Rln3Rw/HVivCVw8DO25K5noetZvGw95M
PsrlsIL/GJ/2AHrk7ZznYLhd5kGiQ4M9fx91/LPiOma9/yJPY+CvvGa6RMPKKRAC
rQRoGossMe5qu3OEmnflgSv1WxkgmDQvf+aPuFC75Qd7bC1cSTBNKG9kqKNvPNka
ht4xwu9dCMexD3DSp8gp+XFn9il6GiGVo44zzkU+RiRWMQpC7s0qgnaRCJ7GP+4n
u2dfg7yejBuX613g/5P/cuU5oLxC6UybP9sdGOdzdu2sOOEv1lE=
=rdXT
-----END PGP SIGNATURE-----

J
J
Jakob L. Kreuze wrote on 19 Aug 2019 18:43
Re: [bug#37083] [PATCH 1/1] machine: Implement 'digital-ocean-environment-type'.
(address . 37083@debbugs.gnu.org)
87blwlf6nc.fsf@sdf.lonestar.org
gnu/machine/digital-ocean.scm: New file.
gnu/local.mk (GNU_SYSTEM_MODULES): Add it.
doc/guix.texi (Invoking 'guix deploy'): Add documentation for
'digital-ocean-configuration'.
---
doc/guix.texi | 21 +-
gnu/local.mk | 1 +
gnu/machine/digital-ocean.scm | 409 ++++++++++++++++++++++++++++++++++
3 files changed, 428 insertions(+), 3 deletions(-)
create mode 100644 gnu/machine/digital-ocean.scm

Toggle diff (466 lines)
diff --git a/doc/guix.texi b/doc/guix.texi
index 043851e418..f86a7ceac4 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -25566,12 +25566,10 @@ The object of the operating system configuration to deploy.
@item @code{environment}
An @code{environment-type} describing how the machine should be provisioned.
-At the moment, the only supported value is
-@code{managed-host-environment-type}.
@item @code{configuration} (default: @code{#f})
An object describing the configuration for the machine's @code{environment}.
-If the @code{environment} has a default configuration, @code{#f} maybe used.
+If the @code{environment} has a default configuration, @code{#f} may be used.
If @code{#f} is used for an environment with no default configuration,
however, an error will be thrown.
@end table
@@ -25599,6 +25597,23 @@ remote host.
@end table
@end deftp
+@deftp {Data Type} digital-ocean-configuration
+This is the data type describing the Droplet that should be created for a
+machine with an @code{environment} of @code{digital-ocean-environment-type}.
+
+@table @asis
+@item @code{ssh-key}
+The path to the SSH private key to use to authenticate with the remote
+host. In the future, this field may not exist.
+@item @code{region}
+A Digital Ocean region slug, such as @code{"nyc3"}.
+@item @code{size}
+A Digital Ocean size slug, such as @code{"s-1vcpu-1gb"}
+@item @code{enable-ipv6}
+Whether or not the droplet should be created with IPv6 networking.
+@end table
+@end deftp
+
@node Running Guix in a VM
@section Running Guix in a Virtual Machine
diff --git a/gnu/local.mk b/gnu/local.mk
index aab29beb0a..e89562a1e2 100644
--- a/gnu/local.mk
+++ b/gnu/local.mk
@@ -570,6 +570,7 @@ GNU_SYSTEM_MODULES = \
%D%/system/vm.scm \
\
%D%/machine.scm \
+ %D%/machine/digital-ocean.scm \
%D%/machine/ssh.scm \
\
%D%/build/accounts.scm \
diff --git a/gnu/machine/digital-ocean.scm b/gnu/machine/digital-ocean.scm
new file mode 100644
index 0000000000..01393ccc35
--- /dev/null
+++ b/gnu/machine/digital-ocean.scm
@@ -0,0 +1,409 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2019 Jakob L. Kreuze <zerodaysfordays@sdf.lonestar.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 machine digital-ocean)
+ #:use-module (gnu machine ssh)
+ #:use-module (gnu machine)
+ #:use-module (gnu services networking)
+ #:use-module (gnu system)
+ #:use-module (guix base32)
+ #:use-module (guix derivations)
+ #:use-module (guix i18n)
+ #:use-module (guix import json)
+ #:use-module (guix monads)
+ #:use-module (guix records)
+ #:use-module (guix ssh)
+ #:use-module (guix store)
+ #:use-module (ice-9 hash-table)
+ #:use-module (ice-9 iconv)
+ #:use-module (json)
+ #:use-module (rnrs bytevectors)
+ #:use-module (srfi srfi-1)
+ #:use-module (srfi srfi-2)
+ #:use-module (srfi srfi-34)
+ #:use-module (srfi srfi-35)
+ #:use-module (ssh key)
+ #:use-module (ssh sftp)
+ #:use-module (ssh shell)
+ #:use-module (web client)
+ #:use-module (web request)
+ #:use-module (web response)
+ #:use-module (web uri)
+ #:export (digital-ocean-configuration
+ digital-ocean-configuration?
+
+ digital-ocean-configuration-ssh-key
+ digital-ocean-configuration-region
+ digital-ocean-configuration-size
+ digital-ocean-configuration-enable-ipv6
+
+ digital-ocean-environment-type))
+
+;;; Commentary:
+;;;
+;;; This module implements a high-level interface for provisioning "droplets"
+;;; from the Digital Ocean virtual private server (VPS) service.
+;;;
+;;; Code:
+
+(define %api-base "https://api.digitalocean.com")
+
+(define %digital-ocean-token
+ (make-parameter (getenv "GUIX_DIGITAL_OCEAN_TOKEN")))
+
+(define* (post-endpoint endpoint body)
+ "Encode BODY as JSON and send it to the Digital Ocean API endpoint
+ENDPOINT. This procedure is quite a bit more specialized than 'http-post', as
+it takes care to set headers such as 'Content-Type', 'Content-Length', and
+'Authorization' appropriately."
+ (let* ((uri (string->uri (string-append %api-base endpoint)))
+ (body (string->bytevector (scm->json-string body) "UTF-8"))
+ (headers `((User-Agent . "Guix Deploy")
+ (Accept . "application/json")
+ (Content-Type . "application/json")
+ (Authorization . ,(format #f "Bearer ~a"
+ (%digital-ocean-token)))
+ (Content-Length . ,(number->string
+ (bytevector-length body)))))
+ (port (open-socket-for-uri uri))
+ (request (build-request uri
+ #:method 'POST
+ #:version '(1 . 1)
+ #:headers headers
+ #:port port))
+ (request (write-request request port)))
+ (write-request-body request body)
+ (force-output (request-port request))
+ (let* ((response (read-response port))
+ (body (read-response-body response)))
+ (unless (= 2 (floor/ (response-code response) 100))
+ (raise
+ (condition (&message
+ (message (format
+ #f
+ (G_ "~a: HTTP post failed: ~a (~s)")
+ (uri->string uri)
+ (response-code response)
+ (response-reason-phrase response)))))))
+ (close-port port)
+ (bytevector->string body "UTF-8"))))
+
+(define (fetch-endpoint endpoint)
+ "Return the contents of the Digital Ocean API endpoint ENDPOINT as a Guile
+hash-table. This procedure is quite a bit more specialized than 'json-fetch',
+as it takes care to set headers such as 'Accept' and 'Authorization'
+appropriately."
+ (define headers
+ `((user-agent . "Guix Deploy")
+ (Accept . "application/json")
+ (Authorization . ,(format #f "Bearer ~a" (%digital-ocean-token)))))
+ (json-fetch (string-append %api-base endpoint) #:headers headers))
+
+
+;;;
+;;; Parameters for droplet creation.
+;;;
+
+(define-record-type* <digital-ocean-configuration> digital-ocean-configuration
+ make-digital-ocean-configuration
+ digital-ocean-configuration?
+ this-digital-ocean-configuration
+ (ssh-key digital-ocean-configuration-ssh-key) ; string
+ (region digital-ocean-configuration-region) ; string
+ (size digital-ocean-configuration-size) ; string
+ (enable-ipv6 digital-ocean-configuration-enable-ipv6)) ; boolean
+
+(define (read-key-fingerprint file-name)
+ "Read the private key at FILE-NAME and return the key's fingerprint as a hex
+string."
+ (let* ((privkey (private-key-from-file file-name))
+ (pubkey (private-key->public-key privkey))
+ (hash (get-public-key-hash pubkey 'md5)))
+ (bytevector->hex-string hash)))
+
+(define (droplet-name machine)
+ "Return a string uniquely identifying MACHINE."
+ (mlet* %store-monad ((os -> (machine-operating-system machine))
+ (osdrv (operating-system-derivation os)))
+ (return
+ (format #f "~a-~a"
+ (machine-display-name machine)
+ (bytevector->base32-string (derivation-hash osdrv))))))
+
+(define (droplet-by-name name)
+ "Return a Guile hash-table describing the droplet named DROPLET-NAME."
+ (find (lambda (droplet)
+ (string= (hash-ref droplet "name") name))
+ (hash-ref (fetch-endpoint "/v2/droplets") "droplets")))
+
+(define (droplet-public-ipv4-network droplet-name)
+ "Return the public IPv4 network interface for the droplet named DROPLET-NAME
+as a Guile hash-table. The expected fields are 'ip_address', 'netmask', and
+'gateway'."
+ (and-let* ((droplet (droplet-by-name droplet-name))
+ (networks (hash-ref droplet "networks"))
+ (network (find (lambda (network)
+ (string= "public" (hash-ref network "type")))
+ (hash-ref networks "v4"))))
+ network))
+
+
+;;;
+;;; Remote evaluation.
+;;;
+
+(define (digital-ocean-remote-eval target exp)
+ "Internal implementation of 'machine-remote-eval' for MACHINE instances with
+an environment type of 'digital-ocean-environment-type'."
+ (mlet* %store-monad ((name (droplet-name target))
+ (network -> (droplet-public-ipv4-network name))
+ (address -> (hash-ref network "ip_address"))
+ (ssh-key -> (digital-ocean-configuration-ssh-key
+ (machine-configuration target)))
+ (delegate -> (machine
+ (inherit target)
+ (environment managed-host-environment-type)
+ (configuration
+ (machine-ssh-configuration
+ (host-name address)
+ (identity ssh-key)
+ (system "x86_64-linux"))))))
+ (machine-remote-eval delegate exp)))
+
+
+;;;
+;;; System deployment.
+;;;
+
+;; The following script was adapted from the guide available at
+;; <https://wiki.pantherx.org/Installation-digital-ocean/>.
+(define (guix-infect network)
+ "Given NETWORK, a Guile hash-table describing the Droplet's public IPv4
+network interface, return a Bash script that will install the Guix system."
+ (format #f "#!/bin/bash
+
+apt-get update
+apt-get install xz-utils -y
+wget https://ftp.gnu.org/gnu/guix/guix-binary-1.0.1.x86_64-linux.tar.xz
+cd /tmp
+tar --warning=no-timestamp -xf ~~/guix-binary-1.0.1.x86_64-linux.tar.xz
+mv var/guix /var/ && mv gnu /
+mkdir -p ~~root/.config/guix
+ln -sf /var/guix/profiles/per-user/root/current-guix ~~root/.config/guix/current
+export GUIX_PROFILE=\"`echo ~~root`/.config/guix/current\" ;
+source $GUIX_PROFILE/etc/profile
+groupadd --system guixbuild
+for i in `seq -w 1 10`; do
+ useradd -g guixbuild -G guixbuild \
+ -d /var/empty -s `which nologin` \
+ -c \"Guix build user $i\" --system \
+ guixbuilder$i;
+done;
+cp ~~root/.config/guix/current/lib/systemd/system/guix-daemon.service /etc/systemd/system/
+systemctl start guix-daemon && systemctl enable guix-daemon
+mkdir -p /usr/local/bin
+cd /usr/local/bin
+ln -s /var/guix/profiles/per-user/root/current-guix/bin/guix
+mkdir -p /usr/local/share/info
+cd /usr/local/share/info
+for i in /var/guix/profiles/per-user/root/current-guix/share/info/*; do
+ ln -s $i;
+done
+guix archive --authorize < ~~root/.config/guix/current/share/guix/ci.guix.gnu.org.pub
+guix pull
+guix package -i glibc-utf8-locales
+export GUIX_LOCPATH=\"$HOME/.guix-profile/lib/locale\"
+guix package -i openssl
+cat > /etc/bootstrap-config.scm << EOF
+(use-modules (gnu))
+(use-service-modules networking ssh)
+
+(operating-system
+ (host-name \"gnu-bootstrap\")
+ (timezone \"Etc/UTC\")
+ (bootloader (bootloader-configuration
+ (bootloader grub-bootloader)
+ (target \"/dev/vda\")
+ (terminal-outputs '(console))))
+ (file-systems (cons (file-system
+ (mount-point \"/\")
+ (device \"/dev/vda1\")
+ (type \"ext4\"))
+ %base-file-systems))
+ (services
+ (append (list (static-networking-service \"eth0\" \"~a\"
+ #:netmask \"~a\"
+ #:gateway \"~a\"
+ #:name-servers '(\"84.200.69.80\" \"84.200.70.40\"))
+ (service openssh-service-type
+ (openssh-configuration
+ (permit-root-login 'without-password))))
+ %base-services)))
+EOF
+guix pull
+guix system build /etc/bootstrap-config.scm
+guix system reconfigure /etc/bootstrap-config.scm
+mv /etc /old-etc
+mkdir /etc
+cp -r /old-etc/{passwd,group,shadow,gshadow,mtab,guix,bootstrap-config.scm} /etc/
+guix system reconfigure /etc/bootstrap-config.scm"
+ (hash-ref network "ip_address")
+ (hash-ref network "netmask")
+ (hash-ref network "gateway")))
+
+(define (droplet-wait-until-available droplet-name)
+ "Block until the initial Debian image has been installed on the droplet
+named DROPLET-NAME."
+ (and-let* ((droplet (droplet-by-name droplet-name))
+ (droplet-id (hash-ref droplet "id"))
+ (endpoint (format #f "/v2/droplets/~a/actions" droplet-id)))
+ (let loop ()
+ (let ((actions (hash-ref (fetch-endpoint endpoint) "actions")))
+ (unless (every (lambda (action)
+ (string= "completed" (hash-ref action "status")))
+ actions)
+ (sleep 5)
+ (loop))))))
+
+(define (wait-for-ssh address ssh-key)
+ "Block until the an SSH session can be made as 'root' with SSH-KEY at ADDRESS."
+ (let loop ()
+ (catch #t
+ (lambda ()
+ (open-ssh-session address #:user "root" #:identity ssh-key))
+ (lambda args
+ (sleep 5)
+ (loop)))))
+
+(define (add-static-networking target network)
+ "Return an <operating-system> based on TARGET with a static networking
+configuration for the public IPv4 network described by the Guile hash-table
+NETWORK."
+ (operating-system
+ (inherit (machine-operating-system target))
+ (services (cons (static-networking-service "eth0"
+ (hash-ref network "ip_address")
+ #:netmask (hash-ref network "netmask")
+ #:gateway (hash-ref network "gateway")
+ #:name-servers '("84.200.69.80" "84.200.70.40"))
+ (operating-system-services
+ (machine-operating-system target))))))
+
+(define (deploy-digital-ocean target)
+ "Internal implementation of 'deploy-machine' for 'machine' instances with an
+environment type of 'digital-ocean-environment-type'."
+ (maybe-raise-missing-api-key-error)
+ (maybe-raise-unsupported-configuration-error target)
+ (mlet* %store-monad ((config -> (machine-configuration target))
+ (name (droplet-name target))
+ (region -> (digital-ocean-configuration-region config))
+ (size -> (digital-ocean-configuration-size config))
+ (ssh-key -> (digital-ocean-configuration-ssh-key config))
+ (enable-ipv6 -> (digital-ocean-configuration-enable-ipv6 config))
+ (fingerprint -> (read-key-fingerprint ssh-key))
+ (request-body -> `(("name" . ,name)
+ ("region" . ,region)
+ ("size" . ,size)
+ ("image" . "debian-9-x64")
+ ("ssh_keys" . (,fingerprint))
+ ("backups" . #f)
+ ("ipv6" . ,enable-ipv6)
+ ("user_data" . #nil)
+ ("private_networking" . #nil)
+ ("volumes" . #nil)
+ ("tags" . ())))
+ (response -> (post-endpoint "/v2/droplets" request-body)))
+ (droplet-wait-until-available name)
+ (let* ((network (droplet-public-ipv4-network name))
+ (address (hash-ref network "ip_address")))
+ (wait-for-ssh address ssh-key)
+ (let* ((ssh-session (open-ssh-session address #:user "root" #:identity ssh-key))
+ (sftp-session (make-sftp-session ssh-session)))
+ (call-with-remote-output-file sftp-session "/tmp/guix-infect.sh"
+ (lambda (port)
+ (display (guix-infect network) port)))
+ (rexec ssh-session "/bin/bash /tmp/guix-infect.sh")
+ ;; Session will close upon rebooting, which will raise 'guile-ssh-error.
+ (catch 'guile-ssh-error
+ (lambda () (rexec ssh-session "reboot"))
+ (lambda args #t)))
+ (wait-for-ssh address ssh-key)
+ (let ((delegate (machine
+ (operating-system (add-static-networking target network))
+ (environment managed-host-environment-type)
+ (configuration
+ (machine-ssh-configuration
+ (host-name address)
+ (identity ssh-key)
+ (system "x86_64-linux"))))))
+ (deploy-machine delegate)))))
+
+
+;;;
+;;; Roll-back.
+;;;
+
+(define (roll-back-digital-ocean machine)
+ "Internal implementation of 'roll-back-machine' for MACHINE instances with
+an environment type of 'digital-ocean-environment-type'. This destroys the
+associated droplet."
+ (mlet* %store-monad ((name (droplet-name machine)))
+ (let* ((droplet (droplet-by-name name))
+ (droplet-id (hash-ref droplet "id"))
+ (headers `((Content-Type . "application/json")
+ (user-agent . "Guix Deploy")
+ (Authorization . ,(format #f "Bearer ~a"
+ (%digital-ocean-token))))))
+ (http-delete (format #f "~a/v2/droplets/~a" %api-base droplet-id)
+ #:headers headers))))
+
+
+;;;
+;;; Environment type.
+;;;
+
+(define digital-ocean-environment-type
+ (environment-type
+ (machine-remote-eval digital-ocean-remote-eval)
+ (deploy-machine deploy-digital-ocean)
+ (roll-back-machine roll-back-digital-ocean)
+ (name 'digital-ocean-environment-type)
+ (description "Provisioning of \"droplets\": virtual machines
+ provided by the Digital Ocean virtual private server (VPS) service.")))
+
+
+(define (maybe-raise-missing-api-key-error)
+ (unless (%digital-ocean-token)
+ (raise (condition
+ (&message
+ (message (G_ "No Digital Ocean access token was provided. This \
+may be fixed by setting the environment variable GUIX_DIGITAL_OCAEN_TOKEN to \
+one procured from https://cloud.digitalocean.com/account/api/tokens.")))))))
+
+(define (maybe-raise-unsupported-configuration-error machine)
+ "Raise an error if MACHINE's configuration is not an instance of
+<digital-ocean-configuration>."
+ (let ((config (machine-configuration machine))
+ (environment (environment-type-name (machine-environment machine))))
+ (unless (and config (digital-ocean-configuration? config))
+ (raise (condition
+ (&message
+ (message (format #f (G_ "unsupported machine configuration '~a
This message was truncated. Download the full message here.
-----BEGIN PGP SIGNATURE-----

iQIzBAEBCAAdFiEEa1VJLOiXAjQ2BGSm9Qb9Fp2P2VoFAl1a0ZcACgkQ9Qb9Fp2P
2VoBNg//ezkavJiag7iM5rJ/gaFK4SX+UCBWuXMaNAXD+LqEoR95u1lE+9zBgbig
pHJvWKqyUntM3DYmS5nXEp7DWkllUgVbj35jI11nQsO9S4pkJv8uj8jy4Muy7FNN
jm/Qlgcp29xwX04uuO9qEjQvd64QCAbM4b7w+2Pq2y/uVNDTRz7vMjLCY1eKf9It
8tJ4RecGDPH7gCjdFrl1IW7dQ14LKGHmHwitAjUC18oL73znuxaBhYFsfLSKZwnP
inQYJJPUiV5zmKJQBR+GqBdd8SR+tGscSr+ar4YYW4DzdxaALWIk4CXKCJ6+OrEb
+bNhAHxyyblK8Vb4vTgtuudJsZ/uVYjxND3Zo9UY/oeEcs4LHg/iwQ2IVte0tBtL
b99UI36UY/E5iL/Pplp1/MR5+8PuU5jmvpcIMhIvZQz1gTruJnXN9I1IXkGQxD8n
KOn3ykrzxNNrtZsi5GGEYpmgv1Qznf7aTGLS97Rj6hCq86rkvZ5v4n/L/XSXxs3o
WBv32UpSfyWBh1+zjV3rSX2qPEvkIZD9JVp5dDT/oih6QM3GBVN6wvYp0FWmmqdo
PWPkwVGzolwG3Obn4deWcSImgjRs0rDoJTbr64jCSrwQC2lSFjFIpJ0ULjmHCLyd
g5SdbkWJFZEKMEaEYvCcLKpCioFbKGZHPFokBH1pYmK83fK5YJo=
=6Hqh
-----END PGP SIGNATURE-----

L
L
Ludovic Courtès wrote on 27 Aug 2019 12:38
Re: [bug#37083] [PATCH 0/1] (Help needed!) machine: Implement 'digital-ocean-environment-type'.
(name . Jakob L. Kreuze)(address . zerodaysfordays@sdf.lonestar.org)(address . 37083@debbugs.gnu.org)
875zmirizk.fsf@gnu.org
Hi Jakob,

Nice that you’re working on Digital Ocean support!

zerodaysfordays@sdf.lonestar.org (Jakob L. Kreuze) skribis:

Toggle quote (5 lines)
> 'deploy-digital-ocean' gets to a point where there's a droplet running a
> "bootstrap" configuration of the Guix System, but I can't keep an open
> SSH channel for sending over the operating-system configuration
> specified for the deployment.

[...]

Toggle quote (10 lines)
> (services
> (append (list (static-networking-service "eth0" "~a"
> #:netmask "~a"
> #:gateway "~a"
> #:name-servers '("84.200.69.80" "84.200.70.40"))
> (service openssh-service-type
> (openssh-configuration
> (permit-root-login 'without-password))))
> %base-services)))

Could you add (log-level 'debug) to ‘openssh-configuration’, then try
again ‘guix deploy’, and finally grab the OpenSSH log from that machine?
That would allow us to see if there’s something wrong with SSH.

Hmm now that I think about it, ‘send-files’ may be failing because the
(guix …) modules aren’t in GUILE_LOAD_PATH on the remote side. On the
berlin build machines, we have this:

(simple-service 'guile-load-path-in-global-env
session-environment-service-type
`(("GUILE_LOAD_PATH"
. "/run/current-system/profile/share/guile/site/2.2")
("GUILE_LOAD_COMPILED_PATH"
. ,(string-append "/run/current-system/profile/lib/guile/2.2/site-ccache:"
"/run/current-system/profile/share/guile/site/2.2"))))

It’s ridiculous that we have to do this, but that’s how it is.

Can you try that?

HTH,
Ludo’.
L
L
Ludovic Courtès wrote on 4 Sep 2019 14:08
(name . Jakob L. Kreuze)(address . zerodaysfordays@sdf.lonestar.org)(address . 37083@debbugs.gnu.org)
87y2z45knp.fsf@gnu.org
Hi Jakob,

Did you have a chance to try this out?

Thanks,
Ludo’.

Ludovic Courtès <ludo@gnu.org> skribis:

Toggle quote (45 lines)
> Hi Jakob,
>
> Nice that you’re working on Digital Ocean support!
>
> zerodaysfordays@sdf.lonestar.org (Jakob L. Kreuze) skribis:
>
>> 'deploy-digital-ocean' gets to a point where there's a droplet running a
>> "bootstrap" configuration of the Guix System, but I can't keep an open
>> SSH channel for sending over the operating-system configuration
>> specified for the deployment.
>
> [...]
>
>> (services
>> (append (list (static-networking-service "eth0" "~a"
>> #:netmask "~a"
>> #:gateway "~a"
>> #:name-servers '("84.200.69.80" "84.200.70.40"))
>> (service openssh-service-type
>> (openssh-configuration
>> (permit-root-login 'without-password))))
>> %base-services)))
>
> Could you add (log-level 'debug) to ‘openssh-configuration’, then try
> again ‘guix deploy’, and finally grab the OpenSSH log from that machine?
> That would allow us to see if there’s something wrong with SSH.
>
> Hmm now that I think about it, ‘send-files’ may be failing because the
> (guix …) modules aren’t in GUILE_LOAD_PATH on the remote side. On the
> berlin build machines, we have this:
>
> (simple-service 'guile-load-path-in-global-env
> session-environment-service-type
> `(("GUILE_LOAD_PATH"
> . "/run/current-system/profile/share/guile/site/2.2")
> ("GUILE_LOAD_COMPILED_PATH"
> . ,(string-append "/run/current-system/profile/lib/guile/2.2/site-ccache:"
> "/run/current-system/profile/share/guile/site/2.2"))))
>
> It’s ridiculous that we have to do this, but that’s how it is.
>
> Can you try that?
>
> HTH,
> Ludo’.
J
J
Jakob L. Kreuze wrote on 5 Sep 2019 16:15
(name . Ludovic Courtès)(address . ludo@gnu.org)(address . 37083@debbugs.gnu.org)
87blvysuaz.fsf@sdf.lonestar.org
Hi Ludovic,

Ludovic Courtès <ludo@gnu.org> writes:

Toggle quote (2 lines)
> Did you have a chance to try this out?

So sorry about this -- I've been busy moving in for fall semester and
the little bit of time I had to work on this was spent migrating the
code to the newer guile-json API. I will have some time this weekend to
see if it fixes the issue.

Regards,
Jakob
-----BEGIN PGP SIGNATURE-----

iQIzBAEBCAAdFiEEa1VJLOiXAjQ2BGSm9Qb9Fp2P2VoFAl1xGJUACgkQ9Qb9Fp2P
2Vq1mA//Vpopj61bsSoukzfZn4Q8Wadw/hhadxJk0PyIPtb2KJNMWx+6d0BtXzla
IMmwebI1Qdg1YYkcN3LXdPg+lh9da313jxH6sePz+SEiOSyNldfFuSVAjKAFuFP9
OQ+vpa/6+RTrO0emZdYzn2XwgVh+I1r7NHIRuuI3T+2ZQok1Tx4FwOO5cBfQT/QG
DQoINiQyMoliHxwIrK2A2+17ZN/zi4kJb3bfS4Rlg94+lVOPwVjDc1T6Im0pFTeT
iTbtuSBOTaPUeNoy0jV4Jm+M6sgKcxIUL5p+ZAkpREGf+/b/bsDqiR/SP5MKn4vx
utDYRJg9tYFmZnObBIElZSnQ6eBf8SzKeBQPwb0Yt5jk/u+x+V76xy4f0SN68pzb
z99oa/hBRaOqZLO80sEoamEoYE8WvUrlE6IIrZ+bEJGRG+htpI2e2o6uLA2raFNx
pa7Yu4E686E6/f/5QXdfyhu6Yzft1ZusriqbYYfPKZulh7nTQy8xktK3Pr9p4hc1
gqfQr/WbetqXUVvpWCkmrRynZQcyHu1YEitCZyI/IH2maxKoroxY7tstaU76miz/
w3kP3q50jK9t5wZIs+gvULvJwB10JYeyNgdnE47w6VEz4Y50YVYDCO8PkG9U4UZ2
9UdQwansJTKlpHJ90NfdoWEtGvzdrwflvfjWJH3Okzgm/lC6Dk0=
=olCe
-----END PGP SIGNATURE-----

J
J
Jakob L. Kreuze wrote on 7 Sep 2019 22:10
(name . Ludovic Courtès)(address . ludo@gnu.org)(address . 37083@debbugs.gnu.org)
87tv9nc1fz.fsf@sdf.lonestar.org
zerodaysfordays@sdf.lonestar.org (Jakob L. Kreuze) writes:

Toggle quote (5 lines)
> So sorry about this -- I've been busy moving in for fall semester and
> the little bit of time I had to work on this was spent migrating the
> code to the newer guile-json API. I will have some time this weekend to
> see if it fixes the issue.

Indeed, it does :) Now, to fix the other issues with this. I'm getting a
"more than one target service of type 'shepherd-root'" error, which is
unusual. I'll investigate further.

Regards,
Jakob
-----BEGIN PGP SIGNATURE-----

iQIzBAEBCAAdFiEEa1VJLOiXAjQ2BGSm9Qb9Fp2P2VoFAl10DrEACgkQ9Qb9Fp2P
2VpBOA//YwIf56z0W2TOkCrFyroKp14jU9jfUVTKppSapEdKhugvUV7Og1f+t0+j
Dy7xJT/TO9ex/MIXN0NxnnaR7NgfVc5s6hMnwbOB8CZRLMdB2GgXiDqiOd7GCa6u
YPHTVeNer6wb/UExZ+00myBEUQ8nlV67W9Mb5HJBdgQttC5Tdahn0M1cSSrYQ/5H
U19KF2d36Kkc7X79HqEi9ArVTmWkNhCoj+bKSqp5Qnkgyg6FPrJu6LCzVq5MVs0T
SEoh7CYwn1ZBUtiwH8VehUGfhHAdSCWLNMyqd4QyKIOiMdDn9lxot6JYY9vAvf3w
geYx/43WG0rnhxqvNbcNXsxdtapc1w59fx/udKPK/G8P2nC/EFB0NbXvYpZke2Xo
qY1aaHIoJZV9kU8ucxon5tjxSFQQf0zrqbMelZ5xOclO2xef3faPztrRqhTihHaW
1HApN70ozpn5PusqgTIFC6wJwlN78uVhIyCtohsmAyYT8DUh5qmW3WNphwhkN6NJ
7jTodd69CxdV0qC+zjXUYGBIG57Y9MDImrH3EnRh5BRQXfuIfJdVZhf+rCjF8zkn
1PXAswvdtiC5D/iQMSYweW+QJ4vKSqK6fesiUR0v2yaDVQtpfZNKiMbAHK4UbLIo
fKFJCpWR9CdGq44nigyxHM74knBUEaLaz2DqZ39U9j7EJE47+UU=
=XFwB
-----END PGP SIGNATURE-----

L
L
Ludovic Courtès wrote on 8 Sep 2019 21:37
(name . Jakob L. Kreuze)(address . zerodaysfordays@sdf.lonestar.org)(address . 37083@debbugs.gnu.org)
87y2yy7f64.fsf@gnu.org
Hi Jakob,

zerodaysfordays@sdf.lonestar.org (Jakob L. Kreuze) skribis:

Toggle quote (2 lines)
> Indeed, it does :)

Yay!

Toggle quote (4 lines)
> Now, to fix the other issues with this. I'm getting a "more than one
> target service of type 'shepherd-root'" error, which is unusual. I'll
> investigate further.

Presumably there’s more than one service of type
‘shepherd-root-service-type’ in the ‘services’ field? Let me know if I
can help.

Good luck with your other endeavors!

Thanks,
Ludo’.
J
J
Jakob L. Kreuze wrote on 21 Sep 2019 22:56
(name . Ludovic Courtès)(address . ludo@gnu.org)(address . 37083@debbugs.gnu.org)
87y2yh5px7.fsf@sdf.lonestar.org
Hey Ludovic,

Ludovic Courtès <ludo@gnu.org> writes:

Toggle quote (4 lines)
> Presumably there’s more than one service of type
> ‘shepherd-root-service-type’ in the ‘services’ field? Let me know if I
> can help.

Sorry about how long this has been taking, I've been plucking away at it
on the weekends, but I've reached the point where I have to admit that
I'm stuck and I really need help if I'm ever going to finish this.

I have this procedure to create a static networking service for the
Digital Ocean droplet based on an API response:

(define (add-static-networking target network)
"Return an <operating-system> based on TARGET with a static networking
configuration for the public IPv4 network described by the alist NETWORK."
(operating-system
(inherit (machine-operating-system target))
(services (cons (static-networking-service "eth0"
(assoc-ref network "ip_address")
#:netmask (assoc-ref network "netmask")
#:gateway (assoc-ref network "gateway")
#:name-servers '("84.200.69.80" "84.200.70.40"))
(operating-system-services
(machine-operating-system target))))))

And when this operating system is deployed with the basic SSH
environment-type, I get the following backtrace:
Backtrace:
6 (apply-smob/1 #<catch-closure 23ab600>)
In ice-9/boot-9.scm:
705:2 5 (call-with-prompt _ _ #<procedure default-prompt-handle…>)
In ice-9/eval.scm:
619:8 4 (_ #(#(#<directory (guile-user) 24a1140>)))
In guix/ui.scm:
1692:12 3 (run-guix-command _ . _)
In guix/store.scm:
623:10 2 (call-with-store _)
In srfi/srfi-1.scm:
640:9 1 (for-each #<procedure 4fbf800 at guix/scripts/deploy.s…> …)
In guix/scripts/deploy.scm:
96:20 0 (_ _)

guix/scripts/deploy.scm:96:20: Throw to key `srfi-34' with args `(#<condition %compound [service: #<<service> type: #<service-type openssh 4246960> value: #<<openssh-configuration> openssh: #<package openssh@8.0p1 gnu/packages/ssh.scm:165 3315210> pid-file: "/var/run/sshd.pid" port-number: 22 permit-root-login: #t allow-empty-passwords?: #f password-authentication?: #t public-key-authentication?: #t x11-forwarding?: #f allow-agent-forwarding?: #t allow-tcp-forwarding?: #t gateway-ports?: #f challenge-response-authentication?: #f use-pam?: #t print-last-log?: #t subsystems: (("sftp" "internal-sftp")) accepted-environment: () log-level: info extra-content: "" authorized-keys: () %auto-start?: #t>> target-type: #<service-type shepherd-root 2c4ac30> message: "more than one target service of type 'shepherd-root'"] 5579510>)'.
I have no idea where to begin with this. Why would the OpenSSH service
be giving me this "more than one target service of type 'shepherd-root'"
error?

Regards,
Jakob
-----BEGIN PGP SIGNATURE-----

iQIzBAEBCAAdFiEEa1VJLOiXAjQ2BGSm9Qb9Fp2P2VoFAl2GjoQACgkQ9Qb9Fp2P
2Vr3NA/6AseX9vtQm7Kg/rX6Ga9wUwsPuj7B0zWm4eHU1ybW9eZghjM/vTFA3YOl
j3chqYWLJ2hpLjxmDE2iey45cRJJC9hST21q8SZSPxQlrro6LcAgKpzfLpIyyih3
jQbbp20RuX5q4PfBO/ZFNcOctsXpZVebBzIJTqSk4udWidcQ+DBArEuHiOQbZ9Xw
S4J73Ct2qBJJCfxw5ScapTDmvr+EIctZQNVPkIeX+u5ZZKN603KG9A2VtPl1bZBN
89xTM/sxgV0FYHiyB/4BmyWjgqK+PLqmDP6FSC7hN8ytLbLEu/7NBol9HXaspEsa
7cJV2bdlahZIL+MPE0V6ZNBu26q3Jy3fezFY+UKuyTFPkb9fe6L+RkKkbCFC7gkA
Q1pv/Mv7V5geJmeb/fINW0da38Q7Xe6c43uXHP1MWFTghVMwf+ayiPKK31eh1+Er
OJNfZzGENuPB8laUKoxSgg4nSsm6vDaeK4l17PqP35r9y82xMDz6fI/K/HFIabd6
Jon0yrlQcJwOVy32ZRFQg+SLBvKkPZiQrE6apdbFXp6DHMM17AMB6aae8Ouyz91j
Tf5wFL6Uw3HA3wCSO9CU8AKD/xqTAm8Zux9rcOJU186RH1RbWAaFI+a4o99QFQWn
dMzBkq6dhnO4Sxp5ypQNS+YdQD0IHvYEMghmHN06JslVrkdupxw=
=0z2e
-----END PGP SIGNATURE-----

L
L
Ludovic Courtès wrote on 23 Sep 2019 10:24
(name . Jakob L. Kreuze)(address . zerodaysfordays@sdf.lonestar.org)(address . 37083@debbugs.gnu.org)
87sgonl8t4.fsf@gnu.org
Hi Jakob!

zerodaysfordays@sdf.lonestar.org (Jakob L. Kreuze) skribis:

Toggle quote (26 lines)
> Ludovic Courtès <ludo@gnu.org> writes:
>
>> Presumably there’s more than one service of type
>> ‘shepherd-root-service-type’ in the ‘services’ field? Let me know if I
>> can help.
>
> Sorry about how long this has been taking, I've been plucking away at it
> on the weekends, but I've reached the point where I have to admit that
> I'm stuck and I really need help if I'm ever going to finish this.
>
> I have this procedure to create a static networking service for the
> Digital Ocean droplet based on an API response:
>
> (define (add-static-networking target network)
> "Return an <operating-system> based on TARGET with a static networking
> configuration for the public IPv4 network described by the alist NETWORK."
> (operating-system
> (inherit (machine-operating-system target))
> (services (cons (static-networking-service "eth0"
> (assoc-ref network "ip_address")
> #:netmask (assoc-ref network "netmask")
> #:gateway (assoc-ref network "gateway")
> #:name-servers '("84.200.69.80" "84.200.70.40"))
> (operating-system-services
> (machine-operating-system target))))))

Oooh, got it: right above, you should call
‘operating-system-user-services’, not ‘operating-system-services’.

The latter includes “essential” services like ‘etc’ and ‘shepherd-root’,
which is why we’d end up with two copies of each of these.

Admittedly quite error-prone!

Let me know if there are other stumbling blocks. I look forward to
seeing Digital Ocean support in ‘guix deploy’!

Thanks,
Ludo’.
J
J
Jakob L. Kreuze wrote on 28 Sep 2019 22:46
(name . Ludovic Courtès)(address . ludo@gnu.org)(address . 37083@debbugs.gnu.org)
87ftkgno80.fsf@sdf.lonestar.org
Ludovic Courtès <ludo@gnu.org> writes:

Toggle quote (8 lines)
> Oooh, got it: right above, you should call
> ‘operating-system-user-services’, not ‘operating-system-services’.
>
> The latter includes “essential” services like ‘etc’ and ‘shepherd-root’,
> which is why we’d end up with two copies of each of these.
>
> Admittedly quite error-prone!

Ah, thank you. I feel like I've been bitten by that once before and just
forgot.

Toggle quote (3 lines)
> Let me know if there are other stumbling blocks. I look forward to
> seeing Digital Ocean support in ‘guix deploy’!

With that, I think we've got working support for Digital Ocean :) Patch
to follow.

Regards,
Jakob
-----BEGIN PGP SIGNATURE-----

iQIzBAEBCAAdFiEEa1VJLOiXAjQ2BGSm9Qb9Fp2P2VoFAl2Pxp8ACgkQ9Qb9Fp2P
2VoZ8A/9F5CHx72FD+LQCudfVrWLPBVgWbuLzj5m4e1ZyIVCkdk0D0qYqP7wHZ+j
6ZOI8fW2aNrNUNs6mMazJEfRDzH1SzvIDhexKgniYFAgRktGdCG/5LWx8pKzwM3p
PR3J/u++9GuT0RPhGf27jSKKgJqz2xsqaSm3Z/OqjRYpT7xmekDw6hZcpU2taP1+
+1jKoZysAOsVhKX+x+beLMRbrpPvI8FqEs5JopS7NDER2G8jV0DrlNeZsmSmEsvK
j9SWOlw3JEJaqgpzj38Wnitli3PHA/o7qR6eaDIQNVT9CJ7C9JgDUlT/MZiZ00dV
ByA1G5pSDRE0Cpo0/fV2efCWqWKEYwF3T62zDBP4Sjin9WVKyCZ7a+MDRLvKsqKS
NByMuzuGPHw8JaQvx8LmXyWZj3SDKagaZlEQY7zVWzEDSKWK0q3BP3i6hlb3qCsQ
XJkO1UIGb9uUL+3WLDTcnAvffFMZHU8b/WDy8FPf5fgIiNZEenLAQIszt2MZZ7tM
M9ILRi5wj2QlULiTPpxbS5uU5ZLMK+h4neTfawwXaF9LyqCcyxTrEbE4RXytg7p+
norzo/Meta4HcjffpbE/q+0mMuTs8vdkouBS9QP0EUJsiAHlwDVWA+Ecp70fIWjL
wA3zqDCizzJlt8Jcz7hVOX7tC2v9c4fji3gAne5/M4f88IUbfFU=
=gq1T
-----END PGP SIGNATURE-----

J
J
Jakob L. Kreuze wrote on 28 Sep 2019 22:47
Re: [bug#37083] [PATCH] machine: Implement 'digital-ocean-environment-type'.
(name . Ludovic Courtès)(address . ludo@gnu.org)(address . 37083@debbugs.gnu.org)
87blv4no6e.fsf_-_@sdf.lonestar.org
gnu/machine/digital-ocean.scm: New file.
gnu/local.mk (GNU_SYSTEM_MODULES): Add it.
doc/guix.texi (Invoking 'guix deploy'): Add documentation for
'digital-ocean-configuration'.
---
doc/guix.texi | 24 +-
gnu/local.mk | 1 +
gnu/machine/digital-ocean.scm | 422 ++++++++++++++++++++++++++++++++++
3 files changed, 444 insertions(+), 3 deletions(-)
create mode 100644 gnu/machine/digital-ocean.scm

Toggle diff (467 lines)
diff --git a/doc/guix.texi b/doc/guix.texi
index 0d3bb19325..0c8d531684 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -25929,12 +25929,10 @@ The object of the operating system configuration to deploy.
@item @code{environment}
An @code{environment-type} describing how the machine should be provisioned.
-At the moment, the only supported value is
-@code{managed-host-environment-type}.
@item @code{configuration} (default: @code{#f})
An object describing the configuration for the machine's @code{environment}.
-If the @code{environment} has a default configuration, @code{#f} maybe used.
+If the @code{environment} has a default configuration, @code{#f} may be used.
If @code{#f} is used for an environment with no default configuration,
however, an error will be thrown.
@end table
@@ -25962,6 +25960,26 @@ remote host.
@end table
@end deftp
+@deftp {Data Type} digital-ocean-configuration
+This is the data type describing the Droplet that should be created for a
+machine with an @code{environment} of @code{digital-ocean-environment-type}.
+
+@table @asis
+@item @code{ssh-key}
+The path to the SSH private key to use to authenticate with the remote
+host. In the future, this field may not exist.
+@item @code{tags}
+A list of string ``tags'' that uniquely identify the machine. Must be given
+such that no two machines in the deployment have the same set of tags.
+@item @code{region}
+A Digital Ocean region slug, such as @code{"nyc3"}.
+@item @code{size}
+A Digital Ocean size slug, such as @code{"s-1vcpu-1gb"}
+@item @code{enable-ipv6}
+Whether or not the droplet should be created with IPv6 networking.
+@end table
+@end deftp
+
@node Running Guix in a VM
@section Running Guix in a Virtual Machine
diff --git a/gnu/local.mk b/gnu/local.mk
index b04a5d796e..3bcde0ccc5 100644
--- a/gnu/local.mk
+++ b/gnu/local.mk
@@ -578,6 +578,7 @@ GNU_SYSTEM_MODULES = \
%D%/system/vm.scm \
\
%D%/machine.scm \
+ %D%/machine/digital-ocean.scm \
%D%/machine/ssh.scm \
\
%D%/build/accounts.scm \
diff --git a/gnu/machine/digital-ocean.scm b/gnu/machine/digital-ocean.scm
new file mode 100644
index 0000000000..5ad7c4d4a3
--- /dev/null
+++ b/gnu/machine/digital-ocean.scm
@@ -0,0 +1,422 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2019 Jakob L. Kreuze <zerodaysfordays@sdf.lonestar.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 machine digital-ocean)
+ #:use-module (gnu machine ssh)
+ #:use-module (gnu machine)
+ #:use-module (gnu services)
+ #:use-module (gnu services networking)
+ #:use-module (gnu system)
+ #:use-module (gnu system pam)
+ #:use-module (guix base32)
+ #:use-module (guix derivations)
+ #:use-module (guix i18n)
+ #:use-module (guix import json)
+ #:use-module (guix monads)
+ #:use-module (guix records)
+ #:use-module (guix ssh)
+ #:use-module (guix store)
+ #:use-module (ice-9 iconv)
+ #:use-module (json)
+ #:use-module (rnrs bytevectors)
+ #:use-module (srfi srfi-1)
+ #:use-module (srfi srfi-2)
+ #:use-module (srfi srfi-34)
+ #:use-module (srfi srfi-35)
+ #:use-module (ssh key)
+ #:use-module (ssh sftp)
+ #:use-module (ssh shell)
+ #:use-module (web client)
+ #:use-module (web request)
+ #:use-module (web response)
+ #:use-module (web uri)
+ #:export (digital-ocean-configuration
+ digital-ocean-configuration?
+
+ digital-ocean-configuration-ssh-key
+ digital-ocean-configuration-tags
+ digital-ocean-configuration-region
+ digital-ocean-configuration-size
+ digital-ocean-configuration-enable-ipv6
+
+ digital-ocean-environment-type))
+
+;;; Commentary:
+;;;
+;;; This module implements a high-level interface for provisioning "droplets"
+;;; from the Digital Ocean virtual private server (VPS) service.
+;;;
+;;; Code:
+
+(define %api-base "https://api.digitalocean.com")
+
+(define %digital-ocean-token
+ (make-parameter (getenv "GUIX_DIGITAL_OCEAN_TOKEN")))
+
+(define* (post-endpoint endpoint body)
+ "Encode BODY as JSON and send it to the Digital Ocean API endpoint
+ENDPOINT. This procedure is quite a bit more specialized than 'http-post', as
+it takes care to set headers such as 'Content-Type', 'Content-Length', and
+'Authorization' appropriately."
+ (let* ((uri (string->uri (string-append %api-base endpoint)))
+ (body (string->bytevector (scm->json-string body) "UTF-8"))
+ (headers `((User-Agent . "Guix Deploy")
+ (Accept . "application/json")
+ (Content-Type . "application/json")
+ (Authorization . ,(format #f "Bearer ~a"
+ (%digital-ocean-token)))
+ (Content-Length . ,(number->string
+ (bytevector-length body)))))
+ (port (open-socket-for-uri uri))
+ (request (build-request uri
+ #:method 'POST
+ #:version '(1 . 1)
+ #:headers headers
+ #:port port))
+ (request (write-request request port)))
+ (write-request-body request body)
+ (force-output (request-port request))
+ (let* ((response (read-response port))
+ (body (read-response-body response)))
+ (unless (= 2 (floor/ (response-code response) 100))
+ (raise
+ (condition (&message
+ (message (format
+ #f
+ (G_ "~a: HTTP post failed: ~a (~s)")
+ (uri->string uri)
+ (response-code response)
+ (response-reason-phrase response)))))))
+ (close-port port)
+ (bytevector->string body "UTF-8"))))
+
+(define (fetch-endpoint endpoint)
+ "Return the contents of the Digital Ocean API endpoint ENDPOINT as an
+alist. This procedure is quite a bit more specialized than 'json-fetch', as it
+takes care to set headers such as 'Accept' and 'Authorization' appropriately."
+ (define headers
+ `((user-agent . "Guix Deploy")
+ (Accept . "application/json")
+ (Authorization . ,(format #f "Bearer ~a" (%digital-ocean-token)))))
+ (json-fetch (string-append %api-base endpoint) #:headers headers))
+
+
+;;;
+;;; Parameters for droplet creation.
+;;;
+
+(define-record-type* <digital-ocean-configuration> digital-ocean-configuration
+ make-digital-ocean-configuration
+ digital-ocean-configuration?
+ this-digital-ocean-configuration
+ (ssh-key digital-ocean-configuration-ssh-key) ; string
+ (tags digital-ocean-configuration-tags) ; list of strings
+ (region digital-ocean-configuration-region) ; string
+ (size digital-ocean-configuration-size) ; string
+ (enable-ipv6 digital-ocean-configuration-enable-ipv6)) ; boolean
+
+(define (read-key-fingerprint file-name)
+ "Read the private key at FILE-NAME and return the key's fingerprint as a hex
+string."
+ (let* ((privkey (private-key-from-file file-name))
+ (pubkey (private-key->public-key privkey))
+ (hash (get-public-key-hash pubkey 'md5)))
+ (bytevector->hex-string hash)))
+
+(define (machine-droplet machine)
+ "Return an alist describing the droplet allocated to MACHINE."
+ (let ((tags (digital-ocean-configuration-tags
+ (machine-configuration machine))))
+ (find (lambda (droplet)
+ (equal? (assoc-ref droplet "tags") (list->vector tags)))
+ (vector->list
+ (assoc-ref (fetch-endpoint "/v2/droplets") "droplets")))))
+
+(define (machine-public-ipv4-network machine)
+ "Return the public IPv4 network interface of the droplet allocated to
+MACHINE as an alist. The expected fields are 'ip_address', 'netmask', and
+'gateway'."
+ (and-let* ((droplet (machine-droplet machine))
+ (networks (assoc-ref droplet "networks"))
+ (network (find (lambda (network)
+ (string= "public" (assoc-ref network "type")))
+ (vector->list (assoc-ref networks "v4")))))
+ network))
+
+
+;;;
+;;; Remote evaluation.
+;;;
+
+(define (digital-ocean-remote-eval target exp)
+ "Internal implementation of 'machine-remote-eval' for MACHINE instances with
+an environment type of 'digital-ocean-environment-type'."
+ (let* ((network (machine-public-ipv4-network target))
+ (address (assoc-ref network "ip_address"))
+ (ssh-key (digital-ocean-configuration-ssh-key
+ (machine-configuration target)))
+ (delegate (machine
+ (inherit target)
+ (environment managed-host-environment-type)
+ (configuration
+ (machine-ssh-configuration
+ (host-name address)
+ (identity ssh-key)
+ (system "x86_64-linux"))))))
+ (machine-remote-eval delegate exp)))
+
+
+;;;
+;;; System deployment.
+;;;
+
+;; The following script was adapted from the guide available at
+;; <https://wiki.pantherx.org/Installation-digital-ocean/>.
+(define (guix-infect network)
+ "Given NETWORK, an alist describing the Droplet's public IPv4 network
+interface, return a Bash script that will install the Guix system."
+ (format #f "#!/bin/bash
+
+apt-get update
+apt-get install xz-utils -y
+wget https://ftp.gnu.org/gnu/guix/guix-binary-1.0.1.x86_64-linux.tar.xz
+cd /tmp
+tar --warning=no-timestamp -xf ~~/guix-binary-1.0.1.x86_64-linux.tar.xz
+mv var/guix /var/ && mv gnu /
+mkdir -p ~~root/.config/guix
+ln -sf /var/guix/profiles/per-user/root/current-guix ~~root/.config/guix/current
+export GUIX_PROFILE=\"`echo ~~root`/.config/guix/current\" ;
+source $GUIX_PROFILE/etc/profile
+groupadd --system guixbuild
+for i in `seq -w 1 10`; do
+ useradd -g guixbuild -G guixbuild \
+ -d /var/empty -s `which nologin` \
+ -c \"Guix build user $i\" --system \
+ guixbuilder$i;
+done;
+cp ~~root/.config/guix/current/lib/systemd/system/guix-daemon.service /etc/systemd/system/
+systemctl start guix-daemon && systemctl enable guix-daemon
+mkdir -p /usr/local/bin
+cd /usr/local/bin
+ln -s /var/guix/profiles/per-user/root/current-guix/bin/guix
+mkdir -p /usr/local/share/info
+cd /usr/local/share/info
+for i in /var/guix/profiles/per-user/root/current-guix/share/info/*; do
+ ln -s $i;
+done
+guix archive --authorize < ~~root/.config/guix/current/share/guix/ci.guix.gnu.org.pub
+# guix pull
+guix package -i glibc-utf8-locales
+export GUIX_LOCPATH=\"$HOME/.guix-profile/lib/locale\"
+guix package -i openssl
+cat > /etc/bootstrap-config.scm << EOF
+(use-modules (gnu))
+(use-service-modules networking ssh)
+
+(operating-system
+ (host-name \"gnu-bootstrap\")
+ (timezone \"Etc/UTC\")
+ (bootloader (bootloader-configuration
+ (bootloader grub-bootloader)
+ (target \"/dev/vda\")
+ (terminal-outputs '(console))))
+ (file-systems (cons (file-system
+ (mount-point \"/\")
+ (device \"/dev/vda1\")
+ (type \"ext4\"))
+ %base-file-systems))
+ (services
+ (append (list (static-networking-service \"eth0\" \"~a\"
+ #:netmask \"~a\"
+ #:gateway \"~a\"
+ #:name-servers '(\"84.200.69.80\" \"84.200.70.40\"))
+ (simple-service 'guile-load-path-in-global-env
+ session-environment-service-type
+ \\`((\"GUILE_LOAD_PATH\"
+ . \"/run/current-system/profile/share/guile/site/2.2\")
+ (\"GUILE_LOAD_COMPILED_PATH\"
+ . ,(string-append \"/run/current-system/profile/lib/guile/2.2/site-ccache:\"
+ \"/run/current-system/profile/share/guile/site/2.2\"))))
+ (service openssh-service-type
+ (openssh-configuration
+ (log-level 'debug)
+ (permit-root-login 'without-password))))
+ %base-services)))
+EOF
+# guix pull
+guix system build /etc/bootstrap-config.scm
+guix system reconfigure /etc/bootstrap-config.scm
+mv /etc /old-etc
+mkdir /etc
+cp -r /old-etc/{passwd,group,shadow,gshadow,mtab,guix,bootstrap-config.scm} /etc/
+guix system reconfigure /etc/bootstrap-config.scm"
+ (assoc-ref network "ip_address")
+ (assoc-ref network "netmask")
+ (assoc-ref network "gateway")))
+
+(define (machine-wait-until-available machine)
+ "Block until the initial Debian image has been installed on the droplet
+named DROPLET-NAME."
+ (and-let* ((droplet (machine-droplet machine))
+ (droplet-id (assoc-ref droplet "id"))
+ (endpoint (format #f "/v2/droplets/~a/actions" droplet-id)))
+ (let loop ()
+ (let ((actions (assoc-ref (fetch-endpoint endpoint) "actions")))
+ (unless (every (lambda (action)
+ (string= "completed" (assoc-ref action "status")))
+ (vector->list actions))
+ (sleep 5)
+ (loop))))))
+
+(define (wait-for-ssh address ssh-key)
+ "Block until the an SSH session can be made as 'root' with SSH-KEY at ADDRESS."
+ (let loop ()
+ (catch #t
+ (lambda ()
+ (open-ssh-session address #:user "root" #:identity ssh-key))
+ (lambda args
+ (sleep 5)
+ (loop)))))
+
+(define (add-static-networking target network)
+ "Return an <operating-system> based on TARGET with a static networking
+configuration for the public IPv4 network described by the alist NETWORK."
+ (operating-system
+ (inherit (machine-operating-system target))
+ (services (cons* (static-networking-service "eth0"
+ (assoc-ref network "ip_address")
+ #:netmask (assoc-ref network "netmask")
+ #:gateway (assoc-ref network "gateway")
+ #:name-servers '("84.200.69.80" "84.200.70.40"))
+ (simple-service 'guile-load-path-in-global-env
+ session-environment-service-type
+ `(("GUILE_LOAD_PATH"
+ . "/run/current-system/profile/share/guile/site/2.2")
+ ("GUILE_LOAD_COMPILED_PATH"
+ . ,(string-append "/run/current-system/profile/lib/guile/2.2/site-ccache:"
+ "/run/current-system/profile/share/guile/site/2.2"))))
+ (operating-system-user-services
+ (machine-operating-system target))))))
+
+(define (deploy-digital-ocean target)
+ "Internal implementation of 'deploy-machine' for 'machine' instances with an
+environment type of 'digital-ocean-environment-type'."
+ (maybe-raise-missing-api-key-error)
+ (maybe-raise-unsupported-configuration-error target)
+ (let* ((config (machine-configuration target))
+ (name (machine-display-name target))
+ (region (digital-ocean-configuration-region config))
+ (size (digital-ocean-configuration-size config))
+ (ssh-key (digital-ocean-configuration-ssh-key config))
+ (fingerprint (read-key-fingerprint ssh-key))
+ (enable-ipv6 (digital-ocean-configuration-enable-ipv6 config))
+ (tags (digital-ocean-configuration-tags config))
+ (request-body `(("name" . ,name)
+ ("region" . ,region)
+ ("size" . ,size)
+ ("image" . "debian-9-x64")
+ ("ssh_keys" . ,(vector fingerprint))
+ ("backups" . #f)
+ ("ipv6" . ,enable-ipv6)
+ ("user_data" . #nil)
+ ("private_networking" . #nil)
+ ("volumes" . #nil)
+ ("tags" . ,(list->vector tags))))
+ (response (post-endpoint "/v2/droplets" request-body)))
+ (machine-wait-until-available target)
+ (let* ((network (machine-public-ipv4-network target))
+ (address (assoc-ref network "ip_address")))
+ (wait-for-ssh address ssh-key)
+ (let* ((ssh-session (open-ssh-session address #:user "root" #:identity ssh-key))
+ (sftp-session (make-sftp-session ssh-session)))
+ (call-with-remote-output-file sftp-session "/tmp/guix-infect.sh"
+ (lambda (port)
+ (display (guix-infect network) port)))
+ (rexec ssh-session "/bin/bash /tmp/guix-infect.sh")
+ ;; Session will close upon rebooting, which will raise 'guile-ssh-error.
+ (catch 'guile-ssh-error
+ (lambda () (rexec ssh-session "reboot"))
+ (lambda args #t)))
+ (wait-for-ssh address ssh-key)
+ (let ((delegate (machine
+ (operating-system (add-static-networking target network))
+ (environment managed-host-environment-type)
+ (configuration
+ (machine-ssh-configuration
+ (host-name address)
+ (identity ssh-key)
+ (system "x86_64-linux"))))))
+ (deploy-machine delegate)))))
+
+
+;;;
+;;; Roll-back.
+;;;
+
+(define (roll-back-digital-ocean target)
+ "Internal implementation of 'roll-back-machine' for MACHINE instances with an
+environment type of 'digital-ocean-environment-type'."
+ (let* ((network (machine-public-ipv4-network target))
+ (address (assoc-ref network "ip_address"))
+ (ssh-key (digital-ocean-configuration-ssh-key
+ (machine-configuration target)))
+ (delegate (machine
+ (inherit target)
+ (environment managed-host-environment-type)
+ (configuration
+ (machine-ssh-configuration
+ (host-name address)
+ (identity ssh-key)
+ (system "x86_64-linux"))))))
+ (roll-back-machine delegate)))
+
+
+;;;
+;;; Environment type.
+;;;
+
+(define digital-ocean-environment-type
+ (environment-type
+ (machine-remote-eval digital-ocean-remote-eval)
+ (deploy-machine deploy-digital-ocean)
+ (roll-back-machine roll-back-digital-ocean)
+ (name 'digital-ocean-environment-type)
+ (description "Provisioning of \"droplets\": virtual machines
+ provided by the Digital Ocean virtual private server (VPS) service.")))
+
+
+(define (maybe-raise-missing-api-key-error)
+ (unless (%digital-ocean
This message was truncated. Download the full message here.
-----BEGIN PGP SIGNATURE-----

iQIzBAEBCAAdFiEEa1VJLOiXAjQ2BGSm9Qb9Fp2P2VoFAl2PxtkACgkQ9Qb9Fp2P
2VprtA/5Af4bASvaWOBjHony2lrWbMRKtVIMOLRng9QzEB8WoAe7iOKoc9n3T7m0
HD2PS4HqQRKaavFNVtIJF42BSYPMPph9baNWU/y6Ss+0rV24bF38oiuTmr/nz3bS
G/7jiUb5w/DW5A6bSJMhydlGXiLe5b6m8t6pOJ5fuf9toRucOA6DaTnBKQYjVb14
0rpcZexxkA8lLhV16hLvnpgbOIz4hIi+xohSKyzl7KIotGtMzZ6kjDzFTbN8Wa6Q
jNxBMLQbiuuRh8/BPrW2K0+JWTQb7CX0SEco2GvnZhgBdq5Xm03XS5cowwjr4A5S
JyQW6TKwwjCs77wZhfNeta7SBf4LoIgzNFAlD0noIbUw1l20hY1+W6/nSHZEfCJD
M2GllTKYfXRbHiixhMLHXAPCcFNn6yxxaH2unb7PbsjAqf9GHEOT4xLSnNfCa+3f
ewegTP7zT1iSE7ib3sfb1+ZZng1603Kt/ilpC7mb7vgVojFMFfUv96gv9KMmd4To
1MHI/Oa0NpdGOpoEDJ3fzGZVszlr644FFPh+6uhQPB2rnhMrJ+03eD71cQj2cO1S
X8NN6dy7OqjxahBA00r/qYUK8UiiPHLSIxOzNP6wJv7Zk3zfn+2WUY8nMgWS1bFS
5IVxDJhyiK8u3a2qj9NRLKv23bKWMfBhtVbS8ZSOrImkJFtWlt4=
=wBhm
-----END PGP SIGNATURE-----

L
L
Ludovic Courtès wrote on 29 Sep 2019 00:36
(name . Jakob L. Kreuze)(address . zerodaysfordays@sdf.lonestar.org)(address . 37083@debbugs.gnu.org)
877e5sm4kl.fsf@gnu.org
Hi Jakob!

zerodaysfordays@sdf.lonestar.org (Jakob L. Kreuze) skribis:

Toggle quote (3 lines)
> gnu/machine/digital-ocean.scm: New file.
> gnu/local.mk (GNU_SYSTEM_MODULES): Add it.
> doc/guix.texi (Invoking 'guix deploy'): Add documentation for
^
Nitpick: please add a “*” before each bullet. :-)

Apart from that, LGTM, woohoo!

Toggle quote (10 lines)
> +(define (fetch-endpoint endpoint)
> + "Return the contents of the Digital Ocean API endpoint ENDPOINT as an
> +alist. This procedure is quite a bit more specialized than 'json-fetch', as it
> +takes care to set headers such as 'Accept' and 'Authorization' appropriately."
> + (define headers
> + `((user-agent . "Guix Deploy")
> + (Accept . "application/json")
> + (Authorization . ,(format #f "Bearer ~a" (%digital-ocean-token)))))
> + (json-fetch (string-append %api-base endpoint) #:headers headers))

Note for later: we could use ‘define-json-mapping’ to work on Scheme
records rather than on alists.

Toggle quote (2 lines)
> + (message (G_ "No Digital Ocean access token was provided. This \
> +may be fixed by setting the environment variable GUIX_DIGITAL_OCAEN_TOKEN to \
^^
Typo.

Thank you!

Ludo’.
L
L
Ludovic Courtès wrote on 13 Oct 2019 12:56
(name . Jakob L. Kreuze)(address . zerodaysfordays@sdf.lonestar.org)(address . 37083@debbugs.gnu.org)
87lftoap7u.fsf@gnu.org
Hi Jakob,

A friendly reminder. :-)

I can commit it on your behalf if you lack the bandwidth right now.

Thanks,
Ludo’.

Ludovic Courtès <ludo@gnu.org> skribis:

Toggle quote (33 lines)
> Hi Jakob!
>
> zerodaysfordays@sdf.lonestar.org (Jakob L. Kreuze) skribis:
>
>> gnu/machine/digital-ocean.scm: New file.
>> gnu/local.mk (GNU_SYSTEM_MODULES): Add it.
>> doc/guix.texi (Invoking 'guix deploy'): Add documentation for
> ^
> Nitpick: please add a “*” before each bullet. :-)
>
> Apart from that, LGTM, woohoo!
>
>> +(define (fetch-endpoint endpoint)
>> + "Return the contents of the Digital Ocean API endpoint ENDPOINT as an
>> +alist. This procedure is quite a bit more specialized than 'json-fetch', as it
>> +takes care to set headers such as 'Accept' and 'Authorization' appropriately."
>> + (define headers
>> + `((user-agent . "Guix Deploy")
>> + (Accept . "application/json")
>> + (Authorization . ,(format #f "Bearer ~a" (%digital-ocean-token)))))
>> + (json-fetch (string-append %api-base endpoint) #:headers headers))
>
> Note for later: we could use ‘define-json-mapping’ to work on Scheme
> records rather than on alists.
>
>> + (message (G_ "No Digital Ocean access token was provided. This \
>> +may be fixed by setting the environment variable GUIX_DIGITAL_OCAEN_TOKEN to \
> ^^
> Typo.
>
> Thank you!
>
> Ludo’.
L
L
Ludovic Courtès wrote on 22 Oct 2019 18:34
(name . Jakob L. Kreuze)(address . zerodaysfordays@sdf.lonestar.org)(address . 37083@debbugs.gnu.org)
87pnioyc1s.fsf@gnu.org
Hi,

Ludovic Courtès <ludo@gnu.org> skribis:

Toggle quote (2 lines)
> I can commit it on your behalf if you lack the bandwidth right now.

Done!

I followed up with commit c93994b5e43acc6048b81836d30632e015306c92 to
rename ‘enable-ipv6’ to ‘enable-ipv6?’ with a question mark as is
customary. :-)

Thanks,
Ludo’.
J
J
Jakob L. Kreuze wrote on 22 Oct 2019 22:56
(name . Ludovic Courtès)(address . ludo@gnu.org)(address . 37083@debbugs.gnu.org)
87k18wcxeh.fsf@sdf.lonestar.org
Hi Ludo,

Ludovic Courtès <ludo@gnu.org> writes:

Toggle quote (10 lines)
> Ludovic Courtès <ludo@gnu.org> skribis:
>
>> I can commit it on your behalf if you lack the bandwidth right now.
>
> Done!
>
> I followed up with commit c93994b5e43acc6048b81836d30632e015306c92 to
> rename ‘enable-ipv6’ to ‘enable-ipv6?’ with a question mark as is
> customary. :-)

Thanks! Sorry, I haven't paying as much attention to this mailing list
recently due to the whole you-know-what spiel, so that email ended up
burried in my inbox. Glad it finally made it into master :)

Regards,
Jakob
-----BEGIN PGP SIGNATURE-----

iQIzBAEBCAAdFiEEa1VJLOiXAjQ2BGSm9Qb9Fp2P2VoFAl2vbOgACgkQ9Qb9Fp2P
2VoHmw/+N60IpjiOCT4n4zrPzRTc3ZWgPiFXkKXzFSa0OO9DIubLphemMvhMwmb7
8D+qU2FZXz7ImzMxUQ4kyV4PMsyANMY2qlQ+Mo/ez/XNdvyaqYbAbK/e7mp0fxl5
gaMlrZpafkYIEk+qGtjEpsJjRdbYou/CfMQKhA908RfocJQxyUIG3vjfwo9yn0aI
4wTLPZefEii91t4gFB5j2XM+MZL1BKS7YwT+KDKhNlEdUpEcB3VQNocZqVKOHPk8
UYjTCnkOO7zIj1BlfUCZEsbPgdOLSWgPmUU9lmsTcAo0zKnCZGBXAJrJuHlDawOl
djdBqv2sDMbZQ9wBjtlqLmIS68VaJhum+LYICWrh13r5wyAQvZ3ozI+MrTqfoJNi
Ek2/qn2Boi40I020ehLVEM851WsNUOkj1N+/8PWrG8ODvDUTJhFaSqIJlQvPVE+e
K8NGYq5SFV+SdtoMjNlQFu6fmKpjcd86HKqDdt3+v/qVdknHFDWU/OZ2jYVnBrau
cxaAjjJZzvlqbxUQVqboRoIo3KnYzX5B+jDIPTfV6Kk4/7US14NFHUWsW7TJ/BT5
JwzQEDTUDIL8vhKIqBkF24HYSvMyQJVbtCH+jTqVxp8ZtbWPTcQBzcBLQmj26eOr
Yo5WbPsyzm7pr60H1R/peJT8h3b+hZnnqnTCm2Dyc93/CvubgWc=
=dTXD
-----END PGP SIGNATURE-----

L
L
Ludovic Courtès wrote on 23 Oct 2019 11:42
(name . Jakob L. Kreuze)(address . zerodaysfordays@sdf.lonestar.org)(address . 37083@debbugs.gnu.org)
87d0enx0g9.fsf@gnu.org
Hi Jakob,

zerodaysfordays@sdf.lonestar.org (Jakob L. Kreuze) skribis:

Toggle quote (16 lines)
> Ludovic Courtès <ludo@gnu.org> writes:
>
>> Ludovic Courtès <ludo@gnu.org> skribis:
>>
>>> I can commit it on your behalf if you lack the bandwidth right now.
>>
>> Done!
>>
>> I followed up with commit c93994b5e43acc6048b81836d30632e015306c92 to
>> rename ‘enable-ipv6’ to ‘enable-ipv6?’ with a question mark as is
>> customary. :-)
>
> Thanks! Sorry, I haven't paying as much attention to this mailing list
> recently due to the whole you-know-what spiel, so that email ended up
> burried in my inbox. Glad it finally made it into master :)

Heheh, I understand, and I’m happy it has landed too!

Ludo’.
L
L
Ludovic Courtès wrote on 4 Dec 2019 18:07
control message for bug #37083
(address . control@debbugs.gnu.org)
87h82gc949.fsf@gnu.org
tags 37083 fixed
close 37083
quit
?