diff --git a/Cargo.lock b/Cargo.lock index 62d8aee..844651a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7049,7 +7049,7 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "yaserde" version = "0.12.0" -source = "git+https://github.com/jggc/yaserde.git#c94ca32b6505f9c9a668702a1b1f1f88c6374301" +source = "git+https://github.com/jggc/yaserde.git#adfdb1c5f4d054f114e5bd0ea7bda9c07a369def" dependencies = [ "log", "xml-rs", @@ -7058,7 +7058,7 @@ dependencies = [ [[package]] name = "yaserde_derive" version = "0.12.0" -source = "git+https://github.com/jggc/yaserde.git#c94ca32b6505f9c9a668702a1b1f1f88c6374301" +source = "git+https://github.com/jggc/yaserde.git#adfdb1c5f4d054f114e5bd0ea7bda9c07a369def" dependencies = [ "heck", "log", diff --git a/harmony/src/domain/hardware/mod.rs b/harmony/src/domain/hardware/mod.rs index 3a14e1a..4ad7836 100644 --- a/harmony/src/domain/hardware/mod.rs +++ b/harmony/src/domain/hardware/mod.rs @@ -173,6 +173,10 @@ impl PhysicalHost { self } + pub fn get_mac_address(&self) -> Vec { + self.network.iter().map(|nic| nic.mac_address).collect() + } + pub fn label(mut self, name: String, value: String) -> Self { self.labels.push(Label { name, value }); self diff --git a/harmony/src/modules/dhcp.rs b/harmony/src/modules/dhcp.rs index 9ef45d3..b48f8e1 100644 --- a/harmony/src/modules/dhcp.rs +++ b/harmony/src/modules/dhcp.rs @@ -52,6 +52,104 @@ impl DhcpInterpret { status: InterpretStatus::QUEUED, } } + + async fn set_pxe_options( + &self, + _inventory: &Inventory, + dhcp_server: &D, + ) -> Result { + todo!( + "I don't think this set_pxe_options function still works since the major dnsmasq refactoring. It certainly is not far off, but we use the dedicated okd ipxe score now. They should work together, this needs refactoring." + ); + let pxe_options = PxeOptions { + ipxe_filename: self.score.filenameipxe.clone().unwrap_or_default(), + bios_filename: self.score.filename.clone().unwrap_or_default(), + efi_filename: self.score.filename64.clone().unwrap_or_default(), + tftp_ip: self.score.next_server, + }; + + dhcp_server.set_pxe_options(pxe_options).await?; + + Ok(Outcome::new( + InterpretStatus::SUCCESS, + format!( + "Dhcp Interpret Set next boot to [{:?}], boot_filename to [{:?}], filename to [{:?}], filename64 to [{:?}], filenameipxe to [:{:?}]", + self.score.boot_filename, + self.score.boot_filename, + self.score.filename, + self.score.filename64, + self.score.filenameipxe + ), + )) + } +} + +#[async_trait] +impl Interpret for DhcpInterpret { + fn get_name(&self) -> InterpretName { + InterpretName::OPNSenseDHCP + } + + fn get_version(&self) -> crate::domain::data::Version { + self.version.clone() + } + + fn get_status(&self) -> InterpretStatus { + self.status.clone() + } + + fn get_children(&self) -> Vec { + todo!() + } + + async fn execute( + &self, + inventory: &Inventory, + topology: &T, + ) -> Result { + info!("Executing DhcpInterpret on inventory {inventory:?}"); + + self.set_pxe_options(inventory, topology).await?; + + DhcpHostBindingScore { + host_binding: self.score.host_binding.clone(), + } + .interpret(inventory, topology) + .await?; + + topology.commit_config().await?; + + Ok(Outcome::new( + InterpretStatus::SUCCESS, + "Dhcp Interpret execution successful".to_string(), + )) + } +} + +#[derive(Debug, new, Clone, Serialize)] +pub struct DhcpHostBindingScore { + pub host_binding: Vec, +} + +impl Score for DhcpHostBindingScore { + fn create_interpret(&self) -> Box> { + Box::new(DhcpHostBindingInterpret { + score: self.clone(), + }) + } + + fn name(&self) -> String { + "DhcpHostBindingScore".to_string() + } +} + +// https://docs.opnsense.org/manual/dhcp.html#advanced-settings +#[derive(Debug, Clone)] +pub struct DhcpHostBindingInterpret { + score: DhcpScore, +} + +impl DhcpHostBindingInterpret { async fn add_static_entries( &self, _inventory: &Inventory, @@ -94,47 +192,20 @@ impl DhcpInterpret { format!("Dhcp Interpret registered {} entries", number_new_entries), )) } - - async fn set_pxe_options( - &self, - _inventory: &Inventory, - dhcp_server: &D, - ) -> Result { - let pxe_options = PxeOptions { - ipxe_filename: self.score.filenameipxe.clone().unwrap_or_default(), - bios_filename: self.score.filename.clone().unwrap_or_default(), - efi_filename: self.score.filename64.clone().unwrap_or_default(), - tftp_ip: self.score.next_server, - }; - - dhcp_server.set_pxe_options(pxe_options).await?; - - Ok(Outcome::new( - InterpretStatus::SUCCESS, - format!( - "Dhcp Interpret Set next boot to [{:?}], boot_filename to [{:?}], filename to [{:?}], filename64 to [{:?}], filenameipxe to [:{:?}]", - self.score.boot_filename, - self.score.boot_filename, - self.score.filename, - self.score.filename64, - self.score.filenameipxe - ), - )) - } } #[async_trait] -impl Interpret for DhcpInterpret { +impl Interpret for DhcpHostBindingInterpret { fn get_name(&self) -> InterpretName { - InterpretName::OPNSenseDHCP + InterpretName::Custom("DhcpHostBindingInterpret") } fn get_version(&self) -> crate::domain::data::Version { - self.version.clone() + Version::from("1.0.0").unwrap() } fn get_status(&self) -> InterpretStatus { - self.status.clone() + todo!() } fn get_children(&self) -> Vec { @@ -146,9 +217,10 @@ impl Interpret for DhcpInterpret { inventory: &Inventory, topology: &T, ) -> Result { - info!("Executing DhcpInterpret on inventory {inventory:?}"); - - self.set_pxe_options(inventory, topology).await?; + info!( + "Executing DhcpHostBindingInterpret on {} bindings", + self.score.host_binding.len() + ); self.add_static_entries(inventory, topology).await?; @@ -156,7 +228,10 @@ impl Interpret for DhcpInterpret { Ok(Outcome::new( InterpretStatus::SUCCESS, - "Dhcp Interpret execution successful".to_string(), + format!( + "Dhcp Host Binding Interpret execution successful on {} hosts", + self.score.host_binding.len() + ), )) } } diff --git a/harmony/src/modules/http.rs b/harmony/src/modules/http.rs index fd7a5c8..b0d1678 100644 --- a/harmony/src/modules/http.rs +++ b/harmony/src/modules/http.rs @@ -3,14 +3,14 @@ use derive_new::new; use serde::Serialize; use crate::{ - data::{FileContent, Version}, + data::{FileContent, FilePath, Version}, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, score::Score, topology::{HttpServer, Topology}, }; -use harmony_types::id::Id; use harmony_types::net::Url; +use harmony_types::{id::Id, net::MacAddress}; /// Configure an HTTP server that is provided by the Topology /// @@ -91,3 +91,33 @@ impl Interpret for StaticFilesHttpInterpret { todo!() } } + +#[derive(Debug, new, Clone, Serialize)] +pub struct IPxeMacBootFileScore { + pub content: String, + pub mac_address: Vec, +} + +impl Score for IPxeMacBootFileScore { + fn name(&self) -> String { + "IPxeMacBootFileScore".to_string() + } + + fn create_interpret(&self) -> Box> { + StaticFilesHttpScore { + folder_to_serve: None, + files: self + .mac_address + .iter() + .map(|mac| FileContent { + path: FilePath::Relative(format!( + "byMAC/01-{}.ipxe", + mac.to_string().replace(":", "-") + )), + content: self.content.clone(), + }) + .collect(), + } + .create_interpret() + } +} diff --git a/harmony/src/modules/ipxe.rs b/harmony/src/modules/ipxe.rs deleted file mode 100644 index a7aa472..0000000 --- a/harmony/src/modules/ipxe.rs +++ /dev/null @@ -1,67 +0,0 @@ -use async_trait::async_trait; -use derive_new::new; -use serde::Serialize; - -use crate::{ - data::Version, - interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, - inventory::Inventory, - score::Score, - topology::Topology, -}; -use harmony_types::id::Id; - -#[derive(Debug, new, Clone, Serialize)] -pub struct IpxeScore { - //files_to_serve: Url, -} - -impl Score for IpxeScore { - fn create_interpret(&self) -> Box> { - Box::new(IpxeInterpret::new(self.clone())) - } - - fn name(&self) -> String { - "IpxeScore".to_string() - } -} - -#[derive(Debug, new, Clone)] -pub struct IpxeInterpret { - _score: IpxeScore, -} - -#[async_trait] -impl Interpret for IpxeInterpret { - async fn execute( - &self, - _inventory: &Inventory, - _topology: &T, - ) -> Result { - /* - let http_server = &topology.http_server; - http_server.ensure_initialized().await?; - Ok(Outcome::success(format!( - "Http Server running and serving files from {}", - self.score.files_to_serve - ))) - */ - todo!(); - } - - fn get_name(&self) -> InterpretName { - InterpretName::Ipxe - } - - fn get_version(&self) -> Version { - todo!() - } - - fn get_status(&self) -> InterpretStatus { - todo!() - } - - fn get_children(&self) -> Vec { - todo!() - } -} diff --git a/harmony/src/modules/mod.rs b/harmony/src/modules/mod.rs index 8935278..682e16b 100644 --- a/harmony/src/modules/mod.rs +++ b/harmony/src/modules/mod.rs @@ -6,7 +6,6 @@ pub mod dummy; pub mod helm; pub mod http; pub mod inventory; -pub mod ipxe; pub mod k3d; pub mod k8s; pub mod lamp; diff --git a/harmony/src/modules/okd/installation.rs b/harmony/src/modules/okd/installation.rs index 58b6942..b7f676e 100644 --- a/harmony/src/modules/okd/installation.rs +++ b/harmony/src/modules/okd/installation.rs @@ -50,18 +50,19 @@ use async_trait::async_trait; use derive_new::new; -use harmony_macros::ip; use harmony_types::id::Id; -use log::info; +use log::{error, info}; use serde::{Deserialize, Serialize}; use crate::{ data::Version, + hardware::PhysicalHost, instrumentation::{HarmonyEvent, instrument}, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, + modules::{dhcp::DhcpHostBindingScore, http::IPxeMacBootFileScore}, score::Score, - topology::{DnsRecord, DnsRecordType, DnsServer, Topology}, + topology::{HAClusterTopology, HostBinding}, }; // ------------------------------------------------------------------------------------------------- @@ -78,8 +79,8 @@ pub struct OKDInstallationScore { pub internal_domain: String, } -impl Score for OKDInstallationScore { - fn create_interpret(&self) -> Box> { +impl Score for OKDInstallationScore { + fn create_interpret(&self) -> Box> { Box::new(OKDInstallationInterpret::new(self.clone())) } @@ -109,10 +110,10 @@ impl OKDInstallationInterpret { } } - async fn run_inventory_phase( + async fn run_inventory_phase( &self, inventory: &Inventory, - topology: &T, + topology: &HAClusterTopology, ) -> Result<(), InterpretError> { // 1) Prepare DNS and DHCP lease registration (optional) let dns_score = OKDSetup01InventoryDnsScore::new( @@ -129,10 +130,10 @@ impl OKDInstallationInterpret { Ok(()) } - async fn run_bootstrap_phase( + async fn run_bootstrap_phase( &self, inventory: &Inventory, - topology: &T, + topology: &HAClusterTopology, ) -> Result<(), InterpretError> { // Select and provision bootstrap let bootstrap_score = OKDSetup02BootstrapScore::new( @@ -143,40 +144,40 @@ impl OKDInstallationInterpret { Ok(()) } - async fn run_control_plane_phase( + async fn run_control_plane_phase( &self, inventory: &Inventory, - topology: &T, + topology: &HAClusterTopology, ) -> Result<(), InterpretError> { let control_plane_score = OKDSetup03ControlPlaneScore::new(); control_plane_score.interpret(inventory, topology).await?; Ok(()) } - async fn run_workers_phase( + async fn run_workers_phase( &self, inventory: &Inventory, - topology: &T, + topology: &HAClusterTopology, ) -> Result<(), InterpretError> { let workers_score = OKDSetup04WorkersScore::new(); workers_score.interpret(inventory, topology).await?; Ok(()) } - async fn run_sanity_phase( + async fn run_sanity_phase( &self, inventory: &Inventory, - topology: &T, + topology: &HAClusterTopology, ) -> Result<(), InterpretError> { let sanity_score = OKDSetup05SanityCheckScore::new(); sanity_score.interpret(inventory, topology).await?; Ok(()) } - async fn run_report_phase( + async fn run_report_phase( &self, inventory: &Inventory, - topology: &T, + topology: &HAClusterTopology, ) -> Result<(), InterpretError> { let report_score = OKDSetup06InstallationReportScore::new( self.score.public_domain.clone(), @@ -188,7 +189,7 @@ impl OKDInstallationInterpret { } #[async_trait] -impl Interpret for OKDInstallationInterpret { +impl Interpret for OKDInstallationInterpret { fn get_name(&self) -> InterpretName { InterpretName::Custom("OKDInstallationInterpret") } @@ -208,7 +209,7 @@ impl Interpret for OKDInstallationInterpret { async fn execute( &self, inventory: &Inventory, - topology: &T, + topology: &HAClusterTopology, ) -> Result { instrument(HarmonyEvent::HarmonyStarted).ok(); @@ -251,8 +252,8 @@ struct OKDSetup01InventoryDnsScore { register_dhcp_leases: Option, } -impl Score for OKDSetup01InventoryDnsScore { - fn create_interpret(&self) -> Box> { +impl Score for OKDSetup01InventoryDnsScore { + fn create_interpret(&self) -> Box> { Box::new(OKDSetup01InventoryDnsInterpret::new(self.clone())) } @@ -277,42 +278,10 @@ impl OKDSetup01InventoryDnsInterpret { status: InterpretStatus::QUEUED, } } - - async fn ensure_dns(&self, dns: &T) -> Result<(), InterpretError> { - // Minimal records placeholders; real IPs are set elsewhere in the flow. - // We register the names early to ensure resolvability for clients relying on DNS. - let mut records: Vec = vec![ - DnsRecord { - value: ip!("0.0.0.0"), - host: "api".to_string(), - domain: self.score.internal_domain.clone(), - record_type: DnsRecordType::A, - }, - DnsRecord { - value: ip!("0.0.0.0"), - host: "api-int".to_string(), - domain: self.score.internal_domain.clone(), - record_type: DnsRecordType::A, - }, - DnsRecord { - value: ip!("0.0.0.0"), - host: "*.apps.".to_string(), - domain: self.score.internal_domain.clone(), - record_type: DnsRecordType::A, - }, - ]; - dns.ensure_hosts_registered(records.drain(..).collect()) - .await?; - if let Some(register) = self.score.register_dhcp_leases { - dns.register_dhcp_leases(register).await?; - } - dns.commit_config().await?; - Ok(()) - } } #[async_trait] -impl Interpret for OKDSetup01InventoryDnsInterpret { +impl Interpret for OKDSetup01InventoryDnsInterpret { fn get_name(&self) -> InterpretName { InterpretName::Custom("OKDSetup01InventoryDns") } @@ -332,10 +301,10 @@ impl Interpret for OKDSetup01InventoryDnsInterpret { async fn execute( &self, _inventory: &Inventory, - topology: &T, + topology: &HAClusterTopology, ) -> Result { info!("Ensuring base DNS and DHCP lease registration for discovery phase"); - self.ensure_dns(topology).await?; + error!("TODO setup ipxe score here and launch inventory agent"); Ok(Outcome::new( InterpretStatus::SUCCESS, "Inventory DNS prepared".into(), @@ -354,8 +323,8 @@ struct OKDSetup01InventoryScore { lan_cidr: String, } -impl Score for OKDSetup01InventoryScore { - fn create_interpret(&self) -> Box> { +impl Score for OKDSetup01InventoryScore { + fn create_interpret(&self) -> Box> { Box::new(OKDSetup01InventoryInterpret::new(self.clone())) } @@ -381,9 +350,9 @@ impl OKDSetup01InventoryInterpret { } } - async fn ensure_inventory_assets( + async fn ensure_inventory_assets( &self, - topology: &T, + topology: &HAClusterTopology, ) -> Result<(), InterpretError> { // Placeholder: push or verify iPXE default, Kickstart, and Rust inventory agent are hosted. // Real implementation: publish to the PXE/HTTP server via the topology. @@ -408,7 +377,7 @@ impl OKDSetup01InventoryInterpret { } #[async_trait] -impl Interpret for OKDSetup01InventoryInterpret { +impl Interpret for OKDSetup01InventoryInterpret { fn get_name(&self) -> InterpretName { InterpretName::Custom("OKDSetup01Inventory") } @@ -428,7 +397,7 @@ impl Interpret for OKDSetup01InventoryInterpret { async fn execute( &self, _inventory: &Inventory, - topology: &T, + topology: &HAClusterTopology, ) -> Result { self.ensure_inventory_assets(topology).await?; let count = self.discover_nodes().await?; @@ -454,8 +423,8 @@ struct OKDSetup02BootstrapScore { internal_domain: String, } -impl Score for OKDSetup02BootstrapScore { - fn create_interpret(&self) -> Box> { +impl Score for OKDSetup02BootstrapScore { + fn create_interpret(&self) -> Box> { Box::new(OKDSetup02BootstrapInterpret::new(self.clone())) } @@ -481,9 +450,46 @@ impl OKDSetup02BootstrapInterpret { } } - async fn render_per_mac_pxe(&self) -> Result<(), InterpretError> { + fn get_bootstrap_node<'a>(&self, inventory: &'a Inventory) -> &'a PhysicalHost { + inventory + .worker_host + .first() + .expect("At least one worker host is required to be used as bootstrap node") + } + + async fn configure_host_binding( + &self, + inventory: &Inventory, + topology: &HAClusterTopology, + ) -> Result<(), InterpretError> { + let binding = HostBinding { + logical_host: topology.bootstrap_host.clone(), + physical_host: self.get_bootstrap_node(inventory).clone(), + }; + info!("Configuring host binding for bootstrap node {binding:?}"); + + DhcpHostBindingScore { + host_binding: vec![binding], + } + .interpret(inventory, topology) + .await?; + Ok(()) + } + + async fn render_per_mac_pxe( + &self, + inventory: &Inventory, + topology: &HAClusterTopology, + ) -> Result<(), InterpretError> { // Placeholder: use Harmony templates to emit {MAC}.ipxe selecting SCOS live + bootstrap ignition. info!("[Bootstrap] Rendering per-MAC PXE for bootstrap node"); + let bootstrap_node = self.get_bootstrap_node(inventory); + IPxeMacBootFileScore { + mac_address: bootstrap_node.get_mac_address(), + content: todo!("templace for bootstrap node"), + } + .interpret(inventory, topology) + .await?; Ok(()) } @@ -501,7 +507,7 @@ impl OKDSetup02BootstrapInterpret { } #[async_trait] -impl Interpret for OKDSetup02BootstrapInterpret { +impl Interpret for OKDSetup02BootstrapInterpret { fn get_name(&self) -> InterpretName { InterpretName::Custom("OKDSetup02Bootstrap") } @@ -520,10 +526,11 @@ impl Interpret for OKDSetup02BootstrapInterpret { async fn execute( &self, - _inventory: &Inventory, - _topology: &T, + inventory: &Inventory, + topology: &HAClusterTopology, ) -> Result { - self.render_per_mac_pxe().await?; + self.configure_host_binding(inventory, topology).await?; + self.render_per_mac_pxe(inventory, topology).await?; self.reboot_target().await?; self.wait_for_bootstrap_complete().await?; @@ -543,8 +550,8 @@ impl Interpret for OKDSetup02BootstrapInterpret { #[derive(Debug, Clone, Serialize, new)] struct OKDSetup03ControlPlaneScore {} -impl Score for OKDSetup03ControlPlaneScore { - fn create_interpret(&self) -> Box> { +impl Score for OKDSetup03ControlPlaneScore { + fn create_interpret(&self) -> Box> { Box::new(OKDSetup03ControlPlaneInterpret::new(self.clone())) } @@ -583,7 +590,7 @@ impl OKDSetup03ControlPlaneInterpret { } #[async_trait] -impl Interpret for OKDSetup03ControlPlaneInterpret { +impl Interpret for OKDSetup03ControlPlaneInterpret { fn get_name(&self) -> InterpretName { InterpretName::Custom("OKDSetup03ControlPlane") } @@ -603,7 +610,7 @@ impl Interpret for OKDSetup03ControlPlaneInterpret { async fn execute( &self, _inventory: &Inventory, - _topology: &T, + _topology: &HAClusterTopology, ) -> Result { self.render_and_reboot().await?; self.persist_network_bond().await?; @@ -623,8 +630,8 @@ impl Interpret for OKDSetup03ControlPlaneInterpret { #[derive(Debug, Clone, Serialize, new)] struct OKDSetup04WorkersScore {} -impl Score for OKDSetup04WorkersScore { - fn create_interpret(&self) -> Box> { +impl Score for OKDSetup04WorkersScore { + fn create_interpret(&self) -> Box> { Box::new(OKDSetup04WorkersInterpret::new(self.clone())) } @@ -657,7 +664,7 @@ impl OKDSetup04WorkersInterpret { } #[async_trait] -impl Interpret for OKDSetup04WorkersInterpret { +impl Interpret for OKDSetup04WorkersInterpret { fn get_name(&self) -> InterpretName { InterpretName::Custom("OKDSetup04Workers") } @@ -677,7 +684,7 @@ impl Interpret for OKDSetup04WorkersInterpret { async fn execute( &self, _inventory: &Inventory, - _topology: &T, + _topology: &HAClusterTopology, ) -> Result { self.render_and_reboot().await?; Ok(Outcome::new( @@ -695,8 +702,8 @@ impl Interpret for OKDSetup04WorkersInterpret { #[derive(Debug, Clone, Serialize, new)] struct OKDSetup05SanityCheckScore {} -impl Score for OKDSetup05SanityCheckScore { - fn create_interpret(&self) -> Box> { +impl Score for OKDSetup05SanityCheckScore { + fn create_interpret(&self) -> Box> { Box::new(OKDSetup05SanityCheckInterpret::new(self.clone())) } @@ -729,7 +736,7 @@ impl OKDSetup05SanityCheckInterpret { } #[async_trait] -impl Interpret for OKDSetup05SanityCheckInterpret { +impl Interpret for OKDSetup05SanityCheckInterpret { fn get_name(&self) -> InterpretName { InterpretName::Custom("OKDSetup05SanityCheck") } @@ -749,7 +756,7 @@ impl Interpret for OKDSetup05SanityCheckInterpret { async fn execute( &self, _inventory: &Inventory, - _topology: &T, + _topology: &HAClusterTopology, ) -> Result { self.run_checks().await?; Ok(Outcome::new( @@ -770,8 +777,8 @@ struct OKDSetup06InstallationReportScore { internal_domain: String, } -impl Score for OKDSetup06InstallationReportScore { - fn create_interpret(&self) -> Box> { +impl Score for OKDSetup06InstallationReportScore { + fn create_interpret(&self) -> Box> { Box::new(OKDSetup06InstallationReportInterpret::new(self.clone())) } @@ -807,7 +814,7 @@ impl OKDSetup06InstallationReportInterpret { } #[async_trait] -impl Interpret for OKDSetup06InstallationReportInterpret { +impl Interpret for OKDSetup06InstallationReportInterpret { fn get_name(&self) -> InterpretName { InterpretName::Custom("OKDSetup06InstallationReport") } @@ -827,7 +834,7 @@ impl Interpret for OKDSetup06InstallationReportInterpret { async fn execute( &self, _inventory: &Inventory, - _topology: &T, + _topology: &HAClusterTopology, ) -> Result { self.generate().await?; Ok(Outcome::new( diff --git a/opnsense-config-xml/src/data/dnsmasq.rs b/opnsense-config-xml/src/data/dnsmasq.rs index db2b8c1..fe76b66 100644 --- a/opnsense-config-xml/src/data/dnsmasq.rs +++ b/opnsense-config-xml/src/data/dnsmasq.rs @@ -36,6 +36,27 @@ pub struct DnsMasq { pub dhcp_options: Vec, pub dhcp_boot: Vec, pub dhcp_tags: Vec, + pub hosts: Vec, +} + +#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize, Clone)] +#[yaserde(rename = "hosts")] +pub struct DnsmasqHost { + #[yaserde(attribute = true)] + pub uuid: String, + pub host: String, + pub domain: MaybeString, + pub local: MaybeString, + pub ip: MaybeString, + pub cnames: MaybeString, + pub client_id: MaybeString, + pub hwaddr: MaybeString, + pub lease_time: MaybeString, + pub ignore: Option, + pub set_tag: MaybeString, + pub descr: MaybeString, + pub comments: MaybeString, + pub aliases: MaybeString, } // Represents the element and its nested fields. diff --git a/opnsense-config/src/config/config.rs b/opnsense-config/src/config/config.rs index 56cd503..55464cf 100644 --- a/opnsense-config/src/config/config.rs +++ b/opnsense-config/src/config/config.rs @@ -226,6 +226,7 @@ mod tests { "src/tests/data/config-full-ncd0.xml", "src/tests/data/config-full-25.7.xml", "src/tests/data/config-full-25.7-dummy-dnsmasq-options.xml", + "src/tests/data/config-25.7-dnsmasq-static-host.xml", ] { let mut test_file_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); test_file_path.push(path); diff --git a/opnsense-config/src/modules/dnsmasq.rs b/opnsense-config/src/modules/dnsmasq.rs index bcff8be..c6018cc 100644 --- a/opnsense-config/src/modules/dnsmasq.rs +++ b/opnsense-config/src/modules/dnsmasq.rs @@ -442,7 +442,11 @@ mod test { // This IP belongs to host-a, but the hostname belongs to host-b. dhcp_config - .add_static_mapping("CC:CC:CC:CC:CC:CC", Ipv4Addr::new(192, 168, 1, 10), "host-b") + .add_static_mapping( + "CC:CC:CC:CC:CC:CC", + Ipv4Addr::new(192, 168, 1, 10), + "host-b", + ) .unwrap(); } @@ -457,7 +461,11 @@ mod test { // This IP is ambiguous. dhcp_config - .add_static_mapping("CC:CC:CC:CC:CC:CC", Ipv4Addr::new(192, 168, 1, 30), "new-host") + .add_static_mapping( + "CC:CC:CC:CC:CC:CC", + Ipv4Addr::new(192, 168, 1, 30), + "new-host", + ) .unwrap(); } @@ -509,8 +517,18 @@ mod test { #[test] fn test_remove_mac_from_correct_host_only() { - let host1 = create_host("uuid-1", "host-1", "192.168.1.50", "AA:AA:AA:AA:AA:AA,BB:BB:BB:BB:BB:BB"); - let host2 = create_host("uuid-2", "host-2", "192.168.1.51", "CC:CC:CC:CC:CC:CC,DD:DD:DD:DD:DD:DD"); + let host1 = create_host( + "uuid-1", + "host-1", + "192.168.1.50", + "AA:AA:AA:AA:AA:AA,BB:BB:BB:BB:BB:BB", + ); + let host2 = create_host( + "uuid-2", + "host-2", + "192.168.1.51", + "CC:CC:CC:CC:CC:CC,DD:DD:DD:DD:DD:DD", + ); let mut dhcp_config = setup_test_env(vec![host1.clone(), host2.clone()]); dhcp_config.remove_static_mapping("AA:AA:AA:AA:AA:AA");