diff --git a/SOURCES/0001-npc-Show-brief-network-state-when-no-argument.patch b/SOURCES/0001-npc-Show-brief-network-state-when-no-argument.patch new file mode 100644 index 0000000..1e42ebf --- /dev/null +++ b/SOURCES/0001-npc-Show-brief-network-state-when-no-argument.patch @@ -0,0 +1,375 @@ +From 69cc9aaf259d4c55b74d7b75037992431136ba42 Mon Sep 17 00:00:00 2001 +From: Gris Ge +Date: Fri, 28 May 2021 18:18:13 +0800 +Subject: [PATCH 1/3] npc: Show brief network state when no argument + +When running `npc` without any argument, instead of dumping all +information, this patch changed to show brief network status. + +Added new sub command `iface` to `npc` for old behaviour: + + * `npc eth1` -- show eth1 interface info. + * `npc iface eth1` -- the same as `npc eth1`. + * `npc iface` -- show full information in yaml format. + * `npc` -- show brief network state. + * `npc --json` -- show brief network state in json format. + +Example on running `npc` without argument: + +``` +1: lo: state unknown mtu 65536 + mac 00:00:00:00:00:00 + ipv4 127.0.0.1/8 + ipv6 ::1/128 +2: eth1: state up mtu 1500 + mac 00:01:02:03:04:05 + ipv4 192.0.2.6/24 gw4 192.0.2.1 + ipv6 2001:db8::6/64 fe80::6/64 gw6 fe80::1 +``` + +This patch contains many memory copy done by `to_string()`, `clone()`, +`to_vec()`. We can improve it in future if that cause any problem. + +Signed-off-by: Gris Ge +--- + src/cli/npc.rs | 232 +++++++++++++++++++++++++++++++++++++--- + src/lib/ifaces/iface.rs | 17 +++ + 2 files changed, 237 insertions(+), 12 deletions(-) + +diff --git a/src/cli/npc.rs b/src/cli/npc.rs +index b7171bb..41ddbd8 100644 +--- a/src/cli/npc.rs ++++ b/src/cli/npc.rs +@@ -13,19 +13,188 @@ + // limitations under the License. + + use clap::{clap_app, crate_authors, crate_version}; +-use nispor::{Iface, NetConf, NetState, NisporError, Route, RouteRule}; ++use nispor::{ ++ Iface, IfaceState, NetConf, NetState, NisporError, Route, RouteRule, ++}; + use serde_derive::Serialize; + use serde_json; + use serde_yaml; ++use std::collections::HashMap; + use std::fmt; + use std::io::{stderr, stdout, Write}; + use std::process; + +-#[derive(Serialize)] ++const INDENT: &str = " "; ++ ++#[derive(Serialize, Debug)] + pub struct CliError { + pub msg: String, + } + ++#[derive(Serialize, Default)] ++struct CliIfaceBrief { ++ index: u32, ++ name: String, ++ state: IfaceState, ++ flags: Vec, ++ mac: String, ++ permanent_mac: String, ++ mtu: i64, ++ ipv4: Vec, ++ ipv6: Vec, ++ gw4: Vec, ++ gw6: Vec, ++} ++ ++impl CliIfaceBrief { ++ fn to_string(briefs: &[CliIfaceBrief]) -> String { ++ let mut ret = Vec::new(); ++ for brief in briefs { ++ ret.push(format!( ++ "{}: {}: <{}> state {} mtu {}", ++ brief.index, ++ brief.name, ++ brief.flags.join(","), ++ brief.state, ++ brief.mtu, ++ )); ++ if &brief.mac != "" { ++ ret.push(format!( ++ "{}mac {}{}", ++ INDENT, ++ brief.mac, ++ if &brief.permanent_mac != "" ++ && &brief.permanent_mac != &brief.mac ++ { ++ format!(" permanent_mac: {}", brief.permanent_mac) ++ } else { ++ "".into() ++ } ++ )); ++ } ++ if brief.ipv4.len() > 0 { ++ ret.push(format!( ++ "{}ipv4 {}{}", ++ INDENT, ++ brief.ipv4.join(" "), ++ if brief.gw4.len() > 0 { ++ format!(" gw4 {}", brief.gw4.join(" ")) ++ } else { ++ "".into() ++ }, ++ )); ++ } ++ if brief.ipv6.len() > 0 { ++ ret.push(format!( ++ "{}ipv6 {}{}", ++ INDENT, ++ brief.ipv6.join(" "), ++ if brief.gw6.len() > 0 { ++ format!(" gw6 {}", brief.gw6.join(" ")) ++ } else { ++ "".into() ++ } ++ )); ++ } ++ } ++ ret.join("\n") ++ } ++ ++ fn from_net_state(netstate: &NetState) -> Vec { ++ let mut ret = Vec::new(); ++ let mut iface_to_gw4: HashMap> = HashMap::new(); ++ let mut iface_to_gw6: HashMap> = HashMap::new(); ++ ++ for route in &netstate.routes { ++ if let Route { ++ dst: None, ++ gateway: Some(gw), ++ oif: Some(iface_name), ++ .. ++ } = route ++ { ++ if gw.contains(":") { ++ match iface_to_gw6.get_mut(iface_name) { ++ Some(gateways) => { ++ gateways.push(gw.to_string()); ++ } ++ None => { ++ iface_to_gw6.insert( ++ iface_name.to_string(), ++ vec![gw.to_string()], ++ ); ++ } ++ } ++ } else { ++ match iface_to_gw4.get_mut(iface_name) { ++ Some(gateways) => { ++ gateways.push(gw.to_string()); ++ } ++ None => { ++ iface_to_gw4.insert( ++ iface_name.to_string(), ++ vec![gw.to_string()], ++ ); ++ } ++ } ++ } ++ } ++ } ++ ++ for iface in netstate.ifaces.values() { ++ ret.push(CliIfaceBrief { ++ index: iface.index, ++ name: iface.name.clone(), ++ flags: (&iface.flags) ++ .into_iter() ++ .map(|flag| format!("{:?}", flag).to_uppercase()) ++ .collect(), ++ state: iface.state.clone(), ++ mac: iface.mac_address.clone(), ++ permanent_mac: iface.permanent_mac_address.clone(), ++ mtu: iface.mtu, ++ ipv4: match &iface.ipv4 { ++ Some(ip_info) => { ++ let mut addr_strs = Vec::new(); ++ for addr in &ip_info.addresses { ++ addr_strs.push(format!( ++ "{}/{}", ++ addr.address, addr.prefix_len ++ )); ++ } ++ addr_strs ++ } ++ None => Vec::new(), ++ }, ++ ipv6: match &iface.ipv6 { ++ Some(ip_info) => { ++ let mut addr_strs = Vec::new(); ++ for addr in &ip_info.addresses { ++ addr_strs.push(format!( ++ "{}/{}", ++ addr.address, addr.prefix_len ++ )); ++ } ++ addr_strs ++ } ++ None => Vec::new(), ++ }, ++ gw4: match &iface_to_gw4.get(&iface.name) { ++ Some(gws) => gws.to_vec(), ++ None => Vec::new(), ++ }, ++ gw6: match &iface_to_gw6.get(&iface.name) { ++ Some(gws) => gws.to_vec(), ++ None => Vec::new(), ++ }, ++ ..Default::default() ++ }) ++ } ++ ret.sort_by(|a, b| a.index.cmp(&b.index)); ++ ret ++ } ++} ++ + impl fmt::Display for CliError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.msg) +@@ -34,6 +203,7 @@ impl fmt::Display for CliError { + + enum CliResult { + Pass, ++ Brief(Vec), + Full(NetState), + Ifaces(Vec), + Routes(Vec), +@@ -42,6 +212,7 @@ enum CliResult { + NisporError(NisporError), + } + ++#[derive(PartialEq)] + enum CliOutputType { + Json, + Yaml, +@@ -53,6 +224,7 @@ macro_rules! npc_print { + CliResult::Pass => { + process::exit(0); + } ++ CliResult::Brief(_) => unreachable!(), + CliResult::Full(netstate) => { + writeln!(stdout(), "{}", $display_func(&netstate).unwrap()) + .ok(); +@@ -83,9 +255,24 @@ macro_rules! npc_print { + } + + fn print_result(result: &CliResult, output_type: CliOutputType) { +- match output_type { +- CliOutputType::Json => npc_print!(serde_json::to_string_pretty, result), +- CliOutputType::Yaml => npc_print!(serde_yaml::to_string, result), ++ if let CliResult::Brief(briefs) = result { ++ if output_type == CliOutputType::Json { ++ writeln!( ++ stdout(), ++ "{}", ++ serde_json::to_string_pretty(&briefs).unwrap() ++ ) ++ .ok(); ++ } else { ++ writeln!(stdout(), "{}", CliIfaceBrief::to_string(&briefs)).ok(); ++ } ++ } else { ++ match output_type { ++ CliOutputType::Json => { ++ npc_print!(serde_json::to_string_pretty, result) ++ } ++ CliOutputType::Yaml => npc_print!(serde_yaml::to_string, result), ++ } + } + } + +@@ -133,6 +320,11 @@ fn main() { + (about: "Nispor CLI") + (@arg ifname: [INTERFACE_NAME] "interface name") + (@arg json: -j --json "Show in json format") ++ (@subcommand iface => ++ (@arg json: -j --json "Show in json format") ++ (@arg ifname: [INTERFACE_NAME] "Show only specified interface") ++ (about: "Show interface") ++ ) + (@subcommand route => + (@arg json: -j --json "Show in json format") + (@arg dev: -d --dev [OIF] "Show only route entries with output to the specified interface") +@@ -162,13 +354,21 @@ fn main() { + } else { + let result = match NetState::retrieve() { + Ok(mut state) => { +- if let Some(ifname) = matches.value_of("ifname") { +- if let Some(iface) = state.ifaces.remove(ifname) { +- CliResult::Ifaces(vec![iface]) ++ if let Some(m) = matches.subcommand_matches("iface") { ++ output_format = parse_arg_output_format(m); ++ if let Some(ifname) = m.value_of("ifname") { ++ if let Some(iface) = state.ifaces.remove(ifname) { ++ CliResult::Ifaces(vec![iface]) ++ } else { ++ CliResult::CliError(CliError { ++ msg: format!( ++ "Interface '{}' not found", ++ ifname ++ ), ++ }) ++ } + } else { +- CliResult::CliError(CliError { +- msg: format!("Interface '{}' not found", ifname), +- }) ++ CliResult::Full(state) + } + } else if let Some(m) = matches.subcommand_matches("route") { + output_format = parse_arg_output_format(m); +@@ -176,9 +376,17 @@ fn main() { + } else if let Some(m) = matches.subcommand_matches("rule") { + output_format = parse_arg_output_format(m); + CliResult::RouteRules(state.rules) ++ } else if let Some(ifname) = matches.value_of("ifname") { ++ if let Some(iface) = state.ifaces.remove(ifname) { ++ CliResult::Ifaces(vec![iface]) ++ } else { ++ CliResult::CliError(CliError { ++ msg: format!("Interface '{}' not found", ifname), ++ }) ++ } + } else { + /* Show everything if no cmdline arg has been supplied */ +- CliResult::Full(state) ++ CliResult::Brief(CliIfaceBrief::from_net_state(&state)) + } + } + Err(e) => CliResult::NisporError(e), +diff --git a/src/lib/ifaces/iface.rs b/src/lib/ifaces/iface.rs +index ee71b0f..60afacd 100644 +--- a/src/lib/ifaces/iface.rs ++++ b/src/lib/ifaces/iface.rs +@@ -102,6 +102,23 @@ impl Default for IfaceState { + } + } + ++impl std::fmt::Display for IfaceState { ++ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { ++ write!( ++ f, ++ "{}", ++ match self { ++ Self::Up => "up", ++ Self::Dormant => "dormant", ++ Self::Down => "down", ++ Self::LowerLayerDown => "lower_layer_down", ++ Self::Other(s) => s.as_str(), ++ Self::Unknown => "unknown", ++ } ++ ) ++ } ++} ++ + #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] + #[serde(rename_all = "snake_case")] + pub enum IfaceFlags { +-- +2.32.0 + diff --git a/SOURCES/0002-ethtool-feature-tx-lockless-is-not-changeable.patch b/SOURCES/0002-ethtool-feature-tx-lockless-is-not-changeable.patch new file mode 100644 index 0000000..f86da69 --- /dev/null +++ b/SOURCES/0002-ethtool-feature-tx-lockless-is-not-changeable.patch @@ -0,0 +1,152 @@ +From 235458b72fe54feb97349db4b139babc80821e58 Mon Sep 17 00:00:00 2001 +From: Gris Ge +Date: Fri, 4 Jun 2021 06:23:47 +0800 +Subject: [PATCH 2/3] ethtool feature: tx-lockless is not changeable + +Currently, the `tx-lockless` is shown as changeable, but it actually is +hard coded as not changeable by `NETIF_F_NEVER_CHANGE` in kernel. + +The kernel netlink use `ETHTOOL_A_FEATURES_NOCHANGE` for hard coded +non-changeable features, it take priority over `ETHTOOL_A_FEATURES_HW`. + +Integration test case included, but disable in CI as CI has no ethtool +netlink interface in kernel. + +Signed-off-by: Gris Ge +--- + src/lib/ifaces/ethtool.rs | 19 +++++++++- + src/lib/tests/dummy.rs | 74 +++++++++++++++++++++++++++++++++++++++ + 2 files changed, 92 insertions(+), 1 deletion(-) + +diff --git a/src/lib/ifaces/ethtool.rs b/src/lib/ifaces/ethtool.rs +index 7a5c08a..0697ad5 100644 +--- a/src/lib/ifaces/ethtool.rs ++++ b/src/lib/ifaces/ethtool.rs +@@ -313,6 +313,14 @@ async fn dump_feature_infos( + let mut fixed_features = HashMap::new(); + let mut changeable_features = HashMap::new(); + ++ for nla in &nlas { ++ if let FeatureAttr::NoChange(feature_bits) = nla { ++ for FeatureBit { name, .. } in feature_bits { ++ fixed_features.insert(name.to_string(), false); ++ } ++ } ++ } ++ + for nla in nlas { + if let FeatureAttr::Header(hdrs) = nla { + iface_name = get_iface_name_from_header(&hdrs); +@@ -324,7 +332,16 @@ async fn dump_feature_infos( + name, + value: true, + } => { +- changeable_features.insert(name, false); ++ // Dummy interface show `tx-lockless` is ++ // changeable, but FeatureAttr::NoChange() says ++ // otherwise. The kernel code ++ // `NETIF_F_NEVER_CHANGE` shows `tx-lockless` ++ // should never been changeable. ++ if fixed_features.contains_key(&name) { ++ fixed_features.insert(name, false); ++ } else { ++ changeable_features.insert(name, false); ++ } + } + FeatureBit { + index: _, +diff --git a/src/lib/tests/dummy.rs b/src/lib/tests/dummy.rs +index a0feed3..aab9ffd 100644 +--- a/src/lib/tests/dummy.rs ++++ b/src/lib/tests/dummy.rs +@@ -40,6 +40,66 @@ const EXPECTED_IFACE_STATE: &str = r#"--- + preferred_lft: forever + mac_address: "00:23:45:67:89:1a""#; + ++const EXPECTED_ETHTOOL_FEATURE: &str = r#"--- ++fixed: ++ esp-hw-offload: false ++ esp-tx-csum-hw-offload: false ++ fcoe-mtu: false ++ hw-tc-offload: false ++ l2-fwd-offload: false ++ loopback: false ++ macsec-hw-offload: false ++ netns-local: false ++ rx-all: false ++ rx-checksum: false ++ rx-fcs: false ++ rx-gro-hw: false ++ rx-hashing: false ++ rx-lro: false ++ rx-ntuple-filter: false ++ rx-udp_tunnel-port-offload: false ++ rx-vlan-filter: false ++ rx-vlan-hw-parse: false ++ rx-vlan-stag-filter: false ++ rx-vlan-stag-hw-parse: false ++ tls-hw-record: false ++ tls-hw-rx-offload: false ++ tls-hw-tx-offload: false ++ tx-checksum-fcoe-crc: false ++ tx-checksum-ipv4: false ++ tx-checksum-ipv6: false ++ tx-checksum-sctp: false ++ tx-esp-segmentation: false ++ tx-fcoe-segmentation: false ++ tx-gso-list: false ++ tx-gso-partial: false ++ tx-gso-robust: false ++ tx-lockless: true ++ tx-sctp-segmentation: false ++ tx-tunnel-remcsum-segmentation: false ++ tx-udp-segmentation: false ++ tx-vlan-hw-insert: false ++ tx-vlan-stag-hw-insert: false ++ vlan-challenged: false ++changeable: ++ highdma: true ++ rx-gro: true ++ rx-gro-list: false ++ tx-checksum-ip-generic: true ++ tx-generic-segmentation: true ++ tx-gre-csum-segmentation: true ++ tx-gre-segmentation: true ++ tx-ipxip4-segmentation: true ++ tx-ipxip6-segmentation: true ++ tx-nocache-copy: false ++ tx-scatter-gather-fraglist: true ++ tx-tcp-ecn-segmentation: true ++ tx-tcp-mangleid-segmentation: true ++ tx-tcp-segmentation: true ++ tx-tcp6-segmentation: true ++ tx-udp_tnl-csum-segmentation: true ++ tx-udp_tnl-segmentation: true"#; ++ + #[test] + fn test_get_iface_dummy_yaml() { + with_dummy_iface(|| { +@@ -54,6 +114,20 @@ fn test_get_iface_dummy_yaml() { + }); + } + ++#[test] ++#[ignore] // CI does not have ethtool_netlink kernel module yet ++fn test_get_iface_dummy_ethtool_feature() { ++ with_dummy_iface(|| { ++ let state = NetState::retrieve().unwrap(); ++ let iface = &state.ifaces[IFACE_NAME]; ++ assert_eq!( ++ serde_yaml::to_string(&iface.ethtool.as_ref().unwrap().features) ++ .unwrap(), ++ EXPECTED_ETHTOOL_FEATURE ++ ); ++ }); ++} ++ + fn with_dummy_iface(test: T) -> () + where + T: FnOnce() -> () + panic::UnwindSafe, +-- +2.32.0 + diff --git a/SOURCES/0003-loopback-Fix-interface-type-of-loopback.patch b/SOURCES/0003-loopback-Fix-interface-type-of-loopback.patch new file mode 100644 index 0000000..e0394a1 --- /dev/null +++ b/SOURCES/0003-loopback-Fix-interface-type-of-loopback.patch @@ -0,0 +1,61 @@ +From 7ef2df7e8849f7f674c14190fb997fc3f0e7ba67 Mon Sep 17 00:00:00 2001 +From: Gris Ge +Date: Tue, 8 Jun 2021 16:59:31 +0800 +Subject: [PATCH 3/3] loopback: Fix interface type of loopback + +Using `IFF_LOOPBACK` interface flag to determine whether a interface is +loopback or not is incorrect as other type interface would also hold +this flag. + +Fixed to use `ARPHRD_LOOPBACK` from link_layer_type. + +Signed-off-by: Gris Ge +--- + src/lib/ifaces/iface.rs | 17 ++++++++--------- + 1 file changed, 8 insertions(+), 9 deletions(-) + +diff --git a/src/lib/ifaces/iface.rs b/src/lib/ifaces/iface.rs +index 60afacd..25ba558 100644 +--- a/src/lib/ifaces/iface.rs ++++ b/src/lib/ifaces/iface.rs +@@ -48,10 +48,10 @@ use crate::NisporError; + use netlink_packet_route::rtnl::link::nlas; + use netlink_packet_route::rtnl::LinkMessage; + use netlink_packet_route::rtnl::{ +- ARPHRD_ETHER, IFF_ALLMULTI, IFF_AUTOMEDIA, IFF_BROADCAST, IFF_DEBUG, +- IFF_DORMANT, IFF_LOOPBACK, IFF_LOWER_UP, IFF_MASTER, IFF_MULTICAST, +- IFF_NOARP, IFF_POINTOPOINT, IFF_PORTSEL, IFF_PROMISC, IFF_RUNNING, +- IFF_SLAVE, IFF_UP, ++ ARPHRD_ETHER, ARPHRD_LOOPBACK, IFF_ALLMULTI, IFF_AUTOMEDIA, IFF_BROADCAST, ++ IFF_DEBUG, IFF_DORMANT, IFF_LOOPBACK, IFF_LOWER_UP, IFF_MASTER, ++ IFF_MULTICAST, IFF_NOARP, IFF_POINTOPOINT, IFF_PORTSEL, IFF_PROMISC, ++ IFF_RUNNING, IFF_SLAVE, IFF_UP, + }; + use rtnetlink::new_connection; + +@@ -247,8 +247,10 @@ pub(crate) fn parse_nl_msg_to_iface( + name: name.clone(), + ..Default::default() + }; +- if nl_msg.header.link_layer_type == ARPHRD_ETHER { +- iface_state.iface_type = IfaceType::Ethernet ++ match nl_msg.header.link_layer_type { ++ ARPHRD_ETHER => iface_state.iface_type = IfaceType::Ethernet, ++ ARPHRD_LOOPBACK => iface_state.iface_type = IfaceType::Loopback, ++ _ => (), + } + iface_state.index = nl_msg.header.index; + let mut link: Option = None; +@@ -392,9 +394,6 @@ pub(crate) fn parse_nl_msg_to_iface( + _ => (), + } + } +- if (nl_msg.header.flags & IFF_LOOPBACK) > 0 { +- iface_state.iface_type = IfaceType::Loopback; +- } + iface_state.flags = _parse_iface_flags(nl_msg.header.flags); + Ok(Some(iface_state)) + } +-- +2.32.0 + diff --git a/SPECS/nispor.spec b/SPECS/nispor.spec index 50c3eef..e4197ab 100644 --- a/SPECS/nispor.spec +++ b/SPECS/nispor.spec @@ -1,11 +1,14 @@ Name: nispor Version: 1.1.0 -Release: 1%{?dist} +Release: 2%{?dist} Summary: API for network status querying License: ASL 2.0 URL: https://github.com/nispor/nispor Source: https://github.com/nispor/nispor/archive/v%{version}.tar.gz#/%{name}-%{version}.tar.gz Source1: %{name}-%{version}-vendor.tar.xz +Patch1: 0001-npc-Show-brief-network-state-when-no-argument.patch +Patch2: 0002-ethtool-feature-tx-lockless-is-not-changeable.patch +Patch3: 0003-loopback-Fix-interface-type-of-loopback.patch BuildRequires: pkg-config BuildRequires: python3-devel BuildRequires: rust-toolset @@ -81,6 +84,9 @@ popd %{_libdir}/pkgconfig/nispor.pc %changelog +* Tue Jun 08 2021 Gris Ge - 1.1.0-2 +- Fix cli output, loopback interface and ethtool features. + * Tue May 25 2021 Wen Liang - 1.1.0-1 - Upgrade to 1.1.0. RHBZ#1942459