Skip to main content

monitord/
machines.rs

1use std::collections::HashMap;
2use std::collections::HashSet;
3use std::sync::Arc;
4
5use thiserror::Error;
6use tokio::sync::{Mutex, RwLock};
7use tracing::{debug, error, warn};
8
9use crate::MachineStats;
10use crate::MonitordStats;
11
12/// Cached D-Bus connections to containers, keyed by machine name.
13/// The u32 is the leader PID at the time the connection was established.
14/// A connection is only reused if the current leader PID matches.
15pub type MachineConnections = HashMap<String, (u32, zbus::Connection)>;
16
17/// What action to take for a machine's cached connection.
18#[derive(Debug, PartialEq)]
19enum CacheAction {
20    /// Cached connection exists with matching leader PID — reuse it.
21    Reuse,
22    /// Cached connection exists but leader PID changed — drop old and create new.
23    Replace,
24    /// No cached connection — create new.
25    Create,
26}
27
28#[derive(Error, Debug)]
29pub enum MonitordMachinesError {
30    #[error("Machines D-Bus error: {0}")]
31    ZbusError(#[from] zbus::Error),
32}
33
34pub fn filter_machines(
35    machines: Vec<crate::dbus::zbus_machines::ListedMachine>,
36    allowlist: &HashSet<String>,
37    blocklist: &HashSet<String>,
38) -> Vec<crate::dbus::zbus_machines::ListedMachine> {
39    machines
40        .into_iter()
41        .filter(|c| c.class == "container")
42        .filter(|c| !blocklist.contains(&c.name))
43        .filter(|c| allowlist.is_empty() || allowlist.contains(&c.name))
44        .collect()
45}
46
47pub async fn get_machines(
48    connection: &zbus::Connection,
49    config: &crate::config::Config,
50) -> Result<HashMap<String, u32>, MonitordMachinesError> {
51    let c = crate::dbus::zbus_machines::ManagerProxy::builder(connection)
52        .cache_properties(zbus::proxy::CacheProperties::No)
53        .build()
54        .await?;
55    let mut results = HashMap::<String, u32>::new();
56
57    let machines = c.list_machines().await?;
58
59    for machine in filter_machines(
60        machines,
61        &config.machines.allowlist,
62        &config.machines.blocklist,
63    ) {
64        let m = c.get_machine(&machine.name).await?;
65        let leader_pid = m.leader().await?;
66        results.insert(machine.name, leader_pid);
67    }
68
69    Ok(results)
70}
71
72/// Determine the cache action for a machine based on its cached and current leader PID.
73fn decide_cache_action(cached_pid: Option<u32>, leader_pid: u32) -> CacheAction {
74    match cached_pid {
75        Some(pid) if pid == leader_pid => CacheAction::Reuse,
76        Some(_) => CacheAction::Replace,
77        None => CacheAction::Create,
78    }
79}
80
81/// Remove cached connections for machines that no longer exist.
82async fn evict_stale_connections(
83    cached_connections: &Mutex<MachineConnections>,
84    current_machines: &HashMap<String, u32>,
85) {
86    let mut cache = cached_connections.lock().await;
87    cache.retain(|name, _| current_machines.contains_key(name));
88}
89
90/// Evict a cached connection for a machine that experienced errors.
91async fn evict_failed_connection(cached_connections: &Mutex<MachineConnections>, machine: &str) {
92    debug!(
93        "Evicting cached D-Bus connection for {} due to errors",
94        machine
95    );
96    let mut cache = cached_connections.lock().await;
97    cache.remove(machine);
98}
99
100/// Return a cached D-Bus connection if one exists for the same leader PID,
101/// otherwise create a new connection to the container's system bus.
102async fn get_or_create_connection(
103    config: &crate::config::Config,
104    cached_connections: &Mutex<MachineConnections>,
105    machine: &str,
106    leader_pid: u32,
107) -> anyhow::Result<zbus::Connection> {
108    // Check cache and return if hit; drop the lock before any async work
109    {
110        let mut cache = cached_connections.lock().await;
111        match decide_cache_action(cache.get(machine).map(|(pid, _)| *pid), leader_pid) {
112            CacheAction::Reuse => {
113                debug!("Reusing cached D-Bus connection for {}", machine);
114                let (_, conn) = cache.get(machine).unwrap();
115                return Ok(conn.clone());
116            }
117            CacheAction::Replace => {
118                debug!(
119                    "Leader PID changed for {}, dropping stale connection",
120                    machine
121                );
122                cache.remove(machine);
123            }
124            CacheAction::Create => {}
125        }
126    }
127
128    // Build connection without holding the lock
129    debug!("Creating new D-Bus connection for {}", machine);
130    let container_address = format!(
131        "unix:path=/proc/{}/root/run/dbus/system_bus_socket",
132        leader_pid
133    );
134    let conn = zbus::connection::Builder::address(container_address.as_str())?
135        .method_timeout(std::time::Duration::from_secs(config.monitord.dbus_timeout))
136        .build()
137        .await?;
138
139    // Re-lock to insert
140    {
141        let mut cache = cached_connections.lock().await;
142        cache.insert(machine.to_string(), (leader_pid, conn.clone()));
143    }
144
145    Ok(conn)
146}
147
148pub async fn update_machines_stats(
149    config: Arc<crate::config::Config>,
150    connection: zbus::Connection,
151    locked_monitord_stats: Arc<RwLock<MonitordStats>>,
152    cached_connections: Arc<Mutex<MachineConnections>>,
153) -> anyhow::Result<()> {
154    let locked_machine_stats: Arc<RwLock<MachineStats>> =
155        Arc::new(RwLock::new(MachineStats::default()));
156
157    let current_machines = get_machines(&connection, &config).await?;
158
159    evict_stale_connections(&cached_connections, &current_machines).await;
160
161    for (machine, leader_pid) in current_machines.into_iter() {
162        debug!(
163            "Collecting container: machine: {} leader_pid: {}",
164            machine, leader_pid
165        );
166
167        let sdc = match get_or_create_connection(&config, &cached_connections, &machine, leader_pid)
168            .await
169        {
170            Ok(conn) => conn,
171            Err(e) => {
172                error!("Failed to connect to container {}: {:?}", machine, e);
173                continue;
174            }
175        };
176
177        let mut join_set = tokio::task::JoinSet::new();
178
179        if config.pid1.enabled {
180            join_set.spawn(crate::pid1::update_pid1_stats(
181                leader_pid as i32,
182                locked_machine_stats.clone(),
183            ));
184        }
185
186        if config.networkd.enabled {
187            let config_clone = Arc::clone(&config);
188            let stats_clone = locked_machine_stats.clone();
189            let machine_name = machine.clone();
190            let no_fallback = config_clone.varlink.no_fallback;
191            join_set.spawn(async move {
192                if config_clone
193                    .use_varlink(&[config_clone.machines.varlink, config_clone.networkd.varlink])
194                {
195                    let socket_path = format!(
196                        "/proc/{}/root{}",
197                        leader_pid,
198                        crate::varlink_networkd::NETWORK_SOCKET_PATH
199                    );
200                    match crate::varlink_networkd::get_networkd_state(&socket_path).await {
201                        Ok(networkd_stats) => {
202                            let mut machine_stats = stats_clone.write().await;
203                            machine_stats.networkd = networkd_stats;
204                            machine_stats.varlink_usage.networkd =
205                                Some(crate::CollectorTransport::Varlink);
206                            return Ok(());
207                        }
208                        Err(err) => {
209                            crate::varlink_fallback::report_varlink_failure(
210                                no_fallback,
211                                &format!("container {machine_name} networkd"),
212                                "file-based",
213                                err,
214                            )?;
215                        }
216                    }
217                }
218                stats_clone.write().await.varlink_usage.networkd =
219                    Some(crate::CollectorTransport::Dbus);
220                // Same fs-root prefixing the units/cgroup collectors use:
221                // both the link state files AND the sysfs ifindex map
222                // come from inside the container, so host ifindexes are
223                // never labelled with container interface names (or vice
224                // versa). A container sharing the host netns simply sees
225                // identical trees, which is why CI never caught the old
226                // host-files/host-bus pairing being self-consistent.
227                let container_root = format!("/proc/{leader_pid}/root");
228                let container_sysfs = std::path::PathBuf::from(format!("{container_root}/sys"));
229                let container_links = std::path::PathBuf::from(format!(
230                    "{container_root}{}",
231                    config_clone.networkd.link_state_dir.display()
232                ));
233                crate::networkd::update_networkd_stats(
234                    container_links,
235                    None,
236                    container_sysfs,
237                    None,
238                    stats_clone,
239                )
240                .await
241            });
242        }
243
244        // One Describe per container, shared by its version and system state
245        // collectors, against the container's PID 1 varlink socket seen through
246        // its leader's procfs root.
247        let manager_describe = config
248            .use_varlink(&[config.machines.varlink, config.system_state.varlink])
249            .then(|| {
250                crate::varlink_system::shared_describe(format!(
251                    "/proc/{}/root{}",
252                    leader_pid,
253                    crate::varlink_system::MANAGER_SOCKET_PATH
254                ))
255            });
256
257        if config.system_state.enabled {
258            let sdc_clone = sdc.clone();
259            let stats_clone = locked_machine_stats.clone();
260            let machine_name = machine.clone();
261            let no_fallback = config.varlink.no_fallback;
262            let describe = manager_describe.clone();
263            join_set.spawn(async move {
264                if let Some(describe) = describe {
265                    match crate::varlink_system::update_system_stats(describe, stats_clone.clone())
266                        .await
267                    {
268                        Ok(()) => {
269                            stats_clone.write().await.varlink_usage.system_state =
270                                Some(crate::CollectorTransport::Varlink);
271                            return Ok(());
272                        }
273                        Err(err) => {
274                            crate::varlink_fallback::report_varlink_failure(
275                                no_fallback,
276                                &format!("container {machine_name} system state"),
277                                "D-Bus",
278                                err,
279                            )?;
280                        }
281                    }
282                }
283                stats_clone.write().await.varlink_usage.system_state =
284                    Some(crate::CollectorTransport::Dbus);
285                crate::system::update_system_stats(sdc_clone, stats_clone.clone()).await
286            });
287        }
288
289        {
290            let sdc_clone = sdc.clone();
291            let stats_clone = locked_machine_stats.clone();
292            let machine_name = machine.clone();
293            let no_fallback = config.varlink.no_fallback;
294            let describe = manager_describe.clone();
295            join_set.spawn(async move {
296                if let Some(describe) = describe {
297                    match crate::varlink_system::update_version(describe, stats_clone.clone()).await
298                    {
299                        Ok(()) => {
300                            stats_clone.write().await.varlink_usage.version =
301                                Some(crate::CollectorTransport::Varlink);
302                            return Ok(());
303                        }
304                        Err(err) => {
305                            crate::varlink_fallback::report_varlink_failure(
306                                no_fallback,
307                                &format!("container {machine_name} version"),
308                                "D-Bus",
309                                err,
310                            )?;
311                        }
312                    }
313                }
314                stats_clone.write().await.varlink_usage.version =
315                    Some(crate::CollectorTransport::Dbus);
316                crate::system::update_version(sdc_clone, stats_clone.clone()).await
317            });
318        }
319
320        if config.units.enabled {
321            if config.use_varlink(&[config.machines.varlink, config.units.varlink]) {
322                let config_clone = Arc::clone(&config);
323                let sdc_clone = sdc.clone();
324                let stats_clone = locked_machine_stats.clone();
325                let no_fallback = config_clone.varlink.no_fallback;
326                let machine_name = machine.clone();
327                let container_socket_path = format!(
328                    "/proc/{}/root{}",
329                    leader_pid,
330                    crate::varlink_units::METRICS_SOCKET_PATH
331                );
332                join_set.spawn(async move {
333                    match crate::varlink_units::update_unit_stats(
334                        Arc::clone(&config_clone),
335                        stats_clone.clone(),
336                        container_socket_path,
337                    )
338                    .await
339                    {
340                        Ok(_timer_names) => {
341                            // Containers keep the D-Bus backfill rather than the
342                            // host's io.systemd.Unit.List path: a container's
343                            // varlink sockets are unreachable from here, with
344                            // the connection accepted and then reset unless the
345                            // caller is inside the container's PID namespace
346                            // (observed, see #211). The timer names collected
347                            // above go unused for the same reason.
348                            let timer_start = std::time::Instant::now();
349                            let timer_result = crate::timer::collect_all_timers_dbus(
350                                &sdc_clone,
351                                &config_clone,
352                            )
353                            .await;
354                            let timer_elapsed_ms =
355                                timer_start.elapsed().as_secs_f64() * 1000.0;
356                            match timer_result {
357                                Ok(timer_stats) => {
358                                    let mut ms = stats_clone.write().await;
359                                    crate::timer::merge_timer_stats(
360                                        &mut ms.units,
361                                        timer_stats,
362                                        timer_elapsed_ms,
363                                    );
364                                }
365                                Err(err) => {
366                                    warn!(
367                                        "Varlink timer stats (D-Bus fallback) failed for container {}: {:?}",
368                                        machine_name, err
369                                    );
370                                    let mut ms = stats_clone.write().await;
371                                    crate::timer::record_backfill_duration(
372                                        &mut ms.units,
373                                        timer_elapsed_ms,
374                                    );
375                                }
376                            }
377                            // Service type is not exposed via varlink metrics; resolve
378                            // it over the container's D-Bus connection (same as host).
379                            crate::varlink_units::apply_oneshot_dbus_override(
380                                &sdc_clone,
381                                &stats_clone,
382                                &config_clone.units,
383                            )
384                            .await;
385                            let container_root = format!("/proc/{}/root", leader_pid);
386                            if config_clone.units.unit_files {
387                                let unit_files =
388                                    crate::units::collect_unit_files_stats(&container_root).await;
389                                let mut ms = stats_clone.write().await;
390                                ms.units.unit_files = unit_files;
391                            }
392                            stats_clone.write().await.varlink_usage.units =
393                                Some(crate::CollectorTransport::Varlink);
394                            Ok(())
395                        }
396                        Err(err) => {
397                            crate::varlink_fallback::report_varlink_failure(
398                                no_fallback,
399                                &format!("container {machine_name} units"),
400                                "D-Bus",
401                                err,
402                            )?;
403                            let container_root = format!("/proc/{}/root", leader_pid);
404                            // Set before the call (the lib.rs ordering): if
405                            // the D-Bus collection errors, the gauge still
406                            // says D-Bus rather than going stale or absent.
407                            stats_clone.write().await.varlink_usage.units =
408                                Some(crate::CollectorTransport::Dbus);
409                            crate::units::update_unit_stats(
410                                config_clone,
411                                sdc_clone,
412                                stats_clone,
413                                container_root,
414                            )
415                            .await
416                        }
417                    }
418                });
419            } else {
420                let container_root = format!("/proc/{}/root", leader_pid);
421                let config_clone = Arc::clone(&config);
422                let sdc_clone = sdc.clone();
423                let stats_clone = locked_machine_stats.clone();
424                join_set.spawn(async move {
425                    // Set before the call (the lib.rs ordering): if the
426                    // collection errors, the gauge still says D-Bus rather
427                    // than going stale or absent.
428                    stats_clone.write().await.varlink_usage.units =
429                        Some(crate::CollectorTransport::Dbus);
430                    crate::units::update_unit_stats(
431                        config_clone,
432                        sdc_clone,
433                        stats_clone,
434                        container_root,
435                    )
436                    .await
437                });
438            }
439        }
440
441        if config.dbus_stats.enabled {
442            join_set.spawn(crate::dbus_stats::update_machine_dbus_stats(
443                Arc::clone(&config),
444                sdc.clone(),
445                locked_machine_stats.clone(),
446            ));
447        }
448
449        let mut had_error = false;
450        while let Some(res) = join_set.join_next().await {
451            match res {
452                Ok(r) => match r {
453                    Ok(_) => (),
454                    Err(e) => {
455                        had_error = true;
456                        error!(
457                            "Collection specific failure (container {}): {:?}",
458                            machine, e
459                        );
460                    }
461                },
462                Err(e) => {
463                    had_error = true;
464                    error!("Join error (container {}): {:?}", machine, e);
465                }
466            }
467        }
468
469        if had_error {
470            evict_failed_connection(&cached_connections, &machine).await;
471        }
472
473        {
474            let mut monitord_stats = locked_monitord_stats.write().await;
475            let machine_stats = locked_machine_stats.read().await;
476            monitord_stats
477                .machines
478                .insert(machine, machine_stats.clone());
479        }
480    }
481
482    Ok(())
483}
484
485#[cfg(test)]
486mod tests {
487    use std::collections::HashSet;
488    use zbus::zvariant::OwnedObjectPath;
489
490    use super::{decide_cache_action, CacheAction};
491
492    #[test]
493    fn test_filter_machines() {
494        let machines = vec![
495            crate::dbus::zbus_machines::ListedMachine {
496                name: "foo".to_string(),
497                class: "container".to_string(),
498                service: "".to_string(),
499                path: OwnedObjectPath::try_from("/sample/object").unwrap(),
500            },
501            crate::dbus::zbus_machines::ListedMachine {
502                name: "bar".to_string(),
503                class: "container".to_string(),
504                service: "".to_string(),
505                path: OwnedObjectPath::try_from("/sample/object").unwrap(),
506            },
507            crate::dbus::zbus_machines::ListedMachine {
508                name: "baz".to_string(),
509                class: "container".to_string(),
510                service: "".to_string(),
511                path: OwnedObjectPath::try_from("/sample/object").unwrap(),
512            },
513        ];
514        let allowlist = HashSet::from(["foo".to_string(), "baz".to_string()]);
515        let blocklist = HashSet::from(["bar".to_string()]);
516
517        let filtered = super::filter_machines(machines, &allowlist, &blocklist);
518
519        assert_eq!(filtered.len(), 2);
520        assert_eq!(filtered[0].name, "foo");
521        assert_eq!(filtered[1].name, "baz");
522    }
523
524    #[test]
525    fn test_decide_cache_action_reuse_on_same_pid() {
526        assert_eq!(decide_cache_action(Some(42), 42), CacheAction::Reuse);
527    }
528
529    #[test]
530    fn test_decide_cache_action_replace_on_pid_change() {
531        assert_eq!(decide_cache_action(Some(42), 99), CacheAction::Replace);
532    }
533
534    #[test]
535    fn test_decide_cache_action_create_on_miss() {
536        assert_eq!(decide_cache_action(None, 42), CacheAction::Create);
537    }
538}