forked from NationTech/harmony
		
	feat: Can now save configuration, refactored repository into manager as it also executes commands to reload services and calling it a repository was found misleading by @stremblay"
This commit is contained in:
		
							parent
							
								
									9a37aa1321
								
							
						
					
					
						commit
						b14d0ab686
					
				
							
								
								
									
										66
									
								
								harmony-rs/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										66
									
								
								harmony-rs/Cargo.lock
									
									
									
										generated
									
									
									
								
							| @ -67,6 +67,21 @@ dependencies = [ | |||||||
|  "memchr", |  "memchr", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "android-tzdata" | ||||||
|  | version = "0.1.1" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "android_system_properties" | ||||||
|  | version = "0.1.5" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" | ||||||
|  | dependencies = [ | ||||||
|  |  "libc", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "anstream" | name = "anstream" | ||||||
| version = "0.6.15" | version = "0.6.15" | ||||||
| @ -243,9 +258,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" | |||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "bytes" | name = "bytes" | ||||||
| version = "1.7.1" | version = "1.8.0" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" | checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "cbc" | name = "cbc" | ||||||
| @ -282,6 +297,20 @@ dependencies = [ | |||||||
|  "cpufeatures", |  "cpufeatures", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "chrono" | ||||||
|  | version = "0.4.38" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" | ||||||
|  | dependencies = [ | ||||||
|  |  "android-tzdata", | ||||||
|  |  "iana-time-zone", | ||||||
|  |  "js-sys", | ||||||
|  |  "num-traits", | ||||||
|  |  "wasm-bindgen", | ||||||
|  |  "windows-targets 0.52.6", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "cidr" | name = "cidr" | ||||||
| version = "0.2.3" | version = "0.2.3" | ||||||
| @ -950,6 +979,29 @@ dependencies = [ | |||||||
|  "tokio-native-tls", |  "tokio-native-tls", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "iana-time-zone" | ||||||
|  | version = "0.1.61" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" | ||||||
|  | dependencies = [ | ||||||
|  |  "android_system_properties", | ||||||
|  |  "core-foundation-sys", | ||||||
|  |  "iana-time-zone-haiku", | ||||||
|  |  "js-sys", | ||||||
|  |  "wasm-bindgen", | ||||||
|  |  "windows-core", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "iana-time-zone-haiku" | ||||||
|  | version = "0.1.2" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" | ||||||
|  | dependencies = [ | ||||||
|  |  "cc", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "idna" | name = "idna" | ||||||
| version = "0.5.0" | version = "0.5.0" | ||||||
| @ -1246,6 +1298,7 @@ name = "opnsense-config" | |||||||
| version = "0.1.0" | version = "0.1.0" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "async-trait", |  "async-trait", | ||||||
|  |  "chrono", | ||||||
|  "env_logger", |  "env_logger", | ||||||
|  "log", |  "log", | ||||||
|  "opnsense-config-xml", |  "opnsense-config-xml", | ||||||
| @ -2440,6 +2493,15 @@ version = "0.4.0" | |||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "windows-core" | ||||||
|  | version = "0.52.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" | ||||||
|  | dependencies = [ | ||||||
|  |  "windows-targets 0.52.6", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "windows-sys" | name = "windows-sys" | ||||||
| version = "0.48.0" | version = "0.48.0" | ||||||
|  | |||||||
| @ -15,6 +15,7 @@ thiserror = "1.0" | |||||||
| async-trait = { workspace = true } | 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" | ||||||
| 
 | 
 | ||||||
| [dev-dependencies] | [dev-dependencies] | ||||||
| pretty_assertions = "1.4.1" | pretty_assertions = "1.4.1" | ||||||
|  | |||||||
| @ -2,17 +2,17 @@ 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::ConfigRepository; | use super::ConfigManager; | ||||||
| 
 | 
 | ||||||
| #[derive(Debug)] | #[derive(Debug)] | ||||||
| pub struct Config { | pub struct Config { | ||||||
|     opnsense: OPNsense, |     opnsense: OPNsense, | ||||||
|     repository: Box<dyn ConfigRepository + Send + Sync>, |     repository: Box<dyn ConfigManager + Send + Sync>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl Config { | impl Config { | ||||||
|     pub async fn new(repository: Box<dyn ConfigRepository + Send + Sync>) -> Result<Self, Error> { |     pub async fn new(repository: Box<dyn ConfigManager + Send + Sync>) -> Result<Self, Error> { | ||||||
|         let xml = repository.load().await?; |         let xml = repository.load_as_str().await?; | ||||||
|         trace!("xml {}", xml); |         trace!("xml {}", xml); | ||||||
| 
 | 
 | ||||||
|         let opnsense = OPNsense::from(xml); |         let opnsense = OPNsense::from(xml); | ||||||
| @ -27,14 +27,14 @@ impl Config { | |||||||
|         DhcpConfig::new(&mut self.opnsense) |         DhcpConfig::new(&mut self.opnsense) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub async fn save(&self) -> Result<(), Error> { |     pub async fn apply(&self) -> Result<(), Error> { | ||||||
|         self.repository.save(&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::LocalFileConfigRepository; |     use crate::config::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,8 +55,8 @@ 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(LocalFileConfigRepository::new(config_file_path)); |             let repository = Box::new(LocalFileConfigManager::new(config_file_path)); | ||||||
|             let config_file_str = repository.load().await.unwrap(); |             let config_file_str = repository.load_as_str().await.unwrap(); | ||||||
|             let config = Config::new(repository) |             let config = Config::new(repository) | ||||||
|                 .await |                 .await | ||||||
|                 .expect("Failed to load config"); |                 .expect("Failed to load config"); | ||||||
| @ -82,7 +82,7 @@ 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(LocalFileConfigRepository::new(config_file_path)); |         let repository = Box::new(LocalFileConfigManager::new(config_file_path)); | ||||||
|         let mut config = Config::new(repository) |         let mut config = Config::new(repository) | ||||||
|             .await |             .await | ||||||
|             .expect("Failed to load config"); |             .expect("Failed to load config"); | ||||||
| @ -107,8 +107,8 @@ 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(LocalFileConfigRepository::new(config_file_path)); |         let repository = Box::new(LocalFileConfigManager::new(config_file_path)); | ||||||
|         let expected_config_file_str = repository.load().await.unwrap(); |         let expected_config_file_str = repository.load_as_str().await.unwrap(); | ||||||
|         assert_eq!(expected_config_file_str, serialized); |         assert_eq!(expected_config_file_str, serialized); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										26
									
								
								harmony-rs/opnsense-config/src/config/manager/local_file.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								harmony-rs/opnsense-config/src/config/manager/local_file.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,26 @@ | |||||||
|  | use crate::config::manager::ConfigManager; | ||||||
|  | use crate::error::Error; | ||||||
|  | use async_trait::async_trait; | ||||||
|  | use std::fs; | ||||||
|  | 
 | ||||||
|  | #[derive(Debug)] | ||||||
|  | pub struct LocalFileConfigManager { | ||||||
|  |     file_path: String, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl LocalFileConfigManager { | ||||||
|  |     pub fn new(file_path: String) -> Self { | ||||||
|  |         Self { file_path } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[async_trait] | ||||||
|  | impl ConfigManager for LocalFileConfigManager { | ||||||
|  |     async fn load_as_str(&self) -> Result<String, Error> { | ||||||
|  |         Ok(fs::read_to_string(&self.file_path)?) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async fn apply_new_config(&self, content: &str) -> Result<(), Error> { | ||||||
|  |         Ok(fs::write(&self.file_path, content)?) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										13
									
								
								harmony-rs/opnsense-config/src/config/manager/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								harmony-rs/opnsense-config/src/config/manager/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | |||||||
|  | mod ssh; | ||||||
|  | mod local_file; | ||||||
|  | use async_trait::async_trait; | ||||||
|  | pub use ssh::*; | ||||||
|  | pub use local_file::*; | ||||||
|  | 
 | ||||||
|  | use crate::Error; | ||||||
|  | 
 | ||||||
|  | #[async_trait] | ||||||
|  | pub trait ConfigManager: std::fmt::Debug { | ||||||
|  |     async fn load_as_str(&self) -> Result<String, Error>; | ||||||
|  |     async fn apply_new_config(&self, content: &str) -> Result<(), Error>; | ||||||
|  | } | ||||||
							
								
								
									
										206
									
								
								harmony-rs/opnsense-config/src/config/manager/ssh.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										206
									
								
								harmony-rs/opnsense-config/src/config/manager/ssh.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,206 @@ | |||||||
|  | use crate::config::manager::ConfigManager; | ||||||
|  | use crate::error::Error; | ||||||
|  | use async_trait::async_trait; | ||||||
|  | use log::{debug, info}; | ||||||
|  | use russh::{ | ||||||
|  |     client::{Config as SshConfig, Handler, Msg}, | ||||||
|  |     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)] | ||||||
|  | pub enum SshCredentials { | ||||||
|  |     SshKey { username: String, key: Arc<KeyPair> }, | ||||||
|  |     Password { username: String, password: String }, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug)] | ||||||
|  | pub struct SshConfigManager { | ||||||
|  |     ssh_config: Arc<SshConfig>, | ||||||
|  |     credentials: SshCredentials, | ||||||
|  |     host: (Ipv4Addr, u16), | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl SshConfigManager { | ||||||
|  |     pub fn new( | ||||||
|  |         host: (Ipv4Addr, u16), | ||||||
|  |         credentials: SshCredentials, | ||||||
|  |         ssh_config: Arc<SshConfig>, | ||||||
|  |     ) -> Self { | ||||||
|  |         Self { | ||||||
|  |             ssh_config, | ||||||
|  |             credentials, | ||||||
|  |             host, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 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> { | ||||||
|  |         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)) | ||||||
|  |             .await | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async fn move_to_live_config(&self, new_config_path: &str) -> Result<String, Error> { | ||||||
|  |         info!("Overwriting OPNSense /conf/config.xml with {new_config_path}"); | ||||||
|  |         self.run_command(&format!("mv {new_config_path} /conf/config.xml")) | ||||||
|  |             .await | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async fn reload_all_services(&self) -> Result<String, Error> { | ||||||
|  |         info!("Reloading all opnsense services"); | ||||||
|  |         self.run_command(&format!("configctl service reload all")) | ||||||
|  |             .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] | ||||||
|  | impl ConfigManager for SshConfigManager { | ||||||
|  |     async fn load_as_str(&self) -> Result<String, Error> { | ||||||
|  |         let mut channel = self.get_ssh_channel().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> { | ||||||
|  |         let temp_filename = self.write_content_to_temp_file(content).await?; | ||||||
|  |         self.backup_config_remote().await?; | ||||||
|  |         self.move_to_live_config(&temp_filename).await?; | ||||||
|  |         self.reload_all_services().await?; | ||||||
|  | 
 | ||||||
|  |         // 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,4 @@ | |||||||
| mod config; | mod config; | ||||||
| mod repository; | mod manager; | ||||||
| pub use repository::*; | pub use manager::*; | ||||||
| pub use config::*; | pub use config::*; | ||||||
|  | |||||||
| @ -1,151 +0,0 @@ | |||||||
| use crate::error::Error; |  | ||||||
| use async_trait::async_trait; |  | ||||||
| use log::info; |  | ||||||
| use russh::{ |  | ||||||
|     client::{Config as SshConfig, Handler, Msg}, |  | ||||||
|     Channel, |  | ||||||
| }; |  | ||||||
| use russh_keys::key::{self, KeyPair}; |  | ||||||
| use std::{fs, net::Ipv4Addr, sync::Arc}; |  | ||||||
| 
 |  | ||||||
| #[async_trait] |  | ||||||
| pub trait ConfigRepository: std::fmt::Debug { |  | ||||||
|     async fn load(&self) -> Result<String, Error>; |  | ||||||
|     async fn save(&self, content: &str) -> Result<(), Error>; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 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)] |  | ||||||
| pub enum SshCredentials { |  | ||||||
|     SshKey { username: String, key: Arc<KeyPair> }, |  | ||||||
|     Password { username: String, password: String }, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[derive(Debug)] |  | ||||||
| pub struct SshConfigRepository { |  | ||||||
|     ssh_config: Arc<SshConfig>, |  | ||||||
|     credentials: SshCredentials, |  | ||||||
|     host: (Ipv4Addr, u16), |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl SshConfigRepository { |  | ||||||
|     pub fn new( |  | ||||||
|         host: (Ipv4Addr, u16), |  | ||||||
|         credentials: SshCredentials, |  | ||||||
|         ssh_config: Arc<SshConfig>, |  | ||||||
|     ) -> Self { |  | ||||||
|         Self { |  | ||||||
|             ssh_config, |  | ||||||
|             credentials, |  | ||||||
|             host, |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl SshConfigRepository { |  | ||||||
|     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_trait] |  | ||||||
| impl ConfigRepository for SshConfigRepository { |  | ||||||
|     async fn load(&self) -> Result<String, Error> { |  | ||||||
|         let mut channel = self.get_ssh_channel().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 save(&self, content: &str) -> Result<(), Error> { |  | ||||||
|         todo!("Backup, Validate, Reload config file"); |  | ||||||
|         let mut channel = self.get_ssh_channel().await?; |  | ||||||
| 
 |  | ||||||
|         let command = format!( |  | ||||||
|             "echo '{}' > /conf/config.xml", |  | ||||||
|             content.replace("'", "'\"'\"'") |  | ||||||
|         ); |  | ||||||
|         channel.exec(true, command.as_bytes()).await?; |  | ||||||
| 
 |  | ||||||
|         loop { |  | ||||||
|             let Some(msg) = channel.wait().await else { |  | ||||||
|                 break; |  | ||||||
|             }; |  | ||||||
| 
 |  | ||||||
|             match msg { |  | ||||||
|                 russh::ChannelMsg::ExitStatus { exit_status } => { |  | ||||||
|                     if exit_status != 0 { |  | ||||||
|                         return Err(Error::Ssh(russh::Error::Disconnect)); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|                 _ => {} |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         Ok(()) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[derive(Debug)] |  | ||||||
| pub struct LocalFileConfigRepository { |  | ||||||
|     file_path: String, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl LocalFileConfigRepository { |  | ||||||
|     pub fn new(file_path: String) -> Self { |  | ||||||
|         Self { file_path } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[async_trait] |  | ||||||
| impl ConfigRepository for LocalFileConfigRepository { |  | ||||||
|     async fn load(&self) -> Result<String, Error> { |  | ||||||
|         Ok(fs::read_to_string(&self.file_path)?) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     async fn save(&self, content: &str) -> Result<(), Error> { |  | ||||||
|         Ok(fs::write(&self.file_path, content)?) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -6,8 +6,12 @@ pub enum Error { | |||||||
|     Xml(String), |     Xml(String), | ||||||
|     #[error("SSH error: {0}")] |     #[error("SSH error: {0}")] | ||||||
|     Ssh(#[from] russh::Error), |     Ssh(#[from] russh::Error), | ||||||
|  |     #[error("Command failed : {0}")] | ||||||
|  |     Command(String), | ||||||
|     #[error("I/O error: {0}")] |     #[error("I/O error: {0}")] | ||||||
|     Io(#[from] std::io::Error), |     Io(#[from] std::io::Error), | ||||||
|     #[error("Config error: {0}")] |     #[error("Config error: {0}")] | ||||||
|     Config(String), |     Config(String), | ||||||
|  |     #[error("Unexpected error: {0}")] | ||||||
|  |     Unexpected(String), | ||||||
| } | } | ||||||
|  | |||||||
| @ -6,7 +6,7 @@ pub use config::Config; | |||||||
| pub use error::Error; | pub use error::Error; | ||||||
| #[cfg(test)] | #[cfg(test)] | ||||||
| mod test { | mod test { | ||||||
|     use config::SshConfigRepository; |     use config::SshConfigManager; | ||||||
|     use russh::client; |     use russh::client; | ||||||
|     use std::{net::Ipv4Addr, sync::Arc, time::Duration}; |     use std::{net::Ipv4Addr, sync::Arc, time::Duration}; | ||||||
| 
 | 
 | ||||||
| @ -28,18 +28,18 @@ mod test { | |||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         let repo = |         let repo = | ||||||
|             SshConfigRepository::new((Ipv4Addr::new(192, 168, 5, 229), 22), credentials, config); |             SshConfigManager::new((Ipv4Addr::new(192, 168, 5, 229), 22), credentials, config); | ||||||
|  | 
 | ||||||
|         let mut config = Config::new(Box::new(repo)).await.unwrap(); |         let mut config = Config::new(Box::new(repo)).await.unwrap(); | ||||||
|         config |         config | ||||||
|             .dhcp() |             .dhcp() | ||||||
|             .add_static_mapping( |             .add_static_mapping( | ||||||
|                 "test_mac", |                 "11:22:33:44:55:66", | ||||||
|                 Ipv4Addr::new(192, 168, 168, 168), |                 Ipv4Addr::new(10, 100, 8, 200), | ||||||
|                 "test_hostname", |                 "test_hostname", | ||||||
|             ) |             ) | ||||||
|             .unwrap(); |             .unwrap(); | ||||||
| 
 | 
 | ||||||
|         todo!(); |         config.apply().await.unwrap(); | ||||||
|         // opnsense.apply_changes().await;
 |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user