public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pve-devel] [RFC cluster/manager/network/proxmox{-ve-rs, -perl-rs} 00/11] Add SDN Fabrics
@ 2025-02-14 13:39 Gabriel Goller
  2025-02-14 13:39 ` [pve-devel] [PATCH proxmox-ve-rs 01/11] add crate with common network types Gabriel Goller
                   ` (11 more replies)
  0 siblings, 12 replies; 32+ messages in thread
From: Gabriel Goller @ 2025-02-14 13:39 UTC (permalink / raw)
  To: pve-devel

This series allows the user to add fabrics such as OpenFabric and OSPF over
their clusters.

Note that this is a very early RFC and its sole purpose is to get some initial
feedback and architectural suggestions.

Overview
--------
Add a new section to the sdn panel in the datacenter options which allows
creating OpenFabric and OSPF fabrics. One can add Nodes to the fabrics by
selecting them from a dropdown which shows all the nodes in the cluster.
Additionally the user can then select the interfaces of the node which should
be added to the fabric. There are also protocol-specific options such as "passive",
"hello-interval" etc. available to select on the interface.

Implementation
--------------
Add config files for every fabric type, so currently we add sdn/ospf.cfg and
sdn/openfabric.cfg. These config file get read by pve-network and the raw
section config gets passed to rust (proxmox-perl-rs), where we parse it using
helpers and schemas from proxmox-ve-config crate (proxmox-ve-rs repo). The
config parsed from the section-config is then converted into a better 
representation (no PropertyString, embed Nodes into Fabrics, etc.), which is
also guaranteed to be correct (everything is typed and the values are
semantically checked). This intermediate representation can then be converted
into a `FrrConfig`, which lives in the proxmox-frr crate (proxmox-ve-rs repo).
This representation is very close to the real frr config (eg contains routers,
interfaces, etc.) and can either be converted to a `PerlFrr` config (which will
be used by pve-network to merge with the existin config) or (optional and
experimental) into a string, which would be the actual frr config as it can be
found in `frr.conf`.

This series also relies on: 
https://lore.proxmox.com/pve-devel/20250205161340.740740-1-g.goller@proxmox.com/

Open Questions/Issues:
 * generate openfabric net from the selected interface ip (the net is quite
   hard to get right otherwise).
 * Reminder to apply configuration -> Probably add a "state" column which shows
   "new" (when not applied) like in the sdn/controllers grid.
 * Add ability for the user to create a "standard" setup, where he can select a
   node and we automatically add an ip address to the loopback address and add
   the loopback interface as passive to the openfabric/ospf fabric. (Maybe we
   are able to get frr to support dummy interfaces in the meantime, which would
   be even better.)
 * Check if we want continue using the pve-network perl frr merging code or if
   we want to transition to rust -> vtysh. So the config gets flushed to the
   daemon directly using vtysh, this allows the user to change the frr config
   manually and their settings not getting overwritten by us (we also avoid
   reloading the daemon).
 * Write some extensive documentation on what the Fabrics can/cannot do.

proxmox-ve-rs:

Gabriel Goller (3):
  add crate with common network types
  add proxmox-frr crate with frr types
  add intermediate fabric representation

 Cargo.toml                                    |   7 +
 proxmox-frr/Cargo.toml                        |  25 +
 proxmox-frr/src/common.rs                     |  54 ++
 proxmox-frr/src/lib.rs                        | 223 ++++++++
 proxmox-frr/src/openfabric.rs                 | 137 +++++
 proxmox-frr/src/ospf.rs                       | 148 ++++++
 proxmox-network-types/Cargo.toml              |  15 +
 proxmox-network-types/src/lib.rs              |   1 +
 proxmox-network-types/src/net.rs              | 239 +++++++++
 proxmox-ve-config/Cargo.toml                  |  10 +-
 proxmox-ve-config/debian/control              |   4 +-
 proxmox-ve-config/src/sdn/fabric/common.rs    |  90 ++++
 proxmox-ve-config/src/sdn/fabric/mod.rs       |  68 +++
 .../src/sdn/fabric/openfabric.rs              | 494 ++++++++++++++++++
 proxmox-ve-config/src/sdn/fabric/ospf.rs      | 375 +++++++++++++
 proxmox-ve-config/src/sdn/mod.rs              |   1 +
 16 files changed, 1885 insertions(+), 6 deletions(-)
 create mode 100644 proxmox-frr/Cargo.toml
 create mode 100644 proxmox-frr/src/common.rs
 create mode 100644 proxmox-frr/src/lib.rs
 create mode 100644 proxmox-frr/src/openfabric.rs
 create mode 100644 proxmox-frr/src/ospf.rs
 create mode 100644 proxmox-network-types/Cargo.toml
 create mode 100644 proxmox-network-types/src/lib.rs
 create mode 100644 proxmox-network-types/src/net.rs
 create mode 100644 proxmox-ve-config/src/sdn/fabric/common.rs
 create mode 100644 proxmox-ve-config/src/sdn/fabric/mod.rs
 create mode 100644 proxmox-ve-config/src/sdn/fabric/openfabric.rs
 create mode 100644 proxmox-ve-config/src/sdn/fabric/ospf.rs


proxmox-perl-rs:

Gabriel Goller (1):
  fabrics: add CRUD and generate fabrics methods

 pve-rs/Cargo.toml            |   5 +-
 pve-rs/Makefile              |   3 +
 pve-rs/src/lib.rs            |   1 +
 pve-rs/src/sdn/fabrics.rs    | 202 ++++++++++++++++
 pve-rs/src/sdn/mod.rs        |   3 +
 pve-rs/src/sdn/openfabric.rs | 454 +++++++++++++++++++++++++++++++++++
 pve-rs/src/sdn/ospf.rs       | 425 ++++++++++++++++++++++++++++++++
 7 files changed, 1092 insertions(+), 1 deletion(-)
 create mode 100644 pve-rs/src/sdn/fabrics.rs
 create mode 100644 pve-rs/src/sdn/mod.rs
 create mode 100644 pve-rs/src/sdn/openfabric.rs
 create mode 100644 pve-rs/src/sdn/ospf.rs


pve-cluster:

Gabriel Goller (1):
  cluster: add sdn fabrics config files

 src/PVE/Cluster.pm | 2 ++
 1 file changed, 2 insertions(+)


pve-network:

Gabriel Goller (3):
  add config file and common read/write methods
  merge the frr config with the fabrics frr config on apply
  add api endpoints for fabrics

 src/PVE/API2/Network/SDN.pm                   |   7 +
 src/PVE/API2/Network/SDN/Fabrics.pm           |  57 +++
 src/PVE/API2/Network/SDN/Fabrics/Common.pm    | 111 +++++
 src/PVE/API2/Network/SDN/Fabrics/Makefile     |   9 +
 .../API2/Network/SDN/Fabrics/OpenFabric.pm    | 460 ++++++++++++++++++
 src/PVE/API2/Network/SDN/Fabrics/Ospf.pm      | 433 +++++++++++++++++
 src/PVE/API2/Network/SDN/Makefile             |   3 +-
 src/PVE/Network/SDN.pm                        |   8 +-
 src/PVE/Network/SDN/Controllers.pm            |   1 -
 src/PVE/Network/SDN/Controllers/EvpnPlugin.pm |   3 -
 src/PVE/Network/SDN/Controllers/Frr.pm        |  13 +
 src/PVE/Network/SDN/Fabrics.pm                |  86 ++++
 src/PVE/Network/SDN/Makefile                  |   2 +-
 13 files changed, 1186 insertions(+), 7 deletions(-)
 create mode 100644 src/PVE/API2/Network/SDN/Fabrics.pm
 create mode 100644 src/PVE/API2/Network/SDN/Fabrics/Common.pm
 create mode 100644 src/PVE/API2/Network/SDN/Fabrics/Makefile
 create mode 100644 src/PVE/API2/Network/SDN/Fabrics/OpenFabric.pm
 create mode 100644 src/PVE/API2/Network/SDN/Fabrics/Ospf.pm
 create mode 100644 src/PVE/Network/SDN/Fabrics.pm


pve-manager:

Gabriel Goller (3):
  sdn: add Fabrics view
  sdn: add fabric edit/delete forms
  network: return loopback interface on network endpoint

 PVE/API2/Cluster.pm                           |   7 +-
 PVE/API2/Network.pm                           |   9 +-
 www/manager6/.lint-incremental                |   0
 www/manager6/Makefile                         |   8 +
 www/manager6/dc/Config.js                     |   8 +
 www/manager6/sdn/FabricsView.js               | 359 ++++++++++++++++++
 www/manager6/sdn/fabrics/Common.js            | 222 +++++++++++
 .../sdn/fabrics/openfabric/FabricEdit.js      |  67 ++++
 .../sdn/fabrics/openfabric/InterfaceEdit.js   |  92 +++++
 .../sdn/fabrics/openfabric/NodeEdit.js        | 187 +++++++++
 www/manager6/sdn/fabrics/ospf/FabricEdit.js   |  60 +++
 .../sdn/fabrics/ospf/InterfaceEdit.js         |  46 +++
 www/manager6/sdn/fabrics/ospf/NodeEdit.js     | 191 ++++++++++
 13 files changed, 1244 insertions(+), 12 deletions(-)
 create mode 100644 www/manager6/.lint-incremental
 create mode 100644 www/manager6/sdn/FabricsView.js
 create mode 100644 www/manager6/sdn/fabrics/Common.js
 create mode 100644 www/manager6/sdn/fabrics/openfabric/FabricEdit.js
 create mode 100644 www/manager6/sdn/fabrics/openfabric/InterfaceEdit.js
 create mode 100644 www/manager6/sdn/fabrics/openfabric/NodeEdit.js
 create mode 100644 www/manager6/sdn/fabrics/ospf/FabricEdit.js
 create mode 100644 www/manager6/sdn/fabrics/ospf/InterfaceEdit.js
 create mode 100644 www/manager6/sdn/fabrics/ospf/NodeEdit.js


Summary over all repositories:
  50 files changed, 5409 insertions(+), 26 deletions(-)

-- 
Generated by git-murpp 0.7.1


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


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

* [pve-devel] [PATCH proxmox-ve-rs 01/11] add crate with common network types
  2025-02-14 13:39 [pve-devel] [RFC cluster/manager/network/proxmox{-ve-rs, -perl-rs} 00/11] Add SDN Fabrics Gabriel Goller
@ 2025-02-14 13:39 ` Gabriel Goller
  2025-03-03 15:08   ` Stefan Hanreich
  2025-02-14 13:39 ` [pve-devel] [PATCH proxmox-ve-rs 02/11] add proxmox-frr crate with frr types Gabriel Goller
                   ` (10 subsequent siblings)
  11 siblings, 1 reply; 32+ messages in thread
From: Gabriel Goller @ 2025-02-14 13:39 UTC (permalink / raw)
  To: pve-devel

The new proxmox-network-types crate holds some common types that are
used by proxmox-frr, proxmox-ve-config and proxmox-perl-rs. These types
are here because we don't want proxmox-frr to be a dependency of
proxmox-ve-config or vice-versa (or at least it should be feature-gated).
They should be independent.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 Cargo.toml                       |   6 +
 proxmox-network-types/Cargo.toml |  15 ++
 proxmox-network-types/src/lib.rs |   1 +
 proxmox-network-types/src/net.rs | 239 +++++++++++++++++++++++++++++++
 4 files changed, 261 insertions(+)
 create mode 100644 proxmox-network-types/Cargo.toml
 create mode 100644 proxmox-network-types/src/lib.rs
 create mode 100644 proxmox-network-types/src/net.rs

diff --git a/Cargo.toml b/Cargo.toml
index dc7f312fb8a9..e452c931e78c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,7 @@
 [workspace]
 members = [
     "proxmox-ve-config",
+    "proxmox-network-types",
 ]
 exclude = [
     "build",
@@ -15,3 +16,8 @@ homepage = "https://proxmox.com"
 exclude = [ "debian" ]
 rust-version = "1.82"
 
+[workspace.dependencies]
+proxmox-section-config = "2.1.1"
+serde = "1"
+serde_with = "3.8.1"
+thiserror = "1.0.59"
diff --git a/proxmox-network-types/Cargo.toml b/proxmox-network-types/Cargo.toml
new file mode 100644
index 000000000000..93f4df87a59f
--- /dev/null
+++ b/proxmox-network-types/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "proxmox-network-types"
+version = "0.1.0"
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+homepage.workspace = true
+exclude.workspace = true
+rust-version.workspace = true
+
+[dependencies]
+thiserror = { workspace = true }
+anyhow = "1"
+serde = { workspace = true, features = [ "derive" ] }
+serde_with = { workspace = true }
diff --git a/proxmox-network-types/src/lib.rs b/proxmox-network-types/src/lib.rs
new file mode 100644
index 000000000000..f9faf2ff6542
--- /dev/null
+++ b/proxmox-network-types/src/lib.rs
@@ -0,0 +1 @@
+pub mod net;
diff --git a/proxmox-network-types/src/net.rs b/proxmox-network-types/src/net.rs
new file mode 100644
index 000000000000..5fdbe3920800
--- /dev/null
+++ b/proxmox-network-types/src/net.rs
@@ -0,0 +1,239 @@
+use std::{fmt::Display, str::FromStr};
+
+use serde::Serialize;
+use serde_with::{DeserializeFromStr, SerializeDisplay};
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum NetError {
+    #[error("Some octets are missing")]
+    WrongLength,
+    #[error("The NET selector must be two characters wide and be 00")]
+    InvalidNetSelector,
+    #[error("Invalid AFI (wrong size or position)")]
+    InvalidAFI,
+    #[error("Invalid Area (wrong size or position)")]
+    InvalidArea,
+    #[error("Invalid SystemId (wrong size or position)")]
+    InvalidSystemId,
+}
+
+/// Address Family authority Identifier - 49 The AFI value 49 is what IS-IS (and openfabric) uses
+/// for private addressing.
+#[derive(
+    Debug, DeserializeFromStr, SerializeDisplay, Clone, Hash, PartialEq, Eq, PartialOrd, Ord,
+)]
+struct NetAFI(String);
+
+impl Default for NetAFI {
+    fn default() -> Self {
+        Self("49".to_owned())
+    }
+}
+
+impl Display for NetAFI {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self.0)
+    }
+}
+
+impl FromStr for NetAFI {
+    type Err = NetError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        if s.len() != 2 {
+            Err(NetError::InvalidAFI)
+        } else {
+            Ok(Self(s.to_owned()))
+        }
+    }
+}
+
+/// Area identifier: 0001 IS-IS area number (numerical area 1)
+/// The second part (system) of the `net` identifier. Every node has to have a different system
+/// number.
+#[derive(Debug, DeserializeFromStr, Serialize, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
+struct NetArea(String);
+
+impl Default for NetArea {
+    fn default() -> Self {
+        Self("0001".to_owned())
+    }
+}
+
+impl Display for NetArea {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self.0)
+    }
+}
+
+impl FromStr for NetArea {
+    type Err = NetError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        if s.len() != 4 {
+            Err(NetError::InvalidArea)
+        } else {
+            Ok(Self(s.to_owned()))
+        }
+    }
+}
+
+/// System identifier: 1921.6800.1002 - for system identifiers we recommend to use IP address or
+/// MAC address of the router itself. The way to construct this is to keep all of the zeroes of the
+/// router IP address, and then change the periods from being every three numbers to every four
+/// numbers. The address that is listed here is 192.168.1.2, which if expanded will turn into
+/// 192.168.001.002. Then all one has to do is move the dots to have four numbers instead of three.
+/// This gives us 1921.6800.1002.
+#[derive(Debug, DeserializeFromStr, Serialize, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
+struct NetSystemId(String);
+
+impl Display for NetSystemId {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self.0)
+    }
+}
+
+impl FromStr for NetSystemId {
+    type Err = NetError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        if s.split(".").count() != 3 || s.split(".").any(|octet| octet.len() != 4) {
+            Err(NetError::InvalidArea)
+        } else {
+            Ok(Self(s.to_owned()))
+        }
+    }
+}
+
+/// NET selector: 00 Must always be 00. This setting indicates “this system” or “local system.”
+#[derive(Debug, DeserializeFromStr, Serialize, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
+struct NetSelector(String);
+
+impl Default for NetSelector {
+    fn default() -> Self {
+        Self("00".to_owned())
+    }
+}
+
+impl Display for NetSelector {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self.0)
+    }
+}
+
+impl FromStr for NetSelector {
+    type Err = NetError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        if s.len() != 2 {
+            Err(NetError::InvalidNetSelector)
+        } else {
+            Ok(Self(s.to_owned()))
+        }
+    }
+}
+
+/// The first part (area) of the `net` identifier. The entire OpenFabric fabric has to have the
+/// same area.
+/// f.e.: "49.0001"
+#[derive(
+    Debug, DeserializeFromStr, SerializeDisplay, Clone, Hash, PartialEq, Eq, PartialOrd, Ord,
+)]
+pub struct Net {
+    afi: NetAFI,
+    area: NetArea,
+    system: NetSystemId,
+    selector: NetSelector,
+}
+
+impl FromStr for Net {
+    type Err = NetError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        if s.split(".").count() != 6 {
+            return Err(NetError::WrongLength);
+        }
+        let mut iter = s.split(".");
+        let afi = iter.next().ok_or(NetError::WrongLength)?;
+        let area = iter.next().ok_or(NetError::WrongLength)?;
+        let system = format!(
+            "{}.{}.{}",
+            iter.next().ok_or(NetError::WrongLength)?,
+            iter.next().ok_or(NetError::WrongLength)?,
+            iter.next().ok_or(NetError::WrongLength)?
+        );
+        let selector = iter.next().ok_or(NetError::WrongLength)?;
+        Ok(Self {
+            afi: afi.parse()?,
+            area: area.parse()?,
+            system: system.parse()?,
+            selector: selector.parse()?,
+        })
+    }
+}
+
+impl Display for Net {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "{}.{}.{}.{}",
+            self.afi, self.area, self.system, self.selector
+        )
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_net_from_str() {
+        let input = "49.0001.1921.6800.1002.00";
+        let net = input.parse::<Net>().expect("this net should parse");
+        assert_eq!(net.afi, NetAFI("49".to_owned()));
+        assert_eq!(net.area, NetArea("0001".to_owned()));
+        assert_eq!(net.system, NetSystemId("1921.6800.1002".to_owned()));
+        assert_eq!(net.selector, NetSelector("00".to_owned()));
+
+        let input = "45.0200.0100.1001.0010.01";
+        let net = input.parse::<Net>().expect("this net should parse");
+        assert_eq!(net.afi, NetAFI("45".to_owned()));
+        assert_eq!(net.area, NetArea("0200".to_owned()));
+        assert_eq!(net.system, NetSystemId("0100.1001.0010".to_owned()));
+        assert_eq!(net.selector, NetSelector("01".to_owned()));
+    }
+
+    #[test]
+    fn test_net_from_str_failed() {
+        let input = "49.0001.1921.6800.1002.000";
+        assert!(matches!(
+            input.parse::<Net>(),
+            Err(NetError::InvalidNetSelector)
+        ));
+
+        let input = "49.0001.1921.6800.1002.00.00";
+        assert!(matches!(input.parse::<Net>(), Err(NetError::WrongLength)));
+
+        let input = "49.0001.1921.6800.10002.00";
+        assert!(matches!(input.parse::<Net>(), Err(NetError::InvalidArea)));
+
+        let input = "409.0001.1921.6800.1002.00";
+        assert!(matches!(input.parse::<Net>(), Err(NetError::InvalidAFI)));
+
+        let input = "49.00001.1921.6800.1002.00";
+        assert!(matches!(input.parse::<Net>(), Err(NetError::InvalidArea)));
+    }
+
+    #[test]
+    fn test_net_display() {
+        let net = Net {
+            afi: NetAFI("49".to_owned()),
+            area: NetArea("0001".to_owned()),
+            system: NetSystemId("1921.6800.1002".to_owned()),
+            selector: NetSelector("00".to_owned()),
+        };
+        assert_eq!(format!("{net}"), "49.0001.1921.6800.1002.00");
+    }
+}
+
-- 
2.39.5



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

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

* [pve-devel] [PATCH proxmox-ve-rs 02/11] add proxmox-frr crate with frr types
  2025-02-14 13:39 [pve-devel] [RFC cluster/manager/network/proxmox{-ve-rs, -perl-rs} 00/11] Add SDN Fabrics Gabriel Goller
  2025-02-14 13:39 ` [pve-devel] [PATCH proxmox-ve-rs 01/11] add crate with common network types Gabriel Goller
@ 2025-02-14 13:39 ` Gabriel Goller
  2025-03-03 16:29   ` Stefan Hanreich
  2025-02-14 13:39 ` [pve-devel] [PATCH proxmox-ve-rs 03/11] add intermediate fabric representation Gabriel Goller
                   ` (9 subsequent siblings)
  11 siblings, 1 reply; 32+ messages in thread
From: Gabriel Goller @ 2025-02-14 13:39 UTC (permalink / raw)
  To: pve-devel

This crate contains types that represent the frr config. For example it
contains a `Router` and `Interface` struct. This Frr-Representation can
then be converted to the real frr config.

Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 Cargo.toml                    |   1 +
 proxmox-frr/Cargo.toml        |  25 ++++
 proxmox-frr/src/common.rs     |  54 ++++++++
 proxmox-frr/src/lib.rs        | 223 ++++++++++++++++++++++++++++++++++
 proxmox-frr/src/openfabric.rs | 137 +++++++++++++++++++++
 proxmox-frr/src/ospf.rs       | 148 ++++++++++++++++++++++
 6 files changed, 588 insertions(+)
 create mode 100644 proxmox-frr/Cargo.toml
 create mode 100644 proxmox-frr/src/common.rs
 create mode 100644 proxmox-frr/src/lib.rs
 create mode 100644 proxmox-frr/src/openfabric.rs
 create mode 100644 proxmox-frr/src/ospf.rs

diff --git a/Cargo.toml b/Cargo.toml
index e452c931e78c..ffda1233b17a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,7 @@
 [workspace]
 members = [
     "proxmox-ve-config",
+    "proxmox-frr",
     "proxmox-network-types",
 ]
 exclude = [
diff --git a/proxmox-frr/Cargo.toml b/proxmox-frr/Cargo.toml
new file mode 100644
index 000000000000..bea8a0f8bab3
--- /dev/null
+++ b/proxmox-frr/Cargo.toml
@@ -0,0 +1,25 @@
+[package]
+name = "proxmox-frr"
+version = "0.1.0"
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+homepage.workspace = true
+exclude.workspace = true
+rust-version.workspace = true
+
+[dependencies]
+thiserror = { workspace = true }
+anyhow = "1"
+tracing = "0.1"
+
+serde = { workspace = true, features = [ "derive" ] }
+serde_with = { workspace = true }
+itoa = "1.0.9"
+
+proxmox-ve-config = { path = "../proxmox-ve-config", optional = true }
+proxmox-section-config = { workspace = true, optional = true }
+proxmox-network-types = { path = "../proxmox-network-types/" }
+
+[features]
+config-ext = ["dep:proxmox-ve-config", "dep:proxmox-section-config" ]
diff --git a/proxmox-frr/src/common.rs b/proxmox-frr/src/common.rs
new file mode 100644
index 000000000000..0d99bb4da6e2
--- /dev/null
+++ b/proxmox-frr/src/common.rs
@@ -0,0 +1,54 @@
+use std::{fmt::Display, str::FromStr};
+
+use serde_with::{DeserializeFromStr, SerializeDisplay};
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum FrrWordError {
+    #[error("word is empty")]
+    IsEmpty,
+    #[error("word contains invalid character")]
+    InvalidCharacter,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, DeserializeFromStr, SerializeDisplay)]
+pub struct FrrWord(String);
+
+impl FrrWord {
+    pub fn new(name: String) -> Result<Self, FrrWordError> {
+        if name.is_empty() {
+            return Err(FrrWordError::IsEmpty);
+        }
+
+        if name
+            .as_bytes()
+            .iter()
+            .any(|c| !c.is_ascii() || c.is_ascii_whitespace())
+        {
+            return Err(FrrWordError::InvalidCharacter);
+        }
+
+        Ok(Self(name))
+    }
+}
+
+impl FromStr for FrrWord {
+    type Err = FrrWordError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        FrrWord::new(s.to_string())
+    }
+}
+
+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
+    }
+}
+
diff --git a/proxmox-frr/src/lib.rs b/proxmox-frr/src/lib.rs
new file mode 100644
index 000000000000..ceef82999619
--- /dev/null
+++ b/proxmox-frr/src/lib.rs
@@ -0,0 +1,223 @@
+pub mod common;
+pub mod openfabric;
+pub mod ospf;
+
+use std::{collections::{hash_map::Entry, HashMap}, fmt::Display, str::FromStr};
+
+use common::{FrrWord, FrrWordError};
+use proxmox_ve_config::sdn::fabric::common::Hostname;
+#[cfg(feature = "config-ext")]
+use proxmox_ve_config::sdn::fabric::FabricConfig;
+
+use serde::{Deserialize, Serialize};
+use serde_with::{DeserializeFromStr, SerializeDisplay};
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum RouterNameError {
+    #[error("invalid name")]
+    InvalidName,
+    #[error("invalid frr word")]
+    FrrWordError(#[from] FrrWordError),
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
+pub enum Router {
+    OpenFabric(openfabric::OpenFabricRouter),
+    Ospf(ospf::OspfRouter),
+}
+
+impl From<openfabric::OpenFabricRouter> for Router {
+    fn from(value: openfabric::OpenFabricRouter) -> Self {
+        Router::OpenFabric(value)
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, DeserializeFromStr, SerializeDisplay)]
+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),
+        }
+    }
+}
+
+impl FromStr for RouterName {
+    type Err = RouterNameError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        if let Ok(router) = s.parse() {
+            return Ok(Self::OpenFabric(router));
+        }
+
+        Err(RouterNameError::InvalidName)
+    }
+}
+
+/// The interface name is the same on ospf and openfabric, but it is an enum so we can have two
+/// different entries in the hashmap. This allows us to have an interface in an ospf and openfabric
+/// fabric.
+#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash)]
+pub enum InterfaceName {
+    OpenFabric(FrrWord),
+    Ospf(FrrWord),
+}
+
+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),
+        }
+        
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
+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(Clone, Debug, PartialEq, Eq, Default, Deserialize, Serialize)]
+pub struct FrrConfig {
+    router: HashMap<RouterName, Router>,
+    interfaces: HashMap<InterfaceName, Interface>,
+}
+
+impl FrrConfig {
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    #[cfg(feature = "config-ext")]
+    pub fn builder() -> FrrConfigBuilder {
+        FrrConfigBuilder::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()
+    }
+}
+
+#[derive(Default)]
+#[cfg(feature = "config-ext")]
+pub struct FrrConfigBuilder {
+    fabrics: FabricConfig,
+    //bgp: Option<internal::BgpConfig>
+}
+
+#[cfg(feature = "config-ext")]
+impl FrrConfigBuilder {
+    pub fn add_fabrics(mut self, fabric: FabricConfig) -> FrrConfigBuilder {
+        self.fabrics = fabric;
+        self
+    }
+
+    pub fn build(self, current_node: &str) -> Result<FrrConfig, anyhow::Error> {
+        let mut router: HashMap<RouterName, Router> = HashMap::new();
+        let mut interfaces: HashMap<InterfaceName, Interface> = HashMap::new();
+
+        if let Some(openfabric) = self.fabrics.openfabric() {
+            // openfabric
+            openfabric
+                .fabrics()
+                .iter()
+                .try_for_each(|(fabric_id, fabric_config)| {
+                    let node_config = fabric_config.nodes().get(&Hostname::new(current_node));
+                    if let Some(node_config) = node_config {
+                        let ofr = openfabric::OpenFabricRouter::from((fabric_config, node_config));
+                        let router_item = Router::OpenFabric(ofr);
+                        let router_name = RouterName::OpenFabric(
+                            openfabric::OpenFabricRouterName::try_from(fabric_id)?,
+                        );
+                        router.insert(router_name.clone(), router_item);
+                        node_config.interfaces().try_for_each(|interface| {
+                            let mut openfabric_interface: openfabric::OpenFabricInterface =
+                                (fabric_id, interface).try_into()?;
+                            // If no specific hello_interval is set, get default one from fabric
+                            // config
+                            if openfabric_interface.hello_interval().is_none() {
+                                openfabric_interface
+                                    .set_hello_interval(fabric_config.hello_interval().clone());
+                            }
+                            let interface_name = InterfaceName::OpenFabric(FrrWord::from_str(interface.name())?);
+                            // Openfabric doesn't allow an interface to be in multiple openfabric
+                            // fabrics. Frr will just ignore it and take the first one.
+                            if let Entry::Vacant(e) = interfaces.entry(interface_name) {
+                                e.insert(openfabric_interface.into());
+                            } else {
+                                tracing::warn!("An interface cannot be in multiple openfabric fabrics");
+                            }
+                            Ok::<(), anyhow::Error>(())
+                        })?;
+                    } else {
+                        tracing::warn!("no node configuration for fabric \"{fabric_id}\" – this fabric is not configured for this node.");
+                        return Ok::<(), anyhow::Error>(());
+                    }
+                    Ok(())
+                })?;
+        }
+        if let Some(ospf) = self.fabrics.ospf() {
+            // ospf
+            ospf.fabrics()
+                .iter()
+                .try_for_each(|(fabric_id, fabric_config)| {
+                    let node_config = fabric_config.nodes().get(&Hostname::new(current_node));
+                    if let Some(node_config) = node_config {
+                        let ospf_router = ospf::OspfRouter::from((fabric_config, node_config));
+                        let router_item = Router::Ospf(ospf_router);
+                        let router_name = RouterName::Ospf(ospf::OspfRouterName::from(ospf::Area::try_from(fabric_id)?));
+                        router.insert(router_name.clone(), router_item);
+                        node_config.interfaces().try_for_each(|interface| {
+                            let ospf_interface: ospf::OspfInterface = (fabric_id, interface).try_into()?;
+
+                            let interface_name = InterfaceName::Ospf(FrrWord::from_str(interface.name())?);
+                            // Ospf only allows one area per interface, so one interface cannot be
+                            // in two areas (fabrics). Though even if this happens, it is not a big
+                            // problem as frr filters it out.
+                            if let Entry::Vacant(e) = interfaces.entry(interface_name) {
+                                e.insert(ospf_interface.into());
+                            } else {
+                                tracing::warn!("An interface cannot be in multiple ospf areas");
+                            }
+                            Ok::<(), anyhow::Error>(())
+                        })?;
+                    } else {
+                        tracing::warn!("no node configuration for fabric \"{fabric_id}\" – this fabric is not configured for this node.");
+                        return Ok::<(), anyhow::Error>(()); 
+                    }
+                    Ok(())
+                })?;
+        }
+        Ok(FrrConfig { router, interfaces })
+    }
+}
diff --git a/proxmox-frr/src/openfabric.rs b/proxmox-frr/src/openfabric.rs
new file mode 100644
index 000000000000..12cfc61236cb
--- /dev/null
+++ b/proxmox-frr/src/openfabric.rs
@@ -0,0 +1,137 @@
+use std::fmt::Debug;
+use std::{fmt::Display, str::FromStr};
+
+use proxmox_network_types::net::Net;
+use serde::{Deserialize, Serialize};
+use serde_with::{DeserializeFromStr, SerializeDisplay};
+
+#[cfg(feature = "config-ext")]
+use proxmox_ve_config::sdn::fabric::openfabric::{self, internal};
+use thiserror::Error;
+
+use crate::common::FrrWord;
+use crate::RouterNameError;
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, DeserializeFromStr, SerializeDisplay)]
+pub struct OpenFabricRouterName(FrrWord);
+
+impl From<FrrWord> for OpenFabricRouterName {
+    fn from(value: FrrWord) -> Self {
+        Self(value)
+    }
+}
+
+impl OpenFabricRouterName {
+    pub fn new(name: FrrWord) -> Self {
+        Self(name)
+    }
+}
+
+impl FromStr for OpenFabricRouterName {
+    type Err = RouterNameError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        if let Some(name) = s.strip_prefix("openfabric ") {
+            return Ok(Self::new(
+                FrrWord::from_str(name).map_err(|_| RouterNameError::InvalidName)?,
+            ));
+        }
+
+        Err(RouterNameError::InvalidName)
+    }
+}
+
+impl Display for OpenFabricRouterName {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "openfabric {}", self.0)
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
+pub struct OpenFabricRouter {
+    net: Net,
+}
+
+impl OpenFabricRouter {
+    pub fn new(net: Net) -> Self {
+        Self {
+            net,
+        }
+    }
+
+    pub fn net(&self) -> &Net {
+        &self.net
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
+pub struct OpenFabricInterface {
+    // Note: an interface can only be a part of a single fabric (so no vec needed here)
+    fabric_id: OpenFabricRouterName,
+    passive: Option<bool>,
+    hello_interval: Option<openfabric::HelloInterval>,
+    csnp_interval: Option<openfabric::CsnpInterval>,
+    hello_multiplier: Option<openfabric::HelloMultiplier>,
+}
+
+impl OpenFabricInterface {
+    pub fn fabric_id(&self) -> &OpenFabricRouterName {
+        &self.fabric_id
+    }
+    pub fn passive(&self) -> &Option<bool> {
+        &self.passive
+    }
+    pub fn hello_interval(&self) -> &Option<openfabric::HelloInterval> {
+        &self.hello_interval
+    }
+    pub fn csnp_interval(&self) -> &Option<openfabric::CsnpInterval> {
+        &self.csnp_interval
+    }
+    pub fn hello_multiplier(&self) -> &Option<openfabric::HelloMultiplier> {
+        &self.hello_multiplier
+    }
+    pub fn set_hello_interval(&mut self, interval: Option<openfabric::HelloInterval>) {
+        self.hello_interval = interval;
+    }
+}
+
+#[derive(Error, Debug)]
+pub enum OpenFabricInterfaceError {
+    #[error("Unknown error converting to OpenFabricInterface")]
+    UnknownError,
+    #[error("Error converting router name")]
+    RouterNameError(#[from] RouterNameError),
+}
+
+#[cfg(feature = "config-ext")]
+impl TryFrom<(&internal::FabricId, &internal::Interface)> for OpenFabricInterface {
+    type Error = OpenFabricInterfaceError;
+
+    fn try_from(value: (&internal::FabricId, &internal::Interface)) -> Result<Self, Self::Error> {
+        Ok(Self {
+            fabric_id: OpenFabricRouterName::try_from(value.0)?,
+            passive: value.1.passive(),
+            hello_interval: value.1.hello_interval().clone(),
+            csnp_interval: value.1.csnp_interval().clone(),
+            hello_multiplier: value.1.hello_multiplier().clone(),
+        })
+    }
+}
+
+#[cfg(feature = "config-ext")]
+impl TryFrom<&internal::FabricId> for OpenFabricRouterName {
+    type Error = RouterNameError;
+
+    fn try_from(value: &internal::FabricId) -> Result<Self, Self::Error> {
+        Ok(OpenFabricRouterName::new(FrrWord::new(value.to_string())?))
+    }
+}
+
+#[cfg(feature = "config-ext")]
+impl From<(&internal::FabricConfig, &internal::NodeConfig)> for OpenFabricRouter {
+    fn from(value: (&internal::FabricConfig, &internal::NodeConfig)) -> Self {
+        Self {
+            net: value.1.net().to_owned(),
+        }
+    }
+}
diff --git a/proxmox-frr/src/ospf.rs b/proxmox-frr/src/ospf.rs
new file mode 100644
index 000000000000..a14ef2c55c27
--- /dev/null
+++ b/proxmox-frr/src/ospf.rs
@@ -0,0 +1,148 @@
+use std::fmt::Debug;
+use std::net::Ipv4Addr;
+use std::{fmt::Display, str::FromStr};
+
+use serde::{Deserialize, Serialize};
+
+#[cfg(feature = "config-ext")]
+use proxmox_ve_config::sdn::fabric::ospf::internal;
+use thiserror::Error;
+
+use crate::common::{FrrWord, FrrWordError};
+
+/// The name of the ospf frr router. There is only one ospf fabric possible in frr (ignoring
+/// multiple invocations of the ospfd daemon) and the separation is done with areas. Still,
+/// different areas have the same frr router, so the name of the router is just "ospf" in "router
+/// ospf". This type still contains the Area so that we can insert it in the Hashmap.
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
+pub struct OspfRouterName(Area);
+
+impl From<Area> for OspfRouterName {
+    fn from(value: Area) -> Self {
+        Self(value)
+    }
+}
+
+impl Display for OspfRouterName {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "ospf")
+    }
+}
+
+#[derive(Error, Debug)]
+pub enum AreaParsingError {
+    #[error("Invalid area idenitifier. Area must be a number or an ipv4 address.")]
+    InvalidArea,
+    #[error("Invalid area idenitifier. Missing 'area' prefix.")]
+    MissingPrefix,
+    #[error("Error parsing to FrrWord")]
+    FrrWordError(#[from] FrrWordError),
+}
+
+/// The OSPF Area. Most commonly, this is just a number, e.g. 5, but sometimes also a
+/// pseudo-ipaddress, e.g. 0.0.0.0
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
+pub struct Area(FrrWord);
+
+impl TryFrom<FrrWord> for Area {
+    type Error = AreaParsingError;
+
+    fn try_from(value: FrrWord) -> Result<Self, Self::Error> {
+        Area::new(value)
+    }
+}
+
+impl Area {
+    pub fn new(name: FrrWord) -> Result<Self, AreaParsingError> {
+        if name.as_ref().parse::<i32>().is_ok() || name.as_ref().parse::<Ipv4Addr>().is_ok() {
+            Ok(Self(name))
+        } else {
+            Err(AreaParsingError::InvalidArea)
+        }
+    }
+}
+
+impl FromStr for Area {
+    type Err = AreaParsingError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        if let Some(name) = s.strip_prefix("area ") {
+            return Self::new(FrrWord::from_str(name).map_err(|_| AreaParsingError::InvalidArea)?);
+        }
+
+        Err(AreaParsingError::MissingPrefix)
+    }
+}
+
+impl Display for Area {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "area {}", self.0)
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
+pub struct OspfRouter {
+    router_id: Ipv4Addr,
+}
+
+impl OspfRouter {
+    pub fn new(router_id: Ipv4Addr) -> Self {
+        Self { router_id }
+    }
+
+    pub fn router_id(&self) -> &Ipv4Addr {
+        &self.router_id
+    }
+}
+
+#[derive(Error, Debug)]
+pub enum OspfInterfaceParsingError {
+    #[error("Error parsing area")]
+    AreaParsingError(#[from] AreaParsingError)
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
+pub struct OspfInterface {
+    // Note: an interface can only be a part of a single area(so no vec needed here)
+    area: Area,
+    passive: Option<bool>,
+}
+
+impl OspfInterface {
+    pub fn area(&self) -> &Area {
+        &self.area
+    }
+    pub fn passive(&self) -> &Option<bool> {
+        &self.passive
+    }
+}
+
+#[cfg(feature = "config-ext")]
+impl TryFrom<(&internal::Area, &internal::Interface)> for OspfInterface {
+    type Error = OspfInterfaceParsingError;
+
+    fn try_from(value: (&internal::Area, &internal::Interface)) -> Result<Self, Self::Error> {
+        Ok(Self {
+            area: Area::try_from(value.0)?,
+            passive: value.1.passive(),
+        })
+    }
+}
+
+#[cfg(feature = "config-ext")]
+impl TryFrom<&internal::Area> for Area {
+    type Error = AreaParsingError;
+
+    fn try_from(value: &internal::Area) -> Result<Self, Self::Error> {
+        Area::new(FrrWord::new(value.to_string())?)
+    }
+}
+
+#[cfg(feature = "config-ext")]
+impl From<(&internal::FabricConfig, &internal::NodeConfig)> for OspfRouter {
+    fn from(value: (&internal::FabricConfig, &internal::NodeConfig)) -> Self {
+        Self {
+            router_id: value.1.router_id,
+        }
+    }
+}
-- 
2.39.5



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

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

* [pve-devel] [PATCH proxmox-ve-rs 03/11] add intermediate fabric representation
  2025-02-14 13:39 [pve-devel] [RFC cluster/manager/network/proxmox{-ve-rs, -perl-rs} 00/11] Add SDN Fabrics Gabriel Goller
  2025-02-14 13:39 ` [pve-devel] [PATCH proxmox-ve-rs 01/11] add crate with common network types Gabriel Goller
  2025-02-14 13:39 ` [pve-devel] [PATCH proxmox-ve-rs 02/11] add proxmox-frr crate with frr types Gabriel Goller
@ 2025-02-14 13:39 ` Gabriel Goller
  2025-02-28 13:57   ` Thomas Lamprecht
  2025-03-04  8:45   ` Stefan Hanreich
  2025-02-14 13:39 ` [pve-devel] [PATCH proxmox-perl-rs 04/11] fabrics: add CRUD and generate fabrics methods Gabriel Goller
                   ` (8 subsequent siblings)
  11 siblings, 2 replies; 32+ messages in thread
From: Gabriel Goller @ 2025-02-14 13:39 UTC (permalink / raw)
  To: pve-devel

This adds the intermediate, type-checked fabrics config. This one is
parsed from the SectionConfig and can be converted into the
Frr-Representation.

Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 proxmox-ve-config/Cargo.toml                  |  10 +-
 proxmox-ve-config/debian/control              |   4 +-
 proxmox-ve-config/src/sdn/fabric/common.rs    |  90 ++++
 proxmox-ve-config/src/sdn/fabric/mod.rs       |  68 +++
 .../src/sdn/fabric/openfabric.rs              | 494 ++++++++++++++++++
 proxmox-ve-config/src/sdn/fabric/ospf.rs      | 375 +++++++++++++
 proxmox-ve-config/src/sdn/mod.rs              |   1 +
 7 files changed, 1036 insertions(+), 6 deletions(-)
 create mode 100644 proxmox-ve-config/src/sdn/fabric/common.rs
 create mode 100644 proxmox-ve-config/src/sdn/fabric/mod.rs
 create mode 100644 proxmox-ve-config/src/sdn/fabric/openfabric.rs
 create mode 100644 proxmox-ve-config/src/sdn/fabric/ospf.rs

diff --git a/proxmox-ve-config/Cargo.toml b/proxmox-ve-config/Cargo.toml
index 0c8f6166e75d..3a0fc9fa6618 100644
--- a/proxmox-ve-config/Cargo.toml
+++ b/proxmox-ve-config/Cargo.toml
@@ -10,13 +10,15 @@ exclude.workspace = true
 log = "0.4"
 anyhow = "1"
 nix = "0.26"
-thiserror = "1.0.59"
+thiserror = { workspace = true }
 
-serde = { version = "1", features = [ "derive" ] }
+serde = { workspace = true, features = [ "derive" ] }
+serde_with = { workspace = true }
 serde_json = "1"
 serde_plain = "1"
-serde_with = "3"
 
-proxmox-schema = "3.1.2"
+proxmox-section-config = { workspace = true }
+proxmox-schema = "4.0.0"
 proxmox-sys = "0.6.4"
 proxmox-sortable-macro = "0.1.3"
+proxmox-network-types = { path = "../proxmox-network-types/" }
diff --git a/proxmox-ve-config/debian/control b/proxmox-ve-config/debian/control
index 24814c11b471..bff03afba747 100644
--- a/proxmox-ve-config/debian/control
+++ b/proxmox-ve-config/debian/control
@@ -9,7 +9,7 @@ Build-Depends: cargo:native,
                librust-log-0.4+default-dev (>= 0.4.17-~~),
                librust-nix-0.26+default-dev (>= 0.26.1-~~),
                librust-thiserror-dev (>= 1.0.59-~~),
-               librust-proxmox-schema-3+default-dev,
+               librust-proxmox-schema-4+default-dev,
                librust-proxmox-sortable-macro-dev,
                librust-proxmox-sys-dev,
                librust-serde-1+default-dev,
@@ -33,7 +33,7 @@ Depends:
  librust-log-0.4+default-dev (>= 0.4.17-~~),
  librust-nix-0.26+default-dev (>= 0.26.1-~~),
  librust-thiserror-dev (>= 1.0.59-~~),
- librust-proxmox-schema-3+default-dev,
+ librust-proxmox-schema-4+default-dev,
  librust-proxmox-sortable-macro-dev,
  librust-proxmox-sys-dev,
  librust-serde-1+default-dev,
diff --git a/proxmox-ve-config/src/sdn/fabric/common.rs b/proxmox-ve-config/src/sdn/fabric/common.rs
new file mode 100644
index 000000000000..400f5b6d6b12
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/fabric/common.rs
@@ -0,0 +1,90 @@
+use serde::{Deserialize, Serialize};
+use std::fmt::Display;
+use thiserror::Error;
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Error)]
+pub enum ConfigError {
+    #[error("node id has invalid format")]
+    InvalidNodeId,
+}
+
+#[derive(Debug, Deserialize, Serialize, Clone, Eq, Hash, PartialOrd, Ord, PartialEq)]
+pub struct Hostname(String);
+
+impl From<String> for Hostname {
+    fn from(value: String) -> Self {
+        Hostname::new(value)
+    }
+}
+
+impl AsRef<str> for Hostname {
+    fn as_ref(&self) -> &str {
+        &self.0
+    }
+}
+
+impl Display for Hostname {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+impl Hostname {
+    pub fn new(name: impl Into<String>) -> Hostname {
+        Self(name.into())
+    }
+}
+
+// parses a bool from a string OR bool
+pub mod serde_option_bool {
+    use std::fmt;
+
+    use serde::{
+        de::{Deserializer, Error, Visitor}, ser::Serializer
+    };
+
+    use crate::firewall::parse::parse_bool;
+
+    pub fn deserialize<'de, D: Deserializer<'de>>(
+        deserializer: D,
+    ) -> Result<Option<bool>, D::Error> {
+        struct V;
+
+        impl<'de> Visitor<'de> for V {
+            type Value = Option<bool>;
+
+            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
+                f.write_str("a boolean-like value")
+            }
+
+            fn visit_bool<E: Error>(self, v: bool) -> Result<Self::Value, E> {
+                Ok(Some(v))
+            }
+
+            fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
+                parse_bool(v).map_err(E::custom).map(Some)
+            }
+
+            fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
+                Ok(None)
+            }
+
+            fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
+            where
+                D: Deserializer<'de>,
+            {
+                deserializer.deserialize_any(self)
+            }
+        }
+
+        deserializer.deserialize_any(V)
+    }
+
+    pub fn serialize<S: Serializer>(from: &Option<bool>, serializer: S) -> Result<S::Ok, S::Error> {
+        if *from == Some(true) {
+            serializer.serialize_str("1")
+        } else {
+            serializer.serialize_str("0")
+        }
+    }
+}
diff --git a/proxmox-ve-config/src/sdn/fabric/mod.rs b/proxmox-ve-config/src/sdn/fabric/mod.rs
new file mode 100644
index 000000000000..6453fb9bb98f
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/fabric/mod.rs
@@ -0,0 +1,68 @@
+pub mod common;
+pub mod openfabric;
+pub mod ospf;
+
+use proxmox_section_config::typed::ApiSectionDataEntry;
+use proxmox_section_config::typed::SectionConfigData;
+use serde::de::DeserializeOwned;
+use serde::Deserialize;
+use serde::Serialize;
+
+#[derive(Serialize, Deserialize, Debug, Default)]
+pub struct FabricConfig {
+    openfabric: Option<openfabric::internal::OpenFabricConfig>,
+    ospf: Option<ospf::internal::OspfConfig>,
+}
+
+impl FabricConfig {
+    pub fn new(raw_openfabric: &str, raw_ospf: &str) -> Result<Self, anyhow::Error> {
+        let openfabric =
+            openfabric::internal::OpenFabricConfig::default(raw_openfabric)?;
+        let ospf = ospf::internal::OspfConfig::default(raw_ospf)?;
+
+        Ok(Self {
+            openfabric: Some(openfabric),
+            ospf: Some(ospf),
+        })
+    }
+
+    pub fn openfabric(&self) -> &Option<openfabric::internal::OpenFabricConfig>{
+        &self.openfabric
+    }
+    pub fn ospf(&self) -> &Option<ospf::internal::OspfConfig>{
+        &self.ospf
+    }
+
+    pub fn with_openfabric(config: openfabric::internal::OpenFabricConfig) -> FabricConfig {
+        Self {
+            openfabric: Some(config),
+            ospf: None,
+        }
+    }
+
+    pub fn with_ospf(config: ospf::internal::OspfConfig) -> FabricConfig {
+        Self {
+            ospf: Some(config),
+            openfabric: None,
+        }
+    }
+}
+
+pub trait FromSectionConfig
+where
+    Self: Sized + TryFrom<SectionConfigData<Self::Section>>,
+    <Self as TryFrom<SectionConfigData<Self::Section>>>::Error: std::fmt::Debug,
+{
+    type Section: ApiSectionDataEntry + DeserializeOwned;
+
+    fn from_section_config(raw: &str) -> Result<Self, anyhow::Error> {
+        let section_config_data = Self::Section::section_config()
+            .parse(Self::filename(), raw)?
+            .try_into()?;
+
+        let output = Self::try_from(section_config_data).unwrap();
+        Ok(output)
+    }
+
+    fn filename() -> String;
+}
diff --git a/proxmox-ve-config/src/sdn/fabric/openfabric.rs b/proxmox-ve-config/src/sdn/fabric/openfabric.rs
new file mode 100644
index 000000000000..531610f7d7e9
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/fabric/openfabric.rs
@@ -0,0 +1,494 @@
+use proxmox_network_types::net::Net;
+use proxmox_schema::property_string::PropertyString;
+use proxmox_sortable_macro::sortable;
+use std::{fmt::Display, num::ParseIntError, sync::OnceLock};
+
+use crate::sdn::fabric::common::serde_option_bool;
+use internal::OpenFabricConfig;
+use proxmox_schema::{
+    ApiStringFormat, ApiType, ArraySchema, BooleanSchema, IntegerSchema, ObjectSchema, Schema,
+    StringSchema,
+};
+use proxmox_section_config::{typed::ApiSectionDataEntry, SectionConfig, SectionConfigPlugin};
+use serde::{Deserialize, Serialize};
+use thiserror::Error;
+
+use super::FromSectionConfig;
+
+#[sortable]
+const FABRIC_SCHEMA: ObjectSchema = ObjectSchema::new(
+    "fabric schema",
+    &sorted!([(
+        "hello_interval",
+        true,
+        &IntegerSchema::new("OpenFabric hello_interval in seconds")
+            .minimum(1)
+            .maximum(600)
+            .schema(),
+    ),]),
+);
+
+#[sortable]
+const INTERFACE_SCHEMA: Schema = ObjectSchema::new(
+    "interface",
+    &sorted!([
+        (
+            "hello_interval",
+            true,
+            &IntegerSchema::new("OpenFabric Hello interval in seconds")
+                .minimum(1)
+                .maximum(600)
+                .schema(),
+        ),
+        (
+            "name",
+            false,
+            &StringSchema::new("Interface name")
+                .min_length(1)
+                .max_length(15)
+                .schema(),
+        ),
+        (
+            "passive",
+            true,
+            &BooleanSchema::new("OpenFabric passive mode for this interface").schema(),
+        ),
+        (
+            "csnp_interval",
+            true,
+            &IntegerSchema::new("OpenFabric csnp interval in seconds")
+                .minimum(1)
+                .maximum(600)
+                .schema()
+        ),
+        (
+            "hello_multiplier",
+            true,
+            &IntegerSchema::new("OpenFabric multiplier for Hello holding time")
+                .minimum(2)
+                .maximum(100)
+                .schema()
+        ),
+    ]),
+)
+.schema();
+
+#[sortable]
+const NODE_SCHEMA: ObjectSchema = ObjectSchema::new(
+    "node schema",
+    &sorted!([
+        (
+            "interface",
+            false,
+            &ArraySchema::new(
+                "OpenFabric name",
+                &StringSchema::new("OpenFabric Interface")
+                    .format(&ApiStringFormat::PropertyString(&INTERFACE_SCHEMA))
+                    .schema(),
+            )
+            .schema(),
+        ),
+        (
+            "net",
+            true,
+            &StringSchema::new("OpenFabric net").min_length(3).schema(),
+        ),
+    ]),
+);
+
+const ID_SCHEMA: Schema = StringSchema::new("id").min_length(2).schema();
+
+#[derive(Error, Debug)]
+pub enum IntegerRangeError {
+    #[error("The value must be between {min} and {max} seconds")]
+    OutOfRange { min: i32, max: i32 },
+    #[error("Error parsing to number")]
+    ParsingError(#[from] ParseIntError),
+}
+
+#[derive(Serialize, Deserialize, Hash, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+pub struct CsnpInterval(u16);
+
+impl TryFrom<u16> for CsnpInterval {
+    type Error = IntegerRangeError;
+
+    fn try_from(number: u16) -> Result<Self, Self::Error> {
+        if (1..=600).contains(&number) {
+            Ok(CsnpInterval(number))
+        } else {
+            Err(IntegerRangeError::OutOfRange { min: 1, max: 600 })
+        }
+    }
+}
+
+impl Display for CsnpInterval {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+#[derive(Serialize, Deserialize, Hash, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+pub struct HelloInterval(u16);
+
+impl TryFrom<u16> for HelloInterval {
+    type Error = IntegerRangeError;
+
+    fn try_from(number: u16) -> Result<Self, Self::Error> {
+        if (1..=600).contains(&number) {
+            Ok(HelloInterval(number))
+        } else {
+            Err(IntegerRangeError::OutOfRange { min: 1, max: 600 })
+        }
+    }
+}
+
+impl Display for HelloInterval {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+#[derive(Serialize, Deserialize, Hash, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+pub struct HelloMultiplier(u16);
+
+impl TryFrom<u16> for HelloMultiplier {
+    type Error = IntegerRangeError;
+
+    fn try_from(number: u16) -> Result<Self, Self::Error> {
+        if (2..=100).contains(&number) {
+            Ok(HelloMultiplier(number))
+        } else {
+            Err(IntegerRangeError::OutOfRange { min: 2, max: 100 })
+        }
+    }
+}
+
+impl Display for HelloMultiplier {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct FabricSection {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub hello_interval: Option<HelloInterval>,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+pub struct NodeSection {
+    pub net: Net,
+    pub interface: Vec<PropertyString<InterfaceProperties>>,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+pub struct InterfaceProperties {
+    pub name: String,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    #[serde(default, with = "serde_option_bool")]
+    pub passive: Option<bool>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub hello_interval: Option<HelloInterval>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub csnp_interval: Option<CsnpInterval>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub hello_multiplier: Option<HelloMultiplier>,
+}
+
+impl InterfaceProperties {
+    pub fn passive(&self) -> Option<bool> {
+        self.passive
+    }
+}
+
+impl ApiType for InterfaceProperties {
+    const API_SCHEMA: Schema = INTERFACE_SCHEMA;
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone)]
+pub enum OpenFabricSectionConfig {
+    #[serde(rename = "fabric")]
+    Fabric(FabricSection),
+    #[serde(rename = "node")]
+    Node(NodeSection),
+}
+
+impl ApiSectionDataEntry for OpenFabricSectionConfig {
+    const INTERNALLY_TAGGED: Option<&'static str> = None;
+
+    fn section_config() -> &'static SectionConfig {
+        static SC: OnceLock<SectionConfig> = OnceLock::new();
+
+        SC.get_or_init(|| {
+            let mut config = SectionConfig::new(&ID_SCHEMA);
+
+            let fabric_plugin =
+                SectionConfigPlugin::new("fabric".to_string(), None, &FABRIC_SCHEMA);
+            config.register_plugin(fabric_plugin);
+
+            let node_plugin = SectionConfigPlugin::new("node".to_string(), None, &NODE_SCHEMA);
+            config.register_plugin(node_plugin);
+
+            config
+        })
+    }
+
+    fn section_type(&self) -> &'static str {
+        match self {
+            Self::Node(_) => "node",
+            Self::Fabric(_) => "fabric",
+        }
+    }
+}
+
+pub mod internal {
+    use std::{collections::HashMap, fmt::Display, str::FromStr};
+
+    use proxmox_network_types::net::Net;
+    use serde::{Deserialize, Serialize};
+    use thiserror::Error;
+
+    use proxmox_section_config::typed::SectionConfigData;
+
+    use crate::sdn::fabric::common::{self, ConfigError, Hostname};
+
+    use super::{
+        CsnpInterval, FabricSection, FromSectionConfig, HelloInterval, HelloMultiplier,
+        InterfaceProperties, NodeSection, OpenFabricSectionConfig,
+    };
+
+    #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+    pub struct FabricId(String);
+
+    impl FabricId {
+        pub fn new(id: impl Into<String>) -> Result<Self, anyhow::Error> {
+            Ok(Self(id.into()))
+        }
+    }
+
+    impl AsRef<str> for FabricId {
+        fn as_ref(&self) -> &str {
+            &self.0
+        }
+    }
+
+    impl FromStr for FabricId {
+        type Err = anyhow::Error;
+
+        fn from_str(s: &str) -> Result<Self, Self::Err> {
+            Self::new(s)
+        }
+    }
+
+    impl From<String> for FabricId {
+        fn from(value: String) -> Self {
+            FabricId(value)
+        }
+    }
+
+    impl Display for FabricId {
+        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+            self.0.fmt(f)
+        }
+    }
+
+    /// The NodeId comprises node and fabric information.
+    ///
+    /// It has a format of "{fabric}_{node}". This is because the node alone doesn't suffice, we need
+    /// to store the fabric as well (a node can be apart of multiple fabrics).
+    #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+    pub struct NodeId {
+        pub fabric: FabricId,
+        pub node: Hostname,
+    }
+
+    impl NodeId {
+        pub fn new(fabric: impl Into<FabricId>, node: impl Into<Hostname>) -> NodeId {
+            Self {
+                fabric: fabric.into(),
+                node: node.into(),
+            }
+        }
+    }
+
+    impl Display for NodeId {
+        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+            write!(f, "{}_{}", self.fabric, self.node)
+        }
+    }
+
+    impl FromStr for NodeId {
+        type Err = ConfigError;
+
+        fn from_str(s: &str) -> Result<Self, Self::Err> {
+            if let Some((fabric_id, node_id)) = s.split_once('_') {
+                return Ok(Self::new(fabric_id.to_string(), node_id.to_string()));
+            }
+
+            Err(ConfigError::InvalidNodeId)
+        }
+    }
+
+    #[derive(Debug, Deserialize, Serialize)]
+    pub struct OpenFabricConfig {
+        fabrics: HashMap<FabricId, FabricConfig>,
+    }
+
+    impl OpenFabricConfig {
+        pub fn fabrics(&self) -> &HashMap<FabricId, FabricConfig> {
+            &self.fabrics
+        }
+    }
+
+    #[derive(Debug, Deserialize, Serialize)]
+    pub struct FabricConfig {
+        nodes: HashMap<Hostname, NodeConfig>,
+        hello_interval: Option<HelloInterval>,
+    }
+
+    impl FabricConfig {
+        pub fn nodes(&self) -> &HashMap<Hostname, NodeConfig> {
+            &self.nodes
+        }
+        pub fn hello_interval(&self) -> &Option<HelloInterval> {
+            &self.hello_interval
+        }
+    }
+
+    impl TryFrom<FabricSection> for FabricConfig {
+        type Error = OpenFabricConfigError;
+
+        fn try_from(value: FabricSection) -> Result<Self, Self::Error> {
+            Ok(FabricConfig {
+                nodes: HashMap::new(),
+                hello_interval: value.hello_interval,
+            })
+        }
+    }
+
+    #[derive(Debug, Deserialize, Serialize)]
+    pub struct NodeConfig {
+        net: Net,
+        interfaces: Vec<Interface>,
+    }
+
+    impl NodeConfig {
+        pub fn net(&self) -> &Net {
+            &self.net
+        }
+        pub fn interfaces(&self) -> impl Iterator<Item = &Interface> + '_ {
+            self.interfaces.iter()
+        }
+    }
+
+    impl TryFrom<NodeSection> for NodeConfig {
+        type Error = OpenFabricConfigError;
+
+        fn try_from(value: NodeSection) -> Result<Self, Self::Error> {
+            Ok(NodeConfig {
+                net: value.net,
+                interfaces: value
+                    .interface
+                    .into_iter()
+                    .map(|i| Interface::try_from(i.into_inner()).unwrap())
+                    .collect(),
+            })
+        }
+    }
+
+    #[derive(Debug, Deserialize, Serialize)]
+    pub struct Interface {
+        name: String,
+        passive: Option<bool>,
+        hello_interval: Option<HelloInterval>,
+        csnp_interval: Option<CsnpInterval>,
+        hello_multiplier: Option<HelloMultiplier>,
+    }
+
+    impl Interface {
+        pub fn name(&self) -> &str {
+            &self.name
+        }
+        pub fn passive(&self) -> Option<bool> {
+            self.passive
+        }
+        pub fn hello_interval(&self) -> &Option<HelloInterval> {
+            &self.hello_interval
+        }
+        pub fn csnp_interval(&self) -> &Option<CsnpInterval> {
+            &self.csnp_interval
+        }
+        pub fn hello_multiplier(&self) -> &Option<HelloMultiplier> {
+            &self.hello_multiplier
+        }
+    }
+
+    impl TryFrom<InterfaceProperties> for Interface {
+        type Error = OpenFabricConfigError;
+
+        fn try_from(value: InterfaceProperties) -> Result<Self, Self::Error> {
+            Ok(Interface {
+                name: value.name.clone(),
+                passive: value.passive(),
+                hello_interval: value.hello_interval,
+                csnp_interval: value.csnp_interval,
+                hello_multiplier: value.hello_multiplier,
+            })
+        }
+    }
+
+    #[derive(Error, Debug)]
+    pub enum OpenFabricConfigError {
+        #[error("Unknown error occured")]
+        Unknown,
+        #[error("NodeId parse error")]
+        NodeIdError(#[from] common::ConfigError),
+        #[error("Corresponding fabric to the node not found")]
+        FabricNotFound,
+    }
+
+    impl TryFrom<SectionConfigData<OpenFabricSectionConfig>> for OpenFabricConfig {
+        type Error = OpenFabricConfigError;
+
+        fn try_from(
+            value: SectionConfigData<OpenFabricSectionConfig>,
+        ) -> Result<Self, Self::Error> {
+            let mut fabrics = HashMap::new();
+            let mut nodes = HashMap::new();
+
+            for (id, config) in value {
+                match config {
+                    OpenFabricSectionConfig::Fabric(fabric_section) => {
+                        fabrics.insert(FabricId::from(id), FabricConfig::try_from(fabric_section)?);
+                    }
+                    OpenFabricSectionConfig::Node(node_section) => {
+                        nodes.insert(id.parse::<NodeId>()?, NodeConfig::try_from(node_section)?);
+                    }
+                }
+            }
+
+            for (id, node) in nodes {
+                let fabric = fabrics
+                    .get_mut(&id.fabric)
+                    .ok_or(OpenFabricConfigError::FabricNotFound)?;
+
+                fabric.nodes.insert(id.node, node);
+            }
+            Ok(OpenFabricConfig { fabrics })
+        }
+    }
+
+    impl OpenFabricConfig {
+        pub fn default(raw: &str) -> Result<Self, anyhow::Error> {
+            OpenFabricConfig::from_section_config(raw)
+        }
+    }
+}
+
+impl FromSectionConfig for OpenFabricConfig {
+    type Section = OpenFabricSectionConfig;
+
+    fn filename() -> String {
+        "ospf.cfg".to_owned()
+    }
+}
diff --git a/proxmox-ve-config/src/sdn/fabric/ospf.rs b/proxmox-ve-config/src/sdn/fabric/ospf.rs
new file mode 100644
index 000000000000..2f2720a5759f
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/fabric/ospf.rs
@@ -0,0 +1,375 @@
+use internal::OspfConfig;
+use proxmox_schema::property_string::PropertyString;
+use proxmox_schema::ObjectSchema;
+use proxmox_schema::{ApiStringFormat, ApiType, ArraySchema, BooleanSchema, Schema, StringSchema};
+use proxmox_section_config::{typed::ApiSectionDataEntry, SectionConfig, SectionConfigPlugin};
+use proxmox_sortable_macro::sortable;
+use serde::{Deserialize, Serialize};
+use std::sync::OnceLock;
+
+use super::FromSectionConfig;
+
+#[sortable]
+const FABRIC_SCHEMA: ObjectSchema = ObjectSchema::new(
+    "fabric schema",
+    &sorted!([(
+        "area",
+        true,
+        &StringSchema::new("Area identifier").min_length(1).schema()
+    )]),
+);
+
+#[sortable]
+const INTERFACE_SCHEMA: Schema = ObjectSchema::new(
+    "interface",
+    &sorted!([
+        (
+            "name",
+            false,
+            &StringSchema::new("Interface name")
+                .min_length(1)
+                .max_length(15)
+                .schema(),
+        ),
+        (
+            "passive",
+            true,
+            &BooleanSchema::new("passive interface").schema(),
+        ),
+    ]),
+)
+.schema();
+
+#[sortable]
+const NODE_SCHEMA: ObjectSchema = ObjectSchema::new(
+    "node schema",
+    &sorted!([
+        (
+            "interface",
+            false,
+            &ArraySchema::new(
+                "OSPF name",
+                &StringSchema::new("OSPF Interface")
+                    .format(&ApiStringFormat::PropertyString(&INTERFACE_SCHEMA))
+                    .schema(),
+            )
+            .schema(),
+        ),
+        (
+            "router_id",
+            true,
+            &StringSchema::new("OSPF router id").min_length(3).schema(),
+        ),
+    ]),
+);
+
+const ID_SCHEMA: Schema = StringSchema::new("id").min_length(2).schema();
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+pub struct InterfaceProperties {
+    pub name: String,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub passive: Option<bool>,
+}
+
+impl ApiType for InterfaceProperties {
+    const API_SCHEMA: Schema = INTERFACE_SCHEMA;
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+pub struct NodeSection {
+    pub router_id: String,
+    pub interface: Vec<PropertyString<InterfaceProperties>>,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct FabricSection {}
+
+#[derive(Serialize, Deserialize, Debug, Clone)]
+pub enum OspfSectionConfig {
+    #[serde(rename = "fabric")]
+    Fabric(FabricSection),
+    #[serde(rename = "node")]
+    Node(NodeSection),
+}
+
+impl ApiSectionDataEntry for OspfSectionConfig {
+    const INTERNALLY_TAGGED: Option<&'static str> = None;
+
+    fn section_config() -> &'static SectionConfig {
+        static SC: OnceLock<SectionConfig> = OnceLock::new();
+
+        SC.get_or_init(|| {
+            let mut config = SectionConfig::new(&ID_SCHEMA);
+
+            let fabric_plugin = SectionConfigPlugin::new(
+                "fabric".to_string(),
+                Some("area".to_string()),
+                &FABRIC_SCHEMA,
+            );
+            config.register_plugin(fabric_plugin);
+
+            let node_plugin = SectionConfigPlugin::new("node".to_string(), None, &NODE_SCHEMA);
+            config.register_plugin(node_plugin);
+
+            config
+        })
+    }
+
+    fn section_type(&self) -> &'static str {
+        match self {
+            Self::Node(_) => "node",
+            Self::Fabric(_) => "fabric",
+        }
+    }
+}
+
+pub mod internal {
+    use std::{
+        collections::HashMap,
+        fmt::Display,
+        net::{AddrParseError, Ipv4Addr},
+        str::FromStr,
+    };
+
+    use serde::{Deserialize, Serialize};
+    use thiserror::Error;
+
+    use proxmox_section_config::typed::SectionConfigData;
+
+    use crate::sdn::fabric::{common::Hostname, FromSectionConfig};
+
+    use super::{FabricSection, InterfaceProperties, NodeSection, OspfSectionConfig};
+
+    #[derive(Error, Debug)]
+    pub enum NodeIdError {
+        #[error("Invalid area identifier")]
+        InvalidArea(#[from] AreaParsingError),
+        #[error("Invalid node identifier")]
+        InvalidNodeId,
+    }
+
+    /// The NodeId comprises node and fabric(area) information.
+    ///
+    /// It has a format of "{area}_{node}". This is because the node alone doesn't suffice, we need
+    /// to store the fabric as well (a node can be apart of multiple fabrics).
+    #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+    pub struct NodeId {
+        pub area: Area,
+        pub node: Hostname,
+    }
+
+    impl NodeId {
+        pub fn new(fabric: String, node: String) -> Result<NodeId, NodeIdError> {
+            Ok(Self {
+                area: fabric.try_into()?,
+                node: node.into(),
+            })
+        }
+    }
+
+    impl Display for NodeId {
+        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+            write!(f, "{}_{}", self.area, self.node)
+        }
+    }
+
+    impl FromStr for NodeId {
+        type Err = NodeIdError;
+
+        fn from_str(s: &str) -> Result<Self, Self::Err> {
+            if let Some((area_id, node_id)) = s.split_once('_') {
+                return Self::new(area_id.to_owned(), node_id.to_owned());
+            }
+
+            Err(Self::Err::InvalidNodeId)
+        }
+    }
+
+    #[derive(Error, Debug)]
+    pub enum OspfConfigError {
+        #[error("Unknown error occured")]
+        Unknown,
+        #[error("Error parsing router id ip address")]
+        RouterIdParseError(#[from] AddrParseError),
+        #[error("The corresponding fabric for this node has not been found")]
+        FabricNotFound,
+        #[error("The OSPF Area could not be parsed")]
+        AreaParsingError(#[from] AreaParsingError),
+        #[error("NodeId parse error")]
+        NodeIdError(#[from] NodeIdError),
+    }
+
+    #[derive(Error, Debug)]
+    pub enum AreaParsingError {
+        #[error("Invalid area identifier. Area must be a number or a ipv4 address.")]
+        InvalidArea,
+    }
+
+    /// OSPF Area, which is unique and is used to differentiate between different ospf fabrics.
+    #[derive(Debug, Deserialize, Serialize, Hash, PartialEq, Eq, PartialOrd, Ord, Clone)]
+    pub struct Area(String);
+
+    impl Area {
+        pub fn new(area: String) -> Result<Area, AreaParsingError> {
+            if area.parse::<i32>().is_ok() || area.parse::<Ipv4Addr>().is_ok() {
+                Ok(Self(area))
+            } else {
+                Err(AreaParsingError::InvalidArea)
+            }
+        }
+    }
+
+    impl TryFrom<String> for Area {
+        type Error = AreaParsingError;
+
+        fn try_from(value: String) -> Result<Self, Self::Error> {
+            Area::new(value)
+        }
+    }
+
+    impl AsRef<str> for Area {
+        fn as_ref(&self) -> &str {
+            &self.0
+        }
+    }
+
+    impl Display for Area {
+        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+            self.0.fmt(f)
+        }
+    }
+
+    #[derive(Debug, Deserialize, Serialize)]
+    pub struct OspfConfig {
+        fabrics: HashMap<Area, FabricConfig>,
+    }
+
+    impl OspfConfig {
+        pub fn fabrics(&self) -> &HashMap<Area, FabricConfig> {
+            &self.fabrics
+        }
+    }
+
+    #[derive(Debug, Deserialize, Serialize)]
+    pub struct FabricConfig {
+        nodes: HashMap<Hostname, NodeConfig>,
+    }
+
+    impl FabricConfig {
+        pub fn nodes(&self) -> &HashMap<Hostname, NodeConfig> {
+            &self.nodes
+        }
+    }
+
+    impl TryFrom<FabricSection> for FabricConfig {
+        type Error = OspfConfigError;
+
+        fn try_from(_value: FabricSection) -> Result<Self, Self::Error> {
+            // currently no attributes here
+            Ok(FabricConfig {
+                nodes: HashMap::new(),
+            })
+        }
+    }
+
+    #[derive(Debug, Deserialize, Serialize)]
+    pub struct NodeConfig {
+        pub router_id: Ipv4Addr,
+        pub interfaces: Vec<Interface>,
+    }
+
+    impl NodeConfig {
+        pub fn router_id(&self) -> &Ipv4Addr {
+            &self.router_id
+        }
+        pub fn interfaces(&self) -> impl Iterator<Item = &Interface> + '_ {
+            self.interfaces.iter()
+        }
+    }
+
+    impl TryFrom<NodeSection> for NodeConfig {
+        type Error = OspfConfigError;
+
+        fn try_from(value: NodeSection) -> Result<Self, Self::Error> {
+            Ok(NodeConfig {
+                router_id: value.router_id.parse()?,
+                interfaces: value
+                    .interface
+                    .into_iter()
+                    .map(|i| Interface::try_from(i.into_inner()).unwrap())
+                    .collect(),
+            })
+        }
+    }
+
+    #[derive(Debug, Deserialize, Serialize)]
+    pub struct Interface {
+        name: String,
+        passive: Option<bool>,
+    }
+
+    impl Interface {
+        pub fn name(&self) -> &str {
+            &self.name
+        }
+        pub fn passive(&self) -> Option<bool> {
+            self.passive
+        }
+    }
+
+    impl TryFrom<InterfaceProperties> for Interface {
+        type Error = OspfConfigError;
+
+        fn try_from(value: InterfaceProperties) -> Result<Self, Self::Error> {
+            Ok(Interface {
+                name: value.name.clone(),
+                passive: value.passive,
+            })
+        }
+    }
+
+    impl TryFrom<SectionConfigData<OspfSectionConfig>> for OspfConfig {
+        type Error = OspfConfigError;
+
+        fn try_from(value: SectionConfigData<OspfSectionConfig>) -> Result<Self, Self::Error> {
+            let mut fabrics = HashMap::new();
+            let mut nodes = HashMap::new();
+
+            for (id, config) in value {
+                match config {
+                    OspfSectionConfig::Fabric(fabric_section) => {
+                        fabrics
+                            .insert(Area::try_from(id)?, FabricConfig::try_from(fabric_section)?);
+                    }
+                    OspfSectionConfig::Node(node_section) => {
+                        nodes.insert(id.parse::<NodeId>()?, NodeConfig::try_from(node_section)?);
+                    }
+                }
+            }
+
+            for (id, node) in nodes {
+                let fabric = fabrics
+                    .get_mut(&id.area)
+                    .ok_or(OspfConfigError::FabricNotFound)?;
+
+                fabric.nodes.insert(id.node, node);
+            }
+            Ok(OspfConfig { fabrics })
+        }
+    }
+
+    impl OspfConfig {
+        pub fn default(raw: &str) -> Result<Self, anyhow::Error> {
+            OspfConfig::from_section_config(raw)
+        }
+    }
+}
+
+impl FromSectionConfig for OspfConfig {
+    type Section = OspfSectionConfig;
+
+    fn filename() -> String {
+        "ospf.cfg".to_owned()
+    }
+}
diff --git a/proxmox-ve-config/src/sdn/mod.rs b/proxmox-ve-config/src/sdn/mod.rs
index c8dc72471693..811fa21c483a 100644
--- a/proxmox-ve-config/src/sdn/mod.rs
+++ b/proxmox-ve-config/src/sdn/mod.rs
@@ -1,4 +1,5 @@
 pub mod config;
+pub mod fabric;
 pub mod ipam;
 
 use std::{error::Error, fmt::Display, str::FromStr};
-- 
2.39.5



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


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

* [pve-devel] [PATCH proxmox-perl-rs 04/11] fabrics: add CRUD and generate fabrics methods
  2025-02-14 13:39 [pve-devel] [RFC cluster/manager/network/proxmox{-ve-rs, -perl-rs} 00/11] Add SDN Fabrics Gabriel Goller
                   ` (2 preceding siblings ...)
  2025-02-14 13:39 ` [pve-devel] [PATCH proxmox-ve-rs 03/11] add intermediate fabric representation Gabriel Goller
@ 2025-02-14 13:39 ` Gabriel Goller
  2025-03-04  9:28   ` Stefan Hanreich
  2025-02-14 13:39 ` [pve-devel] [PATCH pve-cluster 05/11] cluster: add sdn fabrics config files Gabriel Goller
                   ` (7 subsequent siblings)
  11 siblings, 1 reply; 32+ messages in thread
From: Gabriel Goller @ 2025-02-14 13:39 UTC (permalink / raw)
  To: pve-devel

Add CRUD and generate fabrics method for perlmod. These can be called
from perl with the raw configuration to edit/read/update/delete the
configuration. It also contains functions to generate the frr config
from the passed SectionConfig.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 pve-rs/Cargo.toml            |   5 +-
 pve-rs/Makefile              |   3 +
 pve-rs/src/lib.rs            |   1 +
 pve-rs/src/sdn/fabrics.rs    | 202 ++++++++++++++++
 pve-rs/src/sdn/mod.rs        |   3 +
 pve-rs/src/sdn/openfabric.rs | 454 +++++++++++++++++++++++++++++++++++
 pve-rs/src/sdn/ospf.rs       | 425 ++++++++++++++++++++++++++++++++
 7 files changed, 1092 insertions(+), 1 deletion(-)
 create mode 100644 pve-rs/src/sdn/fabrics.rs
 create mode 100644 pve-rs/src/sdn/mod.rs
 create mode 100644 pve-rs/src/sdn/openfabric.rs
 create mode 100644 pve-rs/src/sdn/ospf.rs

diff --git a/pve-rs/Cargo.toml b/pve-rs/Cargo.toml
index 4b6dec6ff452..67806810e560 100644
--- a/pve-rs/Cargo.toml
+++ b/pve-rs/Cargo.toml
@@ -40,9 +40,12 @@ proxmox-log = "0.2"
 proxmox-notify = { version = "0.5", features = ["pve-context"] }
 proxmox-openid = "0.10"
 proxmox-resource-scheduling = "0.3.0"
+proxmox-schema = "4.0.0"
+proxmox-section-config = "2.1.1"
 proxmox-shared-cache = "0.1.0"
 proxmox-subscription = "0.5"
 proxmox-sys = "0.6"
 proxmox-tfa = { version = "5", features = ["api"] }
 proxmox-time = "2"
-proxmox-ve-config = { version = "0.2.1" }
+proxmox-ve-config = "0.2.1"
+proxmox-frr = { version = "0.1", features = ["config-ext"] }
diff --git a/pve-rs/Makefile b/pve-rs/Makefile
index d01da692d8c9..5bd4d3c58b36 100644
--- a/pve-rs/Makefile
+++ b/pve-rs/Makefile
@@ -31,6 +31,9 @@ PERLMOD_PACKAGES := \
 	  PVE::RS::Firewall::SDN \
 	  PVE::RS::OpenId \
 	  PVE::RS::ResourceScheduling::Static \
+	  PVE::RS::SDN::Fabrics \
+	  PVE::RS::SDN::Fabrics::OpenFabric \
+	  PVE::RS::SDN::Fabrics::Ospf \
 	  PVE::RS::TFA
 
 PERLMOD_PACKAGE_FILES := $(addsuffix .pm,$(subst ::,/,$(PERLMOD_PACKAGES)))
diff --git a/pve-rs/src/lib.rs b/pve-rs/src/lib.rs
index 3de37d17fab6..12ee87a91cc6 100644
--- a/pve-rs/src/lib.rs
+++ b/pve-rs/src/lib.rs
@@ -15,6 +15,7 @@ pub mod apt;
 pub mod firewall;
 pub mod openid;
 pub mod resource_scheduling;
+pub mod sdn;
 pub mod tfa;
 
 #[perlmod::package(name = "Proxmox::Lib::PVE", lib = "pve_rs")]
diff --git a/pve-rs/src/sdn/fabrics.rs b/pve-rs/src/sdn/fabrics.rs
new file mode 100644
index 000000000000..53c7f47bec4c
--- /dev/null
+++ b/pve-rs/src/sdn/fabrics.rs
@@ -0,0 +1,202 @@
+#[perlmod::package(name = "PVE::RS::SDN::Fabrics", lib = "pve_rs")]
+pub mod export {
+    use std::{collections::HashMap, fmt, str::FromStr, sync::Mutex};
+
+    use anyhow::Error;
+    use proxmox_frr::{
+        openfabric::{OpenFabricInterface, OpenFabricRouter},
+        ospf::{OspfInterface, OspfRouter},
+        FrrConfig, Interface, Router,
+    };
+    use proxmox_section_config::{
+        typed::ApiSectionDataEntry, typed::SectionConfigData as TypedSectionConfigData,
+    };
+    use proxmox_ve_config::sdn::fabric::{
+        openfabric::OpenFabricSectionConfig,
+        ospf::OspfSectionConfig,
+    };
+    use serde::{Deserialize, Serialize};
+
+    #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)]
+    pub struct PerlRouter {
+        #[serde(skip_serializing_if = "HashMap::is_empty")]
+        address_family: HashMap<String, Vec<String>>,
+        #[serde(rename = "")]
+        root_properties: Vec<String>,
+    }
+
+    impl From<&Router> for PerlRouter {
+        fn from(value: &Router) -> Self {
+            match value {
+                Router::OpenFabric(router) => PerlRouter::from(router),
+                Router::Ospf(router) => PerlRouter::from(router),
+            }
+        }
+    }
+
+    impl From<&OpenFabricRouter> for PerlRouter {
+        fn from(value: &OpenFabricRouter) -> Self {
+            let mut router = PerlRouter::default();
+            router.root_properties.push(format!("net {}", value.net()));
+
+            router
+        }
+    }
+
+    impl From<&OspfRouter> for PerlRouter {
+        fn from(value: &OspfRouter) -> Self {
+            let mut router = PerlRouter::default();
+            router
+                .root_properties
+                .push(format!("ospf router-id {}", value.router_id()));
+
+            router
+        }
+    }
+
+    #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)]
+    pub struct PerlInterfaceProperties(Vec<String>);
+
+    impl From<&Interface> for PerlInterfaceProperties {
+        fn from(value: &Interface) -> Self {
+            match value {
+                Interface::OpenFabric(openfabric) => PerlInterfaceProperties::from(openfabric),
+                Interface::Ospf(ospf) => PerlInterfaceProperties::from(ospf),
+            }
+        }
+    }
+
+    impl From<&OpenFabricInterface> for PerlInterfaceProperties {
+        fn from(value: &OpenFabricInterface) -> Self {
+            let mut interface = PerlInterfaceProperties::default();
+            // Note: the "openfabric" is printed by the OpenFabricRouterName Display impl
+            interface.0.push(format!("ip router {}", value.fabric_id()));
+            if *value.passive() == Some(true) {
+                interface.0.push("openfabric passive".to_string());
+            }
+            if let Some(hello_interval) = value.hello_interval() {
+                interface
+                    .0
+                    .push(format!("openfabric hello-interval {}", hello_interval));
+            }
+            if let Some(csnp_interval) = value.csnp_interval() {
+                interface
+                    .0
+                    .push(format!("openfabric csnp-interval {}", csnp_interval));
+            }
+            if let Some(hello_multiplier) = value.hello_multiplier() {
+                interface
+                    .0
+                    .push(format!("openfabric hello-multiplier {}", hello_multiplier));
+            }
+
+            interface
+        }
+    }
+    impl From<&OspfInterface> for PerlInterfaceProperties {
+        fn from(value: &OspfInterface) -> Self {
+            let mut interface = PerlInterfaceProperties::default();
+            // the area is printed by the Display impl.
+            interface.0.push(format!("ip ospf {}", value.area()));
+            if *value.passive() == Some(true) {
+                interface.0.push("ip ospf passive".to_string());
+            }
+
+            interface
+        }
+    }
+
+    #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
+    pub struct PerlFrrRouter {
+        pub router: HashMap<String, PerlRouter>,
+    }
+
+    #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
+    pub struct PerlFrrConfig {
+        frr: PerlFrrRouter,
+        frr_interface: HashMap<String, PerlInterfaceProperties>,
+    }
+
+    #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
+    pub enum Protocol {
+        #[serde(rename = "openfabric")]
+        OpenFabric,
+        #[serde(rename = "ospf")]
+        Ospf,
+    }
+
+    /// Will be used as a filename in the write method in pve-cluster, so this should not be
+    /// changed unless the filename of the config is also changed.
+    impl fmt::Display for Protocol {
+        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+            write!(f, "{}", format!("{:?}", self).to_lowercase())
+        }
+    }
+
+    impl FromStr for Protocol {
+        type Err = anyhow::Error;
+
+        fn from_str(input: &str) -> Result<Protocol, Self::Err> {
+            match input {
+                "openfabric" => Ok(Protocol::OpenFabric),
+                "ospf" => Ok(Protocol::Ospf),
+                _ => Err(anyhow::anyhow!("protocol not implemented")),
+            }
+        }
+    }
+
+    pub struct PerlSectionConfig<T> {
+        pub section_config: Mutex<TypedSectionConfigData<T>>,
+    }
+
+    impl<T> PerlSectionConfig<T>
+    where
+        T: Send + Sync + Clone,
+    {
+        pub fn into_inner(self) -> Result<TypedSectionConfigData<T>, anyhow::Error> {
+            let value = self.section_config.into_inner().unwrap();
+            Ok(value.clone())
+        }
+    }
+
+    impl From<FrrConfig> for PerlFrrConfig {
+        fn from(value: FrrConfig) -> PerlFrrConfig {
+            let router = PerlFrrRouter {
+                router: value
+                    .router()
+                    .map(|(name, data)| (name.to_string(), PerlRouter::from(data)))
+                    .collect(),
+            };
+
+            Self {
+                frr: router,
+                frr_interface: value
+                    .interfaces()
+                    .map(|(name, data)| (name.to_string(), PerlInterfaceProperties::from(data)))
+                    .collect(),
+            }
+        }
+    }
+
+    #[derive(Serialize, Deserialize)]
+    struct AllConfigs {
+        openfabric: HashMap<String, OpenFabricSectionConfig>,
+        ospf: HashMap<String, OspfSectionConfig>,
+    }
+
+    /// Get all the config. This takes the raw openfabric and ospf config, parses, and returns
+    /// both.
+    #[export]
+    fn config(raw_openfabric: &[u8], raw_ospf: &[u8]) -> Result<AllConfigs, Error> {
+        let raw_openfabric = std::str::from_utf8(raw_openfabric)?;
+        let raw_ospf = std::str::from_utf8(raw_ospf)?;
+
+        let openfabric = OpenFabricSectionConfig::parse_section_config("openfabric.cfg", raw_openfabric)?;
+        let ospf = OspfSectionConfig::parse_section_config("ospf.cfg", raw_ospf)?;
+
+        Ok(AllConfigs {
+            openfabric: openfabric.into_iter().collect(),
+            ospf: ospf.into_iter().collect(),
+        })
+    }
+}
diff --git a/pve-rs/src/sdn/mod.rs b/pve-rs/src/sdn/mod.rs
new file mode 100644
index 000000000000..6700c989483f
--- /dev/null
+++ b/pve-rs/src/sdn/mod.rs
@@ -0,0 +1,3 @@
+pub mod fabrics;
+pub mod openfabric;
+pub mod ospf;
diff --git a/pve-rs/src/sdn/openfabric.rs b/pve-rs/src/sdn/openfabric.rs
new file mode 100644
index 000000000000..1f84930fd0da
--- /dev/null
+++ b/pve-rs/src/sdn/openfabric.rs
@@ -0,0 +1,454 @@
+#[perlmod::package(name = "PVE::RS::SDN::Fabrics::OpenFabric", lib = "pve_rs")]
+mod export {
+    use core::str;
+    use std::{collections::HashMap, sync::{Mutex, MutexGuard}};
+
+    use anyhow::{Context, Error};
+    use perlmod::Value;
+    use proxmox_frr::FrrConfigBuilder;
+    use proxmox_schema::property_string::PropertyString;
+    use proxmox_section_config::typed::{ApiSectionDataEntry, SectionConfigData};
+    use proxmox_ve_config::sdn::fabric::{
+        openfabric::{internal::{FabricId, NodeId, OpenFabricConfig}, FabricSection, InterfaceProperties, NodeSection, OpenFabricSectionConfig}, FabricConfig,
+    };
+    use serde::{Deserialize, Serialize};
+
+    use crate::sdn::fabrics::export::{PerlFrrConfig, PerlSectionConfig};
+
+    perlmod::declare_magic!(Box<PerlSectionConfig<OpenFabricSectionConfig>> : &PerlSectionConfig<OpenFabricSectionConfig> as "PVE::RS::SDN::Fabrics::OpenFabric");
+
+    #[derive(Debug, Serialize, Deserialize)]
+    pub struct AddFabric {
+        name: String,
+        r#type: String,
+        #[serde(deserialize_with = "deserialize_empty_string_to_none")]
+        hello_interval: Option<u16>,
+    }
+
+    #[derive(Debug, Serialize, Deserialize)]
+    pub struct DeleteFabric {
+        fabric: String,
+    }
+
+    #[derive(Debug, Serialize, Deserialize)]
+    pub struct DeleteNode {
+        fabric: String,
+        node: String,
+    }
+
+    #[derive(Debug, Serialize, Deserialize)]
+    pub struct DeleteInterface {
+        fabric: String,
+        node: String,
+        /// interface name
+        name: String,
+    }
+
+    fn deserialize_empty_string_to_none<'de, D>(deserializer: D) -> Result<Option<u16>, D::Error>
+    where
+        D: serde::de::Deserializer<'de>,
+    {
+        let s: &str = serde::de::Deserialize::deserialize(deserializer)?;
+        if s.is_empty() {
+            Ok(None)
+        } else {
+            serde_json::from_str(s).map_err(serde::de::Error::custom)
+        }
+    }
+
+    #[derive(Debug, Serialize, Deserialize)]
+    pub struct EditFabric {
+        fabric: String,
+        #[serde(deserialize_with = "deserialize_empty_string_to_none")]
+        hello_interval: Option<u16>,
+    }
+
+    #[derive(Debug, Deserialize)]
+    pub struct AddNode {
+        fabric: String,
+        node: String,
+        net: String,
+        interfaces: Vec<String>,
+    }
+
+    #[derive(Debug, Serialize, Deserialize)]
+    pub struct EditNode {
+        node: String,
+        fabric: String,
+        net: String,
+        interfaces: Vec<String>,
+    }
+
+    #[derive(Debug, Serialize, Deserialize)]
+    pub struct EditInterface {
+        node: String,
+        fabric: String,
+        name: String,
+        passive: bool,
+        #[serde(deserialize_with = "deserialize_empty_string_to_none")]
+        hello_interval: Option<u16>,
+        #[serde(deserialize_with = "deserialize_empty_string_to_none")]
+        hello_multiplier: Option<u16>,
+        #[serde(deserialize_with = "deserialize_empty_string_to_none")]
+        csnp_interval: Option<u16>,
+    }
+    
+    fn interface_exists(
+        config: &MutexGuard<SectionConfigData<OpenFabricSectionConfig>>,
+        interface_name: &str,
+        node_name: &str,
+    ) -> bool {
+        config.sections.iter().any(|(k, v)| {
+            if let OpenFabricSectionConfig::Node(n) = v {
+                k.parse::<NodeId>().ok().is_some_and(|id| {
+                    id.node.as_ref() == node_name
+                        && n.interface.iter().any(|i| i.name == interface_name)
+                })
+            } else {
+                false
+            }
+        })
+    }
+
+    impl PerlSectionConfig<OpenFabricSectionConfig> {
+        pub fn add_fabric(&self, new_config: AddFabric) -> Result<(), anyhow::Error> {
+            let fabricid = FabricId::from(new_config.name).to_string();
+            let new_fabric = OpenFabricSectionConfig::Fabric(FabricSection {
+                hello_interval: new_config
+                    .hello_interval
+                    .map(|x| x.try_into())
+                    .transpose()?,
+            });
+            let mut config = self.section_config.lock().unwrap();
+            if config.sections.contains_key(&fabricid) {
+                anyhow::bail!("fabric already exists");
+            }
+            config.sections.insert(fabricid, new_fabric);
+            Ok(())
+        }
+
+        pub fn edit_fabric(&self, new_config: EditFabric) -> Result<(), anyhow::Error> {
+            let mut config = self.section_config.lock().unwrap();
+
+            let fabricid = new_config.fabric.parse::<FabricId>()?;
+
+            if let OpenFabricSectionConfig::Fabric(fs) = config
+                .sections
+                .get_mut(fabricid.as_ref())
+                .context("fabric doesn't exists")?
+            {
+                fs.hello_interval = new_config
+                    .hello_interval
+                    .map(|x| x.try_into())
+                    .transpose()
+                    .unwrap_or(None);
+            }
+            Ok(())
+        }
+
+        pub fn add_node(&self, new_config: AddNode) -> Result<(), anyhow::Error> {
+            let mut interfaces: Vec<PropertyString<InterfaceProperties>> = vec![];
+            for i in new_config.interfaces {
+                let ps: PropertyString<InterfaceProperties> = i.parse()?;
+                interfaces.push(ps);
+            }
+
+            let nodeid = NodeId::new(new_config.fabric, new_config.node);
+            let nodeid_key = nodeid.to_string();
+
+            let mut config = self.section_config.lock().unwrap();
+            if config.sections.contains_key(&nodeid_key) {
+                anyhow::bail!("node already exists");
+            }
+            if interfaces.iter().any(|i| interface_exists(&config, &i.name, nodeid.node.as_ref())) {
+                anyhow::bail!("One interface cannot be a part of two fabrics");
+            }
+            let new_fabric = OpenFabricSectionConfig::Node(NodeSection {
+                net: new_config.net.parse()?,
+                interface: interfaces,
+            });
+            config.sections.insert(nodeid_key, new_fabric);
+            Ok(())
+        }
+
+        pub fn edit_node(&self, new_config: EditNode) -> Result<(), anyhow::Error> {
+            let mut interfaces: Vec<PropertyString<InterfaceProperties>> = vec![];
+            for i in new_config.interfaces {
+                let ps: PropertyString<InterfaceProperties> = i.parse()?;
+                interfaces.push(ps);
+            }
+            let net = new_config.net.parse()?;
+
+            let nodeid = NodeId::new(new_config.fabric, new_config.node).to_string();
+
+            let mut config = self.section_config.lock().unwrap();
+            if !config.sections.contains_key(&nodeid) {
+                anyhow::bail!("node not found");
+            }
+            config.sections.entry(nodeid).and_modify(|n| {
+                if let OpenFabricSectionConfig::Node(n) = n {
+                    n.net = net;
+                    n.interface = interfaces;
+                }
+            });
+            Ok(())
+        }
+
+        pub fn edit_interface(&self, new_config: EditInterface) -> Result<(), anyhow::Error> {
+            let mut config = self.section_config.lock().unwrap();
+            let nodeid = NodeId::new(new_config.fabric, new_config.node).to_string();
+            if !config.sections.contains_key(&nodeid) {
+                anyhow::bail!("interface not found");
+            }
+
+            config.sections.entry(nodeid).and_modify(|n| {
+                if let OpenFabricSectionConfig::Node(n) = n {
+                    n.interface.iter_mut().for_each(|i| {
+                        if i.name == new_config.name {
+                            i.passive = Some(new_config.passive);
+                            i.hello_interval =
+                                new_config.hello_interval.and_then(|hi| hi.try_into().ok());
+                            i.hello_multiplier =
+                                new_config.hello_multiplier.and_then(|ci| ci.try_into().ok());
+                            i.csnp_interval =
+                                new_config.csnp_interval.and_then(|ci| ci.try_into().ok());
+                        }
+                    });
+                }
+            });
+            Ok(())
+        }
+
+        pub fn delete_fabric(&self, new_config: DeleteFabric) -> Result<(), anyhow::Error> {
+            let mut config = self.section_config.lock().unwrap();
+
+            let fabricid = FabricId::new(new_config.fabric)?;
+
+            config
+                .sections
+                .remove(fabricid.as_ref())
+                .ok_or(anyhow::anyhow!("fabric not found"))?;
+            // remove all the nodes
+            config.sections.retain(|k, _v| {
+                if let Ok(nodeid) = k.parse::<NodeId>() {
+                    return nodeid.fabric != fabricid;
+                }
+                true
+            });
+            Ok(())
+        }
+
+        pub fn delete_node(&self, new_config: DeleteNode) -> Result<(), anyhow::Error> {
+            let mut config = self.section_config.lock().unwrap();
+            let nodeid = NodeId::new(new_config.fabric, new_config.node).to_string();
+            config
+                .sections
+                .remove(&nodeid)
+                .ok_or(anyhow::anyhow!("node not found"))?;
+            Ok(())
+        }
+
+        pub fn delete_interface(&self, new_config: DeleteInterface) -> Result<(), anyhow::Error> {
+            let mut config = self.section_config.lock().unwrap();
+            let mut removed = false;
+            let nodeid = NodeId::new(new_config.fabric, new_config.node).to_string();
+            config.sections.entry(nodeid).and_modify(|v| {
+                if let OpenFabricSectionConfig::Node(f) = v {
+                    if f.interface.len() > 1 {
+                        removed = true;
+                        f.interface.retain(|x| x.name != new_config.name);
+                    }
+                }
+            });
+            if !removed {
+                anyhow::bail!("error removing interface");
+            }
+            Ok(())
+        }
+
+        pub fn write(&self) -> Result<String, anyhow::Error> {
+            let guard = self.section_config.lock().unwrap().clone();
+            OpenFabricSectionConfig::write_section_config("sdn/fabrics/openfabric.cfg", &guard)
+        }
+    }
+
+    #[export(raw_return)]
+    fn config(#[raw] class: Value, raw_config: &[u8]) -> Result<perlmod::Value, anyhow::Error> {
+        let raw_config = std::str::from_utf8(raw_config)?;
+
+        let config = OpenFabricSectionConfig::parse_section_config("openfabric.cfg", raw_config)?;
+        let return_value = PerlSectionConfig {
+            section_config: Mutex::new(config),
+        };
+
+        Ok(perlmod::instantiate_magic!(&class, MAGIC => Box::new(
+                return_value
+        )))
+    }
+
+    /// Writes the config to a string and returns the configuration and the protocol.
+    #[export]
+    fn write(
+        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
+    ) -> Result<(String, String), Error> {
+        let full_new_config = this.write()?;
+
+        // We return the protocol here as well, so that in perl we can write to
+        // the correct config file
+        Ok((full_new_config, "openfabric".to_string()))
+    }
+
+    #[export]
+    fn add_fabric(
+        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
+        new_config: AddFabric,
+    ) -> Result<(), Error> {
+        this.add_fabric(new_config)?;
+
+        Ok(())
+    }
+
+    #[export]
+    fn add_node(
+        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
+        new_config: AddNode,
+    ) -> Result<(), Error> {
+        this.add_node(new_config)
+    }
+
+    #[export]
+    fn edit_fabric(
+        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
+        new_config: EditFabric,
+    ) -> Result<(), Error> {
+        this.edit_fabric(new_config)
+    }
+
+    #[export]
+    fn edit_node(
+        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
+        new_config: EditNode,
+    ) -> Result<(), Error> {
+        this.edit_node(new_config)
+    }
+
+    #[export]
+    fn edit_interface(
+        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
+        new_config: EditInterface,
+    ) -> Result<(), Error> {
+        this.edit_interface(new_config)
+    }
+
+    #[export]
+    fn delete_fabric(
+        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
+        delete_config: DeleteFabric,
+    ) -> Result<(), Error> {
+        this.delete_fabric(delete_config)?;
+
+        Ok(())
+    }
+
+    #[export]
+    fn delete_node(
+        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
+        delete_config: DeleteNode,
+    ) -> Result<(), Error> {
+        this.delete_node(delete_config)?;
+
+        Ok(())
+    }
+
+    #[export]
+    fn delete_interface(
+        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
+        delete_config: DeleteInterface,
+    ) -> Result<(), Error> {
+        this.delete_interface(delete_config)?;
+
+        Ok(())
+    }
+
+    #[export]
+    fn get_inner(
+        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
+    ) -> HashMap<String, OpenFabricSectionConfig> {
+        let guard = this.section_config.lock().unwrap();
+        guard.clone().into_iter().collect()
+    }
+
+    #[export]
+    fn get_fabric(
+        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
+        fabric: String,
+    ) -> Result<OpenFabricSectionConfig, Error> {
+        let guard = this.section_config.lock().unwrap();
+        guard
+            .get(&fabric)
+            .cloned()
+            .ok_or(anyhow::anyhow!("fabric not found"))
+    }
+
+    #[export]
+    fn get_node(
+        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
+        fabric: String,
+        node: String,
+    ) -> Result<OpenFabricSectionConfig, Error> {
+        let guard = this.section_config.lock().unwrap();
+        let nodeid = NodeId::new(fabric, node).to_string();
+        guard
+            .get(&nodeid)
+            .cloned()
+            .ok_or(anyhow::anyhow!("node not found"))
+    }
+
+    #[export]
+    fn get_interface(
+        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
+        fabric: String,
+        node: String,
+        interface_name: String,
+    ) -> Result<InterfaceProperties, Error> {
+        let guard = this.section_config.lock().unwrap();
+        let nodeid = NodeId::new(fabric, node).to_string();
+        guard
+            .get(&nodeid)
+            .and_then(|v| {
+                if let OpenFabricSectionConfig::Node(f) = v {
+                    let interface = f.interface.clone().into_iter().find_map(|i| {
+                        if i.name == interface_name {
+                            return Some(i.into_inner());
+                        }
+                        None
+                    });
+                    Some(interface)
+                } else {
+                    None
+                }
+            })
+            .flatten()
+            .ok_or(anyhow::anyhow!("interface not found"))
+    }
+
+    #[export]
+    pub fn get_perl_frr_repr(
+        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
+        hostname: &[u8],
+    ) -> Result<PerlFrrConfig, Error> {
+        let hostname = str::from_utf8(hostname)?;
+        let config = this.section_config.lock().unwrap();
+        let openfabric_config: OpenFabricConfig =
+            OpenFabricConfig::try_from(config.clone())?;
+
+        let config = FabricConfig::with_openfabric(openfabric_config);
+        let frr_config = FrrConfigBuilder::default()
+            .add_fabrics(config)
+            .build(hostname)?;
+
+        let perl_config = PerlFrrConfig::from(frr_config);
+
+        Ok(perl_config)
+    }
+}
diff --git a/pve-rs/src/sdn/ospf.rs b/pve-rs/src/sdn/ospf.rs
new file mode 100644
index 000000000000..d7d614fcbc2b
--- /dev/null
+++ b/pve-rs/src/sdn/ospf.rs
@@ -0,0 +1,425 @@
+#[perlmod::package(name = "PVE::RS::SDN::Fabrics::Ospf", lib = "pve_rs")]
+mod export {
+    use std::{
+        collections::HashMap,
+        str,
+        sync::{Mutex, MutexGuard},
+    };
+
+    use anyhow::{Context, Error};
+    use perlmod::Value;
+    use proxmox_frr::FrrConfigBuilder;
+    use proxmox_schema::property_string::PropertyString;
+    use proxmox_section_config::typed::{ApiSectionDataEntry, SectionConfigData};
+    use proxmox_ve_config::sdn::fabric::{
+        ospf::{
+            internal::{Area, NodeId, OspfConfig},
+            FabricSection, InterfaceProperties, NodeSection, OspfSectionConfig,
+        },
+        FabricConfig,
+    };
+    use serde::{Deserialize, Serialize};
+
+    use crate::sdn::fabrics::export::{PerlFrrConfig, PerlSectionConfig};
+
+    perlmod::declare_magic!(Box<PerlSectionConfig<OspfSectionConfig>> : &PerlSectionConfig<OspfSectionConfig> as "PVE::RS::SDN::Fabrics::Ospf");
+
+    #[derive(Debug, Serialize, Deserialize)]
+    pub struct AddFabric {
+        name: String,
+        r#type: String,
+    }
+
+    #[derive(Debug, Deserialize)]
+    pub struct AddNode {
+        node: String,
+        fabric: String,
+        router_id: String,
+        interfaces: Vec<String>,
+    }
+
+    #[derive(Debug, Serialize, Deserialize)]
+    pub struct DeleteFabric {
+        fabric: String,
+    }
+
+    #[derive(Debug, Serialize, Deserialize)]
+    pub struct DeleteNode {
+        fabric: String,
+        node: String,
+    }
+
+    #[derive(Debug, Serialize, Deserialize)]
+    pub struct DeleteInterface {
+        fabric: String,
+        node: String,
+        /// interface name
+        name: String,
+    }
+
+    #[derive(Debug, Serialize, Deserialize)]
+    pub struct EditFabric {
+        name: String,
+    }
+
+    #[derive(Debug, Serialize, Deserialize)]
+    pub struct EditNode {
+        fabric: String,
+        node: String,
+
+        router_id: String,
+        interfaces: Vec<String>,
+    }
+
+    #[derive(Debug, Serialize, Deserialize)]
+    pub struct EditInterface {
+        fabric: String,
+        node: String,
+        name: String,
+
+        passive: bool,
+    }
+
+    fn interface_exists(
+        config: &MutexGuard<SectionConfigData<OspfSectionConfig>>,
+        interface_name: &str,
+        node_name: &str,
+    ) -> bool {
+        config.sections.iter().any(|(k, v)| {
+            if let OspfSectionConfig::Node(n) = v {
+                k.parse::<NodeId>().ok().is_some_and(|id| {
+                    id.node.as_ref() == node_name
+                        && n.interface.iter().any(|i| i.name == interface_name)
+                })
+            } else {
+                false
+            }
+        })
+    }
+
+    impl PerlSectionConfig<OspfSectionConfig> {
+        pub fn add_fabric(&self, new_config: AddFabric) -> Result<(), anyhow::Error> {
+            let new_fabric = OspfSectionConfig::Fabric(FabricSection {});
+            let area = Area::new(new_config.name)?.to_string();
+            let mut config = self.section_config.lock().unwrap();
+            if config.sections.contains_key(&area) {
+                anyhow::bail!("fabric already exists");
+            }
+            config.sections.insert(area, new_fabric);
+            Ok(())
+        }
+
+        pub fn add_node(&self, new_config: AddNode) -> Result<(), anyhow::Error> {
+            let mut interfaces: Vec<PropertyString<InterfaceProperties>> = vec![];
+            for i in new_config.interfaces {
+                let ps: PropertyString<InterfaceProperties> = i.parse()?;
+                interfaces.push(ps);
+            }
+
+            let nodeid = NodeId::new(new_config.fabric, new_config.node)?;
+            let nodeid_key = nodeid.to_string();
+            let mut config = self.section_config.lock().unwrap();
+            if config.sections.contains_key(&nodeid_key) {
+                anyhow::bail!("node already exists");
+            }
+            if interfaces
+                .iter()
+                .any(|i| interface_exists(&config, &i.name, nodeid.node.as_ref()))
+            {
+                anyhow::bail!("One interface cannot be a part of two areas");
+            }
+
+            let new_fabric = OspfSectionConfig::Node(NodeSection {
+                router_id: new_config.router_id,
+                interface: interfaces,
+            });
+            config.sections.insert(nodeid_key, new_fabric);
+            Ok(())
+        }
+
+        pub fn edit_fabric(&self, new_config: EditFabric) -> Result<(), anyhow::Error> {
+            let mut config = self.section_config.lock().unwrap();
+
+            if let OspfSectionConfig::Fabric(_fs) = config
+                .sections
+                .get_mut(&new_config.name)
+                .context("fabric doesn't exists")?
+            {
+                // currently no properties exist here
+            }
+            Ok(())
+        }
+
+        pub fn delete_fabric(&self, new_config: DeleteFabric) -> Result<(), anyhow::Error> {
+            let mut config = self.section_config.lock().unwrap();
+
+            let area = Area::new(new_config.fabric)?;
+            config
+                .sections
+                .remove(area.as_ref())
+                .ok_or(anyhow::anyhow!("no fabric found"))?;
+
+            // remove all the nodes
+            config.sections.retain(|k, _v| {
+                if let Ok(nodeid) = k.parse::<NodeId>() {
+                    return nodeid.area != area;
+                }
+                true
+            });
+            Ok(())
+        }
+
+        pub fn edit_node(&self, new_config: EditNode) -> Result<(), anyhow::Error> {
+            let mut interfaces: Vec<PropertyString<InterfaceProperties>> = vec![];
+            for i in new_config.interfaces {
+                let ps: PropertyString<InterfaceProperties> = i.parse()?;
+                interfaces.push(ps);
+            }
+            let nodeid = NodeId::new(new_config.fabric, new_config.node)?.to_string();
+
+            let mut config = self.section_config.lock().unwrap();
+            if !config.sections.contains_key(&nodeid) {
+                anyhow::bail!("node not found");
+            }
+            config.sections.entry(nodeid).and_modify(|n| {
+                if let OspfSectionConfig::Node(n) = n {
+                    n.router_id = new_config.router_id;
+                    n.interface = interfaces;
+                }
+            });
+            Ok(())
+        }
+
+        pub fn edit_interface(&self, new_config: EditInterface) -> Result<(), anyhow::Error> {
+            let mut config = self.section_config.lock().unwrap();
+            let nodeid = NodeId::new(new_config.fabric, new_config.node)?.to_string();
+            if !config.sections.contains_key(&nodeid) {
+                anyhow::bail!("interface not found");
+            }
+
+            config.sections.entry(nodeid).and_modify(|n| {
+                if let OspfSectionConfig::Node(n) = n {
+                    n.interface.iter_mut().for_each(|i| {
+                        if i.name == new_config.name {
+                            i.passive = Some(new_config.passive);
+                        }
+                    });
+                }
+            });
+            Ok(())
+        }
+
+        pub fn delete_node(&self, new_config: DeleteNode) -> Result<(), anyhow::Error> {
+            let mut config = self.section_config.lock().unwrap();
+            let nodeid = NodeId::new(new_config.fabric, new_config.node)?.to_string();
+            config
+                .sections
+                .remove(&nodeid)
+                .ok_or(anyhow::anyhow!("node not found"))?;
+            Ok(())
+        }
+
+        pub fn delete_interface(&self, new_config: DeleteInterface) -> Result<(), anyhow::Error> {
+            let mut config = self.section_config.lock().unwrap();
+            let mut removed = false;
+            let nodeid = NodeId::new(new_config.fabric, new_config.node)?.to_string();
+            config.sections.entry(nodeid).and_modify(|v| {
+                if let OspfSectionConfig::Node(f) = v {
+                    if f.interface.len() > 1 {
+                        removed = true;
+                        f.interface.retain(|x| x.name != new_config.name);
+                    }
+                }
+            });
+            if !removed {
+                anyhow::bail!("error removing interface");
+            }
+            Ok(())
+        }
+
+        pub fn write(&self) -> Result<String, anyhow::Error> {
+            let guard = self.section_config.lock().unwrap().clone();
+            OspfSectionConfig::write_section_config("sdn/fabrics/ospf.cfg", &guard)
+        }
+    }
+
+    #[export(raw_return)]
+    fn config(#[raw] class: Value, raw_config: &[u8]) -> Result<perlmod::Value, anyhow::Error> {
+        let raw_config = std::str::from_utf8(raw_config)?;
+
+        let config = OspfSectionConfig::parse_section_config("ospf.cfg", raw_config)?;
+        let return_value = PerlSectionConfig {
+            section_config: Mutex::new(config),
+        };
+
+        Ok(perlmod::instantiate_magic!(&class, MAGIC => Box::new(
+                return_value
+        )))
+    }
+
+    /// Writes the config to a string and returns the configuration and the protocol.
+    #[export]
+    fn write(
+        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
+    ) -> Result<(String, String), Error> {
+        let full_new_config = this.write()?;
+
+        // We return the protocol here as well, so that in perl we can write to
+        // the correct config file
+        Ok((full_new_config, "ospf".to_string()))
+    }
+
+    #[export]
+    fn add_fabric(
+        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
+        new_config: AddFabric,
+    ) -> Result<(), Error> {
+        this.add_fabric(new_config)?;
+
+        Ok(())
+    }
+
+    #[export]
+    fn add_node(
+        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
+        new_config: AddNode,
+    ) -> Result<(), Error> {
+        this.add_node(new_config)
+    }
+
+    #[export]
+    fn edit_fabric(
+        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
+        new_config: EditFabric,
+    ) -> Result<(), Error> {
+        this.edit_fabric(new_config)
+    }
+
+    #[export]
+    fn edit_node(
+        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
+        new_config: EditNode,
+    ) -> Result<(), Error> {
+        this.edit_node(new_config)
+    }
+
+    #[export]
+    fn edit_interface(
+        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
+        new_config: EditInterface,
+    ) -> Result<(), Error> {
+        this.edit_interface(new_config)
+    }
+
+    #[export]
+    fn delete_fabric(
+        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
+        delete_config: DeleteFabric,
+    ) -> Result<(), Error> {
+        this.delete_fabric(delete_config)?;
+
+        Ok(())
+    }
+
+    #[export]
+    fn delete_node(
+        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
+        delete_config: DeleteNode,
+    ) -> Result<(), Error> {
+        this.delete_node(delete_config)?;
+
+        Ok(())
+    }
+
+    #[export]
+    fn delete_interface(
+        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
+        delete_config: DeleteInterface,
+    ) -> Result<(), Error> {
+        this.delete_interface(delete_config)?;
+
+        Ok(())
+    }
+
+    #[export]
+    fn get_inner(
+        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
+    ) -> HashMap<String, OspfSectionConfig> {
+        let guard = this.section_config.lock().unwrap();
+        guard.clone().into_iter().collect()
+    }
+
+    #[export]
+    fn get_fabric(
+        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
+        fabric: String,
+    ) -> Result<OspfSectionConfig, Error> {
+        let guard = this.section_config.lock().unwrap();
+        guard
+            .get(&fabric)
+            .cloned()
+            .ok_or(anyhow::anyhow!("fabric not found"))
+    }
+
+    #[export]
+    fn get_node(
+        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
+        fabric: String,
+        node: String,
+    ) -> Result<OspfSectionConfig, Error> {
+        let guard = this.section_config.lock().unwrap();
+        let nodeid = NodeId::new(fabric, node)?.to_string();
+        guard
+            .get(&nodeid)
+            .cloned()
+            .ok_or(anyhow::anyhow!("node not found"))
+    }
+
+    #[export]
+    fn get_interface(
+        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
+        fabric: String,
+        node: String,
+        interface_name: String,
+    ) -> Result<InterfaceProperties, Error> {
+        let guard = this.section_config.lock().unwrap();
+        let nodeid = NodeId::new(fabric, node)?.to_string();
+        guard
+            .get(&nodeid)
+            .and_then(|v| {
+                if let OspfSectionConfig::Node(f) = v {
+                    let interface = f.interface.clone().into_iter().find_map(|i| {
+                        let interface = i.into_inner();
+                        if interface.name == interface_name {
+                            return Some(interface);
+                        }
+                        None
+                    });
+                    Some(interface)
+                } else {
+                    None
+                }
+            })
+            .flatten()
+            .ok_or(anyhow::anyhow!("interface not found"))
+    }
+
+    #[export]
+    pub fn get_perl_frr_repr(
+        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
+        hostname: &[u8],
+    ) -> Result<PerlFrrConfig, Error> {
+        let hostname = str::from_utf8(hostname)?;
+        let config = this.section_config.lock().unwrap();
+        let openfabric_config: OspfConfig = OspfConfig::try_from(config.clone())?;
+
+        let config = FabricConfig::with_ospf(openfabric_config);
+        let frr_config = FrrConfigBuilder::default()
+            .add_fabrics(config)
+            .build(hostname)?;
+
+        let perl_config = PerlFrrConfig::from(frr_config);
+
+        Ok(perl_config)
+    }
+}
-- 
2.39.5



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


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

* [pve-devel] [PATCH pve-cluster 05/11] cluster: add sdn fabrics config files
  2025-02-14 13:39 [pve-devel] [RFC cluster/manager/network/proxmox{-ve-rs, -perl-rs} 00/11] Add SDN Fabrics Gabriel Goller
                   ` (3 preceding siblings ...)
  2025-02-14 13:39 ` [pve-devel] [PATCH proxmox-perl-rs 04/11] fabrics: add CRUD and generate fabrics methods Gabriel Goller
@ 2025-02-14 13:39 ` Gabriel Goller
  2025-02-28 12:19   ` Thomas Lamprecht
  2025-02-14 13:39 ` [pve-devel] [PATCH pve-network 06/11] add config file and common read/write methods Gabriel Goller
                   ` (6 subsequent siblings)
  11 siblings, 1 reply; 32+ messages in thread
From: Gabriel Goller @ 2025-02-14 13:39 UTC (permalink / raw)
  To: pve-devel

Add the sdn fabrics config files. These are split into two, as we
currently support two fabric types: ospf and openfabric. They hold the
whole configuration for the respective protocols. They are read and
written by pve-network.

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

diff --git a/src/PVE/Cluster.pm b/src/PVE/Cluster.pm
index e0e3ee995085..a325b67905b8 100644
--- a/src/PVE/Cluster.pm
+++ b/src/PVE/Cluster.pm
@@ -81,6 +81,8 @@ my $observed = {
     'sdn/pve-ipam-state.json' => 1,
     'sdn/mac-cache.json' => 1,
     'sdn/dns.cfg' => 1,
+    'sdn/fabrics/openfabric.cfg' => 1,
+    'sdn/fabrics/ospf.cfg' => 1,
     'sdn/.running-config' => 1,
     'virtual-guest/cpu-models.conf' => 1,
     'virtual-guest/profiles.cfg' => 1,
-- 
2.39.5



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


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

* [pve-devel] [PATCH pve-network 06/11] add config file and common read/write methods
  2025-02-14 13:39 [pve-devel] [RFC cluster/manager/network/proxmox{-ve-rs, -perl-rs} 00/11] Add SDN Fabrics Gabriel Goller
                   ` (4 preceding siblings ...)
  2025-02-14 13:39 ` [pve-devel] [PATCH pve-cluster 05/11] cluster: add sdn fabrics config files Gabriel Goller
@ 2025-02-14 13:39 ` Gabriel Goller
  2025-02-14 13:39 ` [pve-devel] [PATCH pve-network 07/11] merge the frr config with the fabrics frr config on apply Gabriel Goller
                   ` (5 subsequent siblings)
  11 siblings, 0 replies; 32+ messages in thread
From: Gabriel Goller @ 2025-02-14 13:39 UTC (permalink / raw)
  To: pve-devel

Add the config file for ospf and openfabric and add common read/write
functions. We also add the ospf/openfabric config to the
`.running-config` – even though we don't ready from it later.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 src/PVE/Network/SDN.pm         |  8 +++-
 src/PVE/Network/SDN/Fabrics.pm | 86 ++++++++++++++++++++++++++++++++++
 src/PVE/Network/SDN/Makefile   |  2 +-
 3 files changed, 94 insertions(+), 2 deletions(-)
 create mode 100644 src/PVE/Network/SDN/Fabrics.pm

diff --git a/src/PVE/Network/SDN.pm b/src/PVE/Network/SDN.pm
index c7dccfa89fcf..399435b07cd2 100644
--- a/src/PVE/Network/SDN.pm
+++ b/src/PVE/Network/SDN.pm
@@ -150,13 +150,19 @@ sub commit_config {
     my $zones_cfg = PVE::Network::SDN::Zones::config();
     my $controllers_cfg = PVE::Network::SDN::Controllers::config();
     my $subnets_cfg = PVE::Network::SDN::Subnets::config();
+    my $openfabrics_cfg_rust = PVE::Network::SDN::Fabrics::get_config("openfabric");
+    my $openfabrics_cfg = $openfabrics_cfg_rust->get_inner();
+    my $ospf_config_rust = PVE::Network::SDN::Fabrics::get_config("ospf");
+    my $ospf_cfg = $ospf_config_rust->get_inner();
 
     my $vnets = { ids => $vnets_cfg->{ids} };
     my $zones = { ids => $zones_cfg->{ids} };
     my $controllers = { ids => $controllers_cfg->{ids} };
     my $subnets = { ids => $subnets_cfg->{ids} };
+    my $openfabric = { ids => $openfabrics_cfg };
+    my $ospf = { ids => $ospf_cfg };
 
-    $cfg = { version => $version, vnets => $vnets, zones => $zones, controllers => $controllers, subnets => $subnets };
+    $cfg = { version => $version, vnets => $vnets, zones => $zones, controllers => $controllers, subnets => $subnets, openfabric => $openfabric, ospf => $ospf };
 
     cfs_write_file($running_cfg, $cfg);
 }
diff --git a/src/PVE/Network/SDN/Fabrics.pm b/src/PVE/Network/SDN/Fabrics.pm
new file mode 100644
index 000000000000..bbd07cf30624
--- /dev/null
+++ b/src/PVE/Network/SDN/Fabrics.pm
@@ -0,0 +1,86 @@
+package PVE::Network::SDN::Fabrics;
+
+use strict;
+use warnings;
+
+use PVE::Cluster qw(cfs_register_file cfs_read_file cfs_lock_file cfs_write_file);
+use PVE::RS::SDN::Fabrics;
+use PVE::RS::SDN::Fabrics::Ospf;
+use PVE::RS::SDN::Fabrics::OpenFabric;
+
+cfs_register_file(
+    'sdn/fabrics/openfabric.cfg',
+    \&parse_fabrics_config,
+    \&write_fabrics_config,
+);
+
+cfs_register_file(
+    'sdn/fabrics/ospf.cfg',
+    \&parse_fabrics_config,
+    \&write_fabrics_config,
+);
+
+sub parse_fabrics_config {
+    my ($filename, $raw) = @_;
+
+    $raw = '' if !defined($raw);
+    return $raw;
+}
+
+sub write_fabrics_config {
+    my ($filename, $config) = @_;
+    return $config;
+}
+
+sub lock_config {
+    my ($protocol, $code, $timeout) = @_;
+
+    if ($protocol eq "openfabric") {
+	cfs_lock_file('sdn/fabrics/openfabric.cfg', $timeout, $code);
+	die $@ if $@;
+    } elsif ($protocol eq "ospf") {
+	cfs_lock_file('sdn/fabrics/openfabric.cfg', $timeout, $code);
+	die $@ if $@;
+    } else {
+	die "cannot lock fabric config \"$protocol\": not implemented";
+    }
+}
+
+sub get_all_configs {
+    my $openfabric = cfs_read_file('sdn/fabrics/openfabric.cfg');
+    my $ospf = cfs_read_file('sdn/fabrics/ospf.cfg');
+
+    return PVE::RS::SDN::Fabrics::config($openfabric, $ospf);
+}
+
+sub get_config {
+    my ($protocol) = @_;
+
+    my $config;
+    my $fabric_config;
+
+    if ($protocol eq "openfabric") {
+	$config = cfs_read_file('sdn/fabrics/openfabric.cfg');
+	$fabric_config = PVE::RS::SDN::Fabrics::OpenFabric->config($config);
+    } elsif ($protocol eq "ospf") {
+	$config = cfs_read_file('sdn/fabrics/ospf.cfg');
+	$fabric_config = PVE::RS::SDN::Fabrics::Ospf->config($config);
+    } else {
+	die "cannot get fabric config \"$protocol\": not implemented";
+    }
+
+    return $fabric_config;
+}
+
+sub write_config {
+    my ($config) = @_;
+
+    my ($new_config, $protocol) = $config->write();
+
+    # It is safe to use the protocol in the path here as it comes from rust. There
+    # the protocol is stored in an enum so we know it is correct.
+    cfs_write_file("sdn/fabrics/$protocol.cfg", $new_config, 1);
+}
+
+1;
+
diff --git a/src/PVE/Network/SDN/Makefile b/src/PVE/Network/SDN/Makefile
index 3e6e5fb4c6f2..a256642e3044 100644
--- a/src/PVE/Network/SDN/Makefile
+++ b/src/PVE/Network/SDN/Makefile
@@ -1,4 +1,4 @@
-SOURCES=Vnets.pm VnetPlugin.pm Zones.pm Controllers.pm Subnets.pm SubnetPlugin.pm Ipams.pm Dns.pm Dhcp.pm
+SOURCES=Vnets.pm VnetPlugin.pm Zones.pm Controllers.pm Subnets.pm SubnetPlugin.pm Ipams.pm Dns.pm Dhcp.pm Fabrics.pm
 
 
 PERL5DIR=${DESTDIR}/usr/share/perl5
-- 
2.39.5



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

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

* [pve-devel] [PATCH pve-network 07/11] merge the frr config with the fabrics frr config on apply
  2025-02-14 13:39 [pve-devel] [RFC cluster/manager/network/proxmox{-ve-rs, -perl-rs} 00/11] Add SDN Fabrics Gabriel Goller
                   ` (5 preceding siblings ...)
  2025-02-14 13:39 ` [pve-devel] [PATCH pve-network 06/11] add config file and common read/write methods Gabriel Goller
@ 2025-02-14 13:39 ` Gabriel Goller
  2025-02-14 13:39 ` [pve-devel] [PATCH pve-network 08/11] add api endpoints for fabrics Gabriel Goller
                   ` (4 subsequent siblings)
  11 siblings, 0 replies; 32+ messages in thread
From: Gabriel Goller @ 2025-02-14 13:39 UTC (permalink / raw)
  To: pve-devel

Get the config directly from the file and convert it to the
Perl-Representation (the Frr-Representation), then generate the config
with the existing functions.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 src/PVE/Network/SDN/Controllers.pm            |  1 -
 src/PVE/Network/SDN/Controllers/EvpnPlugin.pm |  3 ---
 src/PVE/Network/SDN/Controllers/Frr.pm        | 13 +++++++++++++
 3 files changed, 13 insertions(+), 4 deletions(-)

diff --git a/src/PVE/Network/SDN/Controllers.pm b/src/PVE/Network/SDN/Controllers.pm
index 43f154b7338e..da3c957fe44d 100644
--- a/src/PVE/Network/SDN/Controllers.pm
+++ b/src/PVE/Network/SDN/Controllers.pm
@@ -143,7 +143,6 @@ sub generate_controller_config {
 
 
 sub reload_controller {
-
     my $cfg = PVE::Network::SDN::running_config();
     my $controller_cfg = $cfg->{controllers};
 
diff --git a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
index 6f875cb5dbf9..4b7120091b4b 100644
--- a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
+++ b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
@@ -359,14 +359,11 @@ sub find_isis_controller {
 
 sub generate_controller_rawconfig {
     my ($class, $plugin_config, $config) = @_;
-    #return PVE::Network::SDN::Controllers::Frr::generate_controller_rawconfig($class, $plugin_config, $config);
     die "implemented in the Frr helper";
 }
 
 sub write_controller_config {
     my ($class, $plugin_config, $config) = @_;
-    
-    #return PVE::Network::SDN::Controllers::Frr::write_controller_config($class, $plugin_config, $config);
     die "implemented in the Frr helper";
 }
 
diff --git a/src/PVE/Network/SDN/Controllers/Frr.pm b/src/PVE/Network/SDN/Controllers/Frr.pm
index 386dcae543e8..e9546f4d5e82 100644
--- a/src/PVE/Network/SDN/Controllers/Frr.pm
+++ b/src/PVE/Network/SDN/Controllers/Frr.pm
@@ -67,6 +67,19 @@ sub generate_controller_rawconfig {
     generate_frr_routemap($final_config, $config->{frr_routemap});
     generate_frr_simple_list($final_config, $config->{frr_ip_protocol});
 
+    # fabric config
+    # openfabric
+    my $openfabric_config = PVE::Network::SDN::Fabrics::get_config("openfabric");
+    my $openfabric_frr = $openfabric_config->get_perl_frr_repr($nodename);
+    generate_frr_interfaces($final_config, $openfabric_frr->{frr_interface});
+    generate_frr_recurse($final_config, $openfabric_frr->{frr}, undef, 0);
+
+    # ospf
+    my $ospf_config = PVE::Network::SDN::Fabrics::get_config("ospf");
+    my $ospf_frr = $ospf_config->get_perl_frr_repr($nodename);
+    generate_frr_interfaces($final_config, $ospf_frr->{frr_interface});
+    generate_frr_recurse($final_config, $ospf_frr->{frr}, undef, 0);
+
     push @{$final_config}, "!";
     push @{$final_config}, "line vty";
     push @{$final_config}, "!";
-- 
2.39.5



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


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

* [pve-devel] [PATCH pve-network 08/11] add api endpoints for fabrics
  2025-02-14 13:39 [pve-devel] [RFC cluster/manager/network/proxmox{-ve-rs, -perl-rs} 00/11] Add SDN Fabrics Gabriel Goller
                   ` (6 preceding siblings ...)
  2025-02-14 13:39 ` [pve-devel] [PATCH pve-network 07/11] merge the frr config with the fabrics frr config on apply Gabriel Goller
@ 2025-02-14 13:39 ` Gabriel Goller
  2025-03-04  9:51   ` Stefan Hanreich
  2025-02-14 13:39 ` [pve-devel] [PATCH pve-manager 09/11] sdn: add Fabrics view Gabriel Goller
                   ` (3 subsequent siblings)
  11 siblings, 1 reply; 32+ messages in thread
From: Gabriel Goller @ 2025-02-14 13:39 UTC (permalink / raw)
  To: pve-devel

Add api endpoints for CRUD of fabrics, nodes, and interfaces.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 src/PVE/API2/Network/SDN.pm                   |   7 +
 src/PVE/API2/Network/SDN/Fabrics.pm           |  57 +++
 src/PVE/API2/Network/SDN/Fabrics/Common.pm    | 111 +++++
 src/PVE/API2/Network/SDN/Fabrics/Makefile     |   9 +
 .../API2/Network/SDN/Fabrics/OpenFabric.pm    | 460 ++++++++++++++++++
 src/PVE/API2/Network/SDN/Fabrics/Ospf.pm      | 433 +++++++++++++++++
 src/PVE/API2/Network/SDN/Makefile             |   3 +-
 7 files changed, 1079 insertions(+), 1 deletion(-)
 create mode 100644 src/PVE/API2/Network/SDN/Fabrics.pm
 create mode 100644 src/PVE/API2/Network/SDN/Fabrics/Common.pm
 create mode 100644 src/PVE/API2/Network/SDN/Fabrics/Makefile
 create mode 100644 src/PVE/API2/Network/SDN/Fabrics/OpenFabric.pm
 create mode 100644 src/PVE/API2/Network/SDN/Fabrics/Ospf.pm

diff --git a/src/PVE/API2/Network/SDN.pm b/src/PVE/API2/Network/SDN.pm
index d216e4878b61..ccbf0777e3d4 100644
--- a/src/PVE/API2/Network/SDN.pm
+++ b/src/PVE/API2/Network/SDN.pm
@@ -17,6 +17,7 @@ use PVE::API2::Network::SDN::Vnets;
 use PVE::API2::Network::SDN::Zones;
 use PVE::API2::Network::SDN::Ipams;
 use PVE::API2::Network::SDN::Dns;
+use PVE::API2::Network::SDN::Fabrics;
 
 use base qw(PVE::RESTHandler);
 
@@ -45,6 +46,11 @@ __PACKAGE__->register_method ({
     path => 'dns',
 });
 
+__PACKAGE__->register_method ({
+    subclass => "PVE::API2::Network::SDN::Fabrics",
+    path => 'fabrics',
+});
+
 __PACKAGE__->register_method({
     name => 'index',
     path => '',
@@ -76,6 +82,7 @@ __PACKAGE__->register_method({
 	    { id => 'controllers' },
 	    { id => 'ipams' },
 	    { id => 'dns' },
+	    { id => 'fabrics' },
 	];
 
 	return $res;
diff --git a/src/PVE/API2/Network/SDN/Fabrics.pm b/src/PVE/API2/Network/SDN/Fabrics.pm
new file mode 100644
index 000000000000..8eb88efca102
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/Fabrics.pm
@@ -0,0 +1,57 @@
+package PVE::API2::Network::SDN::Fabrics;
+
+use strict;
+use warnings;
+
+use Storable qw(dclone);
+
+use PVE::RPCEnvironment;
+use PVE::Tools qw(extract_param);
+
+use PVE::API2::Network::SDN::Fabrics::OpenFabric;
+use PVE::API2::Network::SDN::Fabrics::Ospf;
+
+use PVE::Network::SDN::Fabrics;
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+
+__PACKAGE__->register_method ({
+    subclass => "PVE::API2::Network::SDN::Fabrics::OpenFabric",
+    path => 'openfabric',
+});
+__PACKAGE__->register_method ({
+    subclass => "PVE::API2::Network::SDN::Fabrics::Ospf",
+    path => 'ospf',
+});
+
+__PACKAGE__->register_method({
+    name => 'index',
+    path => '',
+    method => 'GET',
+    description => 'Index of SDN Fabrics',
+    permissions => {
+	description => "Only list entries where you have 'SDN.Audit' or 'SDN.Allocate' permissions on '/sdn/fabrics/<fabric>'",
+	user => 'all',
+    },
+    parameters => {
+    	additionalProperties => 0,
+    },
+    returns => {
+	type => 'array',
+	items => {
+	    oneOf => [
+
+	    ],
+	},
+    },
+    code => sub {
+	my ($param) = @_;
+
+	my $config = PVE::Network::SDN::Fabrics::get_all_configs();
+	return $config;
+    },
+});
+ 
+1;
diff --git a/src/PVE/API2/Network/SDN/Fabrics/Common.pm b/src/PVE/API2/Network/SDN/Fabrics/Common.pm
new file mode 100644
index 000000000000..f3042a18090d
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/Fabrics/Common.pm
@@ -0,0 +1,111 @@
+package PVE::API2::Network::SDN::Fabrics::Common;
+
+use strict;
+use warnings;
+
+use PVE::Network::SDN::Fabrics;
+
+sub delete_fabric {
+    my ($type, $param) = @_;
+
+    my $fabrics = PVE::Network::SDN::Fabrics::get_config($type);
+    $fabrics->delete_fabric($param);
+    PVE::Network::SDN::Fabrics::write_config($fabrics);
+    return $fabrics->get_inner();
+}
+
+sub delete_node {
+    my ($type, $param) = @_;
+
+    my $fabrics = PVE::Network::SDN::Fabrics::get_config($type);
+    $fabrics->delete_node($param);
+    PVE::Network::SDN::Fabrics::write_config($fabrics);
+    return $fabrics->get_inner();
+}
+
+sub delete_interface {
+    my ($type, $param) = @_;
+
+    my $fabrics = PVE::Network::SDN::Fabrics::get_config($type);
+    $fabrics->delete_interface($param);
+    PVE::Network::SDN::Fabrics::write_config($fabrics);
+    return $fabrics->get_inner();
+}
+
+sub add_node {
+    my ($type, $param) = @_;
+
+    my $fabrics = PVE::Network::SDN::Fabrics::get_config($type);
+    $fabrics->add_node($param);
+    PVE::Network::SDN::Fabrics::write_config($fabrics);
+
+    return $fabrics->get_inner();
+}
+
+sub add_fabric {
+    my ($type, $param) = @_;
+
+    my $fabrics = PVE::Network::SDN::Fabrics::get_config($type);
+    $fabrics->add_fabric($param);
+    PVE::Network::SDN::Fabrics::write_config($fabrics);
+
+    return $fabrics->get_inner();
+}
+
+sub get_fabric {
+    my ($type, $param) = @_;
+
+    my $fabrics = PVE::Network::SDN::Fabrics::get_config($type);
+    my $return_value = $fabrics->get_fabric($param->{fabric});
+    # Add the fabric id to the return value. The rust return value doesn't contain 
+    # the fabric name (as it's in the key of the section config hashmap, so we add it here).
+    $return_value->{fabric}->{name} = $param->{fabric};
+    return $return_value;
+}
+
+sub get_node {
+    my ($type, $param) = @_;
+
+    my $fabrics = PVE::Network::SDN::Fabrics::get_config($type);
+    my $return_value = $fabrics->get_node($param->{fabric}, $param->{node});
+    # Add the node id to the return value. The rust return value doesn't contain 
+    # the nodename (as it's in the key of the section config hashmap, so we add it here).
+    $return_value->{node}->{node} = $param->{node};
+    return $return_value;
+}
+
+sub get_interface {
+    my ($type, $param) = @_;
+
+    my $fabrics = PVE::Network::SDN::Fabrics::get_config($type);
+    return $fabrics->get_interface($param->{fabric}, $param->{node}, $param->{name});
+}
+
+sub edit_fabric {
+    my ($type, $param) = @_;
+
+    my $fabrics = PVE::Network::SDN::Fabrics::get_config($type);
+    $fabrics->edit_fabric($param);
+    PVE::Network::SDN::Fabrics::write_config($fabrics);
+    return $fabrics->get_inner();
+}
+
+sub edit_node {
+    my ($type, $param) = @_;
+
+    my $fabrics = PVE::Network::SDN::Fabrics::get_config($type);
+    $fabrics->edit_node($param);
+    PVE::Network::SDN::Fabrics::write_config($fabrics);
+    return $fabrics->get_inner();
+}
+
+sub edit_interface {
+    my ($type, $param) = @_;
+
+    my $fabrics = PVE::Network::SDN::Fabrics::get_config($type);
+    $fabrics->edit_interface($param);
+    PVE::Network::SDN::Fabrics::write_config($fabrics);
+    return $fabrics->get_inner();
+}
+
+1;
diff --git a/src/PVE/API2/Network/SDN/Fabrics/Makefile b/src/PVE/API2/Network/SDN/Fabrics/Makefile
new file mode 100644
index 000000000000..e433f2e7d0a6
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/Fabrics/Makefile
@@ -0,0 +1,9 @@
+SOURCES=OpenFabric.pm Ospf.pm Common.pm
+
+
+PERL5DIR=${DESTDIR}/usr/share/perl5
+
+.PHONY: install
+install:
+	for i in ${SOURCES}; do install -D -m 0644 $$i ${PERL5DIR}/PVE/API2/Network/SDN/Fabrics/$$i; done
+
diff --git a/src/PVE/API2/Network/SDN/Fabrics/OpenFabric.pm b/src/PVE/API2/Network/SDN/Fabrics/OpenFabric.pm
new file mode 100644
index 000000000000..626893aa61b7
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/Fabrics/OpenFabric.pm
@@ -0,0 +1,460 @@
+package PVE::API2::Network::SDN::Fabrics::OpenFabric;
+
+use strict;
+use warnings;
+
+use Storable qw(dclone);
+
+use PVE::RPCEnvironment;
+use PVE::Tools qw(extract_param);
+
+use PVE::Network::SDN::Fabrics;
+use PVE::API2::Network::SDN::Fabrics::Common;
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method({
+    name => 'delete_fabric',
+    path => '{fabric}',
+    method => 'DELETE',
+    description => 'Delete SDN Fabric',
+    protected => 1,
+    permissions => {
+	check => ['perm', '/sdn/fabrics/openfabric/{fabric}', [ 'SDN.Allocate' ]],
+    },
+    parameters => {
+    	additionalProperties => 1,
+	properties => {
+	    fabric => {
+		type => 'string',
+	    },
+	},
+    },
+    returns => {
+	type => 'object',
+	properties => {
+	    data => {
+		type => 'object',
+	    }
+	}
+    },
+    code => sub {
+	my ($param) = @_;
+
+	return PVE::API2::Network::SDN::Fabrics::Common::delete_fabric("openfabric", $param);
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'delete_node',
+    path => '{fabric}/node/{node}',
+    method => 'DELETE',
+    description => 'Delete SDN Fabric Node',
+    protected => 1,
+    permissions => {
+	check => ['perm', '/sdn/fabrics/openfabric/{fabric}/node/{node}', [ 'SDN.Allocate' ]],
+    },
+    parameters => {
+    	additionalProperties => 1,
+	properties => {
+	    fabric => {
+		type => 'string',
+	    },
+	    node => {
+		type => 'string',
+	    },
+	},
+    },
+    returns => {
+	type => 'object',
+	properties => {
+	    data => {
+		type => 'object',
+	    }
+	}
+    },
+    code => sub {
+	my ($param) = @_;
+
+	return PVE::API2::Network::SDN::Fabrics::Common::delete_node("openfabric", $param);
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'delete_interface',
+    path => '{fabric}/node/{node}/interface/{name}',
+    method => 'DELETE',
+    description => 'Delete SDN Fabric Node Interface',
+    protected => 1,
+    permissions => {
+	check => ['perm', '/sdn/fabrics/openfabric/{fabric}/node/{node}/interface/{name}', [ 'SDN.Allocate' ]],
+    },
+    parameters => {
+    	additionalProperties => 1,
+	properties => {
+	    fabric => {
+		type => 'string',
+	    },
+	    node => {
+		type => 'string',
+	    },
+	    name => {
+		type => 'string',
+	    },
+	},
+    },
+    returns => {
+	type => 'object',
+    },
+    code => sub {
+	my ($param) = @_;
+
+	return PVE::API2::Network::SDN::Fabrics::Common::delete_interface("openfabric", $param);
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'update_fabric',
+    path => '{fabric}',
+    method => 'PUT',
+    description => 'Update SDN Fabric configuration',
+    protected => 1,
+    permissions => {
+	check => ['perm', '/sdn/fabrics/openfabric/{fabric}', [ 'SDN.Allocate' ]],
+    },
+    parameters => {
+	additionalProperties => 1,
+	properties => {
+	    fabric => {
+		type => 'string'
+	    },
+	    hello_interval => {
+		type => 'integer',
+	    },
+	},
+    },
+    returns => {
+	type => 'object',
+	properties => {
+	    data => {
+		type => 'object',
+	    }
+	}
+    },
+    code => sub {
+	my ($param) = @_;
+
+	return PVE::API2::Network::SDN::Fabrics::Common::edit_fabric("openfabric", $param);
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'update_node',
+    path => '{fabric}/node/{node}',
+    method => 'PUT',
+    description => 'Update SDN Fabric Node configuration',
+    protected => 1,
+    permissions => {
+	check => ['perm', '/sdn/fabrics/openfabric/{fabric}/node/{node}', [ 'SDN.Allocate' ]],
+    },
+    parameters => {
+	additionalProperties => 1,
+	properties => {
+	    fabric => {
+		type => 'string',
+	    },
+	    node => {
+		type => 'string',
+	    },
+	    net => {
+		type => 'string',
+	    },
+	    interfaces => {
+		type => 'array',
+		items => {
+		    type => 'string',
+		},
+	    },
+	},
+    },
+    returns => {
+	type => 'object',
+	properties => {
+	    data => {
+		type => 'object',
+	    }
+	}
+    },
+    code => sub {
+	my ($param) = @_;
+
+	return PVE::API2::Network::SDN::Fabrics::Common::edit_node("openfabric", $param);
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'update_interface',
+    path => '{fabric}/node/{node}/interface/{name}',
+    method => 'PUT',
+    description => 'Update SDN Fabric Interface configuration',
+    protected => 1,
+    permissions => {
+	check => ['perm', '/sdn/fabrics/openfabric/{fabric}/node/{node}/interface/{name}', [ 'SDN.Allocate' ]],
+    },
+    parameters => {
+       additionalProperties => 1,
+	properties => {
+	    fabric => {
+		type => 'string',
+	    },
+	    node => {
+		type => 'string',
+	    },
+	    name => {
+		type => 'string',
+	    },
+	    passive => {
+		type => 'boolean',
+	    },
+	    hello_interval => {
+		optional => 1,
+		type => 'string',
+	    },
+	    hello_multiplier => {
+		optional => 1,
+		type => 'string',
+	    },
+	    csnp_interval => {
+		optional => 1,
+		type => 'string',
+	    },
+	},
+    },
+    returns => {
+	type => 'object',
+	properties => {
+	    data => {
+		type => 'object',
+	    }
+	}
+    },
+    code => sub {
+	my ($param) = @_;
+
+	return PVE::API2::Network::SDN::Fabrics::Common::edit_interface("openfabric", $param);
+    },
+});
+
+
+__PACKAGE__->register_method({
+    name => 'get_fabric',
+    path => '{fabric}',
+    method => 'GET',
+    description => 'Get SDN Fabric configuration',
+    permissions => {
+	check => ['perm', '/sdn/fabrics/openfabric/{fabric}', [ 'SDN.Allocate' ]],
+    },
+    parameters => {
+    	additionalProperties => 1,
+	properties => {}
+    },
+    returns => {
+	type => 'object',
+	properties => {
+	    data => {
+		type => 'object',
+		properties => {
+		    fabric => {
+			type => 'object',
+			properties => {
+			    name => {
+				type => 'string'
+			    }
+			}
+		    }
+		}
+	    }
+	}
+    },
+    code => sub {
+	my ($param) = @_;
+
+	return PVE::API2::Network::SDN::Fabrics::Common::get_fabric("openfabric", $param);
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'get_node',
+    path => '{fabric}/node/{node}',
+    method => 'GET',
+    description => 'Get SDN Fabric Node configuration',
+    permissions => {
+	check => ['perm', '/sdn/fabrics/openfabric/{fabric}', [ 'SDN.Allocate' ]],
+    },
+    parameters => {
+    	additionalProperties => 1,
+       properties => {}
+    },
+    returns => {
+	type => 'object',
+	properties => {
+	    data => {
+		type => 'object',
+		properties => {
+		    node => {
+			type => 'object',
+			properties => {
+			    net => {
+				type => 'string',
+			    },
+			    node => {
+				type => 'string',
+			    },
+			    interface => {
+				type => 'array',
+				items => {
+				    type => 'string'
+				}
+			    },
+			}
+		    }
+		}
+	    }
+	}
+    },
+    code => sub {
+	my ($param) = @_;
+
+	return PVE::API2::Network::SDN::Fabrics::Common::get_node("openfabric", $param);
+    },
+});
+
+
+__PACKAGE__->register_method({
+    name => 'get_interface',
+    path => '{fabric}/node/{node}/interface/{name}',
+    method => 'GET',
+    description => 'Get SDN Fabric Interface configuration',
+    permissions => {
+	check => ['perm', '/sdn/fabrics/openfabric/{fabric}', [ 'SDN.Allocate' ]],
+    },
+    parameters => {
+       additionalProperties => 1,
+    },
+    returns => {
+	type => 'object',
+	properties => {
+	    data => {
+		type => 'object',
+		properties => {
+		    name => {
+			type => 'string',
+		    },
+		    passive => {
+			type => 'boolean',
+		    },
+		    hello_interval => {
+			optional => 1,
+			type => 'number',
+		    },
+		    hello_multiplier => {
+			optional => 1,
+			type => 'number',
+		    },
+		    csnp_interval => {
+			optional => 1,
+			type => 'number',
+		    },
+		}
+	    }
+	}
+    },
+    code => sub {
+	my ($param) = @_;
+
+	return PVE::API2::Network::SDN::Fabrics::Common::get_interface("openfabric", $param);
+    },
+});
+
+
+__PACKAGE__->register_method({
+    name => 'add_fabric',
+    path => '/',
+    method => 'POST',
+    description => 'Create SDN Fabric configuration',
+    protected => 1,
+    permissions => {
+	check => ['perm', '/sdn/fabrics/openfabric', [ 'SDN.Allocate' ]],
+    },
+    parameters => {
+    	additionalProperties => 1,
+	properties => {
+	    "type" => {
+		type => 'string',
+	    },
+	    "name" => {
+		type => 'string',
+	    },
+	},
+    },
+    returns => {
+	type => 'object',
+	properties => {
+	    data => {
+		type => 'object',
+	    }
+	}
+    },
+    code => sub {
+	my ($param) = @_;
+
+	return PVE::API2::Network::SDN::Fabrics::Common::add_fabric("openfabric", $param);
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'add_node',
+    path => '{fabric}/node/{node}',
+    method => 'POST',
+    description => 'Create SDN Fabric Node configuration',
+    protected => 1,
+    permissions => {
+	check => ['perm', '/sdn/fabrics/openfabric/{fabric}/node/{node}', [ 'SDN.Allocate' ]],
+    },
+    parameters => {
+    	additionalProperties => 1,
+	properties => {
+	    fabric => {
+		type => 'string',
+	    },
+	    node => {
+		type => 'string',
+	    },
+	    net => {
+		type => 'string',
+	    },
+	    interfaces => {
+		type => 'array',
+		items => {
+		    type => 'string',
+		},
+	    },
+	},
+    },
+    returns => {
+	type => 'object',
+	properties => {
+	    data => {
+		type => 'object',
+	    }
+	}
+    },
+    code => sub {
+	my ($param) = @_;
+
+	return PVE::API2::Network::SDN::Fabrics::Common::add_node("openfabric", $param);
+    },
+});
+
+1;
diff --git a/src/PVE/API2/Network/SDN/Fabrics/Ospf.pm b/src/PVE/API2/Network/SDN/Fabrics/Ospf.pm
new file mode 100644
index 000000000000..309d29788667
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/Fabrics/Ospf.pm
@@ -0,0 +1,433 @@
+package PVE::API2::Network::SDN::Fabrics::Ospf;
+
+use strict;
+use warnings;
+
+use Storable qw(dclone);
+
+use PVE::RPCEnvironment;
+use PVE::Tools qw(extract_param);
+
+use PVE::Network::SDN::Fabrics;
+use PVE::API2::Network::SDN::Fabrics::Common;
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method({
+    name => 'delete_fabric',
+    path => '{fabric}',
+    method => 'DELETE',
+    description => 'Delete SDN Fabric',
+    protected => 1,
+    permissions => {
+	check => ['perm', '/sdn/fabrics/ospf/{fabric}', [ 'SDN.Allocate' ]],
+    },
+    parameters => {
+    	additionalProperties => 1,
+	properties => {
+	    fabric => {
+		type => 'string',
+	    },
+	},
+    },
+    returns => {
+	type => 'object',
+	properties => {
+	    data => {
+		type => 'object',
+	    }
+	}
+    },
+    code => sub {
+	my ($param) = @_;
+
+	return PVE::API2::Network::SDN::Fabrics::Common::delete_fabric("ospf", $param);
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'delete_node',
+    path => '{fabric}/node/{node}',
+    method => 'DELETE',
+    description => 'Delete SDN Fabric Node',
+    protected => 1,
+    permissions => {
+	check => ['perm', '/sdn/fabrics/ospf/{fabric}/node/{node}', [ 'SDN.Allocate' ]],
+    },
+    parameters => {
+    	additionalProperties => 1,
+	properties => {
+	    fabric => {
+		type => 'string',
+	    },
+	    node => {
+		type => 'string',
+	    },
+	},
+    },
+    returns => {
+	type => 'object',
+	properties => {
+	    data => {
+		type => 'object',
+	    }
+	}
+    },
+    code => sub {
+	my ($param) = @_;
+
+	return PVE::API2::Network::SDN::Fabrics::Common::delete_node("ospf", $param);
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'delete_interface',
+    path => '{fabric}/node/{node}/interface/{name}',
+    method => 'DELETE',
+    description => 'Delete SDN Fabric Node Interface',
+    protected => 1,
+    permissions => {
+	check => ['perm', '/sdn/fabrics/ospf/{fabric}/node/{node}/interface/{name}', [ 'SDN.Allocate' ]],
+    },
+    parameters => {
+    	additionalProperties => 1,
+	properties => {
+	    fabric => {
+		type => 'string',
+	    },
+	    node => {
+		type => 'string',
+	    },
+	    name => {
+		type => 'string',
+	    },
+	},
+    },
+    returns => {
+	type => 'null',
+    },
+    code => sub {
+	my ($param) = @_;
+
+	return PVE::API2::Network::SDN::Fabrics::Common::delete_interface("ospf", $param);
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'update_fabric',
+    path => '{fabric}',
+    method => 'PUT',
+    description => 'Update SDN Fabric configuration',
+    protected => 1,
+    permissions => {
+	check => ['perm', '/sdn/fabrics/ospf/{fabric}', [ 'SDN.Allocate' ]],
+    },
+    parameters => {
+	additionalProperties => 1,
+	properties => {
+	    fabric => {
+		type => 'string'
+	    },
+	},
+    },
+    returns => {
+	type => 'object',
+	properties => {
+	    data => {
+		type => 'object',
+	    }
+	}
+    },
+    code => sub {
+	my ($param) = @_;
+
+	return PVE::API2::Network::SDN::Fabrics::Common::edit_fabric("ospf", $param);
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'update_node',
+    path => '{fabric}/node/{node}',
+    method => 'PUT',
+    description => 'Update SDN Fabric Interface configuration',
+    protected => 1,
+    permissions => {
+	check => ['perm', '/sdn/fabrics/ospf/{fabric}', [ 'SDN.Allocate' ]],
+    },
+    parameters => {
+	additionalProperties => 1,
+	properties => {
+	    fabric => {
+		type => 'string',
+	    },
+	    node => {
+		type => 'string',
+	    },
+	    router_id => {
+		type => 'string',
+	    },
+	    interfaces => {
+		type => 'array',
+		items => {
+		    type => 'string',
+		},
+	    },
+	},
+    },
+    returns => {
+	type => 'object',
+	properties => {
+	    data => {
+		type => 'object',
+	    }
+	}
+    },
+    code => sub {
+	my ($param) = @_;
+
+	return PVE::API2::Network::SDN::Fabrics::Common::edit_node("ospf", $param);
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'update_interface',
+    path => '{fabric}/node/{node}/interface/{name}',
+    method => 'PUT',
+    description => 'Update SDN Fabric Interface configuration',
+    protected => 1,
+    permissions => {
+	check => ['perm', '/sdn/fabrics/ospf/{fabric}/node/{node}/interface/{name}', [ 'SDN.Allocate' ]],
+    },
+    parameters => {
+       additionalProperties => 1,
+	properties => {
+	    fabric => {
+		type => 'string',
+	    },
+	    node => {
+		type => 'string',
+	    },
+	    name => {
+		type => 'string',
+	    },
+	    passive => {
+		type => 'boolean',
+	    },
+	},
+    },
+    returns => {
+	type => 'object',
+	properties => {
+	    data => {
+		type => 'object',
+	    }
+	}
+    },
+    code => sub {
+	my ($param) = @_;
+
+	return PVE::API2::Network::SDN::Fabrics::Common::edit_interface("ospf", $param);
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'get_fabric',
+    path => '{fabric}',
+    method => 'GET',
+    description => 'Get SDN Fabric configuration',
+    permissions => {
+	check => ['perm', '/sdn/fabrics/ospf/{fabric}', [ 'SDN.Allocate' ]],
+    },
+    parameters => {
+	additionalProperties => 1,
+	properties => {}
+    },
+    returns => {
+	type => 'object',
+	properties => {
+	    data => {
+		type => 'object',
+		properties => {
+		    fabric => {
+			type => 'object',
+			properties => {
+			    name => {
+				type => 'string'
+			    }
+			}
+		    }
+		}
+	    }
+	}
+    },
+    code => sub {
+	my ($param) = @_;
+
+	return PVE::API2::Network::SDN::Fabrics::Common::get_fabric("ospf", $param);
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'get_node',
+    path => '{fabric}/node/{node}',
+    method => 'GET',
+    description => 'Get SDN Fabric Node configuration',
+    permissions => {
+	check => ['perm', '/sdn/fabrics/ospf/{fabric}', [ 'SDN.Allocate' ]],
+    },
+    parameters => {
+    	additionalProperties => 1,
+       properties => {}
+    },
+    returns => {
+	type => 'object',
+	properties => {
+	    data => {
+		type => 'object',
+		properties => {
+		    node => {
+			type => 'object',
+			properties => {
+			    router_id => {
+				type => 'string',
+			    },
+			    node => {
+				type => 'string',
+			    },
+			    interface => {
+				type => 'array',
+				items => {
+				    type => 'string'
+				}
+			    },
+			}
+		    }
+		}
+	    }
+	}
+    },
+    code => sub {
+	my ($param) = @_;
+
+	return PVE::API2::Network::SDN::Fabrics::Common::get_node("ospf", $param);
+    },
+});
+
+
+__PACKAGE__->register_method({
+    name => 'get_interface',
+    path => '{fabric}/node/{node}/interface/{name}',
+    method => 'GET',
+    description => 'Get SDN Fabric Interface configuration',
+    permissions => {
+	check => ['perm', '/sdn/fabrics/ospf/{fabric}', [ 'SDN.Allocate' ]],
+    },
+    parameters => {
+       additionalProperties => 1,
+    },
+    returns => {
+	type => 'object',
+	properties => {
+	    data => {
+		type => 'object',
+		properties => {
+		    name => {
+			type => 'string',
+		    },
+		    passive => {
+			type => 'boolean',
+		    },
+		}
+	    }
+	}
+    },
+    code => sub {
+	my ($param) = @_;
+
+	return PVE::API2::Network::SDN::Fabrics::Common::get_interface("ospf", $param);
+    },
+});
+
+
+__PACKAGE__->register_method({
+    name => 'add_fabric',
+    path => '/',
+    method => 'POST',
+    description => 'Create SDN Fabric configuration',
+    protected => 1,
+    permissions => {
+	check => ['perm', '/sdn/fabrics/ospf', [ 'SDN.Allocate' ]],
+    },
+    parameters => {
+    	additionalProperties => 1,
+	properties => {
+	    "type" => {
+		type => 'string',
+	    },
+	    "name" => {
+		type => 'string',
+	    },
+	},
+    },
+    returns => {
+	type => 'object',
+	properties => {
+	    data => {
+		type => 'object',
+	    }
+	}
+    },
+    code => sub {
+	my ($param) = @_;
+
+	return PVE::API2::Network::SDN::Fabrics::Common::add_fabric("ospf", $param);
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'add_node',
+    path => '{fabric}/node/{node}',
+    method => 'POST',
+    description => 'Create SDN Fabric Node configuration',
+    protected => 1,
+    permissions => {
+	check => ['perm', '/sdn/fabrics/ospf/{fabric}/node/{node}', [ 'SDN.Allocate' ]],
+    },
+    parameters => {
+    	additionalProperties => 1,
+	properties => {
+	    fabric => {
+		type => 'string',
+	    },
+	    node => {
+		type => 'string',
+	    },
+	    router_id => {
+		type => 'string',
+	    },
+	    interfaces => {
+		type => 'array',
+		items => {
+		    type => 'string',
+		},
+	    },
+	},
+    },
+    returns => {
+	type => 'object',
+	properties => {
+	    data => {
+		type => 'object',
+	    }
+	}
+    },
+    code => sub {
+	my ($param) = @_;
+
+	return PVE::API2::Network::SDN::Fabrics::Common::add_node("ospf", $param);
+    },
+});
+
+
+1;
diff --git a/src/PVE/API2/Network/SDN/Makefile b/src/PVE/API2/Network/SDN/Makefile
index abd1bfae020e..08bec7535530 100644
--- a/src/PVE/API2/Network/SDN/Makefile
+++ b/src/PVE/API2/Network/SDN/Makefile
@@ -1,4 +1,4 @@
-SOURCES=Vnets.pm Zones.pm Controllers.pm Subnets.pm Ipams.pm Dns.pm Ips.pm
+SOURCES=Vnets.pm Zones.pm Controllers.pm Subnets.pm Ipams.pm Dns.pm Ips.pm Fabrics.pm
 
 
 PERL5DIR=${DESTDIR}/usr/share/perl5
@@ -7,4 +7,5 @@ PERL5DIR=${DESTDIR}/usr/share/perl5
 install:
 	for i in ${SOURCES}; do install -D -m 0644 $$i ${PERL5DIR}/PVE/API2/Network/SDN/$$i; done
 	make -C Zones install
+	make -C Fabrics install
 
-- 
2.39.5



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


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

* [pve-devel] [PATCH pve-manager 09/11] sdn: add Fabrics view
  2025-02-14 13:39 [pve-devel] [RFC cluster/manager/network/proxmox{-ve-rs, -perl-rs} 00/11] Add SDN Fabrics Gabriel Goller
                   ` (7 preceding siblings ...)
  2025-02-14 13:39 ` [pve-devel] [PATCH pve-network 08/11] add api endpoints for fabrics Gabriel Goller
@ 2025-02-14 13:39 ` Gabriel Goller
  2025-03-04  9:57   ` Stefan Hanreich
  2025-02-14 13:39 ` [pve-devel] [PATCH pve-manager 10/11] sdn: add fabric edit/delete forms Gabriel Goller
                   ` (2 subsequent siblings)
  11 siblings, 1 reply; 32+ messages in thread
From: Gabriel Goller @ 2025-02-14 13:39 UTC (permalink / raw)
  To: pve-devel

Add the FabricsView in the sdn category of the datacenter view. The
FabricsView allows to show all the fabrics on all the nodes of the
cluster.

Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 PVE/API2/Cluster.pm             |   7 +-
 PVE/API2/Network.pm             |   7 +-
 www/manager6/.lint-incremental  |   0
 www/manager6/Makefile           |   8 +
 www/manager6/dc/Config.js       |   8 +
 www/manager6/sdn/FabricsView.js | 359 ++++++++++++++++++++++++++++++++
 6 files changed, 379 insertions(+), 10 deletions(-)
 create mode 100644 www/manager6/.lint-incremental
 create mode 100644 www/manager6/sdn/FabricsView.js

diff --git a/PVE/API2/Cluster.pm b/PVE/API2/Cluster.pm
index a0e5c11b6e8e..7730aab82a25 100644
--- a/PVE/API2/Cluster.pm
+++ b/PVE/API2/Cluster.pm
@@ -35,11 +35,8 @@ use PVE::API2::Firewall::Cluster;
 use PVE::API2::HAConfig;
 use PVE::API2::ReplicationConfig;
 
-my $have_sdn;
-eval {
-    require PVE::API2::Network::SDN;
-    $have_sdn = 1;
-};
+my $have_sdn = 1;
+require PVE::API2::Network::SDN;
 
 use base qw(PVE::RESTHandler);
 
diff --git a/PVE/API2/Network.pm b/PVE/API2/Network.pm
index cfccdd9e3da3..3c45fe2fb7bf 100644
--- a/PVE/API2/Network.pm
+++ b/PVE/API2/Network.pm
@@ -16,11 +16,8 @@ use IO::File;
 
 use base qw(PVE::RESTHandler);
 
-my $have_sdn;
-eval {
-    require PVE::Network::SDN;
-    $have_sdn = 1;
-};
+my $have_sdn = 1;
+require PVE::Network::SDN;
 
 my $iflockfn = "/etc/network/.pve-interfaces.lock";
 
diff --git a/www/manager6/.lint-incremental b/www/manager6/.lint-incremental
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index c94a5cdfbf70..224b6079e833 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -303,6 +303,14 @@ JSSRC= 							\
 	sdn/zones/SimpleEdit.js				\
 	sdn/zones/VlanEdit.js				\
 	sdn/zones/VxlanEdit.js				\
+	sdn/FabricsView.js				\
+	sdn/fabrics/Common.js				\
+	sdn/fabrics/openfabric/FabricEdit.js		\
+	sdn/fabrics/openfabric/NodeEdit.js		\
+	sdn/fabrics/openfabric/InterfaceEdit.js		\
+	sdn/fabrics/ospf/FabricEdit.js			\
+	sdn/fabrics/ospf/NodeEdit.js			\
+	sdn/fabrics/ospf/InterfaceEdit.js		\
 	storage/ContentView.js				\
 	storage/BackupView.js				\
 	storage/Base.js					\
diff --git a/www/manager6/dc/Config.js b/www/manager6/dc/Config.js
index 74728c8320e9..68f7be8d6042 100644
--- a/www/manager6/dc/Config.js
+++ b/www/manager6/dc/Config.js
@@ -229,6 +229,14 @@ Ext.define('PVE.dc.Config', {
 		    hidden: true,
 		    iconCls: 'fa fa-shield',
 		    itemId: 'sdnfirewall',
+		},
+		{
+		    xtype: 'pveSDNFabricView',
+		    groups: ['sdn'],
+		    title: gettext('Fabrics'),
+		    hidden: true,
+		    iconCls: 'fa fa-road',
+		    itemId: 'sdnfabrics',
 		});
 	    }
 
diff --git a/www/manager6/sdn/FabricsView.js b/www/manager6/sdn/FabricsView.js
new file mode 100644
index 000000000000..f090ee894b75
--- /dev/null
+++ b/www/manager6/sdn/FabricsView.js
@@ -0,0 +1,359 @@
+const FABRIC_PANELS = {
+    'openfabric': 'PVE.sdn.Fabric.OpenFabric.Fabric.Edit',
+    'ospf': 'PVE.sdn.Fabric.Ospf.Fabric.Edit',
+};
+
+const NODE_PANELS = {
+    'openfabric': 'PVE.sdn.Fabric.OpenFabric.Node.Edit',
+    'ospf': 'PVE.sdn.Fabric.Ospf.Node.Edit',
+};
+
+const INTERFACE_PANELS = {
+    'openfabric': 'PVE.sdn.Fabric.OpenFabric.Interface.Edit',
+    'ospf': 'PVE.sdn.Fabric.Ospf.Interface.Edit',
+};
+
+Ext.define('PVE.sdn.Fabric.View', {
+    extend: 'Ext.tree.Panel',
+
+    xtype: 'pveSDNFabricView',
+
+    columns: [
+	{
+	    xtype: 'treecolumn',
+	    text: gettext('Name'),
+	    dataIndex: 'name',
+	    width: 200,
+	},
+	{
+	    text: gettext('Identifier'),
+	    dataIndex: 'identifier',
+	    width: 200,
+	},
+	{
+	    text: gettext('Action'),
+	    xtype: 'actioncolumn',
+	    dataIndex: 'text',
+	    width: 150,
+	    items: [
+		{
+		    handler: 'addAction',
+		    getTip: (_v, _m, _rec) => gettext('Add'),
+		    getClass: (_v, _m, { data }) => {
+			if (data.type === 'fabric') {
+			    return 'fa fa-plus-square';
+			}
+
+			return 'pmx-hidden';
+		    },
+		    isActionDisabled: (_v, _r, _c, _i, { data }) => data.type !== 'fabric',
+		},
+		{
+		    tooltip: gettext('Edit'),
+		    handler: 'editAction',
+		    getClass: (_v, _m, { data }) => {
+			// the fabric type (openfabric, ospf, etc.) cannot be edited
+			if (data.type) {
+			    return 'fa fa-pencil fa-fw';
+			}
+
+			return 'pmx-hidden';
+		    },
+		    isActionDisabled: (_v, _r, _c, _i, { data }) => !data.type,
+		},
+		{
+		    tooltip: gettext('Delete'),
+		    handler: 'deleteAction',
+		    getClass: (_v, _m, { data }) => {
+			// the fabric type (openfabric, ospf, etc.) cannot be deleted
+			if (data.type) {
+			    return 'fa critical fa-trash-o';
+			}
+
+			return 'pmx-hidden';
+		    },
+		    isActionDisabled: (_v, _r, _c, _i, { data }) => !data.type,
+		},
+	    ],
+	},
+    ],
+
+    store: {
+	sorters: ['name'],
+    },
+
+    layout: 'fit',
+    rootVisible: false,
+    animate: false,
+
+    tbar: [
+	{
+	    text: gettext('Add Fabric'),
+	    menu: [
+		{
+		    text: gettext('OpenFabric'),
+		    handler: 'openAddOpenFabricWindow',
+		},
+		{
+		    text: gettext('OSPF'),
+		    handler: 'openAddOspfWindow',
+		},
+	    ],
+	},
+	{
+	    xtype: 'proxmoxButton',
+	    text: gettext('Reload'),
+	    handler: 'reload',
+	},
+    ],
+
+    controller: {
+	xclass: 'Ext.app.ViewController',
+
+	reload: function() {
+	    let me = this;
+
+	    Proxmox.Utils.API2Request({
+		url: `/cluster/sdn/fabrics/`,
+		method: 'GET',
+		success: function(response, opts) {
+		    let ospf = Object.entries(response.result.data.ospf);
+		    let openfabric = Object.entries(response.result.data.openfabric);
+
+		    // add some metadata so we can merge the objects later and still know the protocol/type
+		    ospf = ospf.map(x => {
+			if (x["1"].fabric) {
+			    return Object.assign(x["1"].fabric, { _protocol: "ospf", _type: "fabric", name: x["0"] });
+			} else if (x["1"].node) {
+			    let id = x["0"].split("_");
+			    return Object.assign(x["1"].node,
+				{
+				    _protocol: "ospf",
+				    _type: "node",
+				    node: id[1],
+				    fabric: id[0],
+				},
+			    );
+			} else {
+			    return x;
+			}
+		    });
+		    openfabric = openfabric.map(x => {
+			if (x["1"].fabric) {
+			    return Object.assign(x["1"].fabric, { _protocol: "openfabric", _type: "fabric", name: x["0"] });
+			} else if (x["1"].node) {
+			    let id = x["0"].split("_");
+			    return Object.assign(x["1"].node,
+				{
+				    _protocol: "openfabric",
+				    _type: "node",
+				    node: id[1],
+				    fabric: id[0],
+				},
+			    );
+			} else {
+			    return x;
+			}
+		    });
+
+		    let data = {};
+		    data.ospf = ospf;
+		    data.openfabric = openfabric;
+
+		    let fabrics = Object.entries(data).map((protocol) => {
+			let protocol_entry = {};
+			protocol_entry.children = protocol["1"].filter(e => e._type === "fabric").map(fabric => {
+			    fabric.children = protocol["1"].filter(e => e._type === "node")
+				.filter((node) =>
+				    node.fabric === fabric.name && node._protocol === fabric._protocol)
+					.map((node) => {
+					    node.children = node.interface
+						.map((nic) => {
+							let parsed = PVE.Parser.parsePropertyString(nic);
+							parsed.leaf = true;
+							parsed.type = 'interface';
+							// Add meta information that we need to edit and remove
+							parsed._protocol = node._protocol;
+							parsed._fabric = fabric.name;
+							parsed._node = node.node;
+							parsed.iconCls = 'x-tree-icon-none';
+							return parsed;
+						});
+
+						node.expanded = true;
+						node.type = 'node';
+						node.name = node.node;
+						node._fabric = fabric.name;
+						node.identifier = node.net || node.router_id;
+						node.iconCls = 'fa fa-desktop x-fa-treepanel';
+
+						return node;
+					});
+
+					fabric.type = 'fabric';
+					fabric.expanded = true;
+					fabric.iconCls = 'fa fa-road x-fa-treepanel';
+
+					return fabric;
+				});
+				protocol_entry.name = protocol["0"];
+				protocol_entry.expanded = true;
+				return protocol_entry;
+			});
+
+			me.getView().setRootNode({
+			    name: '__root',
+			    expanded: true,
+			    children: fabrics,
+			});
+		},
+	    });
+	},
+
+	getFabricEditPanel: function(type) {
+	    return FABRIC_PANELS[type];
+	},
+
+	getNodeEditPanel: function(type) {
+	    return NODE_PANELS[type];
+	},
+
+	getInterfaceEditPanel: function(type) {
+	    return INTERFACE_PANELS[type];
+	},
+
+	addAction: function(_grid, _rI, _cI, _item, _e, rec) {
+	    let me = this;
+
+	    let component = me.getNodeEditPanel(rec.data._protocol);
+
+	    if (!component) {
+		console.warn(`unknown protocol ${rec.data._protocol}`);
+		return;
+	    }
+
+	    let extraRequestParams = {
+		type: rec.data.type,
+		protocol: rec.data._protocol,
+		fabric: rec.data.name,
+	    };
+
+	    let window = Ext.create(component, {
+		autoShow: true,
+		isCreate: true,
+		autoLoad: false,
+		extraRequestParams,
+	    });
+
+	    window.on('destroy', () => me.reload());
+	},
+
+	editAction: function(_grid, _rI, _cI, _item, _e, rec) {
+	    let me = this;
+
+	    let component = '';
+	    let url = '';
+	    let autoLoad = true;
+
+	    if (rec.data.type === 'fabric') {
+		component = me.getFabricEditPanel(rec.data._protocol);
+		url = `/cluster/sdn/fabrics/${rec.data._protocol}/${rec.data.name}`;
+	    } else if (rec.data.type === 'node') {
+		component = me.getNodeEditPanel(rec.data._protocol);
+		// no url, every request is done manually
+		url = `/cluster/sdn/fabrics/${rec.data._protocol}/${rec.data._fabric}/node/${rec.data.node}`;
+		autoLoad = false;
+	    } else if (rec.data.type === 'interface') {
+		component = me.getInterfaceEditPanel(rec.data._protocol);
+		url = `/cluster/sdn/fabrics/${rec.data._protocol}/${rec.data._fabric}/node\
+		/${rec.data._node}/interface/${rec.data.name}`;
+	    }
+
+	    if (!component) {
+		console.warn(`unknown protocol ${rec.data._protocol} or unknown type ${rec.data.type}`);
+		return;
+	    }
+
+	    let window = Ext.create(component, {
+		autoShow: true,
+		autoLoad: autoLoad,
+		isCreate: false,
+		submitUrl: url,
+		loadUrl: url,
+		fabric: rec.data._fabric,
+		node: rec.data.node,
+	    });
+
+	    window.on('destroy', () => me.reload());
+	},
+
+	deleteAction: function(table, rI, cI, item, e, { data }) {
+	    let me = this;
+	    let view = me.getView();
+
+	    Ext.Msg.show({
+		title: gettext('Confirm'),
+		icon: Ext.Msg.WARNING,
+		message: Ext.String.format(gettext('Are you sure you want to remove the fabric {0}?'), `${data.name}`),
+		buttons: Ext.Msg.YESNO,
+		defaultFocus: 'no',
+		callback: function(btn) {
+		    if (btn !== 'yes') {
+			return;
+		    }
+
+		    let url;
+		    if (data.type === "node") {
+			url = `/cluster/sdn/fabrics/${data._protocol}/${data._fabric}/node/${data.name}`;
+		    } else if (data.type === "fabric") {
+			url = `/cluster/sdn/fabrics/${data._protocol}/${data.name}`;
+		    } else if (data.type === "interface") {
+			url = `/cluster/sdn/fabrics/${data._protocol}/${data._fabric}/node/\
+			${data._node}/interface/${data.name}`;
+		    } else {
+			console.warn("deleteAction: missing type");
+		    }
+
+		    Proxmox.Utils.API2Request({
+			url,
+			method: 'DELETE',
+			waitMsgTarget: view,
+			failure: function(response, opts) {
+			    Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+			},
+			callback: me.reload.bind(me),
+		    });
+		},
+	    });
+	},
+
+	openAddOpenFabricWindow: function() {
+	    let me = this;
+
+	    let window = Ext.create('PVE.sdn.Fabric.OpenFabric.Fabric.Edit', {
+		autoShow: true,
+		autoLoad: false,
+		isCreate: true,
+	    });
+
+	    window.on('destroy', () => me.reload());
+	},
+
+	openAddOspfWindow: function() {
+	    let me = this;
+
+	    let window = Ext.create('PVE.sdn.Fabric.Ospf.Fabric.Edit', {
+		autoShow: true,
+		autoLoad: false,
+		isCreate: true,
+	    });
+
+	    window.on('destroy', () => me.reload());
+	},
+
+	init: function(view) {
+	    let me = this;
+	    me.reload();
+	},
+    },
+});
-- 
2.39.5



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


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

* [pve-devel] [PATCH pve-manager 10/11] sdn: add fabric edit/delete forms
  2025-02-14 13:39 [pve-devel] [RFC cluster/manager/network/proxmox{-ve-rs, -perl-rs} 00/11] Add SDN Fabrics Gabriel Goller
                   ` (8 preceding siblings ...)
  2025-02-14 13:39 ` [pve-devel] [PATCH pve-manager 09/11] sdn: add Fabrics view Gabriel Goller
@ 2025-02-14 13:39 ` Gabriel Goller
  2025-03-04 10:07   ` Stefan Hanreich
  2025-02-14 13:39 ` [pve-devel] [PATCH pve-manager 11/11] network: return loopback interface on network endpoint Gabriel Goller
  2025-03-03 16:58 ` [pve-devel] [RFC cluster/manager/network/proxmox{-ve-rs, -perl-rs} 00/11] Add SDN Fabrics Stefan Hanreich
  11 siblings, 1 reply; 32+ messages in thread
From: Gabriel Goller @ 2025-02-14 13:39 UTC (permalink / raw)
  To: pve-devel

Add the add/edit/delete modals for the FabricsView. This allows us to
create, edit, and delete fabrics, nodes, and interfaces.

Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 www/manager6/sdn/fabrics/Common.js            | 222 ++++++++++++++++++
 .../sdn/fabrics/openfabric/FabricEdit.js      |  67 ++++++
 .../sdn/fabrics/openfabric/InterfaceEdit.js   |  92 ++++++++
 .../sdn/fabrics/openfabric/NodeEdit.js        | 187 +++++++++++++++
 www/manager6/sdn/fabrics/ospf/FabricEdit.js   |  60 +++++
 .../sdn/fabrics/ospf/InterfaceEdit.js         |  46 ++++
 www/manager6/sdn/fabrics/ospf/NodeEdit.js     | 191 +++++++++++++++
 7 files changed, 865 insertions(+)
 create mode 100644 www/manager6/sdn/fabrics/Common.js
 create mode 100644 www/manager6/sdn/fabrics/openfabric/FabricEdit.js
 create mode 100644 www/manager6/sdn/fabrics/openfabric/InterfaceEdit.js
 create mode 100644 www/manager6/sdn/fabrics/openfabric/NodeEdit.js
 create mode 100644 www/manager6/sdn/fabrics/ospf/FabricEdit.js
 create mode 100644 www/manager6/sdn/fabrics/ospf/InterfaceEdit.js
 create mode 100644 www/manager6/sdn/fabrics/ospf/NodeEdit.js

diff --git a/www/manager6/sdn/fabrics/Common.js b/www/manager6/sdn/fabrics/Common.js
new file mode 100644
index 000000000000..72ec093fc928
--- /dev/null
+++ b/www/manager6/sdn/fabrics/Common.js
@@ -0,0 +1,222 @@
+Ext.define('PVE.sdn.Fabric.InterfacePanel', {
+    extend: 'Ext.grid.Panel',
+    mixins: ['Ext.form.field.Field'],
+
+    network_interfaces: undefined,
+
+    selectionChange: function(_grid, _selection) {
+	let me = this;
+	me.value = me.getSelection().map((rec) => {
+	    delete rec.data.cidr;
+	    delete rec.data.cidr6;
+	    delete rec.data.selected;
+	    return PVE.Parser.printPropertyString(rec.data);
+	});
+	me.checkChange();
+    },
+
+    getValue: function() {
+	let me = this;
+	return me.value ?? [];
+    },
+
+    setValue: function(value) {
+	let me = this;
+
+	value ??= [];
+
+	me.updateSelectedInterfaces(value);
+
+	return me.mixins.field.setValue.call(me, value);
+    },
+
+    addInterfaces: function(fabric_interfaces) {
+	let me = this;
+	if (me.network_interfaces) {
+	    let node_interfaces = me.network_interfaces
+	    //.filter((elem) => elem.type === 'eth')
+		.map((elem) => {
+		    const obj = {
+			name: elem.iface,
+			cidr: elem.cidr,
+			cidr6: elem.cidr6,
+		    };
+		    return obj;
+		});
+
+	    if (fabric_interfaces) {
+		node_interfaces = node_interfaces.map(i => {
+		    let elem = fabric_interfaces.find(j => j.name === i.name);
+		    return Object.assign(i, elem);
+		});
+		let store = me.getStore();
+		store.setData(node_interfaces);
+	    } else {
+		let store = me.getStore();
+		store.setData(node_interfaces);
+	    }
+	} else if (fabric_interfaces) {
+	    // We could not get the available interfaces of the node, so we display the configured ones only.
+		let interfaces = fabric_interfaces.map((elem) => {
+		    const obj = {
+			name: elem.name,
+			cidr: 'unknown',
+			cidr6: 'unknown',
+			...elem,
+		    };
+		    return obj;
+		});
+
+	    let store = me.getStore();
+	    store.setData(interfaces);
+	} else {
+	    console.warn("no fabric_interfaces and cluster_interfaces available!");
+	}
+    },
+
+    updateSelectedInterfaces: function(values) {
+	let me = this;
+	if (values) {
+	    let recs = [];
+	    let store = me.getStore();
+
+	    for (const i of values) {
+		let rec = store.getById(i.name);
+		if (rec) {
+		    recs.push(rec);
+		}
+	    }
+	    me.suspendEvent('change');
+	    me.setSelection();
+	    me.setSelection(recs);
+	    me.resumeEvent('change');
+	} else {
+	    me.suspendEvent('change');
+	    me.setSelection();
+	    me.resumeEvent('change');
+	}
+    },
+
+    setNetworkInterfaces: function(network_interfaces) {
+	this.network_interfaces = network_interfaces;
+    },
+
+    getSubmitData: function() {
+	let records = this.getSelection().map((record) => {
+	    // we don't need the cidr, cidr6, and selected parameters
+	    delete record.data.cidr;
+	    delete record.data.cidr6;
+	    delete record.data.selected;
+	    return Proxmox.Utils.printPropertyString(record.data);
+	});
+	return {
+	    'interfaces': records,
+	};
+    },
+
+    controller: {
+	onValueChange: function(field, value) {
+	    let me = this;
+	    let record = field.getWidgetRecord();
+	    let column = field.getWidgetColumn();
+	    if (record) {
+		record.set(column.dataIndex, value);
+		record.commit();
+
+		me.getView().checkChange();
+		me.getView().selectionChange();
+	    }
+	},
+
+	control: {
+	    'field': {
+		change: 'onValueChange',
+	    },
+	},
+    },
+
+    selModel: {
+	type: 'checkboxmodel',
+	mode: 'SIMPLE',
+    },
+
+    listeners: {
+	selectionchange: function() {
+	    this.selectionChange(...arguments);
+	},
+    },
+
+    commonColumns: [
+	{
+	    text: gettext('Name'),
+	    dataIndex: 'name',
+	    flex: 2,
+	},
+	{
+	    text: gettext('IPv4'),
+	    dataIndex: 'cidr',
+	    flex: 2,
+	},
+	{
+	    text: gettext('IPv6'),
+	    dataIndex: 'cidr6',
+	    flex: 2,
+	},
+    ],
+
+    additionalColumns: [],
+
+    initComponent: function() {
+	let me = this;
+
+	Ext.apply(me, {
+	    store: Ext.create("Ext.data.Store", {
+		model: "Pve.sdn.Interface",
+		sorters: {
+		    property: 'name',
+		    direction: 'ASC',
+		},
+	    }),
+	    columns: me.commonColumns.concat(me.additionalColumns),
+	});
+
+	me.callParent();
+
+	Proxmox.Utils.monStoreErrors(me, me.getStore(), true);
+	me.initField();
+    },
+});
+
+
+Ext.define('Pve.sdn.Fabric', {
+    extend: 'Ext.data.Model',
+    idProperty: 'name',
+    fields: [
+	'name',
+	'type',
+    ],
+});
+
+Ext.define('Pve.sdn.Node', {
+    extend: 'Ext.data.Model',
+    idProperty: 'name',
+    fields: [
+	'name',
+	'fabric',
+	'type',
+    ],
+});
+
+Ext.define('Pve.sdn.Interface', {
+    extend: 'Ext.data.Model',
+    idProperty: 'name',
+    fields: [
+	'name',
+	'cidr',
+	'cidr6',
+	'passive',
+	'hello_interval',
+	'hello_multiplier',
+	'csnp_interval',
+    ],
+});
diff --git a/www/manager6/sdn/fabrics/openfabric/FabricEdit.js b/www/manager6/sdn/fabrics/openfabric/FabricEdit.js
new file mode 100644
index 000000000000..0431a00e7302
--- /dev/null
+++ b/www/manager6/sdn/fabrics/openfabric/FabricEdit.js
@@ -0,0 +1,67 @@
+Ext.define('PVE.sdn.Fabric.OpenFabric.Fabric.Edit', {
+    extend: 'Proxmox.window.Edit',
+    xtype: 'pveSDNOpenFabricRouteEdit',
+
+    subject: gettext('Add OpenFabric'),
+
+    url: '/cluster/sdn/fabrics/openfabric',
+    type: 'openfabric',
+
+    isCreate: undefined,
+
+    viewModel: {
+	data: {
+	    isCreate: true,
+	},
+    },
+
+    items: [
+	{
+	    xtype: 'textfield',
+	    name: 'type',
+	    value: 'openfabric',
+	    allowBlank: false,
+	    hidden: true,
+	},
+	{
+	    xtype: 'textfield',
+	    fieldLabel: gettext('Name'),
+	    labelWidth: 120,
+	    name: 'name',
+	    allowBlank: false,
+	    bind: {
+		disabled: '{!isCreate}',
+	    },
+	},
+	{
+	    xtype: 'numberfield',
+	    fieldLabel: gettext('Hello Interval'),
+	    labelWidth: 120,
+	    name: 'hello_interval',
+	    allowBlank: true,
+	},
+    ],
+
+    submitUrl: function(url, values) {
+	let me = this;
+	return `${me.url}`;
+    },
+
+    initComponent: function() {
+	let me = this;
+
+	let view = me.getViewModel();
+	view.set('isCreate', me.isCreate);
+
+	me.method = me.isCreate ? 'POST' : 'PUT';
+	me.callParent();
+
+	if (!me.isCreate) {
+	    me.load({
+		success: function(response, opts) {
+		    me.setValues(response.result.data.fabric);
+		},
+	    });
+	}
+    },
+});
diff --git a/www/manager6/sdn/fabrics/openfabric/InterfaceEdit.js b/www/manager6/sdn/fabrics/openfabric/InterfaceEdit.js
new file mode 100644
index 000000000000..ef33c16b784f
--- /dev/null
+++ b/www/manager6/sdn/fabrics/openfabric/InterfaceEdit.js
@@ -0,0 +1,92 @@
+Ext.define('PVE.sdn.Fabric.OpenFabric.Interface.Edit', {
+    extend: 'Proxmox.window.Edit',
+    xtype: 'pveSDNOpenFabricInterfaceEdit',
+
+    initComponent: function() {
+	let me = this;
+
+	Ext.apply(me, {
+	    items: [{
+		xtype: 'inputpanel',
+		items: [
+		    {
+			xtype: 'textfield',
+			fieldLabel: gettext('Interface'),
+			name: 'name',
+			disabled: true,
+		    },
+		    {
+			xtype: 'proxmoxcheckbox',
+			fieldLabel: gettext('Passive'),
+			name: 'passive',
+			uncheckedValue: 0,
+		    },
+		    {
+			xtype: 'numberfield',
+			fieldLabel: gettext('Hello Interval'),
+			name: 'hello_interval',
+			allowBlank: true,
+		    },
+		    {
+			xtype: 'numberfield',
+			fieldLabel: gettext('Hello Multiplier'),
+			name: 'hello_multiplier',
+			allowBlank: true,
+		    },
+		    {
+			xtype: 'numberfield',
+			fieldLabel: gettext('CSNP Interval'),
+			name: 'csnp_interval',
+			allowBlank: true,
+		    },
+		],
+	    }],
+	});
+
+	me.callParent();
+    },
+});
+
+Ext.define('PVE.sdn.Fabric.OpenFabric.InterfacePanel', {
+    extend: 'PVE.sdn.Fabric.InterfacePanel',
+
+    additionalColumns: [
+	{
+	    text: gettext('Passive'),
+	    xtype: 'widgetcolumn',
+	    dataIndex: 'passive',
+	    flex: 1,
+	    widget: {
+		xtype: 'checkbox',
+	    },
+	},
+	{
+	    text: gettext('Hello Interval'),
+	    xtype: 'widgetcolumn',
+	    dataIndex: 'hello_interval',
+	    flex: 1,
+	    widget: {
+		xtype: 'numberfield',
+	    },
+	},
+	{
+	    text: gettext('Hello Multiplier'),
+	    xtype: 'widgetcolumn',
+	    dataIndex: 'hello_multiplier',
+	    flex: 1,
+	    widget: {
+		xtype: 'numberfield',
+	    },
+	},
+	{
+	    text: gettext('CSNP Interval'),
+	    xtype: 'widgetcolumn',
+	    dataIndex: 'csnp_interval',
+	    flex: 1,
+	    widget: {
+		xtype: 'numberfield',
+	    },
+	},
+    ],
+});
+
diff --git a/www/manager6/sdn/fabrics/openfabric/NodeEdit.js b/www/manager6/sdn/fabrics/openfabric/NodeEdit.js
new file mode 100644
index 000000000000..ce61f0c15b49
--- /dev/null
+++ b/www/manager6/sdn/fabrics/openfabric/NodeEdit.js
@@ -0,0 +1,187 @@
+Ext.define('PVE.sdn.Fabric.OpenFabric.Node.InputPanel', {
+    extend: 'Proxmox.panel.InputPanel',
+
+    viewModel: {},
+
+    isCreate: undefined,
+    loadClusterInterfaces: undefined,
+
+    interface_selector: undefined,
+    node_not_accessible_warning: undefined,
+
+    onSetValues: function(values) {
+	let me = this;
+	me.interface_selector.setNetworkInterfaces(values.network_interfaces);
+	if (values.node) {
+	    // this means we are in edit mode and we have a config
+	    me.interface_selector.addInterfaces(values.node.interface);
+	    me.interface_selector.updateSelectedInterfaces(values.node.interface);
+	    return { node: values.node.node, net: values.node.net, interfaces: values.node.interface };
+	} else {
+	    // this means we are in create mode, so don't select any interfaces
+	    me.interface_selector.addInterfaces(null);
+	    me.interface_selector.updateSelectedInterfaces(null);
+	    return {};
+	}
+    },
+
+    initComponent: function() {
+	let me = this;
+	me.items = [
+	    {
+		xtype: 'pveNodeSelector',
+		reference: 'nodeselector',
+		fieldLabel: gettext('Node'),
+		labelWidth: 120,
+		name: 'node',
+		allowBlank: false,
+		disabled: !me.isCreate,
+		onlineValidator: me.isCreate,
+		autoSelect: me.isCreate,
+		listeners: {
+		    change: function(f, value) {
+			if (me.isCreate) {
+			    me.loadClusterInterfaces(value, (result) => {
+				me.setValues({ network_interfaces: result });
+			    });
+			}
+		    },
+		},
+		listConfig: {
+		    columns: [
+			{
+			    header: gettext('Node'),
+			    dataIndex: 'node',
+			    sortable: true,
+			    hideable: false,
+			    flex: 1,
+			},
+		    ],
+		},
+
+	    },
+	    me.node_not_accessible_warning,
+	    {
+		xtype: 'textfield',
+		fieldLabel: gettext('Net'),
+		labelWidth: 120,
+		name: 'net',
+		allowBlank: false,
+	    },
+	    me.interface_selector,
+	];
+	me.callParent();
+    },
+});
+
+Ext.define('PVE.sdn.Fabric.OpenFabric.Node.Edit', {
+    extend: 'Proxmox.window.Edit',
+    xtype: 'pveSDNFabricAddNode',
+
+    width: 800,
+
+    // dummyurl
+    url: '/cluster/sdn/fabrics/openfabric',
+
+    interface_selector: undefined,
+    isCreate: undefined,
+
+    controller: {
+	xclass: 'Ext.app.ViewController',
+    },
+
+    submitUrl: function(url, values) {
+	let me = this;
+	return `${me.url}/${me.extraRequestParams.fabric}/node/${values.node}`;
+    },
+
+    loadClusterInterfaces: function(node, onSuccess) {
+	Proxmox.Utils.API2Request({
+				  url: `/api2/extjs/nodes/${node}/network`,
+				  method: 'GET',
+				  success: function(response, _opts) {
+				      onSuccess(response.result.data);
+				  },
+				  // No failure callback because this api call can't fail, it
+				  // just hangs the request :) (if the node doesn't exist it gets proxied)
+	});
+    },
+    loadFabricInterfaces: function(fabric, node, onSuccess, onFailure) {
+	Proxmox.Utils.API2Request({
+				  url: `/cluster/sdn/fabrics/openfabric/${fabric}/node/${node}`,
+				  method: 'GET',
+				  success: function(response, _opts) {
+				      onSuccess(response.result.data);
+				  },
+				  failure: onFailure,
+	});
+    },
+    loadAllAvailableNodes: function(onSuccess) {
+	Proxmox.Utils.API2Request({
+				  url: `/cluster/config/nodes`,
+				  method: 'GET',
+				  success: function(response, _opts) {
+				      onSuccess(response.result.data);
+				  },
+	});
+    },
+
+    initComponent: function() {
+	let me = this;
+
+	me.interface_selector = Ext.create('PVE.sdn.Fabric.OpenFabric.InterfacePanel', {
+	    name: 'interfaces',
+	});
+
+	me.node_not_accessible_warning = Ext.create('Proxmox.form.field.DisplayEdit', {
+	    userCls: 'pmx-hint',
+	    value: gettext('The node is not accessible.'),
+	    hidden: true,
+	});
+
+	let ipanel = Ext.create('PVE.sdn.Fabric.OpenFabric.Node.InputPanel', {
+	    interface_selector: me.interface_selector,
+	    node_not_accessible_warning: me.node_not_accessible_warning,
+	    isCreate: me.isCreate,
+	    loadClusterInterfaces: me.loadClusterInterfaces,
+	});
+
+	Ext.apply(me, {
+	    subject: gettext('Node'),
+	    items: [ipanel],
+	});
+
+	me.callParent();
+
+	if (!me.isCreate) {
+	    me.loadAllAvailableNodes((allNodes) => {
+		if (allNodes.some(i => i.name === me.node)) {
+		    me.loadClusterInterfaces(me.node, (clusterResult) => {
+			me.loadFabricInterfaces(me.fabric, me.node, (fabricResult) => {
+			    fabricResult.node.interface = fabricResult.node.interface
+				.map(i => PVE.Parser.parsePropertyString(i));
+			    fabricResult.network_interfaces = clusterResult;
+			    // this will also set them as selected
+			    ipanel.setValues(fabricResult);
+			});
+		    });
+		} else {
+		    me.node_not_accessible_warning.setHidden(false);
+		    // If the node is not currently in the cluster and not available (we can't get it's interfaces).
+			me.loadFabricInterfaces(me.fabric, me.node, (fabricResult) => {
+			    fabricResult.node.interface = fabricResult.node.interface
+				.map(i => PVE.Parser.parsePropertyString(i));
+			    ipanel.setValues(fabricResult);
+			});
+		}
+	    });
+	}
+
+	if (me.isCreate) {
+	    me.method = 'POST';
+	} else {
+	    me.method = 'PUT';
+	}
+    },
+});
+
diff --git a/www/manager6/sdn/fabrics/ospf/FabricEdit.js b/www/manager6/sdn/fabrics/ospf/FabricEdit.js
new file mode 100644
index 000000000000..2ce88e443cdd
--- /dev/null
+++ b/www/manager6/sdn/fabrics/ospf/FabricEdit.js
@@ -0,0 +1,60 @@
+Ext.define('PVE.sdn.Fabric.Ospf.Fabric.Edit', {
+    extend: 'Proxmox.window.Edit',
+    xtype: 'pveSDNOpenFabricRouteEdit',
+
+    subject: gettext('Add OSPF'),
+
+    url: '/cluster/sdn/fabrics/ospf',
+    type: 'ospf',
+
+    isCreate: undefined,
+
+    viewModel: {
+	data: {
+	    isCreate: true,
+	},
+    },
+
+    items: [
+	{
+	    xtype: 'textfield',
+	    name: 'type',
+	    value: 'ospf',
+	    allowBlank: false,
+	    hidden: true,
+	},
+	{
+	    xtype: 'textfield',
+	    fieldLabel: gettext('Area'),
+	    labelWidth: 120,
+	    name: 'name',
+	    allowBlank: false,
+	    bind: {
+		disabled: '{!isCreate}',
+	    },
+	},
+    ],
+
+    submitUrl: function(url, values) {
+	let me = this;
+	return `${me.url}`;
+    },
+
+    initComponent: function() {
+	let me = this;
+
+	let view = me.getViewModel();
+	view.set('isCreate', me.isCreate);
+
+	me.method = me.isCreate ? 'POST' : 'PUT';
+
+	me.callParent();
+	if (!me.isCreate) {
+	    me.load({
+		success: function(response, opts) {
+		    me.setValues(response.result.data.fabric);
+		},
+	    });
+	}
+    },
+});
diff --git a/www/manager6/sdn/fabrics/ospf/InterfaceEdit.js b/www/manager6/sdn/fabrics/ospf/InterfaceEdit.js
new file mode 100644
index 000000000000..e7810b3f34c9
--- /dev/null
+++ b/www/manager6/sdn/fabrics/ospf/InterfaceEdit.js
@@ -0,0 +1,46 @@
+Ext.define('PVE.sdn.Fabric.Ospf.Interface.Edit', {
+    extend: 'Proxmox.window.Edit',
+    xtype: 'pveSDNOspfInterfaceEdit',
+
+    initComponent: function() {
+	let me = this;
+
+	Ext.apply(me, {
+	    items: [{
+		xtype: 'inputpanel',
+		items: [
+		    {
+			xtype: 'textfield',
+			fieldLabel: gettext('Interface'),
+			name: 'name',
+			disabled: true,
+		    },
+		    {
+			xtype: 'proxmoxcheckbox',
+			fieldLabel: gettext('Passive'),
+			name: 'passive',
+			uncheckedValue: 0,
+		    },
+		],
+	    }],
+	});
+
+	me.callParent();
+    },
+});
+
+Ext.define('PVE.sdn.Fabric.Ospf.InterfacePanel', {
+    extend: 'PVE.sdn.Fabric.InterfacePanel',
+
+    additionalColumns: [
+	{
+	    text: gettext('Passive'),
+	    xtype: 'widgetcolumn',
+	    dataIndex: 'passive',
+	    flex: 1,
+	    widget: {
+		xtype: 'checkbox',
+	    },
+	},
+    ],
+});
diff --git a/www/manager6/sdn/fabrics/ospf/NodeEdit.js b/www/manager6/sdn/fabrics/ospf/NodeEdit.js
new file mode 100644
index 000000000000..41778e930bfb
--- /dev/null
+++ b/www/manager6/sdn/fabrics/ospf/NodeEdit.js
@@ -0,0 +1,191 @@
+Ext.define('PVE.sdn.Fabric.Ospf.Node.InputPanel', {
+    extend: 'Proxmox.panel.InputPanel',
+
+    viewModel: {},
+
+    isCreate: undefined,
+    loadClusterInterfaces: undefined,
+
+    interface_selector: undefined,
+    node_not_accessible_warning: undefined,
+
+    onSetValues: function(values) {
+	let me = this;
+	me.interface_selector.setNetworkInterfaces(values.network_interfaces);
+	if (values.node) {
+	    // this means we are in edit mode and we have a config
+	    me.interface_selector.addInterfaces(values.node.interface);
+	    me.interface_selector.updateSelectedInterfaces(values.node.interface);
+	    return {
+		node: values.node.node,
+		router_id: values.node.router_id,
+		interfaces: values.node.interface,
+	    };
+	} else {
+	    // this means we are in create mode, so don't select any interfaces
+	    me.interface_selector.addInterfaces(null);
+	    me.interface_selector.updateSelectedInterfaces(null);
+	    return {};
+	}
+    },
+
+    initComponent: function() {
+	let me = this;
+	me.items = [
+	    {
+		xtype: 'pveNodeSelector',
+		reference: 'nodeselector',
+		fieldLabel: gettext('Node'),
+		labelWidth: 120,
+		name: 'node',
+		allowBlank: false,
+		disabled: !me.isCreate,
+		onlineValidator: me.isCreate,
+		autoSelect: me.isCreate,
+		listeners: {
+		    change: function(f, value) {
+			if (me.isCreate) {
+			    me.loadClusterInterfaces(value, (result) => {
+				me.setValues({ network_interfaces: result });
+			    });
+			}
+		    },
+		},
+		listConfig: {
+		    columns: [
+			{
+			    header: gettext('Node'),
+			    dataIndex: 'node',
+			    sortable: true,
+			    hideable: false,
+			    flex: 1,
+			},
+		    ],
+		},
+
+	    },
+	    me.node_not_accessible_warning,
+	    {
+		xtype: 'textfield',
+		fieldLabel: gettext('Router-Id'),
+		labelWidth: 120,
+		name: 'router_id',
+		allowBlank: false,
+	    },
+	    me.interface_selector,
+	];
+	me.callParent();
+    },
+});
+
+Ext.define('PVE.sdn.Fabric.Ospf.Node.Edit', {
+    extend: 'Proxmox.window.Edit',
+    xtype: 'pveSDNFabricAddNode',
+
+    width: 800,
+
+    // dummyurl
+    url: '/cluster/sdn/fabrics/ospf',
+
+    interface_selector: undefined,
+    isCreate: undefined,
+
+    controller: {
+	xclass: 'Ext.app.ViewController',
+    },
+
+    submitUrl: function(url, values) {
+	let me = this;
+	return `${me.url}/${me.extraRequestParams.fabric}/node/${values.node}`;
+    },
+
+    loadClusterInterfaces: function(node, onSuccess) {
+	Proxmox.Utils.API2Request({
+	    url: `/api2/extjs/nodes/${node}/network`,
+	    method: 'GET',
+	    success: function(response, _opts) {
+	        onSuccess(response.result.data);
+	    },
+	    // No failure callback because this api call can't fail, it
+	    // just hangs the request :) (if the node doesn't exist it gets proxied)
+	});
+    },
+    loadFabricInterfaces: function(fabric, node, onSuccess, onFailure) {
+	Proxmox.Utils.API2Request({
+	    url: `/cluster/sdn/fabrics/ospf/${fabric}/node/${node}`,
+	    method: 'GET',
+	    success: function(response, _opts) {
+		onSuccess(response.result.data);
+	    },
+	    failure: onFailure,
+	});
+    },
+    loadAllAvailableNodes: function(onSuccess) {
+	Proxmox.Utils.API2Request({
+	    url: `/cluster/config/nodes`,
+	    method: 'GET',
+	    success: function(response, _opts) {
+	        onSuccess(response.result.data);
+	    },
+	});
+    },
+
+    initComponent: function() {
+	let me = this;
+
+	me.interface_selector = Ext.create('PVE.sdn.Fabric.Ospf.InterfacePanel', {
+	    name: 'interfaces',
+	});
+
+	me.node_not_accessible_warning = Ext.create('Proxmox.form.field.DisplayEdit', {
+	    userCls: 'pmx-hint',
+	    value: gettext('The node is not accessible.'),
+	    hidden: true,
+	});
+
+
+	let ipanel = Ext.create('PVE.sdn.Fabric.Ospf.Node.InputPanel', {
+	    interface_selector: me.interface_selector,
+	    node_not_accessible_warning: me.node_not_accessible_warning,
+	    isCreate: me.isCreate,
+	    loadClusterInterfaces: me.loadClusterInterfaces,
+	});
+
+	Ext.apply(me, {
+	    subject: gettext('Node'),
+	    items: [ipanel],
+	});
+
+	me.callParent();
+
+	if (!me.isCreate) {
+	    me.loadAllAvailableNodes((allNodes) => {
+		if (allNodes.some(i => i.name === me.node)) {
+		    me.loadClusterInterfaces(me.node, (clusterResult) => {
+			me.loadFabricInterfaces(me.fabric, me.node, (fabricResult) => {
+			    fabricResult.node.interface = fabricResult.node.interface
+				.map(i => PVE.Parser.parsePropertyString(i));
+			    fabricResult.network_interfaces = clusterResult;
+			    // this will also set them as selected
+			    ipanel.setValues(fabricResult);
+			});
+		    });
+		} else {
+		    me.node_not_accessible_warning.setHidden(false);
+		    // If the node is not currently in the cluster and not available (we can't get it's interfaces).
+			me.loadFabricInterfaces(me.fabric, me.node, (fabricResult) => {
+			    fabricResult.node.interface = fabricResult.node.interface
+				.map(i => PVE.Parser.parsePropertyString(i));
+			    ipanel.setValues(fabricResult);
+			});
+		}
+	    });
+	}
+
+	if (me.isCreate) {
+	    me.method = 'POST';
+	} else {
+	    me.method = 'PUT';
+	}
+    },
+});
-- 
2.39.5



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


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

* [pve-devel] [PATCH pve-manager 11/11] network: return loopback interface on network endpoint
  2025-02-14 13:39 [pve-devel] [RFC cluster/manager/network/proxmox{-ve-rs, -perl-rs} 00/11] Add SDN Fabrics Gabriel Goller
                   ` (9 preceding siblings ...)
  2025-02-14 13:39 ` [pve-devel] [PATCH pve-manager 10/11] sdn: add fabric edit/delete forms Gabriel Goller
@ 2025-02-14 13:39 ` Gabriel Goller
  2025-03-03 16:58 ` [pve-devel] [RFC cluster/manager/network/proxmox{-ve-rs, -perl-rs} 00/11] Add SDN Fabrics Stefan Hanreich
  11 siblings, 0 replies; 32+ messages in thread
From: Gabriel Goller @ 2025-02-14 13:39 UTC (permalink / raw)
  To: pve-devel

The endpoint '/network' returns all the interfaces but for some unknown
reason filters out the loopback address. This is not desirable for the
sdn fabrics, as we want to be able to add a loopback address to a
fabric.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 PVE/API2/Network.pm | 2 --
 1 file changed, 2 deletions(-)

diff --git a/PVE/API2/Network.pm b/PVE/API2/Network.pm
index 3c45fe2fb7bf..870164e9b639 100644
--- a/PVE/API2/Network.pm
+++ b/PVE/API2/Network.pm
@@ -356,8 +356,6 @@ __PACKAGE__->register_method({
 
 	my $ifaces = $config->{ifaces};
 
-	delete $ifaces->{lo}; # do not list the loopback device
-
 	if (my $tfilter = $param->{type}) {
 	    my $vnets;
 
-- 
2.39.5



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


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

* Re: [pve-devel] [PATCH pve-cluster 05/11] cluster: add sdn fabrics config files
  2025-02-14 13:39 ` [pve-devel] [PATCH pve-cluster 05/11] cluster: add sdn fabrics config files Gabriel Goller
@ 2025-02-28 12:19   ` Thomas Lamprecht
  2025-02-28 12:52     ` Gabriel Goller
  0 siblings, 1 reply; 32+ messages in thread
From: Thomas Lamprecht @ 2025-02-28 12:19 UTC (permalink / raw)
  To: Proxmox VE development discussion, Gabriel Goller

Am 14.02.25 um 14:39 schrieb Gabriel Goller:
> Add the sdn fabrics config files. These are split into two, as we
> currently support two fabric types: ospf and openfabric. They hold the
> whole configuration for the respective protocols. They are read and
> written by pve-network.
> 
> Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
> ---
>  src/PVE/Cluster.pm | 2 ++
>  1 file changed, 2 insertions(+)
> 
> diff --git a/src/PVE/Cluster.pm b/src/PVE/Cluster.pm
> index e0e3ee995085..a325b67905b8 100644
> --- a/src/PVE/Cluster.pm
> +++ b/src/PVE/Cluster.pm
> @@ -81,6 +81,8 @@ my $observed = {
>      'sdn/pve-ipam-state.json' => 1,
>      'sdn/mac-cache.json' => 1,
>      'sdn/dns.cfg' => 1,
> +    'sdn/fabrics/openfabric.cfg' => 1,
> +    'sdn/fabrics/ospf.cfg' => 1,

You also need to add these to the pmxcfs' memdb_change_array in status.c
to actually do something.

FWIW, could also use the singular for fabric here, but there isn't that
clearly consistent existing style, so the biggest benefit would be shorter
paths, not that a single character is much worth here, just noting as it
stuck out.

>      'sdn/.running-config' => 1,
>      'virtual-guest/cpu-models.conf' => 1,
>      'virtual-guest/profiles.cfg' => 1,



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


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

* Re: [pve-devel] [PATCH pve-cluster 05/11] cluster: add sdn fabrics config files
  2025-02-28 12:19   ` Thomas Lamprecht
@ 2025-02-28 12:52     ` Gabriel Goller
  0 siblings, 0 replies; 32+ messages in thread
From: Gabriel Goller @ 2025-02-28 12:52 UTC (permalink / raw)
  To: Thomas Lamprecht; +Cc: Proxmox VE development discussion

On 28.02.2025 13:19, Thomas Lamprecht wrote:
>Am 14.02.25 um 14:39 schrieb Gabriel Goller:
>> Add the sdn fabrics config files. These are split into two, as we
>> currently support two fabric types: ospf and openfabric. They hold the
>> whole configuration for the respective protocols. They are read and
>> written by pve-network.
>>
>> Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
>> ---
>>  src/PVE/Cluster.pm | 2 ++
>>  1 file changed, 2 insertions(+)
>>
>> diff --git a/src/PVE/Cluster.pm b/src/PVE/Cluster.pm
>> index e0e3ee995085..a325b67905b8 100644
>> --- a/src/PVE/Cluster.pm
>> +++ b/src/PVE/Cluster.pm
>> @@ -81,6 +81,8 @@ my $observed = {
>>      'sdn/pve-ipam-state.json' => 1,
>>      'sdn/mac-cache.json' => 1,
>>      'sdn/dns.cfg' => 1,
>> +    'sdn/fabrics/openfabric.cfg' => 1,
>> +    'sdn/fabrics/ospf.cfg' => 1,
>
>You also need to add these to the pmxcfs' memdb_change_array in status.c
>to actually do something.

Ah, didn't see that one, thanks!

>FWIW, could also use the singular for fabric here, but there isn't that
>clearly consistent existing style, so the biggest benefit would be shorter
>paths, not that a single character is much worth here, just noting as it
>stuck out.

We seem to use plural for all the sdn config files (subnets.cfg,
controllers.cfg, vnets.cfg, etc.).

But no hard feelings here.

>>      'sdn/.running-config' => 1,
>>      'virtual-guest/cpu-models.conf' => 1,
>>      'virtual-guest/profiles.cfg' => 1,

Thanks for reviewing this!


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


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

* Re: [pve-devel] [PATCH proxmox-ve-rs 03/11] add intermediate fabric representation
  2025-02-14 13:39 ` [pve-devel] [PATCH proxmox-ve-rs 03/11] add intermediate fabric representation Gabriel Goller
@ 2025-02-28 13:57   ` Thomas Lamprecht
  2025-02-28 16:19     ` Gabriel Goller
  2025-03-04 17:30     ` Gabriel Goller
  2025-03-04  8:45   ` Stefan Hanreich
  1 sibling, 2 replies; 32+ messages in thread
From: Thomas Lamprecht @ 2025-02-28 13:57 UTC (permalink / raw)
  To: Proxmox VE development discussion, Gabriel Goller

Am 14.02.25 um 14:39 schrieb Gabriel Goller:
> This adds the intermediate, type-checked fabrics config. This one is
> parsed from the SectionConfig and can be converted into the
> Frr-Representation.

The short description of the patch is good, but I would like to see more
rationale here about choosing this way, like benefits and trade-offs to other
options that got evaluated, if this can/will be generic for all fabrics planned,
..., and definitively some more rust-documentation for public types and modules.

One thing I noticed below, I did not managed to do a thorough review besides
of that yet though.

> 
> Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
> Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
> ---
>  proxmox-ve-config/Cargo.toml                  |  10 +-
>  proxmox-ve-config/debian/control              |   4 +-
>  proxmox-ve-config/src/sdn/fabric/common.rs    |  90 ++++
>  proxmox-ve-config/src/sdn/fabric/mod.rs       |  68 +++
>  .../src/sdn/fabric/openfabric.rs              | 494 ++++++++++++++++++
>  proxmox-ve-config/src/sdn/fabric/ospf.rs      | 375 +++++++++++++
>  proxmox-ve-config/src/sdn/mod.rs              |   1 +
>  7 files changed, 1036 insertions(+), 6 deletions(-)
>  create mode 100644 proxmox-ve-config/src/sdn/fabric/common.rs
>  create mode 100644 proxmox-ve-config/src/sdn/fabric/mod.rs
>  create mode 100644 proxmox-ve-config/src/sdn/fabric/openfabric.rs
>  create mode 100644 proxmox-ve-config/src/sdn/fabric/ospf.rs
> 

> diff --git a/proxmox-ve-config/src/sdn/fabric/common.rs b/proxmox-ve-config/src/sdn/fabric/common.rs
> new file mode 100644
> index 000000000000..400f5b6d6b12
> --- /dev/null
> +++ b/proxmox-ve-config/src/sdn/fabric/common.rs
> @@ -0,0 +1,90 @@
> +use serde::{Deserialize, Serialize};
> +use std::fmt::Display;
> +use thiserror::Error;
> +
> +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Error)]
> +pub enum ConfigError {
> +    #[error("node id has invalid format")]
> +    InvalidNodeId,
> +}
> +
> +#[derive(Debug, Deserialize, Serialize, Clone, Eq, Hash, PartialOrd, Ord, PartialEq)]
> +pub struct Hostname(String);
> +
> +impl From<String> for Hostname {
> +    fn from(value: String) -> Self {
> +        Hostname::new(value)
> +    }
> +}
> +
> +impl AsRef<str> for Hostname {
> +    fn as_ref(&self) -> &str {
> +        &self.0
> +    }
> +}
> +
> +impl Display for Hostname {
> +    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
> +        self.0.fmt(f)
> +    }
> +}
> +
> +impl Hostname {
> +    pub fn new(name: impl Into<String>) -> Hostname {
> +        Self(name.into())
> +    }
> +}
> +
> +// parses a bool from a string OR bool
> +pub mod serde_option_bool {

might be maybe something to put in proxmox-serde?


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


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

* Re: [pve-devel] [PATCH proxmox-ve-rs 03/11] add intermediate fabric representation
  2025-02-28 13:57   ` Thomas Lamprecht
@ 2025-02-28 16:19     ` Gabriel Goller
  2025-03-04 17:30     ` Gabriel Goller
  1 sibling, 0 replies; 32+ messages in thread
From: Gabriel Goller @ 2025-02-28 16:19 UTC (permalink / raw)
  To: Thomas Lamprecht; +Cc: Proxmox VE development discussion

On 28.02.2025 14:57, Thomas Lamprecht wrote:
>Am 14.02.25 um 14:39 schrieb Gabriel Goller:
>> This adds the intermediate, type-checked fabrics config. This one is
>> parsed from the SectionConfig and can be converted into the
>> Frr-Representation.
>
>The short description of the patch is good, but I would like to see more
>rationale here about choosing this way, like benefits and trade-offs to other
>options that got evaluated, if this can/will be generic for all fabrics planned,
>..., and definitively some more rust-documentation for public types and modules.

Yep, wrote together a small reasoning and will write some more
documentation for public types.

>One thing I noticed below, I did not managed to do a thorough review besides
>of that yet though.
>> [snip]
>> +impl Hostname {
>> +    pub fn new(name: impl Into<String>) -> Hostname {
>> +        Self(name.into())
>> +    }
>> +}
>> +
>> +// parses a bool from a string OR bool
>> +pub mod serde_option_bool {
>
>might be maybe something to put in proxmox-serde?

Yep, I agree.

Thanks for the review!



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


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

* Re: [pve-devel] [PATCH proxmox-ve-rs 01/11] add crate with common network types
  2025-02-14 13:39 ` [pve-devel] [PATCH proxmox-ve-rs 01/11] add crate with common network types Gabriel Goller
@ 2025-03-03 15:08   ` Stefan Hanreich
  2025-03-05  8:28     ` Gabriel Goller
  0 siblings, 1 reply; 32+ messages in thread
From: Stefan Hanreich @ 2025-03-03 15:08 UTC (permalink / raw)
  To: Proxmox VE development discussion, Gabriel Goller

Maybe we should think about internally representing the Network Entity
Title and its parts as bytes rather than strings? So [u8; n]? Since in
practice it's exactly that. I think this would simplify a lot of the
methods here, and obsolete some stuff like e.g. length validation. I've
noted some parts below where I think we could benefit from that.

We could also provide conversion methods for MAC addresses easily then,
although IP addresses might be a bit awkward since they map to network
entity titles as binary-coded decimal and there's no std support for
that so we'd have to brew our own conversions...

Also, theoretically, the Area ID is of variable length, but for our use
case the current additional constraints are fine imo and not worth the
hassle of implementing variable length Area IDs. Might be good to note
this though.

On 2/14/25 14:39, Gabriel Goller wrote:
> The new proxmox-network-types crate holds some common types that are
> used by proxmox-frr, proxmox-ve-config and proxmox-perl-rs. These types
> are here because we don't want proxmox-frr to be a dependency of
> proxmox-ve-config or vice-versa (or at least it should be feature-gated).
> They should be independent.
> 
> Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
> ---
>  Cargo.toml                       |   6 +
>  proxmox-network-types/Cargo.toml |  15 ++
>  proxmox-network-types/src/lib.rs |   1 +
>  proxmox-network-types/src/net.rs | 239 +++++++++++++++++++++++++++++++
>  4 files changed, 261 insertions(+)
>  create mode 100644 proxmox-network-types/Cargo.toml
>  create mode 100644 proxmox-network-types/src/lib.rs
>  create mode 100644 proxmox-network-types/src/net.rs
> 
> diff --git a/Cargo.toml b/Cargo.toml
> index dc7f312fb8a9..e452c931e78c 100644
> --- a/Cargo.toml
> +++ b/Cargo.toml
> @@ -1,6 +1,7 @@
>  [workspace]
>  members = [
>      "proxmox-ve-config",
> +    "proxmox-network-types",
>  ]
>  exclude = [
>      "build",
> @@ -15,3 +16,8 @@ homepage = "https://proxmox.com"
>  exclude = [ "debian" ]
>  rust-version = "1.82"
>  
> +[workspace.dependencies]
> +proxmox-section-config = "2.1.1"
> +serde = "1"
> +serde_with = "3.8.1"
> +thiserror = "1.0.59"
> diff --git a/proxmox-network-types/Cargo.toml b/proxmox-network-types/Cargo.toml
> new file mode 100644
> index 000000000000..93f4df87a59f
> --- /dev/null
> +++ b/proxmox-network-types/Cargo.toml
> @@ -0,0 +1,15 @@
> +[package]
> +name = "proxmox-network-types"
> +version = "0.1.0"
> +authors.workspace = true
> +edition.workspace = true
> +license.workspace = true
> +homepage.workspace = true
> +exclude.workspace = true
> +rust-version.workspace = true
> +
> +[dependencies]
> +thiserror = { workspace = true }
> +anyhow = "1"
> +serde = { workspace = true, features = [ "derive" ] }
> +serde_with = { workspace = true }
> diff --git a/proxmox-network-types/src/lib.rs b/proxmox-network-types/src/lib.rs
> new file mode 100644
> index 000000000000..f9faf2ff6542
> --- /dev/null
> +++ b/proxmox-network-types/src/lib.rs
> @@ -0,0 +1 @@
> +pub mod net;
> diff --git a/proxmox-network-types/src/net.rs b/proxmox-network-types/src/net.rs
> new file mode 100644
> index 000000000000..5fdbe3920800
> --- /dev/null
> +++ b/proxmox-network-types/src/net.rs
> @@ -0,0 +1,239 @@
> +use std::{fmt::Display, str::FromStr};
> +
> +use serde::Serialize;
> +use serde_with::{DeserializeFromStr, SerializeDisplay};
> +use thiserror::Error;
> +
> +#[derive(Error, Debug)]
> +pub enum NetError {
> +    #[error("Some octets are missing")]
> +    WrongLength,
> +    #[error("The NET selector must be two characters wide and be 00")]
> +    InvalidNetSelector,
> +    #[error("Invalid AFI (wrong size or position)")]
> +    InvalidAFI,
> +    #[error("Invalid Area (wrong size or position)")]
> +    InvalidArea,
> +    #[error("Invalid SystemId (wrong size or position)")]
> +    InvalidSystemId,
> +}
> +
> +/// Address Family authority Identifier - 49 The AFI value 49 is what IS-IS (and openfabric) uses
> +/// for private addressing.
> +#[derive(
> +    Debug, DeserializeFromStr, SerializeDisplay, Clone, Hash, PartialEq, Eq, PartialOrd, Ord,
> +)]
> +struct NetAFI(String);
> +
> +impl Default for NetAFI {
> +    fn default() -> Self {
> +        Self("49".to_owned())
> +    }
> +}
> +
> +impl Display for NetAFI {
> +    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
> +        write!(f, "{}", self.0)
> +    }
> +}
> +
> +impl FromStr for NetAFI {
> +    type Err = NetError;
> +
> +    fn from_str(s: &str) -> Result<Self, Self::Err> {
> +        if s.len() != 2 {
> +            Err(NetError::InvalidAFI)
> +        } else {
> +            Ok(Self(s.to_owned()))
> +        }
> +    }
> +}
> +
> +/// Area identifier: 0001 IS-IS area number (numerical area 1)
> +/// The second part (system) of the `net` identifier. Every node has to have a different system
> +/// number.
> +#[derive(Debug, DeserializeFromStr, Serialize, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
> +struct NetArea(String);
> +
> +impl Default for NetArea {
> +    fn default() -> Self {
> +        Self("0001".to_owned())
> +    }
> +}
> +
> +impl Display for NetArea {
> +    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
> +        write!(f, "{}", self.0)
> +    }
> +}
> +
> +impl FromStr for NetArea {
> +    type Err = NetError;
> +
> +    fn from_str(s: &str) -> Result<Self, Self::Err> {
> +        if s.len() != 4 {
> +            Err(NetError::InvalidArea)
> +        } else {
> +            Ok(Self(s.to_owned()))
> +        }
> +    }
> +}
> +
> +/// System identifier: 1921.6800.1002 - for system identifiers we recommend to use IP address or
> +/// MAC address of the router itself. The way to construct this is to keep all of the zeroes of the
> +/// router IP address, and then change the periods from being every three numbers to every four
> +/// numbers. The address that is listed here is 192.168.1.2, which if expanded will turn into
> +/// 192.168.001.002. Then all one has to do is move the dots to have four numbers instead of three.
> +/// This gives us 1921.6800.1002.
> +#[derive(Debug, DeserializeFromStr, Serialize, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
> +struct NetSystemId(String);
> +
> +impl Display for NetSystemId {
> +    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
> +        write!(f, "{}", self.0)
> +    }
> +}
> +
> +impl FromStr for NetSystemId {
> +    type Err = NetError;
> +
> +    fn from_str(s: &str) -> Result<Self, Self::Err> {
> +        if s.split(".").count() != 3 || s.split(".").any(|octet| octet.len() != 4) {

would simplify those checks a lot imo. and would also be a more natural
representation since the nomenclature here (octets, but len == 4) is a
bit misleading as well..

> +            Err(NetError::InvalidArea)
> +        } else {
> +            Ok(Self(s.to_owned()))
> +        }
> +    }
> +}
> +
> +/// NET selector: 00 Must always be 00. This setting indicates “this system” or “local system.”
> +#[derive(Debug, DeserializeFromStr, Serialize, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
> +struct NetSelector(String);
> +
> +impl Default for NetSelector {
> +    fn default() -> Self {
> +        Self("00".to_owned())
> +    }
> +}
> +
> +impl Display for NetSelector {
> +    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
> +        write!(f, "{}", self.0)
> +    }
> +}
> +
> +impl FromStr for NetSelector {
> +    type Err = NetError;
> +
> +    fn from_str(s: &str) -> Result<Self, Self::Err> {
> +        if s.len() != 2 {
> +            Err(NetError::InvalidNetSelector)
> +        } else {
> +            Ok(Self(s.to_owned()))
> +        }
> +    }
> +}
> +
> +/// The first part (area) of the `net` identifier. The entire OpenFabric fabric has to have the
> +/// same area.
> +/// f.e.: "49.0001"
> +#[derive(
> +    Debug, DeserializeFromStr, SerializeDisplay, Clone, Hash, PartialEq, Eq, PartialOrd, Ord,
> +)]
> +pub struct Net {
> +    afi: NetAFI,
> +    area: NetArea,
> +    system: NetSystemId,
> +    selector: NetSelector,
> +}
> +
> +impl FromStr for Net {
> +    type Err = NetError;
> +
> +    fn from_str(s: &str) -> Result<Self, Self::Err> {
> +        if s.split(".").count() != 6 {
> +            return Err(NetError::WrongLength);
> +        }

parsing here could be greatly simplified imo if we convert everything to u8

> +        let mut iter = s.split(".");
> +        let afi = iter.next().ok_or(NetError::WrongLength)?;
> +        let area = iter.next().ok_or(NetError::WrongLength)?;
> +        let system = format!(
> +            "{}.{}.{}",
> +            iter.next().ok_or(NetError::WrongLength)?,
> +            iter.next().ok_or(NetError::WrongLength)?,
> +            iter.next().ok_or(NetError::WrongLength)?
> +        );
> +        let selector = iter.next().ok_or(NetError::WrongLength)?;
> +        Ok(Self {
> +            afi: afi.parse()?,
> +            area: area.parse()?,
> +            system: system.parse()?,
> +            selector: selector.parse()?,
> +        })
> +    }
> +}
> +
> +impl Display for Net {
> +    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
> +        write!(
> +            f,
> +            "{}.{}.{}.{}",
> +            self.afi, self.area, self.system, self.selector
> +        )
> +    }
> +}
> +
> +#[cfg(test)]
> +mod tests {
> +    use super::*;
> +
> +    #[test]
> +    fn test_net_from_str() {
> +        let input = "49.0001.1921.6800.1002.00";
> +        let net = input.parse::<Net>().expect("this net should parse");
> +        assert_eq!(net.afi, NetAFI("49".to_owned()));
> +        assert_eq!(net.area, NetArea("0001".to_owned()));
> +        assert_eq!(net.system, NetSystemId("1921.6800.1002".to_owned()));
> +        assert_eq!(net.selector, NetSelector("00".to_owned()));
> +
> +        let input = "45.0200.0100.1001.0010.01";
> +        let net = input.parse::<Net>().expect("this net should parse");
> +        assert_eq!(net.afi, NetAFI("45".to_owned()));
> +        assert_eq!(net.area, NetArea("0200".to_owned()));
> +        assert_eq!(net.system, NetSystemId("0100.1001.0010".to_owned()));
> +        assert_eq!(net.selector, NetSelector("01".to_owned()));
> +    }
> +
> +    #[test]
> +    fn test_net_from_str_failed() {
> +        let input = "49.0001.1921.6800.1002.000";
> +        assert!(matches!(
> +            input.parse::<Net>(),
> +            Err(NetError::InvalidNetSelector)
> +        ));
> +
> +        let input = "49.0001.1921.6800.1002.00.00";
> +        assert!(matches!(input.parse::<Net>(), Err(NetError::WrongLength)));
> +
> +        let input = "49.0001.1921.6800.10002.00";
> +        assert!(matches!(input.parse::<Net>(), Err(NetError::InvalidArea)));
> +
> +        let input = "409.0001.1921.6800.1002.00";
> +        assert!(matches!(input.parse::<Net>(), Err(NetError::InvalidAFI)));
> +
> +        let input = "49.00001.1921.6800.1002.00";
> +        assert!(matches!(input.parse::<Net>(), Err(NetError::InvalidArea)));
> +    }
> +
> +    #[test]
> +    fn test_net_display() {
> +        let net = Net {
> +            afi: NetAFI("49".to_owned()),
> +            area: NetArea("0001".to_owned()),
> +            system: NetSystemId("1921.6800.1002".to_owned()),
> +            selector: NetSelector("00".to_owned()),
> +        };
> +        assert_eq!(format!("{net}"), "49.0001.1921.6800.1002.00");
> +    }
> +}
> +



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

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

* Re: [pve-devel] [PATCH proxmox-ve-rs 02/11] add proxmox-frr crate with frr types
  2025-02-14 13:39 ` [pve-devel] [PATCH proxmox-ve-rs 02/11] add proxmox-frr crate with frr types Gabriel Goller
@ 2025-03-03 16:29   ` Stefan Hanreich
  2025-03-04 16:28     ` Gabriel Goller
  0 siblings, 1 reply; 32+ messages in thread
From: Stefan Hanreich @ 2025-03-03 16:29 UTC (permalink / raw)
  To: Gabriel Goller, pve-devel

This uses stuff from a later patch, doesn't it? Shouldn't the order of
patches 2 and 3 be flipped?

On 2/14/25 14:39, Gabriel Goller wrote:
> This crate contains types that represent the frr config. For example it
> contains a `Router` and `Interface` struct. This Frr-Representation can
> then be converted to the real frr config.
> 
> Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
> Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
> ---
>  Cargo.toml                    |   1 +
>  proxmox-frr/Cargo.toml        |  25 ++++
>  proxmox-frr/src/common.rs     |  54 ++++++++
>  proxmox-frr/src/lib.rs        | 223 ++++++++++++++++++++++++++++++++++
>  proxmox-frr/src/openfabric.rs | 137 +++++++++++++++++++++
>  proxmox-frr/src/ospf.rs       | 148 ++++++++++++++++++++++
>  6 files changed, 588 insertions(+)
>  create mode 100644 proxmox-frr/Cargo.toml
>  create mode 100644 proxmox-frr/src/common.rs
>  create mode 100644 proxmox-frr/src/lib.rs
>  create mode 100644 proxmox-frr/src/openfabric.rs
>  create mode 100644 proxmox-frr/src/ospf.rs
> 
> diff --git a/Cargo.toml b/Cargo.toml
> index e452c931e78c..ffda1233b17a 100644
> --- a/Cargo.toml
> +++ b/Cargo.toml
> @@ -1,6 +1,7 @@
>  [workspace]
>  members = [
>      "proxmox-ve-config",
> +    "proxmox-frr",
>      "proxmox-network-types",
>  ]
>  exclude = [
> diff --git a/proxmox-frr/Cargo.toml b/proxmox-frr/Cargo.toml
> new file mode 100644
> index 000000000000..bea8a0f8bab3
> --- /dev/null
> +++ b/proxmox-frr/Cargo.toml
> @@ -0,0 +1,25 @@
> +[package]
> +name = "proxmox-frr"
> +version = "0.1.0"
> +authors.workspace = true
> +edition.workspace = true
> +license.workspace = true
> +homepage.workspace = true
> +exclude.workspace = true
> +rust-version.workspace = true
> +
> +[dependencies]
> +thiserror = { workspace = true }
> +anyhow = "1"
> +tracing = "0.1"
> +
> +serde = { workspace = true, features = [ "derive" ] }
> +serde_with = { workspace = true }
> +itoa = "1.0.9"
> +
> +proxmox-ve-config = { path = "../proxmox-ve-config", optional = true }
> +proxmox-section-config = { workspace = true, optional = true }
> +proxmox-network-types = { path = "../proxmox-network-types/" }
> +
> +[features]
> +config-ext = ["dep:proxmox-ve-config", "dep:proxmox-section-config" ]
> diff --git a/proxmox-frr/src/common.rs b/proxmox-frr/src/common.rs
> new file mode 100644
> index 000000000000..0d99bb4da6e2
> --- /dev/null
> +++ b/proxmox-frr/src/common.rs
> @@ -0,0 +1,54 @@
> +use std::{fmt::Display, str::FromStr};
> +
> +use serde_with::{DeserializeFromStr, SerializeDisplay};
> +use thiserror::Error;
> +
> +#[derive(Error, Debug)]
> +pub enum FrrWordError {
> +    #[error("word is empty")]
> +    IsEmpty,
> +    #[error("word contains invalid character")]
> +    InvalidCharacter,
> +}
> +
> +#[derive(Clone, Debug, PartialEq, Eq, Hash, DeserializeFromStr, SerializeDisplay)]
> +pub struct FrrWord(String);
> +
> +impl FrrWord {
> +    pub fn new(name: String) -> Result<Self, FrrWordError> {
> +        if name.is_empty() {
> +            return Err(FrrWordError::IsEmpty);
> +        }
> +
> +        if name
> +            .as_bytes()
> +            .iter()
> +            .any(|c| !c.is_ascii() || c.is_ascii_whitespace())
> +        {
> +            return Err(FrrWordError::InvalidCharacter);
> +        }
> +
> +        Ok(Self(name))
> +    }
> +}
> +
> +impl FromStr for FrrWord {
> +    type Err = FrrWordError;
> +
> +    fn from_str(s: &str) -> Result<Self, Self::Err> {
> +        FrrWord::new(s.to_string())
> +    }
> +}
> +
> +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
> +    }
> +}
> +
> diff --git a/proxmox-frr/src/lib.rs b/proxmox-frr/src/lib.rs
> new file mode 100644
> index 000000000000..ceef82999619
> --- /dev/null
> +++ b/proxmox-frr/src/lib.rs
> @@ -0,0 +1,223 @@
> +pub mod common;
> +pub mod openfabric;
> +pub mod ospf;
> +
> +use std::{collections::{hash_map::Entry, HashMap}, fmt::Display, str::FromStr};
> +
> +use common::{FrrWord, FrrWordError};
> +use proxmox_ve_config::sdn::fabric::common::Hostname;

Maybe move it to network-types, if it is always needed? Seems like a
better fit. Especially since the dependency is optional.

> +#[cfg(feature = "config-ext")]
> +use proxmox_ve_config::sdn::fabric::FabricConfig;
> +
> +use serde::{Deserialize, Serialize};
> +use serde_with::{DeserializeFromStr, SerializeDisplay};
> +use thiserror::Error;
> +
> +#[derive(Error, Debug)]
> +pub enum RouterNameError {
> +    #[error("invalid name")]
> +    InvalidName,
> +    #[error("invalid frr word")]
> +    FrrWordError(#[from] FrrWordError),
> +}
> +
> +#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
> +pub enum Router {
> +    OpenFabric(openfabric::OpenFabricRouter),
> +    Ospf(ospf::OspfRouter),
> +}
> +
> +impl From<openfabric::OpenFabricRouter> for Router {
> +    fn from(value: openfabric::OpenFabricRouter) -> Self {
> +        Router::OpenFabric(value)
> +    }
> +}
> +
> +#[derive(Clone, Debug, PartialEq, Eq, Hash, DeserializeFromStr, SerializeDisplay)]
> +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),
> +        }
> +    }
> +}
> +
> +impl FromStr for RouterName {
> +    type Err = RouterNameError;
> +
> +    fn from_str(s: &str) -> Result<Self, Self::Err> {
> +        if let Ok(router) = s.parse() {
> +            return Ok(Self::OpenFabric(router));
> +        }

does this make sense here? can we actually make a clear distinction on
whether this is a OpenFabric / OSPF router name (and for all other
future RouterNames) from the string alone? I think it might be better to
explicitly construct the specific type and then make a RouterName out of
it and do not implement FromStr at all. Or get rid of RouterName
altogether (see below).

It's also constructing an OpenFabric RouterName but never an OSPF router
name.

> +
> +        Err(RouterNameError::InvalidName)
> +    }
> +}
> +
> +/// The interface name is the same on ospf and openfabric, but it is an enum so we can have two
> +/// different entries in the hashmap. This allows us to have an interface in an ospf and openfabric
> +/// fabric.
> +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash)]
> +pub enum InterfaceName {
> +    OpenFabric(FrrWord),
> +    Ospf(FrrWord),
> +}

maybe this should be a struct representing a linux interface name
(nul-terminated 16byte string) instead of an FrrWord?

> +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),
> +        }
> +        
> +    }
> +}
> +
> +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
> +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(Clone, Debug, PartialEq, Eq, Default, Deserialize, Serialize)]
> +pub struct FrrConfig {
> +    router: HashMap<RouterName, Router>,
> +    interfaces: HashMap<InterfaceName, Interface>,

are we ever querying the frr router/interface by name? judging from the
public API we don't and only iterate over it. we could move the name
into the Router/Interface then, this would ensure that the name always
fits the concrete router. We could probably also make converting from
the section config easier then and possibly save us the whole RouterName
struct.

Are duplicates possible with how the ID in the SectionConfig works? If
we want to avoid that, we could probably use other ways.

> +}
> +
> +impl FrrConfig {
> +    pub fn new() -> Self {
> +        Self::default()
> +    }
> +
> +    #[cfg(feature = "config-ext")]
> +    pub fn builder() -> FrrConfigBuilder {
> +        FrrConfigBuilder::default()
> +    }

see above for if we really need a builder here or if implementing
conversion traits suffices.

> +
> +    pub fn router(&self) -> impl Iterator<Item = (&RouterName, &Router)> + '_ {
> +        self.router.iter()
> +    }
> +
> +    pub fn interfaces(&self) -> impl Iterator<Item = (&InterfaceName, &Interface)> + '_ {
> +        self.interfaces.iter()
> +    }
> +}
> +
> +#[derive(Default)]
> +#[cfg(feature = "config-ext")]
> +pub struct FrrConfigBuilder {
> +    fabrics: FabricConfig,
> +    //bgp: Option<internal::BgpConfig>
> +}
> +
> +#[cfg(feature = "config-ext")]
> +impl FrrConfigBuilder {
> +    pub fn add_fabrics(mut self, fabric: FabricConfig) -> FrrConfigBuilder {
> +        self.fabrics = fabric;
> +        self
> +    }

From<FabricConfig> might be better if it replaces self.fabrics /
consumes FabricConfig anyway? Maybe even TryFrom<FabricConfig> for
FrrConfig itself?

> +
> +    pub fn build(self, current_node: &str) -> Result<FrrConfig, anyhow::Error> {
> +        let mut router: HashMap<RouterName, Router> = HashMap::new();
> +        let mut interfaces: HashMap<InterfaceName, Interface> = HashMap::new();
> +
> +        if let Some(openfabric) = self.fabrics.openfabric() {
> +            // openfabric
> +            openfabric
> +                .fabrics()
> +                .iter()
> +                .try_for_each(|(fabric_id, fabric_config)| {
> +                    let node_config = fabric_config.nodes().get(&Hostname::new(current_node));
> +                    if let Some(node_config) = node_config {
> +                        let ofr = openfabric::OpenFabricRouter::from((fabric_config, node_config));
> +                        let router_item = Router::OpenFabric(ofr);
> +                        let router_name = RouterName::OpenFabric(
> +                            openfabric::OpenFabricRouterName::try_from(fabric_id)?,
> +                        );
> +                        router.insert(router_name.clone(), router_item);
> +                        node_config.interfaces().try_for_each(|interface| {
> +                            let mut openfabric_interface: openfabric::OpenFabricInterface =
> +                                (fabric_id, interface).try_into()?;

The only fallible thing here is constructing the RouterName, so we could
just clone it from above and make a
OpenFabricInterface::from_section_config() method that accepts the name
+ sectionconfig structs?

> +                            // If no specific hello_interval is set, get default one from fabric
> +                            // config
> +                            if openfabric_interface.hello_interval().is_none() {
> +                                openfabric_interface
> +                                    .set_hello_interval(fabric_config.hello_interval().clone());
> +                            }
> +                            let interface_name = InterfaceName::OpenFabric(FrrWord::from_str(interface.name())?);
> +                            // Openfabric doesn't allow an interface to be in multiple openfabric
> +                            // fabrics. Frr will just ignore it and take the first one.
> +                            if let Entry::Vacant(e) = interfaces.entry(interface_name) {
> +                                e.insert(openfabric_interface.into());
> +                            } else {
> +                                tracing::warn!("An interface cannot be in multiple openfabric fabrics");
> +                            }

if let Err(_) = interfaces.try_insert(..) maybe (if we keep the HashMap)?

> +                            Ok::<(), anyhow::Error>(())
> +                        })?;
> +                    } else {
> +                        tracing::warn!("no node configuration for fabric \"{fabric_id}\" – this fabric is not configured for this node.");

Maybe it would make sense to split this into two functions, where we
could just return early if there is no configuration for this node?

Then here an early return would suffice, since otherwise the log gets
spammed on nodes that are simply not part of a fabric (which is
perfectly valid)?

> +                        return Ok::<(), anyhow::Error>(());
> +                    }
> +                    Ok(())
> +                })?;
> +        }
> +        if let Some(ospf) = self.fabrics.ospf() {
> +            // ospf
> +            ospf.fabrics()
> +                .iter()
> +                .try_for_each(|(fabric_id, fabric_config)| {
> +                    let node_config = fabric_config.nodes().get(&Hostname::new(current_node));
> +                    if let Some(node_config) = node_config {
> +                        let ospf_router = ospf::OspfRouter::from((fabric_config, node_config));
> +                        let router_item = Router::Ospf(ospf_router);
> +                        let router_name = RouterName::Ospf(ospf::OspfRouterName::from(ospf::Area::try_from(fabric_id)?));
> +                        router.insert(router_name.clone(), router_item);
> +                        node_config.interfaces().try_for_each(|interface| {
> +                            let ospf_interface: ospf::OspfInterface = (fabric_id, interface).try_into()?;
> +
> +                            let interface_name = InterfaceName::Ospf(FrrWord::from_str(interface.name())?);
> +                            // Ospf only allows one area per interface, so one interface cannot be
> +                            // in two areas (fabrics). Though even if this happens, it is not a big
> +                            // problem as frr filters it out.
> +                            if let Entry::Vacant(e) = interfaces.entry(interface_name) {
> +                                e.insert(ospf_interface.into());
> +                            } else {
> +                                tracing::warn!("An interface cannot be in multiple ospf areas");
> +                            }
> +                            Ok::<(), anyhow::Error>(())
> +                        })?;
> +                    } else {
> +                        tracing::warn!("no node configuration for fabric \"{fabric_id}\" – this fabric is not configured for this node.");
> +                        return Ok::<(), anyhow::Error>(()); 
> +                    }
> +                    Ok(())
> +                })?;
> +        }
> +        Ok(FrrConfig { router, interfaces })

same points as above basically

> +    }
> +}
> diff --git a/proxmox-frr/src/openfabric.rs b/proxmox-frr/src/openfabric.rs
> new file mode 100644
> index 000000000000..12cfc61236cb
> --- /dev/null
> +++ b/proxmox-frr/src/openfabric.rs
> @@ -0,0 +1,137 @@
> +use std::fmt::Debug;
> +use std::{fmt::Display, str::FromStr};
> +
> +use proxmox_network_types::net::Net;
> +use serde::{Deserialize, Serialize};
> +use serde_with::{DeserializeFromStr, SerializeDisplay};
> +
> +#[cfg(feature = "config-ext")]
> +use proxmox_ve_config::sdn::fabric::openfabric::{self, internal};
> +use thiserror::Error;
> +
> +use crate::common::FrrWord;
> +use crate::RouterNameError;
> +
> +#[derive(Clone, Debug, PartialEq, Eq, Hash, DeserializeFromStr, SerializeDisplay)]
> +pub struct OpenFabricRouterName(FrrWord);
> +
> +impl From<FrrWord> for OpenFabricRouterName {
> +    fn from(value: FrrWord) -> Self {
> +        Self(value)
> +    }
> +}
> +
> +impl OpenFabricRouterName {
> +    pub fn new(name: FrrWord) -> Self {
> +        Self(name)
> +    }
> +}
> +
> +impl FromStr for OpenFabricRouterName {
> +    type Err = RouterNameError;
> +
> +    fn from_str(s: &str) -> Result<Self, Self::Err> {
> +        if let Some(name) = s.strip_prefix("openfabric ") {
> +            return Ok(Self::new(
> +                FrrWord::from_str(name).map_err(|_| RouterNameError::InvalidName)?,
> +            ));
> +        }
> +
> +        Err(RouterNameError::InvalidName)
> +    }
> +}
> +
> +impl Display for OpenFabricRouterName {
> +    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
> +        write!(f, "openfabric {}", self.0)
> +    }
> +}
> +
> +#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
> +pub struct OpenFabricRouter {
> +    net: Net,
> +}
> +
> +impl OpenFabricRouter {
> +    pub fn new(net: Net) -> Self {
> +        Self {
> +            net,
> +        }
> +    }
> +
> +    pub fn net(&self) -> &Net {
> +        &self.net
> +    }
> +}
> +
> +#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
> +pub struct OpenFabricInterface {
> +    // Note: an interface can only be a part of a single fabric (so no vec needed here)
> +    fabric_id: OpenFabricRouterName,
> +    passive: Option<bool>,
> +    hello_interval: Option<openfabric::HelloInterval>,
> +    csnp_interval: Option<openfabric::CsnpInterval>,
> +    hello_multiplier: Option<openfabric::HelloMultiplier>,
> +}
> +
> +impl OpenFabricInterface {
> +    pub fn fabric_id(&self) -> &OpenFabricRouterName {
> +        &self.fabric_id
> +    }
> +    pub fn passive(&self) -> &Option<bool> {
> +        &self.passive
> +    }
> +    pub fn hello_interval(&self) -> &Option<openfabric::HelloInterval> {
> +        &self.hello_interval
> +    }
> +    pub fn csnp_interval(&self) -> &Option<openfabric::CsnpInterval> {
> +        &self.csnp_interval
> +    }
> +    pub fn hello_multiplier(&self) -> &Option<openfabric::HelloMultiplier> {
> +        &self.hello_multiplier
> +    }

If we implement Copy for those types it's usually just easier to return
them owned.

> +    pub fn set_hello_interval(&mut self, interval: Option<openfabric::HelloInterval>) {

nit: I usually like impl Into<Option<..>> because it makes the API nicer
(don't have to write Some(..) all the time ...)

> +        self.hello_interval = interval;
> +    }
> +}
> +
> +#[derive(Error, Debug)]
> +pub enum OpenFabricInterfaceError {
> +    #[error("Unknown error converting to OpenFabricInterface")]
> +    UnknownError,
> +    #[error("Error converting router name")]
> +    RouterNameError(#[from] RouterNameError),
> +}
> +
> +#[cfg(feature = "config-ext")]
> +impl TryFrom<(&internal::FabricId, &internal::Interface)> for OpenFabricInterface {
> +    type Error = OpenFabricInterfaceError;
> +
> +    fn try_from(value: (&internal::FabricId, &internal::Interface)) -> Result<Self, Self::Error> {
> +        Ok(Self {
> +            fabric_id: OpenFabricRouterName::try_from(value.0)?,
> +            passive: value.1.passive(),
> +            hello_interval: value.1.hello_interval().clone(),
> +            csnp_interval: value.1.csnp_interval().clone(),
> +            hello_multiplier: value.1.hello_multiplier().clone(),

We can easily implement Copy for those values, since they're u16.

> +        })
> +    }
> +}
> +
> +#[cfg(feature = "config-ext")]
> +impl TryFrom<&internal::FabricId> for OpenFabricRouterName {
> +    type Error = RouterNameError;
> +
> +    fn try_from(value: &internal::FabricId) -> Result<Self, Self::Error> {
> +        Ok(OpenFabricRouterName::new(FrrWord::new(value.to_string())?))
> +    }
> +}
> +
> +#[cfg(feature = "config-ext")]
> +impl From<(&internal::FabricConfig, &internal::NodeConfig)> for OpenFabricRouter {
> +    fn from(value: (&internal::FabricConfig, &internal::NodeConfig)) -> Self {
> +        Self {
> +            net: value.1.net().to_owned(),
> +        }
> +    }
> +}

We never use value.0 here, but we might if we have some global options,
right?

> diff --git a/proxmox-frr/src/ospf.rs b/proxmox-frr/src/ospf.rs
> new file mode 100644
> index 000000000000..a14ef2c55c27
> --- /dev/null
> +++ b/proxmox-frr/src/ospf.rs
> @@ -0,0 +1,148 @@
> +use std::fmt::Debug;
> +use std::net::Ipv4Addr;
> +use std::{fmt::Display, str::FromStr};
> +
> +use serde::{Deserialize, Serialize};
> +
> +#[cfg(feature = "config-ext")]
> +use proxmox_ve_config::sdn::fabric::ospf::internal;
> +use thiserror::Error;
> +
> +use crate::common::{FrrWord, FrrWordError};
> +
> +/// The name of the ospf frr router. There is only one ospf fabric possible in frr (ignoring
> +/// multiple invocations of the ospfd daemon) and the separation is done with areas. Still,
> +/// different areas have the same frr router, so the name of the router is just "ospf" in "router
> +/// ospf". This type still contains the Area so that we can insert it in the Hashmap.
> +#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
> +pub struct OspfRouterName(Area);
> +
> +impl From<Area> for OspfRouterName {
> +    fn from(value: Area) -> Self {
> +        Self(value)
> +    }
> +}
> +
> +impl Display for OspfRouterName {
> +    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
> +        write!(f, "ospf")

this is missing the area, or?

> +    }
> +}
> +
> +#[derive(Error, Debug)]
> +pub enum AreaParsingError {
> +    #[error("Invalid area idenitifier. Area must be a number or an ipv4 address.")]
> +    InvalidArea,
> +    #[error("Invalid area idenitifier. Missing 'area' prefix.")]
> +    MissingPrefix,
> +    #[error("Error parsing to FrrWord")]
> +    FrrWordError(#[from] FrrWordError),
> +}
> +
> +/// The OSPF Area. Most commonly, this is just a number, e.g. 5, but sometimes also a
> +/// pseudo-ipaddress, e.g. 0.0.0.0
> +#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
> +pub struct Area(FrrWord);
> +
> +impl TryFrom<FrrWord> for Area {
> +    type Error = AreaParsingError;
> +
> +    fn try_from(value: FrrWord) -> Result<Self, Self::Error> {
> +        Area::new(value)
> +    }
> +}
> +
> +impl Area {
> +    pub fn new(name: FrrWord) -> Result<Self, AreaParsingError> {
> +        if name.as_ref().parse::<i32>().is_ok() || name.as_ref().parse::<Ipv4Addr>().is_ok() {

use u32 here? otherwise area ID can be negative, which isn't allowed afaict?

> +            Ok(Self(name))
> +        } else {
> +            Err(AreaParsingError::InvalidArea)
> +        }
> +    }
> +}
> +
> +impl FromStr for Area {
> +    type Err = AreaParsingError;
> +
> +    fn from_str(s: &str) -> Result<Self, Self::Err> {
> +        if let Some(name) = s.strip_prefix("area ") {
> +            return Self::new(FrrWord::from_str(name).map_err(|_| AreaParsingError::InvalidArea)?);
> +        }
> +
> +        Err(AreaParsingError::MissingPrefix)
> +    }
> +}
> +
> +impl Display for Area {
> +    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
> +        write!(f, "area {}", self.0)
> +    }
> +}
> +
> +#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
> +pub struct OspfRouter {
> +    router_id: Ipv4Addr,
> +}
> +
> +impl OspfRouter {
> +    pub fn new(router_id: Ipv4Addr) -> Self {
> +        Self { router_id }
> +    }
> +
> +    pub fn router_id(&self) -> &Ipv4Addr {
> +        &self.router_id
> +    }
> +}
> +
> +#[derive(Error, Debug)]
> +pub enum OspfInterfaceParsingError {
> +    #[error("Error parsing area")]
> +    AreaParsingError(#[from] AreaParsingError)
> +}
> +
> +#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
> +pub struct OspfInterface {
> +    // Note: an interface can only be a part of a single area(so no vec needed here)
> +    area: Area,
> +    passive: Option<bool>,
> +}
> +
> +impl OspfInterface {
> +    pub fn area(&self) -> &Area {
> +        &self.area
> +    }
> +    pub fn passive(&self) -> &Option<bool> {
> +        &self.passive
> +    }
> +}
> +
> +#[cfg(feature = "config-ext")]
> +impl TryFrom<(&internal::Area, &internal::Interface)> for OspfInterface {
> +    type Error = OspfInterfaceParsingError;
> +
> +    fn try_from(value: (&internal::Area, &internal::Interface)) -> Result<Self, Self::Error> {
> +        Ok(Self {
> +            area: Area::try_from(value.0)?,
> +            passive: value.1.passive(),
> +        })
> +    }
> +}
> +
> +#[cfg(feature = "config-ext")]
> +impl TryFrom<&internal::Area> for Area {
> +    type Error = AreaParsingError;
> +
> +    fn try_from(value: &internal::Area) -> Result<Self, Self::Error> {
> +        Area::new(FrrWord::new(value.to_string())?)
> +    }
> +}
> +
> +#[cfg(feature = "config-ext")]
> +impl From<(&internal::FabricConfig, &internal::NodeConfig)> for OspfRouter {
> +    fn from(value: (&internal::FabricConfig, &internal::NodeConfig)) -> Self {
> +        Self {
> +            router_id: value.1.router_id,
> +        }
> +    }
> +}



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

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

* Re: [pve-devel] [RFC cluster/manager/network/proxmox{-ve-rs, -perl-rs} 00/11] Add SDN Fabrics
  2025-02-14 13:39 [pve-devel] [RFC cluster/manager/network/proxmox{-ve-rs, -perl-rs} 00/11] Add SDN Fabrics Gabriel Goller
                   ` (10 preceding siblings ...)
  2025-02-14 13:39 ` [pve-devel] [PATCH pve-manager 11/11] network: return loopback interface on network endpoint Gabriel Goller
@ 2025-03-03 16:58 ` Stefan Hanreich
  11 siblings, 0 replies; 32+ messages in thread
From: Stefan Hanreich @ 2025-03-03 16:58 UTC (permalink / raw)
  To: Proxmox VE development discussion, Gabriel Goller

Could you please rebase this (patch 3 in particular doesn't apply) and
make sure everything builds debian packages properly (in ve-rs via
build.sh)?

I'll continue tomorrow with the review!

On 2/14/25 14:39, Gabriel Goller wrote:
> This series allows the user to add fabrics such as OpenFabric and OSPF over
> their clusters.
> 
> Note that this is a very early RFC and its sole purpose is to get some initial
> feedback and architectural suggestions.
> 
> Overview
> --------
> Add a new section to the sdn panel in the datacenter options which allows
> creating OpenFabric and OSPF fabrics. One can add Nodes to the fabrics by
> selecting them from a dropdown which shows all the nodes in the cluster.
> Additionally the user can then select the interfaces of the node which should
> be added to the fabric. There are also protocol-specific options such as "passive",
> "hello-interval" etc. available to select on the interface.
> 
> Implementation
> --------------
> Add config files for every fabric type, so currently we add sdn/ospf.cfg and
> sdn/openfabric.cfg. These config file get read by pve-network and the raw
> section config gets passed to rust (proxmox-perl-rs), where we parse it using
> helpers and schemas from proxmox-ve-config crate (proxmox-ve-rs repo). The
> config parsed from the section-config is then converted into a better 
> representation (no PropertyString, embed Nodes into Fabrics, etc.), which is
> also guaranteed to be correct (everything is typed and the values are
> semantically checked). This intermediate representation can then be converted
> into a `FrrConfig`, which lives in the proxmox-frr crate (proxmox-ve-rs repo).
> This representation is very close to the real frr config (eg contains routers,
> interfaces, etc.) and can either be converted to a `PerlFrr` config (which will
> be used by pve-network to merge with the existin config) or (optional and
> experimental) into a string, which would be the actual frr config as it can be
> found in `frr.conf`.
> 
> This series also relies on: 
> https://lore.proxmox.com/pve-devel/20250205161340.740740-1-g.goller@proxmox.com/
> 
> Open Questions/Issues:
>  * generate openfabric net from the selected interface ip (the net is quite
>    hard to get right otherwise).
>  * Reminder to apply configuration -> Probably add a "state" column which shows
>    "new" (when not applied) like in the sdn/controllers grid.
>  * Add ability for the user to create a "standard" setup, where he can select a
>    node and we automatically add an ip address to the loopback address and add
>    the loopback interface as passive to the openfabric/ospf fabric. (Maybe we
>    are able to get frr to support dummy interfaces in the meantime, which would
>    be even better.)
>  * Check if we want continue using the pve-network perl frr merging code or if
>    we want to transition to rust -> vtysh. So the config gets flushed to the
>    daemon directly using vtysh, this allows the user to change the frr config
>    manually and their settings not getting overwritten by us (we also avoid
>    reloading the daemon).
>  * Write some extensive documentation on what the Fabrics can/cannot do.
> 
> proxmox-ve-rs:
> 
> Gabriel Goller (3):
>   add crate with common network types
>   add proxmox-frr crate with frr types
>   add intermediate fabric representation
> 
>  Cargo.toml                                    |   7 +
>  proxmox-frr/Cargo.toml                        |  25 +
>  proxmox-frr/src/common.rs                     |  54 ++
>  proxmox-frr/src/lib.rs                        | 223 ++++++++
>  proxmox-frr/src/openfabric.rs                 | 137 +++++
>  proxmox-frr/src/ospf.rs                       | 148 ++++++
>  proxmox-network-types/Cargo.toml              |  15 +
>  proxmox-network-types/src/lib.rs              |   1 +
>  proxmox-network-types/src/net.rs              | 239 +++++++++
>  proxmox-ve-config/Cargo.toml                  |  10 +-
>  proxmox-ve-config/debian/control              |   4 +-
>  proxmox-ve-config/src/sdn/fabric/common.rs    |  90 ++++
>  proxmox-ve-config/src/sdn/fabric/mod.rs       |  68 +++
>  .../src/sdn/fabric/openfabric.rs              | 494 ++++++++++++++++++
>  proxmox-ve-config/src/sdn/fabric/ospf.rs      | 375 +++++++++++++
>  proxmox-ve-config/src/sdn/mod.rs              |   1 +
>  16 files changed, 1885 insertions(+), 6 deletions(-)
>  create mode 100644 proxmox-frr/Cargo.toml
>  create mode 100644 proxmox-frr/src/common.rs
>  create mode 100644 proxmox-frr/src/lib.rs
>  create mode 100644 proxmox-frr/src/openfabric.rs
>  create mode 100644 proxmox-frr/src/ospf.rs
>  create mode 100644 proxmox-network-types/Cargo.toml
>  create mode 100644 proxmox-network-types/src/lib.rs
>  create mode 100644 proxmox-network-types/src/net.rs
>  create mode 100644 proxmox-ve-config/src/sdn/fabric/common.rs
>  create mode 100644 proxmox-ve-config/src/sdn/fabric/mod.rs
>  create mode 100644 proxmox-ve-config/src/sdn/fabric/openfabric.rs
>  create mode 100644 proxmox-ve-config/src/sdn/fabric/ospf.rs
> 
> 
> proxmox-perl-rs:
> 
> Gabriel Goller (1):
>   fabrics: add CRUD and generate fabrics methods
> 
>  pve-rs/Cargo.toml            |   5 +-
>  pve-rs/Makefile              |   3 +
>  pve-rs/src/lib.rs            |   1 +
>  pve-rs/src/sdn/fabrics.rs    | 202 ++++++++++++++++
>  pve-rs/src/sdn/mod.rs        |   3 +
>  pve-rs/src/sdn/openfabric.rs | 454 +++++++++++++++++++++++++++++++++++
>  pve-rs/src/sdn/ospf.rs       | 425 ++++++++++++++++++++++++++++++++
>  7 files changed, 1092 insertions(+), 1 deletion(-)
>  create mode 100644 pve-rs/src/sdn/fabrics.rs
>  create mode 100644 pve-rs/src/sdn/mod.rs
>  create mode 100644 pve-rs/src/sdn/openfabric.rs
>  create mode 100644 pve-rs/src/sdn/ospf.rs
> 
> 
> pve-cluster:
> 
> Gabriel Goller (1):
>   cluster: add sdn fabrics config files
> 
>  src/PVE/Cluster.pm | 2 ++
>  1 file changed, 2 insertions(+)
> 
> 
> pve-network:
> 
> Gabriel Goller (3):
>   add config file and common read/write methods
>   merge the frr config with the fabrics frr config on apply
>   add api endpoints for fabrics
> 
>  src/PVE/API2/Network/SDN.pm                   |   7 +
>  src/PVE/API2/Network/SDN/Fabrics.pm           |  57 +++
>  src/PVE/API2/Network/SDN/Fabrics/Common.pm    | 111 +++++
>  src/PVE/API2/Network/SDN/Fabrics/Makefile     |   9 +
>  .../API2/Network/SDN/Fabrics/OpenFabric.pm    | 460 ++++++++++++++++++
>  src/PVE/API2/Network/SDN/Fabrics/Ospf.pm      | 433 +++++++++++++++++
>  src/PVE/API2/Network/SDN/Makefile             |   3 +-
>  src/PVE/Network/SDN.pm                        |   8 +-
>  src/PVE/Network/SDN/Controllers.pm            |   1 -
>  src/PVE/Network/SDN/Controllers/EvpnPlugin.pm |   3 -
>  src/PVE/Network/SDN/Controllers/Frr.pm        |  13 +
>  src/PVE/Network/SDN/Fabrics.pm                |  86 ++++
>  src/PVE/Network/SDN/Makefile                  |   2 +-
>  13 files changed, 1186 insertions(+), 7 deletions(-)
>  create mode 100644 src/PVE/API2/Network/SDN/Fabrics.pm
>  create mode 100644 src/PVE/API2/Network/SDN/Fabrics/Common.pm
>  create mode 100644 src/PVE/API2/Network/SDN/Fabrics/Makefile
>  create mode 100644 src/PVE/API2/Network/SDN/Fabrics/OpenFabric.pm
>  create mode 100644 src/PVE/API2/Network/SDN/Fabrics/Ospf.pm
>  create mode 100644 src/PVE/Network/SDN/Fabrics.pm
> 
> 
> pve-manager:
> 
> Gabriel Goller (3):
>   sdn: add Fabrics view
>   sdn: add fabric edit/delete forms
>   network: return loopback interface on network endpoint
> 
>  PVE/API2/Cluster.pm                           |   7 +-
>  PVE/API2/Network.pm                           |   9 +-
>  www/manager6/.lint-incremental                |   0
>  www/manager6/Makefile                         |   8 +
>  www/manager6/dc/Config.js                     |   8 +
>  www/manager6/sdn/FabricsView.js               | 359 ++++++++++++++++++
>  www/manager6/sdn/fabrics/Common.js            | 222 +++++++++++
>  .../sdn/fabrics/openfabric/FabricEdit.js      |  67 ++++
>  .../sdn/fabrics/openfabric/InterfaceEdit.js   |  92 +++++
>  .../sdn/fabrics/openfabric/NodeEdit.js        | 187 +++++++++
>  www/manager6/sdn/fabrics/ospf/FabricEdit.js   |  60 +++
>  .../sdn/fabrics/ospf/InterfaceEdit.js         |  46 +++
>  www/manager6/sdn/fabrics/ospf/NodeEdit.js     | 191 ++++++++++
>  13 files changed, 1244 insertions(+), 12 deletions(-)
>  create mode 100644 www/manager6/.lint-incremental
>  create mode 100644 www/manager6/sdn/FabricsView.js
>  create mode 100644 www/manager6/sdn/fabrics/Common.js
>  create mode 100644 www/manager6/sdn/fabrics/openfabric/FabricEdit.js
>  create mode 100644 www/manager6/sdn/fabrics/openfabric/InterfaceEdit.js
>  create mode 100644 www/manager6/sdn/fabrics/openfabric/NodeEdit.js
>  create mode 100644 www/manager6/sdn/fabrics/ospf/FabricEdit.js
>  create mode 100644 www/manager6/sdn/fabrics/ospf/InterfaceEdit.js
>  create mode 100644 www/manager6/sdn/fabrics/ospf/NodeEdit.js
> 
> 
> Summary over all repositories:
>   50 files changed, 5409 insertions(+), 26 deletions(-)
> 



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


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

* Re: [pve-devel] [PATCH proxmox-ve-rs 03/11] add intermediate fabric representation
  2025-02-14 13:39 ` [pve-devel] [PATCH proxmox-ve-rs 03/11] add intermediate fabric representation Gabriel Goller
  2025-02-28 13:57   ` Thomas Lamprecht
@ 2025-03-04  8:45   ` Stefan Hanreich
  2025-03-05  9:09     ` Gabriel Goller
  1 sibling, 1 reply; 32+ messages in thread
From: Stefan Hanreich @ 2025-03-04  8:45 UTC (permalink / raw)
  To: Gabriel Goller, pve-devel



On 2/14/25 14:39, Gabriel Goller wrote:
> This adds the intermediate, type-checked fabrics config. This one is
> parsed from the SectionConfig and can be converted into the
> Frr-Representation.
> 
> Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
> Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
> ---
>  proxmox-ve-config/Cargo.toml                  |  10 +-
>  proxmox-ve-config/debian/control              |   4 +-
>  proxmox-ve-config/src/sdn/fabric/common.rs    |  90 ++++
>  proxmox-ve-config/src/sdn/fabric/mod.rs       |  68 +++
>  .../src/sdn/fabric/openfabric.rs              | 494 ++++++++++++++++++
>  proxmox-ve-config/src/sdn/fabric/ospf.rs      | 375 +++++++++++++
>  proxmox-ve-config/src/sdn/mod.rs              |   1 +
>  7 files changed, 1036 insertions(+), 6 deletions(-)
>  create mode 100644 proxmox-ve-config/src/sdn/fabric/common.rs
>  create mode 100644 proxmox-ve-config/src/sdn/fabric/mod.rs
>  create mode 100644 proxmox-ve-config/src/sdn/fabric/openfabric.rs
>  create mode 100644 proxmox-ve-config/src/sdn/fabric/ospf.rs
> 
> diff --git a/proxmox-ve-config/Cargo.toml b/proxmox-ve-config/Cargo.toml
> index 0c8f6166e75d..3a0fc9fa6618 100644
> --- a/proxmox-ve-config/Cargo.toml
> +++ b/proxmox-ve-config/Cargo.toml
> @@ -10,13 +10,15 @@ exclude.workspace = true
>  log = "0.4"
>  anyhow = "1"
>  nix = "0.26"
> -thiserror = "1.0.59"
> +thiserror = { workspace = true }
>  
> -serde = { version = "1", features = [ "derive" ] }
> +serde = { workspace = true, features = [ "derive" ] }
> +serde_with = { workspace = true }
>  serde_json = "1"
>  serde_plain = "1"
> -serde_with = "3"
>  
> -proxmox-schema = "3.1.2"
> +proxmox-section-config = { workspace = true }
> +proxmox-schema = "4.0.0"
>  proxmox-sys = "0.6.4"
>  proxmox-sortable-macro = "0.1.3"
> +proxmox-network-types = { path = "../proxmox-network-types/" }
> diff --git a/proxmox-ve-config/debian/control b/proxmox-ve-config/debian/control
> index 24814c11b471..bff03afba747 100644
> --- a/proxmox-ve-config/debian/control
> +++ b/proxmox-ve-config/debian/control
> @@ -9,7 +9,7 @@ Build-Depends: cargo:native,
>                 librust-log-0.4+default-dev (>= 0.4.17-~~),
>                 librust-nix-0.26+default-dev (>= 0.26.1-~~),
>                 librust-thiserror-dev (>= 1.0.59-~~),
> -               librust-proxmox-schema-3+default-dev,
> +               librust-proxmox-schema-4+default-dev,
>                 librust-proxmox-sortable-macro-dev,
>                 librust-proxmox-sys-dev,
>                 librust-serde-1+default-dev,
> @@ -33,7 +33,7 @@ Depends:
>   librust-log-0.4+default-dev (>= 0.4.17-~~),
>   librust-nix-0.26+default-dev (>= 0.26.1-~~),
>   librust-thiserror-dev (>= 1.0.59-~~),
> - librust-proxmox-schema-3+default-dev,
> + librust-proxmox-schema-4+default-dev,
>   librust-proxmox-sortable-macro-dev,
>   librust-proxmox-sys-dev,
>   librust-serde-1+default-dev,
> diff --git a/proxmox-ve-config/src/sdn/fabric/common.rs b/proxmox-ve-config/src/sdn/fabric/common.rs
> new file mode 100644
> index 000000000000..400f5b6d6b12
> --- /dev/null
> +++ b/proxmox-ve-config/src/sdn/fabric/common.rs
> @@ -0,0 +1,90 @@
> +use serde::{Deserialize, Serialize};
> +use std::fmt::Display;
> +use thiserror::Error;
> +
> +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Error)]
> +pub enum ConfigError {
> +    #[error("node id has invalid format")]
> +    InvalidNodeId,
> +}
> +
> +#[derive(Debug, Deserialize, Serialize, Clone, Eq, Hash, PartialOrd, Ord, PartialEq)]
> +pub struct Hostname(String);
> +
> +impl From<String> for Hostname {
> +    fn from(value: String) -> Self {
> +        Hostname::new(value)
> +    }
> +}
> +
> +impl AsRef<str> for Hostname {
> +    fn as_ref(&self) -> &str {
> +        &self.0
> +    }
> +}
> +
> +impl Display for Hostname {
> +    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
> +        self.0.fmt(f)
> +    }
> +}
> +
> +impl Hostname {
> +    pub fn new(name: impl Into<String>) -> Hostname {
> +        Self(name.into())
> +    }
> +}
> +
> +// parses a bool from a string OR bool
> +pub mod serde_option_bool {
> +    use std::fmt;
> +
> +    use serde::{
> +        de::{Deserializer, Error, Visitor}, ser::Serializer
> +    };
> +
> +    use crate::firewall::parse::parse_bool;
> +
> +    pub fn deserialize<'de, D: Deserializer<'de>>(
> +        deserializer: D,
> +    ) -> Result<Option<bool>, D::Error> {
> +        struct V;
> +
> +        impl<'de> Visitor<'de> for V {
> +            type Value = Option<bool>;
> +
> +            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
> +                f.write_str("a boolean-like value")
> +            }
> +
> +            fn visit_bool<E: Error>(self, v: bool) -> Result<Self::Value, E> {
> +                Ok(Some(v))
> +            }
> +
> +            fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
> +                parse_bool(v).map_err(E::custom).map(Some)
> +            }
> +
> +            fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
> +                Ok(None)
> +            }
> +
> +            fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
> +            where
> +                D: Deserializer<'de>,
> +            {
> +                deserializer.deserialize_any(self)
> +            }
> +        }
> +
> +        deserializer.deserialize_any(V)
> +    }
> +
> +    pub fn serialize<S: Serializer>(from: &Option<bool>, serializer: S) -> Result<S::Ok, S::Error> {
> +        if *from == Some(true) {
> +            serializer.serialize_str("1")
> +        } else {
> +            serializer.serialize_str("0")
> +        }
> +    }
> +}
> diff --git a/proxmox-ve-config/src/sdn/fabric/mod.rs b/proxmox-ve-config/src/sdn/fabric/mod.rs
> new file mode 100644
> index 000000000000..6453fb9bb98f
> --- /dev/null
> +++ b/proxmox-ve-config/src/sdn/fabric/mod.rs
> @@ -0,0 +1,68 @@
> +pub mod common;
> +pub mod openfabric;
> +pub mod ospf;
> +
> +use proxmox_section_config::typed::ApiSectionDataEntry;
> +use proxmox_section_config::typed::SectionConfigData;
> +use serde::de::DeserializeOwned;
> +use serde::Deserialize;
> +use serde::Serialize;
> +
> +#[derive(Serialize, Deserialize, Debug, Default)]
> +pub struct FabricConfig {
> +    openfabric: Option<openfabric::internal::OpenFabricConfig>,
> +    ospf: Option<ospf::internal::OspfConfig>,
> +}
> +
> +impl FabricConfig {
> +    pub fn new(raw_openfabric: &str, raw_ospf: &str) -> Result<Self, anyhow::Error> {
> +        let openfabric =
> +            openfabric::internal::OpenFabricConfig::default(raw_openfabric)?;
> +        let ospf = ospf::internal::OspfConfig::default(raw_ospf)?;

Maybe rename the two methods to new, since default usually has no
arguments and this kinda breaks with this convention?

> +        Ok(Self {
> +            openfabric: Some(openfabric),
> +            ospf: Some(ospf),
> +        })
> +    }
> +
> +    pub fn openfabric(&self) -> &Option<openfabric::internal::OpenFabricConfig>{
> +        &self.openfabric
> +    }
> +    pub fn ospf(&self) -> &Option<ospf::internal::OspfConfig>{
> +        &self.ospf
> +    }
> +
> +    pub fn with_openfabric(config: openfabric::internal::OpenFabricConfig) -> FabricConfig {
> +        Self {
> +            openfabric: Some(config),
> +            ospf: None,
> +        }
> +    }
> +
> +    pub fn with_ospf(config: ospf::internal::OspfConfig) -> FabricConfig {
> +        Self {
> +            ospf: Some(config),
> +            openfabric: None,
> +        }
> +    }
> +}
> +
> +pub trait FromSectionConfig
> +where
> +    Self: Sized + TryFrom<SectionConfigData<Self::Section>>,
> +    <Self as TryFrom<SectionConfigData<Self::Section>>>::Error: std::fmt::Debug,
> +{
> +    type Section: ApiSectionDataEntry + DeserializeOwned;
> +
> +    fn from_section_config(raw: &str) -> Result<Self, anyhow::Error> {
> +        let section_config_data = Self::Section::section_config()
> +            .parse(Self::filename(), raw)?
> +            .try_into()?;
> +
> +        let output = Self::try_from(section_config_data).unwrap();
> +        Ok(output)
> +    }
> +
> +    fn filename() -> String;
> +}
> diff --git a/proxmox-ve-config/src/sdn/fabric/openfabric.rs b/proxmox-ve-config/src/sdn/fabric/openfabric.rs
> new file mode 100644
> index 000000000000..531610f7d7e9
> --- /dev/null
> +++ b/proxmox-ve-config/src/sdn/fabric/openfabric.rs
> @@ -0,0 +1,494 @@
> +use proxmox_network_types::net::Net;
> +use proxmox_schema::property_string::PropertyString;
> +use proxmox_sortable_macro::sortable;
> +use std::{fmt::Display, num::ParseIntError, sync::OnceLock};
> +
> +use crate::sdn::fabric::common::serde_option_bool;
> +use internal::OpenFabricConfig;
> +use proxmox_schema::{
> +    ApiStringFormat, ApiType, ArraySchema, BooleanSchema, IntegerSchema, ObjectSchema, Schema,
> +    StringSchema,
> +};
> +use proxmox_section_config::{typed::ApiSectionDataEntry, SectionConfig, SectionConfigPlugin};
> +use serde::{Deserialize, Serialize};
> +use thiserror::Error;
> +
> +use super::FromSectionConfig;
> +
> +#[sortable]
> +const FABRIC_SCHEMA: ObjectSchema = ObjectSchema::new(
> +    "fabric schema",
> +    &sorted!([(
> +        "hello_interval",
> +        true,
> +        &IntegerSchema::new("OpenFabric hello_interval in seconds")
> +            .minimum(1)
> +            .maximum(600)
> +            .schema(),
> +    ),]),
> +);
> +
> +#[sortable]
> +const INTERFACE_SCHEMA: Schema = ObjectSchema::new(
> +    "interface",
> +    &sorted!([
> +        (
> +            "hello_interval",
> +            true,
> +            &IntegerSchema::new("OpenFabric Hello interval in seconds")
> +                .minimum(1)
> +                .maximum(600)
> +                .schema(),
> +        ),
> +        (
> +            "name",
> +            false,
> +            &StringSchema::new("Interface name")
> +                .min_length(1)
> +                .max_length(15)
> +                .schema(),
> +        ),
> +        (
> +            "passive",
> +            true,
> +            &BooleanSchema::new("OpenFabric passive mode for this interface").schema(),
> +        ),
> +        (
> +            "csnp_interval",
> +            true,
> +            &IntegerSchema::new("OpenFabric csnp interval in seconds")
> +                .minimum(1)
> +                .maximum(600)
> +                .schema()
> +        ),
> +        (
> +            "hello_multiplier",
> +            true,
> +            &IntegerSchema::new("OpenFabric multiplier for Hello holding time")
> +                .minimum(2)
> +                .maximum(100)
> +                .schema()
> +        ),
> +    ]),
> +)
> +.schema();
> +
> +#[sortable]
> +const NODE_SCHEMA: ObjectSchema = ObjectSchema::new(
> +    "node schema",
> +    &sorted!([
> +        (
> +            "interface",
> +            false,
> +            &ArraySchema::new(
> +                "OpenFabric name",
> +                &StringSchema::new("OpenFabric Interface")
> +                    .format(&ApiStringFormat::PropertyString(&INTERFACE_SCHEMA))
> +                    .schema(),
> +            )
> +            .schema(),
> +        ),
> +        (
> +            "net",
> +            true,
> +            &StringSchema::new("OpenFabric net").min_length(3).schema(),
> +        ),
> +    ]),
> +);
> +
> +const ID_SCHEMA: Schema = StringSchema::new("id").min_length(2).schema();
> +
> +#[derive(Error, Debug)]
> +pub enum IntegerRangeError {
> +    #[error("The value must be between {min} and {max} seconds")]
> +    OutOfRange { min: i32, max: i32 },
> +    #[error("Error parsing to number")]
> +    ParsingError(#[from] ParseIntError),
> +}
> +
> +#[derive(Serialize, Deserialize, Hash, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]

derive Copy for ergonomics

> +pub struct CsnpInterval(u16);
> +
> +impl TryFrom<u16> for CsnpInterval {
> +    type Error = IntegerRangeError;
> +
> +    fn try_from(number: u16) -> Result<Self, Self::Error> {
> +        if (1..=600).contains(&number) {
> +            Ok(CsnpInterval(number))
> +        } else {
> +            Err(IntegerRangeError::OutOfRange { min: 1, max: 600 })
> +        }
> +    }
> +}
> +
> +impl Display for CsnpInterval {
> +    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
> +        self.0.fmt(f)
> +    }
> +}
> +
> +#[derive(Serialize, Deserialize, Hash, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]

derive Copy for ergonomics

> +pub struct HelloInterval(u16);
> +
> +impl TryFrom<u16> for HelloInterval {
> +    type Error = IntegerRangeError;
> +
> +    fn try_from(number: u16) -> Result<Self, Self::Error> {
> +        if (1..=600).contains(&number) {
> +            Ok(HelloInterval(number))
> +        } else {
> +            Err(IntegerRangeError::OutOfRange { min: 1, max: 600 })
> +        }
> +    }
> +}
> +
> +impl Display for HelloInterval {
> +    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
> +        self.0.fmt(f)
> +    }
> +}
> +
> +#[derive(Serialize, Deserialize, Hash, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]

derive Copy for ergonomics

> +pub struct HelloMultiplier(u16);
> +
> +impl TryFrom<u16> for HelloMultiplier {
> +    type Error = IntegerRangeError;
> +
> +    fn try_from(number: u16) -> Result<Self, Self::Error> {
> +        if (2..=100).contains(&number) {
> +            Ok(HelloMultiplier(number))
> +        } else {
> +            Err(IntegerRangeError::OutOfRange { min: 2, max: 100 })
> +        }
> +    }
> +}
> +
> +impl Display for HelloMultiplier {
> +    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
> +        self.0.fmt(f)
> +    }
> +}
> +
> +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
> +pub struct FabricSection {
> +    #[serde(skip_serializing_if = "Option::is_none")]
> +    pub hello_interval: Option<HelloInterval>,
> +}
> +
> +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
> +pub struct NodeSection {
> +    pub net: Net,
> +    pub interface: Vec<PropertyString<InterfaceProperties>>,
> +}
> +
> +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
> +pub struct InterfaceProperties {
> +    pub name: String,
> +    #[serde(skip_serializing_if = "Option::is_none")]
> +    #[serde(default, with = "serde_option_bool")]
> +    pub passive: Option<bool>,
> +    #[serde(skip_serializing_if = "Option::is_none")]
> +    pub hello_interval: Option<HelloInterval>,
> +    #[serde(skip_serializing_if = "Option::is_none")]
> +    pub csnp_interval: Option<CsnpInterval>,
> +    #[serde(skip_serializing_if = "Option::is_none")]
> +    pub hello_multiplier: Option<HelloMultiplier>,
> +}
> +
> +impl InterfaceProperties {
> +    pub fn passive(&self) -> Option<bool> {
> +        self.passive
> +    }
> +}
> +
> +impl ApiType for InterfaceProperties {
> +    const API_SCHEMA: Schema = INTERFACE_SCHEMA;
> +}
> +
> +#[derive(Serialize, Deserialize, Debug, Clone)]
> +pub enum OpenFabricSectionConfig {
> +    #[serde(rename = "fabric")]
> +    Fabric(FabricSection),
> +    #[serde(rename = "node")]
> +    Node(NodeSection),
> +}
> +
> +impl ApiSectionDataEntry for OpenFabricSectionConfig {
> +    const INTERNALLY_TAGGED: Option<&'static str> = None;
> +
> +    fn section_config() -> &'static SectionConfig {
> +        static SC: OnceLock<SectionConfig> = OnceLock::new();
> +
> +        SC.get_or_init(|| {
> +            let mut config = SectionConfig::new(&ID_SCHEMA);
> +
> +            let fabric_plugin =
> +                SectionConfigPlugin::new("fabric".to_string(), None, &FABRIC_SCHEMA);
> +            config.register_plugin(fabric_plugin);
> +
> +            let node_plugin = SectionConfigPlugin::new("node".to_string(), None, &NODE_SCHEMA);
> +            config.register_plugin(node_plugin);
> +
> +            config
> +        })
> +    }
> +
> +    fn section_type(&self) -> &'static str {
> +        match self {
> +            Self::Node(_) => "node",
> +            Self::Fabric(_) => "fabric",
> +        }
> +    }
> +}
> +
> +pub mod internal {
> +    use std::{collections::HashMap, fmt::Display, str::FromStr};
> +
> +    use proxmox_network_types::net::Net;
> +    use serde::{Deserialize, Serialize};
> +    use thiserror::Error;
> +
> +    use proxmox_section_config::typed::SectionConfigData;
> +
> +    use crate::sdn::fabric::common::{self, ConfigError, Hostname};
> +
> +    use super::{
> +        CsnpInterval, FabricSection, FromSectionConfig, HelloInterval, HelloMultiplier,
> +        InterfaceProperties, NodeSection, OpenFabricSectionConfig,
> +    };
> +
> +    #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
> +    pub struct FabricId(String);
> +
> +    impl FabricId {
> +        pub fn new(id: impl Into<String>) -> Result<Self, anyhow::Error> {
> +            Ok(Self(id.into()))
> +        }
> +    }
> +
> +    impl AsRef<str> for FabricId {
> +        fn as_ref(&self) -> &str {
> +            &self.0
> +        }
> +    }
> +
> +    impl FromStr for FabricId {
> +        type Err = anyhow::Error;
> +
> +        fn from_str(s: &str) -> Result<Self, Self::Err> {
> +            Self::new(s)
> +        }
> +    }
> +
> +    impl From<String> for FabricId {
> +        fn from(value: String) -> Self {
> +            FabricId(value)
> +        }
> +    }
> +
> +    impl Display for FabricId {
> +        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
> +            self.0.fmt(f)
> +        }
> +    }
> +
> +    /// The NodeId comprises node and fabric information.
> +    ///
> +    /// It has a format of "{fabric}_{node}". This is because the node alone doesn't suffice, we need
> +    /// to store the fabric as well (a node can be apart of multiple fabrics).
> +    #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
> +    pub struct NodeId {
> +        pub fabric: FabricId,
> +        pub node: Hostname,
> +    }
> +
> +    impl NodeId {
> +        pub fn new(fabric: impl Into<FabricId>, node: impl Into<Hostname>) -> NodeId {
> +            Self {
> +                fabric: fabric.into(),
> +                node: node.into(),
> +            }
> +        }
> +    }
> +
> +    impl Display for NodeId {
> +        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
> +            write!(f, "{}_{}", self.fabric, self.node)
> +        }
> +    }
> +
> +    impl FromStr for NodeId {
> +        type Err = ConfigError;
> +
> +        fn from_str(s: &str) -> Result<Self, Self::Err> {
> +            if let Some((fabric_id, node_id)) = s.split_once('_') {
> +                return Ok(Self::new(fabric_id.to_string(), node_id.to_string()));
> +            }
> +
> +            Err(ConfigError::InvalidNodeId)
> +        }
> +    }
> +
> +    #[derive(Debug, Deserialize, Serialize)]
> +    pub struct OpenFabricConfig {
> +        fabrics: HashMap<FabricId, FabricConfig>,
> +    }
> +
> +    impl OpenFabricConfig {
> +        pub fn fabrics(&self) -> &HashMap<FabricId, FabricConfig> {
> +            &self.fabrics
> +        }
> +    }
> +
> +    #[derive(Debug, Deserialize, Serialize)]
> +    pub struct FabricConfig {
> +        nodes: HashMap<Hostname, NodeConfig>,
> +        hello_interval: Option<HelloInterval>,
> +    }
> +
> +    impl FabricConfig {
> +        pub fn nodes(&self) -> &HashMap<Hostname, NodeConfig> {
> +            &self.nodes
> +        }
> +        pub fn hello_interval(&self) -> &Option<HelloInterval> {
> +            &self.hello_interval
> +        }
> +    }
> +
> +    impl TryFrom<FabricSection> for FabricConfig {
> +        type Error = OpenFabricConfigError;
> +
> +        fn try_from(value: FabricSection) -> Result<Self, Self::Error> {
> +            Ok(FabricConfig {
> +                nodes: HashMap::new(),
> +                hello_interval: value.hello_interval,
> +            })
> +        }
> +    }
> +
> +    #[derive(Debug, Deserialize, Serialize)]
> +    pub struct NodeConfig {
> +        net: Net,
> +        interfaces: Vec<Interface>,
> +    }
> +
> +    impl NodeConfig {
> +        pub fn net(&self) -> &Net {
> +            &self.net
> +        }
> +        pub fn interfaces(&self) -> impl Iterator<Item = &Interface> + '_ {
> +            self.interfaces.iter()
> +        }
> +    }
> +
> +    impl TryFrom<NodeSection> for NodeConfig {
> +        type Error = OpenFabricConfigError;
> +
> +        fn try_from(value: NodeSection) -> Result<Self, Self::Error> {
> +            Ok(NodeConfig {
> +                net: value.net,
> +                interfaces: value
> +                    .interface
> +                    .into_iter()
> +                    .map(|i| Interface::try_from(i.into_inner()).unwrap())
> +                    .collect(),
> +            })
> +        }
> +    }
> +
> +    #[derive(Debug, Deserialize, Serialize)]
> +    pub struct Interface {
> +        name: String,
> +        passive: Option<bool>,
> +        hello_interval: Option<HelloInterval>,
> +        csnp_interval: Option<CsnpInterval>,
> +        hello_multiplier: Option<HelloMultiplier>,
> +    }
> +
> +    impl Interface {
> +        pub fn name(&self) -> &str {
> +            &self.name
> +        }
> +        pub fn passive(&self) -> Option<bool> {
> +            self.passive
> +        }
> +        pub fn hello_interval(&self) -> &Option<HelloInterval> {
> +            &self.hello_interval
> +        }
> +        pub fn csnp_interval(&self) -> &Option<CsnpInterval> {
> +            &self.csnp_interval
> +        }
> +        pub fn hello_multiplier(&self) -> &Option<HelloMultiplier> {
> +            &self.hello_multiplier
> +        }
> +    }
> +
> +    impl TryFrom<InterfaceProperties> for Interface {
> +        type Error = OpenFabricConfigError;
> +
> +        fn try_from(value: InterfaceProperties) -> Result<Self, Self::Error> {
> +            Ok(Interface {
> +                name: value.name.clone(),
> +                passive: value.passive(),
> +                hello_interval: value.hello_interval,
> +                csnp_interval: value.csnp_interval,
> +                hello_multiplier: value.hello_multiplier,
> +            })
> +        }
> +    }

are we anticipating this to be fallible in the future?

> +    #[derive(Error, Debug)]
> +    pub enum OpenFabricConfigError {
> +        #[error("Unknown error occured")]
> +        Unknown,
> +        #[error("NodeId parse error")]
> +        NodeIdError(#[from] common::ConfigError),
> +        #[error("Corresponding fabric to the node not found")]
> +        FabricNotFound,
> +    }
> +
> +    impl TryFrom<SectionConfigData<OpenFabricSectionConfig>> for OpenFabricConfig {
> +        type Error = OpenFabricConfigError;
> +
> +        fn try_from(
> +            value: SectionConfigData<OpenFabricSectionConfig>,
> +        ) -> Result<Self, Self::Error> {
> +            let mut fabrics = HashMap::new();
> +            let mut nodes = HashMap::new();
> +
> +            for (id, config) in value {
> +                match config {
> +                    OpenFabricSectionConfig::Fabric(fabric_section) => {
> +                        fabrics.insert(FabricId::from(id), FabricConfig::try_from(fabric_section)?);
> +                    }
> +                    OpenFabricSectionConfig::Node(node_section) => {
> +                        nodes.insert(id.parse::<NodeId>()?, NodeConfig::try_from(node_section)?);
> +                    }
> +                }
> +            }
> +
> +            for (id, node) in nodes {
> +                let fabric = fabrics
> +                    .get_mut(&id.fabric)
> +                    .ok_or(OpenFabricConfigError::FabricNotFound)?;
> +
> +                fabric.nodes.insert(id.node, node);
> +            }
> +            Ok(OpenFabricConfig { fabrics })
> +        }
> +    }
> +
> +    impl OpenFabricConfig {
> +        pub fn default(raw: &str) -> Result<Self, anyhow::Error> {
> +            OpenFabricConfig::from_section_config(raw)
> +        }
> +    }
> +}
> +
> +impl FromSectionConfig for OpenFabricConfig {
> +    type Section = OpenFabricSectionConfig;
> +
> +    fn filename() -> String {
> +        "ospf.cfg".to_owned()
> +    }
> +}
> diff --git a/proxmox-ve-config/src/sdn/fabric/ospf.rs b/proxmox-ve-config/src/sdn/fabric/ospf.rs
> new file mode 100644
> index 000000000000..2f2720a5759f
> --- /dev/null
> +++ b/proxmox-ve-config/src/sdn/fabric/ospf.rs
> @@ -0,0 +1,375 @@
> +use internal::OspfConfig;
> +use proxmox_schema::property_string::PropertyString;
> +use proxmox_schema::ObjectSchema;
> +use proxmox_schema::{ApiStringFormat, ApiType, ArraySchema, BooleanSchema, Schema, StringSchema};
> +use proxmox_section_config::{typed::ApiSectionDataEntry, SectionConfig, SectionConfigPlugin};
> +use proxmox_sortable_macro::sortable;
> +use serde::{Deserialize, Serialize};
> +use std::sync::OnceLock;
> +
> +use super::FromSectionConfig;
> +
> +#[sortable]
> +const FABRIC_SCHEMA: ObjectSchema = ObjectSchema::new(
> +    "fabric schema",
> +    &sorted!([(
> +        "area",
> +        true,
> +        &StringSchema::new("Area identifier").min_length(1).schema()
> +    )]),
> +);
> +
> +#[sortable]
> +const INTERFACE_SCHEMA: Schema = ObjectSchema::new(
> +    "interface",
> +    &sorted!([
> +        (
> +            "name",
> +            false,
> +            &StringSchema::new("Interface name")
> +                .min_length(1)
> +                .max_length(15)
> +                .schema(),
> +        ),
> +        (
> +            "passive",
> +            true,
> +            &BooleanSchema::new("passive interface").schema(),
> +        ),
> +    ]),
> +)
> +.schema();
> +
> +#[sortable]
> +const NODE_SCHEMA: ObjectSchema = ObjectSchema::new(
> +    "node schema",
> +    &sorted!([
> +        (
> +            "interface",
> +            false,
> +            &ArraySchema::new(
> +                "OSPF name",
> +                &StringSchema::new("OSPF Interface")
> +                    .format(&ApiStringFormat::PropertyString(&INTERFACE_SCHEMA))
> +                    .schema(),
> +            )
> +            .schema(),
> +        ),
> +        (
> +            "router_id",
> +            true,
> +            &StringSchema::new("OSPF router id").min_length(3).schema(),
> +        ),
> +    ]),
> +);
> +
> +const ID_SCHEMA: Schema = StringSchema::new("id").min_length(2).schema();
> +
> +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
> +pub struct InterfaceProperties {
> +    pub name: String,
> +    #[serde(skip_serializing_if = "Option::is_none")]
> +    pub passive: Option<bool>,
> +}
> +
> +impl ApiType for InterfaceProperties {
> +    const API_SCHEMA: Schema = INTERFACE_SCHEMA;
> +}
> +
> +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
> +pub struct NodeSection {
> +    pub router_id: String,
> +    pub interface: Vec<PropertyString<InterfaceProperties>>,
> +}
> +
> +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
> +pub struct FabricSection {}
> +
> +#[derive(Serialize, Deserialize, Debug, Clone)]
> +pub enum OspfSectionConfig {
> +    #[serde(rename = "fabric")]
> +    Fabric(FabricSection),
> +    #[serde(rename = "node")]
> +    Node(NodeSection),
> +}
> +
> +impl ApiSectionDataEntry for OspfSectionConfig {
> +    const INTERNALLY_TAGGED: Option<&'static str> = None;
> +
> +    fn section_config() -> &'static SectionConfig {
> +        static SC: OnceLock<SectionConfig> = OnceLock::new();
> +
> +        SC.get_or_init(|| {
> +            let mut config = SectionConfig::new(&ID_SCHEMA);
> +
> +            let fabric_plugin = SectionConfigPlugin::new(
> +                "fabric".to_string(),
> +                Some("area".to_string()),
> +                &FABRIC_SCHEMA,
> +            );
> +            config.register_plugin(fabric_plugin);
> +
> +            let node_plugin = SectionConfigPlugin::new("node".to_string(), None, &NODE_SCHEMA);
> +            config.register_plugin(node_plugin);
> +
> +            config
> +        })
> +    }
> +
> +    fn section_type(&self) -> &'static str {
> +        match self {
> +            Self::Node(_) => "node",
> +            Self::Fabric(_) => "fabric",
> +        }
> +    }
> +}
> +
> +pub mod internal {
> +    use std::{
> +        collections::HashMap,
> +        fmt::Display,
> +        net::{AddrParseError, Ipv4Addr},
> +        str::FromStr,
> +    };
> +
> +    use serde::{Deserialize, Serialize};
> +    use thiserror::Error;
> +
> +    use proxmox_section_config::typed::SectionConfigData;
> +
> +    use crate::sdn::fabric::{common::Hostname, FromSectionConfig};
> +
> +    use super::{FabricSection, InterfaceProperties, NodeSection, OspfSectionConfig};
> +
> +    #[derive(Error, Debug)]
> +    pub enum NodeIdError {
> +        #[error("Invalid area identifier")]
> +        InvalidArea(#[from] AreaParsingError),
> +        #[error("Invalid node identifier")]
> +        InvalidNodeId,
> +    }
> +
> +    /// The NodeId comprises node and fabric(area) information.
> +    ///
> +    /// It has a format of "{area}_{node}". This is because the node alone doesn't suffice, we need
> +    /// to store the fabric as well (a node can be apart of multiple fabrics).
> +    #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
> +    pub struct NodeId {
> +        pub area: Area,
> +        pub node: Hostname,
> +    }
> +
> +    impl NodeId {
> +        pub fn new(fabric: String, node: String) -> Result<NodeId, NodeIdError> {
> +            Ok(Self {
> +                area: fabric.try_into()?,
> +                node: node.into(),
> +            })
> +        }
> +    }
> +
> +    impl Display for NodeId {
> +        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
> +            write!(f, "{}_{}", self.area, self.node)
> +        }
> +    }
> +
> +    impl FromStr for NodeId {
> +        type Err = NodeIdError;
> +
> +        fn from_str(s: &str) -> Result<Self, Self::Err> {
> +            if let Some((area_id, node_id)) = s.split_once('_') {
> +                return Self::new(area_id.to_owned(), node_id.to_owned());
> +            }
> +
> +            Err(Self::Err::InvalidNodeId)
> +        }
> +    }
> +
> +    #[derive(Error, Debug)]
> +    pub enum OspfConfigError {
> +        #[error("Unknown error occured")]
> +        Unknown,
> +        #[error("Error parsing router id ip address")]
> +        RouterIdParseError(#[from] AddrParseError),
> +        #[error("The corresponding fabric for this node has not been found")]
> +        FabricNotFound,
> +        #[error("The OSPF Area could not be parsed")]
> +        AreaParsingError(#[from] AreaParsingError),
> +        #[error("NodeId parse error")]
> +        NodeIdError(#[from] NodeIdError),
> +    }
> +
> +    #[derive(Error, Debug)]
> +    pub enum AreaParsingError {
> +        #[error("Invalid area identifier. Area must be a number or a ipv4 address.")]
> +        InvalidArea,
> +    }
> +
> +    /// OSPF Area, which is unique and is used to differentiate between different ospf fabrics.
> +    #[derive(Debug, Deserialize, Serialize, Hash, PartialEq, Eq, PartialOrd, Ord, Clone)]
> +    pub struct Area(String);
> +
> +    impl Area {
> +        pub fn new(area: String) -> Result<Area, AreaParsingError> {
> +            if area.parse::<i32>().is_ok() || area.parse::<Ipv4Addr>().is_ok() {
> +                Ok(Self(area))
> +            } else {
> +                Err(AreaParsingError::InvalidArea)
> +            }
> +        }
> +    }
> +
> +    impl TryFrom<String> for Area {
> +        type Error = AreaParsingError;
> +
> +        fn try_from(value: String) -> Result<Self, Self::Error> {
> +            Area::new(value)
> +        }
> +    }
> +
> +    impl AsRef<str> for Area {
> +        fn as_ref(&self) -> &str {
> +            &self.0
> +        }
> +    }
> +
> +    impl Display for Area {
> +        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
> +            self.0.fmt(f)
> +        }
> +    }
> +
> +    #[derive(Debug, Deserialize, Serialize)]
> +    pub struct OspfConfig {
> +        fabrics: HashMap<Area, FabricConfig>,
> +    }
> +
> +    impl OspfConfig {
> +        pub fn fabrics(&self) -> &HashMap<Area, FabricConfig> {
> +            &self.fabrics
> +        }
> +    }
> +
> +    #[derive(Debug, Deserialize, Serialize)]
> +    pub struct FabricConfig {
> +        nodes: HashMap<Hostname, NodeConfig>,
> +    }
> +
> +    impl FabricConfig {
> +        pub fn nodes(&self) -> &HashMap<Hostname, NodeConfig> {
> +            &self.nodes
> +        }
> +    }
> +
> +    impl TryFrom<FabricSection> for FabricConfig {
> +        type Error = OspfConfigError;
> +
> +        fn try_from(_value: FabricSection) -> Result<Self, Self::Error> {
> +            // currently no attributes here
> +            Ok(FabricConfig {
> +                nodes: HashMap::new(),
> +            })
> +        }
> +    }
> +
> +    #[derive(Debug, Deserialize, Serialize)]
> +    pub struct NodeConfig {
> +        pub router_id: Ipv4Addr,
> +        pub interfaces: Vec<Interface>,
> +    }
> +
> +    impl NodeConfig {
> +        pub fn router_id(&self) -> &Ipv4Addr {
> +            &self.router_id
> +        }
> +        pub fn interfaces(&self) -> impl Iterator<Item = &Interface> + '_ {
> +            self.interfaces.iter()
> +        }
> +    }
> +
> +    impl TryFrom<NodeSection> for NodeConfig {
> +        type Error = OspfConfigError;
> +
> +        fn try_from(value: NodeSection) -> Result<Self, Self::Error> {
> +            Ok(NodeConfig {
> +                router_id: value.router_id.parse()?,
> +                interfaces: value
> +                    .interface
> +                    .into_iter()
> +                    .map(|i| Interface::try_from(i.into_inner()).unwrap())
> +                    .collect(),
> +            })
> +        }
> +    }
> +
> +    #[derive(Debug, Deserialize, Serialize)]
> +    pub struct Interface {
> +        name: String,
> +        passive: Option<bool>,
> +    }
> +
> +    impl Interface {
> +        pub fn name(&self) -> &str {
> +            &self.name
> +        }
> +        pub fn passive(&self) -> Option<bool> {
> +            self.passive
> +        }
> +    }
> +
> +    impl TryFrom<InterfaceProperties> for Interface {
> +        type Error = OspfConfigError;
> +
> +        fn try_from(value: InterfaceProperties) -> Result<Self, Self::Error> {
> +            Ok(Interface {
> +                name: value.name.clone(),
> +                passive: value.passive,
> +            })
> +        }
> +    }

are we anticipating this to be fallible in the future?

> +    impl TryFrom<SectionConfigData<OspfSectionConfig>> for OspfConfig {
> +        type Error = OspfConfigError;
> +
> +        fn try_from(value: SectionConfigData<OspfSectionConfig>) -> Result<Self, Self::Error> {
> +            let mut fabrics = HashMap::new();
> +            let mut nodes = HashMap::new();
> +
> +            for (id, config) in value {
> +                match config {
> +                    OspfSectionConfig::Fabric(fabric_section) => {
> +                        fabrics
> +                            .insert(Area::try_from(id)?, FabricConfig::try_from(fabric_section)?);
> +                    }
> +                    OspfSectionConfig::Node(node_section) => {
> +                        nodes.insert(id.parse::<NodeId>()?, NodeConfig::try_from(node_section)?);
> +                    }
> +                }
> +            }
> +
> +            for (id, node) in nodes {
> +                let fabric = fabrics
> +                    .get_mut(&id.area)
> +                    .ok_or(OspfConfigError::FabricNotFound)?;
> +
> +                fabric.nodes.insert(id.node, node);
> +            }
> +            Ok(OspfConfig { fabrics })
> +        }
> +    }
> +
> +    impl OspfConfig {
> +        pub fn default(raw: &str) -> Result<Self, anyhow::Error> {
> +            OspfConfig::from_section_config(raw)
> +        }
> +    }
> +}
> +
> +impl FromSectionConfig for OspfConfig {
> +    type Section = OspfSectionConfig;
> +
> +    fn filename() -> String {
> +        "ospf.cfg".to_owned()
> +    }
> +}
> diff --git a/proxmox-ve-config/src/sdn/mod.rs b/proxmox-ve-config/src/sdn/mod.rs
> index c8dc72471693..811fa21c483a 100644
> --- a/proxmox-ve-config/src/sdn/mod.rs
> +++ b/proxmox-ve-config/src/sdn/mod.rs
> @@ -1,4 +1,5 @@
>  pub mod config;
> +pub mod fabric;
>  pub mod ipam;
>  
>  use std::{error::Error, fmt::Display, str::FromStr};



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


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

* Re: [pve-devel] [PATCH proxmox-perl-rs 04/11] fabrics: add CRUD and generate fabrics methods
  2025-02-14 13:39 ` [pve-devel] [PATCH proxmox-perl-rs 04/11] fabrics: add CRUD and generate fabrics methods Gabriel Goller
@ 2025-03-04  9:28   ` Stefan Hanreich
  2025-03-05 10:20     ` Gabriel Goller
  0 siblings, 1 reply; 32+ messages in thread
From: Stefan Hanreich @ 2025-03-04  9:28 UTC (permalink / raw)
  To: Proxmox VE development discussion, Gabriel Goller

comments inline

On 2/14/25 14:39, Gabriel Goller wrote:
> Add CRUD and generate fabrics method for perlmod. These can be called
> from perl with the raw configuration to edit/read/update/delete the
> configuration. It also contains functions to generate the frr config
> from the passed SectionConfig.
> 
> Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
> ---
>  pve-rs/Cargo.toml            |   5 +-
>  pve-rs/Makefile              |   3 +
>  pve-rs/src/lib.rs            |   1 +
>  pve-rs/src/sdn/fabrics.rs    | 202 ++++++++++++++++
>  pve-rs/src/sdn/mod.rs        |   3 +
>  pve-rs/src/sdn/openfabric.rs | 454 +++++++++++++++++++++++++++++++++++
>  pve-rs/src/sdn/ospf.rs       | 425 ++++++++++++++++++++++++++++++++
>  7 files changed, 1092 insertions(+), 1 deletion(-)
>  create mode 100644 pve-rs/src/sdn/fabrics.rs
>  create mode 100644 pve-rs/src/sdn/mod.rs
>  create mode 100644 pve-rs/src/sdn/openfabric.rs
>  create mode 100644 pve-rs/src/sdn/ospf.rs
> 
> diff --git a/pve-rs/Cargo.toml b/pve-rs/Cargo.toml
> index 4b6dec6ff452..67806810e560 100644
> --- a/pve-rs/Cargo.toml
> +++ b/pve-rs/Cargo.toml
> @@ -40,9 +40,12 @@ proxmox-log = "0.2"
>  proxmox-notify = { version = "0.5", features = ["pve-context"] }
>  proxmox-openid = "0.10"
>  proxmox-resource-scheduling = "0.3.0"
> +proxmox-schema = "4.0.0"
> +proxmox-section-config = "2.1.1"
>  proxmox-shared-cache = "0.1.0"
>  proxmox-subscription = "0.5"
>  proxmox-sys = "0.6"
>  proxmox-tfa = { version = "5", features = ["api"] }
>  proxmox-time = "2"
> -proxmox-ve-config = { version = "0.2.1" }
> +proxmox-ve-config = "0.2.1"
> +proxmox-frr = { version = "0.1", features = ["config-ext"] }
> diff --git a/pve-rs/Makefile b/pve-rs/Makefile
> index d01da692d8c9..5bd4d3c58b36 100644
> --- a/pve-rs/Makefile
> +++ b/pve-rs/Makefile
> @@ -31,6 +31,9 @@ PERLMOD_PACKAGES := \
>  	  PVE::RS::Firewall::SDN \
>  	  PVE::RS::OpenId \
>  	  PVE::RS::ResourceScheduling::Static \
> +	  PVE::RS::SDN::Fabrics \
> +	  PVE::RS::SDN::Fabrics::OpenFabric \
> +	  PVE::RS::SDN::Fabrics::Ospf \
>  	  PVE::RS::TFA
>  
>  PERLMOD_PACKAGE_FILES := $(addsuffix .pm,$(subst ::,/,$(PERLMOD_PACKAGES)))
> diff --git a/pve-rs/src/lib.rs b/pve-rs/src/lib.rs
> index 3de37d17fab6..12ee87a91cc6 100644
> --- a/pve-rs/src/lib.rs
> +++ b/pve-rs/src/lib.rs
> @@ -15,6 +15,7 @@ pub mod apt;
>  pub mod firewall;
>  pub mod openid;
>  pub mod resource_scheduling;
> +pub mod sdn;
>  pub mod tfa;
>  
>  #[perlmod::package(name = "Proxmox::Lib::PVE", lib = "pve_rs")]
> diff --git a/pve-rs/src/sdn/fabrics.rs b/pve-rs/src/sdn/fabrics.rs
> new file mode 100644
> index 000000000000..53c7f47bec4c
> --- /dev/null
> +++ b/pve-rs/src/sdn/fabrics.rs
> @@ -0,0 +1,202 @@
> +#[perlmod::package(name = "PVE::RS::SDN::Fabrics", lib = "pve_rs")]
> +pub mod export {
> +    use std::{collections::HashMap, fmt, str::FromStr, sync::Mutex};
> +
> +    use anyhow::Error;
> +    use proxmox_frr::{
> +        openfabric::{OpenFabricInterface, OpenFabricRouter},
> +        ospf::{OspfInterface, OspfRouter},
> +        FrrConfig, Interface, Router,
> +    };
> +    use proxmox_section_config::{
> +        typed::ApiSectionDataEntry, typed::SectionConfigData as TypedSectionConfigData,
> +    };
> +    use proxmox_ve_config::sdn::fabric::{
> +        openfabric::OpenFabricSectionConfig,
> +        ospf::OspfSectionConfig,
> +    };
> +    use serde::{Deserialize, Serialize};
> +
> +    #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)]
> +    pub struct PerlRouter {
> +        #[serde(skip_serializing_if = "HashMap::is_empty")]
> +        address_family: HashMap<String, Vec<String>>,
> +        #[serde(rename = "")]
> +        root_properties: Vec<String>,
> +    }
> +
> +    impl From<&Router> for PerlRouter {
> +        fn from(value: &Router) -> Self {
> +            match value {
> +                Router::OpenFabric(router) => PerlRouter::from(router),
> +                Router::Ospf(router) => PerlRouter::from(router),
> +            }
> +        }
> +    }
> +
> +    impl From<&OpenFabricRouter> for PerlRouter {
> +        fn from(value: &OpenFabricRouter) -> Self {
> +            let mut router = PerlRouter::default();
> +            router.root_properties.push(format!("net {}", value.net()));
> +
> +            router
> +        }
> +    }
> +
> +    impl From<&OspfRouter> for PerlRouter {
> +        fn from(value: &OspfRouter) -> Self {
> +            let mut router = PerlRouter::default();
> +            router
> +                .root_properties
> +                .push(format!("ospf router-id {}", value.router_id()));
> +
> +            router
> +        }
> +    }
> +
> +    #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)]
> +    pub struct PerlInterfaceProperties(Vec<String>);
> +
> +    impl From<&Interface> for PerlInterfaceProperties {
> +        fn from(value: &Interface) -> Self {
> +            match value {
> +                Interface::OpenFabric(openfabric) => PerlInterfaceProperties::from(openfabric),
> +                Interface::Ospf(ospf) => PerlInterfaceProperties::from(ospf),
> +            }
> +        }
> +    }
> +
> +    impl From<&OpenFabricInterface> for PerlInterfaceProperties {
> +        fn from(value: &OpenFabricInterface) -> Self {
> +            let mut interface = PerlInterfaceProperties::default();
> +            // Note: the "openfabric" is printed by the OpenFabricRouterName Display impl
> +            interface.0.push(format!("ip router {}", value.fabric_id()));
> +            if *value.passive() == Some(true) {
> +                interface.0.push("openfabric passive".to_string());
> +            }
> +            if let Some(hello_interval) = value.hello_interval() {
> +                interface
> +                    .0
> +                    .push(format!("openfabric hello-interval {}", hello_interval));
> +            }
> +            if let Some(csnp_interval) = value.csnp_interval() {
> +                interface
> +                    .0
> +                    .push(format!("openfabric csnp-interval {}", csnp_interval));
> +            }
> +            if let Some(hello_multiplier) = value.hello_multiplier() {
> +                interface
> +                    .0
> +                    .push(format!("openfabric hello-multiplier {}", hello_multiplier));
> +            }
> +
> +            interface
> +        }
> +    }
> +    impl From<&OspfInterface> for PerlInterfaceProperties {
> +        fn from(value: &OspfInterface) -> Self {
> +            let mut interface = PerlInterfaceProperties::default();
> +            // the area is printed by the Display impl.
> +            interface.0.push(format!("ip ospf {}", value.area()));
> +            if *value.passive() == Some(true) {
> +                interface.0.push("ip ospf passive".to_string());
> +            }
> +
> +            interface
> +        }
> +    }
> +
> +    #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
> +    pub struct PerlFrrRouter {
> +        pub router: HashMap<String, PerlRouter>,
> +    }
> +
> +    #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
> +    pub struct PerlFrrConfig {
> +        frr: PerlFrrRouter,
> +        frr_interface: HashMap<String, PerlInterfaceProperties>,
> +    }
> +
> +    #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
> +    pub enum Protocol {
> +        #[serde(rename = "openfabric")]
> +        OpenFabric,
> +        #[serde(rename = "ospf")]
> +        Ospf,
> +    }
> +
> +    /// Will be used as a filename in the write method in pve-cluster, so this should not be
> +    /// changed unless the filename of the config is also changed.
> +    impl fmt::Display for Protocol {
> +        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
> +            write!(f, "{}", format!("{:?}", self).to_lowercase())
> +        }
> +    }
> +
> +    impl FromStr for Protocol {
> +        type Err = anyhow::Error;
> +
> +        fn from_str(input: &str) -> Result<Protocol, Self::Err> {
> +            match input {
> +                "openfabric" => Ok(Protocol::OpenFabric),
> +                "ospf" => Ok(Protocol::Ospf),
> +                _ => Err(anyhow::anyhow!("protocol not implemented")),
> +            }
> +        }
> +    }
> +
> +    pub struct PerlSectionConfig<T> {
> +        pub section_config: Mutex<TypedSectionConfigData<T>>,
> +    }
> +
> +    impl<T> PerlSectionConfig<T>
> +    where
> +        T: Send + Sync + Clone,
> +    {
> +        pub fn into_inner(self) -> Result<TypedSectionConfigData<T>, anyhow::Error> {
> +            let value = self.section_config.into_inner().unwrap();
> +            Ok(value.clone())
> +        }
> +    }
> +
> +    impl From<FrrConfig> for PerlFrrConfig {
> +        fn from(value: FrrConfig) -> PerlFrrConfig {
> +            let router = PerlFrrRouter {
> +                router: value
> +                    .router()
> +                    .map(|(name, data)| (name.to_string(), PerlRouter::from(data)))
> +                    .collect(),
> +            };
> +
> +            Self {
> +                frr: router,
> +                frr_interface: value
> +                    .interfaces()
> +                    .map(|(name, data)| (name.to_string(), PerlInterfaceProperties::from(data)))
> +                    .collect(),
> +            }
> +        }
> +    }
> +
> +    #[derive(Serialize, Deserialize)]
> +    struct AllConfigs {
> +        openfabric: HashMap<String, OpenFabricSectionConfig>,
> +        ospf: HashMap<String, OspfSectionConfig>,
> +    }
> +
> +    /// Get all the config. This takes the raw openfabric and ospf config, parses, and returns
> +    /// both.
> +    #[export]
> +    fn config(raw_openfabric: &[u8], raw_ospf: &[u8]) -> Result<AllConfigs, Error> {
> +        let raw_openfabric = std::str::from_utf8(raw_openfabric)?;
> +        let raw_ospf = std::str::from_utf8(raw_ospf)?;
> +
> +        let openfabric = OpenFabricSectionConfig::parse_section_config("openfabric.cfg", raw_openfabric)?;
> +        let ospf = OspfSectionConfig::parse_section_config("ospf.cfg", raw_ospf)?;
> +
> +        Ok(AllConfigs {
> +            openfabric: openfabric.into_iter().collect(),
> +            ospf: ospf.into_iter().collect(),
> +        })
> +    }
> +}
> diff --git a/pve-rs/src/sdn/mod.rs b/pve-rs/src/sdn/mod.rs
> new file mode 100644
> index 000000000000..6700c989483f
> --- /dev/null
> +++ b/pve-rs/src/sdn/mod.rs
> @@ -0,0 +1,3 @@
> +pub mod fabrics;
> +pub mod openfabric;
> +pub mod ospf;
> diff --git a/pve-rs/src/sdn/openfabric.rs b/pve-rs/src/sdn/openfabric.rs
> new file mode 100644
> index 000000000000..1f84930fd0da
> --- /dev/null
> +++ b/pve-rs/src/sdn/openfabric.rs
> @@ -0,0 +1,454 @@
> +#[perlmod::package(name = "PVE::RS::SDN::Fabrics::OpenFabric", lib = "pve_rs")]
> +mod export {
> +    use core::str;
> +    use std::{collections::HashMap, sync::{Mutex, MutexGuard}};
> +
> +    use anyhow::{Context, Error};
> +    use perlmod::Value;
> +    use proxmox_frr::FrrConfigBuilder;
> +    use proxmox_schema::property_string::PropertyString;
> +    use proxmox_section_config::typed::{ApiSectionDataEntry, SectionConfigData};
> +    use proxmox_ve_config::sdn::fabric::{
> +        openfabric::{internal::{FabricId, NodeId, OpenFabricConfig}, FabricSection, InterfaceProperties, NodeSection, OpenFabricSectionConfig}, FabricConfig,
> +    };
> +    use serde::{Deserialize, Serialize};
> +
> +    use crate::sdn::fabrics::export::{PerlFrrConfig, PerlSectionConfig};
> +
> +    perlmod::declare_magic!(Box<PerlSectionConfig<OpenFabricSectionConfig>> : &PerlSectionConfig<OpenFabricSectionConfig> as "PVE::RS::SDN::Fabrics::OpenFabric");
> +
> +    #[derive(Debug, Serialize, Deserialize)]
> +    pub struct AddFabric {
> +        name: String,
> +        r#type: String,
> +        #[serde(deserialize_with = "deserialize_empty_string_to_none")]
> +        hello_interval: Option<u16>,
> +    }
> +
> +    #[derive(Debug, Serialize, Deserialize)]
> +    pub struct DeleteFabric {
> +        fabric: String,
> +    }
> +
> +    #[derive(Debug, Serialize, Deserialize)]
> +    pub struct DeleteNode {
> +        fabric: String,
> +        node: String,
> +    }
> +
> +    #[derive(Debug, Serialize, Deserialize)]
> +    pub struct DeleteInterface {
> +        fabric: String,
> +        node: String,
> +        /// interface name
> +        name: String,
> +    }
> +
> +    fn deserialize_empty_string_to_none<'de, D>(deserializer: D) -> Result<Option<u16>, D::Error>
> +    where
> +        D: serde::de::Deserializer<'de>,
> +    {
> +        let s: &str = serde::de::Deserialize::deserialize(deserializer)?;
> +        if s.is_empty() {
> +            Ok(None)
> +        } else {
> +            serde_json::from_str(s).map_err(serde::de::Error::custom)
> +        }
> +    }
> +
> +    #[derive(Debug, Serialize, Deserialize)]
> +    pub struct EditFabric {
> +        fabric: String,
> +        #[serde(deserialize_with = "deserialize_empty_string_to_none")]
> +        hello_interval: Option<u16>,
> +    }
> +
> +    #[derive(Debug, Deserialize)]
> +    pub struct AddNode {
> +        fabric: String,
> +        node: String,
> +        net: String,
> +        interfaces: Vec<String>,
> +    }
> +
> +    #[derive(Debug, Serialize, Deserialize)]
> +    pub struct EditNode {
> +        node: String,
> +        fabric: String,
> +        net: String,
> +        interfaces: Vec<String>,
> +    }
> +
> +    #[derive(Debug, Serialize, Deserialize)]
> +    pub struct EditInterface {
> +        node: String,
> +        fabric: String,
> +        name: String,
> +        passive: bool,
> +        #[serde(deserialize_with = "deserialize_empty_string_to_none")]
> +        hello_interval: Option<u16>,
> +        #[serde(deserialize_with = "deserialize_empty_string_to_none")]
> +        hello_multiplier: Option<u16>,
> +        #[serde(deserialize_with = "deserialize_empty_string_to_none")]
> +        csnp_interval: Option<u16>,
> +    }
> +    
> +    fn interface_exists(
> +        config: &MutexGuard<SectionConfigData<OpenFabricSectionConfig>>,
> +        interface_name: &str,
> +        node_name: &str,
> +    ) -> bool {
> +        config.sections.iter().any(|(k, v)| {
> +            if let OpenFabricSectionConfig::Node(n) = v {
> +                k.parse::<NodeId>().ok().is_some_and(|id| {
> +                    id.node.as_ref() == node_name
> +                        && n.interface.iter().any(|i| i.name == interface_name)
> +                })
> +            } else {
> +                false
> +            }
> +        })
> +    }
> +
> +    impl PerlSectionConfig<OpenFabricSectionConfig> {
> +        pub fn add_fabric(&self, new_config: AddFabric) -> Result<(), anyhow::Error> {
> +            let fabricid = FabricId::from(new_config.name).to_string();

Could we simplify this method and the ones below by just using the
concrete types (here FabricId) inside the argument structs (AddFabric)?
There's potential for quite a few here afaict, also with the
Option<u16>'s. Would save us a lot of conversion / validation logic if
we just did it at deserialization.

I pointed out some instances below.

I guess the error messages would be a bit worse then?

> +            let new_fabric = OpenFabricSectionConfig::Fabric(FabricSection {
> +                hello_interval: new_config
> +                    .hello_interval
> +                    .map(|x| x.try_into())
> +                    .transpose()?,
> +            });
> +            let mut config = self.section_config.lock().unwrap();
> +            if config.sections.contains_key(&fabricid) {
> +                anyhow::bail!("fabric already exists");
> +            }
> +            config.sections.insert(fabricid, new_fabric);

try_insert instead of contains_key + insert?

> +            Ok(())
> +        }
> +
> +        pub fn edit_fabric(&self, new_config: EditFabric) -> Result<(), anyhow::Error> {
> +            let mut config = self.section_config.lock().unwrap();
> +
> +            let fabricid = new_config.fabric.parse::<FabricId>()?;
> +
> +            if let OpenFabricSectionConfig::Fabric(fs) = config
> +                .sections
> +                .get_mut(fabricid.as_ref())
> +                .context("fabric doesn't exists")?
> +            {
> +                fs.hello_interval = new_config
> +                    .hello_interval
> +                    .map(|x| x.try_into())
> +                    .transpose()
> +                    .unwrap_or(None);

maybe simpler with concrete types in arg struct?

> +            }
> +            Ok(())
> +        }
> +
> +        pub fn add_node(&self, new_config: AddNode) -> Result<(), anyhow::Error> {
> +            let mut interfaces: Vec<PropertyString<InterfaceProperties>> = vec![];
> +            for i in new_config.interfaces {
> +                let ps: PropertyString<InterfaceProperties> = i.parse()?;
> +                interfaces.push(ps);
> +            }
> +
> +            let nodeid = NodeId::new(new_config.fabric, new_config.node);
> +            let nodeid_key = nodeid.to_string();
> +
> +            let mut config = self.section_config.lock().unwrap();
> +            if config.sections.contains_key(&nodeid_key) {
> +                anyhow::bail!("node already exists");
> +            }
> +            if interfaces.iter().any(|i| interface_exists(&config, &i.name, nodeid.node.as_ref())) {
> +                anyhow::bail!("One interface cannot be a part of two fabrics");
> +            }
> +            let new_fabric = OpenFabricSectionConfig::Node(NodeSection {
> +                net: new_config.net.parse()?,
> +                interface: interfaces,
> +            });
> +            config.sections.insert(nodeid_key, new_fabric);
> +            Ok(())
> +        }
> +
> +        pub fn edit_node(&self, new_config: EditNode) -> Result<(), anyhow::Error> {
> +            let mut interfaces: Vec<PropertyString<InterfaceProperties>> = vec![];
> +            for i in new_config.interfaces {
> +                let ps: PropertyString<InterfaceProperties> = i.parse()?;
> +                interfaces.push(ps);
> +            }
> +            let net = new_config.net.parse()?;
> +
> +            let nodeid = NodeId::new(new_config.fabric, new_config.node).to_string();
> +
> +            let mut config = self.section_config.lock().unwrap();
> +            if !config.sections.contains_key(&nodeid) {
> +                anyhow::bail!("node not found");
> +            }
> +            config.sections.entry(nodeid).and_modify(|n| {
> +                if let OpenFabricSectionConfig::Node(n) = n {
> +                    n.net = net;
> +                    n.interface = interfaces;
> +                }
> +            });

wouldn't get_mut be easier here? also would save the extra contains_key

> +            Ok(())
> +        }
> +
> +        pub fn edit_interface(&self, new_config: EditInterface) -> Result<(), anyhow::Error> {
> +            let mut config = self.section_config.lock().unwrap();
> +            let nodeid = NodeId::new(new_config.fabric, new_config.node).to_string();
> +            if !config.sections.contains_key(&nodeid) {
> +                anyhow::bail!("interface not found");
> +            }
> +
> +            config.sections.entry(nodeid).and_modify(|n| {

maybe get_mut is easier here too?

> +                if let OpenFabricSectionConfig::Node(n) = n {
> +                    n.interface.iter_mut().for_each(|i| {
> +                        if i.name == new_config.name {
> +                            i.passive = Some(new_config.passive);
> +                            i.hello_interval =
> +                                new_config.hello_interval.and_then(|hi| hi.try_into().ok());
> +                            i.hello_multiplier =
> +                                new_config.hello_multiplier.and_then(|ci| ci.try_into().ok());
> +                            i.csnp_interval =
> +                                new_config.csnp_interval.and_then(|ci| ci.try_into().ok());
> +                        }

maybe simpler with concrete types in arg struct?

> +                    });
> +                }
> +            });
> +            Ok(())
> +        }
> +
> +        pub fn delete_fabric(&self, new_config: DeleteFabric) -> Result<(), anyhow::Error> {
> +            let mut config = self.section_config.lock().unwrap();
> +
> +            let fabricid = FabricId::new(new_config.fabric)?;
> +
> +            config
> +                .sections
> +                .remove(fabricid.as_ref())
> +                .ok_or(anyhow::anyhow!("fabric not found"))?;
> +            // remove all the nodes
> +            config.sections.retain(|k, _v| {
> +                if let Ok(nodeid) = k.parse::<NodeId>() {
> +                    return nodeid.fabric != fabricid;
> +                }
> +                true
> +            });
> +            Ok(())
> +        }
> +
> +        pub fn delete_node(&self, new_config: DeleteNode) -> Result<(), anyhow::Error> {
> +            let mut config = self.section_config.lock().unwrap();
> +            let nodeid = NodeId::new(new_config.fabric, new_config.node).to_string();
> +            config
> +                .sections
> +                .remove(&nodeid)
> +                .ok_or(anyhow::anyhow!("node not found"))?;
> +            Ok(())
> +        }
> +
> +        pub fn delete_interface(&self, new_config: DeleteInterface) -> Result<(), anyhow::Error> {
> +            let mut config = self.section_config.lock().unwrap();
> +            let mut removed = false;
> +            let nodeid = NodeId::new(new_config.fabric, new_config.node).to_string();
> +            config.sections.entry(nodeid).and_modify(|v| {
> +                if let OpenFabricSectionConfig::Node(f) = v {
> +                    if f.interface.len() > 1 {
> +                        removed = true;
> +                        f.interface.retain(|x| x.name != new_config.name);
> +                    }
> +                }
> +            });
> +            if !removed {
> +                anyhow::bail!("error removing interface");
> +            }
> +            Ok(())
> +        }
> +
> +        pub fn write(&self) -> Result<String, anyhow::Error> {
> +            let guard = self.section_config.lock().unwrap().clone();
> +            OpenFabricSectionConfig::write_section_config("sdn/fabrics/openfabric.cfg", &guard)
> +        }
> +    }
> +
> +    #[export(raw_return)]
> +    fn config(#[raw] class: Value, raw_config: &[u8]) -> Result<perlmod::Value, anyhow::Error> {
> +        let raw_config = std::str::from_utf8(raw_config)?;
> +
> +        let config = OpenFabricSectionConfig::parse_section_config("openfabric.cfg", raw_config)?;
> +        let return_value = PerlSectionConfig {
> +            section_config: Mutex::new(config),
> +        };
> +
> +        Ok(perlmod::instantiate_magic!(&class, MAGIC => Box::new(
> +                return_value
> +        )))
> +    }
> +
> +    /// Writes the config to a string and returns the configuration and the protocol.
> +    #[export]
> +    fn write(
> +        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
> +    ) -> Result<(String, String), Error> {
> +        let full_new_config = this.write()?;
> +
> +        // We return the protocol here as well, so that in perl we can write to
> +        // the correct config file
> +        Ok((full_new_config, "openfabric".to_string()))
> +    }
> +
> +    #[export]
> +    fn add_fabric(
> +        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
> +        new_config: AddFabric,
> +    ) -> Result<(), Error> {
> +        this.add_fabric(new_config)?;
> +
> +        Ok(())
> +    }
> +
> +    #[export]
> +    fn add_node(
> +        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
> +        new_config: AddNode,
> +    ) -> Result<(), Error> {
> +        this.add_node(new_config)
> +    }
> +
> +    #[export]
> +    fn edit_fabric(
> +        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
> +        new_config: EditFabric,
> +    ) -> Result<(), Error> {
> +        this.edit_fabric(new_config)
> +    }
> +
> +    #[export]
> +    fn edit_node(
> +        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
> +        new_config: EditNode,
> +    ) -> Result<(), Error> {
> +        this.edit_node(new_config)
> +    }
> +
> +    #[export]
> +    fn edit_interface(
> +        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
> +        new_config: EditInterface,
> +    ) -> Result<(), Error> {
> +        this.edit_interface(new_config)
> +    }
> +
> +    #[export]
> +    fn delete_fabric(
> +        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
> +        delete_config: DeleteFabric,
> +    ) -> Result<(), Error> {
> +        this.delete_fabric(delete_config)?;
> +
> +        Ok(())
> +    }
> +
> +    #[export]
> +    fn delete_node(
> +        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
> +        delete_config: DeleteNode,
> +    ) -> Result<(), Error> {
> +        this.delete_node(delete_config)?;
> +
> +        Ok(())
> +    }
> +
> +    #[export]
> +    fn delete_interface(
> +        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
> +        delete_config: DeleteInterface,
> +    ) -> Result<(), Error> {
> +        this.delete_interface(delete_config)?;
> +
> +        Ok(())
> +    }
> +
> +    #[export]
> +    fn get_inner(
> +        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
> +    ) -> HashMap<String, OpenFabricSectionConfig> {
> +        let guard = this.section_config.lock().unwrap();
> +        guard.clone().into_iter().collect()
> +    }
> +
> +    #[export]
> +    fn get_fabric(
> +        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
> +        fabric: String,
> +    ) -> Result<OpenFabricSectionConfig, Error> {
> +        let guard = this.section_config.lock().unwrap();
> +        guard
> +            .get(&fabric)
> +            .cloned()
> +            .ok_or(anyhow::anyhow!("fabric not found"))
> +    }
> +
> +    #[export]
> +    fn get_node(
> +        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
> +        fabric: String,
> +        node: String,
> +    ) -> Result<OpenFabricSectionConfig, Error> {
> +        let guard = this.section_config.lock().unwrap();
> +        let nodeid = NodeId::new(fabric, node).to_string();
> +        guard
> +            .get(&nodeid)
> +            .cloned()
> +            .ok_or(anyhow::anyhow!("node not found"))
> +    }
> +
> +    #[export]
> +    fn get_interface(
> +        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
> +        fabric: String,
> +        node: String,
> +        interface_name: String,
> +    ) -> Result<InterfaceProperties, Error> {
> +        let guard = this.section_config.lock().unwrap();
> +        let nodeid = NodeId::new(fabric, node).to_string();
> +        guard
> +            .get(&nodeid)
> +            .and_then(|v| {
> +                if let OpenFabricSectionConfig::Node(f) = v {
> +                    let interface = f.interface.clone().into_iter().find_map(|i| {
> +                        if i.name == interface_name {
> +                            return Some(i.into_inner());
> +                        }
> +                        None
> +                    });
> +                    Some(interface)
> +                } else {
> +                    None
> +                }
> +            })
> +            .flatten()
> +            .ok_or(anyhow::anyhow!("interface not found"))
> +    }
> +
> +    #[export]
> +    pub fn get_perl_frr_repr(
> +        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
> +        hostname: &[u8],
> +    ) -> Result<PerlFrrConfig, Error> {
> +        let hostname = str::from_utf8(hostname)?;
> +        let config = this.section_config.lock().unwrap();
> +        let openfabric_config: OpenFabricConfig =
> +            OpenFabricConfig::try_from(config.clone())?;
> +
> +        let config = FabricConfig::with_openfabric(openfabric_config);
> +        let frr_config = FrrConfigBuilder::default()
> +            .add_fabrics(config)
> +            .build(hostname)?;
> +
> +        let perl_config = PerlFrrConfig::from(frr_config);
> +
> +        Ok(perl_config)
> +    }
> +}
> diff --git a/pve-rs/src/sdn/ospf.rs b/pve-rs/src/sdn/ospf.rs
> new file mode 100644
> index 000000000000..d7d614fcbc2b
> --- /dev/null
> +++ b/pve-rs/src/sdn/ospf.rs

the remarks from above (cocnrete types in argument structs, hashmap
methods) apply to this file here as well, since they're architecturally
the same.

> @@ -0,0 +1,425 @@
> +#[perlmod::package(name = "PVE::RS::SDN::Fabrics::Ospf", lib = "pve_rs")]
> +mod export {
> +    use std::{
> +        collections::HashMap,
> +        str,
> +        sync::{Mutex, MutexGuard},
> +    };
> +
> +    use anyhow::{Context, Error};
> +    use perlmod::Value;
> +    use proxmox_frr::FrrConfigBuilder;
> +    use proxmox_schema::property_string::PropertyString;
> +    use proxmox_section_config::typed::{ApiSectionDataEntry, SectionConfigData};
> +    use proxmox_ve_config::sdn::fabric::{
> +        ospf::{
> +            internal::{Area, NodeId, OspfConfig},
> +            FabricSection, InterfaceProperties, NodeSection, OspfSectionConfig,
> +        },
> +        FabricConfig,
> +    };
> +    use serde::{Deserialize, Serialize};
> +
> +    use crate::sdn::fabrics::export::{PerlFrrConfig, PerlSectionConfig};
> +
> +    perlmod::declare_magic!(Box<PerlSectionConfig<OspfSectionConfig>> : &PerlSectionConfig<OspfSectionConfig> as "PVE::RS::SDN::Fabrics::Ospf");
> +
> +    #[derive(Debug, Serialize, Deserialize)]
> +    pub struct AddFabric {
> +        name: String,
> +        r#type: String,
> +    }
> +
> +    #[derive(Debug, Deserialize)]
> +    pub struct AddNode {
> +        node: String,
> +        fabric: String,
> +        router_id: String,
> +        interfaces: Vec<String>,
> +    }
> +
> +    #[derive(Debug, Serialize, Deserialize)]
> +    pub struct DeleteFabric {
> +        fabric: String,
> +    }
> +
> +    #[derive(Debug, Serialize, Deserialize)]
> +    pub struct DeleteNode {
> +        fabric: String,
> +        node: String,
> +    }
> +
> +    #[derive(Debug, Serialize, Deserialize)]
> +    pub struct DeleteInterface {
> +        fabric: String,
> +        node: String,
> +        /// interface name
> +        name: String,
> +    }
> +
> +    #[derive(Debug, Serialize, Deserialize)]
> +    pub struct EditFabric {
> +        name: String,
> +    }
> +
> +    #[derive(Debug, Serialize, Deserialize)]
> +    pub struct EditNode {
> +        fabric: String,
> +        node: String,
> +
> +        router_id: String,
> +        interfaces: Vec<String>,
> +    }
> +
> +    #[derive(Debug, Serialize, Deserialize)]
> +    pub struct EditInterface {
> +        fabric: String,
> +        node: String,
> +        name: String,
> +
> +        passive: bool,
> +    }
> +
> +    fn interface_exists(
> +        config: &MutexGuard<SectionConfigData<OspfSectionConfig>>,
> +        interface_name: &str,
> +        node_name: &str,
> +    ) -> bool {
> +        config.sections.iter().any(|(k, v)| {
> +            if let OspfSectionConfig::Node(n) = v {
> +                k.parse::<NodeId>().ok().is_some_and(|id| {
> +                    id.node.as_ref() == node_name
> +                        && n.interface.iter().any(|i| i.name == interface_name)
> +                })
> +            } else {
> +                false
> +            }
> +        })
> +    }
> +
> +    impl PerlSectionConfig<OspfSectionConfig> {
> +        pub fn add_fabric(&self, new_config: AddFabric) -> Result<(), anyhow::Error> {
> +            let new_fabric = OspfSectionConfig::Fabric(FabricSection {});
> +            let area = Area::new(new_config.name)?.to_string();
> +            let mut config = self.section_config.lock().unwrap();
> +            if config.sections.contains_key(&area) {
> +                anyhow::bail!("fabric already exists");
> +            }
> +            config.sections.insert(area, new_fabric);
> +            Ok(())
> +        }
> +
> +        pub fn add_node(&self, new_config: AddNode) -> Result<(), anyhow::Error> {
> +            let mut interfaces: Vec<PropertyString<InterfaceProperties>> = vec![];
> +            for i in new_config.interfaces {
> +                let ps: PropertyString<InterfaceProperties> = i.parse()?;
> +                interfaces.push(ps);
> +            }
> +
> +            let nodeid = NodeId::new(new_config.fabric, new_config.node)?;
> +            let nodeid_key = nodeid.to_string();
> +            let mut config = self.section_config.lock().unwrap();
> +            if config.sections.contains_key(&nodeid_key) {
> +                anyhow::bail!("node already exists");
> +            }
> +            if interfaces
> +                .iter()
> +                .any(|i| interface_exists(&config, &i.name, nodeid.node.as_ref()))
> +            {
> +                anyhow::bail!("One interface cannot be a part of two areas");
> +            }
> +
> +            let new_fabric = OspfSectionConfig::Node(NodeSection {
> +                router_id: new_config.router_id,
> +                interface: interfaces,
> +            });
> +            config.sections.insert(nodeid_key, new_fabric);
> +            Ok(())
> +        }
> +
> +        pub fn edit_fabric(&self, new_config: EditFabric) -> Result<(), anyhow::Error> {
> +            let mut config = self.section_config.lock().unwrap();
> +
> +            if let OspfSectionConfig::Fabric(_fs) = config
> +                .sections
> +                .get_mut(&new_config.name)
> +                .context("fabric doesn't exists")?
> +            {
> +                // currently no properties exist here
> +            }
> +            Ok(())
> +        }
> +
> +        pub fn delete_fabric(&self, new_config: DeleteFabric) -> Result<(), anyhow::Error> {
> +            let mut config = self.section_config.lock().unwrap();
> +
> +            let area = Area::new(new_config.fabric)?;
> +            config
> +                .sections
> +                .remove(area.as_ref())
> +                .ok_or(anyhow::anyhow!("no fabric found"))?;
> +
> +            // remove all the nodes
> +            config.sections.retain(|k, _v| {
> +                if let Ok(nodeid) = k.parse::<NodeId>() {
> +                    return nodeid.area != area;
> +                }
> +                true
> +            });
> +            Ok(())
> +        }
> +
> +        pub fn edit_node(&self, new_config: EditNode) -> Result<(), anyhow::Error> {
> +            let mut interfaces: Vec<PropertyString<InterfaceProperties>> = vec![];
> +            for i in new_config.interfaces {
> +                let ps: PropertyString<InterfaceProperties> = i.parse()?;
> +                interfaces.push(ps);
> +            }
> +            let nodeid = NodeId::new(new_config.fabric, new_config.node)?.to_string();
> +
> +            let mut config = self.section_config.lock().unwrap();
> +            if !config.sections.contains_key(&nodeid) {
> +                anyhow::bail!("node not found");
> +            }
> +            config.sections.entry(nodeid).and_modify(|n| {
> +                if let OspfSectionConfig::Node(n) = n {
> +                    n.router_id = new_config.router_id;
> +                    n.interface = interfaces;
> +                }
> +            });
> +            Ok(())
> +        }
> +
> +        pub fn edit_interface(&self, new_config: EditInterface) -> Result<(), anyhow::Error> {
> +            let mut config = self.section_config.lock().unwrap();
> +            let nodeid = NodeId::new(new_config.fabric, new_config.node)?.to_string();
> +            if !config.sections.contains_key(&nodeid) {
> +                anyhow::bail!("interface not found");
> +            }
> +
> +            config.sections.entry(nodeid).and_modify(|n| {
> +                if let OspfSectionConfig::Node(n) = n {
> +                    n.interface.iter_mut().for_each(|i| {
> +                        if i.name == new_config.name {
> +                            i.passive = Some(new_config.passive);
> +                        }
> +                    });
> +                }
> +            });
> +            Ok(())
> +        }
> +
> +        pub fn delete_node(&self, new_config: DeleteNode) -> Result<(), anyhow::Error> {
> +            let mut config = self.section_config.lock().unwrap();
> +            let nodeid = NodeId::new(new_config.fabric, new_config.node)?.to_string();
> +            config
> +                .sections
> +                .remove(&nodeid)
> +                .ok_or(anyhow::anyhow!("node not found"))?;
> +            Ok(())
> +        }
> +
> +        pub fn delete_interface(&self, new_config: DeleteInterface) -> Result<(), anyhow::Error> {
> +            let mut config = self.section_config.lock().unwrap();
> +            let mut removed = false;
> +            let nodeid = NodeId::new(new_config.fabric, new_config.node)?.to_string();
> +            config.sections.entry(nodeid).and_modify(|v| {
> +                if let OspfSectionConfig::Node(f) = v {
> +                    if f.interface.len() > 1 {
> +                        removed = true;
> +                        f.interface.retain(|x| x.name != new_config.name);
> +                    }
> +                }
> +            });
> +            if !removed {
> +                anyhow::bail!("error removing interface");
> +            }
> +            Ok(())
> +        }
> +
> +        pub fn write(&self) -> Result<String, anyhow::Error> {
> +            let guard = self.section_config.lock().unwrap().clone();
> +            OspfSectionConfig::write_section_config("sdn/fabrics/ospf.cfg", &guard)
> +        }
> +    }
> +
> +    #[export(raw_return)]
> +    fn config(#[raw] class: Value, raw_config: &[u8]) -> Result<perlmod::Value, anyhow::Error> {
> +        let raw_config = std::str::from_utf8(raw_config)?;
> +
> +        let config = OspfSectionConfig::parse_section_config("ospf.cfg", raw_config)?;
> +        let return_value = PerlSectionConfig {
> +            section_config: Mutex::new(config),
> +        };
> +
> +        Ok(perlmod::instantiate_magic!(&class, MAGIC => Box::new(
> +                return_value
> +        )))
> +    }
> +
> +    /// Writes the config to a string and returns the configuration and the protocol.
> +    #[export]
> +    fn write(
> +        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
> +    ) -> Result<(String, String), Error> {
> +        let full_new_config = this.write()?;
> +
> +        // We return the protocol here as well, so that in perl we can write to
> +        // the correct config file
> +        Ok((full_new_config, "ospf".to_string()))
> +    }
> +
> +    #[export]
> +    fn add_fabric(
> +        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
> +        new_config: AddFabric,
> +    ) -> Result<(), Error> {
> +        this.add_fabric(new_config)?;
> +
> +        Ok(())
> +    }
> +
> +    #[export]
> +    fn add_node(
> +        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
> +        new_config: AddNode,
> +    ) -> Result<(), Error> {
> +        this.add_node(new_config)
> +    }
> +
> +    #[export]
> +    fn edit_fabric(
> +        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
> +        new_config: EditFabric,
> +    ) -> Result<(), Error> {
> +        this.edit_fabric(new_config)
> +    }
> +
> +    #[export]
> +    fn edit_node(
> +        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
> +        new_config: EditNode,
> +    ) -> Result<(), Error> {
> +        this.edit_node(new_config)
> +    }
> +
> +    #[export]
> +    fn edit_interface(
> +        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
> +        new_config: EditInterface,
> +    ) -> Result<(), Error> {
> +        this.edit_interface(new_config)
> +    }
> +
> +    #[export]
> +    fn delete_fabric(
> +        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
> +        delete_config: DeleteFabric,
> +    ) -> Result<(), Error> {
> +        this.delete_fabric(delete_config)?;
> +
> +        Ok(())
> +    }
> +
> +    #[export]
> +    fn delete_node(
> +        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
> +        delete_config: DeleteNode,
> +    ) -> Result<(), Error> {
> +        this.delete_node(delete_config)?;
> +
> +        Ok(())
> +    }
> +
> +    #[export]
> +    fn delete_interface(
> +        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
> +        delete_config: DeleteInterface,
> +    ) -> Result<(), Error> {
> +        this.delete_interface(delete_config)?;
> +
> +        Ok(())
> +    }
> +
> +    #[export]
> +    fn get_inner(
> +        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
> +    ) -> HashMap<String, OspfSectionConfig> {
> +        let guard = this.section_config.lock().unwrap();
> +        guard.clone().into_iter().collect()
> +    }
> +
> +    #[export]
> +    fn get_fabric(
> +        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
> +        fabric: String,
> +    ) -> Result<OspfSectionConfig, Error> {
> +        let guard = this.section_config.lock().unwrap();
> +        guard
> +            .get(&fabric)
> +            .cloned()
> +            .ok_or(anyhow::anyhow!("fabric not found"))
> +    }
> +
> +    #[export]
> +    fn get_node(
> +        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
> +        fabric: String,
> +        node: String,
> +    ) -> Result<OspfSectionConfig, Error> {
> +        let guard = this.section_config.lock().unwrap();
> +        let nodeid = NodeId::new(fabric, node)?.to_string();
> +        guard
> +            .get(&nodeid)
> +            .cloned()
> +            .ok_or(anyhow::anyhow!("node not found"))
> +    }
> +
> +    #[export]
> +    fn get_interface(
> +        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
> +        fabric: String,
> +        node: String,
> +        interface_name: String,
> +    ) -> Result<InterfaceProperties, Error> {
> +        let guard = this.section_config.lock().unwrap();
> +        let nodeid = NodeId::new(fabric, node)?.to_string();
> +        guard
> +            .get(&nodeid)
> +            .and_then(|v| {
> +                if let OspfSectionConfig::Node(f) = v {
> +                    let interface = f.interface.clone().into_iter().find_map(|i| {
> +                        let interface = i.into_inner();
> +                        if interface.name == interface_name {
> +                            return Some(interface);
> +                        }
> +                        None
> +                    });
> +                    Some(interface)
> +                } else {
> +                    None
> +                }
> +            })
> +            .flatten()
> +            .ok_or(anyhow::anyhow!("interface not found"))
> +    }
> +
> +    #[export]
> +    pub fn get_perl_frr_repr(
> +        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
> +        hostname: &[u8],
> +    ) -> Result<PerlFrrConfig, Error> {
> +        let hostname = str::from_utf8(hostname)?;
> +        let config = this.section_config.lock().unwrap();
> +        let openfabric_config: OspfConfig = OspfConfig::try_from(config.clone())?;
> +
> +        let config = FabricConfig::with_ospf(openfabric_config);
> +        let frr_config = FrrConfigBuilder::default()
> +            .add_fabrics(config)
> +            .build(hostname)?;
> +
> +        let perl_config = PerlFrrConfig::from(frr_config);
> +
> +        Ok(perl_config)
> +    }
> +}



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


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

* Re: [pve-devel] [PATCH pve-network 08/11] add api endpoints for fabrics
  2025-02-14 13:39 ` [pve-devel] [PATCH pve-network 08/11] add api endpoints for fabrics Gabriel Goller
@ 2025-03-04  9:51   ` Stefan Hanreich
  0 siblings, 0 replies; 32+ messages in thread
From: Stefan Hanreich @ 2025-03-04  9:51 UTC (permalink / raw)
  To: Proxmox VE development discussion, Gabriel Goller

please add descriptions to the properties, it serves as the
documentation and we also cannot autogenerate the Rust types for the API
methods.

Some methods only have type => 'object' as return type. It would be good
to properly document the properties of those as well for the same
reasons as above.

It might make sense to define some standard options so we can reuse them
among multiple API methods.

On 2/14/25 14:39, Gabriel Goller wrote:
> Add api endpoints for CRUD of fabrics, nodes, and interfaces.
> 
> Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
> ---
>  src/PVE/API2/Network/SDN.pm                   |   7 +
>  src/PVE/API2/Network/SDN/Fabrics.pm           |  57 +++
>  src/PVE/API2/Network/SDN/Fabrics/Common.pm    | 111 +++++
>  src/PVE/API2/Network/SDN/Fabrics/Makefile     |   9 +
>  .../API2/Network/SDN/Fabrics/OpenFabric.pm    | 460 ++++++++++++++++++
>  src/PVE/API2/Network/SDN/Fabrics/Ospf.pm      | 433 +++++++++++++++++
>  src/PVE/API2/Network/SDN/Makefile             |   3 +-
>  7 files changed, 1079 insertions(+), 1 deletion(-)
>  create mode 100644 src/PVE/API2/Network/SDN/Fabrics.pm
>  create mode 100644 src/PVE/API2/Network/SDN/Fabrics/Common.pm
>  create mode 100644 src/PVE/API2/Network/SDN/Fabrics/Makefile
>  create mode 100644 src/PVE/API2/Network/SDN/Fabrics/OpenFabric.pm
>  create mode 100644 src/PVE/API2/Network/SDN/Fabrics/Ospf.pm
> 
> diff --git a/src/PVE/API2/Network/SDN.pm b/src/PVE/API2/Network/SDN.pm
> index d216e4878b61..ccbf0777e3d4 100644
> --- a/src/PVE/API2/Network/SDN.pm
> +++ b/src/PVE/API2/Network/SDN.pm
> @@ -17,6 +17,7 @@ use PVE::API2::Network::SDN::Vnets;
>  use PVE::API2::Network::SDN::Zones;
>  use PVE::API2::Network::SDN::Ipams;
>  use PVE::API2::Network::SDN::Dns;
> +use PVE::API2::Network::SDN::Fabrics;
>  
>  use base qw(PVE::RESTHandler);
>  
> @@ -45,6 +46,11 @@ __PACKAGE__->register_method ({
>      path => 'dns',
>  });
>  
> +__PACKAGE__->register_method ({
> +    subclass => "PVE::API2::Network::SDN::Fabrics",
> +    path => 'fabrics',
> +});
> +
>  __PACKAGE__->register_method({
>      name => 'index',
>      path => '',
> @@ -76,6 +82,7 @@ __PACKAGE__->register_method({
>  	    { id => 'controllers' },
>  	    { id => 'ipams' },
>  	    { id => 'dns' },
> +	    { id => 'fabrics' },
>  	];
>  
>  	return $res;
> diff --git a/src/PVE/API2/Network/SDN/Fabrics.pm b/src/PVE/API2/Network/SDN/Fabrics.pm
> new file mode 100644
> index 000000000000..8eb88efca102
> --- /dev/null
> +++ b/src/PVE/API2/Network/SDN/Fabrics.pm
> @@ -0,0 +1,57 @@
> +package PVE::API2::Network::SDN::Fabrics;
> +
> +use strict;
> +use warnings;
> +
> +use Storable qw(dclone);
> +
> +use PVE::RPCEnvironment;
> +use PVE::Tools qw(extract_param);
> +
> +use PVE::API2::Network::SDN::Fabrics::OpenFabric;
> +use PVE::API2::Network::SDN::Fabrics::Ospf;
> +
> +use PVE::Network::SDN::Fabrics;
> +
> +use PVE::RESTHandler;
> +use base qw(PVE::RESTHandler);
> +
> +
> +__PACKAGE__->register_method ({
> +    subclass => "PVE::API2::Network::SDN::Fabrics::OpenFabric",
> +    path => 'openfabric',
> +});
> +__PACKAGE__->register_method ({
> +    subclass => "PVE::API2::Network::SDN::Fabrics::Ospf",
> +    path => 'ospf',
> +});
> +
> +__PACKAGE__->register_method({
> +    name => 'index',
> +    path => '',
> +    method => 'GET',
> +    description => 'Index of SDN Fabrics',
> +    permissions => {
> +	description => "Only list entries where you have 'SDN.Audit' or 'SDN.Allocate' permissions on '/sdn/fabrics/<fabric>'",
> +	user => 'all',
> +    },
> +    parameters => {
> +    	additionalProperties => 0,
> +    },
> +    returns => {
> +	type => 'array',
> +	items => {
> +	    oneOf => [
> +
> +	    ],

something missing here?

> +	},
> +    },
> +    code => sub {
> +	my ($param) = @_;
> +
> +	my $config = PVE::Network::SDN::Fabrics::get_all_configs();
> +	return $config;
> +    },
> +});
> + 
> +1;
> diff --git a/src/PVE/API2/Network/SDN/Fabrics/Common.pm b/src/PVE/API2/Network/SDN/Fabrics/Common.pm
> new file mode 100644
> index 000000000000..f3042a18090d
> --- /dev/null
> +++ b/src/PVE/API2/Network/SDN/Fabrics/Common.pm
> @@ -0,0 +1,111 @@
> +package PVE::API2::Network::SDN::Fabrics::Common;
> +
> +use strict;
> +use warnings;
> +
> +use PVE::Network::SDN::Fabrics;
> +
> +sub delete_fabric {
> +    my ($type, $param) = @_;
> +
> +    my $fabrics = PVE::Network::SDN::Fabrics::get_config($type);
> +    $fabrics->delete_fabric($param);
> +    PVE::Network::SDN::Fabrics::write_config($fabrics);
> +    return $fabrics->get_inner();
> +}
> +
> +sub delete_node {
> +    my ($type, $param) = @_;
> +
> +    my $fabrics = PVE::Network::SDN::Fabrics::get_config($type);
> +    $fabrics->delete_node($param);
> +    PVE::Network::SDN::Fabrics::write_config($fabrics);
> +    return $fabrics->get_inner();
> +}
> +
> +sub delete_interface {
> +    my ($type, $param) = @_;
> +
> +    my $fabrics = PVE::Network::SDN::Fabrics::get_config($type);
> +    $fabrics->delete_interface($param);
> +    PVE::Network::SDN::Fabrics::write_config($fabrics);
> +    return $fabrics->get_inner();
> +}
> +
> +sub add_node {
> +    my ($type, $param) = @_;
> +
> +    my $fabrics = PVE::Network::SDN::Fabrics::get_config($type);
> +    $fabrics->add_node($param);
> +    PVE::Network::SDN::Fabrics::write_config($fabrics);
> +
> +    return $fabrics->get_inner();
> +}
> +
> +sub add_fabric {
> +    my ($type, $param) = @_;
> +
> +    my $fabrics = PVE::Network::SDN::Fabrics::get_config($type);
> +    $fabrics->add_fabric($param);
> +    PVE::Network::SDN::Fabrics::write_config($fabrics);
> +
> +    return $fabrics->get_inner();
> +}
> +
> +sub get_fabric {
> +    my ($type, $param) = @_;
> +
> +    my $fabrics = PVE::Network::SDN::Fabrics::get_config($type);
> +    my $return_value = $fabrics->get_fabric($param->{fabric});
> +    # Add the fabric id to the return value. The rust return value doesn't contain 
> +    # the fabric name (as it's in the key of the section config hashmap, so we add it here).
> +    $return_value->{fabric}->{name} = $param->{fabric};
> +    return $return_value;
> +}
> +
> +sub get_node {
> +    my ($type, $param) = @_;
> +
> +    my $fabrics = PVE::Network::SDN::Fabrics::get_config($type);
> +    my $return_value = $fabrics->get_node($param->{fabric}, $param->{node});
> +    # Add the node id to the return value. The rust return value doesn't contain 
> +    # the nodename (as it's in the key of the section config hashmap, so we add it here).
> +    $return_value->{node}->{node} = $param->{node};
> +    return $return_value;
> +}
> +
> +sub get_interface {
> +    my ($type, $param) = @_;
> +
> +    my $fabrics = PVE::Network::SDN::Fabrics::get_config($type);
> +    return $fabrics->get_interface($param->{fabric}, $param->{node}, $param->{name});
> +}
> +
> +sub edit_fabric {
> +    my ($type, $param) = @_;
> +
> +    my $fabrics = PVE::Network::SDN::Fabrics::get_config($type);
> +    $fabrics->edit_fabric($param);
> +    PVE::Network::SDN::Fabrics::write_config($fabrics);
> +    return $fabrics->get_inner();
> +}
> +
> +sub edit_node {
> +    my ($type, $param) = @_;
> +
> +    my $fabrics = PVE::Network::SDN::Fabrics::get_config($type);
> +    $fabrics->edit_node($param);
> +    PVE::Network::SDN::Fabrics::write_config($fabrics);
> +    return $fabrics->get_inner();
> +}
> +
> +sub edit_interface {
> +    my ($type, $param) = @_;
> +
> +    my $fabrics = PVE::Network::SDN::Fabrics::get_config($type);
> +    $fabrics->edit_interface($param);
> +    PVE::Network::SDN::Fabrics::write_config($fabrics);
> +    return $fabrics->get_inner();
> +}
> +
> +1;
> diff --git a/src/PVE/API2/Network/SDN/Fabrics/Makefile b/src/PVE/API2/Network/SDN/Fabrics/Makefile
> new file mode 100644
> index 000000000000..e433f2e7d0a6
> --- /dev/null
> +++ b/src/PVE/API2/Network/SDN/Fabrics/Makefile
> @@ -0,0 +1,9 @@
> +SOURCES=OpenFabric.pm Ospf.pm Common.pm
> +
> +
> +PERL5DIR=${DESTDIR}/usr/share/perl5
> +
> +.PHONY: install
> +install:
> +	for i in ${SOURCES}; do install -D -m 0644 $$i ${PERL5DIR}/PVE/API2/Network/SDN/Fabrics/$$i; done
> +
> diff --git a/src/PVE/API2/Network/SDN/Fabrics/OpenFabric.pm b/src/PVE/API2/Network/SDN/Fabrics/OpenFabric.pm
> new file mode 100644
> index 000000000000..626893aa61b7
> --- /dev/null
> +++ b/src/PVE/API2/Network/SDN/Fabrics/OpenFabric.pm
> @@ -0,0 +1,460 @@
> +package PVE::API2::Network::SDN::Fabrics::OpenFabric;
> +
> +use strict;
> +use warnings;
> +
> +use Storable qw(dclone);
> +
> +use PVE::RPCEnvironment;
> +use PVE::Tools qw(extract_param);
> +
> +use PVE::Network::SDN::Fabrics;
> +use PVE::API2::Network::SDN::Fabrics::Common;
> +
> +use PVE::RESTHandler;
> +use base qw(PVE::RESTHandler);
> +
> +__PACKAGE__->register_method({
> +    name => 'delete_fabric',
> +    path => '{fabric}',
> +    method => 'DELETE',
> +    description => 'Delete SDN Fabric',
> +    protected => 1,
> +    permissions => {
> +	check => ['perm', '/sdn/fabrics/openfabric/{fabric}', [ 'SDN.Allocate' ]],
> +    },
> +    parameters => {
> +    	additionalProperties => 1,
> +	properties => {
> +	    fabric => {
> +		type => 'string',
> +	    },
> +	},
> +    },
> +    returns => {
> +	type => 'object',
> +	properties => {
> +	    data => {
> +		type => 'object',
> +	    }
> +	}
> +    },
> +    code => sub {
> +	my ($param) = @_;
> +
> +	return PVE::API2::Network::SDN::Fabrics::Common::delete_fabric("openfabric", $param);
> +    },
> +});
> +
> +__PACKAGE__->register_method({
> +    name => 'delete_node',
> +    path => '{fabric}/node/{node}',
> +    method => 'DELETE',
> +    description => 'Delete SDN Fabric Node',
> +    protected => 1,
> +    permissions => {
> +	check => ['perm', '/sdn/fabrics/openfabric/{fabric}/node/{node}', [ 'SDN.Allocate' ]],
> +    },
> +    parameters => {
> +    	additionalProperties => 1,
> +	properties => {
> +	    fabric => {
> +		type => 'string',
> +	    },
> +	    node => {
> +		type => 'string',
> +	    },
> +	},
> +    },
> +    returns => {
> +	type => 'object',
> +	properties => {
> +	    data => {
> +		type => 'object',
> +	    }
> +	}
> +    },
> +    code => sub {
> +	my ($param) = @_;
> +
> +	return PVE::API2::Network::SDN::Fabrics::Common::delete_node("openfabric", $param);
> +    },
> +});
> +
> +__PACKAGE__->register_method({
> +    name => 'delete_interface',
> +    path => '{fabric}/node/{node}/interface/{name}',
> +    method => 'DELETE',
> +    description => 'Delete SDN Fabric Node Interface',
> +    protected => 1,
> +    permissions => {
> +	check => ['perm', '/sdn/fabrics/openfabric/{fabric}/node/{node}/interface/{name}', [ 'SDN.Allocate' ]],
> +    },
> +    parameters => {
> +    	additionalProperties => 1,
> +	properties => {
> +	    fabric => {
> +		type => 'string',
> +	    },
> +	    node => {
> +		type => 'string',
> +	    },
> +	    name => {
> +		type => 'string',
> +	    },
> +	},
> +    },
> +    returns => {
> +	type => 'object',
> +    },
> +    code => sub {
> +	my ($param) = @_;
> +
> +	return PVE::API2::Network::SDN::Fabrics::Common::delete_interface("openfabric", $param);
> +    },
> +});
> +
> +__PACKAGE__->register_method({
> +    name => 'update_fabric',
> +    path => '{fabric}',
> +    method => 'PUT',
> +    description => 'Update SDN Fabric configuration',
> +    protected => 1,
> +    permissions => {
> +	check => ['perm', '/sdn/fabrics/openfabric/{fabric}', [ 'SDN.Allocate' ]],
> +    },
> +    parameters => {
> +	additionalProperties => 1,
> +	properties => {
> +	    fabric => {
> +		type => 'string'
> +	    },
> +	    hello_interval => {
> +		type => 'integer',
> +	    },
> +	},
> +    },
> +    returns => {
> +	type => 'object',
> +	properties => {
> +	    data => {
> +		type => 'object',
> +	    }
> +	}
> +    },
> +    code => sub {
> +	my ($param) = @_;
> +
> +	return PVE::API2::Network::SDN::Fabrics::Common::edit_fabric("openfabric", $param);
> +    },
> +});
> +
> +__PACKAGE__->register_method({
> +    name => 'update_node',
> +    path => '{fabric}/node/{node}',
> +    method => 'PUT',
> +    description => 'Update SDN Fabric Node configuration',
> +    protected => 1,
> +    permissions => {
> +	check => ['perm', '/sdn/fabrics/openfabric/{fabric}/node/{node}', [ 'SDN.Allocate' ]],
> +    },
> +    parameters => {
> +	additionalProperties => 1,
> +	properties => {
> +	    fabric => {
> +		type => 'string',
> +	    },
> +	    node => {
> +		type => 'string',
> +	    },
> +	    net => {
> +		type => 'string',
> +	    },
> +	    interfaces => {
> +		type => 'array',
> +		items => {
> +		    type => 'string',

you can use the additional format parameter for documenting property
strings

> +		},
> +	    },
> +	},
> +    },
> +    returns => {
> +	type => 'object',
> +	properties => {
> +	    data => {
> +		type => 'object',
> +	    }
> +	}
> +    },
> +    code => sub {
> +	my ($param) = @_;
> +
> +	return PVE::API2::Network::SDN::Fabrics::Common::edit_node("openfabric", $param);
> +    },
> +});
> +
> +__PACKAGE__->register_method({
> +    name => 'update_interface',
> +    path => '{fabric}/node/{node}/interface/{name}',
> +    method => 'PUT',
> +    description => 'Update SDN Fabric Interface configuration',
> +    protected => 1,
> +    permissions => {
> +	check => ['perm', '/sdn/fabrics/openfabric/{fabric}/node/{node}/interface/{name}', [ 'SDN.Allocate' ]],
> +    },
> +    parameters => {
> +       additionalProperties => 1,
> +	properties => {
> +	    fabric => {
> +		type => 'string',
> +	    },
> +	    node => {
> +		type => 'string',
> +	    },
> +	    name => {
> +		type => 'string',
> +	    },
> +	    passive => {
> +		type => 'boolean',
> +	    },
> +	    hello_interval => {
> +		optional => 1,
> +		type => 'string',
> +	    },
> +	    hello_multiplier => {
> +		optional => 1,
> +		type => 'string',
> +	    },
> +	    csnp_interval => {
> +		optional => 1,
> +		type => 'string',
> +	    },

should we turn those into numbers as well? in get_interface they're
types as numbers afaict.

> +	},
> +    },
> +    returns => {
> +	type => 'object',
> +	properties => {
> +	    data => {
> +		type => 'object',
> +	    }
> +	}
> +    },
> +    code => sub {
> +	my ($param) = @_;
> +
> +	return PVE::API2::Network::SDN::Fabrics::Common::edit_interface("openfabric", $param);
> +    },
> +});
> +
> +
> +__PACKAGE__->register_method({
> +    name => 'get_fabric',
> +    path => '{fabric}',
> +    method => 'GET',
> +    description => 'Get SDN Fabric configuration',
> +    permissions => {
> +	check => ['perm', '/sdn/fabrics/openfabric/{fabric}', [ 'SDN.Allocate' ]],
> +    },
> +    parameters => {
> +    	additionalProperties => 1,
> +	properties => {}
> +    },
> +    returns => {
> +	type => 'object',
> +	properties => {
> +	    data => {
> +		type => 'object',
> +		properties => {
> +		    fabric => {
> +			type => 'object',
> +			properties => {
> +			    name => {
> +				type => 'string'
> +			    }
> +			}
> +		    }
> +		}
> +	    }
> +	}
> +    },
> +    code => sub {
> +	my ($param) = @_;
> +
> +	return PVE::API2::Network::SDN::Fabrics::Common::get_fabric("openfabric", $param);
> +    },
> +});
> +
> +__PACKAGE__->register_method({
> +    name => 'get_node',
> +    path => '{fabric}/node/{node}',
> +    method => 'GET',
> +    description => 'Get SDN Fabric Node configuration',
> +    permissions => {
> +	check => ['perm', '/sdn/fabrics/openfabric/{fabric}', [ 'SDN.Allocate' ]],
> +    },
> +    parameters => {
> +    	additionalProperties => 1,
> +       properties => {}
> +    },
> +    returns => {
> +	type => 'object',
> +	properties => {
> +	    data => {
> +		type => 'object',
> +		properties => {
> +		    node => {
> +			type => 'object',
> +			properties => {
> +			    net => {
> +				type => 'string',
> +			    },
> +			    node => {
> +				type => 'string',
> +			    },
> +			    interface => {
> +				type => 'array',
> +				items => {
> +				    type => 'string'
> +				}
> +			    },
> +			}
> +		    }
> +		}
> +	    }
> +	}
> +    },
> +    code => sub {
> +	my ($param) = @_;
> +
> +	return PVE::API2::Network::SDN::Fabrics::Common::get_node("openfabric", $param);
> +    },
> +});
> +
> +
> +__PACKAGE__->register_method({
> +    name => 'get_interface',
> +    path => '{fabric}/node/{node}/interface/{name}',
> +    method => 'GET',
> +    description => 'Get SDN Fabric Interface configuration',
> +    permissions => {
> +	check => ['perm', '/sdn/fabrics/openfabric/{fabric}', [ 'SDN.Allocate' ]],
> +    },
> +    parameters => {
> +       additionalProperties => 1,
> +    },
> +    returns => {
> +	type => 'object',
> +	properties => {
> +	    data => {
> +		type => 'object',
> +		properties => {
> +		    name => {
> +			type => 'string',
> +		    },
> +		    passive => {
> +			type => 'boolean',
> +		    },
> +		    hello_interval => {
> +			optional => 1,
> +			type => 'number',
> +		    },
> +		    hello_multiplier => {
> +			optional => 1,
> +			type => 'number',
> +		    },
> +		    csnp_interval => {
> +			optional => 1,
> +			type => 'number',
> +		    },
> +		}
> +	    }
> +	}
> +    },
> +    code => sub {
> +	my ($param) = @_;
> +
> +	return PVE::API2::Network::SDN::Fabrics::Common::get_interface("openfabric", $param);
> +    },
> +});
> +
> +
> +__PACKAGE__->register_method({
> +    name => 'add_fabric',
> +    path => '/',
> +    method => 'POST',
> +    description => 'Create SDN Fabric configuration',
> +    protected => 1,
> +    permissions => {
> +	check => ['perm', '/sdn/fabrics/openfabric', [ 'SDN.Allocate' ]],
> +    },
> +    parameters => {
> +    	additionalProperties => 1,
> +	properties => {
> +	    "type" => {
> +		type => 'string',
> +	    },
> +	    "name" => {
> +		type => 'string',
> +	    },
> +	},
> +    },
> +    returns => {
> +	type => 'object',
> +	properties => {
> +	    data => {
> +		type => 'object',
> +	    }
> +	}
> +    },
> +    code => sub {
> +	my ($param) = @_;
> +
> +	return PVE::API2::Network::SDN::Fabrics::Common::add_fabric("openfabric", $param);
> +    },
> +});
> +
> +__PACKAGE__->register_method({
> +    name => 'add_node',
> +    path => '{fabric}/node/{node}',
> +    method => 'POST',
> +    description => 'Create SDN Fabric Node configuration',
> +    protected => 1,
> +    permissions => {
> +	check => ['perm', '/sdn/fabrics/openfabric/{fabric}/node/{node}', [ 'SDN.Allocate' ]],
> +    },
> +    parameters => {
> +    	additionalProperties => 1,
> +	properties => {
> +	    fabric => {
> +		type => 'string',
> +	    },
> +	    node => {
> +		type => 'string',
> +	    },
> +	    net => {
> +		type => 'string',
> +	    },
> +	    interfaces => {
> +		type => 'array',
> +		items => {
> +		    type => 'string',
> +		},
> +	    },
> +	},
> +    },
> +    returns => {
> +	type => 'object',
> +	properties => {
> +	    data => {
> +		type => 'object',
> +	    }
> +	}
> +    },
> +    code => sub {
> +	my ($param) = @_;
> +
> +	return PVE::API2::Network::SDN::Fabrics::Common::add_node("openfabric", $param);
> +    },
> +});
> +
> +1;
> diff --git a/src/PVE/API2/Network/SDN/Fabrics/Ospf.pm b/src/PVE/API2/Network/SDN/Fabrics/Ospf.pm
> new file mode 100644
> index 000000000000..309d29788667
> --- /dev/null
> +++ b/src/PVE/API2/Network/SDN/Fabrics/Ospf.pm
> @@ -0,0 +1,433 @@
> +package PVE::API2::Network::SDN::Fabrics::Ospf;
> +
> +use strict;
> +use warnings;
> +
> +use Storable qw(dclone);
> +
> +use PVE::RPCEnvironment;
> +use PVE::Tools qw(extract_param);
> +
> +use PVE::Network::SDN::Fabrics;
> +use PVE::API2::Network::SDN::Fabrics::Common;
> +
> +use PVE::RESTHandler;
> +use base qw(PVE::RESTHandler);
> +
> +__PACKAGE__->register_method({
> +    name => 'delete_fabric',
> +    path => '{fabric}',
> +    method => 'DELETE',
> +    description => 'Delete SDN Fabric',
> +    protected => 1,
> +    permissions => {
> +	check => ['perm', '/sdn/fabrics/ospf/{fabric}', [ 'SDN.Allocate' ]],
> +    },
> +    parameters => {
> +    	additionalProperties => 1,
> +	properties => {
> +	    fabric => {
> +		type => 'string',
> +	    },
> +	},
> +    },
> +    returns => {
> +	type => 'object',
> +	properties => {
> +	    data => {
> +		type => 'object',
> +	    }
> +	}
> +    },
> +    code => sub {
> +	my ($param) = @_;
> +
> +	return PVE::API2::Network::SDN::Fabrics::Common::delete_fabric("ospf", $param);
> +    },
> +});
> +
> +__PACKAGE__->register_method({
> +    name => 'delete_node',
> +    path => '{fabric}/node/{node}',
> +    method => 'DELETE',
> +    description => 'Delete SDN Fabric Node',
> +    protected => 1,
> +    permissions => {
> +	check => ['perm', '/sdn/fabrics/ospf/{fabric}/node/{node}', [ 'SDN.Allocate' ]],
> +    },
> +    parameters => {
> +    	additionalProperties => 1,
> +	properties => {
> +	    fabric => {
> +		type => 'string',
> +	    },
> +	    node => {
> +		type => 'string',
> +	    },
> +	},
> +    },
> +    returns => {
> +	type => 'object',
> +	properties => {
> +	    data => {
> +		type => 'object',
> +	    }
> +	}
> +    },
> +    code => sub {
> +	my ($param) = @_;
> +
> +	return PVE::API2::Network::SDN::Fabrics::Common::delete_node("ospf", $param);
> +    },
> +});
> +
> +__PACKAGE__->register_method({
> +    name => 'delete_interface',
> +    path => '{fabric}/node/{node}/interface/{name}',
> +    method => 'DELETE',
> +    description => 'Delete SDN Fabric Node Interface',
> +    protected => 1,
> +    permissions => {
> +	check => ['perm', '/sdn/fabrics/ospf/{fabric}/node/{node}/interface/{name}', [ 'SDN.Allocate' ]],
> +    },
> +    parameters => {
> +    	additionalProperties => 1,
> +	properties => {
> +	    fabric => {
> +		type => 'string',
> +	    },
> +	    node => {
> +		type => 'string',
> +	    },
> +	    name => {
> +		type => 'string',
> +	    },
> +	},
> +    },
> +    returns => {
> +	type => 'null',
> +    },
> +    code => sub {
> +	my ($param) = @_;
> +
> +	return PVE::API2::Network::SDN::Fabrics::Common::delete_interface("ospf", $param);
> +    },
> +});
> +
> +__PACKAGE__->register_method({
> +    name => 'update_fabric',
> +    path => '{fabric}',
> +    method => 'PUT',
> +    description => 'Update SDN Fabric configuration',
> +    protected => 1,
> +    permissions => {
> +	check => ['perm', '/sdn/fabrics/ospf/{fabric}', [ 'SDN.Allocate' ]],
> +    },
> +    parameters => {
> +	additionalProperties => 1,
> +	properties => {
> +	    fabric => {
> +		type => 'string'
> +	    },
> +	},
> +    },
> +    returns => {
> +	type => 'object',
> +	properties => {
> +	    data => {
> +		type => 'object',
> +	    }
> +	}
> +    },
> +    code => sub {
> +	my ($param) = @_;
> +
> +	return PVE::API2::Network::SDN::Fabrics::Common::edit_fabric("ospf", $param);
> +    },
> +});
> +
> +__PACKAGE__->register_method({
> +    name => 'update_node',
> +    path => '{fabric}/node/{node}',
> +    method => 'PUT',
> +    description => 'Update SDN Fabric Interface configuration',
> +    protected => 1,
> +    permissions => {
> +	check => ['perm', '/sdn/fabrics/ospf/{fabric}', [ 'SDN.Allocate' ]],
> +    },
> +    parameters => {
> +	additionalProperties => 1,
> +	properties => {
> +	    fabric => {
> +		type => 'string',
> +	    },
> +	    node => {
> +		type => 'string',
> +	    },
> +	    router_id => {
> +		type => 'string',
> +	    },
> +	    interfaces => {
> +		type => 'array',
> +		items => {
> +		    type => 'string',
> +		},
> +	    },
> +	},
> +    },
> +    returns => {
> +	type => 'object',
> +	properties => {
> +	    data => {
> +		type => 'object',
> +	    }
> +	}
> +    },
> +    code => sub {
> +	my ($param) = @_;
> +
> +	return PVE::API2::Network::SDN::Fabrics::Common::edit_node("ospf", $param);
> +    },
> +});
> +
> +__PACKAGE__->register_method({
> +    name => 'update_interface',
> +    path => '{fabric}/node/{node}/interface/{name}',
> +    method => 'PUT',
> +    description => 'Update SDN Fabric Interface configuration',
> +    protected => 1,
> +    permissions => {
> +	check => ['perm', '/sdn/fabrics/ospf/{fabric}/node/{node}/interface/{name}', [ 'SDN.Allocate' ]],
> +    },
> +    parameters => {
> +       additionalProperties => 1,
> +	properties => {
> +	    fabric => {
> +		type => 'string',
> +	    },
> +	    node => {
> +		type => 'string',
> +	    },
> +	    name => {
> +		type => 'string',
> +	    },
> +	    passive => {
> +		type => 'boolean',
> +	    },
> +	},
> +    },
> +    returns => {
> +	type => 'object',
> +	properties => {
> +	    data => {
> +		type => 'object',
> +	    }
> +	}
> +    },
> +    code => sub {
> +	my ($param) = @_;
> +
> +	return PVE::API2::Network::SDN::Fabrics::Common::edit_interface("ospf", $param);
> +    },
> +});
> +
> +__PACKAGE__->register_method({
> +    name => 'get_fabric',
> +    path => '{fabric}',
> +    method => 'GET',
> +    description => 'Get SDN Fabric configuration',
> +    permissions => {
> +	check => ['perm', '/sdn/fabrics/ospf/{fabric}', [ 'SDN.Allocate' ]],
> +    },
> +    parameters => {
> +	additionalProperties => 1,
> +	properties => {}
> +    },
> +    returns => {
> +	type => 'object',
> +	properties => {
> +	    data => {
> +		type => 'object',
> +		properties => {
> +		    fabric => {
> +			type => 'object',
> +			properties => {
> +			    name => {
> +				type => 'string'
> +			    }
> +			}
> +		    }
> +		}
> +	    }
> +	}
> +    },
> +    code => sub {
> +	my ($param) = @_;
> +
> +	return PVE::API2::Network::SDN::Fabrics::Common::get_fabric("ospf", $param);
> +    },
> +});
> +
> +__PACKAGE__->register_method({
> +    name => 'get_node',
> +    path => '{fabric}/node/{node}',
> +    method => 'GET',
> +    description => 'Get SDN Fabric Node configuration',
> +    permissions => {
> +	check => ['perm', '/sdn/fabrics/ospf/{fabric}', [ 'SDN.Allocate' ]],
> +    },
> +    parameters => {
> +    	additionalProperties => 1,
> +       properties => {}
> +    },
> +    returns => {
> +	type => 'object',
> +	properties => {
> +	    data => {
> +		type => 'object',
> +		properties => {
> +		    node => {
> +			type => 'object',
> +			properties => {
> +			    router_id => {
> +				type => 'string',
> +			    },
> +			    node => {
> +				type => 'string',
> +			    },
> +			    interface => {
> +				type => 'array',
> +				items => {
> +				    type => 'string'
> +				}
> +			    },
> +			}
> +		    }
> +		}
> +	    }
> +	}
> +    },
> +    code => sub {
> +	my ($param) = @_;
> +
> +	return PVE::API2::Network::SDN::Fabrics::Common::get_node("ospf", $param);
> +    },
> +});
> +
> +
> +__PACKAGE__->register_method({
> +    name => 'get_interface',
> +    path => '{fabric}/node/{node}/interface/{name}',
> +    method => 'GET',
> +    description => 'Get SDN Fabric Interface configuration',
> +    permissions => {
> +	check => ['perm', '/sdn/fabrics/ospf/{fabric}', [ 'SDN.Allocate' ]],
> +    },
> +    parameters => {
> +       additionalProperties => 1,
> +    },
> +    returns => {
> +	type => 'object',
> +	properties => {
> +	    data => {
> +		type => 'object',
> +		properties => {
> +		    name => {
> +			type => 'string',
> +		    },
> +		    passive => {
> +			type => 'boolean',
> +		    },
> +		}
> +	    }
> +	}
> +    },
> +    code => sub {
> +	my ($param) = @_;
> +
> +	return PVE::API2::Network::SDN::Fabrics::Common::get_interface("ospf", $param);
> +    },
> +});
> +
> +
> +__PACKAGE__->register_method({
> +    name => 'add_fabric',
> +    path => '/',
> +    method => 'POST',
> +    description => 'Create SDN Fabric configuration',
> +    protected => 1,
> +    permissions => {
> +	check => ['perm', '/sdn/fabrics/ospf', [ 'SDN.Allocate' ]],
> +    },
> +    parameters => {
> +    	additionalProperties => 1,
> +	properties => {
> +	    "type" => {
> +		type => 'string',
> +	    },
> +	    "name" => {
> +		type => 'string',
> +	    },
> +	},
> +    },
> +    returns => {
> +	type => 'object',
> +	properties => {
> +	    data => {
> +		type => 'object',
> +	    }
> +	}
> +    },
> +    code => sub {
> +	my ($param) = @_;
> +
> +	return PVE::API2::Network::SDN::Fabrics::Common::add_fabric("ospf", $param);
> +    },
> +});
> +
> +__PACKAGE__->register_method({
> +    name => 'add_node',
> +    path => '{fabric}/node/{node}',
> +    method => 'POST',
> +    description => 'Create SDN Fabric Node configuration',
> +    protected => 1,
> +    permissions => {
> +	check => ['perm', '/sdn/fabrics/ospf/{fabric}/node/{node}', [ 'SDN.Allocate' ]],
> +    },
> +    parameters => {
> +    	additionalProperties => 1,
> +	properties => {
> +	    fabric => {
> +		type => 'string',
> +	    },
> +	    node => {
> +		type => 'string',
> +	    },
> +	    router_id => {
> +		type => 'string',
> +	    },
> +	    interfaces => {
> +		type => 'array',
> +		items => {
> +		    type => 'string',
> +		},
> +	    },
> +	},
> +    },
> +    returns => {
> +	type => 'object',
> +	properties => {
> +	    data => {
> +		type => 'object',
> +	    }
> +	}
> +    },
> +    code => sub {
> +	my ($param) = @_;
> +
> +	return PVE::API2::Network::SDN::Fabrics::Common::add_node("ospf", $param);
> +    },
> +});
> +
> +
> +1;
> diff --git a/src/PVE/API2/Network/SDN/Makefile b/src/PVE/API2/Network/SDN/Makefile
> index abd1bfae020e..08bec7535530 100644
> --- a/src/PVE/API2/Network/SDN/Makefile
> +++ b/src/PVE/API2/Network/SDN/Makefile
> @@ -1,4 +1,4 @@
> -SOURCES=Vnets.pm Zones.pm Controllers.pm Subnets.pm Ipams.pm Dns.pm Ips.pm
> +SOURCES=Vnets.pm Zones.pm Controllers.pm Subnets.pm Ipams.pm Dns.pm Ips.pm Fabrics.pm
>  
>  
>  PERL5DIR=${DESTDIR}/usr/share/perl5
> @@ -7,4 +7,5 @@ PERL5DIR=${DESTDIR}/usr/share/perl5
>  install:
>  	for i in ${SOURCES}; do install -D -m 0644 $$i ${PERL5DIR}/PVE/API2/Network/SDN/$$i; done
>  	make -C Zones install
> +	make -C Fabrics install
>  



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


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

* Re: [pve-devel] [PATCH pve-manager 09/11] sdn: add Fabrics view
  2025-02-14 13:39 ` [pve-devel] [PATCH pve-manager 09/11] sdn: add Fabrics view Gabriel Goller
@ 2025-03-04  9:57   ` Stefan Hanreich
  2025-03-07 15:57     ` Gabriel Goller
  0 siblings, 1 reply; 32+ messages in thread
From: Stefan Hanreich @ 2025-03-04  9:57 UTC (permalink / raw)
  To: Gabriel Goller, pve-devel


On 2/14/25 14:39, Gabriel Goller wrote:
> Add the FabricsView in the sdn category of the datacenter view. The
> FabricsView allows to show all the fabrics on all the nodes of the
> cluster.
> 
> Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
> Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
> ---
>  PVE/API2/Cluster.pm             |   7 +-
>  PVE/API2/Network.pm             |   7 +-
>  www/manager6/.lint-incremental  |   0
>  www/manager6/Makefile           |   8 +
>  www/manager6/dc/Config.js       |   8 +
>  www/manager6/sdn/FabricsView.js | 359 ++++++++++++++++++++++++++++++++
>  6 files changed, 379 insertions(+), 10 deletions(-)
>  create mode 100644 www/manager6/.lint-incremental
>  create mode 100644 www/manager6/sdn/FabricsView.js
> 
> diff --git a/PVE/API2/Cluster.pm b/PVE/API2/Cluster.pm
> index a0e5c11b6e8e..7730aab82a25 100644
> --- a/PVE/API2/Cluster.pm
> +++ b/PVE/API2/Cluster.pm
> @@ -35,11 +35,8 @@ use PVE::API2::Firewall::Cluster;
>  use PVE::API2::HAConfig;
>  use PVE::API2::ReplicationConfig;
>  
> -my $have_sdn;
> -eval {
> -    require PVE::API2::Network::SDN;
> -    $have_sdn = 1;
> -};
> +my $have_sdn = 1;
> +require PVE::API2::Network::SDN;
>  
>  use base qw(PVE::RESTHandler);
>  
> diff --git a/PVE/API2/Network.pm b/PVE/API2/Network.pm
> index cfccdd9e3da3..3c45fe2fb7bf 100644
> --- a/PVE/API2/Network.pm
> +++ b/PVE/API2/Network.pm
> @@ -16,11 +16,8 @@ use IO::File;
>  
>  use base qw(PVE::RESTHandler);
>  
> -my $have_sdn;
> -eval {
> -    require PVE::Network::SDN;
> -    $have_sdn = 1;
> -};
> +my $have_sdn = 1;
> +require PVE::Network::SDN;
>  
>  my $iflockfn = "/etc/network/.pve-interfaces.lock";
>  
> diff --git a/www/manager6/.lint-incremental b/www/manager6/.lint-incremental
> new file mode 100644
> index 000000000000..e69de29bb2d1
> diff --git a/www/manager6/Makefile b/www/manager6/Makefile
> index c94a5cdfbf70..224b6079e833 100644
> --- a/www/manager6/Makefile
> +++ b/www/manager6/Makefile
> @@ -303,6 +303,14 @@ JSSRC= 							\
>  	sdn/zones/SimpleEdit.js				\
>  	sdn/zones/VlanEdit.js				\
>  	sdn/zones/VxlanEdit.js				\
> +	sdn/FabricsView.js				\
> +	sdn/fabrics/Common.js				\
> +	sdn/fabrics/openfabric/FabricEdit.js		\
> +	sdn/fabrics/openfabric/NodeEdit.js		\
> +	sdn/fabrics/openfabric/InterfaceEdit.js		\
> +	sdn/fabrics/ospf/FabricEdit.js			\
> +	sdn/fabrics/ospf/NodeEdit.js			\
> +	sdn/fabrics/ospf/InterfaceEdit.js		\
>  	storage/ContentView.js				\
>  	storage/BackupView.js				\
>  	storage/Base.js					\
> diff --git a/www/manager6/dc/Config.js b/www/manager6/dc/Config.js
> index 74728c8320e9..68f7be8d6042 100644
> --- a/www/manager6/dc/Config.js
> +++ b/www/manager6/dc/Config.js
> @@ -229,6 +229,14 @@ Ext.define('PVE.dc.Config', {
>  		    hidden: true,
>  		    iconCls: 'fa fa-shield',
>  		    itemId: 'sdnfirewall',
> +		},
> +		{
> +		    xtype: 'pveSDNFabricView',
> +		    groups: ['sdn'],
> +		    title: gettext('Fabrics'),
> +		    hidden: true,
> +		    iconCls: 'fa fa-road',
> +		    itemId: 'sdnfabrics',
>  		});
>  	    }
>  
> diff --git a/www/manager6/sdn/FabricsView.js b/www/manager6/sdn/FabricsView.js
> new file mode 100644
> index 000000000000..f090ee894b75
> --- /dev/null
> +++ b/www/manager6/sdn/FabricsView.js
> @@ -0,0 +1,359 @@
> +const FABRIC_PANELS = {
> +    'openfabric': 'PVE.sdn.Fabric.OpenFabric.Fabric.Edit',
> +    'ospf': 'PVE.sdn.Fabric.Ospf.Fabric.Edit',
> +};
> +
> +const NODE_PANELS = {
> +    'openfabric': 'PVE.sdn.Fabric.OpenFabric.Node.Edit',
> +    'ospf': 'PVE.sdn.Fabric.Ospf.Node.Edit',
> +};
> +
> +const INTERFACE_PANELS = {
> +    'openfabric': 'PVE.sdn.Fabric.OpenFabric.Interface.Edit',
> +    'ospf': 'PVE.sdn.Fabric.Ospf.Interface.Edit',
> +};
> +
> +Ext.define('PVE.sdn.Fabric.View', {
> +    extend: 'Ext.tree.Panel',
> +
> +    xtype: 'pveSDNFabricView',
> +
> +    columns: [
> +	{
> +	    xtype: 'treecolumn',
> +	    text: gettext('Name'),
> +	    dataIndex: 'name',
> +	    width: 200,
> +	},
> +	{
> +	    text: gettext('Identifier'),
> +	    dataIndex: 'identifier',
> +	    width: 200,
> +	},
> +	{
> +	    text: gettext('Action'),
> +	    xtype: 'actioncolumn',
> +	    dataIndex: 'text',
> +	    width: 150,
> +	    items: [
> +		{
> +		    handler: 'addAction',
> +		    getTip: (_v, _m, _rec) => gettext('Add'),
> +		    getClass: (_v, _m, { data }) => {
> +			if (data.type === 'fabric') {
> +			    return 'fa fa-plus-square';
> +			}
> +
> +			return 'pmx-hidden';
> +		    },
> +		    isActionDisabled: (_v, _r, _c, _i, { data }) => data.type !== 'fabric',
> +		},
> +		{
> +		    tooltip: gettext('Edit'),
> +		    handler: 'editAction',
> +		    getClass: (_v, _m, { data }) => {
> +			// the fabric type (openfabric, ospf, etc.) cannot be edited
> +			if (data.type) {
> +			    return 'fa fa-pencil fa-fw';
> +			}
> +
> +			return 'pmx-hidden';
> +		    },
> +		    isActionDisabled: (_v, _r, _c, _i, { data }) => !data.type,
> +		},
> +		{
> +		    tooltip: gettext('Delete'),
> +		    handler: 'deleteAction',
> +		    getClass: (_v, _m, { data }) => {
> +			// the fabric type (openfabric, ospf, etc.) cannot be deleted
> +			if (data.type) {
> +			    return 'fa critical fa-trash-o';
> +			}
> +
> +			return 'pmx-hidden';
> +		    },
> +		    isActionDisabled: (_v, _r, _c, _i, { data }) => !data.type,
> +		},
> +	    ],
> +	},
> +    ],
> +
> +    store: {
> +	sorters: ['name'],
> +    },
> +
> +    layout: 'fit',
> +    rootVisible: false,
> +    animate: false,
> +
> +    tbar: [
> +	{
> +	    text: gettext('Add Fabric'),
> +	    menu: [
> +		{
> +		    text: gettext('OpenFabric'),
> +		    handler: 'openAddOpenFabricWindow',
> +		},
> +		{
> +		    text: gettext('OSPF'),
> +		    handler: 'openAddOspfWindow',
> +		},
> +	    ],
> +	},
> +	{
> +	    xtype: 'proxmoxButton',
> +	    text: gettext('Reload'),
> +	    handler: 'reload',
> +	},
> +    ],
> +
> +    controller: {
> +	xclass: 'Ext.app.ViewController',
> +
> +	reload: function() {
> +	    let me = this;
> +
> +	    Proxmox.Utils.API2Request({
> +		url: `/cluster/sdn/fabrics/`,
> +		method: 'GET',
> +		success: function(response, opts) {
> +		    let ospf = Object.entries(response.result.data.ospf);
> +		    let openfabric = Object.entries(response.result.data.openfabric);
> +
> +		    // add some metadata so we can merge the objects later and still know the protocol/type
> +		    ospf = ospf.map(x => {
> +			if (x["1"].fabric) {
> +			    return Object.assign(x["1"].fabric, { _protocol: "ospf", _type: "fabric", name: x["0"] });
> +			} else if (x["1"].node) {
> +			    let id = x["0"].split("_");

I think we already talked about this, but I don't really remember the
outcome. Can we return this already from the API so we don't have to
parse it in the frontend?

> +			    return Object.assign(x["1"].node,
> +				{
> +				    _protocol: "ospf",
> +				    _type: "node",
> +				    node: id[1],
> +				    fabric: id[0],
> +				},
> +			    );
> +			} else {
> +			    return x;
> +			}
> +		    });
> +		    openfabric = openfabric.map(x => {
> +			if (x["1"].fabric) {
> +			    return Object.assign(x["1"].fabric, { _protocol: "openfabric", _type: "fabric", name: x["0"] });
> +			} else if (x["1"].node) {
> +			    let id = x["0"].split("_");
> +			    return Object.assign(x["1"].node,
> +				{
> +				    _protocol: "openfabric",
> +				    _type: "node",
> +				    node: id[1],
> +				    fabric: id[0],
> +				},
> +			    );
> +			} else {
> +			    return x;
> +			}
> +		    });
> +
> +		    let data = {};
> +		    data.ospf = ospf;
> +		    data.openfabric = openfabric;
> +
> +		    let fabrics = Object.entries(data).map((protocol) => {
> +			let protocol_entry = {};
> +			protocol_entry.children = protocol["1"].filter(e => e._type === "fabric").map(fabric => {
> +			    fabric.children = protocol["1"].filter(e => e._type === "node")
> +				.filter((node) =>
> +				    node.fabric === fabric.name && node._protocol === fabric._protocol)
> +					.map((node) => {
> +					    node.children = node.interface
> +						.map((nic) => {
> +							let parsed = PVE.Parser.parsePropertyString(nic);
> +							parsed.leaf = true;
> +							parsed.type = 'interface';
> +							// Add meta information that we need to edit and remove
> +							parsed._protocol = node._protocol;
> +							parsed._fabric = fabric.name;
> +							parsed._node = node.node;
> +							parsed.iconCls = 'x-tree-icon-none';
> +							return parsed;
> +						});
> +
> +						node.expanded = true;
> +						node.type = 'node';
> +						node.name = node.node;
> +						node._fabric = fabric.name;
> +						node.identifier = node.net || node.router_id;
> +						node.iconCls = 'fa fa-desktop x-fa-treepanel';
> +
> +						return node;
> +					});
> +
> +					fabric.type = 'fabric';
> +					fabric.expanded = true;
> +					fabric.iconCls = 'fa fa-road x-fa-treepanel';
> +
> +					return fabric;
> +				});
> +				protocol_entry.name = protocol["0"];
> +				protocol_entry.expanded = true;
> +				return protocol_entry;
> +			});
> +
> +			me.getView().setRootNode({
> +			    name: '__root',
> +			    expanded: true,
> +			    children: fabrics,
> +			});
> +		},
> +	    });
> +	},
> +
> +	getFabricEditPanel: function(type) {
> +	    return FABRIC_PANELS[type];
> +	},
> +
> +	getNodeEditPanel: function(type) {
> +	    return NODE_PANELS[type];
> +	},
> +
> +	getInterfaceEditPanel: function(type) {
> +	    return INTERFACE_PANELS[type];
> +	},
> +
> +	addAction: function(_grid, _rI, _cI, _item, _e, rec) {
> +	    let me = this;
> +
> +	    let component = me.getNodeEditPanel(rec.data._protocol);
> +
> +	    if (!component) {
> +		console.warn(`unknown protocol ${rec.data._protocol}`);
> +		return;
> +	    }
> +
> +	    let extraRequestParams = {
> +		type: rec.data.type,
> +		protocol: rec.data._protocol,
> +		fabric: rec.data.name,
> +	    };
> +
> +	    let window = Ext.create(component, {
> +		autoShow: true,
> +		isCreate: true,
> +		autoLoad: false,
> +		extraRequestParams,
> +	    });
> +
> +	    window.on('destroy', () => me.reload());
> +	},
> +
> +	editAction: function(_grid, _rI, _cI, _item, _e, rec) {
> +	    let me = this;
> +
> +	    let component = '';
> +	    let url = '';
> +	    let autoLoad = true;
> +
> +	    if (rec.data.type === 'fabric') {
> +		component = me.getFabricEditPanel(rec.data._protocol);
> +		url = `/cluster/sdn/fabrics/${rec.data._protocol}/${rec.data.name}`;
> +	    } else if (rec.data.type === 'node') {
> +		component = me.getNodeEditPanel(rec.data._protocol);
> +		// no url, every request is done manually
> +		url = `/cluster/sdn/fabrics/${rec.data._protocol}/${rec.data._fabric}/node/${rec.data.node}`;
> +		autoLoad = false;
> +	    } else if (rec.data.type === 'interface') {
> +		component = me.getInterfaceEditPanel(rec.data._protocol);
> +		url = `/cluster/sdn/fabrics/${rec.data._protocol}/${rec.data._fabric}/node\
> +		/${rec.data._node}/interface/${rec.data.name}`;
> +	    }
> +
> +	    if (!component) {
> +		console.warn(`unknown protocol ${rec.data._protocol} or unknown type ${rec.data.type}`);
> +		return;
> +	    }
> +
> +	    let window = Ext.create(component, {
> +		autoShow: true,
> +		autoLoad: autoLoad,
> +		isCreate: false,
> +		submitUrl: url,
> +		loadUrl: url,
> +		fabric: rec.data._fabric,
> +		node: rec.data.node,
> +	    });
> +
> +	    window.on('destroy', () => me.reload());
> +	},
> +
> +	deleteAction: function(table, rI, cI, item, e, { data }) {
> +	    let me = this;
> +	    let view = me.getView();
> +
> +	    Ext.Msg.show({
> +		title: gettext('Confirm'),
> +		icon: Ext.Msg.WARNING,
> +		message: Ext.String.format(gettext('Are you sure you want to remove the fabric {0}?'), `${data.name}`),
> +		buttons: Ext.Msg.YESNO,
> +		defaultFocus: 'no',
> +		callback: function(btn) {
> +		    if (btn !== 'yes') {
> +			return;
> +		    }
> +
> +		    let url;
> +		    if (data.type === "node") {
> +			url = `/cluster/sdn/fabrics/${data._protocol}/${data._fabric}/node/${data.name}`;
> +		    } else if (data.type === "fabric") {
> +			url = `/cluster/sdn/fabrics/${data._protocol}/${data.name}`;
> +		    } else if (data.type === "interface") {
> +			url = `/cluster/sdn/fabrics/${data._protocol}/${data._fabric}/node/\
> +			${data._node}/interface/${data.name}`;
> +		    } else {
> +			console.warn("deleteAction: missing type");
> +		    }
> +
> +		    Proxmox.Utils.API2Request({
> +			url,
> +			method: 'DELETE',
> +			waitMsgTarget: view,
> +			failure: function(response, opts) {
> +			    Ext.Msg.alert(gettext('Error'), response.htmlStatus);
> +			},
> +			callback: me.reload.bind(me),
> +		    });
> +		},
> +	    });
> +	},
> +
> +	openAddOpenFabricWindow: function() {
> +	    let me = this;
> +
> +	    let window = Ext.create('PVE.sdn.Fabric.OpenFabric.Fabric.Edit', {
> +		autoShow: true,
> +		autoLoad: false,
> +		isCreate: true,
> +	    });
> +
> +	    window.on('destroy', () => me.reload());
> +	},
> +
> +	openAddOspfWindow: function() {
> +	    let me = this;
> +
> +	    let window = Ext.create('PVE.sdn.Fabric.Ospf.Fabric.Edit', {
> +		autoShow: true,
> +		autoLoad: false,
> +		isCreate: true,
> +	    });
> +
> +	    window.on('destroy', () => me.reload());
> +	},
> +
> +	init: function(view) {
> +	    let me = this;
> +	    me.reload();
> +	},
> +    },
> +});



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


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

* Re: [pve-devel] [PATCH pve-manager 10/11] sdn: add fabric edit/delete forms
  2025-02-14 13:39 ` [pve-devel] [PATCH pve-manager 10/11] sdn: add fabric edit/delete forms Gabriel Goller
@ 2025-03-04 10:07   ` Stefan Hanreich
  2025-03-07 16:04     ` Gabriel Goller
  0 siblings, 1 reply; 32+ messages in thread
From: Stefan Hanreich @ 2025-03-04 10:07 UTC (permalink / raw)
  To: Gabriel Goller, pve-devel

comments inline

On 2/14/25 14:39, Gabriel Goller wrote:
> Add the add/edit/delete modals for the FabricsView. This allows us to
> create, edit, and delete fabrics, nodes, and interfaces.
> 
> Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
> Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
> ---
>  www/manager6/sdn/fabrics/Common.js            | 222 ++++++++++++++++++
>  .../sdn/fabrics/openfabric/FabricEdit.js      |  67 ++++++
>  .../sdn/fabrics/openfabric/InterfaceEdit.js   |  92 ++++++++
>  .../sdn/fabrics/openfabric/NodeEdit.js        | 187 +++++++++++++++
>  www/manager6/sdn/fabrics/ospf/FabricEdit.js   |  60 +++++
>  .../sdn/fabrics/ospf/InterfaceEdit.js         |  46 ++++
>  www/manager6/sdn/fabrics/ospf/NodeEdit.js     | 191 +++++++++++++++
>  7 files changed, 865 insertions(+)
>  create mode 100644 www/manager6/sdn/fabrics/Common.js
>  create mode 100644 www/manager6/sdn/fabrics/openfabric/FabricEdit.js
>  create mode 100644 www/manager6/sdn/fabrics/openfabric/InterfaceEdit.js
>  create mode 100644 www/manager6/sdn/fabrics/openfabric/NodeEdit.js
>  create mode 100644 www/manager6/sdn/fabrics/ospf/FabricEdit.js
>  create mode 100644 www/manager6/sdn/fabrics/ospf/InterfaceEdit.js
>  create mode 100644 www/manager6/sdn/fabrics/ospf/NodeEdit.js
> 
> diff --git a/www/manager6/sdn/fabrics/Common.js b/www/manager6/sdn/fabrics/Common.js
> new file mode 100644
> index 000000000000..72ec093fc928
> --- /dev/null
> +++ b/www/manager6/sdn/fabrics/Common.js
> @@ -0,0 +1,222 @@
> +Ext.define('PVE.sdn.Fabric.InterfacePanel', {
> +    extend: 'Ext.grid.Panel',
> +    mixins: ['Ext.form.field.Field'],
> +
> +    network_interfaces: undefined,
> +
> +    selectionChange: function(_grid, _selection) {
> +	let me = this;
> +	me.value = me.getSelection().map((rec) => {
> +	    delete rec.data.cidr;
> +	    delete rec.data.cidr6;
> +	    delete rec.data.selected;
> +	    return PVE.Parser.printPropertyString(rec.data);

maybe we could explicitly select the fields we want to include here, so
this doesn't break when we add new fields?

> +	});
> +	me.checkChange();
> +    },
> +
> +    getValue: function() {
> +	let me = this;
> +	return me.value ?? [];
> +    },
> +
> +    setValue: function(value) {
> +	let me = this;
> +
> +	value ??= [];
> +
> +	me.updateSelectedInterfaces(value);
> +
> +	return me.mixins.field.setValue.call(me, value);
> +    },
> +
> +    addInterfaces: function(fabric_interfaces) {
> +	let me = this;
> +	if (me.network_interfaces) {
> +	    let node_interfaces = me.network_interfaces
> +	    //.filter((elem) => elem.type === 'eth')
> +		.map((elem) => {
> +		    const obj = {
> +			name: elem.iface,
> +			cidr: elem.cidr,
> +			cidr6: elem.cidr6,
> +		    };
> +		    return obj;
> +		});
> +
> +	    if (fabric_interfaces) {
> +		node_interfaces = node_interfaces.map(i => {
> +		    let elem = fabric_interfaces.find(j => j.name === i.name);
> +		    return Object.assign(i, elem);
> +		});
> +		let store = me.getStore();
> +		store.setData(node_interfaces);
> +	    } else {
> +		let store = me.getStore();
> +		store.setData(node_interfaces);
> +	    }
> +	} else if (fabric_interfaces) {
> +	    // We could not get the available interfaces of the node, so we display the configured ones only.
> +		let interfaces = fabric_interfaces.map((elem) => {
> +		    const obj = {
> +			name: elem.name,
> +			cidr: 'unknown',
> +			cidr6: 'unknown',
> +			...elem,
> +		    };
> +		    return obj;
> +		});
> +
> +	    let store = me.getStore();
> +	    store.setData(interfaces);
> +	} else {
> +	    console.warn("no fabric_interfaces and cluster_interfaces available!");
> +	}
> +    },
> +
> +    updateSelectedInterfaces: function(values) {
> +	let me = this;
> +	if (values) {
> +	    let recs = [];
> +	    let store = me.getStore();
> +
> +	    for (const i of values) {
> +		let rec = store.getById(i.name);
> +		if (rec) {
> +		    recs.push(rec);
> +		}
> +	    }
> +	    me.suspendEvent('change');
> +	    me.setSelection();
> +	    me.setSelection(recs);
> +	    me.resumeEvent('change');
> +	} else {
> +	    me.suspendEvent('change');
> +	    me.setSelection();
> +	    me.resumeEvent('change');
> +	}

could avoid some duplication by moving the methods calls above / below
the if/else

> +    },
> +
> +    setNetworkInterfaces: function(network_interfaces) {
> +	this.network_interfaces = network_interfaces;
> +    },
> +
> +    getSubmitData: function() {
> +	let records = this.getSelection().map((record) => {
> +	    // we don't need the cidr, cidr6, and selected parameters
> +	    delete record.data.cidr;
> +	    delete record.data.cidr6;
> +	    delete record.data.selected;
> +	    return Proxmox.Utils.printPropertyString(record.data);

same w.r.t selecting only the fields we care about here

> +	});
> +	return {
> +	    'interfaces': records,
> +	};
> +    },
> +
> +    controller: {
> +	onValueChange: function(field, value) {
> +	    let me = this;
> +	    let record = field.getWidgetRecord();
> +	    let column = field.getWidgetColumn();
> +	    if (record) {
> +		record.set(column.dataIndex, value);
> +		record.commit();
> +
> +		me.getView().checkChange();
> +		me.getView().selectionChange();
> +	    }
> +	},
> +
> +	control: {
> +	    'field': {
> +		change: 'onValueChange',
> +	    },
> +	},
> +    },
> +
> +    selModel: {
> +	type: 'checkboxmodel',
> +	mode: 'SIMPLE',
> +    },
> +
> +    listeners: {
> +	selectionchange: function() {
> +	    this.selectionChange(...arguments);
> +	},
> +    },
> +
> +    commonColumns: [
> +	{
> +	    text: gettext('Name'),
> +	    dataIndex: 'name',
> +	    flex: 2,
> +	},
> +	{
> +	    text: gettext('IPv4'),
> +	    dataIndex: 'cidr',
> +	    flex: 2,
> +	},
> +	{
> +	    text: gettext('IPv6'),
> +	    dataIndex: 'cidr6',
> +	    flex: 2,
> +	},
> +    ],
> +
> +    additionalColumns: [],
> +
> +    initComponent: function() {
> +	let me = this;
> +
> +	Ext.apply(me, {
> +	    store: Ext.create("Ext.data.Store", {
> +		model: "Pve.sdn.Interface",
> +		sorters: {
> +		    property: 'name',
> +		    direction: 'ASC',
> +		},
> +	    }),
> +	    columns: me.commonColumns.concat(me.additionalColumns),
> +	});
> +
> +	me.callParent();
> +
> +	Proxmox.Utils.monStoreErrors(me, me.getStore(), true);
> +	me.initField();
> +    },
> +});
> +
> +
> +Ext.define('Pve.sdn.Fabric', {
> +    extend: 'Ext.data.Model',
> +    idProperty: 'name',
> +    fields: [
> +	'name',
> +	'type',
> +    ],
> +});
> +
> +Ext.define('Pve.sdn.Node', {
> +    extend: 'Ext.data.Model',
> +    idProperty: 'name',
> +    fields: [
> +	'name',
> +	'fabric',
> +	'type',
> +    ],
> +});
> +
> +Ext.define('Pve.sdn.Interface', {
> +    extend: 'Ext.data.Model',
> +    idProperty: 'name',
> +    fields: [
> +	'name',
> +	'cidr',
> +	'cidr6',
> +	'passive',
> +	'hello_interval',
> +	'hello_multiplier',
> +	'csnp_interval',
> +    ],
> +});
> diff --git a/www/manager6/sdn/fabrics/openfabric/FabricEdit.js b/www/manager6/sdn/fabrics/openfabric/FabricEdit.js
> new file mode 100644
> index 000000000000..0431a00e7302
> --- /dev/null
> +++ b/www/manager6/sdn/fabrics/openfabric/FabricEdit.js
> @@ -0,0 +1,67 @@
> +Ext.define('PVE.sdn.Fabric.OpenFabric.Fabric.Edit', {
> +    extend: 'Proxmox.window.Edit',
> +    xtype: 'pveSDNOpenFabricRouteEdit',
> +
> +    subject: gettext('Add OpenFabric'),
> +
> +    url: '/cluster/sdn/fabrics/openfabric',
> +    type: 'openfabric',
> +
> +    isCreate: undefined,
> +
> +    viewModel: {
> +	data: {
> +	    isCreate: true,
> +	},
> +    },
> +
> +    items: [
> +	{
> +	    xtype: 'textfield',
> +	    name: 'type',
> +	    value: 'openfabric',
> +	    allowBlank: false,
> +	    hidden: true,
> +	},
> +	{
> +	    xtype: 'textfield',
> +	    fieldLabel: gettext('Name'),
> +	    labelWidth: 120,
> +	    name: 'name',
> +	    allowBlank: false,
> +	    bind: {
> +		disabled: '{!isCreate}',
> +	    },
> +	},
> +	{
> +	    xtype: 'numberfield',
> +	    fieldLabel: gettext('Hello Interval'),
> +	    labelWidth: 120,
> +	    name: 'hello_interval',
> +	    allowBlank: true,
> +	},
> +    ],
> +
> +    submitUrl: function(url, values) {
> +	let me = this;
> +	return `${me.url}`;
> +    },
> +
> +    initComponent: function() {
> +	let me = this;
> +
> +	let view = me.getViewModel();
> +	view.set('isCreate', me.isCreate);
> +
> +	me.method = me.isCreate ? 'POST' : 'PUT';
> +	me.callParent();
> +
> +	if (!me.isCreate) {
> +	    me.load({
> +		success: function(response, opts) {
> +		    me.setValues(response.result.data.fabric);
> +		},
> +	    });
> +	}
> +    },
> +});
> diff --git a/www/manager6/sdn/fabrics/openfabric/InterfaceEdit.js b/www/manager6/sdn/fabrics/openfabric/InterfaceEdit.js
> new file mode 100644
> index 000000000000..ef33c16b784f
> --- /dev/null
> +++ b/www/manager6/sdn/fabrics/openfabric/InterfaceEdit.js
> @@ -0,0 +1,92 @@
> +Ext.define('PVE.sdn.Fabric.OpenFabric.Interface.Edit', {
> +    extend: 'Proxmox.window.Edit',
> +    xtype: 'pveSDNOpenFabricInterfaceEdit',
> +
> +    initComponent: function() {
> +	let me = this;
> +
> +	Ext.apply(me, {
> +	    items: [{
> +		xtype: 'inputpanel',
> +		items: [
> +		    {
> +			xtype: 'textfield',
> +			fieldLabel: gettext('Interface'),
> +			name: 'name',
> +			disabled: true,
> +		    },
> +		    {
> +			xtype: 'proxmoxcheckbox',
> +			fieldLabel: gettext('Passive'),
> +			name: 'passive',
> +			uncheckedValue: 0,
> +		    },
> +		    {
> +			xtype: 'numberfield',
> +			fieldLabel: gettext('Hello Interval'),
> +			name: 'hello_interval',
> +			allowBlank: true,
> +		    },
> +		    {
> +			xtype: 'numberfield',
> +			fieldLabel: gettext('Hello Multiplier'),
> +			name: 'hello_multiplier',
> +			allowBlank: true,
> +		    },
> +		    {
> +			xtype: 'numberfield',
> +			fieldLabel: gettext('CSNP Interval'),
> +			name: 'csnp_interval',
> +			allowBlank: true,
> +		    },
> +		],
> +	    }],
> +	});
> +
> +	me.callParent();
> +    },
> +});
> +
> +Ext.define('PVE.sdn.Fabric.OpenFabric.InterfacePanel', {
> +    extend: 'PVE.sdn.Fabric.InterfacePanel',
> +
> +    additionalColumns: [
> +	{
> +	    text: gettext('Passive'),
> +	    xtype: 'widgetcolumn',
> +	    dataIndex: 'passive',
> +	    flex: 1,
> +	    widget: {
> +		xtype: 'checkbox',
> +	    },
> +	},
> +	{
> +	    text: gettext('Hello Interval'),
> +	    xtype: 'widgetcolumn',
> +	    dataIndex: 'hello_interval',
> +	    flex: 1,
> +	    widget: {
> +		xtype: 'numberfield',
> +	    },
> +	},
> +	{
> +	    text: gettext('Hello Multiplier'),
> +	    xtype: 'widgetcolumn',
> +	    dataIndex: 'hello_multiplier',
> +	    flex: 1,
> +	    widget: {
> +		xtype: 'numberfield',
> +	    },
> +	},
> +	{
> +	    text: gettext('CSNP Interval'),
> +	    xtype: 'widgetcolumn',
> +	    dataIndex: 'csnp_interval',
> +	    flex: 1,
> +	    widget: {
> +		xtype: 'numberfield',
> +	    },
> +	},
> +    ],
> +});
> +
> diff --git a/www/manager6/sdn/fabrics/openfabric/NodeEdit.js b/www/manager6/sdn/fabrics/openfabric/NodeEdit.js
> new file mode 100644
> index 000000000000..ce61f0c15b49
> --- /dev/null
> +++ b/www/manager6/sdn/fabrics/openfabric/NodeEdit.js
> @@ -0,0 +1,187 @@
> +Ext.define('PVE.sdn.Fabric.OpenFabric.Node.InputPanel', {
> +    extend: 'Proxmox.panel.InputPanel',
> +
> +    viewModel: {},
> +
> +    isCreate: undefined,
> +    loadClusterInterfaces: undefined,
> +
> +    interface_selector: undefined,
> +    node_not_accessible_warning: undefined,
> +
> +    onSetValues: function(values) {
> +	let me = this;
> +	me.interface_selector.setNetworkInterfaces(values.network_interfaces);
> +	if (values.node) {
> +	    // this means we are in edit mode and we have a config
> +	    me.interface_selector.addInterfaces(values.node.interface);
> +	    me.interface_selector.updateSelectedInterfaces(values.node.interface);
> +	    return { node: values.node.node, net: values.node.net, interfaces: values.node.interface };
> +	} else {
> +	    // this means we are in create mode, so don't select any interfaces
> +	    me.interface_selector.addInterfaces(null);
> +	    me.interface_selector.updateSelectedInterfaces(null);
> +	    return {};
> +	}
> +    },
> +
> +    initComponent: function() {
> +	let me = this;
> +	me.items = [
> +	    {
> +		xtype: 'pveNodeSelector',
> +		reference: 'nodeselector',
> +		fieldLabel: gettext('Node'),
> +		labelWidth: 120,
> +		name: 'node',
> +		allowBlank: false,
> +		disabled: !me.isCreate,
> +		onlineValidator: me.isCreate,
> +		autoSelect: me.isCreate,
> +		listeners: {
> +		    change: function(f, value) {
> +			if (me.isCreate) {
> +			    me.loadClusterInterfaces(value, (result) => {
> +				me.setValues({ network_interfaces: result });
> +			    });
> +			}
> +		    },
> +		},
> +		listConfig: {
> +		    columns: [
> +			{
> +			    header: gettext('Node'),
> +			    dataIndex: 'node',
> +			    sortable: true,
> +			    hideable: false,
> +			    flex: 1,
> +			},
> +		    ],
> +		},
> +
> +	    },
> +	    me.node_not_accessible_warning,
> +	    {
> +		xtype: 'textfield',
> +		fieldLabel: gettext('Net'),
> +		labelWidth: 120,
> +		name: 'net',
> +		allowBlank: false,
> +	    },
> +	    me.interface_selector,
> +	];
> +	me.callParent();
> +    },
> +});
> +
> +Ext.define('PVE.sdn.Fabric.OpenFabric.Node.Edit', {
> +    extend: 'Proxmox.window.Edit',
> +    xtype: 'pveSDNFabricAddNode',
> +
> +    width: 800,
> +
> +    // dummyurl
> +    url: '/cluster/sdn/fabrics/openfabric',
> +
> +    interface_selector: undefined,
> +    isCreate: undefined,
> +
> +    controller: {
> +	xclass: 'Ext.app.ViewController',
> +    },
> +
> +    submitUrl: function(url, values) {
> +	let me = this;
> +	return `${me.url}/${me.extraRequestParams.fabric}/node/${values.node}`;
> +    },
> +
> +    loadClusterInterfaces: function(node, onSuccess) {
> +	Proxmox.Utils.API2Request({
> +				  url: `/api2/extjs/nodes/${node}/network`,
> +				  method: 'GET',
> +				  success: function(response, _opts) {
> +				      onSuccess(response.result.data);
> +				  },
> +				  // No failure callback because this api call can't fail, it
> +				  // just hangs the request :) (if the node doesn't exist it gets proxied)
> +	});
> +    },
> +    loadFabricInterfaces: function(fabric, node, onSuccess, onFailure) {
> +	Proxmox.Utils.API2Request({
> +				  url: `/cluster/sdn/fabrics/openfabric/${fabric}/node/${node}`,
> +				  method: 'GET',
> +				  success: function(response, _opts) {
> +				      onSuccess(response.result.data);
> +				  },
> +				  failure: onFailure,
> +	});
> +    },
> +    loadAllAvailableNodes: function(onSuccess) {
> +	Proxmox.Utils.API2Request({
> +				  url: `/cluster/config/nodes`,
> +				  method: 'GET',
> +				  success: function(response, _opts) {
> +				      onSuccess(response.result.data);
> +				  },
> +	});
> +    },
> +
> +    initComponent: function() {
> +	let me = this;
> +
> +	me.interface_selector = Ext.create('PVE.sdn.Fabric.OpenFabric.InterfacePanel', {
> +	    name: 'interfaces',
> +	});
> +
> +	me.node_not_accessible_warning = Ext.create('Proxmox.form.field.DisplayEdit', {
> +	    userCls: 'pmx-hint',
> +	    value: gettext('The node is not accessible.'),
> +	    hidden: true,
> +	});
> +
> +	let ipanel = Ext.create('PVE.sdn.Fabric.OpenFabric.Node.InputPanel', {
> +	    interface_selector: me.interface_selector,
> +	    node_not_accessible_warning: me.node_not_accessible_warning,
> +	    isCreate: me.isCreate,
> +	    loadClusterInterfaces: me.loadClusterInterfaces,
> +	});
> +
> +	Ext.apply(me, {
> +	    subject: gettext('Node'),
> +	    items: [ipanel],
> +	});
> +
> +	me.callParent();
> +
> +	if (!me.isCreate) {
> +	    me.loadAllAvailableNodes((allNodes) => {
> +		if (allNodes.some(i => i.name === me.node)) {
> +		    me.loadClusterInterfaces(me.node, (clusterResult) => {
> +			me.loadFabricInterfaces(me.fabric, me.node, (fabricResult) => {
> +			    fabricResult.node.interface = fabricResult.node.interface
> +				.map(i => PVE.Parser.parsePropertyString(i));
> +			    fabricResult.network_interfaces = clusterResult;
> +			    // this will also set them as selected
> +			    ipanel.setValues(fabricResult);
> +			});
> +		    });
> +		} else {
> +		    me.node_not_accessible_warning.setHidden(false);
> +		    // If the node is not currently in the cluster and not available (we can't get it's interfaces).
> +			me.loadFabricInterfaces(me.fabric, me.node, (fabricResult) => {
> +			    fabricResult.node.interface = fabricResult.node.interface
> +				.map(i => PVE.Parser.parsePropertyString(i));
> +			    ipanel.setValues(fabricResult);
> +			});
> +		}
> +	    });
> +	}
> +
> +	if (me.isCreate) {
> +	    me.method = 'POST';
> +	} else {
> +	    me.method = 'PUT';
> +	}
> +    },
> +});
> +
> diff --git a/www/manager6/sdn/fabrics/ospf/FabricEdit.js b/www/manager6/sdn/fabrics/ospf/FabricEdit.js
> new file mode 100644
> index 000000000000..2ce88e443cdd
> --- /dev/null
> +++ b/www/manager6/sdn/fabrics/ospf/FabricEdit.js
> @@ -0,0 +1,60 @@
> +Ext.define('PVE.sdn.Fabric.Ospf.Fabric.Edit', {
> +    extend: 'Proxmox.window.Edit',
> +    xtype: 'pveSDNOpenFabricRouteEdit',
> +
> +    subject: gettext('Add OSPF'),
> +
> +    url: '/cluster/sdn/fabrics/ospf',
> +    type: 'ospf',
> +
> +    isCreate: undefined,
> +
> +    viewModel: {
> +	data: {
> +	    isCreate: true,
> +	},
> +    },
> +
> +    items: [
> +	{
> +	    xtype: 'textfield',
> +	    name: 'type',
> +	    value: 'ospf',
> +	    allowBlank: false,
> +	    hidden: true,
> +	},
> +	{
> +	    xtype: 'textfield',
> +	    fieldLabel: gettext('Area'),
> +	    labelWidth: 120,
> +	    name: 'name',
> +	    allowBlank: false,
> +	    bind: {
> +		disabled: '{!isCreate}',
> +	    },
> +	},
> +    ],
> +
> +    submitUrl: function(url, values) {
> +	let me = this;
> +	return `${me.url}`;
> +    },
> +
> +    initComponent: function() {
> +	let me = this;
> +
> +	let view = me.getViewModel();
> +	view.set('isCreate', me.isCreate);
> +
> +	me.method = me.isCreate ? 'POST' : 'PUT';
> +
> +	me.callParent();
> +	if (!me.isCreate) {
> +	    me.load({
> +		success: function(response, opts) {
> +		    me.setValues(response.result.data.fabric);
> +		},
> +	    });
> +	}
> +    },
> +});
> diff --git a/www/manager6/sdn/fabrics/ospf/InterfaceEdit.js b/www/manager6/sdn/fabrics/ospf/InterfaceEdit.js
> new file mode 100644
> index 000000000000..e7810b3f34c9
> --- /dev/null
> +++ b/www/manager6/sdn/fabrics/ospf/InterfaceEdit.js
> @@ -0,0 +1,46 @@
> +Ext.define('PVE.sdn.Fabric.Ospf.Interface.Edit', {
> +    extend: 'Proxmox.window.Edit',
> +    xtype: 'pveSDNOspfInterfaceEdit',
> +
> +    initComponent: function() {
> +	let me = this;
> +
> +	Ext.apply(me, {
> +	    items: [{
> +		xtype: 'inputpanel',
> +		items: [
> +		    {
> +			xtype: 'textfield',
> +			fieldLabel: gettext('Interface'),
> +			name: 'name',
> +			disabled: true,
> +		    },
> +		    {
> +			xtype: 'proxmoxcheckbox',
> +			fieldLabel: gettext('Passive'),
> +			name: 'passive',
> +			uncheckedValue: 0,
> +		    },
> +		],
> +	    }],
> +	});
> +
> +	me.callParent();
> +    },
> +});
> +
> +Ext.define('PVE.sdn.Fabric.Ospf.InterfacePanel', {
> +    extend: 'PVE.sdn.Fabric.InterfacePanel',
> +
> +    additionalColumns: [
> +	{
> +	    text: gettext('Passive'),
> +	    xtype: 'widgetcolumn',
> +	    dataIndex: 'passive',
> +	    flex: 1,
> +	    widget: {
> +		xtype: 'checkbox',
> +	    },
> +	},
> +    ],
> +});
> diff --git a/www/manager6/sdn/fabrics/ospf/NodeEdit.js b/www/manager6/sdn/fabrics/ospf/NodeEdit.js
> new file mode 100644
> index 000000000000..41778e930bfb
> --- /dev/null
> +++ b/www/manager6/sdn/fabrics/ospf/NodeEdit.js
> @@ -0,0 +1,191 @@
> +Ext.define('PVE.sdn.Fabric.Ospf.Node.InputPanel', {
> +    extend: 'Proxmox.panel.InputPanel',
> +
> +    viewModel: {},
> +
> +    isCreate: undefined,
> +    loadClusterInterfaces: undefined,
> +
> +    interface_selector: undefined,
> +    node_not_accessible_warning: undefined,
> +
> +    onSetValues: function(values) {
> +	let me = this;
> +	me.interface_selector.setNetworkInterfaces(values.network_interfaces);
> +	if (values.node) {
> +	    // this means we are in edit mode and we have a config
> +	    me.interface_selector.addInterfaces(values.node.interface);
> +	    me.interface_selector.updateSelectedInterfaces(values.node.interface);
> +	    return {
> +		node: values.node.node,
> +		router_id: values.node.router_id,
> +		interfaces: values.node.interface,
> +	    };
> +	} else {
> +	    // this means we are in create mode, so don't select any interfaces
> +	    me.interface_selector.addInterfaces(null);
> +	    me.interface_selector.updateSelectedInterfaces(null);
> +	    return {};
> +	}
> +    },
> +
> +    initComponent: function() {
> +	let me = this;
> +	me.items = [
> +	    {
> +		xtype: 'pveNodeSelector',
> +		reference: 'nodeselector',
> +		fieldLabel: gettext('Node'),
> +		labelWidth: 120,
> +		name: 'node',
> +		allowBlank: false,
> +		disabled: !me.isCreate,
> +		onlineValidator: me.isCreate,
> +		autoSelect: me.isCreate,
> +		listeners: {
> +		    change: function(f, value) {
> +			if (me.isCreate) {
> +			    me.loadClusterInterfaces(value, (result) => {
> +				me.setValues({ network_interfaces: result });
> +			    });
> +			}
> +		    },
> +		},
> +		listConfig: {
> +		    columns: [
> +			{
> +			    header: gettext('Node'),
> +			    dataIndex: 'node',
> +			    sortable: true,
> +			    hideable: false,
> +			    flex: 1,
> +			},
> +		    ],
> +		},
> +
> +	    },
> +	    me.node_not_accessible_warning,
> +	    {
> +		xtype: 'textfield',
> +		fieldLabel: gettext('Router-Id'),
> +		labelWidth: 120,
> +		name: 'router_id',
> +		allowBlank: false,
> +	    },
> +	    me.interface_selector,
> +	];
> +	me.callParent();
> +    },
> +});
> +
> +Ext.define('PVE.sdn.Fabric.Ospf.Node.Edit', {
> +    extend: 'Proxmox.window.Edit',
> +    xtype: 'pveSDNFabricAddNode',
> +
> +    width: 800,
> +
> +    // dummyurl
> +    url: '/cluster/sdn/fabrics/ospf',
> +
> +    interface_selector: undefined,
> +    isCreate: undefined,
> +
> +    controller: {
> +	xclass: 'Ext.app.ViewController',
> +    },
> +
> +    submitUrl: function(url, values) {
> +	let me = this;
> +	return `${me.url}/${me.extraRequestParams.fabric}/node/${values.node}`;
> +    },
> +
> +    loadClusterInterfaces: function(node, onSuccess) {
> +	Proxmox.Utils.API2Request({
> +	    url: `/api2/extjs/nodes/${node}/network`,
> +	    method: 'GET',
> +	    success: function(response, _opts) {
> +	        onSuccess(response.result.data);
> +	    },
> +	    // No failure callback because this api call can't fail, it
> +	    // just hangs the request :) (if the node doesn't exist it gets proxied)
> +	});
> +    },
> +    loadFabricInterfaces: function(fabric, node, onSuccess, onFailure) {
> +	Proxmox.Utils.API2Request({
> +	    url: `/cluster/sdn/fabrics/ospf/${fabric}/node/${node}`,
> +	    method: 'GET',
> +	    success: function(response, _opts) {
> +		onSuccess(response.result.data);
> +	    },
> +	    failure: onFailure,
> +	});
> +    },
> +    loadAllAvailableNodes: function(onSuccess) {
> +	Proxmox.Utils.API2Request({
> +	    url: `/cluster/config/nodes`,
> +	    method: 'GET',
> +	    success: function(response, _opts) {
> +	        onSuccess(response.result.data);
> +	    },
> +	});
> +    },
> +
> +    initComponent: function() {
> +	let me = this;
> +
> +	me.interface_selector = Ext.create('PVE.sdn.Fabric.Ospf.InterfacePanel', {
> +	    name: 'interfaces',
> +	});
> +
> +	me.node_not_accessible_warning = Ext.create('Proxmox.form.field.DisplayEdit', {
> +	    userCls: 'pmx-hint',
> +	    value: gettext('The node is not accessible.'),
> +	    hidden: true,
> +	});
> +
> +
> +	let ipanel = Ext.create('PVE.sdn.Fabric.Ospf.Node.InputPanel', {
> +	    interface_selector: me.interface_selector,
> +	    node_not_accessible_warning: me.node_not_accessible_warning,
> +	    isCreate: me.isCreate,
> +	    loadClusterInterfaces: me.loadClusterInterfaces,
> +	});
> +
> +	Ext.apply(me, {
> +	    subject: gettext('Node'),
> +	    items: [ipanel],
> +	});
> +
> +	me.callParent();
> +
> +	if (!me.isCreate) {
> +	    me.loadAllAvailableNodes((allNodes) => {
> +		if (allNodes.some(i => i.name === me.node)) {
> +		    me.loadClusterInterfaces(me.node, (clusterResult) => {
> +			me.loadFabricInterfaces(me.fabric, me.node, (fabricResult) => {
> +			    fabricResult.node.interface = fabricResult.node.interface
> +				.map(i => PVE.Parser.parsePropertyString(i));
> +			    fabricResult.network_interfaces = clusterResult;
> +			    // this will also set them as selected
> +			    ipanel.setValues(fabricResult);
> +			});
> +		    });
> +		} else {
> +		    me.node_not_accessible_warning.setHidden(false);
> +		    // If the node is not currently in the cluster and not available (we can't get it's interfaces).
> +			me.loadFabricInterfaces(me.fabric, me.node, (fabricResult) => {
> +			    fabricResult.node.interface = fabricResult.node.interface
> +				.map(i => PVE.Parser.parsePropertyString(i));
> +			    ipanel.setValues(fabricResult);
> +			});
> +		}
> +	    });
> +	}
> +
> +	if (me.isCreate) {
> +	    me.method = 'POST';
> +	} else {
> +	    me.method = 'PUT';
> +	}
> +    },
> +});



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


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

* Re: [pve-devel] [PATCH proxmox-ve-rs 02/11] add proxmox-frr crate with frr types
  2025-03-03 16:29   ` Stefan Hanreich
@ 2025-03-04 16:28     ` Gabriel Goller
  0 siblings, 0 replies; 32+ messages in thread
From: Gabriel Goller @ 2025-03-04 16:28 UTC (permalink / raw)
  To: Stefan Hanreich; +Cc: pve-devel

On 03.03.2025 17:29, Stefan Hanreich wrote:
>This uses stuff from a later patch, doesn't it? Shouldn't the order of
>patches 2 and 3 be flipped?

oops, yeah mb.

>> [snip]
>> diff --git a/proxmox-frr/src/lib.rs b/proxmox-frr/src/lib.rs
>> new file mode 100644
>> index 000000000000..ceef82999619
>> --- /dev/null
>> +++ b/proxmox-frr/src/lib.rs
>> @@ -0,0 +1,223 @@
>> +pub mod common;
>> +pub mod openfabric;
>> +pub mod ospf;
>> +
>> +use std::{collections::{hash_map::Entry, HashMap}, fmt::Display, str::FromStr};
>> +
>> +use common::{FrrWord, FrrWordError};
>> +use proxmox_ve_config::sdn::fabric::common::Hostname;
>
>Maybe move it to network-types, if it is always needed? Seems like a
>better fit. Especially since the dependency is optional.

Agree.

>> [snip]
>> +#[derive(Clone, Debug, PartialEq, Eq, Hash, DeserializeFromStr, SerializeDisplay)]
>> +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),
>> +        }
>> +    }
>> +}
>> +
>> +impl FromStr for RouterName {
>> +    type Err = RouterNameError;
>> +
>> +    fn from_str(s: &str) -> Result<Self, Self::Err> {
>> +        if let Ok(router) = s.parse() {
>> +            return Ok(Self::OpenFabric(router));
>> +        }
>
>does this make sense here? can we actually make a clear distinction on
>whether this is a OpenFabric / OSPF router name (and for all other
>future RouterNames) from the string alone? I think it might be better to
>explicitly construct the specific type and then make a RouterName out of
>it and do not implement FromStr at all. Or get rid of RouterName
>altogether (see below).
>
>It's also constructing an OpenFabric RouterName but never an OSPF router
>name.

nope, this doesn't make any sense at all, no idea how it got in here.
Removed it and the Deserialize derive for all the parent types as it
isn't needed anyway.

>> +        Err(RouterNameError::InvalidName)
>> +    }
>> +}
>> +
>> +/// The interface name is the same on ospf and openfabric, but it is an enum so we can have two
>> +/// different entries in the hashmap. This allows us to have an interface in an ospf and openfabric
>> +/// fabric.
>> +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash)]
>> +pub enum InterfaceName {
>> +    OpenFabric(FrrWord),
>> +    Ospf(FrrWord),
>> +}
>
>maybe this should be a struct representing a linux interface name
>(nul-terminated 16byte string) instead of an FrrWord?

Makes sense, added a different InterfaceName struct that wraps a String and
checks the length upon creation.

>> [snip]
>> +#[derive(Clone, Debug, PartialEq, Eq, Default, Deserialize, Serialize)]
>> +pub struct FrrConfig {
>> +    router: HashMap<RouterName, Router>,
>> +    interfaces: HashMap<InterfaceName, Interface>,
>
>are we ever querying the frr router/interface by name? judging from the
>public API we don't and only iterate over it. we could move the name
>into the Router/Interface then, this would ensure that the name always
>fits the concrete router. We could probably also make converting from
>the section config easier then and possibly save us the whole RouterName
>struct.
>
>Are duplicates possible with how the ID in the SectionConfig works? If
>we want to avoid that, we could probably use other ways.

The problem here are duplicate interfaces over different fabrics
(areas), which are not allowed.

These are not checked by SectionConfig because the key is:
"<area>_<interface-name>".

We could simply implement checks when inserting, sure, but why not just
use HashMaps and seal the deal? :)

>> +}
>> +
>> +impl FrrConfig {
>> +    pub fn new() -> Self {
>> +        Self::default()
>> +    }
>> +
>> +    #[cfg(feature = "config-ext")]
>> +    pub fn builder() -> FrrConfigBuilder {
>> +        FrrConfigBuilder::default()
>> +    }
>
>see above for if we really need a builder here or if implementing
>conversion traits suffices.

My idea is that we can add the bgp stuff here as well (soonTM).
I wanted to avoid a TryFrom<(FabricConfig, BgpConfig, ...)>
implementation to be honest.

>> +    pub fn router(&self) -> impl Iterator<Item = (&RouterName, &Router)> + '_ {
>> +        self.router.iter()
>> +    }
>> +
>> +    pub fn interfaces(&self) -> impl Iterator<Item = (&InterfaceName, &Interface)> + '_ {
>> +        self.interfaces.iter()
>> +    }
>> +}
>> +
>> +#[derive(Default)]
>> +#[cfg(feature = "config-ext")]
>> +pub struct FrrConfigBuilder {
>> +    fabrics: FabricConfig,
>> +    //bgp: Option<internal::BgpConfig>
>> +}
>> +
>> +#[cfg(feature = "config-ext")]
>> +impl FrrConfigBuilder {
>> +    pub fn add_fabrics(mut self, fabric: FabricConfig) -> FrrConfigBuilder {
>> +        self.fabrics = fabric;
>> +        self
>> +    }
>
>From<FabricConfig> might be better if it replaces self.fabrics /
>consumes FabricConfig anyway? Maybe even TryFrom<FabricConfig> for
>FrrConfig itself?

see above.

>> +
>> +    pub fn build(self, current_node: &str) -> Result<FrrConfig, anyhow::Error> {
>> +        let mut router: HashMap<RouterName, Router> = HashMap::new();
>> +        let mut interfaces: HashMap<InterfaceName, Interface> = HashMap::new();
>> +
>> +        if let Some(openfabric) = self.fabrics.openfabric() {
>> +            // openfabric
>> +            openfabric
>> +                .fabrics()
>> +                .iter()
>> +                .try_for_each(|(fabric_id, fabric_config)| {
>> +                    let node_config = fabric_config.nodes().get(&Hostname::new(current_node));
>> +                    if let Some(node_config) = node_config {
>> +                        let ofr = openfabric::OpenFabricRouter::from((fabric_config, node_config));
>> +                        let router_item = Router::OpenFabric(ofr);
>> +                        let router_name = RouterName::OpenFabric(
>> +                            openfabric::OpenFabricRouterName::try_from(fabric_id)?,
>> +                        );
>> +                        router.insert(router_name.clone(), router_item);
>> +                        node_config.interfaces().try_for_each(|interface| {
>> +                            let mut openfabric_interface: openfabric::OpenFabricInterface =
>> +                                (fabric_id, interface).try_into()?;
>
>The only fallible thing here is constructing the RouterName, so we could
>just clone it from above and make a
>OpenFabricInterface::from_section_config() method that accepts the name
>+ sectionconfig structs?

fair – or just have the 
TryFrom<(&internal::FabricId,&internal::Interface)> for OpenFabricInterface
be a 
TryFrom<(&OpenFabricRouterName, &internal::Interface)> for OpenFabricInterface

imo that's even better.

>> +                            // If no specific hello_interval is set, get default one from fabric
>> +                            // config
>> +                            if openfabric_interface.hello_interval().is_none() {
>> +                                openfabric_interface
>> +                                    .set_hello_interval(fabric_config.hello_interval().clone());
>> +                            }
>> +                            let interface_name = InterfaceName::OpenFabric(FrrWord::from_str(interface.name())?);
>> +                            // Openfabric doesn't allow an interface to be in multiple openfabric
>> +                            // fabrics. Frr will just ignore it and take the first one.
>> +                            if let Entry::Vacant(e) = interfaces.entry(interface_name) {
>> +                                e.insert(openfabric_interface.into());
>> +                            } else {
>> +                                tracing::warn!("An interface cannot be in multiple openfabric fabrics");
>> +                            }
>
>if let Err(_) = interfaces.try_insert(..) maybe (if we keep the HashMap)?

try_insert is unstable :(

>> +                            Ok::<(), anyhow::Error>(())
>> +                        })?;
>> +                    } else {
>> +                        tracing::warn!("no node configuration for fabric \"{fabric_id}\" – this fabric is not configured for this node.");
>
>Maybe it would make sense to split this into two functions, where we
>could just return early if there is no configuration for this node?

do you mean one function for ospf and one for openfabric?
I'd agree to that.

>Then here an early return would suffice, since otherwise the log gets
>spammed on nodes that are simply not part of a fabric (which is
>perfectly valid)?

True, did not consider that. I think it's ok if we just make this a
debug print. No need to add some fancy checks imo.

>> [snip]
>> +#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
>> +pub struct OpenFabricInterface {
>> +    // Note: an interface can only be a part of a single fabric (so no vec needed here)
>> +    fabric_id: OpenFabricRouterName,
>> +    passive: Option<bool>,
>> +    hello_interval: Option<openfabric::HelloInterval>,
>> +    csnp_interval: Option<openfabric::CsnpInterval>,
>> +    hello_multiplier: Option<openfabric::HelloMultiplier>,
>> +}
>> +
>> +impl OpenFabricInterface {
>> +    pub fn fabric_id(&self) -> &OpenFabricRouterName {
>> +        &self.fabric_id
>> +    }
>> +    pub fn passive(&self) -> &Option<bool> {
>> +        &self.passive
>> +    }
>> +    pub fn hello_interval(&self) -> &Option<openfabric::HelloInterval> {
>> +        &self.hello_interval
>> +    }
>> +    pub fn csnp_interval(&self) -> &Option<openfabric::CsnpInterval> {
>> +        &self.csnp_interval
>> +    }
>> +    pub fn hello_multiplier(&self) -> &Option<openfabric::HelloMultiplier> {
>> +        &self.hello_multiplier
>> +    }
>
>If we implement Copy for those types it's usually just easier to return
>them owned.

true

>> +    pub fn set_hello_interval(&mut self, interval: Option<openfabric::HelloInterval>) {
>
>nit: I usually like impl Into<Option<..>> because it makes the API nicer
>(don't have to write Some(..) all the time ...)

yep.

>> [snip]
>> +#[cfg(feature = "config-ext")]
>> +impl TryFrom<(&internal::FabricId, &internal::Interface)> for OpenFabricInterface {
>> +    type Error = OpenFabricInterfaceError;
>> +
>> +    fn try_from(value: (&internal::FabricId, &internal::Interface)) -> Result<Self, Self::Error> {
>> +        Ok(Self {
>> +            fabric_id: OpenFabricRouterName::try_from(value.0)?,
>> +            passive: value.1.passive(),
>> +            hello_interval: value.1.hello_interval().clone(),
>> +            csnp_interval: value.1.csnp_interval().clone(),
>> +            hello_multiplier: value.1.hello_multiplier().clone(),
>
>We can easily implement Copy for those values, since they're u16.

nice

>> +        })
>> +    }
>> +}
>> +
>> +#[cfg(feature = "config-ext")]
>> +impl TryFrom<&internal::FabricId> for OpenFabricRouterName {
>> +    type Error = RouterNameError;
>> +
>> +    fn try_from(value: &internal::FabricId) -> Result<Self, Self::Error> {
>> +        Ok(OpenFabricRouterName::new(FrrWord::new(value.to_string())?))
>> +    }
>> +}
>> +
>> +#[cfg(feature = "config-ext")]
>> +impl From<(&internal::FabricConfig, &internal::NodeConfig)> for OpenFabricRouter {
>> +    fn from(value: (&internal::FabricConfig, &internal::NodeConfig)) -> Self {
>> +        Self {
>> +            net: value.1.net().to_owned(),
>> +        }
>> +    }
>> +}
>
>We never use value.0 here, but we might if we have some global options,
>right?

Exactly. Although I've been thinking about that. Maybe we should just
all chuck them in to the Intermediate Representation (and expand the
properties to every node) and don't bother with them in the FrrConfig?

>> [snip]
>> +/// The name of the ospf frr router. There is only one ospf fabric possible in frr (ignoring
>> +/// multiple invocations of the ospfd daemon) and the separation is done with areas. Still,
>> +/// different areas have the same frr router, so the name of the router is just "ospf" in "router
>> +/// ospf". This type still contains the Area so that we can insert it in the Hashmap.
>> +#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
>> +pub struct OspfRouterName(Area);
>> +
>> +impl From<Area> for OspfRouterName {
>> +    fn from(value: Area) -> Self {
>> +        Self(value)
>> +    }
>> +}
>> +
>> +impl Display for OspfRouterName {
>> +    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
>> +        write!(f, "ospf")
>
>this is missing the area, or?

Nope, this is fine, you can't have more than one ospf router except if
your having multiple ospfd daemons. Note that the router-id also
per-daemon, not per-area.

>> [snip]
>> +/// The OSPF Area. Most commonly, this is just a number, e.g. 5, but sometimes also a
>> +/// pseudo-ipaddress, e.g. 0.0.0.0
>> +#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
>> +pub struct Area(FrrWord);
>> +
>> +impl TryFrom<FrrWord> for Area {
>> +    type Error = AreaParsingError;
>> +
>> +    fn try_from(value: FrrWord) -> Result<Self, Self::Error> {
>> +        Area::new(value)
>> +    }
>> +}
>> +
>> +impl Area {
>> +    pub fn new(name: FrrWord) -> Result<Self, AreaParsingError> {
>> +        if name.as_ref().parse::<i32>().is_ok() || name.as_ref().parse::<Ipv4Addr>().is_ok() {
>
>use u32 here? otherwise area ID can be negative, which isn't allowed afaict?

true.

Thanks for the review!


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

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

* Re: [pve-devel] [PATCH proxmox-ve-rs 03/11] add intermediate fabric representation
  2025-02-28 13:57   ` Thomas Lamprecht
  2025-02-28 16:19     ` Gabriel Goller
@ 2025-03-04 17:30     ` Gabriel Goller
  2025-03-05  9:03       ` Wolfgang Bumiller
  1 sibling, 1 reply; 32+ messages in thread
From: Gabriel Goller @ 2025-03-04 17:30 UTC (permalink / raw)
  To: Thomas Lamprecht; +Cc: Wolfgang Bumiller, Proxmox VE development discussion

On 28.02.2025 14:57, Thomas Lamprecht wrote:
>Am 14.02.25 um 14:39 schrieb Gabriel Goller:
>> This adds the intermediate, type-checked fabrics config. This one is
>> parsed from the SectionConfig and can be converted into the
>> Frr-Representation.
>
>The short description of the patch is good, but I would like to see more
>rationale here about choosing this way, like benefits and trade-offs to other
>options that got evaluated, if this can/will be generic for all fabrics planned,
>..., and definitively some more rust-documentation for public types and modules.
>
>One thing I noticed below, I did not managed to do a thorough review besides
>of that yet though.

I just spoke again with Stefan and there were some doubts and are
unsure if this is actually useful or just unnecessary abstraction. Some
feedback would be very appreciated!

We planned this intermediate config as a layer between the SectionConfig
(including Vecs, PropertyStrings, etc.) and the FrrConfig, which would
hold Frr-specific stuff such as routers, interfaces, etc..

The two outer layers, so SectionConfig and FrrConfig, are very specific
to their respective config files, so they don't look that nice, nor are
easy to work with.

The intermediate layer acts as a layer above the SectionConfig types
that:
  * Correct hierarchial representation (e.g.: nodes are stored in fabrics)
  * Enforces invariants and allows to include runtime-checks (e.g.:
    BTreeMap doesn't allow duplicate nodes in fabrics, check that
    router-id is unique).
  * Doesn't use section-config-specific types such as `PropertyString`.
  * Allows us to (eventually) switch config file format bit easier and
    makes proxmox-frr easier to isolate (as an independent lib).
  * Would allow us to eventually separate section-config (stored config)
    and running-config. (Intermediate Config could be parsed out of the
    running-config.)

The Intermediate layer is generally written per-protocol as there are
protocol-specific attributes that are difficult to generalize and we
want to use the protocol-specific terminology as well. Nevertheless we
have common types (mostly the simple ones such as Hostname, Net,
RouterId), that are stored in the common proxmox-network-types crate and
get used by all of the layers and protocols.

                         ┌───────────────┐
                         │ SectionConfig │
                         └───────┬───────┘
                                 │
           ┌─────────────────────┼─────────────────────┐
           ▼                     ▼                     ▼
┌──────────────────────────────────────────────────────────┐
│                                                          │ SectionConfig-specific types
│  OspfSectionConfig    OpenFabricSectionConfig      ...   │ (proxmox-ve-config::sdn::fabric
│         │                     │                     │    │  ::openfabric::OpenFabricSectionConfig)
└─────────┼─────────────────────┼─────────────────────┼────┘
           │                     │                     │
           │                     │                     │
┌─────────┼─────────────────────┼─────────────────────┼────┐
│         ▼                     ▼                     ▼    │ Intermediate Representation
│  OspfConfig           OpenFabricConfig             ...   │ (proxmox-ve-config::sdn::fabric
│         │                     │                     │    │  ::openfabric::internal::OpenFabricConfig)
└─────────┼─────────────────────┼─────────────────────┼────┘
           │                     │                     │
           │                     │                     │
┌─────────┼─────────────────────┼─────────────────────┼───┐
│         ▼                     ▼                     ▼   │
│  OspfRouter           OpenFabricRouter             ...  │ Frr-representation
│  OspfInterface        OpenFabricInterface               │ (proxmox-frr::openfabric::OpenFabricRouter)
│                                                         │
└─────────────────────────────────────────────────────────┘


The other, simpler option would be to parse directly from the
SectionConfig to the FrrConfig. This would greatly reduce the amount of
code needed and deduplicate lots of options/parameters. Downside is that
semantic checking would be hard to do (not that we do a lot there, but
anyway) and proxmox-frr would be kinda weird including SectionConfig
types (gated by feature-flags, but still).




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

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

* Re: [pve-devel] [PATCH proxmox-ve-rs 01/11] add crate with common network types
  2025-03-03 15:08   ` Stefan Hanreich
@ 2025-03-05  8:28     ` Gabriel Goller
  0 siblings, 0 replies; 32+ messages in thread
From: Gabriel Goller @ 2025-03-05  8:28 UTC (permalink / raw)
  To: Stefan Hanreich; +Cc: Proxmox VE development discussion

On 03.03.2025 16:08, Stefan Hanreich wrote:
>Maybe we should think about internally representing the Network Entity
>Title and its parts as bytes rather than strings? So [u8; n]? Since in
>practice it's exactly that. I think this would simplify a lot of the
>methods here, and obsolete some stuff like e.g. length validation. I've
>noted some parts below where I think we could benefit from that.
>
>We could also provide conversion methods for MAC addresses easily then,
>although IP addresses might be a bit awkward since they map to network
>entity titles as binary-coded decimal and there's no std support for
>that so we'd have to brew our own conversions...
>
>Also, theoretically, the Area ID is of variable length, but for our use
>case the current additional constraints are fine imo and not worth the
>hassle of implementing variable length Area IDs. Might be good to note
>this though.

Just to keep the mailing list in the loop: We discussed this offlist a
bit and turns out it's a little bit tricky as some sections (systemid
especially) have a weird length (not a power of two). This makes the
whole handling a bit weird and we're probably not gonna gain any
performance here anyway.

So this is something to maybe revisit later.

The hex-part is still valid though, will fix that for the next version!


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


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

* Re: [pve-devel] [PATCH proxmox-ve-rs 03/11] add intermediate fabric representation
  2025-03-04 17:30     ` Gabriel Goller
@ 2025-03-05  9:03       ` Wolfgang Bumiller
  0 siblings, 0 replies; 32+ messages in thread
From: Wolfgang Bumiller @ 2025-03-05  9:03 UTC (permalink / raw)
  To: Thomas Lamprecht, Proxmox VE development discussion, Wolfgang Bumiller

On Tue, Mar 04, 2025 at 06:30:50PM +0100, Gabriel Goller wrote:
> On 28.02.2025 14:57, Thomas Lamprecht wrote:
> > Am 14.02.25 um 14:39 schrieb Gabriel Goller:
> > > This adds the intermediate, type-checked fabrics config. This one is
> > > parsed from the SectionConfig and can be converted into the
> > > Frr-Representation.
> > 
> > The short description of the patch is good, but I would like to see more
> > rationale here about choosing this way, like benefits and trade-offs to other
> > options that got evaluated, if this can/will be generic for all fabrics planned,
> > ..., and definitively some more rust-documentation for public types and modules.
> > 
> > One thing I noticed below, I did not managed to do a thorough review besides
> > of that yet though.
> 
> I just spoke again with Stefan and there were some doubts and are
> unsure if this is actually useful or just unnecessary abstraction. Some
> feedback would be very appreciated!
> 
> We planned this intermediate config as a layer between the SectionConfig
> (including Vecs, PropertyStrings, etc.) and the FrrConfig, which would
> hold Frr-specific stuff such as routers, interfaces, etc..
> 
> The two outer layers, so SectionConfig and FrrConfig, are very specific
> to their respective config files, so they don't look that nice, nor are
> easy to work with.
> 
> The intermediate layer acts as a layer above the SectionConfig types
> that:
>  * Correct hierarchial representation (e.g.: nodes are stored in fabrics)
>  * Enforces invariants and allows to include runtime-checks (e.g.:
>    BTreeMap doesn't allow duplicate nodes in fabrics, check that
>    router-id is unique).
>  * Doesn't use section-config-specific types such as `PropertyString`.
>  * Allows us to (eventually) switch config file format bit easier and
>    makes proxmox-frr easier to isolate (as an independent lib).
>  * Would allow us to eventually separate section-config (stored config)
>    and running-config. (Intermediate Config could be parsed out of the
>    running-config.)
> 
> The Intermediate layer is generally written per-protocol as there are
> protocol-specific attributes that are difficult to generalize and we
> want to use the protocol-specific terminology as well. Nevertheless we
> have common types (mostly the simple ones such as Hostname, Net,
> RouterId), that are stored in the common proxmox-network-types crate and
> get used by all of the layers and protocols.
> 
>                         ┌───────────────┐
>                         │ SectionConfig │
>                         └───────┬───────┘
>                                 │
>           ┌─────────────────────┼─────────────────────┐
>           ▼                     ▼                     ▼
> ┌──────────────────────────────────────────────────────────┐
> │                                                          │ SectionConfig-specific types
> │  OspfSectionConfig    OpenFabricSectionConfig      ...   │ (proxmox-ve-config::sdn::fabric
> │         │                     │                     │    │  ::openfabric::OpenFabricSectionConfig)
> └─────────┼─────────────────────┼─────────────────────┼────┘
>           │                     │                     │
>           │                     │                     │
> ┌─────────┼─────────────────────┼─────────────────────┼────┐
> │         ▼                     ▼                     ▼    │ Intermediate Representation
> │  OspfConfig           OpenFabricConfig             ...   │ (proxmox-ve-config::sdn::fabric
> │         │                     │                     │    │  ::openfabric::internal::OpenFabricConfig)
> └─────────┼─────────────────────┼─────────────────────┼────┘
>           │                     │                     │
>           │                     │                     │
> ┌─────────┼─────────────────────┼─────────────────────┼───┐
> │         ▼                     ▼                     ▼   │
> │  OspfRouter           OpenFabricRouter             ...  │ Frr-representation
> │  OspfInterface        OpenFabricInterface               │ (proxmox-frr::openfabric::OpenFabricRouter)
> │                                                         │
> └─────────────────────────────────────────────────────────┘
> 
> 
> The other, simpler option would be to parse directly from the
> SectionConfig to the FrrConfig. This would greatly reduce the amount of
> code needed and deduplicate lots of options/parameters. Downside is that
> semantic checking would be hard to do (not that we do a lot there, but
> anyway) and proxmox-frr would be kinda weird including SectionConfig
> types (gated by feature-flags, but still).

So, the question is with or without intermediate types.

If I'm reading this right, currently the intermediate types and
section-config types live in the same crate.
Currently `proxmox-frr` depends on `proxmox-ve-config`.
I think this goes a bit against the idea of potentially separating it,
or at least, if we start off this way, I'm not convinced the task would
be much easier than if we skip the intermediate rep.
For it to be effective for later, the dependency would have to be
reversed: ve-config would depend on frr, frr would *not* depend on
ve-config, for shared types such as `Hostname` we'd need some separate
crate (or frr would copy the portions it needs), and ve-config would
have the code to convert to frr types.

If the extra type layer allows a more lean implementation further down
the stack and can validate some common basic things and get rid of
having to deal with schema stuff, it might still be a win. Encoding
invariants and limitations in the type system usually helps with code
maintenance after all. (And fewer explicit dependencies on the schema
crate will make future bumps less painful...)

Also, I think *dropping* the intermediate layer later would probably be
less work on than *adding* it later.


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

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

* Re: [pve-devel] [PATCH proxmox-ve-rs 03/11] add intermediate fabric representation
  2025-03-04  8:45   ` Stefan Hanreich
@ 2025-03-05  9:09     ` Gabriel Goller
  0 siblings, 0 replies; 32+ messages in thread
From: Gabriel Goller @ 2025-03-05  9:09 UTC (permalink / raw)
  To: Stefan Hanreich; +Cc: pve-devel

>> [snip]
>> +#[derive(Serialize, Deserialize, Debug, Default)]
>> +pub struct FabricConfig {
>> +    openfabric: Option<openfabric::internal::OpenFabricConfig>,
>> +    ospf: Option<ospf::internal::OspfConfig>,
>> +}
>> +
>> +impl FabricConfig {
>> +    pub fn new(raw_openfabric: &str, raw_ospf: &str) -> Result<Self, anyhow::Error> {
>> +        let openfabric =
>> +            openfabric::internal::OpenFabricConfig::default(raw_openfabric)?;
>> +        let ospf = ospf::internal::OspfConfig::default(raw_ospf)?;
>
>Maybe rename the two methods to new, since default usually has no
>arguments and this kinda breaks with this convention?

Good point.

>> +#[derive(Error, Debug)]
>> +pub enum IntegerRangeError {
>> +    #[error("The value must be between {min} and {max} seconds")]
>> +    OutOfRange { min: i32, max: i32 },
>> +    #[error("Error parsing to number")]
>> +    ParsingError(#[from] ParseIntError),
>> +}
>> +
>> +#[derive(Serialize, Deserialize, Hash, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
>
>derive Copy for ergonomics

Done – also for all the other types.

>> +    impl TryFrom<InterfaceProperties> for Interface {
>> +        type Error = OpenFabricConfigError;
>> +
>> +        fn try_from(value: InterfaceProperties) -> Result<Self, Self::Error> {
>> +            Ok(Interface {
>> +                name: value.name.clone(),
>> +                passive: value.passive(),
>> +                hello_interval: value.hello_interval,
>> +                csnp_interval: value.csnp_interval,
>> +                hello_multiplier: value.hello_multiplier,
>> +            })
>> +        }
>> +    }
>
>are we anticipating this to be fallible in the future?

Hmm not really. These simple properties all have the same type in the
SectionConfig so they are validated already. Same goes for a few other
SectionConfig -> IntermediateConfig conversions.
Changed them to simple From impls.

Note that a few (e.g. Node) conversions need to be TryFrom as we need to
parse the router_id as an IpV4Addr.

Thanks for the review!


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

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

* Re: [pve-devel] [PATCH proxmox-perl-rs 04/11] fabrics: add CRUD and generate fabrics methods
  2025-03-04  9:28   ` Stefan Hanreich
@ 2025-03-05 10:20     ` Gabriel Goller
  0 siblings, 0 replies; 32+ messages in thread
From: Gabriel Goller @ 2025-03-05 10:20 UTC (permalink / raw)
  To: Stefan Hanreich; +Cc: Proxmox VE development discussion

>> [snip]
>> +    impl PerlSectionConfig<OpenFabricSectionConfig> {
>> +        pub fn add_fabric(&self, new_config: AddFabric) -> Result<(), anyhow::Error> {
>> +            let fabricid = FabricId::from(new_config.name).to_string();
>
>Could we simplify this method and the ones below by just using the
>concrete types (here FabricId) inside the argument structs (AddFabric)?
>There's potential for quite a few here afaict, also with the
>Option<u16>'s. Would save us a lot of conversion / validation logic if
>we just did it at deserialization.

Yep, that would work. We just need to change the serde deserialize
override to be generic.

>I pointed out some instances below.
>
>I guess the error messages would be a bit worse then?

Nope, they are quite the same. We can just wrap them in serde custom
errors.

>> +            let new_fabric = OpenFabricSectionConfig::Fabric(FabricSection {
>> +                hello_interval: new_config
>> +                    .hello_interval
>> +                    .map(|x| x.try_into())
>> +                    .transpose()?,
>> +            });
>> +            let mut config = self.section_config.lock().unwrap();
>> +            if config.sections.contains_key(&fabricid) {
>> +                anyhow::bail!("fabric already exists");
>> +            }
>> +            config.sections.insert(fabricid, new_fabric);
>
>try_insert instead of contains_key + insert?

still deprecated :)

>> [snip]
>> +            let mut config = self.section_config.lock().unwrap();
>> +            if !config.sections.contains_key(&nodeid) {
>> +                anyhow::bail!("node not found");
>> +            }
>> +            config.sections.entry(nodeid).and_modify(|n| {
>> +                if let OpenFabricSectionConfig::Node(n) = n {
>> +                    n.net = net;
>> +                    n.interface = interfaces;
>> +                }
>> +            });
>
>wouldn't get_mut be easier here? also would save the extra contains_key

Agree, also changed everywhere else.

Thanks!


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


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

* Re: [pve-devel] [PATCH pve-manager 09/11] sdn: add Fabrics view
  2025-03-04  9:57   ` Stefan Hanreich
@ 2025-03-07 15:57     ` Gabriel Goller
  0 siblings, 0 replies; 32+ messages in thread
From: Gabriel Goller @ 2025-03-07 15:57 UTC (permalink / raw)
  To: Stefan Hanreich; +Cc: pve-devel

>> +	    Proxmox.Utils.API2Request({
>> +		url: `/cluster/sdn/fabrics/`,
>> +		method: 'GET',
>> +		success: function(response, opts) {
>> +		    let ospf = Object.entries(response.result.data.ospf);
>> +		    let openfabric = Object.entries(response.result.data.openfabric);
>> +
>> +		    // add some metadata so we can merge the objects later and still know the protocol/type
>> +		    ospf = ospf.map(x => {
>> +			if (x["1"].fabric) {
>> +			    return Object.assign(x["1"].fabric, { _protocol: "ospf", _type: "fabric", name: x["0"] });
>> +			} else if (x["1"].node) {
>> +			    let id = x["0"].split("_");
>
>I think we already talked about this, but I don't really remember the
>outcome. Can we return this already from the API so we don't have to
>parse it in the frontend?

Hmm I can't remember. If we want to do this api-side we need to create
new api types, because currently we just return the SectionConfig types.
I don't think there is any way to "parse" (i.e. split at the '_') the
key and throw it into a property.

>> +			    return Object.assign(x["1"].node,
>> +				{
>> +				    _protocol: "ospf",
>> +				    _type: "node",
>> +				    node: id[1],
>> +				    fabric: id[0],
>> +				},
>> +			    );
>> +			} else {
>> +			    return x;
>> +			}
>> +		    });


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


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

* Re: [pve-devel] [PATCH pve-manager 10/11] sdn: add fabric edit/delete forms
  2025-03-04 10:07   ` Stefan Hanreich
@ 2025-03-07 16:04     ` Gabriel Goller
  0 siblings, 0 replies; 32+ messages in thread
From: Gabriel Goller @ 2025-03-07 16:04 UTC (permalink / raw)
  To: Stefan Hanreich; +Cc: pve-devel

>> diff --git a/www/manager6/sdn/fabrics/Common.js b/www/manager6/sdn/fabrics/Common.js
>> new file mode 100644
>> index 000000000000..72ec093fc928
>> --- /dev/null
>> +++ b/www/manager6/sdn/fabrics/Common.js
>> @@ -0,0 +1,222 @@
>> +Ext.define('PVE.sdn.Fabric.InterfacePanel', {
>> +    extend: 'Ext.grid.Panel',
>> +    mixins: ['Ext.form.field.Field'],
>> +
>> +    network_interfaces: undefined,
>> +
>> +    selectionChange: function(_grid, _selection) {
>> +	let me = this;
>> +	me.value = me.getSelection().map((rec) => {
>> +	    delete rec.data.cidr;
>> +	    delete rec.data.cidr6;
>> +	    delete rec.data.selected;
>> +	    return PVE.Parser.printPropertyString(rec.data);
>
>maybe we could explicitly select the fields we want to include here, so
>this doesn't break when we add new fields?

Depends on which fields :)
If we add fields to the interface, it won't break, if we add more
"display-only" fields it will break.

Anyway this is a common component, so we would need to pass/add a check for
the protocol and then select the protocol specific attributes.

>> +    updateSelectedInterfaces: function(values) {
>> +	let me = this;
>> +	if (values) {
>> +	    let recs = [];
>> +	    let store = me.getStore();
>> +
>> +	    for (const i of values) {
>> +		let rec = store.getById(i.name);
>> +		if (rec) {
>> +		    recs.push(rec);
>> +		}
>> +	    }
>> +	    me.suspendEvent('change');
>> +	    me.setSelection();
>> +	    me.setSelection(recs);
>> +	    me.resumeEvent('change');
>> +	} else {
>> +	    me.suspendEvent('change');
>> +	    me.setSelection();
>> +	    me.resumeEvent('change');
>> +	}
>
>could avoid some duplication by moving the methods calls above / below
>the if/else

I can extract the resumeEvent call, but keeping suspendEvent within each
branch is safer. If store operations fail between suspend and resume,
we'd risk permanently disabling the 'change' event listener.

Thanks!


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


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

end of thread, other threads:[~2025-03-07 16:05 UTC | newest]

Thread overview: 32+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-02-14 13:39 [pve-devel] [RFC cluster/manager/network/proxmox{-ve-rs, -perl-rs} 00/11] Add SDN Fabrics Gabriel Goller
2025-02-14 13:39 ` [pve-devel] [PATCH proxmox-ve-rs 01/11] add crate with common network types Gabriel Goller
2025-03-03 15:08   ` Stefan Hanreich
2025-03-05  8:28     ` Gabriel Goller
2025-02-14 13:39 ` [pve-devel] [PATCH proxmox-ve-rs 02/11] add proxmox-frr crate with frr types Gabriel Goller
2025-03-03 16:29   ` Stefan Hanreich
2025-03-04 16:28     ` Gabriel Goller
2025-02-14 13:39 ` [pve-devel] [PATCH proxmox-ve-rs 03/11] add intermediate fabric representation Gabriel Goller
2025-02-28 13:57   ` Thomas Lamprecht
2025-02-28 16:19     ` Gabriel Goller
2025-03-04 17:30     ` Gabriel Goller
2025-03-05  9:03       ` Wolfgang Bumiller
2025-03-04  8:45   ` Stefan Hanreich
2025-03-05  9:09     ` Gabriel Goller
2025-02-14 13:39 ` [pve-devel] [PATCH proxmox-perl-rs 04/11] fabrics: add CRUD and generate fabrics methods Gabriel Goller
2025-03-04  9:28   ` Stefan Hanreich
2025-03-05 10:20     ` Gabriel Goller
2025-02-14 13:39 ` [pve-devel] [PATCH pve-cluster 05/11] cluster: add sdn fabrics config files Gabriel Goller
2025-02-28 12:19   ` Thomas Lamprecht
2025-02-28 12:52     ` Gabriel Goller
2025-02-14 13:39 ` [pve-devel] [PATCH pve-network 06/11] add config file and common read/write methods Gabriel Goller
2025-02-14 13:39 ` [pve-devel] [PATCH pve-network 07/11] merge the frr config with the fabrics frr config on apply Gabriel Goller
2025-02-14 13:39 ` [pve-devel] [PATCH pve-network 08/11] add api endpoints for fabrics Gabriel Goller
2025-03-04  9:51   ` Stefan Hanreich
2025-02-14 13:39 ` [pve-devel] [PATCH pve-manager 09/11] sdn: add Fabrics view Gabriel Goller
2025-03-04  9:57   ` Stefan Hanreich
2025-03-07 15:57     ` Gabriel Goller
2025-02-14 13:39 ` [pve-devel] [PATCH pve-manager 10/11] sdn: add fabric edit/delete forms Gabriel Goller
2025-03-04 10:07   ` Stefan Hanreich
2025-03-07 16:04     ` Gabriel Goller
2025-02-14 13:39 ` [pve-devel] [PATCH pve-manager 11/11] network: return loopback interface on network endpoint Gabriel Goller
2025-03-03 16:58 ` [pve-devel] [RFC cluster/manager/network/proxmox{-ve-rs, -perl-rs} 00/11] Add SDN Fabrics Stefan Hanreich

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