1use super::{command_mechanism, ActionSpec};
2use sysknife_types::RiskLevel;
3
4pub fn specs() -> Vec<ActionSpec> {
5 vec![
6 list_services(),
7 start_service("NetworkManager.service"),
8 stop_service("NetworkManager.service"),
9 restart_service("NetworkManager.service"),
10 set_service_enabled("sshd.service", true),
11 mask_service("cups.service"),
12 unmask_service("cups.service"),
13 get_service_logs("NetworkManager.service"),
14 get_service_status("nginx.service"),
15 reload_service("nginx.service"),
16 list_timers(),
17 reload_daemon(),
18 create_scheduled_job("example", "/usr/bin/true", "*-*-* 02:00:00"),
19 get_service_resource_limits("nginx.service"),
20 set_service_resource_limits(
21 "nginx.service",
22 &["MemoryMax=500M".to_string(), "CPUQuota=50%".to_string()],
23 ),
24 ]
25}
26
27pub fn get_service_resource_limits(unit: &str) -> ActionSpec {
32 ActionSpec {
33 action_name: "GetServiceResourceLimits",
34 mechanism: command_mechanism(
35 "systemctl",
36 [
37 "show",
38 unit,
39 "--property=MemoryMax,MemoryHigh,CPUQuotaPerSecUSec,TasksMax",
40 ],
41 ),
42 risk_level: RiskLevel::Low,
43 reboot_required: false,
44 rollback_available: false,
45 }
46}
47
48pub fn set_service_resource_limits(unit: &str, assignments: &[String]) -> ActionSpec {
58 let mut args = vec![
59 "systemctl".to_string(),
60 "set-property".to_string(),
61 unit.to_string(),
62 ];
63 args.extend(assignments.iter().cloned());
64 ActionSpec {
65 action_name: "SetServiceResourceLimits",
66 mechanism: command_mechanism("sudo", args),
67 risk_level: RiskLevel::Medium,
68 reboot_required: false,
69 rollback_available: false,
70 }
71}
72
73const SCHEDULED_JOB_HELPER: &str = "/usr/lib/sysknife/scheduled-job-edit";
77const SCHEDULED_JOB_UNIT_DIR: &str = "/etc/systemd/system";
78const SCHEDULED_JOB_UNIT_PREFIX: &str = "sysknife-";
79
80pub fn scheduled_job_unit_paths(name: &str) -> Option<(String, String)> {
85 if name.is_empty()
86 || name.len() > 64
87 || !name
88 .chars()
89 .next()
90 .is_some_and(|c| c.is_ascii_alphanumeric())
91 || !name
92 .chars()
93 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
94 {
95 return None;
96 }
97
98 let stem = format!("{SCHEDULED_JOB_UNIT_PREFIX}{name}");
99 Some((
100 format!("{SCHEDULED_JOB_UNIT_DIR}/{stem}.service"),
101 format!("{SCHEDULED_JOB_UNIT_DIR}/{stem}.timer"),
102 ))
103}
104
105const ACTION_STEPS: &str = "/usr/lib/sysknife/action-steps";
114
115pub fn create_scheduled_job(name: &str, command: &str, schedule: &str) -> ActionSpec {
125 ActionSpec {
126 action_name: "CreateScheduledJob",
127 mechanism: command_mechanism(
128 "sudo",
129 [
130 SCHEDULED_JOB_HELPER,
131 "--name",
132 name,
133 "--command",
134 command,
135 "--schedule",
136 schedule,
137 ],
138 ),
139 risk_level: RiskLevel::High,
140 reboot_required: false,
141 rollback_available: false,
142 }
143}
144
145pub fn list_services() -> ActionSpec {
146 ActionSpec {
147 action_name: "ListServices",
148 mechanism: command_mechanism(
149 "systemctl",
150 [
151 "list-units",
152 "--type=service",
153 "--all",
154 "--no-legend",
155 "--no-pager",
156 ],
157 ),
158 risk_level: RiskLevel::Low,
159 reboot_required: false,
160 rollback_available: false,
161 }
162}
163
164pub fn start_service(unit: &str) -> ActionSpec {
165 ActionSpec {
166 action_name: "StartService",
167 mechanism: command_mechanism("sudo", [ACTION_STEPS, "unit", "start", unit]),
168 risk_level: RiskLevel::Medium,
169 reboot_required: false,
170 rollback_available: false,
171 }
172}
173
174pub fn stop_service(unit: &str) -> ActionSpec {
175 ActionSpec {
176 action_name: "StopService",
177 mechanism: command_mechanism("sudo", [ACTION_STEPS, "unit", "stop", unit]),
178 risk_level: RiskLevel::Medium,
179 reboot_required: false,
180 rollback_available: false,
181 }
182}
183
184pub fn restart_service(unit: &str) -> ActionSpec {
185 ActionSpec {
186 action_name: "RestartService",
187 mechanism: command_mechanism("sudo", [ACTION_STEPS, "unit", "restart", unit]),
188 risk_level: RiskLevel::Medium,
189 reboot_required: false,
190 rollback_available: false,
191 }
192}
193
194pub fn set_service_enabled(unit: &str, enabled: bool) -> ActionSpec {
195 let verb = if enabled { "enable" } else { "disable" };
196
197 ActionSpec {
198 action_name: "SetServiceEnabled",
199 mechanism: command_mechanism("sudo", [ACTION_STEPS, "unit", verb, unit]),
200 risk_level: RiskLevel::Medium,
201 reboot_required: false,
202 rollback_available: false,
203 }
204}
205
206pub fn mask_service(unit: &str) -> ActionSpec {
207 ActionSpec {
208 action_name: "MaskService",
209 mechanism: command_mechanism("sudo", [ACTION_STEPS, "unit", "mask", unit]),
210 risk_level: RiskLevel::High,
211 reboot_required: false,
212 rollback_available: false,
213 }
214}
215
216pub fn unmask_service(unit: &str) -> ActionSpec {
217 ActionSpec {
218 action_name: "UnmaskService",
219 mechanism: command_mechanism("sudo", [ACTION_STEPS, "unit", "unmask", unit]),
220 risk_level: RiskLevel::Medium,
221 reboot_required: false,
222 rollback_available: false,
223 }
224}
225
226pub fn get_service_status(unit: &str) -> ActionSpec {
227 ActionSpec {
231 action_name: "GetServiceStatus",
232 mechanism: command_mechanism("systemctl", ["status", unit, "--no-pager"]),
233 risk_level: RiskLevel::Low,
234 reboot_required: false,
235 rollback_available: false,
236 }
237}
238
239pub fn reload_service(unit: &str) -> ActionSpec {
240 ActionSpec {
246 action_name: "ReloadService",
247 mechanism: command_mechanism("sudo", [ACTION_STEPS, "unit", "reload", unit]),
248 risk_level: RiskLevel::Medium,
249 reboot_required: false,
250 rollback_available: false,
251 }
252}
253
254pub fn list_timers() -> ActionSpec {
255 ActionSpec {
259 action_name: "ListTimers",
260 mechanism: command_mechanism(
261 "systemctl",
262 ["list-timers", "--all", "--no-legend", "--no-pager"],
263 ),
264 risk_level: RiskLevel::Low,
265 reboot_required: false,
266 rollback_available: false,
267 }
268}
269
270pub fn reload_daemon() -> ActionSpec {
271 ActionSpec {
276 action_name: "ReloadDaemon",
277 mechanism: command_mechanism("sudo", ["systemctl", "daemon-reload"]),
278 risk_level: RiskLevel::Medium,
279 reboot_required: false,
280 rollback_available: false,
281 }
282}
283
284pub fn get_service_logs(unit: &str) -> ActionSpec {
285 ActionSpec {
286 action_name: "GetServiceLogs",
287 mechanism: command_mechanism("journalctl", ["-u", unit, "-n", "200", "--no-pager"]),
288 risk_level: RiskLevel::Low,
289 reboot_required: false,
290 rollback_available: false,
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297 use crate::actions::ActionMechanism;
298
299 fn args_of(spec: &ActionSpec) -> (&'static str, Vec<String>) {
300 match &spec.mechanism {
301 ActionMechanism::Command { program, args } => (program, args.clone()),
302 other => panic!("expected Command, got {other:?}"),
303 }
304 }
305
306 #[test]
307 fn get_resource_limits_is_read_only_show() {
308 let spec = get_service_resource_limits("nginx.service");
309 let (program, args) = args_of(&spec);
310 assert_eq!(program, "systemctl");
311 assert_eq!(args[0], "show");
312 assert_eq!(args[1], "nginx.service");
313 assert!(args[2].contains("MemoryMax") && args[2].contains("CPUQuotaPerSecUSec"));
314 assert_eq!(spec.risk_level, RiskLevel::Low);
315 }
316
317 #[test]
318 fn set_resource_limits_builds_set_property() {
319 let spec = set_service_resource_limits(
320 "nginx.service",
321 &["MemoryMax=500M".to_string(), "CPUQuota=50%".to_string()],
322 );
323 let (program, args) = args_of(&spec);
324 assert_eq!(program, "sudo");
325 assert_eq!(
326 args,
327 vec![
328 "systemctl",
329 "set-property",
330 "nginx.service",
331 "MemoryMax=500M",
332 "CPUQuota=50%"
333 ]
334 );
335 assert_eq!(spec.risk_level, RiskLevel::Medium);
336 }
337}