From: Stefan Hanreich <s.hanreich@proxmox.com>
To: Proxmox VE development discussion <pve-devel@lists.proxmox.com>,
Gabriel Goller <g.goller@proxmox.com>
Subject: Re: [pve-devel] [PATCH proxmox-perl-rs 04/11] fabrics: add CRUD and generate fabrics methods
Date: Tue, 4 Mar 2025 10:28:15 +0100 [thread overview]
Message-ID: <08c86c9f-e442-4497-8fee-aac0ce846136@proxmox.com> (raw)
In-Reply-To: <20250214133951.344500-5-g.goller@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
next prev parent reply other threads:[~2025-03-04 9:29 UTC|newest]
Thread overview: 32+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-02-14 13:39 [pve-devel] [RFC cluster/manager/network/proxmox{-ve-rs, -perl-rs} 00/11] Add SDN Fabrics Gabriel Goller
2025-02-14 13:39 ` [pve-devel] [PATCH proxmox-ve-rs 01/11] add crate with common network types Gabriel Goller
2025-03-03 15:08 ` Stefan Hanreich
2025-03-05 8:28 ` Gabriel Goller
2025-02-14 13:39 ` [pve-devel] [PATCH proxmox-ve-rs 02/11] add proxmox-frr crate with frr types Gabriel Goller
2025-03-03 16:29 ` Stefan Hanreich
2025-03-04 16:28 ` Gabriel Goller
2025-02-14 13:39 ` [pve-devel] [PATCH proxmox-ve-rs 03/11] add intermediate fabric representation Gabriel Goller
2025-02-28 13:57 ` Thomas Lamprecht
2025-02-28 16:19 ` Gabriel Goller
2025-03-04 17:30 ` Gabriel Goller
2025-03-05 9:03 ` Wolfgang Bumiller
2025-03-04 8:45 ` Stefan Hanreich
2025-03-05 9:09 ` Gabriel Goller
2025-02-14 13:39 ` [pve-devel] [PATCH proxmox-perl-rs 04/11] fabrics: add CRUD and generate fabrics methods Gabriel Goller
2025-03-04 9:28 ` Stefan Hanreich [this message]
2025-03-05 10:20 ` Gabriel Goller
2025-02-14 13:39 ` [pve-devel] [PATCH pve-cluster 05/11] cluster: add sdn fabrics config files Gabriel Goller
2025-02-28 12:19 ` Thomas Lamprecht
2025-02-28 12:52 ` Gabriel Goller
2025-02-14 13:39 ` [pve-devel] [PATCH pve-network 06/11] add config file and common read/write methods Gabriel Goller
2025-02-14 13:39 ` [pve-devel] [PATCH pve-network 07/11] merge the frr config with the fabrics frr config on apply Gabriel Goller
2025-02-14 13:39 ` [pve-devel] [PATCH pve-network 08/11] add api endpoints for fabrics Gabriel Goller
2025-03-04 9:51 ` Stefan Hanreich
2025-02-14 13:39 ` [pve-devel] [PATCH pve-manager 09/11] sdn: add Fabrics view Gabriel Goller
2025-03-04 9:57 ` Stefan Hanreich
2025-03-07 15:57 ` Gabriel Goller
2025-02-14 13:39 ` [pve-devel] [PATCH pve-manager 10/11] sdn: add fabric edit/delete forms Gabriel Goller
2025-03-04 10:07 ` Stefan Hanreich
2025-03-07 16:04 ` Gabriel Goller
2025-02-14 13:39 ` [pve-devel] [PATCH pve-manager 11/11] network: return loopback interface on network endpoint Gabriel Goller
2025-03-03 16:58 ` [pve-devel] [RFC cluster/manager/network/proxmox{-ve-rs, -perl-rs} 00/11] Add SDN Fabrics Stefan Hanreich
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=08c86c9f-e442-4497-8fee-aac0ce846136@proxmox.com \
--to=s.hanreich@proxmox.com \
--cc=g.goller@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.