[pve-devel] [PATCH proxmox-ve-rs 03/11] add intermediate fabric representation
Gabriel Goller
g.goller at proxmox.com
Fri Feb 14 14:39:43 CET 2025
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)?;
+
+ Ok(Self {
+ openfabric: Some(openfabric),
+ ospf: Some(ospf),
+ })
+ }
+
+ pub fn openfabric(&self) -> &Option<openfabric::internal::OpenFabricConfig>{
+ &self.openfabric
+ }
+ pub fn ospf(&self) -> &Option<ospf::internal::OspfConfig>{
+ &self.ospf
+ }
+
+ pub fn with_openfabric(config: openfabric::internal::OpenFabricConfig) -> FabricConfig {
+ Self {
+ openfabric: Some(config),
+ ospf: None,
+ }
+ }
+
+ pub fn with_ospf(config: ospf::internal::OspfConfig) -> FabricConfig {
+ Self {
+ ospf: Some(config),
+ openfabric: None,
+ }
+ }
+}
+
+pub trait FromSectionConfig
+where
+ Self: Sized + TryFrom<SectionConfigData<Self::Section>>,
+ <Self as TryFrom<SectionConfigData<Self::Section>>>::Error: std::fmt::Debug,
+{
+ type Section: ApiSectionDataEntry + DeserializeOwned;
+
+ fn from_section_config(raw: &str) -> Result<Self, anyhow::Error> {
+ let section_config_data = Self::Section::section_config()
+ .parse(Self::filename(), raw)?
+ .try_into()?;
+
+ let output = Self::try_from(section_config_data).unwrap();
+ Ok(output)
+ }
+
+ fn filename() -> String;
+}
diff --git a/proxmox-ve-config/src/sdn/fabric/openfabric.rs b/proxmox-ve-config/src/sdn/fabric/openfabric.rs
new file mode 100644
index 000000000000..531610f7d7e9
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/fabric/openfabric.rs
@@ -0,0 +1,494 @@
+use proxmox_network_types::net::Net;
+use proxmox_schema::property_string::PropertyString;
+use proxmox_sortable_macro::sortable;
+use std::{fmt::Display, num::ParseIntError, sync::OnceLock};
+
+use crate::sdn::fabric::common::serde_option_bool;
+use internal::OpenFabricConfig;
+use proxmox_schema::{
+ ApiStringFormat, ApiType, ArraySchema, BooleanSchema, IntegerSchema, ObjectSchema, Schema,
+ StringSchema,
+};
+use proxmox_section_config::{typed::ApiSectionDataEntry, SectionConfig, SectionConfigPlugin};
+use serde::{Deserialize, Serialize};
+use thiserror::Error;
+
+use super::FromSectionConfig;
+
+#[sortable]
+const FABRIC_SCHEMA: ObjectSchema = ObjectSchema::new(
+ "fabric schema",
+ &sorted!([(
+ "hello_interval",
+ true,
+ &IntegerSchema::new("OpenFabric hello_interval in seconds")
+ .minimum(1)
+ .maximum(600)
+ .schema(),
+ ),]),
+);
+
+#[sortable]
+const INTERFACE_SCHEMA: Schema = ObjectSchema::new(
+ "interface",
+ &sorted!([
+ (
+ "hello_interval",
+ true,
+ &IntegerSchema::new("OpenFabric Hello interval in seconds")
+ .minimum(1)
+ .maximum(600)
+ .schema(),
+ ),
+ (
+ "name",
+ false,
+ &StringSchema::new("Interface name")
+ .min_length(1)
+ .max_length(15)
+ .schema(),
+ ),
+ (
+ "passive",
+ true,
+ &BooleanSchema::new("OpenFabric passive mode for this interface").schema(),
+ ),
+ (
+ "csnp_interval",
+ true,
+ &IntegerSchema::new("OpenFabric csnp interval in seconds")
+ .minimum(1)
+ .maximum(600)
+ .schema()
+ ),
+ (
+ "hello_multiplier",
+ true,
+ &IntegerSchema::new("OpenFabric multiplier for Hello holding time")
+ .minimum(2)
+ .maximum(100)
+ .schema()
+ ),
+ ]),
+)
+.schema();
+
+#[sortable]
+const NODE_SCHEMA: ObjectSchema = ObjectSchema::new(
+ "node schema",
+ &sorted!([
+ (
+ "interface",
+ false,
+ &ArraySchema::new(
+ "OpenFabric name",
+ &StringSchema::new("OpenFabric Interface")
+ .format(&ApiStringFormat::PropertyString(&INTERFACE_SCHEMA))
+ .schema(),
+ )
+ .schema(),
+ ),
+ (
+ "net",
+ true,
+ &StringSchema::new("OpenFabric net").min_length(3).schema(),
+ ),
+ ]),
+);
+
+const ID_SCHEMA: Schema = StringSchema::new("id").min_length(2).schema();
+
+#[derive(Error, Debug)]
+pub enum IntegerRangeError {
+ #[error("The value must be between {min} and {max} seconds")]
+ OutOfRange { min: i32, max: i32 },
+ #[error("Error parsing to number")]
+ ParsingError(#[from] ParseIntError),
+}
+
+#[derive(Serialize, Deserialize, Hash, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+pub struct CsnpInterval(u16);
+
+impl TryFrom<u16> for CsnpInterval {
+ type Error = IntegerRangeError;
+
+ fn try_from(number: u16) -> Result<Self, Self::Error> {
+ if (1..=600).contains(&number) {
+ Ok(CsnpInterval(number))
+ } else {
+ Err(IntegerRangeError::OutOfRange { min: 1, max: 600 })
+ }
+ }
+}
+
+impl Display for CsnpInterval {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+#[derive(Serialize, Deserialize, Hash, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+pub struct HelloInterval(u16);
+
+impl TryFrom<u16> for HelloInterval {
+ type Error = IntegerRangeError;
+
+ fn try_from(number: u16) -> Result<Self, Self::Error> {
+ if (1..=600).contains(&number) {
+ Ok(HelloInterval(number))
+ } else {
+ Err(IntegerRangeError::OutOfRange { min: 1, max: 600 })
+ }
+ }
+}
+
+impl Display for HelloInterval {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+#[derive(Serialize, Deserialize, Hash, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+pub struct HelloMultiplier(u16);
+
+impl TryFrom<u16> for HelloMultiplier {
+ type Error = IntegerRangeError;
+
+ fn try_from(number: u16) -> Result<Self, Self::Error> {
+ if (2..=100).contains(&number) {
+ Ok(HelloMultiplier(number))
+ } else {
+ Err(IntegerRangeError::OutOfRange { min: 2, max: 100 })
+ }
+ }
+}
+
+impl Display for HelloMultiplier {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct FabricSection {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub hello_interval: Option<HelloInterval>,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+pub struct NodeSection {
+ pub net: Net,
+ pub interface: Vec<PropertyString<InterfaceProperties>>,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+pub struct InterfaceProperties {
+ pub name: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ #[serde(default, with = "serde_option_bool")]
+ pub passive: Option<bool>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub hello_interval: Option<HelloInterval>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub csnp_interval: Option<CsnpInterval>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub hello_multiplier: Option<HelloMultiplier>,
+}
+
+impl InterfaceProperties {
+ pub fn passive(&self) -> Option<bool> {
+ self.passive
+ }
+}
+
+impl ApiType for InterfaceProperties {
+ const API_SCHEMA: Schema = INTERFACE_SCHEMA;
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone)]
+pub enum OpenFabricSectionConfig {
+ #[serde(rename = "fabric")]
+ Fabric(FabricSection),
+ #[serde(rename = "node")]
+ Node(NodeSection),
+}
+
+impl ApiSectionDataEntry for OpenFabricSectionConfig {
+ const INTERNALLY_TAGGED: Option<&'static str> = None;
+
+ fn section_config() -> &'static SectionConfig {
+ static SC: OnceLock<SectionConfig> = OnceLock::new();
+
+ SC.get_or_init(|| {
+ let mut config = SectionConfig::new(&ID_SCHEMA);
+
+ let fabric_plugin =
+ SectionConfigPlugin::new("fabric".to_string(), None, &FABRIC_SCHEMA);
+ config.register_plugin(fabric_plugin);
+
+ let node_plugin = SectionConfigPlugin::new("node".to_string(), None, &NODE_SCHEMA);
+ config.register_plugin(node_plugin);
+
+ config
+ })
+ }
+
+ fn section_type(&self) -> &'static str {
+ match self {
+ Self::Node(_) => "node",
+ Self::Fabric(_) => "fabric",
+ }
+ }
+}
+
+pub mod internal {
+ use std::{collections::HashMap, fmt::Display, str::FromStr};
+
+ use proxmox_network_types::net::Net;
+ use serde::{Deserialize, Serialize};
+ use thiserror::Error;
+
+ use proxmox_section_config::typed::SectionConfigData;
+
+ use crate::sdn::fabric::common::{self, ConfigError, Hostname};
+
+ use super::{
+ CsnpInterval, FabricSection, FromSectionConfig, HelloInterval, HelloMultiplier,
+ InterfaceProperties, NodeSection, OpenFabricSectionConfig,
+ };
+
+ #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+ pub struct FabricId(String);
+
+ impl FabricId {
+ pub fn new(id: impl Into<String>) -> Result<Self, anyhow::Error> {
+ Ok(Self(id.into()))
+ }
+ }
+
+ impl AsRef<str> for FabricId {
+ fn as_ref(&self) -> &str {
+ &self.0
+ }
+ }
+
+ impl FromStr for FabricId {
+ type Err = anyhow::Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ Self::new(s)
+ }
+ }
+
+ impl From<String> for FabricId {
+ fn from(value: String) -> Self {
+ FabricId(value)
+ }
+ }
+
+ impl Display for FabricId {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+ }
+
+ /// The NodeId comprises node and fabric information.
+ ///
+ /// It has a format of "{fabric}_{node}". This is because the node alone doesn't suffice, we need
+ /// to store the fabric as well (a node can be apart of multiple fabrics).
+ #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+ pub struct NodeId {
+ pub fabric: FabricId,
+ pub node: Hostname,
+ }
+
+ impl NodeId {
+ pub fn new(fabric: impl Into<FabricId>, node: impl Into<Hostname>) -> NodeId {
+ Self {
+ fabric: fabric.into(),
+ node: node.into(),
+ }
+ }
+ }
+
+ impl Display for NodeId {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}_{}", self.fabric, self.node)
+ }
+ }
+
+ impl FromStr for NodeId {
+ type Err = ConfigError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ if let Some((fabric_id, node_id)) = s.split_once('_') {
+ return Ok(Self::new(fabric_id.to_string(), node_id.to_string()));
+ }
+
+ Err(ConfigError::InvalidNodeId)
+ }
+ }
+
+ #[derive(Debug, Deserialize, Serialize)]
+ pub struct OpenFabricConfig {
+ fabrics: HashMap<FabricId, FabricConfig>,
+ }
+
+ impl OpenFabricConfig {
+ pub fn fabrics(&self) -> &HashMap<FabricId, FabricConfig> {
+ &self.fabrics
+ }
+ }
+
+ #[derive(Debug, Deserialize, Serialize)]
+ pub struct FabricConfig {
+ nodes: HashMap<Hostname, NodeConfig>,
+ hello_interval: Option<HelloInterval>,
+ }
+
+ impl FabricConfig {
+ pub fn nodes(&self) -> &HashMap<Hostname, NodeConfig> {
+ &self.nodes
+ }
+ pub fn hello_interval(&self) -> &Option<HelloInterval> {
+ &self.hello_interval
+ }
+ }
+
+ impl TryFrom<FabricSection> for FabricConfig {
+ type Error = OpenFabricConfigError;
+
+ fn try_from(value: FabricSection) -> Result<Self, Self::Error> {
+ Ok(FabricConfig {
+ nodes: HashMap::new(),
+ hello_interval: value.hello_interval,
+ })
+ }
+ }
+
+ #[derive(Debug, Deserialize, Serialize)]
+ pub struct NodeConfig {
+ net: Net,
+ interfaces: Vec<Interface>,
+ }
+
+ impl NodeConfig {
+ pub fn net(&self) -> &Net {
+ &self.net
+ }
+ pub fn interfaces(&self) -> impl Iterator<Item = &Interface> + '_ {
+ self.interfaces.iter()
+ }
+ }
+
+ impl TryFrom<NodeSection> for NodeConfig {
+ type Error = OpenFabricConfigError;
+
+ fn try_from(value: NodeSection) -> Result<Self, Self::Error> {
+ Ok(NodeConfig {
+ net: value.net,
+ interfaces: value
+ .interface
+ .into_iter()
+ .map(|i| Interface::try_from(i.into_inner()).unwrap())
+ .collect(),
+ })
+ }
+ }
+
+ #[derive(Debug, Deserialize, Serialize)]
+ pub struct Interface {
+ name: String,
+ passive: Option<bool>,
+ hello_interval: Option<HelloInterval>,
+ csnp_interval: Option<CsnpInterval>,
+ hello_multiplier: Option<HelloMultiplier>,
+ }
+
+ impl Interface {
+ pub fn name(&self) -> &str {
+ &self.name
+ }
+ pub fn passive(&self) -> Option<bool> {
+ self.passive
+ }
+ pub fn hello_interval(&self) -> &Option<HelloInterval> {
+ &self.hello_interval
+ }
+ pub fn csnp_interval(&self) -> &Option<CsnpInterval> {
+ &self.csnp_interval
+ }
+ pub fn hello_multiplier(&self) -> &Option<HelloMultiplier> {
+ &self.hello_multiplier
+ }
+ }
+
+ impl TryFrom<InterfaceProperties> for Interface {
+ type Error = OpenFabricConfigError;
+
+ fn try_from(value: InterfaceProperties) -> Result<Self, Self::Error> {
+ Ok(Interface {
+ name: value.name.clone(),
+ passive: value.passive(),
+ hello_interval: value.hello_interval,
+ csnp_interval: value.csnp_interval,
+ hello_multiplier: value.hello_multiplier,
+ })
+ }
+ }
+
+ #[derive(Error, Debug)]
+ pub enum OpenFabricConfigError {
+ #[error("Unknown error occured")]
+ Unknown,
+ #[error("NodeId parse error")]
+ NodeIdError(#[from] common::ConfigError),
+ #[error("Corresponding fabric to the node not found")]
+ FabricNotFound,
+ }
+
+ impl TryFrom<SectionConfigData<OpenFabricSectionConfig>> for OpenFabricConfig {
+ type Error = OpenFabricConfigError;
+
+ fn try_from(
+ value: SectionConfigData<OpenFabricSectionConfig>,
+ ) -> Result<Self, Self::Error> {
+ let mut fabrics = HashMap::new();
+ let mut nodes = HashMap::new();
+
+ for (id, config) in value {
+ match config {
+ OpenFabricSectionConfig::Fabric(fabric_section) => {
+ fabrics.insert(FabricId::from(id), FabricConfig::try_from(fabric_section)?);
+ }
+ OpenFabricSectionConfig::Node(node_section) => {
+ nodes.insert(id.parse::<NodeId>()?, NodeConfig::try_from(node_section)?);
+ }
+ }
+ }
+
+ for (id, node) in nodes {
+ let fabric = fabrics
+ .get_mut(&id.fabric)
+ .ok_or(OpenFabricConfigError::FabricNotFound)?;
+
+ fabric.nodes.insert(id.node, node);
+ }
+ Ok(OpenFabricConfig { fabrics })
+ }
+ }
+
+ impl OpenFabricConfig {
+ pub fn default(raw: &str) -> Result<Self, anyhow::Error> {
+ OpenFabricConfig::from_section_config(raw)
+ }
+ }
+}
+
+impl FromSectionConfig for OpenFabricConfig {
+ type Section = OpenFabricSectionConfig;
+
+ fn filename() -> String {
+ "ospf.cfg".to_owned()
+ }
+}
diff --git a/proxmox-ve-config/src/sdn/fabric/ospf.rs b/proxmox-ve-config/src/sdn/fabric/ospf.rs
new file mode 100644
index 000000000000..2f2720a5759f
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/fabric/ospf.rs
@@ -0,0 +1,375 @@
+use internal::OspfConfig;
+use proxmox_schema::property_string::PropertyString;
+use proxmox_schema::ObjectSchema;
+use proxmox_schema::{ApiStringFormat, ApiType, ArraySchema, BooleanSchema, Schema, StringSchema};
+use proxmox_section_config::{typed::ApiSectionDataEntry, SectionConfig, SectionConfigPlugin};
+use proxmox_sortable_macro::sortable;
+use serde::{Deserialize, Serialize};
+use std::sync::OnceLock;
+
+use super::FromSectionConfig;
+
+#[sortable]
+const FABRIC_SCHEMA: ObjectSchema = ObjectSchema::new(
+ "fabric schema",
+ &sorted!([(
+ "area",
+ true,
+ &StringSchema::new("Area identifier").min_length(1).schema()
+ )]),
+);
+
+#[sortable]
+const INTERFACE_SCHEMA: Schema = ObjectSchema::new(
+ "interface",
+ &sorted!([
+ (
+ "name",
+ false,
+ &StringSchema::new("Interface name")
+ .min_length(1)
+ .max_length(15)
+ .schema(),
+ ),
+ (
+ "passive",
+ true,
+ &BooleanSchema::new("passive interface").schema(),
+ ),
+ ]),
+)
+.schema();
+
+#[sortable]
+const NODE_SCHEMA: ObjectSchema = ObjectSchema::new(
+ "node schema",
+ &sorted!([
+ (
+ "interface",
+ false,
+ &ArraySchema::new(
+ "OSPF name",
+ &StringSchema::new("OSPF Interface")
+ .format(&ApiStringFormat::PropertyString(&INTERFACE_SCHEMA))
+ .schema(),
+ )
+ .schema(),
+ ),
+ (
+ "router_id",
+ true,
+ &StringSchema::new("OSPF router id").min_length(3).schema(),
+ ),
+ ]),
+);
+
+const ID_SCHEMA: Schema = StringSchema::new("id").min_length(2).schema();
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+pub struct InterfaceProperties {
+ pub name: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub passive: Option<bool>,
+}
+
+impl ApiType for InterfaceProperties {
+ const API_SCHEMA: Schema = INTERFACE_SCHEMA;
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+pub struct NodeSection {
+ pub router_id: String,
+ pub interface: Vec<PropertyString<InterfaceProperties>>,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct FabricSection {}
+
+#[derive(Serialize, Deserialize, Debug, Clone)]
+pub enum OspfSectionConfig {
+ #[serde(rename = "fabric")]
+ Fabric(FabricSection),
+ #[serde(rename = "node")]
+ Node(NodeSection),
+}
+
+impl ApiSectionDataEntry for OspfSectionConfig {
+ const INTERNALLY_TAGGED: Option<&'static str> = None;
+
+ fn section_config() -> &'static SectionConfig {
+ static SC: OnceLock<SectionConfig> = OnceLock::new();
+
+ SC.get_or_init(|| {
+ let mut config = SectionConfig::new(&ID_SCHEMA);
+
+ let fabric_plugin = SectionConfigPlugin::new(
+ "fabric".to_string(),
+ Some("area".to_string()),
+ &FABRIC_SCHEMA,
+ );
+ config.register_plugin(fabric_plugin);
+
+ let node_plugin = SectionConfigPlugin::new("node".to_string(), None, &NODE_SCHEMA);
+ config.register_plugin(node_plugin);
+
+ config
+ })
+ }
+
+ fn section_type(&self) -> &'static str {
+ match self {
+ Self::Node(_) => "node",
+ Self::Fabric(_) => "fabric",
+ }
+ }
+}
+
+pub mod internal {
+ use std::{
+ collections::HashMap,
+ fmt::Display,
+ net::{AddrParseError, Ipv4Addr},
+ str::FromStr,
+ };
+
+ use serde::{Deserialize, Serialize};
+ use thiserror::Error;
+
+ use proxmox_section_config::typed::SectionConfigData;
+
+ use crate::sdn::fabric::{common::Hostname, FromSectionConfig};
+
+ use super::{FabricSection, InterfaceProperties, NodeSection, OspfSectionConfig};
+
+ #[derive(Error, Debug)]
+ pub enum NodeIdError {
+ #[error("Invalid area identifier")]
+ InvalidArea(#[from] AreaParsingError),
+ #[error("Invalid node identifier")]
+ InvalidNodeId,
+ }
+
+ /// The NodeId comprises node and fabric(area) information.
+ ///
+ /// It has a format of "{area}_{node}". This is because the node alone doesn't suffice, we need
+ /// to store the fabric as well (a node can be apart of multiple fabrics).
+ #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+ pub struct NodeId {
+ pub area: Area,
+ pub node: Hostname,
+ }
+
+ impl NodeId {
+ pub fn new(fabric: String, node: String) -> Result<NodeId, NodeIdError> {
+ Ok(Self {
+ area: fabric.try_into()?,
+ node: node.into(),
+ })
+ }
+ }
+
+ impl Display for NodeId {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}_{}", self.area, self.node)
+ }
+ }
+
+ impl FromStr for NodeId {
+ type Err = NodeIdError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ if let Some((area_id, node_id)) = s.split_once('_') {
+ return Self::new(area_id.to_owned(), node_id.to_owned());
+ }
+
+ Err(Self::Err::InvalidNodeId)
+ }
+ }
+
+ #[derive(Error, Debug)]
+ pub enum OspfConfigError {
+ #[error("Unknown error occured")]
+ Unknown,
+ #[error("Error parsing router id ip address")]
+ RouterIdParseError(#[from] AddrParseError),
+ #[error("The corresponding fabric for this node has not been found")]
+ FabricNotFound,
+ #[error("The OSPF Area could not be parsed")]
+ AreaParsingError(#[from] AreaParsingError),
+ #[error("NodeId parse error")]
+ NodeIdError(#[from] NodeIdError),
+ }
+
+ #[derive(Error, Debug)]
+ pub enum AreaParsingError {
+ #[error("Invalid area identifier. Area must be a number or a ipv4 address.")]
+ InvalidArea,
+ }
+
+ /// OSPF Area, which is unique and is used to differentiate between different ospf fabrics.
+ #[derive(Debug, Deserialize, Serialize, Hash, PartialEq, Eq, PartialOrd, Ord, Clone)]
+ pub struct Area(String);
+
+ impl Area {
+ pub fn new(area: String) -> Result<Area, AreaParsingError> {
+ if area.parse::<i32>().is_ok() || area.parse::<Ipv4Addr>().is_ok() {
+ Ok(Self(area))
+ } else {
+ Err(AreaParsingError::InvalidArea)
+ }
+ }
+ }
+
+ impl TryFrom<String> for Area {
+ type Error = AreaParsingError;
+
+ fn try_from(value: String) -> Result<Self, Self::Error> {
+ Area::new(value)
+ }
+ }
+
+ impl AsRef<str> for Area {
+ fn as_ref(&self) -> &str {
+ &self.0
+ }
+ }
+
+ impl Display for Area {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+ }
+
+ #[derive(Debug, Deserialize, Serialize)]
+ pub struct OspfConfig {
+ fabrics: HashMap<Area, FabricConfig>,
+ }
+
+ impl OspfConfig {
+ pub fn fabrics(&self) -> &HashMap<Area, FabricConfig> {
+ &self.fabrics
+ }
+ }
+
+ #[derive(Debug, Deserialize, Serialize)]
+ pub struct FabricConfig {
+ nodes: HashMap<Hostname, NodeConfig>,
+ }
+
+ impl FabricConfig {
+ pub fn nodes(&self) -> &HashMap<Hostname, NodeConfig> {
+ &self.nodes
+ }
+ }
+
+ impl TryFrom<FabricSection> for FabricConfig {
+ type Error = OspfConfigError;
+
+ fn try_from(_value: FabricSection) -> Result<Self, Self::Error> {
+ // currently no attributes here
+ Ok(FabricConfig {
+ nodes: HashMap::new(),
+ })
+ }
+ }
+
+ #[derive(Debug, Deserialize, Serialize)]
+ pub struct NodeConfig {
+ pub router_id: Ipv4Addr,
+ pub interfaces: Vec<Interface>,
+ }
+
+ impl NodeConfig {
+ pub fn router_id(&self) -> &Ipv4Addr {
+ &self.router_id
+ }
+ pub fn interfaces(&self) -> impl Iterator<Item = &Interface> + '_ {
+ self.interfaces.iter()
+ }
+ }
+
+ impl TryFrom<NodeSection> for NodeConfig {
+ type Error = OspfConfigError;
+
+ fn try_from(value: NodeSection) -> Result<Self, Self::Error> {
+ Ok(NodeConfig {
+ router_id: value.router_id.parse()?,
+ interfaces: value
+ .interface
+ .into_iter()
+ .map(|i| Interface::try_from(i.into_inner()).unwrap())
+ .collect(),
+ })
+ }
+ }
+
+ #[derive(Debug, Deserialize, Serialize)]
+ pub struct Interface {
+ name: String,
+ passive: Option<bool>,
+ }
+
+ impl Interface {
+ pub fn name(&self) -> &str {
+ &self.name
+ }
+ pub fn passive(&self) -> Option<bool> {
+ self.passive
+ }
+ }
+
+ impl TryFrom<InterfaceProperties> for Interface {
+ type Error = OspfConfigError;
+
+ fn try_from(value: InterfaceProperties) -> Result<Self, Self::Error> {
+ Ok(Interface {
+ name: value.name.clone(),
+ passive: value.passive,
+ })
+ }
+ }
+
+ impl TryFrom<SectionConfigData<OspfSectionConfig>> for OspfConfig {
+ type Error = OspfConfigError;
+
+ fn try_from(value: SectionConfigData<OspfSectionConfig>) -> Result<Self, Self::Error> {
+ let mut fabrics = HashMap::new();
+ let mut nodes = HashMap::new();
+
+ for (id, config) in value {
+ match config {
+ OspfSectionConfig::Fabric(fabric_section) => {
+ fabrics
+ .insert(Area::try_from(id)?, FabricConfig::try_from(fabric_section)?);
+ }
+ OspfSectionConfig::Node(node_section) => {
+ nodes.insert(id.parse::<NodeId>()?, NodeConfig::try_from(node_section)?);
+ }
+ }
+ }
+
+ for (id, node) in nodes {
+ let fabric = fabrics
+ .get_mut(&id.area)
+ .ok_or(OspfConfigError::FabricNotFound)?;
+
+ fabric.nodes.insert(id.node, node);
+ }
+ Ok(OspfConfig { fabrics })
+ }
+ }
+
+ impl OspfConfig {
+ pub fn default(raw: &str) -> Result<Self, anyhow::Error> {
+ OspfConfig::from_section_config(raw)
+ }
+ }
+}
+
+impl FromSectionConfig for OspfConfig {
+ type Section = OspfSectionConfig;
+
+ fn filename() -> String {
+ "ospf.cfg".to_owned()
+ }
+}
diff --git a/proxmox-ve-config/src/sdn/mod.rs b/proxmox-ve-config/src/sdn/mod.rs
index c8dc72471693..811fa21c483a 100644
--- a/proxmox-ve-config/src/sdn/mod.rs
+++ b/proxmox-ve-config/src/sdn/mod.rs
@@ -1,4 +1,5 @@
pub mod config;
+pub mod fabric;
pub mod ipam;
use std::{error::Error, fmt::Display, str::FromStr};
--
2.39.5
More information about the pve-devel
mailing list