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 DAA6F1FF168 for <inbox@lore.proxmox.com>; Tue, 4 Mar 2025 09:45:42 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id F03F618B95; Tue, 4 Mar 2025 09:45:36 +0100 (CET) Message-ID: <d35222d0-306e-4e5a-a717-e97446a583d1@proxmox.com> Date: Tue, 4 Mar 2025 09:45:27 +0100 MIME-Version: 1.0 User-Agent: Mozilla Thunderbird From: Stefan Hanreich <s.hanreich@proxmox.com> To: Gabriel Goller <g.goller@proxmox.com>, pve-devel@lists.proxmox.com References: <20250214133951.344500-1-g.goller@proxmox.com> <20250214133951.344500-4-g.goller@proxmox.com> Content-Language: en-US In-Reply-To: <20250214133951.344500-4-g.goller@proxmox.com> X-SPAM-LEVEL: Spam detection results: 0 AWL 0.671 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment RCVD_IN_VALIDITY_CERTIFIED_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_RPBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_SAFE_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [self.net, self.name, ospf.rs, mod.rs] Subject: Re: [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> On 2/14/25 14:39, Gabriel Goller wrote: > 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)?; Maybe rename the two methods to new, since default usually has no arguments and this kinda breaks with this convention? > + 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)] derive Copy for ergonomics > +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)] derive Copy for ergonomics > +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)] derive Copy for ergonomics > +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, > + }) > + } > + } are we anticipating this to be fallible in the future? > + #[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, > + }) > + } > + } are we anticipating this to be fallible in the future? > + 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}; _______________________________________________ pve-devel mailing list pve-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel