[pve-devel] [PATCH proxmox-ve-rs 03/11] add intermediate fabric representation

Gabriel Goller g.goller at proxmox.com
Fri Feb 14 14:39:43 CET 2025


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 at proxmox.com>
Signed-off-by: Gabriel Goller <g.goller at 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





More information about the pve-devel mailing list