forked from NationTech/harmony
feat(harmony): add TFTP server functionality (#10)
Introduce a new module and interface for serving files via TFTP in the HAClusterTopology structure. This includes adding the necessary dependencies, creating the `TftpServer` trait, implementing it where appropriate, and integrating its usage within the topology struct. Reviewed-on: NationTech/harmony#10 Co-authored-by: Jean-Gabriel Gill-Couture <jg@nationtech.io> Co-committed-by: Jean-Gabriel Gill-Couture <jg@nationtech.io>
This commit is contained in:
@@ -9,6 +9,7 @@ use crate::Error;
|
||||
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>;
|
||||
async fn upload_folder(&self, source: &str, destination: &str) -> Result<String, Error>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -24,4 +25,7 @@ impl OPNsenseShell for DummyOPNSenseShell {
|
||||
async fn write_content_to_temp_file(&self, _content: &str) -> Result<String, Error> {
|
||||
unimplemented!("This is a dummy implementation");
|
||||
}
|
||||
async fn upload_folder(&self, _source: &str, _destination: &str) -> Result<String, Error> {
|
||||
unimplemented!("This is a dummy implementation");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::{
|
||||
sync::Arc,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use log::{debug, info};
|
||||
@@ -11,12 +12,15 @@ use russh::{
|
||||
Channel,
|
||||
};
|
||||
use russh_keys::key;
|
||||
use russh_sftp::client::SftpSession;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use russh_sftp::{client::SftpSession, protocol::OpenFlags};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
use crate::{config::SshCredentials, Error};
|
||||
|
||||
use super::OPNsenseShell;
|
||||
use tokio::fs::read_dir;
|
||||
use tokio::fs::File;
|
||||
use tokio_util::codec::{BytesCodec, FramedRead};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SshOPNSenseShell {
|
||||
@@ -54,6 +58,72 @@ impl OPNsenseShell for SshOPNSenseShell {
|
||||
|
||||
Ok(temp_filename)
|
||||
}
|
||||
|
||||
async fn upload_folder(&self, source: &str, destination: &str) -> Result<String, Error> {
|
||||
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");
|
||||
|
||||
if !sftp.try_exists(destination).await? {
|
||||
info!("Creating remote directory {destination}");
|
||||
sftp.create_dir(destination).await?;
|
||||
}
|
||||
|
||||
info!("Reading local directory {source}");
|
||||
let mut entries = read_dir(source).await?;
|
||||
while let Some(entry) = entries.next_entry().await? {
|
||||
info!(
|
||||
"Checking directory entry {}",
|
||||
entry
|
||||
.path()
|
||||
.to_str()
|
||||
.expect("Directory entry should have a path : {entry:?}")
|
||||
);
|
||||
if entry.file_type().await?.is_file() {
|
||||
debug!("Got a file");
|
||||
let local_path = entry.path();
|
||||
debug!("path {local_path:?}");
|
||||
let file_name = local_path.file_name().unwrap().to_string_lossy();
|
||||
let remote_path = format!("{}/{}", destination, file_name);
|
||||
info!(
|
||||
"Uploading local file {} to remote {}",
|
||||
local_path.to_str().unwrap_or_default(),
|
||||
remote_path
|
||||
);
|
||||
|
||||
debug!("Creating file {remote_path:?}");
|
||||
let mut remote_file = sftp.create(remote_path.as_str()).await?;
|
||||
|
||||
debug!("Writing file {remote_path:?}");
|
||||
let local_file = File::open(&local_path).await?;
|
||||
let mut reader = FramedRead::new(local_file, BytesCodec::new());
|
||||
|
||||
while let Some(result) = reader.next().await {
|
||||
match result {
|
||||
Ok(bytes) => {
|
||||
if !bytes.is_empty() {
|
||||
remote_file.write(&bytes).await?;
|
||||
}
|
||||
}
|
||||
Err(e) => todo!("Error unhandled {e}"),
|
||||
};
|
||||
}
|
||||
} else if entry.file_type().await?.is_dir() {
|
||||
let sub_source = entry.path();
|
||||
let sub_destination =
|
||||
format!("{}/{}", destination, entry.file_name().to_string_lossy());
|
||||
self.upload_folder(sub_source.to_str().unwrap(), &sub_destination)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(destination.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl SshOPNSenseShell {
|
||||
@@ -80,6 +150,7 @@ impl SshOPNSenseShell {
|
||||
}
|
||||
|
||||
pub fn new(host: (IpAddr, u16), credentials: SshCredentials, ssh_config: Arc<Config>) -> Self {
|
||||
info!("Initializing SshOPNSenseShell on host {host:?}");
|
||||
Self {
|
||||
host,
|
||||
credentials,
|
||||
|
||||
Reference in New Issue
Block a user