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