rd_util/
storage_info.rs

1// Copyright (c) Facebook, Inc. and its affiliates.
2use anyhow::bail;
3use anyhow::Result;
4use glob::glob;
5use log::{debug, trace, warn};
6use proc_mounts::{MountInfo, MountList, SwapIter};
7use scan_fmt::scan_fmt;
8use std::ffi::{OsStr, OsString};
9use std::fs;
10use std::io::Read;
11use std::os::linux::fs::MetadataExt;
12use std::path::{Path, PathBuf};
13use std::process::Command;
14
15lazy_static::lazy_static! {
16    static ref MOUNT_LIST: Result<MountList> = Ok(MountList::new()?);
17    static ref FINDMNT_BIN: String = super::find_bin("findmnt", Option::<&str>::None)
18        .expect("Failed to find findmnt bin")
19        .to_str()
20        .unwrap()
21        .to_owned();
22}
23
24/// Given a path, find out the containing mountpoint.
25pub fn path_to_mountpoint<P: AsRef<Path>>(path_in: P) -> Result<MountInfo> {
26    let path = path_in.as_ref();
27    let mut abs_path = fs::canonicalize(&path)?;
28    let mounts = match MOUNT_LIST.as_ref() {
29        Ok(v) => v,
30        Err(e) => bail!("Failed to list mount points ({:?})", &e),
31    };
32
33    loop {
34        if let Some(mi) = mounts.get_mount_by_dest(&abs_path) {
35            debug!("found mount point {:?} for {:?}", &mi.dest, &path);
36            return Ok(mi.clone());
37        }
38        if !abs_path.pop() {
39            bail!("Failed to find mount point for {:?}", path);
40        }
41    }
42}
43
44fn match_devnr<P: AsRef<Path>>(path_in: P, devnr: u64) -> bool {
45    let path = path_in.as_ref();
46    let mut buf = String::new();
47    if let Err(err) = fs::File::open(&path).and_then(|mut f| f.read_to_string(&mut buf)) {
48        warn!("Failed to open {:?} ({:?})", &path, &err);
49        return false;
50    }
51
52    trace!("matching {:?} content '{}'", &path, buf.trim());
53    let (maj, min) = match scan_fmt!(&buf, "{d}:{d}", u32, u32) {
54        Ok(v) => v,
55        Err(e) => {
56            warn!("Failed to parse '{}' ({:?})", buf.trim(), &e);
57            return false;
58        }
59    };
60
61    if devnr == libc::makedev(maj, min) {
62        trace!("matched {:?}", &path);
63        return true;
64    }
65    return false;
66}
67
68/// Given a device number, find the kernel device name.
69fn devnr_to_devname(devnr: u64) -> Result<OsString> {
70    let blk_dir = Path::new("/sys/block");
71    let blk_iter = blk_dir.read_dir()?;
72
73    for blk_path in blk_iter.filter_map(|r| r.ok()).map(|e| e.path()) {
74        let mut dev_path = PathBuf::from(&blk_path);
75
76        dev_path.push("dev");
77        if match_devnr(&dev_path, devnr) {
78            dev_path.pop();
79            return Ok(dev_path.file_name().unwrap().into());
80        }
81        dev_path.pop();
82
83        let pattern = format!(
84            "{}/{}*/dev",
85            dev_path.to_str().unwrap().to_string(),
86            dev_path.file_name().unwrap().to_str().unwrap()
87        );
88        for part in glob(&pattern).unwrap().filter_map(|x| x.ok()) {
89            if match_devnr(&part, devnr) {
90                return Ok(dev_path.file_name().unwrap().into());
91            }
92        }
93    }
94    bail!("No matching dev for 0x{:x}", devnr);
95}
96
97/// Given a device name, find the device number.
98pub fn devname_to_devnr<D: AsRef<OsStr>>(name_in: D) -> Result<(u32, u32)> {
99    let mut path = PathBuf::from("/dev");
100    path.push(name_in.as_ref());
101    let rdev = fs::metadata(path)?.st_rdev();
102    Ok((unsafe { libc::major(rdev) }, unsafe { libc::minor(rdev) }))
103}
104
105/// Given a path, find the underlying device.
106pub fn path_to_devname<P: AsRef<Path>>(path: P) -> Result<OsString> {
107    let mtp = path_to_mountpoint(path.as_ref())?;
108
109    // Ideally, mtp.source should point to the device node which can be
110    // canonicalized by reading its devnr and mapping that to devname.
111    // Unfortunately, determining the device for a filesystem, especially
112    // root, is not trivial. For example, $mtp.source might point to
113    // "/dev/root" which doesn't exist. `findmnt` has a bunch of heuristics
114    // to map mount point to device. Let's use that instead.
115    let output = Command::new(&*FINDMNT_BIN)
116        .arg("-no")
117        .arg("SOURCE")
118        .arg(&mtp.dest)
119        .output()?;
120
121    if !output.status.success() {
122        bail!("findmnt on {:?} failed with {:?}", &mtp.dest, &output);
123    }
124
125    let source = String::from_utf8(output.stdout).unwrap();
126    trace!("path_to_devname path={:?} source={:?}", path.as_ref(), source.trim());
127    devnr_to_devname(fs::metadata(source.trim())?.st_rdev())
128}
129
130fn read_model(dev_path: &Path) -> Result<String> {
131    let mut path = PathBuf::from(dev_path);
132    path.push("device");
133    path.push("model");
134
135    let mut model = String::new();
136    let mut f = fs::File::open(&path)?;
137    f.read_to_string(&mut model)?;
138    Ok(model.trim_end().to_string())
139}
140
141fn read_fwrev(dev_path: &Path) -> Result<String> {
142    let mut path = PathBuf::from(dev_path);
143    path.push("device");
144    path.push("firmware_rev");
145
146    let mut fwrev = String::new();
147    trace!("trying {:?}", &path);
148    if path.exists() {
149        let mut f = fs::File::open(&path)?;
150        f.read_to_string(&mut fwrev)?;
151    } else {
152        path.pop();
153        path.push("rev");
154        trace!("trying {:?}", &path);
155        if path.exists() {
156            let mut f = fs::File::open(&path)?;
157            f.read_to_string(&mut fwrev)?;
158        } else {
159            bail!("neither \"firmware_dev\" or \"rev\" is found");
160        }
161    }
162    Ok(fwrev.trim_end().to_string())
163}
164
165/// Given a device name, determine its model, firmware version and size.
166pub fn devname_to_model_fwrev_size<D: AsRef<OsStr>>(name_in: D) -> Result<(String, String, u64)> {
167    let unknown = "<UNKNOWN>";
168    let mut dev_path = PathBuf::from("/sys/block");
169    dev_path.push(name_in.as_ref());
170
171    let model = match read_model(&dev_path) {
172        Ok(v) => v,
173        Err(e) => {
174            warn!(
175                "storage_info: Failed to read model string for {:?} ({:#}), \
176                 dm / md devices are not supported",
177                &dev_path, &e
178            );
179            unknown.to_string()
180        }
181    };
182
183    let fwrev = match read_fwrev(&dev_path) {
184        Ok(v) => v,
185        Err(e) => {
186            warn!(
187                "storage_info: Failed to read firmware revision for {:?} ({:#}), \
188                 dm / md devices are not supported",
189                &dev_path, &e
190            );
191            unknown.to_string()
192        }
193    };
194
195    let mut size_path = dev_path.clone();
196    size_path.push("size");
197
198    let mut size = String::new();
199    let mut f = fs::File::open(&size_path)?;
200    f.read_to_string(&mut size)?;
201    let size = size.trim().parse::<u64>()? * 512;
202
203    Ok((model, fwrev, size))
204}
205
206/// Find all devices hosting swap
207pub fn swap_devnames() -> Result<Vec<OsString>> {
208    let mut devnames = Vec::new();
209    for swap in SwapIter::new()?.filter_map(|sw| sw.ok()) {
210        if swap.kind == "partition" {
211            devnames.push(devnr_to_devname(fs::metadata(&swap.source)?.st_rdev())?);
212        } else {
213            devnames.push(path_to_devname(&swap.source)?);
214        }
215    }
216    Ok(devnames)
217}
218
219/// Given a device name, determine whether it's rotational.
220pub fn is_devname_rotational<P: AsRef<OsStr>>(devname: P) -> Result<bool> {
221    let mut sysblk_path = PathBuf::from("/sys/block");
222    sysblk_path.push(devname.as_ref());
223    sysblk_path.push("queue");
224    sysblk_path.push("rotational");
225
226    let mut buf = String::new();
227    fs::File::open(&sysblk_path).and_then(|mut f| f.read_to_string(&mut buf))?;
228
229    trace!("read {:?} content '{}'", &sysblk_path, buf.trim());
230    match scan_fmt!(&buf, "{d}", u32) {
231        Ok(v) => Ok(v != 0),
232        Err(e) => bail!("parse error: '{}' ({:?})", &buf, &e),
233    }
234}
235
236/// Give a path, determine whether it's backed by a HDD.
237pub fn is_path_rotational<P: AsRef<Path>>(path_in: P) -> bool {
238    let path = path_in.as_ref();
239
240    let devname = match path_to_devname(path) {
241        Ok(v) => v,
242        Err(e) => {
243            warn!(
244                "Failed to determine device name for {:?} ({:?}), assuming SSD",
245                path, &e
246            );
247            return false;
248        }
249    };
250
251    match is_devname_rotational(&devname) {
252        Ok(v) => v,
253        Err(e) => {
254            warn!(
255                "Failed to determine whether {:?} is rotational ({:?}), assuming SSD",
256                &path, &e
257            );
258            false
259        }
260    }
261}
262
263/// Is any of the swaps rotational?
264pub fn is_swap_rotational() -> bool {
265    match swap_devnames() {
266        Ok(devnames) => {
267            debug!("swap devices: {:?}", &devnames);
268            for devname in devnames {
269                match is_devname_rotational(&devname) {
270                    Ok(true) => {
271                        debug!("swap device {:?} is rotational", &devname);
272                        return true;
273                    }
274                    Ok(false) => debug!("swap device {:?} is not rotational", &devname),
275                    Err(e) => warn!(
276                        "Failed to determine whether {:?} is rotational ({:?})",
277                        &devname, &e
278                    ),
279                }
280            }
281        }
282        Err(e) => warn!("Failed to determine swap devices ({:?}), assuming SSD", &e),
283    }
284    false
285}
286
287#[cfg(test)]
288mod tests {
289    use std::env;
290
291    #[test]
292    fn test() {
293        let _ = ::env_logger::try_init();
294        if let Ok(v) = env::var("ROTATIONAL_TARGET") {
295            println!("{} rotational={}", &v, super::is_path_rotational(&v));
296        }
297        println!("CWD rotational={}", super::is_path_rotational("."));
298        println!("swap rotational={}", super::is_swap_rotational());
299    }
300}