From: Gabriel Goller <g.goller@proxmox.com>
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 [thread overview]
Message-ID: <20260828113255.176546-8-g.goller@proxmox.com> (raw)
In-Reply-To: <20260828113255.176546-1-g.goller@proxmox.com>
Extend fabric status reporting to cover the IS-IS fabric alongside the
existing OpenFabric and OSPF support.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
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<ospf::NeighborStatus>),
WireGuard(Vec<wireguard::NeighborStatus>),
Bgp(Vec<bgp::NeighborStatus>),
+ Isis(Vec<isis::NeighborStatus>),
}
impl From<Vec<openfabric::NeighborStatus>> for NeighborStatus {
@@ -134,6 +162,11 @@ impl From<Vec<bgp::NeighborStatus>> for NeighborStatus {
NeighborStatus::Bgp(value)
}
}
+impl From<Vec<isis::NeighborStatus>> for NeighborStatus {
+ fn from(value: Vec<isis::NeighborStatus>) -> 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<ospf::InterfaceStatus>),
WireGuard(Vec<wireguard::InterfaceStatus>),
Bgp(Vec<bgp::InterfaceStatus>),
+ Isis(Vec<isis::InterfaceStatus>),
}
impl From<Vec<openfabric::InterfaceStatus>> for InterfaceStatus {
@@ -160,6 +194,11 @@ impl From<Vec<bgp::InterfaceStatus>> for InterfaceStatus {
InterfaceStatus::Bgp(value)
}
}
+impl From<Vec<isis::InterfaceStatus>> for InterfaceStatus {
+ fn from(value: Vec<isis::InterfaceStatus>) -> 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<Vec<isis::NeighborStatus>, anyhow::Error> {
+ let mut stats: Vec<isis::NeighborStatus> = 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<Vec<isis::InterfaceStatus>, anyhow::Error> {
+ let mut stats: Vec<isis::InterfaceStatus> = 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
next prev parent reply other threads:[~2026-08-28 11:34 UTC|newest]
Thread overview: 14+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-28 11:32 [PATCH docs/gui-tests/manager/network/proxmox{-ve-rs,-perl-rs} v3 00/13] Add IS-IS protocol to fabrics Gabriel Goller
2026-08-28 11:32 ` [PATCH proxmox-ve-rs v3 01/13] frr: add fabric properties to ISIS types and rename domain Gabriel Goller
2026-08-28 11:32 ` [PATCH proxmox-ve-rs v3 02/13] ve-config: add IS-IS fabric config parsing and frr config generation Gabriel Goller
2026-08-28 11:32 ` [PATCH proxmox-ve-rs v3 03/13] ve-config: add integration tests for IS-IS fabrics Gabriel Goller
2026-08-28 11:32 ` [PATCH proxmox-ve-rs v3 04/13] ve-config: add IS-IS status deserialization types Gabriel Goller
2026-08-28 11:32 ` [PATCH proxmox-ve-rs v3 05/13] frr: accept legacy ISIS domain field Gabriel Goller
2026-08-28 11:32 ` [PATCH proxmox-perl-rs v3 06/13] pve-rs: fabrics: add IS-IS protocol ifupdown config generation Gabriel Goller
2026-08-28 11:32 ` Gabriel Goller [this message]
2026-08-28 11:32 ` [PATCH pve-network v3 08/13] fabrics: add IS-IS api types Gabriel Goller
2026-08-28 11:32 ` [PATCH pve-network v3 09/13] sdn: controllers: rename isis domain to fabric_id Gabriel Goller
2026-08-28 11:32 ` [PATCH pve-manager v3 10/13] fabrics: add IS-IS panels Gabriel Goller
2026-08-28 11:32 ` [PATCH pve-manager v3 11/13] sdn: add warning about IS-IS controller deprecation Gabriel Goller
2026-08-28 11:32 ` [PATCH pve-docs v3 12/13] sdn: add section about IS-IS fabric Gabriel Goller
2026-08-28 11:32 ` [PATCH pve-gui-tests v3 13/13] fabrics: add screenshots for IS-IS fabric and nodes Gabriel Goller
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260828113255.176546-8-g.goller@proxmox.com \
--to=g.goller@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox