use std::net::Ipv4Addr;
use isoloom_plan::Environment;
use isoloom_render::container::UTILITY_IMAGE;
use isoloom_render::{router, trunks};
use isoloom_spec::{validate, vlans};
use crate::file::{Condition, Healthcheck, NetworkMode, Restart, Seconds, Service};
use crate::images::TC_IMAGE;
use crate::interpolation::literal;
use crate::machine::Machine;
use crate::render::readiness;
use crate::{appliances, ipv6, networks};
pub(crate) fn needed(env: &Environment, m: Machine<'_>) -> bool {
!commands(env, m).is_empty()
}
pub(crate) fn routes(env: &Environment, host: &str, m: Machine<'_>) -> Service {
let spec = env.spec();
let image = if impaired(env, m) { TC_IMAGE } else { UTILITY_IMAGE };
let family = if m.plan.addresses.keys().any(|n| ipv6::only(env, n)) { "" } else { "-4 " };
let script = format!(
"{} && while ip -o {family}addr show | grep -qv ' lo '; do sleep 2; done; exit 1",
commands(env, m).join(" && ")
);
let mut r = Service {
image: Some(env.images.pull(image)),
network_mode: Some(NetworkMode::Service(host.to_owned())),
cap_add: vec!["NET_ADMIN".into()],
init: true,
entrypoint: vec!["/bin/sh".into(), "-c".into(), script],
healthcheck: Some(Healthcheck::shell(ready(env, m), Seconds(2), 30)),
restart: Some(Restart::UnlessStopped),
..Service::default()
};
r.after(host, Condition::Started);
let ts = trunks::containers(spec);
for t in trunks::of(&ts, m.name) {
match vlans::switch_of(spec, &t.lan) {
Some(sw) => r.after(sw, Condition::Started),
None => r.after(trunks::switch_name(&t.lan), Condition::Healthy),
}
}
let through_router = m.spec.networks.keys().any(|n| env.networks.get(n).is_some_and(|p| p.gateway.is_none()));
if env.router.is_some() && through_router {
r.after(router::NAME, Condition::Healthy);
}
for gw in validate::starts_after(spec, m.name, m.spec).into_iter().filter_map(|g| Machine::of(env, g)) {
if m.spec.depends_on.iter().any(|d| d == gw.name) || !gw.plan.is_gateway || gw.spec.docker.is_none() {
continue;
}
r.after(gw.name, readiness(gw.spec));
}
r
}
fn own_default(env: &Environment, m: Machine<'_>) -> Option<Ipv4Addr> {
if !m.plan.is_gateway || m.plan.default_gateway.is_some() {
return None;
}
m.plan
.addresses
.keys()
.filter_map(|n| env.networks.get(n))
.find(|n| n.gateway.is_none() && n.internet)
.map(|n| n.host)
}
fn drops_default(env: &Environment, m: Machine<'_>) -> bool {
m.plan.offline && m.plan.addresses.keys().any(|n| !networks::internal(env, n))
}
fn on_tc(env: &Environment, m: Machine<'_>) -> bool {
m.plan.addresses.keys().any(|n| env.networks.get(n).is_some_and(|p| p.tc.is_some()))
}
fn commands(env: &Environment, m: Machine<'_>) -> Vec<String> {
let spec = env.spec();
let address = |n: &str, o: u8| isoloom_plan::addressing::address(spec, n, o);
if m.is_appliance() {
return appliances::sidecar_commands(appliances::ports(spec, m.name, m.spec, &address).len());
}
let ts = trunks::containers(spec);
let mut cmds: Vec<String> = trunks::of(&ts, m.name)
.flat_map(|t| trunks::machine_commands(spec, m.spec, t, address, |n| networks::docker_address(env, n)))
.map(|c| literal(&c))
.collect();
cmds.extend(m.plan.routes.iter().map(|r| format!("ip route replace {} via {}", r.cidr, r.via)));
cmds.extend(ipv6::route_commands(m.plan));
if let Some(gw) = m.plan.default_gateway.or_else(|| own_default(env, m)) {
cmds.push(format!("ip route replace default via {gw}"));
}
if drops_default(env, m) {
cmds.push("(ip route del default 2>/dev/null || true)".into());
cmds.extend(ipv6::on_ipv6(env, m.plan).then(|| "(ip -6 route del default 2>/dev/null || true)".into()));
}
if let Some(script) = router::machine_tc_script(spec, m.spec, address) {
cmds.extend(script.lines().map(|l| format!("({})", literal(l))));
}
cmds
}
fn ready(env: &Environment, m: Machine<'_>) -> String {
if m.is_appliance() {
return "! ip -o -4 addr show | grep -q ' eth[1-9]'".into();
}
let mut ready = Vec::new();
if let Some(first) = m.plan.routes.first() {
ready.push(format!("ip route show {} | grep -q via", first.cidr));
}
if let Some(first) = m.plan.routes6.first() {
ready.push(format!("ip -6 route show {} | grep -q via", first.cidr));
}
if let Some(gw) = m.plan.default_gateway.or_else(|| own_default(env, m)) {
ready.push(format!("ip route | grep -q '^default via {gw} '"));
}
if drops_default(env, m) {
ready.push("! ip route | grep -q '^default'".into());
ready.extend(ipv6::on_ipv6(env, m.plan).then(|| "! ip -6 route | grep -q '^default'".into()));
}
if on_tc(env, m) {
ready.push("tc qdisc show | grep -q netem".into());
}
let ts = trunks::containers(env.spec());
ready.extend(trunks::of(&ts, m.name).map(trunks::machine_ready));
ready.join(" && ")
}
fn impaired(env: &Environment, m: Machine<'_>) -> bool {
let vlans = m
.plan
.addresses
.keys()
.filter(|n| env.networks.get(*n).is_some_and(|p| p.vlan.is_some()))
.count();
on_tc(env, m) || m.plan.shape == isoloom_plan::MachineShape::Container && vlans >= 2
}