The previous implementation blindly added HAProxy components without checking for existing configurations on the same port, which caused duplicate entries and errors when a service was updated. This commit refactors the logic to a robust "remove-then-add" strategy. The configure_service method now finds and removes any existing frontend and its dependent components (backend, servers, health check) before adding the new, complete service definition. This change makes the process fully idempotent, preventing configuration drift and ensuring a predictable state. Co-authored-by: Ian Letourneau <letourneau.ian@gmail.com> Reviewed-on: #129
97 lines
2.9 KiB
Rust
97 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(¤t_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
|
|
}
|