From: Gabriel Goller <g.goller@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH proxmox-perl-rs 1/3] fabrics: add function to get status of fabric
Date: Wed, 13 Aug 2025 15:30:07 +0200 [thread overview]
Message-ID: <20250813133023.288351-2-g.goller@proxmox.com> (raw)
In-Reply-To: <20250813133023.288351-1-g.goller@proxmox.com>
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@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
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
next prev parent reply other threads:[~2025-08-13 13:29 UTC|newest]
Thread overview: 10+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-08-13 13:30 [pve-devel] [PATCH manager/network/proxmox-perl-rs 0/8] Add fabric status view Gabriel Goller
2025-08-13 13:30 ` Gabriel Goller [this message]
2025-08-13 13:30 ` [pve-devel] [PATCH proxmox-perl-rs 2/3] fabrics: add function to get all routes distributed by the fabrics Gabriel Goller
2025-08-13 13:30 ` [pve-devel] [PATCH proxmox-perl-rs 3/3] fabrics: add function to get all neighbors of the fabric Gabriel Goller
2025-08-13 13:30 ` [pve-devel] [PATCH pve-network 1/3] fabrics: add fabrics status to SDN::status function Gabriel Goller
2025-08-13 13:30 ` [pve-devel] [PATCH pve-network 2/3] fabrics: add api endpoint to return fabrics routes Gabriel Goller
2025-08-13 13:30 ` [pve-devel] [PATCH pve-network 3/3] fabrics: add api endpoint to return fabric neighbors Gabriel Goller
2025-08-13 13:30 ` [pve-devel] [PATCH pve-manager 1/2] pvestatd: add fabrics status to pvestatd Gabriel Goller
2025-08-13 13:30 ` [pve-devel] [PATCH pve-manager 2/2] fabrics: add resource view for fabrics Gabriel Goller
2025-08-22 9:01 ` [pve-devel] [PATCH manager/network/proxmox-perl-rs 0/8] Add fabric status view 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=20250813133023.288351-2-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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.