Merge branch 'master' into core-updates
[jackhill/guix/guix.git] / gnu / build / vm.scm
1 ;;; GNU Guix --- Functional package management for GNU
2 ;;; Copyright © 2013, 2014, 2015, 2016, 2017, 2018 Ludovic Courtès <ludo@gnu.org>
3 ;;; Copyright © 2016 Christopher Allan Webber <cwebber@dustycloud.org>
4 ;;; Copyright © 2016, 2017 Leo Famulari <leo@famulari.name>
5 ;;; Copyright © 2017 Mathieu Othacehe <m.othacehe@gmail.com>
6 ;;; Copyright © 2017 Marius Bakke <mbakke@fastmail.com>
7 ;;; Copyright © 2018 Chris Marusich <cmmarusich@gmail.com>
8 ;;;
9 ;;; This file is part of GNU Guix.
10 ;;;
11 ;;; GNU Guix is free software; you can redistribute it and/or modify it
12 ;;; under the terms of the GNU General Public License as published by
13 ;;; the Free Software Foundation; either version 3 of the License, or (at
14 ;;; your option) any later version.
15 ;;;
16 ;;; GNU Guix is distributed in the hope that it will be useful, but
17 ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
18 ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 ;;; GNU General Public License for more details.
20 ;;;
21 ;;; You should have received a copy of the GNU General Public License
22 ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
23
24 (define-module (gnu build vm)
25 #:use-module (guix build utils)
26 #:use-module (guix build store-copy)
27 #:use-module (guix build syscalls)
28 #:use-module ((guix store database) #:select (reset-timestamps))
29 #:use-module (gnu build linux-boot)
30 #:use-module (gnu build install)
31 #:use-module (gnu system uuid)
32 #:use-module (guix records)
33 #:use-module ((guix combinators) #:select (fold2))
34 #:use-module (ice-9 format)
35 #:use-module (ice-9 match)
36 #:use-module (ice-9 regex)
37 #:use-module (ice-9 popen)
38 #:use-module (srfi srfi-1)
39 #:use-module (srfi srfi-9)
40 #:use-module (srfi srfi-26)
41 #:export (qemu-command
42 load-in-linux-vm
43 format-partition
44
45 partition
46 partition?
47 partition-device
48 partition-size
49 partition-file-system
50 partition-label
51 partition-flags
52 partition-initializer
53
54 estimated-partition-size
55 root-partition-initializer
56 initialize-partition-table
57 initialize-hard-disk
58 make-iso9660-image))
59
60 ;;; Commentary:
61 ;;;
62 ;;; This module provides supporting code to run virtual machines and build
63 ;;; virtual machine images using QEMU.
64 ;;;
65 ;;; Code:
66
67 (define* (qemu-command #:optional (system %host-type))
68 "Return the default name of the QEMU command for SYSTEM."
69 (let ((cpu (substring system 0
70 (string-index system #\-))))
71 (string-append "qemu-system-"
72 (if (string-match "^i[3456]86$" cpu)
73 "i386"
74 cpu))))
75
76 (define* (load-in-linux-vm builder
77 #:key
78 output
79 (qemu (qemu-command)) (memory-size 512)
80 linux initrd
81 make-disk-image?
82 single-file-output?
83 target-arm32?
84 (disk-image-size (* 100 (expt 2 20)))
85 (disk-image-format "qcow2")
86 (references-graphs '()))
87 "Run BUILDER, a Scheme file, into a VM running LINUX with INITRD, and copy
88 the result to OUTPUT. If SINGLE-FILE-OUTPUT? is true, copy a single file from
89 /xchg to OUTPUT. Otherwise, copy the contents of /xchg to a new directory
90 OUTPUT.
91
92 When MAKE-DISK-IMAGE? is true, OUTPUT will contain a VM image of
93 DISK-IMAGE-SIZE bytes resulting from the execution of BUILDER, which may
94 access it via /dev/hda.
95
96 REFERENCES-GRAPHS can specify a list of reference-graph files as produced by
97 the #:references-graphs parameter of 'derivation'."
98
99 (define arch-specific-flags
100 `(;; On ARM, a machine has to be specified. Use "virt" machine to avoid
101 ;; hardware limits imposed by other machines.
102 ,@(if target-arm32? '("-M" "virt") '())
103
104 ;; Only enable kvm if we see /dev/kvm exists. This allows users without
105 ;; hardware virtualization to still use these commands. KVM support is
106 ;; still buggy on some ARM32 boards. Do not use it even if available.
107 ,@(if (and (file-exists? "/dev/kvm")
108 (not target-arm32?))
109 '("-enable-kvm")
110 '())
111
112 ;; Pass "panic=1" so that the guest dies upon error.
113 "-append"
114 ,(string-append "panic=1 --load=" builder
115
116 ;; The serial port name differs between emulated
117 ;; architectures/machines.
118 " console="
119 (if target-arm32? "ttyAMA0" "ttyS0"))
120
121 ;; NIC is not supported on ARM "virt" machine, so use a user mode
122 ;; network stack instead.
123 ,@(if target-arm32?
124 '("-device" "virtio-net-pci,netdev=mynet"
125 "-netdev" "user,id=mynet")
126 '("-net" "nic,model=virtio"))))
127
128 (when make-disk-image?
129 (format #t "creating ~a image of ~,2f MiB...~%"
130 disk-image-format (/ disk-image-size (expt 2 20)))
131 (force-output)
132 (invoke "qemu-img" "create" "-f" disk-image-format output
133 (number->string disk-image-size)))
134
135 (mkdir "xchg")
136 (mkdir "tmp")
137
138 (match references-graphs
139 ((graph-files ...)
140 ;; Copy the reference-graph files under xchg/ so EXP can access it.
141 (map (lambda (file)
142 (copy-file file (string-append "xchg/" file)))
143 graph-files))
144 (_ #f))
145
146 (apply invoke qemu "-nographic" "-no-reboot"
147 "-m" (number->string memory-size)
148 "-object" "rng-random,filename=/dev/urandom,id=guixsd-vm-rng"
149 "-device" "virtio-rng-pci,rng=guixsd-vm-rng"
150 "-virtfs"
151 (string-append "local,id=store_dev,path="
152 (%store-directory)
153 ",security_model=none,mount_tag=store")
154 "-virtfs"
155 (string-append "local,id=xchg_dev,path=xchg"
156 ",security_model=none,mount_tag=xchg")
157 "-virtfs"
158 ;; Some programs require more space in /tmp than is normally
159 ;; available in the guest. Accommodate such programs by sharing a
160 ;; temporary directory.
161 (string-append "local,id=tmp_dev,path=tmp"
162 ",security_model=none,mount_tag=tmp")
163 "-kernel" linux
164 "-initrd" initrd
165 (append
166 (if make-disk-image?
167 `("-device" "virtio-blk,drive=myhd"
168 "-drive" ,(string-append "if=none,file=" output
169 ",format=" disk-image-format
170 ",id=myhd"))
171 '())
172 arch-specific-flags))
173
174 ;; When MAKE-DISK-IMAGE? is true, the image is in OUTPUT already.
175 (unless make-disk-image?
176 (if single-file-output?
177 (let ((graph? (lambda (name stat)
178 (member (basename name) references-graphs))))
179 (match (find-files "xchg" (negate graph?))
180 ((result)
181 (copy-file result output))
182 (x
183 (error "did not find a single result file" x))))
184 (begin
185 (mkdir output)
186 (copy-recursively "xchg" output)))))
187
188 \f
189 ;;;
190 ;;; Partitions.
191 ;;;
192
193 (define-record-type* <partition> partition make-partition
194 partition?
195 (device partition-device (default #f))
196 (size partition-size)
197 (file-system partition-file-system (default "ext4"))
198 (label partition-label (default #f))
199 (uuid partition-uuid (default #f))
200 (flags partition-flags (default '()))
201 (initializer partition-initializer (default (const #t))))
202
203 (define (estimated-partition-size graphs)
204 "Return the estimated size of a partition that can store the store items
205 given by GRAPHS, a list of file names produced by #:references-graphs."
206 ;; Simply add a 25% overhead.
207 (round (* 1.25 (closure-size graphs))))
208
209 (define* (initialize-partition-table device partitions
210 #:key
211 (label-type "msdos")
212 (offset (expt 2 20)))
213 "Create on DEVICE a partition table of type LABEL-TYPE, containing the given
214 PARTITIONS (a list of <partition> objects), starting at OFFSET bytes. On
215 success, return PARTITIONS with their 'device' field changed to reflect their
216 actual /dev name based on DEVICE."
217 (define (partition-options part offset index)
218 (cons* "mkpart" "primary" "ext2"
219 (format #f "~aB" offset)
220 (format #f "~aB" (+ offset (partition-size part)))
221 (append-map (lambda (flag)
222 (list "set" (number->string index)
223 (symbol->string flag) "on"))
224 (partition-flags part))))
225
226 (define (options partitions offset)
227 (let loop ((partitions partitions)
228 (offset offset)
229 (index 1)
230 (result '()))
231 (match partitions
232 (()
233 (concatenate (reverse result)))
234 ((head tail ...)
235 (loop tail
236 ;; Leave one sector (512B) between partitions to placate
237 ;; Parted.
238 (+ offset 512 (partition-size head))
239 (+ 1 index)
240 (cons (partition-options head offset index)
241 result))))))
242
243 (format #t "creating partition table with ~a partitions (~a)...\n"
244 (length partitions)
245 (string-join (map (compose (cut string-append <> " MiB")
246 number->string
247 (lambda (size)
248 (round (/ size (expt 2. 20))))
249 partition-size)
250 partitions)
251 ", "))
252 (apply invoke "parted" "--script"
253 device "mklabel" label-type
254 (options partitions offset))
255
256 ;; Set the 'device' field of each partition.
257 (reverse
258 (fold2 (lambda (part result index)
259 (values (cons (partition
260 (inherit part)
261 (device (string-append device
262 (number->string index))))
263 result)
264 (+ 1 index)))
265 '()
266 1
267 partitions)))
268
269 (define MS_BIND 4096) ; <sys/mounts.h> again!
270
271 (define* (create-ext-file-system partition type
272 #:key label uuid)
273 "Create an ext-family file system of TYPE on PARTITION. If LABEL is true,
274 use that as the volume name. If UUID is true, use it as the partition UUID."
275 (format #t "creating ~a partition... ~@[label: ~s~] ~@[uuid: ~s~]\n"
276 type label (and uuid (uuid->string uuid)))
277 (apply invoke (string-append "mkfs." type)
278 "-F" partition
279 `(,@(if label
280 `("-L" ,label)
281 '())
282 ,@(if uuid
283 `("-U" ,(uuid->string uuid))
284 '()))))
285
286 (define* (create-fat-file-system partition
287 #:key label uuid)
288 "Create a FAT file system on PARTITION. The number of File Allocation Tables
289 will be determined based on file system size. If LABEL is true, use that as the
290 volume name."
291 ;; FIXME: UUID is ignored!
292 (format #t "creating FAT partition...\n")
293 (apply invoke "mkfs.fat" partition
294 (if label `("-n" ,label) '())))
295
296 (define* (format-partition partition type
297 #:key label uuid)
298 "Create a file system TYPE on PARTITION. If LABEL is true, use that as the
299 volume name."
300 (cond ((string-prefix? "ext" type)
301 (create-ext-file-system partition type #:label label #:uuid uuid))
302 ((or (string-prefix? "fat" type) (string= "vfat" type))
303 (create-fat-file-system partition #:label label #:uuid uuid))
304 (else (error "Unsupported file system."))))
305
306 (define (initialize-partition partition)
307 "Format PARTITION, a <partition> object with a non-#f 'device' field, mount
308 it, run its initializer, and unmount it."
309 (let ((target "/fs"))
310 (format-partition (partition-device partition)
311 (partition-file-system partition)
312 #:label (partition-label partition)
313 #:uuid (partition-uuid partition))
314 (mkdir-p target)
315 (mount (partition-device partition) target
316 (partition-file-system partition))
317
318 ((partition-initializer partition) target)
319
320 (umount target)
321 partition))
322
323 (define* (root-partition-initializer #:key (closures '())
324 copy-closures?
325 (register-closures? #t)
326 system-directory
327 (deduplicate? #t))
328 "Return a procedure to initialize a root partition.
329
330 If REGISTER-CLOSURES? is true, register all of CLOSURES in the partition's
331 store. If DEDUPLICATE? is true, then also deduplicate files common to
332 CLOSURES and the rest of the store when registering the closures. If
333 COPY-CLOSURES? is true, copy all of CLOSURES to the partition.
334 SYSTEM-DIRECTORY is the name of the directory of the 'system' derivation."
335 (lambda (target)
336 (define target-store
337 (string-append target (%store-directory)))
338
339 (when copy-closures?
340 ;; Populate the store.
341 (populate-store (map (cut string-append "/xchg/" <>) closures)
342 target))
343
344 ;; Populate /dev.
345 (make-essential-device-nodes #:root target)
346
347 ;; Optionally, register the inputs in the image's store.
348 (when register-closures?
349 (unless copy-closures?
350 ;; XXX: 'register-closure' wants to palpate the things it registers, so
351 ;; bind-mount the store on the target.
352 (mkdir-p target-store)
353 (mount (%store-directory) target-store "" MS_BIND))
354
355 (display "registering closures...\n")
356 (for-each (lambda (closure)
357 (register-closure target
358 (string-append "/xchg/" closure)
359 #:reset-timestamps? copy-closures?
360 #:deduplicate? deduplicate?))
361 closures)
362 (unless copy-closures?
363 (umount target-store)))
364
365 ;; Add the non-store directories and files.
366 (display "populating...\n")
367 (populate-root-file-system system-directory target)
368
369 ;; 'register-closure' resets timestamps and everything, so no need to do it
370 ;; once more in that case.
371 (unless register-closures?
372 (reset-timestamps target))))
373
374 (define (register-bootcfg-root target bootcfg)
375 "On file system TARGET, register BOOTCFG as a GC root."
376 (let ((directory (string-append target "/var/guix/gcroots")))
377 (mkdir-p directory)
378 (symlink bootcfg (string-append directory "/bootcfg"))))
379
380 (define (install-efi grub esp config-file)
381 "Write a self-contained GRUB EFI loader to the mounted ESP using CONFIG-FILE."
382 (let* ((system %host-type)
383 ;; Hard code the output location to a well-known path recognized by
384 ;; compliant firmware. See "3.5.1.1 Removable Media Boot Behaviour":
385 ;; http://www.uefi.org/sites/default/files/resources/UEFI%20Spec%202_6.pdf
386 (grub-mkstandalone (string-append grub "/bin/grub-mkstandalone"))
387 (efi-directory (string-append esp "/EFI/BOOT"))
388 ;; Map grub target names to boot file names.
389 (efi-targets (cond ((string-prefix? "x86_64" system)
390 '("x86_64-efi" . "BOOTX64.EFI"))
391 ((string-prefix? "i686" system)
392 '("i386-efi" . "BOOTIA32.EFI"))
393 ((string-prefix? "armhf" system)
394 '("arm-efi" . "BOOTARM.EFI"))
395 ((string-prefix? "aarch64" system)
396 '("arm64-efi" . "BOOTAA64.EFI")))))
397 ;; grub-mkstandalone requires a TMPDIR to prepare the firmware image.
398 (setenv "TMPDIR" esp)
399
400 (mkdir-p efi-directory)
401 (invoke grub-mkstandalone "-O" (car efi-targets)
402 "-o" (string-append efi-directory "/"
403 (cdr efi-targets))
404 ;; Graft the configuration file onto the image.
405 (string-append "boot/grub/grub.cfg=" config-file))))
406
407 (define* (make-iso9660-image grub config-file os-drv target
408 #:key (volume-id "GuixSD_image") (volume-uuid #f)
409 register-closures? (closures '()))
410 "Given a GRUB package, creates an iso image as TARGET, using CONFIG-FILE as
411 GRUB configuration and OS-DRV as the stuff in it."
412 (define grub-mkrescue
413 (string-append grub "/bin/grub-mkrescue"))
414
415 (define target-store
416 (string-append "/tmp/root" (%store-directory)))
417
418 (define items
419 ;; The store items to add to the image.
420 (delete-duplicates
421 (append-map (lambda (closure)
422 (map store-info-item
423 (call-with-input-file (string-append "/xchg/" closure)
424 read-reference-graph)))
425 closures)))
426
427 (populate-root-file-system os-drv "/tmp/root")
428 (mount (%store-directory) target-store "" MS_BIND)
429
430 (when register-closures?
431 (display "registering closures...\n")
432 (for-each (lambda (closure)
433 (register-closure
434 "/tmp/root"
435 (string-append "/xchg/" closure)
436
437 ;; TARGET-STORE is a read-only bind-mount so we shouldn't try
438 ;; to modify it.
439 #:deduplicate? #f
440 #:reset-timestamps? #f))
441 closures)
442 (register-bootcfg-root "/tmp/root" config-file))
443
444 (let ((pipe
445 (apply open-pipe* OPEN_WRITE
446 grub-mkrescue "-o" target
447 (string-append "boot/grub/grub.cfg=" config-file)
448 "etc=/tmp/root/etc"
449 "var=/tmp/root/var"
450 "run=/tmp/root/run"
451 ;; /mnt is used as part of the installation
452 ;; process, as the mount point for the target
453 ;; file system, so create it.
454 "mnt=/tmp/root/mnt"
455 "-path-list" "-"
456 "--"
457 "-volid" (string-upcase volume-id)
458 (if volume-uuid
459 `("-volume_date" "uuid"
460 ,(string-filter (lambda (value)
461 (not (char=? #\- value)))
462 (iso9660-uuid->string
463 volume-uuid)))
464 `()))))
465 ;; Pass lines like 'gnu/store/…-x=/gnu/store/…-x' corresponding to the
466 ;; '-path-list -' option.
467 (for-each (lambda (item)
468 (format pipe "~a=~a~%"
469 (string-drop item 1) item))
470 items)
471 (unless (zero? (close-pipe pipe))
472 (error "oh, my! grub-mkrescue failed" grub-mkrescue))))
473
474 (define* (initialize-hard-disk device
475 #:key
476 bootloader-package
477 bootcfg
478 bootcfg-location
479 bootloader-installer
480 (grub-efi #f)
481 (partitions '()))
482 "Initialize DEVICE as a disk containing all the <partition> objects listed
483 in PARTITIONS, and using BOOTCFG as its bootloader configuration file.
484
485 Each partition is initialized by calling its 'initializer' procedure,
486 passing it a directory name where it is mounted."
487
488 (define (partition-bootable? partition)
489 "Return the first partition found with the boot flag set."
490 (member 'boot (partition-flags partition)))
491
492 (define (partition-esp? partition)
493 "Return the first EFI System Partition."
494 (member 'esp (partition-flags partition)))
495
496 (let* ((partitions (initialize-partition-table device partitions))
497 (root (find partition-bootable? partitions))
498 (esp (find partition-esp? partitions))
499 (target "/fs"))
500 (unless root
501 (error "no bootable partition specified" partitions))
502
503 (for-each initialize-partition partitions)
504
505 (display "mounting root partition...\n")
506 (mkdir-p target)
507 (mount (partition-device root) target (partition-file-system root))
508 (install-boot-config bootcfg bootcfg-location target)
509 (when bootloader-installer
510 (display "installing bootloader...\n")
511 (bootloader-installer bootloader-package device target))
512
513 (when esp
514 ;; Mount the ESP somewhere and install GRUB UEFI image.
515 (let ((mount-point (string-append target "/boot/efi"))
516 (grub-config (string-append target "/tmp/grub-standalone.cfg")))
517 (display "mounting EFI system partition...\n")
518 (mkdir-p mount-point)
519 (mount (partition-device esp) mount-point
520 (partition-file-system esp))
521
522 ;; Create a tiny configuration file telling the embedded grub
523 ;; where to load the real thing.
524 ;; XXX This is quite fragile, and can prevent the image from booting
525 ;; when there's more than one volume with this label present.
526 ;; Reproducible almost-UUIDs could reduce the risk (not eliminate it).
527 (call-with-output-file grub-config
528 (lambda (port)
529 (format port
530 "insmod part_msdos~@
531 search --set=root --label GuixSD_image~@
532 configfile /boot/grub/grub.cfg~%")))
533
534 (display "creating EFI firmware image...")
535 (install-efi grub-efi mount-point grub-config)
536 (display "done.\n")
537
538 (delete-file grub-config)
539 (umount mount-point)))
540
541 ;; Register BOOTCFG as a GC root.
542 (register-bootcfg-root target bootcfg)
543
544 (umount target)))
545
546 ;;; vm.scm ends here