This crate contains types that represent the frr config. For example it
contains a `Router` and `Interface` struct. This Frr-Representation can
then be converted to the real frr config.

Co-authored-by: Stefan Hanreich <s.hanre...@proxmox.com>
Signed-off-by: Gabriel Goller <g.gol...@proxmox.com>
---
 Cargo.toml                    |   1 +
 proxmox-frr/Cargo.toml        |  25 ++++
 proxmox-frr/src/common.rs     |  54 ++++++++
 proxmox-frr/src/lib.rs        | 223 ++++++++++++++++++++++++++++++++++
 proxmox-frr/src/openfabric.rs | 137 +++++++++++++++++++++
 proxmox-frr/src/ospf.rs       | 148 ++++++++++++++++++++++
 6 files changed, 588 insertions(+)
 create mode 100644 proxmox-frr/Cargo.toml
 create mode 100644 proxmox-frr/src/common.rs
 create mode 100644 proxmox-frr/src/lib.rs
 create mode 100644 proxmox-frr/src/openfabric.rs
 create mode 100644 proxmox-frr/src/ospf.rs

diff --git a/Cargo.toml b/Cargo.toml
index e452c931e78c..ffda1233b17a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,7 @@
 [workspace]
 members = [
     "proxmox-ve-config",
+    "proxmox-frr",
     "proxmox-network-types",
 ]
 exclude = [
diff --git a/proxmox-frr/Cargo.toml b/proxmox-frr/Cargo.toml
new file mode 100644
index 000000000000..bea8a0f8bab3
--- /dev/null
+++ b/proxmox-frr/Cargo.toml
@@ -0,0 +1,25 @@
+[package]
+name = "proxmox-frr"
+version = "0.1.0"
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+homepage.workspace = true
+exclude.workspace = true
+rust-version.workspace = true
+
+[dependencies]
+thiserror = { workspace = true }
+anyhow = "1"
+tracing = "0.1"
+
+serde = { workspace = true, features = [ "derive" ] }
+serde_with = { workspace = true }
+itoa = "1.0.9"
+
+proxmox-ve-config = { path = "../proxmox-ve-config", optional = true }
+proxmox-section-config = { workspace = true, optional = true }
+proxmox-network-types = { path = "../proxmox-network-types/" }
+
+[features]
+config-ext = ["dep:proxmox-ve-config", "dep:proxmox-section-config" ]
diff --git a/proxmox-frr/src/common.rs b/proxmox-frr/src/common.rs
new file mode 100644
index 000000000000..0d99bb4da6e2
--- /dev/null
+++ b/proxmox-frr/src/common.rs
@@ -0,0 +1,54 @@
+use std::{fmt::Display, str::FromStr};
+
+use serde_with::{DeserializeFromStr, SerializeDisplay};
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum FrrWordError {
+    #[error("word is empty")]
+    IsEmpty,
+    #[error("word contains invalid character")]
+    InvalidCharacter,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, DeserializeFromStr, 
SerializeDisplay)]
+pub struct FrrWord(String);
+
+impl FrrWord {
+    pub fn new(name: String) -> Result<Self, FrrWordError> {
+        if name.is_empty() {
+            return Err(FrrWordError::IsEmpty);
+        }
+
+        if name
+            .as_bytes()
+            .iter()
+            .any(|c| !c.is_ascii() || c.is_ascii_whitespace())
+        {
+            return Err(FrrWordError::InvalidCharacter);
+        }
+
+        Ok(Self(name))
+    }
+}
+
+impl FromStr for FrrWord {
+    type Err = FrrWordError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        FrrWord::new(s.to_string())
+    }
+}
+
+impl Display for FrrWord {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+impl AsRef<str> for FrrWord {
+    fn as_ref(&self) -> &str {
+        &self.0
+    }
+}
+
diff --git a/proxmox-frr/src/lib.rs b/proxmox-frr/src/lib.rs
new file mode 100644
index 000000000000..ceef82999619
--- /dev/null
+++ b/proxmox-frr/src/lib.rs
@@ -0,0 +1,223 @@
+pub mod common;
+pub mod openfabric;
+pub mod ospf;
+
+use std::{collections::{hash_map::Entry, HashMap}, fmt::Display, str::FromStr};
+
+use common::{FrrWord, FrrWordError};
+use proxmox_ve_config::sdn::fabric::common::Hostname;
+#[cfg(feature = "config-ext")]
+use proxmox_ve_config::sdn::fabric::FabricConfig;
+
+use serde::{Deserialize, Serialize};
+use serde_with::{DeserializeFromStr, SerializeDisplay};
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum RouterNameError {
+    #[error("invalid name")]
+    InvalidName,
+    #[error("invalid frr word")]
+    FrrWordError(#[from] FrrWordError),
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
+pub enum Router {
+    OpenFabric(openfabric::OpenFabricRouter),
+    Ospf(ospf::OspfRouter),
+}
+
+impl From<openfabric::OpenFabricRouter> for Router {
+    fn from(value: openfabric::OpenFabricRouter) -> Self {
+        Router::OpenFabric(value)
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, DeserializeFromStr, 
SerializeDisplay)]
+pub enum RouterName {
+    OpenFabric(openfabric::OpenFabricRouterName),
+    Ospf(ospf::OspfRouterName),
+}
+
+impl From<openfabric::OpenFabricRouterName> for RouterName {
+    fn from(value: openfabric::OpenFabricRouterName) -> Self {
+        Self::OpenFabric(value)
+    }
+}
+
+impl Display for RouterName {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::OpenFabric(r) => r.fmt(f),
+            Self::Ospf(r) => r.fmt(f),
+        }
+    }
+}
+
+impl FromStr for RouterName {
+    type Err = RouterNameError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        if let Ok(router) = s.parse() {
+            return Ok(Self::OpenFabric(router));
+        }
+
+        Err(RouterNameError::InvalidName)
+    }
+}
+
+/// The interface name is the same on ospf and openfabric, but it is an enum 
so we can have two
+/// different entries in the hashmap. This allows us to have an interface in 
an ospf and openfabric
+/// fabric.
+#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash)]
+pub enum InterfaceName {
+    OpenFabric(FrrWord),
+    Ospf(FrrWord),
+}
+
+impl Display for InterfaceName {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            InterfaceName::OpenFabric(frr_word) => frr_word.fmt(f),
+            InterfaceName::Ospf(frr_word) => frr_word.fmt(f),
+        }
+        
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
+pub enum Interface {
+    OpenFabric(openfabric::OpenFabricInterface),
+    Ospf(ospf::OspfInterface),
+}
+
+impl From<openfabric::OpenFabricInterface> for Interface {
+    fn from(value: openfabric::OpenFabricInterface) -> Self {
+        Self::OpenFabric(value)
+    }
+}
+
+impl From<ospf::OspfInterface> for Interface {
+    fn from(value: ospf::OspfInterface) -> Self {
+        Self::Ospf(value)
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Default, Deserialize, Serialize)]
+pub struct FrrConfig {
+    router: HashMap<RouterName, Router>,
+    interfaces: HashMap<InterfaceName, Interface>,
+}
+
+impl FrrConfig {
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    #[cfg(feature = "config-ext")]
+    pub fn builder() -> FrrConfigBuilder {
+        FrrConfigBuilder::default()
+    }
+
+    pub fn router(&self) -> impl Iterator<Item = (&RouterName, &Router)> + '_ {
+        self.router.iter()
+    }
+
+    pub fn interfaces(&self) -> impl Iterator<Item = (&InterfaceName, 
&Interface)> + '_ {
+        self.interfaces.iter()
+    }
+}
+
+#[derive(Default)]
+#[cfg(feature = "config-ext")]
+pub struct FrrConfigBuilder {
+    fabrics: FabricConfig,
+    //bgp: Option<internal::BgpConfig>
+}
+
+#[cfg(feature = "config-ext")]
+impl FrrConfigBuilder {
+    pub fn add_fabrics(mut self, fabric: FabricConfig) -> FrrConfigBuilder {
+        self.fabrics = fabric;
+        self
+    }
+
+    pub fn build(self, current_node: &str) -> Result<FrrConfig, anyhow::Error> 
{
+        let mut router: HashMap<RouterName, Router> = HashMap::new();
+        let mut interfaces: HashMap<InterfaceName, Interface> = HashMap::new();
+
+        if let Some(openfabric) = self.fabrics.openfabric() {
+            // openfabric
+            openfabric
+                .fabrics()
+                .iter()
+                .try_for_each(|(fabric_id, fabric_config)| {
+                    let node_config = 
fabric_config.nodes().get(&Hostname::new(current_node));
+                    if let Some(node_config) = node_config {
+                        let ofr = 
openfabric::OpenFabricRouter::from((fabric_config, node_config));
+                        let router_item = Router::OpenFabric(ofr);
+                        let router_name = RouterName::OpenFabric(
+                            
openfabric::OpenFabricRouterName::try_from(fabric_id)?,
+                        );
+                        router.insert(router_name.clone(), router_item);
+                        node_config.interfaces().try_for_each(|interface| {
+                            let mut openfabric_interface: 
openfabric::OpenFabricInterface =
+                                (fabric_id, interface).try_into()?;
+                            // If no specific hello_interval is set, get 
default one from fabric
+                            // config
+                            if openfabric_interface.hello_interval().is_none() 
{
+                                openfabric_interface
+                                    
.set_hello_interval(fabric_config.hello_interval().clone());
+                            }
+                            let interface_name = 
InterfaceName::OpenFabric(FrrWord::from_str(interface.name())?);
+                            // Openfabric doesn't allow an interface to be in 
multiple openfabric
+                            // fabrics. Frr will just ignore it and take the 
first one.
+                            if let Entry::Vacant(e) = 
interfaces.entry(interface_name) {
+                                e.insert(openfabric_interface.into());
+                            } else {
+                                tracing::warn!("An interface cannot be in 
multiple openfabric fabrics");
+                            }
+                            Ok::<(), anyhow::Error>(())
+                        })?;
+                    } else {
+                        tracing::warn!("no node configuration for fabric 
\"{fabric_id}\" – this fabric is not configured for this node.");
+                        return Ok::<(), anyhow::Error>(());
+                    }
+                    Ok(())
+                })?;
+        }
+        if let Some(ospf) = self.fabrics.ospf() {
+            // ospf
+            ospf.fabrics()
+                .iter()
+                .try_for_each(|(fabric_id, fabric_config)| {
+                    let node_config = 
fabric_config.nodes().get(&Hostname::new(current_node));
+                    if let Some(node_config) = node_config {
+                        let ospf_router = 
ospf::OspfRouter::from((fabric_config, node_config));
+                        let router_item = Router::Ospf(ospf_router);
+                        let router_name = 
RouterName::Ospf(ospf::OspfRouterName::from(ospf::Area::try_from(fabric_id)?));
+                        router.insert(router_name.clone(), router_item);
+                        node_config.interfaces().try_for_each(|interface| {
+                            let ospf_interface: ospf::OspfInterface = 
(fabric_id, interface).try_into()?;
+
+                            let interface_name = 
InterfaceName::Ospf(FrrWord::from_str(interface.name())?);
+                            // Ospf only allows one area per interface, so one 
interface cannot be
+                            // in two areas (fabrics). Though even if this 
happens, it is not a big
+                            // problem as frr filters it out.
+                            if let Entry::Vacant(e) = 
interfaces.entry(interface_name) {
+                                e.insert(ospf_interface.into());
+                            } else {
+                                tracing::warn!("An interface cannot be in 
multiple ospf areas");
+                            }
+                            Ok::<(), anyhow::Error>(())
+                        })?;
+                    } else {
+                        tracing::warn!("no node configuration for fabric 
\"{fabric_id}\" – this fabric is not configured for this node.");
+                        return Ok::<(), anyhow::Error>(()); 
+                    }
+                    Ok(())
+                })?;
+        }
+        Ok(FrrConfig { router, interfaces })
+    }
+}
diff --git a/proxmox-frr/src/openfabric.rs b/proxmox-frr/src/openfabric.rs
new file mode 100644
index 000000000000..12cfc61236cb
--- /dev/null
+++ b/proxmox-frr/src/openfabric.rs
@@ -0,0 +1,137 @@
+use std::fmt::Debug;
+use std::{fmt::Display, str::FromStr};
+
+use proxmox_network_types::net::Net;
+use serde::{Deserialize, Serialize};
+use serde_with::{DeserializeFromStr, SerializeDisplay};
+
+#[cfg(feature = "config-ext")]
+use proxmox_ve_config::sdn::fabric::openfabric::{self, internal};
+use thiserror::Error;
+
+use crate::common::FrrWord;
+use crate::RouterNameError;
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, DeserializeFromStr, 
SerializeDisplay)]
+pub struct OpenFabricRouterName(FrrWord);
+
+impl From<FrrWord> for OpenFabricRouterName {
+    fn from(value: FrrWord) -> Self {
+        Self(value)
+    }
+}
+
+impl OpenFabricRouterName {
+    pub fn new(name: FrrWord) -> Self {
+        Self(name)
+    }
+}
+
+impl FromStr for OpenFabricRouterName {
+    type Err = RouterNameError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        if let Some(name) = s.strip_prefix("openfabric ") {
+            return Ok(Self::new(
+                FrrWord::from_str(name).map_err(|_| 
RouterNameError::InvalidName)?,
+            ));
+        }
+
+        Err(RouterNameError::InvalidName)
+    }
+}
+
+impl Display for OpenFabricRouterName {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "openfabric {}", self.0)
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
+pub struct OpenFabricRouter {
+    net: Net,
+}
+
+impl OpenFabricRouter {
+    pub fn new(net: Net) -> Self {
+        Self {
+            net,
+        }
+    }
+
+    pub fn net(&self) -> &Net {
+        &self.net
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
+pub struct OpenFabricInterface {
+    // Note: an interface can only be a part of a single fabric (so no vec 
needed here)
+    fabric_id: OpenFabricRouterName,
+    passive: Option<bool>,
+    hello_interval: Option<openfabric::HelloInterval>,
+    csnp_interval: Option<openfabric::CsnpInterval>,
+    hello_multiplier: Option<openfabric::HelloMultiplier>,
+}
+
+impl OpenFabricInterface {
+    pub fn fabric_id(&self) -> &OpenFabricRouterName {
+        &self.fabric_id
+    }
+    pub fn passive(&self) -> &Option<bool> {
+        &self.passive
+    }
+    pub fn hello_interval(&self) -> &Option<openfabric::HelloInterval> {
+        &self.hello_interval
+    }
+    pub fn csnp_interval(&self) -> &Option<openfabric::CsnpInterval> {
+        &self.csnp_interval
+    }
+    pub fn hello_multiplier(&self) -> &Option<openfabric::HelloMultiplier> {
+        &self.hello_multiplier
+    }
+    pub fn set_hello_interval(&mut self, interval: 
Option<openfabric::HelloInterval>) {
+        self.hello_interval = interval;
+    }
+}
+
+#[derive(Error, Debug)]
+pub enum OpenFabricInterfaceError {
+    #[error("Unknown error converting to OpenFabricInterface")]
+    UnknownError,
+    #[error("Error converting router name")]
+    RouterNameError(#[from] RouterNameError),
+}
+
+#[cfg(feature = "config-ext")]
+impl TryFrom<(&internal::FabricId, &internal::Interface)> for 
OpenFabricInterface {
+    type Error = OpenFabricInterfaceError;
+
+    fn try_from(value: (&internal::FabricId, &internal::Interface)) -> 
Result<Self, Self::Error> {
+        Ok(Self {
+            fabric_id: OpenFabricRouterName::try_from(value.0)?,
+            passive: value.1.passive(),
+            hello_interval: value.1.hello_interval().clone(),
+            csnp_interval: value.1.csnp_interval().clone(),
+            hello_multiplier: value.1.hello_multiplier().clone(),
+        })
+    }
+}
+
+#[cfg(feature = "config-ext")]
+impl TryFrom<&internal::FabricId> for OpenFabricRouterName {
+    type Error = RouterNameError;
+
+    fn try_from(value: &internal::FabricId) -> Result<Self, Self::Error> {
+        Ok(OpenFabricRouterName::new(FrrWord::new(value.to_string())?))
+    }
+}
+
+#[cfg(feature = "config-ext")]
+impl From<(&internal::FabricConfig, &internal::NodeConfig)> for 
OpenFabricRouter {
+    fn from(value: (&internal::FabricConfig, &internal::NodeConfig)) -> Self {
+        Self {
+            net: value.1.net().to_owned(),
+        }
+    }
+}
diff --git a/proxmox-frr/src/ospf.rs b/proxmox-frr/src/ospf.rs
new file mode 100644
index 000000000000..a14ef2c55c27
--- /dev/null
+++ b/proxmox-frr/src/ospf.rs
@@ -0,0 +1,148 @@
+use std::fmt::Debug;
+use std::net::Ipv4Addr;
+use std::{fmt::Display, str::FromStr};
+
+use serde::{Deserialize, Serialize};
+
+#[cfg(feature = "config-ext")]
+use proxmox_ve_config::sdn::fabric::ospf::internal;
+use thiserror::Error;
+
+use crate::common::{FrrWord, FrrWordError};
+
+/// The name of the ospf frr router. There is only one ospf fabric possible in 
frr (ignoring
+/// multiple invocations of the ospfd daemon) and the separation is done with 
areas. Still,
+/// different areas have the same frr router, so the name of the router is 
just "ospf" in "router
+/// ospf". This type still contains the Area so that we can insert it in the 
Hashmap.
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
+pub struct OspfRouterName(Area);
+
+impl From<Area> for OspfRouterName {
+    fn from(value: Area) -> Self {
+        Self(value)
+    }
+}
+
+impl Display for OspfRouterName {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "ospf")
+    }
+}
+
+#[derive(Error, Debug)]
+pub enum AreaParsingError {
+    #[error("Invalid area idenitifier. Area must be a number or an ipv4 
address.")]
+    InvalidArea,
+    #[error("Invalid area idenitifier. Missing 'area' prefix.")]
+    MissingPrefix,
+    #[error("Error parsing to FrrWord")]
+    FrrWordError(#[from] FrrWordError),
+}
+
+/// The OSPF Area. Most commonly, this is just a number, e.g. 5, but sometimes 
also a
+/// pseudo-ipaddress, e.g. 0.0.0.0
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
+pub struct Area(FrrWord);
+
+impl TryFrom<FrrWord> for Area {
+    type Error = AreaParsingError;
+
+    fn try_from(value: FrrWord) -> Result<Self, Self::Error> {
+        Area::new(value)
+    }
+}
+
+impl Area {
+    pub fn new(name: FrrWord) -> Result<Self, AreaParsingError> {
+        if name.as_ref().parse::<i32>().is_ok() || 
name.as_ref().parse::<Ipv4Addr>().is_ok() {
+            Ok(Self(name))
+        } else {
+            Err(AreaParsingError::InvalidArea)
+        }
+    }
+}
+
+impl FromStr for Area {
+    type Err = AreaParsingError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        if let Some(name) = s.strip_prefix("area ") {
+            return Self::new(FrrWord::from_str(name).map_err(|_| 
AreaParsingError::InvalidArea)?);
+        }
+
+        Err(AreaParsingError::MissingPrefix)
+    }
+}
+
+impl Display for Area {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "area {}", self.0)
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
+pub struct OspfRouter {
+    router_id: Ipv4Addr,
+}
+
+impl OspfRouter {
+    pub fn new(router_id: Ipv4Addr) -> Self {
+        Self { router_id }
+    }
+
+    pub fn router_id(&self) -> &Ipv4Addr {
+        &self.router_id
+    }
+}
+
+#[derive(Error, Debug)]
+pub enum OspfInterfaceParsingError {
+    #[error("Error parsing area")]
+    AreaParsingError(#[from] AreaParsingError)
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
+pub struct OspfInterface {
+    // Note: an interface can only be a part of a single area(so no vec needed 
here)
+    area: Area,
+    passive: Option<bool>,
+}
+
+impl OspfInterface {
+    pub fn area(&self) -> &Area {
+        &self.area
+    }
+    pub fn passive(&self) -> &Option<bool> {
+        &self.passive
+    }
+}
+
+#[cfg(feature = "config-ext")]
+impl TryFrom<(&internal::Area, &internal::Interface)> for OspfInterface {
+    type Error = OspfInterfaceParsingError;
+
+    fn try_from(value: (&internal::Area, &internal::Interface)) -> 
Result<Self, Self::Error> {
+        Ok(Self {
+            area: Area::try_from(value.0)?,
+            passive: value.1.passive(),
+        })
+    }
+}
+
+#[cfg(feature = "config-ext")]
+impl TryFrom<&internal::Area> for Area {
+    type Error = AreaParsingError;
+
+    fn try_from(value: &internal::Area) -> Result<Self, Self::Error> {
+        Area::new(FrrWord::new(value.to_string())?)
+    }
+}
+
+#[cfg(feature = "config-ext")]
+impl From<(&internal::FabricConfig, &internal::NodeConfig)> for OspfRouter {
+    fn from(value: (&internal::FabricConfig, &internal::NodeConfig)) -> Self {
+        Self {
+            router_id: value.1.router_id,
+        }
+    }
+}
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

Reply via email to