feat(opnsense-config): dnsmasq dhcp static mappings (#130)
All checks were successful
Run Check Script / check (pull_request) Successful in 59s

Co-authored-by: Jean-Gabriel Gill-Couture <jeangabriel.gc@gmail.com>
Co-authored-by: Ian Letourneau <ian@noma.to>
Reviewed-on: #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>
This commit is contained in:
2025-09-08 19:06:17 +00:00
committed by Ian Letourneau
parent b6be44202e
commit da5a869771
94 changed files with 5107 additions and 1469 deletions

View File

@@ -1,10 +1,10 @@
use std::sync::Arc;
use crate::{
config::{SshConfigManager, SshCredentials, SshOPNSenseShell},
config::{check_hash, get_hash, SshConfigManager, SshCredentials, SshOPNSenseShell},
error::Error,
modules::{
caddy::CaddyConfig, dhcp_legacy::DhcpConfigLegacyISC, dns::DnsConfig,
caddy::CaddyConfig, dhcp_legacy::DhcpConfigLegacyISC, dns::UnboundDnsConfig,
dnsmasq::DhcpConfigDnsMasq, load_balancer::LoadBalancerConfig, tftp::TftpConfig,
},
};
@@ -12,6 +12,7 @@ use log::{debug, info, trace, warn};
use opnsense_config_xml::OPNsense;
use russh::client;
use serde::Serialize;
use sha2::Digest;
use super::{ConfigManager, OPNsenseShell};
@@ -20,6 +21,7 @@ pub struct Config {
opnsense: OPNsense,
repository: Arc<dyn ConfigManager>,
shell: Arc<dyn OPNsenseShell>,
hash: String,
}
impl Serialize for Config {
@@ -36,8 +38,10 @@ impl Config {
repository: Arc<dyn ConfigManager>,
shell: Arc<dyn OPNsenseShell>,
) -> Result<Self, Error> {
let (opnsense, hash) = Self::get_opnsense_instance(repository.clone()).await?;
Ok(Self {
opnsense: Self::get_opnsense_instance(repository.clone()).await?,
opnsense,
hash,
repository,
shell,
})
@@ -51,8 +55,8 @@ impl Config {
DhcpConfigDnsMasq::new(&mut self.opnsense, self.shell.clone())
}
pub fn dns(&mut self) -> DnsConfig<'_> {
DnsConfig::new(&mut self.opnsense)
pub fn dns(&mut self) -> DhcpConfigDnsMasq<'_> {
DhcpConfigDnsMasq::new(&mut self.opnsense, self.shell.clone())
}
pub fn tftp(&mut self) -> TftpConfig<'_> {
@@ -146,7 +150,7 @@ impl Config {
async fn reload_config(&mut self) -> Result<(), Error> {
info!("Reloading opnsense live config");
self.opnsense = Self::get_opnsense_instance(self.repository.clone()).await?;
let (opnsense, sha2) = Self::get_opnsense_instance(self.repository.clone()).await?;
Ok(())
}
@@ -158,14 +162,15 @@ impl Config {
/// Save the config to the repository. This method is meant NOT to reload services, only save
/// the config to the live file/database and perhaps take a backup when relevant.
pub async fn save(&self) -> Result<(), Error> {
self.repository.save_config(&self.opnsense.to_xml()).await
let xml = &self.opnsense.to_xml();
self.repository.save_config(xml, &self.hash).await
}
/// Save the configuration and reload all services. Be careful with this one as it will cause
/// downtime in many cases, such as a PPPoE renegociation
pub async fn apply(&self) -> Result<(), Error> {
self.repository
.apply_new_config(&self.opnsense.to_xml())
.apply_new_config(&self.opnsense.to_xml(), &self.hash)
.await
}
@@ -193,11 +198,14 @@ impl Config {
Config::new(manager, shell).await.unwrap()
}
async fn get_opnsense_instance(repository: Arc<dyn ConfigManager>) -> Result<OPNsense, Error> {
async fn get_opnsense_instance(
repository: Arc<dyn ConfigManager>,
) -> Result<(OPNsense, String), Error> {
let xml = repository.load_as_str().await?;
trace!("xml {}", xml);
Ok(OPNsense::from(xml))
let hash = get_hash(&xml);
Ok((OPNsense::from(xml), hash))
}
pub async fn run_command(&self, command: &str) -> Result<String, Error> {
@@ -219,13 +227,14 @@ mod tests {
#[tokio::test]
async fn test_load_config_from_local_file() {
for path in [
"src/tests/data/config-opnsense-25.1.xml",
"src/tests/data/config-vm-test.xml",
// "src/tests/data/config-opnsense-25.1.xml",
// "src/tests/data/config-vm-test.xml",
"src/tests/data/config-structure.xml",
"src/tests/data/config-full-1.xml",
"src/tests/data/config-full-ncd0.xml",
"src/tests/data/config-full-25.7.xml",
"src/tests/data/config-full-25.7-dummy-dnsmasq-options.xml",
// "src/tests/data/config-full-ncd0.xml",
// "src/tests/data/config-full-25.7.xml",
// "src/tests/data/config-full-25.7-dummy-dnsmasq-options.xml",
"src/tests/data/config-25.7-dnsmasq-static-host.xml",
] {
let mut test_file_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
test_file_path.push(path);
@@ -243,13 +252,13 @@ mod tests {
let serialized = config.opnsense.to_xml();
fs::write("/tmp/serialized.xml", &serialized).unwrap();
// Since the order of all fields is not always the same in opnsense config files
// I think it is good enough to have exactly the same amount of the same lines
[config_file_str.lines().collect::<Vec<_>>()].sort();
[config_file_str.lines().collect::<Vec<_>>()].sort();
assert_eq!((), ());
let mut before = config_file_str.lines().collect::<Vec<_>>();
let mut after = serialized.lines().collect::<Vec<_>>();
before.sort();
after.sort();
assert_eq!(before, after);
}
}
@@ -279,8 +288,6 @@ mod tests {
let serialized = config.opnsense.to_xml();
fs::write("/tmp/serialized.xml", &serialized).unwrap();
let mut test_file_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
test_file_path.push("src/tests/data/config-structure-with-dhcp-staticmap-entry.xml");