diff --git a/Cargo.lock b/Cargo.lock index befd87e..337e48f 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" @@ -389,7 +398,7 @@ dependencies = [ "rustc-hash", "serde", "serde_derive", - "syn", + "syn 2.0.105", ] [[package]] @@ -451,7 +460,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -462,7 +471,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -849,9 +858,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", @@ -859,9 +868,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", @@ -871,14 +880,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]] @@ -1190,7 +1199,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -1214,7 +1223,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.105", ] [[package]] @@ -1225,7 +1234,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -1263,7 +1272,7 @@ checksum = "2cdc8d50f426189eef89dac62fabfa0abb27d5cc008f25bf4156a0203325becc" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -1283,7 +1292,7 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", "unicode-xid", ] @@ -1349,7 +1358,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]] @@ -1378,7 +1400,7 @@ dependencies = [ "anyhow", "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -1435,7 +1457,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -1506,7 +1528,7 @@ checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -1777,6 +1799,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" @@ -1821,6 +1865,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" @@ -1941,7 +1996,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -2126,6 +2181,7 @@ dependencies = [ "fqdn", "futures-util", "harmony-secret-derive", + "harmony_inventory_agent", "harmony_macros", "harmony_types", "helm-wrapper-rs", @@ -2192,7 +2248,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -2242,9 +2298,12 @@ dependencies = [ "actix-web", "env_logger", "log", + "mdns-sd", "serde", "serde_json", "sysinfo", + "thiserror 2.0.14", + "tokio", ] [[package]] @@ -2256,7 +2315,7 @@ dependencies = [ "quote", "serde", "serde_yaml", - "syn", + "syn 2.0.105", ] [[package]] @@ -2811,6 +2870,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" @@ -2926,7 +2995,7 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -2998,7 +3067,7 @@ checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -3183,7 +3252,7 @@ dependencies = [ "quote", "serde", "serde_json", - "syn", + "syn 2.0.105", ] [[package]] @@ -3354,6 +3423,34 @@ 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", + "log", + "mdns-sd", + "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 = "memchr" version = "2.7.5" @@ -3607,7 +3704,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -3851,7 +3948,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -3881,7 +3978,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -4285,7 +4382,7 @@ checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -4294,7 +4391,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", @@ -4306,7 +4403,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", ] @@ -4793,7 +4890,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn", + "syn 2.0.105", ] [[package]] @@ -4925,7 +5022,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -4936,7 +5033,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -4969,7 +5066,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -4990,7 +5087,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn", + "syn 2.0.105", ] [[package]] @@ -5034,7 +5131,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -5176,7 +5273,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]] @@ -5204,6 +5312,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" @@ -5312,7 +5423,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn", + "syn 2.0.105", ] [[package]] @@ -5325,7 +5436,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn", + "syn 2.0.105", ] [[package]] @@ -5334,6 +5445,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" @@ -5360,6 +5482,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" @@ -5368,7 +5502,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -5502,7 +5636,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -5513,7 +5647,7 @@ checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -5618,7 +5752,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -5789,7 +5923,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -5999,7 +6133,7 @@ checksum = "26b682e8c381995ea03130e381928e0e005b7c9eb483c6c8682f50e07b33c2b7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -6075,7 +6209,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn", + "syn 2.0.105", "wasm-bindgen-shared", ] @@ -6110,7 +6244,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -6229,7 +6363,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -6240,7 +6374,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -6584,7 +6718,7 @@ dependencies = [ "quote", "serde", "serde_tokenstream", - "syn", + "syn 2.0.105", "xml-rs", ] @@ -6608,8 +6742,8 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn", - "synstructure", + "syn 2.0.105", + "synstructure 0.13.2", ] [[package]] @@ -6629,7 +6763,7 @@ checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -6649,8 +6783,8 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", - "synstructure", + "syn 2.0.105", + "synstructure 0.13.2", ] [[package]] @@ -6689,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..ec97a88 --- /dev/null +++ b/adr/agent_discovery/mdns/Cargo.toml @@ -0,0 +1,15 @@ +[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"] } diff --git a/adr/agent_discovery/mdns/src/advertise.rs b/adr/agent_discovery/mdns/src/advertise.rs new file mode 100644 index 0000000..ee8e4ce --- /dev/null +++ b/adr/agent_discovery/mdns/src/advertise.rs @@ -0,0 +1,52 @@ +// 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 service_info = ServiceInfo::new( + SERVICE_TYPE, + &instance_name, + "harmony-host.local.", // A hostname for the service + (), // No specific IP addresses, let the daemon figure it out + 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."); + + // 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..7fbe2b0 --- /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 = "_harmory._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/examples/cli/src/main.rs b/examples/cli/src/main.rs index 34a032b..03c4e00 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,7 @@ async fn main() { Box::new(SuccessScore {}), Box::new(ErrorScore {}), Box::new(PanicScore {}), + Box::new(DiscoverInventoryAgentScore { discovery_timeout: Some(10) }), ], None, ) diff --git a/harmony/Cargo.toml b/harmony/Cargo.toml index 1ba4c94..72be153 100644 --- a/harmony/Cargo.toml +++ b/harmony/Cargo.toml @@ -67,7 +67,8 @@ 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-secret-derive = { path = "../harmony_secret_derive" } +harmony_inventory_agent = { path = "../harmony_inventory_agent" } [dev-dependencies] pretty_assertions.workspace = true diff --git a/harmony/src/domain/interpret/mod.rs b/harmony/src/domain/interpret/mod.rs index cfbf2b5..d9213c2 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, } impl std::fmt::Display for InterpretName { @@ -58,6 +59,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"), } } } diff --git a/harmony/src/modules/inventory/mod.rs b/harmony/src/modules/inventory/mod.rs new file mode 100644 index 0000000..c4a9971 --- /dev/null +++ b/harmony/src/modules/inventory/mod.rs @@ -0,0 +1,71 @@ +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, + ); + } + + 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..c613233 100644 --- a/harmony/src/modules/mod.rs +++ b/harmony/src/modules/mod.rs @@ -17,3 +17,4 @@ pub mod prometheus; pub mod storage; pub mod tenant; pub mod tftp; +pub mod inventory; diff --git a/harmony_inventory_agent/Cargo.toml b/harmony_inventory_agent/Cargo.toml index 3b1be2c..03810ae 100644 --- a/harmony_inventory_agent/Cargo.toml +++ b/harmony_inventory_agent/Cargo.toml @@ -10,3 +10,6 @@ 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" diff --git a/harmony_inventory_agent/src/lib.rs b/harmony_inventory_agent/src/lib.rs new file mode 100644 index 0000000..eeb21bb --- /dev/null +++ b/harmony_inventory_agent/src/lib.rs @@ -0,0 +1,2 @@ +pub mod local_presence; +mod hwinfo; 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..82e26f8 --- /dev/null +++ b/harmony_inventory_agent/src/local_presence/advertise.rs @@ -0,0 +1,73 @@ +use log::{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 service_info = ServiceInfo::new( + SERVICE_NAME, + &instance_name, + &format!("{}.local.", instance_name), + (), // Let the daemon determine the host IPs + 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..6bd1141 --- /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::ServiceData(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..faaaeff 100644 --- a/harmony_inventory_agent/src/main.rs +++ b/harmony_inventory_agent/src/main.rs @@ -1,9 +1,15 @@ // 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 +32,15 @@ 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()