feat: Harmony now sets dhcp next boot server for PXE on okd setup

This commit is contained in:
Jean-Gabriel Gill-Couture 2024-12-17 22:45:37 -05:00
parent b15df3c93f
commit 18c67adfad
7 changed files with 123 additions and 82 deletions

View File

@ -7,7 +7,6 @@ use super::{
data::{Id, Version}, data::{Id, Version},
executors::ExecutorError, executors::ExecutorError,
inventory::Inventory, inventory::Inventory,
score::Score,
topology::HAClusterTopology, topology::HAClusterTopology,
}; };
@ -41,9 +40,13 @@ pub struct Outcome {
status: InterpretStatus, status: InterpretStatus,
message: String, message: String,
} }
impl std::fmt::Display for Outcome {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { impl Outcome {
f.write_fmt(format_args!("Outcome {}: {}", self.status, self.message)) pub fn noop() -> Self {
Self {
status: InterpretStatus::NOOP,
message: String::new(),
}
} }
} }
@ -54,6 +57,7 @@ pub enum InterpretStatus {
RUNNING, RUNNING,
QUEUED, QUEUED,
BLOCKED, BLOCKED,
NOOP,
} }
impl std::fmt::Display for InterpretStatus { impl std::fmt::Display for InterpretStatus {
@ -64,6 +68,7 @@ impl std::fmt::Display for InterpretStatus {
InterpretStatus::RUNNING => "RUNNING", InterpretStatus::RUNNING => "RUNNING",
InterpretStatus::QUEUED => "QUEUED", InterpretStatus::QUEUED => "QUEUED",
InterpretStatus::BLOCKED => "BLOCKED", InterpretStatus::BLOCKED => "BLOCKED",
InterpretStatus::NOOP => "NO_OP",
}; };
f.write_str(msg) f.write_str(msg)
} }

View File

@ -45,6 +45,7 @@ pub trait DhcpServer: Send + Sync {
async fn add_static_mapping(&self, entry: &DHCPStaticEntry) -> Result<(), ExecutorError>; async fn add_static_mapping(&self, entry: &DHCPStaticEntry) -> Result<(), ExecutorError>;
async fn remove_static_mapping(&self, mac: &MacAddress) -> Result<(), ExecutorError>; async fn remove_static_mapping(&self, mac: &MacAddress) -> Result<(), ExecutorError>;
async fn list_static_mappings(&self) -> Vec<(MacAddress, IpAddress)>; async fn list_static_mappings(&self) -> Vec<(MacAddress, IpAddress)>;
async fn set_next_server(&self, ip: IpAddress) -> Result<(), ExecutorError>;
fn get_ip(&self) -> IpAddress; fn get_ip(&self) -> IpAddress;
fn get_host(&self) -> LogicalHost; fn get_host(&self) -> LogicalHost;
async fn commit_config(&self) -> Result<(), ExecutorError>; async fn commit_config(&self) -> Result<(), ExecutorError>;

View File

@ -136,9 +136,24 @@ impl DhcpServer for OPNSenseFirewall {
fn get_ip(&self) -> IpAddress { fn get_ip(&self) -> IpAddress {
OPNSenseFirewall::get_ip(self) OPNSenseFirewall::get_ip(self)
} }
fn get_host(&self) -> LogicalHost { fn get_host(&self) -> LogicalHost {
self.host.clone() self.host.clone()
} }
async fn set_next_server(&self, ip: IpAddress) -> Result<(), ExecutorError> {
let ipv4 = match ip {
std::net::IpAddr::V4(ipv4_addr) => ipv4_addr,
std::net::IpAddr::V6(_) => todo!("ipv6 not supported yet"),
};
{
let mut writable_opnsense = self.opnsense_config.write().await;
writable_opnsense.dhcp().set_next_server(ipv4);
debug!("OPNsense dhcp server set next server {ipv4}");
}
Ok(())
}
} }
impl DnsServer for OPNSenseFirewall { impl DnsServer for OPNSenseFirewall {

View File

@ -11,7 +11,7 @@ use crate::{
}, },
interpret::{Interpret, InterpretError, InterpretName, Outcome}, interpret::{Interpret, InterpretError, InterpretName, Outcome},
inventory::Inventory, inventory::Inventory,
topology::{DHCPStaticEntry, HAClusterTopology, HostBinding}, topology::{DHCPStaticEntry, HAClusterTopology, HostBinding, IpAddress},
}; };
use crate::domain::score::Score; use crate::domain::score::Score;
@ -19,6 +19,7 @@ use crate::domain::score::Score;
#[derive(Debug, new, Clone)] #[derive(Debug, new, Clone)]
pub struct DhcpScore { pub struct DhcpScore {
host_binding: Vec<HostBinding>, host_binding: Vec<HostBinding>,
next_server: Option<IpAddress>,
} }
impl Score for DhcpScore { impl Score for DhcpScore {
@ -53,6 +54,67 @@ impl DhcpInterpret {
status: InterpretStatus::QUEUED, status: InterpretStatus::QUEUED,
} }
} }
async fn add_static_entries(
&self,
_inventory: &Inventory,
topology: &HAClusterTopology,
) -> Result<Outcome, InterpretError> {
let dhcp_entries: Vec<DHCPStaticEntry> = self
.score
.host_binding
.iter()
.map(|binding| {
let ip = match binding.logical_host.ip {
std::net::IpAddr::V4(ipv4) => ipv4,
std::net::IpAddr::V6(_) => {
unimplemented!("DHCPStaticEntry only supports ipv4 at the moment")
}
};
DHCPStaticEntry {
name: binding.logical_host.name.clone(),
mac: binding.physical_host.cluster_mac(),
ip,
}
})
.collect();
info!("DHCPStaticEntry : {:?}", dhcp_entries);
let dhcp_server = Arc::new(topology.dhcp_server.clone());
info!("DHCP server : {:?}", dhcp_server);
let number_new_entries = dhcp_entries.len();
for entry in dhcp_entries.into_iter() {
match dhcp_server.add_static_mapping(&entry).await {
Ok(_) => info!("Successfully registered DHCPStaticEntry {}", entry),
Err(_) => todo!(),
}
}
Ok(Outcome::new(
InterpretStatus::SUCCESS,
format!("Dhcp Interpret registered {} entries", number_new_entries),
))
}
async fn set_next_server(
&self,
_inventory: &Inventory,
topology: &HAClusterTopology,
) -> Result<Outcome, InterpretError> {
match self.score.next_server {
Some(next_server) => {
let dhcp_server = Arc::new(topology.dhcp_server.clone());
dhcp_server.set_next_server(next_server).await?;
Ok(Outcome::new(
InterpretStatus::SUCCESS,
format!("Dhcp Interpret Set next boot to {next_server}"),
))
}
None => Ok(Outcome::noop()),
}
}
} }
#[async_trait] #[async_trait]
@ -80,68 +142,15 @@ impl Interpret for DhcpInterpret {
) -> Result<Outcome, InterpretError> { ) -> Result<Outcome, InterpretError> {
info!("Executing {} on inventory {inventory:?}", self.get_name()); info!("Executing {} on inventory {inventory:?}", self.get_name());
let dhcp_entries: Vec<DHCPStaticEntry> = self self.set_next_server(inventory, topology).await?;
.score
.host_binding
.iter()
.map(|binding| {
let ip = match binding.logical_host.ip {
std::net::IpAddr::V4(ipv4) => ipv4,
std::net::IpAddr::V6(_) => {
unimplemented!("DHCPStaticEntry only supports ipv4 at the moment")
}
};
DHCPStaticEntry { // self.add_static_entries(inventory, topology).await?;
name: binding.logical_host.name.clone(),
mac: binding.physical_host.cluster_mac(),
ip,
}
})
.collect();
info!("DHCPStaticEntry : {:?}", dhcp_entries);
let dhcp_server = Arc::new(Box::new(topology.dhcp_server.clone())); topology.dhcp_server.commit_config().await?;
info!("DHCP server : {:?}", dhcp_server);
let number_new_entries = dhcp_entries.len();
for entry in dhcp_entries.into_iter() {
match dhcp_server.add_static_mapping(&entry).await {
Ok(_) => info!("Successfully registered DHCPStaticEntry {}", entry),
Err(_) => todo!(),
}
}
dhcp_server.commit_config().await;
Ok(Outcome::new( Ok(Outcome::new(
InterpretStatus::SUCCESS, InterpretStatus::SUCCESS,
format!("Dhcp Interpret registered {} entries", number_new_entries), format!("Dhcp Interpret execution successful"),
)) ))
} }
} }
#[cfg(test)]
mod test {
#[test]
fn opnsense_dns_score_should_do_nothing_on_empty_inventory() {
todo!();
}
#[test]
fn opnsense_dns_score_should_set_entry_for_bootstrap_node() {
todo!();
}
#[test]
fn opnsense_dns_score_should_set_entry_for_control_plane_members() {
todo!();
}
#[test]
fn opnsense_dns_score_should_set_entry_for_workers() {
todo!();
}
}

View File

@ -12,25 +12,25 @@ pub struct OKDBootstrapDhcpScore {
impl OKDBootstrapDhcpScore { impl OKDBootstrapDhcpScore {
pub fn new(topology: &HAClusterTopology, inventory: &Inventory) -> Self { pub fn new(topology: &HAClusterTopology, inventory: &Inventory) -> Self {
let host_binding = topology
.control_plane
.iter()
.enumerate()
.map(|(index, topology_entry)| HostBinding {
logical_host: topology_entry.clone(),
physical_host: inventory
.control_plane_host
.get(index)
.expect("Iventory should contain at least as many physical hosts as topology")
.clone(),
})
.collect();
Self { Self {
dhcp_score: DhcpScore::new( dhcp_score: DhcpScore::new(
topology host_binding,
.control_plane // TODO : we should add a tftp server to the topology instead of relying on the
.iter() // router address, this is leaking implementation details
.enumerate() Some(topology.router.get_gateway()),
.map(|(index, topology_entry)| {
HostBinding {
logical_host: topology_entry.clone(),
physical_host: inventory
.control_plane_host
.get(index)
.expect(
"Iventory should contain at least as many physical hosts as topology",
)
.clone(),
}
})
.collect(),
), ),
} }
} }

View File

@ -16,6 +16,8 @@ pub struct DhcpInterface {
pub enable: Option<MaybeString>, pub enable: Option<MaybeString>,
pub gateway: Option<MaybeString>, pub gateway: Option<MaybeString>,
pub domain: Option<MaybeString>, pub domain: Option<MaybeString>,
pub netboot: Option<u32>,
pub nextserver: Option<String>,
#[yaserde(rename = "ddnsdomainalgorithm")] #[yaserde(rename = "ddnsdomainalgorithm")]
pub ddns_domain_algorithm: Option<MaybeString>, pub ddns_domain_algorithm: Option<MaybeString>,
#[yaserde(rename = "numberoptions")] #[yaserde(rename = "numberoptions")]

View File

@ -54,7 +54,9 @@ impl<'a> DhcpConfig<'a> {
pub fn remove_static_mapping(&mut self, mac: &str) { pub fn remove_static_mapping(&mut self, mac: &str) {
let lan_dhcpd = self.get_lan_dhcpd(); let lan_dhcpd = self.get_lan_dhcpd();
lan_dhcpd.staticmaps.retain(|static_entry| static_entry.mac != mac); lan_dhcpd
.staticmaps
.retain(|static_entry| static_entry.mac != mac);
} }
fn get_lan_dhcpd(&mut self) -> &mut opnsense_config_xml::DhcpInterface { fn get_lan_dhcpd(&mut self) -> &mut opnsense_config_xml::DhcpInterface {
@ -77,7 +79,6 @@ impl<'a> DhcpConfig<'a> {
let mac = mac.to_string(); let mac = mac.to_string();
let hostname = hostname.to_string(); let hostname = hostname.to_string();
let lan_dhcpd = self.get_lan_dhcpd(); let lan_dhcpd = self.get_lan_dhcpd();
let range = &lan_dhcpd.range;
let existing_mappings: &mut Vec<StaticMap> = &mut lan_dhcpd.staticmaps; let existing_mappings: &mut Vec<StaticMap> = &mut lan_dhcpd.staticmaps;
if !Self::is_valid_mac(&mac) { if !Self::is_valid_mac(&mac) {
@ -87,7 +88,7 @@ impl<'a> DhcpConfig<'a> {
// TODO verify if address is in subnet range // TODO verify if address is in subnet range
// This check here does not do what we want to do, as we want to assign static leases // This check here does not do what we want to do, as we want to assign static leases
// outside of the dynamic DHCP pool // outside of the dynamic DHCP pool
// // let range = &lan_dhcpd.range;
// if !Self::is_ip_in_range(&ipaddr, range) { // if !Self::is_ip_in_range(&ipaddr, range) {
// return Err(DhcpError::IpAddressOutOfRange(ipaddr.to_string())); // return Err(DhcpError::IpAddressOutOfRange(ipaddr.to_string()));
// } // }
@ -177,6 +178,14 @@ impl<'a> DhcpConfig<'a> {
Ok(static_maps) Ok(static_maps)
} }
pub fn enable_netboot(&mut self) {
self.get_lan_dhcpd().netboot = Some(1);
}
pub fn set_next_server(&mut self, ip: Ipv4Addr) {
self.enable_netboot();
self.get_lan_dhcpd().nextserver = Some(ip.to_string());
}
} }
#[cfg(test)] #[cfg(test)]