[pve-devel] [PATCH proxmox-ve-rs 02/11] add proxmox-frr crate with frr types
Gabriel Goller
g.goller at proxmox.com
Fri Feb 14 14:39:42 CET 2025
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.hanreich at proxmox.com>
Signed-off-by: Gabriel Goller <g.goller at 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
More information about the pve-devel
mailing list