From 2b34e9c0e2e34c37c5c1279698d4b4e6299cb38b Mon Sep 17 00:00:00 2001 From: Connor Worley Date: Fri, 1 Sep 2023 01:26:23 -0700 Subject: [PATCH] Add channel get and set functionality --- examples/dump_channels.rs | 29 +++++++ examples/set_rx_count.rs | 46 +++++++++++ src/channel/attr.rs | 155 ++++++++++++++++++++++++++++++++++++++ src/channel/get.rs | 34 +++++++++ src/channel/handle.rs | 28 +++++++ src/channel/mod.rs | 13 ++++ src/channel/set.rs | 97 ++++++++++++++++++++++++ src/handle.rs | 10 ++- src/lib.rs | 5 ++ src/message.rs | 46 +++++++++++ tests/channels.rs | 133 ++++++++++++++++++++++++++++++++ 11 files changed, 593 insertions(+), 3 deletions(-) create mode 100644 examples/dump_channels.rs create mode 100644 examples/set_rx_count.rs create mode 100644 src/channel/attr.rs create mode 100644 src/channel/get.rs create mode 100644 src/channel/handle.rs create mode 100644 src/channel/mod.rs create mode 100644 src/channel/set.rs create mode 100644 tests/channels.rs diff --git a/examples/dump_channels.rs b/examples/dump_channels.rs new file mode 100644 index 0000000..ab32dfd --- /dev/null +++ b/examples/dump_channels.rs @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT + +use futures::stream::TryStreamExt; + +// Once we find a way to load netsimdev kernel module in CI, we can convert this +// to a test +fn main() { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_io() + .build() + .unwrap(); + rt.block_on(get_channels(None)); +} + +async fn get_channels(iface_name: Option<&str>) { + let (connection, mut handle, _) = ethtool::new_connection().unwrap(); + tokio::spawn(connection); + + let mut channel_handle = handle.channel().get(iface_name).execute().await; + + let mut msgs = Vec::new(); + while let Some(msg) = channel_handle.try_next().await.unwrap() { + msgs.push(msg); + } + assert!(!msgs.is_empty()); + for msg in msgs { + println!("{msg:?}"); + } +} diff --git a/examples/set_rx_count.rs b/examples/set_rx_count.rs new file mode 100644 index 0000000..b7ec1d6 --- /dev/null +++ b/examples/set_rx_count.rs @@ -0,0 +1,46 @@ +// 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(set_rx_count(link_name)); +} + +async fn set_rx_count(iface_name: &str) { + let (connection, mut handle, _) = ethtool::new_connection().unwrap(); + tokio::spawn(connection); + + let result = handle.channel().set(iface_name).rx_count(4).execute().await; + + if let Err(error) = result { + panic!("{:?}", error); + } +} + +fn usage() { + eprintln!( + "usage: + cargo run --example set_rx_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 + +Then find the binary in the target directory: + + cd target/debug/example ; sudo ./set_rx_count " + ); +} diff --git a/src/channel/attr.rs b/src/channel/attr.rs new file mode 100644 index 0000000..d026c5f --- /dev/null +++ b/src/channel/attr.rs @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: MIT + +use anyhow::Context; +use byteorder::{ByteOrder, NativeEndian}; +use netlink_packet_utils::{ + nla::{DefaultNla, Nla, NlaBuffer, NlasIterator, NLA_F_NESTED}, + parsers::parse_u32, + DecodeError, Emitable, Parseable, +}; + +use crate::{EthtoolAttr, EthtoolHeader}; + +const ETHTOOL_A_CHANNELS_HEADER: u16 = 1; +const ETHTOOL_A_CHANNELS_RX_MAX: u16 = 2; +const ETHTOOL_A_CHANNELS_TX_MAX: u16 = 3; +const ETHTOOL_A_CHANNELS_OTHER_MAX: u16 = 4; +const ETHTOOL_A_CHANNELS_COMBINED_MAX: u16 = 5; +const ETHTOOL_A_CHANNELS_RX_COUNT: u16 = 6; +const ETHTOOL_A_CHANNELS_TX_COUNT: u16 = 7; +const ETHTOOL_A_CHANNELS_OTHER_COUNT: u16 = 8; +const ETHTOOL_A_CHANNELS_COMBINED_COUNT: u16 = 9; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum EthtoolChannelAttr { + Header(Vec), + RxMax(u32), + TxMax(u32), + OtherMax(u32), + CombinedMax(u32), + RxCount(u32), + TxCount(u32), + OtherCount(u32), + CombinedCount(u32), + Other(DefaultNla), +} + +impl Nla for EthtoolChannelAttr { + fn value_len(&self) -> usize { + match self { + Self::Header(hdrs) => hdrs.as_slice().buffer_len(), + Self::RxMax(_) + | Self::TxMax(_) + | Self::OtherMax(_) + | Self::CombinedMax(_) + | Self::RxCount(_) + | Self::TxCount(_) + | Self::OtherCount(_) + | Self::CombinedCount(_) => 4, + Self::Other(attr) => attr.value_len(), + } + } + + fn kind(&self) -> u16 { + match self { + Self::Header(_) => ETHTOOL_A_CHANNELS_HEADER | NLA_F_NESTED, + Self::RxMax(_) => ETHTOOL_A_CHANNELS_RX_MAX, + Self::TxMax(_) => ETHTOOL_A_CHANNELS_TX_MAX, + Self::OtherMax(_) => ETHTOOL_A_CHANNELS_OTHER_MAX, + Self::CombinedMax(_) => ETHTOOL_A_CHANNELS_COMBINED_MAX, + Self::RxCount(_) => ETHTOOL_A_CHANNELS_RX_COUNT, + Self::TxCount(_) => ETHTOOL_A_CHANNELS_TX_COUNT, + Self::OtherCount(_) => ETHTOOL_A_CHANNELS_OTHER_COUNT, + Self::CombinedCount(_) => ETHTOOL_A_CHANNELS_COMBINED_COUNT, + Self::Other(attr) => attr.kind(), + } + } + + fn emit_value(&self, buffer: &mut [u8]) { + match self { + Self::Header(ref nlas) => nlas.as_slice().emit(buffer), + Self::RxMax(d) + | Self::TxMax(d) + | Self::OtherMax(d) + | Self::CombinedMax(d) + | Self::RxCount(d) + | Self::TxCount(d) + | Self::OtherCount(d) + | Self::CombinedCount(d) => NativeEndian::write_u32(buffer, *d), + Self::Other(ref attr) => attr.emit(buffer), + } + } +} + +impl<'a, T: AsRef<[u8]> + ?Sized> Parseable> + for EthtoolChannelAttr +{ + fn parse(buf: &NlaBuffer<&'a T>) -> Result { + let payload = buf.value(); + Ok(match buf.kind() { + ETHTOOL_A_CHANNELS_HEADER => { + let mut nlas = Vec::new(); + let error_msg = "failed to parse channel header attributes"; + for nla in NlasIterator::new(payload) { + let nla = &nla.context(error_msg)?; + let parsed = + EthtoolHeader::parse(nla).context(error_msg)?; + nlas.push(parsed); + } + Self::Header(nlas) + } + ETHTOOL_A_CHANNELS_RX_MAX => Self::RxMax( + parse_u32(payload) + .context("Invalid ETHTOOL_A_CHANNELS_RX_MAX value")?, + ), + ETHTOOL_A_CHANNELS_TX_MAX => Self::TxMax( + parse_u32(payload) + .context("Invalid ETHTOOL_A_CHANNELS_RX_MAX value")?, + ), + ETHTOOL_A_CHANNELS_OTHER_MAX => Self::OtherMax( + parse_u32(payload) + .context("Invalid ETHTOOL_A_CHANNELS_RX_MAX value")?, + ), + ETHTOOL_A_CHANNELS_COMBINED_MAX => Self::CombinedMax( + parse_u32(payload) + .context("Invalid ETHTOOL_A_CHANNELS_RX_MAX value")?, + ), + ETHTOOL_A_CHANNELS_RX_COUNT => Self::RxCount( + parse_u32(payload) + .context("Invalid ETHTOOL_A_CHANNELS_RX_COUNT value")?, + ), + ETHTOOL_A_CHANNELS_TX_COUNT => Self::TxCount( + parse_u32(payload) + .context("Invalid ETHTOOL_A_CHANNELS_RX_COUNT value")?, + ), + ETHTOOL_A_CHANNELS_OTHER_COUNT => Self::OtherCount( + parse_u32(payload) + .context("Invalid ETHTOOL_A_CHANNELS_RX_COUNT value")?, + ), + ETHTOOL_A_CHANNELS_COMBINED_COUNT => Self::CombinedCount( + parse_u32(payload) + .context("Invalid ETHTOOL_A_CHANNELS_RX_COUNT value")?, + ), + kind => { + Self::Other(DefaultNla::parse(buf).context(format!( + "invalid ethtool channel NLA kind {kind}" + ))?) + } + }) + } +} + +pub(crate) fn parse_channel_nlas( + buffer: &[u8], +) -> Result, DecodeError> { + let mut nlas = Vec::new(); + for nla in NlasIterator::new(buffer) { + let error_msg = format!( + "Failed to parse ethtool channel message attribute {nla:?}" + ); + let nla = &nla.context(error_msg.clone())?; + let parsed = EthtoolChannelAttr::parse(nla).context(error_msg)?; + nlas.push(EthtoolAttr::Channel(parsed)); + } + Ok(nlas) +} diff --git a/src/channel/get.rs b/src/channel/get.rs new file mode 100644 index 0000000..2ebe54f --- /dev/null +++ b/src/channel/get.rs @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT + +use futures::TryStream; +use netlink_packet_generic::GenlMessage; + +use crate::{ethtool_execute, EthtoolError, EthtoolHandle, EthtoolMessage}; + +pub struct EthtoolChannelGetRequest { + handle: EthtoolHandle, + iface_name: Option, +} + +impl EthtoolChannelGetRequest { + pub(crate) fn new(handle: EthtoolHandle, iface_name: Option<&str>) -> Self { + EthtoolChannelGetRequest { + handle, + iface_name: iface_name.map(|i| i.to_string()), + } + } + + pub async fn execute( + self, + ) -> impl TryStream, Error = EthtoolError> + { + let EthtoolChannelGetRequest { + mut handle, + iface_name, + } = self; + + let ethtool_msg = + EthtoolMessage::new_channel_get(iface_name.as_deref()); + ethtool_execute(&mut handle, iface_name.is_none(), ethtool_msg).await + } +} diff --git a/src/channel/handle.rs b/src/channel/handle.rs new file mode 100644 index 0000000..5046e4a --- /dev/null +++ b/src/channel/handle.rs @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT + +use crate::{ + EthtoolChannelGetRequest, EthtoolChannelSetRequest, EthtoolHandle, +}; + +pub struct EthtoolChannelHandle(EthtoolHandle); + +impl EthtoolChannelHandle { + pub fn new(handle: EthtoolHandle) -> Self { + EthtoolChannelHandle(handle) + } + + /// Retrieve the ethtool Channels of a interface (equivalent to `ethtool -l + /// eth1`) + pub fn get( + &mut self, + iface_name: Option<&str>, + ) -> EthtoolChannelGetRequest { + EthtoolChannelGetRequest::new(self.0.clone(), iface_name) + } + + /// Set the ethtool Channels of a interface (equivalent to `ethtool -L + /// eth1`) + pub fn set(&mut self, iface_name: &str) -> EthtoolChannelSetRequest { + EthtoolChannelSetRequest::new(self.0.clone(), iface_name) + } +} diff --git a/src/channel/mod.rs b/src/channel/mod.rs new file mode 100644 index 0000000..8d2b980 --- /dev/null +++ b/src/channel/mod.rs @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT + +mod attr; +mod get; +mod handle; +mod set; + +pub(crate) use attr::parse_channel_nlas; + +pub use attr::EthtoolChannelAttr; +pub use get::EthtoolChannelGetRequest; +pub use handle::EthtoolChannelHandle; +pub use set::EthtoolChannelSetRequest; diff --git a/src/channel/set.rs b/src/channel/set.rs new file mode 100644 index 0000000..80a6cea --- /dev/null +++ b/src/channel/set.rs @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: MIT + +use futures::StreamExt; +use netlink_packet_core::{NetlinkMessage, NLM_F_ACK, NLM_F_REQUEST}; +use netlink_packet_generic::GenlMessage; + +use crate::{ + try_ethtool, EthtoolAttr, EthtoolChannelAttr, EthtoolError, EthtoolHandle, + EthtoolMessage, +}; + +pub struct EthtoolChannelSetRequest { + handle: EthtoolHandle, + message: EthtoolMessage, + rx_count: Option, + tx_count: Option, + other_count: Option, + combined_count: Option, +} + +impl EthtoolChannelSetRequest { + pub(crate) fn new(handle: EthtoolHandle, iface_name: &str) -> Self { + EthtoolChannelSetRequest { + handle, + message: EthtoolMessage::new_channel_set(iface_name), + rx_count: None, + tx_count: None, + other_count: None, + combined_count: None, + } + } + + pub fn rx_count(mut self, count: u32) -> Self { + self.rx_count = Some(count); + self + } + + pub fn tx_count(mut self, count: u32) -> Self { + self.tx_count = Some(count); + self + } + + pub fn other_count(mut self, count: u32) -> Self { + self.other_count = Some(count); + self + } + + pub fn combined_count(mut self, count: u32) -> Self { + self.combined_count = Some(count); + self + } + + pub async fn execute(self) -> Result<(), EthtoolError> { + let EthtoolChannelSetRequest { + mut handle, + mut message, + rx_count, + tx_count, + other_count, + combined_count, + } = self; + + if let Some(count) = rx_count { + message + .nlas + .push(EthtoolAttr::Channel(EthtoolChannelAttr::RxCount(count))); + } + if let Some(count) = tx_count { + message + .nlas + .push(EthtoolAttr::Channel(EthtoolChannelAttr::TxCount(count))); + } + if let Some(count) = other_count { + message.nlas.push(EthtoolAttr::Channel( + EthtoolChannelAttr::OtherCount(count), + )); + } + if let Some(count) = combined_count { + message.nlas.push(EthtoolAttr::Channel( + EthtoolChannelAttr::CombinedCount(count), + )); + } + + let mut nl_msg = + NetlinkMessage::from(GenlMessage::from_payload(message)); + + 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/handle.rs b/src/handle.rs index cf77188..8fa8a78 100644 --- a/src/handle.rs +++ b/src/handle.rs @@ -9,9 +9,9 @@ use netlink_packet_generic::GenlMessage; use netlink_packet_utils::DecodeError; use crate::{ - try_ethtool, EthtoolCoalesceHandle, EthtoolError, EthtoolFeatureHandle, - EthtoolFecHandle, EthtoolLinkModeHandle, EthtoolMessage, - EthtoolPauseHandle, EthtoolRingHandle, EthtoolTsInfoHandle, + try_ethtool, EthtoolChannelHandle, EthtoolCoalesceHandle, EthtoolError, + EthtoolFeatureHandle, EthtoolFecHandle, EthtoolLinkModeHandle, + EthtoolMessage, EthtoolPauseHandle, EthtoolRingHandle, EthtoolTsInfoHandle, }; #[derive(Clone, Debug)] @@ -52,6 +52,10 @@ impl EthtoolHandle { EthtoolFecHandle::new(self.clone()) } + pub fn channel(&mut self) -> EthtoolChannelHandle { + EthtoolChannelHandle::new(self.clone()) + } + pub async fn request( &mut self, message: NetlinkMessage>, diff --git a/src/lib.rs b/src/lib.rs index 9fc2436..87c9469 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT mod bitset_util; +mod channel; mod coalesce; mod connection; mod error; @@ -19,6 +20,10 @@ pub use self::fec::{ EthtoolFecAttr, EthtoolFecGetRequest, EthtoolFecHandle, EthtoolFecMode, EthtoolFecStat, }; +pub use channel::{ + EthtoolChannelAttr, EthtoolChannelGetRequest, EthtoolChannelHandle, + EthtoolChannelSetRequest, +}; pub use coalesce::{ EthtoolCoalesceAttr, EthtoolCoalesceGetRequest, EthtoolCoalesceHandle, }; diff --git a/src/message.rs b/src/message.rs index ff2bcdb..66a79a1 100644 --- a/src/message.rs +++ b/src/message.rs @@ -6,6 +6,7 @@ use netlink_packet_utils::{ }; use crate::{ + channel::{parse_channel_nlas, EthtoolChannelAttr}, coalesce::{parse_coalesce_nlas, EthtoolCoalesceAttr}, feature::{parse_feature_nlas, EthtoolFeatureAttr}, fec::{parse_fec_nlas, EthtoolFecAttr}, @@ -30,6 +31,9 @@ const ETHTOOL_MSG_TSINFO_GET: u8 = 25; const ETHTOOL_MSG_TSINFO_GET_REPLY: u8 = 26; const ETHTOOL_MSG_FEC_GET: u8 = 29; const ETHTOOL_MSG_FEC_GET_REPLY: u8 = 30; +const ETHTOOL_MSG_CHANNELS_GET: u8 = 17; +const ETHTOOL_MSG_CHANNELS_GET_REPLY: u8 = 18; +const ETHTOOL_MSG_CHANNELS_SET: u8 = 18; #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum EthtoolCmd { @@ -47,6 +51,9 @@ pub enum EthtoolCmd { TsInfoGetReply, FecGet, FecGetReply, + ChannelGet, + ChannelGetReply, + ChannelSet, } impl From for u8 { @@ -66,6 +73,9 @@ impl From for u8 { EthtoolCmd::TsInfoGetReply => ETHTOOL_MSG_TSINFO_GET_REPLY, EthtoolCmd::FecGet => ETHTOOL_MSG_FEC_GET, EthtoolCmd::FecGetReply => ETHTOOL_MSG_FEC_GET_REPLY, + EthtoolCmd::ChannelGet => ETHTOOL_MSG_CHANNELS_GET, + EthtoolCmd::ChannelGetReply => ETHTOOL_MSG_CHANNELS_GET_REPLY, + EthtoolCmd::ChannelSet => ETHTOOL_MSG_CHANNELS_SET, } } } @@ -79,6 +89,7 @@ pub enum EthtoolAttr { Coalesce(EthtoolCoalesceAttr), TsInfo(EthtoolTsInfoAttr), Fec(EthtoolFecAttr), + Channel(EthtoolChannelAttr), } impl Nla for EthtoolAttr { @@ -91,6 +102,7 @@ impl Nla for EthtoolAttr { Self::Coalesce(attr) => attr.value_len(), Self::TsInfo(attr) => attr.value_len(), Self::Fec(attr) => attr.value_len(), + Self::Channel(attr) => attr.value_len(), } } @@ -103,6 +115,7 @@ impl Nla for EthtoolAttr { Self::Coalesce(attr) => attr.kind(), Self::TsInfo(attr) => attr.kind(), Self::Fec(attr) => attr.kind(), + Self::Channel(attr) => attr.kind(), } } @@ -115,6 +128,7 @@ impl Nla for EthtoolAttr { Self::Coalesce(attr) => attr.emit_value(buffer), Self::TsInfo(attr) => attr.emit_value(buffer), Self::Fec(attr) => attr.emit_value(buffer), + Self::Channel(attr) => attr.emit_value(buffer), } } } @@ -252,6 +266,34 @@ impl EthtoolMessage { nlas, } } + + pub fn new_channel_get(iface_name: Option<&str>) -> Self { + let nlas = match iface_name { + Some(s) => { + vec![EthtoolAttr::Channel(EthtoolChannelAttr::Header(vec![ + EthtoolHeader::DevName(s.to_string()), + ]))] + } + None => { + vec![EthtoolAttr::Channel(EthtoolChannelAttr::Header(vec![]))] + } + }; + EthtoolMessage { + cmd: EthtoolCmd::ChannelGet, + nlas, + } + } + + pub fn new_channel_set(iface_name: &str) -> Self { + let nlas = + vec![EthtoolAttr::Channel(EthtoolChannelAttr::Header(vec![ + EthtoolHeader::DevName(iface_name.to_string()), + ]))]; + EthtoolMessage { + cmd: EthtoolCmd::ChannelSet, + nlas, + } + } } impl Emitable for EthtoolMessage { @@ -298,6 +340,10 @@ impl ParseableParametrized<[u8], GenlHeader> for EthtoolMessage { cmd: EthtoolCmd::FecGetReply, nlas: parse_fec_nlas(buffer)?, }, + ETHTOOL_MSG_CHANNELS_GET_REPLY => Self { + cmd: EthtoolCmd::ChannelGetReply, + nlas: parse_channel_nlas(buffer)?, + }, cmd => { return Err(DecodeError::from(format!( "Unsupported ethtool reply command: {cmd}" diff --git a/tests/channels.rs b/tests/channels.rs new file mode 100644 index 0000000..e62e3c4 --- /dev/null +++ b/tests/channels.rs @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: MIT + +use ethtool::{ + EthtoolAttr, EthtoolChannelAttr, EthtoolCmd, EthtoolHeader, EthtoolMessage, +}; +use netlink_packet_generic::{GenlBuffer, GenlHeader}; +use netlink_packet_utils::{Emitable, Parseable, ParseableParametrized}; + +#[test] +fn test_channels_get_reply() { + let raw: Vec = vec![ + 0x12, 0x01, 0x00, 0x00, 0x18, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x09, 0x00, 0x02, 0x00, 0x65, 0x74, 0x68, 0x30, + 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x05, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x09, 0x00, 0x02, 0x00, 0x00, 0x00, + ]; + + let expected = EthtoolMessage { + cmd: EthtoolCmd::ChannelGetReply, + nlas: vec![ + EthtoolAttr::Channel(EthtoolChannelAttr::Header(vec![ + EthtoolHeader::DevIndex(2), + EthtoolHeader::DevName("eth0".to_string()), + ])), + EthtoolAttr::Channel(EthtoolChannelAttr::CombinedMax(4)), + EthtoolAttr::Channel(EthtoolChannelAttr::CombinedCount(2)), + ], + }; + + let header = GenlHeader::parse(&GenlBuffer::new(&raw)).unwrap(); + + assert_eq!( + expected, + EthtoolMessage::parse_with_param(&raw[4..], header).unwrap(), + ); + + let mut buffer = vec![0; expected.buffer_len() + header.buffer_len()]; + header.emit(&mut buffer); + expected.emit(&mut buffer[4..]); + assert_eq!(&buffer, &raw); +} + +#[test] +fn test_channels_set_rx() { + let expected: Vec = vec![ + 0x10, 0x00, 0x01, 0x80, 0x09, 0x00, 0x02, 0x00, 0x65, 0x74, 0x68, 0x30, + 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x06, 0x00, 0x02, 0x00, 0x00, 0x00, + ]; + + let msg = EthtoolMessage { + cmd: EthtoolCmd::ChannelSet, + nlas: vec![ + EthtoolAttr::Channel(EthtoolChannelAttr::Header(vec![ + EthtoolHeader::DevName("eth0".to_string()), + ])), + EthtoolAttr::Channel(EthtoolChannelAttr::RxCount(2)), + ], + }; + + let mut raw = vec![0; msg.buffer_len()]; + msg.emit(&mut raw); + + assert_eq!(expected, raw,); +} + +#[test] +fn test_channels_set_tx() { + let expected: Vec = vec![ + 0x10, 0x00, 0x01, 0x80, 0x09, 0x00, 0x02, 0x00, 0x65, 0x74, 0x68, 0x30, + 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x07, 0x00, 0x02, 0x00, 0x00, 0x00, + ]; + + let msg = EthtoolMessage { + cmd: EthtoolCmd::ChannelSet, + nlas: vec![ + EthtoolAttr::Channel(EthtoolChannelAttr::Header(vec![ + EthtoolHeader::DevName("eth0".to_string()), + ])), + EthtoolAttr::Channel(EthtoolChannelAttr::TxCount(2)), + ], + }; + + let mut raw = vec![0; msg.buffer_len()]; + msg.emit(&mut raw); + + assert_eq!(expected, raw,); +} + +#[test] +fn test_channels_set_other() { + let expected: Vec = vec![ + 0x10, 0x00, 0x01, 0x80, 0x09, 0x00, 0x02, 0x00, 0x65, 0x74, 0x68, 0x30, + 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0x02, 0x00, 0x00, 0x00, + ]; + + let msg = EthtoolMessage { + cmd: EthtoolCmd::ChannelSet, + nlas: vec![ + EthtoolAttr::Channel(EthtoolChannelAttr::Header(vec![ + EthtoolHeader::DevName("eth0".to_string()), + ])), + EthtoolAttr::Channel(EthtoolChannelAttr::OtherCount(2)), + ], + }; + + let mut raw = vec![0; msg.buffer_len()]; + msg.emit(&mut raw); + + assert_eq!(expected, raw,); +} + +#[test] +fn test_channels_set_combined() { + let expected: Vec = vec![ + 0x10, 0x00, 0x01, 0x80, 0x09, 0x00, 0x02, 0x00, 0x65, 0x74, 0x68, 0x30, + 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x09, 0x00, 0x02, 0x00, 0x00, 0x00, + ]; + + let msg = EthtoolMessage { + cmd: EthtoolCmd::ChannelSet, + nlas: vec![ + EthtoolAttr::Channel(EthtoolChannelAttr::Header(vec![ + EthtoolHeader::DevName("eth0".to_string()), + ])), + EthtoolAttr::Channel(EthtoolChannelAttr::CombinedCount(2)), + ], + }; + + let mut raw = vec![0; msg.buffer_len()]; + msg.emit(&mut raw); + + assert_eq!(expected, raw,); +}