diff --git a/README.md b/README.md index 4859276..f451012 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,45 @@ # libtopo -Rust bindings for illumos `libtopo` +Rust bindings for illumos +[libtopo](https://illumos.org/man/3LIB/libtopo), the hardware topology +library used by FMA (Fault Management Architecture) and other illumos +consumers. + +This repo contains two crates: + +- **libtopo-sys** — raw FFI bindings generated by + [bindgen](https://github.com/rust-lang/rust-bindgen) +- **libtopo** — idiomatic Rust wrapper with no raw pointers in the + public API + +These crates target illumos only and link against the system `libtopo`. + +## Usage + +```rust +use libtopo::{Scheme, TopoHdl, WalkAction}; + +let hdl = TopoHdl::open()?; +let snap = hdl.snapshot()?; + +snap.walk(Scheme::Hc, |node| { + let fmri = node.resource()?; + println!("{}[{}]\t{}", node.name(), node.instance(), hdl.fmri_to_string(&fmri)?); + Ok(WalkAction::Continue) +})?; +``` + +See [`libtopo/examples/`](libtopo/examples/) for runnable programs. + +## Privileges + +Most libtopo operations require elevated privileges to enumerate +hardware. Run with `pfexec` or appropriate RBAC profiles. + +## Testing + +```sh +pfexec cargo test +``` + +Tests must be run on an illumos host since they link against `libtopo`. diff --git a/libtopo/README.md b/libtopo/README.md index 04083d9..46dd64d 100644 --- a/libtopo/README.md +++ b/libtopo/README.md @@ -2,5 +2,40 @@ Idiomatic Rust bindings for illumos `libtopo`. -This create provids a safe wrapper around the raw FFI bindings in -[libtopo-sys](../libtopo-sys/). \ No newline at end of file +This crate provides a safe wrapper around the raw FFI bindings in +[libtopo-sys](../libtopo-sys/), the hardware topology library used +by FMA (Fault Management Architecture) and other illumos consumers. + +## Usage + +```rust +use libtopo::{Scheme, TopoHdl, WalkAction}; + +let hdl = TopoHdl::open()?; +let snap = hdl.snapshot()?; +println!("snapshot uuid: {}", snap.uuid()); + +snap.walk(Scheme::Hc, |node| { + let fmri = node.resource()?; + let fmri_str = hdl.fmri_to_string(&fmri)?; + println!("{}[{}]\t{}", node.name(), node.instance(), fmri_str); + Ok(WalkAction::Continue) +})?; +``` + +See [`examples/`](examples/) for runnable programs. + +## Privileges + +Most libtopo operations require elevated privileges to enumerate +hardware. Run with `pfexec` or appropriate RBAC profiles. + +## Testing + +Tests must be run on an illumos host since they link against `libtopo`. +Walker-dependent integration tests gracefully skip when the host has no +hardware topology (e.g. a CI VM). + +```sh +pfexec cargo test +``` diff --git a/libtopo/examples/fmri_resolve.rs b/libtopo/examples/fmri_resolve.rs new file mode 100644 index 0000000..9776554 --- /dev/null +++ b/libtopo/examples/fmri_resolve.rs @@ -0,0 +1,50 @@ +//! Parse an FMRI string and report its status (present/replaced/unusable). +//! +//! Run with `pfexec`: +//! +//! ```sh +//! pfexec cargo run --example fmri_resolve -- 'hc:///chassis=0' +//! ``` + +use libtopo::TopoHdl; + +fn main() { + let args: Vec = std::env::args().collect(); + if args.len() < 2 { + eprintln!( + "usage: {} ", + args.first().map_or("fmri_resolve", |s| s.as_str()) + ); + std::process::exit(2); + } + let fmri_str = &args[1]; + + let hdl = TopoHdl::open().expect("failed to open libtopo handle"); + // Hold a snapshot — fmri_present/replaced/unusable consult the + // topology to resolve the resource. + let _snap = hdl.snapshot().expect("failed to take snapshot"); + + let fmri = match hdl.fmri_parse(fmri_str) { + Ok(f) => f, + Err(e) => { + eprintln!("fmri_parse failed: {e}"); + std::process::exit(1); + } + }; + + let formatted = hdl + .fmri_to_string(&fmri) + .unwrap_or_else(|e| format!("")); + println!("fmri: {formatted}"); + + print_predicate("present", hdl.fmri_present(&fmri)); + print_predicate("replaced", hdl.fmri_replaced(&fmri)); + print_predicate("unusable", hdl.fmri_unusable(&fmri)); +} + +fn print_predicate(name: &str, result: Result) { + match result { + Ok(b) => println!("{name}: {b}"), + Err(e) => println!("{name}: error: {e}"), + } +} diff --git a/libtopo/examples/list_topology.rs b/libtopo/examples/list_topology.rs new file mode 100644 index 0000000..146f24e --- /dev/null +++ b/libtopo/examples/list_topology.rs @@ -0,0 +1,147 @@ +//! Open a libtopo handle, take a snapshot, and walk a topology scheme, +//! printing each node's name, instance, FMRI, and label (if any). +//! +//! The scheme defaults to `hc`; pass another scheme name as the first +//! positional argument to walk it instead. Pass `-v` / `--verbose` +//! (anywhere in argv) to also dump every property group and property: +//! +//! ```sh +//! pfexec cargo run --example list_topology # hc (default) +//! pfexec cargo run --example list_topology -- cpu +//! pfexec cargo run --example list_topology -- pcie -v +//! pfexec cargo run --example list_topology -- -v +//! ``` + +use libtopo::{PropValue, Scheme, TopoHdl, WalkAction}; + +fn main() { + let args: Vec = std::env::args().collect(); + let verbose = args.iter().any(|a| a == "-v" || a == "--verbose"); + let scheme = match args + .iter() + .skip(1) + .find(|a| !a.starts_with('-')) + .map(String::as_str) + { + None => Scheme::Hc, + Some(name) => match parse_scheme(name) { + Some(s) => s, + None => { + eprintln!("unknown scheme: {name:?}"); + eprintln!( + "valid schemes: hc, cpu, mem, dev, mod, svc, sw, zfs, pcie, path, fmd, pkg, legacy-hc" + ); + std::process::exit(2); + } + }, + }; + + let hdl = TopoHdl::open().expect("failed to open libtopo handle"); + let snap = hdl.snapshot().expect("failed to take snapshot"); + + println!("snapshot uuid: {}", snap.uuid()); + println!("scheme: {:?}", scheme); + println!(); + + let walk = snap.walk(scheme, |node| { + let fmri = match node.resource() { + Ok(f) => f, + Err(e) => { + eprintln!( + "warn: resource lookup failed on node {}[{}]: {e}", + node.name(), + node.instance(), + ); + return Ok(WalkAction::Continue); + } + }; + let fmri_str = hdl + .fmri_to_string(&fmri) + .unwrap_or_else(|e| format!("")); + println!("{}[{}]\t{}", node.name(), node.instance(), fmri_str); + if let Ok(label) = node.label() + && !label.is_empty() + { + println!(" label: {label}"); + } + + if verbose { + match node.property_groups() { + Ok(groups) => { + for pg in &groups { + println!(" [{}]", pg.name); + for p in &pg.properties { + print_prop_value(&hdl, &p.name, &p.value); + } + } + } + Err(e) => eprintln!( + "warn: property_groups failed on {}[{}]: {e}", + node.name(), + node.instance(), + ), + } + } + Ok(WalkAction::Continue) + }); + + if let Err(e) = walk { + eprintln!("walk failed: {e}"); + std::process::exit(1); + } +} + +fn print_prop_value(hdl: &TopoHdl, name: &str, v: &PropValue) { + match v { + PropValue::String(s) => println!(" {name} = {s:?}"), + PropValue::Boolean(v) => println!(" {name} = {v}"), + PropValue::Int32(v) => println!(" {name} = {v}"), + PropValue::UInt32(v) => println!(" {name} = {v}"), + PropValue::Int64(v) => println!(" {name} = {v}"), + PropValue::UInt64(v) => println!(" {name} = {v}"), + PropValue::Double(v) => println!(" {name} = {v}"), + PropValue::Fmri(f) => match hdl.fmri_to_string(f) { + Ok(s) => println!(" {name} = {s}"), + Err(_) => println!(" {name} = "), + }, + PropValue::Time(t) => println!(" {name} = {t} (time)"), + PropValue::Size(b) => println!(" {name} = {b} bytes"), + PropValue::StringArray(vs) => println!(" {name} = {vs:?}"), + PropValue::Int32Array(vs) => println!(" {name} = {vs:?}"), + PropValue::UInt32Array(vs) => println!(" {name} = {vs:?}"), + PropValue::Int64Array(vs) => println!(" {name} = {vs:?}"), + PropValue::UInt64Array(vs) => println!(" {name} = {vs:?}"), + PropValue::FmriArray(fs) => { + let rendered: Vec = fs + .iter() + .map(|f| { + hdl.fmri_to_string(f) + .unwrap_or_else(|_| "".into()) + }) + .collect(); + println!(" {name} = {rendered:?}"); + } + PropValue::Unknown { type_code } => { + println!(" {name} = ") + } + } +} + +fn parse_scheme(s: &str) -> Option { + match s { + "hc" => Some(Scheme::Hc), + "cpu" => Some(Scheme::Cpu), + "mem" => Some(Scheme::Mem), + "dev" => Some(Scheme::Dev), + "mod" => Some(Scheme::Mod), + "svc" => Some(Scheme::Svc), + "sw" => Some(Scheme::Sw), + "zfs" => Some(Scheme::Zfs), + "pcie" => Some(Scheme::Pcie), + "path" => Some(Scheme::Path), + "fmd" => Some(Scheme::Fmd), + "pkg" => Some(Scheme::Pkg), + "legacy-hc" => Some(Scheme::Legacy), + _ => None, + } +}