scripts: Typo in args-fold*.
[jackhill/guix/guix.git] / guix / ssh.scm
CommitLineData
987a29ba 1;;; GNU Guix --- Functional package management for GNU
8f53d734 2;;; Copyright © 2016, 2017, 2018, 2019, 2020 Ludovic Courtès <ludo@gnu.org>
987a29ba
LC
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 (guix ssh)
20 #:use-module (guix store)
af15fe13 21 #:use-module (guix inferior)
52d174d6 22 #:use-module (guix i18n)
896fec47 23 #:use-module ((guix utils) #:select (&fix-hint))
3033d59a 24 #:use-module (gcrypt pk-crypto)
615c5298
LC
25 #:use-module (ssh session)
26 #:use-module (ssh auth)
27 #:use-module (ssh key)
987a29ba
LC
28 #:use-module (ssh channel)
29 #:use-module (ssh popen)
30 #:use-module (ssh session)
0e3c8528 31 #:use-module (srfi srfi-1)
987a29ba 32 #:use-module (srfi srfi-11)
d06d54e3 33 #:use-module (srfi srfi-26)
13164a21
LC
34 #:use-module (srfi srfi-34)
35 #:use-module (srfi srfi-35)
987a29ba 36 #:use-module (ice-9 match)
416a7c69 37 #:use-module (ice-9 format)
13164a21 38 #:use-module (ice-9 binary-ports)
615c5298 39 #:export (open-ssh-session
114dcb42
LC
40 authenticate-server*
41
af15fe13 42 remote-inferior
e5378337 43 remote-daemon-channel
615c5298 44 connect-to-remote-daemon
2c8e04f1 45 remote-system
3033d59a 46 remote-authorize-signing-key
987a29ba
LC
47 send-files
48 retrieve-files
d06d54e3 49 retrieve-files*
4eb0f9ae
LC
50 remote-store-host
51
52 report-guile-error
53 report-module-error))
987a29ba
LC
54
55;;; Commentary:
56;;;
57;;; This module provides tools to support communication with remote stores
58;;; over SSH, using Guile-SSH.
59;;;
60;;; Code:
61
615c5298
LC
62(define %compression
63 "zlib@openssh.com,zlib")
64
114dcb42
LC
65(define (host-key->type+key host-key)
66 "Destructure HOST-KEY, an OpenSSH host key string, and return two values:
67its key type as a symbol, and the actual base64-encoded string."
68 (define (type->symbol type)
69 (and (string-prefix? "ssh-" type)
70 (string->symbol (string-drop type 4))))
71
72 (match (string-tokenize host-key)
73 ((type key x)
74 (values (type->symbol type) key))
75 ((type key)
76 (values (type->symbol type) key))))
77
78(define (authenticate-server* session key)
79 "Make sure the server for SESSION has the given KEY, where KEY is a string
80such as \"ssh-ed25519 AAAAC3Nz… root@example.org\". Raise an exception if the
81actual key does not match."
82 (let-values (((server) (get-server-public-key session))
83 ((type key) (host-key->type+key key)))
84 (unless (and (or (not (get-key-type server))
85 (eq? (get-key-type server) type))
86 (string=? (public-key->string server) key))
87 ;; Key mismatch: something's wrong. XXX: It could be that the server
88 ;; provided its Ed25519 key when we where expecting its RSA key. XXX:
89 ;; Guile-SSH 0.10.1 doesn't know about ed25519 keys and 'get-key-type'
90 ;; returns #f in that case.
91 (raise (condition
92 (&message
93 (message (format #f (G_ "server at '~a' returned host key \
94'~a' of type '~a' instead of '~a' of type '~a'~%")
95 (session-get session 'host)
96 (public-key->string server)
97 (get-key-type server)
98 key type))))))))
99
a9b09ed7 100(define* (open-ssh-session host #:key user port identity
2b868284 101 host-key
81c5873a
LC
102 (compression %compression)
103 (timeout 3600))
a9b09ed7
JK
104 "Open an SSH session for HOST and return it. IDENTITY specifies the file
105name of a private key to use for authenticating with the host. When USER,
106PORT, or IDENTITY are #f, use default values or whatever '~/.ssh/config'
2b868284
LC
107specifies; otherwise use them.
108
109When HOST-KEY is true, it must be a string like \"ssh-ed25519 AAAAC3Nz…
110root@example.org\"; the server is authenticated and an error is raised if its
111host key is different from HOST-KEY.
112
113Install TIMEOUT as the maximum time in seconds after which a read or write
114operation on a channel of the returned session is considered as failing.
81c5873a
LC
115
116Throw an error on failure."
615c5298 117 (let ((session (make-session #:user user
a9b09ed7 118 #:identity identity
615c5298
LC
119 #:host host
120 #:port port
121 #:timeout 10 ;seconds
122 ;; #:log-verbosity 'protocol
123
2b868284
LC
124 ;; Prevent libssh from reading
125 ;; ~/.ssh/known_hosts when the caller provides
126 ;; a HOST-KEY to match against.
127 #:knownhosts (and host-key "/dev/null")
128
615c5298
LC
129 ;; We need lightweight compression when
130 ;; exchanging full archives.
131 #:compression compression
132 #:compression-level 3)))
133
134 ;; Honor ~/.ssh/config.
135 (session-parse-config! session)
136
137 (match (connect! session)
138 ('ok
2b868284
LC
139 (if host-key
140 ;; Make sure the server's key is what we expect.
141 (authenticate-server* session host-key)
142
143 ;; Authenticate against ~/.ssh/known_hosts.
144 (match (authenticate-server session)
145 ('ok #f)
146 (reason
147 (raise (condition
148 (&message
149 (message (format #f (G_ "failed to authenticate \
f5c18018 150server at '~a': ~a")
2b868284
LC
151 (session-get session 'host)
152 reason))))))))
f5c18018 153
615c5298
LC
154 ;; Use public key authentication, via the SSH agent if it's available.
155 (match (userauth-public-key/auto! session)
156 ('success
81c5873a 157 (session-set! session 'timeout timeout)
615c5298
LC
158 session)
159 (x
35f35111
LDB
160 (match (userauth-gssapi! session)
161 ('success
162 (session-set! session 'timeout timeout)
163 session)
164 (x
165 (disconnect! session)
166 (raise (condition
167 (&message
168 (message (format #f (G_ "SSH authentication failed for '~a': ~a~%")
169 host (get-error session)))))))))))
615c5298
LC
170 (x
171 ;; Connection failed or timeout expired.
172 (raise (condition
173 (&message
69daee23 174 (message (format #f (G_ "SSH connection to '~a' failed: ~a~%")
615c5298
LC
175 host (get-error session))))))))))
176
5ea7537b
JK
177(define* (remote-inferior session #:optional become-command)
178 "Return a remote inferior for the given SESSION. If BECOME-COMMAND is
179given, use that to invoke the remote Guile REPL."
180 (let* ((repl-command (append (or become-command '())
181 '("guix" "repl" "-t" "machine")))
182 (pipe (apply open-remote-pipe* session OPEN_BOTH repl-command)))
5ea7537b 183 (when (eof-object? (peek-char pipe))
e09c7f4a
LC
184 (let ((status (channel-get-exit-status pipe)))
185 (close-port pipe)
186 (raise (condition
187 (&message
188 (message (format #f (G_ "remote command '~{~a~^ ~}' failed \
189with status ~a")
190 repl-command status)))))))
af15fe13
LC
191 (port->inferior pipe)))
192
5ea7537b 193(define* (inferior-remote-eval exp session #:optional become-command)
ed7b4437 194 "Evaluate EXP in a new inferior running in SESSION, and close the inferior
5ea7537b
JK
195right away. If BECOME-COMMAND is given, use that to invoke the remote Guile
196REPL."
197 (let ((inferior (remote-inferior session become-command)))
ed7b4437
LC
198 (dynamic-wind
199 (const #t)
200 (lambda ()
201 (inferior-eval exp inferior))
202 (lambda ()
203 ;; Close INFERIOR right away to prevent finalization from happening in
204 ;; another thread at the wrong time (see
205 ;; <https://bugs.gnu.org/26976>.)
206 (close-inferior inferior)))))
207
e5378337
LC
208(define* (remote-daemon-channel session
209 #:optional
210 (socket-name
211 "/var/guix/daemon-socket/socket"))
212 "Return an input/output port (an SSH channel) to the daemon at SESSION."
987a29ba
LC
213 (define redirect
214 ;; Code run in SESSION to redirect the remote process' stdin/stdout to the
215 ;; daemon's socket, à la socat. The SSH protocol supports forwarding to
216 ;; Unix-domain sockets but libssh doesn't have an API for that, hence this
217 ;; hack.
218 `(begin
17af5d51 219 (use-modules (ice-9 match) (rnrs io ports)
0dcf675c 220 (rnrs bytevectors))
987a29ba 221
8446dc5a
LC
222 (let ((sock (socket AF_UNIX SOCK_STREAM 0))
223 (stdin (current-input-port))
224 (stdout (current-output-port))
225 (select* (lambda (read write except)
226 ;; This is a workaround for
227 ;; <https://bugs.gnu.org/30365> in Guile < 2.2.4:
228 ;; since 'select' sometimes returns non-empty sets for
229 ;; no good reason, call 'select' a second time with a
230 ;; zero timeout to filter out incorrect replies.
231 (match (select read write except)
232 ((read write except)
233 (select read write except 0))))))
76832d34 234 (setvbuf stdout 'none)
0dcf675c
LC
235
236 ;; Use buffered ports so that 'get-bytevector-some' returns up to the
237 ;; whole buffer like read(2) would--see <https://bugs.gnu.org/30066>.
76832d34
LC
238 (setvbuf stdin 'block 65536)
239 (setvbuf sock 'block 65536)
0dcf675c 240
987a29ba
LC
241 (connect sock AF_UNIX ,socket-name)
242
243 (let loop ()
8446dc5a 244 (match (select* (list stdin sock) '() '())
55f40fdb 245 ((reads () ())
987a29ba 246 (when (memq stdin reads)
0dcf675c
LC
247 (match (get-bytevector-some stdin)
248 ((? eof-object?)
987a29ba 249 (primitive-exit 0))
0dcf675c
LC
250 (bv
251 (put-bytevector sock bv)
252 (force-output sock))))
987a29ba 253 (when (memq sock reads)
0dcf675c
LC
254 (match (get-bytevector-some sock)
255 ((? eof-object?)
987a29ba 256 (primitive-exit 0))
0dcf675c
LC
257 (bv
258 (put-bytevector stdout bv))))
987a29ba
LC
259 (loop))
260 (_
261 (primitive-exit 1)))))))
262
e5378337
LC
263 (open-remote-pipe* session OPEN_BOTH
264 ;; Sort-of shell-quote REDIRECT.
265 "guile" "-c"
266 (object->string
267 (object->string redirect))))
268
269(define* (connect-to-remote-daemon session
270 #:optional
271 (socket-name
272 "/var/guix/daemon-socket/socket"))
273 "Connect to the remote build daemon listening on SOCKET-NAME over SESSION,
de9fbe9c 274an SSH session. Return a <store-connection> object."
2e4d8339 275 (open-connection #:port (remote-daemon-channel session socket-name)))
e5378337 276
987a29ba
LC
277
278(define (store-import-channel session)
279 "Return an output port to which archives to be exported to SESSION's store
280can be written."
281 ;; Using the 'import-paths' RPC on a remote store would be slow because it
282 ;; makes a round trip every time 32 KiB have been transferred. This
283 ;; procedure instead opens a separate channel to use the remote
284 ;; 'import-paths' procedure, which consumes all the data in a single round
de9d8f0e
LC
285 ;; trip. This optimizes the successful case at the expense of error
286 ;; conditions: errors can only be reported once all the input has been
287 ;; consumed.
987a29ba
LC
288 (define import
289 `(begin
de9d8f0e
LC
290 (use-modules (guix) (srfi srfi-34)
291 (rnrs io ports) (rnrs bytevectors))
987a29ba 292
de9d8f0e
LC
293 (define (consume-input port)
294 (let ((bv (make-bytevector 32768)))
295 (let loop ()
296 (let ((n (get-bytevector-n! port bv 0
297 (bytevector-length bv))))
298 (unless (eof-object? n)
299 (loop))))))
987a29ba 300
de9d8f0e
LC
301 ;; Upon completion, write an sexp that denotes the status.
302 (write
303 (catch #t
304 (lambda ()
305 (guard (c ((nix-protocol-error? c)
306 ;; Consume all the input since the only time we can
307 ;; report the error is after everything has been
308 ;; consumed.
309 (consume-input (current-input-port))
310 (list 'protocol-error (nix-protocol-error-message c))))
311 (with-store store
76832d34 312 (setvbuf (current-input-port) 'none)
de9d8f0e
LC
313 (import-paths store (current-input-port))
314 '(success))))
315 (lambda args
316 (cons 'error args))))))
987a29ba 317
de9d8f0e
LC
318 (open-remote-pipe session
319 (string-join
320 `("guile" "-c"
321 ,(object->string (object->string import))))
322 OPEN_BOTH))
987a29ba 323
e9629e82
LC
324(define* (store-export-channel session files
325 #:key recursive?)
987a29ba 326 "Return an input port from which an export of FILES from SESSION's store can
e9629e82 327be read. When RECURSIVE? is true, the closure of FILES is exported."
987a29ba
LC
328 ;; Same as above: this is more efficient than calling 'export-paths' on a
329 ;; remote store.
330 (define export
331 `(begin
896fec47
LC
332 (eval-when (load expand eval)
333 (unless (resolve-module '(guix) #:ensure #f)
334 (write `(module-error))
335 (exit 7)))
336
337 (use-modules (guix) (srfi srfi-1)
338 (srfi srfi-26) (srfi srfi-34))
339
340 (guard (c ((nix-connection-error? c)
341 (write `(connection-error ,(nix-connection-error-file c)
342 ,(nix-connection-error-code c))))
343 ((nix-protocol-error? c)
344 (write `(protocol-error ,(nix-protocol-error-status c)
345 ,(nix-protocol-error-message c))))
346 (else
347 (write `(exception))))
348 (with-store store
349 (let* ((files ',files)
350 (invalid (remove (cut valid-path? store <>)
351 files)))
352 (unless (null? invalid)
353 (write `(invalid-items ,invalid))
354 (exit 1))
355
0e3c8528
LC
356 ;; TODO: When RECURSIVE? is true, we could send the list of store
357 ;; items in the closure so that the other end can filter out
358 ;; those it already has.
359
896fec47
LC
360 (write '(exporting)) ;we're ready
361 (force-output)
362
76832d34 363 (setvbuf (current-output-port) 'none)
896fec47
LC
364 (export-paths store files (current-output-port)
365 #:recursive? ,recursive?))))))
987a29ba
LC
366
367 (open-remote-input-pipe session
368 (string-join
369 `("guile" "-c"
370 ,(object->string
371 (object->string export))))))
372
2c8e04f1
JK
373(define (remote-system session)
374 "Return the system type as expected by Nix, usually ARCHITECTURE-KERNEL, of
375the machine on the other end of SESSION."
376 (inferior-remote-eval '(begin (use-modules (guix utils)) (%current-system))
377 session))
3033d59a 378
4cc5e520 379(define* (remote-authorize-signing-key key session #:optional become-command)
3033d59a
JK
380 "Send KEY, a canonical sexp containing a public key, over SESSION and add it
381to the system ACL file if it has not yet been authorized."
382 (inferior-remote-eval
383 `(begin
384 (use-modules (guix build utils)
385 (guix pki)
386 (guix utils)
387 (gcrypt pk-crypto)
388 (srfi srfi-26))
389
390 (define acl (current-acl))
391 (define key (string->canonical-sexp ,(canonical-sexp->string key)))
392
393 (unless (authorized-key? key)
394 (let ((acl (public-keys->acl (cons key (acl->public-keys acl)))))
395 (mkdir-p (dirname %acl-file))
396 (with-atomic-file-output %acl-file
397 (cut write-acl acl <>)))))
4cc5e520
JK
398 session
399 become-command))
2c8e04f1 400
987a29ba 401(define* (send-files local files remote
e9629e82
LC
402 #:key
403 recursive?
404 (log-port (current-error-port)))
987a29ba 405 "Send the subset of FILES from LOCAL (a local store) that's missing to
23973e4f
LC
406REMOTE, a remote store. When RECURSIVE? is true, send the closure of FILES.
407Return the list of store items actually sent."
8f53d734
LC
408 (define (inferior-remote-eval* exp session)
409 (guard (c ((inferior-exception? c)
410 (match (inferior-exception-arguments c)
411 (('quit 7)
412 (report-module-error (remote-store-host remote)))
413 (_
414 (report-inferior-exception c (remote-store-host remote))))))
415 (inferior-remote-eval exp session)))
416
987a29ba 417 ;; Compute the subset of FILES missing on SESSION and send them.
e9629e82 418 (let* ((files (if recursive? (requisites local files) files))
de9fbe9c 419 (session (channel-get-session (store-connection-socket remote)))
8f53d734 420 (missing (inferior-remote-eval*
ed7b4437 421 `(begin
8f53d734
LC
422 (eval-when (load expand eval)
423 (unless (resolve-module '(guix) #:ensure #f)
424 (exit 7)))
425
ed7b4437
LC
426 (use-modules (guix)
427 (srfi srfi-1) (srfi srfi-26))
987a29ba 428
ed7b4437
LC
429 (with-store store
430 (remove (cut valid-path? store <>)
431 ',files)))
432 session))
987a29ba 433 (count (length missing))
b90d97ec
LC
434 (sizes (map (lambda (item)
435 (path-info-nar-size (query-path-info local item)))
436 missing))
987a29ba 437 (port (store-import-channel session)))
b90d97ec
LC
438 (format log-port (N_ "sending ~a store item (~h MiB) to '~a'...~%"
439 "sending ~a store items (~h MiB) to '~a'...~%" count)
440 count
441 (inexact->exact (round (/ (reduce + 0 sizes) (expt 2. 20))))
442 (session-get session 'host))
987a29ba
LC
443
444 ;; Send MISSING in topological order.
445 (export-paths local missing port)
446
447 ;; Tell the remote process that we're done. (In theory the end-of-archive
448 ;; mark of 'export-paths' would be enough, but in practice it's not.)
449 (channel-send-eof port)
450
de9d8f0e 451 ;; Wait for completion of the remote process and read the status sexp from
63fd9f08
LC
452 ;; PORT. Wait for the exit status only when 'read' completed; otherwise,
453 ;; we might wait forever if the other end is stuck.
de9d8f0e 454 (let* ((result (false-if-exception (read port)))
63fd9f08
LC
455 (status (and result
456 (zero? (channel-get-exit-status port)))))
987a29ba 457 (close-port port)
de9d8f0e
LC
458 (match result
459 (('success . _)
460 missing)
461 (('protocol-error message)
462 (raise (condition
f9e8a123 463 (&store-protocol-error (message message) (status 42)))))
de9d8f0e
LC
464 (('error key args ...)
465 (raise (condition
f9e8a123 466 (&store-protocol-error
de9d8f0e
LC
467 (message (call-with-output-string
468 (lambda (port)
469 (print-exception port #f key args))))
470 (status 43)))))
471 (_
472 (raise (condition
f9e8a123 473 (&store-protocol-error
de9d8f0e
LC
474 (message "unknown error while sending files over SSH")
475 (status 44)))))))))
987a29ba
LC
476
477(define (remote-store-session remote)
478 "Return the SSH channel beneath REMOTE, a remote store as returned by
479'connect-to-remote-daemon', or #f."
de9fbe9c 480 (channel-get-session (store-connection-socket remote)))
987a29ba
LC
481
482(define (remote-store-host remote)
483 "Return the name of the host REMOTE is connected to, where REMOTE is a
484remote store as returned by 'connect-to-remote-daemon'."
485 (match (remote-store-session remote)
486 (#f #f)
487 ((? session? session)
488 (session-get session 'host))))
489
e9629e82
LC
490(define* (file-retrieval-port files remote
491 #:key recursive?)
987a29ba
LC
492 "Return an input port from which to retrieve FILES (a list of store items)
493from REMOTE, along with the number of items to retrieve (lower than or equal
494to the length of FILES.)"
e9629e82
LC
495 (values (store-export-channel (remote-store-session remote) files
496 #:recursive? recursive?)
497 (length files))) ;XXX: inaccurate when RECURSIVE? is true
987a29ba 498
896fec47
LC
499(define-syntax raise-error
500 (syntax-rules (=>)
501 ((_ fmt args ... (=> hint-fmt hint-args ...))
502 (raise (condition
503 (&message
504 (message (format #f fmt args ...)))
505 (&fix-hint
506 (hint (format #f hint-fmt hint-args ...))))))
507 ((_ fmt args ...)
508 (raise (condition
509 (&message
510 (message (format #f fmt args ...))))))))
511
d06d54e3
LC
512(define* (retrieve-files* files remote
513 #:key recursive? (log-port (current-error-port))
514 (import (const #f)))
515 "Pass IMPORT an input port from which to read the sequence of FILES coming
516from REMOTE. When RECURSIVE? is true, retrieve the closure of FILES."
987a29ba 517 (let-values (((port count)
e9629e82
LC
518 (file-retrieval-port files remote
519 #:recursive? recursive?)))
896fec47
LC
520 (match (read port) ;read the initial status
521 (('exporting)
522 (format #t (N_ "retrieving ~a store item from '~a'...~%"
523 "retrieving ~a store items from '~a'...~%" count)
524 count (remote-store-host remote))
525
d06d54e3
LC
526 (dynamic-wind
527 (const #t)
528 (lambda ()
529 (import port))
530 (lambda ()
531 (close-port port))))
896fec47 532 ((? eof-object?)
4eb0f9ae 533 (report-guile-error (remote-store-host remote)))
896fec47 534 (('module-error . _)
4eb0f9ae 535 (report-module-error (remote-store-host remote)))
896fec47
LC
536 (('connection-error file code . _)
537 (raise-error (G_ "failed to connect to '~A' on remote host '~A': ~a")
538 file (remote-store-host remote) (strerror code)))
539 (('invalid-items items . _)
540 (raise-error (N_ "no such item on remote host '~A':~{ ~a~}"
541 "no such items on remote host '~A':~{ ~a~}"
542 (length items))
543 (remote-store-host remote) items))
544 (('protocol-error status message . _)
545 (raise-error (G_ "protocol error on remote host '~A': ~a")
546 (remote-store-host remote) message))
547 (_
548 (raise-error (G_ "failed to retrieve store items from '~a'")
549 (remote-store-host remote))))))
987a29ba 550
d06d54e3
LC
551(define* (retrieve-files local files remote
552 #:key recursive? (log-port (current-error-port)))
553 "Retrieve FILES from REMOTE and import them using the 'import-paths' RPC on
554LOCAL. When RECURSIVE? is true, retrieve the closure of FILES."
0e3c8528
LC
555 (retrieve-files* (remove (cut valid-path? local <>) files)
556 remote
d06d54e3
LC
557 #:recursive? recursive?
558 #:log-port log-port
559 #:import (lambda (port)
560 (import-paths local port))))
561
4eb0f9ae
LC
562\f
563;;;
564;;; Error reporting.
565;;;
566
567(define (report-guile-error host)
568 (raise-error (G_ "failed to start Guile on remote host '~A'") host
569 (=> (G_ "Make sure @command{guile} can be found in
570@code{$PATH} on the remote host. Run @command{ssh ~A guile --version} to
571check.")
572 host)))
573
574(define (report-module-error host)
575 "Report an error about missing Guix modules on HOST."
576 ;; TRANSLATORS: Leave "Guile" untranslated.
577 (raise-error (G_ "Guile modules not found on remote host '~A'") host
578 (=> (G_ "Make sure @code{GUILE_LOAD_PATH} includes Guix'
579own module directory. Run @command{ssh ~A env | grep GUILE_LOAD_PATH} to
580check.")
581 host)))
582
8f53d734
LC
583(define (report-inferior-exception exception host)
584 "Report EXCEPTION, an &inferior-exception that occurred on HOST."
585 (raise-error (G_ "exception occurred on remote host '~A': ~s")
586 host (inferior-exception-arguments exception)))
587
987a29ba 588;;; ssh.scm ends here