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#[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 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 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 config.pid1.enabled = read_config_bool(&ini_config, "pid1", "enabled")?;
285
286 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 config.system_state.enabled = read_config_bool(&ini_config, "system-state", "enabled")?;
294
295 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 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 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 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 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 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 config.varlink.enabled = read_config_bool(&ini_config, "varlink", "enabled")?;
397
398 Ok(config)
399 }
400}
401
402fn 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
424fn 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 assert_eq!(
569 expected_config.monitord.output_format,
570 MonitordOutputFormat::JsonFlat,
571 );
572 assert!(!expected_config.networkd.enabled);
574 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 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}