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        let mut parts = no_v_version.split('.');
107        let split_count = parts.clone().count();
108        let major = parts
109            .next()
110            .ok_or_else(|| MonitordSystemError::VersionParseError("No valid major version".into()))?
111            .parse::<u32>()?;
112        let minor = parts
113            .next()
114            .ok_or_else(|| MonitordSystemError::VersionParseError("No valid minor version".into()))?
115            .to_string();
116        let mut revision = None;
117        if split_count > 3 {
118            revision = parts.next().and_then(|s| s.parse::<u32>().ok());
119        }
120        let os = parts.collect::<Vec<&str>>().join(".");
121        Ok(SystemdVersion {
122            major,
123            minor,
124            revision,
125            os,
126        })
127    }
128}
129
130//pub fn get_system_state(dbus_address: &str) -> Result<SystemdSystemState, dbus::Error> {
131pub async fn get_system_state(
132    connection: &zbus::Connection,
133) -> Result<SystemdSystemState, MonitordSystemError> {
134    let p = crate::dbus::zbus_systemd::ManagerProxy::new(connection)
135        .await
136        .map_err(MonitordSystemError::ZbusError)?;
137
138    let state = match p.system_state().await {
139        Ok(system_state) => match system_state.as_str() {
140            "initializing" => crate::system::SystemdSystemState::initializing,
141            "starting" => crate::system::SystemdSystemState::starting,
142            "running" => crate::system::SystemdSystemState::running,
143            "degraded" => crate::system::SystemdSystemState::degraded,
144            "maintenance" => crate::system::SystemdSystemState::maintenance,
145            "stopping" => crate::system::SystemdSystemState::stopping,
146            "offline" => crate::system::SystemdSystemState::offline,
147            _ => crate::system::SystemdSystemState::unknown,
148        },
149        Err(err) => {
150            error!("Failed to get system-state: {:?}", err);
151            crate::system::SystemdSystemState::unknown
152        }
153    };
154    Ok(state)
155}
156
157/// Async wrapper than can update system stats when passed a locked struct
158pub async fn update_system_stats(
159    connection: zbus::Connection,
160    locked_machine_stats: Arc<RwLock<MachineStats>>,
161) -> anyhow::Result<()> {
162    let mut machine_stats = locked_machine_stats.write().await;
163    machine_stats.system_state = crate::system::get_system_state(&connection)
164        .await
165        .map_err(|e| anyhow::anyhow!("Error getting system state: {:?}", e))?;
166    Ok(())
167}
168
169pub async fn get_version(
170    connection: &zbus::Connection,
171) -> Result<SystemdVersion, MonitordSystemError> {
172    let p = crate::dbus::zbus_systemd::ManagerProxy::new(connection)
173        .await
174        .map_err(MonitordSystemError::ZbusError)?;
175    let version_string = p.version().await?;
176    version_string.try_into()
177}
178
179/// Async wrapper than can update system stats when passed a locked struct
180pub async fn update_version(
181    connection: zbus::Connection,
182    locked_machine_stats: Arc<RwLock<MachineStats>>,
183) -> anyhow::Result<()> {
184    let mut machine_stats = locked_machine_stats.write().await;
185    machine_stats.version = crate::system::get_version(&connection)
186        .await
187        .map_err(|e| anyhow::anyhow!("Error getting systemd version: {:?}", e))?;
188    Ok(())
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn test_display_struct() {
197        assert_eq!(
198            format!("{}", SystemdSystemState::running),
199            String::from("running"),
200        )
201    }
202
203    #[test]
204    fn test_parsing_systemd_versions() -> Result<(), MonitordSystemError> {
205        let parsed: SystemdVersion = "969.1.69.fc69".to_string().try_into()?;
206        assert_eq!(
207            SystemdVersion::new(969, String::from("1"), Some(69), String::from("fc69")),
208            parsed
209        );
210
211        // No revision
212        let parsed: SystemdVersion = "969.1.fc69".to_string().try_into()?;
213        assert_eq!(
214            SystemdVersion::new(969, String::from("1"), None, String::from("fc69")),
215            parsed
216        );
217
218        // #bigCompany strings
219        let parsed: SystemdVersion = String::from("969.6-9.9.hs+fb.el9").try_into()?;
220        assert_eq!(
221            SystemdVersion::new(969, String::from("6-9"), Some(9), String::from("hs+fb.el9")),
222            parsed
223        );
224
225        let parsed: SystemdVersion = String::from("v299.6-9.9.hs+fb.el9").try_into()?;
226        assert_eq!(
227            SystemdVersion::new(299, String::from("6-9"), Some(9), String::from("hs+fb.el9")),
228            parsed
229        );
230
231        Ok(())
232    }
233}