diff --git a/Cargo.lock b/Cargo.lock index b1295be..31ca7ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,7 +65,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -182,7 +182,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -249,6 +249,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81ce3d38065e618af2d7b77e10c5ad9a069859b4be3c2250f674af3840d9c8a5" +dependencies = [ + "memchr", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -362,6 +371,48 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +[[package]] +name = "askama" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4" +dependencies = [ + "askama_derive", + "itoa", + "percent-encoding", + "serde", + "serde_json", +] + +[[package]] +name = "askama_derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f" +dependencies = [ + "askama_parser", + "basic-toml", + "memchr", + "proc-macro2", + "quote", + "rustc-hash", + "serde", + "serde_derive", + "syn 2.0.105", +] + +[[package]] +name = "askama_parser" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358" +dependencies = [ + "memchr", + "serde", + "serde_derive", + "winnow", +] + [[package]] name = "assert_cmd" version = "2.0.17" @@ -409,7 +460,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -420,7 +471,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -485,6 +536,15 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + [[package]] name = "bcrypt-pbkdf" version = "0.10.0" @@ -581,7 +641,7 @@ dependencies = [ "tokio-util", "tower-service", "url", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -655,6 +715,12 @@ dependencies = [ "bytes", ] +[[package]] +name = "c_linked_list" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4964518bd3b4a8190e832886cdc0da9794f12e8e6c1613a9e90ff331c4c8724b" + [[package]] name = "camino" version = "1.1.10" @@ -798,9 +864,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.40" +version = "4.5.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" +checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57" dependencies = [ "clap_builder", "clap_derive", @@ -808,9 +874,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.40" +version = "4.5.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" +checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41" dependencies = [ "anstream", "anstyle", @@ -820,14 +886,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.40" +version = "4.5.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" +checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -1042,7 +1108,7 @@ dependencies = [ "parking_lot", "signal-hook", "signal-hook-mio", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1059,7 +1125,7 @@ dependencies = [ "rustix 0.38.44", "signal-hook", "signal-hook-mio", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1068,7 +1134,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1139,7 +1205,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -1163,7 +1229,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.105", ] [[package]] @@ -1174,7 +1240,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -1212,7 +1278,7 @@ checksum = "2cdc8d50f426189eef89dac62fabfa0abb27d5cc008f25bf4156a0203325becc" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -1232,7 +1298,7 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", "unicode-xid", ] @@ -1298,7 +1364,20 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", +] + +[[package]] +name = "dmidecode" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e529c1bd93d69804dc1e0a0c73aacd12bb13c7a18c659497411abdc6acf5e5f" +dependencies = [ + "aho-corasick 0.6.10", + "bitflags 1.3.2", + "failure", + "failure_derive", + "lazy_static", ] [[package]] @@ -1327,7 +1406,7 @@ dependencies = [ "anyhow", "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -1384,7 +1463,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -1455,7 +1534,7 @@ checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -1655,6 +1734,24 @@ dependencies = [ "url", ] +[[package]] +name = "example-pxe" +version = "0.1.0" +dependencies = [ + "cidr", + "env_logger", + "harmony", + "harmony_cli", + "harmony_macros", + "harmony_secret", + "harmony_secret_derive", + "harmony_types", + "log", + "serde", + "tokio", + "url", +] + [[package]] name = "example-rust" version = "0.1.0" @@ -1700,6 +1797,15 @@ dependencies = [ "url", ] +[[package]] +name = "example_validate_ceph_cluster_health" +version = "0.1.0" +dependencies = [ + "harmony", + "harmony_cli", + "tokio", +] + [[package]] name = "eyre" version = "0.6.12" @@ -1710,6 +1816,28 @@ dependencies = [ "once_cell", ] +[[package]] +name = "failure" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +dependencies = [ + "backtrace", + "failure_derive", +] + +[[package]] +name = "failure_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure 0.12.6", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -1754,6 +1882,17 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + [[package]] name = "flurry" version = "0.5.2" @@ -1778,21 +1917,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.1" @@ -1874,7 +1998,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -1925,6 +2049,12 @@ dependencies = [ "byteorder", ] +[[package]] +name = "gcc" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" + [[package]] name = "generic-array" version = "0.14.7" @@ -1936,6 +2066,28 @@ dependencies = [ "zeroize", ] +[[package]] +name = "get_if_addrs" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abddb55a898d32925f3148bd281174a68eeb68bbfd9a5938a57b18f506ee4ef7" +dependencies = [ + "c_linked_list", + "get_if_addrs-sys", + "libc", + "winapi 0.2.8", +] + +[[package]] +name = "get_if_addrs-sys" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d04f9fb746cf36b191c00f3ede8bde9c8e64f9f4b05ae2694a9ccf5e3f5ab48" +dependencies = [ + "gcc", + "libc", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -2044,6 +2196,7 @@ dependencies = [ name = "harmony" version = "0.1.0" dependencies = [ + "askama", "async-trait", "base64 0.22.1", "bollard", @@ -2058,8 +2211,9 @@ dependencies = [ "env_logger", "fqdn", "futures-util", - "harmony-secret-derive", + "harmony_inventory_agent", "harmony_macros", + "harmony_secret_derive", "harmony_types", "helm-wrapper-rs", "hex", @@ -2069,7 +2223,6 @@ dependencies = [ "kube", "kube-derive", "lazy_static", - "libredfish", "log", "non-blank-string-rs", "once_cell", @@ -2099,35 +2252,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "harmony-secret" -version = "0.1.0" -dependencies = [ - "async-trait", - "directories", - "harmony-secret-derive", - "http 1.3.1", - "infisical", - "lazy_static", - "log", - "pretty_assertions", - "serde", - "serde_json", - "tempfile", - "thiserror 2.0.14", - "tokio", -] - -[[package]] -name = "harmony-secret-derive" -version = "0.1.0" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "harmony_cli" version = "0.1.0" @@ -2174,10 +2298,14 @@ version = "0.1.0" dependencies = [ "actix-web", "env_logger", + "local-ip-address", "log", + "mdns-sd 0.14.1 (git+https://github.com/jggc/mdns-sd.git?branch=patch-1)", "serde", "serde_json", "sysinfo", + "thiserror 2.0.14", + "tokio", ] [[package]] @@ -2189,7 +2317,36 @@ dependencies = [ "quote", "serde", "serde_yaml", - "syn", + "syn 2.0.105", +] + +[[package]] +name = "harmony_secret" +version = "0.1.0" +dependencies = [ + "async-trait", + "directories", + "harmony_secret_derive", + "http 1.3.1", + "infisical", + "lazy_static", + "log", + "pretty_assertions", + "serde", + "serde_json", + "tempfile", + "thiserror 2.0.14", + "tokio", +] + +[[package]] +name = "harmony_secret_derive" +version = "0.1.0" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.105", ] [[package]] @@ -2481,12 +2638,12 @@ dependencies = [ "headers", "http 1.3.1", "hyper 1.6.0", - "hyper-rustls", + "hyper-rustls 0.27.7", "hyper-util", "pin-project-lite", "rustls-native-certs 0.7.3", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.2", "tower-service", ] @@ -2502,7 +2659,21 @@ dependencies = [ "pin-project-lite", "tokio", "tower-service", - "winapi", + "winapi 0.3.9", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.32", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", ] [[package]] @@ -2515,13 +2686,13 @@ dependencies = [ "hyper 1.6.0", "hyper-util", "log", - "rustls", + "rustls 0.23.28", "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.2", "tower-service", - "webpki-roots", + "webpki-roots 1.0.2", ] [[package]] @@ -2537,35 +2708,6 @@ dependencies = [ "tower-service", ] -[[package]] -name = "hyper-tls" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" -dependencies = [ - "bytes", - "hyper 0.14.32", - "native-tls", - "tokio", - "tokio-native-tls", -] - -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper 1.6.0", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - [[package]] name = "hyper-util" version = "0.1.14" @@ -2585,11 +2727,9 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2 0.5.10", - "system-configuration 0.6.1", "tokio", "tower-service", "tracing", - "windows-registry", ] [[package]] @@ -2744,6 +2884,16 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "if-addrs" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf39cc0423ee66021dc5eccface85580e4a001e0c5288bae8bea7ecb69225e90" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "impl-more" version = "0.1.9" @@ -2810,8 +2960,7 @@ checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" [[package]] name = "infisical" version = "0.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d97c33b08e22b2f7b9f87a8fc06a7d247442db7bf216ffc6661a74ed8aea658" +source = "git+https://github.com/jggc/rust-sdk.git?branch=patch-1#5a8509ef5483a5798c5d1a7f7ebeb5ba5b783253" dependencies = [ "base64 0.22.1", "reqwest 0.12.20", @@ -2859,7 +3008,7 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -2931,7 +3080,7 @@ checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -3065,14 +3214,14 @@ dependencies = [ "http-body-util", "hyper 1.6.0", "hyper-http-proxy", - "hyper-rustls", + "hyper-rustls 0.27.7", "hyper-timeout", "hyper-util", "jsonpath-rust", "k8s-openapi", "kube-core", "pem", - "rustls", + "rustls 0.23.28", "secrecy", "serde", "serde_json", @@ -3116,7 +3265,7 @@ dependencies = [ "quote", "serde", "serde_json", - "syn", + "syn 2.0.105", ] [[package]] @@ -3173,19 +3322,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" -[[package]] -name = "libredfish" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7f0a8985e53d18c60dc82e7b5fa512fd194ea4c0d8bf1409b65cf44f8b0a8d9" -dependencies = [ - "log", - "reqwest 0.11.27", - "serde", - "serde_derive", - "serde_json", -] - [[package]] name = "libredox" version = "0.1.4" @@ -3226,6 +3362,18 @@ dependencies = [ "local-waker", ] +[[package]] +name = "local-ip-address" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "656b3b27f8893f7bbf9485148ff9a65f019e3f33bd5cdc87c83cab16b3fd9ec8" +dependencies = [ + "libc", + "neli", + "thiserror 2.0.14", + "windows-sys 0.59.0", +] + [[package]] name = "local-waker" version = "0.1.4" @@ -3287,6 +3435,50 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" +[[package]] +name = "mdns" +version = "0.1.0" +dependencies = [ + "clap", + "dmidecode", + "env_logger", + "futures", + "get_if_addrs", + "local-ip-address", + "log", + "mdns-sd 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio", +] + +[[package]] +name = "mdns-sd" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e0a59b04e17a195b0674198b3182931801c4759d00f36acad51b5a97210a692" +dependencies = [ + "fastrand", + "flume", + "if-addrs", + "log", + "mio 1.0.4", + "socket-pktinfo", + "socket2 0.6.0", +] + +[[package]] +name = "mdns-sd" +version = "0.14.1" +source = "git+https://github.com/jggc/mdns-sd.git?branch=patch-1#9e4619599d1493ec15395d62d82d40a43fbef9e7" +dependencies = [ + "fastrand", + "flume", + "if-addrs", + "log", + "mio 1.0.4", + "socket-pktinfo", + "socket2 0.6.0", +] + [[package]] name = "memchr" version = "2.7.5" @@ -3333,20 +3525,28 @@ dependencies = [ ] [[package]] -name = "native-tls" -version = "0.2.14" +name = "neli" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +checksum = "93062a0dce6da2517ea35f301dfc88184ce18d3601ec786a727a87bf535deca9" dependencies = [ + "byteorder", "libc", "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework 2.11.1", - "security-framework-sys", - "tempfile", + "neli-proc-macros", +] + +[[package]] +name = "neli-proc-macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8034b7fbb6f9455b2a96c19e6edf8dc9fc34c70449938d8ee3b4df363f61fe" +dependencies = [ + "either", + "proc-macro2", + "quote", + "serde", + "syn 1.0.109", ] [[package]] @@ -3373,7 +3573,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -3478,7 +3678,7 @@ dependencies = [ "http-body 1.0.1", "http-body-util", "hyper 1.6.0", - "hyper-rustls", + "hyper-rustls 0.27.7", "hyper-timeout", "hyper-util", "jsonwebtoken", @@ -3517,50 +3717,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" -[[package]] -name = "openssl" -version = "0.10.73" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" -dependencies = [ - "bitflags 2.9.1", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "openssl-probe" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" -[[package]] -name = "openssl-sys" -version = "0.9.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "opnsense-config" version = "0.1.0" @@ -3580,6 +3742,7 @@ dependencies = [ "tokio", "tokio-stream", "tokio-util", + "uuid", ] [[package]] @@ -3783,7 +3946,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -3813,7 +3976,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -4016,7 +4179,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls", + "rustls 0.23.28", "socket2 0.5.10", "thiserror 2.0.14", "tokio", @@ -4036,7 +4199,7 @@ dependencies = [ "rand 0.9.1", "ring", "rustc-hash", - "rustls", + "rustls 0.23.28", "rustls-pki-types", "slab", "thiserror 2.0.14", @@ -4217,7 +4380,7 @@ checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -4226,7 +4389,7 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ - "aho-corasick", + "aho-corasick 1.1.3", "memchr", "regex-automata", "regex-syntax", @@ -4238,7 +4401,7 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ - "aho-corasick", + "aho-corasick 1.1.3", "memchr", "regex-syntax", ] @@ -4270,28 +4433,29 @@ dependencies = [ "http 0.2.12", "http-body 0.4.6", "hyper 0.14.32", - "hyper-tls 0.5.0", + "hyper-rustls 0.24.2", "ipnet", "js-sys", "log", "mime", - "native-tls", "once_cell", "percent-encoding", "pin-project-lite", + "rustls 0.21.12", "rustls-pemfile 1.0.4", "serde", "serde_json", "serde_urlencoded", "sync_wrapper 0.1.2", - "system-configuration 0.5.1", + "system-configuration", "tokio", - "tokio-native-tls", + "tokio-rustls 0.24.1", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots 0.25.4", "winreg", ] @@ -4303,7 +4467,6 @@ checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" dependencies = [ "base64 0.22.1", "bytes", - "encoding_rs", "futures-channel", "futures-core", "futures-util", @@ -4312,25 +4475,21 @@ dependencies = [ "http-body 1.0.1", "http-body-util", "hyper 1.6.0", - "hyper-rustls", - "hyper-tls 0.6.0", + "hyper-rustls 0.27.7", "hyper-util", "js-sys", "log", - "mime", - "native-tls", "percent-encoding", "pin-project-lite", "quinn", - "rustls", + "rustls 0.23.28", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper 1.0.2", "tokio", - "tokio-native-tls", - "tokio-rustls", + "tokio-rustls 0.26.2", "tokio-util", "tower", "tower-http", @@ -4340,7 +4499,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots", + "webpki-roots 1.0.2", ] [[package]] @@ -4438,7 +4597,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fadd2c0ab350e21c66556f94ee06f766d8bdae3213857ba7610bfd8e10e51880" dependencies = [ "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -4571,6 +4730,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + [[package]] name = "rustls" version = "0.23.28" @@ -4581,7 +4752,7 @@ dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.103.3", "subtle", "zeroize", ] @@ -4639,6 +4810,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "rustls-webpki" version = "0.103.3" @@ -4725,7 +4906,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn", + "syn 2.0.105", ] [[package]] @@ -4745,6 +4926,16 @@ dependencies = [ "sha2", ] +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "sec1" version = "0.7.3" @@ -4857,7 +5048,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -4868,7 +5059,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -4901,7 +5092,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -4922,7 +5113,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn", + "syn 2.0.105", ] [[package]] @@ -4966,7 +5157,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -5108,7 +5299,18 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.105", +] + +[[package]] +name = "socket-pktinfo" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927136cc2ae6a1b0e66ac6b1210902b75c3f726db004a73bc18686dcd0dcd22f" +dependencies = [ + "libc", + "socket2 0.6.0", + "windows-sys 0.60.2", ] [[package]] @@ -5136,6 +5338,9 @@ name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] [[package]] name = "spki" @@ -5244,7 +5449,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn", + "syn 2.0.105", ] [[package]] @@ -5257,7 +5462,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn", + "syn 2.0.105", ] [[package]] @@ -5266,6 +5471,17 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.105" @@ -5292,6 +5508,18 @@ dependencies = [ "futures-core", ] +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -5300,7 +5528,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -5326,18 +5554,7 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", - "system-configuration-sys 0.5.0", -] - -[[package]] -name = "system-configuration" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" -dependencies = [ - "bitflags 2.9.1", - "core-foundation 0.9.4", - "system-configuration-sys 0.6.0", + "system-configuration-sys", ] [[package]] @@ -5350,16 +5567,6 @@ dependencies = [ "libc", ] -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "tap" version = "1.0.1" @@ -5434,7 +5641,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -5445,7 +5652,7 @@ checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -5550,16 +5757,16 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] -name = "tokio-native-tls" -version = "0.3.1" +name = "tokio-rustls" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "native-tls", + "rustls 0.21.12", "tokio", ] @@ -5569,7 +5776,7 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls", + "rustls 0.23.28", "tokio", ] @@ -5721,7 +5928,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -5931,7 +6138,7 @@ checksum = "26b682e8c381995ea03130e381928e0e005b7c9eb483c6c8682f50e07b33c2b7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -5940,12 +6147,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - [[package]] name = "version_check" version = "0.9.5" @@ -6007,7 +6208,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn", + "syn 2.0.105", "wasm-bindgen-shared", ] @@ -6042,7 +6243,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -6090,6 +6291,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + [[package]] name = "webpki-roots" version = "1.0.2" @@ -6099,6 +6306,12 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + [[package]] name = "winapi" version = "0.3.9" @@ -6161,7 +6374,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -6172,7 +6385,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -6181,17 +6394,6 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" -[[package]] -name = "windows-registry" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" -dependencies = [ - "windows-link", - "windows-result", - "windows-strings", -] - [[package]] name = "windows-result" version = "0.3.4" @@ -6516,7 +6718,7 @@ dependencies = [ "quote", "serde", "serde_tokenstream", - "syn", + "syn 2.0.105", "xml-rs", ] @@ -6540,8 +6742,8 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn", - "synstructure", + "syn 2.0.105", + "synstructure 0.13.2", ] [[package]] @@ -6561,7 +6763,7 @@ checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -6581,8 +6783,8 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", - "synstructure", + "syn 2.0.105", + "synstructure 0.13.2", ] [[package]] @@ -6621,7 +6823,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index fcc315f..81de5e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ members = [ "harmony_composer", "harmony_inventory_agent", "harmony_secret_derive", - "harmony_secret", + "harmony_secret", "adr/agent_discovery/mdns", ] [workspace.package] diff --git a/adr/agent_discovery/mdns/Cargo.toml b/adr/agent_discovery/mdns/Cargo.toml new file mode 100644 index 0000000..b5fc525 --- /dev/null +++ b/adr/agent_discovery/mdns/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "mdns" +edition = "2024" +version.workspace = true +readme.workspace = true +license.workspace = true + +[dependencies] +mdns-sd = "0.14" +tokio = { version = "1", features = ["full"] } +futures = "0.3" +dmidecode = "0.2" # For getting the motherboard ID on the agent +log.workspace=true +env_logger.workspace=true +clap = { version = "4.5.46", features = ["derive"] } +get_if_addrs = "0.5.3" +local-ip-address = "0.6.5" diff --git a/adr/agent_discovery/mdns/src/advertise.rs b/adr/agent_discovery/mdns/src/advertise.rs new file mode 100644 index 0000000..a98f237 --- /dev/null +++ b/adr/agent_discovery/mdns/src/advertise.rs @@ -0,0 +1,60 @@ +// harmony-agent/src/main.rs + +use log::info; +use mdns_sd::{ServiceDaemon, ServiceInfo}; +use std::collections::HashMap; + +use crate::SERVICE_TYPE; + +// The service we are advertising. +const SERVICE_PORT: u16 = 43210; // A port for the service. It needs one, even if unused. + +pub async fn advertise() { + info!("Starting Harmony Agent..."); + + // Get a unique ID for this machine. + let motherboard_id = "some motherboard id"; + let instance_name = format!("harmony-agent-{}", motherboard_id); + info!("This agent's instance name: {}", instance_name); + info!("Advertising with ID: {}", motherboard_id); + + // Create a new mDNS daemon. + let mdns = ServiceDaemon::new().expect("Failed to create mDNS daemon"); + + // Create a TXT record HashMap to hold our metadata. + let mut properties = HashMap::new(); + properties.insert("id".to_string(), motherboard_id.to_string()); + properties.insert("version".to_string(), "1.0".to_string()); + + // Create the service information. + // The instance name should be unique on the network. + let local_ip = local_ip_address::local_ip().unwrap(); + let service_info = ServiceInfo::new( + SERVICE_TYPE, + &instance_name, + "harmony-host.local.", // A hostname for the service + local_ip, + // "0.0.0.0", + SERVICE_PORT, + Some(properties), + ) + .expect("Failed to create service info"); + + // Register our service with the daemon. + mdns.register(service_info) + .expect("Failed to register service"); + + info!( + "Service '{}' registered and now being advertised.", + instance_name + ); + info!("Agent is running. Press Ctrl+C to exit."); + + for iface in get_if_addrs::get_if_addrs().unwrap() { + println!("{:#?}", iface); + } + + // Keep the agent running indefinitely. + tokio::signal::ctrl_c().await.unwrap(); + info!("Shutting down agent."); +} diff --git a/adr/agent_discovery/mdns/src/discover.rs b/adr/agent_discovery/mdns/src/discover.rs new file mode 100644 index 0000000..bf339de --- /dev/null +++ b/adr/agent_discovery/mdns/src/discover.rs @@ -0,0 +1,110 @@ +use log::debug; +use mdns_sd::{ServiceDaemon, ServiceEvent}; + +use crate::SERVICE_TYPE; + +pub async fn discover() { + println!("Starting Harmony Master and browsing for agents..."); + + // Create a new mDNS daemon. + let mdns = ServiceDaemon::new().expect("Failed to create mDNS daemon"); + + // Start browsing for the service type. + // The receiver will be a stream of events. + let receiver = mdns.browse(SERVICE_TYPE).expect("Failed to browse"); + + println!( + "Listening for mDNS events for '{}'. Press Ctrl+C to exit.", + SERVICE_TYPE + ); + + std::thread::spawn(move || { + while let Ok(event) = receiver.recv() { + match event { + ServiceEvent::ServiceData(resolved) => { + println!("Resolved a new service: {}", resolved.fullname); + } + other_event => { + println!("Received other event: {:?}", &other_event); + } + } + } + }); + + // Gracefully shutdown the daemon. + std::thread::sleep(std::time::Duration::from_secs(1000000)); + mdns.shutdown().unwrap(); + + // Process events as they come in. + // while let Ok(event) = receiver.recv_async().await { + // debug!("Received event {event:?}"); + // // match event { + // // ServiceEvent::ServiceFound(svc_type, fullname) => { + // // println!("\n--- Agent Discovered ---"); + // // println!(" Service Name: {}", fullname()); + // // // You can now resolve this service to get its IP, port, and TXT records + // // // The resolve operation is a separate network call. + // // let receiver = mdns.browse(info.get_fullname()).unwrap(); + // // if let Ok(resolve_event) = receiver.recv_timeout(Duration::from_secs(2)) { + // // if let ServiceEvent::ServiceResolved(info) = resolve_event { + // // let ip = info.get_addresses().iter().next().unwrap(); + // // let port = info.get_port(); + // // let motherboard_id = info.get_property("id").map_or("N/A", |v| v.val_str()); + // // + // // println!(" IP: {}:{}", ip, port); + // // println!(" Motherboard ID: {}", motherboard_id); + // // println!("------------------------"); + // // + // // // TODO: Add this agent to your central list of discovered hosts. + // // } + // // } else { + // // println!("Could not resolve service '{}' in time.", info.get_fullname()); + // // } + // // } + // // ServiceEvent::ServiceRemoved(info) => { + // // println!("\n--- Agent Removed ---"); + // // println!(" Service Name: {}", info.get_fullname()); + // // println!("---------------------"); + // // // TODO: Remove this agent from your list. + // // } + // // _ => { + // // // We don't care about other event types for this example + // // } + // // } + // } +} + +async fn discover_example() { + use mdns_sd::{ServiceDaemon, ServiceEvent}; + + // Create a daemon + let mdns = ServiceDaemon::new().expect("Failed to create daemon"); + + // Use recently added `ServiceEvent::ServiceData`. + mdns.use_service_data(true) + .expect("Failed to use ServiceData"); + + // Browse for a service type. + let service_type = "_mdns-sd-my-test._udp.local."; + let receiver = mdns.browse(service_type).expect("Failed to browse"); + + // Receive the browse events in sync or async. Here is + // an example of using a thread. Users can call `receiver.recv_async().await` + // if running in async environment. + std::thread::spawn(move || { + while let Ok(event) = receiver.recv() { + match event { + ServiceEvent::ServiceData(resolved) => { + println!("Resolved a new service: {}", resolved.fullname); + } + other_event => { + println!("Received other event: {:?}", &other_event); + } + } + } + }); + + // Gracefully shutdown the daemon. + std::thread::sleep(std::time::Duration::from_secs(1)); + mdns.shutdown().unwrap(); +} diff --git a/adr/agent_discovery/mdns/src/main.rs b/adr/agent_discovery/mdns/src/main.rs new file mode 100644 index 0000000..9e22da5 --- /dev/null +++ b/adr/agent_discovery/mdns/src/main.rs @@ -0,0 +1,31 @@ +use clap::{Parser, ValueEnum}; + +mod advertise; +mod discover; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + #[arg(value_enum)] + profile: Profiles, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] +enum Profiles { + Advertise, + Discover, +} + +// The service type we are looking for. +const SERVICE_TYPE: &str = "_harmony._tcp.local."; + +#[tokio::main] +async fn main() { + env_logger::init(); + let args = Args::parse(); + + match args.profile { + Profiles::Advertise => advertise::advertise().await, + Profiles::Discover => discover::discover().await, + } +} diff --git a/check.sh b/check.sh index 3bcbc6a..616bccb 100755 --- a/check.sh +++ b/check.sh @@ -1,6 +1,7 @@ #!/bin/sh set -e +rustc --version cargo check --all-targets --all-features --keep-going cargo fmt --check cargo clippy diff --git a/data/pxe/okd/README.md b/data/pxe/okd/README.md new file mode 100644 index 0000000..7c53daf --- /dev/null +++ b/data/pxe/okd/README.md @@ -0,0 +1,8 @@ +Here lies all the data files required for an OKD cluster PXE boot setup. + +This inclues ISO files, binary boot files, ipxe, etc. + +TODO as of august 2025 : + +- `harmony_inventory_agent` should be downloaded from official releases, this embedded version is practical for now though +- The cluster ssh key should be generated and handled by harmony with the private key saved in a secret store diff --git a/data/pxe/okd/http_files/.gitattributes b/data/pxe/okd/http_files/.gitattributes new file mode 100644 index 0000000..f6bc73d --- /dev/null +++ b/data/pxe/okd/http_files/.gitattributes @@ -0,0 +1,9 @@ +harmony_inventory_agent filter=lfs diff=lfs merge=lfs -text +os filter=lfs diff=lfs merge=lfs -text +os/centos-stream-9 filter=lfs diff=lfs merge=lfs -text +os/centos-stream-9/images filter=lfs diff=lfs merge=lfs -text +os/centos-stream-9/initrd.img filter=lfs diff=lfs merge=lfs -text +os/centos-stream-9/vmlinuz filter=lfs diff=lfs merge=lfs -text +os/centos-stream-9/images/efiboot.img filter=lfs diff=lfs merge=lfs -text +os/centos-stream-9/images/install.img filter=lfs diff=lfs merge=lfs -text +os/centos-stream-9/images/pxeboot filter=lfs diff=lfs merge=lfs -text diff --git a/data/pxe/okd/http_files/cluster_ssh_key.pub b/data/pxe/okd/http_files/cluster_ssh_key.pub new file mode 100644 index 0000000..8a68662 --- /dev/null +++ b/data/pxe/okd/http_files/cluster_ssh_key.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBx6bDylvC68cVpjKfEFtLQJ/dOFi6PVS2vsIOqPDJIc jeangab@liliane2 diff --git a/data/pxe/okd/http_files/harmony_inventory_agent b/data/pxe/okd/http_files/harmony_inventory_agent new file mode 100755 index 0000000..ada5282 --- /dev/null +++ b/data/pxe/okd/http_files/harmony_inventory_agent @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aed14f47246bc20c5ada082f782da77da90ef9f78ef18fbf9f160f2101d9c92a +size 8129096 diff --git a/data/pxe/okd/http_files/os/centos-stream-9/images/efiboot.img b/data/pxe/okd/http_files/os/centos-stream-9/images/efiboot.img new file mode 100644 index 0000000..0c96e5d --- /dev/null +++ b/data/pxe/okd/http_files/os/centos-stream-9/images/efiboot.img @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dcf735affacdc66c3255ef74434bb5bc6c55387adbfc880e598b5ab68f2371d0 +size 7899136 diff --git a/data/pxe/okd/http_files/os/centos-stream-9/images/install.img b/data/pxe/okd/http_files/os/centos-stream-9/images/install.img new file mode 100644 index 0000000..89a855e --- /dev/null +++ b/data/pxe/okd/http_files/os/centos-stream-9/images/install.img @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9bf91e65af208695993a5e2b8b263d60e934dec83f139cc9ebe10a6bf91a24a +size 1197342720 diff --git a/data/pxe/okd/http_files/os/centos-stream-9/images/pxeboot/initrd.img b/data/pxe/okd/http_files/os/centos-stream-9/images/pxeboot/initrd.img new file mode 100644 index 0000000..8103aff Binary files /dev/null and b/data/pxe/okd/http_files/os/centos-stream-9/images/pxeboot/initrd.img differ diff --git a/data/pxe/okd/http_files/os/centos-stream-9/images/pxeboot/vmlinuz b/data/pxe/okd/http_files/os/centos-stream-9/images/pxeboot/vmlinuz new file mode 100755 index 0000000..50e9afa Binary files /dev/null and b/data/pxe/okd/http_files/os/centos-stream-9/images/pxeboot/vmlinuz differ diff --git a/data/pxe/okd/http_files/os/centos-stream-9/initrd.img b/data/pxe/okd/http_files/os/centos-stream-9/initrd.img new file mode 100644 index 0000000..f6811c3 --- /dev/null +++ b/data/pxe/okd/http_files/os/centos-stream-9/initrd.img @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9499b71d6d1d1c8e62ee6b8247d32a5d729ab7c55b310a5dcf9507fecc85142 +size 158657144 diff --git a/data/pxe/okd/http_files/os/centos-stream-9/vmlinuz b/data/pxe/okd/http_files/os/centos-stream-9/vmlinuz new file mode 100755 index 0000000..050a67c --- /dev/null +++ b/data/pxe/okd/http_files/os/centos-stream-9/vmlinuz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8dd3361f0b6db53f8d98e8bdaf32cf9ca4ae788847576fd2e9c5d6c890e53a1 +size 15083560 diff --git a/data/pxe/okd/tftpboot/ipxe.efi b/data/pxe/okd/tftpboot/ipxe.efi new file mode 100644 index 0000000..24a9510 Binary files /dev/null and b/data/pxe/okd/tftpboot/ipxe.efi differ diff --git a/data/pxe/okd/tftpboot/undionly.kpxe b/data/pxe/okd/tftpboot/undionly.kpxe new file mode 100644 index 0000000..a265f30 Binary files /dev/null and b/data/pxe/okd/tftpboot/undionly.kpxe differ diff --git a/docs/pxe_test/README.md b/docs/pxe_test/README.md new file mode 100644 index 0000000..c515aab --- /dev/null +++ b/docs/pxe_test/README.md @@ -0,0 +1,108 @@ +# OPNsense PXE Lab Environment + +This project contains a script to automatically set up a virtual lab environment for testing PXE boot services managed by an OPNsense firewall. + +## Overview + +The `pxe_vm_lab_setup.sh` script will create the following resources using libvirt/KVM: + +1. **A Virtual Network**: An isolated network named `harmonylan` (`virbr1`) for the lab. +2. **Two Virtual Machines**: + * `opnsense-pxe`: A firewall VM that will act as the gateway and PXE server. + * `pxe-node-1`: A client VM configured to boot from the network. + +## Prerequisites + +Ensure you have the following software installed on your Arch Linux host: + +* `libvirt` +* `qemu` +* `virt-install` (from the `virt-install` package) +* `curl` +* `bzip2` + +## Usage + +### 1. Create the Environment + +Run the `up` command to download the necessary images and create the network and VMs. + +```bash +sudo ./pxe_vm_lab_setup.sh up +``` + +### 2. Install and Configure OPNsense + +The OPNsense VM is created but the OS needs to be installed manually via the console. + +1. **Connect to the VM console**: + ```bash + sudo virsh console opnsense-pxe + ``` + +2. **Log in as the installer**: + * Username: `installer` + * Password: `opnsense` + +3. **Follow the on-screen installation wizard**. When prompted to assign network interfaces (`WAN` and `LAN`): + * Find the MAC address for the `harmonylan` interface by running this command in another terminal: + ```bash + virsh domiflist opnsense-pxe + # Example output: + # Interface Type Source Model MAC + # --------------------------------------------------------- + # vnet18 network default virtio 52:54:00:b5:c4:6d + # vnet19 network harmonylan virtio 52:54:00:21:f9:ba + ``` + * Assign the interface connected to `harmonylan` (e.g., `vtnet1` with MAC `52:54:00:21:f9:ba`) as your **LAN**. + * Assign the other interface as your **WAN**. + +4. After the installation is complete, **shut down** the VM from the console menu. + +5. **Detach the installation media** by editing the VM's configuration: + ```bash + sudo virsh edit opnsense-pxe + ``` + Find and **delete** the entire `` block corresponding to the `.img` file (the one with ``). + +6. **Start the VM** to boot into the newly installed system: + ```bash + sudo virsh start opnsense-pxe + ``` + +### 3. Connect to OPNsense from Your Host + +To configure OPNsense, you need to connect your host to the `harmonylan` network. + +1. By default, OPNsense configures its LAN interface with the IP `192.168.1.1`. +2. Assign a compatible IP address to your host's `virbr1` bridge interface: + ```bash + sudo ip addr add 192.168.1.5/24 dev virbr1 + ``` +3. You can now access the OPNsense VM from your host: + * **SSH**: `ssh root@192.168.1.1` (password: `opnsense`) + * **Web UI**: `https://192.168.1.1` + +### 4. Configure PXE Services with Harmony + +With connectivity established, you can now use Harmony to configure the OPNsense firewall for PXE booting. Point your Harmony OPNsense scores to the firewall using these details: + +* **Hostname/IP**: `192.168.1.1` +* **Credentials**: `root` / `opnsense` + +### 5. Boot the PXE Client + +Once your Harmony configuration has been applied and OPNsense is serving DHCP/TFTP, start the client VM. It will automatically attempt to boot from the network. + +```bash +sudo virsh start pxe-node-1 +sudo virsh console pxe-node-1 +``` + +## Cleanup + +To destroy all VMs and networks created by the script, run the `clean` command: + +```bash +sudo ./pxe_vm_lab_setup.sh clean +``` diff --git a/docs/pxe_test/pxe_vm_lab_setup.sh b/docs/pxe_test/pxe_vm_lab_setup.sh new file mode 100755 index 0000000..f53cea3 --- /dev/null +++ b/docs/pxe_test/pxe_vm_lab_setup.sh @@ -0,0 +1,191 @@ +#!/usr/bin/env bash +set -euo pipefail + +# --- Configuration --- +LAB_DIR="/var/lib/harmony_pxe_test" +IMG_DIR="${LAB_DIR}/images" +STATE_DIR="${LAB_DIR}/state" +VM_OPN="opnsense-pxe" +VM_PXE="pxe-node-1" +NET_HARMONYLAN="harmonylan" + +# Network settings for the isolated LAN +VLAN_CIDR="192.168.150.0/24" +VLAN_GW="192.168.150.1" +VLAN_MASK="255.255.255.0" + +# VM Specifications +RAM_OPN="2048" +VCPUS_OPN="2" +DISK_OPN_GB="10" +OS_VARIANT_OPN="freebsd14.0" # Updated to a more recent FreeBSD variant + +RAM_PXE="4096" +VCPUS_PXE="2" +DISK_PXE_GB="40" +OS_VARIANT_LINUX="centos-stream9" + +OPN_IMG_URL="https://mirror.ams1.nl.leaseweb.net/opnsense/releases/25.7/OPNsense-25.7-serial-amd64.img.bz2" +OPN_IMG_PATH="${IMG_DIR}/OPNsense-25.7-serial-amd64.img" +CENTOS_ISO_URL="https://mirror.stream.centos.org/9-stream/BaseOS/x86_64/os/images/boot.iso" +CENTOS_ISO_PATH="${IMG_DIR}/CentOS-Stream-9-latest-boot.iso" + +CONNECT_URI="qemu:///system" + +download_if_missing() { + local url="$1" + local dest="$2" + if [[ ! -f "$dest" ]]; then + echo "Downloading $url to $dest" + mkdir -p "$(dirname "$dest")" + local tmp + tmp="$(mktemp)" + curl -L --progress-bar "$url" -o "$tmp" + case "$url" in + *.bz2) bunzip2 -c "$tmp" > "$dest" && rm -f "$tmp" ;; + *) mv "$tmp" "$dest" ;; + esac + else + echo "Already present: $dest" + fi +} + +# Ensures a libvirt network is defined and active +ensure_network() { + local net_name="$1" + local net_xml_path="$2" + if virsh --connect "${CONNECT_URI}" net-info "${net_name}" >/dev/null 2>&1; then + echo "Network ${net_name} already exists." + else + echo "Defining network ${net_name} from ${net_xml_path}" + virsh --connect "${CONNECT_URI}" net-define "${net_xml_path}" + fi + + if ! virsh --connect "${CONNECT_URI}" net-info "${net_name}" | grep "Active: *yes"; then + echo "Starting network ${net_name}..." + virsh --connect "${CONNECT_URI}" net-start "${net_name}" + virsh --connect "${CONNECT_URI}" net-autostart "${net_name}" + fi +} + +# Destroys a VM completely +destroy_vm() { + local vm_name="$1" + if virsh --connect "${CONNECT_URI}" dominfo "$vm_name" >/dev/null 2>&1; then + echo "Destroying and undefining VM: ${vm_name}" + virsh --connect "${CONNECT_URI}" destroy "$vm_name" || true + virsh --connect "${CONNECT_URI}" undefine "$vm_name" --nvram + fi +} + +# Destroys a libvirt network +destroy_network() { + local net_name="$1" + if virsh --connect "${CONNECT_URI}" net-info "$net_name" >/dev/null 2>&1; then + echo "Destroying and undefining network: ${net_name}" + virsh --connect "${CONNECT_URI}" net-destroy "$net_name" || true + virsh --connect "${CONNECT_URI}" net-undefine "$net_name" + fi +} + +# --- Main Logic --- +create_lab_environment() { + # Create network definition files + cat > "${STATE_DIR}/default.xml" < + default + + + + + + + + +EOF + + cat > "${STATE_DIR}/${NET_HARMONYLAN}.xml" < + ${NET_HARMONYLAN} + + +EOF + + # Ensure both networks exist and are active + ensure_network "default" "${STATE_DIR}/default.xml" + ensure_network "${NET_HARMONYLAN}" "${STATE_DIR}/${NET_HARMONYLAN}.xml" + + # --- Create OPNsense VM (MODIFIED SECTION) --- + local disk_opn="${IMG_DIR}/${VM_OPN}.qcow2" + if [[ ! -f "$disk_opn" ]]; then + qemu-img create -f qcow2 "$disk_opn" "${DISK_OPN_GB}G" + fi + + echo "Creating OPNsense VM using serial image..." + virt-install \ + --connect "${CONNECT_URI}" \ + --name "${VM_OPN}" \ + --ram "${RAM_OPN}" \ + --vcpus "${VCPUS_OPN}" \ + --cpu host-passthrough \ + --os-variant "${OS_VARIANT_OPN}" \ + --graphics none \ + --noautoconsole \ + --disk path="${disk_opn}",device=disk,bus=virtio,boot.order=1 \ + --disk path="${OPN_IMG_PATH}",device=disk,bus=usb,readonly=on,boot.order=2 \ + --network network=default,model=virtio \ + --network network="${NET_HARMONYLAN}",model=virtio \ + --boot uefi,menu=on + + echo "OPNsense VM created. Connect with: sudo virsh console ${VM_OPN}" + echo "The VM will boot from the serial installation image." + echo "Login with user 'installer' and password 'opnsense' to start the installation." + echo "Install onto the VirtIO disk (vtbd0)." + echo "After installation, shutdown the VM, then run 'sudo virsh edit ${VM_OPN}' and remove the USB disk block to boot from the installed system." + + # --- Create PXE Client VM --- + local disk_pxe="${IMG_DIR}/${VM_PXE}.qcow2" + if [[ ! -f "$disk_pxe" ]]; then + qemu-img create -f qcow2 "$disk_pxe" "${DISK_PXE_GB}G" + fi + + echo "Creating PXE client VM..." + virt-install \ + --connect "${CONNECT_URI}" \ + --name "${VM_PXE}" \ + --ram "${RAM_PXE}" \ + --vcpus "${VCPUS_PXE}" \ + --cpu host-passthrough \ + --os-variant "${OS_VARIANT_LINUX}" \ + --graphics none \ + --noautoconsole \ + --disk path="${disk_pxe}",format=qcow2,bus=virtio \ + --network network="${NET_HARMONYLAN}",model=virtio \ + --pxe \ + --boot uefi,menu=on + + echo "PXE VM created. It will attempt to netboot on ${NET_HARMONYLAN}." +} + +# --- Script Entrypoint --- +case "${1:-}" in + up) + mkdir -p "${IMG_DIR}" "${STATE_DIR}" + download_if_missing "$OPN_IMG_URL" "$OPN_IMG_PATH" + download_if_missing "$CENTOS_ISO_URL" "$CENTOS_ISO_PATH" + create_lab_environment + echo "Lab setup complete. Use 'sudo virsh list --all' to see VMs." + ;; + clean) + destroy_vm "${VM_PXE}" + destroy_vm "${VM_OPN}" + destroy_network "${NET_HARMONYLAN}" + # Optionally destroy the default network if you want a full reset + # destroy_network "default" + echo "Cleanup complete." + ;; + *) + echo "Usage: sudo $0 {up|clean}" + exit 1 + ;; +esac diff --git a/examples/cli/src/main.rs b/examples/cli/src/main.rs index 34a032b..524d69c 100644 --- a/examples/cli/src/main.rs +++ b/examples/cli/src/main.rs @@ -1,6 +1,9 @@ use harmony::{ inventory::Inventory, - modules::dummy::{ErrorScore, PanicScore, SuccessScore}, + modules::{ + dummy::{ErrorScore, PanicScore, SuccessScore}, + inventory::DiscoverInventoryAgentScore, + }, topology::LocalhostTopology, }; @@ -13,6 +16,9 @@ async fn main() { Box::new(SuccessScore {}), Box::new(ErrorScore {}), Box::new(PanicScore {}), + Box::new(DiscoverInventoryAgentScore { + discovery_timeout: Some(10), + }), ], None, ) diff --git a/examples/nanodc/src/main.rs b/examples/nanodc/src/main.rs index 10754b4..10ba715 100644 --- a/examples/nanodc/src/main.rs +++ b/examples/nanodc/src/main.rs @@ -125,9 +125,12 @@ async fn main() { harmony::modules::okd::load_balancer::OKDLoadBalancerScore::new(&topology); let tftp_score = TftpScore::new(Url::LocalFolder("./data/watchguard/tftpboot".to_string())); - let http_score = StaticFilesHttpScore::new(Url::LocalFolder( - "./data/watchguard/pxe-http-files".to_string(), - )); + let http_score = StaticFilesHttpScore { + folder_to_serve: Some(Url::LocalFolder( + "./data/watchguard/pxe-http-files".to_string(), + )), + files: vec![], + }; let ipxe_score = IpxeScore::new(); harmony_tui::run( diff --git a/examples/okd_pxe/Cargo.toml b/examples/okd_pxe/Cargo.toml new file mode 100644 index 0000000..f75f42b --- /dev/null +++ b/examples/okd_pxe/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "example-pxe" +edition = "2024" +version.workspace = true +readme.workspace = true +license.workspace = true +publish = false + +[dependencies] +harmony = { path = "../../harmony" } +harmony_cli = { path = "../../harmony_cli" } +harmony_types = { path = "../../harmony_types" } +harmony_secret = { path = "../../harmony_secret" } +harmony_secret_derive = { path = "../../harmony_secret_derive" } +cidr = { workspace = true } +tokio = { workspace = true } +harmony_macros = { path = "../../harmony_macros" } +log = { workspace = true } +env_logger = { workspace = true } +url = { workspace = true } +serde.workspace = true diff --git a/examples/okd_pxe/src/main.rs b/examples/okd_pxe/src/main.rs new file mode 100644 index 0000000..42e4729 --- /dev/null +++ b/examples/okd_pxe/src/main.rs @@ -0,0 +1,24 @@ +mod topology; + +use crate::topology::{get_inventory, get_topology}; +use harmony::modules::okd::ipxe::OkdIpxeScore; + +#[tokio::main] +async fn main() { + let inventory = get_inventory(); + let topology = get_topology().await; + + let kickstart_filename = "inventory.kickstart".to_string(); + let cluster_pubkey_filename = "cluster_ssh_key.pub".to_string(); + let harmony_inventory_agent = "harmony_inventory_agent".to_string(); + + let ipxe_score = OkdIpxeScore { + kickstart_filename, + harmony_inventory_agent, + cluster_pubkey_filename, + }; + + harmony_cli::run(inventory, topology, vec![Box::new(ipxe_score)], None) + .await + .unwrap(); +} diff --git a/examples/okd_pxe/src/topology.rs b/examples/okd_pxe/src/topology.rs new file mode 100644 index 0000000..eb23908 --- /dev/null +++ b/examples/okd_pxe/src/topology.rs @@ -0,0 +1,78 @@ +use cidr::Ipv4Cidr; +use harmony::{ + hardware::{FirewallGroup, HostCategory, Location, PhysicalHost, SwitchGroup}, + infra::opnsense::OPNSenseManagementInterface, + inventory::Inventory, + topology::{HAClusterTopology, LogicalHost, UnmanagedRouter}, +}; +use harmony_macros::{ip, ipv4}; +use harmony_secret::{Secret, SecretManager}; +use serde::{Deserialize, Serialize}; +use std::{net::IpAddr, sync::Arc}; + +#[derive(Secret, Serialize, Deserialize, Debug, PartialEq)] +struct OPNSenseFirewallConfig { + username: String, + password: String, +} + +pub async fn get_topology() -> HAClusterTopology { + let firewall = harmony::topology::LogicalHost { + ip: ip!("192.168.1.1"), + name: String::from("opnsense-1"), + }; + + let config = SecretManager::get::().await; + let config = config.unwrap(); + + let opnsense = Arc::new( + harmony::infra::opnsense::OPNSenseFirewall::new( + firewall, + None, + &config.username, + &config.password, + ) + .await, + ); + let lan_subnet = ipv4!("192.168.1.0"); + let gateway_ipv4 = ipv4!("192.168.1.1"); + let gateway_ip = IpAddr::V4(gateway_ipv4); + harmony::topology::HAClusterTopology { + domain_name: "demo.harmony.mcd".to_string(), + router: Arc::new(UnmanagedRouter::new( + gateway_ip, + Ipv4Cidr::new(lan_subnet, 24).unwrap(), + )), + load_balancer: opnsense.clone(), + firewall: opnsense.clone(), + tftp_server: opnsense.clone(), + http_server: opnsense.clone(), + dhcp_server: opnsense.clone(), + dns_server: opnsense.clone(), + control_plane: vec![LogicalHost { + ip: ip!("10.100.8.20"), + name: "cp0".to_string(), + }], + bootstrap_host: LogicalHost { + ip: ip!("10.100.8.20"), + name: "cp0".to_string(), + }, + workers: vec![], + switch: vec![], + } +} + +pub fn get_inventory() -> Inventory { + Inventory { + location: Location::new( + "Some virtual machine or maybe a physical machine if you're cool".to_string(), + "testopnsense".to_string(), + ), + switch: SwitchGroup::from([]), + firewall: FirewallGroup::from([PhysicalHost::empty(HostCategory::Firewall) + .management(Arc::new(OPNSenseManagementInterface::new()))]), + storage_host: vec![], + worker_host: vec![], + control_plane_host: vec![], + } +} diff --git a/examples/okd_pxe/ssh_example_key b/examples/okd_pxe/ssh_example_key new file mode 100644 index 0000000..272bfb3 --- /dev/null +++ b/examples/okd_pxe/ssh_example_key @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACAcemw8pbwuvHFaYynxBbS0Cf3ThYuj1Utr7CDqjwySHAAAAJikacCNpGnA +jQAAAAtzc2gtZWQyNTUxOQAAACAcemw8pbwuvHFaYynxBbS0Cf3ThYuj1Utr7CDqjwySHA +AAAECiiKk4V6Q5cVs6axDM4sjAzZn/QCZLQekmYQXS9XbEYxx6bDylvC68cVpjKfEFtLQJ +/dOFi6PVS2vsIOqPDJIcAAAAEGplYW5nYWJAbGlsaWFuZTIBAgMEBQ== +-----END OPENSSH PRIVATE KEY----- diff --git a/examples/okd_pxe/ssh_example_key.pub b/examples/okd_pxe/ssh_example_key.pub new file mode 100644 index 0000000..8a68662 --- /dev/null +++ b/examples/okd_pxe/ssh_example_key.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBx6bDylvC68cVpjKfEFtLQJ/dOFi6PVS2vsIOqPDJIc jeangab@liliane2 diff --git a/examples/opnsense/src/main.rs b/examples/opnsense/src/main.rs index 61f8f18..e868829 100644 --- a/examples/opnsense/src/main.rs +++ b/examples/opnsense/src/main.rs @@ -80,9 +80,12 @@ async fn main() { let load_balancer_score = OKDLoadBalancerScore::new(&topology); let tftp_score = TftpScore::new(Url::LocalFolder("./data/watchguard/tftpboot".to_string())); - let http_score = StaticFilesHttpScore::new(Url::LocalFolder( - "./data/watchguard/pxe-http-files".to_string(), - )); + let http_score = StaticFilesHttpScore { + folder_to_serve: Some(Url::LocalFolder( + "./data/watchguard/pxe-http-files".to_string(), + )), + files: vec![], + }; harmony_tui::run( inventory, diff --git a/harmony/Cargo.toml b/harmony/Cargo.toml index 1ba4c94..56cce84 100644 --- a/harmony/Cargo.toml +++ b/harmony/Cargo.toml @@ -11,8 +11,7 @@ testing = [] [dependencies] rand = "0.9" hex = "0.4" -libredfish = "0.1.1" -reqwest = { version = "0.11", features = ["blocking", "json"] } +reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features = false } russh = "0.45.0" rust-ipmi = "0.1.1" semver = "1.0.23" @@ -67,7 +66,9 @@ bollard.workspace = true tar.workspace = true base64.workspace = true once_cell = "1.21.3" -harmony-secret-derive = { version = "0.1.0", path = "../harmony_secret_derive" } +harmony_inventory_agent = { path = "../harmony_inventory_agent" } +harmony_secret_derive = { version = "0.1.0", path = "../harmony_secret_derive" } +askama = "0.14.0" [dev-dependencies] pretty_assertions.workspace = true diff --git a/harmony/src/domain/data/file.rs b/harmony/src/domain/data/file.rs new file mode 100644 index 0000000..3ae7f3a --- /dev/null +++ b/harmony/src/domain/data/file.rs @@ -0,0 +1,22 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FileContent { + pub path: FilePath, + pub content: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum FilePath { + Relative(String), + Absolute(String), +} + +impl std::fmt::Display for FilePath { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + FilePath::Relative(path) => f.write_fmt(format_args!("./{path}")), + FilePath::Absolute(path) => f.write_fmt(format_args!("/{path}")), + } + } +} diff --git a/harmony/src/domain/data/mod.rs b/harmony/src/domain/data/mod.rs index e122b20..8d90a52 100644 --- a/harmony/src/domain/data/mod.rs +++ b/harmony/src/domain/data/mod.rs @@ -1,4 +1,6 @@ +mod file; mod id; mod version; +pub use file::*; pub use id::*; pub use version::*; diff --git a/harmony/src/domain/instrumentation.rs b/harmony/src/domain/instrumentation.rs index 6f0497e..538ea67 100644 --- a/harmony/src/domain/instrumentation.rs +++ b/harmony/src/domain/instrumentation.rs @@ -1,6 +1,5 @@ -use log::debug; use once_cell::sync::Lazy; -use tokio::sync::broadcast; +use std::{collections::HashMap, sync::Mutex}; use crate::modules::application::ApplicationFeatureStatus; @@ -40,43 +39,46 @@ pub enum HarmonyEvent { }, } -static HARMONY_EVENT_BUS: Lazy> = Lazy::new(|| { - // TODO: Adjust channel capacity - let (tx, _rx) = broadcast::channel(100); - tx -}); +type Subscriber = Box; -pub fn instrument(event: HarmonyEvent) -> Result<(), &'static str> { - if cfg!(any(test, feature = "testing")) { - let _ = event; // Suppress the "unused variable" warning for `event` - Ok(()) - } else { - match HARMONY_EVENT_BUS.send(event) { - Ok(_) => Ok(()), - Err(_) => Err("send error: no subscribers"), - } - } -} +static SUBSCRIBERS: Lazy>> = + Lazy::new(|| Mutex::new(HashMap::new())); -pub async fn subscribe(name: &str, mut handler: F) +/// Subscribes a listener to all instrumentation events. +/// +/// Simply provide a unique name and a closure to run when an event happens. +/// +/// # Example +/// ``` +/// use harmony::instrumentation; +/// instrumentation::subscribe("my_logger", |event| { +/// println!("Event occurred: {:?}", event); +/// }); +/// ``` +pub fn subscribe(name: &str, callback: F) where - F: FnMut(HarmonyEvent) -> Fut + Send + 'static, - Fut: Future + Send, + F: Fn(&HarmonyEvent) + Send + Sync + 'static, { - let mut rx = HARMONY_EVENT_BUS.subscribe(); - debug!("[{name}] Service started. Listening for events..."); - loop { - match rx.recv().await { - Ok(event) => { - if !handler(event).await { - debug!("[{name}] Handler requested exit."); - break; - } - } - Err(broadcast::error::RecvError::Lagged(n)) => { - debug!("[{name}] Lagged behind by {n} messages."); - } - Err(_) => break, - } - } + let mut subs = SUBSCRIBERS.lock().unwrap(); + subs.insert(name.to_string(), Box::new(callback)); +} + +/// Instruments an event, notifying all subscribers. +/// +/// This will call every closure that was registered with `subscribe`. +/// +/// # Example +/// ``` +/// use harmony::instrumentation; +/// use harmony::instrumentation::HarmonyEvent; +/// instrumentation::instrument(HarmonyEvent::HarmonyStarted); +/// ``` +pub fn instrument(event: HarmonyEvent) -> Result<(), &'static str> { + let subs = SUBSCRIBERS.lock().unwrap(); + + for callback in subs.values() { + callback(&event); + } + + Ok(()) } diff --git a/harmony/src/domain/interpret/mod.rs b/harmony/src/domain/interpret/mod.rs index 0e66a95..be671a2 100644 --- a/harmony/src/domain/interpret/mod.rs +++ b/harmony/src/domain/interpret/mod.rs @@ -32,6 +32,7 @@ pub enum InterpretName { Lamp, ApplicationMonitoring, K8sPrometheusCrdAlerting, + DiscoverInventoryAgent, CephClusterHealth, } @@ -59,6 +60,7 @@ impl std::fmt::Display for InterpretName { InterpretName::Lamp => f.write_str("LAMP"), InterpretName::ApplicationMonitoring => f.write_str("ApplicationMonitoring"), InterpretName::K8sPrometheusCrdAlerting => f.write_str("K8sPrometheusCrdAlerting"), + InterpretName::DiscoverInventoryAgent => f.write_str("DiscoverInventoryAgent"), InterpretName::CephClusterHealth => f.write_str("CephClusterHealth"), } } diff --git a/harmony/src/domain/maestro/mod.rs b/harmony/src/domain/maestro/mod.rs index d9587c8..3469ea3 100644 --- a/harmony/src/domain/maestro/mod.rs +++ b/harmony/src/domain/maestro/mod.rs @@ -74,6 +74,7 @@ impl Maestro { fn is_topology_initialized(&self) -> bool { self.topology_state.status == TopologyStatus::Success + || self.topology_state.status == TopologyStatus::Noop } pub async fn interpret(&self, score: Box>) -> Result { diff --git a/harmony/src/domain/topology/ha_cluster.rs b/harmony/src/domain/topology/ha_cluster.rs index 737419f..31d1a7f 100644 --- a/harmony/src/domain/topology/ha_cluster.rs +++ b/harmony/src/domain/topology/ha_cluster.rs @@ -1,9 +1,12 @@ use async_trait::async_trait; use harmony_macros::ip; use harmony_types::net::MacAddress; +use log::debug; use log::info; +use crate::data::FileContent; use crate::executors::ExecutorError; +use crate::topology::PxeOptions; use super::DHCPStaticEntry; use super::DhcpServer; @@ -49,9 +52,10 @@ impl Topology for HAClusterTopology { "HAClusterTopology" } async fn ensure_ready(&self) -> Result { - todo!( + debug!( "ensure_ready, not entirely sure what it should do here, probably something like verify that the hosts are reachable and all services are up and ready." - ) + ); + Ok(PreparationOutcome::Noop) } } @@ -153,12 +157,10 @@ impl DhcpServer for HAClusterTopology { async fn list_static_mappings(&self) -> Vec<(MacAddress, IpAddress)> { self.dhcp_server.list_static_mappings().await } - async fn set_next_server(&self, ip: IpAddress) -> Result<(), ExecutorError> { - self.dhcp_server.set_next_server(ip).await - } - async fn set_boot_filename(&self, boot_filename: &str) -> Result<(), ExecutorError> { - self.dhcp_server.set_boot_filename(boot_filename).await + async fn set_pxe_options(&self, options: PxeOptions) -> Result<(), ExecutorError> { + self.dhcp_server.set_pxe_options(options).await } + fn get_ip(&self) -> IpAddress { self.dhcp_server.get_ip() } @@ -168,16 +170,6 @@ impl DhcpServer for HAClusterTopology { async fn commit_config(&self) -> Result<(), ExecutorError> { self.dhcp_server.commit_config().await } - - async fn set_filename(&self, filename: &str) -> Result<(), ExecutorError> { - self.dhcp_server.set_filename(filename).await - } - async fn set_filename64(&self, filename64: &str) -> Result<(), ExecutorError> { - self.dhcp_server.set_filename64(filename64).await - } - async fn set_filenameipxe(&self, filenameipxe: &str) -> Result<(), ExecutorError> { - self.dhcp_server.set_filenameipxe(filenameipxe).await - } } #[async_trait] @@ -221,17 +213,21 @@ impl HttpServer for HAClusterTopology { self.http_server.serve_files(url).await } + async fn serve_file_content(&self, file: &FileContent) -> Result<(), ExecutorError> { + self.http_server.serve_file_content(file).await + } + fn get_ip(&self) -> IpAddress { - unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) + self.http_server.get_ip() } async fn ensure_initialized(&self) -> Result<(), ExecutorError> { - unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) + self.http_server.ensure_initialized().await } async fn commit_config(&self) -> Result<(), ExecutorError> { - unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) + self.http_server.commit_config().await } async fn reload_restart(&self) -> Result<(), ExecutorError> { - unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) + self.http_server.reload_restart().await } } @@ -299,19 +295,7 @@ impl DhcpServer for DummyInfra { async fn list_static_mappings(&self) -> Vec<(MacAddress, IpAddress)> { unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) } - async fn set_next_server(&self, _ip: IpAddress) -> Result<(), ExecutorError> { - unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) - } - async fn set_boot_filename(&self, _boot_filename: &str) -> Result<(), ExecutorError> { - unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) - } - async fn set_filename(&self, _filename: &str) -> Result<(), ExecutorError> { - unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) - } - async fn set_filename64(&self, _filename: &str) -> Result<(), ExecutorError> { - unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) - } - async fn set_filenameipxe(&self, _filenameipxe: &str) -> Result<(), ExecutorError> { + async fn set_pxe_options(&self, _options: PxeOptions) -> Result<(), ExecutorError> { unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) } fn get_ip(&self) -> IpAddress { @@ -381,6 +365,9 @@ impl HttpServer for DummyInfra { async fn serve_files(&self, _url: &Url) -> Result<(), ExecutorError> { unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) } + async fn serve_file_content(&self, _file: &FileContent) -> Result<(), ExecutorError> { + unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) + } fn get_ip(&self) -> IpAddress { unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) } diff --git a/harmony/src/domain/topology/http.rs b/harmony/src/domain/topology/http.rs index 42a38af..1d35621 100644 --- a/harmony/src/domain/topology/http.rs +++ b/harmony/src/domain/topology/http.rs @@ -1,4 +1,4 @@ -use crate::executors::ExecutorError; +use crate::{data::FileContent, executors::ExecutorError}; use async_trait::async_trait; use super::{IpAddress, Url}; @@ -6,6 +6,7 @@ use super::{IpAddress, Url}; #[async_trait] pub trait HttpServer: Send + Sync { async fn serve_files(&self, url: &Url) -> Result<(), ExecutorError>; + async fn serve_file_content(&self, file: &FileContent) -> Result<(), ExecutorError>; fn get_ip(&self) -> IpAddress; // async fn set_ip(&self, ip: IpAddress) -> Result<(), ExecutorError>; diff --git a/harmony/src/domain/topology/k8s.rs b/harmony/src/domain/topology/k8s.rs index 1887c33..c9d0d58 100644 --- a/harmony/src/domain/topology/k8s.rs +++ b/harmony/src/domain/topology/k8s.rs @@ -185,7 +185,10 @@ impl K8sClient { if let Some(s) = status.status { let mut stdout_buf = String::new(); if let Some(mut stdout) = process.stdout().take() { - stdout.read_to_string(&mut stdout_buf).await; + stdout + .read_to_string(&mut stdout_buf) + .await + .map_err(|e| format!("Failed to get status stdout {e}"))?; } debug!("Status: {} - {:?}", s, status.details); if s == "Success" { diff --git a/harmony/src/domain/topology/network.rs b/harmony/src/domain/topology/network.rs index 42ff8c3..f68d29c 100644 --- a/harmony/src/domain/topology/network.rs +++ b/harmony/src/domain/topology/network.rs @@ -46,16 +46,19 @@ pub trait K8sclient: Send + Sync { async fn k8s_client(&self) -> Result, String>; } +pub struct PxeOptions { + pub ipxe_filename: String, + pub bios_filename: String, + pub efi_filename: String, + pub tftp_ip: Option, +} + #[async_trait] pub trait DhcpServer: Send + Sync + std::fmt::Debug { async fn add_static_mapping(&self, entry: &DHCPStaticEntry) -> Result<(), ExecutorError>; async fn remove_static_mapping(&self, mac: &MacAddress) -> Result<(), ExecutorError>; async fn list_static_mappings(&self) -> Vec<(MacAddress, IpAddress)>; - async fn set_next_server(&self, ip: IpAddress) -> Result<(), ExecutorError>; - async fn set_boot_filename(&self, boot_filename: &str) -> Result<(), ExecutorError>; - async fn set_filename(&self, filename: &str) -> Result<(), ExecutorError>; - async fn set_filename64(&self, filename64: &str) -> Result<(), ExecutorError>; - async fn set_filenameipxe(&self, filenameipxe: &str) -> Result<(), ExecutorError>; + async fn set_pxe_options(&self, pxe_options: PxeOptions) -> Result<(), ExecutorError>; fn get_ip(&self) -> IpAddress; fn get_host(&self) -> LogicalHost; async fn commit_config(&self) -> Result<(), ExecutorError>; diff --git a/harmony/src/infra/opnsense/dhcp.rs b/harmony/src/infra/opnsense/dhcp.rs index bea44fe..ee4ba6c 100644 --- a/harmony/src/infra/opnsense/dhcp.rs +++ b/harmony/src/infra/opnsense/dhcp.rs @@ -1,10 +1,10 @@ use async_trait::async_trait; use harmony_types::net::MacAddress; -use log::debug; +use log::info; use crate::{ executors::ExecutorError, - topology::{DHCPStaticEntry, DhcpServer, IpAddress, LogicalHost}, + topology::{DHCPStaticEntry, DhcpServer, IpAddress, LogicalHost, PxeOptions}, }; use super::OPNSenseFirewall; @@ -26,7 +26,7 @@ impl DhcpServer for OPNSenseFirewall { .unwrap(); } - debug!("Registered {:?}", entry); + info!("Registered {:?}", entry); Ok(()) } @@ -46,57 +46,25 @@ impl DhcpServer for OPNSenseFirewall { 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 fn set_filename(&self, filename: &str) -> Result<(), ExecutorError> { - { - let mut writable_opnsense = self.opnsense_config.write().await; - writable_opnsense.dhcp().set_filename(filename); - debug!("OPNsense dhcp server set filename {filename}"); - } - - Ok(()) - } - - async fn set_filename64(&self, filename: &str) -> Result<(), ExecutorError> { - { - let mut writable_opnsense = self.opnsense_config.write().await; - writable_opnsense.dhcp().set_filename64(filename); - debug!("OPNsense dhcp server set filename {filename}"); - } - - Ok(()) - } - - async fn set_filenameipxe(&self, filenameipxe: &str) -> Result<(), ExecutorError> { - { - let mut writable_opnsense = self.opnsense_config.write().await; - writable_opnsense.dhcp().set_filenameipxe(filenameipxe); - debug!("OPNsense dhcp server set filenameipxe {filenameipxe}"); - } - - Ok(()) + async fn set_pxe_options(&self, options: PxeOptions) -> Result<(), ExecutorError> { + let mut writable_opnsense = self.opnsense_config.write().await; + let PxeOptions { + ipxe_filename, + bios_filename, + efi_filename, + tftp_ip, + } = options; + writable_opnsense + .dhcp() + .set_pxe_options( + tftp_ip.map(|i| i.to_string()), + bios_filename, + efi_filename, + ipxe_filename, + ) + .await + .map_err(|dhcp_error| { + ExecutorError::UnexpectedError(format!("Failed to set_pxe_options : {dhcp_error}")) + }) } } diff --git a/harmony/src/infra/opnsense/http.rs b/harmony/src/infra/opnsense/http.rs index a51bf34..1d34d4c 100644 --- a/harmony/src/infra/opnsense/http.rs +++ b/harmony/src/infra/opnsense/http.rs @@ -2,23 +2,23 @@ use async_trait::async_trait; use log::info; use crate::{ + data::FileContent, executors::ExecutorError, topology::{HttpServer, IpAddress, Url}, }; use super::OPNSenseFirewall; +const OPNSENSE_HTTP_ROOT_PATH: &str = "/usr/local/http"; #[async_trait] impl HttpServer for OPNSenseFirewall { async fn serve_files(&self, url: &Url) -> Result<(), ExecutorError> { - let http_root_path = "/usr/local/http"; - let config = self.opnsense_config.read().await; - info!("Uploading files from url {url} to {http_root_path}"); + info!("Uploading files from url {url} to {OPNSENSE_HTTP_ROOT_PATH}"); match url { Url::LocalFolder(path) => { config - .upload_files(path, http_root_path) + .upload_files(path, OPNSENSE_HTTP_ROOT_PATH) .await .map_err(|e| ExecutorError::UnexpectedError(e.to_string()))?; } @@ -27,8 +27,29 @@ impl HttpServer for OPNSenseFirewall { Ok(()) } + async fn serve_file_content(&self, file: &FileContent) -> Result<(), ExecutorError> { + let path = match &file.path { + crate::data::FilePath::Relative(path) => { + format!("{OPNSENSE_HTTP_ROOT_PATH}/{}", path.to_string()) + } + crate::data::FilePath::Absolute(path) => { + return Err(ExecutorError::ConfigurationError(format!( + "Cannot serve file from http server with absolute path : {path}" + ))); + } + }; + + let config = self.opnsense_config.read().await; + info!("Uploading file content to {}", path); + config + .upload_file_content(&path, &file.content) + .await + .map_err(|e| ExecutorError::UnexpectedError(e.to_string()))?; + Ok(()) + } + fn get_ip(&self) -> IpAddress { - todo!(); + OPNSenseFirewall::get_ip(self) } async fn commit_config(&self) -> Result<(), ExecutorError> { diff --git a/harmony/src/infra/opnsense/tftp.rs b/harmony/src/infra/opnsense/tftp.rs index c7b7f2b..1bf7b6c 100644 --- a/harmony/src/infra/opnsense/tftp.rs +++ b/harmony/src/infra/opnsense/tftp.rs @@ -28,7 +28,7 @@ impl TftpServer for OPNSenseFirewall { } fn get_ip(&self) -> IpAddress { - todo!() + OPNSenseFirewall::get_ip(self) } async fn set_ip(&self, ip: IpAddress) -> Result<(), ExecutorError> { diff --git a/harmony/src/modules/dhcp.rs b/harmony/src/modules/dhcp.rs index 04ef093..81643db 100644 --- a/harmony/src/modules/dhcp.rs +++ b/harmony/src/modules/dhcp.rs @@ -7,7 +7,7 @@ use crate::{ domain::{data::Version, interpret::InterpretStatus}, interpret::{Interpret, InterpretError, InterpretName, Outcome}, inventory::Inventory, - topology::{DHCPStaticEntry, DhcpServer, HostBinding, IpAddress, Topology}, + topology::{DHCPStaticEntry, DhcpServer, HostBinding, IpAddress, PxeOptions, Topology}, }; use crate::domain::score::Score; @@ -98,69 +98,14 @@ impl DhcpInterpret { _inventory: &Inventory, dhcp_server: &D, ) -> Result { - let next_server_outcome = match self.score.next_server { - Some(next_server) => { - dhcp_server.set_next_server(next_server).await?; - Outcome::new( - InterpretStatus::SUCCESS, - format!("Dhcp Interpret Set next boot to {next_server}"), - ) - } - None => Outcome::noop(), + let pxe_options = PxeOptions { + ipxe_filename: self.score.filenameipxe.clone().unwrap_or_default(), + bios_filename: self.score.filename.clone().unwrap_or_default(), + efi_filename: self.score.filename64.clone().unwrap_or_default(), + tftp_ip: self.score.next_server, }; - let boot_filename_outcome = match &self.score.boot_filename { - Some(boot_filename) => { - dhcp_server.set_boot_filename(boot_filename).await?; - Outcome::new( - InterpretStatus::SUCCESS, - format!("Dhcp Interpret Set boot filename to {boot_filename}"), - ) - } - None => Outcome::noop(), - }; - - let filename_outcome = match &self.score.filename { - Some(filename) => { - dhcp_server.set_filename(filename).await?; - Outcome::new( - InterpretStatus::SUCCESS, - format!("Dhcp Interpret Set filename to {filename}"), - ) - } - None => Outcome::noop(), - }; - - let filename64_outcome = match &self.score.filename64 { - Some(filename64) => { - dhcp_server.set_filename64(filename64).await?; - Outcome::new( - InterpretStatus::SUCCESS, - format!("Dhcp Interpret Set filename64 to {filename64}"), - ) - } - None => Outcome::noop(), - }; - - let filenameipxe_outcome = match &self.score.filenameipxe { - Some(filenameipxe) => { - dhcp_server.set_filenameipxe(filenameipxe).await?; - Outcome::new( - InterpretStatus::SUCCESS, - format!("Dhcp Interpret Set filenameipxe to {filenameipxe}"), - ) - } - None => Outcome::noop(), - }; - - if next_server_outcome.status == InterpretStatus::NOOP - && boot_filename_outcome.status == InterpretStatus::NOOP - && filename_outcome.status == InterpretStatus::NOOP - && filename64_outcome.status == InterpretStatus::NOOP - && filenameipxe_outcome.status == InterpretStatus::NOOP - { - return Ok(Outcome::noop()); - } + dhcp_server.set_pxe_options(pxe_options).await?; Ok(Outcome::new( InterpretStatus::SUCCESS, diff --git a/harmony/src/modules/http.rs b/harmony/src/modules/http.rs index 36af092..a04ea73 100644 --- a/harmony/src/modules/http.rs +++ b/harmony/src/modules/http.rs @@ -3,7 +3,7 @@ use derive_new::new; use serde::Serialize; use crate::{ - data::{Id, Version}, + data::{FileContent, Id, Version}, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, score::Score, @@ -23,7 +23,8 @@ use crate::{ /// ``` #[derive(Debug, new, Clone, Serialize)] pub struct StaticFilesHttpScore { - files_to_serve: Url, + pub folder_to_serve: Option, + pub files: Vec, } impl Score for StaticFilesHttpScore { @@ -50,12 +51,25 @@ impl Interpret for StaticFilesHttpInterpret { ) -> Result { http_server.ensure_initialized().await?; // http_server.set_ip(topology.router.get_gateway()).await?; - http_server.serve_files(&self.score.files_to_serve).await?; + if let Some(folder) = self.score.folder_to_serve.as_ref() { + http_server.serve_files(folder).await?; + } + + for f in self.score.files.iter() { + http_server.serve_file_content(&f).await? + } + http_server.commit_config().await?; http_server.reload_restart().await?; Ok(Outcome::success(format!( - "Http Server running and serving files from {}", - self.score.files_to_serve + "Http Server running and serving files from folder {:?} and content for {}", + self.score.folder_to_serve, + self.score + .files + .iter() + .map(|f| f.path.to_string()) + .collect::>() + .join(",") ))) } diff --git a/harmony/src/modules/inventory/mod.rs b/harmony/src/modules/inventory/mod.rs new file mode 100644 index 0000000..3570d14 --- /dev/null +++ b/harmony/src/modules/inventory/mod.rs @@ -0,0 +1,72 @@ +use async_trait::async_trait; +use harmony_inventory_agent::local_presence::DiscoveryEvent; +use log::info; +use serde::{Deserialize, Serialize}; + +use crate::{ + data::{Id, Version}, + interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, + inventory::Inventory, + score::Score, + topology::Topology, +}; + +/// This launches an harmony_inventory_agent discovery process +/// This will allow us to register/update hosts running harmony_inventory_agent +/// from LAN in the Harmony inventory +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DiscoverInventoryAgentScore { + pub discovery_timeout: Option, +} + +impl Score for DiscoverInventoryAgentScore { + fn name(&self) -> String { + "DiscoverInventoryAgentScore".to_string() + } + + fn create_interpret(&self) -> Box> { + Box::new(DiscoverInventoryAgentInterpret { + score: self.clone(), + }) + } +} + +#[derive(Debug)] +struct DiscoverInventoryAgentInterpret { + score: DiscoverInventoryAgentScore, +} + +#[async_trait] +impl Interpret for DiscoverInventoryAgentInterpret { + async fn execute( + &self, + inventory: &Inventory, + topology: &T, + ) -> Result { + harmony_inventory_agent::local_presence::discover_agents( + self.score.discovery_timeout, + on_discover_event, + ); + todo!() + } + + fn get_name(&self) -> InterpretName { + InterpretName::DiscoverInventoryAgent + } + + fn get_version(&self) -> Version { + todo!() + } + + fn get_status(&self) -> InterpretStatus { + todo!() + } + + fn get_children(&self) -> Vec { + todo!() + } +} + +fn on_discover_event(event: &DiscoveryEvent) { + info!("got discovery event {event:?}"); +} diff --git a/harmony/src/modules/mod.rs b/harmony/src/modules/mod.rs index 6df5c41..8935278 100644 --- a/harmony/src/modules/mod.rs +++ b/harmony/src/modules/mod.rs @@ -5,6 +5,7 @@ pub mod dns; pub mod dummy; pub mod helm; pub mod http; +pub mod inventory; pub mod ipxe; pub mod k3d; pub mod k8s; diff --git a/harmony/src/modules/monitoring/ntfy/ntfy.rs b/harmony/src/modules/monitoring/ntfy/ntfy.rs index 77822d9..68106b7 100644 --- a/harmony/src/modules/monitoring/ntfy/ntfy.rs +++ b/harmony/src/modules/monitoring/ntfy/ntfy.rs @@ -37,18 +37,6 @@ pub struct NtfyInterpret { pub score: NtfyScore, } -#[derive(Debug, EnumString, Display)] -enum NtfyAccessMode { - #[strum(serialize = "read-write", serialize = "rw")] - ReadWrite, - #[strum(serialize = "read-only", serialize = "ro", serialize = "read")] - ReadOnly, - #[strum(serialize = "write-only", serialize = "wo", serialize = "write")] - WriteOnly, - #[strum(serialize = "deny", serialize = "none")] - Deny, -} - #[derive(Debug, EnumString, Display)] enum NtfyRole { #[strum(serialize = "user")] diff --git a/harmony/src/modules/okd/ipxe.rs b/harmony/src/modules/okd/ipxe.rs new file mode 100644 index 0000000..a0b32d4 --- /dev/null +++ b/harmony/src/modules/okd/ipxe.rs @@ -0,0 +1,148 @@ +use askama::Template; +use async_trait::async_trait; +use derive_new::new; +use serde::Serialize; +use std::net::IpAddr; + +use crate::{ + data::{FileContent, FilePath, Id, Version}, + interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, + inventory::Inventory, + modules::{dhcp::DhcpScore, http::StaticFilesHttpScore, tftp::TftpScore}, + score::Score, + topology::{DhcpServer, HttpServer, Router, TftpServer, Topology, Url}, +}; + +#[derive(Debug, new, Clone, Serialize)] +pub struct OkdIpxeScore { + pub kickstart_filename: String, + pub harmony_inventory_agent: String, + pub cluster_pubkey_filename: String, +} + +impl Score for OkdIpxeScore { + fn create_interpret(&self) -> Box> { + Box::new(IpxeInterpret::new(self.clone())) + } + + fn name(&self) -> String { + "OkdIpxeScore".to_string() + } +} + +#[derive(Debug, new, Clone)] +pub struct IpxeInterpret { + score: OkdIpxeScore, +} + +#[async_trait] +impl Interpret for IpxeInterpret { + async fn execute( + &self, + inventory: &Inventory, + topology: &T, + ) -> Result { + let gateway_ip = topology.get_gateway(); + + let scores: Vec>> = vec![ + Box::new(DhcpScore { + host_binding: vec![], + next_server: Some(topology.get_gateway()), + boot_filename: None, + filename: Some("undionly.kpxe".to_string()), + filename64: Some("ipxe.efi".to_string()), + filenameipxe: Some(format!("http://{gateway_ip}:8080/boot.ipxe").to_string()), + }), + Box::new(TftpScore { + files_to_serve: Url::LocalFolder("./data/pxe/okd/tftpboot/".to_string()), + }), + Box::new(StaticFilesHttpScore { + // TODO The current russh based copy is way too slow, check for a lib update or use scp + // when available + // + // For now just run : + // scp -r data/pxe/okd/http_files/* root@192.168.1.1:/usr/local/http/ + // + folder_to_serve: None, + // folder_to_serve: Some(Url::LocalFolder("./data/pxe/okd/http_files/".to_string())), + files: vec![ + FileContent { + path: FilePath::Relative("boot.ipxe".to_string()), + content: BootIpxeTpl { + gateway_ip: &gateway_ip, + } + .to_string(), + }, + FileContent { + path: FilePath::Relative(self.score.kickstart_filename.clone()), + content: InventoryKickstartTpl { + gateway_ip: &gateway_ip, + harmony_inventory_agent: &self.score.harmony_inventory_agent, + cluster_pubkey_filename: &self.score.cluster_pubkey_filename, + } + .to_string(), + }, + FileContent { + path: FilePath::Relative("fallback.ipxe".to_string()), + content: FallbackIpxeTpl { + gateway_ip: &gateway_ip, + kickstart_filename: &self.score.kickstart_filename, + } + .to_string(), + }, + ], + }), + ]; + + for score in scores { + let result = score.interpret(inventory, topology).await; + match result { + Ok(outcome) => match outcome.status { + InterpretStatus::SUCCESS => continue, + InterpretStatus::NOOP => continue, + _ => return Err(InterpretError::new(outcome.message)), + }, + Err(e) => return Err(e), + }; + } + + Ok(Outcome::success("Ipxe installed".to_string())) + } + + fn get_name(&self) -> InterpretName { + InterpretName::Ipxe + } + + fn get_version(&self) -> Version { + todo!() + } + + fn get_status(&self) -> InterpretStatus { + todo!() + } + + fn get_children(&self) -> Vec { + todo!() + } +} + +#[derive(Template)] +#[template(path = "boot.ipxe.j2")] +struct BootIpxeTpl<'a> { + gateway_ip: &'a IpAddr, +} + +#[derive(Template)] +#[template(path = "fallback.ipxe.j2")] +struct FallbackIpxeTpl<'a> { + gateway_ip: &'a IpAddr, + kickstart_filename: &'a str, +} + +#[derive(Template)] +#[template(path = "inventory.kickstart.j2")] +struct InventoryKickstartTpl<'a> { + gateway_ip: &'a IpAddr, + cluster_pubkey_filename: &'a str, + harmony_inventory_agent: &'a str, +} diff --git a/harmony/src/modules/okd/mod.rs b/harmony/src/modules/okd/mod.rs index 8811771..fe61b1e 100644 --- a/harmony/src/modules/okd/mod.rs +++ b/harmony/src/modules/okd/mod.rs @@ -2,5 +2,6 @@ pub mod bootstrap_dhcp; pub mod bootstrap_load_balancer; pub mod dhcp; pub mod dns; +pub mod ipxe; pub mod load_balancer; pub mod upgrade; diff --git a/harmony/src/modules/storage/ceph/ceph_osd_replacement_score.rs b/harmony/src/modules/storage/ceph/ceph_osd_replacement_score.rs index a4b0cb0..708c501 100644 --- a/harmony/src/modules/storage/ceph/ceph_osd_replacement_score.rs +++ b/harmony/src/modules/storage/ceph/ceph_osd_replacement_score.rs @@ -1,5 +1,4 @@ use std::{ - process::Command, sync::Arc, time::{Duration, Instant}, }; diff --git a/harmony/src/modules/tftp.rs b/harmony/src/modules/tftp.rs index 357e480..6763ec0 100644 --- a/harmony/src/modules/tftp.rs +++ b/harmony/src/modules/tftp.rs @@ -12,7 +12,7 @@ use crate::{ #[derive(Debug, new, Clone, Serialize)] pub struct TftpScore { - files_to_serve: Url, + pub files_to_serve: Url, } impl Score for TftpScore { diff --git a/harmony/templates/boot.ipxe.j2 b/harmony/templates/boot.ipxe.j2 new file mode 100644 index 0000000..94ea07b --- /dev/null +++ b/harmony/templates/boot.ipxe.j2 @@ -0,0 +1,6 @@ +#!ipxe + +set base-url http://{{ gateway_ip }}:8080 +set hostfile ${base-url}/byMAC/01-${mac:hexhyp}.ipxe + +chain ${hostfile} || chain ${base-url}/fallback.ipxe diff --git a/harmony/templates/fallback.ipxe.j2 b/harmony/templates/fallback.ipxe.j2 new file mode 100644 index 0000000..44c40a9 --- /dev/null +++ b/harmony/templates/fallback.ipxe.j2 @@ -0,0 +1,40 @@ +#!ipxe + +# ================================================================= +# Harmony Discovery Agent - Default Boot Script (default.ipxe) +# ================================================================= +# +# This script boots the CentOS Stream live environment for the +# purpose of hardware inventory. It loads the kernel and initramfs +# directly and passes a Kickstart URL for full automation. +# + +# --- Configuration +# Set the base URL for where the CentOS kernel/initrd are hosted. +set os_base_url http://{{gateway_ip}}:8080/os/centos-stream-9 +# Set the URL for the Kickstart file. +set ks_url http://{{ gateway_ip }}:8080/{{ kickstart_filename }} + +# --- Boot Process +echo "Harmony: Starting automated node discovery..." +echo "Fetching kernel from ${os_base_url}/vmlinuz..." +kernel ${os_base_url}/vmlinuz + +echo "Fetching initramfs from ${os_base_url}/initrd.img..." +initrd ${os_base_url}/initrd.img + +echo "Configuring kernel boot arguments..." +# Kernel Arguments Explained: +# - initrd=initrd.img: Specifies the initial ramdisk to use. +# - inst.stage2: Points to the OS source. For a live boot, the base URL is sufficient. +# - inst.ks: CRITICAL: Points to our Kickstart file for automation. +# - ip=dhcp: Ensures the live environment configures its network. +# - console=...: Provides boot output on both serial and graphical consoles for debugging. +imgargs vmlinuz initrd=initrd.img inst.sshd inst.stage2=${os_base_url} inst.ks=${ks_url} ip=dhcp console=ttyS0,115200 console=tty1 + +echo "Booting into CentOS Stream 9 live environment..." +boot || goto failed + +:failed +echo "Boot failed. Dropping to iPXE shell." +shell diff --git a/harmony/templates/inventory.kickstart.j2 b/harmony/templates/inventory.kickstart.j2 new file mode 100644 index 0000000..8f8b36c --- /dev/null +++ b/harmony/templates/inventory.kickstart.j2 @@ -0,0 +1,129 @@ +# --- Pre-Boot Scripting (The Main Goal) --- +# This section runs after the live environment has booted into RAM. +# It sets up SSH and downloads/runs the harmony-inventory-agent. +%pre --log=/root/ks-pre.log + +echo "Harmony Kickstart: Pre-boot script started." + +# 1. Configure SSH Access for Root +# Create the .ssh directory and set correct permissions. +echo " - Setting up SSH authorized_keys for root..." +mkdir -p /root/.ssh +chmod 700 /root/.ssh + +# Download the public key from the provisioning server. +# The -sS flags make curl silent but show errors. -L follows redirects. +curl -vSL "http://{{ gateway_ip }}:8080/{{ cluster_pubkey_filename }}" -o /root/.ssh/authorized_keys +if [ $? -ne 0 ]; then + echo " - ERROR: Failed to download SSH public key." +else + echo " - SSH key downloaded successfully." + chmod 600 /root/.ssh/authorized_keys +fi + +# 2. Download the Harmony Inventory Agent +echo " - Downloading harmony-inventory-agent..." +curl -vSL "http://{{ gateway_ip }}:8080/{{ harmony_inventory_agent }}" -o /usr/bin/harmony-inventory-agent +if [ $? -ne 0 ]; then + echo " - ERROR: Failed to download harmony_inventory_agent." +else + echo " - Agent binary downloaded successfully." + chmod +x /usr/bin/harmony-inventory-agent +fi + +# 3. Create a systemd service to run the agent persistently. +# This is the most robust method to ensure the agent stays running. +echo " - Creating systemd service for the agent..." +cat > /etc/systemd/system/harmony-agent.service << EOF +[Unit] +Description=Harmony Inventory Agent +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +ExecStart=/usr/bin/harmony-inventory-agent +Restart=on-failure +RestartSec=5 +Environment="RUST_LOG=info" + +[Install] +WantedBy=multi-user.target + +EOF + +# 4. Enable and start the service +# The 'systemctl' commands will work correctly within the chroot environment of the %pre script. +echo " - Enabling and starting harmony-agent.service..." +systemctl daemon-reload +systemctl enable --now harmony-agent.service + +# Check if the service started correctly +systemctl is-active --quiet harmony-agent.service +if [ $? -eq 0 ]; then + echo " - Harmony Inventory Agent service is now running." +else + echo " - ERROR: Harmony Inventory Agent service failed to start." +fi + +echo "Harmony Kickstart: Pre-boot script finished. The machine is ready for inventory." + +echo "Running cat - to pause system indefinitely" +cat - + +%end + +# ================================================================= +# Harmony Discovery Agent - Kickstart File (NON-INSTALL, LIVE BOOT) +# ================================================================= +# +# This file achieves a fully automated, non-interactive boot into a +# live CentOS environment. It does NOT install to disk. +# + +# --- Automation and Interaction Control --- +# Perform the installation in command-line mode. This is critical for +# preventing Anaconda from starting a UI and halting for input. +cmdline + +# Accept the End User License Agreement to prevent a prompt. +eula --agreed + +# --- Core System Configuration (Required by Anaconda) --- +# Set keyboard and language. These are mandatory. +keyboard --vckeymap=us --xlayouts='us' +lang en_US.UTF-8 + +# Configure networking. This is essential for the %post script to work. +# The --activate flag ensures this device is brought up in the installer environment. +network --bootproto=dhcp --device=link --activate + +# Set a locked root password. This is a mandatory command. +rootpw --lock + +# Set the timezone. This is a mandatory command. +timezone UTC + +# --- Disable Installation-Specific Features --- +# CRITICAL: Do not install a bootloader. The --disabled flag prevents +# this step and avoids errors about where to install it. +bootloader --disabled + +# CRITICAL: Ignore all disks. This prevents Anaconda from stopping at the +# "Installation Destination" screen asking where to install. +# ignoredisk --drives /dev/sda + +# Do not run the Initial Setup wizard on first boot. +firstboot --disable + +# --- Package Selection --- +# We are not installing, so this section can be minimal. +# An empty %packages section is valid and ensures no time is wasted +# resolving dependencies for an installation that will not happen. +%packages +%end + + +# IMPORTANT: Do not include a final action command like 'reboot' or 'poweroff'. +# The default action is 'halt', which in cmdline mode will leave the system +# running in the live environment with the agent active, which is the desired state. diff --git a/harmony_cli/src/cli_logger.rs b/harmony_cli/src/cli_logger.rs index c2fc8d0..be61c2a 100644 --- a/harmony_cli/src/cli_logger.rs +++ b/harmony_cli/src/cli_logger.rs @@ -7,19 +7,11 @@ use harmony::{ }; use log::{error, info, log_enabled}; use std::io::Write; -use std::sync::{Arc, Mutex}; +use std::sync::Mutex; -pub fn init() -> tokio::task::JoinHandle<()> { +pub fn init() { configure_logger(); - let handle = tokio::spawn(handle_events()); - - loop { - if instrumentation::instrument(HarmonyEvent::HarmonyStarted).is_ok() { - break; - } - } - - handle + handle_events(); } fn configure_logger() { @@ -86,119 +78,114 @@ fn configure_logger() { .init(); } -async fn handle_events() { - let preparing_topology = Arc::new(Mutex::new(false)); - let current_score: Arc>> = Arc::new(Mutex::new(None)); +fn handle_events() { + let preparing_topology = Mutex::new(false); + let current_score: Mutex> = Mutex::new(None); instrumentation::subscribe("Harmony CLI Logger", { move |event| { - let preparing_topology = Arc::clone(&preparing_topology); - let current_score = Arc::clone(¤t_score); + let mut preparing_topology = preparing_topology.lock().unwrap(); + let mut current_score = current_score.lock().unwrap(); - async move { - let mut preparing_topology = preparing_topology.lock().unwrap(); - let mut current_score = current_score.lock().unwrap(); - - match event { - HarmonyEvent::HarmonyStarted => {} - HarmonyEvent::HarmonyFinished => { - let emoji = crate::theme::EMOJI_HARMONY.to_string(); - info!(emoji = emoji.as_str(); "Harmony completed"); - return false; - } - HarmonyEvent::TopologyStateChanged { - topology, - status, - message, - } => match status { - TopologyStatus::Queued => {} - TopologyStatus::Preparing => { - let emoji = format!("{}", style(crate::theme::EMOJI_TOPOLOGY.to_string()).yellow()); - info!(emoji = emoji.as_str(); "Preparing environment: {topology}..."); - (*preparing_topology) = true; - } - TopologyStatus::Success => { - (*preparing_topology) = false; - if let Some(message) = message { - info!(status = "finished"; "{message}"); - } - } - TopologyStatus::Noop => { - (*preparing_topology) = false; - if let Some(message) = message { - info!(status = "skipped"; "{message}"); - } - } - TopologyStatus::Error => { - (*preparing_topology) = false; - if let Some(message) = message { - error!(status = "failed"; "{message}"); - } - } - }, - HarmonyEvent::InterpretExecutionStarted { - execution_id: _, - topology: _, - interpret: _, - score, - message, - } => { - if *preparing_topology || current_score.is_some() { - info!("{message}"); - } else { - (*current_score) = Some(score.clone()); - let emoji = format!("{}", style(crate::theme::EMOJI_SCORE).blue()); - info!(emoji = emoji.as_str(); "Interpreting score: {score}..."); - } - } - HarmonyEvent::InterpretExecutionFinished { - execution_id: _, - topology: _, - interpret: _, - score, - outcome, - } => { - if current_score.is_some() && current_score.clone().unwrap() == score { - (*current_score) = None; - } - - match outcome { - Ok(outcome) => match outcome.status { - harmony::interpret::InterpretStatus::SUCCESS => { - info!(status = "finished"; "{}", outcome.message); - } - harmony::interpret::InterpretStatus::NOOP => { - info!(status = "skipped"; "{}", outcome.message); - } - _ => { - error!(status = "failed"; "{}", outcome.message); - } - }, - Err(err) => { - error!(status = "failed"; "{}", err); - } - } - } - HarmonyEvent::ApplicationFeatureStateChanged { - topology: _, - application, - feature, - status, - } => match status { - ApplicationFeatureStatus::Installing => { - info!("Installing feature '{}' for '{}'...", feature, application); - } - ApplicationFeatureStatus::Installed => { - info!(status = "finished"; "Feature '{}' installed", feature); - } - ApplicationFeatureStatus::Failed { details } => { - error!(status = "failed"; "Feature '{}' installation failed: {}", feature, details); - } - }, + match event { + HarmonyEvent::HarmonyStarted => {} + HarmonyEvent::HarmonyFinished => { + let emoji = crate::theme::EMOJI_HARMONY.to_string(); + info!(emoji = emoji.as_str(); "Harmony completed"); } - true + HarmonyEvent::TopologyStateChanged { + topology, + status, + message, + } => match status { + TopologyStatus::Queued => {} + TopologyStatus::Preparing => { + let emoji = format!( + "{}", + style(crate::theme::EMOJI_TOPOLOGY.to_string()).yellow() + ); + info!(emoji = emoji.as_str(); "Preparing environment: {topology}..."); + (*preparing_topology) = true; + } + TopologyStatus::Success => { + (*preparing_topology) = false; + if let Some(message) = message { + info!(status = "finished"; "{message}"); + } + } + TopologyStatus::Noop => { + (*preparing_topology) = false; + if let Some(message) = message { + info!(status = "skipped"; "{message}"); + } + } + TopologyStatus::Error => { + (*preparing_topology) = false; + if let Some(message) = message { + error!(status = "failed"; "{message}"); + } + } + }, + HarmonyEvent::InterpretExecutionStarted { + execution_id: _, + topology: _, + interpret: _, + score, + message, + } => { + if *preparing_topology || current_score.is_some() { + info!("{message}"); + } else { + (*current_score) = Some(score.clone()); + let emoji = format!("{}", style(crate::theme::EMOJI_SCORE).blue()); + info!(emoji = emoji.as_str(); "Interpreting score: {score}..."); + } + } + HarmonyEvent::InterpretExecutionFinished { + execution_id: _, + topology: _, + interpret: _, + score, + outcome, + } => { + if current_score.is_some() && ¤t_score.clone().unwrap() == score { + (*current_score) = None; + } + + match outcome { + Ok(outcome) => match outcome.status { + harmony::interpret::InterpretStatus::SUCCESS => { + info!(status = "finished"; "{}", outcome.message); + } + harmony::interpret::InterpretStatus::NOOP => { + info!(status = "skipped"; "{}", outcome.message); + } + _ => { + error!(status = "failed"; "{}", outcome.message); + } + }, + Err(err) => { + error!(status = "failed"; "{err}"); + } + } + } + HarmonyEvent::ApplicationFeatureStateChanged { + topology: _, + application, + feature, + status, + } => match status { + ApplicationFeatureStatus::Installing => { + info!("Installing feature '{feature}' for '{application}'..."); + } + ApplicationFeatureStatus::Installed => { + info!(status = "finished"; "Feature '{feature}' installed"); + } + ApplicationFeatureStatus::Failed { details } => { + error!(status = "failed"; "Feature '{feature}' installation failed: {details}"); + } + }, } } - }) - .await; + }); } diff --git a/harmony_cli/src/lib.rs b/harmony_cli/src/lib.rs index 53de86e..0bfb1e7 100644 --- a/harmony_cli/src/lib.rs +++ b/harmony_cli/src/lib.rs @@ -115,7 +115,7 @@ pub async fn run_cli( scores: Vec>>, args: Args, ) -> Result<(), Box> { - let cli_logger_handle = cli_logger::init(); + cli_logger::init(); let mut maestro = Maestro::initialize(inventory, topology).await.unwrap(); maestro.register_all(scores); @@ -123,7 +123,6 @@ pub async fn run_cli( let result = init(maestro, args).await; instrumentation::instrument(instrumentation::HarmonyEvent::HarmonyFinished).unwrap(); - let _ = tokio::try_join!(cli_logger_handle); result } diff --git a/harmony_composer/src/harmony_composer_logger.rs b/harmony_composer/src/harmony_composer_logger.rs index 5e0261f..040a167 100644 --- a/harmony_composer/src/harmony_composer_logger.rs +++ b/harmony_composer/src/harmony_composer_logger.rs @@ -1,82 +1,66 @@ use harmony_cli::progress::{IndicatifProgressTracker, ProgressTracker}; use indicatif::MultiProgress; -use std::sync::Arc; use crate::instrumentation::{self, HarmonyComposerEvent}; -pub fn init() -> tokio::task::JoinHandle<()> { +pub fn init() { configure_logger(); - let handle = tokio::spawn(handle_events()); - - loop { - if instrumentation::instrument(HarmonyComposerEvent::HarmonyComposerStarted).is_ok() { - break; - } - } - - handle + handle_events(); } fn configure_logger() { env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).build(); } -pub async fn handle_events() { - let progress_tracker = Arc::new(IndicatifProgressTracker::new(MultiProgress::new())); +pub fn handle_events() { + let progress_tracker = IndicatifProgressTracker::new(MultiProgress::new()); const SETUP_SECTION: &str = "project-initialization"; const COMPILTATION_TASK: &str = "compilation"; const PROGRESS_DEPLOYMENT: &str = "deployment"; instrumentation::subscribe("Harmony Composer Logger", { - move |event| { - let progress_tracker = Arc::clone(&progress_tracker); - - async move { - match event { - HarmonyComposerEvent::HarmonyComposerStarted => {} - HarmonyComposerEvent::ProjectInitializationStarted => { - progress_tracker.add_section( - SETUP_SECTION, - &format!( - "{} Initializing Harmony project...", - harmony_cli::theme::EMOJI_HARMONY, - ), - ); - } - HarmonyComposerEvent::ProjectInitialized => {} - HarmonyComposerEvent::ProjectCompilationStarted { details } => { - progress_tracker.add_task(SETUP_SECTION, COMPILTATION_TASK, &details); - } - HarmonyComposerEvent::ProjectCompiled => { - progress_tracker.finish_task(COMPILTATION_TASK, "project compiled"); - } - HarmonyComposerEvent::ProjectCompilationFailed { details } => { - progress_tracker.fail_task(COMPILTATION_TASK, &format!("failed to compile project:\n{details}")); - } - HarmonyComposerEvent::DeploymentStarted { target, profile } => { - progress_tracker.add_section( - PROGRESS_DEPLOYMENT, - &format!( - "\n{} Deploying project on target '{target}' with profile '{profile}'...\n", - harmony_cli::theme::EMOJI_DEPLOY, - ), - ); - } - HarmonyComposerEvent::DeploymentCompleted => { - progress_tracker.clear(); - } - HarmonyComposerEvent::DeploymentFailed { details } => { - progress_tracker.add_task(PROGRESS_DEPLOYMENT, "deployment-failed", ""); - progress_tracker.fail_task("deployment-failed", &details); - }, - HarmonyComposerEvent::Shutdown => { - return false; - } - } - true + move |event| match event { + HarmonyComposerEvent::HarmonyComposerStarted => {} + HarmonyComposerEvent::ProjectInitializationStarted => { + progress_tracker.add_section( + SETUP_SECTION, + &format!( + "{} Initializing Harmony project...", + harmony_cli::theme::EMOJI_HARMONY, + ), + ); } + HarmonyComposerEvent::ProjectInitialized => {} + HarmonyComposerEvent::ProjectCompilationStarted { details } => { + progress_tracker.add_task(SETUP_SECTION, COMPILTATION_TASK, details); + } + HarmonyComposerEvent::ProjectCompiled => { + progress_tracker.finish_task(COMPILTATION_TASK, "project compiled"); + } + HarmonyComposerEvent::ProjectCompilationFailed { details } => { + progress_tracker.fail_task( + COMPILTATION_TASK, + &format!("failed to compile project:\n{details}"), + ); + } + HarmonyComposerEvent::DeploymentStarted { target, profile } => { + progress_tracker.add_section( + PROGRESS_DEPLOYMENT, + &format!( + "\n{} Deploying project on target '{target}' with profile '{profile}'...\n", + harmony_cli::theme::EMOJI_DEPLOY, + ), + ); + } + HarmonyComposerEvent::DeploymentCompleted => { + progress_tracker.clear(); + } + HarmonyComposerEvent::DeploymentFailed { details } => { + progress_tracker.add_task(PROGRESS_DEPLOYMENT, "deployment-failed", ""); + progress_tracker.fail_task("deployment-failed", details); + } + HarmonyComposerEvent::Shutdown => {} } }) - .await } diff --git a/harmony_composer/src/instrumentation.rs b/harmony_composer/src/instrumentation.rs index 6f2fa01..b9164b7 100644 --- a/harmony_composer/src/instrumentation.rs +++ b/harmony_composer/src/instrumentation.rs @@ -1,6 +1,5 @@ -use log::debug; use once_cell::sync::Lazy; -use tokio::sync::broadcast; +use std::{collections::HashMap, sync::Mutex}; use crate::{HarmonyProfile, HarmonyTarget}; @@ -27,48 +26,43 @@ pub enum HarmonyComposerEvent { Shutdown, } -static HARMONY_COMPOSER_EVENT_BUS: Lazy> = - Lazy::new(|| { - // TODO: Adjust channel capacity - let (tx, _rx) = broadcast::channel(16); - tx - }); +type Subscriber = Box; -pub fn instrument(event: HarmonyComposerEvent) -> Result<(), &'static str> { - #[cfg(not(test))] - { - match HARMONY_COMPOSER_EVENT_BUS.send(event) { - Ok(_) => Ok(()), - Err(_) => Err("send error: no subscribers"), - } - } +static SUBSCRIBERS: Lazy>> = + Lazy::new(|| Mutex::new(HashMap::new())); - #[cfg(test)] - { - let _ = event; // Suppress the "unused variable" warning for `event` - Ok(()) - } -} - -pub async fn subscribe(name: &str, mut handler: F) +/// Subscribes a listener to all instrumentation events. +/// +/// Simply provide a unique name and a closure to run when an event happens. +/// +/// # Example +/// ``` +/// instrumentation::subscribe("my_logger", |event| { +/// println!("Event occurred: {:?}", event); +/// }); +/// ``` +pub fn subscribe(name: &str, callback: F) where - F: FnMut(HarmonyComposerEvent) -> Fut + Send + 'static, - Fut: Future + Send, + F: Fn(&HarmonyComposerEvent) + Send + Sync + 'static, { - let mut rx = HARMONY_COMPOSER_EVENT_BUS.subscribe(); - debug!("[{name}] Service started. Listening for events..."); - loop { - match rx.recv().await { - Ok(event) => { - if !handler(event).await { - debug!("[{name}] Handler requested exit."); - break; - } - } - Err(broadcast::error::RecvError::Lagged(n)) => { - debug!("[{name}] Lagged behind by {n} messages."); - } - Err(_) => break, - } - } + let mut subs = SUBSCRIBERS.lock().unwrap(); + subs.insert(name.to_string(), Box::new(callback)); +} + +/// Instruments an event, notifying all subscribers. +/// +/// This will call every closure that was registered with `subscribe`. +/// +/// # Example +/// ``` +/// instrumentation::instrument(HarmonyEvent::HarmonyStarted); +/// ``` +pub fn instrument(event: HarmonyComposerEvent) -> Result<(), &'static str> { + let subs = SUBSCRIBERS.lock().unwrap(); + + for callback in subs.values() { + callback(&event); + } + + Ok(()) } diff --git a/harmony_composer/src/main.rs b/harmony_composer/src/main.rs index f0a8513..4119460 100644 --- a/harmony_composer/src/main.rs +++ b/harmony_composer/src/main.rs @@ -99,7 +99,7 @@ impl std::fmt::Display for HarmonyProfile { #[tokio::main] async fn main() { - let hc_logger_handle = harmony_composer_logger::init(); + harmony_composer_logger::init(); let cli_args = GlobalArgs::parse(); let harmony_path = Path::new(&cli_args.harmony_path) @@ -199,8 +199,6 @@ async fn main() { } instrumentation::instrument(HarmonyComposerEvent::Shutdown).unwrap(); - - let _ = tokio::try_join!(hc_logger_handle); } #[derive(Clone, Debug, clap::ValueEnum)] diff --git a/harmony_inventory_agent/Cargo.toml b/harmony_inventory_agent/Cargo.toml index 3b1be2c..9ffe37e 100644 --- a/harmony_inventory_agent/Cargo.toml +++ b/harmony_inventory_agent/Cargo.toml @@ -10,3 +10,8 @@ serde.workspace = true serde_json.workspace = true log.workspace = true env_logger.workspace = true +tokio.workspace = true +thiserror.workspace = true +# mdns-sd = "0.14.1" +mdns-sd = { git = "https://github.com/jggc/mdns-sd.git", branch = "patch-1" } +local-ip-address = "0.6.5" diff --git a/harmony_inventory_agent/README.md b/harmony_inventory_agent/README.md new file mode 100644 index 0000000..57feb70 --- /dev/null +++ b/harmony_inventory_agent/README.md @@ -0,0 +1,22 @@ +## Compiling : + +```bash +cargo build -p harmony_inventory_agent --release --target x86_64-unknown-linux-musl +``` + +This will create a statically linked binary that can run on pretty much any x86_64 system. + +This requires installation of the target + +``` +rustup target add x86_64-unknown-linux-musl +``` + +And installation of the musl tools too. + +On Archlinux, they can be installed with : + +``` +pacman -S musl +``` + diff --git a/harmony_inventory_agent/src/hwinfo.rs b/harmony_inventory_agent/src/hwinfo.rs index 21045cc..d381a14 100644 --- a/harmony_inventory_agent/src/hwinfo.rs +++ b/harmony_inventory_agent/src/hwinfo.rs @@ -1,4 +1,4 @@ -use log::debug; +use log::{debug, warn}; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::fs; @@ -104,48 +104,58 @@ impl PhysicalHost { fn all_tools_available() -> Result<(), String> { let required_tools = [ - ("lsblk", "--version"), - ("lspci", "--version"), - ("lsmod", "--version"), - ("dmidecode", "--version"), - ("smartctl", "--version"), - ("ip", "route"), // No version flag available + ("lsblk", Some("--version")), + ("lspci", Some("--version")), + ("lsmod", None), + ("dmidecode", Some("--version")), + ("smartctl", Some("--version")), + ("ip", Some("route")), // No version flag available ]; let mut missing_tools = Vec::new(); + debug!("Looking for required_tools {required_tools:?}"); for (tool, tool_arg) in required_tools.iter() { // First check if tool exists in PATH using which(1) - let exists = if let Ok(output) = Command::new("which").arg(tool).output() { + let mut exists = if let Ok(output) = Command::new("which").arg(tool).output() { output.status.success() } else { - // Fallback: manual PATH search if which(1) is unavailable - if let Ok(path_var) = std::env::var("PATH") { - path_var.split(':').any(|dir| { - let tool_path = std::path::Path::new(dir).join(tool); - tool_path.exists() && Self::is_executable(&tool_path) - }) - } else { - false - } + false }; if !exists { + // Fallback: manual PATH search if which(1) is unavailable + debug!("Looking for {tool} in path"); + if let Ok(path_var) = std::env::var("PATH") { + debug!("PATH is {path_var}"); + exists = path_var.split(':').any(|dir| { + let tool_path = std::path::Path::new(dir).join(tool); + tool_path.exists() && Self::is_executable(&tool_path) + }) + } + } + + if !exists { + warn!("Unable to find tool {tool} from PATH"); missing_tools.push(*tool); continue; } // Verify tool is functional by checking version/help output let mut cmd = Command::new(tool); - cmd.arg(tool_arg); + if let Some(tool_arg) = tool_arg { + cmd.arg(tool_arg); + } cmd.stdout(std::process::Stdio::null()); cmd.stderr(std::process::Stdio::null()); if let Ok(status) = cmd.status() { if !status.success() { + warn!("Unable to test {tool} status failed"); missing_tools.push(*tool); } } else { + warn!("Unable to test {tool}"); missing_tools.push(*tool); } } @@ -167,6 +177,7 @@ impl PhysicalHost { #[cfg(unix)] fn is_executable(path: &std::path::Path) -> bool { + debug!("Checking if {} is executable", path.to_string_lossy()); use std::os::unix::fs::PermissionsExt; match std::fs::metadata(path) { @@ -285,11 +296,11 @@ impl PhysicalHost { if device_path.exists() { if drive.model.is_empty() { drive.model = Self::read_sysfs_string(&device_path.join("device/model")) - .map_err(|e| format!("Failed to read model for {}: {}", name, e))?; + .unwrap_or(format!("Failed to read model for {}", name)); } if drive.serial.is_empty() { drive.serial = Self::read_sysfs_string(&device_path.join("device/serial")) - .map_err(|e| format!("Failed to read serial for {}: {}", name, e))?; + .unwrap_or(format!("Failed to read serial for {}", name)); } } @@ -655,6 +666,10 @@ impl PhysicalHost { Ok("IDE".to_string()) } else if device_name.starts_with("vd") { Ok("VirtIO".to_string()) + } else if device_name.starts_with("sr") { + Ok("CDROM".to_string()) + } else if device_name.starts_with("zram") { + Ok("Ramdisk".to_string()) } else { // Try to determine from device path let subsystem = Self::read_sysfs_string(&device_path.join("device/subsystem"))?; diff --git a/harmony_inventory_agent/src/lib.rs b/harmony_inventory_agent/src/lib.rs new file mode 100644 index 0000000..cbf208b --- /dev/null +++ b/harmony_inventory_agent/src/lib.rs @@ -0,0 +1,2 @@ +mod hwinfo; +pub mod local_presence; diff --git a/harmony_inventory_agent/src/local_presence/advertise.rs b/harmony_inventory_agent/src/local_presence/advertise.rs new file mode 100644 index 0000000..3ccb4f6 --- /dev/null +++ b/harmony_inventory_agent/src/local_presence/advertise.rs @@ -0,0 +1,90 @@ +use log::{debug, error, info, warn}; +use mdns_sd::{ServiceDaemon, ServiceInfo}; +use std::collections::HashMap; + +use crate::{ + hwinfo::PhysicalHost, + local_presence::{PresenceError, SERVICE_NAME, VERSION}, +}; + +/// Advertises the agent's presence on the local network. +/// +/// This function is synchronous and non-blocking. It spawns a background Tokio task +/// to handle the mDNS advertisement for the lifetime of the application. +pub fn advertise(service_port: u16) -> Result<(), PresenceError> { + let host_id = match PhysicalHost::gather() { + Ok(host) => Some(host.host_uuid), + Err(e) => { + error!("Could not build physical host, harmony presence id will be unavailable : {e}"); + None + } + }; + + let instance_name = format!( + "inventory-agent-{}", + host_id.clone().unwrap_or("unknown".to_string()) + ); + + let spawned_msg = format!("Spawned local presence advertisement task for '{instance_name}'."); + + tokio::spawn(async move { + info!( + "Local presence task started. Advertising as '{}'.", + instance_name + ); + + // The ServiceDaemon must live for the entire duration of the advertisement. + // If it's dropped, the advertisement stops. + let mdns = match ServiceDaemon::new() { + Ok(daemon) => daemon, + Err(e) => { + warn!("Failed to create mDNS daemon: {}. Task shutting down.", e); + return; + } + }; + + let mut props = HashMap::new(); + if let Some(host_id) = host_id { + props.insert("id".to_string(), host_id); + } + props.insert("version".to_string(), VERSION.to_string()); + + let local_ip: Box = match local_ip_address::local_ip() { + Ok(ip) => Box::new(ip), + Err(e) => { + error!( + "Could not figure out local ip, mdns will have to try to figure it out by itself : {e}" + ); + Box::new(()) + } + }; + + debug!("Using local ip {:?}", local_ip.as_ip_addrs()); + + let service_info = ServiceInfo::new( + SERVICE_NAME, + &instance_name, + &format!("{}.local.", instance_name), + local_ip, + service_port, + Some(props), + ) + .expect("ServiceInfo creation should not fail with valid inputs"); + + // The registration handle must also be kept alive. + let _registration_handle = match mdns.register(service_info) { + Ok(handle) => { + info!("Service successfully registered on the local network."); + handle + } + Err(e) => { + warn!("Failed to register service: {}. Task shutting down.", e); + return; + } + }; + }); + + info!("{spawned_msg}"); + + Ok(()) +} diff --git a/harmony_inventory_agent/src/local_presence/discover.rs b/harmony_inventory_agent/src/local_presence/discover.rs new file mode 100644 index 0000000..fd7d2b3 --- /dev/null +++ b/harmony_inventory_agent/src/local_presence/discover.rs @@ -0,0 +1,34 @@ +use mdns_sd::{ServiceDaemon, ServiceEvent}; + +use crate::local_presence::SERVICE_NAME; + +pub type DiscoveryEvent = ServiceEvent; + +pub fn discover_agents(timeout: Option, on_event: fn(&DiscoveryEvent)) { + // Create a new mDNS daemon. + let mdns = ServiceDaemon::new().expect("Failed to create mDNS daemon"); + + // Start browsing for the service type. + // The receiver will be a stream of events. + let receiver = mdns.browse(SERVICE_NAME).expect("Failed to browse"); + + std::thread::spawn(move || { + while let Ok(event) = receiver.recv() { + on_event(&event); + match event { + ServiceEvent::ServiceResolved(resolved) => { + println!("Resolved a new service: {}", resolved.fullname); + } + other_event => { + println!("Received other event: {:?}", &other_event); + } + } + } + }); + + if let Some(timeout) = timeout { + // Gracefully shutdown the daemon. + std::thread::sleep(std::time::Duration::from_secs(timeout)); + mdns.shutdown().unwrap(); + } +} diff --git a/harmony_inventory_agent/src/local_presence/mod.rs b/harmony_inventory_agent/src/local_presence/mod.rs new file mode 100644 index 0000000..d501934 --- /dev/null +++ b/harmony_inventory_agent/src/local_presence/mod.rs @@ -0,0 +1,16 @@ +mod discover; +pub use discover::*; +mod advertise; +pub use advertise::*; + +pub const SERVICE_NAME: &str = "_harmony._tcp.local."; +const VERSION: &str = env!("CARGO_PKG_VERSION"); + +// A specific error type for our module enhances clarity and usability. +#[derive(thiserror::Error, Debug)] +pub enum PresenceError { + #[error("Failed to create mDNS daemon")] + DaemonCreationFailed(#[from] mdns_sd::Error), + #[error("The shutdown signal has already been sent")] + ShutdownFailed, +} diff --git a/harmony_inventory_agent/src/main.rs b/harmony_inventory_agent/src/main.rs index 8784b00..d93ea4f 100644 --- a/harmony_inventory_agent/src/main.rs +++ b/harmony_inventory_agent/src/main.rs @@ -1,9 +1,12 @@ // src/main.rs use actix_web::{App, HttpServer, Responder, get}; -use hwinfo::PhysicalHost; +use log::error; use std::env; +use crate::hwinfo::PhysicalHost; + mod hwinfo; +mod local_presence; #[get("/inventory")] async fn inventory() -> impl Responder { @@ -26,10 +29,17 @@ async fn main() -> std::io::Result<()> { env_logger::init(); let port = env::var("HARMONY_INVENTORY_AGENT_PORT").unwrap_or_else(|_| "8080".to_string()); + let port = port + .parse::() + .expect(&format!("Invalid port number, cannot parse to u16 {port}")); let bind_addr = format!("0.0.0.0:{}", port); log::info!("Starting inventory agent on {}", bind_addr); + if let Err(e) = local_presence::advertise(port) { + error!("Could not start advertise local presence : {e}"); + } + HttpServer::new(|| App::new().service(inventory)) .bind(&bind_addr)? .run() diff --git a/harmony_secret/Cargo.toml b/harmony_secret/Cargo.toml index 48c8f5c..88f93ac 100644 --- a/harmony_secret/Cargo.toml +++ b/harmony_secret/Cargo.toml @@ -1,19 +1,20 @@ [package] -name = "harmony-secret" +name = "harmony_secret" edition = "2024" version.workspace = true readme.workspace = true license.workspace = true [dependencies] -harmony-secret-derive = { version = "0.1.0", path = "../harmony_secret_derive" } +harmony_secret_derive = { version = "0.1.0", path = "../harmony_secret_derive" } serde = { version = "1.0.209", features = ["derive", "rc"] } serde_json = "1.0.127" thiserror.workspace = true lazy_static.workspace = true directories.workspace = true log.workspace = true -infisical = "0.0.2" +# infisical = "0.0.2" +infisical = { git = "https://github.com/jggc/rust-sdk.git", branch = "patch-1" } tokio.workspace = true async-trait.workspace = true http.workspace = true diff --git a/harmony_secret/src/lib.rs b/harmony_secret/src/lib.rs index a12fb8e..4f54d95 100644 --- a/harmony_secret/src/lib.rs +++ b/harmony_secret/src/lib.rs @@ -126,6 +126,7 @@ impl SecretManager { #[cfg(test)] mod test { use super::*; + #[cfg(secrete2etest)] use pretty_assertions::assert_eq; use serde::{Deserialize, Serialize}; diff --git a/harmony_secret/src/store/local_file.rs b/harmony_secret/src/store/local_file.rs index 84334fa..ed81c65 100644 --- a/harmony_secret/src/store/local_file.rs +++ b/harmony_secret/src/store/local_file.rs @@ -70,7 +70,6 @@ mod tests { #[tokio::test] async fn test_set_and_get_raw_successfully() { let dir = tempdir().unwrap(); - let store = LocalFileSecretStore::default(); let ns = "test-ns"; let key = "test-key"; let value = b"{\"data\":\"test-value\"}"; diff --git a/harmony_secret_derive/Cargo.toml b/harmony_secret_derive/Cargo.toml index 5d24b72..7142c27 100644 --- a/harmony_secret_derive/Cargo.toml +++ b/harmony_secret_derive/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "harmony-secret-derive" +name = "harmony_secret_derive" version = "0.1.0" edition = "2024" diff --git a/harmony_secret_derive/src/lib.rs b/harmony_secret_derive/src/lib.rs index 8aa83df..71822af 100644 --- a/harmony_secret_derive/src/lib.rs +++ b/harmony_secret_derive/src/lib.rs @@ -10,10 +10,10 @@ pub fn derive_secret(input: TokenStream) -> TokenStream { // The key for the secret will be the stringified name of the struct itself. // e.g., `struct OKDClusterSecret` becomes key `"OKDClusterSecret"`. - let key = struct_ident.to_string(); + let key = struct_ident.to_string(); // TODO: Utiliser path complet de la struct // Find the path to the `harmony_secret` crate. - let secret_crate_path = match crate_name("harmony-secret") { + let secret_crate_path = match crate_name("harmony_secret") { Ok(FoundCrate::Itself) => quote!(crate), Ok(FoundCrate::Name(name)) => { let ident = Ident::new(&name, proc_macro2::Span::call_site()); diff --git a/harmony_tui/src/lib.rs b/harmony_tui/src/lib.rs index 4fb4591..35b5dea 100644 --- a/harmony_tui/src/lib.rs +++ b/harmony_tui/src/lib.rs @@ -94,13 +94,9 @@ async fn init_instrumentation() -> tokio::task::JoinHandle<()> { } async fn handle_harmony_events() { - instrumentation::subscribe("Harmony TUI Logger", async |event| { - if let HarmonyEvent::HarmonyFinished = event { - return false; - }; - true - }) - .await; + instrumentation::subscribe("Harmony TUI Logger", |_| { + // TODO: Display events in the TUI + }); } pub struct HarmonyTUI { diff --git a/k3d/Cargo.toml b/k3d/Cargo.toml index aaa15ce..23f1a4d 100644 --- a/k3d/Cargo.toml +++ b/k3d/Cargo.toml @@ -11,7 +11,7 @@ async-trait = { workspace = true } tokio = { workspace = true } octocrab = "0.44.0" regex = "1.11.1" -reqwest = { version = "0.12", features = ["stream"] } +reqwest = { version = "0.12", features = ["stream", "rustls-tls", "http2"], default-features = false } url.workspace = true sha2 = "0.10.8" futures-util = "0.3.31" diff --git a/opnsense-config-xml/src/data/caddy.rs b/opnsense-config-xml/src/data/caddy.rs index b4ca0fc..80088b9 100644 --- a/opnsense-config-xml/src/data/caddy.rs +++ b/opnsense-config-xml/src/data/caddy.rs @@ -30,15 +30,15 @@ pub struct CaddyGeneral { #[yaserde(rename = "TlsDnsApiKey")] pub tls_dns_api_key: MaybeString, #[yaserde(rename = "TlsDnsSecretApiKey")] - pub tls_dns_secret_api_key: MaybeString, + pub tls_dns_secret_api_key: Option, #[yaserde(rename = "TlsDnsOptionalField1")] - pub tls_dns_optional_field1: MaybeString, + pub tls_dns_optional_field1: Option, #[yaserde(rename = "TlsDnsOptionalField2")] - pub tls_dns_optional_field2: MaybeString, + pub tls_dns_optional_field2: Option, #[yaserde(rename = "TlsDnsOptionalField3")] - pub tls_dns_optional_field3: MaybeString, + pub tls_dns_optional_field3: Option, #[yaserde(rename = "TlsDnsOptionalField4")] - pub tls_dns_optional_field4: MaybeString, + pub tls_dns_optional_field4: Option, #[yaserde(rename = "TlsDnsPropagationTimeout")] pub tls_dns_propagation_timeout: Option, #[yaserde(rename = "TlsDnsPropagationTimeoutPeriod")] @@ -47,6 +47,8 @@ pub struct CaddyGeneral { pub tls_dns_propagation_delay: Option, #[yaserde(rename = "TlsDnsPropagationResolvers")] pub tls_dns_propagation_resolvers: MaybeString, + #[yaserde(rename = "TlsDnsEchDomain")] + pub tls_dns_ech_domain: Option, pub accesslist: MaybeString, #[yaserde(rename = "DisableSuperuser")] pub disable_superuser: Option, @@ -56,6 +58,10 @@ pub struct CaddyGeneral { pub http_version: Option, #[yaserde(rename = "HttpVersions")] pub http_versions: Option, + pub timeout_read_body: Option, + pub timeout_read_header: Option, + pub timeout_write: Option, + pub timeout_idle: Option, #[yaserde(rename = "LogCredentials")] pub log_credentials: MaybeString, #[yaserde(rename = "LogAccessPlain")] diff --git a/opnsense-config-xml/src/data/dnsmasq.rs b/opnsense-config-xml/src/data/dnsmasq.rs new file mode 100644 index 0000000..db2b8c1 --- /dev/null +++ b/opnsense-config-xml/src/data/dnsmasq.rs @@ -0,0 +1,113 @@ +use yaserde::{MaybeString, RawXml}; +use yaserde_derive::{YaDeserialize, YaSerialize}; + +// This is the top-level struct that represents the entire element. +#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] +pub struct DnsMasq { + #[yaserde(attribute = true)] + pub version: String, + #[yaserde(attribute = true)] + pub persisted_at: Option, + + pub enable: u8, + pub regdhcp: u8, + pub regdhcpstatic: u8, + pub dhcpfirst: u8, + pub strict_order: u8, + pub domain_needed: u8, + pub no_private_reverse: u8, + pub no_resolv: Option, + pub log_queries: u8, + pub no_hosts: u8, + pub strictbind: u8, + pub dnssec: u8, + pub regdhcpdomain: MaybeString, + pub interface: Option, + pub port: Option, + pub dns_forward_max: MaybeString, + pub cache_size: MaybeString, + pub local_ttl: MaybeString, + pub add_mac: Option, + pub add_subnet: Option, + pub strip_subnet: Option, + pub no_ident: Option, + pub dhcp: Option, + pub dhcp_ranges: Vec, + pub dhcp_options: Vec, + pub dhcp_boot: Vec, + pub dhcp_tags: Vec, +} + +// Represents the element and its nested fields. +#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] +#[yaserde(rename = "dhcp")] +pub struct Dhcp { + pub no_interface: MaybeString, + pub fqdn: u8, + pub domain: MaybeString, + pub local: Option, + pub lease_max: MaybeString, + pub authoritative: u8, + pub default_fw_rules: u8, + pub reply_delay: MaybeString, + pub enable_ra: u8, + pub nosync: u8, +} + +// Represents a single element. +#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] +#[yaserde(rename = "dhcp_ranges")] +pub struct DhcpRange { + #[yaserde(attribute = true)] + pub uuid: Option, + pub interface: Option, + pub set_tag: Option, + pub start_addr: Option, + pub end_addr: Option, + pub subnet_mask: Option, + pub constructor: Option, + pub mode: Option, + pub prefix_len: Option, + pub lease_time: Option, + pub domain_type: Option, + pub domain: Option, + pub nosync: Option, + pub ra_mode: Option, + pub ra_priority: Option, + pub ra_mtu: Option, + pub ra_interval: Option, + pub ra_router_lifetime: Option, + pub description: Option, +} + +// Represents a single element. +#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] +#[yaserde(rename = "dhcp_boot")] +pub struct DhcpBoot { + #[yaserde(attribute = true)] + pub uuid: String, + pub interface: MaybeString, + pub tag: MaybeString, + pub filename: Option, + pub servername: String, + pub address: String, + pub description: Option, +} + +// Represents a single element. +#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] +#[yaserde(rename = "dhcp_options")] +pub struct DhcpOptions { + #[yaserde(attribute = true)] + pub uuid: String, + #[yaserde(rename = "type")] + pub _type: String, + pub option: MaybeString, + pub option6: MaybeString, + pub interface: MaybeString, + pub tag: MaybeString, + pub set_tag: MaybeString, + pub value: String, + pub force: Option, + pub description: MaybeString, +} diff --git a/opnsense-config-xml/src/data/interfaces.rs b/opnsense-config-xml/src/data/interfaces.rs index e0a84d3..b06f392 100644 --- a/opnsense-config-xml/src/data/interfaces.rs +++ b/opnsense-config-xml/src/data/interfaces.rs @@ -8,10 +8,12 @@ pub struct Interface { #[yaserde(rename = "if")] pub physical_interface_name: String, pub descr: Option, + pub mtu: Option, pub enable: MaybeString, pub lock: Option, #[yaserde(rename = "spoofmac")] pub spoof_mac: Option, + pub mss: Option, pub ipaddr: Option, pub dhcphostname: Option, #[yaserde(rename = "alias-address")] diff --git a/opnsense-config-xml/src/data/mod.rs b/opnsense-config-xml/src/data/mod.rs index 68cee6b..712f878 100644 --- a/opnsense-config-xml/src/data/mod.rs +++ b/opnsense-config-xml/src/data/mod.rs @@ -1,5 +1,6 @@ mod caddy; mod dhcpd; +pub mod dnsmasq; mod haproxy; mod interfaces; mod opnsense; diff --git a/opnsense-config-xml/src/data/opnsense.rs b/opnsense-config-xml/src/data/opnsense.rs index 8ffe3e2..c39f1c5 100644 --- a/opnsense-config-xml/src/data/opnsense.rs +++ b/opnsense-config-xml/src/data/opnsense.rs @@ -1,3 +1,4 @@ +use crate::dnsmasq::DnsMasq; use crate::HAProxy; use crate::{data::dhcpd::DhcpInterface, xml_utils::to_xml_str}; use log::error; @@ -22,7 +23,7 @@ pub struct OPNsense { pub load_balancer: Option, pub rrd: Option, pub ntpd: Ntpd, - pub widgets: Widgets, + pub widgets: Option, pub revision: Revision, #[yaserde(rename = "OPNsense")] pub opnsense: OPNsenseXmlSection, @@ -45,7 +46,7 @@ pub struct OPNsense { #[yaserde(rename = "Pischem")] pub pischem: Option, pub ifgroups: Ifgroups, - pub dnsmasq: Option, + pub dnsmasq: Option, } impl From for OPNsense { @@ -165,9 +166,9 @@ pub struct Sysctl { #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] pub struct SysctlItem { - pub descr: MaybeString, - pub tunable: String, - pub value: MaybeString, + pub descr: Option, + pub tunable: Option, + pub value: Option, } #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] @@ -182,8 +183,8 @@ pub struct System { pub domain: String, pub group: Vec, pub user: Vec, - pub nextuid: u32, - pub nextgid: u32, + pub nextuid: Option, + pub nextgid: Option, pub timezone: String, pub timeservers: String, pub webgui: WebGui, @@ -242,6 +243,7 @@ pub struct Ssh { pub passwordauth: u8, pub keysig: MaybeString, pub permitrootlogin: u8, + pub rekeylimit: Option, } #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] @@ -271,6 +273,7 @@ pub struct Group { pub member: Vec, #[yaserde(rename = "priv")] pub priv_field: String, + pub source_networks: Option, } #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] @@ -1506,7 +1509,7 @@ pub struct Vlans { #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] pub struct Bridges { - pub bridged: MaybeString, + pub bridged: Option, } #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] diff --git a/opnsense-config/Cargo.toml b/opnsense-config/Cargo.toml index 938414e..e70bc12 100644 --- a/opnsense-config/Cargo.toml +++ b/opnsense-config/Cargo.toml @@ -20,6 +20,7 @@ russh-sftp = "2.0.6" serde_json = "1.0.133" tokio-util = { version = "0.7.13", features = ["codec"] } tokio-stream = "0.1.17" +uuid.workspace = true [dev-dependencies] pretty_assertions.workspace = true diff --git a/opnsense-config/src/config/config.rs b/opnsense-config/src/config/config.rs index f99159a..56cd503 100644 --- a/opnsense-config/src/config/config.rs +++ b/opnsense-config/src/config/config.rs @@ -4,8 +4,8 @@ use crate::{ config::{SshConfigManager, SshCredentials, SshOPNSenseShell}, error::Error, modules::{ - caddy::CaddyConfig, dhcp::DhcpConfig, dns::DnsConfig, load_balancer::LoadBalancerConfig, - tftp::TftpConfig, + caddy::CaddyConfig, dhcp_legacy::DhcpConfigLegacyISC, dns::DnsConfig, + dnsmasq::DhcpConfigDnsMasq, load_balancer::LoadBalancerConfig, tftp::TftpConfig, }, }; use log::{debug, info, trace, warn}; @@ -43,23 +43,27 @@ impl Config { }) } - pub fn dhcp(&mut self) -> DhcpConfig { - DhcpConfig::new(&mut self.opnsense, self.shell.clone()) + pub fn dhcp_legacy_isc(&mut self) -> DhcpConfigLegacyISC<'_> { + DhcpConfigLegacyISC::new(&mut self.opnsense, self.shell.clone()) } - pub fn dns(&mut self) -> DnsConfig { + pub fn dhcp(&mut self) -> DhcpConfigDnsMasq<'_> { + DhcpConfigDnsMasq::new(&mut self.opnsense, self.shell.clone()) + } + + pub fn dns(&mut self) -> DnsConfig<'_> { DnsConfig::new(&mut self.opnsense) } - pub fn tftp(&mut self) -> TftpConfig { + pub fn tftp(&mut self) -> TftpConfig<'_> { TftpConfig::new(&mut self.opnsense, self.shell.clone()) } - pub fn caddy(&mut self) -> CaddyConfig { + pub fn caddy(&mut self) -> CaddyConfig<'_> { CaddyConfig::new(&mut self.opnsense, self.shell.clone()) } - pub fn load_balancer(&mut self) -> LoadBalancerConfig { + pub fn load_balancer(&mut self) -> LoadBalancerConfig<'_> { LoadBalancerConfig::new(&mut self.opnsense, self.shell.clone()) } @@ -67,6 +71,10 @@ impl Config { self.shell.upload_folder(source, destination).await } + pub async fn upload_file_content(&self, path: &str, content: &str) -> Result { + self.shell.write_content_to_file(content, path).await + } + /// Checks in config file if system.firmware.plugins csv field contains the specified package /// name. /// @@ -200,7 +208,7 @@ impl Config { #[cfg(test)] mod tests { use crate::config::{DummyOPNSenseShell, LocalFileConfigManager}; - use crate::modules::dhcp::DhcpConfig; + use crate::modules::dhcp_legacy::DhcpConfigLegacyISC; use std::fs; use std::net::Ipv4Addr; @@ -215,6 +223,9 @@ mod tests { "src/tests/data/config-vm-test.xml", "src/tests/data/config-structure.xml", "src/tests/data/config-full-1.xml", + "src/tests/data/config-full-ncd0.xml", + "src/tests/data/config-full-25.7.xml", + "src/tests/data/config-full-25.7-dummy-dnsmasq-options.xml", ] { let mut test_file_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); test_file_path.push(path); @@ -257,7 +268,7 @@ mod tests { println!("Config {:?}", config); - let mut dhcp_config = DhcpConfig::new(&mut config.opnsense, shell); + let mut dhcp_config = DhcpConfigLegacyISC::new(&mut config.opnsense, shell); dhcp_config .add_static_mapping( "00:00:00:00:00:00", diff --git a/opnsense-config/src/config/shell/mod.rs b/opnsense-config/src/config/shell/mod.rs index 9159606..aa03837 100644 --- a/opnsense-config/src/config/shell/mod.rs +++ b/opnsense-config/src/config/shell/mod.rs @@ -9,6 +9,7 @@ use crate::Error; pub trait OPNsenseShell: std::fmt::Debug + Send + Sync { async fn exec(&self, command: &str) -> Result; async fn write_content_to_temp_file(&self, content: &str) -> Result; + async fn write_content_to_file(&self, content: &str, filename: &str) -> Result; async fn upload_folder(&self, source: &str, destination: &str) -> Result; } @@ -25,6 +26,14 @@ impl OPNsenseShell for DummyOPNSenseShell { async fn write_content_to_temp_file(&self, _content: &str) -> Result { unimplemented!("This is a dummy implementation"); } + + async fn write_content_to_file( + &self, + _content: &str, + _filename: &str, + ) -> Result { + unimplemented!("This is a dummy implementation"); + } async fn upload_folder(&self, _source: &str, _destination: &str) -> Result { unimplemented!("This is a dummy implementation"); } diff --git a/opnsense-config/src/config/shell/ssh.rs b/opnsense-config/src/config/shell/ssh.rs index 6b29658..95069fc 100644 --- a/opnsense-config/src/config/shell/ssh.rs +++ b/opnsense-config/src/config/shell/ssh.rs @@ -1,5 +1,6 @@ use std::{ net::IpAddr, + path::Path, sync::Arc, time::{SystemTime, UNIX_EPOCH}, }; @@ -44,6 +45,10 @@ impl OPNsenseShell for SshOPNSenseShell { .unwrap() .as_millis() ); + self.write_content_to_file(content, &temp_filename).await + } + + async fn write_content_to_file(&self, content: &str, filename: &str) -> Result { let channel = self.get_ssh_channel().await?; channel .request_subsystem(true, "sftp") @@ -53,10 +58,18 @@ impl OPNsenseShell for SshOPNSenseShell { .await .expect("Should acquire sftp subsystem"); - let mut file = sftp.create(&temp_filename).await.unwrap(); + if let Some(parent) = Path::new(filename).parent() { + if let Some(parent_str) = parent.to_str() { + if !parent_str.is_empty() { + self.ensure_remote_dir_exists(&sftp, parent_str).await?; + } + } + } + + let mut file = sftp.create(filename).await.unwrap(); file.write_all(content.as_bytes()).await?; - Ok(temp_filename) + Ok(filename.to_string()) } async fn upload_folder(&self, source: &str, destination: &str) -> Result { @@ -69,10 +82,7 @@ impl OPNsenseShell for SshOPNSenseShell { .await .expect("Should acquire sftp subsystem"); - if !sftp.try_exists(destination).await? { - info!("Creating remote directory {destination}"); - sftp.create_dir(destination).await?; - } + self.ensure_remote_dir_exists(&sftp, destination).await?; info!("Reading local directory {source}"); let mut entries = read_dir(source).await?; @@ -149,6 +159,14 @@ impl SshOPNSenseShell { wait_for_completion(&mut channel).await } + async fn ensure_remote_dir_exists(&self, sftp: &SftpSession, path: &str) -> Result<(), Error> { + if !sftp.try_exists(path).await? { + info!("Creating remote directory {path}"); + sftp.create_dir(path).await?; + } + Ok(()) + } + pub fn new(host: (IpAddr, u16), credentials: SshCredentials, ssh_config: Arc) -> Self { info!("Initializing SshOPNSenseShell on host {host:?}"); Self { diff --git a/opnsense-config/src/modules/dhcp.rs b/opnsense-config/src/modules/dhcp.rs index 6b2d752..8ec3519 100644 --- a/opnsense-config/src/modules/dhcp.rs +++ b/opnsense-config/src/modules/dhcp.rs @@ -1,19 +1,3 @@ -use log::info; -use opnsense_config_xml::MaybeString; -use opnsense_config_xml::StaticMap; -use std::net::Ipv4Addr; -use std::sync::Arc; - -use opnsense_config_xml::OPNsense; - -use crate::config::OPNsenseShell; -use crate::Error; - -pub struct DhcpConfig<'a> { - opnsense: &'a mut OPNsense, - opnsense_shell: Arc, -} - #[derive(Debug)] pub enum DhcpError { InvalidMacAddress(String), @@ -21,6 +5,7 @@ pub enum DhcpError { IpAddressAlreadyMapped(String), MacAddressAlreadyMapped(String), IpAddressOutOfRange(String), + Configuration(String), } impl std::fmt::Display for DhcpError { @@ -37,158 +22,9 @@ impl std::fmt::Display for DhcpError { DhcpError::IpAddressOutOfRange(ip) => { write!(f, "IP address {} is out of interface range", ip) } + DhcpError::Configuration(msg) => f.write_str(&msg), } } } impl std::error::Error for DhcpError {} - -impl<'a> DhcpConfig<'a> { - pub fn new(opnsense: &'a mut OPNsense, opnsense_shell: Arc) -> Self { - Self { - opnsense, - opnsense_shell, - } - } - - pub fn remove_static_mapping(&mut self, mac: &str) { - let lan_dhcpd = self.get_lan_dhcpd(); - lan_dhcpd - .staticmaps - .retain(|static_entry| static_entry.mac != mac); - } - - fn get_lan_dhcpd(&mut self) -> &mut opnsense_config_xml::DhcpInterface { - &mut self - .opnsense - .dhcpd - .elements - .iter_mut() - .find(|(name, _config)| name == "lan") - .expect("Interface lan should have dhcpd activated") - .1 - } - - pub fn add_static_mapping( - &mut self, - mac: &str, - ipaddr: Ipv4Addr, - hostname: &str, - ) -> Result<(), DhcpError> { - let mac = mac.to_string(); - let hostname = hostname.to_string(); - let lan_dhcpd = self.get_lan_dhcpd(); - let existing_mappings: &mut Vec = &mut lan_dhcpd.staticmaps; - - if !Self::is_valid_mac(&mac) { - return Err(DhcpError::InvalidMacAddress(mac)); - } - - // TODO validate that address is in subnet range - - if existing_mappings.iter().any(|m| { - m.ipaddr - .parse::() - .expect("Mapping contains invalid ipv4") - == ipaddr - && m.mac == mac - }) { - info!("Mapping already exists for {} [{}], skipping", ipaddr, mac); - return Ok(()); - } - - if existing_mappings.iter().any(|m| { - m.ipaddr - .parse::() - .expect("Mapping contains invalid ipv4") - == ipaddr - }) { - return Err(DhcpError::IpAddressAlreadyMapped(ipaddr.to_string())); - } - - if existing_mappings.iter().any(|m| m.mac == mac) { - return Err(DhcpError::MacAddressAlreadyMapped(mac)); - } - - let static_map = StaticMap { - mac, - ipaddr: ipaddr.to_string(), - hostname, - descr: Default::default(), - winsserver: Default::default(), - dnsserver: Default::default(), - ntpserver: Default::default(), - }; - - existing_mappings.push(static_map); - Ok(()) - } - - fn is_valid_mac(mac: &str) -> bool { - let parts: Vec<&str> = mac.split(':').collect(); - if parts.len() != 6 { - return false; - } - - parts - .iter() - .all(|part| part.len() <= 2 && part.chars().all(|c| c.is_ascii_hexdigit())) - } - - pub async fn get_static_mappings(&self) -> Result, Error> { - let list_static_output = self - .opnsense_shell - .exec("configctl dhcpd list static") - .await?; - - let value: serde_json::Value = serde_json::from_str(&list_static_output) - .unwrap_or_else(|_| panic!("Got invalid json from configctl {list_static_output}")); - let static_maps = value["dhcpd"] - .as_array() - .ok_or(Error::Command(format!( - "Invalid DHCP data from configctl command, got {list_static_output}" - )))? - .iter() - .map(|entry| StaticMap { - mac: entry["mac"].as_str().unwrap_or_default().to_string(), - ipaddr: entry["ipaddr"].as_str().unwrap_or_default().to_string(), - hostname: entry["hostname"].as_str().unwrap_or_default().to_string(), - descr: entry["descr"].as_str().map(MaybeString::from), - winsserver: MaybeString::default(), - dnsserver: MaybeString::default(), - ntpserver: MaybeString::default(), - }) - .collect(); - - Ok(static_maps) - } - pub fn enable_netboot(&mut self) { - self.get_lan_dhcpd().netboot = Some(1); - } - - pub fn set_next_server(&mut self, ip: Ipv4Addr) { - self.enable_netboot(); - self.get_lan_dhcpd().nextserver = Some(ip.to_string()); - self.get_lan_dhcpd().tftp = Some(ip.to_string()); - } - - pub fn set_boot_filename(&mut self, boot_filename: &str) { - self.enable_netboot(); - self.get_lan_dhcpd().bootfilename = Some(boot_filename.to_string()); - } - - pub fn set_filename(&mut self, filename: &str) { - self.enable_netboot(); - self.get_lan_dhcpd().filename = Some(filename.to_string()); - } - - pub fn set_filename64(&mut self, filename64: &str) { - self.enable_netboot(); - self.get_lan_dhcpd().filename64 = Some(filename64.to_string()); - } - - pub fn set_filenameipxe(&mut self, filenameipxe: &str) { - self.enable_netboot(); - self.get_lan_dhcpd().filenameipxe = Some(filenameipxe.to_string()); - } -} diff --git a/opnsense-config/src/modules/dhcp_legacy.rs b/opnsense-config/src/modules/dhcp_legacy.rs new file mode 100644 index 0000000..1d36ac6 --- /dev/null +++ b/opnsense-config/src/modules/dhcp_legacy.rs @@ -0,0 +1,166 @@ +use crate::modules::dhcp::DhcpError; +use log::info; +use opnsense_config_xml::MaybeString; +use opnsense_config_xml::StaticMap; +use std::net::Ipv4Addr; +use std::sync::Arc; + +use opnsense_config_xml::OPNsense; + +use crate::config::OPNsenseShell; +use crate::Error; + +pub struct DhcpConfigLegacyISC<'a> { + opnsense: &'a mut OPNsense, + opnsense_shell: Arc, +} + +impl<'a> DhcpConfigLegacyISC<'a> { + pub fn new(opnsense: &'a mut OPNsense, opnsense_shell: Arc) -> Self { + Self { + opnsense, + opnsense_shell, + } + } + + pub fn remove_static_mapping(&mut self, mac: &str) { + let lan_dhcpd = self.get_lan_dhcpd(); + lan_dhcpd + .staticmaps + .retain(|static_entry| static_entry.mac != mac); + } + + fn get_lan_dhcpd(&mut self) -> &mut opnsense_config_xml::DhcpInterface { + &mut self + .opnsense + .dhcpd + .elements + .iter_mut() + .find(|(name, _config)| name == "lan") + .expect("Interface lan should have dhcpd activated") + .1 + } + + pub fn add_static_mapping( + &mut self, + mac: &str, + ipaddr: Ipv4Addr, + hostname: &str, + ) -> Result<(), DhcpError> { + let mac = mac.to_string(); + let hostname = hostname.to_string(); + let lan_dhcpd = self.get_lan_dhcpd(); + let existing_mappings: &mut Vec = &mut lan_dhcpd.staticmaps; + + if !Self::is_valid_mac(&mac) { + return Err(DhcpError::InvalidMacAddress(mac)); + } + + // TODO validate that address is in subnet range + + if existing_mappings.iter().any(|m| { + m.ipaddr + .parse::() + .expect("Mapping contains invalid ipv4") + == ipaddr + && m.mac == mac + }) { + info!("Mapping already exists for {} [{}], skipping", ipaddr, mac); + return Ok(()); + } + + if existing_mappings.iter().any(|m| { + m.ipaddr + .parse::() + .expect("Mapping contains invalid ipv4") + == ipaddr + }) { + return Err(DhcpError::IpAddressAlreadyMapped(ipaddr.to_string())); + } + + if existing_mappings.iter().any(|m| m.mac == mac) { + return Err(DhcpError::MacAddressAlreadyMapped(mac)); + } + + let static_map = StaticMap { + mac, + ipaddr: ipaddr.to_string(), + hostname, + descr: Default::default(), + winsserver: Default::default(), + dnsserver: Default::default(), + ntpserver: Default::default(), + }; + + existing_mappings.push(static_map); + Ok(()) + } + + fn is_valid_mac(mac: &str) -> bool { + let parts: Vec<&str> = mac.split(':').collect(); + if parts.len() != 6 { + return false; + } + + parts + .iter() + .all(|part| part.len() <= 2 && part.chars().all(|c| c.is_ascii_hexdigit())) + } + + pub async fn get_static_mappings(&self) -> Result, Error> { + let list_static_output = self + .opnsense_shell + .exec("configctl dhcpd list static") + .await?; + + let value: serde_json::Value = serde_json::from_str(&list_static_output) + .unwrap_or_else(|_| panic!("Got invalid json from configctl {list_static_output}")); + let static_maps = value["dhcpd"] + .as_array() + .ok_or(Error::Command(format!( + "Invalid DHCP data from configctl command, got {list_static_output}" + )))? + .iter() + .map(|entry| StaticMap { + mac: entry["mac"].as_str().unwrap_or_default().to_string(), + ipaddr: entry["ipaddr"].as_str().unwrap_or_default().to_string(), + hostname: entry["hostname"].as_str().unwrap_or_default().to_string(), + descr: entry["descr"].as_str().map(MaybeString::from), + winsserver: MaybeString::default(), + dnsserver: MaybeString::default(), + ntpserver: MaybeString::default(), + }) + .collect(); + + Ok(static_maps) + } + pub fn enable_netboot(&mut self) { + self.get_lan_dhcpd().netboot = Some(1); + } + + pub fn set_next_server(&mut self, ip: Ipv4Addr) { + self.enable_netboot(); + self.get_lan_dhcpd().nextserver = Some(ip.to_string()); + self.get_lan_dhcpd().tftp = Some(ip.to_string()); + } + + pub fn set_boot_filename(&mut self, boot_filename: &str) { + self.enable_netboot(); + self.get_lan_dhcpd().bootfilename = Some(boot_filename.to_string()); + } + + pub fn set_filename(&mut self, filename: &str) { + self.enable_netboot(); + self.get_lan_dhcpd().filename = Some(filename.to_string()); + } + + pub fn set_filename64(&mut self, filename64: &str) { + self.enable_netboot(); + self.get_lan_dhcpd().filename64 = Some(filename64.to_string()); + } + + pub fn set_filenameipxe(&mut self, filenameipxe: &str) { + self.enable_netboot(); + self.get_lan_dhcpd().filenameipxe = Some(filenameipxe.to_string()); + } +} diff --git a/opnsense-config/src/modules/dnsmasq.rs b/opnsense-config/src/modules/dnsmasq.rs new file mode 100644 index 0000000..1430ba6 --- /dev/null +++ b/opnsense-config/src/modules/dnsmasq.rs @@ -0,0 +1,187 @@ +// dnsmasq.rs +use crate::modules::dhcp::DhcpError; +use log::{debug, info}; +use opnsense_config_xml::{MaybeString, StaticMap}; +use std::net::Ipv4Addr; +use std::sync::Arc; + +use opnsense_config_xml::OPNsense; + +use crate::config::OPNsenseShell; +use crate::Error; + +pub struct DhcpConfigDnsMasq<'a> { + opnsense: &'a mut OPNsense, + opnsense_shell: Arc, +} + +const DNS_MASQ_PXE_CONFIG_FILE: &str = "/usr/local/etc/dnsmasq.conf.d/pxe.conf"; + +impl<'a> DhcpConfigDnsMasq<'a> { + pub fn new(opnsense: &'a mut OPNsense, opnsense_shell: Arc) -> Self { + Self { + opnsense, + opnsense_shell, + } + } + + /// Removes a static mapping by its MAC address. + /// Static mappings are stored in the section of the config, shared with the ISC module. + pub fn remove_static_mapping(&mut self, mac: &str) { + let lan_dhcpd = self.get_lan_dhcpd(); + lan_dhcpd + .staticmaps + .retain(|static_entry| static_entry.mac != mac); + } + + /// Retrieves a mutable reference to the LAN interface's DHCP configuration. + /// This is located in the shared section of the config. + fn get_lan_dhcpd(&mut self) -> &mut opnsense_config_xml::DhcpInterface { + &mut self + .opnsense + .dhcpd + .elements + .iter_mut() + .find(|(name, _config)| name == "lan") + .expect("Interface lan should have dhcpd activated") + .1 + } + + /// Adds a new static DHCP mapping. + /// Validates the MAC address and checks for existing mappings to prevent conflicts. + pub fn add_static_mapping( + &mut self, + mac: &str, + ipaddr: Ipv4Addr, + hostname: &str, + ) -> Result<(), DhcpError> { + let mac = mac.to_string(); + let hostname = hostname.to_string(); + let lan_dhcpd = self.get_lan_dhcpd(); + let existing_mappings: &mut Vec = &mut lan_dhcpd.staticmaps; + + if !Self::is_valid_mac(&mac) { + return Err(DhcpError::InvalidMacAddress(mac)); + } + + // TODO: Validate that the IP address is within a configured DHCP range. + + if existing_mappings + .iter() + .any(|m| m.ipaddr == ipaddr.to_string() && m.mac == mac) + { + info!("Mapping already exists for {} [{}], skipping", ipaddr, mac); + return Ok(()); + } + + if existing_mappings + .iter() + .any(|m| m.ipaddr == ipaddr.to_string()) + { + return Err(DhcpError::IpAddressAlreadyMapped(ipaddr.to_string())); + } + + if existing_mappings.iter().any(|m| m.mac == mac) { + return Err(DhcpError::MacAddressAlreadyMapped(mac)); + } + + let static_map = StaticMap { + mac, + ipaddr: ipaddr.to_string(), + hostname: hostname, + ..Default::default() + }; + + existing_mappings.push(static_map); + Ok(()) + } + + /// Helper function to validate a MAC address format. + fn is_valid_mac(mac: &str) -> bool { + let parts: Vec<&str> = mac.split(':').collect(); + if parts.len() != 6 { + return false; + } + parts + .iter() + .all(|part| part.len() <= 2 && part.chars().all(|c| c.is_ascii_hexdigit())) + } + + /// Retrieves the list of current static mappings by shelling out to `configctl`. + /// This provides the real-time state from the running system. + pub async fn get_static_mappings(&self) -> Result, Error> { + let list_static_output = self + .opnsense_shell + .exec("configctl dhcpd list static") + .await?; + + let value: serde_json::Value = serde_json::from_str(&list_static_output) + .unwrap_or_else(|_| panic!("Got invalid json from configctl {list_static_output}")); + let static_maps = value["dhcpd"] + .as_array() + .ok_or(Error::Command(format!( + "Invalid DHCP data from configctl command, got {list_static_output}" + )))? + .iter() + .map(|entry| StaticMap { + mac: entry["mac"].as_str().unwrap_or_default().to_string(), + ipaddr: entry["ipaddr"].as_str().unwrap_or_default().to_string(), + hostname: entry["hostname"].as_str().unwrap_or_default().to_string(), + descr: entry["descr"].as_str().map(MaybeString::from), + ..Default::default() + }) + .collect(); + + Ok(static_maps) + } + + pub async fn set_pxe_options( + &self, + tftp_ip: Option, + bios_filename: String, + efi_filename: String, + ipxe_filename: String, + ) -> Result<(), DhcpError> { + // As of writing this opnsense does not support negative tags, and the dnsmasq config is a + // bit complicated anyways. So we are writing directly a dnsmasq config file to + // /usr/local/etc/dnsmasq.conf.d + let tftp_str = tftp_ip.map_or(String::new(), |i| format!(",{i},{i}")); + + let config = format!( + " +# Add tag ipxe to dhcp requests with user class (77) = iPXE +dhcp-match=set:ipxe,77,iPXE +# Add tag bios to dhcp requests with arch (93) = 0 +dhcp-match=set:bios,93,0 +# Add tag efi to dhcp requests with arch (93) = 7 +dhcp-match=set:efi,93,7 + +# Provide ipxe efi file to uefi but NOT ipxe clients +dhcp-boot=tag:efi,tag:!ipxe,{efi_filename}{tftp_str} + +# Provide ipxe boot script to ipxe clients +dhcp-boot=tag:ipxe,{ipxe_filename}{tftp_str} + +# Provide undionly to legacy bios clients +dhcp-boot=tag:bios,{bios_filename}{tftp_str} +" + ); + info!("Writing configuration file to {DNS_MASQ_PXE_CONFIG_FILE}"); + debug!("Content:\n{config}"); + self.opnsense_shell + .write_content_to_file(&config, DNS_MASQ_PXE_CONFIG_FILE) + .await + .map_err(|e| { + DhcpError::Configuration(format!( + "Could not configure pxe for dhcp because of : {e}" + )) + })?; + + info!("Restarting dnsmasq to apply changes"); + self.opnsense_shell + .exec("configctl dnsmasq restart") + .await + .map_err(|e| DhcpError::Configuration(format!("Restarting dnsmasq failed : {e}")))?; + Ok(()) + } +} diff --git a/opnsense-config/src/modules/mod.rs b/opnsense-config/src/modules/mod.rs index dc3fd7c..3448075 100644 --- a/opnsense-config/src/modules/mod.rs +++ b/opnsense-config/src/modules/mod.rs @@ -1,5 +1,7 @@ pub mod caddy; pub mod dhcp; +pub mod dhcp_legacy; pub mod dns; +pub mod dnsmasq; pub mod load_balancer; pub mod tftp; diff --git a/opnsense-config/src/tests/data/config-full-25.7-dnsmasq-options.xml b/opnsense-config/src/tests/data/config-full-25.7-dnsmasq-options.xml new file mode 100644 index 0000000..879d8d6 --- /dev/null +++ b/opnsense-config/src/tests/data/config-full-25.7-dnsmasq-options.xml @@ -0,0 +1,896 @@ + + + opnsense + + + + + 115200 + serial + normal + OPNsense + testpxe.harmony.mcd + + admins + System Administrators + system + 1999 + 0 + page-all + + + + root + System Administrator + system + $2y$10$YRVoF4SgskIsrXOvOQjGieB9XqHPRra9R7d80B3BZdbY/j21TwBfS + + 0 + 0 + + + + + + + + + + + + + Etc/UTC + 0.opnsense.pool.ntp.org 1.opnsense.pool.ntp.org 2.opnsense.pool.ntp.org 3.opnsense.pool.ntp.org + + https + 68a72b6f7f776 + + + + + + 1 + yes + 1 + 1 + 1 + 1 + 1 + 1 + hadp + hadp + hadp + + monthly + + 1 + 1 + + admins + 1 + + + + + + enabled + 1 + + 1 + + + -1 + -1 + + + + os-tftp + + + 0 + + en_US + + 1 + + + + + vtnet0 + + 1 + + + dhcp + + 0 + 1 + + dhcp6 + 0 + + + + + + vtnet1 + 1 + 192.168.1.1 + 24 + track6 + 64 + + + wan + 0 + + + 1 + lo0 + Loopback + 1 + 127.0.0.1 + none + 1 + 8 + ::1 + 128 + + + + + + + public + + + + + automatic + + + + + pass + lan + inet + Default allow LAN to any rule + + lan + + + + + + + pass + lan + inet6 + Default allow LAN IPv6 to any rule + + lan + + + + + + + + + + + 0.opnsense.pool.ntp.org + + + root@192.168.1.5 + /api/dnsmasq/settings/set made changes + + + + + + + + + + + + + + + v9 + + + + 0 + + 1800 + 15 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 0 + wan + 192.168.0.0/16,10.0.0.0/8,172.16.0.0/12 + + + W0D23 + 4 + + + + + + + 0 + 0 + 0 + + + + 0 + 0 + + + + 0 + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + + 16 + 32 + 4 + 1000 + 1 + 0 + 0 + 0 + + + + + + + + 1 + 0 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + + + + + + + + + + 0 + + + + + + + 0 + 0 + + + ipsec + 0 + 1 + + + + + + + + + + + + + 0 + 0 + + 4000 + 1 + raw + + + 0 + + 2 + + + + + + + + 0 + 127.0.0.1 + 8000 + + + + + 0 + 0 + + 4000 + 1 + + + 0 + + 2 + + + + + + + + + + 0 + 120 + 120 + 127.0.0.1 + 25 + + + 0 + auto + 1 + + + + + 0 + root + + 2812 + + + 5 + 1 + + + 0 + root@localhost.local + 0 + + + + + + + 1 + $HOST + + system + + + + 300 + 30 +
+ + + + cfed35dc-f74b-417d-9ed9-682c5de96495,f961277a-07f1-49a4-90ee-bb15738d9ebb,30b2cce2-f650-4e44-a3e2-ee53886cda3f,3c86136f-35a4-4126-865b-82732c6542d9 + + + + + 1 + RootFs + + filesystem + + + / + 300 + 30 +
+ + + + fbb8dfe2-b9ad-4730-a0f3-41d7ecda6289 + + + + + 0 + carp_status_change + + custom + + + /usr/local/opnsense/scripts/OPNsense/Monit/carp_status + 300 + 30 +
+ + + + 11ceca8a-dff8-45e0-9dc5-ed80dc4b3947 + + + + + 0 + gateway_alert + + custom + + + /usr/local/opnsense/scripts/OPNsense/Monit/gateway_alert + 300 + 30 +
+ + + + fad1f465-4a92-4b93-be66-59d7059b8779 + + + + + Ping + NetworkPing + failed ping + alert + + + + NetworkLink + NetworkInterface + failed link + alert + + + + NetworkSaturation + NetworkInterface + saturation is greater than 75% + alert + + + + MemoryUsage + SystemResource + memory usage is greater than 75% + alert + + + + CPUUsage + SystemResource + cpu usage is greater than 75% + alert + + + + LoadAvg1 + SystemResource + loadavg (1min) is greater than 4 + alert + + + + LoadAvg5 + SystemResource + loadavg (5min) is greater than 3 + alert + + + + LoadAvg15 + SystemResource + loadavg (15min) is greater than 2 + alert + + + + SpaceUsage + SpaceUsage + space usage is greater than 75% + alert + + + + ChangedStatus + ProgramStatus + changed status + alert + + + + NonZeroStatus + ProgramStatus + status != 0 + alert + + + + + + + + + 1 + 1 + 31 + + + + + + + + + + + + 1 + 53 + 0 + + 0 + 0 + + 0 + 0 + + 0 + 0 + 0 + 0 + 0 + transparent + + 0 + + + 0 + 0 + 0 + 0 + 0 + 1 + 0 + + + 0 + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 1 + 0 + + 0.0.0.0/8,10.0.0.0/8,100.64.0.0/10,169.254.0.0/16,172.16.0.0/12,192.0.2.0/24,192.168.0.0/16,198.18.0.0/15,198.51.100.0/24,203.0.113.0/24,233.252.0.0/24,::1/128,2001:db8::/32,fc00::/8,fd00::/8,fe80::/10 + + + + + + + + + + + + + + 0 + + + + + allow + + + 0 + 0 + + + + + +
+ 0 + + + 0 + + + + + + + + + 0 + 0 + 0 + 1 + 0 + + + + + + + + + + + 1 + 192.168.1.1 + + + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + 68a72b6f7f776 + Web GUI TLS certificate + LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUhFakNDQlBxZ0F3SUJBZ0lVUkZqWUQ0Z1U0bzRNZGdiN2pIc29KNU9GVGFnd0RRWUpLb1pJaHZjTkFRRUwKQlFBd2dZWXhHakFZQmdOVkJBTU1FVTlRVG5ObGJuTmxMbWx1ZEdWeWJtRnNNUXN3Q1FZRFZRUUdFd0pPVERFVgpNQk1HQTFVRUNBd01XblZwWkMxSWIyeHNZVzVrTVJVd0V3WURWUVFIREF4TmFXUmtaV3hvWVhKdWFYTXhMVEFyCkJnTlZCQW9NSkU5UVRuTmxibk5sSUhObGJHWXRjMmxuYm1Wa0lIZGxZaUJqWlhKMGFXWnBZMkYwWlRBZUZ3MHkKTlRBNE1qRXhOREl4TXpaYUZ3MHlOakE1TWpJeE5ESXhNelphTUlHR01Sb3dHQVlEVlFRRERCRlBVRTV6Wlc1egpaUzVwYm5SbGNtNWhiREVMTUFrR0ExVUVCaE1DVGt3eEZUQVRCZ05WQkFnTURGcDFhV1F0U0c5c2JHRnVaREVWCk1CTUdBMVVFQnd3TVRXbGtaR1ZzYUdGeWJtbHpNUzB3S3dZRFZRUUtEQ1JQVUU1elpXNXpaU0J6Wld4bUxYTnAKWjI1bFpDQjNaV0lnWTJWeWRHbG1hV05oZEdVd2dnSWlNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUNEd0F3Z2dJSwpBb0lDQVFDbENkeFJ3ZWJQQkxvYlVORnYvL2t3TEdKWExweDl6OFFHV2lyWTNpamVDeUxDQ0FwczBLaE1adTNRClhkczMranppbDRnSE96L0hvUEo5Z0xxMy9FYnR4cE9ENWkvQzZwbXc3SGM1M2tTQ3JCK2tlWUFnVWZ1aDU3MzAKZyt3cGc5RDQzaHFBNzF1L3F0ZC95eitnTVJnTWdZMndEK3ZWQWRrdGxVSWlmN2piTmR1RDRGMmdkL0gwbzljWApEUm5zMzNNQVptTkZwajN4QWFwQi9RWnhKV1JMZ1J5K1A5MWcyZEZFNzhNaWY4ZTRNSCtrU29ndzIwVG1JbmpzCitKdEVTc0xQZmx2eUZUa0lkTVdFbURWOG1HUk5hNXVoYXlEbVNEUU9xV0NUTlZHV3ZVWjZTRnJRZ1Q1MDBEdXgKWnRtYlhGdEVqRzlIaGd5SW5QT0dKbWYzTWVzS3dYclVNMW1BenVCRVBFR0lwOTc3UTY2SitpTDYzWTUvTTB3aAphMGVVNGppNTVRQnJOQjlaWjJsa080bGU2TXdmZm50c29JakMrVDh5RW5tbW5nQTlTdWNPRW9CcFFhd3cvRGhOCmtSNGk4TUptR1JNdmpLazlHVzZ3Z2VNVThJVDhKZDRjTmJOVzdFSGpzV08xN1luTVhIMEUxOVZqa2d1R3dIODAKZ3ZROGtzTmV4WVA3WWo0b0VycnRKbWVhWU8wbFVkV0tGektNdS8va0UvNG5HK0h4emlRUnA5QmdyTURNYks4ZgpkM29mY2tqZFZTTW9Vc1FJaWlmdTFMK1I4V1Y3K3hsTzdTWS80dGk3Y28zcjNXRTYyVlE4Vk9QMVphcStWRFpvClNIMVRCa0lTSU5paVJFRzhZSDQvRHJwNWZ2dHBPcERBRGN1TGdDNDJHcExmS1pwVEtRSURBUUFCbzRJQmREQ0MKQVhBd0NRWURWUjBUQkFJd0FEQVJCZ2xnaGtnQmh2aENBUUVFQkFNQ0JrQXdOQVlKWUlaSUFZYjRRZ0VOQkNjVwpKVTlRVG5ObGJuTmxJRWRsYm1WeVlYUmxaQ0JUWlhKMlpYSWdRMlZ5ZEdsbWFXTmhkR1V3SFFZRFZSME9CQllFCkZIdUVQK05yYlorZWdMdWZVSUFKaUo2M1c4SDFNSUd3QmdOVkhTTUVnYWd3Z2FXaGdZeWtnWWt3Z1lZeEdqQVkKQmdOVkJBTU1FVTlRVG5ObGJuTmxMbWx1ZEdWeWJtRnNNUXN3Q1FZRFZRUUdFd0pPVERFVk1CTUdBMVVFQ0F3TQpXblZwWkMxSWIyeHNZVzVrTVJVd0V3WURWUVFIREF4TmFXUmtaV3hvWVhKdWFYTXhMVEFyQmdOVkJBb01KRTlRClRuTmxibk5sSUhObGJHWXRjMmxuYm1Wa0lIZGxZaUJqWlhKMGFXWnBZMkYwWllJVVJGallENGdVNG80TWRnYjcKakhzb0o1T0ZUYWd3SFFZRFZSMGxCQll3RkFZSUt3WUJCUVVIQXdFR0NDc0dBUVVGQ0FJQ01Bc0dBMVVkRHdRRQpBd0lGb0RBY0JnTlZIUkVFRlRBVGdoRlBVRTV6Wlc1elpTNXBiblJsY201aGJEQU5CZ2txaGtpRzl3MEJBUXNGCkFBT0NBZ0VBV2JzM2MwSXYwcEd3Y0wvUmRlbnBiZVJHQ3FsODY0V1ZITEtMZzJIR3BkKytJdmRFcHJEZkZ3SCsKdHdOd2VrZTlXUEtYa20vUkZDWE5DQmVLNjkxeURVWCtCNUJOMjMvSks5N1lzRVdtMURIV3FvSDE1WmdqelZ0QQp2d2JmbnRQdlhCWU1wV2ZQY0Zua0hjN3pxUjI3RzBEZHFUeGg2TjhFenV1S3JRWXFtaWhJUXFkNU9HRVhteW9ZCmdPVjdoZ0lWSUR6a1Z0QkRiS3dFV3VFN2pKYzViMXR4Mk1FUFRsVklEZGo0Zm5vdURWemdkczA2RER4aFM4eXAKbXJOSXhxb045ekEzYXVtTnRNZ2haSHVZRHdjbm5GSnBNZHlJSEdHZ1dlNnZZNHFtdEFSVDd3a0x6MTZnUG9LMAo5bFhVU0RmV3YwUDJGUXFHZTJjaXQ3VVE2ZGtsUWsrVGVtUEFwNnhEV09HR3oxRkdmUUoxN040b3AvOGtlOUo2Cm96RVp3QTh1aDVYTUl2N3loM2dobjV1d1R6RDUyZ1BBZFdaekEyaHVWV3p5cVM0WVc0N3ZkaGV6TTFTUndabVEKUmYzNDk0UVFydWd0bzdycWdMUlRTSXN4WEtkU21MaHZjT0hsSlhISW1XNTRzeFlXNm9NUStpRExFT29ZVVdpcgp1aUJvT1RsNEJaOG5Xcm9pV0JtWlFLaVRPYlFRczVWTkIwYnVybmRISTJVdmtIRTE3QTM0bFYySjY5Q0dNNzJ2CjQ5aE9TN3B2Tzg4cEVKZm90d01YYlRhdkR2WTBHazZxbERFMVBud1U2Wm8ySEprcFdUTUxOSzh1alZ1RkhlMGkKR2JvZi9va08vZW4rUi9PUXNyd1JYbzFwVTRiWnlyWGVQeUdqSSsrdFYzemhjd0IwWjNJPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUpRUUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQ1Nzd2dna25BZ0VBQW9JQ0FRQ2xDZHhSd2ViUEJMb2IKVU5Gdi8va3dMR0pYTHB4OXo4UUdXaXJZM2lqZUN5TENDQXBzMEtoTVp1M1FYZHMzK2p6aWw0Z0hPei9Ib1BKOQpnTHEzL0VidHhwT0Q1aS9DNnBtdzdIYzUza1NDckIra2VZQWdVZnVoNTczMGcrd3BnOUQ0M2hxQTcxdS9xdGQvCnl6K2dNUmdNZ1kyd0QrdlZBZGt0bFVJaWY3amJOZHVENEYyZ2QvSDBvOWNYRFJuczMzTUFabU5GcGozeEFhcEIKL1FaeEpXUkxnUnkrUDkxZzJkRkU3OE1pZjhlNE1IK2tTb2d3MjBUbUluanMrSnRFU3NMUGZsdnlGVGtJZE1XRQptRFY4bUdSTmE1dWhheURtU0RRT3FXQ1ROVkdXdlVaNlNGclFnVDUwMER1eFp0bWJYRnRFakc5SGhneUluUE9HCkptZjNNZXNLd1hyVU0xbUF6dUJFUEVHSXA5NzdRNjZKK2lMNjNZNS9NMHdoYTBlVTRqaTU1UUJyTkI5WloybGsKTzRsZTZNd2ZmbnRzb0lqQytUOHlFbm1tbmdBOVN1Y09Fb0JwUWF3dy9EaE5rUjRpOE1KbUdSTXZqS2s5R1c2dwpnZU1VOElUOEpkNGNOYk5XN0VIanNXTzE3WW5NWEgwRTE5VmprZ3VHd0g4MGd2UThrc05leFlQN1lqNG9FcnJ0CkptZWFZTzBsVWRXS0Z6S011Ly9rRS80bkcrSHh6aVFScDlCZ3JNRE1iSzhmZDNvZmNramRWU01vVXNRSWlpZnUKMUwrUjhXVjcreGxPN1NZLzR0aTdjbzNyM1dFNjJWUThWT1AxWmFxK1ZEWm9TSDFUQmtJU0lOaWlSRUc4WUg0LwpEcnA1ZnZ0cE9wREFEY3VMZ0M0MkdwTGZLWnBUS1FJREFRQUJBb0lDQUFTSHc4Tit4aDR5ckFVcDc4WGFTZlhYCmtnK0FtUTBmRWV0MnVDeGgxTTlia09Xd29OQ2gzYXpUT24zNHhaYkF5TUVUbGNsVkNBZ3IwOXc4RjJRTGljcm4KSTQrQVZ4bExwVkprKzFUY1ZCY2VNSFFzWGFjRmVSblZxYkkzbU5qKzVGS2dqaXV4NWx2WmpiYlZWbmJJUWplOQpxcTBGa3R5ekEwb3NDYmUydDlWVW9pVDVtTGhaOG90Ym9BRGkvQzR6YUEyL3djUGNyMkNaUWhvem51U21PUjJWCmVydUNOMHA4VURGTFA1a0gxdXlvY0NpTFh6ZXdIVEVRQ3krK0YwMEZuRmxqeDVSYW5za3JvMnhqWFR5QlZtZUYKcDYwRHF0Q0hkTjVlS2VlQWxDL0dIRlFvL2swdzd3ejMxbHVsVGgza3FDQzJsaXRwYzVpZ2JsTGxaUDgxSUpXTQp0bkhlczNsTXk1RGNDWUx3L3huZFdmVDZFMTB4WlhFNWI0QTdxYjF4Yjhsd1FoNHFJckhDZ2p1NDVPYXNCMERJClBYZ3E2eWkwL2FKWXV6SU5kcjRTeFRibExGUkp6MXlQaGZTZDVGbjdWQVBYU1JNTDlDbzJrL0M1SDlwdG1HMjYKZHBLQVNib1ZMcStrbXg3anVKYXc0a1JNNHZmYndHZGNMZEhqMXByZ08xNkd1ckpQOVRRQ0x5YzhaR0xOekcvaApIMzBpU2FlclJOUmtDRlhmeTEzWWJJZTZHTE12KzVmODlYSENGNmZrZ1JkZjVrbTA3cEc3SCtMZytmZFdtd2lZCm0waExNSFVZeHJ3WkFma2tvZjhlSllscEVQVmQ3ZytCVjd2eTZhYW0yQituUTdHYk84WUprSnlJME04amlSaDEKeGdjRmFZaGZlT21RZGtVbi9BcUJBb0lCQVFEU1JZbDl0SnJyQk5UOXlZN0twWTJiOGVURFJqdDByU1NQRUJvNgppeWoyVWR5S1ZSbHFFdzRma2IrejV3WWt2bnBpMW1uS3NjNFlLZmoyaDVSdXVDbzVzTUNLbmpDUXBpbll4bWRFCk45Z3l6SWRYMmlzRUh6dXNSZkZiajBVSWU1dHc0TE9pL3cyVzFYWGJUc3liOFdhTmhkbVQ4TGxDNjQ5WkNNUWQKeDZkeTdOWS9uYnVWVVQ0KzM3WmV0VlR1eDg1ekl5OTdnMWp4dFZhaXZrd2hQVWtLcWpXWDYzaUZidjFuY1FVdgpiQURrWkFoOXRWYWV2UGZ2NURDeDZITldiVFlObjVRZWd3OTRyVndoSjhYb1V5ZDRqWFB0VmdXU2VkN0tWd2U5CmNkNW9CZWFBOVhDdnJxdkNIRjI4QXg2OUI2YWQrQlk1S0dVcGU2LythQnlKdlQwUkFvSUJBUURJN2c3c0dMc3AKVWJ4dGhJQm9yRzF5MDRKWWlnaE5VMlF4YjdzSkxocnRTc2NtRkxSZU5DYy8zOTBCV3ZrbVFIazFnZkpxV3hDLwp2R0VMT0Iwd3U5VFBBRWFsS25IZ2RhNkFTMURuM3NTWTltcFBRRjYvbEY2cm00cDlNUU1TTFo1V3ZsL0ZNRklHCjUvaXVSVjJaOVdkeTV4QVFWNG5uZmdMOWJDNzhPa2k3VnFPTDJDZk0vcEJEMHdzRUZmOGZiejFSZXo4dEFRZ2QKVXY4cEpFTWdDTCtQeEdkdG5DYUcxYm5obXhEUUxiWmQ4TTFOQkpKOWZLZFgzVWtLcTlDdmFJVXBIZlduSFBWVAprVWNwMUVTYnEzOFVhTzFSS1NBNUtQd1ZiNkVPVGJBSGtlaEN4ZVhpN2F3YkZwYXlTelpIaWl4Y05QQjk1YUtSCkpJQ0J5ekFwQTVTWkFvSUJBRlZKYXlrWGxqWjVNVUwyKy9ucUNIUVdPeW1SVlJCUUlpSDg4QWFLNTBSeGs3aHcKSit6RWFkZ1lMOTl5ZHlWME5RUGQzKzhkQzNEMXBVdXBWbVZLUWFaQXNQZ0lqYjQrQjM4cmlqczdRMi9uVVlZcQpzWVBzZnpHeTlPQ2tUZVhRN1ExdHRxOElNS1RiVkFCdUI4UEF1RTN5Mm51TkNqZkFmOVluSGhUT0pIY1M1UnZNCmlJZForcHRaOWdpWUdDajUxaDBSU25NWXBYejBobjFnSGxUbEhMazhySnhBSUJSUEhtMVVoRHZsM0w3R2JFTkEKeUM5K2lqbzlIaHNySTQwTW92NEhtZlorUmtvMlZzWUQ4ZHYzem15eFF6SWkwQVBIZHJ3dmJLNUVmMmRGN1dhbApKdDI3UldOb1NnUzJaME5ZMVJZQnlGSEt0cTJLdzZtMjVNeGhlMkVDZ2dFQVhSNFdSRXhoMEpCVXB0eVZOZTFTCis3Z1IzRDU4QW5uM0lRSUt5QUpaOEVhTGJKYUQwSFNUREFNUFJTV0grYlkvZGhDMjY1c3djK3MxZmlHUFJacUcKMFRmcmhYZmFOby9UUXhta2NSRElRNnRQTVZNL2xjR0k3amF6UTdtSEZ0R1ZZOVh1UkZCVWMyYmwxTDNJMXlUbgp3RlJkR1hXNEwxUXl4b2R3YnV3RDhPNEI5VGxEbUxrUTJwM2ZxUkVZbnRUS3NneFFCdWRIZjIrTFdPRzVTZ3RECjI3akZ4Z0pyeUdrY0wvWFJJT2xPYnRLK0VrZGdMRStzcmdlYlpocWlKK2hrYmQyNGpxM1k4OVdNQ1ZLYVNScDkKVmxRYVIxYXIzRkdtSWJrT0JyYnlNVS9wTjZqSEZSZllmdVZGQ1hQWnYrWEZFU1pubmJEaVdpbDBkTEpacTJoQgpZUUtDQVFBOVlTcE1wS3dhVGwrTmhTZlovMXU0NjZiMkpZcmJPYlRJM2VCZUowMnFwNXdQTjZYUHJ5aVZaZ1FXClh5cG04a3M5MEJIblBPNUczNFJnKzhLRFlONU1Ed1hBclJubDdSeTEySG5oV3lSaHNKYmdZOEh1c2d4SEROMU8KMEcwSFBpVWtIbTYydlRNYll6bkhPeE5sS1hFdFhBcTlZM3dQYkFjb01rRXZ0MzEwdEdLSUNtdTdEWkpXRlVvTAp1Y3RVS3Boc0V5VWdHbHIwRjJKekVoQWdMRXplczB0S1JpRWdlaFdnbXdlMEhlTEhCdW5oRFBTMmFJY2lCME1pCjY2SGc3cVZyMDlneXpFeGxrY3RLRzhsSm9WbU8vdlhucWQrWDB5M21YTUVZbkFIWHpIeG1Pd2JCNnF3Y3VWTlEKZytqRXliUWF3d3A2OC9id0JncFREQUhORGxrRQotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tCg== + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + 0 + 0 + + 1400 + + + + + 1 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + + lan + 0 + + + + + 0 + 0 + + + 1 + + + 0 + 1 + + 0 + 0 + + 1 + + ipxe + + + pxeEfi + + + pxeBios + + + lan + + 192.168.1.41 + 192.168.1.245 + + + + + + range + + 0 + + + + + + + + + match + + + + + 8d190cf3-8d2d-47db-ab9b-fa21016b533e + iPXE + + + + + match + + + + + 993e079f-09b9-4a0f-a70f-8898872b9983 + 0 + + + + + match + + + + + 0b2982da-198c-4ca4-9a3e-95813667047c + 7 + + + + + + 0b2982da-198c-4ca4-9a3e-95813667047c + ipxe.efi + 192.168.1.1 +
192.168.1.1
+ +
+ + + 8d190cf3-8d2d-47db-ab9b-fa21016b533e + http://192.168.1.1:8080/boot.ipxe + 192.168.1.1 +
192.168.1.1
+ +
+ + + 993e079f-09b9-4a0f-a70f-8898872b9983 + undionly.kpxe + 192.168.1.1 +
192.168.1.1
+ +
+
+ diff --git a/opnsense-config/src/tests/data/config-full-25.7-dummy-dnsmasq-options.xml b/opnsense-config/src/tests/data/config-full-25.7-dummy-dnsmasq-options.xml new file mode 100644 index 0000000..5e22137 --- /dev/null +++ b/opnsense-config/src/tests/data/config-full-25.7-dummy-dnsmasq-options.xml @@ -0,0 +1,867 @@ + + + opnsense + + + + + 115200 + serial + normal + OPNsense + internal + 1 + + + 1999 + admins + system + System Administrators + page-all + 0 + + + + 0 + root + 0 + system + + + + + $2y$10$YRVoF4SgskIsrXOvOQjGieB9XqHPRra9R7d80B3BZdbY/j21TwBfS + + + + + + + + System Administrator + + + Etc/UTC + 0.opnsense.pool.ntp.org 1.opnsense.pool.ntp.org 2.opnsense.pool.ntp.org 3.opnsense.pool.ntp.org + + https + 68a5faf1685db + + + + + + yes + 1 + 1 + 1 + 1 + 1 + 1 + 1 + hadp + hadp + hadp + + monthly + + 1 + 1 + + admins + 1 + + + + + + + + enabled + 1 + 1 + + -1 + -1 + + + + + + + 0 + + + en_US + + + + 1 + vtnet0 + + dhcp + dhcp6 + + + 0 + 1 + + + 0 + + + + + + 1 + vtnet1 + 192.168.1.1 + 24 + track6 + 64 + + + wan + 0 + + + 1 + Loopback + 1 + lo0 + 127.0.0.1 + ::1 + 8 + 128 + none + 1 + + + + 1 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + + lan + 0 + + + + + 0 + 0 + + + 1 + + + 0 + 1 + + 0 + 0 + + 1 + + lan + + 192.168.1.41 + 192.168.1.245 + + + + + + range + + 0 + + + + + + + + + set + + + lan + + + test/boot/filename + 0 + + + + set + + + lan + + + test some pxe setting vendor specific 128 + 0 + + + + set + + + + + + pxelinux magic what is this (on any interface) + 0 + + + + + + boot options filename + boot servername +
boot server address
+ boot description +
+
+ + + + public + + + + automatic + + + + + pass + inet + Default allow LAN to any rule + lan + + lan + + + + + + + pass + inet6 + Default allow LAN IPv6 to any rule + lan + + lan + + + + + + + + + + + 0.opnsense.pool.ntp.org + + + root@192.168.1.5 + /api/dnsmasq/settings/set made changes + + + + + + + + + 0 + + + + + + + + + 0 + 0 + + + + + + + 16 + 32 + 4 + 1000 + 1 + 0 + 0 + 0 + + + + + + + + 1 + 0 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + + + + + + + + + + 0 + + + + + + + 0 + 0 + + + ipsec + 0 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + v9 + + + + 0 + + 1800 + 15 + + + + + + + + + 0 + 0 + 0 + wan + 192.168.0.0/16,10.0.0.0/8,172.16.0.0/12 + + + W0D23 + 4 + + + + + + + 0 + 0 + 0 + + + + 0 + 0 + + + + 0 + 0 + 0 + + + + + + + + + + + + + + 0 + 127.0.0.1 + 8000 + + + + + 0 + 0 + + 4000 + 1 + raw + + + 0 + + 2 + + + + + + + + 0 + 0 + + 4000 + 1 + + + 0 + + 2 + + + + + + + + + + 0 + 120 + 120 + 127.0.0.1 + 25 + + + 0 + auto + 1 + + + + + 0 + root + + 2812 + + + 5 + 1 + + + 0 + root@localhost.local + 0 + + + + + + + 1 + $HOST + + system + + + + 300 + 30 +
+ + + + 02014be3-fc31-4af3-a0d5-061eaa67d28a,ebfd0d97-ae21-45d5-8b42-5220c75ce46f,d37f25f0-89e3-44b6-8ad2-280ac83a8904,37afd0d9-990c-4f03-a817-45691461e3d0 + + + + + 1 + RootFs + + filesystem + + + / + 300 + 30 +
+ + + + b44b859c-bc72-4c2e-82c9-4f56d84a5497 + + + + + 0 + carp_status_change + + custom + + + /usr/local/opnsense/scripts/OPNsense/Monit/carp_status + 300 + 30 +
+ + + + 0909801b-cd11-41c8-afeb-369396247308 + + + + + 0 + gateway_alert + + custom + + + /usr/local/opnsense/scripts/OPNsense/Monit/gateway_alert + 300 + 30 +
+ + + + 56e67d76-cef6-4167-a51e-2c69a921ebc9 + + + + + Ping + NetworkPing + failed ping + alert + + + + NetworkLink + NetworkInterface + failed link + alert + + + + NetworkSaturation + NetworkInterface + saturation is greater than 75% + alert + + + + MemoryUsage + SystemResource + memory usage is greater than 75% + alert + + + + CPUUsage + SystemResource + cpu usage is greater than 75% + alert + + + + LoadAvg1 + SystemResource + loadavg (1min) is greater than 4 + alert + + + + LoadAvg5 + SystemResource + loadavg (5min) is greater than 3 + alert + + + + LoadAvg15 + SystemResource + loadavg (15min) is greater than 2 + alert + + + + SpaceUsage + SpaceUsage + space usage is greater than 75% + alert + + + + ChangedStatus + ProgramStatus + changed status + alert + + + + NonZeroStatus + ProgramStatus + status != 0 + alert + + + + + + + 1 + 1 + 31 + + + + + + + + + + + + 0 + 0 + 0 + 1 + 0 + + + + + + + + + + + 1 + 53 + 0 + + 0 + 0 + + 0 + 0 + + 0 + 0 + 0 + 0 + 0 + transparent + + 0 + + + 0 + 0 + 0 + 0 + 0 + 1 + 0 + + + 0 + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 1 + 0 + + 0.0.0.0/8,10.0.0.0/8,100.64.0.0/10,169.254.0.0/16,172.16.0.0/12,192.0.2.0/24,192.168.0.0/16,198.18.0.0/15,198.51.100.0/24,203.0.113.0/24,233.252.0.0/24,::1/128,2001:db8::/32,fc00::/8,fd00::/8,fe80::/10 + + + + + + + + + + + + + + 0 + + + + + allow + + + 0 + 0 + + + + + +
+ 0 + + + 0 + + + + + + + + 0 + 0 + + + 1400 + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 68a5faf1685db + Web GUI TLS certificate + + LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUhFakNDQlBxZ0F3SUJBZ0lVQlpQYjUwMXNaM3hhTTZzSDVUM1ZNRG9mcnlNd0RRWUpLb1pJaHZjTkFRRUwKQlFBd2dZWXhHakFZQmdOVkJBTU1FVTlRVG5ObGJuTmxMbWx1ZEdWeWJtRnNNUXN3Q1FZRFZRUUdFd0pPVERFVgpNQk1HQTFVRUNBd01XblZwWkMxSWIyeHNZVzVrTVJVd0V3WURWUVFIREF4TmFXUmtaV3hvWVhKdWFYTXhMVEFyCkJnTlZCQW9NSkU5UVRuTmxibk5sSUhObGJHWXRjMmxuYm1Wa0lIZGxZaUJqWlhKMGFXWnBZMkYwWlRBZUZ3MHkKTlRBNE1qQXhOalF5TWpaYUZ3MHlOakE1TWpFeE5qUXlNalphTUlHR01Sb3dHQVlEVlFRRERCRlBVRTV6Wlc1egpaUzVwYm5SbGNtNWhiREVMTUFrR0ExVUVCaE1DVGt3eEZUQVRCZ05WQkFnTURGcDFhV1F0U0c5c2JHRnVaREVWCk1CTUdBMVVFQnd3TVRXbGtaR1ZzYUdGeWJtbHpNUzB3S3dZRFZRUUtEQ1JQVUU1elpXNXpaU0J6Wld4bUxYTnAKWjI1bFpDQjNaV0lnWTJWeWRHbG1hV05oZEdVd2dnSWlNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUNEd0F3Z2dJSwpBb0lDQVFEQVozZk1lakdOckZYVFhZd24vTUp5YXBYMDdVOTVWZ0JERzZmWU1wZkZEU1NXeFRValRLRlNvR3JRCkpDb0ZyQ0NRQ3BsWnlua0ZjVFZvWVAraGszcndWWmxHZVFyL0RyR2ZiM1lPc2RGbEtublo5YzRTOGVrSkc2WTIKYWI0eFJTcnBja0hoTWQ4MHNENUFRdks1Skc2MS9UMEhNRXVMcGlraUF3MFpCempWbjlVUUpSRTJiS29kOW9IdgpEbG5DVGNTV1FUNWYrV3A0Sll0anVBVHBZMUlRVW4wbkxuWDBHUC9JRGtsWjFrdEZOczRPOGcrQmRVWFU0MUdvCjNGTFpCc1hQVm90WFVrRVl2R0ZldjJRMlBwSnNib25aWEpoR255amREUlZkZFZGOGhacGJua0wwU2xJTVFNeFQKSTRXK051ZmUvUTZGRDdmZnFRa0toemZ3SlJ2N0dGM2RTRlEwWldPUGNTZVZXY3lWTlVVMTMvcnBteUpvNXZhWQpYR000THcxb1d2c1FjUWxaUllnSkxhSWMzM0dnMGQySHhvZTIvdytIeFRXOEw4ZldqbzgxK250YWJXTlZhV0IwCnd6TXNFNGRBOWtxR2dmcWZjbm96ckovamJtNEFBTTB6QTlVZFh0SUJtRGdpeFNkVzB4eHNQNlZWRTdMdnlURTgKWnBJTjRoL1FCYURyc2hhRXJ6TXhUd01IZXJ1RlV4bFpxZEdSa3ErQXRJU1VwRE05VXB0YWZZMk5ydzgxVDFhSwoycFduVFlFQktCUnVwdk00TzFHNXN5NU5GZm13NFRTc0pqRUhtMFEvVTZ4ZU45bVg0OFhIYUZFNnhvQTJEZEMrCjJHS2lKWFlrYi9nZm13cmp1Y3NGWGpBS2tDNWF6ZXFqaERXeGxDbmJNS1YwSTQxOWp3SURBUUFCbzRJQmREQ0MKQVhBd0NRWURWUjBUQkFJd0FEQVJCZ2xnaGtnQmh2aENBUUVFQkFNQ0JrQXdOQVlKWUlaSUFZYjRRZ0VOQkNjVwpKVTlRVG5ObGJuTmxJRWRsYm1WeVlYUmxaQ0JUWlhKMlpYSWdRMlZ5ZEdsbWFXTmhkR1V3SFFZRFZSME9CQllFCkZMK2YzU0tCM0tMSi9nWStBUTJIZDBhTzVPRU1NSUd3QmdOVkhTTUVnYWd3Z2FXaGdZeWtnWWt3Z1lZeEdqQVkKQmdOVkJBTU1FVTlRVG5ObGJuTmxMbWx1ZEdWeWJtRnNNUXN3Q1FZRFZRUUdFd0pPVERFVk1CTUdBMVVFQ0F3TQpXblZwWkMxSWIyeHNZVzVrTVJVd0V3WURWUVFIREF4TmFXUmtaV3hvWVhKdWFYTXhMVEFyQmdOVkJBb01KRTlRClRuTmxibk5sSUhObGJHWXRjMmxuYm1Wa0lIZGxZaUJqWlhKMGFXWnBZMkYwWllJVUJaUGI1MDFzWjN4YU02c0gKNVQzVk1Eb2ZyeU13SFFZRFZSMGxCQll3RkFZSUt3WUJCUVVIQXdFR0NDc0dBUVVGQ0FJQ01Bc0dBMVVkRHdRRQpBd0lGb0RBY0JnTlZIUkVFRlRBVGdoRlBVRTV6Wlc1elpTNXBiblJsY201aGJEQU5CZ2txaGtpRzl3MEJBUXNGCkFBT0NBZ0VBY3F3VEN0RmVwWDlFOEFMYXRaWEE4dkN2b1YwZkxuaElPM0lWT2tZNEhYMTRRRU9NUGo4KzVXMXoKc3hyUGNscEJQVU5tbXJRb0hWUHBuWTRFM2hFanlYY1JWSzZtTEFrSkpRUDJPOEE2NFFpL3FNSkhCYUlEU0MzKwpqMHdkMGYyODRvcEppQ0F2UnF0SGh1bDd3akd4QzNZUXRxWTFMODNWUHBOWGRjOVVsZExTUGZ6WWluMlBPekMrClFDWU9qN3VQUDlCVGExTURudkdPdkdrOHdQeUJGaFZQWVFBWjYwb1ZaS2psOUI2R1piRzF1SG1ON3p3a3k0eEIKNk1RSlF3cHFscDdXQ09QSHJlZTFiVGZaMkJMZFQrZzJHakVMT0xNRGJMTHAzNUw0blBDUmNGRkJZNEszMHBncQpVWjdncEtNTmhuR3huQ29lR3dHUk54ZHFoZnNlT3FqaURVM0hBVDEya3FReU1vOEZNT1g1bG9EaVpXSGZTS0JrClVRaG5Tc1BTMG0vZ2dSRVgwNVQzYWM2NUxNOVVXMEs2MXppZUs5WFhTcjNXWlQ1TkhNV2JmU0VScUR4SGRtZ0YKeGt3YXkxZWpCZTFoZzdGMGpicTVDMGo1ZXB5ZDNOc1BTVUtDY2FDNi9aWTNkUHVYWVZZL0J2dnFsZHZJSzRBeAo0R1BLQ0xzaStGRjliSXZRWG4zbTV2KzJoQWF2WlpxcmJOTFUzVFQ0aDd2QllBOVRNcXpwd3lEaW5BR3RhaEE3CnhDSW5IU01kZXpQcnNkZDJrTW5TckhPdWtGeGdKb2lNZ3krVlJmdTdvZk9NMjhYQ1FySWF6djBFYmFhTU1ZMTcKRzlXOFd3SXZUb2lYY1ZWNTk3K1NUNHRsSTVIM3lDZFBtSk1kRm5GcDg1K2JzbW42MFQwPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + + LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUpRZ0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQ1N3d2dna29BZ0VBQW9JQ0FRREFaM2ZNZWpHTnJGWFQKWFl3bi9NSnlhcFgwN1U5NVZnQkRHNmZZTXBmRkRTU1d4VFVqVEtGU29HclFKQ29GckNDUUNwbFp5bmtGY1RWbwpZUCtoazNyd1ZabEdlUXIvRHJHZmIzWU9zZEZsS25uWjljNFM4ZWtKRzZZMmFiNHhSU3JwY2tIaE1kODBzRDVBClF2SzVKRzYxL1QwSE1FdUxwaWtpQXcwWkJ6alZuOVVRSlJFMmJLb2Q5b0h2RGxuQ1RjU1dRVDVmK1dwNEpZdGoKdUFUcFkxSVFVbjBuTG5YMEdQL0lEa2xaMWt0Rk5zNE84ZytCZFVYVTQxR28zRkxaQnNYUFZvdFhVa0VZdkdGZQp2MlEyUHBKc2JvblpYSmhHbnlqZERSVmRkVkY4aFpwYm5rTDBTbElNUU14VEk0VytOdWZlL1E2RkQ3ZmZxUWtLCmh6ZndKUnY3R0YzZFNGUTBaV09QY1NlVldjeVZOVVUxMy9ycG15Sm81dmFZWEdNNEx3MW9XdnNRY1FsWlJZZ0oKTGFJYzMzR2cwZDJIeG9lMi93K0h4VFc4TDhmV2pvODErbnRhYldOVmFXQjB3ek1zRTRkQTlrcUdnZnFmY25vegpySi9qYm00QUFNMHpBOVVkWHRJQm1EZ2l4U2RXMHh4c1A2VlZFN0x2eVRFOFpwSU40aC9RQmFEcnNoYUVyek14ClR3TUhlcnVGVXhsWnFkR1JrcStBdElTVXBETTlVcHRhZlkyTnJ3ODFUMWFLMnBXblRZRUJLQlJ1cHZNNE8xRzUKc3k1TkZmbXc0VFNzSmpFSG0wUS9VNnhlTjltWDQ4WEhhRkU2eG9BMkRkQysyR0tpSlhZa2IvZ2Ztd3JqdWNzRgpYakFLa0M1YXplcWpoRFd4bENuYk1LVjBJNDE5andJREFRQUJBb0lDQUNVVkRBTE4zalVXN09leTFPdDBES251Cm52NDRxcU9SRHJYZ1k2WUlnalhKUmE4RlRTdURmbWdsWU5EQzE1S0dUVFJWeHA2R3BuS0ZFaTBPM05Yd1RiWjYKV1BNN0t3SmplNXBsNmhRRTgzMlRCUzhiNzk2NDN4Z1JTeVNibHJ0NlFENEQ5bXlIcHlSSmY0WDFJVURMbzhiUgppdXlTdzB5ajlyT0djUVRNM29oVnFNUFcwUTF6UGdwT1UxYVdwbmdMY3dNZWlmNEhYUnpRNTUrTmZPemFacHVjCnVtQk4xUS81clhxS1BscmhNVnFpcUc0Nit3QVJjU2NKdE5oZHRsMzdyeTQ1Mk5zNGtERkxSVnowZUVUNEpGSmYKcjVQRUE5bEFuYWlVOS9RdVEwbERtcTlqdmpYRkNURXhYKy82SGJHK2RVd0Y2OEY3ZVEzVFQxbkhHK0hkMVJsbgpOWm1JM0p2d0Z1cG9JeU9VdlpJb3VGVmo2ak8ra0JLejkza1BHWmdMbnNmUUw5WDhRbTU3cjh4K3Z1eFNudGI1CjV4WVBxRkdrOWQrbDUwbTlQakdkekxGT3UwYnJ5TmQ4MFVMS2tuUlFtUVpvTngxck5GTUxpSjNlZENWUS9lclUKT1BDQ0Z0WEJMemJGTjR2ZzVWRjZMUkhvZGxqcEgxRzJOSXNoSzJhc1FuWS9RWDFpUUNLSk1tWERSUndMTWVsNQp3MUF4T2FqYVkzbWx2ZlRVd2xqdkE3a0tFUDBvZzRPeXZldDA2WTVRWk1EQXc1V00yT0pZVDVxcmFlYjZDbTdMCjlNckk4bG50TGp3WFVSZG4yU3U2RCtCWXNpcC9KK3BvOFNqYlJBaGJIc0lJbkJ1QWJnbGxqdTB2QXRXZmFkQlQKOTg4YnUwK3VUb1Q2T1Jkbk84Y1JBb0lCQVFEcStWYkVUQWVpSHN6K29jZnFWV3VwVHJrc2FNNm1wUUMwb0tqZApwb1FzWGVuTmNiNThHQ3FhWHkvdTJHWmNKUnR1QXRHamEyUVpEUUFUSjQyVTFmaTFhWm90Y053eXhPdmlud1NjCmVLZyt0ZGcwdW9LeGs2aXJKRFptaDBIK3Ewblg2RFJYK25RNDVmWVNmRkRFK0ZLd1lac0dQMkhYV3dKaVZ6OE0KU2NkL2pETTFRTWV2OXIzZWx1dS9DWFlvZ1N0N00wMklyczVoNjRuNjFmdVZjNHI4YmUwdFkrUTVsUnlwWk9NVwpkQ2VkWGFOV3RaNjF2bEFxamNiWkpkdXFBUjJjNzAyR3NML201TXA4Zmd3YmY2aG51TXJLaVlpQjlZalZxalc2CmYyUW1PclZtMUk0MFJBMC9OaFBTR2NXejBkNXZrdXY0VHUra2JFbERZTCsxaHY1M0FvSUJBUURSbnZaTmJaa1UKTXpmUTRLWEdML3dLUXJEbjNvL0RENWVBR1ZDTGcwTUkyYlAxYWpubHNVTjE4NCs1UWF6cVVOaWlZT3laODczeQpQYkw0cTBOZWFDYXdxby9WbjJMSkVIUFVTTVhUWjB4ckxTa1hPUjFuMDUwT2tDWXhVbFpOUXFvZU1xcHJGNXZLCm1NNlJxalN4NS8ydU9IUlR1SDRVV2RETEpwTDVUN2RpUCtXcFUwSDlSUWhrNDdkQUJaUjZEZjNxaDJEYmVxUWoKdWcxY0hWUVNqaldhUGpVZGlLR2dHemdvdlE2UkdNZDA1UVUzdkRMdzBCSkNPQ25XV2x0VXkvMW1jMUpPUHR2ZQp4UGltV2tRNmlkRHZ4RGZFRGg5U05zY1FPMnBTVjZxNnhCTWlqVGgvTldGN2NsOU1LYUhJWGxzTmt0RFVXWHZyCmNKRlM4eE1TcDhlcEFvSUJBUUNtWktVTjRxMHhIOUNZckdYT1Nta3Yvc0JnYzJPTFhLTXdSZWp1OVFENkRoTUgKMmZsREZUWHVGV1B6SmlqdUxaVE1CWkVBd1lhanVySUgzbVdETlRhbStMNG1XWnFGRlMvWlRqUk12YUNlcjlVSQpHZDk4OG94cGpQNDlBcUU0UDRIT00vQUZNU1ZtT1dwVTB0VzdkZ0hRUjM0cElXOGV1cUxva3RIaDJNaytTRURuCkFCV29SUGxWaTlncmN2N0tWaFk5YXlvSGxZb3VpMFl0YTZSNXc5VnpSa0REZU01Zi9Iak1kOVhieTZ0VjQ3NU0KSTliYzZvVUliVmVYNUJnMnZnMkRXVzZ6NTZ3dFRHMGJWWU1yWWU0V2JTU2w0bGpaZHM5TVJ2ay9OUUR0bFh0cAo4ekUwVDlCMXA4ekhabHE3S08zMFlyMVpIRVRWVVoxYjZrSTN3UDJuQW9JQkFFZ1VGKzlCMjF4RnpGQ0hucGtLClVPa2FTNGcvVUVHcmI5VzlYcVBLUzllVVBEd0wvY0tNZEh6dmRpRW1neFhESE9xZzExcU1wR2pTYkdMelNPUUMKZmlOTFV0QUswVVgvNFVSQ2pidUdqcEZmNHZ3NFNITTJJWkFyWXVhY3dFNHF1U0pQRzZoZFl0V0VPNnQ4MGtmRwpWTVYrWmdtUHE5TEZtM1R2VzZSY2s5czF5M3V3eEVVWllxeUdYTEduK1lrS25KL3pVd3ZGSFFHbjdRWWFrNWtaCnl6YXhZMFEzZ2hQeXFCbmlBRXRHTVBkeDlKeFltMCtRekdaMnQzUWNkOEV0cjRGMTcvdzF3eGJUdGdoRmk2WngKVXlYTzI3b1BmUmVnL0V3SmtpS2tRSEdlRUZKV0t2SWE0ZDAzMDZyMXVjcVRIMDRJaU1RcnpOK0ZRb002VC9tZgpOWmtDZ2dFQUsrRVJNVVdJZTE3V1k3VDIycy9lOEplN0xxSXlUcU9mSGovaWFUUjhHbXhqcU1HNEdod1RpVXJsCkh0Skhud3BMVGFjVmdYUjV3UmtYcEhRT2JqSUFzeVNBUGxwSzBvZUkyK2kvS0cyQjZ2U0cza0V2b1VZY0RlRk4KdzhHd0oxNDNTd21LQXM4eUtWMmd1RjhmRXNNVitEQzNzVHFlZXJmMy82bFprMUVCVFF0QTZqVHdqK0picXgwVgpaalZJUXBwUE8vc1VHdi9LZVE3MW5ockJpT0lXclJ0dDRTUDJ2aWx2em9DUTQxVjFqZ09wS3VwU3E1Y2J3VDRxCmp1bkJIMkx5VnNQaUc4M0Vha1JSUEhDK0craTk1MFJxckxVRUJOeVdHNGlMNTdUdU9xYVJuSmRnN2ZFb2lVLzMKNld4TjlvR2VRWjV0NjZkdTJEL01WSUZ4ZzJ1cXRBPT0KLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQo= + + + diff --git a/opnsense-config/src/tests/data/config-full-25.7.xml b/opnsense-config/src/tests/data/config-full-25.7.xml new file mode 100644 index 0000000..1cd4909 --- /dev/null +++ b/opnsense-config/src/tests/data/config-full-25.7.xml @@ -0,0 +1,826 @@ + + + opnsense + + + + + 115200 + serial + normal + OPNsense + internal + 1 + + + 1999 + admins + system + System Administrators + page-all + 0 + + + + 0 + root + 0 + system + + + + + $2y$10$YRVoF4SgskIsrXOvOQjGieB9XqHPRra9R7d80B3BZdbY/j21TwBfS + + + + + + + + System Administrator + + + Etc/UTC + 0.opnsense.pool.ntp.org 1.opnsense.pool.ntp.org 2.opnsense.pool.ntp.org 3.opnsense.pool.ntp.org + + https + 68a5faf1685db + + + + + + yes + 1 + 1 + 1 + 1 + 1 + 1 + 1 + hadp + hadp + hadp + + monthly + + 1 + 1 + + admins + 1 + + + + + + + + enabled + 1 + 1 + + -1 + -1 + + + + + + + 0 + + + en_US + + + + 1 + vtnet0 + + dhcp + dhcp6 + + + 0 + 1 + + + 0 + + + + + + 1 + vtnet1 + 192.168.1.1 + 24 + track6 + 64 + + + wan + 0 + + + 1 + Loopback + 1 + lo0 + 127.0.0.1 + ::1 + 8 + 128 + none + 1 + + + + 1 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + + lan + 0 + + + + + 0 + 0 + + + 1 + + + 0 + 1 + + 0 + 0 + + 1 + + lan + + 192.168.1.41 + 192.168.1.245 + + + + + + range + + 0 + + + + + + + + + + + + public + + + + automatic + + + + + pass + inet + Default allow LAN to any rule + lan + + lan + + + + + + + pass + inet6 + Default allow LAN IPv6 to any rule + lan + + lan + + + + + + + + + + + 0.opnsense.pool.ntp.org + + + root@192.168.1.5 + /system_advanced_admin.php made changes + + + + + + + + + 0 + + + + + + + + + 0 + 0 + + + + + + + 16 + 32 + 4 + 1000 + 1 + 0 + 0 + 0 + + + + + + + + 1 + 0 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + + + + + + + + + + 0 + + + + + + + 0 + 0 + + + ipsec + 0 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + v9 + + + + 0 + + 1800 + 15 + + + + + + + + + 0 + 0 + 0 + wan + 192.168.0.0/16,10.0.0.0/8,172.16.0.0/12 + + + W0D23 + 4 + + + + + + + 0 + 0 + 0 + + + + 0 + 0 + + + + 0 + 0 + 0 + + + + + + + + + + + + + + 0 + 127.0.0.1 + 8000 + + + + + 0 + 0 + + 4000 + 1 + raw + + + 0 + + 2 + + + + + + + + 0 + 0 + + 4000 + 1 + + + 0 + + 2 + + + + + + + + + + 0 + 120 + 120 + 127.0.0.1 + 25 + + + 0 + auto + 1 + + + + + 0 + root + + 2812 + + + 5 + 1 + + + 0 + root@localhost.local + 0 + + + + + + + 1 + $HOST + + system + + + + 300 + 30 +
+ + + + 02014be3-fc31-4af3-a0d5-061eaa67d28a,ebfd0d97-ae21-45d5-8b42-5220c75ce46f,d37f25f0-89e3-44b6-8ad2-280ac83a8904,37afd0d9-990c-4f03-a817-45691461e3d0 + + + + + 1 + RootFs + + filesystem + + + / + 300 + 30 +
+ + + + b44b859c-bc72-4c2e-82c9-4f56d84a5497 + + + + + 0 + carp_status_change + + custom + + + /usr/local/opnsense/scripts/OPNsense/Monit/carp_status + 300 + 30 +
+ + + + 0909801b-cd11-41c8-afeb-369396247308 + + + + + 0 + gateway_alert + + custom + + + /usr/local/opnsense/scripts/OPNsense/Monit/gateway_alert + 300 + 30 +
+ + + + 56e67d76-cef6-4167-a51e-2c69a921ebc9 + + + + + Ping + NetworkPing + failed ping + alert + + + + NetworkLink + NetworkInterface + failed link + alert + + + + NetworkSaturation + NetworkInterface + saturation is greater than 75% + alert + + + + MemoryUsage + SystemResource + memory usage is greater than 75% + alert + + + + CPUUsage + SystemResource + cpu usage is greater than 75% + alert + + + + LoadAvg1 + SystemResource + loadavg (1min) is greater than 4 + alert + + + + LoadAvg5 + SystemResource + loadavg (5min) is greater than 3 + alert + + + + LoadAvg15 + SystemResource + loadavg (15min) is greater than 2 + alert + + + + SpaceUsage + SpaceUsage + space usage is greater than 75% + alert + + + + ChangedStatus + ProgramStatus + changed status + alert + + + + NonZeroStatus + ProgramStatus + status != 0 + alert + + + + + + + 1 + 1 + 31 + + + + + + + + + + + + 0 + 0 + 0 + 1 + 0 + + + + + + + + + + + 1 + 53 + 0 + + 0 + 0 + + 0 + 0 + + 0 + 0 + 0 + 0 + 0 + transparent + + 0 + + + 0 + 0 + 0 + 0 + 0 + 1 + 0 + + + 0 + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 1 + 0 + + 0.0.0.0/8,10.0.0.0/8,100.64.0.0/10,169.254.0.0/16,172.16.0.0/12,192.0.2.0/24,192.168.0.0/16,198.18.0.0/15,198.51.100.0/24,203.0.113.0/24,233.252.0.0/24,::1/128,2001:db8::/32,fc00::/8,fd00::/8,fe80::/10 + + + + + + + + + + + + + + 0 + + + + + allow + + + 0 + 0 + + + + + +
+ 0 + + + 0 + + + + + + + + 0 + 0 + + + 1400 + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 68a5faf1685db + Web GUI TLS certificate + + LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUhFakNDQlBxZ0F3SUJBZ0lVQlpQYjUwMXNaM3hhTTZzSDVUM1ZNRG9mcnlNd0RRWUpLb1pJaHZjTkFRRUwKQlFBd2dZWXhHakFZQmdOVkJBTU1FVTlRVG5ObGJuTmxMbWx1ZEdWeWJtRnNNUXN3Q1FZRFZRUUdFd0pPVERFVgpNQk1HQTFVRUNBd01XblZwWkMxSWIyeHNZVzVrTVJVd0V3WURWUVFIREF4TmFXUmtaV3hvWVhKdWFYTXhMVEFyCkJnTlZCQW9NSkU5UVRuTmxibk5sSUhObGJHWXRjMmxuYm1Wa0lIZGxZaUJqWlhKMGFXWnBZMkYwWlRBZUZ3MHkKTlRBNE1qQXhOalF5TWpaYUZ3MHlOakE1TWpFeE5qUXlNalphTUlHR01Sb3dHQVlEVlFRRERCRlBVRTV6Wlc1egpaUzVwYm5SbGNtNWhiREVMTUFrR0ExVUVCaE1DVGt3eEZUQVRCZ05WQkFnTURGcDFhV1F0U0c5c2JHRnVaREVWCk1CTUdBMVVFQnd3TVRXbGtaR1ZzYUdGeWJtbHpNUzB3S3dZRFZRUUtEQ1JQVUU1elpXNXpaU0J6Wld4bUxYTnAKWjI1bFpDQjNaV0lnWTJWeWRHbG1hV05oZEdVd2dnSWlNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUNEd0F3Z2dJSwpBb0lDQVFEQVozZk1lakdOckZYVFhZd24vTUp5YXBYMDdVOTVWZ0JERzZmWU1wZkZEU1NXeFRValRLRlNvR3JRCkpDb0ZyQ0NRQ3BsWnlua0ZjVFZvWVAraGszcndWWmxHZVFyL0RyR2ZiM1lPc2RGbEtublo5YzRTOGVrSkc2WTIKYWI0eFJTcnBja0hoTWQ4MHNENUFRdks1Skc2MS9UMEhNRXVMcGlraUF3MFpCempWbjlVUUpSRTJiS29kOW9IdgpEbG5DVGNTV1FUNWYrV3A0Sll0anVBVHBZMUlRVW4wbkxuWDBHUC9JRGtsWjFrdEZOczRPOGcrQmRVWFU0MUdvCjNGTFpCc1hQVm90WFVrRVl2R0ZldjJRMlBwSnNib25aWEpoR255amREUlZkZFZGOGhacGJua0wwU2xJTVFNeFQKSTRXK051ZmUvUTZGRDdmZnFRa0toemZ3SlJ2N0dGM2RTRlEwWldPUGNTZVZXY3lWTlVVMTMvcnBteUpvNXZhWQpYR000THcxb1d2c1FjUWxaUllnSkxhSWMzM0dnMGQySHhvZTIvdytIeFRXOEw4ZldqbzgxK250YWJXTlZhV0IwCnd6TXNFNGRBOWtxR2dmcWZjbm96ckovamJtNEFBTTB6QTlVZFh0SUJtRGdpeFNkVzB4eHNQNlZWRTdMdnlURTgKWnBJTjRoL1FCYURyc2hhRXJ6TXhUd01IZXJ1RlV4bFpxZEdSa3ErQXRJU1VwRE05VXB0YWZZMk5ydzgxVDFhSwoycFduVFlFQktCUnVwdk00TzFHNXN5NU5GZm13NFRTc0pqRUhtMFEvVTZ4ZU45bVg0OFhIYUZFNnhvQTJEZEMrCjJHS2lKWFlrYi9nZm13cmp1Y3NGWGpBS2tDNWF6ZXFqaERXeGxDbmJNS1YwSTQxOWp3SURBUUFCbzRJQmREQ0MKQVhBd0NRWURWUjBUQkFJd0FEQVJCZ2xnaGtnQmh2aENBUUVFQkFNQ0JrQXdOQVlKWUlaSUFZYjRRZ0VOQkNjVwpKVTlRVG5ObGJuTmxJRWRsYm1WeVlYUmxaQ0JUWlhKMlpYSWdRMlZ5ZEdsbWFXTmhkR1V3SFFZRFZSME9CQllFCkZMK2YzU0tCM0tMSi9nWStBUTJIZDBhTzVPRU1NSUd3QmdOVkhTTUVnYWd3Z2FXaGdZeWtnWWt3Z1lZeEdqQVkKQmdOVkJBTU1FVTlRVG5ObGJuTmxMbWx1ZEdWeWJtRnNNUXN3Q1FZRFZRUUdFd0pPVERFVk1CTUdBMVVFQ0F3TQpXblZwWkMxSWIyeHNZVzVrTVJVd0V3WURWUVFIREF4TmFXUmtaV3hvWVhKdWFYTXhMVEFyQmdOVkJBb01KRTlRClRuTmxibk5sSUhObGJHWXRjMmxuYm1Wa0lIZGxZaUJqWlhKMGFXWnBZMkYwWllJVUJaUGI1MDFzWjN4YU02c0gKNVQzVk1Eb2ZyeU13SFFZRFZSMGxCQll3RkFZSUt3WUJCUVVIQXdFR0NDc0dBUVVGQ0FJQ01Bc0dBMVVkRHdRRQpBd0lGb0RBY0JnTlZIUkVFRlRBVGdoRlBVRTV6Wlc1elpTNXBiblJsY201aGJEQU5CZ2txaGtpRzl3MEJBUXNGCkFBT0NBZ0VBY3F3VEN0RmVwWDlFOEFMYXRaWEE4dkN2b1YwZkxuaElPM0lWT2tZNEhYMTRRRU9NUGo4KzVXMXoKc3hyUGNscEJQVU5tbXJRb0hWUHBuWTRFM2hFanlYY1JWSzZtTEFrSkpRUDJPOEE2NFFpL3FNSkhCYUlEU0MzKwpqMHdkMGYyODRvcEppQ0F2UnF0SGh1bDd3akd4QzNZUXRxWTFMODNWUHBOWGRjOVVsZExTUGZ6WWluMlBPekMrClFDWU9qN3VQUDlCVGExTURudkdPdkdrOHdQeUJGaFZQWVFBWjYwb1ZaS2psOUI2R1piRzF1SG1ON3p3a3k0eEIKNk1RSlF3cHFscDdXQ09QSHJlZTFiVGZaMkJMZFQrZzJHakVMT0xNRGJMTHAzNUw0blBDUmNGRkJZNEszMHBncQpVWjdncEtNTmhuR3huQ29lR3dHUk54ZHFoZnNlT3FqaURVM0hBVDEya3FReU1vOEZNT1g1bG9EaVpXSGZTS0JrClVRaG5Tc1BTMG0vZ2dSRVgwNVQzYWM2NUxNOVVXMEs2MXppZUs5WFhTcjNXWlQ1TkhNV2JmU0VScUR4SGRtZ0YKeGt3YXkxZWpCZTFoZzdGMGpicTVDMGo1ZXB5ZDNOc1BTVUtDY2FDNi9aWTNkUHVYWVZZL0J2dnFsZHZJSzRBeAo0R1BLQ0xzaStGRjliSXZRWG4zbTV2KzJoQWF2WlpxcmJOTFUzVFQ0aDd2QllBOVRNcXpwd3lEaW5BR3RhaEE3CnhDSW5IU01kZXpQcnNkZDJrTW5TckhPdWtGeGdKb2lNZ3krVlJmdTdvZk9NMjhYQ1FySWF6djBFYmFhTU1ZMTcKRzlXOFd3SXZUb2lYY1ZWNTk3K1NUNHRsSTVIM3lDZFBtSk1kRm5GcDg1K2JzbW42MFQwPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + + LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUpRZ0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQ1N3d2dna29BZ0VBQW9JQ0FRREFaM2ZNZWpHTnJGWFQKWFl3bi9NSnlhcFgwN1U5NVZnQkRHNmZZTXBmRkRTU1d4VFVqVEtGU29HclFKQ29GckNDUUNwbFp5bmtGY1RWbwpZUCtoazNyd1ZabEdlUXIvRHJHZmIzWU9zZEZsS25uWjljNFM4ZWtKRzZZMmFiNHhSU3JwY2tIaE1kODBzRDVBClF2SzVKRzYxL1QwSE1FdUxwaWtpQXcwWkJ6alZuOVVRSlJFMmJLb2Q5b0h2RGxuQ1RjU1dRVDVmK1dwNEpZdGoKdUFUcFkxSVFVbjBuTG5YMEdQL0lEa2xaMWt0Rk5zNE84ZytCZFVYVTQxR28zRkxaQnNYUFZvdFhVa0VZdkdGZQp2MlEyUHBKc2JvblpYSmhHbnlqZERSVmRkVkY4aFpwYm5rTDBTbElNUU14VEk0VytOdWZlL1E2RkQ3ZmZxUWtLCmh6ZndKUnY3R0YzZFNGUTBaV09QY1NlVldjeVZOVVUxMy9ycG15Sm81dmFZWEdNNEx3MW9XdnNRY1FsWlJZZ0oKTGFJYzMzR2cwZDJIeG9lMi93K0h4VFc4TDhmV2pvODErbnRhYldOVmFXQjB3ek1zRTRkQTlrcUdnZnFmY25vegpySi9qYm00QUFNMHpBOVVkWHRJQm1EZ2l4U2RXMHh4c1A2VlZFN0x2eVRFOFpwSU40aC9RQmFEcnNoYUVyek14ClR3TUhlcnVGVXhsWnFkR1JrcStBdElTVXBETTlVcHRhZlkyTnJ3ODFUMWFLMnBXblRZRUJLQlJ1cHZNNE8xRzUKc3k1TkZmbXc0VFNzSmpFSG0wUS9VNnhlTjltWDQ4WEhhRkU2eG9BMkRkQysyR0tpSlhZa2IvZ2Ztd3JqdWNzRgpYakFLa0M1YXplcWpoRFd4bENuYk1LVjBJNDE5andJREFRQUJBb0lDQUNVVkRBTE4zalVXN09leTFPdDBES251Cm52NDRxcU9SRHJYZ1k2WUlnalhKUmE4RlRTdURmbWdsWU5EQzE1S0dUVFJWeHA2R3BuS0ZFaTBPM05Yd1RiWjYKV1BNN0t3SmplNXBsNmhRRTgzMlRCUzhiNzk2NDN4Z1JTeVNibHJ0NlFENEQ5bXlIcHlSSmY0WDFJVURMbzhiUgppdXlTdzB5ajlyT0djUVRNM29oVnFNUFcwUTF6UGdwT1UxYVdwbmdMY3dNZWlmNEhYUnpRNTUrTmZPemFacHVjCnVtQk4xUS81clhxS1BscmhNVnFpcUc0Nit3QVJjU2NKdE5oZHRsMzdyeTQ1Mk5zNGtERkxSVnowZUVUNEpGSmYKcjVQRUE5bEFuYWlVOS9RdVEwbERtcTlqdmpYRkNURXhYKy82SGJHK2RVd0Y2OEY3ZVEzVFQxbkhHK0hkMVJsbgpOWm1JM0p2d0Z1cG9JeU9VdlpJb3VGVmo2ak8ra0JLejkza1BHWmdMbnNmUUw5WDhRbTU3cjh4K3Z1eFNudGI1CjV4WVBxRkdrOWQrbDUwbTlQakdkekxGT3UwYnJ5TmQ4MFVMS2tuUlFtUVpvTngxck5GTUxpSjNlZENWUS9lclUKT1BDQ0Z0WEJMemJGTjR2ZzVWRjZMUkhvZGxqcEgxRzJOSXNoSzJhc1FuWS9RWDFpUUNLSk1tWERSUndMTWVsNQp3MUF4T2FqYVkzbWx2ZlRVd2xqdkE3a0tFUDBvZzRPeXZldDA2WTVRWk1EQXc1V00yT0pZVDVxcmFlYjZDbTdMCjlNckk4bG50TGp3WFVSZG4yU3U2RCtCWXNpcC9KK3BvOFNqYlJBaGJIc0lJbkJ1QWJnbGxqdTB2QXRXZmFkQlQKOTg4YnUwK3VUb1Q2T1Jkbk84Y1JBb0lCQVFEcStWYkVUQWVpSHN6K29jZnFWV3VwVHJrc2FNNm1wUUMwb0tqZApwb1FzWGVuTmNiNThHQ3FhWHkvdTJHWmNKUnR1QXRHamEyUVpEUUFUSjQyVTFmaTFhWm90Y053eXhPdmlud1NjCmVLZyt0ZGcwdW9LeGs2aXJKRFptaDBIK3Ewblg2RFJYK25RNDVmWVNmRkRFK0ZLd1lac0dQMkhYV3dKaVZ6OE0KU2NkL2pETTFRTWV2OXIzZWx1dS9DWFlvZ1N0N00wMklyczVoNjRuNjFmdVZjNHI4YmUwdFkrUTVsUnlwWk9NVwpkQ2VkWGFOV3RaNjF2bEFxamNiWkpkdXFBUjJjNzAyR3NML201TXA4Zmd3YmY2aG51TXJLaVlpQjlZalZxalc2CmYyUW1PclZtMUk0MFJBMC9OaFBTR2NXejBkNXZrdXY0VHUra2JFbERZTCsxaHY1M0FvSUJBUURSbnZaTmJaa1UKTXpmUTRLWEdML3dLUXJEbjNvL0RENWVBR1ZDTGcwTUkyYlAxYWpubHNVTjE4NCs1UWF6cVVOaWlZT3laODczeQpQYkw0cTBOZWFDYXdxby9WbjJMSkVIUFVTTVhUWjB4ckxTa1hPUjFuMDUwT2tDWXhVbFpOUXFvZU1xcHJGNXZLCm1NNlJxalN4NS8ydU9IUlR1SDRVV2RETEpwTDVUN2RpUCtXcFUwSDlSUWhrNDdkQUJaUjZEZjNxaDJEYmVxUWoKdWcxY0hWUVNqaldhUGpVZGlLR2dHemdvdlE2UkdNZDA1UVUzdkRMdzBCSkNPQ25XV2x0VXkvMW1jMUpPUHR2ZQp4UGltV2tRNmlkRHZ4RGZFRGg5U05zY1FPMnBTVjZxNnhCTWlqVGgvTldGN2NsOU1LYUhJWGxzTmt0RFVXWHZyCmNKRlM4eE1TcDhlcEFvSUJBUUNtWktVTjRxMHhIOUNZckdYT1Nta3Yvc0JnYzJPTFhLTXdSZWp1OVFENkRoTUgKMmZsREZUWHVGV1B6SmlqdUxaVE1CWkVBd1lhanVySUgzbVdETlRhbStMNG1XWnFGRlMvWlRqUk12YUNlcjlVSQpHZDk4OG94cGpQNDlBcUU0UDRIT00vQUZNU1ZtT1dwVTB0VzdkZ0hRUjM0cElXOGV1cUxva3RIaDJNaytTRURuCkFCV29SUGxWaTlncmN2N0tWaFk5YXlvSGxZb3VpMFl0YTZSNXc5VnpSa0REZU01Zi9Iak1kOVhieTZ0VjQ3NU0KSTliYzZvVUliVmVYNUJnMnZnMkRXVzZ6NTZ3dFRHMGJWWU1yWWU0V2JTU2w0bGpaZHM5TVJ2ay9OUUR0bFh0cAo4ekUwVDlCMXA4ekhabHE3S08zMFlyMVpIRVRWVVoxYjZrSTN3UDJuQW9JQkFFZ1VGKzlCMjF4RnpGQ0hucGtLClVPa2FTNGcvVUVHcmI5VzlYcVBLUzllVVBEd0wvY0tNZEh6dmRpRW1neFhESE9xZzExcU1wR2pTYkdMelNPUUMKZmlOTFV0QUswVVgvNFVSQ2pidUdqcEZmNHZ3NFNITTJJWkFyWXVhY3dFNHF1U0pQRzZoZFl0V0VPNnQ4MGtmRwpWTVYrWmdtUHE5TEZtM1R2VzZSY2s5czF5M3V3eEVVWllxeUdYTEduK1lrS25KL3pVd3ZGSFFHbjdRWWFrNWtaCnl6YXhZMFEzZ2hQeXFCbmlBRXRHTVBkeDlKeFltMCtRekdaMnQzUWNkOEV0cjRGMTcvdzF3eGJUdGdoRmk2WngKVXlYTzI3b1BmUmVnL0V3SmtpS2tRSEdlRUZKV0t2SWE0ZDAzMDZyMXVjcVRIMDRJaU1RcnpOK0ZRb002VC9tZgpOWmtDZ2dFQUsrRVJNVVdJZTE3V1k3VDIycy9lOEplN0xxSXlUcU9mSGovaWFUUjhHbXhqcU1HNEdod1RpVXJsCkh0Skhud3BMVGFjVmdYUjV3UmtYcEhRT2JqSUFzeVNBUGxwSzBvZUkyK2kvS0cyQjZ2U0cza0V2b1VZY0RlRk4KdzhHd0oxNDNTd21LQXM4eUtWMmd1RjhmRXNNVitEQzNzVHFlZXJmMy82bFprMUVCVFF0QTZqVHdqK0picXgwVgpaalZJUXBwUE8vc1VHdi9LZVE3MW5ockJpT0lXclJ0dDRTUDJ2aWx2em9DUTQxVjFqZ09wS3VwU3E1Y2J3VDRxCmp1bkJIMkx5VnNQaUc4M0Vha1JSUEhDK0craTk1MFJxckxVRUJOeVdHNGlMNTdUdU9xYVJuSmRnN2ZFb2lVLzMKNld4TjlvR2VRWjV0NjZkdTJEL01WSUZ4ZzJ1cXRBPT0KLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQo= + + + diff --git a/opnsense-config/src/tests/data/config-full-ncd0.xml b/opnsense-config/src/tests/data/config-full-ncd0.xml new file mode 100644 index 0000000..9243cf2 --- /dev/null +++ b/opnsense-config/src/tests/data/config-full-ncd0.xml @@ -0,0 +1,2572 @@ + + + opnsense + + + vfs.read_max + + Increase UFS read-ahead speeds to match the state of hard drives and NCQ. + + + net.inet.ip.portrange.first + + Set the ephemeral port range to be lower. + + + net.inet.tcp.blackhole + + Drop packets to closed TCP ports without returning a RST + + + net.inet.udp.blackhole + + Do not send ICMP port unreachable messages for closed UDP ports + + + net.inet.ip.random_id + + Randomize the ID field in IP packets + + + net.inet.ip.sourceroute + + Source routing is another way for an attacker to try to reach non-routable addresses behind your box. + It can also be used to probe for information about your internal networks. These functions come enabled + as part of the standard FreeBSD core system. + + + net.inet.ip.accept_sourceroute + + Source routing is another way for an attacker to try to reach non-routable addresses behind your box. + It can also be used to probe for information about your internal networks. These functions come enabled + as part of the standard FreeBSD core system. + + + net.inet.icmp.log_redirect + + This option turns off the logging of redirect packets because there is no limit and this could fill + up your logs consuming your whole hard drive. + + + net.inet.tcp.drop_synfin + + Drop SYN-FIN packets (breaks RFC1379, but nobody uses it anyway) + + + net.inet6.ip6.redirect + + Enable sending IPv6 redirects + + + net.inet6.ip6.use_tempaddr + + Enable privacy settings for IPv6 (RFC 4941) + + + net.inet6.ip6.prefer_tempaddr + + Prefer privacy addresses and use them over the normal addresses + + + net.inet.tcp.syncookies + + Generate SYN cookies for outbound SYN-ACK packets + + + net.inet.tcp.recvspace + + Maximum incoming/outgoing TCP datagram size (receive) + + + net.inet.tcp.sendspace + + Maximum incoming/outgoing TCP datagram size (send) + + + net.inet.tcp.delayed_ack + + Do not delay ACK to try and piggyback it onto a data packet + + + net.inet.udp.maxdgram + + Maximum outgoing UDP datagram size + + + net.link.bridge.pfil_onlyip + + Handling of non-IP packets which are not passed to pfil (see if_bridge(4)) + + + net.link.bridge.pfil_local_phys + + Set to 1 to additionally filter on the physical interface for locally destined packets + + + net.link.bridge.pfil_member + + Set to 0 to disable filtering on the incoming and outgoing member interfaces. + + + net.link.bridge.pfil_bridge + + Set to 1 to enable filtering on the bridge interface + + + net.link.tap.user_open + + Allow unprivileged access to tap(4) device nodes + + + kern.randompid + + Randomize PID's (see src/sys/kern/kern_fork.c: sysctl_kern_randompid()) + + + hw.syscons.kbd_reboot + + Disable CTRL+ALT+Delete reboot from keyboard. + + + net.inet.tcp.log_debug + + Enable TCP extended debugging + + + net.inet.icmp.icmplim + + Set ICMP Limits + + + net.inet.tcp.tso + + TCP Offload Engine + + + net.inet.udp.checksum + + UDP Checksums + + + kern.ipc.maxsockbuf + + Maximum socket buffer size + + + vm.pmap.pti + + Page Table Isolation (Meltdown mitigation, requires reboot.) + + + hw.ibrs_disable + + Disable Indirect Branch Restricted Speculation (Spectre V2 mitigation) + + + security.bsd.see_other_gids + + Hide processes running as other groups + + + security.bsd.see_other_uids + + Hide processes running as other users + + + net.inet.ip.redirect + + Enable/disable sending of ICMP redirects in response to IP packets for which a better, + and for the sender directly reachable, route and next hop is known. + + + net.local.dgram.maxdgram + + Maximum outgoing UDP datagram size + + + + 115200 + video + normal + fw0 + ncd0.harmony.mcd + + 1999 + admins + system + System Administrators + page-all + 0 + + + 0 + root + 0 + system + + + + + $2y$10$YRVoF4SgskIsrXOvOQjGieB9XqHPRra9R7d80B3BZdbY/j21TwBfS + + + + + + + + System Administrator + + + 2000 + 2000 + America/Toronto + 0.opnsense.pool.ntp.org 1.opnsense.pool.ntp.org 2.opnsense.pool.ntp.org 3.opnsense.pool.ntp.org + + https + 6796970f3b58c + 8000 + + + + + 1 + yes + 1 + 1 + 1 + 1 + 1 + 1 + hadp + hadp + hadp + + monthly + + 1 + 1 + + admins + 1 + + + + + + enabled + 1 + + 1 + + -1 + -1 + + + + os-caddy,os-haproxy,os-tftp + + + + + en_US + 1.1.1.1 + 8.8.8.8 + none + none + none + none + none + none + none + none + 1 + + + + + igc3 + 1 + dhcp + dhcp6 + 0 + WAN_GW + + + WAN + + + igc0 + 1 + 192.168.33.1 + 24 + + + + + + + LAN + + + 1 + lo0 + Loopback + 1 + 127.0.0.1 + none + 1 + 8 + ::1 + 128 + + + 1 + WireGuard (Group) + wireguard + 1 + 1 + group + + + + wg0 + ncd0 + 1 + 1 + + + + + + 1 + 192.168.33.1 + 1 + 192.168.33.1 + ipxe.efi + undionly.kpxe + http://192.168.33.1:8080/boot.ipxe + hmac-md5 + + + + + 192.168.33.10 + 192.168.33.245 + + + + + + c4:62:37:02:60:fa + 192.168.33.20 + cp0 + + + + + + c4:62:37:02:61:1a + 192.168.33.21 + cp1 + + + + + + c4:62:37:01:bc:68 + 192.168.33.22 + cp2 + + + + + + c4:62:37:02:61:0f + 192.168.33.30 + wk0 + + + + + + c4:62:37:02:61:70 + 192.168.33.32 + wk2 + + + + + + c4:62:37:02:61:26 + 192.168.33.31 + wk1 + + + + + + + + + + + public + + + + + automatic + + + + + pass + wan + inet + keep state + Allow inbound traffic to wireguard + in + wireguard + 1 + udp + + 1 + + + wanip + 51821 + + + root@192.168.33.5 + + /firewall_rules_edit.php made changes + + + root@192.168.33.5 + + /firewall_rules_edit.php made changes + + + + pass + lan + inet + Default allow LAN to any rule + + lan + + + + + + + pass + lan + inet6 + Default allow LAN IPv6 to any rule + + lan + + + + + + + pass + opt1 + inet + keep state + Allow wireguard clients to use this interface for NAT + in + 1 + + opt1 + + + 1 + + + root@192.168.33.5 + + /firewall_rules_edit.php made changes + + + root@192.168.33.5 + + /firewall_rules_edit.php made changes + + + + + wireguard + any + any + 24 + any + 24 + 1380 + Wireguard MSS Clamping IPv4 + + root@192.168.33.5 + + /firewall_scrub_edit.php made changes + + + root@192.168.33.5 + + /firewall_scrub_edit.php made changes + + + + + + + + + 0.opnsense.pool.ntp.org + + + system_information-container:00000000-col3:show,services_status-container:00000001-col4:show,gateways-container:00000002-col4:show,interface_list-container:00000003-col4:show + 2 + + + root@172.33.0.3 + /api/wireguard/general/set made changes + + + + + + + + + + + + + + + v9 + + + + 0 + + 1800 + 15 + + + + + + + + + wireguard + 1 + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 0 + wan + 192.168.0.0/16,10.0.0.0/8,172.16.0.0/12 + + + W0D23 + 4 + + + + + + + 0 + 0 + 0 + + + + 0 + 0 + + + + 0 + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + + 16 + 32 + 4 + 1000 + 1 + 0 + 0 + + + + + + + + + 1 + 0 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + + + + + + + + + + + 0 + + + + + + + 0 + 0 + + + ipsec + 0 + 1 + + + + + + + + + + + + + 0 + 127.0.0.1 + 8000 + + + + + 0 + + 4000 + 1 + raw + + + 0 + + 2 + + + + + + + + + 0 + 120 + 120 + 127.0.0.1 + 25 + + + 0 + auto + 1 + + + + + 0 + root + + 2812 + + + 5 + 1 + + + 0 + root@localhost.local + 0 + + + + + + + 1 + $HOST + + system + + + + 300 + 30 +
+ + + + b9a4410c-bc95-4de6-ac83-567dd8cf60c0,c1ec5aac-2fda-45f5-a0d8-9bea7db470d6,f1a4fa3a-65a2-43da-b6e0-18b0361ac43b,76c03880-d148-413e-8097-3ee0aa33cb2c + + + + + 1 + RootFs + + filesystem + + + / + 300 + 30 +
+ + + + f24e0d56-0445-4e5f-9b58-3af0f246b80d + + + + + 0 + carp_status_change + + custom + + + /usr/local/opnsense/scripts/OPNsense/Monit/carp_status + 300 + 30 +
+ + + + f48cfab6-de1e-4006-bcd7-c8f8990d25d6 + + + + + 0 + gateway_alert + + custom + + + /usr/local/opnsense/scripts/OPNsense/Monit/gateway_alert + 300 + 30 +
+ + + + 5e0dc1c7-90ac-48cc-944e-e0b20c482656 + + + + + Ping + NetworkPing + failed ping + alert + + + + NetworkLink + NetworkInterface + failed link + alert + + + + NetworkSaturation + NetworkInterface + saturation is greater than 75% + alert + + + + MemoryUsage + SystemResource + memory usage is greater than 75% + alert + + + + CPUUsage + SystemResource + cpu usage is greater than 75% + alert + + + + LoadAvg1 + SystemResource + loadavg (1min) is greater than 8 + alert + + + + LoadAvg5 + SystemResource + loadavg (5min) is greater than 6 + alert + + + + LoadAvg15 + SystemResource + loadavg (15min) is greater than 4 + alert + + + + SpaceUsage + SpaceUsage + space usage is greater than 75% + alert + + + + ChangedStatus + ProgramStatus + changed status + alert + + + + NonZeroStatus + ProgramStatus + status != 0 + alert + + + + + + + + + 1 + 1 + 31 + + + + + + + + + + + + 1 + 53 + + + + + + + 1 + + 1 + + + + + transparent + + + + + 0 + 0 + 0 + 0 + 0 + 1 + 0 + + + 0 + + 0 + 0 + 0 + 0 + 0 + + + 1 + 0 + + 0.0.0.0/8,10.0.0.0/8,100.64.0.0/10,169.254.0.0/16,172.16.0.0/12,192.0.2.0/24,192.168.0.0/16,198.18.0.0/15,198.51.100.0/24,203.0.113.0/24,233.252.0.0/24,::1/128,2001:db8::/32,fc00::/8,fd00::/8,fe80::/10 + + + + + + + + + + + + + + + + + + + allow + + + 0 + + + + + + +
+ 0 + + + 0 + + + + + 1 + api + ncd0.harmony.mcd + A + + + + 192.168.33.1 + + + 1 + api-int + ncd0.harmony.mcd + A + + + + 192.168.33.1 + + + 1 + * + apps.ncd0.harmony.mcd + A + + + + 192.168.33.1 + + + + + + + + 0 + 0 + 0 + 1 + 0 + + + + + + + + + + + 1 + 192.168.33.1 + + + + + 1 + + + + + 1 + ncd0 + 0 + PFmk/jbPetnRN1r+eyu4yg8UWnexz1UeYFUHIa6j4XA= + aM7qq2KOKpqhrR/PM1SUob9O6c+YeOKhLk9SeLGrKFo= + 51821 + + + 172.33.0.1/24 + 0 + + + 28494fc4-f207-449e-9a52-9aa2084ac631,3ec7612c-20cf-446b-a9fe-c06478eec225,e97ec826-3256-4544-95a5-90bb6a36da03 + ncd0.nationtech.io:51821 + 192.168.33.1,ncd0.harmony.mcd + + + + + + + 1 + jg-liliane2 + T+AMURuCUmkTnZln7bLzpJTBaUzbvBFIgyIKix1RByI= + + 172.33.0.3/24 + + + + + + 1 + ianletourneau + OLH36I5q6uFWKiiKuV63QzKarsUuc+YKzSh25H3+cmY= + + 172.33.0.2/32 + + + + + + 1 + wrolleman + qExmit/3m6QamjJ2azpYRvKVFtWsbjRpxlvrveLoYWQ= + + 172.33.0.4/32 + + + + + + + + + + + + + + + + + + + + + + + + 0 + WAN_GW + WAN Gateway + wan + inet + + 1 + + 1 + + + + 255 + 1 + + + + + + + + + + + + + 1 + 0 + 60s + + 0 + 0 + 1 + + 0 + + + 1024 + + + 1024 + + + 0 + + 1 + ipv4 + ignore + 2048 + 16384 + 2 + 0 + 0 + + 0 + 300 + 3600 + 0 + prefer-client-ciphers + TLSv1.2 + + ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256 + TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 + + + + + + + + + + + 30s + 30s + + 30s + 3 + x-1 + last,libc + + + + 127.0.0.1 + local0 + info + + + + 0 + 8822 + 0 + + 0 + + + + + 0 + *:8404 + /metrics + + + 0 + 4 + 60 + + 0 + 10 + + + + + 894e544289f1d829.3f0193d7 + 1 + frontend_192.168.33.1:80 + + 192.168.33.1:80 + + tcp + b58293a8-01a7-400b-b1a6-f6c598a198cf + 0 + + + + 0 + + + + + + 0 + 0 + 0 + 0 + 0 + + + + 0 + + + + + + + + + 0 + 0 + 0 + 0 + 0 + + + + + 0 + + + + + + + + + 0 + 0 + + 0 + 0 + + + + + + + + 1fdcee9ce32000ee.6720f746 + 1 + frontend_192.168.33.1:443 + + 192.168.33.1:443 + + tcp + 2ce6372e-30d8-4808-8683-7ff059b346b3 + 0 + + + + 0 + + + + + + 0 + 0 + 0 + 0 + 0 + + + + 0 + + + + + + + + + 0 + 0 + 0 + 0 + 0 + + + + + 0 + + + + + + + + + 0 + 0 + + 0 + 0 + + + + + + + + b2b0821e71423fd9.bb87bdb2 + 1 + frontend_192.168.33.1:22623 + + 192.168.33.1:22623 + + tcp + 284a9201-f139-4622-809b-f876a5812d61 + 0 + + + + 0 + + + + + + 0 + 0 + 0 + 0 + 0 + + + + 0 + + + + + + + + + 0 + 0 + 0 + 0 + 0 + + + + + 0 + + + + + + + + + 0 + 0 + + 0 + 0 + + + + + + + + 98fdab464008b9d0.26152082 + 1 + frontend_192.168.33.1:6443 + + 192.168.33.1:6443 + + tcp + 48380579-d54b-41fd-91b7-22f1a065be10 + 0 + + + + 0 + + + + + + 0 + 0 + 0 + 0 + 0 + + + + 0 + + + + + + + + + 0 + 0 + 0 + 0 + 0 + + + + + 0 + + + + + + + + + 0 + 0 + + 0 + 0 + + + + + + + + + + e59d902d1ed09be4.d164b0e3 + 1 + backend_192.168.33.1_80 + + tcp + roundrobin + 2 + + 2dd9cace-32ab-4e19-a59c-26744ee9531d,79a8d772-3814-4ade-9c12-b6ad5e1ec9da,b13c0744-2ca3-440d-b572-ad3357bca2d2,a3be6e26-095e-4af3-ba59-54f3e5732b6c,a0c71f9b-51d0-46a1-8686-1a7a24f89ed0,432001e4-eddd-4cc7-97a8-8c8d68049b1f + + + + + + 1 + 41663990-0ffc-46a0-8c56-1c194dde8dc4 + 0 + + + + + + 0 + 0 + + 0 + 0 + + sticktable + piggyback + + 0 + + + 30m + 50k + + + 10s + 10s + 10s + 10s + 1m + 1m + 0 + + + + + + + + + 0 + + 0 + + + + + b18b8d840ac79ebe.44a5f27f + 1 + backend_192.168.33.1_443 + + tcp + roundrobin + 2 + + 621f9277-3351-462e-ac79-0a50d9297daa,613de55f-0f04-4b3b-9cfe-4bc720686c17,2778f5cc-f347-4506-bcb3-8b7beaee43b3,2c670f2a-576d-4d57-ac1c-402229ceaebe,dc3d126a-2368-4fd2-b5ba-e1ddf550cdc0,9d966027-c3e9-4253-b771-89ea9954f25d + + + + + + 1 + 943bb69a-c19e-461c-9755-c05b8e860eb1 + 0 + + + + + + 0 + 0 + + 0 + 0 + + sticktable + piggyback + + 0 + + + 30m + 50k + + + 10s + 10s + 10s + 10s + 1m + 1m + 0 + + + + + + + + + 0 + + 0 + + + + + d876f8143ec06bdd.d8c8f390 + 1 + backend_192.168.33.1_22623 + + tcp + roundrobin + 2 + + d69299da-ae0a-406c-8658-319f01906c6c,cfffecc2-b3ee-45aa-be2d-c0d2bd224ff4,976922b3-bc0b-4745-b319-fbdd61321379 + + + + + + 1 + 4a6a2a77-b35e-4bf0-8639-31fadafa2d81 + 0 + + + + + + 0 + 0 + + 0 + 0 + + sticktable + piggyback + + 0 + + + 30m + 50k + + + 10s + 10s + 10s + 10s + 1m + 1m + 0 + + + + + + + + + 0 + + 0 + + + + + dedd33d162fc85de.36a7389d + 1 + backend_192.168.33.1_6443 + + tcp + roundrobin + 2 + + 85c6a978-63f1-4af3-afac-9009ce5483b8,9415acdf-bb7e-4e97-b61e-b85bbabe397a,b0da12c3-5199-4c7e-b224-69836b1eebe9 + + + + + + 1 + 943bb69a-c19e-461c-9755-c05b8e860eb1 + 0 + + + + + + 0 + 0 + + 0 + 0 + + sticktable + piggyback + + 0 + + + 30m + 50k + + + 10s + 10s + 10s + 10s + 1m + 1m + 0 + + + + + + + + + 0 + + 0 + + + + + + + 68c8ff6c38f62657.41d2104d + 1 + 192.168.33.20_80 + +
192.168.33.20
+ 80 + + active + + static + + + + + + 0 + + 0 + + + + + + + + + + +
+ + 9d3e1b4532081cde.9604f10f + 1 + 192.168.33.21_80 + +
192.168.33.21
+ 80 + + active + + static + + + + + + 0 + + 0 + + + + + + + + + + +
+ + fb02e85101a55583.442e22e2 + 1 + 192.168.33.22_80 + +
192.168.33.22
+ 80 + + active + + static + + + + + + 0 + + 0 + + + + + + + + + + +
+ + d7c1cfbc58cde8cf.a44d6720 + 1 + 192.168.33.30_443 + +
192.168.33.30
+ 443 + + active + + static + + + + + + 0 + + 0 + + + + + + + + + + +
+ + 1fdd6caa0d234653.f072d6a4 + 1 + 192.168.33.21_443 + +
192.168.33.21
+ 443 + + active + + static + + + + + + 0 + + 0 + + + + + + + + + + +
+ + c983cc4193bcfe6f.b365aa92 + 1 + 192.168.33.22_443 + +
192.168.33.22
+ 443 + + active + + static + + + + + + 0 + + 0 + + + + + + + + + + +
+ + 807c6a5a6d827047.6a59b632 + 1 + 192.168.33.20_22623 + +
192.168.33.20
+ 22623 + + active + + static + + + + + + 0 + + 0 + + + + + + + + + + +
+ + 1b58748e4092d03a.a6b8e9f7 + 1 + 192.168.33.21_22623 + +
192.168.33.21
+ 22623 + + active + + static + + + + + + 0 + + 0 + + + + + + + + + + +
+ + f1022774f9bb613f.5eaaf49f + 1 + 192.168.33.22_22623 + +
192.168.33.22
+ 22623 + + active + + static + + + + + + 0 + + 0 + + + + + + + + + + +
+ + 8651865f070701d8.7820f31b + 1 + 192.168.33.20_6443 + +
192.168.33.20
+ 6443 + + active + + static + + + + + + 0 + + 0 + + + + + + + + + + +
+ + b41297ac041c49f8.5898a56d + 1 + 192.168.33.21_6443 + +
192.168.33.21
+ 6443 + + active + + static + + + + + + 0 + + 0 + + + + + + + + + + +
+ + 3bdec82af2c6071d.a288e254 + 1 + 192.168.33.22_6443 + +
192.168.33.22
+ 6443 + + active + + static + + + + + + 0 + + 0 + + + + + + + + + + +
+ + 68818e55a5d8e8.27621290 + 1 + 192.168.33.30_80 + +
192.168.33.30
+ 80 + + active + + static + + + + + + 0 + + 0 + + + + + + + + + + +
+ + 68818e64139dd6.41162612 + 1 + 192.168.33.31_80 + +
192.168.33.31
+ 80 + + active + + static + + + + + + 0 + + 0 + + + + + + + + + + +
+ + 68818e6e488d45.84039549 + 1 + 192.168.33.32_80 + +
192.168.33.32
+ 80 + + active + + static + + + + + + 0 + + 0 + + + + + + + + + + +
+ + 68818e90906b75.70067928 + 1 + 192.168.33.20_443 + +
192.168.33.20
+ 443 + + active + + static + + + + + + 0 + + 0 + + + + + + + + + + +
+ + 68818e9c9fc677.25194625 + 1 + 192.168.33.31_443 + +
192.168.33.31
+ 443 + + active + + static + + + + + + 0 + + 0 + + + + + + + + + + +
+ + 68818ea887e1b7.80748222 + 1 + 192.168.33.32_443 + +
192.168.33.32
+ 443 + + active + + static + + + + + + 0 + + 0 + + + + + + + + + + +
+
+ + + TCP_serverport + + tcp + 2s + nopref + + 0 + + + + + + 0 + + + + + + + + + + + + + + + + + + + + TCP_serverport + + tcp + 2s + nopref + + 0 + + + + + + 0 + + + + + + + + + + + + + + + + + + + + TCP_serverport + + tcp + 2s + nopref + + 0 + + + + + + 0 + + + + + + + + + + + + + + + + + + + + HTTP_GET_/readyz + + http + 2s + nopref + + 0 + + GET + /readyz + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + 0 + + 0 + + 0 + + + +
+ + + + + + + 6796970f3b58c + Web GUI TLS certificate + LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUhIakNDQlFhZ0F3SUJBZ0lVR3B1ZFZqRzR5a0JVYS9NUWxyU2ZBcWs5VCtRd0RRWUpLb1pJaHZjTkFRRUwKQlFBd2dZa3hIVEFiQmdOVkJBTU1GRTlRVG5ObGJuTmxMbXh2WTJGc1pHOXRZV2x1TVFzd0NRWURWUVFHRXdKTwpUREVWTUJNR0ExVUVDQXdNV25WcFpDMUliMnhzWVc1a01SVXdFd1lEVlFRSERBeE5hV1JrWld4b1lYSnVhWE14CkxUQXJCZ05WQkFvTUpFOVFUbk5sYm5ObElITmxiR1l0YzJsbmJtVmtJSGRsWWlCalpYSjBhV1pwWTJGMFpUQWUKRncweU5UQXhNall5TURFeU1EWmFGdzB5TmpBeU1qY3lNREV5TURaYU1JR0pNUjB3R3dZRFZRUUREQlJQVUU1egpaVzV6WlM1c2IyTmhiR1J2YldGcGJqRUxNQWtHQTFVRUJoTUNUa3d4RlRBVEJnTlZCQWdNREZwMWFXUXRTRzlzCmJHRnVaREVWTUJNR0ExVUVCd3dNVFdsa1pHVnNhR0Z5Ym1sek1TMHdLd1lEVlFRS0RDUlBVRTV6Wlc1elpTQnoKWld4bUxYTnBaMjVsWkNCM1pXSWdZMlZ5ZEdsbWFXTmhkR1V3Z2dJaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQwpEd0F3Z2dJS0FvSUNBUUM0alhjOXE4VENxMmZzc05zRU94dktuZm1FVjRNTzFRWDZNdmRReVN2QXIzNmRlNXVMCmZ3bkJSRnVkRC9zQ1B0ZzhXajVCaCtiNGF3WC9mOUdnQnNJbnhKNVB2SWFXMlBtWThpS0Q1Q01WQXhxUE1lMlQKT0VxaEYvWkJxZlNNUWk2RkVhTTRFS0J6bllMbzhnMlAyTW93VjBDbmI3aXVJVWlKRFF3a2JWOVZESG55VkhGaApkVVlONjlpTXRYMEZiSHVtY2tKWHJwQTVQcGFSdVdsaUNnNHl4dWxEOGRsL3dJOXAzZENQY2tNaXgvYjQ1aWdJCjZHbC9GUmhhZmx5VWJ2WWxSMEw5d1pVMmNpcHFQemJJR0tlZ2pkRVdIcGRzVW5sTnJiR1I0azZieWQ4ZGd1a0YKWlNVNUlUSkZRSFdNN3ZNNkZuMTloUE51NG94TEtVUTc1b1YwZDdHV2RaQ1RIYnU3R1hqekhhVHJKYnhTTjMyVApIcDFEMlhUQ1BPTEs1NjNMc2YrNm12Ty9BQSt5SWQyZ0tvWllsdG9GUDdGcHNwWmIwYnhqeWt1amxvOUg0TW1iCmpDeCtWTVkwWUozelVTU3NCOE5IdHpyS0hyVW9oMG9oNTZPNzBNQjZpUWt5dTNaZzNlL1lTUnJwOVJwMTFMMWwKU2F3NmIzMEtFOUh6VHkxWDVXczJlQmxYajFvS2FBVktlTEN4dzlnRDJuSFdqaHViTlFwSmhmdFY2S3F0TjFVeQp4NXBWUTRrL0l1VUNTRlRMK3JOTThWOEc1NCsxTlFTaWZlWVpLdzF4bGRIMlpDbjA1QURrWUhEaFI3VUtkSGhCCncycjkrTjNjdENSaVM1V1BlRTBhQzJ5bFU2czdtU0RwWGM1cy84SnA2QlN5c1ZYL3dpZGRFMWl4N1FJREFRQUIKbzRJQmVqQ0NBWFl3Q1FZRFZSMFRCQUl3QURBUkJnbGdoa2dCaHZoQ0FRRUVCQU1DQmtBd05BWUpZSVpJQVliNApRZ0VOQkNjV0pVOVFUbk5sYm5ObElFZGxibVZ5WVhSbFpDQlRaWEoyWlhJZ1EyVnlkR2xtYVdOaGRHVXdIUVlEClZSME9CQllFRkhIMGNpYU9qNXdrKzBLeFlYUFNtL1pveFpHT01JR3pCZ05WSFNNRWdhc3dnYWloZ1kra2dZd3cKZ1lreEhUQWJCZ05WQkFNTUZFOVFUbk5sYm5ObExteHZZMkZzWkc5dFlXbHVNUXN3Q1FZRFZRUUdFd0pPVERFVgpNQk1HQTFVRUNBd01XblZwWkMxSWIyeHNZVzVrTVJVd0V3WURWUVFIREF4TmFXUmtaV3hvWVhKdWFYTXhMVEFyCkJnTlZCQW9NSkU5UVRuTmxibk5sSUhObGJHWXRjMmxuYm1Wa0lIZGxZaUJqWlhKMGFXWnBZMkYwWllJVUdwdWQKVmpHNHlrQlVhL01RbHJTZkFxazlUK1F3SFFZRFZSMGxCQll3RkFZSUt3WUJCUVVIQXdFR0NDc0dBUVVGQ0FJQwpNQXNHQTFVZER3UUVBd0lGb0RBZkJnTlZIUkVFR0RBV2doUlBVRTV6Wlc1elpTNXNiMk5oYkdSdmJXRnBiakFOCkJna3Foa2lHOXcwQkFRc0ZBQU9DQWdFQUhwcjZXQ2RmMzFWblR2RlMwUjA4RTUySHJvZHc3TjVJMGpwNkYrdHMKR2xsdGxaMDdvOWxvN2E5VFNBK1FnSGFQK1VOMkZUZmlFeHZ0RG0vdDZEQjhSald4ZnV4eloyOTMxeE8wWEhUNwp1Vll3OGpaYnlhOVQyU1VjTGR2ZFpyUHZFajJscnExYXRnc2UwMmhUUTdOSGhpd0hrbEdWTHR5K0FpaVc1STBPCkpPNGFiUHdBU3Z0SEtTU2hYTEhJY1NqWTIwLzF0TjUvdFRMTTFKUUFHZFM1NWU4YVpkdWRHWkVJdHlTZk0ya3kKY21zVnNMS0ZsL1ZLRmUrQnQySmd0d0FPUk0xbzM5WTZLQ2RHTkJPYThGUS9GQWhqZ2JrM1IwYVk3MEFBRDlyNQpXT0dPeEFxS0g5YmdhUUhrY05zSnNWekJlTFZzd3NPYnI2dktqUkUzSm1jTC9COGsxNVJaeXdMV2YxV2N6QzhwCjd5VDNUcW5vRXhQUlZZa3AzSmFQRkNncDN1YzQ0S01UZ1laQW4xeHJLS3hQdUV3NE0rSisyNzBGRzhqckJPN1UKdENMajVPMVpVMXBoVDBEckswVGloOHJlbkgrWDZkVUY4UmY4WDA0QXd5a3MzbjYramNoT3E2azFaaWVYT0NVYwppclM5eTFnNnZCM25YdW1kU05JdEZvQVhTWXRtOE5KaVUxN3kyanlnOWRHK3B4K1FpZTV2YjBMNE5xdzVoMFZhCldiWVpsMjA2cVpFenpFVEJtTHNuaWYydHB0aUlkbWh0UEZPYXBvT3hTL1ByV0V3VXc4NXdNQ0ZLcW1tZ3QzeVYKRUVlRXVRVTJIMFE5NS8wWGJOZnhKcVJSUXltOTBHeTlmTTUzU2VGbnpwVlU2VzJ6WTQyd3AwQktBZGxDdW9CdAplTjg9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + + + LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUpRd0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQ1Mwd2dna3BBZ0VBQW9JQ0FRQzRqWGM5cThUQ3EyZnMKc05zRU94dktuZm1FVjRNTzFRWDZNdmRReVN2QXIzNmRlNXVMZnduQlJGdWREL3NDUHRnOFdqNUJoK2I0YXdYLwpmOUdnQnNJbnhKNVB2SWFXMlBtWThpS0Q1Q01WQXhxUE1lMlRPRXFoRi9aQnFmU01RaTZGRWFNNEVLQnpuWUxvCjhnMlAyTW93VjBDbmI3aXVJVWlKRFF3a2JWOVZESG55VkhGaGRVWU42OWlNdFgwRmJIdW1ja0pYcnBBNVBwYVIKdVdsaUNnNHl4dWxEOGRsL3dJOXAzZENQY2tNaXgvYjQ1aWdJNkdsL0ZSaGFmbHlVYnZZbFIwTDl3WlUyY2lwcQpQemJJR0tlZ2pkRVdIcGRzVW5sTnJiR1I0azZieWQ4ZGd1a0ZaU1U1SVRKRlFIV003dk02Rm4xOWhQTnU0b3hMCktVUTc1b1YwZDdHV2RaQ1RIYnU3R1hqekhhVHJKYnhTTjMyVEhwMUQyWFRDUE9MSzU2M0xzZis2bXZPL0FBK3kKSWQyZ0tvWllsdG9GUDdGcHNwWmIwYnhqeWt1amxvOUg0TW1iakN4K1ZNWTBZSjN6VVNTc0I4Tkh0enJLSHJVbwpoMG9oNTZPNzBNQjZpUWt5dTNaZzNlL1lTUnJwOVJwMTFMMWxTYXc2YjMwS0U5SHpUeTFYNVdzMmVCbFhqMW9LCmFBVktlTEN4dzlnRDJuSFdqaHViTlFwSmhmdFY2S3F0TjFVeXg1cFZRNGsvSXVVQ1NGVEwrck5NOFY4RzU0KzEKTlFTaWZlWVpLdzF4bGRIMlpDbjA1QURrWUhEaFI3VUtkSGhCdzJyOStOM2N0Q1JpUzVXUGVFMGFDMnlsVTZzNwptU0RwWGM1cy84SnA2QlN5c1ZYL3dpZGRFMWl4N1FJREFRQUJBb0lDQUJ4UldLS1Y0TE1lS2V3Zmx2dW5OalI0CjJQaDlsUmFKaVVsQzJNQUVuam9LczVybWhJOTdCcndwQ1FXb2xoTmFJVVBoZFB3SkpsK256RnZQK1JKYzl4MnoKQmJlbWJlQm5tcVRsUW5hS1l2ZXVhanplcEYyYW5aanFYRmJuQlNjZ1lKTDZpZGpvZERaSlRQVUJieU5MV0hyaQphSUZJbTBYY3hZeUIvQUw2NVUzZmhEYXl6bEx0ODduZkhuTTR4ZDQzTHlIekZrcnQ5aU5TZnpnTkF5YVA5RzNHCko3VE5QMXBpNlo2TThwdVFKTTBKY2RQdlBPVmhCQThENWFDOUV1ZVR2eUVwTmhaSnhlTjgwUlZNYmROMk5RSmwKd0Zkc2lqK015Q0FyTHJ2N3hhUVI3YkpSaS8vUDdVNCswYi9lakNyNzMwWmlmUTd0ZjR6Y0pqckNNajRldVF0SwowSHdmYll0L2NLVWc0aTlTd0hCRlg4QUxva2M1WkNkOXkxVEQzSU1LVlpDS0pFY24rT2gvMGRTT0pDWlFoTDdKClN1czBtUU1BVDYwTGkwZXhyMC92NjZ2dWJqTWpNcHhQVjlIY2RDbTRSbVU3aFMvQlJpaGpBc0ZqelhOVlFXQzMKSUVWVTNNRC92Q1hQRzZKb284bi9UOGhQWHc0T3U0NkE5RXF5dnR4VzFlWDgrMERFVFBreCtkbzJ2R0ZWRGtRMQpoNmNLSGVmMFFGSUViaHV2V1FKRTRWNE9VdEYyRlp6VjliS3laWE5jQnNTNWdkTlFKRmt1ZkYvOFV2SlFLT0tHCi9VeVYvejZrY01xMHNUc2JSRjNVSzM2QnUxSENudldoVlVvV3NBb2JKV3pBemRpRkt0V1BpclBJNW9TUWFob24Kc2JqejAwdUJJQkFhdk5rUVhiY0JBb0lCQVFEemF5bHc1VEFWYnBza3A1WUpnaWppTzVhRUNPQlNka1FwZ1Jodwo1Y0VWenZhbUlZZ2NDMVBjSE1DeUUvMjFITFF3OUtpcVRqSGJrTWhOMEs0TW1HeWJwM1hKL2hHYkd6UzRVSGFKClQrdyt4NDRWTUxxN2l4Q2dmc3JXRHNOU2RhOUFGYnEzYWg4REpaVjNIcHplRVlOR3BIZ2hzM041STNNckR2Q2QKeXowWjhNOStWdUpIVDBjYXVqa0JlTWtZbEhYVEdabnFtYzBoaFlUdDlZVGxTTXNMV1JEdUpkdDNoQjkyTDZLOApOa082Z2lxeURteWswbHZWejhSdFZBdUQ2TWtJaDEwT3lCRksyaEpCaVhQa0R5dmM4b1JSa2xpSmxaR3UzN2VXCmVacENYSmJ4MU5meGhnckhBbnhtbU91N3dVUG5oUUNGRW1RMmZicnNZcVkxZGVrMUFvSUJBUURDRjJoV0YwQjgKc0dUMVBoek0zR3RiVkpyVFh3bXFxb3BVYjlTVi9IU1JGMkJiWUUxNWFFMy9vMmVQVmovNDVoaUlyYVhweklVeQpaWkV4Yml4WkhoZy9Qa2NqeG1oTllmTUJrdFdQZHBvMFh6ZVBWQ1hFV0J2VXNBclc4RDI1ZzlkbHEzTlEyd0hqCktDQUtoZGJudU9OeUllT25DZ0tTTEdNbjJMbGJ0dWFJcmcxUnFkOU5KOCszOVQrUXZmNHVvWmxTenpPRHgwaTEKRlFMUEgxcitRdVZYMUVqL08va3kvSzJxcWRZSXlyUHRGTXpjSVdTZ3EyTWErZ1c5T29CK3NhL2JwNWZOMEl4cAp4WjlKa2grNDRjaFgvVkE4dDJORk9HVW5TOXNXRTlsNE9wcEdxSDNtZmtLMk12SnRpS3FSTG43S0k3dnVhRnRtCnk1eFdXdXNURW5UWkFvSUJBQUYxcmd6d1F1YU9BRDRyQnhwTmZvTkV5alZHZkZuaVBheG1Dc2g2aURyaVA5WmwKTXhTLytLUEVSRitOQVNONTVaYTVrTjFjbEszMVkwNGNKejhLRnZTai8yL1RwelZmNTJRSGozNXBUVWhmRi9vRwpqY2djSUdCbUFqOWdYVWw4VFMyOE01OXY4bm1wV3drWTFPWDhBdWFFaS9mZnhKeUFXdXR5TG4wenY4ME5CYUdEClVkNE5tcWFOWVZRaDdrckljU0J2OGQxWFNNU2ZzVmxmOUlrUGM3QkF1M3BDSGR2TW5nZXVaM0pyZk9KOGIxY2MKQVFqSC9pYjlGUGQyM283TzhZMnNpaUZSajlEOEY4bnUwaFFYQnpOTy9QNGtPNFd2c096MGlIeE5oR0JMZjlnNQpaNFlhUUt4SzFvWWkrcDdvbk1paG9vd3B2UklhbE9sZitoRXVBTlVDZ2dFQkFMcWJpc01MQkFOZURSTUZMdWVBCkhPL29maHN2U3JuOTBaV3hGM0ZGRWtYVmRkMGswQmdrUXFuQVQzY3VjNzg0YXVvdUdsQ1pSSTdadkNrTVJqTkEKamd0d016R2dOdlAvY29aV3lHRndwSDRwOWQ4bUJsR3FiTWVtb2lWWlFkODFkVWpZK0x3S2ROd1QzZ3AvOThrKwpwOTg1MmdqbHhPY0pLaVJMYUp0WFZIcWc3VWxReTlNQXJlT3VOZmxSMGlxL1VBeWdEbVZxbXVzUFVtNFZOWVUwCmlCQlRtQU5kaEJDVGc5Mk1BSzdmUlBKeWh5dzJKdXViSEdQNWNyOG1taGcxZW1EejF5NFlqb2U3YTVSdW0zVkUKRHowWjNhVWlwSjBPeGFKc2VpM1YwOGFXZ1hIaDJYcGNkb042cEQ3UG9UNkl0M3BkdFBoWStWZng5MVBIZ2pBSwpGTEVDZ2dFQkFLeVVmRjBqK09TZWFSV3JtdnhCdSs2czF0Y3hFNG1IYzNtaTZuenp3WGxPRTFuSGR2b2lyNmtVCldscVRVcU5kbFF3NU9oUUlOeHMxWXpyNWdvVmlSZlhsSUd3VVFJOTRuY09XSWhNNXY5SXU4RUV4alY2TnNjUUoKRGIzNjdFamV2NWJnbFJ5ejExcmdQZTlBVzlWcXpzZHNMcjdJUVcybHlaRWR6VmEzRUVCbXpaTW00T1V6a1dkVQo5TVd4N2kyRFlWNVF2b1BSMzcwNzR2elNNeFYrZDhYZmp4MFcwbzl2WWU1UHNVMVRTaEp3SkVtWXc0bmNmL3h1CitBVzRNQWtISlRONm1FYXg4SUlpV2VIN3VZT3UveDZtazZBejVIK3UyK2ZRcHpyZEtVVm02SWFLT2I1THlLQzcKTnFrZnpXeXdTNXlUWmorN3FibVZ2b3ZLYjZubXczRT0KLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQo= + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + 0 + 0 + + 1400 + + + + + + 1 + 0 + 8080 + 8443 + + + + + + + + + + + + + + + + 0 + 10 + h1,h2 + + + 10 + + + + + + + + + + + 0 + + + + + + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + + + + + + + +