monitord/
config.rs

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