Skip to main content

isoloom_plan/
hardware.rs

1//! What a machine asks of the hardware under it (`gpus`, `devices`, `nested_virtualization`),
2//! and what that requires of the backend, by the machine's shape on the target: a container
3//! gets the host's GPUs and devices from its runtime, a VM gets GPUs passed through or sized in
4//! and runs VMs of its own through its hypervisor. An existing host is used as it is.
5
6use std::fmt;
7
8use isoloom_spec::model::{Device, Gpus, Machine, Spec};
9
10use crate::environment::MachineShape;
11use crate::requirements::{Feature, Requirement};
12
13/// A machine's hardware, as the spec asks for it.
14#[derive(Debug, Clone, Default, PartialEq, Eq)]
15#[non_exhaustive]
16pub struct Hardware {
17    /// Its GPUs, if it asks for any.
18    pub gpus: Option<Gpus>,
19    /// The host's devices it is given.
20    pub devices: Vec<Device>,
21    /// Whether it runs VMs of its own.
22    pub nested_virtualization: bool,
23}
24
25impl Hardware {
26    /// What machine `m` asks for.
27    #[must_use]
28    pub fn of(m: &Machine) -> Hardware {
29        Hardware {
30            gpus: m.gpus.clone(),
31            devices: m.devices.clone(),
32            nested_virtualization: m.nested_virtualization,
33        }
34    }
35}
36
37/// One hardware need of a machine, in its shape on the target.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39#[non_exhaustive]
40pub enum Need {
41    /// A container asks for GPUs.
42    ContainerGpus,
43    /// A container asks for the host's devices.
44    ContainerDevices,
45    /// A container asks to run VMs inside.
46    ContainerNested,
47    /// A VM asks for GPUs.
48    VmGpus,
49    /// A VM asks for the host's devices.
50    VmDevices,
51    /// A VM asks to run VMs inside.
52    VmNested,
53}
54
55impl Need {
56    /// The capability a backend needs to meet it.
57    #[must_use]
58    pub fn feature(self) -> Feature {
59        match self {
60            Need::ContainerGpus => Feature::ContainerGpus,
61            Need::ContainerDevices => Feature::ContainerDevices,
62            Need::ContainerNested => Feature::ContainerNestedVirtualization,
63            Need::VmGpus => Feature::VmGpus,
64            Need::VmDevices => Feature::VmDevices,
65            Need::VmNested => Feature::NestedVirtualization,
66        }
67    }
68
69    /// Why a backend without the capability refuses it, for machine `machine`. Each message names
70    /// the targets that lack it, since only those ever show it.
71    pub(crate) fn reason(self, machine: &str, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        match self {
73            Need::ContainerGpus => write!(
74                f,
75                "machine `{machine}` asks for GPUs (`gpus`): its container runs on a VM Isoloom creates, which has no GPU (the `docker` and `kubernetes` targets give the host's)"
76            ),
77            Need::ContainerDevices => write!(
78                f,
79                "machine `{machine}` is given host devices (`devices`): only a container on your own Docker host reaches them (the `docker` target), not one in a VM Isoloom creates or a Kubernetes pod"
80            ),
81            Need::ContainerNested => write!(
82                f,
83                "machine `{machine}` runs VMs inside (`nested_virtualization`), which a container can't: it runs as a VM on the VM targets"
84            ),
85            Need::VmGpus => write!(
86                f,
87                "machine `{machine}` asks for GPUs (`gpus`): Vagrant passes no GPU to a local VM (Proxmox and the cloud targets do)"
88            ),
89            Need::VmDevices => write!(
90                f,
91                "machine `{machine}` is given host devices (`devices`): a VM has its own virtual devices, so only a container gets the host's (a GPU reaches a VM through `gpus`)"
92            ),
93            Need::VmNested => write!(
94                f,
95                "machine `{machine}` runs VMs inside (`nested_virtualization`): this target's VMs can't nest (Vagrant, Proxmox, AWS, Google Cloud and Oracle Cloud can)"
96            ),
97        }
98    }
99}
100
101/// What the machines' hardware asks of the backend: one requirement per machine and need, in
102/// machine order. `shape` is each machine's shape on the target; an existing or supplied
103/// machine asks nothing (it is used as it is).
104pub fn requirements(spec: &Spec, shape: impl Fn(&str) -> Option<MachineShape>) -> Vec<Requirement> {
105    let mut out = Vec::new();
106    for (name, m) in &spec.machines {
107        let needs: &[(bool, Need, Need)] = &[
108            (m.gpus.is_some(), Need::ContainerGpus, Need::VmGpus),
109            (!m.devices.is_empty(), Need::ContainerDevices, Need::VmDevices),
110            (m.nested_virtualization, Need::ContainerNested, Need::VmNested),
111        ];
112        for (_, container, vm) in needs.iter().filter(|n| n.0) {
113            let need = match shape(name) {
114                Some(MachineShape::Container) => *container,
115                Some(MachineShape::Vm) => *vm,
116                _ => continue,
117            };
118            out.push(Requirement::Hardware { machine: name.clone(), need });
119        }
120    }
121    out
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    fn spec() -> Spec {
129        crate::testing::spec(
130            "version: 1\nname: hw\nnetworks: { lan: { cidr: 10.0.0.0/24 } }\nmachines:\n  \
131             a: { networks: { lan: 10 }, hardware: { gpus: 1, devices: [/dev/fuse] }, os: debian@12, app: { image: x, steps: [{ run: a.sh }] } }\n  \
132             b: { networks: { lan: 11 }, hardware: { nested_virtualization: true }, os: debian@12, app: { steps: [{ run: a.sh }] } }\n",
133        )
134    }
135
136    fn needs(shape: MachineShape) -> Vec<(String, Need)> {
137        requirements(&spec(), |_| Some(shape))
138            .into_iter()
139            .map(|r| match r {
140                Requirement::Hardware { machine, need } => (machine, need),
141                other => panic!("{other:?}"),
142            })
143            .collect()
144    }
145
146    #[test]
147    fn each_need_follows_the_machine_shape() {
148        let n = |m: &str, need| (m.to_owned(), need);
149        assert_eq!(
150            needs(MachineShape::Container),
151            [n("a", Need::ContainerGpus), n("a", Need::ContainerDevices), n("b", Need::ContainerNested)]
152        );
153        assert_eq!(needs(MachineShape::Vm), [n("a", Need::VmGpus), n("a", Need::VmDevices), n("b", Need::VmNested)]);
154        assert_eq!(needs(MachineShape::External), []);
155        assert!(Hardware::of(&spec().machines["b"]).nested_virtualization);
156    }
157
158    #[test]
159    fn every_need_says_why_and_names_its_field() {
160        for (need, field) in [
161            (Need::ContainerGpus, "gpus"),
162            (Need::ContainerDevices, "devices"),
163            (Need::ContainerNested, "nested_virtualization"),
164            (Need::VmGpus, "gpus"),
165            (Need::VmDevices, "devices"),
166            (Need::VmNested, "nested_virtualization"),
167        ] {
168            let r = Requirement::Hardware { machine: "a".into(), need };
169            assert!(r.to_string().contains(&format!("(`{field}`)")), "{r}");
170            assert_eq!(r.feature(), need.feature());
171        }
172    }
173}