Skip to main content

monitord/
system.rs

1//! # system module
2//!
3//! Handle systemd's overall "system" state. Basically says if we've successfully
4//! booted, stated all units or have been asked to stop, be offline etc.
5
6use std::convert::TryInto;
7use std::fmt;
8use std::sync::Arc;
9
10use int_enum::IntEnum;
11use serde_repr::Deserialize_repr;
12use serde_repr::Serialize_repr;
13use strum_macros::EnumIter;
14use strum_macros::EnumString;
15use thiserror::Error;
16use tokio::sync::RwLock;
17use tracing::error;
18use tracing::Instrument;
19
20use crate::MachineStats;
21
22#[derive(Error, Debug)]
23pub enum MonitordSystemError {
24    #[error("Unable to connect to DBUS via zbus: {0:#}")]
25    ZbusError(#[from] zbus::Error),
26    #[error("Version parse error: {0}")]
27    VersionParseError(String),
28    #[error("Integer parse error: {0}")]
29    IntParseError(#[from] std::num::ParseIntError),
30}
31
32/// Overall system state reported by the systemd manager (PID 1).
33/// Reflects whether the system has fully booted, is shutting down, or has failures.
34/// Queried via the SystemState property on org.freedesktop.systemd1.Manager.
35#[allow(non_camel_case_types)]
36#[derive(
37    Serialize_repr,
38    Deserialize_repr,
39    Clone,
40    Copy,
41    Debug,
42    Default,
43    Eq,
44    PartialEq,
45    EnumIter,
46    EnumString,
47    IntEnum,
48    strum_macros::Display,
49)]
50#[repr(u8)]
51pub enum SystemdSystemState {
52    /// System state could not be determined
53    #[default]
54    unknown = 0,
55    /// systemd is loading and setting up its internal state early in the boot process
56    initializing = 1,
57    /// systemd is starting units as part of the boot sequence
58    starting = 2,
59    /// All units have been started successfully and the system is fully operational
60    running = 3,
61    /// System is operational but one or more units have failed
62    degraded = 4,
63    /// System is in rescue or emergency mode (single-user maintenance)
64    maintenance = 5,
65    /// System is shutting down
66    stopping = 6,
67    /// systemd is not running (seen on non-booted containers or during very early boot)
68    offline = 7,
69}
70
71/// Parsed systemd version from the Version property on org.freedesktop.systemd1.Manager.
72/// Format: "major.minor[.revision].os" (e.g. "256.1.fc40", "255.6-9.9.hs+fb.el9")
73#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, Eq, PartialEq)]
74pub struct SystemdVersion {
75    /// Major version number (e.g. 256)
76    major: u32,
77    /// Minor version string; may contain hyphens for distro-patched versions (e.g. "6-9")
78    minor: String,
79    /// Optional patch/revision number, present when the version string has 4+ dot-separated parts
80    revision: Option<u32>,
81    /// OS/distribution suffix (e.g. "fc40", "hs+fb.el9")
82    os: String,
83}
84impl SystemdVersion {
85    pub fn new(major: u32, minor: String, revision: Option<u32>, os: String) -> SystemdVersion {
86        Self {
87            major,
88            minor,
89            revision,
90            os,
91        }
92    }
93}
94impl fmt::Display for SystemdVersion {
95    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
96        if let Some(revision) = self.revision {
97            return write!(f, "{}.{}.{}.{}", self.major, self.minor, revision, self.os);
98        }
99        write!(f, "{}.{}.{}", self.major, self.minor, self.os)
100    }
101}
102impl TryFrom<String> for SystemdVersion {
103    type Error = MonitordSystemError;
104
105    fn try_from(s: String) -> Result<Self, Self::Error> {
106        let no_v_version = s.strip_prefix('v').unwrap_or(&s);
107
108        // Handle RC/pre-release versions like "260~rc1-5.fc45".
109        // The '~' separates the major version number from the pre-release identifier.
110        if let Some(tilde_pos) = no_v_version.find('~') {
111            let major = no_v_version[..tilde_pos].parse::<u32>()?;
112            let after_tilde = &no_v_version[tilde_pos + 1..];
113            let mut tilde_parts = after_tilde.splitn(2, '.');
114            let minor = tilde_parts.next().unwrap_or("").to_string();
115            let os = tilde_parts.next().unwrap_or("").to_string();
116            return Ok(SystemdVersion {
117                major,
118                minor,
119                revision: None,
120                os,
121            });
122        }
123
124        let mut parts = no_v_version.split('.');
125        let split_count = parts.clone().count();
126        let major = parts
127            .next()
128            .ok_or_else(|| MonitordSystemError::VersionParseError("No valid major version".into()))?
129            .parse::<u32>()?;
130        let minor = parts
131            .next()
132            .ok_or_else(|| MonitordSystemError::VersionParseError("No valid minor version".into()))?
133            .to_string();
134        let mut revision = None;
135        if split_count > 3 {
136            revision = parts.next().and_then(|s| s.parse::<u32>().ok());
137        }
138        let os = parts.collect::<Vec<&str>>().join(".");
139        Ok(SystemdVersion {
140            major,
141            minor,
142            revision,
143            os,
144        })
145    }
146}
147
148//pub fn get_system_state(dbus_address: &str) -> Result<SystemdSystemState, dbus::Error> {
149pub async fn get_system_state(
150    connection: &zbus::Connection,
151) -> Result<SystemdSystemState, MonitordSystemError> {
152    let p = crate::dbus::zbus_systemd::ManagerProxy::builder(connection)
153        .cache_properties(zbus::proxy::CacheProperties::No)
154        .build()
155        .await
156        .map_err(MonitordSystemError::ZbusError)?;
157
158    let state = match p.system_state().await {
159        Ok(system_state) => match system_state.as_str() {
160            "initializing" => crate::system::SystemdSystemState::initializing,
161            "starting" => crate::system::SystemdSystemState::starting,
162            "running" => crate::system::SystemdSystemState::running,
163            "degraded" => crate::system::SystemdSystemState::degraded,
164            "maintenance" => crate::system::SystemdSystemState::maintenance,
165            "stopping" => crate::system::SystemdSystemState::stopping,
166            "offline" => crate::system::SystemdSystemState::offline,
167            _ => crate::system::SystemdSystemState::unknown,
168        },
169        Err(err) => {
170            error!("Failed to get system-state: {:?}", err);
171            crate::system::SystemdSystemState::unknown
172        }
173    };
174    Ok(state)
175}
176
177/// Async wrapper than can update system stats when passed a locked struct
178///
179/// The D-Bus call runs before the write lock is taken (not while holding it):
180/// `locked_machine_stats` is shared by every collector, so holding it across an
181/// awaited D-Bus round trip would block every other collector's own (already
182/// finished) write until this one's D-Bus call completes.
183pub async fn update_system_stats(
184    connection: zbus::Connection,
185    locked_machine_stats: Arc<RwLock<MachineStats>>,
186) -> anyhow::Result<()> {
187    let system_state = crate::system::get_system_state(&connection)
188        .instrument(tracing::debug_span!("system_state_dbus_call"))
189        .await
190        .map_err(|e| anyhow::anyhow!("Error getting system state: {:?}", e))?;
191    let mut machine_stats = locked_machine_stats
192        .write()
193        .instrument(tracing::debug_span!("system_state_acquire_write_lock"))
194        .await;
195    machine_stats.system_state = system_state;
196    Ok(())
197}
198
199pub async fn get_version(
200    connection: &zbus::Connection,
201) -> Result<SystemdVersion, MonitordSystemError> {
202    let p = crate::dbus::zbus_systemd::ManagerProxy::builder(connection)
203        .cache_properties(zbus::proxy::CacheProperties::No)
204        .build()
205        .await
206        .map_err(MonitordSystemError::ZbusError)?;
207    let version_string = p.version().await?;
208    version_string.try_into()
209}
210
211/// Async wrapper than can update system stats when passed a locked struct
212///
213/// See `update_system_stats` above: the D-Bus call runs before the write lock
214/// is taken so it doesn't hold the shared lock for the round trip's duration.
215pub async fn update_version(
216    connection: zbus::Connection,
217    locked_machine_stats: Arc<RwLock<MachineStats>>,
218) -> anyhow::Result<()> {
219    let version = crate::system::get_version(&connection)
220        .instrument(tracing::debug_span!("version_dbus_call"))
221        .await
222        .map_err(|e| anyhow::anyhow!("Error getting systemd version: {:?}", e))?;
223    let mut machine_stats = locked_machine_stats
224        .write()
225        .instrument(tracing::debug_span!("version_acquire_write_lock"))
226        .await;
227    machine_stats.version = version;
228    Ok(())
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234
235    #[test]
236    fn test_display_struct() {
237        assert_eq!(
238            format!("{}", SystemdSystemState::running),
239            String::from("running"),
240        )
241    }
242
243    #[test]
244    fn test_parsing_systemd_versions() -> Result<(), MonitordSystemError> {
245        let parsed: SystemdVersion = "969.1.69.fc69".to_string().try_into()?;
246        assert_eq!(
247            SystemdVersion::new(969, String::from("1"), Some(69), String::from("fc69")),
248            parsed
249        );
250
251        // No revision
252        let parsed: SystemdVersion = "969.1.fc69".to_string().try_into()?;
253        assert_eq!(
254            SystemdVersion::new(969, String::from("1"), None, String::from("fc69")),
255            parsed
256        );
257
258        // #bigCompany strings
259        let parsed: SystemdVersion = String::from("969.6-9.9.hs+fb.el9").try_into()?;
260        assert_eq!(
261            SystemdVersion::new(969, String::from("6-9"), Some(9), String::from("hs+fb.el9")),
262            parsed
263        );
264
265        let parsed: SystemdVersion = String::from("v299.6-9.9.hs+fb.el9").try_into()?;
266        assert_eq!(
267            SystemdVersion::new(299, String::from("6-9"), Some(9), String::from("hs+fb.el9")),
268            parsed
269        );
270
271        // RC / pre-release versions like those seen on Fedora Rawhide: "260~rc1-5.fc45"
272        let parsed: SystemdVersion = String::from("260~rc1-5.fc45").try_into()?;
273        assert_eq!(
274            SystemdVersion::new(260, String::from("rc1-5"), None, String::from("fc45")),
275            parsed
276        );
277
278        Ok(())
279    }
280}