nvme_amz/
lib.rs

1//! A Rust library to probe NVMe devices in Amazon EC2.
2//!
3//! It provides functionality similar to that of the `ebsnvme-id` command but adds
4//! information about instance store devices, not only EBS.
5//!
6//! The library implements [`TryFrom<File>`] for [`Nvme`], to use as the constructor.
7//!
8//! # Example
9//!
10//! ```no_run
11//! use std::env::args;
12//! use std::fs::File;
13//!
14//! use nvme_amz::Nvme;
15//!
16//! fn main() {
17//!     let path = args().nth(1).expect("device path required");
18//!     let file = File::open(path).expect("unable to open device");
19//!     let nvme: Nvme = file.try_into().expect("unable to probe device");
20//!     println!("{:?}", nvme);
21//!     let name = nvme.name();
22//!     println!("name: {}", name);
23//! }
24//! ```
25
26use std::ffi::{c_char, c_uchar, c_uint, c_ulonglong, c_ushort};
27#[cfg(any(feature = "ioctl-nix", feature = "ioctl-rustix"))]
28use std::fs::File;
29use std::os::fd::AsFd;
30use std::{error, fmt, io};
31
32const AMZ_EBS_MN: &str = "Amazon Elastic Block Store";
33const AMZ_INST_STORE_MN: &str = "Amazon EC2 NVMe Instance Storage";
34const AMZ_VENDOR_ID: c_ushort = 0x1D0F;
35
36const NVME_ADMIN_IDENTIFY: u8 = 0x06;
37const NVME_IOCTL_ADMIN_CMD_NUM: u8 = 0x41;
38
39/// The error type for this crate.
40#[derive(Debug)]
41pub enum Error {
42    /// Device name not found in the vendor specific field.
43    DeviceNameNotFound,
44    /// Wrapper for [`std::io::Error`].
45    Io(io::Error),
46    /// The device name could not be parsed.
47    UnparseableDeviceName(String),
48    /// A vendor ID other than Amazon was found.
49    UnrecognizedVendorId(u16),
50    /// A model other than EBS or instance store was found.
51    UnrecognizedModel(String),
52    /// Wrapper for [`nix::errno::Errno`].
53    #[cfg(feature = "ioctl-nix")]
54    NixErrno(nix::errno::Errno),
55    /// Wrapper for [`rustix::io::Errno`].
56    #[cfg(feature = "ioctl-rustix")]
57    RustixErrno(rustix::io::Errno),
58}
59
60impl error::Error for Error {}
61
62impl From<io::Error> for Error {
63    fn from(e: io::Error) -> Self {
64        Self::Io(e)
65    }
66}
67
68#[cfg(feature = "ioctl-nix")]
69impl From<nix::errno::Errno> for Error {
70    fn from(e: nix::errno::Errno) -> Self {
71        Self::NixErrno(e)
72    }
73}
74
75#[cfg(feature = "ioctl-rustix")]
76impl From<rustix::io::Errno> for Error {
77    fn from(e: rustix::io::Errno) -> Self {
78        Self::RustixErrno(e)
79    }
80}
81
82impl std::fmt::Display for Error {
83    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
84        match self {
85            Self::DeviceNameNotFound => write!(f, "device name not found"),
86            Self::Io(e) => write!(f, "{}", e),
87            Self::UnparseableDeviceName(name) => write!(f, "unparseable device name: {}", name),
88            Self::UnrecognizedVendorId(id) => write!(f, "unrecognized vendor id: {}", id),
89            Self::UnrecognizedModel(model) => write!(f, "unrecognized model: {}", model),
90            #[cfg(feature = "ioctl-nix")]
91            Self::NixErrno(e) => write!(f, "{}", e),
92            #[cfg(feature = "ioctl-rustix")]
93            Self::RustixErrno(e) => write!(f, "{}", e),
94        }
95    }
96}
97
98type Result<T> = std::result::Result<T, Error>;
99
100#[repr(C)]
101#[derive(Debug, Clone, Copy)]
102struct NvmeIdPsd {
103    mp: c_ushort,
104    rsvd2: c_uchar,
105    flags: c_uchar,
106    enlat: c_uint,
107    exlat: c_uint,
108    rrt: c_uchar,
109    rrl: c_uchar,
110    rwt: c_uchar,
111    rwl: c_uchar,
112    idlp: c_ushort,
113    ips: c_uchar,
114    rsvd19: c_uchar,
115    actp: c_ushort,
116    apws: c_uchar,
117    rsvd23: [c_uchar; 9],
118}
119
120impl Default for NvmeIdPsd {
121    fn default() -> Self {
122        Self {
123            mp: 0,
124            rsvd2: 0,
125            flags: 0,
126            enlat: 0,
127            exlat: 0,
128            rrt: 0,
129            rrl: 0,
130            rwt: 0,
131            rwl: 0,
132            idlp: 0,
133            ips: 0,
134            rsvd19: 0,
135            actp: 0,
136            apws: 0,
137            rsvd23: [0; 9],
138        }
139    }
140}
141
142#[repr(C)]
143#[derive(Debug, Clone, Copy)]
144struct NvmeVuIdCtrlField {
145    bdev: [c_uchar; 32],
146    reserved0: [c_uchar; 992],
147}
148
149impl Default for NvmeVuIdCtrlField {
150    fn default() -> Self {
151        Self {
152            bdev: [0; 32],
153            reserved0: [0; 992],
154        }
155    }
156}
157
158#[repr(C)]
159#[derive(Debug, Clone, Copy)]
160struct NvmeIdCtrl {
161    vid: c_ushort,
162    ssvid: c_ushort,
163    sn: [c_char; 20],
164    mn: [c_char; 40],
165    fr: [c_char; 8],
166    rab: c_uchar,
167    ieee: [c_uchar; 3],
168    cmic: c_uchar,
169    mdts: c_uchar,
170    cntlid: c_ushort,
171    ver: c_uint,
172    rtd3r: c_uint,
173    rtd3e: c_uint,
174    oaes: c_uint,
175    ctratt: c_uint,
176    rrls: c_ushort,
177    rsvd102: [c_uchar; 9],
178    cntrltype: c_uchar,
179    fguid: [c_uchar; 16],
180    crdt1: c_ushort,
181    crdt2: c_ushort,
182    crdt3: c_ushort,
183    rsvd134: [c_uchar; 119],
184    nvmsr: c_uchar,
185    vwci: c_uchar,
186    mec: c_uchar,
187    oacs: c_ushort,
188    acl: c_uchar,
189    aerl: c_uchar,
190    frmw: c_uchar,
191    lpa: c_uchar,
192    elpe: c_uchar,
193    npss: c_uchar,
194    avscc: c_uchar,
195    apsta: c_uchar,
196    wctemp: c_ushort,
197    cctemp: c_ushort,
198    mtfa: c_ushort,
199    hmpre: c_uint,
200    hmmin: c_uint,
201    tnvmcap: [c_uchar; 16],
202    unvmcap: [c_uchar; 16],
203    rpmbs: c_uint,
204    edstt: c_ushort,
205    dsto: c_uchar,
206    fwug: c_uchar,
207    kas: c_ushort,
208    hctma: c_ushort,
209    mntmt: c_ushort,
210    mxtmt: c_ushort,
211    sanicap: c_uint,
212    hmminds: c_uint,
213    hmmaxd: c_ushort,
214    nsetidmax: c_ushort,
215    endgidmax: c_ushort,
216    anatt: c_uchar,
217    anacap: c_uchar,
218    anagrpmax: c_uint,
219    nanagrpid: c_uint,
220    pels: c_uint,
221    domainid: c_ushort,
222    rsvd358: [c_uchar; 10],
223    megcap: [c_uchar; 16],
224    tmpthha: c_uchar,
225    rsvd385: [c_uchar; 127],
226    sqes: c_uchar,
227    cqes: c_uchar,
228    maxcmd: c_ushort,
229    nn: c_uint,
230    oncs: c_ushort,
231    fuses: c_ushort,
232    fna: c_uchar,
233    vwc: c_uchar,
234    awun: c_ushort,
235    awupf: c_ushort,
236    icsvscc: c_uchar,
237    nwpc: c_uchar,
238    acwu: c_ushort,
239    ocfs: c_ushort,
240    sgls: c_uint,
241    mnan: c_uint,
242    maxdna: [c_uchar; 16],
243    maxcna: c_uint,
244    oaqd: c_uint,
245    rsvd568: [c_uchar; 200],
246    subnqn: [c_char; 256],
247    rsvd1024: [c_uchar; 768],
248    ioccsz: c_uint,
249    iorcsz: c_uint,
250    icdoff: c_ushort,
251    fcatt: c_uchar,
252    msdbd: c_uchar,
253    ofcs: c_ushort,
254    dctype: c_uchar,
255    rsvd1807: [c_uchar; 241],
256    psd: [NvmeIdPsd; 32],
257    vs: NvmeVuIdCtrlField,
258}
259
260impl Default for NvmeIdCtrl {
261    fn default() -> Self {
262        Self {
263            vid: 0,
264            ssvid: 0,
265            sn: [0; 20],
266            mn: [0; 40],
267            fr: [0; 8],
268            rab: 0,
269            ieee: [0; 3],
270            cmic: 0,
271            mdts: 0,
272            cntlid: 0,
273            ver: 0,
274            rtd3r: 0,
275            rtd3e: 0,
276            oaes: 0,
277            ctratt: 0,
278            rrls: 0,
279            rsvd102: [0; 9],
280            cntrltype: 0,
281            fguid: [0; 16],
282            crdt1: 0,
283            crdt2: 0,
284            crdt3: 0,
285            rsvd134: [0; 119],
286            nvmsr: 0,
287            vwci: 0,
288            mec: 0,
289            oacs: 0,
290            acl: 0,
291            aerl: 0,
292            frmw: 0,
293            lpa: 0,
294            elpe: 0,
295            npss: 0,
296            avscc: 0,
297            apsta: 0,
298            wctemp: 0,
299            cctemp: 0,
300            mtfa: 0,
301            hmpre: 0,
302            hmmin: 0,
303            tnvmcap: [0; 16],
304            unvmcap: [0; 16],
305            rpmbs: 0,
306            edstt: 0,
307            dsto: 0,
308            fwug: 0,
309            kas: 0,
310            hctma: 0,
311            mntmt: 0,
312            mxtmt: 0,
313            sanicap: 0,
314            hmminds: 0,
315            hmmaxd: 0,
316            nsetidmax: 0,
317            endgidmax: 0,
318            anatt: 0,
319            anacap: 0,
320            anagrpmax: 0,
321            nanagrpid: 0,
322            pels: 0,
323            domainid: 0,
324            rsvd358: [0; 10],
325            megcap: [0; 16],
326            tmpthha: 0,
327            rsvd385: [0; 127],
328            sqes: 0,
329            cqes: 0,
330            maxcmd: 0,
331            nn: 0,
332            oncs: 0,
333            fuses: 0,
334            fna: 0,
335            vwc: 0,
336            awun: 0,
337            awupf: 0,
338            icsvscc: 0,
339            nwpc: 0,
340            acwu: 0,
341            ocfs: 0,
342            sgls: 0,
343            mnan: 0,
344            maxdna: [0; 16],
345            maxcna: 0,
346            oaqd: 0,
347            rsvd568: [0; 200],
348            subnqn: [0; 256],
349            rsvd1024: [0; 768],
350            ioccsz: 0,
351            iorcsz: 0,
352            icdoff: 0,
353            fcatt: 0,
354            msdbd: 0,
355            ofcs: 0,
356            dctype: 0,
357            rsvd1807: [0; 241],
358            psd: [NvmeIdPsd::default(); 32],
359            vs: NvmeVuIdCtrlField::default(),
360        }
361    }
362}
363
364#[repr(C)]
365#[derive(Debug, Default, Clone, Copy)]
366struct NvmePassthruCmd {
367    opcode: c_uchar,
368    flags: c_uchar,
369    rsvd1: c_ushort,
370    nsid: c_uint,
371    cdw2: c_uint,
372    cdw3: c_uint,
373    metadata: c_ulonglong,
374    addr: c_ulonglong,
375    metadata_len: c_uint,
376    data_len: c_uint,
377    cdw10: c_uint,
378    cdw11: c_uint,
379    cdw12: c_uint,
380    cdw13: c_uint,
381    cdw14: c_uint,
382    cdw15: c_uint,
383    timeout_ms: c_uint,
384    result: c_uint,
385}
386
387type NvmeAdminCmd = NvmePassthruCmd;
388
389#[cfg(feature = "ioctl-nix")]
390mod ioctl_nix {
391    use std::os::fd::{AsFd, AsRawFd};
392
393    use nix::ioctl_readwrite;
394
395    use super::*;
396
397    pub(super) fn nvme_identify_ctrl<F: AsFd>(fd: F) -> Result<NvmeIdCtrl> {
398        ioctl_readwrite!(
399            nvme_identify_ctrl_inner,
400            b'N',
401            NVME_IOCTL_ADMIN_CMD_NUM,
402            NvmeAdminCmd
403        );
404        let mut out = NvmeIdCtrl::default();
405        let out_ptr = &mut out as *mut _;
406        let mut nvme_admin_cmd = NvmeAdminCmd {
407            addr: out_ptr as c_ulonglong,
408            cdw10: 1,
409            data_len: std::mem::size_of::<NvmeIdCtrl>() as c_uint,
410            opcode: NVME_ADMIN_IDENTIFY,
411            ..Default::default()
412        };
413        let nvme_admin_cmd_ptr = &mut nvme_admin_cmd as *mut _;
414        unsafe { nvme_identify_ctrl_inner(fd.as_fd().as_raw_fd(), nvme_admin_cmd_ptr) }?;
415        Ok(out)
416    }
417}
418
419#[cfg(feature = "ioctl-rustix")]
420mod ioctl_rustix {
421    use std::ffi::c_void;
422    use std::os::fd::AsFd;
423
424    use rustix::io;
425    use rustix::ioctl::{ioctl, opcode, Direction, Ioctl, IoctlOutput, Opcode};
426
427    use super::*;
428
429    unsafe impl Ioctl for NvmeAdminCmd {
430        type Output = NvmeIdCtrl;
431
432        const IS_MUTATING: bool = false;
433
434        fn opcode(&self) -> Opcode {
435            opcode::from_components(
436                Direction::ReadWrite,
437                b'N',
438                NVME_IOCTL_ADMIN_CMD_NUM,
439                std::mem::size_of::<NvmeAdminCmd>(),
440            )
441        }
442
443        fn as_ptr(&mut self) -> *mut c_void {
444            self as *const _ as *mut _
445        }
446
447        unsafe fn output_from_ptr(ret: IoctlOutput, ptr: *mut c_void) -> io::Result<Self::Output> {
448            if ret != 0 {
449                return Err(io::Errno::from_raw_os_error(ret));
450            }
451            let sellf = ptr.cast::<NvmeAdminCmd>().read();
452            let data_ptr = sellf.addr as *const NvmeIdCtrl;
453            let output = data_ptr.cast::<NvmeIdCtrl>().read();
454            Ok(output)
455        }
456    }
457
458    pub(super) fn nvme_identify_ctrl<F: AsFd>(fd: F) -> Result<NvmeIdCtrl> {
459        let mut data = NvmeIdCtrl::default();
460        let nvme_admin_cmd = NvmeAdminCmd {
461            addr: &mut data as *mut _ as c_ulonglong,
462            cdw10: 1,
463            data_len: std::mem::size_of::<NvmeIdCtrl>() as c_uint,
464            opcode: NVME_ADMIN_IDENTIFY,
465            ..Default::default()
466        };
467        let output = unsafe { ioctl(fd, nvme_admin_cmd) }?;
468        Ok(output)
469    }
470}
471
472/// A structure containing vendor-specific device names.
473pub struct Names {
474    /// Device name defined in the block device mapping.
475    pub device_name: Option<String>,
476    /// Virtual name for instance store volumes, such as ephemeral0.
477    pub virtual_name: Option<String>,
478
479    // Force internal creation so the name() method cannot panic, by ensuring
480    // either device_name or virtual_name have Some(value).
481    _internal: (),
482}
483
484impl fmt::Debug for Names {
485    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
486        f.debug_struct("Names")
487            .field("device_name", &self.device_name)
488            .field("virtual_name", &self.virtual_name)
489            .finish()
490    }
491}
492
493impl TryFrom<&[c_uchar]> for Names {
494    type Error = Error;
495
496    fn try_from(chars: &[u8]) -> Result<Self> {
497        const COLON: u8 = 0x3a;
498        const SPACE: u8 = 0x20;
499        const NULL: u8 = 0x0;
500
501        // For instance store volumes, the value of chars is delimited by a colon,
502        // and contains <virtual_name>:<device_name> when the device is defined in
503        // the block device mapping. When the device is not defined in the block
504        // device mapping, the value is <virtual_name>:none. The virtual name is
505        // automatically defined as ephemeral<n>, where <n> is incremented for each
506        // attached volume.
507        // For EBS volumes, which must have a device name defined when attached to
508        // an instance, the value has no virtual name and no delimiter. The single
509        // value corresponds to the device name.
510
511        let mut field1 = String::new();
512        let mut field2 = String::new();
513
514        let mut has_delim = false;
515
516        for c in chars.iter() {
517            if *c == NULL || *c == SPACE {
518                break;
519            }
520            if *c == COLON {
521                has_delim = true;
522                continue;
523            }
524            if !has_delim {
525                field1.push(*c as char);
526            } else {
527                field2.push(*c as char);
528            }
529        }
530
531        if field1.starts_with("/dev/") {
532            field1 = field1[5..].into();
533        }
534
535        if field2.starts_with("/dev/") {
536            field2 = field2[5..].into();
537        }
538
539        let device_name = if has_delim {
540            if field2 == "none" {
541                None
542            } else {
543                Some(&field2)
544            }
545        } else {
546            Some(&field1)
547        };
548
549        let virtual_name = if has_delim { Some(&field1) } else { None };
550
551        if device_name.is_none() && virtual_name.is_none() {
552            return Err(Error::UnparseableDeviceName(
553                chars.iter().map(|c| *c as char).collect(),
554            ));
555        }
556
557        Ok(Self {
558            device_name: device_name.cloned(),
559            virtual_name: virtual_name.cloned(),
560            _internal: (),
561        })
562    }
563}
564
565/// The model of the NVMe device.
566#[derive(Debug)]
567pub enum Model {
568    /// Elastic Block Store volume.
569    AmazonElasticBlockStore,
570    /// Instance store volume.
571    AmazonInstanceStore,
572}
573
574/// The vendor ID of the NVMe device.
575#[derive(Debug)]
576pub struct VendorId(pub u16);
577
578/// An NVMe device, containing a subset of all identifying information.
579#[derive(Debug)]
580pub struct Nvme {
581    /// The [model](Model) of the device.
582    pub model: Model,
583    /// The [structure](Names) containing vendor-specific device names.
584    pub names: Names,
585    /// The [vendor ID](VendorId) of the device.
586    pub vendor_id: VendorId,
587}
588
589impl Nvme {
590    /// Get the vendor specific device name or fall back to the virtual name if it
591    /// is an instance store volume.
592    pub fn name(&self) -> &str {
593        self.names
594            .device_name
595            .as_ref()
596            .unwrap_or_else(|| self.names.virtual_name.as_ref().unwrap())
597    }
598
599    fn from_fd<F, IoctlFn>(fd: F, f: IoctlFn) -> Result<Self>
600    where
601        F: AsFd,
602        IoctlFn: FnOnce(F) -> Result<NvmeIdCtrl>,
603    {
604        let ctrl = f(fd)?;
605        if ctrl.vid != AMZ_VENDOR_ID {
606            return Err(Error::UnrecognizedVendorId(ctrl.vid));
607        }
608        let mut model_str = String::from_iter(ctrl.mn.map(|c| c as u8 as char));
609        model_str.truncate(model_str.trim_end().len());
610        let model = match model_str.as_str() {
611            AMZ_EBS_MN => Model::AmazonElasticBlockStore,
612            AMZ_INST_STORE_MN => Model::AmazonInstanceStore,
613            _ => return Err(Error::UnrecognizedModel(model_str)),
614        };
615        let names = ctrl.vs.bdev.as_slice().try_into()?;
616        Ok(Self {
617            model,
618            names,
619            vendor_id: VendorId(ctrl.vid),
620        })
621    }
622}
623
624#[cfg(any(feature = "ioctl-nix", feature = "ioctl-rustix"))]
625impl TryFrom<File> for Nvme {
626    type Error = Error;
627
628    #[cfg(feature = "ioctl-nix")]
629    fn try_from(f: File) -> Result<Self> {
630        Self::from_fd(f.as_fd(), ioctl_nix::nvme_identify_ctrl)
631    }
632
633    #[cfg(feature = "ioctl-rustix")]
634    fn try_from(f: File) -> Result<Self> {
635        Self::from_fd(f.as_fd(), ioctl_rustix::nvme_identify_ctrl)
636    }
637}
638
639#[cfg(all(feature = "ioctl-nix", feature = "ioctl-rustix"))]
640compile_error!("The features ioctl-nix and ioctl-rustix are mutually exclusive");