all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Gabriel Goller <g.goller@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH proxmox-perl-rs 08/15] pve-rs: fabrics: include VRF routes in aggregate status
Date: Fri, 21 Aug 2026 16:03:52 +0200	[thread overview]
Message-ID: <20260821140404.322081-9-g.goller@proxmox.com> (raw)
In-Reply-To: <20260821140404.322081-1-g.goller@proxmox.com>

Collect OSPF and BGP routes from every VRF used by a fabric on the
local node, in addition to routes from the default routing table. Run
the related commands in one vtysh process and merge their JSON output,
including entries which share the same prefix.

Match routes by VRF when deciding whether a fabric has active routes.
This prevents routes from another routing table from affecting its
status. Add a parser test for duplicate prefixes across VRFs.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 pve-rs/src/bindings/sdn/fabrics.rs | 169 +++++++++++++++++------------
 pve-rs/src/sdn/status.rs           |  19 ++--
 2 files changed, 113 insertions(+), 75 deletions(-)

diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
index 0fb1ccec9b7c..b714b83951a8 100644
--- a/pve-rs/src/bindings/sdn/fabrics.rs
+++ b/pve-rs/src/bindings/sdn/fabrics.rs
@@ -5,7 +5,7 @@ pub mod pve_rs_sdn_fabrics {
     //! This provides the configuration for the SDN fabrics, as well as helper methods for reading
     //! / writing the configuration, as well as for generating ifupdown2 and FRR configuration.
 
-    use std::collections::{BTreeMap, HashMap, HashSet};
+    use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
     use std::fmt::Write;
     use std::net::IpAddr;
     use std::ops::Deref;
@@ -1109,6 +1109,68 @@ pub mod pve_rs_sdn_fabrics {
         }
     }
 
+    fn extend_routes(destination: &mut proxmox_frr::de::Routes, source: proxmox_frr::de::Routes) {
+        for (prefix, routes) in source.0 {
+            destination.0.entry(prefix).or_default().extend(routes);
+        }
+    }
+
+    /// Query and merge routes from multiple FRR commands in a single `vtysh` process.
+    fn query_routes(
+        commands: impl IntoIterator<Item = String>,
+        protocol: &str,
+    ) -> Result<proxmox_frr::de::Routes, Error> {
+        let mut command = Command::new("vtysh");
+        command.env("VTYSH_HISTFILE", "/dev/null");
+        for query in commands {
+            command.args(["-c", &query]);
+        }
+
+        let output = command.output()?;
+        if !output.status.success() {
+            anyhow::bail!(
+                "error querying {protocol} routes: {}",
+                String::from_utf8_lossy(&output.stderr).trim()
+            );
+        }
+
+        parse_routes(&output.stdout, protocol)
+    }
+
+    fn parse_routes(raw: &[u8], protocol: &str) -> Result<proxmox_frr::de::Routes, Error> {
+        let mut routes = proxmox_frr::de::Routes::default();
+        let route_stream =
+            serde_json::Deserializer::from_slice(raw).into_iter::<proxmox_frr::de::Routes>();
+        for queried_routes in route_stream {
+            extend_routes(
+                &mut routes,
+                queried_routes.with_context(|| format!("error parsing {protocol} routes"))?,
+            );
+        }
+
+        Ok(routes)
+    }
+
+    #[cfg(test)]
+    mod tests {
+        use super::parse_routes;
+
+        #[test]
+        fn parses_batched_routes_with_duplicate_prefixes() {
+            let raw = br#"
+                {"10.0.0.0/24":[{"nexthops":[],"metric":10,"protocol":"bgp","vrfName":"vrf_a"}]}
+                {"10.0.0.0/24":[{"nexthops":[],"metric":10,"protocol":"bgp","vrfName":"vrf_b"}]}
+            "#;
+
+            let routes = parse_routes(raw, "BGP").expect("batched routes should parse");
+            let routes = routes.0.values().next().expect("route should exist");
+
+            assert_eq!(routes.len(), 2);
+            assert_eq!(routes[0].vrf_name, "vrf_a");
+            assert_eq!(routes[1].vrf_name, "vrf_b");
+        }
+    }
+
     /// 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.
@@ -1124,76 +1186,47 @@ pub mod pve_rs_sdn_fabrics {
             return Ok(HashMap::new());
         };
 
-        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: 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);
-        }
-
-        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")?
-        };
-
-        let bgp_ipv4_routes_string = String::from_utf8(
-            Command::new("sh")
-                .env("VTYSH_HISTFILE", "/dev/null")
-                .args(["-c", "vtysh -c 'show ip route bgp json'"])
-                .output()?
-                .stdout,
-        )?;
-
-        let bgp_ipv6_routes_string = String::from_utf8(
-            Command::new("sh")
-                .env("VTYSH_HISTFILE", "/dev/null")
-                .args(["-c", "vtysh -c 'show ipv6 route bgp json'"])
-                .output()?
-                .stdout,
+        let openfabric_routes = query_routes(
+            [
+                "show ip route openfabric json".to_string(),
+                "show ipv6 route openfabric json".to_string(),
+            ],
+            "OpenFabric",
         )?;
 
-        let mut bgp_routes: proxmox_frr::de::Routes = if bgp_ipv4_routes_string.is_empty() {
-            proxmox_frr::de::Routes::default()
-        } else {
-            serde_json::from_str(&bgp_ipv4_routes_string)
-                .with_context(|| "error parsing bgp ipv4 routes")?
-        };
-        if !bgp_ipv6_routes_string.is_empty() {
-            let bgp_ipv6_routes: proxmox_frr::de::Routes =
-                serde_json::from_str(&bgp_ipv6_routes_string)
-                    .with_context(|| "error parsing bgp ipv6 routes")?;
-            bgp_routes.0.extend(bgp_ipv6_routes.0);
+        let node_id = NodeId::from_string(proxmox_sys::nodename().to_string())?;
+        let ospf_vrfs: BTreeSet<_> = config
+            .values()
+            .filter(|entry| {
+                matches!(entry, FabricEntry::Ospf(_)) && entry.get_node(&node_id).is_ok()
+            })
+            .filter_map(fabric_vrf_name)
+            .collect();
+        let mut ospf_commands = vec!["show ip route ospf json".to_string()];
+        ospf_commands.extend(
+            ospf_vrfs
+                .iter()
+                .map(|vrf| format!("show ip route vrf {vrf} ospf json")),
+        );
+        let ospf_routes = query_routes(ospf_commands, "OSPF")?;
+
+        let bgp_vrfs: BTreeSet<_> = config
+            .values()
+            .filter(|entry| {
+                matches!(entry, FabricEntry::Bgp(_)) && entry.get_node(&node_id).is_ok()
+            })
+            .filter_map(fabric_vrf_name)
+            .collect();
+        let mut bgp_commands = vec![
+            "show ip route bgp json".to_string(),
+            "show ipv6 route bgp json".to_string(),
+        ];
+        for vrf in bgp_vrfs {
+            bgp_commands.extend(
+                ["ip", "ipv6"].map(|family| format!("show {family} route vrf {vrf} bgp json")),
+            );
         }
+        let bgp_routes = query_routes(bgp_commands, "BGP")?;
 
         let route_status = status::RoutesParsed {
             openfabric: openfabric_routes,
diff --git a/pve-rs/src/sdn/status.rs b/pve-rs/src/sdn/status.rs
index 7a1334d20804..56a5646ded6d 100644
--- a/pve-rs/src/sdn/status.rs
+++ b/pve-rs/src/sdn/status.rs
@@ -18,6 +18,8 @@ use proxmox_ve_config::{
     },
 };
 
+use crate::bindings::pve_rs_sdn_fabrics::fabric_vrf_name;
+
 // The status of a fabric interface
 //
 // Either up or down.
@@ -535,6 +537,8 @@ pub fn get_status(
             continue;
         }
         let fabric_id = node.id().fabric_id();
+        let vrf_name =
+            fabric_vrf_name(config.get_fabric(fabric_id)?).unwrap_or_else(|| "default".to_string());
 
         let (current_protocol, all_routes) = match &node {
             ConfigNode::Openfabric(_) => (Protocol::Openfabric, &routes.openfabric.0),
@@ -565,13 +569,14 @@ pub fn get_status(
         // determine status by checking if any routes exist for our interfaces
         let has_routes = all_routes.values().any(|v| {
             v.iter().any(|route| {
-                route.nexthops.iter().any(|nexthop| {
-                    if let Some(iface_name) = &nexthop.interface_name {
-                        interface_names.contains(iface_name.as_str())
-                    } else {
-                        false
-                    }
-                })
+                route.vrf_name == vrf_name
+                    && route.nexthops.iter().any(|nexthop| {
+                        if let Some(iface_name) = &nexthop.interface_name {
+                            interface_names.contains(iface_name.as_str())
+                        } else {
+                            false
+                        }
+                    })
             })
         });
 
-- 
2.47.3





  parent reply	other threads:[~2026-08-21 14:05 UTC|newest]

Thread overview: 16+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-21 14:03 [RFC manager/network/proxmox{-ve-rs,-perl-rs} 00/15] SDN VRF support Gabriel Goller
2026-08-21 14:03 ` [PATCH proxmox-ve-rs 01/15] frr: add VRF-aware OSPF rendering Gabriel Goller
2026-08-21 14:03 ` [PATCH proxmox-ve-rs 02/15] sdn: add zone references to OSPF and BGP fabrics Gabriel Goller
2026-08-21 14:03 ` [PATCH proxmox-ve-rs 03/15] sdn: generate fabric routing configuration in zone VRFs Gabriel Goller
2026-08-21 14:03 ` [PATCH proxmox-ve-rs 04/15] sdn: fix VRF route-map scoping and zone ID validation Gabriel Goller
2026-08-21 14:03 ` [PATCH proxmox-ve-rs 05/15] tests: fabrics: add test for fabrics in VRFs Gabriel Goller
2026-08-21 14:03 ` [PATCH proxmox-perl-rs 06/15] pve-rs: fabrics: assign network interfaces to configured VRFs Gabriel Goller
2026-08-21 14:03 ` [PATCH proxmox-perl-rs 07/15] pve-rs: fabrics: make per-fabric status queries VRF-aware Gabriel Goller
2026-08-21 14:03 ` Gabriel Goller [this message]
2026-08-21 14:03 ` [PATCH pve-network 09/15] sdn: add optional VRFs for simple zones Gabriel Goller
2026-08-21 14:03 ` [PATCH pve-network 10/15] sdn: allow fabrics to use simple zone VRFs Gabriel Goller
2026-08-21 14:03 ` [PATCH pve-network 11/15] sdn: always generate EVPN " Gabriel Goller
2026-08-21 14:03 ` [PATCH pve-network 12/15] api: sdn: allow EVPN zones as fabric VRFs Gabriel Goller
2026-08-21 14:03 ` [PATCH pve-network 13/15] api: sdn: disallow BGP fabrics in EVPN zone VRFs Gabriel Goller
2026-08-21 14:03 ` [PATCH pve-manager 14/15] ui: sdn: add VRF zone selection for fabrics Gabriel Goller
2026-08-21 14:03 ` [PATCH pve-manager 15/15] ui: sdn: expose EVPN zones as fabric VRFs 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=20260821140404.322081-9-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.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal