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
156    pub user_stats: bool,
157    pub user_allowlist: HashSet<String>,
158    pub user_blocklist: HashSet<String>,
159
160    pub peer_stats: bool,
161    pub peer_well_known_names_only: bool,
162    pub peer_allowlist: HashSet<String>,
163    pub peer_blocklist: HashSet<String>,
164
165    pub cgroup_stats: bool,
166    pub cgroup_allowlist: HashSet<String>,
167    pub cgroup_blocklist: HashSet<String>,
168}
169impl Default for DBusStatsConfig {
170    fn default() -> Self {
171        DBusStatsConfig {
172            enabled: true,
173
174            user_stats: false,
175            user_allowlist: HashSet::new(),
176            user_blocklist: HashSet::new(),
177
178            peer_stats: false,
179            peer_well_known_names_only: false,
180            peer_allowlist: HashSet::new(),
181            peer_blocklist: HashSet::new(),
182
183            cgroup_stats: false,
184            cgroup_allowlist: HashSet::new(),
185            cgroup_blocklist: HashSet::new(),
186        }
187    }
188}
189
190#[derive(Clone, Debug, Eq, PartialEq)]
191pub struct BootBlameConfig {
192    pub enabled: bool,
193    pub num_slowest_units: u64,
194    pub allowlist: HashSet<String>,
195    pub blocklist: HashSet<String>,
196}
197impl Default for BootBlameConfig {
198    fn default() -> Self {
199        BootBlameConfig {
200            enabled: false,
201            num_slowest_units: 5,
202            allowlist: HashSet::new(),
203            blocklist: HashSet::new(),
204        }
205    }
206}
207
208#[derive(Clone, Debug, Default, Eq, PartialEq)]
209pub struct VerifyConfig {
210    pub enabled: bool,
211    pub allowlist: HashSet<String>,
212    pub blocklist: HashSet<String>,
213}
214
215#[derive(Clone, Debug, Default, Eq, PartialEq)]
216pub struct VarlinkConfig {
217    pub enabled: bool,
218}
219
220/// Config struct
221/// Each section represents an ini file section
222#[derive(Clone, Debug, Default, Eq, PartialEq)]
223pub struct Config {
224    pub machines: MachinesConfig,
225    pub monitord: MonitordConfig,
226    pub networkd: NetworkdConfig,
227    pub pid1: Pid1Config,
228    pub services: HashSet<String>,
229    pub system_state: SystemStateConfig,
230    pub timers: TimersConfig,
231    pub units: UnitsConfig,
232    pub dbus_stats: DBusStatsConfig,
233    pub boot_blame: BootBlameConfig,
234    pub verify: VerifyConfig,
235    pub varlink: VarlinkConfig,
236}
237
238impl TryFrom<Ini> for Config {
239    type Error = MonitordConfigError;
240
241    fn try_from(ini_config: Ini) -> Result<Self, MonitordConfigError> {
242        let mut config = Config::default();
243
244        // [monitord] section
245        if let Some(dbus_address) = ini_config.get("monitord", "dbus_address") {
246            config.monitord.dbus_address = dbus_address;
247        }
248        if let Ok(Some(dbus_timeout)) = ini_config.getuint("monitord", "dbus_timeout") {
249            config.monitord.dbus_timeout = dbus_timeout;
250        }
251        config.monitord.daemon = read_config_bool(&ini_config, "monitord", "daemon")?;
252        if let Ok(Some(daemon_stats_refresh_secs)) =
253            ini_config.getuint("monitord", "daemon_stats_refresh_secs")
254        {
255            config.monitord.daemon_stats_refresh_secs = daemon_stats_refresh_secs;
256        }
257        if let Some(key_prefix) = ini_config.get("monitord", "key_prefix") {
258            config.monitord.key_prefix = key_prefix;
259        }
260        let output_format_str = ini_config.get("monitord", "output_format").ok_or_else(|| {
261            MonitordConfigError::MissingKey {
262                section: "monitord".into(),
263                key: "output_format".into(),
264            }
265        })?;
266        config.monitord.output_format = MonitordOutputFormat::from_str(&output_format_str)
267            .map_err(|e| MonitordConfigError::InvalidValue {
268                section: "monitord".into(),
269                key: "output_format".into(),
270                reason: e.to_string(),
271            })?;
272
273        // [networkd] section
274        config.networkd.enabled = read_config_bool(&ini_config, "networkd", "enabled")?;
275        if let Some(link_state_dir) = ini_config.get("networkd", "link_state_dir") {
276            config.networkd.link_state_dir = link_state_dir.into();
277        }
278
279        // [pid1] section
280        config.pid1.enabled = read_config_bool(&ini_config, "pid1", "enabled")?;
281
282        // [services] section
283        let config_map = ini_config.get_map().unwrap_or(IndexMap::from([]));
284        if let Some(services) = config_map.get("services") {
285            config.services = services.keys().map(|s| s.to_string()).collect();
286        }
287
288        // [system-state] section
289        config.system_state.enabled = read_config_bool(&ini_config, "system-state", "enabled")?;
290
291        // [timers] section
292        config.timers.enabled = read_config_bool(&ini_config, "timers", "enabled")?;
293        if let Some(timers_allowlist) = config_map.get("timers.allowlist") {
294            config.timers.allowlist = timers_allowlist.keys().map(|s| s.to_string()).collect();
295        }
296        if let Some(timers_blocklist) = config_map.get("timers.blocklist") {
297            config.timers.blocklist = timers_blocklist.keys().map(|s| s.to_string()).collect();
298        }
299
300        // [units] section
301        config.units.enabled = read_config_bool(&ini_config, "units", "enabled")?;
302        config.units.state_stats = read_config_bool(&ini_config, "units", "state_stats")?;
303        if let Some(state_stats_allowlist) = config_map.get("units.state_stats.allowlist") {
304            config.units.state_stats_allowlist = state_stats_allowlist
305                .keys()
306                .map(|s| s.to_string())
307                .collect();
308        }
309        if let Some(state_stats_blocklist) = config_map.get("units.state_stats.blocklist") {
310            config.units.state_stats_blocklist = state_stats_blocklist
311                .keys()
312                .map(|s| s.to_string())
313                .collect();
314        }
315        config.units.state_stats_time_in_state =
316            read_config_bool(&ini_config, "units", "state_stats_time_in_state")?;
317
318        // [machines] section
319        config.machines.enabled = read_config_bool(&ini_config, "machines", "enabled")?;
320        if let Some(machines_allowlist) = config_map.get("machines.allowlist") {
321            config.machines.allowlist = machines_allowlist.keys().map(|s| s.to_string()).collect();
322        }
323        if let Some(machines_blocklist) = config_map.get("machines.blocklist") {
324            config.machines.blocklist = machines_blocklist.keys().map(|s| s.to_string()).collect();
325        }
326
327        // [dbus] section
328        config.dbus_stats.enabled = read_config_bool(&ini_config, "dbus", "enabled")?;
329
330        config.dbus_stats.user_stats = read_config_bool(&ini_config, "dbus", "user_stats")?;
331        if let Some(user_allowlist) = config_map.get("dbus.user.allowlist") {
332            config.dbus_stats.user_allowlist =
333                user_allowlist.keys().map(|s| s.to_string()).collect();
334        }
335        if let Some(user_blocklist) = config_map.get("dbus.user.blocklist") {
336            config.dbus_stats.user_blocklist =
337                user_blocklist.keys().map(|s| s.to_string()).collect();
338        }
339
340        config.dbus_stats.peer_stats = read_config_bool(&ini_config, "dbus", "peer_stats")?;
341        config.dbus_stats.peer_well_known_names_only =
342            read_config_bool(&ini_config, "dbus", "peer_well_known_names_only")?;
343        if let Some(peer_allowlist) = config_map.get("dbus.peer.allowlist") {
344            config.dbus_stats.peer_allowlist =
345                peer_allowlist.keys().map(|s| s.to_string()).collect();
346        }
347        if let Some(peer_blocklist) = config_map.get("dbus.peer.blocklist") {
348            config.dbus_stats.peer_blocklist =
349                peer_blocklist.keys().map(|s| s.to_string()).collect();
350        }
351
352        config.dbus_stats.cgroup_stats = read_config_bool(&ini_config, "dbus", "cgroup_stats")?;
353        if let Some(cgroup_allowlist) = config_map.get("dbus.cgroup.allowlist") {
354            config.dbus_stats.cgroup_allowlist =
355                cgroup_allowlist.keys().map(|s| s.to_string()).collect();
356        }
357        if let Some(cgroup_blocklist) = config_map.get("dbus.cgroup.blocklist") {
358            config.dbus_stats.cgroup_blocklist =
359                cgroup_blocklist.keys().map(|s| s.to_string()).collect();
360        }
361
362        // [boot] section
363        config.boot_blame.enabled = read_config_bool(&ini_config, "boot", "enabled")?;
364        if let Ok(Some(num_slowest_units)) = ini_config.getuint("boot", "num_slowest_units") {
365            config.boot_blame.num_slowest_units = num_slowest_units;
366        }
367        if let Some(boot_allowlist) = config_map.get("boot.allowlist") {
368            config.boot_blame.allowlist = boot_allowlist.keys().map(|s| s.to_string()).collect();
369        }
370        if let Some(boot_blocklist) = config_map.get("boot.blocklist") {
371            config.boot_blame.blocklist = boot_blocklist.keys().map(|s| s.to_string()).collect();
372        }
373
374        // [verify] section
375        config.verify.enabled = read_config_bool(&ini_config, "verify", "enabled")?;
376        if let Some(verify_allowlist) = config_map.get("verify.allowlist") {
377            config.verify.allowlist = verify_allowlist.keys().map(|s| s.to_string()).collect();
378        }
379        if let Some(verify_blocklist) = config_map.get("verify.blocklist") {
380            config.verify.blocklist = verify_blocklist.keys().map(|s| s.to_string()).collect();
381        }
382
383        // [varlink] section
384        config.varlink.enabled = read_config_bool(&ini_config, "varlink", "enabled")?;
385
386        Ok(config)
387    }
388}
389
390/// Helper function to read "bool" config options
391fn read_config_bool(config: &Ini, section: &str, key: &str) -> Result<bool, MonitordConfigError> {
392    let option_bool =
393        config
394            .getbool(section, key)
395            .map_err(|err| MonitordConfigError::InvalidValue {
396                section: section.into(),
397                key: key.into(),
398                reason: err,
399            })?;
400    match option_bool {
401        Some(bool_value) => Ok(bool_value),
402        None => {
403            error!(
404                "No value for '{}' in '{}' section ... assuming false",
405                key, section
406            );
407            Ok(false)
408        }
409    }
410}
411
412#[cfg(test)]
413mod tests {
414    use std::io::Write;
415
416    use tempfile::NamedTempFile;
417
418    use super::*;
419
420    const FULL_CONFIG: &str = r###"
421[monitord]
422dbus_address = unix:path=/system_bus_socket
423dbus_timeout = 2
424daemon = true
425daemon_stats_refresh_secs = 0
426key_prefix = unittest
427output_format = json-pretty
428
429[networkd]
430enabled = true
431link_state_dir = /links
432
433[pid1]
434enabled = true
435
436[services]
437foo.service
438bar.service
439
440[system-state]
441enabled = true
442
443[timers]
444enabled = true
445
446[timers.allowlist]
447foo.timer
448
449[timers.blocklist]
450bar.timer
451
452[units]
453enabled = true
454state_stats = true
455state_stats_time_in_state = true
456
457[units.state_stats.allowlist]
458foo.service
459
460[units.state_stats.blocklist]
461bar.service
462
463[machines]
464enabled = true
465
466[machines.allowlist]
467foo
468bar
469
470[machines.blocklist]
471foo2
472
473[dbus]
474enabled = true
475user_stats = true
476peer_stats = true
477peer_well_known_names_only = true
478cgroup_stats = true
479
480[dbus.user.allowlist]
481foo
482bar
483
484[dbus.user.blocklist]
485foo2
486
487[dbus.peer.allowlist]
488foo
489bar
490
491[dbus.peer.blocklist]
492foo2
493
494[dbus.cgroup.allowlist]
495foo
496bar
497
498[dbus.cgroup.blocklist]
499foo2
500
501[boot]
502enabled = true
503num_slowest_units = 10
504
505[boot.allowlist]
506foo.service
507
508[boot.blocklist]
509bar.service
510
511[varlink]
512enabled = true
513"###;
514
515    const MINIMAL_CONFIG: &str = r###"
516[monitord]
517output_format = json-flat
518"###;
519
520    #[test]
521    fn test_default_config() {
522        assert!(Config::default().units.enabled)
523    }
524
525    #[test]
526    fn test_minimal_config() {
527        let mut monitord_config = NamedTempFile::new().expect("Unable to make named tempfile");
528        monitord_config
529            .write_all(MINIMAL_CONFIG.as_bytes())
530            .expect("Unable to write out temp config file");
531
532        let mut ini_config = Ini::new();
533        let _config_map = ini_config
534            .load(monitord_config.path())
535            .expect("Unable to load ini config");
536
537        let expected_config: Config = ini_config.try_into().expect("Failed to parse config");
538        // See our one setting is not the default 'json' enum value
539        assert_eq!(
540            expected_config.monitord.output_format,
541            MonitordOutputFormat::JsonFlat,
542        );
543        // See that one of the enabled bools are false
544        assert!(!expected_config.networkd.enabled);
545    }
546
547    #[test]
548    fn test_full_config() {
549        let expected_config = Config {
550            monitord: MonitordConfig {
551                dbus_address: String::from("unix:path=/system_bus_socket"),
552                daemon: true,
553                daemon_stats_refresh_secs: u64::MIN,
554                key_prefix: String::from("unittest"),
555                output_format: MonitordOutputFormat::JsonPretty,
556                dbus_timeout: 2 as u64,
557            },
558            networkd: NetworkdConfig {
559                enabled: true,
560                link_state_dir: "/links".into(),
561            },
562            pid1: Pid1Config { enabled: true },
563            services: HashSet::from([String::from("foo.service"), String::from("bar.service")]),
564            system_state: SystemStateConfig { enabled: true },
565            timers: TimersConfig {
566                enabled: true,
567                allowlist: HashSet::from([String::from("foo.timer")]),
568                blocklist: HashSet::from([String::from("bar.timer")]),
569            },
570            units: UnitsConfig {
571                enabled: true,
572                state_stats: true,
573                state_stats_allowlist: HashSet::from([String::from("foo.service")]),
574                state_stats_blocklist: HashSet::from([String::from("bar.service")]),
575                state_stats_time_in_state: true,
576            },
577            machines: MachinesConfig {
578                enabled: true,
579                allowlist: HashSet::from([String::from("foo"), String::from("bar")]),
580                blocklist: HashSet::from([String::from("foo2")]),
581            },
582            dbus_stats: DBusStatsConfig {
583                enabled: true,
584                user_stats: true,
585                user_allowlist: HashSet::from([String::from("foo"), String::from("bar")]),
586                user_blocklist: HashSet::from([String::from("foo2")]),
587                peer_stats: true,
588                peer_well_known_names_only: true,
589                peer_allowlist: HashSet::from([String::from("foo"), String::from("bar")]),
590                peer_blocklist: HashSet::from([String::from("foo2")]),
591                cgroup_stats: true,
592                cgroup_allowlist: HashSet::from([String::from("foo"), String::from("bar")]),
593                cgroup_blocklist: HashSet::from([String::from("foo2")]),
594            },
595            boot_blame: BootBlameConfig {
596                enabled: true,
597                num_slowest_units: 10,
598                allowlist: HashSet::from([String::from("foo.service")]),
599                blocklist: HashSet::from([String::from("bar.service")]),
600            },
601            verify: VerifyConfig {
602                enabled: false,
603                allowlist: HashSet::new(),
604                blocklist: HashSet::new(),
605            },
606            varlink: VarlinkConfig { enabled: true },
607        };
608
609        let mut monitord_config = NamedTempFile::new().expect("Unable to make named tempfile");
610        monitord_config
611            .write_all(FULL_CONFIG.as_bytes())
612            .expect("Unable to write out temp config file");
613
614        let mut ini_config = Ini::new();
615        let _config_map = ini_config
616            .load(monitord_config.path())
617            .expect("Unable to load ini config");
618
619        // See everything set / overloaded ...
620        let actual_config: Config = ini_config.try_into().expect("Failed to parse config");
621        assert_eq!(expected_config, actual_config);
622    }
623
624    #[test]
625    fn test_invalid_config_returns_error() {
626        let invalid_config = "[monitord]\ndaemon = notabool\noutput_format = json\n";
627        let mut monitord_config = NamedTempFile::new().expect("Unable to make named tempfile");
628        monitord_config
629            .write_all(invalid_config.as_bytes())
630            .expect("Unable to write out temp config file");
631
632        let mut ini_config = Ini::new();
633        let _config_map = ini_config
634            .load(monitord_config.path())
635            .expect("Unable to load ini config");
636
637        let result: Result<Config, _> = ini_config.try_into();
638        assert!(result.is_err());
639    }
640}