public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pve-devel] [PATCH manager/network/proxmox-perl-rs 0/8] Add fabric status view
@ 2025-08-13 13:30 Gabriel Goller
  2025-08-13 13:30 ` [pve-devel] [PATCH proxmox-perl-rs 1/3] fabrics: add function to get status of fabric Gabriel Goller
                   ` (8 more replies)
  0 siblings, 9 replies; 10+ messages in thread
From: Gabriel Goller @ 2025-08-13 13:30 UTC (permalink / raw)
  To: pve-devel

This patch series adds fabric status monitoring to the UI by adding the fabric
status into pvestatd, making it visible in the SDN view and node resources.

There is also a new FabricContentView visible when clicking on the fabrics in
the node resource tree. There you can see the routes distributed by the fabric
and the neighbors of the current node. These statistics are retrieved using
separate api calls and are not stored in pvestatd.

The fabric status is "ok" when at least one route exists.

Route and neighbor data comes from FRR via `vtysh` commands. Since routes and
neighbors often lack fabric association, we match them by the interface name:
we retrieve interfaces configured for the fabric on the current node, then
match against e.g. a routes outgoing interface.

Open Questions: 
 * Should FRR parsing types be in proxmox-frr or proxmox-sdn-types?

proxmox-perl-rs:

Gabriel Goller (3):
  fabrics: add function to get status of fabric
  fabrics: add function to get all routes distributed by the fabrics
  fabrics: add function to get all neighbors of the fabric

 pve-rs/src/bindings/sdn/fabrics.rs | 711 +++++++++++++++++++++++++++++
 1 file changed, 711 insertions(+)


pve-network:

Gabriel Goller (3):
  fabrics: add fabrics status to SDN::status function
  fabrics: add api endpoint to return fabrics routes
  fabrics: add api endpoint to return fabric neighbors

 src/PVE/API2/Network/SDN/Fabrics.pm       | 117 +++++++++++++++++++++-
 src/PVE/API2/Network/SDN/Zones/Content.pm |   2 +-
 src/PVE/API2/Network/SDN/Zones/Status.pm  |   2 +-
 src/PVE/Network/SDN.pm                    |   6 +-
 src/test/debug/statuscheck.pl             |   3 +-
 5 files changed, 124 insertions(+), 6 deletions(-)


pve-manager:

Gabriel Goller (2):
  pvestatd: add fabrics status to pvestatd
  fabrics: add resource view for fabrics

 PVE/API2/Cluster.pm                    |  73 ++++++++++++---
 PVE/Service/pvestatd.pm                |  12 ++-
 www/manager6/Makefile                  |   1 +
 www/manager6/sdn/Browser.js            | 120 ++++++++++++++++++++-----
 www/manager6/sdn/FabricsContentView.js |  91 +++++++++++++++++++
 www/manager6/sdn/StatusView.js         |   2 +-
 6 files changed, 259 insertions(+), 40 deletions(-)
 create mode 100644 www/manager6/sdn/FabricsContentView.js


Summary over all repositories:
  12 files changed, 1094 insertions(+), 46 deletions(-)

-- 
Generated by git-murpp 0.8.0


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 10+ messages in thread

* [pve-devel] [PATCH proxmox-perl-rs 1/3] fabrics: add function to get status of fabric
  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
  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
                   ` (7 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Gabriel Goller @ 2025-08-13 13:30 UTC (permalink / raw)
  To: pve-devel

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


^ permalink raw reply	[flat|nested] 10+ messages in thread

* [pve-devel] [PATCH proxmox-perl-rs 2/3] fabrics: add function to get all routes distributed by the fabrics
  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 ` [pve-devel] [PATCH proxmox-perl-rs 1/3] fabrics: add function to get status of fabric Gabriel Goller
@ 2025-08-13 13:30 ` 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
                   ` (6 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Gabriel Goller @ 2025-08-13 13:30 UTC (permalink / raw)
  To: pve-devel

Add a function that returns a list of all the routes which are
distributed using the fabrics. For this we again need to read the config
(in order to get the interface names and thus connect the fabric to the
discovered route) and we need to query frr (using vtysh) for all the
routes (ipv4 and ipv6) distributed by a specific protocol (once for
openfabric and once for ospf). This method is used in the
FabricContentView so that clicking on the fabric resource shows the
routes distributed by the fabric.

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

diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
index 03bc597e13ef..e211ce4af92f 100644
--- a/pve-rs/src/bindings/sdn/fabrics.rs
+++ b/pve-rs/src/bindings/sdn/fabrics.rs
@@ -597,6 +597,18 @@ pub mod pve_rs_sdn_fabrics {
         use serde::{Deserialize, Serialize};
 
 
+        /// The status of a route.
+        ///
+        /// Contains the route, the fabric and protocol it belongs to and some extra nexthop
+        /// information.
+        #[derive(Debug, Serialize)]
+        pub struct RouteStatus {
+            route: String,
+            via: Vec<String>,
+            fabric_id: FabricId,
+            protocol: Protocol,
+        }
+
         /// Protocol
         #[derive(Debug, Serialize, Clone, Copy)]
         pub enum Protocol {
@@ -642,6 +654,94 @@ pub mod pve_rs_sdn_fabrics {
             pub ospf: Routes,
         }
 
+        impl TryInto<Vec<RouteStatus>> for RoutesParsed {
+            type Error = anyhow::Error;
+
+            fn try_into(self) -> Result<Vec<RouteStatus>, 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: Vec<RouteStatus> = Vec::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,
+                    };
+
+                    // 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(),
+                    };
+
+                    let mut all_routes = HashMap::new();
+                    match current_protocol {
+                        Protocol::Openfabric => all_routes.extend(&self.openfabric.0),
+                        Protocol::Ospf => all_routes.extend(&self.ospf.0),
+                    }
+
+                    for (route_key, route_list) in all_routes {
+                        let mut route_belongs_to_fabric = false;
+                        for route in route_list {
+                            for nexthop in &route.nexthops {
+                                if interface_names.contains(&nexthop.interface_name) {
+                                    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 {
+                                        nexthop.interface_name.clone()
+                                    };
+                                    via_list.push(via);
+                                }
+                            }
+
+                            stats.push(RouteStatus {
+                                route: route_key.to_string(),
+                                via: via_list,
+                                protocol: current_protocol,
+                                fabric_id: fabric_id.clone(),
+                            });
+                        }
+                    }
+                }
+                Ok(stats)
+            }
+        }
+
         impl TryInto<HashMap<FabricId, Status>> for RoutesParsed {
             type Error = anyhow::Error;
 
@@ -816,6 +916,62 @@ pub mod pve_rs_sdn_fabrics {
         pub struct Routes(pub HashMap<Cidr, Vec<Route>>);
     }
 
+    /// Get all the routes for all the fabrics on this node.
+    ///
+    /// Use FRR to get all the routes that have been inserted by either `openfabric` or 'ospf` and
+    /// associate them with the respective fabric by checking the interface they point to. Return a
+    /// single array with all routes.
+    #[export]
+    fn routes() -> Result<Vec<status::RouteStatus>, 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()
+    }
+
     /// 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.
-- 
2.47.2



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 10+ messages in thread

* [pve-devel] [PATCH proxmox-perl-rs 3/3] fabrics: add function to get all neighbors of the fabric
  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 ` [pve-devel] [PATCH proxmox-perl-rs 1/3] fabrics: add function to get status of fabric Gabriel Goller
  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 ` Gabriel Goller
  2025-08-13 13:30 ` [pve-devel] [PATCH pve-network 1/3] fabrics: add fabrics status to SDN::status function Gabriel Goller
                   ` (5 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Gabriel Goller @ 2025-08-13 13:30 UTC (permalink / raw)
  To: pve-devel

In order to also display the neighbors of a specific node in the
FabricContentView resource window get the Neighbors of the all the
fabrics. Query frr (vtysh) to get the neighbors of both openefabric and
ospf, parse it and then compile a array containing all neighbors and
the fabric it relates to.

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

diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
index e211ce4af92f..28667b8b9bf6 100644
--- a/pve-rs/src/bindings/sdn/fabrics.rs
+++ b/pve-rs/src/bindings/sdn/fabrics.rs
@@ -596,6 +596,17 @@ pub mod pve_rs_sdn_fabrics {
         };
         use serde::{Deserialize, Serialize};
 
+        /// The status of a neighbor.
+        ///
+        /// Contains the neighbor, the fabric and protocol it belongs to and the some status
+        /// information.
+        #[derive(Debug, Serialize)]
+        pub struct NeighborStatus {
+            neighbor: String,
+            status: String,
+            fabric_id: FabricId,
+            protocol: Protocol,
+        }
 
         /// The status of a route.
         ///
@@ -654,6 +665,19 @@ pub mod pve_rs_sdn_fabrics {
             pub ospf: Routes,
         }
 
+        /// Parsed neighbors for all protocols
+        ///
+        /// These are the neighbors parsed from the json output of:
+        /// `vtysh -c 'show openfabric neighbor json'` and
+        /// `vtysh -c 'show ip ospf neighbor json'`.
+        #[derive(Debug, Serialize)]
+        pub struct NeighborsParsed {
+            /// The openfabric neighbors in FRR
+            pub openfabric: openfabric::Neighbors,
+            /// The ospf neighbors in FRR
+            pub ospf: ospf::Neighbors,
+        }
+
         impl TryInto<Vec<RouteStatus>> for RoutesParsed {
             type Error = anyhow::Error;
 
@@ -742,6 +766,90 @@ pub mod pve_rs_sdn_fabrics {
             }
         }
 
+        impl TryInto<Vec<NeighborStatus>> for NeighborsParsed {
+            type Error = anyhow::Error;
+
+            fn try_into(self) -> Result<Vec<NeighborStatus>, Self::Error> {
+                let hostname = proxmox_sys::nodename();
+
+                // get all nodes
+                let raw_config = std::fs::read_to_string("/etc/pve/sdn/fabrics.cfg")?;
+                let config = FabricConfig::parse_section_config(&raw_config)?;
+
+                let mut stats: Vec<NeighborStatus> = Vec::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();
+
+                    match node {
+                        ConfigNode::Openfabric(_) => {
+                            for area in &self.openfabric.areas {
+                                if area.area == fabric_id.as_str() {
+                                    for circuit in &area.circuits {
+                                        if let (Some(adj), Some(state)) =
+                                            (&circuit.adj, &circuit.state)
+                                        {
+                                            stats.push(NeighborStatus {
+                                                neighbor: adj.clone(),
+                                                status: state.clone(),
+                                                protocol: Protocol::Openfabric,
+                                                fabric_id: fabric_id.clone(),
+                                            });
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                        ConfigNode::Ospf(node) => {
+                            let interface_names: HashSet<&str> = node
+                                .properties()
+                                .interfaces()
+                                .map(|i| i.name().as_str())
+                                .collect();
+
+                            for (neighbor_key, neighbor_list) in &self.ospf.neighbors {
+                                let mut has_matching_neighbor = false;
+                                for neighbor in neighbor_list {
+                                    match neighbor.interface_name.split_once(":") {
+                                        Some((interface_name, _)) => {
+                                            if interface_names.contains(interface_name) {
+                                                has_matching_neighbor = true;
+                                                break;
+                                            }
+                                        }
+                                        _ => {
+                                            continue;
+                                        }
+                                    }
+                                }
+                                if has_matching_neighbor {
+                                    let status = neighbor_list
+                                        .first()
+                                        .map(|n| n.neighbor_state.clone())
+                                        .unwrap_or_default();
+                                    stats.push(NeighborStatus {
+                                        neighbor: neighbor_key.clone(),
+                                        status,
+                                        protocol: Protocol::Ospf,
+                                        fabric_id: fabric_id.clone(),
+                                    });
+                                }
+                            }
+                        }
+                    }
+                }
+
+                Ok(stats)
+            }
+        }
+
         impl TryInto<HashMap<FabricId, Status>> for RoutesParsed {
             type Error = anyhow::Error;
 
@@ -914,6 +1022,117 @@ pub mod pve_rs_sdn_fabrics {
         /// 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>>);
+
+        /// Types to parse the output of vtysh commands on the ospf daemon.
+        ///
+        /// Currently it contains structs to parse the `show ip ospf neighbor json` command.
+        pub mod ospf {
+            use std::collections::HashMap;
+
+            use serde::{Deserialize, Serialize};
+
+            /// Information about the Neighbor (Peer) of the Adjacency.
+            #[derive(Debug, Serialize, Deserialize)]
+            #[serde(rename_all = "camelCase")]
+            pub struct Neighbor {
+                /// The full state of the neighbor. This is "{converged}/{role}".
+                #[serde(rename = "nbrState")]
+                pub neighbor_state: String,
+                /// Priority of the Neighbor
+                #[serde(rename = "nbrPriority")]
+                pub neighbor_priority: u32,
+                /// The current state of the adjancecy, this is a complex state machine with many
+                /// states. The most important ones are "Full" if the full table has been exchanged
+                /// and "Init" when the adjacency has been formed but no routing information has
+                /// been exchanged.
+                pub converged: String,
+                /// Role of the peer (If he's a designated router (DR) or not (DROther)
+                pub role: String,
+                /// Uptime in milliseconds
+                #[serde(rename = "upTimeInMsec")]
+                pub up_time_in_msec: u64,
+                /// Router Dead Interval Timer Due in milliseconds
+                #[serde(rename = "routerDeadIntervalTimerDueMsec")]
+                pub router_dead_interval_timer_due_msec: u64,
+                /// Uptime of the adjacency
+                #[serde(rename = "upTime")]
+                pub up_time: String,
+                /// Expires in countdown
+                #[serde(rename = "deadTime")]
+                pub dead_time: String,
+                /// The remote interface address, so the address of the other peer.
+                #[serde(rename = "ifaceAddress")]
+                pub interface_address: String,
+                /// The interface name of this adjacency. This is always a combination of interface
+                /// name and address. e.g. "ens21:5.5.5.3".
+                #[serde(rename = "ifaceName")]
+                pub interface_name: String,
+                /// Link State Retransmission List Counter
+                #[serde(rename = "linkStateRetransmissionListCounter")]
+                pub link_state_retransmission_list_counter: u32,
+                /// Link State Request List Counter
+                #[serde(rename = "linkStateRequestListCounter")]
+                pub link_state_request_list_counter: u32,
+                /// Database Summary List Counter
+                #[serde(rename = "databaseSummaryListCounter")]
+                pub database_summary_list_counter: u32,
+            }
+
+            /// The parsed OSPF neighbors
+            #[derive(Debug, Serialize, Deserialize, Default)]
+            pub struct Neighbors {
+                /// The OSPF neighbors. This is nearly always a ip-address - neighbor mapping.
+                pub neighbors: HashMap<String, Vec<Neighbor>>,
+            }
+        }
+
+        /// Structs to parse the vtysh output of the openfabric daemon.
+        ///
+        /// Currently only the output of: `show openfabric neighbor json` is modeled here.
+        pub mod openfabric {
+            use serde::{Deserialize, Serialize};
+
+            /// Adjacency information
+            ///
+            /// Circuits are Layer-2 Broadcast domains (Either point-to-point or LAN).
+            #[derive(Debug, Serialize, Deserialize)]
+            pub struct Circuit {
+                /// The circuit id
+                pub circuit: u32,
+                /// The hostname of the adjacency peer
+                pub adj: Option<String>,
+                /// The interface on which this adjacency exists
+                pub interface: Option<String>,
+                /// If the adjacent router is a L1 or L2 router
+                pub level: Option<u32>,
+                /// The state of the adjacency, this is "Up" when everything is well
+                pub state: Option<String>,
+                /// When the adjacency expires
+                #[serde(rename = "expires-in")]
+                pub expires_in: Option<String>,
+                /// Subnetwork Point of Attachment
+                pub snpa: Option<String>,
+            }
+
+            /// An openfabric area the same as SDN fabric.
+            #[derive(Debug, Serialize, Deserialize)]
+            pub struct Area {
+                /// The are name, this is the same as the fabric_id, so the name of the fabric.
+                pub area: String,
+                /// Circuits are Layer-2 Broadcast domains (Either point-to-point or LAN).
+                pub circuits: Vec<Circuit>,
+            }
+
+            /// The parsed neighbors.
+            ///
+            /// This models the output of:
+            /// `vtysh -c 'show openfabric neighbor json'`.
+            #[derive(Debug, Serialize, Deserialize, Default)]
+            pub struct Neighbors {
+                /// Every sdn fabric is also an openfabric 'area'
+                pub areas: Vec<Area>,
+            }
+        }
     }
 
     /// Get all the routes for all the fabrics on this node.
@@ -972,6 +1191,49 @@ pub mod pve_rs_sdn_fabrics {
         route_status.try_into()
     }
 
+    /// Get all the neighbors of all the fabrics on this node.
+    ///
+    /// Go through all fabrics that exist on this node. Then get the neighbors of them all and
+    /// concat them into a single array.
+    #[export]
+    fn neighbors() -> Result<Vec<status::NeighborStatus>, Error> {
+        let openfabric_neighbors_string = String::from_utf8(
+            Command::new("sh")
+                .args(["-c", "vtysh -c 'show openfabric neighbor json'"])
+                .output()?
+                .stdout,
+        )?;
+
+        let ospf_neighbors_string = String::from_utf8(
+            Command::new("sh")
+                .args(["-c", "vtysh -c 'show ip ospf neighbor json'"])
+                .output()?
+                .stdout,
+        )?;
+
+        let openfabric_neighbors: status::openfabric::Neighbors =
+            if openfabric_neighbors_string.is_empty() {
+                status::openfabric::Neighbors::default()
+            } else {
+                serde_json::from_str(&openfabric_neighbors_string)
+                    .with_context(|| "error parsing openfabric neighbors")?
+            };
+
+        let ospf_neighbors: status::ospf::Neighbors = if ospf_neighbors_string.is_empty() {
+            status::ospf::Neighbors::default()
+        } else {
+            serde_json::from_str(&ospf_neighbors_string)
+                .with_context(|| "error parsing ospf neighbors")?
+        };
+
+        let neighbor_status = status::NeighborsParsed {
+            openfabric: openfabric_neighbors,
+            ospf: ospf_neighbors,
+        };
+
+        neighbor_status.try_into()
+    }
+
     /// 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.
-- 
2.47.2



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 10+ messages in thread

* [pve-devel] [PATCH pve-network 1/3] fabrics: add fabrics status to SDN::status function
  2025-08-13 13:30 [pve-devel] [PATCH manager/network/proxmox-perl-rs 0/8] Add fabric status view Gabriel Goller
                   ` (2 preceding siblings ...)
  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 ` Gabriel Goller
  2025-08-13 13:30 ` [pve-devel] [PATCH pve-network 2/3] fabrics: add api endpoint to return fabrics routes Gabriel Goller
                   ` (4 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Gabriel Goller @ 2025-08-13 13:30 UTC (permalink / raw)
  To: pve-devel

Add the fabrics to the global SDN::status function. This is needed in
pve-manager to insert the status into the pvestatd resources.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 src/PVE/API2/Network/SDN/Zones/Content.pm | 2 +-
 src/PVE/API2/Network/SDN/Zones/Status.pm  | 2 +-
 src/PVE/Network/SDN.pm                    | 6 ++++--
 src/test/debug/statuscheck.pl             | 3 ++-
 4 files changed, 8 insertions(+), 5 deletions(-)

diff --git a/src/PVE/API2/Network/SDN/Zones/Content.pm b/src/PVE/API2/Network/SDN/Zones/Content.pm
index 76663210067d..2427abe70c93 100644
--- a/src/PVE/API2/Network/SDN/Zones/Content.pm
+++ b/src/PVE/API2/Network/SDN/Zones/Content.pm
@@ -70,7 +70,7 @@ __PACKAGE__->register_method({
 
         my $res = [];
 
-        my ($zone_status, $vnet_status) = PVE::Network::SDN::status();
+        my ($zone_status, $vnet_status, $fabric_status) = PVE::Network::SDN::status();
 
         foreach my $id (keys %{$vnet_status}) {
             if ($vnet_status->{$id}->{zone} eq $zoneid) {
diff --git a/src/PVE/API2/Network/SDN/Zones/Status.pm b/src/PVE/API2/Network/SDN/Zones/Status.pm
index 495756795f47..f9e79ffcef7a 100644
--- a/src/PVE/API2/Network/SDN/Zones/Status.pm
+++ b/src/PVE/API2/Network/SDN/Zones/Status.pm
@@ -63,7 +63,7 @@ __PACKAGE__->register_method({
 
         my $res = [];
 
-        my ($zone_status, $vnet_status) = PVE::Network::SDN::status();
+        my ($zone_status, $vnet_status, $fabric_status) = PVE::Network::SDN::status();
 
         foreach my $id (sort keys %{$zone_status}) {
             my $item->{zone} = $id;
diff --git a/src/PVE/Network/SDN.pm b/src/PVE/Network/SDN.pm
index 83f2cc71845e..19efde8b6b95 100644
--- a/src/PVE/Network/SDN.pm
+++ b/src/PVE/Network/SDN.pm
@@ -16,6 +16,8 @@ use PVE::RESTEnvironment qw(log_warn);
 use PVE::RPCEnvironment;
 use PVE::Tools qw(file_get_contents file_set_contents extract_param dir_glob_regex run_command);
 
+use PVE::RS::SDN::Fabrics;
+
 use PVE::Network::SDN::Vnets;
 use PVE::Network::SDN::Zones;
 use PVE::Network::SDN::Controllers;
@@ -87,9 +89,9 @@ sub ifquery_check {
 }
 
 sub status {
-
     my ($zone_status, $vnet_status) = PVE::Network::SDN::Zones::status();
-    return ($zone_status, $vnet_status);
+    my $fabric_status = PVE::RS::SDN::Fabrics::status();
+    return ($zone_status, $vnet_status, $fabric_status);
 }
 
 sub running_config {
diff --git a/src/test/debug/statuscheck.pl b/src/test/debug/statuscheck.pl
index e43003ba8774..e963117659d8 100644
--- a/src/test/debug/statuscheck.pl
+++ b/src/test/debug/statuscheck.pl
@@ -3,7 +3,8 @@ use warnings;
 use PVE::Network::SDN;
 use Data::Dumper;
 
-my ($transport_status, $vnet_status) = PVE::Network::SDN::status();
+my ($transport_status, $vnet_status, $fabric_status) = PVE::Network::SDN::status();
 
+print Dumper($fabric_status);
 print Dumper($vnet_status);
 print Dumper($transport_status);
-- 
2.47.2



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 10+ messages in thread

* [pve-devel] [PATCH pve-network 2/3] fabrics: add api endpoint to return fabrics routes
  2025-08-13 13:30 [pve-devel] [PATCH manager/network/proxmox-perl-rs 0/8] Add fabric status view Gabriel Goller
                   ` (3 preceding siblings ...)
  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 ` Gabriel Goller
  2025-08-13 13:30 ` [pve-devel] [PATCH pve-network 3/3] fabrics: add api endpoint to return fabric neighbors Gabriel Goller
                   ` (3 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Gabriel Goller @ 2025-08-13 13:30 UTC (permalink / raw)
  To: pve-devel

Add api endpoint that returns all the routes distributed through the
fabrics.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 src/PVE/API2/Network/SDN/Fabrics.pm | 61 ++++++++++++++++++++++++++++-
 1 file changed, 60 insertions(+), 1 deletion(-)

diff --git a/src/PVE/API2/Network/SDN/Fabrics.pm b/src/PVE/API2/Network/SDN/Fabrics.pm
index 5644fbee0fff..94905e865ce1 100644
--- a/src/PVE/API2/Network/SDN/Fabrics.pm
+++ b/src/PVE/API2/Network/SDN/Fabrics.pm
@@ -12,6 +12,7 @@ use PVE::API2::Network::SDN::Fabrics::Fabric;
 use PVE::API2::Network::SDN::Fabrics::Node;
 
 use PVE::RESTHandler;
+use PVE::JSONSchema qw(get_standard_option);
 use base qw(PVE::RESTHandler);
 
 __PACKAGE__->register_method({
@@ -49,7 +50,10 @@ __PACKAGE__->register_method({
         my ($param) = @_;
 
         my $res = [
-            { subdir => 'fabric' }, { subdir => 'node' }, { subdir => 'all' },
+            { subdir => 'fabric' },
+            { subdir => 'node' },
+            { subdir => 'all' },
+            { subdir => 'routes' },
         ];
 
         return $res;
@@ -175,4 +179,59 @@ __PACKAGE__->register_method({
     },
 });
 
+__PACKAGE__->register_method({
+    name => 'routes',
+    path => 'routes',
+    method => 'GET',
+    description => "Get routes of all fabrics.",
+    permissions => {
+        description => "Only list entries where you have 'SDN.Audit' or 'SDN.Allocate'",
+        user => 'all',
+    },
+    protected => 1,
+    proxyto => 'node',
+    parameters => {
+        additionalProperties => 0,
+        properties => {
+            node => get_standard_option('pve-node'),
+        },
+    },
+    returns => {
+        type => 'array',
+        items => {
+            type => "object",
+            properties => {
+                fabric_id => get_standard_option('pve-sdn-fabric-id'),
+                protocol => get_standard_option('pve-sdn-fabric-protocol'),
+                route => {
+                    description => "Route",
+                    type => 'string',
+                },
+                via => {
+                    description => "Nexthop",
+                    type => 'string',
+                },
+            },
+        },
+    },
+    code => sub {
+        my ($param) = @_;
+
+        my $rpcenv = PVE::RPCEnvironment::get();
+        my $authuser = $rpcenv->get_user();
+
+        my $res = [];
+
+        my $routes = PVE::RS::SDN::Fabrics::routes();
+        my $fabric_privs = ['SDN.Audit', 'SDN.Allocate'];
+        for my $route (@$routes) {
+            my $fabric_id = $route->{fabric_id};
+            next if !$rpcenv->check_any($authuser, "/sdn/fabrics/$fabric_id", $fabric_privs, 1);
+            push @$res, $route;
+        }
+
+        return $res;
+    },
+});
+
 1;
-- 
2.47.2



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 10+ messages in thread

* [pve-devel] [PATCH pve-network 3/3] fabrics: add api endpoint to return fabric neighbors
  2025-08-13 13:30 [pve-devel] [PATCH manager/network/proxmox-perl-rs 0/8] Add fabric status view Gabriel Goller
                   ` (4 preceding siblings ...)
  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 ` Gabriel Goller
  2025-08-13 13:30 ` [pve-devel] [PATCH pve-manager 1/2] pvestatd: add fabrics status to pvestatd Gabriel Goller
                   ` (2 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Gabriel Goller @ 2025-08-13 13:30 UTC (permalink / raw)
  To: pve-devel

Add api endpoint that returns all the fabric neighbors of the current
node.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 src/PVE/API2/Network/SDN/Fabrics.pm | 56 +++++++++++++++++++++++++++++
 1 file changed, 56 insertions(+)

diff --git a/src/PVE/API2/Network/SDN/Fabrics.pm b/src/PVE/API2/Network/SDN/Fabrics.pm
index 94905e865ce1..65666b69cd9e 100644
--- a/src/PVE/API2/Network/SDN/Fabrics.pm
+++ b/src/PVE/API2/Network/SDN/Fabrics.pm
@@ -54,6 +54,7 @@ __PACKAGE__->register_method({
             { subdir => 'node' },
             { subdir => 'all' },
             { subdir => 'routes' },
+            { subdir => 'neighbors' },
         ];
 
         return $res;
@@ -234,4 +235,59 @@ __PACKAGE__->register_method({
     },
 });
 
+__PACKAGE__->register_method({
+    name => 'neighbors',
+    path => 'neighbors',
+    method => 'GET',
+    description => "Get neighbors of all fabrics.",
+    permissions => {
+        description => "Only list entries where you have 'SDN.Audit' or 'SDN.Allocate'",
+        user => 'all',
+    },
+    protected => 1,
+    proxyto => 'node',
+    parameters => {
+        additionalProperties => 0,
+        properties => {
+            node => get_standard_option('pve-node'),
+        },
+    },
+    returns => {
+        type => 'array',
+        items => {
+            type => "object",
+            properties => {
+                fabric_id => get_standard_option('pve-sdn-fabric-id'),
+                protocol => get_standard_option('pve-sdn-fabric-protocol'),
+                neighbor => {
+                    description => "Neighbor",
+                    type => 'string',
+                },
+                status => {
+                    description => "Status",
+                    type => 'string',
+                },
+            },
+        },
+    },
+    code => sub {
+        my ($param) = @_;
+
+        my $rpcenv = PVE::RPCEnvironment::get();
+        my $authuser = $rpcenv->get_user();
+
+        my $res = [];
+
+        my $neighbors = PVE::RS::SDN::Fabrics::neighbors();
+        my $fabric_privs = ['SDN.Audit', 'SDN.Allocate'];
+        for my $neighbor (@$neighbors) {
+            my $fabric_id = $neighbor->{fabric_id};
+            next if !$rpcenv->check_any($authuser, "/sdn/fabrics/$fabric_id", $fabric_privs, 1);
+            push @$res, $neighbor;
+        }
+
+        return $res;
+    },
+});
+
 1;
-- 
2.47.2



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 10+ messages in thread

* [pve-devel] [PATCH pve-manager 1/2] pvestatd: add fabrics status to pvestatd
  2025-08-13 13:30 [pve-devel] [PATCH manager/network/proxmox-perl-rs 0/8] Add fabric status view Gabriel Goller
                   ` (5 preceding siblings ...)
  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 ` 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
  8 siblings, 0 replies; 10+ messages in thread
From: Gabriel Goller @ 2025-08-13 13:30 UTC (permalink / raw)
  To: pve-devel

Add the fabric status returned by SDN::status to the pvestatd resources.
This makes the fabrics visible in the SDN resources and in the node
resources. Previously the only SDN entity to be added where the SDN
zones, so a bit of restructuring is needed:

 * we also return (and add to the resources) a `sdn_type`, which shows
   which type of sdn entity this is (e.g. fabric, zone, etc.).
 * we return/add to resources the `protocol` for fabrics as well, so in
   the future we can do protocol-specific views.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 PVE/API2/Cluster.pm     | 73 +++++++++++++++++++++++++++++++++--------
 PVE/Service/pvestatd.pm | 12 ++++---
 2 files changed, 68 insertions(+), 17 deletions(-)

diff --git a/PVE/API2/Cluster.pm b/PVE/API2/Cluster.pm
index 02a7ceffee02..c22f8d4fb0e1 100644
--- a/PVE/API2/Cluster.pm
+++ b/PVE/API2/Cluster.pm
@@ -423,6 +423,21 @@ __PACKAGE__->register_method({
                     optional => 1,
                     default => 0,
                 },
+                sdn => {
+                    description => "The name of the sdn entity.",
+                    type => "string",
+                    optional => 1,
+                },
+                sdn_type => {
+                    description => "The protocol if this item is a SDN fabric.",
+                    type => "string",
+                    optional => 1,
+                },
+                protocol => {
+                    description => "The protocol if this item is a SDN fabric.",
+                    type => "string",
+                    optional => 1,
+                },
             },
         },
     },
@@ -585,6 +600,8 @@ __PACKAGE__->register_method({
                         node => $node,
                         type => 'sdn',
                         status => 'ok',
+                        # in the UI we want to show the localnetwork as a zone
+                        sdn_type => 'zone',
                     };
                     push @$res, $local_sdn;
                 }
@@ -594,19 +611,49 @@ __PACKAGE__->register_method({
                 my $nodes = PVE::Cluster::get_node_kv("sdn");
 
                 for my $node (sort keys %{$nodes}) {
-                    my $sdns = decode_json($nodes->{$node});
-
-                    for my $id (sort keys %{$sdns}) {
-                        next if !$rpcenv->check($authuser, "/sdn/zones/$id", ['SDN.Audit'], 1);
-                        my $sdn = $sdns->{$id};
-                        my $entry = {
-                            id => "sdn/$node/$id",
-                            sdn => $id,
-                            node => $node,
-                            type => 'sdn',
-                            status => $sdn->{'status'},
-                        };
-                        push @$res, $entry;
+                    my $node_config = decode_json($nodes->{$node});
+
+                    # iterate through all sdn item types (vnet, zone, fabric, etc.)
+                    for my $item_type (sort keys %{$node_config}) {
+                        # the configuration of a sdn item type (all zones, all fabrics, etc.)
+                        my $type_config = $node_config->{$item_type};
+                        for my $id (sort keys %{$type_config}) {
+                            my $status = $type_config->{$id};
+
+                            my $new_entry = {
+                                "sdn" => $id,
+                                "node" => $node,
+                                "type" => 'sdn',
+                                "sdn_type" => $item_type,
+                                "status" => $status->{status},
+                            };
+
+                            if ($item_type eq "zone") {
+                                next
+                                    if !$rpcenv->check($authuser, "/sdn/zones/$id", ['SDN.Audit'],
+                                        1);
+
+                                $new_entry->{id} = "sdn/$node/$item_type/$id";
+                                push @$res, $new_entry;
+                            } elsif ($item_type eq "fabric") {
+                                next
+                                    if !$rpcenv->check_any(
+                                        $authuser,
+                                        "/sdn/fabrics/$id",
+                                        ['SDN.Audit', 'SDN.Allocate'],
+                                        1,
+                                    );
+
+                                my $protocol = $status->{protocol};
+                                $new_entry->{id} = "sdn/$node/$item_type/$protocol/$id";
+                                $new_entry->{protocol} = $protocol;
+                                push @$res, $new_entry;
+                            } else {
+                                # if the sdn type is not zones or fabric, just add it
+                                $new_entry->{id} = "sdn/$node/$item_type/$id";
+                                push @$res, $new_entry;
+                            }
+                        }
                     }
                 }
             }
diff --git a/PVE/Service/pvestatd.pm b/PVE/Service/pvestatd.pm
index 618d6139af3e..507df4f6c475 100755
--- a/PVE/Service/pvestatd.pm
+++ b/PVE/Service/pvestatd.pm
@@ -766,12 +766,16 @@ sub update_ceph_metadata {
 }
 
 sub update_sdn_status {
-
     if ($have_sdn) {
-        my ($transport_status, $vnet_status) = PVE::Network::SDN::status();
+        my ($zone_status, $vnet_status, $fabric_status) = PVE::Network::SDN::status();
+
+        my $status = {};
+        $status->{zone} = $zone_status;
+        # don't include vnet status, as we don't have a UI panel to show infos for it
+        #$status->{vnet} = $vnet_status;
+        $status->{fabric} = $fabric_status;
 
-        my $status = $transport_status ? encode_json($transport_status) : undef;
-        PVE::Cluster::broadcast_node_kv("sdn", $status);
+        PVE::Cluster::broadcast_node_kv("sdn", encode_json($status));
     }
 }
 
-- 
2.47.2



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 10+ messages in thread

* [pve-devel] [PATCH pve-manager 2/2] fabrics: add resource view for fabrics
  2025-08-13 13:30 [pve-devel] [PATCH manager/network/proxmox-perl-rs 0/8] Add fabric status view Gabriel Goller
                   ` (6 preceding siblings ...)
  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 ` Gabriel Goller
  2025-08-22  9:01 ` [pve-devel] [PATCH manager/network/proxmox-perl-rs 0/8] Add fabric status view Gabriel Goller
  8 siblings, 0 replies; 10+ messages in thread
From: Gabriel Goller @ 2025-08-13 13:30 UTC (permalink / raw)
  To: pve-devel

When clicking on the fabric resources a new content view is available.
It shows the routes and the neighbors of the fabric on that specific
node.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 www/manager6/Makefile                  |   1 +
 www/manager6/sdn/Browser.js            | 120 ++++++++++++++++++++-----
 www/manager6/sdn/FabricsContentView.js |  91 +++++++++++++++++++
 www/manager6/sdn/StatusView.js         |   2 +-
 4 files changed, 191 insertions(+), 23 deletions(-)
 create mode 100644 www/manager6/sdn/FabricsContentView.js

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 07401f21520b..8b4d672f5145 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -313,6 +313,7 @@ JSSRC= 							\
 	sdn/zones/VlanEdit.js				\
 	sdn/zones/VxlanEdit.js				\
 	sdn/FabricsView.js				\
+	sdn/FabricsContentView.js			\
 	sdn/fabrics/Common.js				\
 	sdn/fabrics/InterfacePanel.js				\
 	sdn/fabrics/NodeEdit.js				\
diff --git a/www/manager6/sdn/Browser.js b/www/manager6/sdn/Browser.js
index f7694ae91864..af82bd6390d3 100644
--- a/www/manager6/sdn/Browser.js
+++ b/www/manager6/sdn/Browser.js
@@ -15,39 +15,115 @@ Ext.define('PVE.sdn.Browser', {
         if (!sdnId) {
             throw 'no sdn ID specified';
         }
+        let sdnType = me.pveSelNode.data.sdn_type;
+        if (!sdnType) {
+            throw 'no sdn object type specified';
+        }
 
         me.items = [];
 
+        const caps = Ext.state.Manager.get('GuiCap');
+
+        switch (sdnType) {
+            case 'zone':
+                me.items.push({
+                    nodename: nodename,
+                    zone: sdnId,
+                    xtype: 'pveSDNZoneContentPanel',
+                    title: gettext('Content'),
+                    iconCls: 'fa fa-th',
+                    itemId: 'content',
+                });
+
+                if (caps.sdn['Permissions.Modify']) {
+                    me.items.push({
+                        xtype: 'pveACLView',
+                        title: gettext('Permissions'),
+                        iconCls: 'fa fa-unlock',
+                        itemId: 'permissions',
+                        path: `/sdn/zones/${sdnId}`,
+                    });
+                }
+                break;
+            case 'fabric':
+                {
+                    let neighborStore = new Ext.data.Store({
+                        model: 'Neighbor',
+                        proxy: {
+                            type: 'proxmox',
+                            url: '/api2/json/cluster/sdn/fabrics/neighbors',
+                            reader: {
+                                type: 'json',
+                                rootProperty: 'data',
+                            },
+                            extraParams: {
+                                node: nodename,
+                            },
+                        },
+                        autoLoad: true,
+                    });
+
+                    let routeStore = new Ext.data.Store({
+                        model: 'Route',
+                        proxy: {
+                            type: 'proxmox',
+                            url: '/api2/json/cluster/sdn/fabrics/routes',
+                            reader: {
+                                type: 'json',
+                                rootProperty: 'data',
+                            },
+                            extraParams: {
+                                node: nodename,
+                            },
+                        },
+                        autoLoad: true,
+                    });
+
+                    me.items.push({
+                        nodename: nodename,
+                        routeStore: routeStore,
+                        fabricId: sdnId,
+                        protocol: me.pveSelNode.data.protocol,
+                        xtype: 'pveSDNFabricRoutesContentView',
+                        title: gettext('Routes'),
+                        iconCls: 'fa fa-th',
+                        itemId: 'routes',
+                        width: '100%',
+                    });
+                    me.items.push({
+                        nodename: nodename,
+                        neighborStore: neighborStore,
+                        fabricId: sdnId,
+                        protocol: me.pveSelNode.data.protocol,
+                        xtype: 'pveSDNFabricNeighborsContentView',
+                        title: gettext('Neighbors'),
+                        iconCls: 'fa fa-th',
+                        itemId: 'neighbors',
+                        width: '100%',
+                    });
+                }
+                break;
+        }
+
         Ext.apply(me, {
             title: Ext.String.format(
-                gettext('Zone {0} on node {1}'),
+                gettext('{0} {1} on node {2}'),
+                `${sdnType}`,
                 `'${sdnId}'`,
                 `'${nodename}'`,
             ),
             hstateid: 'sdntab',
         });
 
-        const caps = Ext.state.Manager.get('GuiCap');
-
-        me.items.push({
-            nodename: nodename,
-            zone: sdnId,
-            xtype: 'pveSDNZoneContentPanel',
-            title: gettext('Content'),
-            iconCls: 'fa fa-th',
-            itemId: 'content',
-        });
-
-        if (caps.sdn['Permissions.Modify']) {
-            me.items.push({
-                xtype: 'pveACLView',
-                title: gettext('Permissions'),
-                iconCls: 'fa fa-unlock',
-                itemId: 'permissions',
-                path: `/sdn/zones/${sdnId}`,
-            });
-        }
-
         me.callParent();
     },
 });
+
+Ext.define('Route', {
+    extend: 'Ext.data.Model',
+    fields: ['route', 'via', 'fabric_id', 'protocol'],
+});
+Ext.define('Neighbor', {
+    extend: 'Ext.data.Model',
+    fields: ['neighbor', 'status', 'fabric_id', 'protocol'],
+});
diff --git a/www/manager6/sdn/FabricsContentView.js b/www/manager6/sdn/FabricsContentView.js
new file mode 100644
index 000000000000..f1e5ec146b8b
--- /dev/null
+++ b/www/manager6/sdn/FabricsContentView.js
@@ -0,0 +1,91 @@
+Ext.define('PVE.sdn.FabricRoutesContentView', {
+    extend: 'Ext.grid.GridPanel',
+    alias: 'widget.pveSDNFabricRoutesContentView',
+
+    initComponent: function () {
+        let me = this;
+        let sm = Ext.create('Ext.selection.RowModel', {});
+
+        me.routeStore.addFilter([
+            {
+                property: 'fabric_id',
+                value: me.fabricId,
+            },
+            {
+                property: 'protocol',
+                value: me.protocol,
+            },
+        ]);
+        me.routeStore.sort('route', 'ASC');
+
+        Ext.apply(me, {
+            store: me.routeStore,
+            selModel: sm,
+            columns: [
+                {
+                    header: 'Route',
+                    sortable: true,
+                    dataIndex: 'route',
+                    flex: 1,
+                },
+                {
+                    header: 'Via',
+                    sortable: true,
+                    dataIndex: 'via',
+                    renderer: (value) => {
+                        if (Ext.isArray(value)) {
+                            return value.join('<br>');
+                        }
+                        return value || '';
+                    },
+                    flex: 1,
+                },
+            ],
+        });
+
+        me.callParent();
+    },
+});
+
+Ext.define('PVE.sdn.FabricNeighborsContentView', {
+    extend: 'Ext.grid.GridPanel',
+    alias: 'widget.pveSDNFabricNeighborsContentView',
+
+    initComponent: function () {
+        let me = this;
+        let sm = Ext.create('Ext.selection.RowModel', {});
+
+        me.neighborStore.addFilter([
+            {
+                property: 'fabric_id',
+                value: me.fabricId,
+            },
+            {
+                property: 'protocol',
+                value: me.protocol,
+            },
+        ]);
+        me.neighborStore.sort('neighbor', 'ASC');
+
+        Ext.apply(me, {
+            store: me.neighborStore,
+            selModel: sm,
+            columns: [
+                {
+                    header: 'Neighbor',
+                    sortable: true,
+                    dataIndex: 'neighbor',
+                    flex: 1,
+                },
+                {
+                    header: 'Status',
+                    sortable: true,
+                    dataIndex: 'status',
+                    flex: 0.5,
+                },
+            ],
+        });
+
+        me.callParent();
+    },
+});
diff --git a/www/manager6/sdn/StatusView.js b/www/manager6/sdn/StatusView.js
index dd05c73fdfcf..e66e3f624354 100644
--- a/www/manager6/sdn/StatusView.js
+++ b/www/manager6/sdn/StatusView.js
@@ -102,7 +102,7 @@ Ext.define(
     function () {
         Ext.define('pve-sdn-status', {
             extend: 'Ext.data.Model',
-            fields: ['id', 'type', 'node', 'status', 'sdn'],
+            fields: ['id', 'type', 'node', 'status', 'sdn', 'sdn_type'],
             idProperty: 'id',
         });
     },
-- 
2.47.2



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [pve-devel] [PATCH manager/network/proxmox-perl-rs 0/8] Add fabric status view
  2025-08-13 13:30 [pve-devel] [PATCH manager/network/proxmox-perl-rs 0/8] Add fabric status view Gabriel Goller
                   ` (7 preceding siblings ...)
  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 ` Gabriel Goller
  8 siblings, 0 replies; 10+ messages in thread
From: Gabriel Goller @ 2025-08-22  9:01 UTC (permalink / raw)
  To: pve-devel

New version available here:
https://lore.proxmox.com/pve-devel/20250822090102.102949-1-g.goller@proxmox.com/


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 10+ messages in thread

end of thread, other threads:[~2025-08-22  9:03 UTC | newest]

Thread overview: 10+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
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 ` [pve-devel] [PATCH proxmox-perl-rs 1/3] fabrics: add function to get status of fabric Gabriel Goller
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

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal