Add support to generate FRR configurations (using proxmox-frr types) from a valid FabricConfig. Introduce a feature flag for an optional dependency on proxmox-frr for the FRR types. Add the FrrConfigBuild struct, which currently holds a Valid<FabricConfig> but will eventually contain all configurations that output an FRR config. This ensures all the different configurations are combined in one struct and converted into a single valid FRR config.
When converting to FRR config types, iterate over the fabrics to find the current node (retrieved from the Perl function invocation). Match the protocol and generate the FRR configuration for this node and all its interfaces. OpenFabric supports IPv4 and IPv6, requiring special cases in the interface configuration (e.g., 'ip6 router...' vs. 'ip router...'). OpenFabric can also operate in dual-stack mode using both IPv6 and IPv4. Currently, the FRR configuration includes five object types: routers, interfaces, access-lists, route-maps, and `ip protocol` statements. Routers define the fabric and its fabric-wide properties, while interfaces specify their fabric membership and other fabric-specific properties. Access-lists, route-maps, and `ip protocol` statements, though not strictly necessary, allow us to create better routes. They edit the learned routes so that they remap the source address of outgoing packets. Access-lists create a list of prefixes matched by route-maps, which then rewrite the source address. `ip protocol` statements add the route-map to the protocol daemon, ensuring all routes from that protocol are matched. Co-authored-by: Gabriel Goller <g.gol...@proxmox.com> Signed-off-by: Stefan Hanreich <s.hanre...@proxmox.com> --- Cargo.toml | 1 + proxmox-ve-config/Cargo.toml | 5 + proxmox-ve-config/debian/control | 23 +- proxmox-ve-config/src/sdn/fabric/frr.rs | 390 ++++++++++++++++++++++++ proxmox-ve-config/src/sdn/fabric/mod.rs | 2 + proxmox-ve-config/src/sdn/frr.rs | 42 +++ proxmox-ve-config/src/sdn/mod.rs | 2 + 7 files changed, 463 insertions(+), 2 deletions(-) create mode 100644 proxmox-ve-config/src/sdn/fabric/frr.rs create mode 100644 proxmox-ve-config/src/sdn/frr.rs diff --git a/Cargo.toml b/Cargo.toml index e51fb59..29b7875 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ serde = { version = "1" } serde_with = "3" thiserror = "1.0.59" +proxmox-frr = { version = "0.1", path = "proxmox-frr" } proxmox-network-types = { version = "0.1" } proxmox-schema = { version = "4" } proxmox-sdn-types = { version = "0.1", path = "proxmox-sdn-types" } diff --git a/proxmox-ve-config/Cargo.toml b/proxmox-ve-config/Cargo.toml index c5aeba9..cea541e 100644 --- a/proxmox-ve-config/Cargo.toml +++ b/proxmox-ve-config/Cargo.toml @@ -13,6 +13,7 @@ nix = "0.26" regex = { workspace = true } const_format = { workspace = true } thiserror = { workspace = true } +tracing = "0.1.37" serde = { workspace = true, features = [ "derive" ] } serde_json = "1" @@ -20,9 +21,13 @@ serde_plain = "1" serde_with = { workspace = true } proxmox-serde = { version = "0.1.2", features = [ "perl" ]} +proxmox-frr = { workspace = true, optional = true } proxmox-network-types = { workspace = true, features = [ "api-types" ] } proxmox-schema = { workspace = true, features = [ "api-types" ] } proxmox-sdn-types = { workspace = true } proxmox-section-config = { version = "3" } proxmox-sys = "0.6.4" proxmox-sortable-macro = "0.1.3" + +[features] +frr = ["dep:proxmox-frr"] diff --git a/proxmox-ve-config/debian/control b/proxmox-ve-config/debian/control index 57d7987..807e9cd 100644 --- a/proxmox-ve-config/debian/control +++ b/proxmox-ve-config/debian/control @@ -26,7 +26,8 @@ Build-Depends-Arch: cargo:native <!nocheck>, librust-serde-json-1+default-dev <!nocheck>, librust-serde-plain-1+default-dev <!nocheck>, librust-serde-with-3+default-dev <!nocheck>, - librust-thiserror-1+default-dev (>= 1.0.59-~~) <!nocheck> + librust-thiserror-1+default-dev (>= 1.0.59-~~) <!nocheck>, + librust-tracing-0.1+default-dev (>= 0.1.37-~~) <!nocheck> Maintainer: Proxmox Support Team <supp...@proxmox.com> Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox-ve-rs.git @@ -59,7 +60,10 @@ Depends: librust-serde-json-1+default-dev, librust-serde-plain-1+default-dev, librust-serde-with-3+default-dev, - librust-thiserror-1+default-dev (>= 1.0.59-~~) + librust-thiserror-1+default-dev (>= 1.0.59-~~), + librust-tracing-0.1+default-dev (>= 0.1.37-~~) +Suggests: + librust-proxmox-ve-config+frr-dev (= ${binary:Version}) Provides: librust-proxmox-ve-config+default-dev (= ${binary:Version}), librust-proxmox-ve-config-0-dev (= ${binary:Version}), @@ -70,3 +74,18 @@ Provides: librust-proxmox-ve-config-0.2.3+default-dev (= ${binary:Version}) Description: Rust crate "proxmox-ve-config" - Rust source code Source code for Debianized Rust crate "proxmox-ve-config" + +Package: librust-proxmox-ve-config+frr-dev +Architecture: any +Multi-Arch: same +Depends: + ${misc:Depends}, + librust-proxmox-ve-config-dev (= ${binary:Version}), + librust-proxmox-frr-0.1+default-dev +Provides: + librust-proxmox-ve-config-0+frr-dev (= ${binary:Version}), + librust-proxmox-ve-config-0.2+frr-dev (= ${binary:Version}), + librust-proxmox-ve-config-0.2.3+frr-dev (= ${binary:Version}) +Description: Rust crate "proxmox-ve-config" - feature "frr" + This metapackage enables feature "frr" for the Rust proxmox-ve-config crate, by + pulling in any additional dependencies needed by that feature. diff --git a/proxmox-ve-config/src/sdn/fabric/frr.rs b/proxmox-ve-config/src/sdn/fabric/frr.rs new file mode 100644 index 0000000..1857956 --- /dev/null +++ b/proxmox-ve-config/src/sdn/fabric/frr.rs @@ -0,0 +1,390 @@ +use std::net::{IpAddr, Ipv4Addr}; +use tracing; + +use proxmox_frr::{ + ospf::{self, NetworkType}, + route_map::{ + AccessAction, AccessList, AccessListName, AccessListRule, ProtocolRouteMap, ProtocolType, + RouteMap, RouteMapMatch, RouteMapMatchInner, RouteMapName, RouteMapSet, + }, + FrrConfig, FrrWord, Interface, InterfaceName, Router, RouterName, +}; +use proxmox_network_types::ip_address::Cidr; +use proxmox_sdn_types::net::Net; + +use crate::common::valid::Valid; + +use crate::sdn::fabric::{ + section_config::{ + fabric::FabricId, + node::NodeId, + protocol::{ + openfabric::{OpenfabricInterfaceProperties, OpenfabricProperties}, + ospf::OspfInterfaceProperties, + }, + }, + FabricConfig, FabricEntry, +}; + +/// Constructs the FRR config from the the passed [Valid<FabricConfig>]. +/// +/// Iterates over the [FabricConfig] and constructs all the FRR routers, interfaces, route-maps, +/// etc. which area all appended to the passed [FrrConfig]. +pub fn build_fabric( + current_node: NodeId, + config: Valid<FabricConfig>, + frr_config: &mut FrrConfig, +) -> Result<(), anyhow::Error> { + let mut routemap_seq = 100; + let mut current_router_id: Option<Ipv4Addr> = None; + let mut current_net: Option<Net> = None; + + for (fabric_id, entry) in config.into_inner().iter() { + match entry { + FabricEntry::Openfabric(openfabric_entry) => { + // Get the current node of this fabric, if it doesn't exist, skip this fabric and + // don't generate any FRR config. + let Ok(node) = openfabric_entry.node_section(¤t_node) else { + continue; + }; + + if current_net.is_none() { + current_net = match (node.ip(), node.ip6()) { + (Some(ip), _) => Some(ip.into()), + (_, Some(ip6)) => Some(ip6.into()), + (_, _) => None, + } + } + + let net = current_net + .as_ref() + .ok_or_else(|| anyhow::anyhow!("no IPv4 or IPv6 set for node"))?; + let (router_name, router_item) = build_openfabric_router(fabric_id, net.clone())?; + frr_config.router.insert(router_name, router_item); + + // Create dummy interface for fabric + let (interface, interface_name) = build_openfabric_dummy_interface( + fabric_id, + node.ip().is_some(), + node.ip6().is_some(), + )?; + + if frr_config + .interfaces + .insert(interface_name, interface) + .is_some() + { + tracing::error!( + "An interface with the same name as the dummy interface exists" + ); + } + + let fabric = openfabric_entry.fabric_section(); + + for interface in node.properties().interfaces.iter() { + let (interface, interface_name) = build_openfabric_interface( + fabric_id, + interface, + fabric.properties(), + node.ip().is_some(), + node.ip6().is_some(), + )?; + + if frr_config + .interfaces + .insert(interface_name, interface) + .is_some() + { + tracing::warn!("An interface cannot be in multiple openfabric fabrics"); + } + } + + if let Some(ipv4cidr) = fabric.ip_prefix() { + let rule = AccessListRule { + action: AccessAction::Permit, + network: Cidr::from(ipv4cidr), + seq: None, + }; + let access_list_name = + AccessListName::new(format!("pve_openfabric_{}_ips", fabric_id)); + frr_config.access_lists.push(AccessList { + name: access_list_name, + rules: vec![rule], + }); + } + if let Some(ipv6cidr) = fabric.ip6_prefix() { + let rule = AccessListRule { + action: AccessAction::Permit, + network: Cidr::from(ipv6cidr), + seq: None, + }; + let access_list_name = + AccessListName::new(format!("pve_openfabric_{}_ip6s", fabric_id)); + frr_config.access_lists.push(AccessList { + name: access_list_name, + rules: vec![rule], + }); + } + + if let Some(ipv4) = node.ip() { + // create route-map + frr_config.routemaps.push(build_openfabric_routemap( + fabric_id, + IpAddr::V4(ipv4), + routemap_seq, + )); + routemap_seq += 10; + + let protocol_routemap = ProtocolRouteMap { + is_ipv6: false, + protocol: ProtocolType::Openfabric, + routemap_name: RouteMapName::new("pve_openfabric".to_owned()), + }; + + frr_config.protocol_routemaps.insert(protocol_routemap); + } + if let Some(ipv6) = node.ip6() { + // create route-map + frr_config.routemaps.push(build_openfabric_routemap( + fabric_id, + IpAddr::V6(ipv6), + routemap_seq, + )); + routemap_seq += 10; + + let protocol_routemap = ProtocolRouteMap { + is_ipv6: true, + protocol: ProtocolType::Openfabric, + routemap_name: RouteMapName::new("pve_openfabric6".to_owned()), + }; + + frr_config.protocol_routemaps.insert(protocol_routemap); + } + } + FabricEntry::Ospf(ospf_entry) => { + let Ok(node) = ospf_entry.node_section(¤t_node) else { + continue; + }; + + let router_id = current_router_id + .get_or_insert(node.ip().expect("node must have an ipv4 address")); + + let fabric = ospf_entry.fabric_section(); + + let frr_word_area = FrrWord::new(fabric.properties().area.to_string())?; + let frr_area = ospf::Area::new(frr_word_area)?; + let (router_name, router_item) = build_ospf_router(*router_id)?; + frr_config.router.insert(router_name, router_item); + + // Add dummy interface + let (interface, interface_name) = + build_ospf_dummy_interface(fabric_id, frr_area.clone())?; + + if frr_config + .interfaces + .insert(interface_name, interface) + .is_some() + { + tracing::error!( + "An interface with the same name as the dummy interface exists" + ); + } + + for interface in node.properties().interfaces.iter() { + let (interface, interface_name) = + build_ospf_interface(frr_area.clone(), interface)?; + + if frr_config + .interfaces + .insert(interface_name, interface) + .is_some() + { + tracing::warn!("An interface cannot be in multiple openfabric fabrics"); + } + } + + let access_list_name = AccessListName::new(format!("pve_ospf_{}_ips", fabric_id)); + + let rule = AccessListRule { + action: AccessAction::Permit, + network: Cidr::from( + fabric.ip_prefix().expect("fabric must have a ipv4 prefix"), + ), + seq: None, + }; + + frr_config.access_lists.push(AccessList { + name: access_list_name, + rules: vec![rule], + }); + + let routemap = build_ospf_dummy_routemap( + fabric_id, + node.ip().expect("node must have an ipv4 address"), + routemap_seq, + )?; + + routemap_seq += 10; + frr_config.routemaps.push(routemap); + + let protocol_routemap = ProtocolRouteMap { + is_ipv6: false, + protocol: ProtocolType::Ospf, + routemap_name: RouteMapName::new("pve_ospf".to_owned()), + }; + + frr_config.protocol_routemaps.insert(protocol_routemap); + } + } + } + Ok(()) +} + +/// Helper that builds a OSPF router with a the router_id. +fn build_ospf_router(router_id: Ipv4Addr) -> Result<(RouterName, Router), anyhow::Error> { + let ospf_router = proxmox_frr::ospf::OspfRouter { router_id }; + let router_item = Router::Ospf(ospf_router); + let router_name = RouterName::Ospf(proxmox_frr::ospf::OspfRouterName); + Ok((router_name, router_item)) +} + +/// Helper that builds a OpenFabric router from a fabric_id and a [Net]. +fn build_openfabric_router( + fabric_id: &FabricId, + net: Net, +) -> Result<(RouterName, Router), anyhow::Error> { + let ofr = proxmox_frr::openfabric::OpenfabricRouter { net }; + let router_item = Router::Openfabric(ofr); + let frr_word_id = FrrWord::new(fabric_id.to_string())?; + let router_name = RouterName::Openfabric(frr_word_id.into()); + Ok((router_name, router_item)) +} + +/// Helper that builds a OSPF interface from an [ospf::Area] and the [OspfInterfaceProperties]. +fn build_ospf_interface( + area: ospf::Area, + interface: &OspfInterfaceProperties, +) -> Result<(Interface, InterfaceName), anyhow::Error> { + let frr_interface = proxmox_frr::ospf::OspfInterface { + area, + // Interfaces are always none-passive + passive: None, + network_type: if interface.ip.is_some() { + None + } else { + Some(NetworkType::PointToPoint) + }, + }; + + let interface_name = InterfaceName::Ospf(interface.name.parse()?); + Ok((frr_interface.into(), interface_name)) +} + +/// Helper that builds the OSPF dummy interface using the [FabricId] and the [ospf::Area]. +fn build_ospf_dummy_interface( + fabric_id: &FabricId, + area: ospf::Area, +) -> Result<(Interface, InterfaceName), anyhow::Error> { + let frr_interface = proxmox_frr::ospf::OspfInterface { + area, + passive: Some(true), + network_type: None, + }; + let interface_name = InterfaceName::Openfabric(format!("dummy_{}", fabric_id).parse()?); + Ok((frr_interface.into(), interface_name)) +} + +/// Helper that builds the OpenFabric interface. +/// +/// Takes the [FabricId], [OpenfabricInterfaceProperties], [OpenfabricProperties] and flags for +/// ipv4 and ipv6. +fn build_openfabric_interface( + fabric_id: &FabricId, + interface: &OpenfabricInterfaceProperties, + fabric_config: &OpenfabricProperties, + is_ipv4: bool, + is_ipv6: bool, +) -> Result<(Interface, InterfaceName), anyhow::Error> { + let frr_word = FrrWord::new(fabric_id.to_string())?; + let mut frr_interface = proxmox_frr::openfabric::OpenfabricInterface { + fabric_id: frr_word.into(), + // Every interface is not passive by default + passive: None, + // Get properties from fabric + hello_interval: fabric_config.hello_interval, + csnp_interval: fabric_config.csnp_interval, + hello_multiplier: interface.hello_multiplier, + is_ipv4, + is_ipv6, + }; + // If no specific hello_interval is set, get default one from fabric + // config + if frr_interface.hello_interval().is_none() { + frr_interface.set_hello_interval(fabric_config.hello_interval); + } + let interface_name = InterfaceName::Openfabric(interface.name.parse()?); + Ok((frr_interface.into(), interface_name)) +} + +/// Helper that builds a OpenFabric interface using a [FabricId] and ipv4/6 flags. +fn build_openfabric_dummy_interface( + fabric_id: &FabricId, + is_ipv4: bool, + is_ipv6: bool, +) -> Result<(Interface, InterfaceName), anyhow::Error> { + let frr_word = FrrWord::new(fabric_id.to_string())?; + let frr_interface = proxmox_frr::openfabric::OpenfabricInterface { + fabric_id: frr_word.into(), + hello_interval: None, + passive: Some(true), + csnp_interval: None, + hello_multiplier: None, + is_ipv4, + is_ipv6, + }; + let interface_name = InterfaceName::Openfabric(format!("dummy_{}", fabric_id).parse()?); + Ok((frr_interface.into(), interface_name)) +} + +/// Helper that builds a RouteMap for the OpenFabric protocol. +fn build_openfabric_routemap(fabric_id: &FabricId, router_ip: IpAddr, seq: u32) -> RouteMap { + let routemap_name = match router_ip { + IpAddr::V4(_) => RouteMapName::new("pve_openfabric".to_owned()), + IpAddr::V6(_) => RouteMapName::new("pve_openfabric6".to_owned()), + }; + RouteMap { + name: routemap_name.clone(), + seq, + action: AccessAction::Permit, + matches: vec![match router_ip { + IpAddr::V4(_) => RouteMapMatch::V4(RouteMapMatchInner::IpAddress(AccessListName::new( + format!("pve_openfabric_{fabric_id}_ips"), + ))), + IpAddr::V6(_) => RouteMapMatch::V6(RouteMapMatchInner::IpAddress(AccessListName::new( + format!("pve_openfabric_{fabric_id}_ip6s"), + ))), + }], + sets: vec![RouteMapSet::IpSrc(router_ip)], + } +} + +/// Helper that builds a RouteMap for the OSPF protocol. +fn build_ospf_dummy_routemap( + fabric_id: &FabricId, + router_ip: Ipv4Addr, + seq: u32, +) -> Result<RouteMap, anyhow::Error> { + let routemap_name = RouteMapName::new("pve_ospf".to_owned()); + // create route-map + let routemap = RouteMap { + name: routemap_name.clone(), + seq, + action: AccessAction::Permit, + matches: vec![RouteMapMatch::V4(RouteMapMatchInner::IpAddress( + AccessListName::new(format!("pve_ospf_{fabric_id}_ips")), + ))], + sets: vec![RouteMapSet::IpSrc(IpAddr::from(router_ip))], + }; + + Ok(routemap) +} diff --git a/proxmox-ve-config/src/sdn/fabric/mod.rs b/proxmox-ve-config/src/sdn/fabric/mod.rs index 57e2e6f..9cd9e37 100644 --- a/proxmox-ve-config/src/sdn/fabric/mod.rs +++ b/proxmox-ve-config/src/sdn/fabric/mod.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "frr")] +pub mod frr; pub mod section_config; use std::collections::{BTreeMap, HashSet}; diff --git a/proxmox-ve-config/src/sdn/frr.rs b/proxmox-ve-config/src/sdn/frr.rs new file mode 100644 index 0000000..f7929c1 --- /dev/null +++ b/proxmox-ve-config/src/sdn/frr.rs @@ -0,0 +1,42 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use proxmox_frr::FrrConfig; + +use crate::common::valid::Valid; +use crate::sdn::fabric::{section_config::node::NodeId, FabricConfig}; + +/// Builder that helps constructing the FrrConfig. +/// +/// The goal is to have one struct collect all the rust-based configurations and then construct the +/// [`FrrConfig`] from it using the build method. In the future the controller configuration will +/// be added here as well. +#[derive(Default)] +pub struct FrrConfigBuilder { + fabrics: Valid<FabricConfig>, +} + +impl FrrConfigBuilder { + /// Add fabric configuration to the builder + pub fn add_fabrics(mut self, fabric: Valid<FabricConfig>) -> FrrConfigBuilder { + self.fabrics = fabric; + self + } + + /// Build the complete [`FrrConfig`] from this builder configuration given the hostname of the + /// node for which we want to build the config. We also inject the common fabric-level options + /// into the interfaces here. (e.g. the fabric-level "hello-interval" gets added to every + /// interface if there isn't a more specific one.) + pub fn build(self, current_node: NodeId) -> Result<FrrConfig, anyhow::Error> { + let mut frr_config = FrrConfig { + router: BTreeMap::new(), + interfaces: BTreeMap::new(), + access_lists: Vec::new(), + routemaps: Vec::new(), + protocol_routemaps: BTreeSet::new(), + }; + + crate::sdn::fabric::frr::build_fabric(current_node, self.fabrics, &mut frr_config)?; + + Ok(frr_config) + } +} diff --git a/proxmox-ve-config/src/sdn/mod.rs b/proxmox-ve-config/src/sdn/mod.rs index 7a46db3..6252601 100644 --- a/proxmox-ve-config/src/sdn/mod.rs +++ b/proxmox-ve-config/src/sdn/mod.rs @@ -1,5 +1,7 @@ pub mod config; pub mod fabric; +#[cfg(feature = "frr")] +pub mod frr; pub mod ipam; use std::{error::Error, fmt::Display, str::FromStr}; -- 2.39.5 _______________________________________________ pve-devel mailing list pve-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel