From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id CB0651FF140 for ; Fri, 27 Mar 2026 16:10:26 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 3C1F8115D1; Fri, 27 Mar 2026 16:10:50 +0100 (CET) From: Hannes Laimer To: pve-devel@lists.proxmox.com Subject: [PATCH proxmox-perl-rs 2/2] sdn: fabrics: add BGP status endpoints Date: Fri, 27 Mar 2026 16:10:28 +0100 Message-ID: <20260327151031.149360-4-h.laimer@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260327151031.149360-1-h.laimer@proxmox.com> References: <20260327151031.149360-1-h.laimer@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1774624189043 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.081 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 SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: JTMLNHFFO5TI7KIR6XEO5BF2MUYMZXD3 X-Message-ID-Hash: JTMLNHFFO5TI7KIR6XEO5BF2MUYMZXD3 X-MailFrom: h.laimer@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Add status reporting for BGP fabrics by querying vtysh for BGP routes, neighbors, and interface state. Neighbors are filtered by peer-group name matching the fabric ID, and interface status is derived from the BGP session state. Signed-off-by: Hannes Laimer --- pve-rs/src/bindings/sdn/fabrics.rs | 77 +++++++++++++++++++++ pve-rs/src/sdn/status.rs | 105 ++++++++++++++++++++++++++++- 2 files changed, 180 insertions(+), 2 deletions(-) diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs index 8af4f3f..0065f42 100644 --- a/pve-rs/src/bindings/sdn/fabrics.rs +++ b/pve-rs/src/bindings/sdn/fabrics.rs @@ -682,6 +682,35 @@ pub mod pve_rs_sdn_fabrics { status::get_routes(fabric_id, config, ospf_routes, proxmox_sys::nodename()) } + FabricEntry::Bgp(_) => { + let bgp_ipv4_routes_string = String::from_utf8( + Command::new("sh") + .args(["-c", "vtysh -c 'show ip route bgp json'"]) + .output()? + .stdout, + )?; + + let bgp_ipv6_routes_string = String::from_utf8( + Command::new("sh") + .args(["-c", "vtysh -c 'show ipv6 route bgp json'"]) + .output()? + .stdout, + )?; + + 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); + } + status::get_routes(fabric_id, config, bgp_routes, proxmox_sys::nodename()) + } } } @@ -740,6 +769,23 @@ pub mod pve_rs_sdn_fabrics { ) .map(|v| v.into()) } + FabricEntry::Bgp(_) => { + let bgp_neighbors_string = String::from_utf8( + Command::new("sh") + .args(["-c", "vtysh -c 'show bgp neighbors json'"]) + .output()? + .stdout, + )?; + let bgp_neighbors: std::collections::BTreeMap = + if bgp_neighbors_string.is_empty() { + std::collections::BTreeMap::new() + } else { + serde_json::from_str(&bgp_neighbors_string) + .with_context(|| "error parsing bgp neighbors")? + }; + + status::get_neighbors_bgp(fabric_id, bgp_neighbors).map(|v| v.into()) + } } } @@ -799,6 +845,23 @@ pub mod pve_rs_sdn_fabrics { ) .map(|v| v.into()) } + FabricEntry::Bgp(_) => { + let bgp_neighbors_string = String::from_utf8( + Command::new("sh") + .args(["-c", "vtysh -c 'show bgp neighbors json'"]) + .output()? + .stdout, + )?; + let bgp_neighbors: std::collections::BTreeMap = + if bgp_neighbors_string.is_empty() { + std::collections::BTreeMap::new() + } else { + serde_json::from_str(&bgp_neighbors_string) + .with_context(|| "error parsing bgp neighbors")? + }; + + status::get_interfaces_bgp(fabric_id, bgp_neighbors).map(|v| v.into()) + } } } @@ -859,9 +922,23 @@ pub mod pve_rs_sdn_fabrics { .with_context(|| "error parsing ospf routes")? }; + let bgp_routes_string = String::from_utf8( + Command::new("sh") + .args(["-c", "vtysh -c 'show ip route bgp json'"]) + .output()? + .stdout, + )?; + + let bgp_routes: proxmox_frr::de::Routes = if bgp_routes_string.is_empty() { + proxmox_frr::de::Routes::default() + } else { + serde_json::from_str(&bgp_routes_string).with_context(|| "error parsing bgp routes")? + }; + let route_status = status::RoutesParsed { openfabric: openfabric_routes, ospf: ospf_routes, + bgp: bgp_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 e1e3362..1f373d0 100644 --- a/pve-rs/src/sdn/status.rs +++ b/pve-rs/src/sdn/status.rs @@ -6,6 +6,7 @@ use proxmox_network_types::mac_address::MacAddress; use serde::{Deserialize, Serialize}; use proxmox_frr::de::{self}; +use proxmox_ve_config::sdn::fabric::section_config::protocol::bgp::BgpNode; use proxmox_ve_config::sdn::fabric::section_config::protocol::ospf::{ OspfNodeProperties, OspfProperties, }; @@ -80,12 +81,32 @@ mod openfabric { } } -/// Common NeighborStatus that contains either OSPF or Openfabric neighbors +mod bgp { + use serde::Serialize; + + /// The status of a BGP neighbor. + #[derive(Debug, Serialize, PartialEq, Eq)] + pub struct NeighborStatus { + pub neighbor: String, + pub status: String, + pub uptime: String, + } + + /// The status of a BGP fabric interface. + #[derive(Debug, Serialize, PartialEq, Eq)] + pub struct InterfaceStatus { + pub name: String, + pub state: super::InterfaceState, + } +} + +/// Common NeighborStatus that contains either OSPF, Openfabric, or BGP neighbors #[derive(Debug, Serialize)] #[serde(untagged)] pub enum NeighborStatus { Openfabric(Vec), Ospf(Vec), + Bgp(Vec), } impl From> for NeighborStatus { @@ -98,13 +119,19 @@ impl From> for NeighborStatus { NeighborStatus::Ospf(value) } } +impl From> for NeighborStatus { + fn from(value: Vec) -> Self { + NeighborStatus::Bgp(value) + } +} -/// Common InterfaceStatus that contains either OSPF or Openfabric interfaces +/// Common InterfaceStatus that contains either OSPF, Openfabric, or BGP interfaces #[derive(Debug, Serialize)] #[serde(untagged)] pub enum InterfaceStatus { Openfabric(Vec), Ospf(Vec), + Bgp(Vec), } impl From> for InterfaceStatus { @@ -117,6 +144,11 @@ impl From> for InterfaceStatus { InterfaceStatus::Ospf(value) } } +impl From> for InterfaceStatus { + fn from(value: Vec) -> Self { + InterfaceStatus::Bgp(value) + } +} /// The status of a route. /// @@ -135,6 +167,8 @@ pub enum Protocol { Openfabric, /// OSPF Ospf, + /// BGP + Bgp, } /// The status of a fabric. @@ -173,6 +207,8 @@ pub struct RoutesParsed { pub openfabric: de::Routes, /// All ospf routes in FRR pub ospf: de::Routes, + /// All bgp routes in FRR + pub bgp: de::Routes, } /// Config used to parse the fabric part of the running-config @@ -217,6 +253,10 @@ pub fn get_routes( .interfaces() .map(|i| i.name().as_str()) .collect(), + ConfigNode::Bgp(n) => match n.properties() { + BgpNode::Internal(props) => props.interfaces().map(|i| i.name().as_str()).collect(), + BgpNode::External(_) => HashSet::new(), + }, }; let dummy_interface = format!("dummy_{}", fabric_id.as_str()); @@ -408,6 +448,62 @@ pub fn get_interfaces_ospf( Ok(stats) } +/// Convert the `show bgp neighbors json` output into a list of [`bgp::NeighborStatus`]. +/// +/// BGP neighbors are filtered by the fabric's peer-group name (which matches the fabric ID). +pub fn get_neighbors_bgp( + fabric_id: FabricId, + neighbors: BTreeMap, +) -> Result, anyhow::Error> { + let mut stats = Vec::new(); + + for (peer_name, info) in &neighbors { + if info.peer_group.as_deref() == Some(fabric_id.as_str()) { + stats.push(bgp::NeighborStatus { + neighbor: peer_name.clone(), + status: info.bgp_state.clone(), + uptime: info.bgp_timer_up_string.clone().unwrap_or_default(), + }); + } + } + + Ok(stats) +} + +/// Convert the `show bgp neighbors json` output into a list of [`bgp::InterfaceStatus`]. +/// +/// For BGP unnumbered, each interface peer maps to a fabric interface. +pub fn get_interfaces_bgp( + fabric_id: FabricId, + neighbors: BTreeMap, +) -> Result, anyhow::Error> { + let mut stats = Vec::new(); + + for (peer_name, info) in &neighbors { + if info.peer_group.as_deref() == Some(fabric_id.as_str()) { + stats.push(bgp::InterfaceStatus { + name: peer_name.clone(), + state: if info.bgp_state == "Established" { + InterfaceState::Up + } else { + InterfaceState::Down + }, + }); + } + } + + Ok(stats) +} + +/// Minimal BGP neighbor info from `show bgp neighbors json` +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BgpNeighborInfo { + pub bgp_state: String, + pub peer_group: Option, + pub bgp_timer_up_string: Option, +} + /// 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 @@ -429,6 +525,7 @@ pub fn get_status( let (current_protocol, all_routes) = match &node { ConfigNode::Openfabric(_) => (Protocol::Openfabric, &routes.openfabric.0), ConfigNode::Ospf(_) => (Protocol::Ospf, &routes.ospf.0), + ConfigNode::Bgp(_) => (Protocol::Bgp, &routes.bgp.0), }; // get interfaces @@ -443,6 +540,10 @@ pub fn get_status( .interfaces() .map(|i| i.name().as_str()) .collect(), + ConfigNode::Bgp(n) => match n.properties() { + BgpNode::Internal(props) => props.interfaces().map(|i| i.name().as_str()).collect(), + BgpNode::External(_) => HashSet::new(), + }, }; // determine status by checking if any routes exist for our interfaces -- 2.47.3