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

Stefan Hanreich s.hanreich at proxmox.com
Tue Mar 4 09:45:27 CET 2025



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 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)?;

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};





More information about the pve-devel mailing list