Skip to main content

fmd_adm/
lib.rs

1pub use illumos_nvpair::{NvList, NvValue};
2
3use std::ffi::{CStr, CString};
4use std::os::raw::c_void;
5
6use uuid::Uuid;
7
8use fmd_adm_sys::{
9    FMD_ADM_MOD_FAILED, FMD_ADM_PROGRAM, FMD_ADM_RSRC_FAULTY, FMD_ADM_RSRC_INVISIBLE,
10    FMD_ADM_RSRC_UNUSABLE, FMD_ADM_SERD_FIRED, FMD_ADM_VERSION, FMD_TYPE_BOOL, FMD_TYPE_INT32,
11    FMD_TYPE_INT64, FMD_TYPE_SIZE, FMD_TYPE_STRING, FMD_TYPE_TIME, FMD_TYPE_UINT32,
12    FMD_TYPE_UINT64, fmd_adm_caseinfo_t, fmd_adm_close, fmd_adm_errmsg, fmd_adm_modinfo_t,
13    fmd_adm_rsrcinfo_t, fmd_adm_serdinfo_t, fmd_adm_stats_t, fmd_adm_t, fmd_stat_t,
14};
15
16#[derive(Debug, thiserror::Error)]
17pub enum Error {
18    #[error("failed to open fmd adm handle")]
19    Open,
20    #[error("fmd: {0}")]
21    Fmd(String),
22    #[error("interior nul byte in string argument")]
23    Nul(#[from] std::ffi::NulError),
24    #[error("invalid UUID from fmd: {0}")]
25    Uuid(#[from] uuid::Error),
26}
27
28/// A handle to the Fault Management Daemon administrative interface.
29///
30/// This handle wraps a C `fmd_adm_t` pointer that is not thread-safe.
31/// `FmdAdm` is `!Send` and `!Sync` — it cannot be shared across threads.
32///
33/// All iterator methods (`modules()`, `cases()`, `resources()`, etc.)
34/// eagerly collect results into a `Vec`. The underlying C API uses
35/// callbacks that own the data only for the duration of each
36/// invocation, so results must be copied before the callback returns.
37pub struct FmdAdm {
38    handle: *mut fmd_adm_t,
39}
40
41/// Safely convert a C string pointer to an owned `String`.
42/// Returns an empty string if the pointer is null.
43///
44/// # Safety
45///
46/// If non-null, `p` must point to a valid, nul-terminated C string.
47unsafe fn cstr_to_owned(p: *const std::os::raw::c_char) -> String {
48    if p.is_null() {
49        String::new()
50    } else {
51        unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned()
52    }
53}
54
55impl FmdAdm {
56    /// Open a connection to the local fault management daemon.
57    pub fn open() -> Result<Self, Error> {
58        let handle = unsafe {
59            fmd_adm_sys::fmd_adm_open(std::ptr::null(), FMD_ADM_PROGRAM, FMD_ADM_VERSION as i32)
60        };
61        if handle.is_null() {
62            return Err(Error::Open);
63        }
64        Ok(Self { handle })
65    }
66
67    fn errmsg(&self) -> String {
68        let p = unsafe { fmd_adm_errmsg(self.handle) };
69        if p.is_null() {
70            "unknown error".to_string()
71        } else {
72            unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned()
73        }
74    }
75
76    /// Iterate over loaded FMD modules.
77    pub fn modules(&self) -> Result<Vec<ModuleInfo>, Error> {
78        let mut results: Vec<ModuleInfo> = Vec::new();
79
80        unsafe extern "C" fn callback(
81            info: *const fmd_adm_modinfo_t,
82            arg: *mut c_void,
83        ) -> std::os::raw::c_int {
84            unsafe {
85                let vec = &mut *(arg as *mut Vec<ModuleInfo>);
86                let info = &*info;
87                vec.push(ModuleInfo {
88                    name: cstr_to_owned(info.ami_name),
89                    description: cstr_to_owned(info.ami_desc),
90                    version: cstr_to_owned(info.ami_vers),
91                    failed: (info.ami_flags & FMD_ADM_MOD_FAILED) != 0,
92                });
93                0
94            }
95        }
96
97        let rc = unsafe {
98            fmd_adm_sys::fmd_adm_module_iter(
99                self.handle,
100                Some(callback),
101                &mut results as *mut _ as *mut c_void,
102            )
103        };
104        if rc != 0 {
105            return Err(Error::Fmd(self.errmsg()));
106        }
107        Ok(results)
108    }
109
110    /// Iterate over faulty resources.
111    ///
112    /// If `all` is true, includes resources that are not directly visible.
113    pub fn resources(&self, all: bool) -> Result<Vec<ResourceInfo>, Error> {
114        // Collect raw strings from the callback, then parse UUIDs
115        // afterwards so we can propagate errors.
116        struct RawResourceInfo {
117            fmri: String,
118            uuid: String,
119            case: String,
120            flags: u32,
121        }
122
123        let mut raw: Vec<RawResourceInfo> = Vec::new();
124
125        unsafe extern "C" fn callback(
126            info: *const fmd_adm_rsrcinfo_t,
127            arg: *mut c_void,
128        ) -> std::os::raw::c_int {
129            unsafe {
130                let vec = &mut *(arg as *mut Vec<RawResourceInfo>);
131                let info = &*info;
132                vec.push(RawResourceInfo {
133                    fmri: cstr_to_owned(info.ari_fmri),
134                    uuid: cstr_to_owned(info.ari_uuid),
135                    case: cstr_to_owned(info.ari_case),
136                    flags: info.ari_flags,
137                });
138                0
139            }
140        }
141
142        let rc = unsafe {
143            fmd_adm_sys::fmd_adm_rsrc_iter(
144                self.handle,
145                if all { 1 } else { 0 },
146                Some(callback),
147                &mut raw as *mut _ as *mut c_void,
148            )
149        };
150        if rc != 0 {
151            return Err(Error::Fmd(self.errmsg()));
152        }
153
154        raw.into_iter()
155            .map(|r| {
156                Ok(ResourceInfo {
157                    fmri: r.fmri,
158                    uuid: r.uuid.parse()?,
159                    case: r.case.parse()?,
160                    faulty: (r.flags & FMD_ADM_RSRC_FAULTY) != 0,
161                    unusable: (r.flags & FMD_ADM_RSRC_UNUSABLE) != 0,
162                    invisible: (r.flags & FMD_ADM_RSRC_INVISIBLE) != 0,
163                })
164            })
165            .collect()
166    }
167
168    /// Get the count of faulty resources.
169    pub fn resource_count(&self, all: bool) -> Result<u32, Error> {
170        let mut count: u32 = 0;
171        let rc = unsafe {
172            fmd_adm_sys::fmd_adm_rsrc_count(self.handle, if all { 1 } else { 0 }, &mut count)
173        };
174        if rc != 0 {
175            return Err(Error::Fmd(self.errmsg()));
176        }
177        Ok(count)
178    }
179
180    /// Mark a resource (by FMRI) as repaired.
181    pub fn resource_repaired(&mut self, fmri: &str) -> Result<(), Error> {
182        let fmri = CString::new(fmri)?;
183        let rc = unsafe { fmd_adm_sys::fmd_adm_rsrc_repaired(self.handle, fmri.as_ptr()) };
184        if rc != 0 {
185            return Err(Error::Fmd(self.errmsg()));
186        }
187        Ok(())
188    }
189
190    /// Mark a resource (by FMRI) as replaced.
191    pub fn resource_replaced(&mut self, fmri: &str) -> Result<(), Error> {
192        let fmri = CString::new(fmri)?;
193        let rc = unsafe { fmd_adm_sys::fmd_adm_rsrc_replaced(self.handle, fmri.as_ptr()) };
194        if rc != 0 {
195            return Err(Error::Fmd(self.errmsg()));
196        }
197        Ok(())
198    }
199
200    /// Acquit a resource (by FMRI) for a specific case UUID.
201    pub fn resource_acquit(&mut self, fmri: &str, case_uuid: &Uuid) -> Result<(), Error> {
202        let fmri = CString::new(fmri)?;
203        let uuid_c = CString::new(case_uuid.to_string())?;
204        let rc = unsafe {
205            fmd_adm_sys::fmd_adm_rsrc_acquit(self.handle, fmri.as_ptr(), uuid_c.as_ptr())
206        };
207        if rc != 0 {
208            return Err(Error::Fmd(self.errmsg()));
209        }
210        Ok(())
211    }
212
213    /// Iterate over cases, optionally filtered by URL.
214    pub fn cases(&self, url: Option<&str>) -> Result<Vec<CaseInfo>, Error> {
215        struct RawCaseInfo {
216            uuid: String,
217            code: String,
218            url: String,
219            event: Option<NvList>,
220        }
221
222        let mut raw: Vec<RawCaseInfo> = Vec::new();
223        let url_c = url.map(CString::new).transpose()?;
224
225        unsafe extern "C" fn callback(
226            info: *const fmd_adm_caseinfo_t,
227            arg: *mut c_void,
228        ) -> std::os::raw::c_int {
229            unsafe {
230                let vec = &mut *(arg as *mut Vec<RawCaseInfo>);
231                let info = &*info;
232                let event = if info.aci_event.is_null() {
233                    None
234                } else {
235                    // SAFETY: from_raw borrows the nvlist and deep-copies
236                    // all values into owned Rust types. It does not take
237                    // ownership or free the pointer — fmd_adm_case_iter
238                    // remains responsible for calling nvlist_free after
239                    // this callback returns.
240                    NvList::from_raw(info.aci_event.cast()).ok()
241                };
242                vec.push(RawCaseInfo {
243                    uuid: cstr_to_owned(info.aci_uuid),
244                    code: cstr_to_owned(info.aci_code),
245                    url: cstr_to_owned(info.aci_url),
246                    event,
247                });
248                0
249            }
250        }
251
252        let url_ptr = url_c
253            .as_ref()
254            .map(|c| c.as_ptr())
255            .unwrap_or(std::ptr::null());
256
257        let rc = unsafe {
258            fmd_adm_sys::fmd_adm_case_iter(
259                self.handle,
260                url_ptr,
261                Some(callback),
262                &mut raw as *mut _ as *mut c_void,
263            )
264        };
265        if rc != 0 {
266            return Err(Error::Fmd(self.errmsg()));
267        }
268
269        raw.into_iter()
270            .map(|r| {
271                Ok(CaseInfo {
272                    uuid: r.uuid.parse()?,
273                    code: r.code,
274                    url: r.url,
275                    event: r.event,
276                })
277            })
278            .collect()
279    }
280
281    /// Acquit a case by UUID.
282    pub fn case_acquit(&mut self, uuid: &Uuid) -> Result<(), Error> {
283        let uuid = CString::new(uuid.to_string())?;
284        let rc = unsafe { fmd_adm_sys::fmd_adm_case_acquit(self.handle, uuid.as_ptr()) };
285        if rc != 0 {
286            return Err(Error::Fmd(self.errmsg()));
287        }
288        Ok(())
289    }
290
291    /// Iterate over SERD engines for a module.
292    pub fn serd_engines(&self, module: &str) -> Result<Vec<SerdInfo>, Error> {
293        let mut results: Vec<SerdInfo> = Vec::new();
294        let module = CString::new(module)?;
295
296        unsafe extern "C" fn callback(
297            info: *const fmd_adm_serdinfo_t,
298            arg: *mut c_void,
299        ) -> std::os::raw::c_int {
300            unsafe {
301                let vec = &mut *(arg as *mut Vec<SerdInfo>);
302                let info = &*info;
303                vec.push(SerdInfo {
304                    name: cstr_to_owned(info.asi_name),
305                    delta_ns: info.asi_delta,
306                    n: info.asi_n,
307                    t_ns: info.asi_t,
308                    count: info.asi_count,
309                    fired: (info.asi_flags & FMD_ADM_SERD_FIRED) != 0,
310                });
311                0
312            }
313        }
314
315        let rc = unsafe {
316            fmd_adm_sys::fmd_adm_serd_iter(
317                self.handle,
318                module.as_ptr(),
319                Some(callback),
320                &mut results as *mut _ as *mut c_void,
321            )
322        };
323        if rc != 0 {
324            return Err(Error::Fmd(self.errmsg()));
325        }
326        Ok(results)
327    }
328
329    /// Iterate over transports.
330    pub fn transports(&self) -> Result<Vec<TransportId>, Error> {
331        let mut results: Vec<TransportId> = Vec::new();
332
333        unsafe extern "C" fn callback(id: fmd_adm_sys::id_t, arg: *mut c_void) {
334            unsafe {
335                let vec = &mut *(arg as *mut Vec<TransportId>);
336                vec.push(TransportId(id));
337            }
338        }
339
340        let rc = unsafe {
341            fmd_adm_sys::fmd_adm_xprt_iter(
342                self.handle,
343                Some(callback),
344                &mut results as *mut _ as *mut c_void,
345            )
346        };
347        if rc != 0 {
348            return Err(Error::Fmd(self.errmsg()));
349        }
350        Ok(results)
351    }
352
353    /// Read statistics, optionally for a specific module.
354    pub fn stats(&self, module: Option<&str>) -> Result<Vec<Stat>, Error> {
355        let module_c = module.map(CString::new).transpose()?;
356        let module_ptr = module_c
357            .as_ref()
358            .map(|c| c.as_ptr())
359            .unwrap_or(std::ptr::null());
360
361        let mut raw_stats = fmd_adm_stats_t {
362            ams_buf: std::ptr::null_mut(),
363            ams_len: 0,
364        };
365
366        let rc =
367            unsafe { fmd_adm_sys::fmd_adm_stats_read(self.handle, module_ptr, &mut raw_stats) };
368        if rc != 0 {
369            return Err(Error::Fmd(self.errmsg()));
370        }
371
372        // Ensure stats_free runs even if from_raw or collect panics.
373        struct StatsGuard<'a> {
374            handle: *mut fmd_adm_t,
375            raw: &'a mut fmd_adm_stats_t,
376        }
377        impl Drop for StatsGuard<'_> {
378            fn drop(&mut self) {
379                unsafe {
380                    fmd_adm_sys::fmd_adm_stats_free(self.handle, self.raw);
381                }
382            }
383        }
384        let guard = StatsGuard {
385            handle: self.handle,
386            raw: &mut raw_stats,
387        };
388
389        let len = guard.raw.ams_len as usize;
390        let stats = if len == 0 || guard.raw.ams_buf.is_null() {
391            Vec::new()
392        } else {
393            let slice = unsafe { std::slice::from_raw_parts(guard.raw.ams_buf, len) };
394            slice.iter().map(|s| unsafe { Stat::from_raw(s) }).collect()
395        };
396
397        // Explicitly drop to free the C buffer before returning.
398        drop(guard);
399
400        Ok(stats)
401    }
402}
403
404impl Drop for FmdAdm {
405    fn drop(&mut self) {
406        unsafe { fmd_adm_close(self.handle) };
407    }
408}
409
410#[derive(Debug, Clone)]
411pub struct ModuleInfo {
412    pub name: String,
413    pub description: String,
414    pub version: String,
415    pub failed: bool,
416}
417
418#[derive(Debug, Clone)]
419pub struct ResourceInfo {
420    pub fmri: String,
421    pub uuid: Uuid,
422    pub case: Uuid,
423    pub faulty: bool,
424    pub unusable: bool,
425    pub invisible: bool,
426}
427
428#[derive(Debug, Clone)]
429pub struct CaseInfo {
430    pub uuid: Uuid,
431    pub code: String,
432    pub url: String,
433    /// The full fault event payload as an nvlist, if present.
434    pub event: Option<NvList>,
435}
436
437#[derive(Debug, Clone)]
438pub struct SerdInfo {
439    pub name: String,
440    /// Nanoseconds from oldest event to now.
441    pub delta_ns: u64,
442    /// N parameter (event count threshold).
443    pub n: u64,
444    /// T parameter (nanoseconds window).
445    pub t_ns: u64,
446    /// Number of events currently in engine.
447    pub count: u32,
448    /// Whether the SERD engine has fired.
449    pub fired: bool,
450}
451
452/// An opaque identifier for an FMD transport.
453#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
454pub struct TransportId(i32);
455
456impl TransportId {
457    pub fn as_raw(&self) -> i32 {
458        self.0
459    }
460}
461
462impl std::fmt::Display for TransportId {
463    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
464        write!(f, "{}", self.0)
465    }
466}
467
468#[derive(Debug, Clone)]
469pub enum StatValue {
470    Bool(bool),
471    Int32(i32),
472    UInt32(u32),
473    Int64(i64),
474    UInt64(u64),
475    /// A time value in nanoseconds.
476    Time(u64),
477    /// A size value in bytes.
478    Size(u64),
479    String(String),
480    /// A stat type not recognized by this crate.
481    Unknown {
482        type_code: u32,
483        raw: u64,
484    },
485}
486
487impl std::fmt::Display for StatValue {
488    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
489        match self {
490            StatValue::Bool(v) => write!(f, "{v}"),
491            StatValue::Int32(v) => write!(f, "{v}"),
492            StatValue::UInt32(v) => write!(f, "{v}"),
493            StatValue::Int64(v) => write!(f, "{v}"),
494            StatValue::UInt64(v) => write!(f, "{v}"),
495            StatValue::Time(v) => write!(f, "{v}ns"),
496            StatValue::Size(v) => write!(f, "{v}B"),
497            StatValue::String(v) => write!(f, "{v}"),
498            StatValue::Unknown { type_code, raw } => {
499                write!(f, "unknown(type={type_code}, raw={raw})")
500            }
501        }
502    }
503}
504
505#[derive(Debug, Clone)]
506pub struct Stat {
507    pub name: String,
508    pub description: String,
509    pub value: StatValue,
510}
511
512impl Stat {
513    /// Convert a raw `fmd_stat_t` into an owned `Stat`.
514    ///
515    /// # Safety
516    /// The `fmd_stat_t` must have been obtained from a valid fmd call.
517    unsafe fn from_raw(raw: &fmd_stat_t) -> Self {
518        let name = unsafe { CStr::from_ptr(raw.fmds_name.as_ptr()) }
519            .to_string_lossy()
520            .into_owned();
521        let description = unsafe { CStr::from_ptr(raw.fmds_desc.as_ptr()) }
522            .to_string_lossy()
523            .into_owned();
524        let value = match raw.fmds_type {
525            FMD_TYPE_BOOL => StatValue::Bool(unsafe { raw.fmds_value.bool_ } != 0),
526            FMD_TYPE_INT32 => StatValue::Int32(unsafe { raw.fmds_value.i32_ }),
527            FMD_TYPE_UINT32 => StatValue::UInt32(unsafe { raw.fmds_value.ui32 }),
528            FMD_TYPE_INT64 => StatValue::Int64(unsafe { raw.fmds_value.i64_ }),
529            FMD_TYPE_UINT64 => StatValue::UInt64(unsafe { raw.fmds_value.ui64 }),
530            FMD_TYPE_TIME => StatValue::Time(unsafe { raw.fmds_value.ui64 }),
531            FMD_TYPE_SIZE => StatValue::Size(unsafe { raw.fmds_value.ui64 }),
532            FMD_TYPE_STRING => {
533                let p = unsafe { raw.fmds_value.str_ };
534                if p.is_null() {
535                    StatValue::String(String::new())
536                } else {
537                    StatValue::String(unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned())
538                }
539            }
540            other => StatValue::Unknown {
541                type_code: other,
542                raw: unsafe { raw.fmds_value.ui64 },
543            },
544        };
545        Self {
546            name,
547            description,
548            value,
549        }
550    }
551}