fxcp_core/operations/
container.rs1use std::path::Path;
7use tracing::debug;
8
9#[derive(Debug, Clone)]
15pub struct ContainerInfo {
16 pub in_container: bool,
18 pub engine: Option<String>,
20 pub rootless: bool,
22}
23
24#[derive(Debug, Clone)]
26pub struct DmStackInfo {
27 pub has_crypt: bool,
29 pub has_integrity: bool,
31 pub has_cache: bool,
33 pub has_thin: bool,
35 pub has_vdo: bool,
37 pub has_stratis: bool,
39 pub crypt_sector_size: u32,
41 pub integrity_tag_size: u32,
43 pub thin_pool_data_pct: f64,
45 pub thin_pool_meta_pct: f64,
47 pub stack_depth: u8,
49 pub physical_block_size: u32,
51 pub optimal_io_size: u32,
53 pub base_device: Option<String>,
55}
56
57impl Default for DmStackInfo {
58 fn default() -> Self {
59 Self {
60 has_crypt: false, has_integrity: false, has_cache: false,
61 has_thin: false, has_vdo: false, has_stratis: false,
62 crypt_sector_size: 0, integrity_tag_size: 0,
63 thin_pool_data_pct: 0.0, thin_pool_meta_pct: 0.0,
64 stack_depth: 0, physical_block_size: 512, optimal_io_size: 0,
65 base_device: None,
66 }
67 }
68}
69
70pub(crate) fn detect_container() -> ContainerInfo {
75 if let Ok(content) = std::fs::read_to_string("/run/.containerenv") {
77 let engine = content.lines()
78 .find(|l| l.starts_with("engine="))
79 .map(|l| l.trim_start_matches("engine=").trim_matches('"').to_string());
80 let rootless = content.contains("rootless=1");
81 return ContainerInfo { in_container: true, engine, rootless };
82 }
83 if Path::new("/.dockerenv").exists() {
85 return ContainerInfo { in_container: true, engine: Some("docker".into()), rootless: false };
86 }
87 ContainerInfo { in_container: false, engine: None, rootless: false }
88}
89
90fn resolve_backing_device(path: &Path) -> Option<String> {
97 let entry = crate::mount_info::find_mount_for_path(path)?;
98 if entry.source.is_empty() || entry.source == "none" || entry.source == "overlay" {
99 return None;
100 }
101 debug!("resolve_backing_device: {:?} -> mount={} source={}", path, entry.mount_point, entry.source);
102 Some(entry.source)
103}
104
105fn resolve_sysfs_block_name(device_path: &str) -> Option<String> {
107 let dev_path = Path::new(device_path);
108 let resolved = if device_path.starts_with("/dev/mapper/") {
110 std::fs::read_link(dev_path).ok()?
111 } else {
112 dev_path.to_path_buf()
113 };
114 resolved.file_name()?.to_str().map(String::from)
116}
117
118pub(crate) fn probe_dm_stack(path: &Path) -> Option<DmStackInfo> {
120 let device = resolve_backing_device(path)?;
121 let block_name = resolve_sysfs_block_name(&device)?;
122
123 let mut info = DmStackInfo::default();
124 let mut current_dev = block_name.clone();
125
126 loop {
128 let dm_uuid_path = format!("/sys/block/{}/dm/uuid", current_dev);
129 if let Ok(uuid) = std::fs::read_to_string(&dm_uuid_path) {
130 let uuid = uuid.trim();
131 info.stack_depth += 1;
132
133 if uuid.starts_with("CRYPT-LUKS2-") {
134 info.has_crypt = true;
135 info.crypt_sector_size = 4096;
136 } else if uuid.starts_with("CRYPT-LUKS1-") || uuid.starts_with("CRYPT-") {
137 info.has_crypt = true;
138 info.crypt_sector_size = 512;
139 } else if uuid.starts_with("INTEGRITY-") {
140 info.has_integrity = true;
141 info.integrity_tag_size = 4096;
143 } else if uuid.starts_with("LVM-") {
144 let table_path = format!("/sys/block/{}/dm/name", current_dev);
146 if let Ok(name) = std::fs::read_to_string(&table_path)
147 && (name.trim().contains("tpool") || name.trim().contains("thin")) {
148 info.has_thin = true;
149 }
150 } else if uuid.starts_with("VDO-") {
151 info.has_vdo = true;
152 }
153
154 let name_path = format!("/sys/block/{}/dm/name", current_dev);
156 if let Ok(name) = std::fs::read_to_string(&name_path)
157 && name.trim().contains("stratis") {
158 info.has_stratis = true;
159 }
160 }
161
162 let slaves_path = format!("/sys/block/{}/slaves", current_dev);
164 if let Ok(entries) = std::fs::read_dir(&slaves_path) {
165 let slaves: Vec<String> = entries
166 .filter_map(|e| e.ok())
167 .map(|e| e.file_name().to_string_lossy().into_owned())
168 .collect();
169 if slaves.len() == 1 {
170 current_dev = slaves[0].clone();
171 continue; } else if slaves.is_empty() {
173 info.base_device = Some(current_dev.clone());
175 break;
176 } else {
177 info.base_device = Some(slaves[0].clone());
179 break;
180 }
181 } else {
182 info.base_device = Some(current_dev.clone());
184 break;
185 }
186 }
187
188 if let Some(ref base) = info.base_device {
190 let pbs_path = format!("/sys/block/{}/queue/physical_block_size", base);
191 if let Ok(val) = std::fs::read_to_string(&pbs_path) {
192 info.physical_block_size = val.trim().parse().unwrap_or(512);
193 }
194 let oio_path = format!("/sys/block/{}/queue/optimal_io_size", base);
195 if let Ok(val) = std::fs::read_to_string(&oio_path) {
196 info.optimal_io_size = val.trim().parse().unwrap_or(0);
197 }
198 }
199
200 Some(info)
201}
202
203#[cfg(test)]
204mod tests {
205 #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
206 use super::*;
207
208 #[test]
209 fn test_detect_container_not_in_container() {
210 let info = detect_container();
211 if !Path::new("/run/.containerenv").exists() && !Path::new("/.dockerenv").exists() {
212 assert!(!info.in_container);
213 assert!(info.engine.is_none());
214 assert!(!info.rootless);
215 }
216 }
217
218 #[test]
219 fn test_container_info_clone() {
220 let info = detect_container();
221 let _ = format!("{:?}", info);
222 let cloned = info.clone();
223 assert_eq!(cloned.in_container, info.in_container);
224 assert_eq!(cloned.engine, info.engine);
225 assert_eq!(cloned.rootless, info.rootless);
226 }
227
228 #[test]
229 fn test_resolve_sysfs_block_name_dev_dm() {
230 assert_eq!(resolve_sysfs_block_name("/dev/dm-0"), Some("dm-0".to_string()));
231 }
232
233 #[test]
234 fn test_resolve_sysfs_block_name_dev_sda() {
235 assert_eq!(resolve_sysfs_block_name("/dev/sda1"), Some("sda1".to_string()));
236 }
237
238 #[test]
239 fn test_resolve_sysfs_block_name_dev_nvme() {
240 assert_eq!(resolve_sysfs_block_name("/dev/nvme0n1p1"), Some("nvme0n1p1".to_string()));
241 }
242
243 #[test]
244 fn test_resolve_sysfs_block_name_mapper_returns_none_if_no_symlink() {
245 assert!(resolve_sysfs_block_name("/dev/mapper/nonexistent_device_xyz").is_none());
246 }
247
248 #[test]
249 fn test_dm_stack_info_default() {
250 let info = DmStackInfo::default();
251 assert!(!info.has_crypt);
252 assert!(!info.has_integrity);
253 assert!(!info.has_cache);
254 assert!(!info.has_thin);
255 assert!(!info.has_vdo);
256 assert!(!info.has_stratis);
257 assert_eq!(info.crypt_sector_size, 0);
258 assert_eq!(info.integrity_tag_size, 0);
259 assert_eq!(info.thin_pool_data_pct, 0.0);
260 assert_eq!(info.thin_pool_meta_pct, 0.0);
261 assert_eq!(info.stack_depth, 0);
262 assert_eq!(info.physical_block_size, 512);
263 assert_eq!(info.optimal_io_size, 0);
264 assert!(info.base_device.is_none());
265 }
266}