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

DoneSubmitted by Jakob L. Kreuze.
Details
2 participants
  • Ludovic Courtès
  • Jakob L. Kreuze
Owner
unassigned
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 supportto 'guix deploy' for some more complicated use-cases. I think I've madesome decent progress, but I've reached a point where I'm having an issuethat'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 openSSH channel for sending over the operating-system configurationspecified 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 tothe droplet with 'managed-host-environment-type' fails with the sameerror. I am still able to deploy to my various Guix QEMU guests using'managed-host-environment-type' without fail -- this seems to bespecific 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 bootstrapsystem's SSH daemon, but the logs are devoid of anything particularlytelling. If anyone is willing to offer up their knowledge of SSH tosuggest 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-----
iQIzBAEBCAAdFiEEa1VJLOiXAjQ2BGSm9Qb9Fp2P2VoFAl1a0TcACgkQ9Qb9Fp2P2Vqh3w/+Lg+d2EBJNXk1PxvjxOfodzr2unuUTPsnVgLxvFrVG3zG+FGXzIKAHm5jE5pcsagV/URrSFcwQtfAAcvfXUG9j9EiNlAOEXSr6UkWu9Ue4DbOx6QqkIrxrhy5I6KhKoVu9FqmOGfmWAp/kPYcOd9g6Nf/A0A0kLcYQFPCLC8rHEcVPbZB4f8BKj0a6RMPtD/oK/uN6lDzISLc6H7USSsL2xUcQDHGUw0JC32Rtd1PQVE2eXD8dsD5XTxrwd10H0tJzviOe2G7pcWxaCTG8potNv87R8EznErw6hYnNFVPXxBmOQCYTXjYdOqaD4jOgY2HYkzW37qNq+SgYJWL8Zv8j6zwexgIEtizbyEKrVMbj/+2iTkq3iR91yzb8fDcdo2ijayaLKVE59q7NW28PIizBq87Rln3Rw/HVivCVw8DO25K5noetZvGw95MPsrlsIL/GJ/2AHrk7ZznYLhd5kGiQ4M9fx91/LPiOma9/yJPY+CvvGa6RMPKKRACrQRoGossMe5qu3OEmnflgSv1WxkgmDQvf+aPuFC75Qd7bC1cSTBNKG9kqKNvPNkaht4xwu9dCMexD3DSp8gp+XFn9il6GiGVo44zzkU+RiRWMQpC7s0qgnaRCJ7GP+4nu2dfg7yejBuX613g/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 (471 lines)diff --git a/doc/guix.texi b/doc/guix.texiindex 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.mkindex 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.scmnew file mode 100644index 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'+for environment of type '~a'")+ config+ environment))))))))-- 2.22.0
-----BEGIN PGP SIGNATURE-----
iQIzBAEBCAAdFiEEa1VJLOiXAjQ2BGSm9Qb9Fp2P2VoFAl1a0ZcACgkQ9Qb9Fp2P2VoBNg//ezkavJiag7iM5rJ/gaFK4SX+UCBWuXMaNAXD+LqEoR95u1lE+9zBgbigpHJvWKqyUntM3DYmS5nXEp7DWkllUgVbj35jI11nQsO9S4pkJv8uj8jy4Muy7FNNjm/Qlgcp29xwX04uuO9qEjQvd64QCAbM4b7w+2Pq2y/uVNDTRz7vMjLCY1eKf9It8tJ4RecGDPH7gCjdFrl1IW7dQ14LKGHmHwitAjUC18oL73znuxaBhYFsfLSKZwnPinQYJJPUiV5zmKJQBR+GqBdd8SR+tGscSr+ar4YYW4DzdxaALWIk4CXKCJ6+OrEb+bNhAHxyyblK8Vb4vTgtuudJsZ/uVYjxND3Zo9UY/oeEcs4LHg/iwQ2IVte0tBtLb99UI36UY/E5iL/Pplp1/MR5+8PuU5jmvpcIMhIvZQz1gTruJnXN9I1IXkGQxD8nKOn3ykrzxNNrtZsi5GGEYpmgv1Qznf7aTGLS97Rj6hCq86rkvZ5v4n/L/XSXxs3oWBv32UpSfyWBh1+zjV3rSX2qPEvkIZD9JVp5dDT/oih6QM3GBVN6wvYp0FWmmqdoPWPkwVGzolwG3Obn4deWcSImgjRs0rDoJTbr64jCSrwQC2lSFjFIpJ0ULjmHCLydg5SdbkWJFZEKMEaEYvCcLKpCioFbKGZHPFokBH1pYmK83fK5YJo==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 tryagain ‘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 theberlin 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 andthe little bit of time I had to work on this was spent migrating thecode to the newer guile-json API. I will have some time this weekend tosee if it fixes the issue.
Regards,Jakob
-----BEGIN PGP SIGNATURE-----
iQIzBAEBCAAdFiEEa1VJLOiXAjQ2BGSm9Qb9Fp2P2VoFAl1xGJUACgkQ9Qb9Fp2P2Vq1mA//Vpopj61bsSoukzfZn4Q8Wadw/hhadxJk0PyIPtb2KJNMWx+6d0BtXzlaIMmwebI1Qdg1YYkcN3LXdPg+lh9da313jxH6sePz+SEiOSyNldfFuSVAjKAFuFP9OQ+vpa/6+RTrO0emZdYzn2XwgVh+I1r7NHIRuuI3T+2ZQok1Tx4FwOO5cBfQT/QGDQoINiQyMoliHxwIrK2A2+17ZN/zi4kJb3bfS4Rlg94+lVOPwVjDc1T6Im0pFTeTiTbtuSBOTaPUeNoy0jV4Jm+M6sgKcxIUL5p+ZAkpREGf+/b/bsDqiR/SP5MKn4vxutDYRJg9tYFmZnObBIElZSnQ6eBf8SzKeBQPwb0Yt5jk/u+x+V76xy4f0SN68pzbz99oa/hBRaOqZLO80sEoamEoYE8WvUrlE6IIrZ+bEJGRG+htpI2e2o6uLA2raFNxpa7Yu4E686E6/f/5QXdfyhu6Yzft1ZusriqbYYfPKZulh7nTQy8xktK3Pr9p4hc1gqfQr/WbetqXUVvpWCkmrRynZQcyHu1YEitCZyI/IH2maxKoroxY7tstaU76miz/w3kP3q50jK9t5wZIs+gvULvJwB10JYeyNgdnE47w6VEz4Y50YVYDCO8PkG9U4UZ29UdQwansJTKlpHJ90NfdoWEtGvzdrwflvfjWJH3Okzgm/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 isunusual. I'll investigate further.
Regards,Jakob
-----BEGIN PGP SIGNATURE-----
iQIzBAEBCAAdFiEEa1VJLOiXAjQ2BGSm9Qb9Fp2P2VoFAl10DrEACgkQ9Qb9Fp2P2VpBOA//YwIf56z0W2TOkCrFyroKp14jU9jfUVTKppSapEdKhugvUV7Og1f+t0+jDy7xJT/TO9ex/MIXN0NxnnaR7NgfVc5s6hMnwbOB8CZRLMdB2GgXiDqiOd7GCa6uYPHTVeNer6wb/UExZ+00myBEUQ8nlV67W9Mb5HJBdgQttC5Tdahn0M1cSSrYQ/5HU19KF2d36Kkc7X79HqEi9ArVTmWkNhCoj+bKSqp5Qnkgyg6FPrJu6LCzVq5MVs0TSEoh7CYwn1ZBUtiwH8VehUGfhHAdSCWLNMyqd4QyKIOiMdDn9lxot6JYY9vAvf3wgeYx/43WG0rnhxqvNbcNXsxdtapc1w59fx/udKPK/G8P2nC/EFB0NbXvYpZke2XoqY1aaHIoJZV9kU8ucxon5tjxSFQQf0zrqbMelZ5xOclO2xef3faPztrRqhTihHaW1HApN70ozpn5PusqgTIFC6wJwlN78uVhIyCtohsmAyYT8DUh5qmW3WNphwhkN6NJ7jTodd69CxdV0qC+zjXUYGBIG57Y9MDImrH3EnRh5BRQXfuIfJdVZhf+rCjF8zkn1PXAswvdtiC5D/iQMSYweW+QJ4vKSqK6fesiUR0v2yaDVQtpfZNKiMbAHK4UbLIofKFJCpWR9CdGq44nigyxHM74knBUEaLaz2DqZ39U9j7EJE47+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 Ican 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 iton the weekends, but I've reached the point where I have to admit thatI'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 theDigital Ocean droplet based on an API response:
(define (add-static-networking target network) "Return an <operating-system> based on TARGET with a static networkingconfiguration 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 SSHenvironment-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 servicebe giving me this "more than one target service of type 'shepherd-root'"error?
Regards,Jakob
-----BEGIN PGP SIGNATURE-----
iQIzBAEBCAAdFiEEa1VJLOiXAjQ2BGSm9Qb9Fp2P2VoFAl2GjoQACgkQ9Qb9Fp2P2Vr3NA/6AseX9vtQm7Kg/rX6Ga9wUwsPuj7B0zWm4eHU1ybW9eZghjM/vTFA3YOlj3chqYWLJ2hpLjxmDE2iey45cRJJC9hST21q8SZSPxQlrro6LcAgKpzfLpIyyih3jQbbp20RuX5q4PfBO/ZFNcOctsXpZVebBzIJTqSk4udWidcQ+DBArEuHiOQbZ9XwS4J73Ct2qBJJCfxw5ScapTDmvr+EIctZQNVPkIeX+u5ZZKN603KG9A2VtPl1bZBN89xTM/sxgV0FYHiyB/4BmyWjgqK+PLqmDP6FSC7hN8ytLbLEu/7NBol9HXaspEsa7cJV2bdlahZIL+MPE0V6ZNBu26q3Jy3fezFY+UKuyTFPkb9fe6L+RkKkbCFC7gkAQ1pv/Mv7V5geJmeb/fINW0da38Q7Xe6c43uXHP1MWFTghVMwf+ayiPKK31eh1+ErOJNfZzGENuPB8laUKoxSgg4nSsm6vDaeK4l17PqP35r9y82xMDz6fI/K/HFIabd6Jon0yrlQcJwOVy32ZRFQg+SLBvKkPZiQrE6apdbFXp6DHMM17AMB6aae8Ouyz91jTf5wFL6Uw3HA3wCSO9CU8AKD/xqTAm8Zux9rcOJU186RH1RbWAaFI+a4o99QFQWndMzBkq6dhnO4Sxp5ypQNS+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 toseeing 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 justforgot.
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 :) Patchto follow.
Regards,Jakob
-----BEGIN PGP SIGNATURE-----
iQIzBAEBCAAdFiEEa1VJLOiXAjQ2BGSm9Qb9Fp2P2VoFAl2Pxp8ACgkQ9Qb9Fp2P2VoZ8A/9F5CHx72FD+LQCudfVrWLPBVgWbuLzj5m4e1ZyIVCkdk0D0qYqP7wHZ+j6ZOI8fW2aNrNUNs6mMazJEfRDzH1SzvIDhexKgniYFAgRktGdCG/5LWx8pKzwM3pPR3J/u++9GuT0RPhGf27jSKKgJqz2xsqaSm3Z/OqjRYpT7xmekDw6hZcpU2taP1++1jKoZysAOsVhKX+x+beLMRbrpPvI8FqEs5JopS7NDER2G8jV0DrlNeZsmSmEsvKj9SWOlw3JEJaqgpzj38Wnitli3PHA/o7qR6eaDIQNVT9CJ7C9JgDUlT/MZiZ00dVByA1G5pSDRE0Cpo0/fV2efCWqWKEYwF3T62zDBP4Sjin9WVKyCZ7a+MDRLvKsqKSNByMuzuGPHw8JaQvx8LmXyWZj3SDKagaZlEQY7zVWzEDSKWK0q3BP3i6hlb3qCsQXJkO1UIGb9uUL+3WLDTcnAvffFMZHU8b/WDy8FPf5fgIiNZEenLAQIszt2MZZ7tMM9ILRi5wj2QlULiTPpxbS5uU5ZLMK+h4neTfawwXaF9LyqCcyxTrEbE4RXytg7p+norzo/Meta4HcjffpbE/q+0mMuTs8vdkouBS9QP0EUJsiAHlwDVWA+Ecp70fIWjLwA3zqDCizzJlt8Jcz7hVOX7tC2v9c4fji3gAne5/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 (487 lines)diff --git a/doc/guix.texi b/doc/guix.texiindex 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.mkindex 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.scmnew file mode 100644index 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-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'+for environment of type '~a'")+ config+ environment))))))))-- 2.21.0
-----BEGIN PGP SIGNATURE-----
iQIzBAEBCAAdFiEEa1VJLOiXAjQ2BGSm9Qb9Fp2P2VoFAl2PxtkACgkQ9Qb9Fp2P2VprtA/5Af4bASvaWOBjHony2lrWbMRKtVIMOLRng9QzEB8WoAe7iOKoc9n3T7m0HD2PS4HqQRKaavFNVtIJF42BSYPMPph9baNWU/y6Ss+0rV24bF38oiuTmr/nz3bSG/7jiUb5w/DW5A6bSJMhydlGXiLe5b6m8t6pOJ5fuf9toRucOA6DaTnBKQYjVb140rpcZexxkA8lLhV16hLvnpgbOIz4hIi+xohSKyzl7KIotGtMzZ6kjDzFTbN8Wa6QjNxBMLQbiuuRh8/BPrW2K0+JWTQb7CX0SEco2GvnZhgBdq5Xm03XS5cowwjr4A5SJyQW6TKwwjCs77wZhfNeta7SBf4LoIgzNFAlD0noIbUw1l20hY1+W6/nSHZEfCJDM2GllTKYfXRbHiixhMLHXAPCcFNn6yxxaH2unb7PbsjAqf9GHEOT4xLSnNfCa+3fewegTP7zT1iSE7ib3sfb1+ZZng1603Kt/ilpC7mb7vgVojFMFfUv96gv9KMmd4To1MHI/Oa0NpdGOpoEDJ3fzGZVszlr644FFPh+6uhQPB2rnhMrJ+03eD71cQj2cO1SX8NN6dy7OqjxahBA00r/qYUK8UiiPHLSIxOzNP6wJv7Zk3zfn+2WUY8nMgWS1bFS5IVxDJhyiK8u3a2qj9NRLKv23bKWMfBhtVbS8ZSOrImkJFtWlt4==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 Schemerecords 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 torename ‘enable-ipv6’ to ‘enable-ipv6?’ with a question mark as iscustomary. :-)
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 listrecently due to the whole you-know-what spiel, so that email ended upburried in my inbox. Glad it finally made it into master :)
Regards,Jakob
-----BEGIN PGP SIGNATURE-----
iQIzBAEBCAAdFiEEa1VJLOiXAjQ2BGSm9Qb9Fp2P2VoFAl2vbOgACgkQ9Qb9Fp2P2VoHmw/+N60IpjiOCT4n4zrPzRTc3ZWgPiFXkKXzFSa0OO9DIubLphemMvhMwmb78D+qU2FZXz7ImzMxUQ4kyV4PMsyANMY2qlQ+Mo/ez/XNdvyaqYbAbK/e7mp0fxl5gaMlrZpafkYIEk+qGtjEpsJjRdbYou/CfMQKhA908RfocJQxyUIG3vjfwo9yn0aI4wTLPZefEii91t4gFB5j2XM+MZL1BKS7YwT+KDKhNlEdUpEcB3VQNocZqVKOHPk8UYjTCnkOO7zIj1BlfUCZEsbPgdOLSWgPmUU9lmsTcAo0zKnCZGBXAJrJuHlDawOldjdBqv2sDMbZQ9wBjtlqLmIS68VaJhum+LYICWrh13r5wyAQvZ3ozI+MrTqfoJNiEk2/qn2Boi40I020ehLVEM851WsNUOkj1N+/8PWrG8ODvDUTJhFaSqIJlQvPVE+eK8NGYq5SFV+SdtoMjNlQFu6fmKpjcd86HKqDdt3+v/qVdknHFDWU/OZ2jYVnBraucxaAjjJZzvlqbxUQVqboRoIo3KnYzX5B+jDIPTfV6Kk4/7US14NFHUWsW7TJ/BT5JwzQEDTUDIL8vhKIqBkF24HYSvMyQJVbtCH+jTqVxp8ZtbWPTcQBzcBLQmj26eOrYo5WbPsyzm7pr60H1R/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 fixedclose 37083 quit
?
Your comment

This issue is archived.

To comment on this conversation send email to 37083@debbugs.gnu.org