flodl_cli/util/
platform.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum Platform {
18 Debian,
20 Rhel,
23 MacOs,
26 Other,
29}
30
31impl Platform {
32 pub fn detect() -> Self {
34 if cfg!(target_os = "macos") {
35 return Platform::MacOs;
36 }
37 if !cfg!(target_os = "linux") {
38 return Platform::Other;
39 }
40 match std::fs::read_to_string("/etc/os-release") {
41 Ok(c) => Self::from_os_release(&c),
42 Err(_) => Platform::Other,
43 }
44 }
45
46 pub fn from_os_release(body: &str) -> Self {
51 const RHEL: &[&str] = &["rhel", "fedora", "centos", "rocky", "almalinux"];
52 const DEB: &[&str] = &["debian", "ubuntu"];
53 let tokens: Vec<String> = body
54 .lines()
55 .filter_map(|l| {
56 let l = l.trim();
57 l.strip_prefix("ID=").or_else(|| l.strip_prefix("ID_LIKE="))
58 })
59 .flat_map(|v| {
60 v.trim_matches('"')
61 .split_whitespace()
62 .map(str::to_string)
63 .collect::<Vec<_>>()
64 })
65 .collect();
66 if tokens.iter().any(|t| RHEL.contains(&t.as_str())) {
67 return Platform::Rhel;
68 }
69 if tokens.iter().any(|t| DEB.contains(&t.as_str())) {
70 return Platform::Debian;
71 }
72 Platform::Debian
73 }
74
75 pub fn install(&self, packages: &[&str]) -> Option<String> {
78 let list = packages.join(" ");
79 match self {
80 Platform::Debian => Some(format!("sudo apt install -y {list}")),
81 Platform::Rhel => Some(format!("sudo dnf install -y {list}")),
82 Platform::MacOs => Some(format!("brew install {list}")),
83 Platform::Other => None,
84 }
85 }
86
87 pub fn sshd_package(&self) -> Option<&'static str> {
89 match self {
90 Platform::Debian | Platform::Rhel => Some("openssh-server"),
91 Platform::MacOs | Platform::Other => None,
93 }
94 }
95
96 pub fn ssh_service(&self) -> Option<&'static str> {
98 match self {
99 Platform::Debian => Some("ssh.service"),
100 Platform::Rhel => Some("sshd.service"),
101 Platform::MacOs | Platform::Other => None,
102 }
103 }
104
105 pub fn enable_sshd(&self) -> Vec<String> {
112 match self {
113 Platform::Debian => vec![
114 "sudo systemctl disable --now ssh.socket".to_string(),
115 "sudo systemctl enable --now ssh.service".to_string(),
116 ],
117 Platform::Rhel => vec!["sudo systemctl enable --now sshd.service".to_string()],
118 Platform::MacOs => vec!["sudo systemsetup -setremotelogin on".to_string()],
119 Platform::Other => vec![],
120 }
121 }
122
123 pub fn open_port(&self, port: u16) -> Option<String> {
126 match self {
127 Platform::Debian => Some(format!("sudo ufw allow {port}/tcp # if ufw is active")),
128 Platform::Rhel => Some(format!(
129 "sudo firewall-cmd --permanent --add-port={port}/tcp && sudo firewall-cmd --reload"
130 )),
131 Platform::MacOs | Platform::Other => None,
132 }
133 }
134
135 pub fn allow_ssh_port(&self, port: u16) -> Option<String> {
139 match self {
140 Platform::Rhel if port != 22 => Some(format!(
141 "sudo semanage port -a -t ssh_port_t -p tcp {port} \
142 # SELinux; needs policycoreutils-python-utils"
143 )),
144 _ => None,
145 }
146 }
147
148 pub fn has_sshd_config_d(&self) -> bool {
152 matches!(self, Platform::Debian | Platform::Rhel)
153 }
154
155 pub fn rrsync_fix(&self) -> Option<String> {
166 match self {
167 Platform::Debian => self.install(&["rsync"]),
168 Platform::Rhel => Some(
169 concat!(
170 "sudo dnf install -y rsync && ",
171 "sudo install -m 755 /usr/share/doc/rsync/support/rrsync ",
172 "/usr/local/bin/rrsync",
173 )
174 .to_string(),
175 ),
176 Platform::MacOs => Some(
179 "brew install rsync, then put its support/rrsync on PATH as an executable"
180 .to_string(),
181 ),
182 Platform::Other => None,
183 }
184 }
185
186 pub fn in_container() -> bool {
194 if std::path::Path::new("/.dockerenv").exists() {
195 return true;
196 }
197 std::fs::read_to_string("/proc/1/cgroup")
198 .map(|c| c.contains("docker") || c.contains("containerd") || c.contains("libpod"))
199 .unwrap_or(false)
200 }
201
202 pub fn name(&self) -> &'static str {
204 match self {
205 Platform::Debian => "Debian/Ubuntu",
206 Platform::Rhel => "RHEL/Fedora",
207 Platform::MacOs => "macOS",
208 Platform::Other => "this OS",
209 }
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn os_release_picks_the_family_from_id_or_id_like() {
219 let rocky = "NAME=\"Rocky Linux\"\nID=\"rocky\"\nID_LIKE=\"rhel centos fedora\"\n";
220 let fedora = "ID=fedora\n";
221 let ubuntu = "NAME=\"Ubuntu\"\nID=ubuntu\nID_LIKE=debian\n";
222 let debian = "ID=debian\n";
223 assert_eq!(Platform::from_os_release(rocky), Platform::Rhel);
224 assert_eq!(Platform::from_os_release(fedora), Platform::Rhel);
225 assert_eq!(Platform::from_os_release(ubuntu), Platform::Debian);
226 assert_eq!(Platform::from_os_release(debian), Platform::Debian);
227 assert_eq!(Platform::from_os_release("ID=weird\n"), Platform::Debian);
230 }
231
232 #[test]
233 fn the_ssh_unit_and_socket_trap_differ_by_family() {
234 let deb = Platform::Debian.enable_sshd();
237 assert!(
238 deb.iter().any(|c| c.contains("disable --now ssh.socket")),
239 "{deb:?}"
240 );
241 assert!(
242 deb.iter().any(|c| c.contains("enable --now ssh.service")),
243 "{deb:?}"
244 );
245 assert_eq!(Platform::Debian.ssh_service(), Some("ssh.service"));
246
247 let rhel = Platform::Rhel.enable_sshd();
248 assert!(rhel.iter().any(|c| c.contains("sshd.service")), "{rhel:?}");
249 assert!(
250 !rhel.iter().any(|c| c.contains("socket")),
251 "no socket unit on RHEL: {rhel:?}"
252 );
253 assert_eq!(Platform::Rhel.ssh_service(), Some("sshd.service"));
254 }
255
256 #[test]
257 fn selinux_labeling_is_named_only_where_it_bites() {
258 assert!(Platform::Rhel.allow_ssh_port(2022).is_some());
261 assert!(Platform::Rhel.allow_ssh_port(22).is_none());
262 assert!(Platform::Debian.allow_ssh_port(2022).is_none());
263 assert!(Platform::MacOs.allow_ssh_port(2022).is_none());
264 }
265
266 #[test]
267 fn container_detection_answers_without_panicking() {
268 let _ = Platform::in_container();
272 }
273
274 #[test]
275 fn rrsync_guidance_knows_rhel_ships_it_unexecutable() {
276 let deb = Platform::Debian.rrsync_fix().unwrap();
279 assert!(deb.contains("apt") && deb.contains("rsync"), "{deb}");
280 assert!(
281 !deb.contains("install -m"),
282 "no copy needed on Debian: {deb}"
283 );
284 let rhel = Platform::Rhel.rrsync_fix().unwrap();
287 assert!(
288 rhel.contains("install -m 755"),
289 "must make it executable: {rhel}"
290 );
291 assert!(
292 rhel.contains("/usr/share/doc/rsync/support/rrsync"),
293 "{rhel}"
294 );
295 }
296
297 #[test]
298 fn package_commands_speak_each_families_manager() {
299 assert!(
300 Platform::Debian
301 .install(&["rsync"])
302 .unwrap()
303 .starts_with("sudo apt")
304 );
305 assert!(
306 Platform::Rhel
307 .install(&["rsync"])
308 .unwrap()
309 .starts_with("sudo dnf")
310 );
311 assert!(
312 Platform::MacOs
313 .install(&["rsync"])
314 .unwrap()
315 .starts_with("brew")
316 );
317 assert!(Platform::Other.install(&["rsync"]).is_none());
318 assert_eq!(Platform::MacOs.sshd_package(), None);
320 assert_eq!(Platform::Debian.sshd_package(), Some("openssh-server"));
321 }
322}