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