1 ;;; GNU Guix --- Functional package management for GNU
2 ;;; Copyright © 2013, 2014, 2015, 2016, 2017 Ludovic Courtès <ludo@gnu.org>
3 ;;; Copyright © 2016 Christopher Allan Webber <cwebber@dustycloud.org>
4 ;;; Copyright © 2016 Leo Famulari <leo@famulari.name>
5 ;;; Copyright © 2017 Mathieu Othacehe <m.othacehe@gmail.com>
6 ;;; Copyright © 2017 Marius Bakke <mbakke@fastmail.com>
8 ;;; This file is part of GNU Guix.
10 ;;; GNU Guix is free software; you can redistribute it and/or modify it
11 ;;; under the terms of the GNU General Public License as published by
12 ;;; the Free Software Foundation; either version 3 of the License, or (at
13 ;;; your option) any later version.
15 ;;; GNU Guix is distributed in the hope that it will be useful, but
16 ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 ;;; GNU General Public License for more details.
20 ;;; You should have received a copy of the GNU General Public License
21 ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
23 (define-module (gnu system vm)
24 #:use-module (guix config)
25 #:use-module (guix store)
26 #:use-module (guix gexp)
27 #:use-module (guix derivations)
28 #:use-module (guix packages)
29 #:use-module (guix monads)
30 #:use-module (guix records)
31 #:use-module (guix modules)
33 #:use-module ((gnu build vm)
34 #:select (qemu-command))
35 #:use-module (gnu packages base)
36 #:use-module (gnu packages bootloaders)
37 #:use-module (gnu packages cdrom)
38 #:use-module (gnu packages guile)
39 #:use-module (gnu packages gawk)
40 #:use-module (gnu packages bash)
41 #:use-module (gnu packages less)
42 #:use-module (gnu packages virtualization)
43 #:use-module (gnu packages disk)
44 #:use-module (gnu packages zile)
45 #:use-module (gnu packages linux)
46 #:use-module (gnu packages package-management)
47 #:use-module ((gnu packages make-bootstrap)
48 #:select (%guile-static-stripped))
49 #:use-module (gnu packages admin)
51 #:use-module (gnu bootloader)
52 #:use-module ((gnu bootloader grub) #:select (grub-mkrescue-bootloader))
53 #:use-module (gnu system shadow)
54 #:use-module (gnu system pam)
55 #:use-module (gnu system linux-initrd)
56 #:use-module (gnu bootloader)
57 #:use-module (gnu system file-systems)
58 #:use-module (gnu system)
59 #:use-module (gnu services)
61 #:use-module (srfi srfi-1)
62 #:use-module (srfi srfi-26)
63 #:use-module (ice-9 match)
65 #:export (expression->derivation-in-linux-vm
67 virtualized-operating-system
70 system-qemu-image/shared-store
71 system-qemu-image/shared-store-script
80 ;;; Tools to evaluate build expressions within virtual machines.
84 (define %linux-vm-file-systems
85 ;; File systems mounted for 'derivation-in-linux-vm'. The store and /xchg
86 ;; directory are shared with the host over 9p.
88 (mount-point (%store-prefix))
92 (options "trans=virtio")
99 (options "trans=virtio")
102 (define* (expression->derivation-in-linux-vm name exp
104 (system (%current-system))
112 (single-file-output? #f)
113 (make-disk-image? #f)
114 (references-graphs #f)
116 (disk-image-format "qcow2")
117 (disk-image-size 'guess))
118 "Evaluate EXP in a QEMU virtual machine running LINUX with INITRD (a
119 derivation). The virtual machine runs with MEMORY-SIZE MiB of memory. In the
120 virtual machine, EXP has access to all its inputs from the store; it should
121 put its output file(s) in the '/xchg' directory.
123 If SINGLE-FILE-OUTPUT? is true, copy a single file from '/xchg' to OUTPUT.
124 Otherwise, copy the contents of /xchg to a new directory OUTPUT.
126 When MAKE-DISK-IMAGE? is true, then create a QEMU disk image of type
127 DISK-IMAGE-FORMAT (e.g., 'qcow2' or 'raw'), of DISK-IMAGE-SIZE bytes and
128 return it. When DISK-IMAGE-SIZE is 'guess, estimate the image size based
129 based on the size of the closure of REFERENCES-GRAPHS.
131 When REFERENCES-GRAPHS is true, it must be a list of file name/store path
132 pairs, as for `derivation'. The files containing the reference graphs are
133 made available under the /xchg CIFS share."
135 ((user-builder (gexp->file "builder-in-linux-vm" exp))
136 (loader (gexp->file "linux-vm-loader"
137 #~(primitive-load #$user-builder)))
138 (coreutils -> (canonical-package coreutils))
139 (initrd (if initrd ; use the default initrd?
141 (base-initrd %linux-vm-file-systems
144 #:qemu-networking? #t))))
147 ;; Code that launches the VM that evaluates EXP.
148 (with-imported-modules (source-module-closure '((guix build utils)
151 (use-modules (guix build utils)
154 (let* ((inputs '#$(list qemu coreutils))
155 (linux (string-append #$linux "/"
156 #$(system-linux-image-file-name)))
157 (initrd (string-append #$initrd "/initrd"))
159 (graphs '#$(match references-graphs
160 (((graph-files . _) ...) graph-files)
162 (size #$(if (eq? 'guess disk-image-size)
163 #~(+ (* 70 (expt 2 20)) ;ESP
164 (estimated-partition-size graphs))
167 (set-path-environment-variable "PATH" '("bin") inputs)
169 (load-in-linux-vm loader
171 #:linux linux #:initrd initrd
172 #:memory-size #$memory-size
173 #:make-disk-image? #$make-disk-image?
174 #:single-file-output? #$single-file-output?
175 #:disk-image-format #$disk-image-format
176 #:disk-image-size size
177 #:references-graphs graphs)))))
179 (gexp->derivation name builder
180 ;; TODO: Require the "kvm" feature.
183 #:guile-for-build guile-for-build
184 #:references-graphs references-graphs)))
186 (define* (iso9660-image #:key
187 (name "iso9660-image")
190 (system (%current-system))
197 "Return a bootable, stand-alone iso9660 image.
199 INPUTS is a list of inputs (as for packages)."
200 (expression->derivation-in-linux-vm
202 (with-imported-modules (source-module-closure '((gnu build vm)
205 (use-modules (gnu build vm)
209 '#$(append (list qemu parted e2fsprogs dosfstools xorriso)
210 (map canonical-package
211 (list sed grep coreutils findutils gawk))
212 (if register-closures? (list guix) '())))
215 (graphs '#$(match inputs
218 ;; This variable is unused but allows us to add INPUTS-TO-COPY
221 '#$(map (match-lambda
223 ((name thing output) `(,thing ,output)))
226 (set-path-environment-variable "PATH" '("bin" "sbin") inputs)
227 (make-iso9660-image #$(bootloader-package bootloader)
231 #:register-closures? #$register-closures?
233 #:volume-id #$file-system-label
234 #:volume-uuid #$file-system-uuid)
237 #:make-disk-image? #f
238 #:single-file-output? #t
239 #:references-graphs inputs))
241 (define* (qemu-image #:key
243 (system (%current-system))
245 (disk-image-size 'guess)
246 (disk-image-format "qcow2")
247 (file-system-type "ext4")
252 (register-closures? #t)
255 "Return a bootable, stand-alone QEMU image of type DISK-IMAGE-FORMAT (e.g.,
256 'qcow2' or 'raw'), with a root partition of type FILE-SYSTEM-TYPE.
257 Optionally, FILE-SYSTEM-LABEL can be specified as the volume name for the root
258 partition. The returned image is a full disk image that runs OS-DERIVATION,
259 with a GRUB installation that uses GRUB-CONFIGURATION as its configuration
260 file (GRUB-CONFIGURATION must be the name of a file in the VM.)
262 INPUTS is a list of inputs (as for packages). When COPY-INPUTS? is true, copy
263 all of INPUTS into the image being built. When REGISTER-CLOSURES? is true,
264 register INPUTS in the store database of the image so that Guix can be used in
266 (expression->derivation-in-linux-vm
268 (with-imported-modules (source-module-closure '((gnu build vm)
271 (use-modules (gnu build vm)
276 '#$(append (list qemu parted e2fsprogs dosfstools)
277 (map canonical-package
278 (list sed grep coreutils findutils gawk))
279 (if register-closures? (list guix) '())))
281 ;; This variable is unused but allows us to add INPUTS-TO-COPY
284 '#$(map (match-lambda
286 ((name thing output) `(,thing ,output)))
289 (set-path-environment-variable "PATH" '("bin" "sbin") inputs)
291 (let* ((graphs '#$(match inputs
294 (initialize (root-partition-initializer
296 #:copy-closures? #$copy-inputs?
297 #:register-closures? #$register-closures?
298 #:system-directory #$os-drv))
299 (root-size #$(if (eq? 'guess disk-image-size)
300 #~(estimated-partition-size
301 (map (cut string-append "/xchg/" <>)
304 (* 50 (expt 2 20)))))
305 (partitions (list (partition
307 (label #$file-system-label)
308 (file-system #$file-system-type)
310 (initializer initialize))
311 ;; Append a small EFI System Partition for
312 ;; use with UEFI bootloaders.
314 ;; The standalone grub image is about 10MiB, but
315 ;; leave some room for custom or multiple images.
316 (size (* 40 (expt 2 20)))
317 (label "GNU-ESP") ;cosmetic only
318 ;; Use "vfat" here since this property is used
319 ;; when mounting. The actual FAT-ness is based
320 ;; on filesystem size (16 in this case).
323 (initialize-hard-disk "/dev/vda"
324 #:partitions partitions
325 #:grub-efi #$grub-efi
327 #$(bootloader-package bootloader)
328 #:bootcfg #$bootcfg-drv
330 #$(bootloader-configuration-file bootloader)
331 #:bootloader-installer
332 #$(bootloader-installer bootloader))
335 #:make-disk-image? #t
336 #:disk-image-size disk-image-size
337 #:disk-image-format disk-image-format
338 #:references-graphs inputs))
342 ;;; VM and disk images.
345 (define* (system-disk-image os
348 (file-system-type "ext4")
349 (disk-image-size (* 900 (expt 2 20)))
351 "Return the derivation of a disk image of DISK-IMAGE-SIZE bytes of the
352 system described by OS. Said image can be copied on a USB stick as is. When
353 VOLATILE? is true, the root file system is made volatile; this is useful
354 to USB sticks meant to be read-only."
355 (define normalize-label
356 ;; ISO labels are all-caps (case-insensitive), but since
357 ;; 'find-partition-by-label' is case-sensitive, make it all-caps here.
358 (if (string=? "iso9660" file-system-type)
362 ;; Volume name of the root file system. Since we don't know which device
363 ;; will hold it, we use the volume name to find it (using the UUID would
364 ;; be even better, but somewhat less convenient.)
365 (normalize-label "GuixSD_image"))
367 (define file-systems-to-keep
369 (string=? (file-system-mount-point fs) "/"))
370 (operating-system-file-systems os)))
372 (let ((os (operating-system (inherit os)
373 ;; Since this is meant to be used on real hardware, don't
374 ;; install QEMU networking or anything like that. Assume USB
375 ;; mass storage devices (usb-storage.ko) are available.
376 (initrd (lambda (file-systems . rest)
377 (apply base-initrd file-systems
381 (bootloader (if (string=? "iso9660" file-system-type)
382 (bootloader-configuration
383 (inherit (operating-system-bootloader os))
384 (bootloader grub-mkrescue-bootloader))
385 (operating-system-bootloader os)))
387 ;; Force our own root file system.
388 (file-systems (cons (file-system
392 (type file-system-type))
393 file-systems-to-keep)))))
395 (mlet* %store-monad ((os-drv (operating-system-derivation os))
396 (bootcfg (operating-system-bootcfg os)))
397 (if (string=? "iso9660" file-system-type)
398 (iso9660-image #:name name
399 #:file-system-label root-label
400 #:file-system-uuid #f
402 #:bootcfg-drv bootcfg
403 #:bootloader (bootloader-configuration-bootloader
404 (operating-system-bootloader os))
405 #:inputs `(("system" ,os-drv)
406 ("bootcfg" ,bootcfg)))
407 (qemu-image #:name name
409 #:bootcfg-drv bootcfg
410 #:bootloader (bootloader-configuration-bootloader
411 (operating-system-bootloader os))
412 #:disk-image-size disk-image-size
413 #:disk-image-format "raw"
414 #:file-system-type file-system-type
415 #:file-system-label root-label
417 #:register-closures? #t
418 #:inputs `(("system" ,os-drv)
419 ("bootcfg" ,bootcfg)))))))
421 (define* (system-qemu-image os
423 (file-system-type "ext4")
424 (disk-image-size (* 900 (expt 2 20))))
425 "Return the derivation of a freestanding QEMU image of DISK-IMAGE-SIZE bytes
426 of the GNU system as described by OS."
427 (define file-systems-to-keep
428 ;; Keep only file systems other than root and not normally bound to real
431 (let ((target (file-system-mount-point fs))
432 (source (file-system-device fs)))
433 (or (string=? target "/")
434 (string-prefix? "/dev/" source))))
435 (operating-system-file-systems os)))
437 (let ((os (operating-system (inherit os)
438 ;; Use an initrd with the whole QEMU shebang.
439 (initrd (lambda (file-systems . rest)
440 (apply base-initrd file-systems
444 ;; Force our own root file system.
445 (file-systems (cons (file-system
448 (type file-system-type))
449 file-systems-to-keep)))))
451 ((os-drv (operating-system-derivation os))
452 (bootcfg (operating-system-bootcfg os)))
453 (qemu-image #:os-drv os-drv
454 #:bootcfg-drv bootcfg
455 #:bootloader (bootloader-configuration-bootloader
456 (operating-system-bootloader os))
457 #:disk-image-size disk-image-size
458 #:file-system-type file-system-type
459 #:inputs `(("system" ,os-drv)
460 ("bootcfg" ,bootcfg))
461 #:copy-inputs? #t))))
465 ;;; VMs that share file systems with the host.
468 (define (file-system->mount-tag fs)
469 "Return a 9p mount tag for host file system FS."
470 ;; QEMU mount tags cannot contain slashes and cannot start with '_'.
471 ;; Compute an identifier that corresponds to the rules.
473 (string-map (match-lambda
478 (define (mapping->file-system mapping)
479 "Return a 9p file system that realizes MAPPING."
481 (($ <file-system-mapping> source target writable?)
484 (device (file-system->mount-tag source))
486 (flags (if writable? '() '(read-only)))
487 (options (string-append "trans=virtio"))
489 (create-mount-point? #t)))))
491 (define* (virtualized-operating-system os mappings #:optional (full-boot? #f))
492 "Return an operating system based on OS suitable for use in a virtualized
493 environment with the store shared with the host. MAPPINGS is a list of
494 <file-system-mapping> to realize in the virtualized OS."
495 (define user-file-systems
496 ;; Remove file systems that conflict with those added below, or that are
497 ;; normally bound to real devices.
499 (let ((target (file-system-mount-point fs))
500 (source (file-system-device fs)))
501 (or (string=? target (%store-prefix))
502 (string=? target "/")
503 (and (eq? 'device (file-system-title fs))
504 (string-prefix? "/dev/" source)))))
505 (operating-system-file-systems os)))
507 (define virtual-file-systems
513 (append (map mapping->file-system mappings)
516 (operating-system (inherit os)
517 (initrd (lambda (file-systems . rest)
518 (apply base-initrd file-systems
526 ;; XXX: When FULL-BOOT? is true, do not add a 9p mount for /gnu/store
527 ;; since that would lead the bootloader config to look for the kernel and
529 (file-systems (if full-boot?
533 (inherit (mapping->file-system %store-mapping))
534 (needed-for-boot? #t))
535 virtual-file-systems)))))
537 (define* (system-qemu-image/shared-store
541 (disk-image-size (* (if full-boot? 500 30) (expt 2 20))))
542 "Return a derivation that builds a QEMU image of OS that shares its store
545 When FULL-BOOT? is true, return an image that does a complete boot sequence,
546 bootloaded included; thus, make a disk image that contains everything the
547 bootloader refers to: OS kernel, initrd, bootloader data, etc."
548 (mlet* %store-monad ((os-drv (operating-system-derivation os))
549 (bootcfg (operating-system-bootcfg os)))
550 ;; XXX: When FULL-BOOT? is true, we end up creating an image that contains
551 ;; BOOTCFG and all its dependencies, including the output of OS-DRV.
552 ;; This is more than needed (we only need the kernel, initrd, GRUB for its
553 ;; font, and the background image), but it's hard to filter that.
554 (qemu-image #:os-drv os-drv
555 #:bootcfg-drv bootcfg
556 #:bootloader (bootloader-configuration-bootloader
557 (operating-system-bootloader os))
558 #:disk-image-size disk-image-size
559 #:inputs (if full-boot?
560 `(("bootcfg" ,bootcfg))
563 ;; XXX: Passing #t here is too slow, so let it off by default.
564 #:register-closures? #f
565 #:copy-inputs? full-boot?)))
567 (define* (common-qemu-options image shared-fs)
568 "Return the a string-value gexp with the common QEMU options to boot IMAGE,
569 with '-virtfs' options for the host file systems listed in SHARED-FS."
571 (define (virtfs-option fs)
572 #~(format #f "-virtfs local,path=~s,security_model=none,mount_tag=~s"
573 #$fs #$(file-system->mount-tag fs)))
575 #~(;; Only enable kvm if we see /dev/kvm exists.
576 ;; This allows users without hardware virtualization to still use these
578 #$@(if (file-exists? "/dev/kvm")
583 "-net nic,model=virtio"
585 #$@(map virtfs-option shared-fs)
587 (format #f "-drive file=~a,if=virtio,cache=writeback,werror=report,readonly"
590 (define* (system-qemu-image/shared-store-script os
598 (* (if full-boot? 500 70)
601 "Return a derivation that builds a script to run a virtual machine image of
602 OS that shares its store with the host. The virtual machine runs with
603 MEMORY-SIZE MiB of memory.
605 MAPPINGS is a list of <file-system-mapping> specifying mapping of host file
606 systems into the guest.
608 When FULL-BOOT? is true, the returned script runs everything starting from the
609 bootloader; otherwise it directly starts the operating system kernel. The
610 DISK-IMAGE-SIZE parameter specifies the size in bytes of the root disk image;
611 it is mostly useful when FULL-BOOT? is true."
612 (mlet* %store-monad ((os -> (virtualized-operating-system os mappings full-boot?))
613 (os-drv (operating-system-derivation os))
614 (image (system-qemu-image/shared-store
616 #:full-boot? full-boot?
617 #:disk-image-size disk-image-size)))
618 (define kernel-arguments
619 #~(list #$@(if graphic? #~() #~("console=ttyS0"))
620 #+@(operating-system-kernel-arguments os os-drv "/dev/vda1")))
623 #~(list (string-append #$qemu "/bin/" #$(qemu-command (%current-system)))
626 #~("-kernel" #$(operating-system-kernel-file os)
627 "-initrd" #$(file-append os-drv "/initrd")
628 (format #f "-append ~s"
629 (string-join #$kernel-arguments " "))))
630 #$@(common-qemu-options image
631 (map file-system-mapping-source
632 (cons %store-mapping mappings)))
633 "-m " (number->string #$memory-size)
637 #~(call-with-output-file #$output
639 (format port "#!~a~% exec ~a \"$@\"~%"
640 #$(file-append bash "/bin/sh")
641 (string-join #$qemu-exec " "))
642 (chmod port #o555))))
644 (gexp->derivation "run-vm.sh" builder)))
648 ;;; High-level abstraction.
651 (define-record-type* <virtual-machine> %virtual-machine
654 (operating-system virtual-machine-operating-system) ;<operating-system>
655 (qemu virtual-machine-qemu ;<package>
657 (graphic? virtual-machine-graphic? ;Boolean
659 (memory-size virtual-machine-memory-size ;integer (MiB)
661 (port-forwardings virtual-machine-port-forwardings ;list of integer pairs
664 (define-syntax virtual-machine
666 "Declare a virtual machine running the specified OS, with the given
669 (%virtual-machine (operating-system os)))
671 (%virtual-machine fields ...))))
673 (define (port-forwardings->qemu-options forwardings)
674 "Return the QEMU option for the given port FORWARDINGS as a string, where
675 FORWARDINGS is a list of host-port/guest-port pairs."
678 ((host-port . guest-port)
679 (string-append "hostfwd=tcp::"
680 (number->string host-port)
681 "-:" (number->string guest-port))))
685 (define-gexp-compiler (virtual-machine-compiler (vm <virtual-machine>)
687 ;; XXX: SYSTEM and TARGET are ignored.
689 (($ <virtual-machine> os qemu graphic? memory-size ())
690 (system-qemu-image/shared-store-script os
693 #:memory-size memory-size))
694 (($ <virtual-machine> os qemu graphic? memory-size forwardings)
696 `("-net" ,(string-append
698 (port-forwardings->qemu-options forwardings)))))
699 (system-qemu-image/shared-store-script os
702 #:memory-size memory-size
703 #:options options)))))