From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 11F9D1FF137 for ; Tue, 03 Feb 2026 17:05:43 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id CB77B27282; Tue, 3 Feb 2026 17:04:07 +0100 (CET) From: Gabriel Goller To: pve-devel@lists.proxmox.com Subject: [PATCH proxmox-ve-rs 5/9] frr: add template serializer and serialize fabrics using templates Date: Tue, 3 Feb 2026 17:01:12 +0100 Message-ID: <20260203160246.353351-6-g.goller@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260203160246.353351-1-g.goller@proxmox.com> References: <20260203160246.353351-1-g.goller@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1770134496576 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.003 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: 7G4CL2GELCP7EM5XZDZLY2YOEO6BZ5VV X-Message-ID-Hash: 7G4CL2GELCP7EM5XZDZLY2YOEO6BZ5VV X-MailFrom: g.goller@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Add a new serializer which uses templates in `/etc/proxmox-frr/templates` or from the `proxmox-frr-templates` package in `/usr/share/proxmox-frr/templates` to generate the frr config file. Also update the `build_fabric` function and the tests accordingly. Signed-off-by: Gabriel Goller --- proxmox-frr/Cargo.toml | 3 + proxmox-frr/debian/control | 10 + proxmox-frr/src/ser/mod.rs | 247 ++++++-------- proxmox-frr/src/ser/openfabric.rs | 26 +- proxmox-frr/src/ser/ospf.rs | 56 +--- proxmox-frr/src/ser/route_map.rs | 175 ++++------ proxmox-frr/src/ser/serializer.rs | 259 ++++----------- proxmox-sdn-types/src/net.rs | 4 +- proxmox-ve-config/src/common/valid.rs | 4 +- proxmox-ve-config/src/sdn/fabric/frr.rs | 302 ++++++++++-------- .../fabric__openfabric_default_pve.snap | 2 +- .../fabric__openfabric_default_pve1.snap | 2 +- .../fabric__openfabric_dualstack_pve.snap | 13 +- .../fabric__openfabric_ipv6_only_pve.snap | 4 +- .../fabric__openfabric_multi_fabric_pve1.snap | 2 +- .../snapshots/fabric__ospf_default_pve.snap | 2 +- .../snapshots/fabric__ospf_default_pve1.snap | 2 +- .../fabric__ospf_multi_fabric_pve1.snap | 2 +- 18 files changed, 468 insertions(+), 647 deletions(-) diff --git a/proxmox-frr/Cargo.toml b/proxmox-frr/Cargo.toml index 31e93395cd20..560a04b42980 100644 --- a/proxmox-frr/Cargo.toml +++ b/proxmox-frr/Cargo.toml @@ -15,6 +15,9 @@ anyhow = "1" tracing = "0.1" serde = { workspace = true, features = [ "derive" ] } serde_repr = "0.1" +minijinja = { version = "2.5", features = [ "multi_template", "loader" ] } +bon = "3.7" proxmox-network-types = { workspace = true } proxmox-sdn-types = { workspace = true } +proxmox-serde = { workspace = true } diff --git a/proxmox-frr/debian/control b/proxmox-frr/debian/control index 6336ec362b45..10651e640ba3 100644 --- a/proxmox-frr/debian/control +++ b/proxmox-frr/debian/control @@ -7,8 +7,13 @@ Build-Depends-Arch: cargo:native , rustc:native (>= 1.82) , libstd-rust-dev , librust-anyhow-1+default-dev , + librust-bon-3+default-dev (>= 3.7-~~) , + librust-minijinja-2+default-dev (>= 2.5-~~) , + librust-minijinja-2+loader-dev (>= 2.5-~~) , + librust-minijinja-2+multi-template-dev (>= 2.5-~~) , librust-proxmox-network-types-0.1+default-dev (>= 0.1.1-~~) , librust-proxmox-sdn-types-0.1+default-dev , + librust-proxmox-serde-1+default-dev , librust-serde-1+default-dev , librust-serde-1+derive-dev , librust-serde-repr-0.1+default-dev , @@ -27,8 +32,13 @@ Multi-Arch: same Depends: ${misc:Depends}, librust-anyhow-1+default-dev, + librust-bon-3+default-dev (>= 3.7-~~), + librust-minijinja-2+default-dev (>= 2.5-~~), + librust-minijinja-2+loader-dev (>= 2.5-~~), + librust-minijinja-2+multi-template-dev (>= 2.5-~~), librust-proxmox-network-types-0.1+default-dev (>= 0.1.1-~~), librust-proxmox-sdn-types-0.1+default-dev, + librust-proxmox-serde-1+default-dev, librust-serde-1+default-dev, librust-serde-1+derive-dev, librust-serde-repr-0.1+default-dev, diff --git a/proxmox-frr/src/ser/mod.rs b/proxmox-frr/src/ser/mod.rs index a90397b59a9b..666845ecab74 100644 --- a/proxmox-frr/src/ser/mod.rs +++ b/proxmox-frr/src/ser/mod.rs @@ -3,104 +3,17 @@ pub mod ospf; pub mod route_map; pub mod serializer; -use std::collections::{BTreeMap, BTreeSet}; -use std::fmt::Display; +use std::collections::BTreeMap; +use std::net::IpAddr; use std::str::FromStr; -use crate::ser::route_map::{AccessList, ProtocolRouteMap, RouteMap}; +use crate::ser::route_map::{AccessListName, AccessListRule, RouteMapEntry, RouteMapName}; +use bon::Builder; +use proxmox_network_types::ip_address::Cidr; +use serde::{Deserialize, Serialize}; use thiserror::Error; -/// Generic FRR router. -/// -/// This generic FRR router contains all the protocols that we implement. -/// In FRR this is e.g.: -/// ```text -/// router openfabric test -/// !.... -/// ! or -/// router ospf -/// !.... -/// ``` -#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub enum Router { - Openfabric(openfabric::OpenfabricRouter), - Ospf(ospf::OspfRouter), -} - -impl From for Router { - fn from(value: openfabric::OpenfabricRouter) -> Self { - Router::Openfabric(value) - } -} - -/// Generic FRR routername. -/// -/// The variants represent different protocols. Some have `router `, others have -/// `router `, some only have `router `. -#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub enum RouterName { - Openfabric(openfabric::OpenfabricRouterName), - Ospf(ospf::OspfRouterName), -} - -impl From 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), - } - } -} - -/// The interface name is the same on ospf and openfabric, but it is an enum so that we can have -/// two different entries in the btreemap. This allows us to have an interface in a ospf and -/// openfabric fabric. -#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub enum InterfaceName { - Openfabric(CommonInterfaceName), - Ospf(CommonInterfaceName), -} - -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), - } - } -} - -/// Generic FRR Interface. -/// -/// In FRR config it looks like this: -/// ```text -/// interface -/// ! ... -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub enum Interface { - Openfabric(openfabric::OpenfabricInterface), - Ospf(ospf::OspfInterface), -} - -impl From for Interface { - fn from(value: openfabric::OpenfabricInterface) -> Self { - Self::Openfabric(value) - } -} - -impl From for Interface { - fn from(value: ospf::OspfInterface) -> Self { - Self::Ospf(value) - } -} - #[derive(Error, Debug)] pub enum FrrWordError { #[error("word is empty")] @@ -113,7 +26,7 @@ pub enum FrrWordError { /// /// Every string argument or value in FRR is an FrrWord. FrrWords must only contain ascii /// characters and must not have a whitespace. -#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] pub struct FrrWord(String); impl FrrWord { @@ -144,12 +57,6 @@ impl FromStr for FrrWord { } } -impl Display for FrrWord { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} - impl AsRef for FrrWord { fn as_ref(&self) -> &str { &self.0 @@ -157,7 +64,7 @@ impl AsRef for FrrWord { } #[derive(Error, Debug)] -pub enum CommonInterfaceNameError { +pub enum InterfaceNameError { #[error("interface name too long")] TooLong, } @@ -166,76 +73,128 @@ pub enum CommonInterfaceNameError { /// /// FRR itself doesn't enforce any limits, but the kernel does. Linux only allows interface names /// to be a maximum of 16 bytes. This is enforced by this struct. -#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct CommonInterfaceName(String); +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +pub struct InterfaceName(String); -impl TryFrom<&str> for CommonInterfaceName { - type Error = CommonInterfaceNameError; +impl TryFrom<&str> for InterfaceName { + type Error = InterfaceNameError; fn try_from(value: &str) -> Result { Self::new(value) } } -impl TryFrom for CommonInterfaceName { - type Error = CommonInterfaceNameError; +impl TryFrom for InterfaceName { + type Error = InterfaceNameError; fn try_from(value: String) -> Result { Self::new(value) } } -impl CommonInterfaceName { - pub fn new + Into>(s: T) -> Result { +impl InterfaceName { + pub fn new + Into>(s: T) -> Result { if s.as_ref().len() <= 15 { Ok(Self(s.into())) } else { - Err(CommonInterfaceNameError::TooLong) + Err(InterfaceNameError::TooLong) } } } -impl Display for CommonInterfaceName { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) +#[derive(Debug, Clone, Serialize, PartialEq, Eq, Deserialize)] +pub struct Interface { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub addresses: Vec, + + #[serde(flatten)] + pub properties: T, +} +impl From for Interface { + fn from(value: openfabric::OpenfabricInterface) -> Self { + Interface { + addresses: Vec::new(), + properties: value, + } } } -/// Main FRR config. -/// -/// Contains the two main frr building blocks: routers and interfaces. It also holds other -/// top-level FRR options, such as access-lists, router-maps and protocol-routemaps. This struct -/// gets generated using the `FrrConfigBuilder` in `proxmox-ve-config`. -#[derive(Clone, Debug, PartialEq, Eq, Default)] -pub struct FrrConfig { - pub router: BTreeMap, - pub interfaces: BTreeMap, - pub access_lists: Vec, - pub routemaps: Vec, - pub protocol_routemaps: BTreeSet, +impl From for Interface { + fn from(value: ospf::OspfInterface) -> Self { + Interface { + addresses: Vec::new(), + properties: value, + } + } } -impl FrrConfig { - pub fn new() -> Self { - Self::default() - } +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(untagged)] +pub enum IpOrInterface { + Ip(IpAddr), + Interface(InterfaceName), +} - pub fn router(&self) -> impl Iterator + '_ { - self.router.iter() - } +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Builder, Deserialize)] +pub struct IpRoute { + #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")] + is_ipv6: bool, + prefix: Cidr, + via: IpOrInterface, + vrf: Option, +} - pub fn interfaces(&self) -> impl Iterator + '_ { - self.interfaces.iter() - } +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum FrrProtocol { + Ospf, + Openfabric, + Bgp, +} - pub fn access_lists(&self) -> impl Iterator + '_ { - self.access_lists.iter() - } - pub fn routemaps(&self) -> impl Iterator + '_ { - self.routemaps.iter() - } +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +pub struct IpProtocolRouteMap { + pub v4: Option, + pub v6: Option, +} - pub fn protocol_routemaps(&self) -> impl Iterator + '_ { - self.protocol_routemaps.iter() - } +/// Main FRR config. +/// +/// Contains the two main frr building blocks: routers and interfaces. It also holds other +/// top-level FRR options, such as access-lists, router-maps and protocol-routemaps. This struct +/// gets generated using the `FrrConfigBuilder` in `proxmox-ve-config`. +#[derive(Clone, Debug, PartialEq, Eq, Default, Builder, Serialize, Deserialize)] +pub struct FrrConfig { + #[builder(default)] + #[serde(default)] + pub openfabric: OpenfabricFrrConfig, + #[builder(default)] + #[serde(default)] + pub ospf: OspfFrrConfig, + #[builder(default)] + #[serde(default)] + pub protocol_routemaps: BTreeMap, + + #[builder(default)] + #[serde(default)] + pub routemaps: BTreeMap>, + #[builder(default)] + #[serde(default)] + pub access_lists: BTreeMap>, +} + +#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct OpenfabricFrrConfig { + #[serde(default)] + pub router: BTreeMap, + #[serde(default)] + pub interfaces: BTreeMap>, +} + +#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct OspfFrrConfig { + #[serde(default)] + pub router: Option, + #[serde(default)] + pub interfaces: BTreeMap>, } diff --git a/proxmox-frr/src/ser/openfabric.rs b/proxmox-frr/src/ser/openfabric.rs index 0f0c65062d36..8c82c60b0de5 100644 --- a/proxmox-frr/src/ser/openfabric.rs +++ b/proxmox-frr/src/ser/openfabric.rs @@ -1,15 +1,17 @@ use std::fmt::Debug; -use std::fmt::Display; +use bon::Builder; use proxmox_sdn_types::net::Net; +use serde::Deserialize; +use serde::Serialize; use thiserror::Error; use crate::ser::FrrWord; use crate::ser::FrrWordError; /// The name of a OpenFabric router. Is an FrrWord. -#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] pub struct OpenfabricRouterName(FrrWord); impl From for OpenfabricRouterName { @@ -24,16 +26,16 @@ impl OpenfabricRouterName { } } -impl Display for OpenfabricRouterName { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "openfabric {}", self.0) +impl std::fmt::Display for OpenfabricRouterName { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + self.0.fmt(f) } } /// All the properties a OpenFabric router can hold. /// /// These can serialized with a " " space prefix as they are in the `router openfabric` block. -#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] pub struct OpenfabricRouter { /// The NET address pub net: Net, @@ -67,15 +69,25 @@ impl OpenfabricRouter { /// /// The is_ipv4 and is_ipv6 properties decide if we need to add `ip router openfabric`, `ipv6 /// router openfabric`, or both. An interface can only be part of a single fabric. -#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, Builder)] pub struct OpenfabricInterface { // Note: an interface can only be a part of a single fabric (so no vec needed here) pub fabric_id: OpenfabricRouterName, + #[serde( + default, + deserialize_with = "proxmox_serde::perl::deserialize_bool", + skip_serializing_if = "Option::is_none" + )] pub passive: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub hello_interval: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub csnp_interval: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub hello_multiplier: Option, + #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")] pub is_ipv4: bool, + #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")] pub is_ipv6: bool, } diff --git a/proxmox-frr/src/ser/ospf.rs b/proxmox-frr/src/ser/ospf.rs index 67e39a45b8de..8b26f42e2e46 100644 --- a/proxmox-frr/src/ser/ospf.rs +++ b/proxmox-frr/src/ser/ospf.rs @@ -1,32 +1,12 @@ use std::fmt::Debug; -use std::fmt::Display; use std::net::Ipv4Addr; +use bon::Builder; +use serde::{Deserialize, Serialize}; use thiserror::Error; use crate::ser::{FrrWord, FrrWordError}; -/// The name of the ospf frr router. -/// -/// We can only have a single ospf router (ignoring multiple invocations of the ospfd daemon) -/// because the router-id needs to be the same between different routers on a single node. -/// We can still have multiple fabrics by separating them using areas. Still, different areas have -/// the same frr router, so the name of the router is just "ospf" in "router ospf". -/// -/// This serializes roughly to: -/// ```text -/// router ospf -/// !... -/// ``` -#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct OspfRouterName; - -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.")] @@ -44,7 +24,7 @@ pub enum AreaParsingError { /// or "0" as an area, which then gets translated to "0.0.0.5" and "0.0.0.0" by FRR. We allow both /// a number or an ip-address. Note that the area "0" (or "0.0.0.0") is a special area - it creates /// a OSPF "backbone" area. -#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] pub struct Area(FrrWord); impl TryFrom for Area { @@ -65,12 +45,6 @@ impl Area { } } -impl Display for Area { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "area {}", self.0) - } -} - /// The OSPF router properties. /// /// Currently the only property of a OSPF router is the router_id. The router_id is used to @@ -84,7 +58,7 @@ impl Display for Area { /// router ospf /// router-id /// ``` -#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] pub struct OspfRouter { pub router_id: Ipv4Addr, } @@ -119,7 +93,8 @@ pub enum OspfInterfaceError { /// ! or /// ip ospf network broadcast /// ``` -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] pub enum NetworkType { Broadcast, NonBroadcast, @@ -132,17 +107,6 @@ pub enum NetworkType { PointToMultipoint, } -impl Display for NetworkType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - NetworkType::Broadcast => write!(f, "broadcast"), - NetworkType::NonBroadcast => write!(f, "non-broadcast"), - NetworkType::PointToPoint => write!(f, "point-to-point"), - NetworkType::PointToMultipoint => write!(f, "point-to-multicast"), - } - } -} - /// The OSPF interface properties. /// /// The interface gets tied to its fabric by the area property and the FRR `ip ospf area ` @@ -156,10 +120,16 @@ impl Display for NetworkType { /// ip ospf passive /// ip ospf network /// ``` -#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, Builder)] pub struct OspfInterface { // Note: an interface can only be a part of a single area(so no vec needed here) pub area: Area, + #[serde( + default, + skip_serializing_if = "Option::is_none", + deserialize_with = "proxmox_serde::perl::deserialize_bool" + )] pub passive: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub network_type: Option, } diff --git a/proxmox-frr/src/ser/route_map.rs b/proxmox-frr/src/ser/route_map.rs index 0918a3cead14..995e21655cd0 100644 --- a/proxmox-frr/src/ser/route_map.rs +++ b/proxmox-frr/src/ser/route_map.rs @@ -1,29 +1,19 @@ -use std::{ - fmt::{self, Display}, - net::IpAddr, -}; +use std::net::IpAddr; use proxmox_network_types::ip_address::Cidr; +use serde::{Deserialize, Serialize}; /// The action for a [`AccessListRule`]. /// /// The default is Permit. Deny can be used to create a NOT match (e.g. match all routes that are /// NOT in 10.10.10.0/24 using `ip access-list TEST deny 10.10.10.0/24`). -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] pub enum AccessAction { Permit, Deny, } -impl fmt::Display for AccessAction { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - AccessAction::Permit => write!(f, "permit"), - AccessAction::Deny => write!(f, "deny"), - } - } -} - /// A single [`AccessList`] rule. /// /// Every rule in a [`AccessList`] is its own command and gets written into a new line (with the @@ -40,29 +30,29 @@ impl fmt::Display for AccessAction { /// ! or /// ipv6 access-list filter permit 2001:db8::/64 /// ``` -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct AccessListRule { pub action: AccessAction, pub network: Cidr, + #[serde(default, skip_serializing_if = "Option::is_none")] pub seq: Option, + #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")] + pub is_ipv6: bool, } /// The name of an [`AccessList`]. -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub struct AccessListName(String); +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct PrefixListName(String); + impl AccessListName { pub fn new(name: String) -> AccessListName { AccessListName(name) } } -impl Display for AccessListName { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) - } -} - /// A FRR access-list. /// /// Holds a vec of rules. Each rule will get its own line, FRR will collect all the rules with the @@ -75,12 +65,29 @@ impl Display for AccessListName { /// ip access-list pve_test permit 12.1.1.0/24 /// ip access-list pve_test deny 8.8.8.8/32 /// ``` -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct AccessList { pub name: AccessListName, pub rules: Vec, } +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct PrefixList { + pub name: PrefixListName, + pub rules: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct PrefixListRule { + pub action: AccessAction, + pub network: Cidr, + pub seq: Option, + pub le: Option, + pub ge: Option, + #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")] + pub is_ipv6: bool, +} + /// A match statement inside a route-map. /// /// A route-map has one or more match statements which decide on which routes the route-map will @@ -98,66 +105,58 @@ pub struct AccessList { /// ! or /// match ipv6 next-hop /// ``` -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "protocol_type")] pub enum RouteMapMatch { + #[serde(rename = "ip")] V4(RouteMapMatchInner), + #[serde(rename = "ipv6")] V6(RouteMapMatchInner), + Vni(u32), } -impl Display for RouteMapMatch { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - RouteMapMatch::V4(route_map_match_v4) => match route_map_match_v4 { - RouteMapMatchInner::IpAddress(access_list_name) => { - write!(f, "match ip address {access_list_name}") - } - RouteMapMatchInner::IpNextHop(next_hop) => { - write!(f, "match ip next-hop {next_hop}") - } - }, - RouteMapMatch::V6(route_map_match_v6) => match route_map_match_v6 { - RouteMapMatchInner::IpAddress(access_list_name) => { - write!(f, "match ipv6 address {access_list_name}") - } - RouteMapMatchInner::IpNextHop(next_hop) => { - write!(f, "match ipv6 next-hop {next_hop}") - } - }, - } - } +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "list_type", content = "list_name", rename_all = "lowercase")] +pub enum AccessListOrPrefixList { + PrefixList(PrefixListName), + AccessList(AccessListName), } /// A route-map match statement generic on the IP-version. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "match_type", content = "value", rename_all = "kebab-case")] pub enum RouteMapMatchInner { - IpAddress(AccessListName), - IpNextHop(String), + Address(AccessListOrPrefixList), + NextHop(String), +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum SetIpNextHopValue { + PeerAddress, + Unchanged, + IpAddr(IpAddr), +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum SetTagValue { + Untagged, + Numeric(u32), } /// Defines the Action a route-map takes when it matches on a route. /// /// If the route matches the [`RouteMapMatch`], then a [`RouteMapSet`] action will be executed. /// We currently only use the IpSrc command which changes the source address of the route. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "set_type", content = "value", rename_all = "kebab-case")] pub enum RouteMapSet { LocalPreference(u32), - IpSrc(IpAddr), + Src(IpAddr), Metric(u32), Community(String), } -impl Display for RouteMapSet { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - RouteMapSet::LocalPreference(pref) => write!(f, "set local-preference {}", pref), - RouteMapSet::IpSrc(addr) => write!(f, "set src {}", addr), - RouteMapSet::Metric(metric) => write!(f, "set metric {}", metric), - RouteMapSet::Community(community) => write!(f, "set community {}", community), - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub struct RouteMapName(String); impl RouteMapName { @@ -166,12 +165,6 @@ impl RouteMapName { } } -impl Display for RouteMapName { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) - } -} - /// A FRR route-map. /// /// In FRR route-maps are used to manipulate routes learned by protocols. We can match on specific @@ -186,48 +179,14 @@ impl Display for RouteMapName { /// set src /// exit /// ``` -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RouteMap { - pub name: RouteMapName, +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct RouteMapEntry { pub seq: u32, pub action: AccessAction, + #[serde(default)] pub matches: Vec, + #[serde(default)] pub sets: Vec, -} - -/// The ProtocolType used in the [`ProtocolRouteMap`]. -/// -/// Specifies to which protocols we can attach route-maps. -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum ProtocolType { - Openfabric, - Ospf, -} - -impl Display for ProtocolType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - ProtocolType::Openfabric => write!(f, "openfabric"), - ProtocolType::Ospf => write!(f, "ospf"), - } - } -} - -/// ProtocolRouteMap statement. -/// -/// This statement attaches the route-map to the protocol, so that all the routes learned through -/// the specified protocol can be matched on and manipulated with the route-map. -/// -/// This serializes to: -/// -/// ```text -/// ip protocol route-map -/// ! or -/// ipv6 protocol route-map -/// ``` -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct ProtocolRouteMap { - pub is_ipv6: bool, - pub protocol: ProtocolType, - pub routemap_name: RouteMapName, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub custom_frr_config: Vec, } diff --git a/proxmox-frr/src/ser/serializer.rs b/proxmox-frr/src/ser/serializer.rs index 3a681e2f0d7a..da7a31b7cbf6 100644 --- a/proxmox-frr/src/ser/serializer.rs +++ b/proxmox-frr/src/ser/serializer.rs @@ -1,203 +1,66 @@ -use std::fmt::{self, Write}; - -use crate::ser::{ - openfabric::{OpenfabricInterface, OpenfabricRouter}, - ospf::{OspfInterface, OspfRouter}, - route_map::{AccessList, AccessListName, ProtocolRouteMap, RouteMap}, - FrrConfig, Interface, InterfaceName, Router, RouterName, -}; - -pub struct FrrConfigBlob<'a> { - buf: &'a mut (dyn Write + 'a), -} - -impl Write for FrrConfigBlob<'_> { - fn write_str(&mut self, s: &str) -> Result<(), fmt::Error> { - self.buf.write_str(s) - } -} - -pub trait FrrSerializer { - fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result; -} - -pub fn to_raw_config(frr_config: &FrrConfig) -> Result, anyhow::Error> { - let mut out = String::new(); - let mut blob = FrrConfigBlob { buf: &mut out }; - frr_config.serialize(&mut blob)?; - - Ok(out.as_str().lines().map(String::from).collect()) -} - -pub fn dump(config: &FrrConfig) -> Result { - let mut out = String::new(); - let mut blob = FrrConfigBlob { buf: &mut out }; - config.serialize(&mut blob)?; - Ok(out) -} - -impl FrrSerializer for FrrConfig { - fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result { - self.router().try_for_each(|router| router.serialize(f))?; - self.interfaces() - .try_for_each(|interface| interface.serialize(f))?; - self.access_lists().try_for_each(|list| list.serialize(f))?; - self.routemaps().try_for_each(|map| map.serialize(f))?; - self.protocol_routemaps() - .try_for_each(|pm| pm.serialize(f))?; - Ok(()) - } -} - -impl FrrSerializer for (&RouterName, &Router) { - fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result { - let router_name = self.0; - let router = self.1; - writeln!(f, "router {router_name}")?; - router.serialize(f)?; - writeln!(f, "exit")?; - writeln!(f, "!")?; - Ok(()) - } -} - -impl FrrSerializer for (&InterfaceName, &Interface) { - fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result { - let interface_name = self.0; - let interface = self.1; - writeln!(f, "interface {interface_name}")?; - interface.serialize(f)?; - writeln!(f, "exit")?; - writeln!(f, "!")?; - Ok(()) - } -} - -impl FrrSerializer for (&AccessListName, &AccessList) { - fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result { - self.1.serialize(f)?; - writeln!(f, "!") - } -} - -impl FrrSerializer for Interface { - fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result { - match self { - Interface::Openfabric(openfabric_interface) => openfabric_interface.serialize(f)?, - Interface::Ospf(ospf_interface) => ospf_interface.serialize(f)?, +use std::{fs, path::PathBuf}; + +use anyhow::Context; +use minijinja::Environment; + +use crate::ser::FrrConfig; + +fn create_env<'a>() -> Environment<'a> { + let mut env = Environment::new(); + + // avoid unnecessary additional newlines + env.set_trim_blocks(true); + env.set_lstrip_blocks(true); + + env.set_loader(move |name| { + let override_path = PathBuf::from(format!("/etc/proxmox-frr/templates/{name}")); + // first read the override template: + match fs::read_to_string(override_path) { + Ok(template_content) => Ok(Some(template_content)), + // if that fails, read the vendored template: + Err(_) => match name { + "fabricd.jinja" => Ok(Some( + include_str!("/usr/share/proxmox-frr/templates/fabricd.jinja").to_owned(), + )), + "ospfd.jinja" => Ok(Some( + include_str!("/usr/share/proxmox-frr/templates/ospfd.jinja").to_owned(), + )), + "interface.jinja" => Ok(Some( + include_str!("/usr/share/proxmox-frr/templates/interface.jinja").to_owned(), + )), + "access_lists.jinja" => Ok(Some( + include_str!("/usr/share/proxmox-frr/templates/access_lists.jinja").to_owned(), + )), + "route_maps.jinja" => Ok(Some( + include_str!("/usr/share/proxmox-frr/templates/route_maps.jinja").to_owned(), + )), + "protocol_routemaps.jinja" => Ok(Some( + include_str!("/usr/share/proxmox-frr/templates/protocol_routemaps.jinja") + .to_owned(), + )), + "frr.conf.jinja" => Ok(Some( + include_str!("/usr/share/proxmox-frr/templates/frr.conf.jinja").to_owned(), + )), + _ => Ok(None), + }, } - Ok(()) - } -} + }); -impl FrrSerializer for OpenfabricInterface { - fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result { - if self.is_ipv6 { - writeln!(f, " ipv6 router {}", self.fabric_id)?; - } - if self.is_ipv4 { - writeln!(f, " ip router {}", self.fabric_id)?; - } - if self.passive == Some(true) { - writeln!(f, " openfabric passive")?; - } - if let Some(interval) = self.hello_interval { - writeln!(f, " openfabric hello-interval {interval}",)?; - } - if let Some(multiplier) = self.hello_multiplier { - writeln!(f, " openfabric hello-multiplier {multiplier}",)?; - } - if let Some(interval) = self.csnp_interval { - writeln!(f, " openfabric csnp-interval {interval}",)?; - } - Ok(()) - } -} - -impl FrrSerializer for OspfInterface { - fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result { - writeln!(f, " ip ospf {}", self.area)?; - if self.passive == Some(true) { - writeln!(f, " ip ospf passive")?; - } - if let Some(network_type) = &self.network_type { - writeln!(f, " ip ospf network {network_type}")?; - } - Ok(()) - } -} - -impl FrrSerializer for Router { - fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result { - match self { - Router::Openfabric(open_fabric_router) => open_fabric_router.serialize(f), - Router::Ospf(ospf_router) => ospf_router.serialize(f), - } - } -} - -impl FrrSerializer for OpenfabricRouter { - fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result { - writeln!(f, " net {}", self.net())?; - Ok(()) - } -} - -impl FrrSerializer for OspfRouter { - fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result { - writeln!(f, " ospf router-id {}", self.router_id())?; - Ok(()) - } -} - -impl FrrSerializer for AccessList { - fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result { - for i in &self.rules { - if i.network.is_ipv6() { - write!(f, "ipv6 ")?; - } - write!(f, "access-list {} ", self.name)?; - if let Some(seq) = i.seq { - write!(f, "seq {seq} ")?; - } - write!(f, "{} ", i.action)?; - writeln!(f, "{}", i.network)?; - } - writeln!(f, "!")?; - Ok(()) - } + env } -impl FrrSerializer for RouteMap { - fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result { - writeln!(f, "route-map {} {} {}", self.name, self.action, self.seq)?; - for i in &self.matches { - writeln!(f, " {}", i)?; - } - for i in &self.sets { - writeln!(f, " {}", i)?; - } - writeln!(f, "exit")?; - writeln!(f, "!") - } -} - -impl FrrSerializer for ProtocolRouteMap { - fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result { - if self.is_ipv6 { - writeln!( - f, - "ipv6 protocol {} route-map {}", - self.protocol, self.routemap_name - )?; - } else { - writeln!( - f, - "ip protocol {} route-map {}", - self.protocol, self.routemap_name - )?; - } - writeln!(f, "!")?; - Ok(()) - } +/// Render the passed [`FrrConfig`] into a single string containing the whole config. +pub fn dump(config: &FrrConfig) -> Result { + create_env() + .get_template("frr.conf.jinja") + .with_context(|| "could not obtain frr template from environment")? + .render(config) + .with_context(|| "could not render frr template") +} + +/// Render the passed [`FrrConfig`] into the literal Frr config. +/// +/// The Frr config is returned as lines stored in a Vec. +pub fn to_raw_config(config: &FrrConfig) -> Result, anyhow::Error> { + Ok(dump(config)?.lines().map(|line| line.to_owned()).collect()) } diff --git a/proxmox-sdn-types/src/net.rs b/proxmox-sdn-types/src/net.rs index 3e523fb12d9b..21822bacab1c 100644 --- a/proxmox-sdn-types/src/net.rs +++ b/proxmox-sdn-types/src/net.rs @@ -139,7 +139,7 @@ impl Default for NetSelector { /// between fabrics on the same node. It contains the [`NetSystemId`] and the [`NetSelector`]. /// e.g.: "1921.6800.1002.00" #[api] -#[derive(Debug, Deserialize, Serialize, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] pub struct Net { afi: NetAFI, area: NetArea, @@ -147,6 +147,8 @@ pub struct Net { selector: NetSelector, } +proxmox_serde::forward_serialize_to_display!(Net); +proxmox_serde::forward_deserialize_to_from_str!(Net); impl UpdaterType for Net { type Updater = Option; diff --git a/proxmox-ve-config/src/common/valid.rs b/proxmox-ve-config/src/common/valid.rs index 1f92ef9bb409..f59ddd0a8806 100644 --- a/proxmox-ve-config/src/common/valid.rs +++ b/proxmox-ve-config/src/common/valid.rs @@ -1,5 +1,7 @@ use std::ops::Deref; +use serde::{Deserialize, Serialize}; + /// A wrapper type for validatable structs. /// /// It can only be constructed by implementing the [`Validatable`] type for a struct. Its contents @@ -8,7 +10,7 @@ use std::ops::Deref; /// /// If you want to edit the content, this struct has to be unwrapped via [`Valid::into_inner`]. #[repr(transparent)] -#[derive(Clone, Default, Debug)] +#[derive(Clone, Default, Debug, Serialize, Deserialize)] pub struct Valid(T); impl Valid { diff --git a/proxmox-ve-config/src/sdn/fabric/frr.rs b/proxmox-ve-config/src/sdn/fabric/frr.rs index 10025b3544b9..ac5e88e905a3 100644 --- a/proxmox-ve-config/src/sdn/fabric/frr.rs +++ b/proxmox-ve-config/src/sdn/fabric/frr.rs @@ -1,12 +1,20 @@ use std::net::{IpAddr, Ipv4Addr}; + use tracing; -use proxmox_frr::ser::{self}; +use proxmox_frr::ser::openfabric::{OpenfabricInterface, OpenfabricRouter, OpenfabricRouterName}; +use proxmox_frr::ser::ospf::{self, OspfInterface, OspfRouter}; +use proxmox_frr::ser::route_map::{ + AccessAction, AccessListName, AccessListOrPrefixList, RouteMapEntry, RouteMapMatch, + RouteMapMatchInner, RouteMapName, RouteMapSet, +}; +use proxmox_frr::ser::{ + self, FrrConfig, FrrProtocol, FrrWord, Interface, InterfaceName, IpProtocolRouteMap, +}; use proxmox_network_types::ip_address::Cidr; use proxmox_sdn_types::net::Net; use crate::common::valid::Valid; - use crate::sdn::fabric::section_config::protocol::{ openfabric::{OpenfabricInterfaceProperties, OpenfabricProperties}, ospf::OspfInterfaceProperties, @@ -17,11 +25,11 @@ use crate::sdn::fabric::{FabricConfig, FabricEntry}; /// Constructs the FRR config from the the passed [`Valid`]. /// /// Iterates over the [`FabricConfig`] and constructs all the FRR routers, interfaces, route-maps, -/// etc. which area all appended to the passed [`FrrConfig`]. +/// etc. pub fn build_fabric( current_node: NodeId, config: Valid, - frr_config: &mut ser::FrrConfig, + frr_config: &mut FrrConfig, ) -> Result<(), anyhow::Error> { let mut routemap_seq = 100; let mut current_router_id: Option = None; @@ -48,7 +56,15 @@ pub fn build_fabric( .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); + + if frr_config + .openfabric + .router + .insert(router_name, router_item) + .is_some() + { + tracing::error!("duplicate OpenFabric router"); + } // Create dummy interface for fabric let (interface, interface_name) = build_openfabric_dummy_interface( @@ -58,6 +74,7 @@ pub fn build_fabric( )?; if frr_config + .openfabric .interfaces .insert(interface_name, interface) .is_some() @@ -79,6 +96,7 @@ pub fn build_fabric( )?; if frr_config + .openfabric .interfaces .insert(interface_name, interface) .is_some() @@ -91,70 +109,85 @@ pub fn build_fabric( let rule = ser::route_map::AccessListRule { action: ser::route_map::AccessAction::Permit, network: Cidr::from(ipv4cidr), + is_ipv6: false, seq: None, }; - let access_list_name = ser::route_map::AccessListName::new(format!( - "pve_openfabric_{}_ips", - fabric_id - )); - frr_config.access_lists.push(ser::route_map::AccessList { - name: access_list_name, - rules: vec![rule], - }); + let access_list_name = + AccessListName::new(format!("pve_openfabric_{}_ips", fabric_id)); + frr_config.access_lists.insert(access_list_name, vec![rule]); } if let Some(ipv6cidr) = fabric.ip6_prefix() { let rule = ser::route_map::AccessListRule { action: ser::route_map::AccessAction::Permit, network: Cidr::from(ipv6cidr), + is_ipv6: true, seq: None, }; - let access_list_name = ser::route_map::AccessListName::new(format!( - "pve_openfabric_{}_ip6s", - fabric_id - )); - frr_config.access_lists.push(ser::route_map::AccessList { - name: access_list_name, - rules: vec![rule], - }); + let access_list_name = + AccessListName::new(format!("pve_openfabric_{}_ip6s", fabric_id)); + frr_config.access_lists.insert(access_list_name, 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 (routemap_name, routemap_rule) = + build_openfabric_routemap(fabric_id, IpAddr::V4(ipv4), routemap_seq); + + if let Some(routemap) = frr_config.routemaps.get_mut(&routemap_name) { + routemap.push(routemap_rule) + } else { + frr_config + .routemaps + .insert(routemap_name.clone(), vec![routemap_rule]); + } - let protocol_routemap = ser::route_map::ProtocolRouteMap { - is_ipv6: false, - protocol: ser::route_map::ProtocolType::Openfabric, - routemap_name: ser::route_map::RouteMapName::new( - "pve_openfabric".to_owned(), - ), - }; + routemap_seq += 10; - frr_config.protocol_routemaps.insert(protocol_routemap); + if let Some(routemap) = frr_config + .protocol_routemaps + .get_mut(&FrrProtocol::Openfabric) + { + routemap.v4 = Some(routemap_name); + } else { + frr_config.protocol_routemaps.insert( + FrrProtocol::Openfabric, + IpProtocolRouteMap { + v4: Some(routemap_name), + v6: None, + }, + ); + } } + 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 (routemap_name, routemap_rule) = + build_openfabric_routemap(fabric_id, IpAddr::V6(ipv6), routemap_seq); + + if let Some(routemap) = frr_config.routemaps.get_mut(&routemap_name) { + routemap.push(routemap_rule) + } else { + frr_config + .routemaps + .insert(routemap_name.clone(), vec![routemap_rule]); + } - let protocol_routemap = ser::route_map::ProtocolRouteMap { - is_ipv6: true, - protocol: ser::route_map::ProtocolType::Openfabric, - routemap_name: ser::route_map::RouteMapName::new( - "pve_openfabric6".to_owned(), - ), - }; + routemap_seq += 10; - frr_config.protocol_routemaps.insert(protocol_routemap); + if let Some(routemap) = frr_config + .protocol_routemaps + .get_mut(&FrrProtocol::Openfabric) + { + routemap.v6 = Some(routemap_name); + } else { + frr_config.protocol_routemaps.insert( + FrrProtocol::Openfabric, + IpProtocolRouteMap { + v4: None, + v6: Some(routemap_name), + }, + ); + } } } FabricEntry::Ospf(ospf_entry) => { @@ -169,14 +202,17 @@ pub fn build_fabric( let frr_word_area = ser::FrrWord::new(fabric.properties().area.to_string())?; let frr_area = ser::ospf::Area::new(frr_word_area)?; - let (router_name, router_item) = build_ospf_router(*router_id)?; - frr_config.router.insert(router_name, router_item); + + if frr_config.ospf.router.is_none() { + frr_config.ospf.router = Some(build_ospf_router(*router_id)?); + } // Add dummy interface let (interface, interface_name) = build_ospf_dummy_interface(fabric_id, frr_area.clone())?; if frr_config + .ospf .interfaces .insert(interface_name, interface) .is_some() @@ -191,11 +227,12 @@ pub fn build_fabric( build_ospf_interface(frr_area.clone(), interface)?; if frr_config + .ospf .interfaces .insert(interface_name, interface) .is_some() { - tracing::warn!("An interface cannot be in multiple openfabric fabrics"); + tracing::warn!("An interface cannot be in multiple ospf fabrics"); } } @@ -207,53 +244,59 @@ pub fn build_fabric( network: Cidr::from( fabric.ip_prefix().expect("fabric must have a ipv4 prefix"), ), + is_ipv6: false, seq: None, }; - frr_config.access_lists.push(ser::route_map::AccessList { - name: access_list_name, - rules: vec![rule], - }); + frr_config.access_lists.insert(access_list_name, vec![rule]); - let routemap = build_ospf_dummy_routemap( + let (routemap_name, routemap_rule) = 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 = ser::route_map::ProtocolRouteMap { - is_ipv6: false, - protocol: ser::route_map::ProtocolType::Ospf, - routemap_name: ser::route_map::RouteMapName::new("pve_ospf".to_owned()), - }; + if let Some(routemap) = frr_config.routemaps.get_mut(&routemap_name) { + routemap.push(routemap_rule) + } else { + frr_config + .routemaps + .insert(routemap_name.clone(), vec![routemap_rule]); + } - frr_config.protocol_routemaps.insert(protocol_routemap); + if let Some(routemap) = frr_config.protocol_routemaps.get_mut(&FrrProtocol::Ospf) { + routemap.v4 = Some(routemap_name); + } else { + frr_config.protocol_routemaps.insert( + FrrProtocol::Ospf, + IpProtocolRouteMap { + v4: Some(routemap_name), + v6: None, + }, + ); + } } } } + Ok(()) } /// Helper that builds a OSPF router with a the router_id. -fn build_ospf_router(router_id: Ipv4Addr) -> Result<(ser::RouterName, ser::Router), anyhow::Error> { - let ospf_router = ser::ospf::OspfRouter { router_id }; - let router_item = ser::Router::Ospf(ospf_router); - let router_name = ser::RouterName::Ospf(ser::ospf::OspfRouterName); - Ok((router_name, router_item)) +fn build_ospf_router(router_id: Ipv4Addr) -> Result { + Ok(ser::ospf::OspfRouter { router_id }) } /// Helper that builds a OpenFabric router from a fabric_id and a [`Net`]. fn build_openfabric_router( fabric_id: &FabricId, net: Net, -) -> Result<(ser::RouterName, ser::Router), anyhow::Error> { - let ofr = ser::openfabric::OpenfabricRouter { net }; - let router_item = ser::Router::Openfabric(ofr); - let frr_word_id = ser::FrrWord::new(fabric_id.to_string())?; - let router_name = ser::RouterName::Openfabric(frr_word_id.into()); +) -> Result<(OpenfabricRouterName, OpenfabricRouter), anyhow::Error> { + let router_item = ser::openfabric::OpenfabricRouter { net }; + let frr_word_id = FrrWord::new(fabric_id.to_string())?; + let router_name = frr_word_id.into(); Ok((router_name, router_item)) } @@ -261,10 +304,10 @@ fn build_openfabric_router( fn build_ospf_interface( area: ser::ospf::Area, interface: &OspfInterfaceProperties, -) -> Result<(ser::Interface, ser::InterfaceName), anyhow::Error> { +) -> Result<(Interface, InterfaceName), anyhow::Error> { let frr_interface = ser::ospf::OspfInterface { area, - // Interfaces are always none-passive + // Interfaces are always non-passive passive: None, network_type: if interface.ip.is_some() { None @@ -273,21 +316,20 @@ fn build_ospf_interface( }, }; - let interface_name = ser::InterfaceName::Ospf(interface.name.as_str().try_into()?); + let interface_name = interface.name.as_str().try_into()?; 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: ser::ospf::Area, -) -> Result<(ser::Interface, ser::InterfaceName), anyhow::Error> { - let frr_interface = ser::ospf::OspfInterface { - area, - passive: Some(true), - network_type: None, - }; - let interface_name = ser::InterfaceName::Openfabric(format!("dummy_{}", fabric_id).try_into()?); + area: ospf::Area, +) -> Result<(Interface, InterfaceName), anyhow::Error> { + let frr_interface = ser::ospf::OspfInterface::builder() + .area(area) + .passive(true) + .build(); + let interface_name = format!("dummy_{}", fabric_id).try_into()?; Ok((frr_interface.into(), interface_name)) } @@ -301,8 +343,8 @@ fn build_openfabric_interface( fabric_config: &OpenfabricProperties, is_ipv4: bool, is_ipv6: bool, -) -> Result<(ser::Interface, ser::InterfaceName), anyhow::Error> { - let frr_word = ser::FrrWord::new(fabric_id.to_string())?; +) -> Result<(Interface, InterfaceName), anyhow::Error> { + let frr_word = FrrWord::new(fabric_id.to_string())?; let mut frr_interface = ser::openfabric::OpenfabricInterface { fabric_id: frr_word.into(), // Every interface is not passive by default @@ -319,7 +361,7 @@ fn build_openfabric_interface( if frr_interface.hello_interval.is_none() { frr_interface.hello_interval = fabric_config.hello_interval; } - let interface_name = ser::InterfaceName::Openfabric(interface.name.as_str().try_into()?); + let interface_name = interface.name.as_str().try_into()?; Ok((frr_interface.into(), interface_name)) } @@ -328,18 +370,15 @@ fn build_openfabric_dummy_interface( fabric_id: &FabricId, is_ipv4: bool, is_ipv6: bool, -) -> Result<(ser::Interface, ser::InterfaceName), anyhow::Error> { - let frr_word = ser::FrrWord::new(fabric_id.to_string())?; - let frr_interface = ser::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 = ser::InterfaceName::Openfabric(format!("dummy_{}", fabric_id).try_into()?); +) -> Result<(Interface, InterfaceName), anyhow::Error> { + let frr_word = FrrWord::new(fabric_id.to_string())?; + let frr_interface = ser::openfabric::OpenfabricInterface::builder() + .fabric_id(frr_word.into()) + .passive(true) + .is_ipv4(is_ipv4) + .is_ipv6(is_ipv6) + .build(); + let interface_name = format!("dummy_{}", fabric_id).try_into()?; Ok((frr_interface.into(), interface_name)) } @@ -348,29 +387,32 @@ fn build_openfabric_routemap( fabric_id: &FabricId, router_ip: IpAddr, seq: u32, -) -> ser::route_map::RouteMap { +) -> (RouteMapName, RouteMapEntry) { let routemap_name = match router_ip { IpAddr::V4(_) => ser::route_map::RouteMapName::new("pve_openfabric".to_owned()), IpAddr::V6(_) => ser::route_map::RouteMapName::new("pve_openfabric6".to_owned()), }; - ser::route_map::RouteMap { - name: routemap_name.clone(), - seq, - action: ser::route_map::AccessAction::Permit, - matches: vec![match router_ip { - IpAddr::V4(_) => { - ser::route_map::RouteMapMatch::V4(ser::route_map::RouteMapMatchInner::IpAddress( - ser::route_map::AccessListName::new(format!("pve_openfabric_{fabric_id}_ips")), - )) - } - IpAddr::V6(_) => { - ser::route_map::RouteMapMatch::V6(ser::route_map::RouteMapMatchInner::IpAddress( - ser::route_map::AccessListName::new(format!("pve_openfabric_{fabric_id}_ip6s")), - )) - } - }], - sets: vec![ser::route_map::RouteMapSet::IpSrc(router_ip)], - } + ( + routemap_name, + RouteMapEntry { + seq, + action: ser::route_map::AccessAction::Permit, + matches: vec![match router_ip { + IpAddr::V4(_) => RouteMapMatch::V4(RouteMapMatchInner::Address( + AccessListOrPrefixList::AccessList(AccessListName::new(format!( + "pve_openfabric_{fabric_id}_ips" + ))), + )), + IpAddr::V6(_) => RouteMapMatch::V6(RouteMapMatchInner::Address( + AccessListOrPrefixList::AccessList(AccessListName::new(format!( + "pve_openfabric_{fabric_id}_ip6s" + ))), + )), + }], + sets: vec![RouteMapSet::Src(router_ip)], + custom_frr_config: Vec::new(), + }, + ) } /// Helper that builds a RouteMap for the OSPF protocol. @@ -378,20 +420,20 @@ fn build_ospf_dummy_routemap( fabric_id: &FabricId, router_ip: Ipv4Addr, seq: u32, -) -> Result { +) -> Result<(RouteMapName, RouteMapEntry), anyhow::Error> { let routemap_name = ser::route_map::RouteMapName::new("pve_ospf".to_owned()); // create route-map - let routemap = ser::route_map::RouteMap { - name: routemap_name.clone(), + let routemap = RouteMapEntry { seq, - action: ser::route_map::AccessAction::Permit, - matches: vec![ser::route_map::RouteMapMatch::V4( - ser::route_map::RouteMapMatchInner::IpAddress(ser::route_map::AccessListName::new( - format!("pve_ospf_{fabric_id}_ips"), - )), - )], - sets: vec![ser::route_map::RouteMapSet::IpSrc(IpAddr::from(router_ip))], + action: AccessAction::Permit, + matches: vec![RouteMapMatch::V4(RouteMapMatchInner::Address( + AccessListOrPrefixList::AccessList(AccessListName::new(format!( + "pve_ospf_{fabric_id}_ips" + ))), + ))], + sets: vec![RouteMapSet::Src(IpAddr::from(router_ip))], + custom_frr_config: Vec::new(), }; - Ok(routemap) + Ok((routemap_name, routemap)) } diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve.snap index 98eb50415e36..052a3f7e501c 100644 --- a/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve.snap +++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve.snap @@ -3,6 +3,7 @@ source: proxmox-ve-config/tests/fabric/main.rs expression: output snapshot_kind: text --- +! router openfabric uwu net 49.0001.1921.6800.2008.00 exit @@ -31,4 +32,3 @@ route-map pve_openfabric permit 100 exit ! ip protocol openfabric route-map pve_openfabric -! diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve1.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve1.snap index 4453ac49377f..f456e819098a 100644 --- a/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve1.snap +++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve1.snap @@ -3,6 +3,7 @@ source: proxmox-ve-config/tests/fabric/main.rs expression: output snapshot_kind: text --- +! router openfabric uwu net 49.0001.1921.6800.2009.00 exit @@ -30,4 +31,3 @@ route-map pve_openfabric permit 100 exit ! ip protocol openfabric route-map pve_openfabric -! diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_dualstack_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_dualstack_pve.snap index 48ac9092045e..ae81da3a4a00 100644 --- a/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_dualstack_pve.snap +++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_dualstack_pve.snap @@ -1,35 +1,35 @@ --- source: proxmox-ve-config/tests/fabric/main.rs expression: output -snapshot_kind: text --- +! router openfabric uwu net 49.0001.1921.6800.2008.00 exit ! interface dummy_uwu - ipv6 router openfabric uwu ip router openfabric uwu + ipv6 router openfabric uwu openfabric passive exit ! interface ens19 - ipv6 router openfabric uwu ip router openfabric uwu + ipv6 router openfabric uwu openfabric hello-interval 4 exit ! interface ens20 - ipv6 router openfabric uwu ip router openfabric uwu + ipv6 router openfabric uwu openfabric hello-interval 4 openfabric hello-multiplier 50 exit ! -access-list pve_openfabric_uwu_ips permit 192.168.2.0/24 -! ipv6 access-list pve_openfabric_uwu_ip6s permit 2001:db8::/64 ! +access-list pve_openfabric_uwu_ips permit 192.168.2.0/24 +! route-map pve_openfabric permit 100 match ip address pve_openfabric_uwu_ips set src 192.168.2.8 @@ -43,4 +43,3 @@ exit ip protocol openfabric route-map pve_openfabric ! ipv6 protocol openfabric route-map pve_openfabric6 -! diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_ipv6_only_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_ipv6_only_pve.snap index d7ab1d7e2a61..21c75e4f5861 100644 --- a/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_ipv6_only_pve.snap +++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_ipv6_only_pve.snap @@ -1,8 +1,8 @@ --- source: proxmox-ve-config/tests/fabric/main.rs expression: output -snapshot_kind: text --- +! router openfabric uwu net 49.0001.0000.0000.000a.00 exit @@ -30,5 +30,5 @@ route-map pve_openfabric6 permit 100 set src a:b::a exit ! -ipv6 protocol openfabric route-map pve_openfabric6 ! +ipv6 protocol openfabric route-map pve_openfabric6 diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_multi_fabric_pve1.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_multi_fabric_pve1.snap index ad6c6db8eb8b..5e0c18eb2493 100644 --- a/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_multi_fabric_pve1.snap +++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_multi_fabric_pve1.snap @@ -3,6 +3,7 @@ source: proxmox-ve-config/tests/fabric/main.rs expression: output snapshot_kind: text --- +! router openfabric test1 net 49.0001.1921.6800.2009.00 exit @@ -46,4 +47,3 @@ route-map pve_openfabric permit 110 exit ! ip protocol openfabric route-map pve_openfabric -! diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve.snap index a303f31f3d1a..ee47866edd67 100644 --- a/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve.snap +++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve.snap @@ -3,6 +3,7 @@ source: proxmox-ve-config/tests/fabric/main.rs expression: output snapshot_kind: text --- +! router ospf ospf router-id 10.10.10.1 exit @@ -29,4 +30,3 @@ route-map pve_ospf permit 100 exit ! ip protocol ospf route-map pve_ospf -! diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve1.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve1.snap index 46c30b22abdf..209f3406757b 100644 --- a/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve1.snap +++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve1.snap @@ -3,6 +3,7 @@ source: proxmox-ve-config/tests/fabric/main.rs expression: output snapshot_kind: text --- +! router ospf ospf router-id 10.10.10.2 exit @@ -25,4 +26,3 @@ route-map pve_ospf permit 100 exit ! ip protocol ospf route-map pve_ospf -! diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_multi_fabric_pve1.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_multi_fabric_pve1.snap index 1d2a7c3c272d..225d60bf7edb 100644 --- a/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_multi_fabric_pve1.snap +++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_multi_fabric_pve1.snap @@ -3,6 +3,7 @@ source: proxmox-ve-config/tests/fabric/main.rs expression: output snapshot_kind: text --- +! router ospf ospf router-id 192.168.1.9 exit @@ -42,4 +43,3 @@ route-map pve_ospf permit 110 exit ! ip protocol ospf route-map pve_ospf -! -- 2.47.3