use std::net::Ipv4Addr;
use isoloom_plan::addressing::cidr;
use isoloom_spec::images::is_windows;
use isoloom_spec::model::{Machine, Spec};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Vlan {
pub network: String,
pub id: u16,
pub octet: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Trunk {
pub lan: String,
pub machine: String,
pub vlans: Vec<Vlan>,
}
#[must_use]
pub fn containers(spec: &Spec) -> Vec<Trunk> {
of_machines(spec, |m| m.docker.is_some())
}
#[must_use]
pub fn vms(spec: &Spec) -> Vec<Trunk> {
of_machines(spec, |m| m.vm.as_ref().is_some_and(|vm| !is_windows(&vm.os)))
}
fn of_machines(spec: &Spec, kept: impl Fn(&Machine) -> bool) -> Vec<Trunk> {
let mut out: Vec<Trunk> = Vec::new();
for (name, m) in spec.machines.iter().filter(|(_, m)| kept(m)) {
for (net, octet) in &m.networks {
let Some((lan, id)) = spec.networks.get(net).and_then(|n| n.vlan.as_ref()) else {
continue;
};
let vlan = Vlan {
network: net.clone(),
id: *id,
octet: *octet,
};
match out.iter_mut().find(|t| &t.machine == name && &t.lan == lan) {
Some(t) => t.vlans.push(vlan),
None => out.push(Trunk {
lan: lan.clone(),
machine: name.clone(),
vlans: vec![vlan],
}),
}
}
}
out.retain(|t| t.vlans.len() >= 2);
out
}
pub fn of<'a>(trunks: &'a [Trunk], machine: &str) -> impl Iterator<Item = &'a Trunk> {
trunks.iter().filter(move |t| t.machine == machine)
}
#[must_use]
pub fn carried(trunks: &[Trunk], machine: &str, network: &str) -> bool {
of(trunks, machine).any(|t| t.vlans.iter().any(|v| v.network == network))
}
#[must_use]
pub fn link_network(t: &Trunk) -> String {
format!("isoloom-trunk-{}-{}", t.lan, t.machine)
}
#[must_use]
pub fn switch_name(lan: &str) -> String {
format!("isoloom-switch-{lan}")
}
#[must_use]
pub fn interface(lan: &str) -> String {
lan.chars().take(10).collect::<String>().trim_end_matches('-').to_owned()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum End {
Machine,
Switch,
}
#[must_use]
pub fn mac(t: &Trunk, end: End) -> String {
let hash = format!("{}/{}", t.lan, t.machine)
.bytes()
.fold(2_166_136_261_u32, |hash, byte| (hash ^ u32::from(byte)).wrapping_mul(16_777_619));
let [first, second, third, last] = hash.to_be_bytes();
let side = match end {
End::Machine => 0,
End::Switch => 1,
};
format!("02:1e:{first:02x}:{second:02x}:{third:02x}:{:02x}", (last & 0xfe) | side)
}
fn find_by_mac(mac: &str) -> String {
format!("IF=$(ip -o link | awk 'tolower($0) ~ /{mac}/ {{print $2}}' | cut -d@ -f1 | tr -d : | head -n 1)")
}
fn find_by_address(addr: Ipv4Addr) -> String {
format!(
"IF=$(ip -o -4 addr show | awk '$4 ~ /^{}\\//{{print $2}}' | head -n 1)",
addr.to_string().replace('.', "\\.")
)
}
#[must_use]
pub fn switch_address(spec: &Spec, network: &str) -> Ipv4Addr {
cidr(spec, network).controller()
}
#[must_use]
pub fn switch_script(spec: &Spec, trunks: &[&Trunk]) -> String {
let mut bridged: Vec<&Vlan> = Vec::new();
for v in trunks.iter().flat_map(|t| &t.vlans) {
if !bridged.iter().any(|b| b.id == v.id) {
bridged.push(v);
}
}
let bridge_lines = bridged.iter().map(|v| {
format!(
"{} && ip addr flush dev $IF && ip link add br{id} type bridge && ip link set $IF master br{id} && ip link set br{id} up && ",
find_by_address(switch_address(spec, &v.network)),
id = v.id
)
});
let trunk_lines = trunks.iter().map(|t| {
let tagged: String = t
.vlans
.iter()
.map(|v| {
format!(
"ip link add link $IF name $IF.{id} type vlan id {id} && ip link set $IF.{id} master br{id} && ip link set $IF.{id} up && ",
id = v.id
)
})
.collect::<Vec<_>>()
.concat();
format!(
"{} && ip addr flush dev $IF && ip link set $IF up && {tagged}",
find_by_mac(&mac(t, End::Switch))
)
});
bridge_lines
.chain(trunk_lines)
.chain(std::iter::once("exec sleep infinity".to_owned()))
.collect()
}
#[must_use]
pub fn switch_ready(trunks: &[&Trunk]) -> String {
let mut ids: Vec<u16> = trunks.iter().flat_map(|t| t.vlans.iter().map(|v| v.id)).collect();
ids.sort_unstable();
ids.dedup();
ids.iter()
.map(|id| format!("ip link show br{id} | grep -q 'state UP'"))
.collect::<Vec<_>>()
.join(" && ")
}
#[must_use]
pub fn machine_commands(spec: &Spec, m: &Machine, t: &Trunk, address: impl Fn(&str, u8) -> Ipv4Addr, gateway: impl Fn(&str) -> Ipv4Addr) -> Vec<String> {
let name = interface(&t.lan);
let mut cmds = vec![format!(
"{} && ip addr flush dev $IF && ip link set $IF down && ip link set $IF name {name} && ip link set {name} up",
find_by_mac(&mac(t, End::Machine))
)];
for v in &t.vlans {
cmds.push(format!(
"ip link add link {name} name {name}.{id} type vlan id {id} && ip addr add {}/{} dev {name}.{id} && ip link set {name}.{id} up",
address(&v.network, v.octet),
cidr(spec, &v.network).len,
id = v.id
));
}
let internet = |n: &str| spec.networks.get(n).is_some_and(|net| net.internet);
let other_way_out = m.networks.keys().any(|n| !t.vlans.iter().any(|v| &v.network == n) && internet(n));
if !other_way_out && let Some(v) = t.vlans.iter().find(|v| internet(&v.network)) {
cmds.push(format!("ip route replace default via {}", gateway(&v.network)));
}
cmds
}
#[must_use]
pub fn machine_ready(t: &Trunk) -> String {
let name = interface(&t.lan);
t.vlans
.iter()
.map(|v| format!("ip link show {name}.{} | grep -q 'UP'", v.id))
.collect::<Vec<_>>()
.join(" && ")
}
fn vm_peer(lan: &str) -> String {
format!("{}-s", lan.chars().take(8).collect::<String>().trim_end_matches('-'))
}
#[must_use]
pub fn vm_commands(spec: &Spec, t: &Trunk, address: impl Fn(&str, u8) -> Ipv4Addr) -> Vec<String> {
let name = interface(&t.lan);
let peer = vm_peer(&t.lan);
let mut steps = vec![
format!("ip link add {name} type veth peer name {peer}"),
format!("ip link set {name} up"),
format!("ip link set {peer} up"),
];
for v in &t.vlans {
let (id, addr) = (v.id, address(&v.network, v.octet));
let len = cidr(spec, &v.network).len;
let local = format!("02:1f:{}", addr.octets().map(|o| format!("{o:02x}")).join(":"));
steps.push(format!(
"{} && MAC=$(cat \"/sys/class/net/$IF/address\") && ip addr flush dev \"$IF\" && ip link set \"$IF\" address {local}",
find_by_address(addr)
));
steps.push(format!(
"ip link add br{id} type bridge && ip link set \"$IF\" master br{id} && ip link set br{id} up"
));
steps.push(format!(
"ip link add link {peer} name {peer}.{id} type vlan id {id} && ip link set {peer}.{id} master br{id} && ip link set {peer}.{id} up"
));
steps.push(format!(
"ip link add link {name} name {name}.{id} address \"$MAC\" type vlan id {id} && ip addr add {addr}/{len} dev {name}.{id} && ip link set {name}.{id} up"
));
}
vec![format!("ip link show {name} >/dev/null 2>&1 || {{ {}; }}", steps.join(" && "))]
}
#[cfg(test)]
mod tests;