monitord/
config.rs

1use std::path::PathBuf;
2use std::str::FromStr;
3
4use configparser::ini::Ini;
5use indexmap::map::IndexMap;
6use int_enum::IntEnum;
7use strum_macros::EnumString;
8use tracing::error;
9
10#[derive(Clone, Debug, Default, EnumString, Eq, IntEnum, PartialEq, strum_macros::Display)]
11#[repr(u8)]
12pub enum MonitordOutputFormat {
13    #[default]
14    #[strum(serialize = "json", serialize = "JSON", serialize = "Json")]
15    Json = 0,
16    #[strum(
17        serialize = "json-flat",
18        serialize = "json_flat",
19        serialize = "jsonflat"
20    )]
21    JsonFlat = 1,
22    #[strum(
23        serialize = "json-pretty",
24        serialize = "json_pretty",
25        serialize = "jsonpretty"
26    )]
27    JsonPretty = 2,
28}
29
30#[derive(Clone, Debug, Eq, PartialEq)]
31pub struct MonitordConfig {
32    pub dbus_address: String,
33    pub daemon: bool,
34    pub daemon_stats_refresh_secs: u64,
35    pub key_prefix: String,
36    pub output_format: MonitordOutputFormat,
37    pub dbus_timeout: u64,
38}
39impl Default for MonitordConfig {
40    fn default() -> Self {
41        MonitordConfig {
42            dbus_address: crate::DEFAULT_DBUS_ADDRESS.into(),
43            daemon: false,
44            daemon_stats_refresh_secs: 30,
45            key_prefix: "".to_string(),
46            output_format: MonitordOutputFormat::default(),
47            dbus_timeout: 30,
48        }
49    }
50}
51
52#[derive(Clone, Debug, Eq, PartialEq)]
53pub struct NetworkdConfig {
54    pub enabled: bool,
55    pub link_state_dir: PathBuf,
56}
57impl Default for NetworkdConfig {
58    fn default() -> Self {
59        NetworkdConfig {
60            enabled: false,
61            link_state_dir: crate::networkd::NETWORKD_STATE_FILES.into(),
62        }
63    }
64}
65
66#[derive(Clone, Debug, Eq, PartialEq)]
67pub struct Pid1Config {
68    pub enabled: bool,
69}
70impl Default for Pid1Config {
71    fn default() -> Self {
72        Pid1Config { enabled: true }
73    }
74}
75
76#[derive(Clone, Debug, Eq, PartialEq)]
77pub struct SystemStateConfig {
78    pub enabled: bool,
79}
80impl Default for SystemStateConfig {
81    fn default() -> Self {
82        SystemStateConfig { enabled: true }
83    }
84}
85
86#[derive(Clone, Debug, Eq, PartialEq)]
87pub struct TimersConfig {
88    pub enabled: bool,
89    pub allowlist: Vec<String>,
90    pub blocklist: Vec<String>,
91}
92impl Default for TimersConfig {
93    fn default() -> Self {
94        TimersConfig {
95            enabled: true,
96            allowlist: Vec::new(),
97            blocklist: Vec::new(),
98        }
99    }
100}
101
102#[derive(Clone, Debug, Eq, PartialEq)]
103pub struct UnitsConfig {
104    pub enabled: bool,
105    pub state_stats: bool,
106    pub state_stats_allowlist: Vec<String>,
107    pub state_stats_blocklist: Vec<String>,
108    pub state_stats_time_in_state: bool,
109}
110impl Default for UnitsConfig {
111    fn default() -> Self {
112        UnitsConfig {
113            enabled: true,
114            state_stats: false,
115            state_stats_allowlist: Vec::new(),
116            state_stats_blocklist: Vec::new(),
117            state_stats_time_in_state: true,
118        }
119    }
120}
121
122#[derive(Clone, Debug, Eq, PartialEq)]
123pub struct MachinesConfig {
124    pub enabled: bool,
125    pub allowlist: Vec<String>,
126    pub blocklist: Vec<String>,
127}
128impl Default for MachinesConfig {
129    fn default() -> Self {
130        MachinesConfig {
131            enabled: true,
132            allowlist: Vec::new(),
133            blocklist: Vec::new(),
134        }
135    }
136}
137
138#[derive(Clone, Debug, Eq, PartialEq)]
139pub struct DBusStatsConfig {
140    pub enabled: bool,
141}
142impl Default for DBusStatsConfig {
143    fn default() -> Self {
144        DBusStatsConfig { enabled: true }
145    }
146}
147
148/// Config struct
149/// Each section represents an ini file section
150#[derive(Clone, Debug, Default, Eq, PartialEq)]
151pub struct Config {
152    pub machines: MachinesConfig,
153    pub monitord: MonitordConfig,
154    pub networkd: NetworkdConfig,
155    pub pid1: Pid1Config,
156    pub services: Vec<String>,
157    pub system_state: SystemStateConfig,
158    pub timers: TimersConfig,
159    pub units: UnitsConfig,
160    pub dbus_stats: DBusStatsConfig,
161}
162
163impl From<Ini> for Config {
164    fn from(ini_config: Ini) -> Self {
165        let mut config = Config::default();
166
167        // [monitord] section
168        if let Some(dbus_address) = ini_config.get("monitord", "dbus_address") {
169            config.monitord.dbus_address = dbus_address;
170        }
171        if let Ok(Some(dbus_timeout)) = ini_config.getuint("monitord", "dbus_timeout") {
172            config.monitord.dbus_timeout = dbus_timeout;
173        }
174        config.monitord.daemon = read_config_bool(
175            &ini_config,
176            String::from("monitord"),
177            String::from("daemon"),
178        );
179        if let Ok(Some(daemon_stats_refresh_secs)) =
180            ini_config.getuint("monitord", "daemon_stats_refresh_secs")
181        {
182            config.monitord.daemon_stats_refresh_secs = daemon_stats_refresh_secs;
183        }
184        if let Some(key_prefix) = ini_config.get("monitord", "key_prefix") {
185            config.monitord.key_prefix = key_prefix;
186        }
187        config.monitord.output_format = MonitordOutputFormat::from_str(
188            &ini_config
189                .get("monitord", "output_format")
190                .expect("Need 'output_format' set in config"),
191        )
192        .expect("Need a valid value for the enum");
193
194        // [networkd] section
195        config.networkd.enabled = read_config_bool(
196            &ini_config,
197            String::from("networkd"),
198            String::from("enabled"),
199        );
200        if let Some(link_state_dir) = ini_config.get("networkd", "link_state_dir") {
201            config.networkd.link_state_dir = link_state_dir.into();
202        }
203
204        // [pid1] section
205        config.pid1.enabled =
206            read_config_bool(&ini_config, String::from("pid1"), String::from("enabled"));
207
208        // [services] section
209        let config_map = ini_config.get_map().unwrap_or(IndexMap::from([]));
210        if let Some(services) = config_map.get("services") {
211            config.services = services.keys().map(|s| s.to_string()).collect();
212        }
213
214        // [system-state] section
215        config.system_state.enabled = read_config_bool(
216            &ini_config,
217            String::from("system-state"),
218            String::from("enabled"),
219        );
220
221        // [timers] section
222        config.timers.enabled =
223            read_config_bool(&ini_config, String::from("timers"), String::from("enabled"));
224        if let Some(timers_allowlist) = config_map.get("timers.allowlist") {
225            config.timers.allowlist = timers_allowlist.keys().map(|s| s.to_string()).collect();
226        }
227        if let Some(timers_blocklist) = config_map.get("timers.blocklist") {
228            config.timers.blocklist = timers_blocklist.keys().map(|s| s.to_string()).collect();
229        }
230
231        // [units] section
232        config.units.enabled =
233            read_config_bool(&ini_config, String::from("units"), String::from("enabled"));
234        config.units.state_stats = read_config_bool(
235            &ini_config,
236            String::from("units"),
237            String::from("state_stats"),
238        );
239        if let Some(state_stats_allowlist) = config_map.get("units.state_stats.allowlist") {
240            config.units.state_stats_allowlist = state_stats_allowlist
241                .keys()
242                .map(|s| s.to_string())
243                .collect();
244        }
245        if let Some(state_stats_blocklist) = config_map.get("units.state_stats.blocklist") {
246            config.units.state_stats_blocklist = state_stats_blocklist
247                .keys()
248                .map(|s| s.to_string())
249                .collect();
250        }
251        config.units.state_stats_time_in_state = read_config_bool(
252            &ini_config,
253            String::from("units"),
254            String::from("state_stats_time_in_state"),
255        );
256
257        // [machines] section
258        config.machines.enabled = read_config_bool(
259            &ini_config,
260            String::from("machines"),
261            String::from("enabled"),
262        );
263        if let Some(machines_allowlist) = config_map.get("machines.allowlist") {
264            config.machines.allowlist = machines_allowlist.keys().map(|s| s.to_string()).collect();
265        }
266        if let Some(machines_blocklist) = config_map.get("machines.blocklist") {
267            config.machines.blocklist = machines_blocklist.keys().map(|s| s.to_string()).collect();
268        }
269
270        // [dbus] section
271        config.dbus_stats.enabled =
272            read_config_bool(&ini_config, String::from("dbus"), String::from("enabled"));
273
274        config
275    }
276}
277
278/// Helper function to read "bool" config options
279fn read_config_bool(config: &Ini, section: String, key: String) -> bool {
280    let option_bool = match config.getbool(&section, &key) {
281        Ok(config_option_bool) => config_option_bool,
282        Err(err) => panic!(
283            "Unable to find '{}' key in '{}' section in config file: {}",
284            key, section, err
285        ),
286    };
287    match option_bool {
288        Some(bool_value) => bool_value,
289        None => {
290            error!(
291                "No value for '{}' in '{}' section ... assuming false",
292                key, section
293            );
294            false
295        }
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use std::io::Write;
302
303    use tempfile::NamedTempFile;
304
305    use super::*;
306
307    const FULL_CONFIG: &str = r###"
308[monitord]
309dbus_address = unix:path=/system_bus_socket
310dbus_timeout = 2
311daemon = true
312daemon_stats_refresh_secs = 0
313key_prefix = unittest
314output_format = json-pretty
315
316[networkd]
317enabled = true
318link_state_dir = /links
319
320[pid1]
321enabled = true
322
323[services]
324foo.service
325bar.service
326
327[system-state]
328enabled = true
329
330[timers]
331enabled = true
332
333[timers.allowlist]
334foo.timer
335
336[timers.blocklist]
337bar.timer
338
339[units]
340enabled = true
341state_stats = true
342state_stats_time_in_state = true
343
344[units.state_stats.allowlist]
345foo.service
346
347[units.state_stats.blocklist]
348bar.service
349
350[machines]
351enabled = true
352
353[machines.allowlist]
354foo
355bar
356
357[machines.blocklist]
358foo2
359
360[dbus]
361enabled = true
362"###;
363
364    const MINIMAL_CONFIG: &str = r###"
365[monitord]
366output_format = json-flat
367"###;
368
369    #[test]
370    fn test_default_config() {
371        assert!(Config::default().units.enabled)
372    }
373
374    #[test]
375    fn test_minimal_config() {
376        let mut monitord_config = NamedTempFile::new().expect("Unable to make named tempfile");
377        monitord_config
378            .write_all(MINIMAL_CONFIG.as_bytes())
379            .expect("Unable to write out temp config file");
380
381        let mut ini_config = Ini::new();
382        let _config_map = ini_config
383            .load(monitord_config.path())
384            .expect("Unable to load ini config");
385
386        let expected_config: Config = ini_config.into();
387        // See our one setting is not the default 'json' enum value
388        assert_eq!(
389            expected_config.monitord.output_format,
390            MonitordOutputFormat::JsonFlat,
391        );
392        // See that one of the enabled bools are false
393        assert!(!expected_config.networkd.enabled);
394    }
395
396    #[test]
397    fn test_full_config() {
398        let expected_config = Config {
399            monitord: MonitordConfig {
400                dbus_address: String::from("unix:path=/system_bus_socket"),
401                daemon: true,
402                daemon_stats_refresh_secs: u64::MIN,
403                key_prefix: String::from("unittest"),
404                output_format: MonitordOutputFormat::JsonPretty,
405                dbus_timeout: 2 as u64,
406            },
407            networkd: NetworkdConfig {
408                enabled: true,
409                link_state_dir: "/links".into(),
410            },
411            pid1: Pid1Config { enabled: true },
412            services: Vec::from([String::from("foo.service"), String::from("bar.service")]),
413            system_state: SystemStateConfig { enabled: true },
414            timers: TimersConfig {
415                enabled: true,
416                allowlist: Vec::from([String::from("foo.timer")]),
417                blocklist: Vec::from([String::from("bar.timer")]),
418            },
419            units: UnitsConfig {
420                enabled: true,
421                state_stats: true,
422                state_stats_allowlist: Vec::from([String::from("foo.service")]),
423                state_stats_blocklist: Vec::from([String::from("bar.service")]),
424                state_stats_time_in_state: true,
425            },
426            machines: MachinesConfig {
427                enabled: true,
428                allowlist: Vec::from([String::from("foo"), String::from("bar")]),
429                blocklist: Vec::from([String::from("foo2")]),
430            },
431            dbus_stats: DBusStatsConfig { enabled: true },
432        };
433
434        let mut monitord_config = NamedTempFile::new().expect("Unable to make named tempfile");
435        monitord_config
436            .write_all(FULL_CONFIG.as_bytes())
437            .expect("Unable to write out temp config file");
438
439        let mut ini_config = Ini::new();
440        let _config_map = ini_config
441            .load(monitord_config.path())
442            .expect("Unable to load ini config");
443
444        // See everything set / overloaded ...
445        assert_eq!(expected_config, ini_config.into(),);
446    }
447}