[pve-devel] [PATCH proxmox-perl-rs 1/3] fabrics: add function to get status of fabric

Gabriel Goller g.goller at proxmox.com
Wed Aug 13 15:30:07 CEST 2025


Add a function to get the status of a fabric. This is the status which
will then be inserted into the pvestatd daemon and returned through the
resources api. In order the generate the HashMap of statuses for all
fabrics we need to read the fabric config and execute a vtysh (frr)
command to get the routes of the corresponding fabric. If there is at
least one route which is related to the fabric, the fabric is considered
"ok".

Signed-off-by: Gabriel Goller <g.goller at proxmox.com>
---
 pve-rs/src/bindings/sdn/fabrics.rs | 293 +++++++++++++++++++++++++++++
 1 file changed, 293 insertions(+)

diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
index 587b1d68c8fb..03bc597e13ef 100644
--- a/pve-rs/src/bindings/sdn/fabrics.rs
+++ b/pve-rs/src/bindings/sdn/fabrics.rs
@@ -9,8 +9,10 @@ pub mod pve_rs_sdn_fabrics {
     use std::fmt::Write;
     use std::net::IpAddr;
     use std::ops::Deref;
+    use std::process::Command;
     use std::sync::Mutex;
 
+    use anyhow::Context;
     use anyhow::Error;
     use openssl::hash::{MessageDigest, hash};
     use serde::{Deserialize, Serialize};
@@ -578,4 +580,295 @@ pub mod pve_rs_sdn_fabrics {
 
         Ok(interfaces)
     }
+
+    /// This module contains status-related structs that represent Routes and Neighbors for all
+    /// protocols
+    pub mod status {
+        use std::{
+            collections::{HashMap, HashSet},
+            net::IpAddr,
+        };
+
+        use proxmox_network_types::ip_address::Cidr;
+        use proxmox_ve_config::sdn::fabric::{
+            FabricConfig,
+            section_config::{fabric::FabricId, node::Node as ConfigNode},
+        };
+        use serde::{Deserialize, Serialize};
+
+
+        /// Protocol
+        #[derive(Debug, Serialize, Clone, Copy)]
+        pub enum Protocol {
+            /// Openfabric
+            Openfabric,
+            /// OSPF
+            Ospf,
+        }
+
+        /// The status of a fabric.
+        #[derive(Debug, Serialize)]
+        pub enum FabricStatus {
+            /// The fabric exists and has a route
+            #[serde(rename = "ok")]
+            Ok,
+            /// The fabric does not exist or doesn't distribute any routes
+            #[serde(rename = "not ok")]
+            NotOk,
+        }
+
+        /// Status of a fabric.
+        ///
+        /// Check if there are any routes, if yes, then the status is ok, otherwise not ok.
+        #[derive(Debug, Serialize)]
+        pub struct Status {
+            #[serde(rename = "type")]
+            ty: String,
+            status: FabricStatus,
+            protocol: Protocol,
+            sdn: FabricId,
+            sdn_type: String,
+        }
+
+        /// Parsed routes for all protocols
+        ///
+        /// These are the routes parsed from the json output of:
+        /// `vtysh -c 'show ip route <protocol> json'`.
+        #[derive(Debug, Serialize)]
+        pub struct RoutesParsed {
+            /// All openfabric routes in FRR
+            pub openfabric: Routes,
+            /// All ospf routes in FRR
+            pub ospf: Routes,
+        }
+
+        impl TryInto<HashMap<FabricId, Status>> for RoutesParsed {
+            type Error = anyhow::Error;
+
+            fn try_into(self) -> Result<HashMap<FabricId, Status>, Self::Error> {
+                let hostname = proxmox_sys::nodename();
+
+                // to associate a route to a fabric, we get all the interfaces which are associated
+                // with a fabric on this node and compare them with the interfaces on the route.
+                let raw_config = std::fs::read_to_string("/etc/pve/sdn/fabrics.cfg")?;
+                let config = FabricConfig::parse_section_config(&raw_config)?;
+
+                let mut stats: HashMap<FabricId, Status> = HashMap::new();
+
+                for (nodeid, node) in config.values().flat_map(|entry| {
+                    entry
+                        .nodes()
+                        .map(|(id, node)| (id.to_string(), node.clone()))
+                }) {
+                    if nodeid != hostname {
+                        continue;
+                    }
+                    let fabric_id = node.id().fabric_id().clone();
+
+                    let current_protocol = match &node {
+                        ConfigNode::Openfabric(_) => Protocol::Openfabric,
+                        ConfigNode::Ospf(_) => Protocol::Ospf,
+                    };
+
+                    let mut all_routes = HashMap::new();
+                    match &node {
+                        ConfigNode::Openfabric(_) => all_routes.extend(&self.openfabric.0),
+                        ConfigNode::Ospf(_) => all_routes.extend(&self.ospf.0),
+                    }
+
+                    // get interfaces
+                    let interface_names: HashSet<String> = match node {
+                        ConfigNode::Openfabric(n) => n
+                            .properties()
+                            .interfaces()
+                            .map(|i| i.name().to_string())
+                            .collect(),
+                        ConfigNode::Ospf(n) => n
+                            .properties()
+                            .interfaces()
+                            .map(|i| i.name().to_string())
+                            .collect(),
+                    };
+
+                    // determine status by checking if any routes exist for our interfaces
+                    let has_routes = all_routes.iter().any(|(_, v)| {
+                        v.iter().any(|route| {
+                            route
+                                .nexthops
+                                .iter()
+                                .any(|nexthop| interface_names.contains(&nexthop.interface_name))
+                        })
+                    });
+
+                    let fabric = Status {
+                        ty: "sdn".to_owned(),
+                        status: if has_routes {
+                            FabricStatus::Ok
+                        } else {
+                            FabricStatus::NotOk
+                        },
+                        sdn_type: "fabric".to_string(),
+                        protocol: current_protocol,
+                        sdn: fabric_id.clone(),
+                    };
+                    stats.insert(fabric_id, fabric);
+                }
+
+                Ok(stats)
+            }
+        }
+
+        /// A nexthop of a route
+        #[derive(Debug, Serialize, Deserialize, Clone)]
+        pub struct NextHop {
+            /// Flags
+            pub flags: i32,
+            /// If the route is in the FIB (Forward Information Base)
+            pub fib: Option<bool>,
+            /// IP of the nexthoip
+            pub ip: Option<IpAddr>,
+            /// AFI (either IPv4, IPv6 or something else)
+            pub afi: String,
+            /// Index of the outgoing interface
+            #[serde(rename = "interfaceIndex")]
+            pub interface_index: i32,
+            #[serde(rename = "interfaceName")]
+            /// Name of the outgoing interface
+            pub interface_name: String,
+            /// If the nexthop is active
+            pub active: bool,
+            /// If the route has the onlink flag. Onlink means that we pretend that the nexthop is
+            /// directly attached to this link, even if it does not match any interface prefix.
+            #[serde(rename = "onLink")]
+            pub on_link: bool,
+            /// Remap-Source, this rewrites the source address to the following address, if this
+            /// nexthop is used.
+            #[serde(rename = "rmapSource")]
+            pub remap_source: Option<IpAddr>,
+            /// Weight of the nexthop
+            pub weight: i32,
+        }
+
+        /// route
+        #[derive(Debug, Serialize, Deserialize, Clone)]
+        pub struct Route {
+            /// Prefix of the route
+            pub prefix: Cidr,
+            /// Prefix Length
+            #[serde(rename = "prefixLen")]
+            pub prefix_len: u32,
+            /// Protocol from which the route originates
+            pub protocol: String,
+            /// VRF id
+            #[serde(rename = "vrfId")]
+            pub vrf_id: u32,
+            /// VRF name
+            #[serde(rename = "vrfName")]
+            pub vrf_name: String,
+            /// If the route has been selected (if multiple of the same routes from different
+            /// daemons exist, the one with the shortest distance is selected).
+            pub selected: Option<bool>,
+            /// Destination Selected
+            #[serde(rename = "destSelected")]
+            pub destination_selected: Option<bool>,
+            /// Distance of the route
+            pub distance: Option<i32>,
+            /// Metric of the route
+            pub metric: i32,
+            /// If the route is installed in the kernel routing table
+            pub installed: Option<bool>,
+            /// The id of the routing table
+            pub table: i32,
+            /// Internal Status
+            #[serde(rename = "internalStatus")]
+            pub internal_status: i32,
+            /// Internal Flags
+            #[serde(rename = "internalFlags")]
+            pub internal_flags: i32,
+            /// Internal Nexthop Num, this is the id to lookup the nexthop (visible in e.g. `ip
+            /// nexthop ls`).
+            #[serde(rename = "internalNextHopNum")]
+            pub internal_nexthop_num: i32,
+            /// Internal Nexthop Active Num
+            #[serde(rename = "internalNextHopActiveNum")]
+            pub internal_nexthop_active_num: i32,
+            /// Nexthop Group Id
+            #[serde(rename = "nexthopGroupId")]
+            pub nexthop_group_id: i32,
+            /// Installed Nexthop Group Id
+            #[serde(rename = "installedNexthopGroupId")]
+            pub installed_nexthop_group_id: Option<i32>,
+            /// The uptime of the route
+            pub uptime: String,
+
+            /// Array of all the nexthops associated with this route. When you have e.g. two
+            /// connections between two nodes, there is going to be one route, but two nexthops.
+            pub nexthops: Vec<NextHop>,
+        }
+
+        /// Struct to parse zebra routes by FRR.
+        ///
+        /// To get the routes from FRR, instead of asking the daemon of every protocol for their
+        /// routes we simply ask zebra which routes have been inserted and filter them by protocol.
+        /// The following command is used to accomplish this: `show ip route <protocol> json`.
+        /// This struct can be used the deserialize the output of that command.
+        #[derive(Debug, Serialize, Deserialize, Default)]
+        pub struct Routes(pub HashMap<Cidr, Vec<Route>>);
+    }
+
+    /// Return the status of all fabrics on this node.
+    ///
+    /// Go through all fabrics in the config, then filter out the ones that exist on this node.
+    /// Check if there are any routes in the routing table that use the interface specified in the
+    /// config. If there are, show "ok" as status, otherwise "not ok".
+    #[export]
+    fn status() -> Result<HashMap<FabricId, status::Status>, Error> {
+        let openfabric_ipv4_routes_string = String::from_utf8(
+            Command::new("sh")
+                .args(["-c", "vtysh -c 'show ip route openfabric json'"])
+                .output()?
+                .stdout,
+        )?;
+
+        let openfabric_ipv6_routes_string = String::from_utf8(
+            Command::new("sh")
+                .args(["-c", "vtysh -c 'show ipv6 route openfabric json'"])
+                .output()?
+                .stdout,
+        )?;
+
+        let ospf_routes_string = String::from_utf8(
+            Command::new("sh")
+                .args(["-c", "vtysh -c 'show ip route ospf json'"])
+                .output()?
+                .stdout,
+        )?;
+
+        let mut openfabric_routes: status::Routes = if openfabric_ipv4_routes_string.is_empty() {
+            status::Routes::default()
+        } else {
+            serde_json::from_str(&openfabric_ipv4_routes_string)
+                .with_context(|| "error parsing openfabric ipv4 routes")?
+        };
+        if !openfabric_ipv6_routes_string.is_empty() {
+            let openfabric_ipv6_routes: status::Routes =
+                serde_json::from_str(&openfabric_ipv6_routes_string)
+                    .with_context(|| "error parsing openfabric ipv6 routes")?;
+            openfabric_routes.0.extend(openfabric_ipv6_routes.0);
+        }
+
+        let ospf_routes: status::Routes = if ospf_routes_string.is_empty() {
+            status::Routes::default()
+        } else {
+            serde_json::from_str(&ospf_routes_string)
+                .with_context(|| "error parsing ospf routes")?
+        };
+
+        let route_status = status::RoutesParsed {
+            openfabric: openfabric_routes,
+            ospf: ospf_routes,
+        };
+
+        route_status.try_into()
+    }
 }
-- 
2.47.2





More information about the pve-devel mailing list