diff --git a/harmony-rs/Cargo.lock b/harmony-rs/Cargo.lock index 22e902d..dc80e38 100644 --- a/harmony-rs/Cargo.lock +++ b/harmony-rs/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -152,7 +152,7 @@ checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -471,7 +471,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -499,7 +499,7 @@ checksum = "2cdc8d50f426189eef89dac62fabfa0abb27d5cc008f25bf4156a0203325becc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -784,7 +784,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -905,6 +905,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "harmony_macros" +version = "1.0.0" +dependencies = [ + "harmony", + "quote", + "syn 2.0.90", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -1346,7 +1355,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -1620,9 +1629,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -2062,7 +2071,7 @@ checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -2247,9 +2256,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.77" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", @@ -2319,7 +2328,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -2371,7 +2380,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -2542,7 +2551,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", "wasm-bindgen-shared", ] @@ -2576,7 +2585,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2786,6 +2795,16 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "wk" +version = "0.1.0" +dependencies = [ + "cidr", + "harmony", + "harmony_macros", + "tokio", +] + [[package]] name = "wyz" version = "0.5.1" @@ -2845,7 +2864,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] diff --git a/harmony-rs/Cargo.toml b/harmony-rs/Cargo.toml index 20aba71..d965ab6 100644 --- a/harmony-rs/Cargo.toml +++ b/harmony-rs/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = [ "private_repos/*", "harmony", - "opnsense-config", "opnsense-config-xml", + "opnsense-config", "opnsense-config-xml", "harmony_macros", ] [workspace.package] diff --git a/harmony-rs/harmony/src/domain/hardware/mod.rs b/harmony-rs/harmony/src/domain/hardware/mod.rs index 8ec4f81..2b2c02a 100644 --- a/harmony-rs/harmony/src/domain/hardware/mod.rs +++ b/harmony-rs/harmony/src/domain/hardware/mod.rs @@ -96,10 +96,10 @@ pub enum StorageKind { } #[derive(Debug, new, Clone)] pub struct Storage { - connection: StorageConnectionType, - kind: StorageKind, - size: u64, - serial: String, + pub connection: StorageConnectionType, + pub kind: StorageKind, + pub size: u64, + pub serial: String, } #[derive(Debug, Clone)] diff --git a/harmony-rs/harmony/src/domain/interpret/mod.rs b/harmony-rs/harmony/src/domain/interpret/mod.rs index ecf953e..7ec1362 100644 --- a/harmony-rs/harmony/src/domain/interpret/mod.rs +++ b/harmony-rs/harmony/src/domain/interpret/mod.rs @@ -7,7 +7,6 @@ use super::{ data::{Id, Version}, executors::ExecutorError, inventory::Inventory, - score::Score, topology::HAClusterTopology, }; @@ -41,9 +40,13 @@ pub struct Outcome { status: InterpretStatus, message: String, } -impl std::fmt::Display for Outcome { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("Outcome {}: {}", self.status, self.message)) + +impl Outcome { + pub fn noop() -> Self { + Self { + status: InterpretStatus::NOOP, + message: String::new(), + } } } @@ -54,6 +57,7 @@ pub enum InterpretStatus { RUNNING, QUEUED, BLOCKED, + NOOP, } impl std::fmt::Display for InterpretStatus { @@ -64,6 +68,7 @@ impl std::fmt::Display for InterpretStatus { InterpretStatus::RUNNING => "RUNNING", InterpretStatus::QUEUED => "QUEUED", InterpretStatus::BLOCKED => "BLOCKED", + InterpretStatus::NOOP => "NO_OP", }; f.write_str(msg) } diff --git a/harmony-rs/harmony/src/domain/topology/mod.rs b/harmony-rs/harmony/src/domain/topology/mod.rs index bfb9c96..ccad53b 100644 --- a/harmony-rs/harmony/src/domain/topology/mod.rs +++ b/harmony-rs/harmony/src/domain/topology/mod.rs @@ -34,7 +34,79 @@ pub type IpAddress = IpAddr; /// This abstraction focuses on the logical role and services, independent of the physical hardware. #[derive(Debug, Clone)] pub struct LogicalHost { - /// The set of services this logical host provides + /// The IP address of this logical host. pub ip: IpAddress, + /// The name of this logical host. pub name: String, } + +impl LogicalHost { + /// Creates a list of `LogicalHost` instances. + /// + /// # Arguments + /// + /// * `number_hosts` - The number of logical hosts to create. + /// * `start_ip` - The starting IP address. Each subsequent host's IP will be incremented. + /// * `hostname_prefix` - The prefix for the host names. Host names will be in the form `prefix`. + /// + /// # Returns + /// + /// A `Vec` containing the specified number of logical hosts, each with a unique IP and name. + /// + /// # Panics + /// + /// This function will panic if adding `number_hosts` to `start_ip` exceeds the valid range of IP addresses. + /// + /// # Examples + /// + /// ``` + /// use std::str::FromStr; + /// use harmony::topology::{IpAddress, LogicalHost}; + /// + /// let start_ip = IpAddress::from_str("192.168.0.20").unwrap(); + /// let hosts = LogicalHost::create_hosts(3, start_ip, "worker"); + /// + /// assert_eq!(hosts.len(), 3); + /// assert_eq!(hosts[0].ip, IpAddress::from_str("192.168.0.20").unwrap()); + /// assert_eq!(hosts[0].name, "worker0"); + /// assert_eq!(hosts[1].ip, IpAddress::from_str("192.168.0.21").unwrap()); + /// assert_eq!(hosts[1].name, "worker1"); + /// assert_eq!(hosts[2].ip, IpAddress::from_str("192.168.0.22").unwrap()); + /// assert_eq!(hosts[2].name, "worker2"); + /// ``` + pub fn create_hosts(number_hosts: u32, start_ip: IpAddress, hostname_prefix: &str) -> Vec { + let mut hosts = Vec::with_capacity(number_hosts.try_into().unwrap()); + for i in 0..number_hosts { + let new_ip = increment_ip(start_ip, i).expect("IP address overflow"); + let name = format!("{}{}", hostname_prefix, i); + hosts.push(LogicalHost { ip: new_ip, name }); + } + hosts + } +} + +/// Increments an IP address by a given value. +/// +/// # Arguments +/// +/// * `ip` - The starting IP address. +/// * `increment` - The amount to add to the IP address. +/// +/// # Returns +/// +/// A new `IpAddress` that is the result of incrementing the original by `increment`. +/// +/// # Panics +/// +/// This function panics if the resulting IP address exceeds the valid range. +fn increment_ip(ip: IpAddress, increment: u32) -> Option { + match ip { + IpAddress::V4(ipv4) => { + let new_ip = u32::from(ipv4) + increment; + Some(IpAddress::V4(new_ip.into())) + } + IpAddress::V6(_) => { + todo!("Ipv6 not supported yet") + } + } +} diff --git a/harmony-rs/harmony/src/domain/topology/network.rs b/harmony-rs/harmony/src/domain/topology/network.rs index be55f37..ff19dc0 100644 --- a/harmony-rs/harmony/src/domain/topology/network.rs +++ b/harmony-rs/harmony/src/domain/topology/network.rs @@ -45,8 +45,10 @@ pub trait DhcpServer: Send + Sync { async fn add_static_mapping(&self, entry: &DHCPStaticEntry) -> Result<(), ExecutorError>; async fn remove_static_mapping(&self, mac: &MacAddress) -> Result<(), ExecutorError>; 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_host(&self) -> LogicalHost; + async fn commit_config(&self) -> Result<(), ExecutorError>; } impl std::fmt::Debug for dyn DhcpServer { diff --git a/harmony-rs/harmony/src/infra/opnsense/management.rs b/harmony-rs/harmony/src/infra/opnsense/management.rs index ce0c6b9..81f062f 100644 --- a/harmony-rs/harmony/src/infra/opnsense/management.rs +++ b/harmony-rs/harmony/src/infra/opnsense/management.rs @@ -3,9 +3,7 @@ use derive_new::new; use crate::{hardware::ManagementInterface, topology::MacAddress}; #[derive(new)] -pub struct OPNSenseManagementInterface { - mac: MacAddress, -} +pub struct OPNSenseManagementInterface {} impl ManagementInterface for OPNSenseManagementInterface { fn boot_to_pxe(&self) { @@ -13,7 +11,7 @@ impl ManagementInterface for OPNSenseManagementInterface { } fn get_mac_address(&self) -> MacAddress { - self.mac.clone() + todo!("OPNSense can have multiple mac addresses using SSH. I'm not sure it even belongs in the ManagementInterface trait") } fn get_supported_protocol_names(&self) -> String { diff --git a/harmony-rs/harmony/src/infra/opnsense/mod.rs b/harmony-rs/harmony/src/infra/opnsense/mod.rs index 66b00e6..7aa4407 100644 --- a/harmony-rs/harmony/src/infra/opnsense/mod.rs +++ b/harmony-rs/harmony/src/infra/opnsense/mod.rs @@ -1,9 +1,10 @@ mod management; -use std::sync::{Arc, Mutex, RwLock, RwLockWriteGuard}; +use std::sync::Arc; use async_trait::async_trait; use log::debug; pub use management::*; +use tokio::sync::RwLock; use crate::{ executors::ExecutorError, @@ -97,14 +98,24 @@ impl LoadBalancer for OPNSenseFirewall { #[async_trait] impl DhcpServer for OPNSenseFirewall { + async fn commit_config(&self) -> Result<(), ExecutorError> { + self.opnsense_config + .read() + .await + .apply() + .await + .map_err(|e| ExecutorError::UnexpectedError(e.to_string())) + } + async fn add_static_mapping(&self, entry: &DHCPStaticEntry) -> Result<(), ExecutorError> { let mac: String = String::from(&entry.mac); { - let mut writable_opnsense = self.opnsense_config.write().unwrap(); + let mut writable_opnsense = self.opnsense_config.write().await; writable_opnsense .dhcp() - .add_static_mapping(&mac, entry.ip, &entry.name).unwrap(); + .add_static_mapping(&mac, entry.ip, &entry.name) + .unwrap(); } debug!("Registered {:?}", entry); @@ -125,9 +136,24 @@ impl DhcpServer for OPNSenseFirewall { fn get_ip(&self) -> IpAddress { OPNSenseFirewall::get_ip(self) } + fn get_host(&self) -> LogicalHost { 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 { diff --git a/harmony-rs/harmony/src/lib.rs b/harmony-rs/harmony/src/lib.rs index be55110..3c2dff0 100644 --- a/harmony-rs/harmony/src/lib.rs +++ b/harmony-rs/harmony/src/lib.rs @@ -2,3 +2,8 @@ mod domain; pub use domain::*; pub mod infra; pub mod modules; + +#[cfg(test)] +mod test { + use crate::infra::opnsense::OPNSenseFirewall; +} diff --git a/harmony-rs/harmony/src/modules/dhcp.rs b/harmony-rs/harmony/src/modules/dhcp.rs index 37e49cb..09dd87a 100644 --- a/harmony-rs/harmony/src/modules/dhcp.rs +++ b/harmony-rs/harmony/src/modules/dhcp.rs @@ -1,4 +1,4 @@ -use std::{net::Ipv4Addr, sync::Arc}; +use std::sync::Arc; use async_trait::async_trait; use derive_new::new; @@ -11,50 +11,15 @@ use crate::{ }, interpret::{Interpret, InterpretError, InterpretName, Outcome}, inventory::Inventory, - topology::{DHCPStaticEntry, HAClusterTopology, HostBinding}, + topology::{DHCPStaticEntry, HAClusterTopology, HostBinding, IpAddress}, }; use crate::domain::score::Score; -/// OPNSenseDhcpScore will set static DHCP entries using index based hostname -/// and ip addresses. -/// -/// For example : -/// ```rust -/// -/// let node1 = todo!(); // Node pointing to clustermember controlplane0 with ip 10.10.0.20 and host with mac 01 -/// let node2 = todo!(); // Node pointing to clustermember controlplane1 with ip 10.10.0.21 and host with mac 02 -/// let node3 = todo!(); // Node pointing to clustermember controlplane2 with ip 10.10.0.22 and host with mac 03 -/// -/// let score = OPNSenseDhcpScore { -/// nodes: vec![node1, node2, node3], -/// } -/// ``` -/// -/// Running such a score would create these static entries : -/// -/// ```rust -/// let entries = vec![ -/// DHCPEntry { -/// mac: 01, -/// ip: 10.10.0.20, -/// hostname: "controlplane0" -/// } -/// DHCPEntry { -/// mac: 02, -/// ip: 10.10.0.21, -/// hostname: "controlplane0" -/// } -/// DHCPEntry { -/// mac: 03, -/// ip: 10.10.0.22, -/// hostname: "controlplane2" -/// } -/// ] -/// ``` #[derive(Debug, new, Clone)] pub struct DhcpScore { host_binding: Vec, + next_server: Option, } impl Score for DhcpScore { @@ -78,7 +43,7 @@ pub struct DhcpInterpret { impl DhcpInterpret { pub fn new(score: DhcpScore) -> Self { let version = Version::from("1.0.0").expect("Version should be valid"); - let name = "OPNSenseDhcpScore".to_string(); + let name = "DhcpInterpret".to_string(); let id = Id::from_string(format!("{name}_{version}")); Self { @@ -89,6 +54,67 @@ impl DhcpInterpret { status: InterpretStatus::QUEUED, } } + async fn add_static_entries( + &self, + _inventory: &Inventory, + topology: &HAClusterTopology, + ) -> Result { + let dhcp_entries: Vec = 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 { + 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] @@ -116,63 +142,15 @@ impl Interpret for DhcpInterpret { ) -> Result { info!("Executing {} on inventory {inventory:?}", self.get_name()); - let dhcp_entries: Vec = 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"), - }; + self.set_next_server(inventory, topology).await?; - DHCPStaticEntry { - name: binding.logical_host.name.clone(), - mac: binding.physical_host.cluster_mac(), - ip, - } - }) - .collect(); - info!("DHCPStaticEntry : {:?}", dhcp_entries); + // self.add_static_entries(inventory, topology).await?; - let dhcp = Arc::new(Box::new(topology.dhcp_server.clone())); - info!("DHCP server : {:?}", dhcp); - for entry in dhcp_entries.into_iter() { - match dhcp.add_static_mapping(&entry).await { - Ok(_) => info!("Successfully registered DHCPStaticEntry {}", entry), - Err(_) => todo!(), - } - } - - todo!("Configure DHCPServer"); + topology.dhcp_server.commit_config().await?; Ok(Outcome::new( InterpretStatus::SUCCESS, - "Connection test successful".to_string(), + 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!(); - } -} diff --git a/harmony-rs/harmony/src/modules/mod.rs b/harmony-rs/harmony/src/modules/mod.rs index 5833493..84aeb83 100644 --- a/harmony-rs/harmony/src/modules/mod.rs +++ b/harmony-rs/harmony/src/modules/mod.rs @@ -1 +1,2 @@ pub mod dhcp; +pub mod okd; diff --git a/harmony-rs/harmony/src/modules/okd/dhcp.rs b/harmony-rs/harmony/src/modules/okd/dhcp.rs new file mode 100644 index 0000000..b22efff --- /dev/null +++ b/harmony-rs/harmony/src/modules/okd/dhcp.rs @@ -0,0 +1,45 @@ +use crate::{ + inventory::Inventory, + modules::dhcp::DhcpScore, + score::Score, + topology::{HAClusterTopology, HostBinding}, +}; + +#[derive(Debug)] +pub struct OKDBootstrapDhcpScore { + dhcp_score: DhcpScore, +} + +impl OKDBootstrapDhcpScore { + 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 { + dhcp_score: DhcpScore::new( + host_binding, + // TODO : we should add a tftp server to the topology instead of relying on the + // router address, this is leaking implementation details + Some(topology.router.get_gateway()), + ), + } + } +} + +impl Score for OKDBootstrapDhcpScore { + type InterpretType = ::InterpretType; + + fn create_interpret(self) -> Self::InterpretType { + self.dhcp_score.create_interpret() + } +} diff --git a/harmony-rs/harmony/src/modules/okd/mod.rs b/harmony-rs/harmony/src/modules/okd/mod.rs new file mode 100644 index 0000000..8c32480 --- /dev/null +++ b/harmony-rs/harmony/src/modules/okd/mod.rs @@ -0,0 +1,2 @@ +pub mod dhcp; + diff --git a/harmony-rs/harmony_macros/Cargo.lock b/harmony-rs/harmony_macros/Cargo.lock new file mode 100644 index 0000000..7070e6a --- /dev/null +++ b/harmony-rs/harmony_macros/Cargo.lock @@ -0,0 +1,46 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "harmony_macros" +version = "1.0.0" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "2.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" diff --git a/harmony-rs/harmony_macros/Cargo.toml b/harmony-rs/harmony_macros/Cargo.toml new file mode 100644 index 0000000..e43389c --- /dev/null +++ b/harmony-rs/harmony_macros/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "harmony_macros" +edition = "2024" +version = "1.0.0" + +[lib] +proc-macro = true + +[dependencies] +harmony = { version = "0.1.0", path = "../harmony" } +quote = "1.0.37" +syn = "2.0.90" diff --git a/harmony-rs/harmony_macros/src/lib.rs b/harmony-rs/harmony_macros/src/lib.rs new file mode 100644 index 0000000..6a86640 --- /dev/null +++ b/harmony-rs/harmony_macros/src/lib.rs @@ -0,0 +1,66 @@ +extern crate proc_macro; + +use harmony::topology::MacAddress; +use proc_macro::TokenStream; +use quote::quote; +use syn::{LitStr, parse_macro_input}; + +#[proc_macro] +pub fn ip(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as LitStr); + let ip_str = input.value(); + + if let Ok(_) = ip_str.parse::() { + let expanded = + quote! { std::net::IpAddr::V4(#ip_str.parse::().unwrap()) }; + return TokenStream::from(expanded); + } + + if let Ok(_) = ip_str.parse::() { + let expanded = + quote! { std::net::IpAddr::V4(#ip_str.parse::().unwrap()) }; + return TokenStream::from(expanded); + } + + panic!("Invalid IP address: {}", ip_str); +} + +#[proc_macro] +pub fn mac_address(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as LitStr); + let mac_str = input.value(); + + match parse_mac_address(&mac_str) { + Ok(bytes) => { + let b0 = bytes[0]; + let b1 = bytes[1]; + let b2 = bytes[2]; + let b3 = bytes[3]; + let b4 = bytes[4]; + let b5 = bytes[5]; + + quote! { + MacAddress( [#b0, #b1, #b2, #b3, #b4, #b5] ) + } + .into() + } + Err(err) => syn::Error::new(input.span(), err).to_compile_error().into(), + } +} + +fn parse_mac_address(mac: &str) -> Result<[u8; 6], String> { + let parts: Vec<&str> = mac.split(':').collect(); + if parts.len() != 6 { + return Err("MAC address must contain exactly six octets separated by colons".to_string()); + } + + let mut bytes = [0u8; 6]; + for (i, part) in parts.iter().enumerate() { + match u8::from_str_radix(part, 16) { + Ok(byte) => bytes[i] = byte, + Err(_) => return Err(format!("Invalid MAC address octet: {}", part)), + } + } + + Ok(bytes) +} diff --git a/harmony-rs/opnsense-config-xml/src/data/dhcpd.rs b/harmony-rs/opnsense-config-xml/src/data/dhcpd.rs index 9c06716..212758a 100644 --- a/harmony-rs/opnsense-config-xml/src/data/dhcpd.rs +++ b/harmony-rs/opnsense-config-xml/src/data/dhcpd.rs @@ -16,6 +16,8 @@ pub struct DhcpInterface { pub enable: Option, pub gateway: Option, pub domain: Option, + pub netboot: Option, + pub nextserver: Option, #[yaserde(rename = "ddnsdomainalgorithm")] pub ddns_domain_algorithm: Option, #[yaserde(rename = "numberoptions")] diff --git a/harmony-rs/opnsense-config-xml/src/data/opnsense.rs b/harmony-rs/opnsense-config-xml/src/data/opnsense.rs index 48ff582..04c4977 100644 --- a/harmony-rs/opnsense-config-xml/src/data/opnsense.rs +++ b/harmony-rs/opnsense-config-xml/src/data/opnsense.rs @@ -27,7 +27,7 @@ pub struct OPNsense { pub opnsense: OPNsenseXmlSection, pub staticroutes: StaticRoutes, pub ca: MaybeString, - pub gateways: Option, + pub gateways: Option, pub cert: Vec, pub dhcpdv6: DhcpDv6, pub virtualip: VirtualIp, @@ -60,7 +60,6 @@ impl OPNsense { } } - #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] pub struct LoadBalancer { pub monitor_type: Vec, @@ -415,6 +414,8 @@ pub struct OPNsenseXmlSection { pub ipsec: Option, #[yaserde(rename = "Interfaces")] pub interfaces: Option, + #[yaserde(rename = "NodeExporter")] + pub node_exporter: Option, #[yaserde(rename = "Kea")] pub kea: Option, pub monit: Option, @@ -428,6 +429,7 @@ pub struct OPNsenseXmlSection { pub unboundplus: Option, #[yaserde(rename = "DHCRelay")] pub dhcrelay: Option, + pub trust: Option, pub wireguard: Option, #[yaserde(rename = "Swanctl")] pub swanctl: Swanctl, @@ -479,6 +481,8 @@ pub struct IDSGeneral { #[yaserde(rename = "LogPayload")] log_payload: Option, verbosity: MaybeString, + #[yaserde(rename = "eveLog")] + eve_log: Option, } #[derive(Debug, YaSerialize, YaDeserialize, PartialEq)] @@ -498,11 +502,15 @@ pub struct IPsec { key_pairs: MaybeString, #[yaserde(rename = "preSharedKeys")] pre_shared_keys: MaybeString, + charon: Option, } #[derive(Debug, YaSerialize, YaDeserialize, PartialEq)] pub struct GeneralIpsec { enabled: MaybeString, + preferred_oldsa: MaybeString, + disablevpnrules: MaybeString, + passthrough_networks: MaybeString, } #[derive(Debug, YaSerialize, YaDeserialize, PartialEq)] @@ -1214,6 +1222,8 @@ pub struct WireguardServerItem { pub gateway: MaybeString, pub carp_depend_on: MaybeString, pub peers: String, + pub endpoint: MaybeString, + pub peer_dns: MaybeString, } #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] @@ -1320,7 +1330,6 @@ pub struct ConfigOpenVPN { pub StaticKeys: MaybeString, } - #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[yaserde(rename = "HAProxy")] pub struct HAProxy { @@ -1411,6 +1420,8 @@ pub struct Tuning { #[yaserde(rename = "maxConnections")] pub max_connections: MaybeString, pub nbthread: i32, + #[yaserde(rename = "resolversPrefer")] + pub resolvers_prefer: String, #[yaserde(rename = "sslServerVerify")] pub ssl_server_verify: String, #[yaserde(rename = "maxDHSize")] @@ -1425,6 +1436,12 @@ pub struct Tuning { pub lua_max_mem: i32, #[yaserde(rename = "customOptions")] pub custom_options: MaybeString, + #[yaserde(rename = "ocspUpdateEnabled")] + pub ocs_update_enabled: MaybeString, + #[yaserde(rename = "ocspUpdateMinDelay")] + pub ocs_update_min_delay: MaybeString, + #[yaserde(rename = "ocspUpdateMaxDelay")] + pub ocs_update_max_delay: MaybeString, #[yaserde(rename = "ssl_defaultsEnabled")] pub ssl_defaults_enabled: i32, #[yaserde(rename = "ssl_bindOptions")] @@ -1437,6 +1454,19 @@ pub struct Tuning { pub ssl_cipher_list: String, #[yaserde(rename = "ssl_cipherSuites")] pub ssl_cipher_suites: String, + #[yaserde(rename = "h2_initialWindowSize")] + pub h2_initial_window_size: Option, + #[yaserde(rename = "h2_initialWindowSizeOutgoing")] + pub h2_initial_window_size_outgoing: Option, + #[yaserde(rename = "h2_initialWindowSizeIncoming")] + pub h2_initial_window_size_incoming: Option, + #[yaserde(rename = "h2_maxConcurrentStreams")] + pub h2_max_concurrent_streams: Option, + #[yaserde(rename = "h2_maxConcurrentStreamsOutgoing")] + pub h2_max_concurrent_streams_outgoing: Option, + #[yaserde(rename = "h2_maxConcurrentStreamsIncoming")] + pub h2_max_concurrent_streams_incoming: Option, + } #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] @@ -1684,25 +1714,22 @@ pub struct Backend { pub http2_enabled_nontls: u8, #[yaserde(rename = "ba_advertised_protocols")] pub ba_advertised_protocols: String, - #[yaserde(rename = "persistence")] - pub persistence: String, - #[yaserde(rename = "persistence_cookiemode")] + #[yaserde(rename = "forwardFor")] + pub forward_for: Option, + #[yaserde(rename = "forwardedHeader")] + pub forwarded_header: Option, + #[yaserde(rename = "forwardedHeaderParameters")] + pub forwarded_header_parameters: Option, + pub persistence: MaybeString, pub persistence_cookiemode: String, - #[yaserde(rename = "persistence_cookiename")] pub persistence_cookiename: MaybeString, - #[yaserde(rename = "persistence_stripquotes")] pub persistence_stripquotes: u8, - #[yaserde(rename = "stickiness_pattern")] - pub stickiness_pattern: String, + pub stickiness_pattern: MaybeString, #[yaserde(rename = "stickiness_dataTypes")] pub stickiness_data_types: MaybeString, - #[yaserde(rename = "stickiness_expire")] pub stickiness_expire: String, - #[yaserde(rename = "stickiness_size")] pub stickiness_size: String, - #[yaserde(rename = "stickiness_cookiename")] pub stickiness_cookiename: MaybeString, - #[yaserde(rename = "stickiness_cookielength")] pub stickiness_cookielength: MaybeString, #[yaserde(rename = "stickiness_connRatePeriod")] pub stickiness_conn_rate_period: String, @@ -1863,12 +1890,6 @@ pub struct StaticRoutes { #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] pub struct Ca {} -#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] -pub struct Gateways { - #[yaserde(rename = "gateway_item")] - pub gateway_item: RawXml -} - #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] pub struct Cert { #[yaserde(attribute)] @@ -1975,14 +1996,14 @@ pub struct Bridges { pub struct Gifs { #[yaserde(attribute)] pub version: Option, - pub gif: MaybeString, + pub gif: Option, } #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] pub struct Gres { #[yaserde(attribute)] pub version: Option, - pub gre: MaybeString, + pub gre: Option, } #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] diff --git a/harmony-rs/opnsense-config/src/config/manager/ssh.rs b/harmony-rs/opnsense-config/src/config/manager/ssh.rs index a32298b..fdd8d03 100644 --- a/harmony-rs/opnsense-config/src/config/manager/ssh.rs +++ b/harmony-rs/opnsense-config/src/config/manager/ssh.rs @@ -24,9 +24,10 @@ impl SshConfigManager { impl SshConfigManager { async fn backup_config_remote(&self) -> Result { - let backup_filename = format!("config_{}.xml", chrono::Local::now().format("%Y%m%d%H%M%S")); + let ts = chrono::Utc::now(); + let backup_filename = format!("config-{}-harmony.xml", ts.format("%s%.3f")); - self.opnsense_shell.exec(&format!("cp /conf/config.xml /tmp/{}", backup_filename)) + self.opnsense_shell.exec(&format!("cp /conf/config.xml /conf/backup/{}", backup_filename)) .await } diff --git a/harmony-rs/opnsense-config/src/modules/dhcp.rs b/harmony-rs/opnsense-config/src/modules/dhcp.rs index a337cac..ae9ccbc 100644 --- a/harmony-rs/opnsense-config/src/modules/dhcp.rs +++ b/harmony-rs/opnsense-config/src/modules/dhcp.rs @@ -54,7 +54,9 @@ impl<'a> DhcpConfig<'a> { pub fn remove_static_mapping(&mut self, mac: &str) { 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 { @@ -77,16 +79,19 @@ impl<'a> DhcpConfig<'a> { let mac = mac.to_string(); let hostname = hostname.to_string(); let lan_dhcpd = self.get_lan_dhcpd(); - let range = &lan_dhcpd.range; let existing_mappings: &mut Vec = &mut lan_dhcpd.staticmaps; if !Self::is_valid_mac(&mac) { return Err(DhcpError::InvalidMacAddress(mac)); } - if !Self::is_ip_in_range(&ipaddr, range) { - return Err(DhcpError::IpAddressOutOfRange(ipaddr.to_string())); - } + // 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 + // outside of the dynamic DHCP pool + // let range = &lan_dhcpd.range; + // if !Self::is_ip_in_range(&ipaddr, range) { + // return Err(DhcpError::IpAddressOutOfRange(ipaddr.to_string())); + // } if existing_mappings.iter().any(|m| { m.ipaddr @@ -123,7 +128,7 @@ impl<'a> DhcpConfig<'a> { parts .iter() - .all(|part| part.len() == 2 && part.chars().all(|c| c.is_ascii_hexdigit())) + .all(|part| part.len() <= 2 && part.chars().all(|c| c.is_ascii_hexdigit())) } fn is_ip_in_range(ip: &Ipv4Addr, range: &Range) -> bool { @@ -173,6 +178,14 @@ impl<'a> DhcpConfig<'a> { 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)]