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:
2025-01-07 19:12:35 +00:00
committed by johnride
parent 098cb30523
commit 925e84e4d2
25 changed files with 914 additions and 320 deletions

View File

@@ -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");
}
}

View File

@@ -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,