feat(opnsense-config): Refactor config to use a repository trait, implement file based and ssh, save a full config file
This commit is contained in:
		
							parent
							
								
									8459c38499
								
							
						
					
					
						commit
						b332723431
					
				
							
								
								
									
										8
									
								
								harmony-rs/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								harmony-rs/Cargo.lock
									
									
									
										generated
									
									
									
								
							| @ -1240,8 +1240,10 @@ name = "opnsense-config" | |||||||
| version = "0.1.0" | version = "0.1.0" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "async-trait", |  "async-trait", | ||||||
|  |  "log", | ||||||
|  "russh", |  "russh", | ||||||
|  "russh-keys", |  "russh-keys", | ||||||
|  |  "serde", | ||||||
|  "thiserror", |  "thiserror", | ||||||
|  "tokio", |  "tokio", | ||||||
|  "xml-rs", |  "xml-rs", | ||||||
| @ -2594,8 +2596,7 @@ dependencies = [ | |||||||
| [[package]] | [[package]] | ||||||
| name = "yaserde" | name = "yaserde" | ||||||
| version = "0.11.1" | version = "0.11.1" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "git+https://git.nationtech.io/NationTech/yaserde#353558737f3ef73e93164c596ff920d4344f30a3" | ||||||
| checksum = "d8198a8ee4113411b7be1086e10b654f83653c01e4bd176fb98fe9d11951af5e" |  | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "log", |  "log", | ||||||
|  "xml-rs", |  "xml-rs", | ||||||
| @ -2604,8 +2605,7 @@ dependencies = [ | |||||||
| [[package]] | [[package]] | ||||||
| name = "yaserde_derive" | name = "yaserde_derive" | ||||||
| version = "0.11.1" | version = "0.11.1" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "git+https://git.nationtech.io/NationTech/yaserde#353558737f3ef73e93164c596ff920d4344f30a3" | ||||||
| checksum = "82eaa312529cc56b0df120253c804a8c8d593d2b5fe8deb5402714f485f62d79" |  | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "heck", |  "heck", | ||||||
|  "log", |  "log", | ||||||
|  | |||||||
| @ -4,10 +4,12 @@ version = "0.1.0" | |||||||
| edition = "2021" | edition = "2021" | ||||||
| 
 | 
 | ||||||
| [dependencies] | [dependencies] | ||||||
|  | serde = { version = "1.0.123", features = [ "derive" ] } | ||||||
|  | log = { workspace = true } | ||||||
| russh = { workspace = true } | russh = { workspace = true } | ||||||
| russh-keys = { workspace = true } | russh-keys = { workspace = true } | ||||||
| yaserde = "0.11.1" | yaserde = { git = "https://git.nationtech.io/NationTech/yaserde" } | ||||||
| yaserde_derive = "0.11.1" | yaserde_derive = { git = "https://git.nationtech.io/NationTech/yaserde" } | ||||||
| xml-rs = "0.8" | xml-rs = "0.8" | ||||||
| thiserror = "1.0" | thiserror = "1.0" | ||||||
| async-trait = { workspace = true } | async-trait = { workspace = true } | ||||||
|  | |||||||
| @ -1,16 +1,19 @@ | |||||||
| use crate::error::Error; | use crate::error::Error; | ||||||
| use crate::modules::opnsense::OPNsense; | use crate::modules::opnsense::OPNsense; | ||||||
| use async_trait::async_trait; | use async_trait::async_trait; | ||||||
|  | use log::info; | ||||||
| use russh::client::{Config as SshConfig, Handler}; | use russh::client::{Config as SshConfig, Handler}; | ||||||
| use russh_keys::key; | use russh_keys::key; | ||||||
| use std::{fmt::Write as _, sync::Arc}; | use std::{fs, net::Ipv4Addr, path::Path, sync::Arc}; | ||||||
| use tokio::io::AsyncWriteExt; | 
 | ||||||
|  | #[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 {} | struct Client {} | ||||||
| 
 | 
 | ||||||
| // More SSH event handlers
 |  | ||||||
| // can be defined in this trait
 |  | ||||||
| // In this example, we're only using Channel, so these aren't needed.
 |  | ||||||
| #[async_trait] | #[async_trait] | ||||||
| impl Handler for Client { | impl Handler for Client { | ||||||
|     type Error = Error; |     type Error = Error; | ||||||
| @ -23,55 +26,131 @@ impl Handler for Client { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| pub struct Config { | #[derive(Debug)] | ||||||
|     opnsense: OPNsense, | pub struct SshConfigRepository { | ||||||
|     ssh_config: Arc<SshConfig>, |     ssh_config: Arc<SshConfig>, | ||||||
|     host: String, |  | ||||||
|     username: String, |     username: String, | ||||||
|     key: Arc<key::KeyPair>, |     key: Arc<key::KeyPair>, | ||||||
|  |     host: (Ipv4Addr, u16), | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl Config { | impl SshConfigRepository { | ||||||
|     pub async fn new(host: &str, username: &str, key_path: &str) -> Result<Self, Error> { |     pub fn new( | ||||||
|         let key = russh_keys::load_secret_key(key_path, None).expect("Secret key failed loading"); |         host: (Ipv4Addr, u16), | ||||||
|         let key = Arc::new(key); |         username: String, | ||||||
|         let config = SshConfig::default(); |         key: Arc<key::KeyPair>, | ||||||
|         let config = Arc::new(config); |         ssh_config: Arc<SshConfig>, | ||||||
|  |     ) -> Self { | ||||||
|  |         Self { | ||||||
|  |             ssh_config, | ||||||
|  |             username, | ||||||
|  |             key, | ||||||
|  |             host, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
|         let mut ssh = russh::client::connect(config.clone(), host, Client {}).await?; | #[async_trait] | ||||||
|         ssh.authenticate_publickey(username, key.clone()).await?; | impl ConfigRepository for SshConfigRepository { | ||||||
|  |     async fn load(&self) -> Result<String, Error> { | ||||||
|  |         let mut ssh = russh::client::connect(self.ssh_config.clone(), self.host, Client {}).await?; | ||||||
|  |         ssh.authenticate_publickey(&self.username, self.key.clone()) | ||||||
|  |             .await?; | ||||||
| 
 | 
 | ||||||
|         let mut channel = ssh.channel_open_session().await?; |         let mut channel = ssh.channel_open_session().await?; | ||||||
| 
 | 
 | ||||||
|         channel.exec(true, "cat /conf/config.xml").await?; |         channel.exec(true, "cat /conf/config.xml").await?; | ||||||
|         let mut code; |         let mut output: Vec<u8> = vec![]; | ||||||
|         let mut output = String::new(); |         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> { | ||||||
|  |         let mut ssh = russh::client::connect(self.ssh_config.clone(), self.host, Client {}).await?; | ||||||
|  |         ssh.authenticate_publickey(&self.username, self.key.clone()) | ||||||
|  |             .await?; | ||||||
|  | 
 | ||||||
|  |         let mut channel = ssh.channel_open_session().await?; | ||||||
|  | 
 | ||||||
|  |         let command = format!( | ||||||
|  |             "echo '{}' > /conf/config.xml", | ||||||
|  |             content.replace("'", "'\"'\"'") | ||||||
|  |         ); | ||||||
|  |         channel.exec(true, command.as_bytes()).await?; | ||||||
|  | 
 | ||||||
|         loop { |         loop { | ||||||
|             let Some(msg) = channel.wait().await else { |             let Some(msg) = channel.wait().await else { | ||||||
|                 break; |                 break; | ||||||
|             }; |             }; | ||||||
| 
 | 
 | ||||||
|             match msg { |             match msg { | ||||||
|                 russh::ChannelMsg::Data { ref data } => { |  | ||||||
|                     write!(&mut output, "{:?}", data); |  | ||||||
|                     println!("Got data {output}"); |  | ||||||
|                 } |  | ||||||
|                 russh::ChannelMsg::ExitStatus { exit_status } => { |                 russh::ChannelMsg::ExitStatus { exit_status } => { | ||||||
|                     code = Some(exit_status); |                     if exit_status != 0 { | ||||||
|                 } |                         return Err(Error::Ssh(russh::Error::Disconnect)); | ||||||
|                 _ => todo!(), |  | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|         let xml = output; |                 _ => {} | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         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)?) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug)] | ||||||
|  | pub struct Config { | ||||||
|  |     opnsense: OPNsense, | ||||||
|  |     repository: Box<dyn ConfigRepository + Send + Sync>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Config { | ||||||
|  |     pub async fn new(repository: Box<dyn ConfigRepository + Send + Sync>) -> Result<Self, Error> { | ||||||
|  |         let xml = repository.load().await?; | ||||||
|  |         info!("xml {}", xml); | ||||||
| 
 | 
 | ||||||
|         let opnsense = yaserde::de::from_str(&xml).map_err(|e| Error::Xml(e.to_string()))?; |         let opnsense = yaserde::de::from_str(&xml).map_err(|e| Error::Xml(e.to_string()))?; | ||||||
| 
 | 
 | ||||||
|         Ok(Self { |         Ok(Self { | ||||||
|             opnsense, |             opnsense, | ||||||
|             ssh_config: config, |             repository, | ||||||
|             host: host.to_string(), |  | ||||||
|             username: username.to_string(), |  | ||||||
|             key, |  | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -85,15 +164,28 @@ impl Config { | |||||||
| 
 | 
 | ||||||
|     pub async fn save(&self) -> Result<(), Error> { |     pub async fn save(&self) -> Result<(), Error> { | ||||||
|         let xml = yaserde::ser::to_string(&self.opnsense).map_err(|e| Error::Xml(e.to_string()))?; |         let xml = yaserde::ser::to_string(&self.opnsense).map_err(|e| Error::Xml(e.to_string()))?; | ||||||
| 
 |         self.repository.save(&xml).await | ||||||
|         let mut ssh = |     } | ||||||
|             russh::client::connect(self.ssh_config.clone(), &self.host, Client {}).await?; | } | ||||||
|         ssh.authenticate_publickey(&self.username, self.key.clone()).await?; | 
 | ||||||
|         todo!("Writing config file to remote host {xml}"); | #[cfg(test)] | ||||||
| 
 | mod tests { | ||||||
|         // ssh.exec(true, &format!("echo '{}' > /conf/config.xml", xml))
 |     use super::*; | ||||||
|         //     .await?;
 |     use std::path::PathBuf; | ||||||
| 
 | 
 | ||||||
|         // Ok(())
 |     #[tokio::test] | ||||||
|  |     async fn test_load_config_from_local_file() { | ||||||
|  |         let mut test_file_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); | ||||||
|  |         test_file_path.push("src/tests/data/config-full-1.xml"); | ||||||
|  | 
 | ||||||
|  |         let config_file_path = test_file_path.to_str().unwrap().to_string(); | ||||||
|  |         println!("File path {config_file_path}"); | ||||||
|  |         let repository = Box::new(LocalFileConfigRepository::new(config_file_path)); | ||||||
|  |         let config = Config::new(repository) | ||||||
|  |             .await | ||||||
|  |             .expect("Failed to load config"); | ||||||
|  | 
 | ||||||
|  |         println!("Config {:?}", config); | ||||||
|  |         assert!(false); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										2778
									
								
								harmony-rs/opnsense-config/src/tests/data/config-full-1.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2778
									
								
								harmony-rs/opnsense-config/src/tests/data/config-full-1.xml
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Loading…
	
		Reference in New Issue
	
	Block a user