123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366 |
- ;;; GNU Guix --- Functional package management for GNU
- ;;; Copyright © 2020 Ludovic Courtès <ludo@gnu.org>
- ;;; Copyright © 2020 Mathieu Othacehe <m.othacehe@gmail.com>
- ;;;
- ;;; This file is part of GNU Guix.
- ;;;
- ;;; GNU Guix is free software; you can redistribute it and/or modify it
- ;;; under the terms of the GNU General Public License as published by
- ;;; the Free Software Foundation; either version 3 of the License, or (at
- ;;; your option) any later version.
- ;;;
- ;;; GNU Guix is distributed in the hope that it will be useful, but
- ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
- ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ;;; GNU General Public License for more details.
- ;;;
- ;;; You should have received a copy of the GNU General Public License
- ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
- (define-module (gnu installer tests)
- #:use-module (srfi srfi-1)
- #:use-module (srfi srfi-34)
- #:use-module (srfi srfi-35)
- #:use-module (ice-9 match)
- #:use-module (ice-9 regex)
- #:use-module (ice-9 pretty-print)
- #:export (&pattern-not-matched
- pattern-not-matched?
- %installer-socket-file
- open-installer-socket
- converse
- conversation-log-port
- choose-locale+keyboard
- enter-host-name+passwords
- choose-services
- choose-partitioning
- conclude-installation
- edit-configuration-file))
- ;;; Commentary:
- ;;;
- ;;; This module provides tools to test the guided "graphical" installer in a
- ;;; non-interactive fashion. The core of it is 'converse': it allows you to
- ;;; state Expect-style dialogues, which happen over the Unix-domain socket the
- ;;; installer listens to. Higher-level procedures such as
- ;;; 'choose-locale+keyboard' are provided to perform specific parts of the
- ;;; dialogue.
- ;;;
- ;;; Code:
- (define %installer-socket-file
- ;; Socket the installer listens to.
- "/var/guix/installer-socket")
- (define* (open-installer-socket #:optional (file %installer-socket-file))
- "Return a socket connected to the installer."
- (let ((sock (socket AF_UNIX SOCK_STREAM 0)))
- (connect sock AF_UNIX file)
- sock))
- (define-condition-type &pattern-not-matched &error
- pattern-not-matched?
- (pattern pattern-not-matched-pattern)
- (sexp pattern-not-matched-sexp))
- (define (pattern-error pattern sexp)
- (raise (condition
- (&pattern-not-matched
- (pattern pattern) (sexp sexp)))))
- (define conversation-log-port
- ;; Port where debugging info is logged
- (make-parameter (current-error-port)))
- (define (converse-debug pattern)
- (format (conversation-log-port)
- "conversation expecting pattern ~s~%"
- pattern))
- (define-syntax converse
- (lambda (s)
- "Convert over PORT: read sexps from there, match them against each
- PATTERN, and send the corresponding REPLY. Raise to '&pattern-not-matched'
- when one of the PATTERNs is not matched."
- ;; XXX: Strings that appear in PATTERNs must be in the language the
- ;; installer is running in. In the future, we should add support to allow
- ;; writing English strings in PATTERNs and have the pattern matcher
- ;; automatically translate them.
- ;; Here we emulate 'pmatch' syntax on top of 'match'. This is ridiculous
- ;; but that's because 'pmatch' compares objects with 'eq?', making it
- ;; pretty useless, and it doesn't support ellipses and such.
- (define (quote-pattern s)
- ;; Rewrite the pattern S from pmatch style (a ,b) to match style like
- ;; ('a b).
- (with-ellipsis :::
- (syntax-case s (unquote _ ...)
- ((unquote id) #'id)
- (_ #'_)
- (... #'...)
- (id
- (identifier? #'id)
- #''id)
- ((lst :::) (map quote-pattern #'(lst :::)))
- (pattern #'pattern))))
- (define (match-pattern s)
- ;; Match one pattern without a guard.
- (syntax-case s ()
- ((port (pattern reply) continuation)
- (with-syntax ((pattern (quote-pattern #'pattern)))
- #'(let ((pat 'pattern))
- (converse-debug pat)
- (match (read port)
- (pattern
- (let ((data (call-with-values (lambda () reply)
- list)))
- (for-each (lambda (obj)
- (write obj port)
- (newline port))
- data)
- (force-output port)
- (continuation port)))
- (sexp
- (pattern-error pat sexp))))))))
- (syntax-case s ()
- ((_ port (pattern reply) rest ...)
- (match-pattern #'(port (pattern reply)
- (lambda (port)
- (converse port rest ...)))))
- ((_ port (pattern guard reply) rest ...)
- #`(let ((skip? (not guard))
- (next (lambda (p)
- (converse p rest ...))))
- (if skip?
- (next port)
- #,(match-pattern #'(port (pattern reply) next)))))
- ((_ port)
- #t))))
- (define* (choose-locale+keyboard port
- #:key
- (language "English")
- (location "Hong Kong")
- (timezone '("Europe" "Zagreb"))
- (keyboard
- '("English (US)"
- "English (intl., with AltGr dead keys)")))
- "Converse over PORT with the guided installer to choose the specified
- LANGUAGE, LOCATION, TIMEZONE, and KEYBOARD."
- (converse port
- ((list-selection (title "Locale language")
- (multiple-choices? #f)
- (items _))
- language)
- ((list-selection (title "Locale location")
- (multiple-choices? #f)
- (items _))
- location)
- ((menu (title "GNU Guix install")
- (text _)
- (items (,guided _ ...))) ;"Guided graphical installation"
- guided)
- ((list-selection (title "Timezone")
- (multiple-choices? #f)
- (items _))
- (first timezone))
- ((list-selection (title "Timezone")
- (multiple-choices? #f)
- (items _))
- (second timezone))
- ((list-selection (title "Layout")
- (multiple-choices? #f)
- (items _))
- (first keyboard))
- ((list-selection (title "Variant")
- (multiple-choices? #f)
- (items _))
- (second keyboard))))
- (define* (enter-host-name+passwords port
- #:key
- (host-name "guix")
- (root-password "foo")
- (users '(("alice" "pass1")
- ("bob" "pass2")
- ("charlie" "pass3"))))
- "Converse over PORT with the guided installer to choose HOST-NAME,
- ROOT-PASSWORD, and USERS."
- (converse port
- ((input (title "Hostname") (text _) (default _))
- host-name)
- ((input (title "System administrator password") (text _) (default _))
- root-password)
- ((input (title "Password confirmation required") (text _) (default _))
- root-password)
- ((add-users)
- (match users
- (((names passwords) ...)
- (map (lambda (name password)
- `(user (name ,name) (real-name ,(string-titlecase name))
- (home-directory ,(string-append "/home/" name))
- (password ,password)))
- names passwords))))))
- (define* (choose-services port
- #:key
- (choose-desktop-environment? (const #f))
- (choose-network-service?
- (lambda (service)
- (or (string-contains service "SSH")
- (string-contains service "NSS"))))
- (choose-network-management-tool?
- (lambda (service)
- (string-contains service "DHCP"))))
- "Converse over PORT to choose networking services."
- (define desktop-environments '())
- (converse port
- ((checkbox-list (title "Desktop environment") (text _)
- (items ,services))
- (let ((desktops (filter choose-desktop-environment? services)))
- (set! desktop-environments desktops)
- desktops))
- ((checkbox-list (title "Network service") (text _)
- (items ,services))
- (filter choose-network-service? services))
- ;; The "Network management" dialog shows up only when no desktop
- ;; environments have been selected, hence the guard.
- ((list-selection (title "Network management")
- (multiple-choices? #f)
- (items ,services))
- (null? desktop-environments)
- (find choose-network-management-tool? services))))
- (define (edit-configuration-file file)
- "Edit FILE, an operating system configuration file generated by the
- installer, by adding a marionette service such that the installed OS is
- instrumented for further testing."
- (define (read-expressions port)
- (let loop ((result '()))
- (match (read port)
- ((? eof-object?)
- (reverse result))
- (exp
- (loop (cons exp result))))))
- (define (edit exp)
- (match exp
- (('operating-system _ ...)
- `(marionette-operating-system ,exp
- #:imported-modules
- '((gnu services herd)
- (guix build utils)
- (guix combinators))))
- (_
- exp)))
- (let ((content (call-with-input-file file read-expressions)))
- (call-with-output-file file
- (lambda (port)
- (format port "\
- ;; Operating system configuration edited for automated testing.~%~%")
- (pretty-print '(use-modules (gnu tests)) port)
- (for-each (lambda (exp)
- (pretty-print (edit exp) port)
- (newline port))
- content)))
- #t))
- (define* (choose-partitioning port
- #:key
- (encrypted? #t)
- (passphrase "thepassphrase")
- (edit-configuration-file
- edit-configuration-file))
- "Converse over PORT to choose the partitioning method. When ENCRYPTED? is
- true, choose full-disk encryption with PASSPHRASE as the LUKS passphrase.
- This conversation stops when the user partitions have been formatted, right
- before the installer generates the configuration file and shows it in a dialog
- box."
- (converse port
- ((list-selection (title "Partitioning method")
- (multiple-choices? #f)
- (items (,not-encrypted ,encrypted _ ...)))
- (if encrypted?
- encrypted
- not-encrypted))
- ((list-selection (title "Disk") (multiple-choices? #f)
- (items (,disks ...)))
- ;; When running the installation from an ISO image, the CD/DVD drive
- ;; shows up in the list. Avoid it.
- (find (lambda (disk)
- (not (or (string-contains disk "DVD")
- (string-contains disk "CD-ROM"))))
- disks))
- ;; The "Partition table" dialog pops up only if there's not already a
- ;; partition table.
- ((list-selection (title "Partition table")
- (multiple-choices? #f)
- (items _))
- "gpt")
- ((list-selection (title "Partition scheme")
- (multiple-choices? #f)
- (items (,one-partition _ ...)))
- one-partition)
- ((list-selection (title "Guided partitioning")
- (multiple-choices? #f)
- (items (,disk _ ...)))
- disk)
- ((input (title "Password required")
- (text _) (default #f))
- encrypted? ;only when ENCRYPTED?
- passphrase)
- ((input (title "Password confirmation required")
- (text _) (default #f))
- encrypted?
- passphrase)
- ((confirmation (title "Format disk?") (text _))
- #t)
- ((info (title "Preparing partitions") _ ...)
- (values)) ;nothing to return
- ((starting-final-step)
- ;; Do not return anything. The reply will be sent by
- ;; 'conclude-installation' and in the meantime the installer just waits
- ;; for us, giving us a chance to do things such as changing partition
- ;; UUIDs before it generates the configuration file.
- (values))))
- (define (conclude-installation port)
- "Conclude the installation by checking over PORT that we get the generated
- configuration file, accepting it and starting the installation, and then
- receiving the final messages once the 'guix system init' process has
- completed."
- ;; Assume the previous message received was 'starting-final-step'; here we
- ;; send the reply to that message, which lets the installer continue.
- (write #t port)
- (newline port)
- (force-output port)
- (converse port
- ((file-dialog (title "Configuration file")
- (text _)
- (file ,configuration-file))
- (edit-configuration-file configuration-file))
- ((pause) ;"Press Enter to continue."
- #t)
- ((installation-complete) ;congratulations!
- (values))))
- ;;; Local Variables:
- ;;; eval: (put 'converse 'scheme-indent-function 1)
- ;;; eval: (put 'with-ellipsis 'scheme-indent-function 1)
- ;;; End:
|