summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMaxim Cournoyer <maxim@guixotic.coop>2026-04-30 15:28:44 +0900
committerMaxim Cournoyer <maxim@guixotic.coop>2026-05-23 13:21:16 +0900
commit3777bf133e00d9ef685e463e51a1fb0350ffb056 (patch)
treec7e79d5b89945e5bb146abc732517c4dcdf162dc
parent6e7d100ee39bdfdb2ad350633c34bcd2dc4ca73a (diff)
machine: hetzner: Fix initial deploy when ssh-key is #f.
This fixes a regression introduced in commit a2ef2bcbfd7 ("machine: hetzner: Allow connections using ssh-agent"), where having no ssh-key would cause the early Hetzner provisioning to fail due to the lack of SSH authentication. * gnu/machine/hetzner.scm (%hetzner-ssh-key-file): New parameter. (<hetzner-configuration>) [ssh-public-key]: Compute default according to the value of the private ssh-key field. Introduce an indirection via... (hetzner-configuration-ssh-public-key): ... this new procedure, to honor %hetzner-ssh-key-file. (hetzner-configuration-ssh-key): Likewise for the private ssh-key. (hetzner-configuration-ssh-key-fingerprint): Rename to... (public-key->fingerprint): ... this, taking just the public key as argument. Update doc. (hetzner-configuration-ssh-key-public): Rename to... (public-key->string-with-type): ...this, for clarity, and accept just a public-key object. Update doc. (hetzner-machine-ssh-key): Exit early with #f when no ssh key is provided. (temporary-ssh-key-file): New procedure. (hetzner-machine-ssh-key-create): Assert a public key is defined. Adjust calls to renamed procedures, and adjust for the `hetzner-api-server-create' API change. (hetzner-machine-enable-rescue-system): Refine doc and fix code path when ssh-key is #f. (hetzner-machine-rescue-install-os): Document. (hetzner-machine-provision): Refine doc. Remove extraneous sleep and #:unwind argument. Limit ssh-session dynamic scope to where it's needed. (cleanup-temporary-ssh-key/maybe): New procedure. (deploy-hetzner): Create and use a temporary SSH key when none is defined, and clean it up when done or on errors. * gnu/machine/hetzner/http.scm (hetzner-api-server-create): Make ssh-keys a keyword argument, and fix execution when it's #f. (hetzner-api-server-enable-rescue-system): Likewise. * tests/machine/hetzner.scm ("deploy-machine-mock-with-unprovisioned-server"): Adjust test. * tests/machine/hetzner/http.scm (create-server) ("hetzner-api-server-create-unit") ("hetzner-api-server-enable-rescue-system-unit") ("hetzner-api-actions-integration") ("hetzner-api-server-enable-rescue-system-integration"): Likewise. * doc/guix.texi (Invoking guix deploy): Precise what happens when 'ssh-key' is #f in hetzner-configuration, and suggest declaratively authorizing your SSH key. Change-Id: I812b348fb553f3b5aebd0bf66850c6ecb9e06653
-rw-r--r--doc/guix.texi14
-rw-r--r--gnu/machine/hetzner.scm145
-rw-r--r--gnu/machine/hetzner/http.scm19
-rw-r--r--tests/machine/hetzner.scm4
-rw-r--r--tests/machine/hetzner/http.scm15
5 files changed, 143 insertions, 54 deletions
diff --git a/doc/guix.texi b/doc/guix.texi
index b194dab4b89..14bcc5817b0 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -50346,6 +50346,8 @@ accepts store items it receives from the coordinator:
50346# guix archive --authorize < coordinator-public-key.txt 50346# guix archive --authorize < coordinator-public-key.txt
50347@end example 50347@end example
50348 50348
50349@cindex @command{guix deploy, sudo configuration}
50350@anchor{guix-deploy-via-sudo}
50349@code{user}, in this example, specifies the name of the user account to log in 50351@code{user}, in this example, specifies the name of the user account to log in
50350as to perform the deployment. Its default value is @code{root}, but root 50352as to perform the deployment. Its default value is @code{root}, but root
50351login over SSH may be forbidden in some cases. To work around this, 50353login over SSH may be forbidden in some cases. To work around this,
@@ -50562,7 +50564,17 @@ small to bootstrap a Guix system from.
50562 50564
50563@item @code{ssh-key} (default: @code{#f}) 50565@item @code{ssh-key} (default: @code{#f})
50564If specified, the file name of the SSH private key to use to 50566If specified, the file name of the SSH private key to use to
50565authenticate with the remote host. 50567authenticate with the remote host. If left unspecified, a temporary SSH
50568key is used during the early provisioning of the machine, after which
50569the authentication with the final machine is handled by your SSH client
50570or agent. It's a good idea to have your public SSH key authorized
50571declaratively in the @code{operating-system} specification provided, via
50572the @code{authorized-keys} field of the @code{openssh-configuration}
50573record. To be able to run @samp{guix deploy} after the initial
50574provisioning is done, an SSH connection to the root account or a
50575password-less sudo configuration is needed (@pxref{guix-deploy-via-sudo,
50576Invoking guix deploy}). For the later, you must also explicitly specify
50577the @code{user} field, which otherwise defaults to @code{"root"}.
50566 50578
50567@item @code{ssh-public-key} (default: extracted from @code{ssh-key}) 50579@item @code{ssh-public-key} (default: extracted from @code{ssh-key})
50568If specified, either a public key as returned by 50580If specified, either a public key as returned by
diff --git a/gnu/machine/hetzner.scm b/gnu/machine/hetzner.scm
index b6a13ac8466..b4858fa3d26 100644
--- a/gnu/machine/hetzner.scm
+++ b/gnu/machine/hetzner.scm
@@ -2,6 +2,7 @@
2;;; Copyright © 2024 Roman Scherer <roman@burningswell.com> 2;;; Copyright © 2024 Roman Scherer <roman@burningswell.com>
3;;; Copyright © 2025 Owen T. Heisler <writer@owenh.net> 3;;; Copyright © 2025 Owen T. Heisler <writer@owenh.net>
4;;; Copyright © 2025 Remco van 't Veer <remco@remworks.net> 4;;; Copyright © 2025 Remco van 't Veer <remco@remworks.net>
5;;; Copyright © 2026 Maxim Cournoyer <maxim@guixotic.coop>
5;;; 6;;;
6;;; This file is part of GNU Guix. 7;;; This file is part of GNU Guix.
7;;; 8;;;
@@ -56,6 +57,7 @@
56 #:use-module (ice-9 string-fun) 57 #:use-module (ice-9 string-fun)
57 #:use-module (ice-9 textual-ports) 58 #:use-module (ice-9 textual-ports)
58 #:use-module (json) 59 #:use-module (json)
60 #:use-module ((rnrs base) #:select (assert))
59 #:use-module (srfi srfi-1) 61 #:use-module (srfi srfi-1)
60 #:use-module (srfi srfi-2) 62 #:use-module (srfi srfi-2)
61 #:use-module (srfi srfi-34) 63 #:use-module (srfi srfi-34)
@@ -240,6 +242,11 @@ Have you run 'guix archive --generate-key'?")
240;;; Hetzner configuration. 242;;; Hetzner configuration.
241;;; 243;;;
242 244
245;;; This parameter overrides the value returned by
246;;; `hetzner-configuration-ssh-key'.
247(define %hetzner-ssh-key-file
248 (make-parameter #f))
249
243(define-record-type* <hetzner-configuration> hetzner-configuration 250(define-record-type* <hetzner-configuration> hetzner-configuration
244 make-hetzner-configuration hetzner-configuration? this-hetzner-configuration 251 make-hetzner-configuration hetzner-configuration? this-hetzner-configuration
245 (allow-downgrades? hetzner-configuration-allow-downgrades? ; boolean 252 (allow-downgrades? hetzner-configuration-allow-downgrades? ; boolean
@@ -262,28 +269,39 @@ Have you run 'guix archive --generate-key'?")
262 (default #t)) 269 (default #t))
263 (ipv6 hetzner-configuration-ipv6 ; boolean | string 270 (ipv6 hetzner-configuration-ipv6 ; boolean | string
264 (default #t)) 271 (default #t))
265 (ssh-public-key hetzner-configuration-ssh-public-key ; public-key | string 272 (ssh-public-key hetzner-configuration-ssh-public-key* ; #f | public-key | string
266 (thunked) 273 (thunked)
267 (default (public-key-from-file (hetzner-configuration-ssh-key this-hetzner-configuration))) 274 (default (and=> (hetzner-configuration-ssh-key
268 (sanitize 275 this-hetzner-configuration)
269 (lambda (value) 276 public-key-from-file))
270 (if (string? value) (public-key-from-file value) value)))) 277 (sanitize (lambda (value)
271 (ssh-key hetzner-configuration-ssh-key 278 (if (string? value)
272 (default #f)) ; #f | string 279 (public-key-from-file value)
273 (user hetzner-configuration-user 280 value))))
274 (default "root"))) ;string 281 (ssh-key hetzner-configuration-ssh-key* ; #f | string
275 282 (default #f))
276(define (hetzner-configuration-ssh-key-fingerprint config) 283 (user hetzner-configuration-user ;string
277 "Return the SSH public key fingerprint of CONFIG as a string." 284 (default "root")))
278 (and-let* ((pubkey (hetzner-configuration-ssh-public-key config)) 285
279 (hash (get-public-key-hash pubkey 'md5))) 286(define (hetzner-configuration-ssh-key config)
287 (or (%hetzner-ssh-key-file)
288 (hetzner-configuration-ssh-key* config)))
289
290(define (hetzner-configuration-ssh-public-key config)
291 (or (and=> (%hetzner-ssh-key-file) public-key-from-file)
292 (hetzner-configuration-ssh-public-key* config)))
293
294(define (public-key->fingerprint public-key)
295 "Return the SSH public key fingerprint of PUBLIC-KEY, a Guile-SSH public key
296object, in the expected format for the Hetzner API."
297 (let ((hash (get-public-key-hash public-key 'md5)))
280 (bytevector->hex-string hash))) 298 (bytevector->hex-string hash)))
281 299
282(define (hetzner-configuration-ssh-key-public config) 300(define (public-key->string-with-type public-key)
283 "Return the SSH public key of CONFIG as a string." 301 "Return the SSH public key string of PUBLIC-KEY, a Guile-SSH public key
284 (let ((public-key (hetzner-configuration-ssh-public-key config))) 302object, in the expected format for the Hetzner API."
285 (format #f "ssh-~a ~a" (get-key-type public-key) 303 (format #f "ssh-~a ~a" (get-key-type public-key)
286 (public-key->string public-key)))) 304 (public-key->string public-key)))
287 305
288 306
289;;; 307;;;
@@ -477,8 +495,9 @@ values: list of output lines returned by CMD and its exit code."
477 495
478(define (hetzner-machine-ssh-key machine) 496(define (hetzner-machine-ssh-key machine)
479 "Find the SSH key for MACHINE on the Hetzner API." 497 "Find the SSH key for MACHINE on the Hetzner API."
480 (let* ((config (machine-configuration machine)) 498 (and-let* ((config (machine-configuration machine))
481 (expected (hetzner-configuration-ssh-key-fingerprint config))) 499 (public-key (hetzner-configuration-ssh-public-key config))
500 (expected (public-key->fingerprint public-key)))
482 (find (lambda (ssh-key) 501 (find (lambda (ssh-key)
483 (equal? expected (hetzner-ssh-key-fingerprint ssh-key))) 502 (equal? expected (hetzner-ssh-key-fingerprint ssh-key)))
484 (hetzner-api-ssh-keys 503 (hetzner-api-ssh-keys
@@ -491,10 +510,12 @@ values: list of output lines returned by CMD and its exit code."
491 (format #t "creating ssh key for '~a'...\n" name) 510 (format #t "creating ssh key for '~a'...\n" name)
492 (let* ((config (machine-configuration machine)) 511 (let* ((config (machine-configuration machine))
493 (api (hetzner-configuration-api config)) 512 (api (hetzner-configuration-api config))
513 (pubkey (hetzner-configuration-ssh-public-key config))
514 (_ (assert pubkey))
494 (ssh-key (hetzner-api-ssh-key-create 515 (ssh-key (hetzner-api-ssh-key-create
495 (hetzner-configuration-api config) 516 api
496 (hetzner-configuration-ssh-key-fingerprint config) 517 (public-key->fingerprint pubkey)
497 (hetzner-configuration-ssh-key-public config) 518 (public-key->string-with-type pubkey)
498 #:labels (hetzner-configuration-labels config)))) 519 #:labels (hetzner-configuration-labels config))))
499 (format #t "successfully created ssh key for '~a'\n" name) 520 (format #t "successfully created ssh key for '~a'\n" name)
500 ssh-key))) 521 ssh-key)))
@@ -520,6 +541,16 @@ values: list of output lines returned by CMD and its exit code."
520 (formatted-message (G_ "primary ip '~a' does not exist.") 541 (formatted-message (G_ "primary ip '~a' does not exist.")
521 name)))) 542 name))))
522 543
544(define temporary-ssh-key-file
545 (let ((ssh-key-file (port-filename (mkstemp "/tmp/hetzner-key-XXXXXX")))
546 (ssh-key #f))
547 (lambda ()
548 "Return a dynamically created, temporary private SSH key."
549 (unless ssh-key
550 (set! ssh-key (make-keypair 'ed25519))
551 (private-key-to-file ssh-key ssh-key-file))
552 ssh-key-file)))
553
523(define (hetzner-machine-create-server machine) 554(define (hetzner-machine-create-server machine)
524 "Create the Hetzner server for MACHINE." 555 "Create the Hetzner server for MACHINE."
525 (let* ((config (machine-configuration machine)) 556 (let* ((config (machine-configuration machine))
@@ -527,13 +558,14 @@ values: list of output lines returned by CMD and its exit code."
527 (server-type (hetzner-configuration-server-type config))) 558 (server-type (hetzner-configuration-server-type config)))
528 (format #t "creating '~a' server for '~a'...\n" server-type name) 559 (format #t "creating '~a' server for '~a'...\n" server-type name)
529 (let* ((ssh-key (hetzner-machine-ssh-key machine)) 560 (let* ((ssh-key (hetzner-machine-ssh-key machine))
561 (_ (assert ssh-key)) ;required during provisioning
530 (ipv4 (hetzner-configuration-ipv4 config)) 562 (ipv4 (hetzner-configuration-ipv4 config))
531 (ipv6 (hetzner-configuration-ipv6 config)) 563 (ipv6 (hetzner-configuration-ipv6 config))
532 (api (hetzner-configuration-api config)) 564 (api (hetzner-configuration-api config))
533 (server (hetzner-api-server-create 565 (server (hetzner-api-server-create
534 api 566 api
535 (machine-display-name machine) 567 (machine-display-name machine)
536 (list ssh-key) 568 #:ssh-keys (list ssh-key)
537 #:ipv4 (if (string? ipv4) 569 #:ipv4 (if (string? ipv4)
538 (hetzner-primary-ip-id (hetzner-resolve-ip api ipv4)) 570 (hetzner-primary-ip-id (hetzner-resolve-ip api ipv4))
539 ipv4) 571 ipv4)
@@ -581,13 +613,15 @@ values: list of output lines returned by CMD and its exit code."
581 (write-known-host! ssh-session))) 613 (write-known-host! ssh-session)))
582 614
583(define (hetzner-machine-enable-rescue-system machine server) 615(define (hetzner-machine-enable-rescue-system machine server)
584 "Enable the rescue system on the Hetzner SERVER for MACHINE." 616 "Enable the rescue system on the Hetzner SERVER for MACHINE, provisioning it
617with the configured SSH key, or a dynamically created temporary one."
585 (let* ((name (machine-display-name machine)) 618 (let* ((name (machine-display-name machine))
586 (config (machine-configuration machine)) 619 (config (machine-configuration machine))
587 (api (hetzner-configuration-api config)) 620 (api (hetzner-configuration-api config))
588 (ssh-keys (list (hetzner-machine-ssh-key machine)))) 621 (ssh-keys (and=> (hetzner-machine-ssh-key machine) list)))
589 (format #t "enabling rescue system on '~a'...\n" name) 622 (format #t "enabling rescue system on '~a'...\n" name)
590 (let ((action (hetzner-api-server-enable-rescue-system api server ssh-keys))) 623 (let ((action (hetzner-api-server-enable-rescue-system
624 api server #:ssh-keys ssh-keys)))
591 (format #t "successfully enabled rescue system on '~a'\n" name) 625 (format #t "successfully enabled rescue system on '~a'\n" name)
592 action))) 626 action)))
593 627
@@ -623,6 +657,7 @@ values: list of output lines returned by CMD and its exit code."
623(set! hetzner-machine-ssh-run-script hetzner-machine-ssh-run-script) 657(set! hetzner-machine-ssh-run-script hetzner-machine-ssh-run-script)
624 658
625(define (hetzner-machine-rescue-install-os machine ssh-session server) 659(define (hetzner-machine-rescue-install-os machine ssh-session server)
660 "Init a minimal Guix System to bootstrap from."
626 (let ((name (machine-display-name machine)) 661 (let ((name (machine-display-name machine))
627 (os (hetzner-machine-bootstrap-os-form machine server))) 662 (os (hetzner-machine-bootstrap-os-form machine server)))
628 (format #t "installing guix operating system on '~a'...\n" name) 663 (format #t "installing guix operating system on '~a'...\n" name)
@@ -719,7 +754,8 @@ apt-get install cloud-initramfs-growroot uidmap --assume-yes"))
719 action))) 754 action)))
720 755
721(define (hetzner-machine-provision machine) 756(define (hetzner-machine-provision machine)
722 "Provision a server for MACHINE on the Hetzner Cloud service." 757 "Provision a Guix System bootstrap server for MACHINE on the Hetzner Cloud
758service."
723 (with-exception-handler 759 (with-exception-handler
724 (lambda (exception) 760 (lambda (exception)
725 (let ((config (machine-configuration machine)) 761 (let ((config (machine-configuration machine))
@@ -734,12 +770,10 @@ apt-get install cloud-initramfs-growroot uidmap --assume-yes"))
734 (let ((ssh-session (hetzner-machine-wait-for-ssh machine server))) 770 (let ((ssh-session (hetzner-machine-wait-for-ssh machine server)))
735 (hetzner-machine-rescue-install-packages machine ssh-session) 771 (hetzner-machine-rescue-install-packages machine ssh-session)
736 (hetzner-machine-rescue-partition machine ssh-session) 772 (hetzner-machine-rescue-partition machine ssh-session)
737 (hetzner-machine-rescue-install-os machine ssh-session server) 773 (hetzner-machine-rescue-install-os machine ssh-session server))
738 (hetzner-machine-reboot machine server) 774 (hetzner-machine-reboot machine server)
739 (sleep 5) 775 (hetzner-machine-authenticate-host machine server)
740 (hetzner-machine-authenticate-host machine server) 776 server))))
741 server)))
742 #:unwind? #t))
743 777
744(define (machine-not-provisioned machine) 778(define (machine-not-provisioned machine)
745 (formatted-message 779 (formatted-message
@@ -765,16 +799,47 @@ an environment type of 'hetzner-environment-type'."
765;;; System deployment. 799;;; System deployment.
766;;; 800;;;
767 801
802(define (cleanup-temporary-ssh-key/maybe api ssh-key)
803 "Delete the temporary SSH private key file from the file system and the
804Hetzner API, if one was created used."
805 (when (%hetzner-ssh-key-file)
806 (format #t "cleaning up temporary ssh key~%")
807 (delete-file (%hetzner-ssh-key-file))
808 (%hetzner-ssh-key-file #f)
809 (hetzner-api-ssh-key-delete api ssh-key)))
810
768(define (deploy-hetzner machine) 811(define (deploy-hetzner machine)
769 "Internal implementation of 'deploy-machine' for 'machine' instances with an 812 "Internal implementation of 'deploy-machine' for 'machine' instances with an
770environment type of 'hetzner-environment-type'." 813environment type of 'hetzner-environment-type'."
771 (hetzner-machine-validate machine) 814 (hetzner-machine-validate machine)
772 (unless (hetzner-machine-ssh-key machine)
773 (hetzner-machine-ssh-key-create machine))
774 (let ((server (or (hetzner-machine-server machine)
775 (hetzner-machine-provision machine))))
776 (deploy-machine (hetzner-machine-delegate machine server))))
777 815
816 (define server (hetzner-machine-server machine))
817 (define config (machine-configuration machine))
818 (define api (hetzner-configuration-api config))
819 (define ssh-key #f) ;hetzner API key
820
821 ;; Provisioning requires uploading an SSH key via the Hetzner API to
822 ;; authenticate non-interactively with the newly created server.
823 (unless server ;provisioning?
824 (unless (hetzner-configuration-ssh-public-key config)
825 (format #t "using a temporary ssh key for provisioning~%")
826 (%hetzner-ssh-key-file (temporary-ssh-key-file)))
827 (set! ssh-key (or (hetzner-machine-ssh-key machine)
828 (hetzner-machine-ssh-key-create machine))))
829
830 ;; Cleaning-up here is a bit tricky, as `deploy-machine' returns a monadic
831 ;; value immediately, to be executed later; this means the clean-up must be
832 ;; delayed until monadic evaluation time.
833 (let* ((server (or server (hetzner-machine-provision machine)))
834 (delegate-machine (hetzner-machine-delegate machine server)))
835 ;; FIXME: The clean-up in the guard does not run even when
836 ;; `deploy-machine' fails.
837 (guard (c (#t (cleanup-temporary-ssh-key/maybe api ssh-key)
838 (raise c)))
839 (mlet %store-monad
840 ((value (deploy-machine delegate-machine)))
841 (cleanup-temporary-ssh-key/maybe api ssh-key)
842 (return value)))))
778 843
779 844
780;;; 845;;;
diff --git a/gnu/machine/hetzner/http.scm b/gnu/machine/hetzner/http.scm
index bd12e8ee5eb..4f81b27ca92 100644
--- a/gnu/machine/hetzner/http.scm
+++ b/gnu/machine/hetzner/http.scm
@@ -598,8 +598,8 @@
598 (apply hetzner-api-list api "/locations" "locations" json->hetzner-location options)) 598 (apply hetzner-api-list api "/locations" "locations" json->hetzner-location options))
599 599
600(define* (hetzner-api-server-create 600(define* (hetzner-api-server-create
601 api name ssh-keys 601 api name #:key
602 #:key 602 ssh-keys
603 (ipv4 #f) 603 (ipv4 #f)
604 (ipv6 #f) 604 (ipv6 #f)
605 (image %hetzner-default-server-image) 605 (image %hetzner-default-server-image)
@@ -620,7 +620,11 @@
620 ,@(if (integer? ipv6) `(("ipv6" . ,ipv6)) '()))) 620 ,@(if (integer? ipv6) `(("ipv6" . ,ipv6)) '())))
621 ("location" . ,location) 621 ("location" . ,location)
622 ("server_type" . ,server-type) 622 ("server_type" . ,server-type)
623 ("ssh_keys" . ,(apply vector (map hetzner-ssh-key-id ssh-keys))) 623 ,@(if ssh-keys
624 `(("ssh_keys" .
625 ,(apply vector (map hetzner-ssh-key-id
626 ssh-keys))))
627 '())
624 ("start_after_create" . ,start-after-create?))))) 628 ("start_after_create" . ,start-after-create?)))))
625 (hetzner-api-action-wait api (hetzner-api-body-action body)) 629 (hetzner-api-action-wait api (hetzner-api-body-action body))
626 (json->hetzner-server (assoc-ref body "server")))) 630 (json->hetzner-server (assoc-ref body "server"))))
@@ -631,12 +635,15 @@
631 (hetzner-api-action-wait api (hetzner-api-body-action body)))) 635 (hetzner-api-action-wait api (hetzner-api-body-action body))))
632 636
633(define* (hetzner-api-server-enable-rescue-system 637(define* (hetzner-api-server-enable-rescue-system
634 api server ssh-keys #:key (type "linux64")) 638 api server #:key ssh-keys (type "linux64"))
635 "Enable the rescue system for SERVER with the Hetzner API." 639 "Enable the rescue system for SERVER with the Hetzner API."
636 (let* ((ssh-keys (apply vector (map hetzner-ssh-key-id ssh-keys))) 640 (let* ((ssh-keys (and ssh-keys
641 (apply vector (map hetzner-ssh-key-id ssh-keys))))
637 (body (hetzner-api-post 642 (body (hetzner-api-post
638 api (hetzner-server-path server "/actions/enable_rescue") 643 api (hetzner-server-path server "/actions/enable_rescue")
639 #:body `(("ssh_keys" . ,ssh-keys) 644 #:body `(,@(if ssh-keys
645 `(("ssh_keys" . ,ssh-keys))
646 '())
640 ("type" . ,type))))) 647 ("type" . ,type)))))
641 (hetzner-api-action-wait api (hetzner-api-body-action body)))) 648 (hetzner-api-action-wait api (hetzner-api-body-action body))))
642 649
diff --git a/tests/machine/hetzner.scm b/tests/machine/hetzner.scm
index 1552bcb9a08..6f02c349f91 100644
--- a/tests/machine/hetzner.scm
+++ b/tests/machine/hetzner.scm
@@ -234,11 +234,11 @@
234 (lambda* (api . options) 234 (lambda* (api . options)
235 servers)) 235 servers))
236 ((gnu machine hetzner http) hetzner-api-server-create 236 ((gnu machine hetzner http) hetzner-api-server-create
237 (lambda* (api name ssh-keys . options) 237 (lambda* (api name #:key ssh-keys #:allow-other-keys)
238 (set! servers (list (mock-server machine))) 238 (set! servers (list (mock-server machine)))
239 (car servers))) 239 (car servers)))
240 ((gnu machine hetzner http) hetzner-api-server-enable-rescue-system 240 ((gnu machine hetzner http) hetzner-api-server-enable-rescue-system
241 (lambda (api server ssh-keys) 241 (lambda* (api server #:key ssh-keys #:allow-other-keys)
242 (mock-action "enable_rescue"))) 242 (mock-action "enable_rescue")))
243 ((gnu machine hetzner http) hetzner-api-server-power-on 243 ((gnu machine hetzner http) hetzner-api-server-power-on
244 (lambda (api server) 244 (lambda (api server)
diff --git a/tests/machine/hetzner/http.scm b/tests/machine/hetzner/http.scm
index 6c6d848a578..bd516db023b 100644
--- a/tests/machine/hetzner/http.scm
+++ b/tests/machine/hetzner/http.scm
@@ -286,7 +286,8 @@
286 #:labels labels)) 286 #:labels labels))
287 287
288(define* (create-server api ssh-key #:key (labels %labels)) 288(define* (create-server api ssh-key #:key (labels %labels))
289 (hetzner-api-server-create api %server-name (list ssh-key) 289 (hetzner-api-server-create api %server-name
290 #:ssh-keys (list ssh-key)
290 #:labels labels 291 #:labels labels
291 #:server-type "cpx31")) 292 #:server-type "cpx31"))
292 293
@@ -384,7 +385,8 @@
384 (body `(("actions" . ,(vector (cons `("status" . "success") 385 (body `(("actions" . ,(vector (cons `("status" . "success")
385 action-create-server-alist))) 386 action-create-server-alist)))
386 ("meta" . ,meta-page-alist)))))))) 387 ("meta" . ,meta-page-alist))))))))
387 (hetzner-api-server-create (hetzner-api) %server-name (list ssh-key-root)))) 388 (hetzner-api-server-create (hetzner-api) %server-name
389 #:ssh-keys (list ssh-key-root))))
388 390
389(test-equal "hetzner-api-server-delete-unit" 391(test-equal "hetzner-api-server-delete-unit"
390 (make-hetzner-action 392 (make-hetzner-action
@@ -425,7 +427,8 @@
425 (body `(("actions" . ,(vector (cons `("status" . "success") 427 (body `(("actions" . ,(vector (cons `("status" . "success")
426 action-enable-rescue-alist))) 428 action-enable-rescue-alist)))
427 ("meta" . ,meta-page-alist)))))))) 429 ("meta" . ,meta-page-alist))))))))
428 (hetzner-api-server-enable-rescue-system (hetzner-api) server-x86 (list ssh-key-root)))) 430 (hetzner-api-server-enable-rescue-system
431 (hetzner-api) server-x86 #:ssh-keys (list ssh-key-root))))
429 432
430(test-equal "hetzner-api-server-power-on-unit" 433(test-equal "hetzner-api-server-power-on-unit"
431 action-power-on 434 action-power-on
@@ -557,7 +560,8 @@
557 (with-cleanup-api (api (hetzner-api)) 560 (with-cleanup-api (api (hetzner-api))
558 (let* ((ssh-key (create-ssh-key api %ssh-key)) 561 (let* ((ssh-key (create-ssh-key api %ssh-key))
559 (server (create-server api ssh-key)) 562 (server (create-server api ssh-key))
560 (action (hetzner-api-server-enable-rescue-system api server (list ssh-key)))) 563 (action (hetzner-api-server-enable-rescue-system
564 api server #:ssh-keys (list ssh-key))))
561 (member action (hetzner-api-actions api (list (hetzner-action-id action))))))) 565 (member action (hetzner-api-actions api (list (hetzner-action-id action)))))))
562 566
563(test-skip %when-no-token) 567(test-skip %when-no-token)
@@ -595,7 +599,8 @@
595 (with-cleanup-api (api (hetzner-api)) 599 (with-cleanup-api (api (hetzner-api))
596 (let* ((ssh-key (create-ssh-key api %ssh-key)) 600 (let* ((ssh-key (create-ssh-key api %ssh-key))
597 (server (create-server api ssh-key)) 601 (server (create-server api ssh-key))
598 (action (hetzner-api-server-enable-rescue-system api server (list ssh-key)))) 602 (action (hetzner-api-server-enable-rescue-system
603 api server #:ssh-keys (list ssh-key))))
599 (and (hetzner-action? action) 604 (and (hetzner-action? action)
600 (equal? "enable_rescue" 605 (equal? "enable_rescue"
601 (hetzner-action-command action)))))) 606 (hetzner-action-command action))))))