use std::net::Ipv4Addr;
use isoloom_spec::validate::Cidr;
use crate::document::NetworkDoc;
use crate::draft::Draft;
use crate::{Error, Link, Result, Topology};
const LINK_BASE: [u8; 2] = [10, 200];
pub(crate) const LINK_BLOCK: &str = "10.200.0.0/16";
const LINKS_PER_THIRD: usize = 32;
const MAX_LINKS: usize = 256 * LINKS_PER_THIRD;
fn link_network(i: usize) -> String {
format!("link-{i}")
}
pub(crate) fn is_link_network(name: &str) -> bool {
name.starts_with("link-")
}
fn link_block(i: usize) -> Option<(String, u8, u8)> {
let third = u8::try_from(i / LINKS_PER_THIRD).ok()?;
let fourth = u8::try_from((i % LINKS_PER_THIRD) * 8).ok()?;
let [a, b] = LINK_BASE;
let block = Cidr {
base: u32::from(Ipv4Addr::new(a, b, third, fourth)),
len: 29,
};
Some((block.to_string(), fourth + 2, fourth + 3))
}
pub(crate) fn check_count(topo: &Topology) -> Result<()> {
match topo.links.len() {
n if n > MAX_LINKS => Err(Error::TooManyLinks(n)),
_ => Ok(()),
}
}
pub(crate) fn allocate(topo: &Topology, draft: &mut Draft) -> Result<()> {
let mut used_ports: Vec<(&str, &str)> = Vec::new();
let blocks = (0..).map_while(link_block);
for (i, (link, block)) in topo.links.iter().zip(blocks).enumerate() {
check(topo, i, link, &mut used_ports)?;
cable(draft, i, link, block);
}
Ok(())
}
fn check<'a>(topo: &Topology, i: usize, link: &'a Link, used_ports: &mut Vec<(&'a str, &'a str)>) -> Result<()> {
for end in [&link.a, &link.b] {
if !topo.nodes.contains_key(&end.node) {
return Err(Error::UnknownNode {
link: i,
node: end.node.clone(),
});
}
if used_ports.contains(&(end.node.as_str(), end.port.as_str())) {
return Err(Error::PortInUse {
node: end.node.clone(),
port: end.port.clone(),
});
}
used_ports.push((end.node.as_str(), end.port.as_str()));
}
if link.a.node == link.b.node {
return Err(Error::SelfLink {
link: i,
node: link.a.node.clone(),
});
}
Ok(())
}
fn cable(draft: &mut Draft, i: usize, link: &Link, (cidr, a_octet, b_octet): (String, u8, u8)) {
if draft.is_router(&link.a.node) && draft.is_router(&link.b.node) {
return;
}
let net = link_network(i);
draft.networks.insert(net.clone(), NetworkDoc::new(cidr, false, link.tc.clone()));
draft.attach(&link.a.node, &net, a_octet);
draft.attach(&link.b.node, &net, b_octet);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_last_cable_takes_the_last_block() {
assert_eq!(link_block(MAX_LINKS - 1), Some(("10.200.255.248/29".to_owned(), 250, 251)));
assert_eq!(link_block(MAX_LINKS), None);
}
#[test]
fn the_block_holds_8192_cables() {
let end = |port: &str| crate::Endpoint {
node: "a".into(),
port: port.into(),
};
let mut topo = Topology {
name: "many".into(),
nodes: indexmap::IndexMap::new(),
networks: indexmap::IndexMap::new(),
links: vec![
Link {
a: end("x"),
b: end("y"),
tc: None
};
MAX_LINKS
],
};
assert!(check_count(&topo).is_ok());
topo.links.push(Link {
a: end("x"),
b: end("y"),
tc: None,
});
assert!(matches!(check_count(&topo), Err(Error::TooManyLinks(8193))));
}
#[test]
fn the_blocks_run_through_the_whole_16_in_order() {
let blocks: Vec<_> = (0..).map_while(link_block).collect();
assert_eq!(blocks.len(), MAX_LINKS);
assert_eq!(blocks.first(), Some(&("10.200.0.0/29".to_owned(), 2, 3)));
assert_eq!(blocks.get(32), Some(&("10.200.1.0/29".to_owned(), 2, 3)));
}
#[test]
fn the_cables_block_is_said_as_its_first_octets_make_it() {
let [a, b] = LINK_BASE;
assert_eq!(LINK_BLOCK, format!("{a}.{b}.0.0/16"));
}
#[test]
fn a_name_starting_with_link_is_the_cables() {
assert!(is_link_network("link-0") && is_link_network("link-x"));
assert!(!is_link_network("office") && !is_link_network("uplink-1"));
}
}