From mboxrd@z Thu Jan  1 00:00:00 1970
Return-Path: <pve-devel-bounces@lists.proxmox.com>
Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68])
	by lore.proxmox.com (Postfix) with ESMTPS id 2B40E1FF168
	for <inbox@lore.proxmox.com>; Tue,  4 Mar 2025 10:29:01 +0100 (CET)
Received: from firstgate.proxmox.com (localhost [127.0.0.1])
	by firstgate.proxmox.com (Proxmox) with ESMTP id 747E919A60;
	Tue,  4 Mar 2025 10:28:55 +0100 (CET)
Message-ID: <08c86c9f-e442-4497-8fee-aac0ce846136@proxmox.com>
Date: Tue, 4 Mar 2025 10:28:15 +0100
MIME-Version: 1.0
User-Agent: Mozilla Thunderbird
To: Proxmox VE development discussion <pve-devel@lists.proxmox.com>,
 Gabriel Goller <g.goller@proxmox.com>
References: <20250214133951.344500-1-g.goller@proxmox.com>
 <20250214133951.344500-5-g.goller@proxmox.com>
Content-Language: en-US
From: Stefan Hanreich <s.hanreich@proxmox.com>
In-Reply-To: <20250214133951.344500-5-g.goller@proxmox.com>
X-SPAM-LEVEL: Spam detection results:  0
 AWL 0.671 Adjusted score from AWL reputation of From: address
 BAYES_00                 -1.9 Bayes spam probability is 0 to 1%
 DMARC_MISSING             0.1 Missing DMARC policy
 KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment
 RCVD_IN_VALIDITY_CERTIFIED_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to
 Validity was blocked. See
 https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more
 information.
 RCVD_IN_VALIDITY_RPBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to
 Validity was blocked. See
 https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more
 information.
 RCVD_IN_VALIDITY_SAFE_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to
 Validity was blocked. See
 https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more
 information.
 SPF_HELO_NONE           0.001 SPF: HELO does not publish an SPF Record
 SPF_PASS               -0.001 SPF: sender matches SPF record
 URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See
 http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more
 information. [interface.name, mod.rs, i.name, openfabric.rs, n.net, ospf.rs,
 lib.rs, fabrics.rs, x.name]
Subject: Re: [pve-devel] [PATCH proxmox-perl-rs 04/11] fabrics: add CRUD and
 generate fabrics methods
X-BeenThere: pve-devel@lists.proxmox.com
X-Mailman-Version: 2.1.29
Precedence: list
List-Id: Proxmox VE development discussion <pve-devel.lists.proxmox.com>
List-Unsubscribe: <https://lists.proxmox.com/cgi-bin/mailman/options/pve-devel>, 
 <mailto:pve-devel-request@lists.proxmox.com?subject=unsubscribe>
List-Archive: <http://lists.proxmox.com/pipermail/pve-devel/>
List-Post: <mailto:pve-devel@lists.proxmox.com>
List-Help: <mailto:pve-devel-request@lists.proxmox.com?subject=help>
List-Subscribe: <https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel>, 
 <mailto:pve-devel-request@lists.proxmox.com?subject=subscribe>
Reply-To: Proxmox VE development discussion <pve-devel@lists.proxmox.com>
Content-Type: text/plain; charset="us-ascii"
Content-Transfer-Encoding: 7bit
Errors-To: pve-devel-bounces@lists.proxmox.com
Sender: "pve-devel" <pve-devel-bounces@lists.proxmox.com>

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