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 {
62 let mut args = vec!["dns".to_string(), interface.to_string()];
64 args.extend(servers.iter().map(|ip| ip.to_string()));
65 ActionSpec {
66 action_name: "ResolvectlSetDns",
67 mechanism: super::ActionMechanism::Command {
68 program: "sudo",
69 args: {
70 let mut full = vec!["resolvectl".to_string()];
71 full.extend(args);
72 full
73 },
74 },
75 risk_level: RiskLevel::High,
81 reboot_required: false,
82 rollback_available: false,
83 }
84}
85
86#[cfg(test)]
91mod tests {
92 use super::*;
93 use crate::actions::ActionMechanism;
94 use std::str::FromStr;
95
96 fn extract_args(spec: &ActionSpec) -> (&'static str, Vec<String>) {
97 match &spec.mechanism {
98 ActionMechanism::Command { program, args } => (*program, args.clone()),
99 _ => panic!("expected Command mechanism"),
100 }
101 }
102
103 #[test]
106 fn resolvectl_status_action_name() {
107 assert_eq!(resolvectl_status().action_name, "ResolvectlStatus");
108 }
109
110 #[test]
111 fn resolvectl_status_risk_is_low() {
112 assert_eq!(resolvectl_status().risk_level, RiskLevel::Low);
113 }
114
115 #[test]
116 fn resolvectl_status_argv() {
117 let spec = resolvectl_status();
118 let (prog, args) = extract_args(&spec);
119 assert_eq!(prog, "resolvectl");
120 assert_eq!(args, vec!["status"]);
121 }
122
123 #[test]
124 fn resolvectl_status_no_sudo() {
125 let (prog, _) = extract_args(&resolvectl_status());
126 assert_ne!(prog, "sudo");
127 }
128
129 fn ip(s: &str) -> IpAddr {
132 IpAddr::from_str(s).expect("test fixture must be a valid IP")
133 }
134
135 #[test]
136 fn resolvectl_set_dns_action_name() {
137 assert_eq!(
138 resolvectl_set_dns("eth0", &[ip("1.1.1.1")]).action_name,
139 "ResolvectlSetDns"
140 );
141 }
142
143 #[test]
144 fn resolvectl_set_dns_risk_is_high() {
145 assert_eq!(
147 resolvectl_set_dns("eth0", &[ip("1.1.1.1")]).risk_level,
148 RiskLevel::High
149 );
150 }
151
152 #[test]
153 fn resolvectl_set_dns_argv_ordering() {
154 let spec = resolvectl_set_dns("eth0", &[ip("1.1.1.1"), ip("8.8.8.8")]);
156 let (prog, args) = extract_args(&spec);
157 assert_eq!(prog, "sudo");
158 let a: Vec<&str> = args.iter().map(String::as_str).collect();
159 assert_eq!(a[0], "resolvectl");
160 assert_eq!(a[1], "dns");
161 assert_eq!(a[2], "eth0");
162 assert_eq!(a[3], "1.1.1.1");
163 assert_eq!(a[4], "8.8.8.8");
164 }
165
166 #[test]
167 fn resolvectl_set_dns_single_server() {
168 let spec = resolvectl_set_dns("wlp1s0", &[ip("9.9.9.9")]);
169 let (_, args) = extract_args(&spec);
170 let a: Vec<&str> = args.iter().map(String::as_str).collect();
171 assert!(a.contains(&"wlp1s0"));
172 assert!(a.contains(&"9.9.9.9"));
173 }
174
175 #[test]
176 fn resolvectl_set_dns_accepts_ipv6() {
177 let spec = resolvectl_set_dns("eth0", &[ip("2606:4700:4700::1111")]);
180 let (_, args) = extract_args(&spec);
181 let a: Vec<&str> = args.iter().map(String::as_str).collect();
182 assert!(a.contains(&"2606:4700:4700::1111"));
183 }
184
185 #[test]
188 fn specs_covers_all_action_names() {
189 let expected = ["ResolvectlStatus", "ResolvectlSetDns"];
190 let spec_names: Vec<&str> = specs().iter().map(|s| s.action_name).collect();
191 for name in &expected {
192 assert!(spec_names.contains(name), "specs() missing {name}");
193 }
194 }
195}