diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..4a73da2 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,5 @@ +[target.x86_64-pc-windows-msvc] +rustflags = [ + "-C", "target-feature=+crt-static", + "-C", "link-args=/nodefaultlib:libucrt.lib /defaultlib:ucrt.lib", +] diff --git a/Cargo.lock b/Cargo.lock index 09ef812..f69bfd5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,20 +17,11 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - [[package]] name = "anstream" -version = "0.6.15" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", @@ -43,66 +34,171 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.4" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "once_cell", + "windows-sys 0.59.0", ] [[package]] name = "anyhow" -version = "1.0.89" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" [[package]] -name = "async-stream" -version = "0.3.6" +name = "async-channel" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" dependencies = [ - "async-stream-impl", + "concurrent-queue", + "event-listener-strategy", "futures-core", "pin-project-lite", ] [[package]] -name = "async-stream-impl" -version = "0.3.6" +name = "async-executor" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" dependencies = [ - "proc-macro2", - "quote", - "syn", + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" +dependencies = [ + "async-lock", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", + "tracing", ] +[[package]] +name = "async-signal" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.4.0" @@ -125,30 +221,29 @@ dependencies = [ ] [[package]] -name = "bindgen" -version = "0.70.1" +name = "bitflags" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" + +[[package]] +name = "blocking" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" dependencies = [ - "bitflags", - "cexpr", - "clang-sys", - "itertools", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn", + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", ] [[package]] -name = "bitflags" -version = "2.6.0" +name = "bumpalo" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "byteorder" @@ -158,17 +253,17 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.2" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" +checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" [[package]] -name = "cexpr" -version = "0.6.0" +name = "cc" +version = "1.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +checksum = "0c3d1b2e905a3a7b00a6141adb0e4c0bb941d11caf55349d863942a1cc44e3c9" dependencies = [ - "nom", + "shlex", ] [[package]] @@ -177,22 +272,11 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - [[package]] name = "clap" -version = "4.5.19" +version = "4.5.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615" +checksum = "92b7b18d71fad5313a1e320fa9897994228ce274b60faa4d694fe0ea89cd9e6d" dependencies = [ "clap_builder", "clap_derive", @@ -200,9 +284,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.19" +version = "4.5.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b" +checksum = "a35db2071778a7344791a4fb4f95308b5673d219dee3ae348b86642574ecc90c" dependencies = [ "anstream", "anstyle", @@ -212,9 +296,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.18" +version = "4.5.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" dependencies = [ "heck", "proc-macro2", @@ -224,15 +308,30 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "colorchoice" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "deranged" @@ -243,12 +342,6 @@ dependencies = [ "powerfmt", ] -[[package]] -name = "either" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" - [[package]] name = "enum-as-inner" version = "0.6.1" @@ -261,29 +354,47 @@ dependencies = [ "syn", ] +[[package]] +name = "enum_dispatch" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "env_filter" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" dependencies = [ "log", - "regex", ] [[package]] name = "env_logger" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" dependencies = [ - "anstream", - "anstyle", "env_filter", - "humantime", "log", ] +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "error-chain" version = "0.12.4" @@ -293,11 +404,50 @@ dependencies = [ "version_check", ] +[[package]] +name = "event-listener" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "spin", +] + [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -310,9 +460,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -320,15 +470,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -337,15 +487,28 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", @@ -354,21 +517,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -382,6 +545,31 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets", +] + [[package]] name = "gimli" version = "0.31.1" @@ -389,10 +577,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +name = "guest-metrics" +version = "0.6.0-dev" +dependencies = [ + "flume", + "os_info", + "uuid", +] [[package]] name = "heck" @@ -402,9 +593,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.3.9" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" [[package]] name = "hostname" @@ -417,21 +608,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - -[[package]] -name = "ipnetwork" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" -dependencies = [ - "serde", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -439,35 +615,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] -name = "itertools" -version = "0.13.0" +name = "itoa" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] -name = "itoa" -version = "1.0.11" +name = "js-sys" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] [[package]] name = "libc" -version = "0.2.159" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] -name = "libloading" -version = "0.8.5" +name = "linux-raw-sys" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" -dependencies = [ - "cfg-if", - "windows-targets", -] +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "lock_api" @@ -481,9 +654,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" [[package]] name = "match_cfg" @@ -497,33 +670,35 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b" dependencies = [ "adler2", ] [[package]] name = "mio" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ - "hermit-abi", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom 0.2.15", +] + [[package]] name = "netlink-packet-core" version = "0.7.0" @@ -558,29 +733,28 @@ dependencies = [ "anyhow", "byteorder", "paste", - "thiserror", + "thiserror 1.0.69", ] [[package]] name = "netlink-proto" -version = "0.11.3" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b33524dc0968bfad349684447bfce6db937a9ac3332a1fe60c0c5a5ce63f21" +checksum = "72452e012c2f8d612410d89eea01e2d9b56205274abb35d53f60200b2ec41d60" dependencies = [ "bytes", "futures", "log", "netlink-packet-core", "netlink-sys", - "thiserror", - "tokio", + "thiserror 2.0.11", ] [[package]] name = "netlink-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416060d346fbaf1f23f9512963e3e878f1a78e707cb699ba9215761754244307" +checksum = "16c903aa70590cb93691bf97a767c8d1d6122d2cc9070433deb3bbf36ce8bd23" dependencies = [ "bytes", "futures", @@ -590,30 +764,26 @@ dependencies = [ ] [[package]] -name = "nix" -version = "0.27.1" +name = "network-interface" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +checksum = "433419f898328beca4f2c6c73a1b52540658d92b0a99f0269330457e0fd998d5" dependencies = [ - "bitflags", - "cfg-if", + "cc", "libc", + "thiserror 1.0.69", + "winapi", ] [[package]] -name = "no-std-net" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" - -[[package]] -name = "nom" -version = "7.1.3" +name = "nix" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ - "memchr", - "minimal-lexical", + "bitflags", + "cfg-if", + "libc", ] [[package]] @@ -633,45 +803,35 @@ dependencies = [ [[package]] name = "object" -version = "0.36.5" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] [[package]] -name = "os_info" -version = "3.8.2" +name = "once_cell" +version = "1.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae99c7fa6dd38c7cafe1ec085e804f8f555a2f8659b0dbe03f1f9963a9b51092" -dependencies = [ - "log", - "windows-sys 0.52.0", -] +checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" [[package]] -name = "parking_lot" -version = "0.12.3" +name = "os_info" +version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "2a604e53c24761286860eba4e2c8b23a0161526476b1de520139d69cdb85a6b5" dependencies = [ - "lock_api", - "parking_lot_core", + "log", + "serde", + "windows-sys 0.52.0", ] [[package]] -name = "parking_lot_core" -version = "0.9.10" +name = "parking" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets", -] +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "paste" @@ -681,9 +841,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -692,41 +852,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] -name = "pkg-config" -version = "0.3.31" +name = "piper" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" - -[[package]] -name = "pnet_base" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc190d4067df16af3aba49b3b74c469e611cad6314676eaf1157f31aa0fb2f7" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" dependencies = [ - "no-std-net", -] - -[[package]] -name = "pnet_datalink" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79e70ec0be163102a332e1d2d5586d362ad76b01cec86f830241f2b6452a7b7" -dependencies = [ - "ipnetwork", - "libc", - "pnet_base", - "pnet_sys", - "winapi", + "atomic-waker", + "fastrand", + "futures-io", ] [[package]] -name = "pnet_sys" -version = "0.35.0" +name = "polling" +version = "3.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d4643d3d4db6b08741050c2f3afa9a892c4244c085a72fcda93c9c2c9a00f4b" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" dependencies = [ - "libc", - "winapi", + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "tracing", + "windows-sys 0.59.0", ] [[package]] @@ -736,70 +884,96 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] -name = "prettyplease" -version = "0.2.22" +name = "proc-macro2" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ - "proc-macro2", - "syn", + "unicode-ident", ] [[package]] -name = "proc-macro2" -version = "1.0.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +name = "provider-memory" +version = "0.1.0" dependencies = [ - "unicode-ident", + "flume", + "futures", + "guest-metrics", + "smol", + "windows", ] [[package]] -name = "quote" -version = "1.0.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +name = "provider-netlink" +version = "0.1.0" dependencies = [ - "proc-macro2", + "anyhow", + "flume", + "futures", + "guest-metrics", + "log", + "netlink-packet-core", + "netlink-packet-route", + "netlink-proto", + "rtnetlink", + "smol", + "tokio", + "uuid", + "vif-detect", ] [[package]] -name = "redox_syscall" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +name = "provider-os" +version = "0.6.0-dev" dependencies = [ - "bitflags", + "flume", + "guest-metrics", + "smol", + "uname", ] [[package]] -name = "regex" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +name = "provider-simple" +version = "0.1.0" dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", + "flume", + "futures", + "guest-metrics", + "network-interface", + "smol", + "uuid", + "vif-detect", ] [[package]] -name = "regex-automata" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +name = "publisher-console" +version = "0.6.0-dev" dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", + "flume", + "guest-metrics", + "uuid", +] + +[[package]] +name = "publisher-xenstore" +version = "0.1.0" +dependencies = [ + "flume", + "guest-metrics", + "log", + "uuid", + "xenstore-rs", + "xenstore-win", ] [[package]] -name = "regex-syntax" -version = "0.8.5" +name = "quote" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] [[package]] name = "rtnetlink" @@ -815,7 +989,7 @@ dependencies = [ "netlink-proto", "netlink-sys", "nix", - "thiserror", + "thiserror 1.0.69", "tokio", ] @@ -826,10 +1000,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] -name = "rustc-hash" -version = "1.1.0" +name = "rustix" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] [[package]] name = "same-file" @@ -848,18 +1029,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.210" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", @@ -891,21 +1072,41 @@ dependencies = [ ] [[package]] -name = "smallvec" -version = "1.13.2" +name = "smol" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f" +dependencies = [ + "async-channel", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-net", + "async-process", + "blocking", + "futures-lite", +] [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", "windows-sys 0.52.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + [[package]] name = "strsim" version = "0.11.1" @@ -914,9 +1115,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.79" +version = "2.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" dependencies = [ "proc-macro2", "quote", @@ -933,7 +1134,7 @@ dependencies = [ "byteorder", "enum-as-inner", "libc", - "thiserror", + "thiserror 1.0.69", "walkdir", ] @@ -952,18 +1153,38 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl 2.0.11", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", @@ -972,9 +1193,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", "itoa", @@ -995,9 +1216,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" dependencies = [ "num-conv", "time-core", @@ -1005,33 +1226,34 @@ dependencies = [ [[package]] name = "tokio" -version = "1.40.0" +version = "1.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" dependencies = [ "backtrace", - "bytes", "libc", "mio", - "parking_lot", "pin-project-lite", - "signal-hook-registry", "socket2", - "tokio-macros", "windows-sys 0.52.0", ] [[package]] -name = "tokio-macros" -version = "2.4.0" +name = "tracing" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ - "proc-macro2", - "quote", - "syn", + "pin-project-lite", + "tracing-core", ] +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" + [[package]] name = "uname" version = "0.1.1" @@ -1043,9 +1265,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" [[package]] name = "utf8parse" @@ -1053,12 +1275,31 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c1f41ffb7cf259f1ecc2876861a17e7142e63ead296f671f81f6ae85903e0d6" +dependencies = [ + "getrandom 0.3.1", +] + [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vif-detect" +version = "0.1.0" +dependencies = [ + "guest-metrics", + "log", + "xenstore-rs", + "xenstore-win", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -1075,6 +1316,78 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "widestring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" + [[package]] name = "winapi" version = "0.3.9" @@ -1106,6 +1419,81 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core", + "windows-targets", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-strings", + "windows-targets", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-service" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "193cae8e647981c35bc947fdd57ba7928b1fa0d4a79305f6dd2dc55221ac35ac" +dependencies = [ + "bitflags", + "widestring", + "windows-sys 0.59.0", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -1188,49 +1576,52 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags", +] + [[package]] name = "xen-guest-agent" version = "0.5.0-dev" dependencies = [ - "async-stream", + "anyhow", "clap", + "enum_dispatch", "env_logger", + "flume", "futures", - "ipnetwork", - "libc", + "guest-metrics", "log", - "netlink-packet-core", - "netlink-packet-route", - "netlink-proto", - "os_info", - "pnet_base", - "pnet_datalink", - "rtnetlink", + "provider-memory", + "provider-netlink", + "provider-os", + "provider-simple", + "publisher-console", + "publisher-xenstore", + "smol", "sysctl", "syslog", - "tokio", - "uname", - "xenstore-rs", + "windows", + "windows-service", ] [[package]] name = "xenstore-rs" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb033ee238b7347ab2f1f3e550094efdb19b7175e6e4664978c72ff768c43f0a" -dependencies = [ - "libc", - "libloading", - "log", - "xenstore-sys", -] +checksum = "8df0b81a6d7038b1540a5368e43febc3b48c05d9426c96f815aba9a33b0e8d45" [[package]] -name = "xenstore-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a864a4c2dfde8487a06452d052f0b2116829c3e9b48f8ac71a3f1421e555b709" +name = "xenstore-win" +version = "0.1.0" +source = "git+https://github.com/TSnake41/xenstore-win.git#d79e6dfa10748839403c2a6134a98660d814c0d9" dependencies = [ - "bindgen", - "pkg-config", + "log", + "windows", + "xenstore-rs", ] diff --git a/Cargo.toml b/Cargo.toml index a707160..71d5f0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,48 +1,21 @@ -[package] -name = "xen-guest-agent" -version = "0.5.0-dev" -authors = ["Yann Dirson "] -edition = "2018" -rust-version = "1.71" -license = "AGPL-3.0-only" +[workspace] +package.version = "0.6.0-dev" +package.repository = "https://gitlab.com/xen-project/xen-guest-agent" +package.categories = ["virtualization"] +package.edition = "2021" +resolver = "2" -[dependencies] -futures = "0.3.26" -libc = "0.2.139" -tokio = { version = "1.25.0", features = ["full"] } -netlink-packet-core = { version = "0.7.0", optional = true } -netlink-packet-route = { version = ">=0.18.0, <0.20", optional = true } -netlink-proto = { version = "0.11.2", optional = true } -rtnetlink = { version = "0.14.0", optional = true } -async-stream = "0.3.4" -os_info = { version = "3", default-features = false } -pnet_datalink = { version = "*", optional = true } -pnet_base = { version = "*", optional = true } -ipnetwork = { version = "*", optional = true } -log = "0.4.0" -env_logger = ">=0.10.0" -clap = { version = "4.4.8", features = ["derive"] } - -[dependencies.xenstore-rs] -optional = true -version = "0.7.0" -#git = "https://github.com/Wenzel/xenstore.git" -default-features = false - -[target.'cfg(unix)'.dependencies] -uname = "0.1.1" -syslog = "6.0" - -[target.'cfg(target_os = "freebsd")'.dependencies] -sysctl = "0.5.0" - -[features] -default = ["xenstore", "net_netlink"] -xenstore = ["dep:xenstore-rs"] -static = ["xenstore-rs?/static"] -net_netlink = ["dep:netlink-proto", "dep:netlink-packet-core", "dep:netlink-packet-route", - "dep:rtnetlink"] -net_pnet = ["dep:pnet_datalink", "dep:pnet_base", "dep:ipnetwork"] +members = [ + "xen-guest-agent", + "guest-metrics", + "providers/provider-os", + "providers/provider-memory", + #"providers/provider-netlink", + "providers/provider-simple", + "publishers/publisher-xenstore", + "publishers/publisher-console", +] [profile.release] lto = true +debug = "full" diff --git a/guest-metrics/Cargo.toml b/guest-metrics/Cargo.toml new file mode 100644 index 0000000..85c078c --- /dev/null +++ b/guest-metrics/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "guest-metrics" +edition.workspace = true +version.workspace = true +repository.workspace = true +categories.workspace = true + +[dependencies] +flume = "0.11.1" +uuid = "1.11" +os_info = "3" \ No newline at end of file diff --git a/guest-metrics/src/lib.rs b/guest-metrics/src/lib.rs new file mode 100644 index 0000000..04df3a3 --- /dev/null +++ b/guest-metrics/src/lib.rs @@ -0,0 +1,66 @@ +pub mod plugin; + +use std::net::IpAddr; + +use uuid::Uuid; + +#[derive(Debug)] +pub struct KernelInfo { + pub release: String, +} + +#[non_exhaustive] +#[derive(Clone, Debug, Default)] +pub enum ToolstackNetInterface { + #[default] + Unknown, + Vif(u32), + // SRIOV, + // PciPassthrough, + // UsbPassthrough, +} + +#[derive(Clone, Debug)] +pub struct NetInterface { + pub uuid: Uuid, + pub index: u32, + pub name: String, + pub toolstack_iface: ToolstackNetInterface, +} + +#[derive(Debug)] +pub enum NetEventOp { + AddMac(String), + RmMac(String), + AddIp(IpAddr), + RmIp(IpAddr), +} + +#[derive(Debug)] +pub struct NetEvent { + pub iface_id: Uuid, + pub op: NetEventOp, +} + +#[derive(Debug)] +pub struct OsInfo { + pub os_info: os_info::Info, + pub kernel_info: Option, +} + +#[derive(Debug)] +pub struct MemoryInfo { + pub mem_free: usize, + pub mem_total: usize, +} + +pub enum GuestMetric { + OperatingSystem(OsInfo), + AddIface(NetInterface), + RmIface(Uuid), + Memory(MemoryInfo), + Network(NetEvent), + CleanupIfaces, +} + +pub use os_info; \ No newline at end of file diff --git a/guest-metrics/src/plugin.rs b/guest-metrics/src/plugin.rs new file mode 100644 index 0000000..b04d85d --- /dev/null +++ b/guest-metrics/src/plugin.rs @@ -0,0 +1,11 @@ +use std::future::Future; + +use crate::GuestMetric; + +pub trait GuestAgentPlugin { + fn run(self, channel: flume::Sender) -> impl Future + Send; +} + +pub trait GuestAgentPublisher { + fn run(self, channel: flume::Receiver) -> impl Future + Send; +} diff --git a/providers/provider-memory/Cargo.toml b/providers/provider-memory/Cargo.toml new file mode 100644 index 0000000..bf9f623 --- /dev/null +++ b/providers/provider-memory/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "provider-memory" +version = "0.1.0" +edition = "2021" + +[dependencies] +guest-metrics = { path = "../../guest-metrics" } +futures = "0.3" +flume = "0.11.1" +smol = "2.0.2" + +[target.'cfg(target_os = "windows")'.dependencies] +windows = { version = "0.58", features = ["Win32_System_SystemInformation"] } diff --git a/providers/provider-memory/src/bsd.rs b/providers/provider-memory/src/bsd.rs new file mode 100644 index 0000000..986fe30 --- /dev/null +++ b/providers/provider-memory/src/bsd.rs @@ -0,0 +1,109 @@ +use std::io; +use sysctl::Sysctl; + +use super::MemorySource; + +pub struct BsdMemorySource { + memtotal_ctl: sysctl::Ctl, + pagesize: usize, + meminactive_ctl: sysctl::Ctl, + memcache_ctl: sysctl::Ctl, + memfree_ctl: sysctl::Ctl, +} + +impl MemorySource for BsdMemorySource { + fn new() -> io::Result { + let pagesize_ctl = new_sysctl("hw.pagesize")?; + let pagesize_value = pagesize_ctl.value().map_err(sysctrlerror_to_ioerror)?; + let pagesize = *pagesize_value.as_int().ok_or(io::Error::new( + io::ErrorKind::InvalidData, + "cannot parse hw.pagesize as int", + ))? as usize; + Ok(Self { + memtotal_ctl: new_sysctl("hw.physmem")?, + pagesize, + meminactive_ctl: new_sysctl("vm.stats.vm.v_inactive_count")?, + memcache_ctl: new_sysctl("vm.stats.vm.v_cache_count")?, + memfree_ctl: new_sysctl("vm.stats.vm.v_free_count")?, + }) + } + fn get_total_kb(&mut self) -> io::Result { + Ok(get_field_ulong(&self.memtotal_ctl)? as usize / 1024) + } + fn get_available_kb(&mut self) -> io::Result { + let available = (get_field_u32(&self.meminactive_ctl)? as usize + + get_field_uint(&self.memcache_ctl)? as usize + + get_field_u32(&self.memfree_ctl)? as usize) + * self.pagesize; + Ok(available / 1024) + } +} + +// helper to create a sysctl with errors mapped to io::Error +fn new_sysctl(name: &str) -> io::Result { + match sysctl::Ctl::new(name) { + Err(sysctl::SysctlError::NotFound(_)) => Err(io::Error::new( + io::ErrorKind::NotFound, + format!("sysctl {} not found", name), + )), + Err(e) => Err(io::Error::new( + io::ErrorKind::Other, + format!("Unexpected error type creating sysctl::Ctl: {:?}", e), + )), + Ok(ctl) => Ok(ctl), + } +} + +fn sysctrlerror_to_ioerror(error: sysctl::SysctlError) -> io::Error { + match error { + e => io::Error::new(io::ErrorKind::Other, format!("sysctl error: {:?}", e)), + } +} + +fn get_field_ulong(ctl: &sysctl::Ctl) -> io::Result { + let v = ctl.value().map_err(sysctrlerror_to_ioerror)?; + if let Some(value) = v.as_ulong() { + Ok(*value) + } else { + Err(io::Error::new( + io::ErrorKind::Other, + format!( + "cannot interpret {} as ulong: {:?}", + ctl.name().map_err(sysctrlerror_to_ioerror)?, + v + ), + )) + } +} + +fn get_field_u32(ctl: &sysctl::Ctl) -> io::Result { + let v = ctl.value().map_err(sysctrlerror_to_ioerror)?; + if let Some(value) = v.as_u32() { + Ok(*value) + } else { + Err(io::Error::new( + io::ErrorKind::Other, + format!( + "cannot interpret {} as u32: {:?}", + ctl.name().map_err(sysctrlerror_to_ioerror)?, + v + ), + )) + } +} + +fn get_field_uint(ctl: &sysctl::Ctl) -> io::Result { + let v = ctl.value().map_err(sysctrlerror_to_ioerror)?; + if let Some(value) = v.as_uint() { + Ok(*value) + } else { + Err(io::Error::new( + io::ErrorKind::Other, + format!( + "cannot interpret {} as uint: {:?}", + ctl.name().map_err(sysctrlerror_to_ioerror)?, + v + ), + )) + } +} diff --git a/providers/provider-memory/src/lib.rs b/providers/provider-memory/src/lib.rs new file mode 100644 index 0000000..7d96395 --- /dev/null +++ b/providers/provider-memory/src/lib.rs @@ -0,0 +1,53 @@ +use std::{io, time::Duration}; + +use futures::StreamExt; +use guest_metrics::{plugin::GuestAgentPlugin, MemoryInfo}; + +#[cfg(target_os = "freebsd")] +pub mod bsd; + +#[cfg(target_os = "linux")] +pub mod linux; + +#[cfg(target_os = "windows")] +pub mod windows; + +pub trait MemorySource: Sized { + fn new() -> io::Result; + fn get_total_kb(&mut self) -> io::Result; + fn get_available_kb(&mut self) -> io::Result; +} + +#[cfg(target_os = "linux")] +pub type PlatformMemorySource = linux::LinuxMemorySource; + +#[cfg(target_os = "freebsd")] +pub type PlatformMemorySource = bsd::BsdMemorySource; + +#[cfg(target_os = "windows")] +pub type PlatformMemorySource = windows::WindowsMemorySource; + +pub struct MemoryPlugin; + +impl GuestAgentPlugin for MemoryPlugin { + async fn run(self, channel: flume::Sender) { + let mut timer = smol::Timer::interval(Duration::from_secs_f32(5.0)); + let mut memory_source = + PlatformMemorySource::new().expect("Unable to get memory information"); + + loop { + timer.next().await; + + if channel + .send_async(guest_metrics::GuestMetric::Memory(MemoryInfo { + mem_free: memory_source.get_available_kb().unwrap(), + mem_total: memory_source.get_total_kb().unwrap(), + })) + .await + .is_err() + { + break; + } + } + } +} diff --git a/providers/provider-memory/src/linux.rs b/providers/provider-memory/src/linux.rs new file mode 100644 index 0000000..9f30485 --- /dev/null +++ b/providers/provider-memory/src/linux.rs @@ -0,0 +1,48 @@ +use std::fs::File; +use std::io::{self, Read, Seek}; + +use super::MemorySource; + +pub struct LinuxMemorySource { + meminfo: File, +} + +impl LinuxMemorySource { + fn get_num_field(&mut self, tag: &str) -> io::Result { + self.meminfo.rewind()?; + let mut rawdata = String::new(); + self.meminfo.read_to_string(&mut rawdata)?; + let tagindex = rawdata.find(tag).ok_or(io::Error::new( + io::ErrorKind::InvalidData, + format!("could not find {tag}"), + ))?; + let numindex = rawdata[tagindex + tag.len()..] + .find(|c: char| c.is_ascii_digit()) + .ok_or(io::Error::new( + io::ErrorKind::InvalidData, + format!("no number after {tag}"), + ))?; + let num_start = tagindex + tag.len() + numindex; + let num_end = num_start + + (rawdata[num_start..] + .find(|c: char| !c.is_ascii_digit()) + .unwrap()); + Ok(rawdata[num_start..num_end].parse().unwrap()) + } +} + +impl MemorySource for LinuxMemorySource { + fn new() -> io::Result { + let meminfo = File::open("/proc/meminfo")?; + Ok(Self { meminfo }) + } + + fn get_total_kb(&mut self) -> io::Result { + self.get_num_field("MemTotal:") + .map(|m| /* Convert from KB */ m * 1024) + } + fn get_available_kb(&mut self) -> io::Result { + self.get_num_field("MemAvailable:") + .map(|m| /* Convert from KB */ m * 1024) + } +} diff --git a/providers/provider-memory/src/windows.rs b/providers/provider-memory/src/windows.rs new file mode 100644 index 0000000..b571930 --- /dev/null +++ b/providers/provider-memory/src/windows.rs @@ -0,0 +1,30 @@ +use std::io; + +use windows::Win32::System::SystemInformation::{GlobalMemoryStatusEx, MEMORYSTATUSEX}; + +use crate::MemorySource; + +#[derive(Default)] +pub struct WindowsMemorySource; + +fn read_memstatus_ex() -> io::Result { + let mut mem_status = MEMORYSTATUSEX::default(); + mem_status.dwLength = size_of_val(&mem_status) as u32; + + unsafe { GlobalMemoryStatusEx(&mut mem_status).map_err(io::Error::other)? }; + Ok(mem_status) +} + +impl MemorySource for WindowsMemorySource { + fn new() -> io::Result { + Ok(Self) + } + + fn get_total_kb(&mut self) -> io::Result { + Ok(read_memstatus_ex()?.ullTotalPhys as usize) + } + + fn get_available_kb(&mut self) -> io::Result { + Ok(read_memstatus_ex()?.ullAvailPhys as usize) + } +} diff --git a/providers/provider-netlink/Cargo.toml b/providers/provider-netlink/Cargo.toml new file mode 100644 index 0000000..4758a82 --- /dev/null +++ b/providers/provider-netlink/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "provider-netlink" +version = "0.1.0" +edition = "2021" + +[dependencies] +futures = "0.3" +guest-metrics = { path = "../../guest-metrics" } +tokio = { version = "1", features = ["rt"] } +smol = "2.0.2" +flume = "0.11.1" +uuid = "1.11" +anyhow = "*" +log = "*" + +netlink-packet-core = { version = "0.7.0" } +netlink-packet-route = { version = ">=0.18.0, <0.20" } +netlink-proto = { version = "0.11.2" } +rtnetlink = { version = "0.14.0" } + +vif-detect = { path = "../vif-detect" } diff --git a/providers/provider-netlink/src/lib.rs b/providers/provider-netlink/src/lib.rs new file mode 100644 index 0000000..013ba43 --- /dev/null +++ b/providers/provider-netlink/src/lib.rs @@ -0,0 +1,223 @@ +use std::{collections::HashMap, io}; + +use guest_metrics::{plugin::GuestAgentPlugin, GuestMetric, NetEvent, NetEventOp, NetInterface}; +use vif_detect::VifDetector; + +use futures::{channel::mpsc::UnboundedReceiver, StreamExt}; +use uuid::Uuid; + +use netlink_packet_core::{ + NetlinkHeader, NetlinkMessage, NetlinkPayload, NLM_F_DUMP, NLM_F_REQUEST, +}; +use netlink_packet_route::{ + address::{AddressAttribute, AddressMessage}, + link::{LinkAttribute, LinkMessage}, + RouteNetlinkMessage, +}; +use netlink_proto::{ + new_connection, + sys::{protocols::NETLINK_ROUTE, AsyncSocket, SocketAddr}, +}; +use rtnetlink::constants::{RTMGRP_IPV4_IFADDR, RTMGRP_IPV6_IFADDR, RTMGRP_LINK}; + +struct NetlinkConnection { + handle: netlink_proto::ConnectionHandle, + messages: UnboundedReceiver<(NetlinkMessage, SocketAddr)>, +} + +impl NetlinkConnection { + fn new() -> io::Result { + let (mut connection, handle, messages) = new_connection(NETLINK_ROUTE)?; + // What kinds of broadcast messages we want to listen for. + let nl_mgroup_flags = RTMGRP_LINK | RTMGRP_IPV4_IFADDR | RTMGRP_IPV6_IFADDR; + let nl_addr = SocketAddr::new(0, nl_mgroup_flags); + connection + .socket_mut() + .socket_mut() + .bind(&nl_addr) + .expect("failed to bind to netlink"); + + tokio::spawn(connection); + Ok(Self { handle, messages }) + } +} + +#[derive(Default)] +pub struct NetlinkPlugin; + +impl GuestAgentPlugin for NetlinkPlugin { + async fn run(self, channel: flume::Sender) { + let connection = NetlinkConnection::new().unwrap(); + let vif_identify = vif_detect::PlatformVifDetector::default(); + let mut interfaces = HashMap::new(); + + // Create the netlink message that requests the links to be dumped + let mut nl_hdr = NetlinkHeader::default(); + nl_hdr.flags = NLM_F_DUMP | NLM_F_REQUEST; + // Send the request + let link_stream = connection + .handle + .request( + NetlinkMessage::new( + nl_hdr, + RouteNetlinkMessage::GetLink(LinkMessage::default()).into(), + ), + SocketAddr::new(0, 0), + ) + .unwrap(); + + // Create the netlink message that requests the addresses to be dumped + let mut nl_hdr = NetlinkHeader::default(); + nl_hdr.flags = NLM_F_DUMP | NLM_F_REQUEST; + // Send the request + let address_stream = connection + .handle + .request( + NetlinkMessage::new( + nl_hdr, + RouteNetlinkMessage::GetAddress(AddressMessage::default()).into(), + ), + SocketAddr::new(0, 0), + ) + .unwrap(); + + let mut stream = link_stream + .chain(address_stream) + .chain(connection.messages.map(|(msg, _)| msg)); + + while let Some(msg) = stream.next().await { + if let NetlinkPayload::InnerMessage(inner_msg) = msg.payload { + if let Err(e) = + process_message(inner_msg, &channel, &vif_identify, &mut interfaces).await + { + log::error!("Unable to process netlink message: {e}"); + } + } + } + } +} + +async fn process_message( + inner_msg: RouteNetlinkMessage, + channel: &flume::Sender, + vif_identify: &impl VifDetector, + interfaces: &mut HashMap, +) -> anyhow::Result<()> { + match inner_msg { + RouteNetlinkMessage::NewLink(link_message) | RouteNetlinkMessage::GetLink(link_message) => { + let Some(ifname) = + link_message + .attributes + .iter() + .find_map(|attribute| match attribute { + LinkAttribute::IfName(n) => Some(n), + _ => None, + }) + else { + log::warn!("Ignoring NewLink/GetLink message without ifname"); + return Ok(()); + }; + + let mac = link_message + .attributes + .iter() + .find_map(|attribute| match attribute { + LinkAttribute::Address(addr) => Some( + addr.iter() + .map(|b| format!("{b:02x}")) + .collect::>() + .join(":"), + ), + _ => None, + }); + + let Some(toolstack_iface) = + vif_identify.get_toolstack_interface(ifname, mac.as_deref()) + else { + log::debug!("Unknown interface {ifname} (mac: {mac:?})"); + return Ok(()); + }; + + let uuid = Uuid::new_v4(); + + interfaces.insert(link_message.header.index, uuid); + channel + .send_async(GuestMetric::AddIface(NetInterface { + uuid, + index: link_message.header.index, + name: ifname.clone(), + toolstack_iface, + })) + .await?; + } + RouteNetlinkMessage::DelLink(link_message) => { + let Some(&uuid) = interfaces.get(&link_message.header.index) else { + return Ok(()); + }; + + channel.send_async(GuestMetric::RmIface(uuid)).await.ok(); + } + RouteNetlinkMessage::NewAddress(address_message) + | RouteNetlinkMessage::GetAddress(address_message) => { + let Some(&iface_id) = interfaces.get(&address_message.header.index) else { + log::warn!( + "Ignoring NewAddress/GetAddress on unknown interface with index={}", + address_message.header.index + ); + return Ok(()); + }; + + let Some(&addr) = + address_message + .attributes + .iter() + .find_map(|attribute| match attribute { + AddressAttribute::Address(addr) => Some(addr), + _ => None, + }) + else { + log::debug!("Got NewAddress/GetAddress without IP."); + return Ok(()); + }; + + channel + .send_async(GuestMetric::Network(NetEvent { + iface_id, + op: NetEventOp::AddIp(addr), + })) + .await?; + } + RouteNetlinkMessage::DelAddress(address_message) => { + let Some(&iface_id) = interfaces.get(&address_message.header.index) else { + log::warn!( + "Ignoring DelAddress on unknown interface with index={}", + address_message.header.index + ); + return Ok(()); + }; + + let Some(&addr) = + address_message + .attributes + .iter() + .find_map(|attribute| match attribute { + AddressAttribute::Address(addr) => Some(addr), + _ => None, + }) + else { + log::debug!("Got DelAddress without IP."); + return Ok(()); + }; + + channel + .send_async(GuestMetric::Network(NetEvent { + iface_id, + op: NetEventOp::RmIp(addr), + })) + .await?; + } + _ => {} + } + + Ok(()) +} diff --git a/providers/provider-os/Cargo.toml b/providers/provider-os/Cargo.toml new file mode 100644 index 0000000..f999cab --- /dev/null +++ b/providers/provider-os/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "provider-os" +edition = "2021" +version.workspace = true +repository.workspace = true +categories.workspace = true + +[dependencies] +guest-metrics = { path = "../../guest-metrics" } +smol = "2.0.2" +flume = "0.11.1" + +[target.'cfg(unix)'.dependencies] +uname = "0.1.1" \ No newline at end of file diff --git a/providers/provider-os/src/lib.rs b/providers/provider-os/src/lib.rs new file mode 100644 index 0000000..c79aab7 --- /dev/null +++ b/providers/provider-os/src/lib.rs @@ -0,0 +1,33 @@ +use guest_metrics::{os_info, plugin::GuestAgentPlugin, GuestMetric, KernelInfo, OsInfo}; +use std::io; + +// UNIX uname() implementation +#[cfg(unix)] +pub fn collect_kernel() -> io::Result> { + let uname_info = uname::uname()?; + Ok(Some(KernelInfo { + release: uname_info.release, + })) +} + +// default implementation +#[cfg(not(unix))] +pub fn collect_kernel() -> io::Result> { + Ok(None) +} + +pub struct OsInfoPlugin; + +impl GuestAgentPlugin for OsInfoPlugin { + async fn run(self, channel: flume::Sender) { + let kernel_info = collect_kernel().expect("Unable to fetch kernel information"); + + channel + .send_async(GuestMetric::OperatingSystem(OsInfo { + os_info: os_info::get(), + kernel_info, + })) + .await + .ok(); + } +} diff --git a/providers/provider-simple/Cargo.toml b/providers/provider-simple/Cargo.toml new file mode 100644 index 0000000..8675e49 --- /dev/null +++ b/providers/provider-simple/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "provider-simple" +version = "0.1.0" +edition = "2021" + +[dependencies] +futures = "0.3" +guest-metrics = { path = "../../guest-metrics" } +uuid = { version = "1.11", features = ["v4"] } +smol = "2.0.2" +flume = "0.11.1" + +vif-detect = { path = "../vif-detect" } +network-interface = "2.0" diff --git a/providers/provider-simple/src/lib.rs b/providers/provider-simple/src/lib.rs new file mode 100644 index 0000000..52dd1c9 --- /dev/null +++ b/providers/provider-simple/src/lib.rs @@ -0,0 +1,177 @@ +use std::{collections::HashMap, time::Duration}; + +use futures::StreamExt; +use guest_metrics::{plugin::GuestAgentPlugin, GuestMetric, NetEvent, NetEventOp, NetInterface}; +use network_interface::{NetworkInterface, NetworkInterfaceConfig}; +use uuid::Uuid; +use vif_detect::{PlatformVifDetector, VifDetector}; + +#[derive(Default)] +pub struct SimpleNetworkPlugin { + interfaces: HashMap, + uuid_map: HashMap, +} + +impl GuestAgentPlugin for SimpleNetworkPlugin { + async fn run(mut self, channel: flume::Sender) { + let mut timer = smol::Timer::interval(Duration::from_secs_f32(5.0)); + let vif_detector = PlatformVifDetector::default(); + + loop { + self.track_interfaces(&vif_detector, &channel).await; + + timer.next().await; + } + } +} + +impl SimpleNetworkPlugin { + async fn track_interfaces( + &mut self, + vif_detector: &impl VifDetector, + channel: &flume::Sender, + ) { + let interfaces = network_interface::NetworkInterface::show().unwrap(); + + // Check for new interfaces (ones not in uuid_map) + let new_interfaces = interfaces + .iter() + .filter(|interface| !self.uuid_map.contains_key(&interface.name)) + .collect::>(); + + let removed_interfaces = self + .uuid_map + .keys() + .filter(|&name| !interfaces.iter().any(|interface| interface.name == *name)) + .cloned() + .collect::>(); + + let changed_interfaces = interfaces + .iter() + .filter(|&interface| { + if let Some(current) = self.interfaces.get(&interface.name) { + interface != current + } else { + false + } + }) + .collect::>(); + + for interface in new_interfaces { + let uuid = Uuid::new_v4(); + self.interfaces + .insert(interface.name.clone(), interface.clone()); + self.uuid_map.insert(interface.name.clone(), uuid); + + channel + .send_async(GuestMetric::AddIface(NetInterface { + uuid, + index: interface.index, + name: interface.name.clone(), + toolstack_iface: vif_detector + .get_toolstack_interface(&interface.name, interface.mac_addr.as_deref()) + .unwrap_or_default(), + })) + .await + .unwrap(); + + for addr in &interface.addr { + channel + .send_async(GuestMetric::Network(NetEvent { + iface_id: uuid, + op: NetEventOp::AddIp(addr.ip()), + })) + .await + .unwrap(); + } + + if let Some(mac) = interface.mac_addr.clone() { + channel + .send_async(GuestMetric::Network(NetEvent { + iface_id: uuid, + op: NetEventOp::AddMac(mac), + })) + .await + .unwrap(); + } + } + + for interface in removed_interfaces { + let uuid = self.uuid_map[&interface]; + + channel + .send_async(GuestMetric::RmIface(uuid)) + .await + .unwrap(); + self.interfaces.remove(&interface); + self.uuid_map.remove(&interface); + } + + for interface in changed_interfaces { + let current_interface = &self.interfaces[&interface.name]; + let uuid = self.uuid_map[&interface.name]; + + // Check what is added and what is removed + + // Added addresses + for addr in interface.addr.iter().filter(|&addr| { + current_interface + .addr + .iter() + .all(|current_addr| addr != current_addr) + }) { + channel + .send_async(GuestMetric::Network(NetEvent { + iface_id: uuid, + op: NetEventOp::AddIp(addr.ip()), + })) + .await + .unwrap(); + } + + // Removed addresses + for addr in current_interface.addr.iter().filter(|&addr| { + interface + .addr + .iter() + .all(|current_addr| addr != current_addr) + }) { + channel + .send_async(GuestMetric::Network(NetEvent { + iface_id: uuid, + op: NetEventOp::RmIp(addr.ip()), + })) + .await + .unwrap(); + } + + // Changed MAC + if interface.mac_addr != current_interface.mac_addr { + if let Some(mac) = current_interface.mac_addr.clone() { + // Remove MAC + channel + .send_async(GuestMetric::Network(NetEvent { + iface_id: uuid, + op: NetEventOp::RmMac(mac), + })) + .await + .unwrap() + } + + if let Some(mac) = interface.mac_addr.clone() { + // Remove MAC + channel + .send_async(GuestMetric::Network(NetEvent { + iface_id: uuid, + op: NetEventOp::AddMac(mac), + })) + .await + .unwrap() + } + } + + self.interfaces + .insert(interface.name.clone(), interface.clone()); + } + } +} diff --git a/src/collector_net_pnet.rs b/providers/provider-simple/src/original.rs similarity index 71% rename from src/collector_net_pnet.rs rename to providers/provider-simple/src/original.rs index a503cab..211bf1b 100644 --- a/src/collector_net_pnet.rs +++ b/providers/provider-simple/src/original.rs @@ -1,5 +1,3 @@ -use crate::datastructs::{NetEvent, NetEventOp, NetInterface, NetInterfaceCache}; -use async_stream::try_stream; use futures::stream::Stream; use ipnetwork::IpNetwork; use pnet_base::MacAddr; @@ -26,7 +24,10 @@ struct InterfaceInfo { impl InterfaceInfo { pub fn new(name: &str) -> InterfaceInfo { - InterfaceInfo { name: name.to_string(), addresses: HashSet::new() } + InterfaceInfo { + name: name.to_string(), + addresses: HashSet::new(), + } } } @@ -38,7 +39,10 @@ pub struct NetworkSource { impl NetworkSource { pub fn new(iface_cache: &'static mut NetInterfaceCache) -> io::Result { - Ok(NetworkSource {addresses_cache: AddressesState::new(), iface_cache}) + Ok(NetworkSource { + addresses_cache: AddressesState::new(), + iface_cache, + }) } pub async fn collect_current(&mut self) -> Result, Box> { @@ -87,13 +91,16 @@ impl NetworkSource { for (cached_iface_index, cached_info) in self.addresses_cache.iter() { // iface object from iface_cache let iface = match self.iface_cache.entry(*cached_iface_index) { - hash_map::Entry::Occupied(entry) => { entry.get().clone() }, + hash_map::Entry::Occupied(entry) => entry.get().clone(), hash_map::Entry::Vacant(_) => { return Err(io::Error::new( io::ErrorKind::InvalidData, - format!("disappearing interface with index {} not in iface_cache", - cached_iface_index))); - }, + format!( + "disappearing interface with index {} not in iface_cache", + cached_iface_index + ), + )); + } }; // notify addresses or full iface removal match current_addresses.get(cached_iface_index) { @@ -106,41 +113,51 @@ impl NetworkSource { op: match disappearing { Address::IP(ip) => NetEventOp::RmIp(ip.ip()), Address::MAC(mac) => NetEventOp::RmMac((*mac).to_string()), - }}); + }, + }); } - }, + } None => { - events.push(NetEvent{iface: iface.clone(), op: NetEventOp::RmIface}); - }, + events.push(NetEvent { + iface: iface.clone(), + op: NetEventOp::RmIface, + }); + } }; } // appearing addresses for (iface_index, iface_info) in current_addresses.iter() { - let iface = self.iface_cache + let iface = self + .iface_cache .entry(*iface_index) .or_insert_with_key(|index| { - let iface = Rc::new(RefCell::new( - NetInterface::new(*index, Some(iface_info.name.clone())))); - events.push(NetEvent{iface: iface.clone(), op: NetEventOp::AddIface}); + let iface = Rc::new(RefCell::new(NetInterface::new( + *index, + Some(iface_info.name.clone()), + ))); + events.push(NetEvent { + iface: iface.clone(), + op: NetEventOp::AddIface, + }); iface }) .clone(); - let cache_adresses = - if let Some(cache_info) = self.addresses_cache.get(iface_index) { - &cache_info.addresses - } else { - &empty_address_set - }; + let cache_adresses = if let Some(cache_info) = self.addresses_cache.get(iface_index) { + &cache_info.addresses + } else { + &empty_address_set + }; for appearing in iface_info.addresses.difference(cache_adresses) { log::trace!("appearing {}: {:?}", iface.borrow().name, appearing); - events.push(NetEvent{iface: iface.clone(), - op: match appearing { - Address::IP(ip) => NetEventOp::AddIp(ip.ip()), - Address::MAC(mac) => NetEventOp::AddMac((*mac).to_string()), - }}); + events.push(NetEvent { + iface: iface.clone(), + op: match appearing { + Address::IP(ip) => NetEventOp::AddIp(ip.ip()), + Address::MAC(mac) => NetEventOp::AddMac((*mac).to_string()), + }, + }); } - } // replace cache with view diff --git a/providers/vif-detect/Cargo.toml b/providers/vif-detect/Cargo.toml new file mode 100644 index 0000000..0b0c42b --- /dev/null +++ b/providers/vif-detect/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "vif-detect" +version = "0.1.0" +edition = "2021" + +[dependencies] +log = "*" +guest-metrics = { path = "../../guest-metrics" } + +[target.'cfg(target_os = "windows")'.dependencies] +xenstore-rs = "0.8" +xenstore-win = { git = "https://github.com/TSnake41/xenstore-win.git" } \ No newline at end of file diff --git a/providers/vif-detect/src/freebsd.rs b/providers/vif-detect/src/freebsd.rs new file mode 100644 index 0000000..5559d17 --- /dev/null +++ b/providers/vif-detect/src/freebsd.rs @@ -0,0 +1,26 @@ +use guest_metrics::ToolstackNetInterface; + +#[derive(Default)] +pub struct FreebsdVifDetector; + +impl super::VifDetector for FreebsdVifDetector { + // identifies a VIF as named "xn%ID" + fn get_toolstack_interface( + &self, + iface_name: &str, + _mac_addr: Option<&str>, + ) -> Option { + const PREFIX: &str = "xn"; + if !iface_name.starts_with(PREFIX) { + log::debug!("ignoring interface {iface_name} as not starting with '{PREFIX}'"); + return None; + } + + let index = iface_name[PREFIX.len()..] + .parse() + .inspect_err(|e| log::error!("cannot parse a VIF number adter {PREFIX}: {e}")) + .ok()?; + + Some(ToolstackNetInterface::Vif(index)) + } +} diff --git a/providers/vif-detect/src/lib.rs b/providers/vif-detect/src/lib.rs new file mode 100644 index 0000000..087a7d9 --- /dev/null +++ b/providers/vif-detect/src/lib.rs @@ -0,0 +1,25 @@ +use guest_metrics::ToolstackNetInterface; + +#[cfg(target_os = "freebsd")] +pub mod freebsd; +#[cfg(target_os = "linux")] +pub mod linux; +#[cfg(target_os = "windows")] +pub mod windows; + +pub trait VifDetector: Default { + fn get_toolstack_interface( + &self, + iface_name: &str, + mac_addr: Option<&str>, + ) -> Option; +} + +#[cfg(target_os = "linux")] +pub type PlatformVifDetector = linux::LinuxVifDetector; + +#[cfg(target_os = "freebsd")] +pub type PlatformVifDetector = freebsd::FreebsdVifDetector; + +#[cfg(target_os = "windows")] +pub type PlatformVifDetector = windows::WindowsVifDetector; diff --git a/providers/vif-detect/src/linux.rs b/providers/vif-detect/src/linux.rs new file mode 100644 index 0000000..4f5a6ce --- /dev/null +++ b/providers/vif-detect/src/linux.rs @@ -0,0 +1,48 @@ +use std::fs; + +use guest_metrics::ToolstackNetInterface; + +use super::VifDetector; + +// identifies a VIF from sysfs as devtype="vif", and take the VIF id +// from nodename="device/vif/$ID" + +// FIXME does not attempt to detect sr-iov VIFs + +#[derive(Default)] +pub struct LinuxVifDetector; + +impl VifDetector for LinuxVifDetector { + fn get_toolstack_interface( + &self, + iface_name: &str, + _mac_addr: Option<&str>, + ) -> Option { + // FIXME: using ETHTOOL ioctl could be better + let device_path = format!("/sys/class/net/{iface_name}/device"); + let Some(devtype) = fs::read_to_string(format!("{device_path}/devtype")) + .inspect_err(|e| log::debug!("reading {device_path}/devtype: {e}")) + .ok() + else { + return Some(ToolstackNetInterface::Unknown); + }; + + let "vif" = devtype.trim() else { + return Some(ToolstackNetInterface::Unknown); + }; + + let nodename = fs::read_to_string(format!("{device_path}/nodename")) + .inspect_err(|e| log::error!("reading {device_path}/nodename: {e}")) + .ok()?; + let nodename = nodename.trim(); + + const PREFIX: &str = "device/vif/"; + if !nodename.starts_with(PREFIX) { + log::debug!("ignoring interface {nodename} as not under {PREFIX}"); + return None; + } + let vif_id = nodename[PREFIX.len()..].parse().unwrap(); + + Some(ToolstackNetInterface::Vif(vif_id)) + } +} diff --git a/providers/vif-detect/src/windows.rs b/providers/vif-detect/src/windows.rs new file mode 100644 index 0000000..1e38e3a --- /dev/null +++ b/providers/vif-detect/src/windows.rs @@ -0,0 +1,47 @@ +use std::sync::Mutex; + +use guest_metrics::ToolstackNetInterface; +use xenstore_rs::Xs; +use xenstore_win::XsWindows; + +pub struct WindowsVifDetector(Option>); + +impl Default for WindowsVifDetector { + fn default() -> Self { + Self( + XsWindows::new() + .inspect_err(|e| log::warn!("Unable to load xenstore: {e}")) + .ok() + .map(Mutex::new), + ) + } +} + +impl super::VifDetector for WindowsVifDetector { + fn get_toolstack_interface( + &self, + iface_name: &str, + mac_addr: Option<&str>, + ) -> Option { + let xs = self.0.as_ref()?.lock().unwrap(); + let mac_addr = mac_addr?; + + log::info!("Probing {iface_name} (MAC: {mac_addr})"); + + for vif_id in xs.directory("device/vif").ok()? { + let Some(vif_mac) = xs.read(&format!("device/vif/{vif_id}/mac")).ok() else { + log::warn!("vif/{vif_id} has no MAC address"); + continue; + }; + + if mac_addr.trim().eq_ignore_ascii_case(vif_mac.trim()) { + log::info!("{iface_name} is vif/{vif_id}"); + return Some(ToolstackNetInterface::Vif( + vif_id.parse().expect("Unable to parse vif id"), + )); + } + } + + Some(ToolstackNetInterface::Unknown) + } +} diff --git a/publishers/publisher-console/Cargo.toml b/publishers/publisher-console/Cargo.toml new file mode 100644 index 0000000..69049f1 --- /dev/null +++ b/publishers/publisher-console/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "publisher-console" +edition = "2021" +version.workspace = true +repository.workspace = true +categories.workspace = true + +[dependencies] +guest-metrics = { path = "../../guest-metrics" } +uuid = "1.11" +flume = "0.11.1" \ No newline at end of file diff --git a/publishers/publisher-console/src/lib.rs b/publishers/publisher-console/src/lib.rs new file mode 100644 index 0000000..76030fc --- /dev/null +++ b/publishers/publisher-console/src/lib.rs @@ -0,0 +1,77 @@ +use std::collections::HashMap; + +use guest_metrics::{ + plugin::GuestAgentPublisher, GuestMetric, KernelInfo, NetEventOp, NetInterface, + ToolstackNetInterface, +}; +use uuid::Uuid; + +#[derive(Default)] +pub struct ConsolePublisher { + ifaces: HashMap, +} + +impl ConsolePublisher { + fn process_message(&mut self, metric: GuestMetric) { + match metric { + GuestMetric::OperatingSystem(os_info) => { + println!( + "OS: {} - Version: {}", + os_info.os_info.os_type(), + os_info.os_info.version() + ); + if let Some(KernelInfo { release }) = &os_info.kernel_info { + println!("Kernel version: {release}"); + } + } + GuestMetric::Memory(memory_info) => { + println!( + "Memory: {}/{} KB", + memory_info.mem_free / 1024, + memory_info.mem_total / 1024 + ); + } + GuestMetric::AddIface(iface) => { + let ifkind = match iface.toolstack_iface { + ToolstackNetInterface::Unknown => String::from("unknown"), + ToolstackNetInterface::Vif(id) => format!("vif/{id}"), + _ => todo!(), + }; + println!("{} +IFACE ({})", iface.index, ifkind); + self.ifaces.insert(iface.uuid, iface); + } + GuestMetric::RmIface(iface_id) => { + let Some((_, iface)) = self.ifaces.remove_entry(&iface_id) else { + return; + }; + + println!("{} -IFACE", iface.index); + } + GuestMetric::Network(net_event) => { + let Some(iface) = self.ifaces.get(&net_event.iface_id) else { + return; + }; + + match &net_event.op { + NetEventOp::AddIp(address) => println!("{} +IP {address}", iface.index), + NetEventOp::RmIp(address) => println!("{} -IP {address}", iface.index), + NetEventOp::AddMac(mac_address) => { + println!("{} +MAC {mac_address}", iface.index) + } + NetEventOp::RmMac(mac_address) => { + println!("{} -MAC {mac_address}", iface.index) + } + } + } + GuestMetric::CleanupIfaces => {} + } + } +} + +impl GuestAgentPublisher for ConsolePublisher { + async fn run(mut self, channel: flume::Receiver) { + while let Ok(msg) = channel.recv_async().await { + self.process_message(msg) + } + } +} diff --git a/publishers/publisher-xenstore/Cargo.toml b/publishers/publisher-xenstore/Cargo.toml new file mode 100644 index 0000000..34bdfd2 --- /dev/null +++ b/publishers/publisher-xenstore/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "publisher-xenstore" +version = "0.1.0" +edition = "2021" + +[dependencies] +guest-metrics = { path = "../../guest-metrics" } +uuid = "1.11" +flume = "0.11.1" +log = "*" +xenstore-rs = "0.8" + +# Also use xenstore-win on Windows +[target.'cfg(target_os = "windows")'.dependencies] +xenstore-win = { git = "https://github.com/TSnake41/xenstore-win.git" } \ No newline at end of file diff --git a/publishers/publisher-xenstore/src/lib.rs b/publishers/publisher-xenstore/src/lib.rs new file mode 100644 index 0000000..40e2eef --- /dev/null +++ b/publishers/publisher-xenstore/src/lib.rs @@ -0,0 +1,51 @@ +mod rfc; +mod std; + +use ::std::io; + +use guest_metrics::plugin::GuestAgentPublisher; +use xenstore_rs::Xs; + +pub fn xs_publish(xs: &impl Xs, key: &str, value: &str) -> io::Result<()> { + log::trace!("+ {}={:?}", key, value); + xs.write(key, value) +} + +pub fn xs_unpublish(xs: &impl Xs, key: &str) -> io::Result<()> { + log::trace!("- {}", key); + xs.rm(key) +} + +pub struct XenstoreRfcPublisher; + +impl GuestAgentPublisher for XenstoreRfcPublisher { + async fn run(self, channel: flume::Receiver) { + #[cfg(not(target_os = "windows"))] + let xs = xenstore_rs::unix::XsUnix::new().expect("Unable to initialize xenstore"); + + #[cfg(target_os = "windows")] + let xs = xenstore_win::XsWindows::new().expect("Unable to initialize xenstore"); + + rfc::XenstoreRfc::new(xs) + .run(channel) + .await + .expect("Xenstore failure") + } +} + +pub struct XenstoreStdPublisher; + +impl GuestAgentPublisher for XenstoreStdPublisher { + async fn run(self, channel: flume::Receiver) { + #[cfg(not(target_os = "windows"))] + let xs = xenstore_rs::unix::XsUnix::new().expect("Unable to initialize xenstore"); + + #[cfg(target_os = "windows")] + let xs = xenstore_win::XsWindows::new().expect("Unable to initialize xenstore"); + + std::XenstoreStd::new(xs) + .run(channel) + .await + .expect("Xenstore failure") + } +} diff --git a/publishers/publisher-xenstore/src/rfc.rs b/publishers/publisher-xenstore/src/rfc.rs new file mode 100644 index 0000000..c4efc31 --- /dev/null +++ b/publishers/publisher-xenstore/src/rfc.rs @@ -0,0 +1,133 @@ +use guest_metrics::{ + GuestMetric, MemoryInfo, NetEvent, NetEventOp, NetInterface, OsInfo, ToolstackNetInterface, +}; +use std::collections::HashMap; +use std::io; +use std::net::IpAddr; +use uuid::Uuid; +use xenstore_rs::Xs; + +use super::{xs_publish, xs_unpublish}; + +#[derive(Clone)] +pub struct XenstoreRfc { + xs: XS, + + ifaces: HashMap, +} + +const PROTOCOL_VERSION: &str = env!("CARGO_PKG_VERSION"); + +fn iface_prefix(iface_id: u32) -> String { + format!("data/net/{iface_id}") +} + +// FIXME: this should be a runtime config of xenstore-std.rs + +impl XenstoreRfc { + pub fn new(xs: XS) -> Self { + XenstoreRfc { + xs, + ifaces: HashMap::new(), + } + } + + fn publish_osinfo(&mut self, info: &OsInfo) -> io::Result<()> { + xs_publish(&self.xs, "data/xen-guest-agent", PROTOCOL_VERSION)?; + xs_publish( + &self.xs, + "data/os/name", + &format!("{} {}", info.os_info.os_type(), info.os_info.version()), + )?; + xs_publish( + &self.xs, + "data/os/version", + &info.os_info.version().to_string(), + )?; + xs_publish(&self.xs, "data/os/class", "unix")?; + if let Some(kernel_info) = &info.kernel_info { + xs_publish( + &self.xs, + "data/os/unix/kernel-version", + &kernel_info.release, + )?; + } + + Ok(()) + } + + fn cleanup_ifaces(&mut self) -> io::Result<()> { + // Currently only vif interfaces are cleaned + xs_unpublish(&self.xs, "data/net") + } + + fn publish_memory(&mut self, _mem_info: &MemoryInfo) -> io::Result<()> { + //xs_publish(&self.xs, "data/meminfo_free", &mem_free_kb.to_string())?; + Ok(()) + } + + #[allow(clippy::useless_format)] + fn publish_netevent(&mut self, event: &NetEvent) -> io::Result<()> { + let Some(iface) = self.ifaces.get(&event.iface_id) else { + return Err(io::Error::new( + io::ErrorKind::NotFound, + format!("Got event from unknown interface ({})", event.iface_id), + )); + }; + + let ToolstackNetInterface::Vif(iface_id) = iface.toolstack_iface else { + log::warn!("Got event from unsupported interface {:?}", iface); + return Ok(()); + }; + + let xs_iface_prefix = iface_prefix(iface_id); + match &event.op { + NetEventOp::AddIp(address) => { + let key_suffix = munged_address(address); + xs_publish(&self.xs, &format!("{xs_iface_prefix}/{key_suffix}"), "")?; + } + NetEventOp::RmIp(address) => { + let key_suffix = munged_address(address); + xs_unpublish(&self.xs, &format!("{xs_iface_prefix}/{key_suffix}"))?; + } + NetEventOp::AddMac(mac_address) => { + xs_publish(&self.xs, &format!("{xs_iface_prefix}"), mac_address)? + } + NetEventOp::RmMac(_) => xs_unpublish(&self.xs, &format!("{xs_iface_prefix}"))?, + } + Ok(()) + } + + pub async fn run(mut self, channel: flume::Receiver) -> io::Result<()> { + while let Ok(metric) = channel.recv_async().await { + match metric { + GuestMetric::OperatingSystem(os_info) => self.publish_osinfo(&os_info)?, + GuestMetric::Memory(memory_info) => self.publish_memory(&memory_info)?, + GuestMetric::Network(net_event) => self.publish_netevent(&net_event)?, + GuestMetric::CleanupIfaces => self.cleanup_ifaces()?, + GuestMetric::AddIface(net_interface) => { + if let ToolstackNetInterface::Vif(iface_id) = net_interface.toolstack_iface { + xs_publish(&self.xs, &iface_prefix(iface_id), "")?; + } + self.ifaces.insert(net_interface.uuid, net_interface); + } + GuestMetric::RmIface(uuid) => { + if let Some(interface) = self.ifaces.remove(&uuid) { + if let ToolstackNetInterface::Vif(iface_id) = interface.toolstack_iface { + xs_unpublish(&self.xs, &iface_prefix(iface_id))?; + } + } + } + } + } + + Ok(()) + } +} + +fn munged_address(addr: &IpAddr) -> String { + match addr { + IpAddr::V4(addr) => "ipv4/".to_string() + &addr.to_string().replace('.', "_"), + IpAddr::V6(addr) => "ipv6/".to_string() + &addr.to_string().replace(':', "_"), + } +} diff --git a/publishers/publisher-xenstore/src/std.rs b/publishers/publisher-xenstore/src/std.rs new file mode 100644 index 0000000..2a422e2 --- /dev/null +++ b/publishers/publisher-xenstore/src/std.rs @@ -0,0 +1,247 @@ +use guest_metrics::{ + os_info, GuestMetric, MemoryInfo, NetEvent, NetEventOp, NetInterface, OsInfo, + ToolstackNetInterface, +}; +use std::collections::HashMap; +use std::io; +use std::net::IpAddr; +use uuid::Uuid; +use xenstore_rs::Xs; + +use super::{xs_publish, xs_unpublish}; + +pub struct XenstoreStd { + xs: XS, + // use of integer indices for IP addresses requires to keep a mapping + ip_addresses: IpList, + + // control/feature-balloon is a control node of XAPI's squeezed, + // and gets created by the guest because xenopsd sets ~/control/ + // with domain ownership. OTOH libxl creates it readonly, so we + // catch the case where it is so to avoid uselessly retrying. + forbidden_control_feature_balloon: bool, + + ifaces: HashMap, +} + +const NUM_IFACE_IPS: usize = 10; +type IfaceIpList = [Option; NUM_IFACE_IPS]; +struct IfaceIpStruct { + v4: IfaceIpList, + v6: IfaceIpList, +} +type IpList = HashMap; + +// pseudo version for xe-daemon compatibility, real agent version in +// BuildVersion below +const AGENT_VERSION_MAJOR: &str = "1"; // XO does not show version at all if 0 +const AGENT_VERSION_MINOR: &str = "0"; +const AGENT_VERSION_MICRO: &str = "0"; // XAPI exposes "-1" if missing + +impl XenstoreStd { + pub fn new(xs: XS) -> Self { + let ip_addresses = IpList::new(); + XenstoreStd { + xs, + ip_addresses, + forbidden_control_feature_balloon: false, + ifaces: HashMap::new(), + } + } +} + +fn iface_prefix(iface_id: u32) -> String { + format!("attr/vif/{iface_id}") +} + +impl XenstoreStd { + fn publish_osinfo(&mut self, info: &OsInfo) -> io::Result<()> { + // FIXME this is not anywhere standard, just minimal XS compatibility + xs_publish(&self.xs, "attr/PVAddons/MajorVersion", AGENT_VERSION_MAJOR)?; + xs_publish(&self.xs, "attr/PVAddons/MinorVersion", AGENT_VERSION_MINOR)?; + xs_publish(&self.xs, "attr/PVAddons/MicroVersion", AGENT_VERSION_MICRO)?; + let agent_version_build = format!("proto-{}", &env!("CARGO_PKG_VERSION")); + xs_publish(&self.xs, "attr/PVAddons/BuildVersion", &agent_version_build)?; + + xs_publish( + &self.xs, + "data/os_distro", + &info.os_info.os_type().to_string(), + )?; + xs_publish( + &self.xs, + "data/os_name", + &format!("{} {}", info.os_info.os_type(), info.os_info.version()), + )?; + // FIXME .version only has "major" component right now; not a + // big deal for a proto, os_minorver is known to be unreliable + // in xe-guest-utilities at least for Debian + let os_version = info.os_info.version(); + match os_version { + os_info::Version::Semantic(major, minor, _patch) => { + xs_publish(&self.xs, "data/os_majorver", &major.to_string())?; + xs_publish(&self.xs, "data/os_minorver", &minor.to_string())?; + } + _ => { + // FIXME what to do with strings? + // the lack of `os_*ver` is anyway not a big deal + log::info!("cannot parse yet os version {:?}", os_version); + } + } + if let Some(kernel_info) = &info.kernel_info { + xs_publish(&self.xs, "data/os_uname", &kernel_info.release)?; + } + + if !self.forbidden_control_feature_balloon { + // we may want to be more clever some day, e.g. by + // checking if the guest indeed has ballooning, and if the + // balloon driver has reached the requested initial + // `~/memory/target` value (or, possibly, to rely on the + // balloon driver to do the job of signaling this + // condition) + match xs_publish(&self.xs, "control/feature-balloon", "1") { + Err(e) if e.kind() == io::ErrorKind::PermissionDenied => { + log::warn!("cannot write control/feature-balloon (impacts XAPI's squeezed)"); + self.forbidden_control_feature_balloon = true; + } + Ok(_) => (), + e => return e, + } + } + + Ok(()) + } + + fn publish_memory(&mut self, mem_info: &MemoryInfo) -> io::Result<()> { + xs_publish( + &self.xs, + "data/meminfo_free", + &(mem_info.mem_free / 1024).to_string(), + )?; + xs_publish( + &self.xs, + "data/meminfo_total", + &(mem_info.mem_total / 1024).to_string(), + )?; + + Ok(()) + } + + // see https://xenbits.xen.org/docs/unstable/misc/xenstore-paths.html#domain-controlled-paths + fn publish_netevent(&mut self, event: &NetEvent) -> io::Result<()> { + let Some(iface) = self.ifaces.get(&event.iface_id) else { + return Err(io::Error::new( + io::ErrorKind::NotFound, + format!("Got event from unknown interface ({})", event.iface_id), + )); + }; + + let ToolstackNetInterface::Vif(iface_id) = iface.toolstack_iface else { + log::warn!("Got event from unsupported interface {:?}", iface); + return Ok(()); + }; + + let xs_iface_prefix = iface_prefix(iface_id); + match &event.op { + NetEventOp::AddIp(address) => { + let key_suffix = self.munged_address(address, iface_id)?; + xs_publish( + &self.xs, + &format!("{xs_iface_prefix}/{key_suffix}"), + &address.to_string(), + )?; + } + NetEventOp::RmIp(address) => { + let key_suffix = self.munged_address(address, iface_id)?; + xs_unpublish(&self.xs, &format!("{xs_iface_prefix}/{key_suffix}"))?; + } + + // FIXME extend IfaceIpStruct for this + NetEventOp::AddMac(_mac_address) => { + log::debug!("AddMac not applied") + } + NetEventOp::RmMac(_mac_address) => { + log::debug!("RmMac not applied") + } + } + Ok(()) + } + + fn cleanup_ifaces(&mut self) -> io::Result<()> { + // Currently only vif interfaces are cleaned + xs_unpublish(&self.xs, "attr/vif") + } + + fn munged_address(&mut self, addr: &IpAddr, iface_index: u32) -> io::Result { + let ip_entry = self + .ip_addresses + .entry(iface_index) + .or_insert(IfaceIpStruct { + v4: [None; NUM_IFACE_IPS], + v6: [None; NUM_IFACE_IPS], + }); + let ip_list = match addr { + IpAddr::V4(_) => &mut ip_entry.v4, + IpAddr::V6(_) => &mut ip_entry.v6, + }; + let ip_slot = get_ip_slot(addr, ip_list)?; + match addr { + IpAddr::V4(_) => Ok(format!("ipv4/{ip_slot}")), + IpAddr::V6(_) => Ok(format!("ipv6/{ip_slot}")), + } + } + + pub async fn run(mut self, channel: flume::Receiver) -> io::Result<()> { + while let Ok(metric) = channel.recv_async().await { + match metric { + GuestMetric::OperatingSystem(os_info) => self.publish_osinfo(&os_info)?, + GuestMetric::Memory(memory_info) => self.publish_memory(&memory_info)?, + GuestMetric::Network(net_event) => self.publish_netevent(&net_event)?, + GuestMetric::CleanupIfaces => self.cleanup_ifaces()?, + GuestMetric::AddIface(net_interface) => { + if let ToolstackNetInterface::Vif(iface_id) = net_interface.toolstack_iface { + xs_publish(&self.xs, &iface_prefix(iface_id), "")?; + } + + self.ifaces.insert(net_interface.uuid, net_interface); + } + GuestMetric::RmIface(uuid) => { + if let Some(interface) = self.ifaces.remove(&uuid) { + if let ToolstackNetInterface::Vif(iface_id) = interface.toolstack_iface { + xs_unpublish(&self.xs, &iface_prefix(iface_id))?; + } + } + } + } + } + + Ok(()) + } +} + +fn get_ip_slot(ip: &IpAddr, list: &mut IfaceIpList) -> io::Result { + let mut empty_idx: Option = None; + for (idx, item) in list.iter().enumerate() { + match item { + Some(item) => { + if item == ip { + return Ok(idx); + } + } // found + None => { + if empty_idx.is_none() { + empty_idx = Some(idx) + } + } + } + } + // not found, insert in empty space if possible + if let Some(idx) = empty_idx { + list[idx] = Some(*ip); + return Ok(idx); + } + Err(io::Error::new( + io::ErrorKind::OutOfMemory, /*StorageFull?*/ + "no free slot for a new IP address", + )) +} diff --git a/src/collector_memory.rs b/src/collector_memory.rs deleted file mode 100644 index 3800dde..0000000 --- a/src/collector_memory.rs +++ /dev/null @@ -1,16 +0,0 @@ -use std::io; - -pub struct MemorySource {} - -impl MemorySource { - pub fn new() -> io::Result { - Ok(MemorySource {}) - } - - pub fn get_total_kb(&mut self) -> io::Result { - Err(io::Error::new(io::ErrorKind::Unsupported, "no implementation for mem_total")) - } - pub fn get_available_kb(&mut self) -> io::Result { - Err(io::Error::new(io::ErrorKind::Unsupported, "no implementation for mem_avail")) - } -} diff --git a/src/collector_memory_bsd.rs b/src/collector_memory_bsd.rs deleted file mode 100644 index 26e8b0d..0000000 --- a/src/collector_memory_bsd.rs +++ /dev/null @@ -1,88 +0,0 @@ -use std::io; -use sysctl::Sysctl; - -pub struct MemorySource { - memtotal_ctl: sysctl::Ctl, - pagesize: usize, - meminactive_ctl: sysctl::Ctl, - memcache_ctl: sysctl::Ctl, - memfree_ctl: sysctl::Ctl, -} - -impl MemorySource { - pub fn new() -> io::Result { - let pagesize_ctl = new_sysctl("hw.pagesize")?; - let pagesize_value = pagesize_ctl.value().map_err(sysctrlerror_to_ioerror)?; - let pagesize = *pagesize_value - .as_int().ok_or(io::Error::new(io::ErrorKind::InvalidData, - "cannot parse hw.pagesize as int"))? - as usize; - Ok(MemorySource { memtotal_ctl: new_sysctl("hw.physmem")?, - pagesize, - meminactive_ctl: new_sysctl("vm.stats.vm.v_inactive_count")?, - memcache_ctl: new_sysctl("vm.stats.vm.v_cache_count")?, - memfree_ctl: new_sysctl("vm.stats.vm.v_free_count")?, - }) - } - - pub fn get_total_kb(&mut self) -> io::Result { - Ok(get_field_ulong(&self.memtotal_ctl)? as usize / 1024) - } - pub fn get_available_kb(&mut self) -> io::Result { - let available = (get_field_u32(&self.meminactive_ctl)? as usize + - get_field_uint(&self.memcache_ctl)? as usize + - get_field_u32(&self.memfree_ctl)? as usize) * self.pagesize; - Ok(available / 1024) - } -} - -// helper to create a sysctl with errors mapped to io::Error -fn new_sysctl(name: &str) -> io::Result { - match sysctl::Ctl::new(name) { - Err(sysctl::SysctlError::NotFound(_)) => - Err(io::Error::new(io::ErrorKind::NotFound, format!("sysctl {} not found", name))), - Err(e) => - Err(io::Error::new(io::ErrorKind::Other, - format!("Unexpected error type creating sysctl::Ctl: {:?}", e))), - Ok(ctl) => Ok(ctl), - } -} - -fn sysctrlerror_to_ioerror(error: sysctl::SysctlError) -> io::Error { - match error { - e => io::Error::new(io::ErrorKind::Other, format!("sysctl error: {:?}", e)), - } -} - -fn get_field_ulong(ctl: &sysctl::Ctl) -> io::Result { - let v = ctl.value().map_err(sysctrlerror_to_ioerror)?; - if let Some(value) = v.as_ulong() { - Ok(*value) - } else { - Err(io::Error::new(io::ErrorKind::Other, - format!("cannot interpret {} as ulong: {:?}", - ctl.name().map_err(sysctrlerror_to_ioerror)?, v))) - } -} - -fn get_field_u32(ctl: &sysctl::Ctl) -> io::Result { - let v = ctl.value().map_err(sysctrlerror_to_ioerror)?; - if let Some(value) = v.as_u32() { - Ok(*value) - } else { - Err(io::Error::new(io::ErrorKind::Other, - format!("cannot interpret {} as u32: {:?}", - ctl.name().map_err(sysctrlerror_to_ioerror)?, v))) - } -} - -fn get_field_uint(ctl: &sysctl::Ctl) -> io::Result { - let v = ctl.value().map_err(sysctrlerror_to_ioerror)?; - if let Some(value) = v.as_uint() { - Ok(*value) - } else { - Err(io::Error::new(io::ErrorKind::Other, - format!("cannot interpret {} as uint: {:?}", - ctl.name().map_err(sysctrlerror_to_ioerror)?, v))) - } -} diff --git a/src/collector_memory_linux.rs b/src/collector_memory_linux.rs deleted file mode 100644 index 0a850be..0000000 --- a/src/collector_memory_linux.rs +++ /dev/null @@ -1,39 +0,0 @@ -use std::fs::File; -use std::io::{self, Read, Seek}; - -pub struct MemorySource { - meminfo: File, -} - -impl MemorySource { - pub fn new() -> io::Result { - let meminfo = File::open("/proc/meminfo")?; - Ok(MemorySource { meminfo }) - } - - pub fn get_total_kb(&mut self) -> io::Result { - self.get_num_field("MemTotal:") - } - pub fn get_available_kb(&mut self) -> io::Result { - self.get_num_field("MemAvailable:") - } - - fn get_num_field(&mut self, tag: &str) -> io::Result { - self.meminfo.rewind()?; - let mut rawdata = String::new(); - self.meminfo.read_to_string(&mut rawdata)?; - let tagindex = rawdata - .find(tag) - .ok_or(io::Error::new(io::ErrorKind::InvalidData, - format!("could not find {tag}")))?; - let numindex = rawdata[tagindex + tag.len()..] - .find(|c: char| c.is_ascii_digit()) - .ok_or(io::Error::new(io::ErrorKind::InvalidData, - format!("no number after {tag}")))?; - let num_start = tagindex + tag.len() + numindex; - let num_end = num_start + (rawdata[num_start..] - .find(|c: char| !c.is_ascii_digit()) - .unwrap()); - Ok(rawdata[num_start..num_end].parse().unwrap()) - } -} diff --git a/src/collector_net.rs b/src/collector_net.rs deleted file mode 100644 index cd35e18..0000000 --- a/src/collector_net.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::datastructs::{NetEvent, NetInterfaceCache}; -use futures::stream::Stream; -use std::error::Error; -use std::io; - -pub struct NetworkSource {} - -impl NetworkSource { - pub fn new(_cache: &'static mut NetInterfaceCache) -> io::Result { - Ok(NetworkSource {}) - } - - pub async fn collect_current(&mut self) -> Result, Box> { - Ok(vec![]) - } - - pub fn stream(&mut self) -> impl Stream> + '_ { - futures::stream::empty::>() - } -} diff --git a/src/collector_net_netlink.rs b/src/collector_net_netlink.rs deleted file mode 100644 index 9659391..0000000 --- a/src/collector_net_netlink.rs +++ /dev/null @@ -1,203 +0,0 @@ -use crate::datastructs::{NetEvent, NetEventOp, NetInterface, NetInterfaceCache}; -use async_stream::try_stream; -use futures::channel::mpsc::UnboundedReceiver; -use futures::stream::{Stream, StreamExt}; -use netlink_packet_core::{ - NetlinkHeader, NetlinkMessage, NetlinkPayload, NLM_F_DUMP, NLM_F_REQUEST, -}; -use netlink_packet_route::{ - address, address::AddressMessage, link, link::LinkMessage, RouteNetlinkMessage, -}; -use netlink_proto::{ - self, new_connection, - sys::{protocols::NETLINK_ROUTE, AsyncSocket, SocketAddr}, -}; -use rtnetlink::constants::{RTMGRP_IPV4_IFADDR, RTMGRP_IPV6_IFADDR, RTMGRP_LINK}; -use std::cell::RefCell; -use std::collections::hash_map; -use std::error::Error; -use std::io; -use std::net::IpAddr; -use std::rc::Rc; -use std::vec::Vec; - -pub struct NetworkSource { - handle: netlink_proto::ConnectionHandle, - messages: UnboundedReceiver<(NetlinkMessage, SocketAddr)>, - iface_cache: &'static mut NetInterfaceCache, -} - -impl NetworkSource { - pub fn new(iface_cache: &'static mut NetInterfaceCache) -> io::Result { - let (mut connection, handle, messages) = new_connection(NETLINK_ROUTE)?; - // What kinds of broadcast messages we want to listen for. - let nl_mgroup_flags = RTMGRP_LINK | RTMGRP_IPV4_IFADDR | RTMGRP_IPV6_IFADDR; - let nl_addr = SocketAddr::new(0, nl_mgroup_flags); - connection - .socket_mut() - .socket_mut() - .bind(&nl_addr) - .expect("failed to bind"); - tokio::spawn(connection); - Ok(NetworkSource { handle, messages, iface_cache }) - } - - pub async fn collect_current(&mut self) -> Result, Box> { - let mut events = Vec::::new(); - - // Create the netlink message that requests the links to be dumped - let mut nl_hdr = NetlinkHeader::default(); - nl_hdr.flags = NLM_F_DUMP | NLM_F_REQUEST; - let nl_msg = NetlinkMessage::new( - nl_hdr, - RouteNetlinkMessage::GetLink(LinkMessage::default()).into(), - ); - // Send the request - let mut nl_response = self.handle.request(nl_msg, SocketAddr::new(0, 0))?; - // Handle response - while let Some(packet) = nl_response.next().await { - if let NetlinkMessage{payload: NetlinkPayload::InnerMessage(msg), ..} = packet { - events.extend(self.netevent_from_rtnetlink(&msg)?); - } - } - - // Create the netlink message that requests the addresses to be dumped - let mut nl_hdr = NetlinkHeader::default(); - nl_hdr.flags = NLM_F_DUMP | NLM_F_REQUEST; - let nl_msg = NetlinkMessage::new( - nl_hdr, - RouteNetlinkMessage::GetAddress(AddressMessage::default()).into(), - ); - // Send the request - let mut nl_response = self.handle.request(nl_msg, SocketAddr::new(0, 0))?; - // Handle response - while let Some(packet) = nl_response.next().await { - if let NetlinkMessage{payload: NetlinkPayload::InnerMessage(msg), ..} = packet { - events.extend(self.netevent_from_rtnetlink(&msg)?); - } - } - - Ok(events) - } - - pub fn stream(&mut self) -> impl Stream> + '_ { - try_stream! { - while let Some((message, _)) = self.messages.next().await { - if let NetlinkMessage{payload: NetlinkPayload::InnerMessage(msg), ..} = message { - for event in self.netevent_from_rtnetlink(&msg)? { - yield event; - } - } - }; - } - } - - fn netevent_from_rtnetlink(&mut self, nl_msg: &RouteNetlinkMessage) - -> io::Result> { - let mut events = Vec::::new(); - match nl_msg { - RouteNetlinkMessage::NewLink(link_msg) => { - let (iface, mac_address) = self.nl_linkmessage_decode(link_msg)?; - log::debug!("NewLink({iface:?} {mac_address:?})"); - events.push(NetEvent{iface: iface.clone(), op: NetEventOp::AddIface}); - if let Some(mac_address) = mac_address { - events.push(NetEvent{iface, op: NetEventOp::AddMac(mac_address)}); - } - }, - RouteNetlinkMessage::DelLink(link_msg) => { - let (iface, mac_address) = self.nl_linkmessage_decode(link_msg)?; - log::debug!("DelLink({iface:?} {mac_address:?})"); - if let Some(mac_address) = mac_address { - events.push(NetEvent{iface: iface.clone(), - op: NetEventOp::RmMac(mac_address)}); // redundant - } - events.push(NetEvent{iface, op: NetEventOp::RmIface}); - }, - RouteNetlinkMessage::NewAddress(address_msg) => { - // FIXME does not distinguish when IP is on DOWN iface - let (iface, address) = self.nl_addressmessage_decode(address_msg)?; - log::debug!("NewAddress({iface:?} {address})"); - events.push(NetEvent{iface, op: NetEventOp::AddIp(address)}); - }, - RouteNetlinkMessage::DelAddress(address_msg) => { - let (iface, address) = self.nl_addressmessage_decode(address_msg)?; - log::debug!("DelAddress({iface:?} {address})"); - events.push(NetEvent{iface, op: NetEventOp::RmIp(address)}); - }, - _ => { - return Err(io::Error::new(io::ErrorKind::InvalidData, - format!("unhandled RouteNetlinkMessage: {nl_msg:?}"))); - }, - }; - Ok(events) - } - - fn nl_linkmessage_decode( - &mut self, msg: &LinkMessage - ) -> io::Result<(Rc>, // ref to the (possibly new) impacted interface - Option, // MAC address - )> { - let LinkMessage{header, attributes, ..} = msg; - - // extract fields of interest - let mut iface_name: Option = None; - let mut address_bytes: Option<&Vec> = None; - for nla in attributes { - if let link::LinkAttribute::IfName(name) = nla { - iface_name = Some(name.to_string()); - } - if let link::LinkAttribute::Address(addr) = nla { - address_bytes = Some(addr); - } - } - // make sure message contains an address - let mac_address = address_bytes.map(|address_bytes| address_bytes.iter() - .map(|b| format!("{b:02x}")) - .collect::>().join(":")); - - let iface = self.iface_cache - .entry(header.index) - .or_insert_with_key(|index| - RefCell::new(NetInterface::new(*index, iface_name.clone())) - .into()); - - // handle renaming - if let Some(iface_name) = iface_name { - let iface_renamed = iface.borrow().name != iface_name; - if iface_renamed { - log::trace!("name change: {iface:?} now named '{iface_name}'"); - iface.borrow_mut().name = iface_name; - } - }; - - Ok((iface.clone(), mac_address)) - } - - fn nl_addressmessage_decode(&mut self, msg: &AddressMessage) - -> io::Result<(Rc>, IpAddr)> { - let AddressMessage{header, attributes, ..} = msg; - - // extract fields of interest - let mut address: Option<&IpAddr> = None; - for nla in attributes { - if let address::AddressAttribute::Address(addr) = nla { - address = Some(addr); - break; - } - } - - let iface = match self.iface_cache.entry(header.index) { - hash_map::Entry::Occupied(entry) => { entry.get().clone() }, - hash_map::Entry::Vacant(_) => { - return Err(io::Error::new(io::ErrorKind::InvalidData, - format!("unknown interface for index {}", header.index))); - }, - }; - - match address { - Some(address) => Ok((iface.clone(), *address)), - None => Err(io::Error::new(io::ErrorKind::InvalidData, "unknown address")), - } - } -} - diff --git a/src/datastructs.rs b/src/datastructs.rs deleted file mode 100644 index 67e759f..0000000 --- a/src/datastructs.rs +++ /dev/null @@ -1,75 +0,0 @@ -use std::cell::RefCell; -use std::collections::HashMap; -use std::net::IpAddr; -use std::rc::Rc; - -pub struct KernelInfo { - pub release: String, -} - -#[non_exhaustive] -#[derive(Clone, Debug)] -pub enum ToolstackNetInterface { - None, - Vif(u32), - // SRIOV, - // PciPassthrough, - // UsbPassthrough, -} - -impl ToolstackNetInterface { - pub fn is_none(&self) -> bool { - if let ToolstackNetInterface::None = self { - return true; - } - false - } -} - -#[derive(Clone, Debug)] -pub struct NetInterface { - pub index: u32, - pub name: String, - pub toolstack_iface: ToolstackNetInterface, -} - -impl NetInterface { - pub fn new(index: u32, name: Option) -> NetInterface { - let name = match name { - Some(string) => string, - None => { - log::error!("new interface with index {index} has no name"); - String::from("") // this is not valid, but user will now be aware - }, - }; - NetInterface { index, - name: name.clone(), - toolstack_iface: crate::vif_detect::get_toolstack_interface(&name), - } - } -} - -// The cache of currently-known network interfaces. We have to use -// reference counting on the cached items, as we want on one hand to -// use references to those items from NetEvent, and OTOH we want to -// remove interfaces from here once unplugged. And Rust won't let us -// use `&'static NetInterface` because we can do the latter, which is -// good in the end. -// The interface may change name after creation (hence `RefCell`). -pub type NetInterfaceCache = HashMap>>; - -#[derive(Debug)] -pub enum NetEventOp { - AddIface, - RmIface, - AddMac(String), - RmMac(String), - AddIp(IpAddr), - RmIp(IpAddr), -} - -#[derive(Debug)] -pub struct NetEvent { - pub iface: Rc>, - pub op: NetEventOp, -} diff --git a/src/error.rs b/src/error.rs deleted file mode 100644 index 12ba900..0000000 --- a/src/error.rs +++ /dev/null @@ -1,24 +0,0 @@ -use std::error::Error; -use std::fmt::{Debug, Formatter, Result, Display}; - -pub enum XenError { - NotInGuest, - HypervisorNotXen, -} - -impl Error for XenError {} - -impl Debug for XenError { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { - write!(f, "{}", self) - } -} - -impl Display for XenError { - fn fmt(&self, f: &mut Formatter) -> Result { - match *self { - XenError::NotInGuest => write!(f, "Cannot identify hypervisor"), - XenError::HypervisorNotXen => write!(f, "Hypervisor is not Xen"), - } - } -} diff --git a/src/hypervisor.rs b/src/hypervisor.rs deleted file mode 100644 index 29a8ce6..0000000 --- a/src/hypervisor.rs +++ /dev/null @@ -1,7 +0,0 @@ -use crate::error::XenError; - -pub fn check_is_in_xen_guest() -> Result<(), XenError> { - // NOTE: 'Ok' here implies that we do not know how to check for the hypervisor, - // so we assume the users are aware of their actions. - Ok(()) -} diff --git a/src/hypervisor_linux.rs b/src/hypervisor_linux.rs deleted file mode 100644 index 1222916..0000000 --- a/src/hypervisor_linux.rs +++ /dev/null @@ -1,16 +0,0 @@ -use std::fs; -use crate::error::XenError; - -pub fn check_is_in_xen_guest() -> Result<(), XenError> { - match fs::read_to_string("/sys/hypervisor/type") { - Ok(hypervisor_type) => { - let hypervisor_type = hypervisor_type.trim(); - log::debug!("hypervisor_type {hypervisor_type}"); - if hypervisor_type.eq("xen") { Ok(()) } else { Err(XenError::HypervisorNotXen) } - }, - Err(err) => { - log::error!("could not identify hypervisor type, {err}"); - Err(XenError::NotInGuest) - } - } -} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 3a56fbc..0000000 --- a/src/main.rs +++ /dev/null @@ -1,187 +0,0 @@ -mod datastructs; - -#[cfg_attr(feature = "xenstore", path = "publisher_xenstore.rs")] -mod publisher; -#[cfg(feature = "xenstore")] -mod xenstore_schema_rfc; -#[cfg(feature = "xenstore")] -mod xenstore_schema_std; - -#[cfg_attr(feature = "net_netlink", path = "collector_net_netlink.rs")] -#[cfg_attr(feature = "net_pnet", path = "collector_net_pnet.rs")] -mod collector_net; - -#[cfg_attr(target_os = "linux", path = "collector_memory_linux.rs")] -#[cfg_attr(target_os = "freebsd", path = "collector_memory_bsd.rs")] -mod collector_memory; - -#[cfg_attr(target_os = "linux", path = "vif_detect_linux.rs")] -#[cfg_attr(target_os = "freebsd", path = "vif_detect_freebsd.rs")] -mod vif_detect; - -#[cfg_attr(target_os = "linux", path = "hypervisor_linux.rs")] -mod hypervisor; - -mod error; - -use clap::Parser; - -use crate::collector_memory::MemorySource; -use crate::collector_net::NetworkSource; -use crate::datastructs::KernelInfo; -use crate::hypervisor::check_is_in_xen_guest; -use crate::publisher::Publisher; - -use futures::{pin_mut, select, FutureExt, TryStreamExt}; -use std::error::Error; -use std::io; -use std::str::FromStr; -use std::time::Duration; - -const REPORT_INTERNAL_NICS: bool = false; // FIXME make this a CLI flag -const MEM_PERIOD_SECONDS: u64 = 60; -const DEFAULT_LOGLEVEL: &str = "info"; - - -#[tokio::main] -async fn main() -> Result<(), Box> { - let cli = Cli::parse(); - - setup_logger(cli.stderr, &cli.loglevel)?; - - if let Err(err) = check_is_in_xen_guest() { - log::error!("not starting xen-guest-agent, {err}"); - return Err(err.into()) - } - - let mut publisher = Publisher::new()?; - - let mut collector_memory = MemorySource::new()?; - - // Remove old entries from previous agent to avoid having unknown - // interfaces. We will repopulate existing ones immediatly. - publisher.cleanup_ifaces()?; - - let kernel_info = collect_kernel()?; - let mem_total_kb = match collector_memory.get_total_kb() { - Ok(mem_total_kb) => Some(mem_total_kb), - Err(error) if error.kind() == io::ErrorKind::Unsupported - => { log::warn!("Memory stats not supported"); - None - }, - // propagate errors other than io::ErrorKind::Unsupported - Err(error) => Err(error)?, - }; - publisher.publish_static(&os_info::get(), &kernel_info, mem_total_kb)?; - - // periodic memory stat - let mut timer_stream = tokio::time::interval(Duration::from_secs(MEM_PERIOD_SECONDS)); - - // network events - let network_cache = Box::leak(Box::default()); - let mut collector_net = NetworkSource::new(network_cache)?; - for event in collector_net.collect_current().await? { - if REPORT_INTERNAL_NICS || ! event.iface.borrow().toolstack_iface.is_none() { - publisher.publish_netevent(&event)?; - } - } - let netevent_stream = collector_net.stream(); - pin_mut!(netevent_stream); // needed for iteration - - // main loop - loop { - select! { - event = netevent_stream.try_next().fuse() => { - match event? { - Some(event) => { - if REPORT_INTERNAL_NICS || ! event.iface.borrow().toolstack_iface.is_none() { - publisher.publish_netevent(&event)?; - } else { - log::debug!("no toolstack iface in {event:?}"); - } - }, - // FIXME can't we handle those in `select!` directly? - None => { /* closed? */ }, - }; - }, - _ = timer_stream.tick().fuse() => { - match collector_memory.get_available_kb() { - Ok(mem_avail_kb) => publisher.publish_memfree(mem_avail_kb)?, - Err(ref e) if e.kind() == io::ErrorKind::Unsupported => (), - Err(e) => Err(e)?, - } - }, - complete => break, - } - } - - Ok(()) -} - -#[derive(clap::Parser)] -struct Cli { - /// Print logs to stderr instead of system logs - #[arg(short, long)] - stderr: bool, - - /// Highest level of detail to log - #[arg(short, long, default_value_t = String::from(DEFAULT_LOGLEVEL))] - loglevel: String, -} - -fn setup_logger(use_stderr:bool, loglevel_string: &str) -> Result<(), Box> { - if use_stderr { - setup_env_logger(loglevel_string)?; - } else { - #[cfg(not(unix))] - panic!("no system logger supported"); - - #[cfg(unix)] - setup_system_logger(loglevel_string)?; - } - Ok(()) -} - -// stdout logger for platforms with no specific implementation -fn setup_env_logger(loglevel_string: &str) -> Result<(), Box> { - // set default threshold to "info" not "error" - let env = env_logger::Env::default().default_filter_or(loglevel_string); - env_logger::Builder::from_env(env).init(); - Ok(()) -} - -#[cfg(unix)] -// syslog logger -fn setup_system_logger(loglevel_string: &str) -> Result<(), Box> { - let formatter = syslog::Formatter3164 { - facility: syslog::Facility::LOG_USER, - hostname: None, - process: env!("CARGO_PKG_NAME").into(), - pid: 0, - }; - - let logger = match syslog::unix(formatter) { - Err(e) => { eprintln!("impossible to connect to syslog: {:?}", e); return Ok(()); }, - Ok(logger) => logger, - }; - log::set_boxed_logger(Box::new(syslog::BasicLogger::new(logger)))?; - log::set_max_level(log::LevelFilter::from_str(loglevel_string)?); - Ok(()) -} - -// UNIX uname() implementation -#[cfg(unix)] -fn collect_kernel() -> io::Result> { - let uname_info = uname::uname()?; - let info = KernelInfo { - release: uname_info.release, - }; - - Ok(Some(info)) -} - -// default implementation -#[cfg(not(unix))] -fn collect_kernel() -> io::Result> { - Ok(None) -} diff --git a/src/publisher.rs b/src/publisher.rs deleted file mode 100644 index 2ebc2d9..0000000 --- a/src/publisher.rs +++ /dev/null @@ -1,46 +0,0 @@ -// default no-op Publisher implementation -use crate::datastructs::{KernelInfo, NetEvent, NetEventOp}; -use os_info; -use std::error::Error; -use std::io; - -pub struct Publisher {} - -impl Publisher { - pub fn new() -> Result> { - Ok(Publisher {}) - } - - pub fn publish_static(&self, os_info: &os_info::Info, kernel_info: &Option, - mem_total_kb: Option, - ) -> io::Result<()> { - println!("OS: {} - Version: {}", os_info.os_type(), os_info.version()); - if let Some(mem_total_kb) = mem_total_kb { - println!("Total memory: {mem_total_kb} KB"); - } - if let Some(KernelInfo { release }) = kernel_info { - println!("Kernel version: {}", release); - } - Ok(()) - } - pub fn publish_memfree(&mut self, mem_free_kb: usize) -> io::Result<()> { - println!("Free memory: {mem_free_kb} KB"); - Ok(()) - } - pub fn publish_netevent(&self, event: &NetEvent) -> io::Result<()> { - let iface_id = &event.iface.borrow().name; - match &event.op { - NetEventOp::AddIface => println!("{iface_id} +IFACE"), - NetEventOp::RmIface => println!("{iface_id} -IFACE"), - NetEventOp::AddIp(address) => println!("{iface_id} +IP {address}"), - NetEventOp::RmIp(address) => println!("{iface_id} -IP {address}"), - NetEventOp::AddMac(mac_address) => println!("{iface_id} +MAC {mac_address}"), - NetEventOp::RmMac(mac_address) => println!("{iface_id} -MAC {mac_address}"), - } - Ok(()) - } - - pub fn cleanup_ifaces(&mut self) -> io::Result<()> { - Ok(()) - } -} diff --git a/src/publisher_xenstore.rs b/src/publisher_xenstore.rs deleted file mode 100644 index d4e2ffa..0000000 --- a/src/publisher_xenstore.rs +++ /dev/null @@ -1,63 +0,0 @@ -use crate::datastructs::{KernelInfo, NetEvent}; -use std::env; -use std::error::Error; -use std::io; -use xenstore_rs::{Xs, XsOpenFlags}; - -pub trait XenstoreSchema { - fn publish_static(&mut self, os_info: &os_info::Info, kernel_info: &Option, - mem_total_kb: Option, - ) -> io::Result<()>; - fn publish_memfree(&self, mem_free_kb: usize) -> io::Result<()>; - fn publish_netevent(&mut self, event: &NetEvent) -> io::Result<()>; - fn cleanup_ifaces(&mut self) -> io::Result<()>; -} - -pub struct Publisher { - schema: Box, -} - -impl Publisher { - pub fn new() -> Result> { - let xs = Xs::new(XsOpenFlags::ReadOnly)?; - let schema_name = env::var("XENSTORE_SCHEMA").unwrap_or("std".to_string()); - let schema_ctor = schema_from_name(&schema_name)?; - let schema = schema_ctor(xs); - Ok(Publisher { schema }) - } - - pub fn publish_static(&mut self, os_info: &os_info::Info, kernel_info: &Option, - mem_total_kb: Option, - ) -> io::Result<()> { - self.schema.publish_static(os_info, kernel_info, mem_total_kb) - } - pub fn publish_memfree(&mut self, mem_free_kb: usize) -> io::Result<()> { - self.schema.publish_memfree(mem_free_kb) - } - pub fn publish_netevent(&mut self, event: &NetEvent) -> io::Result<()> { - self.schema.publish_netevent(event) - } - - pub fn cleanup_ifaces(&mut self) -> io::Result<()> { - self.schema.cleanup_ifaces() - } -} - -fn schema_from_name(name: &str) -> io::Result<&'static dyn Fn(Xs) -> Box> { - match name { - "std" => Ok(&crate::xenstore_schema_std::Schema::new), - "rfc" => Ok(&crate::xenstore_schema_rfc::Schema::new), - _ => Err(io::Error::new(io::ErrorKind::InvalidData, - format!("unknown schema '{name}'"))), - } -} - -pub fn xs_publish(xs: &Xs, key: &str, value: &str) -> io::Result<()> { - log::trace!("+ {}={:?}", key, value); - xs.write(None, key, value) -} - -pub fn xs_unpublish(xs: &Xs, key: &str) -> io::Result<()> { - log::trace!("- {}", key); - xs.rm(None, key) -} diff --git a/src/vif_detect.rs b/src/vif_detect.rs deleted file mode 100644 index 1590719..0000000 --- a/src/vif_detect.rs +++ /dev/null @@ -1,5 +0,0 @@ -use crate::datastructs::{NetEvent, ToolstackNetInterface}; - -pub fn get_toolstack_interface(iface_name: &str) -> ToolstackNetInterface { - return ToolstackNetInterface::None; -} diff --git a/src/vif_detect_freebsd.rs b/src/vif_detect_freebsd.rs deleted file mode 100644 index d3eb09e..0000000 --- a/src/vif_detect_freebsd.rs +++ /dev/null @@ -1,18 +0,0 @@ -use crate::datastructs::ToolstackNetInterface; - -// identifies a VIF as named "xn%ID" - -pub fn get_toolstack_interface(iface_name: &str) -> ToolstackNetInterface { - const PREFIX: &str = "xn"; - if !iface_name.starts_with(PREFIX) { - log::debug!("ignoring interface {iface_name} as not starting with '{PREFIX}'"); - return ToolstackNetInterface::None; - } - match iface_name[PREFIX.len()..].parse() { - Ok(index) => { return ToolstackNetInterface::Vif(index); }, - Err(e) => { - log::error!("cannot parse a VIF number adter {PREFIX}: {e}"); - return ToolstackNetInterface::None; - }, - } -} diff --git a/src/vif_detect_linux.rs b/src/vif_detect_linux.rs deleted file mode 100644 index e1b9d71..0000000 --- a/src/vif_detect_linux.rs +++ /dev/null @@ -1,44 +0,0 @@ -use crate::datastructs::ToolstackNetInterface; -use std::fs; - -// identifies a VIF from sysfs as devtype="vif", and take the VIF id -// from nodename="device/vif/$ID" - -// FIXME does not attempt to detect sr-iov VIFs - -pub fn get_toolstack_interface(iface_name: &str) -> ToolstackNetInterface { - // FIXME: using ETHTOOL ioctl could be better - let device_path = format!("/sys/class/net/{}/device", iface_name); - match fs::read_to_string(format!("{device_path}/devtype")) { - Ok(devtype) => { - let devtype = devtype.trim(); - if devtype != "vif" { - log::debug!("ignoring device {device_path}, devtype {devtype:?} not 'vif'"); - return ToolstackNetInterface::None; - } - match fs::read_to_string(format!("{device_path}/nodename")) { - Ok(nodename) => { - let nodename = nodename.trim(); - const PREFIX: &str = "device/vif/"; - if !nodename.starts_with(PREFIX) { - log::debug!("ignoring interface {nodename} as not under {PREFIX}"); - return ToolstackNetInterface::None; - } - let vif_id = nodename[PREFIX.len()..].parse().unwrap(); - - ToolstackNetInterface::Vif(vif_id) - }, - Err(e) => { - log::error!("reading {device_path}/nodename: {e}"); - - ToolstackNetInterface::None - }, - } - }, - Err(e) => { - log::debug!("reading {device_path}/devtype: {e}"); - - ToolstackNetInterface::None - }, - } -} diff --git a/src/xenstore_schema_rfc.rs b/src/xenstore_schema_rfc.rs deleted file mode 100644 index a250949..0000000 --- a/src/xenstore_schema_rfc.rs +++ /dev/null @@ -1,84 +0,0 @@ -use crate::datastructs::{KernelInfo, NetEvent, NetEventOp}; -use crate::publisher::{xs_publish, xs_unpublish, XenstoreSchema}; -use std::io; -use std::net::IpAddr; -use xenstore_rs::Xs; - -pub struct Schema { - xs: Xs, -} - -const PROTOCOL_VERSION: &str = env!("CARGO_PKG_VERSION"); - -// FIXME: this should be a runtime config of xenstore-std.rs - -impl Schema { - pub fn new(xs: Xs) -> Box { - Box::new(Schema { xs }) - } -} - -impl XenstoreSchema for Schema { - fn publish_static(&mut self, os_info: &os_info::Info, kernel_info: &Option, - _mem_total_kb: Option, - ) -> io::Result<()> { - xs_publish(&self.xs, "data/xen-guest-agent", PROTOCOL_VERSION)?; - xs_publish(&self.xs, "data/os/name", - &format!("{} {}", os_info.os_type(), os_info.version()))?; - xs_publish(&self.xs, "data/os/version", &os_info.version().to_string())?; - xs_publish(&self.xs, "data/os/class", "unix")?; - if let Some(kernel_info) = kernel_info { - xs_publish(&self.xs, "data/os/unix/kernel-version", &kernel_info.release)?; - } - - Ok(()) - } - - fn cleanup_ifaces(&mut self) -> io::Result<()> { - // Currently only vif interfaces are cleaned - xs_unpublish(&self.xs, "data/net") - } - - fn publish_memfree(&self, _mem_free_kb: usize) -> io::Result<()> { - //xs_publish(&self.xs, "data/meminfo_free", &mem_free_kb.to_string())?; - Ok(()) - } - - #[allow(clippy::useless_format)] - fn publish_netevent(&mut self, event: &NetEvent) -> io::Result<()> { - let iface_id = &event.iface.borrow().index; - let xs_iface_prefix = format!("data/net/{iface_id}"); - match &event.op { - NetEventOp::AddIface => { - xs_publish(&self.xs, &format!("{xs_iface_prefix}"), &event.iface.borrow().name)?; - }, - NetEventOp::RmIface => { - xs_unpublish(&self.xs, &format!("{xs_iface_prefix}"))?; - }, - NetEventOp::AddIp(address) => { - let key_suffix = munged_address(address); - xs_publish(&self.xs, &format!("{xs_iface_prefix}/{key_suffix}"), "")?; - }, - NetEventOp::RmIp(address) => { - let key_suffix = munged_address(address); - xs_unpublish(&self.xs, &format!("{xs_iface_prefix}/{key_suffix}"))?; - }, - NetEventOp::AddMac(mac_address) => { - xs_publish(&self.xs, &format!("{xs_iface_prefix}"), mac_address)?; - }, - NetEventOp::RmMac(_) => { - xs_unpublish(&self.xs, &format!("{xs_iface_prefix}"))?; - }, - } - Ok(()) - } -} - -fn munged_address(addr: &IpAddr) -> String { - match addr { - IpAddr::V4(addr) => - "ipv4/".to_string() + &addr.to_string().replace('.', "_"), - IpAddr::V6(addr) => - "ipv6/".to_string() + &addr.to_string().replace(':', "_"), - } -} diff --git a/src/xenstore_schema_std.rs b/src/xenstore_schema_std.rs deleted file mode 100644 index 9598d3b..0000000 --- a/src/xenstore_schema_std.rs +++ /dev/null @@ -1,177 +0,0 @@ -use crate::datastructs::{KernelInfo, NetEvent, NetEventOp, NetInterface, ToolstackNetInterface}; -use crate::publisher::{xs_publish, xs_unpublish, XenstoreSchema}; -use std::collections::HashMap; -use std::io; -use std::net::IpAddr; -use xenstore_rs::Xs; - -pub struct Schema { - xs: Xs, - // use of integer indices for IP addresses requires to keep a mapping - ip_addresses: IpList, - - // control/feature-balloon is a control node of XAPI's squeezed, - // and gets created by the guest because xenopsd sets ~/control/ - // with domain ownership. OTOH libxl creates it readonly, so we - // catch the case where it is so to avoid uselessly retrying. - forbidden_control_feature_balloon: bool, -} - -const NUM_IFACE_IPS: usize = 10; -type IfaceIpList = [Option; NUM_IFACE_IPS]; -struct IfaceIpStruct { - v4: IfaceIpList, - v6: IfaceIpList, -} -type IpList = HashMap; - -// pseudo version for xe-daemon compatibility, real agent version in -// BuildVersion below -const AGENT_VERSION_MAJOR: &str = "1"; // XO does not show version at all if 0 -const AGENT_VERSION_MINOR: &str = "0"; -const AGENT_VERSION_MICRO: &str = "0"; // XAPI exposes "-1" if missing - -impl Schema { - pub fn new(xs: Xs) -> Box { - let ip_addresses = IpList::new(); - Box::new(Schema { xs, ip_addresses, - forbidden_control_feature_balloon: false}) - } -} - -impl XenstoreSchema for Schema { - fn publish_static(&mut self, os_info: &os_info::Info, kernel_info: &Option, - mem_total_kb: Option, - ) -> io::Result<()> { - // FIXME this is not anywhere standard, just minimal XS compatibility - xs_publish(&self.xs, "attr/PVAddons/MajorVersion", AGENT_VERSION_MAJOR)?; - xs_publish(&self.xs, "attr/PVAddons/MinorVersion", AGENT_VERSION_MINOR)?; - xs_publish(&self.xs, "attr/PVAddons/MicroVersion", AGENT_VERSION_MICRO)?; - let agent_version_build = format!("proto-{}", &env!("CARGO_PKG_VERSION")); - xs_publish(&self.xs, "attr/PVAddons/BuildVersion", &agent_version_build)?; - - xs_publish(&self.xs, "data/os_distro", &os_info.os_type().to_string())?; - xs_publish(&self.xs, "data/os_name", - &format!("{} {}", os_info.os_type(), os_info.version()))?; - // FIXME .version only has "major" component right now; not a - // big deal for a proto, os_minorver is known to be unreliable - // in xe-guest-utilities at least for Debian - let os_version = os_info.version(); - match os_version { - os_info::Version::Semantic(major, minor, _patch) => { - xs_publish(&self.xs, "data/os_majorver", &major.to_string())?; - xs_publish(&self.xs, "data/os_minorver", &minor.to_string())?; - }, - _ => { - // FIXME what to do with strings? - // the lack of `os_*ver` is anyway not a big deal - log::info!("cannot parse yet os version {:?}", os_version); - } - } - if let Some(kernel_info) = kernel_info { - xs_publish(&self.xs, "data/os_uname", &kernel_info.release)?; - } - - if let Some(mem_total_kb) = mem_total_kb { - xs_publish(&self.xs, "data/meminfo_total", &mem_total_kb.to_string())?; - } - - if !self.forbidden_control_feature_balloon { - // we may want to be more clever some day, e.g. by - // checking if the guest indeed has ballooning, and if the - // balloon driver has reached the requested initial - // `~/memory/target` value (or, possibly, to rely on the - // balloon driver to do the job of signaling this - // condition) - match xs_publish(&self.xs, "control/feature-balloon", "1") { - Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => { - log::warn!("cannot write control/feature-balloon (impacts XAPI's squeezed)"); - self.forbidden_control_feature_balloon = true; - }, - Ok(_) => (), - e => return e, - } - } - - Ok(()) - } - - fn publish_memfree(&self, mem_free_kb: usize) -> io::Result<()> { - xs_publish(&self.xs, "data/meminfo_free", &mem_free_kb.to_string())?; - Ok(()) - } - - // see https://xenbits.xen.org/docs/unstable/misc/xenstore-paths.html#domain-controlled-paths - fn publish_netevent(&mut self, event: &NetEvent) -> io::Result<()> { - let iface_id = match event.iface.borrow().toolstack_iface { - ToolstackNetInterface::Vif(id) => id, - ToolstackNetInterface::None => { - panic!("publish_netevent called with no toolstack iface for {:?}", event); - }, - }; - let xs_iface_prefix = format!("attr/vif/{iface_id}"); - match &event.op { - NetEventOp::AddIface => { - xs_publish(&self.xs, &xs_iface_prefix, "")?; - }, - NetEventOp::RmIface => { - xs_unpublish(&self.xs, &xs_iface_prefix)?; - }, - NetEventOp::AddIp(address) => { - let key_suffix = self.munged_address(address, &event.iface.borrow())?; - xs_publish(&self.xs, &format!("{xs_iface_prefix}/{key_suffix}"), - &address.to_string())?; - }, - NetEventOp::RmIp(address) => { - let key_suffix = self.munged_address(address, &event.iface.borrow())?; - xs_unpublish(&self.xs, &format!("{xs_iface_prefix}/{key_suffix}"))?; - }, - - // FIXME extend IfaceIpStruct for this - NetEventOp::AddMac(_mac_address) => { - log::debug!("AddMac not applied"); - }, - NetEventOp::RmMac(_mac_address) => { - log::debug!("RmMac not applied"); - }, - } - Ok(()) - } - - fn cleanup_ifaces(&mut self) -> io::Result<()> { - // Currently only vif interfaces are cleaned - xs_unpublish(&self.xs, "attr/vif") - } -} - -impl Schema { - fn munged_address(&mut self, addr: &IpAddr, iface: &NetInterface) -> io::Result { - let ip_entry = self.ip_addresses - .entry(iface.index) - .or_insert(IfaceIpStruct{v4: [None; NUM_IFACE_IPS], v6: [None; NUM_IFACE_IPS]}); - let ip_list = match addr { IpAddr::V4(_) => &mut ip_entry.v4, - IpAddr::V6(_) => &mut ip_entry.v6 }; - let ip_slot = get_ip_slot(addr, ip_list)?; - match addr { - IpAddr::V4(_) => Ok(format!("ipv4/{ip_slot}")), - IpAddr::V6(_) => Ok(format!("ipv6/{ip_slot}")), - } - } -} - -fn get_ip_slot(ip: &IpAddr, list: &mut IfaceIpList) -> io::Result { - let mut empty_idx: Option = None; - for (idx, item) in list.iter().enumerate() { - match item { - Some(item) => if item == ip { return Ok(idx) }, // found - None => if empty_idx.is_none() { empty_idx = Some(idx) } - } - } - // not found, insert in empty space if possible - if let Some(idx) = empty_idx { - list[idx] = Some(*ip); - return Ok(idx); - } - Err(io::Error::new(io::ErrorKind::OutOfMemory /*StorageFull?*/, - "no free slot for a new IP address")) -} diff --git a/xen-guest-agent/Cargo.toml b/xen-guest-agent/Cargo.toml new file mode 100644 index 0000000..afa5598 --- /dev/null +++ b/xen-guest-agent/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "xen-guest-agent" +version = "0.5.0-dev" +authors = ["Yann Dirson "] +edition = "2021" +rust-version = "1.76" +license = "AGPL-3.0-only" + +[dependencies] +futures = "0.3.26" +smol = "2.0.2" +log = "0.4.0" +env_logger = { version = ">=0.10.0", default-features = false } +clap = { version = "4.4.8", features = ["derive"] } +enum_dispatch = "0.3" +anyhow = "1.0" +flume = "0.11.1" + +guest-metrics = { path = "../guest-metrics" } + +publisher-console = { path = "../publishers/publisher-console" } +publisher-xenstore = { path = "../publishers/publisher-xenstore" } + +provider-memory = { path = "../providers/provider-memory" } +provider-os = { path = "../providers/provider-os" } +provider-simple = { path = "../providers/provider-simple" } +provider-netlink = { path = "../providers/provider-netlink", optional = true } + +[target.'cfg(target_os = "freebsd")'.dependencies] +sysctl = "0.5.0" + +[target.'cfg(target_family = "unix")'.dependencies] +syslog = "6.0" + +[target."cfg(windows)".dependencies] +windows = { version = "0.58", features = [ + "Win32_Foundation", + "Win32_System_Diagnostics_Debug", +] } +windows-service = "0.8" + +[features] +netlink = ["dep:provider-netlink"] diff --git a/xen-guest-agent/src/main.rs b/xen-guest-agent/src/main.rs new file mode 100644 index 0000000..b3bd54e --- /dev/null +++ b/xen-guest-agent/src/main.rs @@ -0,0 +1,152 @@ +mod plugins; +mod publisher; +#[cfg(windows)] +mod windows_debug_logger; +#[cfg(windows)] +mod windows_service_main; + +use clap::Parser; +use flume::Receiver; +use futures::future::{join_all, select}; +use log::LevelFilter; +use smol::Executor; + +use guest_metrics::{plugin::GuestAgentPlugin, GuestMetric}; +use plugins::{NetworkPlugin, NetworkPluginKind}; +use publisher::{AgentPublisher, PublisherKind}; + +#[cfg(windows)] +use windows_debug_logger::WindowsDebugLogger; + +const MEM_PERIOD_SECONDS: f64 = 5.0; + +#[derive(clap::Parser)] +struct GuestAgentConfig { + /// Print logs to stderr instead of system logs + #[arg(short, long)] + stderr: bool, + + /// Highest level of detail to log + #[arg(short, long, default_value_t = LevelFilter::Info)] + log_level: LevelFilter, + + /// Whether we report NICs. + #[arg(short, long, default_value_t = true)] + report_nics: bool, + + /// Update period. + #[arg(short, long, default_value_t = MEM_PERIOD_SECONDS)] + period: f64, + + #[arg(long, value_enum, default_value_t = Default::default())] + publisher: PublisherKind, + + #[arg(long, value_enum, default_value_t = Default::default())] + network: NetworkPluginKind, + + /// Run as a Windows service. + #[cfg(windows)] + #[arg(long)] + service: bool, +} + +pub(crate) async fn run_async( + config: &GuestAgentConfig, + stop_rx: Receiver<()>, +) -> anyhow::Result<()> { + let (tx, rx) = flume::bounded(4); + let publisher = AgentPublisher::new(config.publisher)?; + let mut tasks = vec![]; + let executor = Executor::new(); + + tasks.push(executor.spawn(publisher.run(rx))); + + if config.report_nics { + // Remove old entries from previous agent to avoid having unknown + // interfaces. We will repopulate existing ones immediatly. + tx.send_async(GuestMetric::CleanupIfaces).await?; + tasks.push(executor.spawn(NetworkPlugin::new(config.network)?.run(tx.clone()))); + } + + tasks.push(executor.spawn(provider_os::OsInfoPlugin.run(tx.clone()))); + tasks.push(executor.spawn(provider_memory::MemoryPlugin.run(tx.clone()))); + + executor + .run(async { + log::info!("Waiting for exit command"); + select(join_all(tasks), stop_rx.recv_async()).await; + log::info!("Got exit command"); + anyhow::Ok(()) + }) + .await?; + + Ok(()) +} + +#[cfg(unix)] +fn main() -> anyhow::Result<()> { + let config = GuestAgentConfig::parse(); + setup_logger(config.stderr, config.log_level)?; + + let (_stop_tx, stop_rx) = flume::bounded(0); + smol::block_on(run_async(&config, stop_rx)) +} + +#[cfg(windows)] +fn main() -> anyhow::Result<()> { + let config = GuestAgentConfig::parse(); + setup_logger(config.stderr, config.log_level)?; + + if config.service { + windows_service_main::dispatch_main() + } else { + let (_stop_tx, stop_rx) = flume::bounded(0); + smol::block_on(run_async(&config, stop_rx)) + } +} + +fn setup_logger(use_stderr: bool, level: LevelFilter) -> anyhow::Result<()> { + if use_stderr { + setup_env_logger(level)?; + } else { + setup_system_logger(level)?; + } + Ok(()) +} + +// stdout logger for platforms with no specific implementation +fn setup_env_logger(level: LevelFilter) -> anyhow::Result<()> { + // set default threshold to "info" not "error" + let env = env_logger::Env::default().default_filter_or(level.as_str()); + env_logger::Builder::from_env(env).init(); + Ok(()) +} + +#[cfg(unix)] +// syslog logger +fn setup_system_logger(level: LevelFilter) -> anyhow::Result<()> { + let formatter = syslog::Formatter3164 { + facility: syslog::Facility::LOG_USER, + hostname: None, + process: env!("CARGO_PKG_NAME").into(), + pid: 0, + }; + + let logger = match syslog::unix(formatter) { + Err(e) => { + eprintln!("impossible to connect to syslog: {:?}", e); + return Ok(()); + } + Ok(logger) => logger, + }; + log::set_boxed_logger(Box::new(syslog::BasicLogger::new(logger)))?; + log::set_max_level(level); + Ok(()) +} + +#[cfg(windows)] +fn setup_system_logger(level: LevelFilter) -> anyhow::Result<()> { + log::set_boxed_logger(Box::new(WindowsDebugLogger {}))?; + log::set_max_level(level); + Ok(()) +} diff --git a/xen-guest-agent/src/plugins.rs b/xen-guest-agent/src/plugins.rs new file mode 100644 index 0000000..e066321 --- /dev/null +++ b/xen-guest-agent/src/plugins.rs @@ -0,0 +1,44 @@ +use std::io; + +use guest_metrics::{plugin::GuestAgentPlugin, GuestMetric}; +use provider_simple::SimpleNetworkPlugin; + +#[cfg(feature = "netlink")] +use provider_netlink::NetlinkPlugin; + +#[derive(Clone, Copy, Debug, clap::ValueEnum)] +pub enum NetworkPluginKind { + Simple, + #[cfg(feature = "netlink")] + Netlink, +} + +impl Default for NetworkPluginKind { + fn default() -> Self { + Self::Simple + } +} + +pub enum NetworkPlugin { + Simple(SimpleNetworkPlugin), + #[cfg(feature = "netlink")] + Netlink(NetlinkPlugin), +} + +impl NetworkPlugin { + pub fn new(kind: NetworkPluginKind) -> io::Result { + match kind { + NetworkPluginKind::Simple => Ok(Self::Simple(SimpleNetworkPlugin::default())), + #[cfg(feature = "netlink")] + NetworkPluginKind::Netlink => Ok(Self::Netlink(NetlinkPlugin)), + } + } + + pub async fn run(self, channel: flume::Sender) { + match self { + NetworkPlugin::Simple(plugin) => plugin.run(channel).await, + #[cfg(feature = "netlink")] + NetworkPlugin::Netlink(plugin) => plugin.run(channel).await, + } + } +} diff --git a/xen-guest-agent/src/publisher.rs b/xen-guest-agent/src/publisher.rs new file mode 100644 index 0000000..f1dd372 --- /dev/null +++ b/xen-guest-agent/src/publisher.rs @@ -0,0 +1,37 @@ +use std::io; + +use guest_metrics::{plugin::GuestAgentPublisher, GuestMetric}; +use publisher_console::ConsolePublisher; +use publisher_xenstore::{XenstoreRfcPublisher, XenstoreStdPublisher}; + +#[derive(Clone, Copy, Default, Debug, clap::ValueEnum)] +pub enum PublisherKind { + Console, + #[default] + Xenstore, + XenstoreRfc, +} + +pub enum AgentPublisher { + Console(ConsolePublisher), + XenstoreRfc(XenstoreRfcPublisher), + XenstoreStd(XenstoreStdPublisher), +} + +impl AgentPublisher { + pub fn new(kind: PublisherKind) -> io::Result { + match kind { + PublisherKind::Console => Ok(Self::Console(ConsolePublisher::default())), + PublisherKind::Xenstore => Ok(Self::XenstoreStd(XenstoreStdPublisher)), + PublisherKind::XenstoreRfc => Ok(Self::XenstoreRfc(XenstoreRfcPublisher)), + } + } + + pub async fn run(self, channel: flume::Receiver) { + match self { + AgentPublisher::Console(publisher) => publisher.run(channel).await, + AgentPublisher::XenstoreRfc(publisher) => publisher.run(channel).await, + AgentPublisher::XenstoreStd(publisher) => publisher.run(channel).await, + } + } +} diff --git a/xen-guest-agent/src/windows_debug_logger.rs b/xen-guest-agent/src/windows_debug_logger.rs new file mode 100644 index 0000000..9613230 --- /dev/null +++ b/xen-guest-agent/src/windows_debug_logger.rs @@ -0,0 +1,23 @@ +use log::Log; +use windows::{core::HSTRING, Win32::System::Diagnostics::Debug::OutputDebugStringW}; + +pub(crate) struct WindowsDebugLogger {} + +impl Log for WindowsDebugLogger { + fn enabled(&self, _metadata: &log::Metadata) -> bool { + true + } + + fn log(&self, record: &log::Record) { + let message = format!( + "[xen-guest-agent] {}: {}\r\n", + record.level().as_str(), + record.args() + ); + unsafe { + OutputDebugStringW(&HSTRING::from(message)); + } + } + + fn flush(&self) {} +} diff --git a/xen-guest-agent/src/windows_service_main.rs b/xen-guest-agent/src/windows_service_main.rs new file mode 100644 index 0000000..1501fb9 --- /dev/null +++ b/xen-guest-agent/src/windows_service_main.rs @@ -0,0 +1,84 @@ +use std::time::Duration; + +use clap::Parser; +use windows::Win32::Foundation::{ERROR_INVALID_PARAMETER, ERROR_SUCCESS}; + +use windows_service::service::{ + ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus, ServiceType, +}; +use windows_service::service_control_handler::{self, ServiceControlHandlerResult}; +use windows_service::service_dispatcher; + +use crate::{run_async, GuestAgentConfig}; + +const SERVICE_NAME: &str = "xenguestagent-rs"; + +fn service_main() -> anyhow::Result<()> { + let (stop_tx, stop_rx) = flume::bounded(0); + + let event_handler = move |control_event| -> ServiceControlHandlerResult { + match control_event { + ServiceControl::Stop => { + log::info!("Sending service stop message"); + stop_tx.send(()).unwrap(); + ServiceControlHandlerResult::NoError + } + ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, + _ => ServiceControlHandlerResult::NotImplemented, + } + }; + + let status_handle = service_control_handler::register(SERVICE_NAME, event_handler)?; + + status_handle.set_service_status(ServiceStatus { + service_type: ServiceType::OWN_PROCESS, + current_state: ServiceState::Running, + controls_accepted: ServiceControlAccept::STOP, + exit_code: ServiceExitCode::Win32(ERROR_SUCCESS.0), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + })?; + + log::info!("Service starting"); + + let service_result: anyhow::Result<()> = smol::block_on(async { + let config = GuestAgentConfig::parse(); + run_async(&config, stop_rx).await?; + Ok(()) + }); + match service_result { + Ok(_) => log::info!("Service returned successfully"), + Err(ref e) => log::error!("Service returned error {e}"), + } + + status_handle.set_service_status(ServiceStatus { + service_type: ServiceType::OWN_PROCESS, + current_state: ServiceState::Stopped, + controls_accepted: ServiceControlAccept::empty(), + exit_code: if service_result.is_ok() { + ServiceExitCode::Win32(ERROR_SUCCESS.0) + } else { + ServiceExitCode::Win32(ERROR_INVALID_PARAMETER.0) + }, + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + })?; + + Ok(()) +} + +extern "system" fn ffi_service_main( + _num_service_arguments: u32, + _service_arguments: *mut *mut u16, +) { + if let Err(e) = service_main() { + log::error!("Service start encountered an error {e}"); + } +} + +pub(crate) fn dispatch_main() -> anyhow::Result<()> { + service_dispatcher::start(SERVICE_NAME, ffi_service_main)?; + Ok(()) +}