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