use std::net::Ipv4Addr;
use indexmap::IndexMap;
use isoloom_spec::validate::Cidr;
use crate::document::{NetworkDoc, ReachDoc, VlanDoc};
use crate::draft::Draft;
use crate::{Error, Lan, Result, Topology};
pub(crate) fn vlan_ref(lan: &Lan) -> Option<String> {
let (switch, id) = vlan_of(lan)?;
Some(format!("{switch}.vlan{id}"))
}
fn vlan_of(lan: &Lan) -> Option<(&str, u16)> {
Some((lan.switch.as_deref()?, lan.vlan?))
}
pub(crate) fn check_switch_names(topo: &Topology) -> Result<()> {
let plain = |name: &String| topo.networks.get(name).is_some_and(|l| vlan_of(l).is_none());
match topo.networks.values().find_map(|l| l.switch.as_ref().filter(|s| plain(s))) {
Some(switch) => Err(Error::SwitchNameTaken { switch: switch.clone() }),
None => Ok(()),
}
}
pub(crate) fn nest(topo: &Topology, draft: &mut Draft) {
let renamed: IndexMap<&String, String> = topo.networks.iter().filter_map(|(name, lan)| Some((name, vlan_ref(lan)?))).collect();
if renamed.is_empty() {
return;
}
draft.networks = nested(topo, std::mem::take(&mut draft.networks));
for parent in draft.networks.values_mut().filter(|n| !n.vlans.is_empty()) {
settle_parent(parent);
}
rename_refs(draft, &renamed);
}
fn nested(topo: &Topology, flat: IndexMap<String, NetworkDoc>) -> IndexMap<String, NetworkDoc> {
let mut networks: IndexMap<String, NetworkDoc> = IndexMap::new();
for (name, net) in flat {
let Some((lan, (switch, id))) = topo.networks.get(&name).and_then(|l| Some((l, vlan_of(l)?))) else {
networks.insert(name, net);
continue;
};
let internet = net.reaches_internet();
let parent = networks
.entry(switch.to_owned())
.or_insert_with(|| NetworkDoc::new(String::new(), internet, None));
if lan.device.is_some() {
parent.switch.clone_from(&lan.device);
}
parent.vlans.insert(
id,
VlanDoc {
cidr: net.cidr,
internet: Some(internet),
},
);
}
networks
}
fn settle_parent(parent: &mut NetworkDoc) {
parent.cidr = covering(parent.vlans.values().map(|v| v.cidr.as_str()));
let all: Vec<bool> = parent.vlans.values().filter_map(|v| v.internet).collect();
let agreed = match all.split_first() {
Some((first, rest)) if rest.iter().all(|i| i == first) => Some(*first),
_ => None,
};
parent.set_internet(agreed.unwrap_or(true));
for v in parent.vlans.values_mut().filter(|v| agreed.is_some() || v.internet == Some(true)) {
v.internet = None;
}
}
fn rename_refs(draft: &mut Draft, renamed: &IndexMap<&String, String>) {
let rename = |n: &String| renamed.get(n).cloned().unwrap_or_else(|| n.clone());
for m in draft.machines.values_mut() {
m.networks = m.networks.iter().map(|(n, o)| (rename(n), *o)).collect();
}
for r in &mut draft.reach {
*r = ReachDoc {
from: rename(&r.from),
to: rename(&r.to),
};
}
}
fn block(cidr: &str) -> Option<(u32, u8)> {
let (address, len) = cidr.split_once('/')?;
Some((u32::from(address.parse::<Ipv4Addr>().ok()?), len.parse().ok()?))
}
fn mask(len: u8) -> u32 {
u32::MAX.checked_shl(32 - u32::from(len.min(32))).unwrap_or(0)
}
fn covering<'a>(cidrs: impl Iterator<Item = &'a str>) -> String {
let blocks: Vec<(u32, u8)> = cidrs.filter_map(block).collect();
let Some(&(first, _)) = blocks.first() else {
return String::new();
};
let mut len = blocks.iter().map(|(_, l)| *l).min().unwrap_or(24);
while len > 8 && !blocks.iter().all(|(b, _)| b & mask(len) == first & mask(len)) {
len -= 1;
}
Cidr { base: first & mask(len), len }.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_covering_block_holds_every_vlan() {
assert_eq!(covering(["10.10.10.0/24", "10.10.20.0/24"].into_iter()), "10.10.0.0/19");
assert_eq!(covering(["10.10.10.0/24"].into_iter()), "10.10.10.0/24");
assert_eq!(covering(["10.10.10.0/24", "11.0.0.0/24"].into_iter()), "10.0.0.0/8", "at most a /8");
assert_eq!(
covering(["10.10.10.0/24", "bad", "10.10.11.0/24"].into_iter()),
"10.10.10.0/23",
"what isn't a block is left out"
);
assert_eq!(covering(std::iter::empty()), "");
}
#[test]
fn a_mask_has_its_prefix_bits_set() {
assert_eq!(mask(0), 0);
assert_eq!(mask(8), 0xff00_0000);
assert_eq!(mask(32), u32::MAX);
assert_eq!(mask(40), u32::MAX, "a longer prefix is a whole address");
}
fn vlan(cidr: &str, internet: bool) -> VlanDoc {
VlanDoc {
cidr: cidr.into(),
internet: Some(internet),
}
}
#[test]
fn vlans_that_agree_say_their_internet_once_on_their_lan() {
let mut lan = NetworkDoc::new(String::new(), true, None);
lan.vlans.insert(10, vlan("10.10.10.0/24", false));
lan.vlans.insert(20, vlan("10.10.20.0/24", false));
settle_parent(&mut lan);
assert_eq!((lan.cidr.as_str(), lan.internet), ("10.10.0.0/19", Some(false)));
assert!(lan.vlans.values().all(|v| v.internet.is_none()));
}
#[test]
fn vlans_that_disagree_keep_the_default_and_the_one_without_says_so() {
let mut lan = NetworkDoc::new(String::new(), false, None);
lan.vlans.insert(10, vlan("10.10.10.0/24", true));
lan.vlans.insert(20, vlan("10.10.20.0/24", false));
settle_parent(&mut lan);
assert_eq!(lan.internet, None, "yes, Isoloom's default");
assert_eq!(lan.vlans.values().map(|v| v.internet).collect::<Vec<_>>(), [None, Some(false)]);
}
#[test]
fn a_vlan_is_named_under_its_switch_and_a_plain_lan_is_not() {
let lan = |switch: Option<&str>, vlan: Option<u16>| Lan {
cidr: "10.10.10.0/24".into(),
internet: true,
switch: switch.map(str::to_owned),
vlan,
device: None,
tc: None,
};
assert_eq!(vlan_ref(&lan(Some("sw1"), Some(10))).as_deref(), Some("sw1.vlan10"));
assert_eq!(vlan_ref(&lan(Some("sw1"), None)), None);
assert_eq!(vlan_ref(&lan(None, Some(10))), None);
}
}