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 BCD621FF164 for <inbox@lore.proxmox.com>; Fri, 14 Feb 2025 14:40:42 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id EE9F11AD34; Fri, 14 Feb 2025 14:40:11 +0100 (CET) From: Gabriel Goller <g.goller@proxmox.com> To: pve-devel@lists.proxmox.com Date: Fri, 14 Feb 2025 14:39:43 +0100 Message-Id: <20250214133951.344500-4-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-ve-rs 03/11] add intermediate fabric representation 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> This adds the intermediate, type-checked fabrics config. This one is parsed from the SectionConfig and can be converted into the Frr-Representation. Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com> Signed-off-by: Gabriel Goller <g.goller@proxmox.com> --- proxmox-ve-config/Cargo.toml | 10 +- proxmox-ve-config/debian/control | 4 +- proxmox-ve-config/src/sdn/fabric/common.rs | 90 ++++ proxmox-ve-config/src/sdn/fabric/mod.rs | 68 +++ .../src/sdn/fabric/openfabric.rs | 494 ++++++++++++++++++ proxmox-ve-config/src/sdn/fabric/ospf.rs | 375 +++++++++++++ proxmox-ve-config/src/sdn/mod.rs | 1 + 7 files changed, 1036 insertions(+), 6 deletions(-) create mode 100644 proxmox-ve-config/src/sdn/fabric/common.rs create mode 100644 proxmox-ve-config/src/sdn/fabric/mod.rs create mode 100644 proxmox-ve-config/src/sdn/fabric/openfabric.rs create mode 100644 proxmox-ve-config/src/sdn/fabric/ospf.rs diff --git a/proxmox-ve-config/Cargo.toml b/proxmox-ve-config/Cargo.toml index 0c8f6166e75d..3a0fc9fa6618 100644 --- a/proxmox-ve-config/Cargo.toml +++ b/proxmox-ve-config/Cargo.toml @@ -10,13 +10,15 @@ exclude.workspace = true log = "0.4" anyhow = "1" nix = "0.26" -thiserror = "1.0.59" +thiserror = { workspace = true } -serde = { version = "1", features = [ "derive" ] } +serde = { workspace = true, features = [ "derive" ] } +serde_with = { workspace = true } serde_json = "1" serde_plain = "1" -serde_with = "3" -proxmox-schema = "3.1.2" +proxmox-section-config = { workspace = true } +proxmox-schema = "4.0.0" proxmox-sys = "0.6.4" proxmox-sortable-macro = "0.1.3" +proxmox-network-types = { path = "../proxmox-network-types/" } diff --git a/proxmox-ve-config/debian/control b/proxmox-ve-config/debian/control index 24814c11b471..bff03afba747 100644 --- a/proxmox-ve-config/debian/control +++ b/proxmox-ve-config/debian/control @@ -9,7 +9,7 @@ Build-Depends: cargo:native, librust-log-0.4+default-dev (>= 0.4.17-~~), librust-nix-0.26+default-dev (>= 0.26.1-~~), librust-thiserror-dev (>= 1.0.59-~~), - librust-proxmox-schema-3+default-dev, + librust-proxmox-schema-4+default-dev, librust-proxmox-sortable-macro-dev, librust-proxmox-sys-dev, librust-serde-1+default-dev, @@ -33,7 +33,7 @@ Depends: librust-log-0.4+default-dev (>= 0.4.17-~~), librust-nix-0.26+default-dev (>= 0.26.1-~~), librust-thiserror-dev (>= 1.0.59-~~), - librust-proxmox-schema-3+default-dev, + librust-proxmox-schema-4+default-dev, librust-proxmox-sortable-macro-dev, librust-proxmox-sys-dev, librust-serde-1+default-dev, diff --git a/proxmox-ve-config/src/sdn/fabric/common.rs b/proxmox-ve-config/src/sdn/fabric/common.rs new file mode 100644 index 000000000000..400f5b6d6b12 --- /dev/null +++ b/proxmox-ve-config/src/sdn/fabric/common.rs @@ -0,0 +1,90 @@ +use serde::{Deserialize, Serialize}; +use std::fmt::Display; +use thiserror::Error; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Error)] +pub enum ConfigError { + #[error("node id has invalid format")] + InvalidNodeId, +} + +#[derive(Debug, Deserialize, Serialize, Clone, Eq, Hash, PartialOrd, Ord, PartialEq)] +pub struct Hostname(String); + +impl From<String> for Hostname { + fn from(value: String) -> Self { + Hostname::new(value) + } +} + +impl AsRef<str> for Hostname { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl Display for Hostname { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl Hostname { + pub fn new(name: impl Into<String>) -> Hostname { + Self(name.into()) + } +} + +// parses a bool from a string OR bool +pub mod serde_option_bool { + use std::fmt; + + use serde::{ + de::{Deserializer, Error, Visitor}, ser::Serializer + }; + + use crate::firewall::parse::parse_bool; + + pub fn deserialize<'de, D: Deserializer<'de>>( + deserializer: D, + ) -> Result<Option<bool>, D::Error> { + struct V; + + impl<'de> Visitor<'de> for V { + type Value = Option<bool>; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("a boolean-like value") + } + + fn visit_bool<E: Error>(self, v: bool) -> Result<Self::Value, E> { + Ok(Some(v)) + } + + fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> { + parse_bool(v).map_err(E::custom).map(Some) + } + + fn visit_none<E: Error>(self) -> Result<Self::Value, E> { + Ok(None) + } + + fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error> + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(self) + } + } + + deserializer.deserialize_any(V) + } + + pub fn serialize<S: Serializer>(from: &Option<bool>, serializer: S) -> Result<S::Ok, S::Error> { + if *from == Some(true) { + serializer.serialize_str("1") + } else { + serializer.serialize_str("0") + } + } +} diff --git a/proxmox-ve-config/src/sdn/fabric/mod.rs b/proxmox-ve-config/src/sdn/fabric/mod.rs new file mode 100644 index 000000000000..6453fb9bb98f --- /dev/null +++ b/proxmox-ve-config/src/sdn/fabric/mod.rs @@ -0,0 +1,68 @@ +pub mod common; +pub mod openfabric; +pub mod ospf; + +use proxmox_section_config::typed::ApiSectionDataEntry; +use proxmox_section_config::typed::SectionConfigData; +use serde::de::DeserializeOwned; +use serde::Deserialize; +use serde::Serialize; + +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct FabricConfig { + openfabric: Option<openfabric::internal::OpenFabricConfig>, + ospf: Option<ospf::internal::OspfConfig>, +} + +impl FabricConfig { + pub fn new(raw_openfabric: &str, raw_ospf: &str) -> Result<Self, anyhow::Error> { + let openfabric = + openfabric::internal::OpenFabricConfig::default(raw_openfabric)?; + let ospf = ospf::internal::OspfConfig::default(raw_ospf)?; + + Ok(Self { + openfabric: Some(openfabric), + ospf: Some(ospf), + }) + } + + pub fn openfabric(&self) -> &Option<openfabric::internal::OpenFabricConfig>{ + &self.openfabric + } + pub fn ospf(&self) -> &Option<ospf::internal::OspfConfig>{ + &self.ospf + } + + pub fn with_openfabric(config: openfabric::internal::OpenFabricConfig) -> FabricConfig { + Self { + openfabric: Some(config), + ospf: None, + } + } + + pub fn with_ospf(config: ospf::internal::OspfConfig) -> FabricConfig { + Self { + ospf: Some(config), + openfabric: None, + } + } +} + +pub trait FromSectionConfig +where + Self: Sized + TryFrom<SectionConfigData<Self::Section>>, + <Self as TryFrom<SectionConfigData<Self::Section>>>::Error: std::fmt::Debug, +{ + type Section: ApiSectionDataEntry + DeserializeOwned; + + fn from_section_config(raw: &str) -> Result<Self, anyhow::Error> { + let section_config_data = Self::Section::section_config() + .parse(Self::filename(), raw)? + .try_into()?; + + let output = Self::try_from(section_config_data).unwrap(); + Ok(output) + } + + fn filename() -> String; +} diff --git a/proxmox-ve-config/src/sdn/fabric/openfabric.rs b/proxmox-ve-config/src/sdn/fabric/openfabric.rs new file mode 100644 index 000000000000..531610f7d7e9 --- /dev/null +++ b/proxmox-ve-config/src/sdn/fabric/openfabric.rs @@ -0,0 +1,494 @@ +use proxmox_network_types::net::Net; +use proxmox_schema::property_string::PropertyString; +use proxmox_sortable_macro::sortable; +use std::{fmt::Display, num::ParseIntError, sync::OnceLock}; + +use crate::sdn::fabric::common::serde_option_bool; +use internal::OpenFabricConfig; +use proxmox_schema::{ + ApiStringFormat, ApiType, ArraySchema, BooleanSchema, IntegerSchema, ObjectSchema, Schema, + StringSchema, +}; +use proxmox_section_config::{typed::ApiSectionDataEntry, SectionConfig, SectionConfigPlugin}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use super::FromSectionConfig; + +#[sortable] +const FABRIC_SCHEMA: ObjectSchema = ObjectSchema::new( + "fabric schema", + &sorted!([( + "hello_interval", + true, + &IntegerSchema::new("OpenFabric hello_interval in seconds") + .minimum(1) + .maximum(600) + .schema(), + ),]), +); + +#[sortable] +const INTERFACE_SCHEMA: Schema = ObjectSchema::new( + "interface", + &sorted!([ + ( + "hello_interval", + true, + &IntegerSchema::new("OpenFabric Hello interval in seconds") + .minimum(1) + .maximum(600) + .schema(), + ), + ( + "name", + false, + &StringSchema::new("Interface name") + .min_length(1) + .max_length(15) + .schema(), + ), + ( + "passive", + true, + &BooleanSchema::new("OpenFabric passive mode for this interface").schema(), + ), + ( + "csnp_interval", + true, + &IntegerSchema::new("OpenFabric csnp interval in seconds") + .minimum(1) + .maximum(600) + .schema() + ), + ( + "hello_multiplier", + true, + &IntegerSchema::new("OpenFabric multiplier for Hello holding time") + .minimum(2) + .maximum(100) + .schema() + ), + ]), +) +.schema(); + +#[sortable] +const NODE_SCHEMA: ObjectSchema = ObjectSchema::new( + "node schema", + &sorted!([ + ( + "interface", + false, + &ArraySchema::new( + "OpenFabric name", + &StringSchema::new("OpenFabric Interface") + .format(&ApiStringFormat::PropertyString(&INTERFACE_SCHEMA)) + .schema(), + ) + .schema(), + ), + ( + "net", + true, + &StringSchema::new("OpenFabric net").min_length(3).schema(), + ), + ]), +); + +const ID_SCHEMA: Schema = StringSchema::new("id").min_length(2).schema(); + +#[derive(Error, Debug)] +pub enum IntegerRangeError { + #[error("The value must be between {min} and {max} seconds")] + OutOfRange { min: i32, max: i32 }, + #[error("Error parsing to number")] + ParsingError(#[from] ParseIntError), +} + +#[derive(Serialize, Deserialize, Hash, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct CsnpInterval(u16); + +impl TryFrom<u16> for CsnpInterval { + type Error = IntegerRangeError; + + fn try_from(number: u16) -> Result<Self, Self::Error> { + if (1..=600).contains(&number) { + Ok(CsnpInterval(number)) + } else { + Err(IntegerRangeError::OutOfRange { min: 1, max: 600 }) + } + } +} + +impl Display for CsnpInterval { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +#[derive(Serialize, Deserialize, Hash, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct HelloInterval(u16); + +impl TryFrom<u16> for HelloInterval { + type Error = IntegerRangeError; + + fn try_from(number: u16) -> Result<Self, Self::Error> { + if (1..=600).contains(&number) { + Ok(HelloInterval(number)) + } else { + Err(IntegerRangeError::OutOfRange { min: 1, max: 600 }) + } + } +} + +impl Display for HelloInterval { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +#[derive(Serialize, Deserialize, Hash, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct HelloMultiplier(u16); + +impl TryFrom<u16> for HelloMultiplier { + type Error = IntegerRangeError; + + fn try_from(number: u16) -> Result<Self, Self::Error> { + if (2..=100).contains(&number) { + Ok(HelloMultiplier(number)) + } else { + Err(IntegerRangeError::OutOfRange { min: 2, max: 100 }) + } + } +} + +impl Display for HelloMultiplier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct FabricSection { + #[serde(skip_serializing_if = "Option::is_none")] + pub hello_interval: Option<HelloInterval>, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct NodeSection { + pub net: Net, + pub interface: Vec<PropertyString<InterfaceProperties>>, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct InterfaceProperties { + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, with = "serde_option_bool")] + pub passive: Option<bool>, + #[serde(skip_serializing_if = "Option::is_none")] + pub hello_interval: Option<HelloInterval>, + #[serde(skip_serializing_if = "Option::is_none")] + pub csnp_interval: Option<CsnpInterval>, + #[serde(skip_serializing_if = "Option::is_none")] + pub hello_multiplier: Option<HelloMultiplier>, +} + +impl InterfaceProperties { + pub fn passive(&self) -> Option<bool> { + self.passive + } +} + +impl ApiType for InterfaceProperties { + const API_SCHEMA: Schema = INTERFACE_SCHEMA; +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum OpenFabricSectionConfig { + #[serde(rename = "fabric")] + Fabric(FabricSection), + #[serde(rename = "node")] + Node(NodeSection), +} + +impl ApiSectionDataEntry for OpenFabricSectionConfig { + const INTERNALLY_TAGGED: Option<&'static str> = None; + + fn section_config() -> &'static SectionConfig { + static SC: OnceLock<SectionConfig> = OnceLock::new(); + + SC.get_or_init(|| { + let mut config = SectionConfig::new(&ID_SCHEMA); + + let fabric_plugin = + SectionConfigPlugin::new("fabric".to_string(), None, &FABRIC_SCHEMA); + config.register_plugin(fabric_plugin); + + let node_plugin = SectionConfigPlugin::new("node".to_string(), None, &NODE_SCHEMA); + config.register_plugin(node_plugin); + + config + }) + } + + fn section_type(&self) -> &'static str { + match self { + Self::Node(_) => "node", + Self::Fabric(_) => "fabric", + } + } +} + +pub mod internal { + use std::{collections::HashMap, fmt::Display, str::FromStr}; + + use proxmox_network_types::net::Net; + use serde::{Deserialize, Serialize}; + use thiserror::Error; + + use proxmox_section_config::typed::SectionConfigData; + + use crate::sdn::fabric::common::{self, ConfigError, Hostname}; + + use super::{ + CsnpInterval, FabricSection, FromSectionConfig, HelloInterval, HelloMultiplier, + InterfaceProperties, NodeSection, OpenFabricSectionConfig, + }; + + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct FabricId(String); + + impl FabricId { + pub fn new(id: impl Into<String>) -> Result<Self, anyhow::Error> { + Ok(Self(id.into())) + } + } + + impl AsRef<str> for FabricId { + fn as_ref(&self) -> &str { + &self.0 + } + } + + impl FromStr for FabricId { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + Self::new(s) + } + } + + impl From<String> for FabricId { + fn from(value: String) -> Self { + FabricId(value) + } + } + + impl Display for FabricId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } + } + + /// The NodeId comprises node and fabric information. + /// + /// It has a format of "{fabric}_{node}". This is because the node alone doesn't suffice, we need + /// to store the fabric as well (a node can be apart of multiple fabrics). + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct NodeId { + pub fabric: FabricId, + pub node: Hostname, + } + + impl NodeId { + pub fn new(fabric: impl Into<FabricId>, node: impl Into<Hostname>) -> NodeId { + Self { + fabric: fabric.into(), + node: node.into(), + } + } + } + + impl Display for NodeId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}_{}", self.fabric, self.node) + } + } + + impl FromStr for NodeId { + type Err = ConfigError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + if let Some((fabric_id, node_id)) = s.split_once('_') { + return Ok(Self::new(fabric_id.to_string(), node_id.to_string())); + } + + Err(ConfigError::InvalidNodeId) + } + } + + #[derive(Debug, Deserialize, Serialize)] + pub struct OpenFabricConfig { + fabrics: HashMap<FabricId, FabricConfig>, + } + + impl OpenFabricConfig { + pub fn fabrics(&self) -> &HashMap<FabricId, FabricConfig> { + &self.fabrics + } + } + + #[derive(Debug, Deserialize, Serialize)] + pub struct FabricConfig { + nodes: HashMap<Hostname, NodeConfig>, + hello_interval: Option<HelloInterval>, + } + + impl FabricConfig { + pub fn nodes(&self) -> &HashMap<Hostname, NodeConfig> { + &self.nodes + } + pub fn hello_interval(&self) -> &Option<HelloInterval> { + &self.hello_interval + } + } + + impl TryFrom<FabricSection> for FabricConfig { + type Error = OpenFabricConfigError; + + fn try_from(value: FabricSection) -> Result<Self, Self::Error> { + Ok(FabricConfig { + nodes: HashMap::new(), + hello_interval: value.hello_interval, + }) + } + } + + #[derive(Debug, Deserialize, Serialize)] + pub struct NodeConfig { + net: Net, + interfaces: Vec<Interface>, + } + + impl NodeConfig { + pub fn net(&self) -> &Net { + &self.net + } + pub fn interfaces(&self) -> impl Iterator<Item = &Interface> + '_ { + self.interfaces.iter() + } + } + + impl TryFrom<NodeSection> for NodeConfig { + type Error = OpenFabricConfigError; + + fn try_from(value: NodeSection) -> Result<Self, Self::Error> { + Ok(NodeConfig { + net: value.net, + interfaces: value + .interface + .into_iter() + .map(|i| Interface::try_from(i.into_inner()).unwrap()) + .collect(), + }) + } + } + + #[derive(Debug, Deserialize, Serialize)] + pub struct Interface { + name: String, + passive: Option<bool>, + hello_interval: Option<HelloInterval>, + csnp_interval: Option<CsnpInterval>, + hello_multiplier: Option<HelloMultiplier>, + } + + impl Interface { + pub fn name(&self) -> &str { + &self.name + } + pub fn passive(&self) -> Option<bool> { + self.passive + } + pub fn hello_interval(&self) -> &Option<HelloInterval> { + &self.hello_interval + } + pub fn csnp_interval(&self) -> &Option<CsnpInterval> { + &self.csnp_interval + } + pub fn hello_multiplier(&self) -> &Option<HelloMultiplier> { + &self.hello_multiplier + } + } + + impl TryFrom<InterfaceProperties> for Interface { + type Error = OpenFabricConfigError; + + fn try_from(value: InterfaceProperties) -> Result<Self, Self::Error> { + Ok(Interface { + name: value.name.clone(), + passive: value.passive(), + hello_interval: value.hello_interval, + csnp_interval: value.csnp_interval, + hello_multiplier: value.hello_multiplier, + }) + } + } + + #[derive(Error, Debug)] + pub enum OpenFabricConfigError { + #[error("Unknown error occured")] + Unknown, + #[error("NodeId parse error")] + NodeIdError(#[from] common::ConfigError), + #[error("Corresponding fabric to the node not found")] + FabricNotFound, + } + + impl TryFrom<SectionConfigData<OpenFabricSectionConfig>> for OpenFabricConfig { + type Error = OpenFabricConfigError; + + fn try_from( + value: SectionConfigData<OpenFabricSectionConfig>, + ) -> Result<Self, Self::Error> { + let mut fabrics = HashMap::new(); + let mut nodes = HashMap::new(); + + for (id, config) in value { + match config { + OpenFabricSectionConfig::Fabric(fabric_section) => { + fabrics.insert(FabricId::from(id), FabricConfig::try_from(fabric_section)?); + } + OpenFabricSectionConfig::Node(node_section) => { + nodes.insert(id.parse::<NodeId>()?, NodeConfig::try_from(node_section)?); + } + } + } + + for (id, node) in nodes { + let fabric = fabrics + .get_mut(&id.fabric) + .ok_or(OpenFabricConfigError::FabricNotFound)?; + + fabric.nodes.insert(id.node, node); + } + Ok(OpenFabricConfig { fabrics }) + } + } + + impl OpenFabricConfig { + pub fn default(raw: &str) -> Result<Self, anyhow::Error> { + OpenFabricConfig::from_section_config(raw) + } + } +} + +impl FromSectionConfig for OpenFabricConfig { + type Section = OpenFabricSectionConfig; + + fn filename() -> String { + "ospf.cfg".to_owned() + } +} diff --git a/proxmox-ve-config/src/sdn/fabric/ospf.rs b/proxmox-ve-config/src/sdn/fabric/ospf.rs new file mode 100644 index 000000000000..2f2720a5759f --- /dev/null +++ b/proxmox-ve-config/src/sdn/fabric/ospf.rs @@ -0,0 +1,375 @@ +use internal::OspfConfig; +use proxmox_schema::property_string::PropertyString; +use proxmox_schema::ObjectSchema; +use proxmox_schema::{ApiStringFormat, ApiType, ArraySchema, BooleanSchema, Schema, StringSchema}; +use proxmox_section_config::{typed::ApiSectionDataEntry, SectionConfig, SectionConfigPlugin}; +use proxmox_sortable_macro::sortable; +use serde::{Deserialize, Serialize}; +use std::sync::OnceLock; + +use super::FromSectionConfig; + +#[sortable] +const FABRIC_SCHEMA: ObjectSchema = ObjectSchema::new( + "fabric schema", + &sorted!([( + "area", + true, + &StringSchema::new("Area identifier").min_length(1).schema() + )]), +); + +#[sortable] +const INTERFACE_SCHEMA: Schema = ObjectSchema::new( + "interface", + &sorted!([ + ( + "name", + false, + &StringSchema::new("Interface name") + .min_length(1) + .max_length(15) + .schema(), + ), + ( + "passive", + true, + &BooleanSchema::new("passive interface").schema(), + ), + ]), +) +.schema(); + +#[sortable] +const NODE_SCHEMA: ObjectSchema = ObjectSchema::new( + "node schema", + &sorted!([ + ( + "interface", + false, + &ArraySchema::new( + "OSPF name", + &StringSchema::new("OSPF Interface") + .format(&ApiStringFormat::PropertyString(&INTERFACE_SCHEMA)) + .schema(), + ) + .schema(), + ), + ( + "router_id", + true, + &StringSchema::new("OSPF router id").min_length(3).schema(), + ), + ]), +); + +const ID_SCHEMA: Schema = StringSchema::new("id").min_length(2).schema(); + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct InterfaceProperties { + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub passive: Option<bool>, +} + +impl ApiType for InterfaceProperties { + const API_SCHEMA: Schema = INTERFACE_SCHEMA; +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct NodeSection { + pub router_id: String, + pub interface: Vec<PropertyString<InterfaceProperties>>, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct FabricSection {} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum OspfSectionConfig { + #[serde(rename = "fabric")] + Fabric(FabricSection), + #[serde(rename = "node")] + Node(NodeSection), +} + +impl ApiSectionDataEntry for OspfSectionConfig { + const INTERNALLY_TAGGED: Option<&'static str> = None; + + fn section_config() -> &'static SectionConfig { + static SC: OnceLock<SectionConfig> = OnceLock::new(); + + SC.get_or_init(|| { + let mut config = SectionConfig::new(&ID_SCHEMA); + + let fabric_plugin = SectionConfigPlugin::new( + "fabric".to_string(), + Some("area".to_string()), + &FABRIC_SCHEMA, + ); + config.register_plugin(fabric_plugin); + + let node_plugin = SectionConfigPlugin::new("node".to_string(), None, &NODE_SCHEMA); + config.register_plugin(node_plugin); + + config + }) + } + + fn section_type(&self) -> &'static str { + match self { + Self::Node(_) => "node", + Self::Fabric(_) => "fabric", + } + } +} + +pub mod internal { + use std::{ + collections::HashMap, + fmt::Display, + net::{AddrParseError, Ipv4Addr}, + str::FromStr, + }; + + use serde::{Deserialize, Serialize}; + use thiserror::Error; + + use proxmox_section_config::typed::SectionConfigData; + + use crate::sdn::fabric::{common::Hostname, FromSectionConfig}; + + use super::{FabricSection, InterfaceProperties, NodeSection, OspfSectionConfig}; + + #[derive(Error, Debug)] + pub enum NodeIdError { + #[error("Invalid area identifier")] + InvalidArea(#[from] AreaParsingError), + #[error("Invalid node identifier")] + InvalidNodeId, + } + + /// The NodeId comprises node and fabric(area) information. + /// + /// It has a format of "{area}_{node}". This is because the node alone doesn't suffice, we need + /// to store the fabric as well (a node can be apart of multiple fabrics). + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct NodeId { + pub area: Area, + pub node: Hostname, + } + + impl NodeId { + pub fn new(fabric: String, node: String) -> Result<NodeId, NodeIdError> { + Ok(Self { + area: fabric.try_into()?, + node: node.into(), + }) + } + } + + impl Display for NodeId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}_{}", self.area, self.node) + } + } + + impl FromStr for NodeId { + type Err = NodeIdError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + if let Some((area_id, node_id)) = s.split_once('_') { + return Self::new(area_id.to_owned(), node_id.to_owned()); + } + + Err(Self::Err::InvalidNodeId) + } + } + + #[derive(Error, Debug)] + pub enum OspfConfigError { + #[error("Unknown error occured")] + Unknown, + #[error("Error parsing router id ip address")] + RouterIdParseError(#[from] AddrParseError), + #[error("The corresponding fabric for this node has not been found")] + FabricNotFound, + #[error("The OSPF Area could not be parsed")] + AreaParsingError(#[from] AreaParsingError), + #[error("NodeId parse error")] + NodeIdError(#[from] NodeIdError), + } + + #[derive(Error, Debug)] + pub enum AreaParsingError { + #[error("Invalid area identifier. Area must be a number or a ipv4 address.")] + InvalidArea, + } + + /// OSPF Area, which is unique and is used to differentiate between different ospf fabrics. + #[derive(Debug, Deserialize, Serialize, Hash, PartialEq, Eq, PartialOrd, Ord, Clone)] + pub struct Area(String); + + impl Area { + pub fn new(area: String) -> Result<Area, AreaParsingError> { + if area.parse::<i32>().is_ok() || area.parse::<Ipv4Addr>().is_ok() { + Ok(Self(area)) + } else { + Err(AreaParsingError::InvalidArea) + } + } + } + + impl TryFrom<String> for Area { + type Error = AreaParsingError; + + fn try_from(value: String) -> Result<Self, Self::Error> { + Area::new(value) + } + } + + impl AsRef<str> for Area { + fn as_ref(&self) -> &str { + &self.0 + } + } + + impl Display for Area { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } + } + + #[derive(Debug, Deserialize, Serialize)] + pub struct OspfConfig { + fabrics: HashMap<Area, FabricConfig>, + } + + impl OspfConfig { + pub fn fabrics(&self) -> &HashMap<Area, FabricConfig> { + &self.fabrics + } + } + + #[derive(Debug, Deserialize, Serialize)] + pub struct FabricConfig { + nodes: HashMap<Hostname, NodeConfig>, + } + + impl FabricConfig { + pub fn nodes(&self) -> &HashMap<Hostname, NodeConfig> { + &self.nodes + } + } + + impl TryFrom<FabricSection> for FabricConfig { + type Error = OspfConfigError; + + fn try_from(_value: FabricSection) -> Result<Self, Self::Error> { + // currently no attributes here + Ok(FabricConfig { + nodes: HashMap::new(), + }) + } + } + + #[derive(Debug, Deserialize, Serialize)] + pub struct NodeConfig { + pub router_id: Ipv4Addr, + pub interfaces: Vec<Interface>, + } + + impl NodeConfig { + pub fn router_id(&self) -> &Ipv4Addr { + &self.router_id + } + pub fn interfaces(&self) -> impl Iterator<Item = &Interface> + '_ { + self.interfaces.iter() + } + } + + impl TryFrom<NodeSection> for NodeConfig { + type Error = OspfConfigError; + + fn try_from(value: NodeSection) -> Result<Self, Self::Error> { + Ok(NodeConfig { + router_id: value.router_id.parse()?, + interfaces: value + .interface + .into_iter() + .map(|i| Interface::try_from(i.into_inner()).unwrap()) + .collect(), + }) + } + } + + #[derive(Debug, Deserialize, Serialize)] + pub struct Interface { + name: String, + passive: Option<bool>, + } + + impl Interface { + pub fn name(&self) -> &str { + &self.name + } + pub fn passive(&self) -> Option<bool> { + self.passive + } + } + + impl TryFrom<InterfaceProperties> for Interface { + type Error = OspfConfigError; + + fn try_from(value: InterfaceProperties) -> Result<Self, Self::Error> { + Ok(Interface { + name: value.name.clone(), + passive: value.passive, + }) + } + } + + impl TryFrom<SectionConfigData<OspfSectionConfig>> for OspfConfig { + type Error = OspfConfigError; + + fn try_from(value: SectionConfigData<OspfSectionConfig>) -> Result<Self, Self::Error> { + let mut fabrics = HashMap::new(); + let mut nodes = HashMap::new(); + + for (id, config) in value { + match config { + OspfSectionConfig::Fabric(fabric_section) => { + fabrics + .insert(Area::try_from(id)?, FabricConfig::try_from(fabric_section)?); + } + OspfSectionConfig::Node(node_section) => { + nodes.insert(id.parse::<NodeId>()?, NodeConfig::try_from(node_section)?); + } + } + } + + for (id, node) in nodes { + let fabric = fabrics + .get_mut(&id.area) + .ok_or(OspfConfigError::FabricNotFound)?; + + fabric.nodes.insert(id.node, node); + } + Ok(OspfConfig { fabrics }) + } + } + + impl OspfConfig { + pub fn default(raw: &str) -> Result<Self, anyhow::Error> { + OspfConfig::from_section_config(raw) + } + } +} + +impl FromSectionConfig for OspfConfig { + type Section = OspfSectionConfig; + + fn filename() -> String { + "ospf.cfg".to_owned() + } +} diff --git a/proxmox-ve-config/src/sdn/mod.rs b/proxmox-ve-config/src/sdn/mod.rs index c8dc72471693..811fa21c483a 100644 --- a/proxmox-ve-config/src/sdn/mod.rs +++ b/proxmox-ve-config/src/sdn/mod.rs @@ -1,4 +1,5 @@ pub mod config; +pub mod fabric; pub mod ipam; use std::{error::Error, fmt::Display, str::FromStr}; -- 2.39.5 _______________________________________________ pve-devel mailing list pve-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel