Skip to content

Commit fe722a5

Browse files
authored
Merge pull request #149 from ytsssun/pciclient
source: add pciclient crate for high level access to lspci
2 parents a53cf79 + 75c225c commit fe722a5

File tree

9 files changed

+588
-0
lines changed

9 files changed

+588
-0
lines changed

packages/os/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ source-groups = [
1111
"bottlerocket-release",
1212
"metricdog",
1313
"parse-datetime",
14+
"pciclient",
1415
"ghostdog",
1516
"updater",
1617
"logdog",

sources/Cargo.lock

Lines changed: 53 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sources/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ members = [
6464

6565
"parse-datetime",
6666

67+
"pciclient",
68+
6769
"retry-read",
6870

6971
"updater/block-party",
@@ -90,6 +92,7 @@ generate-readme = { version = "0.1", path = "generate-readme" }
9092
imdsclient = { version = "0.1", path = "imdsclient" }
9193
models = { version = "0.1", path = "models" }
9294
parse-datetime = { version = "0.1", path = "parse-datetime" }
95+
pciclient = { version = "0.1", path = "pciclient" }
9396
retry-read = { version = "0.1", path = "retry-read" }
9497
signpost = { version = "0.1", path = "updater/signpost" }
9598
storewolf = { version = "0.1", path = "api/storewolf" }
@@ -114,12 +117,14 @@ aws-smithy-runtime = "1"
114117
aws-smithy-types = "1"
115118
aws-types = "1"
116119
bit_field = "0.10"
120+
bon = "2"
117121
bytes = "1"
118122
cached = "0.49"
119123
cargo-readme = "3"
120124
chrono = { version = "0.4", default-features = false }
121125
cidr = "0.2"
122126
darling = { version = "0.20", default-features = false }
127+
derive-getters = "0.5"
123128
dns-lookup = "2"
124129
env_logger = "0.11"
125130
envy = "0.4"

sources/pciclient/Cargo.toml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[package]
2+
name = "pciclient"
3+
version = "0.1.0"
4+
authors = ["Yutong Sun <yutongsu@amazon.com>"]
5+
license = "Apache-2.0 OR MIT"
6+
edition = "2021"
7+
publish = false
8+
build = "build.rs"
9+
# Don't rebuild crate just because of changes to README.
10+
exclude = ["README.md"]
11+
12+
[dependencies]
13+
bon.workspace = true
14+
derive-getters.workspace = true
15+
log.workspace = true
16+
serde_json.workspace = true
17+
snafu.workspace = true
18+
19+
[build-dependencies]
20+
generate-readme.workspace = true
21+
22+
[dev-dependencies]
23+
test-case.workspace = true

sources/pciclient/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# pciclient
2+
3+
Current version: 0.1.0
4+
5+
`pciclient` provides high-level methods to invoke `lspci` and query for attached devices information.
6+
7+
pciclient provides util functions that can:
8+
- List the devices attached with some filtering option.
9+
- Detect specifically if EFA device is attached.
10+
11+
## Colophon
12+
13+
This text was generated from `README.tpl` using [cargo-readme](https://crates.io/crates/cargo-readme), and includes the rustdoc from `src/lib.rs`.

sources/pciclient/README.tpl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# {{crate}}
2+
3+
Current version: {{version}}
4+
5+
{{readme}}
6+
7+
## Colophon
8+
9+
This text was generated from `README.tpl` using [cargo-readme](https://crates.io/crates/cargo-readme), and includes the rustdoc from `src/lib.rs`.

sources/pciclient/build.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
fn main() {
2+
generate_readme::from_lib().unwrap();
3+
}

sources/pciclient/src/lib.rs

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/*!
2+
`pciclient` provides high-level methods to invoke `lspci` and query for attached devices information.
3+
4+
pciclient provides util functions that can:
5+
- List the devices attached with some filtering option.
6+
- Detect specifically if EFA device is attached.
7+
*/
8+
mod private;
9+
10+
use private::{call_list_devices, check_efa_attachment, PciClient};
11+
12+
use bon::Builder;
13+
use derive_getters::Getters;
14+
use std::ffi::OsString;
15+
16+
/// This [`ListDevicesParam`] is based on the "-d" flag for `lspci`, which
17+
/// allows selection/filtering of the output. Spec for "-d":
18+
/// ```sh
19+
/// -d [<vendor>]:[<device>][:<class>[:<prog-if>]]
20+
///
21+
/// Show only devices with specified vendor, device, class ID,
22+
/// and programming interface. The ID's are given in
23+
/// hexadecimal and may be omitted or given as "*", both
24+
/// meaning "any value". The class ID can contain "x"
25+
/// characters which stand for "any digit".
26+
/// ```
27+
#[derive(Debug, Default, PartialEq, Builder)]
28+
pub struct ListDevicesParam {
29+
#[builder(into)]
30+
vendor: Option<String>,
31+
#[builder(into)]
32+
device: Option<String>,
33+
#[builder(into)]
34+
class: Option<String>,
35+
#[builder(into)]
36+
program_interface: Option<String>,
37+
}
38+
39+
impl ListDevicesParam {
40+
/// Convert the ListDevicesParam into proper command line arguments so that
41+
/// we can feed it to the lspci() function where it makes the actual call to the
42+
/// lspci binary.
43+
///
44+
/// As mentioned in [`list_devices`], we will use "-n" and "-m" to decorate the output
45+
/// so that it returns machine-readable format which ensures compatibility and numeric output
46+
/// for the vendor code and device codes.
47+
fn into_command_args(self) -> Vec<OsString> {
48+
let mut args: Vec<String> = vec!["-n".into(), "-m".into()];
49+
if self != ListDevicesParam::default() {
50+
let parts: Vec<String> =
51+
vec![self.vendor, self.device, self.class, self.program_interface]
52+
.into_iter()
53+
.map(|part| part.unwrap_or_default())
54+
.collect();
55+
let additional_args = parts.join(":");
56+
args.push("-d".to_string());
57+
args.push(additional_args);
58+
}
59+
args.into_iter().map(OsString::from).collect()
60+
}
61+
}
62+
63+
/// The [`ListDevicesOutput`] is based on the output format that is decorated by "-n -m".
64+
#[derive(Debug, Default, PartialEq, Eq, Getters)]
65+
pub struct ListDevicesOutput {
66+
pci_slot: String,
67+
class: String,
68+
vendor: String,
69+
device: String,
70+
subsystem_vendor: Option<String>,
71+
subsystem_device: Option<String>,
72+
revision: Option<String>,
73+
program_interface: Option<String>,
74+
}
75+
76+
/// Query the list of the devices with options to filter the output.
77+
/// # Input
78+
/// list_devices_param: [`ListDevicesParam`]. We will under the hood convert the [`ListDevicesParam`]
79+
/// to the low level arguments to `lspci`.
80+
/// According to https://man7.org/linux/man-pages/man8/lspci.8.html#MACHINE_READABLE_OUTPUT,
81+
/// > If you intend to process the output of lspci automatically,
82+
/// > please use one of the machine-readable output formats (-m, -vm, -vmm)
83+
/// > described in this section. All other formats are likely to change between versions of lspci.
84+
///
85+
/// * We will use `-m` to get the machine-readable format which ensures compatibility.
86+
/// * We will use `-n` to get the numeric output for the vendor code and device codes.
87+
/// * We will optionally insert `-d <selection_expression>` to filter the output.
88+
///
89+
/// # Output
90+
/// Output is [`Result<Vec<ListDevicesOutput>>`], the [`ListDevicesOutput`] is based on
91+
/// the output format that is decorated by "-n -m".
92+
///
93+
/// # Example
94+
/// ```rust,ignore
95+
/// use pciclient::{ListDevicesParam, list_devices};
96+
///
97+
/// let list_devices_param = ListDevicesParam::builder().vendor("1d0f").build();
98+
/// match list_devices(list_devices_param) {
99+
/// Ok(list_devices_output) => {
100+
/// for device_info in list_devices_output {
101+
/// println!("vendor: {}, class: {}, device: {}", device_info.vendor(), device_info.class(), device_info.device());
102+
/// }
103+
/// },
104+
/// Err(_) => {
105+
/// println!("Failed to list devices");
106+
/// }
107+
/// }
108+
/// ```
109+
pub fn list_devices(list_devices_param: ListDevicesParam) -> Result<Vec<ListDevicesOutput>> {
110+
call_list_devices(PciClient {}, list_devices_param)
111+
}
112+
113+
/// Call `lspci` and check if there is any EFA device attached.
114+
pub fn is_efa_attached() -> Result<bool> {
115+
check_efa_attachment(PciClient {})
116+
}
117+
118+
mod error {
119+
use snafu::Snafu;
120+
121+
#[derive(Debug, Snafu)]
122+
#[snafu(visibility(pub(super)))]
123+
124+
pub enum PciClientError {
125+
#[snafu(display("Deserialization error: {}", source))]
126+
Serde { source: serde_json::Error },
127+
128+
#[snafu(display("Command lspci failed: {}", source))]
129+
CommandFailure { source: std::io::Error },
130+
131+
#[snafu(display("Exection of lspci failed: {}", reason))]
132+
ExecutionFailure { reason: String },
133+
134+
#[snafu(display("Failed to parse the lspci output: {}, reason: {}", output, reason))]
135+
ParseListDevicesOutputFailure { output: String, reason: String },
136+
137+
#[snafu(display(
138+
"Failed to parse the lspci output, missing required field: {}",
139+
required_field
140+
))]
141+
MissingRequiredField { required_field: String },
142+
}
143+
}
144+
145+
pub use error::PciClientError;
146+
pub type Result<T> = std::result::Result<T, PciClientError>;

0 commit comments

Comments
 (0)