nvme-telem 0.3.2

NVMe S.M.A.R.T. / telemetry collection for Linux
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
//! High-level telemetry and monitoring API for NVMe devices.
//!
//! This module provides functions for collecting telemetry data
//! from NVMe controllers, automatically handling device identification and
//! data enrichment.

use crate::nvme::io::*;
use crate::nvme::ocp::{OcpSmartData, read_ocp_smart_log_fd};
use crate::nvme::types::{self, *};
use std::fs::{self, OpenOptions};
use std::os::fd::{AsFd, OwnedFd};
use std::path::Path;

/// A handle to an open NVMe character device.
///
/// `Device` owns the underlying file descriptor for a device such as
/// `/dev/nvme0`, opened once via [`Device::open`]. Each accessor method
/// (`smart_log`, `identity`, `error_log`, `ocp_smart_log`) reuses that
/// same descriptor to issue its NVMe Admin commands, rather than
/// reopening the device path per call.
///
/// The descriptor is closed automatically when the `Device` is dropped.
///
/// # Requirements
///
/// Opening a device and issuing any of the commands below requires
/// root/sudo privileges to access `/dev/nvme*`.
///
/// # Safety
///
/// All commands issued by `Device` are read-only NVMe Admin commands
/// (Identify and Get Log Page) — they do not modify device state.
#[derive(Debug)]
pub struct Device {
    fd: OwnedFd,
    nvme_name: String,
}

impl Device {
    /// Open an NVMe character device (e.g. `/dev/nvme0`).
    ///
    /// The device is opened once; the resulting file descriptor is reused
    /// by every accessor method on the returned `Device`. Requires
    /// root/sudo privileges.
    ///
    /// # Errors
    ///
    /// Returns an error if the device path does not exist or cannot be
    /// opened (e.g. insufficient permissions).
    pub fn open(path: impl AsRef<Path>) -> Result<Device> {
        let path = path.as_ref();
        let file = OpenOptions::new()
            .read(true)
            .write(true) // Admin permission required
            .open(path)?;

        let nvme_name = path
            .to_string_lossy()
            .trim_start_matches("/dev/")
            .to_string();

        Ok(Device {
            fd: file.into(),
            nvme_name,
        })
    }

    /// Retrieve S.M.A.R.T./Health Information from this device.
    ///
    /// Issues an Identify Controller command (for the serial number) and a
    /// Get Log Page command for the S.M.A.R.T./Health Information log
    /// (Log ID 0x02). Both are read-only NVMe Admin commands.
    ///
    /// # Returns
    ///
    /// Returns [`NvmeSmartLog`] containing:
    /// - Device identification (name and serial number)
    /// - Critical warnings and health status
    /// - Temperature readings from all available sensors
    /// - Storage capacity usage (percentage used, available spare)
    /// - Lifetime statistics (power cycles, power-on hours, data read/written)
    /// - Error and reliability metrics (media errors, unsafe shutdowns)
    /// - Thermal management history
    ///
    /// # Errors
    ///
    /// This function will return an error if:
    /// - The process lacks sufficient permissions (requires root/sudo)
    /// - The NVMe controller does not respond or returns an error status
    /// - The device is not a valid NVMe controller
    pub fn smart_log(&self) -> Result<NvmeSmartLog> {
        let id_ctrl = read_nvme_id_ctrl_fd(self.fd.as_fd())?;
        let serial_number = types::parse_ascii_field(&id_ctrl.sn);

        let raw_smart = read_nvme_smart_log_fd(self.fd.as_fd())?;

        Ok(NvmeSmartLog::new(
            self.nvme_name.clone(),
            serial_number,
            &raw_smart,
        ))
    }

    /// Retrieve Controller Identification data from this device.
    ///
    /// Issues a single, read-only Identify Controller NVMe Admin command.
    ///
    /// # Returns
    ///
    /// Returns [`CtrlIdentity`] containing:
    /// - Vendor information (PCI VID, Subsystem VID, IEEE OUI)
    /// - Device identification (serial number, model number, firmware revision)
    /// - Controller identifiers (Controller ID, NVM Subsystem NQN)
    /// - Hardware identification (FRU GUID)
    /// - NVMe specification version supported by the controller
    /// - Controller type
    ///
    /// # Errors
    ///
    /// This function will return an error if:
    /// - The process lacks sufficient permissions (requires root/sudo)
    /// - The Identify Controller command fails
    /// - The device is not a valid NVMe controller
    pub fn identity(&self) -> Result<CtrlIdentity> {
        let raw = read_nvme_id_ctrl_fd(self.fd.as_fd())?;
        Ok(CtrlIdentity::new(self.nvme_name.clone(), &raw))
    }

    /// Retrieve Error Information Log from this device.
    ///
    /// Issues an Identify Controller command (to determine the ELPE —
    /// Error Log Page Entries — field) followed by a Get Log Page command
    /// for the Error Information log (Log ID 0x01). Both are read-only
    /// NVMe Admin commands.
    ///
    /// The number of error entries retrieved is automatically determined by
    /// querying the controller's ELPE field, ensuring all available error
    /// history is collected.
    ///
    /// # Returns
    ///
    /// Returns [`NvmeErrorLog`] containing:
    /// - Device identification (name and serial number)
    /// - Vector of error entries, each including:
    ///   - Error count and timestamp information
    ///   - Command details (queue ID, command ID)
    ///   - Error status and location
    ///   - Affected LBA and namespace
    ///   - Vendor-specific diagnostic data
    ///
    /// Note: Only populated error entries are returned (entries with `error_count != 0`).
    /// A healthy drive may return an empty error list.
    ///
    /// # Errors
    ///
    /// This function will return an error if:
    /// - The process lacks sufficient permissions (requires root/sudo)
    /// - The Identify Controller command fails
    /// - The Get Log Page command fails
    /// - The device is not a valid NVMe controller
    pub fn error_log(&self) -> Result<NvmeErrorLog> {
        let id_ctrl = read_nvme_id_ctrl_fd(self.fd.as_fd())?;
        let diag = CtrlDiagnostics::new(self.nvme_name.clone(), &id_ctrl);
        let serial_number = types::parse_ascii_field(&id_ctrl.sn);

        // ELPE is 0-based, so 255 means 256 entries; widen before adding.
        let max_entries = u16::from(diag.elpe) + 1;

        let raw_entries = read_error_log_raw_fd(self.fd.as_fd(), max_entries)?;
        Ok(NvmeErrorLog::new(
            self.nvme_name.clone(),
            serial_number,
            raw_entries,
        ))
    }

    /// Retrieve and parse the OCP SMART Extended Log from this device.
    ///
    /// Issues an Identify Controller command (for the serial number)
    /// followed by a Get Log Page command for the OCP SMART Extended Log
    /// (Log ID 0xC0). Both are read-only NVMe Admin commands.
    ///
    /// # Returns
    ///
    /// Returns an [`OcpSmartData`] on success, or an [`std::io::Error`] if any
    /// underlying device read fails.
    ///
    /// # Errors
    ///
    /// This function will return an error if:
    /// * The process lacks sufficient permissions (requires root/sudo)
    /// * Reading the NVMe Identify Controller data fails.
    /// * Reading the OCP SMART Additional Log fails (including when the
    ///   device does not support the OCP extended SMART log).
    pub fn ocp_smart_log(&self) -> Result<OcpSmartData> {
        let id_ctrl = read_nvme_id_ctrl_fd(self.fd.as_fd())?;
        let serial_number = types::parse_ascii_field(&id_ctrl.sn);

        let raw_smart_add_log = read_ocp_smart_log_fd(self.fd.as_fd())?;

        Ok(OcpSmartData::new(
            self.nvme_name.clone(),
            serial_number,
            &raw_smart_add_log,
        ))
    }
}

/// Retrieve S.M.A.R.T./Health Information from an NVMe device.
///
/// This function collects comprehensive health and telemetry data from the specified
/// NVMe controller, including device identification (serial number) for proper tracking.
/// The data includes temperature, wear indicators, power statistics, error counts, and
/// thermal management metrics.
///
/// # Arguments
///
/// * `dev_path` - Path to the NVMe character device (e.g., `"/dev/nvme0"`)
///
/// # Returns
///
/// Returns [`NvmeSmartLog`] containing:
/// - Device identification (name and serial number)
/// - Critical warnings and health status
/// - Temperature readings from all available sensors
/// - Storage capacity usage (percentage used, available spare)
/// - Lifetime statistics (power cycles, power-on hours, data read/written)
/// - Error and reliability metrics (media errors, unsafe shutdowns)
/// - Thermal management history
///
/// # Errors
///
/// This function will return an error if:
/// - The device path does not exist or cannot be opened
/// - The process lacks sufficient permissions (requires root/sudo)
/// - The NVMe controller does not respond or returns an error status
/// - The device is not a valid NVMe controller
#[deprecated(since = "0.3.2", note = "use Device::open(path)?.smart_log() instead")]
pub fn get_smart_log(dev_path: &str) -> Result<NvmeSmartLog> {
    Device::open(dev_path)?.smart_log()
}

/// Retrieve Error Information Log from an NVMe device.
///
/// This function collects the complete error history from the specified NVMe controller,
/// including device identification. The error log contains detailed information about
/// all errors encountered by the controller, stored in a circular buffer.
///
/// The number of error entries retrieved is automatically determined by querying the
/// controller's ELPE (Error Log Page Entries) field, ensuring all available error
/// history is collected.
///
/// # Arguments
///
/// * `dev_path` - Path to the NVMe character device (e.g., `"/dev/nvme0"`)
///
/// # Returns
///
/// Returns [`NvmeErrorLog`] containing:
/// - Device identification (name and serial number)
/// - Vector of error entries, each including:
///   - Error count and timestamp information
///   - Command details (queue ID, command ID)
///   - Error status and location
///   - Affected LBA and namespace
///   - Vendor-specific diagnostic data
///
/// Note: Only populated error entries are returned (entries with `error_count != 0`).
/// A healthy drive may return an empty error list.
///
/// # Errors
///
/// This function will return an error if:
/// - The device path does not exist or cannot be opened
/// - The process lacks sufficient permissions (requires root/sudo)
/// - The Identify Controller command fails
/// - The Get Log Page command fails
/// - The device is not a valid NVMe controller
#[deprecated(since = "0.3.2", note = "use Device::open(path)?.error_log() instead")]
pub fn get_error_log(dev_path: &str) -> Result<NvmeErrorLog> {
    Device::open(dev_path)?.error_log()
}

/// Retrieve Controller Identification data from an NVMe device.
///
/// This function collects comprehensive identification and configuration information
/// from the specified NVMe controller. This data is fundamental for device inventory,
/// compatibility checking, and feature detection.
///
/// # Arguments
///
/// * `dev_path` - Path to the NVMe character device (e.g., `"/dev/nvme0"`)
///
/// # Returns
///
/// Returns [`CtrlIdentity`] containing:
/// - Vendor information (PCI VID, Subsystem VID, IEEE OUI)
/// - Device identification (serial number, model number, firmware revision)
/// - Controller identifiers (Controller ID, NVM Subsystem NQN)
/// - Hardware identification (FRU GUID)
/// - NVMe specification version supported by the controller
/// - Controller type
///
/// # Errors
///
/// This function will return an error if:
/// - The device path does not exist or cannot be opened
/// - The process lacks sufficient permissions (requires root/sudo)
/// - The Identify Controller command fails
/// - The device is not a valid NVMe controller
#[deprecated(since = "0.3.2", note = "use Device::open(path)?.identity() instead")]
pub fn get_controller_identity(dev_path: &str) -> Result<CtrlIdentity> {
    Device::open(dev_path)?.identity()
}

/// Discover NVMe controllers available on the system.
///
/// Scans `/sys/class/nvme` to enumerate all NVMe controllers exposed by the kernel.
/// This is typically the first step before collecting telemetry data from specific devices.
///
/// # Returns
///
/// Returns a vector of NVMe controller names (e.g., `["nvme0", "nvme1"]`).
/// Returns an empty vector if no controllers are found or if `/sys/class/nvme` cannot be read.
///
/// # Note
///
/// The returned names can be used to construct device paths by prepending `/dev/`
/// (e.g., `nvme0` becomes `/dev/nvme0`).
pub fn list_nvme_controllers() -> Vec<String> {
    let mut names = Vec::new();

    if let Ok(entries) = fs::read_dir("/sys/class/nvme") {
        for entry in entries.flatten() {
            let name = entry.file_name().to_string_lossy().into_owned();
            names.push(name);
        }
    }

    names
}

/// Retrieve and parses the OCP SMART Extended Log for a given NVMe device.
///
/// This function reads the NVMe Identify Controller data to extract the device's
/// serial number, then fetches the OCP SMART Additional Log and combines both
/// into a complete [`OcpSmartData`] structure.
///
/// # Arguments
///
/// * `dev_path` - The filesystem path to the NVMe device (e.g., `"/dev/nvme0"`).
///
/// # Returns
///
/// Returns an [`OcpSmartData`] on success, or an [`std::io::Error`] if any
/// underlying device read fails.
///
/// # Errors
///
/// This function will return an error if:
/// * Reading the NVMe Identify Controller data fails.
/// * Reading the OCP SMART Additional Log fails.
#[deprecated(
    since = "0.3.2",
    note = "use Device::open(path)?.ocp_smart_log() instead"
)]
pub fn get_smart_add_log(dev_path: &str) -> Result<OcpSmartData> {
    Device::open(dev_path)?.ocp_smart_log()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn open_nonexistent_path_returns_error() {
        let err = Device::open("/dev/nvme-telem-does-not-exist-xyz").unwrap_err();
        assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
    }

    #[test]
    #[allow(deprecated)]
    fn deprecated_wrappers_fail_the_same_way_as_device_open() {
        let path = "/dev/nvme-telem-does-not-exist-xyz";

        let device_err = Device::open(path).unwrap_err();
        let smart_err = get_smart_log(path).unwrap_err();
        let identity_err = get_controller_identity(path).unwrap_err();
        let error_log_err = get_error_log(path).unwrap_err();
        let ocp_err = get_smart_add_log(path).unwrap_err();

        assert_eq!(device_err.kind(), smart_err.kind());
        assert_eq!(device_err.kind(), identity_err.kind());
        assert_eq!(device_err.kind(), error_log_err.kind());
        assert_eq!(device_err.kind(), ocp_err.kind());
    }

    #[test]
    fn device_name_strips_dev_prefix() {
        // /dev/null is always present, readable/writable, and lets us reach
        // past `open()` to confirm the name parsing without real NVMe hardware.
        let device = Device::open("/dev/null").expect("opening /dev/null should succeed");
        assert_eq!(device.nvme_name, "null");
    }

    #[test]
    fn drop_closes_fd_without_panicking() {
        let device = Device::open("/dev/null").expect("opening /dev/null should succeed");
        drop(device);
    }
}