Skip to main content

list_nvme/
list_nvme.rs

1//! Rust port of `nvme list` — walks the libnvme tree and prints a table.
2
3use libnvme::{Controller, Namespace, Root};
4
5fn main() -> Result<(), Box<dyn std::error::Error>> {
6    let root = Root::scan()?;
7
8    let mut rows: Vec<Row> = Vec::new();
9    for host in root.hosts() {
10        for subsys in host.subsystems() {
11            for ctrl in subsys.controllers() {
12                let mut had_namespace = false;
13                for ns in ctrl.namespaces() {
14                    rows.push(Row::from(&ctrl, Some(&ns)));
15                    had_namespace = true;
16                }
17                if !had_namespace {
18                    rows.push(Row::from(&ctrl, None));
19                }
20            }
21        }
22    }
23
24    if rows.is_empty() {
25        println!("(no NVMe devices found)");
26        return Ok(());
27    }
28
29    print_table(&rows);
30    Ok(())
31}
32
33struct Row {
34    node: String,
35    model: String,
36    serial: String,
37    firmware: String,
38    transport: String,
39    address: String,
40    nsid: String,
41    size: String,
42}
43
44impl Row {
45    fn from(ctrl: &Controller<'_>, ns: Option<&Namespace<'_>>) -> Self {
46        Row {
47            node: ns
48                .and_then(|n| n.name().ok().map(|s| format!("/dev/{s}")))
49                .unwrap_or_else(|| {
50                    ctrl.name()
51                        .ok()
52                        .map(|s| format!("/dev/{s}"))
53                        .unwrap_or_default()
54                }),
55            model: ctrl.model().unwrap_or("").to_string(),
56            serial: ctrl.serial().unwrap_or("").to_string(),
57            firmware: ctrl.firmware().unwrap_or("").to_string(),
58            transport: ctrl.transport().unwrap_or("").to_string(),
59            address: ctrl.address().unwrap_or("").to_string(),
60            nsid: ns.map(|n| n.nsid().to_string()).unwrap_or_default(),
61            size: ns.map(|n| format_bytes(n.size_bytes())).unwrap_or_default(),
62        }
63    }
64}
65
66fn format_bytes(bytes: u64) -> String {
67    const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB", "PB"];
68    let mut value = bytes as f64;
69    let mut unit_idx = 0;
70    while value >= 1000.0 && unit_idx + 1 < UNITS.len() {
71        value /= 1000.0;
72        unit_idx += 1;
73    }
74    if unit_idx == 0 {
75        format!("{bytes} {}", UNITS[0])
76    } else {
77        format!("{value:.2} {}", UNITS[unit_idx])
78    }
79}
80
81fn print_table(rows: &[Row]) {
82    let headers = [
83        "Node",
84        "NSID",
85        "Model",
86        "Serial",
87        "FW",
88        "Size",
89        "Transport",
90        "Address",
91    ];
92    let widths: Vec<usize> = (0..headers.len())
93        .map(|i| {
94            let max_data = rows.iter().map(|r| field(r, i).len()).max().unwrap_or(0);
95            std::cmp::max(headers[i].len(), max_data)
96        })
97        .collect();
98
99    for (i, header) in headers.iter().enumerate() {
100        print!("{:<width$}  ", header, width = widths[i]);
101    }
102    println!();
103    for w in &widths {
104        print!("{:-<width$}  ", "", width = *w);
105    }
106    println!();
107
108    for row in rows {
109        for (i, w) in widths.iter().enumerate() {
110            print!("{:<width$}  ", field(row, i), width = *w);
111        }
112        println!();
113    }
114}
115
116fn field(row: &Row, idx: usize) -> &str {
117    match idx {
118        0 => &row.node,
119        1 => &row.nsid,
120        2 => &row.model,
121        3 => &row.serial,
122        4 => &row.firmware,
123        5 => &row.size,
124        6 => &row.transport,
125        7 => &row.address,
126        _ => "",
127    }
128}