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