From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id 412C91FF0DF for ; Fri, 28 Aug 2026 13:34:59 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id D676A215E3; Fri, 28 Aug 2026 13:34:20 +0200 (CEST) From: Gabriel Goller To: pve-devel@lists.proxmox.com Subject: [PATCH proxmox-perl-rs v3 07/13] sdn: add IS-IS fabric status reporting Date: Fri, 28 Aug 2026 13:32:46 +0200 Message-ID: <20260828113255.176546-8-g.goller@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260828113255.176546-1-g.goller@proxmox.com> References: <20260828113255.176546-1-g.goller@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1787916770836 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.305 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) PROLO_LEO1 0.1 Meta Catches all Leo drug variations so far PROLO_LEO2 0.1 Meta Catches all Leo drug variations so far RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: S24E4BHGESQLGNB3D7BPZ3AHAYEEZ4YP X-Message-ID-Hash: S24E4BHGESQLGNB3D7BPZ3AHAYEEZ4YP X-MailFrom: g.goller@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Extend fabric status reporting to cover the IS-IS fabric alongside the existing OpenFabric and OSPF support. Signed-off-by: Gabriel Goller --- pve-rs/src/bindings/sdn/fabrics.rs | 92 +++ pve-rs/src/sdn/status.rs | 949 +++++++++++++++++++++++++++-- 2 files changed, 982 insertions(+), 59 deletions(-) diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs index 33ed92cfd033..ffc5c7fb580e 100644 --- a/pve-rs/src/bindings/sdn/fabrics.rs +++ b/pve-rs/src/bindings/sdn/fabrics.rs @@ -891,6 +891,36 @@ pub mod pve_rs_sdn_fabrics { proxmox_sys::nodename(), ) } + FabricEntry::Isis(_) => { + let isis_ipv4_routes_string = String::from_utf8( + Command::new("sh") + .args(["-c", "vtysh -c 'show ip route isis json'"]) + .output()? + .stdout, + )?; + + let isis_ipv6_routes_string = String::from_utf8( + Command::new("sh") + .args(["-c", "vtysh -c 'show ipv6 route isis json'"]) + .output()? + .stdout, + )?; + + let mut isis_routes: proxmox_frr::de::Routes = if isis_ipv4_routes_string.is_empty() + { + proxmox_frr::de::Routes::default() + } else { + serde_json::from_str(&isis_ipv4_routes_string) + .with_context(|| "error parsing isis ipv4 routes")? + }; + if !isis_ipv6_routes_string.is_empty() { + let isis_ipv6_routes: proxmox_frr::de::Routes = + serde_json::from_str(&isis_ipv6_routes_string) + .with_context(|| "error parsing isis ipv6 routes")?; + isis_routes.0.extend(isis_ipv6_routes.0); + } + status::get_routes(fabric_id, config, isis_routes, proxmox_sys::nodename()) + } FabricEntry::Ospf(_) => { let ospf_routes_string = String::from_utf8( Command::new("sh") @@ -974,6 +1004,23 @@ pub mod pve_rs_sdn_fabrics { status::get_neighbors_openfabric(fabric_id, openfabric_neighbors).map(|v| v.into()) } + FabricEntry::Isis(_) => { + let isis_neighbors_string = String::from_utf8( + Command::new("sh") + .args(["-c", "vtysh -c 'show isis neighbor detail json'"]) + .output()? + .stdout, + )?; + let isis_neighbors: proxmox_frr::de::isis::Neighbors = + if isis_neighbors_string.is_empty() { + proxmox_frr::de::isis::Neighbors::default() + } else { + serde_json::from_str(&isis_neighbors_string) + .with_context(|| "error parsing isis neighbors")? + }; + + status::get_neighbors_isis(fabric_id, isis_neighbors).map(|v| v.into()) + } FabricEntry::Ospf(fabric) => { let ospf_neighbors_string = String::from_utf8( Command::new("sh") @@ -1052,6 +1099,23 @@ pub mod pve_rs_sdn_fabrics { status::get_interfaces_openfabric(fabric_id, openfabric_interfaces) .map(|v| v.into()) } + FabricEntry::Isis(_) => { + let isis_interface_string = String::from_utf8( + Command::new("sh") + .args(["-c", "vtysh -c 'show isis interface json'"]) + .output()? + .stdout, + )?; + let isis_interfaces: proxmox_frr::de::isis::Interfaces = + if isis_interface_string.is_empty() { + proxmox_frr::de::isis::Interfaces::default() + } else { + serde_json::from_str(&isis_interface_string) + .with_context(|| "error parsing isis interfaces")? + }; + + status::get_interfaces_isis(fabric_id, isis_interfaces).map(|v| v.into()) + } FabricEntry::Ospf(fabric) => { let ospf_interfaces_string = String::from_utf8( Command::new("sh") @@ -1126,6 +1190,20 @@ pub mod pve_rs_sdn_fabrics { .stdout, )?; + let isis_ipv4_routes_string = String::from_utf8( + Command::new("sh") + .args(["-c", "vtysh -c 'show ip route isis json'"]) + .output()? + .stdout, + )?; + + let isis_ipv6_routes_string = String::from_utf8( + Command::new("sh") + .args(["-c", "vtysh -c 'show ipv6 route isis json'"]) + .output()? + .stdout, + )?; + let ospf_routes_string = String::from_utf8( Command::new("sh") .args(["-c", "vtysh -c 'show ip route ospf json'"]) @@ -1147,6 +1225,19 @@ pub mod pve_rs_sdn_fabrics { openfabric_routes.0.extend(openfabric_ipv6_routes.0); } + let mut isis_routes: proxmox_frr::de::Routes = if isis_ipv4_routes_string.is_empty() { + proxmox_frr::de::Routes::default() + } else { + serde_json::from_str(&isis_ipv4_routes_string) + .with_context(|| "error parsing isis ipv4 routes")? + }; + if !isis_ipv6_routes_string.is_empty() { + let isis_ipv6_routes: proxmox_frr::de::Routes = + serde_json::from_str(&isis_ipv6_routes_string) + .with_context(|| "error parsing isis ipv6 routes")?; + isis_routes.0.extend(isis_ipv6_routes.0); + } + let ospf_routes: proxmox_frr::de::Routes = if ospf_routes_string.is_empty() { proxmox_frr::de::Routes::default() } else { @@ -1187,6 +1278,7 @@ pub mod pve_rs_sdn_fabrics { openfabric: openfabric_routes, ospf: ospf_routes, bgp: bgp_routes, + isis: isis_routes, }; status::get_status(config, route_status, proxmox_sys::nodename()) diff --git a/pve-rs/src/sdn/status.rs b/pve-rs/src/sdn/status.rs index 7a1334d20804..b43bc953a87d 100644 --- a/pve-rs/src/sdn/status.rs +++ b/pve-rs/src/sdn/status.rs @@ -81,6 +81,33 @@ mod openfabric { } } +mod isis { + use proxmox_frr::de; + use serde::Serialize; + + /// The status of a neighbor. + /// + /// Contains the neighbor name and the neighbor status. + #[derive(Debug, Serialize, PartialEq, Eq)] + pub struct NeighborStatus { + pub neighbor: String, + pub status: de::isis::AdjacencyState, + pub uptime: String, + } + + /// The status of a fabric interface + /// + /// Contains the interface name, the interface state (so if the interface is up/down) and the type + /// of the interface (e.g. point-to-point, broadcast, etc.). + #[derive(Debug, Serialize, PartialEq, Eq)] + pub struct InterfaceStatus { + pub name: String, + pub state: de::isis::CircuitState, + #[serde(rename = "type")] + pub ty: de::isis::NetworkType, + } +} + mod wireguard { use serde::Serialize; @@ -117,6 +144,7 @@ pub enum NeighborStatus { Ospf(Vec), WireGuard(Vec), Bgp(Vec), + Isis(Vec), } impl From> for NeighborStatus { @@ -134,6 +162,11 @@ impl From> for NeighborStatus { NeighborStatus::Bgp(value) } } +impl From> for NeighborStatus { + fn from(value: Vec) -> Self { + NeighborStatus::Isis(value) + } +} /// Common InterfaceStatus that contains either OSPF, Openfabric, or BGP interfaces #[derive(Debug, Serialize)] @@ -143,6 +176,7 @@ pub enum InterfaceStatus { Ospf(Vec), WireGuard(Vec), Bgp(Vec), + Isis(Vec), } impl From> for InterfaceStatus { @@ -160,6 +194,11 @@ impl From> for InterfaceStatus { InterfaceStatus::Bgp(value) } } +impl From> for InterfaceStatus { + fn from(value: Vec) -> Self { + InterfaceStatus::Isis(value) + } +} /// The status of a route. /// @@ -182,6 +221,8 @@ pub enum Protocol { WireGuard, /// BGP Bgp, + /// IS-IS + Isis, } /// The status of a fabric. @@ -222,6 +263,8 @@ pub struct RoutesParsed { pub ospf: de::Routes, /// All bgp routes in FRR pub bgp: de::Routes, + /// All isis routes in FRR + pub isis: de::Routes, } /// Config used to parse the fabric part of the running-config @@ -271,6 +314,11 @@ pub fn get_routes( BgpNode::Internal(props) => props.interfaces().map(|i| i.name().as_str()).collect(), BgpNode::External(_) => HashSet::new(), }, + ConfigNode::Isis(n) => n + .properties() + .interfaces() + .map(|i| i.name().as_str()) + .collect(), }; let dummy_interface = format!("dummy_{}", fabric_id.as_str()); @@ -355,6 +403,38 @@ pub fn get_neighbors_openfabric( Ok(stats) } +/// Convert the parsed isis neighbor neighbor information into a list of +/// [`isis::NeighborStatus`]. +/// +/// ISIS uses the name of the fabric as an "area", so simply match that to the fabric_id. +pub fn get_neighbors_isis( + fabric_id: FabricId, + neighbors: de::isis::Neighbors, +) -> Result, anyhow::Error> { + let mut stats: Vec = Vec::new(); + + for area in &neighbors.areas { + if area.area != fabric_id.as_str() { + continue; + } + for circuit in &area.circuits { + let (Some(adj), Some(interface)) = (&circuit.adj, &circuit.interface) else { + continue; + }; + let Some(state) = interface.state else { + continue; + }; + stats.push(isis::NeighborStatus { + neighbor: adj.clone(), + status: state, + uptime: interface.last_ago.clone(), + }); + } + } + + Ok(stats) +} + /// Convert the parsed ospf neighbor neighbor information into a list of [`ospf::NeighborStatus`]. /// /// Ospf does not use the name of the fabric at all, so we again need to retrieve the interfaces of @@ -421,6 +501,30 @@ pub fn get_interfaces_openfabric( Ok(stats) } +/// Conver the `show isis interface` output into a list of [`isis::InterfaceStatus`]. +/// +/// isis uses the name of the fabric as an "area", so simply match that to the fabric_id. +pub fn get_interfaces_isis( + fabric_id: FabricId, + interfaces: de::isis::Interfaces, +) -> Result, anyhow::Error> { + let mut stats: Vec = Vec::new(); + + for area in &interfaces.areas { + if area.area == fabric_id.as_str() { + for circuit in &area.circuits { + stats.push(isis::InterfaceStatus { + name: circuit.interface.name.clone(), + state: circuit.interface.state, + ty: circuit.interface.ty, + }); + } + } + } + + Ok(stats) +} + /// Convert the `show ip ospf interface` output into a list of [`ospf::InterfaceStatus`]. /// /// Ospf does not use the name of the fabric at all, so we again need to retrieve the interfaces of @@ -541,6 +645,7 @@ pub fn get_status( ConfigNode::Ospf(_) => (Protocol::Ospf, &routes.ospf.0), ConfigNode::WireGuard(_) => (Protocol::WireGuard, &BTreeMap::new()), ConfigNode::Bgp(_) => (Protocol::Bgp, &routes.bgp.0), + ConfigNode::Isis(_) => (Protocol::Isis, &routes.isis.0), }; // get interfaces @@ -560,6 +665,11 @@ pub fn get_status( BgpNode::Internal(props) => props.interfaces().map(|i| i.name().as_str()).collect(), BgpNode::External(_) => HashSet::new(), }, + ConfigNode::Isis(n) => n + .properties() + .interfaces() + .map(|i| i.name().as_str()) + .collect(), }; // determine status by checking if any routes exist for our interfaces @@ -580,6 +690,7 @@ pub fn get_status( Protocol::Ospf if has_routes => FabricStatus::Ok, Protocol::Bgp if has_routes => FabricStatus::Ok, Protocol::WireGuard => FabricStatus::Ok, + Protocol::Isis if has_routes => FabricStatus::Ok, _ => FabricStatus::NotOk, }; @@ -825,7 +936,530 @@ mod tests { .expect("error converting section config to fabricconfig") } - mod openfabric { + mod openfabric { + use super::super::*; + + #[test] + fn neighbors() { + let json_output = r#" + { + "areas":[ + { + "area":"test", + "circuits":[ + { + "circuit":0 + }, + { + "circuit":0, + "adj":"node2", + "interface":{ + "name":"ens19", + "state":"Up", + "adj-flaps":1, + "last-ago":"11m5s", + "circuit-type":"L2", + "speaks":"IPv4", + "snpa":"2020.2020.2020", + "area-address":{ + "isonet":"49.0001" + }, + "ipv4-address":{ + "ipv4":"172.16.6.2" + }, + "adj-sid":{} + }, + "level":2, + "expires-in":"29s" + } + ] + } + ] + } + "#; + + let neighbors: de::openfabric::Neighbors = if json_output.is_empty() { + de::openfabric::Neighbors::default() + } else { + serde_json::from_str(json_output).expect("error parsing json output") + }; + + let output = get_neighbors_openfabric( + FabricId::from_string("test".to_owned()).expect("error parsing fabricId"), + neighbors, + ) + .expect("error converting vtysh output"); + + let reference = vec![openfabric::NeighborStatus { + neighbor: "node2".to_owned(), + status: de::openfabric::AdjacencyState::Up, + uptime: "11m5s".to_owned(), + }]; + assert_eq!(reference, output); + } + + #[test] + fn multiple_neighbors() { + let json_output = r#" + { + "areas":[ + { + "area":"test", + "circuits":[ + { + "circuit":0 + }, + { + "circuit":0, + "adj":"node1", + "interface":{ + "name":"ens19", + "state":"Up", + "adj-flaps":1, + "last-ago":"25m26s", + "circuit-type":"L2", + "speaks":"IPv4", + "snpa":"2020.2020.2020", + "area-address":{ + "isonet":"49.0001" + }, + "ipv4-address":{ + "ipv4":"172.16.6.1" + }, + "adj-sid":{} + }, + "level":2, + "expires-in":"28s" + }, + { + "circuit":0, + "adj":"node3", + "interface":{ + "name":"ens20", + "state":"Up", + "adj-flaps":1, + "last-ago":"25m21s", + "circuit-type":"L2", + "speaks":"IPv4", + "snpa":"2020.2020.2020", + "area-address":{ + "isonet":"49.0001" + }, + "ipv4-address":{ + "ipv4":"172.16.6.3" + }, + "adj-sid":{} + }, + "level":2, + "expires-in":"29s" + } + ] + } + ] + } + "#; + + let neighbors: de::openfabric::Neighbors = if json_output.is_empty() { + de::openfabric::Neighbors::default() + } else { + serde_json::from_str(json_output).expect("error parsing json output") + }; + + let output = get_neighbors_openfabric( + FabricId::from_string("test".to_owned()).expect("error parsing fabricId"), + neighbors, + ) + .expect("error converting vtysh output"); + + let reference = vec![ + openfabric::NeighborStatus { + neighbor: "node1".to_owned(), + status: de::openfabric::AdjacencyState::Up, + uptime: "25m26s".to_owned(), + }, + openfabric::NeighborStatus { + neighbor: "node3".to_owned(), + status: de::openfabric::AdjacencyState::Up, + uptime: "25m21s".to_owned(), + }, + ]; + assert_eq!(reference, output); + } + + #[test] + fn multiple_neighbors_multiple_areas() { + let json_output = r#" + { + "areas":[ + { + "area":"test", + "circuits":[ + { + "circuit":0 + }, + { + "circuit":0, + "adj":"node1", + "interface":{ + "name":"ens19", + "state":"Up", + "adj-flaps":1, + "last-ago":"33m39s", + "circuit-type":"L2", + "speaks":"IPv4", + "snpa":"2020.2020.2020", + "area-address":{ + "isonet":"49.0001" + }, + "ipv4-address":{ + "ipv4":"172.16.6.1" + }, + "adj-sid":{} + }, + "level":2, + "expires-in":"29s" + }, + { + "circuit":0, + "adj":"node3", + "interface":{ + "name":"ens20", + "state":"Up", + "adj-flaps":1, + "last-ago":"33m34s", + "circuit-type":"L2", + "speaks":"IPv4", + "snpa":"2020.2020.2020", + "area-address":{ + "isonet":"49.0001" + }, + "ipv4-address":{ + "ipv4":"172.16.6.3" + }, + "adj-sid":{} + }, + "level":2, + "expires-in":"29s" + } + ] + }, + { + "area":"test1", + "circuits":[ + { + "circuit":0 + }, + { + "circuit":0, + "adj":"node1", + "interface":{ + "name":"ens21", + "state":"Up", + "adj-flaps":1, + "last-ago":"56s", + "circuit-type":"L2", + "speaks":"IPv4", + "snpa":"2020.2020.2020", + "area-address":{ + "isonet":"49.0001" + }, + "ipv4-address":{ + "ipv4":"172.16.7.1" + }, + "adj-sid":{} + }, + "level":2, + "expires-in":"28s" + }, + { + "circuit":0, + "adj":"node3", + "interface":{ + "name":"ens22", + "state":"Up", + "adj-flaps":1, + "last-ago":"1m2s", + "circuit-type":"L2", + "speaks":"IPv4", + "snpa":"2020.2020.2020", + "area-address":{ + "isonet":"49.0001" + }, + "ipv4-address":{ + "ipv4":"172.16.7.3" + }, + "adj-sid":{} + }, + "level":2, + "expires-in":"28s" + } + ] + } + ] + } + "#; + + let neighbors: de::openfabric::Neighbors = if json_output.is_empty() { + de::openfabric::Neighbors::default() + } else { + serde_json::from_str(json_output).expect("error parsing json output") + }; + + let output_node1 = get_neighbors_openfabric( + FabricId::from_string("test".to_owned()).expect("error parsing fabricId"), + neighbors.clone(), + ) + .expect("error converting vtysh output"); + + let reference_node1 = vec![ + openfabric::NeighborStatus { + neighbor: "node1".to_owned(), + status: de::openfabric::AdjacencyState::Up, + uptime: "33m39s".to_owned(), + }, + openfabric::NeighborStatus { + neighbor: "node3".to_owned(), + status: de::openfabric::AdjacencyState::Up, + uptime: "33m34s".to_owned(), + }, + ]; + assert_eq!(reference_node1, output_node1); + + let output_node2 = get_neighbors_openfabric( + FabricId::from_string("test1".to_owned()).expect("error parsing fabricId"), + neighbors, + ) + .expect("error converting vtysh output"); + + let reference_node2 = vec![ + openfabric::NeighborStatus { + neighbor: "node1".to_owned(), + status: de::openfabric::AdjacencyState::Up, + uptime: "56s".to_owned(), + }, + openfabric::NeighborStatus { + neighbor: "node3".to_owned(), + status: de::openfabric::AdjacencyState::Up, + uptime: "1m2s".to_owned(), + }, + ]; + assert_eq!(reference_node2, output_node2); + } + + #[test] + fn interfaces() { + let json_output = r#" + { + "areas":[ + { + "area":"test1", + "circuits":[ + { + "circuit":0, + "interface":{ + "name":"dummy_test1", + "circuit-id":"0x0", + "state":"Up", + "type":"loopback", + "level":"L2" + } + }, + { + "circuit":0, + "interface":{ + "name":"ens21", + "circuit-id":"0x0", + "state":"Up", + "type":"p2p", + "level":"L2" + } + }, + { + "circuit":0, + "interface":{ + "name":"ens22", + "circuit-id":"0x0", + "state":"Up", + "type":"p2p", + "level":"L2" + } + } + ] + } + ] + } + "#; + + let interfaces: de::openfabric::Interfaces = if json_output.is_empty() { + de::openfabric::Interfaces::default() + } else { + serde_json::from_str(json_output).expect("error parsing json output") + }; + + let output = get_interfaces_openfabric( + FabricId::from_string("test1".to_owned()).expect("error parsing fabricId"), + interfaces, + ) + .expect("error converting vtysh output"); + + let reference = vec![ + openfabric::InterfaceStatus { + name: "dummy_test1".to_owned(), + state: de::openfabric::CircuitState::Up, + ty: de::openfabric::NetworkType::Loopback, + }, + openfabric::InterfaceStatus { + name: "ens21".to_owned(), + state: de::openfabric::CircuitState::Up, + ty: de::openfabric::NetworkType::PointToPoint, + }, + openfabric::InterfaceStatus { + name: "ens22".to_owned(), + state: de::openfabric::CircuitState::Up, + ty: de::openfabric::NetworkType::PointToPoint, + }, + ]; + assert_eq!(reference, output); + } + + #[test] + fn interfaces_multiple_areas() { + let json_output = r#" + { + "areas":[ + { + "area":"test", + "circuits":[ + { + "circuit":0, + "interface":{ + "name":"dummy_test", + "circuit-id":"0x0", + "state":"Up", + "type":"loopback", + "level":"L2" + } + }, + { + "circuit":0, + "interface":{ + "name":"ens19", + "circuit-id":"0x0", + "state":"Up", + "type":"p2p", + "level":"L2" + } + }, + { + "circuit":0, + "interface":{ + "name":"ens20", + "circuit-id":"0x0", + "state":"Up", + "type":"p2p", + "level":"L2" + } + } + ] + }, + { + "area":"test1", + "circuits":[ + { + "circuit":0, + "interface":{ + "name":"dummy_test1", + "circuit-id":"0x0", + "state":"Up", + "type":"loopback", + "level":"L2" + } + }, + { + "circuit":0, + "interface":{ + "name":"ens21", + "circuit-id":"0x0", + "state":"Up", + "type":"p2p", + "level":"L2" + } + }, + { + "circuit":0, + "interface":{ + "name":"ens22", + "circuit-id":"0x0", + "state":"Up", + "type":"p2p", + "level":"L2" + } + } + ] + } + ] + } + "#; + + let interfaces: de::openfabric::Interfaces = if json_output.is_empty() { + de::openfabric::Interfaces::default() + } else { + serde_json::from_str(json_output).expect("error parsing json output") + }; + + let output_fabric1 = get_interfaces_openfabric( + FabricId::from_string("test".to_owned()).expect("error parsing fabricId"), + interfaces.clone(), + ) + .expect("error converting vtysh output"); + + let reference_fabric1 = vec![ + openfabric::InterfaceStatus { + name: "dummy_test".to_owned(), + state: de::openfabric::CircuitState::Up, + ty: de::openfabric::NetworkType::Loopback, + }, + openfabric::InterfaceStatus { + name: "ens19".to_owned(), + state: de::openfabric::CircuitState::Up, + ty: de::openfabric::NetworkType::PointToPoint, + }, + openfabric::InterfaceStatus { + name: "ens20".to_owned(), + state: de::openfabric::CircuitState::Up, + ty: de::openfabric::NetworkType::PointToPoint, + }, + ]; + assert_eq!(reference_fabric1, output_fabric1); + + let output_fabric2 = get_interfaces_openfabric( + FabricId::from_string("test1".to_owned()).expect("error parsing fabricId"), + interfaces, + ) + .expect("error converting vtysh output"); + + let reference_fabric2 = vec![ + openfabric::InterfaceStatus { + name: "dummy_test1".to_owned(), + state: de::openfabric::CircuitState::Up, + ty: de::openfabric::NetworkType::Loopback, + }, + openfabric::InterfaceStatus { + name: "ens21".to_owned(), + state: de::openfabric::CircuitState::Up, + ty: de::openfabric::NetworkType::PointToPoint, + }, + openfabric::InterfaceStatus { + name: "ens22".to_owned(), + state: de::openfabric::CircuitState::Up, + ty: de::openfabric::NetworkType::PointToPoint, + }, + ]; + assert_eq!(reference_fabric2, output_fabric2); + } + } + + mod isis { use super::super::*; #[test] @@ -867,21 +1501,21 @@ mod tests { } "#; - let neighbors: de::openfabric::Neighbors = if json_output.is_empty() { - de::openfabric::Neighbors::default() + let neighbors: de::isis::Neighbors = if json_output.is_empty() { + de::isis::Neighbors::default() } else { serde_json::from_str(json_output).expect("error parsing json output") }; - let output = get_neighbors_openfabric( + let output = get_neighbors_isis( FabricId::from_string("test".to_owned()).expect("error parsing fabricId"), neighbors, ) .expect("error converting vtysh output"); - let reference = vec![openfabric::NeighborStatus { + let reference = vec![isis::NeighborStatus { neighbor: "node2".to_owned(), - status: de::openfabric::AdjacencyState::Up, + status: de::isis::AdjacencyState::Up, uptime: "11m5s".to_owned(), }]; assert_eq!(reference, output); @@ -948,27 +1582,27 @@ mod tests { } "#; - let neighbors: de::openfabric::Neighbors = if json_output.is_empty() { - de::openfabric::Neighbors::default() + let neighbors: de::isis::Neighbors = if json_output.is_empty() { + de::isis::Neighbors::default() } else { serde_json::from_str(json_output).expect("error parsing json output") }; - let output = get_neighbors_openfabric( + let output = get_neighbors_isis( FabricId::from_string("test".to_owned()).expect("error parsing fabricId"), neighbors, ) .expect("error converting vtysh output"); let reference = vec![ - openfabric::NeighborStatus { + isis::NeighborStatus { neighbor: "node1".to_owned(), - status: de::openfabric::AdjacencyState::Up, + status: de::isis::AdjacencyState::Up, uptime: "25m26s".to_owned(), }, - openfabric::NeighborStatus { + isis::NeighborStatus { neighbor: "node3".to_owned(), - status: de::openfabric::AdjacencyState::Up, + status: de::isis::AdjacencyState::Up, uptime: "25m21s".to_owned(), }, ]; @@ -1088,47 +1722,47 @@ mod tests { } "#; - let neighbors: de::openfabric::Neighbors = if json_output.is_empty() { - de::openfabric::Neighbors::default() + let neighbors: de::isis::Neighbors = if json_output.is_empty() { + de::isis::Neighbors::default() } else { serde_json::from_str(json_output).expect("error parsing json output") }; - let output_node1 = get_neighbors_openfabric( + let output_node1 = get_neighbors_isis( FabricId::from_string("test".to_owned()).expect("error parsing fabricId"), neighbors.clone(), ) .expect("error converting vtysh output"); let reference_node1 = vec![ - openfabric::NeighborStatus { + isis::NeighborStatus { neighbor: "node1".to_owned(), - status: de::openfabric::AdjacencyState::Up, + status: de::isis::AdjacencyState::Up, uptime: "33m39s".to_owned(), }, - openfabric::NeighborStatus { + isis::NeighborStatus { neighbor: "node3".to_owned(), - status: de::openfabric::AdjacencyState::Up, + status: de::isis::AdjacencyState::Up, uptime: "33m34s".to_owned(), }, ]; assert_eq!(reference_node1, output_node1); - let output_node2 = get_neighbors_openfabric( + let output_node2 = get_neighbors_isis( FabricId::from_string("test1".to_owned()).expect("error parsing fabricId"), neighbors, ) .expect("error converting vtysh output"); let reference_node2 = vec![ - openfabric::NeighborStatus { + isis::NeighborStatus { neighbor: "node1".to_owned(), - status: de::openfabric::AdjacencyState::Up, + status: de::isis::AdjacencyState::Up, uptime: "56s".to_owned(), }, - openfabric::NeighborStatus { + isis::NeighborStatus { neighbor: "node3".to_owned(), - status: de::openfabric::AdjacencyState::Up, + status: de::isis::AdjacencyState::Up, uptime: "1m2s".to_owned(), }, ]; @@ -1179,33 +1813,33 @@ mod tests { } "#; - let interfaces: de::openfabric::Interfaces = if json_output.is_empty() { - de::openfabric::Interfaces::default() + let interfaces: de::isis::Interfaces = if json_output.is_empty() { + de::isis::Interfaces::default() } else { serde_json::from_str(json_output).expect("error parsing json output") }; - let output = get_interfaces_openfabric( + let output = get_interfaces_isis( FabricId::from_string("test1".to_owned()).expect("error parsing fabricId"), interfaces, ) .expect("error converting vtysh output"); let reference = vec![ - openfabric::InterfaceStatus { + isis::InterfaceStatus { name: "dummy_test1".to_owned(), - state: de::openfabric::CircuitState::Up, - ty: de::openfabric::NetworkType::Loopback, + state: de::isis::CircuitState::Up, + ty: de::isis::NetworkType::Loopback, }, - openfabric::InterfaceStatus { + isis::InterfaceStatus { name: "ens21".to_owned(), - state: de::openfabric::CircuitState::Up, - ty: de::openfabric::NetworkType::PointToPoint, + state: de::isis::CircuitState::Up, + ty: de::isis::NetworkType::PointToPoint, }, - openfabric::InterfaceStatus { + isis::InterfaceStatus { name: "ens22".to_owned(), - state: de::openfabric::CircuitState::Up, - ty: de::openfabric::NetworkType::PointToPoint, + state: de::isis::CircuitState::Up, + ty: de::isis::NetworkType::PointToPoint, }, ]; assert_eq!(reference, output); @@ -1290,58 +1924,58 @@ mod tests { } "#; - let interfaces: de::openfabric::Interfaces = if json_output.is_empty() { - de::openfabric::Interfaces::default() + let interfaces: de::isis::Interfaces = if json_output.is_empty() { + de::isis::Interfaces::default() } else { serde_json::from_str(json_output).expect("error parsing json output") }; - let output_fabric1 = get_interfaces_openfabric( + let output_fabric1 = get_interfaces_isis( FabricId::from_string("test".to_owned()).expect("error parsing fabricId"), interfaces.clone(), ) .expect("error converting vtysh output"); let reference_fabric1 = vec![ - openfabric::InterfaceStatus { + isis::InterfaceStatus { name: "dummy_test".to_owned(), - state: de::openfabric::CircuitState::Up, - ty: de::openfabric::NetworkType::Loopback, + state: de::isis::CircuitState::Up, + ty: de::isis::NetworkType::Loopback, }, - openfabric::InterfaceStatus { + isis::InterfaceStatus { name: "ens19".to_owned(), - state: de::openfabric::CircuitState::Up, - ty: de::openfabric::NetworkType::PointToPoint, + state: de::isis::CircuitState::Up, + ty: de::isis::NetworkType::PointToPoint, }, - openfabric::InterfaceStatus { + isis::InterfaceStatus { name: "ens20".to_owned(), - state: de::openfabric::CircuitState::Up, - ty: de::openfabric::NetworkType::PointToPoint, + state: de::isis::CircuitState::Up, + ty: de::isis::NetworkType::PointToPoint, }, ]; assert_eq!(reference_fabric1, output_fabric1); - let output_fabric2 = get_interfaces_openfabric( + let output_fabric2 = get_interfaces_isis( FabricId::from_string("test1".to_owned()).expect("error parsing fabricId"), interfaces, ) .expect("error converting vtysh output"); let reference_fabric2 = vec![ - openfabric::InterfaceStatus { + isis::InterfaceStatus { name: "dummy_test1".to_owned(), - state: de::openfabric::CircuitState::Up, - ty: de::openfabric::NetworkType::Loopback, + state: de::isis::CircuitState::Up, + ty: de::isis::NetworkType::Loopback, }, - openfabric::InterfaceStatus { + isis::InterfaceStatus { name: "ens21".to_owned(), - state: de::openfabric::CircuitState::Up, - ty: de::openfabric::NetworkType::PointToPoint, + state: de::isis::CircuitState::Up, + ty: de::isis::NetworkType::PointToPoint, }, - openfabric::InterfaceStatus { + isis::InterfaceStatus { name: "ens22".to_owned(), - state: de::openfabric::CircuitState::Up, - ty: de::openfabric::NetworkType::PointToPoint, + state: de::isis::CircuitState::Up, + ty: de::isis::NetworkType::PointToPoint, }, ]; assert_eq!(reference_fabric2, output_fabric2); @@ -2243,6 +2877,203 @@ mod tests { assert_eq!(reference_fabric2, output_fabric2); } + #[test] + fn routes_isis() { + let json_output = r#" + { + "172.16.6.1/32": [ + { + "prefix": "172.16.6.1/32", + "prefixLen": 32, + "protocol": "isis", + "vrfId": 0, + "vrfName": "default", + "selected": true, + "destSelected": true, + "distance": 115, + "metric": 20, + "installed": true, + "table": 254, + "internalStatus": 16, + "internalFlags": 8, + "internalNextHopNum": 1, + "internalNextHopActiveNum": 1, + "nexthopGroupId": 74, + "installedNexthopGroupId": 74, + "uptime": "00:00:32", + "nexthops": [ + { + "flags": 11, + "fib": true, + "ip": "172.16.6.1", + "afi": "ipv4", + "interfaceIndex": 3, + "interfaceName": "ens19", + "active": true, + "onLink": true, + "rmapSource": "172.16.6.2", + "weight": 1 + } + ] + } + ], + "172.16.6.3/32": [ + { + "prefix": "172.16.6.3/32", + "prefixLen": 32, + "protocol": "isis", + "vrfId": 0, + "vrfName": "default", + "selected": true, + "destSelected": true, + "distance": 115, + "metric": 20, + "installed": true, + "table": 254, + "internalStatus": 16, + "internalFlags": 8, + "internalNextHopNum": 1, + "internalNextHopActiveNum": 1, + "nexthopGroupId": 75, + "installedNexthopGroupId": 75, + "uptime": "00:00:32", + "nexthops": [ + { + "flags": 11, + "fib": true, + "ip": "172.16.6.3", + "afi": "ipv4", + "interfaceIndex": 4, + "interfaceName": "ens20", + "active": true, + "onLink": true, + "rmapSource": "172.16.6.2", + "weight": 1 + } + ] + } + ], + "172.16.7.1/32": [ + { + "prefix": "172.16.7.1/32", + "prefixLen": 32, + "protocol": "isis", + "vrfId": 0, + "vrfName": "default", + "selected": true, + "destSelected": true, + "distance": 115, + "metric": 20, + "installed": true, + "table": 254, + "internalStatus": 16, + "internalFlags": 8, + "internalNextHopNum": 1, + "internalNextHopActiveNum": 1, + "nexthopGroupId": 76, + "installedNexthopGroupId": 76, + "uptime": "00:00:32", + "nexthops": [ + { + "flags": 11, + "fib": true, + "ip": "172.16.7.1", + "afi": "ipv4", + "interfaceIndex": 5, + "interfaceName": "ens21", + "active": true, + "onLink": true, + "rmapSource": "172.16.7.2", + "weight": 1 + } + ] + } + ], + "172.16.7.3/32": [ + { + "prefix": "172.16.7.3/32", + "prefixLen": 32, + "protocol": "isis", + "vrfId": 0, + "vrfName": "default", + "selected": true, + "destSelected": true, + "distance": 115, + "metric": 20, + "installed": true, + "table": 254, + "internalStatus": 16, + "internalFlags": 8, + "internalNextHopNum": 1, + "internalNextHopActiveNum": 1, + "nexthopGroupId": 77, + "installedNexthopGroupId": 77, + "uptime": "00:00:32", + "nexthops": [ + { + "flags": 11, + "fib": true, + "ip": "172.16.7.3", + "afi": "ipv4", + "interfaceIndex": 6, + "interfaceName": "ens22", + "active": true, + "onLink": true, + "rmapSource": "172.16.7.2", + "weight": 1 + } + ] + } + ] + } + "#; + + let routes: de::Routes = if json_output.is_empty() { + de::Routes::default() + } else { + serde_json::from_str(json_output).expect("error parsing json output") + }; + + let fabric_config = sample_two_fabric_config(); + + let fabric_id = + FabricId::from_string("test".to_owned()).expect("error parsing fabricId"); + + let output_fabric1 = + get_routes(fabric_id, fabric_config.clone(), routes.clone(), "node2") + .expect("error converting vtysh output"); + + let reference_fabric1 = vec![ + RouteStatus { + route: "172.16.6.1/32".to_owned(), + via: vec!["172.16.6.1".to_owned()], + }, + RouteStatus { + route: "172.16.6.3/32".to_owned(), + via: vec!["172.16.6.3".to_owned()], + }, + ]; + assert_eq!(reference_fabric1, output_fabric1); + + let fabric_id = + FabricId::from_string("test1".to_owned()).expect("error parsing fabricId"); + + let output_fabric2 = get_routes(fabric_id, fabric_config, routes, "node2") + .expect("error converting vtysh output"); + + let reference_fabric2 = vec![ + RouteStatus { + route: "172.16.7.1/32".to_owned(), + via: vec!["172.16.7.1".to_owned()], + }, + RouteStatus { + route: "172.16.7.3/32".to_owned(), + via: vec!["172.16.7.3".to_owned()], + }, + ]; + assert_eq!(reference_fabric2, output_fabric2); + } + #[test] fn routes_openfabric() { let json_output = r#" -- 2.47.3