1{ config, pkgs, lib, ... }:
2
3with lib;
4
5{
6 options.proxmox = {
7 qemuConf = {
8 # essential configs
9 boot = mkOption {
10 type = types.str;
11 default = "";
12 example = "order=scsi0;net0";
13 description = lib.mdDoc ''
14 Default boot device. PVE will try all devices in its default order if this value is empty.
15 '';
16 };
17 scsihw = mkOption {
18 type = types.str;
19 default = "virtio-scsi-pci";
20 example = "lsi";
21 description = lib.mdDoc ''
22 SCSI controller type. Must be one of the supported values given in
23 <https://pve.proxmox.com/wiki/Qemu/KVM_Virtual_Machines>
24 '';
25 };
26 virtio0 = mkOption {
27 type = types.str;
28 default = "local-lvm:vm-9999-disk-0";
29 example = "ceph:vm-123-disk-0";
30 description = lib.mdDoc ''
31 Configuration for the default virtio disk. It can be used as a cue for PVE to autodetect the target storage.
32 This parameter is required by PVE even if it isn't used.
33 '';
34 };
35 ostype = mkOption {
36 type = types.str;
37 default = "l26";
38 description = lib.mdDoc ''
39 Guest OS type
40 '';
41 };
42 cores = mkOption {
43 type = types.ints.positive;
44 default = 1;
45 description = lib.mdDoc ''
46 Guest core count
47 '';
48 };
49 memory = mkOption {
50 type = types.ints.positive;
51 default = 1024;
52 description = lib.mdDoc ''
53 Guest memory in MB
54 '';
55 };
56 bios = mkOption {
57 type = types.enum [ "seabios" "ovmf" ];
58 default = "seabios";
59 description = ''
60 Select BIOS implementation (seabios = Legacy BIOS, ovmf = UEFI).
61 '';
62 };
63
64 # optional configs
65 name = mkOption {
66 type = types.str;
67 default = "nixos-${config.system.nixos.label}";
68 description = lib.mdDoc ''
69 VM name
70 '';
71 };
72 additionalSpace = mkOption {
73 type = types.str;
74 default = "512M";
75 example = "2048M";
76 description = lib.mdDoc ''
77 additional disk space to be added to the image if diskSize "auto"
78 is used.
79 '';
80 };
81 bootSize = mkOption {
82 type = types.str;
83 default = "256M";
84 example = "512M";
85 description = lib.mdDoc ''
86 Size of the boot partition. Is only used if partitionTableType is
87 either "efi" or "hybrid".
88 '';
89 };
90 diskSize = mkOption {
91 type = types.str;
92 default = "auto";
93 example = "20480";
94 description = lib.mdDoc ''
95 The size of the disk, in megabytes.
96 if "auto" size is calculated based on the contents copied to it and
97 additionalSpace is taken into account.
98 '';
99 };
100 net0 = mkOption {
101 type = types.commas;
102 default = "virtio=00:00:00:00:00:00,bridge=vmbr0,firewall=1";
103 description = lib.mdDoc ''
104 Configuration for the default interface. When restoring from VMA, check the
105 "unique" box to ensure device mac is randomized.
106 '';
107 };
108 serial0 = mkOption {
109 type = types.str;
110 default = "socket";
111 example = "/dev/ttyS0";
112 description = lib.mdDoc ''
113 Create a serial device inside the VM (n is 0 to 3), and pass through a host serial device (i.e. /dev/ttyS0),
114 or create a unix socket on the host side (use qm terminal to open a terminal connection).
115 '';
116 };
117 agent = mkOption {
118 type = types.bool;
119 apply = x: if x then "1" else "0";
120 default = true;
121 description = lib.mdDoc ''
122 Expect guest to have qemu agent running
123 '';
124 };
125 };
126 qemuExtraConf = mkOption {
127 type = with types; attrsOf (oneOf [ str int ]);
128 default = {};
129 example = literalExpression ''
130 {
131 cpu = "host";
132 onboot = 1;
133 }
134 '';
135 description = lib.mdDoc ''
136 Additional options appended to qemu-server.conf
137 '';
138 };
139 partitionTableType = mkOption {
140 type = types.enum [ "efi" "hybrid" "legacy" "legacy+gpt" ];
141 description = ''
142 Partition table type to use. See make-disk-image.nix partitionTableType for details.
143 Defaults to 'legacy' for 'proxmox.qemuConf.bios="seabios"' (default), other bios values defaults to 'efi'.
144 Use 'hybrid' to build grub-based hybrid bios+efi images.
145 '';
146 default = if config.proxmox.qemuConf.bios == "seabios" then "legacy" else "efi";
147 defaultText = lib.literalExpression ''if config.proxmox.qemuConf.bios == "seabios" then "legacy" else "efi"'';
148 example = "hybrid";
149 };
150 filenameSuffix = mkOption {
151 type = types.str;
152 default = config.proxmox.qemuConf.name;
153 example = "999-nixos_template";
154 description = lib.mdDoc ''
155 Filename of the image will be vzdump-qemu-''${filenameSuffix}.vma.zstd.
156 This will also determine the default name of the VM on restoring the VMA.
157 Start this value with a number if you want the VMA to be detected as a backup of
158 any specific VMID.
159 '';
160 };
161 };
162
163 config = let
164 cfg = config.proxmox;
165 cfgLine = name: value: ''
166 ${name}: ${builtins.toString value}
167 '';
168 virtio0Storage = builtins.head (builtins.split ":" cfg.qemuConf.virtio0);
169 cfgFile = fileName: properties: pkgs.writeTextDir fileName ''
170 # generated by NixOS
171 ${lib.concatStrings (lib.mapAttrsToList cfgLine properties)}
172 #qmdump#map:virtio0:drive-virtio0:${virtio0Storage}:raw:
173 '';
174 inherit (cfg) partitionTableType;
175 supportEfi = partitionTableType == "efi" || partitionTableType == "hybrid";
176 supportBios = partitionTableType == "legacy" || partitionTableType == "hybrid" || partitionTableType == "legacy+gpt";
177 hasBootPartition = partitionTableType == "efi" || partitionTableType == "hybrid";
178 hasNoFsPartition = partitionTableType == "hybrid" || partitionTableType == "legacy+gpt";
179 in {
180 assertions = [
181 {
182 assertion = config.boot.loader.systemd-boot.enable -> config.proxmox.qemuConf.bios == "ovmf";
183 message = "systemd-boot requires 'ovmf' bios";
184 }
185 {
186 assertion = partitionTableType == "efi" -> config.proxmox.qemuConf.bios == "ovmf";
187 message = "'efi' disk partitioning requires 'ovmf' bios";
188 }
189 {
190 assertion = partitionTableType == "legacy" -> config.proxmox.qemuConf.bios == "seabios";
191 message = "'legacy' disk partitioning requires 'seabios' bios";
192 }
193 {
194 assertion = partitionTableType == "legacy+gpt" -> config.proxmox.qemuConf.bios == "seabios";
195 message = "'legacy+gpt' disk partitioning requires 'seabios' bios";
196 }
197 ];
198 system.build.VMA = import ../../lib/make-disk-image.nix {
199 name = "proxmox-${cfg.filenameSuffix}";
200 inherit (cfg) partitionTableType;
201 postVM = let
202 # Build qemu with PVE's patch that adds support for the VMA format
203 vma = (pkgs.qemu_kvm.override {
204 alsaSupport = false;
205 pulseSupport = false;
206 sdlSupport = false;
207 jackSupport = false;
208 gtkSupport = false;
209 vncSupport = false;
210 smartcardSupport = false;
211 spiceSupport = false;
212 ncursesSupport = false;
213 libiscsiSupport = false;
214 tpmSupport = false;
215 numaSupport = false;
216 seccompSupport = false;
217 guestAgentSupport = false;
218 }).overrideAttrs ( super: rec {
219
220 version = "7.2.1";
221 src = pkgs.fetchurl {
222 url= "https://download.qemu.org/qemu-${version}.tar.xz";
223 sha256 = "sha256-jIVpms+dekOl/immTN1WNwsMLRrQdLr3CYqCTReq1zs=";
224 };
225 patches = [
226 # Proxmox' VMA tool is published as a particular patch upon QEMU
227 (pkgs.fetchpatch {
228 url =
229 let
230 rev = "abb04bb6272c1202ca9face0827917552b9d06f6";
231 path = "debian/patches/pve/0027-PVE-Backup-add-vma-backup-format-code.patch";
232 in "https://git.proxmox.com/?p=pve-qemu.git;a=blob_plain;hb=${rev};f=${path}";
233 hash = "sha256-3d0HHdvaExCry6zcULnziYnWIAnn24vECkI4sjj2BMg=";
234 })
235
236 # Proxmox' VMA tool uses O_DIRECT which fails on tmpfs
237 # Filed to upstream issue tracker: https://bugzilla.proxmox.com/show_bug.cgi?id=4710
238 (pkgs.writeText "inline.patch" ''
239 --- a/vma-writer.c 2023-05-01 15:11:13.361341177 +0200
240 +++ b/vma-writer.c 2023-05-01 15:10:51.785293129 +0200
241 @@ -306,7 +306,7 @@
242 /* try to use O_NONBLOCK */
243 fcntl(vmaw->fd, F_SETFL, fcntl(vmaw->fd, F_GETFL)|O_NONBLOCK);
244 } else {
245 - oflags = O_NONBLOCK|O_DIRECT|O_WRONLY|O_EXCL;
246 + oflags = O_NONBLOCK|O_WRONLY|O_EXCL;
247 vmaw->fd = qemu_create(filename, oflags, 0644, errp);
248 }
249 '')
250 ];
251
252 buildInputs = super.buildInputs ++ [ pkgs.libuuid ];
253 nativeBuildInputs = super.nativeBuildInputs ++ [ pkgs.perl ];
254
255 });
256 in
257 ''
258 ${vma}/bin/vma create "vzdump-qemu-${cfg.filenameSuffix}.vma" \
259 -c ${cfgFile "qemu-server.conf" (cfg.qemuConf // cfg.qemuExtraConf)}/qemu-server.conf drive-virtio0=$diskImage
260 rm $diskImage
261 ${pkgs.zstd}/bin/zstd "vzdump-qemu-${cfg.filenameSuffix}.vma"
262 mv "vzdump-qemu-${cfg.filenameSuffix}.vma.zst" $out/
263
264 mkdir -p $out/nix-support
265 echo "file vma $out/vzdump-qemu-${cfg.filenameSuffix}.vma.zst" >> $out/nix-support/hydra-build-products
266 '';
267 inherit (cfg.qemuConf) additionalSpace diskSize bootSize;
268 format = "raw";
269 inherit config lib pkgs;
270 };
271
272 boot = {
273 growPartition = true;
274 kernelParams = [ "console=ttyS0" ];
275 loader.grub = {
276 device = lib.mkDefault (if (hasNoFsPartition || supportBios) then
277 # Even if there is a separate no-fs partition ("/dev/disk/by-partlabel/no-fs" i.e. "/dev/vda2"),
278 # which will be used the bootloader, do not set it as loader.grub.device.
279 # GRUB installation fails, unless the whole disk is selected.
280 "/dev/vda"
281 else
282 "nodev");
283 efiSupport = lib.mkDefault supportEfi;
284 efiInstallAsRemovable = lib.mkDefault supportEfi;
285 };
286
287 loader.timeout = 0;
288 initrd.availableKernelModules = [ "uas" "virtio_blk" "virtio_pci" ];
289 };
290
291 fileSystems."/" = {
292 device = "/dev/disk/by-label/nixos";
293 autoResize = true;
294 fsType = "ext4";
295 };
296 fileSystems."/boot" = lib.mkIf hasBootPartition {
297 device = "/dev/disk/by-label/ESP";
298 fsType = "vfat";
299 };
300
301 services.qemuGuest.enable = lib.mkDefault true;
302 };
303}