~ruther/guix-local

957afcae3cded622f4260385f69b40dbdcaade9f — Ludovic Courtès 10 years ago b2fef04
Add (gnu tests) and (gnu build marionette).

* gnu/build/marionette.scm, gnu/tests.scm: New files.
* gnu/local.mk (GNU_SYSTEM_MODULES): Add them.
* gnu/system/vm.scm (common-qemu-options): Remove '-serial stdio'.
4 files changed, 341 insertions(+), 2 deletions(-)

A gnu/build/marionette.scm
M gnu/local.mk
M gnu/system/vm.scm
A gnu/tests.scm
A gnu/build/marionette.scm => gnu/build/marionette.scm +206 -0
@@ 0,0 1,206 @@
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2016 Ludovic Courtès <ludo@gnu.org>
;;;
;;; 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 build marionette)
  #:use-module (srfi srfi-9)
  #:use-module (srfi srfi-26)
  #:use-module (rnrs io ports)
  #:use-module (ice-9 match)
  #:export (marionette?
            make-marionette
            marionette-eval
            marionette-control
            %qwerty-us-keystrokes
            marionette-type))

;;; Commentary:
;;;
;;; Instrumentation tools for QEMU virtual machines (VMs).  A "marionette" is
;;; essentially a VM (a QEMU instance) with its monitor connected to a
;;; Unix-domain socket, and with a REPL inside the guest listening on a
;;; virtual console, which is itself connected to the host via a Unix-domain
;;; socket--these are the marionette's strings, connecting it to the almighty
;;; puppeteer.
;;;
;;; Code:

(define-record-type <marionette>
  (marionette command pid monitor repl)
  marionette?
  (command    marionette-command)                 ;list of strings
  (pid        marionette-pid)                     ;integer
  (monitor    marionette-monitor)                 ;port
  (repl       marionette-repl))                   ;port

(define* (wait-for-monitor-prompt port #:key (quiet? #t))
  "Read from PORT until we have seen all of QEMU's monitor prompt.  When
QUIET? is false, the monitor's output is written to the current output port."
  (define full-prompt
    (string->list "(qemu) "))

  (let loop ((prompt full-prompt)
             (matches '())
             (prefix  '()))
    (match prompt
      (()
       ;; It's useful to set QUIET? so we don't display the echo of our own
       ;; commands.
       (unless quiet?
         (for-each (lambda (line)
                     (format #t "qemu monitor: ~a~%" line))
                   (string-tokenize (list->string (reverse prefix))
                                    (char-set-complement (char-set #\newline))))))
      ((chr rest ...)
       (let ((read (read-char port)))
         (cond ((eqv? read chr)
                (loop rest (cons read matches) prefix))
               ((eof-object? read)
                (error "EOF while waiting for QEMU monitor prompt"
                       (list->string (reverse prefix))))
               (else
                (loop full-prompt
                      '()
                      (cons read (append matches prefix))))))))))

(define* (make-marionette command
                          #:key (socket-directory "/tmp") (timeout 20))
  "Return a QEMU marionette--i.e., a virtual machine with open connections to the
QEMU monitor and to the guest's backdoor REPL."
  (define (file->sockaddr file)
    (make-socket-address AF_UNIX
                         (string-append socket-directory "/" file)))

  (define extra-options
    (list "-nographic"
          "-monitor" (string-append "unix:" socket-directory "/monitor")
          "-chardev" (string-append "socket,id=repl,path=" socket-directory
                                    "/repl")
          "-device" "virtio-serial"
          "-device" "virtconsole,chardev=repl"))

  (let ((monitor (socket AF_UNIX SOCK_STREAM 0))
        (repl    (socket AF_UNIX SOCK_STREAM 0)))
    (bind monitor (file->sockaddr "monitor"))
    (listen monitor 1)
    (bind repl (file->sockaddr "repl"))
    (listen repl 1)

    (match (primitive-fork)
      (0
       (catch #t
         (lambda ()
           (close monitor)
           (close repl)
           (match command
             ((program . args)
              (apply execl program program
                     (append args extra-options)))))
         (lambda (key . args)
           (print-exception (current-error-port)
                            (stack-ref (make-stack #t) 1)
                            key args)
           (primitive-exit 1))))
      (pid
       (format #t "QEMU runs as PID ~a~%" pid)
       (sigaction SIGALRM
         (lambda (signum)
           (display "time is up!\n")              ;FIXME: break
           #t))
       (alarm timeout)

       (match (accept monitor)
         ((monitor-conn . _)
          (display "connected to QEMU's monitor\n")
          (close-port monitor)
          (wait-for-monitor-prompt monitor-conn)
          (display "read QEMU monitor prompt\n")
          (match (accept repl)
            ((repl-conn . addr)
             (display "connected to guest REPL\n")
             (close-port repl)
             (match (read repl-conn)
               ('ready
                (alarm 0)
                (sigaction SIGALRM SIG_DFL)
                (display "marionette is ready\n")
                (marionette (append command extra-options) pid
                            monitor-conn repl-conn)))))))))))

(define (marionette-eval exp marionette)
  "Evaluate EXP in MARIONETTE's backdoor REPL.  Return the result."
  (match marionette
    (($ <marionette> command pid monitor repl)
     (write exp repl)
     (newline repl)
     (read repl))))

(define (marionette-control command marionette)
  "Run COMMAND in the QEMU monitor of MARIONETTE.  COMMAND is a string such as
\"sendkey ctrl-alt-f1\" or \"screendump foo.ppm\" (info \"(qemu-doc)
pcsys_monitor\")."
  (match marionette
    (($ <marionette> _ _ monitor)
     (display command monitor)
     (newline monitor)
     (wait-for-monitor-prompt monitor))))

(define %qwerty-us-keystrokes
  ;; Maps "special" characters to their keystrokes.
  '((#\newline . "ret")
    (#\space . "spc")
    (#\- . "minus")
    (#\+ . "shift-equal")
    (#\* . "shift-8")
    (#\= . "equal")
    (#\? . "shift-slash")
    (#\[ . "bracket_left")
    (#\] . "bracket_right")
    (#\( . "shift-9")
    (#\) . "shift-0")
    (#\/ . "slash")
    (#\< . "less")
    (#\> . "shift-less")
    (#\. . "dot")
    (#\, . "comma")
    (#\; . "semicolon")
    (#\bs . "backspace")
    (#\tab . "tab")))

(define* (string->keystroke-commands str
                                     #:optional
                                     (keystrokes
                                      %qwerty-us-keystrokes))
  "Return a list of QEMU monitor commands to send the keystrokes corresponding
to STR.  KEYSTROKES is an alist specifying a mapping from characters to
keystrokes."
  (string-fold-right (lambda (chr result)
                       (cons (string-append "sendkey "
                                            (or (assoc-ref keystrokes chr)
                                                (string chr)))
                             result))
                     '()
                     str))

(define* (marionette-type str marionette
                          #:key (keystrokes %qwerty-us-keystrokes))
  "Type STR on MARIONETTE's keyboard, using the KEYSTROKES alist to map characters
to actual keystrokes."
  (for-each (cut marionette-control <> marionette)
            (string->keystroke-commands str keystrokes)))

;;; marionette.scm ends here

M gnu/local.mk => gnu/local.mk +4 -1
@@ 398,7 398,10 @@ GNU_SYSTEM_MODULES =				\
  gnu/build/linux-container.scm			\
  gnu/build/linux-initrd.scm			\
  gnu/build/linux-modules.scm			\
  gnu/build/vm.scm
  gnu/build/marionette.scm			\
  gnu/build/vm.scm				\
						\
  gnu/tests.scm


patchdir = $(guilemoduledir)/gnu/packages/patches

M gnu/system/vm.scm => gnu/system/vm.scm +1 -1
@@ 468,7 468,7 @@ with '-virtfs' options for the host file systems listed in SHARED-FS."
     " -no-reboot -net nic,model=virtio \
  " #$@(map virtfs-option shared-fs) " \
  -net user \
  -serial stdio -vga std \
  -vga std \
  -drive file=" #$image
  ",if=virtio,cache=writeback,werror=report,readonly \
  -m 256"))

A gnu/tests.scm => gnu/tests.scm +130 -0
@@ 0,0 1,130 @@
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2016 Ludovic Courtès <ludo@gnu.org>
;;;
;;; 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 tests)
  #:use-module (guix gexp)
  #:use-module (gnu system)
  #:use-module (gnu services)
  #:use-module (gnu services shepherd)
  #:export (backdoor-service-type
            marionette-operating-system))

;;; Commentary:
;;;
;;; This module provides the infrastructure to run operating system tests.
;;; The most important part of that is tools to instrument the OS under test,
;;; essentially allowing to run in a virtual machine controlled by the host
;;; system--hence the name "marionette".
;;;
;;; Code:

(define (marionette-shepherd-service imported-modules)
  "Return the Shepherd service for the marionette REPL"
  (define device
    "/dev/hvc0")

  (list (shepherd-service
         (provision '(marionette))
         (requirement '(udev))                    ;so that DEVICE is available
         (modules '((ice-9 match)
                    (srfi srfi-9 gnu)
                    (guix build syscalls)
                    (rnrs bytevectors)))
         (imported-modules `((guix build syscalls)
                             ,@imported-modules))
         (start
          #~(lambda ()
              (define (clear-echo termios)
                (set-field termios (termios-local-flags)
                           (logand (lognot (local-flags ECHO))
                                   (termios-local-flags termios))))

              (define (self-quoting? x)
                (letrec-syntax ((one-of (syntax-rules ()
                                          ((_) #f)
                                          ((_ pred rest ...)
                                           (or (pred x)
                                               (one-of rest ...))))))
                  (one-of symbol? string? pair? null? vector?
                          bytevector? number? boolean?)))

              (match (primitive-fork)
                (0
                 (dynamic-wind
                   (const #t)
                   (lambda ()
                     (let* ((repl    (open-file #$device "r+0"))
                            (termios (tcgetattr (fileno repl)))
                            (console (open-file "/dev/console" "r+0")))
                       ;; Don't echo input back.
                       (tcsetattr (fileno repl) (tcsetattr-action TCSANOW)
                                  (clear-echo termios))

                       ;; Redirect output to the console.
                       (close-fdes 1)
                       (close-fdes 2)
                       (dup2 (fileno console) 1)
                       (dup2 (fileno console) 2)
                       (close-port console)

                       (display 'ready repl)
                       (let loop ()
                         (newline repl)

                         (match (read repl)
                           ((? eof-object?)
                            (primitive-exit 0))
                           (expr
                            (catch #t
                              (lambda ()
                                (let ((result (primitive-eval expr)))
                                  (write (if (self-quoting? result)
                                             result
                                             (object->string result))
                                         repl)))
                              (lambda (key . args)
                                (print-exception (current-error-port)
                                                 (stack-ref (make-stack #t) 1)
                                                 key args)
                                (write #f repl)))))
                         (loop))))
                   (lambda ()
                     (primitive-exit 1))))
                (pid
                 pid))))
         (stop #~(make-kill-destructor)))))

(define marionette-service-type
  ;; This is the type of the "marionette" service, allowing a guest system to
  ;; be manipulated from the host.  This marionette REPL is essentially a
  ;; universal marionette.
  (service-type (name 'marionette-repl)
                (extensions
                 (list (service-extension shepherd-root-service-type
                                          marionette-shepherd-service)))))

(define* (marionette-operating-system os
                                      #:key (imported-modules '()))
  "Return a marionetteed variant of OS such that OS can be used as a marionette
in a virtual machine--i.e., controlled from the host system."
  (operating-system
    (inherit os)
    (services (cons (service marionette-service-type imported-modules)
                    (operating-system-user-services os)))))

;;; tests.scm ends here