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:
parent
098cb30523
commit
925e84e4d2
310
harmony-rs/Cargo.lock
generated
310
harmony-rs/Cargo.lock
generated
@ -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",
|
||||
]
|
||||
|
@ -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"
|
||||
|
@ -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 }
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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 }
|
||||
|
@ -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)]
|
||||
|
@ -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");
|
||||
|
24
harmony-rs/harmony/src/domain/topology/tftp.rs
Normal file
24
harmony-rs/harmony/src/domain/topology/tftp.rs
Normal 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()
|
||||
))
|
||||
}
|
||||
}
|
71
harmony-rs/harmony/src/infra/opnsense/dhcp.rs
Normal file
71
harmony-rs/harmony/src/infra/opnsense/dhcp.rs
Normal 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(())
|
||||
}
|
||||
}
|
88
harmony-rs/harmony/src/infra/opnsense/dns.rs
Normal file
88
harmony-rs/harmony/src/infra/opnsense/dns.rs
Normal 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()))
|
||||
}
|
||||
}
|
24
harmony-rs/harmony/src/infra/opnsense/firewall.rs
Normal file
24
harmony-rs/harmony/src/infra/opnsense/firewall.rs
Normal 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()
|
||||
}
|
||||
}
|
@ -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>,
|
@ -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()))
|
||||
}
|
||||
}
|
||||
|
77
harmony-rs/harmony/src/infra/opnsense/tftp.rs
Normal file
77
harmony-rs/harmony/src/infra/opnsense/tftp.rs
Normal 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(())
|
||||
}
|
||||
}
|
@ -2,3 +2,4 @@ pub mod dhcp;
|
||||
pub mod dns;
|
||||
pub mod okd;
|
||||
pub mod load_balancer;
|
||||
pub mod tftp;
|
||||
|
64
harmony-rs/harmony/src/modules/tftp.rs
Normal file
64
harmony-rs/harmony/src/modules/tftp.rs
Normal 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!()
|
||||
}
|
||||
}
|
@ -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)]
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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}")]
|
||||
|
@ -1,3 +1,4 @@
|
||||
pub mod dhcp;
|
||||
pub mod dns;
|
||||
pub mod load_balancer;
|
||||
pub mod tftp;
|
||||
|
48
harmony-rs/opnsense-config/src/modules/tftp.rs
Normal file
48
harmony-rs/opnsense-config/src/modules/tftp.rs
Normal 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(())
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user