1use std::fmt;
7
8use isoloom_spec::model::{Device, Gpus, Machine, Spec};
9
10use crate::environment::MachineShape;
11use crate::requirements::{Feature, Requirement};
12
13#[derive(Debug, Clone, Default, PartialEq, Eq)]
15#[non_exhaustive]
16pub struct Hardware {
17 pub gpus: Option<Gpus>,
19 pub devices: Vec<Device>,
21 pub nested_virtualization: bool,
23}
24
25impl Hardware {
26 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39#[non_exhaustive]
40pub enum Need {
41 ContainerGpus,
43 ContainerDevices,
45 ContainerNested,
47 VmGpus,
49 VmDevices,
51 VmNested,
53}
54
55impl Need {
56 #[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 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
101pub 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}