Skip to main content

sysknife_daemon/actions/
logging.rs

1//! Log-management actions: logrotate drop-ins + rsyslog remote forwarding.
2//!
3//! `GetLogrotateStatus` is a read-only dry-run (`logrotate -d`). The mutating
4//! actions delegate to the root-owned helper `/usr/lib/sysknife/log-edit`, which
5//! validates each config before keeping it (`logrotate -d` for rotation,
6//! `rsyslogd -N1` for forwarding) and rolls back on rejection.
7
8use super::{command_mechanism, ActionSpec};
9use sysknife_types::RiskLevel;
10
11const HELPER: &str = "/usr/lib/sysknife/log-edit";
12
13pub fn specs() -> Vec<ActionSpec> {
14    vec![
15        get_logrotate_status(None),
16        configure_log_rotation("nginx", "/var/log/nginx/*.log", "daily", 14, true),
17        remove_log_rotation("nginx"),
18        configure_remote_syslog("logs.example.com", 514, "tcp"),
19        remove_remote_syslog(),
20    ]
21}
22
23/// Dry-run logrotate to show what would rotate (`logrotate -d <conf>`).
24/// Read-only — `-d` (debug) never actually rotates. Defaults to the main config.
25pub fn get_logrotate_status(config: Option<&str>) -> ActionSpec {
26    let conf = config.unwrap_or("/etc/logrotate.conf");
27    ActionSpec {
28        action_name: "GetLogrotateStatus",
29        mechanism: command_mechanism("logrotate", ["-d", conf]),
30        risk_level: RiskLevel::Low,
31        reboot_required: false,
32        rollback_available: false,
33    }
34}
35
36/// Write a logrotate drop-in (`log-edit --op logrotate …`).
37pub fn configure_log_rotation(
38    name: &str,
39    path: &str,
40    frequency: &str,
41    rotate: u32,
42    compress: bool,
43) -> ActionSpec {
44    let rot = rotate.to_string();
45    let mut args = vec![
46        HELPER.to_string(),
47        "--op".to_string(),
48        "logrotate".to_string(),
49        "--name".to_string(),
50        name.to_string(),
51        "--path".to_string(),
52        path.to_string(),
53        "--frequency".to_string(),
54        frequency.to_string(),
55        "--rotate".to_string(),
56        rot,
57    ];
58    if compress {
59        args.push("--compress".to_string());
60    }
61    ActionSpec {
62        action_name: "ConfigureLogRotation",
63        mechanism: command_mechanism("sudo", args),
64        risk_level: RiskLevel::Medium,
65        reboot_required: false,
66        rollback_available: false,
67    }
68}
69
70/// Remove a SysKnife-managed logrotate drop-in.
71pub fn remove_log_rotation(name: &str) -> ActionSpec {
72    ActionSpec {
73        action_name: "RemoveLogRotation",
74        mechanism: command_mechanism("sudo", [HELPER, "--op", "rm-logrotate", "--name", name]),
75        risk_level: RiskLevel::Medium,
76        reboot_required: false,
77        rollback_available: false,
78    }
79}
80
81/// Configure rsyslog to forward all logs to a remote collector.
82pub fn configure_remote_syslog(host: &str, port: u16, protocol: &str) -> ActionSpec {
83    let p = port.to_string();
84    ActionSpec {
85        action_name: "ConfigureRemoteSyslog",
86        mechanism: command_mechanism(
87            "sudo",
88            [
89                HELPER,
90                "--op",
91                "rsyslog-forward",
92                "--host",
93                host,
94                "--port",
95                &p,
96                "--protocol",
97                protocol,
98            ],
99        ),
100        risk_level: RiskLevel::High,
101        reboot_required: false,
102        rollback_available: false,
103    }
104}
105
106/// Remove the rsyslog remote-forwarding drop-in.
107pub fn remove_remote_syslog() -> ActionSpec {
108    ActionSpec {
109        action_name: "RemoveRemoteSyslog",
110        mechanism: command_mechanism("sudo", [HELPER, "--op", "rm-forward"]),
111        risk_level: RiskLevel::High,
112        reboot_required: false,
113        rollback_available: false,
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use crate::actions::ActionMechanism;
121
122    fn args_of(spec: &ActionSpec) -> (&'static str, Vec<String>) {
123        match &spec.mechanism {
124            ActionMechanism::Command { program, args } => (program, args.clone()),
125            other => panic!("expected Command, got {other:?}"),
126        }
127    }
128
129    #[test]
130    fn logrotate_status_dry_run() {
131        let (program, args) = args_of(&get_logrotate_status(None));
132        assert_eq!(program, "logrotate");
133        assert_eq!(args, vec!["-d", "/etc/logrotate.conf"]);
134        assert_eq!(get_logrotate_status(None).risk_level, RiskLevel::Low);
135    }
136
137    #[test]
138    fn configure_rotation_with_compress() {
139        let (program, args) = args_of(&configure_log_rotation(
140            "nginx",
141            "/var/log/nginx/*.log",
142            "daily",
143            14,
144            true,
145        ));
146        assert_eq!(program, "sudo");
147        assert_eq!(
148            args,
149            vec![
150                HELPER,
151                "--op",
152                "logrotate",
153                "--name",
154                "nginx",
155                "--path",
156                "/var/log/nginx/*.log",
157                "--frequency",
158                "daily",
159                "--rotate",
160                "14",
161                "--compress"
162            ]
163        );
164        let (_, no_comp) = args_of(&configure_log_rotation(
165            "n",
166            "/var/log/x",
167            "weekly",
168            4,
169            false,
170        ));
171        assert!(!no_comp.iter().any(|a| a == "--compress"));
172    }
173
174    #[test]
175    fn syslog_forward_tcp_and_removes() {
176        let (_, fwd) = args_of(&configure_remote_syslog("logs.example.com", 514, "tcp"));
177        assert_eq!(
178            fwd,
179            vec![
180                HELPER,
181                "--op",
182                "rsyslog-forward",
183                "--host",
184                "logs.example.com",
185                "--port",
186                "514",
187                "--protocol",
188                "tcp"
189            ]
190        );
191        assert_eq!(
192            configure_remote_syslog("h", 1, "udp").risk_level,
193            RiskLevel::High
194        );
195        let (_, rm) = args_of(&remove_remote_syslog());
196        assert_eq!(rm, vec![HELPER, "--op", "rm-forward"]);
197    }
198}