[pve-devel] [PATCH proxmox-ve-rs 03/11] add intermediate fabric representation
Stefan Hanreich
s.hanreich at proxmox.com
Tue Mar 4 09:45:27 CET 2025
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 at proxmox.com>
> Signed-off-by: Gabriel Goller <g.goller at 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};
More information about the pve-devel
mailing list