Skip to main content

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