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