From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 5B74E1FF13C for ; Thu, 19 Mar 2026 11:32:45 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 97DB0199BC; Thu, 19 Mar 2026 11:32:58 +0100 (CET) Date: Thu, 19 Mar 2026 11:32:39 +0100 From: Wolfgang Bumiller To: Gabriel Goller Subject: Re: [PATCH proxmox-ve-rs v6 05/20] frr: add template serializer and serialize fabrics using templates Message-ID: References: <20260312142732.370403-1-g.goller@proxmox.com> <20260312142732.370403-6-g.goller@proxmox.com> MIME-Version: 1.0 Content-Type: text/plain; charset=us-ascii Content-Disposition: inline In-Reply-To: <20260312142732.370403-6-g.goller@proxmox.com> X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1773916317372 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.981 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 RCVD_IN_VALIDITY_CERTIFIED_BLOCKED 0.408 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_RPBL_BLOCKED 0.819 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_SAFE_BLOCKED 0.903 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. 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: MJFGQ36N3QEYDV6WZTGGQ45GMNA4ILDK X-Message-ID-Hash: MJFGQ36N3QEYDV6WZTGGQ45GMNA4ILDK X-MailFrom: w.bumiller@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 CC: pve-devel@lists.proxmox.com X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: On Thu, Mar 12, 2026 at 03:26:41PM +0100, Gabriel Goller wrote: > Add a new serializer which uses only the builtin (include_str!) templates 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. > > Use the `phf` crate to store them in a const map. ^ This is the only thing I have an issue with in the `proxmox-ve-rs` part of this series, but it's quite a lot, so could have easily missed things. A `LazyLock` or `sortable!()`+`binary_serach` should be fine here IMO. It's a very tiny map. > > Signed-off-by: Gabriel Goller > --- > proxmox-frr/Cargo.toml | 3 + > proxmox-frr/debian/control | 12 + > proxmox-frr/src/ser/mod.rs | 272 ++++++++--------- > proxmox-frr/src/ser/openfabric.rs | 37 +-- > proxmox-frr/src/ser/ospf.rs | 78 +---- > proxmox-frr/src/ser/route_map.rs | 218 +++++--------- > proxmox-frr/src/ser/serializer.rs | 227 ++------------ > proxmox-sdn-types/src/net.rs | 4 +- > proxmox-ve-config/src/common/valid.rs | 4 +- > proxmox-ve-config/src/sdn/fabric/frr.rs | 284 ++++++++++-------- > .../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, 460 insertions(+), 708 deletions(-) > > diff --git a/proxmox-frr/Cargo.toml b/proxmox-frr/Cargo.toml > index 159f8606956d..d69eb63d967b 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" ] } > +phf = { version = "0.11.2", features = ["macros"] } > > 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 bc407807ad4f..427a5fccc59d 100644 > --- a/proxmox-frr/debian/control > +++ b/proxmox-frr/debian/control > @@ -7,8 +7,14 @@ Build-Depends-Arch: cargo:native , > rustc:native (>= 1.82) , > libstd-rust-dev , > librust-anyhow-1+default-dev , > + librust-minijinja-2+default-dev (>= 2.5-~~) , > + librust-minijinja-2+loader-dev (>= 2.5-~~) , > + librust-minijinja-2+multi-template-dev (>= 2.5-~~) , > + librust-phf-0.11+default-dev (>= 0.11.2-~~) , > + librust-phf-0.11+macros-dev (>= 0.11.2-~~) , > librust-proxmox-network-types-1+default-dev (>= 1.0.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 +33,14 @@ Multi-Arch: same > Depends: > ${misc:Depends}, > librust-anyhow-1+default-dev, > + librust-minijinja-2+default-dev (>= 2.5-~~), > + librust-minijinja-2+loader-dev (>= 2.5-~~), > + librust-minijinja-2+multi-template-dev (>= 2.5-~~), > + librust-phf-0.11+default-dev (>= 0.11.2-~~), > + librust-phf-0.11+macros-dev (>= 0.11.2-~~), > librust-proxmox-network-types-1+default-dev (>= 1.0.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..ef56d742857b 100644 > --- a/proxmox-frr/src/ser/mod.rs > +++ b/proxmox-frr/src/ser/mod.rs > @@ -3,104 +3,20 @@ 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 proxmox_network_types::{ > + ip_address::{Ipv4Cidr, Ipv6Cidr}, > + Cidr, > +}; > +use proxmox_serde::forward_deserialize_to_from_str; > +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,9 +29,11 @@ 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)] > pub struct FrrWord(String); > > +forward_deserialize_to_from_str!(FrrWord); > + > impl FrrWord { > pub fn new + Into>(name: T) -> Result { > if name.as_ref().is_empty() { > @@ -144,12 +62,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 +69,7 @@ impl AsRef for FrrWord { > } > > #[derive(Error, Debug)] > -pub enum CommonInterfaceNameError { > +pub enum InterfaceNameError { > #[error("interface name too long")] > TooLong, > } > @@ -166,76 +78,138 @@ 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) > + fn try_from(s: &str) -> Result { > + Self::validate(s).map(Self::from_str_unchecked) > } > } > > -impl TryFrom for CommonInterfaceName { > - type Error = CommonInterfaceNameError; > +impl TryFrom for InterfaceName { > + type Error = InterfaceNameError; > > fn try_from(value: String) -> Result { > - Self::new(value) > + if Self::validate(&value).is_ok() { > + Ok(Self::from_string_unchecked(value)) > + } else { > + Err(InterfaceNameError::TooLong) > + } > } > } > > -impl CommonInterfaceName { > - pub fn new + Into>(s: T) -> Result { > - if s.as_ref().len() <= 15 { > - Ok(Self(s.into())) > +impl InterfaceName { > + fn validate(s: &str) -> Result<&str, InterfaceNameError> { > + if s.len() <= 15 { > + Ok(s) > } else { > - Err(CommonInterfaceNameError::TooLong) > + Err(InterfaceNameError::TooLong) > } > } > -} > + fn from_string_unchecked(s: String) -> InterfaceName { > + Self(s) > + } > > -impl Display for CommonInterfaceName { > - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { > - self.0.fmt(f) > + fn from_str_unchecked(s: &str) -> InterfaceName { > + Self::from_string_unchecked(s.to_string()) > } > } > > -/// 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, > -} > +#[derive(Debug, Clone, Serialize, PartialEq, Eq, Deserialize)] > +pub struct Interface { > + // We can't use `Cidr` because then the template doesn't know if it's IPv6 > + // or IPv4, and we need to prefix the FRR command with either "ipv6 ip" or "ip" > + #[serde(default, skip_serializing_if = "Vec::is_empty")] > + pub addresses_v4: Vec, > + #[serde(default, skip_serializing_if = "Vec::is_empty")] > + pub addresses_v6: Vec, > > -impl FrrConfig { > - pub fn new() -> Self { > - Self::default() > + #[serde(flatten)] > + pub properties: T, > +} > +impl From for Interface { > + fn from(value: openfabric::OpenfabricInterface) -> Self { > + Interface { > + addresses_v4: Vec::new(), > + addresses_v6: Vec::new(), > + properties: value, > + } > } > +} > > - pub fn router(&self) -> impl Iterator + '_ { > - self.router.iter() > +impl From for Interface { > + fn from(value: ospf::OspfInterface) -> Self { > + Interface { > + addresses_v4: Vec::new(), > + addresses_v6: Vec::new(), > + properties: value, > + } > } > +} > > - pub fn interfaces(&self) -> impl Iterator + '_ { > - self.interfaces.iter() > - } > +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] > +#[serde(untagged)] > +pub enum IpOrInterface { > + Ip(IpAddr), > + Interface(InterfaceName), > +} > > - 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 IpRoute { > + #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")] > + is_ipv6: bool, > + prefix: Cidr, > + via: IpOrInterface, > + vrf: Option, > +} > > - pub fn protocol_routemaps(&self) -> impl Iterator + '_ { > - self.protocol_routemaps.iter() > - } > +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] > +#[serde(rename_all = "lowercase")] > +pub enum FrrProtocol { > + Ospf, > + Openfabric, > + Bgp, > +} > + > +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] > +pub struct IpProtocolRouteMap { > + pub v4: Option, > + pub v6: Option, > +} > + > +/// 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. > +#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] > +pub struct FrrConfig { > + #[serde(default)] > + pub openfabric: OpenfabricFrrConfig, > + #[serde(default)] > + pub ospf: OspfFrrConfig, > + #[serde(default)] > + pub protocol_routemaps: BTreeMap, > + #[serde(default)] > + pub routemaps: BTreeMap>, > + #[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..58d55da285b1 100644 > --- a/proxmox-frr/src/ser/openfabric.rs > +++ b/proxmox-frr/src/ser/openfabric.rs > @@ -1,15 +1,16 @@ > use std::fmt::Debug; > -use std::fmt::Display; > > 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 +25,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, > @@ -53,29 +54,29 @@ impl OpenfabricRouter { > /// > /// This struct holds all the OpenFabric interface properties. The most important one here is the > /// fabric_id, which ties the interface to a fabric. When serialized these properties all get > -/// prefixed with a space (" ") as they are inside the interface block. They serialize roughly to: > -/// > -/// ```text > -/// interface ens20 > -/// ip router openfabric > -/// ipv6 router openfabric > -/// openfabric hello-interval > -/// openfabric hello-multiplier > -/// openfabric csnp-interval > -/// openfabric passive > -/// ``` > +/// prefixed with a space (" ") as they are inside the interface block. > /// > /// 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)] > 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..8c9992e901f2 100644 > --- a/proxmox-frr/src/ser/ospf.rs > +++ b/proxmox-frr/src/ser/ospf.rs > @@ -1,32 +1,11 @@ > use std::fmt::Debug; > -use std::fmt::Display; > use std::net::Ipv4Addr; > > +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 +23,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,26 +44,13 @@ 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 > /// differentiate between nodes and every node in the same area must have a different router_id. > /// The router_id must also be the same on the different fabrics on the same node. The OSPFv2 > /// daemon only supports IPv4. > -/// Note that these properties also serialize with a space prefix (" ") as they are inside the OSPF > -/// router block. It serializes roughly to: > -/// > -/// ```text > -/// 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, > } > @@ -112,14 +78,8 @@ pub enum OspfInterfaceError { > /// The most important options here are Broadcast (which is the default) and PointToPoint. > /// When PointToPoint is set, then the interface has to have a /32 address and will be treated as > /// unnumbered. > -/// > -/// This roughly serializes to: > -/// ```text > -/// ip ospf network point-to-point > -/// ! 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,34 +92,20 @@ 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 ` > /// command. > -/// > -/// This serializes to: > -/// > -/// ```text > -/// router ospf > -/// ip ospf area > -/// ip ospf passive > -/// ip ospf network > -/// ``` > -#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] > +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] > 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..8e7f4546ccb7 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 > @@ -32,132 +22,119 @@ impl fmt::Display for AccessAction { > /// between access-lists of the same name and rules. Every [`AccessListRule`] has to have a > /// different seq number. > /// The `ip` or `ipv6` prefix gets decided based on the Cidr address passed. > -/// > -/// This serializes to: > -/// > -/// ```text > -/// ip access-list filter permit 10.0.0.0/8 > -/// ! 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 > /// same name and combine them. > -/// > -/// This serializes to: > -/// > -/// ```text > -/// ip access-list pve_test permit 10.0.0.0/24 > -/// 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, > +} > + > +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] > +pub struct VniMatch { > + pub vni: u32, > +} > + > /// 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 > /// execute its actions. If we match on an IP, there are two different syntaxes: `match ip ...` or > /// `match ipv6 ...`. > -/// > -/// Serializes to: > -/// > -/// ```text > -/// match ip address > -/// ! or > -/// match ip next-hop > -/// ! or > -/// match ipv6 address > -/// ! 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), > + #[serde(rename = "vni")] > + 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,68 +143,19 @@ 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 > /// routes (from specific protocols or subnets) and then change them, by e.g. editing the source > /// address or adding a metric, bgp community, or local preference. > -/// > -/// This serializes to: > -/// > -/// ```text > -/// route-map permit 100 > -/// match ip address > -/// 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..51c46729c8ce 100644 > --- a/proxmox-frr/src/ser/serializer.rs > +++ b/proxmox-frr/src/ser/serializer.rs > @@ -1,203 +1,42 @@ > -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, > +use anyhow::Context; > +use minijinja::Environment; > + > +use crate::ser::FrrConfig; > + > +pub static TEMPLATES: phf::Map<&'static str, &'static str> = phf::phf_map! { > + "fabricd.jinja" => include_str!("/usr/share/proxmox-frr/templates/fabricd.jinja"), > + "ospfd.jinja" => include_str!("/usr/share/proxmox-frr/templates/ospfd.jinja"), > + "interface.jinja" => include_str!("/usr/share/proxmox-frr/templates/interface.jinja"), > + "access_lists.jinja" => include_str!("/usr/share/proxmox-frr/templates/access_lists.jinja"), > + "route_maps.jinja" => include_str!("/usr/share/proxmox-frr/templates/route_maps.jinja"), > + "protocol_routemaps.jinja" => include_str!("/usr/share/proxmox-frr/templates/protocol_routemaps.jinja"), > + "frr.conf.jinja" => include_str!("/usr/share/proxmox-frr/templates/frr.conf.jinja"), > }; > > -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) > - } > -} > +fn create_env<'a>() -> Environment<'a> { > + let mut env = Environment::new(); > > -pub trait FrrSerializer { > - fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result; > -} > + // avoid unnecessary additional newlines > + env.set_trim_blocks(true); > + env.set_lstrip_blocks(true); > > -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)?; > + env.set_loader(move |name| Ok(TEMPLATES.get(name).map(|template| (*template).to_owned()))); > > - Ok(out.as_str().lines().map(String::from).collect()) > + env > } > > +/// Render the passed [`FrrConfig`] into a single string containing the whole config. > 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)?, > - } > - 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(()) > - } > -} > - > -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(()) > - } > + 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..f2b7c7290c08 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,21 @@ fn build_ospf_interface( > }, > }; > > - let interface_name = ser::InterfaceName::Ospf(interface.name.as_str().try_into()?); > + let interface_name = interface.name.as_ref().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> { > + area: ospf::Area, > +) -> Result<(Interface, 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()?); > + let interface_name = format!("dummy_{}", fabric_id).try_into()?; > Ok((frr_interface.into(), interface_name)) > } > > @@ -301,8 +344,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 +362,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 +371,18 @@ 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())?; > +) -> Result<(Interface, InterfaceName), anyhow::Error> { > + let frr_word = 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, > + hello_interval: None, > + csnp_interval: None, > + hello_multiplier: None, > }; > - let interface_name = ser::InterfaceName::Openfabric(format!("dummy_{}", fabric_id).try_into()?); > + let interface_name = format!("dummy_{}", fabric_id).try_into()?; > Ok((frr_interface.into(), interface_name)) > } > > @@ -348,29 +391,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 +424,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 > > > > >