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: https://git.nationtech.io/NationTech/harmony/pulls/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:
Jean-Gabriel Gill-Couture 2025-01-07 19:12:35 +00:00 committed by johnride
parent 098cb30523
commit 925e84e4d2
25 changed files with 914 additions and 320 deletions

310
harmony-rs/Cargo.lock generated
View File

@ -529,6 +529,17 @@ dependencies = [
"subtle",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
]
[[package]]
name = "ecdsa"
version = "0.16.9"
@ -893,6 +904,7 @@ dependencies = [
"serde",
"serde_json",
"tokio",
"url",
"uuid",
]
@ -1057,13 +1069,142 @@ dependencies = [
]
[[package]]
name = "idna"
version = "0.5.0"
name = "icu_collections"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
dependencies = [
"unicode-bidi",
"unicode-normalization",
"displaydoc",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_locid"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
dependencies = [
"displaydoc",
"litemap",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
name = "icu_locid_transform"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
dependencies = [
"displaydoc",
"icu_locid",
"icu_locid_transform_data",
"icu_provider",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_locid_transform_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
[[package]]
name = "icu_normalizer"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
dependencies = [
"displaydoc",
"icu_collections",
"icu_normalizer_data",
"icu_properties",
"icu_provider",
"smallvec",
"utf16_iter",
"utf8_iter",
"write16",
"zerovec",
]
[[package]]
name = "icu_normalizer_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
[[package]]
name = "icu_properties"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5"
dependencies = [
"displaydoc",
"icu_collections",
"icu_locid_transform",
"icu_properties_data",
"icu_provider",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_properties_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
[[package]]
name = "icu_provider"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
dependencies = [
"displaydoc",
"icu_locid",
"icu_provider_macros",
"stable_deref_trait",
"tinystr",
"writeable",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_provider_macros"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
]
[[package]]
name = "idna"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
dependencies = [
"idna_adapter",
"smallvec",
"utf8_iter",
]
[[package]]
name = "idna_adapter"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71"
dependencies = [
"icu_normalizer",
"icu_properties",
]
[[package]]
@ -1153,6 +1294,12 @@ version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]]
name = "litemap"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104"
[[package]]
name = "lock_api"
version = "0.4.12"
@ -1384,6 +1531,8 @@ dependencies = [
"serde_json",
"thiserror",
"tokio",
"tokio-stream",
"tokio-util",
]
[[package]]
@ -2230,6 +2379,12 @@ dependencies = [
"zeroize",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "subtle"
version = "2.6.1"
@ -2264,6 +2419,17 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]]
name = "synstructure"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
]
[[package]]
name = "system-configuration"
version = "0.5.1"
@ -2334,20 +2500,15 @@ dependencies = [
]
[[package]]
name = "tinyvec"
version = "1.8.0"
name = "tinystr"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938"
checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
dependencies = [
"tinyvec_macros",
"displaydoc",
"zerovec",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.40.0"
@ -2388,9 +2549,9 @@ dependencies = [
[[package]]
name = "tokio-stream"
version = "0.1.16"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1"
checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
dependencies = [
"futures-core",
"pin-project-lite",
@ -2399,9 +2560,9 @@ dependencies = [
[[package]]
name = "tokio-util"
version = "0.7.12"
version = "0.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a"
checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078"
dependencies = [
"bytes",
"futures-core",
@ -2447,27 +2608,12 @@ version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "unicode-bidi"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unicode-normalization"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
dependencies = [
"tinyvec",
]
[[package]]
name = "universal-hash"
version = "0.5.1"
@ -2480,15 +2626,27 @@ dependencies = [
[[package]]
name = "url"
version = "2.5.2"
version = "2.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
]
[[package]]
name = "utf16_iter"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utf8parse"
version = "0.2.2"
@ -2527,6 +2685,7 @@ dependencies = [
"harmony_macros",
"log",
"tokio",
"url",
]
[[package]]
@ -2834,6 +2993,18 @@ dependencies = [
"tokio",
]
[[package]]
name = "write16"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
[[package]]
name = "writeable"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "wyz"
version = "0.5.1"
@ -2875,6 +3046,30 @@ dependencies = [
"xml-rs",
]
[[package]]
name = "yoke"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40"
dependencies = [
"serde",
"stable_deref_trait",
"yoke-derive",
"zerofrom",
]
[[package]]
name = "yoke-derive"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
"synstructure",
]
[[package]]
name = "zerocopy"
version = "0.7.35"
@ -2896,8 +3091,51 @@ dependencies = [
"syn 2.0.90",
]
[[package]]
name = "zerofrom"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
"synstructure",
]
[[package]]
name = "zeroize"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
[[package]]
name = "zerovec"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
dependencies = [
"yoke",
"zerofrom",
"zerovec-derive",
]
[[package]]
name = "zerovec-derive"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
]

View File

@ -17,11 +17,12 @@ log = "0.4.22"
env_logger = "0.11.5"
derive-new = "0.7.0"
async-trait = "0.1.82"
tokio = { version = "1.40.0", features = ["io-std"] }
tokio = { version = "1.40.0", features = ["io-std", "fs"] }
cidr = "0.2.3"
russh = "0.45.0"
russh-keys = "0.45.0"
rand = "0.8.5"
url = "2.5.4"
[workspace.dependencies.uuid]
version = "1.11.0"

View File

@ -12,3 +12,4 @@ tokio = { workspace = true }
harmony_macros = { version = "1.0.0", path = "../../harmony_macros" }
log = { workspace = true }
env_logger = { workspace = true }
url = { workspace = true }

View File

@ -9,8 +9,8 @@ use harmony::{
infra::opnsense::OPNSenseManagementInterface,
inventory::Inventory,
maestro::Maestro,
modules::okd::{dhcp::OKDBootstrapDhcpScore, dns::OKDBootstrapDnsScore},
topology::{LogicalHost, UnmanagedRouter},
modules::{okd::{dhcp::OKDBootstrapDhcpScore, dns::OKDBootstrapDnsScore}, tftp::TftpScore},
topology::{LogicalHost, UnmanagedRouter, Url},
};
use harmony_macros::ip;
@ -23,14 +23,10 @@ async fn main() {
name: String::from("opnsense-1"),
};
let firewall = harmony::topology::LogicalHost {
ip: ip!("127.0.0.1"),
name: String::from("opnsense-1"),
};
let opnsense = Arc::new(
harmony::infra::opnsense::OPNSenseFirewall::new(
firewall,
Some(2222),
None,
"lan",
"root",
"opnsense",
@ -48,6 +44,7 @@ async fn main() {
)),
load_balancer: opnsense.clone(),
firewall: opnsense.clone(),
tftp_server: opnsense.clone(),
dhcp_server: opnsense.clone(),
dns_server: opnsense.clone(),
control_plane: vec![LogicalHost {
@ -81,11 +78,13 @@ async fn main() {
// let dhcp_score = OKDBootstrapDhcpScore::new(&topology, &inventory);
// let dns_score = OKDBootstrapDnsScore::new(&topology);
let load_balancer_score =
harmony::modules::okd::load_balancer::OKDLoadBalancerScore::new(&topology);
// let load_balancer_score =
// harmony::modules::okd::load_balancer::OKDLoadBalancerScore::new(&topology);
let tftp_score = TftpScore::new(Url::LocalFolder("../../../watchguard/tftpboot".to_string()));
let maestro = Maestro::new(inventory, topology);
// maestro.interpret(dns_score).await.unwrap();
// maestro.interpret(dhcp_score).await.unwrap();
maestro.interpret(load_balancer_score).await.unwrap();
// maestro.interpret(load_balancer_score).await.unwrap();
maestro.interpret(tftp_score).await.unwrap();
}

View File

@ -20,3 +20,4 @@ cidr = { workspace = true }
opnsense-config = { path = "../opnsense-config" }
opnsense-config-xml = { path = "../opnsense-config-xml" }
uuid = { workspace = true }
url = { workspace = true }

View File

@ -14,6 +14,7 @@ pub enum InterpretName {
OPNSenseDHCP,
OPNSenseDns,
LoadBalancer,
Tftp
}
impl std::fmt::Display for InterpretName {
@ -22,6 +23,7 @@ impl std::fmt::Display for InterpretName {
InterpretName::OPNSenseDHCP => f.write_str("OPNSenseDHCP"),
InterpretName::OPNSenseDns => f.write_str("OPNSenseDns"),
InterpretName::LoadBalancer => f.write_str("LoadBalancer"),
InterpretName::Tftp => f.write_str("Tftp"),
}
}
}
@ -52,6 +54,13 @@ impl Outcome {
message: String::new(),
}
}
pub fn success(message: String) -> Self {
Self {
status: InterpretStatus::SUCCESS,
message,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]

View File

@ -1,11 +1,13 @@
mod host_binding;
mod load_balancer;
mod router;
mod tftp;
pub use load_balancer::*;
pub use router::*;
mod network;
pub use host_binding::*;
pub use network::*;
pub use tftp::*;
use std::{net::IpAddr, sync::Arc};
@ -16,6 +18,7 @@ pub struct HAClusterTopology {
pub load_balancer: Arc<dyn LoadBalancer>,
pub firewall: Arc<dyn Firewall>,
pub dhcp_server: Arc<dyn DhcpServer>,
pub tftp_server: Arc<dyn TftpServer>,
pub dns_server: Arc<dyn DnsServer>,
pub control_plane: Vec<LogicalHost>,
pub workers: Vec<LogicalHost>,
@ -24,6 +27,21 @@ pub struct HAClusterTopology {
pub type IpAddress = IpAddr;
#[derive(Debug, Clone)]
pub enum Url {
LocalFolder(String),
Remote(url::Url),
}
impl std::fmt::Display for Url {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Url::LocalFolder(path) => write!(f, "{}", path),
Url::Remote(url) => write!(f, "{}", url),
}
}
}
/// Represents a logical member of a cluster that provides one or more services.
///
/// A LogicalHost can represent various roles within the infrastructure, such as:
@ -75,7 +93,11 @@ impl LogicalHost {
/// assert_eq!(hosts[2].ip, IpAddress::from_str("192.168.0.22").unwrap());
/// assert_eq!(hosts[2].name, "worker2");
/// ```
pub fn create_hosts(number_hosts: u32, start_ip: IpAddress, hostname_prefix: &str) -> Vec<LogicalHost> {
pub fn create_hosts(
number_hosts: u32,
start_ip: IpAddress,
hostname_prefix: &str,
) -> Vec<LogicalHost> {
let mut hosts = Vec::with_capacity(number_hosts.try_into().unwrap());
for i in 0..number_hosts {
let new_ip = increment_ip(start_ip, i).expect("IP address overflow");

View File

@ -0,0 +1,24 @@
use crate::executors::ExecutorError;
use async_trait::async_trait;
use super::{IpAddress, Url};
#[async_trait]
pub trait TftpServer: Send + Sync {
async fn serve_files(&self, url: &Url) -> Result<(), ExecutorError>;
fn get_ip(&self) -> IpAddress;
async fn set_ip(&self, ip: IpAddress) -> Result<(), ExecutorError>;
async fn ensure_initialized(&self) -> Result<(), ExecutorError>;
async fn commit_config(&self) -> Result<(), ExecutorError>;
async fn reload_restart(&self) -> Result<(), ExecutorError>;
}
impl std::fmt::Debug for dyn TftpServer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!(
"TftpServer serving files at {}",
self.get_ip()
))
}
}

View File

@ -0,0 +1,71 @@
use async_trait::async_trait;
use log::debug;
use crate::{executors::ExecutorError, topology::{DHCPStaticEntry, DhcpServer, IpAddress, LogicalHost}};
use super::OPNSenseFirewall;
#[async_trait]
impl DhcpServer for OPNSenseFirewall {
async fn commit_config(&self) -> Result<(), ExecutorError> {
OPNSenseFirewall::commit_config(self).await
}
async fn add_static_mapping(&self, entry: &DHCPStaticEntry) -> Result<(), ExecutorError> {
let mac: String = String::from(&entry.mac);
{
let mut writable_opnsense = self.opnsense_config.write().await;
writable_opnsense
.dhcp()
.add_static_mapping(&mac, entry.ip, &entry.name)
.unwrap();
}
debug!("Registered {:?}", entry);
Ok(())
}
async fn remove_static_mapping(
&self,
_mac: &crate::topology::MacAddress,
) -> Result<(), ExecutorError> {
todo!()
}
async fn list_static_mappings(&self) -> Vec<(crate::topology::MacAddress, IpAddress)> {
todo!()
}
fn get_ip(&self) -> IpAddress {
OPNSenseFirewall::get_ip(self)
}
fn get_host(&self) -> LogicalHost {
self.host.clone()
}
async fn set_next_server(&self, ip: IpAddress) -> Result<(), ExecutorError> {
let ipv4 = match ip {
std::net::IpAddr::V4(ipv4_addr) => ipv4_addr,
std::net::IpAddr::V6(_) => todo!("ipv6 not supported yet"),
};
{
let mut writable_opnsense = self.opnsense_config.write().await;
writable_opnsense.dhcp().set_next_server(ipv4);
debug!("OPNsense dhcp server set next server {ipv4}");
}
Ok(())
}
async fn set_boot_filename(&self, boot_filename: &str) -> Result<(), ExecutorError> {
{
let mut writable_opnsense = self.opnsense_config.write().await;
writable_opnsense.dhcp().set_boot_filename(boot_filename);
debug!("OPNsense dhcp server set boot filename {boot_filename}");
}
Ok(())
}
}

View File

@ -0,0 +1,88 @@
use crate::infra::opnsense::Host;
use crate::infra::opnsense::IpAddress;
use crate::infra::opnsense::LogicalHost;
use async_trait::async_trait;
use crate::{executors::ExecutorError, topology::{DnsRecord, DnsServer}};
use super::OPNSenseFirewall;
#[async_trait]
impl DnsServer for OPNSenseFirewall {
async fn register_hosts(&self, hosts: Vec<DnsRecord>) -> Result<(), ExecutorError> {
let mut writable_opnsense = self.opnsense_config.write().await;
let mut dns = writable_opnsense.dns();
let hosts = hosts
.iter()
.map(|h| {
Host::new(
h.host.clone(),
h.domain.clone(),
h.record_type.to_string(),
h.value.to_string(),
)
})
.collect();
dns.register_hosts(hosts);
Ok(())
}
fn remove_record(
&mut self,
_name: &str,
_record_type: crate::topology::DnsRecordType,
) -> Result<(), ExecutorError> {
todo!()
}
async fn list_records(&self) -> Vec<crate::topology::DnsRecord> {
self.opnsense_config
.write()
.await
.dns()
.get_hosts()
.iter()
.map(|h| DnsRecord {
host: h.hostname.clone(),
domain: h.domain.clone(),
record_type: h
.rr
.parse()
.expect("received invalid record type {h.rr} from opnsense"),
value: h
.server
.parse()
.expect("received invalid ipv4 record from opnsense {h.server}"),
})
.collect()
}
fn get_ip(&self) -> IpAddress {
OPNSenseFirewall::get_ip(&self)
}
fn get_host(&self) -> LogicalHost {
self.host.clone()
}
async fn register_dhcp_leases(&self, register: bool) -> Result<(), ExecutorError> {
let mut writable_opnsense = self.opnsense_config.write().await;
let mut dns = writable_opnsense.dns();
dns.register_dhcp_leases(register);
Ok(())
}
async fn commit_config(&self) -> Result<(), ExecutorError> {
let opnsense = self.opnsense_config.read().await;
opnsense
.save()
.await
.map_err(|e| ExecutorError::UnexpectedError(e.to_string()))?;
opnsense
.restart_dns()
.await
.map_err(|e| ExecutorError::UnexpectedError(e.to_string()))
}
}

View File

@ -0,0 +1,24 @@
use crate::{executors::ExecutorError, topology::{Firewall, FirewallRule, IpAddress, LogicalHost}};
use super::OPNSenseFirewall;
impl Firewall for OPNSenseFirewall {
fn add_rule(&mut self, _rule: FirewallRule) -> Result<(), ExecutorError> {
todo!()
}
fn remove_rule(&mut self, _rule_id: &str) -> Result<(), ExecutorError> {
todo!()
}
fn list_rules(&self) -> Vec<FirewallRule> {
todo!()
}
fn get_ip(&self) -> IpAddress {
OPNSenseFirewall::get_ip(self)
}
fn get_host(&self) -> LogicalHost {
self.host.clone()
}
}

View File

@ -1,11 +1,72 @@
use async_trait::async_trait;
use log::{debug, info, warn};
use opnsense_config::Config;
use opnsense_config_xml::{Frontend, HAProxy, HAProxyBackend, HAProxyHealthCheck, HAProxyServer};
use uuid::Uuid;
use crate::topology::{
BackendServer, HealthCheck, HttpMethod, HttpStatusCode, LoadBalancerService,
};
use crate::{executors::ExecutorError, topology::{
BackendServer, HealthCheck, HttpMethod, HttpStatusCode, IpAddress, LoadBalancer, LoadBalancerService, LogicalHost
}};
use super::OPNSenseFirewall;
#[async_trait]
impl LoadBalancer for OPNSenseFirewall {
fn get_ip(&self) -> IpAddress {
OPNSenseFirewall::get_ip(self)
}
fn get_host(&self) -> LogicalHost {
self.host.clone()
}
async fn add_service(&self, service: &LoadBalancerService) -> Result<(), ExecutorError> {
let mut config = self.opnsense_config.write().await;
let (frontend, backend, servers, healthcheck) =
harmony_load_balancer_service_to_haproxy_xml(service);
let mut load_balancer = config.load_balancer();
load_balancer.add_backend(backend);
load_balancer.add_frontend(frontend);
load_balancer.add_servers(servers);
if let Some(healthcheck) = healthcheck {
load_balancer.add_healthcheck(healthcheck);
}
Ok(())
}
async fn remove_service(&self, service: &LoadBalancerService) -> Result<(), ExecutorError> {
todo!()
}
async fn commit_config(&self) -> Result<(), ExecutorError> {
OPNSenseFirewall::commit_config(self).await?;
todo!("Make sure load balancer is reloaded properly")
}
async fn ensure_initialized(&self) -> Result<(), ExecutorError> {
let mut config = self.opnsense_config.write().await;
let load_balancer = config.load_balancer();
if let Some(_) = load_balancer.get_full_config() {
debug!("HAProxy config available in opnsense config, assuming it is already installed");
return Ok(());
}
config.install_package("os-haproxy").await.map_err(|e| {
ExecutorError::UnexpectedError(format!(
"Executor failed when trying to install os-haproxy package with error {e:?}"
))
})?;
config.load_balancer().enable(true);
Ok(())
}
async fn list_services(&self) -> Vec<LoadBalancerService> {
let mut config = self.opnsense_config.write().await;
let load_balancer = config.load_balancer();
let haproxy_xml_config = load_balancer.get_full_config();
haproxy_xml_config_to_harmony_loadbalancer(haproxy_xml_config)
}
}
pub(crate) fn haproxy_xml_config_to_harmony_loadbalancer(
haproxy: &Option<HAProxy>,

View File

@ -1,22 +1,18 @@
mod haproxy;
mod dhcp;
mod dns;
mod firewall;
mod load_balancer;
mod management;
mod tftp;
use std::sync::Arc;
use async_trait::async_trait;
use haproxy::{
haproxy_xml_config_to_harmony_loadbalancer, harmony_load_balancer_service_to_haproxy_xml,
};
use log::debug;
pub use management::*;
use opnsense_config_xml::Host;
use tokio::sync::RwLock;
use crate::{
executors::ExecutorError,
topology::{
DHCPStaticEntry, DhcpServer, DnsRecord, DnsServer, Firewall, FirewallRule, IpAddress,
LoadBalancer, LoadBalancerService, LogicalHost,
},
topology::{IpAddress, LogicalHost},
};
#[derive(Clone)]
@ -56,228 +52,3 @@ impl OPNSenseFirewall {
.map_err(|e| ExecutorError::UnexpectedError(e.to_string()))
}
}
impl Firewall for OPNSenseFirewall {
fn add_rule(&mut self, _rule: FirewallRule) -> Result<(), ExecutorError> {
todo!()
}
fn remove_rule(&mut self, _rule_id: &str) -> Result<(), ExecutorError> {
todo!()
}
fn list_rules(&self) -> Vec<FirewallRule> {
todo!()
}
fn get_ip(&self) -> IpAddress {
OPNSenseFirewall::get_ip(self)
}
fn get_host(&self) -> LogicalHost {
self.host.clone()
}
}
#[async_trait]
impl LoadBalancer for OPNSenseFirewall {
fn get_ip(&self) -> IpAddress {
OPNSenseFirewall::get_ip(self)
}
fn get_host(&self) -> LogicalHost {
self.host.clone()
}
async fn add_service(&self, service: &LoadBalancerService) -> Result<(), ExecutorError> {
let mut config = self.opnsense_config.write().await;
let (frontend, backend, servers, healthcheck) =
harmony_load_balancer_service_to_haproxy_xml(service);
let mut load_balancer = config.load_balancer();
load_balancer.add_backend(backend);
load_balancer.add_frontend(frontend);
load_balancer.add_servers(servers);
if let Some(healthcheck) = healthcheck {
load_balancer.add_healthcheck(healthcheck);
}
Ok(())
}
async fn remove_service(&self, service: &LoadBalancerService) -> Result<(), ExecutorError> {
todo!()
}
async fn commit_config(&self) -> Result<(), ExecutorError> {
OPNSenseFirewall::commit_config(self).await?;
todo!("Make sure load balancer is reloaded properly")
}
async fn ensure_initialized(&self) -> Result<(), ExecutorError> {
let mut config = self.opnsense_config.write().await;
let load_balancer = config.load_balancer();
if let Some(_) = load_balancer.get_full_config() {
debug!("HAProxy config available in opnsense config, assuming it is already installed");
return Ok(());
}
config.install_package("os-haproxy").await.map_err(|e| {
ExecutorError::UnexpectedError(format!(
"Executor failed when trying to install os-haproxy package with error {e:?}"
))
})?;
config.load_balancer().enable(true);
Ok(())
}
async fn list_services(&self) -> Vec<LoadBalancerService> {
let mut config = self.opnsense_config.write().await;
let load_balancer = config.load_balancer();
let haproxy_xml_config = load_balancer.get_full_config();
haproxy_xml_config_to_harmony_loadbalancer(haproxy_xml_config)
}
}
#[async_trait]
impl DhcpServer for OPNSenseFirewall {
async fn commit_config(&self) -> Result<(), ExecutorError> {
OPNSenseFirewall::commit_config(self).await
}
async fn add_static_mapping(&self, entry: &DHCPStaticEntry) -> Result<(), ExecutorError> {
let mac: String = String::from(&entry.mac);
{
let mut writable_opnsense = self.opnsense_config.write().await;
writable_opnsense
.dhcp()
.add_static_mapping(&mac, entry.ip, &entry.name)
.unwrap();
}
debug!("Registered {:?}", entry);
Ok(())
}
async fn remove_static_mapping(
&self,
_mac: &crate::topology::MacAddress,
) -> Result<(), ExecutorError> {
todo!()
}
async fn list_static_mappings(&self) -> Vec<(crate::topology::MacAddress, IpAddress)> {
todo!()
}
fn get_ip(&self) -> IpAddress {
OPNSenseFirewall::get_ip(self)
}
fn get_host(&self) -> LogicalHost {
self.host.clone()
}
async fn set_next_server(&self, ip: IpAddress) -> Result<(), ExecutorError> {
let ipv4 = match ip {
std::net::IpAddr::V4(ipv4_addr) => ipv4_addr,
std::net::IpAddr::V6(_) => todo!("ipv6 not supported yet"),
};
{
let mut writable_opnsense = self.opnsense_config.write().await;
writable_opnsense.dhcp().set_next_server(ipv4);
debug!("OPNsense dhcp server set next server {ipv4}");
}
Ok(())
}
async fn set_boot_filename(&self, boot_filename: &str) -> Result<(), ExecutorError> {
{
let mut writable_opnsense = self.opnsense_config.write().await;
writable_opnsense.dhcp().set_boot_filename(boot_filename);
debug!("OPNsense dhcp server set boot filename {boot_filename}");
}
Ok(())
}
}
#[async_trait]
impl DnsServer for OPNSenseFirewall {
async fn register_hosts(&self, hosts: Vec<DnsRecord>) -> Result<(), ExecutorError> {
let mut writable_opnsense = self.opnsense_config.write().await;
let mut dns = writable_opnsense.dns();
let hosts = hosts
.iter()
.map(|h| {
Host::new(
h.host.clone(),
h.domain.clone(),
h.record_type.to_string(),
h.value.to_string(),
)
})
.collect();
dns.register_hosts(hosts);
Ok(())
}
fn remove_record(
&mut self,
_name: &str,
_record_type: crate::topology::DnsRecordType,
) -> Result<(), ExecutorError> {
todo!()
}
async fn list_records(&self) -> Vec<crate::topology::DnsRecord> {
self.opnsense_config
.write()
.await
.dns()
.get_hosts()
.iter()
.map(|h| DnsRecord {
host: h.hostname.clone(),
domain: h.domain.clone(),
record_type: h
.rr
.parse()
.expect("received invalid record type {h.rr} from opnsense"),
value: h
.server
.parse()
.expect("received invalid ipv4 record from opnsense {h.server}"),
})
.collect()
}
fn get_ip(&self) -> IpAddress {
OPNSenseFirewall::get_ip(&self)
}
fn get_host(&self) -> LogicalHost {
self.host.clone()
}
async fn register_dhcp_leases(&self, register: bool) -> Result<(), ExecutorError> {
let mut writable_opnsense = self.opnsense_config.write().await;
let mut dns = writable_opnsense.dns();
dns.register_dhcp_leases(register);
Ok(())
}
async fn commit_config(&self) -> Result<(), ExecutorError> {
let opnsense = self.opnsense_config.read().await;
opnsense
.save()
.await
.map_err(|e| ExecutorError::UnexpectedError(e.to_string()))?;
opnsense
.restart_dns()
.await
.map_err(|e| ExecutorError::UnexpectedError(e.to_string()))
}
}

View File

@ -0,0 +1,77 @@
use async_trait::async_trait;
use log::{debug, info};
use crate::{
executors::ExecutorError,
topology::{IpAddress, TftpServer, Url},
};
use super::OPNSenseFirewall;
#[async_trait]
impl TftpServer for OPNSenseFirewall {
async fn serve_files(&self, url: &Url) -> Result<(), ExecutorError> {
let tftp_root_path = "/usr/local/tftp";
let config = self.opnsense_config.write().await;
info!("Uploading files from url {url} to {tftp_root_path}");
match url {
Url::LocalFolder(path) => {
config
.upload_files(path, tftp_root_path)
.await
.map_err(|e| ExecutorError::UnexpectedError(e.to_string()))?;
}
Url::Remote(url) => todo!(),
}
Ok(())
}
fn get_ip(&self) -> IpAddress {
OPNSenseFirewall::get_ip(self)
}
async fn set_ip(&self, ip: IpAddress) -> Result<(), ExecutorError> {
info!("Setting listen_ip to {}", &ip);
self.opnsense_config
.write()
.await
.tftp()
.listen_ip(&ip.to_string());
Ok(())
}
async fn commit_config(&self) -> Result<(), ExecutorError> {
OPNSenseFirewall::commit_config(self).await
}
async fn reload_restart(&self) -> Result<(), ExecutorError> {
self.opnsense_config
.write()
.await
.tftp()
.reload_restart()
.await
.map_err(|e| ExecutorError::UnexpectedError(e.to_string()))
}
async fn ensure_initialized(&self) -> Result<(), ExecutorError> {
let mut config = self.opnsense_config.write().await;
let tftp = config.tftp();
if let None = tftp.get_full_config() {
info!("Tftp config not available in opnsense config, installing package");
config.install_package("os-tftp").await.map_err(|e| {
ExecutorError::UnexpectedError(format!(
"Executor failed when trying to install os-tftp package with error {e:?}"
))
})?;
} else {
info!("Tftp config available in opnsense config, assuming it is already installed");
}
info!("Enabling tftp server");
config.tftp().enable(true);
Ok(())
}
}

View File

@ -2,3 +2,4 @@ pub mod dhcp;
pub mod dns;
pub mod okd;
pub mod load_balancer;
pub mod tftp;

View File

@ -0,0 +1,64 @@
use async_trait::async_trait;
use derive_new::new;
use crate::{
data::{Id, Version},
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
inventory::Inventory,
score::Score,
topology::{HAClusterTopology, Url},
};
#[derive(Debug, new, Clone)]
pub struct TftpScore {
files_to_serve: Url,
}
impl Score for TftpScore {
type InterpretType = TftpInterpret;
fn create_interpret(self) -> Self::InterpretType {
TftpInterpret::new(self)
}
}
#[derive(Debug, new, Clone)]
pub struct TftpInterpret {
score: TftpScore,
}
#[async_trait]
impl Interpret for TftpInterpret {
async fn execute(
&self,
inventory: &Inventory,
topology: &HAClusterTopology,
) -> Result<Outcome, InterpretError> {
let tftp_server = &topology.tftp_server;
tftp_server.ensure_initialized().await?;
tftp_server.set_ip(topology.router.get_gateway()).await?;
tftp_server.serve_files(&self.score.files_to_serve).await?;
tftp_server.commit_config().await?;
tftp_server.reload_restart().await?;
Ok(Outcome::success(format!(
"TFTP Server running and serving files from {}",
self.score.files_to_serve
)))
}
fn get_name(&self) -> InterpretName {
InterpretName::Tftp
}
fn get_version(&self) -> Version {
todo!()
}
fn get_status(&self) -> InterpretStatus {
todo!()
}
fn get_children(&self) -> Vec<Id> {
todo!()
}
}

View File

@ -453,57 +453,57 @@ pub struct OPNsenseXmlSection {
#[derive(Debug, YaSerialize, YaDeserialize, PartialEq)]
pub struct Tftp {
general: TftpGeneral,
pub general: TftpGeneral,
}
#[derive(Debug, YaSerialize, YaDeserialize, PartialEq)]
pub struct TftpGeneral {
#[yaserde(attribute)]
version: String,
enabled: u8,
listen: String,
pub version: String,
pub enabled: u8,
pub listen: String,
}
#[derive(Debug, YaSerialize, YaDeserialize, PartialEq)]
#[yaserde(rename = "IDS")]
pub struct IDS {
#[yaserde(attribute)]
version: String,
rules: MaybeString,
policies: MaybeString,
pub version: String,
pub rules: MaybeString,
pub policies: MaybeString,
#[yaserde(rename = "userDefinedRules")]
user_defined_rules: MaybeString,
files: MaybeString,
pub user_defined_rules: MaybeString,
pub files: MaybeString,
#[yaserde(rename = "fileTags")]
file_tags: MaybeString,
general: IDSGeneral,
pub file_tags: MaybeString,
pub general: IDSGeneral,
}
#[derive(Debug, YaSerialize, YaDeserialize, PartialEq)]
pub struct IDSGeneral {
enabled: Option<u8>,
ips: Option<u8>,
promisc: Option<u8>,
interfaces: String,
homenet: String,
pub enabled: Option<u8>,
pub ips: Option<u8>,
pub promisc: Option<u8>,
pub interfaces: String,
pub homenet: String,
#[yaserde(rename = "defaultPacketSize")]
default_packet_size: MaybeString,
pub default_packet_size: MaybeString,
#[yaserde(rename = "UpdateCron")]
update_cron: MaybeString,
pub update_cron: MaybeString,
#[yaserde(rename = "AlertLogrotate")]
alert_logrotate: String,
pub alert_logrotate: String,
#[yaserde(rename = "AlertSaveLogs")]
alert_save_logs: u8,
pub alert_save_logs: u8,
#[yaserde(rename = "MPMAlgo")]
mpm_algo: MaybeString,
detect: Detect,
syslog: Option<u8>,
syslog_eve: Option<u8>,
pub mpm_algo: MaybeString,
pub detect: Detect,
pub syslog: Option<u8>,
pub syslog_eve: Option<u8>,
#[yaserde(rename = "LogPayload")]
log_payload: Option<u8>,
verbosity: MaybeString,
pub log_payload: Option<u8>,
pub verbosity: MaybeString,
#[yaserde(rename = "eveLog")]
eve_log: Option<RawXml>,
pub eve_log: Option<RawXml>,
}
#[derive(Debug, YaSerialize, YaDeserialize, PartialEq)]

View File

@ -18,6 +18,8 @@ opnsense-config-xml = { path = "../opnsense-config-xml" }
chrono = "0.4.38"
russh-sftp = "2.0.6"
serde_json = "1.0.133"
tokio-util = "0.7.13"
tokio-stream = "0.1.17"
[dev-dependencies]
pretty_assertions = "1.4.1"

View File

@ -3,7 +3,7 @@ use std::{sync::Arc, time::Duration};
use crate::{
config::{SshConfigManager, SshCredentials, SshOPNSenseShell},
error::Error,
modules::{dhcp::DhcpConfig, dns::DnsConfig, load_balancer::LoadBalancerConfig},
modules::{dhcp::DhcpConfig, dns::DnsConfig, load_balancer::LoadBalancerConfig, tftp::TftpConfig},
};
use log::{info, trace};
use opnsense_config_xml::OPNsense;
@ -38,10 +38,18 @@ impl Config {
DnsConfig::new(&mut self.opnsense, self.shell.clone())
}
pub fn tftp(&mut self) -> TftpConfig {
TftpConfig::new(&mut self.opnsense, self.shell.clone())
}
pub fn load_balancer(&mut self) -> LoadBalancerConfig {
LoadBalancerConfig::new(&mut self.opnsense, self.shell.clone())
}
pub async fn upload_files(&self, source: &str, destination: &str) -> Result<String, Error> {
self.shell.upload_folder(source, destination).await
}
pub async fn install_package(&mut self, package_name: &str) -> Result<(), Error> {
info!("Installing opnsense package {package_name}");
let output = self.shell

View File

@ -27,19 +27,25 @@ impl SshConfigManager {
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))
self.opnsense_shell
.exec(&format!(
"cp /conf/config.xml /conf/backup/{}",
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.opnsense_shell.exec(&format!("mv {new_config_path} /conf/config.xml"))
self.opnsense_shell
.exec(&format!("mv {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(&format!("configctl service reload all"))
self.opnsense_shell
.exec(&format!("configctl service reload all"))
.await
}
}

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,

View File

@ -6,6 +6,8 @@ pub enum Error {
Xml(String),
#[error("SSH error: {0}")]
Ssh(#[from] russh::Error),
#[error("SSH Client error: {0}")]
SftpClient(#[from] russh_sftp::client::error::Error),
#[error("Command failed : {0}")]
Command(String),
#[error("I/O error: {0}")]

View File

@ -1,3 +1,4 @@
pub mod dhcp;
pub mod dns;
pub mod load_balancer;
pub mod tftp;

View File

@ -0,0 +1,48 @@
use std::sync::Arc;
use opnsense_config_xml::{OPNsense, Tftp};
use crate::{config::OPNsenseShell, Error};
pub struct TftpConfig<'a> {
opnsense: &'a mut OPNsense,
opnsense_shell: Arc<dyn OPNsenseShell>,
}
impl<'a> TftpConfig<'a> {
pub fn new(opnsense: &'a mut OPNsense, opnsense_shell: Arc<dyn OPNsenseShell>) -> Self {
Self {
opnsense,
opnsense_shell,
}
}
pub fn get_full_config(&self) -> &Option<Tftp> {
&self.opnsense.opnsense.tftp
}
fn with_tftp<F, R>(&mut self, f: F) -> R
where
F: FnOnce(&mut Tftp) -> R,
{
match &mut self.opnsense.opnsense.tftp.as_mut() {
Some(tftp) => f(tftp),
None => unimplemented!("Accessing tftp config is not supported when not available yet"),
}
}
pub fn enable(&mut self, enabled: bool) {
self.with_tftp(|tftp| tftp.general.enabled = enabled as u8);
}
pub fn listen_ip(&mut self, ip: &str) {
self.with_tftp(|tftp| tftp.general.listen = ip.to_string());
}
pub async fn reload_restart(&self) -> Result<(), Error> {
self.opnsense_shell.exec("configctl tftp stop").await?;
self.opnsense_shell.exec("configctl template reload OPNsense/Tftp").await?;
self.opnsense_shell.exec("configctl tftp start").await?;
Ok(())
}
}