use std::fmt;
use isoloom_spec::model::{Device, Gpus, Machine, Spec};
use crate::environment::MachineShape;
use crate::requirements::{Feature, Requirement};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct Hardware {
pub gpus: Option<Gpus>,
pub devices: Vec<Device>,
pub nested_virtualization: bool,
}
impl Hardware {
#[must_use]
pub fn of(m: &Machine) -> Hardware {
Hardware {
gpus: m.gpus.clone(),
devices: m.devices.clone(),
nested_virtualization: m.nested_virtualization,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Need {
ContainerGpus,
ContainerDevices,
ContainerNested,
VmGpus,
VmDevices,
VmNested,
}
impl Need {
#[must_use]
pub fn feature(self) -> Feature {
match self {
Need::ContainerGpus => Feature::ContainerGpus,
Need::ContainerDevices => Feature::ContainerDevices,
Need::ContainerNested => Feature::ContainerNestedVirtualization,
Need::VmGpus => Feature::VmGpus,
Need::VmDevices => Feature::VmDevices,
Need::VmNested => Feature::NestedVirtualization,
}
}
pub(crate) fn reason(self, machine: &str, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Need::ContainerGpus => write!(
f,
"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)"
),
Need::ContainerDevices => write!(
f,
"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"
),
Need::ContainerNested => write!(
f,
"machine `{machine}` runs VMs inside (`nested_virtualization`), which a container can't: it runs as a VM on the VM targets"
),
Need::VmGpus => write!(
f,
"machine `{machine}` asks for GPUs (`gpus`): Vagrant passes no GPU to a local VM (Proxmox and the cloud targets do)"
),
Need::VmDevices => write!(
f,
"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`)"
),
Need::VmNested => write!(
f,
"machine `{machine}` runs VMs inside (`nested_virtualization`): this target's VMs can't nest (Vagrant, Proxmox, AWS, Google Cloud and Oracle Cloud can)"
),
}
}
}
pub fn requirements(spec: &Spec, shape: impl Fn(&str) -> Option<MachineShape>) -> Vec<Requirement> {
let mut out = Vec::new();
for (name, m) in &spec.machines {
let needs: &[(bool, Need, Need)] = &[
(m.gpus.is_some(), Need::ContainerGpus, Need::VmGpus),
(!m.devices.is_empty(), Need::ContainerDevices, Need::VmDevices),
(m.nested_virtualization, Need::ContainerNested, Need::VmNested),
];
for (_, container, vm) in needs.iter().filter(|n| n.0) {
let need = match shape(name) {
Some(MachineShape::Container) => *container,
Some(MachineShape::Vm) => *vm,
_ => continue,
};
out.push(Requirement::Hardware { machine: name.clone(), need });
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn spec() -> Spec {
crate::testing::spec(
"version: 1\nname: hw\nnetworks: { lan: { cidr: 10.0.0.0/24 } }\nmachines:\n \
a: { networks: { lan: 10 }, hardware: { gpus: 1, devices: [/dev/fuse] }, os: debian@12, app: { image: x, steps: [{ run: a.sh }] } }\n \
b: { networks: { lan: 11 }, hardware: { nested_virtualization: true }, os: debian@12, app: { steps: [{ run: a.sh }] } }\n",
)
}
fn needs(shape: MachineShape) -> Vec<(String, Need)> {
requirements(&spec(), |_| Some(shape))
.into_iter()
.map(|r| match r {
Requirement::Hardware { machine, need } => (machine, need),
other => panic!("{other:?}"),
})
.collect()
}
#[test]
fn each_need_follows_the_machine_shape() {
let n = |m: &str, need| (m.to_owned(), need);
assert_eq!(
needs(MachineShape::Container),
[n("a", Need::ContainerGpus), n("a", Need::ContainerDevices), n("b", Need::ContainerNested)]
);
assert_eq!(needs(MachineShape::Vm), [n("a", Need::VmGpus), n("a", Need::VmDevices), n("b", Need::VmNested)]);
assert_eq!(needs(MachineShape::External), []);
assert!(Hardware::of(&spec().machines["b"]).nested_virtualization);
}
#[test]
fn every_need_says_why_and_names_its_field() {
for (need, field) in [
(Need::ContainerGpus, "gpus"),
(Need::ContainerDevices, "devices"),
(Need::ContainerNested, "nested_virtualization"),
(Need::VmGpus, "gpus"),
(Need::VmDevices, "devices"),
(Need::VmNested, "nested_virtualization"),
] {
let r = Requirement::Hardware { machine: "a".into(), need };
assert!(r.to_string().contains(&format!("(`{field}`)")), "{r}");
assert_eq!(r.feature(), need.feature());
}
}
}