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