summaryrefslogtreecommitdiff
path: root/gnu/machine
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 /gnu/machine
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
Diffstat (limited to 'gnu/machine')
-rw-r--r--gnu/machine/hetzner.scm145
-rw-r--r--gnu/machine/hetzner/http.scm19
2 files changed, 118 insertions, 46 deletions
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