forked from NationTech/harmony
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