logging and config sweep
This commit is contained in:
Generated
+284
@@ -91,6 +91,17 @@ version = "1.0.102"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-trait"
|
||||||
|
version = "0.1.89"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atomic-waker"
|
name = "atomic-waker"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
@@ -464,6 +475,12 @@ version = "1.0.20"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -496,6 +513,12 @@ version = "0.1.9"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fnv"
|
||||||
|
version = "1.0.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foldhash"
|
name = "foldhash"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@@ -655,6 +678,25 @@ version = "0.32.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
|
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "h2"
|
||||||
|
version = "0.4.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54"
|
||||||
|
dependencies = [
|
||||||
|
"atomic-waker",
|
||||||
|
"bytes",
|
||||||
|
"fnv",
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"http",
|
||||||
|
"indexmap 2.13.1",
|
||||||
|
"slab",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.12.3"
|
version = "0.12.3"
|
||||||
@@ -743,6 +785,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"h2",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"httparse",
|
"httparse",
|
||||||
@@ -771,6 +814,19 @@ dependencies = [
|
|||||||
"webpki-roots 1.0.6",
|
"webpki-roots 1.0.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-timeout"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0"
|
||||||
|
dependencies = [
|
||||||
|
"hyper",
|
||||||
|
"hyper-util",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-util"
|
name = "hyper-util"
|
||||||
version = "0.1.20"
|
version = "0.1.20"
|
||||||
@@ -984,6 +1040,15 @@ version = "1.70.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
@@ -1236,6 +1301,82 @@ version = "1.70.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "opentelemetry"
|
||||||
|
version = "0.31.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"js-sys",
|
||||||
|
"pin-project-lite",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "opentelemetry-http"
|
||||||
|
version = "0.31.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"bytes",
|
||||||
|
"http",
|
||||||
|
"opentelemetry",
|
||||||
|
"reqwest",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "opentelemetry-otlp"
|
||||||
|
version = "0.31.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1f69cd6acbb9af919df949cd1ec9e5e7fdc2ef15d234b6b795aaa525cc02f71f"
|
||||||
|
dependencies = [
|
||||||
|
"http",
|
||||||
|
"opentelemetry",
|
||||||
|
"opentelemetry-http",
|
||||||
|
"opentelemetry-proto",
|
||||||
|
"opentelemetry_sdk",
|
||||||
|
"prost",
|
||||||
|
"reqwest",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tokio",
|
||||||
|
"tonic",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "opentelemetry-proto"
|
||||||
|
version = "0.31.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f"
|
||||||
|
dependencies = [
|
||||||
|
"opentelemetry",
|
||||||
|
"opentelemetry_sdk",
|
||||||
|
"prost",
|
||||||
|
"tonic",
|
||||||
|
"tonic-prost",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "opentelemetry_sdk"
|
||||||
|
version = "0.31.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-executor",
|
||||||
|
"futures-util",
|
||||||
|
"opentelemetry",
|
||||||
|
"percent-encoding",
|
||||||
|
"rand 0.9.2",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "owo-colors"
|
name = "owo-colors"
|
||||||
version = "4.3.0"
|
version = "4.3.0"
|
||||||
@@ -1277,6 +1418,26 @@ version = "2.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project"
|
||||||
|
version = "1.1.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517"
|
||||||
|
dependencies = [
|
||||||
|
"pin-project-internal",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-internal"
|
||||||
|
version = "1.1.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project-lite"
|
name = "pin-project-lite"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
@@ -1326,6 +1487,29 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prost"
|
||||||
|
version = "0.14.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"prost-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prost-derive"
|
||||||
|
version = "0.14.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"itertools",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quinn"
|
name = "quinn"
|
||||||
version = "0.11.9"
|
version = "0.11.9"
|
||||||
@@ -1515,7 +1699,9 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
@@ -2061,6 +2247,17 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-stream"
|
||||||
|
version = "0.1.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-tungstenite"
|
name = "tokio-tungstenite"
|
||||||
version = "0.24.0"
|
version = "0.24.0"
|
||||||
@@ -2089,6 +2286,19 @@ dependencies = [
|
|||||||
"tungstenite 0.28.0",
|
"tungstenite 0.28.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-util"
|
||||||
|
version = "0.7.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.8.23"
|
version = "0.8.23"
|
||||||
@@ -2130,6 +2340,43 @@ version = "0.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tonic"
|
||||||
|
version = "0.14.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"base64",
|
||||||
|
"bytes",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper",
|
||||||
|
"hyper-timeout",
|
||||||
|
"hyper-util",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
|
"tower",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tonic-prost"
|
||||||
|
version = "0.14.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"prost",
|
||||||
|
"tonic",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower"
|
name = "tower"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
@@ -2138,9 +2385,12 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"indexmap 2.13.1",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"slab",
|
||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
@@ -2230,6 +2480,32 @@ dependencies = [
|
|||||||
"tracing-core",
|
"tracing-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-opentelemetry"
|
||||||
|
version = "0.32.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1ac28f2d093c6c477eaa76b23525478f38de514fa9aeb1285738d4b97a9552fc"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"opentelemetry",
|
||||||
|
"smallvec",
|
||||||
|
"tracing",
|
||||||
|
"tracing-core",
|
||||||
|
"tracing-log",
|
||||||
|
"tracing-subscriber",
|
||||||
|
"web-time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-serde"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"tracing-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-subscriber"
|
name = "tracing-subscriber"
|
||||||
version = "0.3.23"
|
version = "0.3.23"
|
||||||
@@ -2240,12 +2516,15 @@ dependencies = [
|
|||||||
"nu-ansi-term",
|
"nu-ansi-term",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"regex-automata",
|
"regex-automata",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"sharded-slab",
|
"sharded-slab",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thread_local",
|
"thread_local",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
"tracing-log",
|
"tracing-log",
|
||||||
|
"tracing-serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2434,11 +2713,16 @@ dependencies = [
|
|||||||
"clap",
|
"clap",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"nix",
|
"nix",
|
||||||
|
"opentelemetry",
|
||||||
|
"opentelemetry-otlp",
|
||||||
|
"opentelemetry_sdk",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"toml",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"tracing-opentelemetry",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"uuid",
|
"uuid",
|
||||||
"wakey-agent",
|
"wakey-agent",
|
||||||
|
|||||||
@@ -70,6 +70,82 @@ The project now uses:
|
|||||||
- `wakey-agent` for outbound enrollment and websocket execution
|
- `wakey-agent` for outbound enrollment and websocket execution
|
||||||
- `wakey-control-plane` for enrollment, registry, and command relay
|
- `wakey-control-plane` for enrollment, registry, and command relay
|
||||||
|
|
||||||
|
## Logging and troubleshooting
|
||||||
|
|
||||||
|
Both daemons emit structured tracing logs to stderr. If you are not seeing
|
||||||
|
enrollment/command activity, run with increased verbosity.
|
||||||
|
|
||||||
|
Control-plane also supports a config file at
|
||||||
|
`/etc/wakey-control-plane/config.toml` (override with `--config-file`) so you
|
||||||
|
can persist telemetry settings instead of passing flags.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
bind = "0.0.0.0:8080"
|
||||||
|
public_url = "https://cp.example.com"
|
||||||
|
state_file = "/var/lib/wakey-control-plane/state.json"
|
||||||
|
pid_file = "/var/run/wakey-control-plane.pid"
|
||||||
|
command_timeout_ms = 30000
|
||||||
|
|
||||||
|
[telemetry]
|
||||||
|
otlp_endpoint = "http://127.0.0.1:4317"
|
||||||
|
service_name = "wakey-control-plane"
|
||||||
|
json_logs = false
|
||||||
|
```
|
||||||
|
|
||||||
|
If `telemetry.otlp_endpoint` is omitted, logs still work normally and only local
|
||||||
|
structured logs are emitted.
|
||||||
|
|
||||||
|
### Quick start
|
||||||
|
|
||||||
|
Control-plane:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
wakey-control-plane -v serve --bind 0.0.0.0:8787 --public-url https://cp.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
Agent:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
wakey-agent -v serve --config /etc/wakey-agent/config.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fine-grained log filters
|
||||||
|
|
||||||
|
Use `RUST_LOG` when you want to focus on websocket/API internals.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
RUST_LOG=wakey_control_plane=debug,wakey_agent=debug wakey-control-plane serve
|
||||||
|
RUST_LOG=wakey_agent=debug wakey-agent serve
|
||||||
|
```
|
||||||
|
|
||||||
|
### What you should see
|
||||||
|
|
||||||
|
During registration/enroll:
|
||||||
|
|
||||||
|
- control-plane: `issued enroll token`, then `agent enrollment accepted`
|
||||||
|
- agent: `starting agent enrollment`, then `agent enrollment succeeded and config was written`
|
||||||
|
|
||||||
|
During live connectivity:
|
||||||
|
|
||||||
|
- control-plane: `agent websocket upgraded`, `agent authenticated`, `agent disconnected`
|
||||||
|
- agent: `connecting agent websocket`, `agent websocket session authenticated`, `heartbeat sent` (debug)
|
||||||
|
|
||||||
|
During command relay:
|
||||||
|
|
||||||
|
- control-plane: `dispatching command to agent`
|
||||||
|
- agent: `received command from control-plane`, then `command execution completed` (or `command dispatch failed`)
|
||||||
|
- control-plane: `agent command completed` (or timeout/error warnings)
|
||||||
|
|
||||||
|
During daemon control/state operations:
|
||||||
|
|
||||||
|
- control-plane: `wrote control-plane pid file`, `saved control-plane store`, `reloaded control-plane store from disk`
|
||||||
|
- agent: `wrote wakey-agent pid file`, `sending wakey-agent reload signal`
|
||||||
|
|
||||||
|
If commands still appear silent, verify both processes are running with `-v`
|
||||||
|
and that `RUST_LOG` is not overriding to a stricter level.
|
||||||
|
|
||||||
## CLI
|
## CLI
|
||||||
|
|
||||||
`wakey` is usable as a local/operator CLI.
|
`wakey` is usable as a local/operator CLI.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use tracing::{debug, instrument};
|
use tracing::{debug, info, instrument};
|
||||||
|
|
||||||
use crate::protocol::{
|
use crate::protocol::{
|
||||||
AgentCommand, CommandResult, DevsRequest, InventoryRequest, LeasesRequest, StatusRequest,
|
AgentCommand, CommandResult, DevsRequest, InventoryRequest, LeasesRequest, StatusRequest,
|
||||||
@@ -8,6 +8,8 @@ use crate::protocol::{
|
|||||||
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn dispatch_command(command: AgentCommand) -> Result<CommandResult> {
|
pub async fn dispatch_command(command: AgentCommand) -> Result<CommandResult> {
|
||||||
|
let kind = command_kind(&command);
|
||||||
|
info!(command = %kind, "dispatching command into local wakey services");
|
||||||
match command {
|
match command {
|
||||||
AgentCommand::Status(req) => dispatch_status(req).await,
|
AgentCommand::Status(req) => dispatch_status(req).await,
|
||||||
AgentCommand::Leases(req) => dispatch_leases(req).await,
|
AgentCommand::Leases(req) => dispatch_leases(req).await,
|
||||||
@@ -38,6 +40,7 @@ async fn dispatch_leases(req: LeasesRequest) -> Result<CommandResult> {
|
|||||||
include_state: req.include_state,
|
include_state: req.include_state,
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
debug!(rows = leases.len(), include_state = req.include_state, "dispatched leases command");
|
||||||
Ok(CommandResult::Leases(leases))
|
Ok(CommandResult::Leases(leases))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,11 +53,13 @@ async fn dispatch_devs(req: DevsRequest) -> Result<CommandResult> {
|
|||||||
if req.up_only {
|
if req.up_only {
|
||||||
devs.retain(|dev| dev.operstate == "up");
|
devs.retain(|dev| dev.operstate == "up");
|
||||||
}
|
}
|
||||||
|
debug!(rows = devs.len(), up_only = req.up_only, "dispatched devs command");
|
||||||
Ok(CommandResult::Devs(devs))
|
Ok(CommandResult::Devs(devs))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn dispatch_inventory(req: InventoryRequest) -> Result<CommandResult> {
|
async fn dispatch_inventory(req: InventoryRequest) -> Result<CommandResult> {
|
||||||
let inventory = wakey::inventory(req.into_device_query()).await?;
|
let inventory = wakey::inventory(req.into_device_query()).await?;
|
||||||
|
debug!(rows = inventory.devices.len(), "dispatched inventory command");
|
||||||
Ok(CommandResult::Inventory(inventory))
|
Ok(CommandResult::Inventory(inventory))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,9 +70,20 @@ async fn dispatch_wake(req: WakeRequest) -> Result<CommandResult> {
|
|||||||
(None, Some(mac), ip) => wakey::wake_explicit(mac, ip).await?,
|
(None, Some(mac), ip) => wakey::wake_explicit(mac, ip).await?,
|
||||||
_ => unreachable!("wake request validated before dispatch"),
|
_ => unreachable!("wake request validated before dispatch"),
|
||||||
};
|
};
|
||||||
|
debug!("dispatched wake command");
|
||||||
Ok(CommandResult::Wake(result))
|
Ok(CommandResult::Wake(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn command_kind(command: &AgentCommand) -> &'static str {
|
||||||
|
match command {
|
||||||
|
AgentCommand::Status(_) => "status",
|
||||||
|
AgentCommand::Leases(_) => "leases",
|
||||||
|
AgentCommand::Devs(_) => "devs",
|
||||||
|
AgentCommand::Inventory(_) => "inventory",
|
||||||
|
AgentCommand::Wake(_) => "wake",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn validate_wake_request(req: &WakeRequest) -> Result<()> {
|
pub fn validate_wake_request(req: &WakeRequest) -> Result<()> {
|
||||||
let has_query = req.query.is_some();
|
let has_query = req.query.is_some();
|
||||||
let has_mac = req.mac.is_some();
|
let has_mac = req.mac.is_some();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use anyhow::{Context, Result};
|
|||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use tracing::info;
|
use tracing::{info, warn};
|
||||||
|
|
||||||
use crate::config::{AgentConfig, save_config};
|
use crate::config::{AgentConfig, save_config};
|
||||||
|
|
||||||
@@ -21,6 +21,7 @@ struct EnrollResponse {
|
|||||||
pub async fn enroll(server_url: &str, enroll_token: &str, config_path: &Path) -> Result<AgentConfig> {
|
pub async fn enroll(server_url: &str, enroll_token: &str, config_path: &Path) -> Result<AgentConfig> {
|
||||||
let server_url = normalize_server_url(server_url);
|
let server_url = normalize_server_url(server_url);
|
||||||
let endpoint = format!("{server_url}/api/v1/agents/enroll");
|
let endpoint = format!("{server_url}/api/v1/agents/enroll");
|
||||||
|
info!(endpoint = %endpoint, config_path = %config_path.display(), "starting agent enrollment");
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
let response = client
|
let response = client
|
||||||
.post(endpoint)
|
.post(endpoint)
|
||||||
@@ -35,6 +36,7 @@ pub async fn enroll(server_url: &str, enroll_token: &str, config_path: &Path) ->
|
|||||||
.text()
|
.text()
|
||||||
.await
|
.await
|
||||||
.unwrap_or_else(|_| "<unable to read enrollment error body>".into());
|
.unwrap_or_else(|_| "<unable to read enrollment error body>".into());
|
||||||
|
warn!(status = %status, "agent enrollment request rejected by control-plane");
|
||||||
anyhow::bail!("enrollment failed with {status}: {body}");
|
anyhow::bail!("enrollment failed with {status}: {body}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +53,7 @@ pub async fn enroll(server_url: &str, enroll_token: &str, config_path: &Path) ->
|
|||||||
reconnect_max_ms: 30_000,
|
reconnect_max_ms: 30_000,
|
||||||
};
|
};
|
||||||
save_config(config_path, &config)?;
|
save_config(config_path, &config)?;
|
||||||
info!(config_path = %config_path.display(), "wrote agent config");
|
info!(agent_id = %config.agent_id, config_path = %config_path.display(), "agent enrollment succeeded and config was written");
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+13
-3
@@ -17,8 +17,12 @@ async fn main() -> Result<()> {
|
|||||||
tracing::init(cli.verbose);
|
tracing::init(cli.verbose);
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Command::Serve(args) => serve::serve(args).await?,
|
Command::Serve(args) => {
|
||||||
|
::tracing::info!("wakey-agent command: serve");
|
||||||
|
serve::serve(args).await?
|
||||||
|
}
|
||||||
Command::Enroll(args) => {
|
Command::Enroll(args) => {
|
||||||
|
::tracing::info!(server_url = %args.server_url, config = %args.config.display(), "wakey-agent command: enroll");
|
||||||
let config = enroll::enroll(&args.server_url, &args.enroll_token, &args.config).await?;
|
let config = enroll::enroll(&args.server_url, &args.enroll_token, &args.config).await?;
|
||||||
println!("agent_id={}", config.agent_id);
|
println!("agent_id={}", config.agent_id);
|
||||||
println!("config={}", args.config.display());
|
println!("config={}", args.config.display());
|
||||||
@@ -29,8 +33,14 @@ async fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::InitConfig(args) => init_config(args)?,
|
Command::InitConfig(args) => {
|
||||||
Command::Reload(args) => serve::reload_daemon(&args.pid_file)?,
|
::tracing::info!(config = %args.config.display(), force = args.force, "wakey-agent command: init-config");
|
||||||
|
init_config(args)?
|
||||||
|
}
|
||||||
|
Command::Reload(args) => {
|
||||||
|
::tracing::info!(pid_file = %args.pid_file.display(), "wakey-agent command: reload");
|
||||||
|
serve::reload_daemon(&args.pid_file)?
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ pub async fn serve(args: ServeArgs) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
write_pid_file(&args.pid_file)?;
|
write_pid_file(&args.pid_file)?;
|
||||||
|
info!(pid_file = %args.pid_file.display(), "wrote wakey-agent pid file");
|
||||||
|
|
||||||
let mut cfg = config::load_config(&args.config)?;
|
let mut cfg = config::load_config(&args.config)?;
|
||||||
info!(config_path = %args.config.display(), agent_id = %cfg.agent_id, "starting wakey-agent");
|
info!(config_path = %args.config.display(), agent_id = %cfg.agent_id, "starting wakey-agent");
|
||||||
@@ -48,6 +49,7 @@ pub async fn serve(args: ServeArgs) -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
join = &mut worker => {
|
join = &mut worker => {
|
||||||
|
warn!("agent worker task exited; shutting down daemon");
|
||||||
let _ = remove_pid_file(&args.pid_file);
|
let _ = remove_pid_file(&args.pid_file);
|
||||||
return join.context("agent session join failed")?;
|
return join.context("agent session join failed")?;
|
||||||
}
|
}
|
||||||
@@ -69,6 +71,7 @@ pub async fn serve(args: ServeArgs) -> Result<()> {
|
|||||||
|
|
||||||
pub fn reload_daemon(pid_file: &Path) -> Result<()> {
|
pub fn reload_daemon(pid_file: &Path) -> Result<()> {
|
||||||
let pid = read_pid(pid_file)?;
|
let pid = read_pid(pid_file)?;
|
||||||
|
info!(pid, pid_file = %pid_file.display(), "sending wakey-agent reload signal");
|
||||||
send_hup(pid)
|
send_hup(pid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ use anyhow::{Context, Result};
|
|||||||
use futures_util::{SinkExt, StreamExt};
|
use futures_util::{SinkExt, StreamExt};
|
||||||
use tokio::time::{Duration, MissedTickBehavior, interval, sleep};
|
use tokio::time::{Duration, MissedTickBehavior, interval, sleep};
|
||||||
use tokio_tungstenite::{connect_async, tungstenite::Message};
|
use tokio_tungstenite::{connect_async, tungstenite::Message};
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, info_span, warn};
|
||||||
|
|
||||||
use crate::config::AgentConfig;
|
use crate::config::AgentConfig;
|
||||||
use crate::dispatch::dispatch_command;
|
use crate::dispatch::dispatch_command;
|
||||||
use crate::protocol::{ClientMessage, ErrorPayload, ServerMessage};
|
use crate::protocol::{AgentCommand, ClientMessage, ErrorPayload, ServerMessage};
|
||||||
|
|
||||||
pub async fn run(config: AgentConfig) -> Result<()> {
|
pub async fn run(config: AgentConfig) -> Result<()> {
|
||||||
let mut backoff = config.reconnect_base_ms.max(100);
|
let mut backoff = config.reconnect_base_ms.max(100);
|
||||||
@@ -25,6 +25,17 @@ pub async fn run(config: AgentConfig) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn run_once(config: &AgentConfig) -> Result<()> {
|
async fn run_once(config: &AgentConfig) -> Result<()> {
|
||||||
|
let session_id = format!(
|
||||||
|
"{}-{}",
|
||||||
|
std::process::id(),
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_millis())
|
||||||
|
.unwrap_or_default()
|
||||||
|
);
|
||||||
|
let span = info_span!("agent_session", session_id = %session_id, agent_id = %config.agent_id);
|
||||||
|
let _span_guard = span.enter();
|
||||||
|
|
||||||
let ws_url = websocket_url(&config.server_url)?;
|
let ws_url = websocket_url(&config.server_url)?;
|
||||||
info!(%ws_url, agent_id = %config.agent_id, "connecting agent websocket");
|
info!(%ws_url, agent_id = %config.agent_id, "connecting agent websocket");
|
||||||
let (stream, _) = connect_async(ws_url.as_str())
|
let (stream, _) = connect_async(ws_url.as_str())
|
||||||
@@ -44,6 +55,7 @@ async fn run_once(config: &AgentConfig) -> Result<()> {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
info!(agent_id = %config.agent_id, "agent websocket session authenticated");
|
||||||
|
|
||||||
let mut heartbeat = interval(Duration::from_secs(30));
|
let mut heartbeat = interval(Duration::from_secs(30));
|
||||||
heartbeat.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
heartbeat.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
||||||
@@ -54,11 +66,15 @@ async fn run_once(config: &AgentConfig) -> Result<()> {
|
|||||||
send_json(&mut sink, &ClientMessage::Heartbeat {
|
send_json(&mut sink, &ClientMessage::Heartbeat {
|
||||||
agent_id: config.agent_id.clone(),
|
agent_id: config.agent_id.clone(),
|
||||||
}).await?;
|
}).await?;
|
||||||
|
debug!(agent_id = %config.agent_id, "heartbeat sent");
|
||||||
}
|
}
|
||||||
maybe_msg = source.next() => {
|
maybe_msg = source.next() => {
|
||||||
let msg = match maybe_msg {
|
let msg = match maybe_msg {
|
||||||
Some(msg) => msg.context("websocket frame failed")?,
|
Some(msg) => msg.context("websocket frame failed")?,
|
||||||
None => anyhow::bail!("websocket closed by server"),
|
None => {
|
||||||
|
info!("websocket stream closed by server");
|
||||||
|
anyhow::bail!("websocket closed by server");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match msg {
|
match msg {
|
||||||
@@ -79,6 +95,7 @@ async fn run_once(config: &AgentConfig) -> Result<()> {
|
|||||||
}
|
}
|
||||||
Message::Pong(_) => {}
|
Message::Pong(_) => {}
|
||||||
Message::Close(frame) => {
|
Message::Close(frame) => {
|
||||||
|
info!(close = ?frame, "received close frame from server");
|
||||||
anyhow::bail!("websocket closed: {:?}", frame);
|
anyhow::bail!("websocket closed: {:?}", frame);
|
||||||
}
|
}
|
||||||
Message::Binary(_) => {
|
Message::Binary(_) => {
|
||||||
@@ -105,12 +122,16 @@ where
|
|||||||
ServerMessage::Command {
|
ServerMessage::Command {
|
||||||
request_id,
|
request_id,
|
||||||
command,
|
command,
|
||||||
} => match dispatch_command(command).await {
|
} => {
|
||||||
|
let kind = command_kind(&command);
|
||||||
|
info!(request_id = %request_id, command = %kind, "received command from control-plane");
|
||||||
|
match dispatch_command(command).await {
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
|
info!(request_id = %request_id, command = %kind, "command execution completed");
|
||||||
send_json(sink, &ClientMessage::Result { request_id, result }).await?;
|
send_json(sink, &ClientMessage::Result { request_id, result }).await?;
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(request_id = %request_id, error = %err, "command dispatch failed");
|
error!(request_id = %request_id, command = %kind, error = %err, "command dispatch failed");
|
||||||
send_json(
|
send_json(
|
||||||
sink,
|
sink,
|
||||||
&ClientMessage::Error {
|
&ClientMessage::Error {
|
||||||
@@ -124,7 +145,8 @@ where
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -135,12 +157,33 @@ where
|
|||||||
<S as futures_util::Sink<Message>>::Error: std::error::Error + Send + Sync + 'static,
|
<S as futures_util::Sink<Message>>::Error: std::error::Error + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
let payload = serde_json::to_string(message).context("failed to serialize websocket message")?;
|
let payload = serde_json::to_string(message).context("failed to serialize websocket message")?;
|
||||||
|
debug!(message_type = %client_message_kind(message), "sending websocket message");
|
||||||
sink.send(Message::Text(payload))
|
sink.send(Message::Text(payload))
|
||||||
.await
|
.await
|
||||||
.context("failed to send websocket message")?;
|
.context("failed to send websocket message")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn command_kind(command: &AgentCommand) -> &'static str {
|
||||||
|
match command {
|
||||||
|
AgentCommand::Status(_) => "status",
|
||||||
|
AgentCommand::Leases(_) => "leases",
|
||||||
|
AgentCommand::Devs(_) => "devs",
|
||||||
|
AgentCommand::Inventory(_) => "inventory",
|
||||||
|
AgentCommand::Wake(_) => "wake",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn client_message_kind(message: &ClientMessage) -> &'static str {
|
||||||
|
match message {
|
||||||
|
ClientMessage::Hello { .. } => "hello",
|
||||||
|
ClientMessage::Auth { .. } => "auth",
|
||||||
|
ClientMessage::Heartbeat { .. } => "heartbeat",
|
||||||
|
ClientMessage::Result { .. } => "result",
|
||||||
|
ClientMessage::Error { .. } => "error",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn websocket_url(server_url: &str) -> Result<url::Url> {
|
pub fn websocket_url(server_url: &str) -> Result<url::Url> {
|
||||||
let base = url::Url::parse(server_url).context("invalid server_url")?;
|
let base = url::Url::parse(server_url).context("invalid server_url")?;
|
||||||
let scheme = match base.scheme() {
|
let scheme = match base.scheme() {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ nix = { version = "0.30", default-features = false, features = ["signal", "proce
|
|||||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
toml = "0.8"
|
||||||
tokio = { version = "1", features = [
|
tokio = { version = "1", features = [
|
||||||
"macros",
|
"macros",
|
||||||
"rt-multi-thread",
|
"rt-multi-thread",
|
||||||
@@ -21,6 +22,10 @@ tokio = { version = "1", features = [
|
|||||||
"signal",
|
"signal",
|
||||||
] }
|
] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
tracing-opentelemetry = "0.32"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "json"] }
|
||||||
|
opentelemetry = "0.31"
|
||||||
|
opentelemetry-otlp = { version = "0.31", features = ["grpc-tonic", "trace"] }
|
||||||
|
opentelemetry_sdk = { version = "0.31", features = ["rt-tokio"] }
|
||||||
uuid = { version = "1", features = ["v4", "serde"] }
|
uuid = { version = "1", features = ["v4", "serde"] }
|
||||||
wakey-agent = { path = "../wakey-agent", registry = "gitea" }
|
wakey-agent = { path = "../wakey-agent", registry = "gitea" }
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use axum::extract::{Path as AxumPath, State};
|
|||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tracing::{info, info_span, warn};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use wakey_agent::protocol::{AgentCommand, ErrorPayload, RequestId, ServerMessage};
|
use wakey_agent::protocol::{AgentCommand, ErrorPayload, RequestId, ServerMessage};
|
||||||
|
|
||||||
@@ -56,19 +57,25 @@ pub async fn enroll(
|
|||||||
Json(req): Json<EnrollRequest>,
|
Json(req): Json<EnrollRequest>,
|
||||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
||||||
match state.store.enroll(&req.enroll_token).await {
|
match state.store.enroll(&req.enroll_token).await {
|
||||||
Ok(issued) => Ok((
|
Ok(issued) => {
|
||||||
|
info!(agent_id = %issued.agent_id, "agent enrollment accepted");
|
||||||
|
Ok((
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
Json(EnrollResponse {
|
Json(EnrollResponse {
|
||||||
agent_id: issued.agent_id,
|
agent_id: issued.agent_id,
|
||||||
agent_token: issued.agent_token,
|
agent_token: issued.agent_token,
|
||||||
server_url: state.public_url,
|
server_url: state.public_url,
|
||||||
}),
|
}),
|
||||||
)),
|
))
|
||||||
Err(err) => Err(json_error(
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = %err, "agent enrollment rejected");
|
||||||
|
Err(json_error(
|
||||||
StatusCode::UNAUTHORIZED,
|
StatusCode::UNAUTHORIZED,
|
||||||
"enrollment_rejected",
|
"enrollment_rejected",
|
||||||
&err.to_string(),
|
&err.to_string(),
|
||||||
)),
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,17 +83,23 @@ pub async fn issue_enroll_token(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
||||||
match state.store.issue_enroll_token().await {
|
match state.store.issue_enroll_token().await {
|
||||||
Ok(token) => Ok((
|
Ok(token) => {
|
||||||
|
info!("issued enroll token");
|
||||||
|
Ok((
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
Json(IssueEnrollTokenResponse {
|
Json(IssueEnrollTokenResponse {
|
||||||
enroll_token: token,
|
enroll_token: token,
|
||||||
}),
|
}),
|
||||||
)),
|
))
|
||||||
Err(err) => Err(json_error(
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = %err, "failed to issue enroll token");
|
||||||
|
Err(json_error(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
"issue_enroll_token_failed",
|
"issue_enroll_token_failed",
|
||||||
&err.to_string(),
|
&err.to_string(),
|
||||||
)),
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +126,15 @@ pub async fn run_command(
|
|||||||
Json(req): Json<RelayCommandRequest>,
|
Json(req): Json<RelayCommandRequest>,
|
||||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
||||||
let request_id_string = format!("req-{}", Uuid::new_v4());
|
let request_id_string = format!("req-{}", Uuid::new_v4());
|
||||||
|
let command = command_kind(&req.command);
|
||||||
|
let span = info_span!(
|
||||||
|
"relay_command",
|
||||||
|
agent_id = %agent_id,
|
||||||
|
request_id = %request_id_string,
|
||||||
|
command = %command,
|
||||||
|
);
|
||||||
|
let _span_guard = span.enter();
|
||||||
|
|
||||||
let request_id = RequestId::try_from(request_id_string.clone()).map_err(|err| {
|
let request_id = RequestId::try_from(request_id_string.clone()).map_err(|err| {
|
||||||
json_error(
|
json_error(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
@@ -126,6 +148,7 @@ pub async fn run_command(
|
|||||||
sessions.get(&agent_id).cloned()
|
sessions.get(&agent_id).cloned()
|
||||||
}
|
}
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
|
warn!("command rejected: agent not connected");
|
||||||
json_error(
|
json_error(
|
||||||
StatusCode::NOT_FOUND,
|
StatusCode::NOT_FOUND,
|
||||||
"agent_not_connected",
|
"agent_not_connected",
|
||||||
@@ -140,11 +163,16 @@ pub async fn run_command(
|
|||||||
.await
|
.await
|
||||||
.insert(request_id_string.clone(), pending_tx);
|
.insert(request_id_string.clone(), pending_tx);
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"dispatching command to agent"
|
||||||
|
);
|
||||||
|
|
||||||
if let Err(err) = tx.send(ServerMessage::Command {
|
if let Err(err) = tx.send(ServerMessage::Command {
|
||||||
request_id,
|
request_id,
|
||||||
command: req.command,
|
command: req.command,
|
||||||
}) {
|
}) {
|
||||||
state.pending.lock().await.remove(&request_id_string);
|
state.pending.lock().await.remove(&request_id_string);
|
||||||
|
warn!(error = %err, "failed sending command to agent session");
|
||||||
return Err(json_error(
|
return Err(json_error(
|
||||||
StatusCode::BAD_GATEWAY,
|
StatusCode::BAD_GATEWAY,
|
||||||
"agent_send_failed",
|
"agent_send_failed",
|
||||||
@@ -159,19 +187,26 @@ pub async fn run_command(
|
|||||||
);
|
);
|
||||||
let outcome = tokio::time::timeout(timeout, pending_rx).await;
|
let outcome = tokio::time::timeout(timeout, pending_rx).await;
|
||||||
let response = match outcome {
|
let response = match outcome {
|
||||||
Ok(Ok(AgentReply::Result(result))) => RelayCommandResponse {
|
Ok(Ok(AgentReply::Result(result))) => {
|
||||||
|
info!("agent command completed");
|
||||||
|
RelayCommandResponse {
|
||||||
request_id: request_id_string,
|
request_id: request_id_string,
|
||||||
status: "ok".into(),
|
status: "ok".into(),
|
||||||
result: Some(result),
|
result: Some(result),
|
||||||
error: None,
|
error: None,
|
||||||
},
|
}
|
||||||
Ok(Ok(AgentReply::Error(error))) => RelayCommandResponse {
|
}
|
||||||
|
Ok(Ok(AgentReply::Error(error))) => {
|
||||||
|
warn!(code = %error.code, "agent command returned error");
|
||||||
|
RelayCommandResponse {
|
||||||
request_id: request_id_string,
|
request_id: request_id_string,
|
||||||
status: "error".into(),
|
status: "error".into(),
|
||||||
result: None,
|
result: None,
|
||||||
error: Some(error),
|
error: Some(error),
|
||||||
},
|
}
|
||||||
|
}
|
||||||
Ok(Err(_)) => {
|
Ok(Err(_)) => {
|
||||||
|
warn!("agent response channel dropped");
|
||||||
return Err(json_error(
|
return Err(json_error(
|
||||||
StatusCode::BAD_GATEWAY,
|
StatusCode::BAD_GATEWAY,
|
||||||
"agent_response_dropped",
|
"agent_response_dropped",
|
||||||
@@ -180,6 +215,7 @@ pub async fn run_command(
|
|||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
state.pending.lock().await.remove(&request_id_string);
|
state.pending.lock().await.remove(&request_id_string);
|
||||||
|
warn!(timeout_ms = timeout.as_millis() as u64, "agent command timed out");
|
||||||
return Err(json_error(
|
return Err(json_error(
|
||||||
StatusCode::GATEWAY_TIMEOUT,
|
StatusCode::GATEWAY_TIMEOUT,
|
||||||
"agent_timeout",
|
"agent_timeout",
|
||||||
@@ -206,3 +242,13 @@ pub fn json_error(
|
|||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn command_kind(command: &AgentCommand) -> &'static str {
|
||||||
|
match command {
|
||||||
|
AgentCommand::Status(_) => "status",
|
||||||
|
AgentCommand::Leases(_) => "leases",
|
||||||
|
AgentCommand::Devs(_) => "devs",
|
||||||
|
AgentCommand::Inventory(_) => "inventory",
|
||||||
|
AgentCommand::Wake(_) => "wake",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use clap::{ArgAction, Args, Parser, Subcommand};
|
|||||||
|
|
||||||
pub const DEFAULT_STATE_FILE: &str = "/var/lib/wakey-control-plane/state.json";
|
pub const DEFAULT_STATE_FILE: &str = "/var/lib/wakey-control-plane/state.json";
|
||||||
pub const DEFAULT_PID_FILE: &str = "/var/run/wakey-control-plane.pid";
|
pub const DEFAULT_PID_FILE: &str = "/var/run/wakey-control-plane.pid";
|
||||||
|
pub const DEFAULT_CONFIG_FILE: &str = "/etc/wakey-control-plane/config.toml";
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "wakey-control-plane")]
|
#[command(name = "wakey-control-plane")]
|
||||||
@@ -29,23 +30,26 @@ pub enum Command {
|
|||||||
|
|
||||||
#[derive(Args, Clone)]
|
#[derive(Args, Clone)]
|
||||||
pub struct ServeArgs {
|
pub struct ServeArgs {
|
||||||
#[arg(long, default_value = "0.0.0.0:8080")]
|
#[arg(long)]
|
||||||
pub bind: SocketAddr,
|
pub bind: Option<SocketAddr>,
|
||||||
|
|
||||||
#[arg(long, default_value = "http://127.0.0.1:8080")]
|
#[arg(long)]
|
||||||
pub public_url: String,
|
pub public_url: Option<String>,
|
||||||
|
|
||||||
#[arg(long, default_value = DEFAULT_STATE_FILE)]
|
#[arg(long)]
|
||||||
pub state_file: PathBuf,
|
pub state_file: Option<PathBuf>,
|
||||||
|
|
||||||
#[arg(long = "enroll-token")]
|
#[arg(long = "enroll-token")]
|
||||||
pub enroll_tokens: Vec<String>,
|
pub enroll_tokens: Vec<String>,
|
||||||
|
|
||||||
#[arg(long, default_value_t = 30_000)]
|
#[arg(long)]
|
||||||
pub command_timeout_ms: u64,
|
pub command_timeout_ms: Option<u64>,
|
||||||
|
|
||||||
#[arg(long, default_value = DEFAULT_PID_FILE)]
|
#[arg(long)]
|
||||||
pub pid_file: PathBuf,
|
pub pid_file: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[arg(long, default_value = DEFAULT_CONFIG_FILE)]
|
||||||
|
pub config_file: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::cli::ServeArgs;
|
use crate::cli::ServeArgs;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -11,18 +15,136 @@ pub struct DaemonConfig {
|
|||||||
pub state_file: PathBuf,
|
pub state_file: PathBuf,
|
||||||
pub command_timeout: Duration,
|
pub command_timeout: Duration,
|
||||||
pub pid_file: PathBuf,
|
pub pid_file: PathBuf,
|
||||||
|
pub enroll_tokens: Vec<String>,
|
||||||
|
pub telemetry: TelemetryConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TelemetryConfig {
|
||||||
|
pub otlp_endpoint: Option<String>,
|
||||||
|
pub service_name: String,
|
||||||
|
pub json_logs: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TelemetryConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
otlp_endpoint: None,
|
||||||
|
service_name: "wakey-control-plane".to_string(),
|
||||||
|
json_logs: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Default)]
|
||||||
|
struct FileConfig {
|
||||||
|
bind: Option<String>,
|
||||||
|
public_url: Option<String>,
|
||||||
|
state_file: Option<PathBuf>,
|
||||||
|
command_timeout_ms: Option<u64>,
|
||||||
|
pid_file: Option<PathBuf>,
|
||||||
|
enroll_tokens: Option<Vec<String>>,
|
||||||
|
telemetry: Option<FileTelemetryConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Default)]
|
||||||
|
struct FileTelemetryConfig {
|
||||||
|
otlp_endpoint: Option<String>,
|
||||||
|
service_name: Option<String>,
|
||||||
|
json_logs: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DaemonConfig {
|
impl DaemonConfig {
|
||||||
pub fn from_serve_args(args: &ServeArgs) -> Self {
|
pub fn from_serve_args(args: &ServeArgs) -> Result<Self> {
|
||||||
Self {
|
let file = load_file_config(&args.config_file)?;
|
||||||
bind: args.bind,
|
|
||||||
public_url: normalize_public_url(&args.public_url),
|
let bind = match args.bind {
|
||||||
state_file: args.state_file.clone(),
|
Some(bind) => bind,
|
||||||
command_timeout: Duration::from_millis(args.command_timeout_ms.max(1)),
|
None => match file.bind {
|
||||||
pid_file: args.pid_file.clone(),
|
Some(ref bind) => bind
|
||||||
|
.parse::<SocketAddr>()
|
||||||
|
.with_context(|| format!("invalid bind address `{bind}` in {}", args.config_file.display()))?,
|
||||||
|
None => "0.0.0.0:8080".parse().expect("static default bind should parse"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let public_url = normalize_public_url(
|
||||||
|
args.public_url
|
||||||
|
.as_deref()
|
||||||
|
.or(file.public_url.as_deref())
|
||||||
|
.unwrap_or("http://127.0.0.1:8080"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let state_file = args
|
||||||
|
.state_file
|
||||||
|
.clone()
|
||||||
|
.or(file.state_file)
|
||||||
|
.unwrap_or_else(|| PathBuf::from(crate::cli::DEFAULT_STATE_FILE));
|
||||||
|
|
||||||
|
let command_timeout = Duration::from_millis(
|
||||||
|
args.command_timeout_ms
|
||||||
|
.or(file.command_timeout_ms)
|
||||||
|
.unwrap_or(30_000)
|
||||||
|
.max(1),
|
||||||
|
);
|
||||||
|
|
||||||
|
let pid_file = args
|
||||||
|
.pid_file
|
||||||
|
.clone()
|
||||||
|
.or(file.pid_file)
|
||||||
|
.unwrap_or_else(|| PathBuf::from(crate::cli::DEFAULT_PID_FILE));
|
||||||
|
|
||||||
|
let enroll_tokens = if args.enroll_tokens.is_empty() {
|
||||||
|
file.enroll_tokens.unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
args.enroll_tokens.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
let telemetry = resolve_telemetry(file.telemetry);
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
bind,
|
||||||
|
public_url,
|
||||||
|
state_file,
|
||||||
|
command_timeout,
|
||||||
|
pid_file,
|
||||||
|
enroll_tokens,
|
||||||
|
telemetry,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_file_config(path: &Path) -> Result<FileConfig> {
|
||||||
|
if !path.exists() {
|
||||||
|
return Ok(FileConfig::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
let raw = std::fs::read_to_string(path)
|
||||||
|
.with_context(|| format!("failed to read config file {}", path.display()))?;
|
||||||
|
toml::from_str::<FileConfig>(&raw)
|
||||||
|
.with_context(|| format!("failed to parse config file {}", path.display()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_telemetry(file: Option<FileTelemetryConfig>) -> TelemetryConfig {
|
||||||
|
let mut out = TelemetryConfig::default();
|
||||||
|
if let Some(file) = file {
|
||||||
|
if let Some(endpoint) = file.otlp_endpoint {
|
||||||
|
let trimmed = endpoint.trim().to_string();
|
||||||
|
if !trimmed.is_empty() {
|
||||||
|
out.otlp_endpoint = Some(trimmed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(name) = file.service_name {
|
||||||
|
let trimmed = name.trim();
|
||||||
|
if !trimmed.is_empty() {
|
||||||
|
out.service_name = trimmed.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(json_logs) = file.json_logs {
|
||||||
|
out.json_logs = json_logs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn normalize_public_url(url: &str) -> String {
|
pub fn normalize_public_url(url: &str) -> String {
|
||||||
|
|||||||
@@ -13,11 +13,20 @@ use cli::{Cli, Command};
|
|||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
tracing::init(cli.verbose);
|
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Command::Serve(args) => runtime::serve(args).await,
|
Command::Serve(args) => {
|
||||||
Command::IssueEnrollToken(args) => runtime::issue_enroll_token(args).await,
|
let daemon = config::DaemonConfig::from_serve_args(&args)?;
|
||||||
Command::Reload(args) => runtime::reload_daemon(&args.pid_file),
|
tracing::init(cli.verbose, &daemon.telemetry)?;
|
||||||
|
runtime::serve(daemon).await
|
||||||
|
}
|
||||||
|
Command::IssueEnrollToken(args) => {
|
||||||
|
tracing::init(cli.verbose, &config::TelemetryConfig::default())?;
|
||||||
|
runtime::issue_enroll_token(args).await
|
||||||
|
}
|
||||||
|
Command::Reload(args) => {
|
||||||
|
tracing::init(cli.verbose, &config::TelemetryConfig::default())?;
|
||||||
|
runtime::reload_daemon(&args.pid_file)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use tracing::{info, warn};
|
|||||||
use wakey_agent::protocol::{ErrorPayload, ServerMessage};
|
use wakey_agent::protocol::{ErrorPayload, ServerMessage};
|
||||||
|
|
||||||
use crate::api;
|
use crate::api;
|
||||||
use crate::cli::{IssueEnrollTokenArgs, ServeArgs};
|
use crate::cli::IssueEnrollTokenArgs;
|
||||||
use crate::config;
|
use crate::config;
|
||||||
use crate::state;
|
use crate::state;
|
||||||
use crate::ws;
|
use crate::ws;
|
||||||
@@ -31,11 +31,11 @@ pub enum AgentReply {
|
|||||||
Error(ErrorPayload),
|
Error(ErrorPayload),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn serve(args: ServeArgs) -> Result<()> {
|
pub async fn serve(daemon: config::DaemonConfig) -> Result<()> {
|
||||||
let daemon = config::DaemonConfig::from_serve_args(&args);
|
|
||||||
write_pid_file(&daemon.pid_file)?;
|
write_pid_file(&daemon.pid_file)?;
|
||||||
|
info!(pid_file = %daemon.pid_file.display(), "wrote control-plane pid file");
|
||||||
|
|
||||||
let store = state::Store::load_or_init(&daemon.state_file, args.enroll_tokens)
|
let store = state::Store::load_or_init(&daemon.state_file, daemon.enroll_tokens.clone())
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("failed to initialize store {}", daemon.state_file.display()))?;
|
.with_context(|| format!("failed to initialize store {}", daemon.state_file.display()))?;
|
||||||
|
|
||||||
@@ -109,6 +109,7 @@ pub async fn issue_enroll_token(args: IssueEnrollTokenArgs) -> Result<()> {
|
|||||||
if let Some(url) = args.public_url {
|
if let Some(url) = args.public_url {
|
||||||
let base = config::normalize_public_url(&url);
|
let base = config::normalize_public_url(&url);
|
||||||
let endpoint = config::issue_token_endpoint(&base);
|
let endpoint = config::issue_token_endpoint(&base);
|
||||||
|
info!(endpoint = %endpoint, "requesting live enroll token from running control-plane daemon");
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
let response = client
|
let response = client
|
||||||
@@ -130,6 +131,7 @@ pub async fn issue_enroll_token(args: IssueEnrollTokenArgs) -> Result<()> {
|
|||||||
.json()
|
.json()
|
||||||
.await
|
.await
|
||||||
.context("failed to decode live issuance response")?;
|
.context("failed to decode live issuance response")?;
|
||||||
|
info!("received live enroll token response");
|
||||||
|
|
||||||
println!("enroll_token={}", payload.enroll_token);
|
println!("enroll_token={}", payload.enroll_token);
|
||||||
println!(
|
println!(
|
||||||
@@ -140,6 +142,7 @@ pub async fn issue_enroll_token(args: IssueEnrollTokenArgs) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fallback for offline tooling: writes to state file, requires daemon reload to pick up.
|
// Fallback for offline tooling: writes to state file, requires daemon reload to pick up.
|
||||||
|
info!(state_file = %args.state_file.display(), "issuing enroll token via offline state file fallback");
|
||||||
let store = state::Store::load_or_init(&args.state_file, args.enroll_tokens)
|
let store = state::Store::load_or_init(&args.state_file, args.enroll_tokens)
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("failed to initialize store {}", args.state_file.display()))?;
|
.with_context(|| format!("failed to initialize store {}", args.state_file.display()))?;
|
||||||
@@ -154,6 +157,7 @@ pub async fn issue_enroll_token(args: IssueEnrollTokenArgs) -> Result<()> {
|
|||||||
|
|
||||||
pub fn reload_daemon(pid_file: &Path) -> Result<()> {
|
pub fn reload_daemon(pid_file: &Path) -> Result<()> {
|
||||||
let pid = read_pid(pid_file)?;
|
let pid = read_pid(pid_file)?;
|
||||||
|
info!(pid, pid_file = %pid_file.display(), "sending control-plane reload signal");
|
||||||
send_hup(pid)
|
send_hup(pid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use std::path::{Path, PathBuf};
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
use tracing::{debug, info, warn};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -35,9 +36,11 @@ impl Store {
|
|||||||
let raw = tokio::fs::read_to_string(path)
|
let raw = tokio::fs::read_to_string(path)
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("failed to read store {}", path.display()))?;
|
.with_context(|| format!("failed to read store {}", path.display()))?;
|
||||||
|
debug!(path = %path.display(), bytes = raw.len(), "loading persisted control-plane store");
|
||||||
serde_json::from_str::<PersistedState>(&raw)
|
serde_json::from_str::<PersistedState>(&raw)
|
||||||
.with_context(|| format!("failed to decode store {}", path.display()))?
|
.with_context(|| format!("failed to decode store {}", path.display()))?
|
||||||
} else {
|
} else {
|
||||||
|
info!(path = %path.display(), seeded_enroll_tokens = seeded_tokens.len(), "initializing new control-plane store");
|
||||||
PersistedState {
|
PersistedState {
|
||||||
enroll_tokens: seeded_tokens,
|
enroll_tokens: seeded_tokens,
|
||||||
agents: HashMap::new(),
|
agents: HashMap::new(),
|
||||||
@@ -49,12 +52,23 @@ impl Store {
|
|||||||
state: RwLock::new(initial),
|
state: RwLock::new(initial),
|
||||||
};
|
};
|
||||||
store.save().await?;
|
store.save().await?;
|
||||||
|
let (enroll_tokens, agents) = {
|
||||||
|
let snapshot = store.state.read().await;
|
||||||
|
(snapshot.enroll_tokens.len(), snapshot.agents.len())
|
||||||
|
};
|
||||||
|
info!(
|
||||||
|
path = %store.path.display(),
|
||||||
|
enroll_tokens,
|
||||||
|
agents,
|
||||||
|
"control-plane store ready"
|
||||||
|
);
|
||||||
Ok(store)
|
Ok(store)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn enroll(&self, enroll_token: &str) -> Result<IssuedAgent> {
|
pub async fn enroll(&self, enroll_token: &str) -> Result<IssuedAgent> {
|
||||||
let mut state = self.state.write().await;
|
let mut state = self.state.write().await;
|
||||||
if !state.enroll_tokens.remove(enroll_token) {
|
if !state.enroll_tokens.remove(enroll_token) {
|
||||||
|
warn!("rejecting enroll attempt with invalid or consumed token");
|
||||||
anyhow::bail!("invalid or already-used enroll token");
|
anyhow::bail!("invalid or already-used enroll token");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +77,7 @@ impl Store {
|
|||||||
state.agents.insert(agent_id.clone(), agent_token.clone());
|
state.agents.insert(agent_id.clone(), agent_token.clone());
|
||||||
drop(state);
|
drop(state);
|
||||||
self.save().await?;
|
self.save().await?;
|
||||||
|
info!(agent_id = %agent_id, "issued persistent agent credentials");
|
||||||
|
|
||||||
Ok(IssuedAgent {
|
Ok(IssuedAgent {
|
||||||
agent_id,
|
agent_id,
|
||||||
@@ -76,11 +91,13 @@ impl Store {
|
|||||||
state.enroll_tokens.insert(token.clone());
|
state.enroll_tokens.insert(token.clone());
|
||||||
drop(state);
|
drop(state);
|
||||||
self.save().await?;
|
self.save().await?;
|
||||||
|
info!("persisted new enroll token");
|
||||||
Ok(token)
|
Ok(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn reload_from_disk(&self) -> Result<()> {
|
pub async fn reload_from_disk(&self) -> Result<()> {
|
||||||
if !self.path.exists() {
|
if !self.path.exists() {
|
||||||
|
warn!(path = %self.path.display(), "reload requested but store file is missing");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +107,10 @@ impl Store {
|
|||||||
let decoded = serde_json::from_str::<PersistedState>(&raw)
|
let decoded = serde_json::from_str::<PersistedState>(&raw)
|
||||||
.with_context(|| format!("failed to decode store {}", self.path.display()))?;
|
.with_context(|| format!("failed to decode store {}", self.path.display()))?;
|
||||||
|
|
||||||
|
let enroll_tokens = decoded.enroll_tokens.len();
|
||||||
|
let agents = decoded.agents.len();
|
||||||
*self.state.write().await = decoded;
|
*self.state.write().await = decoded;
|
||||||
|
info!(path = %self.path.display(), enroll_tokens, agents, "reloaded control-plane store from disk");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +141,7 @@ impl Store {
|
|||||||
let snapshot = self.state.read().await;
|
let snapshot = self.state.read().await;
|
||||||
let body =
|
let body =
|
||||||
serde_json::to_string_pretty(&*snapshot).context("failed to serialize store state")?;
|
serde_json::to_string_pretty(&*snapshot).context("failed to serialize store state")?;
|
||||||
|
let body_len = body.len();
|
||||||
|
|
||||||
if let Some(parent) = self.path.parent() {
|
if let Some(parent) = self.path.parent() {
|
||||||
tokio::fs::create_dir_all(parent)
|
tokio::fs::create_dir_all(parent)
|
||||||
@@ -139,6 +160,13 @@ impl Store {
|
|||||||
self.path.display()
|
self.path.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
debug!(
|
||||||
|
path = %self.path.display(),
|
||||||
|
bytes = body_len,
|
||||||
|
enroll_tokens = snapshot.enroll_tokens.len(),
|
||||||
|
agents = snapshot.agents.len(),
|
||||||
|
"saved control-plane store"
|
||||||
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,85 @@
|
|||||||
|
use anyhow::{Context, Result};
|
||||||
|
use opentelemetry::trace::TracerProvider as _;
|
||||||
|
use opentelemetry::global;
|
||||||
|
use opentelemetry_otlp::WithExportConfig;
|
||||||
|
use opentelemetry_sdk::Resource;
|
||||||
|
use opentelemetry_sdk::trace::{SdkTracerProvider, Tracer};
|
||||||
use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt};
|
use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
|
||||||
pub fn init(verbose: u8) {
|
use crate::config::TelemetryConfig;
|
||||||
|
|
||||||
|
pub fn init(verbose: u8, telemetry: &TelemetryConfig) -> Result<()> {
|
||||||
let filter = EnvFilter::try_from_default_env()
|
let filter = EnvFilter::try_from_default_env()
|
||||||
.or_else(|_| EnvFilter::try_new(default_filter(verbose)))
|
.or_else(|_| EnvFilter::try_new(default_filter(verbose)))
|
||||||
.expect("static tracing filter should parse");
|
.expect("static tracing filter should parse");
|
||||||
|
|
||||||
|
let otel = build_otel_layer(telemetry)?;
|
||||||
|
|
||||||
|
if telemetry.json_logs {
|
||||||
|
if let Some(otel_layer) = otel {
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(otel_layer)
|
||||||
|
.with(filter)
|
||||||
|
.with(fmt::layer().json())
|
||||||
|
.init();
|
||||||
|
tracing::info!(endpoint = ?telemetry.otlp_endpoint, json_logs = telemetry.json_logs, "tracing initialized with otlp exporter");
|
||||||
|
} else {
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(filter)
|
||||||
|
.with(fmt::layer().json())
|
||||||
|
.init();
|
||||||
|
tracing::info!(json_logs = telemetry.json_logs, "tracing initialized without otlp exporter");
|
||||||
|
}
|
||||||
|
} else if let Some(otel_layer) = otel {
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(otel_layer)
|
||||||
|
.with(filter)
|
||||||
|
.with(fmt::layer())
|
||||||
|
.init();
|
||||||
|
tracing::info!(endpoint = ?telemetry.otlp_endpoint, json_logs = telemetry.json_logs, "tracing initialized with otlp exporter");
|
||||||
|
} else {
|
||||||
tracing_subscriber::registry()
|
tracing_subscriber::registry()
|
||||||
.with(filter)
|
.with(filter)
|
||||||
.with(fmt::layer())
|
.with(fmt::layer())
|
||||||
.init();
|
.init();
|
||||||
|
tracing::info!(json_logs = telemetry.json_logs, "tracing initialized without otlp exporter");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_otel_layer(
|
||||||
|
telemetry: &TelemetryConfig,
|
||||||
|
) -> Result<
|
||||||
|
Option<
|
||||||
|
tracing_opentelemetry::OpenTelemetryLayer<
|
||||||
|
tracing_subscriber::Registry,
|
||||||
|
Tracer,
|
||||||
|
>,
|
||||||
|
>,
|
||||||
|
> {
|
||||||
|
let Some(endpoint) = telemetry.otlp_endpoint.as_deref() else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let exporter = opentelemetry_otlp::SpanExporter::builder()
|
||||||
|
.with_tonic()
|
||||||
|
.with_endpoint(endpoint)
|
||||||
|
.build()
|
||||||
|
.context("failed to build OTLP span exporter")?;
|
||||||
|
|
||||||
|
let provider = SdkTracerProvider::builder()
|
||||||
|
.with_batch_exporter(exporter)
|
||||||
|
.with_resource(
|
||||||
|
Resource::builder_empty()
|
||||||
|
.with_service_name(telemetry.service_name.clone())
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let tracer = provider.tracer(telemetry.service_name.clone());
|
||||||
|
global::set_tracer_provider(provider);
|
||||||
|
Ok(Some(tracing_opentelemetry::layer().with_tracer(tracer)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_filter(verbose: u8) -> &'static str {
|
fn default_filter(verbose: u8) -> &'static str {
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ use axum::response::IntoResponse;
|
|||||||
use futures_util::{SinkExt, StreamExt};
|
use futures_util::{SinkExt, StreamExt};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, info_span, warn};
|
||||||
|
use uuid::Uuid;
|
||||||
use wakey_agent::protocol::{ErrorPayload, RequestId, ServerMessage};
|
use wakey_agent::protocol::{ErrorPayload, RequestId, ServerMessage};
|
||||||
|
|
||||||
use crate::runtime::{AgentReply, AppState};
|
use crate::runtime::{AgentReply, AppState};
|
||||||
@@ -38,6 +39,11 @@ pub async fn agent_ws(ws: WebSocketUpgrade, State(state): State<AppState>) -> im
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_agent_socket(state: AppState, socket: WebSocket) {
|
async fn handle_agent_socket(state: AppState, socket: WebSocket) {
|
||||||
|
let connection_id = Uuid::new_v4().to_string();
|
||||||
|
let span = info_span!("agent_ws_connection", connection_id = %connection_id);
|
||||||
|
let _span_guard = span.enter();
|
||||||
|
info!("agent websocket upgraded");
|
||||||
|
|
||||||
let (mut write, mut read) = socket.split();
|
let (mut write, mut read) = socket.split();
|
||||||
let (tx, mut rx) = mpsc::unbounded_channel::<ServerMessage>();
|
let (tx, mut rx) = mpsc::unbounded_channel::<ServerMessage>();
|
||||||
|
|
||||||
@@ -55,6 +61,7 @@ async fn handle_agent_socket(state: AppState, socket: WebSocket) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
debug!("websocket writer loop ended");
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut authed_agent_id: Option<String> = None;
|
let mut authed_agent_id: Option<String> = None;
|
||||||
@@ -67,7 +74,10 @@ async fn handle_agent_socket(state: AppState, socket: WebSocket) {
|
|||||||
warn!(error = %err, "agent websocket receive error");
|
warn!(error = %err, "agent websocket receive error");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
None => break,
|
None => {
|
||||||
|
info!("agent websocket stream ended by peer");
|
||||||
|
break;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match msg {
|
match msg {
|
||||||
@@ -80,7 +90,10 @@ async fn handle_agent_socket(state: AppState, socket: WebSocket) {
|
|||||||
}
|
}
|
||||||
Message::Ping(_) => {}
|
Message::Ping(_) => {}
|
||||||
Message::Pong(_) => {}
|
Message::Pong(_) => {}
|
||||||
Message::Close(_) => break,
|
Message::Close(frame) => {
|
||||||
|
info!(close = ?frame, "agent websocket close frame received");
|
||||||
|
break;
|
||||||
|
}
|
||||||
Message::Binary(_) => {
|
Message::Binary(_) => {
|
||||||
debug!("ignoring unexpected binary websocket frame");
|
debug!("ignoring unexpected binary websocket frame");
|
||||||
}
|
}
|
||||||
@@ -93,6 +106,7 @@ async fn handle_agent_socket(state: AppState, socket: WebSocket) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
writer.abort();
|
writer.abort();
|
||||||
|
debug!("agent websocket connection cleanup complete");
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn process_agent_text(
|
async fn process_agent_text(
|
||||||
@@ -117,6 +131,7 @@ async fn process_agent_text(
|
|||||||
.verify_agent_token(&agent_id, &agent_token)
|
.verify_agent_token(&agent_id, &agent_token)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
warn!(agent_id = %agent_id, "agent auth rejected");
|
||||||
anyhow::bail!("agent auth rejected");
|
anyhow::bail!("agent auth rejected");
|
||||||
}
|
}
|
||||||
state
|
state
|
||||||
@@ -140,6 +155,8 @@ async fn process_agent_text(
|
|||||||
let key = request_id.as_str().to_string();
|
let key = request_id.as_str().to_string();
|
||||||
if let Some(waiter) = state.pending.lock().await.remove(&key) {
|
if let Some(waiter) = state.pending.lock().await.remove(&key) {
|
||||||
let _ = waiter.send(AgentReply::Result(result));
|
let _ = waiter.send(AgentReply::Result(result));
|
||||||
|
} else {
|
||||||
|
debug!(request_id = %key, "dropping unsolicited result from agent");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
IncomingClientMessage::Error { request_id, error } => {
|
IncomingClientMessage::Error { request_id, error } => {
|
||||||
@@ -149,6 +166,8 @@ async fn process_agent_text(
|
|||||||
let key = request_id.as_str().to_string();
|
let key = request_id.as_str().to_string();
|
||||||
if let Some(waiter) = state.pending.lock().await.remove(&key) {
|
if let Some(waiter) = state.pending.lock().await.remove(&key) {
|
||||||
let _ = waiter.send(AgentReply::Error(error));
|
let _ = waiter.send(AgentReply::Error(error));
|
||||||
|
} else {
|
||||||
|
debug!(request_id = %key, "dropping unsolicited error from agent");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user