Genode Packages collection https://git.sr.ht/~ehmry/genodepkgs/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

qemu-vm.nix 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558
  1. { config, lib, pkgs, ... }:
  2. with lib;
  3. with import ../tests/lib/qemu-flags.nix { inherit pkgs; };
  4. let
  5. qemu = config.system.build.qemu;
  6. cfg = config.virtualisation;
  7. consoles = lib.concatMapStringsSep " " (c: "console=${c}") cfg.qemu.consoles;
  8. driveOpts = { ... }: {
  9. options = {
  10. file = mkOption {
  11. type = types.str;
  12. description = "The file image used for this drive.";
  13. };
  14. driveExtraOpts = mkOption {
  15. type = types.attrsOf types.str;
  16. default = { };
  17. description = "Extra options passed to drive flag.";
  18. };
  19. deviceExtraOpts = mkOption {
  20. type = types.attrsOf types.str;
  21. default = { };
  22. description = "Extra options passed to device flag.";
  23. };
  24. name = mkOption {
  25. type = types.nullOr types.str;
  26. default = null;
  27. description =
  28. "A name for the drive. Must be unique in the drives list. Not passed to qemu.";
  29. };
  30. };
  31. };
  32. driveCmdline = idx:
  33. { file, driveExtraOpts, deviceExtraOpts, ... }:
  34. let
  35. drvId = "drive${toString idx}";
  36. mkKeyValue = generators.mkKeyValueDefault { } "=";
  37. mkOpts = opts: concatStringsSep "," (mapAttrsToList mkKeyValue opts);
  38. driveOpts = mkOpts (driveExtraOpts // {
  39. index = idx;
  40. id = drvId;
  41. "if" = "none";
  42. inherit file;
  43. });
  44. deviceOpts = mkOpts (deviceExtraOpts // { drive = drvId; });
  45. device = if cfg.qemu.diskInterface == "scsi" then
  46. "-device lsi53c895a -device scsi-hd,${deviceOpts}"
  47. else
  48. "-device virtio-blk-pci,${deviceOpts}";
  49. in "-drive ${driveOpts} ${device}";
  50. drivesCmdLine = drives: concatStringsSep " " (imap1 driveCmdline drives);
  51. # Creates a device name from a 1-based a numerical index, e.g.
  52. # * `driveDeviceName 1` -> `/dev/vda`
  53. # * `driveDeviceName 2` -> `/dev/vdb`
  54. driveDeviceName = idx:
  55. let letter = elemAt lowerChars (idx - 1);
  56. in if cfg.qemu.diskInterface == "scsi" then
  57. "/dev/sd${letter}"
  58. else
  59. "/dev/vd${letter}";
  60. lookupDriveDeviceName = driveName: driveList:
  61. (findSingle (drive: drive.name == driveName)
  62. (throw "Drive ${driveName} not found")
  63. (throw "Multiple drives named ${driveName}") driveList).device;
  64. addDeviceNames =
  65. imap1 (idx: drive: drive // { device = driveDeviceName idx; });
  66. efiPrefix = if (pkgs.stdenv.isi686 || pkgs.stdenv.isx86_64) then
  67. "${pkgs.buildPackages.OVMF.fd}/FV/OVMF"
  68. else if pkgs.stdenv.isAarch64 then
  69. "${pkgs.buildPackages.OVMF.fd}/FV/AAVMF"
  70. else
  71. throw "No EFI firmware available for platform";
  72. efiFirmware = "${efiPrefix}_CODE.fd";
  73. efiVarsDefault = "${efiPrefix}_VARS.fd";
  74. # Shell script to start the VM.
  75. startVM = ''
  76. #! ${pkgs.buildPackages.runtimeShell}
  77. NIX_DISK_IMAGE=$(readlink -f ''${NIX_DISK_IMAGE:-${config.virtualisation.diskImage}})
  78. if ! test -e "$NIX_DISK_IMAGE"; then
  79. ${qemu}/bin/qemu-img create -f qcow2 "$NIX_DISK_IMAGE" \
  80. ${toString config.virtualisation.diskSize}M || exit 1
  81. fi
  82. # Create a directory for storing temporary data of the running VM.
  83. if [ -z "$TMPDIR" -o -z "$USE_TMPDIR" ]; then
  84. TMPDIR=$(mktemp -d nix-vm.XXXXXXXXXX --tmpdir)
  85. fi
  86. # Create a directory for exchanging data with the VM.
  87. mkdir -p $TMPDIR/xchg
  88. ${if cfg.useBootLoader then ''
  89. # Create a writable copy/snapshot of the boot disk.
  90. # A writable boot disk can be booted from automatically.
  91. ${qemu}/bin/qemu-img create -f qcow2 -b ${bootDisk}/disk.img $TMPDIR/disk.img || exit 1
  92. NIX_EFI_VARS=$(readlink -f ''${NIX_EFI_VARS:-${cfg.efiVars}})
  93. ${if cfg.useEFIBoot then ''
  94. # VM needs writable EFI vars
  95. if ! test -e "$NIX_EFI_VARS"; then
  96. cp ${bootDisk}/efi-vars.fd "$NIX_EFI_VARS" || exit 1
  97. chmod 0644 "$NIX_EFI_VARS" || exit 1
  98. fi
  99. '' else
  100. ""}
  101. '' else
  102. ""}
  103. cd $TMPDIR
  104. idx=0
  105. ${flip concatMapStrings cfg.emptyDiskImages (size: ''
  106. if ! test -e "empty$idx.qcow2"; then
  107. ${qemu}/bin/qemu-img create -f qcow2 "empty$idx.qcow2" "${
  108. toString size
  109. }M"
  110. fi
  111. idx=$((idx + 1))
  112. '')}
  113. # Start QEMU.
  114. exec ${qemuBinary qemu} \
  115. -name ${config.system.name} \
  116. -m ${toString config.virtualisation.memorySize} \
  117. -smp ${toString config.virtualisation.cores} \
  118. -device virtio-rng-pci \
  119. ${concatStringsSep " " config.virtualisation.qemu.networkingOptions} \
  120. -virtfs local,path=/nix/store,security_model=none,mount_tag=store \
  121. -virtfs local,path=$TMPDIR/xchg,security_model=none,mount_tag=xchg \
  122. -virtfs local,path=''${SHARED_DIR:-$TMPDIR/xchg},security_model=none,mount_tag=shared \
  123. ${drivesCmdLine config.virtualisation.qemu.drives} \
  124. ${toString config.virtualisation.qemu.options} \
  125. $QEMU_OPTS \
  126. "$@"
  127. '';
  128. regInfo =
  129. pkgs.closureInfo { rootPaths = config.virtualisation.pathsInNixDB; };
  130. # Generate a hard disk image containing a /boot partition and GRUB
  131. # in the MBR. Used when the `useBootLoader' option is set.
  132. # Uses `runInLinuxVM` to create the image in a throwaway VM.
  133. # See note [Disk layout with `useBootLoader`].
  134. # FIXME: use nixos/lib/make-disk-image.nix.
  135. bootDisk = pkgs.vmTools.runInLinuxVM (pkgs.runCommand "nixos-boot-disk" {
  136. preVM = ''
  137. mkdir $out
  138. diskImage=$out/disk.img
  139. ${qemu}/bin/qemu-img create -f qcow2 $diskImage "60M"
  140. ${if cfg.useEFIBoot then ''
  141. efiVars=$out/efi-vars.fd
  142. cp ${efiVarsDefault} $efiVars
  143. chmod 0644 $efiVars
  144. '' else
  145. ""}
  146. '';
  147. buildInputs = [ pkgs.utillinux ];
  148. QEMU_OPTS = "-nographic -serial stdio -monitor none"
  149. + lib.optionalString cfg.useEFIBoot
  150. (" -drive if=pflash,format=raw,unit=0,readonly=on,file=${efiFirmware}"
  151. + " -drive if=pflash,format=raw,unit=1,file=$efiVars");
  152. } ''
  153. # Create a /boot EFI partition with 60M and arbitrary but fixed GUIDs for reproducibility
  154. ${pkgs.gptfdisk}/bin/sgdisk \
  155. --set-alignment=1 --new=1:34:2047 --change-name=1:BIOSBootPartition --typecode=1:ef02 \
  156. --set-alignment=512 --largest-new=2 --change-name=2:EFISystem --typecode=2:ef00 \
  157. --attributes=1:set:1 \
  158. --attributes=2:set:2 \
  159. --disk-guid=97FD5997-D90B-4AA3-8D16-C1723AEA73C1 \
  160. --partition-guid=1:1C06F03B-704E-4657-B9CD-681A087A2FDC \
  161. --partition-guid=2:970C694F-AFD0-4B99-B750-CDB7A329AB6F \
  162. --hybrid 2 \
  163. --recompute-chs /dev/vda
  164. ${optionalString (config.boot.loader.grub.device != "/dev/vda")
  165. # In this throwaway VM, we only have the /dev/vda disk, but the
  166. # actual VM described by `config` (used by `switch-to-configuration`
  167. # below) may set `boot.loader.grub.device` to a different device
  168. # that's nonexistent in the throwaway VM.
  169. # Create a symlink for that device, so that the `grub-install`
  170. # by `switch-to-configuration` will hit /dev/vda anyway.
  171. ''
  172. ln -s /dev/vda ${config.boot.loader.grub.device}
  173. ''}
  174. ${pkgs.dosfstools}/bin/mkfs.fat -F16 /dev/vda2
  175. export MTOOLS_SKIP_CHECK=1
  176. ${pkgs.mtools}/bin/mlabel -i /dev/vda2 ::boot
  177. mkdir /boot
  178. mount /dev/vda2 /boot
  179. ${optionalString config.boot.loader.efi.canTouchEfiVariables ''
  180. mount -t efivarfs efivarfs /sys/firmware/efi/efivars
  181. ''}
  182. # This is needed for GRUB 0.97, which doesn't know about virtio devices.
  183. mkdir /boot/grub
  184. echo '(hd0) /dev/vda' > /boot/grub/device.map
  185. # This is needed for systemd-boot to find ESP, and udev is not available here to create this
  186. mkdir -p /dev/block
  187. ln -s /dev/vda2 /dev/block/254:2
  188. # Install bootloader
  189. touch /etc/NIXOS
  190. export NIXOS_INSTALL_BOOTLOADER=1
  191. ${config.system.build.toplevel}/bin/switch-to-configuration boot
  192. umount /boot
  193. '' # */
  194. );
  195. in {
  196. options = {
  197. virtualisation.memorySize = mkOption {
  198. default = 384;
  199. description = ''
  200. Memory size (M) of virtual machine.
  201. '';
  202. };
  203. virtualisation.diskSize = mkOption {
  204. default = 512;
  205. description = ''
  206. Disk size (M) of virtual machine.
  207. '';
  208. };
  209. virtualisation.diskImage = mkOption {
  210. default = "./${config.system.name}.qcow2";
  211. description = ''
  212. Path to the disk image containing the root filesystem.
  213. The image will be created on startup if it does not
  214. exist.
  215. '';
  216. };
  217. virtualisation.bootDevice = mkOption {
  218. type = types.str;
  219. example = "/dev/vda";
  220. description = ''
  221. The disk to be used for the root filesystem.
  222. '';
  223. };
  224. virtualisation.emptyDiskImages = mkOption {
  225. default = [ ];
  226. type = types.listOf types.int;
  227. description = ''
  228. Additional disk images to provide to the VM. The value is
  229. a list of size in megabytes of each disk. These disks are
  230. writeable by the VM.
  231. '';
  232. };
  233. virtualisation.graphics = mkOption {
  234. default = true;
  235. description = ''
  236. Whether to run QEMU with a graphics window, or in nographic mode.
  237. Serial console will be enabled on both settings, but this will
  238. change the preferred console.
  239. '';
  240. };
  241. virtualisation.cores = mkOption {
  242. default = 1;
  243. type = types.int;
  244. description = ''
  245. Specify the number of cores the guest is permitted to use.
  246. The number can be higher than the available cores on the
  247. host system.
  248. '';
  249. };
  250. virtualisation.pathsInNixDB = mkOption {
  251. default = [ ];
  252. description = ''
  253. The list of paths whose closure is registered in the Nix
  254. database in the VM. All other paths in the host Nix store
  255. appear in the guest Nix store as well, but are considered
  256. garbage (because they are not registered in the Nix
  257. database in the guest).
  258. '';
  259. };
  260. virtualisation.vlans = mkOption {
  261. default = [ 1 ];
  262. example = [ 1 2 ];
  263. description = ''
  264. Virtual networks to which the VM is connected. Each
  265. number <replaceable>N</replaceable> in this list causes
  266. the VM to have a virtual Ethernet interface attached to a
  267. separate virtual network on which it will be assigned IP
  268. address
  269. <literal>192.168.<replaceable>N</replaceable>.<replaceable>M</replaceable></literal>,
  270. where <replaceable>M</replaceable> is the index of this VM
  271. in the list of VMs.
  272. '';
  273. };
  274. virtualisation.writableStore = mkOption {
  275. default = true; # FIXME
  276. description = ''
  277. If enabled, the Nix store in the VM is made writable by
  278. layering an overlay filesystem on top of the host's Nix
  279. store.
  280. '';
  281. };
  282. virtualisation.writableStoreUseTmpfs = mkOption {
  283. default = true;
  284. description = ''
  285. Use a tmpfs for the writable store instead of writing to the VM's
  286. own filesystem.
  287. '';
  288. };
  289. networking.primaryIPAddress = mkOption {
  290. default = "";
  291. internal = true;
  292. description = "Primary IP address used in /etc/hosts.";
  293. };
  294. virtualisation.qemu = {
  295. options = mkOption {
  296. type = types.listOf types.unspecified;
  297. default = [ ];
  298. example = [ "-vga std" ];
  299. description = "Options passed to QEMU.";
  300. };
  301. consoles = mkOption {
  302. type = types.listOf types.str;
  303. default = let consoles = [ "${qemuSerialDevice},115200n8" "tty0" ];
  304. in if cfg.graphics then consoles else reverseList consoles;
  305. example = [ "console=tty1" ];
  306. description = ''
  307. The output console devices to pass to the kernel command line via the
  308. <literal>console</literal> parameter, the primary console is the last
  309. item of this list.
  310. By default it enables both serial console and
  311. <literal>tty0</literal>. The preferred console (last one) is based on
  312. the value of <option>virtualisation.graphics</option>.
  313. '';
  314. };
  315. networkingOptions = mkOption {
  316. default = [
  317. "-net nic,netdev=user.0,model=virtio"
  318. "-netdev user,id=user.0\${QEMU_NET_OPTS:+,$QEMU_NET_OPTS}"
  319. ];
  320. type = types.listOf types.str;
  321. description = ''
  322. Networking-related command-line options that should be passed to qemu.
  323. The default is to use userspace networking (slirp).
  324. If you override this option, be advised to keep
  325. ''${QEMU_NET_OPTS:+,$QEMU_NET_OPTS} (as seen in the default)
  326. to keep the default runtime behaviour.
  327. '';
  328. };
  329. drives = mkOption {
  330. type = types.listOf (types.submodule driveOpts);
  331. description = "Drives passed to qemu.";
  332. apply = addDeviceNames;
  333. };
  334. diskInterface = mkOption {
  335. default = "virtio";
  336. example = "scsi";
  337. type = types.enum [ "virtio" "scsi" "ide" ];
  338. description = "The interface used for the virtual hard disks.";
  339. };
  340. guestAgent.enable = mkOption {
  341. default = true;
  342. type = types.bool;
  343. description = ''
  344. Enable the Qemu guest agent.
  345. '';
  346. };
  347. };
  348. virtualisation.useBootLoader = mkOption {
  349. default = false;
  350. description = ''
  351. If enabled, the virtual machine will be booted using the
  352. regular boot loader (i.e., GRUB 1 or 2). This allows
  353. testing of the boot loader. If
  354. disabled (the default), the VM directly boots the NixOS
  355. kernel and initial ramdisk, bypassing the boot loader
  356. altogether.
  357. '';
  358. };
  359. virtualisation.useEFIBoot = mkOption {
  360. default = false;
  361. description = ''
  362. If enabled, the virtual machine will provide a EFI boot
  363. manager.
  364. useEFIBoot is ignored if useBootLoader == false.
  365. '';
  366. };
  367. virtualisation.efiVars = mkOption {
  368. default = "./${config.system.name}-efi-vars.fd";
  369. description = ''
  370. Path to nvram image containing UEFI variables. The will be created
  371. on startup if it does not exist.
  372. '';
  373. };
  374. virtualisation.bios = mkOption {
  375. default = null;
  376. type = types.nullOr types.package;
  377. description = ''
  378. An alternate BIOS (such as <package>qboot</package>) with which to start the VM.
  379. Should contain a file named <literal>bios.bin</literal>.
  380. If <literal>null</literal>, QEMU's builtin SeaBIOS will be used.
  381. '';
  382. };
  383. };
  384. config = {
  385. # Note [Disk layout with `useBootLoader`]
  386. #
  387. # If `useBootLoader = true`, we configure 2 drives:
  388. # `/dev/?da` for the root disk, and `/dev/?db` for the boot disk
  389. # which has the `/boot` partition and the boot loader.
  390. # Concretely:
  391. #
  392. # * The second drive's image `disk.img` is created in `bootDisk = ...`
  393. # using a throwaway VM. Note that there the disk is always `/dev/vda`,
  394. # even though in the final VM it will be at `/dev/*b`.
  395. # * The disks are attached in `virtualisation.qemu.drives`.
  396. # Their order makes them appear as devices `a`, `b`, etc.
  397. # * `fileSystems."/boot"` is adjusted to be on device `b`.
  398. # If `useBootLoader`, GRUB goes to the second disk, see
  399. # note [Disk layout with `useBootLoader`].
  400. boot.loader.grub.device = mkVMOverride (if cfg.useBootLoader then
  401. driveDeviceName 2 # second disk
  402. else
  403. cfg.bootDevice);
  404. boot.initrd.extraUtilsCommands = ''
  405. # We need mke2fs in the initrd.
  406. copy_bin_and_libs ${pkgs.e2fsprogs}/bin/mke2fs
  407. '';
  408. boot.initrd.postDeviceCommands = ''
  409. # If the disk image appears to be empty, run mke2fs to
  410. # initialise.
  411. FSTYPE=$(blkid -o value -s TYPE ${cfg.bootDevice} || true)
  412. if test -z "$FSTYPE"; then
  413. mke2fs -t ext4 ${cfg.bootDevice}
  414. fi
  415. '';
  416. boot.initrd.postMountCommands = ''
  417. # Mark this as a NixOS machine.
  418. mkdir -p $targetRoot/etc
  419. echo -n > $targetRoot/etc/NIXOS
  420. # Fix the permissions on /tmp.
  421. chmod 1777 $targetRoot/tmp
  422. mkdir -p $targetRoot/boot
  423. ${optionalString cfg.writableStore ''
  424. echo "mounting overlay filesystem on /nix/store..."
  425. mkdir -p 0755 $targetRoot/nix/.rw-store/store $targetRoot/nix/.rw-store/work $targetRoot/nix/store
  426. mount -t overlay overlay $targetRoot/nix/store \
  427. -o lowerdir=$targetRoot/nix/.ro-store,upperdir=$targetRoot/nix/.rw-store/store,workdir=$targetRoot/nix/.rw-store/work || fail
  428. ''}
  429. '';
  430. virtualisation.bootDevice = mkDefault (driveDeviceName 1);
  431. # FIXME: Consolidate this one day.
  432. virtualisation.qemu.options = mkMerge [
  433. (mkIf (pkgs.stdenv.isi686 || pkgs.stdenv.isx86_64) [
  434. "-usb"
  435. "-device usb-tablet,bus=usb-bus.0"
  436. ])
  437. (mkIf (pkgs.stdenv.isAarch32 || pkgs.stdenv.isAarch64) [
  438. "-device virtio-gpu-pci"
  439. "-device usb-ehci,id=usb0"
  440. "-device usb-kbd"
  441. "-device usb-tablet"
  442. ])
  443. (mkIf cfg.useEFIBoot [
  444. "-drive if=pflash,format=raw,unit=0,readonly,file=${efiFirmware}"
  445. "-drive if=pflash,format=raw,unit=1,file=$NIX_EFI_VARS"
  446. ])
  447. (mkIf (cfg.bios != null) [ "-bios ${cfg.bios}/bios.bin" ])
  448. (mkIf (!cfg.graphics) [ "-nographic" ])
  449. ];
  450. virtualisation.qemu.drives = mkMerge [
  451. [{
  452. name = "root";
  453. file = "$NIX_DISK_IMAGE";
  454. driveExtraOpts.cache = "writeback";
  455. driveExtraOpts.werror = "report";
  456. }]
  457. (mkIf cfg.useBootLoader [
  458. # The order of this list determines the device names, see
  459. # note [Disk layout with `useBootLoader`].
  460. {
  461. name = "boot";
  462. file = "$TMPDIR/disk.img";
  463. driveExtraOpts.media = "disk";
  464. deviceExtraOpts.bootindex = "1";
  465. }
  466. ])
  467. (imap0 (idx: _: {
  468. file = "$(pwd)/empty${toString idx}.qcow2";
  469. driveExtraOpts.werror = "report";
  470. }) cfg.emptyDiskImages)
  471. ];
  472. system.build.vm = pkgs.runCommand "nixos-vm" { preferLocalBuild = true; } ''
  473. mkdir -p $out/bin
  474. ln -s ${
  475. pkgs.writeScript "run-nixos-vm" startVM
  476. } $out/bin/run-${config.system.name}-vm
  477. '';
  478. };
  479. }