diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..5608611 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[target.x86_64-unknown-linux-gnu] +runner = 'sudo' diff --git a/Cargo.toml b/Cargo.toml index 25da188..d1d2503 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,5 +37,9 @@ tokio = { version = "1.44.0", features = ["macros", "rt", "rt-multi-thread"] } env_logger = "0.9.3" [[example]] -name = "dump_pause" +name = "get_pause_conf" +required-features = ["tokio_socket"] + +[[example]] +name = "apply_pause_conf" required-features = ["tokio_socket"] diff --git a/README.md b/README.md index 3a7627e..14dabbe 100644 --- a/README.md +++ b/README.md @@ -1 +1,49 @@ -# Rust crate for linux ethtool +# Rust crate for linux ethtool netlink management + +This crate provides methods to query and configure the Linux network +interface settings via the ethtool generic netlink protocol. + +The examples are built with the crate and need root (`CAP_NET_ADMIN`) to run: + +```bash +cargo build --examples +sudo ./target/debug/examples/get_pause_conf +``` + +## Query + +- [get or dump pause configuration](examples/get_pause_conf.rs) +- [query features](examples/query_features.rs) +- [query link modes](examples/query_link_mode.rs) +- [query rings](examples/query_rings.rs) +- [query coalesce](examples/query_coalesce.rs) +- [query timestamping information](examples/query_tsinfo.rs) +- [query FEC](examples/query_fec.rs) +- [query channels](examples/query_channels.rs) +- [query module EEPROM](examples/query_eeprom.rs) + +## Set + +- [apply pause configuration](examples/apply_pause_conf.rs) +- [set channel count](examples/set_channel_count.rs) + +## Notes + +- `EthtoolHandle::get_pause()` takes a mandatory interface name and returns + a single `EthtoolPauseParsed`, `EthtoolHandle::dump_pause()` takes no + interface name and returns a `Vec`. +- `EthtoolHandle::apply_pause()` applies an `EthtoolPauseParsed`, usually + one returned by `get_pause()` or `dump_pause()` after modifying it. The + sub-handle request builders (`handle.pause().get(...)`, + `handle.pause().set(...)`) stay available as well. +- `EthtoolPauseParsed` is `#[non_exhaustive]`, so it can not be built with + a struct literal outside of this crate. Use `Default::default()` and + assign the properties instead. +- A parsed configuration holds only the properties the kernel reported, a + field holding `None` is not sent in a set request and the kernel keeps + its current value. +- The statistics properties of `EthtoolPauseParsed` are read-only, the + kernel rejects them in a set request, therefore they are not sent. +- `apply_pause_conf` shows how a queried configuration is modified and + applied back, `set_channel_count` uses the builder of the channel set + request. diff --git a/TODO.md b/TODO.md index 0575d90..da571c1 100644 --- a/TODO.md +++ b/TODO.md @@ -26,6 +26,8 @@ virtio-net, veth and netdevsim do not report tx types/rx filters. - Non-empty `EthtoolLinkModeAttr::Ours`/`Peer` bitsets are not covered, none of the available virtual NICs reports supported link modes. +- `EthtoolPauseStatAttr::TxPauseStormEvents` parse path is not covered, + netdevsim does not report pause storm events. ## API review @@ -44,8 +46,6 @@ `ETHTOOL_A_RINGS_TX_PUSH_BUF_LEN_MAX` (16), `ETHTOOL_A_RINGS_HDS_THRESH` (17), `ETHTOOL_A_RINGS_HDS_THRESH_MAX` (18). -- `pause`: `ETHTOOL_A_PAUSE_STATS_SRC` (6) and - `ETHTOOL_A_PAUSE_STAT_TX_PAUSE_STORM_EVENTS` (4). - `tsinfo`: `ETHTOOL_A_TSINFO_HWTSTAMP_PROVIDER` (7), `ETHTOOL_A_TSINFO_HWTSTAMP_SOURCE` (8), `ETHTOOL_A_TSINFO_HWTSTAMP_PHYINDEX` (9). @@ -53,6 +53,24 @@ `OFFSET`/`LENGTH`/`PAGE`/`BANK`/`I2C_ADDRESS` fall through to `DefaultNla` (they are only used in requests today). +## apply support + +`pause` is the pilot for the parsed/apply design: + +- A get request executes into parsed types (`EthtoolPauseParsed`): the flat + `EthtoolHandle::get_pause()` takes a mandatory interface name and returns + one parsed configuration, `EthtoolHandle::dump_pause()` returns all of + them. `EthtoolHandle::apply_pause()` applies a parsed type back with a + set request. The old sub-handle request builders stay available. +- `build()` of the parsed type emits only fields holding `Some` (the + kernel keeps the value of the others) and validates the input, failing + with `EthtoolError::InvalidInput`. +- Roll out to the commands with kernel `*_SET` support not covered yet: + `link_mode` (`ADVERTISING`), `feature` (`WANTED`), `ring`, `coalesce`, + `fec` (`MODES`), `tsinfo`, EEE. +- `EthtoolError` could hold `{kind, msg, netlink_msg}` like + `WireguardError` instead of the current enum. + ## missing support - `EthtoolMessage` parses a subset of ethtool commands only: `STATS_GET`, diff --git a/examples/apply_pause_conf.rs b/examples/apply_pause_conf.rs new file mode 100644 index 0000000..27951f5 --- /dev/null +++ b/examples/apply_pause_conf.rs @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT + +use std::env; + +// Once we find a way to load netsimdev kernel module in CI, we can convert this +// to a test +fn main() { + let args: Vec = env::args().collect(); + if args.len() != 2 { + usage(); + return; + } + let link_name = &args[1]; + let rt = tokio::runtime::Builder::new_current_thread() + .enable_io() + .build() + .unwrap(); + rt.block_on(apply_pause_conf(link_name)); +} + +async fn apply_pause_conf(iface_name: &str) { + let (connection, mut handle, _) = ethtool::new_connection().unwrap(); + tokio::spawn(connection); + + // Fetch the current configuration, change what is wanted and apply it + // back. Fields left as None are not touched by the kernel. + let mut config = handle.get_pause(iface_name).await.unwrap(); + config.rx = Some(true); + config.tx = Some(true); + + let result = handle.apply_pause(config).await; + + if let Err(error) = result { + panic!("{:?}", error); + } +} + +fn usage() { + eprintln!( + "usage: + cargo run --example apply_pause_conf -- + +Note that you need to run this program as root. Instead of running cargo as root, +build the example normally: + + cd ethtool ; cargo build --example apply_pause_conf + +Then find the binary in the target directory: + + cd target/debug/examples ; sudo ./apply_pause_conf " + ); +} diff --git a/examples/dump_pause.rs b/examples/get_pause_conf.rs similarity index 50% rename from examples/dump_pause.rs rename to examples/get_pause_conf.rs index 7834e61..b6b2834 100644 --- a/examples/dump_pause.rs +++ b/examples/get_pause_conf.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -use futures_util::stream::StreamExt; +use std::env; // Once we find a way to load netsimdev kernel module in CI, we can convert this // to a test @@ -10,20 +10,22 @@ fn main() { .enable_io() .build() .unwrap(); - rt.block_on(get_pause(None)); + rt.block_on(run()); } -async fn get_pause(iface_name: Option<&str>) { +async fn run() { let (connection, mut handle, _) = ethtool::new_connection().unwrap(); tokio::spawn(connection); - let mut pause_handle = - handle.pause().get(iface_name).execute().await.unwrap(); - - let mut msgs = Vec::new(); - while let Some(Ok(msg)) = pause_handle.next().await { - msgs.push(msg); + match env::args().nth(1) { + Some(iface_name) => { + let config = handle.get_pause(&iface_name).await.unwrap(); + println!("{config:?}"); + } + None => { + for config in handle.dump_pause().await.unwrap() { + println!("{config:?}"); + } + } } - assert!(!msgs.is_empty()); - println!("{msgs:?}"); } diff --git a/examples/dump_channels.rs b/examples/query_channels.rs similarity index 100% rename from examples/dump_channels.rs rename to examples/query_channels.rs diff --git a/examples/dump_coalesce.rs b/examples/query_coalesce.rs similarity index 100% rename from examples/dump_coalesce.rs rename to examples/query_coalesce.rs diff --git a/examples/dump_eeprom_page.rs b/examples/query_eeprom.rs similarity index 100% rename from examples/dump_eeprom_page.rs rename to examples/query_eeprom.rs diff --git a/examples/dump_features.rs b/examples/query_features.rs similarity index 100% rename from examples/dump_features.rs rename to examples/query_features.rs diff --git a/examples/dump_fec.rs b/examples/query_fec.rs similarity index 100% rename from examples/dump_fec.rs rename to examples/query_fec.rs diff --git a/examples/dump_link_mode.rs b/examples/query_link_mode.rs similarity index 100% rename from examples/dump_link_mode.rs rename to examples/query_link_mode.rs diff --git a/examples/dump_rings.rs b/examples/query_rings.rs similarity index 100% rename from examples/dump_rings.rs rename to examples/query_rings.rs diff --git a/examples/dump_tsinfo.rs b/examples/query_tsinfo.rs similarity index 100% rename from examples/dump_tsinfo.rs rename to examples/query_tsinfo.rs diff --git a/examples/set_rx_count.rs b/examples/set_channel_count.rs similarity index 76% rename from examples/set_rx_count.rs rename to examples/set_channel_count.rs index b7ec1d6..9859a5a 100644 --- a/examples/set_rx_count.rs +++ b/examples/set_channel_count.rs @@ -15,10 +15,10 @@ fn main() { .enable_io() .build() .unwrap(); - rt.block_on(set_rx_count(link_name)); + rt.block_on(set_channel_count(link_name)); } -async fn set_rx_count(iface_name: &str) { +async fn set_channel_count(iface_name: &str) { let (connection, mut handle, _) = ethtool::new_connection().unwrap(); tokio::spawn(connection); @@ -32,15 +32,15 @@ async fn set_rx_count(iface_name: &str) { fn usage() { eprintln!( "usage: - cargo run --example set_rx_count -- + cargo run --example set_channel_count -- Note that you need to run this program as root. Instead of running cargo as root, build the example normally: - cd ethtool ; cargo build --example set_rx_count + cd ethtool ; cargo build --example set_channel_count Then find the binary in the target directory: - cd target/debug/example ; sudo ./set_rx_count " + cd target/debug/examples ; sudo ./set_channel_count " ); } diff --git a/src/error.rs b/src/error.rs index c0d3193..48e650f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -18,6 +18,9 @@ pub enum EthtoolError { #[error("A netlink request failed")] RequestFailed(String), + #[error("Invalid input: {0}")] + InvalidInput(String), + #[error("A bug in this crate")] Bug(String), } diff --git a/src/header.rs b/src/header.rs index 93944a6..9d1e41f 100644 --- a/src/header.rs +++ b/src/header.rs @@ -7,7 +7,7 @@ use netlink_packet_core::{ Nla, NlaBuffer, Parseable, }; -const ALTIFNAMSIZ: usize = 128; +pub(crate) const ALTIFNAMSIZ: usize = 128; const ETHTOOL_A_HEADER_DEV_INDEX: u16 = 1; const ETHTOOL_A_HEADER_DEV_NAME: u16 = 2; const ETHTOOL_A_HEADER_FLAGS: u16 = 3; diff --git a/src/lib.rs b/src/lib.rs index 8cf2fd2..e01aed0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,7 +54,7 @@ pub use link_mode::{ pub use message::{EthtoolAttr, EthtoolCmd, EthtoolMessage}; pub use pause::{ EthtoolPauseAttr, EthtoolPauseGetRequest, EthtoolPauseHandle, - EthtoolPauseStatAttr, + EthtoolPauseParsed, EthtoolPauseSetRequest, EthtoolPauseStatAttr, }; pub use ring::{EthtoolRingAttr, EthtoolRingGetRequest, EthtoolRingHandle}; pub use tsinfo::{ @@ -62,3 +62,8 @@ pub use tsinfo::{ }; pub(crate) use handle::ethtool_execute; + +// Compile the examples of the README as doc tests. +#[cfg(doctest)] +#[doc = include_str!("../README.md")] +struct ReadmeDoctests; diff --git a/src/message.rs b/src/message.rs index 6274cc7..98db611 100644 --- a/src/message.rs +++ b/src/message.rs @@ -17,6 +17,9 @@ use crate::{ }; const ETHTOOL_MSG_PAUSE_GET: u8 = 21; +/// The kernel uses the same numeric value for the `GET_REPLY` (kernel +/// namespace) and the `SET` (user namespace) command. +const ETHTOOL_MSG_PAUSE_SET: u8 = 22; const ETHTOOL_MSG_PAUSE_GET_REPLY: u8 = 22; const ETHTOOL_MSG_FEATURES_GET: u8 = 11; const ETHTOOL_MSG_FEATURES_GET_REPLY: u8 = 11; @@ -40,6 +43,7 @@ const ETHTOOL_MSG_MODULE_EEPROM_GET_REPLY: u8 = 32; pub enum EthtoolCmd { PauseGet, PauseGetReply, + PauseSet, FeatureGet, FeatureGetReply, LinkModeGet, @@ -64,6 +68,7 @@ impl From for u8 { match cmd { EthtoolCmd::PauseGet => ETHTOOL_MSG_PAUSE_GET, EthtoolCmd::PauseGetReply => ETHTOOL_MSG_PAUSE_GET_REPLY, + EthtoolCmd::PauseSet => ETHTOOL_MSG_PAUSE_SET, EthtoolCmd::FeatureGet => ETHTOOL_MSG_FEATURES_GET, EthtoolCmd::FeatureGetReply => ETHTOOL_MSG_FEATURES_GET_REPLY, EthtoolCmd::LinkModeGet => ETHTOOL_MSG_LINKMODES_GET, diff --git a/src/pause/attr.rs b/src/pause/attr.rs index 516505c..8a21377 100644 --- a/src/pause/attr.rs +++ b/src/pause/attr.rs @@ -1,8 +1,9 @@ // SPDX-License-Identifier: MIT use netlink_packet_core::{ - emit_u64, parse_u64, parse_u8, DecodeError, DefaultNla, Emitable, - ErrorContext, Nla, NlaBuffer, NlasIterator, Parseable, NLA_F_NESTED, + emit_u32, emit_u64, parse_u32, parse_u64, parse_u8, DecodeError, + DefaultNla, Emitable, ErrorContext, Nla, NlaBuffer, NlasIterator, + Parseable, NLA_F_NESTED, }; use crate::{EthtoolAttr, EthtoolHeader}; @@ -12,21 +13,24 @@ const ETHTOOL_A_PAUSE_AUTONEG: u16 = 2; const ETHTOOL_A_PAUSE_RX: u16 = 3; const ETHTOOL_A_PAUSE_TX: u16 = 4; const ETHTOOL_A_PAUSE_STATS: u16 = 5; +const ETHTOOL_A_PAUSE_STATS_SRC: u16 = 6; const ETHTOOL_A_PAUSE_STAT_TX_FRAMES: u16 = 2; const ETHTOOL_A_PAUSE_STAT_RX_FRAMES: u16 = 3; +const ETHTOOL_A_PAUSE_STAT_TX_PAUSE_STORM_EVENTS: u16 = 4; #[derive(Debug, PartialEq, Eq, Clone)] pub enum EthtoolPauseStatAttr { Rx(u64), Tx(u64), + TxPauseStormEvents(u64), Other(DefaultNla), } impl Nla for EthtoolPauseStatAttr { fn value_len(&self) -> usize { match self { - Self::Rx(_) | Self::Tx(_) => 8, + Self::Rx(_) | Self::Tx(_) | Self::TxPauseStormEvents(_) => 8, Self::Other(attr) => attr.value_len(), } } @@ -35,13 +39,18 @@ impl Nla for EthtoolPauseStatAttr { match self { Self::Rx(_) => ETHTOOL_A_PAUSE_STAT_RX_FRAMES, Self::Tx(_) => ETHTOOL_A_PAUSE_STAT_TX_FRAMES, + Self::TxPauseStormEvents(_) => { + ETHTOOL_A_PAUSE_STAT_TX_PAUSE_STORM_EVENTS + } Self::Other(attr) => attr.kind(), } } fn emit_value(&self, buffer: &mut [u8]) { match self { - Self::Rx(value) | Self::Tx(value) => { + Self::Rx(value) + | Self::Tx(value) + | Self::TxPauseStormEvents(value) => { emit_u64(buffer, *value).unwrap() } Self::Other(ref attr) => attr.emit_value(buffer), @@ -63,6 +72,11 @@ impl<'a, T: AsRef<[u8]> + ?Sized> Parseable> parse_u64(payload) .context("invalid ETHTOOL_A_PAUSE_STAT_RX_FRAMES value")?, ), + ETHTOOL_A_PAUSE_STAT_TX_PAUSE_STORM_EVENTS => { + Self::TxPauseStormEvents(parse_u64(payload).context( + "invalid ETHTOOL_A_PAUSE_STAT_TX_PAUSE_STORM_EVENTS value", + )?) + } _ => Self::Other( DefaultNla::parse(buf).context("invalid NLA (unknown kind)")?, ), @@ -77,6 +91,7 @@ pub enum EthtoolPauseAttr { Rx(bool), Tx(bool), Stats(Vec), + StatsSrc(u32), Other(DefaultNla), } @@ -86,6 +101,7 @@ impl Nla for EthtoolPauseAttr { Self::Header(hdrs) => hdrs.as_slice().buffer_len(), Self::AutoNeg(_) | Self::Rx(_) | Self::Tx(_) => 1, Self::Stats(ref nlas) => nlas.as_slice().buffer_len(), + Self::StatsSrc(_) => 4, Self::Other(attr) => attr.value_len(), } } @@ -97,6 +113,7 @@ impl Nla for EthtoolPauseAttr { Self::Rx(_) => ETHTOOL_A_PAUSE_RX, Self::Tx(_) => ETHTOOL_A_PAUSE_TX, Self::Stats(_) => ETHTOOL_A_PAUSE_STATS | NLA_F_NESTED, + Self::StatsSrc(_) => ETHTOOL_A_PAUSE_STATS_SRC, Self::Other(attr) => attr.kind(), } } @@ -108,6 +125,7 @@ impl Nla for EthtoolPauseAttr { buffer[0] = *value as u8 } Self::Stats(ref nlas) => nlas.as_slice().emit(buffer), + Self::StatsSrc(value) => emit_u32(buffer, *value).unwrap(), Self::Other(ref attr) => attr.emit_value(buffer), } } @@ -156,6 +174,10 @@ impl<'a, T: AsRef<[u8]> + ?Sized> Parseable> } Self::Stats(nlas) } + ETHTOOL_A_PAUSE_STATS_SRC => Self::StatsSrc( + parse_u32(payload) + .context("invalid ETHTOOL_A_PAUSE_STATS_SRC value")?, + ), _ => Self::Other( DefaultNla::parse(buf).context("invalid NLA (unknown kind)")?, ), diff --git a/src/pause/get.rs b/src/pause/get.rs index 66e9b47..44dfc1a 100644 --- a/src/pause/get.rs +++ b/src/pause/get.rs @@ -1,9 +1,11 @@ // SPDX-License-Identifier: MIT -use futures_util::Stream; -use netlink_packet_generic::GenlMessage; +use futures_util::{Stream, StreamExt}; -use crate::{ethtool_execute, EthtoolError, EthtoolHandle, EthtoolMessage}; +use crate::{ + ethtool_execute, EthtoolError, EthtoolHandle, EthtoolMessage, + EthtoolPauseParsed, +}; pub struct EthtoolPauseGetRequest { handle: EthtoolHandle, @@ -18,10 +20,12 @@ impl EthtoolPauseGetRequest { } } + /// Retrieve the pause configuration of one interface, or of all + /// interfaces when no interface name was given. pub async fn execute( self, ) -> Result< - impl Stream, EthtoolError>>, + impl Stream>, EthtoolError, > { let EthtoolPauseGetRequest { @@ -30,6 +34,12 @@ impl EthtoolPauseGetRequest { } = self; let ethtool_msg = EthtoolMessage::new_pause_get(iface_name.as_deref()); - ethtool_execute(&mut handle, iface_name.is_none(), ethtool_msg).await + let response = + ethtool_execute(&mut handle, iface_name.is_none(), ethtool_msg) + .await?; + + Ok(response.map(|msg| { + msg.map(|genl_msg| EthtoolPauseParsed::from(genl_msg.payload)) + })) } } diff --git a/src/pause/handle.rs b/src/pause/handle.rs index 6db72f7..ec07365 100644 --- a/src/pause/handle.rs +++ b/src/pause/handle.rs @@ -1,6 +1,76 @@ // SPDX-License-Identifier: MIT -use crate::{EthtoolHandle, EthtoolPauseGetRequest}; +use futures_util::StreamExt; + +use crate::{ + EthtoolError, EthtoolHandle, EthtoolPauseGetRequest, EthtoolPauseParsed, + EthtoolPauseSetRequest, +}; + +impl EthtoolHandle { + /// Retrieve the pause configuration of `iface_name` (equivalent to + /// `ethtool -a eth1`). + /// + /// The `iface_name` is mandatory, use [EthtoolHandle::dump_pause] to + /// query all interfaces. + pub async fn get_pause( + &mut self, + iface_name: &str, + ) -> Result { + let mut stream = + EthtoolPauseGetRequest::new(self.clone(), Some(iface_name)) + .execute() + .await?; + + let mut configs = Vec::new(); + while let Some(config) = stream.next().await { + configs.push(config?); + } + + match configs.len() { + 1 => Ok(configs.remove(0)), + 0 => Err(EthtoolError::Bug(format!( + "Got no pause reply from kernel for interface `{iface_name}`" + ))), + count => Err(EthtoolError::Bug(format!( + "Got {count} pause replies from kernel for interface \ + `{iface_name}`" + ))), + } + } + + /// Retrieve the pause configuration of all interfaces (equivalent to + /// `ethtool -a`). + pub async fn dump_pause( + &mut self, + ) -> Result, EthtoolError> { + let mut stream = EthtoolPauseGetRequest::new(self.clone(), None) + .execute() + .await?; + + let mut configs = Vec::new(); + while let Some(config) = stream.next().await { + configs.push(config?); + } + + Ok(configs) + } + + /// Apply a pause configuration (equivalent to `ethtool -A eth1 ...`). + /// + /// Only fields holding `Some` in `config` are sent, the kernel keeps the + /// current value of the other pause properties. The interface is taken + /// from [EthtoolPauseParsed::iface_name] or, when it is not set, from + /// [EthtoolPauseParsed::iface_index]. + pub async fn apply_pause( + &mut self, + config: EthtoolPauseParsed, + ) -> Result<(), EthtoolError> { + EthtoolPauseSetRequest::new(self.clone(), config) + .execute() + .await + } +} pub struct EthtoolPauseHandle(EthtoolHandle); @@ -14,4 +84,15 @@ impl EthtoolPauseHandle { pub fn get(&mut self, iface_name: Option<&str>) -> EthtoolPauseGetRequest { EthtoolPauseGetRequest::new(self.0.clone(), iface_name) } + + /// Apply a pause configuration (equivalent to `ethtool -A eth1 ...`). + /// + /// Only fields holding `Some` in `config` are sent, the kernel keeps + /// the current value of the other pause properties. + pub fn set( + &mut self, + config: EthtoolPauseParsed, + ) -> EthtoolPauseSetRequest { + EthtoolPauseSetRequest::new(self.0.clone(), config) + } } diff --git a/src/pause/mod.rs b/src/pause/mod.rs index 486cd95..c34232a 100644 --- a/src/pause/mod.rs +++ b/src/pause/mod.rs @@ -3,8 +3,12 @@ mod attr; mod get; mod handle; +mod parsed; +mod set; pub(crate) use attr::parse_pause_nlas; pub use attr::{EthtoolPauseAttr, EthtoolPauseStatAttr}; pub use get::EthtoolPauseGetRequest; pub use handle::EthtoolPauseHandle; +pub use parsed::EthtoolPauseParsed; +pub use set::EthtoolPauseSetRequest; diff --git a/src/pause/parsed.rs b/src/pause/parsed.rs new file mode 100644 index 0000000..24aa940 --- /dev/null +++ b/src/pause/parsed.rs @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: MIT + +use super::attr::{EthtoolPauseAttr, EthtoolPauseStatAttr}; +use crate::{ + header::ALTIFNAMSIZ, EthtoolAttr, EthtoolCmd, EthtoolError, EthtoolHeader, + EthtoolMessage, +}; + +/// Pause configuration of one interface, as `ethtool -a` reports it. +/// +/// A field holding `None` is not emitted by [EthtoolPauseParsed::build], so +/// a configuration parsed from a reply can be modified and applied without +/// changing what was not touched. +/// +/// The statistics fields ([EthtoolPauseParsed::rx_frames], +/// [EthtoolPauseParsed::tx_frames], +/// [EthtoolPauseParsed::tx_pause_storm_events] and +/// [EthtoolPauseParsed::stats_src]) are read-only, the kernel rejects them +/// in a set request, therefore [EthtoolPauseParsed::build] ignores them. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[non_exhaustive] +pub struct EthtoolPauseParsed { + pub iface_name: Option, + pub iface_index: Option, + pub autoneg: Option, + pub rx: Option, + pub tx: Option, + /// Number of received pause frames. + pub rx_frames: Option, + /// Number of transmitted pause frames. + pub tx_frames: Option, + /// Read-only, ignored by [EthtoolPauseParsed::build]. + pub tx_pause_storm_events: Option, + /// Source of the statistics, `ETHTOOL_MAC_STATS_SRC_*`. + pub stats_src: Option, +} + +impl From for EthtoolPauseParsed { + fn from(msg: EthtoolMessage) -> Self { + let mut parsed = Self::default(); + for nla in msg.nlas { + match nla { + EthtoolAttr::Pause(pause_attr) => match pause_attr { + EthtoolPauseAttr::Header(headers) => { + for header in headers { + match header { + EthtoolHeader::DevIndex(index) => { + parsed.iface_index = Some(index) + } + EthtoolHeader::DevName(name) => { + parsed.iface_name = Some(name) + } + _ => { + log::debug!( + "Unsupported pause header {header:?}" + ); + } + } + } + } + EthtoolPauseAttr::AutoNeg(value) => { + parsed.autoneg = Some(value) + } + EthtoolPauseAttr::Rx(value) => parsed.rx = Some(value), + EthtoolPauseAttr::Tx(value) => parsed.tx = Some(value), + EthtoolPauseAttr::Stats(stats) => { + for stat in stats { + match stat { + EthtoolPauseStatAttr::Rx(value) => { + parsed.rx_frames = Some(value) + } + EthtoolPauseStatAttr::Tx(value) => { + parsed.tx_frames = Some(value) + } + EthtoolPauseStatAttr::TxPauseStormEvents( + value, + ) => parsed.tx_pause_storm_events = Some(value), + EthtoolPauseStatAttr::Other(nla) => { + log::debug!( + "Unsupported pause stat attribute \ + {nla:?}" + ); + } + } + } + } + EthtoolPauseAttr::StatsSrc(value) => { + parsed.stats_src = Some(value) + } + EthtoolPauseAttr::Other(nla) => { + log::debug!("Unsupported EthtoolPauseAttr {nla:?}"); + } + }, + nla => log::debug!("Unsupported EthtoolAttr {nla:?}"), + } + } + parsed + } +} + +impl EthtoolPauseParsed { + /// Build the `PAUSE_SET` [EthtoolMessage]. + /// + /// Only fields holding `Some` are emitted, the kernel keeps the current + /// value of the other pause properties. The read-only statistics fields + /// and the statistics source are ignored. + /// + /// This fails with [EthtoolError::InvalidInput] when neither + /// [EthtoolPauseParsed::iface_name] nor + /// [EthtoolPauseParsed::iface_index] is set or when the interface name + /// is invalid. + pub fn build(&self) -> Result { + let mut header = Vec::new(); + // The kernel resolves the interface from either the name or the + // index, sending both makes no sense. The name wins, like + // `ethtool -A` does. + if let Some(iface_name) = self.iface_name.as_ref() { + if iface_name.len() >= ALTIFNAMSIZ { + return Err(EthtoolError::InvalidInput(format!( + "`iface_name` can not be longer than {} bytes, but \ + `{iface_name}` holds {} bytes", + ALTIFNAMSIZ - 1, + iface_name.len() + ))); + } + if iface_name.contains('\0') { + return Err(EthtoolError::InvalidInput( + "`iface_name` can not hold a NUL byte, the kernel would \ + use the name in front of it" + .to_string(), + )); + } + header.push(EthtoolHeader::DevName(iface_name.clone())); + } else if let Some(iface_index) = self.iface_index { + header.push(EthtoolHeader::DevIndex(iface_index)); + } else { + return Err(EthtoolError::InvalidInput( + "Neither `iface_name` nor `iface_index` is defined".to_string(), + )); + } + + let mut nlas = + vec![EthtoolAttr::Pause(EthtoolPauseAttr::Header(header))]; + if let Some(autoneg) = self.autoneg { + nlas.push(EthtoolAttr::Pause(EthtoolPauseAttr::AutoNeg(autoneg))); + } + if let Some(rx) = self.rx { + nlas.push(EthtoolAttr::Pause(EthtoolPauseAttr::Rx(rx))); + } + if let Some(tx) = self.tx { + nlas.push(EthtoolAttr::Pause(EthtoolPauseAttr::Tx(tx))); + } + + Ok(EthtoolMessage { + cmd: EthtoolCmd::PauseSet, + nlas, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn build_uses_iface_name_when_both_are_defined() { + // A parsed reply holds both, the kernel only needs one. + let parsed = EthtoolPauseParsed { + iface_name: Some("eth1".to_string()), + iface_index: Some(3), + ..Default::default() + }; + + let msg = parsed.build().unwrap(); + + assert_eq!( + msg.nlas, + vec![EthtoolAttr::Pause(EthtoolPauseAttr::Header(vec![ + EthtoolHeader::DevName("eth1".to_string()) + ]))] + ); + } + + #[test] + fn build_accepts_iface_index_only() { + let parsed = EthtoolPauseParsed { + iface_index: Some(3), + ..Default::default() + }; + + let msg = parsed.build().unwrap(); + + assert_eq!( + msg.nlas, + vec![EthtoolAttr::Pause(EthtoolPauseAttr::Header(vec![ + EthtoolHeader::DevIndex(3) + ]))] + ); + } + + #[test] + fn build_requires_an_interface() { + let err = EthtoolPauseParsed::default().build().unwrap_err(); + + assert!(matches!(err, EthtoolError::InvalidInput(_))); + } + + #[test] + fn iface_name_is_limited_to_altifnamsiz() { + let longest = EthtoolPauseParsed { + iface_name: Some("a".repeat(ALTIFNAMSIZ - 1)), + ..Default::default() + }; + assert!(longest.build().is_ok()); + + let too_long = EthtoolPauseParsed { + iface_name: Some("a".repeat(ALTIFNAMSIZ)), + ..Default::default() + }; + assert!(matches!( + too_long.build().unwrap_err(), + EthtoolError::InvalidInput(_) + )); + + // The kernel would use the name in front of an embedded NUL. + let with_nul = EthtoolPauseParsed { + iface_name: Some("eth0\0eth1".to_string()), + ..Default::default() + }; + assert!(matches!( + with_nul.build().unwrap_err(), + EthtoolError::InvalidInput(_) + )); + } + + #[test] + fn build_ignores_read_only_fields() { + let parsed = EthtoolPauseParsed { + iface_name: Some("eth1".to_string()), + autoneg: Some(true), + rx_frames: Some(1), + tx_frames: Some(2), + tx_pause_storm_events: Some(3), + stats_src: Some(1), + ..Default::default() + }; + + let msg = parsed.build().unwrap(); + + assert_eq!( + msg.nlas, + vec![ + EthtoolAttr::Pause(EthtoolPauseAttr::Header(vec![ + EthtoolHeader::DevName("eth1".to_string()) + ])), + EthtoolAttr::Pause(EthtoolPauseAttr::AutoNeg(true)), + ] + ); + } +} diff --git a/src/pause/set.rs b/src/pause/set.rs new file mode 100644 index 0000000..03422b8 --- /dev/null +++ b/src/pause/set.rs @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT + +use futures_util::StreamExt; +use netlink_packet_core::{NetlinkMessage, NLM_F_ACK, NLM_F_REQUEST}; +use netlink_packet_generic::GenlMessage; + +use crate::{try_ethtool, EthtoolError, EthtoolHandle, EthtoolPauseParsed}; + +/// Apply a [EthtoolPauseParsed] configuration (equivalent to `ethtool -A +/// eth1 ...`). +/// +/// Only fields holding `Some` are sent, the kernel keeps the current value +/// of the other pause properties. +pub struct EthtoolPauseSetRequest { + handle: EthtoolHandle, + config: EthtoolPauseParsed, +} + +impl EthtoolPauseSetRequest { + pub(crate) fn new( + handle: EthtoolHandle, + config: EthtoolPauseParsed, + ) -> Self { + EthtoolPauseSetRequest { handle, config } + } + + pub async fn execute(self) -> Result<(), EthtoolError> { + let EthtoolPauseSetRequest { mut handle, config } = self; + + let mut nl_msg = + NetlinkMessage::from(GenlMessage::from_payload(config.build()?)); + + nl_msg.header.flags = NLM_F_REQUEST | NLM_F_ACK; + + let mut response = handle.request(nl_msg).await?; + + while let Some(message) = response.next().await { + try_ethtool!(message); + } + + Ok(()) + } +} diff --git a/src/tests/pause.rs b/src/tests/pause.rs index f1ec9dc..2817ebc 100644 --- a/src/tests/pause.rs +++ b/src/tests/pause.rs @@ -12,9 +12,10 @@ use super::parse_and_round_trip; use crate::{ EthtoolAttr, EthtoolCmd, EthtoolHeader, EthtoolMessage, EthtoolPauseAttr, - EthtoolPauseStatAttr, + EthtoolPauseParsed, EthtoolPauseStatAttr, }; -use netlink_packet_core::DefaultNla; +use netlink_packet_core::Emitable; +use netlink_packet_generic::GenlHeader; #[test] fn test_parsing_pause_netdevsim() { @@ -44,6 +45,20 @@ fn test_parsing_pause_netdevsim() { ], } ); + + let parsed = EthtoolPauseParsed::from(msg); + + assert_eq!( + parsed, + EthtoolPauseParsed { + iface_name: Some("sim0".to_string()), + iface_index: Some(163), + autoneg: Some(false), + rx: Some(true), + tx: Some(true), + ..Default::default() + } + ); } #[test] @@ -74,11 +89,7 @@ fn test_parsing_pause_stats_netdevsim() { EthtoolAttr::Pause(EthtoolPauseAttr::AutoNeg(false)), EthtoolAttr::Pause(EthtoolPauseAttr::Rx(true)), EthtoolAttr::Pause(EthtoolPauseAttr::Tx(true)), - // ETHTOOL_A_PAUSE_STATS_SRC (6) - EthtoolAttr::Pause(EthtoolPauseAttr::Other(DefaultNla::new( - 6, - vec![0, 0, 0, 0] - ))), + EthtoolAttr::Pause(EthtoolPauseAttr::StatsSrc(0)), EthtoolAttr::Pause(EthtoolPauseAttr::Stats(vec![ EthtoolPauseStatAttr::Tx(2), EthtoolPauseStatAttr::Rx(1), @@ -86,4 +97,57 @@ fn test_parsing_pause_stats_netdevsim() { ], } ); + + let parsed = EthtoolPauseParsed::from(msg); + + assert_eq!( + parsed, + EthtoolPauseParsed { + iface_name: Some("sim0".to_string()), + iface_index: Some(163), + autoneg: Some(false), + rx: Some(true), + tx: Some(true), + rx_frames: Some(1), + tx_frames: Some(2), + stats_src: Some(0), + ..Default::default() + } + ); +} + +#[test] +fn test_building_pause_set_netdevsim() { + // Kernel reply to `ethtool -a sim0` with the configuration below + // applied, captured with an nlmon device on Linux 7.3. + let raw_reply: Vec = vec![ + 0x16, 0x01, 0x00, 0x00, 0x18, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0xa3, 0x00, 0x00, 0x00, 0x09, 0x00, 0x02, 0x00, 0x73, 0x69, 0x6d, 0x30, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x01, 0x00, 0x00, 0x00, + ]; + + let parsed = EthtoolPauseParsed::from(parse_and_round_trip(&raw_reply)); + + // Request of `ethtool -A sim0 autoneg off rx on tx on`, captured with an + // nlmon device on Linux 7.3. The kernel resolves the interface from the + // name, the device index of the reply is not part of the request. + let raw_request: Vec = vec![ + 0x16, 0x01, 0x00, 0x00, 0x10, 0x00, 0x01, 0x80, 0x09, 0x00, 0x02, 0x00, + 0x73, 0x69, 0x6d, 0x30, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x02, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, + ]; + + let msg = parsed.build().unwrap(); + let header = GenlHeader { + cmd: EthtoolCmd::PauseSet.into(), + version: 1, + }; + let mut buffer = vec![0; header.buffer_len() + msg.buffer_len()]; + header.emit(&mut buffer); + msg.emit(&mut buffer[header.buffer_len()..]); + + assert_eq!(buffer, raw_request); } diff --git a/tests/channel.rs b/tests/channel.rs new file mode 100644 index 0000000..7dedd43 --- /dev/null +++ b/tests/channel.rs @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT + +//! Query and set tests for the channel subsystem. +//! +//! The loopback interface does not support channels, netdevsim is required. +//! See [common::netdevsim_iface] for how to instantiate one, the tests skip +//! when netdevsim is not available. + +mod common; + +use ethtool::{EthtoolAttr, EthtoolChannelAttr, EthtoolCmd, EthtoolHeader}; +use futures_util::stream::StreamExt; + +#[tokio::test] +async fn query_channels_netdevsim() { + let Some(iface_name) = common::netdevsim_iface() else { + eprintln!( + "skipped query_channels_netdevsim: netdevsim is not available" + ); + return; + }; + + let mut handle = common::connect().await; + let mut stream = handle + .channel() + .get(Some(&iface_name)) + .execute() + .await + .expect("failed to request the channels"); + + let mut msgs = Vec::new(); + while let Some(reply) = stream.next().await { + msgs.push(reply.expect("failed to parse a channel reply")); + } + + assert_eq!(msgs.len(), 1); + let ethtool_msg = &msgs[0].payload; + assert_eq!(ethtool_msg.cmd, EthtoolCmd::ChannelGetReply); + match ðtool_msg.nlas[0] { + EthtoolAttr::Channel(EthtoolChannelAttr::Header(headers)) => { + assert!( + headers.contains(&EthtoolHeader::DevName(iface_name.clone())), + "unexpected channel header {headers:?}" + ); + } + nla => panic!("unexpected first channel attribute {nla:?}"), + } +} + +#[tokio::test] +async fn set_channel_count_netdevsim() { + let Some(iface_name) = common::netdevsim_iface() else { + eprintln!( + "skipped set_channel_count_netdevsim: netdevsim is not available" + ); + return; + }; + + let mut handle = common::connect().await; + let result = handle + .channel() + .set(&iface_name) + .combined_count(1) + .execute() + .await; + + if let Err(err) = result { + if common::is_skippable(&err) { + eprintln!("skipped set_channel_count_netdevsim: {err}"); + return; + } + panic!("failed to set the channel count: {err}"); + } +} diff --git a/tests/coalesce.rs b/tests/coalesce.rs new file mode 100644 index 0000000..836c70c --- /dev/null +++ b/tests/coalesce.rs @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT + +//! Query test for the coalesce subsystem. +//! +//! The loopback interface does not support coalesce, netdevsim is required. +//! See [common::netdevsim_iface] for how to instantiate one, the test skips +//! when netdevsim is not available. + +mod common; + +use ethtool::{EthtoolAttr, EthtoolCmd, EthtoolCoalesceAttr, EthtoolHeader}; +use futures_util::stream::StreamExt; + +#[tokio::test] +async fn query_coalesce_netdevsim() { + let Some(iface_name) = common::netdevsim_iface() else { + eprintln!( + "skipped query_coalesce_netdevsim: netdevsim is not available" + ); + return; + }; + + let mut handle = common::connect().await; + let mut stream = handle + .coalesce() + .get(Some(&iface_name)) + .execute() + .await + .expect("failed to request the coalesce parameters"); + + let mut msgs = Vec::new(); + while let Some(reply) = stream.next().await { + msgs.push(reply.expect("failed to parse a coalesce reply")); + } + + assert_eq!(msgs.len(), 1); + let ethtool_msg = &msgs[0].payload; + assert_eq!(ethtool_msg.cmd, EthtoolCmd::CoalesceGetReply); + match ðtool_msg.nlas[0] { + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::Header(headers)) => { + assert!( + headers.contains(&EthtoolHeader::DevName(iface_name.clone())), + "unexpected coalesce header {headers:?}" + ); + } + nla => panic!("unexpected first coalesce attribute {nla:?}"), + } +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..bf55158 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT + +//! Helpers shared by the integration tests. + +#![allow(dead_code)] + +use ethtool::{EthtoolError, EthtoolHandle}; + +/// `EPERM`, the kernel returns it when the test process misses +/// `CAP_NET_ADMIN`. +const EPERM: i32 = 1; + +/// `EOPNOTSUPP`, the kernel returns it when the interface does not support +/// the command. +const EOPNOTSUPP: i32 = 95; + +/// Whether `err` makes a test skip instead of fail. +/// +/// A test skips when the interface does not support the command +/// (`EOPNOTSUPP`) or when the process is not privileged enough +/// (`EPERM`), so that `cargo test` passes without `CAP_NET_ADMIN`. +pub fn is_skippable(err: &EthtoolError) -> bool { + let EthtoolError::NetlinkError(nl_err) = err else { + return false; + }; + matches!( + nl_err.code.map(|code| code.get()), + Some(code) if code == -EOPNOTSUPP || code == -EPERM + ) +} + +/// Name of the first netdevsim interface, or `None` when netdevsim is not +/// available, in which case the caller skips the test. +/// +/// Instantiate a device to run the netdevsim tests: +/// +/// ```bash +/// sudo modprobe netdevsim +/// echo "1 1" | sudo tee /sys/bus/netdevsim/new_device +/// ``` +pub fn netdevsim_iface() -> Option { + for device in std::fs::read_dir("/sys/bus/netdevsim/devices") + .ok()? + .flatten() + { + if let Ok(mut ifaces) = std::fs::read_dir(device.path().join("net")) { + if let Some(Ok(iface)) = ifaces.next() { + return Some(iface.file_name().to_string_lossy().into_owned()); + } + } + } + None +} + +/// Open a new ethtool netlink connection and spawn its processing loop. +pub async fn connect() -> EthtoolHandle { + let (connection, handle, _) = + ethtool::new_connection().expect("failed to open netlink socket"); + tokio::spawn(connection); + handle +} diff --git a/tests/dump_link_modes.rs b/tests/dump_link_modes.rs deleted file mode 100644 index 5d7260e..0000000 --- a/tests/dump_link_modes.rs +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: MIT - -use futures_util::stream::StreamExt; - -#[test] -// CI container normally have a veth for external communication which support -// link modes of ethtool. -fn test_dump_link_modes() { - let rt = tokio::runtime::Builder::new_current_thread() - .enable_io() - .build() - .unwrap(); - rt.block_on(dump_link_modes()); -} - -async fn dump_link_modes() { - let (connection, mut handle, _) = ethtool::new_connection().unwrap(); - tokio::spawn(connection); - - let mut link_modes_handle = - handle.link_mode().get(None).execute().await.unwrap(); - - let mut msgs = Vec::new(); - while let Some(Ok(msg)) = link_modes_handle.next().await { - msgs.push(msg); - } - assert!(!msgs.is_empty()); - let ethtool_msg = &msgs[0].payload; - println!("ethtool_msg {ethtool_msg:?}"); - - assert!(ethtool_msg.cmd == ethtool::EthtoolCmd::LinkModeGetReply); - assert!(ethtool_msg.nlas.len() > 1); -} diff --git a/tests/eeprom.rs b/tests/eeprom.rs new file mode 100644 index 0000000..eea5254 --- /dev/null +++ b/tests/eeprom.rs @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT + +//! Query test for the module EEPROM subsystem. +//! +//! Module EEPROM requires SFP hardware which none of the virtual NICs +//! provide, therefore the test skips unless the queried interface has one. + +mod common; + +use futures_util::stream::StreamExt; + +#[tokio::test] +async fn query_eeprom_lo() { + let mut handle = common::connect().await; + + let mut stream = handle + .eeprom() + .get(Some("lo"), 0, 1, 0, 0, 0x50) + .execute() + .await + .expect("failed to request the module EEPROM page"); + + let mut msgs = Vec::new(); + while let Some(reply) = stream.next().await { + match reply { + Ok(msg) => msgs.push(msg), + Err(err) if common::is_skippable(&err) => { + eprintln!("skipped query_eeprom_lo: no module EEPROM: {err}"); + return; + } + Err(err) => { + panic!("failed to parse the module EEPROM reply: {err:?}") + } + } + } + + assert_eq!(msgs.len(), 1); +} diff --git a/tests/feature.rs b/tests/feature.rs new file mode 100644 index 0000000..1590d7c --- /dev/null +++ b/tests/feature.rs @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT + +//! Query test for the feature subsystem. + +mod common; + +use ethtool::{EthtoolAttr, EthtoolCmd, EthtoolFeatureAttr, EthtoolHeader}; +use futures_util::stream::StreamExt; + +#[tokio::test] +async fn query_features_lo() { + let mut handle = common::connect().await; + + let mut stream = handle + .feature() + .get(Some("lo")) + .execute() + .await + .expect("failed to request the features of `lo`"); + + let mut msgs = Vec::new(); + while let Some(reply) = stream.next().await { + msgs.push(reply.expect("failed to parse a feature reply")); + } + + assert_eq!(msgs.len(), 1); + let ethtool_msg = &msgs[0].payload; + + assert_eq!(ethtool_msg.cmd, EthtoolCmd::FeatureGetReply); + assert!(ethtool_msg.nlas.len() > 1); + assert_eq!( + ethtool_msg.nlas[0], + EthtoolAttr::Feature(EthtoolFeatureAttr::Header(vec![ + EthtoolHeader::DevIndex(1), + EthtoolHeader::DevName("lo".into()) + ])) + ); +} diff --git a/tests/fec.rs b/tests/fec.rs new file mode 100644 index 0000000..4b5122e --- /dev/null +++ b/tests/fec.rs @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT + +//! Query test for the FEC subsystem. +//! +//! The loopback interface does not support FEC, netdevsim is required. See +//! [common::netdevsim_iface] for how to instantiate one, the test skips +//! when netdevsim is not available. + +mod common; + +use ethtool::{EthtoolAttr, EthtoolCmd, EthtoolFecAttr, EthtoolHeader}; +use futures_util::stream::StreamExt; + +#[tokio::test] +async fn query_fec_netdevsim() { + let Some(iface_name) = common::netdevsim_iface() else { + eprintln!("skipped query_fec_netdevsim: netdevsim is not available"); + return; + }; + + let mut handle = common::connect().await; + let mut stream = handle + .fec() + .get(Some(&iface_name)) + .execute() + .await + .expect("failed to request the FEC parameters"); + + let mut msgs = Vec::new(); + while let Some(reply) = stream.next().await { + msgs.push(reply.expect("failed to parse a FEC reply")); + } + + assert_eq!(msgs.len(), 1); + let ethtool_msg = &msgs[0].payload; + assert_eq!(ethtool_msg.cmd, EthtoolCmd::FecGetReply); + match ðtool_msg.nlas[0] { + EthtoolAttr::Fec(EthtoolFecAttr::Header(headers)) => { + assert!( + headers.contains(&EthtoolHeader::DevName(iface_name.clone())), + "unexpected FEC header {headers:?}" + ); + } + nla => panic!("unexpected first FEC attribute {nla:?}"), + } +} diff --git a/tests/get_features_lo.rs b/tests/get_features_lo.rs deleted file mode 100644 index 08dd2e2..0000000 --- a/tests/get_features_lo.rs +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: MIT - -use futures_util::stream::StreamExt; - -#[test] -fn test_get_features_of_loopback() { - let rt = tokio::runtime::Builder::new_current_thread() - .enable_io() - .build() - .unwrap(); - rt.block_on(get_feature(Some("lo"))); -} - -async fn get_feature(iface_name: Option<&str>) { - let (connection, mut handle, _) = ethtool::new_connection().unwrap(); - tokio::spawn(connection); - - let mut feature_handle = - handle.feature().get(iface_name).execute().await.unwrap(); - - let mut msgs = Vec::new(); - while let Some(Ok(msg)) = feature_handle.next().await { - msgs.push(msg); - } - assert!(msgs.len() == 1); - let ethtool_msg = &msgs[0].payload; - - assert!(ethtool_msg.cmd == ethtool::EthtoolCmd::FeatureGetReply); - assert!(ethtool_msg.nlas.len() > 1); - assert!( - ethtool_msg.nlas[0] - == ethtool::EthtoolAttr::Feature( - ethtool::EthtoolFeatureAttr::Header(vec![ - ethtool::EthtoolHeader::DevIndex(1), - ethtool::EthtoolHeader::DevName("lo".into()) - ]) - ) - ); -} diff --git a/tests/link_mode.rs b/tests/link_mode.rs new file mode 100644 index 0000000..587fa10 --- /dev/null +++ b/tests/link_mode.rs @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT + +//! Query test for the link mode subsystem. + +mod common; + +use ethtool::EthtoolCmd; +use futures_util::stream::StreamExt; + +#[tokio::test] +// CI container normally have a veth for external communication which support +// link modes of ethtool. +async fn query_link_modes_dump() { + let mut handle = common::connect().await; + + let mut stream = handle + .link_mode() + .get(None) + .execute() + .await + .expect("failed to request the link modes"); + + let mut msgs = Vec::new(); + while let Some(reply) = stream.next().await { + msgs.push(reply.expect("failed to parse a link mode reply")); + } + + assert!(!msgs.is_empty()); + let ethtool_msg = &msgs[0].payload; + println!("ethtool_msg {ethtool_msg:?}"); + + assert_eq!(ethtool_msg.cmd, EthtoolCmd::LinkModeGetReply); + assert!(ethtool_msg.nlas.len() > 1); +} diff --git a/tests/pause.rs b/tests/pause.rs new file mode 100644 index 0000000..8f362f1 --- /dev/null +++ b/tests/pause.rs @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT + +//! Query and set tests for the pause subsystem. +//! +//! The loopback interface does not support pause, netdevsim is required. +//! See [common::netdevsim_iface] for how to instantiate one, the tests skip +//! when netdevsim is not available. + +mod common; + +use ethtool::EthtoolPauseParsed; + +async fn get_pause(iface_name: &str) -> EthtoolPauseParsed { + let mut handle = common::connect().await; + + handle + .get_pause(iface_name) + .await + .expect("failed to request the pause configuration") +} + +#[tokio::test] +async fn get_pause_netdevsim() { + let Some(iface_name) = common::netdevsim_iface() else { + eprintln!("skipped get_pause_netdevsim: netdevsim is not available"); + return; + }; + + let config = get_pause(&iface_name).await; + + assert_eq!(config.iface_name.as_deref(), Some(iface_name.as_str())); + assert!(config.autoneg.is_some()); +} + +#[tokio::test] +async fn dump_pause_netdevsim() { + let Some(iface_name) = common::netdevsim_iface() else { + eprintln!("skipped dump_pause_netdevsim: netdevsim is not available"); + return; + }; + + let mut handle = common::connect().await; + let configs = handle + .dump_pause() + .await + .expect("failed to dump the pause configurations"); + + let config = configs + .iter() + .find(|config| { + config.iface_name.as_deref() == Some(iface_name.as_str()) + }) + .expect("the netdevsim interface is missing from the dump"); + assert!(config.autoneg.is_some()); +} + +#[tokio::test] +async fn set_pause_netdevsim() { + let Some(iface_name) = common::netdevsim_iface() else { + eprintln!("skipped set_pause_netdevsim: netdevsim is not available"); + return; + }; + + let mut config = EthtoolPauseParsed::default(); + config.iface_name = Some(iface_name.clone()); + config.autoneg = Some(false); + config.rx = Some(true); + config.tx = Some(true); + + let mut handle = common::connect().await; + if let Err(err) = handle.apply_pause(config).await { + if common::is_skippable(&err) { + eprintln!("skipped set_pause_netdevsim: {err}"); + return; + } + panic!("failed to apply the pause configuration: {err}"); + } + + let config = get_pause(&iface_name).await; + + assert_eq!(config.autoneg, Some(false)); + assert_eq!(config.rx, Some(true)); + assert_eq!(config.tx, Some(true)); +} diff --git a/tests/ring.rs b/tests/ring.rs new file mode 100644 index 0000000..8c6a287 --- /dev/null +++ b/tests/ring.rs @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT + +//! Query test for the ring subsystem. +//! +//! The loopback interface does not support rings, netdevsim is required. +//! See [common::netdevsim_iface] for how to instantiate one, the test skips +//! when netdevsim is not available. + +mod common; + +use ethtool::{EthtoolAttr, EthtoolCmd, EthtoolHeader, EthtoolRingAttr}; +use futures_util::stream::StreamExt; + +#[tokio::test] +async fn query_rings_netdevsim() { + let Some(iface_name) = common::netdevsim_iface() else { + eprintln!("skipped query_rings_netdevsim: netdevsim is not available"); + return; + }; + + let mut handle = common::connect().await; + let mut stream = handle + .ring() + .get(Some(&iface_name)) + .execute() + .await + .expect("failed to request the ring parameters"); + + let mut msgs = Vec::new(); + while let Some(reply) = stream.next().await { + msgs.push(reply.expect("failed to parse a ring reply")); + } + + assert_eq!(msgs.len(), 1); + let ethtool_msg = &msgs[0].payload; + assert_eq!(ethtool_msg.cmd, EthtoolCmd::RingGetReply); + match ðtool_msg.nlas[0] { + EthtoolAttr::Ring(EthtoolRingAttr::Header(headers)) => { + assert!( + headers.contains(&EthtoolHeader::DevName(iface_name.clone())), + "unexpected ring header {headers:?}" + ); + } + nla => panic!("unexpected first ring attribute {nla:?}"), + } +} diff --git a/tests/tsinfo.rs b/tests/tsinfo.rs new file mode 100644 index 0000000..f6c20ed --- /dev/null +++ b/tests/tsinfo.rs @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT + +//! Query test for the timestamping subsystem. + +mod common; + +use ethtool::{EthtoolAttr, EthtoolCmd, EthtoolHeader, EthtoolTsInfoAttr}; +use futures_util::stream::StreamExt; + +#[tokio::test] +async fn query_tsinfo_lo() { + let mut handle = common::connect().await; + + // The loopback interface provides software timestamping. + let mut stream = handle + .tsinfo() + .get(Some("lo")) + .execute() + .await + .expect("failed to request the timestamping information of `lo`"); + + let mut msgs = Vec::new(); + while let Some(reply) = stream.next().await { + msgs.push(reply.expect("failed to parse a timestamping reply")); + } + + assert_eq!(msgs.len(), 1); + let ethtool_msg = &msgs[0].payload; + assert_eq!(ethtool_msg.cmd, EthtoolCmd::TsInfoGetReply); + match ðtool_msg.nlas[0] { + EthtoolAttr::TsInfo(EthtoolTsInfoAttr::Header(headers)) => { + assert!( + headers.contains(&EthtoolHeader::DevName("lo".to_string())), + "unexpected timestamping header {headers:?}" + ); + } + nla => panic!("unexpected first timestamping attribute {nla:?}"), + } +}