From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 697F81FF17E for ; Thu, 30 Oct 2025 16:58:28 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 7EDE92EB; Thu, 30 Oct 2025 16:58:57 +0100 (CET) From: Stefan Hanreich To: pve-devel@lists.proxmox.com Date: Thu, 30 Oct 2025 16:48:23 +0100 Message-ID: <20251030154851.540408-17-s.hanreich@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20251030154851.540408-1-s.hanreich@proxmox.com> References: <20251030154851.540408-1-s.hanreich@proxmox.com> MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.185 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment KAM_LAZY_DOMAIN_SECURITY 1 Sending domain does not have any anti-forgery methods RCVD_IN_VALIDITY_CERTIFIED_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_RPBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_SAFE_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RDNS_NONE 0.793 Delivered to internal network by a host with no rDNS SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_NONE 0.001 SPF: sender does not publish an SPF Record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [status.rs, fabrics.rs] Subject: [pve-devel] [PATCH proxmox-perl-rs 08/10] pve-rs: fabrics: add function to get routes learned by a fabric X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Proxmox VE development discussion Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pve-devel-bounces@lists.proxmox.com Sender: "pve-devel" From: Gabriel Goller Add function to retrieve routes learned via OpenFabric or OSPF for a specific fabric. Query FRR using `show ip route ` commands so that we get a common json schema for every protocol. Match routes to the fabric by comparing outgoing interfaces against the fabric's configured interfaces on the local node. Signed-off-by: Gabriel Goller Signed-off-by: Stefan Hanreich --- pve-rs/src/bindings/sdn/fabrics.rs | 61 +++++++++++++++++++ pve-rs/src/sdn/status.rs | 94 +++++++++++++++++++++++++++++- 2 files changed, 154 insertions(+), 1 deletion(-) diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs index a1f056d..5fbb67e 100644 --- a/pve-rs/src/bindings/sdn/fabrics.rs +++ b/pve-rs/src/bindings/sdn/fabrics.rs @@ -605,6 +605,67 @@ pub mod pve_rs_sdn_fabrics { .with_context(|| "error converting section config to fabricconfig") } + /// Get the routes that have been learned and distributed by this specific fabric on this node. + /// + /// Read and parse the fabric config to get the protocol and the interfaces. Parse the vtysh + /// output and assign the routes to a fabric by using the interface list. Return a list of + /// common route structs. + #[export] + fn routes(fabric_id: FabricId) -> Result, Error> { + // Read fabric config to get protocol of fabric + let config = get_fabrics_config()?; + + let fabric = config.get_fabric(&fabric_id)?; + match fabric { + FabricEntry::Openfabric(_) => { + 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 mut openfabric_routes: proxmox_frr::de::Routes = + if openfabric_ipv4_routes_string.is_empty() { + proxmox_frr::de::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: proxmox_frr::de::Routes = + serde_json::from_str(&openfabric_ipv6_routes_string) + .with_context(|| "error parsing openfabric ipv6 routes")?; + openfabric_routes.0.extend(openfabric_ipv6_routes.0); + } + status::get_routes(fabric_id, config, openfabric_routes) + } + FabricEntry::Ospf(_) => { + let ospf_routes_string = String::from_utf8( + Command::new("sh") + .args(["-c", "vtysh -c 'show ip route ospf json'"]) + .output()? + .stdout, + )?; + let ospf_routes: proxmox_frr::de::Routes = if ospf_routes_string.is_empty() { + proxmox_frr::de::Routes::default() + } else { + serde_json::from_str(&ospf_routes_string) + .with_context(|| "error parsing ospf routes")? + }; + + status::get_routes(fabric_id, config, ospf_routes) + } + } + } + /// 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. diff --git a/pve-rs/src/sdn/status.rs b/pve-rs/src/sdn/status.rs index 0c9dc0f..ba7fcf7 100644 --- a/pve-rs/src/sdn/status.rs +++ b/pve-rs/src/sdn/status.rs @@ -10,10 +10,19 @@ use proxmox_ve_config::{ common::valid::Valid, sdn::fabric::{ FabricConfig, - section_config::{Section, fabric::FabricId, node::Node as ConfigNode}, + section_config::{Section, fabric::FabricId, node::Node as ConfigNode, node::NodeId}, }, }; +/// The status of a route. +/// +/// Contains the route and all the nexthops. This is common across all protocols. +#[derive(Debug, Serialize)] +pub struct RouteStatus { + route: String, + via: Vec, +} + /// Protocol #[derive(Debug, Serialize, Clone, Copy)] #[serde(rename_all = "lowercase")] @@ -73,6 +82,89 @@ pub struct FabricsRunningConfig { pub ids: BTreeMap, } +/// Converts the parsed `show ip route x` frr route output into a list of common [`RouteStatus`] +/// structs. +/// +/// We always execute `show ip route ` so we only get routes generated from a specific +/// protocol. The problem is that we can't definitely link a specific route to a specific fabric. +/// To solve this, we retrieve all the interfaces configured on a fabric on this node and check +/// which route contains a output interface of the fabric. +pub fn get_routes( + fabric_id: FabricId, + config: Valid, + routes: de::Routes, +) -> Result, anyhow::Error> { + let hostname = proxmox_sys::nodename(); + + let mut stats: Vec = Vec::new(); + + if let Ok(node) = config + .get_fabric(&fabric_id)? + .get_node(&NodeId::from_string(hostname.to_string())?) + { + let mut interface_names: HashSet<&str> = match node { + ConfigNode::Openfabric(n) => n + .properties() + .interfaces() + .map(|i| i.name().as_str()) + .collect(), + ConfigNode::Ospf(n) => n + .properties() + .interfaces() + .map(|i| i.name().as_str()) + .collect(), + }; + + let dummy_interface = format!("dummy_{}", fabric_id.as_str()); + interface_names.insert(&dummy_interface); + + for (route_key, route_list) in routes.0 { + let mut route_belongs_to_fabric = false; + for route in &route_list { + if !route.installed.unwrap_or_default() { + continue; + } + + for nexthop in &route.nexthops { + if let Some(iface_name) = &nexthop.interface_name { + if interface_names.contains(iface_name.as_str()) { + route_belongs_to_fabric = true; + break; + } + } + } + if route_belongs_to_fabric { + break; + } + } + + if route_belongs_to_fabric { + let mut via_list = Vec::new(); + for route in route_list { + for nexthop in &route.nexthops { + let via = if let Some(ip) = nexthop.ip { + ip.to_string() + } else if let Some(iface_name) = &nexthop.interface_name { + iface_name.clone() + } else if let Some(true) = &nexthop.unreachable { + "unreachable".to_string() + } else { + continue; + }; + via_list.push(via); + } + } + + stats.push(RouteStatus { + route: route_key.to_string(), + via: via_list, + }); + } + } + } + Ok(stats) +} + /// Get the status for each fabric using the parsed routes from frr /// /// Using the parsed routes we get from frr, filter and map them to a HashMap mapping every -- 2.47.3 _______________________________________________ pve-devel mailing list pve-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel