* gnu/installer/tests.scm (choose-services): Accept a new choose-other-service? keyword argument, defaulting to false. Expect an "Other services" checkbox-list. Update docstring.
		
			
				
	
	
		
			391 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			Scheme
		
	
	
	
	
	
			
		
		
	
	
			391 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			Scheme
		
	
	
	
	
	
;;; 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
 | 
						|
            start-installation
 | 
						|
            complete-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")))
 | 
						|
                          (choose-other-service? (const #f)))
 | 
						|
  "Converse over PORT to choose 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))
 | 
						|
 | 
						|
    ((checkbox-list (title "Other services") (text _)
 | 
						|
                    (items ,services))
 | 
						|
     (filter choose-other-service? 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)
 | 
						|
                              (uefi-support? #f)
 | 
						|
                              (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.
 | 
						|
 | 
						|
When UEFI-SUPPORT? is true, assume that we are running the installation tests
 | 
						|
on an UEFI capable machine.
 | 
						|
 | 
						|
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 and if the system does not support UEFI.
 | 
						|
    ((list-selection (title "Partition table")
 | 
						|
                     (multiple-choices? #f)
 | 
						|
                     (items _))
 | 
						|
     ;; When UEFI is supported, the partition is forced to GPT by the
 | 
						|
     ;; installer.
 | 
						|
     (not uefi-support?)
 | 
						|
     "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 (start-installation port)
 | 
						|
  "Start the installation by checking over PORT that we get the generated
 | 
						|
configuration file, accepting it and starting the installation, and then
 | 
						|
receiving the pause message 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."
 | 
						|
     (values))))
 | 
						|
 | 
						|
(define (complete-installation port)
 | 
						|
  "Complete the installation by replying to the installer pause message and
 | 
						|
waiting for the installation-complete message."
 | 
						|
  ;; Assume the previous message received was 'pause'; here we send the reply
 | 
						|
  ;; to that message, which lets the installer continue.
 | 
						|
  (write #t port)
 | 
						|
  (newline port)
 | 
						|
  (force-output port)
 | 
						|
 | 
						|
  (converse port
 | 
						|
    ((installation-complete)
 | 
						|
     (values))))
 | 
						|
 | 
						|
;;; Local Variables:
 | 
						|
;;; eval: (put 'converse 'scheme-indent-function 1)
 | 
						|
;;; eval: (put 'with-ellipsis 'scheme-indent-function 1)
 | 
						|
;;; End:
 |