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