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 = ''
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 = ''
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 = ''
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 = ''
39 Guest OS type
40 '';
41 };
42 cores = mkOption {
43 type = types.ints.positive;
44 default = 1;
45 description = ''
46 Guest core count
47 '';
48 };
49 memory = mkOption {
50 type = types.ints.positive;
51 default = 1024;
52 description = ''
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 = ''
69 VM name
70 '';
71 };
72 additionalSpace = mkOption {
73 type = types.str;
74 default = "512M";
75 example = "2048M";
76 description = ''
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 = ''
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 = ''
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 = ''
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 = ''
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 = ''
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 = ''
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 = ''
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}