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, warn};
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 varlink: bool,
73 pub link_state_dir: PathBuf,
74}
75impl Default for NetworkdConfig {
76 fn default() -> Self {
77 NetworkdConfig {
78 enabled: false,
79 varlink: true,
80 link_state_dir: crate::networkd::NETWORKD_STATE_FILES.into(),
81 }
82 }
83}
84
85#[derive(Clone, Debug, Eq, PartialEq)]
86pub struct Pid1Config {
87 pub enabled: bool,
88}
89impl Default for Pid1Config {
90 fn default() -> Self {
91 Pid1Config { enabled: true }
92 }
93}
94
95#[derive(Clone, Debug, Eq, PartialEq)]
96pub struct SystemStateConfig {
97 pub enabled: bool,
98 pub varlink: bool,
103}
104impl Default for SystemStateConfig {
105 fn default() -> Self {
106 SystemStateConfig {
107 enabled: true,
108 varlink: true,
109 }
110 }
111}
112
113#[derive(Clone, Debug, Eq, PartialEq)]
114pub struct TimersConfig {
115 pub enabled: bool,
116 pub allowlist: HashSet<String>,
117 pub blocklist: HashSet<String>,
118}
119impl Default for TimersConfig {
120 fn default() -> Self {
121 TimersConfig {
122 enabled: true,
123 allowlist: HashSet::new(),
124 blocklist: HashSet::new(),
125 }
126 }
127}
128
129#[derive(Clone, Debug, Eq, PartialEq)]
130pub struct UnitsConfig {
131 pub enabled: bool,
132 pub varlink: bool,
136 pub state_stats: bool,
137 pub state_stats_allowlist: HashSet<String>,
138 pub state_stats_blocklist: HashSet<String>,
139 pub state_stats_time_in_state: bool,
140 pub ignore_inactive_oneshot_services: bool,
141 pub unit_files: bool,
142 pub per_unit_concurrency: u64,
147 pub slowest_units_count: u64,
150}
151impl Default for UnitsConfig {
152 fn default() -> Self {
153 UnitsConfig {
154 enabled: true,
155 varlink: true,
156 state_stats: false,
157 state_stats_allowlist: HashSet::new(),
158 state_stats_blocklist: HashSet::new(),
159 state_stats_time_in_state: true,
160 ignore_inactive_oneshot_services: true,
161 unit_files: true,
162 per_unit_concurrency: 8,
163 slowest_units_count: 5,
164 }
165 }
166}
167
168#[derive(Clone, Debug, Eq, PartialEq)]
169pub struct MachinesConfig {
170 pub enabled: bool,
171 pub varlink: bool,
178 pub allowlist: HashSet<String>,
179 pub blocklist: HashSet<String>,
180}
181impl Default for MachinesConfig {
182 fn default() -> Self {
183 MachinesConfig {
184 enabled: true,
185 varlink: true,
186 allowlist: HashSet::new(),
187 blocklist: HashSet::new(),
188 }
189 }
190}
191
192#[derive(Clone, Debug, Eq, PartialEq)]
193pub struct DBusStatsConfig {
194 pub enabled: bool,
195 pub stale_fd_stats: bool,
196
197 pub user_stats: bool,
198 pub user_allowlist: HashSet<String>,
199 pub user_blocklist: HashSet<String>,
200
201 pub peer_stats: bool,
202 pub peer_well_known_names_only: bool,
203 pub peer_allowlist: HashSet<String>,
204 pub peer_blocklist: HashSet<String>,
205 pub peer_name_concurrency: u64,
210
211 pub cgroup_stats: bool,
212 pub cgroup_allowlist: HashSet<String>,
213 pub cgroup_blocklist: HashSet<String>,
214}
215impl Default for DBusStatsConfig {
216 fn default() -> Self {
217 DBusStatsConfig {
218 enabled: true,
219 stale_fd_stats: true,
220
221 user_stats: false,
222 user_allowlist: HashSet::new(),
223 user_blocklist: HashSet::new(),
224
225 peer_stats: false,
226 peer_well_known_names_only: false,
227 peer_allowlist: HashSet::new(),
228 peer_blocklist: HashSet::new(),
229 peer_name_concurrency: 8,
230
231 cgroup_stats: false,
232 cgroup_allowlist: HashSet::new(),
233 cgroup_blocklist: HashSet::new(),
234 }
235 }
236}
237
238#[derive(Clone, Debug, Eq, PartialEq)]
239pub struct BootBlameConfig {
240 pub enabled: bool,
241 pub varlink: bool,
245 pub cache_enabled: bool,
246 pub cache_dir: String,
247 pub num_slowest_units: u64,
248 pub allowlist: HashSet<String>,
249 pub blocklist: HashSet<String>,
250}
251impl Default for BootBlameConfig {
252 fn default() -> Self {
253 BootBlameConfig {
254 enabled: false,
255 varlink: true,
256 cache_enabled: true,
257 cache_dir: "/run/monitord".to_string(),
258 num_slowest_units: 5,
259 allowlist: HashSet::new(),
260 blocklist: HashSet::new(),
261 }
262 }
263}
264
265#[derive(Clone, Debug, Eq, PartialEq)]
266pub struct VerifyConfig {
267 pub enabled: bool,
268 pub varlink: bool,
272 pub allowlist: HashSet<String>,
273 pub blocklist: HashSet<String>,
274}
275impl Default for VerifyConfig {
276 fn default() -> Self {
277 VerifyConfig {
278 enabled: false,
279 varlink: true,
280 allowlist: HashSet::new(),
281 blocklist: HashSet::new(),
282 }
283 }
284}
285
286#[derive(Clone, Debug, Default, Eq, PartialEq)]
287pub struct VarlinkConfig {
288 pub enabled: bool,
292 pub no_fallback: bool,
298}
299
300#[derive(Clone, Debug, Default, Eq, PartialEq)]
303pub struct Config {
304 pub machines: MachinesConfig,
305 pub monitord: MonitordConfig,
306 pub networkd: NetworkdConfig,
307 pub pid1: Pid1Config,
308 pub services: HashSet<String>,
309 pub system_state: SystemStateConfig,
310 pub timers: TimersConfig,
311 pub units: UnitsConfig,
312 pub dbus_stats: DBusStatsConfig,
313 pub boot_blame: BootBlameConfig,
314 pub verify: VerifyConfig,
315 pub varlink: VarlinkConfig,
316}
317
318impl Config {
319 pub fn use_varlink(&self, sections: &[bool]) -> bool {
325 self.varlink.enabled && sections.iter().all(|toggle| *toggle)
326 }
327}
328
329impl TryFrom<Ini> for Config {
330 type Error = MonitordConfigError;
331
332 fn try_from(ini_config: Ini) -> Result<Self, MonitordConfigError> {
333 let mut config = Config::default();
334
335 if let Some(dbus_address) = ini_config.get("monitord", "dbus_address") {
337 config.monitord.dbus_address = dbus_address;
338 }
339 if let Ok(Some(dbus_timeout)) = ini_config.getuint("monitord", "dbus_timeout") {
340 config.monitord.dbus_timeout = dbus_timeout;
341 }
342 config.monitord.daemon = read_config_bool(&ini_config, "monitord", "daemon")?;
343 if let Ok(Some(daemon_stats_refresh_secs)) =
344 ini_config.getuint("monitord", "daemon_stats_refresh_secs")
345 {
346 config.monitord.daemon_stats_refresh_secs = daemon_stats_refresh_secs;
347 }
348 if let Some(key_prefix) = ini_config.get("monitord", "key_prefix") {
349 config.monitord.key_prefix = key_prefix;
350 }
351 let output_format_str = ini_config.get("monitord", "output_format").ok_or_else(|| {
352 MonitordConfigError::MissingKey {
353 section: "monitord".into(),
354 key: "output_format".into(),
355 }
356 })?;
357 config.monitord.output_format = MonitordOutputFormat::from_str(&output_format_str)
358 .map_err(|e| MonitordConfigError::InvalidValue {
359 section: "monitord".into(),
360 key: "output_format".into(),
361 reason: e.to_string(),
362 })?;
363
364 config.networkd.enabled = read_config_bool(&ini_config, "networkd", "enabled")?;
366 if let Some(varlink) = read_config_optional_bool(&ini_config, "networkd", "varlink")? {
367 config.networkd.varlink = varlink;
368 }
369 if let Some(link_state_dir) = ini_config.get("networkd", "link_state_dir") {
370 config.networkd.link_state_dir = link_state_dir.into();
371 }
372
373 config.pid1.enabled = read_config_bool(&ini_config, "pid1", "enabled")?;
375
376 let config_map = ini_config.get_map().unwrap_or(IndexMap::from([]));
378 if let Some(services) = config_map.get("services") {
379 config.services = services.keys().map(|s| s.to_string()).collect();
380 }
381
382 config.system_state.enabled = read_config_bool(&ini_config, "system-state", "enabled")?;
384 if let Some(varlink) = read_config_optional_bool(&ini_config, "system-state", "varlink")? {
385 config.system_state.varlink = varlink;
386 }
387
388 config.timers.enabled = read_config_bool(&ini_config, "timers", "enabled")?;
390 if let Some(timers_allowlist) = config_map.get("timers.allowlist") {
391 config.timers.allowlist = timers_allowlist.keys().map(|s| s.to_string()).collect();
392 }
393 if let Some(timers_blocklist) = config_map.get("timers.blocklist") {
394 config.timers.blocklist = timers_blocklist.keys().map(|s| s.to_string()).collect();
395 }
396
397 config.units.enabled = read_config_bool(&ini_config, "units", "enabled")?;
399 if let Some(varlink) = read_config_optional_bool(&ini_config, "units", "varlink")? {
400 config.units.varlink = varlink;
401 }
402 config.units.state_stats = read_config_bool(&ini_config, "units", "state_stats")?;
403 if let Some(state_stats_allowlist) = config_map.get("units.state_stats.allowlist") {
404 config.units.state_stats_allowlist = state_stats_allowlist
405 .keys()
406 .map(|s| s.to_string())
407 .collect();
408 }
409 if let Some(state_stats_blocklist) = config_map.get("units.state_stats.blocklist") {
410 config.units.state_stats_blocklist = state_stats_blocklist
411 .keys()
412 .map(|s| s.to_string())
413 .collect();
414 }
415 config.units.state_stats_time_in_state =
416 read_config_bool(&ini_config, "units", "state_stats_time_in_state")?;
417 if let Some(ignore_inactive_oneshot_services) =
418 read_config_optional_bool(&ini_config, "units", "ignore_inactive_oneshot_services")?
419 {
420 config.units.ignore_inactive_oneshot_services = ignore_inactive_oneshot_services;
421 }
422 if let Some(unit_files) = read_config_optional_bool(&ini_config, "units", "unit_files")? {
423 config.units.unit_files = unit_files;
424 }
425 if let Ok(Some(per_unit_concurrency)) = ini_config.getuint("units", "per_unit_concurrency")
426 {
427 config.units.per_unit_concurrency = per_unit_concurrency;
428 }
429 if let Ok(Some(slowest_units_count)) = ini_config.getuint("units", "slowest_units_count") {
430 config.units.slowest_units_count = slowest_units_count;
431 }
432
433 config.machines.enabled = read_config_bool(&ini_config, "machines", "enabled")?;
435 if let Some(varlink) = read_config_optional_bool(&ini_config, "machines", "varlink")? {
436 config.machines.varlink = varlink;
437 }
438 if let Some(machines_allowlist) = config_map.get("machines.allowlist") {
439 config.machines.allowlist = machines_allowlist.keys().map(|s| s.to_string()).collect();
440 }
441 if let Some(machines_blocklist) = config_map.get("machines.blocklist") {
442 config.machines.blocklist = machines_blocklist.keys().map(|s| s.to_string()).collect();
443 }
444
445 config.dbus_stats.enabled = read_config_bool(&ini_config, "dbus", "enabled")?;
447 if let Some(stale_fd_stats) =
448 read_config_optional_bool(&ini_config, "dbus", "stale_fd_stats")?
449 {
450 config.dbus_stats.stale_fd_stats = stale_fd_stats;
451 }
452
453 config.dbus_stats.user_stats = read_config_bool(&ini_config, "dbus", "user_stats")?;
454 if let Some(user_allowlist) = config_map.get("dbus.user.allowlist") {
455 config.dbus_stats.user_allowlist =
456 user_allowlist.keys().map(|s| s.to_string()).collect();
457 }
458 if let Some(user_blocklist) = config_map.get("dbus.user.blocklist") {
459 config.dbus_stats.user_blocklist =
460 user_blocklist.keys().map(|s| s.to_string()).collect();
461 }
462
463 config.dbus_stats.peer_stats = read_config_bool(&ini_config, "dbus", "peer_stats")?;
464 config.dbus_stats.peer_well_known_names_only =
465 read_config_bool(&ini_config, "dbus", "peer_well_known_names_only")?;
466 if let Some(peer_allowlist) = config_map.get("dbus.peer.allowlist") {
467 config.dbus_stats.peer_allowlist =
468 peer_allowlist.keys().map(|s| s.to_string()).collect();
469 }
470 if let Some(peer_blocklist) = config_map.get("dbus.peer.blocklist") {
471 config.dbus_stats.peer_blocklist =
472 peer_blocklist.keys().map(|s| s.to_string()).collect();
473 }
474 if let Ok(Some(peer_name_concurrency)) = ini_config.getuint("dbus", "peer_name_concurrency")
475 {
476 config.dbus_stats.peer_name_concurrency = peer_name_concurrency;
477 }
478
479 config.dbus_stats.cgroup_stats = read_config_bool(&ini_config, "dbus", "cgroup_stats")?;
480 if let Some(cgroup_allowlist) = config_map.get("dbus.cgroup.allowlist") {
481 config.dbus_stats.cgroup_allowlist =
482 cgroup_allowlist.keys().map(|s| s.to_string()).collect();
483 }
484 if let Some(cgroup_blocklist) = config_map.get("dbus.cgroup.blocklist") {
485 config.dbus_stats.cgroup_blocklist =
486 cgroup_blocklist.keys().map(|s| s.to_string()).collect();
487 }
488
489 config.boot_blame.enabled = read_config_bool(&ini_config, "boot", "enabled")?;
491 if let Some(varlink) = read_config_optional_bool(&ini_config, "boot", "varlink")? {
492 config.boot_blame.varlink = varlink;
493 }
494 if let Some(cache_enabled) =
495 read_config_optional_bool(&ini_config, "boot", "cache_enabled")?
496 {
497 config.boot_blame.cache_enabled = cache_enabled;
498 }
499 if let Some(cache_dir) = ini_config.get("boot", "cache_dir") {
500 config.boot_blame.cache_dir = cache_dir;
501 }
502 if let Ok(Some(num_slowest_units)) = ini_config.getuint("boot", "num_slowest_units") {
503 config.boot_blame.num_slowest_units = num_slowest_units;
504 }
505 if let Some(boot_allowlist) = config_map.get("boot.allowlist") {
506 config.boot_blame.allowlist = boot_allowlist.keys().map(|s| s.to_string()).collect();
507 }
508 if let Some(boot_blocklist) = config_map.get("boot.blocklist") {
509 config.boot_blame.blocklist = boot_blocklist.keys().map(|s| s.to_string()).collect();
510 }
511
512 config.verify.enabled = read_config_bool(&ini_config, "verify", "enabled")?;
514 if let Some(varlink) = read_config_optional_bool(&ini_config, "verify", "varlink")? {
515 config.verify.varlink = varlink;
516 }
517 if let Some(verify_allowlist) = config_map.get("verify.allowlist") {
518 config.verify.allowlist = verify_allowlist.keys().map(|s| s.to_string()).collect();
519 }
520 if let Some(verify_blocklist) = config_map.get("verify.blocklist") {
521 config.verify.blocklist = verify_blocklist.keys().map(|s| s.to_string()).collect();
522 }
523
524 config.varlink.enabled = read_config_bool(&ini_config, "varlink", "enabled")?;
526 if let Some(no_fallback) = read_config_optional_bool(&ini_config, "varlink", "no_fallback")?
527 {
528 config.varlink.no_fallback = no_fallback;
529 }
530 if config.varlink.no_fallback && !config.varlink.enabled {
534 warn!(
535 "[varlink] no_fallback=true has no effect while [varlink] enabled=false; \
536 collectors will use D-Bus without a word"
537 );
538 }
539
540 for entry in unknown_config_entries(&config_map) {
541 warn!("Ignoring {entry}; check for a typo'd key or section");
542 }
543
544 Ok(config)
545 }
546}
547
548const KNOWN_SECTION_KEYS: &[(&str, &[&str])] = &[
554 (
555 "monitord",
556 &[
557 "dbus_address",
558 "dbus_timeout",
559 "daemon",
560 "daemon_stats_refresh_secs",
561 "key_prefix",
562 "output_format",
563 ],
564 ),
565 ("networkd", &["enabled", "varlink", "link_state_dir"]),
566 ("pid1", &["enabled"]),
567 ("system-state", &["enabled", "varlink"]),
568 ("timers", &["enabled"]),
569 (
570 "units",
571 &[
572 "enabled",
573 "varlink",
574 "state_stats",
575 "state_stats_time_in_state",
576 "ignore_inactive_oneshot_services",
577 "unit_files",
578 "per_unit_concurrency",
579 "slowest_units_count",
580 ],
581 ),
582 ("machines", &["enabled", "varlink"]),
583 (
584 "dbus",
585 &[
586 "enabled",
587 "stale_fd_stats",
588 "user_stats",
589 "peer_stats",
590 "peer_well_known_names_only",
591 "peer_name_concurrency",
592 "cgroup_stats",
593 ],
594 ),
595 (
596 "boot",
597 &[
598 "enabled",
599 "varlink",
600 "cache_enabled",
601 "cache_dir",
602 "num_slowest_units",
603 ],
604 ),
605 ("verify", &["enabled", "varlink"]),
606 ("varlink", &["enabled", "no_fallback"]),
607];
608
609const DATA_SECTIONS: &[&str] = &[
611 "services",
612 "timers.allowlist",
613 "timers.blocklist",
614 "units.state_stats.allowlist",
615 "units.state_stats.blocklist",
616 "machines.allowlist",
617 "machines.blocklist",
618 "dbus.user.allowlist",
619 "dbus.user.blocklist",
620 "dbus.peer.allowlist",
621 "dbus.peer.blocklist",
622 "dbus.cgroup.allowlist",
623 "dbus.cgroup.blocklist",
624 "boot.allowlist",
625 "boot.blocklist",
626 "verify.allowlist",
627 "verify.blocklist",
628];
629
630fn unknown_config_entries(
634 config_map: &IndexMap<String, IndexMap<String, Option<String>>>,
635) -> Vec<String> {
636 let mut unknown = Vec::new();
637 for (section, keys) in config_map {
638 if DATA_SECTIONS.contains(§ion.as_str()) {
639 continue;
640 }
641 match KNOWN_SECTION_KEYS.iter().find(|(name, _)| name == section) {
642 None => unknown.push(format!("unknown section [{section}]")),
643 Some((_, known_keys)) => {
644 for key in keys.keys() {
645 if !known_keys.contains(&key.as_str()) {
646 unknown.push(format!("unknown key '{key}' in [{section}]"));
647 }
648 }
649 }
650 }
651 }
652 unknown
653}
654
655fn read_config_bool(config: &Ini, section: &str, key: &str) -> Result<bool, MonitordConfigError> {
657 let option_bool =
658 config
659 .getbool(section, key)
660 .map_err(|err| MonitordConfigError::InvalidValue {
661 section: section.into(),
662 key: key.into(),
663 reason: err,
664 })?;
665 match option_bool {
666 Some(bool_value) => Ok(bool_value),
667 None => {
668 error!(
669 "No value for '{}' in '{}' section ... assuming false",
670 key, section
671 );
672 Ok(false)
673 }
674 }
675}
676
677fn read_config_optional_bool(
679 config: &Ini,
680 section: &str,
681 key: &str,
682) -> Result<Option<bool>, MonitordConfigError> {
683 config
684 .getbool(section, key)
685 .map_err(|err| MonitordConfigError::InvalidValue {
686 section: section.into(),
687 key: key.into(),
688 reason: err,
689 })
690}
691
692#[cfg(test)]
693mod tests {
694 use std::io::Write;
695
696 use tempfile::NamedTempFile;
697
698 use super::*;
699
700 const FULL_CONFIG: &str = r###"
701[monitord]
702dbus_address = unix:path=/system_bus_socket
703dbus_timeout = 2
704daemon = true
705daemon_stats_refresh_secs = 0
706key_prefix = unittest
707output_format = json-pretty
708
709[networkd]
710enabled = true
711varlink = false
712link_state_dir = /links
713
714[pid1]
715enabled = true
716
717[services]
718foo.service
719bar.service
720
721[system-state]
722enabled = true
723varlink = false
724
725[timers]
726enabled = true
727
728[timers.allowlist]
729foo.timer
730
731[timers.blocklist]
732bar.timer
733
734[units]
735enabled = true
736varlink = false
737state_stats = true
738state_stats_time_in_state = true
739ignore_inactive_oneshot_services = true
740unit_files = true
741per_unit_concurrency = 16
742slowest_units_count = 3
743
744[units.state_stats.allowlist]
745foo.service
746
747[units.state_stats.blocklist]
748bar.service
749
750[machines]
751enabled = true
752varlink = false
753
754[machines.allowlist]
755foo
756bar
757
758[machines.blocklist]
759foo2
760
761[dbus]
762enabled = true
763stale_fd_stats = true
764user_stats = true
765peer_stats = true
766peer_well_known_names_only = true
767peer_name_concurrency = 12
768cgroup_stats = true
769
770[dbus.user.allowlist]
771foo
772bar
773
774[dbus.user.blocklist]
775foo2
776
777[dbus.peer.allowlist]
778foo
779bar
780
781[dbus.peer.blocklist]
782foo2
783
784[dbus.cgroup.allowlist]
785foo
786bar
787
788[dbus.cgroup.blocklist]
789foo2
790
791[boot]
792enabled = true
793varlink = true
794cache_enabled = false
795cache_dir = /tmp/monitord-test
796num_slowest_units = 10
797
798[boot.allowlist]
799foo.service
800
801[boot.blocklist]
802bar.service
803
804[verify]
805enabled = true
806varlink = false
807
808[varlink]
809enabled = true
810no_fallback = true
811"###;
812
813 const MINIMAL_CONFIG: &str = r###"
814[monitord]
815output_format = json-flat
816"###;
817
818 #[test]
819 fn test_default_config() {
820 assert!(Config::default().units.enabled);
821 let default_config = Config::default();
823 assert!(default_config.units.varlink);
824 assert!(default_config.networkd.varlink);
825 assert!(default_config.system_state.varlink);
826 assert!(default_config.machines.varlink);
827 assert!(default_config.boot_blame.varlink);
828 assert!(default_config.verify.varlink);
829 }
830
831 #[test]
832 fn test_minimal_config() {
833 let mut monitord_config = NamedTempFile::new().expect("Unable to make named tempfile");
834 monitord_config
835 .write_all(MINIMAL_CONFIG.as_bytes())
836 .expect("Unable to write out temp config file");
837
838 let mut ini_config = Ini::new();
839 let _config_map = ini_config
840 .load(monitord_config.path())
841 .expect("Unable to load ini config");
842
843 let expected_config: Config = ini_config.try_into().expect("Failed to parse config");
844 assert_eq!(
846 expected_config.monitord.output_format,
847 MonitordOutputFormat::JsonFlat,
848 );
849 assert!(!expected_config.networkd.enabled);
851 assert!(expected_config.boot_blame.cache_enabled);
853 assert!(expected_config.units.ignore_inactive_oneshot_services);
855 }
856
857 #[test]
858 fn test_units_ignore_inactive_oneshot_services_override() {
859 let units_override_config = r###"
860[monitord]
861output_format = json
862
863[units]
864ignore_inactive_oneshot_services = false
865"###;
866 let mut monitord_config = NamedTempFile::new().expect("Unable to make named tempfile");
867 monitord_config
868 .write_all(units_override_config.as_bytes())
869 .expect("Unable to write out temp config file");
870
871 let mut ini_config = Ini::new();
872 let _config_map = ini_config
873 .load(monitord_config.path())
874 .expect("Unable to load ini config");
875
876 let parsed_config: Config = ini_config.try_into().expect("Failed to parse config");
877 assert!(!parsed_config.units.ignore_inactive_oneshot_services);
878 }
879
880 #[test]
881 fn test_units_per_unit_concurrency_override() {
882 let units_override_config = r###"
883[monitord]
884output_format = json
885
886[units]
887per_unit_concurrency = 32
888slowest_units_count = 0
889"###;
890 let mut monitord_config = NamedTempFile::new().expect("Unable to make named tempfile");
891 monitord_config
892 .write_all(units_override_config.as_bytes())
893 .expect("Unable to write out temp config file");
894
895 let mut ini_config = Ini::new();
896 let _config_map = ini_config
897 .load(monitord_config.path())
898 .expect("Unable to load ini config");
899
900 let parsed_config: Config = ini_config.try_into().expect("Failed to parse config");
901 assert_eq!(parsed_config.units.per_unit_concurrency, 32);
902 assert_eq!(parsed_config.units.slowest_units_count, 0);
903 }
904
905 #[test]
906 fn test_use_varlink_conjunction() {
907 let mut config = Config::default();
912 assert!(!config.use_varlink(&[true]));
913 config.varlink.enabled = true;
914 assert!(config.use_varlink(&[true]));
915 assert!(!config.use_varlink(&[false]));
916 assert!(!config.use_varlink(&[true, false]));
917 assert!(config.use_varlink(&[true, true]));
918 config.varlink.enabled = false;
919 assert!(!config.use_varlink(&[true, true]));
920 }
921
922 #[test]
923 fn test_unknown_config_entries() {
924 let typo_config = r###"
928[monitord]
929output_format = json
930
931[units]
932varlnik = false
933
934[timers]
935varlink = false
936
937[services]
938foo.service
939
940[bogus]
941key = value
942"###;
943 let mut monitord_config = NamedTempFile::new().expect("Unable to make named tempfile");
944 monitord_config
945 .write_all(typo_config.as_bytes())
946 .expect("Unable to write out temp config file");
947
948 let mut ini_config = Ini::new();
949 ini_config
950 .load(monitord_config.path())
951 .expect("Unable to load ini config");
952
953 let unknown = unknown_config_entries(&ini_config.get_map().expect("config map"));
954 assert!(unknown.contains(&"unknown key 'varlnik' in [units]".to_string()));
955 assert!(unknown.contains(&"unknown key 'varlink' in [timers]".to_string()));
956 assert!(unknown.contains(&"unknown section [bogus]".to_string()));
957 assert_eq!(unknown.len(), 3);
958 }
959
960 #[test]
961 fn test_no_fallback_parses_and_defaults_off() {
962 let on = r###"
965[monitord]
966output_format = json
967
968[varlink]
969enabled = true
970no_fallback = true
971"###;
972 let mut f = NamedTempFile::new().expect("tempfile");
973 f.write_all(on.as_bytes()).expect("write");
974 let mut ini = Ini::new();
975 ini.load(f.path()).expect("load");
976 let parsed: Config = ini.try_into().expect("parse");
977 assert!(parsed.varlink.no_fallback);
978
979 let off = r###"
980[monitord]
981output_format = json
982
983[varlink]
984enabled = true
985"###;
986 let mut f = NamedTempFile::new().expect("tempfile");
987 f.write_all(off.as_bytes()).expect("write");
988 let mut ini = Ini::new();
989 ini.load(f.path()).expect("load");
990 let parsed: Config = ini.try_into().expect("parse");
991 assert!(!parsed.varlink.no_fallback);
992 }
993
994 #[test]
995 fn test_per_section_varlink_toggle_defaults_and_override() {
996 let varlink_override_config = r###"
999[monitord]
1000output_format = json
1001
1002[varlink]
1003enabled = true
1004
1005[units]
1006varlink = false
1007"###;
1008 let mut monitord_config = NamedTempFile::new().expect("Unable to make named tempfile");
1009 monitord_config
1010 .write_all(varlink_override_config.as_bytes())
1011 .expect("Unable to write out temp config file");
1012
1013 let mut ini_config = Ini::new();
1014 let _config_map = ini_config
1015 .load(monitord_config.path())
1016 .expect("Unable to load ini config");
1017
1018 let parsed_config: Config = ini_config.try_into().expect("Failed to parse config");
1019 assert!(parsed_config.varlink.enabled);
1020 assert!(!parsed_config.units.varlink);
1021 assert!(parsed_config.networkd.varlink);
1022 assert!(parsed_config.system_state.varlink);
1023 assert!(parsed_config.machines.varlink);
1024 assert!(parsed_config.boot_blame.varlink);
1025 assert!(parsed_config.verify.varlink);
1026 }
1027
1028 #[test]
1029 fn test_full_config() {
1030 let expected_config = Config {
1031 monitord: MonitordConfig {
1032 dbus_address: String::from("unix:path=/system_bus_socket"),
1033 daemon: true,
1034 daemon_stats_refresh_secs: u64::MIN,
1035 key_prefix: String::from("unittest"),
1036 output_format: MonitordOutputFormat::JsonPretty,
1037 dbus_timeout: 2 as u64,
1038 },
1039 networkd: NetworkdConfig {
1040 enabled: true,
1041 varlink: false,
1042 link_state_dir: "/links".into(),
1043 },
1044 pid1: Pid1Config { enabled: true },
1045 services: HashSet::from([String::from("foo.service"), String::from("bar.service")]),
1046 system_state: SystemStateConfig {
1047 enabled: true,
1048 varlink: false,
1049 },
1050 timers: TimersConfig {
1051 enabled: true,
1052 allowlist: HashSet::from([String::from("foo.timer")]),
1053 blocklist: HashSet::from([String::from("bar.timer")]),
1054 },
1055 units: UnitsConfig {
1056 enabled: true,
1057 varlink: false,
1058 state_stats: true,
1059 state_stats_allowlist: HashSet::from([String::from("foo.service")]),
1060 state_stats_blocklist: HashSet::from([String::from("bar.service")]),
1061 state_stats_time_in_state: true,
1062 ignore_inactive_oneshot_services: true,
1063 unit_files: true,
1064 per_unit_concurrency: 16,
1065 slowest_units_count: 3,
1066 },
1067 machines: MachinesConfig {
1068 enabled: true,
1069 varlink: false,
1070 allowlist: HashSet::from([String::from("foo"), String::from("bar")]),
1071 blocklist: HashSet::from([String::from("foo2")]),
1072 },
1073 dbus_stats: DBusStatsConfig {
1074 enabled: true,
1075 stale_fd_stats: true,
1076 user_stats: true,
1077 user_allowlist: HashSet::from([String::from("foo"), String::from("bar")]),
1078 user_blocklist: HashSet::from([String::from("foo2")]),
1079 peer_stats: true,
1080 peer_well_known_names_only: true,
1081 peer_allowlist: HashSet::from([String::from("foo"), String::from("bar")]),
1082 peer_blocklist: HashSet::from([String::from("foo2")]),
1083 peer_name_concurrency: 12,
1084 cgroup_stats: true,
1085 cgroup_allowlist: HashSet::from([String::from("foo"), String::from("bar")]),
1086 cgroup_blocklist: HashSet::from([String::from("foo2")]),
1087 },
1088 boot_blame: BootBlameConfig {
1089 enabled: true,
1090 varlink: true,
1094 cache_enabled: false,
1095 cache_dir: "/tmp/monitord-test".to_string(),
1096 num_slowest_units: 10,
1097 allowlist: HashSet::from([String::from("foo.service")]),
1098 blocklist: HashSet::from([String::from("bar.service")]),
1099 },
1100 verify: VerifyConfig {
1101 enabled: true,
1102 varlink: false,
1103 allowlist: HashSet::new(),
1104 blocklist: HashSet::new(),
1105 },
1106 varlink: VarlinkConfig {
1107 enabled: true,
1108 no_fallback: true,
1109 },
1110 };
1111
1112 let mut monitord_config = NamedTempFile::new().expect("Unable to make named tempfile");
1113 monitord_config
1114 .write_all(FULL_CONFIG.as_bytes())
1115 .expect("Unable to write out temp config file");
1116
1117 let mut ini_config = Ini::new();
1118 let _config_map = ini_config
1119 .load(monitord_config.path())
1120 .expect("Unable to load ini config");
1121
1122 let actual_config: Config = ini_config.try_into().expect("Failed to parse config");
1124 assert_eq!(expected_config, actual_config);
1125 }
1126
1127 #[test]
1128 fn test_invalid_config_returns_error() {
1129 let invalid_config = "[monitord]\ndaemon = notabool\noutput_format = json\n";
1130 let mut monitord_config = NamedTempFile::new().expect("Unable to make named tempfile");
1131 monitord_config
1132 .write_all(invalid_config.as_bytes())
1133 .expect("Unable to write out temp config file");
1134
1135 let mut ini_config = Ini::new();
1136 let _config_map = ini_config
1137 .load(monitord_config.path())
1138 .expect("Unable to load ini config");
1139
1140 let result: Result<Config, _> = ini_config.try_into();
1141 assert!(result.is_err());
1142 }
1143}