sysknife_daemon/actions/
resolvectl.rs1use std::net::IpAddr;
8
9use super::{command_mechanism, ActionSpec};
10use sysknife_types::RiskLevel;
11
12pub fn specs() -> Vec<ActionSpec> {
18 use std::str::FromStr;
19 vec![
20 resolvectl_status(),
21 resolvectl_set_dns(
22 "eth0",
23 &[
24 IpAddr::from_str("1.1.1.1").unwrap(),
25 IpAddr::from_str("8.8.8.8").unwrap(),
26 ],
27 ),
28 ]
29}
30
31pub fn resolvectl_status() -> ActionSpec {
40 ActionSpec {
41 action_name: "ResolvectlStatus",
42 mechanism: command_mechanism("resolvectl", ["status"]),
43 risk_level: RiskLevel::Low,
44 reboot_required: false,
45 rollback_available: false,
46 }
47}
48
49pub fn resolvectl_set_dns(interface: &str, servers: &[IpAddr]) -> ActionSpec {
60 let mut args = vec!["dns".to_string(), interface.to_string()];
62 args.extend(servers.iter().map(|ip| ip.to_string()));
63 ActionSpec {
64 action_name: "ResolvectlSetDns",
65 mechanism: super::ActionMechanism::Command {
66 program: "sudo",
67 args: {
68 let mut full = vec!["resolvectl".to_string()];
69 full.extend(args);
70 full
71 },
72 },
73 risk_level: RiskLevel::High,
79 reboot_required: false,
80 rollback_available: false,
81 }
82}
83
84#[cfg(test)]
89mod tests {
90 use super::*;
91 use crate::actions::ActionMechanism;
92 use std::str::FromStr;
93
94 fn extract_args(spec: &ActionSpec) -> (&'static str, Vec<String>) {
95 match &spec.mechanism {
96 ActionMechanism::Command { program, args } => (*program, args.clone()),
97 _ => panic!("expected Command mechanism"),
98 }
99 }
100
101 #[test]
104 fn resolvectl_status_action_name() {
105 assert_eq!(resolvectl_status().action_name, "ResolvectlStatus");
106 }
107
108 #[test]
109 fn resolvectl_status_risk_is_low() {
110 assert_eq!(resolvectl_status().risk_level, RiskLevel::Low);
111 }
112
113 #[test]
114 fn resolvectl_status_argv() {
115 let spec = resolvectl_status();
116 let (prog, args) = extract_args(&spec);
117 assert_eq!(prog, "resolvectl");
118 assert_eq!(args, vec!["status"]);
119 }
120
121 #[test]
122 fn resolvectl_status_no_sudo() {
123 let (prog, _) = extract_args(&resolvectl_status());
124 assert_ne!(prog, "sudo");
125 }
126
127 fn ip(s: &str) -> IpAddr {
130 IpAddr::from_str(s).expect("test fixture must be a valid IP")
131 }
132
133 #[test]
134 fn resolvectl_set_dns_action_name() {
135 assert_eq!(
136 resolvectl_set_dns("eth0", &[ip("1.1.1.1")]).action_name,
137 "ResolvectlSetDns"
138 );
139 }
140
141 #[test]
142 fn resolvectl_set_dns_risk_is_high() {
143 assert_eq!(
145 resolvectl_set_dns("eth0", &[ip("1.1.1.1")]).risk_level,
146 RiskLevel::High
147 );
148 }
149
150 #[test]
151 fn resolvectl_set_dns_argv_ordering() {
152 let spec = resolvectl_set_dns("eth0", &[ip("1.1.1.1"), ip("8.8.8.8")]);
154 let (prog, args) = extract_args(&spec);
155 assert_eq!(prog, "sudo");
156 let a: Vec<&str> = args.iter().map(String::as_str).collect();
157 assert_eq!(a[0], "resolvectl");
158 assert_eq!(a[1], "dns");
159 assert_eq!(a[2], "eth0");
160 assert_eq!(a[3], "1.1.1.1");
161 assert_eq!(a[4], "8.8.8.8");
162 }
163
164 #[test]
165 fn resolvectl_set_dns_single_server() {
166 let spec = resolvectl_set_dns("wlp1s0", &[ip("9.9.9.9")]);
167 let (_, args) = extract_args(&spec);
168 let a: Vec<&str> = args.iter().map(String::as_str).collect();
169 assert!(a.contains(&"wlp1s0"));
170 assert!(a.contains(&"9.9.9.9"));
171 }
172
173 #[test]
174 fn resolvectl_set_dns_accepts_ipv6() {
175 let spec = resolvectl_set_dns("eth0", &[ip("2606:4700:4700::1111")]);
178 let (_, args) = extract_args(&spec);
179 let a: Vec<&str> = args.iter().map(String::as_str).collect();
180 assert!(a.contains(&"2606:4700:4700::1111"));
181 }
182
183 #[test]
186 fn specs_covers_all_action_names() {
187 let expected = ["ResolvectlStatus", "ResolvectlSetDns"];
188 let spec_names: Vec<&str> = specs().iter().map(|s| s.action_name).collect();
189 for name in &expected {
190 assert!(spec_names.contains(name), "specs() missing {name}");
191 }
192 }
193}