1use std::time::Duration;
12
13use anyhow::{Result, anyhow};
14use zenoh::Session;
15use zenoh::query::{ConsolidationMode, QueryTarget};
16
17#[derive(Debug, Clone)]
19pub struct AdminEntry {
20 pub key: String,
21 pub value: serde_json::Value,
22}
23
24pub async fn admin_get(
27 session: &Session,
28 selector: &str,
29 timeout: Duration,
30) -> Result<Vec<AdminEntry>> {
31 let replies = session
32 .get(selector)
33 .target(QueryTarget::All)
34 .consolidation(ConsolidationMode::None)
35 .timeout(timeout)
36 .await
37 .map_err(|e| anyhow!("admin get {selector}: {e}"))?;
38 let mut out = Vec::new();
39 while let Ok(reply) = replies.recv_async().await {
40 let Ok(sample) = reply.result() else { continue };
41 let bytes = sample.payload().to_bytes();
42 let value = serde_json::from_slice(&bytes).unwrap_or_else(|_| {
43 serde_json::Value::String(String::from_utf8_lossy(&bytes).to_string())
44 });
45 out.push(AdminEntry {
46 key: sample.key_expr().as_str().to_string(),
47 value,
48 });
49 }
50 out.sort_by(|a, b| a.key.cmp(&b.key));
51 Ok(out)
52}
53
54#[derive(Debug, Clone, serde::Serialize)]
56pub struct RouterInfo {
57 pub zid: String,
58 #[serde(skip_serializing_if = "Option::is_none")]
59 pub version: Option<String>,
60 #[serde(skip_serializing_if = "Vec::is_empty")]
61 pub locators: Vec<String>,
62 pub raw: serde_json::Value,
64}
65
66pub async fn routers(session: &Session, timeout: Duration) -> Result<Vec<RouterInfo>> {
69 let entries = admin_get(session, "@/*/router", timeout).await?;
70 Ok(entries
71 .into_iter()
72 .map(|e| {
73 let zid = e
74 .value
75 .get("zid")
76 .and_then(|v| v.as_str())
77 .map(str::to_string)
78 .unwrap_or_else(|| {
79 e.key.split('/').nth(1).unwrap_or("?").to_string()
81 });
82 let version = e
83 .value
84 .get("version")
85 .and_then(|v| v.as_str())
86 .map(str::to_string);
87 let locators = e
88 .value
89 .get("locators")
90 .and_then(|v| v.as_array())
91 .map(|a| {
92 a.iter()
93 .filter_map(|l| l.as_str().map(str::to_string))
94 .collect()
95 })
96 .unwrap_or_default();
97 RouterInfo {
98 zid,
99 version,
100 locators,
101 raw: e.value,
102 }
103 })
104 .collect())
105}
106
107#[derive(Debug, Clone, serde::Serialize)]
109pub struct StorageInfo {
110 pub zid: String,
111 pub name: String,
112 #[serde(skip_serializing_if = "Option::is_none")]
114 pub key_expr: Option<String>,
115 pub raw: serde_json::Value,
117}
118
119pub fn storage_from_admin_entry(key: &str, value: &serde_json::Value) -> Option<StorageInfo> {
124 let chunks: Vec<&str> = key.split('/').collect();
125 let storages_pos = chunks.iter().position(|c| *c == "storages")?;
126 if chunks.get(storages_pos.checked_sub(1)?) != Some(&"storage_manager") {
128 return None;
129 }
130 let name = chunks.get(storages_pos + 1)?;
131 let zid = chunks.get(1).unwrap_or(&"?");
132 let key_expr = value
133 .get("key_expr")
134 .and_then(|v| v.as_str())
135 .map(str::to_string);
136 Some(StorageInfo {
137 zid: (*zid).to_string(),
138 name: (*name).to_string(),
139 key_expr,
140 raw: value.clone(),
141 })
142}
143
144pub async fn storages(session: &Session, timeout: Duration) -> Result<Vec<StorageInfo>> {
147 let entries = admin_get(
148 session,
149 "@/*/router/**/storage_manager/storages/**",
150 timeout,
151 )
152 .await?;
153 let mut out: Vec<StorageInfo> = entries
154 .iter()
155 .filter_map(|e| storage_from_admin_entry(&e.key, &e.value))
156 .collect();
157 out.sort_by(|a, b| (&a.zid, &a.name).cmp(&(&b.zid, &b.name)));
159 out.dedup_by(|a, b| {
160 if a.zid == b.zid && a.name == b.name {
161 if b.key_expr.is_none() {
163 b.key_expr = a.key_expr.take();
164 }
165 true
166 } else {
167 false
168 }
169 });
170 Ok(out)
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
175#[serde(tag = "coverage", content = "storage")]
176pub enum Coverage {
177 Covered(String),
179 Partial(String),
181 Uncovered,
185}
186
187#[derive(Debug, Clone, serde::Serialize)]
188pub struct CoverageRow {
189 pub producer: String,
190 pub path: String,
191 #[serde(skip_serializing_if = "Option::is_none")]
192 pub ttl_s: Option<i64>,
193 #[serde(flatten)]
194 pub coverage: Coverage,
195}
196
197pub fn state_coverage(
201 slices: &crate::registry::SliceSet,
202 base: &str,
203 storages: &[StorageInfo],
204) -> Vec<CoverageRow> {
205 use zenoh::key_expr::keyexpr;
206 let storage_kes: Vec<(&StorageInfo, &keyexpr)> = storages
207 .iter()
208 .filter_map(|s| {
209 let ke = s.key_expr.as_deref()?;
210 keyexpr::new(ke).ok().map(|ke| (s, ke))
211 })
212 .collect();
213 let mut rows = Vec::new();
214 for slice in slices.slices() {
215 for subject in &slice.subjects {
216 if subject.class != "state" {
217 continue;
218 }
219 let Ok(pattern) = zenkey::pattern::SubjectPattern::parse(&subject.path) else {
220 continue;
221 };
222 let selector = match &slice.service_origin {
226 Some(origin) => zenkey::grammar::with_base(
227 base,
228 format!("v1/{origin}/state/{}", pattern.selector_tail()),
229 ),
230 None => zenkey::grammar::with_base(
231 base,
232 format!("v1/*/state/{}/{}", slice.name, pattern.selector_tail()),
233 ),
234 };
235 let Ok(family) = keyexpr::new(selector.as_str()) else {
236 continue;
237 };
238 let mut coverage = Coverage::Uncovered;
239 for (info, ke) in &storage_kes {
240 if ke.includes(family) {
241 coverage = Coverage::Covered(format!("{}@{}", info.name, info.zid));
242 break;
243 }
244 if ke.intersects(family) && coverage == Coverage::Uncovered {
245 coverage = Coverage::Partial(format!("{}@{}", info.name, info.zid));
246 }
247 }
248 rows.push(CoverageRow {
249 producer: slice.name.clone(),
250 path: subject.path.clone(),
251 ttl_s: subject.ttl_s,
252 coverage,
253 });
254 }
255 }
256 rows
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 #[test]
264 fn storage_extraction_tolerates_layouts() {
265 let v = serde_json::json!({"key_expr": "zs/v1/*/state/**", "volume": "fs"});
267 let s = storage_from_admin_entry(
268 "@/abc123/router/config/plugins/storage_manager/storages/latest",
269 &v,
270 )
271 .unwrap();
272 assert_eq!(s.zid, "abc123");
273 assert_eq!(s.name, "latest");
274 assert_eq!(s.key_expr.as_deref(), Some("zs/v1/*/state/**"));
275 let s = storage_from_admin_entry(
277 "@/abc123/router/status/plugins/storage_manager/storages/latest/info",
278 &serde_json::json!("ok"),
279 )
280 .unwrap();
281 assert_eq!(s.name, "latest");
282 assert!(s.key_expr.is_none());
283 assert!(
285 storage_from_admin_entry(
286 "@/abc123/router/config/plugins/storage_manager/volumes/fs",
287 &serde_json::json!({}),
288 )
289 .is_none()
290 );
291 }
292
293 fn slices_with_state() -> crate::registry::SliceSet {
294 let toml = r#"
295 [registry]
296 version = "1.0"
297 app = "t"
298 convention = 1
299 [producer]
300 name = "tc"
301 [[subject]]
302 path = "health"
303 class = "state"
304 type = "Health"
305 ttl_s = 60
306 [[subject]]
307 path = "config/{iface}"
308 class = "state"
309 type = "Config"
310 ttl_s = 120
311 [[subject]]
312 path = "bandwidth"
313 class = "telemetry"
314 type = "Point"
315 "#;
316 crate::registry::SliceSet::from_toml_for_tests(toml)
317 }
318
319 fn storage(name: &str, key_expr: &str) -> StorageInfo {
320 StorageInfo {
321 zid: "z1".into(),
322 name: name.into(),
323 key_expr: Some(key_expr.into()),
324 raw: serde_json::Value::Null,
325 }
326 }
327
328 #[test]
329 fn coverage_judges_covered_partial_uncovered() {
330 let slices = slices_with_state();
331 let rows = state_coverage(&slices, "zs", &[storage("latest", "zs/v1/*/state/**")]);
333 assert_eq!(rows.len(), 2);
334 assert!(
335 rows.iter()
336 .all(|r| matches!(r.coverage, Coverage::Covered(_)))
337 );
338
339 let rows = state_coverage(
341 &slices,
342 "zs",
343 &[storage("one", "zs/v1/*/state/tc/config/eth0")],
344 );
345 let health = rows.iter().find(|r| r.path == "health").unwrap();
346 assert_eq!(health.coverage, Coverage::Uncovered);
347 let config = rows.iter().find(|r| r.path == "config/{iface}").unwrap();
348 assert!(matches!(config.coverage, Coverage::Partial(_)));
349
350 let rows = state_coverage(&slices, "zs", &[]);
352 assert!(rows.iter().all(|r| r.coverage == Coverage::Uncovered));
353
354 let rows = state_coverage(&slices, "", &[storage("latest", "v1/*/state/**")]);
357 assert_eq!(rows.len(), 2);
358 assert!(
359 rows.iter()
360 .all(|r| matches!(r.coverage, Coverage::Covered(_)))
361 );
362 }
363}