[PATCH] machine: Implement 'hetzner-environment-type'.

  • Done
  • quality assurance status badge
Details
5 participants
  • Ludovic Courtès
  • Maxim Cournoyer
  • pelzflorian (Florian Pelz)
  • Roman Scherer
  • Sergey Trofimov
Owner
unassigned
Submitted by
Roman Scherer
Severity
normal

Debbugs page

Roman Scherer wrote 3 months ago
(address . guix-patches@gnu.org)(name . Roman Scherer)(address . roman@burningswell.com)
6ff52cb81582c81835e39beebc7e6f7f3ecfd81d.1735317980.git.roman@burningswell.com
* gnu/machine/hetzner.scm: New file.
* gnu/local.mk (GNU_SYSTEM_MODULES): Add it.
* guix/ssh.scm (open-ssh-session): Add stricthostkeycheck option.
* doc/guix.texi (Invoking guix deploy): Add documentation for
'hetzner-configuration'.

Change-Id: Idc17dbc33279ecbf3cbfe2c53d7699140f8b9f41
---
doc/guix.texi | 86 ++++
gnu/local.mk | 1 +
gnu/machine/hetzner.scm | 1039 +++++++++++++++++++++++++++++++++++++++
guix/ssh.scm | 19 +-
4 files changed, 1137 insertions(+), 8 deletions(-)
create mode 100644 gnu/machine/hetzner.scm

Toggle diff (514 lines)
diff --git a/doc/guix.texi b/doc/guix.texi
index da4d2f5ebc..020f460327 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -44399,6 +44399,92 @@ Invoking guix deploy
@end table
@end deftp
+@deftp {Data Type} hetzner-configuration
+This is the data type describing the server that should be created for a
+machine with an @code{environment} of @code{hetzner-environment-type}.
+
+@table @asis
+@item @code{allow-downgrades?} (default: @code{#f})
+Whether to allow potential downgrades.
+@item @code{authorize?} (default: @code{#t})
+If true, the coordinator's public signing key
+@code{"/etc/guix/signing-key.pub"} will be added to the server's ACL
+keyring.
+@item @code{build-locally?} (default: @code{#t})
+If false, system derivations will be built on the machine being deployed to.
+@item @code{delete?} (default: @code{#t})
+If true, the server will be deleted when an error happens in the
+provisioning phase. If false, the server will be kept in order to debug
+any issues.
+@item @code{enable-ipv6?} (default: @code{#t})
+If true, attach an IPv6 on the public NIC. If false, no IPv6 address will be attached.
+@item @code{labels} (default: @code{'()})
+A user defined alist of key/value pairs attached to the server. Keys and
+values must be strings. For more information, see
+@uref{https://docs.hetzner.cloud/#labels, Labels}.
+@item @code{location} (default: @code{"fsn1"})
+The name of a @uref{https://docs.hetzner.com/cloud/general/locations,
+location} to create the server in.
+@item @code{cleanup} (default: @code{#t})
+Whether to delete the Hetzner server if provisioning fails or not.
+@item @code{server-type} (default: @code{"cx42"})
+The name of the
+@uref{https://docs.hetzner.com/cloud/servers/overview#server-types,
+server type} this server should be created with.
+@item @code{ssh-key}
+The path to the SSH private key to use to authenticate with the remote
+host.
+@end table
+
+When deploying a machine with the @code{hetzner-environment-type} a
+virtual private server (VPS) is created for it on the
+@uref{https://www.hetzner.com/cloud, Hetzner Cloud} service. The server
+is first booted into the
+@uref{https://docs.hetzner.com/cloud/servers/getting-started/rescue-system,
+Rescue System} to setup the partitions of the server and install a
+minimal Guix system, which is then used with the
+@code{managed-host-environment-type} to complete the deployment.
+
+Servers on the Hetzner Cloud service can be provisioned on the
+@code{aarch64} architecture using UEFI boot mode, or on the
+@code{x86_64} architecture using BIOS boot mode. The @code{(gnu machine
+hetzner)} module exports the @code{%hetzner-os-arm} and
+@code{%hetzner-os-x86} operating systems that are compatible with those
+2 architectures, and can be used as a base for defining your custom
+operating system.
+
+The following example shows the definition of 2 machines that are
+deployed on the Hetzner Cloud service. The first one uses the
+@code{%hetzner-os-arm} operating system to run a server with 16 shared
+vCPUs and 32 GB of RAM on the @code{aarch64} architecture, the second
+one uses the @code{%hetzner-os-x86} operating system on a server with 16
+shared vCPUs and 32 GB of RAM on the @code{x86_64} architecture.
+
+@lisp
+(use-modules (gnu machine)
+ (gnu machine hetzner))
+
+(list (machine
+ (operating-system %hetzner-os-arm)
+ (environment hetzner-environment-type)
+ (configuration (hetzner-configuration
+ (server-type "cax41")
+ (ssh-key "/home/charlie/.ssh/id_rsa"))))
+ (machine
+ (operating-system %hetzner-os-x86)
+ (environment hetzner-environment-type)
+ (configuration (hetzner-configuration
+ (server-type "cpx51")
+ (ssh-key "/home/charlie/.ssh/id_rsa")))))
+@end lisp
+
+Passing this file to @command{guix deploy} with the environment variable
+@env{GUIX_HETZNER_API_TOKEN} set to a valid Hetzner
+@uref{https://docs.hetzner.com/cloud/api/getting-started/generating-api-token,
+API key} should provision 2 machines for you.
+
+@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 84160f407a..98000766af 100644
--- a/gnu/local.mk
+++ b/gnu/local.mk
@@ -911,6 +911,7 @@ if HAVE_GUILE_SSH
GNU_SYSTEM_MODULES += \
%D%/machine/digital-ocean.scm \
+ %D%/machine/hetzner.scm \
%D%/machine/ssh.scm
endif HAVE_GUILE_SSH
diff --git a/gnu/machine/hetzner.scm b/gnu/machine/hetzner.scm
new file mode 100644
index 0000000000..9f8c3806b3
--- /dev/null
+++ b/gnu/machine/hetzner.scm
@@ -0,0 +1,1039 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2024 Roman Scherer <roman@burningswell.com>
+;;;
+;;; 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 hetzner)
+ #:use-module (gnu bootloader grub)
+ #:use-module (gnu bootloader)
+ #:use-module (gnu machine ssh)
+ #:use-module (gnu machine)
+ #:use-module (gnu packages ssh)
+ #:use-module (gnu services base)
+ #:use-module (gnu services networking)
+ #:use-module (gnu services ssh)
+ #:use-module (gnu services)
+ #:use-module (gnu system file-systems)
+ #:use-module (gnu system image)
+ #:use-module (gnu system linux-initrd)
+ #:use-module (gnu system pam)
+ #:use-module (gnu system)
+ #:use-module (guix base32)
+ #:use-module (guix colors)
+ #:use-module (guix derivations)
+ #:use-module (guix diagnostics)
+ #:use-module (guix gexp)
+ #:use-module (guix i18n)
+ #:use-module (guix import json)
+ #:use-module (guix monads)
+ #:use-module (guix packages)
+ #:use-module (guix pki)
+ #:use-module (guix records)
+ #:use-module (guix ssh)
+ #:use-module (guix store)
+ #:use-module (ice-9 format)
+ #:use-module (ice-9 iconv)
+ #:use-module (ice-9 match)
+ #:use-module (ice-9 popen)
+ #:use-module (ice-9 pretty-print)
+ #:use-module (ice-9 rdelim)
+ #:use-module (ice-9 receive)
+ #:use-module (ice-9 string-fun)
+ #:use-module (ice-9 textual-ports)
+ #: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 channel)
+ #:use-module (ssh key)
+ #:use-module (ssh popen)
+ #:use-module (ssh session)
+ #: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 (%hetzner-os-arm
+ %hetzner-os-x86
+ deploy-hetzner
+ hetzner-api
+ hetzner-api-auth-token
+ hetzner-api-base-url
+ hetzner-configuration
+ hetzner-configuration-allow-downgrades?
+ hetzner-configuration-authorize?
+ hetzner-configuration-build-locally?
+ hetzner-configuration-delete?
+ hetzner-configuration-enable-ipv6?
+ hetzner-configuration-labels
+ hetzner-configuration-location
+ hetzner-configuration-networks
+ hetzner-configuration-server-type
+ hetzner-configuration-ssh-key
+ hetzner-configuration?
+ hetzner-environment-type))
+
+;;; Commentary:
+;;;
+;;; This module implements a high-level interface for provisioning "servers"
+;;; from the Hetzner Cloud service.
+;;;
+
+(define %hetzner-api-token
+ (make-parameter (getenv "GUIX_HETZNER_API_TOKEN")))
+
+
+;;;
+;;; Hetzner operating systems.
+;;;
+
+;; Operating system for arm servers using UEFI boot mode.
+
+(define %hetzner-os-arm
+ (operating-system
+ (host-name "guix-arm")
+ (bootloader
+ (bootloader-configuration
+ (bootloader grub-efi-bootloader)
+ (targets (list "/boot/efi"))
+ (terminal-outputs '(console))))
+ (file-systems
+ (cons* (file-system
+ (mount-point "/")
+ (device "/dev/sda1")
+ (type "ext4"))
+ (file-system
+ (mount-point "/boot/efi")
+ (device "/dev/sda15")
+ (type "vfat"))
+ %base-file-systems))
+ (initrd-modules
+ (cons* "sd_mod" "virtio_scsi" %base-initrd-modules))
+ (services
+ (cons* (service dhcp-client-service-type)
+ (service openssh-service-type
+ (openssh-configuration
+ (openssh openssh-sans-x)
+ (permit-root-login 'prohibit-password)))
+ %base-services))))
+
+;; Operating system for x86 servers using BIOS boot mode.
+
+(define %hetzner-os-x86
+ (operating-system
+ (inherit %hetzner-os-arm)
+ (host-name "guix-x86")
+ (bootloader
+ (bootloader-configuration
+ (bootloader grub-bootloader)
+ (targets (list "/dev/sda"))
+ (terminal-outputs '(console))))
+ (initrd-modules
+ (cons "virtio_scsi" %base-initrd-modules))
+ (file-systems
+ (cons (file-system
+ (mount-point "/")
+ (device "/dev/sda1")
+ (type "ext4"))
+ %base-file-systems))))
+
+(define (operating-system-authorize os)
+ "Authorize the OS with the public signing key of the current machine."
+ (if (file-exists? %public-key-file)
+ (operating-system
+ (inherit os)
+ (services
+ (modify-services (operating-system-user-services os)
+ (guix-service-type
+ config => (guix-configuration
+ (inherit config)
+ (authorized-keys
+ (cons*
+ (local-file %public-key-file)
+ (guix-configuration-authorized-keys config))))))))
+ (raise (formatted-message (G_ "no signing key '~a'. \
+Have you run 'guix archive --generate-key'?")
+ %public-key-file))))
+
+(define (operating-system-root-file-system-type os)
+ "Return the root file system type of the operating system OS."
+ (let ((root-fs (find (lambda (file-system)
+ (equal? "/" (file-system-mount-point file-system)))
+ (operating-system-file-systems os))))
+ (if (file-system? root-fs)
+ (file-system-type root-fs)
+ (raise (formatted-message
+ (G_ "could not determine root file system type"))))))
+
+
+;;;
+;;; Helper functions.
+;;;
+
+(define (escape-backticks str)
+ "Escape all backticks in STR."
+ (string-replace-substring str "`" "\\`"))
+
+(define (format-query-param param)
+ "Format the query PARAM as a string."
+ (string-append (uri-encode (format #f "~a" (car param))) "="
+ (uri-encode (format #f "~a" (cdr param)))))
+
+(define (format-query-params params)
+ "Format the query PARAMS as a string."
+ (if (> (length params) 0)
+ (string-append
+ "?"
+ (string-join
+ (map format-query-param params)
+ "&"))
+ ""))
+
+
+
+;;;
+;;; Hetzner API response.
+;;;
+
+(define-record-type* <hetzner-api-response> hetzner-api-response
+ make-hetzner-api-response hetzner-api-response? hetzner-api-response
+ (body hetzner-api-response-body)
+ (headers hetzner-api-response-headers)
+ (status hetzner-api-response-status))
+
+(define (hetzner-api-response-meta response)
+ "Return the meta information of the Hetzner API response."
+ (assoc-ref (hetzner-api-response-body response) "meta"))
+
+(define (hetzner-api-response-pagination response)
+ "Return the meta information of the Hetzner API response."
+ (assoc-ref (hetzner-api-response-meta response) "pagination"))
+
+(define (hetzner-api-response-pagination-combine resource responses)
+ "Combine multiple Hetzner API pagination responses into a single response."
+ (if (positive? (length responses))
+ (let* ((response (car responses))
+ (pagination (hetzner-api-response-pagination response))
+ (total-entries (assoc-ref pagination "total_entries")))
+ (hetzner-api-response
+ (inherit response)
+ (body `(("meta"
+ ("pagination"
+ ("last_page" . 1)
+ ("next_page" . null)
+ ("page" . 1)
+ ("per_page" . ,total-entries)
+ ("previous_page" . null)
+ ("total_entries" . ,total-entries)))
+ (,resource . ,(append-map
+ (lambda (body)
+ (vector->list (assoc-ref body resource)))
+ (map hetzner-api-response-body responses)))))))
+ (raise (formatted-message
+ (G_ "Expected a list of Hetzner API responses")))))
+
+(define (hetzner-api-response-read port)
+ "Read the Hetzner API response from PORT."
+ (let* ((response (read-response port))
+ (body (read-response-body response)))
+ (hetzner-api-response
+ (body (json-string->scm (bytevector->string body "UTF-8")))
+ (headers (response-headers response))
+ (status (response-code response)))))
+
+(define (hetzner-api-response-validate-status response expected)
+ "Raise an error if the HTTP status code of RESPONSE is not in EXPECTED."
+ (when (not (member (hetzner-api-response-status response) expected))
+ (raise (formatted-message
+ (G_ "Unexpected HTTP status code: ~a, expected: ~a~%~a")
+ (hetzner-api-response-status response)
+ expected
+ (hetzner-api-response-body response)))))
+
+
+
+;;;
+;;; Hetzner API request.
+;;;
+
+(define-record-type* <hetzner-api-request> hetzner-api-request
+ make-hetzner-api-request hetzner-api-request? hetzner-api-request
+ (body hetzner-api-request-body (default *unspecified*))
+ (headers hetzner-api-request-headers (default '()))
+ (method hetzner-api-request-method (default 'GET))
+ (params hetzner-api-request-params (default '()))
+ (url hetzner-api-request-url))
+
+(define (hetzner-api-request-uri request)
+ "Return the URI object of the Hetzner API request."
+ (let ((params (hetzner-api-request-params request)))
+ (string->uri (string-append (hetzner-api-request-url request)
+ (format-query-params params)))))
+
+(define (hetzner-api-request-body-bytevector request)
+ "Return the body of the Hetzner API REQUEST as a bytevector."
+ (let* ((body (hetzner-api-request-body request))
+ (string (if (unspecified? body) "" (scm->json-string body))))
+ (string->bytevector string "UTF-8")))
+
+(define (hetzner-api-request-write port request)
+ "Write the Hetzner API REQUEST to PORT."
+ (let* ((body (hetzner-api-request-body-bytevector request))
+ (request (build-request
+ (hetzner-api-request-uri request)
+ #:method (hetzner-api-request-method request)
+ #:version '(1 . 1)
+ #:headers (cons* `(Content-Length
+ . ,(number->string
+ (if (unspecified? body)
+ 0 (bytevector-length body))))
+ (hetzner-api-request-headers request))
+ #:port port))
+ (request (write-request request port)))
+ (unless (unspecified? body)
+ (write-request-body request body))
+ (force-output (request-port request))))
+
+(define* (hetzner-api-request-send request #:key (expected (list 200 201)))
+ "Send the Hetzner API REQUEST via HTTP."
+ (let ((port (open-socket-for-uri (hetzner-api-request-uri request))))
+ (hetzner-api-request-write port request)
+ (let ((response (hetzner-api-response-read port)))
+ (close-port port)
+ (hetzner-api-response-validate-status response expected)
+ response)))
+
+(define (hetzner-api-request-next-params request)
+ "Return the pagination params for the next page of the REQUEST."
+ (let* ((params (hetzner-api-request-params request))
+ (page (or (assoc-ref params "page") 1)))
+ (map (lambda (param)
+ (if (equal? "page" (car param))
+ (cons (car param) (+ page 1))
+ param))
+ params)))
+
+(define (hetzner-api-request-paginate request)
+ "Fetch all pages of the REQUEST via pagination and return all responses."
+ (let* ((response (hetzner-api-request-send request))
+ (pagination (hetzner-api-response-pagination response))
+ (next-page (assoc-ref pagination "next_page")))
+ (if (number? next-page)
+ (cons response
+ (hetzner-api-request-paginate
+ (hetzner-api-request
+ (inherit request)
+ (params (hetzner-api-request-next-params request)))))
+ (list response))))
+
+
+
+;;;
+;;; Hetzner API.
+;;;
+
+(define-record-type* <hetzner-api> hetzner-api
+ make-hetzner-api hetzner-api? hetzner-api
+ (auth-token hetzner-api-auth-token ; string
+ (default (%hetzner-api-token)))
+ (base-url hetzner-api-base-url ; string
+ (default "https://api.hetzner.cloud/v1")))
+
+(define (hetzner-api-authorization-header api)
+ "Return the authorization header the Hetzner API."
+ (format #f "Bearer ~a" (hetzner-api-auth-token api)))
+
+(define (hetzner-api-default-headers api)
+ "Returns the default headers of the Hetzner API."
+ `((user-agent . "Guix Deploy")
+ (Accept . "application/json")
+ (Authorization . ,(hetzner-api-authorization-header api))
+ (Content-Type . "application/json")))
+
+(define (hetzner-api-url api path)
+ "Append PATH to the base url of the Hetzner API."
+ (string-append (hetzner-api-base-url api) path))
+
+(define (hetzner-api-delete api path)
+ "Delelte the resource at PATH with the Hetzner API."
+ (hetzner-api-request-send
+ (hetzner-api-request
+ (headers (hetzner-api-default-headers api))
+ (method 'DELETE)
+ (url (hetzner-api-url api path)))))
+
+(define* (hetzner-api-list api path resources #:key (params '()))
+ "Fetch all objects of RESOURCE from the Hetzner API."
+ (assoc-ref (hetzner-api-response-body
+ (hetzner-api-response-pagination-combine
+ resources (hetzner-api-request-paginate
+ (hetzner-api-request
+ (url (hetzner-api-url api path))
+ (headers (hetzner-api-default-headers api))
+ (params (cons '("page" . 1) params))))))
+ resources))
+
+(define* (hetzner-api-post api path #:key (body *unspecified*))
+ "Send a POST request to the Hetzner API at PATH using BODY."
+ (hetzner-api-response-body
+ (hetzner-api-request-send
+ (hetzner-api-request
+ (body body)
+ (method 'POST)
+ (url (hetz
This message was truncated. Download the full message here.
Ludovic Courtès wrote 2 months ago
(name . Roman Scherer)(address . roman@burningswell.com)(name . Josselin Poiret)(address . dev@jpoiret.xyz)(name . Maxim Cournoyer)(address . maxim.cournoyer@gmail.com)(name . Simon Tournier)(address . zimon.toutoune@gmail.com)(name . Mathieu Othacehe)(address . othacehe@gnu.org)(name . Tobias Geerinckx-Rice)(address . me@tobias.gr)(name . Christopher Baines)(address . guix@cbaines.net)(address . 75144@debbugs.gnu.org)
8734hi1mdh.fsf@gnu.org
Hello Roman,

Roman Scherer <roman@burningswell.com> skribis:

Toggle quote (8 lines)
> * gnu/machine/hetzner.scm: New file.
> * gnu/local.mk (GNU_SYSTEM_MODULES): Add it.
> * guix/ssh.scm (open-ssh-session): Add stricthostkeycheck option.
> * doc/guix.texi (Invoking guix deploy): Add documentation for
> 'hetzner-configuration'.
>
> Change-Id: Idc17dbc33279ecbf3cbfe2c53d7699140f8b9f41

Thumbs up for this big piece of work, one that I think is important for
the project! ‘guix deploy’ is a great idea but it desperately needs
more backends like this one.

I’m not familiar with Hetzner so I’ll comment on more general aspects.
Chris, perhaps you can provide feedback on Hetzner-specific issues? I
think we could put this backend to good use for Guix infra since a few
services are running at Hetzner.

Toggle quote (4 lines)
> +@deftp {Data Type} hetzner-configuration
> +This is the data type describing the server that should be created for a
> +machine with an @code{environment} of @code{hetzner-environment-type}.

Could you add a sentence providing more context like:

It allows you to configure deployment to a @acronym{VPS, virtual
private server} hosted by @uref{https://www.hetzner.com, Hetzner}.

Toggle quote (3 lines)
> +@item @code{authorize?} (default: @code{#t})
> +If true, the coordinator's public signing key

“coordinator” has nothing to do here I guess.

Toggle quote (5 lines)
> +@item @code{labels} (default: @code{'()})
> +A user defined alist of key/value pairs attached to the server. Keys and
> +values must be strings. For more information, see
> +@uref{https://docs.hetzner.cloud/#labels, Labels}.

Maybe add a short example?

Toggle quote (4 lines)
> +@item @code{location} (default: @code{"fsn1"})
> +The name of a @uref{https://docs.hetzner.com/cloud/general/locations,
> +location} to create the server in.

Maybe add: “For example, @code{"fsn1"} corresponds to the Hetzner site
in Falkenstein, Germany, while @code{"sin"} corresponds to its site in
Singapore.”

Toggle quote (5 lines)
> +@item @code{server-type} (default: @code{"cx42"})
> +The name of the
> +@uref{https://docs.hetzner.com/cloud/servers/overview#server-types,
> +server type} this server should be created with.

Likewise, an example would be elcome.

Toggle quote (4 lines)
> +@item @code{ssh-key}
> +The path to the SSH private key to use to authenticate with the remote
> +host.

s/path to/file name of/

Toggle quote (2 lines)
> +The following example shows the definition of 2 machines that are

s/2/two/

Toggle quote (2 lines)
> +vCPUs and 32 GB of RAM on the @code{aarch64} architecture, the second

s/@code{aarch64}/AArch64/

Toggle quote (2 lines)
> +shared vCPUs and 32 GB of RAM on the @code{x86_64} architecture.

Drop @code.

Toggle quote (17 lines)
> +@lisp
> +(use-modules (gnu machine)
> + (gnu machine hetzner))
> +
> +(list (machine
> + (operating-system %hetzner-os-arm)
> + (environment hetzner-environment-type)
> + (configuration (hetzner-configuration
> + (server-type "cax41")
> + (ssh-key "/home/charlie/.ssh/id_rsa"))))
> + (machine
> + (operating-system %hetzner-os-x86)
> + (environment hetzner-environment-type)
> + (configuration (hetzner-configuration
> + (server-type "cpx51")
> + (ssh-key "/home/charlie/.ssh/id_rsa")))))

Nice!

Toggle quote (2 lines)
> +API key} should provision 2 machines for you.

s/2/two/

Toggle quote (2 lines)
> + #:use-module (ice-9 receive)

The code base preferable uses SRFI-71 for multiple-value returns.

Toggle quote (3 lines)
> + (raise (formatted-message
> + (G_ "Expected a list of Hetzner API responses")))))

Messages should start with a lower-case letter (for all the messages in
this file).

Please add the file to ‘po/guix/POTFILES.in’ so that it’s actually
subject to translation.

Toggle quote (7 lines)
> +(define (hetzner-api-response-read port)
> + "Read the Hetzner API response from PORT."
> + (let* ((response (read-response port))
> + (body (read-response-body response)))
> + (hetzner-api-response
> + (body (json-string->scm (bytevector->string body "UTF-8")))

Just ‘string->utf8’ (shorter).

More importantly: instead of ‘json-string->scm’ (which gives an alist,
leading to ‘assoc-ref’ calls all over the code base along with free-form
alists, which is very error-prone), could you use ‘define-json-mapping’?

In essence it’s like ‘define-record-type’ but it additionally define how
to map a JSON dictionary to a Scheme record. There are several examples
in Guix, such as (guix swh).

For clarity, it might be useful to move all the hetzner-api-* bits to a
separate module, for example (gnu machine hetzner http). WDYT?


The rest of the code looks nice to me (modulo alists :-)) but that’s
about all I can say. It’s quite a significant body of code. What would
you suggest to prevent bitrot and support maintenance? Are there parts
of it that could be usefully tested automatically, possibly by mocking
part of the Hetzner API? Or are there tips on how you tested it that
could be written down in the file itself?


Could you move the (guix ssh) bits to a separate patch?

Toggle quote (12 lines)
> +++ b/guix/ssh.scm
> @@ -103,7 +103,8 @@ (define* (open-ssh-session host #:key user port identity
> host-key
> (compression %compression)
> (timeout 3600)
> - (connection-timeout 10))
> + (connection-timeout 10)
> + (stricthostkeycheck #t))
> "Open an SSH session for HOST and return it. IDENTITY specifies the file
> name of a private key to use for authenticating with the host. When USER,
> PORT, or IDENTITY are #f, use default values or whatever '~/.ssh/config'

Please update the docstring.

Rather ‘strict-host-key-check?’ to match naming conventions, even if
Guile-SSH calls it that way.

Toggle quote (8 lines)
> @@ -137,7 +138,8 @@ (define* (open-ssh-session host #:key user port identity
>
> ;; Speed up RPCs by creating sockets with
> ;; TCP_NODELAY.
> - #:nodelay #t)))
> + #:nodelay #t
> + #:stricthostkeycheck stricthostkeycheck)))

Not sure what this does actually. Looks like the main part is the
“when stricthostkeycheck” condition that comes below, no?

Could you send a second version?

Thank you!

Ludo’.
Ludovic Courtès wrote 2 months ago
(name . Roman Scherer)(address . roman@burningswell.com)(name . Josselin Poiret)(address . dev@jpoiret.xyz)(name . Maxim Cournoyer)(address . maxim.cournoyer@gmail.com)(name . Simon Tournier)(address . zimon.toutoune@gmail.com)(name . Mathieu Othacehe)(address . othacehe@gnu.org)(name . Tobias Geerinckx-Rice)(address . me@tobias.gr)(name . Christopher Baines)(address . guix@cbaines.net)(address . 75144@debbugs.gnu.org)
871px21mcg.fsf@gnu.org
Hello Roman,

Roman Scherer <roman@burningswell.com> skribis:

Toggle quote (8 lines)
> * gnu/machine/hetzner.scm: New file.
> * gnu/local.mk (GNU_SYSTEM_MODULES): Add it.
> * guix/ssh.scm (open-ssh-session): Add stricthostkeycheck option.
> * doc/guix.texi (Invoking guix deploy): Add documentation for
> 'hetzner-configuration'.
>
> Change-Id: Idc17dbc33279ecbf3cbfe2c53d7699140f8b9f41

Thumbs up for this big piece of work, one that I think is important for
the project! ‘guix deploy’ is a great idea but it desperately needs
more backends like this one.

I’m not familiar with Hetzner so I’ll comment on more general aspects.
Chris, perhaps you can provide feedback on Hetzner-specific issues? I
think we could put this backend to good use for Guix infra since a few
services are running at Hetzner.

Toggle quote (4 lines)
> +@deftp {Data Type} hetzner-configuration
> +This is the data type describing the server that should be created for a
> +machine with an @code{environment} of @code{hetzner-environment-type}.

Could you add a sentence providing more context like:

It allows you to configure deployment to a @acronym{VPS, virtual
private server} hosted by @uref{https://www.hetzner.com, Hetzner}.

Toggle quote (3 lines)
> +@item @code{authorize?} (default: @code{#t})
> +If true, the coordinator's public signing key

“coordinator” has nothing to do here I guess.

Toggle quote (5 lines)
> +@item @code{labels} (default: @code{'()})
> +A user defined alist of key/value pairs attached to the server. Keys and
> +values must be strings. For more information, see
> +@uref{https://docs.hetzner.cloud/#labels, Labels}.

Maybe add a short example?

Toggle quote (4 lines)
> +@item @code{location} (default: @code{"fsn1"})
> +The name of a @uref{https://docs.hetzner.com/cloud/general/locations,
> +location} to create the server in.

Maybe add: “For example, @code{"fsn1"} corresponds to the Hetzner site
in Falkenstein, Germany, while @code{"sin"} corresponds to its site in
Singapore.”

Toggle quote (5 lines)
> +@item @code{server-type} (default: @code{"cx42"})
> +The name of the
> +@uref{https://docs.hetzner.com/cloud/servers/overview#server-types,
> +server type} this server should be created with.

Likewise, an example would be elcome.

Toggle quote (4 lines)
> +@item @code{ssh-key}
> +The path to the SSH private key to use to authenticate with the remote
> +host.

s/path to/file name of/

Toggle quote (2 lines)
> +The following example shows the definition of 2 machines that are

s/2/two/

Toggle quote (2 lines)
> +vCPUs and 32 GB of RAM on the @code{aarch64} architecture, the second

s/@code{aarch64}/AArch64/

Toggle quote (2 lines)
> +shared vCPUs and 32 GB of RAM on the @code{x86_64} architecture.

Drop @code.

Toggle quote (17 lines)
> +@lisp
> +(use-modules (gnu machine)
> + (gnu machine hetzner))
> +
> +(list (machine
> + (operating-system %hetzner-os-arm)
> + (environment hetzner-environment-type)
> + (configuration (hetzner-configuration
> + (server-type "cax41")
> + (ssh-key "/home/charlie/.ssh/id_rsa"))))
> + (machine
> + (operating-system %hetzner-os-x86)
> + (environment hetzner-environment-type)
> + (configuration (hetzner-configuration
> + (server-type "cpx51")
> + (ssh-key "/home/charlie/.ssh/id_rsa")))))

Nice!

Toggle quote (2 lines)
> +API key} should provision 2 machines for you.

s/2/two/

Toggle quote (2 lines)
> + #:use-module (ice-9 receive)

The code base preferable uses SRFI-71 for multiple-value returns.

Toggle quote (3 lines)
> + (raise (formatted-message
> + (G_ "Expected a list of Hetzner API responses")))))

Messages should start with a lower-case letter (for all the messages in
this file).

Please add the file to ‘po/guix/POTFILES.in’ so that it’s actually
subject to translation.

Toggle quote (7 lines)
> +(define (hetzner-api-response-read port)
> + "Read the Hetzner API response from PORT."
> + (let* ((response (read-response port))
> + (body (read-response-body response)))
> + (hetzner-api-response
> + (body (json-string->scm (bytevector->string body "UTF-8")))

Just ‘string->utf8’ (shorter).

More importantly: instead of ‘json-string->scm’ (which gives an alist,
leading to ‘assoc-ref’ calls all over the code base along with free-form
alists, which is very error-prone), could you use ‘define-json-mapping’?

In essence it’s like ‘define-record-type’ but it additionally define how
to map a JSON dictionary to a Scheme record. There are several examples
in Guix, such as (guix swh).

For clarity, it might be useful to move all the hetzner-api-* bits to a
separate module, for example (gnu machine hetzner http). WDYT?


The rest of the code looks nice to me (modulo alists :-)) but that’s
about all I can say. It’s quite a significant body of code. What would
you suggest to prevent bitrot and support maintenance? Are there parts
of it that could be usefully tested automatically, possibly by mocking
part of the Hetzner API? Or are there tips on how you tested it that
could be written down in the file itself?


Could you move the (guix ssh) bits to a separate patch?

Toggle quote (12 lines)
> +++ b/guix/ssh.scm
> @@ -103,7 +103,8 @@ (define* (open-ssh-session host #:key user port identity
> host-key
> (compression %compression)
> (timeout 3600)
> - (connection-timeout 10))
> + (connection-timeout 10)
> + (stricthostkeycheck #t))
> "Open an SSH session for HOST and return it. IDENTITY specifies the file
> name of a private key to use for authenticating with the host. When USER,
> PORT, or IDENTITY are #f, use default values or whatever '~/.ssh/config'

Please update the docstring.

Rather ‘strict-host-key-check?’ to match naming conventions, even if
Guile-SSH calls it that way.

Toggle quote (8 lines)
> @@ -137,7 +138,8 @@ (define* (open-ssh-session host #:key user port identity
>
> ;; Speed up RPCs by creating sockets with
> ;; TCP_NODELAY.
> - #:nodelay #t)))
> + #:nodelay #t
> + #:stricthostkeycheck stricthostkeycheck)))

Not sure what this does actually. Looks like the main part is the
“when stricthostkeycheck” condition that comes below, no?

Could you send a second version?

Thank you!

Ludo’.
Roman Scherer wrote 2 months ago
(name . Ludovic Courtès)(address . ludo@gnu.org)(name . Roman Scherer)(address . roman@burningswell.com)(name . Maxim Cournoyer)(address . maxim.cournoyer@gmail.com)(name . Simon Tournier)(address . zimon.toutoune@gmail.com)(name . Mathieu Othacehe)(address . othacehe@gnu.org)(name . Tobias Geerinckx-Rice)(address . me@tobias.gr)(name . Josselin Poiret)(address . dev@jpoiret.xyz)(name . Christopher Baines)(address . guix@cbaines.net)(address . 75144@debbugs.gnu.org)
868qr6n3j9.fsf@burningswell.com
Hi Ludo,

thanks for your review. Here is a v2, I hope I addressed your previous
comments with it, but I need some help.

As you suggested I also added some tests. Some use mocking, and some run
against the Hetzner API, if the GUIX_HETZNER_API_TOKEN env var is set.

./pre-inst-env make check TESTS="tests/machine/hetzner/http.scm"
./pre-inst-env make check TESTS="tests/machine/hetzner.scm"

All tests pass when I run them in the Geiser REPL, where I developed them.

But I have some trouble with one test that uses mocking. The
"deploy-machine-mock-with-unprovisioned-server" test in
tests/machine/hetzner.scm only fails when run in the terminal. :?

I'm using the "mock" function from (guix tests) to mock some HTTP and SSH
calls. The issue is that I see different behaviour whether I run the tests in
Geiser vs in the Terminal.

In Geiser I see the following output for this test, in it passes:

-------------------------------------------------------------------------------
creating 'cx42' server for 'guix-x86'...
successfully created 'cx42' x86 server for 'guix-x86'
enabling rescue system on 'guix-x86'...
MOCK ENABLE RESUCE
successfully enabled rescue system on 'guix-x86'
powering on server for 'guix-x86'...
MOCK POWER ON
successfully powered on server for 'guix-x86'
connecting via SSH to '1.2.3.4' using '/tmp/guix-hetzner-machine-test-key'...
MOCK OPEN SSH SESSION
installing rescue system packages on 'guix-x86'...
MOCK RUNNING SCRIPT: /tmp/guix/deploy/hetzner-machine-rescue-install-packages
successfully installed rescue system packages on 'guix-x86'
setting up partitions on 'guix-x86'...
MOCK RUNNING SCRIPT: /tmp/guix/deploy/hetzner-machine-rescue-partition
successfully setup partitions on 'guix-x86'
installing guix operating system on 'guix-x86'...
MOCK RUNNING SCRIPT: /tmp/guix/deploy/hetzner-machine-rescue-install-os
successfully installed guix operating system on 'guix-x86'
rebooting server for 'guix-x86'...
successfully rebooted server for 'guix-x86'
connecting via SSH to '1.2.3.4' using '/tmp/guix-hetzner-machine-test-key'...
MOCK OPEN SSH SESSION
-------------------------------------------------------------------------------

You can see that calls to "hetzner-machine-ssh-run-script" are mocked, because
"MOCK RUNNING SCRIPT" is printed multiple times.

But in a "guix shell -D" terminal I see the following output for the test, and
it is failing:

-------------------------------------------------------------------------------

creating 'cx42' server for 'guix-x86'...
successfully created 'cx42' x86 server for 'guix-x86'
enabling rescue system on 'guix-x86'...
MOCK ENABLE RESUCE
successfully enabled rescue system on 'guix-x86'
powering on server for 'guix-x86'...
MOCK POWER ON
successfully powered on server for 'guix-x86'
connecting via SSH to '1.2.3.4' using '/tmp/guix-hetzner-machine-test-key'...
MOCK OPEN SSH SESSION
installing rescue system packages on 'guix-x86'...
test-name: deploy-machine-mock-with-unprovisioned-server
location: /home/roman/workspace/guix/tests/machine/hetzner.scm:189

actual-value: #f
actual-error:
+ (guile-ssh-error
+ "%gssh-make-sftp-session"
+ "Could not create a SFTP session"
+ #<session #<undefined>@1.2.3.4:22 (disconnected) ffff85596de0>
+ #f)
result: FAIL

;;; [2025/01/19 17:39:16.791023, 0] [GSSH ERROR] Could not create a SFTP session: #<session #<undefined>@1.2.3.4:22 (disconnected) ffff85596de0>

-------------------------------------------------------------------------------

The tests fails here trying to use a disconnected SSH session object, that I
returned in a mocked call. This code should actually never be reached, because
I mock the "hetzner-machine-ssh-run-script" call. But for some reason the mock
is not working here. The "MOCK RUNNING SCRIPT" output is missing.

Do you have any ideas what could be going on here? I suspect this might be due
to some optimization or env issue, but I'm pretty lost.

I attached a WIP v2 for now. Will send a v3 and a separate patch for the ssh
modification once I fixed this mock test.

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

Toggle quote (175 lines)
> Hello Roman,
>
> Roman Scherer <roman@burningswell.com> skribis:
>
>> * gnu/machine/hetzner.scm: New file.
>> * gnu/local.mk (GNU_SYSTEM_MODULES): Add it.
>> * guix/ssh.scm (open-ssh-session): Add stricthostkeycheck option.
>> * doc/guix.texi (Invoking guix deploy): Add documentation for
>> 'hetzner-configuration'.
>>
>> Change-Id: Idc17dbc33279ecbf3cbfe2c53d7699140f8b9f41
>
> Thumbs up for this big piece of work, one that I think is important for
> the project! ‘guix deploy’ is a great idea but it desperately needs
> more backends like this one.
>
> I’m not familiar with Hetzner so I’ll comment on more general aspects.
> Chris, perhaps you can provide feedback on Hetzner-specific issues? I
> think we could put this backend to good use for Guix infra since a few
> services are running at Hetzner.
>
>> +@deftp {Data Type} hetzner-configuration
>> +This is the data type describing the server that should be created for a
>> +machine with an @code{environment} of @code{hetzner-environment-type}.
>
> Could you add a sentence providing more context like:
>
> It allows you to configure deployment to a @acronym{VPS, virtual
> private server} hosted by @uref{https://www.hetzner.com, Hetzner}.
>
>> +@item @code{authorize?} (default: @code{#t})
>> +If true, the coordinator's public signing key
>
> “coordinator” has nothing to do here I guess.
>
>> +@item @code{labels} (default: @code{'()})
>> +A user defined alist of key/value pairs attached to the server. Keys and
>> +values must be strings. For more information, see
>> +@uref{https://docs.hetzner.cloud/#labels, Labels}.
>
> Maybe add a short example?
>
>> +@item @code{location} (default: @code{"fsn1"})
>> +The name of a @uref{https://docs.hetzner.com/cloud/general/locations,
>> +location} to create the server in.
>
> Maybe add: “For example, @code{"fsn1"} corresponds to the Hetzner site
> in Falkenstein, Germany, while @code{"sin"} corresponds to its site in
> Singapore.”
>
>> +@item @code{server-type} (default: @code{"cx42"})
>> +The name of the
>> +@uref{https://docs.hetzner.com/cloud/servers/overview#server-types,
>> +server type} this server should be created with.
>
> Likewise, an example would be elcome.
>
>> +@item @code{ssh-key}
>> +The path to the SSH private key to use to authenticate with the remote
>> +host.
>
> s/path to/file name of/
>
>> +The following example shows the definition of 2 machines that are
>
> s/2/two/
>
>> +vCPUs and 32 GB of RAM on the @code{aarch64} architecture, the second
>
> s/@code{aarch64}/AArch64/
>
>> +shared vCPUs and 32 GB of RAM on the @code{x86_64} architecture.
>
> Drop @code.
>
>> +@lisp
>> +(use-modules (gnu machine)
>> + (gnu machine hetzner))
>> +
>> +(list (machine
>> + (operating-system %hetzner-os-arm)
>> + (environment hetzner-environment-type)
>> + (configuration (hetzner-configuration
>> + (server-type "cax41")
>> + (ssh-key "/home/charlie/.ssh/id_rsa"))))
>> + (machine
>> + (operating-system %hetzner-os-x86)
>> + (environment hetzner-environment-type)
>> + (configuration (hetzner-configuration
>> + (server-type "cpx51")
>> + (ssh-key "/home/charlie/.ssh/id_rsa")))))
>
> Nice!
>
>> +API key} should provision 2 machines for you.
>
> s/2/two/
>
>> + #:use-module (ice-9 receive)
>
> The code base preferable uses SRFI-71 for multiple-value returns.
>
>> + (raise (formatted-message
>> + (G_ "Expected a list of Hetzner API responses")))))
>
> Messages should start with a lower-case letter (for all the messages in
> this file).
>
> Please add the file to ‘po/guix/POTFILES.in’ so that it’s actually
> subject to translation.
>
>> +(define (hetzner-api-response-read port)
>> + "Read the Hetzner API response from PORT."
>> + (let* ((response (read-response port))
>> + (body (read-response-body response)))
>> + (hetzner-api-response
>> + (body (json-string->scm (bytevector->string body "UTF-8")))
>
> Just ‘string->utf8’ (shorter).
>
> More importantly: instead of ‘json-string->scm’ (which gives an alist,
> leading to ‘assoc-ref’ calls all over the code base along with free-form
> alists, which is very error-prone), could you use ‘define-json-mapping’?
>
> In essence it’s like ‘define-record-type’ but it additionally define how
> to map a JSON dictionary to a Scheme record. There are several examples
> in Guix, such as (guix swh).
>
> For clarity, it might be useful to move all the hetzner-api-* bits to a
> separate module, for example (gnu machine hetzner http). WDYT?
>
>
> The rest of the code looks nice to me (modulo alists :-)) but that’s
> about all I can say. It’s quite a significant body of code. What would
> you suggest to prevent bitrot and support maintenance? Are there parts
> of it that could be usefully tested automatically, possibly by mocking
> part of the Hetzner API? Or are there tips on how you tested it that
> could be written down in the file itself?
>
>
> Could you move the (guix ssh) bits to a separate patch?
>
>> +++ b/guix/ssh.scm
>> @@ -103,7 +103,8 @@ (define* (open-ssh-session host #:key user port identity
>> host-key
>> (compression %compression)
>> (timeout 3600)
>> - (connection-timeout 10))
>> + (connection-timeout 10)
>> + (stricthostkeycheck #t))
>> "Open an SSH session for HOST and return it. IDENTITY specifies the file
>> name of a private key to use for authenticating with the host. When USER,
>> PORT, or IDENTITY are #f, use default values or whatever '~/.ssh/config'
>
> Please update the docstring.
>
> Rather ‘strict-host-key-check?’ to match naming conventions, even if
> Guile-SSH calls it that way.
>
>> @@ -137,7 +138,8 @@ (define* (open-ssh-session host #:key user port identity
>>
>> ;; Speed up RPCs by creating sockets with
>> ;; TCP_NODELAY.
>> - #:nodelay #t)))
>> + #:nodelay #t
>> + #:stricthostkeycheck stricthostkeycheck)))
>
> Not sure what this does actually. Looks like the main part is the
> “when stricthostkeycheck” condition that comes below, no?
>
> Could you send a second version?
>
> Thank you!
>
> Ludo’.
-----BEGIN PGP SIGNATURE-----

iQFLBAEBCAA1FiEE0iajOdjfRIFd3gygPdpSUn0qwZkFAmeNL1oXHHJvbWFuQGJ1
cm5pbmdzd2VsbC5jb20ACgkQPdpSUn0qwZm9dQgAsLRwAkE6KFUS4Z0zCdIy668C
q6MoIUNFzyTIL94UOHVxHMUIbApE1CkuNsoVHx73/4vcmMekxnvYB9pd8ng8QwcV
q3u3iGLzwS0LIIJMiY5FwcVD6OaYgjz1LrO23Wn7qRx4f9u2PqpQAfuaec0GjBQM
F6T6P5KGJ7eefv84nMT/h4PLNUGYTYQd0R+oIoYog30ILv/NQfLaFlgY7iu478nd
/lJZm7WvXwTrysGYCFE2icgD7eIAk5i30DCdYys9eFgQjlKuxpOFr/bqqlz+7yCC
XSnYxw1bQ3D6ApotpBujuStPGap3NuoEd5pzEK10uIF5qjGbr9bK46qT25hJkg==
=CGHe
-----END PGP SIGNATURE-----

Roman Scherer wrote 2 months ago
(name . Roman Scherer)(address . roman@burningswell.com)(name . Josselin Poiret)(address . dev@jpoiret.xyz)(name . Maxim Cournoyer)(address . maxim.cournoyer@gmail.com)(name . Simon Tournier)(address . zimon.toutoune@gmail.com)(name . Mathieu Othacehe)(address . othacehe@gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)(name . Tobias Geerinckx-Rice)(address . me@tobias.gr)(name . Christopher Baines)(address . guix@cbaines.net)(address . 75144@debbugs.gnu.org)
87ed0rt3oz.fsf@burningswell.com
I made a `mock*` macro to get around this ugly nesting in the meantime.


But I'm still wondering why the `mock` in `deploy-machine-mock-with-unprovisioned-server` is working in the REPL,
but failing when I run the test with make ...

Roman Scherer <roman@burningswell.com> writes:

Toggle quote (277 lines)
> Hi Ludo,
>
> thanks for your review. Here is a v2, I hope I addressed your previous
> comments with it, but I need some help.
>
> As you suggested I also added some tests. Some use mocking, and some run
> against the Hetzner API, if the GUIX_HETZNER_API_TOKEN env var is set.
>
> ./pre-inst-env make check TESTS="tests/machine/hetzner/http.scm"
> ./pre-inst-env make check TESTS="tests/machine/hetzner.scm"
>
> All tests pass when I run them in the Geiser REPL, where I developed them.
>
> But I have some trouble with one test that uses mocking. The
> "deploy-machine-mock-with-unprovisioned-server" test in
> tests/machine/hetzner.scm only fails when run in the terminal. :?
>
> I'm using the "mock" function from (guix tests) to mock some HTTP and SSH
> calls. The issue is that I see different behaviour whether I run the tests in
> Geiser vs in the Terminal.
>
> In Geiser I see the following output for this test, in it passes:
>
> -------------------------------------------------------------------------------
> creating 'cx42' server for 'guix-x86'...
> successfully created 'cx42' x86 server for 'guix-x86'
> enabling rescue system on 'guix-x86'...
> MOCK ENABLE RESUCE
> successfully enabled rescue system on 'guix-x86'
> powering on server for 'guix-x86'...
> MOCK POWER ON
> successfully powered on server for 'guix-x86'
> connecting via SSH to '1.2.3.4' using '/tmp/guix-hetzner-machine-test-key'...
> MOCK OPEN SSH SESSION
> installing rescue system packages on 'guix-x86'...
> MOCK RUNNING SCRIPT: /tmp/guix/deploy/hetzner-machine-rescue-install-packages
> successfully installed rescue system packages on 'guix-x86'
> setting up partitions on 'guix-x86'...
> MOCK RUNNING SCRIPT: /tmp/guix/deploy/hetzner-machine-rescue-partition
> successfully setup partitions on 'guix-x86'
> installing guix operating system on 'guix-x86'...
> MOCK RUNNING SCRIPT: /tmp/guix/deploy/hetzner-machine-rescue-install-os
> successfully installed guix operating system on 'guix-x86'
> rebooting server for 'guix-x86'...
> successfully rebooted server for 'guix-x86'
> connecting via SSH to '1.2.3.4' using '/tmp/guix-hetzner-machine-test-key'...
> MOCK OPEN SSH SESSION
> -------------------------------------------------------------------------------
>
> You can see that calls to "hetzner-machine-ssh-run-script" are mocked, because
> "MOCK RUNNING SCRIPT" is printed multiple times.
>
> But in a "guix shell -D" terminal I see the following output for the test, and
> it is failing:
>
> -------------------------------------------------------------------------------
>
> creating 'cx42' server for 'guix-x86'...
> successfully created 'cx42' x86 server for 'guix-x86'
> enabling rescue system on 'guix-x86'...
> MOCK ENABLE RESUCE
> successfully enabled rescue system on 'guix-x86'
> powering on server for 'guix-x86'...
> MOCK POWER ON
> successfully powered on server for 'guix-x86'
> connecting via SSH to '1.2.3.4' using '/tmp/guix-hetzner-machine-test-key'...
> MOCK OPEN SSH SESSION
> installing rescue system packages on 'guix-x86'...
> test-name: deploy-machine-mock-with-unprovisioned-server
> location: /home/roman/workspace/guix/tests/machine/hetzner.scm:189
>
> actual-value: #f
> actual-error:
> + (guile-ssh-error
> + "%gssh-make-sftp-session"
> + "Could not create a SFTP session"
> + #<session #<undefined>@1.2.3.4:22 (disconnected) ffff85596de0>
> + #f)
> result: FAIL
>
> ;;; [2025/01/19 17:39:16.791023, 0] [GSSH ERROR] Could not create a SFTP session: #<session #<undefined>@1.2.3.4:22 (disconnected) ffff85596de0>
>
> -------------------------------------------------------------------------------
>
> The tests fails here trying to use a disconnected SSH session object, that I
> returned in a mocked call. This code should actually never be reached, because
> I mock the "hetzner-machine-ssh-run-script" call. But for some reason the mock
> is not working here. The "MOCK RUNNING SCRIPT" output is missing.
>
> Do you have any ideas what could be going on here? I suspect this might be due
> to some optimization or env issue, but I'm pretty lost.
>
> I attached a WIP v2 for now. Will send a v3 and a separate patch for the ssh
> modification once I fixed this mock test.
>
> Thanks, Roman.
>
> [2. text/x-patch; v2-0001-machine-Implement-hetzner-environment-type.patch]...
>
>
> Ludovic Courtès <ludo@gnu.org> writes:
>
>> Hello Roman,
>>
>> Roman Scherer <roman@burningswell.com> skribis:
>>
>>> * gnu/machine/hetzner.scm: New file.
>>> * gnu/local.mk (GNU_SYSTEM_MODULES): Add it.
>>> * guix/ssh.scm (open-ssh-session): Add stricthostkeycheck option.
>>> * doc/guix.texi (Invoking guix deploy): Add documentation for
>>> 'hetzner-configuration'.
>>>
>>> Change-Id: Idc17dbc33279ecbf3cbfe2c53d7699140f8b9f41
>>
>> Thumbs up for this big piece of work, one that I think is important for
>> the project! ‘guix deploy’ is a great idea but it desperately needs
>> more backends like this one.
>>
>> I’m not familiar with Hetzner so I’ll comment on more general aspects.
>> Chris, perhaps you can provide feedback on Hetzner-specific issues? I
>> think we could put this backend to good use for Guix infra since a few
>> services are running at Hetzner.
>>
>>> +@deftp {Data Type} hetzner-configuration
>>> +This is the data type describing the server that should be created for a
>>> +machine with an @code{environment} of @code{hetzner-environment-type}.
>>
>> Could you add a sentence providing more context like:
>>
>> It allows you to configure deployment to a @acronym{VPS, virtual
>> private server} hosted by @uref{https://www.hetzner.com, Hetzner}.
>>
>>> +@item @code{authorize?} (default: @code{#t})
>>> +If true, the coordinator's public signing key
>>
>> “coordinator” has nothing to do here I guess.
>>
>>> +@item @code{labels} (default: @code{'()})
>>> +A user defined alist of key/value pairs attached to the server. Keys and
>>> +values must be strings. For more information, see
>>> +@uref{https://docs.hetzner.cloud/#labels, Labels}.
>>
>> Maybe add a short example?
>>
>>> +@item @code{location} (default: @code{"fsn1"})
>>> +The name of a @uref{https://docs.hetzner.com/cloud/general/locations,
>>> +location} to create the server in.
>>
>> Maybe add: “For example, @code{"fsn1"} corresponds to the Hetzner site
>> in Falkenstein, Germany, while @code{"sin"} corresponds to its site in
>> Singapore.”
>>
>>> +@item @code{server-type} (default: @code{"cx42"})
>>> +The name of the
>>> +@uref{https://docs.hetzner.com/cloud/servers/overview#server-types,
>>> +server type} this server should be created with.
>>
>> Likewise, an example would be elcome.
>>
>>> +@item @code{ssh-key}
>>> +The path to the SSH private key to use to authenticate with the remote
>>> +host.
>>
>> s/path to/file name of/
>>
>>> +The following example shows the definition of 2 machines that are
>>
>> s/2/two/
>>
>>> +vCPUs and 32 GB of RAM on the @code{aarch64} architecture, the second
>>
>> s/@code{aarch64}/AArch64/
>>
>>> +shared vCPUs and 32 GB of RAM on the @code{x86_64} architecture.
>>
>> Drop @code.
>>
>>> +@lisp
>>> +(use-modules (gnu machine)
>>> + (gnu machine hetzner))
>>> +
>>> +(list (machine
>>> + (operating-system %hetzner-os-arm)
>>> + (environment hetzner-environment-type)
>>> + (configuration (hetzner-configuration
>>> + (server-type "cax41")
>>> + (ssh-key "/home/charlie/.ssh/id_rsa"))))
>>> + (machine
>>> + (operating-system %hetzner-os-x86)
>>> + (environment hetzner-environment-type)
>>> + (configuration (hetzner-configuration
>>> + (server-type "cpx51")
>>> + (ssh-key "/home/charlie/.ssh/id_rsa")))))
>>
>> Nice!
>>
>>> +API key} should provision 2 machines for you.
>>
>> s/2/two/
>>
>>> + #:use-module (ice-9 receive)
>>
>> The code base preferable uses SRFI-71 for multiple-value returns.
>>
>>> + (raise (formatted-message
>>> + (G_ "Expected a list of Hetzner API responses")))))
>>
>> Messages should start with a lower-case letter (for all the messages in
>> this file).
>>
>> Please add the file to ‘po/guix/POTFILES.in’ so that it’s actually
>> subject to translation.
>>
>>> +(define (hetzner-api-response-read port)
>>> + "Read the Hetzner API response from PORT."
>>> + (let* ((response (read-response port))
>>> + (body (read-response-body response)))
>>> + (hetzner-api-response
>>> + (body (json-string->scm (bytevector->string body "UTF-8")))
>>
>> Just ‘string->utf8’ (shorter).
>>
>> More importantly: instead of ‘json-string->scm’ (which gives an alist,
>> leading to ‘assoc-ref’ calls all over the code base along with free-form
>> alists, which is very error-prone), could you use ‘define-json-mapping’?
>>
>> In essence it’s like ‘define-record-type’ but it additionally define how
>> to map a JSON dictionary to a Scheme record. There are several examples
>> in Guix, such as (guix swh).
>>
>> For clarity, it might be useful to move all the hetzner-api-* bits to a
>> separate module, for example (gnu machine hetzner http). WDYT?
>>
>>
>> The rest of the code looks nice to me (modulo alists :-)) but that’s
>> about all I can say. It’s quite a significant body of code. What would
>> you suggest to prevent bitrot and support maintenance? Are there parts
>> of it that could be usefully tested automatically, possibly by mocking
>> part of the Hetzner API? Or are there tips on how you tested it that
>> could be written down in the file itself?
>>
>>
>> Could you move the (guix ssh) bits to a separate patch?
>>
>>> +++ b/guix/ssh.scm
>>> @@ -103,7 +103,8 @@ (define* (open-ssh-session host #:key user port identity
>>> host-key
>>> (compression %compression)
>>> (timeout 3600)
>>> - (connection-timeout 10))
>>> + (connection-timeout 10)
>>> + (stricthostkeycheck #t))
>>> "Open an SSH session for HOST and return it. IDENTITY specifies the file
>>> name of a private key to use for authenticating with the host. When USER,
>>> PORT, or IDENTITY are #f, use default values or whatever '~/.ssh/config'
>>
>> Please update the docstring.
>>
>> Rather ‘strict-host-key-check?’ to match naming conventions, even if
>> Guile-SSH calls it that way.
>>
>>> @@ -137,7 +138,8 @@ (define* (open-ssh-session host #:key user port identity
>>>
>>> ;; Speed up RPCs by creating sockets with
>>> ;; TCP_NODELAY.
>>> - #:nodelay #t)))
>>> + #:nodelay #t
>>> + #:stricthostkeycheck stricthostkeycheck)))
>>
>> Not sure what this does actually. Looks like the main part is the
>> “when stricthostkeycheck” condition that comes below, no?
>>
>> Could you send a second version?
>>
>> Thank you!
>>
>> Ludo’.
-----BEGIN PGP SIGNATURE-----

iQFLBAEBCAA1FiEE0iajOdjfRIFd3gygPdpSUn0qwZkFAmeU6QwXHHJvbWFuQGJ1
cm5pbmdzd2VsbC5jb20ACgkQPdpSUn0qwZkaCgf8D9vItAoQLPOR2dLsR5hL5rYA
O+UuBocypV2COfipG6d0Dt+3CbKUS4sbJWg2hodRVxG2ZHY+nyBn05DwDpUdGT0l
SxWHMQMiVjlyIvxW0VxTOFVwymFCdKWCrY2q2T+UunwmzvPC5ALbm9asUP6SF5UL
IYB+2+nD7fuML3ti7Un7VK9S/QgwbRpZynjFpavfWrZzPUT0lvo6FFhx9VYoLi0C
nHo0vRAm5pUpywmlX5b4+OXQoTlw9hwX4zghukwvvBezkuhh/lTJtAcskd4Qd+Md
lqFgPkMZQyrdWcHv+s85MXO3yHqFetgUuv2ZU9fiSO9dh6TpRm7l1S180B2Tbw==
=TKJR
-----END PGP SIGNATURE-----

Maxim Cournoyer wrote 2 months ago
(name . Roman Scherer)(address . roman@burningswell.com)(name . Josselin Poiret)(address . dev@jpoiret.xyz)(name . Simon Tournier)(address . zimon.toutoune@gmail.com)(name . Mathieu Othacehe)(address . othacehe@gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)(name . Tobias Geerinckx-Rice)(address . me@tobias.gr)(name . Christopher Baines)(address . guix@cbaines.net)(address . 75144@debbugs.gnu.org)
87o6zt5bjs.fsf@gmail.com
Hi Roman,

Roman Scherer <roman@burningswell.com> writes:

Toggle quote (9 lines)
> I made a `mock*` macro to get around this ugly nesting in the meantime.
>
> https://github.com/r0man/guix/blob/hetzner-machine-v2-mock-star/tests/machine/hetzner.scm#L165-L248
>
> But I'm still wondering why the `mock` in
> `deploy-machine-mock-with-unprovisioned-server` is working in the
> REPL,
> but failing when I run the test with make ...

Could it be that you are tricked by the caching of HTTP queries? I've
been tricked by this before, as if you expect to have to mock each
individual request it may not happen as some will already be cached.

If that's the case, either disabling cache could do, or more easily, use
something like done with mock-http-fetch in the tests/go.scm file.

Hope that helps,

--
Thanks,
Maxim
Roman Scherer wrote 1 months ago
(name . Maxim Cournoyer)(address . maxim.cournoyer@gmail.com)(name . Josselin Poiret)(address . dev@jpoiret.xyz)(name . Simon Tournier)(address . zimon.toutoune@gmail.com)(name . Mathieu Othacehe)(address . othacehe@gnu.org)(name . Ludovic Courtè s)(address . ludo@gnu.org)(name . Tobias Geerinckx-Rice)(address . me@tobias.gr)(name . Roman Scherer)(address . roman@burningswell.com)(name . Christopher Baines)(address . guix@cbaines.net)(address . 75144@debbugs.gnu.org)
87tt9je0sr.fsf@burningswell.com
Hi Maxim,

thanks for your help and the tip about caching. Unless I'm missing
something, I don't think the caching of HTTP requests is involved
here.

I'm trying to test the (gnu machine hetzner) module and mock the
functions it uses from the (gnu machine hetzner http) module.

When I run the mocked test I expect no code from the (gnu machine
hetzner http) module to be executed, since I mocked all those
functions. This seems to work in the Geiser REPL, but for some reason it
does not work when I run the test with:

./pre-inst-env make check TESTS="tests/machine/hetzner.scm"

To me it looks like the mock function behaves differently in those 2
situations. In the meaintime I also tried setting -O0, but that didn't
make any difference either. :/

Roman

Maxim Cournoyer <maxim.cournoyer@gmail.com> writes:

Toggle quote (21 lines)
> Hi Roman,
>
> Roman Scherer <roman@burningswell.com> writes:
>
>> I made a `mock*` macro to get around this ugly nesting in the meantime.
>>
>> https://github.com/r0man/guix/blob/hetzner-machine-v2-mock-star/tests/machine/hetzner.scm#L165-L248
>>
>> But I'm still wondering why the `mock` in
>> `deploy-machine-mock-with-unprovisioned-server` is working in the
>> REPL,
>> but failing when I run the test with make ...
>
> Could it be that you are tricked by the caching of HTTP queries? I've
> been tricked by this before, as if you expect to have to mock each
> individual request it may not happen as some will already be cached.
>
> If that's the case, either disabling cache could do, or more easily, use
> something like done with mock-http-fetch in the tests/go.scm file.
>
> Hope that helps,
-----BEGIN PGP SIGNATURE-----

iQFLBAEBCAA1FiEE0iajOdjfRIFd3gygPdpSUn0qwZkFAmeYpXQXHHJvbWFuQGJ1
cm5pbmdzd2VsbC5jb20ACgkQPdpSUn0qwZkabAgAlM8siGqs+Q1WJfIojVyij+ln
SEAhfELYT//itSJXSqep+AFn195XT84uY9jXiN1yD6NDpPVabKlVmglOKwehjbF6
TSHCpl2+rHTp49hvoPQTT00fknzaRHn63J0mKSHdtHK7i1qKwGTCH45VkYaRVTHT
eKpeadMyygdQiFQ8wWO/UnBNFa0x4aDSMdeKM7EEMiBBZkeTqiHAxINfBbtqbBXV
QruGzhtPRe7b0tyQn1ttpBLXaNbIM/As660S/oRk5r5QQ58dw8DD8lvdXylM7r9H
2ou2XdMaoIx04NPiROsQ8Ee1Wy9UX7huye88zx+WceFGxMMdVaVeF3COiiS7nA==
=/ELQ
-----END PGP SIGNATURE-----

Ludovic Courtès wrote 1 months ago
(name . Roman Scherer)(address . roman@burningswell.com)(name . Josselin Poiret)(address . dev@jpoiret.xyz)(name . Maxim Cournoyer)(address . maxim.cournoyer@gmail.com)(name . Simon Tournier)(address . zimon.toutoune@gmail.com)(name . Mathieu Othacehe)(address . othacehe@gnu.org)(name . Tobias Geerinckx-Rice)(address . me@tobias.gr)(name . Christopher Baines)(address . guix@cbaines.net)(address . 75144@debbugs.gnu.org)
87y0yvdxej.fsf@gnu.org
Hi,

Roman Scherer <roman@burningswell.com> skribis:

Toggle quote (11 lines)
> When I run the mocked test I expect no code from the (gnu machine
> hetzner http) module to be executed, since I mocked all those
> functions. This seems to work in the Geiser REPL, but for some reason it
> does not work when I run the test with:
>
> ./pre-inst-env make check TESTS="tests/machine/hetzner.scm"
>
> To me it looks like the mock function behaves differently in those 2
> situations. In the meaintime I also tried setting -O0, but that didn't
> make any difference either. :/

Hmm. I was going to say that the likely problem is that code from (gnu
machines hetzner http) gets inlined so you cannot really mock it.

To make sure this can be mocked, you can use this trick:

(set! proc proc)

where ‘proc’ is the procedure you want to mock (that statement prevents
the compiler from inlining it).

Ludo’.
Roman Scherer wrote 1 months ago
(name . Ludovic Courtès)(address . ludo@gnu.org)(name . Josselin Poiret)(address . dev@jpoiret.xyz)(name . Maxim Cournoyer)(address . maxim.cournoyer@gmail.com)(name . Simon Tournier)(address . zimon.toutoune@gmail.com)(name . Mathieu Othacehe)(address . othacehe@gnu.org)(name . Tobias Geerinckx-Rice)(address . me@tobias.gr)(name . Roman Scherer)(address . roman@burningswell.com)(name . Christopher Baines)(address . guix@cbaines.net)(address . 75144@debbugs.gnu.org)
867c6e90ei.fsf@burningswell.com
Hi Ludo,

that's what I was looking for. Now it is working as expected!

I will send an updated patch soon.

Thanks for your help!

Roman

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

Toggle quote (26 lines)
> Hi,
>
> Roman Scherer <roman@burningswell.com> skribis:
>
>> When I run the mocked test I expect no code from the (gnu machine
>> hetzner http) module to be executed, since I mocked all those
>> functions. This seems to work in the Geiser REPL, but for some reason it
>> does not work when I run the test with:
>>
>> ./pre-inst-env make check TESTS="tests/machine/hetzner.scm"
>>
>> To me it looks like the mock function behaves differently in those 2
>> situations. In the meaintime I also tried setting -O0, but that didn't
>> make any difference either. :/
>
> Hmm. I was going to say that the likely problem is that code from (gnu
> machines hetzner http) gets inlined so you cannot really mock it.
>
> To make sure this can be mocked, you can use this trick:
>
> (set! proc proc)
>
> where ‘proc’ is the procedure you want to mock (that statement prevents
> the compiler from inlining it).
>
> Ludo’.
-----BEGIN PGP SIGNATURE-----

iQFLBAEBCAA1FiEE0iajOdjfRIFd3gygPdpSUn0qwZkFAmeZNrUXHHJvbWFuQGJ1
cm5pbmdzd2VsbC5jb20ACgkQPdpSUn0qwZnvGgf/R1e7zvy3OC/q5GqRjEy1+hEA
BTx1KN1nw29B5TsArmG3PQO4HB6qocUPJtwLdAXy9mg+q6f26bl9ytc+20cqbX1z
7xC3v6vSd/3SZg3jGFPw5t1+iF6YI/ZT7Il/oomCbiP5iSMGSjq3Wc7vVGI2IWSa
Mfj6a9xfXAoQjtk40E4NNWxgsvhmABdbxqHRZzBrpcsDcgHpnX5l4ScEStwdYdwb
JIIGMj56T0FKU/s89s8kimd4Fj4b/Y0fANhJfxh7QzGQSQ1IjqEbXfPqclEWIwCL
j/AbCEwf2h904G4uyAdwAolGurTmiufFrN5Y/81Dd5TT2JUPtGrCTFaYuxxKrA==
=0O6G
-----END PGP SIGNATURE-----

Roman Scherer wrote 1 months ago
[PATCH v3 1/2] guix: ssh: Add strict-host-key-check? option.
(address . 75144@debbugs.gnu.org)(name . Roman Scherer)(address . roman@burningswell.com)
53d36027832470a5f3a38d4003ce426fabedb97b.1738695552.git.roman@burningswell.com
* guix/ssh.scm (open-ssh-session): Add strict-host-key-check? option.

Change-Id: Iae5df5ac8d45033b6b636e9c872f8910d4f6cfe9
---
guix/ssh.scm | 22 ++++++++++++++--------
1 file changed, 14 insertions(+), 8 deletions(-)

Toggle diff (59 lines)
diff --git a/guix/ssh.scm b/guix/ssh.scm
index ae506df14c..8decfdbab9 100644
--- a/guix/ssh.scm
+++ b/guix/ssh.scm
@@ -103,7 +103,8 @@ (define* (open-ssh-session host #:key user port identity
host-key
(compression %compression)
(timeout 3600)
- (connection-timeout 10))
+ (connection-timeout 10)
+ (strict-host-key-check? #t))
"Open an SSH session for HOST and return it. IDENTITY specifies the file
name of a private key to use for authenticating with the host. When USER,
PORT, or IDENTITY are #f, use default values or whatever '~/.ssh/config'
@@ -117,6 +118,9 @@ (define* (open-ssh-session host #:key user port identity
seconds. Install TIMEOUT as the maximum time in seconds after which a read or
write operation on a channel of the returned session is considered as failing.
+IF STRICT-HOST-KEY-CHECK? is #f, strict host key checking is turned off for
+the new session.
+
Throw an error on failure."
(let ((session (make-session #:user user
#:identity identity
@@ -137,7 +141,8 @@ (define* (open-ssh-session host #:key user port identity
;; Speed up RPCs by creating sockets with
;; TCP_NODELAY.
- #:nodelay #t)))
+ #:nodelay #t
+ #:stricthostkeycheck strict-host-key-check?)))
;; Honor ~/.ssh/config.
(session-parse-config! session)
@@ -149,13 +154,14 @@ (define* (open-ssh-session host #:key user port identity
(authenticate-server* session host-key)
;; Authenticate against ~/.ssh/known_hosts.
- (match (authenticate-server session)
- ('ok #f)
- (reason
- (raise (formatted-message (G_ "failed to authenticate \
+ (when strict-host-key-check?
+ (match (authenticate-server session)
+ ('ok #f)
+ (reason
+ (raise (formatted-message (G_ "failed to authenticate \
server at '~a': ~a")
- (session-get session 'host)
- reason)))))
+ (session-get session 'host)
+ reason))))))
;; Use public key authentication, via the SSH agent if it's available.
(match (userauth-public-key/auto! session)

base-commit: 97fee203a5441f4d3004ccf43ed72fa3b51a7cdc
--
2.48.1
Roman Scherer wrote 1 months ago
[PATCH v3 2/2] machine: Implement 'hetzner-environment-type'.
(address . 75144@debbugs.gnu.org)(name . Roman Scherer)(address . roman@burningswell.com)
7b51e5d7ae56f7f9792252e98b57371b2904a3fe.1738695552.git.roman@burningswell.com
* Makefile.am (SCM_TESTS): Add test modules.
* doc/guix.texi: Add documentation.
* gnu/local.mk (GNU_SYSTEM_MODULES): Add modules.
* gnu/machine/hetzner.scm: Add hetzner-environment-type.
* gnu/machine/hetzner/http.scm: Add HTTP API.
* po/guix/POTFILES.in: Add Hetzner modules.
* tests/machine/hetzner.scm: Add machine tests.
* tests/machine/hetzner/http.scm Add HTTP API tests.

Change-Id: I276ed5afed676bbccc6c852c56ee4db57ce3c1ea
---
Makefile.am | 2 +
doc/guix.texi | 128 ++++++
gnu/local.mk | 2 +
gnu/machine/hetzner.scm | 705 +++++++++++++++++++++++++++++++++
gnu/machine/hetzner/http.scm | 664 +++++++++++++++++++++++++++++++
po/guix/POTFILES.in | 2 +
tests/machine/hetzner.scm | 267 +++++++++++++
tests/machine/hetzner/http.scm | 631 +++++++++++++++++++++++++++++
8 files changed, 2401 insertions(+)
create mode 100644 gnu/machine/hetzner.scm
create mode 100644 gnu/machine/hetzner/http.scm
create mode 100644 tests/machine/hetzner.scm
create mode 100644 tests/machine/hetzner/http.scm

Toggle diff (496 lines)
diff --git a/Makefile.am b/Makefile.am
index f759803b8b..7bb75aa146 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -562,6 +562,8 @@ SCM_TESTS = \
tests/import-utils.scm \
tests/inferior.scm \
tests/lint.scm \
+ tests/machine/hetzner.scm \
+ tests/machine/hetzner/http.scm \
tests/minetest.scm \
tests/modules.scm \
tests/monads.scm \
diff --git a/doc/guix.texi b/doc/guix.texi
index bb5f29277f..4226d7ae26 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -44783,6 +44783,134 @@ Invoking guix deploy
@end table
@end deftp
+@deftp {Data Type} hetzner-configuration
+This is the data type describing the server that should be created for a
+machine with an @code{environment} of
+@code{hetzner-environment-type}. It allows you to configure deployment
+to a @acronym{VPS, virtual private server} hosted by
+@uref{https://www.hetzner.com, Hetzner}.
+
+@table @asis
+
+@item @code{allow-downgrades?} (default: @code{#f})
+Whether to allow potential downgrades.
+
+@item @code{authorize?} (default: @code{#t})
+If true, the public signing key @code{"/etc/guix/signing-key.pub"} of
+the machine that invokes @command{guix deploy} will be added to the
+operating system ACL keyring of the target machine.
+
+@item @code{build-locally?} (default: @code{#t})
+If true, system derivations will be built on the machine that invokes
+@command{guix deploy}, otherwise derivations are build on the target
+machine. Set this to @code{#f} if the machine you are deploying from
+has a different architecture than the target machine and you can't build
+derivations for the target architecture by other means, like offloading
+(@pxref{Daemon Offload Setup}) or emulation
+(@pxref{transparent-emulation-qemu, Transparent Emulation with QEMU}).
+
+@item @code{delete?} (default: @code{#t})
+If true, the server will be deleted when an error happens in the
+provisioning phase. If false, the server will be kept in order to debug
+any issues.
+
+@item @code{labels} (default: @code{'()})
+A user defined alist of key/value pairs attached to the SSH key and the
+server on the Hetzner API. Keys and values must be strings,
+e.g. @code{'(("environment" . "development"))}. For more information,
+see @uref{https://docs.hetzner.cloud/#labels, Labels}.
+
+@item @code{location} (default: @code{"fsn1"})
+The name of a @uref{https://docs.hetzner.com/cloud/general/locations,
+location} to create the server in. For example, @code{"fsn1"}
+corresponds to the Hetzner site in Falkenstein, Germany, while
+@code{"sin"} corresponds to its site in Singapore.
+
+@item @code{server-type} (default: @code{"cx42"})
+The name of the
+@uref{https://docs.hetzner.com/cloud/servers/overview#server-types,
+server type} this virtual server should be created with. For example,
+@code{"cx42"} corresponds to a x86_64 server that has 8 VCPUs, 16 GB of
+memory and 160 GB of storage, while @code{"cax31"} to the AArch64
+equivalent. Other server types and their current prices can be found
+@uref{https://www.hetzner.com/cloud/#pricing, here}.
+
+@item @code{ssh-key}
+The file name of the SSH private key to use to authenticate with the
+remote host.
+
+@end table
+
+When deploying a machine for the first time, the following steps are
+taken to provision a server for the machine on the
+@uref{https://www.hetzner.com/cloud, Hetzner Cloud} service:
+
+@itemize
+
+@item
+Create the SSH key of the machine on the Hetzner API.
+
+@item
+Create a server for the machine on the Hetzner API.
+
+@item
+Format the root partition of the disk using the file system of the
+machine's operating system. Supported file systems are btrfs and ext4.
+
+@item
+Install a minimal Guix operating system on the server using the
+@uref{https://docs.hetzner.com/cloud/servers/getting-started/rescue-system,
+rescue mode}. This minimal system is used to install the machine's
+operating system, after rebooting.
+
+@item
+Reboot the server and apply the machine's operating system on the
+server.
+
+@end itemize
+
+Once the server has been provisioned and SSH is available, deployment
+continues by delegating it to the @code{managed-host-environment-type}.
+
+Servers on the Hetzner Cloud service can be provisioned on the AArch64
+architecture using UEFI boot mode, or on the x86_64 architecture using
+BIOS boot mode. The @code{(gnu machine hetzner)} module exports the
+@code{%hetzner-os-arm} and @code{%hetzner-os-x86} operating systems that
+are compatible with those two architectures, and can be used as a base
+for defining your custom operating system.
+
+The following example shows the definition of two machines that are
+deployed on the Hetzner Cloud service. The first one uses the
+@code{%hetzner-os-arm} operating system to run a server with 16 shared
+vCPUs and 32 GB of RAM on the @code{aarch64} architecture, the second
+one uses the @code{%hetzner-os-x86} operating system on a server with 16
+shared vCPUs and 32 GB of RAM on the @code{x86_64} architecture.
+
+@lisp
+(use-modules (gnu machine)
+ (gnu machine hetzner))
+
+(list (machine
+ (operating-system %hetzner-os-arm)
+ (environment hetzner-environment-type)
+ (configuration (hetzner-configuration
+ (server-type "cax41")
+ (ssh-key "/home/charlie/.ssh/id_rsa"))))
+ (machine
+ (operating-system %hetzner-os-x86)
+ (environment hetzner-environment-type)
+ (configuration (hetzner-configuration
+ (server-type "cpx51")
+ (ssh-key "/home/charlie/.ssh/id_rsa")))))
+@end lisp
+
+Passing this file to @command{guix deploy} with the environment variable
+@env{GUIX_HETZNER_API_TOKEN} set to a valid Hetzner
+@uref{https://docs.hetzner.com/cloud/api/getting-started/generating-api-token,
+API key} should provision two machines for you.
+
+@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 83abc86fe2..cc812ad6f3 100644
--- a/gnu/local.mk
+++ b/gnu/local.mk
@@ -921,6 +921,8 @@ if HAVE_GUILE_SSH
GNU_SYSTEM_MODULES += \
%D%/machine/digital-ocean.scm \
+ %D%/machine/hetzner.scm \
+ %D%/machine/hetzner/http.scm \
%D%/machine/ssh.scm
endif HAVE_GUILE_SSH
diff --git a/gnu/machine/hetzner.scm b/gnu/machine/hetzner.scm
new file mode 100644
index 0000000000..5e17bfae21
--- /dev/null
+++ b/gnu/machine/hetzner.scm
@@ -0,0 +1,705 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2024 Roman Scherer <roman@burningswell.com>
+;;;
+;;; 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 hetzner)
+ #:use-module (gnu bootloader grub)
+ #:use-module (gnu bootloader)
+ #:use-module (gnu machine hetzner http)
+ #:use-module (gnu machine ssh)
+ #:use-module (gnu machine)
+ #:use-module (gnu packages ssh)
+ #:use-module (gnu services base)
+ #:use-module (gnu services networking)
+ #:use-module (gnu services ssh)
+ #:use-module (gnu services)
+ #:use-module (gnu system file-systems)
+ #:use-module (gnu system image)
+ #:use-module (gnu system linux-initrd)
+ #:use-module (gnu system pam)
+ #:use-module (gnu system)
+ #:use-module (guix base32)
+ #:use-module (guix colors)
+ #:use-module (guix derivations)
+ #:use-module (guix diagnostics)
+ #:use-module (guix gexp)
+ #:use-module (guix i18n)
+ #:use-module (guix import json)
+ #:use-module (guix monads)
+ #:use-module (guix packages)
+ #:use-module (guix pki)
+ #:use-module (guix records)
+ #:use-module (guix ssh)
+ #:use-module (guix store)
+ #:use-module (ice-9 format)
+ #:use-module (ice-9 iconv)
+ #:use-module (ice-9 match)
+ #:use-module (ice-9 popen)
+ #:use-module (ice-9 rdelim)
+ #:use-module (ice-9 string-fun)
+ #:use-module (ice-9 textual-ports)
+ #:use-module (json)
+ #:use-module (srfi srfi-1)
+ #:use-module (srfi srfi-2)
+ #:use-module (srfi srfi-34)
+ #:use-module (srfi srfi-35)
+ #:use-module (srfi srfi-71)
+ #:use-module (ssh channel)
+ #:use-module (ssh key)
+ #:use-module (ssh popen)
+ #:use-module (ssh session)
+ #:use-module (ssh sftp)
+ #:use-module (ssh shell)
+ #:export (%hetzner-os-arm
+ %hetzner-os-x86
+ deploy-hetzner
+ hetzner-configuration
+ hetzner-configuration-allow-downgrades?
+ hetzner-configuration-api
+ hetzner-configuration-authorize?
+ hetzner-configuration-build-locally?
+ hetzner-configuration-delete?
+ hetzner-configuration-labels
+ hetzner-configuration-location
+ hetzner-configuration-server-type
+ hetzner-configuration-ssh-key
+ hetzner-configuration?
+ hetzner-environment-type))
+
+;;; Commentary:
+;;;
+;;; This module implements a high-level interface for provisioning machines on
+;;; the Hetzner Cloud service https://docs.hetzner.cloud.
+;;;
+
+
+;;;
+;;; Hetzner operating systems.
+;;;
+
+;; Operating system for arm servers using UEFI boot mode.
+
+(define %hetzner-os-arm
+ (operating-system
+ (host-name "guix-arm")
+ (bootloader
+ (bootloader-configuration
+ (bootloader grub-efi-bootloader)
+ (targets (list "/boot/efi"))
+ (terminal-outputs '(console))))
+ (file-systems
+ (cons* (file-system
+ (mount-point "/")
+ (device "/dev/sda1")
+ (type "ext4"))
+ (file-system
+ (mount-point "/boot/efi")
+ (device "/dev/sda15")
+ (type "vfat"))
+ %base-file-systems))
+ (initrd-modules
+ (cons* "sd_mod" "virtio_scsi" %base-initrd-modules))
+ (services
+ (cons* (service dhcp-client-service-type)
+ (service openssh-service-type
+ (openssh-configuration
+ (openssh openssh-sans-x)
+ (permit-root-login 'prohibit-password)))
+ %base-services))))
+
+;; Operating system for x86 servers using BIOS boot mode.
+
+(define %hetzner-os-x86
+ (operating-system
+ (inherit %hetzner-os-arm)
+ (host-name "guix-x86")
+ (bootloader
+ (bootloader-configuration
+ (bootloader grub-bootloader)
+ (targets (list "/dev/sda"))
+ (terminal-outputs '(console))))
+ (initrd-modules
+ (cons "virtio_scsi" %base-initrd-modules))
+ (file-systems
+ (cons (file-system
+ (mount-point "/")
+ (device "/dev/sda1")
+ (type "ext4"))
+ %base-file-systems))))
+
+(define (operating-system-authorize os)
+ "Authorize the OS with the public signing key of the current machine."
+ (if (file-exists? %public-key-file)
+ (operating-system
+ (inherit os)
+ (services
+ (modify-services (operating-system-user-services os)
+ (guix-service-type
+ config => (guix-configuration
+ (inherit config)
+ (authorized-keys
+ (cons*
+ (local-file %public-key-file)
+ (guix-configuration-authorized-keys config))))))))
+ (raise-exception
+ (formatted-message (G_ "no signing key '~a'. \
+Have you run 'guix archive --generate-key'?")
+ %public-key-file))))
+
+(define (operating-system-root-file-system-type os)
+ "Return the root file system type of the operating system OS."
+ (let ((root-fs (find (lambda (file-system)
+ (equal? "/" (file-system-mount-point file-system)))
+ (operating-system-file-systems os))))
+ (if (file-system? root-fs)
+ (file-system-type root-fs)
+ (raise-exception
+ (formatted-message
+ (G_ "could not determine root file system type"))))))
+
+
+;;;
+;;; Helper functions.
+;;;
+
+(define (escape-backticks str)
+ "Escape all backticks in STR."
+ (string-replace-substring str "`" "\\`"))
+
+
+
+;;;
+;;; Hetzner configuration.
+;;;
+
+(define-record-type* <hetzner-configuration> hetzner-configuration
+ make-hetzner-configuration hetzner-configuration? this-hetzner-configuration
+ (allow-downgrades? hetzner-configuration-allow-downgrades? ; boolean
+ (default #f))
+ (api hetzner-configuration-api ; <hetzner-api>
+ (default (hetzner-api)))
+ (authorize? hetzner-configuration-authorize? ; boolean
+ (default #t))
+ (build-locally? hetzner-configuration-build-locally? ; boolean
+ (default #t))
+ (delete? hetzner-configuration-delete? ; boolean
+ (default #f))
+ (labels hetzner-configuration-labels ; list of strings
+ (default '()))
+ (location hetzner-configuration-location ; #f | string
+ (default "fsn1"))
+ (server-type hetzner-configuration-server-type ; string
+ (default "cx42"))
+ (ssh-key hetzner-configuration-ssh-key)) ; string
+
+(define (hetzner-configuration-ssh-key-fingerprint config)
+ "Return the SSH public key fingerprint of CONFIG as a string."
+ (and-let* ((file-name (hetzner-configuration-ssh-key config))
+ (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 (hetzner-configuration-ssh-key-public config)
+ "Return the SSH public key of CONFIG as a string."
+ (and-let* ((ssh-key (hetzner-configuration-ssh-key config))
+ (public-key (public-key-from-file ssh-key)))
+ (format #f "ssh-~a ~a" (get-key-type public-key)
+ (public-key->string public-key))))
+
+
+;;;
+;;; Hetzner Machine.
+;;;
+
+(define (hetzner-machine-delegate target server)
+ "Return the delagate machine that uses SSH for deployment."
+ (let* ((config (machine-configuration target))
+ ;; Get the operating system WITHOUT the provenance service to avoid a
+ ;; duplicate symlink conflict in the store.
+ (os ((@@ (gnu machine) %machine-operating-system) target)))
+ (machine
+ (inherit target)
+ (operating-system
+ (if (hetzner-configuration-authorize? config)
+ (operating-system-authorize os)
+ os))
+ (environment managed-host-environment-type)
+ (configuration
+ (machine-ssh-configuration
+ (allow-downgrades? (hetzner-configuration-allow-downgrades? config))
+ (authorize? (hetzner-configuration-authorize? config))
+ (build-locally? (hetzner-configuration-build-locally? config))
+ (host-name (hetzner-server-public-ipv4 server))
+ (identity (hetzner-configuration-ssh-key config))
+ (system (hetzner-server-system server)))))))
+
+(define (hetzner-machine-location machine)
+ "Find the location of MACHINE on the Hetzner API."
+ (let* ((config (machine-configuration machine))
+ (expected (hetzner-configuration-location config)))
+ (find (lambda (location)
+ (equal? expected (hetzner-location-name location)))
+ (hetzner-api-locations
+ (hetzner-configuration-api config)
+ #:params `(("name" . ,expected))))))
+
+(define (hetzner-machine-server-type machine)
+ "Find the server type of MACHINE on the Hetzner API."
+ (let* ((config (machine-configuration machine))
+ (expected (hetzner-configuration-server-type config)))
+ (find (lambda (server-type)
+ (equal? expected (hetzner-server-type-name server-type)))
+ (hetzner-api-server-types
+ (hetzner-configuration-api config)
+ #:params `(("name" . ,expected))))))
+
+(define (hetzner-machine-validate-api-token machine)
+ "Validate the Hetzner API authentication token of MACHINE."
+ (let* ((config (machine-configuration machine))
+ (api (hetzner-configuration-api config)))
+ (unless (hetzner-api-token api)
+ (raise-exception
+ (formatted-message
+ (G_ "Hetzner Cloud access token was not provided. \
+This may be fixed by setting the environment variable GUIX_HETZNER_API_TOKEN \
+to one procured from \
+https://docs.hetzner.com/cloud/api/getting-started/generating-api-token"))))))
+
+(define (hetzner-machine-validate-configuration-type machine)
+ "Raise an error if MACHINE's configuration is not an instance of
+<hetzner-configuration>."
+ (let ((config (machine-configuration machine))
+ (environment (environment-type-name (machine-environment machine))))
+ (unless (and config (hetzner-configuration? config))
+ (raise-exception
+ (formatted-message (G_ "unsupported machine configuration '~a' \
+for environment of type '~a'")
+ config
+ environment)))))
+
+(define (hetzner-machine-validate-server-type machine)
+ "Raise an error if the server type of MACHINE is not supported."
+ (unless (hetzner-machine-server-type machine)
+ (let* ((config (machine-configuration machine))
+ (api (hetzner-configuration-api config)))
+ (raise-exception
+ (formatted-message
+ (G_ "server type '~a' not supported~%~%\
+Available server types:~%~%~a~%~%For more details and prices, see: ~a")
+ (hetzner-configuration-server-type config)
+ (string-join
+ (map (lambda (type)
+ (format #f " - ~a: ~a, ~a ~a cores, ~a GB mem, ~a GB disk"
+ (colorize-string
+ (hetzner-server-type-name type)
+ (color BOLD))
+ (hetzner-server-type-architecture type)
+ (hetzner-server-type-cores type)
+ (hetzner-server-type-cpu-type type)
+ (hetzner-server-type-memory type)
+ (hetzner-server-type-disk type)))
+ (hetzner-api-server-types api))
+ "\n")
+ "https://www.hetzner.com/cloud#pricing")))))
+
+(define (hetzner-machine-validate-location machine)
+ "Raise an error if the location of MACHINE is not supported."
+ (unless (hetzner-machine-location machine)
+ (let* ((config (machine-configuration machine))
+ (api (hetzner-configuration-api config)))
+
This message was truncated. Download the full message here.
Roman Scherer wrote 1 months ago
Re: [bug#75144] [PATCH] machine: Implement 'hetzner-environment-type'.
(name . Roman Scherer)(address . roman@burningswell.com)(name . Josselin Poiret)(address . dev@jpoiret.xyz)(name . Maxim Cournoyer)(address . maxim.cournoyer@gmail.com)(name . Simon Tournier)(address . zimon.toutoune@gmail.com)(name . Mathieu Othacehe)(address . othacehe@gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)(name . Tobias Geerinckx-Rice)(address . me@tobias.gr)(name . Christopher Baines)(address . guix@cbaines.net)(address . 75144@debbugs.gnu.org)
865xlph6f6.fsf@burningswell.com
References: <6ff52cb81582c81835e39beebc7e6f7f3ecfd81d.1735317980.git.roman@burningswell.com>
<8734hi1mdh.fsf@gnu.org> <868qr6n3j9.fsf@burningswell.com>
<87ed0rt3oz.fsf@burningswell.com> <87o6zt5bjs.fsf@gmail.com>
<87tt9je0sr.fsf@burningswell.com> <87y0yvdxej.fsf@gnu.org>
<867c6e90ei.fsf@burningswell.com>
User-Agent: mu4e 1.12.8; emacs 29.4
Hi Ludo,

I just sent v3 of the patch series in which I added test. There are now unit
and integration tests. You can run them with:

./pre-inst-env make check TESTS="tests/machine/hetzner/http.scm"
./pre-inst-env make check TESTS="tests/machine/hetzner.scm"

The integration tests require network access and the GUIX_HETZNER_API_TOKEN
environment variable to be set, otherwise they are skipped.

Can you have another look please?

And Christopher Baines, since Ludo mentioned you have a Hetzner account, would
you be interested in trying this out and provide some feedback?

Things to improve another day:

- Get Hetzner to add a Guix image to their collectin of supported images. That
would remove the need for using the rescue system to install an initial Guix system.

- Installing the initial Guix system via the rescue system is kind of slow
(especially if there are no substituyes), and done in sequence. I'm not sure
how this could be parallelized with how things are invoke by guix deploy.

Roman

Date: Tue, 04 Feb 2025 20:10:53 +0100

Roman Scherer <roman@burningswell.com> writes:

Toggle quote (38 lines)
> Hi Ludo,
>
> that's what I was looking for. Now it is working as expected!
>
> I will send an updated patch soon.
>
> Thanks for your help!
>
> Roman
>
> Ludovic Courtès <ludo@gnu.org> writes:
>
>> Hi,
>>
>> Roman Scherer <roman@burningswell.com> skribis:
>>
>>> When I run the mocked test I expect no code from the (gnu machine
>>> hetzner http) module to be executed, since I mocked all those
>>> functions. This seems to work in the Geiser REPL, but for some reason it
>>> does not work when I run the test with:
>>>
>>> ./pre-inst-env make check TESTS="tests/machine/hetzner.scm"
>>>
>>> To me it looks like the mock function behaves differently in those 2
>>> situations. In the meaintime I also tried setting -O0, but that didn't
>>> make any difference either. :/
>>
>> Hmm. I was going to say that the likely problem is that code from (gnu
>> machines hetzner http) gets inlined so you cannot really mock it.
>>
>> To make sure this can be mocked, you can use this trick:
>>
>> (set! proc proc)
>>
>> where ‘proc’ is the procedure you want to mock (that statement prevents
>> the compiler from inlining it).
>>
>> Ludo’.
-----BEGIN PGP SIGNATURE-----

iQFLBAEBCAA1FiEE0iajOdjfRIFd3gygPdpSUn0qwZkFAmeiZj0XHHJvbWFuQGJ1
cm5pbmdzd2VsbC5jb20ACgkQPdpSUn0qwZmAPwgAinUdwi4V0EyxDEC/DzYztdlo
mfUA9uPq/pK3eScunl0FJxf7eSXVdGocSlhwpdlc8PI3tSZZDAJO0heZyOYvSz/S
CIbQ1TN1GNip2FchVcr4RuRs2FaNrh7/l+j17HcpXS25siFFSOA3aaR5t0L4mHCT
5NEt40U+igXlXBBIoAuZo0EkF0HGkfwDLp2G0HvcX8WJ2rbm3G97KydyrgV2X+Ms
lNwOABPHC9MR9q3G8nNgZiuJGct0r8qC5L+Z9d6qPYFoLJDsiDxdJqPeMLnSGNvB
1GRCyGw/sKp9l9OwSIH5culqsOXpcmMHBd8docOsHklSHr0hb/V1S6NSx0SqgA==
=yTix
-----END PGP SIGNATURE-----

Maxim Cournoyer wrote 1 months ago
(name . Roman Scherer)(address . roman@burningswell.com)(name . Josselin Poiret)(address . dev@jpoiret.xyz)(name . Simon Tournier)(address . zimon.toutoune@gmail.com)(name . Mathieu Othacehe)(address . othacehe@gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)(name . Tobias Geerinckx-Rice)(address . me@tobias.gr)(name . Christopher Baines)(address . guix@cbaines.net)(address . 75144@debbugs.gnu.org)
87y0yh53f6.fsf@gmail.com
Hi Roman,

Roman Scherer <roman@burningswell.com> writes:

[...]

Toggle quote (9 lines)
> Things to improve another day:
>
> - Get Hetzner to add a Guix image to their collectin of supported images. That
> would remove the need for using the rescue system to install an initial Guix system.
>
> - Installing the initial Guix system via the rescue system is kind of slow
> (especially if there are no substituyes), and done in sequence. I'm not sure
> how this could be parallelized with how things are invoke by guix deploy.

Forgive my ignorance, but I thought the idea of a deploy <machine>
environment type was to allow fully provisioning the OS via the service
API?

I haven't reviewed the change yet; perhaps you mean that currently such
provision must happen by going through the rescue system path (but is
still automated by this new environment type?)

--
Thanks,
Maxim
Roman Scherer wrote 1 months ago
(name . Maxim Cournoyer)(address . maxim.cournoyer@gmail.com)(name . Josselin Poiret)(address . dev@jpoiret.xyz)(name . Simon Tournier)(address . zimon.toutoune@gmail.com)(name . Mathieu Othacehe)(address . othacehe@gnu.org)(name . Ludovic Courtè s)(address . ludo@gnu.org)(name . Tobias Geerinckx-Rice)(address . me@tobias.gr)(name . Roman Scherer)(address . roman@burningswell.com)(name . Christopher Baines)(address . guix@cbaines.net)(address . 75144@debbugs.gnu.org)
86y0yh6hai.fsf@burningswell.com
Hi Maxim,

yes, it is fully automated. What happens is:

- a server is provisioned through the Hetzner API
- the the server is booted into the rescue system via the API
- partitions are setup in the rescue system (enlarged)
- a minimal Guix system is installed
- then the server re-booted, starting the minimal Guix system
- then the machine-ssh-environment takes over and applies the final system configuration
- this all is done once, when the server is initially provisioned

Previsouly I tried the guix-infect.sh approach that installs a Guix
system on top of a debian/ubuntu image, but I found this was very
brittle (issues with dns when you remove /etc, etc.). From my experience
working with this I found the approach with the rescue system both more
reliable and faster.

Does this mnake sense?

Roman

Maxim Cournoyer <maxim.cournoyer@gmail.com> writes:

Toggle quote (22 lines)
> Hi Roman,
>
> Roman Scherer <roman@burningswell.com> writes:
>
> [...]
>
>> Things to improve another day:
>>
>> - Get Hetzner to add a Guix image to their collectin of supported images. That
>> would remove the need for using the rescue system to install an initial Guix system.
>>
>> - Installing the initial Guix system via the rescue system is kind of slow
>> (especially if there are no substituyes), and done in sequence. I'm not sure
>> how this could be parallelized with how things are invoke by guix deploy.
>
> Forgive my ignorance, but I thought the idea of a deploy <machine>
> environment type was to allow fully provisioning the OS via the service
> API?
>
> I haven't reviewed the change yet; perhaps you mean that currently such
> provision must happen by going through the rescue system path (but is
> still automated by this new environment type?)
-----BEGIN PGP SIGNATURE-----

iQFLBAEBCAA1FiEE0iajOdjfRIFd3gygPdpSUn0qwZkFAmemA/UXHHJvbWFuQGJ1
cm5pbmdzd2VsbC5jb20ACgkQPdpSUn0qwZnwhggAiGxyuPWgucR2wbEqM7js7hnw
FxcSfVhWhwxo0pfXHOlNnz2EapFTed832Fk3++ivTcfegiFwFoV+mNI7ab6WCXdf
TMTfhvJa7w7mQWtsfK3t+RevYCBbjpWZBm9lXsTRiy/Xm2/7NStOfZACjmjgdxy7
tRwHWrlOt+Dh5TVhxvrCGWaru9dNqDotNNqlkdEkkkbtriV4UI09pLA6DlBzJMCq
sEjeVpkpmDEFtT1olv8mNFva0nHhHCtJVaPzpgBof7/Gx2a0GqodOi5xCz9bd4mq
qKpMaAivVYsqACjrelQirW4OmmsCZKFL/osWBcnlL9Ptz56FIavqxIlxSXn9nQ==
=hJn2
-----END PGP SIGNATURE-----

Maxim Cournoyer wrote 1 months ago
(name . Roman Scherer)(address . roman@burningswell.com)(name . Josselin Poiret)(address . dev@jpoiret.xyz)(name . Simon Tournier)(address . zimon.toutoune@gmail.com)(name . Mathieu Othacehe)(address . othacehe@gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)(name . Tobias Geerinckx-Rice)(address . me@tobias.gr)(name . Christopher Baines)(address . guix@cbaines.net)(address . 75144@debbugs.gnu.org)
87frkpalv8.fsf@gmail.com
Hi Roman,

Roman Scherer <roman@burningswell.com> writes:

Toggle quote (20 lines)
> Hi Maxim,
>
> yes, it is fully automated. What happens is:
>
> - a server is provisioned through the Hetzner API
> - the the server is booted into the rescue system via the API
> - partitions are setup in the rescue system (enlarged)
> - a minimal Guix system is installed
> - then the server re-booted, starting the minimal Guix system
> - then the machine-ssh-environment takes over and applies the final system configuration
> - this all is done once, when the server is initially provisioned
>
> Previsouly I tried the guix-infect.sh approach that installs a Guix
> system on top of a debian/ubuntu image, but I found this was very
> brittle (issues with dns when you remove /etc, etc.). From my experience
> working with this I found the approach with the rescue system both more
> reliable and faster.
>
> Does this mnake sense?

Thanks for the clear explanation, it makes a lot of sense and it's
awesome that you could automate all that! It looks a lot like the
manual steps I had to go through to install Guix System on a cheap OVH
VPS [0]. It'd be fun to review if their API would allow automating all
that as what you did here for Hetzner. The nice thing with OVH is that
they do not place any upper limit on the amount of bandwidth consumed
(no extra billing), and it's quite inexpensive (I currently pay less
than 2 CAD/month, although that's only for the first year -- after it's
similar to Hetzner, about 6 CAD/month IIRC).


--
Thanks,
Maxim
Roman Scherer wrote 1 months ago
(name . Maxim Cournoyer)(address . maxim.cournoyer@gmail.com)(name . Josselin Poiret)(address . dev@jpoiret.xyz)(name . Simon Tournier)(address . zimon.toutoune@gmail.com)(name . Mathieu Othacehe)(address . othacehe@gnu.org)(name . Ludovic Courtè s)(address . ludo@gnu.org)(name . Tobias Geerinckx-Rice)(address . me@tobias.gr)(name . Roman Scherer)(address . roman@burningswell.com)(name . Christopher Baines)(address . guix@cbaines.net)(address . 75144@debbugs.gnu.org)
86ikpl7ku7.fsf@burningswell.com
Hi Maxim,

I'm not really familiar with the OVH rescue mode. But a quick search
showed up this:


So, if it works similar to the Hetzner rescue system, which I think it
does, and you can install guix on it (the package manager is enough) I
don't see why this approach should not work there as well.

Thanks, Roman

Maxim Cournoyer <maxim.cournoyer@gmail.com> writes:

Toggle quote (35 lines)
> Hi Roman,
>
> Roman Scherer <roman@burningswell.com> writes:
>
>> Hi Maxim,
>>
>> yes, it is fully automated. What happens is:
>>
>> - a server is provisioned through the Hetzner API
>> - the the server is booted into the rescue system via the API
>> - partitions are setup in the rescue system (enlarged)
>> - a minimal Guix system is installed
>> - then the server re-booted, starting the minimal Guix system
>> - then the machine-ssh-environment takes over and applies the final system configuration
>> - this all is done once, when the server is initially provisioned
>>
>> Previsouly I tried the guix-infect.sh approach that installs a Guix
>> system on top of a debian/ubuntu image, but I found this was very
>> brittle (issues with dns when you remove /etc, etc.). From my experience
>> working with this I found the approach with the rescue system both more
>> reliable and faster.
>>
>> Does this mnake sense?
>
> Thanks for the clear explanation, it makes a lot of sense and it's
> awesome that you could automate all that! It looks a lot like the
> manual steps I had to go through to install Guix System on a cheap OVH
> VPS [0]. It'd be fun to review if their API would allow automating all
> that as what you did here for Hetzner. The nice thing with OVH is that
> they do not place any upper limit on the amount of bandwidth consumed
> (no extra billing), and it's quite inexpensive (I currently pay less
> than 2 CAD/month, although that's only for the first year -- after it's
> similar to Hetzner, about 6 CAD/month IIRC).
>
> [0] https://lists.gnu.org/archive/html/help-guix/2024-08/msg00125.html
-----BEGIN PGP SIGNATURE-----

iQFLBAEBCAA1FiEE0iajOdjfRIFd3gygPdpSUn0qwZkFAmemO8AXHHJvbWFuQGJ1
cm5pbmdzd2VsbC5jb20ACgkQPdpSUn0qwZnQUwf+N/iZ4DSmF0FtpJwtWkdwFNNj
qjxs8C0KOpHyd0ohz5zZyztY8eq02HYDmYBEDU3aT3DB+ryh0Kdt4EngsJDpQgoY
J3feBuGDONxHbuE/G7KMam5/nv6bwKjGQTOjzPr8H43z8kPmRPeqnhy1K4LdH9og
F2jbmv1NiVRHhkKxTGOIFIiKK7nwGix/59m+EVb+FCRAUqCZPn0lhO1ASZWzb6Qb
8/PO3hK7Y9GO8PrwM9oMZteiTR40SC9saMSrrxYp/RcZv1z9eS+r2UaeIguUDi8A
NDOLFQ4+AiqNp6zsTxURCtI0eTJSGH2mz2tmLrg2vVBvFPYb+nyJl1aJZWGgsw==
=f9oh
-----END PGP SIGNATURE-----

Ludovic Courtès wrote 1 months ago
Re: [bug#75144] [PATCH v3 2/2] machine: Implement 'hetzner-environment-type'.
(name . Roman Scherer)(address . roman@burningswell.com)(name . Julien Lepiller)(address . julien@lepiller.eu)(name . Maxim Cournoyer)(address . maxim.cournoyer@gmail.com)(name . Florian Pelz)(address . pelzflorian@pelzflorian.de)(address . 75144@debbugs.gnu.org)
87h653qd7w.fsf@gnu.org
Hello Roman,

Applied with the one-line change below.

I wasn’t able to run tests that require an API token because I don’t
have one (but I may well give that a try eventually); other tests went
well.

Feel free to submit an entry for ‘etc/news.scm’ (make sure to provide
enough context so users can tell whether this is something of interest
to them). A blog post for guix.gnu.org/blog showing how you use it and
how it’s implemented would also be welcome if you feel so inclined!

Thanks for all the work!

Ludo’.
Ludovic Courtès wrote 1 months ago
control message for bug #75144
(address . control@debbugs.gnu.org)
87frknqd7n.fsf@gnu.org
close 75144
quit
Roman Scherer wrote 1 months ago
Re: [bug#75144] [PATCH v3 2/2] machine: Implement 'hetzner-environment-type'.
(name . Ludovic Courtès)(address . ludo@gnu.org)(name . Julien Lepiller)(address . julien@lepiller.eu)(name . Roman Scherer)(address . roman@burningswell.com)(name . Maxim Cournoyer)(address . maxim.cournoyer@gmail.com)(name . Florian Pelz)(address . pelzflorian@pelzflorian.de)(address . 75144@debbugs.gnu.org)
867c5x1s03.fsf@burningswell.com
Hi Ludo, and everyone still listening,

thanks for merging it and your help on this! I plan to submit a news
entry patch tomorrow.

I don't have the time for a blog post unfortunatly. Too busy with other
things at the moment, sorry. :/ Maybe another time.

Another feedback I wanted to mention. We should really aim to improve on
substitute availability and stability of Guix if we want people to rely
on Guix and `guix deploy`. I think this was also mentioned in the
survey.

While working on this the user experience of guix deploy really
shined/falled, depending on substitute availability and stability. I'm
probably biased and having bad luck with aarch-64 based Guix systems.

For example, using the ARM based servers (which are cheaper than x86)
with Guix on Hetzner can lead to a headache if you or the the servers
you deploy to start building Rust and friends. :/

I think we get there, thanks again, and happy hacking!

Roman

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

Toggle quote (16 lines)
> Hello Roman,
>
> Applied with the one-line change below.
>
> I wasn’t able to run tests that require an API token because I don’t
> have one (but I may well give that a try eventually); other tests went
> well.
>
> Feel free to submit an entry for ‘etc/news.scm’ (make sure to provide
> enough context so users can tell whether this is something of interest
> to them). A blog post for guix.gnu.org/blog showing how you use it and
> how it’s implemented would also be welcome if you feel so inclined!
>
> Thanks for all the work!
>
> Ludo’.
-----BEGIN PGP SIGNATURE-----

iQFLBAEBCAA1FiEE0iajOdjfRIFd3gygPdpSUn0qwZkFAmeqXPwXHHJvbWFuQGJ1
cm5pbmdzd2VsbC5jb20ACgkQPdpSUn0qwZkPdAgAyRHIETVJGhQCq5smY5ZnnUB6
/XwWjJvlYcodcgZUdO0JB3TMGvw8OT2kCcFN+GpZ+CpE21yiba7Iwa3TJcczI8XD
Q+EY+EJXAoCMi9TQzvnICz5CT6ARmjZA4HzoW1I+wJxtGk4RxNpP2bcAr6H5XFHr
NGJPUJU3j6LEcZ42t1e4a/pcae1O3LG0QkvyD/9Y+rUUhejuXd+gD4emQ/MaCV8H
kswTNW9Kg9LcDlW3HDfNziM6LbNW2M0MeaeQifwM5hXqjzfYmvPOIEWj21mLBNdB
c+QiQ+58/t047CPl5O2BUPRG8NA7ej32aJyg6RK60hsodj8VqgdzszH4VdGV1Q==
=SVUp
-----END PGP SIGNATURE-----

Roman Scherer wrote 1 months ago
[PATCH] news: Add entry for 'hetzner-environment-type'
(address . 75144@debbugs.gnu.org)(name . Roman Scherer)(address . roman@burningswell.com)
404da01b9d49158bda7764d75e215100afb15fda.1739265567.git.roman@burningswell.com
* etc/news.scm: Add entry.

Change-Id: I7d2575d8e69855516cbf4c3747a23c344890321a
---
etc/news.scm | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)

Toggle diff (40 lines)
diff --git a/etc/news.scm b/etc/news.scm
index dfc64d59cd..147972548c 100644
--- a/etc/news.scm
+++ b/etc/news.scm
@@ -27,6 +27,8 @@
;; Copyright © 2024 Zheng Junjie <873216071@qq.com>
;; Copyright © 2024 Nicolas Graves <ngraves@ngraves.fr>
;; Copyright © 2024 Sebastian Dümcke <code@sam-d.com>
+;; Copyright © 2024 Roman Scherer <roman@burningswell.com>
+
;;
;; Copying and distribution of this file, with or without modification, are
;; permitted in any medium without royalty provided the copyright notice and
@@ -35,6 +37,22 @@
(channel-news
(version 0)
+ (entry (commit "0753a17ddf6f4fab98b93c25f1a93b97ff9e46bb")
+ (title
+ (en "The @command{guix deploy} command now supports the Hetzner Cloud
+service"))
+ (body
+ (en "In addition to deploying machines over SSH and on the Digital
+Ocean cloud service, the @command{guix deploy} command now supports deployment
+on the Hetzner Cloud service as well. When deploying a machine with the new
+@code{hetzner-environment-type}, a @acronym{VPS, virtual private server} will
+be provisioned on the Hetzner Cloud, and the machine configuration's operating
+system will be installed on it. Provisioning happens through the Hetzner
+Cloud API and you need to set the @code{GUIX_HETZNER_API_TOKEN} environment
+variable to a Hetzner Cloud API token. Additionally, you can use the
+@code{hetzner-configuration} record to customize the deployment, such as the
+system architecture, type of VPS, etc.")))
+
(entry (commit "616ae36e0f557cecb4abe58c5b0973b9428d25e0")
(title
(en "Kernel persistent storage in UEFI disabled"))

base-commit: d7ca62b15de7ef89c88ef9b1118d29481ca50122
--
2.48.1
Roman Scherer wrote 1 months ago
Re: [bug#75144] [PATCH v3 2/2] machine: Implement 'hetzner-environment-type'.
(name . Roman Scherer)(address . roman@burningswell.com)(name . Julien Lepiller)(address . julien@lepiller.eu)(name . Ludovic Courtès)(address . ludo@gnu.org)(name . Maxim Cournoyer)(address . maxim.cournoyer@gmail.com)(name . Florian Pelz)(address . pelzflorian@pelzflorian.de)(address . 75144@debbugs.gnu.org)
86v7tgzve8.fsf@burningswell.com
Hello,

I just send another patch for the news entry to this bug number.

Could someone please review it?

Thanks, Roman

Roman Scherer <roman@burningswell.com> writes:

Toggle quote (43 lines)
> Hi Ludo, and everyone still listening,
>
> thanks for merging it and your help on this! I plan to submit a news
> entry patch tomorrow.
>
> I don't have the time for a blog post unfortunatly. Too busy with other
> things at the moment, sorry. :/ Maybe another time.
>
> Another feedback I wanted to mention. We should really aim to improve on
> substitute availability and stability of Guix if we want people to rely
> on Guix and `guix deploy`. I think this was also mentioned in the
> survey.
>
> While working on this the user experience of guix deploy really
> shined/falled, depending on substitute availability and stability. I'm
> probably biased and having bad luck with aarch-64 based Guix systems.
>
> For example, using the ARM based servers (which are cheaper than x86)
> with Guix on Hetzner can lead to a headache if you or the the servers
> you deploy to start building Rust and friends. :/
>
> I think we get there, thanks again, and happy hacking!
>
> Roman
>
> Ludovic Courtès <ludo@gnu.org> writes:
>
>> Hello Roman,
>>
>> Applied with the one-line change below.
>>
>> I wasn’t able to run tests that require an API token because I don’t
>> have one (but I may well give that a try eventually); other tests went
>> well.
>>
>> Feel free to submit an entry for ‘etc/news.scm’ (make sure to provide
>> enough context so users can tell whether this is something of interest
>> to them). A blog post for guix.gnu.org/blog showing how you use it and
>> how it’s implemented would also be welcome if you feel so inclined!
>>
>> Thanks for all the work!
>>
>> Ludo’.
-----BEGIN PGP SIGNATURE-----

iQFLBAEBCAA1FiEE0iajOdjfRIFd3gygPdpSUn0qwZkFAmerF08XHHJvbWFuQGJ1
cm5pbmdzd2VsbC5jb20ACgkQPdpSUn0qwZlBBAf+LNVqUBYvFHU6IJdiSFFJZPyk
9pd/0+rg3DY575GuL8ZFE9w7nj9ZnRXHL1zI0j8n5BlZlH4wDgEFh+b9qsbYzkOL
yVpjZPX9IZQ575pMmpfkaAXdysr8nqoyNVkU627FWgHe5cWtCKTnYZTim8D0Ryzm
IDPHsnVutk1DhbtQtRafb01nAYffjAifv3dOolJc1/5dacY80i4N4bOJN4xtk/sI
+jXEEGz/lV4fEWU9q4OK1Hqh7fkG4i935lNbat8mDRnkPVXj2svCNj6Fhk5+ZvRf
fZ24nN2sfwMIhH4wQ/Ilak2s4M9/Cxw+kMaXeo4dUFaZKVaBRHADXkbl48DMwQ==
=XNI+
-----END PGP SIGNATURE-----

pelzflorian (Florian Pelz) wrote 1 months ago
Re: [bug#75144] [PATCH] news: Add entry for 'hetzner-environment-type'
(name . Roman Scherer)(address . roman@burningswell.com)(name . Julien Lepiller)(address . julien@lepiller.eu)(name . Maxim Cournoyer)(address . maxim.cournoyer@gmail.com)(name . Ludovic Courtès)(address . ludo@gnu.org)(address . 75144-done@debbugs.gnu.org)
87frkk1r9a.fsf@pelzflorian.de
Pushed the news as 0bf82b3fd5fcca2baef872ee06b40995cfbba7df
with an added German translation.

I set your copyright year to 2025, though. I hope the 2024 you wrote
had been a typo. Also I ended the commit message’s first line with a
period.

Hetzner support is exciting news, though I have no account there and
have not tested.

By the way I just had to build rust’s bootstrap chain, before I could
install Guix System on my new ARM machine with which I committed your
patch. Board info for its RAM is not free software; maybe substitutes
are missing for lack of fast freedom-respecting ARM devices.

Regards,
Florian
Closed
Roman Scherer wrote 1 months ago
(name . pelzflorian (Florian Pelz))(address . pelzflorian@pelzflorian.de)(name . Roman Scherer)(address . roman@burningswell.com)(name . Maxim Cournoyer)(address . maxim.cournoyer@gmail.com)(name . Ludovic Courtès)(address . ludo@gnu.org)(name . Julien Lepiller)(address . julien@lepiller.eu)(address . 75144-done@debbugs.gnu.org)
87a5asa4hi.fsf@burningswell.com
Hi Florian,

thanks for applying the patch!

"pelzflorian (Florian Pelz)" <pelzflorian@pelzflorian.de> writes:

Toggle quote (7 lines)
> Pushed the news as 0bf82b3fd5fcca2baef872ee06b40995cfbba7df
> with an added German translation.
>
> I set your copyright year to 2025, though. I hope the 2024 you wrote
> had been a typo. Also I ended the commit message’s first line with a
> period.

Yes, it was a typo. I worked on this in 2024 and 2025. Thanks for fixing it.

Toggle quote (8 lines)
> Hetzner support is exciting news, though I have no account there and
> have not tested.
>
> By the way I just had to build rust’s bootstrap chain, before I could
> install Guix System on my new ARM machine with which I committed your
> patch. Board info for its RAM is not free software; maybe substitutes
> are missing for lack of fast freedom-respecting ARM devices.

Yes, I also ran into this multiple times. I also run my own substitute
server for my asahi guix channel on Hetzner. That helps a bit. I don't
have hard data on this, but my feeling is it builds the Gnome/KDE
desktops, and Rust bootstrap chain more reliable/faster than the Guix
infrastructure. But I might be wrong.

Toggle quote (2 lines)
> Regards,
> Florian
-----BEGIN PGP SIGNATURE-----

iQFLBAEBCAA1FiEE0iajOdjfRIFd3gygPdpSUn0qwZkFAmera8oXHHJvbWFuQGJ1
cm5pbmdzd2VsbC5jb20ACgkQPdpSUn0qwZmOrAf+KK1hiV+gl/TU6XIf/1SWQ7Fy
l5t6zMZRkw9a3Bpz62rWeHCyqiA66YlteCQdlguF0RbvLzXQ9M85bXZlP0Y2AYOa
kyZ1CRJ3DWg+hO4MUawP8K7s2p46exZ5ZIiVSZamGwsP29YeRCb6tv2AJ/DrOutM
kC5UGUNtO98CadKgMQG9ozBYdzcXiaVu66r2v9WaYhqCzXqM64wv2N1SnO+Y7Cvs
sagcm845IKYT2QbjHvL6kDmkdSmhu8sdRMLBo2gGOpe2ns8GBCDoGOMuq69VgoPT
XoQzTXRtu6QB+6spbEKuU19RtvFPPGuhCtXSlDGoGuWNfiIUc21AiinbNekXeQ==
=YGHw
-----END PGP SIGNATURE-----

Closed
Ludovic Courtès wrote 3 weeks ago
Re: [bug#75144] [PATCH v3 2/2] machine: Implement 'hetzner-environment-type'.
(name . Roman Scherer)(address . roman@burningswell.com)(name . Julien Lepiller)(address . julien@lepiller.eu)(name . Maxim Cournoyer)(address . maxim.cournoyer@gmail.com)(name . Florian Pelz)(address . pelzflorian@pelzflorian.de)(address . 75144@debbugs.gnu.org)
878qpzeioa.fsf@gnu.org
Hi,

Roman Scherer <roman@burningswell.com> skribis:

Toggle quote (8 lines)
> While working on this the user experience of guix deploy really
> shined/falled, depending on substitute availability and stability. I'm
> probably biased and having bad luck with aarch-64 based Guix systems.
>
> For example, using the ARM based servers (which are cheaper than x86)
> with Guix on Hetzner can lead to a headache if you or the the servers
> you deploy to start building Rust and friends. :/

Yup, I agree. bordeaux.guix used to have very high substitute
availability for aarch64, while ci.guix has always been lagging behind,
mostly because it’s underpowered in aarch64. For a couple of months,
bordeaux.guix was also lagging behind on all architectures, but that
appears to be fixed now:


Ludo’.
Roman Scherer wrote 3 weeks ago
(name . Ludovic Courtès)(address . ludo@gnu.org)(name . Julien Lepiller)(address . julien@lepiller.eu)(name . Roman Scherer)(address . roman@burningswell.com)(name . Maxim Cournoyer)(address . maxim.cournoyer@gmail.com)(name . Florian Pelz)(address . pelzflorian@pelzflorian.de)(address . 75144@debbugs.gnu.org)
86ldtzxabu.fsf@burningswell.com
Hi Ludo,

thanks for the insights. It's great bordeaux catched up again.

Roman

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

Toggle quote (21 lines)
> Hi,
>
> Roman Scherer <roman@burningswell.com> skribis:
>
>> While working on this the user experience of guix deploy really
>> shined/falled, depending on substitute availability and stability. I'm
>> probably biased and having bad luck with aarch-64 based Guix systems.
>>
>> For example, using the ARM based servers (which are cheaper than x86)
>> with Guix on Hetzner can lead to a headache if you or the the servers
>> you deploy to start building Rust and friends. :/
>
> Yup, I agree. bordeaux.guix used to have very high substitute
> availability for aarch64, while ci.guix has always been lagging behind,
> mostly because it’s underpowered in aarch64. For a couple of months,
> bordeaux.guix was also lagging behind on all architectures, but that
> appears to be fixed now:
>
> https://qa.guix.gnu.org/branch/master
>
> Ludo’.
-----BEGIN PGP SIGNATURE-----

iQFLBAEBCAA1FiEE0iajOdjfRIFd3gygPdpSUn0qwZkFAme47nUXHHJvbWFuQGJ1
cm5pbmdzd2VsbC5jb20ACgkQPdpSUn0qwZkc+AgAgm16xpe3YZkzu/z6Ws5y76Nv
6EhbFnvDMBxHVLeubJOIW7mGINu9CqZJoJlRWWmHCqEOhvGQd+rRPM/orDOHRzbN
aDt18IEj+yCLOewHbGK90SxgeovAoZF3gOe0tarhZABLaLy7cmWz+aA0+S+wijEX
SiAzdQ5An/n4X/l+fby1xc3XVWkkgBkOGhdMJGGFjIL7GY3+YrpO/0jHoJWkEawd
+w/uTogL4inTjX8grQm+KeZ9wtfgn6disCfTSTBVksiENG4eLiL7mbPyY/PRSC2I
VNd4gEt+tWfIDgfvaZjWJxstOFIFHhpPbLeYOUYCHzb14R8EQsN710cR+Rv1yQ==
=Inza
-----END PGP SIGNATURE-----

Sergey Trofimov wrote 35 hours ago
Re: [bug#75144] [PATCH] machine: Implement 'hetzner-environment-type'.
(name . Roman Scherer)(address . roman@burningswell.com)(name . Josselin Poiret)(address . dev@jpoiret.xyz)(name . Maxim Cournoyer)(address . maxim.cournoyer@gmail.com)(name . Simon Tournier)(address . zimon.toutoune@gmail.com)(name . Mathieu Othacehe)(address . othacehe@gnu.org)(name . Ludovic Court?s)(address . ludo@gnu.org)(name . Tobias Geerinckx-Rice)(address . me@tobias.gr)(name . Christopher Baines)(address . guix@cbaines.net)(address . 75144@debbugs.gnu.org)
87jz8t451l.fsf@sarg.org.ru
Hi Roman,

Roman Scherer <roman@burningswell.com> writes:

Toggle quote (7 lines)
> * gnu/machine/hetzner.scm: New file.
> * gnu/local.mk (GNU_SYSTEM_MODULES): Add it.
> * guix/ssh.scm (open-ssh-session): Add stricthostkeycheck option.
> * doc/guix.texi (Invoking guix deploy): Add documentation for
> 'hetzner-configuration'.
>

That's a very welcome change and I'm glad it is merged already. I've
given it a try and stumbled upon some errors. See them below.

I started with the minimal configuration similar to the example from docs.
Toggle snippet (6 lines)
(hetzner-configuration
(server-type "cax11")
(location "hel1")
(ssh-key ".../id_rsa"))

Deploying it worked only half-way - the server got created, but
deploying actual OS failed due to my host (x86_64) not able to build
derivations for aarch64-linux.

This one I fixed by adding `qemu-binfmt-service-type` to my system config.
Second deployment attempt picked up the work (that's nice!) and started
building the derivations. After about 40 minutes I'd aborted it,
although I'm pretty sure it would've completed successfully. I just
didn't want to wait too long.

Next I've addded `(build-locally? #f)` to the VM config and repeated the
deployment. This one progressed much quicker, however it failed while
building `linux-modules`:

Toggle snippet (28 lines)
building path(s) `/gnu/store/lsa1716vbccxf9flpnzbfqzbm9rh4ljl-linux-modules'
Backtrace:
18 (primitive-load "/gnu/store/mfk843zl41s21banhzwkyfdxapa?")
In ice-9/eval.scm:
619:8 17 (_ #f)
626:19 16 (_ #<directory (guile-user) fffff771ec80>)
293:34 15 (_ #(#<directory (guile-user) fffff771ec80> #<procedu?>))
In srfi/srfi-1.scm:
586:29 14 (map1 _)
586:29 13 (map1 _)
586:29 12 (map1 _)
586:29 11 (map1 _)
586:29 10 (map1 _)
586:29 9 (map1 _)
586:29 8 (map1 _)
586:29 7 (map1 _)
586:29 6 (map1 _)
586:29 5 (map1 _)
586:29 4 (map1 _)
586:29 3 (map1 _)
586:29 2 (map1 _)
586:17 1 (map1 ("pata_acpi" "pata_atiixp" "isci" "virtio_pci" # ?))
In gnu/build/linux-modules.scm:
278:5 0 (_)

gnu/build/linux-modules.scm:278:5: kernel module not found "pata_acpi" "/gnu/store/nh5icvr5qvlaq1y54gpkqndy0rv2cq9r-linux-libre-6.13.6/lib/modules"

This seem to be caused by `deploy` not supporting `--target` parameter.
Adding these looked simple and I've jotted a small patch:

Toggle snippet (94 lines)
From 0d438d2fadc95fbe2eca73fc3c7f4278d58829d7 Mon Sep 17 00:00:00 2001
Message-ID: <0d438d2fadc95fbe2eca73fc3c7f4278d58829d7.1741858564.git.sarg@sarg.org.ru>
From: Sergey Trofimov <sarg@sarg.org.ru>
Subject: [PATCH] Support --target and --system in guix deploy

---
guix/scripts/deploy.scm | 28 +++++++++++++++++++---------
1 file changed, 19 insertions(+), 9 deletions(-)

diff --git a/guix/scripts/deploy.scm b/guix/scripts/deploy.scm
index e2ef0006e0..5b6c6b8e79 100644
--- a/guix/scripts/deploy.scm
+++ b/guix/scripts/deploy.scm
@@ -26,6 +26,7 @@ (define-module (guix scripts deploy)
#:use-module (guix scripts)
#:use-module (guix scripts build)
#:use-module (guix store)
+ #:use-module (guix utils)
#:use-module (guix gexp)
#:use-module (guix ui)
#:use-module ((guix status) #:select (with-status-verbosity))
@@ -93,21 +94,22 @@ (define %options
(option '(#\x "execute") #f #f
(lambda (opt name arg result)
(alist-cons 'execute-command? #t result)))
- (option '(#\s "system") #t #f
- (lambda (opt name arg result)
- (alist-cons 'system arg
- (alist-delete 'system result eq?))))
(option '(#\v "verbosity") #t #f
(lambda (opt name arg result)
(let ((level (string->number* arg)))
(alist-cons 'verbosity level
(alist-delete 'verbosity result)))))

- %standard-build-options))
+ (append
+ %standard-build-options
+ %standard-native-build-options
+ %standard-cross-build-options)))

(define %default-options
;; Alist of default option values.
`((verbosity . 1)
+ (system . ,(%current-system))
+ (target . #f)
(debug . 0)
(graft? . #t)
(substitutes? . #t)
@@ -186,9 +188,13 @@ (define (deploy-machine* store machine)
(when (deploy-error-should-roll-back c)
(info (G_ "rolling back ~a...~%")
(machine-display-name machine))
- (run-with-store store (roll-back-machine machine)))
+ (run-with-store store (roll-back-machine machine)
+ #:system (%current-system)
+ #:target (%current-target-system)))
(apply throw (deploy-error-captured-args c))))
- (run-with-store store (deploy-machine machine))
+ (run-with-store store (deploy-machine machine)
+ #:system (%current-system)
+ #:target (%current-target-system))

(info (G_ "successfully deployed ~a~%")
(machine-display-name machine))))
@@ -266,7 +272,9 @@ (define (invoke-command store machine command)
(loop (cons line lines))))))))

(match (run-with-store store
- (machine-remote-eval machine invocation))
+ (machine-remote-eval machine invocation)
+ #:system (%current-system)
+ #:target (%current-target-system))
((code output)
(match code
((? zero?)
@@ -325,7 +333,9 @@ (define-command (guix-deploy . args)
#:verbosity
(assoc-ref opts 'verbosity)
#:dry-run? dry-run?)
- (parameterize ((%graft? (assq-ref opts 'graft?)))
+ (parameterize ((%graft? (assq-ref opts 'graft?))
+ (%current-target-system (assoc-ref opts 'target))
+ (%current-system (assoc-ref opts 'system)))
(if execute-command?
(match command
(("--" command ..1)

base-commit: 9449ab3c2025820d2e6fd679fa7e34832b667ea7
--
2.48.1


I wasn't able to confirm the patch works as during the deployment it
tries to build the toolchain which I can't afford on my host:

Toggle snippet (17 lines)
The following derivations will be built:
/gnu/store/gxr8v1yisdiyndka0abxrc0xzrra66sv-binutils-cross-aarch64-linux-gnu-2.41.drv
/gnu/store/lch3711iiczn6smxsr7r3sj991p8avwv-ld-wrapper-aarch64-linux-gnu-0.drv
/gnu/store/zmsnlbyml0vmphfdxyxw4ps25bgrwz92-gcc-cross-sans-libc-aarch64-linux-gnu-14.2.0.drv
/gnu/store/57jnlmvqlvk6jkyvqcnrk4psffhmak91-linux-libre-headers-cross-aarch64-linux-gnu-5.15.49.drv
/gnu/store/b4f1my595ggl7d5qn46vr6qllwx7g49z-glibc-cross-aarch64-linux-gnu-2.39.drv
/gnu/store/sl5vfnwdarghf9ypbspq1bdlamnz3j2a-gcc-cross-aarch64-linux-gnu-14.2.0.drv
/gnu/store/3vp8a7mz1576xbk278k9b73nx2zqmzlw-libffi-3.4.4.drv
/gnu/store/5ij0pv5z7mi25r397y4k62ma7q38qrka-pkg-config-aarch64-linux-gnu-0.29.2.drv
/gnu/store/y3hqwsbc8rb2g1mac8c9vsdmaacf20xm-libatomic-ops-7.6.12.drv
/gnu/store/bd09d178ni5sp9db62w869c6m7d3sh6v-libgc-8.2.4.drv
/gnu/store/cs7mzhrypgdad8v0v29arafc8brl7ynd-bash-minimal-5.1.16.drv
/gnu/store/np51g0ak713az6shj6sv9j3wkq4cjvjx-libunistring-1.1.drv
/gnu/store/rbkb4ig158h9gblbrah5nx5annvfpb4q-libxcrypt-4.4.36.drv
/gnu/store/lfmamfv5vx690l9n6a1ixbbk6kzw3gsr-guile-3.0.9.drv

Finally, I want to highlight a couple things that I haven't figured out
for my use-case yet:
1. My private ssh key is stored in GnuPG and I'd like to keep it that
way. Afaik `managed-host-environment-type` can utilise the running
ssh-agent, could it be also implemented for hetzner machines?

2. My use-case is an on-demand wireguard VPN. In my current setup I have
created a static ipv6 address which I attach to the VM created using
`hcloud`. The wireguard config hardcodes the same ipv6 and is installed
on the VM during cloud-init provision (`--user-data-from-file`
parameter). To replicate the same in guix deploy,
`hetzner-configuration` should be more flexible in regards to public ip
addresses. I.e. it should allow to use either v4 or v6 and to accept
existing one provided by the user.
Roman Scherer wrote 8 hours ago
(name . Sergey Trofimov)(address . sarg@sarg.org.ru)(name . Roman Scherer)(address . roman@burningswell.com)(name . Maxim Cournoyer)(address . maxim.cournoyer@gmail.com)(name . Simon Tournier)(address . zimon.toutoune@gmail.com)(name . Mathieu Othacehe)(address . othacehe@gnu.org)(name . Ludovic Court?s)(address . ludo@gnu.org)(name . Tobias Geerinckx-Rice)(address . me@tobias.gr)(name . Josselin Poiret)(address . dev@jpoiret.xyz)(name . Christopher Baines)(address . guix@cbaines.net)(address . 75144@debbugs.gnu.org)
8734ffbxtu.fsf@burningswell.com
Sergey Trofimov <sarg@sarg.org.ru> writes:

Hi Sergey,

thanks for trying this out and your feedback.

Toggle quote (26 lines)
> Hi Roman,
>
> Roman Scherer <roman@burningswell.com> writes:
>
> > * gnu/machine/hetzner.scm: New file.
> > * gnu/local.mk (GNU_SYSTEM_MODULES): Add it.
> > * guix/ssh.scm (open-ssh-session): Add stricthostkeycheck option.
> > * doc/guix.texi (Invoking guix deploy): Add documentation for
> > 'hetzner-configuration'.
> >
>
> That's a very welcome change and I'm glad it is merged already. I've
> given it a try and stumbled upon some errors. See them below.
>
> I started with the minimal configuration similar to the example from docs.
> --8<---------------cut here---------------start------------->8---
> (hetzner-configuration
> (server-type "cax11")
> (location "hel1")
> (ssh-key ".../id_rsa"))
> --8<---------------cut here---------------end--------------->8---
>
> Deploying it worked only half-way - the server got created, but
> deploying actual OS failed due to my host (x86_64) not able to build
> derivations for aarch64-linux.

You need some way to be able to build for the target
architecture. qemu-binfmt-service-type is one solution to it, I
personally use offloading, since I have machines with different
architectures available.

Toggle quote (6 lines)
> This one I fixed by adding `qemu-binfmt-service-type` to my system config.
> Second deployment attempt picked up the work (that's nice!) and started
> building the derivations. After about 40 minutes I'd aborted it,
> although I'm pretty sure it would've completed successfully. I just
> didn't want to wait too long.

I also have seen these long deploy times. You probably run into a
situation where substitutes were not available for the thing you are
deploying. It's a know issue with aarch64.

Toggle quote (4 lines)
> Next I've addded `(build-locally? #f)` to the VM config and repeated the
> deployment. This one progressed much quicker, however it failed while
> building `linux-modules`:

I usually had the best experience with build-locally? set to #t, since
for further deploys I have the build artifacts in my local store, and
not have to build them again and again on the servers I'm deploying to.

Toggle quote (33 lines)
> --8<---------------cut here---------------start------------->8---
> building path(s) `/gnu/store/lsa1716vbccxf9flpnzbfqzbm9rh4ljl-linux-modules'
> Backtrace:
> 18 (primitive-load "/gnu/store/mfk843zl41s21banhzwkyfdxapa?")
> In ice-9/eval.scm:
> 619:8 17 (_ #f)
> 626:19 16 (_ #<directory (guile-user) fffff771ec80>)
> 293:34 15 (_ #(#<directory (guile-user) fffff771ec80> #<procedu?>))
> In srfi/srfi-1.scm:
> 586:29 14 (map1 _)
> 586:29 13 (map1 _)
> 586:29 12 (map1 _)
> 586:29 11 (map1 _)
> 586:29 10 (map1 _)
> 586:29 9 (map1 _)
> 586:29 8 (map1 _)
> 586:29 7 (map1 _)
> 586:29 6 (map1 _)
> 586:29 5 (map1 _)
> 586:29 4 (map1 _)
> 586:29 3 (map1 _)
> 586:29 2 (map1 _)
> 586:17 1 (map1 ("pata_acpi" "pata_atiixp" "isci" "virtio_pci" # ?))
> In gnu/build/linux-modules.scm:
> 278:5 0 (_)
>
> gnu/build/linux-modules.scm:278:5: kernel module not found "pata_acpi" "/gnu/store/nh5icvr5qvlaq1y54gpkqndy0rv2cq9r-linux-libre-6.13.6/lib/modules"
> --8<---------------cut here---------------end--------------->8---
>
> This seem to be caused by `deploy` not supporting `--target` parameter.
> Adding these looked simple and I've jotted a small patch:
>

Nice! Do you plan to submit this as a patch, once you got it working?

Toggle quote (122 lines)
> --8<---------------cut here---------------start------------->8---
> From 0d438d2fadc95fbe2eca73fc3c7f4278d58829d7 Mon Sep 17 00:00:00 2001
> Message-ID: <0d438d2fadc95fbe2eca73fc3c7f4278d58829d7.1741858564.git.sarg@sarg.org.ru>
> From: Sergey Trofimov <sarg@sarg.org.ru>
> Subject: [PATCH] Support --target and --system in guix deploy
>
> ---
> guix/scripts/deploy.scm | 28 +++++++++++++++++++---------
> 1 file changed, 19 insertions(+), 9 deletions(-)
>
> diff --git a/guix/scripts/deploy.scm b/guix/scripts/deploy.scm
> index e2ef0006e0..5b6c6b8e79 100644
> --- a/guix/scripts/deploy.scm
> +++ b/guix/scripts/deploy.scm
> @@ -26,6 +26,7 @@ (define-module (guix scripts deploy)
> #:use-module (guix scripts)
> #:use-module (guix scripts build)
> #:use-module (guix store)
> + #:use-module (guix utils)
> #:use-module (guix gexp)
> #:use-module (guix ui)
> #:use-module ((guix status) #:select (with-status-verbosity))
> @@ -93,21 +94,22 @@ (define %options
> (option '(#\x "execute") #f #f
> (lambda (opt name arg result)
> (alist-cons 'execute-command? #t result)))
> - (option '(#\s "system") #t #f
> - (lambda (opt name arg result)
> - (alist-cons 'system arg
> - (alist-delete 'system result eq?))))
> (option '(#\v "verbosity") #t #f
> (lambda (opt name arg result)
> (let ((level (string->number* arg)))
> (alist-cons 'verbosity level
> (alist-delete 'verbosity result)))))
>
> - %standard-build-options))
> + (append
> + %standard-build-options
> + %standard-native-build-options
> + %standard-cross-build-options)))
>
> (define %default-options
> ;; Alist of default option values.
> `((verbosity . 1)
> + (system . ,(%current-system))
> + (target . #f)
> (debug . 0)
> (graft? . #t)
> (substitutes? . #t)
> @@ -186,9 +188,13 @@ (define (deploy-machine* store machine)
> (when (deploy-error-should-roll-back c)
> (info (G_ "rolling back ~a...~%")
> (machine-display-name machine))
> - (run-with-store store (roll-back-machine machine)))
> + (run-with-store store (roll-back-machine machine)
> + #:system (%current-system)
> + #:target (%current-target-system)))
> (apply throw (deploy-error-captured-args c))))
> - (run-with-store store (deploy-machine machine))
> + (run-with-store store (deploy-machine machine)
> + #:system (%current-system)
> + #:target (%current-target-system))
>
> (info (G_ "successfully deployed ~a~%")
> (machine-display-name machine))))
> @@ -266,7 +272,9 @@ (define (invoke-command store machine command)
> (loop (cons line lines))))))))
>
> (match (run-with-store store
> - (machine-remote-eval machine invocation))
> + (machine-remote-eval machine invocation)
> + #:system (%current-system)
> + #:target (%current-target-system))
> ((code output)
> (match code
> ((? zero?)
> @@ -325,7 +333,9 @@ (define-command (guix-deploy . args)
> #:verbosity
> (assoc-ref opts 'verbosity)
> #:dry-run? dry-run?)
> - (parameterize ((%graft? (assq-ref opts 'graft?)))
> + (parameterize ((%graft? (assq-ref opts 'graft?))
> + (%current-target-system (assoc-ref opts 'target))
> + (%current-system (assoc-ref opts 'system)))
> (if execute-command?
> (match command
> (("--" command ..1)
>
> base-commit: 9449ab3c2025820d2e6fd679fa7e34832b667ea7
> --
> 2.48.1
>
> --8<---------------cut here---------------end--------------->8---
>
> I wasn't able to confirm the patch works as during the deployment it
> tries to build the toolchain which I can't afford on my host:
>
> --8<---------------cut here---------------start------------->8---
> The following derivations will be built:
> /gnu/store/gxr8v1yisdiyndka0abxrc0xzrra66sv-binutils-cross-aarch64-linux-gnu-2.41.drv
> /gnu/store/lch3711iiczn6smxsr7r3sj991p8avwv-ld-wrapper-aarch64-linux-gnu-0.drv
> /gnu/store/zmsnlbyml0vmphfdxyxw4ps25bgrwz92-gcc-cross-sans-libc-aarch64-linux-gnu-14.2.0.drv
> /gnu/store/57jnlmvqlvk6jkyvqcnrk4psffhmak91-linux-libre-headers-cross-aarch64-linux-gnu-5.15.49.drv
> /gnu/store/b4f1my595ggl7d5qn46vr6qllwx7g49z-glibc-cross-aarch64-linux-gnu-2.39.drv
> /gnu/store/sl5vfnwdarghf9ypbspq1bdlamnz3j2a-gcc-cross-aarch64-linux-gnu-14.2.0.drv
> /gnu/store/3vp8a7mz1576xbk278k9b73nx2zqmzlw-libffi-3.4.4.drv
> /gnu/store/5ij0pv5z7mi25r397y4k62ma7q38qrka-pkg-config-aarch64-linux-gnu-0.29.2.drv
> /gnu/store/y3hqwsbc8rb2g1mac8c9vsdmaacf20xm-libatomic-ops-7.6.12.drv
> /gnu/store/bd09d178ni5sp9db62w869c6m7d3sh6v-libgc-8.2.4.drv
> /gnu/store/cs7mzhrypgdad8v0v29arafc8brl7ynd-bash-minimal-5.1.16.drv
> /gnu/store/np51g0ak713az6shj6sv9j3wkq4cjvjx-libunistring-1.1.drv
> /gnu/store/rbkb4ig158h9gblbrah5nx5annvfpb4q-libxcrypt-4.4.36.drv
> /gnu/store/lfmamfv5vx690l9n6a1ixbbk6kzw3gsr-guile-3.0.9.drv
> --8<---------------cut here---------------end--------------->8---
>
> Finally, I want to highlight a couple things that I haven't figured out
> for my use-case yet:
> 1. My private ssh key is stored in GnuPG and I'd like to keep it that
> way. Afaik `managed-host-environment-type` can utilise the running
> ssh-agent, could it be also implemented for hetzner machines?

Your public key needs to be added as an SSH key via the Hetzner API. I
believe the guix deploy command is doing the same here as the digital
ocean one. It takes the ssh key from the machine config and creates the
public key with the Hetzner API on the server.

Maybe we could also support specifiy a fingerprint in the machine
configuration and somehow get the public ssh key for it somehow from
your GPG agent in Guile. Not sure how to do this though.

I think the difference to managed-host-environment-type, is that with
managed-host-environment-type someone already put the public key on the
server (and authorized it) and Guix is using the private key from the
SSH agent when it connects to it.

Toggle quote (10 lines)
> 2. My use-case is an on-demand wireguard VPN. In my current setup I have
> created a static ipv6 address which I attach to the VM created using
> `hcloud`. The wireguard config hardcodes the same ipv6 and is installed
> on the VM during cloud-init provision (`--user-data-from-file`
> parameter). To replicate the same in guix deploy,
> `hetzner-configuration` should be more flexible in regards to public ip
> addresses. I.e. it should allow to use either v4 or v6 and to accept
> existing one provided by the user.
>

Enabling/disabling IPv4/IPv4 should be easy to implement. The public_net
option has settings for enable_ipv4 and enable_ipv6. They both default
to #t, but it should be easy to add a configuration option for it.


The public_net also support ipv4 and ipv6 fields. The docs say:

ID of the ipv4 Primary IP to use. If omitted and enable_ipv4 is true, a
new ipv4 Primary IP will automatically be created.

And this seems to be the endpoint for creating those IPs:


We don't have code to manage primary IPs in the Hetzner modules yet, but
it shouldn't be hard to add it.

I won't have time to look into this right now, but if you plan to do it,
I can certainly answer questions you might have or support you on this.

I hope that helps, and sorry your use case isn't covered yet.

Roman
-----BEGIN PGP SIGNATURE-----

iQFLBAEBCAA1FiEE0iajOdjfRIFd3gygPdpSUn0qwZkFAmfUIP0XHHJvbWFuQGJ1
cm5pbmdzd2VsbC5jb20ACgkQPdpSUn0qwZlQ2wf/TXCvDlUjj6RwAsoLaM0ujOLS
cDIva9iA31kFn/Nx6Ia95DcH0XfF4/+IRA0GWoUGF6/q2dv6cBNdLVDAPqsa4wpX
itrapIHNeUUm9sDL2kbBoiBi6mehyxfVVzy5uCTJr01/MwdgmW6vSa3PSzZCw9u0
c+pk+cEDnxha2G9+sLQRdLQjiMFtKEn4OeZI6El32q6VX5mMdpfswXHd+SU2g9/5
V2++vd2dGy3Y1X1JJjX87NAg5hzcJtNaPg0FLFqf+7MrBoSrEJJCjwXsc2WJCwWV
iaCrRLn2gdBK5K5rb+7Ooc5ttPqEJe20QwCGX3nn54Y4SgEfIbIEqqf9UBdGlw==
=Anq+
-----END PGP SIGNATURE-----

?
Your comment

Commenting via the web interface is currently disabled.

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

To respond to this issue using the mumi CLI, first switch to it
mumi current 75144
Then, you may apply the latest patchset in this issue (with sign off)
mumi am -- -s
Or, compose a reply to this issue
mumi compose
Or, send patches to this issue
mumi send-email *.patch
You may also tag this issue. See list of standard tags. For example, to set the confirmed and easy tags
mumi command -t +confirmed -t +easy
Or, remove the moreinfo tag and set the help tag
mumi command -t -moreinfo -t +help