use crate::Config;
use anyhow::{Context, Result, bail};
use ipnetwork::Ipv4Network;
use log::{debug, warn};
use nix::unistd::gethostname;
use std::{
net::{Ipv4Addr, SocketAddr},
process::Command,
};
#[derive(Clone)]
#[must_use]
pub struct Network {
cluster_cidr: Ipv4Network,
crio_cidrs: Vec<Ipv4Network>,
service_cidr: Ipv4Network,
etcd_client: SocketAddr,
etcd_peer: SocketAddr,
hostname: String,
}
impl Network {
pub const INTERFACE_PREFIX: &'static str = "kubernix";
pub fn cluster_cidr(&self) -> &Ipv4Network {
&self.cluster_cidr
}
pub fn crio_cidrs(&self) -> &[Ipv4Network] {
&self.crio_cidrs
}
pub fn service_cidr(&self) -> &Ipv4Network {
&self.service_cidr
}
pub fn etcd_client(&self) -> &SocketAddr {
&self.etcd_client
}
pub fn etcd_peer(&self) -> &SocketAddr {
&self.etcd_peer
}
pub fn hostname(&self) -> &str {
&self.hostname
}
fn subnet_prefix(parent_prefix: u8, nodes: u8) -> Result<u8> {
let required = 2 + u32::from(nodes); for prefix in parent_prefix..=30 {
let subnets_available = 1u32 << (prefix - parent_prefix);
if subnets_available >= required {
return Ok(prefix);
}
}
bail!(
"CIDR /{} is too small to fit {} required subnets",
parent_prefix,
required,
)
}
pub fn new(config: &Config) -> Result<Self> {
let subnet_prefix = Self::subnet_prefix(config.cidr().prefix(), config.nodes())?;
Self::warn_overlapping_route(config.cidr())?;
let cluster_cidr = Ipv4Network::new(config.cidr().ip(), subnet_prefix)?;
debug!("Using cluster CIDR {}", cluster_cidr);
let service_cidr = Ipv4Network::new(
config
.cidr()
.nth(cluster_cidr.size())
.context("Unable to retrieve service CIDR start IP")?,
subnet_prefix,
)?;
debug!("Using service CIDR {}", service_cidr);
let mut crio_cidrs = vec![];
let mut offset = cluster_cidr.size() + service_cidr.size();
for node in 0..config.nodes() {
let cidr = Ipv4Network::new(
config
.cidr()
.nth(offset)
.context("Unable to retrieve CRI-O CIDR start IP")?,
subnet_prefix,
)?;
offset += cidr.size();
debug!("Using CRI-O ({}) CIDR {}", node, cidr);
crio_cidrs.push(cidr);
}
let etcd_client = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 2379);
let etcd_peer = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 2380);
let hostname = gethostname()
.context("Unable to get hostname")?
.to_str()
.context("Unable to convert hostname into string")?
.into();
Ok(Self {
cluster_cidr,
crio_cidrs,
service_cidr,
etcd_client,
etcd_peer,
hostname,
})
}
fn warn_overlapping_route(cidr: Ipv4Network) -> Result<()> {
let cmd = Command::new("ip")
.arg("route")
.output()
.context("Failed to run 'ip route'; is iproute2 installed?")?;
if !cmd.status.success() {
bail!(
"'ip route' exited with status {} (stderr: {})",
cmd.status,
String::from_utf8_lossy(&cmd.stderr),
)
}
String::from_utf8(cmd.stdout)?
.lines()
.filter(|x| !x.contains(Self::INTERFACE_PREFIX))
.filter_map(|x| x.split_whitespace().next())
.filter_map(|x| x.parse::<Ipv4Network>().ok())
.filter(|x| x.is_supernet_of(cidr))
.for_each(|x| {
warn!(
"There seems to be an overlapping IP route {}. {}",
x, "the cluster may not work as expected",
);
});
Ok(())
}
pub fn api(&self) -> Result<Ipv4Addr> {
self.service_cidr().nth(1).with_context(|| {
format!(
"Unable to retrieve first IP from service CIDR: {}",
self.service_cidr()
)
})
}
pub fn dns(&self) -> Result<Ipv4Addr> {
self.service_cidr().nth(2).with_context(|| {
format!(
"Unable to retrieve second IP from service CIDR: {}",
self.service_cidr()
)
})
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::config::tests::{test_config, test_config_wrong_cidr};
pub fn test_network() -> Result<Network> {
let c = test_config()?;
Network::new(&c)
}
#[test]
fn new_success() -> Result<()> {
let c = test_config()?;
let n = Network::new(&c)?;
assert_eq!(
n.cluster_cidr().to_string(),
"10.10.0.0/18",
"cluster CIDR should be the first /18 subnet"
);
assert_eq!(
n.service_cidr().to_string(),
"10.10.64.0/18",
"service CIDR should be the second /18 subnet"
);
assert_eq!(
n.crio_cidrs().len(),
1,
"single node should have one CRI-O CIDR"
);
assert_eq!(
n.crio_cidrs()[0].to_string(),
"10.10.128.0/18",
"CRI-O CIDR should be the third /18 subnet"
);
Ok(())
}
#[test]
fn new_failure() -> Result<()> {
let c = test_config_wrong_cidr()?;
assert!(Network::new(&c).is_err());
Ok(())
}
#[test]
fn api_success() -> Result<()> {
let c = test_config()?;
let n = Network::new(&c)?;
assert_eq!(n.api()?, Ipv4Addr::new(10, 10, 64, 1));
Ok(())
}
#[test]
fn dns_success() -> Result<()> {
let c = test_config()?;
let n = Network::new(&c)?;
assert_eq!(n.dns()?, Ipv4Addr::new(10, 10, 64, 2));
Ok(())
}
#[test]
fn subnet_prefix_single_node() -> Result<()> {
assert_eq!(Network::subnet_prefix(16, 1)?, 18);
Ok(())
}
#[test]
fn subnet_prefix_many_nodes() -> Result<()> {
assert_eq!(Network::subnet_prefix(16, 6)?, 19);
Ok(())
}
#[test]
fn subnet_prefix_too_small() {
assert!(Network::subnet_prefix(30, 1).is_err());
}
}