forked from NationTech/harmony
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:
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())
|
||||
}
|
||||
Reference in New Issue
Block a user