summaryrefslogtreecommitdiff
path: root/gnu/machine/hetzner
diff options
context:
space:
mode:
authorRoman Scherer <roman@burningswell.com>2025-02-04 20:01:14 +0100
committerLudovic Courtès <ludo@gnu.org>2025-02-09 18:20:42 +0100
commit0753a17ddf6f4fab98b93c25f1a93b97ff9e46bb (patch)
treee56f2bcb4c52186364ee63a065bc6a20a2e252be /gnu/machine/hetzner
parent96f05f003a862c198e803901abf6f50b23969697 (diff)
machine: Implement 'hetzner-environment-type'.
* 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 Signed-off-by: Ludovic Courtès <ludo@gnu.org>
Diffstat (limited to 'gnu/machine/hetzner')
-rw-r--r--gnu/machine/hetzner/http.scm664
1 files changed, 664 insertions, 0 deletions
diff --git a/gnu/machine/hetzner/http.scm b/gnu/machine/hetzner/http.scm
new file mode 100644
index 00000000000..bfd65554721
--- /dev/null
+++ b/gnu/machine/hetzner/http.scm
@@ -0,0 +1,664 @@
1;;; GNU Guix --- Functional package management for GNU
2;;; Copyright © 2024 Roman Scherer <roman@burningswell.com>
3;;;
4;;; This file is part of GNU Guix.
5;;;
6;;; GNU Guix is free software; you can redistribute it and/or modify it
7;;; under the terms of the GNU General Public License as published by
8;;; the Free Software Foundation; either version 3 of the License, or (at
9;;; your option) any later version.
10;;;
11;;; GNU Guix is distributed in the hope that it will be useful, but
12;;; WITHOUT ANY WARRANTY; without even the implied warranty of
13;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14;;; GNU General Public License for more details.
15;;;
16;;; You should have received a copy of the GNU General Public License
17;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
18
19(define-module (gnu machine hetzner http)
20 #:use-module (guix diagnostics)
21 #:use-module (guix i18n)
22 #:use-module (guix records)
23 #:use-module (ice-9 iconv)
24 #:use-module (ice-9 match)
25 #:use-module (ice-9 pretty-print)
26 #:use-module (ice-9 textual-ports)
27 #:use-module (json)
28 #:use-module (rnrs bytevectors)
29 #:use-module (srfi srfi-1)
30 #:use-module (srfi srfi-2)
31 #:use-module (ssh key)
32 #:use-module (web client)
33 #:use-module (web request)
34 #:use-module (web response)
35 #:use-module (web uri)
36 #:export (%hetzner-default-api-token
37 %hetzner-default-server-image
38 %hetzner-default-server-location
39 %hetzner-default-server-type
40 hetzner-action
41 hetzner-action-command
42 hetzner-action-error
43 hetzner-action-finished
44 hetzner-action-id
45 hetzner-action-progress
46 hetzner-action-resources
47 hetzner-action-started
48 hetzner-action-status
49 hetzner-action?
50 hetzner-api
51 hetzner-api-action-wait
52 hetzner-api-actions
53 hetzner-api-create-ssh-key
54 hetzner-api-locations
55 hetzner-api-request-body
56 hetzner-api-request-headers
57 hetzner-api-request-method
58 hetzner-api-request-params
59 hetzner-api-request-send
60 hetzner-api-request-url
61 hetzner-api-request?
62 hetzner-api-response
63 hetzner-api-response-body
64 hetzner-api-response-headers
65 hetzner-api-response-status
66 hetzner-api-response?
67 hetzner-api-server-create
68 hetzner-api-server-delete
69 hetzner-api-server-enable-rescue-system
70 hetzner-api-server-power-off
71 hetzner-api-server-power-on
72 hetzner-api-server-reboot
73 hetzner-api-server-types
74 hetzner-api-servers
75 hetzner-api-ssh-key-create
76 hetzner-api-ssh-key-delete
77 hetzner-api-ssh-keys
78 hetzner-api-token
79 hetzner-api?
80 hetzner-error-code
81 hetzner-error-message
82 hetzner-error?
83 hetzner-ipv4-blocked?
84 hetzner-ipv4-dns-ptr
85 hetzner-ipv4-id
86 hetzner-ipv4-ip
87 hetzner-ipv4?
88 hetzner-ipv6-blocked?
89 hetzner-ipv6-dns-ptr
90 hetzner-ipv6-id
91 hetzner-ipv6-ip
92 hetzner-ipv6?
93 hetzner-location
94 hetzner-location-city
95 hetzner-location-country
96 hetzner-location-description
97 hetzner-location-id
98 hetzner-location-latitude
99 hetzner-location-longitude
100 hetzner-location-name
101 hetzner-location-network-zone
102 hetzner-location?
103 hetzner-public-net
104 hetzner-public-net-ipv4
105 hetzner-public-net-ipv6
106 hetzner-resource
107 hetzner-resource-id
108 hetzner-resource-type
109 hetzner-resource?
110 hetzner-server-architecture
111 hetzner-server-created
112 hetzner-server-id
113 hetzner-server-labels
114 hetzner-server-name
115 hetzner-server-public-ipv4
116 hetzner-server-public-net
117 hetzner-server-rescue-enabled?
118 hetzner-server-system
119 hetzner-server-type
120 hetzner-server-type-architecture
121 hetzner-server-type-cores
122 hetzner-server-type-cpu-type
123 hetzner-server-type-deprecated
124 hetzner-server-type-deprecation
125 hetzner-server-type-description
126 hetzner-server-type-disk
127 hetzner-server-type-id
128 hetzner-server-type-memory
129 hetzner-server-type-name
130 hetzner-server-type-storage-type
131 hetzner-server-type?
132 hetzner-server?
133 hetzner-ssh-key-created
134 hetzner-ssh-key-fingerprint
135 hetzner-ssh-key-id
136 hetzner-ssh-key-labels
137 hetzner-ssh-key-name
138 hetzner-ssh-key-public-key
139 hetzner-ssh-key-read-file
140 hetzner-ssh-key?
141 make-hetzner-action
142 make-hetzner-error
143 make-hetzner-ipv4
144 make-hetzner-ipv6
145 make-hetzner-location
146 make-hetzner-public-net
147 make-hetzner-resource
148 make-hetzner-server
149 make-hetzner-server-type
150 make-hetzner-ssh-key))
151
152;;; Commentary:
153;;;
154;;; This module implements a lower-level interface for interacting with the
155;;; Hetzner Cloud API https://docs.hetzner.cloud.
156;;;
157
158(define %hetzner-default-api-token
159 (make-parameter (getenv "GUIX_HETZNER_API_TOKEN")))
160
161;; Ideally this would be a Guix image. Maybe one day.
162(define %hetzner-default-server-image "debian-11")
163
164;; Falkenstein, Germany
165(define %hetzner-default-server-location "fsn1")
166
167;; x86, 8 VCPUs, 16 GB mem, 160 GB disk
168(define %hetzner-default-server-type "cx42")
169
170
171;;;
172;;; Helper functions.
173;;;
174
175(define (format-query-param param)
176 "Format the query PARAM as a string."
177 (string-append (uri-encode (format #f "~a" (car param))) "="
178 (uri-encode (format #f "~a" (cdr param)))))
179
180(define (format-query-params params)
181 "Format the query PARAMS as a string."
182 (if (> (length params) 0)
183 (string-append
184 "?"
185 (string-join
186 (map format-query-param params)
187 "&"))
188 ""))
189
190(define (json->maybe-hetzner-error json)
191 (and (list? json) (json->hetzner-error json)))
192
193(define (string->time s)
194 (when (string? s) (car (strptime "%FT%T%z" s))))
195
196(define (json->hetzner-dnses vector)
197 (map json->hetzner-dns (vector->list vector)))
198
199(define (json->hetzner-resources vector)
200 (map json->hetzner-resource (vector->list vector)))
201
202
203;;;
204;;; Domain models.
205;;;
206
207(define-json-mapping <hetzner-action>
208 make-hetzner-action hetzner-action? json->hetzner-action
209 (command hetzner-action-command) ; string
210 (error hetzner-action-error "error"
211 json->maybe-hetzner-error) ; <hetzner-error> | #f
212 (finished hetzner-action-finished "finished" string->time) ; time
213 (id hetzner-action-id) ; integer
214 (progress hetzner-action-progress) ; integer
215 (resources hetzner-action-resources "resources"
216 json->hetzner-resources) ; list of <hetzner-resource>
217 (started hetzner-action-started "started" string->time) ; time
218 (status hetzner-action-status))
219
220(define-json-mapping <hetzner-deprecation>
221 make-hetzner-deprecation hetzner-deprecation? json->hetzner-deprecation
222 (announced hetzner-deprecation-announced) ; string
223 (unavailable-after hetzner-deprecation-unavailable-after
224 "unavailable_after")) ; string
225
226(define-json-mapping <hetzner-dns>
227 make-hetzner-dns hetzner-dns? json->hetzner-dns
228 (ip hetzner-dns-ip) ; string
229 (ptr hetzner-dns-ptr "dns_ptr")) ; string
230
231(define-json-mapping <hetzner-error>
232 make-hetzner-error hetzner-error? json->hetzner-error
233 (code hetzner-error-code) ; string
234 (message hetzner-error-message)) ; <string>
235
236(define-json-mapping <hetzner-ipv4>
237 make-hetzner-ipv4 hetzner-ipv4? json->hetzner-ipv4
238 (blocked? hetzner-ipv4-blocked? "blocked") ; boolean
239 (dns-ptr hetzner-ipv4-dns-ptr "dns_ptr") ; string
240 (id hetzner-ipv4-id) ; integer
241 (ip hetzner-ipv4-ip)) ; string
242
243(define-json-mapping <hetzner-ipv6>
244 make-hetzner-ipv6 hetzner-ipv6? json->hetzner-ipv6
245 (blocked? hetzner-ipv6-blocked? "blocked") ; boolean
246 (dns-ptr hetzner-ipv6-dns-ptr "dns_ptr"
247 json->hetzner-dnses) ; list of <hetzner-dns>
248 (id hetzner-ipv6-id) ; integer
249 (ip hetzner-ipv6-ip)) ; string
250
251(define-json-mapping <hetzner-location>
252 make-hetzner-location hetzner-location? json->hetzner-location
253 (city hetzner-location-city) ; string
254 (country hetzner-location-country) ; string
255 (description hetzner-location-description) ; string
256 (id hetzner-location-id) ; integer
257 (latitude hetzner-location-latitude) ; decimal
258 (longitude hetzner-location-longitude) ; decimal
259 (name hetzner-location-name) ; string
260 (network-zone hetzner-location-network-zone "network_zone"))
261
262(define-json-mapping <hetzner-public-net>
263 make-hetzner-public-net hetzner-public-net? json->hetzner-public-net
264 (ipv4 hetzner-public-net-ipv4 "ipv4" json->hetzner-ipv4) ; <hetzner-ipv4>
265 (ipv6 hetzner-public-net-ipv6 "ipv6" json->hetzner-ipv6)) ; <hetzner-ipv6>
266
267(define-json-mapping <hetzner-resource>
268 make-hetzner-resource hetzner-resource? json->hetzner-resource
269 (id hetzner-resource-id) ; integer
270 (type hetzner-resource-type)) ; string
271
272(define-json-mapping <hetzner-server>
273 make-hetzner-server hetzner-server? json->hetzner-server
274 (created hetzner-server-created) ; time
275 (id hetzner-server-id) ; integer
276 (labels hetzner-server-labels) ; alist of string/string
277 (name hetzner-server-name) ; string
278 (public-net hetzner-server-public-net "public_net"
279 json->hetzner-public-net) ; <hetzner-public-net>
280 (rescue-enabled? hetzner-server-rescue-enabled? "rescue_enabled") ; boolean
281 (server-type hetzner-server-type "server_type"
282 json->hetzner-server-type)) ; <hetzner-server-type>
283
284(define-json-mapping <hetzner-server-type>
285 make-hetzner-server-type hetzner-server-type? json->hetzner-server-type
286 (architecture hetzner-server-type-architecture) ; string
287 (cores hetzner-server-type-cores) ; integer
288 (cpu-type hetzner-server-type-cpu-type "cpu_type") ; string
289 (deprecated hetzner-server-type-deprecated) ; boolean
290 (deprecation hetzner-server-type-deprecation
291 json->hetzner-deprecation) ; <hetzner-deprecation>
292 (description hetzner-server-type-description) ; string
293 (disk hetzner-server-type-disk) ; integer
294 (id hetzner-server-type-id) ; integer
295 (memory hetzner-server-type-memory) ; integer
296 (name hetzner-server-type-name) ; string
297 (storage-type hetzner-server-type-storage-type "storage_type")) ; string
298
299(define-json-mapping <hetzner-ssh-key>
300 make-hetzner-ssh-key hetzner-ssh-key? json->hetzner-ssh-key
301 (created hetzner-ssh-key-created "created" string->time) ; time
302 (fingerprint hetzner-ssh-key-fingerprint) ; string
303 (id hetzner-ssh-key-id) ; integer
304 (labels hetzner-ssh-key-labels) ; alist of string/string
305 (name hetzner-ssh-key-name) ; string
306 (public_key hetzner-ssh-key-public-key "public_key")) ; string
307
308(define (hetzner-server-architecture server)
309 "Return the architecture of the Hetzner SERVER."
310 (hetzner-server-type-architecture (hetzner-server-type server)))
311
312(define* (hetzner-server-path server #:optional (path ""))
313 "Return the PATH of the Hetzner SERVER."
314 (format #f "/servers/~a~a" (hetzner-server-id server) path))
315
316(define (hetzner-server-public-ipv4 server)
317 "Return the public IPv4 address of the SERVER."
318 (and-let* ((public-net (hetzner-server-public-net server))
319 (ipv4 (hetzner-public-net-ipv4 public-net)))
320 (hetzner-ipv4-ip ipv4)))
321
322(define (hetzner-server-system server)
323 "Return the Guix system architecture of the Hetzner SERVER."
324 (match (hetzner-server-architecture server)
325 ("arm" "aarch64-linux")
326 ("x86" "x86_64-linux")))
327
328(define* (hetzner-ssh-key-path ssh-key #:optional (path ""))
329 "Return the PATH of the Hetzner SSH-KEY."
330 (format #f "/ssh_keys/~a~a" (hetzner-ssh-key-id ssh-key) path))
331
332(define (hetzner-ssh-key-read-file file)
333 "Read the SSH private key from FILE and return a Hetzner SSH key."
334 (let* ((privkey (private-key-from-file file))
335 (pubkey (private-key->public-key privkey))
336 (hash (get-public-key-hash pubkey 'md5))
337 (fingerprint (bytevector->hex-string hash))
338 (public-key (format #f "ssh-~a ~a" (get-key-type pubkey)
339 (public-key->string pubkey))))
340 (make-hetzner-ssh-key #f fingerprint #f '() (basename file) public-key)))
341
342
343;;;
344;;; Hetzner API response.
345;;;
346
347(define-record-type* <hetzner-api-response>
348 hetzner-api-response make-hetzner-api-response hetzner-api-response?
349 (body hetzner-api-response-body (default *unspecified*))
350 (headers hetzner-api-response-headers (default '()))
351 (status hetzner-api-response-status (default 200)))
352
353(define (hetzner-api-response-meta response)
354 "Return the meta information of the Hetzner API response."
355 (assoc-ref (hetzner-api-response-body response) "meta"))
356
357(define (hetzner-api-response-pagination response)
358 "Return the meta information of the Hetzner API response."
359 (assoc-ref (hetzner-api-response-meta response) "pagination"))
360
361(define (hetzner-api-response-pagination-combine resource responses)
362 "Combine multiple Hetzner API pagination responses into a single response."
363 (if (positive? (length responses))
364 (let* ((response (car responses))
365 (pagination (hetzner-api-response-pagination response))
366 (total-entries (assoc-ref pagination "total_entries")))
367 (hetzner-api-response
368 (inherit response)
369 (body `(("meta"
370 ("pagination"
371 ("last_page" . 1)
372 ("next_page" . null)
373 ("page" . 1)
374 ("per_page" . ,total-entries)
375 ("previous_page" . null)
376 ("total_entries" . ,total-entries)))
377 (,resource . ,(append-map
378 (lambda (body)
379 (vector->list (assoc-ref body resource)))
380 (map hetzner-api-response-body responses)))))))
381 (raise-exception
382 (formatted-message
383 (G_ "expected a list of Hetzner API responses")))))
384
385(define (hetzner-api-body-action body)
386 "Return the Hetzner API action from BODY."
387 (let ((json (assoc-ref body "action")))
388 (and json (json->hetzner-action json))))
389
390(define (hetzner-api-response-read port)
391 "Read the Hetzner API response from PORT."
392 (let* ((response (read-response port))
393 (body (read-response-body response)))
394 (hetzner-api-response
395 (body (and body (json-string->scm (utf8->string body))))
396 (headers (response-headers response))
397 (status (response-code response)))))
398
399(define (hetzner-api-response-validate-status response expected)
400 "Raise an error if the HTTP status code of RESPONSE is not in EXPECTED."
401 (when (not (member (hetzner-api-response-status response) expected))
402 (raise-exception
403 (formatted-message
404 (G_ "unexpected HTTP status code: ~a, expected: ~a~%~a")
405 (hetzner-api-response-status response)
406 expected
407 (with-output-to-string
408 (lambda ()
409 (pretty-print (hetzner-api-response-body response))))))))
410
411
412;;;
413;;; Hetzner API request.
414;;;
415
416(define-record-type* <hetzner-api-request>
417 hetzner-api-request make-hetzner-api-request hetzner-api-request?
418 (body hetzner-api-request-body (default *unspecified*))
419 (headers hetzner-api-request-headers (default '()))
420 (method hetzner-api-request-method (default 'GET))
421 (params hetzner-api-request-params (default '()))
422 (url hetzner-api-request-url))
423
424(define (hetzner-api-request-uri request)
425 "Return the URI object of the Hetzner API request."
426 (let ((params (hetzner-api-request-params request)))
427 (string->uri (string-append (hetzner-api-request-url request)
428 (format-query-params params)))))
429
430(define (hetzner-api-request-body-bytevector request)
431 "Return the body of the Hetzner API REQUEST as a bytevector."
432 (let ((body (hetzner-api-request-body request)))
433 (string->utf8 (if (unspecified? body) "" (scm->json-string body)))))
434
435(define (hetzner-api-request-write port request)
436 "Write the Hetzner API REQUEST to PORT."
437 (let* ((body (hetzner-api-request-body-bytevector request))
438 (request (build-request
439 (hetzner-api-request-uri request)
440 #:method (hetzner-api-request-method request)
441 #:version '(1 . 1)
442 #:headers (cons* `(Content-Length
443 . ,(number->string
444 (if (unspecified? body)
445 0 (bytevector-length body))))
446 (hetzner-api-request-headers request))
447 #:port port))
448 (request (write-request request port)))
449 (unless (unspecified? body)
450 (write-request-body request body))
451 (force-output (request-port request))))
452
453(define* (hetzner-api-request-send request #:key (expected (list 200 201 204)))
454 "Send the Hetzner API REQUEST via HTTP."
455 (let ((port (open-socket-for-uri (hetzner-api-request-uri request))))
456 (hetzner-api-request-write port request)
457 (let ((response (hetzner-api-response-read port)))
458 (close-port port)
459 (hetzner-api-response-validate-status response expected)
460 response)))
461
462;; Prevent compiler from inlining this function, so we can mock it in tests.
463(set! hetzner-api-request-send hetzner-api-request-send)
464
465(define (hetzner-api-request-next-params request)
466 "Return the pagination params for the next page of the REQUEST."
467 (let* ((params (hetzner-api-request-params request))
468 (page (or (assoc-ref params "page") 1)))
469 (map (lambda (param)
470 (if (equal? "page" (car param))
471 (cons (car param) (+ page 1))
472 param))
473 params)))
474
475(define (hetzner-api-request-paginate request)
476 "Fetch all pages of the REQUEST via pagination and return all responses."
477 (let* ((response (hetzner-api-request-send request))
478 (pagination (hetzner-api-response-pagination response))
479 (next-page (assoc-ref pagination "next_page")))
480 (if (number? next-page)
481 (cons response
482 (hetzner-api-request-paginate
483 (hetzner-api-request
484 (inherit request)
485 (params (hetzner-api-request-next-params request)))))
486 (list response))))
487
488
489
490;;;
491;;; Hetzner API.
492;;;
493
494(define-record-type* <hetzner-api>
495 hetzner-api make-hetzner-api hetzner-api?
496 (base-url hetzner-api-base-url ; string
497 (default "https://api.hetzner.cloud/v1"))
498 (token hetzner-api-token ; string
499 (default (%hetzner-default-api-token))))
500
501(define (hetzner-api-authorization-header api)
502 "Return the authorization header for the Hetzner API."
503 (format #f "Bearer ~a" (hetzner-api-token api)))
504
505(define (hetzner-api-default-headers api)
506 "Returns the default headers of the Hetzner API."
507 `((user-agent . "Guix Deploy")
508 (Accept . "application/json")
509 (Authorization . ,(hetzner-api-authorization-header api))
510 (Content-Type . "application/json")))
511
512(define (hetzner-api-url api path)
513 "Append PATH to the base url of the Hetzner API."
514 (string-append (hetzner-api-base-url api) path))
515
516(define (hetzner-api-delete api path)
517 "Delelte the resource at PATH with the Hetzner API."
518 (hetzner-api-response-body
519 (hetzner-api-request-send
520 (hetzner-api-request
521 (headers (hetzner-api-default-headers api))
522 (method 'DELETE)
523 (url (hetzner-api-url api path))))))
524
525(define* (hetzner-api-list api path resources json->object #:key (params '()))
526 "Fetch all objects of RESOURCE from the Hetzner API."
527 (let ((body (hetzner-api-response-body
528 (hetzner-api-response-pagination-combine
529 resources (hetzner-api-request-paginate
530 (hetzner-api-request
531 (url (hetzner-api-url api path))
532 (headers (hetzner-api-default-headers api))
533 (params (cons '("page" . 1) params))))))))
534 (map json->object (assoc-ref body resources))))
535
536(define* (hetzner-api-post api path #:key (body *unspecified*))
537 "Send a POST request to the Hetzner API at PATH using BODY."
538 (hetzner-api-response-body
539 (hetzner-api-request-send
540 (hetzner-api-request
541 (body body)
542 (method 'POST)
543 (url (hetzner-api-url api path))
544 (headers (hetzner-api-default-headers api))))))
545
546(define (hetzner-api-actions api ids)
547 "Get actions from the Hetzner API."
548 (if (zero? (length ids))
549 (raise-exception
550 (formatted-message
551 (G_ "expected at least one action id, but got '~a'")
552 (length ids)))
553 (hetzner-api-list
554 api "/actions" "actions" json->hetzner-action
555 #:params `(("id" . ,(string-join (map number->string ids) ","))))))
556
557(define* (hetzner-api-action-wait api action #:optional (status "success"))
558 "Wait until the ACTION has reached STATUS on the Hetzner API."
559 (let ((id (hetzner-action-id action)))
560 (let loop ()
561 (let ((actions (hetzner-api-actions api (list id))))
562 (cond
563 ((zero? (length actions))
564 (raise-exception
565 (formatted-message (G_ "server action '~a' not found") id)))
566 ((not (= 1 (length actions)))
567 (raise-exception
568 (formatted-message
569 (G_ "expected one server action, but got '~a'")
570 (length actions))))
571 ((string= status (hetzner-action-status (car actions)))
572 (car actions))
573 (else
574 (sleep 5)
575 (loop)))))))
576
577(define* (hetzner-api-locations api . options)
578 "Get deployment locations from the Hetzner API."
579 (apply hetzner-api-list api "/locations" "locations" json->hetzner-location options))
580
581(define* (hetzner-api-server-create
582 api name ssh-keys
583 #:key
584 (enable-ipv4? #t)
585 (enable-ipv6? #t)
586 (image %hetzner-default-server-image)
587 (labels '())
588 (location %hetzner-default-server-location)
589 (public-net #f)
590 (server-type %hetzner-default-server-type)
591 (start-after-create? #f))
592 "Create a server with the Hetzner API."
593 (let ((body (hetzner-api-post
594 api "/servers"
595 #:body `(("image" . ,image)
596 ("labels" . ,labels)
597 ("name" . ,name)
598 ("public_net"
599 . (("enable_ipv4" . ,enable-ipv4?)
600 ("enable_ipv6" . ,enable-ipv6?)))
601 ("location" . ,location)
602 ("server_type" . ,server-type)
603 ("ssh_keys" . ,(apply vector (map hetzner-ssh-key-id ssh-keys)))
604 ("start_after_create" . ,start-after-create?)))))
605 (hetzner-api-action-wait api (hetzner-api-body-action body))
606 (json->hetzner-server (assoc-ref body "server"))))
607
608(define (hetzner-api-server-delete api server)
609 "Delete the SERVER with the Hetzner API."
610 (let ((body (hetzner-api-delete api (hetzner-server-path server))))
611 (hetzner-api-action-wait api (hetzner-api-body-action body))))
612
613(define* (hetzner-api-server-enable-rescue-system
614 api server ssh-keys #:key (type "linux64"))
615 "Enable the rescue system for SERVER with the Hetzner API."
616 (let* ((ssh-keys (apply vector (map hetzner-ssh-key-id ssh-keys)))
617 (body (hetzner-api-post
618 api (hetzner-server-path server "/actions/enable_rescue")
619 #:body `(("ssh_keys" . ,ssh-keys)
620 ("type" . ,type)))))
621 (hetzner-api-action-wait api (hetzner-api-body-action body))))
622
623(define* (hetzner-api-servers api . options)
624 "Get servers from the Hetzner API."
625 (apply hetzner-api-list api "/servers" "servers" json->hetzner-server options))
626
627(define (hetzner-api-server-power-on api server)
628 "Send a power on request for SERVER to the Hetzner API."
629 (let ((body (hetzner-api-post api (hetzner-server-path server "/actions/poweron"))))
630 (hetzner-api-action-wait api (hetzner-api-body-action body))))
631
632(define (hetzner-api-server-power-off api server)
633 "Send a power off request for SERVER to the Hetzner API."
634 (let ((body (hetzner-api-post api (hetzner-server-path server "/actions/poweroff"))))
635 (hetzner-api-action-wait api (hetzner-api-body-action body))))
636
637(define (hetzner-api-server-reboot api server)
638 "Send a reboot request for SERVER to the Hetzner API."
639 (let ((body (hetzner-api-post api (hetzner-server-path server "/actions/reboot"))))
640 (hetzner-api-action-wait api (hetzner-api-body-action body))))
641
642(define* (hetzner-api-ssh-key-create api name public-key #:key (labels '()))
643 "Create a SSH key with the Hetzner API."
644 (let ((body (hetzner-api-post
645 api "/ssh_keys"
646 #:body `(("name" . ,name)
647 ("public_key" . ,public-key)
648 ("labels" . ,labels)))))
649 (json->hetzner-ssh-key (assoc-ref body "ssh_key"))))
650
651(define (hetzner-api-ssh-key-delete api ssh-key)
652 "Delete the SSH key on the Hetzner API."
653 (hetzner-api-delete api (hetzner-ssh-key-path ssh-key))
654 #t)
655
656(define* (hetzner-api-ssh-keys api . options)
657 "Get SSH keys from the Hetzner API."
658 (apply hetzner-api-list api "/ssh_keys" "ssh_keys"
659 json->hetzner-ssh-key options))
660
661(define* (hetzner-api-server-types api . options)
662 "Get server types from the Hetzner API."
663 (apply hetzner-api-list api "/server_types" "server_types"
664 json->hetzner-server-type options))