use crate::model::{self, Resources};
pub const DEFAULT_CPUS: u32 = 1;
pub const DEFAULT_MEMORY_MB: u32 = 1024;
pub const DEFAULT_DISK_GB: u32 = 20;
pub const CONTAINER_MEMORY_MB: u32 = 512;
pub const CONTAINER_DISK_GB: u32 = 5;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Totals {
pub machines: u32,
pub cpus: u32,
pub memory_mb: u32,
pub disk_gb: u32,
}
fn machine_size(m: &model::Machine, container: bool) -> (u32, u32, u32) {
let r = m.resources.unwrap_or(Resources {
cpus: None,
memory_mb: None,
disk_gb: None,
});
let (memory_default, disk_default) = if container {
(CONTAINER_MEMORY_MB, CONTAINER_DISK_GB)
} else {
(DEFAULT_MEMORY_MB, DEFAULT_DISK_GB)
};
(
r.cpus.unwrap_or(DEFAULT_CPUS),
r.memory_mb.unwrap_or(memory_default),
r.disk_gb.unwrap_or(disk_default),
)
}
fn runs_as_container(target: model::Target, m: &model::Machine) -> bool {
use model::Shape;
match target.needs() {
Shape::Docker => true,
Shape::Vm | Shape::External | Shape::Cloud => false,
Shape::VmOrLxc => m.docker.as_ref().is_some_and(|d| d.lxc),
Shape::Either => m.docker.is_some(),
}
}
fn add_up(spec: &model::Spec, size: impl Fn(&model::Machine) -> (u32, u32, u32)) -> Totals {
spec.machines.values().fold(Totals::default(), |t, m| {
let (cpus, memory_mb, disk_gb) = size(m);
Totals {
machines: t.machines + 1,
cpus: t.cpus.saturating_add(cpus),
memory_mb: t.memory_mb.saturating_add(memory_mb),
disk_gb: t.disk_gb.saturating_add(disk_gb),
}
})
}
#[must_use]
pub fn totals(spec: &model::Spec) -> Totals {
add_up(spec, |m| machine_size(m, false))
}
#[must_use]
pub fn totals_for(spec: &model::Spec, target: model::Target) -> Totals {
add_up(spec, |m| machine_size(m, runs_as_container(target, m)))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::Target;
fn spec(machines: &str) -> model::Spec {
crate::testing::model(&crate::testing::lab(machines)).unwrap()
}
#[test]
fn totals_add_each_machines_size_or_the_defaults() {
let spec = spec(
"machines:\n a: { networks: { lan: 10 }, docker: { image: x }, resources: { cpus: 4, memory_mb: 512, disk_gb: 5 } }\n b: { networks: { lan: 11 }, docker: { image: x }, resources: { cpus: 2 } }\n c: { networks: { lan: 12 }, docker: { image: x } }\n",
);
assert_eq!(
totals(&spec),
Totals {
machines: 3,
cpus: 4 + 2 + DEFAULT_CPUS,
memory_mb: 512 + 2 * DEFAULT_MEMORY_MB,
disk_gb: 5 + 2 * DEFAULT_DISK_GB,
}
);
}
#[test]
fn totals_saturate_instead_of_overflowing() {
let huge = u32::MAX;
let spec = spec(&format!(
"machines:\n a: {{ networks: {{ lan: 10 }}, docker: {{ image: x }}, resources: {{ cpus: {huge} }} }}\n b: {{ networks: {{ lan: 11 }}, docker: {{ image: x }}, resources: {{ cpus: 3 }} }}\n"
));
assert_eq!(totals(&spec).cpus, u32::MAX);
}
#[test]
fn a_target_sizes_containers_lighter_than_vms() {
let spec = spec("machines:\n a: { networks: { lan: 10 }, docker: { image: x }, vm: { os: debian@12 } }\n");
let docker = totals_for(&spec, Target::Docker);
let vagrant = totals_for(&spec, Target::Vagrant);
assert_eq!((docker.memory_mb, docker.disk_gb), (CONTAINER_MEMORY_MB, CONTAINER_DISK_GB));
assert_eq!((vagrant.memory_mb, vagrant.disk_gb), (DEFAULT_MEMORY_MB, DEFAULT_DISK_GB));
assert_eq!((docker.cpus, vagrant.cpus), (DEFAULT_CPUS, DEFAULT_CPUS), "a core is a core");
assert_eq!(totals(&spec), vagrant, "totals is the all-VM sizing");
}
#[test]
fn declared_resources_win_over_the_kind_default() {
let spec = spec("machines:\n a: { networks: { lan: 10 }, docker: { image: x }, resources: { memory_mb: 2048 } }\n");
let docker = totals_for(&spec, Target::Docker);
assert_eq!(docker.memory_mb, 2048, "the spec's number wins");
assert_eq!(docker.disk_gb, CONTAINER_DISK_GB, "the field it left out takes the container default");
}
#[test]
fn hybrid_sizes_each_machine_by_its_own_kind() {
let spec = spec("machines:\n web: { networks: { lan: 10 }, docker: { image: x } }\n win: { networks: { lan: 11 }, vm: { os: windows-server@2022 } }\n");
let hybrid = totals_for(&spec, Target::Hybrid);
assert_eq!(hybrid.memory_mb, CONTAINER_MEMORY_MB + DEFAULT_MEMORY_MB);
assert_eq!(hybrid.disk_gb, CONTAINER_DISK_GB + DEFAULT_DISK_GB);
}
#[test]
fn a_proxmox_lxc_container_is_lighter_than_a_proxmox_vm() {
let lxc = spec("machines:\n a: { networks: { lan: 10 }, docker: { image: x, lxc: true } }\n");
let vm = spec("machines:\n a: { networks: { lan: 10 }, vm: { os: debian@12 } }\n");
assert_eq!(totals_for(&lxc, Target::Proxmox).memory_mb, CONTAINER_MEMORY_MB);
assert_eq!(totals_for(&vm, Target::Proxmox).memory_mb, DEFAULT_MEMORY_MB);
}
}