feat: Can now save configuration, refactored repository into manager as it also executes commands to reload services and calling it a repository was found misleading by @stremblay"

This commit is contained in:
Jean-Gabriel Gill-Couture
2024-11-22 14:15:23 -05:00
parent 9a37aa1321
commit b14d0ab686
10 changed files with 334 additions and 173 deletions

View File

@@ -0,0 +1,206 @@
use crate::config::manager::ConfigManager;
use crate::error::Error;
use async_trait::async_trait;
use log::{debug, info};
use russh::{
client::{Config as SshConfig, Handler, Msg},
Channel,
};
use russh_keys::key::{self, KeyPair};
use russh_sftp::client::SftpSession;
use std::{
net::Ipv4Addr,
sync::Arc,
time::{SystemTime, UNIX_EPOCH},
};
use tokio::io::AsyncWriteExt;
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)
}
}
#[derive(Debug)]
pub enum SshCredentials {
SshKey { username: String, key: Arc<KeyPair> },
Password { username: String, password: String },
}
#[derive(Debug)]
pub struct SshConfigManager {
ssh_config: Arc<SshConfig>,
credentials: SshCredentials,
host: (Ipv4Addr, u16),
}
impl SshConfigManager {
pub fn new(
host: (Ipv4Addr, u16),
credentials: SshCredentials,
ssh_config: Arc<SshConfig>,
) -> Self {
Self {
ssh_config,
credentials,
host,
}
}
}
impl SshConfigManager {
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 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)
}
async fn backup_config_remote(&self) -> Result<String, Error> {
let backup_filename = format!("config_{}.xml", chrono::Local::now().format("%Y%m%d%H%M%S"));
self.run_command(&format!("cp /conf/config.xml /tmp/{}", backup_filename))
.await
}
async fn move_to_live_config(&self, new_config_path: &str) -> Result<String, Error> {
info!("Overwriting OPNSense /conf/config.xml with {new_config_path}");
self.run_command(&format!("mv {new_config_path} /conf/config.xml"))
.await
}
async fn reload_all_services(&self) -> Result<String, Error> {
info!("Reloading all opnsense services");
self.run_command(&format!("configctl service reload all"))
.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
}
}
#[async_trait]
impl ConfigManager for SshConfigManager {
async fn load_as_str(&self) -> Result<String, Error> {
let mut channel = self.get_ssh_channel().await?;
channel.exec(true, "cat /conf/config.xml").await?;
let mut output: Vec<u8> = vec![];
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 apply_new_config(&self, content: &str) -> Result<(), Error> {
let temp_filename = self.write_content_to_temp_file(content).await?;
self.backup_config_remote().await?;
self.move_to_live_config(&temp_filename).await?;
self.reload_all_services().await?;
// TODO
// 1. use russh to copy the current opnsense /conf/config.xml to the backup folder with a
// proper name
//
// 2. use russh scp functionality to copy the content directly into a file
// if necessary, save it first to a local file with minimal permissions in
// /tmp/{randomname}
//
// 3. Use opnsense cli to validate the config file
//
// 4. Reload all opnsense services using opnsense cli (still through russh commands)
//
todo!("apply_new_config not fully implemented yet")
}
}
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())
}