Compare commits
15 Commits
67b5c2df07
...
feat/ceph-
| Author | SHA1 | Date | |
|---|---|---|---|
| ac7fd53d5e | |||
| 5895f867cf | |||
| d36c574590 | |||
| bfca9cf163 | |||
| cd3ea6fc10 | |||
| 89eb88d10e | |||
| 72fb05b5cc | |||
| 6685b05cc5 | |||
| 07116eb8a6 | |||
| 3f34f868eb | |||
| bc6f7336d2 | |||
| 01da8631da | |||
| 19cb7f73bc | |||
| d1a274b705 | |||
| b43ca7c740 |
411
Cargo.lock
generated
411
Cargo.lock
generated
@@ -2,6 +2,189 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "actix-codec"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-http"
|
||||
version = "3.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44dfe5c9e0004c623edc65391dfd51daa201e7e30ebd9c9bedf873048ec32bc2"
|
||||
dependencies = [
|
||||
"actix-codec",
|
||||
"actix-rt",
|
||||
"actix-service",
|
||||
"actix-utils",
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.9.1",
|
||||
"brotli",
|
||||
"bytes",
|
||||
"bytestring",
|
||||
"derive_more",
|
||||
"encoding_rs",
|
||||
"flate2",
|
||||
"foldhash",
|
||||
"futures-core",
|
||||
"h2 0.3.26",
|
||||
"http 0.2.12",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"language-tags",
|
||||
"local-channel",
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rand 0.9.1",
|
||||
"sha1",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-macros"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-router"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8"
|
||||
dependencies = [
|
||||
"bytestring",
|
||||
"cfg-if",
|
||||
"http 0.2.12",
|
||||
"regex",
|
||||
"regex-lite",
|
||||
"serde",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-rt"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-server"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502"
|
||||
dependencies = [
|
||||
"actix-rt",
|
||||
"actix-service",
|
||||
"actix-utils",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"mio 1.0.4",
|
||||
"socket2 0.5.10",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-service"
|
||||
version = "2.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-utils"
|
||||
version = "3.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8"
|
||||
dependencies = [
|
||||
"local-waker",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-web"
|
||||
version = "4.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a597b77b5c6d6a1e1097fddde329a83665e25c5437c696a3a9a4aa514a614dea"
|
||||
dependencies = [
|
||||
"actix-codec",
|
||||
"actix-http",
|
||||
"actix-macros",
|
||||
"actix-router",
|
||||
"actix-rt",
|
||||
"actix-server",
|
||||
"actix-service",
|
||||
"actix-utils",
|
||||
"actix-web-codegen",
|
||||
"bytes",
|
||||
"bytestring",
|
||||
"cfg-if",
|
||||
"cookie",
|
||||
"derive_more",
|
||||
"encoding_rs",
|
||||
"foldhash",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"impl-more",
|
||||
"itoa",
|
||||
"language-tags",
|
||||
"log",
|
||||
"mime",
|
||||
"once_cell",
|
||||
"pin-project-lite",
|
||||
"regex",
|
||||
"regex-lite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"smallvec",
|
||||
"socket2 0.5.10",
|
||||
"time",
|
||||
"tracing",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-web-codegen"
|
||||
version = "4.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8"
|
||||
dependencies = [
|
||||
"actix-router",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.24.2"
|
||||
@@ -75,6 +258,21 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alloc-no-stdlib"
|
||||
version = "2.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
|
||||
|
||||
[[package]]
|
||||
name = "alloc-stdlib"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
@@ -398,6 +596,27 @@ dependencies = [
|
||||
"serde_with",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "8.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
"alloc-stdlib",
|
||||
"brotli-decompressor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli-decompressor"
|
||||
version = "5.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
"alloc-stdlib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.12.0"
|
||||
@@ -427,6 +646,15 @@ version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||
|
||||
[[package]]
|
||||
name = "bytestring"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e465647ae23b2823b0753f50decb2d5a86d2bb2cac04788fafd1f80e45378e5f"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "camino"
|
||||
version = "1.1.10"
|
||||
@@ -506,6 +734,8 @@ version = "1.2.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
@@ -710,6 +940,17 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.16.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"time",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
@@ -763,6 +1004,25 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
|
||||
dependencies = [
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
@@ -973,6 +1233,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1907,6 +2168,18 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "harmony_inventory_agent"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"actix-web",
|
||||
"env_logger",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sysinfo",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "harmony_macros"
|
||||
version = "0.1.0"
|
||||
@@ -2346,7 +2619,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2471,6 +2744,12 @@ dependencies = [
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "impl-more"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2"
|
||||
|
||||
[[package]]
|
||||
name = "indenter"
|
||||
version = "0.3.3"
|
||||
@@ -2655,6 +2934,16 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
|
||||
dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.77"
|
||||
@@ -2857,6 +3146,12 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "language-tags"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
@@ -2920,6 +3215,23 @@ version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
|
||||
|
||||
[[package]]
|
||||
name = "local-channel"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"local-waker",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "local-waker"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.13"
|
||||
@@ -3055,6 +3367,15 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ntapi"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.6"
|
||||
@@ -3839,6 +4160,26 @@ dependencies = [
|
||||
"unicode-width 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.13"
|
||||
@@ -3902,6 +4243,12 @@ dependencies = [
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-lite"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.5"
|
||||
@@ -4956,6 +5303,21 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sysinfo"
|
||||
version = "0.30.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a5b4ddaee55fb2bea2bf0e5000747e5f5c0de765e5a5ff87f4cd106439f4bb3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"ntapi",
|
||||
"once_cell",
|
||||
"rayon",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.5.1"
|
||||
@@ -5759,6 +6121,25 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
|
||||
dependencies = [
|
||||
"windows-core 0.52.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.61.2"
|
||||
@@ -6242,3 +6623,31 @@ dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
|
||||
dependencies = [
|
||||
"zstd-safe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-safe"
|
||||
version = "7.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
|
||||
dependencies = [
|
||||
"zstd-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-sys"
|
||||
version = "2.0.15+zstd.1.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
@@ -12,6 +12,7 @@ members = [
|
||||
"harmony_cli",
|
||||
"k3d",
|
||||
"harmony_composer",
|
||||
"harmony_inventory_agent",
|
||||
"harmony_secret_derive",
|
||||
"harmony_secret",
|
||||
]
|
||||
@@ -62,3 +63,5 @@ tar = "0.4.44"
|
||||
lazy_static = "1.5.0"
|
||||
directories = "6.0.0"
|
||||
thiserror = "2.0.14"
|
||||
serde = { version = "1.0.209", features = ["derive", "rc"] }
|
||||
serde_json = "1.0.127"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM docker.io/rust:1.87.0 AS build
|
||||
FROM docker.io/rust:1.89.0 AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -6,7 +6,7 @@ COPY . .
|
||||
|
||||
RUN cargo build --release --bin harmony_composer
|
||||
|
||||
FROM docker.io/rust:1.87.0
|
||||
FROM docker.io/rust:1.89.0
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ reqwest = { version = "0.11", features = ["blocking", "json"] }
|
||||
russh = "0.45.0"
|
||||
rust-ipmi = "0.1.1"
|
||||
semver = "1.0.23"
|
||||
serde = { version = "1.0.209", features = ["derive", "rc"] }
|
||||
serde_json = "1.0.127"
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio.workspace = true
|
||||
derive-new.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
@@ -5,7 +5,7 @@ use k8s_openapi::{
|
||||
};
|
||||
use kube::{
|
||||
Client, Config, Error, Resource,
|
||||
api::{Api, AttachParams, ListParams, Patch, PatchParams, ResourceExt},
|
||||
api::{Api, AttachParams, DeleteParams, ListParams, Patch, PatchParams, ResourceExt},
|
||||
config::{KubeConfigOptions, Kubeconfig},
|
||||
core::ErrorResponse,
|
||||
runtime::reflector::Lookup,
|
||||
@@ -17,7 +17,9 @@ use kube::{
|
||||
};
|
||||
use log::{debug, error, trace};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use serde_json::json;
|
||||
use similar::TextDiff;
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
#[derive(new, Clone)]
|
||||
pub struct K8sClient {
|
||||
@@ -51,6 +53,66 @@ impl K8sClient {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_deployment(
|
||||
&self,
|
||||
name: &str,
|
||||
namespace: Option<&str>,
|
||||
) -> Result<Option<Deployment>, Error> {
|
||||
let deps: Api<Deployment> = if let Some(ns) = namespace {
|
||||
Api::namespaced(self.client.clone(), ns)
|
||||
} else {
|
||||
Api::default_namespaced(self.client.clone())
|
||||
};
|
||||
Ok(deps.get_opt(name).await?)
|
||||
}
|
||||
|
||||
pub async fn get_pod(&self, name: &str, namespace: Option<&str>) -> Result<Option<Pod>, Error> {
|
||||
let pods: Api<Pod> = if let Some(ns) = namespace {
|
||||
Api::namespaced(self.client.clone(), ns)
|
||||
} else {
|
||||
Api::default_namespaced(self.client.clone())
|
||||
};
|
||||
Ok(pods.get_opt(name).await?)
|
||||
}
|
||||
|
||||
pub async fn scale_deployment(
|
||||
&self,
|
||||
name: &str,
|
||||
namespace: Option<&str>,
|
||||
replicas: u32,
|
||||
) -> Result<(), Error> {
|
||||
let deployments: Api<Deployment> = if let Some(ns) = namespace {
|
||||
Api::namespaced(self.client.clone(), ns)
|
||||
} else {
|
||||
Api::default_namespaced(self.client.clone())
|
||||
};
|
||||
|
||||
let patch = json!({
|
||||
"spec": {
|
||||
"replicas": replicas
|
||||
}
|
||||
});
|
||||
let pp = PatchParams::default();
|
||||
let scale = Patch::Apply(&patch);
|
||||
deployments.patch_scale(name, &pp, &scale).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_deployment(
|
||||
&self,
|
||||
name: &str,
|
||||
namespace: Option<&str>,
|
||||
) -> Result<(), Error> {
|
||||
let deployments: Api<Deployment> = if let Some(ns) = namespace {
|
||||
Api::namespaced(self.client.clone(), ns)
|
||||
} else {
|
||||
Api::default_namespaced(self.client.clone())
|
||||
};
|
||||
let delete_params = DeleteParams::default();
|
||||
deployments.delete(name, &delete_params).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn wait_until_deployment_ready(
|
||||
&self,
|
||||
name: String,
|
||||
@@ -76,6 +138,68 @@ impl K8sClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Will execute a commond in the first pod found that matches the specified label
|
||||
/// '{label}={name}'
|
||||
pub async fn exec_app_capture_output(
|
||||
&self,
|
||||
name: String,
|
||||
label: String,
|
||||
namespace: Option<&str>,
|
||||
command: Vec<&str>,
|
||||
) -> Result<String, String> {
|
||||
let api: Api<Pod>;
|
||||
|
||||
if let Some(ns) = namespace {
|
||||
api = Api::namespaced(self.client.clone(), ns);
|
||||
} else {
|
||||
api = Api::default_namespaced(self.client.clone());
|
||||
}
|
||||
let pod_list = api
|
||||
.list(&ListParams::default().labels(format!("{label}={name}").as_str()))
|
||||
.await
|
||||
.expect("couldn't get list of pods");
|
||||
|
||||
let res = api
|
||||
.exec(
|
||||
pod_list
|
||||
.items
|
||||
.first()
|
||||
.expect("couldn't get pod")
|
||||
.name()
|
||||
.expect("couldn't get pod name")
|
||||
.into_owned()
|
||||
.as_str(),
|
||||
command,
|
||||
&AttachParams::default().stdout(true).stderr(true),
|
||||
)
|
||||
.await;
|
||||
match res {
|
||||
Err(e) => Err(e.to_string()),
|
||||
Ok(mut process) => {
|
||||
let status = process
|
||||
.take_status()
|
||||
.expect("Couldn't get status")
|
||||
.await
|
||||
.expect("Couldn't unwrap status");
|
||||
|
||||
if let Some(s) = status.status {
|
||||
let mut stdout_buf = String::new();
|
||||
if let Some(mut stdout) = process.stdout().take() {
|
||||
stdout.read_to_string(&mut stdout_buf).await;
|
||||
}
|
||||
debug!("Status: {} - {:?}", s, status.details);
|
||||
if s == "Success" {
|
||||
Ok(stdout_buf)
|
||||
} else {
|
||||
Err(s)
|
||||
}
|
||||
} else {
|
||||
Err("Couldn't get inner status of pod exec".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Will execute a command in the first pod found that matches the label `app.kubernetes.io/name={name}`
|
||||
pub async fn exec_app(
|
||||
&self,
|
||||
|
||||
@@ -14,5 +14,6 @@ pub mod monitoring;
|
||||
pub mod okd;
|
||||
pub mod opnsense;
|
||||
pub mod prometheus;
|
||||
pub mod storage;
|
||||
pub mod tenant;
|
||||
pub mod tftp;
|
||||
|
||||
419
harmony/src/modules/storage/ceph/ceph_osd_replacement_score.rs
Normal file
419
harmony/src/modules/storage/ceph/ceph_osd_replacement_score.rs
Normal file
@@ -0,0 +1,419 @@
|
||||
use std::{
|
||||
process::Command,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use log::{info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::time::sleep;
|
||||
|
||||
use crate::{
|
||||
data::{Id, Version},
|
||||
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
||||
inventory::Inventory,
|
||||
score::Score,
|
||||
topology::{K8sclient, Topology, k8s::K8sClient},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct CephRemoveOsd {
|
||||
osd_deployment_name: String,
|
||||
rook_ceph_namespace: String,
|
||||
}
|
||||
|
||||
impl<T: Topology + K8sclient> Score<T> for CephRemoveOsd {
|
||||
fn name(&self) -> String {
|
||||
format!("CephRemoveOsdScore")
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||
Box::new(CephRemoveOsdInterpret {
|
||||
score: self.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CephRemoveOsdInterpret {
|
||||
score: CephRemoveOsd,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: Topology + K8sclient> Interpret<T> for CephRemoveOsdInterpret {
|
||||
async fn execute(
|
||||
&self,
|
||||
_inventory: &Inventory,
|
||||
topology: &T,
|
||||
) -> Result<Outcome, InterpretError> {
|
||||
let client = topology.k8s_client().await.unwrap();
|
||||
self.verify_ceph_toolbox_exists(client.clone()).await?;
|
||||
self.scale_deployment(client.clone()).await?;
|
||||
self.verify_deployment_scaled(client.clone()).await?;
|
||||
self.delete_deployment(client.clone()).await?;
|
||||
self.verify_deployment_deleted(client.clone()).await?;
|
||||
let osd_id_full = self.get_ceph_osd_id().unwrap();
|
||||
self.purge_ceph_osd(client.clone(), &osd_id_full).await?;
|
||||
self.verify_ceph_osd_removal(client.clone(), &osd_id_full)
|
||||
.await?;
|
||||
|
||||
Ok(Outcome::success(format!(
|
||||
"Successfully removed OSD {} from rook-ceph cluster by deleting deployment {}",
|
||||
osd_id_full, self.score.osd_deployment_name
|
||||
)))
|
||||
}
|
||||
fn get_name(&self) -> InterpretName {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_version(&self) -> Version {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_status(&self) -> InterpretStatus {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_children(&self) -> Vec<Id> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl CephRemoveOsdInterpret {
|
||||
pub fn get_ceph_osd_id(&self) -> Result<String, InterpretError> {
|
||||
let osd_id_numeric = self
|
||||
.score
|
||||
.osd_deployment_name
|
||||
.split('-')
|
||||
.nth(3)
|
||||
.ok_or_else(|| {
|
||||
InterpretError::new(format!(
|
||||
"Could not parse OSD id from deployment name {}",
|
||||
self.score.osd_deployment_name
|
||||
))
|
||||
})?;
|
||||
let osd_id_full = format!("osd.{}", osd_id_numeric);
|
||||
|
||||
info!(
|
||||
"Targeting Ceph OSD: {} (parsed from deployment {})",
|
||||
osd_id_full, self.score.osd_deployment_name
|
||||
);
|
||||
|
||||
Ok(osd_id_full)
|
||||
}
|
||||
|
||||
pub async fn verify_ceph_toolbox_exists(
|
||||
&self,
|
||||
client: Arc<K8sClient>,
|
||||
) -> Result<Outcome, InterpretError> {
|
||||
let toolbox_dep = "rook-ceph-tools".to_string();
|
||||
|
||||
match client
|
||||
.get_deployment(&toolbox_dep, Some(&self.score.rook_ceph_namespace))
|
||||
.await
|
||||
{
|
||||
Ok(Some(deployment)) => {
|
||||
if let Some(status) = deployment.status {
|
||||
let ready_count = status.ready_replicas.unwrap_or(0);
|
||||
if ready_count >= 1 {
|
||||
return Ok(Outcome::success(format!(
|
||||
"'{}' is ready with {} replica(s).",
|
||||
&toolbox_dep, ready_count
|
||||
)));
|
||||
} else {
|
||||
return Err(InterpretError::new(
|
||||
"ceph-tool-box not ready in cluster".to_string(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
Err(InterpretError::new(format!(
|
||||
"failed to get deployment status {}",
|
||||
&toolbox_dep
|
||||
)))
|
||||
}
|
||||
}
|
||||
Ok(None) => Err(InterpretError::new(format!(
|
||||
"Deployment '{}' not found in namespace '{}'.",
|
||||
&toolbox_dep, self.score.rook_ceph_namespace
|
||||
))),
|
||||
Err(e) => Err(InterpretError::new(format!(
|
||||
"Failed to query for deployment '{}': {}",
|
||||
&toolbox_dep, e
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn scale_deployment(
|
||||
&self,
|
||||
client: Arc<K8sClient>,
|
||||
) -> Result<Outcome, InterpretError> {
|
||||
info!(
|
||||
"Scaling down OSD deployment: {}",
|
||||
self.score.osd_deployment_name
|
||||
);
|
||||
client
|
||||
.scale_deployment(
|
||||
&self.score.osd_deployment_name,
|
||||
Some(&self.score.rook_ceph_namespace),
|
||||
0,
|
||||
)
|
||||
.await?;
|
||||
Ok(Outcome::success(format!(
|
||||
"Scaled down deployment {}",
|
||||
self.score.osd_deployment_name
|
||||
)))
|
||||
}
|
||||
|
||||
pub async fn verify_deployment_scaled(
|
||||
&self,
|
||||
client: Arc<K8sClient>,
|
||||
) -> Result<Outcome, InterpretError> {
|
||||
let (timeout, interval, start) = self.build_timer();
|
||||
|
||||
info!("Waiting for OSD deployment to scale down to 0 replicas");
|
||||
loop {
|
||||
let dep = client
|
||||
.get_deployment(
|
||||
&self.score.osd_deployment_name,
|
||||
Some(&self.score.rook_ceph_namespace),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(deployment) = dep {
|
||||
if let Some(status) = deployment.status {
|
||||
if status.replicas.unwrap_or(1) == 0 && status.ready_replicas.unwrap_or(1) == 0
|
||||
{
|
||||
return Ok(Outcome::success(
|
||||
"Deployment successfully scaled down.".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if start.elapsed() > timeout {
|
||||
return Err(InterpretError::new(format!(
|
||||
"Timed out waiting for deployment {} to scale down",
|
||||
self.score.osd_deployment_name
|
||||
)));
|
||||
}
|
||||
sleep(interval).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn build_timer(&self) -> (Duration, Duration, Instant) {
|
||||
let timeout = Duration::from_secs(120);
|
||||
let interval = Duration::from_secs(5);
|
||||
let start = Instant::now();
|
||||
(timeout, interval, start)
|
||||
}
|
||||
pub async fn delete_deployment(
|
||||
&self,
|
||||
client: Arc<K8sClient>,
|
||||
) -> Result<Outcome, InterpretError> {
|
||||
info!(
|
||||
"Deleting OSD deployment: {}",
|
||||
self.score.osd_deployment_name
|
||||
);
|
||||
client
|
||||
.delete_deployment(
|
||||
&self.score.osd_deployment_name,
|
||||
Some(&self.score.rook_ceph_namespace),
|
||||
)
|
||||
.await?;
|
||||
Ok(Outcome::success(format!(
|
||||
"deployment {} deleted",
|
||||
self.score.osd_deployment_name
|
||||
)))
|
||||
}
|
||||
|
||||
pub async fn verify_deployment_deleted(
|
||||
&self,
|
||||
client: Arc<K8sClient>,
|
||||
) -> Result<Outcome, InterpretError> {
|
||||
let (timeout, interval, start) = self.build_timer();
|
||||
|
||||
info!("Waiting for OSD deployment to scale down to 0 replicas");
|
||||
loop {
|
||||
let dep = client
|
||||
.get_deployment(
|
||||
&self.score.osd_deployment_name,
|
||||
Some(&self.score.rook_ceph_namespace),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if dep.is_none() {
|
||||
info!(
|
||||
"Deployment {} successfully deleted.",
|
||||
self.score.osd_deployment_name
|
||||
);
|
||||
return Ok(Outcome::success(format!(
|
||||
"Deployment {} deleted.",
|
||||
self.score.osd_deployment_name
|
||||
)));
|
||||
}
|
||||
|
||||
if start.elapsed() > timeout {
|
||||
return Err(InterpretError::new(format!(
|
||||
"Timed out waiting for deployment {} to be deleted",
|
||||
self.score.osd_deployment_name
|
||||
)));
|
||||
}
|
||||
sleep(interval).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn get_osd_tree(&self, json: serde_json::Value) -> Result<CephOsdTree, InterpretError> {
|
||||
let nodes = json.get("nodes").ok_or_else(|| {
|
||||
InterpretError::new("Missing 'nodes' field in ceph osd tree JSON".to_string())
|
||||
})?;
|
||||
let tree: CephOsdTree = CephOsdTree {
|
||||
nodes: serde_json::from_value(nodes.clone()).map_err(|e| {
|
||||
InterpretError::new(format!("Failed to parse ceph osd tree JSON: {}", e))
|
||||
})?,
|
||||
};
|
||||
Ok(tree)
|
||||
}
|
||||
|
||||
pub async fn purge_ceph_osd(
|
||||
&self,
|
||||
client: Arc<K8sClient>,
|
||||
osd_id_full: &str,
|
||||
) -> Result<Outcome, InterpretError> {
|
||||
info!(
|
||||
"Purging OSD {} from Ceph cluster and removing its auth key",
|
||||
osd_id_full
|
||||
);
|
||||
client
|
||||
.exec_app_capture_output(
|
||||
"rook-ceph-tools".to_string(),
|
||||
"app".to_string(),
|
||||
Some(&self.score.rook_ceph_namespace),
|
||||
vec![
|
||||
format!("ceph osd purge {osd_id_full} --yes-i-really-mean-it").as_str(),
|
||||
format!("ceph auth del osd.{osd_id_full}").as_str(),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
Ok(Outcome::success(format!(
|
||||
"osd id {} removed from osd tree",
|
||||
osd_id_full
|
||||
)))
|
||||
}
|
||||
|
||||
pub async fn verify_ceph_osd_removal(
|
||||
&self,
|
||||
client: Arc<K8sClient>,
|
||||
osd_id_full: &str,
|
||||
) -> Result<Outcome, InterpretError> {
|
||||
let (timeout, interval, start) = self.build_timer();
|
||||
info!(
|
||||
"Verifying OSD {} has been removed from the Ceph tree...",
|
||||
osd_id_full
|
||||
);
|
||||
loop {
|
||||
let output = client
|
||||
.exec_app_capture_output(
|
||||
"rook-ceph-tools".to_string(),
|
||||
"app".to_string(),
|
||||
Some(&self.score.rook_ceph_namespace),
|
||||
vec!["ceph osd tree -f json"],
|
||||
)
|
||||
.await?;
|
||||
let tree =
|
||||
self.get_osd_tree(serde_json::from_str(&output).expect("could not extract json"));
|
||||
|
||||
let osd_found = tree
|
||||
.unwrap()
|
||||
.nodes
|
||||
.iter()
|
||||
.any(|node| node.name == osd_id_full);
|
||||
|
||||
if !osd_found {
|
||||
return Ok(Outcome::success(format!(
|
||||
"Successfully verified that OSD {} is removed from the Ceph cluster.",
|
||||
osd_id_full,
|
||||
)));
|
||||
}
|
||||
|
||||
if start.elapsed() > timeout {
|
||||
return Err(InterpretError::new(format!(
|
||||
"Timed out waiting for OSD {} to be removed from Ceph tree",
|
||||
osd_id_full
|
||||
)));
|
||||
}
|
||||
|
||||
warn!(
|
||||
"OSD {} still found in Ceph tree, retrying in {:?}...",
|
||||
osd_id_full, interval
|
||||
);
|
||||
sleep(interval).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
pub struct CephOsdTree {
|
||||
pub nodes: Vec<CephNode>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
pub struct CephNode {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
#[serde(rename = "type")]
|
||||
pub node_type: String,
|
||||
pub type_id: Option<i32>,
|
||||
pub children: Option<Vec<i32>>,
|
||||
pub exists: Option<i32>,
|
||||
pub status: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_get_osd_tree() {
|
||||
let json_data = json!({
|
||||
"nodes": [
|
||||
{"id": 1, "name": "osd.1", "type": "osd", "primary_affinity":"1"},
|
||||
{"id": 2, "name": "osd.2", "type": "osd", "crush_weight": 1.22344}
|
||||
]
|
||||
});
|
||||
let interpret = CephRemoveOsdInterpret {
|
||||
score: CephRemoveOsd {
|
||||
osd_deployment_name: "osd-1".to_string(),
|
||||
rook_ceph_namespace: "dummy_ns".to_string(),
|
||||
},
|
||||
};
|
||||
let json = interpret.get_osd_tree(json_data).unwrap();
|
||||
|
||||
let expected = CephOsdTree {
|
||||
nodes: vec![
|
||||
CephNode {
|
||||
id: 1,
|
||||
name: "osd.1".to_string(),
|
||||
node_type: "osd".to_string(),
|
||||
type_id: None,
|
||||
children: None,
|
||||
exists: None,
|
||||
status: None,
|
||||
},
|
||||
CephNode {
|
||||
id: 2,
|
||||
name: "osd.2".to_string(),
|
||||
node_type: "osd".to_string(),
|
||||
type_id: None,
|
||||
children: None,
|
||||
exists: None,
|
||||
status: None,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
assert_eq!(json, expected);
|
||||
}
|
||||
}
|
||||
4
harmony/src/modules/storage/ceph/mod.rs
Normal file
4
harmony/src/modules/storage/ceph/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod ceph_remove_osd_score;
|
||||
pub mod rook_ceph_helm_chart_score;
|
||||
pub mod rook_ceph_cluster_helm_chart_score;
|
||||
pub mod rook_ceph_install_score;
|
||||
@@ -0,0 +1,44 @@
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use non_blank_string_rs::NonBlankString;
|
||||
|
||||
use crate::modules::helm::chart::HelmChartScore;
|
||||
|
||||
pub fn rook_ceph_cluster_helm_chart(ns: &str) -> HelmChartScore {
|
||||
let values = r#"
|
||||
monitoring:
|
||||
enabled: true
|
||||
createPrometheusRules: true
|
||||
cephClusterSpec:
|
||||
placement:
|
||||
all:
|
||||
nodeAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: storage-node
|
||||
operator: In
|
||||
values:
|
||||
- "true"
|
||||
dashboard:
|
||||
ssl: false
|
||||
prometheusEndpoint: http://prometheus-operated:9090
|
||||
prometheusEndpointSSLVerify: false
|
||||
toolbox:
|
||||
enabled: true
|
||||
|
||||
"#
|
||||
.to_string();
|
||||
HelmChartScore {
|
||||
namespace: Some(NonBlankString::from_str(ns).unwrap()),
|
||||
release_name: NonBlankString::from_str("rook-ceph").unwrap(),
|
||||
chart_name: NonBlankString::from_str("https://charts.rook.io/release/rook-release/rook-ceph-cluster").unwrap(),
|
||||
chart_version: todo!(),
|
||||
values_overrides: todo!(),
|
||||
values_yaml: Some(values.to_string()),
|
||||
create_namespace: todo!(),
|
||||
install_only: todo!(),
|
||||
repository: todo!(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use non_blank_string_rs::NonBlankString;
|
||||
|
||||
use crate::modules::helm::chart::HelmChartScore;
|
||||
|
||||
pub fn rook_ceph_helm_chart(ns: &str) -> HelmChartScore {
|
||||
let values = r#"
|
||||
monitoring:
|
||||
enabled: true
|
||||
"#
|
||||
.to_string();
|
||||
HelmChartScore {
|
||||
namespace: Some(NonBlankString::from_str(ns).unwrap()),
|
||||
release_name: NonBlankString::from_str("rook-ceph").unwrap(),
|
||||
chart_name: NonBlankString::from_str("https://charts.rook.io/release/rook-release/rook-ceph").unwrap(),
|
||||
chart_version: todo!(),
|
||||
values_overrides: todo!(),
|
||||
values_yaml: Some(values.to_string()),
|
||||
create_namespace: todo!(),
|
||||
install_only: todo!(),
|
||||
repository: todo!(),
|
||||
}
|
||||
}
|
||||
81
harmony/src/modules/storage/ceph/rook_ceph_install_score.rs
Normal file
81
harmony/src/modules/storage/ceph/rook_ceph_install_score.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
data::{Id, Version},
|
||||
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
||||
inventory::Inventory,
|
||||
score::Score,
|
||||
topology::{HelmCommand, Topology},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct RookCephInstall {
|
||||
namespace: String,
|
||||
}
|
||||
|
||||
impl<T: Topology + HelmCommand> Score<T> for RookCephInstall {
|
||||
fn name(&self) -> String {
|
||||
"RookCephInstall".to_string()
|
||||
}
|
||||
|
||||
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||
Box::new(RookCephInstallInterpret {
|
||||
score: self.score.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RookCephInstallInterpret {
|
||||
score: RookCephInstall,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: Topology + HelmCommand> Interpret<T> for RookCephInstallInterpret {
|
||||
async fn execute(
|
||||
&self,
|
||||
inventory: &Inventory,
|
||||
topology: &T,
|
||||
) -> Result<InterpretError, Outcome> {
|
||||
self.label_nodes();
|
||||
self.install_rook_helm_chart(self.score.namespace);
|
||||
self.install_rook_cluster_helm_chart(self.score.namespace);
|
||||
//TODO I think we will need to add a capability OCClient to interact with the okd
|
||||
//cli tool
|
||||
self.add_oc_adm_policy(self.score.namespace);
|
||||
}
|
||||
|
||||
fn get_name(&self) -> InterpretName {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_version(&self) -> Version {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_status(&self) -> InterpretStatus {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_children(&self) -> Vec<Id> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl RookCephInstallInterpret {
|
||||
fn label_nodes(&self) -> _ {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn install_rook_helm_chart(&self, namespace: String) -> _ {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn install_rook_cluster_helm_chart(&self, namespace: String) -> _ {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn add_oc_adm_policy(&self, namespace: String) -> _ {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
1
harmony/src/modules/storage/mod.rs
Normal file
1
harmony/src/modules/storage/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod ceph;
|
||||
12
harmony_inventory_agent/Cargo.toml
Normal file
12
harmony_inventory_agent/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "harmony_inventory_agent"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
actix-web = "4.4"
|
||||
sysinfo = "0.30"
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
log.workspace = true
|
||||
env_logger.workspace = true
|
||||
825
harmony_inventory_agent/src/hwinfo.rs
Normal file
825
harmony_inventory_agent/src/hwinfo.rs
Normal file
@@ -0,0 +1,825 @@
|
||||
use log::debug;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use sysinfo::System;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct PhysicalHost {
|
||||
pub storage_drives: Vec<StorageDrive>,
|
||||
pub storage_controller: StorageController,
|
||||
pub memory_modules: Vec<MemoryModule>,
|
||||
pub cpus: Vec<CPU>,
|
||||
pub chipset: Chipset,
|
||||
pub network_interfaces: Vec<NetworkInterface>,
|
||||
pub management_interface: Option<ManagementInterface>,
|
||||
pub host_uuid: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct StorageDrive {
|
||||
pub name: String,
|
||||
pub model: String,
|
||||
pub serial: String,
|
||||
pub size_bytes: u64,
|
||||
pub logical_block_size: u32,
|
||||
pub physical_block_size: u32,
|
||||
pub rotational: bool,
|
||||
pub wwn: Option<String>,
|
||||
pub interface_type: String,
|
||||
pub smart_status: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct StorageController {
|
||||
pub name: String,
|
||||
pub driver: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct MemoryModule {
|
||||
pub size_bytes: u64,
|
||||
pub speed_mhz: Option<u32>,
|
||||
pub manufacturer: Option<String>,
|
||||
pub part_number: Option<String>,
|
||||
pub serial_number: Option<String>,
|
||||
pub rank: Option<u8>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct CPU {
|
||||
pub model: String,
|
||||
pub vendor: String,
|
||||
pub cores: u32,
|
||||
pub threads: u32,
|
||||
pub frequency_mhz: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Chipset {
|
||||
pub name: String,
|
||||
pub vendor: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct NetworkInterface {
|
||||
pub name: String,
|
||||
pub mac_address: String,
|
||||
pub speed_mbps: Option<u32>,
|
||||
pub is_up: bool,
|
||||
pub mtu: u32,
|
||||
pub ipv4_addresses: Vec<String>,
|
||||
pub ipv6_addresses: Vec<String>,
|
||||
pub driver: String,
|
||||
pub firmware_version: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct ManagementInterface {
|
||||
pub kind: String,
|
||||
pub address: Option<String>,
|
||||
pub firmware: Option<String>,
|
||||
}
|
||||
|
||||
impl PhysicalHost {
|
||||
pub fn gather() -> Result<Self, String> {
|
||||
let mut sys = System::new_all();
|
||||
sys.refresh_all();
|
||||
|
||||
Self::all_tools_available()?;
|
||||
|
||||
Ok(Self {
|
||||
storage_drives: Self::gather_storage_drives()?,
|
||||
storage_controller: Self::gather_storage_controller()?,
|
||||
memory_modules: Self::gather_memory_modules()?,
|
||||
cpus: Self::gather_cpus(&sys)?,
|
||||
chipset: Self::gather_chipset()?,
|
||||
network_interfaces: Self::gather_network_interfaces()?,
|
||||
management_interface: Self::gather_management_interface()?,
|
||||
host_uuid: Self::get_host_uuid()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn all_tools_available() -> Result<(), String> {
|
||||
let required_tools = [
|
||||
("lsblk", "--version"),
|
||||
("lspci", "--version"),
|
||||
("lsmod", "--version"),
|
||||
("dmidecode", "--version"),
|
||||
("smartctl", "--version"),
|
||||
("ip", "route"), // No version flag available
|
||||
];
|
||||
|
||||
let mut missing_tools = Vec::new();
|
||||
|
||||
for (tool, tool_arg) in required_tools.iter() {
|
||||
// First check if tool exists in PATH using which(1)
|
||||
let exists = if let Ok(output) = Command::new("which").arg(tool).output() {
|
||||
output.status.success()
|
||||
} else {
|
||||
// Fallback: manual PATH search if which(1) is unavailable
|
||||
if let Ok(path_var) = std::env::var("PATH") {
|
||||
path_var.split(':').any(|dir| {
|
||||
let tool_path = std::path::Path::new(dir).join(tool);
|
||||
tool_path.exists() && Self::is_executable(&tool_path)
|
||||
})
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if !exists {
|
||||
missing_tools.push(*tool);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verify tool is functional by checking version/help output
|
||||
let mut cmd = Command::new(tool);
|
||||
cmd.arg(tool_arg);
|
||||
cmd.stdout(std::process::Stdio::null());
|
||||
cmd.stderr(std::process::Stdio::null());
|
||||
|
||||
if let Ok(status) = cmd.status() {
|
||||
if !status.success() {
|
||||
missing_tools.push(*tool);
|
||||
}
|
||||
} else {
|
||||
missing_tools.push(*tool);
|
||||
}
|
||||
}
|
||||
|
||||
if !missing_tools.is_empty() {
|
||||
let missing_str = missing_tools
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ");
|
||||
return Err(format!(
|
||||
"The following required tools are not available: {}. Please install these tools to use PhysicalHost::gather()",
|
||||
missing_str
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn is_executable(path: &std::path::Path) -> bool {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
match std::fs::metadata(path) {
|
||||
Ok(meta) => meta.permissions().mode() & 0o111 != 0,
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn is_executable(_path: &std::path::Path) -> bool {
|
||||
// On non-Unix systems, we assume existence implies executability
|
||||
true
|
||||
}
|
||||
|
||||
fn gather_storage_drives() -> Result<Vec<StorageDrive>, String> {
|
||||
let mut drives = Vec::new();
|
||||
|
||||
// Use lsblk with JSON output for robust parsing
|
||||
let output = Command::new("lsblk")
|
||||
.args([
|
||||
"-d",
|
||||
"-o",
|
||||
"NAME,MODEL,SERIAL,SIZE,ROTA,WWN",
|
||||
"-n",
|
||||
"-e",
|
||||
"7",
|
||||
"--json",
|
||||
])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to execute lsblk: {}", e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(format!(
|
||||
"lsblk command failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
}
|
||||
|
||||
let json: Value = serde_json::from_slice(&output.stdout)
|
||||
.map_err(|e| format!("Failed to parse lsblk JSON output: {}", e))?;
|
||||
|
||||
let blockdevices = json
|
||||
.get("blockdevices")
|
||||
.and_then(|v| v.as_array())
|
||||
.ok_or("Invalid lsblk JSON: missing 'blockdevices' array")?;
|
||||
|
||||
for device in blockdevices {
|
||||
let name = device
|
||||
.get("name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("Missing 'name' in lsblk device")?
|
||||
.to_string();
|
||||
|
||||
if name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let model = device
|
||||
.get("model")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.trim().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
let serial = device
|
||||
.get("serial")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.trim().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
let size_str = device
|
||||
.get("size")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("Missing 'size' in lsblk device")?;
|
||||
let size_bytes = Self::parse_size(size_str)?;
|
||||
|
||||
let rotational = device
|
||||
.get("rota")
|
||||
.and_then(|v| v.as_bool())
|
||||
.ok_or("Missing 'rota' in lsblk device")?;
|
||||
|
||||
let wwn = device
|
||||
.get("wwn")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty() && s != "null");
|
||||
|
||||
let device_path = Path::new("/sys/block").join(&name);
|
||||
|
||||
let logical_block_size = Self::read_sysfs_u32(
|
||||
&device_path.join("queue/logical_block_size"),
|
||||
)
|
||||
.map_err(|e| format!("Failed to read logical block size for {}: {}", name, e))?;
|
||||
|
||||
let physical_block_size = Self::read_sysfs_u32(
|
||||
&device_path.join("queue/physical_block_size"),
|
||||
)
|
||||
.map_err(|e| format!("Failed to read physical block size for {}: {}", name, e))?;
|
||||
|
||||
let interface_type = Self::get_interface_type(&name, &device_path)?;
|
||||
let smart_status = Self::get_smart_status(&name)?;
|
||||
|
||||
let mut drive = StorageDrive {
|
||||
name: name.clone(),
|
||||
model,
|
||||
serial,
|
||||
size_bytes,
|
||||
logical_block_size,
|
||||
physical_block_size,
|
||||
rotational,
|
||||
wwn,
|
||||
interface_type,
|
||||
smart_status,
|
||||
};
|
||||
|
||||
// Enhance with additional sysfs info if available
|
||||
if device_path.exists() {
|
||||
if drive.model.is_empty() {
|
||||
drive.model = Self::read_sysfs_string(&device_path.join("device/model"))
|
||||
.map_err(|e| format!("Failed to read model for {}: {}", name, e))?;
|
||||
}
|
||||
if drive.serial.is_empty() {
|
||||
drive.serial = Self::read_sysfs_string(&device_path.join("device/serial"))
|
||||
.map_err(|e| format!("Failed to read serial for {}: {}", name, e))?;
|
||||
}
|
||||
}
|
||||
|
||||
drives.push(drive);
|
||||
}
|
||||
|
||||
Ok(drives)
|
||||
}
|
||||
|
||||
fn gather_storage_controller() -> Result<StorageController, String> {
|
||||
let mut controller = StorageController {
|
||||
name: "Unknown".to_string(),
|
||||
driver: "Unknown".to_string(),
|
||||
};
|
||||
|
||||
// Use lspci with JSON output if available
|
||||
let output = Command::new("lspci")
|
||||
.args(["-nn", "-d", "::0100", "-J"]) // Storage controllers class with JSON
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to execute lspci: {}", e))?;
|
||||
|
||||
if output.status.success() {
|
||||
let json: Value = serde_json::from_slice(&output.stdout)
|
||||
.map_err(|e| format!("Failed to parse lspci JSON output: {}", e))?;
|
||||
|
||||
if let Some(devices) = json.as_array() {
|
||||
for device in devices {
|
||||
if let Some(device_info) = device.as_object()
|
||||
&& let Some(name) = device_info
|
||||
.get("device")
|
||||
.and_then(|v| v.as_object())
|
||||
.and_then(|v| v.get("name"))
|
||||
.and_then(|v| v.as_str())
|
||||
{
|
||||
controller.name = name.to_string();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to text output if JSON fails or no device found
|
||||
if controller.name == "Unknown" {
|
||||
let output = Command::new("lspci")
|
||||
.args(["-nn", "-d", "::0100"]) // Storage controllers class
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to execute lspci (fallback): {}", e))?;
|
||||
|
||||
if output.status.success() {
|
||||
let output_str = String::from_utf8_lossy(&output.stdout);
|
||||
if let Some(line) = output_str.lines().next() {
|
||||
let parts: Vec<&str> = line.split(':').collect();
|
||||
if parts.len() > 2 {
|
||||
controller.name = parts[2].trim().to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get driver info from lsmod
|
||||
let output = Command::new("lsmod")
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to execute lsmod: {}", e))?;
|
||||
|
||||
if output.status.success() {
|
||||
let output_str = String::from_utf8_lossy(&output.stdout);
|
||||
for line in output_str.lines() {
|
||||
if line.contains("ahci")
|
||||
|| line.contains("nvme")
|
||||
|| line.contains("megaraid")
|
||||
|| line.contains("mpt3sas")
|
||||
{
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if !parts.is_empty() {
|
||||
controller.driver = parts[0].to_string();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(controller)
|
||||
}
|
||||
|
||||
fn gather_memory_modules() -> Result<Vec<MemoryModule>, String> {
|
||||
let mut modules = Vec::new();
|
||||
|
||||
let output = Command::new("dmidecode")
|
||||
.arg("--type")
|
||||
.arg("17")
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to execute dmidecode: {}", e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(format!(
|
||||
"dmidecode command failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
}
|
||||
|
||||
let output_str = String::from_utf8(output.stdout)
|
||||
.map_err(|e| format!("Failed to parse dmidecode output: {}", e))?;
|
||||
|
||||
let sections: Vec<&str> = output_str.split("Memory Device").collect();
|
||||
|
||||
for section in sections.into_iter().skip(1) {
|
||||
let mut module = MemoryModule {
|
||||
size_bytes: 0,
|
||||
speed_mhz: None,
|
||||
manufacturer: None,
|
||||
part_number: None,
|
||||
serial_number: None,
|
||||
rank: None,
|
||||
};
|
||||
|
||||
for line in section.lines() {
|
||||
let line = line.trim();
|
||||
if let Some(size_str) = line.strip_prefix("Size: ") {
|
||||
if size_str != "No Module Installed"
|
||||
&& let Some((num, unit)) = size_str.split_once(' ')
|
||||
&& let Ok(num) = num.parse::<u64>()
|
||||
{
|
||||
module.size_bytes = match unit {
|
||||
"MB" => num * 1024 * 1024,
|
||||
"GB" => num * 1024 * 1024 * 1024,
|
||||
"KB" => num * 1024,
|
||||
_ => 0,
|
||||
};
|
||||
}
|
||||
} else if let Some(speed_str) = line.strip_prefix("Speed: ") {
|
||||
if let Some((num, _unit)) = speed_str.split_once(' ') {
|
||||
module.speed_mhz = num.parse().ok();
|
||||
}
|
||||
} else if let Some(man) = line.strip_prefix("Manufacturer: ") {
|
||||
module.manufacturer = Some(man.to_string());
|
||||
} else if let Some(part) = line.strip_prefix("Part Number: ") {
|
||||
module.part_number = Some(part.to_string());
|
||||
} else if let Some(serial) = line.strip_prefix("Serial Number: ") {
|
||||
module.serial_number = Some(serial.to_string());
|
||||
} else if let Some(rank) = line.strip_prefix("Rank: ") {
|
||||
module.rank = rank.parse().ok();
|
||||
}
|
||||
}
|
||||
|
||||
if module.size_bytes > 0 {
|
||||
modules.push(module);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(modules)
|
||||
}
|
||||
|
||||
fn gather_cpus(sys: &System) -> Result<Vec<CPU>, String> {
|
||||
let mut cpus = Vec::new();
|
||||
let global_cpu = sys.global_cpu_info();
|
||||
|
||||
cpus.push(CPU {
|
||||
model: global_cpu.brand().to_string(),
|
||||
vendor: global_cpu.vendor_id().to_string(),
|
||||
cores: sys.physical_core_count().unwrap_or(1) as u32,
|
||||
threads: sys.cpus().len() as u32,
|
||||
frequency_mhz: global_cpu.frequency(),
|
||||
});
|
||||
|
||||
Ok(cpus)
|
||||
}
|
||||
|
||||
fn gather_chipset() -> Result<Chipset, String> {
|
||||
Ok(Chipset {
|
||||
name: Self::read_dmi("baseboard-product-name")?,
|
||||
vendor: Self::read_dmi("baseboard-manufacturer")?,
|
||||
})
|
||||
}
|
||||
|
||||
fn gather_network_interfaces() -> Result<Vec<NetworkInterface>, String> {
|
||||
let mut interfaces = Vec::new();
|
||||
let sys_net_path = Path::new("/sys/class/net");
|
||||
|
||||
let entries = fs::read_dir(sys_net_path)
|
||||
.map_err(|e| format!("Failed to read /sys/class/net: {}", e))?;
|
||||
|
||||
for entry in entries {
|
||||
let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?;
|
||||
let iface_name = entry
|
||||
.file_name()
|
||||
.into_string()
|
||||
.map_err(|_| "Invalid UTF-8 in interface name")?;
|
||||
let iface_path = entry.path();
|
||||
|
||||
// Skip virtual interfaces
|
||||
if iface_name.starts_with("lo")
|
||||
|| iface_name.starts_with("docker")
|
||||
|| iface_name.starts_with("virbr")
|
||||
|| iface_name.starts_with("veth")
|
||||
|| iface_name.starts_with("br-")
|
||||
|| iface_name.starts_with("tun")
|
||||
|| iface_name.starts_with("wg")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if it's a physical interface by looking for device directory
|
||||
if !iface_path.join("device").exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mac_address = Self::read_sysfs_string(&iface_path.join("address"))
|
||||
.map_err(|e| format!("Failed to read MAC address for {}: {}", iface_name, e))?;
|
||||
|
||||
let speed_mbps = if iface_path.join("speed").exists() {
|
||||
match Self::read_sysfs_u32(&iface_path.join("speed")) {
|
||||
Ok(speed) => Some(speed),
|
||||
Err(e) => {
|
||||
debug!(
|
||||
"Failed to read speed for {}: {} . This is expected to fail on wifi interfaces.",
|
||||
iface_name, e
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let operstate = Self::read_sysfs_string(&iface_path.join("operstate"))
|
||||
.map_err(|e| format!("Failed to read operstate for {}: {}", iface_name, e))?;
|
||||
|
||||
let mtu = Self::read_sysfs_u32(&iface_path.join("mtu"))
|
||||
.map_err(|e| format!("Failed to read MTU for {}: {}", iface_name, e))?;
|
||||
|
||||
let driver =
|
||||
Self::read_sysfs_symlink_basename(&iface_path.join("device/driver/module"))
|
||||
.map_err(|e| format!("Failed to read driver for {}: {}", iface_name, e))?;
|
||||
|
||||
let firmware_version = Self::read_sysfs_opt_string(
|
||||
&iface_path.join("device/firmware_version"),
|
||||
)
|
||||
.map_err(|e| format!("Failed to read firmware version for {}: {}", iface_name, e))?;
|
||||
|
||||
// Get IP addresses using ip command with JSON output
|
||||
let (ipv4_addresses, ipv6_addresses) = Self::get_interface_ips_json(&iface_name)
|
||||
.map_err(|e| format!("Failed to get IP addresses for {}: {}", iface_name, e))?;
|
||||
|
||||
interfaces.push(NetworkInterface {
|
||||
name: iface_name,
|
||||
mac_address,
|
||||
speed_mbps,
|
||||
is_up: operstate == "up",
|
||||
mtu,
|
||||
ipv4_addresses,
|
||||
ipv6_addresses,
|
||||
driver,
|
||||
firmware_version,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(interfaces)
|
||||
}
|
||||
|
||||
fn gather_management_interface() -> Result<Option<ManagementInterface>, String> {
|
||||
if Path::new("/dev/ipmi0").exists() {
|
||||
Ok(Some(ManagementInterface {
|
||||
kind: "IPMI".to_string(),
|
||||
address: None,
|
||||
firmware: Some(Self::read_dmi("bios-version")?),
|
||||
}))
|
||||
} else if Path::new("/sys/class/misc/mei").exists() {
|
||||
Ok(Some(ManagementInterface {
|
||||
kind: "Intel ME".to_string(),
|
||||
address: None,
|
||||
firmware: None,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_host_uuid() -> Result<String, String> {
|
||||
Self::read_dmi("system-uuid")
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
fn read_sysfs_string(path: &Path) -> Result<String, String> {
|
||||
fs::read_to_string(path)
|
||||
.map(|s| s.trim().to_string())
|
||||
.map_err(|e| format!("Failed to read {}: {}", path.display(), e))
|
||||
}
|
||||
|
||||
fn read_sysfs_opt_string(path: &Path) -> Result<Option<String>, String> {
|
||||
match fs::read_to_string(path) {
|
||||
Ok(s) => {
|
||||
let s = s.trim().to_string();
|
||||
Ok(if s.is_empty() { None } else { Some(s) })
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
||||
Err(e) => Err(format!("Failed to read {}: {}", path.display(), e)),
|
||||
}
|
||||
}
|
||||
|
||||
fn read_sysfs_u32(path: &Path) -> Result<u32, String> {
|
||||
fs::read_to_string(path)
|
||||
.map_err(|e| format!("Failed to read {}: {}", path.display(), e))?
|
||||
.trim()
|
||||
.parse()
|
||||
.map_err(|e| format!("Failed to parse {}: {}", path.display(), e))
|
||||
}
|
||||
|
||||
fn read_sysfs_symlink_basename(path: &Path) -> Result<String, String> {
|
||||
match fs::read_link(path) {
|
||||
Ok(target_path) => match target_path.file_name() {
|
||||
Some(name_osstr) => match name_osstr.to_str() {
|
||||
Some(name_str) => Ok(name_str.to_string()),
|
||||
None => Err(format!(
|
||||
"Symlink target basename is not valid UTF-8: {}",
|
||||
target_path.display()
|
||||
)),
|
||||
},
|
||||
None => Err(format!(
|
||||
"Symlink target has no basename: {} -> {}",
|
||||
path.display(),
|
||||
target_path.display()
|
||||
)),
|
||||
},
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(format!(
|
||||
"Could not resolve symlink for path : {}",
|
||||
path.display()
|
||||
)),
|
||||
Err(e) => Err(format!("Failed to read symlink {}: {}", path.display(), e)),
|
||||
}
|
||||
}
|
||||
|
||||
fn read_dmi(field: &str) -> Result<String, String> {
|
||||
let output = Command::new("dmidecode")
|
||||
.arg("-s")
|
||||
.arg(field)
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to execute dmidecode for field {}: {}", field, e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(format!(
|
||||
"dmidecode command failed for field {}: {}",
|
||||
field,
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
}
|
||||
|
||||
String::from_utf8(output.stdout)
|
||||
.map(|s| s.trim().to_string())
|
||||
.map_err(|e| {
|
||||
format!(
|
||||
"Failed to parse dmidecode output for field {}: {}",
|
||||
field, e
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn get_interface_type(device_name: &str, device_path: &Path) -> Result<String, String> {
|
||||
if device_name.starts_with("nvme") {
|
||||
Ok("NVMe".to_string())
|
||||
} else if device_name.starts_with("sd") {
|
||||
Ok("SATA".to_string())
|
||||
} else if device_name.starts_with("hd") {
|
||||
Ok("IDE".to_string())
|
||||
} else if device_name.starts_with("vd") {
|
||||
Ok("VirtIO".to_string())
|
||||
} else {
|
||||
// Try to determine from device path
|
||||
let subsystem = Self::read_sysfs_string(&device_path.join("device/subsystem"))?;
|
||||
Ok(subsystem
|
||||
.split('/')
|
||||
.next_back()
|
||||
.unwrap_or("Unknown")
|
||||
.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn get_smart_status(device_name: &str) -> Result<Option<String>, String> {
|
||||
let output = Command::new("smartctl")
|
||||
.arg("-H")
|
||||
.arg(format!("/dev/{}", device_name))
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to execute smartctl for {}: {}", device_name, e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8(output.stdout)
|
||||
.map_err(|e| format!("Failed to parse smartctl output for {}: {}", device_name, e))?;
|
||||
|
||||
for line in stdout.lines() {
|
||||
if line.contains("SMART overall-health self-assessment") {
|
||||
if let Some(status) = line.split(':').nth(1) {
|
||||
return Ok(Some(status.trim().to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn parse_size(size_str: &str) -> Result<u64, String> {
|
||||
debug!("Parsing size_str '{size_str}'");
|
||||
let size;
|
||||
if size_str.ends_with('T') {
|
||||
size = size_str[..size_str.len() - 1]
|
||||
.parse::<f64>()
|
||||
.map(|t| t * 1024.0 * 1024.0 * 1024.0 * 1024.0)
|
||||
.map_err(|e| format!("Failed to parse T size '{}': {}", size_str, e))
|
||||
} else if size_str.ends_with('G') {
|
||||
size = size_str[..size_str.len() - 1]
|
||||
.parse::<f64>()
|
||||
.map(|g| g * 1024.0 * 1024.0 * 1024.0)
|
||||
.map_err(|e| format!("Failed to parse G size '{}': {}", size_str, e))
|
||||
} else if size_str.ends_with('M') {
|
||||
size = size_str[..size_str.len() - 1]
|
||||
.parse::<f64>()
|
||||
.map(|m| m * 1024.0 * 1024.0)
|
||||
.map_err(|e| format!("Failed to parse M size '{}': {}", size_str, e))
|
||||
} else if size_str.ends_with('K') {
|
||||
size = size_str[..size_str.len() - 1]
|
||||
.parse::<f64>()
|
||||
.map(|k| k * 1024.0)
|
||||
.map_err(|e| format!("Failed to parse K size '{}': {}", size_str, e))
|
||||
} else if size_str.ends_with('B') {
|
||||
size = size_str[..size_str.len() - 1]
|
||||
.parse::<f64>()
|
||||
.map_err(|e| format!("Failed to parse B size '{}': {}", size_str, e))
|
||||
} else {
|
||||
size = size_str
|
||||
.parse::<f64>()
|
||||
.map_err(|e| format!("Failed to parse size '{}': {}", size_str, e))
|
||||
}
|
||||
|
||||
size.map(|s| s as u64)
|
||||
}
|
||||
|
||||
fn get_interface_ips_json(iface_name: &str) -> Result<(Vec<String>, Vec<String>), String> {
|
||||
let mut ipv4 = Vec::new();
|
||||
let mut ipv6 = Vec::new();
|
||||
|
||||
// Get IPv4 addresses using JSON output
|
||||
let output = Command::new("ip")
|
||||
.args(["-j", "-4", "addr", "show", iface_name])
|
||||
.output()
|
||||
.map_err(|e| {
|
||||
format!(
|
||||
"Failed to execute ip command for IPv4 on {}: {}",
|
||||
iface_name, e
|
||||
)
|
||||
})?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(format!(
|
||||
"ip command for IPv4 on {} failed: {}",
|
||||
iface_name,
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
}
|
||||
|
||||
let json: Value = serde_json::from_slice(&output.stdout).map_err(|e| {
|
||||
format!(
|
||||
"Failed to parse ip JSON output for IPv4 on {}: {}",
|
||||
iface_name, e
|
||||
)
|
||||
})?;
|
||||
|
||||
if let Some(addrs) = json.as_array() {
|
||||
for addr_info in addrs {
|
||||
if let Some(addr_info_obj) = addr_info.as_object()
|
||||
&& let Some(addr_info) =
|
||||
addr_info_obj.get("addr_info").and_then(|v| v.as_array())
|
||||
{
|
||||
for addr in addr_info {
|
||||
if let Some(addr_obj) = addr.as_object()
|
||||
&& let Some(ip) = addr_obj.get("local").and_then(|v| v.as_str())
|
||||
{
|
||||
ipv4.push(ip.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get IPv6 addresses using JSON output
|
||||
let output = Command::new("ip")
|
||||
.args(["-j", "-6", "addr", "show", iface_name])
|
||||
.output()
|
||||
.map_err(|e| {
|
||||
format!(
|
||||
"Failed to execute ip command for IPv6 on {}: {}",
|
||||
iface_name, e
|
||||
)
|
||||
})?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(format!(
|
||||
"ip command for IPv6 on {} failed: {}",
|
||||
iface_name,
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
}
|
||||
|
||||
let json: Value = serde_json::from_slice(&output.stdout).map_err(|e| {
|
||||
format!(
|
||||
"Failed to parse ip JSON output for IPv6 on {}: {}",
|
||||
iface_name, e
|
||||
)
|
||||
})?;
|
||||
|
||||
if let Some(addrs) = json.as_array() {
|
||||
for addr_info in addrs {
|
||||
if let Some(addr_info_obj) = addr_info.as_object()
|
||||
&& let Some(addr_info) =
|
||||
addr_info_obj.get("addr_info").and_then(|v| v.as_array())
|
||||
{
|
||||
for addr in addr_info {
|
||||
if let Some(addr_obj) = addr.as_object()
|
||||
&& let Some(ip) = addr_obj.get("local").and_then(|v| v.as_str())
|
||||
{
|
||||
// Skip link-local addresses
|
||||
if !ip.starts_with("fe80::") {
|
||||
ipv6.push(ip.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((ipv4, ipv6))
|
||||
}
|
||||
}
|
||||
37
harmony_inventory_agent/src/main.rs
Normal file
37
harmony_inventory_agent/src/main.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
// src/main.rs
|
||||
use actix_web::{App, HttpServer, Responder, get};
|
||||
use hwinfo::PhysicalHost;
|
||||
use std::env;
|
||||
|
||||
mod hwinfo;
|
||||
|
||||
#[get("/inventory")]
|
||||
async fn inventory() -> impl Responder {
|
||||
log::info!("Received inventory request");
|
||||
let host = PhysicalHost::gather();
|
||||
match host {
|
||||
Ok(host) => {
|
||||
log::info!("Inventory data gathered successfully");
|
||||
actix_web::HttpResponse::Ok().json(host)
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!("Inventory data gathering FAILED");
|
||||
actix_web::HttpResponse::InternalServerError().json(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
env_logger::init();
|
||||
|
||||
let port = env::var("HARMONY_INVENTORY_AGENT_PORT").unwrap_or_else(|_| "8080".to_string());
|
||||
let bind_addr = format!("0.0.0.0:{}", port);
|
||||
|
||||
log::info!("Starting inventory agent on {}", bind_addr);
|
||||
|
||||
HttpServer::new(|| App::new().service(inventory))
|
||||
.bind(&bind_addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
Reference in New Issue
Block a user