monitord/
varlink_verify.rs1use tracing::debug;
16
17use crate::varlink::metrics::ListOutput;
18use crate::varlink_units::SharedMetrics;
19
20fn collect_unit_names(metrics: &[ListOutput]) -> anyhow::Result<Vec<String>> {
34 let mut names: Vec<String> = Vec::new();
35 let mut saw_family = false;
36 let mut skipped = 0u64;
37 for metric in metrics {
38 if metric.name_suffix() != "UnitLoadState" {
39 continue;
40 }
41 saw_family = true;
42 match (metric.object(), metric.value().is_string()) {
43 (Some(unit_name), true) => names.push(unit_name.to_string()),
44 (object, is_string) => {
45 skipped += 1;
46 debug!(
47 "Skipping {} for {object:?}: non-string load state or missing object (is_string={is_string}, value={:?})",
48 metric.name(),
49 metric.value()
50 );
51 }
52 }
53 }
54 if !saw_family {
55 anyhow::bail!("metrics carry no UnitLoadState family, cannot enumerate units");
56 }
57 if skipped > 0 {
58 anyhow::bail!(
59 "{skipped} UnitLoadState metrics skipped (missing object or non-string state), cannot trust enumeration"
60 );
61 }
62 names.sort();
63 names.dedup();
64 if names.is_empty() {
65 anyhow::bail!("UnitLoadState family present but no unit names enumerated");
69 }
70 Ok(names)
71}
72
73pub async fn list_unit_names(metrics: SharedMetrics) -> anyhow::Result<Vec<String>> {
75 let metrics = crate::varlink_units::await_metrics(metrics).await?;
76 collect_unit_names(&metrics)
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 fn metric(unit: Option<&str>, load_state: serde_json::Value) -> ListOutput {
84 ListOutput {
85 name: "io.systemd.Manager.UnitLoadState".to_string(),
86 value: load_state,
87 object: unit.map(|unit| unit.to_string()),
88 fields: None,
89 }
90 }
91
92 fn loaded(unit: &str) -> ListOutput {
93 metric(Some(unit), serde_json::json!("loaded"))
94 }
95
96 #[test]
97 fn test_collects_every_object_whatever_its_load_state() {
98 let mut metrics = vec![
102 loaded("b.service"),
103 loaded("a.service"),
104 metric(Some("masked.service"), serde_json::json!("masked")),
105 metric(Some("ghost.service"), serde_json::json!("not-found")),
106 loaded("a.service"),
108 ];
109
110 metrics.push(ListOutput {
112 name: "io.systemd.Manager.UnitActiveState".to_string(),
113 value: serde_json::json!("active"),
114 object: Some("active-only.service".to_string()),
115 fields: None,
116 });
117
118 assert_eq!(
119 collect_unit_names(&metrics).expect("valid family collects"),
120 vec![
121 "a.service".to_string(),
122 "b.service".to_string(),
123 "ghost.service".to_string(),
124 "masked.service".to_string(),
125 ]
126 );
127 }
128
129 #[test]
130 fn test_skipped_family_members_reject_enumeration() {
131 for metrics in [
134 vec![
135 loaded("good.service"),
136 metric(None, serde_json::json!("loaded")),
137 ],
138 vec![
139 loaded("good.service"),
140 metric(Some("odd.service"), serde_json::json!(3)),
141 ],
142 vec![
143 loaded("good.service"),
144 metric(Some("other.service"), serde_json::json!(null)),
145 ],
146 ] {
147 let err = collect_unit_names(&metrics).expect_err("skips must bail");
148 assert!(
149 err.to_string().contains("skipped"),
150 "unexpected error: {err}"
151 );
152 }
153 }
154
155 #[test]
156 fn test_missing_family_is_rejected() {
157 let without_family = vec![ListOutput {
160 name: "io.systemd.Manager.UnitsTotal".to_string(),
161 value: serde_json::json!(295),
162 object: None,
163 fields: None,
164 }];
165 let err = collect_unit_names(&without_family).expect_err("missing family must bail");
166 assert!(
167 err.to_string().contains("no UnitLoadState family"),
168 "unexpected error: {err}"
169 );
170
171 assert!(collect_unit_names(&[loaded("foo.service")]).is_ok());
172 }
173}