1use std::collections::HashMap;
2use std::collections::HashSet;
3use std::sync::{Arc, OnceLock};
4use std::time::Duration;
5
6use futures_util::stream::TryStreamExt;
7use thiserror::Error;
8use tokio::sync::{Mutex, RwLock};
9use tracing::{debug, error, warn};
10
11use crate::varlink::endpoint::VarlinkEndpoint;
12use crate::varlink::machine::{ListOutput, Machine, MACHINE_SOCKET_PATH};
13use crate::varlink::machine_connector::{self, MachineConnector, MachineSocket};
14use crate::MachineStats;
15use crate::MonitordStats;
16
17type ConnectorSlot = Result<Arc<MachineConnector>, Arc<anyhow::Error>>;
20
21pub struct MachineConnection {
23 leader_pid: u32,
24 dbus: zbus::Connection,
25 varlink: Option<ConnectorSlot>,
27}
28
29pub type MachineConnections = HashMap<String, MachineConnection>;
32
33static MACHINE_VARLINK_UNAVAILABLE: OnceLock<Arc<anyhow::Error>> = OnceLock::new();
37
38#[derive(Debug, PartialEq)]
40enum CacheAction {
41 Reuse,
43 Replace,
45 Create,
47}
48
49#[derive(Error, Debug)]
50pub enum MonitordMachinesError {
51 #[error("Machines D-Bus error: {0}")]
52 ZbusError(#[from] zbus::Error),
53}
54
55fn keep_machine(
58 name: &str,
59 class: &str,
60 allowlist: &HashSet<String>,
61 blocklist: &HashSet<String>,
62) -> bool {
63 class == "container"
64 && !blocklist.contains(name)
65 && (allowlist.is_empty() || allowlist.contains(name))
66}
67
68pub fn filter_machines(
69 machines: Vec<crate::dbus::zbus_machines::ListedMachine>,
70 allowlist: &HashSet<String>,
71 blocklist: &HashSet<String>,
72) -> Vec<crate::dbus::zbus_machines::ListedMachine> {
73 machines
74 .into_iter()
75 .filter(|c| keep_machine(&c.name, &c.class, allowlist, blocklist))
76 .collect()
77}
78
79pub async fn get_machines(
84 config: &crate::config::Config,
85 dbus_cell: &crate::DbusCell,
86 host_stats: &RwLock<MachineStats>,
87) -> anyhow::Result<HashMap<String, u32>> {
88 if config.use_varlink(&[config.machines.varlink]) {
89 let endpoint = VarlinkEndpoint::from(MACHINE_SOCKET_PATH);
90 match get_machines_varlink(endpoint, config).await {
91 Ok(machines) => {
92 host_stats.write().await.varlink_usage.machines =
93 Some(crate::CollectorTransport::Varlink);
94 return Ok(machines);
95 }
96 Err(err) => {
97 crate::varlink_fallback::report_varlink_failure(
98 config.varlink.no_fallback,
99 "machines",
100 "D-Bus",
101 err,
102 )?;
103 }
104 }
105 }
106 host_stats.write().await.varlink_usage.machines = Some(crate::CollectorTransport::Dbus);
109 let connection = crate::dbus_connection(dbus_cell, config.monitord.dbus_timeout).await?;
110 Ok(get_machines_dbus(&connection, config).await?)
111}
112
113async fn get_machines_varlink(
119 endpoint: VarlinkEndpoint,
120 config: &crate::config::Config,
121) -> anyhow::Result<HashMap<String, u32>> {
122 let listed = tokio::task::spawn_blocking(move || {
123 let rt = tokio::runtime::Builder::new_current_thread()
124 .enable_all()
125 .build()?;
126 rt.block_on(async move {
127 let mut conn = endpoint.connect().await?;
128 let stream = conn.list().await?;
129 futures_util::pin_mut!(stream);
130 let mut listed = Vec::new();
131 while let Some(reply) = stream.try_next().await? {
132 listed.push(
133 reply.map_err(|e| anyhow::anyhow!("io.systemd.Machine.List error: {}", e))?,
134 );
135 }
136 anyhow::Ok(listed)
137 })
138 })
139 .await??;
140 Ok(filter_listed_machines(
141 listed,
142 &config.machines.allowlist,
143 &config.machines.blocklist,
144 ))
145}
146
147fn filter_listed_machines(
149 listed: Vec<ListOutput>,
150 allowlist: &HashSet<String>,
151 blocklist: &HashSet<String>,
152) -> HashMap<String, u32> {
153 listed
154 .into_iter()
155 .filter(|m| keep_machine(&m.name, &m.class, allowlist, blocklist))
156 .filter_map(|m| match m.leader.and_then(|leader| leader.pid) {
157 Some(pid) => Some((m.name, pid)),
158 None => {
159 warn!("Machine {} has no leader PID, skipping", m.name);
160 None
161 }
162 })
163 .collect()
164}
165
166async fn get_machines_dbus(
167 connection: &zbus::Connection,
168 config: &crate::config::Config,
169) -> Result<HashMap<String, u32>, MonitordMachinesError> {
170 let c = crate::dbus::zbus_machines::ManagerProxy::builder(connection)
171 .cache_properties(zbus::proxy::CacheProperties::No)
172 .build()
173 .await?;
174 let mut results = HashMap::<String, u32>::new();
175
176 let machines = c.list_machines().await?;
177
178 for machine in filter_machines(
179 machines,
180 &config.machines.allowlist,
181 &config.machines.blocklist,
182 ) {
183 let m = c.get_machine(&machine.name).await?;
184 let leader_pid = m.leader().await?;
185 results.insert(machine.name, leader_pid);
186 }
187
188 Ok(results)
189}
190
191fn decide_cache_action(cached_pid: Option<u32>, leader_pid: u32) -> CacheAction {
193 match cached_pid {
194 Some(pid) if pid == leader_pid => CacheAction::Reuse,
195 Some(_) => CacheAction::Replace,
196 None => CacheAction::Create,
197 }
198}
199
200async fn evict_stale_connections(
202 cached_connections: &Mutex<MachineConnections>,
203 current_machines: &HashMap<String, u32>,
204) {
205 let mut cache = cached_connections.lock().await;
206 cache.retain(|name, _| current_machines.contains_key(name));
207}
208
209async fn evict_failed_connection(cached_connections: &Mutex<MachineConnections>, machine: &str) {
211 debug!("Evicting cached connections for {} due to errors", machine);
212 let mut cache = cached_connections.lock().await;
213 cache.remove(machine);
214}
215
216async fn get_or_create_connection(
219 config: &crate::config::Config,
220 cached_connections: &Mutex<MachineConnections>,
221 machine: &str,
222 leader_pid: u32,
223) -> anyhow::Result<zbus::Connection> {
224 {
226 let mut cache = cached_connections.lock().await;
227 match decide_cache_action(cache.get(machine).map(|c| c.leader_pid), leader_pid) {
228 CacheAction::Reuse => {
229 debug!("Reusing cached D-Bus connection for {}", machine);
230 return Ok(cache[machine].dbus.clone());
231 }
232 CacheAction::Replace => {
233 debug!(
234 "Leader PID changed for {}, dropping stale connections",
235 machine
236 );
237 cache.remove(machine);
238 }
239 CacheAction::Create => {}
240 }
241 }
242
243 debug!("Creating new D-Bus connection for {}", machine);
245 let container_address = format!(
246 "unix:path=/proc/{}/root/run/dbus/system_bus_socket",
247 leader_pid
248 );
249 let conn = zbus::connection::Builder::address(container_address.as_str())
250 .method_timeout(std::time::Duration::from_secs(config.monitord.dbus_timeout))
251 .build()
252 .await?;
253
254 {
256 let mut cache = cached_connections.lock().await;
257 cache.insert(
258 machine.to_string(),
259 MachineConnection {
260 leader_pid,
261 dbus: conn.clone(),
262 varlink: None,
263 },
264 );
265 }
266
267 Ok(conn)
268}
269
270async fn machine_varlink_connector(
277 config: &crate::config::Config,
278 cached_connections: &Mutex<MachineConnections>,
279 machine: &str,
280 leader_pid: u32,
281) -> Option<ConnectorSlot> {
282 if !config.use_varlink(&[config.machines.varlink]) {
283 return None;
284 }
285 if let Some(err) = MACHINE_VARLINK_UNAVAILABLE.get() {
286 return config.varlink.no_fallback.then(|| Err(Arc::clone(err)));
287 }
288 {
289 let cache = cached_connections.lock().await;
290 match cache
291 .get(machine)
292 .filter(|c| c.leader_pid == leader_pid)
293 .and_then(|c| c.varlink.clone())
294 {
295 Some(Ok(connector)) if !connector.is_alive() => {
296 debug!("Varlink connector for {} exited, respawning", machine);
297 }
298 Some(slot) => return Some(slot),
299 None => {}
300 }
301 }
302
303 let timeout = Duration::from_secs(config.monitord.dbus_timeout);
304 let slot: ConnectorSlot =
305 match tokio::task::spawn_blocking(move || MachineConnector::spawn(leader_pid, timeout))
306 .await
307 {
308 Ok(Ok(connector)) => {
309 debug!(
310 "Spawned varlink connector for machine {} (leader {})",
311 machine, leader_pid
312 );
313 Ok(Arc::new(connector))
314 }
315 Ok(Err(err)) => Err(Arc::new(err.into())),
316 Err(err) => Err(Arc::new(err.into())),
317 };
318
319 if let Err(err) = &slot {
320 if machine_connector::is_permission_denied(err) {
321 if MACHINE_VARLINK_UNAVAILABLE.set(Arc::clone(err)).is_ok() {
322 warn!("{:#}; collecting machines over D-Bus instead", err);
323 }
324 return config.varlink.no_fallback.then_some(slot);
325 }
326 }
327
328 let mut cache = cached_connections.lock().await;
329 if let Some(entry) = cache
330 .get_mut(machine)
331 .filter(|c| c.leader_pid == leader_pid)
332 {
333 entry.varlink = Some(slot.clone());
334 }
335 Some(slot)
336}
337
338fn machine_endpoint(
340 connector: &Option<ConnectorSlot>,
341 socket: MachineSocket,
342) -> Option<anyhow::Result<VarlinkEndpoint>> {
343 connector.as_ref().map(|slot| match slot {
344 Ok(connector) => Ok(VarlinkEndpoint::Machine {
345 connector: Arc::clone(connector),
346 socket,
347 }),
348 Err(err) => Err(anyhow::anyhow!("{:#}", err)),
349 })
350}
351
352pub async fn update_machines_stats(
353 config: Arc<crate::config::Config>,
354 dbus_cell: crate::DbusCell,
355 host_stats: Arc<RwLock<MachineStats>>,
356 locked_monitord_stats: Arc<RwLock<MonitordStats>>,
357 cached_connections: Arc<Mutex<MachineConnections>>,
358) -> anyhow::Result<()> {
359 let locked_machine_stats: Arc<RwLock<MachineStats>> =
360 Arc::new(RwLock::new(MachineStats::default()));
361
362 let current_machines = get_machines(&config, &dbus_cell, &host_stats).await?;
363
364 evict_stale_connections(&cached_connections, ¤t_machines).await;
365
366 for (machine, leader_pid) in current_machines.into_iter() {
367 debug!(
368 "Collecting container: machine: {} leader_pid: {}",
369 machine, leader_pid
370 );
371
372 let sdc = match get_or_create_connection(&config, &cached_connections, &machine, leader_pid)
373 .await
374 {
375 Ok(conn) => conn,
376 Err(e) => {
377 error!("Failed to connect to container {}: {:?}", machine, e);
378 continue;
379 }
380 };
381
382 let connector =
383 machine_varlink_connector(&config, &cached_connections, &machine, leader_pid).await;
384
385 let mut join_set = tokio::task::JoinSet::new();
386
387 if config.pid1.enabled {
388 join_set.spawn(crate::pid1::update_pid1_stats(
389 leader_pid as i32,
390 locked_machine_stats.clone(),
391 ));
392 }
393
394 if config.networkd.enabled {
395 let config_clone = Arc::clone(&config);
396 let stats_clone = locked_machine_stats.clone();
397 let machine_name = machine.clone();
398 let no_fallback = config_clone.varlink.no_fallback;
399 let endpoint = config_clone
400 .use_varlink(&[config_clone.networkd.varlink])
401 .then(|| machine_endpoint(&connector, MachineSocket::Network))
402 .flatten();
403 join_set.spawn(async move {
404 if let Some(endpoint) = endpoint {
405 let result = match endpoint {
406 Ok(endpoint) => {
407 crate::varlink_networkd::get_networkd_state(&endpoint).await
408 }
409 Err(err) => Err(err),
410 };
411 match result {
412 Ok(networkd_stats) => {
413 let mut machine_stats = stats_clone.write().await;
414 machine_stats.networkd = networkd_stats;
415 machine_stats.varlink_usage.networkd =
416 Some(crate::CollectorTransport::Varlink);
417 return Ok(());
418 }
419 Err(err) => {
420 crate::varlink_fallback::report_varlink_failure(
421 no_fallback,
422 &format!("container {machine_name} networkd"),
423 "file-based",
424 err,
425 )?;
426 }
427 }
428 }
429 stats_clone.write().await.varlink_usage.networkd =
430 Some(crate::CollectorTransport::Dbus);
431 let container_root = format!("/proc/{leader_pid}/root");
439 let container_sysfs = std::path::PathBuf::from(format!("{container_root}/sys"));
440 let container_links = std::path::PathBuf::from(format!(
441 "{container_root}{}",
442 config_clone.networkd.link_state_dir.display()
443 ));
444 crate::networkd::update_networkd_stats(
445 container_links,
446 None,
447 container_sysfs,
448 None,
449 stats_clone,
450 )
451 .await
452 });
453 }
454
455 let manager_describe = config
458 .use_varlink(&[config.system_state.varlink])
459 .then(|| machine_endpoint(&connector, MachineSocket::Manager))
460 .flatten()
461 .map(|endpoint| endpoint.map(crate::varlink_system::shared_describe));
462
463 if config.system_state.enabled {
464 let sdc_clone = sdc.clone();
465 let stats_clone = locked_machine_stats.clone();
466 let machine_name = machine.clone();
467 let no_fallback = config.varlink.no_fallback;
468 let describe = clone_describe(&manager_describe);
469 join_set.spawn(async move {
470 if let Some(describe) = describe {
471 let result = match describe {
472 Ok(describe) => {
473 crate::varlink_system::update_system_stats(
474 describe,
475 stats_clone.clone(),
476 )
477 .await
478 }
479 Err(err) => Err(err),
480 };
481 match result {
482 Ok(()) => {
483 stats_clone.write().await.varlink_usage.system_state =
484 Some(crate::CollectorTransport::Varlink);
485 return Ok(());
486 }
487 Err(err) => {
488 crate::varlink_fallback::report_varlink_failure(
489 no_fallback,
490 &format!("container {machine_name} system state"),
491 "D-Bus",
492 err,
493 )?;
494 }
495 }
496 }
497 stats_clone.write().await.varlink_usage.system_state =
498 Some(crate::CollectorTransport::Dbus);
499 crate::system::update_system_stats(sdc_clone, stats_clone.clone()).await
500 });
501 }
502
503 {
504 let sdc_clone = sdc.clone();
505 let stats_clone = locked_machine_stats.clone();
506 let machine_name = machine.clone();
507 let no_fallback = config.varlink.no_fallback;
508 let describe = clone_describe(&manager_describe);
509 join_set.spawn(async move {
510 if let Some(describe) = describe {
511 let result = match describe {
512 Ok(describe) => {
513 crate::varlink_system::update_version(describe, stats_clone.clone())
514 .await
515 }
516 Err(err) => Err(err),
517 };
518 match result {
519 Ok(()) => {
520 stats_clone.write().await.varlink_usage.version =
521 Some(crate::CollectorTransport::Varlink);
522 return Ok(());
523 }
524 Err(err) => {
525 crate::varlink_fallback::report_varlink_failure(
526 no_fallback,
527 &format!("container {machine_name} version"),
528 "D-Bus",
529 err,
530 )?;
531 }
532 }
533 }
534 stats_clone.write().await.varlink_usage.version =
535 Some(crate::CollectorTransport::Dbus);
536 crate::system::update_version(sdc_clone, stats_clone.clone()).await
537 });
538 }
539
540 if config.units.enabled {
541 let config_clone = Arc::clone(&config);
542 let sdc_clone = sdc.clone();
543 let stats_clone = locked_machine_stats.clone();
544 let no_fallback = config_clone.varlink.no_fallback;
545 let machine_name = machine.clone();
546 let container_root = format!("/proc/{}/root", leader_pid);
547 let endpoints = config
548 .use_varlink(&[config.units.varlink])
549 .then(|| {
550 let metrics = machine_endpoint(&connector, MachineSocket::Metrics)?;
551 let manager = machine_endpoint(&connector, MachineSocket::Manager)?;
552 Some(metrics.and_then(|metrics| manager.map(|manager| (metrics, manager))))
553 })
554 .flatten();
555 join_set.spawn(async move {
556 if let Some(endpoints) = endpoints {
557 match collect_container_units_varlink(
558 &config_clone,
559 &stats_clone,
560 endpoints,
561 &container_root,
562 )
563 .await
564 {
565 Ok(()) => {
566 stats_clone.write().await.varlink_usage.units =
567 Some(crate::CollectorTransport::Varlink);
568 return Ok(());
569 }
570 Err(err) => {
571 crate::varlink_fallback::report_varlink_failure(
572 no_fallback,
573 &format!("container {machine_name} units"),
574 "D-Bus",
575 err,
576 )?;
577 }
578 }
579 }
580 stats_clone.write().await.varlink_usage.units =
584 Some(crate::CollectorTransport::Dbus);
585 crate::units::update_unit_stats(
586 config_clone,
587 sdc_clone,
588 stats_clone,
589 container_root,
590 )
591 .await
592 });
593 }
594
595 if config.dbus_stats.enabled {
596 join_set.spawn(crate::dbus_stats::update_machine_dbus_stats(
597 Arc::clone(&config),
598 sdc.clone(),
599 locked_machine_stats.clone(),
600 ));
601 }
602
603 let mut had_error = false;
604 while let Some(res) = join_set.join_next().await {
605 match res {
606 Ok(r) => match r {
607 Ok(_) => (),
608 Err(e) => {
609 had_error = true;
610 error!(
611 "Collection specific failure (container {}): {:?}",
612 machine, e
613 );
614 }
615 },
616 Err(e) => {
617 had_error = true;
618 error!("Join error (container {}): {:?}", machine, e);
619 }
620 }
621 }
622
623 if had_error {
624 evict_failed_connection(&cached_connections, &machine).await;
625 }
626
627 {
628 let mut monitord_stats = locked_monitord_stats.write().await;
629 let machine_stats = locked_machine_stats.read().await;
630 monitord_stats
631 .machines
632 .insert(machine, machine_stats.clone());
633 }
634 }
635
636 Ok(())
637}
638
639type MachineDescribe = Option<anyhow::Result<crate::varlink_system::SharedDescribe>>;
640
641fn clone_describe(describe: &MachineDescribe) -> MachineDescribe {
644 describe.as_ref().map(|describe| match describe {
645 Ok(describe) => Ok(describe.clone()),
646 Err(err) => Err(anyhow::anyhow!("{:#}", err)),
647 })
648}
649
650async fn collect_container_units_varlink(
656 config: &Arc<crate::config::Config>,
657 stats: &Arc<RwLock<MachineStats>>,
658 endpoints: anyhow::Result<(VarlinkEndpoint, VarlinkEndpoint)>,
659 container_root: &str,
660) -> anyhow::Result<()> {
661 let (metrics, manager) = endpoints?;
662 let discovery = crate::varlink_units::update_unit_stats(
663 Arc::clone(config),
664 stats.clone(),
665 crate::varlink_units::shared_metrics(metrics),
666 )
667 .await?;
668 crate::varlink_units::apply_unit_details(&manager, stats, config, container_root, &discovery)
669 .await?;
670 if config.units.unit_files {
671 let unit_files = crate::units::collect_unit_files_stats(container_root).await;
672 stats.write().await.units.unit_files = unit_files;
673 }
674 Ok(())
675}
676
677#[cfg(test)]
678mod tests {
679 use std::collections::HashSet;
680 use zbus::OwnedObjectPath;
681
682 use super::{decide_cache_action, CacheAction};
683
684 #[test]
685 fn test_filter_machines() {
686 let machines = vec![
687 crate::dbus::zbus_machines::ListedMachine {
688 name: "foo".to_string(),
689 class: "container".to_string(),
690 service: "".to_string(),
691 path: OwnedObjectPath::try_from("/sample/object").unwrap(),
692 },
693 crate::dbus::zbus_machines::ListedMachine {
694 name: "bar".to_string(),
695 class: "container".to_string(),
696 service: "".to_string(),
697 path: OwnedObjectPath::try_from("/sample/object").unwrap(),
698 },
699 crate::dbus::zbus_machines::ListedMachine {
700 name: "baz".to_string(),
701 class: "container".to_string(),
702 service: "".to_string(),
703 path: OwnedObjectPath::try_from("/sample/object").unwrap(),
704 },
705 ];
706 let allowlist = HashSet::from(["foo".to_string(), "baz".to_string()]);
707 let blocklist = HashSet::from(["bar".to_string()]);
708
709 let filtered = super::filter_machines(machines, &allowlist, &blocklist);
710
711 assert_eq!(filtered.len(), 2);
712 assert_eq!(filtered[0].name, "foo");
713 assert_eq!(filtered[1].name, "baz");
714 }
715
716 #[test]
717 fn test_filter_listed_machines() {
718 let listed: Vec<crate::varlink::machine::ListOutput> = serde_json::from_str(
719 r#"[
720 {"name":"foo","class":"container","leader":{"pid":10}},
721 {"name":"bar","class":"container","leader":{"pid":11}},
722 {"name":"baz","class":"container","leader":{"pid":12}},
723 {"name":"vm0","class":"vm","leader":{"pid":13}},
724 {"name":".host","class":"host","leader":{"pid":1}},
725 {"name":"noleader","class":"container"}
726 ]"#,
727 )
728 .unwrap();
729 let blocklist = HashSet::from(["bar".to_string()]);
730
731 let all = super::filter_listed_machines(listed.clone(), &HashSet::new(), &blocklist);
732 assert_eq!(
733 all,
734 std::collections::HashMap::from([("foo".to_string(), 10), ("baz".to_string(), 12)])
735 );
736
737 let allowlist = HashSet::from(["baz".to_string(), "vm0".to_string()]);
738 let allowed = super::filter_listed_machines(listed, &allowlist, &blocklist);
739 assert_eq!(
740 allowed,
741 std::collections::HashMap::from([("baz".to_string(), 12)])
742 );
743 }
744
745 #[test]
746 fn test_decide_cache_action_reuse_on_same_pid() {
747 assert_eq!(decide_cache_action(Some(42), 42), CacheAction::Reuse);
748 }
749
750 #[test]
751 fn test_decide_cache_action_replace_on_pid_change() {
752 assert_eq!(decide_cache_action(Some(42), 99), CacheAction::Replace);
753 }
754
755 #[test]
756 fn test_decide_cache_action_create_on_miss() {
757 assert_eq!(decide_cache_action(None, 42), CacheAction::Create);
758 }
759}