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