[pve-devel] [PATCH proxmox-perl-rs 04/11] fabrics: add CRUD and generate fabrics methods
Gabriel Goller
g.goller at proxmox.com
Fri Feb 14 14:39:44 CET 2025
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 at 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
More information about the pve-devel
mailing list