use super::Problems;
use crate::model::{Network, Spec};
pub(super) fn networks(spec: &Spec, p: &mut Problems) {
for (name, net) in &spec.networks {
let at = format!("networks.{name}");
if net.external.is_some() && net.bridged.is_some() {
p.add(
&at,
"a network is either `external` (an existing network, joined by name) or `bridged` (to a host interface), not both",
);
}
if let Some(e) = &net.external
&& !word(e)
{
p.add(format!("{at}.external"), "the existing network's name, like `corp-net` or `vmbr1` (no spaces)");
}
if let Some(b) = &net.bridged {
if b.interface.as_deref().is_some_and(|i| i.trim().is_empty()) {
p.add(format!("{at}.bridged.interface"), "name the host interface, or leave `interface` out");
}
if b.default_route && !b.dhcp {
p.add(
format!("{at}.bridged.default_route"),
"the default route comes from the LAN's DHCP server: set `dhcp: true`, or leave `default_route` out",
);
}
if b.vlan.is_some_and(|v| !(1..=4094).contains(&v)) {
p.add(format!("{at}.bridged.vlan"), "a VLAN tag is 1 to 4094");
}
}
if net.is_outside() {
own_settings(&at, name, net, p);
}
}
}
fn word(s: &str) -> bool {
!s.is_empty() && !s.chars().any(char::is_whitespace)
}
fn own_settings(at: &str, name: &str, net: &Network, p: &mut Problems) {
let why = format!("`{name}` is outside the environment: its own routing decides");
if net.gateway.is_some() {
p.add(format!("{at}.gateway"), format!("{why}, so leave `gateway` out"));
}
if !net.internet {
p.add(format!("{at}.internet"), format!("{why}, so leave `internet` out"));
}
if net.tc.is_some() {
p.add(format!("{at}.tc"), format!("{why}, so leave `tc` out (it applies on Isoloom's router)"));
}
if net.docker.is_some() {
p.add(
format!("{at}.docker"),
format!("`{name}` keeps its own block on Docker: leave its `cidr` out of `overrides.docker`"),
);
}
if net.switch.is_some() {
p.add(format!("{at}.switch"), format!("{why}, so leave `switch` out"));
}
if net.ipv6.is_some() {
p.add(
format!("{at}.ipv6"),
format!("`{name}` is outside the environment: its IPv6 is its own, so leave `ipv6` out"),
);
}
}
pub(super) fn reach(spec: &Spec, p: &mut Problems) {
for (i, r) in spec.reach.iter().enumerate() {
for (field, net) in [("from", &r.from), ("to", &r.to)] {
if spec.networks.get(net).is_some_and(Network::is_outside) {
p.add(
format!("reach[{i}].{field}"),
format!("`{net}` is outside the environment: Isoloom's router isn't on it, so its own routing decides"),
);
}
}
}
}
pub(super) fn first_networks(spec: &Spec, p: &mut Problems) {
for (name, m) in &spec.machines {
let Some((first, _)) = m.networks.first() else { continue };
if spec.networks.get(first).is_some_and(Network::is_outside) {
p.add(
format!("machines.{name}.networks"),
format!("`{first}` is outside the environment: list one of the environment's own networks first (a machine is reached on its first network)"),
);
}
}
}
pub(super) fn dhcp_address(spec: &Spec, net: &str, octet: u8, at: &str, p: &mut Problems) -> bool {
let dhcp = spec.networks.get(net).is_some_and(Network::is_dhcp);
match (dhcp, octet) {
(true, 0) => {}
(true, _) => p.add(at, format!("`{net}` gives addresses by DHCP: write `{net}: 0`")),
(false, 0) => p.add(at, "0 asks for an address by DHCP, on a `bridged: { dhcp: true }` network only"),
(false, _) => return false,
}
true
}
#[cfg(test)]
mod tests {
use crate::testing::model as parse;
use crate::validate;
fn problems(yaml: &str) -> Vec<String> {
validate(&parse(yaml).unwrap()).into_iter().map(|p| p.to_string()).collect()
}
const BASE: &str = "version: 1\nname: t\nnetworks:\n lan: { cidr: 10.20.0.0/24 }\n";
#[test]
fn an_outside_network_is_valid_after_an_own_one() {
let spec = format!(
"{BASE} corp: {{ cidr: 192.168.50.0/24, external: corp-net }}\n home: {{ cidr: 10.0.0.0/24, bridged: {{ interface: 'en0: Wi-Fi', dhcp: true, default_route: true }} }}\nmachines:\n a: {{ networks: {{ lan: 10, corp: 10, home: 0 }}, docker: {{ image: nginx }} }}\n"
);
assert_eq!(problems(&spec), Vec::<String>::new());
}
#[test]
fn outside_networks_refuse_what_only_own_networks_take() {
let spec = format!(
"{BASE} corp: {{ cidr: 192.168.50.0/24, external: corp-net, bridged: {{ default_route: true }}, internet: false, tc: {{ delay: 5ms }} }}\nreach: [{{ from: corp, to: lan }}]\nmachines:\n a: {{ networks: {{ corp: 10, lan: 0 }}, docker: {{ image: nginx }} }}\n"
);
let got = problems(&spec);
for want in [
"networks.corp: a network is either `external`",
"networks.corp.bridged.default_route: the default route comes from the LAN's DHCP server",
"networks.corp.internet: `corp` is outside the environment",
"networks.corp.tc: `corp` is outside the environment",
"reach[0].from: `corp` is outside the environment",
"machines.a.networks: `corp` is outside the environment: list one of the environment's own networks first",
"machines.a.networks.lan: 0 asks for an address by DHCP",
] {
assert!(got.iter().any(|g| g.starts_with(want)), "missing {want:?} in {got:#?}");
}
}
#[test]
fn a_dhcp_network_takes_zero_and_a_name_has_no_spaces() {
let spec = format!(
"{BASE} home: {{ cidr: 10.0.0.0/24, bridged: {{ dhcp: true }} }}\n corp: {{ cidr: 10.30.0.0/24, external: 'corp net' }}\nmachines:\n a: {{ networks: {{ lan: 10, home: 10 }}, docker: {{ image: nginx }} }}\n"
);
let got = problems(&spec);
assert!(
got.contains(&"machines.a.networks.home: `home` gives addresses by DHCP: write `home: 0`".to_owned()),
"{got:#?}"
);
assert!(
got.iter().any(|g| g.starts_with("networks.corp.external: the existing network's name")),
"{got:#?}"
);
}
#[test]
fn a_bridged_networks_vlan_tag_is_1_to_4094() {
let tag = |vlan: u16| {
problems(&format!(
"{BASE} home: {{ cidr: 10.0.0.0/24, bridged: {{ vlan: {vlan} }} }}\nmachines:\n a: {{ networks: {{ lan: 10, home: 10 }}, docker: {{ image: nginx }} }}\n"
))
};
assert_eq!(tag(1), Vec::<String>::new());
assert_eq!(tag(4094), Vec::<String>::new());
assert_eq!(tag(0), ["networks.home.bridged.vlan: a VLAN tag is 1 to 4094"]);
assert_eq!(tag(4095), ["networks.home.bridged.vlan: a VLAN tag is 1 to 4094"]);
}
}