summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--gnu/build/marionette.scm206
-rw-r--r--gnu/local.mk5
-rw-r--r--gnu/system/vm.scm2
-rw-r--r--gnu/tests.scm130
4 files changed, 341 insertions, 2 deletions
diff --git a/gnu/build/marionette.scm b/gnu/build/marionette.scm
new file mode 100644
index 0000000000..9399c55313
--- /dev/null
+++ b/gnu/build/marionette.scm
@@ -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
diff --git a/gnu/local.mk b/gnu/local.mk
index 2f77c50940..d7797602e9 100644
--- a/gnu/local.mk
+++ b/gnu/local.mk
@@ -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
diff --git a/gnu/system/vm.scm b/gnu/system/vm.scm
index 2fbef6a3fc..e6ce42467a 100644
--- a/gnu/system/vm.scm
+++ b/gnu/system/vm.scm
@@ -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"))
diff --git a/gnu/tests.scm b/gnu/tests.scm
new file mode 100644
index 0000000000..08d8315ea0
--- /dev/null
+++ b/gnu/tests.scm
@@ -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