Add VM configuration options and migrate to --block

- Rename `disks` to `additionalDisks` with structured format
  (path, readOnly, enableDiscard, blockSize, devIdentifier, useDirect)
- Add custom boot options: rootDisk, kernel, initramfs, rootDiskReadonly
- Add kernelParams for extra kernel command line options
- Add gpu option (default: "context-types=cross-domain:virgl2")
- Add sharedDirectories for crosvm --shared-dir
- Add global crosvmLogLevel option (default: "info")
- Add --name argument to crosvm set to VM name
- Migrate deprecated --disk/--rwdisk to --block format
- Switch flake to nixos-unstable channel

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Davíð Steinn Geirsson 2026-02-04 19:09:15 +00:00
parent 5e32c13b03
commit eb4e2ffcaa
5 changed files with 194 additions and 20 deletions

View file

@ -109,6 +109,7 @@ qubes-lite.lib.makeRootfsNixos "x86_64-linux" {
programs.qubes-lite = {
enable = true;
user = "david";
crosvmLogLevel = "info"; # error, warn, info, debug, trace
guestPrograms = [ pkgs.firefox pkgs.chromium ];
nixosVms = [{
@ -117,6 +118,31 @@ qubes-lite.lib.makeRootfsNixos "x86_64-linux" {
memory = 4096;
disposable = true; # Auto-shutdown when idle
idleTimeout = 120; # Shutdown after 2 minutes idle
# Disk configuration (uses crosvm --block)
additionalDisks = [{
path = "/tmp/data.qcow2";
readOnly = false; # default false
enableDiscard = true; # default true
blockSize = 4096; # default 512
devIdentifier = "data"; # optional device ID
useDirect = false; # default false (O_DIRECT)
}];
# Custom boot (optional - defaults to built rootfs)
# rootDisk = { path = "/path/to/root.qcow2"; };
# kernel = /path/to/bzImage;
# initramfs = /path/to/initrd;
rootDiskReadonly = true; # default true
# Extra kernel parameters
kernelParams = [ "debug" ];
# GPU config (crosvm --gpu=)
gpu = "context-types=cross-domain:virgl2"; # default
# Shared directories (crosvm --shared-dir)
sharedDirectories = [ "/tmp/shared:shared:uid=1000" ];
}];
};
# Access built package via config.programs.qubes-lite.package

14
flake.lock generated
View file

@ -43,16 +43,16 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1770136044,
"narHash": "sha256-tlFqNG/uzz2++aAmn4v8J0vAkV3z7XngeIIB3rM3650=",
"lastModified": 1770115704,
"narHash": "sha256-KHFT9UWOF2yRPlAnSXQJh6uVcgNcWlFqqiAZ7OVlHNc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e576e3c9cf9bad747afcddd9e34f51d18c855b4e",
"rev": "e6eae2ee2110f3d31110d5c222cd395303343b08",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-25.11",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
@ -87,11 +87,11 @@
]
},
"locked": {
"lastModified": 1769691507,
"narHash": "sha256-8aAYwyVzSSwIhP2glDhw/G0i5+wOrren3v6WmxkVonM=",
"lastModified": 1770228511,
"narHash": "sha256-wQ6NJSuFqAEmIg2VMnLdCnUc0b7vslUohqqGGD+Fyxk=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "28b19c5844cc6e2257801d43f2772a4b4c050a1b",
"rev": "337a4fe074be1042a35086f15481d763b8ddc0e7",
"type": "github"
},
"original": {

View file

@ -1,6 +1,6 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
wayland-proxy-virtwl = {
url = "git+https://git.dsg.is/davidlowsec/wayland-proxy-virtwl.git?submodules=1";
inputs.nixpkgs.follows = "nixpkgs";

View file

@ -46,16 +46,50 @@ let
guestConfig = lib.recursiveUpdate cfg.guestConfig vm.guestConfig;
};
# Format a disk configuration as --block argument
formatBlockArg =
disk:
let
parts = [
"path=${disk.path}"
"ro=${lib.boolToString disk.readOnly}"
"sparse=${lib.boolToString disk.enableDiscard}"
"block-size=${toString disk.blockSize}"
"direct=${lib.boolToString disk.useDirect}"
]
++ lib.optional (disk.devIdentifier != null) "id=${disk.devIdentifier}";
in
"--block ${lib.concatStringsSep "," parts}";
# Generate VM launcher script
mkVmScript =
vm:
let
rootfs = buildRootfs vm;
# Only build rootfs if we need it (no custom root/kernel/initramfs)
needsBuiltRootfs = vm.rootDisk == null || vm.kernel == null || vm.initramfs == null;
rootfs = if needsBuiltRootfs then buildRootfs vm else null;
# Determine root disk, kernel, and initramfs sources
rootDiskPath = if vm.rootDisk != null then vm.rootDisk.path else "${rootfs}/nixos.qcow2";
rootDiskConfig = {
path = rootDiskPath;
readOnly = vm.rootDiskReadonly;
enableDiscard = if vm.rootDisk != null then vm.rootDisk.enableDiscard else true;
blockSize = if vm.rootDisk != null then vm.rootDisk.blockSize else 512;
devIdentifier = if vm.rootDisk != null then vm.rootDisk.devIdentifier else null;
useDirect = if vm.rootDisk != null then vm.rootDisk.useDirect else false;
};
kernelPath = if vm.kernel != null then vm.kernel else "${rootfs}/bzImage";
initramfsPath = if vm.initramfs != null then vm.initramfs else "${rootfs}/initrd";
vmIp = "${networkBase}.${toString vm.id}";
gwIp = "${networkBase}.${toString (vm.id - 1)}";
ipv6 = "fd4d:06ff:48e4:${toString (vm.id - 1)}::2/48";
gwv6 = "fd4d:06ff:48e4:${toString (vm.id - 1)}::1";
additionalDisks = lib.concatMapStringsSep " " (d: "--rwdisk ${d}") vm.disks;
additionalDisksArgs = lib.concatMapStringsSep " " formatBlockArg vm.additionalDisks;
sharedDirArgs = lib.concatMapStringsSep " " (d: "--shared-dir ${d}") vm.sharedDirectories;
extraKernelParams = lib.concatMapStringsSep " " (p: "-p \"${p}\"") vm.kernelParams;
in
pkgs.writeShellScript "qubes-lite-start-${vm.name}" ''
#!/bin/sh
@ -65,12 +99,14 @@ let
rm -f "$XDG_RUNTIME_DIR/crosvm-${vm.name}.sock"
exec ${cfg._internal.crosvm}/bin/crosvm run \
--name ${vm.name} \
--log-level=${cfg.crosvmLogLevel} \
-m ${toString vm.memory} \
--initrd=${rootfs}/initrd \
--initrd=${initramfsPath} \
--serial=hardware=virtio-console \
--disk ${rootfs}/nixos.qcow2 \
${additionalDisks} \
-p "init=${rootfs.config.system.build.toplevel}/init" \
${formatBlockArg rootDiskConfig} \
${additionalDisksArgs} \
${lib.optionalString (rootfs != null) ''-p "init=${rootfs.config.system.build.toplevel}/init"''} \
-p "net.ifnames=0" \
-p "spectrumname=${vm.name}" \
${lib.optionalString vm.network ''
@ -84,12 +120,14 @@ let
-p "isDisposable=1" \
-p "idleTimeout=${toString vm.idleTimeout}" \
''} \
${extraKernelParams} \
${sharedDirArgs} \
--cid ${toString vm.id} \
--cpus ${toString vm.cpus} \
--gpu=context-types=cross-domain:virgl2 \
--gpu=${vm.gpu} \
-s "$XDG_RUNTIME_DIR/crosvm-${vm.name}.sock" \
--wayland-sock "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" \
${rootfs}/bzImage
${kernelPath}
'';
# vm-run: Run command in VM (socket-activated)

View file

@ -9,6 +9,49 @@
let
cfg = config.programs.qubes-lite;
# Disk configuration submodule for --block arguments
diskSubmodule = lib.types.submodule {
options = {
path = lib.mkOption {
type = lib.types.str;
description = "Path to the disk image or block device.";
example = "/tmp/data.qcow2";
};
readOnly = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether the disk should be read-only.";
};
enableDiscard = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether the disk should support the discard operation.";
};
blockSize = lib.mkOption {
type = lib.types.int;
default = 512;
description = "Reported block size of the disk in bytes.";
example = 4096;
};
devIdentifier = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Block device identifier (ASCII string, up to 20 characters).";
example = "datadisk";
};
useDirect = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Use O_DIRECT mode to bypass page cache.";
};
};
};
vmSubmodule = lib.types.submodule {
options = {
id = lib.mkOption {
@ -55,16 +98,76 @@ let
description = "Seconds to wait after last command exits before shutting down (only used when disposable=true).";
};
disks = lib.mkOption {
additionalDisks = lib.mkOption {
type = lib.types.listOf diskSubmodule;
default = [ ];
description = "Additional disks to attach to the VM.";
example = lib.literalExpression ''
[{
path = "/tmp/data.qcow2";
readOnly = true;
blockSize = 4096;
devIdentifier = "datadisk";
}]
'';
};
rootDisk = lib.mkOption {
type = lib.types.nullOr diskSubmodule;
default = null;
description = "Custom root disk. If not set, uses the built rootfs image.";
example = lib.literalExpression ''
{
path = "/path/to/custom-root.qcow2";
readOnly = true;
}
'';
};
kernel = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = "Custom kernel image. If not set, uses the built rootfs kernel.";
example = "/path/to/bzImage";
};
initramfs = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = "Custom initramfs. If not set, uses the built rootfs initrd.";
example = "/path/to/initrd";
};
rootDiskReadonly = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether the root disk should be read-only.";
};
kernelParams = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Additional disk paths (qcow2 or block devices).";
description = "Extra kernel command line parameters.";
example = [
"/tmp/data.qcow2"
"/dev/mapper/main-banking"
"acpi=off"
"debug"
];
};
gpu = lib.mkOption {
type = lib.types.str;
default = "context-types=cross-domain:virgl2";
description = "GPU configuration passed to crosvm's --gpu option.";
example = "context-types=cross-domain";
};
sharedDirectories = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Shared directories passed to crosvm's --shared-dir option.";
example = [ "/tmp/shared:shared:uid=1000" ];
};
guestPrograms = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = [ ];
@ -94,6 +197,13 @@ in
default = true;
};
crosvmLogLevel = lib.mkOption {
type = lib.types.str;
default = "info";
description = "Log level for crosvm (error, warn, info, debug, trace).";
example = "debug";
};
user = lib.mkOption {
type = lib.types.str;
description = "User who owns TAP interfaces and runs VMs. Required.";