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