feat(opnsense-config): Public API now complete for dhcp add_static_mapping and remove_static_mapping, not perfect but good enough to move forward
This commit is contained in:
		
							parent
							
								
									b14d0ab686
								
							
						
					
					
						commit
						d30e909b83
					
				
							
								
								
									
										151
									
								
								harmony-rs/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										151
									
								
								harmony-rs/Cargo.lock
									
									
									
										generated
									
									
									
								
							| @ -58,6 +58,19 @@ dependencies = [ | |||||||
|  "subtle", |  "subtle", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "ahash" | ||||||
|  | version = "0.8.11" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" | ||||||
|  | dependencies = [ | ||||||
|  |  "cfg-if", | ||||||
|  |  "const-random", | ||||||
|  |  "once_cell", | ||||||
|  |  "version_check", | ||||||
|  |  "zerocopy", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "aho-corasick" | name = "aho-corasick" | ||||||
| version = "1.1.3" | version = "1.1.3" | ||||||
| @ -203,6 +216,9 @@ name = "bitflags" | |||||||
| version = "2.6.0" | version = "2.6.0" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" | ||||||
|  | dependencies = [ | ||||||
|  |  "serde", | ||||||
|  | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "bitvec" | name = "bitvec" | ||||||
| @ -339,6 +355,26 @@ version = "0.9.6" | |||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" | checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "const-random" | ||||||
|  | version = "0.1.18" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" | ||||||
|  | dependencies = [ | ||||||
|  |  "const-random-macro", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "const-random-macro" | ||||||
|  | version = "0.1.16" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" | ||||||
|  | dependencies = [ | ||||||
|  |  "getrandom", | ||||||
|  |  "once_cell", | ||||||
|  |  "tiny-keccak", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "core-foundation" | name = "core-foundation" | ||||||
| version = "0.9.4" | version = "0.9.4" | ||||||
| @ -373,6 +409,12 @@ dependencies = [ | |||||||
|  "cfg-if", |  "cfg-if", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "crunchy" | ||||||
|  | version = "0.2.2" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "crypto-bigint" | name = "crypto-bigint" | ||||||
| version = "0.5.5" | version = "0.5.5" | ||||||
| @ -627,6 +669,18 @@ dependencies = [ | |||||||
|  "miniz_oxide 0.8.0", |  "miniz_oxide 0.8.0", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "flurry" | ||||||
|  | version = "0.5.2" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "cf5efcf77a4da27927d3ab0509dec5b0954bb3bc59da5a1de9e52642ebd4cdf9" | ||||||
|  | dependencies = [ | ||||||
|  |  "ahash", | ||||||
|  |  "num_cpus", | ||||||
|  |  "parking_lot", | ||||||
|  |  "seize", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "fnv" | name = "fnv" | ||||||
| version = "1.0.7" | version = "1.0.7" | ||||||
| @ -1099,6 +1153,16 @@ version = "0.4.14" | |||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "lock_api" | ||||||
|  | version = "0.4.12" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" | ||||||
|  | dependencies = [ | ||||||
|  |  "autocfg", | ||||||
|  |  "scopeguard", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "log" | name = "log" | ||||||
| version = "0.4.22" | version = "0.4.22" | ||||||
| @ -1228,6 +1292,16 @@ dependencies = [ | |||||||
|  "libm", |  "libm", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "num_cpus" | ||||||
|  | version = "1.16.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" | ||||||
|  | dependencies = [ | ||||||
|  |  "hermit-abi", | ||||||
|  |  "libc", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "object" | name = "object" | ||||||
| version = "0.36.4" | version = "0.36.4" | ||||||
| @ -1305,7 +1379,9 @@ dependencies = [ | |||||||
|  "pretty_assertions", |  "pretty_assertions", | ||||||
|  "russh", |  "russh", | ||||||
|  "russh-keys", |  "russh-keys", | ||||||
|  |  "russh-sftp", | ||||||
|  "serde", |  "serde", | ||||||
|  |  "serde_json", | ||||||
|  "thiserror", |  "thiserror", | ||||||
|  "tokio", |  "tokio", | ||||||
| ] | ] | ||||||
| @ -1364,6 +1440,29 @@ dependencies = [ | |||||||
|  "sha2", |  "sha2", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "parking_lot" | ||||||
|  | version = "0.12.3" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" | ||||||
|  | dependencies = [ | ||||||
|  |  "lock_api", | ||||||
|  |  "parking_lot_core", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "parking_lot_core" | ||||||
|  | version = "0.9.10" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" | ||||||
|  | dependencies = [ | ||||||
|  |  "cfg-if", | ||||||
|  |  "libc", | ||||||
|  |  "redox_syscall", | ||||||
|  |  "smallvec", | ||||||
|  |  "windows-targets 0.52.6", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "password-hash" | name = "password-hash" | ||||||
| version = "0.4.2" | version = "0.4.2" | ||||||
| @ -1582,6 +1681,15 @@ dependencies = [ | |||||||
|  "getrandom", |  "getrandom", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "redox_syscall" | ||||||
|  | version = "0.5.7" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" | ||||||
|  | dependencies = [ | ||||||
|  |  "bitflags 2.6.0", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "regex" | name = "regex" | ||||||
| version = "1.10.6" | version = "1.10.6" | ||||||
| @ -1786,6 +1894,24 @@ dependencies = [ | |||||||
|  "zeroize", |  "zeroize", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "russh-sftp" | ||||||
|  | version = "2.0.6" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "c2a72c8afe2041c17435eecd85d0b7291841486fd3d1c4082e0b212e5437ca42" | ||||||
|  | dependencies = [ | ||||||
|  |  "async-trait", | ||||||
|  |  "bitflags 2.6.0", | ||||||
|  |  "bytes", | ||||||
|  |  "chrono", | ||||||
|  |  "flurry", | ||||||
|  |  "log", | ||||||
|  |  "serde", | ||||||
|  |  "thiserror", | ||||||
|  |  "tokio", | ||||||
|  |  "tokio-util", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "rust-ipmi" | name = "rust-ipmi" | ||||||
| version = "0.1.1" | version = "0.1.1" | ||||||
| @ -1862,6 +1988,12 @@ dependencies = [ | |||||||
|  "windows-sys 0.52.0", |  "windows-sys 0.52.0", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "scopeguard" | ||||||
|  | version = "1.2.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "scrypt" | name = "scrypt" | ||||||
| version = "0.11.0" | version = "0.11.0" | ||||||
| @ -1910,6 +2042,12 @@ dependencies = [ | |||||||
|  "libc", |  "libc", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "seize" | ||||||
|  | version = "0.3.3" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "689224d06523904ebcc9b482c6a3f4f7fb396096645c4cd10c0d2ff7371a34d3" | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "semver" | name = "semver" | ||||||
| version = "1.0.23" | version = "1.0.23" | ||||||
| @ -1938,9 +2076,9 @@ dependencies = [ | |||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "serde_json" | name = "serde_json" | ||||||
| version = "1.0.128" | version = "1.0.133" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" | checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "itoa", |  "itoa", | ||||||
|  "memchr", |  "memchr", | ||||||
| @ -2193,6 +2331,15 @@ dependencies = [ | |||||||
|  "syn 2.0.77", |  "syn 2.0.77", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "tiny-keccak" | ||||||
|  | version = "2.0.2" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" | ||||||
|  | dependencies = [ | ||||||
|  |  "crunchy", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "tinyvec" | name = "tinyvec" | ||||||
| version = "1.8.0" | version = "1.8.0" | ||||||
|  | |||||||
| @ -1,3 +1,4 @@ | |||||||
| mod xml_utils; | mod xml_utils; | ||||||
| mod data; | mod data; | ||||||
| pub use data::*; | pub use data::*; | ||||||
|  | pub use yaserde::MaybeString; | ||||||
|  | |||||||
| @ -16,6 +16,8 @@ async-trait = { workspace = true } | |||||||
| tokio = { workspace = true } | tokio = { workspace = true } | ||||||
| opnsense-config-xml = { path = "../opnsense-config-xml" } | opnsense-config-xml = { path = "../opnsense-config-xml" } | ||||||
| chrono = "0.4.38" | chrono = "0.4.38" | ||||||
|  | russh-sftp = "2.0.6" | ||||||
|  | serde_json = "1.0.133" | ||||||
| 
 | 
 | ||||||
| [dev-dependencies] | [dev-dependencies] | ||||||
| pretty_assertions = "1.4.1" | pretty_assertions = "1.4.1" | ||||||
|  | |||||||
| @ -1,17 +1,23 @@ | |||||||
|  | use std::sync::Arc; | ||||||
|  | 
 | ||||||
| use crate::{error::Error, modules::dhcp::DhcpConfig}; | use crate::{error::Error, modules::dhcp::DhcpConfig}; | ||||||
| use log::trace; | use log::trace; | ||||||
| use opnsense_config_xml::OPNsense; | use opnsense_config_xml::OPNsense; | ||||||
| 
 | 
 | ||||||
| use super::ConfigManager; | use super::{ConfigManager, OPNsenseShell}; | ||||||
| 
 | 
 | ||||||
| #[derive(Debug)] | #[derive(Debug)] | ||||||
| pub struct Config { | pub struct Config { | ||||||
|     opnsense: OPNsense, |     opnsense: OPNsense, | ||||||
|     repository: Box<dyn ConfigManager + Send + Sync>, |     repository: Arc<dyn ConfigManager>, | ||||||
|  |     shell: Arc<dyn OPNsenseShell>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl Config { | impl Config { | ||||||
|     pub async fn new(repository: Box<dyn ConfigManager + Send + Sync>) -> Result<Self, Error> { |     pub async fn new( | ||||||
|  |         repository: Arc<dyn ConfigManager>, | ||||||
|  |         shell: Arc<dyn OPNsenseShell>, | ||||||
|  |     ) -> Result<Self, Error> { | ||||||
|         let xml = repository.load_as_str().await?; |         let xml = repository.load_as_str().await?; | ||||||
|         trace!("xml {}", xml); |         trace!("xml {}", xml); | ||||||
| 
 | 
 | ||||||
| @ -20,21 +26,24 @@ impl Config { | |||||||
|         Ok(Self { |         Ok(Self { | ||||||
|             opnsense, |             opnsense, | ||||||
|             repository, |             repository, | ||||||
|  |             shell, | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub fn dhcp(&mut self) -> DhcpConfig { |     pub fn dhcp(&mut self) -> DhcpConfig { | ||||||
|         DhcpConfig::new(&mut self.opnsense) |         DhcpConfig::new(&mut self.opnsense, self.shell.clone()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub async fn apply(&self) -> Result<(), Error> { |     pub async fn apply(&self) -> Result<(), Error> { | ||||||
|         self.repository.apply_new_config(&self.opnsense.to_xml()).await |         self.repository | ||||||
|  |             .apply_new_config(&self.opnsense.to_xml()) | ||||||
|  |             .await | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[cfg(test)] | #[cfg(test)] | ||||||
| mod tests { | mod tests { | ||||||
|     use crate::config::LocalFileConfigManager; |     use crate::config::{DummyOPNSenseShell, LocalFileConfigManager}; | ||||||
|     use crate::modules::dhcp::DhcpConfig; |     use crate::modules::dhcp::DhcpConfig; | ||||||
|     use std::fs; |     use std::fs; | ||||||
|     use std::net::Ipv4Addr; |     use std::net::Ipv4Addr; | ||||||
| @ -55,9 +64,10 @@ mod tests { | |||||||
| 
 | 
 | ||||||
|             let config_file_path = test_file_path.to_str().unwrap().to_string(); |             let config_file_path = test_file_path.to_str().unwrap().to_string(); | ||||||
|             println!("File path {config_file_path}"); |             println!("File path {config_file_path}"); | ||||||
|             let repository = Box::new(LocalFileConfigManager::new(config_file_path)); |             let repository = Arc::new(LocalFileConfigManager::new(config_file_path)); | ||||||
|  |             let shell = Arc::new(DummyOPNSenseShell {}); | ||||||
|             let config_file_str = repository.load_as_str().await.unwrap(); |             let config_file_str = repository.load_as_str().await.unwrap(); | ||||||
|             let config = Config::new(repository) |             let config = Config::new(repository, shell) | ||||||
|                 .await |                 .await | ||||||
|                 .expect("Failed to load config"); |                 .expect("Failed to load config"); | ||||||
| 
 | 
 | ||||||
| @ -82,14 +92,15 @@ mod tests { | |||||||
| 
 | 
 | ||||||
|         let config_file_path = test_file_path.to_str().unwrap().to_string(); |         let config_file_path = test_file_path.to_str().unwrap().to_string(); | ||||||
|         println!("File path {config_file_path}"); |         println!("File path {config_file_path}"); | ||||||
|         let repository = Box::new(LocalFileConfigManager::new(config_file_path)); |         let repository = Arc::new(LocalFileConfigManager::new(config_file_path)); | ||||||
|         let mut config = Config::new(repository) |         let shell = Arc::new(DummyOPNSenseShell {}); | ||||||
|  |         let mut config = Config::new(repository, shell.clone()) | ||||||
|             .await |             .await | ||||||
|             .expect("Failed to load config"); |             .expect("Failed to load config"); | ||||||
| 
 | 
 | ||||||
|         println!("Config {:?}", config); |         println!("Config {:?}", config); | ||||||
| 
 | 
 | ||||||
|         let mut dhcp_config = DhcpConfig::new(&mut config.opnsense); |         let mut dhcp_config = DhcpConfig::new(&mut config.opnsense, shell); | ||||||
|         dhcp_config |         dhcp_config | ||||||
|             .add_static_mapping( |             .add_static_mapping( | ||||||
|                 "00:00:00:00:00:00", |                 "00:00:00:00:00:00", | ||||||
|  | |||||||
| @ -1,33 +1,9 @@ | |||||||
| use crate::config::manager::ConfigManager; | use crate::config::{manager::ConfigManager, OPNsenseShell}; | ||||||
| use crate::error::Error; | use crate::error::Error; | ||||||
| use async_trait::async_trait; | use async_trait::async_trait; | ||||||
| use log::{debug, info}; | use log::info; | ||||||
| use russh::{ | use russh_keys::key::KeyPair; | ||||||
|     client::{Config as SshConfig, Handler, Msg}, | use std::sync::Arc; | ||||||
|     Channel, |  | ||||||
| }; |  | ||||||
| use russh_keys::key::{self, KeyPair}; |  | ||||||
| use russh_sftp::client::SftpSession; |  | ||||||
| use std::{ |  | ||||||
|     net::Ipv4Addr, |  | ||||||
|     sync::Arc, |  | ||||||
|     time::{SystemTime, UNIX_EPOCH}, |  | ||||||
| }; |  | ||||||
| use tokio::io::AsyncWriteExt; |  | ||||||
| 
 |  | ||||||
| struct Client {} |  | ||||||
| 
 |  | ||||||
| #[async_trait] |  | ||||||
| impl Handler for Client { |  | ||||||
|     type Error = Error; |  | ||||||
| 
 |  | ||||||
|     async fn check_server_key( |  | ||||||
|         &mut self, |  | ||||||
|         _server_public_key: &key::PublicKey, |  | ||||||
|     ) -> Result<bool, Self::Error> { |  | ||||||
|         Ok(true) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| #[derive(Debug)] | #[derive(Debug)] | ||||||
| pub enum SshCredentials { | pub enum SshCredentials { | ||||||
| @ -37,170 +13,50 @@ pub enum SshCredentials { | |||||||
| 
 | 
 | ||||||
| #[derive(Debug)] | #[derive(Debug)] | ||||||
| pub struct SshConfigManager { | pub struct SshConfigManager { | ||||||
|     ssh_config: Arc<SshConfig>, |     opnsense_shell: Arc<dyn OPNsenseShell>, | ||||||
|     credentials: SshCredentials, |  | ||||||
|     host: (Ipv4Addr, u16), |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl SshConfigManager { | impl SshConfigManager { | ||||||
|     pub fn new( |     pub fn new(opnsense_shell: Arc<dyn OPNsenseShell>) -> Self { | ||||||
|         host: (Ipv4Addr, u16), |         Self { opnsense_shell } | ||||||
|         credentials: SshCredentials, |  | ||||||
|         ssh_config: Arc<SshConfig>, |  | ||||||
|     ) -> Self { |  | ||||||
|         Self { |  | ||||||
|             ssh_config, |  | ||||||
|             credentials, |  | ||||||
|             host, |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl SshConfigManager { | impl SshConfigManager { | ||||||
|     async fn get_ssh_channel(&self) -> Result<Channel<Msg>, Error> { |  | ||||||
|         let mut ssh = russh::client::connect(self.ssh_config.clone(), self.host, Client {}).await?; |  | ||||||
| 
 |  | ||||||
|         match &self.credentials { |  | ||||||
|             SshCredentials::SshKey { username, key } => { |  | ||||||
|                 ssh.authenticate_publickey(username, key.clone()).await?; |  | ||||||
|             } |  | ||||||
|             SshCredentials::Password { username, password } => { |  | ||||||
|                 ssh.authenticate_password(username, password).await?; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         Ok(ssh.channel_open_session().await?) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     async fn write_content_to_temp_file(&self, content: &str) -> Result<String, Error> { |  | ||||||
|         let temp_filename = format!( |  | ||||||
|             "/tmp/opnsense-config-tmp-config_{}", |  | ||||||
|             SystemTime::now() |  | ||||||
|                 .duration_since(UNIX_EPOCH) |  | ||||||
|                 .unwrap() |  | ||||||
|                 .as_millis() |  | ||||||
|         ); |  | ||||||
|         let channel = self.get_ssh_channel().await?; |  | ||||||
|         channel |  | ||||||
|             .request_subsystem(true, "sftp") |  | ||||||
|             .await |  | ||||||
|             .expect("Should request sftp subsystem"); |  | ||||||
|         let sftp = SftpSession::new(channel.into_stream()) |  | ||||||
|             .await |  | ||||||
|             .expect("Should acquire sftp subsystem"); |  | ||||||
| 
 |  | ||||||
|         let mut file = sftp.create(&temp_filename).await.unwrap(); |  | ||||||
|         file.write_all(content.as_bytes()).await?; |  | ||||||
| 
 |  | ||||||
|         Ok(temp_filename) |  | ||||||
|     } |  | ||||||
|     async fn backup_config_remote(&self) -> Result<String, Error> { |     async fn backup_config_remote(&self) -> Result<String, Error> { | ||||||
|         let backup_filename = format!("config_{}.xml", chrono::Local::now().format("%Y%m%d%H%M%S")); |         let backup_filename = format!("config_{}.xml", chrono::Local::now().format("%Y%m%d%H%M%S")); | ||||||
| 
 | 
 | ||||||
|         self.run_command(&format!("cp /conf/config.xml /tmp/{}", backup_filename)) |         self.opnsense_shell.exec(&format!("cp /conf/config.xml /tmp/{}", backup_filename)) | ||||||
|             .await |             .await | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async fn move_to_live_config(&self, new_config_path: &str) -> Result<String, Error> { |     async fn move_to_live_config(&self, new_config_path: &str) -> Result<String, Error> { | ||||||
|         info!("Overwriting OPNSense /conf/config.xml with {new_config_path}"); |         info!("Overwriting OPNSense /conf/config.xml with {new_config_path}"); | ||||||
|         self.run_command(&format!("mv {new_config_path} /conf/config.xml")) |         self.opnsense_shell.exec(&format!("mv {new_config_path} /conf/config.xml")) | ||||||
|             .await |             .await | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async fn reload_all_services(&self) -> Result<String, Error> { |     async fn reload_all_services(&self) -> Result<String, Error> { | ||||||
|         info!("Reloading all opnsense services"); |         info!("Reloading all opnsense services"); | ||||||
|         self.run_command(&format!("configctl service reload all")) |         self.opnsense_shell.exec(&format!("configctl service reload all")) | ||||||
|             .await |             .await | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
|     async fn run_command(&self, command: &str) -> Result<String, Error> { |  | ||||||
|         debug!("Running ssh command {command}"); |  | ||||||
|         let mut channel = self.get_ssh_channel().await?; |  | ||||||
|         channel.exec(true, command).await?; |  | ||||||
|         wait_for_completion(&mut channel).await |  | ||||||
|     } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[async_trait] | #[async_trait] | ||||||
| impl ConfigManager for SshConfigManager { | impl ConfigManager for SshConfigManager { | ||||||
|     async fn load_as_str(&self) -> Result<String, Error> { |     async fn load_as_str(&self) -> Result<String, Error> { | ||||||
|         let mut channel = self.get_ssh_channel().await?; |         self.opnsense_shell.exec("cat /conf/config.xml").await | ||||||
| 
 |  | ||||||
|         channel.exec(true, "cat /conf/config.xml").await?; |  | ||||||
|         let mut output: Vec<u8> = vec![]; |  | ||||||
|         loop { |  | ||||||
|             let Some(msg) = channel.wait().await else { |  | ||||||
|                 break; |  | ||||||
|             }; |  | ||||||
| 
 |  | ||||||
|             info!("got msg {:?}", msg); |  | ||||||
|             match msg { |  | ||||||
|                 russh::ChannelMsg::Data { ref data } => { |  | ||||||
|                     output.append(&mut data.to_vec()); |  | ||||||
|                 } |  | ||||||
|                 russh::ChannelMsg::ExitStatus { .. } => {} |  | ||||||
|                 russh::ChannelMsg::WindowAdjusted { .. } => {} |  | ||||||
|                 russh::ChannelMsg::Success { .. } => {} |  | ||||||
|                 russh::ChannelMsg::Eof { .. } => {} |  | ||||||
|                 _ => todo!(), |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         Ok(String::from_utf8(output).expect("Valid utf-8 bytes")) |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async fn apply_new_config(&self, content: &str) -> Result<(), Error> { |     async fn apply_new_config(&self, content: &str) -> Result<(), Error> { | ||||||
|         let temp_filename = self.write_content_to_temp_file(content).await?; |         let temp_filename = self | ||||||
|  |             .opnsense_shell | ||||||
|  |             .write_content_to_temp_file(content) | ||||||
|  |             .await?; | ||||||
|         self.backup_config_remote().await?; |         self.backup_config_remote().await?; | ||||||
|         self.move_to_live_config(&temp_filename).await?; |         self.move_to_live_config(&temp_filename).await?; | ||||||
|         self.reload_all_services().await?; |         self.reload_all_services().await?; | ||||||
| 
 |         Ok(()) | ||||||
|         // TODO
 |  | ||||||
|         // 1. use russh to copy the current opnsense /conf/config.xml to the backup folder with a
 |  | ||||||
|         // proper name
 |  | ||||||
|         //
 |  | ||||||
|         // 2. use russh scp functionality to copy the content directly into a file
 |  | ||||||
|         // if necessary, save it first to a local file with minimal permissions in
 |  | ||||||
|         // /tmp/{randomname}
 |  | ||||||
|         //
 |  | ||||||
|         // 3. Use opnsense cli to validate the config file
 |  | ||||||
|         //
 |  | ||||||
|         // 4. Reload all opnsense services using opnsense cli (still through russh commands)
 |  | ||||||
|         //
 |  | ||||||
|         todo!("apply_new_config not fully implemented yet") |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 |  | ||||||
| async fn wait_for_completion(channel: &mut Channel<Msg>) -> Result<String, Error> { |  | ||||||
|     let mut output = Vec::new(); |  | ||||||
| 
 |  | ||||||
|     loop { |  | ||||||
|         let Some(msg) = channel.wait().await else { |  | ||||||
|             break; |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         match msg { |  | ||||||
|             russh::ChannelMsg::ExtendedData { ref data, .. } |  | ||||||
|             | russh::ChannelMsg::Data { ref data } => { |  | ||||||
|                 output.append(&mut data.to_vec()); |  | ||||||
|             } |  | ||||||
|             russh::ChannelMsg::ExitStatus { exit_status } => { |  | ||||||
|                 if exit_status != 0 { |  | ||||||
|                     return Err(Error::Command(format!( |  | ||||||
|                         "Command failed with exit status {exit_status}, output {}", |  | ||||||
|                         String::from_utf8(output).unwrap_or_default() |  | ||||||
|                     ))); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             russh::ChannelMsg::Success { .. } |  | ||||||
|             | russh::ChannelMsg::WindowAdjusted { .. } |  | ||||||
|             | russh::ChannelMsg::Eof { .. } => {} |  | ||||||
|             _ => { |  | ||||||
|                 return Err(Error::Unexpected(format!( |  | ||||||
|                     "Russh got unexpected msg {msg:?}" |  | ||||||
|                 ))) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     Ok(String::from_utf8(output).unwrap_or_default()) |  | ||||||
| } |  | ||||||
|  | |||||||
| @ -1,4 +1,6 @@ | |||||||
| mod config; | mod config; | ||||||
| mod manager; | mod manager; | ||||||
|  | mod shell; | ||||||
| pub use manager::*; | pub use manager::*; | ||||||
| pub use config::*; | pub use config::*; | ||||||
|  | pub use shell::*; | ||||||
|  | |||||||
							
								
								
									
										27
									
								
								harmony-rs/opnsense-config/src/config/shell/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								harmony-rs/opnsense-config/src/config/shell/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | |||||||
|  | mod ssh; | ||||||
|  | pub use ssh::*; | ||||||
|  | 
 | ||||||
|  | use async_trait::async_trait; | ||||||
|  | 
 | ||||||
|  | use crate::Error; | ||||||
|  | 
 | ||||||
|  | #[async_trait] | ||||||
|  | pub trait OPNsenseShell: std::fmt::Debug + Send + Sync { | ||||||
|  |     async fn exec(&self, command: &str) -> Result<String, Error>; | ||||||
|  |     async fn write_content_to_temp_file(&self, content: &str) -> Result<String, Error>; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[cfg(test)] | ||||||
|  | #[derive(Debug)] | ||||||
|  | pub struct DummyOPNSenseShell; | ||||||
|  | 
 | ||||||
|  | #[cfg(test)] | ||||||
|  | #[async_trait] | ||||||
|  | impl OPNsenseShell for DummyOPNSenseShell { | ||||||
|  |     async fn exec(&self, _command: &str) -> Result<String, Error> { | ||||||
|  |         unimplemented!("This is a dummy implementation"); | ||||||
|  |     } | ||||||
|  |     async fn write_content_to_temp_file(&self, _content: &str) -> Result<String, Error> { | ||||||
|  |         unimplemented!("This is a dummy implementation"); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										141
									
								
								harmony-rs/opnsense-config/src/config/shell/ssh.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								harmony-rs/opnsense-config/src/config/shell/ssh.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,141 @@ | |||||||
|  | use std::{ | ||||||
|  |     net::Ipv4Addr, | ||||||
|  |     sync::Arc, | ||||||
|  |     time::{SystemTime, UNIX_EPOCH}, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | use async_trait::async_trait; | ||||||
|  | use log::debug; | ||||||
|  | use russh::{ | ||||||
|  |     client::{Config, Handler, Msg}, | ||||||
|  |     Channel, | ||||||
|  | }; | ||||||
|  | use russh_keys::key; | ||||||
|  | use russh_sftp::client::SftpSession; | ||||||
|  | use tokio::io::AsyncWriteExt; | ||||||
|  | 
 | ||||||
|  | use crate::{config::SshCredentials, Error}; | ||||||
|  | 
 | ||||||
|  | use super::OPNsenseShell; | ||||||
|  | 
 | ||||||
|  | #[derive(Debug)] | ||||||
|  | pub struct SshOPNSenseShell { | ||||||
|  |     host: (Ipv4Addr, u16), | ||||||
|  |     credentials: SshCredentials, | ||||||
|  |     ssh_config: Arc<Config>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[async_trait] | ||||||
|  | impl OPNsenseShell for SshOPNSenseShell { | ||||||
|  |     async fn exec(&self, command: &str) -> Result<String, Error> { | ||||||
|  |         self.run_command(command).await | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async fn write_content_to_temp_file(&self, content: &str) -> Result<String, Error> { | ||||||
|  |         let temp_filename = format!( | ||||||
|  |             "/tmp/opnsense-config-tmp-config_{}", | ||||||
|  |             SystemTime::now() | ||||||
|  |                 .duration_since(UNIX_EPOCH) | ||||||
|  |                 .unwrap() | ||||||
|  |                 .as_millis() | ||||||
|  |         ); | ||||||
|  |         let channel = self.get_ssh_channel().await?; | ||||||
|  |         channel | ||||||
|  |             .request_subsystem(true, "sftp") | ||||||
|  |             .await | ||||||
|  |             .expect("Should request sftp subsystem"); | ||||||
|  |         let sftp = SftpSession::new(channel.into_stream()) | ||||||
|  |             .await | ||||||
|  |             .expect("Should acquire sftp subsystem"); | ||||||
|  | 
 | ||||||
|  |         let mut file = sftp.create(&temp_filename).await.unwrap(); | ||||||
|  |         file.write_all(content.as_bytes()).await?; | ||||||
|  | 
 | ||||||
|  |         Ok(temp_filename) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl SshOPNSenseShell { | ||||||
|  |     pub async fn get_ssh_channel(&self) -> Result<Channel<Msg>, Error> { | ||||||
|  |         let mut ssh = russh::client::connect(self.ssh_config.clone(), self.host, Client {}).await?; | ||||||
|  | 
 | ||||||
|  |         match &self.credentials { | ||||||
|  |             SshCredentials::SshKey { username, key } => { | ||||||
|  |                 ssh.authenticate_publickey(username, key.clone()).await?; | ||||||
|  |             } | ||||||
|  |             SshCredentials::Password { username, password } => { | ||||||
|  |                 ssh.authenticate_password(username, password).await?; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         Ok(ssh.channel_open_session().await?) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async fn run_command(&self, command: &str) -> Result<String, Error> { | ||||||
|  |         debug!("Running ssh command {command}"); | ||||||
|  |         let mut channel = self.get_ssh_channel().await?; | ||||||
|  |         channel.exec(true, command).await?; | ||||||
|  |         wait_for_completion(&mut channel).await | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn new( | ||||||
|  |         host: (Ipv4Addr, u16), | ||||||
|  |         credentials: SshCredentials, | ||||||
|  |         ssh_config: Arc<Config>, | ||||||
|  |     ) -> Self { | ||||||
|  |         Self { | ||||||
|  |             host, | ||||||
|  |             credentials, | ||||||
|  |             ssh_config, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | struct Client {} | ||||||
|  | 
 | ||||||
|  | #[async_trait] | ||||||
|  | impl Handler for Client { | ||||||
|  |     type Error = Error; | ||||||
|  | 
 | ||||||
|  |     async fn check_server_key( | ||||||
|  |         &mut self, | ||||||
|  |         _server_public_key: &key::PublicKey, | ||||||
|  |     ) -> Result<bool, Self::Error> { | ||||||
|  |         Ok(true) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async fn wait_for_completion(channel: &mut Channel<Msg>) -> Result<String, Error> { | ||||||
|  |     let mut output = Vec::new(); | ||||||
|  | 
 | ||||||
|  |     loop { | ||||||
|  |         let Some(msg) = channel.wait().await else { | ||||||
|  |             break; | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         match msg { | ||||||
|  |             russh::ChannelMsg::ExtendedData { ref data, .. } | ||||||
|  |             | russh::ChannelMsg::Data { ref data } => { | ||||||
|  |                 output.append(&mut data.to_vec()); | ||||||
|  |             } | ||||||
|  |             russh::ChannelMsg::ExitStatus { exit_status } => { | ||||||
|  |                 if exit_status != 0 { | ||||||
|  |                     return Err(Error::Command(format!( | ||||||
|  |                         "Command failed with exit status {exit_status}, output {}", | ||||||
|  |                         String::from_utf8(output).unwrap_or_default() | ||||||
|  |                     ))); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             russh::ChannelMsg::Success { .. } | ||||||
|  |             | russh::ChannelMsg::WindowAdjusted { .. } | ||||||
|  |             | russh::ChannelMsg::Eof { .. } => {} | ||||||
|  |             _ => { | ||||||
|  |                 return Err(Error::Unexpected(format!( | ||||||
|  |                     "Russh got unexpected msg {msg:?}" | ||||||
|  |                 ))) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     Ok(String::from_utf8(output).unwrap_or_default()) | ||||||
|  | } | ||||||
| @ -7,16 +7,37 @@ pub use error::Error; | |||||||
| #[cfg(test)] | #[cfg(test)] | ||||||
| mod test { | mod test { | ||||||
|     use config::SshConfigManager; |     use config::SshConfigManager; | ||||||
|  |     use opnsense_config_xml::StaticMap; | ||||||
|     use russh::client; |     use russh::client; | ||||||
|     use std::{net::Ipv4Addr, sync::Arc, time::Duration}; |     use std::{net::Ipv4Addr, sync::Arc, time::Duration}; | ||||||
| 
 | 
 | ||||||
|     use crate::{ |     use crate::{ | ||||||
|         config::{self, SshCredentials}, |         config::{self, SshCredentials, SshOPNSenseShell}, | ||||||
|         Config, |         Config, | ||||||
|     }; |     }; | ||||||
|  |     use pretty_assertions::assert_eq; | ||||||
| 
 | 
 | ||||||
|     #[tokio::test] |     #[tokio::test] | ||||||
|     async fn test_public_sdk() { |     async fn test_public_sdk() { | ||||||
|  |         let mac = "11:22:33:44:55:66"; | ||||||
|  |         let ip = Ipv4Addr::new(10, 100, 8, 200); | ||||||
|  |         let hostname = "test_hostname"; | ||||||
|  | 
 | ||||||
|  |         remove_static_mapping(mac).await; | ||||||
|  | 
 | ||||||
|  |         // Make sure static mapping does not exist anymore
 | ||||||
|  |         let static_mapping_removed = get_static_mappings().await; | ||||||
|  |         assert!(!static_mapping_removed.iter().any(|e| e.mac == mac)); | ||||||
|  | 
 | ||||||
|  |         add_static_mapping(mac, ip, hostname).await; | ||||||
|  | 
 | ||||||
|  |         // Make sure static mapping has been added successfully
 | ||||||
|  |         let static_mapping_added = get_static_mappings().await; | ||||||
|  |         assert_eq!(static_mapping_added.len(), static_mapping_removed.len() + 1); | ||||||
|  |         assert!(static_mapping_added.iter().any(|e| e.mac == mac)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async fn initialize_config() -> Config { | ||||||
|         let config = Arc::new(client::Config { |         let config = Arc::new(client::Config { | ||||||
|             inactivity_timeout: Some(Duration::from_secs(5)), |             inactivity_timeout: Some(Duration::from_secs(5)), | ||||||
|             ..<_>::default() |             ..<_>::default() | ||||||
| @ -27,19 +48,29 @@ mod test { | |||||||
|             password: String::from("opnsense"), |             password: String::from("opnsense"), | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         let repo = |         let shell = Arc::new(SshOPNSenseShell::new( | ||||||
|             SshConfigManager::new((Ipv4Addr::new(192, 168, 5, 229), 22), credentials, config); |             (Ipv4Addr::new(192, 168, 5, 229), 22), | ||||||
|  |             credentials, | ||||||
|  |             config, | ||||||
|  |         )); | ||||||
|  |         let manager = Arc::new(SshConfigManager::new(shell.clone())); | ||||||
|  |         Config::new(manager, shell).await.unwrap() | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|         let mut config = Config::new(Box::new(repo)).await.unwrap(); |     async fn get_static_mappings() -> Vec<StaticMap> { | ||||||
|         config |         let mut config = initialize_config().await; | ||||||
|             .dhcp() |         config.dhcp().get_static_mappings().await.unwrap() | ||||||
|             .add_static_mapping( |     } | ||||||
|                 "11:22:33:44:55:66", |  | ||||||
|                 Ipv4Addr::new(10, 100, 8, 200), |  | ||||||
|                 "test_hostname", |  | ||||||
|             ) |  | ||||||
|             .unwrap(); |  | ||||||
| 
 | 
 | ||||||
|  |     async fn add_static_mapping(mac: &str, ip: Ipv4Addr, hostname: &str) { | ||||||
|  |         let mut config = initialize_config().await; | ||||||
|  |         config.dhcp().add_static_mapping(mac, ip, hostname).unwrap(); | ||||||
|  |         config.apply().await.unwrap(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async fn remove_static_mapping(mac: &str) { | ||||||
|  |         let mut config = initialize_config().await; | ||||||
|  |         config.dhcp().remove_static_mapping(mac); | ||||||
|         config.apply().await.unwrap(); |         config.apply().await.unwrap(); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,12 +1,18 @@ | |||||||
|  | use opnsense_config_xml::MaybeString; | ||||||
| use opnsense_config_xml::Range; | use opnsense_config_xml::Range; | ||||||
| use opnsense_config_xml::StaticMap; | use opnsense_config_xml::StaticMap; | ||||||
| use std::cmp::Ordering; | use std::cmp::Ordering; | ||||||
| use std::net::Ipv4Addr; | use std::net::Ipv4Addr; | ||||||
|  | use std::sync::Arc; | ||||||
| 
 | 
 | ||||||
| use opnsense_config_xml::OPNsense; | use opnsense_config_xml::OPNsense; | ||||||
| 
 | 
 | ||||||
|  | use crate::config::OPNsenseShell; | ||||||
|  | use crate::Error; | ||||||
|  | 
 | ||||||
| pub struct DhcpConfig<'a> { | pub struct DhcpConfig<'a> { | ||||||
|     opnsense: &'a mut OPNsense, |     opnsense: &'a mut OPNsense, | ||||||
|  |     opnsense_shell: Arc<dyn OPNsenseShell>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[derive(Debug)] | #[derive(Debug)] | ||||||
| @ -39,8 +45,27 @@ impl std::fmt::Display for DhcpError { | |||||||
| impl std::error::Error for DhcpError {} | impl std::error::Error for DhcpError {} | ||||||
| 
 | 
 | ||||||
| impl<'a> DhcpConfig<'a> { | impl<'a> DhcpConfig<'a> { | ||||||
|     pub fn new(opnsense: &'a mut OPNsense) -> Self { |     pub fn new(opnsense: &'a mut OPNsense, opnsense_shell: Arc<dyn OPNsenseShell>) -> Self { | ||||||
|         Self { opnsense } |         Self { | ||||||
|  |             opnsense, | ||||||
|  |             opnsense_shell, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     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); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn get_lan_dhcpd(&mut self) -> &mut opnsense_config_xml::DhcpInterface { | ||||||
|  |         &mut self | ||||||
|  |             .opnsense | ||||||
|  |             .dhcpd | ||||||
|  |             .elements | ||||||
|  |             .iter_mut() | ||||||
|  |             .find(|(name, _config)| return name == "lan") | ||||||
|  |             .expect("Interface lan should have dhcpd activated") | ||||||
|  |             .1 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub fn add_static_mapping( |     pub fn add_static_mapping( | ||||||
| @ -51,16 +76,9 @@ impl<'a> DhcpConfig<'a> { | |||||||
|     ) -> Result<(), DhcpError> { |     ) -> Result<(), DhcpError> { | ||||||
|         let mac = mac.to_string(); |         let mac = mac.to_string(); | ||||||
|         let hostname = hostname.to_string(); |         let hostname = hostname.to_string(); | ||||||
|         let lan_dhcpd = &mut self |         let lan_dhcpd = self.get_lan_dhcpd(); | ||||||
|             .opnsense |  | ||||||
|             .dhcpd |  | ||||||
|             .elements |  | ||||||
|             .iter_mut() |  | ||||||
|             .find(|(name, _config)| return name == "lan") |  | ||||||
|             .expect("Interface lan should have dhcpd activated") |  | ||||||
|             .1; |  | ||||||
| 
 |  | ||||||
|         let range = &lan_dhcpd.range; |         let range = &lan_dhcpd.range; | ||||||
|  |         let existing_mappings: &mut Vec<StaticMap> = &mut lan_dhcpd.staticmaps; | ||||||
| 
 | 
 | ||||||
|         if !Self::is_valid_mac(&mac) { |         if !Self::is_valid_mac(&mac) { | ||||||
|             return Err(DhcpError::InvalidMacAddress(mac)); |             return Err(DhcpError::InvalidMacAddress(mac)); | ||||||
| @ -70,8 +88,6 @@ impl<'a> DhcpConfig<'a> { | |||||||
|             return Err(DhcpError::IpAddressOutOfRange(ipaddr.to_string())); |             return Err(DhcpError::IpAddressOutOfRange(ipaddr.to_string())); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         let existing_mappings: &mut Vec<StaticMap> = &mut lan_dhcpd.staticmaps; |  | ||||||
| 
 |  | ||||||
|         if existing_mappings.iter().any(|m| { |         if existing_mappings.iter().any(|m| { | ||||||
|             m.ipaddr |             m.ipaddr | ||||||
|                 .parse::<Ipv4Addr>() |                 .parse::<Ipv4Addr>() | ||||||
| @ -128,6 +144,35 @@ impl<'a> DhcpConfig<'a> { | |||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     pub async fn get_static_mappings(&self) -> Result<Vec<StaticMap>, Error> { | ||||||
|  |         let list_static_output = self | ||||||
|  |             .opnsense_shell | ||||||
|  |             .exec("configctl dhcpd list static") | ||||||
|  |             .await?; | ||||||
|  | 
 | ||||||
|  |         let value: serde_json::Value = serde_json::from_str(&list_static_output).expect(&format!( | ||||||
|  |             "Got invalid json from configctl {list_static_output}" | ||||||
|  |         )); | ||||||
|  |         let static_maps = value["dhcpd"] | ||||||
|  |             .as_array() | ||||||
|  |             .ok_or(Error::Command(format!( | ||||||
|  |                 "Invalid DHCP data from configctl command, got {list_static_output}" | ||||||
|  |             )))? | ||||||
|  |             .iter() | ||||||
|  |             .map(|entry| StaticMap { | ||||||
|  |                 mac: entry["mac"].as_str().unwrap_or_default().to_string(), | ||||||
|  |                 ipaddr: entry["ipaddr"].as_str().unwrap_or_default().to_string(), | ||||||
|  |                 hostname: entry["hostname"].as_str().unwrap_or_default().to_string(), | ||||||
|  |                 descr: entry["descr"].as_str().map(MaybeString::from), | ||||||
|  |                 winsserver: MaybeString::default(), | ||||||
|  |                 dnsserver: MaybeString::default(), | ||||||
|  |                 ntpserver: MaybeString::default(), | ||||||
|  |             }) | ||||||
|  |             .collect(); | ||||||
|  | 
 | ||||||
|  |         Ok(static_maps) | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[cfg(test)] | #[cfg(test)] | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user