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    pub user_stats: bool,
142    pub peer_stats: bool,
143    pub cgroup_stats: bool,
144}
145impl Default for DBusStatsConfig {
146    fn default() -> Self {
147        DBusStatsConfig {
148            enabled: true,
149            user_stats: false,
150            peer_stats: false,
151            cgroup_stats: false,
152        }
153    }
154}
155
156/// Config struct
157/// Each section represents an ini file section
158#[derive(Clone, Debug, Default, Eq, PartialEq)]
159pub struct Config {
160    pub machines: MachinesConfig,
161    pub monitord: MonitordConfig,
162    pub networkd: NetworkdConfig,
163    pub pid1: Pid1Config,
164    pub services: Vec<String>,
165    pub system_state: SystemStateConfig,
166    pub timers: TimersConfig,
167    pub units: UnitsConfig,
168    pub dbus_stats: DBusStatsConfig,
169}
170
171impl From<Ini> for Config {
172    fn from(ini_config: Ini) -> Self {
173        let mut config = Config::default();
174
175        // [monitord] section
176        if let Some(dbus_address) = ini_config.get("monitord", "dbus_address") {
177            config.monitord.dbus_address = dbus_address;
178        }
179        if let Ok(Some(dbus_timeout)) = ini_config.getuint("monitord", "dbus_timeout") {
180            config.monitord.dbus_timeout = dbus_timeout;
181        }
182        config.monitord.daemon = read_config_bool(
183            &ini_config,
184            String::from("monitord"),
185            String::from("daemon"),
186        );
187        if let Ok(Some(daemon_stats_refresh_secs)) =
188            ini_config.getuint("monitord", "daemon_stats_refresh_secs")
189        {
190            config.monitord.daemon_stats_refresh_secs = daemon_stats_refresh_secs;
191        }
192        if let Some(key_prefix) = ini_config.get("monitord", "key_prefix") {
193            config.monitord.key_prefix = key_prefix;
194        }
195        config.monitord.output_format = MonitordOutputFormat::from_str(
196            &ini_config
197                .get("monitord", "output_format")
198                .expect("Need 'output_format' set in config"),
199        )
200        .expect("Need a valid value for the enum");
201
202        // [networkd] section
203        config.networkd.enabled = read_config_bool(
204            &ini_config,
205            String::from("networkd"),
206            String::from("enabled"),
207        );
208        if let Some(link_state_dir) = ini_config.get("networkd", "link_state_dir") {
209            config.networkd.link_state_dir = link_state_dir.into();
210        }
211
212        // [pid1] section
213        config.pid1.enabled =
214            read_config_bool(&ini_config, String::from("pid1"), String::from("enabled"));
215
216        // [services] section
217        let config_map = ini_config.get_map().unwrap_or(IndexMap::from([]));
218        if let Some(services) = config_map.get("services") {
219            config.services = services.keys().map(|s| s.to_string()).collect();
220        }
221
222        // [system-state] section
223        config.system_state.enabled = read_config_bool(
224            &ini_config,
225            String::from("system-state"),
226            String::from("enabled"),
227        );
228
229        // [timers] section
230        config.timers.enabled =
231            read_config_bool(&ini_config, String::from("timers"), String::from("enabled"));
232        if let Some(timers_allowlist) = config_map.get("timers.allowlist") {
233            config.timers.allowlist = timers_allowlist.keys().map(|s| s.to_string()).collect();
234        }
235        if let Some(timers_blocklist) = config_map.get("timers.blocklist") {
236            config.timers.blocklist = timers_blocklist.keys().map(|s| s.to_string()).collect();
237        }
238
239        // [units] section
240        config.units.enabled =
241            read_config_bool(&ini_config, String::from("units"), String::from("enabled"));
242        config.units.state_stats = read_config_bool(
243            &ini_config,
244            String::from("units"),
245            String::from("state_stats"),
246        );
247        if let Some(state_stats_allowlist) = config_map.get("units.state_stats.allowlist") {
248            config.units.state_stats_allowlist = state_stats_allowlist
249                .keys()
250                .map(|s| s.to_string())
251                .collect();
252        }
253        if let Some(state_stats_blocklist) = config_map.get("units.state_stats.blocklist") {
254            config.units.state_stats_blocklist = state_stats_blocklist
255                .keys()
256                .map(|s| s.to_string())
257                .collect();
258        }
259        config.units.state_stats_time_in_state = read_config_bool(
260            &ini_config,
261            String::from("units"),
262            String::from("state_stats_time_in_state"),
263        );
264
265        // [machines] section
266        config.machines.enabled = read_config_bool(
267            &ini_config,
268            String::from("machines"),
269            String::from("enabled"),
270        );
271        if let Some(machines_allowlist) = config_map.get("machines.allowlist") {
272            config.machines.allowlist = machines_allowlist.keys().map(|s| s.to_string()).collect();
273        }
274        if let Some(machines_blocklist) = config_map.get("machines.blocklist") {
275            config.machines.blocklist = machines_blocklist.keys().map(|s| s.to_string()).collect();
276        }
277
278        // [dbus] section
279        config.dbus_stats.enabled =
280            read_config_bool(&ini_config, String::from("dbus"), String::from("enabled"));
281        config.dbus_stats.user_stats = read_config_bool(
282            &ini_config,
283            String::from("dbus"),
284            String::from("user_stats"),
285        );
286        config.dbus_stats.peer_stats = read_config_bool(
287            &ini_config,
288            String::from("dbus"),
289            String::from("peer_stats"),
290        );
291        config.dbus_stats.cgroup_stats = read_config_bool(
292            &ini_config,
293            String::from("dbus"),
294            String::from("cgroup_stats"),
295        );
296
297        config
298    }
299}
300
301/// Helper function to read "bool" config options
302fn read_config_bool(config: &Ini, section: String, key: String) -> bool {
303    let option_bool = match config.getbool(&section, &key) {
304        Ok(config_option_bool) => config_option_bool,
305        Err(err) => panic!(
306            "Unable to find '{}' key in '{}' section in config file: {}",
307            key, section, err
308        ),
309    };
310    match option_bool {
311        Some(bool_value) => bool_value,
312        None => {
313            error!(
314                "No value for '{}' in '{}' section ... assuming false",
315                key, section
316            );
317            false
318        }
319    }
320}
321
322#[cfg(test)]
323mod tests {
324    use std::io::Write;
325
326    use tempfile::NamedTempFile;
327
328    use super::*;
329
330    const FULL_CONFIG: &str = r###"
331[monitord]
332dbus_address = unix:path=/system_bus_socket
333dbus_timeout = 2
334daemon = true
335daemon_stats_refresh_secs = 0
336key_prefix = unittest
337output_format = json-pretty
338
339[networkd]
340enabled = true
341link_state_dir = /links
342
343[pid1]
344enabled = true
345
346[services]
347foo.service
348bar.service
349
350[system-state]
351enabled = true
352
353[timers]
354enabled = true
355
356[timers.allowlist]
357foo.timer
358
359[timers.blocklist]
360bar.timer
361
362[units]
363enabled = true
364state_stats = true
365state_stats_time_in_state = true
366
367[units.state_stats.allowlist]
368foo.service
369
370[units.state_stats.blocklist]
371bar.service
372
373[machines]
374enabled = true
375
376[machines.allowlist]
377foo
378bar
379
380[machines.blocklist]
381foo2
382
383[dbus]
384enabled = true
385user_stats = true
386peer_stats = true
387cgroup_stats = true
388"###;
389
390    const MINIMAL_CONFIG: &str = r###"
391[monitord]
392output_format = json-flat
393"###;
394
395    #[test]
396    fn test_default_config() {
397        assert!(Config::default().units.enabled)
398    }
399
400    #[test]
401    fn test_minimal_config() {
402        let mut monitord_config = NamedTempFile::new().expect("Unable to make named tempfile");
403        monitord_config
404            .write_all(MINIMAL_CONFIG.as_bytes())
405            .expect("Unable to write out temp config file");
406
407        let mut ini_config = Ini::new();
408        let _config_map = ini_config
409            .load(monitord_config.path())
410            .expect("Unable to load ini config");
411
412        let expected_config: Config = ini_config.into();
413        // See our one setting is not the default 'json' enum value
414        assert_eq!(
415            expected_config.monitord.output_format,
416            MonitordOutputFormat::JsonFlat,
417        );
418        // See that one of the enabled bools are false
419        assert!(!expected_config.networkd.enabled);
420    }
421
422    #[test]
423    fn test_full_config() {
424        let expected_config = Config {
425            monitord: MonitordConfig {
426                dbus_address: String::from("unix:path=/system_bus_socket"),
427                daemon: true,
428                daemon_stats_refresh_secs: u64::MIN,
429                key_prefix: String::from("unittest"),
430                output_format: MonitordOutputFormat::JsonPretty,
431                dbus_timeout: 2 as u64,
432            },
433            networkd: NetworkdConfig {
434                enabled: true,
435                link_state_dir: "/links".into(),
436            },
437            pid1: Pid1Config { enabled: true },
438            services: Vec::from([String::from("foo.service"), String::from("bar.service")]),
439            system_state: SystemStateConfig { enabled: true },
440            timers: TimersConfig {
441                enabled: true,
442                allowlist: Vec::from([String::from("foo.timer")]),
443                blocklist: Vec::from([String::from("bar.timer")]),
444            },
445            units: UnitsConfig {
446                enabled: true,
447                state_stats: true,
448                state_stats_allowlist: Vec::from([String::from("foo.service")]),
449                state_stats_blocklist: Vec::from([String::from("bar.service")]),
450                state_stats_time_in_state: true,
451            },
452            machines: MachinesConfig {
453                enabled: true,
454                allowlist: Vec::from([String::from("foo"), String::from("bar")]),
455                blocklist: Vec::from([String::from("foo2")]),
456            },
457            dbus_stats: DBusStatsConfig {
458                enabled: true,
459                user_stats: true,
460                peer_stats: true,
461                cgroup_stats: true,
462            },
463        };
464
465        let mut monitord_config = NamedTempFile::new().expect("Unable to make named tempfile");
466        monitord_config
467            .write_all(FULL_CONFIG.as_bytes())
468            .expect("Unable to write out temp config file");
469
470        let mut ini_config = Ini::new();
471        let _config_map = ini_config
472            .load(monitord_config.path())
473            .expect("Unable to load ini config");
474
475        // See everything set / overloaded ...
476        assert_eq!(expected_config, ini_config.into(),);
477    }
478}