sysknife_daemon/actions/
journald.rs1use super::{command_mechanism, ActionSpec};
15use sysknife_types::RiskLevel;
16
17pub fn specs() -> Vec<ActionSpec> {
18 vec![
19 get_journal_log(&JournalQuery {
20 lines: 100,
21 unit: Some("ssh.service"),
22 priority: Some("err"),
23 boot: true,
24 ..JournalQuery::default()
25 }),
26 vacuum_journal_by_size(500),
27 ]
28}
29
30#[derive(Default)]
33pub struct JournalQuery<'a> {
34 pub lines: u32,
36 pub unit: Option<&'a str>,
38 pub priority: Option<&'a str>,
40 pub boot: bool,
42 pub kernel: bool,
44 pub since: Option<&'a str>,
46 pub until: Option<&'a str>,
48 pub grep: Option<&'a str>,
50}
51
52pub fn get_journal_log(q: &JournalQuery) -> ActionSpec {
58 let mut args = vec![
59 "--output=json".to_string(),
60 "--no-pager".to_string(),
61 format!("--lines={}", q.lines),
62 ];
63 if let Some(unit) = q.unit {
64 args.push(format!("--unit={unit}"));
65 }
66 if let Some(priority) = q.priority {
67 args.push(format!("--priority={priority}"));
68 }
69 if q.boot {
70 args.push("--boot".to_string());
71 }
72 if q.kernel {
73 args.push("--dmesg".to_string());
74 }
75 if let Some(since) = q.since {
76 args.push(format!("--since={since}"));
77 }
78 if let Some(until) = q.until {
79 args.push(format!("--until={until}"));
80 }
81 if let Some(grep) = q.grep {
82 args.push(format!("--grep={grep}"));
83 }
84 ActionSpec {
85 action_name: "GetJournalLog",
86 mechanism: command_mechanism("journalctl", args),
87 risk_level: RiskLevel::Low,
88 reboot_required: false,
89 rollback_available: false,
90 }
91}
92
93pub fn vacuum_journal_by_size(megabytes: u32) -> ActionSpec {
95 vacuum_journal(format!("--vacuum-size={megabytes}M"))
96}
97
98pub fn vacuum_journal_by_time(days: u32) -> ActionSpec {
100 vacuum_journal(format!("--vacuum-time={days}d"))
101}
102
103fn vacuum_journal(vacuum_flag: String) -> ActionSpec {
104 ActionSpec {
105 action_name: "VacuumJournal",
106 mechanism: command_mechanism("journalctl", [vacuum_flag]),
107 risk_level: RiskLevel::High,
108 reboot_required: false,
109 rollback_available: false,
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use crate::actions::ActionMechanism;
117
118 fn args_of(spec: &ActionSpec) -> (&'static str, Vec<String>) {
119 match &spec.mechanism {
120 ActionMechanism::Command { program, args } => (program, args.clone()),
121 other => panic!("expected Command, got {other:?}"),
122 }
123 }
124
125 #[test]
126 fn get_journal_log_builds_attached_filters() {
127 let spec = get_journal_log(&JournalQuery {
128 lines: 50,
129 unit: Some("nginx.service"),
130 priority: Some("err"),
131 boot: true,
132 kernel: false,
133 since: Some("2026-07-22 10:00:00"),
134 until: None,
135 grep: Some("timeout"),
136 });
137 let (program, args) = args_of(&spec);
138 assert_eq!(program, "journalctl");
139 assert_eq!(args[0], "--output=json");
140 assert!(args.contains(&"--lines=50".to_string()));
141 assert!(args.contains(&"--unit=nginx.service".to_string()));
142 assert!(args.contains(&"--priority=err".to_string()));
143 assert!(args.contains(&"--boot".to_string()));
144 assert!(args.contains(&"--since=2026-07-22 10:00:00".to_string()));
145 assert!(args.contains(&"--grep=timeout".to_string()));
146 assert!(!args.iter().any(|a| a.starts_with("--until=")));
148 assert!(!args.iter().any(|a| a == "--dmesg"));
149 assert_eq!(spec.risk_level, RiskLevel::Low);
150 }
151
152 #[test]
153 fn kernel_query_uses_dmesg() {
154 let spec = get_journal_log(&JournalQuery {
155 lines: 10,
156 kernel: true,
157 ..JournalQuery::default()
158 });
159 let (_, args) = args_of(&spec);
160 assert!(args.contains(&"--dmesg".to_string()));
161 }
162
163 #[test]
164 fn vacuum_by_size_and_time() {
165 let (_, size_args) = args_of(&vacuum_journal_by_size(200));
166 assert_eq!(size_args, vec!["--vacuum-size=200M"]);
167 let (_, time_args) = args_of(&vacuum_journal_by_time(7));
168 assert_eq!(time_args, vec!["--vacuum-time=7d"]);
169 assert_eq!(vacuum_journal_by_size(1).risk_level, RiskLevel::High);
170 }
171}