harmony/opnsense-config/src/config/manager/ssh.rs
Jean-Gabriel Gill-Couture da5a869771
All checks were successful
Run Check Script / check (pull_request) Successful in 59s
feat(opnsense-config): dnsmasq dhcp static mappings (#130)
Co-authored-by: Jean-Gabriel Gill-Couture <jeangabriel.gc@gmail.com>
Co-authored-by: Ian Letourneau <ian@noma.to>
Reviewed-on: https://git.nationtech.io/NationTech/harmony/pulls/130
Reviewed-by: Ian Letourneau <ian@noma.to>
Co-authored-by: Jean-Gabriel Gill-Couture <jg@nationtech.io>
Co-committed-by: Jean-Gabriel Gill-Couture <jg@nationtech.io>
2025-09-08 19:06:17 +00:00

98 lines
2.9 KiB
Rust

use crate::config::{manager::ConfigManager, OPNsenseShell};
use crate::error::Error;
use async_trait::async_trait;
use log::{info, warn};
use russh_keys::key::KeyPair;
use sha2::Digest;
use std::sync::Arc;
#[derive(Debug)]
pub enum SshCredentials {
SshKey { username: String, key: Arc<KeyPair> },
Password { username: String, password: String },
}
#[derive(Debug)]
pub struct SshConfigManager {
opnsense_shell: Arc<dyn OPNsenseShell>,
}
impl SshConfigManager {
pub fn new(opnsense_shell: Arc<dyn OPNsenseShell>) -> Self {
Self { opnsense_shell }
}
}
impl SshConfigManager {
async fn backup_config_remote(&self) -> Result<String, Error> {
let ts = chrono::Utc::now();
let backup_filename = format!("config-{}-harmony.xml", ts.format("%s%.3f"));
self.opnsense_shell
.exec(&format!(
"cp /conf/config.xml /conf/backup/{}",
backup_filename
))
.await
}
async fn copy_to_live_config(&self, new_config_path: &str) -> Result<String, Error> {
info!("Overwriting OPNSense /conf/config.xml with {new_config_path}");
self.opnsense_shell
.exec(&format!("cp {new_config_path} /conf/config.xml"))
.await
}
async fn reload_all_services(&self) -> Result<String, Error> {
info!("Reloading all opnsense services");
self.opnsense_shell
.exec("configctl service reload all")
.await
}
}
#[async_trait]
impl ConfigManager for SshConfigManager {
async fn load_as_str(&self) -> Result<String, Error> {
self.opnsense_shell.exec("cat /conf/config.xml").await
}
async fn save_config(&self, content: &str, hash: &str) -> Result<(), Error> {
let current_content = self.load_as_str().await?;
if !check_hash(&current_content, hash) {
warn!("OPNSense config file changed since loading it! Hash when loading : {hash}");
// return Err(Error::Config(format!(
// "OPNSense config file changed since loading it! Hash when loading : {hash}"
// )));
}
let temp_filename = self
.opnsense_shell
.write_content_to_temp_file(content)
.await?;
self.backup_config_remote().await?;
self.copy_to_live_config(&temp_filename).await?;
Ok(())
}
async fn apply_new_config(&self, content: &str, hash: &str) -> Result<(), Error> {
self.save_config(content, &hash).await?;
self.reload_all_services().await?;
Ok(())
}
}
pub fn get_hash(content: &str) -> String {
let mut hasher = sha2::Sha256::new();
hasher.update(content.as_bytes());
let hash_bytes = hasher.finalize();
let hash_string = format!("{:x}", hash_bytes);
info!("Loaded OPNSense config.xml with hash {hash_string:?}");
hash_string
}
pub fn check_hash(content: &str, source_hash: &str) -> bool {
get_hash(content) == source_hash
}