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#[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 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 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 config.pid1.enabled = read_config_bool(&ini_config, "pid1", "enabled")?;
289
290 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 config.system_state.enabled = read_config_bool(&ini_config, "system-state", "enabled")?;
298
299 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 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 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 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 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 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 config.varlink.enabled = read_config_bool(&ini_config, "varlink", "enabled")?;
409
410 Ok(config)
411 }
412}
413
414fn 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
436fn 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 assert_eq!(
583 expected_config.monitord.output_format,
584 MonitordOutputFormat::JsonFlat,
585 );
586 assert!(!expected_config.networkd.enabled);
588 assert!(expected_config.boot_blame.cache_enabled);
590 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 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}