Skip to main content

isoloom_spec/model/
boot.rs

1//! A VM's own boot (`vm.boot`): a kernel booted directly, a UEFI firmware file, the boot devices'
2//! order and a network boot. Most machines boot their image the way it was built; these are for
3//! the labs that study the boot itself (a custom kernel, a board's device tree, PXE).
4
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7
8/// How a VM boots, instead of the way its image does. Paths are on the hypervisor's host (the
9/// hypervisor reads them, not Isoloom), so they are absolute. Vagrant's libvirt provider boots
10/// every field and its QEMU provider `firmware_format`; the other targets refuse a machine with
11/// one.
12#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
13#[serde(deny_unknown_fields)]
14pub struct Boot {
15    /// A kernel to boot directly, instead of the image's boot loader.
16    #[serde(default, skip_serializing_if = "Option::is_none")]
17    pub kernel: Option<String>,
18    /// The initial RAM disk booted with `kernel`.
19    #[serde(default, skip_serializing_if = "Option::is_none")]
20    pub initrd: Option<String>,
21    /// The command line passed to `kernel` (`console=ttyS0 root=/dev/vda1`).
22    #[serde(default, skip_serializing_if = "Option::is_none")]
23    pub cmdline: Option<String>,
24    /// A device tree blob for `kernel` (ARM boards).
25    #[serde(default, skip_serializing_if = "Option::is_none")]
26    pub dtb: Option<String>,
27    /// The firmware the VM boots through: `bios`, or `uefi` (on libvirt, with its file in
28    /// `loader`; Proxmox and QEMU bring their own).
29    #[serde(default, skip_serializing_if = "Option::is_none")]
30    pub firmware: Option<Firmware>,
31    /// A UEFI firmware file the VM boots through (OVMF's `OVMF_CODE.fd`).
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub loader: Option<String>,
34    /// The file that keeps the firmware's variables between boots (with `loader`).
35    #[serde(default, skip_serializing_if = "Option::is_none")]
36    pub nvram: Option<String>,
37    /// The format of the UEFI firmware files the hypervisor loads: `raw` (QEMU's own, as
38    /// Homebrew ships them) or `qcow2` (some distributions'). Vagrant's QEMU provider needs it
39    /// to load them; libvirt reads the format from the file itself.
40    #[serde(default, skip_serializing_if = "Option::is_none")]
41    pub firmware_format: Option<FirmwareFormat>,
42    /// The devices to boot from, in order (`[network, hd]` tries a network boot first).
43    #[serde(default, skip_serializing_if = "Vec::is_empty")]
44    pub order: Vec<BootDevice>,
45    /// A network boot served on the machine's first network: its TFTP folder and the file
46    /// DHCP hands out. `order` must list `network`.
47    #[serde(default, skip_serializing_if = "Option::is_none")]
48    pub network: Option<NetworkBoot>,
49}
50
51impl Boot {
52    /// The fields set besides `firmware_format` and `firmware: uefi`, by name: what Vagrant's
53    /// QEMU provider can't boot (it boots the box's disk through its own UEFI firmware).
54    #[must_use]
55    pub fn beyond_firmware_format(&self) -> Vec<&'static str> {
56        let set = [
57            ("firmware", self.firmware == Some(Firmware::Bios)),
58            ("kernel", self.kernel.is_some()),
59            ("initrd", self.initrd.is_some()),
60            ("cmdline", self.cmdline.is_some()),
61            ("dtb", self.dtb.is_some()),
62            ("loader", self.loader.is_some()),
63            ("nvram", self.nvram.is_some()),
64            ("order", !self.order.is_empty()),
65            ("network", self.network.is_some()),
66        ];
67        set.into_iter().filter(|(_, on)| *on).map(|(name, _)| name).collect()
68    }
69}
70
71/// The firmware a VM boots through.
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
73#[serde(rename_all = "lowercase")]
74pub enum Firmware {
75    /// A legacy BIOS (SeaBIOS).
76    Bios,
77    /// UEFI (OVMF).
78    Uefi,
79}
80
81/// The format of a UEFI firmware file.
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
83#[serde(rename_all = "lowercase")]
84pub enum FirmwareFormat {
85    /// A raw image (QEMU's `edk2-*-code.fd`).
86    Raw,
87    /// A qcow2 image.
88    Qcow2,
89}
90
91impl FirmwareFormat {
92    /// The name the hypervisors use.
93    #[must_use]
94    pub fn id(self) -> &'static str {
95        match self {
96            FirmwareFormat::Raw => "raw",
97            FirmwareFormat::Qcow2 => "qcow2",
98        }
99    }
100}
101
102/// A device a VM boots from.
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
104#[serde(rename_all = "lowercase")]
105pub enum BootDevice {
106    /// The machine's disk.
107    Hd,
108    /// The network (PXE).
109    Network,
110    /// A CD-ROM drive.
111    Cdrom,
112}
113
114impl BootDevice {
115    /// The name libvirt gives it.
116    #[must_use]
117    pub fn id(self) -> &'static str {
118        match self {
119            BootDevice::Hd => "hd",
120            BootDevice::Network => "network",
121            BootDevice::Cdrom => "cdrom",
122        }
123    }
124}
125
126/// A network boot: what the network's DHCP and TFTP server hand a booting machine.
127#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
128#[serde(deny_unknown_fields)]
129pub struct NetworkBoot {
130    /// The folder the network's TFTP server serves, on the hypervisor's host.
131    #[serde(default, skip_serializing_if = "Option::is_none")]
132    pub tftp_root: Option<String>,
133    /// The boot file DHCP hands out (`pxelinux.0`), from the TFTP folder.
134    #[serde(default, skip_serializing_if = "Option::is_none")]
135    pub file: Option<String>,
136    /// The address of the TFTP server holding `file`, when it isn't the host's own.
137    #[serde(default, skip_serializing_if = "Option::is_none")]
138    pub server: Option<String>,
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    /// The fields of boot `yaml` beyond its firmware format.
146    fn beyond(yaml: &str) -> Result<Vec<&'static str>, serde_yaml_ng::Error> {
147        Ok(serde_yaml_ng::from_str::<Boot>(yaml)?.beyond_firmware_format())
148    }
149
150    #[test]
151    fn uefi_and_its_format_are_all_vagrants_qemu_provider_boots() -> Result<(), serde_yaml_ng::Error> {
152        assert_eq!(beyond("{ firmware: uefi, firmware_format: qcow2 }")?, Vec::<&str>::new());
153        assert_eq!(
154            beyond("{ firmware: bios, kernel: /k, initrd: /i, cmdline: quiet, dtb: /d, loader: /l, nvram: /n, order: [hd] }")?,
155            ["firmware", "kernel", "initrd", "cmdline", "dtb", "loader", "nvram", "order"]
156        );
157        Ok(())
158    }
159
160    #[test]
161    fn a_firmware_format_goes_by_the_name_a_spec_writes() -> Result<(), serde_yaml_ng::Error> {
162        for format in [FirmwareFormat::Raw, FirmwareFormat::Qcow2] {
163            assert_eq!(serde_yaml_ng::from_str::<FirmwareFormat>(format.id())?, format);
164        }
165        Ok(())
166    }
167}