use std::net::Ipv4Addr;
use isoloom_spec::model::{Machine, Spec};
use isoloom_spec::validate::{Cidr, starts_after};
const UNPARSED: Cidr = Cidr { base: 0, len: 24 };
#[must_use]
pub fn cidr(spec: &Spec, network: &str) -> Cidr {
spec.networks.get(network).and_then(|n| Cidr::parse(&n.cidr)).unwrap_or(UNPARSED)
}
#[must_use]
pub fn address(spec: &Spec, network: &str, octet: u8) -> Ipv4Addr {
Ipv4Addr::from((cidr(spec, network).base & !0xff) | u32::from(octet))
}
#[must_use]
pub fn netmask(spec: &Spec, network: &str) -> Ipv4Addr {
let len = cidr(spec, network).len;
Ipv4Addr::from(if len == 0 { 0 } else { u32::MAX << (32 - len) })
}
#[must_use]
pub fn host_address(spec: &Spec, network: &str) -> Ipv4Addr {
let block = cidr(spec, network);
if spec.networks.get(network).is_some_and(|n| n.gateway.is_some()) {
block.router()
} else {
block.gateway()
}
}
#[must_use]
pub fn address_for(spec: &Spec, from: &str, to: &str) -> Ipv4Addr {
let shared = |n: &str| spec.machines.get(from).is_some_and(|a| a.networks.contains_key(n));
let Some(b) = spec.machines.get(to) else {
return Ipv4Addr::UNSPECIFIED;
};
b.networks
.iter()
.find(|(n, _)| shared(n))
.or_else(|| b.networks.first())
.map_or(Ipv4Addr::UNSPECIFIED, |(net, octet)| address(spec, net, *octet))
}
#[must_use]
pub fn names_of(spec: &Spec, name: &str) -> String {
std::iter::once(name)
.chain(spec.machines.get(name).map(|m| m.aliases.iter().map(String::as_str)).into_iter().flatten())
.collect::<Vec<_>>()
.join(" ")
}
#[must_use]
pub fn start_order(spec: &Spec) -> Vec<&str> {
let mut order: Vec<&str> = Vec::new();
for (name, m) in &spec.machines {
visit(spec, (name, m), &mut Vec::new(), &mut order);
}
order
}
fn visit<'a>(spec: &'a Spec, (name, m): (&'a str, &'a Machine), path: &mut Vec<&'a str>, order: &mut Vec<&'a str>) {
if order.contains(&name) || path.contains(&name) {
return;
}
path.push(name);
for dep in starts_after(spec, name, m) {
if let Some((dep, dm)) = spec.machines.get_key_value(dep) {
visit(spec, (dep, dm), path, order);
}
}
path.pop();
order.push(name);
}
#[cfg(test)]
mod tests {
use std::net::Ipv4Addr;
use super::*;
use crate::testing;
const SPEC: &str = "version: 1
name: t
networks: { a: { cidr: 10.1.0.0/24 }, b: { cidr: 10.2.0.0/16 }, c: { cidr: 10.3.0.0/24, gateway: fw } }
machines:
web: { networks: { a: 10, b: 20 }, aliases: [www, site], depends_on: [db], app: { image: nginx } }
db: { networks: { b: 30 }, app: { image: postgres } }
fw: { networks: { a: 1, c: 1 }, app: { image: alpine } }
box: { networks: { c: 40 }, app: { image: alpine } }
lone: { app: { image: alpine } }
";
#[test]
fn an_address_is_the_block_with_the_machines_octet() {
let spec = testing::spec(SPEC);
assert_eq!(address(&spec, "a", 10), Ipv4Addr::new(10, 1, 0, 10));
assert_eq!(address(&spec, "b", 30), Ipv4Addr::new(10, 2, 0, 30));
assert_eq!(netmask(&spec, "a"), Ipv4Addr::new(255, 255, 255, 0));
assert_eq!(netmask(&spec, "b"), Ipv4Addr::new(255, 255, 0, 0));
assert_eq!(cidr(&spec, "nowhere"), UNPARSED);
}
#[test]
fn a_network_routed_by_a_machine_gives_the_host_the_routers_address() {
let spec = testing::spec(SPEC);
assert_eq!(host_address(&spec, "a"), Ipv4Addr::new(10, 1, 0, 1));
assert_eq!(host_address(&spec, "c"), Ipv4Addr::new(10, 3, 0, 254));
}
#[test]
fn a_machine_is_reached_on_a_shared_network_else_at_its_first_address() {
let spec = testing::spec(SPEC);
assert_eq!(address_for(&spec, "db", "web"), Ipv4Addr::new(10, 2, 0, 20));
assert_eq!(address_for(&spec, "fw", "web"), Ipv4Addr::new(10, 1, 0, 10));
assert_eq!(address_for(&spec, "box", "web"), Ipv4Addr::new(10, 1, 0, 10));
assert_eq!(address_for(&spec, "web", "lone"), Ipv4Addr::UNSPECIFIED);
assert_eq!(address_for(&spec, "web", "ghost"), Ipv4Addr::UNSPECIFIED);
}
#[test]
fn names_are_the_machines_own_then_its_aliases() {
let spec = testing::spec(SPEC);
assert_eq!(names_of(&spec, "web"), "web www site");
assert_eq!(names_of(&spec, "db"), "db");
}
#[test]
fn a_machine_starts_after_its_dependencies_and_gateways() {
let spec = testing::spec(SPEC);
assert_eq!(start_order(&spec), ["db", "web", "fw", "box", "lone"]);
}
#[test]
fn a_cycle_still_orders_every_machine_once() {
let mut spec = testing::spec(SPEC);
if let Some(db) = spec.machines.get_mut("db") {
db.depends_on = vec!["web".into()];
}
assert_eq!(start_order(&spec), ["db", "web", "fw", "box", "lone"]);
}
}