machine: hetzner: Allow attaching existing public IPs.

* gnu/machine/hetzner.scm (hetzner-configuration): Add ipv4 and ipv6
fields. Export accessors.
* gnu/machine/hetzner/http.scm (hetnzer-api-primary-ips): New function.
(<hetzner-primary-ip>): New json mapping.
(hetzner-api-server-create): Pass IP addresses in request.
* doc/guix.texi (Invoking guix deploy): Document it.

Change-Id: I44509cc98e041762dc483e876566e79bde85b26a
Signed-off-by: Ludovic Courtès <ludo@gnu.org>
This commit is contained in:
Sergey Trofimov 2025-04-18 17:08:08 +02:00 committed by Ludovic Courtès
parent 5f1ee7ba73
commit 7a4193ec4a
No known key found for this signature in database
GPG key ID: 090B11993D9AEBB5
4 changed files with 100 additions and 6 deletions

View file

@ -46544,6 +46544,12 @@ 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 provisioning phase. If false, the server will be kept in order to debug
any issues. any issues.
@item @code{ipv4} (default: @code{#t})
@itemx @code{ipv6} (default: @code{#t})
When false, no public IP address is attached. Specify the name of an
existing primary ip to attach it to the machine. Other values would
create a new address automatically.
@item @code{labels} (default: @code{'()}) @item @code{labels} (default: @code{'()})
A user defined alist of key/value pairs attached to the SSH key and the 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, server on the Hetzner API. Keys and values must be strings,

View file

@ -73,6 +73,8 @@
hetzner-configuration-authorize? hetzner-configuration-authorize?
hetzner-configuration-build-locally? hetzner-configuration-build-locally?
hetzner-configuration-delete? hetzner-configuration-delete?
hetzner-configuration-ipv4
hetzner-configuration-ipv6
hetzner-configuration-labels hetzner-configuration-labels
hetzner-configuration-location hetzner-configuration-location
hetzner-configuration-server-type hetzner-configuration-server-type
@ -205,6 +207,10 @@ Have you run 'guix archive --generate-key'?")
(default "fsn1")) (default "fsn1"))
(server-type hetzner-configuration-server-type ; string (server-type hetzner-configuration-server-type ; string
(default "cx42")) (default "cx42"))
(ipv4 hetzner-configuration-ipv4 ; boolean | string
(default #t))
(ipv6 hetzner-configuration-ipv6 ; boolean | string
(default #t))
(ssh-public-key hetzner-configuration-ssh-public-key ; public-key | string (ssh-public-key hetzner-configuration-ssh-public-key ; public-key | string
(thunked) (thunked)
(default (public-key-from-file (hetzner-configuration-ssh-key this-hetzner-configuration))) (default (public-key-from-file (hetzner-configuration-ssh-key this-hetzner-configuration)))
@ -445,6 +451,17 @@ values: list of output lines returned by CMD and its exit code."
(hetzner-configuration-api config) (hetzner-configuration-api config)
#:params `(("name" . ,(machine-display-name machine))))))) #:params `(("name" . ,(machine-display-name machine)))))))
(define (hetzner-resolve-ip api name)
"Find the NAME IP address on the Hetzner API."
(or
(find (lambda (primary-ip)
(equal? name (hetzner-primary-ip-name primary-ip)))
(hetzner-api-primary-ips api #:params `(("name" . ,name))))
(raise-exception
(formatted-message (G_ "primary ip '~a' does not exist.")
name))))
(define (hetzner-machine-create-server machine) (define (hetzner-machine-create-server machine)
"Create the Hetzner server for MACHINE." "Create the Hetzner server for MACHINE."
(let* ((config (machine-configuration machine)) (let* ((config (machine-configuration machine))
@ -452,11 +469,19 @@ values: list of output lines returned by CMD and its exit code."
(server-type (hetzner-configuration-server-type config))) (server-type (hetzner-configuration-server-type config)))
(format #t "creating '~a' server for '~a'...\n" server-type name) (format #t "creating '~a' server for '~a'...\n" server-type name)
(let* ((ssh-key (hetzner-machine-ssh-key machine)) (let* ((ssh-key (hetzner-machine-ssh-key machine))
(ipv4 (hetzner-configuration-ipv4 config))
(ipv6 (hetzner-configuration-ipv6 config))
(api (hetzner-configuration-api config)) (api (hetzner-configuration-api config))
(server (hetzner-api-server-create (server (hetzner-api-server-create
api api
(machine-display-name machine) (machine-display-name machine)
(list ssh-key) (list ssh-key)
#:ipv4 (if (string? ipv4)
(hetzner-primary-ip-id (hetzner-resolve-ip api ipv4))
ipv4)
#:ipv6 (if (string? ipv6)
(hetzner-primary-ip-id (hetzner-resolve-ip api ipv6))
ipv6)
#:labels (hetzner-configuration-labels config) #:labels (hetzner-configuration-labels config)
#:location (hetzner-configuration-location config) #:location (hetzner-configuration-location config)
#:server-type (hetzner-configuration-server-type config))) #:server-type (hetzner-configuration-server-type config)))

View file

@ -52,6 +52,7 @@
hetzner-api-actions hetzner-api-actions
hetzner-api-create-ssh-key hetzner-api-create-ssh-key
hetzner-api-locations hetzner-api-locations
hetzner-api-primary-ips
hetzner-api-request-body hetzner-api-request-body
hetzner-api-request-headers hetzner-api-request-headers
hetzner-api-request-method hetzner-api-request-method
@ -100,6 +101,13 @@
hetzner-location-name hetzner-location-name
hetzner-location-network-zone hetzner-location-network-zone
hetzner-location? hetzner-location?
hetzner-primary-ip
hetzner-primary-ip-created
hetzner-primary-ip-id
hetzner-primary-ip-ip
hetzner-primary-ip-labels
hetzner-primary-ip-name
hetzner-primary-ip-type
hetzner-public-net hetzner-public-net
hetzner-public-net-ipv4 hetzner-public-net-ipv4
hetzner-public-net-ipv6 hetzner-public-net-ipv6
@ -144,6 +152,7 @@
make-hetzner-ipv6 make-hetzner-ipv6
make-hetzner-location make-hetzner-location
make-hetzner-public-net make-hetzner-public-net
make-hetzner-primary-ip
make-hetzner-resource make-hetzner-resource
make-hetzner-server make-hetzner-server
make-hetzner-server-type make-hetzner-server-type
@ -296,6 +305,16 @@
(name hetzner-server-type-name) ; string (name hetzner-server-type-name) ; string
(storage-type hetzner-server-type-storage-type "storage_type")) ; string (storage-type hetzner-server-type-storage-type "storage_type")) ; string
;; Reserved IP address. https://docs.hetzner.cloud/#primary-ips
(define-json-mapping <hetzner-primary-ip>
make-hetzner-primary-ip hetzner-primary-ip? json->hetzner-primary-ip
(created hetzner-primary-ip-created "created" string->time) ; time
(id hetzner-primary-ip-id) ; integer
(ip hetzner-primary-ip-ip) ; string
(labels hetzner-primary-ip-labels) ; alist of string/string
(name hetzner-primary-ip-name) ; string
(type hetzner-primary-ip-type)) ; string
(define-json-mapping <hetzner-ssh-key> (define-json-mapping <hetzner-ssh-key>
make-hetzner-ssh-key hetzner-ssh-key? json->hetzner-ssh-key make-hetzner-ssh-key hetzner-ssh-key? json->hetzner-ssh-key
(created hetzner-ssh-key-created "created" string->time) ; time (created hetzner-ssh-key-created "created" string->time) ; time
@ -581,12 +600,11 @@
(define* (hetzner-api-server-create (define* (hetzner-api-server-create
api name ssh-keys api name ssh-keys
#:key #:key
(enable-ipv4? #t) (ipv4 #f)
(enable-ipv6? #t) (ipv6 #f)
(image %hetzner-default-server-image) (image %hetzner-default-server-image)
(labels '()) (labels '())
(location %hetzner-default-server-location) (location %hetzner-default-server-location)
(public-net #f)
(server-type %hetzner-default-server-type) (server-type %hetzner-default-server-type)
(start-after-create? #f)) (start-after-create? #f))
"Create a server with the Hetzner API." "Create a server with the Hetzner API."
@ -595,9 +613,11 @@
#:body `(("image" . ,image) #:body `(("image" . ,image)
("labels" . ,labels) ("labels" . ,labels)
("name" . ,name) ("name" . ,name)
("public_net" ("public_net" .
. (("enable_ipv4" . ,enable-ipv4?) (("enable_ipv4" . ,(and ipv4 #t))
("enable_ipv6" . ,enable-ipv6?))) ("enable_ipv6" . ,(and ipv6 #t))
,@(if (integer? ipv4) `(("ipv4" . ,ipv4)) '())
,@(if (integer? ipv6) `(("ipv6" . ,ipv6)) '())))
("location" . ,location) ("location" . ,location)
("server_type" . ,server-type) ("server_type" . ,server-type)
("ssh_keys" . ,(apply vector (map hetzner-ssh-key-id ssh-keys))) ("ssh_keys" . ,(apply vector (map hetzner-ssh-key-id ssh-keys)))
@ -658,6 +678,11 @@
(apply hetzner-api-list api "/ssh_keys" "ssh_keys" (apply hetzner-api-list api "/ssh_keys" "ssh_keys"
json->hetzner-ssh-key options)) json->hetzner-ssh-key options))
(define* (hetzner-api-primary-ips api . options)
"Get Primary IPs from the Hetzner API."
(apply hetzner-api-list api "/primary_ips" "primary_ips"
json->hetzner-primary-ip options))
(define* (hetzner-api-server-types api . options) (define* (hetzner-api-server-types api . options)
"Get server types from the Hetzner API." "Get server types from the Hetzner API."
(apply hetzner-api-list api "/server_types" "server_types" (apply hetzner-api-list api "/server_types" "server_types"

View file

@ -239,6 +239,30 @@
("status" . "running") ("status" . "running")
("volumes" . #()))) ("volumes" . #())))
(define primary-ip
(make-hetzner-primary-ip
#(55 2 19 28 9 123 6 300 -1 0 #f)
42
"131.232.99.1"
'()
"static-ip"
"ipv4"))
(define primary-ip-alist
`(("created" . "2023-10-28T19:02:55+00:00")
("id" . 42)
("labels")
("name" . "static-ip")
("blocked" . #f)
("ip" . "131.232.99.1")
("datacenter")
("dns_ptr")
("protection" . (("delete" . #f)))
("type" . "ipv4")
("auto_delete" . #t)
("assignee_type" . "server")
("assignee_id" . 17)))
(define ssh-key-root (define ssh-key-root
(make-hetzner-ssh-key (make-hetzner-ssh-key
#(55 2 19 28 9 123 6 300 -1 0 #f) #(55 2 19 28 9 123 6 300 -1 0 #f)
@ -512,6 +536,20 @@
("ssh_keys" . #(,ssh-key-root-alist))))))) ("ssh_keys" . #(,ssh-key-root-alist)))))))
(hetzner-api-ssh-keys (hetzner-api)))) (hetzner-api-ssh-keys (hetzner-api))))
(test-equal "hetzner-api-primary-ips-unit"
(list primary-ip)
(mock ((gnu machine hetzner http) hetzner-api-request-send
(lambda* (request #:key expected)
(assert (equal? 'GET (hetzner-api-request-method request)))
(assert (equal? "https://api.hetzner.cloud/v1/primary_ips"
(hetzner-api-request-url request)))
(assert (unspecified? (hetzner-api-request-body request)))
(assert (equal? '(("page" . 1)) (hetzner-api-request-params request)))
(hetzner-api-response
(body `(("meta" . ,meta-page-alist)
("primary_ips" . #(,primary-ip-alist)))))))
(hetzner-api-primary-ips (hetzner-api))))
;; Integration tests ;; Integration tests
(test-skip %when-no-token) (test-skip %when-no-token)