* [pve-devel] [PATCH manager/network/proxmox{-ve-rs, -perl-rs} v3 00/13] Add fabric status view
@ 2025-08-26 9:49 Gabriel Goller
2025-08-26 9:49 ` [pve-devel] [PATCH proxmox-ve-rs v3 1/3] frr: make room for deserialization structs Gabriel Goller
` (12 more replies)
0 siblings, 13 replies; 14+ messages in thread
From: Gabriel Goller @ 2025-08-26 9:49 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.
Changelog:
v2, thanks @Wobu:
* moved the conversion functions and return types to another module
* removed some allocations
v1, thanks @Stefan:
* moved the frr deserialization structs to proxmox-frr
* fixed typo in documentation
proxmox-ve-rs:
Gabriel Goller (3):
frr: make room for deserialization structs
frr: add deserialization types for openfabric and ospf
ve-config: add helper function to iterate over all nodes in all
fabrics
proxmox-frr/Cargo.toml | 1 +
proxmox-frr/debian/control | 4 +
proxmox-frr/src/de/mod.rs | 104 ++++++++++
proxmox-frr/src/de/openfabric.rs | 42 ++++
proxmox-frr/src/de/ospf.rs | 57 ++++++
proxmox-frr/src/lib.rs | 243 +-----------------------
proxmox-frr/src/ser/mod.rs | 241 +++++++++++++++++++++++
proxmox-frr/src/{ => ser}/openfabric.rs | 4 +-
proxmox-frr/src/{ => ser}/ospf.rs | 2 +-
proxmox-frr/src/{ => ser}/route_map.rs | 0
proxmox-frr/src/{ => ser}/serializer.rs | 2 +-
proxmox-ve-config/src/sdn/fabric/frr.rs | 170 +++++++++--------
proxmox-ve-config/src/sdn/fabric/mod.rs | 5 +
proxmox-ve-config/src/sdn/frr.rs | 2 +-
proxmox-ve-config/tests/fabric/main.rs | 2 +-
15 files changed, 554 insertions(+), 325 deletions(-)
create mode 100644 proxmox-frr/src/de/mod.rs
create mode 100644 proxmox-frr/src/de/openfabric.rs
create mode 100644 proxmox-frr/src/de/ospf.rs
create mode 100644 proxmox-frr/src/ser/mod.rs
rename proxmox-frr/src/{ => ser}/openfabric.rs (97%)
rename proxmox-frr/src/{ => ser}/ospf.rs (99%)
rename proxmox-frr/src/{ => ser}/route_map.rs (100%)
rename proxmox-frr/src/{ => ser}/serializer.rs (99%)
proxmox-perl-rs:
Gabriel Goller (4):
pve: fabrics: update proxmox-frr import path
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 | 163 ++++++++++++++-
pve-rs/src/lib.rs | 2 +
pve-rs/src/sdn/mod.rs | 3 +
pve-rs/src/sdn/status.rs | 313 +++++++++++++++++++++++++++++
4 files changed, 480 insertions(+), 1 deletion(-)
create mode 100644 pve-rs/src/sdn/mod.rs
create mode 100644 pve-rs/src/sdn/status.rs
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 (3):
pvestatd: add fabrics status to pvestatd
fabrics: add resource view for fabrics
permissions: differentiate between zone and fabric paths
PVE/API2/Cluster.pm | 73 ++++++++++++---
PVE/Service/pvestatd.pm | 12 ++-
www/manager6/Makefile | 1 +
www/manager6/data/PermPathStore.js | 9 +-
www/manager6/sdn/Browser.js | 120 ++++++++++++++++++++-----
www/manager6/sdn/FabricsContentView.js | 91 +++++++++++++++++++
www/manager6/sdn/StatusView.js | 2 +-
7 files changed, 267 insertions(+), 41 deletions(-)
create mode 100644 www/manager6/sdn/FabricsContentView.js
Summary over all repositories:
31 files changed, 1425 insertions(+), 373 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] 14+ messages in thread
* [pve-devel] [PATCH proxmox-ve-rs v3 1/3] frr: make room for deserialization structs
2025-08-26 9:49 [pve-devel] [PATCH manager/network/proxmox{-ve-rs, -perl-rs} v3 00/13] Add fabric status view Gabriel Goller
@ 2025-08-26 9:49 ` Gabriel Goller
2025-08-26 9:49 ` [pve-devel] [PATCH proxmox-ve-rs v3 2/3] frr: add deserialization types for openfabric and ospf Gabriel Goller
` (11 subsequent siblings)
12 siblings, 0 replies; 14+ messages in thread
From: Gabriel Goller @ 2025-08-26 9:49 UTC (permalink / raw)
To: pve-devel
Move all the serialization structs to a new subfolder `ser`. This makes
place for the deserialization structs which will land in a new folder
`de`. The deserialization structs will be used to parse the output of
`vtysh` commands, so that we can show statistics of various sdn objects.
Also update all the callsites to use the new subfolder.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
proxmox-frr/src/lib.rs | 242 +-----------------------
proxmox-frr/src/ser/mod.rs | 241 +++++++++++++++++++++++
proxmox-frr/src/{ => ser}/openfabric.rs | 4 +-
proxmox-frr/src/{ => ser}/ospf.rs | 2 +-
proxmox-frr/src/{ => ser}/route_map.rs | 0
proxmox-frr/src/{ => ser}/serializer.rs | 2 +-
proxmox-ve-config/src/sdn/fabric/frr.rs | 170 +++++++++--------
proxmox-ve-config/src/sdn/frr.rs | 2 +-
proxmox-ve-config/tests/fabric/main.rs | 2 +-
9 files changed, 340 insertions(+), 325 deletions(-)
create mode 100644 proxmox-frr/src/ser/mod.rs
rename proxmox-frr/src/{ => ser}/openfabric.rs (97%)
rename proxmox-frr/src/{ => ser}/ospf.rs (99%)
rename proxmox-frr/src/{ => ser}/route_map.rs (100%)
rename proxmox-frr/src/{ => ser}/serializer.rs (99%)
diff --git a/proxmox-frr/src/lib.rs b/proxmox-frr/src/lib.rs
index 86101182fafd..35b62cb39c91 100644
--- a/proxmox-frr/src/lib.rs
+++ b/proxmox-frr/src/lib.rs
@@ -1,241 +1 @@
-pub mod openfabric;
-pub mod ospf;
-pub mod route_map;
-pub mod serializer;
-
-use std::collections::{BTreeMap, BTreeSet};
-use std::fmt::Display;
-use std::str::FromStr;
-
-use crate::route_map::{AccessList, ProtocolRouteMap, RouteMap};
-
-use thiserror::Error;
-
-/// Generic FRR router.
-///
-/// This generic FRR router contains all the protocols that we implement.
-/// In FRR this is e.g.:
-/// ```text
-/// router openfabric test
-/// !....
-/// ! or
-/// router ospf
-/// !....
-/// ```
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
-pub enum Router {
- Openfabric(openfabric::OpenfabricRouter),
- Ospf(ospf::OspfRouter),
-}
-
-impl From<openfabric::OpenfabricRouter> for Router {
- fn from(value: openfabric::OpenfabricRouter) -> Self {
- Router::Openfabric(value)
- }
-}
-
-/// Generic FRR routername.
-///
-/// The variants represent different protocols. Some have `router <protocol> <name>`, others have
-/// `router <protocol> <process-id>`, some only have `router <protocol>`.
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
-pub enum RouterName {
- Openfabric(openfabric::OpenfabricRouterName),
- Ospf(ospf::OspfRouterName),
-}
-
-impl From<openfabric::OpenfabricRouterName> for RouterName {
- fn from(value: openfabric::OpenfabricRouterName) -> Self {
- Self::Openfabric(value)
- }
-}
-
-impl Display for RouterName {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Self::Openfabric(r) => r.fmt(f),
- Self::Ospf(r) => r.fmt(f),
- }
- }
-}
-
-/// The interface name is the same on ospf and openfabric, but it is an enum so that we can have
-/// two different entries in the btreemap. This allows us to have an interface in a ospf and
-/// openfabric fabric.
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
-pub enum InterfaceName {
- Openfabric(CommonInterfaceName),
- Ospf(CommonInterfaceName),
-}
-
-impl Display for InterfaceName {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- InterfaceName::Openfabric(frr_word) => frr_word.fmt(f),
- InterfaceName::Ospf(frr_word) => frr_word.fmt(f),
- }
- }
-}
-
-/// Generic FRR Interface.
-///
-/// In FRR config it looks like this:
-/// ```text
-/// interface <name>
-/// ! ...
-#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
-pub enum Interface {
- Openfabric(openfabric::OpenfabricInterface),
- Ospf(ospf::OspfInterface),
-}
-
-impl From<openfabric::OpenfabricInterface> for Interface {
- fn from(value: openfabric::OpenfabricInterface) -> Self {
- Self::Openfabric(value)
- }
-}
-
-impl From<ospf::OspfInterface> for Interface {
- fn from(value: ospf::OspfInterface) -> Self {
- Self::Ospf(value)
- }
-}
-
-#[derive(Error, Debug)]
-pub enum FrrWordError {
- #[error("word is empty")]
- IsEmpty,
- #[error("word contains invalid character")]
- InvalidCharacter,
-}
-
-/// A simple FRR Word.
-///
-/// Every string argument or value in FRR is an FrrWord. FrrWords must only contain ascii
-/// characters and must not have a whitespace.
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
-pub struct FrrWord(String);
-
-impl FrrWord {
- pub fn new<T: AsRef<str> + Into<String>>(name: T) -> Result<Self, FrrWordError> {
- if name.as_ref().is_empty() {
- return Err(FrrWordError::IsEmpty);
- }
-
- if name
- .as_ref()
- .as_bytes()
- .iter()
- .any(|c| !c.is_ascii() || c.is_ascii_whitespace())
- {
- eprintln!("invalid char in: \"{}\"", name.as_ref());
- return Err(FrrWordError::InvalidCharacter);
- }
-
- Ok(Self(name.into()))
- }
-}
-
-impl FromStr for FrrWord {
- type Err = FrrWordError;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- Self::new(s)
- }
-}
-
-impl Display for FrrWord {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- self.0.fmt(f)
- }
-}
-
-impl AsRef<str> for FrrWord {
- fn as_ref(&self) -> &str {
- &self.0
- }
-}
-
-#[derive(Error, Debug)]
-pub enum CommonInterfaceNameError {
- #[error("interface name too long")]
- TooLong,
-}
-
-/// Name of a interface, which is common between all protocols.
-///
-/// FRR itself doesn't enforce any limits, but the kernel does. Linux only allows interface names
-/// to be a maximum of 16 bytes. This is enforced by this struct.
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
-pub struct CommonInterfaceName(String);
-
-impl TryFrom<&str> for CommonInterfaceName {
- type Error = CommonInterfaceNameError;
-
- fn try_from(value: &str) -> Result<Self, Self::Error> {
- Self::new(value)
- }
-}
-
-impl TryFrom<String> for CommonInterfaceName {
- type Error = CommonInterfaceNameError;
-
- fn try_from(value: String) -> Result<Self, Self::Error> {
- Self::new(value)
- }
-}
-
-impl CommonInterfaceName {
- pub fn new<T: AsRef<str> + Into<String>>(s: T) -> Result<Self, CommonInterfaceNameError> {
- if s.as_ref().len() <= 15 {
- Ok(Self(s.into()))
- } else {
- Err(CommonInterfaceNameError::TooLong)
- }
- }
-}
-
-impl Display for CommonInterfaceName {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- self.0.fmt(f)
- }
-}
-
-/// Main FRR config.
-///
-/// Contains the two main frr building blocks: routers and interfaces. It also holds other
-/// top-level FRR options, such as access-lists, router-maps and protocol-routemaps. This struct
-/// gets generated using the `FrrConfigBuilder` in `proxmox-ve-config`.
-#[derive(Clone, Debug, PartialEq, Eq, Default)]
-pub struct FrrConfig {
- pub router: BTreeMap<RouterName, Router>,
- pub interfaces: BTreeMap<InterfaceName, Interface>,
- pub access_lists: Vec<AccessList>,
- pub routemaps: Vec<RouteMap>,
- pub protocol_routemaps: BTreeSet<ProtocolRouteMap>,
-}
-
-impl FrrConfig {
- pub fn new() -> Self {
- Self::default()
- }
-
- pub fn router(&self) -> impl Iterator<Item = (&RouterName, &Router)> + '_ {
- self.router.iter()
- }
-
- pub fn interfaces(&self) -> impl Iterator<Item = (&InterfaceName, &Interface)> + '_ {
- self.interfaces.iter()
- }
-
- pub fn access_lists(&self) -> impl Iterator<Item = &AccessList> + '_ {
- self.access_lists.iter()
- }
- pub fn routemaps(&self) -> impl Iterator<Item = &RouteMap> + '_ {
- self.routemaps.iter()
- }
-
- pub fn protocol_routemaps(&self) -> impl Iterator<Item = &ProtocolRouteMap> + '_ {
- self.protocol_routemaps.iter()
- }
-}
+pub mod ser;
diff --git a/proxmox-frr/src/ser/mod.rs b/proxmox-frr/src/ser/mod.rs
new file mode 100644
index 000000000000..a90397b59a9b
--- /dev/null
+++ b/proxmox-frr/src/ser/mod.rs
@@ -0,0 +1,241 @@
+pub mod openfabric;
+pub mod ospf;
+pub mod route_map;
+pub mod serializer;
+
+use std::collections::{BTreeMap, BTreeSet};
+use std::fmt::Display;
+use std::str::FromStr;
+
+use crate::ser::route_map::{AccessList, ProtocolRouteMap, RouteMap};
+
+use thiserror::Error;
+
+/// Generic FRR router.
+///
+/// This generic FRR router contains all the protocols that we implement.
+/// In FRR this is e.g.:
+/// ```text
+/// router openfabric test
+/// !....
+/// ! or
+/// router ospf
+/// !....
+/// ```
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub enum Router {
+ Openfabric(openfabric::OpenfabricRouter),
+ Ospf(ospf::OspfRouter),
+}
+
+impl From<openfabric::OpenfabricRouter> for Router {
+ fn from(value: openfabric::OpenfabricRouter) -> Self {
+ Router::Openfabric(value)
+ }
+}
+
+/// Generic FRR routername.
+///
+/// The variants represent different protocols. Some have `router <protocol> <name>`, others have
+/// `router <protocol> <process-id>`, some only have `router <protocol>`.
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub enum RouterName {
+ Openfabric(openfabric::OpenfabricRouterName),
+ Ospf(ospf::OspfRouterName),
+}
+
+impl From<openfabric::OpenfabricRouterName> for RouterName {
+ fn from(value: openfabric::OpenfabricRouterName) -> Self {
+ Self::Openfabric(value)
+ }
+}
+
+impl Display for RouterName {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::Openfabric(r) => r.fmt(f),
+ Self::Ospf(r) => r.fmt(f),
+ }
+ }
+}
+
+/// The interface name is the same on ospf and openfabric, but it is an enum so that we can have
+/// two different entries in the btreemap. This allows us to have an interface in a ospf and
+/// openfabric fabric.
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub enum InterfaceName {
+ Openfabric(CommonInterfaceName),
+ Ospf(CommonInterfaceName),
+}
+
+impl Display for InterfaceName {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ InterfaceName::Openfabric(frr_word) => frr_word.fmt(f),
+ InterfaceName::Ospf(frr_word) => frr_word.fmt(f),
+ }
+ }
+}
+
+/// Generic FRR Interface.
+///
+/// In FRR config it looks like this:
+/// ```text
+/// interface <name>
+/// ! ...
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub enum Interface {
+ Openfabric(openfabric::OpenfabricInterface),
+ Ospf(ospf::OspfInterface),
+}
+
+impl From<openfabric::OpenfabricInterface> for Interface {
+ fn from(value: openfabric::OpenfabricInterface) -> Self {
+ Self::Openfabric(value)
+ }
+}
+
+impl From<ospf::OspfInterface> for Interface {
+ fn from(value: ospf::OspfInterface) -> Self {
+ Self::Ospf(value)
+ }
+}
+
+#[derive(Error, Debug)]
+pub enum FrrWordError {
+ #[error("word is empty")]
+ IsEmpty,
+ #[error("word contains invalid character")]
+ InvalidCharacter,
+}
+
+/// A simple FRR Word.
+///
+/// Every string argument or value in FRR is an FrrWord. FrrWords must only contain ascii
+/// characters and must not have a whitespace.
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct FrrWord(String);
+
+impl FrrWord {
+ pub fn new<T: AsRef<str> + Into<String>>(name: T) -> Result<Self, FrrWordError> {
+ if name.as_ref().is_empty() {
+ return Err(FrrWordError::IsEmpty);
+ }
+
+ if name
+ .as_ref()
+ .as_bytes()
+ .iter()
+ .any(|c| !c.is_ascii() || c.is_ascii_whitespace())
+ {
+ eprintln!("invalid char in: \"{}\"", name.as_ref());
+ return Err(FrrWordError::InvalidCharacter);
+ }
+
+ Ok(Self(name.into()))
+ }
+}
+
+impl FromStr for FrrWord {
+ type Err = FrrWordError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ Self::new(s)
+ }
+}
+
+impl Display for FrrWord {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+impl AsRef<str> for FrrWord {
+ fn as_ref(&self) -> &str {
+ &self.0
+ }
+}
+
+#[derive(Error, Debug)]
+pub enum CommonInterfaceNameError {
+ #[error("interface name too long")]
+ TooLong,
+}
+
+/// Name of a interface, which is common between all protocols.
+///
+/// FRR itself doesn't enforce any limits, but the kernel does. Linux only allows interface names
+/// to be a maximum of 16 bytes. This is enforced by this struct.
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct CommonInterfaceName(String);
+
+impl TryFrom<&str> for CommonInterfaceName {
+ type Error = CommonInterfaceNameError;
+
+ fn try_from(value: &str) -> Result<Self, Self::Error> {
+ Self::new(value)
+ }
+}
+
+impl TryFrom<String> for CommonInterfaceName {
+ type Error = CommonInterfaceNameError;
+
+ fn try_from(value: String) -> Result<Self, Self::Error> {
+ Self::new(value)
+ }
+}
+
+impl CommonInterfaceName {
+ pub fn new<T: AsRef<str> + Into<String>>(s: T) -> Result<Self, CommonInterfaceNameError> {
+ if s.as_ref().len() <= 15 {
+ Ok(Self(s.into()))
+ } else {
+ Err(CommonInterfaceNameError::TooLong)
+ }
+ }
+}
+
+impl Display for CommonInterfaceName {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+/// Main FRR config.
+///
+/// Contains the two main frr building blocks: routers and interfaces. It also holds other
+/// top-level FRR options, such as access-lists, router-maps and protocol-routemaps. This struct
+/// gets generated using the `FrrConfigBuilder` in `proxmox-ve-config`.
+#[derive(Clone, Debug, PartialEq, Eq, Default)]
+pub struct FrrConfig {
+ pub router: BTreeMap<RouterName, Router>,
+ pub interfaces: BTreeMap<InterfaceName, Interface>,
+ pub access_lists: Vec<AccessList>,
+ pub routemaps: Vec<RouteMap>,
+ pub protocol_routemaps: BTreeSet<ProtocolRouteMap>,
+}
+
+impl FrrConfig {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ pub fn router(&self) -> impl Iterator<Item = (&RouterName, &Router)> + '_ {
+ self.router.iter()
+ }
+
+ pub fn interfaces(&self) -> impl Iterator<Item = (&InterfaceName, &Interface)> + '_ {
+ self.interfaces.iter()
+ }
+
+ pub fn access_lists(&self) -> impl Iterator<Item = &AccessList> + '_ {
+ self.access_lists.iter()
+ }
+ pub fn routemaps(&self) -> impl Iterator<Item = &RouteMap> + '_ {
+ self.routemaps.iter()
+ }
+
+ pub fn protocol_routemaps(&self) -> impl Iterator<Item = &ProtocolRouteMap> + '_ {
+ self.protocol_routemaps.iter()
+ }
+}
diff --git a/proxmox-frr/src/openfabric.rs b/proxmox-frr/src/ser/openfabric.rs
similarity index 97%
rename from proxmox-frr/src/openfabric.rs
rename to proxmox-frr/src/ser/openfabric.rs
index 6e2a7200ab37..0f0c65062d36 100644
--- a/proxmox-frr/src/openfabric.rs
+++ b/proxmox-frr/src/ser/openfabric.rs
@@ -5,8 +5,8 @@ use proxmox_sdn_types::net::Net;
use thiserror::Error;
-use crate::FrrWord;
-use crate::FrrWordError;
+use crate::ser::FrrWord;
+use crate::ser::FrrWordError;
/// The name of a OpenFabric router. Is an FrrWord.
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
diff --git a/proxmox-frr/src/ospf.rs b/proxmox-frr/src/ser/ospf.rs
similarity index 99%
rename from proxmox-frr/src/ospf.rs
rename to proxmox-frr/src/ser/ospf.rs
index d0e098e099d2..67e39a45b8de 100644
--- a/proxmox-frr/src/ospf.rs
+++ b/proxmox-frr/src/ser/ospf.rs
@@ -4,7 +4,7 @@ use std::net::Ipv4Addr;
use thiserror::Error;
-use crate::{FrrWord, FrrWordError};
+use crate::ser::{FrrWord, FrrWordError};
/// The name of the ospf frr router.
///
diff --git a/proxmox-frr/src/route_map.rs b/proxmox-frr/src/ser/route_map.rs
similarity index 100%
rename from proxmox-frr/src/route_map.rs
rename to proxmox-frr/src/ser/route_map.rs
diff --git a/proxmox-frr/src/serializer.rs b/proxmox-frr/src/ser/serializer.rs
similarity index 99%
rename from proxmox-frr/src/serializer.rs
rename to proxmox-frr/src/ser/serializer.rs
index f8a3c7238d94..3a681e2f0d7a 100644
--- a/proxmox-frr/src/serializer.rs
+++ b/proxmox-frr/src/ser/serializer.rs
@@ -1,6 +1,6 @@
use std::fmt::{self, Write};
-use crate::{
+use crate::ser::{
openfabric::{OpenfabricInterface, OpenfabricRouter},
ospf::{OspfInterface, OspfRouter},
route_map::{AccessList, AccessListName, ProtocolRouteMap, RouteMap},
diff --git a/proxmox-ve-config/src/sdn/fabric/frr.rs b/proxmox-ve-config/src/sdn/fabric/frr.rs
index 486f7dc51dcb..10025b3544b9 100644
--- a/proxmox-ve-config/src/sdn/fabric/frr.rs
+++ b/proxmox-ve-config/src/sdn/fabric/frr.rs
@@ -1,12 +1,7 @@
use std::net::{IpAddr, Ipv4Addr};
use tracing;
-use proxmox_frr::ospf::{self, NetworkType};
-use proxmox_frr::route_map::{
- AccessAction, AccessList, AccessListName, AccessListRule, ProtocolRouteMap, ProtocolType,
- RouteMap, RouteMapMatch, RouteMapMatchInner, RouteMapName, RouteMapSet,
-};
-use proxmox_frr::{FrrConfig, FrrWord, Interface, InterfaceName, Router, RouterName};
+use proxmox_frr::ser::{self};
use proxmox_network_types::ip_address::Cidr;
use proxmox_sdn_types::net::Net;
@@ -26,7 +21,7 @@ use crate::sdn::fabric::{FabricConfig, FabricEntry};
pub fn build_fabric(
current_node: NodeId,
config: Valid<FabricConfig>,
- frr_config: &mut FrrConfig,
+ frr_config: &mut ser::FrrConfig,
) -> Result<(), anyhow::Error> {
let mut routemap_seq = 100;
let mut current_router_id: Option<Ipv4Addr> = None;
@@ -93,27 +88,31 @@ pub fn build_fabric(
}
if let Some(ipv4cidr) = fabric.ip_prefix() {
- let rule = AccessListRule {
- action: AccessAction::Permit,
+ let rule = ser::route_map::AccessListRule {
+ action: ser::route_map::AccessAction::Permit,
network: Cidr::from(ipv4cidr),
seq: None,
};
- let access_list_name =
- AccessListName::new(format!("pve_openfabric_{}_ips", fabric_id));
- frr_config.access_lists.push(AccessList {
+ let access_list_name = ser::route_map::AccessListName::new(format!(
+ "pve_openfabric_{}_ips",
+ fabric_id
+ ));
+ frr_config.access_lists.push(ser::route_map::AccessList {
name: access_list_name,
rules: vec![rule],
});
}
if let Some(ipv6cidr) = fabric.ip6_prefix() {
- let rule = AccessListRule {
- action: AccessAction::Permit,
+ let rule = ser::route_map::AccessListRule {
+ action: ser::route_map::AccessAction::Permit,
network: Cidr::from(ipv6cidr),
seq: None,
};
- let access_list_name =
- AccessListName::new(format!("pve_openfabric_{}_ip6s", fabric_id));
- frr_config.access_lists.push(AccessList {
+ let access_list_name = ser::route_map::AccessListName::new(format!(
+ "pve_openfabric_{}_ip6s",
+ fabric_id
+ ));
+ frr_config.access_lists.push(ser::route_map::AccessList {
name: access_list_name,
rules: vec![rule],
});
@@ -128,10 +127,12 @@ pub fn build_fabric(
));
routemap_seq += 10;
- let protocol_routemap = ProtocolRouteMap {
+ let protocol_routemap = ser::route_map::ProtocolRouteMap {
is_ipv6: false,
- protocol: ProtocolType::Openfabric,
- routemap_name: RouteMapName::new("pve_openfabric".to_owned()),
+ protocol: ser::route_map::ProtocolType::Openfabric,
+ routemap_name: ser::route_map::RouteMapName::new(
+ "pve_openfabric".to_owned(),
+ ),
};
frr_config.protocol_routemaps.insert(protocol_routemap);
@@ -145,10 +146,12 @@ pub fn build_fabric(
));
routemap_seq += 10;
- let protocol_routemap = ProtocolRouteMap {
+ let protocol_routemap = ser::route_map::ProtocolRouteMap {
is_ipv6: true,
- protocol: ProtocolType::Openfabric,
- routemap_name: RouteMapName::new("pve_openfabric6".to_owned()),
+ protocol: ser::route_map::ProtocolType::Openfabric,
+ routemap_name: ser::route_map::RouteMapName::new(
+ "pve_openfabric6".to_owned(),
+ ),
};
frr_config.protocol_routemaps.insert(protocol_routemap);
@@ -164,8 +167,8 @@ pub fn build_fabric(
let fabric = ospf_entry.fabric_section();
- let frr_word_area = FrrWord::new(fabric.properties().area.to_string())?;
- let frr_area = ospf::Area::new(frr_word_area)?;
+ let frr_word_area = ser::FrrWord::new(fabric.properties().area.to_string())?;
+ let frr_area = ser::ospf::Area::new(frr_word_area)?;
let (router_name, router_item) = build_ospf_router(*router_id)?;
frr_config.router.insert(router_name, router_item);
@@ -196,17 +199,18 @@ pub fn build_fabric(
}
}
- let access_list_name = AccessListName::new(format!("pve_ospf_{}_ips", fabric_id));
+ let access_list_name =
+ ser::route_map::AccessListName::new(format!("pve_ospf_{}_ips", fabric_id));
- let rule = AccessListRule {
- action: AccessAction::Permit,
+ let rule = ser::route_map::AccessListRule {
+ action: ser::route_map::AccessAction::Permit,
network: Cidr::from(
fabric.ip_prefix().expect("fabric must have a ipv4 prefix"),
),
seq: None,
};
- frr_config.access_lists.push(AccessList {
+ frr_config.access_lists.push(ser::route_map::AccessList {
name: access_list_name,
rules: vec![rule],
});
@@ -220,10 +224,10 @@ pub fn build_fabric(
routemap_seq += 10;
frr_config.routemaps.push(routemap);
- let protocol_routemap = ProtocolRouteMap {
+ let protocol_routemap = ser::route_map::ProtocolRouteMap {
is_ipv6: false,
- protocol: ProtocolType::Ospf,
- routemap_name: RouteMapName::new("pve_ospf".to_owned()),
+ protocol: ser::route_map::ProtocolType::Ospf,
+ routemap_name: ser::route_map::RouteMapName::new("pve_ospf".to_owned()),
};
frr_config.protocol_routemaps.insert(protocol_routemap);
@@ -234,10 +238,10 @@ pub fn build_fabric(
}
/// Helper that builds a OSPF router with a the router_id.
-fn build_ospf_router(router_id: Ipv4Addr) -> Result<(RouterName, Router), anyhow::Error> {
- let ospf_router = proxmox_frr::ospf::OspfRouter { router_id };
- let router_item = Router::Ospf(ospf_router);
- let router_name = RouterName::Ospf(proxmox_frr::ospf::OspfRouterName);
+fn build_ospf_router(router_id: Ipv4Addr) -> Result<(ser::RouterName, ser::Router), anyhow::Error> {
+ let ospf_router = ser::ospf::OspfRouter { router_id };
+ let router_item = ser::Router::Ospf(ospf_router);
+ let router_name = ser::RouterName::Ospf(ser::ospf::OspfRouterName);
Ok((router_name, router_item))
}
@@ -245,45 +249,45 @@ fn build_ospf_router(router_id: Ipv4Addr) -> Result<(RouterName, Router), anyhow
fn build_openfabric_router(
fabric_id: &FabricId,
net: Net,
-) -> Result<(RouterName, Router), anyhow::Error> {
- let ofr = proxmox_frr::openfabric::OpenfabricRouter { net };
- let router_item = Router::Openfabric(ofr);
- let frr_word_id = FrrWord::new(fabric_id.to_string())?;
- let router_name = RouterName::Openfabric(frr_word_id.into());
+) -> Result<(ser::RouterName, ser::Router), anyhow::Error> {
+ let ofr = ser::openfabric::OpenfabricRouter { net };
+ let router_item = ser::Router::Openfabric(ofr);
+ let frr_word_id = ser::FrrWord::new(fabric_id.to_string())?;
+ let router_name = ser::RouterName::Openfabric(frr_word_id.into());
Ok((router_name, router_item))
}
/// Helper that builds a OSPF interface from an [`ospf::Area`] and the [`OspfInterfaceProperties`].
fn build_ospf_interface(
- area: ospf::Area,
+ area: ser::ospf::Area,
interface: &OspfInterfaceProperties,
-) -> Result<(Interface, InterfaceName), anyhow::Error> {
- let frr_interface = proxmox_frr::ospf::OspfInterface {
+) -> Result<(ser::Interface, ser::InterfaceName), anyhow::Error> {
+ let frr_interface = ser::ospf::OspfInterface {
area,
// Interfaces are always none-passive
passive: None,
network_type: if interface.ip.is_some() {
None
} else {
- Some(NetworkType::PointToPoint)
+ Some(ser::ospf::NetworkType::PointToPoint)
},
};
- let interface_name = InterfaceName::Ospf(interface.name.as_str().try_into()?);
+ let interface_name = ser::InterfaceName::Ospf(interface.name.as_str().try_into()?);
Ok((frr_interface.into(), interface_name))
}
/// Helper that builds the OSPF dummy interface using the [`FabricId`] and the [`ospf::Area`].
fn build_ospf_dummy_interface(
fabric_id: &FabricId,
- area: ospf::Area,
-) -> Result<(Interface, InterfaceName), anyhow::Error> {
- let frr_interface = proxmox_frr::ospf::OspfInterface {
+ area: ser::ospf::Area,
+) -> Result<(ser::Interface, ser::InterfaceName), anyhow::Error> {
+ let frr_interface = ser::ospf::OspfInterface {
area,
passive: Some(true),
network_type: None,
};
- let interface_name = InterfaceName::Openfabric(format!("dummy_{}", fabric_id).try_into()?);
+ let interface_name = ser::InterfaceName::Openfabric(format!("dummy_{}", fabric_id).try_into()?);
Ok((frr_interface.into(), interface_name))
}
@@ -297,9 +301,9 @@ fn build_openfabric_interface(
fabric_config: &OpenfabricProperties,
is_ipv4: bool,
is_ipv6: bool,
-) -> Result<(Interface, InterfaceName), anyhow::Error> {
- let frr_word = FrrWord::new(fabric_id.to_string())?;
- let mut frr_interface = proxmox_frr::openfabric::OpenfabricInterface {
+) -> Result<(ser::Interface, ser::InterfaceName), anyhow::Error> {
+ let frr_word = ser::FrrWord::new(fabric_id.to_string())?;
+ let mut frr_interface = ser::openfabric::OpenfabricInterface {
fabric_id: frr_word.into(),
// Every interface is not passive by default
passive: None,
@@ -315,7 +319,7 @@ fn build_openfabric_interface(
if frr_interface.hello_interval.is_none() {
frr_interface.hello_interval = fabric_config.hello_interval;
}
- let interface_name = InterfaceName::Openfabric(interface.name.as_str().try_into()?);
+ let interface_name = ser::InterfaceName::Openfabric(interface.name.as_str().try_into()?);
Ok((frr_interface.into(), interface_name))
}
@@ -324,9 +328,9 @@ fn build_openfabric_dummy_interface(
fabric_id: &FabricId,
is_ipv4: bool,
is_ipv6: bool,
-) -> Result<(Interface, InterfaceName), anyhow::Error> {
- let frr_word = FrrWord::new(fabric_id.to_string())?;
- let frr_interface = proxmox_frr::openfabric::OpenfabricInterface {
+) -> Result<(ser::Interface, ser::InterfaceName), anyhow::Error> {
+ let frr_word = ser::FrrWord::new(fabric_id.to_string())?;
+ let frr_interface = ser::openfabric::OpenfabricInterface {
fabric_id: frr_word.into(),
hello_interval: None,
passive: Some(true),
@@ -335,29 +339,37 @@ fn build_openfabric_dummy_interface(
is_ipv4,
is_ipv6,
};
- let interface_name = InterfaceName::Openfabric(format!("dummy_{}", fabric_id).try_into()?);
+ let interface_name = ser::InterfaceName::Openfabric(format!("dummy_{}", fabric_id).try_into()?);
Ok((frr_interface.into(), interface_name))
}
/// Helper that builds a RouteMap for the OpenFabric protocol.
-fn build_openfabric_routemap(fabric_id: &FabricId, router_ip: IpAddr, seq: u32) -> RouteMap {
+fn build_openfabric_routemap(
+ fabric_id: &FabricId,
+ router_ip: IpAddr,
+ seq: u32,
+) -> ser::route_map::RouteMap {
let routemap_name = match router_ip {
- IpAddr::V4(_) => RouteMapName::new("pve_openfabric".to_owned()),
- IpAddr::V6(_) => RouteMapName::new("pve_openfabric6".to_owned()),
+ IpAddr::V4(_) => ser::route_map::RouteMapName::new("pve_openfabric".to_owned()),
+ IpAddr::V6(_) => ser::route_map::RouteMapName::new("pve_openfabric6".to_owned()),
};
- RouteMap {
+ ser::route_map::RouteMap {
name: routemap_name.clone(),
seq,
- action: AccessAction::Permit,
+ action: ser::route_map::AccessAction::Permit,
matches: vec![match router_ip {
- IpAddr::V4(_) => RouteMapMatch::V4(RouteMapMatchInner::IpAddress(AccessListName::new(
- format!("pve_openfabric_{fabric_id}_ips"),
- ))),
- IpAddr::V6(_) => RouteMapMatch::V6(RouteMapMatchInner::IpAddress(AccessListName::new(
- format!("pve_openfabric_{fabric_id}_ip6s"),
- ))),
+ IpAddr::V4(_) => {
+ ser::route_map::RouteMapMatch::V4(ser::route_map::RouteMapMatchInner::IpAddress(
+ ser::route_map::AccessListName::new(format!("pve_openfabric_{fabric_id}_ips")),
+ ))
+ }
+ IpAddr::V6(_) => {
+ ser::route_map::RouteMapMatch::V6(ser::route_map::RouteMapMatchInner::IpAddress(
+ ser::route_map::AccessListName::new(format!("pve_openfabric_{fabric_id}_ip6s")),
+ ))
+ }
}],
- sets: vec![RouteMapSet::IpSrc(router_ip)],
+ sets: vec![ser::route_map::RouteMapSet::IpSrc(router_ip)],
}
}
@@ -366,17 +378,19 @@ fn build_ospf_dummy_routemap(
fabric_id: &FabricId,
router_ip: Ipv4Addr,
seq: u32,
-) -> Result<RouteMap, anyhow::Error> {
- let routemap_name = RouteMapName::new("pve_ospf".to_owned());
+) -> Result<ser::route_map::RouteMap, anyhow::Error> {
+ let routemap_name = ser::route_map::RouteMapName::new("pve_ospf".to_owned());
// create route-map
- let routemap = RouteMap {
+ let routemap = ser::route_map::RouteMap {
name: routemap_name.clone(),
seq,
- action: AccessAction::Permit,
- matches: vec![RouteMapMatch::V4(RouteMapMatchInner::IpAddress(
- AccessListName::new(format!("pve_ospf_{fabric_id}_ips")),
- ))],
- sets: vec![RouteMapSet::IpSrc(IpAddr::from(router_ip))],
+ action: ser::route_map::AccessAction::Permit,
+ matches: vec![ser::route_map::RouteMapMatch::V4(
+ ser::route_map::RouteMapMatchInner::IpAddress(ser::route_map::AccessListName::new(
+ format!("pve_ospf_{fabric_id}_ips"),
+ )),
+ )],
+ sets: vec![ser::route_map::RouteMapSet::IpSrc(IpAddr::from(router_ip))],
};
Ok(routemap)
diff --git a/proxmox-ve-config/src/sdn/frr.rs b/proxmox-ve-config/src/sdn/frr.rs
index f7929c1f6c16..5d4e4b2ebdbd 100644
--- a/proxmox-ve-config/src/sdn/frr.rs
+++ b/proxmox-ve-config/src/sdn/frr.rs
@@ -1,6 +1,6 @@
use std::collections::{BTreeMap, BTreeSet};
-use proxmox_frr::FrrConfig;
+use proxmox_frr::ser::FrrConfig;
use crate::common::valid::Valid;
use crate::sdn::fabric::{section_config::node::NodeId, FabricConfig};
diff --git a/proxmox-ve-config/tests/fabric/main.rs b/proxmox-ve-config/tests/fabric/main.rs
index 47bbbeb77886..09629d406449 100644
--- a/proxmox-ve-config/tests/fabric/main.rs
+++ b/proxmox-ve-config/tests/fabric/main.rs
@@ -1,5 +1,5 @@
#![cfg(feature = "frr")]
-use proxmox_frr::serializer::dump;
+use proxmox_frr::ser::serializer::dump;
use proxmox_ve_config::sdn::{
fabric::{section_config::node::NodeId, FabricConfig},
frr::FrrConfigBuilder,
--
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] 14+ messages in thread
* [pve-devel] [PATCH proxmox-ve-rs v3 2/3] frr: add deserialization types for openfabric and ospf
2025-08-26 9:49 [pve-devel] [PATCH manager/network/proxmox{-ve-rs, -perl-rs} v3 00/13] Add fabric status view Gabriel Goller
2025-08-26 9:49 ` [pve-devel] [PATCH proxmox-ve-rs v3 1/3] frr: make room for deserialization structs Gabriel Goller
@ 2025-08-26 9:49 ` Gabriel Goller
2025-08-26 9:49 ` [pve-devel] [PATCH proxmox-ve-rs v3 3/3] ve-config: add helper function to iterate over all nodes in all fabrics Gabriel Goller
` (10 subsequent siblings)
12 siblings, 0 replies; 14+ messages in thread
From: Gabriel Goller @ 2025-08-26 9:49 UTC (permalink / raw)
To: pve-devel
These are used to deserialize `vtysh` command outputs. The output is in
json if the `json` parameter is appended to the command. Currently the
following commands are parsed:
* show openfabric neighbor
* show ip ospf neighbor
* show ip route <protocol>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
proxmox-frr/Cargo.toml | 1 +
proxmox-frr/debian/control | 4 ++
proxmox-frr/src/de/mod.rs | 104 +++++++++++++++++++++++++++++++
proxmox-frr/src/de/openfabric.rs | 42 +++++++++++++
proxmox-frr/src/de/ospf.rs | 57 +++++++++++++++++
proxmox-frr/src/lib.rs | 1 +
6 files changed, 209 insertions(+)
create mode 100644 proxmox-frr/src/de/mod.rs
create mode 100644 proxmox-frr/src/de/openfabric.rs
create mode 100644 proxmox-frr/src/de/ospf.rs
diff --git a/proxmox-frr/Cargo.toml b/proxmox-frr/Cargo.toml
index 47fb8bb3969c..d1a24a899b55 100644
--- a/proxmox-frr/Cargo.toml
+++ b/proxmox-frr/Cargo.toml
@@ -13,6 +13,7 @@ rust-version.workspace = true
thiserror = { workspace = true }
anyhow = "1"
tracing = "0.1"
+serde = { workspace = true, features = [ "derive" ] }
proxmox-network-types = { workspace = true }
proxmox-sdn-types = { workspace = true }
diff --git a/proxmox-frr/debian/control b/proxmox-frr/debian/control
index 544fc3ec9ec4..aa74860f2b2f 100644
--- a/proxmox-frr/debian/control
+++ b/proxmox-frr/debian/control
@@ -9,6 +9,8 @@ Build-Depends-Arch: cargo:native <!nocheck>,
librust-anyhow-1+default-dev <!nocheck>,
librust-proxmox-network-types-0.1+default-dev (>= 0.1.1-~~) <!nocheck>,
librust-proxmox-sdn-types-0.1+default-dev <!nocheck>,
+ librust-serde-1+default-dev <!nocheck>,
+ librust-serde-1+derive-dev <!nocheck>,
librust-thiserror-2+default-dev <!nocheck>,
librust-tracing-0.1+default-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
@@ -27,6 +29,8 @@ Depends:
librust-anyhow-1+default-dev,
librust-proxmox-network-types-0.1+default-dev (>= 0.1.1-~~),
librust-proxmox-sdn-types-0.1+default-dev,
+ librust-serde-1+default-dev,
+ librust-serde-1+derive-dev,
librust-thiserror-2+default-dev,
librust-tracing-0.1+default-dev
Provides:
diff --git a/proxmox-frr/src/de/mod.rs b/proxmox-frr/src/de/mod.rs
new file mode 100644
index 000000000000..43890506253d
--- /dev/null
+++ b/proxmox-frr/src/de/mod.rs
@@ -0,0 +1,104 @@
+use std::{collections::HashMap, net::IpAddr};
+
+use proxmox_network_types::ip_address::Cidr;
+use serde::{Deserialize, Serialize};
+
+pub mod openfabric;
+pub mod ospf;
+
+/// 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 nexthop
+ 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>>);
diff --git a/proxmox-frr/src/de/openfabric.rs b/proxmox-frr/src/de/openfabric.rs
new file mode 100644
index 000000000000..99d281f24bcd
--- /dev/null
+++ b/proxmox-frr/src/de/openfabric.rs
@@ -0,0 +1,42 @@
+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>,
+}
diff --git a/proxmox-frr/src/de/ospf.rs b/proxmox-frr/src/de/ospf.rs
new file mode 100644
index 000000000000..0e813ff1e614
--- /dev/null
+++ b/proxmox-frr/src/de/ospf.rs
@@ -0,0 +1,57 @@
+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>>,
+}
diff --git a/proxmox-frr/src/lib.rs b/proxmox-frr/src/lib.rs
index 35b62cb39c91..2e6ab62c6119 100644
--- a/proxmox-frr/src/lib.rs
+++ b/proxmox-frr/src/lib.rs
@@ -1 +1,2 @@
+pub mod de;
pub mod ser;
--
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] 14+ messages in thread
* [pve-devel] [PATCH proxmox-ve-rs v3 3/3] ve-config: add helper function to iterate over all nodes in all fabrics
2025-08-26 9:49 [pve-devel] [PATCH manager/network/proxmox{-ve-rs, -perl-rs} v3 00/13] Add fabric status view Gabriel Goller
2025-08-26 9:49 ` [pve-devel] [PATCH proxmox-ve-rs v3 1/3] frr: make room for deserialization structs Gabriel Goller
2025-08-26 9:49 ` [pve-devel] [PATCH proxmox-ve-rs v3 2/3] frr: add deserialization types for openfabric and ospf Gabriel Goller
@ 2025-08-26 9:49 ` Gabriel Goller
2025-08-26 9:49 ` [pve-devel] [PATCH proxmox-perl-rs v3 1/4] pve: fabrics: update proxmox-frr import path Gabriel Goller
` (9 subsequent siblings)
12 siblings, 0 replies; 14+ messages in thread
From: Gabriel Goller @ 2025-08-26 9:49 UTC (permalink / raw)
To: pve-devel
Add helper function to iterate over all nodes in all fabrics. This is
especially useful e.g. when retrieving the stats as we need to find all
the fabrics on the current node (and their node config).
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
proxmox-ve-config/src/sdn/fabric/mod.rs | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/proxmox-ve-config/src/sdn/fabric/mod.rs b/proxmox-ve-config/src/sdn/fabric/mod.rs
index 58a06f9423cb..677a30976297 100644
--- a/proxmox-ve-config/src/sdn/fabric/mod.rs
+++ b/proxmox-ve-config/src/sdn/fabric/mod.rs
@@ -618,6 +618,11 @@ impl FabricConfig {
.ok_or_else(|| FabricConfigError::FabricDoesNotExist(id.to_string()))
}
+ /// Get an iterator over all the nodes in all fabrics.
+ pub fn all_nodes(&self) -> impl Iterator<Item = (&NodeId, &Node)> + '_ {
+ self.values().flat_map(|entry| entry.nodes())
+ }
+
/// Returns an iterator over mutable references to all [`FabricEntry`] in the config
pub fn get_fabrics_mut(&mut self) -> impl Iterator<Item = &mut FabricEntry> {
self.fabrics.values_mut()
--
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] 14+ messages in thread
* [pve-devel] [PATCH proxmox-perl-rs v3 1/4] pve: fabrics: update proxmox-frr import path
2025-08-26 9:49 [pve-devel] [PATCH manager/network/proxmox{-ve-rs, -perl-rs} v3 00/13] Add fabric status view Gabriel Goller
` (2 preceding siblings ...)
2025-08-26 9:49 ` [pve-devel] [PATCH proxmox-ve-rs v3 3/3] ve-config: add helper function to iterate over all nodes in all fabrics Gabriel Goller
@ 2025-08-26 9:49 ` Gabriel Goller
2025-08-26 9:49 ` [pve-devel] [PATCH proxmox-perl-rs v3 2/4] fabrics: add function to get status of fabric Gabriel Goller
` (8 subsequent siblings)
12 siblings, 0 replies; 14+ messages in thread
From: Gabriel Goller @ 2025-08-26 9:49 UTC (permalink / raw)
To: pve-devel
Update the proxmox-frr import path to the new `ser` and `de` subfolders.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
pve-rs/src/bindings/sdn/fabrics.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
index 587b1d68c8fb..1dc8bf4320e6 100644
--- a/pve-rs/src/bindings/sdn/fabrics.rs
+++ b/pve-rs/src/bindings/sdn/fabrics.rs
@@ -16,7 +16,7 @@ pub mod pve_rs_sdn_fabrics {
use serde::{Deserialize, Serialize};
use perlmod::Value;
- use proxmox_frr::serializer::to_raw_config;
+ use proxmox_frr::ser::serializer::to_raw_config;
use proxmox_network_types::ip_address::{Cidr, Ipv4Cidr, Ipv6Cidr};
use proxmox_section_config::typed::SectionConfigData;
use proxmox_ve_config::common::valid::Validatable;
--
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] 14+ messages in thread
* [pve-devel] [PATCH proxmox-perl-rs v3 2/4] fabrics: add function to get status of fabric
2025-08-26 9:49 [pve-devel] [PATCH manager/network/proxmox{-ve-rs, -perl-rs} v3 00/13] Add fabric status view Gabriel Goller
` (3 preceding siblings ...)
2025-08-26 9:49 ` [pve-devel] [PATCH proxmox-perl-rs v3 1/4] pve: fabrics: update proxmox-frr import path Gabriel Goller
@ 2025-08-26 9:49 ` Gabriel Goller
2025-08-26 9:49 ` [pve-devel] [PATCH proxmox-perl-rs v3 3/4] fabrics: add function to get all routes distributed by the fabrics Gabriel Goller
` (7 subsequent siblings)
12 siblings, 0 replies; 14+ messages in thread
From: Gabriel Goller @ 2025-08-26 9:49 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 | 61 +++++++++++++++
pve-rs/src/lib.rs | 2 +
pve-rs/src/sdn/mod.rs | 3 +
pve-rs/src/sdn/status.rs | 121 +++++++++++++++++++++++++++++
4 files changed, 187 insertions(+)
create mode 100644 pve-rs/src/sdn/mod.rs
create mode 100644 pve-rs/src/sdn/status.rs
diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
index 1dc8bf4320e6..ef18ee80b0e1 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};
@@ -34,6 +36,8 @@ pub mod pve_rs_sdn_fabrics {
use proxmox_ve_config::sdn::fabric::{FabricConfig, FabricEntry};
use proxmox_ve_config::sdn::frr::FrrConfigBuilder;
+ use crate::sdn::status;
+
/// A SDN Fabric config instance.
#[derive(Serialize, Deserialize)]
pub struct PerlFabricConfig {
@@ -578,4 +582,61 @@ pub mod pve_rs_sdn_fabrics {
Ok(interfaces)
}
+
+ /// 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: 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 route_status = status::RoutesParsed {
+ openfabric: openfabric_routes,
+ ospf: ospf_routes,
+ };
+
+ status::get_status(route_status)
+ }
}
diff --git a/pve-rs/src/lib.rs b/pve-rs/src/lib.rs
index b2fcaae9fe22..b32b061b1d25 100644
--- a/pve-rs/src/lib.rs
+++ b/pve-rs/src/lib.rs
@@ -13,6 +13,8 @@ use proxmox_notify::{Config, Notification, Severity};
#[path = "../common/src/mod.rs"]
mod common;
+mod sdn;
+
pub mod bindings;
fn send_notification(notification: &Notification) -> Result<(), Error> {
diff --git a/pve-rs/src/sdn/mod.rs b/pve-rs/src/sdn/mod.rs
new file mode 100644
index 000000000000..f4a42faa407b
--- /dev/null
+++ b/pve-rs/src/sdn/mod.rs
@@ -0,0 +1,3 @@
+/// This module contains status-related structs that represent Routes, Neighbors and general Status
+/// for all Protocols and SDN Objects
+pub mod status;
diff --git a/pve-rs/src/sdn/status.rs b/pve-rs/src/sdn/status.rs
new file mode 100644
index 000000000000..5502ed3a10f5
--- /dev/null
+++ b/pve-rs/src/sdn/status.rs
@@ -0,0 +1,121 @@
+use std::collections::{HashMap, HashSet};
+
+use serde::Serialize;
+
+use proxmox_frr::de::{self};
+use proxmox_ve_config::sdn::fabric::{
+ FabricConfig,
+ section_config::{fabric::FabricId, node::Node as ConfigNode},
+};
+
+/// 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.
+///
+/// Models the current state of the fabric, the status is determined by checking if any
+/// routes are propagated. This will be inserted into the PVE resources.
+#[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: de::Routes,
+ /// All ospf routes in FRR
+ pub ospf: de::Routes,
+}
+/// 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
+/// fabric to a status struct containing basic info about the fabric and the status (if it
+/// propagates a route).
+pub fn get_status(routes: RoutesParsed) -> Result<HashMap<FabricId, Status>, anyhow::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.all_nodes() {
+ if nodeid.as_str() != hostname {
+ continue;
+ }
+ let fabric_id = node.id().fabric_id();
+
+ let (current_protocol, all_routes) = match &node {
+ ConfigNode::Openfabric(_) => (Protocol::Openfabric, &routes.openfabric.0),
+ ConfigNode::Ospf(_) => (Protocol::Ospf, &routes.ospf.0),
+ };
+
+ // get interfaces
+ let interface_names: HashSet<&str> = match node {
+ ConfigNode::Openfabric(n) => n
+ .properties()
+ .interfaces()
+ .map(|i| i.name().as_str())
+ .collect(),
+ ConfigNode::Ospf(n) => n
+ .properties()
+ .interfaces()
+ .map(|i| i.name().as_str())
+ .collect(),
+ };
+
+ // 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| interface_names.contains(&nexthop.interface_name.as_str()))
+ })
+ });
+
+ 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.clone(), fabric);
+ }
+
+ Ok(stats)
+}
--
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] 14+ messages in thread
* [pve-devel] [PATCH proxmox-perl-rs v3 3/4] fabrics: add function to get all routes distributed by the fabrics
2025-08-26 9:49 [pve-devel] [PATCH manager/network/proxmox{-ve-rs, -perl-rs} v3 00/13] Add fabric status view Gabriel Goller
` (4 preceding siblings ...)
2025-08-26 9:49 ` [pve-devel] [PATCH proxmox-perl-rs v3 2/4] fabrics: add function to get status of fabric Gabriel Goller
@ 2025-08-26 9:49 ` Gabriel Goller
2025-08-26 9:49 ` [pve-devel] [PATCH proxmox-perl-rs v3 4/4] fabrics: add function to get all neighbors of the fabric Gabriel Goller
` (6 subsequent siblings)
12 siblings, 0 replies; 14+ messages in thread
From: Gabriel Goller @ 2025-08-26 9:49 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 | 57 +++++++++++++++++++
pve-rs/src/sdn/status.rs | 88 ++++++++++++++++++++++++++++++
2 files changed, 145 insertions(+)
diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
index ef18ee80b0e1..14dad216dbf4 100644
--- a/pve-rs/src/bindings/sdn/fabrics.rs
+++ b/pve-rs/src/bindings/sdn/fabrics.rs
@@ -583,6 +583,63 @@ pub mod pve_rs_sdn_fabrics {
Ok(interfaces)
}
+ /// 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: 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 route_status = status::RoutesParsed {
+ openfabric: openfabric_routes,
+ ospf: ospf_routes,
+ };
+
+ status::get_routes(route_status)
+ }
+
/// Return the status of all fabrics on this node.
///
/// Go through all fabrics in the config, then filter out the ones that exist on this node.
diff --git a/pve-rs/src/sdn/status.rs b/pve-rs/src/sdn/status.rs
index 5502ed3a10f5..b15ad0330732 100644
--- a/pve-rs/src/sdn/status.rs
+++ b/pve-rs/src/sdn/status.rs
@@ -8,6 +8,18 @@ use proxmox_ve_config::sdn::fabric::{
section_config::{fabric::FabricId, node::Node as ConfigNode},
};
+/// 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 {
@@ -53,6 +65,82 @@ pub struct RoutesParsed {
/// All ospf routes in FRR
pub ospf: de::Routes,
}
+
+/// Convert the passed frr routes into a list of all routes with properties associating
+/// them to a fabric.
+pub fn get_routes(routes: RoutesParsed) -> Result<Vec<RouteStatus>, anyhow::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.all_nodes() {
+ if nodeid.as_str() != hostname {
+ continue;
+ }
+ let fabric_id = node.id().fabric_id();
+
+ // get interfaces
+ let interface_names: HashSet<&str> = match node {
+ ConfigNode::Openfabric(n) => n
+ .properties()
+ .interfaces()
+ .map(|i| i.name().as_str())
+ .collect(),
+ ConfigNode::Ospf(n) => n
+ .properties()
+ .interfaces()
+ .map(|i| i.name().as_str())
+ .collect(),
+ };
+
+ let (current_protocol, all_routes) = match &node {
+ ConfigNode::Openfabric(_) => (Protocol::Openfabric, &routes.openfabric.0),
+ ConfigNode::Ospf(_) => (Protocol::Ospf, &routes.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.as_str()) {
+ route_belongs_to_fabric = true;
+ break;
+ }
+ }
+ if route_belongs_to_fabric {
+ break;
+ }
+ }
+
+ if route_belongs_to_fabric {
+ let mut via_list = Vec::new();
+ for route in route_list {
+ for nexthop in &route.nexthops {
+ let via = if let Some(ip) = nexthop.ip {
+ ip.to_string()
+ } else {
+ 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)
+}
/// Get the status for each fabric using the parsed routes from frr
///
/// Using the parsed routes we get from frr, filter and map them to a HashMap mapping every
--
2.47.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] 14+ messages in thread
* [pve-devel] [PATCH proxmox-perl-rs v3 4/4] fabrics: add function to get all neighbors of the fabric
2025-08-26 9:49 [pve-devel] [PATCH manager/network/proxmox{-ve-rs, -perl-rs} v3 00/13] Add fabric status view Gabriel Goller
` (5 preceding siblings ...)
2025-08-26 9:49 ` [pve-devel] [PATCH proxmox-perl-rs v3 3/4] fabrics: add function to get all routes distributed by the fabrics Gabriel Goller
@ 2025-08-26 9:49 ` Gabriel Goller
2025-08-26 9:49 ` [pve-devel] [PATCH pve-network v3 1/3] fabrics: add fabrics status to SDN::status function Gabriel Goller
` (5 subsequent siblings)
12 siblings, 0 replies; 14+ messages in thread
From: Gabriel Goller @ 2025-08-26 9:49 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 | 43 ++++++++++++
pve-rs/src/sdn/status.rs | 104 +++++++++++++++++++++++++++++
2 files changed, 147 insertions(+)
diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
index 14dad216dbf4..079a58c27d32 100644
--- a/pve-rs/src/bindings/sdn/fabrics.rs
+++ b/pve-rs/src/bindings/sdn/fabrics.rs
@@ -640,6 +640,49 @@ pub mod pve_rs_sdn_fabrics {
status::get_routes(route_status)
}
+ /// 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: proxmox_frr::de::openfabric::Neighbors =
+ if openfabric_neighbors_string.is_empty() {
+ proxmox_frr::de::openfabric::Neighbors::default()
+ } else {
+ serde_json::from_str(&openfabric_neighbors_string)
+ .with_context(|| "error parsing openfabric neighbors")?
+ };
+
+ let ospf_neighbors: proxmox_frr::de::ospf::Neighbors = if ospf_neighbors_string.is_empty() {
+ proxmox_frr::de::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,
+ };
+
+ status::get_neighbors(neighbor_status)
+ }
+
/// Return the status of all fabrics on this node.
///
/// Go through all fabrics in the config, then filter out the ones that exist on this node.
diff --git a/pve-rs/src/sdn/status.rs b/pve-rs/src/sdn/status.rs
index b15ad0330732..86be8df4a38c 100644
--- a/pve-rs/src/sdn/status.rs
+++ b/pve-rs/src/sdn/status.rs
@@ -8,6 +8,18 @@ use proxmox_ve_config::sdn::fabric::{
section_config::{fabric::FabricId, node::Node as ConfigNode},
};
+/// 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.
///
/// Contains the route, the fabric and protocol it belongs to and some extra nexthop
@@ -66,6 +78,19 @@ pub struct RoutesParsed {
pub ospf: de::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: de::openfabric::Neighbors,
+ /// The ospf neighbors in FRR
+ pub ospf: de::ospf::Neighbors,
+}
+
/// Convert the passed frr routes into a list of all routes with properties associating
/// them to a fabric.
pub fn get_routes(routes: RoutesParsed) -> Result<Vec<RouteStatus>, anyhow::Error> {
@@ -141,6 +166,85 @@ pub fn get_routes(routes: RoutesParsed) -> Result<Vec<RouteStatus>, anyhow::Erro
}
Ok(stats)
}
+
+/// Get a list of all neighbors of all the fabrics configured
+///
+/// Convert the passed neighbors of all frr protocols to a list of neighbors with
+/// with properties associating them to a specific fabric.
+pub fn get_neighbors(neighbors: NeighborsParsed) -> Result<Vec<NeighborStatus>, anyhow::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.all_nodes() {
+ if nodeid.as_str() != hostname {
+ continue;
+ }
+ let fabric_id = node.id().fabric_id();
+
+ match node {
+ ConfigNode::Openfabric(_) => {
+ for area in &neighbors.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 &neighbors.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)
+}
+
/// Get the status for each fabric using the parsed routes from frr
///
/// Using the parsed routes we get from frr, filter and map them to a HashMap mapping every
--
2.47.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] 14+ messages in thread
* [pve-devel] [PATCH pve-network v3 1/3] fabrics: add fabrics status to SDN::status function
2025-08-26 9:49 [pve-devel] [PATCH manager/network/proxmox{-ve-rs, -perl-rs} v3 00/13] Add fabric status view Gabriel Goller
` (6 preceding siblings ...)
2025-08-26 9:49 ` [pve-devel] [PATCH proxmox-perl-rs v3 4/4] fabrics: add function to get all neighbors of the fabric Gabriel Goller
@ 2025-08-26 9:49 ` Gabriel Goller
2025-08-26 9:49 ` [pve-devel] [PATCH pve-network v3 2/3] fabrics: add api endpoint to return fabrics routes Gabriel Goller
` (4 subsequent siblings)
12 siblings, 0 replies; 14+ messages in thread
From: Gabriel Goller @ 2025-08-26 9:49 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] 14+ messages in thread
* [pve-devel] [PATCH pve-network v3 2/3] fabrics: add api endpoint to return fabrics routes
2025-08-26 9:49 [pve-devel] [PATCH manager/network/proxmox{-ve-rs, -perl-rs} v3 00/13] Add fabric status view Gabriel Goller
` (7 preceding siblings ...)
2025-08-26 9:49 ` [pve-devel] [PATCH pve-network v3 1/3] fabrics: add fabrics status to SDN::status function Gabriel Goller
@ 2025-08-26 9:49 ` Gabriel Goller
2025-08-26 9:49 ` [pve-devel] [PATCH pve-network v3 3/3] fabrics: add api endpoint to return fabric neighbors Gabriel Goller
` (3 subsequent siblings)
12 siblings, 0 replies; 14+ messages in thread
From: Gabriel Goller @ 2025-08-26 9:49 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] 14+ messages in thread
* [pve-devel] [PATCH pve-network v3 3/3] fabrics: add api endpoint to return fabric neighbors
2025-08-26 9:49 [pve-devel] [PATCH manager/network/proxmox{-ve-rs, -perl-rs} v3 00/13] Add fabric status view Gabriel Goller
` (8 preceding siblings ...)
2025-08-26 9:49 ` [pve-devel] [PATCH pve-network v3 2/3] fabrics: add api endpoint to return fabrics routes Gabriel Goller
@ 2025-08-26 9:49 ` Gabriel Goller
2025-08-26 9:49 ` [pve-devel] [PATCH pve-manager v3 1/3] pvestatd: add fabrics status to pvestatd Gabriel Goller
` (2 subsequent siblings)
12 siblings, 0 replies; 14+ messages in thread
From: Gabriel Goller @ 2025-08-26 9:49 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] 14+ messages in thread
* [pve-devel] [PATCH pve-manager v3 1/3] pvestatd: add fabrics status to pvestatd
2025-08-26 9:49 [pve-devel] [PATCH manager/network/proxmox{-ve-rs, -perl-rs} v3 00/13] Add fabric status view Gabriel Goller
` (9 preceding siblings ...)
2025-08-26 9:49 ` [pve-devel] [PATCH pve-network v3 3/3] fabrics: add api endpoint to return fabric neighbors Gabriel Goller
@ 2025-08-26 9:49 ` Gabriel Goller
2025-08-26 9:49 ` [pve-devel] [PATCH pve-manager v3 2/3] fabrics: add resource view for fabrics Gabriel Goller
2025-08-26 9:49 ` [pve-devel] [PATCH pve-manager v3 3/3] permissions: differentiate between zone and fabric paths Gabriel Goller
12 siblings, 0 replies; 14+ messages in thread
From: Gabriel Goller @ 2025-08-26 9:49 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] 14+ messages in thread
* [pve-devel] [PATCH pve-manager v3 2/3] fabrics: add resource view for fabrics
2025-08-26 9:49 [pve-devel] [PATCH manager/network/proxmox{-ve-rs, -perl-rs} v3 00/13] Add fabric status view Gabriel Goller
` (10 preceding siblings ...)
2025-08-26 9:49 ` [pve-devel] [PATCH pve-manager v3 1/3] pvestatd: add fabrics status to pvestatd Gabriel Goller
@ 2025-08-26 9:49 ` Gabriel Goller
2025-08-26 9:49 ` [pve-devel] [PATCH pve-manager v3 3/3] permissions: differentiate between zone and fabric paths Gabriel Goller
12 siblings, 0 replies; 14+ messages in thread
From: Gabriel Goller @ 2025-08-26 9:49 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] 14+ messages in thread
* [pve-devel] [PATCH pve-manager v3 3/3] permissions: differentiate between zone and fabric paths
2025-08-26 9:49 [pve-devel] [PATCH manager/network/proxmox{-ve-rs, -perl-rs} v3 00/13] Add fabric status view Gabriel Goller
` (11 preceding siblings ...)
2025-08-26 9:49 ` [pve-devel] [PATCH pve-manager v3 2/3] fabrics: add resource view for fabrics Gabriel Goller
@ 2025-08-26 9:49 ` Gabriel Goller
12 siblings, 0 replies; 14+ messages in thread
From: Gabriel Goller @ 2025-08-26 9:49 UTC (permalink / raw)
To: pve-devel
With the addition of the fabrics status in the resources, there are
fabrics and zones using the 'sdn' type. The finer distinction between
zones and fabrics is done using the new 'sdn_type' parameter.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
www/manager6/data/PermPathStore.js | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/www/manager6/data/PermPathStore.js b/www/manager6/data/PermPathStore.js
index c7ec42314c82..8a5344483f0d 100644
--- a/www/manager6/data/PermPathStore.js
+++ b/www/manager6/data/PermPathStore.js
@@ -43,7 +43,14 @@ Ext.define('PVE.data.PermPathStore', {
path = '/vms/' + record.get('vmid');
break;
case 'sdn':
- path = '/sdn/zones/' + record.get('sdn');
+ switch (record.get('sdn_type')) {
+ case 'zone':
+ path = '/sdn/zones/' + record.get('sdn');
+ break;
+ case 'fabric':
+ path = '/sdn/fabrics/' + record.get('sdn');
+ break;
+ }
break;
case 'storage':
path = '/storage/' + record.get('storage');
--
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] 14+ messages in thread
end of thread, other threads:[~2025-08-26 9:51 UTC | newest]
Thread overview: 14+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-08-26 9:49 [pve-devel] [PATCH manager/network/proxmox{-ve-rs, -perl-rs} v3 00/13] Add fabric status view Gabriel Goller
2025-08-26 9:49 ` [pve-devel] [PATCH proxmox-ve-rs v3 1/3] frr: make room for deserialization structs Gabriel Goller
2025-08-26 9:49 ` [pve-devel] [PATCH proxmox-ve-rs v3 2/3] frr: add deserialization types for openfabric and ospf Gabriel Goller
2025-08-26 9:49 ` [pve-devel] [PATCH proxmox-ve-rs v3 3/3] ve-config: add helper function to iterate over all nodes in all fabrics Gabriel Goller
2025-08-26 9:49 ` [pve-devel] [PATCH proxmox-perl-rs v3 1/4] pve: fabrics: update proxmox-frr import path Gabriel Goller
2025-08-26 9:49 ` [pve-devel] [PATCH proxmox-perl-rs v3 2/4] fabrics: add function to get status of fabric Gabriel Goller
2025-08-26 9:49 ` [pve-devel] [PATCH proxmox-perl-rs v3 3/4] fabrics: add function to get all routes distributed by the fabrics Gabriel Goller
2025-08-26 9:49 ` [pve-devel] [PATCH proxmox-perl-rs v3 4/4] fabrics: add function to get all neighbors of the fabric Gabriel Goller
2025-08-26 9:49 ` [pve-devel] [PATCH pve-network v3 1/3] fabrics: add fabrics status to SDN::status function Gabriel Goller
2025-08-26 9:49 ` [pve-devel] [PATCH pve-network v3 2/3] fabrics: add api endpoint to return fabrics routes Gabriel Goller
2025-08-26 9:49 ` [pve-devel] [PATCH pve-network v3 3/3] fabrics: add api endpoint to return fabric neighbors Gabriel Goller
2025-08-26 9:49 ` [pve-devel] [PATCH pve-manager v3 1/3] pvestatd: add fabrics status to pvestatd Gabriel Goller
2025-08-26 9:49 ` [pve-devel] [PATCH pve-manager v3 2/3] fabrics: add resource view for fabrics Gabriel Goller
2025-08-26 9:49 ` [pve-devel] [PATCH pve-manager v3 3/3] permissions: differentiate between zone and fabric paths Gabriel Goller
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox